/*
 * Color variables are chosen to clear WCAG 2.1 AA contrast minimums (4.5:1 for
 * normal text, 3:1 for large text / UI component boundaries) against every
 * background they're actually used on in this file, verified with a small
 * relative-luminance script, not eyeballed. See git history for the audit.
 */
:root {
    --bg-page: #1a1a1a;
    --bg-panel: #252525;
    --bg-panel-strong: #333;
    --bg-surface: #2a2a2a;
    --bg-header-strip: #1a1a1a;
    --bg-card: #3c3c3c;
    --bg-input: #222;
    --bg-input-alt: #444;
    --bg-button: #444;
    --bg-button-hover: #555;
    --bg-modal-content: #333;
    --bg-modal-accent: #1a1a2e;
    --bg-modal-inset: #0d0d1a;
    --bg-modal-inset-hover: #1a1a3e;
    --border-color: #707070;
    --border-color-strong: #8c8c8c;
    --text-primary: #e0e0e0;
    --text-secondary: #aaa;
    --text-muted: #a6a6a6;
    --accent: #00ffcc;
    --accent-hover: #00cc88;
    --accent-contrast: #1a1a1a;
    --accent-danger: #ff8080;
    --accent-success: #4dff4d;
    --accent-gold: #ffcc00;
    --accent-purple: #e08aff;
    --accent-warn: #ffaa00;
    --bg-deck-spot: #5c3d2e;
    --border-deck-spot: #8b5a2b;
    --text-deck-spot: #ffffff;
    --bg-graveyard-spot: #2b2b2b;
    --border-graveyard-spot: #1a1a1a;
    --bg-exile-spot: #1a0a2e;
    --border-exile-spot: #4a0080;
    --text-exile-spot: #cc88ff;
    --parallax-glow-1: rgba(255, 196, 60, 0.10);
    --parallax-glow-2: rgba(206, 122, 255, 0.12);
    --parallax-glow-3: rgba(0, 235, 196, 0.10);
    --parallax-glow-4: rgba(72, 132, 255, 0.12);

    /* ── Accessibility knobs ──────────────────────────────────────────────────
       --a11y-text-scale is set on <html> by the Settings modal's Text size control
       (applyTextScale in script.js) and defaults to 1 = the original design size.
       Card boxes are derived from it as well as the body font size, because card text
       is sized in em: scaling the text alone would just overflow a fixed 100x140 box.
       Everything that hard-codes a card dimension reads --card-w/--card-h instead. */
    --a11y-text-scale: 1;
    --card-w: calc(100px * var(--a11y-text-scale));
    --card-h: calc(140px * var(--a11y-text-scale));
    /* Focus ring: deliberately NOT --accent. The accent is also the "eligible target" and
       "highlighted slot" color, and a focus ring that reads as a game-state highlight is
       worse than no ring at all. Gold is used for hover, so keyboard focus gets its own hue. */
    --focus-ring: #7cc4ff;
    --row-bg: rgba(255, 255, 255, 0.05);
}

:root[data-theme="light"] {
    --bg-page: #eef0f2;
    --bg-panel: #ffffff;
    --bg-panel-strong: #e4e4e7;
    --bg-surface: #ffffff;
    --bg-header-strip: #e4e4e7;
    --bg-card: #ffffff;
    --bg-input: #ffffff;
    --bg-input-alt: #eeeeee;
    --bg-button: #e2e2e5;
    --bg-button-hover: #d0d0d4;
    --bg-modal-content: #ffffff;
    --bg-modal-accent: #ffffff;
    --bg-modal-inset: #eef0f2;
    --bg-modal-inset-hover: #dfe3e6;
    --border-color: #828288;
    --border-color-strong: #75757c;
    --text-primary: #1a1a1a;
    --text-secondary: #55555a;
    --text-muted: #636367;
    --accent: #00695c;
    --accent-hover: #004d40;
    --accent-contrast: #ffffff;
    --accent-danger: #a93226;
    --accent-success: #166534;
    --accent-gold: #7a5c00;
    --accent-purple: #6a1b9a;
    --accent-warn: #994b00;
    --bg-deck-spot: #d2a679;
    --border-deck-spot: #8b5a2b;
    --text-deck-spot: #3a2410;
    --bg-graveyard-spot: #d6d6d6;
    --border-graveyard-spot: #9a9a9a;
    --bg-exile-spot: #f0e5ff;
    --border-exile-spot: #9b59b6;
    --text-exile-spot: #5e1789;
    --parallax-glow-1: rgba(214, 150, 0, 0.17);
    --parallax-glow-2: rgba(132, 56, 196, 0.15);
    --parallax-glow-3: rgba(0, 146, 128, 0.15);
    --parallax-glow-4: rgba(40, 96, 205, 0.15);
    --focus-ring: #0b4ea2;
    --row-bg: rgba(0, 0, 0, 0.05);
}

/* ── High-contrast themes ─────────────────────────────────────────────────────
   Two extra themes selectable in Settings (Theme: High Contrast Dark / Light). Every
   foreground below clears 7:1 (WCAG AAA) against the surfaces it's used on, verified with
   the same relative-luminance script as the palettes above; the lowest pair is 7.9:1.
   They are ordinary data-theme values, not a @media (prefers-contrast) block, for the same
   reason data-layout isn't a width query: a user choice has to be able to win. script.js
   does default a first-time visitor whose OS asks for more contrast into one of them.
   The shared [data-theme^="hc-"] block further down strips the translucent fills, glows and
   text shadows that no palette swap can fix. */
:root[data-theme="hc-dark"] {
    --bg-page: #000000;
    --bg-panel: #000000;
    --bg-panel-strong: #141414;
    --bg-surface: #000000;
    --bg-header-strip: #000000;
    --bg-card: #0d0d0d;
    --bg-input: #000000;
    --bg-input-alt: #141414;
    --bg-button: #141414;
    --bg-button-hover: #2e2e2e;
    --bg-modal-content: #000000;
    --bg-modal-accent: #000000;
    --bg-modal-inset: #000000;
    --bg-modal-inset-hover: #1f1f1f;
    --border-color: #ffffff;
    --border-color-strong: #ffffff;
    --text-primary: #ffffff;
    --text-secondary: #f0f0f0;
    --text-muted: #e0e0e0;
    --accent: #00ffff;
    --accent-hover: #66ffff;
    --accent-contrast: #000000;
    --accent-danger: #ff8a80;
    --accent-success: #5eff8f;
    --accent-gold: #ffd54a;
    --accent-purple: #e0a6ff;
    --accent-warn: #ffb84d;
    --bg-deck-spot: #000000;
    --border-deck-spot: #ffd54a;
    --text-deck-spot: #ffd54a;
    --bg-graveyard-spot: #000000;
    --border-graveyard-spot: #ffffff;
    --bg-exile-spot: #000000;
    --border-exile-spot: #e0a6ff;
    --text-exile-spot: #e0a6ff;
    --parallax-glow-1: transparent;
    --parallax-glow-2: transparent;
    --parallax-glow-3: transparent;
    --parallax-glow-4: transparent;
    --focus-ring: #ffd54a;
    --row-bg: #000000;
}

:root[data-theme="hc-light"] {
    --bg-page: #ffffff;
    --bg-panel: #ffffff;
    --bg-panel-strong: #ebebeb;
    --bg-surface: #ffffff;
    --bg-header-strip: #ffffff;
    --bg-card: #ffffff;
    --bg-input: #ffffff;
    --bg-input-alt: #ebebeb;
    --bg-button: #ebebeb;
    --bg-button-hover: #d4d4d4;
    --bg-modal-content: #ffffff;
    --bg-modal-accent: #ffffff;
    --bg-modal-inset: #ffffff;
    --bg-modal-inset-hover: #e0e0e0;
    --border-color: #000000;
    --border-color-strong: #000000;
    --text-primary: #000000;
    --text-secondary: #1a1a1a;
    --text-muted: #262626;
    --accent: #00504a;
    --accent-hover: #003d38;
    --accent-contrast: #ffffff;
    --accent-danger: #a80000;
    --accent-success: #00590f;
    --accent-gold: #5c4200;
    --accent-purple: #5b0f8b;
    --accent-warn: #6b3200;
    --bg-deck-spot: #ffffff;
    --border-deck-spot: #5c4200;
    --text-deck-spot: #5c4200;
    --bg-graveyard-spot: #ffffff;
    --border-graveyard-spot: #000000;
    --bg-exile-spot: #ffffff;
    --border-exile-spot: #5b0f8b;
    --text-exile-spot: #5b0f8b;
    --parallax-glow-1: transparent;
    --parallax-glow-2: transparent;
    --parallax-glow-3: transparent;
    --parallax-glow-4: transparent;
    --focus-ring: #a80000;
    --row-bg: #ffffff;
}

/* Title / splash screen fonts: free-for-commercial-use, embedded (not redistributed) per
   their licenses. Augustus for the game title, Norse for the subtitle. */
@font-face {
    font-family: 'Augustus';
    src: url('fonts/Augustus.ttf') format('truetype');
    font-display: swap;
}
@font-face {
    font-family: 'Norse';
    src: url('fonts/Norse.otf') format('opentype');
    font-display: swap;
}

/* Self-hosted so the game renders with no network at all: the convention machine has no
   usable wifi and the Google Fonts <link> that used to live in index.html was render-blocking.
   See GAMEPAD.md O1.

   Playfair Display used to be self-hosted here and is deliberately GONE. Hugging Face's
   pre-receive hook rejects the push over the font binary ("your push was rejected because it
   contains binary files"), and it scans every commit in the push, so shrinking or deleting the
   file in a later commit doesn't clear it. Playfair only ever styled the bee badge tooltip, the
   announcement banner, and two title-screen rules where it sat *behind* the self-hosted Augustus
   and Norse, so the display stack below replaces it with no visible loss and no binary at all.
   Georgia is present on macOS and Windows; Linux/Raspberry Pi falls through to the generic
   serif. If you ever want it back, subset it to WOFF2 (~50 KB) AND keep the blob out of any
   commit HF will receive. */
@font-face {
    font-family: 'Cinzel Decorative';
    src: url('fonts/CinzelDecorative-Regular.ttf') format('truetype');
    font-weight: 400; font-style: normal; font-display: swap;
}
@font-face {
    font-family: 'Cinzel Decorative';
    src: url('fonts/CinzelDecorative-Bold.ttf') format('truetype');
    font-weight: 700; font-style: normal; font-display: swap;
}
@font-face {
    font-family: 'Cinzel Decorative';
    src: url('fonts/CinzelDecorative-Black.ttf') format('truetype');
    font-weight: 900; font-style: normal; font-display: swap;
}

/* Announcement banner (MULTIPLAYER.md Phase A): an operator-posted plaintext strip pinned to
   the very top of the viewport. Deliberately theme-independent, same reasoning as the title
   screen below: it overlays both the (always-warm) title screen and the dark/light game board,
   so its own opaque, high-contrast colors read correctly regardless of which is underneath. */
#announcement-banner {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    z-index: 3500;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 12px;
    padding: 10px 44px;
    font-family: Georgia, 'Times New Roman', serif;
    font-size: 0.95rem;
    line-height: 1.4;
    text-align: center;
    white-space: pre-wrap; /* lets \n in the message render as a real line break, no HTML needed */
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.35);
}
#announcement-banner.hidden { display: none; }

#announcement-banner[data-level="info"]     { background: #2c4a6e; color: #eaf2fb; }
#announcement-banner[data-level="success"]  { background: #1f5c3a; color: #e6fbec; }
#announcement-banner[data-level="warning"]  { background: #6e5220; color: #fbf1de; }
#announcement-banner[data-level="critical"] { background: #6e2626; color: #fbe2e2; }

#announcement-banner-countdown {
    font-weight: 600;
    opacity: 0.85;
    white-space: nowrap;
}

#announcement-banner-dismiss {
    position: absolute;
    right: 10px;
    top: 50%;
    transform: translateY(-50%);
    background: none;
    border: none;
    color: inherit;
    font-size: 1.5rem;
    line-height: 1;
    cursor: pointer;
    opacity: 0.75;
    padding: 4px 10px;
}
#announcement-banner-dismiss:hover { opacity: 1; }
#announcement-banner-dismiss.hidden { display: none; }

/* Title / splash screen: Vanta.js clouds background shown once before the deck-select
   flow. Deliberately theme-independent (its own warm-sky look), not tied to dark/light. */
#title-screen {
    position: fixed;
    inset: 0;
    z-index: 3000;
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: hidden;
    /* Sunset sky behind the cloud layers. Vanta, when enabled, covers this entirely. */
    background: linear-gradient(180deg, #6b3f18 0%, #a0601f 34%, #c97e2b 58%, #e8a54a 80%, #f6c76d 100%);
}

#title-screen.hidden { display: none; }

/* The CSS sky. This is the splash's REAL background, not a fallback strip shown for a moment:
   Vanta (when it runs at all) simply draws its canvas on top of it. That ordering is deliberate
   -- the old flat #2b1d0e meant that anywhere the WebGL canvas did not appear, the splash had no
   background at all, and #title-screen-heading is near-white (#fff8e7), so the title screen
   rendered as invisible cream text on bare page. That is what a Raspberry Pi 3 showed: a blank
   screen with the app underneath it working perfectly.
   Layered radial gradients give the cloud banks and a linear gradient gives the sunset sky; two
   layers drift in opposite directions at different speeds for parallax. Everything here is
   composited from plain gradients, so it costs no WebGL context, no three.js, and no texture
   memory -- which is the whole point on a 1 GB device. */
#vanta-bg {
    position: absolute;
    inset: 0;
    background:
        radial-gradient(ellipse 34% 15% at 14% 42%, rgba(255, 252, 240, 0.85), transparent 70%),
        radial-gradient(ellipse 28% 12% at 38% 34%, rgba(255, 246, 225, 0.70), transparent 72%),
        radial-gradient(ellipse 40% 16% at 68% 46%, rgba(255, 243, 216, 0.78), transparent 70%),
        radial-gradient(ellipse 30% 13% at 88% 38%, rgba(255, 238, 205, 0.62), transparent 72%),
        radial-gradient(ellipse 50% 20% at 30% 72%, rgba(255, 228, 180, 0.55), transparent 68%),
        radial-gradient(ellipse 44% 18% at 78% 78%, rgba(255, 220, 165, 0.50), transparent 70%);
    background-repeat: repeat-x;
    background-size: 150% 100%, 150% 100%, 150% 100%, 150% 100%, 170% 100%, 170% 100%;
    animation: pantheon-clouds-drift 90s linear infinite;
}

/* A second, slower band behind the first. ::before rather than another element so the markup is
   untouched and nothing else has to know the sky is now two layers. */
#vanta-bg::before {
    content: '';
    position: absolute;
    inset: 0;
    background:
        radial-gradient(ellipse 46% 18% at 58% 58%, rgba(255, 206, 145, 0.42), transparent 68%),
        radial-gradient(ellipse 38% 15% at 10% 52%, rgba(255, 190, 120, 0.36), transparent 70%),
        radial-gradient(ellipse 42% 16% at 90% 64%, rgba(250, 196, 132, 0.34), transparent 70%);
    background-repeat: repeat-x;
    background-size: 190% 100%, 190% 100%, 190% 100%;
    animation: pantheon-clouds-drift 150s linear infinite reverse;
}

@keyframes pantheon-clouds-drift {
    from { background-position: 0 0, 0 0, 0 0, 0 0, 0 0, 0 0; }
    to   { background-position: 150% 0, -150% 0, 150% 0, -150% 0, 170% 0, -170% 0; }
}

/* Reduced motion (which is how the Pi launcher runs -- see Scripts/demo-pi.sh) keeps the sky and
   drops only the movement. A still gradient costs nothing to composite, so the splash still looks
   deliberate on hardware that cannot afford an animation. */
@media (prefers-reduced-motion: reduce) {
    #vanta-bg,
    #vanta-bg::before { animation: none; }
    /* backdrop-filter forces the compositor to read back what is behind the element every
       frame. It is the single most expensive property on the splash, and on weak GPUs it is a
       known way to stall frame production outright -- a Pi 3 showed the splash's panel as a
       blank white rectangle while the rest of the page was fine. The panel already has a
       semi-opaque background of its own, so dropping the blur costs almost nothing visually. */
    #title-screen-content { backdrop-filter: none; }
}

#title-screen-content {
    position: relative;
    z-index: 1;
    text-align: center;
    max-width: 90vw;
    box-sizing: border-box;
    padding: clamp(20px, 5vw, 40px) clamp(20px, 6vw, 60px);
    background: rgba(10, 10, 10, 0.35);
    border-radius: 16px;
    backdrop-filter: blur(2px);
}

#title-screen-heading {
    font-family: 'Augustus', Georgia, 'Times New Roman', serif;
    font-size: clamp(2em, 12vw, 5em);
    margin: 0 0 10px;
    color: #fff8e7;
    letter-spacing: clamp(1px, 1vw, 6px);
    white-space: nowrap;
    text-shadow: 0 2px 20px rgba(0, 0, 0, 0.6), 0 0 40px rgba(255, 200, 100, 0.4);
}

#title-screen-tagline {
    font-family: 'Norse', Georgia, 'Times New Roman', serif;
    font-size: clamp(0.85em, 4vw, 1.5em);
    letter-spacing: clamp(1px, 0.6vw, 3px);
    color: #f0e6d2;
    margin: 0 0 30px;
    text-shadow: 0 1px 6px rgba(0, 0, 0, 0.6);
}

#title-screen-enter-btn {
    background: #ff9919;
    color: #1a1a1a;
    font-weight: bold;
    font-size: clamp(0.9em, 3vw, 1.1em);
    padding: clamp(10px, 3vw, 14px) clamp(20px, 6vw, 36px);
    border: none;
    border-radius: 8px;
    cursor: pointer;
    box-shadow: 0 4px 18px rgba(255, 153, 25, 0.5);
    transition: transform 0.15s, box-shadow 0.15s;
}

#title-screen-enter-btn:hover {
    background: #ffaa33;
    transform: translateY(-2px);
    box-shadow: 0 6px 22px rgba(255, 153, 25, 0.65);
}

/* Splash exit: see GAMEPAD.md G11. The content lifts and fades away first, the clouds
   settle out behind it, and only then does #title-screen get .hidden (display:none, which is
   why this needs its own class rather than transitioning straight to hidden). */
#title-screen-content {
    transition: transform 450ms cubic-bezier(0.4, 0, 0.2, 1), opacity 400ms ease-out;
}
#vanta-bg {
    transition: opacity 700ms ease-in 150ms;
}
#title-screen.title-exit {
    pointer-events: none;                 /* no second press lands mid-transition */
    background: transparent;              /* or the #2b1d0e fallback shows through the fade */
    transition: background 700ms ease-in 150ms;
}
#title-screen.title-exit #title-screen-content {
    transform: translateY(-64px) scale(0.94);
    opacity: 0;
}
#title-screen.title-exit #vanta-bg {
    opacity: 0;
}

@media (prefers-reduced-motion: reduce) {
    /* The global reduced-motion net collapses these to ~0ms anyway; this makes the intent
       local and explicit, since the JS also takes an instant path via animationsEnabled. */
    #title-screen-content,
    #vanta-bg,
    #title-screen.title-exit { transition: none; }
}

body {
    margin: 0;
    padding: 0;
    /* 1rem at scale 1, i.e. the browser default this file was designed against. Card text is
       sized in em off this, so the Text size setting reaches the board as well as the chrome. */
    font-size: calc(1rem * var(--a11y-text-scale));
    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
    background-color: var(--bg-page);
    color: var(--text-primary);
    overflow-y: auto;
}

#game-container {
    display: flex;
    flex-direction: column;
    min-height: 100vh; /* NOT a hard height cap: on a viewport too short to fit the board's
                           fixed-size rows + hand + footer, #main-layout has overflow:hidden,
                           so forcing exactly 100vh would clip the board itself instead of
                           the page just growing past 100vh and scrolling (body allows
                           overflow-y); a page scroll is the lesser evil of the two. */
    width: 100vw;
}

.player-stats {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 5px;
    font-weight: bold;
}

/* The stats sit hard right (what justify-content: space-between used to do) — but as one
   group, so the sidebar's ~208px of usable width can't strand WP on a line of its own. */
.stat-chip-group {
    display: flex;
    align-items: center;
    gap: 5px;
    margin-left: auto;
}

/* The name is the only part that may give ground: it keeps to one line and ellipsises rather
   than wrapping mid-word and taking the chips down with it. (Engine names are "Player 1" /
   "Opponent", which fit at every text scale; the clamp is what keeps a longer one from
   rearranging the panel. #side-panel's min-width:0 below is the other half of that.) */
#opponent-name,
#player-name {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* Tighter than a free-standing chip: three of these plus a name have to share one line. */
.player-stats .stat-chip {
    padding: 2px 6px;
}

/* Small pill used for a single stat value (WP/Hand/Deck/CP/MP/AP...): reused in the
   opponent/player info panels so stats wrap cleanly in the narrow sidebar. */
.stat-chip {
    display: inline-flex;
    align-items: center;
    gap: 3px;
    background: rgba(0, 0, 0, 0.18);
    border-radius: 10px;
    padding: 2px 8px;
    font-size: 0.85em;
    font-weight: normal;
    white-space: nowrap;
    cursor: help;
}

:root[data-theme="light"] .stat-chip {
    background: rgba(0, 0, 0, 0.06);
}

.resource-chip-row {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 6px;
    margin-top: 6px;
}

/* Layout for Playfield: deck/graveyard live inline at the end of each row (see
   .deck-spot/.graveyard-spot below), and everything that isn't the board itself (point
   counters, dice, footer actions) lives in #side-panel to the right,
   so the board stays compact instead of stretching across the whole viewport, and the
   vertical space that frees up lets the hand stay on screen alongside the board. */
#main-layout {
    display: flex;
    align-items: flex-start;
    justify-content: center;
    flex: 1;
    padding: 8px;
    position: relative; /* anchors the full-bleed .field-parallax-layer below */
    overflow: hidden; /* clips the parallax layer's 140%/-20% overscan: without this it
                          inflates the page's scrollable width, showing blank dead space
                          past the right edge when scrolled (the layer's own content is
                          unaffected: cards/tooltips inside #play-field/#side-panel don't
                          reach anywhere near this boundary). */
}

/* Groups the board and side panel so they share a top edge (via align-items:flex-start
   below) and are centered as a single unit in the vertical space #main-layout has free;
   centering #play-field alone would leave #side-panel pinned to the very top of the view. */
#board-row {
    display: flex;
    align-items: flex-start;
    gap: 16px;
    align-self: center;
}

#play-field {
    flex: 0 1 auto;
    display: flex;
    flex-direction: column;
    gap: 8px;
    position: relative;
}

/* Ambient backdrop: three slowly drifting aurora layers that give the board a sense of
   depth and motion. Anchored to #main-layout (not #play-field) so it spans the full width of
   the layout (board and side panel alike) instead of stopping at the board's edges. Pure
   CSS transform animation (no scroll/mouse JS, no layout thrash) so it's cheap on mobile;
   disabled entirely when the user prefers reduced motion. Colors are theme variables so
   light/dark both stay subtle enough to never compete with card/text contrast. Everything
   real sits in front of it.

   The layer element itself is EXACTLY the container's box and clips its own overscan; the
   oversized, animated thing is its ::before. Two reasons that split matters:
     - #main-layout switches to overflow-x:auto at any text scale but 100% (see the
       --a11y-text-scale rules), so an oversized layer element would inflate the page's
       scrollable width and show blank dead space past the right edge.
     - The overscan has to be big enough (-45%) that a blob can travel a visible distance and
       still keep the frame covered. An earlier version drifted 7% with no overscan headroom,
       which is why the backdrop read as a static yellow smear rather than as movement. */
.field-parallax-layer {
    position: absolute;
    inset: 0;
    overflow: hidden;
    z-index: 0;
    pointer-events: none;
    /* Stays behind #play-field/#side-panel purely via DOM order (they're the first children
       of #main-layout). Do NOT give those a z-index to "make sure": a positioned element with
       an explicit z-index creates its own stacking context, which traps a descendant row's
       z-index:1000 .card-tooltip inside it, letting a later row/card paint over an earlier
       row's open tooltip. */
}

.field-parallax-layer::before {
    content: "";
    position: absolute;
    inset: -45%;
    will-change: transform;
    transform-origin: 50% 50%;
}

.field-parallax-1::before {
    background:
        radial-gradient(circle at 22% 24%, var(--parallax-glow-1) 0%, transparent 32%),
        radial-gradient(circle at 78% 72%, var(--parallax-glow-3) 0%, transparent 30%);
    animation: field-parallax-drift-1 34s ease-in-out infinite;
}

.field-parallax-2::before {
    background:
        radial-gradient(circle at 74% 20%, var(--parallax-glow-2) 0%, transparent 30%),
        radial-gradient(circle at 18% 76%, var(--parallax-glow-4) 0%, transparent 32%);
    animation: field-parallax-drift-2 47s ease-in-out infinite;
}

/* A third pair of blobs, orbiting rather than drifting: they sit off-center in the box and the
   box turns, so they sweep a wide circle through the frame. Linear and unending, unlike the two
   eased drifts, so something is always visibly moving even at a turnaround. Blobs, not a
   full-width gradient band: a band wide enough to survive the rotation blankets the whole
   viewport in one tint, which is what made the backdrop read as a flat colour wash. The 190%
   box (inset:-45%) is what lets it rotate without a corner entering the frame: a square needs
   142% of its own side length to cover itself at 45deg. */
.field-parallax-3::before {
    background:
        radial-gradient(circle at 50% 16%, var(--parallax-glow-2) 0%, transparent 24%),
        radial-gradient(circle at 50% 84%, var(--parallax-glow-4) 0%, transparent 22%);
    animation: field-parallax-sweep 86s linear infinite;
}

@keyframes field-parallax-drift-1 {
    0%   { transform: translate3d(-7%, -5%, 0) scale(1.04) rotate(0deg); }
    25%  { transform: translate3d(9%, 4%, 0)   scale(1.16) rotate(3deg); }
    50%  { transform: translate3d(5%, -8%, 0)  scale(1.02) rotate(6deg); }
    75%  { transform: translate3d(-9%, 6%, 0)  scale(1.14) rotate(2deg); }
    100% { transform: translate3d(-7%, -5%, 0) scale(1.04) rotate(0deg); }
}

@keyframes field-parallax-drift-2 {
    0%   { transform: translate3d(8%, 6%, 0)   scale(1.12) rotate(0deg); }
    30%  { transform: translate3d(-6%, -7%, 0) scale(1.02) rotate(-4deg); }
    60%  { transform: translate3d(-9%, 5%, 0)  scale(1.18) rotate(-7deg); }
    100% { transform: translate3d(8%, 6%, 0)   scale(1.12) rotate(0deg); }
}

@keyframes field-parallax-sweep {
    from { transform: rotate(0deg); }
    to   { transform: rotate(360deg); }
}

@media (prefers-reduced-motion: reduce) {
    .field-parallax-layer::before { animation: none !important; }
}

/* A fixed 6-column grid, not a centered flex group (2026-08-26, Mitch's mockup): with flex,
   a 5-slot bench row (6 items counting its pile spot) and a 3-slot active row (4 items) each
   centered to their OWN midpoint, so neither the character columns nor the Deck/Graveyard
   boxes lined up between rows. Every row now shares the SAME 6 explicit column tracks (see
   the [data-spot-id]/.deck-spot/.graveyard-spot rules below), so bench slot 3 sits over active
   slot 2, and Deck/Deck and Graveyard/Graveyard always land in the same column, immediately
   after the last visible slot exactly like the mockup (an EARLIER version of this reserved 2
   extra always-empty columns for Okuninushi's granted-slot overflow, which fixed the alignment
   but opened a permanent gap between slot 5 and the pile spot: see the granted-slot comment
   below for where that overflow goes instead).
   justify-content is START, not center: an active row's grid sits inside #active-rows-stack,
   which is narrower than a bench row's own container by exactly one field-spot's width plus
   one gap (#active-lane puts #field-zone, one var(--card-w) card, before it, with the same
   10px gap used here). Centering each row within its OWN container re-introduces the same
   per-row midpoint drift this rule exists to kill; left-aligning both means active's column 1
   lands exactly under bench's column 2 for free, which is what the column numbers below rely
   on. If #active-lane's gap or #field-zone's width ever changes, this offset needs revisiting. */
.field-row {
    display: grid;
    grid-template-columns: repeat(5, var(--card-w)) var(--card-w);
    justify-content: start;
    align-items: center;
    gap: 10px;
    padding: 8px 10px;
    background: var(--row-bg);
    border-radius: 8px;
    position: relative;
    min-height: var(--card-h);
}

/* An active row only ever uses raw columns 1-5 (see the column assignments below), one fewer
   than bench's 6, since #field-zone (also var(--card-w) + this same 10px gap, physically to
   the row's left, see above) is standing in for what would otherwise be its 6th column. Left
   at the shared 6-column template, that 6th column would still reserve its width even though
   nothing is ever placed there, padding out every active row with one card-width of dead space
   past the Graveyard box. Trimming it here is what makes #active-lane's total natural width
   (#field-zone + this row) come out exactly equal to a bench row's, instead of one column over.
   Scoped to NOT [data-layout="mobile"]: these two ids would otherwise outrank (by specificity,
   ids beat classes/attributes regardless of source order) mobile's own .field-row override a
   few hundred lines down, which needs its auto-fit template intact. */
html:not([data-layout="mobile"]) #opponent-active-row,
html:not([data-layout="mobile"]) #player-active-row {
    grid-template-columns: repeat(4, var(--card-w)) var(--card-w);
}

/* Bench: 5 base slots fill columns 1-5 left-to-right, Deck immediately after at 6 (see below).
   Active: thanks to the left-aligned +1-column offset from #field-zone (see above), the 3 base
   slots at raw columns 1-3 land visually under bench's columns 2-4 (the centered middle three),
   so the common 5-bench/3-active board aligns perfectly, with column 4 left over for the
   exposed-top/Thoth-peek callouts and Graveyard landing one column short of Deck's number (5,
   not 6, see below) to cancel that same +1 offset.
   Okuninushi's granted overflow (bench-5/6, active-3/4, hidden by default) deliberately gets
   NO fixed column here: giving it one, like the first version of this fix did, is what reserved
   the always-empty columns 6-7/4-5 causing the gap this rewrite closes. It's rare enough (one
   card's effect) to just auto-place past the pile spot on the rare turn it's visible, rather
   than reserve two permanently-empty columns in the other 99% of turns for it. */
[data-spot-id="opponent-bench-0"], [data-spot-id="bench-0"] { grid-column: 1; }
[data-spot-id="opponent-bench-1"], [data-spot-id="bench-1"] { grid-column: 2; }
[data-spot-id="opponent-bench-2"], [data-spot-id="bench-2"] { grid-column: 3; }
[data-spot-id="opponent-bench-3"], [data-spot-id="bench-3"] { grid-column: 4; }
[data-spot-id="opponent-bench-4"], [data-spot-id="bench-4"] { grid-column: 5; }
[data-spot-id="opponent-active-0"], [data-spot-id="active-0"] { grid-column: 1; }
[data-spot-id="opponent-active-1"], [data-spot-id="active-1"] { grid-column: 2; }
[data-spot-id="opponent-active-2"], [data-spot-id="active-2"] { grid-column: 3; }

/* Exposed-top / Thoth-peek callouts (see .exposed-top-zone below) share the active row but
   have no data-spot-id of their own: park them both in column 4, the one an active row's own
   slots never reach. The two can in principle both be visible at once (Thoth's passive plus
   another exposed-top effect, on the same side) and would overlap there, accepted as a
   rare-combo tradeoff rather than spending a whole extra column on it. */
#opp-exposed-top, #player-exposed-top, #thoth-peek-zone { grid-column: 4; }

/* Deck sits in the true last column (6). Graveyard is one column short of that (5), not 6:
   it lives in an active row, which (per the offset explained above .field-row) is physically
   shifted one column+gap right of a bench row's own left edge. Column 5 in an active row's
   own frame is exactly cancelled by that shift, landing visually in bench's column 6: the
   same trick used for the character slots above. Using 6 here (matching the number, not the
   visual column) was an earlier bug: it put Graveyard a full column to the right of Deck. */
.deck-spot { grid-column: 6; }
.graveyard-spot { grid-column: 5; }

/* Active lane: the shared Field spot sits to the left, spanning the full height of both
   active rows stacked beside it, instead of a full-width horizontal band between them. */
#active-lane {
    display: flex;
    gap: 10px;
    align-items: stretch;
}

#active-rows-stack {
    display: flex;
    flex-direction: column;
    gap: 8px;
    flex: 1;
}

/* Deck/graveyard spots live at the end of their row instead of in a separate side panel:
   pulls the whole board in tight instead of stretching it across wide monitors. */
.deck-spot, .graveyard-spot {
    flex-shrink: 0;
    font-size: 0.7em;
    line-height: 1.3;
    flex-direction: column;
    gap: 2px;
}

.deck-spot .spot-title,
.graveyard-spot .spot-title { font-weight: bold; }
.graveyard-spot .spot-count { font-size: 1.3em; font-weight: bold; }
.graveyard-spot .spot-sub { font-size: 0.85em; opacity: 0.75; }

/* Mobile-only containers: empty (and hidden) on desktop, where deck/GY stay inline at the
   end of their row and the opponent's stats stay in #side-panel. script.js's applyLayout()
   moves the elements in/out; see the mobile block at the bottom of this file. */
.pile-strip,
#opponent-stats-slot { display: none; }

/* Exposed-top / Thoth-peek callouts: shown inline at the end of the relevant active row,
   only occupying space when JS un-hides them (see .hidden). */
.exposed-top-zone {
    flex-shrink: 0;
    text-align: center;
}

.exposed-top-label {
    font-size: 0.65em;
    color: #ff9966;
    margin-bottom: 3px;
}

.thoth-peek-label {
    color: #ffd700;
}

.zone-label {
    position: absolute;
    left: 10px;
    top: 50%;
    transform: translateY(-50%);
    font-size: 0.7em;
    color: var(--text-muted);
    writing-mode: vertical-rl;
    text-transform: uppercase;
    letter-spacing: 1px;
}

.card-spot {
    width: var(--card-w);
    height: var(--card-h);
    border: 2px dashed var(--border-color);
    border-radius: 8px;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 0.7em;
    text-align: center;
    color: var(--text-muted);
}

.inner-label {
    opacity: 0.5;
}

.drop-zone.drag-over {
    border-color: #00ff00;
    background: rgba(0, 255, 0, 0.1);
}

.field-spot {
    border: 2px solid #ffcc00;
    background: rgba(255, 204, 0, 0.05);
}

.deck-spot {
    background: var(--bg-deck-spot);
    border: 2px solid var(--border-deck-spot);
    color: var(--text-deck-spot);
    font-weight: bold;
    position: relative;
}

.top-deck-card {
    width: 100%;
    height: 100%;
    transform: scale(0.9);
    cursor: pointer;
    transition: transform 0.2s;
}

.top-deck-card:hover {
    transform: scale(1.1);
    z-index: 50;
}

.deck-count-overlay {
    position: absolute;
    bottom: 5px;
    right: 5px;
    background: rgba(0, 0, 0, 0.7);
    color: #fff;
    padding: 2px 5px;
    border-radius: 4px;
    font-size: 0.8em;
    pointer-events: none;
}

.graveyard-spot {
    background: var(--bg-graveyard-spot);
    border: 2px solid var(--border-graveyard-spot);
    color: var(--text-primary);
}

#opponent-resource-bar {
    text-align: center;
    color: var(--accent-danger);
    font-family: monospace;
    font-size: 1.05em;
}

#resource-bar {
    text-align: center;
    color: var(--accent);
    font-family: monospace;
    font-size: 1.05em;
}

#dice-values {
    display: grid;
    grid-template-columns: repeat(3, 30px);
    justify-content: center;
    gap: 8px;
    min-height: 34px;
}

.die {
    background: white;
    color: black;
    width: 30px;
    height: 30px;
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: 4px;
    font-weight: bold;
    font-size: 1.2em;
    border: 2px solid #888;
    cursor: pointer;
    transition: all 0.2s;
}

.die.locked {
    background: #ffcc00;
    border-color: #ff9900;
    transform: scale(1.1);
    box-shadow: 0 0 5px rgba(255, 204, 0, 0.5);
}

/* ── Side panel ───────────────────────────────────────────────────────────────
   Everything that isn't the board itself (point counters, dice,
   footer actions) lives here, beside the board, instead of stacked above/below it:
   that's the vertical room that lets the board and hand share the screen. */
#side-panel {
    flex: 0 0 232px;
    display: flex;
    flex-direction: column;
    gap: 10px;
    /* Beats a flex item's default min-width:auto, which is min-CONTENT: without it an
       over-wide child (a long player name, now that the name doesn't wrap) widens the whole
       sidebar past its fixed basis and takes the width out of the board. */
    min-width: 0;
}

.side-panel-section {
    /* bg-panel (not bg-panel-strong) so buttons (which use bg-button) actually contrast
       against the panel behind them; bg-panel-strong and bg-button render almost identically
       in light mode. */
    background: var(--bg-panel);
    border-radius: 10px;
    padding: 10px 12px;
}

/* Opponent / player identity: a soft corner glow plus a small rune mark beside the name,
   rather than a flat accent bar down the card edge (that read as generic dashboard chrome).
   The glow is a blurred inset box-shadow, not a gradient, so it needs no extra markup and
   clips to the panel's own border-radius for free. */
#opponent-info-panel,
#player-info-panel {
    position: relative;
}

#opponent-info-panel {
    box-shadow: inset 28px 26px 34px -30px var(--accent-danger);
}

#player-info-panel {
    box-shadow: inset 28px 26px 34px -30px var(--accent);
}

/* The rune mark. Bigger than the 0.6em it started at because it is no longer only
   decoration: it spins for whoever currently holds priority, so it has to be legible at a
   glance. Everything is in em, so it tracks --a11y-text-scale like the rest of the row.
   The span is just a centring box and a hover target; ::before is the diamond. */
.priority-gem {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 1.15em;
    height: 1.15em;
    font-size: 0.95em;
    line-height: 1;
    cursor: help;             /* it has a real tooltip to offer, like .stat-chip */
    opacity: 0.55;            /* the idle mark recedes; the priority holder's lights up */
    transition: opacity 0.2s ease;
}

/* A square turned 45 degrees, NOT the "\25C6" glyph this used to be. A glyph's ink sits
   off-centre inside its em box (the box has to hold the font's full ascent and descent), so
   rotating the element spun the diamond about a point above its own middle and it visibly
   orbited instead of turning in place. A box's geometric centre — which is what the default
   transform-origin: 50% 50% uses — is exactly where it looks, so this turns dead centre. */
.priority-gem::before {
    content: "";
    display: block;
    width: 0.64em;
    height: 0.64em;
    background: currentColor;
    transform: rotate(45deg);
    transition: box-shadow 0.2s ease;
}

#opponent-priority-gem { color: var(--accent-danger); }
#player-priority-gem { color: var(--accent); }

/* Priority is carried by THREE channels, not just the spin: an animation is no use to a
   screen reader, to someone with Animations off, or under prefers-reduced-motion. The glow
   and the full-strength colour survive all three, and the tooltip/aria-label say it in
   words (see setPriorityGem). */
.priority-gem.has-priority { opacity: 1; }

.priority-gem.has-priority::before {
    box-shadow: 0 0 6px currentColor;
    animation: priority-gem-spin 2.6s linear infinite;
}

/* In-plane rotation, not rotateY: turning the mark edge-on makes it 0px wide twice a cycle,
   so it blinks out of existence — the opposite of "easier to spot". This keeps the whole
   diamond on screen the whole way round, and the slight swell sells the turn. It starts at
   45deg because that is the resting diamond: the square is only ever seen rotated. */
@keyframes priority-gem-spin {
    0%   { transform: rotate(45deg) scale(1); }
    50%  { transform: rotate(225deg) scale(1.15); }
    100% { transform: rotate(405deg) scale(1); }
}

@media (prefers-reduced-motion: reduce) {
    .priority-gem.has-priority::before { animation: none; }
}

/* The Animations setting is a real setting, not just the media query (the attribute is set
   by applyAnimationsSetting). */
html[data-animations="off"] .priority-gem.has-priority::before { animation: none; }

/* The idle gem's 0.55 would put it under the 7:1 these palettes hold every foreground to, so
   in high contrast both gems stay full strength and the spin/glow does all the work. */
:root[data-theme="hc-dark"] .priority-gem,
:root[data-theme="hc-light"] .priority-gem {
    opacity: 1;
}

/* High-contrast themes stay flat and unembellished, matching their existing no-frills palette. */
:root[data-theme="hc-dark"] #opponent-info-panel,
:root[data-theme="hc-light"] #opponent-info-panel,
:root[data-theme="hc-dark"] #player-info-panel,
:root[data-theme="hc-light"] #player-info-panel {
    box-shadow: none;
}

#dice-panel {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 8px;
}

#roll-controls {
    display: flex;
    flex-direction: column;
    gap: 6px;
    width: 100%;
}

#roll-controls button {
    width: 100%;
}

/* Docked at the end of the hand strip. One control: it changes the ORDER the hand is laid
   out in and never hides a card, so there is nothing to collapse away any more (this
   replaced a three-select sort/filter drawer). */
#hand-controls {
    display: flex;
    flex-direction: column;
    justify-content: center;
    flex: 0 0 auto;
    color: var(--text-secondary);
}

#hand-sort-btn {
    min-width: 116px;
    padding: 8px 10px;
    background: var(--bg-button);
    color: var(--text-secondary);
    border: 1px solid var(--border-color-strong);
    border-radius: 10px;
    cursor: pointer;
    font-family: inherit;
    font-size: 0.85em;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    line-height: 1.5;
}

#hand-sort-btn b {
    display: block;
    color: var(--text-primary);
    font-size: 1.15em;
    letter-spacing: 0;
    text-transform: none;
}

#hand-sort-btn:hover {
    background: var(--bg-button-hover);
    border-color: var(--accent);
}

/* ── Hand motion ─────────────────────────────────────────────────────────────
   renderHand rebuilds every card element, so a card that moved (the sort changed, or a
   neighbour was played) arrives already in its new place with nothing to travel. Both of
   these are driven from script.js, which measures first and then plays the movement back:
   .hand-reflow carries a card from where it used to be, .card-flight carries a drawn card
   out of the deck pile. Same reasoning as .hp-no-tween on the HP bars.

   Both are written .card.foo: .card's own `position: relative` and `transition: transform`
   sit later in this file at the same specificity, and would otherwise win on source order:
   which pins a flying ghost back into the layout flow. */
.card.hand-reflow {
    transition: transform 0.34s cubic-bezier(0.22, 0.61, 0.36, 1);
}

.card.card-flight {
    position: fixed;
    margin: 0;
    pointer-events: none;
    z-index: 970; /* just over #floating-numbers-layer, still under every modal (1000+) */
    box-shadow: 0 10px 24px rgba(0, 0, 0, 0.55);
    transition: transform 0.44s cubic-bezier(0.22, 0.61, 0.36, 1), opacity 0.16s ease-out;
}

#actions {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 8px;
}

#actions button, #actions #phase-next-group, #actions #presence-status {
    width: 100%;
}

/* MULTIPLAYER.md Phase 7 */
#presence-status-text {
    margin: 0 0 6px;
    padding: 4px 8px;
    font-size: 0.85em;
    text-align: center;
    color: var(--accent-warn);
    border: 1px solid var(--accent-warn);
    border-radius: 4px;
}

#claim-victory-btn, #concede-btn {
    color: var(--accent-danger);
    border-color: var(--accent-danger);
}

#player-area {
    background: var(--bg-panel);
    padding: 10px;
    border-top: 2px solid var(--border-color);
    position: relative;
}

/* Thin footer strip (just tall enough for the bee logo) so it no longer overlaps the
   hand controls that sit above it (both were competing for the same bottom-right corner
   of #player-area). */
#page-footer {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    padding: 4px 12px;
    background: var(--bg-panel);
    border-top: 1px solid var(--border-color);
}

#designer-credit {
    color: var(--text-muted);
    text-decoration: none;
    font-size: 0.8rem;
    cursor: pointer;
}

#designer-credit:hover,
#designer-credit:focus-visible {
    color: var(--accent-gold);
    text-decoration: underline;
}

/* Matches the gold/Cinzel Decorative wordmark on bignlizard.com's own hero section. */
#designer-credit-name {
    font-family: 'Cinzel Decorative', 'Cinzel', serif;
    font-weight: 700;
    color: #d8b06a;
}

.bee-badge {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 32px;
    height: 32px;
    color: var(--text-muted);
    text-decoration: none;
}
.bee-badge-logo {
    width: 18px;
    height: 18px;
    transition: transform 0.3s cubic-bezier(0.34,1.56,0.64,1), opacity 0.3s ease;
}
.bee-badge-tip {
    position: absolute;
    right: 0;
    top: 50%;
    transform: translate(4px, -50%);
    white-space: nowrap;
    font-family: Georgia, 'Times New Roman', serif;
    font-size: 0.72rem;
    color: var(--text-secondary);
    opacity: 0;
    pointer-events: none;
    transition: transform 0.3s ease 0.1s, opacity 0.3s ease 0.1s;
}
.bee-badge-tip-name { position: relative; color: var(--accent-gold); font-weight: 600; margin-left: 3px; }
.bee-badge-spark {
    position: absolute;
    font-size: 0.55rem;
    line-height: 1;
    color: #ffcc00;
    opacity: 0;
}
.bee-badge-spark-1 { top: -9px; left: -10px; animation-delay: 0s; }
.bee-badge-spark-2 { top: -11px; right: 6px; animation-delay: 0.45s; }
.bee-badge-spark-3 { bottom: -9px; right: -8px; animation-delay: 0.9s; }
.bee-badge-spark-4 { bottom: -7px; left: 10px; animation-delay: 0.25s; }
@keyframes bee-badge-twinkle {
    0%, 100% { opacity: 0; transform: scale(0.3) rotate(0deg); }
    50% { opacity: 1; transform: scale(1) rotate(90deg); }
}
@media (hover: hover) {
    .bee-badge:hover .bee-badge-logo,
    .bee-badge:focus-visible .bee-badge-logo { transform: scale(0); opacity: 0; }
    .bee-badge:hover .bee-badge-tip,
    .bee-badge:focus-visible .bee-badge-tip { transform: translate(0, -50%); opacity: 1; }
    .bee-badge:hover .bee-badge-spark,
    .bee-badge:focus-visible .bee-badge-spark { animation: bee-badge-twinkle 1.4s ease-in-out infinite; }
}
@media (prefers-reduced-motion: reduce) {
    .bee-badge-logo, .bee-badge-tip { transition: none; }
    .bee-badge-spark { animation: none !important; }
}

#opponent-hand-container {
    background: var(--bg-panel);
    padding: 10px;
    border-bottom: 2px solid var(--border-color);
    min-height: 160px;
    display: flex;
    justify-content: center;
}

#opponent-hand {
    display: flex;
    gap: 10px;
    min-height: 150px;
    padding: 10px;
}

#hand-container {
    min-height: 160px;
    display: flex;
    flex-direction: row;
    align-items: stretch;
    gap: 10px;
    margin-bottom: 10px;
}

#hand {
    display: flex;
    gap: 10px;
    min-height: 150px;
    padding: 10px;
    justify-content: center;
    flex: 1 1 auto;
    min-width: 0;
}

.card {
    width: var(--card-w);
    height: var(--card-h);
    background: var(--bg-card);
    border: 2px solid var(--border-color-strong);
    border-radius: 8px;
    padding: 5px;
    font-size: 0.7em;
    display: flex;
    flex-direction: column;
    cursor: grab;
    user-select: none;
    transition: transform 0.2s;
    position: relative; /* For tooltip positioning */
}

.card:hover {
    transform: translateY(-5px);
    z-index: 100; /* Ensure it stays above other cards */
    border-color: var(--accent-gold);
}

/* MULTIPLAYER.md Phase 4 item 4: a real opponent's hand in a real room, rendered as generic
   face-down backs (renderOpponentHandBacks) rather than the real cards renderHand would show in
   local play. No hover lift or grab cursor: these are placeholders, not draggable/interactive. */
.card.card-back {
    background: repeating-linear-gradient(45deg, var(--bg-panel-strong), var(--bg-panel-strong) 8px, var(--bg-card) 8px, var(--bg-card) 16px);
    cursor: default;
}
.card.card-back:hover { transform: none; border-color: var(--border-color-strong); }

/* Detailed Tooltip */
.card-tooltip {
    display: none;
    position: absolute;
    bottom: 120%;
    left: 50%;
    transform: translateX(-50%);
    width: 220px;
    background: var(--bg-surface);
    border: 2px solid var(--accent-gold);
    border-radius: 10px;
    padding: 12px;
    z-index: 1000;
    box-shadow: 0 10px 30px rgba(0,0,0,0.8);
    pointer-events: none;
    color: var(--text-primary);
    font-size: 1.1em;
    line-height: 1.4;
}

.card:hover .card-tooltip {
    display: block;
}

.builder-row:hover {
    background: rgba(255,255,255,0.04);
}

.card-tooltip.builder-float-tooltip {
    position: fixed;
    bottom: auto;
    left: 0;
    top: 0;
    transform: none;
    width: 230px;
    z-index: 1200;
}

.card-tooltip.tooltip-below {
    bottom: auto;
    top: 120%;
}

.tooltip-header {
    border-bottom: 1px solid var(--border-color);
    padding-bottom: 5px;
    margin-bottom: 4px;
    display: flex;
    justify-content: space-between;
    align-items: center;
}

.tooltip-title {
    font-size: 1.1em;
    font-weight: bold;
    color: var(--accent-gold);
}

.tooltip-costs {
    font-family: monospace;
    font-size: 0.9em;
    display: flex;
    gap: 8px;
}

.cost-cp { color: var(--accent); }
.cost-mp { color: var(--accent-purple); }

.tooltip-subtitle {
    font-size: 0.75em;
    color: var(--text-muted);
    margin-bottom: 8px;
    text-transform: uppercase;
    letter-spacing: 0.5px;
}

.tooltip-desc {
    font-size: 0.85em;
    color: var(--text-secondary);
    margin-bottom: 8px;
    line-height: 1.3;
}

.tooltip-keywords {
    color: var(--accent-gold);
    font-size: 0.8em;
    font-weight: bold;
    margin-bottom: 8px;
    font-style: italic;
}

.tooltip-attacks {
    border-top: 1px solid var(--border-color);
    margin-top: 5px;
    padding-top: 8px;
}

.tooltip-attacks strong {
    font-size: 0.8em;
    text-transform: uppercase;
    color: var(--text-muted);
    display: block;
    margin-bottom: 4px;
}

.attack-item {
    background: rgba(0,0,0,0.2);
    border-radius: 4px;
    padding: 5px;
    margin-bottom: 5px;
}

.attack-header {
    display: flex;
    justify-content: space-between;
    font-size: 0.9em;
    font-weight: bold;
}

.attack-name { color: var(--accent-danger); }
.attack-cost-damage { color: var(--accent-warn); }

.attack-desc {
    font-size: 0.75em;
    color: var(--text-secondary);
    margin-top: 2px;
}

.tooltip-footer {
    border-top: 1px solid var(--border-color);
    padding-top: 5px;
    margin-top: 5px;
    display: flex;
    justify-content: space-between;
    font-size: 0.85em;
    font-weight: bold;
}

.footer-hp { color: var(--accent-success); }
.footer-withdraw { color: var(--text-secondary); }

.card.dragging-hand {
    opacity: 0.5;
    border: 2px dashed var(--accent);
    transform: scale(0.9);
}

.card.pending-magic-source {
    border: 2px solid var(--accent-gold);
    box-shadow: 0 0 10px 3px rgba(255, 204, 0, 0.6);
    animation: pending-pulse 1.2s ease-in-out infinite;
}

@keyframes pending-pulse {
    0%, 100% { box-shadow: 0 0 10px 3px rgba(255, 204, 0, 0.6); }
    50%       { box-shadow: 0 0 18px 6px rgba(255, 204, 0, 0.9); }
}

/* Combat animations: attacker lunges toward the opposing board, target flashes on impact,
   screen rumbles briefly. Durations kept under ~350ms per Mitch's "less than half a second" call. */
@keyframes card-lunge-up {
    0%   { transform: translateY(0); }
    45%  { transform: translateY(-16px) scale(1.04); }
    100% { transform: translateY(0); }
}
@keyframes card-lunge-down {
    0%   { transform: translateY(0); }
    45%  { transform: translateY(16px) scale(1.04); }
    100% { transform: translateY(0); }
}
.card.card-lunge-up   { animation: card-lunge-up 0.28s ease-in; z-index: 20; }
.card.card-lunge-down { animation: card-lunge-down 0.28s ease-in; z-index: 20; }

@keyframes card-hit-flash {
    0%   { filter: brightness(1) saturate(1); transform: translateX(0); box-shadow: none; }
    20%  { filter: brightness(2.4) saturate(1.6); transform: translateX(-4px); box-shadow: 0 0 16px 4px rgba(255, 70, 60, 0.85); }
    40%  { transform: translateX(4px); }
    60%  { transform: translateX(-2px); }
    100% { filter: brightness(1) saturate(1); transform: translateX(0); box-shadow: none; }
}
.card.card-hit { animation: card-hit-flash 0.32s ease-out; z-index: 21; }

/* A modal that refused to close on an outside click (the New Game modal before a game is
   running): a short flinch, so the click reads as "not that way" rather than as a dead page. */
@keyframes modal-nudge {
    0%   { transform: translateX(0); }
    25%  { transform: translateX(-6px); }
    50%  { transform: translateX(5px); }
    75%  { transform: translateX(-3px); }
    100% { transform: translateX(0); }
}
.modal-content.modal-nudge { animation: modal-nudge 0.26s ease-in-out; }

@keyframes screen-shake {
    0%   { transform: translate(0, 0); }
    20%  { transform: translate(-5px, 3px); }
    40%  { transform: translate(4px, -3px); }
    60%  { transform: translate(-3px, 2px); }
    80%  { transform: translate(2px, -2px); }
    100% { transform: translate(0, 0); }
}
#game-container.screen-shake { animation: screen-shake 0.32s ease-in-out; }

/* Card-play animations: a played character/field card pops onto the board; a relic or
   blessing/curse attaching to a character gives it a brief glow. */
@keyframes card-played-pop {
    0%   { transform: scale(0.55); opacity: 0; box-shadow: none; }
    60%  { transform: scale(1.08); opacity: 1; box-shadow: 0 0 20px 6px rgba(0, 255, 204, 0.55); }
    100% { transform: scale(1); opacity: 1; box-shadow: none; }
}
.card.card-played { animation: card-played-pop 0.4s cubic-bezier(0.2, 0.9, 0.3, 1.2); z-index: 15; }

@keyframes card-equipped-glow {
    0%, 100% { box-shadow: none; }
    40%      { box-shadow: 0 0 16px 5px rgba(255, 204, 0, 0.75); }
}
.card.card-equipped { animation: card-equipped-glow 0.4s ease-out; z-index: 15; }

/* Dice-roll animation: each newly-rolled (unlocked) die tumbles in. */
@keyframes dice-roll-in {
    0%   { transform: rotate(0deg) scale(0.4); opacity: 0.3; }
    60%  { transform: rotate(300deg) scale(1.15); opacity: 1; }
    100% { transform: rotate(360deg) scale(1); opacity: 1; }
}
.die.dice-roll-in { animation: dice-roll-in 0.4s ease-out; }

@media (prefers-reduced-motion: reduce) {
    .card.card-lunge-up, .card.card-lunge-down, .card.card-hit,
    .card.card-played, .card.card-equipped, .die.dice-roll-in { animation: none !important; }
    #game-container.screen-shake { animation: none !important; }
    .modal-content.modal-nudge { animation: none !important; }
    .d20-btn, .d20-wrap.rolling .d20-svg { animation: none !important; }
    .d20-wrap, .priority-roll-stage.shifted .d20-wrap-you,
    .priority-roll-stage.shifted .d20-wrap-opponent { transition: none !important; }
}

/* Empty slot preview while a hand context menu is open for a character card */
/* A spot handed out by a card in play rather than part of the base allotment: it vanishes
   when the granting card leaves the field. Gold (.slot-granted) means the occupant merely
   falls back to the bench: Spartan Phalanx's spot. Red (.slot-granted-fatal) means the
   occupant is destroyed outright, which is Okuninushi's own card text. The tint sits on
   the spot, not the card, so it stays visible around whatever is standing there. */
.card-spot.slot-granted,
.card-spot.slot-granted-fatal {
    position: relative;
    border-style: dashed;
}

.card-spot.slot-granted {
    border-color: var(--accent-gold);
    background: rgba(255, 204, 0, 0.10);
    box-shadow: inset 0 0 12px rgba(255, 204, 0, 0.18);
}

.card-spot.slot-granted-fatal {
    border-color: var(--accent-danger);
    background: rgba(255, 128, 128, 0.10);
    box-shadow: inset 0 0 12px rgba(255, 128, 128, 0.20);
}

/* Corner marker, so the spot still reads as granted once a card covers the tint */
.card-spot.slot-granted::after,
.card-spot.slot-granted-fatal::after {
    position: absolute;
    top: 2px;
    right: 3px;
    z-index: 6;
    font-size: 10px;
    line-height: 1;
    font-weight: bold;
    background: rgba(0, 0, 0, 0.65);
    border-radius: 3px;
    padding: 1px 3px;
    pointer-events: none;
}

.card-spot.slot-granted::after {
    content: '✦';
    color: var(--accent-gold);
}

.card-spot.slot-granted-fatal::after {
    content: '✦';
    color: var(--accent-danger);
}

:root[data-theme="light"] .card-spot.slot-granted::after,
:root[data-theme="light"] .card-spot.slot-granted-fatal::after {
    background: rgba(255, 255, 255, 0.85);
}

.card-spot.slot-highlight {
    border-color: var(--accent);
    background: rgba(0, 255, 204, 0.12);
    box-shadow: 0 0 10px 2px rgba(0, 255, 204, 0.5);
}

/* Eligible target preview for spell target-selection (enemy/friendly/friendlyNorse/multi) */
.card.target-highlight {
    outline: 2px solid var(--accent);
    box-shadow: 0 0 10px 3px rgba(0, 255, 204, 0.6);
    cursor: pointer;
}

/* "This will affect all of these" preview (e.g. Rigorous Training's all-ally buff) */
.card.affected-highlight {
    outline: 2px solid var(--accent-gold);
    box-shadow: 0 0 10px 3px rgba(255, 204, 0, 0.5);
}

.card.on-board {
    transform: none;
    cursor: default;
}

.card.enlightened {
    border-color: #ffd700;
    box-shadow: 0 0 15px #ffd700;
    background: linear-gradient(135deg, #1a1a1a 0%, #332b00 100%);
}

.enlightened-tag {
    position: absolute;
    top: -10px;
    left: 50%;
    transform: translateX(-50%);
    background: #ffd700;
    color: #000;
    font-size: 0.6em;
    font-weight: bold;
    padding: 2px 6px;
    border-radius: 10px;
    z-index: 10;
    box-shadow: 0 0 5px #ffd700;
}

.card-header {
    display: flex;
    justify-content: flex-end;
}

.card-header-costs {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    flex-wrap: wrap;
    gap: 4px;
    row-gap: 2px;
}

.mana-cost {
    background: var(--bg-button);
    padding: 2px 4px;
    border-radius: 4px;
    font-weight: bold;
}

.card-name {
    font-weight: bold;
    margin: 5px 0;
    text-align: center;
    border-bottom: 1px solid var(--border-color-strong);
}

.card-type {
    font-style: italic;
    color: var(--text-secondary);
    margin-bottom: 5px;
}

.card-desc {
    flex: 1;
    overflow: hidden;
    color: var(--text-secondary);
    font-size: 0.85em;
}

.card-stats {
    display: flex;
    justify-content: space-between;
    font-weight: bold;
    font-size: 1.1em;
}

.stat-health { color: var(--accent-success); }

/* Card-type mark, bottom-right of the card face (replaced the ⚔AttackPower stat, 2026-09-09).
   margin-left:auto rather than relying on .card-stats' space-between: a Magic/Relic/Field card
   has no HP span, so with space-between alone the icon would drift to the left edge. */
.card-type-icon {
    margin-left: auto;
    display: inline-flex;
    align-items: center;
    /* em, so it tracks the card's own font size and --a11y-text-scale with everything else. */
    width: 1.15em;
    height: 1.15em;
    flex: none;
}
.card-type-icon svg { width: 100%; height: 100%; display: block; }
.card-type-icon.type-character { color: var(--accent); }
.card-type-icon.type-relic     { color: var(--accent-gold); }
.card-type-icon.type-magic     { color: var(--accent-purple); }
.card-type-icon.type-field     { color: var(--accent-warn); }

button {
    background: var(--bg-button);
    color: var(--text-primary);
    border: none;
    padding: 8px 16px;
    border-radius: 4px;
    cursor: pointer;
}

button:hover { background: var(--bg-button-hover); }

/* Phase label sits directly above Next Phase, styled as one joined control so the current
   phase is always visible next to the button that advances it. No scrolling up to check. */
#phase-next-group {
    display: flex;
    flex-direction: column;
}

#phase-display {
    font-weight: bold;
    color: var(--accent-gold);
    font-size: 0.7em;
    letter-spacing: 0.5px;
    text-align: center;
    background: var(--bg-button);
    border: 1px solid var(--border-color);
    border-bottom: none;
    border-radius: 4px 4px 0 0;
    padding: 3px 6px;
    white-space: nowrap;
}

#phase-next-group #next-phase-btn {
    border-radius: 0 0 4px 4px;
}

#ai-pace-btn.active,
#animations-toggle-btn.active {
    background: #2d6a4f;
    color: #fff;
}
#ai-pace-btn.active:hover,
#animations-toggle-btn.active:hover {
    background: #2f6b4d;
    color: #fff;
}

#action-history {
    position: fixed;
    bottom: 51px; /* clears #page-footer (~41px tall) so it sits in the hand section's
                     bottom-left corner instead of overlapping the Big N Lizard credit */
    left: 10px;
    z-index: 500;
    display: flex;
    flex-direction: column-reverse;
    align-items: flex-start;
    gap: 6px;
    max-width: 420px;
}

#status-message {
    background: rgba(0,0,0,0.8);
    color: #fff;
    padding: 5px 10px;
    border-radius: 4px;
    font-size: 0.9em;
    cursor: pointer;
    user-select: none;
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

#status-message:hover { background: rgba(0,0,0,0.92); }

#status-message::before {
    content: "🕓 ";
    opacity: 0.7;
}

#action-history-panel {
    background: rgba(0,0,0,0.92);
    border: 1px solid var(--border-color);
    border-radius: 6px;
    padding: 8px 10px;
    width: 380px;
    max-width: 100%;
    max-height: 260px;
    overflow-y: auto;
    font-size: 0.85em;
    display: flex;
    flex-direction: column-reverse;
    gap: 3px;
}

#action-history-panel.hidden { display: none; }

#action-history-panel .action-entry {
    padding: 3px 2px;
    border-bottom: 1px solid rgba(255,255,255,0.08);
    line-height: 1.3;
    color: #ddd;
}

#action-history-panel .action-entry:last-child { border-bottom: none; }

#action-history-panel .action-entry.turn-marker {
    color: #ffd479;
    font-weight: 600;
    text-align: center;
    border-bottom: none;
    margin: 4px 0 2px;
}

#action-history-panel .action-empty {
    opacity: 0.6;
    font-style: italic;
}

/* Context Menu */
.context-menu {
    position: absolute;
    background: var(--bg-surface);
    border: 1px solid var(--accent);
    border-radius: 4px;
    box-shadow: 0 4px 15px rgba(0,0,0,0.5);
    z-index: 1000;
    min-width: 150px;
    overflow: hidden;
}

.menu-header {
    background: var(--bg-header-strip);
    padding: 8px 12px;
    font-weight: bold;
    font-size: 0.9em;
    border-bottom: 1px solid var(--border-color);
    color: var(--accent);
}

.menu-options {
    display: flex;
    flex-direction: column;
}

.menu-options button {
    background: none;
    border: none;
    color: var(--text-primary);
    padding: 10px 15px;
    text-align: left;
    cursor: pointer;
    font-family: inherit;
    font-size: 0.85em;
    transition: background 0.2s;
}

.menu-options button:hover {
    background: var(--accent);
    color: var(--accent-contrast);
}

.menu-options button:disabled {
    color: var(--border-color-strong);
    cursor: not-allowed;
}

/* Reads as disabled but stays clickable on purpose: the Play button is always shown, and
   tapping it while blocked reports the engine's reason (see addSheetButton in script.js).
   A real :disabled would swallow the tap and leave the player with no explanation. */
.menu-options button.menu-btn-blocked {
    color: var(--text-muted);
    font-style: italic;
    opacity: 0.75;
}

.menu-options button.menu-btn-blocked:hover {
    background: var(--bg-header-strip);
    color: var(--text-muted);
}

.hidden {
    display: none !important;
}

.modal {
    position: fixed;
    top: 0; left: 0;
    width: 100%; height: 100%;
    background: rgba(0,0,0,0.85);
    display: flex;
    align-items: center;
    justify-content: center;
    z-index: 1000;
}

#custom-decks-modal, #deck-builder-modal {
    z-index: 1100;
}

#game-over-modal {
    z-index: 1200;
}

.modal-content {
    background: var(--bg-modal-content);
    color: var(--text-primary);
    padding: 30px;
    border-radius: 12px;
    text-align: center;
    border: 2px solid var(--border-color-strong);
}

/* ── Priority roll d20 (#priority-modal) ──────────────────────────────────────
   Both dice are always mounted so the "move left" is a transform transition,
   never a reflow: the opponent's wrap sits pre-positioned in its slot from the
   start, just invisible (opacity 0) and non-interactive, so revealing it is a
   fade, not a layout jump. `--d20-spin-duration` is set per-roll from script.js
   so the CSS spin can decelerate to a stop exactly when the cycling number in
   `.d20-face` (driven from script.js, not CSS) lands on the real result. */
.priority-roll-content { width: 380px; }

.priority-roll-stage {
    position: relative;
    height: 168px;
    margin: 18px 0 4px;
}

.d20-wrap {
    position: absolute;
    top: 0;
    left: 50%;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 8px;
    width: 120px;
    margin-left: -60px;
    transform: translateX(0);
    opacity: 1;
    transition: transform 0.5s ease, opacity 0.4s ease;
}

.d20-wrap-opponent {
    transform: translateX(78px);
    opacity: 0;
    pointer-events: none;
}

.priority-roll-stage.shifted .d20-wrap-you { transform: translateX(-78px); }
.priority-roll-stage.shifted .d20-wrap-opponent { transform: translateX(78px); opacity: 1; }

.d20-btn {
    position: relative;
    width: 110px;
    height: 110px;
    padding: 0;
    background: none;
    border: none;
    display: flex;
    align-items: center;
    justify-content: center;
    color: var(--accent-gold);
}

/* Only the real, clickable die pulses — it's what tells the player where to click. The
   opponent's die is decorative (a plain div, .d20-btn-static) and never pulses. */
#submit-priority-btn { cursor: pointer; animation: d20-pulse 1.6s ease-in-out infinite; }
#submit-priority-btn:hover { background: none; } /* beats the global button:hover fill */
#submit-priority-btn:disabled { cursor: default; }
.d20-wrap-you.rolling #submit-priority-btn,
.d20-wrap-you.settled #submit-priority-btn { animation: none; }

.d20-btn-static { cursor: default; color: var(--text-secondary); }

.d20-svg {
    width: 100%;
    height: 100%;
    /* The artwork is line art (thin filled facet edges, no solid face fill), so it needs a glow
       rather than a shadow to read clearly against the modal's dark background. */
    filter: drop-shadow(0 0 5px currentColor) drop-shadow(0 1px 2px rgba(0, 0, 0, 0.5));
}

.d20-wrap.rolling .d20-svg {
    animation: d20-spin var(--d20-spin-duration, 1100ms) cubic-bezier(0.15, 0.65, 0.35, 1) forwards;
}

.d20-face {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 2em;
    font-weight: bold;
    color: var(--text-primary);
    text-shadow: 0 1px 3px rgba(0, 0, 0, 0.6);
    pointer-events: none;
}

.d20-label {
    font-size: 0.9em;
    color: var(--text-secondary);
}

.priority-roll-result {
    min-height: 1.4em;
    font-weight: bold;
    color: var(--text-primary);
}

#priority-roll-continue-btn {
    margin-top: 10px;
    background: var(--accent);
    color: var(--accent-contrast);
    font-weight: bold;
    padding: 8px 24px;
}

#priority-roll-continue-btn:hover { background: var(--accent-hover); }

@keyframes d20-pulse {
    0%, 100% { transform: scale(1); }
    50% { transform: scale(1.08); }
}

@keyframes d20-spin {
    from { transform: rotate(0deg); }
    to { transform: rotate(1080deg); }
}

.settings-modal-content {
    width: 320px;
    text-align: left;
}

.settings-modal-content h3 {
    text-align: center;
    margin-top: 0;
}

.settings-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    padding: 8px 0;
    border-bottom: 1px solid var(--border-color);
}

.settings-row:last-of-type {
    border-bottom: none;
}

.settings-label {
    color: var(--text-secondary);
}

#priority-input {
    background: var(--bg-input);
    color: var(--text-primary);
    border: 1px solid var(--border-color-strong);
    padding: 10px;
    width: 60px;
    font-size: 1.5em;
    margin: 20px;
}

.deck-options {
    display: flex;
    flex-direction: column;
    gap: 15px;
    margin: 20px 0;
    text-align: left;
}

.deck-select-group {
    display: flex;
    flex-direction: column;
    gap: 5px;
}

.deck-select-group select,
.deck-select-group input[type="text"] {
    padding: 8px;
    background: var(--bg-input-alt);
    color: var(--text-primary);
    border: 1px solid var(--border-color-strong);
    border-radius: 4px;
}

.modal-actions {
    margin-top: 20px;
}

/* ── Feedback modal ───────────────────────────────────────────────────────
   Everything here reads the palette variables rather than literal colours, so the
   four themes (including both high-contrast ones) style it without a second block.
   Sizes are in em so --a11y-text-scale reaches the form the same way it reaches
   the rest of the modal surfaces. */
.feedback-modal-content {
    width: min(480px, 92vw);
    text-align: left;
    /* The form is taller than a laptop viewport once "What gets sent with this" is expanded,
       and a .modal centres its content rather than scrolling it: without this the heading
       and the Send button get clipped off both ends instead. */
    max-height: 88vh;
    overflow-y: auto;
}

.feedback-modal-content h3 {
    text-align: center;
    margin-top: 0;
}

.feedback-intro {
    color: var(--text-secondary);
    font-size: 0.9em;
    line-height: 1.45;
    margin: 0 0 16px;
}

.feedback-field {
    display: flex;
    flex-direction: column;
    gap: 5px;
    margin-bottom: 14px;
}

.feedback-field label {
    color: var(--text-secondary);
    font-size: 0.9em;
}

.feedback-field select,
.feedback-field input[type="text"],
.feedback-field textarea {
    padding: 8px;
    background: var(--bg-input-alt);
    color: var(--text-primary);
    border: 1px solid var(--border-color-strong);
    border-radius: 4px;
    font-family: inherit;
    font-size: 1em;
}

.feedback-field textarea {
    resize: vertical;
    min-height: 5em;
    line-height: 1.45;
}

/* aria-hidden in the markup: the textarea's own maxlength is what actually enforces
   this, and a counter ticking on every keystroke in a live region would be read out
   over the typing. */
.feedback-counter {
    align-self: flex-end;
    color: var(--text-muted);
    font-size: 0.78em;
}

.feedback-context {
    margin-bottom: 12px;
    font-size: 0.85em;
    color: var(--text-secondary);
}

.feedback-context summary {
    cursor: pointer;
    color: var(--accent);
}

.feedback-context ul {
    margin: 8px 0 0;
    padding-left: 18px;
    line-height: 1.5;
}

.feedback-context li {
    color: var(--text-muted);
}

/* Empty until there's something to say, so it takes no vertical space on open and
   the Send button doesn't shift down when a message appears. */
.feedback-status {
    margin: 0 0 4px;
    min-height: 1.2em;
    font-size: 0.9em;
    line-height: 1.35;
}

.feedback-status.is-error { color: var(--accent-danger); }
.feedback-status.is-success { color: var(--accent-success); }
.feedback-status.is-pending { color: var(--text-secondary); }

/* ── Deck / Play modal groups ─────────────────────────────────────────────
   Local Game / Play Online / Custom Decks+How to Play, visually separated so
   the online path is visible up front instead of hiding behind a sidebar
   button the player has to dismiss this modal to reach. */
#deck-modal-content {
    width: min(420px, 92vw);
    max-height: 88vh;
    overflow-y: auto;
}

.modal-group {
    text-align: left;
    padding: 18px 0;
    border-top: 1px solid var(--border-color);
}

.modal-group:first-of-type {
    border-top: none;
    padding-top: 0;
}

.modal-group-title {
    margin: 0 0 8px;
    font-size: 0.78em;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--text-secondary);
}

.modal-group-hint {
    color: var(--text-secondary);
    font-size: 13px;
    margin: 0 0 12px;
}

.modal-group-tertiary {
    padding-top: 14px;
}

.modal-actions-row {
    display: flex;
    gap: 10px;
    justify-content: center;
}

.modal-actions-row button {
    flex: 1;
}

.modal-inline-row {
    display: flex;
    gap: 8px;
}

.modal-inline-row input {
    flex: 1;
    min-width: 0;
}

#create-room-btn {
    width: 100%;
    background: transparent;
    color: var(--accent);
    border: 1px solid var(--accent);
}

#create-room-btn:hover {
    background: var(--accent);
    color: var(--accent-contrast);
}

/* ── How To Play ──────────────────────────────────────────────────────────
   Opens from the deck-select modal as well as the sidebar, so it has to sit
   above #deck-modal's stacking context (see .modal / #custom-decks-modal). */
#howtoplay-modal {
    z-index: 1150;
}

.howto-modal-content {
    width: min(780px, 94vw);
    max-height: 88vh;
    display: flex;
    flex-direction: column;
    text-align: left;
    padding: 24px 24px 16px;
}

.howto-modal-content > h3 {
    margin: 0 0 14px;
    text-align: center;
    color: var(--accent);
}

.howto-nav {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    justify-content: center;
    padding-bottom: 12px;
    border-bottom: 1px solid var(--border-color);
}

.howto-nav button {
    background: var(--bg-modal-inset);
    color: var(--text-secondary);
    border: 1px solid var(--border-color);
    border-radius: 999px;
    padding: 4px 12px;
    font-size: 0.78em;
    cursor: pointer;
}

.howto-nav button:hover,
.howto-nav button:focus-visible {
    background: var(--bg-modal-inset-hover);
    color: var(--accent);
    border-color: var(--accent);
}

.howto-body {
    flex: 1;
    overflow-y: auto;
    padding: 16px 6px 0 0;
    font-size: 0.9em;
    line-height: 1.55;
}

.howto-body section + section {
    margin-top: 28px;
    padding-top: 20px;
    border-top: 1px solid var(--border-color);
}

.howto-body h4 {
    margin: 0 0 10px;
    font-size: 1.15em;
    color: var(--accent);
    letter-spacing: 0.02em;
}

.howto-body h5 {
    margin: 18px 0 6px;
    font-size: 0.95em;
    color: var(--text-primary);
    text-transform: uppercase;
    letter-spacing: 0.06em;
}

.howto-body p {
    margin: 0 0 10px;
    color: var(--text-secondary);
}

.howto-body ul,
.howto-body ol {
    margin: 0 0 10px;
    padding-left: 22px;
    color: var(--text-secondary);
}

.howto-body li {
    margin-bottom: 5px;
}

.howto-body b {
    color: var(--text-primary);
}

/* A rule worth not missing: the win condition, the go/turn distinction. */
.howto-body .howto-callout {
    background: var(--bg-modal-inset);
    border-left: 3px solid var(--accent);
    border-radius: 0 4px 4px 0;
    padding: 10px 14px;
    margin: 12px 0;
}

/* One phase / one card type, boxed so the six phases don't read as one wall. */
.howto-body .howto-phase {
    background: var(--bg-modal-inset);
    border: 1px solid var(--border-color);
    border-radius: 6px;
    padding: 12px 16px;
    margin: 12px 0;
}

.howto-body .howto-phase h5:first-child {
    margin-top: 0;
}

.howto-body .howto-phase > *:last-child {
    margin-bottom: 0;
}

/* Reproduced card text, set apart from the prose explaining it. */
.howto-body .howto-example {
    background: var(--bg-modal-accent);
    border: 1px dashed var(--border-color-strong);
    border-radius: 6px;
    padding: 10px 14px;
    font-size: 0.95em;
}

.howto-body .howto-note {
    font-size: 0.92em;
    font-style: italic;
    color: var(--text-muted);
}

/* Tabs at the top of a modal (e.g. Graveyard / Sealed Away) */
.modal-tabs {
    display: flex;
    justify-content: center;
    gap: 8px;
    margin-bottom: 10px;
    border-bottom: 1px solid var(--border-color);
}

.modal-tab-btn {
    background: none;
    border: none;
    color: var(--text-secondary);
    padding: 8px 18px;
    font-size: 0.95em;
    cursor: pointer;
    border-bottom: 2px solid transparent;
}

.modal-tab-btn:hover {
    color: var(--text-primary);
}

.modal-tab-btn.active {
    color: var(--accent);
    border-bottom-color: var(--accent);
    font-weight: bold;
}

#confirm-decks-btn {
    background: var(--accent);
    color: var(--accent-contrast);
    font-weight: bold;
    padding: 10px 25px;
    font-size: 1.1em;
}

#confirm-decks-btn:hover {
    background: var(--accent-hover);
}

.game-over-content {
    width: 360px;
}

#game-over-title {
    margin: 0 0 4px;
    font-size: 1.8em;
}

#game-over-title.victory { color: var(--accent-success); }
#game-over-title.defeat { color: var(--accent-danger); }

#game-over-subtitle {
    color: var(--text-secondary);
    margin: 0 0 18px;
}

#game-over-stats {
    display: flex;
    flex-direction: column;
    gap: 8px;
}

.game-over-stat-row {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 8px 14px;
    border-radius: 6px;
    background: var(--bg-surface);
    border: 1px solid var(--border-color);
}

.game-over-stat-row.winner-row {
    border-color: var(--accent-gold);
    background: rgba(255, 204, 0, 0.1);
}

.game-over-player-name { font-weight: bold; }
.game-over-wp { color: var(--accent-gold); font-weight: bold; }

#play-again-btn {
    background: var(--accent);
    color: var(--accent-contrast);
    font-weight: bold;
    padding: 10px 25px;
    font-size: 1.05em;
}

#play-again-btn:hover {
    background: var(--accent-hover);
}

.equipped-relic {
    position: absolute;
    top: 15px;
    left: 15px;
    z-index: -1;
    transform: scale(0.95);
    opacity: 0.9;
}

/* Custom Deck Styles */
.custom-deck-item {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 10px;
    background: var(--bg-input-alt);
    border-radius: 4px;
    margin-bottom: 8px;
}

.custom-deck-item button {
    background: #ff4d4d;
    color: #1a1a1a;
    font-size: 0.8em;
}

.builder-card-row {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 8px;
    border-bottom: 1px solid var(--border-color);
}

.builder-card-row:hover {
    background: rgba(255,255,255,0.05);
}

.spinner-controls {
    display: flex;
    align-items: center;
    gap: 10px;
}

.spinner-btn {
    width: 25px;
    height: 25px;
    padding: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--bg-button-hover);
    border-radius: 50%;
}

.spinner-btn:disabled {
    opacity: 0.3;
    cursor: not-allowed;
}

/* ── New UI elements ─────────────────────────────────────────── */

/* Status badges on cards */
.status-badge {
    display: inline-block;
    padding: 1px 4px;
    border-radius: 3px;
    font-size: 0.75em;
    color: white;
    margin: 1px;
    cursor: default;
}

/* Preservation countdown on a field card in play. Same chip shape as .status-badge, but it
   carries a NUMBER that changes every round rather than a fixed marker, so it gets its own
   colour: gold while the protection holds, muted once it has run out. */
.preservation-badge {
    display: inline-block;
    padding: 1px 4px;
    border-radius: 3px;
    font-size: 0.75em;
    margin: 1px;
    cursor: default;
    background: #4a3c12;
    color: var(--accent-gold, #f1c40f);
    border: 1px solid rgba(241, 196, 15, 0.4);
}

.preservation-badge.expired {
    background: var(--bg-panel-strong);
    color: var(--text-secondary);
    border-color: var(--border-color);
}

/* An active/bench spot the row has outgrown (Standoff shrinking the row under a character,
   a seal pushing one outward). It stays visible so its occupant isn't dropped off the board,
   but it must not read as a spot anything can still be played into. */
.card-spot.slot-overflow {
    border-style: dashed;
    opacity: 0.72;
}

/* HP bar
   Three layers deep so damage reads as motion rather than a jump cut: .hp-ghost holds the
   PREVIOUS width for a beat and then chips away behind the fill, .hp-fill tweens down to the
   new one. Both are driven by tweenHPBar() in script.js, which has to re-seed the old width by
   hand: createCardElement rebuilds the card on every render, so the bar always arrives already
   at its new width with nothing for a plain CSS transition to travel. */
.hp-bar {
    position: relative;
    height: 4px;
    background: var(--bg-panel-strong);
    border-radius: 2px;
    margin: 2px 0;
    overflow: hidden;
}
.hp-ghost {
    position: absolute;
    top: 0;
    left: 0;
    bottom: 0;
    width: 0;
    background: #ffd0c4;
    border-radius: 2px;
    transition: width 0.5s ease-in 0.28s;
}
.hp-fill {
    position: relative; /* paints over the absolutely-positioned ghost behind it */
    height: 100%;
    background: linear-gradient(to right, #e74c3c, #2ecc71);
    transition: width 0.42s cubic-bezier(0.22, 0.61, 0.36, 1);
}
/* Applied for one frame while the old width is re-seeded, so that snap is instant. */
.hp-fill.hp-no-tween, .hp-ghost.hp-no-tween { transition: none; }

/* Floating combat numbers: a fixed-position layer over everything, so a number can never be
   clipped by a card's own overflow or buried under a neighbouring slot's stacking context. */
#floating-numbers-layer {
    position: fixed;
    inset: 0;
    pointer-events: none;
    overflow: hidden;
    z-index: 960; /* over the board and the log panel, under every modal (1000+) */
}
.floating-number {
    position: absolute;
    transform: translate(-50%, -50%);
    font-weight: 800;
    font-size: 1.6rem;
    line-height: 1;
    opacity: 0;
    white-space: nowrap;
    /* A number lands on top of the target's own body text, so it needs a hard edge rather than
       a soft glow: a dark ring in every direction, then a shadow to lift it off the card. */
    -webkit-text-stroke: 3px rgba(0, 0, 0, 0.9);
    paint-order: stroke fill;
    text-shadow: 0 2px 4px #000, 0 0 10px rgba(0, 0, 0, 0.9);
    animation: floating-number-rise 0.95s ease-out forwards;
}
.floating-number.fx-damage  { color: #ff5f4d; }
.floating-number.fx-ko      { color: #ff3b30; font-size: 1.15rem; letter-spacing: 0.08em; }
.floating-number.fx-heal    { color: #4ee08a; }
.floating-number.fx-wp-gain { color: #f1c40f; font-size: 1.2rem; }
.floating-number.fx-wp-loss { color: #ff8b6b; font-size: 1.2rem; }

@keyframes floating-number-rise {
    0%   { opacity: 0; transform: translate(-50%, -40%) scale(0.55); }
    16%  { opacity: 1; transform: translate(-50%, -75%) scale(1.28); }
    32%  { opacity: 1; transform: translate(-50%, -92%) scale(1); }
    70%  { opacity: 1; transform: translate(-50%, -145%) scale(1); }
    100% { opacity: 0; transform: translate(-50%, -195%) scale(0.9); }
}

/* Reduced motion keeps the number (it's information, not decoration) but drops the travel. */
@media (prefers-reduced-motion: reduce) {
    .floating-number { animation: floating-number-hold 0.95s ease-out forwards; }
    .hp-fill, .hp-ghost { transition: none; }
}
@keyframes floating-number-hold {
    0%   { opacity: 0; }
    12%  { opacity: 1; }
    70%  { opacity: 1; }
    100% { opacity: 0; }
}

/* Attack usage dots */
.attack-dots { display: flex; gap: 3px; align-items: center; }
.attack-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--accent); }
.attack-dot.used { background: var(--border-color-strong); }

.equipped-relics-strip {
    display: flex;
    flex-wrap: wrap;
    gap: 3px;
    margin-top: 4px;
}
.equipped-relic-badge {
    font-size: 0.65em;
    background: #3a2800;
    color: #f0a030;
    border: 1px solid #f0a030;
    border-radius: 3px;
    padding: 1px 4px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    max-width: 100%;
}

/* Resource token badges */
.token-badge {
    font-size: 0.8em;
    color: var(--accent-gold);
    font-weight: bold;
}

/* Card class indicator */
.card-class {
    font-size: 0.7em;
    color: var(--text-muted);
    padding: 1px 3px;
    border: 1px solid var(--border-color-strong);
    border-radius: 3px;
}

/* Board-position tag ("A2"/"B3"): appears only when a player controls more than one copy
   of the same character in the same row, so attack targeting (and any other target list)
   can point at an exact copy instead of a name that matches two cards on the board. Sits
   with the other header chips (mana-cost/card-class) so it scales identically at every
   card size, mobile included, and needs no hover to read: see createCardElement/renderBoard
   and the matching tag appended to target button text in showContextMenu. */
.slot-pos-badge {
    font-size: 0.7em;
    font-weight: bold;
    color: var(--accent-gold);
    background: var(--bg-button);
    padding: 1px 4px;
    border-radius: 3px;
    border: 1px solid var(--border-color-strong);
    white-space: nowrap;
}

/* Field zone: the shared field-card slot inside #active-lane */
#field-zone {
    display: flex;
    gap: 10px;
    align-items: center;
}

/* Graveyard spots clickable */
.graveyard-spot {
    cursor: pointer;
    font-size: 0.75em;
    text-align: center;
}
.graveyard-spot:hover { border-color: #aaa; background: rgba(255,255,255,0.05); }

/* Scry modal card selection */
#scry-cards .card {
    cursor: pointer;
    transition: border-color 0.2s, opacity 0.2s;
}
#scry-cards .card:hover { border-color: #ff4444; }

/* Search modal rows */
#search-card-list > div:hover { background: rgba(255,255,255,0.05); }

.card-qty {
    width: 20px;
    text-align: center;
    font-weight: bold;
    color: var(--accent);
}

/* ── Mobile Layout ────────────────────────────────────────────────────────────
   Driven entirely by the html[data-layout="mobile"] attribute (mirrors how
   data-theme drives light/dark), never by @media width queries: the Settings
   modal lets a user manually force Mobile or Desktop regardless of actual
   viewport width, which a plain @media breakpoint could never be overridden by.
   JS (script.js) resolves "Auto" to one of these two before setting the attribute. */

/* Fluid card size for everything OUTSIDE a board row: hand cards, the field zone, the
   exposed-top/Thoth-peek callouts. Board slots size themselves off their grid track instead
   (see .field-row below); this mirrors that track width so a loose card sits flush beside a
   slot. 60px is the non-slot overhead of a board row: #main-layout's 6px side padding (12),
   .field-row's 20px left label gutter + 6px right padding (26), four 5px gaps (20), and 2px
   of slack to stay just inside the real track. Capped at the old 72px so a large phone
   doesn't get oversized cards; 1.39 is the original 100/72 card aspect ratio.
   --row-slots is how many slots the widest row currently holds: 5 normally, up to 7 while
   Okuninushi grants extra bench spots; renderBoard sets it on <html>. Row overhead is
   40px fixed (#main-layout's 6px side padding, .field-row's 20px label gutter + 6px right
   padding, 2px slack) plus a 5px gap between each pair of slots. */
html[data-layout="mobile"] {
    --row-slots: 5;
    --slot-w: min(72px, calc((100vw - 40px - (var(--row-slots) - 1) * 5px) / var(--row-slots)));
    --slot-h: calc(var(--slot-w) * 1.39);
}

/* No hover on a touch screen, so tapping a card fires :hover and left the desktop tooltip
   stranded underneath the card sheet the same tap opens: two copies of the same details.
   Suppressed for exactly the cards that open a sheet (board + hand); modal card lists still
   rely on the tooltip, and the deck builder's is a body-level element, not nested in a
   .card, so neither is affected. */
html[data-layout="mobile"] .card.on-board .card-tooltip,
html[data-layout="mobile"] #hand .card .card-tooltip,
html[data-layout="mobile"] #opponent-hand .card .card-tooltip {
    display: none !important;
}

html[data-layout="mobile"] .card,
html[data-layout="mobile"] .card-spot {
    width: var(--slot-w);
    height: var(--slot-h);
}

/* Board slots on mobile are sized by dividing the viewport (--slot-w), so unlike desktop they
   cannot grow with the Text size setting: scaling the card face alone would just push the
   rules text out of the box. The face therefore keeps its fitted size here (that's what the
   division by --a11y-text-scale undoes), and the setting reaches the text that is actually
   for reading: the card sheet a tap opens, the modals, and every panel. */
html[data-layout="mobile"] .card {
    font-size: calc(0.62em / var(--a11y-text-scale));
    padding: 4px;
    cursor: pointer;
}

/* nowrap keeps a two-word label ("OPP BENCH") in a single vertical column: a shorter
   mobile row doesn't give vertical-rl text the height it needs, so it would otherwise wrap
   into two columns and spill out of the row's left gutter over the first slot. */
html[data-layout="mobile"] .zone-label {
    font-size: 0.5em;
    letter-spacing: 0.5px;
    white-space: nowrap;
    left: 3px;
}

/* Board rows are a grid of fixed --slot-w tracks so a full 5-slot bench row can never wrap
   onto a second line (the old flex row's slots overflowed once their borders and gaps were
   counted).
   auto-fit + justify-content:center is what centres a short row: auto-fit collapses the
   repeated tracks that end up empty, so a 3-slot active row keeps 3 tracks and centres them
   rather than sitting left-aligned in tracks 1-3. Because every row shares one track size
   and gap and every row is centred, the 3 active slots land exactly under bench slots 2-4.
   A fixed track (rather than 1fr) also caps slot size on a wide mobile layout: a coarse-
   pointer tablet at 1024px would otherwise stretch each 1fr track to ~193px, giving cards
   nearly twice their desktop size.
   padding-left reserves a gutter for the absolutely-positioned .zone-label: now that a row
   spans the full width there's no longer slack on the left for it to sit in.
   min-height:0 drops the desktop 140px floor: height comes from the slots' aspect-ratio. */
html[data-layout="mobile"] .field-row {
    display: grid;
    grid-template-columns: repeat(auto-fit, var(--slot-w));
    justify-content: center;
    align-items: center;
    min-height: 0;
    gap: 5px;
    padding: 6px 6px 6px 20px;
}

/* grid-column:auto on every direct child overrides the desktop [data-spot-id]/.deck-spot/
   .graveyard-spot/exposed-top-zone column assignments (see .field-row above): those exist to
   fix a fixed-column desktop alignment bug and know nothing about mobile's auto-fit track
   count; left in place they'd force items into fixed columns 1-8 regardless of how many tracks
   actually exist here, which is exactly what the auto-fit + justify-content:center trick above
   relies on NOT happening. Deck/GY are moot either way: MOBILE_RELOCATIONS pulls them out of
   the row entirely on mobile; .zone-label is unaffected either way (position:absolute). */
html[data-layout="mobile"] .field-row > * {
    grid-column: auto;
}

/* Slots fill their grid track instead of carrying a hardcoded width, so the border is
   inside the track (border-box) and the aspect ratio is the original 72x100 card shape. */
html[data-layout="mobile"] .field-row > .card-spot {
    width: 100%;
    height: auto;
    aspect-ratio: 72 / 100;
    box-sizing: border-box;
}

/* Cards are appended inside their slot (see renderBoard): fill it rather than keeping the
   standalone --slot-w size, which would overflow a slot narrower than the 72px cap. */
html[data-layout="mobile"] .field-row > .card-spot > .card {
    width: 100%;
    height: 100%;
    box-sizing: border-box;
}

html[data-layout="mobile"] #main-layout {
    flex-direction: column;
    padding: 6px;
    gap: 10px;
}

/* #board-row stays a row (align-items:flex-start) on desktop to align #play-field/
   #side-panel's top edges; on a narrow phone screen it stacks like #main-layout does,
   and needs full width so #side-panel's 100% (below) means the viewport, not a
   shrink-wrapped row. */
html[data-layout="mobile"] #board-row {
    flex-direction: column;
    align-self: stretch;
    width: 100%;
    gap: 10px;
}

/* #board-row keeps align-items:flex-start (a desktop necessity), so #play-field would
   shrink-wrap its contents. The mobile rows are grids whose tracks divide up the row's
   width, so a shrink-wrapped parent collapses them to min-content: pin it to full width. */
html[data-layout="mobile"] #play-field {
    width: 100%;
}

/* The field spot stacks with the active rows instead of squeezing them horizontally on a
   narrow phone screen. 8px matches #play-field's row rhythm (#active-rows-stack's own 8px
   gap is lost to the display:contents below, and #active-lane's desktop gap is 10px). */
html[data-layout="mobile"] #active-lane {
    flex-direction: column;
    gap: 8px;
}

/* Put the field chip between the two active rows — the neutral middle of the board, which
   also mirrors the desktop layout where it sits alongside both. `order` can only reorder
   siblings, and #field-zone is a sibling of the *stack*, not of the rows inside it, so the
   stack is dissolved with display:contents to lift both rows into #active-lane. */
html[data-layout="mobile"] #active-rows-stack {
    display: contents;
}

/* Deck strips stay outside #active-lane, up with the bench rows they pair with (see the
   HTML comments by #opponent-deck-strip). Inside the lane, order mirrors the desktop pile
   column top-to-bottom: each active row's GY strip sits right against it, both flanking the
   Field chip in the middle — order 3 (field-zone, below) sits between the two. */
html[data-layout="mobile"] #opponent-active-row { order: 1; }
html[data-layout="mobile"] #opponent-gy-strip   { order: 2; }
html[data-layout="mobile"] #player-gy-strip     { order: 4; }
html[data-layout="mobile"] #player-active-row   { order: 5; }

/* #main-layout switches to flex-direction:column above, so #side-panel's fixed 232px
   flex-basis (a width on desktop) would otherwise become a height here — stack it full
   width below the board instead. */
html[data-layout="mobile"] #side-panel {
    flex: 0 0 auto;
    width: 100%;
}

html[data-layout="mobile"] .deck-spot,
html[data-layout="mobile"] .graveyard-spot {
    font-size: 0.6em;
}

/* Deck/graveyard piles, relocated here out of the playfield rows by applyLayout(). They
   carry no positional meaning, so on mobile they collapse from full card-sized slots into
   short horizontal chips — that frees the rows to fit 5 slots across AND reclaims the
   vertical space the old wrapped-onto-a-second-line rows were eating. */
html[data-layout="mobile"] .pile-strip {
    display: flex;
    justify-content: center;
    align-items: stretch;
    gap: 8px;
    flex-wrap: wrap;
}

html[data-layout="mobile"] .pile-strip .card-spot {
    width: auto;
    height: auto;
    flex-direction: row;
    align-items: baseline;
    gap: 5px;
    padding: 5px 10px;
    border-radius: 6px;
    font-size: 0.62em;
}

html[data-layout="mobile"] .pile-strip .spot-count {
    font-size: 1.25em;
}

/* Opponent's stats block, relocated here by applyLayout() so it reads as part of their
   half of the board. Player 1's equivalent stays in #side-panel below the board. */
html[data-layout="mobile"] #opponent-stats-slot {
    display: block;
}

/* ── Field zone, collapsed to a chip strip (mirrors .pile-strip above) ─────────
   #active-lane goes column on mobile, so the field slot became a full-width band holding
   one card's worth of content — ~95px of near-empty vertical space. There's only ever one
   field card in play (the engine enforces it), so it reads fine as a single centred chip.
   Safe to shrink: the slot is a drag/drop target only, and dragging is off on mobile —
   a field card is played by tapping it in hand (see playCardFromHand). */
html[data-layout="mobile"] #field-zone {
    order: 3; /* between the two GY strips, which flank it against their own active rows —
                 see #active-rows-stack's display:contents and the order rules above */
    justify-content: center;
    gap: 8px;
}

/* Empty state — the "FIELD" placeholder. */
html[data-layout="mobile"] #field-zone > .field-spot {
    width: auto;
    height: auto;
    padding: 5px 14px;
    border-radius: 6px;
    font-size: 0.62em;
}

/* Occupied state — renderBoard appends the real card element here and hides the
   placeholder. Laid out as the same chip, keeping only the name and the ⓘ badge, so a tap
   on ⓘ still opens the full card tooltip and a tap on the body still opens the context
   menu, exactly as on any other board card. */
html[data-layout="mobile"] #field-zone > .card {
    width: auto;
    height: auto;
    flex-direction: row;
    align-items: center;
    gap: 6px;
    padding: 4px 10px;
    border-radius: 6px;
    font-size: 0.62em;
    /* Cap the chip at the zone width so a long name can't push it off-screen; min-width:0
       lets it actually shrink (flex items default to min-width:auto, which refuses to go
       below their content's intrinsic width and would defeat the ellipsis below). */
    max-width: 100%;
    min-width: 0;
    box-sizing: border-box;
}

/* Everything the chip has no room for — it's all in the tooltip the ⓘ opens. */
html[data-layout="mobile"] #field-zone > .card > .card-type,
html[data-layout="mobile"] #field-zone > .card > .card-desc,
html[data-layout="mobile"] #field-zone > .card > .hp-bar,
html[data-layout="mobile"] #field-zone > .card > .card-stats,
html[data-layout="mobile"] #field-zone > .card > .equipped-relics-strip,
html[data-layout="mobile"] #field-zone > .card .card-header-costs {
    display: none;
}

/* Label the chip, since hiding the placeholder also hides the only "FIELD" caption. */
html[data-layout="mobile"] #field-zone > .card::before {
    content: 'FIELD';
    font-weight: bold;
    opacity: 0.6;
    letter-spacing: 0.5px;
    flex: 0 0 auto;
}

/* The name is the only part that gives: the FIELD label and the ⓘ both stay whole, and an
   over-long name truncates to an ellipsis (full name is in the tooltip the ⓘ opens). */
html[data-layout="mobile"] #field-zone > .card > .card-name {
    margin: 0;
    border-bottom: none;
    white-space: nowrap;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* order:2 puts the ⓘ (the only surviving child of .card-header) after the name, so the
   chip reads FIELD · Name · ⓘ rather than leading with the badge. */
html[data-layout="mobile"] #field-zone > .card > .card-header {
    order: 2;
    flex: 0 0 auto;
}

/* Hand — a horizontally scrollable strip instead of wrapping/centering */
html[data-layout="mobile"] #hand,
html[data-layout="mobile"] #opponent-hand {
    justify-content: flex-start;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    flex-wrap: nowrap;
}

/* Hand cards keep the full 72px size rather than the viewport-derived --slot-w: the strip
   scrolls, so nothing has to fit across the width, and shrinking them only costs legibility. */
html[data-layout="mobile"] #hand .card,
html[data-layout="mobile"] #opponent-hand .card {
    flex: 0 0 auto;
    width: 72px;
    height: 100px;
}

/* The sort button keeps its own width out of a phone's hand strip — the hand is what has to
   stay readable, and the label is short enough to survive the trim. */
html[data-layout="mobile"] #hand-sort-btn {
    min-width: 0;
    padding: 6px 8px;
    font-size: 0.72em;
}

/* Modals — cap width instead of touching every hardcoded inline pixel width */
html[data-layout="mobile"] .modal-content {
    width: 92vw !important;
    max-width: 92vw !important;
    max-height: 88vh;
    overflow-y: auto;
    padding: 16px;
    box-sizing: border-box;
}

/* How To Play scrolls as one document on mobile. The desktop layout scrolls .howto-body
   inside a fixed-height shell, but the rule above makes .modal-content scrollable too, and
   two nested scroll areas in a narrow viewport means a drag lands in whichever one the
   finger happens to be over. */
html[data-layout="mobile"] .howto-modal-content {
    display: block;
}

html[data-layout="mobile"] .howto-body {
    overflow-y: visible;
    padding-right: 0;
}

html[data-layout="mobile"] .howto-nav {
    /* One row that scrolls sideways rather than four wrapped rows of pills eating the
       screen before the rules start. */
    flex-wrap: nowrap;
    overflow-x: auto;
    justify-content: flex-start;
}

html[data-layout="mobile"] .howto-nav button {
    flex: 0 0 auto;
}

/* The per-card :hover tooltip has no touch equivalent, and on mobile it's superseded by the
   card sheet below — a tap opens that instead. Only the deck builder's floating variant is
   still used here (it's shown by explicit style.display, not :hover). */
html[data-layout="mobile"] .card-tooltip.builder-float-tooltip {
    position: fixed;
    width: min(85vw, 260px);
    bottom: auto;
    transform: none;
    /* Long cards (many attacks/keywords/attachments) can render taller than a short
       viewport — cap and let it scroll internally instead of overflowing off-screen.
       border-box so the 12px padding + 2px border count toward max-height instead of
       being added on top of it (default content-box would let it overflow anyway). */
    box-sizing: border-box;
    max-height: calc(100vh - 20px);
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;
    pointer-events: auto;
}

/* ── Card sheet ───────────────────────────────────────────────────────────────
   On mobile a tap on any card opens #card-context-menu as a centred sheet: the card's full
   details on top (filled by renderCardSheetDetails) and its actions below. This replaced the
   ⓘ badge and its anchored tooltip. Centred rather than positioned at the tap point because
   the details make it far too tall to sit beside a card on a phone screen. */
html[data-layout="mobile"] #card-context-menu {
    position: fixed;
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%);
    width: min(88vw, 340px);
    max-height: 86vh;
    display: flex;
    flex-direction: column;
    z-index: 1200;
}

/* The details scroll; the action buttons stay pinned and reachable no matter how long the
   card's rules text is. */
html[data-layout="mobile"] #context-menu-details {
    flex: 1 1 auto;
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;
    padding: 10px 12px;
    font-size: 0.85em;
    border-bottom: 1px solid var(--border-color);
}

html[data-layout="mobile"] #card-context-menu .menu-options {
    flex: 0 0 auto;
}

html[data-layout="mobile"] #card-context-menu .menu-options button {
    text-align: center;
    border-top: 1px solid var(--border-color);
}

/* Touch target sizing. #dice-values' grid columns are sized for the desktop 30px die
   (style.css ~L479) — widen them to match, or the bigger dice overlap/squish together. */
html[data-layout="mobile"] .die {
    width: 40px;
    height: 40px;
}

html[data-layout="mobile"] #dice-values {
    grid-template-columns: repeat(3, 40px);
}

html[data-layout="mobile"] .menu-options button,
html[data-layout="mobile"] .settings-row button,
html[data-layout="mobile"] #actions button {
    min-height: 40px;
}

/* Docked to the top on mobile instead of the bottom-left corner: on a short viewport
   (e.g. this game embedded in an iframe on its Hugging Face Space) the page content
   often fills the whole screen with no scrolling, so a bottom-fixed toast sits directly
   on top of the New Game/Next Phase/Settings buttons instead of floating below them. */
html[data-layout="mobile"] #action-history {
    top: 6px;
    bottom: auto;
    left: 6px;
    align-items: flex-start;
    flex-direction: column;
    max-width: 70vw;
    z-index: 950;
}

html[data-layout="mobile"] #action-history-panel {
    width: 90vw;
}

/* ═══════════════════════════════════════════════════════════════════════════════
   ACCESSIBILITY
   Everything below exists to make the game usable with a screen reader, with the
   keyboard alone, and at high contrast / large text. Three rules of thumb when
   touching it:

   1. Never remove a focus ring. The board is navigated with the arrow keys via a
      roving tabindex (see the keyboard-navigation section of script.js), so the ring
      is the *only* thing telling a keyboard player which card they are about to act
      on — there is no cursor to look at.
   2. Anything conveyed by color alone needs a second channel. Status badges carry a
      glyph, granted/fatal slots carry a ::after label, the HP bar carries its number.
   3. .sr-only text is real UI. It is what a screen-reader user is handed instead of
      the visual board, so it has to stay accurate — see cardAriaLabel in script.js.
   ═══════════════════════════════════════════════════════════════════════════════ */

/* Visually hidden but read by assistive tech. The clip-path/1px combination is the
   long-standing safe recipe: display:none and visibility:hidden are both skipped by
   screen readers, and width:0 collapses the text in some of them. */
.sr-only {
    position: absolute !important;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip: rect(0 0 0 0);
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
}

/* ── Skip links ──────────────────────────────────────────────────────────────
   First tab stops on the page. Off-screen until focused, so they cost nothing
   visually but save a keyboard player from tabbing through the whole board to
   reach their hand. */
.skip-link {
    position: absolute;
    left: 8px;
    top: -60px;
    z-index: 4000;
    background: var(--bg-panel);
    color: var(--text-primary);
    border: 2px solid var(--focus-ring);
    border-radius: 0 0 6px 6px;
    padding: 10px 16px;
    font-size: 0.95rem;
    text-decoration: none;
    transition: top 0.15s ease-out;
}

.skip-link:focus {
    top: 0;
}

/* ── Focus visibility ────────────────────────────────────────────────────────
   One ring, everywhere, in a hue used for nothing else (see --focus-ring). The
   double box-shadow gives the ring a contrasting inner edge so it stays visible
   whether the element behind it is dark or light. */
:where(button, select, input, textarea, a[href], summary, [tabindex]:not([tabindex="-1"]), [role="button"]):focus-visible,
.card:focus-visible,
.card-spot:focus-visible,
.die:focus-visible {
    outline: 3px solid var(--focus-ring);
    outline-offset: 2px;
    box-shadow: 0 0 0 1px var(--bg-page), 0 0 8px 2px var(--focus-ring);
}

/* Gamepad focus is programmatic, so :focus-visible — a heuristic over TRUSTED input — does
   not reliably match, and the cursor would silently stop being drawn (see GAMEPAD.md G7).
   While a controller is driving, plain :focus gets the same treatment. Every rule below is a
   byte-identical copy of the :focus-visible rule directly above it: they sit adjacent on
   purpose, so the pair cannot drift. */
html[data-input-mode="gamepad"] :where(button, select, input, textarea, a[href], summary, [tabindex]:not([tabindex="-1"]), [role="button"]):focus,
html[data-input-mode="gamepad"] .card:focus,
html[data-input-mode="gamepad"] .card-spot:focus,
html[data-input-mode="gamepad"] .die:focus {
    outline: 3px solid var(--focus-ring);
    outline-offset: 2px;
    box-shadow: 0 0 0 1px var(--bg-page), 0 0 8px 2px var(--focus-ring);
}

/* The menu is overflow:hidden, so an outset ring on the first/last item is clipped.
   Draw this one inside the item instead. */
.menu-options button:focus-visible {
    outline: 3px solid var(--focus-ring);
    outline-offset: -3px;
    box-shadow: none;
    background: var(--bg-button-hover);
}

html[data-input-mode="gamepad"] .menu-options button:focus {
    outline: 3px solid var(--focus-ring);
    outline-offset: -3px;
    box-shadow: none;
    background: var(--bg-button-hover);
}

/* Focus is the keyboard's hover: a focused card lifts and reveals its details panel
   exactly as a hovered one does, so the full rules text is reachable without a mouse.
   Suppressed on mobile for the same reason :hover is (the tap opens the card sheet,
   which already shows the same details). */
.card:focus-visible {
    transform: translateY(-5px);
    z-index: 100;
}

html[data-input-mode="gamepad"] .card:focus {
    transform: translateY(-5px);
    z-index: 100;
}

/* :focus-visible only — deliberately NOT :focus-within, which also matches focus a script
   moved (a dialog focusing its first element), and would pop a tooltip open over the dialog
   before the player has touched anything. */
.card:focus-visible .card-tooltip {
    display: block;
}

html[data-input-mode="gamepad"] .card:focus .card-tooltip {
    display: block;
}

/* Inside a dialog the attached tooltip is suppressed entirely: the card sits in a frame it
   would either escape (upward, over the dialog's own heading) or be clipped by (a scrolling
   list). Those cards show the body-level floating tooltip instead — see createCardElement's
   showFloating in script.js. */
.modal .card .card-tooltip {
    display: none !important;
}

/* ...which therefore has to sit above every dialog. The static ones run to z-index 1200, but
   the runtime-built ones (showMulliganModal and friends) set z-index:9999 inline, so this has
   to clear that. Safe to put on top of everything: it's pointer-events:none, so it can never
   block what's underneath. */
.card-tooltip.builder-float-tooltip {
    z-index: 10000;
}

html[data-layout="mobile"] .card.on-board:focus-visible .card-tooltip,
html[data-layout="mobile"] #hand .card:focus-visible .card-tooltip,
html[data-layout="mobile"][data-input-mode="gamepad"] .card.on-board:focus .card-tooltip,
html[data-layout="mobile"][data-input-mode="gamepad"] #hand .card:focus .card-tooltip {
    display: none !important;
}

/* ── Text size ───────────────────────────────────────────────────────────────
   The scale itself lives in --a11y-text-scale (see :root). These are the places
   whose fixed pixel sizes have to grow with it or the layout would break: the side
   panel would squeeze the board, and the hand tray would clip its own cards. */
#side-panel {
    flex: 0 0 calc(232px * var(--a11y-text-scale));
}

#hand {
    min-height: calc(150px * var(--a11y-text-scale));
}

/* At any scale above 1 the board can be wider than the viewport. #main-layout clips
   (it has to — see its own comment about the parallax overscan), so give the clipped
   axis a scrollbar rather than silently hiding the far end of the bench. */
html:not([data-a11y-scale="100"]) #main-layout {
    overflow-x: auto;
    overflow-y: hidden;
}

/* ── High contrast (data-theme="hc-dark" / "hc-light") ───────────────────────
   The palettes are up at the top with the other themes; what's left here is the
   translucency, glow and shadow that a variable swap can't reach. Rule of thumb:
   in these themes every boundary is a solid opaque line and every fill is opaque. */
:root[data-theme^="hc-"] .card,
:root[data-theme^="hc-"] .card-spot,
:root[data-theme^="hc-"] .modal-content,
:root[data-theme^="hc-"] .context-menu,
:root[data-theme^="hc-"] .card-tooltip {
    box-shadow: none;
    border-style: solid;
    border-width: 3px;
}

:root[data-theme^="hc-"] .card-spot {
    border-style: dashed; /* still has to read as "empty slot", just at full contrast */
}

:root[data-theme^="hc-"] .field-parallax-layer,
:root[data-theme^="hc-"] #vanta-bg {
    display: none;
}

:root[data-theme^="hc-"] .field-row {
    border: 1px solid var(--border-color);
}

/* Decorative glows/tints that carry no information and only muddy the contrast. */
:root[data-theme^="hc-"] .card:hover,
:root[data-theme^="hc-"] .card:focus-visible,
html[data-input-mode="gamepad"]:root[data-theme^="hc-"] .card:focus {
    box-shadow: none;
}

:root[data-theme^="hc-"] .card-spot.slot-highlight,
:root[data-theme^="hc-"] .card.target-highlight,
:root[data-theme^="hc-"] .card.affected-highlight {
    background: transparent;
    box-shadow: none;
    outline: 4px solid var(--accent);
    outline-offset: 1px;
}

:root[data-theme^="hc-"] .card.enlightened {
    background: var(--bg-card);
    border-color: var(--accent-gold);
    box-shadow: none;
}

:root[data-theme^="hc-"] .hp-fill {
    background: var(--accent-success);
}

:root[data-theme^="hc-"] .hp-bar {
    border: 1px solid var(--border-color);
}

:root[data-theme^="hc-"] .floating-number {
    text-shadow: none;
    -webkit-text-stroke: 1px var(--bg-page);
}

:root[data-theme^="hc-"] #status-message,
:root[data-theme^="hc-"] #action-history-panel {
    background: var(--bg-panel);
    color: var(--text-primary);
    border: 2px solid var(--border-color);
}

/* Status badges are read by their glyph, not their hue (see STATUS_STYLES in script.js),
   so at high contrast the hue is dropped entirely for one legible treatment. */
:root[data-theme^="hc-"] .preservation-badge {
    background: var(--bg-panel-strong) !important;
    color: var(--text-primary);
    border: 1px solid var(--border-color);
}

:root[data-theme^="hc-"] .status-badge {
    background: var(--bg-panel-strong) !important;
    color: var(--text-primary);
    border: 1px solid var(--border-color);
}

:root[data-theme^="hc-"] .equipped-relic-badge {
    background: var(--bg-panel-strong) !important;
    color: var(--text-primary) !important;
    border: 1px solid var(--border-color) !important;
}

:root[data-theme^="hc-"] .die {
    background: #ffffff;
    color: #000000;
    border: 3px solid #000000;
}

:root[data-theme^="hc-"] .die.locked {
    background: #000000;
    color: #ffffff;
    border-color: #ffffff;
    box-shadow: none;
}

/* Links and the footer credit need a non-color cue in a high-contrast theme. */
:root[data-theme^="hc-"] a {
    text-decoration: underline;
}

/* The field slot's border and tint are hard-coded gold, which disappears against a white
   page; and a 50%-opacity label is exactly the kind of thing a high-contrast theme exists
   to undo. (Opacity can't be fixed by a variable — it isn't a color.) */
:root[data-theme^="hc-"] .field-spot {
    border-color: var(--accent-gold);
    background: transparent;
}

:root[data-theme^="hc-"] .inner-label,
:root[data-theme^="hc-"] .zone-label {
    opacity: 1;
}

/* The hand tray reads as a distinct zone in the normal themes because of its glow; at high
   contrast a soft glow barely registers, so give it a real edge as well. The glow itself is
   left alone on purpose — updateGameState sets it inline to mark "it's your turn". */
:root[data-theme^="hc-"] #hand {
    border: 2px solid var(--border-color);
    border-radius: 8px;
}

/* ── Forced colors (Windows High Contrast / forced-colors: active) ───────────
   The OS replaces every color, so the only thing worth doing is making sure the
   boundaries we draw with backgrounds alone still have a real border to keep. */
@media (forced-colors: active) {
    .card,
    .card-spot,
    .modal-content,
    .context-menu,
    .card-tooltip,
    .die,
    button {
        border: 1px solid CanvasText;
    }
    .card:focus-visible,
    .card-spot:focus-visible,
    button:focus-visible,
    [tabindex]:focus-visible,
    html[data-input-mode="gamepad"] .card:focus,
    html[data-input-mode="gamepad"] .card-spot:focus,
    html[data-input-mode="gamepad"] button:focus,
    html[data-input-mode="gamepad"] [tabindex]:focus {
        outline: 3px solid Highlight;
        outline-offset: 2px;
    }
    .field-parallax-layer,
    #vanta-bg {
        display: none;
    }
}

/* ── Reduced motion ──────────────────────────────────────────────────────────
   The individual animations already opt out above; this is the net that catches
   the transitions and any animation added later. script.js additionally defaults
   the Animations setting to Off for a first-time visitor who asks for this. */
@media (prefers-reduced-motion: reduce) {
    *, *::before, *::after {
        animation-duration: 0.001ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.001ms !important;
        scroll-behavior: auto !important;
    }
    .card:hover,
    .card:focus-visible,
    html[data-input-mode="gamepad"] .card:focus {
        transform: none;
    }
    /* The draw flight and the sort reflow are both movement for its own sake. The
       transition-duration override above already collapses them; this makes sure a
       flying ghost is never left mid-air if it is created while the query is on. */
    .card.card-flight {
        display: none;
    }
}

/* ── Interactive spots ───────────────────────────────────────────────────────
   The deck/graveyard piles are divs that behave as buttons (they open the pile
   viewer). Given a role and a tabindex by index.html, they still need the cursor
   and hover affordance a real button would have. */
.card-spot[role="button"] {
    cursor: pointer;
}

/* #status-message is a real <button> now (it toggles the history panel) — undo the
   parts of the UA button style that would fight its existing look. */
#status-message {
    border: none;
    font-family: inherit;
    text-align: left;
    display: block;
}

/* Keyboard-shortcut table in the How to Play modal. */
.howto-keys {
    width: 100%;
    border-collapse: collapse;
    margin: 8px 0;
    font-size: 0.95em;
}

.howto-keys th,
.howto-keys td {
    text-align: left;
    padding: 5px 8px;
    border-bottom: 1px solid var(--border-color);
    vertical-align: top;
}

.howto-keys kbd {
    display: inline-block;
    background: var(--bg-button);
    color: var(--text-primary);
    border: 1px solid var(--border-color-strong);
    border-radius: 4px;
    padding: 1px 6px;
    font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
    font-size: 0.9em;
    white-space: nowrap;
}

/* A settings row whose control is a <select> rather than a cycling button. */
.settings-row select {
    background: var(--bg-input);
    color: var(--text-primary);
    border: 1px solid var(--border-color-strong);
    border-radius: 4px;
    padding: 5px 6px;
    font-family: inherit;
    font-size: 0.9em;
    min-width: 150px;
}

/* The dice are <button>s now (they toggle "keep this die"), so the global button padding
   would blow the 30px square out and the UA font would override the panel's. */
.die {
    padding: 0;
    font-family: inherit;
}

/* ═══════════════════════════════════════════════════════════════════════════
   GAMEPAD + KIOSK  (convention demo — see GAMEPAD.md)

   The focus-visibility half of this lives up in the ACCESSIBILITY block, beside
   each :focus-visible rule it mirrors (G7). What is left is the on-screen button
   hints strip and the kiosk-mode hiding.
   ═══════════════════════════════════════════════════════════════════════════ */

/* Controller hints (G8) — gated entirely on the attribute gpStart()/gpStop() manage, so
   there is one switch, not two. pointer-events:none so it can never block the hand under it. */
#gamepad-hints { display: none; }

html[data-gamepad="on"] #gamepad-hints {
    display: flex;
    flex-wrap: wrap;
    gap: 18px;
    justify-content: center;
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 900;
    padding: 6px 12px;
    pointer-events: none;
    background: var(--bg-panel);
    color: var(--text-secondary);
    border-top: 1px solid var(--border-color);
    font-size: calc(12px * var(--a11y-text-scale));
}

#gamepad-hints b {
    display: inline-block;
    min-width: 1.6em;
    margin-right: 4px;
    text-align: center;
    border: 1px solid currentColor;
    border-radius: 4px;
    color: var(--text-primary);
}

/* Kiosk (convention) mode — see GAMEPAD.md O5. These paths need a network and a physical
   keyboard, neither of which the demo machine has: there is no on-screen keyboard, so the
   room-code field is unreachable with a gamepad regardless. */
html[data-kiosk="on"] #deck-modal .modal-group:has(#create-room-btn) { display: none; }
/* Kiosk mode has no online play, so the win-condition hint must not talk about hosting and
   joining: the sentence would describe a control the booth player cannot see. */
html[data-kiosk="on"] .online-only-hint { display: none; }

/* The reset prompt's mouse/keyboard route. Gamepad Start opens the same dialog, but the
   button must exist for an operator with a trackpad and nothing else. */
#reset-demo-btn { display: none; }
html[data-kiosk="on"] #reset-demo-btn { display: block; }

/* "Yes, reset" is the destructive half of the confirmation — tint it so the two options are
   told apart by more than reading order (the focus default is deliberately "No"). */
#reset-demo-yes-btn {
    color: var(--accent-danger);
    border-color: var(--accent-danger);
}

/* AUDIT F21: a reaction window the player stepped away from to play another card first. Sits
   above the board without covering the hand, and uses the theme tokens so all four palettes
   (and the high-contrast ones) get it right. */
#reaction-banner {
    position: fixed;
    top: 10px;
    left: 50%;
    transform: translateX(-50%);
    z-index: 900;
    display: flex;
    gap: 16px;
    align-items: center;
    max-width: calc(100vw - 32px);
    padding: 10px 20px;
    background: var(--bg-modal-inset);
    border: 2px solid var(--accent);
    border-radius: 8px;
    color: var(--text-primary);
}
#reaction-banner.hidden { display: none; }
#reaction-banner button,
#reaction-minimize-btn {
    padding: 6px 14px;
    background: var(--accent);
    color: var(--accent-contrast);
    border: none;
    border-radius: 4px;
    cursor: pointer;
}
#reaction-minimize-btn { margin-left: 8px; background: var(--border-color-strong); color: var(--text-primary); }
#reaction-minimize-btn.hidden { display: none; }
