/*
 * 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;
}

/* ── Cosmos theme ─────────────────────────────────────────────────────────────
   The poster's look: deep space, a violet-and-magenta nebula, gold lettering. The sky is a
   live WebGL backdrop (features/cosmos.js, #cosmos-bg below); this block is the palette, and
   the "Cosmos theme" section further down is everything a palette swap can't reach (the
   translucent panels, the splash). Checked with the same relative-luminance script as the
   others, and with one extra case: the panel and row fills are translucent, so every text and
   border colour is checked against them composited over the BRIGHTEST gas the shader can draw
   behind a board (cosmos.js's DIM_GAME), not just over black. Lowest text pair 5.0:1
   (text-muted on bg-button-hover); the slot's dashed border on a row needs the row's 0.7. */
:root[data-theme="cosmos"] {
    --bg-page: #06030d;
    --bg-panel: rgba(14, 9, 30, 0.8);
    --bg-panel-strong: #2b2050;
    --bg-surface: #160f2b;
    --bg-header-strip: #0e0920;
    --bg-card: #231a3f;
    --bg-input: #110b24;
    --bg-input-alt: #2a1f4b;
    --bg-button: #2c2152;
    --bg-button-hover: #3d2e70;
    --bg-modal-content: #130c27;
    --bg-modal-accent: #1c1338;
    --bg-modal-inset: #0b0619;
    --bg-modal-inset-hover: #22174a;
    --border-color: #7e6cab;
    --border-color-strong: #a08bd6;
    --text-primary: #eee8ff;
    --text-secondary: #c3b8e6;
    --text-muted: #b0a4d6;
    /* Dark's own accent, unchanged: it is also drawn ON cards (the playable marker), and the
       cards must render identically in this theme. */
    --accent: #00ffcc;
    --accent-hover: #00cc88;
    --accent-contrast: #1a1a1a;
    --accent-danger: #ff8c82;
    --accent-success: #6dff9a;
    --accent-gold: #f5c74e;
    --accent-purple: #eda8ff;
    --accent-warn: #ffb04a;
    --bg-deck-spot: #3a2358;
    --border-deck-spot: #a07ad6;
    --text-deck-spot: #ffffff;
    --bg-graveyard-spot: rgba(10, 6, 22, 0.86);
    --border-graveyard-spot: #3c2c63;
    --bg-exile-spot: #1d0b33;
    --border-exile-spot: #9a52e0;
    --text-exile-spot: #dcaaff;
    --parallax-glow-1: transparent;
    --parallax-glow-2: transparent;
    --parallax-glow-3: transparent;
    --parallax-glow-4: transparent;
    --focus-ring: #8fd3ff;
    --row-bg: rgba(14, 9, 30, 0.7);
}

/* 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, shown once before the deck-select flow. Deliberately
   theme-independent: the same gold sky in every theme. The sky is title-sky.js's canvas, over
   #title-sky's CSS; in front of it, the logo straight on the sky (it carries its own outline,
   so it needs no box behind it), a Greek-key band, and the card-back plaque that holds Enter
   and the Discord link. */
#title-screen {
    position: fixed;
    inset: 0;
    z-index: 3000;
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: hidden;
    /* The sky the clouds drift in (sampled from Vanta's, which title-sky.js matches): deep gold
       overhead, pale yellow at the horizon, the cloud sea below it. The canvas covers it. */
    background: linear-gradient(180deg, #f2df7e 0%, #fff186 26%, #ffffa0 47%, #ffe39a 51%,
        #f2c08a 58%, #dcae7e 72%, #c19c70 86%, #a8875f 100%);
}
@media (orientation: portrait) {
    /* The horizon sits lower on a tall screen, as it does in the shader. */
    #title-screen {
        background: linear-gradient(180deg, #f2df7e 0%, #fff186 34%, #ffffa0 58%, #ffe39a 62%,
            #f2c08a 68%, #dcae7e 80%, #c19c70 91%, #a8875f 100%);
    }
}

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

/* Intro: the whole splash starts black and fades in once the clouds have drawn, so the CSS
   sky -> canvas swap happens out of sight. index.html's pre-paint script sets
   data-title-intro="pending" and title.js's revealTitleContent() is the one thing that clears
   it. pointer-events: none so the Enter button stays usable underneath.
   "Loading..." is printed on the overlay itself, from the first paint, so a slow load doesn't
   look like a dead page and the text fades out with the black rather than popping in or out on
   its own (a delayed reveal flashed it just before the splash appeared). The "/ ''" alt text
   keeps it out of the accessibility tree, since the overlay stays in the DOM at opacity 0. */
#title-screen::after {
    content: 'Loading\2026';
    content: 'Loading\2026' / '';
    position: absolute;
    inset: 0;
    z-index: 2;
    display: flex;
    align-items: center;
    justify-content: center;
    background: #000;
    color: #f0e6d2;
    /* No web font: one still loading swaps in mid-display, which read as a glitch. */
    font-family: Georgia, 'Times New Roman', serif;
    font-size: clamp(1em, 3vw, 1.4em);
    letter-spacing: 3px;
    opacity: 0;
    pointer-events: none;
    transition: opacity 700ms ease-out;
}
html[data-title-intro="pending"] #title-screen::after { opacity: 1; }
/* While black, the clouds appear instantly: fading them in under the overlay would show a
   CSS-sky/canvas blend as it lifts. */
html[data-title-intro="pending"] #title-sky > canvas { transition: none; }

/* The CSS sky's clouds. Not a strip shown for a moment: with no WebGL, or before the first
   frame, this is the splash's sky, and anywhere the canvas doesn't appear the splash must still
   have one (a Pi 3 once showed cream title text on bare page). Layered radial gradients make
   the cloud banks along the horizon; two layers drift opposite ways at different speeds for
   parallax. Plain gradients: no WebGL context, no texture memory. */
#title-sky {
    position: absolute;
    inset: 0;
    background:
        radial-gradient(ellipse 34% 7% at 14% 60%, rgba(255, 238, 196, 0.85), transparent 70%),
        radial-gradient(ellipse 28% 6% at 40% 58%, rgba(255, 242, 206, 0.70), transparent 72%),
        radial-gradient(ellipse 40% 8% at 70% 61%, rgba(255, 234, 190, 0.78), transparent 70%),
        radial-gradient(ellipse 30% 7% at 90% 59%, rgba(255, 230, 184, 0.62), transparent 72%),
        radial-gradient(ellipse 50% 11% at 30% 80%, rgba(238, 198, 142, 0.60), transparent 68%),
        radial-gradient(ellipse 44% 10% at 78% 88%, rgba(222, 178, 124, 0.55), 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. */
#title-sky::before {
    content: '';
    position: absolute;
    inset: 0;
    background:
        radial-gradient(ellipse 46% 9% at 58% 70%, rgba(255, 210, 150, 0.42), transparent 68%),
        radial-gradient(ellipse 38% 8% at 10% 66%, rgba(255, 196, 128, 0.36), transparent 70%),
        radial-gradient(ellipse 42% 9% at 90% 74%, rgba(250, 200, 138, 0.34), transparent 70%);
    background-repeat: repeat-x;
    background-size: 190% 100%, 190% 100%, 190% 100%;
    animation: pantheon-clouds-drift 150s linear infinite reverse;
}

/* title-sky.js's canvas fades in over the CSS sky once it has drawn its first frame (it adds
   .title-sky-ready), instead of popping in. */
#title-sky > canvas {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    z-index: 1;
    opacity: 0;
    transition: opacity 700ms ease-out;
}
#title-sky.title-sky-ready > canvas { opacity: 1; }

@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; }
}

#title-screen-content {
    position: relative;
    z-index: 1;
    display: flex;
    flex-direction: column;
    align-items: center;
    width: 100%;
    box-sizing: border-box;
    padding: 16px;
    text-align: center;
}

#title-screen-heading {
    margin: 0;
    line-height: 0;
}

/* As wide as the screen allows, but short enough that the plaque still fits under it on a
   landscape phone: 3.21 is the image's aspect (1453 x 452), 280px everything below it. */
#title-screen-logo {
    display: block;
    width: max(200px, min(92vw, 780px, (100vh - 280px) * 3.21));
    width: max(200px, min(92vw, 780px, (100dvh - 280px) * 3.21));
    height: auto;
    filter: drop-shadow(0 6px 16px rgba(70, 35, 0, 0.35));
    transition: opacity 900ms ease-out, transform 1600ms cubic-bezier(0.16, 1, 0.3, 1);
}
/* The one entrance: the logo settles as the black lifts. Only ever with Animations on, since
   that is the only time the intro is pending. */
html[data-title-intro="pending"] #title-screen-logo {
    opacity: 0;
    transform: scale(1.06);
}

/* A Greek-key band under the logo, in its outline's bronze, fading out at both ends. The gap
   below grows with the screen, so a tall screen keeps the logo high and the plaque low. */
.title-key-band {
    width: min(300px, 62vw);
    height: 12px;
    margin: clamp(8px, 2vh, 18px) 0 clamp(40px, 9vh, 88px);
    background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='12' viewBox='0 0 14 12'%3E%3Cpath d='M0 11.3H14M1 11.3V1.5H11.5V8.5H5.5V5H8.5' fill='none' stroke='%235a320b' stroke-width='1.4'/%3E%3C/svg%3E") repeat-x center / 14px 12px;
    -webkit-mask-image: linear-gradient(90deg, transparent, #000 18%, #000 82%, transparent);
    mask-image: linear-gradient(90deg, transparent, #000 18%, #000 82%, transparent);
}

/* The card back as a menu: its leather (title/leather.webp, cropped from cards/card-back.webp),
   its gold edge, and its gem set into an arched top. */
#title-screen-plaque {
    position: relative;
    width: min(380px, 100%);
    box-sizing: border-box;
    padding: 46px 22px 22px;
    border-radius: 50% 50% 14px 14px / 64px 64px 14px 14px;
    background:
        linear-gradient(180deg, rgba(40, 14, 2, 0.3), rgba(30, 10, 0, 0.58)),
        url('title/leather.webp') center / cover;
    box-shadow:
        inset 0 0 0 2px #c9a548,
        inset 0 0 0 6px rgba(42, 16, 4, 0.85),
        inset 0 0 0 7px rgba(201, 165, 72, 0.6),
        0 18px 40px rgba(60, 25, 0, 0.45),
        0 4px 10px rgba(60, 25, 0, 0.3);
}

.title-plaque-gem {
    position: absolute;
    top: -30px;
    left: 50%;
    width: 60px;
    height: 60px;
    margin-left: -30px;
    filter: drop-shadow(0 4px 6px rgba(30, 10, 0, 0.55));
}

/* Enter is the gold name banner of the card frames: pointed ends, a dark edge (::before), the
   gold (::after), and now and then a glint sweeping across it. The label stays plain text,
   since refreshTitleScreenLabel swaps it for the gamepad prompt, which may wrap. */
#title-screen-enter-btn {
    position: relative;
    isolation: isolate;
    display: block;
    width: 100%;
    min-height: 56px;
    box-sizing: border-box;
    padding: 10px 34px;
    border: 0;
    background: none;
    cursor: pointer;
    font-family: 'Cinzel', Georgia, 'Times New Roman', serif;
    font-weight: 700;
    font-size: 15px;
    line-height: 1.2;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: #2a1004;
    filter: drop-shadow(0 6px 10px rgba(20, 6, 0, 0.45));
    transition: transform 0.15s ease-out;
}
#title-screen-enter-btn::before,
#title-screen-enter-btn::after {
    content: '';
    position: absolute;
    z-index: -1;
    clip-path: polygon(20px 0, calc(100% - 20px) 0, 100% 50%, calc(100% - 20px) 100%, 20px 100%, 0 50%);
}
#title-screen-enter-btn::before {
    inset: 0;
    background: #6e4a0e;
}
#title-screen-enter-btn::after {
    inset: 2px;
    background:
        linear-gradient(105deg, transparent 40%, rgba(255, 255, 240, 0.65) 50%, transparent 60%) no-repeat,
        linear-gradient(180deg, #ffe49a 0%, #f0c24c 50%, #d29f2e 100%);
    background-size: 250% 100%, 100% 100%;
    background-position: 100% 0, 0 0;
    animation: title-glint 5s ease-in-out 1.5s infinite;
    transition: filter 0.15s ease-out;
}
#title-screen-enter-btn:hover { transform: translateY(-1px); }
#title-screen-enter-btn:hover::after { filter: brightness(1.08); }
#title-screen-enter-btn:active { transform: translateY(1px); }
/* Focus is a glow that follows the banner's shape (a filter, so it traces the clip-path), not
   the page's rectangular ring, which around a pointed banner reads as a box. syncOverlays
   focuses Enter as the splash comes up, so this is usually what a player sees first: the
   selected menu item, lit, as a game's would be. Gamepad focus is programmatic, hence :focus. */
#title-screen-enter-btn:focus-visible,
html[data-input-mode="gamepad"] #title-screen-enter-btn:focus {
    outline: none;
    box-shadow: none;
    filter: drop-shadow(0 0 1.5px #fff6d0) drop-shadow(0 0 9px rgba(255, 214, 102, 0.95))
            drop-shadow(0 6px 10px rgba(20, 6, 0, 0.45));
}
@media (forced-colors: active) {
    #title-screen-enter-btn:focus-visible,
    html[data-input-mode="gamepad"] #title-screen-enter-btn:focus {
        outline: 3px solid Highlight;
        outline-offset: 4px;
    }
}
/* The glint parks off the button's left edge, then sweeps across to the right. */
@keyframes title-glint {
    0%, 72% { background-position: 100% 0, 0 0; }
    100%    { background-position: 0% 0, 0 0; }
}
html[data-animations="off"] #title-screen-enter-btn::after { animation: none; }

/* The studio in the corners: Big N Lizard's socials bottom left, its logo bottom right. They
   fall away with the rest of the splash on exit, fading as the title does. */
.title-social,
.title-bnl {
    position: absolute;
    z-index: 1;
    bottom: max(14px, env(safe-area-inset-bottom));
    transition: opacity 300ms ease-out, transform 0.2s ease-out;
}
#title-screen.title-exit .title-social,
#title-screen.title-exit .title-bnl { opacity: 0; }

.title-social {
    left: max(14px, env(safe-area-inset-left));
    display: flex;
    gap: 8px;
}
/* Each a small medallion in the plaque's leather and gold, so it reads over any part of the
   sky, at a full 44px touch target. */
.title-social a {
    display: grid;
    place-items: center;
    width: 44px;
    height: 44px;
    border-radius: 50%;
    background: radial-gradient(circle at 35% 30%, #5a2a12, #2a1004 75%);
    box-shadow:
        inset 0 0 0 1.5px #c9a548,
        inset 0 0 0 4px rgba(42, 16, 4, 0.9),
        0 4px 10px rgba(60, 25, 0, 0.35);
    color: #e2b856;
    transition: transform 0.15s ease-out, color 0.15s;
}
.title-social svg {
    width: 22px;
    height: 22px;
}
.title-social a:hover {
    color: #ffe08a;
    transform: translateY(-2px);
}

.title-bnl {
    right: max(14px, env(safe-area-inset-right));
    display: block;
    width: clamp(48px, 4.5vw, 64px);
    line-height: 0;
    filter: drop-shadow(0 4px 8px rgba(60, 25, 0, 0.35));
    transform-origin: 50% 100%;
}
.title-bnl img {
    display: block;
    width: 100%;
    height: auto;
}
.title-bnl[href]:hover { transform: rotate(-4deg) scale(1.05); }

/* A booth machine stays on this site: no socials, and the logo isn't a link (title.js). */
html[data-kiosk="on"] .title-social { display: none; }

/* Reduced motion (which is how the Pi launcher runs: see Scripts/demo-pi.sh) keeps the sky and
   drops only the movement. */
@media (prefers-reduced-motion: reduce) {
    #title-sky,
    #title-sky::before,
    #title-screen-enter-btn::after { animation: none; }
}

/* Splash exit: see GAMEPAD.md G11. The camera rushes up out of the clouds: the whole splash
   (sky, clouds and title) accelerates down off the screen and fades, under the speed lines
   of fx/warp-lines.js; the title fades first, so its text doesn't smear down the screen with
   it. Only then does #title-screen get .hidden (display:none, which is why this needs its own
   class rather than transitioning straight to hidden). What the falling splash uncovers is the
   page behind it: in Cosmos, the nebula arriving from above (#cosmos-bg.cosmos-arriving). */
#title-screen-content {
    transition: transform 450ms cubic-bezier(0.4, 0, 0.2, 1), opacity 300ms ease-out;
}
#title-screen.title-exit {
    pointer-events: none;                 /* no second press lands mid-transition */
    /* The top of the sky thins into space as the camera climbs, rather than the splash sliding
       off as a hard-edged panel: a mask whose see-through band starts parked above the box
       (position 0 100%) and slides down into its top 30% (0 0) over the first half-second. */
    -webkit-mask-image: linear-gradient(to bottom, transparent, #000 20%);
    mask-image: linear-gradient(to bottom, transparent, #000 20%);
    -webkit-mask-size: 100% 150%;
    mask-size: 100% 150%;
    -webkit-mask-repeat: no-repeat;
    mask-repeat: no-repeat;
    /* 1300ms is TITLE_EXIT_MS in title.js; retune both together. Ease-in: the camera is
       accelerating, so the clouds start slow and leave fast. */
    animation: title-rush-down 1300ms cubic-bezier(0.55, 0, 0.85, 0.3) forwards,
               title-thin-out 500ms ease-out forwards;
}
#title-screen.title-exit #title-screen-content {
    transform: translateY(48px) scale(0.97);
    opacity: 0;
}
@keyframes title-rush-down {
    0%   { transform: translateY(0); opacity: 1; }
    50%  { opacity: 1; }
    100% { transform: translateY(110vh); opacity: 0; }
}
@keyframes title-thin-out {
    from { -webkit-mask-position: 0 100%; mask-position: 0 100%; }
    to   { -webkit-mask-position: 0 0; mask-position: 0 0; }
}

/* The speed lines (fx/warp-lines.js): over the falling splash, under the announcement banner. */
#warp-lines {
    position: fixed;
    inset: 0;
    width: 100%;
    height: 100%;
    z-index: 3100;
    pointer-events: none;
}

/* The board waits, hidden, behind the splash and fades in once it has gone (the menu that
   opens then fades in too: #deck-modal). Opacity, not visibility or display, so nothing about
   the board's layout or focus order changes while it is hidden. */
#game-container { transition: opacity 600ms ease-out; }
body:has(#title-screen:not(.hidden)) #game-container { opacity: 0; }
html[data-animations="off"] #game-container { transition: none; }

@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,
    #game-container { 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;
}

/* One line, never wrapped: with flex-wrap on, a row a few px too wide dropped the whole chip
   group under the name instead of letting the name give ground (it did, for "Opponent" + H +
   WP at 100% text on a Mac, whose Tahoma fallback runs wider than Segoe UI). */
.player-stats {
    display: flex;
    flex-wrap: nowrap;
    align-items: center;
    gap: 4px;
    font-weight: bold;
}

/* The stats sit hard right (what justify-content: space-between used to do), as one group. */
.stat-chip-group {
    display: flex;
    align-items: center;
    gap: 4px;
    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. (Engine names are "Player 1" / "Opponent", which fit whole at every
   text scale with the spacing below; 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: these plus a name have to share ~208px. */
.player-stats .stat-chip {
    padding: 2px 5px;
    gap: 2px;
}

/* 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;
}

/* Stag of the Seasons: the doubled resource's chip during the Roll phase. The "x2" is the
   second channel, so the glow isn't the only thing saying it. */
.stat-chip.chip-doubled {
    background: var(--accent);
    color: var(--accent-contrast);
    box-shadow: 0 0 8px var(--accent);
}
.stat-chip.chip-doubled b { color: inherit; }
.stat-chip.chip-doubled::after {
    content: "\00d7 2";
    font-weight: 700;
    margin-left: 2px;
}

/* Stag of the Seasons: which dice give double points this turn (one per player panel). */
.double-dice-note {
    margin-top: 6px;
    font-size: 0.8em;
    text-align: center;
    color: var(--accent);
}

/* Points and tokens may be spent in either order; shown only while the player has a token. */
.spend-order-toggle {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    margin-top: 6px;
    font-size: 0.8em;
    cursor: pointer;
}
.spend-order-toggle input { margin: 0; cursor: pointer; accent-color: var(--accent); }

/* 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. ui/layout.js's applyLayout()
   moves the elements in/out; see the mobile block at the bottom of this file. */
.panel-piles,
#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;
}

/* A deck pile, yours and the opponent's, is the printed card back (Alpha Set _back.png, cropped
   to the card), the same in every theme: it's a picture of the physical card. The label and
   count sit on a dark tab at the foot, clear of the logo. Desktop only; on mobile the pile is a
   chip in a strip. */
html:not([data-layout="mobile"]) .deck-spot {
    --deck-back: #0d0b0a url('cards/card-back.webp') center / cover no-repeat;
    background: var(--deck-back);
    border-color: #050404;
    justify-content: flex-end;
    gap: 0;
    padding-bottom: 6px;
    color: #fff;
}
html:not([data-layout="mobile"]) .deck-spot > :is(.spot-title, .spot-count) {
    background: rgba(0, 0, 0, 0.72);
    padding: 0 6px;
    line-height: 1.35;
}
html:not([data-layout="mobile"]) .deck-spot > .spot-title { border-radius: 4px 4px 0 0; }
html:not([data-layout="mobile"]) .deck-spot > .spot-count { border-radius: 0 0 4px 4px; font-size: 1.2em; }

/* Your deck pile hops and fans out its top cards on hover / keyboard / gamepad focus, a hint
   that it opens the deck list (features/deck-viewer.js). The face moves to ::before and the two
   back cards are .deck-fan's pseudo-elements, both under the text: `isolation` keeps the negative
   z-indexes inside the pile instead of behind the board. Desktop only; on mobile the pile is a
   chip in a strip. */
html:not([data-layout="mobile"]) #player-deck {
    isolation: isolate;
    background: transparent;
    border-color: transparent;
}
html:not([data-layout="mobile"]) #player-deck::before,
html:not([data-layout="mobile"]) #player-deck .deck-fan::before,
html:not([data-layout="mobile"]) #player-deck .deck-fan::after {
    content: '';
    position: absolute;
    inset: -2px;
    border-radius: 8px;
    background: var(--deck-back);
    border: 2px solid #050404;
}
html:not([data-layout="mobile"]) #player-deck::before { z-index: -1; }
html:not([data-layout="mobile"]) #player-deck .deck-fan {
    position: absolute;
    inset: 0;
    z-index: -2;
    pointer-events: none;
}
html:not([data-layout="mobile"]) #player-deck .deck-fan::before,
html:not([data-layout="mobile"]) #player-deck .deck-fan::after {
    filter: brightness(0.82);
    box-shadow: 0 2px 6px rgba(0, 0, 0, 0.35);
    transform-origin: 50% 100%;
    transition: transform 0.28s cubic-bezier(0.34, 1.56, 0.64, 1);
}
#player-deck .deck-fan { display: none; }
html:not([data-layout="mobile"]) #player-deck .deck-fan { display: block; }
html:not([data-layout="mobile"]) #player-deck:is(:hover, :focus-visible) {
    animation: deck-hop 0.38s ease-out forwards;
    cursor: pointer;
}
html:not([data-layout="mobile"])[data-input-mode="gamepad"] #player-deck:focus {
    animation: deck-hop 0.38s ease-out forwards;
}
html:not([data-layout="mobile"]) #player-deck:is(:hover, :focus-visible) .deck-fan::before,
html:not([data-layout="mobile"])[data-input-mode="gamepad"] #player-deck:focus .deck-fan::before {
    transform: translate(-6px, -3px) rotate(-10deg);
}
html:not([data-layout="mobile"]) #player-deck:is(:hover, :focus-visible) .deck-fan::after,
html:not([data-layout="mobile"])[data-input-mode="gamepad"] #player-deck:focus .deck-fan::after {
    transform: translate(6px, -5px) rotate(8deg);
}
@keyframes deck-hop {
    0%   { transform: translateY(0); }
    40%  { transform: translateY(-9px); }
    70%  { transform: translateY(-2px); }
    100% { transform: translateY(-4px); }
}
/* Animations off: no hop, but the fan still shows (it's a state, not motion), just without
   the swing. prefers-reduced-motion is covered by the global net. */
html[data-animations="off"] #player-deck:is(:hover, :focus-visible, :focus) {
    animation: none !important;
    transform: translateY(-4px);
}
html[data-animations="off"] #player-deck .deck-fan::before,
html[data-animations="off"] #player-deck .deck-fan::after { transition: none; }

.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);
}

/* A Bonus Points double face (the engine's die_faces says it's worth 2): a superscript x2 beside
   the letter, inside the die, so the letter stays the resource and the worth reads at a glance.
   The die's aria-label says it in words; the superscript is aria-hidden. */
.die .die-points {
    align-self: flex-start;
    margin-top: 4px;
    font-size: 0.45em;
    letter-spacing: -0.5px;
    line-height: 1;
    pointer-events: none;
}

/* ── 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;
    /* As tall as the board, so the match log (its last section) can fill the column down
       to the board's bottom edge rather than stopping under the Feedback button. */
    align-self: stretch;
    /* 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;
    /* 2px narrower than the other sections each side: the name row (name, H, WP) needs the
       room for a two-digit count to stay on one line beside the name. */
    padding-inline: 10px;
}

#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 (0.64em,
   so ~0.9em corner to corner; the box is no wider, as it shares the name row's width). */
.priority-gem {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 1em;
    height: 1em;
    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%;
}

/* The match log (ui/status.js): the sidebar's last section, filling whatever height the
   panels above leave and scrolling inside itself, newest at the bottom. A turn is a header
   rule, a go a coloured lead-in, an attack a plain line, an effect indented in italics, a
   defeat in bold. The side colours match the two info panels; every line names its
   player or character too, so colour is never the only cue. */
#match-log {
    flex: 1 1 0;
    min-height: 140px;
    display: flex;
    flex-direction: column;
    padding: 8px 10px;
}
#match-log-title {
    margin: 0 0 6px;
    font-size: 0.72em;
    font-weight: 700;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--text-secondary);
}
#match-log-list {
    flex: 1 1 0;
    min-height: 0;
    overflow-y: auto;
    margin: 0;
    padding: 0 2px 0 0;
    list-style: none;
    font-size: 0.8em;
    line-height: 1.3;
    color: var(--text-primary);
}
#match-log-list:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
#match-log-list.is-empty::before {
    content: "Attacks and turns will appear here.";
    color: var(--text-secondary);
    font-style: italic;
}
.ml-entry { padding: 2px 0; overflow-wrap: anywhere; }
.ml-turn {
    margin-top: 8px;
    padding: 2px 0;
    border-top: 1px solid var(--border-color-strong);
    font-weight: 700;
    letter-spacing: 0.04em;
    color: var(--accent-gold);
}
.ml-entry.ml-turn:first-child { margin-top: 0; border-top: none; }
.ml-phase { color: var(--text-secondary); font-size: 0.92em; }
.ml-go { margin-top: 3px; font-weight: 700; }
.ml-go.ml-mine, .ml-attack.ml-mine, .ml-play.ml-mine { border-left: 3px solid var(--accent); padding-left: 6px; }
.ml-go.ml-theirs, .ml-attack.ml-theirs, .ml-play.ml-theirs { border-left: 3px solid var(--accent-danger); padding-left: 6px; }
.ml-effect { padding-left: 9px; font-style: italic; color: var(--text-secondary); }
.ml-defeat { padding-left: 9px; font-weight: 700; color: var(--accent-danger); }
/* Mobile only: the log's newest line, which opens the whole log (see the mobile block). */
#match-log-latest { display: none; }

/* 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: #0d0b0a url('cards/card-back.webp') center / cover no-repeat; /* the printed back */
    border-color: #050404;
    cursor: default;
}
.card.card-back:hover { transform: none; border-color: #050404; }

/* Detailed Tooltip */
.card-tooltip {
    display: none;
    position: absolute;
    bottom: 120%;
    left: 50%;
    transform: translateX(-50%);
    /* rem, never em: the tooltip lives inside its card, and a board card is shrunk twice
       (.card-spot 0.7em, then .card 0.7em), so an em size made the board's tooltips about 70%
       of the hand's, with rules text down near 7px. Sized from the root it reads the same
       wherever the card sits, and still follows the Text size setting. The width grows with
       it so the text doesn't just wrap into a taller column. */
    width: calc(250px * var(--a11y-text-scale));
    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: calc(0.875rem * var(--a11y-text-scale));
    line-height: 1.4;
}

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

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

/* Deck builder: pantheon / type / class filters and sort, between the search box and the list. */
.builder-filters {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 8px 16px;
    margin-bottom: 15px;
    text-align: left;
}
.builder-filters label {
    display: flex;
    align-items: center;
    gap: 6px;
    color: var(--text-secondary, var(--text-primary));
}
.builder-filters 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;
}
/* Deck builder body: the catalogue on the left, the deck being built on the right (the
   MTG Arena layout). Both columns scroll on their own; min-height:0 lets them shrink inside
   the modal's flex column instead of overflowing it. */
.builder-body {
    flex: 1;
    min-height: 0;
    display: flex;
    gap: 16px;
}
.builder-browse {
    flex: 1;
    min-width: 0;
    display: flex;
    flex-direction: column;
}
.builder-deck-panel {
    flex-shrink: 0;
    display: flex;
    flex-direction: column;
    background: rgba(0, 0, 0, 0.2);
    border: 1px solid var(--border-color);
    border-radius: 8px;
    text-align: left;
}
:root[data-theme="light"] .builder-deck-panel,
:root[data-theme="hc-light"] .builder-deck-panel { background: var(--bg-modal-inset); }
.builder-deck-header {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    padding: 10px 12px;
    border-bottom: 1px solid var(--border-color);
}
.builder-deck-header h4 { margin: 0; font-size: 1em; }
.builder-deck-list {
    flex: 1;
    overflow-y: auto;
    padding: 6px 8px 10px;
}
.builder-deck-group {
    margin: 10px 4px 4px;
    font-size: 0.8em;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--text-secondary, var(--text-primary));
}
.builder-deck-group:first-child { margin-top: 4px; }
/* One deck entry: "3x  Name  2CP", striped down the left in its pantheon's colour. The stripe
   is decoration only: the pantheon is also in the row's accessible name and its preview. */
.builder-deck-row {
    display: flex;
    align-items: center;
    gap: 8px;
    width: 100%;
    margin: 3px 0;
    padding: 6px 8px;
    background: var(--bg-button);
    color: var(--text-primary);
    border: 1px solid var(--border-color);
    border-left: 5px solid var(--pantheon-stripe, var(--border-color));
    border-radius: 4px;
    font: inherit;
    text-align: left;
    cursor: pointer;
}
.builder-deck-row:hover { background: var(--bg-button-hover); }
.builder-deck-row[data-pantheon="Egyptian"] { --pantheon-stripe: #d4a017; }
.builder-deck-row[data-pantheon="Greek"]    { --pantheon-stripe: #3f7fd9; }
.builder-deck-row[data-pantheon="Norse"]    { --pantheon-stripe: #5fa05a; }
.builder-deck-row[data-pantheon="Shinto"]   { --pantheon-stripe: #d9443a; }
.builder-deck-qty { min-width: 2.2em; font-weight: bold; }
.builder-deck-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.builder-deck-cost { font-size: 0.85em; white-space: nowrap; }
.builder-deck-empty { color: var(--text-secondary, var(--text-primary)); text-align: center; margin: 24px 8px; }
/* The panel narrows with the window, and goes away entirely where there isn't room for it
   beside the list: on the mobile layout and on narrow desktop windows. "In deck only" is the
   way to review the deck there, and the name row's compact count takes over the total. */
.builder-deck-panel { width: clamp(220px, 24vw, 320px); }
.builder-count-compact { display: none; white-space: nowrap; }
@media (max-width: 860px) {
    .builder-deck-panel { display: none; }
    .builder-count-compact { display: inline; }
}
html[data-layout="mobile"] .builder-deck-panel { display: none; }
html[data-layout="mobile"] .builder-count-compact { display: inline; }
.builder-in-deck-toggle { cursor: pointer; }
.builder-in-deck-toggle input { margin: 0; cursor: pointer; }
/* The list used to be a hardcoded rgba(0,0,0,0.2) wash, which turns the light themes' white
   into #ccc and drops the copy-count colours below AA there. Dark themes keep the wash. */
#builder-card-list { background: rgba(0, 0, 0, 0.2); }
:root[data-theme="light"] #builder-card-list,
:root[data-theme="hc-light"] #builder-card-list { background: var(--bg-modal-inset); }
.builder-empty { color: var(--text-secondary, var(--text-primary)); text-align: center; margin: 20px 0; }
/* Copy-count heat (renderBuilderCardList sets data-heat). Reuses the palette accents, all
   checked against the list background: >= 4.5:1 in dark/light, >= 7:1 in hc-dark/hc-light. */
.builder-qty[data-heat="0"] { color: var(--text-primary); }
.builder-qty[data-heat="1"] { color: var(--accent-gold); }
.builder-qty[data-heat="2"] { color: var(--accent-warn); }
.builder-qty[data-heat="3"] { color: var(--accent-danger); font-weight: bold; }
/* hc-light's gold and warn are both near-black browns; push orange toward red so the steps
   stay distinguishable (#933500: 7.6:1 on white). */
:root[data-theme="hc-light"] .builder-qty[data-heat="2"] { color: #933500; }
.builder-shown-count {
    margin-left: auto;
    color: var(--text-secondary, var(--text-primary));
    font-size: 0.9em;
}

.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;
}

/* Keyword glossary (KEYWORD_HELP in script.js): each keyword, then one line saying what it
   does. The dt reuses .tooltip-keywords' look; only the spacing changes so a term sits
   directly on its definition. */
.tooltip-glossary {
    margin: 0 0 8px;
}

.tooltip-glossary dt.tooltip-keywords {
    margin: 0;
}

.tooltip-glossary dd {
    margin: 1px 0 6px;
    font-size: 0.75em;
    line-height: 1.3;
    color: var(--text-secondary);
}

.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-dmg-mod {
    margin-left: 0.3em;
    padding: 0 0.35em;
    border-radius: 0.6em;
    border: 1px solid var(--accent-success);
    color: var(--accent-success);
}
.attack-dmg-mod.is-negative { border-color: var(--accent-danger); color: var(--accent-danger); }
.stat-dmg-mod { margin: 0 auto 0 0.4em; font-size: 0.85em; font-weight: 700; white-space: nowrap; }
.attack-dmg-source { font-size: 0.75em; color: var(--accent-success); }

.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;
}

/* Deck builder steppers (-, +) and the Inspect magnifier. A fixed 30px box with the generic
   button's 16px side padding left no room for the glyph, which overflowed to the right. */
.builder-step-btn {
    width: 30px;
    padding: 8px 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    line-height: 1;
}
.builder-inspect-btn svg { width: 13px; height: 13px; }

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;
}


#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;
    user-select: none;
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* Auto-fade (see "Status line auto-fade" in ui/status.js). visibility is delayed until the
   fade has finished, and the region itself stops taking pointer events so the cards under its
   box are reachable. The controller hints share the region but are a reference, so they stay. */
#status-message {
    transition: opacity 0.6s ease, visibility 0s linear 0s;
}
#action-history.log-faded { pointer-events: none; }
#action-history.log-faded #status-message {
    opacity: 0;
    visibility: hidden;
    transition: opacity 0.6s ease, visibility 0s linear 0.6s;
}
html[data-animations="off"] #status-message { transition: none !important; }

/* Music / sound-effect mutes, side by side under the hand sort. A muted button draws a slash
   through its icon (and hides the speaker's waves): the second channel besides colour. */
#audio-toggles {
    display: flex;
    gap: 6px;
    margin-top: 6px;
}
.audio-toggle-btn {
    flex: 1 1 0;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 4px 0;
    background: var(--bg-button);
    color: var(--text-secondary);
    border: 1px solid var(--border-color-strong);
    border-radius: 8px;
    cursor: pointer;
}
.audio-toggle-btn svg { width: 18px; height: 18px; }
.audio-toggle-btn:hover {
    background: var(--bg-button-hover);
    border-color: var(--accent);
    color: var(--text-primary);
}
.audio-toggle-btn .audio-slash { display: none; }
.audio-toggle-btn[aria-pressed="true"] { opacity: 0.65; }
.audio-toggle-btn[aria-pressed="true"] .audio-slash { display: inline; }
.audio-toggle-btn[aria-pressed="true"] .audio-waves { display: none; }
html[data-layout="mobile"] .audio-toggle-btn { padding: 2px 0; }
html[data-layout="mobile"] .audio-toggle-btn svg { width: 14px; height: 14px; }

/* Sounds modal (Settings > Sounds). */
.sounds-modal-content { width: 380px; }
.sounds-modal-content .settings-label { flex: 0 0 9.5em; }
.sounds-modal-content .settings-row input[type="range"] { flex: 1 1 auto; min-width: 0; accent-color: var(--accent); }
.volume-readout {
    width: 3.2em;
    text-align: right;
    color: var(--text-secondary);
    font-variant-numeric: tabular-nums;
}
.music-rotation {
    margin: 12px 0 0;
    padding: 6px 10px 8px;
    border: 1px solid var(--border-color);
    border-radius: 6px;
}
.music-rotation legend { color: var(--text-secondary); padding: 0 4px; }
.music-now-playing { margin: 2px 0 6px; font-size: 0.85em; color: var(--text-secondary); min-height: 1.2em; }
#music-track-list {
    list-style: none;
    margin: 0;
    padding: 0;
    max-height: 240px;
    overflow-y: auto;
}
#music-track-list li label {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 3px 0;
    cursor: pointer;
}
#music-track-list li.now-playing label { color: var(--accent); }
#music-track-list li.now-playing label::after { content: "♪"; margin-left: auto; }
#music-track-list input { accent-color: var(--accent); }
.sound-credits {
    margin: 12px 0 0;
    font-size: 0.85em;
    color: var(--text-secondary);
    text-align: center;
    line-height: 1.5;
}
.sound-credits b { color: var(--text-primary); }
.sound-credits a { color: var(--accent); }


/* 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;
}

/* The new-game menu fades in rather than appearing in one frame: it opens straight after the
   splash's exit, where a hard cut undid the whole transition. On the overlay, so backdrop and
   panel fade together, and NOT on .modal-content, whose animation is the click-outside nudge
   (ui/dismissal.js). Leaving display:none restarts it, so it plays on every showing. */
#deck-modal:not(.hidden) { animation: deck-modal-in 500ms ease-out; }
@keyframes deck-modal-in { from { opacity: 0; } }
html[data-animations="off"] #deck-modal:not(.hidden) { animation: none; }

#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-hint a { color: var(--accent); }

.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;
}

/* ── Deck list viewer (features/deck-viewer.js) ──────────────────────────
   Your own deck's every card. In hand / in play is outlined in the accent colour, graveyard
   and sealed-away cards are greyed; the caption under each card says which in words too. */
.deck-viewer-content {
    width: min(92vw, 1100px);
    max-height: 85vh;
    display: flex;
    flex-direction: column;
}
.deck-viewer-disclosure {
    margin: 0 0 10px;
    font-size: 0.85em;
    font-style: italic;
    color: var(--text-secondary);
}
.deck-viewer-toolbar {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: 8px 16px;
    margin-bottom: 10px;
}
.deck-viewer-toolbar label {
    display: flex;
    align-items: center;
    gap: 6px;
    color: var(--text-secondary);
}
.deck-viewer-toolbar 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;
}
.deck-viewer-summary {
    margin: 0;
    font-size: 0.85em;
    color: var(--text-secondary);
}
.deck-viewer-list {
    flex: 1;
    min-height: 0;
    overflow-y: auto;
    display: flex;
    flex-wrap: wrap;
    gap: 12px;
    justify-content: center;
    padding: 10px;
}
.deck-viewer-entry {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 4px;
}
.deck-viewer-zone {
    font-size: 0.75em;
    color: var(--text-secondary);
}
.deck-viewer-live .card {
    outline: 3px solid var(--accent);
    outline-offset: 2px;
    box-shadow: 0 0 12px var(--accent);
}
.deck-viewer-live .deck-viewer-zone {
    color: var(--accent);
    font-weight: bold;
}
.deck-viewer-spent .card {
    filter: grayscale(1) brightness(0.6);
    opacity: 0.65;
}
.deck-viewer-spent .card:hover,
.deck-viewer-spent .card:focus-visible {
    opacity: 0.9;
}
.deck-viewer-loading {
    width: 100%;
    min-height: 240px;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 12px;
    color: var(--text-secondary);
}
.deck-viewer-spinner {
    width: 36px;
    height: 36px;
    border: 4px solid var(--border-color);
    border-top-color: var(--accent);
    border-radius: 50%;
    animation: deck-viewer-spin 0.8s linear infinite;
}
@keyframes deck-viewer-spin { to { transform: rotate(360deg); } }
/* Animations off: a static ring, the text still says it's loading. (Reduced motion: the
   global net.) */
html[data-animations="off"] .deck-viewer-spinner { animation: none; }
.deck-viewer-empty {
    color: var(--text-secondary);
    padding: 20px;
}

#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: 420px;
    max-width: calc(100vw - 32px);
    max-height: 90vh;
    overflow-y: auto;
    box-sizing: border-box;
}

/* ── Match summary (renderMatchSummary in script.js) ─────────────────────────
   Player 0 is always "you" (the WP rows list you first). The two lines differ by dash as
   well as colour, so neither theme nor colour vision decides whether they can be told apart. */
#game-over-summary {
    margin-top: 14px;
    text-align: left;
}

.match-summary-chart {
    margin: 0;
}

.wp-chart {
    display: block;
    width: 100%;
    height: auto;
}

.wp-chart .wp-line {
    fill: none;
    stroke-width: 2.5;
    stroke-linejoin: round;
    stroke-linecap: round;
}

.wp-chart .wp-line-0 { stroke: var(--accent-gold); }
.wp-chart .wp-line-1 { stroke: var(--text-secondary); stroke-dasharray: 6 4; }
.wp-chart .wp-dot.wp-line-0 { fill: var(--accent-gold); }
.wp-chart .wp-dot.wp-line-1 { fill: var(--text-secondary); stroke-dasharray: none; }

.wp-chart .wp-axis {
    stroke: var(--border-color-strong);
    stroke-width: 1;
}

.wp-chart .wp-goal {
    stroke: var(--accent-success);
    stroke-width: 1;
    stroke-dasharray: 2 3;
}

.wp-chart .wp-tick {
    fill: var(--text-muted);
    font-size: 10px;
}

.wp-legend {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 4px 14px;
    margin-top: 4px;
    font-size: 0.8em;
    color: var(--text-secondary);
}

.wp-swatch {
    display: inline-block;
    width: 18px;
    margin-right: 5px;
    vertical-align: middle;
    border-top: 3px solid var(--accent-gold);
}

.wp-swatch.wp-line-1 { border-top: 3px dashed var(--text-secondary); }
.wp-swatch.wp-goal { border-top: 2px dotted var(--accent-success); }

.match-summary-lead {
    margin: 8px 0 10px;
    text-align: center;
    font-size: 0.9em;
    color: var(--text-primary);
}

.match-stats-table {
    width: 100%;
    border-collapse: collapse;
    font-size: 0.85em;
}

.match-stats-table th,
.match-stats-table td {
    padding: 4px 6px;
    border-bottom: 1px solid var(--border-color);
}

.match-stats-table thead th {
    color: var(--text-secondary);
    font-weight: bold;
    text-align: right;
    overflow-wrap: anywhere;
}

.match-stats-table tbody th {
    font-weight: normal;
    color: var(--text-secondary);
    text-align: left;
}

.match-stats-table td {
    text-align: right;
    font-variant-numeric: tabular-nums;
    font-weight: bold;
}

/* The per-kind breakdown under "Damage dealt": indented, and lighter than the totals. */
.match-stats-table tr.match-stats-sub th { padding-left: 1.4em; }
.match-stats-table tr.match-stats-sub td { font-weight: normal; }

.match-summary-highlights {
    margin: 10px 0 0;
    padding-left: 18px;
    font-size: 0.85em;
    color: var(--text-secondary);
}

.match-summary-highlights li + li {
    margin-top: 3px;
}

#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-rematch-status {
    margin: 12px 0 0;
    text-align: center;
    font-weight: bold;
    color: var(--text-primary);
}

.game-over-community {
    margin: 16px 0 0;
    font-size: 0.85em;
    text-align: center;
    color: var(--text-secondary);
}
.game-over-community a { white-space: nowrap; }   /* wrap before the name, never inside it */
.game-over-community a,
#howto-friend a { color: var(--accent); }

#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-sealed  { color: #c9a8ff; 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;
}

/* The Field chip (order 2, #field-zone below) goes between the two active rows. */
html[data-layout="mobile"] #opponent-active-row { order: 1; }
html[data-layout="mobile"] #player-active-row   { order: 3; }

/* #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%;
}
/* ── Match log: one line on a phone ─────────────────────────────────────────
   The list (a fixed six-line box here, before) moves into #match-log-modal (applyLayout), and
   the section is just #match-log-latest: the newest entry, cut to one line, the whole row a
   button that opens the full log. Side colour as in the list (.ml-mine / .ml-theirs, copied by
   updateMatchLogLatest); the text names the side too. */
html[data-layout="mobile"] #match-log {
    flex: 0 0 auto;
    min-height: 0;
    padding: 0;
}
html[data-layout="mobile"] #match-log-title { display: none; } /* still the section's name */
html[data-layout="mobile"] #match-log-latest {
    display: flex;
    align-items: center;
    gap: 8px;
    width: 100%;
    min-height: 40px;
    padding: 6px 12px;
    background: none;
    border: none;
    border-radius: inherit;
    color: var(--text-primary);
    font: inherit;
    font-size: 0.85em;
    text-align: left;
    cursor: pointer;
}
html[data-layout="mobile"] #match-log-latest.ml-mine { box-shadow: inset 3px 0 0 var(--accent); }
html[data-layout="mobile"] #match-log-latest.ml-theirs { box-shadow: inset 3px 0 0 var(--accent-danger); }
.ml-latest-label {
    flex: 0 0 auto;
    font-size: 0.78em;
    font-weight: 700;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--text-secondary);
}
.ml-latest-text {
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.ml-latest-chevron {
    flex: 0 0 auto;
    font-size: 1.4em;
    line-height: 1;
    color: var(--text-secondary);
}
/* The dialog: the list scrolls inside it, newest at the bottom, as in the sidebar. */
.match-log-modal-content {
    display: flex;
    flex-direction: column;
}
#match-log-modal-body {
    display: flex;
    flex-direction: column;
    min-height: 0;
    flex: 1 1 auto;
}
#match-log-modal #match-log-list {
    flex: 0 1 auto;
    max-height: 60vh;
    text-align: left;
    font-size: 0.9em;
}

/* ── Deck and graveyard, inside each player's info panel ────────────────────
   Relocated out of the board rows by applyLayout() (Mitch, 2026-11-09: the panels have room
   to spare beside the CP/MP/AP row, and the four pile strips they used to sit in cost about
   180px of scrolling). The panel becomes a grid: the name row across the top, then deck |
   points | graveyard. The middle track may shrink (its chips wrap) but the piles keep their
   size, so a token badge or a large text size can't push a pile out of the panel.
   .panel-piles is display:contents, so its two spots are grid items of the panel itself; it
   still exists as one nav row (a11y/nav.js). */
html[data-layout="mobile"] :is(#opponent-info-panel, #player-info-panel) {
    display: grid;
    grid-template-columns: 1fr minmax(0, auto) 1fr;
    grid-template-areas:
        "stats stats stats"
        "deck  res   gy";
    align-items: center;
    column-gap: 6px;
    row-gap: 6px;
}
html[data-layout="mobile"] :is(#opponent-stats, #player-stats) { grid-area: stats; }
html[data-layout="mobile"] :is(#opponent-resource-bar, #resource-bar) { grid-area: res; }
html[data-layout="mobile"] :is(#opponent-info-panel, #player-info-panel) .resource-chip-row { margin-top: 0; }
html[data-layout="mobile"] .panel-piles { display: contents; }
html[data-layout="mobile"] .panel-piles > .deck-spot { grid-area: deck; justify-self: start; }
html[data-layout="mobile"] .panel-piles > .graveyard-spot { grid-area: gy; justify-self: end; }

/* The piles themselves: small chips rather than card-sized slots (they carry no position).
   The graveyard's sealed-away count goes on a second line, which keeps both chips narrow. */
html[data-layout="mobile"] .panel-piles > .card-spot {
    width: auto;
    height: auto;
    min-height: 36px;
    box-sizing: border-box;
    padding: 3px 8px;
    border-radius: 6px;
    font-size: 0.62em;
    line-height: 1.2;
}
html[data-layout="mobile"] .panel-piles > .deck-spot {
    display: grid;
    grid-template-columns: auto auto;
    grid-template-areas: "back title" "back count";
    align-items: center;
    column-gap: 6px;
    row-gap: 0;
}
html[data-layout="mobile"] .panel-piles > .deck-spot > .spot-title { grid-area: title; }
html[data-layout="mobile"] .panel-piles > .deck-spot > .spot-count { grid-area: count; font-size: 1.35em; }
/* "OPP DECK" on the desktop board; inside the opponent's own panel it's just their deck. */
html[data-layout="mobile"] .panel-piles .opp-prefix { display: none; }
/* A deck chip leads with a thumbnail of the printed card back. */
html[data-layout="mobile"] .panel-piles > .deck-spot::before {
    content: '';
    grid-area: back;
    width: 1.55em;
    height: 2.2em;
    border-radius: 2px;
    background: #0d0b0a url('cards/card-back.webp') center / cover no-repeat;
    box-shadow: 0 0 0 1px #050404;
}
html[data-layout="mobile"] .panel-piles > .graveyard-spot {
    display: grid;
    grid-template-columns: auto auto;
    grid-template-areas: "title count" "sub sub";
    align-items: baseline;
    column-gap: 5px;
    row-gap: 0;
}
html[data-layout="mobile"] .panel-piles > .graveyard-spot > .spot-title { grid-area: title; }
html[data-layout="mobile"] .panel-piles > .graveyard-spot > .spot-count { grid-area: count; font-size: 1.35em; }
html[data-layout="mobile"] .panel-piles > .graveyard-spot > .spot-sub { grid-area: sub; }

/* 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 ─────────────────────────────────────────
   #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: 2; /* between the two 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;
}

/* Hot-seat's other hand keeps the full 72px size rather than the viewport-derived --slot-w: the
   strip scrolls, so nothing has to fit across the width. */
html[data-layout="mobile"] #opponent-hand .card {
    flex: 0 0 auto;
    width: 72px;
    height: 100px;
}

/* ── Your hand on a phone: a carousel ───────────────────────────────────────
   72px cards were too small to read without tapping (Mitch, 2026-11-09). Bigger cards,
   overlapping, standing on the bottom of the tray, and the one in the middle of the strip
   bigger again and on top. Swipe to bring another to the middle; scroll-snap settles on one.
   render/hand.js sets two numbers on each card as the strip scrolls: --hand-focus (1 in the
   middle, falling to 0 one card away) and --hand-z (nearer the middle stacks higher). The
   enlargement is the `scale` property, not `transform`, which hand-motion.js's slide and the
   hover lift already use. The first and last cards get half a strip of margin so either end
   can reach the middle; the tray's padding-top is the room the middle card grows into. */
html[data-layout="mobile"] #hand {
    --hand-card-h: 132px;
    --hand-card-w: calc((var(--hand-card-h) - 4px) * 900 / 1302 + 4px); /* the art face's ratio */
    --hand-grow: 0.38;
    --hand-overlap: 0.3;
    position: relative;   /* hand.js measures the cards' offsetLeft against the strip */
    isolation: isolate;   /* --hand-z stacks the cards among themselves, not over the page */
    align-items: flex-end;
    gap: 0;
    min-height: 0;
    padding: calc(var(--hand-card-h) * var(--hand-grow) + 6px) 0 8px;
    scroll-snap-type: x mandatory;
    scrollbar-width: none;
}
html[data-layout="mobile"] #hand::-webkit-scrollbar { display: none; }

html[data-layout="mobile"] #hand > .card {
    flex: 0 0 auto;
    box-sizing: border-box;
    width: var(--hand-card-w);
    height: var(--hand-card-h);
    /* The classic face is em-sized: grow its text with the card (it was 100px tall). */
    font-size: calc(0.62em * 1.32 / var(--a11y-text-scale));
    scroll-snap-align: center;
    transform-origin: 50% 100%;
    scale: calc(1 + var(--hand-grow) * var(--hand-focus, 0));
    z-index: var(--hand-z, 1);
}
html[data-layout="mobile"] #hand > .card:not(.pending-magic-source, .target-highlight) {
    box-shadow: 0 2px 10px rgba(0, 0, 0, 0.6);
}
html[data-layout="mobile"] #hand > .card + .card {
    margin-left: calc(var(--hand-card-w) * var(--hand-overlap) * -1);
}
html[data-layout="mobile"] #hand > .card:first-child { margin-left: calc(50% - var(--hand-card-w) / 2); }
html[data-layout="mobile"] #hand > .card:last-child { margin-right: calc(50% - var(--hand-card-w) / 2); }

/* The Sort button and the two mutes sit in the tray's top corners, over the room the middle
   card grows into, so the hand gets the whole width and the tray no extra height. The cards
   either side of the middle stand lower than the buttons. */
html[data-layout="mobile"] #hand-container {
    position: relative;
    min-height: 0;
    margin-bottom: 0;
}
html[data-layout="mobile"] #hand-controls {
    position: absolute;
    top: 6px;
    left: 6px;
    right: 6px;
    z-index: 2;
    flex-direction: row;
    justify-content: space-between;
    align-items: center;
    padding: 0;
    background: none;
    box-shadow: none;
    pointer-events: none;   /* the strip between the two ends is the hand's */
}
html[data-layout="mobile"] #hand-controls > * { pointer-events: auto; }
html[data-layout="mobile"] #hand-sort-btn {
    min-width: 0;
    padding: 4px 10px;
    font-size: 0.7em;
    line-height: 1.3;
}
html[data-layout="mobile"] #hand-sort-btn b {
    display: inline;
    font-size: 1.1em;
}
html[data-layout="mobile"] #audio-toggles { margin-top: 0; }
html[data-layout="mobile"] #hand-controls .audio-toggle-btn {
    flex: 0 0 auto;
    width: 34px;
    height: 28px;
}

/* 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;
}


/* ═══════════════════════════════════════════════════════════════════════════════
   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
   showCardPreview 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;
}

/* The card hover/focus preview (showCardPreview in script.js): one body-level element placed
   beside the hovered card, on whichever side has the most room. It used to be a child of each
   card, which #main-layout's overflow:hidden clipped wherever a board card's preview ran past
   the board (into the hand tray). left/top/transform are set inline; the origin is the top-left
   corner so a shrink-to-fit scale keeps the position the script computed. Same stacking as the
   floating builder tooltip, for the same reason: it must clear every dialog, and
   pointer-events:none means it can never block anything. */
.card-tooltip.card-preview-float {
    position: fixed;
    bottom: auto;
    transform-origin: 0 0;
    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-"] #title-sky {
    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 {
    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;
}

/* ── Cosmos theme (data-theme="cosmos") ───────────────────────────────────────
   The palette is up top with the others. What's here: the sky, the splash, and the frosted
   panels. Nothing here may select .card or anything inside one: the cards must look exactly
   like the printed ones in every theme.

   #cosmos-bg is fixed at z-index -1, so it paints above the root background and below all
   in-flow content. That only works while nothing between it and the root paints a
   background, which is why the page colour moves from body to html in this theme. Its own
   background is the no-WebGL sky: a still approximation of the poster's band, plus two tiled
   layers of star dots; cosmos.js's canvas fades in over it once it has drawn. */
#cosmos-bg { display: none; }

:root[data-theme="cosmos"] { background: var(--bg-page); }
:root[data-theme="cosmos"] body { background: transparent; }

:root[data-theme="cosmos"] #cosmos-bg {
    display: block;
    position: fixed;
    inset: 0;
    z-index: -1;
    overflow: hidden;
    pointer-events: none;
    background:
        radial-gradient(ellipse 20% 9% at 28% 47%, rgba(232, 84, 218, 0.42), transparent 70%),
        radial-gradient(ellipse 16% 8% at 71% 54%, rgba(236, 96, 226, 0.36), transparent 70%),
        radial-gradient(ellipse 26% 11% at 54% 43%, rgba(92, 64, 226, 0.32), transparent 70%),
        radial-gradient(ellipse 34% 10% at 12% 55%, rgba(150, 40, 200, 0.30), transparent 72%),
        radial-gradient(ellipse 34% 10% at 90% 46%, rgba(150, 40, 200, 0.30), transparent 72%),
        radial-gradient(ellipse 75% 24% at 50% 50%, rgba(104, 26, 172, 0.50), transparent 75%),
        #06030d;
}
:root[data-theme="cosmos"] #cosmos-bg::before,
:root[data-theme="cosmos"] #cosmos-bg::after {
    content: '';
    position: absolute;
    inset: 0;
}
:root[data-theme="cosmos"] #cosmos-bg::before {
    background-image:
        radial-gradient(1px 1px at 23px 41px, rgba(255, 255, 255, 0.8), transparent),
        radial-gradient(1px 1px at 97px 13px, rgba(255, 255, 255, 0.55), transparent),
        radial-gradient(1px 1px at 141px 88px, rgba(255, 255, 255, 0.7), transparent),
        radial-gradient(1px 1px at 61px 127px, rgba(255, 255, 255, 0.5), transparent),
        radial-gradient(1px 1px at 159px 151px, rgba(255, 255, 255, 0.6), transparent),
        radial-gradient(1px 1px at 12px 166px, rgba(255, 240, 255, 0.65), transparent);
    background-size: 173px 173px;
}
:root[data-theme="cosmos"] #cosmos-bg::after {
    background-image:
        radial-gradient(1.5px 1.5px at 51px 71px, rgba(255, 255, 255, 0.9), transparent),
        radial-gradient(1.5px 1.5px at 203px 19px, rgba(230, 236, 255, 0.75), transparent),
        radial-gradient(2px 2px at 139px 233px, rgba(255, 210, 250, 0.8), transparent),
        radial-gradient(1.5px 1.5px at 241px 167px, rgba(255, 255, 255, 0.7), transparent);
    background-size: 263px 263px;
}
:root[data-theme="cosmos"] #cosmos-bg > canvas {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    z-index: 1;   /* over the CSS stars */
    opacity: 0;
    transition: opacity 600ms ease-out;
}
:root[data-theme="cosmos"] #cosmos-bg.cosmos-ready > canvas { opacity: 1; }

/* The splash keeps its gold clouds in this theme too. As they fall away (the splash exit,
   above), the sky slides down from above and fades in behind them, settling as the camera
   slows: cosmos.js's playCosmosArrival adds the class, and its ARRIVAL_MS is this delay plus
   duration. The fill is both-ways so it is invisible during the delay and rests at none. */
:root[data-theme="cosmos"] #cosmos-bg.cosmos-arriving {
    animation: cosmos-arrive 1500ms cubic-bezier(0.16, 1, 0.3, 1) 250ms both;
}
@keyframes cosmos-arrive {
    from { opacity: 0; transform: translateY(-40vh) scale(1.06); }
    to   { opacity: 1; transform: none; }
}

/* The rest is chrome, and is written :where(:root[data-theme="cosmos"]) so the theme adds no
   specificity: each rule beats only the plain base rule it restyles (by coming later), and
   every more specific rule still wins as before: a button's own transparent or accent fill,
   the focus ring's box-shadow, a row's highlight state, an info panel's glow.

   The board: its aurora layers give way to the sky, and the panels become frosted glass with
   a faint starlight rim. The fills are the palette's translucent tokens; there is no
   backdrop-filter, since a blur over a canvas that redraws 30 times a second re-blurs every
   frame. */
:where(:root[data-theme="cosmos"]) .field-parallax-layer { display: none; }
:where(:root[data-theme="cosmos"]) .side-panel-section,
:where(:root[data-theme="cosmos"]) .field-row {
    box-shadow: inset 0 0 0 1px rgba(160, 139, 214, 0.22), 0 0 24px rgba(8, 4, 20, 0.5);
}
:where(:root[data-theme="cosmos"]) .field-spot {
    background: linear-gradient(rgba(255, 204, 0, 0.05), rgba(255, 204, 0, 0.05)), var(--row-bg);
}
:where(:root[data-theme="cosmos"]) #player-area,
:where(:root[data-theme="cosmos"]) #opponent-hand-container {
    border-color: rgba(160, 139, 214, 0.45);
}
:where(:root[data-theme="cosmos"]) #page-footer { border-top-color: rgba(160, 139, 214, 0.35); }

/* Buttons: a soft top sheen on the violet face. Only background-image, so any button with its
   own background (transparent, accent, an inline style) drops it through its own shorthand. */
:where(:root[data-theme="cosmos"]) button {
    background-image: linear-gradient(180deg, rgba(255, 255, 255, 0.08), rgba(255, 255, 255, 0) 70%);
}
:where(:root[data-theme="cosmos"]) button:hover {
    background-image: linear-gradient(180deg, rgba(255, 255, 255, 0.14), rgba(255, 255, 255, 0) 70%);
}

/* Modals: the nebula stays faintly visible behind the dim, and each dialog is a dark panel
   with a violet rim and glow and gold Cinzel headings, after the poster's lettering. */
:where(:root[data-theme="cosmos"]) .modal { background: rgba(5, 2, 14, 0.78); }
:where(:root[data-theme="cosmos"]) .modal-content {
    background: linear-gradient(180deg, #1a1033 0%, var(--bg-modal-content) 45%, #0e0820 100%);
    border: 1px solid rgba(176, 140, 255, 0.5);
    box-shadow: 0 0 0 1px rgba(10, 5, 25, 0.9), 0 0 42px rgba(150, 70, 255, 0.28), 0 18px 50px rgba(0, 0, 0, 0.6);
}
:where(:root[data-theme="cosmos"]) .modal-content > h2,
:where(:root[data-theme="cosmos"]) .modal-content > h3 {
    font-family: 'Cinzel', Georgia, serif;
    font-weight: 700;
    letter-spacing: 0.06em;
    color: var(--accent-gold);
    text-shadow: 0 0 14px rgba(245, 199, 78, 0.25);
}
:where(:root[data-theme="cosmos"]) .context-menu {
    border-color: rgba(176, 140, 255, 0.5);
    box-shadow: 0 0 24px rgba(150, 70, 255, 0.25), 0 8px 24px rgba(0, 0, 0, 0.55);
}

@media (prefers-reduced-motion: reduce) {
    :root[data-theme="cosmos"] #cosmos-bg > canvas { transition: none; }
}

/* ── 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,
    #title-sky {
        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;
}

/* 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;
}

/* Every control in the Settings menu is the same size, buttons and selects alike, and all
   plain gray: the on/off state is in the button's own text ("Animations: On"). */
.settings-row > button,
.settings-row > select {
    flex: 0 0 170px;
    width: 170px;
    box-sizing: border-box;
}
/* A read-only value (Current ruleset), in the same column as the controls */
.settings-row > .settings-value {
    flex: 0 0 170px;
    text-align: center;
    color: var(--text-primary);
}
/* Settings > Sounds: one full-width button, no label beside it. */
.settings-row > button.settings-row-wide {
    flex: 1 1 auto;
    width: 100%;
}

/* 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 Discord invite would take the booth's only browser tab off the game. */
html[data-kiosk="on"] .discord-link { 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; }

/* ── Art cards (2026-10-07; matched to the printed Alpha Set 2026-10-20) ────────
   The printed-card face built by cardFaceHtml (render/card-face.js): the pantheon frame, the
   art behind its transparent oval, cost in the gem, rules text on the parchment. Settings >
   Cards switches back to the classic text face (html[data-card-style="classic"]); only art
   mode emits this markup, so nothing here needs that attribute as a guard.

   The reference is the Alpha Set print sheet (825x1125 per card with bleed; the card itself
   is the 674x974 inside it, which is the frame art's own 900x1302 ratio). Every position below
   was measured off those faces and is in % of the face, and every font size is cqw (the face
   is a size container), so one layout serves a 72px mobile slot and the Inspect dialog alike.
   The face keeps that ratio in any box (see .card.art-card > .card-face).

   Type, as printed: Cinzel for the name, class, HP, type line, Class 3 badge and footer; Old
   Standard TT for the cost and all rules text. Both are local WOFF2s (Latin subset): every
   binary in this repo has to stay under ~100KB or the Hugging Face push is rejected
   (2026-10-08). Re-subset from the OFL sources if a card ever needs a character outside that. */
@font-face {
    font-family: 'Cinzel';
    src: url('fonts/Cinzel-Variable.woff2') format('woff2');
    font-weight: 400 900; font-style: normal; font-display: swap;
}
@font-face {
    font-family: 'Old Standard TT';
    src: url('fonts/OldStandardTT-Regular.woff2') format('woff2');
    font-weight: 400; font-style: normal; font-display: swap;
}
@font-face {
    font-family: 'Old Standard TT';
    src: url('fonts/OldStandardTT-Bold.woff2') format('woff2');
    font-weight: 700; font-style: normal; font-display: swap;
}

/* The box itself takes the printed card's shape, --card-h tall. With .card's content-box
   sizing plus its 5px padding it came out 114x154 (hand) or, squeezed by a 100px board slot,
   100x154: the ratio-locked face then letterboxed inside it, a fat black bar either side in
   the hand and above the face on the board. border-box makes the padding irrelevant (the face
   is absolutely positioned on the padding box), so only the 2px border shows. */
.card.art-card {
    box-sizing: border-box;
    width: calc((var(--card-h) - 4px) * 900 / 1302 + 4px); /* 4px: the two 2px borders */
    background: #0d0b0a;
    border-color: #050404;
    border-radius: 8px;
}
/* The face keeps the printed card's ratio whatever box it's in (a context that sizes the card
   itself, like a phone's 72x100 hand card): it fills the box's height (or width, if that runs
   out first) and is centred, so any mismatch shows as a sliver of the box's black background,
   like the printed card's own black edge. The overlays still cover the whole box. */
.card.art-card > .card-face {
    position: absolute;
    inset: 0;
    margin: auto;
    width: auto;
    height: auto;
    max-width: 100%;
    max-height: 100%;
    aspect-ratio: 900 / 1302;
}
.card.art-card > .cf-overlay {
    position: absolute;
    inset: 0;
}

.card-face {
    container-type: size;
    position: relative;
    overflow: hidden;
    border-radius: 6px;
    background: #0d0b0a;
    color: #1d1a16;
    font-family: 'Old Standard TT', 'Iowan Old Style', 'Palatino Linotype', Georgia, serif;
    text-align: left;
    line-height: 1.15;
    pointer-events: none; /* the card element owns every interaction */
}
.card-face > * { position: absolute; }

.cf-art {
    left: 3.6%; top: 9%; width: 93.3%; height: 42%;
    background: #3a2a2e url('cards/art-placeholder.svg') center / cover no-repeat;
}
.cf-frame {
    inset: 0;
    background: center / 100% 100% no-repeat;
}
.frame-egyptian .cf-frame { background-image: url('cards/frame-egyptian.webp'); }
.frame-greek    .cf-frame { background-image: url('cards/frame-greek.webp'); }
.frame-norse    .cf-frame { background-image: url('cards/frame-norse.webp'); }
.frame-shinto   .cf-frame { background-image: url('cards/frame-shinto.webp'); }
/* No pantheon (nothing in Cards.json today): the Shinto stone frame, desaturated. */
.frame-neutral  .cf-frame { background-image: url('cards/frame-shinto.webp'); filter: grayscale(1); }

/* Play cost, centred on the frame's gem: CP for a character/relic/field, MP for magic. */
.cf-cost {
    left: 8.7%; top: 6.3%;
    transform: translate(-50%, -50%);
    font-weight: 400;
    font-size: 10.5cqw;
    line-height: 1;
    color: #fff;
    text-shadow: 0.02em 0.04em 0.06em rgba(0, 0, 0, 0.85), 0 0 0.12em rgba(0, 0, 0, 0.6);
}

/* Name, class and HP: white Cinzel with a soft drop shadow, as printed. */
.cf-title,
.cf-hp {
    text-shadow: 0.05em 0.07em 0.08em rgba(0, 0, 0, 0.9), 0 0 0.18em rgba(0, 0, 0, 0.55);
}
.cf-title {
    left: 17.8%; right: 1.5%; top: 1.1%;
    color: #fff;
    font-family: 'Cinzel', Georgia, serif;
    font-weight: 700;
    white-space: nowrap;
}
.cf-name {
    font-size: calc(5.6cqw * var(--name-fit, 1));
    line-height: 1.02;
    min-height: 6.2cqw; /* a shrunk name keeps the Class line where it was */
    overflow: hidden;
    text-overflow: ellipsis;
}
.cf-class { font-size: 4.55cqw; line-height: 1; }
.cf-hp {
    right: 1.1%; top: 4.7%; /* on the Class line, under the end of the name */
    font-family: 'Cinzel', Georgia, serif;
    font-weight: 700;
    font-size: 4.45cqw;
    line-height: 1;
    color: #fff;
}
.cf-hp.cf-hp-hurt { color: #ffb4a8; }

.cf-type, .cf-subtype {
    font-family: 'Cinzel', Georgia, serif;
    font-weight: 400;
    color: #1d1a16;
    white-space: nowrap;
    transform: translateY(-50%);
}
/* The type banner ends in an angled point at ~35% across: the type name is centred in the
   banner before it, and the subtype starts a clear gap past the tip. */
.cf-type {
    left: 0; top: 52.6%; width: 32.6%;
    text-align: center;
    font-size: 5.3cqw;
}
.cf-subtype {
    left: 38.9%; right: 1.5%; top: 52.9%;
    font-size: 4.1cqw;
    overflow: hidden;
    text-overflow: ellipsis;
}

.cf-text {
    left: 4.8%; top: 56.5%; width: 91.1%; height: 40.1%;
    padding: 3.2cqw 3cqw 1.2cqw 1.6cqw;
    box-sizing: border-box;
    display: flex;
    flex-direction: column;
}
.cf-text-body {
    flex: 1 1 auto;
    min-height: 0;
    overflow: hidden;
    font-size: calc(4.85cqw * var(--fit, 1));
    line-height: 1.05;
    letter-spacing: 0.02em; /* the print sets its rules text loosely */
}
.cf-text-body p { margin: 0 0 0.45em 0.15em; }
.cf-keywords { font-weight: 700; }
.cf-desc { white-space: pre-line; } /* a line break in the rules text is a paragraph break, as printed */
/* The Class 3 cost: a white pill badge, then the cost itself in small type beside it. */
.cf-c3 {
    display: flex;
    align-items: flex-start;
    gap: 0.25em;
    margin: 0 0 0.45em 0.15em;
}
.cf-c3-badge {
    flex: none;
    font-family: 'Cinzel', Georgia, serif;
    font-weight: 700;
    font-size: 0.78em;
    line-height: 1;
    color: #111;
    background: linear-gradient(#fff, #f1f1ef);
    border: 0.16em solid #6d6d6b;
    box-shadow: 0 0 0 0.06em #222, inset 0 -0.08em 0.1em rgba(0, 0, 0, 0.15);
    border-radius: 999px;
    padding: 0.32em 0.5em 0.26em;
    white-space: nowrap;
}
.cf-c3-text { font-size: 0.7em; line-height: 1.15; padding-top: 0.05em; }
/* Pips are part of the line they start: a black disc each, nearly touching. */
.cf-pips { display: inline-flex; align-items: center; gap: 0.02em; white-space: nowrap; }
.cf-pips b { font-weight: 700; font-size: 0.9em; margin-left: 0.1em; }
.cf-pips-4 { display: inline-grid; grid-template-columns: repeat(2, auto); justify-content: start; }
.cf-pip {
    display: inline-block;
    width: 1.02em; height: 1.02em;
    background: center / contain no-repeat;
}
.cf-pip-attack    { background-image: url('cards/pip-attack.svg'); }
.cf-pip-magic     { background-image: url('cards/pip-magic.svg'); }
.cf-pip-character { background-image: url('cards/pip-character.svg'); }
/* Pips in the rules text ({MP} in Cards.json, cardTextHtml) sit centred on the line, like a
   capital, instead of standing on the baseline. */
.cf-text-pips { vertical-align: -0.2em; }
/* A Class 3 cost paid in points (Isis) prints its pips at body size beside the badge, though
   the cost's words run small. */
.cf-c3-text .cf-text-pips { font-size: calc(1em / 0.7); vertical-align: -0.3em; }
/* An attack: pips | name in a fixed column | damage, then its rules underneath, smaller and
   full width. The name column sits past four pips, so every name on the card lines up. */
.cf-attack {
    display: grid;
    grid-template-columns: 3.55em minmax(0, 1fr) auto;
    align-items: center;
    margin: 0 0 0.45em;
}
.cf-attack-name { font-weight: 700; padding-left: 0.05em; }
.cf-attack-dmg { font-weight: 700; text-align: right; padding: 0 0.65em 0 0.4em; }
/* A standing bonus beside the printed damage (Mead Hall's +1): a green pill with its sign, so
   the printed number stays as printed. Fixed colours: it sits on the parchment in every theme. */
.cf-dmg-mod {
    display: inline-block;
    margin-left: 0.25em;
    padding: 0 0.3em;
    border-radius: 0.6em;
    background: #1d5e2c;
    color: #f4ecd4;
    font-size: 0.8em;
    line-height: 1.3;
    vertical-align: 0.08em;
}
.cf-dmg-mod.is-negative { background: #7a1f1f; }
.cf-attack-desc {
    grid-column: 1 / -1;
    font-size: 0.78em;
    line-height: 1.16;
    letter-spacing: 0.01em;
}
/* An ability: pips, then "Name: rules" inline, wrapping under itself. */
.cf-ability {
    display: flex;
    align-items: flex-start;
    gap: 0.12em;
    margin: 0 0 0.45em;
}
.cf-ability .cf-pips { flex: none; padding-top: 0.1em; }
.cf-ability-text { flex: 1 1 auto; min-width: 0; }
.cf-withdraw {
    position: static;
    align-self: flex-end;
    flex: none;
    margin-right: 5.8cqw;
    font-size: 4.1cqw;
    line-height: 1;
    letter-spacing: 0.03em;
}

.cf-footer {
    left: 1.5%; right: 1.1%; top: 96.9%; bottom: 0;
    display: grid;
    grid-template-columns: 1fr auto 1fr;
    align-items: center;
    gap: 2cqw;
    font-family: 'Cinzel', Georgia, serif;
    font-weight: 700;
    font-size: 2.2cqw;
    color: #e8e6e0;
    white-space: nowrap;
}
.cf-footer > :last-child { text-align: right; }

/* Live-play overlays over the face: position tag + statuses up on the art, gear, attack dots
   and the HP bar down over the footer strip. */
.card.art-card > .cf-overlay {
    pointer-events: none;
    font-size: 0.85em;
}
.cf-overlay-top {
    position: absolute;
    top: 11%; left: 5%; right: 5%;
    display: flex;
    flex-wrap: wrap;
    justify-content: flex-end;
    gap: 2px;
}
.cf-overlay-bottom {
    position: absolute;
    left: 4%; right: 4%; bottom: 0.6%;
    display: flex;
    flex-direction: column;
    gap: 1px;
}
.cf-overlay-bottom .attack-dots { justify-content: center; }
.cf-overlay-bottom .hp-bar { height: 5px; margin: 0; box-shadow: 0 0 0 1px rgba(0,0,0,0.6); }
.cf-overlay-bottom .equipped-relics-strip { margin: 0; }

/* Hover/focus zoom: the same face, enlarged, in the tooltip's slot (so it inherits every bit
   of the tooltip's placement logic: above/below, modal floating, focus-visible). */
.card-tooltip.card-zoom {
    width: calc(250px * var(--a11y-text-scale));
    padding: 0;
    background: transparent;
    border: none;
    box-shadow: none;
    border-radius: 0;
}
.card-zoom .card-face,
.inspect-face .card-face {
    width: 100%;
    aspect-ratio: 900 / 1302;
    border-radius: 3.5% / 2.6%;
    box-shadow: 0 0 0 2px #050404, 0 12px 32px rgba(0,0,0,0.75);
}
.cz-state {
    margin-top: 6px;
    background: var(--bg-surface);
    border: 1px solid var(--border-color-strong);
    border-radius: 8px;
    padding: 6px 8px;
    font-size: calc(0.8rem * var(--a11y-text-scale));
    line-height: 1.35;
    color: var(--text-primary);
}
.cz-status, .cz-curse { color: var(--accent-danger); }
.cz-gear { color: var(--accent-gold); }
.cz-dmg-mod { color: var(--accent-success); }

/* ── Rules glossary terms (render/glossary.js, ui/glossary-tips.js) ─────────────
   A term the glossary explains (Stoic, Twist, Shinto, Warrior...) is marked wherever card text
   shows it, but only Inspect's details panel shows it as one (Mitch, 2026-11-09). The art card
   prints exactly as the physical card does, Inspect's included, and the rest (the hover
   preview, the card sheet, the classic text box) is plain text too. In the details it takes
   the gold the keyword lines already use (each theme's --accent-gold is checked against its
   own panels), underlined so the cue doesn't rely on colour. */
#inspect-modal .inspect-details .kw {
    color: var(--accent-gold);
    text-decoration: underline;
    text-decoration-thickness: from-font;
    text-underline-offset: 0.15em;
    cursor: help;
}
/* Opened with a controller or the keyboard, Inspect moves left to leave room for the boxes on
   its right (render/inspect.js): the column's width, plus its 10px gap and 8px margin. */
#inspect-modal.inspect-has-terms {
    box-sizing: border-box;
    padding-right: calc(230px * var(--a11y-text-scale) + 24px);
}
#inspect-modal.inspect-has-terms .inspect-modal-content { width: min(880px, calc(100% - 3vw)); }

/* One explanation: the tooltip on a term, and each box in the column beside Inspect
   (the Hades look: the term's name in the card's Cinzel, its meaning under it). */
.kw-box {
    box-sizing: border-box;
    background: var(--bg-surface);
    border: 1px solid var(--border-color-strong);
    border-left: 3px solid var(--accent-gold);
    border-radius: 6px;
    padding: 6px 10px 7px;
    box-shadow: 0 6px 18px rgba(0, 0, 0, 0.55);
    color: var(--text-primary);
    font-size: calc(0.8rem * var(--a11y-text-scale));
    line-height: 1.35;
    text-align: left;
}
.kw-box-title {
    font-family: 'Cinzel', Georgia, serif;
    font-weight: 700;
    color: var(--accent-gold);
    letter-spacing: 0.03em;
    margin-bottom: 2px;
}
/* Both body-level and fixed, placed by script, and never in the pointer's way, above every
   dialog (9999 inline on the runtime ones). */
.kw-tip,
.kw-panels {
    position: fixed;
    left: 0;
    top: 0;
    display: none;
    pointer-events: none;
}
.kw-tip {
    z-index: 10001;
    max-width: calc(260px * var(--a11y-text-scale));
}
.kw-panels {
    z-index: 10000;
    flex-direction: column;
    gap: 6px;
    width: calc(230px * var(--a11y-text-scale));
}
:root[data-theme^="hc-"] .kw-box {
    box-shadow: none;
    border-width: 2px 2px 2px 4px;
}

/* Offscreen measuring box for cardTextFit: same face proportions, never seen. */
.card-fit-host {
    position: fixed;
    left: -10000px; top: 0;
    width: 400px;
    visibility: hidden;
    pointer-events: none;
}
.card-fit-host .card-face { width: 400px; aspect-ratio: 900 / 1302; }

/* Inspect dialog: the art face at reading size beside the classic details panel. */
#inspect-modal { z-index: 1300; }
.inspect-modal-content {
    width: min(880px, 94vw);
    max-height: 92vh;
    overflow-y: auto;
    box-sizing: border-box;
    padding: 20px 24px;
}
.inspect-modal-content h3 { margin: 0 0 14px; }
.inspect-body {
    display: flex;
    gap: 22px;
    align-items: flex-start;
    text-align: left;
}
.inspect-face-col { flex: 0 0 min(400px, 46vw); }
.inspect-face.has-full-art { cursor: zoom-in; }
/* "Click to view full art": under the card, its arrow bouncing up at it. */
.inspect-art-hint {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
    margin: 14px auto 0;
    padding: 6px 12px;
    background: none;
    border: none;
    color: var(--text-secondary);
    font-size: calc(0.9rem * var(--a11y-text-scale));
    letter-spacing: 0.04em;
    cursor: pointer;
}
.inspect-art-hint:hover, .inspect-art-hint:focus-visible { color: var(--accent-gold); }
.inspect-art-hint.hidden { display: none; }
.inspect-art-hint-arrow { width: 1.2em; height: 1.2em; animation: art-hint-bounce 1.4s ease-in-out infinite; }
@keyframes art-hint-bounce {
    0%, 100% { transform: translateY(0); }
    50%      { transform: translateY(-5px); }
}
html[data-animations="off"] .inspect-art-hint-arrow { animation: none; }

/* Full-art view (showFullArt in script.js). The picture is position: fixed so it can start on
   the Inspect card's art window and grow from there; script.js sets its box, this only says how
   the box moves. The backdrop and the credit fade in behind it. */
#art-viewer {
    z-index: 1400;
    display: block;
    background: rgba(0, 0, 0, 0);
    transition: background-color 0.45s ease;
    cursor: zoom-out;
}
#art-viewer.hidden { display: none; }
#art-viewer.art-viewer-open { background: rgba(0, 0, 0, 0.92); }
.art-viewer-img {
    position: fixed;
    object-fit: cover;
    box-shadow: 0 18px 60px rgba(0, 0, 0, 0.7);
}
.art-viewer-img.art-viewer-animating {
    transition: left 0.45s cubic-bezier(0.2, 0.8, 0.2, 1), top 0.45s cubic-bezier(0.2, 0.8, 0.2, 1),
                width 0.45s cubic-bezier(0.2, 0.8, 0.2, 1), height 0.45s cubic-bezier(0.2, 0.8, 0.2, 1),
                border-radius 0.45s ease;
}
.art-viewer-caption {
    position: fixed;
    left: 0; right: 0;
    text-align: center;
    color: #f2ead8;
    opacity: 0;
    transition: opacity 0.25s ease;
    pointer-events: none;
}
#art-viewer.art-viewer-open .art-viewer-caption { opacity: 1; transition-delay: 0.3s; }
.art-viewer-name { font-family: 'Cinzel', Georgia, serif; font-size: calc(1.1rem * var(--a11y-text-scale)); }
.art-viewer-artist { margin-top: 2px; font-size: calc(0.9rem * var(--a11y-text-scale)); color: #cfc6b2; font-style: italic; }
.art-viewer-close {
    position: fixed;
    top: 12px; right: 16px;
    width: 44px; height: 44px;
    border-radius: 50%;
    background: rgba(0, 0, 0, 0.55);
    border: 1px solid rgba(255, 255, 255, 0.35);
    color: #fff;
    font-size: 1.6rem;
    line-height: 1;
    cursor: pointer;
    opacity: 0;
    transition: opacity 0.25s ease;
}
#art-viewer.art-viewer-open .art-viewer-close { opacity: 1; }
html[data-animations="off"] #art-viewer,
html[data-animations="off"] .art-viewer-caption,
html[data-animations="off"] .art-viewer-close { transition: none; }
.inspect-details.card-tooltip {
    display: block;
    position: static;
    transform: none;
    width: auto;
    flex: 1 1 auto;
    min-width: 0;
    pointer-events: auto;
    box-shadow: none;
    z-index: auto;
}
html[data-layout="mobile"] .inspect-body { flex-direction: column; align-items: center; }
html[data-layout="mobile"] .inspect-face-col { flex: none; width: min(340px, 82vw); }
html[data-layout="mobile"] .inspect-details.card-tooltip { width: 100%; box-sizing: border-box; }

/* 3D tilt (onCardTiltMove in script.js sets --tilt-x/--tilt-y/--glare-x/--glare-y on the hovered
   element). Only the face and its overlays rotate; the .card box never does. The short
   transition while tilting smooths pointer jitter, the long one eases the card back flat. */
.card.art-card > .card-face,
.card.art-card > .cf-overlay {
    transform: perspective(450px) rotateX(var(--tilt-x, 0deg)) rotateY(var(--tilt-y, 0deg));
    transition: transform 0.35s ease-out;
}
/* The two copies people READ (hover zoom, Inspect) are oversampled: laid out at twice the size,
   then scaled back down by the same transform that tilts them. A browser draws a tilted element
   by rasterising it flat and warping that picture, and on a 1x display (the 1080p monitor) the
   warp smeared small text; warping a 2x picture keeps it crisp. Every size inside .card-face is
   cqw, so the doubled face lays out identically. Geometry: the face is 200% wide; the negative
   margins hand back the extra space (-100% across, -1302/900 of the width down) so the zoom box
   keeps its size; the tilt pivots on the visible card's centre, which after scale(0.5) about the
   top-left corner sits at 25%/25% of the doubled box. Shadow and outline are doubled to come out
   at their usual size. */
.card-zoom .card-face,
.inspect-face .card-face {
    width: 200%;
    margin-right: -100%;
    margin-bottom: -144.67%;
    transform-origin: 0 0;
    transform: translate(25%, 25%) perspective(1000px) rotateX(var(--tilt-x, 0deg))
               rotateY(var(--tilt-y, 0deg)) translate(-25%, -25%) scale(0.5);
    box-shadow: 0 0 0 4px #050404, 0 24px 64px rgba(0,0,0,0.75);
    transition: transform 0.35s ease-out;
}
/* Without this the face's negative bottom margin collapses out through its parent, and the zoom
   box reports the full doubled height (which also pushed the hover zoom out of position). */
.card-tooltip.card-zoom,
.inspect-face {
    padding-bottom: 1px; /* .card-tooltip.card-zoom, to outrank its own padding: 0 */
}
.card.art-card.tilting > .card-face,
.card.art-card.tilting > .cf-overlay,
.tilting .card-zoom .card-face,
.card-zoom.tilting .card-face,
.inspect-face.tilting .card-face {
    transition: transform 0.08s linear;
}
/* The light sits UNDER the lettering: a real element placed right after the frame, so every
   text layer after it in the face paints on top at full contrast. As a ::after it covered the
   text too, and its bright spot washed the rules out wherever it crossed the parchment. */
.cf-glare {
    inset: 0;
    pointer-events: none;
    background: radial-gradient(circle at var(--glare-x, 50%) var(--glare-y, 50%),
        rgba(255, 255, 255, 0.32), rgba(255, 255, 255, 0) 55%);
    mix-blend-mode: soft-light;
    opacity: 0;
    transition: opacity 0.3s;
}
.tilting .cf-glare { opacity: 1; }

/* Attract mode (script.js "Attract mode"): the full-screen black the idle demo fades through on
   the way in and out, above everything including the inline-styled z-index 9999 dialogs, and
   the label shown over the demo match. Neither takes pointer input: script.js's capture
   listeners are what end the demo. */
#attract-fade {
    position: fixed;
    inset: 0;
    z-index: 20000;
    background: #000;
    opacity: 0;
    pointer-events: none;
    transition: opacity 700ms ease;   /* ATTRACT_FADE_MS in script.js */
}
#attract-fade.opaque { opacity: 1; }

#attract-tag {
    position: fixed;
    top: 10px;
    left: 50%;
    transform: translateX(-50%);
    z-index: 19000;
    padding: 6px 16px;
    border-radius: 999px;
    background: rgba(0, 0, 0, 0.72);
    color: #f0e6d2;
    font-family: 'Cinzel', Georgia, serif;
    font-size: 0.85em;
    letter-spacing: 2px;
    text-transform: uppercase;
    pointer-events: none;
    animation: attract-tag-pulse 2.4s ease-in-out infinite;
}
#attract-tag.hidden { display: none; }
@keyframes attract-tag-pulse { 50% { opacity: 0.55; } }
html[data-animations="off"] #attract-tag { animation: none; }
