/* WarHammerHelper — mobile-first dark theme */

:root {
    --bg: #0d0e12;
    --bg-elev: #15171d;
    --bg-elev-2: #1d2028;
    --line: #2a2e3a;
    --text: #e8e8ee;
    --text-dim: #a0a3b0;
    --text-mute: #6e7180;
    --accent: #c8102e;
    --accent-soft: #e63b54;
    --accent-tint: rgba(200, 16, 46, 0.12);
    --gold: #e0b13a;
    --green: #4cb87a;
    --blue: #5cb6ff;

    --topbar-h: 56px;
    --bottombar-h: 64px;
    --radius: 12px;
    --radius-sm: 8px;
    --radius-lg: 18px;

    --shadow-1: 0 1px 2px rgba(0, 0, 0, 0.5);
    --shadow-2: 0 4px 16px rgba(0, 0, 0, 0.4);

    --safe-top: env(safe-area-inset-top, 0px);
    --safe-bot: env(safe-area-inset-bottom, 0px);

}

@font-face {
    font-family: 'ConduitITC';
    /* Use format('opentype') for .otf — 'otf' is not a valid format hint and
       causes some browsers to skip the font entirely. */
    src: url('fonts/Conduit ITC Regular.otf') format('opentype');
    /* Match the entire normal-weight range; if we restrict to 700 the body
       text (default 400) won't match and the browser falls back. */
    font-weight: 100 900;
    font-style: normal;
    font-display: swap;
}

* {
    box-sizing: border-box;
}

/* ============ scrollbars ============
 *
 * Theme every scrollable surface to match the dark UI. Two declarations
 * cover today's browsers:
 *   - Chromium / WebKit: ::-webkit-scrollbar pseudo-elements (custom
 *     track + thumb colours, rounded thumb, slim 10px gutter).
 *   - Firefox: scrollbar-color (thumb / track) + scrollbar-width.
 *
 * Applied to every element so nested overflow regions (modals, panels,
 * action logs, side panels) all match without per-component opt-in.
 */
* {
    scrollbar-width: thin;
    scrollbar-color: var(--bg-elev-2) var(--bg);
}
::-webkit-scrollbar {
    width: 10px;
    height: 10px;
}
::-webkit-scrollbar-track {
    background: var(--bg);
}
::-webkit-scrollbar-thumb {
    background: var(--bg-elev-2);
    border-radius: 5px;
    /* Inner border in track colour creates the appearance of padding
       around the thumb without needing a separate thumb-padding declaration. */
    border: 2px solid var(--bg);
}
::-webkit-scrollbar-thumb:hover {
    background: var(--line);
}
::-webkit-scrollbar-corner {
    background: var(--bg);
}

html, body {
    margin: 0;
    padding: 0;
    background: var(--bg);
    color: var(--text);
    /* ConduitITC leads the stack so the custom face is used whenever it
       loads; system fonts are kept as fallbacks during the initial paint
       and on browsers that fail to load the OTF. */
    font-family: "ConduitITC", -apple-system, BlinkMacSystemFont, Roboto, Oxygen,
        Ubuntu, Cantarell, sans-serif;
    font-size: 16px;
    line-height: 1.5;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    text-rendering: optimizeLegibility;
    overscroll-behavior-y: none;
}

/* Section headings keep the platform UI font — Conduit ITC is too display-y
   for the small uppercase label style used on these. Covers the section-h
   class plus its prefixed siblings (search-, strat-, wound-, wiz-). */
.section-h,
.search-section-h,
.strat-section-h,
.wound-section-h,
.wiz-section-h {
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
        Ubuntu, Cantarell, sans-serif;
}

/* Titles & headers that render in Conduit ITC normalise to weight 700 so the
   display face has a consistent, prominent look across the app. Wrapper-only
   header rows (.ds-header, .editor-head, .game-header, .gv-head, .wiz-h-row,
   .wiz-attack-h, .inbox-h) are not listed — their inner *-title / *-name
   children carry the visible text. */
.topbar-title,
.boot-title,
.hero-title,
.tile-title,
.list-item-title,
.card-h,
.weapons-h,
.search-result-title,
.army-cta-title,
.wiz-h,
.modal-h,
.inbox-h-title,
.stage-title,
.choice-title,
.unit-row-title,
.section-block-h,
.ds-name,
.ability-name,
.wiz-attack-name,
.activation-progress-text,
.activation-unit-name {
    font-weight: 700;
    text-transform: uppercase;
}

body {
    min-height: 100vh;
    min-height: 100dvh;
    transition: background-color 0.4s ease;
}

/* When it's your turn during a live match, the page background tints to a
   very dark green so you can tell at a glance from across the table. The
   class is set/cleared from JS as the game state changes; cleared on every
   navigation so non-match screens never inherit it. */
body.is-my-turn {
    background-color: #0c5832;
}

/* Counter-fight tint: paint a dim mustard yellow on BOTH players' bodies
   while the spectator (non-active player) is the alternating-fights
   picker. Both sides need the visual "this is unusual" cue — the active
   player so they don't accidentally reach for the advance button, and
   the spectator so they realise control has flipped to them. */
body.is-counter-fight {
    background-color: #7c5e0e;
}

/* Reactive-stratagem wait tint: same mustard yellow as counter-fight, but
   triggered when a reactive interrupt (Fire Overwatch / Counter-Offensive
   / Heroic Intervention) is mid-prompt or mid-resolution. Only shows on
   the active player's device — the spectator is the one acting, so they
   don't need the "control flipped" cue. */
body.is-reactive-wait {
    background-color: #7c5e0e;
}

/* Deployment-phase tints: green when it's MY turn to drop the next
   unit, mustard yellow while waiting on the opponent. Same visual
   vocabulary as is-my-turn / is-reactive-wait so the player already
   knows what each colour means by the time they hit deployment. */
body.is-my-deploy { background-color: #0c5832; }
body.is-spectating-deploy { background-color: #7c5e0e; }

/* Attack-flash overlay: fires on both devices when an attacker
 * commits a target in the shoot wizard (TARGETS_SELECTED). A red
 * tint fades up and back out over ~600ms to signal "an attack is
 * about to resolve". Mounted as a fixed-position ::before pseudo-
 * element on the body so it covers the entire viewport regardless
 * of which screen / phase is active. Pointer-events: none so the
 * underlying UI stays interactable. */
body.is-attack-flash::before {
    content: "";
    position: fixed;
    inset: 0;
    z-index: 9000;
    pointer-events: none;
    background: rgba(200, 16, 46, 0.55);
    animation: attack-flash-fade 700ms ease-out forwards;
}
@keyframes attack-flash-fade {
    0%   { opacity: 0; }
    20%  { opacity: 1; }
    100% { opacity: 0; }
}

/* v0.10.179 — Cinematic system. Per-tint flashes share the same
 * fade animation as the legacy .is-attack-flash; only the colour
 * differs. Used by Cinematic._fireFlash.  */
body.is-cinematic-flash-red::before,
body.is-cinematic-flash-gold::before,
body.is-cinematic-flash-green::before,
body.is-cinematic-flash-orange::before,
body.is-cinematic-flash-blue::before,
body.is-cinematic-flash-purple::before,
body.is-cinematic-flash-cyan::before {
    content: "";
    position: fixed;
    inset: 0;
    z-index: 9000;
    pointer-events: none;
    animation: attack-flash-fade 700ms ease-out forwards;
}
body.is-cinematic-flash-red::before    { background: rgba(200,  16,  46, 0.55); }
body.is-cinematic-flash-gold::before   { background: rgba(230, 175,  50, 0.50); }
body.is-cinematic-flash-green::before  { background: rgba( 80, 200, 100, 0.50); }
body.is-cinematic-flash-orange::before { background: rgba(235, 130,  40, 0.55); }
body.is-cinematic-flash-blue::before   { background: rgba( 80, 140, 240, 0.45); }
body.is-cinematic-flash-purple::before { background: rgba(160,  90, 220, 0.50); }
body.is-cinematic-flash-cyan::before   { background: rgba( 90, 200, 230, 0.45); }

/* Slide-in banner. Body-mounted; sits above wizards (z=290) and
 * the reconnect modal (z=1000) but below toasts. Default state =
 * off-screen left; .is-in slides to top-middle, .is-out exits
 * to the right. Tints colour the border + glow; the text stays
 * white for readability across all tints.  */
.vtt-cinematic-banner {
    position: fixed;
    top: 64px;
    left: 50%;
    /* v0.10.255 — off-screen rest/exit positions are viewport-
     * relative (50vw + banner own width) instead of just 150% of
     * banner width. Narrow banner content (e.g. "INFERNUS SQUAD
     * ACTIVATING" at ~300px) on wide viewports left the old
     * `translate(150%)` short by hundreds of px — the banner sat
     * stuck on the right edge mid-slide. The new calc guarantees
     * the banner's leading edge clears the viewport bound on any
     * width, regardless of content length. */
    transform: translateX(calc(-50vw - 100%));
    background: rgba(15, 17, 22, 0.94);
    border: 2px solid var(--accent, #c8102e);
    border-radius: 8px;
    padding: 12px 22px;
    min-width: 280px;
    max-width: min(560px, 78vw);
    text-align: center;
    /* v0.10.261 — Bumped above the tutorial overlay root (z=9000) so
     * phase / deploy / save-roll cinematics aren't buried under the
     * Battle Lore card's full-screen dim wrap. Without this, a phase
     * banner that fired mid-tutorial appeared behind the dim — the
     * player saw nothing, then a card popped explaining the phase
     * they couldn't see happen. */
    z-index: 9500;
    pointer-events: auto;
    box-shadow: 0 6px 24px rgba(0, 0, 0, 0.4), 0 0 24px rgba(200, 16, 46, 0.35);
    transition: transform 350ms cubic-bezier(0.22, 0.91, 0.36, 1);
    will-change: transform;
    user-select: none;
}
.vtt-cinematic-banner.is-in  { transform: translate(-50%, 0); }
.vtt-cinematic-banner.is-out { transform: translateX(calc(50vw + 100%)); transition-duration: 300ms; }

/* v0.10.194 — persistent "waiting on opponent" variant. Slides in
 * the same way as a one-shot cinematic but sits indefinitely with a
 * gentle pulse on its border-glow to telegraph "this is still
 * waiting, not stale." The pulse only runs while is-in is on, so
 * once the banner slides out the animation stops. The pulse
 * intentionally targets `filter: brightness` rather than `box-shadow`
 * (which is already used for the per-tint glow) so the two stack
 * cleanly instead of fighting each other. */
.vtt-cinematic-banner.is-waiting.is-in {
    animation: vtt-cinematic-waiting-pulse 2400ms ease-in-out infinite;
}
@keyframes vtt-cinematic-waiting-pulse {
    0%, 100% { filter: brightness(1); }
    50%      { filter: brightness(1.22); }
}
.vtt-cinematic-text {
    font-size: 17px;
    font-weight: 900;
    color: #fff;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    line-height: 1.25;
    text-shadow: 0 1px 2px rgba(0, 0, 0, 0.7);
}
.vtt-cinematic-subtext {
    margin-top: 4px;
    font-size: 11.5px;
    font-weight: 600;
    color: rgba(255, 255, 255, 0.7);
    letter-spacing: 0.04em;
    text-transform: uppercase;
}

/* Per-tint border + glow. Text stays white. */
.vtt-cinematic-banner[data-tint="red"]    { border-color: #e8334e; box-shadow: 0 6px 24px rgba(0,0,0,0.4), 0 0 28px rgba(200, 16, 46, 0.40); }
.vtt-cinematic-banner[data-tint="gold"]   { border-color: #f0c45a; box-shadow: 0 6px 24px rgba(0,0,0,0.4), 0 0 28px rgba(240, 195,  90, 0.40); }
.vtt-cinematic-banner[data-tint="green"]  { border-color: #66d27a; box-shadow: 0 6px 24px rgba(0,0,0,0.4), 0 0 28px rgba(100, 220, 120, 0.40); }
.vtt-cinematic-banner[data-tint="orange"] { border-color: #f29142; box-shadow: 0 6px 24px rgba(0,0,0,0.4), 0 0 28px rgba(235, 130,  40, 0.40); }
.vtt-cinematic-banner[data-tint="blue"]   { border-color: #6a9cf2; box-shadow: 0 6px 24px rgba(0,0,0,0.4), 0 0 28px rgba(100, 150, 240, 0.40); }
.vtt-cinematic-banner[data-tint="purple"] { border-color: #b079e0; box-shadow: 0 6px 24px rgba(0,0,0,0.4), 0 0 28px rgba(170, 110, 220, 0.40); }
.vtt-cinematic-banner[data-tint="cyan"]   { border-color: #6ec8e6; box-shadow: 0 6px 24px rgba(0,0,0,0.4), 0 0 28px rgba(110, 200, 230, 0.40); }

/* Tap-twice skip confirm flyout. Hangs off the bottom of the
 * banner, slides DOWN-and-IN when armed. Disappears 2.5s after
 * the first tap if no second tap follows. */
.vtt-cinematic-skip-flyout {
    position: absolute;
    bottom: -38px;
    left: 50%;
    transform: translate(-50%, -6px);
    background: rgba(15, 17, 22, 0.92);
    border: 1px solid rgba(255, 255, 255, 0.18);
    border-radius: 4px;
    padding: 4px 12px;
    font-size: 11px;
    font-weight: 700;
    color: rgba(255, 255, 255, 0.85);
    letter-spacing: 0.04em;
    text-transform: uppercase;
    opacity: 0;
    pointer-events: none;
    transition: opacity 180ms ease, transform 180ms cubic-bezier(0.22, 0.91, 0.36, 1);
    white-space: nowrap;
}
.vtt-cinematic-skip-flyout.is-shown {
    opacity: 1;
    transform: translate(-50%, 0);
}

/* v0.10.269 — Cinematics no longer block pointer interaction.
 * Previously `body.cinematic-input-locked` gated the canvas /
 * topbar / bottombar to `pointer-events: none` so the player
 * couldn't accidentally tap underneath the banner; that hard-
 * locked the screen for ~2s every attack/move banner and made
 * the "ON THE MOVE" / TARGETS_SELECTED beats feel sluggish.
 * Now the banner is consumed by a single tap (see _onSkipTap)
 * and the player can keep panning / tapping tokens through
 * the cinematic. Modals + wizards still hide, though — a
 * still-open wizard step underneath the banner would compete
 * for the player's attention. */
body.cinematic-input-locked .modal-overlay {
    visibility: hidden;
}

/* v0.10.315 — VTT cinematic "disabled" mode for the in-match HUD.
 *
 * While a VTT cinematic plays, the .game-header + .match-panel no
 * longer slide off-screen (collapse) or hide (opacity 0). Instead they
 * drop to a non-interactive 40%-opacity DISABLED state — the board
 * context stays visible but it's clear the HUD can't be touched until
 * the cinematic ends.
 *
 * v0.10.316 — PERF. The previous version added `filter: grayscale(1)`
 * + an opacity/filter transition. That was the source of severe lag:
 * the panel floats over a canvas that repaints every frame during the
 * cinematic camera pan, and a `filter` forces the browser to allocate
 * a separate filter layer and re-rasterize it on EVERY one of those
 * frames. Dropped the grayscale entirely. `transition: none` is also
 * required because the panel's bg-mode rule (below) declares
 * `transition: ... opacity 0.24s`, so without an override the dim would
 * still animate; we force the opacity to snap instantly per the user's
 * request. Just `opacity` + `pointer-events`, both cheap, no animation.
 *
 * Routes through two body classes, both set by Cinematic:
 *   • cinematic-input-locked — added by _lockInput() for every blocking
 *     cinematic (i.e. everything except `noInputLock` notifications
 *     like "ON THE MOVE", which intentionally stay interactive).
 *   • cinematic-hide-match-ui — the legacy enemy-deploy flag; kept so
 *     that callout uses the same disabled look.
 * Both cleared in Cinematic._releaseInput().
 *
 * Gated on `tabletop-bg-mode` so this is VTT-only (non-VTT matches are
 * unaffected) and so the selector specificity (0,3,1) beats the
 * `body.tabletop-bg-mode .match-panel { opacity: 1 }` rule below
 * without needing !important. */
body.tabletop-bg-mode.cinematic-input-locked .game-header,
body.tabletop-bg-mode.cinematic-input-locked .match-panel,
body.tabletop-bg-mode.cinematic-hide-match-ui .game-header,
body.tabletop-bg-mode.cinematic-hide-match-ui .match-panel {
    opacity: 0.4;
    pointer-events: none;
    transition: none;
}

/* v0.10.315 — the on-canvas "Activate ▸" button for the selected unit
 * is disabled whenever a cinematic is playing OR any wizard/modal is
 * open, so the player can't kick off a new activation on top of an
 * in-flight cinematic or an already-open wizard. (`:has()` is already
 * used elsewhere in this sheet, so the modal-presence check is safe.)
 * v0.10.316 — instant opacity, no grayscale filter / transition (same
 * per-frame-rasterization perf concern as the HUD rule above). */
body.cinematic-input-locked .activate-overlay,
body:has(.modal-overlay) .activate-overlay {
    opacity: 0.4;
    pointer-events: none;
}

/* v0.10.268 — VTT instruction banner (.vtt-instruction-banner +
 * .vtt-instruction-tag + .vtt-instruction-text +
 * .vtt-instruction-collapse) was removed in this release. Verbatim
 * source for the CSS block AND the matching JS lives at
 * `dev/archived-vtt-instruction-banner.md` for a future rebuild. */

/* Reactive-stratagem gate banner. Body-mounted (singleton) and pinned
   just above the bottom navbar, centered horizontally. Sits above every
   modal layer (max known is hazardous-overlay at 305) so the player
   always sees they're gated, regardless of which screen/phase is up. */
.reactive-gate-banner {
    position: fixed;
    left: 50%;
    transform: translateX(-50%);
    /* Anchored to the top of the screen, just under the topbar, so
       the banner is the first thing the active player sees while
       waiting on a peer reactive decision. Used to live above the
       bottom navbar but that put it on top of toasts and modal
       footers when the viewport got narrow. */
    top: calc(var(--topbar-h) + var(--safe-top) + 8px);
    z-index: 360;
    margin: 0;
    width: calc(100vw - 24px);
    max-width: 520px;
    /* Bump opacity well above the inline-warn banner default — this one
       hovers over arbitrary content, including the yellow body tint, so
       it needs to read clearly without a backdrop. */
    background: rgba(58, 44, 12, 0.96);
    border-color: var(--gold);
    box-shadow: var(--shadow-2);
    backdrop-filter: blur(6px);
    -webkit-backdrop-filter: blur(6px);
    align-items: center;
}

/* Sticky yellow banner shown during counter-fight. Lives at the top of
   the match screen above the Rules-Reminder card. */
.counter-fight-banner {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 10px 12px;
    margin-bottom: 12px;
    background: rgba(200, 160, 80, 0.18);
    border: 1px solid var(--gold);
    border-radius: var(--radius-sm);
    font-size: 13.5px;
    line-height: 1.4;
    color: var(--text);
}
.counter-fight-icon {
    flex-shrink: 0;
    width: 28px;
    height: 28px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    background: var(--gold);
    color: #1a1404;
    border-radius: 50%;
    font-size: 16px;
    font-weight: 900;
}
.counter-fight-msg b { color: var(--gold); }

/* One-shot announcement modal that fires when fightPicker flips to the
   non-active side. Yellow background + border to match the body tint
   and make the unusual game state pop. */
.counter-fight-overlay { z-index: 290; }
.counter-fight-modal {
    padding: 16px;
    border: 2px solid var(--gold);
    box-shadow: 0 0 0 1px rgba(200, 160, 80, 0.35), 0 8px 32px rgba(0,0,0,0.55);
    background: #2a2008;
    max-width: 500px;
}
.counter-fight-modal .card-h { color: var(--gold); }
.counter-fight-modal .text-mute { color: #c8b478; }

a {
    color: var(--accent-soft);
    text-decoration: none;
}

button {
    font: inherit;
    color: inherit;
    background: none;
    border: none;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
}

input, select, textarea {
    font: inherit;
    color: inherit;
}

/* ============ boot splash ============ */
.boot {
    position: fixed;
    inset: 0;
    background: var(--bg);
    display: flex;
    align-items: center;
    justify-content: center;
    z-index: 1000;
    transition: opacity 0.3s ease;
}
.boot.fade-out { opacity: 0; pointer-events: none; }
.boot-inner { text-align: center; }
.boot-mark {
    width: 72px;
    height: 72px;
    border-radius: 16px;
    background: var(--accent);
    display: flex;
    align-items: center;
    justify-content: center;
    color: #fff;
    font-size: 44px;
    font-weight: 900;
    margin: 0 auto 16px;
    box-shadow: 0 8px 24px rgba(200, 16, 46, 0.4);
}
.boot-title { font-size: 22px; font-weight: 800; letter-spacing: -0.02em; }
.boot-sub { color: var(--text-dim); margin-top: 4px; font-size: 14px; }

/* ============ top bar ============ */
/* Flex layout with a strict shrink order: search/Settings buttons NEVER
   shrink (flex-shrink:0); the username chip shrinks/truncates next
   (flex-shrink:1 with ellipsis); the title shrinks/truncates last.
   `overflow: hidden` on the bar clips anything that still doesn't fit
   (extremely narrow viewports) instead of pushing content off-screen. */
.topbar {
    position: sticky;
    top: 0;
    height: calc(var(--topbar-h) + var(--safe-top));
    padding-top: var(--safe-top);
    background: rgba(13, 14, 18, 0.85);
    backdrop-filter: saturate(180%) blur(14px);
    -webkit-backdrop-filter: saturate(180%) blur(14px);
    border-bottom: 1px solid var(--line);
    display: flex;
    align-items: center;
    overflow: hidden;
    z-index: 50;
}
.topbar-title {
    flex: 1 1 0;          /* take remaining space, can shrink to 0 */
    min-width: 0;          /* allow shrinking below content width */
}
.topbar-right {
    display: flex;
    align-items: center;
    gap: 2px;
    padding-right: 4px;
    flex-shrink: 0;        /* the cluster as a whole holds its size */
    /* But allow chip-vs-button shrink negotiation INSIDE the cluster. */
    min-width: 0;
}
.topbar-user {
    font-size: 13px;
    font-weight: 600;
    color: var(--text-dim);
    text-decoration: none;
    max-width: 110px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    padding: 6px 8px;
    border-radius: 6px;
    flex-shrink: 1;        /* chip yields first when the cluster gets tight */
    flex-basis: auto;
    min-width: 0;
}
.topbar-user:active { background: var(--bg-elev); color: var(--text); }
.topbar-user[hidden] { display: none; }
/* Buttons inside the right cluster never shrink — they have to stay
   tappable. */
.topbar-right .topbar-action { flex-shrink: 0; }
@media (max-width: 400px) {
    .topbar-user { max-width: 80px; padding: 6px 4px; }
}
@media (max-width: 320px) {
    .topbar-user { display: none; }
}
.topbar-title {
    margin: 0;
    font-size: 17px;
    font-weight: 700;
    letter-spacing: -0.01em;
    text-align: center;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.topbar-back, .topbar-action {
    height: 44px;
    /* Fixed width so the flex layout has predictable column sizing
       and the back column reserves space even when hidden (see
       visibility:hidden below). */
    width: 44px;
    flex-shrink: 0;
    flex-grow: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    color: var(--text);
    font-size: 26px;
    border-radius: 10px;
    margin: 0 4px;
}
.topbar-back:active, .topbar-action:active { background: var(--bg-elev); }
.topbar-back[hidden] { visibility: hidden; }

/* ============ screen ============ */
.screen {
    /* Use individual longhands so a wider-viewport rule can change side
       padding without clobbering the bottom-nav clearance. */
    padding-top: 16px;
    padding-right: 16px;
    padding-left: 16px;
    /* Reserve space for the fixed .bottombar (height + iOS safe area) plus a
       comfortable gap so the last element on a long screen never tucks under
       the translucent nav bar. */
    padding-bottom: calc(var(--bottombar-h) + var(--safe-bot) + 32px);
    max-width: 880px;
    margin: 0 auto;
}

/* ============ bottom nav ============ */
.bottombar {
    position: fixed;
    bottom: 0;
    left: 0;
    right: 0;
    height: calc(var(--bottombar-h) + var(--safe-bot));
    padding-bottom: var(--safe-bot);
    background: rgba(13, 14, 18, 0.92);
    backdrop-filter: saturate(180%) blur(14px);
    -webkit-backdrop-filter: saturate(180%) blur(14px);
    border-top: 1px solid var(--line);
    display: flex;
    z-index: 40;
}
.navbtn {
    flex: 1;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    color: var(--text-mute);
    text-decoration: none;
    font-size: 11px;
    gap: 2px;
    transition: color 0.15s ease;
}
.navbtn .navicon { font-size: 22px; line-height: 1; }
.navbtn.active { color: var(--accent-soft); }
.navbtn:active { background: var(--bg-elev); }

/* PNG-logo navicon variant (Factions slot uses the Space Marine logo
   so it shares the visual language of the datasheet faction icons).
   The PNG ships white-on-transparent, so a CSS filter chain re-tints
   it: muted gray (#6e7180) inactive and accent-soft red (#e63b54)
   active — same colors the font-icon variants pick up via `color`. */
.navbtn .navicon-img {
    width: 32px;
    height: 32px;
    object-fit: contain;
    flex-shrink: 0;
    /* white → --text-mute (#6e7180) */
    filter: brightness(0) saturate(100%) invert(50%) sepia(11%) saturate(404%) hue-rotate(195deg) brightness(91%) contrast(89%);
    transition: filter 0.15s ease;
}
.navbtn.active .navicon-img {
    /* white → --accent-soft (#e63b54) — same chain the datasheet uses */
    filter: brightness(0) saturate(100%) invert(35%) sepia(64%) saturate(2670%) hue-rotate(326deg) brightness(98%) contrast(91%);
}

/* In-match indicator on the LAN Match nav slot. The class is toggled from
   JS when the player is connected to a live match but is currently viewing
   some other route — a soft blueish glow draws the eye back to the match.
   The :not(.active) gate keeps the indicator silent while the slot is the
   active route, since the active red is already a strong signal there.
   Both the icon and the label pulse, so the glow reads even when the icon
   is briefly out of view (e.g., during dialog overlays). */
.navbtn[data-route="lan-match"].is-in-match:not(.active) {
    color: #0072e0;
    animation: lan-match-glow 2s ease-in-out infinite;
}
.navbtn[data-route="lan-match"].is-in-match:not(.active) .navicon-wh {
    color: #0072e0;
}
@keyframes lan-match-glow {
    0%, 100% {
        text-shadow:
            0 0 5px rgba(0, 114, 224, 0.55),
            0 0 10px rgba(0, 114, 224, 0.25);
    }
    50% {
        text-shadow:
            0 0 10px rgba(0, 114, 224, 0.95),
            0 0 18px rgba(92, 182, 255, 0.65);
    }
}
@media (prefers-reduced-motion: reduce) {
    .navbtn[data-route="lan-match"].is-in-match:not(.active) {
        animation: none;
        text-shadow: 0 0 8px rgba(0, 114, 224, 0.75);
    }
}

/* v0.10.203 — shrink the bottom navbar ~30% on narrow devices.
 * Override --bottombar-h on :root so the padding-bottom rules + toast
 * / modal-footer offsets (lines 658, 5993, 6300, 6422) follow along
 * and the chrome below the navbar stays correctly clamped. Icon +
 * label sizes scale proportionally so the labels still read at
 * 10px without crowding the icon.
 *
 * v0.10.204 — breakpoint widened from 520px to 1024px so the compact
 * navbar engages on tablet-portrait and small-laptop widths where the
 * default 64px bar feels disproportionately tall. Icons shrunk
 * another ~10% on top of the v0.10.203 sizes (22px PNG / 16px font)
 * for tighter visual weight at the new breakpoint. */
@media (max-width: 1024px) {
    :root { --bottombar-h: 45px; }
    .navbtn { font-size: 10px; gap: 1px; }
    .navbtn .navicon { font-size: 16px; }
    .navbtn .navicon-img { width: 22px; height: 22px; }
    /* v0.10.205 — warhammer40k.css defines its own `.navbtn .navicon-wh`
     * font-size at 22px and is loaded after styles.css, so a plain
     * `.navbtn .navicon { font-size: ... }` override loses on cascade
     * (same specificity, later sheet wins). Bumping to `.bottombar
     * .navbtn .navicon-wh` (0,3,0) beats the warhammer40k rule (0,2,0)
     * so the font-glyph icons (Home / PvP Match / About) shrink in
     * lockstep with the Factions <img> at narrow widths. */
    .bottombar .navbtn .navicon-wh { font-size: 16px; }
}

/* ============ home screen ============ */
.hero {
    margin: 8px 0 24px;
    padding: 24px;
    border-radius: var(--radius-lg);
    background:
        radial-gradient(circle at 30% 20%, rgba(200, 16, 46, 0.25), transparent 50%),
        linear-gradient(160deg, var(--bg-elev) 0%, var(--bg-elev-2) 100%);
    border: 1px solid var(--line);
}
.hero-title {
    font-size: 28px;
    font-weight: 900;
    letter-spacing: -0.03em;
    margin: 0 0 6px;
}
.hero-sub {
    color: var(--text-dim);
    margin: 0;
    font-size: 15px;
}
.hero-meta {
    color: var(--text-mute);
    font-size: 12px;
    margin-top: 14px;
}

.discovery-color {
    background:
        radial-gradient(circle at 30% 20%, rgba(16, 200, 25, 0.25), transparent 50%),
        linear-gradient(160deg, var(--bg-elev) 0%, var(--bg-elev-2) 100%) !important;
}

.pvpmatch-color {
    background:
        radial-gradient(circle at 30% 20%, #20344b, transparent 50%),
        linear-gradient(160deg, var(--bg-elev) 0%, var(--bg-elev-2) 100%) !important;
}

.tile-grid {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: 12px;
}
.tile {
    display: flex;
    flex-direction: column;
    padding: 18px;
    border-radius: var(--radius);
    background: var(--bg-elev);
    border: 1px solid var(--line);
    color: var(--text);
    text-decoration: none;
    min-height: 110px;
    justify-content: space-between;
    transition: transform 0.12s ease, border-color 0.15s ease;
}
.tile:active { transform: scale(0.98); border-color: var(--accent); }
.tile-icon { font-size: 26px; opacity: 0.85; }
/* Image variant of the tile icon — used for the lobby HOST/JOIN tiles
   where the icon is a PNG logo (LANGameHostLogo / etc.) rather than a
   wh40k font glyph. height + auto-width + object-fit keeps the artwork
   crisp at any aspect ratio without forcing a square crop. The filter
   chain re-tints the source PNG (any colour) to the app's accent red so
   the lobby tiles match the rest of the UI's accent — same recipe used
   by the Home-page Settings icon. align-self: baseline pins the image
   to the top of the tile so the title/sub line up consistently across
   logos of varying heights. */
.tile-icon.tile-icon-img {
    height: 50px;
    width: auto;
    max-width: 100%;
    object-fit: contain;
    display: block;
    filter: brightness(0) saturate(100%) invert(35%) sepia(64%) saturate(2670%) hue-rotate(326deg) brightness(98%) contrast(91%);
    align-self: baseline;
}

/* Lobby-hub Host/Join tile re-skin. Scoped to the four tiles on
   `/#/lobby` only (via the `.lobby-tile` class added in renderLobbyHub),
   NOT to lobby subscreens or any other elements on the hub. Re-tints
   the PNG icon and the :active border to match the LAN Match home-page
   CTA's blue (--blue, #5cb6ff). Everything else on the page —
   section titles, banners, connected-state pill, etc. — keeps the
   default accent-red palette. */
.tile.lobby-tile .tile-icon.tile-icon-img {
    filter: brightness(0) saturate(100%) invert(72%) sepia(34%) saturate(2061%) hue-rotate(186deg) brightness(101%) contrast(101%);
}
.tile.lobby-tile:active {
    border-color: var(--blue);
}
.tile-title { font-weight: 700; font-size: 16px; margin-top: 8px; }
.tile-sub { font-size: 12px; color: var(--text-dim); margin-top: 2px; }

.section-h {
    margin: 24px 0 12px;
    font-size: 13px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    color: var(--text-mute);
}

/* Collapsible role sections in the unit picker. <details> normally
   hides non-summary children when closed, but the project's higher-
   specificity .list / .list-item rules override that, so this rule
   restores the collapse behavior explicitly. */
details.picker-section:not([open]) > ul.list {
    display: none;
}
details.picker-section > summary.section-h {
    list-style-position: inside;
}
details.picker-section > summary.section-h::-webkit-details-marker {
    color: var(--text-mute);
}

/* v0.10.206 — collapsible role sections in the in-match army panel.
   Same shape as picker-section but the body wrapper is a div, not a
   <ul>, and we add a small count chip on the right of the summary so
   the player can see "Characters · 4" / "Battleline · 6" at a glance
   while the section is collapsed.
   v0.10.344 — re-introduce a visible disclosure caret. v0.10.342 tried
   to use the native `::-webkit-details-marker` like `picker-section`
   does, but the army-panel summary has `display: flex` (to push the
   count chip to the right) which suppresses the native list-item
   marker. So instead we render an explicit ▶ via `::before` and
   rotate it 90° when the parent <details> is `[open]`. Always
   visible, matches the factions-page disclosure-arrow vibe. */
details.army-panel-section { margin: 4px 0 0; }
details.army-panel-section > summary.section-h {
    list-style: none;
    cursor: pointer;
    user-select: none;
    display: flex;
    align-items: center;
    gap: 8px;
}
details.army-panel-section > summary.section-h::-webkit-details-marker { display: none; }
details.army-panel-section > summary.section-h::marker { content: ""; }
details.army-panel-section > summary.section-h::before {
    content: "▶";
    flex: 0 0 auto;
    font-size: 10px;
    line-height: 1;
    color: var(--text-mute);
    opacity: 0.85;
    display: inline-block;
    transform: rotate(0deg);
    transform-origin: 50% 50%;
    transition: transform 160ms ease;
}
details.army-panel-section[open] > summary.section-h::before {
    transform: rotate(90deg);
}
.army-panel-section-count {
    margin-left: auto;           /* push the count chip to the right edge */
    font-size: 11px;
    font-weight: 700;
    color: var(--text-mute);
    background: var(--bg-elev-2);
    border-radius: 999px;
    padding: 2px 8px;
    letter-spacing: 0.04em;
}
.army-panel-section-body { padding-top: 4px; }

/* ============ list (factions, units, etc.) ============ */
.list {
    list-style: none;
    margin: 0;
    padding: 0;
    border-radius: var(--radius);
    background: var(--bg-elev);
    border: 1px solid var(--line);
    overflow: hidden;
}
.list-item {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 14px 16px;
    border-bottom: 1px solid var(--line);
    color: var(--text);
    text-decoration: none;
    min-height: 56px;
}
.list-item:last-child { border-bottom: none; }
.list-item:active { background: var(--bg-elev-2); }
.list-item-main { flex: 1; min-width: 0; }
.list-item-title { font-weight: 600; font-size: 15px; }
.list-item-sub { font-size: 12px; color: var(--text-dim); margin-top: 2px; }
.list-item-chev { color: var(--text-mute); font-size: 22px; }

/* v0.10.341 — faction "integration completeness" badge, sat to the
 * right of the faction title. Only titles that contain a badge become
 * a flex row (scoped via :has so generic list titles are untouched).
 * The name text can truncate; the badge stays intact at the far right. */
.list-item-title:has(.faction-complete-badge) {
    display: flex;
    align-items: center;
    gap: 8px;
}
.faction-title-text {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.faction-complete-badge {
    margin-left: auto;          /* push to the right edge of the title row */
    flex: 0 0 auto;
    font-size: 11px;
    font-weight: 700;
    letter-spacing: 0.02em;
    padding: 2px 8px;
    border-radius: 999px;
    white-space: nowrap;
}
.faction-complete-badge.tier-hi  { color: var(--green);     background: rgba(76, 184, 122, 0.12); }
.faction-complete-badge.tier-mid { color: var(--gold);      background: rgba(214, 178, 92, 0.14); }
.faction-complete-badge.tier-lo  { color: var(--text-mute); background: var(--bg-elev-2); }

/* Share-status icon on rows owned by the local user. Cloud (☁) =
   local-only / private. Globe-with-meridians (🌐) = shared publicly
   and in the Discovery Network. Sits between the row's main content
   and the chevron — same row as the chev so the layout stays clean. */
.share-status {
    flex-shrink: 0;
    font-size: 18px;
    line-height: 1;
    opacity: 0.7;
}
.share-status-public { color: var(--green, #5cd07a); opacity: 0.95; }
.share-status-local  { color: var(--text-mute); }

.list-item-badge {
    flex-shrink: 0;
    padding: 2px 8px;
    border-radius: 10px;
    font-size: 11px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.04em;
}
.role-Battleline { background: rgba(92, 182, 255, 0.12); color: var(--blue); }
.role-Characters { background: rgba(224, 177, 58, 0.14); color: var(--gold); }
.role-Other      { background: rgba(160, 163, 176, 0.12); color: var(--text-dim); }

/* search/filter input */
.filter-bar {
    position: sticky;
    top: calc(var(--topbar-h) + var(--safe-top));
    background: var(--bg);
    z-index: 30;
    padding: 8px 0 12px;
    margin: -8px 0 0;
}
/* Subfaction pill row on the faction page. Horizontal scroll on narrow
   screens so a 12-chapter Space Marines list doesn't push the layout. */
.subfaction-bar {
    display: flex;
    flex-wrap: nowrap;
    gap: 6px;
    margin-bottom: 14px;
    overflow-x: auto;
    padding-bottom: 4px;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: thin;
}
.subfaction-pill {
    flex: 0 0 auto;
    padding: 6px 12px;
    border-radius: 999px;
    background: var(--bg-elev-2);
    border: 1px solid var(--line);
    color: var(--text-dim);
    font-size: 12.5px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    cursor: pointer;
    white-space: nowrap;
    transition: color 0.12s ease, border-color 0.12s ease, background 0.12s ease;
}
.subfaction-pill:hover  { color: var(--text); border-color: var(--accent-soft); }
.subfaction-pill.is-active {
    background: var(--accent);
    border-color: var(--accent);
    color: #fff;
}

/* Two-handle points range slider on the Add Unit / Faction Datasheet pages.
   Two stacked <input type="range"> elements share the same track; the fill
   div under them visually marks the active range. The .range-slider-track
   is positioned relative so the two thumbs and fill overlay on top. */
.range-slider-wrap {
    margin: 4px 0 14px;
    padding: 8px 12px 12px;
    background: var(--bg-elev);
    border: 1px solid var(--line);
    border-radius: 10px;
}
.range-slider-row {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    font-size: 12.5px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--text-dim);
    margin-bottom: 8px;
}
.range-slider-vals {
    color: var(--accent);
    font-family: ui-monospace, "SF Mono", Menlo, monospace;
    text-transform: none;
    letter-spacing: 0;
}
.range-slider-track {
    position: relative;
    height: 28px;
}
.range-slider-track::before {
    /* Inactive track strip behind everything. */
    content: "";
    position: absolute;
    left: 0; right: 0;
    top: 50%;
    transform: translateY(-50%);
    height: 4px;
    background: var(--bg-elev-2);
    border-radius: 2px;
}
.range-slider-fill {
    /* Active strip between min/max — set by JS via left/right %. */
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    height: 4px;
    background: var(--accent);
    border-radius: 2px;
    pointer-events: none;
}
.range-slider-thumb {
    /* The two range inputs are absolutely-positioned full-width on top of
       the track. pointer-events on the input element itself is none so
       the player can't drag the bar — only the thumb circle receives
       pointer events via the ::-webkit-slider-thumb pseudo-element. */
    appearance: none;
    -webkit-appearance: none;
    position: absolute;
    left: 0; right: 0;
    top: 0;
    width: 100%;
    height: 28px;
    background: transparent;
    pointer-events: none;
    margin: 0;
    padding: 0;
}
.range-slider-thumb:focus { outline: none; }
.range-slider-thumb::-webkit-slider-thumb {
    -webkit-appearance: none;
    appearance: none;
    pointer-events: auto;
    width: 18px;
    height: 18px;
    border-radius: 50%;
    background: var(--accent);
    border: 2px solid var(--bg);
    box-shadow: 0 1px 3px rgba(0,0,0,0.4);
    cursor: pointer;
    margin-top: 0;
}
.range-slider-thumb::-moz-range-thumb {
    pointer-events: auto;
    width: 18px;
    height: 18px;
    border-radius: 50%;
    background: var(--accent);
    border: 2px solid var(--bg);
    box-shadow: 0 1px 3px rgba(0,0,0,0.4);
    cursor: pointer;
}
/* The max-handle slider needs z-index so it sits above the min slider on
   the overlap, otherwise dragging max past the centre fails. */
.range-slider-thumb.range-slider-max { z-index: 2; }
.range-slider-thumb.range-slider-min { z-index: 1; }

/* Subfaction badge row on the datasheet header — small accent-soft pill
   for each subfaction this unit explicitly belongs to (Black Templars,
   Sautekh Dynasty, etc.). Only rendered when the unit has subfaction
   keywords; generic faction units stay clean. */
.ds-subfaction-row {
    margin-top: 6px;
    display: flex;
    flex-wrap: wrap;
    gap: 4px;
}
.subfaction-badge {
    display: inline-block;
    padding: 2px 8px;
    border-radius: 999px;
    background: var(--accent-tint);
    border: 1px solid rgba(200, 16, 46, 0.4);
    color: var(--accent-soft);
    font-size: 10.5px;
    font-weight: 800;
    letter-spacing: 0.04em;
    text-transform: uppercase;
}

.filter-input {
    width: 100%;
    padding: 12px 14px;
    border-radius: var(--radius);
    background: var(--bg-elev);
    border: 1px solid var(--line);
    color: var(--text);
    font-size: 15px;
    outline: none;
    transition: border-color 0.15s ease;
}
.filter-input::placeholder { color: var(--text-mute); }
.filter-input:focus { border-color: var(--accent); }

.empty-state {
    padding: 40px 16px;
    text-align: center;
    color: var(--text-dim);
}

/* ============ datasheet ============ */
.ds-header {
    margin-bottom: 16px;
}
/* Model thumbnail variant — the image sits as a low-opacity watermark
 * in the upper-right of the header, in the same slot the faction-icon
 * watermark (.ds-mark) normally occupies. The title block stays
 * readable on top because:
 *   - the thumb is z-index 0 with reduced opacity
 *   - the body block is z-index 1 with right-padding to clear the
 *     thumb's visual carve-out
 *   - the faction icon watermark is hidden when a thumb is present so
 *     the two don't overlap. */
.ds-header.has-thumb {
    position: relative;
    min-height: 120px;
}
.ds-header.has-thumb .ds-header-body {
    position: relative;
    z-index: 1;
    padding-right: 110px;
}
.ds-header.has-thumb .ds-mark {
    display: none;
}
.ds-thumb {
    position: absolute;
    top: -8px;
    right: -8px;
    width: 140px;
    height: 140px;
    object-fit: contain;
    opacity: 0.55;
    pointer-events: none;
    z-index: 0;
}
@media (max-width: 380px) {
    .ds-thumb { width: 100px; height: 100px; }
    .ds-header.has-thumb .ds-header-body { padding-right: 80px; }
}

/* Game-view unit cards (army list view) — same watermark treatment in
   the upper-right of each card, scaled down so it sits behind the
   header text without dominating the smaller card real estate. */
.gv-card.has-thumb {
    position: relative;
}
.gv-card.has-thumb .gv-head {
    position: relative;
    z-index: 1;
    padding-right: 80px;
}
.ds-thumb.gv-thumb {
    position: absolute;
    top: -4px;
    right: -4px;
    width: 90px;
    height: 90px;
    object-fit: contain;
    opacity: 0.55;
    pointer-events: none;
    z-index: 0;
}

/* Picker-row avatar — small left-aligned thumbnail (NOT a watermark)
   so the user can recognise the unit by silhouette while scrolling. */
.list-item.has-avatar {
    align-items: center;
    gap: 10px;
}
.ds-avatar {
    width: 44px;
    height: 44px;
    flex-shrink: 0;
    object-fit: contain;
    background: transparent;
}
.ds-name {
    font-size: 24px;
    font-weight: 900;
    letter-spacing: -0.02em;
    margin: 0;
    line-height: 1.15;
}
.ds-faction {
    color: var(--text-mute);
    font-size: 13px;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    margin-top: 4px;
}
.ds-role-row {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    margin-top: 10px;
}
.chip {
    padding: 4px 10px;
    border-radius: 999px;
    font-size: 12px;
    font-weight: 600;
    background: var(--bg-elev);
    border: 1px solid var(--line);
    color: var(--text-dim);
}
.chip.kw { background: rgba(255, 255, 255, 0.04); }
.chip.fkw { background: var(--accent-tint); color: var(--accent-soft); border-color: rgba(200, 16, 46, 0.3); }
.chip.weapon-ability {
    background: rgba(92, 182, 255, 0.1);
    color: var(--blue);
    border-color: rgba(92, 182, 255, 0.25);
    text-transform: uppercase;
    font-size: 10.5px;
    letter-spacing: 0.04em;
}

.card {
    background: var(--bg-elev);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    padding: 16px;
    margin-bottom: 14px;
}
.card-h {
    font-size: 11px;
    font-weight: 800;
    text-transform: uppercase;
    letter-spacing: 0.1em;
    color: var(--text-mute);
    margin: 0 0 10px;
}

/* model stat blocks */
.models-grid {
    display: flex;
    flex-direction: column;
    gap: 12px;
}
.model-card {
    background: var(--bg-elev-2);
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    padding: 12px;
}
.model-name {
    font-size: 14px;
    font-weight: 700;
    margin-bottom: 8px;
}
.stat-row {
    display: grid;
    grid-template-columns: repeat(6, 1fr);
    gap: 6px;
}
.stat-cell {
    text-align: center;
    background: var(--bg);
    border-radius: 6px;
    padding: 6px 0;
    border: 1px solid var(--line);
}
.stat-label {
    display: block;
    font-size: 9.5px;
    font-weight: 700;
    color: var(--text-mute);
    letter-spacing: 0.06em;
    text-transform: uppercase;
}
.stat-value {
    display: block;
    font-size: 17px;
    font-weight: 800;
    margin-top: 1px;
}
.model-extras {
    margin-top: 8px;
    color: var(--text-dim);
    font-size: 12px;
    display: flex;
    gap: 12px;
    flex-wrap: wrap;
}

/* weapons table */
.weapons-section + .weapons-section { margin-top: 16px; }
.weapons-h {
    font-size: 12px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--text-dim);
    margin: 0 0 8px;
}
.weapon {
    border-bottom: 1px dashed var(--line);
    padding: 10px 0;
}
.weapon:last-child { border-bottom: none; }
.weapon-name {
    font-weight: 700;
    font-size: 14px;
    margin-bottom: 4px;
}
.weapon-name .dice-tag {
    font-size: 11px;
    color: var(--text-mute);
    margin-left: 6px;
    font-weight: 500;
}
.weapon-stats {
    display: grid;
    grid-template-columns: repeat(6, 1fr);
    gap: 4px;
    margin: 6px 0;
}
.weapon-stats .stat-cell {
    background: var(--bg);
    padding: 4px 0;
}
.weapon-stats .stat-value { font-size: 13.5px; }
.weapon-abilities {
    display: flex;
    flex-wrap: wrap;
    gap: 4px;
    margin-top: 6px;
}

/* ability blocks */
.ability {
    padding: 10px 0;
    border-bottom: 1px solid var(--line);
}
.ability:last-child { border-bottom: none; }
.ability-name {
    font-weight: 700;
    font-size: 14px;
    color: var(--accent-soft);
}
.ability-type-tag {
    font-size: 10.5px;
    color: var(--text-mute);
    margin-left: 8px;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    font-weight: 600;
}
.ability-body {
    font-size: 14px;
    color: var(--text);
    margin-top: 4px;
}
.ability-body p { margin: 6px 0; }
.ability-body ul { padding-left: 22px; margin: 6px 0; }
.ability-body .kwb { font-weight: 700; text-transform: uppercase; font-size: 0.92em; letter-spacing: 0.04em; }
.ability-body .kwb2.bluefont { color: var(--blue); font-weight: 700; font-size: 0.9em; letter-spacing: 0.04em; }
.ability-body .redfont { color: var(--accent-soft); font-weight: 700; }
.ability-body .redExample {
    background: rgba(200, 16, 46, 0.06);
    border-left: 3px solid var(--accent);
    padding: 8px 12px;
    border-radius: 4px;
    margin: 8px 0;
    font-size: 13px;
}

.option-list { padding-left: 18px; margin: 0; }
.option-list li { margin: 6px 0; font-size: 14px; }

.cost-grid { display: flex; flex-direction: column; gap: 6px; }
.cost-row {
    display: flex;
    justify-content: space-between;
    align-items: center;
    background: var(--bg-elev-2);
    padding: 8px 12px;
    border-radius: var(--radius-sm);
    font-size: 14px;
}
.cost-pts { font-weight: 800; color: var(--gold); }

.kw-grid { display: flex; flex-wrap: wrap; gap: 6px; }

.damaged-card {
    border-left: 3px solid var(--accent);
    padding-left: 12px;
}
.damaged-range {
    font-size: 11px;
    font-weight: 700;
    color: var(--accent-soft);
    text-transform: uppercase;
    letter-spacing: 0.06em;
}

/* ============ stratagem detail ============ */
.strat-cp {
    display: inline-block;
    background: var(--accent);
    color: #fff;
    font-weight: 800;
    padding: 2px 10px;
    border-radius: 999px;
    font-size: 13px;
    margin-right: 6px;
}
.strat-meta {
    color: var(--text-dim);
    font-size: 12.5px;
    margin: 4px 0 12px;
}

/* ============ dice screen ============ */
.dice-card {
    background: var(--bg-elev);
    border: 1px solid var(--line);
    border-radius: var(--radius-lg);
    padding: 24px;
    margin-bottom: 16px;
}
.dice-result {
    text-align: center;
    font-size: 64px;
    font-weight: 900;
    line-height: 1;
    letter-spacing: -0.04em;
    color: var(--accent-soft);
    margin: 8px 0;
    min-height: 64px;
}
.dice-formula {
    text-align: center;
    color: var(--text-dim);
    font-size: 14px;
    font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
    margin-bottom: 16px;
    word-break: break-all;
}
.dice-rolls {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    justify-content: center;
    margin: 8px 0;
}
.dice-roll {
    width: 36px;
    height: 36px;
    border-radius: 8px;
    background: var(--bg-elev-2);
    border: 1px solid var(--line);
    display: flex;
    align-items: center;
    justify-content: center;
    font-weight: 700;
    font-size: 16px;
}
.dice-roll.crit { background: var(--accent-tint); border-color: var(--accent); color: var(--accent-soft); }
.dice-roll.fail { color: var(--text-mute); }

.dice-buttons {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: 8px;
    margin-bottom: 12px;
}
.dice-btn {
    padding: 12px 0;
    border-radius: var(--radius-sm);
    background: var(--bg-elev-2);
    border: 1px solid var(--line);
    color: var(--text);
    font-weight: 700;
    font-size: 14px;
    transition: transform 0.1s ease;
}
.dice-btn:active { transform: scale(0.95); border-color: var(--accent); }
.dice-input-row {
    display: flex;
    gap: 8px;
    align-items: stretch;
}
.dice-input {
    flex: 1;
    padding: 12px 14px;
    border-radius: var(--radius-sm);
    background: var(--bg);
    border: 1px solid var(--line);
    color: var(--text);
    font-size: 15px;
    font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
    outline: none;
}
.dice-input:focus { border-color: var(--accent); }
.dice-roll-btn {
    padding: 12px 20px;
    border-radius: var(--radius-sm);
    background: var(--accent);
    color: #fff;
    font-weight: 800;
    font-size: 15px;
    border: none;
}
.dice-roll-btn:active { background: var(--accent-soft); }

.dice-hint { color: var(--text-mute); font-size: 12px; text-align: center; }

/* ============ search dialog ============ */
.search-dialog {
    position: fixed;
    inset: 0;
    width: 100%;
    height: 100%;
    max-width: none;
    max-height: none;
    margin: 0;
    padding: 0;
    background: var(--bg);
    color: var(--text);
    border: none;
    z-index: 200;
}
.search-dialog::backdrop { background: rgba(0, 0, 0, 0.6); }
.search-dialog[open] {
    display: flex;
    flex-direction: column;
}
.search-form {
    padding: 8px;
    padding-top: calc(8px + var(--safe-top));
    display: flex;
    gap: 6px;
    align-items: stretch;
    border-bottom: 1px solid var(--line);
    background: var(--bg-elev);
}
.search-form input {
    flex: 1;
    padding: 12px 14px;
    border-radius: var(--radius-sm);
    background: var(--bg);
    border: 1px solid var(--line);
    color: var(--text);
    font-size: 16px; /* ≥16 to prevent iOS zoom */
    outline: none;
}
.search-form input:focus { border-color: var(--accent); }
.search-close {
    width: 44px;
    color: var(--text);
    font-size: 20px;
    border-radius: var(--radius-sm);
}
.search-close:active { background: var(--bg-elev-2); }
.search-results { flex: 1; overflow-y: auto; padding: 8px; padding-bottom: calc(16px + var(--safe-bot)); }
.search-section-h {
    font-size: 11px;
    font-weight: 800;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    color: var(--text-mute);
    margin: 12px 8px 6px;
}
.search-result {
    display: block;
    padding: 12px;
    margin-bottom: 6px;
    background: var(--bg-elev);
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    color: var(--text);
    text-decoration: none;
}
.search-result:active { background: var(--bg-elev-2); }
.search-result-title { font-weight: 700; font-size: 15px; }
.search-result-sub { font-size: 12px; color: var(--text-dim); margin-top: 2px; }
/* Keyword / ability rows toggle their glossary definition inline. */
.search-result-term { cursor: pointer; }
.search-result-desc {
    margin-top: 8px;
    padding-top: 8px;
    border-top: 1px solid var(--line);
    font-size: 13px;
    line-height: 1.45;
    color: var(--text-dim);
}
.search-result-desc[hidden] { display: none; }
.search-result-term[aria-expanded="true"] { background: var(--bg-elev-2); }
.search-result-term[aria-expanded="true"] .search-result-sub { color: var(--accent-soft); }

/* ============ about screen ============ */
.about-stat {
    display: flex;
    justify-content: space-between;
    padding: 8px 0;
    border-bottom: 1px solid var(--line);
    font-size: 14px;
}
.about-stat:last-child { border: none; }
.about-stat .label { color: var(--text-dim); }
.about-stat .value { font-weight: 700; }

/* ============ wider screens (tablet & up) ============ */
@media (min-width: 700px) {
    .tile-grid { grid-template-columns: repeat(3, 1fr); }
    .stat-row { grid-template-columns: repeat(6, 80px); }
    .weapon-stats { grid-template-columns: repeat(6, 80px); }
    .topbar-title { font-size: 19px; }
}
@media (min-width: 980px) {
    .topbar { grid-template-columns: 60px 1fr 60px; }
    .screen {
        padding-top: 24px;
        padding-right: 24px;
        padding-left: 24px;
        /* keep the bottom clearance from the base rule — do NOT use the
           `padding` shorthand here or it resets the bottom calc. */
    }
}

/* utility */
.hidden { display: none !important; }
.spacer-12 { height: 12px; }
.text-mute { color: var(--text-mute); }

/* ============ army builder ============ */

/* big home-page army button */
.army-cta {
    display: flex;
    align-items: center;
    gap: 16px;
    padding: 20px 22px;
    margin: 0 0 24px;
    border-radius: var(--radius-lg);
    background:
        radial-gradient(circle at 100% 0%, rgba(200, 16, 46, 0.5), transparent 55%),
        linear-gradient(140deg, var(--accent) 0%, #7a0a1d 100%);
    color: #fff;
    text-decoration: none;
    border: 1px solid rgba(255, 255, 255, 0.12);
    box-shadow: 0 6px 20px rgba(200, 16, 46, 0.35);
    position: relative;
    overflow: hidden;
    transition: transform 0.12s ease, box-shadow 0.15s ease;
}
.army-cta:active { transform: scale(0.985); }
.army-cta-icon {
    flex-shrink: 0;
    font-size: 38px;
    line-height: 1;
    color: #fff;
    text-shadow: 0 2px 12px rgba(0, 0, 0, 0.4);
}
.army-cta-icon-img {
    width: 44px;
    height: 44px;
    object-fit: contain;
    /* drop-shadow approximates the text-shadow used on the font-icon
     * variants — keeps the visual lift consistent across icon types. */
    filter: drop-shadow(0 2px 12px rgba(0, 0, 0, 0.4));
}
.army-cta-text { flex: 1; min-width: 0; }
.army-cta-title { font-size: 19px; font-weight: 800; letter-spacing: -0.01em; }
.army-cta-sub   { font-size: 12.5px; color: rgba(255, 255, 255, 0.85); margin-top: 2px; }
.army-cta-chev  { font-size: 28px; color: rgba(255, 255, 255, 0.85); }

/* wizard progress */
.wiz-steps {
    display: flex;
    gap: 6px;
    margin: 0 0 20px;
}
.wiz-step {
    flex: 1;
    height: 4px;
    border-radius: 2px;
    background: var(--bg-elev);
    overflow: hidden;
}
.wiz-step.is-done   { background: var(--accent); }
.wiz-step.is-active { background: var(--accent-soft); }

.wiz-h {
    font-size: 22px;
    font-weight: 800;
    letter-spacing: -0.01em;
    margin: 0 0 6px;
}
.wiz-sub {
    color: var(--text-dim);
    font-size: 14px;
    margin: 0 0 18px;
}

.wiz-actions {
    display: flex;
    gap: 8px;
    margin-top: 16px;
}
.btn {
    flex: 1;
    padding: 13px 16px;
    border-radius: var(--radius-sm);
    background: var(--bg-elev);
    border: 1px solid var(--line);
    color: var(--text);
    font-weight: 700;
    font-size: 15px;
    text-align: center;
    text-decoration: none;
    transition: transform 0.1s ease;
}
.btn:active { transform: scale(0.98); }
.btn-primary {
    background: var(--accent);
    border-color: var(--accent);
    color: #fff;
}
.btn-primary:active { background: var(--accent-soft); }
/* "Ready to advance" pulse — applied to the advance-phase button when
   the rules engine has nothing else to ask the active player for. The
   button is already red (var(--accent)); we layer a thick outer halo,
   a wide soft glow, and a text-shadow that all breathe in unison to
   match the intensity of the LAN-match nav indicator. */
.btn-primary.is-ready {
    animation: btn-ready-pulse 1.5s ease-in-out infinite;
}
@keyframes btn-ready-pulse {
    0%, 100% {
        box-shadow:
            0 0 0 2px rgba(255, 60, 90, 0.85),
            0 0 14px rgba(200, 16, 46, 0.70),
            0 0 32px rgba(230, 59, 84, 0.45);
        text-shadow:
            0 0 6px rgba(255, 200, 210, 0.55);
    }
    50% {
        box-shadow:
            0 0 0 4px rgba(255, 60, 90, 1),
            0 0 26px rgba(255, 60, 90, 1),
            0 0 56px rgba(255, 100, 130, 0.95);
        text-shadow:
            0 0 12px rgba(255, 230, 235, 0.95);
    }
}
@media (prefers-reduced-motion: reduce) {
    .btn-primary.is-ready {
        animation: none;
        box-shadow:
            0 0 0 3px rgba(255, 60, 90, 0.9),
            0 0 20px rgba(255, 60, 90, 0.7);
    }
}
.btn-ghost { background: transparent; }
.btn-danger { border-color: rgba(200, 16, 46, 0.4); color: var(--accent-soft); }
.btn:disabled, .btn[aria-disabled="true"] {
    opacity: 0.5;
    pointer-events: none;
}
.btn-sm { padding: 8px 12px; font-size: 13px; flex: 0 0 auto; }
.btn-lg { padding: 16px; font-size: 16px; }

/* big choice cards (battle size, faction, detachment) */
.choice-grid {
    display: grid;
    grid-template-columns: 1fr;
    gap: 10px;
}
@media (min-width: 540px) {
    .choice-grid { grid-template-columns: repeat(2, 1fr); }
}
.choice {
    display: flex;
    align-items: center;
    gap: 14px;
    padding: 14px 16px;
    border-radius: var(--radius);
    background: var(--bg-elev);
    border: 1px solid var(--line);
    color: var(--text);
    text-decoration: none;
    text-align: left;
    cursor: pointer;
    transition: border-color 0.15s ease, background 0.15s ease;
}
.choice:active { background: var(--bg-elev-2); }
.choice.is-selected { border-color: var(--accent); background: var(--accent-tint); }
.choice.is-peer-pick { border-color: var(--blue); background: rgba(92, 182, 255, 0.06); }
.choice.is-agreed {
    border-color: var(--green);
    background: rgba(76, 184, 122, 0.10);
    box-shadow: 0 0 0 1px var(--green);
}

/* Per-card pick badges (You / peer name) shown on a Match-aware choice card. */
.choice-picks {
    display: flex;
    flex-direction: column;
    gap: 3px;
    margin-top: 4px;
    align-items: flex-end;
}
.choice-pick {
    font-size: 10px;
    font-weight: 800;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    padding: 2px 7px;
    border-radius: 999px;
    line-height: 1.4;
    white-space: nowrap;
    max-width: 100px;
    overflow: hidden;
    text-overflow: ellipsis;
}
.choice-pick-me   { background: var(--accent); color: #fff; }
.choice-pick-peer { background: var(--blue);   color: #0d1a26; }
.choice.is-agreed .choice-pick-me,
.choice.is-agreed .choice-pick-peer { background: var(--green); color: #08200f; }

/* Compact "stage confirmed" card — single row with checkmark + label + Change button. */
.card.stage-confirmed {
    background: rgba(76, 184, 122, 0.06);
    border-color: rgba(76, 184, 122, 0.3);
    padding: 12px 14px;
}

/* "Both ready, tap to start" card — same green palette but more pronounced. */
.card.stage-go {
    background: rgba(76, 184, 122, 0.10);
    border-color: var(--green);
    border-width: 2px;
    box-shadow: 0 0 0 1px rgba(76, 184, 122, 0.25);
}

/* Side-by-side ready panel: You vs Peer */
.ready-stage {
    display: flex;
    align-items: stretch;
    gap: 10px;
    margin-top: 10px;
}
.ready-side {
    flex: 1;
    text-align: center;
    padding: 14px 12px;
    background: var(--bg-elev-2);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    min-width: 0;
}
.ready-side.is-ready {
    background: rgba(76, 184, 122, 0.10);
    border-color: var(--green);
}
.ready-name {
    font-size: 11px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--text-mute);
    margin-bottom: 6px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.ready-status {
    font-size: 16px;
    font-weight: 800;
    margin-bottom: 10px;
    color: var(--text);
}
.ready-side.is-ready .ready-status { color: var(--green); }
.ready-divider {
    align-self: center;
    font-size: 11px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    color: var(--text-mute);
}
.ready-waiting {
    font-size: 12px;
    color: var(--text-mute);
    font-style: italic;
}

/* ============ Match: turn wizard ============ */

/* Sticky header showing round / turn / phase / CP */
.game-header {
    position: sticky;
    top: calc(var(--topbar-h) + var(--safe-top));
    z-index: 20;
    margin: -16px -16px 14px;
    padding: 12px 16px 14px;
    background: linear-gradient(180deg, var(--bg-elev) 0%, var(--bg) 100%);
    border-bottom: 1px solid var(--line);
}
.game-round {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 10px;
    font-size: 12px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    color: var(--text-mute);
}
/* Mission name pinned to the right of the round indicator. Truncates with
   ellipsis so a very long mission name doesn't push the round off the bar. */
.game-mission-tag {
    color: var(--accent-soft);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    font-size: 11px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    max-width: 60%;
}
.game-turn {
    font-size: 17px;
    font-weight: 800;
    margin-top: 2px;
    color: var(--text);
}
/* Top-bar CP / army-list row.
 *
 * Wide screens: a single row — left army caption + your CP pill + opponent's
 * CP pill + right army caption. Grid columns: auto 1fr 1fr auto.
 *
 * Narrow screens: collapses to a 2x2 grid with the army captions/buttons on
 * the top row and the CP pills on the bottom, so nothing wraps awkwardly.
 * Children are placed by source order via :nth-child(), so no HTML changes
 * are needed:
 *     1: left  cp-list-col   → top-left
 *     2: cp-pill cp-mine     → bottom-left
 *     3: cp-pill cp-peer     → bottom-right
 *     4: right cp-list-col   → top-right
 */
.game-cp {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr) minmax(0, 1fr) auto;
    align-items: center;
    gap: 6px 8px;
    margin-top: 8px;
}
/* No mobile breakpoint reflow — the cp-list-cols (army-list buttons +
   faction caption) MUST stay anchored to the left and right edges
   beside their owning player's pill at every viewport width. The pills
   already shrink gracefully because `.cp-pill` has `min-width: 0` and
   the player-name span truncates with ellipsis (see `.cp-pill span`),
   so a narrow screen just compresses the names; it never pushes the
   buttons onto a new row. */
.cp-pill {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 6px;
    padding: 6px 10px;
    border-radius: 999px;
    background: var(--bg-elev-2);
    border: 1px solid var(--line);
    font-size: 12px;
    min-width: 0;
    /* Safety net: never let a child (Pain-token chip, long VP value)
       paint past the pill's right edge into the neighbouring pill's
       column on mobile. The responsive rules below shrink content to
       actually fit; this clip is the belt-and-suspenders backstop. */
    overflow: hidden;
}
.cp-pill span {
    color: var(--text-mute);
    text-transform: uppercase;
    letter-spacing: 0.05em;
    font-weight: 700;
    /* Player-name slot — truncate with ellipsis so a long username can't
       grow the pill and push the Army List buttons off-screen on mobile.
       flex: 1 1 auto + min-width: 0 lets the span shrink below its
       intrinsic content width, which is what enables the ellipsis. */
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
/* CP / VP / pain-token badges stay at their natural width so they never
   get clipped — only the name slot gives ground when space is tight. */
.cp-pill b { font-size: 14px; flex: 0 0 auto; }
/* Default colour for the leading <b> in each pill (the CP value). The VP
   <b> gets its own colour below so CP and VP read distinctly. */
.cp-pill.cp-mine  b:first-of-type { color: var(--accent-soft); }
.cp-pill.cp-peer  b:first-of-type { color: var(--blue); }
/* VP slot inside the pill — sits to the right of the CP value, separated by
   a thin divider so the two metrics don't run together at a glance. */
.cp-pill .cp-vp {
    color: var(--gold);
    padding-left: 8px;
    margin-left: 4px;
    border-left: 1px solid var(--line);
    font-family: ui-monospace, "SF Mono", Menlo, monospace;
}
.cp-pill.is-active .cp-vp { color: var(--green); border-left-color: rgba(76, 184, 122, 0.4); }

/* The pill belonging to whoever's turn it currently is gets the same green
   the page background tints to during your turn — plus a pulsing green
   glow so a glance at the top bar tells you who's active. The CP-value
   text also turns green, overriding the per-side red/blue identity color. */
.cp-pill.is-active {
    border-color: var(--green);
    background: rgba(76, 184, 122, 0.12);
    animation: cp-active-pulse 2s ease-in-out infinite;
}
.cp-pill.is-active b { color: var(--green); }
.cp-pill.is-active span { color: var(--green); opacity: 0.85; }
@keyframes cp-active-pulse {
    0%, 100% {
        box-shadow:
            0 0 5px rgba(76, 184, 122, 0.55),
            0 0 10px rgba(76, 184, 122, 0.25);
    }
    50% {
        box-shadow:
            0 0 10px rgba(76, 184, 122, 0.95),
            0 0 18px rgba(76, 184, 122, 0.65);
    }
}
@media (prefers-reduced-motion: reduce) {
    .cp-pill.is-active {
        animation: none;
        box-shadow: 0 0 8px rgba(76, 184, 122, 0.7);
    }
}
/* Mobile sizing — at narrow viewports the Drukhari pill carries up to four
   children (name span + CP + VP + Pain-token chip) which together exceed
   the pill's 1fr grid column. Shrink font + padding so everything fits
   inside the pill; combined with `overflow: hidden` above this guarantees
   no chip bleeds into the neighbour pill. */
@media (max-width: 520px) {
    .cp-pill {
        font-size: 11px;
        padding: 4px 8px;
        gap: 4px;
    }
    .cp-pill b { font-size: 12px; }
    .cp-pill .cp-vp {
        padding-left: 5px;
        margin-left: 2px;
    }
}
@media (max-width: 380px) {
    .cp-pill {
        font-size: 10px;
        padding: 3px 6px;
        gap: 3px;
    }
    .cp-pill b { font-size: 11px; }
    .cp-pill .cp-vp {
        padding-left: 4px;
        margin-left: 1px;
    }
}

/* Wrapper for the army-list trigger button + its faction/detachment
   caption. The caption sits above the button so each side of the CP row
   labels which army the icon will reveal. */
.cp-list-col {
    flex: 0 0 auto;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 4px;
    min-width: 0;
}
.cp-list-meta {
    text-align: center;
    line-height: 1.15;
    max-width: 110px;
}
.cp-list-faction {
    font-size: 11px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--text);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.cp-list-detachment {
    font-size: 10px;
    color: var(--text-mute);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* Army-list trigger buttons that flank the CP pills. The icon is a paper
   sheet — tapping pops out the player's full Game-ready army list as a
   slide-in side panel. */
.cp-list-btn {
    flex: 0 0 auto;
    width: 34px;
    height: 34px;
    padding: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border-radius: 8px;
    background: var(--bg-elev-2);
    border: 1px solid var(--line);
    color: var(--text-dim);
    cursor: pointer;
    transition: color 0.12s ease, border-color 0.12s ease, background 0.12s ease;
}
.cp-list-btn:hover {
    color: var(--accent-soft);
    border-color: var(--accent-soft);
    background: var(--accent-tint);
}
.cp-list-btn svg { display: block; }

/* v0.10.202 — Phase-info icon button. Lives at the end of the
   .game-turn line, inline with the "Player's turn · Charge phase"
   text. Compact, low-contrast at rest so it doesn't fight with the
   turn label for attention; brightens on hover/focus. Background is
   transparent so it sits cleanly inside the header gradient. */
.phase-info-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 22px;
    height: 22px;
    margin-left: 8px;
    padding: 0;
    border: 1px solid transparent;
    border-radius: 50%;
    background: transparent;
    color: var(--text-dim);
    cursor: pointer;
    vertical-align: -3px;
    transition: color 0.12s ease, border-color 0.12s ease, background 0.12s ease;
}
.phase-info-btn:hover,
.phase-info-btn:focus-visible {
    color: var(--accent-soft);
    border-color: var(--accent-soft);
    background: rgba(200, 16, 46, 0.08);
    outline: none;
}
.phase-info-btn svg { display: block; }

/* Phase-info modal — opened by the phase-info-btn. Mirrors the save-
   roll modal's "banner image at the top" layout. */
.phase-info-overlay {
    z-index: 285;
}
.phase-info-modal {
    position: relative;
    max-width: 560px;
    width: min(560px, 92vw);
    padding: 18px 20px 16px;
}
.phase-info-close {
    position: absolute;
    top: 6px;
    right: 8px;
    width: 28px;
    height: 28px;
    padding: 0;
    border: none;
    background: transparent;
    color: var(--text-dim);
    font-size: 18px;
    font-weight: 700;
    line-height: 1;
    cursor: pointer;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    transition: color 0.12s ease;
    z-index: 1;
}
.phase-info-close:hover,
.phase-info-close:focus-visible {
    color: var(--text);
    outline: none;
}
.phase-info-banner-wrap {
    display: flex;
    justify-content: center;
    margin: -4px 0 10px;
}
.phase-info-banner {
    height: 110px;
    width: auto;
    max-width: 100%;
    object-fit: contain;
    opacity: 0.95;
    filter: drop-shadow(0 2px 6px rgba(0, 0, 0, 0.45));
}
.phase-info-title {
    font-size: 18px;
    font-weight: 800;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    text-align: center;
    margin-bottom: 10px;
    color: var(--text);
}
.phase-info-body {
    font-size: 13.5px;
    line-height: 1.55;
    color: var(--text);
    margin: 0 0 12px;
}
.phase-info-toggle-row {
    display: flex;
    justify-content: flex-end;
    margin-top: 4px;
}
.phase-info-toggle {
    font-size: 12.5px;
    font-weight: 700;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--accent-soft);
    cursor: pointer;
    text-decoration: none;
    border-bottom: 1px dashed transparent;
    padding-bottom: 1px;
    transition: color 0.12s ease, border-color 0.12s ease;
}
.phase-info-toggle:hover,
.phase-info-toggle:focus-visible {
    color: var(--text);
    border-bottom-color: var(--accent-soft);
    outline: none;
}

/* Slide-in army list panel */
/* v0.10.340 — selector hoisted to `.modal-overlay.army-panel-overlay` so
   it wins over the base `.modal-overlay { padding: 16px }` rule that
   loads later in this file. Without the extra `.modal-overlay` prefix,
   the cascade kept the 16px inset, which painted a thin dimmed-backdrop
   strip on the docked edge of the panel (visible as the red-rectangle
   highlight in Nathan's screenshot). */
.modal-overlay.army-panel-overlay {
    z-index: 280;
    align-items: stretch;
    padding: 0;
    padding-bottom: 0;
}
.army-panel-overlay.from-left  { justify-content: flex-start; }
.army-panel-overlay.from-right { justify-content: flex-end; }
.army-panel {
    /* v0.10.338 — was width:min(100%,460px) which capped the slider at
       460px on any viewport wider than that, leaving a dimmed-backdrop
       strip on the side opposite the dock. Now fills the full viewport
       width so the panel is flush with both screen edges.
       v0.10.339 — top padding bumped 14 → 36px so the sticky header
       has room to breathe under the browser chrome / iOS notch
       (mirrors the spacing in Nathan's reference screenshot). The
       var(--safe-top) addend folds in hardware safe-area insets so
       the title is never tucked under a notch on supported devices.
       v0.10.340 — restored the 14px horizontal padding inside the
       panel (Nathan's clarifying screenshot showed the unwanted strip
       was OUTSIDE the panel, between the panel edge and the screen
       edge — handled by the overlay-level padding:0 above). */
    /* v0.10.343 — panel width: ~60% of viewport on desktop, but never
       narrower than 400px so the stat / weapon grids inside still read.
       Phones with viewports < 400px get the panel clamped to 100vw
       (effectively full-screen on iPhone-SE-sized viewports, ~90%+ on
       iPhone 12 portrait, and the requested 60% on desktop / tablet). */
    width: max(60vw, 400px);
    max-width: 100vw;
    height: 100vh;
    height: 100dvh;
    overflow-y: auto;
    background: var(--bg);
    /* Top inset is 0 here and lives on .army-panel-header instead. The
       header is position:sticky; top:0 — if the scroll container itself
       carried the top padding, the header would stick 36px DOWN from the
       panel top and scrolling content would show in the gap ABOVE it
       (the "Characters section header floats over YOUR ARMY" bug). With
       the inset on the header, the sticky header covers the full top. */
    padding: 0 14px calc(14px + var(--safe-bot));
    box-shadow: 0 0 30px rgba(0, 0, 0, 0.6);
    -webkit-overflow-scrolling: touch;
}
.army-panel-overlay.from-left  .army-panel { animation: panel-slide-l 0.18s ease-out; border-right: 1px solid var(--line); }
.army-panel-overlay.from-right .army-panel { animation: panel-slide-r 0.18s ease-out; border-left:  1px solid var(--line); }

/* Inside the side panel the global tablet+ rules that fix stat columns to
   80px would overflow — the panel is clamped to ~460px regardless of
   viewport. Force the M/T/Sv/W/Ld/OC and weapon stat grids back to
   fractional columns so they fit the panel's content width on every
   screen size. minmax(0, 1fr) lets them shrink past their min-content
   width if a long value (e.g., long invuln text) would otherwise blow
   out the row. */
.army-panel .stat-row,
.army-panel .weapon-stats {
    grid-template-columns: repeat(6, minmax(0, 1fr));
}
.army-panel .model-card,
.army-panel .gv-card,
.army-panel .card {
    min-width: 0;
}
.army-panel .stat-value,
.army-panel .weapon-stats .stat-value {
    overflow: hidden;
    text-overflow: ellipsis;
}
@keyframes panel-slide-l {
    from { transform: translateX(-100%); }
    to   { transform: translateX(0); }
}
@keyframes panel-slide-r {
    from { transform: translateX(100%); }
    to   { transform: translateX(0); }
}
@media (prefers-reduced-motion: reduce) {
    .army-panel { animation: none; }
}
.army-panel-header {
    position: sticky;
    top: 0;
    background: var(--bg);
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    /* The panel's top inset (browser-chrome / notch breathing room)
       lives on the sticky header, not on .army-panel, so the header
       covers the full top of the scroll area and content can't scroll
       into a gap above it. */
    padding-top: calc(36px + var(--safe-top));
    padding-bottom: 10px;
    margin-bottom: 8px;
    border-bottom: 1px solid var(--line);
    /* Above the unit cards' .gv-head / .ds-header-body, which are
       position:relative; z-index:1. The scroll container (.army-panel)
       isn't its own stacking context, so an equal z-index let the
       later-in-DOM card heads paint over the sticky header while
       scrolling. A higher value keeps content sliding cleanly behind. */
    z-index: 5;
}
.army-panel-title {
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    font-size: 16px;
    color: var(--text);
}
/* v0.10.347 — header actions cluster (Gameplan button + ✕) so the
   ✕ stays on the right while a second affordance can sit next to
   it without breaking the sticky header's flex layout. */
.army-panel-header-actions {
    display: flex;
    align-items: center;
    gap: 8px;
    flex: 0 0 auto;
}
.army-panel-meta {
    display: flex;
    flex-wrap: wrap;
    gap: 6px 14px;
    font-size: 12.5px;
    color: var(--text-dim);
    margin-bottom: 12px;
    padding: 8px 10px;
    background: var(--bg-elev);
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
}

/* ============ mission scoring ============
 *
 * Pre-game mission picker (each row a tappable card with name + rules text),
 * the per-turn scoring modal, the always-visible VP widget that lives in
 * the game header next to the CP pills, and the round-by-round breakdown
 * shown on the game-over card.
 */

/* Pre-game mission picker */
.mission-list {
    display: flex;
    flex-direction: column;
    gap: 8px;
    margin-top: 6px;
}
.mission-pick {
    text-align: left;
    padding: 12px 14px;
    background: var(--bg-elev-2);
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    color: var(--text);
    cursor: pointer;
    transition: border-color 0.12s ease, background 0.12s ease;
}
.mission-pick:hover  { border-color: var(--accent-soft); background: var(--bg-elev); }
.mission-pick:active { transform: scale(0.99); }
/* v0.10.187 — vote-state styles for the now-vote-based mission picker.
 * Mirrors the .choice (battle-size) palette so the lobby reads
 * consistently across all three voting steps. */
.mission-pick.is-selected   { border-color: var(--accent);  box-shadow: 0 0 0 1px rgba(200,16,46,0.35); }
.mission-pick.is-peer-pick  { border-color: rgba(255,200,80,0.7); box-shadow: 0 0 0 1px rgba(255,200,80,0.30); }
.mission-pick.is-agreed     { border-color: var(--green);   box-shadow: 0 0 0 1px rgba(95,220,110,0.40); }
.mission-pick-name {
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    font-size: 15px;
    color: var(--accent-soft);
}
.mission-pick-desc {
    font-size: 13px;
    color: var(--text-dim);
    margin-top: 4px;
    line-height: 1.4;
}

/* Per-turn scoring modal. z-index above .inbox-overlay (295) so the
   modal can't get visually buried under a stray Inbox prompt that
   queued during the active player's turn (Reanimation Protocols,
   Eternal Revenant revive, etc.) — fixed v0.9.95 after reports of
   the modal "never showing" on joiner-side devices. */
.scoring-overlay { z-index: 300; }
.scoring-modal {
    padding: 16px;
    border: 2px solid var(--green);
    box-shadow: 0 0 0 1px rgba(76, 184, 122, 0.35), 0 8px 32px rgba(76, 184, 122, 0.20);
    overflow-y: auto;
}
.scoring-preview {
    margin-top: 10px;
    padding: 10px 12px;
    background: rgba(76, 184, 122, 0.10);
    border: 1px solid var(--green);
    border-radius: var(--radius-sm);
    text-align: center;
    font-size: 13px;
    color: var(--text-dim);
}
.scoring-preview b {
    color: var(--green);
    font-size: 22px;
    font-weight: 900;
    margin: 0 4px;
    font-family: ui-monospace, "SF Mono", Menlo, monospace;
}

/* Game-over round-by-round table */
.game-over-rounds {
    margin-top: 8px;
    background: var(--bg-elev-2);
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    padding: 8px 10px;
}
.game-over-rounds > summary {
    cursor: pointer;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    font-size: 12px;
    color: var(--text-dim);
    list-style: none;
}
.game-over-rounds > summary::-webkit-details-marker { display: none; }
.vp-table {
    width: 100%;
    margin-top: 8px;
    border-collapse: collapse;
    font-size: 13px;
}
.vp-table th {
    text-align: left;
    color: var(--text-mute);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    font-size: 10px;
    padding: 6px 6px;
    border-bottom: 1px solid var(--line);
}
.vp-table td {
    padding: 6px 6px;
    border-bottom: 1px solid rgba(42, 46, 58, 0.6);
}
.vp-table tr:last-child td { border-bottom: none; }

/* Secondary breakdown rows on the game-over table — indented and a
 * touch dimmer than the primary row so the eye reads each round as a
 * group. */
.vp-table tr.vp-sec-row td { color: var(--text-dim); font-size: 12.5px; }
.vp-table tr.vp-sec-row td:nth-child(3) { padding-left: 16px; }
.vp-tag {
    display: inline-block;
    margin-right: 6px;
    padding: 1px 5px;
    border-radius: 3px;
    font-size: 9.5px;
    font-weight: 800;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    vertical-align: 0.05em;
}
.vp-tag-prim { background: rgba(76, 184, 122, 0.18); color: var(--green); }
.vp-tag-sec  { background: rgba(120, 145, 230, 0.18); color: var(--blue); }

/* Game-over verdict summary — shows P + S buckets per side. */
.vp-summary {
    background: var(--bg-elev-2);
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    padding: 8px 10px;
    margin-bottom: 12px;
    font-size: 13px;
}
.vp-summary-row {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    padding: 3px 0;
}
.vp-summary-row + .vp-summary-row { border-top: 1px solid rgba(42, 46, 58, 0.6); }
.vp-summary-row > span:first-child {
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    font-size: 11px;
    color: var(--text-mute);
}
.vp-summary-buckets {
    font-family: ui-monospace, "SF Mono", Menlo, monospace;
    color: var(--text-dim);
}
.vp-summary-buckets b { color: var(--text); }

/* ============ secondary picker (pre-game step 2) ============
 *
 * Multi-select chip-style list. Mirrors .mission-pick visually but
 * supports a "is-selected" state with a check tick that animates in.
 */
.secondary-list {
    display: flex;
    flex-direction: column;
    gap: 8px;
    margin-top: 6px;
}
.secondary-status {
    display: flex;
    flex-direction: column;
    gap: 4px;
    margin-bottom: 8px;
    padding: 8px 10px;
    background: var(--bg-elev-2);
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    font-size: 13px;
}
.secondary-status-line {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
}
.secondary-status-line span {
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    font-size: 11px;
    color: var(--text-mute);
}
.secondary-pick {
    text-align: left;
    padding: 12px 14px;
    background: var(--bg-elev-2);
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    color: var(--text);
    cursor: pointer;
    transition: border-color 0.12s ease, background 0.12s ease, transform 0.08s ease;
    width: 100%;
}
.secondary-pick:hover  { border-color: var(--accent-soft); background: var(--bg-elev); }
.secondary-pick:active { transform: scale(0.99); }
.secondary-pick.is-selected {
    border-color: var(--green);
    background: rgba(76, 184, 122, 0.10);
}
.secondary-pick-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
}
.secondary-pick-name {
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    font-size: 14.5px;
    color: var(--accent-soft);
}
.secondary-pick.is-selected .secondary-pick-name { color: var(--green); }
.secondary-pick-mark {
    width: 22px;
    height: 22px;
    flex-shrink: 0;
    border: 1.5px solid var(--line);
    border-radius: 4px;
    display: flex;
    align-items: center;
    justify-content: center;
    color: transparent;
    font-weight: 900;
    font-size: 14px;
    transition: background 0.12s ease, border-color 0.12s ease, color 0.12s ease;
}
.secondary-pick.is-selected .secondary-pick-mark {
    background: var(--green);
    border-color: var(--green);
    color: #08110b;
}
.secondary-pick-desc {
    font-size: 13px;
    color: var(--text-dim);
    margin-top: 6px;
    line-height: 1.4;
}

/* v0.10.318 — Prioritized pill on the in-match secondary picker.
 * Mirrors the blue treatment .gp-secondary-row:has(input:checked) uses
 * in the army builder so the same secondaries the player flagged in
 * their gameplan stand out at pick-time. Independent of .is-selected
 * (you can prioritize without picking, and vice versa); selection
 * still wins the foreground (green name + checkmark). */
.secondary-pick.is-prioritized {
    border-color: var(--blue, #4ea3ff);
    background: rgba(78, 163, 255, 0.10);
}
.secondary-pick.is-prioritized:hover {
    background: rgba(78, 163, 255, 0.16);
}
.prioritized-pill {
    display: inline-block;
    margin-left: 6px;
    padding: 1px 7px;
    font-size: 10.5px;
    font-weight: 800;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    border-radius: 999px;
    background: rgba(78, 163, 255, 0.22);
    border: 1px solid var(--blue, #4ea3ff);
    color: var(--blue, #4ea3ff);
    vertical-align: middle;
}

/* "Locked in" view of your own picks (after you confirm) */
.secondary-locked {
    margin-top: 4px;
    padding: 10px 12px;
    background: rgba(76, 184, 122, 0.06);
    border: 1px solid var(--green);
    border-radius: var(--radius-sm);
}
.secondary-locked-h {
    font-weight: 800;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    font-size: 11px;
    color: var(--green);
    margin-bottom: 4px;
}
.secondary-locked-list {
    margin: 0 0 6px 0;
    padding-left: 18px;
    font-size: 13.5px;
}
.secondary-locked-list li { margin: 2px 0; }

/* First-turn card summary — two columns of locked-in picks. */
.secondary-summary {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 10px;
    margin: 6px 0 10px;
}
.secondary-summary-col {
    background: var(--bg-elev-2);
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    padding: 8px 10px;
}
.secondary-summary-h {
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    font-size: 11px;
    color: var(--text-mute);
    margin-bottom: 4px;
}
@media (max-width: 480px) {
    .secondary-summary { grid-template-columns: 1fr; }
}

/* ============ reserves picker (pre-game step 3) ============
 *
 * Multi-select list of the player's units. Mirrors .secondary-pick visually
 * but the row is denser (no description text) and carries a small Deep
 * Strike badge when the unit has the ability.
 */
.reserves-list {
    display: flex;
    flex-direction: column;
    gap: 6px;
    margin-top: 6px;
}
.reserves-pick {
    text-align: left;
    padding: 10px 12px;
    background: var(--bg-elev-2);
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    color: var(--text);
    cursor: pointer;
    transition: border-color 0.12s ease, background 0.12s ease, transform 0.08s ease;
    width: 100%;
}
.reserves-pick:hover  { border-color: var(--accent-soft); background: var(--bg-elev); }
.reserves-pick:active { transform: scale(0.99); }
.reserves-pick.is-selected {
    border-color: var(--green);
    background: rgba(76, 184, 122, 0.10);
}
.reserves-pick-head {
    display: flex;
    align-items: center;
    gap: 8px;
}
.reserves-pick-name {
    flex: 1;
    font-weight: 700;
    font-size: 13.5px;
    color: var(--text);
}
.reserves-pick.is-selected .reserves-pick-name { color: var(--green); }
.reserves-pick-mark {
    width: 20px;
    height: 20px;
    flex-shrink: 0;
    border: 1.5px solid var(--line);
    border-radius: 4px;
    display: flex;
    align-items: center;
    justify-content: center;
    color: transparent;
    font-weight: 900;
    font-size: 12px;
    transition: background 0.12s ease, border-color 0.12s ease, color 0.12s ease;
}
.reserves-pick.is-selected .reserves-pick-mark {
    background: var(--green);
    border-color: var(--green);
    color: #08110b;
}
.reserves-ds-badge {
    flex-shrink: 0;
    padding: 1px 6px;
    border-radius: 3px;
    font-size: 9.5px;
    font-weight: 800;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    background: rgba(120, 145, 230, 0.18);
    color: var(--blue);
}
/* Wraps the badge group on a reserves-pick row so multiple chips wrap
   gracefully on narrower screens instead of pushing the ✓ mark off
   the right edge. */
.reserves-pick-badges {
    display: flex;
    flex-wrap: wrap;
    gap: 4px;
    flex-shrink: 1;
    min-width: 0;
    justify-content: flex-end;
}
/* Generic reserves/deployment indicator. Same silhouette as the Deep
   Strike badge but with a neutral muted palette so DS keeps the eye-
   catching blue treatment. Per-kind colour overrides below tag the
   most distinctive faction-flavored arrival mechanics. */
.reserves-deploy-badge {
    flex-shrink: 0;
    padding: 1px 6px;
    border-radius: 3px;
    font-size: 9.5px;
    font-weight: 800;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    background: rgba(180, 180, 200, 0.14);
    color: var(--text-dim);
    border: 1px solid rgba(180, 180, 200, 0.25);
}
.reserves-deploy-badge-scouts {
    background: rgba(76, 184, 122, 0.16);
    color: var(--green);
    border-color: rgba(76, 184, 122, 0.40);
}
.reserves-deploy-badge-infiltrators {
    background: rgba(76, 184, 122, 0.10);
    color: var(--green);
    border-color: rgba(76, 184, 122, 0.30);
}
.reserves-deploy-badge-outflank,
.reserves-deploy-badge-webway,
.reserves-deploy-badge-cult-ambush,
.reserves-deploy-badge-translocate,
.reserves-deploy-badge-teleport,
.reserves-deploy-badge-lurk {
    background: rgba(184, 80, 182, 0.14);
    color: #d29bd1;
    border-color: rgba(184, 80, 182, 0.40);
}
.reserves-deploy-badge-lone-op,
.reserves-deploy-badge-stealth {
    background: rgba(224, 177, 58, 0.10);
    color: var(--gold);
    border-color: rgba(224, 177, 58, 0.30);
}

/* ============ in-game reserves card ============ */
.reserves-card {
    border-left: 3px solid var(--blue);
}
.reserves-card-list {
    display: flex;
    flex-direction: column;
    gap: 6px;
    margin-top: 4px;
}
.reserves-card-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    padding: 9px 12px;
    background: var(--bg-elev-2);
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    color: var(--text);
    width: 100%;
    text-align: left;
    font: inherit;
}
button.reserves-card-row {
    cursor: pointer;
    transition: border-color 0.12s ease, background 0.12s ease, transform 0.08s ease;
}
button.reserves-card-row:hover {
    border-color: var(--blue);
    background: var(--bg-elev);
}
button.reserves-card-row:active { transform: scale(0.99); }
.reserves-card-row-name {
    flex: 1;
    font-weight: 700;
    font-size: 13.5px;
}
.reserves-card-row-meta {
    display: flex;
    align-items: center;
    gap: 8px;
    flex-shrink: 0;
}
.reserves-card-row-action {
    color: var(--blue);
    font-weight: 700;
    font-size: 12.5px;
    text-transform: uppercase;
    letter-spacing: 0.04em;
}

/* ============ PRECISION allocate row ============ */
.precision-opts {
    display: flex;
    flex-wrap: wrap;
    gap: 4px;
    flex-shrink: 0;
}
.precision-opt {
    padding: 4px 8px;
    background: var(--bg);
    border: 1px solid var(--line);
    border-radius: 4px;
    color: var(--text-dim);
    font-size: 12px;
    font-weight: 700;
    cursor: pointer;
    transition: border-color 0.12s ease, background 0.12s ease, color 0.12s ease;
}
.precision-opt:hover { border-color: var(--accent-soft); }
.precision-opt.is-active {
    background: rgba(76, 184, 122, 0.15);
    border-color: var(--green);
    color: var(--green);
}

/* ============ HAZARDOUS test modal ============
 * Two-class selector so we beat .wizard-overlay (z-index: 290) without
 * relying on source order. Hazardous tests are kicked off from the
 * fight/shoot wizard's confirm — the wizard stays mounted underneath
 * while the player rolls, so the hazardous modal must stack on top.
 */
.modal-overlay.hazardous-overlay { z-index: 305; }
.hazardous-modal {
    padding: 16px;
    border: 2px solid var(--accent-soft);
    box-shadow: 0 0 0 1px rgba(180, 90, 90, 0.3), 0 8px 32px rgba(0,0,0,0.5);
    max-width: 480px;
}
.hazardous-list {
    display: flex;
    flex-direction: column;
    gap: 10px;
}
.hazardous-row {
    background: var(--bg-elev-2);
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    padding: 10px 12px;
}
.hazardous-row-h {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: 8px;
    font-size: 13.5px;
    margin-bottom: 6px;
}
.hz-dice {
    display: flex;
    flex-wrap: wrap;
    gap: 4px;
    min-height: 22px;
}
.hz-die {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 22px; height: 22px;
    border-radius: 4px;
    background: var(--bg);
    border: 1px solid var(--line);
    color: var(--text-dim);
    font-size: 12px;
    font-weight: 800;
    font-family: ui-monospace, "SF Mono", Menlo, monospace;
}
.hz-die-fail {
    background: rgba(180, 60, 60, 0.20);
    border-color: var(--accent-soft);
    color: var(--accent-soft);
}

/* ============ pre-game embark picker ============ */
.emb-cap-list {
    display: flex;
    flex-direction: column;
    gap: 4px;
    margin-bottom: 10px;
    padding: 8px 10px;
    background: var(--bg-elev-2);
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
}
.emb-cap-row {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    font-size: 13px;
}
.emb-cap-row.is-over .emb-cap-num b { color: var(--accent-soft); }
.emb-cap-num {
    font-family: ui-monospace, "SF Mono", Menlo, monospace;
    color: var(--text-dim);
}
.emb-cap-num b { color: var(--gold); font-weight: 800; }
.emb-candidate-list {
    display: flex;
    flex-direction: column;
    gap: 6px;
}
.emb-candidate {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 8px;
    padding: 8px 10px;
    background: var(--bg-elev-2);
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    font-size: 13.5px;
}
.emb-candidate-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.emb-candidate-name { font-weight: 700; }
.emb-candidate-warn {
    font-size: 11px;
    color: var(--accent-soft);
    line-height: 1.35;
    font-weight: 600;
}
.emb-select {
    padding: 5px 8px;
    background: var(--bg);
    border: 1px solid var(--line);
    border-radius: 4px;
    color: var(--text);
    font-size: 13px;
    min-width: 140px;
}
.emb-locked-transport {
    margin-bottom: 8px;
}
.emb-locked-h {
    font-weight: 700;
    font-size: 13px;
    color: var(--text);
    margin-bottom: 2px;
}

/* ============ in-game transports card ============ */
.transports-card {
    border-left: 3px solid var(--gold);
}
.transports-list {
    display: flex;
    flex-direction: column;
    gap: 10px;
}
.transport-row {
    background: var(--bg-elev-2);
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    padding: 10px 12px;
}
.transport-row-h {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: 8px;
    margin-bottom: 6px;
}
.transport-row-name {
    font-weight: 700;
    font-size: 14px;
    color: var(--text);
}
.transport-row-cap {
    font-size: 12.5px;
    color: var(--text-dim);
    font-family: ui-monospace, "SF Mono", Menlo, monospace;
}
.transport-row-cap b { color: var(--gold); font-weight: 800; }
.transport-riders {
    display: flex;
    flex-direction: column;
    gap: 4px;
    margin-bottom: 6px;
}
.transport-rider {
    padding: 4px 8px;
    background: var(--bg);
    border: 1px solid var(--line);
    border-radius: 4px;
    font-size: 13px;
}
/* Single-line layout: text on the left, icon buttons (red ↓ disembark
   + white ↑ embark) pinned to the right. The text spans get min-width:0
   so they truncate with ellipsis instead of pushing the icons offscreen
   on narrow rider names. */
.transport-rider-name {
    display: flex;
    align-items: center;
    gap: 6px;
}
.transport-rider-info {
    flex: 1;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.transport-rider-empty {
    background: transparent;
    border-style: dashed;
    border-color: var(--line);
}
.transport-icon-btn {
    flex-shrink: 0;
    width: 24px;
    height: 24px;
    border-radius: 4px;
    border: 1px solid;
    padding: 0;
    margin: 0;
    font: 900 14px/1 ui-sans-serif, system-ui, sans-serif;
    cursor: pointer;
    display: inline-flex;
    align-items: center;
    justify-content: center;
}
.transport-icon-disembark {
    background: rgba(200, 16, 46, 0.18);
    color: var(--accent-soft);
    border-color: var(--accent-soft);
}
.transport-icon-disembark:hover {
    background: rgba(200, 16, 46, 0.28);
}
.transport-icon-embark {
    background: var(--bg-elev-2);
    color: var(--text);
    border-color: var(--line);
}
.transport-icon-embark:hover {
    background: var(--bg-elev);
    border-color: var(--text-dim);
}

/* ============ combat doctrines picker modal ============ */
.doctrine-modal {
    border: 2px solid var(--blue);
    box-shadow: 0 0 0 1px rgba(120, 145, 230, 0.30), 0 8px 32px rgba(0,0,0,0.5);
    padding: 16px;
    max-width: 520px;
}
.doctrine-list {
    display: flex;
    flex-direction: column;
    gap: 8px;
    margin-top: 6px;
}
.doctrine-pick {
    text-align: left;
    padding: 10px 12px;
    background: var(--bg-elev-2);
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    color: var(--text);
    cursor: pointer;
    transition: border-color 0.12s ease, background 0.12s ease;
    width: 100%;
}
.doctrine-pick:not(:disabled):hover {
    border-color: var(--blue);
    background: var(--bg-elev);
}
.doctrine-pick.is-used {
    opacity: 0.55;
    cursor: not-allowed;
}
.doctrine-pick-h {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: 8px;
    margin-bottom: 4px;
}
.doctrine-pick-name {
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    font-size: 14px;
    color: var(--blue);
}
.doctrine-pick-desc {
    font-size: 13px;
    color: var(--text-dim);
    line-height: 1.4;
}
.doctrine-used-tag {
    flex-shrink: 0;
    padding: 1px 6px;
    border-radius: 3px;
    font-size: 9.5px;
    font-weight: 800;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    background: rgba(120, 145, 230, 0.14);
    color: var(--text-mute);
}

/* ============ desperate escape modal ============ */
.desperate-modal {
    border: 2px solid var(--accent-soft);
    box-shadow: 0 0 0 1px rgba(180, 60, 60, 0.30), 0 8px 32px rgba(0,0,0,0.5);
    padding: 16px;
    max-width: 520px;
}
.desperate-rider-list {
    display: flex;
    flex-direction: column;
    gap: 8px;
    margin-top: 8px;
}
.desperate-rider {
    display: flex;
    justify-content: space-between;
    padding: 8px 10px;
    background: var(--bg-elev-2);
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    font-size: 13.5px;
}
.desperate-rider-row {
    background: var(--bg-elev-2);
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    padding: 10px 12px;
}
.desperate-rider-h {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: 8px;
    margin-bottom: 6px;
    font-size: 13.5px;
}

/* ============ embark picker modal ============ */
.embark-overlay { z-index: 285; }
.embark-modal {
    padding: 16px;
    border: 2px solid var(--gold);
    box-shadow: 0 0 0 1px rgba(200, 160, 80, 0.3), 0 8px 32px rgba(0,0,0,0.5);
    max-width: 520px;
}
.embark-list {
    display: flex;
    flex-direction: column;
    gap: 6px;
    margin-top: 6px;
}
.embark-pick {
    text-align: left;
    padding: 10px 12px;
    background: var(--bg-elev-2);
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    color: var(--text);
    cursor: pointer;
    transition: border-color 0.12s ease, background 0.12s ease;
    width: 100%;
}
.embark-pick:hover { border-color: var(--gold); background: var(--bg-elev); }
.embark-pick.is-warning { border-color: var(--accent-soft); }
.embark-pick-name {
    font-weight: 700;
    font-size: 13.5px;
    color: var(--text);
    margin-bottom: 2px;
}
/* Bright leader chip on the embark candidate row so the player can
   tell which unit has which leader attached — important when several
   candidates share a name (e.g., two Crusader Squads, only one of
   them is riding with the Marshal). */
.embark-leader-chip {
    display: inline-block;
    padding: 1px 6px;
    margin-left: 4px;
    border-radius: 3px;
    font-size: 11px;
    font-weight: 700;
    background: rgba(120, 145, 230, 0.18);
    color: var(--blue, #6f9bd6);
    border: 1px solid rgba(120, 145, 230, 0.40);
}
.embark-pick-meta {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 8px;
    font-size: 12px;
    color: var(--text-dim);
}
.embark-warn {
    color: var(--accent-soft);
    font-weight: 700;
}

/* ============ scoring modal sections (primary + secondaries) ============
 *
 * The end-of-turn scoring modal renders one .scoring-section per mission
 * (primary first, then each chosen secondary). Each section has a header
 * row with a tag badge ("Primary" / "Secondary"), the mission name, and
 * a live-updating "+N VP" pill on the right.
 */
.scoring-section {
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    padding: 10px 12px;
    margin-bottom: 10px;
    background: var(--bg-elev-2);
}
.scoring-section-h {
    display: flex;
    align-items: baseline;
    gap: 8px;
    margin-bottom: 6px;
}
.scoring-section-tag {
    display: inline-block;
    padding: 1px 6px;
    border-radius: 3px;
    font-size: 9.5px;
    font-weight: 800;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    background: rgba(76, 184, 122, 0.18);
    color: var(--green);
    flex-shrink: 0;
}
.scoring-section-tag-sec {
    background: rgba(120, 145, 230, 0.18);
    color: var(--blue);
}
.scoring-section-name {
    flex: 1;
    font-weight: 700;
    color: var(--text);
    font-size: 14px;
}
.scoring-section-vp {
    font-family: ui-monospace, "SF Mono", Menlo, monospace;
    font-weight: 800;
    color: var(--text-dim);
    font-size: 13px;
}

/* ============ rules reminder card ============
 *
 * Always-visible during play. Surfaces the active player's detachment
 * rules and faction-wide rules so the player can read them without
 * navigating away mid-turn. Collapsed/expanded via native <details>.
 */
.rules-reminder-card {
    margin-bottom: 12px;
}
/* Shared collapsible-card styling. The summary doubles as the card
   header — full-width tap target with a chevron on the right that
   rotates as the <details> toggles. Used by the in-game rules
   reminder, reserves panel, and transports panel so all three
   collapse/expand consistently. Apply via class="collapsible-card"
   on the <details>; the inner <summary> picks up the styling. */
.collapsible-card > summary,
.rules-reminder-card > summary {
    cursor: pointer;
    list-style: none;
    display: flex;
    align-items: center;
    gap: 8px;
    /* Match .card-h font-weight + size so the summary reads as a card
       header. The .card padding already handles spacing. */
    font-weight: 700;
    font-size: 14px;
}
.collapsible-card > summary::-webkit-details-marker,
.rules-reminder-card > summary::-webkit-details-marker { display: none; }
.collapsible-card > summary::after,
.rules-reminder-card > summary::after {
    content: "▾";
    margin-left: auto;
    color: var(--text-mute);
    transition: transform 0.15s ease;
    flex-shrink: 0;
}
.collapsible-card[open] > summary::after,
.rules-reminder-card[open] > summary::after { transform: rotate(180deg); }
.rules-reminder-sub {
    margin-left: auto;
    padding-right: 22px;
    font-size: 12px;
    font-weight: 500;
    color: var(--text-dim);
    text-transform: none;
    letter-spacing: 0;
}
.rules-reminder-body { margin-top: 8px; }
.rules-block { margin-top: 8px; }
.rules-block-h {
    font-size: 11px;
    font-weight: 800;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--text-mute);
    margin: 8px 0 4px;
}
.rules-ability { margin-bottom: 8px; }
.rules-ability:last-child { margin-bottom: 0; }

/* ============================================================
   Gameplan match-screen card (v0.10.298)
   ============================================================
   Reuses the .rules-reminder-card collapsible silhouette so the
   summary chevron + spacing match its sibling cards. The body is
   a vertical stack of titled sections — overall plan, per-phase
   notes, priority targets, unit roles, stratagem priority,
   secondary mission priority. Phase rows highlight the CURRENT
   phase with the accent colour so the player's eye lands on the
   right beat in a glance. */
.gameplan-match-card { margin-bottom: 12px; }
.gameplan-match-card > summary {
    cursor: pointer;
    list-style: none;
    display: flex;
    align-items: center;
    gap: 8px;
    font-weight: 700;
    font-size: 14px;
}
.gameplan-match-card > summary::-webkit-details-marker { display: none; }
.gameplan-match-card > summary::after {
    content: "▾";
    margin-left: auto;
    color: var(--text-mute);
    transition: transform 0.15s ease;
    flex-shrink: 0;
}
.gameplan-match-card[open] > summary::after { transform: rotate(180deg); }
.gameplan-match-sub {
    margin-left: auto;
    padding-right: 22px;
    font-size: 12px;
    font-weight: 500;
    color: var(--text-dim);
    text-transform: none;
    letter-spacing: 0;
}
.gameplan-match-body { margin-top: 10px; }
.gameplan-match-section {
    margin-top: 10px;
    padding-top: 10px;
    border-top: 1px solid var(--line);
}
.gameplan-match-section:first-child {
    margin-top: 0;
    padding-top: 0;
    border-top: none;
}
.gameplan-match-section-h {
    font-size: 11px;
    font-weight: 800;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--text-mute);
    margin: 0 0 6px;
}
.gameplan-match-overall {
    font-size: 13.5px;
    line-height: 1.5;
    color: var(--text);
    white-space: pre-wrap;
}
.gameplan-match-phases {
    display: flex;
    flex-direction: column;
    gap: 6px;
}
.gameplan-match-phase-row {
    border-left: 3px solid var(--line);
    padding: 4px 0 4px 10px;
}
.gameplan-match-phase-row.is-current {
    border-left-color: var(--accent);
    background: rgba(200, 16, 46, 0.06);
    border-radius: 0 4px 4px 0;
}
.gameplan-match-phase-label {
    font-size: 11px;
    font-weight: 800;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--text-dim);
    margin-bottom: 2px;
}
.gameplan-match-phase-row.is-current .gameplan-match-phase-label {
    color: var(--accent);
}
.gameplan-match-phase-body {
    font-size: 12.5px;
    line-height: 1.45;
    color: var(--text);
    white-space: pre-wrap;
}
.gameplan-match-list {
    margin: 0;
    padding-left: 20px;
    font-size: 13px;
    line-height: 1.5;
    color: var(--text);
}
.gameplan-match-list li { margin-bottom: 4px; }
.gameplan-match-list li:last-child { margin-bottom: 0; }
.gameplan-match-strat-list { color: var(--text); }
.gameplan-match-roles {
    display: flex;
    flex-direction: column;
    gap: 8px;
}
.gameplan-match-role {
    padding: 8px 10px;
    border: 1px solid var(--line);
    border-radius: 6px;
    background: rgba(255, 255, 255, 0.02);
}
.gameplan-match-role-head {
    display: flex;
    align-items: center;
    gap: 6px;
    margin-bottom: 4px;
}
.gameplan-match-role-swatch {
    display: inline-block;
    width: 10px;
    height: 10px;
    border-radius: 2px;
    flex-shrink: 0;
}
.gameplan-match-role-label {
    font-size: 13px;
    font-weight: 700;
    color: var(--text);
}
.gameplan-match-role-units {
    display: flex;
    flex-wrap: wrap;
    gap: 4px;
    margin-bottom: 4px;
}
.gameplan-match-role-unit-chip {
    display: inline-block;
    padding: 1px 6px;
    border-radius: 999px;
    background: rgba(255, 255, 255, 0.06);
    border: 1px solid var(--line);
    font-size: 10.5px;
    font-weight: 600;
    color: var(--text-dim);
}
.gameplan-match-role-plan {
    font-size: 12.5px;
    line-height: 1.45;
    color: var(--text);
    white-space: pre-wrap;
    margin-top: 2px;
}

/* Oath target marker in the shoot/fight target picker */
.wiz-target-row.is-oath {
    border-color: var(--gold);
    background: rgba(224, 177, 58, 0.06);
}
.wiz-target-row.is-oath.is-picked {
    background: rgba(224, 177, 58, 0.14);
    box-shadow: 0 0 0 1px var(--gold);
}
.oath-tag {
    display: inline-block;
    margin-left: 6px;
    padding: 1px 6px;
    border-radius: 999px;
    background: var(--gold);
    color: #1a1404;
    font-size: 9px;
    font-weight: 900;
    letter-spacing: 0.08em;
    vertical-align: middle;
}

/* Generic ability-tag chip for target-row Core ability hints (Lone
   Operative, Stealth, etc.). Shares the chip silhouette with .oath-tag
   but uses faction-neutral neutral colours so it reads as informational
   rather than an action prompt. */
.ability-tag {
    display: inline-block;
    margin-left: 6px;
    padding: 1px 6px;
    border-radius: 999px;
    font-size: 9px;
    font-weight: 900;
    letter-spacing: 0.08em;
    vertical-align: middle;
    text-transform: uppercase;
    border: 1px solid var(--line);
}
.ability-tag.tag-lone    { background: rgba(92, 182, 255, 0.16); color: var(--blue);    border-color: rgba(92, 182, 255, 0.50); }
.ability-tag.tag-stealth { background: rgba(160, 163, 176, 0.18); color: var(--text-dim); border-color: rgba(160, 163, 176, 0.45); }

/* v0.10.319 — Gameplan priority-target tag. Stamped on target-picker
 * rows and on opponent activation rows (when spectating) for enemy
 * units the player's gameplan flagged as priority targets. Gold pill
 * with a 🎯 prefix — distinct from the solid-gold .oath-tag pill so
 * an opponent unit that's BOTH an Oath target and a gameplan priority
 * shows both cleanly. */
.priority-target-tag {
    display: inline-block;
    margin-left: 6px;
    padding: 1px 7px;
    border-radius: 999px;
    font-size: 9px;
    font-weight: 900;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    background: rgba(220, 180, 70, 0.18);
    color: var(--gold, #e0c060);
    border: 1px solid var(--gold, #e0c060);
    vertical-align: middle;
}

/* Pistol XOR per-model picker (Shoot wizard, Pick Weapons step). Shows
   one row per conflicted model — those carrying both a [PISTOL] and a
   non-pistol ranged weapon. The pill toggles between "Other" (default;
   fire non-pistols) and "Pistol" (fire only the pistol). Effective
   attacker counts on each weapon line are recomputed from these picks
   in defaultMods → adjustedAttackerCountForWeapon. */
.pistol-mode-block {
    margin: 0 0 12px;
    padding: 10px 12px;
    background: var(--bg);
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
}
.pistol-mode-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    padding: 6px 0;
    border-top: 1px solid rgba(42, 46, 58, 0.5);
}
.pistol-mode-row:first-of-type { border-top: none; }
.pistol-mode-name {
    flex: 1;
    min-width: 0;
    font-size: 13px;
    font-weight: 700;
    color: var(--text);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.pistol-mode-toggle {
    display: inline-flex;
    border: 1px solid var(--line);
    border-radius: 999px;
    overflow: hidden;
    flex-shrink: 0;
}
.pistol-mode-pill {
    padding: 5px 12px;
    background: var(--bg-elev);
    color: var(--text-dim);
    font-size: 11.5px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    cursor: pointer;
    border: 0;
}
.pistol-mode-pill + .pistol-mode-pill { border-left: 1px solid var(--line); }
.pistol-mode-pill.is-active {
    background: var(--accent);
    color: #fff;
}

/* Command Re-roll picker — small modal with one tappable die per
   pending roll. Uses the same big-die look as the Battle-shock modal so
   players already trained on that aesthetic recognise it. Disabled
   state when CP < 1 dims the dice and routes a Toast on tap. */
/* Two-class selector so we beat the later-in-file `.modal-overlay`
   default (z-index: 280) without relying on source order. */
.modal-overlay.command-reroll-overlay { z-index: 300; }   /* above wizard (290) and inbox (295) */
.modal-overlay.subprofile-info-overlay { z-index: 300; }  /* opened from inside the attack wizard's weapon picker */
.command-reroll-modal { padding: 16px; max-width: 360px; }
.cr-dice-row {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 10px;
    margin: 8px 0;
    flex-wrap: wrap;
}
.cr-die {
    width: 64px;
    height: 64px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    background: var(--bg-elev-2);
    border: 2px solid var(--accent);
    border-radius: 12px;
    font-size: 32px;
    font-weight: 900;
    color: var(--text);
    font-family: ui-monospace, "SF Mono", Menlo, monospace;
    cursor: pointer;
    transition: transform 0.1s ease, background 0.12s ease, box-shadow 0.12s ease;
}
.cr-die:hover  { background: var(--accent-tint); }
.cr-die:active { transform: scale(0.96); }
.cr-die[aria-disabled="true"] { opacity: 0.5; cursor: not-allowed; }

/* Go to Ground modal — green-bordered to read as defensive cover (the
   "tan / mute" idea would clash with the dark theme; green for "safe"
   maps better to the Toast vocabulary and to the existing benefit-of-
   cover visual cues). */
.go-to-ground-modal {
    padding: 16px;
    max-width: 460px;
    border: 2px solid var(--green);
    box-shadow:
        0 0 0 1px rgba(76, 184, 122, 0.30),
        0 8px 32px rgba(76, 184, 122, 0.22);
}
.go-to-ground-modal .inbox-icon.go-to-ground-icon {
    background: var(--green);
    color: #06140a;
    font-weight: 900;
    font-size: 18px;
}

/* Heroic Intervention modal — bright gold to evoke the heroic-character
   silhouette, distinct from Overwatch's softer gold by using a slightly
   warmer surface tint and the gold-on-dark icon swatch. */
.heroic-modal {
    padding: 16px;
    max-width: 460px;
    border: 2px solid var(--gold);
    box-shadow:
        0 0 0 1px rgba(224, 177, 58, 0.34),
        0 8px 32px rgba(224, 177, 58, 0.24);
}
.heroic-modal .inbox-icon.heroic-icon {
    background: var(--gold);
    color: #1a1404;
    font-weight: 900;
    font-size: 22px;
}

/* Counter-Offensive prompt modal — accent-red bordered to read as the
   "fight back" interrupt. Visually distinct from Fire Overwatch's gold
   (movement/charge interrupt) and Lone Op's blue (range check) so the
   player can tell at a glance which interrupt is on screen. */
.counter-offensive-modal {
    padding: 16px;
    max-width: 460px;
    border: 2px solid var(--accent);
    box-shadow:
        0 0 0 1px rgba(200, 16, 46, 0.30),
        0 8px 32px rgba(200, 16, 46, 0.22);
}
.counter-offensive-modal .inbox-icon.counter-offensive-icon {
    background: var(--accent);
    color: #fff;
    font-weight: 900;
    font-size: 22px;
}

/* Fire Overwatch prompt modal — gold-tinted to call attention without
   being mistaken for a damage-allocation prompt. Matches the inbox-modal
   silhouette so it sits in the same visual family as Battle-shock and
   Wound Allocation. */
.overwatch-modal {
    padding: 16px;
    max-width: 460px;
    border: 2px solid var(--gold);
    box-shadow:
        0 0 0 1px rgba(224, 177, 58, 0.30),
        0 8px 32px rgba(224, 177, 58, 0.20);
}
.overwatch-modal .inbox-icon.overwatch-icon {
    background: var(--gold);
    color: #1a1404;
    font-weight: 900;
    font-size: 22px;
}

/* Lone Operative range-confirmation modal — blue-tinted to match the
   LONE OP chip in the target picker. Uses the same silhouette as the
   inbox modals but isn't part of the inbox queue (it's a synchronous
   confirmation that closes either way). */
.loneop-overlay { z-index: 285; }
.loneop-modal {
    padding: 16px;
    max-width: 460px;
    border: 2px solid var(--blue);
    box-shadow:
        0 0 0 1px rgba(92, 182, 255, 0.30),
        0 8px 32px rgba(92, 182, 255, 0.20);
}
.loneop-modal .inbox-icon.loneop-icon {
    background: var(--blue);
    color: #061a2a;
    font-weight: 900;
    font-size: 22px;
}

/* ============ aura reminders in the attack wizard ============
 *
 * The Situation panel surfaces every aura found on the active player's
 * army. Auras carried by attached leaders default ON; others off until
 * the player confirms range. Recognised auras (in AURA_REGISTRY) auto-
 * apply re-rolls / +1 hit / +1 wound when toggled. Unrecognised auras
 * show a "reminder" tag and don't change mods. */
.wiz-auras {
    margin-top: 12px;
    padding-top: 10px;
    border-top: 1px solid var(--line);
}
.aura-row { align-items: flex-start; }
.aura-row .opt-text > b { font-size: 13.5px; }
.aura-source {
    color: var(--text-mute);
    font-size: 11.5px;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    font-weight: 700;
}
.aura-desc {
    margin-top: 4px;
    font-size: 12.5px;
    color: var(--text-dim);
    line-height: 1.45;
    /* Sanitised HTML lives here — keep its inner spans/lists readable. */
}
.aura-desc ul { margin: 4px 0 0 16px; padding: 0; }
/* Aura-row description is collapsed by default to keep the picker
   compact — most players already know what their auras do and just
   want to see the toggle. The <summary> mimics a small chevron link;
   click stops at the summary so it doesn't also toggle the checkbox
   (handled in JS). */
.aura-details { margin-top: 4px; }
.aura-details-summary {
    cursor: pointer;
    font-size: 11.5px;
    color: var(--text-mute);
    list-style: none;
    user-select: none;
    display: inline-flex;
    align-items: center;
    gap: 4px;
}
.aura-details-summary::-webkit-details-marker { display: none; }
.aura-details-summary::before {
    content: "▸";
    font-size: 10px;
    transition: transform 0.12s ease;
}
.aura-details[open] > .aura-details-summary::before { transform: rotate(90deg); }
.aura-details[open] > .aura-details-summary { color: var(--text-dim); }
.aura-tag {
    display: inline-block;
    margin-left: 6px;
    padding: 1px 7px;
    border-radius: 999px;
    font-size: 10px;
    font-weight: 800;
    letter-spacing: 0.04em;
    vertical-align: middle;
    text-transform: uppercase;
}
.aura-tag-known {
    background: rgba(76, 184, 122, 0.18);
    color: var(--green);
    border: 1px solid rgba(76, 184, 122, 0.40);
    text-transform: none;
    letter-spacing: 0;
}
.aura-tag-reminder {
    background: var(--bg-elev-2);
    color: var(--text-mute);
    border: 1px solid var(--line);
}

/* Faction PNG logo — mirrors the size variants of the font-icon
   system (.wh-icon-sm / -md / -lg / -xl / -xxl) so the renderer can
   swap an <img> in for an <i> without any callsite changes. The
   PNGs are white-on-transparent and render natively against the
   dark UI. object-fit: contain handles non-square sources. */
.faction-logo-img {
    width: 32px;
    height: 32px;
    object-fit: contain;
    flex-shrink: 0;
    vertical-align: -0.18em;
}
.faction-logo-img.wh-icon-sm  { width: 16px; height: 16px; }
.faction-logo-img.wh-icon-md  { width: 22px; height: 22px; }
.faction-logo-img.wh-icon-lg  { width: 32px; height: 32px; }
.faction-logo-img.wh-icon-xl  { width: 64px; height: 64px; }
.faction-logo-img.wh-icon-xxl { width: 96px; height: 96px; }
/* Red faction-tint filter chain — calculated to map pure white →
   --accent-soft (#e63b54), matching the colour the font-icons get
   from `color: var(--accent-soft)`. Applied wherever the PNG logo
   should sit alongside (or replace) the existing red font-icon:
     - factions list rows (.faction-row)
     - saved-armies list rows (.faction-row)
     - datasheet faction-name line (.ds-faction-line)
     - in-match army-list panel meta (.army-panel-meta) */
.faction-row .faction-logo-img,
.ds-faction-line .faction-logo-img,
.army-panel-meta .faction-logo-img {
    filter: brightness(0) saturate(100%) invert(35%) sepia(64%) saturate(2670%) hue-rotate(326deg) brightness(98%) contrast(91%);
}

/* Waaagh! call modal — green-tinted to match the ork "go" colour */
.waaagh-modal { padding: 16px; max-width: 460px; }
.waaagh-modal .inbox-icon.waaagh-icon {
    background: #3aaa4d;
    color: #06140a;
    font-weight: 900;
    font-size: 22px;
    /* The icon slot is a circle — when we render the Ork PNG inside
       it instead of the bare "!", contain the image and dim the bg
       so the white logo reads against a softer green halo. */
    overflow: hidden;
    padding: 4px;
}
.waaagh-modal .inbox-icon.waaagh-icon img {
    width: 100%;
    height: 100%;
    object-fit: contain;
    display: block;
}

/* Black Templars modal icon — bone-white circle holding the BT logo
   (Templar Vows + Zealous Litanies pickers). The img must be display:
   block to size cleanly inside the 36px flex slot — without it the
   inline default + flex centering shrinks the logo to nothing. */
.inbox-icon.inbox-icon-bt {
    background: #d8d8d8;
    color: #1a1a1a;
    overflow: hidden;
    padding: 4px;
}
.inbox-icon.inbox-icon-bt img {
    width: 100%;
    height: 100%;
    object-fit: contain;
    display: block;
}
.waaagh-effects {
    margin: 0 0 8px 0;
    padding: 0;
    list-style: none;
}
.waaagh-effects li {
    padding: 6px 10px;
    margin-bottom: 4px;
    background: rgba(58, 170, 77, 0.08);
    border: 1px solid rgba(58, 170, 77, 0.35);
    border-radius: var(--radius-sm);
    font-size: 13.5px;
}
.waaagh-effects li:last-child { margin-bottom: 0; }

/* Oath declaration modal */
.oath-modal {
    padding: 16px;
    max-width: 460px;
}
.oath-target-list {
    display: flex;
    flex-direction: column;
    gap: 6px;
    max-height: 60vh;
    overflow-y: auto;
}
.oath-target-row {
    text-align: left;
    padding: 10px 12px;
    background: var(--bg-elev-2);
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    color: var(--text);
    cursor: pointer;
}
.oath-target-row:hover {
    border-color: var(--gold);
    background: rgba(224, 177, 58, 0.08);
}
.oath-target-name { font-weight: 700; font-size: 14px; }

/* Phase card */
.card.phase-card {
    padding: 16px;
    /* Pin the phase card below the topbar so the active phase + advance
       button stay reachable while the player scrolls down through the
       activation list, action log, etc. The 4px lift keeps a small gap
       between the topbar's bottom border and the card. */
    position: sticky;
    top: calc(var(--topbar-h) + var(--safe-top) + 4px);
    z-index: 30;
    /* Solid background so content scrolling underneath doesn't bleed
       through (the .card surface is already var(--bg-elev), but a sticky
       element with the same surface doesn't auto-occlude — be explicit). */
    background: var(--bg-elev);
    box-shadow: var(--shadow-2);
}
/* Collapsible behavior — the phase card now uses <details>/<summary>.
   Same chevron-rotates-on-open treatment as .tabletop-card so the
   affordance reads instantly. Tapping the header collapses the body
   to free up vertical space for the VTT canvas / activation list. */
.phase-card > summary {
    cursor: pointer;
    list-style: none;
    user-select: none;
    display: flex;
    align-items: center;
    gap: 6px;
    margin: 0;
}
.phase-card > summary::-webkit-details-marker { display: none; }
.phase-card > summary::after {
    content: "▾";
    margin-left: auto;
    color: var(--text-mute);
    font-size: 13px;
    transition: transform 0.18s ease;
}
.phase-card[open] > summary::after { transform: rotate(180deg); }
/* When collapsed, the summary's bottom margin from .card-h would
   leave a phantom gap below the chevron. Reset it. */
.phase-card:not([open]) > summary { margin-bottom: 0; }
.phase-desc {
    color: var(--text-dim);
    font-size: 13.5px;
    margin: 4px 0 0;
    line-height: 1.5;
}
.phase-spectating {
    font-size: 11px;
    font-weight: 700;
    color: var(--blue);
    margin-left: 6px;
    text-transform: uppercase;
    letter-spacing: 0.06em;
}

/* Action log card uses native <details> for collapse */
.action-log-card {
    padding: 0;
    overflow: hidden;
}
.action-log-card > summary {
    cursor: pointer;
    list-style: none;
    padding: 14px 16px;
    font-size: 11px;
    font-weight: 800;
    text-transform: uppercase;
    letter-spacing: 0.1em;
    color: var(--text-mute);
    display: flex;
    align-items: center;
    gap: 6px;
    user-select: none;
}
.action-log-card > summary::-webkit-details-marker { display: none; }
.action-log-card > summary::after {
    content: "▾";
    margin-left: auto;
    color: var(--text-mute);
    transition: transform 0.18s ease;
}
.action-log-card[open] > summary::after { transform: rotate(180deg); }
.action-log-body {
    padding: 0 16px 14px;
    max-height: 360px;
    overflow-y: auto;
}
.action-log-list { display: flex; flex-direction: column; gap: 6px; }

/* Tabletop card — collapsible bird's-eye view. Sticks below the topbar
   like the Phase card (z-index 29 keeps phase-card z=30 on top if the
   two ever overlap). */
.tabletop-card {
    padding: 0;
    overflow: hidden;
    position: sticky;
    top: calc(var(--topbar-h) + var(--safe-top) + 4px);
    z-index: 29;
    background: var(--bg-elev);
    box-shadow: var(--shadow-2);
    border-radius: var(--radius-lg);
}
.tabletop-card > summary {
    cursor: pointer;
    list-style: none;
    padding: 14px 16px;
    font-size: 11px;
    font-weight: 800;
    text-transform: uppercase;
    letter-spacing: 0.1em;
    color: var(--text-mute);
    display: flex;
    align-items: center;
    gap: 6px;
    user-select: none;
}
.tabletop-card > summary::-webkit-details-marker { display: none; }
.tabletop-card > summary::after {
    content: "▾";
    margin-left: auto;
    color: var(--text-mute);
    transition: transform 0.18s ease;
}
.tabletop-card[open] > summary::after { transform: rotate(180deg); }
.tabletop-body {
    padding: 0 16px 14px;
    max-height: 60vh;
    overflow-y: auto;
}
/* The LanGameLogo icon in the Tabletop summary uses the same height
   as the SVG icons elsewhere. PNG ships as white-on-transparent;
   apply the standard white → --accent-soft filter chain so it matches
   the colour of the SVG card-h-icons (other red icons elsewhere). */
.tabletop-summary-icon {
    height: 18px;
    width: auto;
    object-fit: contain;
    flex: 0 0 auto;
    /* white → --accent-soft (#e63b54) — same chain used elsewhere */
    filter: brightness(0) saturate(100%) invert(35%) sepia(64%) saturate(2670%) hue-rotate(326deg) brightness(98%) contrast(91%);
}
.action-entry {
    display: flex;
    gap: 10px;
    padding: 8px 10px;
    border-radius: var(--radius-sm);
    background: var(--bg-elev-2);
    border: 1px solid var(--line);
}
.action-entry-num {
    flex-shrink: 0;
    font-size: 11px;
    font-weight: 700;
    color: var(--text-mute);
    min-width: 36px;
    line-height: 1.5;
    font-family: ui-monospace, "SF Mono", Menlo, monospace;
}
.action-entry-body { flex: 1; min-width: 0; }
.action-entry-text {
    font-size: 13.5px;
    color: var(--text);
    line-height: 1.4;
    word-break: break-word;
}
.action-entry-meta {
    font-size: 11px;
    color: var(--text-mute);
    margin-top: 2px;
    font-family: ui-monospace, "SF Mono", Menlo, monospace;
}

/* Modal — used by dice + note prompts */
.modal-overlay {
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, 0.55);
    z-index: 280;
    display: flex;
    /* Centered on every viewport. Used to be bottom-anchored on
       narrow widths, which let body-mounted toasts (reactive-window
       gate banner, "waiting for opponent" pings) overlap critical
       modal content like Fire Overwatch / Counter-Offensive prompts. */
    align-items: center;
    justify-content: center;
    padding: 16px;
    padding-bottom: max(16px, var(--safe-bot));
    animation: modal-fade 0.18s ease;
}
.modal-card {
    width: 100%;
    max-width: 460px;
    background: var(--bg-elev);
    border: 1px solid var(--line);
    border-radius: var(--radius-lg);
    padding: 18px;
    box-shadow: var(--shadow-2);
    animation: modal-rise 0.22s cubic-bezier(0.2, 0.8, 0.2, 1);
}
.modal-h {
    font-size: 17px;
    font-weight: 800;
    margin: 0 0 8px;
}
.modal-card-tall {
    display: flex;
    flex-direction: column;
    max-height: calc(100vh - 80px);
    max-height: calc(100dvh - 80px);
}
.modal-card-tall .strat-list {
    flex: 1;
    min-height: 0;
    overflow-y: auto;
    margin: 0 -4px;
    padding: 0 4px;
}

/* Mid-match reconnect modal. Sits ABOVE the wizard layer (290) and
   the strat modal (280) so it can suspend any open modal/wizard
   underneath when the connection drops. Backdrop is darker than
   the default modal to clearly signal "match paused". */
.modal-overlay.reconnect-overlay {
    z-index: 1000;
    background: rgba(0, 0, 0, 0.72);
}
.reconnect-card {
    max-width: 380px;
    text-align: center;
}
.reconnect-spinner {
    width: 44px;
    height: 44px;
    margin: 0 auto 14px;
    border-radius: 50%;
    border: 4px solid var(--line);
    border-top-color: var(--accent, #c8102e);
    animation: reconnect-spin 0.9s linear infinite;
}
@keyframes reconnect-spin {
    to { transform: rotate(360deg); }
}

/* Setup wizard — 3-step onboarding (Sign-in → Drive → Discovery).
   Reuses .modal-overlay + .modal-card; these rules only style the
   wizard-specific bits (dots, step header, status pills, toggles). */
.setup-wizard-card {
    max-width: 440px;
    padding: 20px;
}
.setup-wiz-progress {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    margin-bottom: 14px;
}
.setup-wiz-dots { display: flex; gap: 8px; align-items: center; }
.setup-wiz-dots .dot {
    width: 10px;
    height: 10px;
    border-radius: 50%;
    background: var(--line);
    transition: background 0.15s ease;
}
.setup-wiz-dots .dot.done   { background: var(--accent); opacity: 0.55; }
.setup-wiz-dots .dot.active { background: var(--accent); transform: scale(1.15); }
.setup-wiz-step-label {
    font-size: 11px;
    font-weight: 700;
    color: var(--text-dim);
    text-transform: uppercase;
    letter-spacing: 0.06em;
}
.setup-wiz-h {
    font-size: 18px;
    font-weight: 800;
    margin: 0 0 6px;
}
.setup-wiz-sub {
    font-size: 13px;
    color: var(--text-dim);
    margin: 0 0 14px;
    line-height: 1.45;
}
.setup-wiz-google-host {
    display: flex;
    justify-content: center;
    min-height: 44px;
    margin-bottom: 6px;
}
.setup-wiz-or {
    display: flex;
    align-items: center;
    gap: 10px;
    margin: 12px 0 14px;
    color: var(--text-dim);
    font-size: 12px;
}
.setup-wiz-or::before,
.setup-wiz-or::after {
    content: "";
    flex: 1;
    height: 1px;
    background: var(--line);
}
.setup-wiz-label {
    display: block;
    font-size: 13px;
    font-weight: 600;
    margin: 0 0 6px;
}
.setup-wiz-input {
    width: 100%;
    padding: 10px 12px;
    background: var(--bg);
    border: 1px solid var(--line);
    border-radius: 6px;
    color: var(--text);
    font-size: 14px;
    box-sizing: border-box;
}
.setup-wiz-input:focus { outline: 2px solid var(--accent); outline-offset: -1px; }
.setup-wiz-status {
    display: flex;
    align-items: center;
    gap: 10px;
    font-size: 13px;
    padding: 10px 12px;
    border-radius: 6px;
    border: 1px solid var(--line);
    background: var(--bg);
}
.setup-wiz-status .badge {
    width: 22px;
    height: 22px;
    flex-shrink: 0;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    font-weight: 800;
    background: var(--bg-elev-2);
    color: var(--text-dim);
}
.setup-wiz-status.ok          { border-color: rgba(34, 197, 94, 0.35); background: rgba(34, 197, 94, 0.07); }
.setup-wiz-status.ok .badge   { background: rgba(34, 197, 94, 0.85); color: #0b1410; }
.setup-wiz-status + .setup-wiz-status { margin-top: 6px; }
.setup-wiz-hint {
    font-size: 12px;
    color: var(--text-dim);
    margin: 10px 0 0;
    line-height: 1.45;
}
.setup-wiz-toggle {
    display: flex;
    gap: 10px;
    align-items: flex-start;
    padding: 10px 0;
    cursor: pointer;
}
.setup-wiz-toggle + .setup-wiz-toggle { border-top: 1px solid var(--line); }
.setup-wiz-toggle input[type="checkbox"] {
    margin-top: 3px;
    flex-shrink: 0;
    width: 18px;
    height: 18px;
}
.setup-wiz-toggle b { font-size: 13px; }
.setup-wiz-toggle .text-mute { line-height: 1.4; margin-top: 2px; }
.setup-wiz-sub-toggles {
    margin-top: 10px;
    padding: 4px 12px 8px;
    border: 1px solid var(--line);
    border-radius: 6px;
    transition: opacity 0.18s ease;
}
.setup-wiz-sub-toggles.dim { opacity: 0.45; pointer-events: none; }
.setup-wiz-actions {
    display: flex;
    gap: 8px;
    margin-top: 18px;
}
.setup-wiz-actions .btn { flex: 1; }
.setup-wiz-actions .btn-ghost {
    background: transparent;
    border-color: transparent;
    color: var(--text-dim);
    flex: 0 1 auto;
    padding: 13px 12px;
}

/* Stratagem rows in the picker modal */
.strat-section-h {
    font-size: 11px;
    font-weight: 800;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    color: var(--text-mute);
    margin: 10px 4px 6px;
    list-style: none;
    cursor: default;
}
.strat-other > .strat-section-h { cursor: pointer; }
.strat-other > .strat-section-h::-webkit-details-marker { display: none; }
.strat-other > .strat-section-h::after {
    content: " ▾";
    color: var(--text-mute);
    transition: transform 0.18s ease;
    display: inline-block;
}
.strat-other[open] > .strat-section-h::after { transform: rotate(180deg); }

.strat-row {
    background: var(--bg-elev-2);
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    margin-bottom: 6px;
    transition: border-color 0.15s ease;
}
.strat-row[open] { border-color: var(--accent); }
.strat-row.unaffordable { opacity: 0.55; }

/* v0.10.319 — Gameplan-prioritized strat rows. Pinned to the top of
 * each section (sort lives in openStratagemModal) with a gold left-
 * edge accent + an inline "Gameplan #N" pill on the summary so the
 * player can see at-a-glance which stratagems their list was built
 * around at the moment of CP-spend. Sits orthogonally to the
 * .unaffordable / .is-trigger-glow classes — a prioritized strat that
 * can't be afforded still pins to the top but stays dimmed. */
.strat-row.is-gameplan-priority {
    box-shadow: inset 3px 0 0 0 var(--gold, #e0c060);
}
.gameplan-priority-pill {
    display: inline-block;
    margin-left: 6px;
    padding: 1px 7px;
    border-radius: 999px;
    font-size: 9.5px;
    font-weight: 900;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    background: rgba(220, 180, 70, 0.18);
    color: var(--gold, #e0c060);
    border: 1px solid var(--gold, #e0c060);
    vertical-align: middle;
    white-space: nowrap;
}
.strat-row > summary {
    list-style: none;
    cursor: pointer;
    padding: 10px 12px;
    display: flex;
    align-items: center;
    gap: 8px;
    -webkit-tap-highlight-color: transparent;
}
.strat-row > summary::-webkit-details-marker { display: none; }
.strat-row > summary::after {
    content: "▸";
    color: var(--text-mute);
    flex-shrink: 0;
    transition: transform 0.18s ease, color 0.12s ease;
}
.strat-row[open] > summary::after { transform: rotate(90deg); color: var(--accent-soft); }
.strat-row > summary:hover { background: rgba(255, 255, 255, 0.02); }

.strat-row .strat-cp-badge {
    background: var(--accent);
    color: #fff;
    font-weight: 800;
    padding: 2px 9px;
    border-radius: 999px;
    font-size: 12px;
    flex-shrink: 0;
}

/* Faction logo on the strat row — the PNGs are already white-on-
   transparent, so the <img> renders cleanly against the dark UI. We
   use CSS `filter` chains to tint each faction's logo to its colour
   (the chain values were calculated to map pure white → the target
   hex). The base rule keeps a soft drop shadow + opacity so the icon
   reads as decoration, not a photo. */
.strat-faction-logo {
    width: 22px;
    height: 22px;
    flex-shrink: 0;
    object-fit: contain;
    opacity: 0.95;
    /* light shadow so a coloured logo on dark bg keeps definition */
    filter: drop-shadow(0 1px 1px rgba(0, 0, 0, 0.5));
}
/* Per-faction tints — calculated to map white → faction colour. */
.strat-faction-logo-ork {  /* #5dba34 */
    filter: brightness(0) saturate(100%) invert(64%) sepia(66%) saturate(519%) hue-rotate(63deg) brightness(95%) contrast(82%)
            drop-shadow(0 1px 1px rgba(0, 0, 0, 0.5));
}
.strat-faction-logo-tyr {  /* #b850b6 */
    filter: brightness(0) saturate(100%) invert(43%) sepia(94%) saturate(484%) hue-rotate(266deg) brightness(85%) contrast(82%)
            drop-shadow(0 1px 1px rgba(0, 0, 0, 0.5));
}
.strat-faction-logo-bt  {  /* #d8d8d8 */
    filter: brightness(0) saturate(100%) invert(99%) sepia(0%) saturate(0%) hue-rotate(252deg) brightness(95%) contrast(80%)
            drop-shadow(0 1px 1px rgba(0, 0, 0, 0.5));
}
.strat-faction-logo-da  {  /* #1f6e3a */
    filter: brightness(0) saturate(100%) invert(31%) sepia(91%) saturate(381%) hue-rotate(101deg) brightness(89%) contrast(86%)
            drop-shadow(0 1px 1px rgba(0, 0, 0, 0.5));
}
.strat-faction-logo-ih  {  /* #98a0a4 */
    filter: brightness(0) saturate(100%) invert(72%) sepia(7%) saturate(217%) hue-rotate(168deg) brightness(85%) contrast(82%)
            drop-shadow(0 1px 1px rgba(0, 0, 0, 0.5));
}
/* Larger variant used in modal headers */
.strat-faction-logo-lg {
    width: 26px;
    height: 26px;
    margin-right: 4px;
    vertical-align: -6px;
}
.strat-row.unaffordable .strat-cp-badge { background: var(--text-mute); }
.strat-row .strat-name {
    font-weight: 700;
    font-size: 14px;
    flex: 1;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
}
.strat-row .strat-type {
    font-size: 11px;
    color: var(--text-mute);
    white-space: nowrap;
    text-transform: uppercase;
    letter-spacing: 0.04em;
}

/* Red-X indicator for stratagems whose rules-engine effect isn't
   implemented yet. Sits at the right of the row summary, just before
   the disclosure ▸ marker. The tooltip explains it on hover / long-press. */
.strat-row .strat-unimplemented {
    flex-shrink: 0;
    width: 18px;
    height: 18px;
    border-radius: 50%;
    background: rgba(200, 16, 46, 0.18);
    border: 1px solid var(--accent-soft);
    color: var(--accent-soft);
    font-size: 11px;
    font-weight: 900;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    line-height: 1;
}

/* Yellow warning banner shown inside the expanded strat body for
   stratagems that don't have rules-engine support yet. */
.strat-unimplemented-note {
    margin: 0 0 8px;
    padding: 8px 10px;
    background: rgba(200, 160, 80, 0.10);
    border: 1px solid var(--gold);
    border-radius: var(--radius-sm);
    color: var(--text);
    font-size: 12.5px;
    line-height: 1.45;
}

.strat-row .strat-body {
    padding: 12px 14px 14px;
    border-top: 1px solid var(--line);
    font-size: 13.5px;
    color: var(--text);
    line-height: 1.5;
}
.strat-row .strat-body p { margin: 6px 0; }
.strat-row .strat-body b { color: var(--accent-soft); font-weight: 700; }
.strat-row .strat-meta-mini {
    font-size: 11px;
    color: var(--text-mute);
    text-transform: uppercase;
    letter-spacing: 0.05em;
    margin-bottom: 6px;
}
@keyframes modal-fade { from { opacity: 0; } to { opacity: 1; } }
@keyframes modal-rise { from { transform: translateY(20px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }

/* ============ Replay viewer ============ */

.replay-controls {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    flex-wrap: wrap;
    margin-bottom: 8px;
}
.replay-buttons {
    display: flex;
    gap: 4px;
    flex-wrap: wrap;
}
.replay-buttons .btn { font-size: 16px; padding: 8px 12px; min-width: 44px; }
.replay-buttons .btn:disabled { opacity: 0.4; }
.replay-position {
    font-size: 12px;
    color: var(--text-mute);
    text-transform: uppercase;
    letter-spacing: 0.06em;
    font-weight: 700;
}
.replay-position b {
    color: var(--accent-soft);
    font-size: 14px;
}
.replay-scrub {
    width: 100%;
    -webkit-appearance: none;
    appearance: none;
    background: transparent;
    margin: 4px 0 0;
    padding: 6px 0;
    cursor: pointer;
}
.replay-scrub::-webkit-slider-runnable-track {
    height: 6px;
    background: var(--bg-elev-2);
    border: 1px solid var(--line);
    border-radius: 3px;
}
.replay-scrub::-moz-range-track {
    height: 6px;
    background: var(--bg-elev-2);
    border: 1px solid var(--line);
    border-radius: 3px;
}
.replay-scrub::-webkit-slider-thumb {
    -webkit-appearance: none;
    width: 18px;
    height: 18px;
    border-radius: 50%;
    background: var(--accent);
    border: 2px solid #fff;
    margin-top: -7px;
    box-shadow: 0 2px 6px rgba(0, 0, 0, 0.35);
    cursor: grab;
}
.replay-scrub::-moz-range-thumb {
    width: 18px;
    height: 18px;
    border-radius: 50%;
    background: var(--accent);
    border: 2px solid #fff;
    box-shadow: 0 2px 6px rgba(0, 0, 0, 0.35);
    cursor: grab;
}

.replay-action-card { padding-bottom: 16px; }
.replay-action-meta {
    display: flex;
    gap: 10px;
    flex-wrap: wrap;
    align-items: center;
    margin-bottom: 8px;
}
.replay-action-type {
    font-family: ui-monospace, monospace;
    font-size: 10.5px;
    font-weight: 800;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    background: var(--bg-elev-2);
    border: 1px solid var(--line);
    padding: 2px 8px;
    border-radius: 999px;
    color: var(--accent-soft);
}
.replay-action-actor {
    font-size: 12px;
    color: var(--text-dim);
}
.replay-action-ts {
    font-family: ui-monospace, monospace;
    font-size: 11px;
    color: var(--text-mute);
    margin-left: auto;
}
.replay-action-text {
    font-size: 14.5px;
    color: var(--text);
    line-height: 1.5;
}
.replay-action-extras { margin-top: 8px; }
.replay-dice {
    background: var(--bg-elev-2);
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    padding: 8px 12px;
    display: flex;
    align-items: baseline;
    gap: 12px;
}
.replay-dice b { color: var(--accent-soft); font-size: 16px; }

/* Currently-active entry in the action log list (within replay viewer) */
.action-entry.is-current {
    border-color: var(--accent);
    background: var(--accent-tint);
    box-shadow: 0 0 0 1px var(--accent);
}
.action-entry.is-current .action-entry-num { color: var(--accent-soft); }

.replay-log-entry {
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
    transition: border-color 0.12s ease;
}
.replay-log-entry:hover { border-color: var(--accent-soft); }

/* ============ Defender inbox modal ============ */

.inbox-overlay { z-index: 295; }   /* above wizard modals (290) */

.inbox-modal {
    border: 2px solid var(--accent);
    box-shadow: 0 0 0 1px rgba(200, 16, 46, 0.35), 0 8px 32px rgba(200, 16, 46, 0.25);
    animation: inbox-pulse 0.5s ease-out 1;
    position: relative;
}
@keyframes inbox-pulse {
    0%   { transform: scale(0.96); }
    60%  { transform: scale(1.02); }
    100% { transform: scale(1); }
}
@media (prefers-reduced-motion: reduce) {
    .inbox-modal { animation: none; }
}

.modal-timer-countdown {
    display: block;
    text-align: center;
    font-size: 2.4rem;
    font-weight: 900;
    letter-spacing: -0.02em;
    line-height: 1;
    margin-top: 14px;
    color: #22c55e;
    transition: color 0.4s;
}
.modal-timer-countdown.urgent {
    color: #dc2626;
}

.inbox-h {
    display: flex;
    align-items: center;
    gap: 12px;
    margin-bottom: 10px;
}
.inbox-icon {
    width: 36px;
    height: 36px;
    border-radius: 50%;
    background: var(--accent);
    color: #fff;
    display: flex;
    align-items: center;
    justify-content: center;
    font-weight: 900;
    font-size: 18px;
    flex-shrink: 0;
}
.inbox-h-title {
    font-size: 15px;
    font-weight: 800;
    color: var(--accent-soft);
    text-transform: uppercase;
    letter-spacing: 0.06em;
}
.inbox-h-sub {
    font-size: 11px;
    color: var(--text-mute);
    text-transform: uppercase;
    letter-spacing: 0.05em;
    font-weight: 700;
    margin-top: 2px;
}

.inbox-summary {
    font-size: 14.5px;
    line-height: 1.5;
    margin: 0 0 12px;
    color: var(--text);
}

.inbox-result-grid {
    display: grid;
    grid-template-columns: repeat(5, 1fr);
    gap: 6px;
}
.inbox-result-cell {
    text-align: center;
    background: var(--bg);
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    padding: 8px 4px;
}
.inbox-result-num {
    display: block;
    font-size: 22px;
    font-weight: 900;
    color: var(--text);
    line-height: 1;
}
.inbox-result-cell.inbox-result-damage .inbox-result-num { color: var(--accent-soft); }
.inbox-result-lbl {
    display: block;
    font-size: 9.5px;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--text-mute);
    margin-top: 4px;
    font-weight: 700;
}

/* Wound-allocation wizard */
.wound-wizard-modal { padding: 16px; }
.wound-section-h {
    font-size: 11px;
    font-weight: 800;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    color: var(--text-mute);
    margin: 8px 0 6px;
}

/* Damage chips: one per incoming wound, prominent so the defender can see
   the damage profile at a glance before allocating. */
.wound-list {
    display: flex;
    flex-wrap: wrap;
    gap: 5px;
    margin-bottom: 6px;
}
.wound-chip {
    min-width: 32px;
    padding: 6px 10px;
    border-radius: 8px;
    font-size: 16px;
    font-weight: 800;
    background: var(--accent-tint);
    color: var(--accent-soft);
    border: 1px solid rgba(200, 16, 46, 0.4);
    text-align: center;
    font-family: ui-monospace, "SF Mono", Menlo, monospace;
}
.wound-chip.is-mortal {
    background: rgba(200, 16, 46, 0.22);
    border-color: var(--accent);
    color: #fff;
}

/* Model grid: each model in the unit shown as a tappable cell. */
.model-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(72px, 1fr));
    gap: 6px;
    margin: 4px 0;
}
.model-cell {
    background: var(--bg);
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    padding: 10px 6px;
    text-align: center;
    transition: border-color 0.12s ease, background 0.12s ease, transform 0.1s ease;
    user-select: none;
}
.model-cell.is-pickable { cursor: pointer; }
.model-cell.is-pickable:active { transform: scale(0.96); }
@media (hover: hover) {
    .model-cell.is-pickable:hover {
        border-color: var(--accent-soft);
        background: var(--bg-elev-2);
    }
}
.model-cell.is-wounded {
    border-color: var(--gold);
    background: rgba(224, 177, 58, 0.08);
}
.model-cell.is-dead {
    border-color: var(--line);
    opacity: 0.45;
    cursor: default;
}
.model-cell.is-forced {
    border-color: var(--accent);
    background: var(--accent-tint);
    box-shadow: 0 0 0 1px var(--accent);
}
.model-cell.is-picked {
    border-color: var(--accent);
    background: var(--accent-tint);
    box-shadow: 0 0 0 2px rgba(200, 16, 46, 0.35);
}
.model-num {
    font-size: 10px;
    font-weight: 700;
    color: var(--text-mute);
    text-transform: uppercase;
    letter-spacing: 0.06em;
}
.model-w {
    font-size: 14px;
    font-weight: 800;
    color: var(--text);
    margin-top: 4px;
    font-family: ui-monospace, "SF Mono", Menlo, monospace;
}
.model-cell.is-dead .model-w { color: var(--text-mute); font-size: 18px; }
.model-cell.is-wounded .model-w { color: var(--gold); }

/* ============ per-model loadout editor (army builder) ============
 *
 * Mirrors the wound-allocation grid: a row of model cells, click one to
 * select. The cell's body is a tiny summary so the user sees at a
 * glance what each model carries. The selected cell drives the weapon
 * checklist below.
 */
.loadout-grid {
    grid-template-columns: repeat(auto-fill, minmax(96px, 1fr));
    gap: 6px;
    margin: 6px 0 12px;
}
.loadout-cell {
    text-align: left;
    padding: 8px 8px 10px;
    cursor: pointer;
    color: var(--text);
    background: var(--bg);
}
.loadout-cell.is-picked {
    border-color: var(--accent);
    background: var(--accent-tint);
    box-shadow: 0 0 0 1px var(--accent);
}
.loadout-cell .model-num {
    text-align: left;
    font-size: 9.5px;
    margin-bottom: 2px;
}
.loadout-cell-arch {
    font-size: 12px;
    font-weight: 700;
    color: var(--text);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.loadout-cell-summary {
    margin-top: 4px;
    font-size: 11.5px;
    line-height: 1.3;
    color: var(--text-dim);
    overflow: hidden;
    text-overflow: ellipsis;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
}

.loadout-editor {
    margin-top: 4px;
    padding: 12px;
    background: var(--bg);
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
}
/* Small ⓘ button next to a weapon line that's part of a wargear-option
   swap group. Tapping opens the swap-group details modal. Sized to sit
   inline with the weapon name without bumping line height. Stops the
   click propagating to the parent <label> so the checkbox doesn't
   accidentally toggle. */
.loadout-swap-info {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 18px;
    height: 18px;
    margin-left: 6px;
    padding: 0;
    border: none;
    background: transparent;
    color: var(--blue, #6f9bd6);
    font-size: 14px;
    line-height: 1;
    cursor: pointer;
    border-radius: 50%;
    vertical-align: -2px;
}
.loadout-swap-info:hover {
    background: rgba(120, 145, 230, 0.18);
}
.loadout-swap-info:active {
    background: rgba(120, 145, 230, 0.32);
}
.loadout-editor-h {
    font-size: 13px;
    font-weight: 700;
    margin-bottom: 8px;
    color: var(--text);
}
.loadout-opt { align-items: flex-start; }

/* ============ read-only loadout panel (wound allocation) ============ */
.loadout-panel {
    margin-top: 10px;
    padding: 10px 12px;
    background: var(--bg);
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
}
.loadout-panel-h {
    font-size: 11px;
    font-weight: 800;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--accent-soft);
    margin-bottom: 6px;
}
.loadout-panel-line {
    padding: 6px 0;
    border-bottom: 1px solid rgba(42, 46, 58, 0.5);
}
.loadout-panel-line:last-child { border-bottom: none; }
.loadout-panel-name { font-size: 13px; font-weight: 700; color: var(--text); }
.loadout-panel-stats {
    margin-top: 2px;
    font-size: 11.5px;
    color: var(--text-dim);
    font-family: ui-monospace, "SF Mono", Menlo, monospace;
}

/* Static read-out for the auto-derived attacker count in the attack
   wizard's Situation panel (replaces the prior editable input). */
.attacker-count-display {
    width: 64px;
    height: 32px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    background: var(--bg);
    border: 1px solid var(--line);
    border-radius: 6px;
    color: var(--text);
    font-weight: 800;
    font-family: ui-monospace, "SF Mono", Menlo, monospace;
    font-size: 16px;
}

/* ============ Battle-shock test wizard ============ */
.battleshock-modal { padding: 16px; }
.bs-target-row {
    display: flex;
    align-items: center;
    gap: 8px;
    margin: 6px 0 4px;
}
.bs-target-cell {
    flex: 1;
    background: var(--bg);
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    padding: 10px 8px;
    text-align: center;
}
.bs-target-lbl {
    font-size: 10px;
    font-weight: 800;
    color: var(--text-mute);
    text-transform: uppercase;
    letter-spacing: 0.06em;
}
.bs-target-val {
    font-size: 22px;
    font-weight: 900;
    color: var(--text);
    margin-top: 4px;
    font-family: ui-monospace, "SF Mono", Menlo, monospace;
}
.bs-target-arrow { color: var(--text-mute); font-size: 18px; flex-shrink: 0; }
.bs-dice-row {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    margin: 12px 0 14px;
}
.bs-die {
    width: 56px;
    height: 56px;
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--bg-elev-2);
    border: 2px solid var(--accent);
    border-radius: 12px;
    font-size: 30px;
    font-weight: 900;
    color: var(--text);
    font-family: ui-monospace, "SF Mono", Menlo, monospace;
    box-shadow: 0 4px 12px rgba(200, 16, 46, 0.18);
}
.bs-plus, .bs-eq {
    color: var(--text-mute);
    font-size: 22px;
    font-weight: 700;
}
.bs-total {
    width: 70px;
    height: 56px;
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--accent);
    color: #fff;
    border-radius: 12px;
    font-size: 32px;
    font-weight: 900;
    font-family: ui-monospace, "SF Mono", Menlo, monospace;
}
.bs-result-banner {
    border-radius: var(--radius-sm);
    padding: 10px 12px;
    font-size: 14px;
    line-height: 1.4;
    border: 1px solid var(--line);
    background: var(--bg-elev-2);
}
.bs-result-banner.is-pass {
    border-color: var(--green);
    background: rgba(76, 184, 122, 0.10);
    color: var(--text);
}
.bs-result-banner.is-fail {
    border-color: var(--accent);
    background: var(--accent-tint);
    color: var(--text);
}

/* ============ Activation wizard (shoot / charge / fight) ============ */

.wizard-overlay { z-index: 290; }
/* Death-rattle wizard variant: must stack above the wound-allocation
 * modal (.inbox-overlay z=295) that's still mounted underneath while
 * the dying unit fires its final shot. !important defends against any
 * cascade surprise (`.wizard-overlay` at z=290 in the same selector
 * tree, or a base `.modal-overlay` rule bumping z-index late in file).
 * Sits well above the hazardous modal (z=305) — RAW-wise the dying
 * unit's attack still needs the hazardous step to run on top, so
 * hazardous gets its own bump below. */
.modal-overlay.wizard-overlay-deathrattle { z-index: 950 !important; }
.wiz-modal { padding: 16px; }
.wiz-h-row {
    display: flex;
    align-items: center;
    gap: 10px;
    margin-bottom: 8px;
}
.wiz-progress {
    display: flex;
    gap: 4px;
    margin-bottom: 12px;
}
.wiz-progress-step {
    flex: 1;
    text-align: center;
    padding: 6px 4px;
    border-radius: var(--radius-sm);
    background: var(--bg-elev-2);
    border: 1px solid var(--line);
    font-size: 11px;
    font-weight: 700;
    color: var(--text-mute);
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 2px;
}
.wiz-progress-step.is-active {
    background: var(--accent);
    border-color: var(--accent);
    color: #fff;
}
.wiz-progress-step.is-done {
    background: rgba(76, 184, 122, 0.10);
    border-color: var(--green);
    color: var(--green);
}
.wiz-progress-num { font-size: 13px; }
.wiz-progress-lbl { text-transform: uppercase; letter-spacing: 0.05em; font-size: 9.5px; }

/* v0.10.339 — Spectator activation mirror. A fixed, body-mounted HUD
 * card shown while the OPPONENT is mid-wizard, so the spectating player
 * can follow which unit is acting, against whom, and how far through
 * the wizard they are. Anchored above the bottom navbar (and clear of
 * the bg-mode collapsed pill) so it reads in both the full panel and
 * the collapsed board view. Purely informational — pointer-events:none
 * lets board / panel taps pass straight through. */
.spectator-wizard-mirror {
    position: fixed;
    left: 50%;
    transform: translateX(-50%);
    /* Anchored at the TOP, just under the topbar — the same slot the
     * reactive-gate-banner uses. The two never co-occur (the gate
     * banner is for MY waits as active player; the mirror is for the
     * OPPONENT's activations while I spectate), so the slot is free.
     * Top keeps it clear of the bottom-anchored reactive-prompt toasts
     * + collapsed pill, and visible in both the full panel and the
     * bg-mode collapsed board view. */
    top: calc(var(--topbar-h) + var(--safe-top) + 8px);
    z-index: 340;           /* under modals (360+); over board + panel */
    width: calc(100vw - 24px);
    max-width: 520px;
    pointer-events: none;   /* informational — taps pass through to board/panel */
    background: rgba(18, 20, 28, 0.96);
    border: 1px solid var(--line);
    border-radius: var(--radius-md, 12px);
    box-shadow: var(--shadow-2);
    backdrop-filter: blur(6px);
    -webkit-backdrop-filter: blur(6px);
    padding: 10px 14px 12px;
    animation: swm-fade-in 200ms ease-out both;
}
@keyframes swm-fade-in {
    from { opacity: 0; transform: translate(-50%, -8px); }
    to   { opacity: 1; transform: translate(-50%, 0); }
}
.swm-eyebrow {
    font-size: 10px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    color: var(--accent-soft, var(--text-mute));
    margin-bottom: 3px;
}
.swm-title {
    font-size: 14px;
    font-weight: 600;
    color: var(--text);
    line-height: 1.3;
    margin-bottom: 10px;
}
.swm-title .swm-vs {
    color: var(--text-mute);
    font-weight: 700;
    font-size: 12px;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    margin: 0 2px;
}
/* The step tracker reuses .wiz-progress; drop its bottom margin here
 * since the card supplies its own padding. */
.spectator-wizard-mirror .swm-progress { margin-bottom: 0; }

.wiz-body {
    flex: 1;
    overflow-y: auto;
    margin: 0 -4px;
    padding: 0 4px;
}

/* The final .wiz-actions row inside a scrolling wizard body sticks to the
   bottom of the viewport so primary actions (Next, Confirm, Roll!) stay
   tappable regardless of how long the list above is. The bg matches the
   modal so content scrolls cleanly behind it without bleed-through, and the
   negative side margins make the sticky bg extend to the wiz-body edges so
   no content peeks past the buttons left or right. */
.wiz-body > .wiz-actions:last-child {
    position: sticky;
    bottom: 0;
    margin-left: -4px;
    margin-right: -4px;
    margin-bottom: 0;
    padding: 12px 4px 4px;
    background: var(--bg-elev);
    border-top: 1px solid var(--line);
    z-index: 5;
}

.wiz-target-list, .wiz-weapon-list {
    display: flex;
    flex-direction: column;
    gap: 6px;
    margin: 6px 0 4px;
}
.wiz-target-row {
    display: block;
    width: 100%;
    text-align: left;
    padding: 10px 12px;
    background: var(--bg-elev-2);
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    color: var(--text);
    cursor: pointer;
    transition: border-color 0.12s ease;
}
.wiz-target-row.is-picked {
    border-color: var(--accent);
    background: var(--accent-tint);
}
.wiz-target-name { font-weight: 700; font-size: 15px; }
/* Stat boxes for the target picker: each characteristic in its own cell so
   numbers are big enough to read at a glance from across the table. */
.wiz-target-stats {
    display: flex;
    gap: 6px;
    margin-top: 8px;
    flex-wrap: wrap;
}
.wiz-target-stat {
    flex: 1 1 0;
    min-width: 56px;
    background: var(--bg);
    border: 1px solid var(--line);
    border-radius: 6px;
    padding: 6px 4px;
    text-align: center;
}
.wiz-target-stat-label {
    display: block;
    font-size: 9.5px;
    font-weight: 700;
    color: var(--text-mute);
    letter-spacing: 0.06em;
    text-transform: uppercase;
}
.wiz-target-stat-value {
    display: block;
    font-size: 18px;
    font-weight: 800;
    margin-top: 2px;
    color: var(--text);
    font-family: ui-monospace, "SF Mono", Menlo, monospace;
}
.wiz-target-row.is-picked .wiz-target-stat {
    background: var(--bg-elev-2);
    border-color: var(--accent);
}
.wiz-target-row.is-picked .wiz-target-stat-value { color: var(--accent-soft); }
.wiz-target-kw {
    display: flex;
    flex-wrap: wrap;
    gap: 3px;
    margin-top: 8px;
}

.wiz-weapon-row {
    display: flex;
    gap: 10px;
    align-items: flex-start;
    padding: 10px 12px;
    background: var(--bg-elev-2);
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    cursor: pointer;
}
.wiz-weapon-row.is-checked { border-color: var(--accent); background: var(--accent-tint); }
.wiz-weapon-row input[type="checkbox"] {
    width: 20px; height: 20px;
    accent-color: var(--accent);
    flex-shrink: 0;
    margin-top: 2px;
}
.wiz-weapon-info { flex: 1; min-width: 0; }
.wiz-weapon-name { font-weight: 700; font-size: 14px; }
.wiz-weapon-stats {
    display: flex;
    flex-wrap: wrap;
    gap: 4px;
    margin-top: 5px;
}
.wiz-weapon-stat {
    display: inline-flex;
    align-items: baseline;
    gap: 4px;
    padding: 2px 7px;
    background: var(--bg-elev-1, #1a1c22);
    border: 1px solid var(--line);
    border-radius: 999px;
    font-size: 11px;
    line-height: 1.4;
    font-family: ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;
    color: var(--text);
    white-space: nowrap;
}
.wiz-weapon-stat-k { color: var(--text-dim); font-weight: 600; }
.wiz-weapon-stat-v { color: var(--text); font-weight: 700; }
.wiz-weapon-stat-melee { color: var(--text-dim); font-weight: 600; }
.wiz-weapon-abil { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 5px; }

.wiz-attack-h {
    display: flex;
    align-items: baseline;
    gap: 8px;
    flex-wrap: wrap;
    margin-bottom: 4px;
}
.wiz-attack-i {
    background: var(--accent);
    color: #fff;
    padding: 2px 8px;
    border-radius: 999px;
    font-size: 11px;
    font-weight: 800;
}
.wiz-attack-name {
    font-weight: 800;
    font-size: 16px;
}
.wiz-attack-stats { font-size: 12px; font-family: ui-monospace, monospace; }

.wiz-section-h {
    font-size: 11px;
    font-weight: 800;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    color: var(--text-mute);
    margin: 12px 0 6px;
}
.wiz-auto-mods { background: rgba(76, 184, 122, 0.06); border: 1px solid rgba(76, 184, 122, 0.3); border-radius: var(--radius-sm); padding: 8px 12px; margin-top: 4px; }
.wiz-auto-mods .wiz-section-h { color: var(--green); margin-top: 0; }
.wiz-auto-line { font-size: 12.5px; color: var(--text); margin: 3px 0; }

.wiz-conditions, .wiz-manual-mods { margin-top: 4px; }

.wiz-mod-grid {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: 6px;
}
@media (min-width: 540px) {
    .wiz-mod-grid { grid-template-columns: repeat(3, 1fr); }
}
.wiz-mod-chip {
    padding: 8px 10px;
    border-radius: var(--radius-sm);
    background: var(--bg-elev-2);
    border: 1px solid var(--line);
    color: var(--text-dim);
    font-size: 12.5px;
    font-weight: 700;
    cursor: pointer;
    text-align: center;
    transition: border-color 0.1s, background 0.12s, color 0.12s;
}
.wiz-mod-chip.is-on {
    border-color: var(--accent);
    background: var(--accent-tint);
    color: var(--accent-soft);
}

.wiz-result {
    background: var(--bg-elev-2);
    border: 1px solid var(--accent);
    border-radius: var(--radius);
    padding: 14px;
    margin-top: 12px;
}
.wiz-result-grid {
    display: grid;
    grid-template-columns: repeat(5, 1fr);
    gap: 6px;
}
.wiz-result-cell {
    text-align: center;
    background: var(--bg);
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    padding: 8px 4px;
}
.wiz-result-num {
    display: block;
    font-size: 22px;
    font-weight: 900;
    color: var(--text);
    line-height: 1;
}
.wiz-result-cell.wiz-result-damage .wiz-result-num { color: var(--accent-soft); }
.wiz-result-lbl {
    display: block;
    font-size: 9.5px;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--text-mute);
    margin-top: 4px;
    font-weight: 700;
}

.wiz-trace {
    margin-top: 10px;
    background: var(--bg-elev-2);
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
}
.wiz-trace > summary {
    list-style: none;
    cursor: pointer;
    padding: 8px 12px;
    font-size: 11px;
    font-weight: 800;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    color: var(--text-mute);
}
.wiz-trace > summary::-webkit-details-marker { display: none; }
.wiz-trace > summary::after { content: " ▾"; }
.wiz-trace[open] > summary::after { content: " ▴"; }
.wiz-trace-body {
    padding: 0 12px 10px;
    font-size: 12px;
    font-family: ui-monospace, monospace;
    color: var(--text-dim);
    line-height: 1.55;
}
.wiz-trace-line { padding: 2px 0; }

.wiz-summary-list {
    display: flex;
    flex-direction: column;
    gap: 6px;
}
.wiz-summary-row {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 8px 12px;
    background: var(--bg-elev-2);
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    font-size: 13px;
}
.wiz-summary-name { font-weight: 700; }
.wiz-summary-stats { color: var(--text-dim); }
.wiz-summary-stats b { color: var(--accent-soft); }

/* Activation tracker (Shooting / Charge / Fight phases) */
.activation-progress {
    display: flex;
    align-items: center;
    gap: 12px;
    margin: 4px 0 12px;
}
.activation-progress-text {
    flex-shrink: 0;
    font-size: 13px;
    font-weight: 700;
    color: var(--text-dim);
    min-width: 110px;
}
.activation-progress-text.is-complete { color: var(--green); }
.activation-progress-bar {
    flex: 1;
    height: 6px;
    background: var(--bg-elev-2);
    border: 1px solid var(--line);
    border-radius: 3px;
    overflow: hidden;
}
.activation-progress-fill {
    height: 100%;
    background: linear-gradient(90deg, var(--accent-soft), var(--gold));
    transition: width 0.25s cubic-bezier(0.2, 0.8, 0.2, 1);
}
.activation-progress-fill.is-complete {
    background: linear-gradient(90deg, var(--green), var(--gold));
}

.activation-list {
    display: flex;
    flex-direction: column;
    gap: 6px;
}
/* Activation row is a <div role="button"> (not a real button — it must be
   able to host the inner cycle <button>, which HTML forbids inside another
   button). State is keyed off `aria-disabled` rather than the :disabled
   pseudo-class. */
.activation-row {
    position: relative;
    width: 100%;
    padding: 10px 12px;
    background: var(--bg-elev-2);
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    color: var(--text);
    text-align: left;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
    transition: border-color 0.12s ease, background 0.12s ease;
    user-select: none;
}
/* Inner flex container so the centered "Activating…" overlay can sit
   on top of a dimmed copy of the row's normal content. */
.activation-row-content {
    display: flex;
    align-items: center;
    gap: 10px;
    transition: opacity 0.15s ease;
}
.activation-row:not([aria-disabled="true"]):active { background: var(--bg-elev); }
.activation-row:not([aria-disabled="true"]):focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}
.activation-row[aria-disabled="true"] {
    cursor: default;
    opacity: 0.92;
}
.activation-row.is-done {
    background: rgba(76, 184, 122, 0.08);
    border-color: rgba(76, 184, 122, 0.4);
}
/* Mid-activation: the active player has opened a wizard for this unit
   but hasn't committed yet. Yellow background + dim the inline content
   to ~50% so the centered overlay reads as the dominant message. Both
   players see this state — it's the visual hook interrupt stratagems
   (Fire Overwatch, etc.) will key off in upcoming iterations. */
.activation-row.is-activating {
    background: rgba(224, 177, 58, 0.12);
    border-color: rgba(224, 177, 58, 0.55);
}
.activation-row.is-activating .activation-row-content {
    opacity: 0.5;
}
.activation-row-overlay {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    pointer-events: none;
    font-weight: 800;
    text-transform: uppercase;
    letter-spacing: 0.10em;
    font-size: 13px;
    color: var(--gold);
    text-shadow: 0 0 8px rgba(224, 177, 58, 0.45);
}
.activation-row .activation-status {
    flex-shrink: 0;
    width: 26px;
    height: 26px;
    border-radius: 50%;
    background: var(--bg);
    border: 1px solid var(--line);
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 14px;
    font-weight: 800;
    color: var(--text-mute);
    transition: background 0.15s, color 0.15s, border-color 0.15s;
}
.activation-row.is-done .activation-status {
    background: var(--green);
    color: #0a1f12;
    border-color: var(--green);
}
/* Unit-info is now a simple two-line text block (name + attached). The
   per-phase stats (M chip / cycle button / weapon block) live as direct
   siblings of unit-info inside .activation-row, so the row's flex axis
   places them inline rather than forcing them onto a wrapped second line. */
.activation-unit-info {
    flex: 1 1 auto;
    min-width: 0;
}
.activation-unit-name {
    font-weight: 700;
    font-size: 14px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.fights-first-tag {
    display: inline-block;
    margin-left: 6px;
    padding: 1px 6px;
    border-radius: 3px;
    font-size: 9.5px;
    font-weight: 800;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    background: rgba(200, 160, 80, 0.18);
    color: var(--gold);
    vertical-align: 0.05em;
}

/* v0.10.318 — Gameplan role on the activation row. When the player's
 * gameplan assigned this unit a role, the row picks up a left-edge
 * color accent (driven by the --gameplan-role-color CSS var stamped on
 * the row) and the unit name carries an .activation-role-tag chip with
 * the role label. Tag background + border come from inline style on
 * the chip (per-row colors aren't worth a full class-per-color setup);
 * we only declare layout + typography here.
 *
 * v0.10.349 — REVERTED the v0.10.348 full-row background tint. Nathan
 * reported the per-role background colors drowned out the activated/
 * not-activated state — every row was painted red/green/blue based on
 * its role, making it hard to scan which units had already fired. The
 * thin left-edge stripe + the inline-styled role chip on the unit
 * name carry enough role signal without competing with the state
 * colors (yellow activating, green done). */
.activation-row.has-gameplan-role {
    box-shadow: inset 3px 0 0 0 var(--gameplan-role-color, #c8102e);
}
.activation-role-tag {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    margin-left: 6px;
    padding: 1px 6px;
    border-radius: 3px;
    font-size: 9.5px;
    font-weight: 800;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    color: #fff;
    border: 1px solid currentColor;
    vertical-align: 0.05em;
}
.activation-role-tag-icon {
    font-size: 12px;
    line-height: 1;
}
.dd-warn-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    margin-left: 6px;
    width: 18px;
    height: 18px;
    padding: 0;
    border-radius: 50%;
    background: rgba(240, 180, 40, 0.95);
    color: #1a1208;
    font-size: 11px;
    font-weight: 800;
    border: none;
    cursor: pointer;
    line-height: 1;
    vertical-align: 0.05em;
    animation: dd-warn-pulse 1.1s ease-in-out infinite;
}
.dd-warn-btn:hover { filter: brightness(1.1); }
@keyframes dd-warn-pulse {
    0%, 100% {
        background: rgba(240, 180, 40, 0.95);
        box-shadow: 0 0 0 0 rgba(240, 180, 40, 0.65);
    }
    50% {
        background: rgba(255, 210, 70, 1);
        box-shadow: 0 0 0 4px rgba(240, 180, 40, 0);
    }
}
@media (prefers-reduced-motion: reduce) {
    .dd-warn-btn { animation: none; }
}
.activation-attached {
    font-size: 11.5px;
    color: var(--text-mute);
    margin-top: 1px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* "5/5 models remaining" line under the unit name. The bolded numbers
   subtly tint as the unit takes damage so a glance at the activation
   tracker tells you which units are wounded or in trouble. */
.activation-count {
    font-size: 11.5px;
    color: var(--text-mute);
    margin-top: 2px;
    font-weight: 500;
}
.activation-count b {
    color: var(--text);
    font-weight: 800;
    font-family: ui-monospace, "SF Mono", Menlo, monospace;
}
.activation-count.is-wounded   b { color: var(--gold); }
.activation-count.is-below-half b { color: var(--accent-soft); }
/* OC chip — small inline badge after the model count. Sits in the
   row's wound-meta line, not the weapon stat block, since OC is a
   command-phase consideration that's handy at all times. */
.activation-oc {
    display: inline-block;
    margin-left: 6px;
    padding: 1px 6px;
    border-radius: 4px;
    background: rgba(80, 132, 200, 0.16);
    border: 1px solid rgba(80, 132, 200, 0.40);
    color: var(--blue, #6f9bd6);
    font-size: 10.5px;
    font-weight: 800;
    letter-spacing: 0.04em;
    vertical-align: 1px;
}

/* Movement-phase: small "M  6"" chip pinned next to the unit name. */
.activation-mstat {
    display: inline-flex;
    align-items: baseline;
    gap: 4px;
    background: var(--bg);
    border: 1px solid var(--line);
    border-radius: 6px;
    padding: 2px 8px;
    flex-shrink: 0;
}
.activation-mstat-lbl {
    font-size: 9.5px;
    font-weight: 700;
    color: var(--text-mute);
    text-transform: uppercase;
    letter-spacing: 0.06em;
}
.activation-mstat-val {
    font-size: 14px;
    font-weight: 800;
    color: var(--text);
    font-family: ui-monospace, "SF Mono", Menlo, monospace;
}

/* Shoot/Fight: cycle button between unit name and weapon block. Refresh-style
   icon, small but tappable. */
.activation-cycle {
    flex-shrink: 0;
    width: 28px;
    height: 28px;
    border-radius: 50%;
    background: var(--bg);
    border: 1px solid var(--line);
    color: var(--text-dim);
    font-size: 16px;
    line-height: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
    transition: transform 0.18s ease, color 0.12s ease, border-color 0.12s ease;
}
.activation-cycle:active {
    transform: rotate(180deg);
    color: var(--accent-soft);
    border-color: var(--accent);
}
@media (hover: hover) {
    .activation-cycle:hover {
        color: var(--accent-soft);
        border-color: var(--accent);
    }
}

/* The weapon block sits to the right of the cycle button as a direct
   row-level sibling — fixed-content size, vertical stack of name (small)
   over stats (monospace). flex: 0 0 auto keeps it from stretching; max-width
   + overflow ellipsis prevents very long stat lines from pushing the action
   label off the row on narrow screens. */
.activation-weapon {
    flex: 0 0 auto;
    min-width: 0;
    max-width: 240px;
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: 1px;
    text-align: right;
    overflow: hidden;
}
.activation-weapon-name {
    font-size: 10.5px;
    font-weight: 700;
    color: var(--text-mute);
    text-transform: uppercase;
    letter-spacing: 0.05em;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.activation-weapon-i {
    background: var(--bg-elev-2);
    border: 1px solid var(--line);
    border-radius: 999px;
    padding: 0 6px;
    font-size: 9.5px;
    color: var(--text-dim);
    font-weight: 700;
    margin-left: 4px;
}
.activation-weapon-stats {
    display: inline-block;
    align-self: flex-end;
    font-size: 12.5px;
    font-weight: 700;
    color: var(--text);
    background: var(--bg);
    border: 1px solid var(--line);
    border-radius: 6px;
    padding: 2px 8px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    max-width: 100%;
}
.activation-noweapons {
    font-size: 11.5px;
    color: var(--text-mute);
    font-style: italic;
}

/* On narrow screens the full weapon profile (A / BS / S / AP / D) +
   the weapon name compete with the unit name + count for space, so
   the activation row used to hide the preview block entirely below
   520px. v0.10.22: keep the RANGE chip and the cycle button visible
   even on narrow screens — players need to know the weapon's reach at
   a glance, and the cycle button is how they swap between profiles
   when a unit carries more than one. The full stats + weapon name
   still stay hidden below 520px so the row layout doesn't break. */
@media (max-width: 520px) {
    .activation-row .activation-noweapons {
        display: none;
    }
    .activation-row .activation-weapon-name {
        display: none;
    }
    .activation-row .activation-weapon-other {
        display: none;
    }
    /* The .activation-weapon block + .activation-weapon-range chip +
       cycle button stay visible; only the inner full-stats and
       weapon-name pieces hide. */
    .activation-row .activation-weapon {
        max-width: 96px;
    }
    .activation-row .activation-weapon-stats {
        font-size: 11.5px;
        padding: 2px 6px;
    }
}
.activation-action {
    flex-shrink: 0;
    font-size: 11px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--text-mute);
}
.activation-row.is-done .activation-action { color: var(--green); }

/* In-game army summary rows */
.match-armies { display: flex; flex-direction: column; gap: 10px; }
.match-army-row {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 10px 12px;
    background: var(--bg-elev-2);
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
}
.match-army-text { flex: 1; min-width: 0; }
.match-army-name {
    font-weight: 800;
    font-size: 15px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.match-army-meta {
    font-size: 12.5px;
    color: var(--text-dim);
    margin-top: 2px;
}
.stage-row {
    display: flex;
    align-items: center;
    gap: 10px;
    flex-wrap: wrap;
}
.stage-icon {
    flex-shrink: 0;
    color: var(--green);
    font-weight: 900;
    font-size: 18px;
    line-height: 1;
}
.stage-text { flex: 1; min-width: 0; }
.stage-title {
    font-weight: 700;
    font-size: 14px;
    color: var(--text);
}
.stage-sub {
    font-size: 12.5px;
    color: var(--text-dim);
    margin-top: 2px;
}
.choice-icon { flex-shrink: 0; font-size: 28px; color: var(--accent-soft); width: 36px; text-align: center; }
.choice-main { flex: 1; min-width: 0; }
.choice-title { font-weight: 700; font-size: 15px; }
.choice-sub   { font-size: 12.5px; color: var(--text-dim); margin-top: 2px; }
.detachment-incompat {
    margin-top: 4px;
    font-size: 11.5px;
    font-weight: 700;
    color: var(--gold);
    line-height: 1.35;
}
.choice-meta  { font-weight: 800; color: var(--gold); font-size: 13px; }

/* form fields */
.field { margin-bottom: 14px; }
.field-label {
    display: block;
    font-size: 12px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--text-mute);
    margin-bottom: 6px;
}
.field-input {
    width: 100%;
    padding: 12px 14px;
    border-radius: var(--radius-sm);
    background: var(--bg-elev);
    border: 1px solid var(--line);
    color: var(--text);
    font-size: 15px;
    outline: none;
}
.field-input:focus { border-color: var(--accent); }
.field-help { font-size: 12px; color: var(--text-mute); margin-top: 4px; }

/* army editor */
.editor-head {
    margin-bottom: 16px;
    padding-bottom: 14px;
    border-bottom: 1px solid var(--line);
}
.editor-name {
    font-size: 22px;
    font-weight: 800;
    letter-spacing: -0.01em;
    margin: 0;
}
.editor-meta {
    display: flex;
    flex-wrap: wrap;
    gap: 8px 14px;
    margin-top: 6px;
    font-size: 13px;
    color: var(--text-dim);
}
.editor-meta b { color: var(--text); }

.points-bar {
    margin: 12px 0 16px;
    padding: 12px 14px;
    border-radius: var(--radius);
    background: var(--bg-elev);
    border: 1px solid var(--line);
    display: flex;
    align-items: center;
    gap: 12px;
}
.points-bar .label { font-size: 12px; color: var(--text-mute); text-transform: uppercase; letter-spacing: 0.06em; }
.points-bar .num { font-weight: 800; font-size: 17px; }
.points-bar .num.over { color: var(--accent-soft); }
.points-bar .bar {
    flex: 1;
    height: 6px;
    border-radius: 3px;
    background: var(--bg-elev-2);
    overflow: hidden;
    border: 1px solid var(--line);
}
.points-bar .fill {
    height: 100%;
    background: linear-gradient(90deg, var(--green), var(--gold));
    transition: width 0.2s ease;
}
.points-bar .fill.over { background: linear-gradient(90deg, var(--gold), var(--accent)); }

/* validation banners */
.banner {
    padding: 10px 12px;
    border-radius: var(--radius-sm);
    font-size: 13.5px;
    margin-bottom: 8px;
    border: 1px solid var(--line);
    display: flex;
    align-items: flex-start;
    gap: 8px;
}
.banner-error  { background: rgba(200, 16, 46, 0.10); border-color: rgba(200, 16, 46, 0.45); color: var(--accent-soft); }
.banner-warn   { background: rgba(224, 177, 58, 0.08); border-color: rgba(224, 177, 58, 0.40); color: var(--gold); }
.banner-ok     { background: rgba(76, 184, 122, 0.08); border-color: rgba(76, 184, 122, 0.30); color: var(--green); }
.banner-info   { background: var(--bg-elev); }
/* Black Templars themed banner — used to surface Templar Vow effects
   (Suffer Not / Uphold) inline in the relevant phase wizards. Cooler
   purple-blue tone keeps it visually distinct from the gold-warning
   and red-error families that already crowd the wizard. */
.banner-bt     { background: rgba(120, 102, 196, 0.10); border-color: rgba(120, 102, 196, 0.45); color: #b3a8e0; }
/* v0.10.319 — Gameplan per-phase reminder banner. Parchment-gold tint
 * distinguishes it from the engine's rule banners (.banner-info etc.)
 * so the player reads it as "your own plan, not a rules clarification."
 * Mounted by renderActivationCard at the top of the card when the
 * gameplan has a note for the current phase. */
.banner-gameplan-phase {
    background: rgba(220, 180, 70, 0.07);
    border-color: rgba(220, 180, 70, 0.40);
    color: var(--text);
}
.gameplan-phase-note-head {
    font-size: 11.5px;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--gold, #e0c060);
    margin-bottom: 2px;
}
.gameplan-phase-note-text {
    font-size: 13px;
    line-height: 1.4;
    color: var(--text);
    white-space: pre-wrap;
}
.banner-icon { flex-shrink: 0; font-weight: 900; }

/* Feel No Pain — toggle row in the wound-alloc overview, plus the
   "last roll" banner in the allocate step. The banner uses the green
   family because each successful FNP roll is a save in the player's
   favour. */
.fnp-row {
    background: rgba(76, 184, 122, 0.08);
    border-color: rgba(76, 184, 122, 0.40);
}
.fnp-banner {
    background: rgba(76, 184, 122, 0.10);
    border-color: rgba(76, 184, 122, 0.40);
    color: var(--text);
    font-size: 13px;
}
.fnp-banner b { color: var(--green); font-family: ui-monospace, "SF Mono", Menlo, monospace; }
.fnp-banner .banner-icon { color: var(--green); }

/* unit row in editor */
.unit-row {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 12px 14px;
    background: var(--bg-elev);
    border: 1px solid var(--line);
    border-bottom: none;
    color: var(--text);
    text-decoration: none;
    cursor: pointer;
    min-height: 56px;
}
.unit-row:first-of-type { border-top-left-radius: var(--radius); border-top-right-radius: var(--radius); }
.unit-row:last-of-type  { border-bottom: 1px solid var(--line); border-bottom-left-radius: var(--radius); border-bottom-right-radius: var(--radius); }
.unit-row:active { background: var(--bg-elev-2); }
.unit-row-main { flex: 1; min-width: 0; }
.unit-row-title { font-weight: 700; font-size: 15px; }
.unit-row-sub { font-size: 12px; color: var(--text-dim); margin-top: 2px; }
.unit-row-cost { font-weight: 800; color: var(--gold); flex-shrink: 0; }
.unit-row-tags {
    display: flex;
    flex-wrap: wrap;
    gap: 4px;
    margin-top: 4px;
}
.unit-row-tag {
    font-size: 10px;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    padding: 1px 6px;
    border-radius: 4px;
    font-weight: 700;
}
.tag-warlord { background: rgba(224, 177, 58, 0.15); color: var(--gold); }
.tag-leader  { background: rgba(92, 182, 255, 0.15); color: var(--blue); }
.tag-attached{ background: rgba(76, 184, 122, 0.15); color: var(--green); }
.tag-enh     { background: rgba(200, 16, 46, 0.15); color: var(--accent-soft); }
.tag-epic    { background: rgba(255, 255, 255, 0.06); color: var(--text); }

.unit-row + .add-unit-btn { margin-top: 12px; }
.add-unit-btn {
    display: block;
    width: 100%;
    padding: 14px;
    border-radius: var(--radius);
    background: var(--bg-elev);
    border: 1px dashed var(--line);
    color: var(--accent-soft);
    font-weight: 700;
    text-align: center;
    text-decoration: none;
    font-size: 15px;
}
.add-unit-btn:active { background: var(--bg-elev-2); border-color: var(--accent); }

/* unit detail screen sections */
.section-block {
    background: var(--bg-elev);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    padding: 14px;
    margin-bottom: 12px;
}
.section-block-h {
    font-size: 11px;
    font-weight: 800;
    text-transform: uppercase;
    letter-spacing: 0.1em;
    color: var(--text-mute);
    margin: 0 0 10px;
}
.opt-row {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 10px 0;
    border-bottom: 1px solid var(--line);
}
.opt-row:last-child { border-bottom: none; }
.opt-row.is-disabled {
    opacity: 0.45;
    cursor: not-allowed;
    /* Used when an enhancement is over the army-wide 3/3 cap — keeps
       the row readable but visually off-limits. */
}
.opt-row.is-disabled input { cursor: not-allowed; }
.opt-row input[type="checkbox"], .opt-row input[type="radio"] {
    width: 20px;
    height: 20px;
    accent-color: var(--accent);
    flex-shrink: 0;
    cursor: pointer;
}
.opt-row label {
    flex: 1;
    cursor: pointer;
    font-size: 14px;
    line-height: 1.4;
}
.opt-row-cost {
    font-weight: 800;
    color: var(--gold);
    font-size: 13px;
    flex-shrink: 0;
}
.opt-text { font-size: 14px; color: var(--text); }
.opt-text ul { padding-left: 20px; margin: 4px 0; }

/* import file input shim */
.file-import-label {
    display: inline-block;
    cursor: pointer;
}
.file-import-input {
    position: absolute;
    width: 1px;
    height: 1px;
    opacity: 0;
    pointer-events: none;
}

/* small toolbar (export, delete, etc.) */
.toolbar {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    margin: 0 0 16px;
}
.toolbar .btn { flex: 0 1 auto; }
/* The view-toggle buttons (Editor / Game-Ready) inside the toolbar — the
   active view stays as .btn-primary; the inactive one is the standard btn. */
.toolbar .btn[aria-current="page"] { cursor: default; }

/* ============ LAN Match (lobby) ============ */

/* Variant of the Army Builder card for the LAN Match entry — slightly more
   subdued so the two CTAs don't both scream for attention. */
.army-cta.lan-cta {
    background:
        radial-gradient(circle at 100% 0%, rgba(92, 182, 255, 0.35), transparent 55%),
        linear-gradient(140deg, #243a52 0%, #122031 100%);
    box-shadow: 0 6px 20px rgba(40, 70, 110, 0.35);
}

/* v0.10.288 — side-by-side LAN/Online + Solo CTAs on the home screen.
   LAN gets ~60% (the headline match mode); Solo takes ~40%. A thin
   vertical line sits between them as a visual divider. Both CTAs
   share the same `.army-cta.lan-cta` blue palette so they read as
   "two ways to start a match" rather than two separate destinations.
   On narrow viewports (< 540px CSS wide — most phones in portrait)
   the row stacks back to a column so neither CTA gets cramped. */
.home-cta-pair {
    display: flex;
    align-items: stretch;
    gap: 0;
    margin: 0 0 24px;
}
.home-cta-pair > .army-cta {
    /* The pair container owns the bottom margin; individual CTAs lose
       theirs so the row sits tight. */
    margin: 0;
}
.home-cta-pair-major { flex: 3 1 0; min-width: 0; }
.home-cta-pair-minor { flex: 2 1 0; min-width: 0; }
.home-cta-pair-divider {
    flex: 0 0 1px;
    align-self: stretch;
    margin: 8px 12px;
    background: rgba(255, 255, 255, 0.18);
    border-radius: 1px;
    pointer-events: none;
}
/* v0.10.291 — Narrow viewports (phones + tablet portrait): the pair
   collapses into normal stacked CTAs that look identical to the
   Army Builder / Discovery CTAs sitting around them. We use
   `display: contents` on the wrapper so the box itself vanishes —
   the two CTAs and the divider become direct flow siblings of the
   surrounding cards.
   v0.10.293 — the desktop-only `.home-cta-pair > .army-cta {
   margin: 0 }` rule was still applying because `display:contents`
   keeps the children matching that selector. Explicitly restore
   the standard 24px bottom margin on mobile so the LAN + Solo
   CTAs have the same vertical breathing room as Army Builder
   and Discovery sitting around them. The pair only appears
   side-by-side at landscape tablet / desktop widths (≥ 900px
   CSS), matching the user's "wider landscape resolutions" ask. */
@media (max-width: 899px) {
    .home-cta-pair {
        display: contents;
    }
    .home-cta-pair > .army-cta {
        margin: 0 0 24px;
    }
    .home-cta-pair-divider {
        display: none;
    }
}

/* Discovery — green variant. Sits as the third hero CTA under Army
   Builder and LAN Match. Tonally distinct from the other two so the
   three stack reads as three separate destinations at a glance. */
.army-cta.discovery-cta {
    background:
        radial-gradient(circle at 100% 0%, rgba(116, 217, 121, 0.35), transparent 55%),
        linear-gradient(140deg, #1e5a3a 0%, #0d2918 100%);
    box-shadow: 0 6px 20px rgba(40, 110, 60, 0.35);
}
/* The Discovery CTA uses an <img> for its icon (AuspexLogo) instead of
   a font glyph, so size it to match the 38px font-icon footprint. */
.army-cta.discovery-cta .army-cta-icon {
    height: 60px;
    width: 60px;
    object-fit: contain;
    filter: drop-shadow(0 2px 12px rgba(0, 0, 0, 0.4));
}

/* Tab switcher between QR mode and Manual mode (used both as a "share"
   widget and an "ingest" widget). */
.sig-tabs { margin-top: 4px; }
.sig-tabbar {
    display: flex;
    gap: 2px;
    padding: 3px;
    background: var(--bg-elev);
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    margin-bottom: 12px;
}
.sig-tab {
    flex: 1;
    padding: 8px 10px;
    border-radius: 6px;
    background: transparent;
    color: var(--text-dim);
    font-weight: 700;
    font-size: 13px;
    cursor: pointer;
    transition: background 0.12s ease, color 0.12s ease;
}
.sig-tab.is-active {
    background: var(--accent);
    color: #fff;
}
.sig-pane[hidden] { display: none; }

/* QR display: a clean white card so the camera can read it. */
.qr-wrap {
    background: #fff;
    padding: 12px;
    border-radius: 12px;
    margin: 0 auto;
    box-shadow: 0 6px 20px rgba(0, 0, 0, 0.4);
    display: block;
}

/* Camera scanner frame — boxed video with a centered reticle. */
.qr-scanner-frame {
    position: relative;
    width: 100%;
    aspect-ratio: 1 / 1;
    max-width: 360px;
    margin: 0 auto;
    background: #000;
    border-radius: 12px;
    overflow: hidden;
    border: 1px solid var(--line);
}
.qr-scanner-video {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}
.qr-scanner-reticle {
    position: absolute;
    top: 50%;
    left: 50%;
    width: 65%;
    height: 65%;
    transform: translate(-50%, -50%);
    border: 2px solid var(--accent-soft);
    border-radius: 12px;
    box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.35);
    pointer-events: none;
}

/* ============ army game-ready view ============ */
.gv-card { padding: 14px 14px 16px; }
.gv-head {
    display: flex;
    align-items: baseline;
    flex-wrap: wrap;
    gap: 8px;
}
.gv-name {
    margin: 0;
    font-size: 18px;
    font-weight: 800;
    letter-spacing: -0.01em;
    color: var(--text);
}
.gv-meta {
    color: var(--text-mute);
    font-size: 13px;
}

/* Game-view abilities collapse to just their title; tap to expand. */
details.gv-ability {
    margin: 0;
    padding: 0;
    border-bottom: 1px solid var(--line);
}
details.gv-ability:last-of-type { border-bottom: none; }
details.gv-ability > summary {
    list-style: none;            /* hide native marker (Chrome/Firefox) */
    cursor: pointer;
    padding: 10px 4px;
    display: flex;
    align-items: center;
    gap: 10px;
    user-select: none;
    -webkit-tap-highlight-color: transparent;
    color: var(--accent-soft);
    font-weight: 700;
    font-size: 14px;
    border-radius: var(--radius-sm);
    transition: background 0.12s ease;
}
details.gv-ability > summary::-webkit-details-marker { display: none; }   /* Safari */
details.gv-ability > summary::before {
    content: "▸";
    color: var(--text-mute);
    font-size: 11px;
    flex-shrink: 0;
    transition: transform 0.18s ease, color 0.12s ease;
}
details.gv-ability[open] > summary::before {
    transform: rotate(90deg);
    color: var(--accent-soft);
}
details.gv-ability > summary:active { background: var(--bg-elev-2); }
@media (hover: hover) {
    details.gv-ability > summary:hover { background: var(--bg-elev-2); }
}
details.gv-ability > .ability-body {
    padding: 0 4px 12px 26px;    /* indent body under the chevron, drop a touch of bottom space */
    margin-top: -4px;
    color: var(--text);
}

/* ============ toast notifications ============ */
.toast-stack {
    position: fixed;
    right: 16px;
    bottom: calc(var(--bottombar-h) + var(--safe-bot) + 16px);
    z-index: 250;
    display: flex;
    flex-direction: column-reverse;
    gap: 8px;
    pointer-events: none;
    max-width: calc(100vw - 32px);
}
.toast {
    pointer-events: auto;
    display: flex;
    align-items: center;
    gap: 10px;
    min-width: 200px;
    max-width: 360px;
    padding: 12px 16px;
    border-radius: var(--radius);
    font-weight: 700;
    font-size: 14px;
    box-shadow: var(--shadow-2);
    cursor: pointer;
    animation: toast-in 0.25s cubic-bezier(0.2, 0.8, 0.2, 1);
}
.toast.is-leaving {
    animation: toast-out 0.22s ease forwards;
}
.toast-success {
    background: var(--green);
    color: #0a1f12;
    border: 1px solid rgba(0, 0, 0, 0.15);
}
.toast-error {
    background: var(--accent);
    color: #fff;
    border: 1px solid rgba(255, 255, 255, 0.12);
}
.toast-info {
    background: var(--bg-elev);
    color: var(--text);
    border: 1px solid var(--line);
}
/* Yellow "warning" toast — matches banner-warn's gold family. Used for
   informative caution messages (auto-skipped activations, swap-group
   auto-clears) that aren't errors but need to stand out. */
.toast-warn {
    background: var(--gold);
    color: #1a1404;
    border: 1px solid rgba(0, 0, 0, 0.20);
}
.toast-warn .toast-icon { background: rgba(0, 0, 0, 0.18); color: #1a1404; }
.toast-icon {
    flex-shrink: 0;
    width: 22px;
    height: 22px;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 14px;
    font-weight: 900;
    background: rgba(0, 0, 0, 0.18);
}
.toast-error .toast-icon { background: rgba(255, 255, 255, 0.18); }
.toast-info  .toast-icon { background: var(--bg-elev-2); color: var(--accent-soft); }
.toast-msg { flex: 1; line-height: 1.3; }

/* Large interactive toast for stratagem-trigger reminders. Bigger than
   the standard toast (more visible from a distance), uses an info-style
   surface with a green-glow accent border, and exposes an explicit "✕"
   close button in the top-right. Auto-dismiss timer is set by JS. */
.toast.toast-large {
    cursor: default;
    min-width: 280px;
    max-width: 460px;
    padding: 16px 44px 14px 16px;       /* extra top room for the progress bar */
    font-size: 15px;
    background: var(--bg-elev);
    color: var(--text);
    border: 1px solid var(--green);
    box-shadow:
        0 0 0 1px rgba(76, 184, 122, 0.30),
        0 8px 28px rgba(76, 184, 122, 0.22),
        0 12px 32px rgba(0, 0, 0, 0.45);
    position: relative;
    overflow: hidden;                    /* clip the progress bar to rounded corners */
}
/* Clickable variant — Toast.showLarge with onClick handler. Hover state
   nudges the toast so it feels interactive; tap is debounced by the
   inline `cursor: pointer` set by JS. */
.toast.toast-large.toast-clickable {
    transition: transform 120ms ease, box-shadow 120ms ease;
}
.toast.toast-large.toast-clickable:hover {
    transform: translateY(-1px);
    box-shadow:
        0 0 0 1px rgba(76, 184, 122, 0.45),
        0 10px 32px rgba(76, 184, 122, 0.32),
        0 14px 38px rgba(0, 0, 0, 0.55);
}
.toast.toast-large.toast-clickable:active {
    transform: translateY(0);
}
/* Auto-dismiss countdown bar at the top of the toast — animation duration
   is set inline (matches the JS duration) so the bar empties exactly
   when the toast closes. transform-based for compositor-only animation. */
.toast-progress {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    height: 3px;
    background: rgba(76, 184, 122, 0.20);
    overflow: hidden;
}
.toast-progress-fill {
    height: 100%;
    width: 100%;
    background: var(--green);
    transform-origin: left center;
    animation-name: toast-drain;
    animation-timing-function: linear;
    animation-fill-mode: forwards;
}
@keyframes toast-drain {
    from { transform: scaleX(1); }
    to   { transform: scaleX(0); }
}
@media (prefers-reduced-motion: reduce) {
    .toast-progress-fill { animation: none; transform: scaleX(0); }
}
.toast.toast-large .toast-icon {
    width: 32px;
    height: 32px;
    font-size: 18px;
    background: var(--green);
    color: #0a1f12;
}
.toast-close {
    position: absolute;
    top: 8px;
    right: 8px;
    width: 24px;
    height: 24px;
    border-radius: 50%;
    background: transparent;
    border: 1px solid var(--line);
    color: var(--text-mute);
    font-size: 12px;
    line-height: 1;
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0;
    transition: color 0.12s ease, border-color 0.12s ease, background 0.12s ease;
}
.toast-close:hover {
    color: var(--text);
    border-color: var(--text-dim);
    background: var(--bg-elev-2);
}

/* Stratagem-trigger glow — applied to "Use stratagem" buttons and the
   matching stratagem row inside the strat picker while the reminder
   toast is active. Pulsing green border + outer glow draws the eye to
   the manual fallback path. */
.is-trigger-glow {
    border-color: var(--green) !important;
    animation: trigger-glow-pulse 1.6s ease-in-out infinite;
}
@keyframes trigger-glow-pulse {
    0%, 100% {
        box-shadow:
            0 0 0 1px rgba(76, 184, 122, 0.45),
            0 0 8px rgba(76, 184, 122, 0.35);
    }
    50% {
        box-shadow:
            0 0 0 2px rgba(76, 184, 122, 0.85),
            0 0 16px rgba(76, 184, 122, 0.65);
    }
}
@media (prefers-reduced-motion: reduce) {
    .is-trigger-glow {
        animation: none;
        box-shadow: 0 0 0 2px rgba(76, 184, 122, 0.7);
    }
}

@keyframes toast-in {
    from { opacity: 0; transform: translateY(12px) scale(0.96); }
    to   { opacity: 1; transform: translateY(0) scale(1); }
}
@keyframes toast-out {
    to { opacity: 0; transform: translateY(6px); }
}

/* on small phones — give the toast more room and keep it pinned */
@media (max-width: 480px) {
    .toast-stack { left: 16px; right: 16px; align-items: flex-end; }
    .toast       { width: 100%; max-width: none; }
}

/* v0.10.210 — Firing Deck activation picker tiles. Side-by-side
   option cards inside the "Which Unit to activate?" modal. Default
   style = dark surface + faint border; selected = accent border +
   subtle glow; done = dimmed + opacity reduced + crossed cursor. */
.fd-picker-tile {
    background: var(--bg-elev);
    border: 1px solid var(--line);
    border-radius: 8px;
    color: var(--text);
    cursor: pointer;
    transition: border-color 0.15s ease, box-shadow 0.15s ease, background 0.15s ease;
}
.fd-picker-tile:hover:not(.is-done):not(.is-selected) {
    border-color: rgba(255, 255, 255, 0.22);
    background: var(--bg-elev-2);
}
.fd-picker-tile.is-selected {
    border-color: var(--accent-soft);
    box-shadow: 0 0 0 1px var(--accent-soft) inset, 0 4px 14px rgba(200, 16, 46, 0.18);
    background: var(--bg-elev-2);
}
.fd-picker-tile.is-done {
    opacity: 0.55;
    cursor: not-allowed;
}

/* v0.10.207 — in VTT bg-mode + collapsed, the floating UI at the
   bottom of the viewport is the expand-chevron (38px) above the
   phase pill (~50px) with a 10px gap, anchored at
   var(--bottombar-h) + var(--safe-bot) + 14px. The default toast-
   stack `bottom` sits AT that overlay's level and overlaps the
   pill (which is where the Next-phase button lives in bg-collapsed).
   Lift the toast stack above the whole overlay so phase-feedback
   toasts stop covering the pill the player is trying to read / tap.
   Only fires in bg-collapsed — bg-mode-not-collapsed renders the
   match panel below and keeps the original toast position. */
body.tabletop-bg-mode.tabletop-collapsed .toast-stack {
    bottom: calc(var(--bottombar-h) + var(--safe-bot) + 130px);
}

@media (prefers-reduced-motion: reduce) {
    .toast            { animation: none; }
    .toast.is-leaving { animation: none; opacity: 0; transition: opacity 0.15s; }
}

/* ============ tooltip / glossary popover ============ */
.has-glossary {
    cursor: help;
    border-bottom: 1px dotted currentColor;
    transition: background 0.12s ease;
    border-radius: 3px;
}
.has-glossary:active { background: rgba(255,255,255,0.06); }
.chip.has-glossary,
.weapon-ability.has-glossary {
    border-bottom-style: solid;
    border-bottom-width: 1px;
}

.tooltip {
    position: fixed;
    z-index: 300;
    max-width: min(360px, calc(100vw - 24px));
    max-height: 60vh;
    overflow-y: auto;
    overscroll-behavior: contain;
    background: var(--bg-elev);
    border: 1px solid var(--accent);
    border-radius: var(--radius);
    box-shadow: var(--shadow-2);
    padding: 14px 16px;
    font-size: 13.5px;
    line-height: 1.5;
    color: var(--text);
    opacity: 0;
    transform: translateY(-4px);
    pointer-events: none;
    transition: opacity 0.14s ease, transform 0.14s ease;
}
.tooltip.is-open {
    opacity: 1;
    transform: translateY(0);
    pointer-events: auto;
}
.tooltip-name {
    font-size: 14px;
    font-weight: 800;
    color: var(--accent-soft);
    margin: 0 0 6px;
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 8px;
}
.tooltip-name .tooltip-cat {
    font-size: 10px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--text-mute);
    border: 1px solid var(--line);
    padding: 2px 6px;
    border-radius: 999px;
}
.tooltip-body {
    color: var(--text);
    font-size: 13.5px;
}
.tooltip-body p { margin: 6px 0; }
.tooltip-body ul { padding-left: 20px; margin: 6px 0; }
.tooltip-body .kwb { font-weight: 700; text-transform: uppercase; font-size: 0.92em; letter-spacing: 0.04em; }
.tooltip-body .kwb2.bluefont { color: var(--blue); font-weight: 700; font-size: 0.9em; letter-spacing: 0.04em; }
.tooltip-body .redExample {
    background: rgba(200, 16, 46, 0.06);
    border-left: 3px solid var(--accent);
    padding: 6px 10px;
    border-radius: 4px;
    margin: 6px 0;
    font-size: 12.5px;
}
.tooltip-body .ShowFluff { display: block; color: var(--text-dim); font-style: italic; margin-bottom: 6px; }
.tooltip-close {
    position: absolute;
    top: 6px;
    right: 6px;
    width: 28px;
    height: 28px;
    border-radius: 6px;
    color: var(--text-mute);
    font-size: 18px;
    display: flex;
    align-items: center;
    justify-content: center;
}
.tooltip-close:active { background: var(--bg-elev-2); }

/* on hover (desktop), prevent flicker */
@media (hover: hover) {
    .has-glossary:hover { background: rgba(200,16,46,0.08); }
}

/* ============================================================
   Tabletop background mode (v0.10.25)
   ============================================================
   When the match is active and tabletop is enabled, the existing
   .tabletop-card is CSS-pulled out to fill the viewport between
   topbar and bottombar — edge-to-edge, no padding, no visible
   chrome. The rest of the match UI lives in a .match-panel
   wrapper above it; collapse slides the panel offscreen with a
   smooth transform animation, and a minimal floating phase pill
   takes over at the bottom of the viewport. */

/* Tabletop fills the entire space between topbar and bottombar.
   No padding, no margin, no border — table surface goes truly
   edge-to-edge. */
body.tabletop-bg-mode .tabletop-card {
    position: fixed;
    top: calc(var(--topbar-h) + var(--safe-top));
    left: 0;
    right: 0;
    bottom: calc(var(--bottombar-h) + var(--safe-bot));
    margin: 0;
    z-index: 1;
    border-radius: 0;
    box-shadow: none;
    /* Transparent so the body's tint colour shows through —
       dark by default, green when it's our turn (.is-my-turn),
       yellow during counter-fight / reactive-wait. The table
       surface inside the canvas blends with the same tint via
       a low-alpha overlay so the playable area is still visually
       distinct without breaking the colour cue. */
    background: transparent;
    padding: 0;
    max-width: none;
    overflow: hidden;
    border: none;
}
body.tabletop-bg-mode .tabletop-card > summary { display: none; }
/* Body is a containing block for the absolutely-positioned canvas;
   no percentage-height chain to worry about. Transparent so the
   body tint propagates through. */
body.tabletop-bg-mode .tabletop-body {
    position: absolute;
    inset: 0;
    padding: 0;
    max-height: none;
    overflow: hidden;
    background: transparent;
}
body.tabletop-bg-mode .tabletop-body > .text-mute:first-child { display: none; }
/* Canvas fills the body via absolute positioning — bypasses the
   `height: 100%` percentage-height chain that wasn't resolving
   inside the <details> element on mobile WebKit. */
body.tabletop-bg-mode .tabletop-canvas {
    position: absolute !important;
    inset: 0 !important;
    width: 100% !important;
    height: 100% !important;
    border: none !important;
    border-radius: 0 !important;
    background: transparent !important;
    display: block;
}
/* Layer chips + reset-view get pulled out of normal flow into a
   small floating overlay in the top-left corner of the canvas.
   Hidden when the panel covers the canvas (would only confuse
   the user) — only visible in collapsed mode. */
body.tabletop-bg-mode .tabletop-layer-chips,
body.tabletop-bg-mode .tabletop-view-controls {
    position: fixed;
    /* v0.10.206 — push below the .game-header (round info + turn line
     * + CP/VP pill row) so the floating chips/Reset button don't
     * overlap the match header in bg-mode.
     * v0.10.223 — bumped 108 → 160. The CP/VP row stacks faction caption
     * + detachment line above the CP pill in `.cp-list-col`, which made
     * the header ~140px tall on real screens — Reset View was sitting
     * on top of the CP pills. 160 keeps ~16px breathing room. */
    top: calc(var(--topbar-h) + var(--safe-top) + 160px);
    z-index: 5;
    margin: 0 !important;
    padding: 4px 6px;
    background: rgba(13, 14, 18, 0.55);
    border-radius: 8px;
    backdrop-filter: blur(6px);
    -webkit-backdrop-filter: blur(6px);
    pointer-events: auto;
    display: none;
}
body.tabletop-bg-mode .tabletop-layer-chips { left: 8px; }
body.tabletop-bg-mode .tabletop-view-controls { right: 8px; }
body.tabletop-bg-mode.tabletop-collapsed .tabletop-layer-chips,
body.tabletop-bg-mode.tabletop-collapsed .tabletop-view-controls {
    display: flex;
}

/* v0.10.200 — game-header pinned at the top of the VTT in bg-mode.
   The header is now a sibling of .match-panel (rendered onto the
   .screen container in app.js paint()), so it survives the panel's
   slide-off when the player hides the UI.
   v0.10.208 — split the "header chrome" rules so the opaque gradient
   is retained while the panel is EXPANDED (i.e., the match UI is
   visible underneath). Without the gradient the panel cards scroll
   visibly underneath the sticky header, which played awful with the
   user's expectation that body content stops at the header line —
   the previous behaviour relied on the header being a visual mask.
   When the panel is collapsed (canvas-only mode), drop back to
   transparent so the tabletop reads through cleanly. */
body.tabletop-bg-mode.tabletop-collapsed .game-header {
    background: transparent;
    border-bottom: none;
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
    pointer-events: none;
}
/* v0.10.256 — give the header a little breathing room from the
 * viewport edges in bg-mode. The base rule uses `margin: -16px
 * -16px 14px` to escape .screen's 16px side padding so the header
 * spans the full content area. (Originally this override also
 * handled the tutorial route's `#screen { padding: 0 }` rule —
 * removed in v0.10.323 so the tutorial inherits the same padding
 * as PvP/Solo, leaving only the bg-mode "pull past padding"
 * concern below.) Override the side margins to small positive
 * values so the round/CP-pills aren't hard-pinned against the
 * screen edge on ultrawide bg-mode views.
 *
 * v0.10.263 — match the .match-panel's width pattern so the panel
 * cards don't bleed past the header bg sides at narrow widths.
 * Was `margin-left/right: 12px` (24px narrower than the panel at
 * sub-960px viewports); now both elements share `max-width:
 * min(960px, calc(100% - 24px))` so they line up edge-to-edge AND
 * keep 12px breathing room from the viewport. */
/* v0.10.295 — header goes edge-to-edge in bg-mode. The previous
   `max-width: min(960px, calc(100% - 24px))` cap left 12px gutters
   on each side; my prior v0.10.294 fix used `margin-left: 0`
   which left the header constrained by `#screen`'s 16px L/R
   padding (the actual containing box is the .screen element, not
   the viewport). Use the same `-16px` negative margins as the base
   rule so the header pulls out past .screen's padding and spans
   the FULL viewport width — matching the user's "no margin" ask.
   The .match-panel below KEEPS its width cap (that's the "core
   match UI" the user wants framed); only the top header strip
   goes full-width.
   `100vw` would also work but introduces a horizontal scrollbar
   when a vertical scrollbar is present on desktop; negative
   margins stay inside the scroll viewport so no extra scrollbar
   gets created. */
body.tabletop-bg-mode .game-header {
    max-width: none;
    margin-left: -16px;
    margin-right: -16px;
}
body.tabletop-bg-mode .game-header .cp-pill,
body.tabletop-bg-mode .game-header .cp-list-btn,
/* v0.10.208 — the phase-info ⓘ button next to the phase text was
   missing from the whitelist, so in bg-mode the click passed through
   to the canvas underneath and the info modal never opened. */
body.tabletop-bg-mode .game-header .phase-info-btn {
    pointer-events: auto;
}

/* v0.10.294 — shrink the game-header text on narrow viewports so
   the round / turn / CP pills / faction captions all fit without
   truncating mid-word. Targets phones in both orientations + iPad
   portrait (the side-by-side CTA breakpoint elsewhere on the home
   screen). Each step is ~1-2px under the desktop sizing — enough
   to claw back ~15% horizontal text width without the header
   feeling cramped vertically. */
@media (max-width: 720px) {
    .game-header { padding: 10px 14px 12px; }
    .game-round { font-size: 11px; letter-spacing: 0.06em; }
    .game-mission-tag { font-size: 10px; letter-spacing: 0.04em; }
    .game-turn { font-size: 14.5px; }
    .game-cp { gap: 4px 6px; margin-top: 6px; }
    .cp-pill { font-size: 10.5px; padding: 5px 8px; gap: 4px; }
    .cp-list-faction { font-size: 10px; }
    .cp-list-detachment { font-size: 9px; }
}

/* Match panel: in bg-mode it slides offscreen smoothly when
   collapsed. Using transform (not display:none) gives us the
   animation; pointer-events is killed so a stale element doesn't
   eat taps after the slide. */
/* v0.10.248 — cap the panel's "guts" (DEPLOYMENT / ACTION LOG / activation
 * cards / etc.) so they don't stretch full-bleed on ultrawide viewports.
 * The .game-header is a SIBLING of .match-panel (see paint() in app.js)
 * and keeps its own full-width behaviour via its negative margins — only
 * the cards inside the panel are capped here.
 *
 * On the normal route .screen already caps at 880px, so this is a no-op
 * there. On the tutorial route .screen drops max-width to none, and this
 * is where the cap actually kicks in. */
.match-panel {
    position: relative;
    /* v0.10.263 — capped to `min(960px, 100% - 24px)` so the panel
     * keeps the same 12px breathing room from the viewport edges as
     * the bg-mode game-header. Previously `max-width: 960px` alone
     * left the panel at 100% width on sub-960px viewports while the
     * header sat 12px in, producing a visible card-bleed past the
     * header bg on the tutorial route. */
    max-width: min(960px, calc(100% - 24px));
    margin-left: auto;
    margin-right: auto;
    /* v0.10.263 — bottom padding so the action log card + last
     * activation row stays clear of the floating SKIP TUTORIAL pip
     * (bottom-left, tutorial route) and the Export log / End match
     * toolbar at the bottom of the viewport. */
    padding-bottom: 80px;
}
body.tabletop-bg-mode .match-panel {
    z-index: 10;
    /* Transparent so the tabletop shows through the gaps between
       individual cards. Each card carries its own bg, so the UI
       text stays readable on its own card surface. */
    background: transparent;
    transform: translateY(0);
    transition: transform 0.34s cubic-bezier(0.2, 0.7, 0.2, 1),
                opacity 0.24s ease-out;
    opacity: 1;
    will-change: transform;
}
body.tabletop-bg-mode.tabletop-collapsed .match-panel {
    transform: translateY(110vh);
    opacity: 0;
    pointer-events: none;
}
/* v0.10.261 — Lock body + html scroll when the match panel slides
 * off-screen. The translateY(110vh) above keeps the panel in flow
 * (transforms don't reshape layout), so most browsers extend the
 * scrollable area to encompass the transformed bounding box ~110vh
 * below the viewport. The page should never scroll while the
 * battlefield is the focal surface — this both prevents the
 * stray scrollbar and avoids the strange "page scrolls behind the
 * canvas" behaviour reported in the tutorial collapsed view. */
html:has(body.tabletop-bg-mode.tabletop-collapsed),
body.tabletop-bg-mode.tabletop-collapsed {
    overflow: hidden;
}

/* In collapsed state the panel is offscreen, so we render a
   minimal floating UI at the bottom of the viewport: the chevron
   on top, then a slim "phase pill" with the phase name + a single
   contextual action button (advance-phase / counter-strat / etc).
   Pill fades in only while collapsed — sits invisible in DOM
   otherwise so the expand can animate from "card visible" back to
   "card hidden" without the pill leaking through. */
.tabletop-collapsed-overlay {
    position: fixed;
    left: 0;
    right: 0;
    bottom: calc(var(--bottombar-h) + var(--safe-bot) + 14px);
    z-index: 35;
    display: flex;
    flex-direction: column;
    align-items: stretch;
    padding: 0 12px;
    gap: 10px;
    pointer-events: none;
    opacity: 0;
    transform: translateY(20px);
    transition: opacity 0.24s ease-out 0.05s,
                transform 0.34s cubic-bezier(0.2, 0.7, 0.2, 1);
}
body.tabletop-bg-mode.tabletop-collapsed .tabletop-collapsed-overlay {
    opacity: 1;
    transform: translateY(0);
}
.tabletop-collapsed-overlay > * { pointer-events: auto; }

/* The expand chevron — wide, attention-grabbing, gently bobbing.
   Sits above the phase pill in the overlay. */
.match-expand-chevron {
    align-self: center;
    width: 108px;
    height: 38px;
    border: none;
    background: rgba(200, 16, 46, 0.95);
    color: #fff;
    border-radius: 12px;
    font-size: 14px;
    font-weight: 900;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    box-shadow: 0 6px 16px rgba(0, 0, 0, 0.55);
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
    animation: chevron-bob 1.6s ease-in-out infinite;
    /* v0.10.345 — mobile: holding + dragging vertically on this
       button would scroll the page (the match panel above is
       scrollable in bg-collapsed mode), pushing the match header off
       screen. `touch-action: none` matches the canvas behavior so a
       finger-drag on the button does nothing instead of hijacking
       the page scroll. Tap-to-click is unaffected. */
    touch-action: none;
}
.match-expand-chevron:active { transform: scale(0.96); }
@keyframes chevron-bob {
    0%, 100% { transform: translateY(0); }
    50%      { transform: translateY(-5px); }
}

/* Slim phase pill — single line with the phase name on the left
   and an optional contextual button on the right (advance phase
   if it's my turn, or the most relevant reactive trigger if not).
   Tap-the-rest-of-the-pill is a synonym for tap-the-chevron — the
   whole strip is an "open UI" affordance. */
.tabletop-collapsed-pill {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 10px 14px;
    background: rgba(20, 22, 28, 0.92);
    border: 1px solid var(--line);
    border-radius: 14px;
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.55);
    backdrop-filter: blur(8px);
    -webkit-backdrop-filter: blur(8px);
    /* v0.10.345 — same anti-page-scroll fix as .match-expand-chevron.
       Children inherit no touch-action by default, so we also set it
       on each interactive child below (.tabletop-collapsed-pill-label
       and .tabletop-collapsed-pill-action) to make sure a finger-
       drag anywhere inside the pill doesn't scroll the match panel. */
    touch-action: none;
}
.tabletop-collapsed-pill-label {
    flex: 1 1 auto;
    min-width: 0;
    font-size: 14px;
    font-weight: 800;
    color: var(--text);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    touch-action: none;
}
.tabletop-collapsed-pill-tag {
    font-size: 10.5px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--text-mute);
    margin-left: 6px;
}
.tabletop-collapsed-pill-tag.is-active { color: var(--green); }
.tabletop-collapsed-pill-tag.is-spectating { color: var(--blue); }
.tabletop-collapsed-pill-action {
    flex: 0 0 auto;
    padding: 8px 12px;
    border: 1px solid var(--line);
    border-radius: 10px;
    background: rgba(200, 16, 46, 0.18);
    color: var(--accent-soft);
    font-size: 13px;
    font-weight: 800;
    cursor: pointer;
    touch-action: none;
}
.tabletop-collapsed-pill-action.is-primary {
    background: var(--accent);
    color: #fff;
    border-color: transparent;
}
.tabletop-collapsed-pill-action:active { transform: scale(0.97); }

/* Pulse animation for the VTT collapsed-overlay Next-Phase button
 * when the phase is fully resolved and the player just needs to
 * commit — mirrors the match-panel advance button's ready-to-advance
 * affordance so users in bg-collapsed mode don't miss it. */
@keyframes vtt-ready-pulse {
    0%, 100% {
        box-shadow: 0 0 0 0 rgba(255, 64, 96, 0.55), 0 0 8px 2px rgba(200, 16, 46, 0.45);
        transform: scale(1);
    }
    50% {
        box-shadow: 0 0 0 8px rgba(255, 64, 96, 0.0), 0 0 16px 4px rgba(255, 90, 110, 0.65);
        transform: scale(1.04);
    }
}
.tabletop-collapsed-pill-action.is-pulsing {
    animation: vtt-ready-pulse 1.2s ease-in-out infinite;
}
.tabletop-collapsed-pill-action.is-pulsing:active { animation: none; }

/* v0.10.266 — Deployment-phase unit cycler in the collapsed pill.
 * Replaces the right-hand "Next phase →" / "⚡ Strat" button while
 * the local player has units left to deploy: shows the currently
 * picked unit's name + a stacked ▲/▼ pair to flip through the
 * undeployed list without expanding the match panel. Same visual
 * weight as the legacy pill-action button so the pill layout
 * doesn't jump when we swap in/out. */
.deploy-pill-cycler {
    flex: 0 0 auto;
    display: flex;
    align-items: stretch;
    gap: 6px;
    max-width: 60%;
}
.deploy-pill-cycler-name {
    display: flex;
    flex-direction: column;
    justify-content: center;
    padding: 6px 12px;
    background: rgba(255, 255, 255, 0.06);
    border: 1px solid rgba(255, 255, 255, 0.12);
    border-radius: 10px;
    min-width: 0;
    flex: 1 1 auto;
    overflow: hidden;
}
.deploy-pill-cycler-name-label {
    font-size: 13px;
    font-weight: 800;
    color: var(--text);
    line-height: 1.15;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.deploy-pill-cycler-name-sub {
    font-size: 10px;
    font-weight: 700;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--text-mute);
    margin-top: 2px;
    line-height: 1;
}
.deploy-pill-cycler-arrows {
    display: flex;
    flex-direction: column;
    gap: 3px;
    flex: 0 0 auto;
}
.deploy-cycle-btn {
    flex: 1 1 auto;
    min-width: 32px;
    padding: 0 10px;
    background: var(--accent);
    color: #fff;
    border: 1px solid var(--accent);
    border-radius: 6px;
    font-size: 11px;
    font-weight: 800;
    line-height: 1;
    cursor: pointer;
    display: inline-flex;
    align-items: center;
    justify-content: center;
}
.deploy-cycle-btn:hover { filter: brightness(1.1); }
.deploy-cycle-btn:active { transform: scale(0.95); }

/* Game-header in bg-mode (expanded) keeps its solid bg. When the
   panel slides out it goes with it — no need for a transparent
   variant since the panel is gone. */

/* iOS text-selection guard. When the tabletop is the background,
   dragging a token can accidentally trigger the OS text-selection
   callout (Copy / Search with Google) on the topbar title or any
   stray text the gesture sweeps across. Disable selection +
   long-press callout on every chrome element that lives over the
   canvas; the panel's interior (action log, etc.) keeps normal
   selection since the gesture-target is only the canvas region. */
body.tabletop-bg-mode .topbar,
body.tabletop-bg-mode .tabletop-card,
body.tabletop-bg-mode .tabletop-canvas,
body.tabletop-bg-mode .game-header,
body.tabletop-bg-mode .tabletop-collapsed-overlay,
body.tabletop-bg-mode .tabletop-layer-chips,
body.tabletop-bg-mode .tabletop-toolbar,
body.tabletop-bg-mode .tabletop-view-controls {
    -webkit-user-select: none;
    user-select: none;
    -webkit-touch-callout: none;
}

/* Active state for the tool palette + layer chips. Uses the
   aria-pressed attribute so the markup stays semantic. */
.tabletop-tool[aria-pressed="true"],
.tabletop-layer-chip[aria-pressed="true"] {
    background: rgba(95, 158, 255, 0.22);
    border-color: rgba(95, 158, 255, 0.75);
    color: #cfe1ff;
}

/* v0.10.188 — vertical-stacked toolbar with collapsible "Tools" +
   "Height" sections. Each section is a `<details>` element so the
   native disclosure triangle drives open/close; we just style it.
   Buttons inside each section stack column-wise + span full width
   so the palette reads as a clean vertical menu instead of two
   wrapping horizontal rows. */
.tabletop-toolbar.tabletop-toolbar-stacked {
    display: flex;
    flex-direction: column;
    gap: 8px;
    margin-top: 8px;
    align-items: stretch;
}
.tabletop-section { margin: 0; }
.tabletop-section-h {
    font-size: 10.5px;
    font-weight: 800;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--text-mute, #9aa0aa);
    padding: 4px 6px;
    cursor: pointer;
    user-select: none;
    list-style: none;
    display: flex;
    align-items: center;
    gap: 6px;
}
.tabletop-section-h::-webkit-details-marker { display: none; }
.tabletop-section-h::before {
    content: "▸";
    font-size: 10px;
    opacity: 0.7;
    transition: transform 0.15s ease;
    display: inline-block;
}
.tabletop-section[open] > .tabletop-section-h::before { transform: rotate(90deg); }
.tabletop-section-h:hover { color: #fff; }
.tabletop-section-body {
    display: flex;
    flex-direction: column;
    gap: 4px;
    padding: 4px 0 0 0;
}
.tabletop-section-body > .btn { width: 100%; justify-content: flex-start; text-align: left; }
/* In bg-mode, lift the whole toolbar (tools + layer chips) into
   the same floating overlay that already handles the layer chips.
   The inner blocks keep their own flex flow so labels stay readable. */
body.tabletop-bg-mode .tabletop-toolbar {
    position: fixed;
    /* v0.10.206 — same +108 push as the layer-chips/reset-view block
     * above. The Tools palette sits directly below the .game-header
     * so it doesn't cover the round/turn/CP-pill row.
     * v0.10.223 — bumped 108 → 160 to match the chips/reset-view fix:
     * Tools column was overlapping the CP/VP pills on real screens. */
    top: calc(var(--topbar-h) + var(--safe-top) + 160px);
    left: 8px;
    z-index: 5;
    margin: 0 !important;
    padding: 6px;
    background: rgba(13, 14, 18, 0.55);
    border-radius: 8px;
    backdrop-filter: blur(6px);
    -webkit-backdrop-filter: blur(6px);
    pointer-events: auto;
    display: none;
    /* v0.10.141 — stack tool + layer chip groups vertically so the
       palette occupies a narrow column anchored upper-left instead
       of a wide bar that pushes other UI off-screen on mobile. */
    flex-direction: column;
    align-items: stretch;
    gap: 4px;
}
body.tabletop-bg-mode.tabletop-collapsed .tabletop-toolbar {
    display: flex;
}
/* The inner .tabletop-layer-chips used to be the floated element on
   its own — now it sits inside .tabletop-toolbar so cancel its
   stand-alone fixed positioning in bg-mode to avoid double-floating.
   Keep the chips vertical too so the palette stays a single column. */
body.tabletop-bg-mode .tabletop-toolbar .tabletop-layer-chips {
    position: static;
    background: transparent;
    padding: 0;
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
    display: flex;
    flex-direction: column;
    gap: 4px;
}
/* Individual tool / layer chip buttons span the full column width so
   touch targets line up vertically. */
body.tabletop-bg-mode .tabletop-toolbar .tabletop-tool,
body.tabletop-bg-mode .tabletop-toolbar .tabletop-layer-chip {
    justify-content: flex-start;
    text-align: left;
}

/* Pending-move overlay — floating Revert + Confirm-activation
   buttons that hover above the moved token while the player is
   positioning it on the canvas. Mounted to <body> as
   position:fixed; JS updates left/top each paint. */
.pending-move-overlay {
    display: flex;
    flex-direction: row;
    align-items: center;
    gap: 8px;
    padding: 0;
    pointer-events: none;
}
.pending-move-overlay > * { pointer-events: auto; }
.pending-move-overlay .pm-cancel,
.pending-move-overlay .pm-revert,
.pending-move-overlay .pm-confirm {
    padding: 10px 14px;
    font-size: 13px;
    font-weight: 800;
    letter-spacing: 0.04em;
    border-radius: 10px;
    box-shadow: 0 8px 22px rgba(0, 0, 0, 0.55);
    white-space: nowrap;
    cursor: pointer;
    /* v0.10.345 — see .match-expand-chevron comment. Same anti-page-
       scroll fix for the floating Revert / Confirm / Cancel buttons
       that hover above the moved token during a pending move. */
    touch-action: none;
}
.pending-move-overlay .pm-cancel {
    background: rgba(20, 22, 28, 0.92);
    color: var(--text-mute);
    border: 1px solid var(--line);
}
.pending-move-overlay .pm-cancel:hover { color: var(--accent-soft); border-color: rgba(200, 16, 46, 0.4); }
.pending-move-overlay .pm-cancel:active { transform: scale(0.97); }
.pending-move-overlay .pm-revert {
    background: rgba(40, 44, 56, 0.92);
    color: var(--text);
    border: 1px solid var(--line);
}
.pending-move-overlay .pm-revert:active { transform: scale(0.97); }
.pending-move-overlay .pm-confirm {
    background: var(--accent);
    color: #fff;
    border: 1px solid transparent;
}
.pending-move-overlay .pm-confirm:active { transform: scale(0.97); }

/* ===========================================================
 * Custom always-visible number-input spinner buttons
 * ===========================================================
 *
 * Native <input type=number> spin buttons are tiny on desktop and
 * invisible / unreachable on mobile. We wrap every number input via
 * the enhanceNumberInputs() DOM observer with explicit ▲ / ▼ buttons
 * positioned to the right of the field. The native spin buttons are
 * suppressed (`-webkit-inner-spin-button { display:none }`) so the
 * custom ones are the only affordance.
 *
 * Layout: an inline flex row containing the original input + a
 * vertical stack of two square buttons. Each button calls
 * input.stepUp/Down + dispatches a synthetic 'input' event so any
 * existing listener (`row.querySelector("input").addEventListener("input", ...)`)
 * keeps working.
 */
.num-stepper-wrap {
    display: inline-flex;
    align-items: stretch;
    gap: 4px;
    vertical-align: middle;
}
.num-stepper-wrap > input[type="number"] {
    /* Suppress the native browser spinners — the wrapper provides custom ones. */
    -moz-appearance: textfield;
    appearance: textfield;
}
.num-stepper-wrap > input[type="number"]::-webkit-outer-spin-button,
.num-stepper-wrap > input[type="number"]::-webkit-inner-spin-button {
    -webkit-appearance: none;
    appearance: none;
    margin: 0;
}
.num-stepper-buttons {
    display: inline-flex;
    flex-direction: column;
    gap: 2px;
    align-self: stretch;
}
.num-stepper-btn {
    flex: 1 1 0;
    min-width: 28px;
    min-height: 18px;
    padding: 0 6px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    background: rgba(200, 16, 46, 0.16);
    color: #ff6b7e;
    border: 1px solid rgba(200, 16, 46, 0.55);
    border-radius: 4px;
    font-size: 10px;
    line-height: 1;
    cursor: pointer;
    user-select: none;
    -webkit-tap-highlight-color: transparent;
    touch-action: manipulation;
    transition: background-color 0.08s ease, transform 0.05s ease;
}
.num-stepper-btn:hover {
    background: rgba(200, 16, 46, 0.30);
    color: #ff8a99;
}
.num-stepper-btn:active {
    background: rgba(200, 16, 46, 0.45);
    transform: scale(0.94);
}
.num-stepper-btn:disabled {
    opacity: 0.35;
    cursor: not-allowed;
    pointer-events: none;
}
.num-stepper-btn[aria-label="Increment"]::before { content: "\25B2"; }
.num-stepper-btn[aria-label="Decrement"]::before { content: "\25BC"; }

/* VTT Activate-button overlay — appears below the selected token's
 * nameplate during Movement / Shooting phase, letting the player
 * preview range + LOS rings before opening the wizard. */
.activate-overlay {
    pointer-events: auto;
}
.activate-overlay .activate-overlay-btn {
    padding: 8px 16px;
    font-size: 13px;
    font-weight: 700;
    letter-spacing: 0.04em;
    border-radius: 6px;
    box-shadow: 0 4px 14px rgba(0, 0, 0, 0.55), 0 0 0 1px rgba(255, 255, 255, 0.08) inset;
    background: var(--accent);
    color: #fff;
    border: 1px solid rgba(255, 255, 255, 0.18);
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
    /* v0.10.345 — was `manipulation`, which still allows pan and
       therefore let a vertical finger-drag scroll the match panel
       off-screen on mobile. `none` suppresses all touch gestures
       except tap, matching the canvas behavior. */
    touch-action: none;
    transition: transform 0.06s ease, box-shadow 0.12s ease;
    white-space: nowrap;
}
.activate-overlay .activate-overlay-btn:hover { box-shadow: 0 6px 18px rgba(0, 0, 0, 0.65), 0 0 0 1px rgba(255, 255, 255, 0.18) inset; }
.activate-overlay .activate-overlay-btn:active { transform: scale(0.96); }
@media (pointer: coarse) {
    .activate-overlay .activate-overlay-btn { padding: 10px 20px; font-size: 14px; }
}

/* End-of-turn scoring modal — "auto-detected" badge next to prompts
 * whose value was pre-filled from VTT / action-log data so the player
 * knows the number isn't just a zero default. */
.auto-detect-badge {
    display: inline-block;
    margin-left: 8px;
    padding: 1px 6px;
    font-size: 10px;
    font-weight: 700;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: #9be7a0;
    background: rgba(95, 200, 110, 0.14);
    border: 1px solid rgba(95, 200, 110, 0.45);
    border-radius: 3px;
    vertical-align: middle;
    cursor: help;
}
/* Mobile: bigger tap targets. */
@media (pointer: coarse) {
    .num-stepper-btn {
        min-width: 36px;
        min-height: 24px;
        font-size: 12px;
    }
}

/* ============================================================
 * v0.10.211 — First-time-user welcome wizard
 * ============================================================
 *
 * Full-screen step machine that paints over #screen. Grimdark
 * vocabulary (dark surfaces, red accent, oxidised gold, uppercase
 * serif weight on the headline) but kept readable + welcoming with
 * generous padding, large tap targets, and a subdued aquila
 * watermark behind the intro. Faction tiles in step D pick up a
 * per-tile --ftu-accent CSS variable set inline (blue / green /
 * cyan-teal). All buttons are sized for thumb taps. */
.welcome-host {
    /* v0.10.221 — lock the wizard to the visible viewport so no
     * page-level scrolling is possible. `dvh` reflects the current
     * visible area (URL bar collapses or not) on iOS/Android Chrome;
     * `vh` keeps the older browsers from breaking. The fallback
     * gradient is the boot-time look before --welcome-bg-url paints. */
    height: calc(100vh - var(--safe-top) - var(--safe-bot));
    height: calc(100dvh - var(--safe-top) - var(--safe-bot));
    max-height: calc(100vh - var(--safe-top) - var(--safe-bot));
    max-height: calc(100dvh - var(--safe-top) - var(--safe-bot));
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 12px;
    background:
        radial-gradient(ellipse at center top, rgba(200, 16, 46, 0.10) 0%, transparent 55%),
        radial-gradient(ellipse at center bottom, rgba(15, 17, 22, 0.85) 0%, transparent 70%),
        linear-gradient(180deg, var(--bg) 0%, var(--bg-elev) 100%);
}
.welcome-screen {
    margin: auto;
    max-height: 100%;
    width: 100%;
    max-width: 680px;
    padding: 32px 28px;
    background: rgba(13, 14, 18, 0.92);
    border: 1px solid rgba(200, 16, 46, 0.30);
    border-radius: 14px;
    box-shadow:
        0 18px 56px rgba(0, 0, 0, 0.55),
        0 0 0 1px rgba(255, 255, 255, 0.03) inset,
        0 0 28px rgba(200, 16, 46, 0.10);
    color: var(--text);
    text-align: center;
    position: relative;
    /* v0.10.221 — clip horizontally (so the border-radius works) but
     * allow internal vertical scroll as a fallback when content
     * would otherwise be clipped by the host's `overflow: hidden`.
     * Compact-mode rules below try to make every step fit without
     * needing this; the faction-detail card has its own flex-column
     * setup that pins hero + actions and only scrolls the middle. */
    overflow-x: hidden;
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;
}
/* Intro layout — centred aquila over a tall title + CTA stack. */
.welcome-intro { padding: 36px 28px 28px; }
.welcome-aquila {
    font-size: 88px;
    color: rgba(255, 200, 100, 0.65);
    line-height: 1;
    margin-bottom: 14px;
    filter: drop-shadow(0 4px 18px rgba(200, 16, 46, 0.35));
}
.welcome-aquila .wh40k { display: inline-block; }
.welcome-title {
    margin: 0 0 12px;
    font-size: clamp(28px, 6vw, 40px);
    font-weight: 900;
    letter-spacing: -0.01em;
    text-transform: uppercase;
    color: #fff;
    text-shadow: 0 2px 12px rgba(200, 16, 46, 0.35);
}
.welcome-subtitle {
    margin: 0 auto 28px;
    max-width: 540px;
    font-size: 14.5px;
    line-height: 1.55;
    color: var(--text-dim);
    font-style: italic;
}
.welcome-cta-row {
    display: flex;
    flex-direction: column;
    gap: 10px;
    align-items: center;
    margin: 4px auto 22px;
    max-width: 480px;
}
.welcome-cta {
    width: 100%;
    padding: 16px 22px;
    font-size: 15px;
    font-weight: 800;
    letter-spacing: 0.03em;
    text-transform: uppercase;
    border-radius: 10px;
    box-shadow: 0 8px 22px rgba(200, 16, 46, 0.30);
    animation: welcome-cta-pulse 3.6s ease-in-out infinite;
}
@keyframes welcome-cta-pulse {
    0%, 100% { box-shadow: 0 8px 22px rgba(200, 16, 46, 0.30); }
    50%      { box-shadow: 0 10px 28px rgba(200, 16, 46, 0.55); }
}
.welcome-skip {
    background: transparent;
    border: 1px solid var(--line);
    color: var(--text-dim);
    padding: 8px 14px;
    font-size: 12px;
    border-radius: 8px;
}
.welcome-skip:hover { color: var(--text); border-color: rgba(255,255,255,0.25); }
.welcome-disclaimer {
    margin: 22px auto 0;
    max-width: 560px;
    font-size: 11.5px;
    line-height: 1.55;
    color: var(--text-mute);
    padding-top: 14px;
    border-top: 1px solid var(--line);
}
.welcome-disclaimer b { color: var(--text-dim); font-weight: 700; }

/* Generic modal-style steps after the intro. */
.welcome-modal { padding: 28px 24px; }
.welcome-modal-icon {
    font-size: 48px;
    color: rgba(255, 200, 100, 0.70);
    margin-bottom: 6px;
    line-height: 1;
}
.welcome-modal-title {
    margin: 0 0 10px;
    font-size: clamp(20px, 4.2vw, 26px);
    font-weight: 900;
    letter-spacing: 0.01em;
    text-transform: uppercase;
    color: #fff;
    line-height: 1.2;
}
.welcome-modal-title-accent {
    color: var(--accent-soft);
    display: inline-block;
    margin-top: 4px;
}
.welcome-modal-body {
    margin: 0 auto 16px;
    max-width: 560px;
    font-size: 14px;
    line-height: 1.55;
    color: var(--text);
}
.welcome-modal-body.text-mute { color: var(--text-dim); }
.welcome-actions {
    display: flex;
    gap: 10px;
    justify-content: space-between;
    margin-top: 18px;
}
.welcome-back-btn {
    padding: 10px 16px;
    background: transparent;
    border: 1px solid var(--line);
    color: var(--text-dim);
}
.welcome-back-btn:hover { color: var(--text); border-color: rgba(255,255,255,0.25); }
.welcome-continue-btn { flex: 1; padding: 12px 18px; font-size: 14.5px; font-weight: 800; }

/* Yes / No tile grid for steps A + B. Side-by-side on wider
 * screens, stacked on narrow. */
.welcome-yesno-grid {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 12px;
    margin: 10px 0 6px;
}
.welcome-yesno-tile {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 8px;
    padding: 22px 16px;
    background: var(--bg-elev);
    border: 1px solid var(--line);
    border-radius: 10px;
    color: var(--text);
    cursor: pointer;
    transition: transform 0.12s ease, border-color 0.15s ease, background 0.15s ease, box-shadow 0.18s ease;
}
.welcome-yesno-tile:hover {
    transform: translateY(-2px);
    background: var(--bg-elev-2);
    border-color: rgba(255, 255, 255, 0.22);
}
.welcome-yesno-tile.is-yes:hover {
    border-color: rgba(95, 200, 110, 0.55);
    box-shadow: 0 6px 22px rgba(95, 200, 110, 0.18);
}
.welcome-yesno-tile.is-no:hover {
    border-color: rgba(200, 16, 46, 0.55);
    box-shadow: 0 6px 22px rgba(200, 16, 46, 0.18);
}
.welcome-yesno-mark {
    font-size: 28px;
    font-weight: 900;
    line-height: 1;
}
.welcome-yesno-tile.is-yes .welcome-yesno-mark { color: var(--green); }
.welcome-yesno-tile.is-no  .welcome-yesno-mark { color: var(--accent-soft); }
.welcome-yesno-label {
    font-size: 13.5px;
    font-weight: 700;
    line-height: 1.35;
    color: var(--text);
}
@media (max-width: 520px) {
    .welcome-yesno-grid { grid-template-columns: 1fr; }
}

/* Faction picker tiles. --ftu-accent + --ftu-accent-soft are set
 * inline so each card glows in its faction's signature colour. */
.welcome-faction-grid {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 12px;
    margin: 12px 0 6px;
}
.welcome-faction-tile {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 8px;
    padding: 18px 14px 16px;
    background: var(--ftu-accent-soft, var(--bg-elev));
    border: 1px solid var(--ftu-accent, var(--line));
    border-radius: 10px;
    color: var(--text);
    cursor: pointer;
    transition: transform 0.12s ease, box-shadow 0.18s ease, background 0.15s ease;
    box-shadow: 0 6px 22px rgba(0, 0, 0, 0.35);
}
.welcome-faction-tile:hover {
    transform: translateY(-3px);
    box-shadow:
        0 10px 28px rgba(0, 0, 0, 0.45),
        0 0 28px var(--ftu-accent, transparent);
}
.welcome-faction-logo {
    height: 56px;
    width: auto;
    max-width: 80px;
    object-fit: contain;
    /* Tint the white logo to the faction accent. We use a
     * brightness(0) → color filter chain so each logo picks up its
     * own --ftu-accent without needing per-faction filter strings. */
    filter: drop-shadow(0 2px 6px var(--ftu-accent, rgba(255,255,255,0.2)));
}
.welcome-faction-name {
    font-size: 16px;
    font-weight: 900;
    letter-spacing: 0.02em;
    text-transform: uppercase;
    color: #fff;
}
.welcome-faction-blurb {
    font-size: 12.5px;
    line-height: 1.45;
    color: var(--text-dim);
    min-height: 50px;
}
.welcome-faction-detachment {
    font-size: 11px;
    font-weight: 700;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--ftu-accent, var(--text-mute));
    padding-top: 6px;
    border-top: 1px solid rgba(255,255,255,0.08);
    width: 100%;
}
@media (max-width: 720px) {
    .welcome-faction-grid { grid-template-columns: 1fr; }
    .welcome-faction-blurb { min-height: 0; }
}

/* Aspect picker tiles (Army Building / PvP Matches). Inherits
 * --ftu-accent from the selected faction so the colour story
 * carries through the step. */
.welcome-aspect-faction {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 10px;
    margin-bottom: 6px;
    padding: 6px 12px;
    background: var(--ftu-accent-soft, transparent);
    border: 1px solid var(--ftu-accent, var(--line));
    border-radius: 999px;
    width: max-content;
    margin-left: auto;
    margin-right: auto;
}
.welcome-aspect-logo {
    height: 22px;
    width: auto;
    object-fit: contain;
}
.welcome-aspect-faction-name {
    font-size: 12.5px;
    font-weight: 800;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--ftu-accent, var(--text));
}
.welcome-aspect-grid {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 12px;
    margin: 12px 0 6px;
}
.welcome-aspect-tile {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 8px;
    padding: 22px 16px;
    background: var(--bg-elev);
    border: 1px solid var(--line);
    border-radius: 10px;
    color: var(--text);
    cursor: pointer;
    transition: transform 0.12s ease, border-color 0.15s ease, box-shadow 0.18s ease;
}
.welcome-aspect-tile:hover {
    transform: translateY(-3px);
    border-color: var(--ftu-accent, rgba(255, 255, 255, 0.22));
    box-shadow:
        0 10px 28px rgba(0, 0, 0, 0.45),
        0 0 22px var(--ftu-accent-soft, transparent);
}
.welcome-aspect-icon {
    font-size: 30px;
    line-height: 1;
    color: var(--ftu-accent, var(--accent-soft));
}
.welcome-aspect-name {
    font-size: 16px;
    font-weight: 900;
    letter-spacing: 0.02em;
    text-transform: uppercase;
    color: #fff;
}
.welcome-aspect-blurb {
    font-size: 12.5px;
    line-height: 1.45;
    color: var(--text-dim);
    text-align: center;
}
@media (max-width: 520px) {
    .welcome-aspect-grid { grid-template-columns: 1fr; }
}

/* ============================================================
 * v0.10.219 — faction detail card
 *
 * Sits between the faction picker and the aspect picker. Combat
 * Patrol art as a full-width hero strip at the top, faction emblem
 * + name overlaid on the lower-left, then two short text sections
 * (In the lore / On the tabletop) plus a flavour quote. The whole
 * card uses the --ftu-accent set inline by JS so each faction's
 * colour story carries through this screen too.
 * ============================================================ */
.welcome-faction-detail {
    padding: 0;
    overflow: hidden;
    /* v0.10.221 — flex column so the hero stays put up top, the
     * actions stay pinned at the bottom, and only the text content
     * scrolls internally if it doesn't fit. Page never scrolls. */
    display: flex;
    flex-direction: column;
    max-height: 100%;
}
.welcome-fd-hero { flex: 0 0 auto; }
.welcome-fd-content {
    flex: 1 1 auto;
    min-height: 0;            /* required for flex-child overflow */
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;
}
.welcome-faction-detail .welcome-actions { flex: 0 0 auto; }
.welcome-fd-hero {
    position: relative;
    width: 100%;
    /* 16:9 hero — tall enough to read the combat-patrol image, short
     * enough to leave room for the text on phone-height viewports. */
    aspect-ratio: 16 / 9;
    overflow: hidden;
    border-bottom: 1px solid var(--ftu-accent, var(--line));
    background: #08080c;
}
.welcome-fd-hero-img {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    object-position: center center;
}
.welcome-fd-hero-shade {
    position: absolute;
    inset: 0;
    background: linear-gradient(180deg,
        rgba(8, 8, 12, 0.05) 0%,
        rgba(8, 8, 12, 0.20) 55%,
        rgba(8, 8, 12, 0.85) 100%);
    pointer-events: none;
}
.welcome-fd-hero-caption {
    position: absolute;
    left: 14px;
    right: 14px;
    bottom: 10px;
    display: flex;
    align-items: center;
    gap: 12px;
    text-align: left;
}
.welcome-fd-emblem {
    height: 44px;
    width: auto;
    max-width: 56px;
    object-fit: contain;
    filter: drop-shadow(0 2px 8px rgba(0, 0, 0, 0.65));
}
.welcome-fd-hero-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.welcome-fd-name {
    font-size: 20px;
    font-weight: 900;
    letter-spacing: 0.02em;
    text-transform: uppercase;
    color: #fff;
    line-height: 1.1;
    text-shadow: 0 2px 10px rgba(0, 0, 0, 0.75);
}
.welcome-fd-detachment {
    font-size: 11px;
    font-weight: 700;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--ftu-accent, var(--text-mute));
    text-shadow: 0 1px 6px rgba(0, 0, 0, 0.8);
}

.welcome-fd-content {
    padding: 16px 20px 4px;
    text-align: left;
}
.welcome-fd-section { margin-bottom: 12px; }
.welcome-fd-section-h {
    font-size: 11px;
    font-weight: 800;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--ftu-accent, var(--accent-soft));
    margin-bottom: 4px;
}
.welcome-fd-section-body {
    margin: 0;
    font-size: 13.5px;
    line-height: 1.5;
    color: var(--text);
}
.welcome-fd-quote {
    margin: 14px 0 0;
    padding: 10px 12px;
    background: rgba(255, 255, 255, 0.03);
    border-left: 3px solid var(--ftu-accent, var(--accent-soft));
    border-radius: 4px;
}
.welcome-fd-quote-body {
    font-size: 13px;
    line-height: 1.5;
    font-style: italic;
    color: var(--text-dim);
}
.welcome-fd-quote-attrib {
    margin-top: 6px;
    font-size: 11.5px;
    font-weight: 700;
    letter-spacing: 0.04em;
    color: var(--ftu-accent, var(--text-mute));
}
.welcome-faction-detail .welcome-actions {
    padding: 8px 20px 18px;
    margin-top: 0;
}

/* ============================================================
 * v0.10.222 — Begin Mission screens (Solo + Co-op)
 *
 * Two new wizard steps after the Learn-Together pick:
 *   • beginMissionSolo: single "Begin The Mission" primary button
 *   • beginMissionDuo : Host / Join tile pair, mirroring the
 *     lobby's online-host / online-join visual language so the
 *     transition into the actual lobby feels continuous.
 * Both pick up the active faction's --ftu-accent so the colour
 * story carries through.
 * ============================================================ */
.welcome-bm-faction {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    margin-bottom: 8px;
    padding: 5px 12px;
    background: var(--ftu-accent-soft, transparent);
    border: 1px solid var(--ftu-accent, var(--line));
    border-radius: 999px;
    width: max-content;
    margin-left: auto;
    margin-right: auto;
}
.welcome-bm-logo { height: 20px; width: auto; object-fit: contain; }
.welcome-bm-faction-name {
    font-size: 12px;
    font-weight: 800;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--ftu-accent, var(--text));
}

.welcome-bm-solo-stack {
    display: flex;
    justify-content: center;
    margin: 18px 0 6px;
}
.welcome-bm-primary {
    display: inline-flex;
    align-items: center;
    gap: 12px;
    padding: 16px 28px;
    font-size: 16px;
    font-weight: 900;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    border-radius: 10px;
    box-shadow: 0 10px 28px rgba(200, 16, 46, 0.30);
    animation: welcome-cta-pulse 3.6s ease-in-out infinite;
}
.welcome-bm-primary-icon { font-size: 22px; line-height: 1; }

.welcome-bm-duo-grid {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 12px;
    margin: 14px 0 6px;
}
.welcome-bm-tile {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 8px;
    padding: 18px 14px;
    background: var(--bg-elev);
    border: 1px solid var(--line);
    border-radius: 10px;
    color: var(--text);
    cursor: pointer;
    transition: transform 0.12s ease, border-color 0.15s ease, box-shadow 0.18s ease;
}
.welcome-bm-tile:hover {
    transform: translateY(-3px);
    border-color: var(--ftu-accent, rgba(255, 255, 255, 0.22));
    box-shadow:
        0 10px 28px rgba(0, 0, 0, 0.45),
        0 0 22px var(--ftu-accent-soft, transparent);
}
.welcome-bm-tile-icon {
    height: 56px;
    width: auto;
    max-width: 80px;
    object-fit: contain;
    filter: drop-shadow(0 2px 8px rgba(0, 0, 0, 0.55));
}
.welcome-bm-tile-name {
    font-size: 14px;
    font-weight: 900;
    letter-spacing: 0.02em;
    text-transform: uppercase;
    color: #fff;
    text-align: center;
    line-height: 1.2;
}
.welcome-bm-tile-blurb {
    font-size: 12px;
    line-height: 1.4;
    color: var(--text-dim);
    text-align: center;
}

/* Compact-mode trims for the Begin Mission screens */
@media (max-width: 520px), (max-height: 760px) {
    .welcome-bm-faction { padding: 4px 10px; margin-bottom: 4px; }
    .welcome-bm-logo { height: 16px; }
    .welcome-bm-faction-name { font-size: 11px; }
    .welcome-bm-solo-stack { margin: 12px 0 4px; }
    .welcome-bm-primary { padding: 13px 20px; font-size: 14px; gap: 10px; }
    .welcome-bm-primary-icon { font-size: 18px; }
    .welcome-bm-duo-grid { gap: 8px; margin: 8px 0 4px; }
    .welcome-bm-tile { padding: 12px 10px; gap: 6px; }
    .welcome-bm-tile-icon { height: 40px; max-width: 56px; }
    .welcome-bm-tile-name { font-size: 12.5px; }
    .welcome-bm-tile-blurb { font-size: 11px; line-height: 1.3; }
}
@media (max-width: 359px) {
    .welcome-bm-duo-grid { grid-template-columns: 1fr; }
}

/* Compact-mode trims for the faction detail card on phones */
@media (max-width: 520px), (max-height: 760px) {
    .welcome-fd-hero { aspect-ratio: 16 / 7; }
    .welcome-fd-hero-caption { left: 10px; right: 10px; bottom: 6px; gap: 8px; }
    .welcome-fd-emblem { height: 32px; max-width: 42px; }
    .welcome-fd-name { font-size: 16px; }
    .welcome-fd-detachment { font-size: 9.5px; }
    .welcome-fd-content { padding: 10px 14px 2px; }
    .welcome-fd-section { margin-bottom: 8px; }
    .welcome-fd-section-h { font-size: 10px; margin-bottom: 2px; }
    .welcome-fd-section-body { font-size: 12px; line-height: 1.4; }
    .welcome-fd-quote { margin-top: 8px; padding: 8px 10px; }
    .welcome-fd-quote-body { font-size: 11.5px; line-height: 1.4; }
    .welcome-fd-quote-attrib { font-size: 10.5px; }
    .welcome-faction-detail .welcome-actions { padding: 6px 14px 12px; }
}

/* ============================================================
 * v0.10.219 — cinematic backdrop on every wizard step
 *
 * Each step paints a different image as a slow, alternating
 * horizontal pan. The image URL is driven by the inline CSS
 * variable `--welcome-bg-url` set in paint() — that way the CSS
 * doesn't need a `:has()` rule per step, and adding a new step
 * just needs an entry in the JS step→image map.
 *
 * Sizing: `background-size: auto 100%` scales each image to fill
 * the viewport height; its width grows to the image's native
 * aspect ratio. Panning `background-position-x` from 0% (left edge
 * flush) to 100% (right edge flush) shows the whole image over time.
 *
 * Dark overlay: a vertical gradient + soft radial darken preserves
 * text legibility on the white aquila / red CTAs without crushing
 * the art. The card itself is dropped to a translucent fill with a
 * backdrop blur so the image bleeds around it.
 * ============================================================ */
.welcome-host {
    padding: 0;                    /* let the image fill edge-to-edge */
    background-color: #0d0e12;     /* fallback while the image loads */
    position: relative;
    overflow: hidden;              /* no page-level scroll, ever */
    isolation: isolate;
}
/* Image layer — sized `cover` so the image ALWAYS fills the
 * viewport (parts spill off the overflowing side rather than
 * leaving black bars). The pan animation moves through whichever
 * dimension has slack: for a wide image on a portrait viewport
 * the height-axis is what overflows so visible movement is small;
 * for a wide image on a landscape viewport the width-axis has lots
 * of slack and the pan reads as a full L↔R sweep. Either way the
 * image is fully visible — no letterboxing. */
.welcome-host::before {
    content: "";
    position: absolute;
    inset: 0;
    z-index: 0;
    background-image: var(--welcome-bg-url, none);
    background-position: 0% center;
    background-size: cover;
    background-repeat: no-repeat;
    animation: welcome-bg-pan 60s ease-in-out infinite alternate;
}
/* Dark overlay — vertical gradient + soft centred radial darken. */
.welcome-host::after {
    content: "";
    position: absolute;
    inset: 0;
    z-index: 1;
    pointer-events: none;
    background:
        linear-gradient(180deg, rgba(8, 8, 12, 0.55) 0%, rgba(8, 8, 12, 0.80) 100%),
        radial-gradient(ellipse at center, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.55) 80%);
}
/* Card sits on top of both, translucent + backdrop blur. */
.welcome-host .welcome-screen {
    position: relative;
    z-index: 2;
    background: rgba(13, 14, 18, 0.62);
    backdrop-filter: blur(10px) saturate(120%);
    -webkit-backdrop-filter: blur(10px) saturate(120%);
    border-color: rgba(200, 16, 46, 0.45);
    box-shadow:
        0 22px 60px rgba(0, 0, 0, 0.75),
        0 0 0 1px rgba(255, 255, 255, 0.05) inset,
        0 0 36px rgba(200, 16, 46, 0.18);
}
@keyframes welcome-bg-pan {
    0%   { background-position: 0% center; }
    100% { background-position: 100% center; }
}
/* Honour reduced-motion — hold the image at centre. */
@media (prefers-reduced-motion: reduce) {
    .welcome-host::before { animation: none; background-position: 50% center; }
}

/* ============================================================
 * v0.10.216 — hide app chrome while on the FTU wizard route
 *
 * `body.route-welcome` is toggled in navigate() when the hash is
 * `#/welcome`. Hiding the topbar + bottombar gives the wizard the
 * full viewport — important on phones where the bars eat ~110px
 * of vertical space we need for fitting each step. The welcome
 * host already sizes itself to `100vh - topbar - bottombar`; the
 * matching zero-out below makes that calc resolve to the full
 * viewport so the wizard fills the page edge-to-edge.
 * ============================================================ */
body.route-welcome .topbar,
body.route-welcome .bottombar { display: none !important; }
body.route-welcome { --topbar-h: 0px; --bottombar-h: 0px; }
/* v0.10.218 — the wizard needs to span the full viewport width too.
 * `.screen` defaults to `max-width: 880px` + 16px side padding +
 * `margin: 0 auto`, which capped the image background at 880px on
 * wider monitors. Zero out all four edges on the welcome route so
 * the welcome-host can paint edge-to-edge. */
body.route-welcome #screen {
    padding: 0;
    max-width: none;
    margin: 0;
}

/* v0.10.225 — tutorial route hides the topbar + bottom navbar so the
 * lesson feels like an immersive standalone experience, not a tab
 * inside the app shell. v0.10.323 — DROPPED the `#screen { padding:0;
 * max-width:none }` override that originally paired with this. In
 * Phase 1 the tutorial route painted only a narrator banner over a
 * backdrop and an edge-to-edge canvas was the goal. Phase 2 mounts
 * the REAL match UI inside the same route via `useRealMatch`, and
 * that UI was authored expecting `#screen`'s normal horizontal
 * padding to provide the gutters. With `padding:0` the game header
 * faction / CP / VP cells butt up against the viewport edges and
 * labels clip ("TAKE AND HOLE" instead of "TAKE AND HOLD"). The
 * topbar / navbar hides stay — the chrome-free framing is still
 * desired — but #screen inherits the same padding as PvP/Solo so
 * the match panel reads correctly inside the tutorial. */
body.route-tutorial .topbar,
body.route-tutorial .bottombar { display: none !important; }
body.route-tutorial { --topbar-h: 0px; --bottombar-h: 0px; }

/* ============================================================
 * v0.10.225 — Tutorial Mission (Phase 1) — narrator + stage
 *
 * Two-row layout: the stage area on top (placeholder for the VTT
 * tabletop in Phase 2), narrator banner pinned to the bottom.
 * Banner is a translucent dark card with the speaker, body text,
 * and Skip / Next controls. Stage uses the ambient backdrop image
 * authored on the script (`ambientBgUrl`) with a slow pan
 * borrowed from the wizard idiom.
 * ============================================================ */
.tutorial-host {
    position: relative;
    width: 100%;
    height: calc(100vh - var(--safe-top) - var(--safe-bot));
    height: calc(100dvh - var(--safe-top) - var(--safe-bot));
    background-color: #08080c;
    overflow: hidden;
    isolation: isolate;
    display: flex;
    flex-direction: column;
}
.tutorial-host::before {
    content: "";
    position: absolute;
    inset: 0;
    z-index: 0;
    background-image: var(--tutorial-bg-url, none);
    background-position: 0% center;
    background-size: cover;
    background-repeat: no-repeat;
    animation: welcome-bg-pan 90s ease-in-out infinite alternate;
    opacity: 0.55;
}
.tutorial-host::after {
    content: "";
    position: absolute;
    inset: 0;
    z-index: 1;
    pointer-events: none;
    background:
        linear-gradient(180deg, rgba(8, 8, 12, 0.30) 0%, rgba(8, 8, 12, 0.85) 100%);
}
@media (prefers-reduced-motion: reduce) {
    .tutorial-host::before { animation: none; background-position: 50% center; }
}

/* Stage (Phase 1 placeholder). Takes the available space above the
 * narrator banner. Phase 2 will mount the VTT canvas in here. */
.tutorial-stage {
    position: relative;
    z-index: 2;
    flex: 1 1 auto;
    min-height: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 14px;
    padding: 32px 24px;
    text-align: center;
}
.tutorial-stage-watermark {
    font-size: 84px;
    color: rgba(255, 200, 100, 0.40);
    line-height: 1;
    filter: drop-shadow(0 4px 18px rgba(200, 16, 46, 0.30));
}
.tutorial-stage-label {
    font-size: 18px;
    font-weight: 900;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: #fff;
    text-shadow: 0 2px 10px rgba(0, 0, 0, 0.65);
}
.tutorial-stage-sub {
    font-size: 13px;
    color: var(--text-dim);
    max-width: 480px;
    font-style: italic;
}

/* ============================================================
 * v0.10.226 — Tutorial tabletop (Phase 2 stub)
 *
 * A simple 2D board: aspect ratio from the script's widthIn/heightIn,
 * subtle grid pattern, absolute-positioned tokens. Tokens transition
 * `left` / `top` on prop changes so a JS state mutation animates
 * smoothly without rebuilding the DOM. Token colour is per-side
 * with an inline `--tt-color` override for any per-faction accent.
 * Phase 4 will replace this with the full VTT canvas — the script
 * schema is forward-compatible so the same `tabletopAction.type =
 * "move-token"` will dispatch to the real Match action log.
 * ============================================================ */
.tutorial-tabletop {
    position: relative;
    width: 100%;
    max-width: 880px;
    max-height: 100%;
    margin: 0 auto;
    border: 1px solid rgba(200, 16, 46, 0.30);
    border-radius: 8px;
    background:
        /* Grid lines spaced at 1 tabletop unit (1") using a
         * percentage-driven repeating gradient. Subtle, doesn't
         * compete with tokens. */
        linear-gradient(rgba(255, 255, 255, 0.04) 1px, transparent 1px) 0 0 / calc(100% / var(--tt-w, 30)) 100%,
        linear-gradient(90deg, rgba(255, 255, 255, 0.04) 1px, transparent 1px) 0 0 / 100% calc(100% / var(--tt-h, 22)),
        radial-gradient(ellipse at center, rgba(40, 30, 30, 0.85) 0%, rgba(10, 10, 14, 0.90) 100%);
    overflow: hidden;
    box-shadow:
        0 16px 48px rgba(0, 0, 0, 0.55),
        0 0 0 1px rgba(255, 255, 255, 0.03) inset;
}
.tutorial-token {
    position: absolute;
    transform: translate(-50%, -50%);
    min-width: 60px;
    padding: 6px 8px 5px;
    background: linear-gradient(180deg, rgba(20, 20, 24, 0.92) 0%, rgba(10, 10, 14, 0.92) 100%);
    border: 2px solid var(--tt-color, #4a8be8);
    border-radius: 8px;
    color: #fff;
    text-align: center;
    box-shadow:
        0 4px 14px rgba(0, 0, 0, 0.65),
        0 0 18px rgba(0, 0, 0, 0.40),
        0 0 0 1px rgba(255, 255, 255, 0.04) inset;
    /* CSS transition is the actual animation engine for token
     * movements — Tutorial.next() mutates the position state, the
     * listener writes new left/top, the transition handles the
     * easing. Duration tuned for "deliberate" rather than instant. */
    transition: left 1.4s cubic-bezier(0.4, 0, 0.2, 1),
                top  1.4s cubic-bezier(0.4, 0, 0.2, 1),
                transform 0.2s ease;
    pointer-events: none; /* Phase 2: read-only display */
}
.tutorial-token-player {
    box-shadow:
        0 4px 14px rgba(0, 0, 0, 0.65),
        0 0 22px rgba(74, 139, 232, 0.30),
        0 0 0 1px rgba(255, 255, 255, 0.04) inset;
}
.tutorial-token-enemy {
    box-shadow:
        0 4px 14px rgba(0, 0, 0, 0.65),
        0 0 22px rgba(155, 89, 182, 0.35),
        0 0 0 1px rgba(255, 255, 255, 0.04) inset;
}
.tutorial-token-label {
    font-size: 11px;
    font-weight: 900;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    line-height: 1.1;
    color: var(--tt-color, #fff);
}
.tutorial-token-sub {
    font-size: 9.5px;
    line-height: 1.15;
    color: var(--text-dim);
    margin-top: 2px;
}

/* Stage area uses flex layout so the tabletop is centred. The
 * board grows to fill, capped by max-width / max-height so on
 * tall narrow viewports it scales down predictably. */
.tutorial-stage {
    overflow: hidden;
}

/* Reduced-motion preference disables the token-glide animation. */
@media (prefers-reduced-motion: reduce) {
    .tutorial-token { transition: none; }
}

/* ============================================================
 * v0.10.227 — Media cycle overlay (used by Step 2 of SM tutorial)
 *
 * Wide horizontal strip that sits over the top of the stage,
 * cycling through the PhaseTimeline_*.png images so the moving
 * "active phase" glow reads as a continuous animation. Two
 * stacked <img>s crossfade between cycle steps; only the one with
 * `.is-active` is fully opaque.
 * ============================================================ */
.tutorial-media-cycle {
    position: absolute;
    top: 12px;
    left: 50%;
    transform: translateX(-50%);
    width: min(720px, calc(100% - 24px));
    z-index: 4;
    pointer-events: none;
}
.tutorial-media-cycle-inner {
    position: relative;
    width: 100%;
    /* Native image is ~9:1 — match so we don't crop or stretch. */
    aspect-ratio: 9 / 1;
    background: rgba(8, 8, 12, 0.45);
    border: 1px solid rgba(200, 16, 46, 0.30);
    border-radius: 8px;
    padding: 6px 10px;
    box-shadow:
        0 6px 22px rgba(0, 0, 0, 0.55),
        0 0 0 1px rgba(255, 255, 255, 0.04) inset;
    backdrop-filter: blur(6px);
    -webkit-backdrop-filter: blur(6px);
}
.tutorial-media-img {
    position: absolute;
    inset: 6px 10px;
    width: calc(100% - 20px);
    height: calc(100% - 12px);
    object-fit: contain;
    opacity: 0;
    transition: opacity 600ms ease-in-out;
}
.tutorial-media-img.is-active { opacity: 1; }
@media (max-width: 520px), (max-height: 760px) {
    .tutorial-media-cycle { top: 8px; width: calc(100% - 16px); }
    .tutorial-media-cycle-inner { padding: 4px 8px; aspect-ratio: 8 / 1; }
    .tutorial-media-img { inset: 4px 8px; width: calc(100% - 16px); height: calc(100% - 8px); }
}
@media (prefers-reduced-motion: reduce) {
    .tutorial-media-img { transition: none; }
}

/* ============================================================
 * v0.10.229 — Tutorial audio controls (top-right of host)
 *
 * Discrete two-button toolbar: replay narration + mute toggle.
 * Floats above the stage so it stays accessible without competing
 * with the narrator banner for visual weight. Buttons are circular
 * with translucent backgrounds so they sit comfortably over both
 * dark backdrops and the tabletop.
 * ============================================================ */
.tutorial-controls {
    position: absolute;
    top: 12px;
    right: 12px;
    display: flex;
    gap: 8px;
    z-index: 5;
}
.tutorial-ctrl-btn {
    width: 36px;
    height: 36px;
    border-radius: 50%;
    border: 1px solid rgba(255, 255, 255, 0.18);
    background: rgba(13, 14, 18, 0.62);
    color: #fff;
    font-size: 17px;
    line-height: 1;
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    backdrop-filter: blur(8px);
    -webkit-backdrop-filter: blur(8px);
    transition: background 0.15s ease, border-color 0.15s ease, transform 0.12s ease;
}
.tutorial-ctrl-btn:hover {
    background: rgba(30, 30, 38, 0.78);
    border-color: rgba(255, 255, 255, 0.30);
    transform: translateY(-1px);
}
.tutorial-ctrl-btn:active { transform: translateY(0); }
.tutorial-ctrl-btn.is-muted {
    border-color: rgba(200, 16, 46, 0.55);
    background: rgba(40, 14, 18, 0.72);
}
@media (max-width: 520px), (max-height: 760px) {
    .tutorial-controls { top: 8px; right: 8px; gap: 6px; }
    .tutorial-ctrl-btn { width: 32px; height: 32px; font-size: 15px; }
}

/* ============================================================
 * v0.10.227 — Tabletop border pulse (used by Step 3 of SM tutorial)
 *
 * Triggered by adding `.is-pulsing` to `.tutorial-tabletop` in
 * paintStep(). Runs the keyframe a fixed number of times then
 * settles — pulsing indefinitely would distract from the rest of
 * the page. Class is removed and re-added on re-entry so the
 * pulse re-fires (Back nav into the same step etc.).
 * ============================================================ */
@keyframes tutorial-tabletop-pulse {
    0%   { border-color: rgba(200, 16, 46, 0.30); box-shadow: 0 16px 48px rgba(0, 0, 0, 0.55), 0 0 0 1px rgba(255, 255, 255, 0.03) inset; }
    40%  { border-color: rgba(255, 80, 100, 1);   box-shadow: 0 16px 48px rgba(0, 0, 0, 0.55), 0 0 0 1px rgba(255, 255, 255, 0.03) inset, 0 0 32px 4px rgba(200, 16, 46, 0.55); }
    100% { border-color: rgba(200, 16, 46, 0.30); box-shadow: 0 16px 48px rgba(0, 0, 0, 0.55), 0 0 0 1px rgba(255, 255, 255, 0.03) inset; }
}
.tutorial-tabletop.is-pulsing {
    animation: tutorial-tabletop-pulse 1100ms ease-in-out 3;
}
@media (prefers-reduced-motion: reduce) {
    .tutorial-tabletop.is-pulsing { animation: none; }
}

/* Compact-mode trims — shrink tokens on phones so they don't
 * overlap on the small board. */
@media (max-width: 520px), (max-height: 760px) {
    .tutorial-token { min-width: 48px; padding: 4px 6px 3px; }
    .tutorial-token-label { font-size: 9.5px; }
    .tutorial-token-sub { font-size: 8.5px; }
}

/* ============================================================
 * v0.10.242 — TutorialOverlay (2B-infra) — narration + rules
 *
 * Body-attached overlay that paints on TOP of the live match
 * UI while Tutorial is active. Two layered card types:
 *
 *   .tutorial-narration-card — Layer A, modal. Bottom-centred
 *     glass card with a dim backdrop. Hadrian's voice. Blocks
 *     interaction with the match until "Continue →" pressed.
 *
 *   .tutorial-rules-card — Layer B, non-modal. Top-right side
 *     card. Mechanical explanations. Player can read while
 *     still interacting with the match canvas.
 *
 * Skip-Tutorial control floats top-left, low-key.
 * ============================================================ */
.tutorial-overlay-root {
    position: fixed;
    inset: 0;
    z-index: 9000;
    pointer-events: none;   /* children that need clicks re-enable */
}

/* ---- Narration card (non-modal — keeps battlefield visible) ----
 * v0.10.246 — removed the backdrop dim + blur and made the wrap
 * itself pointer-events: none so the player can see AND interact
 * with the battlefield behind the card while Hadrian narrates.
 * The card itself stays clickable so Continue/Skip/audio controls
 * still work. Subtle top-edge gradient gives a sense of "narration
 * is happening" without obscuring the VTT. */
.tutorial-narration-wrap {
    position: fixed;
    inset: 0;
    background: linear-gradient(180deg, transparent 0%, transparent 60%, rgba(0, 0, 0, 0.30) 100%);
    display: flex;
    align-items: flex-end;
    justify-content: center;
    padding: 24px 16px calc(24px + var(--safe-bot));
    pointer-events: none;
    animation: tut-narr-fade-in 240ms ease-out both;
}
.tutorial-narration-card { pointer-events: auto; }
@keyframes tut-narr-fade-in {
    from { opacity: 0; }
    to   { opacity: 1; }
}
.tutorial-narration-card {
    width: 100%;
    max-width: 720px;
    background: rgba(13, 14, 18, 0.92);
    border: 1px solid rgba(200, 16, 46, 0.55);
    border-radius: 14px;
    box-shadow:
        0 22px 60px rgba(0, 0, 0, 0.75),
        0 0 0 1px rgba(255, 255, 255, 0.04) inset,
        0 0 36px rgba(200, 16, 46, 0.20);
    color: var(--text);
    padding: 16px 20px;
    backdrop-filter: blur(10px) saturate(120%);
    -webkit-backdrop-filter: blur(10px) saturate(120%);
    animation: tut-narr-slide-up 280ms cubic-bezier(0.2, 0.8, 0.2, 1) both;
}
@keyframes tut-narr-slide-up {
    from { transform: translateY(16px); opacity: 0; }
    to   { transform: translateY(0);    opacity: 1; }
}
.tut-narr-head {
    display: flex;
    align-items: center;
    gap: 12px;
    margin-bottom: 12px;
    padding-bottom: 12px;
    border-bottom: 1px solid rgba(255, 255, 255, 0.06);
}
.tut-narr-portrait {
    flex: 0 0 auto;
    width: 44px;
    height: 44px;
    border-radius: 50%;
    background: linear-gradient(135deg, #2a1d1d 0%, #3a1010 100%);
    border: 1px solid rgba(200, 16, 46, 0.55);
    display: flex;
    align-items: center;
    justify-content: center;
    color: rgba(255, 200, 100, 0.85);
    font-size: 24px;
    line-height: 1;
    overflow: hidden;       /* v0.10.252 — clip the Hadrian portrait img to the circle */
}
.tut-narr-portrait img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    object-position: center 20%;   /* favour the face — head sits high in the source PNG */
}
.tut-narr-headtext { flex: 1 1 auto; display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.tut-narr-speaker {
    font-size: 14px;
    font-weight: 900;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: #fff;
}
.tut-narr-subtitle {
    font-size: 10.5px;
    font-weight: 700;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--accent-soft);
}
.tut-narr-audio-ctrls { display: flex; gap: 6px; }
.tut-narr-replay-btn,
.tut-narr-mute-btn {
    width: 32px;
    height: 32px;
    border-radius: 50%;
    border: 1px solid rgba(255, 255, 255, 0.18);
    background: rgba(8, 8, 12, 0.55);
    color: #fff;
    font-size: 15px;
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    transition: background 0.15s ease, border-color 0.15s ease;
}
.tut-narr-replay-btn:hover,
.tut-narr-mute-btn:hover {
    background: rgba(40, 40, 50, 0.75);
    border-color: rgba(255, 255, 255, 0.32);
}
.tut-narr-mute-btn.is-muted {
    border-color: rgba(200, 16, 46, 0.55);
    background: rgba(40, 14, 18, 0.72);
}
.tut-narr-body {
    font-size: 14.5px;
    line-height: 1.6;
    color: var(--text);
    margin-bottom: 14px;
}
.tut-narr-body b { color: #fff; }
.tut-narr-body i { color: var(--text-dim); }
.tut-narr-actions {
    display: flex;
    justify-content: flex-end;
    gap: 10px;
}
.tut-narr-continue-btn {
    padding: 10px 22px;
    font-size: 14px;
    font-weight: 800;
    letter-spacing: 0.04em;
}

/* ---- Rules card (focal modal — dims the screen) ----
 * v0.10.248 — promoted from "non-modal side card" to a true
 * focal modal: a full-screen dim backdrop graye the match UI
 * behind the card so the Battle Lore is the unambiguous focus
 * (player explicitly asked for this in v0.10.247 review). The
 * card centres in the viewport; backdrop blocks pointer events
 * so the VTT can't be interacted with until "Understood". */
.tutorial-rules-wrap {
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, 0.62);
    backdrop-filter: blur(3px) saturate(120%);
    -webkit-backdrop-filter: blur(3px) saturate(120%);
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 24px 16px;
    pointer-events: auto;   /* modal — eats clicks meant for the VTT */
    animation: tut-rules-fade-in 220ms ease-out both;
}
@keyframes tut-rules-fade-in {
    from { opacity: 0; }
    to   { opacity: 1; }
}
.tutorial-rules-card {
    position: relative;
    width: 100%;
    max-width: 460px;
    background: rgba(13, 14, 18, 0.96);
    border: 1px solid rgba(110, 200, 230, 0.55);   /* cyan-teal — distinct from narration's red */
    border-radius: 14px;
    box-shadow:
        0 22px 60px rgba(0, 0, 0, 0.75),
        0 0 36px rgba(110, 200, 230, 0.18),
        0 0 0 1px rgba(255, 255, 255, 0.04) inset;
    padding: 18px 22px;
    color: var(--text);
    pointer-events: auto;
    animation: tut-rules-pop-in 320ms cubic-bezier(0.2, 0.8, 0.2, 1) both;
}
@keyframes tut-rules-pop-in {
    from { transform: scale(0.94); opacity: 0; }
    to   { transform: scale(1);    opacity: 1; }
}
.tut-rules-head {
    margin-bottom: 8px;
    padding-bottom: 8px;
    border-bottom: 1px solid rgba(255, 255, 255, 0.06);
}
.tut-rules-tag {
    font-size: 10px;
    font-weight: 800;
    letter-spacing: 0.16em;
    text-transform: uppercase;
    color: rgba(110, 200, 230, 0.85);
}
.tut-rules-headline {
    font-size: 14.5px;
    font-weight: 900;
    letter-spacing: 0.02em;
    text-transform: uppercase;
    color: #fff;
    margin-top: 3px;
    line-height: 1.2;
}
.tut-rules-body {
    font-size: 13px;
    line-height: 1.55;
    color: var(--text);
    margin-bottom: 12px;
}
.tut-rules-body b { color: #fff; }
.tut-rules-body i { color: var(--text-dim); }
/* v0.10.325 — Worked weapon-profile example. Embedded in the
 * "Reading a Weapon Profile" Battle Lore card so the abstract
 * R/A/BS/S/AP/D + keywords vocabulary below has a concrete
 * artefact to point at. Sits above the prose, separated by a
 * thin divider; mirrors the shoot-wizard's chip-strip visual
 * vocabulary so the player recognises the same shapes when
 * they open the real wizard. */
.tut-weapon-example {
    margin: 4px 0 12px;
    padding: 10px 0 12px;
    border-bottom: 1px solid rgba(255, 255, 255, 0.06);
}
.tut-weapon-example-head {
    display: flex;
    align-items: baseline;
    flex-wrap: wrap;
    gap: 8px;
    margin-bottom: 8px;
}
.tut-weapon-example-name {
    color: #fff;
    font-size: 14px;
    font-weight: 700;
    letter-spacing: 0.01em;
}
.tut-weapon-example-name b { color: #fff; font-weight: 700; }
.tut-weapon-example-name i {
    color: var(--text-dim);
    font-style: italic;
    font-weight: 500;
}
.tut-weapon-example-bearer {
    color: var(--text-dim);
    font-size: 12px;
    font-weight: 500;
    flex: 1 1 auto;
    min-width: 0;
}
.tut-weapon-example-info {
    color: rgba(110, 200, 230, 0.95);
    font-size: 15px;
    line-height: 1;
    margin-left: auto;
    flex: 0 0 auto;
}
.tut-weapon-example-stats {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    margin-bottom: 8px;
}
.tut-weapon-example-chip {
    padding: 3px 9px;
    border-radius: 999px;
    border: 1px solid rgba(255, 255, 255, 0.14);
    background: rgba(255, 255, 255, 0.03);
    color: var(--text);
    font-size: 12px;
    font-weight: 600;
    font-variant-numeric: tabular-nums;
    letter-spacing: 0.01em;
    white-space: nowrap;
}
.tut-weapon-example-keywords {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
}
.tut-weapon-example-keyword {
    padding: 3px 11px;
    border-radius: 999px;
    border: 1px solid rgba(140, 110, 220, 0.55);
    background: rgba(110, 80, 200, 0.22);
    color: #d6cffa;
    font-size: 11px;
    font-weight: 700;
    letter-spacing: 0.06em;
    text-transform: uppercase;
}
.tut-weapon-example-prose {
    margin: 0;
}
.tut-rules-actions {
    display: flex;
    justify-content: flex-end;
}
.tut-rules-ack-btn {
    padding: 7px 14px;
    font-size: 12.5px;
    font-weight: 800;
    letter-spacing: 0.04em;
}

/* ---- Coachmark (spotlight + tooltip) — v0.10.324 ----
 * Targeted teach-the-controls beat. Dims the viewport everywhere
 * except a single highlighted UI element (the deploy cycler in
 * Phase 2 — extensible to anywhere `targetSelector` is wired). A
 * floating bubble points at the target with a short instruction.
 * Tap-anywhere dismissal is wired in JS.
 *
 * The wrap is `pointer-events: none` so the dim layer doesn't
 * intercept clicks (the JS-level pointerdown listener captures
 * dismissal); the highlight ring is purely decorative.
 *
 * The spotlight uses a `box-shadow` with 9999px spread to fill
 * the rest of the viewport with dim — far cheaper than a clip-path
 * mask + cross-browser without needing the SVG fallback. */
.tutorial-coach-wrap {
    position: fixed;
    inset: 0;
    pointer-events: none;
    z-index: 50;       /* above match-panel + topbar, below modals */
    animation: tut-coach-fade-in 220ms ease-out both;
}
/* v0.10.331 — While a coachmark is active, lock body scroll + dim
 * the bg-mode chevron / pill-label so the player can't gesture
 * the target off-screen. The JS-side gesture handlers
 * (touchmove / wheel / keydown / chevron-click) also guard on
 * this class — CSS is the visual + first-line-of-defence layer. */
html:has(body.coachmark-active),
body.coachmark-active {
    overflow: hidden !important;
    touch-action: none;
    overscroll-behavior: contain;
}
body.coachmark-active .match-expand-chevron,
body.coachmark-active .tabletop-collapsed-pill-label {
    pointer-events: none;
    opacity: 0.45;
}
@keyframes tut-coach-fade-in {
    from { opacity: 0; }
    to   { opacity: 1; }
}
.tutorial-coach-spotlight {
    position: fixed;
    border-radius: 10px;
    box-shadow:
        0 0 0 9999px rgba(0, 0, 0, 0.62),
        0 0 0 2px rgba(110, 200, 230, 0.95),
        0 0 18px 4px rgba(110, 200, 230, 0.45);
    pointer-events: none;
    transition: left 0.18s ease, top 0.18s ease,
                width 0.18s ease, height 0.18s ease;
}
.tutorial-coach-tooltip {
    position: fixed;
    background: rgba(13, 14, 18, 0.96);
    border: 1px solid rgba(110, 200, 230, 0.55);
    border-radius: 12px;
    padding: 12px 14px;
    box-shadow:
        0 18px 44px rgba(0, 0, 0, 0.7),
        0 0 24px rgba(110, 200, 230, 0.16);
    pointer-events: none;
    animation: tut-coach-pop-in 320ms cubic-bezier(0.2, 0.8, 0.2, 1) both;
}
@keyframes tut-coach-pop-in {
    from { transform: translateY(4px) scale(0.97); opacity: 0; }
    to   { transform: translateY(0)   scale(1);    opacity: 1; }
}
.tutorial-coach-body {
    color: var(--text);
    font-size: 12.5px;
    line-height: 1.5;
}
.tutorial-coach-body b { color: #fff; }
/* Tail/arrow indicator pointing at the spotlighted target.
 * .is-above sits above the target — arrow points DOWN.
 * .is-below sits below the target — arrow points UP. */
.tutorial-coach-tooltip::after {
    content: "";
    position: absolute;
    left: 50%;
    transform: translateX(-50%);
    width: 0;
    height: 0;
    border-left: 8px solid transparent;
    border-right: 8px solid transparent;
}
.tutorial-coach-tooltip.is-above::after {
    bottom: -8px;
    border-top: 8px solid rgba(13, 14, 18, 0.96);
}
.tutorial-coach-tooltip.is-below::after {
    top: -8px;
    border-bottom: 8px solid rgba(13, 14, 18, 0.96);
}
@media (max-width: 520px), (max-height: 760px) {
    .tutorial-coach-tooltip { padding: 10px 12px; }
    .tutorial-coach-body { font-size: 12px; }
}
@media (prefers-reduced-motion: reduce) {
    .tutorial-coach-wrap,
    .tutorial-coach-tooltip { animation: none; }
    .tutorial-coach-spotlight { transition: none; }
}

/* ---- Overlay controls (Skip-Tutorial, bottom-left) ----
 * v0.10.245 — moved out of the top-left where it overlapped the
 * match-screen round counter. Bottom-left clears the round/phase/
 * faction header AND the match's right-edge Reset View button.
 * Lifted ~96px above the bottom edge to clear the match's own
 * bottom action bar (Next Phase / Strat buttons live there). */
.tutorial-overlay-controls {
    position: fixed;
    bottom: calc(96px + var(--safe-bot));
    left: 12px;
    pointer-events: auto;
}
.tut-overlay-skip-btn {
    padding: 6px 12px;
    border: 1px solid rgba(255, 255, 255, 0.18);
    background: rgba(13, 14, 18, 0.72);
    color: var(--text-dim);
    font-size: 11.5px;
    font-weight: 700;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    border-radius: 6px;
    cursor: pointer;
    backdrop-filter: blur(6px);
    -webkit-backdrop-filter: blur(6px);
    transition: color 0.15s ease, border-color 0.15s ease, background 0.15s ease;
}
.tut-overlay-skip-btn:hover {
    color: var(--text);
    border-color: rgba(255, 255, 255, 0.32);
    background: rgba(30, 30, 38, 0.85);
}

/* Compact-mode trims */
@media (max-width: 520px), (max-height: 760px) {
    .tutorial-narration-wrap { padding: 12px 10px calc(12px + var(--safe-bot)); }
    .tutorial-narration-card { padding: 12px 14px; }
    .tut-narr-portrait { width: 36px; height: 36px; font-size: 20px; }
    .tut-narr-speaker { font-size: 12.5px; }
    .tut-narr-subtitle { font-size: 9.5px; }
    .tut-narr-body { font-size: 13px; line-height: 1.5; }
    .tut-narr-continue-btn { padding: 8px 16px; font-size: 13px; }
    .tutorial-rules-wrap { padding: 12px 10px; }
    .tutorial-rules-card { max-width: 380px; padding: 14px 16px; }
    .tut-rules-headline { font-size: 13px; }
    .tut-rules-body { font-size: 12px; line-height: 1.5; }
    .tutorial-overlay-controls { bottom: calc(80px + var(--safe-bot)); left: 8px; }
    .tut-overlay-skip-btn { padding: 5px 10px; font-size: 10.5px; }
}

@media (prefers-reduced-motion: reduce) {
    .tutorial-narration-wrap,
    .tutorial-narration-card,
    .tutorial-rules-wrap,
    .tutorial-rules-card { animation: none; }
}

/* Narrator banner pinned to the bottom of the host. */
.tutorial-banner {
    position: relative;
    z-index: 3;
    flex: 0 0 auto;
    padding: 12px 16px calc(12px + var(--safe-bot));
    background: linear-gradient(180deg, transparent 0%, rgba(8, 8, 12, 0.78) 30%);
}
.tutorial-banner-inner {
    max-width: 760px;
    margin: 0 auto;
    background: rgba(13, 14, 18, 0.86);
    border: 1px solid rgba(200, 16, 46, 0.45);
    border-radius: 14px;
    box-shadow:
        0 18px 56px rgba(0, 0, 0, 0.55),
        0 0 0 1px rgba(255, 255, 255, 0.04) inset,
        0 0 28px rgba(200, 16, 46, 0.12);
    padding: 14px 18px;
    backdrop-filter: blur(8px) saturate(120%);
    -webkit-backdrop-filter: blur(8px) saturate(120%);
}
.tutorial-banner-head {
    display: flex;
    align-items: center;
    gap: 12px;
    margin-bottom: 10px;
    padding-bottom: 10px;
    border-bottom: 1px solid rgba(255, 255, 255, 0.06);
}
.tutorial-banner-portrait {
    flex: 0 0 auto;
    width: 40px;
    height: 40px;
    border-radius: 50%;
    background: linear-gradient(135deg, #2a1d1d 0%, #3a1010 100%);
    border: 1px solid rgba(200, 16, 46, 0.55);
    display: flex;
    align-items: center;
    justify-content: center;
    color: rgba(255, 200, 100, 0.80);
    font-size: 22px;
    line-height: 1;
}
.tutorial-banner-headtext { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.tutorial-banner-speaker {
    font-size: 13.5px;
    font-weight: 900;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: #fff;
}
.tutorial-banner-progress {
    font-size: 11px;
    font-weight: 700;
    letter-spacing: 0.10em;
    text-transform: uppercase;
    color: var(--accent-soft);
}
.tutorial-banner-body {
    font-size: 14.5px;
    line-height: 1.55;
    color: var(--text);
    margin-bottom: 14px;
}
.tutorial-banner-body b { color: #fff; }
.tutorial-banner-body i { color: var(--text-dim); }
.tutorial-banner-actions {
    display: flex;
    justify-content: space-between;
    gap: 10px;
}
.tutorial-skip-btn {
    background: transparent;
    border: 1px solid var(--line);
    color: var(--text-dim);
    padding: 8px 14px;
    font-size: 12.5px;
}
.tutorial-skip-btn:hover { color: var(--text); border-color: rgba(255, 255, 255, 0.25); }
.tutorial-next-btn {
    padding: 10px 22px;
    font-size: 14px;
    font-weight: 800;
    letter-spacing: 0.04em;
}

/* Compact-mode trims — same breakpoint as the wizard. */
@media (max-width: 520px), (max-height: 760px) {
    .tutorial-stage { padding: 18px 16px; gap: 10px; }
    .tutorial-stage-watermark { font-size: 56px; }
    .tutorial-stage-label { font-size: 14.5px; letter-spacing: 0.06em; }
    .tutorial-stage-sub { font-size: 12px; }
    .tutorial-banner { padding: 8px 10px calc(8px + var(--safe-bot)); }
    .tutorial-banner-inner { padding: 12px 14px; border-radius: 12px; }
    .tutorial-banner-head { margin-bottom: 8px; padding-bottom: 8px; gap: 10px; }
    .tutorial-banner-portrait { width: 34px; height: 34px; font-size: 18px; }
    .tutorial-banner-speaker { font-size: 12.5px; }
    .tutorial-banner-progress { font-size: 10px; }
    .tutorial-banner-body { font-size: 13px; line-height: 1.5; margin-bottom: 10px; }
    .tutorial-skip-btn { padding: 7px 11px; font-size: 11.5px; }
    .tutorial-next-btn { padding: 9px 16px; font-size: 13px; }
}

/* ============================================================
 * v0.10.213 — compact welcome wizard for phones
 *
 * Targets any portrait phone (narrow OR short viewport). On iPhone
 * Chrome the usable area after URL bar + topbar + bottombar is
 * roughly 450–650px tall, which the spacious default layout
 * overflowed on every step. This block tightens every dimension so
 * each step fits without vertical scroll, while keeping the visual
 * hierarchy and tap-target sizes acceptable. It also overrides the
 * earlier single-column stacking on the yes/no, faction, and
 * aspect grids — stacking three tiles is what made the faction
 * picker overflow the worst; staying 3-up wide + shorter wins on
 * total height. Uses (max-width OR max-height) so it catches both
 * narrow phones and stubby landscape viewports.
 * ============================================================ */
@media (max-width: 520px), (max-height: 760px) {
    .welcome-host { padding: 8px; }
    .welcome-screen { padding: 18px 16px; }
    .welcome-intro { padding: 18px 16px 14px; }

    /* Intro aquila + headline */
    .welcome-aquila {
        font-size: 56px;
        margin-bottom: 6px;
        filter: drop-shadow(0 3px 12px rgba(200, 16, 46, 0.30));
    }
    .welcome-title {
        font-size: clamp(22px, 6vw, 30px);
        margin-bottom: 8px;
    }
    .welcome-subtitle {
        font-size: 13px;
        line-height: 1.5;
        margin-bottom: 14px;
    }
    .welcome-cta-row { gap: 8px; margin: 0 auto 10px; }
    .welcome-cta {
        padding: 13px 18px;
        font-size: 13.5px;
        letter-spacing: 0.02em;
    }
    .welcome-skip { padding: 6px 12px; font-size: 11.5px; }
    .welcome-disclaimer {
        margin-top: 12px;
        padding-top: 10px;
        font-size: 10.5px;
        line-height: 1.45;
        /* Tighten further by allowing 3 lines max; cuts ~30px on the
         * smallest phones. The legal disclaimer is preserved verbatim
         * elsewhere (About screen) — this is the FTU-screen footer. */
    }

    /* Generic modal steps */
    .welcome-modal { padding: 18px 16px; }
    .welcome-modal-icon { font-size: 36px; margin-bottom: 4px; }
    .welcome-modal-title {
        font-size: clamp(18px, 4.4vw, 22px);
        margin-bottom: 8px;
        line-height: 1.18;
    }
    .welcome-modal-body {
        font-size: 13px;
        line-height: 1.5;
        margin-bottom: 10px;
    }
    .welcome-actions { margin-top: 12px; gap: 8px; }
    .welcome-back-btn { padding: 8px 12px; font-size: 12.5px; }
    .welcome-continue-btn { padding: 10px 14px; font-size: 13.5px; }

    /* Yes/No — keep side-by-side (overrides the earlier 1fr rule).
     * Two tiles wide read fine at 375px and saves ~80px vs stacked. */
    .welcome-yesno-grid {
        grid-template-columns: 1fr 1fr;
        gap: 8px;
        margin: 6px 0 4px;
    }
    .welcome-yesno-tile { padding: 14px 10px; gap: 6px; }
    .welcome-yesno-mark { font-size: 22px; }
    .welcome-yesno-label { font-size: 12.5px; }

    /* Faction picker — keep 3 columns (overrides max-width:720px
     * stacking). Three stacked tiles would eat ~450px; three
     * compact columns fit in one ~130px row. */
    .welcome-faction-grid {
        grid-template-columns: repeat(3, 1fr);
        gap: 8px;
        margin: 6px 0 4px;
    }
    .welcome-faction-tile { padding: 10px 6px 10px; gap: 5px; }
    .welcome-faction-logo { height: 36px; max-width: 56px; }
    .welcome-faction-name { font-size: 12px; letter-spacing: 0.01em; }
    .welcome-faction-blurb {
        font-size: 10.5px;
        line-height: 1.3;
        min-height: 0;
    }
    .welcome-faction-detachment {
        font-size: 9.5px;
        letter-spacing: 0.04em;
        padding-top: 4px;
    }

    /* Aspect / Learn-Together — side-by-side (overrides 1fr) but
     * with compact pad + tighter blurbs so two tiles + back button
     * still clear the viewport. */
    .welcome-aspect-faction { padding: 4px 10px; margin-bottom: 4px; }
    .welcome-aspect-logo { height: 18px; }
    .welcome-aspect-faction-name { font-size: 11.5px; }
    .welcome-aspect-grid {
        grid-template-columns: 1fr 1fr;
        gap: 8px;
        margin: 6px 0 4px;
    }
    .welcome-aspect-tile { padding: 14px 10px; gap: 6px; }
    .welcome-aspect-icon { font-size: 24px; }
    .welcome-aspect-name { font-size: 13px; }
    .welcome-aspect-blurb { font-size: 11px; line-height: 1.35; }
}

/* Very narrow phones (<360px, e.g. iPhone SE 1st gen in portrait
 * with high zoom) — drop the faction grid to 1 column since
 * 105px-wide tiles get unreadable. Other steps already fit. */
@media (max-width: 359px) {
    .welcome-faction-grid { grid-template-columns: 1fr; }
    .welcome-faction-blurb { display: none; }
    .welcome-faction-tile { padding: 10px 12px; flex-direction: row; gap: 10px; }
    .welcome-faction-logo { height: 30px; }
}

/* Safety net — if a step somehow still overflows on an unusual
 * viewport (browser dev-tools at odd sizes, split-screen tablets),
 * allow the host to scroll rather than clipping content. The goal
 * remains "fits without scrolling on common phones"; this is just
 * a graceful fallback. */
/* v0.10.221 — the wizard host never page-scrolls (locked above).
 * The faction-detail card is the only step long enough to need
 * inner scrolling; it manages that itself via flex column +
 * `overflow-y: auto` on its content section. */

/* ============================================================
 * v0.10.213 — first-launch animation
 *
 * Applied via `.welcome-intro.is-launching` on the first intro
 * paint of a fresh wizard session. Sequence:
 *   t=0    aquila scales/rotates in, background red glow blooms
 *   t=600  aquila lands with a 120ms "stamp" pulse
 *   t=600  title fades up
 *   t=850  subtitle fades up
 *   t=1100 CTA stack fades up
 * Subsequent paints (Back navigation, replay) don't get the class
 * so the user isn't re-animated on every transition.
 * ============================================================ */
.welcome-intro.is-launching .welcome-aquila {
    animation:
        welcome-aquila-enter 800ms cubic-bezier(0.16, 1, 0.3, 1) both,
        welcome-aquila-stamp 120ms ease-out 700ms both;
}
.welcome-intro.is-launching::before {
    content: "";
    position: absolute;
    inset: 0;
    border-radius: inherit;
    pointer-events: none;
    background: radial-gradient(circle at 50% 30%, rgba(200, 16, 46, 0.55), transparent 55%);
    opacity: 0;
    animation: welcome-glow-bloom 1100ms ease-out both;
    z-index: 0;
}
.welcome-intro.is-launching .welcome-aquila,
.welcome-intro.is-launching .welcome-title,
.welcome-intro.is-launching .welcome-subtitle,
.welcome-intro.is-launching .welcome-cta-row,
.welcome-intro.is-launching .welcome-disclaimer {
    position: relative;
    z-index: 1;
}
.welcome-intro.is-launching .welcome-title {
    animation: welcome-fadeup 500ms cubic-bezier(0.2, 0.8, 0.2, 1) 600ms both;
}
.welcome-intro.is-launching .welcome-subtitle {
    animation: welcome-fadeup 500ms cubic-bezier(0.2, 0.8, 0.2, 1) 850ms both;
}
.welcome-intro.is-launching .welcome-cta-row {
    animation: welcome-fadeup 500ms cubic-bezier(0.2, 0.8, 0.2, 1) 1100ms both;
}
.welcome-intro.is-launching .welcome-disclaimer {
    animation: welcome-fadeup 500ms cubic-bezier(0.2, 0.8, 0.2, 1) 1300ms both;
}

@keyframes welcome-aquila-enter {
    0%   { opacity: 0; transform: scale(0.25) rotate(-14deg); }
    60%  { opacity: 1; }
    100% { opacity: 1; transform: scale(1) rotate(0deg); }
}
@keyframes welcome-aquila-stamp {
    0%   { transform: scale(1); }
    50%  { transform: scale(1.06); }
    100% { transform: scale(1); }
}
@keyframes welcome-glow-bloom {
    0%   { opacity: 0; }
    40%  { opacity: 0.6; }
    100% { opacity: 0.18; }
}
@keyframes welcome-fadeup {
    0%   { opacity: 0; transform: translateY(10px); }
    100% { opacity: 1; transform: translateY(0); }
}

/* Honour reduced-motion preference — skip animations entirely. */
@media (prefers-reduced-motion: reduce) {
    .welcome-intro.is-launching .welcome-aquila,
    .welcome-intro.is-launching .welcome-title,
    .welcome-intro.is-launching .welcome-subtitle,
    .welcome-intro.is-launching .welcome-cta-row,
    .welcome-intro.is-launching .welcome-disclaimer,
    .welcome-intro.is-launching::before {
        animation: none !important;
        opacity: 1 !important;
        transform: none !important;
    }
}

/* ============================================================
 * Gameplan editor (Phase A)
 *
 * Third tab on the Army Builder. Each form section is a `.card`
 * with a heading + body so it visually matches the existing
 * editor screens. Inputs auto-save on change (no save button).
 * Mobile-friendly — all rows reflow column-wise under 520px.
 * ============================================================ */
.gp-section { padding: 12px 14px; margin-bottom: 12px; }
.gp-section-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    margin-bottom: 8px;
}
.gp-section-title {
    font-size: 14px;
    font-weight: 800;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--text);
}
.gp-section-reset {
    background: transparent;
    border: 1px solid var(--line);
    color: var(--text-dim);
    border-radius: 999px;
    font-size: 11px;
    padding: 3px 10px;
    cursor: pointer;
    transition: color 0.12s, border-color 0.12s;
}
.gp-section-reset:hover { color: var(--text); border-color: var(--accent-soft); }

/* Shared textarea — used for overall, per-phase, and per-role plans. */
.gp-textarea {
    width: 100%;
    background: var(--bg-elev-2);
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    color: var(--text);
    font: inherit;
    font-size: 13.5px;
    line-height: 1.45;
    padding: 8px 10px;
    resize: vertical;
    min-height: 64px;
    transition: border-color 0.12s;
}
.gp-textarea:focus { outline: none; border-color: var(--accent-soft); }

/* Per-phase rows — label above textarea on mobile, side-by-side on wider. */
.gp-phase-row {
    display: grid;
    grid-template-columns: 130px 1fr;
    gap: 8px 12px;
    align-items: start;
    margin-bottom: 10px;
}
.gp-phase-row:last-child { margin-bottom: 0; }
.gp-phase-label {
    font-size: 12px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--text-mute);
    padding-top: 10px;
}
@media (max-width: 520px) {
    .gp-phase-row {
        grid-template-columns: 1fr;
        gap: 4px;
    }
    .gp-phase-label { padding-top: 0; }
}

/* Priority-target chips. Vertical list of editable single-line inputs
   with a remove button on each row. */
.gp-chip-list {
    list-style: none;
    padding: 0;
    margin: 0;
    display: flex;
    flex-direction: column;
    gap: 6px;
}
.gp-chip-row {
    display: flex;
    align-items: center;
    gap: 6px;
}
.gp-chip-input {
    flex: 1;
    background: var(--bg-elev-2);
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    color: var(--text);
    font: inherit;
    font-size: 13.5px;
    padding: 6px 10px;
}
.gp-chip-input:focus { outline: none; border-color: var(--accent-soft); }
.gp-chip-remove {
    flex: 0 0 auto;
    width: 28px;
    height: 28px;
    border-radius: 999px;
    border: 1px solid var(--line);
    background: var(--bg-elev-2);
    color: var(--text-dim);
    cursor: pointer;
    font-size: 14px;
    line-height: 1;
}
.gp-chip-remove:hover { color: var(--red, #c8102e); border-color: var(--red, #c8102e); }

/* Role rows — a card-like row per assigned role with a colored
   "gumball" preview, label/color/glyph controls, unit multi-select,
   and a per-role plan textarea. */
.gp-role-list {
    display: flex;
    flex-direction: column;
    gap: 10px;
}
.gp-role-row {
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    padding: 10px;
    background: var(--bg-elev);
}
.gp-role-head {
    display: flex;
    align-items: center;
    gap: 8px;
    margin-bottom: 8px;
}
.gp-role-label {
    flex: 1 1 auto;
    min-width: 0;
    background: var(--bg-elev-2);
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    color: var(--text);
    font: inherit;
    font-size: 14px;
    font-weight: 700;
    padding: 6px 10px;
}
.gp-role-color {
    flex: 0 0 auto;
    width: 36px;
    height: 32px;
    padding: 0;
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    background: var(--bg-elev-2);
    cursor: pointer;
}
.gp-role-remove {
    flex: 0 0 auto;
    width: 32px;
    height: 32px;
    border-radius: 999px;
    border: 1px solid var(--line);
    background: var(--bg-elev-2);
    color: var(--text-dim);
    cursor: pointer;
}
.gp-role-remove:hover { color: var(--red, #c8102e); border-color: var(--red, #c8102e); }
.gp-role-controls {
    display: flex;
    flex-direction: column;
    gap: 4px;
    margin-bottom: 8px;
}
.gp-mini-label {
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--text-mute);
    font-weight: 700;
}
.gp-role-glyph {
    background: var(--bg-elev-2);
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    color: var(--text);
    font: inherit;
    font-size: 13px;
    padding: 6px 10px;
}
/* Header row for the Assigned-units block: label on the left,
   "Auto-assign from plan" button on the right. Wraps on narrow
   screens so the button drops below the label instead of clipping. */
.gp-role-units-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    flex-wrap: wrap;
    margin-bottom: 2px;
}
.gp-role-auto-assign {
    flex: 0 0 auto;
    font-size: 11.5px;
    padding: 3px 10px;
}
/* Assigned-units checkbox list — replaces the old <select multiple>
   so it works cleanly on mobile (tap to toggle, no Ctrl/Cmd needed). */
.gp-role-units-list {
    display: flex;
    flex-direction: column;
    gap: 2px;
    background: var(--bg-elev-2);
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    padding: 4px;
    max-height: 240px;
    overflow-y: auto;
}
.gp-role-unit-check {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 5px 8px;
    border-radius: var(--radius-sm);
    cursor: pointer;
    font-size: 13.5px;
    transition: background 0.1s;
}
.gp-role-unit-check:hover { background: var(--bg-elev); }
.gp-role-unit-check input[type="checkbox"] {
    flex: 0 0 auto;
    width: 16px;
    height: 16px;
    accent-color: var(--accent-soft, #4ea3ff);
    cursor: pointer;
}
.gp-role-unit-check span {
    flex: 1 1 auto;
    color: var(--text);
    font-weight: 600;
}
/* Unassigned (unchecked) rows render their name muted + italic so
   the eye latches onto the units that ARE in this role. */
.gp-role-unit-check:not(:has(input:checked)) span {
    color: var(--text-mute);
    font-style: italic;
    font-weight: 500;
}

/* The Gumball — a rounded chip carrying the warhammer40k-font glyph
   tinted with the role's chosen color. Color is set inline by the
   render code via background/color/border-color triplet (alpha is
   baked into the background hex with "22" suffix in JS). */
.gp-gumball {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 32px;
    height: 32px;
    border-radius: 999px;
    border: 1px solid currentColor;
    font-size: 18px;
    line-height: 1;
}

/* Stratagem priority — ordered list with rank + name + CP + up/down/remove. */
.gp-strat-list {
    list-style: none;
    padding: 0;
    margin: 0;
    display: flex;
    flex-direction: column;
    gap: 4px;
}
.gp-strat-row {
    display: grid;
    grid-template-columns: 28px 1fr auto auto;
    align-items: center;
    gap: 6px;
    padding: 6px 8px;
    background: var(--bg-elev-2);
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    font-size: 13.5px;
}
.gp-strat-rank {
    font-weight: 800;
    color: var(--text-mute);
    text-align: right;
}
.gp-strat-name { font-weight: 700; color: var(--text); }
.gp-strat-cp {
    font-family: ui-monospace, "SF Mono", Menlo, monospace;
    font-size: 12px;
    color: var(--gold);
    background: rgba(212, 168, 74, 0.12);
    border: 1px solid rgba(212, 168, 74, 0.4);
    border-radius: 999px;
    padding: 1px 8px;
}
.gp-strat-detachment { font-size: 11px; grid-column: 2; }
.gp-strat-actions {
    grid-column: 4;
    display: inline-flex;
    align-items: center;
    gap: 4px;
}
.gp-strat-actions button {
    width: 26px;
    height: 26px;
    padding: 0;
    border: 1px solid var(--line);
    background: var(--bg-elev);
    color: var(--text-dim);
    border-radius: var(--radius-sm);
    cursor: pointer;
    font-size: 11px;
}
.gp-strat-actions button:disabled { opacity: 0.35; cursor: not-allowed; }
.gp-strat-actions button:not(:disabled):hover { color: var(--text); border-color: var(--accent-soft); }
.gp-strat-remove:hover { color: var(--red, #c8102e) !important; border-color: var(--red, #c8102e) !important; }
@media (max-width: 520px) {
    .gp-strat-row { grid-template-columns: 22px 1fr auto; }
    .gp-strat-detachment { display: none; }
    .gp-strat-actions { grid-column: 3; }
}

/* Prioritized secondaries — labelled checkbox rows. */
.gp-secondary-list {
    display: flex;
    flex-direction: column;
    gap: 4px;
}
.gp-secondary-row {
    display: grid;
    grid-template-columns: auto 1fr;
    gap: 6px 10px;
    align-items: start;
    padding: 8px 10px;
    background: var(--bg-elev-2);
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    font-size: 13px;
    cursor: pointer;
}
.gp-secondary-row input[type="checkbox"] {
    margin-top: 3px;
    width: 16px;
    height: 16px;
    accent-color: var(--blue, #4ea3ff);
}
.gp-secondary-row:has(input:checked) {
    border-color: var(--blue, #4ea3ff);
    background: rgba(78, 163, 255, 0.10);
}
.gp-secondary-name { font-weight: 700; color: var(--text); grid-column: 2; }
.gp-secondary-desc {
    font-size: 12.5px;
    line-height: 1.4;
    grid-column: 2;
}

/* ============================================================
   Core Rules reference (v0.10.360)
   ============================================================ */

/* ---- Index ---- */
.cr-hero {
    padding: 4px 0 8px;
    margin-bottom: 4px;
}
.cr-hero-kicker {
    font-size: 12px;
    font-weight: 700;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--accent-soft);
}
.cr-hero-title {
    font-size: 28px;
    font-weight: 900;
    letter-spacing: 0.01em;
    margin: 2px 0 6px;
}
.cr-hero-sub {
    font-size: 14px;
    color: var(--text-dim);
    line-height: 1.5;
}
.rk-term-demo, .cr-hero-sub .rk-term-demo {
    border-bottom: 1px dotted var(--accent-soft);
    color: var(--text);
}
.cr-list {
    display: grid;
    /* Single full-width column. minmax(0,1fr) — not the default `auto` —
       so the track fills the container instead of sizing to the widest
       card's (nowrap) sub-line, which otherwise made each chapter's cards
       a different width. The 0 min lets the sub-line ellipsis engage. */
    grid-template-columns: minmax(0, 1fr);
    gap: 8px;
}
.cr-card {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 13px 14px;
    border: 1px solid var(--line);
    border-radius: var(--radius);
    background: var(--bg-elev);
    color: var(--text);
    text-decoration: none;
    transition: transform 0.12s ease, border-color 0.15s ease;
}
.cr-card:active { transform: scale(0.99); border-color: var(--accent); }
.cr-card-num {
    flex-shrink: 0;
    width: 30px;
    height: 30px;
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: 50%;
    background: var(--bg-elev-2);
    border: 1px solid var(--line);
    color: var(--accent-soft);
    font-weight: 800;
    font-size: 13px;
}
.cr-card-body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.cr-card-title { font-weight: 700; font-size: 15px; }
.cr-card-sub {
    font-size: 12px;
    color: var(--text-dim);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.cr-card-chev { color: var(--text-mute); font-size: 22px; line-height: 1; flex-shrink: 0; }

/* ---- Section page ---- */
.cr-page { padding-bottom: 92px; }   /* clear the fixed prev/next nav */
.cr-breadcrumb { font-size: 12.5px; margin-bottom: 4px; }
.cr-crumb { color: var(--accent-soft); text-decoration: none; }
.cr-crumb-sep { color: var(--text-mute); margin: 0 6px; }
.cr-crumb-cur { color: var(--text-dim); }
.cr-title { font-size: 25px; font-weight: 900; margin: 4px 0 2px; line-height: 1.15; }
.cr-progress { font-size: 12px; color: var(--text-mute); margin-bottom: 14px; }
.cr-subnav { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 18px; }
.cr-subnav-chip {
    font: inherit;
    font-size: 12.5px;
    padding: 5px 11px;
    border-radius: 999px;
    border: 1px solid var(--line);
    background: var(--bg-elev-2);
    color: var(--text-dim);
    cursor: pointer;
}
.cr-subnav-chip:active { border-color: var(--accent); color: var(--text); }

/* ---- Rules content typography ---- */
.rk-content { font-size: 15px; line-height: 1.62; color: var(--text); }
.rk-content h2 {
    font-size: 20px;
    font-weight: 800;
    margin: 24px 0 8px;
    padding-bottom: 6px;
    border-bottom: 1px solid var(--line);
}
.rk-content h3 { font-size: 16.5px; font-weight: 700; margin: 18px 0 6px; color: var(--accent-soft); }
.rk-content h4 { font-size: 15px; font-weight: 700; margin: 14px 0 4px; color: var(--gold); }
.rk-content p { margin: 8px 0; }
.rk-content img {
    max-width: 100%;
    height: auto;
    display: block;
    margin: 14px auto;
    border-radius: var(--radius-sm);
    border: 1px solid var(--line);
}
.rk-content .Columns2, .rk-content .Columns3 { display: block; column-count: 1; }
.rk-content .legend, .rk-content .legend2, .rk-content .legend4 {
    display: block;
    font-style: italic;
    color: var(--text-dim);
    margin-bottom: 10px;
}
.rk-content .frameLight, .rk-content .frameDark {
    border: 1px solid var(--line);
    background: var(--bg-elev-2);
    border-radius: var(--radius-sm);
    padding: 10px 14px;
    margin: 14px 0;
}
.rk-content .frameDark { background: var(--bg-elev); }
.rk-content .BreakInsideAvoid { break-inside: avoid; }
.rk-content ul, .rk-content ol { padding-left: 1.35em; margin: 8px 0; }
.rk-content li { margin: 4px 0; }
.rk-content ul.redSquare { list-style: square; }
.rk-content ul.redSquare li::marker { color: var(--accent); }
.rk-content .kwb,
.rk-content .kwbu,
.rk-content .kwb2,
.rk-content .kwbo,
.rk-content .tt { font-weight: 700; color: var(--text); }
.rk-content .bluefont { color: var(--blue); }
.rk-content .redfont { color: var(--accent-soft); }
.rk-content .redBox {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 22px;
    height: 22px;
    padding: 0 6px;
    background: var(--accent);
    color: #fff;
    font-weight: 800;
    font-size: 13px;
    border-radius: 6px;
}
/* Wahapedia "sequence" timeline boxes — a numbered step list. Steps come in
   two markups: a redDiamond badge as a *sibling* before its label (Fight,
   Battle Round) and a redDiamond3 badge *nested inside* its label span
   (Only War, Muster). Joined in the source by decorative redDiamondLine
   connectors. Render compactly: hide the connectors, float each numbered
   badge left, and make every label a block BFC (overflow:hidden) so its
   text sits beside the badge and standalone sub-steps flow full width. */
.rk-content .redDiamondLine,
.rk-content .redDiamondBottomLine { display: none; }
.rk-content .redDiamond, .rk-content .redDiamond3 {
    float: left;
    clear: left;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 22px;
    height: 22px;
    background: var(--accent);
    color: #fff;
    border-radius: 5px;
    margin: 0 9px 8px 0;
}
.rk-content .redDiamondText, .rk-content .redDiamondText3 { font-weight: 800; font-size: 13px; }
.rk-content .hi_custom, .rk-content .hi_custom_red {
    display: block;
    overflow: hidden;          /* own BFC: text sits beside the floated badge */
    min-height: 22px;
    font-weight: 800;
    font-size: 14px;
    letter-spacing: 0.02em;
    text-transform: uppercase;
    margin: 8px 0 2px;
}
.rk-content .hi_custom_red { color: var(--accent-soft); }
.rk-content .float_center { float: none; }

/* tables — fluid + scrollable when they overflow */
.rk-content table {
    border-collapse: collapse;
    width: 100%;
    margin: 14px 0;
    font-size: 14px;
}
.rk-content td, .rk-content th {
    border: 1px solid var(--line);
    padding: 6px 9px;
    text-align: left;
    vertical-align: top;
}
.rk-content table.customTable { background: var(--bg-elev-2); }

/* keyword tooltips, jump links, anchors */
.rk-term {
    border-bottom: 1px dotted var(--accent-soft);
    cursor: help;
    color: var(--text);
}
.rk-link {
    color: var(--accent-soft);
    cursor: pointer;
    border-bottom: 1px solid transparent;
}
.rk-link:hover { border-bottom-color: var(--accent-soft); }
.rk-anchor { display: block; height: 0; }
.rk-gap { height: 8px; }

/* attribution footnote (per-section + index) */
.cr-attribution {
    margin: 26px 0 4px;
    padding-top: 12px;
    border-top: 1px solid var(--line);
    font-size: 11.5px;
    line-height: 1.55;
    color: var(--text-mute);
}
.cr-attribution strong { color: var(--text-dim); }

/* ---- Always-visible previous / next nav ---- */
.cr-nav {
    position: fixed;
    left: 0;
    right: 0;
    bottom: calc(var(--bottombar-h) + var(--safe-bot));
    z-index: 39;
    background: rgba(13, 14, 18, 0.94);
    backdrop-filter: saturate(180%) blur(14px);
    -webkit-backdrop-filter: saturate(180%) blur(14px);
    border-top: 1px solid var(--line);
}
.cr-nav-inner {
    max-width: 880px;
    margin: 0 auto;
    display: flex;
    gap: 8px;
    padding: 9px 12px;
}
.cr-nav-btn {
    flex: 1 1 0;
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: 2px;
    padding: 8px 12px;
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    background: var(--bg-elev);
    color: var(--text);
    text-decoration: none;
    transition: border-color 0.15s ease;
}
.cr-nav-btn:active { border-color: var(--accent); }
.cr-nav-next { text-align: right; align-items: flex-end; }
.cr-nav-dir {
    font-size: 10.5px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--text-mute);
}
.cr-nav-name {
    font-size: 13px;
    font-weight: 700;
    max-width: 100%;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
