/* ============================================================
 * Oikoumene UI — responsive desktop + mobile
 * ------------------------------------------------------------
 * Toolbar is split into TWO zones:
 *   .toolbar-info  (left)  — live game state (ws-dot, tick, resources)
 *   .toolbar-ctrl  (right) — action buttons (Home / Help / Armies / …)
 *
 * Breakpoints:
 *   ≥ 1024px  → full toolbar, side panels as floating boxes
 *   640–1023  → compact toolbar, narrower panels
 *   < 640px   → mobile: hamburger nav, panels become full-screen drawers
 * ============================================================ */

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

:root {
    /* ui-modal-redesign / TASK_01 (AC01) — atlas palette swap.
       Surface / ink / accent / status tokens for the cartographer's-atelier
       direction (warm parchment instead of cool navy). The legacy navy +
       sky-blue palette (`#1a1a2e`, `#5aa6ff`, sibling rgba()s) is removed
       outright — there is no compatibility alias because the tile PNGs +
       canvas paint already snapshot the new tokens via getComputedStyle.
       Departure from `ui-tile-army-cards-rework`'s navy contract is
       intentional — see `.aidocs/in-progress/ui-modal-redesign/AUDIT.md`. */
    --surface:        #1B1814;
    --surface-elev:   #231F19;
    --surface-sunken: #15120F;
    --ink:            #EBE3D2;
    --ink-mute:       #A89E89;
    --ink-faint:      #6E6754;
    --accent:         #C8923D;
    --accent-strong:  #E0A954;
    --patina:         #5B7A52;
    --vermilion:      #A8442C;
    --lapis:          #3F5E80;
    --border-soft:    rgba(235, 227, 210, 0.08);
    --border-firm:    rgba(235, 227, 210, 0.16);
    --rule:           rgba(235, 227, 210, 0.04);

    /* Legacy aliases — every CSS rule + canvas-paint snapshot still reads
       --bg / --bg-elev / --border / --text* / --color-* / --accent-dark /
       --safe / --danger by name. Re-binding via the new atlas tokens keeps
       the surface change atomic — no per-rule rewrite cascade in this task.
       Subsequent ui-modal-redesign tasks (T02+) migrate consumer rules onto
       the new vocabulary directly and prune these aliases. */
    --bg:             var(--surface);
    --bg-elev:        var(--surface-elev);
    --bg-toolbar:     var(--surface-sunken);
    --border:         var(--border-soft);
    --border-strong:  var(--border-firm);
    --text:           var(--ink);
    --text-dim:       var(--ink-mute);
    --text-muted:     var(--ink-faint);
    --text-secondary: var(--ink-mute);

    /* Design tokens (2026-04) — semantic ladder for typography + status
       colour. Old --color-primary mapped to sky-blue navy; the atlas swap
       re-keys it to the brass-strong accent so existing rules still resolve
       against the new palette. Status colours move onto patina (success) /
       accent-strong (warning) / vermilion (danger). */
    /* mobile-ux-rework TASK_08 / AC34 — body-copy font-size tokens
     * resolve via `rem` so iOS Settings → Display → Text Size (and
     * the desktop equivalent on Chrome / Firefox) scales every
     * panel surface that consumes the token. Display tokens
     * (`--fs-h1` / `--fs-h2` / `--fs-display`) stay in `px` per AC34
     * — they already use rounded geometric sizes whose visual
     * weight depends on the absolute pixel value, not on the user's
     * dynamic-text preference. The root `<html>` element's
     * `font-size: 100%` rule below pegs `1rem = browser default 16
     * px` so the conversion stays neutral on every device that
     * doesn't tweak Larger Text: 13/16 = 0.8125rem, 11/16 =
     * 0.6875rem, etc. */
    --fs-h1:    24px;
    --fs-h2:    16px;
    --fs-body:  0.8125rem;  /* 13 px @ root 16 px */
    --fs-label: 0.6875rem;  /* 11 px @ root 16 px */
    --color-primary: var(--accent-strong);
    --color-success: var(--patina);
    --color-warning: var(--accent-strong);
    --color-danger:  var(--vermilion);

    --accent-dark: var(--accent);
    --danger:      var(--vermilion);
    --safe:        var(--patina);

    /* Spacing & radius ladders (ui-modal-redesign / TASK_01 / AC03).
       Single radius scale: sm 4 / md 8 / lg 12 / pill 999.
       Legacy `--radius` + `--radius-sm` remain aliased so unmigrated rules
       resolve against the new ladder; T26 scrubs the alias. */
    --radius:    var(--radius-md);
    --radius-sm: 4px;
    --radius-md: 8px;
    --radius-lg: 12px;
    --radius-pill: 999px;
    --tap: 48px;        /* minimum touch target (bumped 44→48 for WCAG AAA, ui-mobile-touch-baseline) */
    --tap-sm: 40px;     /* bumped 36→40 — DESKTOP-ONLY (compact toolbar);
                           must NOT appear inside `@media (max-width: 639px)`
                           blocks per mobile-ux-rework TASK_01 / AC01. Mobile
                           call sites consume `--tap-mobile` instead. */
    --toolbar-h: 48px;
    --bottom-nav-h: 56px;  /* used by context-banner mobile override */

    /* mobile-ux-rework / TASK_01 — token additions for the mobile rework.
       New tokens keep the desktop chrome untouched: every consumer is gated
       under `@media (max-width: 639px)` (or the landscape-phone block).
       Until TASK_03 sweeps the mobile call sites onto `--tap-mobile`, the
       tokens declared here are unconsumed — that is intentional, this task
       is the foundation that subsequent tasks build on. See
       `.aidocs/in-progress/mobile-ux-rework/design.md::Token additions`. */
    --tap-mobile:        48px;     /* WCAG AAA + iOS HIG floor (mobile only) */
    --tap-mobile-lg:     56px;     /* Primary CTAs (sheet footer, hero buttons) */
    --lh-body-mobile:    1.45;     /* iOS legibility — bumps body line-height on phones */
    --sheet-grabber-w:   40px;     /* Drag handle width (sheet swipe-to-close) */
    --sheet-grabber-h:   4px;
    --drawer-w:          320px;    /* Off-canvas hamburger drawer width */
    --ctx-menu-min-w:    240px;    /* Long-press context menu */
    --press-scale:       0.98;     /* :active feedback scale (suppressed by reduced-motion) */
    --safe-top:          env(safe-area-inset-top, 0px);
    --safe-bottom:       env(safe-area-inset-bottom, 0px);
    --safe-left:         env(safe-area-inset-left, 0px);
    --safe-right:        env(safe-area-inset-right, 0px);

    /* ui-clarity-pass TASK_18 / AC-I3 — disconnect-banner grace window
       in milliseconds. Default 8000 (mobile-flaky tolerance); E2E test
       seam can override via `document.documentElement.style.setProperty`
       to bypass the 8 s wait. Read by `ws_overlay.js::_readDisconnectGraceMs`. */
    --ws-disconnect-grace: 8000ms;

    /* economy-rework T18 (AC46/AC50/AC52) — capacity-meter colour ladder.
       ui-modal-redesign / TASK_01 / AC01 re-keys the five tiers onto the
       atlas tokens (`--patina` / `--accent-strong` / vermilion-warm /
       `--vermilion` / `--ink-mute`) so load semantics + status colours
       share one vocabulary. The `--load-overload` tier sits between
       brass-strong (warn) and vermilion (block) on the warm-orange band
       (#C97540) — the only literal value in the ramp because no atlas
       token sits exactly at that warm-orange seam. */
    --load-ok:        var(--patina);          /* <30% — bonus tier (-1 AP/step) */
    --load-normal:    var(--ink-mute);
    --load-warn:      var(--accent-strong);   /* 70-100% — near overload */
    --load-overload:  #C97540;                /* 100-200% — capped 1 step / move */
    --load-block:     var(--vermilion);       /* >200% — movement rejected */
    --equipment-frame: rgba(255, 215, 90, 0.18);
    --mount-pegasus:  #b08fff;
    --mount-horse:    #c89968;

    /* ui-modal-clarity AC29 — rarity tokens replace the inline hex map
       previously hardcoded in units_panel.js. Helper classes below map
       1:1 to backend rarity strings (`it.rarity`). */
    --rarity-common:    #ddd;
    --rarity-uncommon:  var(--color-success);
    --rarity-rare:      var(--color-primary);
    --rarity-epic:      #c77dff;
    --rarity-legendary: var(--color-warning);

    /* ui-modal-redesign / TASK_01 (AC02) — typography flip.
       Replaces Cinzel + Manrope (the economy-rework T21 contract) with
       EB Garamond (display, sets every modal title and h2) + Public Sans
       (body, sets every other surface). JetBrains Mono is kept verbatim
       for digits / coords / AP. Cinzel + Manrope `<link>` entries removed
       from `frontend/index.html` in the same commit; system fallbacks
       stay in the value list so CDN-blocked sessions degrade gracefully
       to Iowan Old Style / Georgia (display) and system-ui (body). */
    --font-display: 'EB Garamond', 'Iowan Old Style', Georgia, serif;
    --font-body:    'Public Sans', system-ui, sans-serif;
    --font-mono:    'JetBrains Mono', 'Consolas', 'Courier New', monospace;

    /* Spacing scale — ui-modal-redesign / TASK_01 (AC03) — rebound onto
       the design.md §2.3 ladder: 4 / 8 / 12 / 16 / 20 / 24 / 32 / 48.
       `--space-7` is reinstated (32 px); `--space-8` lifts to 48 px so the
       atlas modal anatomy can lean on a generous gutter. The ui-modal-
       clarity 28-px gap was retired with no consumer; T26 will scrub any
       residual `--space-7: 28px` literals once the audit confirms zero
       fallthrough. */
    --space-0: 0;
    --space-1: 4px;
    --space-2: 8px;
    --space-3: 12px;
    --space-4: 16px;
    --space-5: 20px;
    --space-6: 24px;
    --space-7: 32px;
    --space-8: 48px;

    /* Typography refinements (2026-04 — ui-modal-clarity AC56/AC57/AC58).
       --fs-display: explicit token for sheet-header h3 (was 15px literal).
       --fs-tiny: bumped 10→11 px to match --fs-label floor.
       --lh-* ladder: tight for headings, body for prose, loose kept
       transitional for help-panel migration in TASK_10. */
    --fs-display: 15px;
    /* mobile-ux-rework TASK_08 / AC34 — body-copy tier so resolves
     * via `rem` (matches `--fs-label` floor). */
    --fs-tiny:    0.6875rem;  /* 11 px @ root 16 px */
    --lh-tight:   1.4;
    --lh-body:    1.55;
    --lh-loose:   1.65;

    /* ui-fullscreen-tile-detail TASK_06 — workspace shell tokens.
       Two new design tokens (locked in design.md § Aesthetic direction)
       extend the existing palette without inventing new colours:
         --workspace-curtain: blackout overlay behind the fullscreen pane
         --workspace-divider: vertical seam between diorama + tab pane
                              (leather-bound ledger spine effect) */
    --workspace-curtain: rgba(8, 10, 24, 0.92);
    --workspace-divider: linear-gradient(180deg, transparent 0%, var(--border-strong) 20%, var(--border-strong) 80%, transparent 100%);

    /* ui-tariff-season-chips / TASK_04 — `--season-*` colour tokens for
       the toolbar `#chip-season` background tint (AC7). Single source
       of truth: any season-tinted surface (chip background, future
       event_drama modal banner) reads these instead of inlining hex
       palettes per component. Values picked to mirror the season
       icon palette in the empire-panel hero band — soft pastels so
       chip text + tooltip stay legible against the dark toolbar. */
    --season-spring: #c2e4b8;
    --season-summer: #f3e1a3;
    --season-autumn: #e6c19a;
    --season-winter: #cad9e6;
}

/* ─── Utility classes (2026-04 design tokens) ─────────────── */
.text-success { color: var(--color-success); }
.text-warning { color: var(--color-warning); }
.text-danger  { color: var(--color-danger); }
.text-primary { color: var(--color-primary); }
.text-muted   { color: var(--text-muted); }
.text-dim     { color: var(--text-dim); }
/* ui-modal-clarity AC29 — rarity helpers consume the --rarity-* tokens. */
.rarity-common    { color: var(--rarity-common); }
.rarity-uncommon  { color: var(--rarity-uncommon); }
.rarity-rare      { color: var(--rarity-rare); }
.rarity-epic      { color: var(--rarity-epic); }
.rarity-legendary { color: var(--rarity-legendary); }
.fs-body  { font-size: var(--fs-body); }
.fs-label { font-size: var(--fs-label); }
.fs-tiny  { font-size: var(--fs-tiny); }  /* AC58 — bumped 10→11 to --fs-label floor */
.fw-bold  { font-weight: bold; }
/* Status banners (empire-panel game-over etc.). Background/border from
   token colours, text-colour readable against that bg. */
.banner-danger  { background: #3a1212; border: 1px solid var(--color-danger);  color: #ffb0b0; }
.banner-warning { background: #3a2a12; border: 1px solid var(--color-warning); color: #ffd9a8; }
/* Compact form input used inside panels (trade, guild, treasury). Dev-
   decorative shell: dark bg, thin border, light text. Pair with inline
   width/padding tweaks as needed — those stay in markup. */
.form-input { background: #222; border: 1px solid #444; color: #eee; padding: 4px; }
.form-input.compact { padding: 2px; font-size: var(--fs-label); }

/* mobile-ux-rework TASK_08 / AC34 — root `font-size: 100%` so the
 * `--fs-*` rem tokens scale with the user's browser / OS dynamic-text
 * preference (iOS Settings → Display & Brightness → Text Size, Chrome
 * Settings → Appearance → Font size, etc.). Default is the browser's
 * own 16 px so `1rem = 16 px` on every device that doesn't tweak the
 * preference; users who bump Larger Text +2 ticks see every body-copy
 * surface scale uniformly while display-tier headings (`--fs-h1` /
 * `--fs-h2` / `--fs-display`, all in `px`) keep their geometric
 * weight. The `-webkit-text-size-adjust: 100%` rule on `body` below
 * additionally suppresses Safari's auto-text-zoom on landscape
 * orientation flip. */
html {
    font-size: 100%;
}

body {
    background: var(--bg);
    color: var(--text);
    /* economy-rework T21 (AC51): body font swapped from the bare
       `'Segoe UI', system-ui, sans-serif` literal to the
       `--font-body` token (Manrope-led). The previous fallback
       chain stays inside the token's value list, so users on
       CDN-blocking networks still see Segoe UI. */
    font-family: var(--font-body);
    /* ui-modal-clarity AC56 — explicit body type baseline. Per-element
       rules can still override; this just ensures the cascade default
       is comfortable (13 px / 1.55 line-height) instead of the UA
       default 16 px / 1.2. */
    font-size: var(--fs-body);
    line-height: var(--lh-body);
    overflow: hidden;
    /* Mobile browsers' `100vh` is the LARGEST viewport (URL bar
     * collapsed) — when the URL bar is visible, body ends up taller
     * than the visible area and the bottom of the interface (toolbar,
     * bottom-nav, panel content) sits below the fold. `100dvh` is the
     * dynamic viewport height — re-measures as the URL bar shows /
     * hides — which is what we actually want here. The `100vh` line
     * stays as a fallback for browsers without `dvh` (pre-2022). */
    height: 100vh;
    height: 100dvh;
    width: 100vw;
    width: 100dvw;
    -webkit-tap-highlight-color: transparent;
    -webkit-text-size-adjust: 100%;
    touch-action: manipulation;
}

/* ui-modal-clarity AC59 — form controls inherit body typography. UA
   defaults to platform font (Arial on Linux Chromium) which clashes
   with the Manrope cascade. Placed early so per-button rules can still
   override font-size for size-variants. */
input, select, textarea, button {
    font-family: inherit;
    font-size: inherit;
    line-height: inherit;
}

/* mobile-ux-rework TASK_03 — global mobile touch-target ladder.
   AC05: every `<input>` / `<textarea>` / `<select>` reachable inside
   any panel renders at `font-size: 16px` on phones (suppresses iOS
   focus-zoom — Safari auto-zooms text inputs whose computed size is
   below 16 px, which retro-fits a ~10 % zoom that breaks layout).
   AC32 (partial): `touch-action: manipulation` is set on the same
   surface to suppress the legacy 300 ms tap delay + browser
   double-tap-to-zoom on the form controls.
   Sits early in the cascade so per-feature mobile blocks (auth-form,
   pickup-modal, atlas-inline-rename, etc.) can still override the
   16 px floor upward — they cannot regress it because every
   downstream override matches at equal or higher specificity. */
@media (max-width: 639px) {
    input, textarea, select {
        font-size: 16px;
        touch-action: manipulation;
    }
}

#app { position: relative; width: 100%; height: 100%; }
.screen { width: 100%; height: 100%; }
.hidden { display: none !important; }
.error { color: var(--danger); font-size: 13px; margin-top: 8px; }

/* ─── Auth ─────────────────────────────────────────────── */
#auth-screen {
    display: flex; align-items: center; justify-content: center;
    background: linear-gradient(135deg, #0d0d1a 0%, #1a1a3e 50%, #0d1a2e 100%);
    padding: 16px;
}
.auth-box {
    background: var(--bg-elev);
    border: 1px solid #334;
    border-radius: 12px;
    padding: 32px 28px;
    width: 100%; max-width: 380px;
    text-align: center;
}
.auth-box h1 { color: var(--accent); font-size: 32px; margin-bottom: 4px; }
.auth-box .subtitle { color: var(--text-muted); font-size: 13px; margin-bottom: 24px; }
#auth-form input, #auth-form select {
    display: block; width: 100%;
    padding: 12px;
    margin-bottom: 12px;
    background: #111;
    border: 1px solid #333;
    border-radius: var(--radius-sm);
    color: #ddd;
    font-size: 16px;  /* ≥16px prevents mobile-zoom-on-focus */
    min-height: var(--tap);
}
#auth-form input:focus { outline: none; border-color: var(--accent); }
.auth-buttons { display: flex; gap: 8px; }

/* ─── Buttons ──────────────────────────────────────────── */
.btn {
    flex: 1;
    padding: 12px;
    border: none;
    border-radius: var(--radius-sm);
    font-size: 14px; font-weight: 600;
    cursor: pointer;
    min-height: var(--tap);
    background: #2a6eaa; color: #fff;
    transition: background 120ms;
}
/* mobile-ux-rework TASK_04 / AC18 — every `:hover` rule on the AC18
 * selector list is paired with `:focus-visible` (keyboard nav surfaces
 * the same affordance) AND `:active` (touch lights up the row on tap).
 * AC15 — mobile press feedback adds a transform scale + touch-action
 * sweep further down (consolidated mobile sweep). */
.btn:hover, .btn:focus-visible, .btn:active { background: #3a8ecc; }
.btn-secondary { background: #333; color: #ccc; }
.btn-secondary:hover, .btn-secondary:focus-visible, .btn-secondary:active { background: #444; }

.btn-sm {
    padding: 8px 12px;
    background: #2a4a6e;
    border: 1px solid #446;
    color: #ccc;
    border-radius: 4px;
    cursor: pointer;
    font-size: 12px;
    min-height: var(--tap-sm);
    line-height: 1;
    white-space: nowrap;
}
.btn-sm:hover, .btn-sm:focus-visible, .btn-sm:active { background: #3a5a8e; }
.btn-sm[disabled] { opacity: 0.4; cursor: not-allowed; }
.btn-primary { background: var(--accent-dark); }
.btn-primary:hover, .btn-primary:focus-visible, .btn-primary:active { background: #3a5a9e; }
.btn-icon { min-width: var(--tap-sm); font-weight: 600; }

/* ─── Toolbar: split into info (left) + controls (right) ── */
#toolbar {
    position: absolute;
    top: 0; left: 0; right: 0;
    min-height: var(--toolbar-h);
    background: var(--bg-toolbar);
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 6px 12px;
    z-index: 40;
    border-bottom: 1px solid var(--border-strong);
    flex-wrap: wrap;                 /* wraps on narrow screens */
}

.toolbar-info {
    display: flex;
    align-items: center;
    gap: 10px;
    flex: 1 1 auto;
    min-width: 0;
    flex-wrap: wrap;
}
#toolbar h1 {
    font-size: 16px;
    color: var(--accent);
    letter-spacing: 0.3px;
    white-space: nowrap;
}
.ws-dot {
    font-size: 12px;
    line-height: 1;
    padding: 0 2px;
    cursor: help;
    user-select: none;
}
.info-chip {
    font-size: 12px;
    color: var(--text-dim);
    padding: 3px 8px;
    background: rgba(255, 255, 255, 0.04);
    border-radius: 4px;
    white-space: nowrap;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    max-width: 100%;
}
/* #coords-display is now hidden in DOM (ui-visual-minimalism TASK_04).
   Legacy setCoordsDisplay() hijacks still write textContent; the
   max-width rule is retained only as a no-op safety for any code
   path that unhides it temporarily during debug. */
/* The legacy player-pool stockpile chip in the toolbar was removed in
   economy-rework AC45 — its CSS block + scrollbar override + mobile
   wrap rule were dropped. The role moved to army-panel inventory,
   tile-panel stockpile, and the empire aggregator panel (T18-T21). */

.toolbar-ctrl {
    display: flex;
    align-items: center;
    gap: 4px;
    flex: 0 0 auto;
}

/* Menu (visible on mobile; wraps controls in a dropdown).
   mobile-ux-rework / TASK_05 — the `.ctrl-menu.open .toolbar-ctrl-group`
   dropdown rule retired here; the off-canvas drawer
   (`frontend/static/js/mobile/drawer.js`) replaces it. The desktop
   `.toolbar-ctrl-group` flex-row layout stays — drawer.js only
   reparents on mobile (via runtime install gated by the toggle/
   drawer markup presence; the `.hw-drawer { display: none }` base
   rule below keeps the drawer chrome inert on desktop). */
.ctrl-menu-btn { display: none; }
.toolbar-ctrl-group {
    display: flex;
    flex-wrap: wrap;
    gap: 4px;
    align-items: center;
}

/* ─── Off-canvas hamburger drawer (mobile-ux-rework TASK_05 / AC26) ──
   Base rule keeps the drawer + backdrop markup inert on desktop /
   tablet — the drawer is mobile-only by design. The mobile media
   block below unhides them and wires the slide-in transform. */
.hw-drawer { display: none; }
.hw-drawer-backdrop { display: none; }
/* mobile-ux-rework TASK_10 / AC11 — desktop / tablet keep the long-press
   context menu hidden by default. The mobile media block below paints
   it as a floating fixed host. The pointer gate in
   `longpress_menu.js::installLongPressContextMenu` already short-circuits
   on `pointerType !== 'touch'`, so this is belt-and-braces: even if a
   desktop user managed to call `openContextMenu` directly via DevTools,
   the menu wouldn't paint outside the mobile breakpoint. */
.hw-ctx-menu { display: none; }
.hw-ctx-menu-backdrop { display: none; }

/* ─── Resource-bar strip (ui-clarity-deferred AC4.1) ─────
   Toolbar resource strip — collapsed by default below 1024px, the
   `.is-expanded` modifier (driven by
   `localStorage.hw.toolbar.resourceBarExpanded`) reveals all 16
   chips. NOT to be confused with the legacy `#resource-bar` HUD —
   that selector is locked off by economy-rework AC45's
   IndexHtmlResourceBarRemovedTest; this strip uses the distinct
   `#resource-bar-strip` id so the AC45 invariant stays green. */
#resource-bar-strip {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    padding: 4px 12px;
    border-bottom: 1px solid var(--border);
    background: rgba(0, 0, 0, 0.18);
}
.resource-bar-chips {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    flex: 1 1 auto;
    overflow: hidden;
}
.resource-bar-chip {
    display: inline-flex;
    align-items: center;
    gap: 2px;
    padding: 2px 6px;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    font-size: var(--fs-label);
    font-family: var(--font-mono);
}
.resource-bar-chip[data-rate="up"]   { color: var(--color-success); }
.resource-bar-chip[data-rate="down"] { color: var(--color-danger); }
.resource-bar-toggle {
    flex: 0 0 auto;
    line-height: 1;
}
@media (max-width: 1023px) {
    #resource-bar-strip:not(.is-expanded) .resource-bar-chip:nth-of-type(n + 7) {
        display: none;
    }
}
@media (min-width: 1024px) {
    #resource-bar-strip .resource-bar-toggle { display: none; }
}

/* ─── Empire food-runway threshold pill (ui-clarity-deferred AC4.3) ─
   Reuses the `--load-*` token palette so the green/yellow/red ramp
   matches the capacity-meter contract. */
.e-food-runway {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    padding: 1px 8px;
    border-radius: var(--radius-sm);
    border: 1px solid var(--border);
    font-family: var(--font-mono);
}
.e-food-runway[data-tier="ok"]     { color: var(--load-ok);     border-color: var(--load-ok); }
.e-food-runway[data-tier="warn"]   { color: var(--load-warn);   border-color: var(--load-warn); }
.e-food-runway[data-tier="danger"] { color: var(--load-block);  border-color: var(--load-block); }

/* ─── Leaderboard player-name button (ui-clarity-deferred AC4.5) ─── */
.lb-name-btn {
    background: none;
    border: none;
    padding: 0;
    color: inherit;
    font: inherit;
    cursor: pointer;
    text-decoration: underline dotted var(--text-dim);
    text-underline-offset: 2px;
}
.lb-name-btn:hover, .lb-name-btn:focus, .lb-name-btn:focus-visible, .lb-name-btn:active { color: var(--color-primary); }

/* ─── Context-mode banner ──────────────────────────────── */
/* Shown above the canvas when the UI is in a target-picking mode
 * (move-target / caravan-dest / raid-target). Floats below the toolbar,
 * stays visible on mobile too so the user always sees the active mode +
 * a cancel button. */
#context-banner {
    position: absolute;
    top: calc(var(--toolbar-h) + 8px);
    left: 50%;
    transform: translateX(-50%);
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 6px 12px;
    background: var(--accent-dark);
    color: #fff;
    border: 1px solid var(--accent);
    border-radius: var(--radius-sm);
    font-size: 13px;
    /* Above the toolbar (z-index 40). When in a context mode the banner
     * IS the active control surface — its row-1 Скасувати button must
     * stay clickable even when the toolbar wraps to two rows on
     * mid-width viewports and physically overlaps the banner area. */
    z-index: 41;
    box-shadow: 0 4px 14px rgba(0, 0, 0, 0.4);
    max-width: calc(100vw - 32px);
    /* Subtle slide-down reveal so the banner appearing doesn't feel
     * jarring. Only runs when .hidden is removed. Suppressed under
     * prefers-reduced-motion (bottom of this file). */
    animation: ctx-banner-in 180ms ease-out;
}
@keyframes ctx-banner-in {
    from { opacity: 0; transform: translateX(-50%) translateY(-8px); }
    to   { opacity: 1; transform: translateX(-50%) translateY(0); }
}
#context-banner .btn-sm {
    background: rgba(0, 0, 0, 0.3);
    border-color: rgba(255, 255, 255, 0.3);
    color: #fff;
    min-width: var(--tap-sm);
    min-height: 30px;
    padding: 4px 10px;
    font-size: 13px;
    line-height: 1;
}
#context-banner .btn-sm:hover, #context-banner .btn-sm:active {
    background: rgba(0, 0, 0, 0.5);
}
/* On narrow mobile viewports let the banner span the screen edge-to-edge
 * (no center-float) and tighten the font so long target prompts don't
 * truncate. */
@media (max-width: 639px) {
    #context-banner {
        left: 8px; right: 8px;
        transform: none;
        max-width: none;
        font-size: 12px;
        padding: 5px 8px;
    }
}

/* ─── 3-row context-banner (ui-context-mode-toolbar) ─────
 * Layered on top of the existing #context-banner shell — the host
 * keeps its position/anim/z-index; `.ctx-banner-3` flips the inner
 * layout to a stacked column with a warning-coloured left accent. */
.ctx-banner-3 {
    flex-direction: column;
    align-items: stretch;
    gap: 4px;
    padding: 8px 12px;
    border-left: 4px solid var(--color-warning);
}
.ctx-row { display: flex; align-items: center; gap: 8px; }
.ctx-row-mode { justify-content: space-between; font-weight: 600; }
.ctx-row-hints, .ctx-row-echo {
    font-size: 12px;
    color: #c8c0a8;
}
.ctx-row-echo .echo-preview { color: #fff; }
/* Mobile row-3 truncation per design.md OQ4. Full text remains in the
 * `title` attr (set by `context_mode.js::_renderEcho`) so SR users
 * still get the complete echo even when the visible row clips. */
@media (max-width: 639px) {
    .ctx-row-echo {
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
        display: block;
    }
}
/* Reduced-motion gate for the warning modal. The banner's slide-in
 * keyframe is already gated below; this covers the modal block too. */
@media (prefers-reduced-motion: reduce) {
    #context-warning-modal { animation: none; }
}

/* ─── Loading overlay (first-paint) ────────────────────── */
#loading-overlay {
    position: absolute;
    top: var(--toolbar-h); left: 0; right: 0; bottom: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    background: rgba(10, 10, 20, 0.85);
    backdrop-filter: blur(4px);
    z-index: 25;
    pointer-events: none;
}
.loading-box {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 14px;
    padding: 22px 32px;
    background: var(--bg-elev);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius);
    box-shadow: 0 8px 32px rgba(0,0,0,0.45);
}
.loading-spinner {
    width: 38px; height: 38px;
    border: 3px solid rgba(136, 204, 255, 0.2);
    border-top-color: var(--accent);
    border-radius: 50%;
    animation: loading-spin 0.9s linear infinite;
}
.loading-text {
    font-size: 13px;
    color: var(--text-dim);
    letter-spacing: 0.3px;
}
@keyframes loading-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
    .loading-spinner { animation: none; border-top-color: var(--accent); }
}

/* ─── Canvas ───────────────────────────────────────────── */
#hex-canvas {
    position: absolute;
    top: var(--toolbar-h); left: 0;
    width: 100%;
    height: calc(100% - var(--toolbar-h));
    cursor: grab;
    touch-action: none;
}
#hex-canvas:active { cursor: grabbing; }

/* ─── Panels (shared) ──────────────────────────────────── */
.panel {
    position: absolute;
    background: var(--bg-elev);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius);
    padding: 16px;
    z-index: 30;
    max-height: calc(100vh - var(--toolbar-h) - 32px);
    max-height: calc(100dvh - var(--toolbar-h) - 32px);
    overflow-y: auto;
    box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
}
.panel h3 {
    color: var(--accent);
    margin-bottom: 10px;
    font-size: 15px;
    /* T21 (AC51) — Cinzel display font for non-sheet-modal panel
       headers (legacy panel shells). Same rationale as the
       sheet-header h3 rule below. */
    font-family: var(--font-display);
    letter-spacing: 0.04em;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
}
.panel-close {
    background: transparent;
    border: 1px solid var(--border-strong);
    color: var(--text-dim);
    border-radius: 4px;
    padding: 0;
    font-size: 14px;
    line-height: 1;
    cursor: pointer;
    min-height: var(--tap-sm);
    min-width: var(--tap-sm);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: 0 0 auto;
}
.panel-close:hover, .panel-close:active {
    background: rgba(255,255,255,0.07);
    color: var(--accent);
    border-color: var(--accent);
}

/* Per-panel `--sheet-width` overrides (ui-modal-panels TASK_007).
 * Each sheet keeps a comfortable reading width on desktop while
 * `.sheet-modal` rules drive positioning (centered card). Mobile
 * (≤639px) collapses every sheet to full-width via the bottom-sheet
 * media block below, so these widths only affect desktop and tablet.
 * Bumped 2026-04-28 — the original 360/380 widths felt cramped on
 * desktop (modals looked phone-sized on a 1440px screen). */
#empire-panel,
#leaderboard-panel,
#tile-info,
#unit-panel,
#armies-panel { --sheet-width: min(440px, calc(100vw - 32px)); }
#transmute-panel,
#command-panel,
#activity-panel,
#guilds-panel,
#institutions-panel { --sheet-width: min(480px, calc(100vw - 32px)); }
#help-panel,
#caravan-modal { --sheet-width: min(600px, calc(100vw - 32px)); }

/* ui-modal-clarity AC70 — institutions / transmute panels share the
   same --sheet-section-gap rhythm as #tile-details .t-section so era
   headers + recipe groups align with the other modals. The global
   #tile-details rule is scoped, so we restate the consumption point
   per these two panels. */
#institutions-panel .t-section,
#transmute-panel .t-section {
    margin-top: var(--sheet-section-gap);
    padding-top: 0;
    border-top: 0;
}

/* ui-modal-clarity AC72 — settings logout button rhythm via token
   instead of the legacy `style="margin-top:12px"` literal. */
#btn-settings-logout {
    margin-top: var(--space-3);
}

/* ui-modal-clarity AC72 — institutions / transmute panel inline-style
   sweep. Replace `style="margin:4px 0;border-bottom:1px solid #333;
   padding-bottom:4px"` row literal + `style="margin-top:6px"` era
   header literal with class hooks. */
.inst-row {
    margin: var(--space-1) 0;
    border-bottom: 1px solid var(--border);
    padding-bottom: var(--space-1);
}
.inst-era-title { margin-top: var(--space-2); }
.transmute-action { margin-top: var(--space-1); }
.help-body {
    /* ui-modal-clarity AC71 — line-height bumped onto the --lh-body
       baseline (1.55) so help reads at the same rhythm as the rest of
       the app; --lh-loose stays declared for transitional use. */
    font-size: var(--fs-body);
    line-height: var(--lh-body);
    color: var(--text);
}

/* ─── Help-panel tabstrip (ui-help-panel-tabs) ──────────────
   Three tabs (Перші кроки / Механіки / Пізня гра) swap which
   `.help-section[data-help-tab]` rows render. Active tab key
   is mirrored onto `.help-tabs[data-active]` by `help_panel.js`
   so each tab gets a per-bucket accent colour exposed as the
   `--help-accent` custom property — cascades into the
   `.help-section-title::before` left-border anchor below. */
.help-tabs {
    display: flex;
    gap: 4px;
    border-bottom: 1px solid var(--border);
    margin-bottom: 8px;
}
.help-tabs[data-active="basics"]    { --help-accent: var(--color-success); }
.help-tabs[data-active="mechanics"] { --help-accent: var(--accent); }
.help-tabs[data-active="lategame"]  { --help-accent: var(--color-warning); }
.help-tab {
    background: transparent;
    border: 0;
    border-bottom: 2px solid transparent;
    color: var(--text-muted);
    font: inherit;
    padding: 6px 10px;
    cursor: pointer;
    min-height: var(--tap-sm);
}
.help-tab[aria-selected="true"] {
    color: var(--text);
    border-bottom-color: var(--help-accent);
}
/* mobile-ux-rework TASK_04 / AC18 — `:hover` / `:focus-visible` /
 * `:active` triplet on a single fill so keyboard, mouse, and touch
 * each surface the same affordance. The pre-existing
 * `.help-tab:focus-visible` outline rule stays alongside as the
 * keyboard outline ring (extra layer atop the colour cue). */
.help-tab:hover,
.help-tab:focus-visible,
.help-tab:active {
    color: var(--text);
}
.help-tab:focus-visible {
    outline: 2px solid var(--help-accent);
    outline-offset: 2px;
}

.help-section {
    display: none;
    padding: 10px 0;
    border-bottom: 1px solid var(--border);
}
.help-section.help-section-active { display: block; }
.help-section.help-section-active:last-of-type { border-bottom: 0; }
.help-section:last-child { border-bottom: 0; }
.help-section h4,
.help-section-title {
    color: var(--accent);
    font-size: 13px;
    font-weight: 600;
    margin-bottom: 4px;
    display: flex;
    align-items: center;
    gap: 8px;
}
/* Left-border anchor replaces the decorative emoji that used to prefix
   each help section header (stripped 2026-04). The `--help-accent`
   custom property is set on `.help-tabs[data-active]` and cascades down
   so the left-border tint matches the active tab; the `--accent`
   fallback keeps the rule sensible if the tablist isn't present. */
.help-section-title::before {
    content: '';
    display: inline-block;
    width: 3px;
    height: 1em;
    background: var(--help-accent, var(--accent));
    border-radius: 1px;
    flex-shrink: 0;
}
.help-section p {
    color: var(--text-dim);
    font-size: 12px;
}
.help-section-hint {
    color: var(--text-dim);
    font-size: 11px;
    font-style: italic;
    margin-top: 6px;
}
/* ─── Keyboard-shortcuts table (T3.1 ui-keyboard-shortcuts) ─── */
.help-shortcuts-table {
    width: 100%;
    border-collapse: collapse;
    margin-top: 4px;
    font-size: 12px;
}
.help-shortcuts-table th,
.help-shortcuts-table td {
    padding: 4px 8px;
    text-align: left;
    border-bottom: 1px solid var(--border-soft, rgba(255,255,255,0.06));
}
.help-shortcuts-table th {
    color: var(--text-dim);
    font-weight: 500;
    font-size: 11px;
}
.help-shortcuts-table kbd {
    padding: 1px 5px;
    border: 1px solid var(--border-soft, rgba(255,255,255,0.18));
    border-radius: 3px;
    background: rgba(255,255,255,0.04);
    font-size: 11px;
    /* mobile-ux-rework TASK_08 / AC24 — font-family resolves to the
     * `--font-mono` token instead of the bare `ui-monospace, …` chain.
     * AC24 pins every `font-family` declaration to a `--font-*` token
     * so the cascade stays single-source. */
    font-family: var(--font-mono);
    color: var(--text);
}
.help-dismiss { margin-top: 12px; }

/* ─── Help-panel cross-tab search (ui-clarity-pass AC-H3) ───────────
   Search row sits ABOVE `.help-tabs` and toggles the tabstrip
   off while the input is non-empty. The summary line surfaces the
   per-tab match counts; matched substrings get a `<mark>` wrap. */
.help-search-row {
    display: flex;
    flex-direction: column;
    gap: 4px;
    margin-bottom: 6px;
}
.help-search {
    width: 100%;
    box-sizing: border-box;
    padding: 6px 10px;
    background: rgba(255, 255, 255, 0.04);
    border: 1px solid var(--border, #335);
    border-radius: 4px;
    color: var(--text);
    font: inherit;
    font-size: 13px;
    /* iOS focus-zoom defence: per the static AGENTS convention. */
    line-height: 1.4;
}
.help-search:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 1px;
}
.help-search-summary {
    color: var(--text-dim);
    font-size: 11px;
    font-style: italic;
}
.help-search-hit {
    background: rgba(255, 215, 90, 0.28);
    color: inherit;
    border-radius: 2px;
    padding: 0 2px;
}
.help-section.help-section-search-hit {
    /* Make the highlighted section visually distinct from a regular
       tab-active row: a thin accent border-left helps a player scan a
       long result list back to the per-tab summary header counts. */
    border-left: 2px solid var(--help-accent, var(--accent));
    padding-left: 8px;
}

/* ─── Help button 30-day re-prompt pulse (ui-clarity-pass AC-H4) ───
   Class attached by `help_panel.js::_maybePulseHelpButton` when
   `localStorage.hw.helpLastSeen` is missing or older than 30 days.
   Drops on first explicit interaction (open or close cycle). The
   pulse uses an outline ring so it doesn't disturb the surrounding
   toolbar layout (no width/height shift). Reduced-motion respect
   below disables the keyframe loop without dropping the cue. */
#btn-help.help-pulse {
    animation: help-button-pulse 1.6s ease-in-out infinite;
}
@keyframes help-button-pulse {
    0%, 100% {
        box-shadow: 0 0 0 0 rgba(255, 215, 90, 0.0);
    }
    50% {
        box-shadow: 0 0 0 4px rgba(255, 215, 90, 0.45);
    }
}
@media (prefers-reduced-motion: reduce) {
    #btn-help.help-pulse { animation: none; box-shadow: 0 0 0 2px rgba(255, 215, 90, 0.55); }
}
html.force-reduced-motion #btn-help.help-pulse { animation: none; box-shadow: 0 0 0 2px rgba(255, 215, 90, 0.55); }

/* ─── Tile info panel ──────────────────────────────────── */
/* ui-modal-clarity AC2 — body type ladder consumes tokens. */
#tile-details { font-size: var(--fs-body); line-height: var(--lh-body); }
#tile-details .label { color: var(--text-muted); display: inline-block; width: 85px; }
#tile-details .t-row { display: flex; gap: 10px; flex-wrap: wrap; padding: 2px 0; }

/* ui-modal-clarity AC6 — tile-info header zone. Replaces the legacy 3-loose-
   .t-row layout with a single .t-header block whose two (or three with
   pending-orders) rows establish a clear primary/secondary hierarchy.
   Pending row sits ABOVE row-1 inside the same wrapper so the header reads
   as a single visual unit. */
#tile-details .t-header {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    padding: var(--space-3) 0;
}
#tile-details .t-header-row-1,
#tile-details .t-header-row-2 {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
    align-items: center;
}
#tile-details .t-header-row-1 { font-size: var(--fs-h2); }
#tile-details .t-header-row-2 { font-size: var(--fs-body); }
#tile-details .t-header-pending {
    font-size: var(--fs-label);
    color: var(--color-warning);
    letter-spacing: 0.04em;
}

#tile-details .t-big { font-size: 14px; color: #ddd; }
#tile-details .t-stat { color: var(--text-dim); }
/* ui-modal-clarity AC3 — section rhythm via whitespace, no border-top.
   Margin consumes --sheet-section-gap (16 px desktop, 12 px mobile via
   the .sheet-modal mobile override). */
#tile-details .t-section {
    margin-top: var(--sheet-section-gap);
    padding-top: 0;
    border-top: 0;
}
/* ui-modal-clarity AC4/AC5 — section title font-size on the --fs-label
   floor (11 px), letter-spacing keeps the small-caps cadence. */
#tile-details .t-section-title {
    color: var(--accent);
    font-size: var(--fs-label);
    text-transform: uppercase;
    letter-spacing: 0.5px;
    margin-bottom: 6px;
    opacity: 0.85;
}
#tile-details .t-actions { margin-top: 10px; }
/* ui-modal-clarity AC12/AC16 — secondary action accordion in body.
   Section margin handled by `.t-section`; this rule just ensures the
   dashed-divider rhythm between groups stays consistent and the
   group spacing reads at the section scale. */
#tile-details .t-actions-secondary {
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
}
#tile-details .t-actions-secondary .act-group + .act-group {
    padding-top: var(--space-2);
}

/* Action-category group blocks used inside tile-details .t-actions and
   inside the unit-panel ⚡ Дії section. Shared rules. */
.act-group { margin-top: 8px; }
.act-group + .act-group {
    border-top: 1px dashed var(--border);
    padding-top: 8px;
}
.act-group-head {
    color: var(--accent);
    /* ui-modal-clarity AC5 — bumped 10→11 to --fs-label floor. */
    font-size: var(--fs-label);
    text-transform: uppercase;
    letter-spacing: 0.4px;
    opacity: 0.7;
    margin-bottom: 2px;
}
/* ui-modal-clarity AC18 — hint type at --fs-label floor with 56ch line
   length so long strings wrap before reaching the sheet edge at the
   1280-px viewport. */
#tile-details .t-hint {
    color: var(--text-muted);
    font-size: var(--fs-label);
    max-width: 56ch;
    margin-top: 4px;
    font-style: italic;
}
#tile-details .t-hint--warn { color: var(--color-warning); font-style: normal; }

/* ui-modal-clarity AC7 — independent-city accent card. The 8-percent
   purple wash + 3-px violet border-left signal "city centre" without
   a heavy background or fixed border around the whole card. */
#tile-details .t-section.t-city {
    padding: var(--space-3);
    border-radius: var(--radius-sm);
    background: rgba(180, 120, 220, 0.08);
    border-left: 3px solid #c77dff;
}

/* ui-modal-clarity AC8 — discovery-gauge token classes. Track is a fixed
   80-px reservoir; inner bar's width comes from per-render --pct custom
   property (the only data-driven inline). Colour state maps to the three
   semantic --color-* tokens, keeping the gauge consistent with the rest
   of the status colour system. */
#tile-details .t-section.t-discovery {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    flex-wrap: wrap;
}
#tile-details .t-discovery-track {
    display: inline-block;
    width: 80px;
    height: 8px;
    background: #222;
    border: 1px solid #555;
    vertical-align: middle;
    border-radius: 2px;
    overflow: hidden;
}
#tile-details .t-discovery-bar {
    display: block;
    height: 100%;
    width: var(--pct, 0%);
}
#tile-details .t-discovery-bar--safe   { background-color: var(--color-success); }
#tile-details .t-discovery-bar--warn   { background-color: var(--color-warning); }
#tile-details .t-discovery-bar--danger { background-color: var(--color-danger); }
#tile-details .t-discovery-pct--safe   { color: var(--color-success); }
#tile-details .t-discovery-pct--warn   { color: var(--color-warning); }
#tile-details .t-discovery-pct--danger { color: var(--color-danger); }

/* ui-modal-clarity AC9 — Buildings list (semantic <ul>). */
#tile-details .t-section.t-buildings .t-buildings-list {
    list-style: none;
    padding-left: 0;
    margin: 3px 0 0;
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
}
#tile-details .t-building { line-height: var(--lh-tight); }
#tile-details .t-building-progress { color: var(--color-warning); }
#tile-details .t-building-max { color: var(--color-warning); font-size: var(--fs-tiny); }

/* ui-modal-clarity AC10 — Lairs list (semantic <ul>, no <br>). */
#tile-details .t-section.t-lairs { color: var(--color-danger); }
#tile-details .t-section.t-lairs .lair-list {
    list-style: none;
    padding-left: 0;
    margin: 0;
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
}
#tile-details .t-section.t-lairs .lair-item { max-width: 56ch; }
#tile-details .lair-meta { color: #c99; }

/* ui-modal-clarity AC11 — Units list (mine-first, single-line desktop). */
#tile-details .t-section.t-units { display: flex; flex-direction: column; gap: var(--space-1); }
#tile-details .t-unit-row { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; }
#tile-details .t-unit-link {
    display: inline-flex;
    align-items: center;
    gap: var(--space-1);
    text-decoration: none;
}
#tile-details .t-unit-row--mine  .t-unit-link { color: var(--color-primary); }
#tile-details .t-unit-row--other .t-unit-link { color: var(--color-danger); }
#tile-details .t-unit-row .u-ico { font-size: 16px; line-height: 1; }
#tile-details .t-unit-owner { color: var(--text-dim); }
#tile-details .t-unit-rel   { color: var(--rel-color, var(--text-dim)); font-size: var(--fs-tiny); }
#tile-details .t-unit-stats { color: #888; font-size: var(--fs-label); }

/* combat-multi-line-morale TASK_12 — combat-in-progress badge in tile
   panel. Reuses the existing `--load-warn` / `--load-overload` ramp so
   no new design tokens are introduced; data-side flips the colour
   between attacker (A → warn) and defender (D → overload). The badge
   sits inside its own `.t-section.t-combat-badge-section` wrapper so
   the standard `.t-section` margin rhythm applies; the section itself
   is unstyled beyond the badge it contains. */
#tile-details .t-section.t-combat-badge-section {
    display: flex;
    align-items: center;
    gap: var(--space-2);
}
.combat-badge {
    display: inline-flex;
    align-items: center;
    gap: var(--space-1);
    padding: 2px 6px;
    border-radius: 3px;
    background: var(--load-warn);
    color: #1c1c1c;
    font-size: var(--fs-label);
    font-weight: 600;
}
.combat-badge[data-side="D"] { background: var(--load-overload); }
.combat-badge .combat-badge-icon { font-size: 14px; line-height: 1; }
.combat-badge .combat-badge-text { font-family: var(--font-mono, monospace); }

/* ui-modal-clarity AC1 — action helpers utility classes (replacing
   surviving `style="background:..."` literals in _actCombat / _actSpecial /
   _actMovement and the caravan / recruit input width literals). */
.btn-sm.btn-raid  { background: #6e2a2a; }
.btn-sm.btn-pick  { background: #4a6e2a; }
.btn-sm.btn-found { background: #5b3a8a; }
.select-sm.select-auto { width: auto; }
.select-sm.input-coord  { width: 50px; }
.select-sm.input-amount { width: 60px; }
#tile-details .caravan-row { gap: var(--btn-row-gap); }
#tile-details .caravan-label { font-size: var(--fs-label); color: var(--text-dim); }

/* ui-modal-clarity AC1/AC20 — data-driven owner-badge colour via CSS
   custom property. Inline `style="--owner-color:..."` is the only
   surviving inline shape (documented exemption for dynamic values). */
#tile-details .t-owner-name { color: var(--owner-color, var(--text)); }

/* ui-modal-clarity AC17 — heavy-tile <details> disclosure rhythm so the
   summary reads like the regular section title; section margin remains
   intact. */
#tile-details details.t-section > summary {
    list-style: none;
    cursor: pointer;
    user-select: none;
}
#tile-details details.t-section > summary::-webkit-details-marker {
    display: none;
}
#tile-details details.t-section > summary::before {
    content: '▸ ';
    color: var(--text-dim);
    font-size: var(--fs-label);
}
#tile-details details.t-section[open] > summary::before {
    content: '▾ ';
}

/* ui-modal-clarity AC15 — button-row gap (token via TASK_01 mobile
   override). The tap-target floor (AC14) was consolidated into the
   global `.sheet-modal .btn-sm/.select-sm` rule (TASK_11 AC69) so the
   per-panel scope shrinks here. */
@media (max-width: 639px) {
    #tile-details .btn-row {
        gap: var(--btn-row-gap);
    }
}

/* ui-modal-clarity AC12 — sticky-footer CTA drawer for tile-info.
   Inherits `.sheet-actions` shape (sticky, bg, border-top) from the
   ui-modal-panels scaffold; only adds the wrap + token gap so multiple
   priority buttons line-break gracefully on narrow viewports. */
.sheet-actions.t-actions-primary {
    flex-wrap: wrap;
    gap: var(--space-2);
    justify-content: flex-start;
}
.t-cta { min-height: var(--tap-sm); }

/* ─── Unit-panel hero card + stats matrix (ui-modal-clarity TASK_05) ─ */
.u-hero {
    display: grid;
    grid-template-columns: auto 1fr;
    grid-template-rows: auto auto;
    column-gap: var(--space-3);
    row-gap: var(--space-1);
    padding: var(--space-3) 0;
}
.u-hero-icon {
    font-size: 28px;
    line-height: 1;
    grid-row: 1 / span 2;
    align-self: center;
}
.u-hero-text { display: flex; flex-direction: column; gap: var(--space-1); }
.u-hero-title {
    font-size: var(--fs-display);
    font-family: var(--font-display);
    letter-spacing: 0.04em;
    display: flex;
    gap: var(--space-2);
    align-items: baseline;
    flex-wrap: wrap;
}
.u-hero-lvl    { font-size: var(--fs-body);  color: var(--text-dim); }
.u-hero-leader {
    font-size: var(--fs-label);
    color: var(--color-warning);
    padding: 1px var(--space-1);
    border: 1px solid var(--color-warning);
    border-radius: var(--radius-sm);
}
.u-hero-owner { font-size: var(--fs-label); color: var(--text-dim); }
.u-hero-lineage {
    grid-column: 1 / -1;
    list-style: none;
    padding: 0;
    margin: var(--space-2) 0 0;
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
}
.u-hero-lineage li {
    font-size: var(--fs-label);
    color: var(--text-dim);
    white-space: nowrap;
}

.u-stats {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: var(--space-3);
    margin-top: var(--sheet-section-gap);
}
.u-stats-cell { display: flex; flex-direction: column; gap: var(--space-1); }
.u-stats-cell-label {
    font-size: var(--fs-label);
    color: var(--text-muted);
    text-transform: uppercase;
    letter-spacing: 0.5px;
}
.u-stats-cell-body {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
    font-family: var(--font-mono);
}
.u-stats-cell-body .u-stat { white-space: nowrap; }

/* Mobile-narrow collapses 2-col grid to single column. The 380-px
   threshold matches the existing ultra-narrow query for toolbar
   chip-density. */
/* mobile-ux-rework TASK_01 — single-column lists trigger at the
   Mobile-narrow tier (≤479) instead of the legacy 379 cliff. Matches
   the consolidated breakpoint chain in design.md §Breakpoint chain. */
@media (max-width: 479px) {
    .u-stats { grid-template-columns: 1fr; }
}

/* ui-modal-clarity AC20 (partial) — _showLearnable picker rows
   migrated from inline literals to class hooks. TASK_06 sweeps the
   remaining inline-style sites in units_panel.js. */
.u-learnable-back  { margin-bottom: var(--space-1); }
.u-learnable-title { margin-top: var(--sheet-section-gap); }
.u-learnable-row {
    margin: var(--space-1) 0;
    border-bottom: 1px solid var(--border);
    padding-bottom: var(--space-1);
}
.u-learnable-meta { color: var(--text-muted); font-size: var(--fs-tiny); }
.u-learnable-desc { font-size: var(--fs-label); color: var(--text-dim); }

/* ui-modal-clarity AC27/AC28/AC30 — skills / perks / items disclosure.
   ≥ 4 entries → <details>; < 4 → flat <section>. Skills default open,
   perks + items default closed. Section-title rule mirrors the
   <summary> styling so flat and collapsed shapes read identically. */
.u-skills, .u-perks, .u-items {
    margin-top: var(--sheet-section-gap);
}
.u-skills > summary,
.u-perks > summary,
.u-items > summary,
.u-section-title {
    font-size: var(--fs-label);
    color: var(--accent);
    text-transform: uppercase;
    letter-spacing: 0.5px;
    cursor: pointer;
    list-style: none;
    margin-bottom: var(--space-1);
}
.u-skills > summary::-webkit-details-marker,
.u-perks > summary::-webkit-details-marker,
.u-items > summary::-webkit-details-marker {
    display: none;
}
.u-skills > summary::before,
.u-perks > summary::before,
.u-items > summary::before {
    content: '▸ ';
    color: var(--text-dim);
}
.u-skills[open] > summary::before,
.u-perks[open] > summary::before,
.u-items[open] > summary::before {
    content: '▾ ';
}
.u-skills ul, .u-perks ul, .u-items ul {
    list-style: none;
    padding-left: 0;
    margin: var(--space-2) 0 0;
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
}
.u-skills-counter { color: var(--text-dim); font-family: var(--font-mono); margin-left: var(--space-1); }
.u-skill-meta     { color: var(--text-muted); font-size: var(--fs-tiny); }
.u-perk-tag       { color: var(--text-muted); font-size: var(--fs-tiny); }
.u-item-rarity    { color: var(--text-muted); font-size: var(--fs-tiny); }

/* ui-modal-clarity AC25/AC26/AC32 — sticky-footer action drawer.
   Column layout, full-width buttons, label max-width caps long copy
   (`--act-label-max: 24ch`) so a single category never elongates the
   sheet width. */
.sheet-actions.u-actions {
    flex-direction: column;
    gap: var(--space-2);
    align-items: stretch;
    --act-label-max: 24ch;
}
.sheet-actions.u-actions .act-group {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
}
.sheet-actions.u-actions .act-group-head { font-size: var(--fs-label); }
.sheet-actions.u-actions .btn-row {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
}
.sheet-actions.u-actions button {
    max-width: 100%;
    overflow-wrap: anywhere;
    text-align: left;
    min-height: var(--tap-sm);
}
.u-craft-intro { margin-bottom: var(--space-1); }
.u-craft-list  { list-style: disc; margin: 0 0 var(--space-1) var(--space-4); padding: 0; }

/* ─── Armies-panel rewrite (ui-modal-clarity TASK_07) ────────────── */

#armies-list {
    padding: var(--space-2) var(--sheet-pad-inline);
}
#armies-list .a-empty-hint    { line-height: var(--lh-body); }
#armies-list .a-empty-filter  {
    color: var(--text-muted);
    font-size: var(--fs-label);
    padding: var(--space-3) 0;
    text-align: center;
}

/* Filter strip — sticky at top of scrollable list, wraps to 2 lines
   on mobile so chips never compete for the same horizontal budget. */
.a-filters {
    position: sticky;
    top: 0;
    z-index: 1;
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-1);
    background: var(--bg-elev);
    padding: var(--space-2) 0;
    margin-bottom: var(--space-2);
    border-bottom: 1px solid var(--border);
}
.a-filter-group { display: flex; gap: var(--space-1); }

.a-chip {
    background: transparent;
    border: 1px solid var(--border);
    color: var(--text-dim);
    padding: var(--space-1) var(--space-2);
    border-radius: var(--radius-sm);
    cursor: pointer;
    min-height: var(--tap-sm);
    font-size: var(--fs-label);
}
.a-chip[aria-pressed="true"] {
    background: var(--accent-dark);
    color: var(--text);
    border-color: var(--accent);
}
.a-chip:hover { color: var(--text); border-color: var(--border-strong); }

.a-echo {
    color: var(--text-dim);
    font-size: var(--fs-label);
    font-style: italic;
    padding: var(--space-1) 0;
    border-bottom: 1px dashed var(--border);
    margin-bottom: var(--space-2);
}

/* At-rest / in-motion grouping when ≥ 5 armies. */
.a-group { margin-bottom: var(--space-3); }
.a-group > summary {
    cursor: pointer;
    font-size: var(--fs-label);
    color: var(--accent);
    text-transform: uppercase;
    letter-spacing: 0.5px;
    list-style: none;
    margin-bottom: var(--space-1);
}
.a-group > summary::-webkit-details-marker { display: none; }
.a-group > summary::before { content: '▾ '; color: var(--text-dim); }
.a-group:not([open]) > summary::before { content: '▸ '; }

.a-rows { display: flex; flex-direction: column; gap: var(--space-2); }

.a-row {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    padding: var(--space-2) 0;
    border-bottom: 1px solid var(--border);
}
.a-row:last-child { border-bottom: 0; }
.a-row-head {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
}
.a-row-title {
    display: flex;
    align-items: baseline;
    gap: var(--space-2);
    flex-wrap: wrap;
}
.a-row-mode { font-size: var(--fs-h2); }
.a-row-name { font-weight: bold; }
.a-row-flags {
    display: inline-flex;
    gap: var(--space-1);
}
.a-flag {
    font-size: var(--fs-label);
    padding: 0 var(--space-1);
    border-radius: var(--radius-sm);
    background: var(--bg-toolbar);
}
.a-flag-moving { color: var(--color-primary); }
.a-flag-leader { color: var(--color-warning); }
.a-flag-morale { color: var(--color-danger); }

.a-row-meta {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
    align-items: center;
}

.a-row-actions {
    display: flex;
    flex-wrap: wrap;
    gap: var(--btn-row-gap);
    align-items: center;
}
.a-row-cancel { background: #6e2a2a; }
.a-row-overflow { padding-left: var(--space-2); padding-right: var(--space-2); }

@media (max-width: 639px) {
    /* mobile-ux-rework TASK_03 — `--tap-mobile` (48px) replaces the
       legacy desktop-only `--tap-sm` (40px) so AC01's WCAG AAA + iOS
       HIG floor lands across every mobile call site. */
    .a-row-actions .btn-sm { min-height: var(--tap-mobile); }
}

/* mobile-ux-rework TASK_01 — glyph-priority labels at the Mobile-narrow
   tier (≤479) per design.md breakpoint chain (was 380). Filter strip
   collapses to icon-only earlier so 360-class viewports get the
   compact one-line layout instead of wrapping. */
@media (max-width: 479px) {
    /* Drop chip labels (icon-only) so the filter strip fits one line. */
    .a-chip-label { display: none; }
}

.a-row-inventory:empty { display: none; }

/* Popovers — absolute positioning anchored via JS-set --pop-top/left. */
.a-popover {
    position: fixed;
    top: var(--pop-top, 0);
    left: var(--pop-left, 0);
    background: var(--bg-elev);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-sm);
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
    z-index: 50;
    display: flex;
    flex-direction: column;
    min-width: 200px;
    padding: var(--space-1);
    gap: 2px;
}
.a-popover-item {
    background: transparent;
    border: 0;
    color: var(--text);
    text-align: left;
    padding: var(--space-2);
    border-radius: var(--radius-sm);
    cursor: pointer;
    font-size: var(--fs-body);
    min-height: var(--tap-sm);
}
.a-popover-item:hover, .a-popover-item:focus, .a-popover-item:focus-visible, .a-popover-item:active {
    background: rgba(255, 255, 255, 0.06);
    outline: none;
}

/* ─── Empire-panel rewrite (ui-modal-clarity TASK_08) ────────────── */

/* AC46 risk mitigation — bump --sheet-width so the 2-col grid breathes. */
#empire-panel {
    --sheet-width: min(720px, calc(100vw - 32px));
}

/* AC45 — hero band first, status banner absorbs into the same slot. */
.e-hero-band {
    padding: var(--space-3) 0;
    margin-bottom: var(--sheet-section-gap);
    border-bottom: 1px solid var(--border);
}
.e-hero-chips {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-3);
    list-style: none;
    padding: 0;
    margin: 0;
}
.e-hero-chip {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    min-width: 80px;
    padding: var(--space-2);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: rgba(255, 255, 255, 0.02);
}
.e-hero-chip[data-class="warn"]   { border-color: var(--color-warning); }
.e-hero-chip[data-class="danger"] { border-color: var(--color-danger); }
.e-hero-chip-label {
    font-size: var(--fs-label);
    color: var(--text-dim);
    text-transform: uppercase;
    letter-spacing: 0.5px;
}
.e-hero-chip-value {
    font-size: var(--fs-body);
    font-family: var(--font-mono);
    color: var(--text);
}

/* AC49 — banner shell tokenised; .banner-danger / .banner-warning continue
   to set background+colour via the existing utility classes. */
.banner {
    padding: var(--space-3);
    border-radius: var(--radius-sm);
    display: flex;
    align-items: flex-start;
    gap: var(--space-2);
}
.banner-icon { font-size: var(--fs-h2); flex-shrink: 0; }
.banner-body { display: flex; flex-direction: column; gap: var(--space-1); }
.banner-body p { margin: 0; }

/* AC46 — 2-col desktop grid, single-col tablet, mobile reshuffle. */
.e-grid {
    display: grid;
    grid-template-columns: 1fr 320px;
    gap: var(--sheet-section-gap);
}
.e-grid-main, .e-grid-side {
    display: flex;
    flex-direction: column;
    gap: var(--sheet-section-gap);
    min-width: 0;
}
@media (max-width: 1023px) {
    .e-grid { grid-template-columns: 1fr; }
}
/* AC47 — mobile order reshuffle: hero is naturally first via DOM order;
   inside the grid, CTA row jumps above the dense aggregator. */
@media (max-width: 639px) {
    .e-grid { display: contents; }
    .e-grid-main, .e-grid-side { display: contents; }
    .e-cta-row    { order: 2; }
    .e-research   { order: 3; }
    .e-aggregator { order: 4; }
    .e-world      { order: 5; }
    .e-counts     { order: 6; }
}

/* ─── empire-tab-redesign TASK_02 — atlas-card empire-section ───
 * Per-section stack rendered by `views/empire_view_sections.js` into
 * the empire-tab right column. Each section is a canonical
 * `.atlas-card[data-variant="empire-section"]` carrying a collapsible
 * header (title + count chip + show-on-map toggle) and an entity-row
 * list / empty state. T03–T06 wire the data-empire-action hooks.
 * --------------------------------------------------------------- */
.empire-sections {
    display: flex;
    flex-direction: column;
    gap: var(--sheet-section-gap);
    min-width: 0;
}
.atlas-card[data-variant="empire-section"] {
    /* Override the canonical 2-col atlas-card grid: empire-section has
     * no leading glyph slot, so the header + body stack in a single
     * column inside the card padding. */
    grid-template-columns: 1fr;
    column-gap: 0;
    row-gap: var(--space-2);
    padding: var(--space-3);
}
.atlas-card[data-variant="empire-section"] .empire-section-header {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    flex-wrap: wrap;
}
.empire-section-toggle {
    flex: 1 1 auto;
    min-width: 0;
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-1) 0;
    background: transparent;
    border: 0;
    color: var(--ink);
    cursor: pointer;
    font-family: var(--font-display);
    font-weight: 600;
    font-size: 14px;
    text-align: left;
}
.empire-section-toggle[aria-expanded="false"] .empire-section-glyph::before {
    content: '▸ ';
    color: var(--text-dim);
}
.empire-section-toggle[aria-expanded="true"] .empire-section-glyph::before {
    content: '▾ ';
    color: var(--text-dim);
}
.empire-section-glyph {
    display: inline-flex;
    align-items: baseline;
    gap: var(--space-1);
    color: var(--ink-mute);
    font-size: 14px;
}
.empire-section-title {
    flex: 0 1 auto;
    color: var(--ink);
    text-transform: none;
}
.empire-section-count {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 22px;
    height: 18px;
    padding: 0 6px;
    border-radius: var(--radius-md);
    background: var(--surface-sunken);
    color: var(--ink-mute);
    font-family: var(--font-mono);
    font-size: 11px;
    font-variant-numeric: tabular-nums;
}
.empire-section-highlight {
    flex: 0 0 auto;
    border: 1px solid var(--border-soft);
    background: transparent;
    color: var(--ink-mute);
    font-size: 11px;
    padding: 4px 8px;
    border-radius: var(--radius-sm);
    cursor: pointer;
    transition: background 120ms, color 120ms;
}
.empire-section-highlight:hover,
.empire-section-highlight:focus-visible {
    background: var(--surface-sunken);
    color: var(--ink);
}
.empire-section-highlight[aria-pressed="true"] {
    background: var(--accent);
    border-color: var(--accent);
    color: var(--surface-elev);
}
.empire-section-body {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    padding-top: var(--space-1);
    border-top: 1px solid var(--rule);
}
.empire-section-body[hidden] {
    display: none;
}
.empire-entity-list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 2px;
}
.empire-entity-row {
    display: flex;
    align-items: center;
    width: 100%;
}
.empire-entity-btn {
    flex: 1 1 auto;
    min-width: 0;
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--space-2);
    width: 100%;
    padding: 6px 8px;
    background: transparent;
    border: 0;
    border-radius: var(--radius-sm);
    color: var(--ink);
    cursor: pointer;
    text-align: left;
    font-family: var(--font-body);
    font-size: 13px;
}
.empire-entity-btn:hover,
.empire-entity-btn:focus-visible {
    background: var(--surface-sunken);
    outline: none;
}
.empire-entity-name {
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    color: var(--ink);
}
.empire-entity-meta {
    flex: 0 0 auto;
    display: inline-flex;
    gap: var(--space-2);
    color: var(--ink-mute);
    font-family: var(--font-mono);
    font-variant-numeric: tabular-nums;
    font-size: 12px;
}
.empire-entity-loading,
.empire-entity-loading-text {
    color: var(--ink-mute);
    font-size: 12px;
    padding: 6px 8px;
}
.empire-empty {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    padding: var(--space-2);
    color: var(--ink-mute);
    font-style: italic;
    background: var(--surface-sunken);
    border-radius: var(--radius-sm);
}
.empire-empty-text {
    margin: 0;
    font-size: 12px;
    line-height: var(--lh-body);
}
.empire-empty-cta {
    align-self: flex-start;
    padding: 4px 10px;
    border: 1px solid var(--border-soft);
    background: transparent;
    color: var(--accent);
    border-radius: var(--radius-sm);
    cursor: pointer;
    font-size: 12px;
    font-style: normal;
}
.empire-empty-cta:hover,
.empire-empty-cta:focus-visible {
    background: var(--accent);
    color: var(--surface-elev);
    border-color: var(--accent);
}

/* ─── empire-tab-redesign TASK_05 — sticky-mobile slot ───
 * Mount-point inside `#empire-panel` consumed by
 * `views/empire_view_sticky.js::setStickyContent('section', node)`.
 * Mirrors the workspace-shell `.atlas-modal-sticky` pattern from
 * ui-modal-redesign T04 so an expanded empire-section body can be
 * promoted to a fixed bottom shelf without scrolling on <720px
 * viewports. Desktop (≥720px) keeps `display: none` so the section
 * body stays inline inside `.e-grid-side`.
 *
 * The sheet-modal scrolls vertically (overflow-y: auto), so
 * `position: sticky; bottom: 0` keeps the slot pinned to the bottom
 * of the visible scroll viewport; the surrounding `.empire-sections`
 * column scrolls underneath it. `is-empty` collapses the slot to
 * zero height so an unclaimed slot doesn't visually steal space.
 * --------------------------------------------------------------- */
.empire-sticky {
    display: none;                          /* desktop / fallback */
    position: sticky;
    bottom: 0;
    margin: 0;
    padding: 0;
    background: var(--bg-elev);
    border-top: 1px solid var(--border-strong);
    box-shadow: 0 -8px 24px rgba(0, 0, 0, 0.35);
    z-index: 2;                             /* above the empire-body grid */
    max-height: 50dvh;
    overflow-y: auto;
}
.empire-sticky.is-empty {
    display: none;                          /* empty claim collapses */
}
@media (max-width: 719px) {
    .empire-sticky:not(.is-empty) {
        display: block;
    }
    /* Re-skin the promoted body so it reads as a sticky shelf rather
     * than an inline section: tighter top edge, padding inside the
     * slot, and drop the inline `border-top` rule the section body
     * carried in its inline location (the slot owns its own stroke). */
    .empire-sticky > .empire-section-body {
        padding: var(--space-2) var(--space-3);
        border-top: 0;
    }
}

/* Aggregator wrapper — gives compact-mode rest-disclosure a margin. */
.e-aggregator { min-width: 0; }
.e-aggregator-rest {
    margin-top: var(--space-2);
    border-top: 1px dashed var(--border);
    padding-top: var(--space-2);
}
.e-aggregator-rest > summary {
    font-size: var(--fs-label);
    color: var(--accent);
    cursor: pointer;
    list-style: none;
    padding: var(--space-1) 0;
    text-transform: uppercase;
    letter-spacing: 0.5px;
}
.e-aggregator-rest > summary::-webkit-details-marker { display: none; }
.e-aggregator-rest > summary::before { content: '▸ '; color: var(--text-dim); }
.e-aggregator-rest[open] > summary::before { content: '▾ '; }

/* AC51 — counts as semantic <dl>; three groups (territory/forces/consumption). */
.e-counts {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    margin: 0;
}
.e-counts-group { display: flex; flex-direction: column; gap: var(--space-1); }
.e-counts-group dt {
    font-size: var(--fs-label);
    color: var(--text-dim);
    text-transform: uppercase;
    letter-spacing: 0.5px;
    margin: 0;
}
.e-counts-group dd {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
    font-size: var(--fs-body);
    margin: 0;
}
.e-counts-cities { color: var(--text-dim); }
.e-city-link { color: var(--rarity-epic); text-decoration: none; }
.e-city-link:hover { text-decoration: underline; }
.e-counts-leader-warn { font-style: italic; }

/* AC50 — world line replaces the legacy <hr>; border-top supplies the
   separator inline (no extra block element). */
.e-world {
    padding: var(--space-2) 0;
    color: var(--text-dim);
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
    border-top: 1px solid var(--border);
}
.e-world-name { font-weight: bold; color: var(--text); }

/* ─── science-tech-tree TASK_11 (AC27) ─── Active research row.
   Single-button card; clicking opens the tech panel. Reuses the
   `--load-*` ramp via `.load-ok` / `.load-normal` modifier on the
   progress bar so the colour matches the economy-rework token shape. */
.e-research { margin: 0; }
.e-research-btn {
    display: grid;
    grid-template-columns: 1fr;
    gap: var(--space-1);
    width: 100%;
    padding: var(--space-2);
    background: var(--bg-elev);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    color: inherit;
    text-align: left;
    cursor: pointer;
    font: inherit;
}
.e-research-btn:hover,
.e-research-btn:focus-visible {
    border-color: var(--accent);
    outline: none;
}
.e-research-head {
    display: flex;
    align-items: center;
    gap: var(--space-1);
    font-size: var(--fs-label);
    text-transform: uppercase;
    letter-spacing: 0.5px;
    color: var(--text-dim);
}
.e-research-icon { font-size: 1.05em; }
.e-research-name {
    font-weight: bold;
    color: var(--text);
}
.e-research-stats { color: var(--text-dim); }
.e-research-bar {
    display: block;
    height: 6px;
    background: var(--bg);
    border-radius: var(--radius-pill);
    overflow: hidden;
    /* fallback when --research-pct is not provided inline */
    --research-pct: 0%;
}
.e-research-bar-fill {
    display: block;
    height: 100%;
    width: var(--research-pct, 0%);
    background: var(--load-normal);
    transition: width 240ms ease;
}
.e-research-bar.load-ok      .e-research-bar-fill { background: var(--load-ok); }
.e-research-bar.load-normal  .e-research-bar-fill { background: var(--load-normal); }
@media (prefers-reduced-motion: reduce) {
    .e-research-bar-fill { transition: none; }
}

/* ─── world-population-by-race TASK_09 (AC29) ─── Empire-panel race
   composition: per-empire {race_name: count} aggregate from
   /api/my-empire/. Top-5 rows + "+N ще" expander; matches the
   `.e-counts` row rhythm so it lives next to the other side-column
   composition cards. Header reuses the `.e-counts-group dt` label
   style (uppercase + tracked + dim) without copying the rule via
   semantic class composition. */
.empire-races {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    margin: 0;
}
.empire-races-head {
    margin: 0;
    font-size: var(--fs-label);
    color: var(--text-dim);
    text-transform: uppercase;
    letter-spacing: 0.5px;
    font-weight: normal;
}
.empire-races-list,
.empire-races-overflow {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
}
.empire-race-row {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: var(--space-2);
    font-size: var(--fs-body);
}
.empire-race-name { color: var(--text); }
.empire-race-count {
    color: var(--text-dim);
    font-variant-numeric: tabular-nums;
}
.empire-races-expand {
    align-self: flex-start;
    background: transparent;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    color: var(--text-dim);
    font: inherit;
    font-size: var(--fs-label);
    padding: var(--space-1) var(--space-2);
    cursor: pointer;
    min-height: var(--tap-sm);
}
.empire-races-expand:hover,
.empire-races-expand:focus-visible {
    border-color: var(--accent);
    color: var(--text);
    outline: none;
}

/* ─── Activity-panel temporal grouping (ui-modal-clarity TASK_09) ─── */

.activity-pending,
.ev-bucket {
    margin-bottom: var(--space-2);
}
.activity-pending > summary,
.ev-bucket > summary {
    font-size: var(--fs-label);
    color: var(--accent);
    text-transform: uppercase;
    letter-spacing: 0.5px;
    padding: var(--space-2) 0;
    cursor: pointer;
    list-style: none;
}
.activity-pending > summary::-webkit-details-marker,
.ev-bucket > summary::-webkit-details-marker {
    display: none;
}
.activity-pending > summary::before,
.ev-bucket > summary::before {
    content: '▾ ';
    color: var(--text-dim);
}
.activity-pending:not([open]) > summary::before,
.ev-bucket:not([open]) > summary::before {
    content: '▸ ';
}

.ev-rows { display: flex; flex-direction: column; }

/* AC64 — uniform row rhythm: token padding + border-bottom + min tap height. */
.ev-row {
    display: flex;
    align-items: baseline;
    gap: var(--space-2);
    padding: var(--space-1) 0;
    border-bottom: 1px solid var(--border);
    min-height: 28px;
}
.ev-row:last-child { border-bottom: 0; }
.ev-type {
    font-size: var(--fs-body);
    flex-shrink: 0;
    color: var(--accent);
}
.ev-tick { flex-shrink: 0; min-width: 60px; }
.ev-detail { flex: 1; word-break: break-word; }
.ev-empty { line-height: var(--lh-body); }
/* ui-clarity-pass TASK_16 / AC-G3 + AC-G4 — per-row icon prefix on the
 * ev-type column + jump-to (👁) button anchored to the row end. The
 * icon span is `aria-hidden` since the snake_case verb beside it is
 * the textual handle for screen readers. The jump button is a
 * `btn-sm` re-style — kept in sync with the same idiom in commands.js. */
.ev-icon { display: inline-block; min-width: 1.4em; text-align: center; }
.ev-jump { flex-shrink: 0; }

/* Inline-style replacement for the pending-row fill cell. */
.cmd-status-fill { flex: 1; }

/* ─── Leaderboard collapse (ui-modal-clarity AC65) ───────────────── */

.lb-row {
    border-bottom: 1px solid var(--border);
}
.lb-row > summary {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-2) 0;
    cursor: pointer;
    list-style: none;
    min-height: var(--tap-sm);
}
.lb-row > summary::-webkit-details-marker { display: none; }
.lb-rank {
    font-size: var(--fs-label);
    color: var(--text-dim);
    min-width: 24px;
}
.lb-name { flex: 1; }
.lb-score { font-family: var(--font-mono); font-weight: 600; }
.lb-detail {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-3);
    padding: var(--space-1) 0 var(--space-2) 32px;
    font-size: var(--fs-label);
    color: var(--text-dim);
}
.lb-row--mine > summary,
.lb-row--mine > summary .lb-name {
    color: var(--color-primary);
    font-weight: bold;
}
.lb-row--head {
    color: var(--color-primary);
    font-weight: bold;
    border-bottom: 1px solid var(--border-strong);
}
.lb-row--head > .lb-rank, .lb-row--head > .lb-name, .lb-row--head > .lb-score,
.lb-row--head > .lb-detail { padding: var(--space-2) 0; }
.lb-legend { margin-top: var(--space-1); }

/* Desktop ≥481 px: <details> flattens via display:contents, so all
   cells participate in the parent row's grid layout. The summary +
   detail children appear inline as a single grid row. */
@media (min-width: 481px) {
    .lb-row,
    .lb-row--head {
        display: grid;
        grid-template-columns: 24px 1fr auto auto auto auto auto;
        gap: var(--space-2);
        align-items: center;
        padding: var(--space-1) 0;
    }
    .lb-row > summary { display: contents; }
    .lb-row > .lb-detail {
        display: contents;
    }
    .lb-row > .lb-detail > span {
        font-size: var(--fs-body);
        color: inherit;
        padding: 0;
    }
}
#tile-details .btn-row {
    display: flex; gap: 4px; flex-wrap: wrap;
    align-items: center; margin-top: 6px;
}
#tile-details .select-sm {
    background: #111; color: #ccc;
    border: 1px solid var(--border-strong);
    border-radius: 4px;
    padding: 6px 8px;
    font-size: 12px;
    min-height: var(--tap-sm);
}

/* ─── Minimap (desktop only) ───────────────────────────── */
#minimap-container {
    position: absolute;
    bottom: 16px; left: 16px;
    border: 1px solid var(--border-strong);
    border-radius: 4px;
    overflow: hidden;
    z-index: 20;
    box-shadow: 0 4px 16px rgba(0,0,0,0.4);
}
#minimap { width: 200px; height: 140px; background: #111; display: block; }

/* ui-minimap-tactical TASK_04 / AC-15 — mobile drawer toggle.
   Hidden on tablet + desktop; the mobile media query below makes it
   visible at ≤639px. */
#minimap-toggle { display: none; }

/* ─── Minimap tooltip (ui-minimap-tactical TASK_03 / AC-12) ─────────
   Desktop-only hover tooltip surfaced by `installMinimapTooltip` in
   `init.js`. Single re-used `<div id="minimap-tooltip">` element appended
   to `<body>` once on install; `pointer-events: none` so it never steals
   clicks from `#minimap` underneath. Mobile (≤639 px) hides the element
   defensively even if the JS gate via `matchMedia('(pointer: coarse)')`
   fails on a hybrid device. */
#minimap-tooltip {
    position: absolute;
    pointer-events: none;
    background: rgba(15, 25, 50, 0.92);
    color: var(--text);
    font-size: var(--fs-label);
    line-height: 1.2;
    padding: 2px var(--space-2, 6px);
    border: 1px solid var(--border, #333);
    border-radius: var(--radius-sm);
    z-index: 60;
    display: none;
    white-space: nowrap;
}
#minimap-tooltip.is-visible { display: block; }
@media (max-width: 639px) {
    #minimap-tooltip { display: none !important; }
}

/* ─── Command panel ────────────────────────────────────── */
#command-panel select {
    background: #111;
    color: #ccc;
    border: 1px solid #333;
    border-radius: 4px;
    padding: 6px;
    font-size: 13px;
    min-height: var(--tap-sm);
}
/* ui-modal-clarity AC66 — inner-cap removed; sheet-modal max-height
   handles overflow without double-scroll. */
#command-list { font-size: var(--fs-body); max-height: none; }
#command-list .cmd-item {
    padding: 6px 0;
    border-bottom: 1px solid var(--border);
    display: flex;
    justify-content: space-between;
    gap: 8px;
    align-items: center;
}
#command-list .cmd-type { color: var(--accent); }
#command-list .cmd-status { color: var(--safe); font-size: 11px; }

/* ─── Tablet (< 1024px) ────────────────────────────────── */
@media (max-width: 1023px) {
    /* Tablet: every sheet collapses to a single comfortable column. */
    #tile-info, #leaderboard-panel, #transmute-panel, #unit-panel,
    #command-panel, #activity-panel, #armies-panel, #guilds-panel,
    #institutions-panel, #empire-panel {
        --sheet-width: min(420px, calc(100vw - 32px));
    }
    #help-panel, #caravan-modal {
        --sheet-width: min(540px, calc(100vw - 32px));
    }
    /* Minimap smaller */
    #minimap { width: 150px; height: 100px; }
}

/* ─── Bottom nav (mobile only) ─────────────────────────── */
#bottom-nav { display: none; }

@media (max-width: 639px) {
    :root { --bottom-nav-h: 56px; }
    /* ui-clarity-pass TASK_14 (AC-E1) — bottom-nav now 5 slots (was 4):
     * Карта / Армії / Накази / Імперія / Події. Grid template forces
     * equal-width columns even when one button has a longer label so
     * the row doesn't reflow; flex: 1 (the previous v4-slot rule) gave
     * each child its own intrinsic width which made "Імперія" wider
     * than the rest. */
    #bottom-nav {
        display: grid;
        grid-template-columns: repeat(5, 1fr);
        position: fixed;
        left: 0; right: 0; bottom: 0;
        background: var(--bg-toolbar);
        border-top: 1px solid var(--border-strong);
        padding: 4px;
        gap: 2px;
        z-index: 35;
        box-shadow: 0 -4px 12px rgba(0,0,0,0.35);
        /* Respect phone home-bar inset. */
        padding-bottom: calc(4px + env(safe-area-inset-bottom, 0px));
    }
    .bn-btn {
        background: transparent;
        border: none;
        color: var(--text-dim);
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        gap: 2px;
        padding: 6px 2px;
        min-height: 48px;
        min-width: 0;
        border-radius: var(--radius-md);
        cursor: pointer;
        transition: background 120ms, color 120ms;
    }
    .bn-btn { position: relative; }
    /* mobile-ux-rework TASK_04 / AC18 — pair `:hover` (mouse on
     * mobile-emulated desktop) + `:focus-visible` (keyboard nav)
     * with the existing `:active` (touch tap) so every input modality
     * lights up the bottom-nav button. Same fill across the triplet
     * so the colour cue is modality-agnostic. */
    .bn-btn:hover,
    .bn-btn:focus-visible,
    .bn-btn:active {
        background: rgba(136, 204, 255, 0.08);
        color: var(--accent);
    }
    .bn-ico { font-size: 18px; line-height: 1; }
    /* TASK_14 (AC-E1 follow-on): labels must fit 5 slots even at 360px
     * (≈ 64px per button after padding/gap). Truncate with ellipsis
     * rather than wrap so the row stays single-line. `aria-label` on
     * the button still announces the full word for screen readers. */
    /* mobile-ux-rework TASK_08 / AC23 — bn-lbl bumped 10 → 11 px
     * (matches `--fs-label` floor). 11 px still survives the 360 px
     * 5-slot row (≈64 px per slot ⇒ ellipsis-truncated 11 px label
     * fits); 12 px would force the row to wrap. AC23 explicitly
     * carves out this exception. */
    .bn-lbl {
        font-size: var(--fs-label);
        letter-spacing: 0.2px;
        max-width: 100%;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
    }
    /* ui-clarity-deferred TASK_11 (AC11.1): on viewports < 360px the
     * 5th-slot Імперія label collapses to `Карти` so the row stays on
     * one line with ≈64 px per button. The swap is CSS-only — the
     * `<span class="bn-lbl">Імперія</span>` markup in `index.html`
     * stays put (so `aria-label` on the `#bn-empire` button still
     * announces the full word for screen readers); we hide the inner
     * text via `font-size: 0` and paint the collapsed copy through
     * `::after { content: 'Карти' }` so the visible glyph swaps without
     * a JS round-trip. Sits ABOVE the `≤380px display:none` rule so
     * the icon-only fallback at the narrowest tier keeps winning.
     * Keep the @media query literal `(max-width: 359px)` + the
     * `Карти` literal at the same site — the AC11.1 sentinel grep
     * pins both substrings in the same file. */
    @media (max-width: 359px) {
        #bn-empire .bn-lbl { font-size: 0; }
        #bn-empire .bn-lbl::after {
            content: 'Карти';
            /* mobile-ux-rework TASK_08 / AC23 — Карти painted at the
             * --fs-label floor so the bn-lbl exception applies
             * uniformly across the 5-slot row. */
            font-size: var(--fs-label);
            letter-spacing: 0.2px;
        }
    }
    /* mobile-ux-rework / TASK_01 + spec AC07 — icon-only bottom-nav
     * triggers at the Mobile-tight tier (≤359) instead of the legacy
     * 380-cliff. Pixel 4a / Galaxy S10e (360 px wide) keep the labels;
     * iPhone SE (320 px) and 1st-gen narrow phones still drop them.
     * aria-label announces the slot when the label is hidden. */
    @media (max-width: 359px) {
        .bn-lbl { display: none; }
        .bn-btn { padding: 8px 2px; }
    }
    /* Unread/pending count overlay in the top-right of the button.
     * `:empty` hides it automatically when badges.js clears the
     * count — no display:none flag to toggle. */
    .bn-badge {
        position: absolute;
        top: 2px; right: 10%;
        min-width: 18px; height: 18px;
        padding: 0 5px;
        border-radius: var(--radius-pill);
        background: var(--accent-dark);
        color: #fff;
        /* mobile-ux-rework TASK_08 / AC23 — badge counter on the
         * --fs-label floor (11 px) so the AC23 sentinel can pin "no
         * mobile text below `--fs-label` (11 px) outside the bn-lbl
         * tight-row exception". */
        font-size: var(--fs-label);
        font-weight: 600;
        line-height: 18px;
        text-align: center;
        box-shadow: 0 1px 3px rgba(0,0,0,0.5);
    }
    .bn-badge:empty { display: none; }
    /* Keep canvas out from behind the bottom bar. */
    #hex-canvas {
        height: calc(100% - var(--toolbar-h) - var(--bottom-nav-h));
    }
    /* Panel drawers stop above the bottom nav so both are usable. */
    .panel {
        bottom: var(--bottom-nav-h) !important;
        max-height: calc(85vh - var(--bottom-nav-h));
        max-height: calc(85dvh - var(--bottom-nav-h));
    }
}

/* ─── Mobile (< 640px) ─────────────────────────────────── */
@media (max-width: 639px) {
    :root { --toolbar-h: 44px; }

    /* mobile-ux-rework TASK_08 / AC25 — body-copy line-height floor
     * lifts to `--lh-body-mobile` (1.45 — TASK_01 token) on phones so
     * prose surfaces stay above the AC25 1.4 threshold for iOS
     * legibility. Body cascades into every panel that doesn't override
     * line-height locally; per-component rules with `line-height: 1`
     * (icon glyphs) or `line-height: 1.2` (tight chips, single-line
     * inputs) keep their tight values because they're not body copy.
     * Desktop body line-height stays at `--lh-body` (1.55) — set on
     * `body` itself near the top of this file. */
    body {
        line-height: var(--lh-body-mobile);
    }

    /* Toolbar becomes tight: title + essential chips + hamburger.
       mobile-ux-rework TASK_09 / AC19 — notched iPhones (iPhone X+ /
       Dynamic Island devices) report a non-zero `safe-area-inset-top`
       so the OS chrome (notch / island / camera punch-out) does not
       occlude the hamburger button + chip row. The bottom inset is
       already covered by `#bottom-nav` (AC20 — `padding-bottom: calc(4px
       + env(safe-area-inset-bottom))`). The fallback is the `--safe-top`
       token which itself reads `env(safe-area-inset-top, 0px)` so
       desktop / tablet / non-notched phones see no padding bump. */
    #toolbar { padding: calc(4px + var(--safe-top)) 8px 4px; gap: 6px; }
    #toolbar h1 { font-size: 14px; }
    /* mobile-ux-rework TASK_08 / AC23 — info-chip bumped 11 → 12 px
     * to clear the iOS legibility floor (no mobile text below 12 px,
     * exception: bn-lbl/bn-badge stay at 11 px because the 360-px-wide
     * 5-slot bottom-nav cannot carry a 12 px ellipsised label without
     * breaking the single-line row contract — see AC23 spec wording). */
    .info-chip { font-size: 12px; padding: 2px 6px; }
    /* No hover on touch — the "Наведіть на клітинку" placeholder takes
     * toolbar space for zero value, and even when it shows real coords
     * you just saw what you tapped. Hide on mobile; desktop keeps it.
     * `_setCoordsDisplay` still writes into the span in case the user
     * resizes to a wider viewport mid-session. */
    #coords-display { display: none; }
    /* Player-info chip is redundant on mobile — username is in the
     * empire panel and the hamburger has a logout button. Toolbar needs
     * horizontal budget for title + ws-dot + tick + resources + ☰. */
    #player-info { display: none; }

    /* Collapse all control buttons behind a ☰ toggle.
       mobile-ux-rework TASK_03 — `--tap-mobile` swaps in for the
       legacy `--tap-sm` so the hamburger toggle clears the AC01
       48 px floor on phones (was 40 px). */
    .ctrl-menu-btn {
        display: inline-block;
        background: rgba(255,255,255,0.05);
        border: 1px solid var(--border-strong);
        color: var(--text);
        border-radius: 4px;
        min-width: var(--tap);
        min-height: var(--tap-mobile);
        font-size: 20px;
        line-height: 1;
        cursor: pointer;
    }
    /* mobile-ux-rework TASK_05 / AC26 — the legacy `.ctrl-menu.open
       .toolbar-ctrl-group` absolute popover (right-anchored, ~160 px
       dropdown that pushed the lower category groups off-screen on
       landscape phones) retired here. The off-canvas drawer in
       `frontend/static/js/mobile/drawer.js` reparents the same
       `.toolbar-ctrl-group` element into `.hw-drawer-body` at install
       time, so on mobile the group lives inside the drawer and gets
       a full-width vertical layout via the `.hw-drawer .toolbar-ctrl-group`
       rule below. Pre-reparent (first-paint flash before
       `installMobileDrawer()` runs from `init.js::mountGameScreen`)
       the group is hidden so the legacy popover never flashes. */
    .toolbar-ctrl-group {
        display: none;
    }
    .hw-drawer .toolbar-ctrl-group {
        display: flex;
        flex-direction: column;
        align-items: stretch;
        gap: 4px;
        padding: 0;
        background: transparent;
        border: 0;
        box-shadow: none;
        position: static;
        min-width: 0;
        width: 100%;
    }
    .hw-drawer .toolbar-ctrl-group .btn-sm {
        text-align: left;
        width: 100%;
    }

    /* mobile-ux-rework TASK_05 / AC26 — off-canvas hamburger drawer.
       Slides in from the right via `transform: translateX(100%) → 0`
       over 220 ms `cubic-bezier(.22,.94,.42,1)` (matches the
       workspace-shell motion budget). The `.is-open` class is added
       by `mobile/drawer.js::openDrawer()` (synced with `aria-hidden`
       state for SR users). Backdrop is a separate element so its
       opacity transition can run independently of the drawer slide.
       Z-index 70 sits above sheet modals (46+ stack) and the bottom-
       nav (50) so the drawer always paints on top — pairs with the
       focus trap inside the drawer body so background panels can't
       capture taps while the drawer is open. */
    .hw-drawer {
        display: flex;
        flex-direction: column;
        position: fixed;
        top: 0;
        right: 0;
        bottom: 0;
        width: var(--drawer-w);
        max-width: 90vw;
        transform: translateX(100%);
        transition: transform 220ms cubic-bezier(.22, .94, .42, 1);
        z-index: 70;
        padding: var(--safe-top) 0 var(--safe-bottom);
        background: var(--bg-toolbar, var(--bg-elev, #2a1f17));
        border-left: 1px solid var(--border-strong);
        box-shadow: -8px 0 32px rgba(0, 0, 0, 0.5);
        overflow-y: auto;
        overscroll-behavior-y: contain;
    }
    .hw-drawer.is-open { transform: translateX(0); }
    .hw-drawer-head {
        display: flex;
        align-items: center;
        justify-content: space-between;
        padding: 12px 14px;
        border-bottom: 1px solid var(--border);
        position: sticky;
        top: 0;
        background: inherit;
        z-index: 1;
    }
    .hw-drawer-head h3 {
        margin: 0;
        font-size: 14px;
        font-weight: 600;
        text-transform: uppercase;
        letter-spacing: 0.5px;
        color: var(--text);
    }
    .hw-drawer-close {
        background: transparent;
        border: 1px solid var(--border-strong);
        color: var(--text);
        border-radius: var(--radius-sm);
        min-width: var(--tap-mobile);
        min-height: var(--tap-mobile);
        cursor: pointer;
        display: inline-flex;
        align-items: center;
        justify-content: center;
        touch-action: manipulation;
    }
    .hw-drawer-body {
        display: flex;
        flex-direction: column;
        gap: 4px;
        padding: 8px 14px 14px;
        flex: 1 1 auto;
    }
    .hw-drawer-backdrop {
        display: block;
        position: fixed;
        inset: 0;
        background: rgba(0, 0, 0, 0.4);
        z-index: 65;
        opacity: 0;
        transition: opacity 180ms ease-out;
        pointer-events: none;
    }
    .hw-drawer-backdrop.is-open {
        opacity: 1;
        pointer-events: auto;
    }
    .hw-drawer-backdrop.hidden { display: none; }
    /* Reduced-motion: drop the slide-in / fade-in transitions so the
       drawer + backdrop appear instantly on toggle. The `.is-open`
       class still flips so the visual end-state is identical. */
    .force-reduced-motion .hw-drawer,
    .force-reduced-motion .hw-drawer-backdrop {
        transition: none !important;
    }

    /* Every panel becomes a full-screen drawer from the bottom.
     * Bottom padding reserves space equal to #bottom-nav height so the
     * last item in a scrollable panel (e.g. help-panel's "Зрозуміло"
     * dismiss button) isn't covered by the fixed nav bar and can be
     * reliably tapped. Playwright caught this: the nav's bn-btn subtree
     * was intercepting pointer events for the dismiss click. */
    .panel {
        position: fixed !important;
        left: 0 !important; right: 0 !important;
        top: auto !important; bottom: 0 !important;
        width: 100% !important;
        max-width: none !important;
        transform: none !important;
        max-height: 85vh;
        max-height: 85dvh;
        border-radius: var(--radius) var(--radius) 0 0;
        border-left: none; border-right: none; border-bottom: none;
        padding: 14px 14px calc(22px + var(--bottom-nav-h));
    }
    #help-panel {
        max-height: 92vh;
        max-height: 92dvh;
    }

    /* ui-minimap-tactical TASK_04 / AC-15 — mobile drawer surface.
       Replaces the legacy `display: none` hide with a bottom-anchored
       sheet that the player toggles via `#minimap-toggle`. Anchored
       above `#bottom-nav` (z-index 40 — above canvas, below
       bottom-nav at 50, below open panels at 60 / sheets at 46+).
       Off-screen default via a `translateY(calc(100% + 16px))`
       transform so the slide-in shows the full drawer; `.is-open`
       flips to `translateY(0)`. 220 ms `cubic-bezier(.22,.94,.42,1)`
       transition matches the `views/workspace_shell.js` motion
       budget. The `prefers-reduced-motion` clause at the bottom of
       this file disables the transition for users who opt out. */
    /* mobile-ux-rework TASK_09 / AC20 — both `#minimap-container` (the
       slide-up sheet) and `#minimap-toggle` (the floating round trigger
       above #bottom-nav) are `position: fixed` with a `bottom:` anchor,
       so the AC20 audit pins safe-area-inset-bottom on every fixed-bottom
       surface. The bottom-nav itself already adds `padding-bottom: calc(
       4px + env(safe-area-inset-bottom, 0px))` so the home-indicator
       region inside the nav is reserved; lifting the toggle + drawer
       further up by `var(--safe-bottom)` keeps them visually stacked
       above the bottom-nav top edge on phones that report a non-zero
       inset (iPhone X+ home-indicator phones / Android-3-button phones
       both report 0 here, so this is a no-op for the latter). */
    #minimap-container {
        display: block;
        position: fixed;
        bottom: calc(var(--bottom-nav-h) + var(--safe-bottom) + 8px);
        right: 8px;
        left: auto;
        top: auto;
        z-index: 40;
        transform: translateY(calc(100% + 16px));
        transition: transform 220ms cubic-bezier(.22,.94,.42,1);
    }
    #minimap-container.is-open {
        transform: translateY(0);
    }
    #minimap-toggle {
        display: inline-flex;
        align-items: center;
        justify-content: center;
        position: fixed;
        bottom: calc(var(--bottom-nav-h) + var(--safe-bottom) + 8px);
        right: 8px;
        z-index: 41;
        min-width: var(--tap);
        min-height: var(--tap);
        width: var(--tap);
        height: var(--tap);
        padding: 0;
        border-radius: 50%;
        border: 1px solid var(--border-strong);
        background: var(--bg-toolbar, rgba(15, 25, 50, 0.92));
        color: var(--text);
        font-size: 18px;
        line-height: 1;
        cursor: pointer;
        box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);
    }

    /* Bigger tap targets for tile action buttons.
       mobile-ux-rework TASK_03 — `38px` literals retire onto
       `var(--tap-mobile)` (48 px) per AC03 ("38 px literal in
       `#tile-details .btn-sm` is replaced"). 12 px font-size stays
       (already meets the AC23 ≥12 px floor). */
    #tile-details .btn-sm { min-height: var(--tap-mobile); padding: 8px 12px; font-size: 12px; }
    #tile-details .select-sm { min-height: var(--tap-mobile); }

    /* mobile-ux-rework TASK_10 / AC11 — long-press context menu on the
       canvas. Module: `frontend/static/js/mobile/longpress_menu.js`.
       Floating absolutely-positioned menu host + transparent backdrop
       sibling. Width consumes `--ctx-menu-min-w` (TASK_01 token, 240 px)
       so the design ladder stays single-source. Z-index sits above
       sheet modals (46) + bottom-nav (50) but below the drawer (70) so
       a player who opens the drawer mid-menu sees the drawer drop on
       top (drawer's own backdrop dims the menu). The backdrop owns
       click-outside dismiss; CSS `pointer-events: none` on the
       hidden state keeps the menu from intercepting canvas pan/zoom
       gestures while invisible. */
    .hw-ctx-menu {
        position: fixed;
        z-index: 60;
        min-width: var(--ctx-menu-min-w);
        background: var(--surface-elev);
        border: 1px solid var(--border-firm);
        border-radius: var(--radius);
        box-shadow: 0 6px 24px rgba(0, 0, 0, 0.45);
        padding: 6px;
        display: flex;
        flex-direction: column;
        gap: 2px;
        opacity: 0;
        transform: scale(0.96);
        transition: opacity 140ms ease-out, transform 140ms ease-out;
        pointer-events: none;
    }
    .hw-ctx-menu.is-open {
        opacity: 1;
        transform: scale(1);
        pointer-events: auto;
    }
    .hw-ctx-menu[hidden] { display: none; }
    .hw-ctx-menu-backdrop {
        position: fixed;
        inset: 0;
        z-index: 59;
        background: transparent;
        pointer-events: none;
    }
    .hw-ctx-menu-backdrop.is-open {
        pointer-events: auto;
    }
    .hw-ctx-menu-backdrop[hidden] { display: none; }
    .hw-ctx-menu-item {
        display: flex;
        align-items: center;
        gap: 12px;
        padding: 10px 14px;
        min-height: var(--tap-mobile);
        background: transparent;
        border: none;
        border-radius: var(--radius-sm);
        color: var(--text);
        font-family: var(--font-body);
        font-size: var(--fs-body);
        text-align: left;
        cursor: pointer;
        touch-action: manipulation;
    }
    .hw-ctx-menu-item:hover,
    .hw-ctx-menu-item:focus-visible,
    .hw-ctx-menu-item:active {
        background: rgba(200, 146, 61, 0.10);
        color: var(--accent);
    }
    .hw-ctx-menu-item:active {
        transform: scale(var(--press-scale));
    }
    .hw-ctx-menu-glyph {
        font-size: 18px;
        line-height: 1;
        width: 24px;
        text-align: center;
    }
    .hw-ctx-menu-lbl {
        flex: 1 1 auto;
    }

    /* (legacy stockpile-chip mobile wrap rule was dropped along with
       the desktop block per economy-rework AC45.) */
}

/* mobile-ux-rework TASK_01 — Mobile-tight tier (≤359) per design.md.
   Toolbar drops the brand h1 and chips truncate hardest at this size.
   Was 380; realigned to 359 to leave Pixel 4a / S10e (360 px) on the
   Mobile-narrow contract. */
@media (max-width: 359px) {
    #toolbar h1 { display: none; }  /* save horizontal space */
    .info-chip { max-width: 140px; text-overflow: ellipsis; }
}

/* ─── Reduced-motion respect ───────────────────────────── */
@media (prefers-reduced-motion: reduce) {
    .btn, .btn-sm { transition: none; }
    #context-banner { animation: none; }
    /* ui-minimap-tactical TASK_04 / AC-15 — reduced-motion override
       for the mobile drawer transition. Instant show/hide instead
       of the 220 ms slide-in. */
    #minimap-container { transition: none; }
    /* mobile-ux-rework TASK_05 / AC26 — same instant flip for the
       hamburger drawer + its backdrop. The `.is-open` class still
       toggles, only the slide-in / fade-in transition is dropped. */
    .hw-drawer { transition: none; }
    .hw-drawer-backdrop { transition: none; }
    /* mobile-ux-rework TASK_10 / AC11 — same instant flip for the
       long-press context menu. Drop the 140 ms scale + opacity
       transition; the `.is-open` class still flips so end-state is
       identical. The vibrate(8) call inside `longpress_menu.js` is
       already gated by reduced-motion via the matchMedia +
       `force-reduced-motion` classlist check. */
    .hw-ctx-menu { transition: none; }
}

/* ─── Toast feedback system (ui-toast-feedback) ─────────── */
#toast-container {
    position: fixed;
    bottom: 16px;
    right: 16px;
    display: flex;
    flex-direction: column;
    gap: 8px;
    max-width: 360px;
    z-index: 1000;
    pointer-events: none;
}
#toast-container .toast {
    pointer-events: auto;
    background: var(--bg-elev);
    border: 1px solid var(--border-strong);
    border-left: 3px solid var(--color-primary);
    border-radius: var(--radius-sm);
    padding: 10px 12px;
    color: var(--text);
    font-size: var(--fs-body);
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
    display: flex;
    align-items: center;
    gap: 8px;
    animation: toast-slide-in 180ms ease-out;
}
#toast-container .toast.toast-leaving { opacity: 0; transition: opacity 120ms ease-out; }
#toast-container .toast-success { border-left-color: var(--color-success); }
#toast-container .toast-error   { border-left-color: var(--color-danger); }
#toast-container .toast-warning { border-left-color: var(--color-warning); }
#toast-container .toast-info    { border-left-color: var(--color-primary); }
#toast-container .toast-msg { flex: 1; min-width: 0; }
#toast-container .toast-count {
    font-size: var(--fs-label);
    color: var(--text-muted);
    padding: 0 4px;
}
#toast-container .toast-action {
    background: transparent;
    border: 1px solid var(--color-primary);
    color: var(--color-primary);
    padding: 3px 8px;
    border-radius: var(--radius-sm);
    cursor: pointer;
    font-size: var(--fs-label);
}
#toast-container .toast-action:hover { background: rgba(90, 166, 255, 0.12); }
#toast-container .toast-close {
    background: transparent;
    border: none;
    color: var(--text-muted);
    cursor: pointer;
    font-size: 14px;
    padding: 0 2px;
    line-height: 1;
}
#toast-container .toast-close:hover { color: var(--text); }

@keyframes toast-slide-in {
    from { transform: translateY(8px); opacity: 0; }
    to   { transform: translateY(0);    opacity: 1; }
}

/* Button spinner lock. Used by toast.lockBtn during in-flight POSTs. */
.btn-locked {
    position: relative;
    pointer-events: none;
    opacity: 0.6;
}
.btn-locked::after {
    content: '';
    position: absolute;
    right: 6px;
    top: 50%;
    width: 10px;
    height: 10px;
    margin-top: -5px;
    border: 2px solid rgba(255, 255, 255, 0.25);
    border-top-color: var(--color-primary);
    border-radius: 50%;
    animation: btn-spin 600ms linear infinite;
}
@keyframes btn-spin {
    to { transform: rotate(360deg); }
}

/* Disabled-button hover popover (shows [data-disable-reason] text). */
.disable-popover {
    position: absolute;
    background: var(--bg-elev);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-sm);
    padding: 6px 10px;
    color: var(--text);
    font-size: var(--fs-label);
    max-width: 320px;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);
    z-index: 1100;
    pointer-events: none;
}

/* mobile-ux-rework TASK_09 / AC31 — toast container flips from bottom-
   anchor to top-anchor on mobile so toasts don't get buried under the
   bottom-nav + on-screen keyboard. The legacy mobile rule placed the
   container above the bottom-nav + safe-area, but iOS Safari + Android
   Chrome both raise the keyboard *up to* 280–360 px depending on the
   keyboard mode (autocorrect bar + emoji picker pile on top), occluding
   any toast within that band even with the visualViewport tracking from
   TASK_02 — the keyboard simply paints over the area where the toast
   used to live. Top-anchor (under the toolbar, above the chip row)
   sidesteps this entirely: toasts always paint where the player is
   already looking (toolbar attention) and the keyboard never reaches
   that high. The `top:` value uses a triple-`calc()` stack:
   `var(--toolbar-h)` (44 px on mobile per the existing override) +
   `var(--safe-top)` (notch / dynamic-island clearance, env() inset on
   iPhone X+) + 8 px gutter so the toast sits visually under the toolbar
   bottom edge. The `bottom:` is reset to `auto` to release the legacy
   anchor cleanly. The `toast-slide-in` keyframe (`translateY(8px → 0)`)
   stays correct on a top-anchored host because it's a fade-up-into-
   place gesture which reads identically whether anchored top or bottom. */
@media (max-width: 639px) {
    #toast-container {
        left: 8px;
        right: 8px;
        top: calc(var(--toolbar-h) + var(--safe-top) + 8px);
        bottom: auto;
        max-width: none;
    }
}

/* Respect prefers-reduced-motion for all toast/button animations. */
@media (prefers-reduced-motion: reduce) {
    #toast-container .toast,
    .toast-leaving { animation: none; transition: none; }
    .btn-locked::after { animation: none; }
}

/* ─── Army canvas tooltip (ui-army-canvas-glyphs) ────────── */
#army-tooltip {
    position: fixed;
    z-index: 500;
    background: rgba(26, 30, 50, 0.95);
    border: 1px solid var(--color-primary);
    border-radius: 4px;
    padding: 6px 10px;
    font-size: var(--fs-label);
    color: #fff;
    pointer-events: none;
    max-width: 240px;
    white-space: pre-wrap;
}

/* ─── Tile panel disclosure (ui-tile-panel-disclosure) ────── */
.tile-actions-sticky {
    position: sticky;
    bottom: 0;
    background: var(--bg-elev);
    padding: 8px 0 4px 0;
    margin-top: 8px;
    border-top: 1px solid rgba(136, 204, 255, 0.15);
    z-index: 2;
}
#tile-info.panel {
    max-height: 75vh;
    overflow-y: auto;
    scrollbar-gutter: stable;
}
@media (max-width: 639px) {
    #tile-info.panel {
        max-height: 85vh;
        max-height: 85dvh;
    }
}

/* ─── Toolbar groups (ui-toolbar-regroup) ───────────────── */
.tb-group {
    display: inline-flex;
    gap: 4px;
    align-items: center;
}
.tb-divider {
    display: inline-block;
    width: 1px;
    height: 18px;
    background: var(--border-strong);
    margin: 0 2px;
}
/* ui-clarity-pass TASK_14 (AC-E2) — `<details data-hamburger-group>`
 * wraps every `.tb-group`. Desktop hides the `<summary>` caret + keeps
 * every group inline (the toolbar IS the group divider — collapsing
 * groups makes no sense at 1440px). Mobile shows the caret in the
 * dropdown drawer so the player can collapse a category to declutter.
 * `<details>` natively keeps content visible while open, hidden when
 * closed — no JS toggle required, just open-state persistence in
 * init.js (`localStorage.hw.hamburger.<key>`). */
.tb-details {
    display: inline-flex;
    align-items: center;
}
.tb-details > .tb-summary { display: none; }
.tb-details > .tb-group { display: inline-flex; }
@media (max-width: 639px) {
    /* Hamburger drawer: each `<details>` becomes a stacked block with
     * a clickable summary header (caret), collapsible body. Default-
     * open (set by index.html `open` attr); user clicks toggle the
     * `.open` state and init.js persists it under
     * `localStorage.hw.hamburger.<key>`. */
    .tb-details {
        display: block;
        width: 100%;
        border-bottom: 1px solid var(--border);
    }
    .tb-details:last-child { border-bottom: 0; }
    .tb-details > .tb-summary {
        display: list-item;
        list-style: disclosure-closed inside;
        padding: 8px 4px;
        font-size: 12px;
        font-weight: 600;
        color: var(--text-dim);
        cursor: pointer;
        user-select: none;
        text-transform: uppercase;
        letter-spacing: 0.5px;
    }
    /* mobile-ux-rework TASK_04 / AC18 — `:hover` / `:focus-visible` /
     * `:active` triplet on the hamburger-drawer category summary so
     * keyboard, mouse, and touch each surface the same darken-on-
     * interaction cue. Matches the `.tb-details[open] > .tb-summary`
     * "active category" colour so the hover/focus state is a preview
     * of what the toggle will commit to. */
    .tb-details > .tb-summary:hover,
    .tb-details > .tb-summary:focus-visible,
    .tb-details > .tb-summary:active {
        color: var(--text);
    }
    .tb-details[open] > .tb-summary {
        list-style: disclosure-open inside;
        color: var(--text);
    }
    .tb-details > .tb-group {
        display: flex;
        flex-wrap: wrap;
        width: 100%;
        padding: 4px 0 8px 12px;
    }
    .tb-divider { display: none; }
    /* Reduced-motion users: drop the smooth open/close transition so
     * the caret rotation + content reveal land instantly. `<details>`
     * has no built-in animation in any current browser, but the
     * future spec adds one — pre-empt the override here. */
    .force-reduced-motion .tb-details > .tb-summary,
    .force-reduced-motion .tb-details > .tb-group {
        transition: none !important;
    }
}

/* ─── Settings menu (ui-toolbar-regroup TASK_04) ────────── */
/* Width override now flows through `--sheet-width` (TASK_007) so
 * `.sheet-modal` rules can centre the card on desktop without the
 * legacy `width: 340px` id-rule winning over the class. */
#settings-menu { --sheet-width: min(340px, calc(100vw - 32px)); }
.settings-body { padding: 4px 0; }
.settings-user {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    padding: 6px 0;
    border-bottom: 1px solid var(--border);
    margin-bottom: 8px;
}
.settings-opt {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 8px 0;
    font-size: var(--fs-body);
    cursor: pointer;
}
.settings-opt input[type="checkbox"] { transform: scale(1.1); }
.settings-opt select {
    margin-left: auto;
    background: #222;
    border: 1px solid #444;
    color: var(--text);
    padding: 4px 8px;
    border-radius: var(--radius-sm);
}
.settings-opt input[disabled] + span { color: var(--text-muted); }

/* Forced reduced-motion via settings toggle: overrides OS preference
   so users without reduce-motion set system-wide can still opt in. */
html.force-reduced-motion *,
html.force-reduced-motion *::before,
html.force-reduced-motion *::after {
    animation-duration: 0s !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0s !important;
}

/* ─── Confirm modal (ui-toolbar-regroup TASK_05) ────────── */
.modal {
    position: fixed;
    inset: 0;
    z-index: 1200;
    display: flex;
    align-items: center;
    justify-content: center;
}
.modal.hidden { display: none; }
.modal-backdrop {
    position: absolute;
    inset: 0;
    background: rgba(0, 0, 0, 0.55);
}
.modal-body {
    position: relative;
    background: var(--bg-elev);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius);
    padding: 20px 24px;
    max-width: 360px;
    width: calc(100% - 32px);
    color: var(--text);
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
}
.modal-body h3 {
    font-size: var(--fs-h2);
    margin-bottom: 8px;
}
.modal-body p {
    color: var(--text-dim);
    font-size: var(--fs-body);
    margin-bottom: 16px;
}
.modal-actions {
    display: flex;
    justify-content: flex-end;
    gap: 8px;
}
.btn-danger {
    background: var(--color-danger);
    color: #fff;
    border: 1px solid #b32;
}
.btn-danger:hover, .btn-danger:focus-visible, .btn-danger:active {
    background: #e55a5a;
}

/* mobile-ux-rework TASK_09 / AC20 — generic `.modal-body` (used by the
   context-warning-modal centred dialog) reserves the bottom inset on
   phones so its action row clears the home-indicator on notched
   devices. The host `.modal` itself is `position: fixed; inset: 0` (a
   centred flex host, not a bottom-anchored surface), but the AC20
   audit list explicitly carves out `ContextWarning modal` so the action
   buttons stay tappable when the device is held with the home indicator
   close to the modal's bottom edge. The bump pads only on phones; the
   centred-dialog visual stays unchanged on desktop / tablet. */
@media (max-width: 639px) {
    .modal-body {
        padding-bottom: calc(20px + var(--safe-bottom));
    }
}

/* ─── Mobile drag-handle on panels (ui-mobile-touch-baseline TASK_02) ── */
@media (max-width: 639px) {
    .panel::before {
        content: '';
        display: block;
        width: 40px;
        height: 4px;
        background: #555;
        border-radius: 2px;
        margin: 8px auto;
        touch-action: pan-y;
    }
}

/* ─── Stacked form (ui-mobile-touch-baseline TASK_03+06) ─── */
.stacked-form {
    display: flex;
    flex-direction: column;
    gap: 10px;
}
.stacked-form label {
    display: flex;
    flex-direction: column;
    gap: 4px;
    font-size: var(--fs-label);
    color: var(--text-dim);
}
.stacked-form input,
.stacked-form select {
    width: 100%;
    padding: 10px;
    font-size: 16px;  /* avoid iOS zoom */
    background: #1a1d24;
    color: #ddd;
    border: 1px solid #333;
    border-radius: var(--radius-sm);
    min-height: var(--tap);
}
.form-row { display: flex; gap: 6px; }
.form-row input { flex: 1; }
.form-actions {
    display: flex;
    justify-content: flex-end;
    gap: 8px;
    margin-top: 8px;
}

/* ─── Landscape context-banner (ui-mobile-touch-baseline TASK_04) ── */
@media (orientation: landscape) and (max-height: 500px) {
    #context-banner {
        top: auto;
        bottom: calc(var(--bottom-nav-h, 56px) + env(safe-area-inset-bottom) + 8px);
        left: 8px;
        right: 8px;
    }
}

/* mobile-ux-rework TASK_09 / AC22 — landscape phones (≤ 500 px tall +
   landscape orientation = the iPhone-rotated-sideways tier; iPad portrait
   stays > 500 px tall and is unaffected) demote vertical chrome so the
   reduced viewport height carries the canvas + workspace body without
   suffocating under fixed bars. Three demotions per AC22:

   1. `#bottom-nav` height drops 56 → 44 px and labels collapse to glyph-
      only (`.bn-lbl { display: none }` matches the Mobile-tight 359 px
      rule pattern). The `--bottom-nav-h` token is rewritten so every
      consumer that calculates against it (panels' `bottom: var(--bottom
      -nav-h) !important`, `#toast-container` top-anchor, minimap toggle's
      `calc()` stack, workspace CTA-bar safe-bottom padding) re-resolves
      to the slim height in one place.

   2. `#help-panel` max-height tightens 92 dvh → 80 dvh so the help sheet
      doesn't span the entire short viewport (the player needs to see
      *something* of the canvas behind the help to keep their bearings).
      The legacy 92 dvh rule a few hundred lines earlier (`@media (max-
      width: 639px)` block) is overridden here for the landscape sub-tier.

   3. PathPlanner overlay swaps from a bottom-sheet to a side-pane
      (anchored to the right edge, full-height minus toolbar + bottom-nav,
      ≈ min(360 px, 50 vw) wide) so the planner sits *next to* the canvas
      instead of *under* it. The `body.hw-pp-open` workspace shell rule
      that collapses to a 30 dvh half-sheet at `<640 px` is unsuitable
      for landscape (30 dvh of 360 px = 108 px — too tight for the
      planner controls); the side-pane respects the canvas-above-planner
      hierarchy that AC22 explicitly wants on landscape phones. */
@media (orientation: landscape) and (max-height: 500px) {
    :root { --bottom-nav-h: 44px; }
    #bottom-nav .bn-lbl { display: none; }
    #bottom-nav .bn-btn { padding: 4px 2px; }
    #help-panel {
        max-height: 80vh;
        max-height: 80dvh;
    }
    /* PathPlanner side-pane: floating overlay (the standalone path-
       planner-overlay rule, NOT the body.hw-pp-open workspace
       half-sheet) anchors to the right edge and runs full-height
       between toolbar + bottom-nav. */
    .path-planner-overlay:not(.atlas-modal-sticky *) {
        top: calc(var(--toolbar-h) + var(--safe-top) + 8px);
        bottom: calc(var(--bottom-nav-h, 44px) + var(--safe-bottom) + 8px);
        right: 8px;
        left: auto;
        width: min(360px, 50vw);
        max-height: none;
    }
    /* The workspace half-sheet that body.hw-pp-open normally drops to
       30 dvh on portrait gets cancelled here so the planner side-pane
       owns the right-edge real estate alone — workspace shell stays
       in its full landscape layout behind the canvas. */
    body.hw-pp-open #workspace.workspace {
        display: none;
    }
}

/* ─── Tap :active feedback (ui-mobile-touch-baseline TASK_07) ── */
.btn-sm:active,
.bn-btn:active,
.ctrl-menu-btn:active,
.btn-primary:active,
.btn-danger:active {
    transform: scale(0.97);
    transition: transform 60ms ease-out, background 60ms ease-out;
}
.btn-sm:active    { background: rgba(90, 166, 255, 0.15); }
.btn-primary:active { background: rgba(90, 166, 255, 0.7); }
.btn-danger:active  { background: rgba(255, 107, 107, 0.7); }

@media (prefers-reduced-motion: reduce) {
    .btn-sm:active,
    .bn-btn:active,
    .ctrl-menu-btn:active,
    .btn-primary:active,
    .btn-danger:active {
        transform: none;
        transition: background 60ms ease-out;
    }
}
html.force-reduced-motion .btn-sm:active,
html.force-reduced-motion .bn-btn:active,
html.force-reduced-motion .ctrl-menu-btn:active,
html.force-reduced-motion .btn-primary:active,
html.force-reduced-motion .btn-danger:active {
    transform: none;
}

/* ─── mobile-ux-rework / TASK_04 — :active press feedback + AC32
 *    `touch-action: manipulation` (consolidated mobile sweep) ───────
 *
 * AC15 — every interactive element on phones gets a visible `:active`
 * state. The legacy `ui-mobile-touch-baseline TASK_07` block above
 * covers `.btn-sm` / `.btn-primary` / `.btn-danger` / `.bn-btn` /
 * `.ctrl-menu-btn` only — this block extends the press feedback to
 * the rest of the AC18 selector list (`.btn`, `.btn-secondary`,
 * `.atlas-card[data-variant]`, `.tb-summary`, `.help-tab`,
 * `.a-popover-item`, `.lb-name-btn`, `.atlas-tabs > button[role="tab"]`)
 * via the new `--press-scale` token (0.98 — softer than the legacy
 * 0.97 to keep the existing block's identity while extending coverage).
 *
 * AC32 — `touch-action: manipulation` on every interactive surface
 * suppresses the legacy 300 ms tap-delay + double-tap-to-zoom on
 * older WebKit. Single base rule on the canonical interactive
 * selector list per design.md ("Single root rule on `button,
 * [role="button"], .atlas-card, .info-chip, .tb-summary, .help-tab,
 * .bn-btn`").
 *
 * Reduced-motion users get the colour cue without the transform —
 * the `prefers-reduced-motion: reduce` block at the bottom suppresses
 * the scale across the wider selector list in one place.
 */
@media (max-width: 639px) {
    /* AC32 — base touch-action sweep. Keeps double-tap zoom + the
     * 300 ms FastClick-era delay off the canonical interactive
     * surfaces. The form-control sweep (`input/textarea/select`)
     * landed in TASK_03; this block covers the button/role-button
     * + card/chip/summary/tab/bottom-nav-button surfaces. */
    button,
    [role="button"],
    .atlas-card,
    .info-chip,
    .tb-summary,
    .help-tab,
    .bn-btn {
        touch-action: manipulation;
    }

    /* AC15 — `:active` press scale on the rest of the AC18 selector
     * list. Reduced-motion users keep the colour cue but lose the
     * transform (handled by the `prefers-reduced-motion: reduce`
     * block below + the existing `html.force-reduced-motion`
     * override). */
    .btn:active,
    .btn-secondary:active,
    .atlas-card[data-variant]:active,
    .tb-summary:active,
    .help-tab:active,
    .a-popover-item:active,
    .lb-name-btn:active,
    .atlas-tabs > button[role="tab"]:active,
    .atlas-tabs > .atlas-tab:active {
        transform: scale(var(--press-scale));
        transition: transform 60ms ease-out, background-color 60ms ease-out;
    }
}

@media (max-width: 639px) and (prefers-reduced-motion: reduce) {
    .btn:active,
    .btn-secondary:active,
    .atlas-card[data-variant]:active,
    .tb-summary:active,
    .help-tab:active,
    .a-popover-item:active,
    .lb-name-btn:active,
    .atlas-tabs > button[role="tab"]:active,
    .atlas-tabs > .atlas-tab:active {
        transform: none;
        transition: background-color 60ms ease-out;
    }
}
html.force-reduced-motion .btn:active,
html.force-reduced-motion .btn-secondary:active,
html.force-reduced-motion .atlas-card[data-variant]:active,
html.force-reduced-motion .tb-summary:active,
html.force-reduced-motion .help-tab:active,
html.force-reduced-motion .a-popover-item:active,
html.force-reduced-motion .lb-name-btn:active,
html.force-reduced-motion .atlas-tabs > button[role="tab"]:active,
html.force-reduced-motion .atlas-tabs > .atlas-tab:active {
    transform: none;
}

/* ─── Army mode accordion (ui-mobile-touch-baseline TASK_05) ── */
.army-mode-accordion {
    font-size: var(--fs-label);
    border: 1px solid rgba(136, 204, 255, 0.15);
    border-radius: 3px;
    padding: 2px 6px;
}
.army-mode-accordion summary {
    cursor: pointer;
    list-style: none;
    user-select: none;
    padding: 6px 0;
}
.army-mode-accordion summary::-webkit-details-marker { display: none; }
.army-mode-accordion[open] summary { color: var(--color-primary); }
.army-mode-accordion button {
    display: block;
    width: 100%;
    text-align: left;
    margin: 4px 0;
}

/* ─── Recruit/Build accordion on mobile (ui-mobile-touch-baseline TASK_06) ── */
.action-accordion {
    font-size: var(--fs-label);
    border: 1px solid rgba(136, 204, 255, 0.15);
    border-radius: var(--radius-sm);
    margin: 6px 0;
    padding: 4px 8px;
}
.action-accordion summary {
    cursor: pointer;
    list-style: none;
    user-select: none;
    padding: 8px 0;
    font-size: var(--fs-body);
}
.action-accordion summary::-webkit-details-marker { display: none; }
.action-accordion summary::after {
    content: ' ▾';
    color: var(--text-dim);
    font-size: var(--fs-label);
}
.action-accordion[open] summary { color: var(--color-primary); }
.action-accordion[open] summary::after { content: ' ▴'; }
.action-accordion .stacked-form { padding: 8px 0; }

/* ─── WS disconnect overlay (ui-ws-disconnect-overlay TASK_02) ─────────
   Banner sits at top-centre of #game-screen. Mask sits over the canvas
   only — toolbar (z-index 1000+), panels, and #toast-container stay
   above and remain interactive (read-only inspection during disconnect).
   #ws-failure-modal reuses the .modal/.modal-backdrop classes from
   ui-toolbar-regroup TASK_05 (z-index 1200) so the failure escalation
   stacks above the banner without new layout. Mask uses pointer-events:
   none so canvas clicks still pan/select; only command POSTs fail
   silently (existing 4xx + toast.error path). */
.ws-overlay {
    position: absolute;
    top: 8px;
    left: 50%;
    transform: translateX(-50%);
    z-index: 900;
    background: rgba(40, 20, 20, 0.92);
    color: #ffd0d0;
    padding: 8px 14px;
    border-radius: var(--radius-sm);
    font-weight: 600;
    border: 1px solid rgba(255, 100, 100, 0.4);
    display: flex;
    align-items: center;
    gap: 6px;
    pointer-events: auto;
    animation: ws-overlay-in 180ms ease-out;
}
.ws-overlay.hidden { display: none; }
.ws-overlay.flash-success {
    background: rgba(20, 60, 30, 0.92);
    color: #c8f2c8;
    border-color: rgba(120, 220, 140, 0.4);
}
.ws-overlay.no-anim { animation: none; }
.ws-overlay-icon { font-size: 14px; line-height: 1; }
.ws-overlay-mask {
    position: absolute;
    inset: 0;
    background: rgba(0, 0, 0, 0.35);
    pointer-events: none;
    z-index: 850;
}
.ws-overlay-mask.hidden { display: none; }
@keyframes ws-overlay-in {
    from { opacity: 0; transform: translate(-50%, -8px); }
    to   { opacity: 1; transform: translate(-50%, 0); }
}
@media (prefers-reduced-motion: reduce) {
    .ws-overlay { animation: none; }
}
html.force-reduced-motion .ws-overlay { animation: none; }

/* ─── Onboarding card (ui-onboarding-objectives) ───────────────────────
 * First-session 3-step objectives card sits top-right above the canvas
 * on desktop, full-width band at the top on mobile. Visibility entirely
 * gated on localStorage.hw.onboardingStage by onboarding.js — markup is
 * always in the DOM but `.hidden` keeps it out of the layout for
 * existing-player sessions. Z-index 800 sits below the toolbar (1000)
 * and above the canvas; mirrors `.ws-overlay`'s placement convention. */
.onboarding-card {
    position: absolute;
    top: 56px;
    right: 12px;
    max-width: 280px;
    z-index: 800;
    background: rgba(20, 25, 40, 0.94);
    border: 1px solid var(--border, #335);
    border-radius: var(--radius-md);
    padding: 10px 12px;
    color: var(--text, #ddd);
    box-shadow: 0 4px 14px rgba(0, 0, 0, 0.4);
    animation: onboarding-card-in 200ms ease-out;
}
.onboarding-card.hidden { display: none; }
.onboarding-head {
    display: flex;
    justify-content: space-between;
    align-items: center;
}
.onboarding-head h4 { margin: 0; font-size: 13px; }
.onboarding-dismiss {
    background: transparent;
    border: 0;
    color: var(--text-muted, #889);
    font-size: 16px;
    cursor: pointer;
    line-height: 1;
    padding: 0 4px;
}
.onboarding-steps {
    margin: 6px 0 0;
    padding-left: 0;
    list-style: none;
    font-size: 12px;
    line-height: 1.6;
}
.onboarding-step-done {
    color: var(--text-muted, #889);
    text-decoration: line-through;
}
.onboarding-mark { display: inline-block; width: 1.2em; }
@keyframes onboarding-card-in {
    from { opacity: 0; transform: translateY(-6px); }
    to   { opacity: 1; transform: translateY(0); }
}
@media (max-width: 639px) {
    .onboarding-card { left: 8px; right: 8px; top: 60px; max-width: none; }
}
@media (prefers-reduced-motion: reduce) {
    .onboarding-card { animation: none; }
}
html.force-reduced-motion .onboarding-card { animation: none; }

/* === Onboarding hints (onboarding-portal-quest-flow / TASK_04) ===
 * Just-in-time `<div class="ob-hint">` overlays for the 17-step
 * tutorial. Lives next to the anchored UI element in document order;
 * absolute positioning + z-index 400 keeps it above panels (which sit
 * around 200) but below the toolbar (1000) and the WS overlay (800).
 * Body text is static manifest copy from `onboarding_steps.js::
 * STEP_MANIFEST`; the dismiss button is a real `<button>` for
 * keyboard accessibility (Tab + Enter, AC-B4). */
.ob-hint {
    position: absolute;
    z-index: 400;
    background: rgba(20, 25, 40, 0.96);
    border: 1px solid var(--accent, #88ccff);
    border-radius: var(--radius-md, 6px);
    padding: 10px 14px;
    max-width: 240px;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
    pointer-events: auto;
    color: var(--text, #ddd);
    animation: ob-hint-in 200ms ease-out;
}
.ob-hint-body {
    margin: 0 0 8px;
    font-size: 0.85rem;
    line-height: 1.4;
}
.ob-hint-dismiss {
    font-size: 0.78rem;
    cursor: pointer;
    color: var(--accent, #88ccff);
    background: none;
    border: 0;
    padding: 0;
    text-decoration: underline;
    min-height: var(--tap-sm, 36px);
}
@keyframes ob-hint-in {
    from { opacity: 0; transform: translateY(-4px); }
    to   { opacity: 1; transform: translateY(0); }
}
@media (prefers-reduced-motion: reduce) {
    .ob-hint { animation: none; }
}
html.force-reduced-motion .ob-hint { animation: none; }

/* `🏠 Home` tooltip pinned above the spawn tile for 6s on first paint
 * (ui-onboarding-objectives TASK_03). Lives at document.body level so
 * it's not affected by any panel z-context; positioned with absolute
 * left/top set inline by onboarding.js. */
.onboarding-home-tip {
    position: fixed;
    transform: translate(-50%, -100%);
    background: rgba(20, 25, 40, 0.94);
    color: var(--text, #ddd);
    border: 1px solid #ffd75a;
    border-radius: 4px;
    padding: 4px 8px;
    font-size: 12px;
    font-weight: 600;
    pointer-events: none;
    z-index: 850;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
    animation: onboarding-home-tip-in 200ms ease-out;
}
@keyframes onboarding-home-tip-in {
    from { opacity: 0; transform: translate(-50%, -90%); }
    to   { opacity: 1; transform: translate(-50%, -100%); }
}
@media (prefers-reduced-motion: reduce) {
    .onboarding-home-tip { animation: none; }
}
html.force-reduced-motion .onboarding-home-tip { animation: none; }

/* ─── Empire CTAs (ui-empire-dashboard-ctas) ───────────────────────────
 * Severity-ordered contextual CTAs rendered into `#empire-body` footer
 * by `_showEmpire`. Top-3 buttons render inline; rest collapse behind a
 * `.cta-more` toggle that flips `.cta-overflow.hidden`. Severity ramp
 * maps to three muted accent borders (high/med/low) so the panel stays
 * readable in the existing dashboard greyscale palette without flashy
 * solid fills. `.armies-row.flash` is the per-row preselect feedback
 * from the `army-no-leader` action — 1.5s ease-out animation, with a
 * reduced-motion guard that swaps the keyframe for a static border. */
.cta-row {
    margin-top: 10px;
    padding-top: 8px;
    border-top: 1px solid #333;
    display: flex;
    flex-direction: column;
    gap: 4px;
}
.cta-btn {
    display: block;
    width: 100%;
    margin: 0;
    padding: 8px 10px;
    text-align: left;
    background: rgba(40, 50, 70, 0.5);
    color: var(--text, #ddd);
    border: 1px solid #444;
    border-left-width: 3px;
    border-radius: 4px;
    font-size: 12px;
    cursor: pointer;
}
.cta-btn:hover { background: rgba(60, 75, 100, 0.6); }
.cta-btn.sev-high { border-left-color: var(--color-danger); }
.cta-btn.sev-med  { border-left-color: #ffb95a; }
.cta-btn.sev-low  { border-left-color: var(--lapis); }
.cta-allclear {
    padding: 8px;
    text-align: center;
    font-size: 12px;
    line-height: 1.4;
}
.cta-more {
    align-self: flex-start;
    background: transparent;
    border: 0;
    color: var(--text-muted, #889);
    font-size: 11px;
    cursor: pointer;
    padding: 4px 0;
    text-decoration: underline;
}
.cta-overflow {
    display: flex;
    flex-direction: column;
    gap: 4px;
}
.cta-overflow.hidden { display: none; }

/* Armies-row flash on `army-no-leader` preselect (ui-empire-dashboard-ctas
 * TASK_03). Keeps the action discoverable even when the player isn't
 * looking at the row at the moment the panel opens. */
.armies-row.flash {
    animation: cta-flash 1.5s ease-out;
}
@keyframes cta-flash {
    0%   { background: rgba(255, 200, 80, 0.35); }
    100% { background: transparent; }
}
@media (prefers-reduced-motion: reduce) {
    .armies-row.flash {
        animation: none;
        outline: 2px solid rgba(255, 200, 80, 0.6);
    }
}
html.force-reduced-motion .armies-row.flash {
    animation: none;
    outline: 2px solid rgba(255, 200, 80, 0.6);
}

/* ─── Toolbar activity badge dot (ui-activity-feed-bridge TASK_06) ─────
 * Red pill next to the `Події` label inside `#btn-activity` — count
 * mirrors the mobile bottom-nav `.bn-badge` exactly. Inline-block so it
 * collapses naturally when the count is zero (badges.js writes
 * `'Події'` plain in that case). 99+ overflow handled at the JS layer. */
.badge-dot {
    display: inline-block;
    margin-left: 4px;
    padding: 0 6px;
    min-width: 18px;
    height: 16px;
    line-height: 16px;
    background: var(--color-danger, #ff6b6b);
    color: #fff;
    border-radius: var(--radius-pill);
    font-size: 10px;
    font-weight: 700;
    text-align: center;
    vertical-align: 1px;
}

/* ─── Activity auto-open banner (ui-activity-feed-bridge TASK_05) ──────
 * One-shot `Ось що сталося` notice rendered above the pending group on
 * the first-unseen auto-open. Cleared by the consumer in dashboard_panels.js
 * so a follow-up render (tick re-open, manual re-open) doesn't repeat. */
.activity-banner {
    margin-bottom: 8px;
    padding: 6px 10px;
    background: rgba(255, 183, 0, 0.12);
    border: 1px solid rgba(255, 183, 0, 0.35);
    border-radius: 4px;
    color: var(--color-warning, #ffb700);
    font-size: var(--fs-label, 11px);
    font-weight: 600;
}

/* ─── Activity pending group (ui-activity-feed-bridge TASK_01) ─────────
 * Collapsible `Очікує (N)` section above the events stream in the
 * activity panel. Reads the synchronous _pendingCommands cache, no
 * fetch. Empty cache → not rendered (no "Очікує (0)" shell). */
.activity-pending {
    margin-bottom: 8px;
    padding-bottom: 6px;
    border-bottom: 1px dashed var(--border-strong, #3a3a4a);
}
.activity-pending > summary {
    cursor: pointer;
    color: var(--text-dim, #9aa);
    font-size: var(--fs-label, 11px);
    font-weight: 600;
    padding: 2px 0;
    list-style: none;
    user-select: none;
}
.activity-pending > summary::-webkit-details-marker { display: none; }
.activity-pending .pending-row {
    display: flex;
    align-items: center;
    gap: 6px;
    padding: 4px 0;
    border-bottom: 1px solid var(--border, #2a2a3a);
}
.activity-pending .pending-row:last-of-type { border-bottom: 0; }
.activity-pending .pending-row .cmd-type { color: var(--color-warning, #ffb700); }
.activity-pending .pending-cancel {
    margin-left: auto;
    padding: 2px 8px;
    min-height: var(--tap-sm, 40px);
}

/* ─── Event drama modal (ui-activity-feed-bridge TASK_03) ──────────────
 * Sibling of #game-screen, NOT a panel — bypasses the panels.js Esc
 * priority chain. Owns its own scrim + centred card. Click anywhere on
 * the scrim or card dismisses; event_drama.js wires the listeners.
 * Animation: scale-from-0.8 + fade-in 200ms ease-out. Reduced-motion
 * (OS query OR `html.force-reduced-motion` opt-in) strips both keyframe
 * and transition so the modal appears instantly at full scale. */
.event-drama {
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, 0.55);
    display: flex;
    align-items: center;
    justify-content: center;
    z-index: 1100;
    /* ui-clarity-pass TASK_16 / AC-G2 — click-anywhere-to-dismiss
     * removed; the explicit ✕ button is the only click path now. The
     * pointer cursor used to advertise the dismiss behaviour, kept on
     * the close button only so the rest of the scrim no longer hints
     * at a click affordance the player can't actually use. */
    cursor: default;
}
.event-drama-card {
    position: relative;
    background: var(--bg-elev, rgba(20, 20, 40, 0.96));
    border: 1px solid var(--border-strong, #3a3a4a);
    border-radius: var(--radius, 8px);
    padding: 20px 28px;
    min-width: 240px;
    max-width: min(480px, calc(100vw - 32px));
    text-align: center;
    color: var(--text, #e0e0e0);
    box-shadow: 0 12px 40px rgba(0, 0, 0, 0.6);
    animation: event-drama-in 200ms ease-out;
    outline: none;
}
/* ui-clarity-pass TASK_16 / AC-G2 — explicit ✕ close button. Anchored
 * top-right of the card. ≥36 px tap target per --tap-sm. */
.event-drama-close {
    position: absolute;
    top: 4px;
    right: 4px;
    min-width: 36px;
    min-height: 36px;
    background: transparent;
    border: none;
    color: var(--text-dim, #9aa);
    font-size: 18px;
    line-height: 1;
    cursor: pointer;
    border-radius: var(--radius, 8px);
    padding: 6px 10px;
}
.event-drama-close:hover,
.event-drama-close:focus-visible {
    color: var(--text, #e0e0e0);
    background: rgba(255, 255, 255, 0.05);
    outline: none;
}
.event-drama-headline {
    font-size: var(--fs-h2, 16px);
    font-weight: 700;
    margin-bottom: 6px;
    line-height: 1.3;
}
.event-drama-summary {
    font-size: var(--fs-body, 13px);
    color: var(--text-dim, #9aa);
    line-height: 1.5;
}
@keyframes event-drama-in {
    from { opacity: 0; transform: scale(0.8); }
    to   { opacity: 1; transform: scale(1); }
}
@media (prefers-reduced-motion: reduce) {
    .event-drama-card { animation: none; transition: none; }
}
html.force-reduced-motion .event-drama-card {
    animation: none;
    transition: none;
}

/* ─── Sheet-modal scaffold (ui-modal-panels TASK_001) ───────────
 * Centered card on desktop with backdrop scrim, full-width
 * bottom-sheet on mobile with rounded top + drag handle. Markup
 * still carries `class="panel"` until the per-batch migration
 * (TASK_003-005) flips each panel; until then no panel renders
 * via these rules. Tokens live on `:root`-ish scope here so
 * any future sheet variant can override per-instance via the
 * cascade.
 *
 * z-index slots: backdrop 45, sheet 46. Above panel (30),
 * below modal (1100) and toast (1200) so the existing
 * logout/ws-failure/context-warning modals keep priority.
 */
.sheet-modal {
    --sheet-z-backdrop: 45;
    --sheet-z-sheet: 46;
    --sheet-scrim: rgba(0, 0, 0, 0.45);
    --sheet-width: min(720px, calc(100vw - 64px));
    --sheet-radius-mobile: 16px;
    /* ui-modal-clarity AC54 — semantic-spacing tokens scoped to .sheet-modal.
       Per-modal CSS can stop hard-coding 12/16/8 px literals and read these
       instead. Mobile override below bumps --btn-row-gap and tightens
       --sheet-section-gap for 360-px viewports (tap-spacing safety). */
    --sheet-pad-block:    var(--space-3);
    --sheet-pad-inline:   var(--space-4);
    --sheet-section-gap:  var(--space-4);
    --sheet-row-gap:      var(--space-2);
    --btn-row-gap:        var(--space-1);
    position: fixed;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    width: var(--sheet-width);
    max-height: 85vh;
    max-height: 85dvh;
    overflow-y: auto;
    background: var(--bg-elev);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius);
    box-shadow: 0 16px 48px rgba(0, 0, 0, 0.5);
    z-index: var(--sheet-z-sheet);
    padding: 0;
    display: flex;
    flex-direction: column;
    transition: opacity 180ms ease-out, transform 180ms ease-out;
    opacity: 1;
}
.sheet-modal.hidden {
    display: none;
}
/* Auto body-padding for panels that don't wrap content in `.sheet-body`.
 * Most panels render content into a raw `<div id="…-body">`, so without
 * this rule the body sits flush with the card edges. Excludes the sticky
 * header/handle/actions (which manage their own full-width padding) and
 * `.sheet-body` (which already has its own). */
.sheet-modal > :not(.sheet-handle):not(.sheet-header):not(.sheet-actions):not(.sheet-body) {
    /* ui-modal-clarity AC61 — token-driven body inset. */
    padding-left: var(--sheet-pad-inline);
    padding-right: var(--sheet-pad-inline);
}
.sheet-modal > :last-child:not(.sheet-handle):not(.sheet-header):not(.sheet-actions):not(.sheet-body) {
    padding-bottom: var(--sheet-pad-inline);
}
.sheet-backdrop {
    position: fixed;
    inset: 0;
    background: var(--sheet-scrim);
    z-index: var(--sheet-z-backdrop);
    cursor: pointer;
    transition: opacity 180ms ease-out;
    opacity: 1;
}
.sheet-backdrop.hidden {
    display: none;
}
.sheet-handle {
    display: none;  /* mobile-only affordance — flipped on at ≤639px */
    width: 36px;
    height: 4px;
    margin: 8px auto 4px;
    background: var(--border-strong);
    border-radius: 2px;
    flex-shrink: 0;
    cursor: grab;
    touch-action: none;
}
.sheet-handle:active {
    cursor: grabbing;
}

/* ui-modal-clarity AC67/AC73 — drag-handle pulse hint on the first 3
   sheet opens per device. JS controller `maybeShowHandleHint` in
   panels.js toggles `.is-hint-pulse` + spawns the microcopy span,
   then localStorage caps total shows at 3. Reduced-motion drops both
   to instant (no animation, no transition). */
.sheet-handle.is-hint-pulse {
    animation: handle-pulse 1.6s ease-out 1;
    transform-origin: center;
}
@keyframes handle-pulse {
    0%   { transform: scaleX(1);    opacity: 1;   }
    50%  { transform: scaleX(1.15); opacity: 0.8; }
    100% { transform: scaleX(1);    opacity: 1;   }
}
.sheet-hint-microcopy {
    position: absolute;
    top: 18px;
    left: 50%;
    transform: translateX(-50%);
    font-size: var(--fs-label);
    color: var(--text-dim);
    background: var(--bg-elev);
    padding: var(--space-1) var(--space-2);
    border-radius: var(--radius-sm);
    border: 1px solid var(--border);
    pointer-events: none;
    opacity: 1;
    transition: opacity 200ms ease-out;
    z-index: 3;
    white-space: nowrap;
}
.sheet-hint-microcopy.is-hidden { opacity: 0; }
@media (prefers-reduced-motion: reduce) {
    .sheet-handle.is-hint-pulse { animation: none; }
    .sheet-hint-microcopy { transition: none; }
}
html.force-reduced-motion .sheet-handle.is-hint-pulse { animation: none; }
html.force-reduced-motion .sheet-hint-microcopy { transition: none; }
.sheet-header {
    position: sticky;
    top: 0;
    background: var(--bg-elev);
    /* ui-modal-clarity AC60 — token-driven header rhythm. */
    padding: var(--space-3) var(--sheet-pad-inline) var(--space-2);
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    z-index: 1;
    border-bottom: 1px solid transparent;
}
.sheet-header h3 {
    color: var(--accent);
    font-size: 15px;
    margin: 0;
    /* economy-rework T21 (AC51) — Cinzel display font for panel
       headers gives the empire / inventory / aggregator sheets the
       "ancient atlas" aesthetic design.md "Visual direction" calls
       for. Letter-spacing widens the small-caps glyphs slightly so
       the header reads at panel-title scale even though Cinzel is
       a smaller-x-height face. */
    font-family: var(--font-display);
    letter-spacing: 0.04em;
}
.sheet-modal[data-scrolled="1"] .sheet-header {
    border-bottom-color: var(--border-soft, rgba(255,255,255,0.06));
}
.sheet-body {
    padding: 0 16px 16px;
    overflow-y: visible;
    flex: 1 1 auto;
    min-height: 0;
}
/* ui-modal-clarity AC68 — global sticky-actions rule. Padding + gap +
   z-index migrated onto tokens; safe-area-inset-bottom landed in the
   mobile media query below. Per-panel class modifiers (`.t-actions-
   primary`, `.u-actions`) layer their own flex/order tweaks on top. */
.sheet-actions {
    position: sticky;
    bottom: 0;
    background: var(--bg-elev);
    padding: var(--space-3) var(--sheet-pad-inline);
    display: flex;
    gap: var(--space-2);
    justify-content: flex-end;
    border-top: 1px solid var(--border-soft, rgba(255,255,255,0.06));
    z-index: 2;
}
@media (max-width: 639px) {
    .sheet-modal .sheet-actions {
        padding-bottom: calc(var(--space-2) + env(safe-area-inset-bottom));
    }
    /* ui-modal-clarity AC69 — global mobile tap-target floor across
       every modal that contains a `.btn-sm` or `.select-sm` element.
       Supersedes per-panel mobile rules from TASK_03.
       mobile-ux-rework TASK_03 — `--tap-mobile` (48 px) replaces the
       desktop-only `--tap-sm` so the AC01 ladder applies inside every
       sheet-modal at the WCAG AAA touch-target floor. */
    .sheet-modal .btn-sm,
    .sheet-modal .select-sm {
        min-height: var(--tap-mobile);
    }
}
.sheet-close {
    background: transparent;
    border: 1px solid var(--border-strong);
    color: var(--text-dim);
    border-radius: 4px;
    padding: 0;
    font-size: 14px;
    line-height: 1;
    cursor: pointer;
    min-height: var(--tap-sm);
    min-width: var(--tap-sm);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: 0 0 auto;
}
.sheet-close:hover, .sheet-close:active {
    background: rgba(255, 255, 255, 0.07);
    color: var(--accent);
    border-color: var(--accent);
}
@media (max-width: 639px) {
    .sheet-modal {
        --sheet-width: 100vw;
        /* ui-modal-clarity AC54 — mobile rhythm overrides. Larger
           --btn-row-gap protects against accidental fat-finger taps;
           tighter --sheet-section-gap reclaims vertical real estate
           on 360-px viewports without losing readability. */
        --btn-row-gap:       var(--space-2);
        --sheet-section-gap: var(--space-3);
        top: auto;
        left: 0;
        right: 0;
        bottom: 0;
        width: 100%;
        max-width: 100%;
        max-height: 92vh;
        max-height: 92dvh;
        transform: none;
        border-radius: var(--sheet-radius-mobile) var(--sheet-radius-mobile) 0 0;
        padding-bottom: max(16px, env(safe-area-inset-bottom));
        transform-origin: bottom;
        transition: transform 240ms ease-out, opacity 240ms ease-out;
    }
    .sheet-modal.hidden {
        display: none;
    }
    .sheet-handle {
        display: block;
    }
    .sheet-header {
        /* ui-modal-clarity AC60 — handle already provides top breathing room. */
        padding-top: var(--space-1);
    }
    /* mobile-ux-rework / TASK_06 (AC12) — `overscroll-behavior-y:
       contain` on the sheet body so the browser's native pull-to-
       refresh (Chrome on Android + Safari rubber-band on iOS) does
       NOT fire while the in-app `wireSheetGestures` close / refresh
       gesture is being recognised. The containment also stops a
       fast scroll inside the body from chaining into the document
       background. Applied to `.sheet-modal` itself for panels that
       render content directly under the root and to the inner
       `.sheet-body` wrapper for panels that use it. */
    .sheet-modal,
    .sheet-modal .sheet-body {
        overscroll-behavior-y: contain;
    }
    /* mobile-ux-rework / TASK_02 (AC21) — keyboard-aware sheet sizing.
       `body.keyboard-open` is flipped by `mobile/visual_viewport.js`
       when `window.visualViewport.height` shrinks more than ~80 px
       below `window.innerHeight` (the on-screen keyboard overlay).
       `--vv-h` is the live visual-viewport height in px (set on
       `<html>`); the panel max-height shrinks to clear the keyboard
       so the last interactive row sits above the keyboard. Falls
       back to `100dvh` on legacy browsers without `visualViewport`
       (the variable is undefined → the `var(...)` fallback wins). */
    body.keyboard-open .sheet-modal {
        max-height: calc(var(--vv-h, 100dvh) - 16px);
    }
    /* mobile-ux-rework / TASK_11 (AC13) — pull-to-refresh spinner on
     * roster scroll containers (ArmiesView list / ArmyView Склад /
     * TileView Лагер). The host element gets the spinner inserted as
     * its first child by `panels.js::wirePullToRefresh`; the SVG
     * circle is rotated + faded proportionally during the drag
     * (transform / opacity inline-styled per `dy`), then flipped
     * onto `.is-loading` while the caller's onRefresh promise is
     * pending — the dasharray-spin keyframe paints during that
     * window. Reduced-motion users get the colour cue without the
     * spin (animation suppressed below). 32 × 32 px per spec.
     *
     * Hosts opt-in via the `data-pull-refresh` data attribute so
     * native browser pull-to-refresh + Safari rubber-band stay
     * suppressed under the in-app gesture (matches the AC12
     * `.sheet-modal` precedent). */
    [data-pull-refresh] {
        overscroll-behavior-y: contain;
    }
    .pull-refresh-spinner {
        position: absolute;
        top: -40px;
        left: 50%;
        margin-left: -16px;
        width: 32px;
        height: 32px;
        color: var(--accent, var(--color-warning));
        opacity: 0;
        pointer-events: none;
        will-change: transform, opacity;
        z-index: 4;
    }
    .pull-refresh-spinner svg { display: block; width: 100%; height: 100%; }
    .pull-refresh-spinner circle {
        stroke-dasharray: 60 22;
        stroke-dashoffset: 0;
    }
    .pull-refresh-spinner.is-armed {
        opacity: 1;
    }
    .pull-refresh-spinner.is-loading {
        opacity: 1;
        top: 8px;
        animation: pull-refresh-spin 720ms linear infinite;
    }
    @keyframes pull-refresh-spin {
        from { transform: rotate(0deg); }
        to   { transform: rotate(360deg); }
    }
}
@media (max-width: 639px) and (prefers-reduced-motion: reduce) {
    /* AC13 — reduced-motion users still see the spinner appear (so they
     * know the refresh is in flight) but the rotation animation is
     * suppressed. Mirrors the `.hw-drawer` / `.hw-ctx-menu` precedent. */
    .pull-refresh-spinner.is-loading {
        animation: none;
    }
}
html.force-reduced-motion .pull-refresh-spinner.is-loading {
    animation: none;
}
@media (prefers-reduced-motion: reduce) {
    .sheet-modal,
    .sheet-backdrop {
        transition: none;
    }
}
html.force-reduced-motion .sheet-modal,
html.force-reduced-motion .sheet-backdrop {
    transition: none;
}

/* ─── Search / filter / jump-to modal (T3.2 ui-search-jump-to) ───
 * Inherits the .sheet-modal shape (centered card on desktop,
 * bottom-sheet on mobile). This block only adds the tab strip,
 * filter form layout, result list, and inline error pill. */
#search-modal { --sheet-width: min(560px, calc(100vw - 32px)); }
.search-tabs {
    display: flex;
    gap: 4px;
    border-bottom: 1px solid var(--border-soft, rgba(255, 255, 255, 0.06));
    margin-bottom: 8px;
}
.search-tab {
    background: transparent;
    color: var(--text-dim);
    border: none;
    border-bottom: 2px solid transparent;
    padding: 8px 12px;
    font-size: 13px;
    cursor: pointer;
    min-height: var(--tap-sm);
}
.search-tab:hover, .search-tab:focus-visible {
    color: var(--text);
    outline: none;
}
.search-tab[aria-selected="true"] {
    color: var(--accent);
    border-bottom-color: var(--accent);
}
.search-form {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 8px 12px;
    padding-bottom: 8px;
    font-size: 12px;
}
.search-form label {
    display: flex;
    flex-direction: column;
    gap: 4px;
    color: var(--text-dim);
}
.search-form input[type="number"],
.search-form input[type="text"],
.search-form select {
    background: rgba(255, 255, 255, 0.04);
    border: 1px solid var(--border-soft, rgba(255, 255, 255, 0.1));
    color: var(--text);
    border-radius: 4px;
    padding: 6px 8px;
    font-size: 13px;
    min-height: var(--tap-sm);
}
.search-form input[type="checkbox"] {
    margin-right: 6px;
    transform: translateY(1px);
}
.search-form .search-checkbox {
    grid-column: 1 / -1;
    flex-direction: row;
    align-items: center;
}
.search-radio-group {
    grid-column: 1 / -1;
    border: 1px solid var(--border-soft, rgba(255, 255, 255, 0.06));
    border-radius: 4px;
    padding: 6px 10px;
    margin: 0;
    display: flex;
    gap: 12px;
    flex-wrap: wrap;
}
.search-radio-group legend {
    color: var(--text-dim);
    font-size: 11px;
    padding: 0 4px;
}
.search-coords-form {
    grid-template-columns: 1fr auto;
    align-items: end;
}
.search-coord-error {
    grid-column: 1 / -1;
    color: var(--danger, #d44);
    background: rgba(220, 60, 60, 0.08);
    border: 1px solid rgba(220, 60, 60, 0.3);
    border-radius: 4px;
    padding: 6px 8px;
    font-size: 12px;
}
.search-coord-error.hidden { display: none; }
.search-count {
    color: var(--text-dim);
    font-size: 11px;
    margin-left: auto;
    margin-right: 8px;
    font-weight: normal;
}
.search-results {
    list-style: none;
    margin: 0;
    padding: 0;
    max-height: 50vh;
    overflow-y: auto;
}
.search-row {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 12px;
    padding: 8px 0;
    border-bottom: 1px solid var(--border-soft, rgba(255, 255, 255, 0.04));
    font-size: 12px;
}
.search-row:last-child { border-bottom: 0; }
.search-row-summary {
    display: flex;
    flex-direction: column;
    gap: 2px;
    flex: 1 1 auto;
    min-width: 0;
}
.search-row-summary b {
    color: var(--text);
    font-size: 13px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.search-row-summary .text-muted {
    color: var(--text-dim);
    font-size: 11px;
}
.search-row-actions {
    display: flex;
    gap: 4px;
    flex-shrink: 0;
}
.search-empty,
.virtual-pad {
    text-align: center;
    color: var(--text-dim);
    font-size: 12px;
    padding: 24px 0;
    font-style: italic;
}
.virtual-pad { font-style: normal; padding: 12px 0; }
@media (max-width: 639px) {
    .search-form { grid-template-columns: 1fr; }
    .search-radio-group { flex-direction: column; gap: 6px; }
    .search-results { max-height: 60vh; }
    .search-row { flex-direction: column; align-items: stretch; gap: 6px; }
    .search-row-actions { justify-content: flex-end; }
}

/* ─── economy-rework T18: inventory section + capacity-meter (AC46/AC50) ─
   Two-column inventory layout (Resources left + Equipment right) plus a
   horizontal capacity-meter bar with five tier colours and an overload
   pulse animation. The pulse animation is gated by
   `prefers-reduced-motion: no-preference` so users with reduced-motion
   preference get the static red bar without the strobe. */

.mono {
    font-family: var(--font-mono);
    font-variant-numeric: tabular-nums;
}

.inventory-section {
    margin-top: 8px;
    padding: 8px;
    background: rgba(20, 20, 40, 0.45);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    display: flex;
    flex-direction: column;
    gap: 8px;
}

.inventory-columns {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 12px;
}

.inventory-column h4 {
    margin: 0 0 4px 0;
    font-size: var(--fs-label);
    color: var(--text-dim);
    text-transform: uppercase;
    letter-spacing: 0.06em;
}

.inventory-rows {
    display: flex;
    flex-direction: column;
    gap: 2px;
}

.inventory-row {
    display: grid;
    grid-template-columns: 1fr auto auto;
    gap: 6px;
    align-items: baseline;
    font-size: var(--fs-body);
}

.inventory-row-amount { font-weight: bold; }

.inventory-empty {
    font-size: var(--fs-label);
    font-style: italic;
}

.equipment-bucket {
    margin-bottom: 4px;
}

.equipment-bucket-label {
    margin-bottom: 2px;
    text-transform: uppercase;
    letter-spacing: 0.06em;
}

.equipment-bucket-chips {
    display: flex;
    flex-wrap: wrap;
    gap: 4px;
}

/* Cut-corner geometric chip per design.md "Visual direction" §
   Backgrounds (`clip-path: polygon(0 0, calc(100% - 8px) 0, 100% 8px,
   100% 100%, 0 100%)`). The frame colour is the `--equipment-frame`
   token (gold-tinted at 18% alpha) so chips read against any panel
   background. */
.equipment-chip {
    display: inline-flex;
    gap: 4px;
    padding: 2px 8px;
    background: var(--equipment-frame);
    color: var(--text);
    font-size: var(--fs-label);
    line-height: 1.5;
    clip-path: polygon(0 0, calc(100% - 8px) 0, 100% 8px, 100% 100%, 0 100%);
}

.equipment-chip[data-category="mount"] { background: rgba(200, 153, 104, 0.25); }
.equipment-chip[data-category="transport"] { background: rgba(176, 143, 255, 0.18); }

.equipment-chip-count {
    color: var(--text-dim);
}

/* ── Capacity-meter ───────────────────────────────────────────────── */
.capacity-meter {
    display: flex;
    flex-direction: column;
    gap: 4px;
}

.capacity-meter-track {
    position: relative;
    height: 10px;
    border-radius: var(--radius-sm);
    background: rgba(255, 255, 255, 0.06);
    border: 1px solid var(--border);
    overflow: hidden;
}

.capacity-meter-fill {
    position: absolute;
    inset: 0 auto 0 0;
    height: 100%;
    /* Default colour: `--load-normal` resolves to `--text-dim` so the
       bar still reads if the `data-tier` attribute is missing. The five
       per-tier rules below override based on attribute. */
    background: var(--load-normal);
    transition: width 320ms ease-out, background 200ms linear;
}

/* `data-tier` → fill colour. Selectors target the wrapper so a JS
   change to the attribute (e.g. after a pickup/drop refresh) flips the
   colour without re-rendering. */
.capacity-meter[data-tier="ok"] .capacity-meter-fill        { background: var(--load-ok); }
.capacity-meter[data-tier="normal"] .capacity-meter-fill    { background: var(--load-normal); }
.capacity-meter[data-tier="warn"] .capacity-meter-fill      { background: var(--load-warn); }
.capacity-meter[data-tier="overload"] .capacity-meter-fill  { background: var(--load-overload); }
.capacity-meter[data-tier="block"] .capacity-meter-fill     { background: var(--load-block); }

.capacity-meter-marks {
    position: absolute;
    inset: 0;
    pointer-events: none;
}

.capacity-meter-mark {
    position: absolute;
    top: 0;
    bottom: 0;
    width: 1px;
    background: rgba(255, 255, 255, 0.35);
    transform: translateX(-0.5px);
}

.capacity-meter-mark-30 { background: rgba(93, 200, 168, 0.6); }
.capacity-meter-mark-100 { background: rgba(255, 181, 107, 0.7); }

.capacity-meter-text {
    display: flex;
    gap: 8px;
    font-size: var(--fs-label);
    color: var(--text-dim);
}

/* AC50 — overload pulse. 2 Hz keyframe (period = 500ms; opacity 0.5 ↔
   1.0). Only inside the `prefers-reduced-motion: no-preference` block,
   so users with reduced-motion preference see a static red bar — no
   strobe, no headache trigger. The fill keeps its red colour from the
   `data-tier` rule above; only the alpha cycles. */
@media (prefers-reduced-motion: no-preference) {
    @keyframes capacity-meter-pulse {
        0%   { opacity: 0.5; }
        50%  { opacity: 1.0; }
        100% { opacity: 0.5; }
    }
    .capacity-meter-fill.pulse {
        animation: capacity-meter-pulse 500ms linear infinite;
    }
}

@media (max-width: 639px) {
    .inventory-columns { grid-template-columns: 1fr; }
}

/* ─── economy-rework T19: stockpile section + pickup modal (AC47) ─────
   Renders the `Купа на клітинці` section in the tile panel, plus the
   `<dialog>`-based two-column pickup/drop modal. Section uses the same
   `.t-section` shell as the rest of the tile panel; the modal is a
   floating overlay with its own `.pickup-modal` class. Visual rule of
   thumb: stockpile rows get the `clip-path` cut-corner chip from the
   inventory equipment chips; modal columns are 50/50 flex. */

.stockpile-section .stockpile-warehouse {
    margin-top: 4px;
    padding: 4px 6px;
    border-radius: var(--radius-sm);
    font-size: var(--fs-label);
    line-height: 1.4;
}
.stockpile-section .stockpile-warehouse-mine {
    background: rgba(125, 216, 125, 0.14);
    color: var(--color-success);
}
.stockpile-section .stockpile-warehouse-allied {
    background: rgba(125, 216, 255, 0.14);
    color: var(--color-primary);
}
.stockpile-section .stockpile-warehouse-foreign {
    background: rgba(255, 107, 107, 0.18);
    color: var(--color-danger);
}

.stockpile-section .stockpile-group {
    margin-top: 6px;
}
.stockpile-section .stockpile-group-label {
    margin-bottom: 2px;
    text-transform: uppercase;
    letter-spacing: 0.04em;
}

.stockpile-section .stockpile-row {
    display: grid;
    grid-template-columns: 1fr auto auto;
    align-items: center;
    gap: 6px;
    padding: 2px 4px;
    font-size: var(--fs-body);
}

.stockpile-section .stockpile-row-amount {
    font-variant-numeric: tabular-nums;
    color: var(--text-dim);
}

.stockpile-chip {
    display: inline-block;
    padding: 2px 8px;
    background: var(--equipment-frame);
    color: var(--text);
    font-size: var(--fs-label);
    line-height: 1.2;
    /* Cut-corner geometric chip — same pattern as `.equipment-chip` */
    clip-path: polygon(0 0, calc(100% - 8px) 0, 100% 8px, 100% 100%, 0 100%);
}
.stockpile-chip[data-kind="resource"] {
    background: rgba(176, 143, 255, 0.14);
    color: var(--text-dim);
}
.stockpile-chip[data-kind="equipment"] {
    background: var(--equipment-frame);
    color: var(--text);
}

.stockpile-section .stockpile-empty {
    margin: 4px 0;
    font-style: italic;
}

.stockpile-section .stockpile-actions {
    margin-top: 6px;
}
.stockpile-section .btn-warehouse-attack {
    background: #6e2a2a;
    color: #ffe5d0;
    border-color: #8a3636;
}

/* Stagger reveal for stockpile rows. CSS variable `--row-i` carries
   the row index (capped at 8 in `stockpile_section.js`). Each row
   delays its reveal by `index * 50ms` so a heap of resources flows
   in instead of slamming. Gated by `prefers-reduced-motion` —
   reduced-motion users see them appear instantly. */
@media (prefers-reduced-motion: no-preference) {
    @keyframes stockpile-row-reveal {
        from { opacity: 0; transform: translateY(8px); }
        to   { opacity: 1; transform: translateY(0); }
    }
    .stockpile-section .stockpile-row {
        animation: stockpile-row-reveal 200ms ease-out backwards;
        animation-delay: calc(var(--row-i, 0) * 50ms);
    }
}

/* ─── pickup_modal.js — <dialog> two-column pickup/drop UI ───────────── */

.pickup-modal {
    border: 1px solid var(--border-strong);
    border-radius: var(--radius);
    background: var(--bg-elev);
    color: var(--text);
    padding: 0;
    max-width: 720px;
    width: 90vw;
}
.pickup-modal::backdrop {
    background: rgba(0, 0, 0, 0.65);
}

.pickup-modal-inner {
    padding: 16px;
    display: flex;
    flex-direction: column;
    gap: 12px;
}

.pickup-modal-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    border-bottom: 1px solid var(--border);
    padding-bottom: 8px;
}
.pickup-modal-header h3 {
    margin: 0;
    font-size: var(--fs-h2);
}
.pickup-modal-close {
    background: none;
    border: none;
    color: var(--text-dim);
    font-size: 18px;
    cursor: pointer;
    line-height: 1;
}
.pickup-modal-close:hover { color: var(--text); }

.pickup-modal-columns {
    display: flex;
    gap: 12px;
}
.pickup-modal-column {
    flex: 1 1 50%;
    background: rgba(0, 0, 0, 0.18);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    padding: 8px;
    display: flex;
    flex-direction: column;
    gap: 6px;
    min-width: 0;
}
.pickup-modal-column h4 {
    margin: 0 0 4px 0;
    font-size: var(--fs-body);
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--text-dim);
}

.pickup-modal-row {
    display: grid;
    grid-template-columns: 1fr auto 80px;
    align-items: center;
    gap: 6px;
    font-size: var(--fs-body);
}
.pickup-modal-row-have {
    color: var(--text-dim);
    font-variant-numeric: tabular-nums;
    text-align: right;
}
.pickup-modal-input {
    width: 80px;
    padding: 4px 6px;
    background: rgba(0, 0, 0, 0.35);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    color: var(--text);
    font-family: var(--font-mono);
    font-size: var(--fs-body);
}
.pickup-modal-input:focus {
    border-color: var(--color-primary);
    outline: none;
}

.pickup-modal-empty {
    font-style: italic;
    padding: 8px 0;
    text-align: center;
}

.pickup-modal-footer {
    display: flex;
    flex-direction: column;
    gap: 8px;
    border-top: 1px solid var(--border);
    padding-top: 8px;
}
.pickup-modal-totals {
    font-size: var(--fs-label);
    color: var(--text-dim);
}
.pickup-modal-error {
    background: rgba(255, 107, 107, 0.18);
    color: var(--color-danger);
    border: 1px solid rgba(255, 107, 107, 0.4);
    border-radius: var(--radius-sm);
    padding: 6px 8px;
    font-size: var(--fs-body);
}
.pickup-modal-error[hidden] { display: none; }

.pickup-modal-actions {
    display: flex;
    justify-content: flex-end;
    gap: 8px;
}

@media (max-width: 639px) {
    .pickup-modal { width: 96vw; }
    .pickup-modal-columns { flex-direction: column; }
}

/* ─── Pickup-modal capacity chip + overflow gate (ui-clarity-pass T15 / AC-F2) ───
   The chip mirrors `army_view_logistics.js`'s `[data-tier]` token table;
   we re-use the same `--load-*` colour ramp here so the visual grammar
   for "перевантажена" stays consistent between Army view + the pickup
   modal. The overflow class flips the totals row red + colours the
   "Перевантаження" hint; the Confirm button gets the standard `.btn-locked`
   greyout via `disabled`. */
.pickup-modal-capacity {
    display: flex;
    align-items: center;
    gap: 6px;
    flex-wrap: wrap;
    padding: 6px 8px;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: var(--bg-2, #1a1a1a);
    font-size: var(--fs-label);
}
.pickup-modal-capacity-chip {
    padding: 1px 6px;
    border-radius: var(--radius-sm);
    background: var(--load-normal, #2c2c2c);
    color: var(--text);
}
.pickup-modal-capacity[data-tier="ok"] .pickup-modal-capacity-chip { background: var(--load-ok, #234023); }
.pickup-modal-capacity[data-tier="warn"] .pickup-modal-capacity-chip { background: var(--load-warn, #5a4a23); }
.pickup-modal-capacity[data-tier="overload"] .pickup-modal-capacity-chip { background: var(--load-overload, #5a3023); color: #ffd; }
.pickup-modal-capacity[data-tier="block"] .pickup-modal-capacity-chip { background: var(--load-block, #6a1f1f); color: #fff; }
.pickup-modal-capacity-numeric { color: var(--text-dim); }
.pickup-modal-projected-load { color: var(--text-dim); margin-left: 6px; }
.pickup-modal-projected-load[hidden] { display: none; }
.pickup-modal-overflow-hint { margin-left: 6px; }
.pickup-modal-overflow-hint[hidden] { display: none; }
.pickup-modal-totals.pickup-modal-totals-overflow {
    color: var(--color-danger);
}
.pickup-modal-totals.pickup-modal-totals-overflow .pickup-modal-total-weight {
    color: var(--color-danger);
    font-weight: bold;
}

/* ─── Cost dialog (T20 / AC48) ──────────────────────────────────────────
   Shared cost-vs-have preview for build / recruit / research / transmute.
   Visual rule of thumb: shortfall row uses `--shortfall-color` (a tint
   of `--color-danger`) so the player's eye lands on the missing line
   first; granary discount renders as a subdued chip + strikethrough on
   the original amount; the toggle row sits between body + actions for
   one-thumb reachability on mobile. Modal close vectors and `<dialog>`
   conventions mirror `.pickup-modal`. */

.cost-dialog {
    --shortfall-color: var(--color-danger);
    border: 1px solid var(--color-primary);
    border-radius: 8px;
    background: var(--surface);
    color: var(--ink);
    width: min(640px, 90vw);
    max-height: calc(100vh - 64px);
    padding: 0;
}

.cost-dialog::backdrop {
    background: rgba(0, 0, 0, 0.6);
}

.cost-dialog-inner {
    padding: 16px;
    display: flex;
    flex-direction: column;
    gap: 12px;
}

.cost-dialog-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
}

.cost-dialog-header h3 {
    margin: 0;
    font-size: var(--fs-h2);
}

.cost-dialog-close {
    background: none;
    border: 0;
    color: var(--text-dim, #888);
    cursor: pointer;
    font-size: 18px;
    padding: 4px 8px;
}
.cost-dialog-close:hover { color: #fff; }

.cost-dialog-columns {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 8px;
    border-bottom: 1px solid #333;
    padding-bottom: 4px;
}
.cost-dialog-col-head {
    font-size: var(--fs-label);
    color: var(--text-dim, #888);
    text-transform: uppercase;
    letter-spacing: 0.5px;
}

.cost-dialog-rows {
    display: flex;
    flex-direction: column;
    gap: 4px;
}

.cost-dialog-row {
    display: grid;
    grid-template-columns: 2fr 1fr 2fr;
    gap: 8px;
    align-items: center;
    padding: 4px 0;
    border-bottom: 1px dashed #2a2a3a;
}

/* Shortfall: per AC48, mark rows where `have < cost` so the player sees
   the bottleneck before submit. The `--shortfall-color` token is
   declared on `.cost-dialog` itself; downstream theming can override
   it at that scope without touching the rule below. */
.cost-dialog-row.shortfall {
    color: var(--shortfall-color);
}
.cost-dialog-row.shortfall .cost-dialog-cost,
.cost-dialog-row.shortfall .cost-dialog-have {
    color: var(--shortfall-color);
}

.cost-dialog-name {
    font-size: var(--fs-body);
}

.cost-dialog-cost {
    font-weight: 600;
}
.cost-dialog-cost-strike {
    text-decoration: line-through;
    color: var(--text-dim, #888);
    font-size: var(--fs-label);
    margin-left: 4px;
}
.cost-dialog-discount {
    background: rgba(125, 216, 125, 0.15);
    color: var(--color-success);
    font-size: 10px;
    padding: 1px 6px;
    border-radius: 4px;
    margin-left: 4px;
}

.cost-dialog-have {
    font-size: var(--fs-label);
    display: flex;
    gap: 4px;
    flex-wrap: wrap;
}

.cost-dialog-toggle {
    display: flex;
    align-items: center;
    gap: 8px;
    cursor: pointer;
    user-select: none;
    font-size: var(--fs-body);
    padding: 4px 0;
    border-top: 1px solid #2a2a3a;
}
/* ui-clarity-pass T15 / AC-F1: warehouse-note inline hint sits between
   the rows + toggle so the player reads "tile only is short" → "but
   the backend may dotyahnut" → "and here's the toggle to also pull
   from armies on tile" in one visual top-down scan. */
.cost-dialog-warehouse-note {
    padding: 4px 0;
    border-top: 1px dashed #2a2a3a;
}
.cost-dialog-toggle input[type="checkbox"] {
    width: 16px;
    height: 16px;
}

.cost-dialog-error-banner {
    background: #3a1212;
    border: 1px solid var(--color-danger);
    color: #ffb0b0;
    padding: 8px 12px;
    border-radius: 4px;
    font-size: var(--fs-body);
}
.cost-dialog-error-banner[hidden] { display: none; }

.cost-dialog-actions {
    display: flex;
    justify-content: flex-end;
    gap: 8px;
}

@media (max-width: 639px) {
    .cost-dialog { width: 96vw; }
    .cost-dialog-row {
        grid-template-columns: 1.5fr 1fr 1.5fr;
    }
}

/* ─── economy-rework T21: empire-aggregator data-grid (AC49) ───
   Four-column data grid (Resource / Total / Armies / Tiles /
   Per-day) that replaces the legacy 5-key inline resource list
   in the empire panel. The first column is sticky so the resource
   name stays visible when the grid horizontally overflows on
   narrow viewports (mobile). Equipment table renders below as a
   separate two-column grid because items have different column
   semantics (no per-day rate, no tiles column). Header row uses
   a slightly stronger background tint so it doesn't blur into
   the first data row.

   Wrapped in a horizontally-scrollable container by `overflow-x:
   auto` on `.empire-aggregator` itself — full mobile redesign is
   out of scope for T21, the scroll container is the affordance.

   Reduced-motion: no animations introduced (the panel is
   information-only). */
.empire-aggregator {
    margin-top: 4px;
    margin-bottom: 8px;
    overflow-x: auto;
}
.empire-agg-header {
    /* `Моя імперія` overhead — Cinzel display font for the
       section's "ancient atlas" tone. Matches the panel-header
       letter-spacing so headers nest cleanly. */
    font-family: var(--font-display);
    letter-spacing: 0.04em;
    color: var(--accent);
    font-size: 13px;
    margin: 0 0 6px 0;
    /* Keep the header visible — but DON'T duplicate the panel's
       own header (`<h3>Моя імперія</h3>` in `index.html`). The
       aggregator header is the section sub-title; the panel
       header sits one level above it. */
    display: none;  /* panel already shows the title; the aggregator section header is internal-only */
}
.empire-agg-grid {
    display: grid;
    grid-template-columns: minmax(120px, 1.2fr) repeat(4, minmax(60px, 1fr));
    gap: 1px;
    background: var(--border);
    border: 1px solid var(--border);
    border-radius: 4px;
    overflow: hidden;
}
.empire-agg-row {
    display: contents;
}
.empire-agg-row .empire-agg-cell {
    background: rgba(20, 20, 40, 0.45);
    padding: 6px 8px;
    font-size: var(--fs-body);
}
/* Zebra-stripe odd data rows (skip header). Uses :nth-child against
   the row count — 5 cells per row, 5..9 = row 2, 10..14 = row 3,
   etc. — but `display: contents` makes that brittle, so we mark
   the head row with its own class instead and let it own the
   stronger tint, leaving data rows uniform. The row spacing comes
   from the `gap: 1px` background trick. */
.empire-agg-head-row .empire-agg-cell {
    background: rgba(40, 40, 70, 0.7);
    color: var(--text-dim);
    font-size: var(--fs-label);
    text-transform: uppercase;
    letter-spacing: 0.06em;
}
.empire-agg-name {
    /* Sticky-first-column: keeps the resource name visible when the
       grid horizontally overflows. Backed by the cell background
       so contents don't bleed through. */
    position: sticky;
    left: 0;
    z-index: 1;
    background: rgba(20, 20, 40, 0.95) !important;
}
.empire-agg-head-row .empire-agg-name {
    background: rgba(40, 40, 70, 0.95) !important;
}
.empire-agg-empty {
    padding: 8px;
    text-align: center;
    grid-column: 1 / -1;
}

/* Equipment sub-section. Two-column grid: name + count. Uses the
   same `--equipment-frame` token T18 declared (a faint gold tint)
   to visually separate the equipment block from the resource
   block — items vs resources is the core conceptual split here. */
.empire-agg-eq-header {
    font-family: var(--font-display);
    letter-spacing: 0.04em;
    color: var(--accent);
    font-size: 12px;
    margin: 12px 0 6px 0;
}
.empire-agg-eq-grid {
    display: grid;
    grid-template-columns: 1fr auto;
    gap: 4px 12px;
    padding: 8px 10px;
    background: linear-gradient(
        to right,
        transparent,
        var(--equipment-frame),
        transparent
    );
    border-radius: 4px;
}
.empire-agg-eq-row {
    display: contents;
}
.empire-agg-eq-name {
    font-size: var(--fs-body);
    color: var(--text);
}
.empire-agg-eq-count {
    color: var(--text);
}

@media (max-width: 639px) {
    /* Mobile collapse: trim the grid columns slightly so the four
       numeric columns + sticky name don't push the panel beyond
       the viewport. The `overflow-x: auto` on `.empire-aggregator`
       already lets the user scroll if a row exceeds the width;
       this rule just trims the per-cell padding to reduce the
       chance of needing to scroll. */
    .empire-agg-grid {
        grid-template-columns: minmax(90px, 1fr) repeat(4, minmax(48px, 1fr));
    }
    .empire-agg-row .empire-agg-cell {
        padding: 4px 6px;
        font-size: var(--fs-label);
    }
}

/* ─── Tech panel (science-tech-tree TASK_10) ─────────────────────
 * DAG renderer styling. Four node states map to four `--tech-*`
 * tokens declared on `:root` below; SVG edges use a faint
 * `--tech-edge` token so the connections sit underneath the cards
 * without competing with them. Mobile fallback (<640px) drops the
 * SVG entirely and renders depth-grouped vertical sections via
 * `.tech-depth-section`. Per AC25 + AC26.
 */

:root {
    --tech-researched:  var(--color-success);
    --tech-in-progress: var(--load-warn);
    --tech-available:   var(--text-dim);
    --tech-locked:      #555;
    --tech-edge:        rgba(255, 255, 255, 0.14);
}

.tech-panel {
    /* Inherits `.panel` + `.sheet-modal` layout from index.html.
       Body container holds either the SVG-wrap (desktop) or the
       depth-section list (mobile). */
}

#tech-body {
    overflow: auto;
    max-height: calc(100vh - 160px);
}

.tech-svg-wrap {
    /* Horizontal scroll lets the wide DAG fit in the panel without
       cropping; vertical scroll handled by `#tech-body`. */
    overflow-x: auto;
    overflow-y: visible;
    padding: 4px 0;
}

.tech-svg {
    display: block;
    /* Fill the panel width on small windows; allow expansion past
       the panel width so the SVG itself drives horizontal scroll. */
    min-width: 100%;
}

/* SVG edges — drawn under the foreignObject node group. */
.tech-edge {
    stroke: var(--tech-edge);
    stroke-width: 1.4;
    fill: none;
}

/* Wrapper inside the foreignObject so the card body doesn't escape
   SVG bounds. The actual `.tech-node` card sits inside. */
.tech-node-host {
    width: 100%;
    height: 100%;
}

.tech-node {
    box-sizing: border-box;
    width: 100%;
    height: 100%;
    padding: 6px 8px;
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-sm);
    background: rgba(20, 20, 40, 0.85);
    color: var(--text);
    font-size: var(--fs-label);
    line-height: var(--lh-tight);
    display: flex;
    flex-direction: column;
    gap: 2px;
    overflow: hidden;
}

.tech-node-researched   { border-color: var(--tech-researched); background: rgba(60, 120, 80, 0.45); }
.tech-node-in_progress  { border-color: var(--tech-in-progress); background: rgba(120, 90, 30, 0.45); }
.tech-node-available    { border-color: var(--tech-available); background: rgba(40, 50, 80, 0.55); }
.tech-node-locked       { border-color: var(--tech-locked);    background: rgba(40, 40, 50, 0.55); color: var(--text-dim); }

.tech-node-head {
    display: flex;
    align-items: center;
    gap: 4px;
}

.tech-node-glyph {
    flex: 0 0 auto;
}

.tech-node-name {
    flex: 1 1 auto;
    font-weight: 600;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.tech-node-cost {
    flex: 0 0 auto;
    font-family: var(--font-mono);
    color: var(--text-dim);
}

.tech-node-desc {
    color: var(--text-dim);
    overflow: hidden;
    text-overflow: ellipsis;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
}

.tech-node-progress {
    display: flex;
    align-items: center;
    gap: 6px;
}

.tech-node-bar {
    flex: 1 1 auto;
    height: 5px;
    background: rgba(255, 255, 255, 0.1);
    border-radius: 3px;
    overflow: hidden;
}

.tech-node-bar-fill {
    height: 100%;
    background: var(--tech-in-progress);
    transition: width 200ms ease-out;
}

.tech-node-progress-text {
    flex: 0 0 auto;
    font-family: var(--font-mono);
    font-size: var(--fs-tiny);
}

.tech-node-unlocks {
    color: var(--text-dim);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.tech-unlock-group {
    margin-right: 6px;
}

.tech-node-action {
    align-self: flex-start;
    margin-top: 2px;
}

.tech-node-locked-hint {
    color: var(--text-dim);
    font-size: var(--fs-tiny);
}

/* Mobile depth-grouped list (≤640px). No SVG; each `.tech-node` card
   sits in a vertical column under its depth section. Card height
   becomes auto so progress bars + action buttons get full width. */
.tech-depth-section {
    margin-bottom: 12px;
}

.tech-depth-title {
    font-family: var(--font-display);
    color: var(--accent);
    font-size: var(--fs-h2);
    letter-spacing: 0.04em;
    margin: 0 0 6px 0;
}

.tech-depth-rows {
    display: flex;
    flex-direction: column;
    gap: 6px;
}

.tech-depth-rows .tech-node {
    height: auto;
    min-height: 56px;
}

.tech-flash-error {
    background: rgba(80, 20, 30, 0.6);
    border: 1px solid var(--color-danger);
    border-radius: var(--radius-sm);
    padding: 6px 8px;
    margin-bottom: 6px;
}

@media (max-width: 640px) {
    /* Mobile fallback: list layout. Hide any SVG that slipped past
       the JS branch (defensive — `_render()` already swaps content
       on breakpoint cross). */
    .tech-svg-wrap { display: none; }
    .tech-depth-rows .tech-node {
        font-size: var(--fs-body);
    }
}

@media (prefers-reduced-motion: reduce) {
    .tech-node-bar-fill {
        transition: none;
    }
}


/* ─── Workspace shell (ui-fullscreen-tile-detail TASK_06) ───────────
 * Fullscreen overlay that covers the canvas when a player opens a tile.
 * Two layers:
 *   #workspace-curtain  — full-viewport blackout that absorbs canvas
 *                         clicks so behind-shell interaction is sealed.
 *   #workspace          — the actual two-pane workspace; slides in from
 *                         the right at 220 ms cubic-bezier per design.md.
 *
 * `.is-entering` is briefly applied on open BEFORE the next animation
 * frame strips it — that lets the transition fire from the offscreen
 * starting state. `.is-leaving` reverses for close. Both classes are
 * no-ops under reduced-motion so the shell snaps in/out.
 *
 * Per design.md responsive layout (lines 232-237):
 *   ≥1024 px: side-by-side 36/64 (diorama / body)
 *   640–1023: side-by-side 40/60
 *   <640 px : full-viewport, vertical stack (diorama top, body below)
 */
#workspace-curtain.workspace-curtain {
    position: fixed;
    inset: 0;
    background: var(--workspace-curtain);
    z-index: 50;
    opacity: 1;
    transition: opacity 220ms cubic-bezier(.22, .94, .42, 1);
}
#workspace-curtain.workspace-curtain.is-entering,
#workspace-curtain.workspace-curtain.is-leaving {
    opacity: 0;
}
#workspace-curtain.workspace-curtain.hidden {
    display: none;
}

#workspace.workspace {
    position: fixed;
    inset: var(--toolbar-h) 0 0 0;   /* respect toolbar */
    z-index: 51;                     /* above curtain */
    background: var(--bg);
    color: var(--text);
    display: grid;
    grid-template-columns: 36% 1px 1fr;
    transform: translateX(0);
    opacity: 1;
    transition:
        transform 220ms cubic-bezier(.22, .94, .42, 1),
        opacity   220ms cubic-bezier(.22, .94, .42, 1);
}
#workspace.workspace.is-entering,
#workspace.workspace.is-leaving {
    transform: translateX(100%);
    opacity: 0;
}
#workspace.workspace.hidden {
    display: none;
}

#workspace.workspace .workspace-pane {
    overflow: auto;
    min-width: 0;
    min-height: 0;
}
#workspace.workspace .workspace-pane-diorama {
    background: var(--bg-elev);
    padding: var(--space-4);
}
#workspace.workspace .workspace-pane-body {
    padding: var(--space-4);
    /* Subtle paper-grain — design.md texture note. Kept inline as data
       URI to avoid a network request for a 4% opacity overlay. */
    background:
        var(--bg),
        linear-gradient(180deg, transparent, transparent);
    font-family: var(--font-body);
    font-size: var(--fs-body);
    line-height: var(--lh-body);
}
#workspace.workspace .workspace-divider {
    background: var(--workspace-divider);
}

/* ui-tileview-fixes TASK_01 / AC4 — visible ✕ close button.
   Anchored to the top-right of the workspace shell, above both panes.
   Mirrors `.panel-close` styling (brass tab visual language) but
   renders 32×32 tap target so it stays visible (and reachable) on
   every breakpoint. z-index sits above the panes (which are
   `overflow:auto`) so canvas-style scroll content can't cover it. */
#workspace.workspace .workspace-close {
    position: absolute;
    top: 8px;
    right: 8px;
    width: 32px;
    height: 32px;
    z-index: 2;
    background: transparent;
    border: 1px solid var(--border-strong);
    color: var(--text-dim);
    border-radius: 4px;
    padding: 0;
    font-size: 16px;
    line-height: 1;
    cursor: pointer;
    display: inline-flex;
    align-items: center;
    justify-content: center;
}
#workspace.workspace .workspace-close:hover,
#workspace.workspace .workspace-close:active,
#workspace.workspace .workspace-close:focus-visible {
    background: rgba(255, 255, 255, 0.07);
    color: var(--accent);
    border-color: var(--accent);
    outline: none;
}

/* Loading skeleton — visible during deep-link reload before the
   per-view renderer registers + hydrates. Shape mirrors the eventual
   header + tab content so the layout doesn't shift on swap-in. */
.workspace-skeleton {
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
}
.workspace-skeleton-title {
    font-family: var(--font-display);
    font-size: var(--fs-h1);
    color: var(--accent);
    letter-spacing: 0.04em;
}
.workspace-skeleton-bar {
    height: 14px;
    background: linear-gradient(
        90deg,
        var(--bg-elev) 0%,
        var(--border-strong) 50%,
        var(--bg-elev) 100%
    );
    background-size: 200% 100%;
    border-radius: var(--radius-sm);
    animation: workspace-skeleton-shimmer 1.4s ease-in-out infinite;
}
.workspace-skeleton-bar--short {
    width: 40%;
}
.workspace-skeleton-hint {
    color: var(--text-dim);
    font-size: var(--fs-label);
}
@keyframes workspace-skeleton-shimmer {
    0%   { background-position: 200% 0; }
    100% { background-position: -200% 0; }
}

/* Tablet — narrower diorama. */
@media (max-width: 1023px) and (min-width: 640px) {
    #workspace.workspace {
        grid-template-columns: 40% 1px 1fr;
    }
}

/* Mobile — vertical stack. Diorama collapses to a 38vh band; body
   takes the rest. The vertical divider becomes a horizontal seam. */
@media (max-width: 639px) {
    #workspace.workspace {
        grid-template-columns: 1fr;
        grid-template-rows: 38vh 1px 1fr;
    }
    #workspace.workspace .workspace-divider {
        background: linear-gradient(
            90deg,
            transparent 0%,
            var(--border-strong) 20%,
            var(--border-strong) 80%,
            transparent 100%
        );
    }
}

/* Reduced-motion: snap rather than slide; skeleton shimmer also
   pauses so motion-sensitive players don't see the moving bar. Mirrors
   the existing `force-reduced-motion` opt-in from the settings menu —
   the document-level class wins, the media query falls back. */
@media (prefers-reduced-motion: reduce) {
    #workspace.workspace,
    #workspace-curtain.workspace-curtain {
        transition: none;
    }
    #workspace.workspace.is-entering,
    #workspace.workspace.is-leaving {
        transform: none;
    }
    .workspace-skeleton-bar {
        animation: none;
    }
}
.force-reduced-motion #workspace.workspace,
.force-reduced-motion #workspace-curtain.workspace-curtain {
    transition: none;
}
.force-reduced-motion #workspace.workspace.is-entering,
.force-reduced-motion #workspace.workspace.is-leaving {
    transform: none;
}
.force-reduced-motion .workspace-skeleton-bar {
    animation: none;
}


/* ─── Mobile breakpoint pass (ui-fullscreen-tile-detail TASK_14) ─────
 * Per design.md § "Responsive layout":
 *   <640 px : vertical stack, top 38vh diorama, tabs as bottom-sheet
 *             pills, swipe between tabs.
 *   640–1023: side-by-side 40/60 (body / diorama).
 *   ≥1024 px: side-by-side 36/64 (body / diorama).
 *
 * The 36/64 + 40/60 + 38vh-stack rules are already wired in the
 * workspace shell block above (TASK_06). This block adds the mobile
 * bottom-sheet pill bar for the per-view tab strip plus the
 * PathPlanner half-sheet behaviour: at <640 px when
 * `body.hw-pp-open` is set, the workspace shell collapses to a
 * 30 vh sheet anchored at the bottom so the player sees canvas + plan
 * controls + ArmyView header simultaneously (design.md line 239).
 *
 * Validation targets:
 *   • No horizontal scroll at 320 px width — `min-width: 0` on every
 *     row + `flex-wrap: wrap` on tab pills + `overflow-x: hidden` on
 *     the workspace + `box-sizing: border-box` everywhere.
 *   • All actions reachable in ≤2 taps from canvas at 375 px width —
 *     the bottom-sheet pill bar is sticky inside the body pane so
 *     every tab stays one tap away.
 *   • Bottom-sheet pills do NOT overlap on-canvas controls — the
 *     pills sit INSIDE the workspace body pane (not over the canvas),
 *     so the canvas-overlay PathPlanner (z-index 9000) and bottom-nav
 *     (z-index 35) both clear them.
 */

@media (max-width: 639px) {
    /* Belt-and-braces guard: prevent any nested grid/flex child from
     * pushing horizontal scroll onto the document at 320 px. The
     * workspace shell is the only fullscreen surface that can grow
     * past 100vw if a child sets a fixed width, so cap it here. */
    #workspace.workspace {
        max-width: 100vw;
        overflow-x: hidden;
    }
    #workspace.workspace .workspace-pane {
        max-width: 100vw;
        box-sizing: border-box;
    }

    /* Collapse the brass tabs strip into a bottom-sheet pill bar that
     * sticks to the bottom edge of the scrollable body pane. Three
     * shared selectors — TileView / UnitView / ArmyView all use the
     * same flex+pill vocabulary so we override them in one pass.
     *
     * mobile-ux-rework / TASK_11 (AC14) — `--swipe-dx` is set on the
     * strip during a tab-swipe gesture by
     * `workspace_shell_input.js::wireSwipeBetweenTabs` so the strip
     * translates horizontally as a live preview before the swipe
     * commits. Defaults to `0px`; reset on touchend. Tween is suppressed
     * during touchmove because the JS sets the variable per-frame; the
     * post-commit snap relies on the same per-frame setter, so no
     * extra transition is needed. */
    #workspace.workspace .tile-view-tabs,
    #workspace.workspace .unit-view-tabs,
    #workspace.workspace .army-view-tabs {
        position: sticky;
        bottom: 0;
        order: 99;                   /* push to end of body pane flex */
        margin: var(--space-3) calc(-1 * var(--space-4))
                calc(-1 * var(--space-4));
        padding: var(--space-2) var(--space-3)
                 calc(var(--space-2) + env(safe-area-inset-bottom, 0px));
        background: var(--bg-elev);
        border: none;
        border-top: 1px solid var(--border-strong);
        border-radius: 0;
        flex-wrap: wrap;
        gap: var(--space-2);
        justify-content: center;
        box-shadow: 0 -6px 18px rgba(0, 0, 0, 0.45);
        z-index: 5;                  /* over body content, under modals */
        transform: translateX(var(--swipe-dx, 0px));
    }
    /* Each tab becomes a discrete pill. Brass-tab vocabulary stays
     * (font-display label + mono number + ochre underline on active)
     * but the underline becomes a full pill border so the active
     * state reads on a 1-line strip. */
    #workspace.workspace .tile-view-tab,
    #workspace.workspace .unit-view-tab,
    #workspace.workspace .army-view-tab {
        flex: 0 1 auto;
        min-width: 0;
        border-right: none;
        border: 1px solid var(--border-strong);
        border-radius: 999px;
        padding: var(--space-1) var(--space-3);
        background: var(--bg);
        box-shadow: none;            /* drop the inset 2px underline */
    }
    #workspace.workspace .tile-view-tab.is-active,
    #workspace.workspace .unit-view-tab.is-active,
    #workspace.workspace .army-view-tab.is-active {
        border-color: var(--color-warning);
        background: rgba(255, 183, 0, 0.10);
        box-shadow: none;
    }
    /* Tighten label glyphs on small viewports so 4 pills (TileView
     * worst case) fit on one row at 320 px. The numeric glyph stays;
     * the display label can wrap on overflow because we wrapped the
     * strip itself. */
    #workspace.workspace .tile-view-tab-label,
    #workspace.workspace .unit-view-tab-label,
    #workspace.workspace .army-view-tab-label {
        font-size: var(--fs-label);
    }

    /* Make sure the body pane flexes vertically so the `order: 99`
     * pill bar lands at the very bottom regardless of TileView /
     * UnitView / ArmyView root markup. Each view already wraps in a
     * flex-column root via .tile-view / .unit-view / .army-view; we
     * just enforce min-height so the pill bar doesn't float. */
    #workspace.workspace .workspace-pane-body {
        display: flex;
        flex-direction: column;
        padding-bottom: 0;           /* sticky bar carries its own pad */
    }
    #workspace.workspace .workspace-pane-body > .tile-view,
    #workspace.workspace .workspace-pane-body > .unit-view,
    #workspace.workspace .workspace-pane-body > .army-view {
        min-height: 100%;
        flex: 1 1 auto;
    }

    /* Prevent the inventory two-column flip + filter chip strip + tab
     * content rows from forcing horizontal scroll at 320 px. */
    #workspace.workspace .tile-view-row,
    #workspace.workspace .unit-view-list li,
    #workspace.workspace .unit-view-action-row,
    #workspace.workspace .army-view-row,
    #workspace.workspace .army-view-action-row,
    #workspace.workspace .army-view-forecast-list li,
    #workspace.workspace .army-view-inventory-cell,
    #workspace.workspace .unit-view-inventory-row {
        min-width: 0;
        max-width: 100%;
    }

    /* mobile-ux-rework / TASK_07 (AC29) — workspace sticky CTA bar.
     * Lives at `<footer id="workspace-cta-bar">` injected by
     * `workspace_shell_chrome.js::buildShellInnerHtml`; the inner
     * markup (per-button atlas-card-style CTA) is painted by
     * `workspace_shell_cta_bar.js::renderCtaBar`. The bar pins the
     * bottom edge of the shell on mobile and respects `safe-area-
     * inset-bottom` so iPhone home-indicator clearance survives.
     * Default chrome is mobile-only — desktop / tablet hide the slot
     * entirely (the per-view `.atlas-modal-sticky` mirror covers the
     * primary-action surface above the fold on those viewports). */
    #workspace.workspace #workspace-cta-bar.workspace-cta-bar {
        grid-column: 1 / -1;             /* span the shell column */
        position: sticky;
        bottom: 0;
        left: 0;
        right: 0;
        z-index: 3;                      /* above body, below close ✕ */
        display: grid;
        gap: var(--space-2);
        grid-template-columns: 1fr;      /* single-column on <480 px */
        background: var(--surface-elev, var(--bg-elev));
        border-top: 1px solid var(--border-firm, var(--border-strong));
        padding: var(--space-3) var(--space-4)
            calc(var(--space-3) + var(--safe-bottom, env(safe-area-inset-bottom, 0)));
    }
    /* Mobile shell becomes a 1-column / 3-row grid (diorama / divider /
     * body); the cta-bar lands as the implicit 4th row. The body row's
     * `1fr` absorbs leftover space so the bar pins the bottom edge
     * naturally without a `position: fixed` hack. */
    #workspace.workspace:has(#workspace-cta-bar.workspace-cta-bar:not(.is-empty)):not(:has(#workspace-cta-bar[hidden])) {
        grid-template-rows: 38vh 1px 1fr auto;
    }
    #workspace.workspace #workspace-cta-bar.workspace-cta-bar.is-empty,
    #workspace.workspace #workspace-cta-bar.workspace-cta-bar[hidden] {
        display: none;                   /* AC29 — no CTAs → no bar */
    }
    #workspace.workspace #workspace-cta-bar .workspace-cta {
        min-height: var(--tap-mobile-lg, var(--tap-mobile, 56px));
        padding: var(--space-2) var(--space-4);
        font-family: var(--font-display);
        font-size: var(--fs-body);
        background: var(--accent);
        color: var(--bg);
        border: 1px solid var(--accent);
        border-radius: var(--radius-sm);
        cursor: pointer;
        touch-action: manipulation;
    }
    #workspace.workspace #workspace-cta-bar .workspace-cta:focus-visible {
        outline: 2px solid var(--color-warning);
        outline-offset: 2px;
    }
    #workspace.workspace #workspace-cta-bar .workspace-cta:active {
        transform: scale(var(--press-scale, 0.98));
    }
    #workspace.workspace #workspace-cta-bar .workspace-cta[disabled],
    #workspace.workspace #workspace-cta-bar .workspace-cta[aria-disabled="true"] {
        opacity: 0.45;
        cursor: not-allowed;
        transform: none;
    }
    #workspace.workspace #workspace-cta-bar .workspace-cta--secondary {
        background: transparent;
        color: var(--accent);
    }
}
@media (min-width: 480px) and (max-width: 639px) {
    /* AC29 — two-column layout once the viewport carries enough width
     * for two 56 px-tall pills to sit side-by-side without clipping. */
    #workspace.workspace #workspace-cta-bar.workspace-cta-bar {
        grid-template-columns: 1fr 1fr;
    }
}
@media (min-width: 640px) {
    /* Desktop / tablet — the workspace CTA bar collapses to nothing
     * because the per-view sticky-card slot above the fold already
     * carries the primary CTA. Desktop renders the slot empty in the
     * DOM but display: none keeps it invisible + non-tabbable. */
    #workspace.workspace #workspace-cta-bar.workspace-cta-bar {
        display: none;
    }
}
@media (max-width: 639px) {
    /* PathPlanner half-sheet — when the planner overlay is open
     * (body.hw-pp-open, set by path_planner.js _open/_close) the
     * workspace shell shrinks to the bottom 30 vh so the canvas above
     * stays visible alongside the plan controls. The diorama row
     * collapses (only the body row remains) and the body pane
     * overflows internally. */
    body.hw-pp-open #workspace.workspace {
        top: auto;
        bottom: 0;
        height: 30vh;
        height: 30dvh;               /* iOS-safe; falls back to vh */
        grid-template-rows: 1fr;
        grid-template-columns: 1fr;
    }
    body.hw-pp-open #workspace.workspace .workspace-pane-diorama,
    body.hw-pp-open #workspace.workspace .workspace-divider {
        display: none;
    }
    body.hw-pp-open #workspace.workspace .workspace-pane-body {
        padding-top: var(--space-2);
    }
    /* Curtain dims the canvas behind only as a thin tint at the
     * bottom so the planner controls + ArmyView header stay readable
     * but the canvas above is visibly active. */
    body.hw-pp-open #workspace-curtain.workspace-curtain {
        background: linear-gradient(
            180deg,
            transparent 0%,
            transparent 50%,
            rgba(8, 10, 24, 0.55) 100%
        );
    }
    /* The path planner overlay sits above the half-sheet — bump it
     * up so it never collides with the workspace bottom edge. The
     * existing `<640px` rule below already drops max-height to
     * 50vh; we shift it above the 30vh sheet for a clean 70 / 30
     * split (canvas on top, planner stacked above ArmyView). */
    body.hw-pp-open .path-planner-overlay {
        bottom: calc(30vh + 8px);
        bottom: calc(30dvh + 8px);
        max-height: calc(70vh - 24px);
        max-height: calc(70dvh - 24px);
    }
}

/* PathPlanner — desktop / tablet (≥640 px) hides the workspace shell
 * entirely while the planner overlay is open. The mobile branch above
 * collapses the shell to a 30 vh half-sheet so the player keeps the
 * ArmyView header in view; on wider viewports the canvas is the planner
 * surface and the ArmyView pane would otherwise sit on top of the route
 * arrows. Removing it here gives the planner the full canvas + the
 * floating overlay, and `_close()` (cancel / esc / submit) drops
 * `body.hw-pp-open` so the workspace re-appears unchanged. */
@media (min-width: 640px) {
    body.hw-pp-open #workspace.workspace,
    body.hw-pp-open #workspace-curtain.workspace-curtain {
        display: none;
    }
}


/* ─── TileView (ui-fullscreen-tile-detail TASK_07) ─────────────────────
 * Brass tabs ([1] Гарнізон / [2] Будівлі) with an ochre underline on
 * the active tab, summary chip line, and filter chip strip
 * (Свої/Чужі/Усі) on the Будівлі tab. All tokens reused from the
 * existing palette — no new `--brass-*` / `--ochre-*` tokens introduced
 * (design.md anti-pattern guard: "labelled brass tabs with a 2 px
 * underline in `--color-warning` (ochre)"). Reused tokens:
 *   --color-warning   →  ochre underline + chip active border + focus
 *   --bg-elev         →  tab strip backplate
 *   --border-strong   →  inactive tab divider
 *   --text-dim        →  inactive label colour
 *   --font-display    →  Cinzel for tab labels (engraved feel)
 *   --font-mono       →  JetBrains Mono for the [1] / [2] glyph
 */
.tile-view {
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
    height: 100%;
    min-height: 0;
}

.tile-view-header {
    border-bottom: 1px solid var(--border);
    padding-bottom: var(--space-3);
}
/* ui-clarity-pass TASK_02 — `← Карта` back-to-canvas chip lives on the
   same row as the title so the meta row keeps wrapping freely below.
   Mirrors `unit-view-tile-chip` (outlined warning-colour pill) for a
   consistent back-affordance vocabulary across workspace views. */
.tile-view-title-row {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    flex-wrap: wrap;
    margin: 0 0 var(--space-1) 0;
}
.tile-view-title {
    font-family: var(--font-display);
    font-size: var(--fs-h1);
    font-weight: 600;
    color: var(--text);
    letter-spacing: 0.04em;
    margin: 0;
    flex: 1 1 auto;
    min-width: 0;
}
.tile-view-canvas-chip {
    /* ui-clarity-deferred TASK_03 / AC3.2 — chip drops to a lighter
       visual weight: tighter padding (2px 10px), hairline border via
       `--border`, and `--text-secondary` foreground so the chip stops
       competing with the brass tab strip below it. The `← Карта`
       affordance should read as a quiet ghost chip, not a second
       primary CTA. */
    display: inline-flex;
    align-items: center;
    gap: 4px;
    background: var(--bg-elev);
    border: 1px solid var(--border);
    border-radius: 999px;
    color: var(--text-secondary);
    padding: 2px 10px;
    font-family: var(--font-mono);
    font-size: var(--fs-label);
    cursor: pointer;
    min-height: var(--tap-sm);
    transition: background 140ms ease, border-color 140ms ease, color 140ms ease;
    flex: 0 0 auto;
}
.tile-view-canvas-chip:hover,
.tile-view-canvas-chip:focus-visible {
    background: rgba(255, 255, 255, 0.06);
    border-color: var(--text-secondary);
    color: var(--text);
    outline: none;
}
.tile-view-canvas-chip-arrow {
    font-family: var(--font-body);
}
/* ui-clarity-pass TASK_19 (AC-K2) — owner-only ✏️ rename button +
   inline rename form. Visibility gate is JS-side (button absent when
   `tile.is_mine !== true`); the CSS keeps the button as a small ghost
   chip so it sits next to the title without dominating the row. The
   form replaces the title node while editing — input grows to fill
   the space, save/cancel stay compact. */
.tile-view-rename-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    background: transparent;
    border: 1px solid var(--text-dim);
    border-radius: 999px;
    color: var(--text);
    padding: 2px 8px;
    font-size: var(--fs-label);
    cursor: pointer;
    min-height: var(--tap-sm);
    flex: 0 0 auto;
    transition: background 140ms ease, border-color 140ms ease;
}
.tile-view-rename-btn:hover,
.tile-view-rename-btn:focus-visible {
    background: rgba(255, 255, 255, 0.06);
    border-color: var(--text);
    outline: none;
}
.tile-view-rename-form {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    flex: 1 1 auto;
    min-width: 0;
    flex-wrap: wrap;
}
.tile-view-rename-input {
    flex: 1 1 auto;
    min-width: 8em;
    font-family: var(--font-display);
    font-size: var(--fs-h1);
    font-weight: 600;
    color: var(--text);
    background: var(--bg-elev);
    border: 1px solid var(--text-dim);
    border-radius: var(--radius-sm, 4px);
    padding: 4px 10px;
    /* Suppress iOS focus-zoom — line up with frontend convention. */
    line-height: 1.2;
}
.tile-view-rename-input:focus-visible {
    border-color: var(--color-warning);
    outline: none;
}
.tile-view-rename-save,
.tile-view-rename-cancel {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border-radius: 999px;
    padding: 4px 12px;
    font-family: var(--font-mono);
    font-size: var(--fs-label);
    cursor: pointer;
    min-height: var(--tap-sm);
    border: 1px solid var(--text-dim);
    background: var(--bg-elev);
    color: var(--text);
    flex: 0 0 auto;
    transition: background 140ms ease, border-color 140ms ease;
}
.tile-view-rename-save {
    border-color: var(--color-warning);
    color: var(--color-warning);
}
.tile-view-rename-save:hover,
.tile-view-rename-save:focus-visible {
    background: rgba(255, 183, 0, 0.10);
    outline: none;
}
.tile-view-rename-cancel:hover,
.tile-view-rename-cancel:focus-visible {
    background: rgba(255, 255, 255, 0.06);
    border-color: var(--text);
    outline: none;
}
.tile-view-rename-save[disabled],
.tile-view-rename-cancel[disabled],
.tile-view-rename-input[disabled] {
    opacity: 0.6;
    cursor: progress;
}
@media (prefers-reduced-motion: reduce) {
    .tile-view-canvas-chip {
        transition: none;
    }
    .tile-view-rename-btn,
    .tile-view-rename-save,
    .tile-view-rename-cancel { transition: none; }
}
.force-reduced-motion .tile-view-canvas-chip {
    transition: none;
}
.force-reduced-motion .tile-view-rename-btn,
.force-reduced-motion .tile-view-rename-save,
.force-reduced-motion .tile-view-rename-cancel { transition: none; }
.tile-view-meta {
    display: flex;
    gap: var(--space-3);
    flex-wrap: wrap;
    color: var(--text-dim);
    font-size: var(--fs-label);
}
.tile-view-coords {
    font-family: var(--font-mono);
    color: var(--text);
}
.tile-view-terrain {
    color: var(--color-success);
}
/* Owner chip in the TileView header band — heraldry colour dot + name.
   Mirrors the legacy `.t-owner-name` pattern (data-driven `--owner-color`
   inline custom property). The dot is a plain coloured circle so the
   colour read survives even when the player's locale renders the name
   with a wide font. `is-unowned` greys out the "нічия" fallback so it
   doesn't compete with named owners visually. */
.tile-view-owner {
    display: inline-flex;
    align-items: center;
    gap: var(--space-1);
}
.tile-view-owner-dot {
    display: inline-block;
    width: 0.7em;
    height: 0.7em;
    border-radius: 50%;
    background: var(--owner-color, var(--text-dim));
    border: 1px solid rgba(0, 0, 0, 0.35);
    flex: 0 0 auto;
}
.tile-view-owner-name {
    color: var(--text);
}
.tile-view-owner-name.is-unowned {
    color: var(--text-dim);
    font-style: italic;
}
.tile-view-climate {
    color: var(--text-dim);
}

/* ui-tile-army-cards-rework TASK_05 (AC11/AC12) — population strip +
   recruit CTA. The strip sits between the meta row and the stockpile
   chip strip, mirroring the `.tile-view-stockpile` chip shape so the
   two horizontal rows read as siblings. The CTA is a primary-tone
   ochre button (≥ --tap height) anchored below the strip. Disabled
   state dims the fill + carries `data-disable-reason` /
   `data-disable-hint` for the `attachDisabledPopover` sticky-tap
   chain (3 s, ui-toast-feedback AC-11). */
.tile-view-population {
    display: flex;
    gap: var(--space-2);
    flex-wrap: wrap;
    align-items: baseline;
    margin-top: var(--space-2);
}
.tile-view-population-chip {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    padding: 2px 8px;
    background: rgba(176, 143, 255, 0.14);
    color: var(--text);
    border-radius: var(--radius-sm);
    font-size: var(--fs-label);
    font-variant-numeric: tabular-nums;
    line-height: 1.2;
}
.tile-view-recruit-cta {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    align-self: flex-start;
    margin-top: var(--space-2);
    padding: var(--space-1) var(--space-3);
    min-height: var(--tap);
    background: var(--color-warning);
    color: var(--bg);
    border: 1px solid var(--color-warning);
    border-radius: var(--radius-sm);
    font-family: var(--font-body);
    font-size: var(--fs-body);
    font-weight: 600;
    cursor: pointer;
    transition: background 140ms ease, color 140ms ease;
}
.tile-view-recruit-cta:hover,
.tile-view-recruit-cta:focus-visible {
    background: rgba(255, 183, 0, 0.85);
    outline: 2px solid var(--color-warning);
    outline-offset: 2px;
}
.tile-view-recruit-cta[disabled] {
    background: var(--bg-elev);
    color: var(--text-dim);
    border-color: var(--border);
    cursor: not-allowed;
    opacity: 0.7;
}

/* Stockpile tab body (ui-modal-redesign TASK_17 / AC13 / AC28 / AC29 /
   AC37 visual half) — auto-fill grid of `.atlas-card[data-variant=
   "stockpile"]` cells served by `tile_view_stockpile.js::paintStockpile`.
   Retires the legacy `.tile-view-stockpile-grid` / `.tile-view-stockpile-
   tile` chip chrome — every consumer now reads through the canonical
   atlas-card slot rules in §atlas-card. The grid container shares a
   single `.atlas-card-grid` rule with future stockpile/inventory
   surfaces (ArmyView cargo / UnitView інвентар) so the layout invariant
   stays in one place. Static info surface — no animations on value
   changes per the force-reduced-motion precedent. 200 px tracks keep
   the resource cards readable on desktop while the `auto-fill`
   collapses cleanly down to one column on the mobile bottom-sheet
   (≤640 px). */
.atlas-card-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
    gap: var(--space-3);
    padding: var(--space-2) 0;
}
@media (max-width: 640px) {
    .atlas-card-grid {
        grid-template-columns: 1fr;
    }
}

/* ui-modal-redesign / TASK_19 (AC14) — `.atlas-section-heading` divider
   between two stacked `.atlas-card-grid`s inside a single tab body
   (e.g. ArmyView Склад: squad grid above cargo grid). EB Garamond
   600, ochre accent, 1 px ledger rule below so the divider reads as
   a section break, not a row label. First concrete consumer is
   `army_view_roster.js`; T26 polish revisits the legacy
   `.tile-view-section-title` consumers so all section headings flow
   through one rule. */
.atlas-section-heading {
    font-family: var(--font-display);
    font-size: var(--fs-h2);
    font-weight: 600;
    color: var(--color-warning);
    margin: var(--space-3) 0 var(--space-1) 0;
    padding-bottom: var(--space-1);
    border-bottom: 1px solid var(--rule);
    letter-spacing: 0.03em;
}

/* Stockpile cards reserve a right-edge gutter for the R7 `→`
   transfer affordance. The button is absolutely positioned so the
   primary/secondary lines never collide with the chip on narrow
   widths. */
.atlas-card[data-variant="stockpile"] {
    padding-right: calc(var(--space-4) + 40px);
    position: relative;
}
.atlas-card[data-variant="stockpile"][data-kind="equipment"] {
    border-left-color: var(--accent);
}

/* R7 / AC37 visual half — `→` transfer affordance. Sits top-right of
   the host card (or row, in the inventory call sites that will land
   in T19/T20). 32 × 32 hit target with the `--tap-sm` floor so it
   passes the AC48 touch-target rule. The `data-pending="true"` flag
   that this task emits while T25 wires the click renders the button
   with a dashed border so QA can tell un-wired affordances apart from
   live ones. */
.atlas-row-transfer {
    position: absolute;
    top: var(--space-2);
    right: var(--space-2);
    width: 32px;
    height: 32px;
    min-width: var(--tap-sm, 36px);
    min-height: var(--tap-sm, 36px);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border: 1px solid var(--border-firm);
    border-radius: var(--radius-pill);
    background: var(--surface);
    color: var(--accent);
    font-family: var(--font-mono);
    font-size: 16px;
    line-height: 1;
    cursor: pointer;
    transition: background-color 120ms ease-out, border-color 120ms ease-out;
}
.atlas-row-transfer:hover {
    background: var(--surface-elev);
    border-color: var(--accent);
}
.atlas-row-transfer:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}
.atlas-row-transfer[data-pending="true"] {
    border-style: dashed;
    color: var(--ink-mute);
    cursor: not-allowed;
}
@media (prefers-reduced-motion: reduce) {
    .atlas-row-transfer { transition: none; }
}
/* mobile-ux-rework TASK_03 — AC04: lift the `→` transfer affordance
   to the AC01 mobile floor on phones. Desktop keeps the 32 × 32 hit
   zone (the legacy `--tap-sm, 36px` fallback gives a friendly mouse
   target without crowding the card chrome); the mobile override
   forces `var(--tap-mobile)` (48 px) so the thumb lands on it
   without missing the corner. */
@media (max-width: 639px) {
    .atlas-row-transfer {
        min-width: var(--tap-mobile);
        min-height: var(--tap-mobile);
    }
}

.tile-view-error {
    background: var(--bg-elev);
    border: 1px solid var(--color-danger);
    border-radius: var(--radius-sm);
    padding: var(--space-3);
    color: var(--text);
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3);
}
.tile-view-retry {
    background: var(--bg);
    border: 1px solid var(--color-warning);
    color: var(--color-warning);
    padding: var(--space-1) var(--space-3);
    border-radius: var(--radius-sm);
    cursor: pointer;
    font-family: var(--font-body);
    min-height: var(--tap-sm);
}
.tile-view-retry:hover,
.tile-view-retry:focus-visible {
    background: rgba(255, 183, 0, 0.1);
}
.tile-view-loading {
    color: var(--text-dim);
    font-size: var(--fs-label);
    padding: var(--space-3) 0;
}

/* Brass tabs strip — design.md "labelled brass tabs". The strip is the
 * elevated backplate; per-tab buttons sit on it without their own bg
 * unless active. Active tab gets a 2 px ochre underline below the
 * label row. */
.tile-view-tabs {
    display: flex;
    gap: 0;
    background: var(--bg-elev);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-sm);
    padding: 0;
    margin: 0;
    position: relative;
    overflow: hidden;
}
.tile-view-tab {
    flex: 0 0 auto;
    background: transparent;
    border: none;
    border-right: 1px solid var(--border-strong);
    color: var(--text-dim);
    padding: var(--space-2) var(--space-4);
    cursor: pointer;
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    min-height: var(--tap);
    font-family: var(--font-body);
    font-size: var(--fs-body);
    line-height: var(--lh-tight);
    /* Tab activation underline — 2 px ochre, drawn via box-shadow inset
     * so it doesn't disturb layout (no border-bottom flicker on toggle). */
    box-shadow: inset 0 -2px 0 transparent;
    transition:
        color 140ms ease,
        background 140ms ease,
        box-shadow 140ms ease;
}
.tile-view-tab:last-child {
    border-right: none;
}
.tile-view-tab:hover {
    background: rgba(255, 255, 255, 0.04);
    color: var(--text);
}
.tile-view-tab:focus-visible {
    outline: 2px solid var(--color-warning);
    outline-offset: -2px;
}
.tile-view-tab.is-active {
    color: var(--text);
    background: rgba(255, 183, 0, 0.06);
    box-shadow: inset 0 -2px 0 var(--color-warning);
}
.tile-view-tab-num {
    font-family: var(--font-mono);
    font-size: var(--fs-label);
    color: var(--color-warning);
    opacity: 0.85;
}
.tile-view-tab-label {
    font-family: var(--font-display);
    letter-spacing: 0.03em;
}

/* Tab content panels. Visibility toggled via `.hidden` (matches the
 * existing utility) so screen readers announce only the live one. */
.tile-view-tab-content {
    flex: 1 1 auto;
    min-height: 0;
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
}

/* Summary chip — N армій · M загонів · K лідерів · X юнітів.
 * Reuses --bg-elev + --border-strong + --color-warning for the brass
 * trim feel. Single line with mono font on the dot separator so the
 * counts feel like an engraved label. */
.tile-view-summary-chip {
    background: var(--bg-elev);
    border: 1px solid var(--border-strong);
    border-left: 3px solid var(--color-warning);
    border-radius: var(--radius-sm);
    padding: var(--space-2) var(--space-3);
    color: var(--text);
    font-family: var(--font-body);
    font-size: var(--fs-body);
    font-variant-numeric: tabular-nums;
    line-height: var(--lh-tight);
}

.tile-view-section {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
}
.tile-view-section-title {
    font-family: var(--font-display);
    font-size: var(--fs-h2);
    font-weight: 600;
    color: var(--color-warning);
    margin: var(--space-2) 0 var(--space-1) 0;
    letter-spacing: 0.03em;
}

.tile-view-row {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    background: var(--bg-elev);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    padding: var(--space-2) var(--space-3);
    color: var(--text);
    font-family: var(--font-body);
    font-size: var(--fs-body);
    cursor: pointer;
    text-align: left;
    width: 100%;
    min-height: var(--tap-sm);
    transition: background 140ms ease, border-color 140ms ease;
}
.tile-view-row:hover {
    background: rgba(255, 255, 255, 0.04);
    border-color: var(--border-strong);
}
.tile-view-row:focus-visible {
    outline: 2px solid var(--color-warning);
    outline-offset: -2px;
}
.tile-view-row--building {
    cursor: default;
}
.tile-view-row--building:hover {
    background: var(--bg-elev);
    border-color: var(--border);
}
.tile-view-row.is-foreign {
    border-left: 3px solid var(--color-danger);
}
.tile-view-row-name {
    font-weight: 600;
    color: var(--text);
}
.tile-view-row-meta {
    color: var(--text-dim);
    font-size: var(--fs-label);
    font-variant-numeric: tabular-nums;
}
.tile-view-row-owner {
    color: var(--text-muted);
    font-size: var(--fs-label);
    margin-top: 2px;
}

.tile-view-empty {
    color: var(--text-muted);
    font-size: var(--fs-label);
    font-style: italic;
    padding: var(--space-3) 0;
}

/* ─── Garrison card grid (race-portraits-and-unit-view TASK_07) ────
 * AC14 / AC15 / AC16 / AC17. The Гарнізон-tab body in
 * `tile_view_garrison.js` swaps the legacy row list for a 3-column
 * card layout populated by `_buildUnitCard(u)`. Each card lays out
 * three columns: 64 px portrait · race + template + leader chip stack
 * · count + HP-bar. Per-army header strips reuse the existing
 * `.tile-view-row.tile-view-row--army` rule so no new rule is needed
 * above the grid. Empty state is one full-width card carrying the
 * `ui-clarity-deferred AC1.4` CTA copy.
 */
.garrison-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
    gap: var(--space-2);
    margin-top: var(--space-1);
}
.garrison-card {
    display: grid;
    grid-template-columns: 64px 1fr auto;
    gap: var(--space-2);
    align-items: center;
    padding: var(--space-2);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: var(--bg-elev);
    color: var(--text);
    cursor: pointer;
    text-align: left;
    font: inherit;
    transition: background 140ms ease, border-color 140ms ease;
}
.garrison-card:hover {
    background: rgba(255, 255, 255, 0.04);
    border-color: var(--border-strong);
}
.garrison-card:focus-visible {
    outline: 2px solid var(--color-warning);
    outline-offset: -2px;
}
.garrison-card--leader {
    border-left: 3px solid var(--color-warning);
}
.garrison-card-portrait {
    width: 64px;
    height: 64px;
    border-radius: var(--radius-sm);
    object-fit: cover;
    background: var(--bg-elev);
    border: 1px solid var(--border);
    display: block;
}
.garrison-card-portrait--fallback {
    opacity: 0.7;
    filter: grayscale(0.5);
}
.garrison-card-titles {
    display: flex;
    flex-direction: column;
    gap: 2px;
    min-width: 0;
}
.garrison-card-race {
    /* ui-tile-army-cards-rework AC01/AC02 (TASK_01) — race becomes the
     * card's primary identity line: full body size, font-weight 600,
     * `--text` (not dim). 2-line clamp via `display: -webkit-box`
     * + `-webkit-line-clamp: 2` so long titles ("Mountain Dwarf
     * Pikeman") wrap inside the wider 220 px track instead of
     * ellipsing off. `overflow-wrap: anywhere` lets the wrap happen
     * mid-word on ultra-narrow phones. */
    color: var(--text);
    font-weight: 600;
    font-size: var(--fs-body);
    font-family: var(--font-body);
    white-space: normal;
    overflow-wrap: anywhere;
    overflow: hidden;
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2;
}
.garrison-card-race:empty {
    display: none;
}
.garrison-card-template {
    /* ui-tile-army-cards-rework AC01 (TASK_01) — role label is the
     * secondary line: dim, smaller, single-row ellipsis. CSS `:empty`
     * hides the row entirely when JS leaves `textContent` blank
     * (drop-on-Ополченець rule in `_buildUnitCard`). */
    color: var(--text-dim);
    font-weight: 400;
    font-size: var(--fs-label);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.garrison-card-template:empty {
    display: none;
}
.garrison-card-leader-tag {
    color: var(--color-warning);
    font-size: var(--fs-label);
    letter-spacing: 0.04em;
    text-transform: uppercase;
}
.garrison-card-meta {
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: 4px;
    min-width: 56px;
    font-variant-numeric: tabular-nums;
}
.garrison-card-count {
    color: var(--text);
    font-size: var(--fs-body);
    font-weight: 600;
}
.garrison-card-hpbar {
    width: 56px;
    height: 6px;
    border-radius: 3px;
    background: rgba(255, 255, 255, 0.08);
    overflow: hidden;
}
.garrison-card-hpbar-fill {
    height: 100%;
    background: var(--color-success, #3fb950);
    transition: width 200ms ease;
}
.garrison-card-hpbar-fill.is-wounded {
    background: var(--color-warning);
}
.garrison-card-hpbar-fill.is-critical {
    background: var(--color-danger);
}
/* ui-modal-redesign / TASK_05 (AC05) — `.garrison-card-template` now
 * carries an inline SVG sibling ahead of the role label. Inline-flex
 * keeps the icon + text aligned on one baseline; the legacy emoji
 * prefix lived in the same row's text content so this rule preserves
 * the existing single-row reading order. The SVG sizes to the row's
 * 14-px glyph rather than the 64-px primary slot. Still consumed by
 * ArmyView Склад post-TASK_15; T19 retires the rule when the
 * ArmyView roster migrates to atlas-card. */
.garrison-card-template svg.atlas-glyph {
    width: 14px;
    height: 14px;
    vertical-align: -2px;
    color: var(--text-dim);
}
/* ui-modal-redesign / TASK_15 — `.garrison-card--army`,
 * `.garrison-card--empty`, `.garrison-card-glyph`, and
 * `.garrison-card-army-dest` rules removed. Лагер armies + lone
 * units now render `.atlas-card[data-variant="army|unit"]` chrome;
 * the empty-state surface consumes `.atlas-card--empty` (AC09). */
/* AC14 mobile fold — single-column drop. Portrait stays 64 px so the
 * card anatomy reads identically on phone; the grid just collapses
 * its tracks. Lives in its own per-feature `@media` block alongside
 * the per-view rules (matches the TASK_05 `unit-view-header-top`
 * precedent later in the file). */
@media (max-width: 639px) {
    .garrison-grid {
        grid-template-columns: 1fr;
    }
}

/* Filter chips strip on Будівлі tab. Свої / Чужі / Усі. The active
 * chip glows in ochre; others sit on bg-elev with a thin border. */
.tile-view-filter-strip {
    display: flex;
    gap: var(--space-2);
    flex-wrap: wrap;
    margin-bottom: var(--space-2);
}
.tile-view-filter-chip {
    background: var(--bg-elev);
    border: 1px solid var(--border-strong);
    color: var(--text-dim);
    border-radius: 999px;
    padding: 4px 12px;
    font-family: var(--font-body);
    font-size: var(--fs-label);
    cursor: pointer;
    min-height: var(--tap-sm);
    transition: border-color 140ms ease, color 140ms ease, background 140ms ease;
}
.tile-view-filter-chip:hover {
    color: var(--text);
}
.tile-view-filter-chip:focus-visible {
    outline: 2px solid var(--color-warning);
    outline-offset: 2px;
}
.tile-view-filter-chip.is-active {
    border-color: var(--color-warning);
    color: var(--color-warning);
    background: rgba(255, 183, 0, 0.10);
}

.tile-view-buildings {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
}

/* Diorama (in-body, top row) — frame the canvas in a parchment block
 * so it reads as a focused stage. The canvas itself draws via Canvas2D
 * using the shared view/terrain.js + textures.js primitives, kept
 * consistent with the main-map look. As of ui-tileview-fixes TASK_04
 * the diorama lives in the body pane's top row alongside the
 * description band (see `.tile-view-top` below) — the shell's left
 * `workspace-pane-diorama` slot is collapsed via `:has(.tile-view-top)`
 * so the body pane spans the full shell width on desktop. */
.tile-view-diorama {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 100%;
    height: 100%;
    min-height: 240px;
}
.tile-view-diorama-canvas {
    background: var(--bg);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius);
    max-width: 100%;
    max-height: 100%;
    image-rendering: pixelated;
}

/* === TileView desktop split === */
/*
 * ui-tileview-fixes TASK_04 / AC7-AC9 — TileView body restructure.
 * Top row carries the description band (left, flexible) and the
 * diorama (right, capped 360px) side-by-side; second row carries the
 * tabs strip + active tabpanel. Three breakpoints:
 *   ≥1024px (AC7) — fixed 360px diorama column, description fills rest
 *                   ; diorama height capped 320px.
 *   720..1023px (AC8) — diorama narrows to ≤40% of the row + 360px
 *                       ; height capped 280px.
 *   <720px (AC9) — single column; description above diorama (TASK_05
 *                  follow-up will move description into a tab; for now
 *                  we just stack vertically). Diorama height ≤38vh.
 *
 * The shell-level left pane (`workspace-pane-diorama`) is hidden via
 * `:has(.tile-view-top)` so the body pane spans full width. `:has()` is
 * supported in evergreen browsers (Chrome 105+, Firefox 121+, Safari
 * 15.4+) per the frontend "modern evergreen — last 2 versions" target.
 */
.tile-view-top {
    /* Default (mobile / fallback): vertical stack. The desktop +
     * tablet breakpoints below promote this to a 2-column grid. */
    display: block;
}
.tile-view-top .tile-view-header {
    /* Reset border-bottom from the standalone header rule — inside the
     * top row the diorama-side seam carries the visual separation. */
    border-bottom: none;
    padding-bottom: 0;
    min-width: 0;
}
.tile-view-top-diorama {
    width: 100%;
    min-width: 0;
}
.tile-view-top .tile-view-diorama {
    width: 100%;
    height: auto;
    max-height: 38vh;
    min-height: 200px;
}
@media (min-width: 720px) {
    .tile-view-top {
        display: grid;
        grid-template-columns: minmax(0, 1fr) min(360px, 40%);
        gap: var(--space-3);
        align-items: stretch;
    }
    .tile-view-top .tile-view-header {
        min-width: 0;
    }
    .tile-view-top .tile-view-diorama {
        max-height: 280px;
    }
}
@media (min-width: 1024px) {
    .tile-view-top {
        grid-template-columns: minmax(0, 1fr) 360px;
    }
    .tile-view-top .tile-view-diorama {
        max-height: 320px;
    }
    /* ui-tileview-fixes TASK_05 / AC7 refinement — clamp the rendered
     * hex disc to ~280 px even though the slot is 360 px wide. The
     * extra 80 px breathing room lets the on-canvas building chips +
     * army glyphs sit comfortably without touching the diorama frame
     * border. Container stays 360 px so the surrounding chips/labels
     * rendered alongside (future intel hints) keep their slot. */
    .tile-view-top .tile-view-diorama canvas,
    .tile-view-top .tile-view-diorama-canvas {
        max-width: 280px;
        max-height: 280px;
    }
}
@media (max-width: 719px) {
    /* ui-tileview-fixes TASK_05 / AC9 — mobile collapses the top row
     * to diorama-only; the description band moves into the `[1] 📋
     * Опис` brass tab. The desktop top-row header copy is rendered
     * but hidden so a viewport resize from desktop → mobile (or vice
     * versa) flips visibility without re-render churn. */
    .tile-view-top {
        display: block;
    }
    .tile-view-top .tile-view-header {
        display: none;
    }
    .tile-view-top .tile-view-diorama {
        max-height: 38vh;
        margin-top: var(--space-2);
    }
}
/* ui-tileview-fixes TASK_05 / AC9 — `[1] 📋 Опис` tab + matching
 * tabpanel are mobile-only. At ≥720 px the description band lives in
 * the desktop top row, so the tab button + panel are hidden via
 * `display: none` (matches the existing `.hidden { display: none
 * !important }` utility's display rule but scoped to the breakpoint).
 * `:not(.hidden)` on the panel selector is defensive — `.hidden`
 * already wins via `!important`, but the explicit guard documents
 * which class chain is being defeated. */
@media (min-width: 720px) {
    .tile-view-tab[data-tv-tab="desc"],
    .tile-view-tab-content[data-tv-tab-content="desc"] {
        display: none;
    }
}
/* ui-tile-army-cards-rework TASK_08 / AC13 — terrain illustration
 * `<img>` mounted at the top of `.tile-view-top` by
 * `tile_view_skeleton.js`. The image is one of 18 pre-generated 256×256
 * PNGs under `/static/img/tiles/<tag>.png` (see `gen_images.py
 * --target=tile-illustrations`); the resolver in
 * `views/tile_view_illustration.js` picks the tag from terrain +
 * optional landmark building. A missing PNG hides the slot via
 * `.hidden` (`tile_view.js::_paintBody` binds the `error` listener);
 * the diorama already paints a visual fallback so the player never
 * sees an empty frame. The `1 / 1` aspect-ratio + `max-width: 320px`
 * keeps the slot square at every breakpoint without forcing a
 * fixed-pixel reflow when the surrounding header band wraps. The
 * lazy-load placeholder background (`var(--bg-elev)`) shows through
 * before the PNG decodes so the slot doesn't pop in.
 */
.tile-view-illustration {
    display: block;
    width: 100%;
    max-width: 480px;
    aspect-ratio: 3 / 2;
    margin: 0 auto var(--space-2);
    border-radius: var(--radius-md);
    border: 1px solid var(--border);
    background: var(--bg-elev);
    object-fit: cover;
}
.tile-view-illustration.hidden {
    display: none;
}
@media (min-width: 720px) {
    /* Desktop: the slot lives above the header band in the left
     * column of `.tile-view-top`'s 2-col grid; landscape 3:2 vista
     * sits comfortably at 360 px wide. */
    .tile-view-top .tile-view-illustration {
        max-width: 360px;
    }
}

/* When the body pane carries the new top-row layout, collapse the
 * shell's left diorama pane + the vertical divider so the body pane
 * spans the full shell width. The `:has()` selector is the cleanest
 * way to scope this without coupling tile_view.js to a body-level
 * class (path_planner.js's `body.hw-pp-open` precedent already shows
 * the body-class pattern; this surface deliberately stays JS-free).
 * Two scoping rules so the cascade beats the existing
 * `#workspace.workspace .workspace-pane-diorama` desktop rule. */
#workspace.workspace:has(.tile-view-top) {
    grid-template-columns: 1fr;
    grid-template-rows: 1fr;
}
#workspace.workspace:has(.tile-view-top) .workspace-pane-diorama,
#workspace.workspace:has(.tile-view-top) .workspace-divider {
    display: none;
}

/* Reduced-motion: kill the hover transitions so motion-sensitive
 * players don't see the colour fade on every tab/chip pass. */
@media (prefers-reduced-motion: reduce) {
    .tile-view-tab,
    .tile-view-row,
    .tile-view-filter-chip {
        transition: none;
    }
}
.force-reduced-motion .tile-view-tab,
.force-reduced-motion .tile-view-row,
.force-reduced-motion .tile-view-filter-chip {
    transition: none;
}

/* ─── TileView Розвідка + Чат tabs (ui-fullscreen-tile-detail TASK_10) ──
 * Intel cards + chat embed wrapper. Reuses the brass-tab + summary-chip
 * + row vocabulary already established by TASK_07; adds three new
 * selector blocks (no new design tokens):
 *   .tile-view-tab-badge     — `[4] Чат 💬 N` unread pill on the tab
 *                              button itself, hugs the label.
 *   .tile-view-intel-card    — foreign-army / foreign-unit cards on
 *                              the Розвідка panel; .is-foreign red
 *                              left border (already defined by the
 *                              shared .tile-view-row.is-foreign rule).
 *   .tile-view-chat-embed    — wrapper that hosts the embedded chat
 *                              thread once the chat backend returns
 *                              (currently shows a placeholder note;
 *                              see `tile_view_chat.js` docstring).
 *
 * `.tile-view-tab.hidden` reuses the existing `.hidden { display:none }`
 * utility so the Розвідка button disappears when the intel arrays are
 * empty (test FE-05).
 */
.tile-view-tab-badge {
    display: inline-flex;
    align-items: center;
    margin-left: var(--space-1);
    padding: 0 6px;
    border-radius: 999px;
    background: rgba(255, 183, 0, 0.16);
    color: var(--color-warning);
    border: 1px solid var(--color-warning);
    font-family: var(--font-mono);
    font-size: var(--fs-label);
    font-variant-numeric: tabular-nums;
    line-height: 1;
    min-width: var(--tap-sm);
    height: 1.4em;
    justify-content: center;
}

.tile-view-intel-card {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: var(--space-1);
    cursor: default;
}
.tile-view-intel-card:hover {
    /* No hover affordance — these cards are read-only intel,
     * not actionable rows. Keeps the foreign-red border crisp. */
    background: var(--bg-elev);
    border-color: var(--border);
}
.tile-view-intel-card-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--space-2);
    flex-wrap: wrap;
}
.tile-view-intel-card-units {
    list-style: none;
    margin: 4px 0 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 2px;
    color: var(--text-dim);
    font-size: var(--fs-label);
    font-family: var(--font-body);
    border-top: 1px dashed var(--border);
    padding-top: var(--space-1);
}
.tile-view-intel-card-unit {
    line-height: var(--lh-tight);
    font-variant-numeric: tabular-nums;
}
.tile-view-intel-units {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
}

/* ─── perception-stealth-system / TASK_11 (AC34) ────────────────────
 * Per-tier opacity / fill / hidden-block rules on intel cards. The
 * BE serializer already redacts the row's contents below the tier
 * threshold (silhouette → race only; count → +count; owner → +clan;
 * stats → +HP/morale/level; inventory → +items); the rules below paint
 * the visual signature so the player reads the tier at a glance.
 *
 * Hidden tier never reaches the DOM (BE drops the row per AC36); a
 * `data-tier="hidden"` defensive `display: none` keeps the failsafe
 * traceable to one line.
 */
.atlas-card[data-variant="intel"][data-tier="silhouette"] {
    opacity: 0.65;
    border-left-color: var(--ink-faint);
    background: linear-gradient(180deg,
        var(--surface-elev) 0%,
        rgba(80, 80, 90, 0.05) 100%);
}
.atlas-card[data-variant="intel"][data-tier="silhouette"] .atlas-monogram {
    color: var(--ink-mute);
    font-style: italic;
}
.atlas-card[data-variant="intel"][data-tier="silhouette"] .atlas-card-primary {
    color: var(--ink-mute);
    font-style: italic;
}
.atlas-card[data-variant="intel"][data-tier="count"] {
    opacity: 0.82;
    border-left-color: var(--ink-mute);
}
.atlas-card[data-variant="intel"][data-tier="owner"] {
    opacity: 1;
    border-left-color: var(--ink-mute);
}
.atlas-card[data-variant="intel"][data-tier="stats"] {
    opacity: 1;
    border-left-color: var(--accent);
}
.atlas-card[data-variant="intel"][data-tier="inventory"] {
    opacity: 1;
    border-left-color: var(--accent-strong);
}
.atlas-card[data-variant="intel"][data-tier="hidden"] {
    /* Defensive — BE drops hidden rows BEFORE serialization (AC36).
     * If a half-migrated payload still surfaces a `tier: 'hidden'`
     * row, this rule keeps it off-screen so the FE never leaks even
     * the silhouette signature for an entity the BE meant to hide. */
    display: none;
}
/* perception-stealth-system / TASK_12 (AC35) — per-tier visual grammar
 * for the BuildingCard chrome on the Будівлі tab. Mirrors the intel-
 * unit progression: presence_only reads as a muted grey "something is
 * here" stamp; type adds the kind icon + label; owner reads at full
 * opacity with a muted rail; details promotes the rail to the accent
 * brass; contents promotes the rail to accent-strong (full reveal).
 * Hidden tier never reaches the DOM (BE drops the row per AC19); the
 * defensive `data-tier="hidden"` `display: none` keeps the failsafe
 * traceable to one rule. */
.atlas-card[data-variant="building"][data-tier="presence_only"] {
    opacity: 0.65;
    border-left-color: var(--ink-faint);
    background: linear-gradient(180deg,
        var(--surface-elev) 0%,
        rgba(80, 80, 90, 0.05) 100%);
}
.atlas-card[data-variant="building"][data-tier="presence_only"] .atlas-monogram {
    color: var(--ink-mute);
    font-style: italic;
}
.atlas-card[data-variant="building"][data-tier="presence_only"] .atlas-card-primary {
    color: var(--ink-mute);
    font-style: italic;
}
.atlas-card[data-variant="building"][data-tier="type"] {
    opacity: 0.82;
    border-left-color: var(--ink-mute);
}
.atlas-card[data-variant="building"][data-tier="owner"] {
    opacity: 1;
    border-left-color: var(--ink-mute);
}
.atlas-card[data-variant="building"][data-tier="details"] {
    opacity: 1;
    border-left-color: var(--accent);
}
.atlas-card[data-variant="building"][data-tier="contents"] {
    opacity: 1;
    border-left-color: var(--accent-strong);
}
.atlas-card[data-variant="building"][data-tier="hidden"] {
    /* Defensive — BE drops hidden rows BEFORE serialization (AC19).
     * Mirrors the intel-card hidden-tier rule. */
    display: none;
}
.tile-view-intel-card-hint {
    color: var(--ink-mute);
    font-style: italic;
    font-size: var(--fs-label);
}
.tile-view-intel-card-items {
    list-style: none;
    margin: 4px 0 0;
    padding: 0;
    display: flex;
    flex-wrap: wrap;
    gap: 4px var(--space-2);
    color: var(--text-dim);
    font-size: var(--fs-label);
    font-family: var(--font-body);
    border-top: 1px dashed var(--border);
    padding-top: var(--space-1);
}
.tile-view-intel-card-item {
    line-height: var(--lh-tight);
    padding: 0 6px;
    background: rgba(255, 255, 255, 0.03);
    border: 1px solid var(--border);
    border-radius: var(--radius-pill);
    font-variant-numeric: tabular-nums;
}
/* Per-army-member rows mirror the parent card's tier styling so a
 * mixed-tier army (one member at `stats`, another at `silhouette`)
 * surfaces the per-row reveal level visibly. */
.tile-view-intel-card-unit[data-tier="silhouette"] {
    color: var(--ink-mute);
    font-style: italic;
}
.tile-view-intel-card-unit[data-tier="count"] {
    color: var(--text-dim);
}

.tile-view-chat-embed {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    background: var(--bg-elev);
    border: 1px solid var(--border-strong);
    border-left: 3px solid var(--color-warning);
    border-radius: var(--radius-sm);
    padding: var(--space-3);
    color: var(--text);
    min-height: 120px;
}
.tile-view-chat-placeholder {
    margin: 0;
    color: var(--text-dim);
    font-size: var(--fs-label);
    font-style: italic;
    line-height: var(--lh-relaxed, 1.5);
}

/* Розвідка / Чат tabs share the brass-tab transitions; the
 * reduced-motion respect rule above already covers `.tile-view-tab`,
 * so nothing extra is needed for them. The new badge follows suit
 * because it lives inside the tab button. */


/* ─── UnitView (ui-fullscreen-tile-detail TASK_08) ─────────────────────
 * Read-only fullscreen UnitView. Reuses the brass-tab + summary-chip
 * vocabulary established by TileView (TASK_07) so both surfaces share
 * the same visual language. Tokens reused only — no new ones added.
 *   --color-warning   →  ochre underline + tile-chip border + leader chip
 *   --color-primary   →  HP bar fill (cyan-ish, kept consistent with
 *                        own-army glyphs)
 *   --color-success   →  morale bar fill
 *   --color-danger    →  is-disabled action row marker
 *   --bg-elev         →  cell + chip backplates
 *   --border-strong   →  cell borders
 *   --text-dim        →  meta lines / inactive labels
 *   --font-display    →  Cinzel for unit title + section titles
 *   --font-mono       →  JetBrains Mono for tab number glyph + bar
 *                        numerics
 */
.unit-view {
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
    height: 100%;
    min-height: 0;
}

.unit-view-header {
    border-bottom: 1px solid var(--border);
    padding-bottom: var(--space-3);
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
}
.unit-view-title-row {
    display: flex;
    align-items: baseline;
    gap: var(--space-2);
    flex-wrap: wrap;
}
.unit-view-title {
    font-family: var(--font-display);
    font-size: var(--fs-h1);
    font-weight: 600;
    color: var(--text);
    letter-spacing: 0.04em;
    margin: 0;
}

/* race-portraits-and-unit-view TASK_05 — portrait-led header. 3-column
 * grid: 96 px portrait · race+template stack · level/leader meta. The
 * portrait image uses a distinct `.unit-view-portrait-img` selector so
 * it doesn't collide with the legacy `.unit-view-portrait` glyph card
 * still used inside the Огляд tab body (AC2.4 regression-pinned). */
.unit-view-header-top {
    display: grid;
    grid-template-columns: 96px 1fr auto;
    gap: var(--space-3);
    align-items: center;
}
.unit-view-portrait-img {
    width: 96px;
    height: 96px;
    border-radius: var(--radius-sm);
    object-fit: cover;
    background: var(--bg-elev);
    border: 1px solid var(--border-strong);
    display: block;
}
.unit-view-header-titles {
    display: flex;
    flex-direction: column;
    gap: 2px;
    min-width: 0;
}
.unit-view-race {
    font-family: var(--font-display);
    font-size: var(--fs-label);
    color: var(--text-dim);
    letter-spacing: 0.04em;
    opacity: 0.85;
}
/* Race-agnostic units (`unit.race_name === null`) render an empty
 * `.unit-view-race` div — collapse the row so the template name sits
 * flush. Spec AC10 / risk note: "race-agnostic templates (siege engines,
 * summoned constructs) — Title fallback: if race is null, show
 * template-name only". */
.unit-view-race:empty {
    display: none;
}
.unit-view-template {
    font-family: var(--font-display);
    font-size: var(--fs-h1);
    font-weight: 600;
    color: var(--text);
    letter-spacing: 0.04em;
    margin: 0;
}
.unit-view-header-meta {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    flex-wrap: wrap;
    justify-self: end;
}

/* race-portraits-and-unit-view TASK_05 / AC12 — mobile fold. Portrait
 * shrinks to 64 px; the meta column drops to a new row spanning the
 * full grid so the title stack stays readable next to a smaller
 * thumbnail without truncation. */
@media (max-width: 639px) {
    .unit-view-header-top {
        grid-template-columns: 64px 1fr;
        grid-auto-rows: auto;
        gap: var(--space-2);
    }
    .unit-view-portrait-img {
        width: 64px;
        height: 64px;
    }
    .unit-view-header-meta {
        grid-column: 1 / -1;
        justify-self: start;
    }
}
.unit-view-level {
    font-family: var(--font-mono);
    font-size: var(--fs-label);
    color: var(--text-dim);
}
.unit-view-leader-chip {
    background: rgba(255, 183, 0, 0.10);
    border: 1px solid var(--color-warning);
    color: var(--color-warning);
    border-radius: 999px;
    padding: 2px 10px;
    font-size: var(--fs-label);
    /* ui-modal-redesign / TASK_05 (AC05) — host an inline SVG glyph
     * (via `util/glyph.js::iconSvg('leader')`) on the same baseline
     * as the "Лідер" label. */
    display: inline-flex;
    align-items: center;
    gap: 4px;
}
.unit-view-leader-chip svg.atlas-glyph {
    width: 14px;
    height: 14px;
    display: block;
}

.unit-view-lineage {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    gap: var(--space-3);
    flex-wrap: wrap;
    color: var(--text-dim);
    font-size: var(--fs-label);
}
.unit-view-lineage li {
    display: inline-flex;
    align-items: center;
    gap: 4px;
}
.unit-view-lineage-glyph {
    font-size: 1em;
    /* ui-modal-redesign / TASK_05 (AC05) — host an inline SVG glyph
     * (via `util/glyph.js::iconSvg(slug)`) on the same baseline as
     * the lineage label. `currentColor` carries the dim text-tint
     * into the stroke. */
    display: inline-flex;
    align-items: center;
    color: var(--text-dim);
}
.unit-view-lineage-glyph svg.atlas-glyph {
    width: 14px;
    height: 14px;
    display: block;
}
.unit-view-lineage-label {
    color: var(--text);
}

/* ui-modal-redesign / TASK_05 (AC05) — read-only action-row glyph
 * slot for the "Доступні дії" list. Mounts a 14-px inline SVG
 * (`util/glyph.js::iconSvg(slug)`) ahead of the action label. The
 * row's existing flex layout from `.unit-view-action-row` aligns
 * the glyph + label + status chip on one baseline. */
.unit-view-action-glyph {
    display: inline-flex;
    align-items: center;
    color: var(--text-dim);
    flex: 0 0 auto;
}
.unit-view-action-glyph svg.atlas-glyph {
    width: 14px;
    height: 14px;
    display: block;
}

.unit-view-chips {
    display: flex;
    gap: var(--space-2);
    flex-wrap: wrap;
}
.unit-view-tile-chip {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    background: var(--bg-elev);
    border: 1px solid var(--color-warning);
    border-radius: 999px;
    color: var(--color-warning);
    padding: 4px 12px;
    font-family: var(--font-mono);
    font-size: var(--fs-label);
    cursor: pointer;
    min-height: var(--tap-sm);
    transition: background 140ms ease;
}
.unit-view-tile-chip:hover,
.unit-view-tile-chip:focus-visible {
    background: rgba(255, 183, 0, 0.10);
    outline: none;
}
.unit-view-tile-chip-arrow {
    font-family: var(--font-body);
}
.unit-view-army-chip {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    background: var(--bg-elev);
    border: 1px solid var(--accent, var(--color-warning));
    border-radius: 999px;
    color: var(--accent, var(--color-warning));
    padding: 4px 12px;
    font-family: var(--font-mono);
    font-size: var(--fs-label);
    cursor: pointer;
    min-height: var(--tap-sm);
    transition: background 140ms ease;
}
.unit-view-army-chip:hover,
.unit-view-army-chip:focus-visible {
    background: rgba(255, 183, 0, 0.10);
    outline: none;
}
.unit-view-army-chip-arrow {
    font-family: var(--font-body);
}
.unit-view-owner-chip {
    background: var(--bg-elev);
    border: 1px solid var(--border-strong);
    border-radius: 999px;
    color: var(--text-dim);
    padding: 4px 12px;
    font-size: var(--fs-label);
}

/* HP / morale / AP bars. Each row: label · meter · numeric. The meter
 * fills 0..100% via inline `width` on `.unit-view-bar-fill`. Colours
 * differ per kind so the player can scan all three at a glance. */
.unit-view-bars {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
    gap: var(--space-2);
}
.unit-view-bar {
    display: grid;
    grid-template-columns: 64px 1fr auto;
    align-items: center;
    gap: var(--space-2);
    background: var(--bg-elev);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-sm);
    padding: 4px 10px;
}
.unit-view-bar-label {
    font-family: var(--font-display);
    font-size: var(--fs-label);
    color: var(--text-dim);
    letter-spacing: 0.04em;
}
.unit-view-bar-meter {
    display: block;
    width: 100%;
    height: 8px;
    background: rgba(255, 255, 255, 0.06);
    border-radius: 999px;
    overflow: hidden;
    position: relative;
}
.unit-view-bar-fill {
    display: block;
    height: 100%;
    background: var(--color-primary);
    transition: width 140ms ease;
}
.unit-view-bar--hp .unit-view-bar-fill { background: var(--color-primary); }
/* morale-100-rework TASK_07 / AC15 — three-zone gradient
 * (red 0-30 / yellow 30-60 / green 60-100). Approach: paint the FULL
 * gradient on the meter rail (always 100% wide), leave the fill
 * rectangle visually transparent, and dim the un-filled remainder
 * via a `::after` overlay positioned at `left: var(--morale-fill-pct)`.
 * The `--morale-fill-pct` custom property is written by
 * `unit_view_overview.js::decorateMoraleBar` from the inline
 * `width: N%` already set by `buildBar()`. Net effect: the colour
 * the player sees up to the fill edge maps 1:1 to the absolute
 * morale band (red 0-30 / yellow 30-60 / green 60-100); the
 * remainder is dimmed so the player still reads the future band
 * the bar would land in if morale climbed. See `design.md` §UI for
 * the three-zone wording. */
.unit-view-bar--morale .unit-view-bar-meter {
    background: linear-gradient(
        90deg,
        var(--color-danger) 0%,
        var(--color-danger) 30%,
        var(--color-warning) 30%,
        var(--color-warning) 60%,
        var(--color-success) 60%,
        var(--color-success) 100%
    );
}
.unit-view-bar--morale .unit-view-bar-fill {
    background: transparent;
}
.unit-view-bar--morale .unit-view-bar-meter::after {
    content: '';
    position: absolute;
    top: 0;
    bottom: 0;
    left: var(--morale-fill-pct, 0%);
    right: 0;
    background: rgba(0, 0, 0, 0.55);
    pointer-events: none;
    transition: left 140ms ease;
}
@media (prefers-reduced-motion: reduce) {
    .unit-view-bar--morale .unit-view-bar-meter::after { transition: none; }
}
.unit-view-bar--ap .unit-view-bar-fill { background: var(--color-warning); }
.unit-view-bar-num {
    font-family: var(--font-mono);
    font-size: var(--fs-label);
    color: var(--text);
    font-variant-numeric: tabular-nums;
}

.unit-view-tabs {
    display: flex;
    gap: 0;
    background: var(--bg-elev);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-sm);
    padding: 0;
    margin: 0;
    position: relative;
    overflow: hidden;
}
.unit-view-tab {
    flex: 0 0 auto;
    background: transparent;
    border: none;
    border-right: 1px solid var(--border-strong);
    color: var(--text-dim);
    padding: var(--space-2) var(--space-4);
    cursor: pointer;
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    min-height: var(--tap);
    font-family: var(--font-body);
    font-size: var(--fs-body);
    line-height: var(--lh-tight);
    box-shadow: inset 0 -2px 0 transparent;
    transition: color 140ms ease, background 140ms ease, box-shadow 140ms ease;
}
.unit-view-tab:last-child {
    border-right: none;
}
.unit-view-tab:hover {
    background: rgba(255, 255, 255, 0.04);
    color: var(--text);
}
.unit-view-tab:focus-visible {
    outline: 2px solid var(--color-warning);
    outline-offset: -2px;
}
.unit-view-tab.is-active {
    color: var(--text);
    background: rgba(255, 183, 0, 0.06);
    box-shadow: inset 0 -2px 0 var(--color-warning);
}
.unit-view-tab-num {
    font-family: var(--font-mono);
    font-size: var(--fs-label);
    color: var(--color-warning);
    opacity: 0.85;
}
.unit-view-tab-label {
    font-family: var(--font-display);
    letter-spacing: 0.03em;
}

.unit-view-tab-content {
    flex: 1 1 auto;
    min-height: 0;
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
}

.unit-view-section {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
}
.unit-view-section-title {
    font-family: var(--font-display);
    font-size: var(--fs-h2);
    font-weight: 600;
    color: var(--color-warning);
    margin: var(--space-2) 0 var(--space-1) 0;
    letter-spacing: 0.03em;
}

.unit-view-portrait {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-2);
    padding: var(--space-3);
    background: var(--bg-elev);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius);
    min-height: 96px;
}
.unit-view-portrait-glyph {
    font-size: 56px;
    line-height: 1;
}
.unit-view-portrait-count {
    font-family: var(--font-mono);
    font-size: var(--fs-h2);
    color: var(--text);
}

/* 2×2 stat block — collapses to one column under 380 px. Mirrors the
 * .u-stats vocabulary used by units_panel.js so the workspace surface
 * feels visually adjacent to the legacy panel even though the markup
 * is fresh. */
.unit-view-stats {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: var(--space-2);
}
.unit-view-stats-cell {
    background: var(--bg-elev);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-sm);
    padding: var(--space-2) var(--space-3);
    display: flex;
    flex-direction: column;
    gap: 4px;
}
.unit-view-stats-cell-label {
    font-family: var(--font-display);
    font-size: var(--fs-label);
    color: var(--text-dim);
    letter-spacing: 0.04em;
}
.unit-view-stats-cell-body {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-3);
    font-family: var(--font-mono);
    font-size: var(--fs-body);
    color: var(--text);
    font-variant-numeric: tabular-nums;
}
.unit-view-stat {
    display: inline-flex;
    align-items: baseline;
    gap: 4px;
}
.unit-view-stat-k {
    color: var(--text-dim);
}
.unit-view-stat-v {
    color: var(--text);
}
/* mobile-ux-rework TASK_01 — single-column lists at the Mobile-narrow
   tier (≤479) per design.md breakpoint chain. Was 380. */
@media (max-width: 479px) {
    .unit-view-stats {
        grid-template-columns: 1fr;
    }
}

.unit-view-list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 4px;
}
.unit-view-list li {
    display: flex;
    align-items: baseline;
    gap: var(--space-2);
    background: var(--bg-elev);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    padding: 4px 10px;
}
.unit-view-list-name {
    color: var(--text);
}
.unit-view-list-meta {
    color: var(--text-dim);
    font-size: var(--fs-label);
    font-family: var(--font-mono);
    font-variant-numeric: tabular-nums;
}

/* Available-actions list. `is-disabled` rows surface the gating reason
 * with a `data-disable-reason` attribute so they integrate with the
 * existing `attachDisabledPopover` helper if a future task wants
 * tooltip popovers (today: inline `.unit-view-action-why` text).  */
.unit-view-action-list {
    display: flex;
    flex-direction: column;
    gap: 4px;
}
.unit-view-action-row {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--space-3);
    background: var(--bg-elev);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    padding: 6px 10px;
    color: var(--text);
}
.unit-view-action-row.is-disabled {
    border-left: 3px solid var(--color-danger);
    opacity: 0.75;
}
.unit-view-action-label {
    font-family: var(--font-body);
}
.unit-view-action-why {
    color: var(--color-danger);
    font-size: var(--fs-label);
}
.unit-view-action-ok {
    color: var(--color-success);
    font-size: var(--fs-label);
    font-family: var(--font-mono);
}

.unit-view-empty {
    color: var(--text-muted);
    font-size: var(--fs-label);
    font-style: italic;
    padding: var(--space-3) 0;
}
.unit-view-loading {
    color: var(--text-dim);
    font-size: var(--fs-label);
    padding: var(--space-3) 0;
}
.unit-view-error {
    background: var(--bg-elev);
    border: 1px solid var(--color-danger);
    border-radius: var(--radius-sm);
    padding: var(--space-3);
    color: var(--text);
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3);
}
.unit-view-retry {
    background: var(--bg);
    border: 1px solid var(--color-warning);
    color: var(--color-warning);
    padding: var(--space-1) var(--space-3);
    border-radius: var(--radius-sm);
    cursor: pointer;
    font-family: var(--font-body);
    min-height: var(--tap-sm);
}
.unit-view-retry:hover,
.unit-view-retry:focus-visible {
    background: rgba(255, 183, 0, 0.1);
}

.unit-view-inventory-placeholder {
    text-align: center;
}

/* ─── UnitView Інвентар — ui-fullscreen-tile-detail TASK_11 ────────────
 * Two-column inventory pane: left = unit pack (with `.capacity-meter
 * [data-tier]`), right = tile cache (count chip — the cache is
 * unbounded per current design). Each `Item` model row is a discrete
 * draggable list item; transfer = single row flip via the shipped
 * `item_transfer` instant command (TASK_04).
 */
.unit-view-inventory {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: var(--space-3);
    align-items: start;
}
.unit-view-inventory-col {
    background: var(--bg-elev);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-sm);
    padding: var(--space-2);
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    min-height: 160px;
    transition: border-color 140ms ease, background 140ms ease;
}
.unit-view-inventory-col.is-drag-target {
    border-color: var(--color-warning);
    background: rgba(255, 183, 0, 0.06);
}
.unit-view-inventory-col-head {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
}
.unit-view-inventory-meter {
    min-height: 14px;
}
.unit-view-inventory-cache-chip {
    display: inline-flex;
    align-items: center;
    background: var(--bg);
    border: 1px solid var(--border);
    border-radius: 999px;
    padding: 2px 10px;
    font-family: var(--font-mono);
    font-size: var(--fs-label);
    color: var(--text-dim);
}
.unit-view-inventory-sort {
    display: flex;
    gap: 4px;
    flex-wrap: wrap;
}
.unit-view-inventory-sort-btn {
    background: transparent;
    border: 1px solid var(--border);
    color: var(--text-dim);
    border-radius: var(--radius-sm);
    padding: 2px 8px;
    font-size: var(--fs-label);
    font-family: var(--font-body);
    cursor: pointer;
    min-height: var(--tap-sm);
    transition: color 140ms ease, border-color 140ms ease;
}
.unit-view-inventory-sort-btn:hover,
.unit-view-inventory-sort-btn:focus-visible {
    color: var(--text);
    outline: none;
    border-color: var(--color-warning);
}
.unit-view-inventory-sort-btn.is-active {
    color: var(--color-warning);
    border-color: var(--color-warning);
}
.unit-view-inventory-list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 4px;
}
.unit-view-inventory-row {
    display: grid;
    grid-template-columns: 1fr auto auto;
    align-items: baseline;
    gap: var(--space-2);
    background: var(--bg);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    padding: 6px 10px;
    cursor: grab;
    transition: opacity 140ms ease, border-color 140ms ease;
}
.unit-view-inventory-row:hover {
    border-color: var(--color-warning);
}
.unit-view-inventory-row.is-dragging {
    opacity: 0.5;
    cursor: grabbing;
}
.unit-view-inventory-row.is-pending {
    opacity: 0.6;
    border-style: dashed;
}
.unit-view-inventory-row-name {
    color: var(--text);
}
.unit-view-inventory-row-meta {
    color: var(--text-dim);
    font-size: var(--fs-label);
    font-family: var(--font-mono);
    font-variant-numeric: tabular-nums;
}
.unit-view-inventory-row-arrow {
    background: transparent;
    border: 1px solid var(--border);
    color: var(--color-warning);
    border-radius: 999px;
    width: var(--tap-sm);
    height: var(--tap-sm);
    min-width: var(--tap-sm);
    cursor: pointer;
    font-size: var(--fs-body);
    transition: background 140ms ease, border-color 140ms ease;
}
.unit-view-inventory-row-arrow:hover,
.unit-view-inventory-row-arrow:focus-visible {
    background: rgba(255, 183, 0, 0.1);
    border-color: var(--color-warning);
    outline: none;
}
.unit-view-inventory-empty {
    color: var(--text-muted);
    font-size: var(--fs-label);
    font-style: italic;
    padding: var(--space-2) 0;
    text-align: center;
}
@media (max-width: 639px) {
    .unit-view-inventory {
        grid-template-columns: 1fr;
    }
}

@media (prefers-reduced-motion: reduce) {
    .unit-view-tab,
    .unit-view-tile-chip,
    .unit-view-bar-fill,
    .unit-view-inventory-row,
    .unit-view-inventory-col,
    .unit-view-inventory-row-arrow {
        transition: none;
    }
}
.force-reduced-motion .unit-view-tab,
.force-reduced-motion .unit-view-tile-chip,
.force-reduced-motion .unit-view-bar-fill,
.force-reduced-motion .unit-view-inventory-row,
.force-reduced-motion .unit-view-inventory-col,
.force-reduced-motion .unit-view-inventory-row-arrow {
    transition: none;
}

/* ─── ArmyView — ui-fullscreen-tile-detail TASK_09 ─────────────────────────
 * Reuses the unit-view vocabulary (brass tabs, --color-warning ochre
 * underline, JetBrains Mono numerics, Cinzel display) so the three
 * workspace surfaces feel adjacent. Capacity bar wires through the
 * existing `--load-*` token palette via `data-tier` (matches what the
 * armies-panel meter already does — single source of truth from
 * economy-rework T18).
 */
.army-view {
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
    height: 100%;
    min-height: 0;
}
.army-view-header {
    border-bottom: 1px solid var(--border);
    padding-bottom: var(--space-3);
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
}
.army-view-title-row {
    display: flex;
    align-items: baseline;
    gap: var(--space-2);
    flex-wrap: wrap;
}
.army-view-title {
    font-family: var(--font-display);
    font-size: var(--fs-h1);
    font-weight: 600;
    color: var(--text);
    letter-spacing: 0.04em;
    margin: 0;
}
.army-view-leader-chip {
    background: rgba(255, 183, 0, 0.10);
    border: 1px solid var(--color-warning);
    color: var(--color-warning);
    border-radius: 999px;
    padding: 2px 10px;
    font-size: var(--fs-label);
}
.army-view-lineage {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    gap: var(--space-3);
    flex-wrap: wrap;
    color: var(--text-dim);
    font-size: var(--fs-label);
}
.army-view-lineage li {
    display: inline-flex;
    align-items: center;
    gap: 4px;
}
.army-view-lineage-glyph {
    font-size: 1em;
    /* ui-modal-redesign / TASK_05 (AC05) — host an inline SVG glyph
     * (via `util/glyph.js::iconSvg(slug)`) on the same baseline as
     * the label. `inline-flex` centres the 14-px stroke icon
     * vertically; `currentColor` carries the lineage row's dim
     * text-tint into the stroke. */
    display: inline-flex;
    align-items: center;
    color: var(--text-dim);
}
.army-view-lineage-glyph svg.atlas-glyph {
    width: 14px;
    height: 14px;
    display: block;
}
/* Leader-chip gains the same inline-flex treatment so the SVG glyph
 * sits flush against the "Лідер є" label without a gap collapse. */
.army-view-leader-chip {
    display: inline-flex;
    align-items: center;
    gap: 4px;
}
.army-view-leader-chip svg.atlas-glyph {
    width: 14px;
    height: 14px;
    display: block;
}
.army-view-lineage-label {
    color: var(--text);
}
.army-view-chips {
    display: flex;
    gap: var(--space-2);
    flex-wrap: wrap;
}
.army-view-tile-chip {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    background: var(--bg-elev);
    border: 1px solid var(--color-warning);
    border-radius: 999px;
    color: var(--color-warning);
    padding: 4px 12px;
    font-family: var(--font-mono);
    font-size: var(--fs-label);
    cursor: pointer;
    min-height: var(--tap-sm);
    transition: background 140ms ease;
}
.army-view-tile-chip:hover,
.army-view-tile-chip:focus-visible {
    background: rgba(255, 183, 0, 0.10);
    outline: none;
}
.army-view-tile-chip-arrow {
    font-family: var(--font-body);
}
.army-view-move-chip {
    background: var(--bg-elev);
    border: 1px solid var(--border-strong);
    border-radius: 999px;
    color: var(--text-dim);
    padding: 4px 12px;
    font-family: var(--font-mono);
    font-size: var(--fs-label);
}

.army-view-bars {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
    gap: var(--space-2);
}
.army-view-bar {
    display: grid;
    grid-template-columns: 72px 1fr auto;
    align-items: center;
    gap: var(--space-2);
    background: var(--bg-elev);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-sm);
    padding: 4px 10px;
}
.army-view-bar-label {
    font-family: var(--font-display);
    font-size: var(--fs-label);
    color: var(--text-dim);
    letter-spacing: 0.04em;
}
.army-view-bar-meter {
    display: block;
    width: 100%;
    height: 8px;
    background: rgba(255, 255, 255, 0.06);
    border-radius: 999px;
    overflow: hidden;
    position: relative;
}
.army-view-bar-fill {
    display: block;
    height: 100%;
    background: var(--color-primary);
    transition: width 140ms ease;
}
.army-view-bar--ap     .army-view-bar-fill { background: var(--color-warning); }
.army-view-bar--morale .army-view-bar-fill { background: var(--color-success); }
.army-view-bar--load   .army-view-bar-fill { background: var(--load-normal, var(--color-primary)); }
.army-view-bar--load[data-tier="ok"]       .army-view-bar-fill { background: var(--load-ok, var(--color-success)); }
.army-view-bar--load[data-tier="normal"]   .army-view-bar-fill { background: var(--load-normal, var(--color-primary)); }
.army-view-bar--load[data-tier="warn"]     .army-view-bar-fill { background: var(--load-warn, var(--color-warning)); }
.army-view-bar--load[data-tier="overload"] .army-view-bar-fill { background: var(--load-overload, var(--color-danger)); }
.army-view-bar--load[data-tier="block"]    .army-view-bar-fill { background: var(--load-block, var(--color-danger)); }
.army-view-bar-num {
    font-family: var(--font-mono);
    font-size: var(--fs-label);
    color: var(--text);
    font-variant-numeric: tabular-nums;
}

.army-view-tabs {
    display: flex;
    gap: 0;
    background: var(--bg-elev);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-sm);
    padding: 0;
    margin: 0;
    position: relative;
    overflow: hidden;
}
.army-view-tab {
    flex: 0 0 auto;
    background: transparent;
    border: none;
    border-right: 1px solid var(--border-strong);
    color: var(--text-dim);
    padding: var(--space-2) var(--space-4);
    cursor: pointer;
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    min-height: var(--tap);
    font-family: var(--font-body);
    font-size: var(--fs-body);
    line-height: var(--lh-tight);
    box-shadow: inset 0 -2px 0 transparent;
    transition: color 140ms ease, background 140ms ease, box-shadow 140ms ease;
}
.army-view-tab:last-child {
    border-right: none;
}
.army-view-tab:hover {
    background: rgba(255, 255, 255, 0.04);
    color: var(--text);
}
.army-view-tab:focus-visible {
    outline: 2px solid var(--color-warning);
    outline-offset: -2px;
}
.army-view-tab.is-active {
    color: var(--text);
    background: rgba(255, 183, 0, 0.06);
    box-shadow: inset 0 -2px 0 var(--color-warning);
}
.army-view-tab-num {
    font-family: var(--font-mono);
    font-size: var(--fs-label);
    color: var(--color-warning);
    opacity: 0.85;
}
.army-view-tab-label {
    font-family: var(--font-display);
    letter-spacing: 0.03em;
}

.army-view-tab-content {
    flex: 1 1 auto;
    min-height: 0;
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
}

.army-view-section {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
}
.army-view-section-title {
    font-family: var(--font-display);
    font-size: var(--fs-h2);
    font-weight: 600;
    color: var(--color-warning);
    margin: var(--space-2) 0 var(--space-1) 0;
    letter-spacing: 0.03em;
}

.army-view-roster {
    display: flex;
    flex-direction: column;
    gap: 4px;
}
.army-view-row {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--space-3);
    background: var(--bg-elev);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    padding: 6px 10px;
    color: var(--text);
    cursor: pointer;
    text-align: left;
    font: inherit;
    width: 100%;
}
.army-view-row:hover {
    background: rgba(255, 255, 255, 0.04);
}
.army-view-row:focus-visible {
    outline: 2px solid var(--color-warning);
    outline-offset: -2px;
}
.army-view-row.is-leader {
    border-left: 3px solid var(--color-warning);
}
.army-view-row-name {
    font-family: var(--font-body);
}
.army-view-row-meta {
    color: var(--text-dim);
    font-size: var(--fs-label);
    font-family: var(--font-mono);
    font-variant-numeric: tabular-nums;
}

.army-view-action-list {
    display: flex;
    flex-direction: column;
    gap: 4px;
}
.army-view-action-row {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--space-3);
    background: var(--bg-elev);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    padding: 6px 10px;
    color: var(--text);
    cursor: pointer;
    text-align: left;
    font: inherit;
    width: 100%;
    min-height: var(--tap);
}
.army-view-action-row:hover {
    background: rgba(255, 183, 0, 0.06);
}
.army-view-action-row:focus-visible {
    outline: 2px solid var(--color-warning);
    outline-offset: -2px;
}
.army-view-action-label {
    font-family: var(--font-body);
    color: var(--text);
}
.army-view-action-hint {
    color: var(--text-dim);
    font-size: var(--fs-label);
}
.army-view-actions-hint {
    color: var(--text-muted);
    font-size: var(--fs-label);
    font-style: italic;
    padding: var(--space-2) 0;
}

/* ui-clarity-pass TASK_01 / AC-A1 + AC-A2 — visible-disabled action
 * row used by ArmyView Дії tab (Attack / Patrol / Disband / Set leader)
 * and UnitView "Доступні дії" gated rows. Pointer-events stay on so
 * the native browser tooltip (`title=…`) can fire on hover/focus; the
 * click is short-circuited at the JS delegation site via
 * `data-disabled="true"`. The `.action-row-disabled` class is shared
 * across both views — co-located with the per-view rules so a future
 * theming pass finds it next to its siblings. */
.army-view-action-row.action-row-disabled,
.unit-view-action-row.action-row-disabled {
    opacity: 0.55;
    cursor: not-allowed;
    pointer-events: auto;
}
.army-view-action-row.action-row-disabled:hover {
    background: var(--bg-elev);
}

.army-view-capacity-row {
    display: flex;
    align-items: center;
    gap: var(--space-3);
}
.army-view-capacity-pct {
    font-family: var(--font-mono);
    font-variant-numeric: tabular-nums;
    color: var(--text);
}
.army-view-capacity-tier {
    color: var(--text-dim);
    font-size: var(--fs-label);
}
.army-view-forecast-list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 4px;
}
.army-view-forecast-list li {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--space-3);
    background: var(--bg-elev);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    padding: 4px 10px;
}
.army-view-forecast-label {
    color: var(--text-dim);
    font-size: var(--fs-label);
}
.army-view-forecast-value {
    color: var(--text);
    font-family: var(--font-mono);
    font-variant-numeric: tabular-nums;
}
.army-view-forecast-hint {
    color: var(--text-muted);
    font-size: var(--fs-label);
    font-style: italic;
    margin-top: 4px;
}
.army-view-inventory-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
    gap: 4px;
}
.army-view-inventory-cell {
    display: grid;
    grid-template-columns: auto 1fr auto;
    align-items: baseline;
    gap: var(--space-2);
    background: var(--bg-elev);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    padding: 4px 10px;
}
.army-view-inventory-icon {
    font-size: 1.1em;
}
.army-view-inventory-label {
    color: var(--text);
    font-size: var(--fs-label);
}
.army-view-inventory-qty {
    color: var(--text);
    font-family: var(--font-mono);
    font-variant-numeric: tabular-nums;
}

.army-view-empty {
    color: var(--text-muted);
    font-size: var(--fs-label);
    font-style: italic;
    padding: var(--space-3) 0;
}
.army-view-loading {
    color: var(--text-dim);
    font-size: var(--fs-label);
    padding: var(--space-3) 0;
}
.army-view-error {
    background: var(--bg-elev);
    border: 1px solid var(--color-danger);
    border-radius: var(--radius-sm);
    padding: var(--space-3);
    color: var(--text);
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3);
}
.army-view-retry {
    background: var(--bg);
    border: 1px solid var(--color-warning);
    color: var(--color-warning);
    padding: var(--space-1) var(--space-3);
    border-radius: var(--radius-sm);
    cursor: pointer;
    font-family: var(--font-body);
    min-height: var(--tap-sm);
}
.army-view-retry:hover,
.army-view-retry:focus-visible {
    background: rgba(255, 183, 0, 0.1);
}

/* Placeholder modal (TASK_09 — until per-action mutation surfaces are
 * wired in TASK_11+). Sits over the workspace shell + curtain.
 */
.army-view-modal {
    position: fixed;
    inset: 0;
    background: rgba(8, 10, 24, 0.78);
    display: flex;
    align-items: center;
    justify-content: center;
    z-index: 1000;
    padding: var(--space-3);
}
.army-view-modal-card {
    background: var(--bg-elev);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius);
    padding: var(--space-4);
    max-width: 480px;
    width: 100%;
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
    box-shadow: 0 12px 32px rgba(0, 0, 0, 0.5);
}
.army-view-modal-title {
    font-family: var(--font-display);
    font-size: var(--fs-h2);
    color: var(--color-warning);
    margin: 0;
}
.army-view-modal-body {
    color: var(--text);
    font-size: var(--fs-body);
    line-height: var(--lh-body);
    margin: 0;
}
.army-view-modal-close {
    align-self: flex-end;
    background: var(--bg);
    border: 1px solid var(--color-warning);
    color: var(--color-warning);
    padding: var(--space-2) var(--space-4);
    border-radius: var(--radius-sm);
    cursor: pointer;
    min-height: var(--tap-sm);
    font-family: var(--font-body);
}
.army-view-modal-close:hover,
.army-view-modal-close:focus-visible {
    background: rgba(255, 183, 0, 0.10);
}

@media (prefers-reduced-motion: reduce) {
    .army-view-tab,
    .army-view-tile-chip,
    .army-view-bar-fill {
        transition: none;
    }
}
.force-reduced-motion .army-view-tab,
.force-reduced-motion .army-view-tile-chip,
.force-reduced-motion .army-view-bar-fill {
    transition: none;
}

/* ─── PathPlanner overlay (TASK_12 / ui-modal-redesign TASK_24) ───
 * AC18 — desktop (≥720): floats top-right of the canvas as a
 * `.atlas-card[data-variant="planner"]` (the brass left rail comes
 * from the canonical variant rule). Mobile (<720): mounted inside
 * `.atlas-modal-sticky` via `setStickyContent('planner', el)` so the
 * fixed-positioning + box-shadow chrome falls away (the sticky host
 * already pins the bottom edge). The `:not(.atlas-modal-sticky *)`
 * selector keeps the desktop rule from firing when the overlay is
 * a descendant of the sticky-footer mount-point.
 */
.path-planner-overlay:not(.atlas-modal-sticky *) {
    position: fixed;
    right: 16px;
    bottom: 16px;
    width: min(360px, calc(100vw - 32px));
    max-height: calc(100vh - 32px);
    overflow-y: auto;
    box-shadow: 0 14px 40px rgba(0, 0, 0, 0.55);
    z-index: 9000;
}
.path-planner-overlay {
    background: rgba(20, 22, 38, 0.96);
    color: var(--text, #e6e6f0);
    font-family: var(--font-body, 'Public Sans', sans-serif);
    padding: 14px 14px 12px;
    display: flex;
    flex-direction: column;
    gap: 10px;
}
/* Sticky-footer mount on mobile — drop the floating chrome so the
 * shell-owned `.atlas-modal-sticky` host pins the bottom edge. */
.atlas-modal-sticky .path-planner-overlay {
    width: 100%;
    box-shadow: none;
    border-radius: 0;
}

.path-planner-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
}

.path-planner-title {
    margin: 0;
    font-family: var(--font-display, 'EB Garamond', serif);
    font-size: 1rem;
    letter-spacing: 0.02em;
    color: var(--color-warning, #c8a14a);
}

.path-planner-close {
    background: transparent;
    border: 1px solid var(--border, #2c2c44);
    color: var(--text-muted, #aaaac0);
    border-radius: var(--radius-md);
    width: 28px;
    height: 28px;
    cursor: pointer;
    font-size: 14px;
    line-height: 1;
}

.path-planner-close:hover,
.path-planner-close:focus-visible {
    background: rgba(200, 161, 74, 0.12);
    color: var(--text, #e6e6f0);
}

.path-planner-toggles {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
}

.path-planner-chip {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 6px 10px;
    border-radius: 999px;
    border: 1px solid var(--border, #2c2c44);
    background: rgba(255, 255, 255, 0.02);
    color: var(--text-muted, #aaaac0);
    cursor: pointer;
    font-size: 0.85rem;
    transition: background 120ms ease, color 120ms ease, border-color 120ms ease;
}

.path-planner-chip.is-on {
    background: rgba(200, 161, 74, 0.18);
    color: var(--color-warning, #c8a14a);
    border-color: rgba(200, 161, 74, 0.55);
}

.path-planner-chip:focus-visible {
    outline: 2px solid var(--color-warning, #c8a14a);
    outline-offset: 2px;
}

.path-planner-chip-mark {
    font-family: var(--font-mono, 'JetBrains Mono', monospace);
    font-size: 0.85rem;
    width: 14px;
    text-align: center;
}

.path-planner-summary {
    font-size: 0.9rem;
    color: var(--text, #e6e6f0);
    min-height: 1.2em;
    padding: 6px 8px;
    background: rgba(255, 255, 255, 0.03);
    border-radius: var(--radius-md);
    border-left: 2px solid var(--color-warning, #c8a14a);
}

/* TASK_24 — action-card row inside the planner overlay. The cards
 * themselves (`.atlas-card[data-variant="action"]`) inherit the
 * canonical brass left-rail, AP-cost badge, and locked / disabled
 * states from the global atlas-card variant block; this rule only
 * pins the row layout (two cards, primary right). */
.path-planner-actions {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: var(--space-2, 8px);
}
.path-planner-action-card {
    text-align: left;
    cursor: pointer;
}
.path-planner-action-card[disabled],
.path-planner-action-card[aria-disabled="true"] {
    opacity: 0.45;
    cursor: not-allowed;
}
.path-planner-action-card--primary {
    /* Confirm card highlight; mirrors the brass-pill ladder used by
     * `.atlas-btn--primary` so a player reading the card grid sees
     * the primary action without needing the AP badge. */
    background: rgba(200, 161, 74, 0.10);
}
.path-planner-action-card--primary:hover:not([disabled]),
.path-planner-action-card--primary:focus-visible:not([disabled]) {
    background: rgba(200, 161, 74, 0.20);
}

/* In-modal confirm step shown when Прямий шлях crosses hostile-adjacent. */
.path-planner-confirm-modal {
    position: fixed;
    inset: 0;
    background: rgba(8, 10, 24, 0.78);
    z-index: 9100;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 16px;
}

.path-planner-confirm-card {
    background: var(--bg-elev, #1c1c2e);
    border: 1px solid var(--border-strong, #3a3a55);
    border-radius: 12px;
    padding: 18px 18px 14px;
    width: min(420px, 100%);
    box-shadow: 0 16px 50px rgba(0, 0, 0, 0.65);
}

.path-planner-confirm-title {
    margin: 0 0 8px;
    font-family: var(--font-display, 'EB Garamond', serif);
    font-size: 1.05rem;
    color: var(--color-danger, #d57070);
}

.path-planner-confirm-body {
    margin: 0 0 12px;
    color: var(--text, #e6e6f0);
    font-size: 0.95rem;
    line-height: 1.4;
}

.path-planner-confirm-actions {
    display: flex;
    justify-content: flex-end;
    gap: 8px;
}

.path-planner-confirm-btn {
    border: 1px solid var(--border, #2c2c44);
    background: rgba(255, 255, 255, 0.04);
    color: var(--text, #e6e6f0);
    padding: 8px 14px;
    border-radius: 8px;
    cursor: pointer;
    font-size: 0.95rem;
    min-height: 36px;
}

.path-planner-confirm-btn--yes {
    border-color: rgba(213, 112, 112, 0.7);
    background: rgba(213, 112, 112, 0.18);
    color: var(--color-danger, #d57070);
    font-weight: 600;
}

.path-planner-confirm-btn:hover:not(:disabled),
.path-planner-confirm-btn:focus-visible:not(:disabled) {
    filter: brightness(1.15);
}

@media (max-width: 639px) {
    /* mobile-ux-rework TASK_09 / AC20 — PathPlanner half-sheet is one of
       the four fixed-bottom mobile surfaces explicitly enumerated by the
       AC20 contract. The legacy `bottom: 8px` literal is lifted onto
       `calc(8px + var(--safe-bottom))` so notched iPhones don't tuck the
       overlay under the home-indicator region. The `body.hw-pp-open`
       variant rule above (≈ L5550) continues to override the bottom edge
       to `calc(30dvh + 8px)` when the workspace half-sheet is up — that
       branch sits *above* the 30 vh sheet so safe-area is reserved by
       the workspace shell's own bottom-padding. */
    .path-planner-overlay {
        right: 8px;
        left: 8px;
        bottom: calc(8px + var(--safe-bottom));
        width: auto;
        max-height: 50vh;
    }
}

@media (prefers-reduced-motion: reduce) {
    .path-planner-chip,
    .path-planner-action-card,
    .path-planner-confirm-btn {
        transition: none;
    }
}

.force-reduced-motion .path-planner-chip,
.force-reduced-motion .path-planner-action-card,
.force-reduced-motion .path-planner-confirm-btn {
    transition: none;
}


/* ─── A11y reduced-motion audit (ui-fullscreen-tile-detail TASK_15) ───
 * Single canonical block that mirrors design.md § "Animation
 * choreography" table 1:1. Each `@media (prefers-reduced-motion:
 * reduce)` rule below pins one row of that table so an audit stays a
 * straight diff against the design doc.
 *
 * Six animations must snap when motion-sensitive players opt out:
 *
 *   1. Open TileView (slide + curtain fade)        — workspace shell
 *   2. Switch tab (underline travel + cross-fade)  — *-view-tab
 *   3. Push UnitView from TileView (slide chain)   — workspace shell
 *   4. Inventory transfer (row slide + thud)       — unit-view-inventory
 *   5. Path waypoint added (stroke-dash draw)      — canvas (JS-gated)
 *   6. Auto-follow toast (slide up, sticky)        — toast block
 *
 * Animations 1, 2, 4, and 6 are CSS-driven and already covered by the
 * per-section `@media (prefers-reduced-motion: reduce)` blocks earlier
 * in the file. Animations 3 and 5 are gated in JS:
 *   - 3 reuses the workspace shell slide so the same CSS gate covers
 *     it; the JS `reducedMotion()` helper in workspace_shell.js also
 *     short-circuits the animation classes before they apply.
 *   - 5 lives in `path_planner_render.js`; the canvas paint is
 *     instantaneous (no setInterval / requestAnimationFrame stroke
 *     interpolation) so there is no motion to gate — the arrow chain
 *     appears in one paint when the route returns from the solver.
 *
 * This block is the belt-and-braces sweep: any future workspace
 * transition that someone forgets to gate explicitly will still snap
 * because the wildcard rules below force `transition` and `animation`
 * to none across the entire workspace surface.
 */
@media (prefers-reduced-motion: reduce) {
    /* Workspace surface — every transition / animation snaps. */
    #workspace.workspace,
    #workspace.workspace *,
    #workspace-curtain.workspace-curtain,
    .path-planner-overlay,
    .path-planner-overlay * {
        animation-duration: 0.001ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.001ms !important;
        scroll-behavior: auto !important;
    }
    /* Tab underline travel — keep the colour swap, drop the slide. */
    .tile-view-tab,
    .unit-view-tab,
    .army-view-tab {
        box-shadow: inset 0 -2px 0 transparent;
    }
    .tile-view-tab.is-active,
    .unit-view-tab.is-active,
    .army-view-tab.is-active {
        box-shadow: inset 0 -2px 0 var(--color-warning);
    }
    /* Mobile bottom-sheet half-sheet (TASK_14) — snap to its 30 vh
     * resting position with no slide-in; the workspace shell still
     * collapses, just instantly. */
    body.hw-pp-open #workspace.workspace {
        transition: none !important;
    }
}

.force-reduced-motion #workspace.workspace,
.force-reduced-motion #workspace.workspace *,
.force-reduced-motion #workspace-curtain.workspace-curtain,
.force-reduced-motion .path-planner-overlay,
.force-reduced-motion .path-planner-overlay * {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
}


/* ─── Hostile-occupied hex hatching (ui-fullscreen-tile-detail TASK_15) ─
 * Color-blind safety overlay paired with the red border on hostile-
 * occupied hexes (PathPlanMode). The Canvas-2D path planner draws the
 * hatching directly via `path_planner_render.js::paintHostileBorders`;
 * this CSS rule mirrors the same texture for any DOM-rendered hostile
 * indicator (foreign-army intel cards in TileView, hostile-tile chips
 * elsewhere) so the redundant signal — red HUE + diagonal STRIPE — is
 * applied uniformly across both the canvas and DOM surfaces.
 *
 * Token-aligned: stroke colour matches the existing
 * rgba(220, 70, 70, 0.95) hostile-border red used by the path planner
 * + the foreign-row left border (.tile-view-row.is-foreign), 8x8 px
 * tile mirroring `static/svg/hostile_hatch.svg`.
 */
.is-hostile-hatched,
.tile-view-intel-card.is-foreign {
    background-image:
        repeating-linear-gradient(
            45deg,
            transparent 0,
            transparent 4px,
            rgba(220, 70, 70, 0.18) 4px,
            rgba(220, 70, 70, 0.18) 6px
        );
    background-size: 8px 8px;
}

/* ─── ArmiesView (ui-armies-buildings-redesign TASK_01) ────────────
 *
 * Fullscreen workspace surface — replaces the legacy `armies_panel.js`
 * drawer. TASK_01 ships the skeleton: header (back chip + title with
 * count + lineage), brass tab strip [1] Усі / [2] У русі / [3] Наявні,
 * scrollable list pane for rows. Tokens reuse the workspace shell's
 * existing palette (--bg-elev / --border-strong / --color-warning) so
 * the screen feels visually identical to TileView / ArmyView. Row
 * rendering is TASK_02's job; this block scaffolds the container only.
 */
.armies-view {
    display: flex;
    flex-direction: column;
    height: 100%;
    gap: var(--space-3);
    padding: var(--space-3);
}
.armies-view-header {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
}
.armies-view-back {
    align-self: flex-start;
    background: var(--bg-elev);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-sm);
    color: var(--text);
    font-family: var(--font-body);
    padding: var(--space-1) var(--space-3);
    cursor: pointer;
    min-height: var(--tap-sm);
}
.armies-view-back:hover,
.armies-view-back:focus-visible {
    background: rgba(255, 183, 0, 0.08);
}
.armies-view-title {
    font-family: var(--font-display);
    font-size: var(--fs-h1);
    font-weight: 600;
    color: var(--color-warning);
    margin: 0;
    letter-spacing: 0.03em;
}
.armies-view-count {
    color: var(--text-dim);
    font-family: var(--font-mono);
    font-size: var(--fs-body);
    margin-left: var(--space-1);
}
.armies-view-lineage {
    color: var(--text-dim);
    font-size: var(--fs-label);
    margin: 0;
}
.armies-view-tabs {
    display: flex;
    gap: 0;
    background: var(--bg-elev);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-sm);
    overflow: hidden;
}
.armies-view-tab {
    flex: 0 0 auto;
    background: transparent;
    border: none;
    border-right: 1px solid var(--border-strong);
    color: var(--text-dim);
    padding: var(--space-2) var(--space-4);
    cursor: pointer;
    min-height: var(--tap);
    font-family: var(--font-body);
    font-size: var(--fs-body);
    box-shadow: inset 0 -2px 0 transparent;
    transition:
        color 140ms ease,
        background 140ms ease,
        box-shadow 140ms ease;
}
.armies-view-tab:last-child {
    border-right: none;
}
.armies-view-tab:hover {
    background: rgba(255, 255, 255, 0.04);
    color: var(--text);
}
.armies-view-tab:focus-visible {
    outline: 2px solid var(--color-warning);
    outline-offset: -2px;
}
.armies-view-tab.is-active {
    color: var(--text);
    background: rgba(255, 183, 0, 0.06);
    box-shadow: inset 0 -2px 0 var(--color-warning);
}
.armies-view-list {
    flex: 1 1 auto;
    overflow: auto;
    min-height: 0;
}
.armies-view-loading {
    padding: var(--space-4) 0;
    text-align: center;
    color: var(--text-dim);
    font-size: var(--fs-label);
    opacity: 0.85;
}

/* ─── ui-armies-buildings-redesign TASK_02: rows ────────────────────
 *
 * Row layout: head (mode glyph + name + unit-count) + meta line (tile,
 * eta, morale, load chip) + actions slot (TASK_03 fills it). Tap target
 * ≥44 px on every breakpoint. Mobile (<640 px) collapses to a single
 * column; the meta line wraps so a stealth-mode patrol still reads.
 * Reuses --load-* tokens for the load chip so the colour grammar is
 * shared with the capacity-meter / pickup-modal / food-runway chips.
 */
.armies-view-row {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    padding: var(--space-2) var(--space-3);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-sm);
    background: var(--bg-elev);
    margin-bottom: var(--space-2);
    cursor: pointer;
    min-height: 44px;
    transition:
        background 140ms ease,
        border-color 140ms ease,
        box-shadow 140ms ease;
}
.armies-view-row:hover {
    background: rgba(255, 183, 0, 0.05);
    border-color: var(--color-warning);
}
.armies-view-row:focus-visible {
    outline: 2px solid var(--color-warning);
    outline-offset: -2px;
}
.armies-view-row-head {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    flex-wrap: wrap;
}
.armies-view-row-glyph {
    font-size: var(--fs-h2);
    line-height: 1;
    flex: 0 0 auto;
}
.armies-view-row-name {
    font-family: var(--font-display);
    font-size: var(--fs-body);
    color: var(--text);
    font-weight: 600;
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.armies-view-row-count {
    color: var(--text-dim);
    font-family: var(--font-mono);
    font-size: var(--fs-label);
    flex: 0 0 auto;
}
.armies-view-row-meta {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
    color: var(--text-dim);
    font-size: var(--fs-label);
}
.armies-view-row-tile,
.armies-view-row-eta,
.armies-view-row-morale,
.armies-view-row-load {
    flex: 0 0 auto;
    white-space: nowrap;
}
.armies-view-row-load[data-tier="ok"]       { color: var(--load-ok); }
.armies-view-row-load[data-tier="normal"]   { color: var(--load-normal); }
.armies-view-row-load[data-tier="warn"]     { color: var(--load-warn); }
.armies-view-row-load[data-tier="overload"] { color: var(--load-overload); }
.armies-view-row-load[data-tier="block"]    { color: var(--load-block); }
.armies-view-row-actions {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-1);
}
.armies-view-row-actions:empty {
    display: none;
}
.armies-view-empty {
    padding: var(--space-4) var(--space-3);
    text-align: center;
    color: var(--text-dim);
    font-size: var(--fs-label);
}
.armies-view-error {
    padding: var(--space-3);
    color: var(--load-overload);
    font-size: var(--fs-label);
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2);
}
.armies-view-retry {
    background: var(--bg-elev);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-sm);
    color: var(--text);
    font-family: var(--font-body);
    padding: var(--space-1) var(--space-3);
    cursor: pointer;
    min-height: var(--tap-sm);
}
.armies-view-retry:hover,
.armies-view-retry:focus-visible {
    background: rgba(255, 183, 0, 0.08);
    border-color: var(--color-warning);
}
@media (max-width: 639px) {
    .armies-view-row {
        padding: var(--space-2);
    }
    .armies-view-row-head {
        flex-wrap: wrap;
    }
    .armies-view-row-name {
        flex-basis: 100%;
        white-space: normal;
    }
    .armies-view-row-meta {
        gap: var(--space-1) var(--space-2);
    }
}

/* ─── ui-armies-buildings-redesign TASK_03: row action buttons +
 *     mode popover + merge picker ───────────────────────────────────
 *
 * Action buttons are 44×44 px tap targets on every breakpoint per
 * spec. Mode popover and merge picker live at body level (`position:
 * fixed`) so the parent's `overflow: hidden` slots can't clip them.
 * Both surfaces sit above the workspace shell (z-index 30 vs 20).
 */
.armies-view-row-action {
    background: var(--bg-elev);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-sm);
    color: var(--text);
    cursor: pointer;
    font-family: var(--font-body);
    font-size: var(--fs-h3);
    line-height: 1;
    min-width: 44px;
    min-height: 44px;
    padding: 0 var(--space-2);
    transition:
        background 140ms ease,
        border-color 140ms ease;
    -webkit-touch-callout: none;
    -webkit-user-select: none;
    user-select: none;
}
.armies-view-row-action:hover,
.armies-view-row-action:focus-visible {
    background: rgba(255, 183, 0, 0.08);
    border-color: var(--color-warning);
}
.armies-view-row-action[disabled] {
    opacity: 0.45;
    cursor: not-allowed;
}
.armies-view-row-action[hidden] {
    display: none !important;
}
.armies-view-mode-popover,
.armies-view-merge-picker {
    background: var(--bg-elev);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-sm);
    box-shadow: 0 4px 18px rgba(0, 0, 0, 0.35);
    display: flex;
    flex-direction: column;
    gap: 2px;
    min-width: 180px;
    padding: var(--space-1);
    z-index: 30;
}
.armies-view-mode-popover-item,
.armies-view-merge-picker-item {
    background: transparent;
    border: 1px solid transparent;
    border-radius: var(--radius-sm);
    color: var(--text);
    cursor: pointer;
    font-family: var(--font-body);
    font-size: var(--fs-body);
    padding: var(--space-2) var(--space-3);
    text-align: left;
    min-height: 44px;
}
.armies-view-mode-popover-item:hover,
.armies-view-mode-popover-item:focus-visible,
.armies-view-merge-picker-item:hover,
.armies-view-merge-picker-item:focus-visible {
    background: rgba(255, 183, 0, 0.08);
    border-color: var(--color-warning);
}
.armies-view-merge-picker-head {
    color: var(--text-dim);
    font-size: var(--fs-label);
    padding: var(--space-1) var(--space-2);
}
.armies-view-merge-picker-count {
    color: var(--text-dim);
    font-family: var(--font-mono);
    font-size: var(--fs-label);
    margin-left: var(--space-1);
}

/* ─── ui-modal-redesign / TASK_22: kebab popover (AC16) ─────────────
 *
 * The per-row 4-button strip retires; the secondaries (Mode, Cancel-
 * move, Merge) live in a body-level kebab popover anchored to the
 * footer's `.atlas-row-kebab` trigger. Mirrors the
 * `armies-view-mode-popover` chrome so both surfaces feel identical.
 */
.atlas-row-kebab {
    background: transparent;
    border: 1px solid var(--border-soft, var(--border-strong));
    border-radius: var(--radius-sm);
    color: var(--ink-mute, var(--text-dim));
    cursor: pointer;
    font-family: var(--font-body);
    font-size: var(--fs-h3);
    line-height: 1;
    min-width: 36px;
    min-height: 36px;
    padding: 0 var(--space-2);
    transition:
        background 140ms ease,
        border-color 140ms ease,
        color 140ms ease;
}
.atlas-row-kebab:hover,
.atlas-row-kebab:focus-visible {
    background: rgba(255, 183, 0, 0.08);
    border-color: var(--accent, var(--color-warning));
    color: var(--ink, var(--text));
}
.armies-view-kebab-popover {
    background: var(--bg-elev);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-sm);
    box-shadow: 0 4px 18px rgba(0, 0, 0, 0.35);
    display: flex;
    flex-direction: column;
    gap: 2px;
    min-width: 220px;
    padding: var(--space-1);
    z-index: 30;
}
.armies-view-kebab-popover-item {
    background: transparent;
    border: 1px solid transparent;
    border-radius: var(--radius-sm);
    color: var(--text);
    cursor: pointer;
    font-family: var(--font-body);
    font-size: var(--fs-body);
    padding: var(--space-2) var(--space-3);
    text-align: left;
    min-height: 44px;
    display: flex;
    align-items: center;
    gap: var(--space-2);
}
.armies-view-kebab-popover-item:hover,
.armies-view-kebab-popover-item:focus-visible {
    background: rgba(255, 183, 0, 0.08);
    border-color: var(--color-warning);
}
.armies-view-kebab-popover-glyph,
.armies-view-mode-popover-glyph {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    line-height: 1;
}
.armies-view-kebab-popover-glyph svg,
.armies-view-mode-popover-glyph svg {
    width: 18px;
    height: 18px;
}

@media (max-width: 639px) {
    .armies-view-row-actions {
        flex-basis: 100%;
        gap: var(--space-2);
    }
    .armies-view-mode-popover,
    .armies-view-merge-picker,
    .armies-view-kebab-popover {
        max-width: calc(100vw - 2 * var(--space-3));
    }
    /* AC16 — mobile row collapses to a single-column card stack. The
     * `.atlas-card` parent rule already paints the card chrome; we
     * just shrink the side padding so the row uses the full bleed. */
    .armies-view-row.atlas-card {
        padding: var(--space-3);
    }
}

/* ─── unit-system-rework TASK_12: stat-grid + skill-grid ────────────
 *
 * AC45 — 4 core + 8 derived two-column block under the unit-panel
 * hero card. Each row carries `.stat-row` so the AC45 E2E counts at
 * least 12 rows. Layout collapses to one column under 380 px (mirrors
 * the existing `.u-stats` collapse rule).
 *
 * AC46 — per-skill bars with progress + driving-stat hint. Bars use
 * a unicode `█░` glyph chain so the visual signal works without an
 * SVG / canvas dependency (one file rule, no preprocessor).
 */
.stat-grid {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: var(--space-2);
    margin: var(--space-2) 0;
}
.stat-grid-col {
    display: flex;
    flex-direction: column;
    gap: 4px;
    background: var(--bg-elev);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-sm);
    padding: var(--space-2) var(--space-3);
}
.stat-grid-col-head {
    font-family: var(--font-display);
    font-size: var(--fs-label);
    color: var(--text-dim);
    letter-spacing: 0.04em;
    margin-bottom: 2px;
}
.stat-row {
    display: flex;
    align-items: baseline;
    gap: var(--space-2);
    font-family: var(--font-mono);
    font-size: var(--fs-body);
    color: var(--text);
    font-variant-numeric: tabular-nums;
}
.stat-row-label {
    color: var(--text-dim);
    min-width: 2.5em;
}
.stat-row-name {
    flex: 1;
    color: var(--text-dim);
}
.stat-row-value {
    color: var(--text);
    text-align: right;
    min-width: 3em;
}
.stat-row-hint {
    color: var(--text-dim);
    font-size: var(--fs-label);
}
.stat-spend-cta {
    grid-column: 1 / -1;
    display: flex;
    justify-content: flex-end;
    margin-top: var(--space-1);
}
.stat-spend-btn {
    /* uses existing btn-sm + btn-primary tokens — nothing custom */
}
/* mobile-ux-rework TASK_01 — single-column lists at the Mobile-narrow
   tier (≤479) per design.md breakpoint chain. Was 380. */
@media (max-width: 479px) {
    .stat-grid {
        grid-template-columns: 1fr;
    }
}

.skill-grid {
    display: flex;
    flex-direction: column;
    gap: 4px;
    margin: var(--space-2) 0;
    background: var(--bg-elev);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    padding: var(--space-2) var(--space-3);
}
.skill-grid-head {
    font-family: var(--font-display);
    font-size: var(--fs-label);
    color: var(--text-dim);
    letter-spacing: 0.04em;
}
.skill-grid-slots {
    color: var(--text-dim);
    font-family: var(--font-mono);
}
.skill-row {
    display: flex;
    align-items: baseline;
    gap: var(--space-2);
    font-family: var(--font-mono);
    font-size: var(--fs-body);
    color: var(--text);
    font-variant-numeric: tabular-nums;
    padding: 2px 0;
}
.skill-row-name {
    flex: 1 1 auto;
    color: var(--text);
}
.skill-row-level {
    color: var(--text-dim);
    min-width: 4em;
}
.skill-row-bar {
    color: var(--text);
    letter-spacing: 1px;
}
.skill-row-progress {
    color: var(--text-dim);
    min-width: 6em;
    text-align: right;
}
.skill-row-driving {
    color: var(--text-dim);
    font-size: var(--fs-label);
}

/* ─── unit-system-rework TASK_13: stat-spend / merge-split / train ──
 *
 * AC45 — sliders for STR/DEX/END/INT.
 * AC47 — merge confirm modal + split modal.
 * AC46 — leader-side training modal.
 *
 * All three reuse the native `<dialog>` shape from cost_dialog.js so
 * the backdrop / Esc / focus-trap behaviour is delegated to
 * `modal/base.js::openDialog`.
 */
.spend-stat-modal,
.merge-split-modal,
.train-panel-modal {
    border: 1px solid var(--border-strong);
    border-radius: var(--radius);
    background: var(--bg-elev);
    color: var(--text);
    padding: 0;
    max-width: 480px;
    width: calc(100% - 32px);
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
}
.spend-stat-modal::backdrop,
.merge-split-modal::backdrop,
.train-panel-modal::backdrop {
    background: rgba(0, 0, 0, 0.55);
}
.spend-stat-inner,
.ms-inner,
.train-inner {
    padding: var(--space-3) var(--space-4);
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
}
.spend-stat-header,
.ms-header,
.train-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-2);
}
.spend-stat-header h3,
.ms-header h3,
.train-header h3 {
    font-family: var(--font-display);
    font-size: var(--fs-h2);
    margin: 0;
}
.spend-stat-close,
.ms-close,
.train-close {
    background: transparent;
    border: 0;
    color: var(--text-dim);
    cursor: pointer;
    font-size: 18px;
}
.spend-stat-pool {
    display: flex;
    align-items: baseline;
    gap: var(--space-3);
    background: var(--bg);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    padding: var(--space-2) var(--space-3);
}
.spend-stat-pool.shortfall {
    border-color: var(--color-danger);
    color: var(--color-danger);
}
.spend-stat-pool-label {
    color: var(--text-dim);
    font-size: var(--fs-label);
}
.spend-stat-pool-value {
    font-family: var(--font-mono);
    font-size: var(--fs-body);
}
.spend-stat-pool-remaining {
    color: var(--text-dim);
    font-size: var(--fs-label);
    margin-left: auto;
}
.spend-stat-rows {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
}
.spend-stat-row {
    display: grid;
    grid-template-columns: 7em 1fr;
    grid-template-rows: auto auto;
    column-gap: var(--space-2);
    row-gap: 4px;
    align-items: center;
}
.spend-stat-label {
    grid-column: 1;
    color: var(--text-dim);
    font-size: var(--fs-label);
}
.spend-stat-line {
    grid-column: 2;
    display: flex;
    align-items: baseline;
    gap: 6px;
    font-family: var(--font-mono);
    font-variant-numeric: tabular-nums;
}
.spend-stat-arrow {
    color: var(--text-dim);
}
.spend-stat-projected {
    color: var(--text);
}
.spend-stat-delta {
    color: var(--color-accent, var(--text));
    margin-left: auto;
}
.spend-stat-slider {
    grid-column: 1 / -1;
    width: 100%;
}
.spend-stat-actions,
.ms-actions,
.train-actions {
    display: flex;
    justify-content: flex-end;
    gap: var(--space-2);
}

/* AC47 — merge / split modal grid */
.ms-summary {
    color: var(--text-dim);
    font-size: var(--fs-body);
    margin: 0;
}
.ms-grid {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: var(--space-2);
}
.ms-grid-col {
    background: var(--bg);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    padding: var(--space-2) var(--space-3);
    display: flex;
    flex-direction: column;
    gap: 4px;
}
.ms-grid-head {
    font-family: var(--font-display);
    font-size: var(--fs-label);
    color: var(--text-dim);
    margin-bottom: 4px;
}
.ms-stat-row {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: var(--space-2);
    font-family: var(--font-mono);
    font-variant-numeric: tabular-nums;
}
.ms-stat-label {
    color: var(--text-dim);
}
.ms-stat-value {
    color: var(--text);
}
.ms-skill-list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 2px;
}
.ms-skill-row {
    display: flex;
    justify-content: space-between;
    gap: var(--space-2);
    font-size: var(--fs-label);
}
.ms-skill-name {
    color: var(--text);
}
.ms-skill-meta {
    color: var(--text-dim);
}
.ms-split-rows {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
}
.ms-split-row {
    display: grid;
    grid-template-columns: 8em 1fr auto;
    align-items: center;
    gap: var(--space-2);
}
.ms-split-label {
    color: var(--text-dim);
    font-size: var(--fs-label);
}
.ms-split-count {
    background: var(--bg);
    color: var(--text);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-sm);
    padding: 4px 6px;
}
.ms-split-actions-inline {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-2);
}
.ms-split-sum {
    color: var(--text);
    font-family: var(--font-mono);
}
.ms-split-sum.shortfall {
    color: var(--color-danger);
}

/* AC46 — train modal */
.train-form {
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
}
.train-field {
    display: flex;
    flex-direction: column;
    gap: 4px;
}
.train-field-label {
    color: var(--text-dim);
    font-size: var(--fs-label);
}
.train-skill,
.train-ticks {
    background: var(--bg);
    color: var(--text);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-sm);
    padding: 4px 6px;
    font-family: var(--font-mono);
}
.train-student-list {
    display: flex;
    flex-direction: column;
    gap: 2px;
    max-height: 220px;
    overflow-y: auto;
    background: var(--bg);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    padding: var(--space-2);
}
.train-student-row {
    display: grid;
    grid-template-columns: auto 1fr auto;
    gap: var(--space-2);
    align-items: center;
    padding: 2px 4px;
    cursor: pointer;
}
.train-student-row:hover {
    background: var(--bg-elev);
}
.train-student-name {
    color: var(--text);
}
.train-student-meta {
    color: var(--text-dim);
    font-size: var(--fs-label);
}

@media (max-width: 480px) {
    .ms-grid {
        grid-template-columns: 1fr;
    }
    .ms-split-row {
        grid-template-columns: 6em 1fr auto;
    }
}

/* === Cross-panel links: armies-panel + shared flash === */
/*
 * ui-cross-panel-links (TASK_01 — AC1 + AC2 + AC3 + AC6 + AC11):
 * affordance grammar for the armies-panel row-name link
 * (`.a-row-name--link`) + coord chip (`.a-row-coord-chip`) + the
 * shared row flash classes consumed by `flashArmyRow` (TASK_02 +
 * TASK_03 will share `.row-flash` / `.row-flash--reduced`). Hover-
 * underline + focus-visible outline on both surfaces; coord chip
 * pins `--fs-label` font-size and minimal padding so the in-place
 * upgrade of the bare `(q, r)` parens does not bump `.a-row-meta`
 * height (AC11 single-line budget at 360 px). Mobile clause lifts
 * the chip min-height to `--tap-sm` (40 px) for thumb-friendly
 * activation per `ui-modal-clarity` AC42 / AC69.
 */
.a-row-name--link,
.a-row-coord-chip {
    cursor: pointer;
    background: transparent;
    border: 0;
    padding: 0 var(--space-1);
    font: inherit;
    color: inherit;
    min-height: auto;
}
.a-row-name--link {
    font-weight: bold;
}
.a-row-coord-chip {
    font-size: var(--fs-label);
    color: inherit;
}
.a-row-name--link:hover,
.a-row-coord-chip:hover,
.a-row-name--link:focus-visible,
.a-row-coord-chip:focus-visible {
    text-decoration: underline;
    outline: 2px solid var(--color-primary);
    outline-offset: 2px;
}
.a-row[tabindex="0"]:focus-visible {
    outline: 2px solid var(--color-primary);
    outline-offset: 2px;
}

@media (max-width: 639px) {
    /* mobile-ux-rework TASK_03 — coord chip lifts to `--tap-mobile`
       (48 px) per AC01 / AC18 (paired hover+focus+active). */
    .a-row-coord-chip { min-height: var(--tap-mobile); }
}

/*
 * AC6 row-flash semantics. `.row-flash` is the 1.5 s tint pulse
 * applied by `flashArmyRow` when reduced-motion is OFF; the
 * keyframe pulses `--color-warning` (ochre highlight token used
 * across the existing flag/hint palette) twice over the window.
 * `.row-flash--reduced` is the static-paint sibling for the
 * reduced-motion branch — `animation-duration: 0s` so the
 * computed style reports zero duration (AC6 sentinel anchor) and
 * the timeout strip is the only state change a screen reader's
 * focus indicator experiences. The `prefers-reduced-motion`
 * defensive override keeps `.row-flash` flat even if the JS gate
 * misses a UA where the matchMedia query lies.
 */
@keyframes row-flash {
    0%   { background-color: rgba(255, 183, 0, 0.0); }
    20%  { background-color: rgba(255, 183, 0, 0.35); }
    50%  { background-color: rgba(255, 183, 0, 0.20); }
    100% { background-color: rgba(255, 183, 0, 0.0); }
}
.row-flash {
    animation: row-flash 1500ms ease-out 1;
}
.row-flash--reduced {
    background-color: rgba(255, 183, 0, 0.20);
    animation-duration: 0s;
}
@media (prefers-reduced-motion: reduce) {
    .row-flash { animation: none; }
}

/* === Cross-panel links: tile-info chip === */
/*
 * ui-cross-panel-links (TASK_02 — AC4 + AC9): per-unit `армія #N` chip
 * emitted by `tile_panel_sections.js::renderUnits` after the unit-name
 * anchor on owned-unit rows whose `army_id` is in the player's cached
 * `getState().myArmies` set. Same hover-underline + focus-visible
 * affordance grammar as `.a-row-coord-chip` (zero chrome, `--fs-label`,
 * minimal `--space-1` padding, `min-height: auto` on desktop) so the
 * chip slots in after the unit name without bumping the `.t-unit-row`
 * height; the mobile clause lifts `min-height` to `--tap-sm` (40 px) at
 * `(max-width: 639px)` per `ui-modal-clarity` AC42 / AC69 thumb-target
 * contract. Tile-info row's existing 2-line wrap at 360 px absorbs the
 * extra ~80 px width without a third line per `design.md::"Mobile
 * considerations"::"Tile-info chip width"`.
 */
.t-unit-army-chip {
    cursor: pointer;
    background: transparent;
    border: 0;
    padding: 0 var(--space-1);
    font: inherit;
    color: inherit;
    font-size: var(--fs-label);
    min-height: auto;
}
.t-unit-army-chip:hover,
.t-unit-army-chip:focus-visible {
    text-decoration: underline;
    outline: 2px solid var(--color-primary);
    outline-offset: 2px;
}
@media (max-width: 639px) {
    /* mobile-ux-rework TASK_03 — chip lifts to `--tap-mobile` per
       AC01 (cross-panel link must clear 48 px on phones). */
    .t-unit-army-chip { min-height: var(--tap-mobile); }
}

/* === Cross-panel links: unit-panel === */
/*
 * ui-cross-panel-links (TASK_03 — AC5): unit-panel "Позиція" cell
 * `армія #N` link emitted by `units_panel.js::renderStatsMatrix` for
 * owned units (`u.is_mine === true`); foreign units degrade to plain
 * text. Reads more like an inline link than a chip per `design.md::
 * "Affordance grammar"` table — omits the chip's faint border but
 * keeps the shared hover-underline + focus-visible outline grammar.
 * The mobile clause lifts `min-height` to `--tap-sm` (40 px) for
 * thumb-friendly activation per `ui-modal-clarity` AC42 / AC69.
 */
.u-army-link {
    cursor: pointer;
    background: transparent;
    border: 0;
    padding: 0 var(--space-1);
    font: inherit;
    color: inherit;
    min-height: auto;
}
.u-army-link:hover,
.u-army-link:focus-visible {
    text-decoration: underline;
    outline: 2px solid var(--color-primary);
    outline-offset: 2px;
}
@media (max-width: 639px) {
    /* mobile-ux-rework TASK_03 — cross-panel link inherits the
       mobile floor. AC01. */
    .u-army-link { min-height: var(--tap-mobile); }
}

/* === Disabled action hints: inline span === */
/* ui-disabled-action-hints / TASK_02 (AC-7 + AC-10 + AC-17): inline
   `<span class="disable-hint">` rendered next to (below) every disabled
   `<button>` in the legacy panel set (tile_actions / tile_panel_sections
   / armies_panel / units_panel — TASK_02; quests / trade / transmute
   / guild / institutions — TASK_03). Carries the registry factory's
   short `reason` field so the player learns the gate inline without
   hovering. NO `transition:` / NO `animation:` — AC-17 regression
   guard against a future motion regression on the inline-span reveal.
   Mobile (≤639 px) keeps the span visible (AC-10) since the hover
   popover degrades to a 3 s sticky tap there; the span is the
   canonical surface on phones. */
.disable-hint {
    display: block;
    font-size: 12px;
    line-height: 1.3;
    margin-top: 2px;
    color: var(--fg-muted);
}

/* === Disabled action hints: suggested-action chip === */
/* ui-disabled-action-hints / TASK_06 (AC-12 + AC-14): "Показати на
   мапі" chip rendered inside the hover popover (toast.js::showPopover)
   when the disabled-button render path stamped a
   `data-disable-suggest='{"kind":"jump-to","q":N,"r":M}'` attribute.
   Click → `window._jumpTo(q, r)` + popover closes. Chip only ships
   for the v1 chip-eligible factory set — `missing_building`,
   `missing_skill`, `requires_leader`, `not_adjacent`,
   `requires_owner` per spec.md::AC-14. NO `transition:` / NO
   `animation:` — AC-17 regression guard. */
.disable-suggest-chip {
    display: inline-block;
    margin-top: 6px;
    padding: 2px 8px;
    border-radius: 999px;
    background: var(--brass-soft, var(--bg-elev));
    border: 1px solid var(--border-strong);
    color: var(--text);
    font-size: 12px;
    cursor: pointer;
}
.disable-suggest-chip:focus-visible {
    outline: 2px solid var(--brass, var(--color-primary));
    outline-offset: 2px;
}

/* === Reach legend === */
/*
 * ui-reachable-preview / TASK_02 — terrain-cost legend chip cluster +
 * 4-depth toggle button row. Docks at the bottom-left of the canvas
 * while `reach_preview.js` has an active overlay; hidden otherwise via
 * the `.hidden` class flip in `reach_legend.js`.
 *
 * AC11: base rule has a `transition: opacity 200ms ease, transform
 *       200ms ease;` declaration; the `prefers-reduced-motion: reduce`
 *       media-query branch below drops `transition` to `none` so the
 *       legend snaps on/off without motion.
 * AC13: `prefers-contrast: more` swaps `border-left-width` to 5px AND
 *       swaps `background:` to `var(--color-bg-strong)` so high-
 *       contrast viewports get a heavier visual frame.
 * AC18 + AC19: `.depth-btn.active` background reuses `--color-success`
 *              so the active-tick visual pairs with the green tint
 *              palette of the multi-tick reach overlay.
 * mobile-touch-baseline: 44px min-height on `pointer: coarse` for
 *                        both `.depth-btn` and `.chip` so the touch-
 *                        target invariant holds on mobile.
 */
.reach-legend {
    position: fixed;
    bottom: var(--bottom-nav-h, 0);
    left: 16px;
    background: var(--color-bg-elevated, var(--bg-elev));
    border-left: 3px solid var(--color-success);
    padding: 6px 8px;
    border-radius: var(--radius-md);
    /* mobile-ux-rework TASK_08 / AC24 — `--font-mono` token instead
     * of the bare 'JetBrains Mono', monospace literal. */
    font-family: var(--font-mono);
    font-size: 12px;
    z-index: 30;
    transition: opacity 200ms ease, transform 200ms ease;
}
.reach-legend.hidden { display: none; }
.reach-legend .legend-row { display: flex; flex-wrap: wrap; gap: 4px; align-items: center; }
.reach-legend .chip {
    display: inline-block;
    margin: 0 4px;
    padding: 2px 6px;
    border-radius: 4px;
}
.reach-legend .chip.impassable { color: var(--color-danger); }
.reach-legend .depth-btn {
    min-width: 24px;
    min-height: 24px;
    padding: 2px 8px;
    margin: 0 2px;
    border: 1px solid var(--border-strong);
    border-radius: 4px;
    background: transparent;
    color: var(--text);
    cursor: pointer;
    font-family: inherit;
    font-size: inherit;
}
.reach-legend .depth-btn.active {
    background: var(--color-success);
    color: white;
}
.reach-legend .depth-label { margin-left: 6px; color: var(--fg-muted); }
.reach-legend .legend-announce {
    /* Visually-hidden; kept in DOM for the aria-live=polite announce. */
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    clip-path: inset(50%);
    white-space: nowrap;
}
@media (pointer: coarse) {
    .reach-legend .depth-btn,
    .reach-legend .chip { min-height: 44px; }
}
@media (prefers-reduced-motion: reduce) {
    .reach-legend { transition: none; }
}
@media (prefers-contrast: more) {
    .reach-legend {
        border-left-width: 5px;
        background: var(--color-bg-strong);
    }
}

/* === Toolbar chips: season + lifecycle === */
/* ui-tariff-season-chips / TASK_04 (AC5 markup + AC7 tokens).
   `#chip-season` reuses the `.info-chip` base shape (font-size,
   border-radius, ellipsis) added by the `class="info-chip"` literal in
   `index.html`; this block adds the per-season background tint via the
   `[data-season="..."]` attribute selector. Tokens come from the four
   `--season-*` entries on `:root`. Text colour switches to a dark ink
   so the pastel backgrounds stay readable in dark + light contexts.

   `#chip-lifecycle` + `#chip-toolbar-clock` rule selectors land here so
   TASK_05 only adds breakpoint clauses + popover + climax-pulse
   keyframes against an existing rule shape. */
#chip-season {
    display: inline-flex;
    align-items: center;
    padding: 3px 8px;
    color: var(--surface);
    border-radius: 4px;
    font-weight: 500;
    white-space: nowrap;
}
#chip-season[data-season="spring"]  { background: var(--season-spring); }
#chip-season[data-season="summer"]  { background: var(--season-summer); }
#chip-season[data-season="autumn"]  { background: var(--season-autumn); }
#chip-season[data-season="winter"]  { background: var(--season-winter); }

/* `#chip-lifecycle` base rule — the visible body (icon + label + climax
   pulse) lands in TASK_05. Stays out of layout flow when `hidden` so
   the toolbar wraps cleanly during `growth` (AC8). */
#chip-lifecycle {
    display: inline-flex;
    align-items: center;
    padding: 3px 8px;
}

/* `#chip-toolbar-clock` is the <640 px collapse target — desktop hides
   it by default; the mobile media-query below flips `display` to
   `inline-flex` to surface the 🌐 button + popover trigger. Tap-target
   honours `--tap-sm` (ui-mobile-touch-baseline AC13). */
#chip-toolbar-clock {
    display: none;
    min-height: var(--tap-sm);
    background: rgba(255, 255, 255, 0.04);
    color: var(--text-dim);
    border: 0;
    cursor: pointer;
}

/* ui-tariff-season-chips / TASK_05 (AC8 + AC14).
   `#chip-lifecycle` per-stage rules — `[data-stage="climax"]` paints a
   warm warning tint so the chip stands out in the toolbar; `[data-
   stage="ended"]` dims the text to signal the world is closed. The
   chip text already carries the icon + UA stage label via
   `_STAGE_LBL` from `labels.js`; tooltip via `LIFECYCLE_TOOLTIP`. */
#chip-lifecycle[data-stage="climax"] {
    background: var(--color-warning, rgba(255, 140, 0, 0.18));
    color: var(--text);
}
#chip-lifecycle[data-stage="ended"] {
    color: var(--text-dim);
}

/* ui-tariff-season-chips / TASK_05 (AC10 mobile collapse).
   At <640 px the season + lifecycle chips collapse and the
   `#chip-toolbar-clock` 🌐 button surfaces. Tap → popover (JS-driven). */
@media (max-width: 639px) {
    #chip-season,
    #chip-lifecycle {
        display: none;
    }
    #chip-toolbar-clock {
        display: inline-flex;
    }
}

/* ui-tariff-season-chips / TASK_05 (AC10 + AC11 popover).
   `#toolbar-clock-popover` is a non-modal flyout absolutely positioned
   under `#chip-toolbar-clock` via JS (`getBoundingClientRect()`).
   `position: absolute` is the JS contract — top/left set inline.
   z-index sits above the toolbar bar but below the modal stack. The
   popover is intentionally NOT in `panels.js::PANEL_IDS`. */
#toolbar-clock-popover {
    position: absolute;
    z-index: 60;
    min-width: 200px;
    padding: var(--space-2, 6px) var(--space-3, 10px);
    background: var(--bg-elev, rgba(15, 25, 50, 0.96));
    color: var(--text);
    border: 1px solid var(--border, #333);
    border-radius: var(--radius-sm, 4px);
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.45);
    font-size: var(--fs-body, 13px);
    line-height: 1.4;
}
.toolbar-clock-popover-row {
    padding: 4px 0;
}
.toolbar-clock-popover-row + .toolbar-clock-popover-row {
    border-top: 1px solid var(--border, #333);
    margin-top: 4px;
}
.toolbar-clock-popover-head {
    font-weight: 500;
    color: var(--text);
}
.toolbar-clock-popover-desc {
    color: var(--text-dim);
    font-size: var(--fs-label, 11px);
}

/* ui-tariff-season-chips / TASK_05 (AC9 climax pulse).
   One-off pulse animation runs on first transition to `climax` for a
   given world (gated by `localStorage.hw.lifecycleClimaxSeen.<wid>`
   in `toolbar_chips.js`). 1.5s ease-out × 2 cycles per `design.md`.
   `prefers-reduced-motion` + `.force-reduced-motion` body class both
   suppress the animation; `setTimeout(_, 3500)` fallback in JS still
   removes the `is-pulsing` class on the reduced-motion path so the
   chip never stays in the pulse state. */
@keyframes lifecycle-climax-pulse {
    0%   { box-shadow: 0 0 0 0 rgba(255, 140, 0, 0.55); }
    60%  { box-shadow: 0 0 0 8px rgba(255, 140, 0, 0); }
    100% { box-shadow: 0 0 0 0 rgba(255, 140, 0, 0); }
}
#chip-lifecycle.is-pulsing[data-stage="climax"] {
    animation: lifecycle-climax-pulse 1.5s ease-out 2;
}
@media (prefers-reduced-motion: reduce) {
    #chip-lifecycle.is-pulsing[data-stage="climax"] {
        animation: none;
    }
}
.force-reduced-motion #chip-lifecycle.is-pulsing[data-stage="climax"] {
    animation: none;
}

/* === Residents section (world-population-by-race / TASK_07 / AC27) === */
/* Per-tile race breakdown rendered by `tile_panel_sections.js
   ::renderResidents`. Reuses the surrounding `.t-section` rhythm
   (margin-top via `--sheet-section-gap`); this block scopes the row
   typography + the brass-pill `.resident-defend-chip` next to the
   section head. The chip styling matches the existing
   `.disable-suggest-chip` precedent (border-radius 999px + border-strong
   + text colour). NO `transition:` / NO `animation:` — regression
   guard from `ui-disabled-action-hints / AC-17`. */
#tile-details .tile-residents .resident-row {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: var(--space-2, 6px);
    padding: 1px 0;
    color: var(--text);
    font-size: var(--fs-body, 13px);
}
#tile-details .tile-residents .resident-name {
    color: var(--text);
}
#tile-details .tile-residents .resident-count {
    color: var(--text-dim);
    /* mobile-ux-rework TASK_08 / AC24 — `--font-mono` token. */
    font-family: var(--font-mono);
}
.resident-defend-chip {
    display: inline-block;
    margin-left: 6px;
    padding: 1px 8px;
    border-radius: 999px;
    background: var(--brass-soft, var(--bg-elev));
    border: 1px solid var(--border-strong);
    color: var(--text);
    font-size: var(--fs-label, 11px);
    vertical-align: middle;
}

/* === NPC Clan tile panel row + band chips (world-clans-reputation / TASK_11 / AC24 + AC25) === */
/* Band-colour chips shared between the tile-panel clan row and any future
   activity-feed band-changed entries. Background colours mirror the
   CLAN_BAND_COLOURS palette in overlay.js — one colour vocabulary. */
.band-chip {
    display: inline-block;
    padding: 2px 6px;
    border-radius: 4px;
    font-size: 0.8em;
    vertical-align: middle;
}
.band-chip.band-enemy    { background: #C13B3B; color: #fff; }
.band-chip.band-hostile  { background: #D88A3B; color: #fff; }
.band-chip.band-neutral  { background: #9A9A9A; color: #fff; }
.band-chip.band-friendly { background: #5BA84A; color: #fff; }
.band-chip.band-allied   { background: #3B7DD8; color: #fff; }

/* ui-modal-redesign / TASK_23 (AC12) — `data-band` lookup mirror so
   `<span class="band-chip" data-band="...">` chrome (no inline
   `style.background = colour` write) reads the atlas-palette tokens.
   Replaces the ClanView legacy `band-${bandKey}` className composition
   so the AC04 single-source-of-truth token table drives both surfaces. */
.band-chip[data-band="allied"]   { background: var(--lapis);          color: var(--ink); }
.band-chip[data-band="friendly"] { background: var(--patina);         color: var(--ink); }
.band-chip[data-band="neutral"]  { background: var(--ink-mute);       color: var(--surface); }
.band-chip[data-band="hostile"]  { background: var(--accent-strong);  color: var(--surface); }
.band-chip[data-band="enemy"]    { background: var(--vermilion);      color: var(--ink); }

/* Reputation bar mounted via shared `progress_bar.js::buildBar` reads
   the host card's `data-band` to colour the fill. Delegates the
   bipolar mapping (negative → vermilion, positive → patina) to a
   per-band override on `.atlas-bar-fill`. */
.atlas-card[data-band] .clan-panel-rep-bar .atlas-bar-fill {
    background: var(--ink-mute);
}
.atlas-card[data-band="allied"]   .clan-panel-rep-bar .atlas-bar-fill { background: var(--lapis); }
.atlas-card[data-band="friendly"] .clan-panel-rep-bar .atlas-bar-fill { background: var(--patina); }
.atlas-card[data-band="hostile"]  .clan-panel-rep-bar .atlas-bar-fill { background: var(--accent-strong); }
.atlas-card[data-band="enemy"]    .clan-panel-rep-bar .atlas-bar-fill { background: var(--vermilion); }

/* Clan tile-panel row — same rhythm as tile-row--river and tile-row--bridge. */
.tile-clan-row {
    display: flex;
    align-items: center;
    gap: 4px;
    font-size: var(--fs-body, 13px);
    color: var(--text-dim);
    padding: 2px 0;
}
.tile-clan-row a {
    color: var(--accent, #88ccff);
    text-decoration: none;
}
.tile-clan-row a:hover {
    text-decoration: underline;
}
.clan-relation {
    color: var(--text-muted);
    font-size: 0.85em;
}

/* Lock glyph chip in the tile panel (canvas counterpart is drawn by _drawLockGlyph). */
.clan-tile-lock {
    display: inline-block;
    margin-left: 2px;
    vertical-align: middle;
}

/* === Clan panels (world-clans-reputation / TASK_12) === */
/* Shared panel body reset — gives a scrollable content area below the
   sheet-header inside the sheet-modal frame. */
.clan-panel-body,
.clans-panel-body {
    padding: 8px 12px 16px;
    overflow-y: auto;
    max-height: calc(100% - 48px);
}

/* ── Clan detail panel ── */
.clan-panel-header {
    margin-bottom: 10px;
}
.clan-panel-name-row {
    display: flex;
    align-items: center;
    gap: 6px;
    flex-wrap: wrap;
}
.clan-panel-name {
    font-size: var(--fs-h2, 16px);
    font-weight: 600;
    color: var(--text);
}
.clan-panel-meta {
    display: flex;
    gap: 10px;
    font-size: var(--fs-body, 13px);
    color: var(--text-dim);
    margin-top: 4px;
    flex-wrap: wrap;
}
.clan-panel-jump-btn {
    margin-top: 6px;
}
.clan-panel-section {
    margin-top: 12px;
    border-top: 1px solid var(--border);
    padding-top: 8px;
}
.clan-panel-section-label {
    font-size: var(--fs-body, 13px);
    color: var(--text-dim);
    text-transform: uppercase;
    letter-spacing: 0.04em;
    margin-bottom: 6px;
}
/* Relation bar */
.clan-panel-bar-wrap {
    width: 100%;
    margin-bottom: 4px;
}
.clan-panel-bar-track {
    width: 100%;
    height: 8px;
    background: var(--border-strong);
    border-radius: 4px;
    overflow: hidden;
}
.clan-panel-bar-fill {
    height: 100%;
    border-radius: 4px;
    transition: width 0.3s ease;
}
.clan-panel-rep-value {
    font-size: var(--fs-body, 13px);
    font-weight: 600;
}
/* Defends badge */
.clan-panel-defends-badge {
    font-size: var(--fs-body, 13px);
    color: var(--text-dim);
}
/* Known tile chips */
.clan-panel-tile-chips {
    display: flex;
    flex-wrap: wrap;
    gap: 4px;
}
.clan-panel-tile-chip {
    font-size: 0.8em;
    padding: 2px 6px;
}
/* Timeline */
.clan-panel-timeline {
    display: flex;
    flex-direction: column;
    gap: 4px;
}
.clan-panel-timeline-row {
    display: flex;
    align-items: center;
    gap: 6px;
    font-size: var(--fs-body, 13px);
    flex-wrap: wrap;
}
.clan-panel-ev-tick {
    color: var(--text-muted);
    font-size: 0.8em;
    min-width: 56px;
}
.clan-panel-ev-arrow {
    color: var(--text-dim);
}
.clan-panel-loading,
.clan-panel-error {
    color: var(--text-dim);
    padding: 12px 0;
    font-size: var(--fs-body, 13px);
}

/* ── Clan list panel ── */
.clans-panel-controls {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    margin-bottom: 10px;
    align-items: center;
}
.clans-panel-sort-chip.is-active {
    background: var(--accent, #88ccff);
    color: #000;
}
.clans-panel-list {
    display: flex;
    flex-direction: column;
    gap: 2px;
}
.clans-panel-row {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 6px 4px;
    border-radius: 4px;
    cursor: pointer;
    flex-wrap: wrap;
    font-size: var(--fs-body, 13px);
    border-bottom: 1px solid var(--border);
}
.clans-panel-row:hover {
    background: rgba(255, 255, 255, 0.04);
}
.clans-panel-row:focus {
    outline: 2px solid var(--accent, #88ccff);
    outline-offset: 1px;
}
.clans-row-name {
    font-weight: 500;
    color: var(--text);
    min-width: 80px;
    flex: 1;
}
.clans-row-race {
    color: var(--text-dim);
    font-size: 0.85em;
}
.clans-row-relation {
    color: var(--text-dim);
    font-size: 0.9em;
    min-width: 36px;
    text-align: right;
}
.clans-row-defends {
    font-size: 0.85em;
    color: var(--text-muted);
}
.clans-panel-loading,
.clans-panel-error,
.clans-panel-empty {
    color: var(--text-dim);
    padding: 12px 0;
    font-size: var(--fs-body, 13px);
}

/* === Activity-feed clan event button (world-clans-reputation / TASK_13 / AC26) === */
/* Ghost button rendered inline inside a clan_band_changed feed row.
   Sits next to the band chip; taps jump the camera to the event tile via
   the existing #activity-list data-action="jump-to" delegation. */
.feed-goto-btn {
    display: inline-block;
    padding: 1px 6px;
    border: 1px solid var(--border-strong);
    border-radius: 3px;
    background: transparent;
    color: var(--text-dim);
    font-size: 0.8em;
    cursor: pointer;
    vertical-align: middle;
    line-height: 1.6;
    transition: background 0.15s, color 0.15s;
}
.feed-goto-btn:hover {
    background: rgba(255, 255, 255, 0.06);
    color: var(--text);
}

/* === Tile extraction block (world-resource-extraction-rework / TASK_13 / AC36) === */
/* "🪓 Видобуток" section in the legacy tile panel + fullscreen TileView.
   One .tile-extract-row per resource; button mirrors .btn-sm metrics;
   .depletion-chip coloured by state enum. */
.extraction-block {
    margin-top: 12px;
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
}
.tile-extract-row {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 2px;
}
/* mirrors .btn-sm metrics; override width/padding for resource buttons */
.tile-extract-btn {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    padding: 3px 8px;
    border: 1px solid var(--border-strong);
    border-radius: 4px;
    background: var(--bg-panel, #1e2432);
    color: var(--text);
    font-size: var(--fs-body, 13px);
    cursor: pointer;
    min-height: var(--tap-sm, 36px);
    line-height: 1.4;
    transition: background 0.12s, color 0.12s;
}
.tile-extract-btn:hover {
    background: #3a5a8e;
    color: #fff;
}
.tile-extract-btn:active {
    background: rgba(90, 166, 255, 0.15);
}
.tile-extract-btn[disabled] {
    opacity: 0.4;
    cursor: not-allowed;
}
/* Depletion state chip — colour-coded by resource depletion level */
.depletion-chip {
    display: inline-block;
    padding: 1px 5px;
    border-radius: 3px;
    font-size: 0.78em;
    font-weight: 500;
    line-height: 1.5;
}
.depletion-chip[data-state="fresh"]      { background: #4a7c59; color: white; }
.depletion-chip[data-state="light"]      { background: #c4a23a; color: black; }
.depletion-chip[data-state="heavy"]      { background: #d97a3a; color: white; }
.depletion-chip[data-state="exhausted"]  { background: #b94a3a; color: white; }
/* Reduced-motion: suppress extraction button transitions */
@media (prefers-reduced-motion: reduce) {
    .tile-extract-btn { transition: none; }
}
html.force-reduced-motion .tile-extract-btn { transition: none; }

/* ─── TileView Будівлі-tab cards (ui-armies-buildings-redesign TASK_06) ──
 * Card grid + per-card anatomy for the image-card buildings tab. Pairs
 * with `views/tile_view_buildings.js` (TASK_05) which emits the markup:
 *
 *   .tile-view-buildings                grid wrapper (auto-fill columns)
 *   └─ .tile-view-bld-card              one card (image + body)
 *      ├─ .tile-view-bld-card-img       60×60 (desktop) / 48×48 (mobile)
 *      │  ├─ <img>                      `/static/img/economy/buildings/<kind>.png`
 *      │  └─ .tile-view-bld-card-glyph  text fallback when img.is-broken
 *      └─ .tile-view-bld-card-body
 *         ├─ .tile-view-bld-card-name   bolded label
 *         ├─ .tile-view-bld-card-meta   `Рівень N · завершено / N%`
 *         ├─ .tile-view-bld-card-owner  foreign-only chip (via escapeHtml)
 *         └─ .tile-view-bld-card-progress  in-progress bar (`--pct` driven)
 *
 * Token mapping vs spec:
 *   --bg-1          → var(--bg)              (image-slot recess + progress trough)
 *   --bg-2          → var(--bg-elev)         (card body)
 *   --bg-3          → rgba(255, 183, 0, 0.10) (hover glow — matches filter-chip is-active)
 *   --border-1      → var(--border)          (card outline)
 *   --accent-ochre  → var(--color-warning)   (focus ring + glyph + progress fill)
 *   --rel-hostile   → var(--color-danger)    (foreign card border)
 *   --font-display  → var(--font-display)    (already in :root since T21)
 *
 * The `:has()` selector (already used elsewhere in this file for the
 * workspace shell layout swap) gates the glyph reveal when the
 * `<img>` element fails to load and gets `.is-broken` from the
 * `addEventListener('error', …)` binding in `tile_view_buildings.js`.
 * Modern evergreen targets per `frontend/AGENTS.md` cover Chrome 105+ /
 * Firefox 121+ / Safari 15.4+, all of which support `:has()`.
 */
.tile-view-buildings {
    display: grid;
    gap: 12px;
    grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
}
@media (max-width: 639px) {
    .tile-view-buildings { grid-template-columns: 1fr; gap: 8px; }
}

.tile-view-bld-card {
    display: grid;
    grid-template-columns: 60px 1fr;
    gap: 12px;
    padding: 10px;
    border-radius: var(--radius-md);
    background: var(--bg-elev);
    border: 1px solid var(--border);
    cursor: pointer;
    align-items: center;
    min-height: 56px;
    transition: background 120ms ease, border-color 120ms ease;
}
.tile-view-bld-card:hover,
.tile-view-bld-card:focus-visible {
    background: rgba(255, 183, 0, 0.10);
    outline: none;
}
.tile-view-bld-card:focus-visible {
    box-shadow: 0 0 0 2px var(--color-warning);
}
.tile-view-bld-card.is-foreign {
    border-color: var(--color-danger);
}

.tile-view-bld-card-img {
    position: relative;
    width: 60px;
    height: 60px;
    background: var(--bg);
    border-radius: 4px;
    overflow: hidden;
    flex-shrink: 0;
}
.tile-view-bld-card-img img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}
.tile-view-bld-card-img img.is-broken {
    display: none;
}
.tile-view-bld-card-glyph {
    position: absolute;
    inset: 0;
    display: none;
    align-items: center;
    justify-content: center;
    font-family: var(--font-display);
    font-size: 28px;
    color: var(--color-warning);
}
/* Reveal the glyph when the `<img>` is broken (class flipped by the
 * error listener) or absent altogether (defensive — markup always
 * renders the img today, but `:not(:has(img))` keeps the rule honest
 * in case a future variant drops it). */
.tile-view-bld-card-img:has(img.is-broken) .tile-view-bld-card-glyph,
.tile-view-bld-card-img:not(:has(img)) .tile-view-bld-card-glyph {
    display: flex;
}

.tile-view-bld-card-body {
    min-width: 0;
}
.tile-view-bld-card-name {
    font-weight: 600;
    color: var(--text);
}
.tile-view-bld-card-meta {
    font-size: 0.875rem;
    color: var(--text-dim);
    margin-top: 2px;
}
.tile-view-bld-card-owner {
    display: inline-block;
    margin-top: 4px;
    padding: 2px 6px;
    background: rgba(255, 107, 107, 0.15);
    color: var(--color-danger);
    border-radius: 3px;
    font-size: 0.75rem;
}
.tile-view-bld-card-owner[hidden] {
    display: none;
}
.tile-view-bld-card-progress {
    margin-top: 6px;
    height: 4px;
    background: var(--bg);
    border-radius: 2px;
    overflow: hidden;
}
.tile-view-bld-card-progress[hidden] {
    display: none;
}
.tile-view-bld-card-progress-bar {
    width: var(--pct, 0%);
    height: 100%;
    background: var(--color-warning);
    transition: width 200ms ease;
}

@media (max-width: 639px) {
    .tile-view-bld-card {
        grid-template-columns: 48px 1fr;
        gap: 10px;
    }
    .tile-view-bld-card-img {
        width: 48px;
        height: 48px;
    }
    .tile-view-bld-card-glyph {
        font-size: 22px;
    }
}

/* Reduced-motion: kill the hover/progress fade so motion-sensitive
 * players don't see colour swaps on every pointer pass. */
@media (prefers-reduced-motion: reduce) {
    .tile-view-bld-card,
    .tile-view-bld-card-progress-bar {
        transition: none;
    }
}
html.force-reduced-motion .tile-view-bld-card,
html.force-reduced-motion .tile-view-bld-card-progress-bar {
    transition: none;
}

/* ============================================================
 * ui-modal-redesign / TASK_02 — atlas chrome
 * ------------------------------------------------------------
 * Canonical card + tabs + empty + modal anatomy. Lives parallel
 * to the legacy view-module CSS (`tile-view-*`, `army-view-*`,
 * `garrison-card`, etc.) until per-view migrations T14–T26 swap
 * consumers over. All rules consume the atlas tokens declared
 * on `:root` by TASK_01 (AC01–AC03) — no palette / radius /
 * spacing literals here.
 *
 * Anatomy reference:
 *   design.md §3 — canonical card (`<article class="atlas-card">`)
 *   design.md §4 — canonical modal (`<section class="atlas-modal">`)
 *   design.md §2.4 — motion budget (atlas-modal-in / atlas-tab-fade
 *                    keyframes + atlas-card hover transition only)
 *
 * Anything here that needs to flex per-variant goes through
 * `data-variant="<slug>"` on the `.atlas-card` root — no extra
 * wrapper class, per AC05.
 * ============================================================ */

/* ─── atlas-card — canonical card chrome ─────────────────────
 * Slots (positional, collapse when empty):
 *   .atlas-card-glyph         — 56 desktop / 40 mobile, leading
 *   .atlas-card-body-wrap     — primary + secondary + meta + body
 *   .atlas-card-rule          — 1 px ledger rule, only when footer
 *   .atlas-card-footer        — badges left, primary action right
 * Padding ladder: 16 standard / 12 dense / 24 hero (data-density).
 */
.atlas-card {
    position: relative;
    display: grid;
    grid-template-columns: auto 1fr;
    column-gap: var(--space-3);
    row-gap: var(--space-2);
    align-items: start;
    padding: var(--space-4);
    background: var(--surface-elev);
    color: var(--ink);
    border: 1px solid var(--border-soft);
    border-radius: var(--radius-md);
    font-family: var(--font-body);
    /* Hover transition is the third + last motion budget item per
     * design.md §2.4 (AC47). Background-color + border-color only;
     * no transform, no shadow, no spring. Reduced-motion kills it
     * via the gate below. */
}
.atlas-card[data-density="dense"] {
    padding: var(--space-3);
}
.atlas-card[data-density="hero"] {
    padding: var(--space-6);
}
.atlas-card[data-status="error"] {
    border-color: var(--vermilion);
}
.atlas-card[data-status="warn"] {
    border-color: var(--accent-strong);
}
.atlas-card[data-status="ok"] {
    border-color: var(--patina);
}

.atlas-card-glyph {
    width: 56px;
    height: 56px;
    flex: 0 0 auto;
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: hidden;
    border-radius: var(--radius-sm);
    background: var(--surface-sunken);
    color: var(--ink-mute);
    grid-row: 1 / span 2;
}
.atlas-card-glyph img,
.atlas-card-glyph svg {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}
.atlas-card-glyph .atlas-monogram {
    font-family: var(--font-display);
    font-weight: 600;
    font-size: 22px;
    color: var(--ink);
    line-height: 1;
}

.atlas-card-body-wrap {
    min-width: 0;            /* allow flex children to clamp */
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
}

.atlas-card-primary {
    margin: 0;
    font-family: var(--font-display);
    font-weight: 600;
    font-size: 16px;
    line-height: var(--lh-tight);
    color: var(--ink);
    /* AC05 — primary line clamp at 2 with graceful word-wrap so
     * `Highlands Mountain Dwarf`-class long compounds break inside
     * a token rather than blowing the row. */
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
    overflow-wrap: anywhere;
    word-break: break-word;
}
.atlas-card-secondary {
    margin: 0;
    font-family: var(--font-body);
    font-weight: 400;
    font-size: 12px;
    color: var(--ink-mute);
    line-height: var(--lh-body);
}
.atlas-card-body {
    margin-top: var(--space-2);
    color: var(--ink);
    font-family: var(--font-body);
    font-size: var(--fs-body);
    line-height: var(--lh-body);
}

/* ─── atlas-card-meta — chip strip (AC06) ──────────────────── */
.atlas-card-meta {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-1);
    align-items: center;
}
.atlas-meta-chip {
    display: inline-flex;
    align-items: baseline;
    gap: var(--space-1);
    color: var(--ink-mute);
    font-size: 0.78em;
    line-height: 1.4;
    padding: 0;
    background: transparent;
    border: 0;
}
.atlas-meta-chip + .atlas-meta-chip {
    /* `·` separator between chips per design.md §3 ("·-separated"). */
    position: relative;
    padding-left: calc(var(--space-2) + 4px);
}
.atlas-meta-chip + .atlas-meta-chip::before {
    content: '·';
    position: absolute;
    left: var(--space-1);
    color: var(--ink-faint);
}
.atlas-meta-chip .k {
    color: var(--ink-faint);
    font-weight: 500;
}
.atlas-meta-chip .v {
    color: var(--ink-mute);
    font-weight: 500;
}
.atlas-meta-chip .v.numeric,
.atlas-meta-chip.numeric .v {
    font-family: var(--font-mono);
    font-variant-numeric: tabular-nums;
}
.atlas-meta-chip--overflow {
    /* "+N" overflow chip rendered when >4 — chip carries the count
     * via text content (e.g. "+2"); no separator dot before it. */
    color: var(--ink-faint);
    font-style: italic;
}
.atlas-meta-chip--overflow + .atlas-meta-chip,
.atlas-meta-chip + .atlas-meta-chip--overflow::before {
    content: none;
}

/* ─── atlas-card ledger rule + footer ─────────────────────── */
.atlas-card-rule {
    grid-column: 1 / -1;
    margin: var(--space-2) 0 0 0;
    border: 0;
    height: 1px;
    background: var(--rule);
    width: 100%;
}
.atlas-card-rule:last-child {
    /* AC05 — rule appears `display: none` when no footer. */
    display: none;
}
.atlas-card-footer {
    grid-column: 1 / -1;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-2);
    margin-top: var(--space-2);
}
.atlas-card-footer-badges {
    display: flex;
    align-items: center;
    gap: var(--space-1);
    flex-wrap: wrap;
}
.atlas-card-footer .atlas-btn {
    margin-left: auto;
}

/* ─── atlas-badge — chip-shaped status / role label ──────── */
.atlas-badge {
    display: inline-flex;
    align-items: center;
    gap: var(--space-1);
    padding: 2px 8px;
    border-radius: var(--radius-pill);
    background: var(--surface-sunken);
    color: var(--ink-mute);
    font-family: var(--font-body);
    font-size: 11px;
    font-weight: 600;
    line-height: 1.4;
    text-transform: none;
    border: 1px solid var(--border-soft);
}
.atlas-badge.--leader   { color: var(--accent-strong); border-color: var(--accent-strong); }
.atlas-badge.--allied   { color: var(--lapis); border-color: var(--lapis); }
.atlas-badge.--friendly { color: var(--patina); border-color: var(--patina); }
.atlas-badge.--hostile  { color: var(--accent-strong); border-color: var(--accent-strong); }
.atlas-badge.--enemy    { color: var(--vermilion); border-color: var(--vermilion); }

/* ─── atlas-btn — card primary action ─────────────────────── */
.atlas-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-1);
    min-height: var(--tap-sm);
    padding: 6px var(--space-3);
    border-radius: var(--radius-md);
    border: 1px solid var(--border-firm);
    background: var(--surface-sunken);
    color: var(--ink);
    font-family: var(--font-body);
    font-size: 13px;
    font-weight: 600;
    cursor: pointer;
}
.atlas-btn.--primary {
    background: var(--accent);
    color: var(--surface);
    border-color: var(--accent);
}
.atlas-btn.--primary:hover {
    background: var(--accent-strong);
    border-color: var(--accent-strong);
}
.atlas-btn.--ghost {
    background: transparent;
    border-color: var(--border-firm);
    color: var(--ink-mute);
}
.atlas-btn[disabled],
.atlas-btn[aria-disabled="true"] {
    opacity: 0.55;
    cursor: not-allowed;
}

/* ─── atlas-card variants (data-variant) ──────────────────── */
.atlas-card[data-variant="army"]      { border-left: 3px solid var(--accent); padding-left: calc(var(--space-4) - 3px); }
.atlas-card[data-variant="unit"]      { border-left: 3px solid var(--lapis);  padding-left: calc(var(--space-4) - 3px); }
.atlas-card[data-variant="building"]  { border-left: 3px solid var(--patina); padding-left: calc(var(--space-4) - 3px); }
.atlas-card[data-variant="stockpile"] { border-left: 3px solid var(--ink-faint); padding-left: calc(var(--space-4) - 3px); }
.atlas-card[data-variant="intel"]     { border-left: 3px solid var(--ink-mute);  padding-left: calc(var(--space-4) - 3px); }
.atlas-card[data-variant="clan"]      { border-left: 3px solid var(--ink-mute);  padding-left: calc(var(--space-4) - 3px); }
.atlas-card[data-variant="planner"]   { border-left: 3px solid var(--accent-strong); padding-left: calc(var(--space-4) - 3px); }
.atlas-card[data-variant="hero"]      {
    padding: var(--space-6);
    background: linear-gradient(180deg, var(--surface-elev) 0%, var(--surface) 100%);
    border-color: var(--border-firm);
}
.atlas-card[data-variant="skill"]     { border-left: 3px solid var(--patina); padding-left: calc(var(--space-4) - 3px); }
.atlas-card[data-variant="tech"]      { border-left: 3px solid var(--accent); padding-left: calc(var(--space-4) - 3px); }

/* ─── tech-tree-rework / TASK_06 ──────────────────────────────
 * 5-tier swimlane layout + TechCard chrome (AC08 / AC11 / AC12 /
 * AC13). Locked variant flips desaturate via `data-locked="true"`;
 * pip row visualises `current_level / max_level` per AC12; SVG
 * edge overlay uses the existing `--tech-edge` token shared with
 * the science-tech-tree DAG renderer above.
 */
.tech-tree-heading {
    margin: 0 0 var(--space-4);
}
.tech-tree-wrap {
    position: relative;
    display: flex;
    flex-direction: column;
    gap: var(--space-6);
}
.tech-tree-tier {
    position: relative;
    z-index: 1;
}
.tech-tree-tier-heading {
    display: flex;
    align-items: baseline;
    gap: var(--space-2);
    margin: 0 0 var(--space-3);
}
.tech-tree-tier-count {
    color: var(--ink-mute);
    font-size: 0.85em;
}
.tech-tree-tier-grid {
    /* Inherits `.atlas-card-grid` flex layout from the shared rule;
     * locked tiers paint side-by-side cards horizontally per AC11. */
}
.tech-tree-edges {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    pointer-events: none;
    z-index: 0;
}
.tech-tree-edge {
    fill: none;
    stroke: var(--tech-edge);
    stroke-width: 2;
    stroke-linecap: round;
    opacity: 0.75;
}
.tech-card-art-slot {
    position: relative;
    overflow: hidden;
    border-radius: var(--radius-2);
}
.tech-card-art {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}
.tech-card-art-slot--locked .tech-card-art {
    filter: grayscale(0.85) opacity(0.6);
}
.tech-card-lock-badge {
    position: absolute;
    top: 4px;
    right: 4px;
    font-size: 14px;
    line-height: 1;
    padding: 2px 4px;
    background: rgba(0, 0, 0, 0.55);
    color: #fff;
    border-radius: var(--radius-pill);
}
.tech-card-pips {
    display: flex;
    gap: 4px;
    margin: var(--space-2) 0;
}
.tech-pip {
    width: 10px;
    height: 10px;
    border-radius: 50%;
    background: transparent;
    border: 1.5px solid var(--ink-mute);
    display: inline-block;
}
.tech-pip.filled {
    background: var(--tech-researched, var(--patina));
    border-color: var(--tech-researched, var(--patina));
}
.atlas-card[data-variant="tech"][data-locked="true"] {
    opacity: 0.7;
}
.atlas-card[data-variant="tech"][data-state="maxed"] {
    border-left-color: var(--tech-researched, var(--patina));
}
.atlas-card[data-variant="tech"][data-state="maxed"] .tech-pip {
    background: var(--tech-researched, var(--patina));
    border-color: var(--tech-researched, var(--patina));
}
.tech-tree-empty {
    /* Inherits `.atlas-card--empty` chrome; nothing more to layer. */
}

/* ─── tech-tree-rework / TASK_07 ──────────────────────────────
 * Per-tech detail view (AC09 / AC10 / AC14). Mounted into the
 * workspace shell host pane via `tech_detail_view.js` against the
 * `tech-detail` view kind. Mirrors the `.skill-detail-*` cadence —
 * stacked on mobile, two-column with a hero gutter on desktop.
 */
.atlas-card[data-variant="tech-detail"] {
    display: flex;
    flex-direction: column;
    gap: var(--space-4);
    padding: var(--space-4);
    border-left: 3px solid var(--accent);
}
.tech-detail-card .detail__hero-wrap {
    width: 100%;
    max-width: 480px;
    aspect-ratio: 1 / 1;
    align-self: center;
    border-radius: var(--radius-2);
    overflow: hidden;
    background: var(--ink-mute-2, rgba(0, 0, 0, 0.05));
    display: flex;
    align-items: center;
    justify-content: center;
}
.tech-detail-card .detail__hero {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}
.tech-detail-card .detail__hero-fallback {
    font-size: 88px;
    color: var(--ink-mute);
}
.tech-detail-card .detail__header {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
    align-items: baseline;
    color: var(--ink-mute);
    font-size: 0.9em;
}
.tech-detail-card .detail__tier {
    font-weight: 600;
    color: var(--patina);
}
.tech-detail-card .detail__progress {
    color: var(--ink);
}
.tech-detail-card .detail__xp-progress {
    color: var(--ink-mute);
}
.tech-detail-card .detail__name {
    margin: 0;
    font-size: 1.5em;
}
.tech-detail-card .detail__lore {
    margin: 0;
    color: var(--ink-mute);
    font-style: italic;
    line-height: 1.5;
}
.tech-detail-card .detail__section-title {
    margin: 0 0 var(--space-2);
    font-size: 1em;
    color: var(--patina);
}
.tech-detail-card .detail__levels {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
}
.tech-detail-card .detail__level {
    border: 1px solid var(--ink-mute-2, rgba(0, 0, 0, 0.12));
    border-radius: var(--radius-1);
    padding: var(--space-2) var(--space-3);
}
.tech-detail-card .detail__level[data-state="researched"] {
    border-left: 3px solid var(--tech-researched, var(--patina));
    background: rgba(0, 0, 0, 0.02);
}
.tech-detail-card .detail__level[data-state="next"] {
    border-left: 3px solid var(--accent);
}
.tech-detail-card .detail__level[data-state="locked"] {
    opacity: 0.65;
}
.tech-detail-card .detail__level-head {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
    align-items: baseline;
    margin-bottom: var(--space-1);
}
.tech-detail-card .detail__level-num {
    font-weight: 600;
}
.tech-detail-card .detail__level-state {
    color: var(--ink-mute);
    font-size: 0.85em;
}
.tech-detail-card .detail__level-cost {
    margin-left: auto;
    color: var(--ink-mute);
    font-size: 0.85em;
}
.tech-detail-card .detail__level-rewards {
    margin: 0;
    padding-left: var(--space-4);
}
.tech-detail-card .detail__level-reward {
    margin: 2px 0;
}
.tech-detail-card .detail__prereqs,
.tech-detail-card .detail__unlocks {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
}
.tech-detail-card .detail__prereqs-list,
.tech-detail-card .detail__unlocks-list {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
    margin: 0;
    padding: 0;
    list-style: none;
}
.tech-detail-card .detail__prereqs-item,
.tech-detail-card .detail__unlocks-item {
    list-style: none;
}
.tech-detail-card .detail__prereq-chip,
.tech-detail-card .detail__unlock-chip {
    background: rgba(0, 0, 0, 0.05);
    border: 1px solid var(--ink-mute-2, rgba(0, 0, 0, 0.12));
    border-radius: var(--radius-pill);
    padding: 4px 10px;
    cursor: pointer;
    font: inherit;
    color: inherit;
}
.tech-detail-card .detail__prereq-chip:hover,
.tech-detail-card .detail__unlock-chip:hover {
    background: rgba(0, 0, 0, 0.1);
}
.tech-detail-card .detail__research-btn {
    align-self: flex-start;
    background: var(--accent);
    color: #fff;
    border: none;
    border-radius: var(--radius-1);
    padding: var(--space-2) var(--space-4);
    font-weight: 600;
    cursor: pointer;
}
.tech-detail-card .detail__research-btn:disabled,
.tech-detail-card .detail__research-btn[aria-disabled="true"] {
    opacity: 0.55;
    cursor: not-allowed;
}
.tech-detail-card .detail__empty {
    margin: 0;
    color: var(--ink-mute);
    font-style: italic;
}
@media (min-width: 720px) {
    .atlas-card[data-variant="tech-detail"] {
        flex-direction: row;
        align-items: flex-start;
    }
    .tech-detail-card .detail__hero-wrap {
        flex: 0 0 320px;
        max-width: 320px;
    }
    .tech-detail-card .detail__body {
        flex: 1 1 auto;
        display: flex;
        flex-direction: column;
        gap: var(--space-3);
    }
}
.tech-detail-empty {
    /* Inherits `.atlas-card--empty` chrome; nothing more to layer. */
}

/* clan-band overlay (AC12) — `data-band` lookup replaces inline
 * `el.style.background = …` writes. Bands re-key via
 * `clan_bands.js` BAND_TO_TOKEN (`allied → lapis`, `friendly →
 * patina`, `neutral → ink-mute`, `hostile → accent-strong`,
 * `enemy → vermilion`). The 3 px left rail acts as the visible
 * band even when `data-variant` is also set. */
.atlas-card[data-band="allied"]   { border-left-color: var(--lapis); }
.atlas-card[data-band="friendly"] { border-left-color: var(--patina); }
.atlas-card[data-band="neutral"]  { border-left-color: var(--ink-mute); }
.atlas-card[data-band="hostile"]  { border-left-color: var(--accent-strong); }
.atlas-card[data-band="enemy"]    { border-left-color: var(--vermilion); }

/* ─── atlas-card[data-variant="action"] (AC30) ──────────────
 * AP-cost badge absolutely positioned top-right, brass background,
 * monospace digits. Disabled / locked states surface via
 * `[data-action-locked]` and recolour the badge. */
.atlas-card[data-variant="action"] {
    border-left: 3px solid var(--accent);
    padding-left: calc(var(--space-4) - 3px);
    padding-right: calc(var(--space-4) + 48px);
}
.atlas-card[data-variant="action"] .ap-badge {
    position: absolute;
    top: var(--space-2);
    right: var(--space-2);
    font-family: var(--font-mono);
    font-variant-numeric: tabular-nums;
    background: var(--accent);
    color: var(--surface);
    padding: 2px 8px;
    border-radius: var(--radius-pill);
    font-size: 11px;
    font-weight: 600;
    line-height: 1.4;
}
.atlas-card[data-variant="action"][data-action-locked] .ap-badge {
    background: var(--vermilion);
}
.atlas-card[data-variant="action"][data-action-locked] {
    opacity: 0.65;
    pointer-events: auto; /* tap fires a toast, doesn't fire API */
}

/* ─── atlas-card mobile (≤480 px) ─────────────────────────── */
@media (max-width: 480px) {
    .atlas-card-glyph {
        width: 40px;
        height: 40px;
    }
    .atlas-card-glyph .atlas-monogram {
        font-size: 16px;
    }
    .atlas-card[data-variant="hero"] {
        padding: var(--space-4);
    }
    .atlas-card[data-variant="action"] {
        padding-right: calc(var(--space-3) + 44px);
    }
    /* AC13 mobile contract — TileView header band (single column,
     * gap collapses, glyph 40 px (already covered by the rule
     * above), meta wraps two rows max via `.atlas-card-meta`). */
    .atlas-card[data-variant="hero"].tile-view-header,
    .tile-view-header.atlas-card {
        padding: var(--space-4);
    }
}

/* ─── actions-grid — TileView R6 actions strip (AC30 / AC34) ──
 * Auto-fit grid, 200 px minmax tracks (single column on mobile per
 * spec AC34). Cards inside are `.atlas-card[data-variant="action"]`
 * — see the rule block above for the AP-badge anatomy. The grid
 * sits at the top of the Лагер tab body in `tile_view.js`. */
.actions-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
    gap: var(--space-3);
    margin: 0 0 var(--space-4) 0;
    padding: 0;
    list-style: none;
}
.actions-grid .atlas-card[data-variant="action"] {
    /* Card buttons reset native chrome so the atlas-card variant
     * paint stays canonical. `text-align: left` undoes the
     * `<button>` centred-content default. */
    appearance: none;
    text-align: left;
    cursor: pointer;
    font-family: inherit;
    color: inherit;
    width: 100%;
}
.actions-grid .atlas-card[data-variant="action"] .ap-badge.is-zero {
    background: var(--ink-faint);
    color: var(--ink-mute);
}
@media (max-width: 480px) {
    .actions-grid {
        grid-template-columns: 1fr;
        gap: var(--space-2);
    }
}

/* AC34 — sticky-footer mirror of the primary action-card. The
 * shell's `.atlas-modal-sticky` host already gates visibility to
 * `<640 px`; this rule makes the cloned card sit flush across the
 * full footer width with a subtle top-border separator. */
.atlas-modal-sticky-card.atlas-card[data-variant="action"] {
    border: 0;
    border-top: 1px solid var(--border-firm);
    border-radius: 0;
    margin: 0;
    width: 100%;
    background: var(--surface-elev);
}

/* ─── atlas-card hover (motion budget #3) ─────────────────── */
@media (prefers-reduced-motion: no-preference) {
    .atlas-card {
        transition: background-color 120ms ease-out, border-color 120ms ease-out;
    }
    .atlas-card:hover,
    .atlas-card:focus-visible,
    .atlas-card[data-variant]:active {
        background-color: var(--surface);
        border-color: var(--border-firm);
    }
}
@media (prefers-reduced-motion: reduce) {
    .atlas-card { transition: none; }
}
html.force-reduced-motion .atlas-card { transition: none; }

/* ─── atlas-tabs — canonical tab strip (AC07) ─────────────────
 * Leather-cut tabs with brass underline. Active tab: 2 px
 * `--accent` underline + `--surface-elev` background + EB
 * Garamond 600. Inactive: `--surface`, `--ink-mute`, Public Sans
 * 500. `<sup>` carries the numeric badge per design.md §4 — no
 * brackets, no parentheses. Strip height: 44 desktop / 48 mobile.
 *
 * Single canonical strip — `tile_view_tabs.js`, `army_view.js`,
 * `unit_view.js`, `armies_view.js`, `clan_view.js` will all
 * consume this in T14–T23 (per design.md §1.2). Old per-view
 * tab CSS (`.tile-view-tabs`, etc.) stays parallel until those
 * tasks land. */
.atlas-tabs {
    display: flex;
    align-items: stretch;
    height: 44px;
    gap: 0;
    margin: 0;
    padding: 0 var(--space-2);
    list-style: none;
    background: var(--surface-sunken);
    border-bottom: 1px solid var(--border-soft);
    overflow-x: auto;
    overflow-y: hidden;
    scrollbar-width: thin;
}
.atlas-tabs > button[role="tab"],
.atlas-tabs > .atlas-tab {
    appearance: none;
    border: 0;
    background: var(--surface);
    color: var(--ink-mute);
    font-family: var(--font-body);
    font-weight: 500;
    font-size: 13px;
    line-height: 1;
    padding: 0 var(--space-3);
    margin: 0;
    cursor: pointer;
    height: 100%;
    display: inline-flex;
    align-items: center;
    gap: var(--space-1);
    border-bottom: 2px solid transparent;
    white-space: nowrap;
    flex: 0 0 auto;
}
.atlas-tabs > button[role="tab"]:hover:not([aria-selected="true"]),
.atlas-tabs > button[role="tab"]:focus-visible:not([aria-selected="true"]),
.atlas-tabs > button[role="tab"]:active:not([aria-selected="true"]),
.atlas-tabs > .atlas-tab:hover:not(.atlas-tab--active),
.atlas-tabs > .atlas-tab:focus-visible:not(.atlas-tab--active),
.atlas-tabs > .atlas-tab:active:not(.atlas-tab--active) {
    color: var(--ink);
    background: var(--surface-elev);
}
.atlas-tabs > button[role="tab"][aria-selected="true"],
.atlas-tabs > .atlas-tab.atlas-tab--active {
    background: var(--surface-elev);
    color: var(--ink);
    font-family: var(--font-display);
    font-weight: 600;
    border-bottom: 2px solid var(--accent);
}
/* Filter variant — sort-chip strip (AC17 — ClanView, ClansPanel).
 * Same anatomy, smaller height, pill underline becomes a thin
 * brass underline. */
.atlas-tabs[data-variant="filter"] {
    height: 36px;
}
/* `<sup>` numeric badge per design.md §4. No `[N]` brackets. */
.atlas-tabs sup {
    font-family: var(--font-display);
    font-size: 0.7em;
    color: var(--accent-strong);
    line-height: 1;
    margin-left: 2px;
    vertical-align: super;
    font-weight: 600;
}
.atlas-tabs > button[role="tab"][aria-selected="true"] sup,
.atlas-tabs > .atlas-tab.atlas-tab--active sup {
    color: var(--accent);
}
/* Mobile (≤480 px) — strip height 48 px, hide `<sup>` badges so
 * label text doesn't wrap. */
@media (max-width: 480px) {
    .atlas-tabs {
        height: 48px;
    }
    .atlas-tabs sup {
        display: none;
    }
}
/* Tab body cross-fade is the second motion budget entry
 * (design.md §2.4). 160 ms ease-out, gated behind reduced-motion
 * preference. Active panel uses `.atlas-tab-body` (or `[role="tabpanel"]`
 * inside an `.atlas-modal-body`). */
@media (prefers-reduced-motion: no-preference) {
    .atlas-tab-body { animation: atlas-tab-fade 160ms ease-out both; }
}
@media (prefers-reduced-motion: reduce) {
    .atlas-tab-body { animation: none; }
}
html.force-reduced-motion .atlas-tab-body { animation: none; }
@keyframes atlas-tab-fade {
    from { opacity: 0; }
    to   { opacity: 1; }
}

/* ─── atlas-card--empty — single empty / loading / error layout
 * (AC09). Replaces seven per-view divs (TileView Лагер empty,
 * stockpile empty "Склад порожній", intel "no recent reports",
 * logistics "no manifest", etc.). Single layout: monogram glyph
 * (or single-line copperplate-italic icon), copy line, optional
 * retry primary button.
 *
 * Sub-states via `data-state="<loading|empty|error>"`:
 *   loading — spinner glyph, ink-mute text
 *   empty   — monogram glyph, copperplate-italic line
 *   error   — vermilion accent, retry CTA
 *
 * Centered single column; works inside any panel slot. */
.atlas-card--empty {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: var(--space-3);
    padding: var(--space-7) var(--space-4);
    background: var(--surface-elev);
    border: 1px dashed var(--border-soft);
    border-radius: var(--radius-md);
    color: var(--ink-mute);
    text-align: center;
    min-height: 120px;
    /* Override .atlas-card grid; this is a single column layout. */
    grid-template-columns: none;
}
.atlas-card--empty .atlas-card-glyph {
    width: 48px;
    height: 48px;
    background: transparent;
    border-radius: var(--radius-pill);
    color: var(--ink-faint);
    grid-row: auto;
}
.atlas-card--empty .atlas-card-glyph .atlas-monogram {
    font-family: var(--font-display);
    font-style: italic;
    font-size: 24px;
    color: var(--ink-faint);
}
.atlas-card--empty .atlas-card-primary {
    /* Copperplate-italic line per AC09. */
    font-family: var(--font-display);
    font-style: italic;
    font-weight: 500;
    font-size: 14px;
    color: var(--ink-mute);
    -webkit-line-clamp: unset;
    display: block;
}
.atlas-card--empty .atlas-card-secondary {
    color: var(--ink-faint);
    font-size: 12px;
}
.atlas-card--empty .atlas-btn {
    margin-top: var(--space-2);
}
.atlas-card--empty[data-state="loading"] .atlas-card-glyph {
    color: var(--ink-mute);
}
.atlas-card--empty[data-state="error"] {
    border-color: var(--vermilion);
    color: var(--ink);
}
.atlas-card--empty[data-state="error"] .atlas-card-primary {
    color: var(--vermilion);
}
@media (max-width: 480px) {
    .atlas-card--empty {
        padding: var(--space-5) var(--space-3);
        min-height: 96px;
    }
    .atlas-card--empty .atlas-card-glyph {
        width: 40px;
        height: 40px;
    }
}

/* ─── atlas-modal — canonical modal anatomy (AC08) ────────────
 * Single-frame anatomy (design.md §4):
 *   .atlas-modal-header  — title + breadcrumb chip + shell ✕
 *   .atlas-tabs          — canonical tab strip
 *   .atlas-modal-body    — single scroll, no nested overflow
 *   .atlas-modal-sticky  — mobile-only, materialises on primary CTA
 *
 * Modal entrance is the first motion-budget entry: 240 ms cubic
 * (.22, .94, .42, 1) animation gated behind reduced-motion (per
 * design.md §2.4). Reverse plays at 180 ms via class swap.
 *
 * The shell ✕ button is injected by `workspace_shell.js`; this
 * stylesheet only owns the layout slots. Per-view migrations
 * (T04, T14+) will swap the legacy header + back-chip markup
 * for `<header class="atlas-modal-header">`. */
.atlas-modal {
    position: relative;
    display: flex;
    flex-direction: column;
    background: var(--surface);
    color: var(--ink);
    border: 1px solid var(--border-firm);
    border-radius: var(--radius-lg);
    overflow: hidden;
    /* Body owns the scroll; modal frame stays bounded to viewport. */
    max-height: calc(100vh - 32px);
    max-height: calc(100dvh - 32px);
}

.atlas-modal-header {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    padding: var(--space-4) var(--space-6);
    background: var(--surface-elev);
    border-bottom: 1px solid var(--border-soft);
}
.atlas-modal-title {
    margin: 0;
    font-family: var(--font-display);
    font-weight: 600;
    font-size: 20px;
    line-height: var(--lh-tight);
    color: var(--ink);
    /* Truncate to one line — long titles get the full string in
     * the breadcrumb leaf below. */
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* ─── atlas-breadcrumb — header chip strip (AC08) ──────────── */
.atlas-breadcrumb {
    display: flex;
    align-items: center;
    flex-wrap: nowrap;
    gap: var(--space-1);
    font-family: var(--font-body);
    font-size: 12px;
    color: var(--ink-mute);
    line-height: 1.4;
    overflow: hidden;
}
.atlas-breadcrumb a {
    color: var(--ink-mute);
    text-decoration: none;
    border-bottom: 1px solid transparent;
    padding: 2px 0;
    flex: 0 0 auto;
}
.atlas-breadcrumb a:hover {
    color: var(--ink);
    border-bottom-color: var(--accent);
}
.atlas-breadcrumb .sep {
    color: var(--ink-faint);
    flex: 0 0 auto;
}
.atlas-breadcrumb [aria-current="page"] {
    color: var(--ink);
    font-weight: 500;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    min-width: 0;
}
/* Mobile (<720) — collapse to leaf with `‹` back-affordance.
 * The collapse-back chevron sits before `[aria-current="page"]`;
 * intermediate links + separators hide. */
@media (max-width: 719px) {
    .atlas-breadcrumb a:not(:last-of-type),
    .atlas-breadcrumb .sep {
        display: none;
    }
    .atlas-breadcrumb::before {
        content: '‹';
        color: var(--ink-mute);
        font-size: 16px;
        line-height: 1;
        flex: 0 0 auto;
    }
}

.atlas-modal-body {
    flex: 1 1 auto;
    overflow-y: auto;
    overflow-x: hidden;
    padding: var(--space-6);
    background: var(--surface);
    /* Single scroll — children must NOT introduce nested overflow.
     * AC08 is a structural contract. */
}

.atlas-modal-sticky {
    position: sticky;
    bottom: 0;
    left: 0;
    right: 0;
    background: var(--surface-elev);
    border-top: 1px solid var(--border-firm);
    padding: var(--space-3) var(--space-4);
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: var(--space-2);
    /* Materialises on mobile only — desktop relies on the per-card
     * footer primary action. Hidden via `[hidden]` or absence of
     * the markup; never remove this rule. */
    padding-bottom: calc(var(--space-3) + env(safe-area-inset-bottom, 0));
}
@media (min-width: 720px) {
    .atlas-modal-sticky:not([data-force="desktop"]) {
        /* Desktop default: hide unless forced (e.g. PathPlanner
         * planner overlay collision case per design.md §6). */
        display: none;
    }
}

/* ─── atlas-modal mobile (<720 px) ──────────────────────────── */
@media (max-width: 719px) {
    .atlas-modal {
        border-radius: 0;
        max-height: 100vh;
        max-height: 100dvh;
    }
    .atlas-modal-header {
        padding: var(--space-3) var(--space-4);
    }
    .atlas-modal-title {
        font-size: 17px;
    }
    .atlas-modal-body {
        padding: var(--space-4);
    }
}

/* ─── motion budget #1 — atlas-modal entrance (240 ms) ─────── */
@media (prefers-reduced-motion: no-preference) {
    .atlas-modal {
        animation: atlas-modal-in 240ms cubic-bezier(.22, .94, .42, 1) both;
    }
}
@media (prefers-reduced-motion: reduce) {
    .atlas-modal { animation: none; }
}
html.force-reduced-motion .atlas-modal { animation: none; }
@keyframes atlas-modal-in {
    from { opacity: 0; transform: translateY(8px); }
    to   { opacity: 1; transform: translateY(0); }
}

/* ─── shared progress bar (T03 / AC10) ──────────────────────────────
   The single canonical bar component, replaces three legacy
   `_buildBar` impls (army_view_header / army_view_logistics / per-
   view spot helpers). Variants: --hp / --ap / --xp / --capacity.
   --capacity reads `data-tier` to pick the load-ramp colour from the
   existing `--load-*` token table (one source of truth shared with
   `.capacity-meter` and `.army-view-bar--load`).
   See `frontend/static/js/components/progress_bar.js`. */
.atlas-bar {
    display: grid;
    grid-template-columns: minmax(56px, max-content) 1fr auto;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-1) var(--space-2);
    background: var(--surface-elev);
    border: 1px solid var(--border-soft);
    border-radius: var(--radius-sm);
}
.atlas-bar-label {
    font-family: var(--font-display);
    font-size: 12px;
    color: var(--ink-mute);
    letter-spacing: 0.04em;
}
.atlas-bar-meter {
    display: block;
    width: 100%;
    height: 8px;
    background: rgba(235, 227, 210, 0.06);
    border-radius: var(--radius-pill);
    overflow: hidden;
    position: relative;
}
.atlas-bar-fill {
    display: block;
    height: 100%;
    background: var(--accent);
}
@media (prefers-reduced-motion: no-preference) {
    .atlas-bar-fill { transition: width 140ms ease-out; }
}
.atlas-bar-num {
    font-family: var(--font-mono);
    font-variant-numeric: tabular-nums;
    font-size: 12px;
    color: var(--ink);
}
/* Variant fills — semantic atlas tokens, no literal hex. */
.atlas-bar--hp       .atlas-bar-fill { background: var(--vermilion); }
.atlas-bar--ap       .atlas-bar-fill { background: var(--accent); }
.atlas-bar--xp       .atlas-bar-fill { background: var(--lapis); }
.atlas-bar--capacity .atlas-bar-fill { background: var(--load-normal); }
/* morale-100-rework TASK_07 / AC15 — three-zone gradient (red 0-30 /
 * yellow 30-60 / green 60-100). Same approach as the legacy
 * `.unit-view-bar--morale` rule: paint the full gradient on the
 * meter rail (always 100% wide, so the colour stops stay pinned to
 * the absolute morale band), leave the fill rectangle transparent,
 * and dim the un-filled portion via a `::after` overlay anchored at
 * `left: var(--morale-fill-pct)`. The custom property is written by
 * `army_view_header.js::decorateArmyMoraleBar` from the inline
 * `width: N%` `buildBar()` already set on `.atlas-bar-fill`. */
.atlas-bar--morale .atlas-bar-meter {
    background: linear-gradient(
        90deg,
        var(--color-danger) 0%,
        var(--color-danger) 30%,
        var(--color-warning) 30%,
        var(--color-warning) 60%,
        var(--color-success) 60%,
        var(--color-success) 100%
    );
}
.atlas-bar--morale .atlas-bar-fill {
    background: transparent;
}
.atlas-bar--morale .atlas-bar-meter::after {
    content: '';
    position: absolute;
    top: 0;
    bottom: 0;
    left: var(--morale-fill-pct, 0%);
    right: 0;
    background: rgba(0, 0, 0, 0.55);
    pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
    .atlas-bar--morale .atlas-bar-meter::after { transition: left 140ms ease-out; }
}
/* HP rail recolours to patina so the vermilion fill reads against
   a friendly base (mirrors the `--load-ok` semantic). */
.atlas-bar--hp .atlas-bar-meter { background: rgba(91, 122, 82, 0.15); }
/* Capacity load-ramp — `data-tier` from BE drives the colour. */
.atlas-bar--capacity[data-tier="ok"]       .atlas-bar-fill { background: var(--load-ok); }
.atlas-bar--capacity[data-tier="normal"]   .atlas-bar-fill { background: var(--load-normal); }
.atlas-bar--capacity[data-tier="warn"]     .atlas-bar-fill { background: var(--load-warn); }
.atlas-bar--capacity[data-tier="overload"] .atlas-bar-fill { background: var(--load-overload); }
.atlas-bar--capacity[data-tier="block"]    .atlas-bar-fill { background: var(--load-block); }

/* ─── shared inline rename (T03 / AC11) ─────────────────────────────
   Click-to-edit affordance — replaces the four `window.prompt(...)`
   surfaces (ArmyView header / TileView header / ArmiesView roster /
   ClanView). T26 polish enforces zero matches for `window.prompt(`.
   The component lives in `frontend/static/js/components/inline_rename.js`;
   per-view migrations T14 / T18 / T22 / T23 swap consumers onto it. */
.atlas-inline-rename {
    display: inline-flex;
    align-items: center;
    gap: var(--space-1);
}
.atlas-inline-rename-display {
    display: inline-flex;
    align-items: center;
    gap: var(--space-1);
}
.atlas-inline-rename-value {
    font-family: var(--font-display);
    font-weight: 600;
    color: var(--ink);
}
.atlas-inline-rename-edit {
    background: transparent;
    border: 1px solid transparent;
    border-radius: var(--radius-sm);
    padding: 2px 6px;
    font-size: 14px;
    color: var(--ink-mute);
    cursor: pointer;
    line-height: 1;
}
.atlas-inline-rename-edit:hover,
.atlas-inline-rename-edit:focus-visible {
    border-color: var(--border-soft);
    color: var(--ink);
    outline: none;
}
.atlas-inline-rename-form {
    display: inline-flex;
    align-items: center;
    gap: var(--space-1);
}
.atlas-inline-rename-input {
    font-family: var(--font-body);
    font-size: 16px;
    color: var(--ink);
    background: var(--surface-sunken);
    border: 1px solid var(--border-firm);
    border-radius: var(--radius-sm);
    padding: 4px var(--space-2);
    min-width: 12ch;
}
.atlas-inline-rename-input:focus {
    outline: none;
    border-color: var(--accent);
}
.atlas-inline-rename-save,
.atlas-inline-rename-cancel {
    font-family: var(--font-body);
    font-size: 12px;
    border-radius: var(--radius-sm);
    padding: 4px var(--space-2);
    border: 1px solid var(--border-firm);
    cursor: pointer;
}
.atlas-inline-rename-save {
    background: var(--accent);
    color: var(--surface);
    border-color: var(--accent);
}
.atlas-inline-rename-cancel {
    background: transparent;
    color: var(--ink-mute);
}
.atlas-inline-rename-save[disabled],
.atlas-inline-rename-cancel[disabled],
.atlas-inline-rename-input[disabled] {
    opacity: 0.6;
    cursor: not-allowed;
}


/* ─── transfer-picker — TransferPicker modal (TASK_25, AC36/AC37) ─── */
/* Reuses the atlas-modal + atlas-card chrome; rules below cover only
 * the picker-local slots (locked source line, destination select,
 * qty slider, confirm action-card). */
.transfer-picker .transfer-picker-source {
    cursor: default;
    opacity: 0.92;
}
.transfer-picker .transfer-picker-dest {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    margin-top: var(--space-3);
}
.transfer-picker .transfer-picker-label {
    font: var(--font-meta);
    color: var(--ink-mute);
    text-transform: uppercase;
    letter-spacing: 0.04em;
}
.transfer-picker .transfer-picker-select {
    background: var(--surface);
    color: var(--ink);
    border: 1px solid var(--border-firm);
    border-radius: var(--radius-1);
    padding: var(--space-2);
    font: var(--font-body);
}
.transfer-picker .transfer-picker-qty {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    margin-top: var(--space-3);
}
.transfer-picker .transfer-picker-qty-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--space-2);
}
.transfer-picker .transfer-picker-qty-value {
    font-size: 1.1em;
    color: var(--accent);
}
.transfer-picker .transfer-picker-qty-ceiling {
    margin: 0;
    font: var(--font-meta);
    color: var(--ink-mute);
}
.transfer-picker .transfer-picker-slider {
    width: 100%;
}
.transfer-picker .transfer-picker-confirm {
    margin-top: var(--space-3);
    cursor: pointer;
}
.transfer-picker .transfer-picker-confirm[aria-disabled="true"] {
    cursor: not-allowed;
    opacity: 0.6;
}

/* ─── admin-god-mode / TASK_19 — admin chip + body class + canvas watermark ──
   AC04 (chip): `#chip-admin` is the toolbar toggle. Default OFF state
   inherits the .info-chip base; `[data-admin-on="1"]` flips background
   to `--vermilion` with light text so it's visible at-a-glance in any
   panel state. AC29 (body class): `body[data-admin-mode="on"]` paints
   a 2 px inset border on `#workspace + #game-screen` (red — operator
   visual cue) + the canvas `ADMIN` watermark via `#hex-canvas + ::after`
   (bottom-right, fixed-position relative to viewport so panning the
   map doesn't move it). Title prefix `⚡ Oikoumene` is JS-driven in
   `admin_chip.js::render` because `document.title` isn't a CSS sink. */
#chip-admin {
    cursor: pointer;
    user-select: none;
    border: 1px solid transparent;
    transition: background 0.12s ease, color 0.12s ease, border-color 0.12s ease;
}
#chip-admin[data-admin-on="0"] {
    color: var(--text-dim);
    background: rgba(255, 255, 255, 0.04);
}
#chip-admin[data-admin-on="1"] {
    color: #fff;
    background: var(--vermilion);
    border-color: rgba(255, 255, 255, 0.18);
    font-weight: 600;
    /* Subtle pulse to keep the operator aware that destructive
       affordances are armed. CSS-only; reduced-motion override at the
       bottom of this file disables the keyframes. */
    animation: hw-admin-chip-pulse 2.4s ease-in-out infinite;
}
@keyframes hw-admin-chip-pulse {
    0%, 100% { box-shadow: 0 0 0 0 rgba(168, 68, 44, 0.55); }
    50%      { box-shadow: 0 0 0 4px rgba(168, 68, 44, 0.0); }
}
@media (prefers-reduced-motion: reduce) {
    #chip-admin[data-admin-on="1"] { animation: none; }
}
html.force-reduced-motion #chip-admin[data-admin-on="1"] { animation: none; }

/* AC29 — 2 px inset border on the active workspace surface(s). Uses
   `outline` (inset) instead of `border` so the layout doesn't shift
   when admin mode flips on. Both `#game-screen` (the regular
   gameplay shell) and `#workspace` (the fullscreen TileView /
   UnitView / ArmyView overlay introduced in ui-fullscreen-tile-detail
   TASK_06) carry the same overlay so the operator never loses the
   visual context cue when navigating into a fullscreen view. */
body[data-admin-mode="on"] #game-screen,
body[data-admin-mode="on"] #workspace {
    outline: 2px inset var(--vermilion);
    outline-offset: -2px;
}

/* AC29 canvas watermark — `ADMIN` text affixed bottom-right of the
   viewport when admin mode is on. Anchored to `body` (not `#hex-
   canvas`) so it stays put regardless of canvas pan/zoom. Fixed
   positioning relative to the viewport keeps it visible even when a
   panel covers the canvas. `pointer-events: none` so it can't
   intercept clicks. */
body[data-admin-mode="on"]::after {
    content: 'ADMIN';
    position: fixed;
    right: 12px;
    bottom: 12px;
    z-index: 50;
    font: 600 22px/1 var(--font-display, 'EB Garamond', serif);
    letter-spacing: 0.18em;
    color: var(--vermilion);
    text-shadow: 0 0 8px rgba(0, 0, 0, 0.55);
    opacity: 0.55;
    pointer-events: none;
    user-select: none;
}

/* ─── admin-god-mode / TASK_21 — admin sidebar (right drawer) ────────
   AC30: right-side drawer with 4 tab containers (World inspect /
   Player inspect / Tick controls / Audit log) — lazy-loaded from
   `frontend/static/js/admin/sidebar.js` via the bundle bootstrap
   in `admin/index.js`. The drawer is built and appended to <body>
   on install so it floats above panels + canvas + the workspace
   overlay; visibility is keyed entirely on
   `body[data-admin-sidebar="open"]` so the JS only flips one
   attribute. Mobile breakpoint (TASK_29 / AC41) collapses to a
   bottom-sheet — the desktop rules below stop at the existing
   `≤ 639px` mobile breakpoint elsewhere in this file; the bottom-
   sheet pass lands as a sibling block in TASK_29. */
.admin-sidebar {
    position: fixed;
    top: var(--toolbar-h, 56px);
    right: 0;
    bottom: 0;
    width: min(420px, 92vw);
    z-index: 60;
    display: flex;
    flex-direction: column;
    background: var(--bg-panel, #1c1611);
    color: var(--text, #e9e3d5);
    border-left: 2px solid var(--vermilion);
    box-shadow: -4px 0 18px rgba(0, 0, 0, 0.45);
    transform: translateX(100%);
    transition: transform 0.22s cubic-bezier(.22, .94, .42, 1);
    /* The drawer is invisible but in the DOM until the JS install
       runs. `visibility: hidden` on closed prevents tab focus from
       falling into off-screen tab buttons before the operator has
       opened the drawer; flips back to `visible` on open. */
    visibility: hidden;
}
body[data-admin-sidebar="open"] .admin-sidebar {
    transform: translateX(0);
    visibility: visible;
}
@media (prefers-reduced-motion: reduce) {
    .admin-sidebar { transition: none; }
}
html.force-reduced-motion .admin-sidebar { transition: none; }

.admin-sidebar-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 12px 16px;
    border-bottom: 1px solid rgba(168, 68, 44, 0.32);
    background: rgba(168, 68, 44, 0.08);
}
.admin-sidebar-title {
    font: 600 16px/1 var(--font-display, 'EB Garamond', serif);
    letter-spacing: 0.08em;
    color: var(--vermilion);
    text-transform: uppercase;
}
.admin-sidebar-close {
    background: transparent;
    color: var(--text-dim, #a59c8a);
    border: 1px solid transparent;
    border-radius: 4px;
    padding: 4px 8px;
    font-size: 14px;
    cursor: pointer;
    min-width: 32px;
    min-height: 32px;
}
.admin-sidebar-close:hover,
.admin-sidebar-close:focus {
    color: #fff;
    border-color: var(--vermilion);
    outline: none;
}

.admin-sidebar-tablist {
    display: flex;
    gap: 4px;
    padding: 8px 12px;
    border-bottom: 1px solid rgba(255, 255, 255, 0.06);
    overflow-x: auto;
}
.admin-sidebar-tab {
    flex: 1 1 auto;
    min-width: 64px;
    padding: 8px 10px;
    background: transparent;
    color: var(--text-dim, #a59c8a);
    border: 1px solid transparent;
    border-radius: 4px;
    font-size: 13px;
    cursor: pointer;
    white-space: nowrap;
}
.admin-sidebar-tab:hover { color: var(--text, #e9e3d5); }
.admin-sidebar-tab[aria-selected="true"] {
    color: #fff;
    background: rgba(168, 68, 44, 0.18);
    border-color: var(--vermilion);
    font-weight: 600;
}

.admin-sidebar-body {
    flex: 1 1 auto;
    overflow-y: auto;
    padding: 12px 16px;
}
.admin-sidebar-panel {
    /* Each tab panel hosts its lazy-painted body. The shell sets
       `hidden` on inactive panels; visibility flips via the
       `[hidden]` attribute alone — no CSS rule needed since the UA
       default already hides `[hidden]` elements. */
    min-height: 80px;
}

@media (max-width: 639px) {
    /* Mobile bottom-sheet — TASK_29 / AC41. Sidebar collapses to a
       half-height bottom-sheet so the canvas stays partially
       visible behind it. Slide animation flips from horizontal
       (translateX) to vertical (translateY) so the off-screen
       resting state matches the bottom edge. The body+lite-flag
       qualifier scopes the rule to admin-mode-on viewports so a
       desktop tablet at 540px (rare) still gets the right-edge
       drawer; phones with the lite gate firing get the sheet. */
    body[data-admin-mobile-lite="on"] .admin-sidebar {
        top: auto;
        right: 0;
        left: 0;
        bottom: 0;
        width: 100vw;
        max-width: 100vw;
        max-height: 70vh;
        border-left: none;
        border-top: 2px solid var(--vermilion);
        box-shadow: 0 -4px 18px rgba(0, 0, 0, 0.45);
        transform: translateY(100%);
    }
    body[data-admin-mobile-lite="on"][data-admin-sidebar="open"] .admin-sidebar {
        transform: translateY(0);
    }
    /* Tablet (640..1023) + non-lite phones (rare) keep the legacy
       full-width drawer to avoid layout regressions on devices that
       don't match the lite gate. */
    .admin-sidebar {
        top: 0;
        width: 100vw;
        max-width: 100vw;
        border-left: none;
    }
}


/* ─── Admin command palette (TASK_22 / AC31) ────────────────────
   Ctrl+K fuzzy-filter modal listing the AC31 seed verbs (spawn
   unit / edit tile / jump 100 ticks / kill army / set resource /
   snapshot world) plus the four sidebar-tab dispatchers (audit /
   world / player / tick controls). The palette is a centred
   fixed-position overlay appended to <body>; visibility is keyed
   entirely on `body[data-admin-palette="open"]` so the JS only
   flips one attribute (mirrors the sidebar's contract). The
   palette stack-orders ABOVE the sidebar (z-index 70 > sidebar
   60) so an operator who pops the palette while the sidebar is
   open sees the palette card on top. Mobile breakpoint (TASK_29
   / AC41) collapses to a bottom-sheet — the desktop rules below
   stop at the existing `≤ 639px` mobile breakpoint. */
.admin-command-palette {
    position: fixed;
    inset: 0;
    z-index: 70;
    display: flex;
    align-items: flex-start;
    justify-content: center;
    padding-top: 12vh;
    background: rgba(0, 0, 0, 0.5);
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.16s ease-out;
}
.admin-command-palette[hidden] {
    /* The UA's `[hidden]` default already sets display:none; the
       JS flips the attribute on open/close so we paint nothing
       in the closed state. */
    display: none;
}
body[data-admin-palette="open"] .admin-command-palette {
    opacity: 1;
    pointer-events: auto;
}
@media (prefers-reduced-motion: reduce) {
    .admin-command-palette { transition: none; }
}
html.force-reduced-motion .admin-command-palette { transition: none; }

.admin-command-palette-inner {
    width: min(560px, 92vw);
    max-height: 60vh;
    display: flex;
    flex-direction: column;
    background: var(--bg-panel, #1c1611);
    color: var(--text, #e9e3d5);
    border: 1px solid var(--vermilion);
    border-radius: var(--radius-md);
    box-shadow: 0 18px 48px rgba(0, 0, 0, 0.55);
    overflow: hidden;
}
.admin-command-palette-input-wrap {
    padding: 10px 12px;
    border-bottom: 1px solid rgba(168, 68, 44, 0.32);
    background: rgba(168, 68, 44, 0.08);
}
.admin-command-palette-input {
    width: 100%;
    padding: 10px 12px;
    background: rgba(0, 0, 0, 0.32);
    color: var(--text, #e9e3d5);
    border: 1px solid rgba(255, 255, 255, 0.12);
    border-radius: 4px;
    font-size: 16px;     /* iOS focus-zoom suppression */
    line-height: 1.2;
    outline: none;
}
.admin-command-palette-input:focus {
    border-color: var(--vermilion);
    box-shadow: 0 0 0 2px rgba(168, 68, 44, 0.32);
}

.admin-command-palette-list {
    list-style: none;
    margin: 0;
    padding: 6px 0;
    overflow-y: auto;
    flex: 1 1 auto;
}
.admin-command-palette-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    padding: 10px 16px;
    cursor: pointer;
    border-left: 3px solid transparent;
    color: var(--text-dim, #a59c8a);
}
.admin-command-palette-row:hover {
    background: rgba(255, 255, 255, 0.04);
    color: var(--text, #e9e3d5);
}
.admin-command-palette-row[aria-selected="true"] {
    background: rgba(168, 68, 44, 0.18);
    border-left-color: var(--vermilion);
    color: #fff;
}
.admin-command-palette-label {
    font-size: 14px;
    font-weight: 500;
}
.admin-command-palette-verb {
    font-size: 11px;
    color: var(--text-dim, #a59c8a);
    font-family: var(--font-mono, 'JetBrains Mono', monospace);
    letter-spacing: 0.02em;
    opacity: 0.8;
}
.admin-command-palette-row[aria-selected="true"] .admin-command-palette-verb {
    color: var(--vermilion);
    opacity: 1;
}

.admin-command-palette-empty {
    padding: 24px 16px;
    text-align: center;
    color: var(--text-dim, #a59c8a);
    font-style: italic;
}

@media (max-width: 639px) {
    /* Mobile shell — full-width palette overlay until TASK_29
       lands the bottom-sheet collapse. Keeping the desktop rules
       above for any tablet viewport (640..1023). */
    .admin-command-palette {
        padding-top: 0;
        align-items: stretch;
    }
    .admin-command-palette-inner {
        width: 100vw;
        max-width: 100vw;
        max-height: 100vh;
        border-radius: 0;
        border-left: none;
        border-right: none;
    }
}

/* ─── Admin inline-edit pencil (admin-god-mode TASK_23 / AC32) ─── */
/* Pencil affordance painted next to every admin-editable field
   (resource chip, unit name, tile terrain) when admin mode is ON.
   Hidden by default + revealed only when the chip flips
   `body[data-admin-mode="on"]` so non-admin sessions never see the
   icon even if a stale `data-admin-edit-kind` annotation slipped
   through. The button keys off `--vermilion` so it reads as admin
   chrome at-a-glance like the chip + sidebar header + palette
   border. */
.admin-inline-edit-pencil {
    display: none;
    margin-left: 4px;
    padding: 0 4px;
    border: 1px solid transparent;
    background: transparent;
    color: var(--vermilion);
    font-size: 0.85em;
    line-height: 1;
    cursor: pointer;
    border-radius: 3px;
    vertical-align: middle;
    transition: background-color 120ms ease, border-color 120ms ease;
}

body[data-admin-mode="on"] .admin-inline-edit-pencil {
    display: inline-block;
}

.admin-inline-edit-pencil:hover,
.admin-inline-edit-pencil:focus-visible {
    background: rgba(168, 68, 44, 0.12);
    border-color: var(--vermilion);
    outline: none;
}

@media (prefers-reduced-motion: reduce) {
    .admin-inline-edit-pencil {
        transition: none;
    }
}

html.force-reduced-motion .admin-inline-edit-pencil {
    transition: none;
}

/* Mobile breakpoint stub — TASK_29 will swap the inline pencil
   for a long-press affordance per AC41. Until then we widen the
   tap target so finger-press hits land cleanly.
   mobile-ux-rework TASK_03 — `--tap-mobile` (48 px) replaces the
   legacy `--tap-sm` (40 px) per AC01 — the inline pencil clears
   the WCAG AAA + iOS HIG touch-target floor on phones. */
@media (max-width: 639px) {
    .admin-inline-edit-pencil {
        padding: 4px 8px;
        font-size: 1em;
        min-width: var(--tap-mobile, 48px);
    }
}

/* ─── Admin right-click context menu (admin-god-mode TASK_24 / AC33) ─── */
/* Floating popover painted next to the click coordinates when the
   operator right-clicks an annotated host (canvas tile / army row /
   unit row / tile row) while admin mode is ON. Hidden by default +
   only mounted while open via the JS-set `hidden` attribute; the
   `body[data-admin-context-open="open"]` body-attr is a class hook
   for any sibling chrome that wants to fade with the menu. The menu
   stack-orders ABOVE the palette (z-index 80 > palette 70 > sidebar
   60) so an operator who pops the menu while the palette is open
   sees the menu card on top. */
.admin-context-menu {
    position: fixed;
    z-index: 80;
    min-width: 220px;
    max-width: min(320px, 92vw);
    max-height: 60vh;
    overflow-y: auto;
    background: var(--bg-panel, #1c1611);
    color: var(--text, #e9e3d5);
    border: 1px solid var(--vermilion);
    border-radius: var(--radius-md);
    box-shadow: 0 18px 48px rgba(0, 0, 0, 0.55);
    padding: 6px 0;
    opacity: 1;
    transition: opacity 0.12s ease-out;
}
.admin-context-menu[hidden] {
    /* The UA's `[hidden]` default already sets display:none; the
       JS flips the attribute on open/close so we paint nothing
       in the closed state. */
    display: none;
}
@media (prefers-reduced-motion: reduce) {
    .admin-context-menu { transition: none; }
}
html.force-reduced-motion .admin-context-menu { transition: none; }

.admin-context-menu-list {
    list-style: none;
    margin: 0;
    padding: 0;
}
.admin-context-menu-row {
    display: block;
    width: 100%;
    text-align: left;
    padding: 8px 14px;
    background: transparent;
    border: none;
    border-left: 3px solid transparent;
    color: var(--text-dim, #a59c8a);
    cursor: pointer;
    font-size: 14px;
    line-height: 1.3;
    font-family: inherit;
}
.admin-context-menu-row:hover,
.admin-context-menu-row:focus-visible {
    background: rgba(168, 68, 44, 0.18);
    border-left-color: var(--vermilion);
    color: #fff;
    outline: none;
}

@media (max-width: 639px) {
    /* Mobile breakpoint stub — TASK_29 will swap the right-click
       popover for a long-press bottom-sheet per AC41. Until then
       we widen the menu so finger-press hits land cleanly. */
    .admin-context-menu {
        min-width: min(280px, 92vw);
    }
    .admin-context-menu-row {
        padding: 12px 16px;
        min-height: var(--tap, 44px);
    }
}


/* ─── Admin type-to-confirm + dry-run modal (TASK_28 / AC34 + AC35)
   Single confirm-and-dispatch surface for every admin verb the
   palette / inline-edit / context-menu / audit-log undo / tick-
   controls dispatch via `hw:admin-command`. Modal mounts as a
   fixed-position overlay appended to <body>; visibility is keyed
   on `body[data-admin-confirm="open"]` so the JS only flips one
   attribute (mirrors the palette + sidebar contract). The modal
   stack-orders ABOVE the palette (z-index 90 > palette 70) so a
   palette → confirm flow stacks correctly. Risk-tier accents are
   driven by `[data-admin-confirm-risk="…"]` on root + risk pill.
   Mobile breakpoint (TASK_29 / AC42) collapses the modal to a
   full-screen sheet on `< 640px`. */
.admin-confirm-modal {
    position: fixed;
    inset: 0;
    z-index: 90;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 24px;
    background: rgba(0, 0, 0, 0.6);
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.16s ease-out;
}
.admin-confirm-modal[hidden] {
    display: none;
}
body[data-admin-confirm="open"] .admin-confirm-modal {
    opacity: 1;
    pointer-events: auto;
}
@media (prefers-reduced-motion: reduce) {
    .admin-confirm-modal { transition: none; }
}
html.force-reduced-motion .admin-confirm-modal { transition: none; }

.admin-confirm-modal-inner {
    width: min(520px, 92vw);
    max-height: 80vh;
    display: flex;
    flex-direction: column;
    gap: 12px;
    padding: 18px 20px;
    background: var(--bg-panel, #1c1611);
    color: var(--text, #e9e3d5);
    border: 1px solid var(--vermilion);
    border-radius: var(--radius-md);
    box-shadow: 0 18px 48px rgba(0, 0, 0, 0.55);
    overflow: auto;
}
.admin-confirm-modal[data-admin-confirm-risk="critical"] .admin-confirm-modal-inner,
.admin-confirm-modal[data-admin-confirm-risk="high"] .admin-confirm-modal-inner {
    border-width: 2px;
}

.admin-confirm-modal-header {
    display: flex;
    align-items: baseline;
    gap: 12px;
    border-bottom: 1px solid rgba(255, 255, 255, 0.08);
    padding-bottom: 8px;
}
.admin-confirm-modal-title {
    margin: 0;
    flex: 1;
    font-size: 16px;
    font-family: inherit;
    color: var(--text, #e9e3d5);
}
.admin-confirm-modal-risk {
    padding: 2px 8px;
    border-radius: 4px;
    font-size: 11px;
    letter-spacing: 0.08em;
    font-weight: bold;
    border: 1px solid var(--text-dim, #a59c8a);
    color: var(--text-dim, #a59c8a);
}
.admin-confirm-modal-risk[data-admin-confirm-risk="medium"] {
    color: #d3a13b;
    border-color: #d3a13b;
}
.admin-confirm-modal-risk[data-admin-confirm-risk="high"],
.admin-confirm-modal-risk[data-admin-confirm-risk="critical"] {
    color: var(--vermilion);
    border-color: var(--vermilion);
}

.admin-confirm-modal-banner {
    margin: 0;
    padding: 8px 10px;
    background: rgba(168, 68, 44, 0.18);
    border: 1px solid var(--vermilion);
    border-radius: 4px;
    color: var(--vermilion);
    font-size: 13px;
}

.admin-confirm-modal-params {
    font-size: 13px;
}
.admin-confirm-modal-params-empty {
    margin: 0;
    color: var(--text-dim, #a59c8a);
    font-style: italic;
}
.admin-confirm-modal-params-list {
    display: grid;
    grid-template-columns: max-content 1fr;
    gap: 4px 12px;
    margin: 0;
    padding: 8px 10px;
    background: rgba(0, 0, 0, 0.24);
    border-radius: 4px;
}
.admin-confirm-modal-params-list dt {
    color: var(--text-dim, #a59c8a);
    font-family: var(--font-mono, monospace);
    font-size: 12px;
}
.admin-confirm-modal-params-list dd {
    margin: 0;
    color: var(--text, #e9e3d5);
    word-break: break-word;
}

.admin-confirm-modal-dry-run {
    display: flex;
    flex-direction: column;
    gap: 6px;
}
.admin-confirm-modal-dry-run-button {
    align-self: flex-start;
    padding: 6px 12px;
    background: transparent;
    color: var(--text-dim, #a59c8a);
    border: 1px solid var(--text-dim, #a59c8a);
    border-radius: 4px;
    cursor: pointer;
    font-family: inherit;
    font-size: 13px;
    min-height: var(--tap-sm, 36px);
}
.admin-confirm-modal-dry-run-button:hover,
.admin-confirm-modal-dry-run-button:focus-visible {
    color: var(--text, #e9e3d5);
    border-color: var(--text, #e9e3d5);
    outline: none;
}
.admin-confirm-modal-dry-run-button:disabled {
    opacity: 0.5;
    cursor: not-allowed;
}
.admin-confirm-modal-dry-run-output {
    margin: 0;
    padding: 8px 10px;
    background: rgba(0, 0, 0, 0.36);
    border: 1px solid rgba(255, 255, 255, 0.08);
    border-radius: 4px;
    color: var(--text, #e9e3d5);
    font-family: var(--font-mono, monospace);
    font-size: 12px;
    max-height: 200px;
    overflow: auto;
    white-space: pre-wrap;
    word-break: break-word;
}
.admin-confirm-modal-dry-run-output[data-admin-confirm-dry-run-status="error"] {
    color: var(--vermilion);
    border-color: var(--vermilion);
}

.admin-confirm-modal-typed {
    display: flex;
    flex-direction: column;
    gap: 4px;
}
.admin-confirm-modal-typed-label {
    font-size: 12px;
    color: var(--text-dim, #a59c8a);
}
.admin-confirm-modal-typed-input {
    background: rgba(0, 0, 0, 0.36);
    color: var(--text, #e9e3d5);
    border: 1px solid var(--vermilion);
    border-radius: 4px;
    padding: 8px 10px;
    font-size: 16px;
    font-family: var(--font-mono, monospace);
}
.admin-confirm-modal-typed-input:focus {
    outline: 2px solid var(--vermilion);
    outline-offset: -1px;
}

.admin-confirm-modal-result {
    margin: 0;
    padding: 6px 10px;
    border-radius: 4px;
    background: rgba(0, 0, 0, 0.24);
    color: var(--text, #e9e3d5);
    font-size: 13px;
}
.admin-confirm-modal-result[data-admin-confirm-result="error"] {
    color: var(--vermilion);
    background: rgba(168, 68, 44, 0.12);
}

.admin-confirm-modal-buttons {
    display: flex;
    justify-content: flex-end;
    gap: 8px;
    border-top: 1px solid rgba(255, 255, 255, 0.08);
    padding-top: 8px;
}
.admin-confirm-modal-cancel,
.admin-confirm-modal-confirm {
    padding: 8px 16px;
    border-radius: 4px;
    cursor: pointer;
    font-family: inherit;
    font-size: 14px;
    min-height: var(--tap-sm, 36px);
}
.admin-confirm-modal-cancel {
    background: transparent;
    color: var(--text-dim, #a59c8a);
    border: 1px solid var(--text-dim, #a59c8a);
}
.admin-confirm-modal-cancel:hover,
.admin-confirm-modal-cancel:focus-visible {
    color: var(--text, #e9e3d5);
    border-color: var(--text, #e9e3d5);
    outline: none;
}
.admin-confirm-modal-confirm {
    background: var(--vermilion);
    color: #fff;
    border: 1px solid var(--vermilion);
}
.admin-confirm-modal-confirm:hover:not(:disabled),
.admin-confirm-modal-confirm:focus-visible:not(:disabled) {
    background: rgba(168, 68, 44, 0.85);
    outline: none;
}
.admin-confirm-modal-confirm:disabled {
    opacity: 0.5;
    cursor: not-allowed;
}

@media (max-width: 639px) {
    /* Mobile breakpoint stub — TASK_29 / AC42 routes the modal to
       a full-screen sheet on phones; the dense form + typed-confirm
       input + dry-run preview block don't survive a phone width. */
    .admin-confirm-modal { padding: 0; }
    .admin-confirm-modal-inner {
        width: 100vw;
        max-height: 100vh;
        border-radius: 0;
        border-width: 0;
    }
    .admin-confirm-modal-cancel,
    .admin-confirm-modal-confirm {
        min-height: var(--tap, 44px);
    }
}


/* ─── Admin audit log viewer (TASK_25 / AC30 audit slot) ────────
   Sidebar-tab body painted by `frontend/static/js/admin/
   audit_log_viewer.js`. Mounts inside the AC30 sidebar's
   `[data-admin-tab-panel="audit"]` host that the TASK_21 shell
   stamps; the painter writes a `.admin-audit-log` root, a filter
   bar, a status line, the row list, and a `Load more` pagination
   button. Reads ActionLog rows via `GET /api/admin/world/<wid>/
   audit-log/` (the BE endpoint shipped alongside this module);
   the row list newest-first, paginated by id-cursor.

   Mobile breakpoint (TASK_29 / AC42) routes the audit viewer to
   an "Open on desktop" card on `< 640px` viewports because the
   filter chip row + dense rows don't survive a phone width; the
   media query stub at the bottom is what TASK_29 will flesh out
   into the desktop-deep-link card. */

.admin-audit-log {
    display: flex;
    flex-direction: column;
    gap: 10px;
    font-size: 13px;
}
.admin-audit-log-filters {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    padding: 8px;
    border: 1px solid rgba(255, 255, 255, 0.06);
    border-radius: var(--radius-md);
    background: rgba(0, 0, 0, 0.18);
}
.admin-audit-log-filter {
    display: flex;
    flex-direction: column;
    gap: 2px;
    flex: 1 1 110px;
    min-width: 110px;
}
.admin-audit-log-filter-label {
    font-size: 10px;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    color: var(--text-dim, #a59c8a);
}
.admin-audit-log-filter-select {
    background: rgba(0, 0, 0, 0.36);
    color: var(--text, #e9e3d5);
    border: 1px solid rgba(168, 68, 44, 0.32);
    border-radius: 4px;
    padding: 6px 8px;
    font-size: 13px;
    font-family: inherit;
}
.admin-audit-log-filter-select:focus {
    outline: 2px solid var(--vermilion);
    outline-offset: -1px;
    border-color: var(--vermilion);
}
.admin-audit-log-refresh {
    align-self: flex-end;
    padding: 6px 12px;
    background: transparent;
    color: var(--text-dim, #a59c8a);
    border: 1px solid var(--vermilion);
    border-radius: 4px;
    cursor: pointer;
    font-family: inherit;
    font-size: 13px;
    min-height: var(--tap-sm, 36px);
}
.admin-audit-log-refresh:hover,
.admin-audit-log-refresh:focus-visible {
    color: #fff;
    background: rgba(168, 68, 44, 0.18);
    outline: none;
}

.admin-audit-log-status {
    color: var(--text-dim, #a59c8a);
    font-size: 12px;
    padding: 2px 4px;
}
.admin-audit-log-status[data-audit-status="error"] {
    color: var(--vermilion);
}

.admin-audit-log-list {
    display: flex;
    flex-direction: column;
    gap: 6px;
}
.admin-audit-log-row {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 8px;
    padding: 8px 10px;
    border: 1px solid rgba(255, 255, 255, 0.06);
    border-left: 3px solid transparent;
    border-radius: 4px;
    background: rgba(0, 0, 0, 0.18);
    line-height: 1.3;
}
.admin-audit-log-row[data-audit-risk="HIGH"] {
    border-left-color: var(--vermilion);
}
.admin-audit-log-row[data-audit-risk="CRITICAL"] {
    border-left-color: var(--vermilion);
    background: rgba(168, 68, 44, 0.12);
}

.admin-audit-log-tick {
    font-family: var(--font-mono, 'JetBrains Mono', monospace);
    font-size: 11px;
    color: var(--text-dim, #a59c8a);
    background: rgba(0, 0, 0, 0.36);
    padding: 2px 6px;
    border-radius: 3px;
}
.admin-audit-log-time {
    font-family: var(--font-mono, 'JetBrains Mono', monospace);
    font-size: 11px;
    color: var(--text-dim, #a59c8a);
}
.admin-audit-log-risk {
    font-size: 10px;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    padding: 2px 6px;
    border-radius: 3px;
    border: 1px solid currentColor;
    color: var(--text-dim, #a59c8a);
}
.admin-audit-log-risk[data-risk="MEDIUM"] {
    color: #d3a13b;
}
.admin-audit-log-risk[data-risk="HIGH"],
.admin-audit-log-risk[data-risk="CRITICAL"] {
    color: var(--vermilion);
    background: rgba(168, 68, 44, 0.14);
}

.admin-audit-log-verb {
    font-family: var(--font-mono, 'JetBrains Mono', monospace);
    font-size: 12px;
    color: var(--text, #e9e3d5);
    background: transparent;
    border: 1px solid transparent;
    padding: 2px 6px;
    border-radius: 3px;
    cursor: pointer;
}
.admin-audit-log-verb:hover,
.admin-audit-log-verb:focus-visible {
    border-color: var(--vermilion);
    color: #fff;
    outline: none;
}
.admin-audit-log-actor {
    font-size: 12px;
    color: var(--text, #e9e3d5);
}
.admin-audit-log-ip {
    font-family: var(--font-mono, 'JetBrains Mono', monospace);
    font-size: 11px;
    color: var(--text-dim, #a59c8a);
}
.admin-audit-log-summary {
    font-family: var(--font-mono, 'JetBrains Mono', monospace);
    font-size: 11px;
    color: var(--text-dim, #a59c8a);
    flex: 1 1 auto;
    word-break: break-all;
}
.admin-audit-log-undo {
    margin-left: auto;
    padding: 4px 10px;
    background: transparent;
    color: var(--vermilion);
    border: 1px solid var(--vermilion);
    border-radius: 3px;
    cursor: pointer;
    font-family: inherit;
    font-size: 12px;
    min-height: var(--tap-sm, 36px);
}
.admin-audit-log-undo:hover,
.admin-audit-log-undo:focus-visible {
    background: rgba(168, 68, 44, 0.18);
    color: #fff;
    outline: none;
}

.admin-audit-log-load-more {
    align-self: center;
    padding: 6px 18px;
    background: transparent;
    color: var(--text-dim, #a59c8a);
    border: 1px dashed var(--vermilion);
    border-radius: 4px;
    cursor: pointer;
    font-family: inherit;
    font-size: 13px;
    margin-top: 4px;
    min-height: var(--tap-sm, 36px);
}
.admin-audit-log-load-more:hover,
.admin-audit-log-load-more:focus-visible {
    color: #fff;
    background: rgba(168, 68, 44, 0.10);
    border-style: solid;
    outline: none;
}

@media (prefers-reduced-motion: reduce) {
    .admin-audit-log-row,
    .admin-audit-log-verb,
    .admin-audit-log-undo,
    .admin-audit-log-refresh,
    .admin-audit-log-load-more {
        transition: none;
    }
}
html.force-reduced-motion .admin-audit-log-row,
html.force-reduced-motion .admin-audit-log-verb,
html.force-reduced-motion .admin-audit-log-undo,
html.force-reduced-motion .admin-audit-log-refresh,
html.force-reduced-motion .admin-audit-log-load-more {
    transition: none;
}

@media (max-width: 639px) {
    /* Mobile breakpoint stub — TASK_29 / AC42 routes the audit
       viewer to an "Open on desktop" card on phone widths. Until
       then we collapse the filter row to one chip per line + bump
       row padding for finger-press hits. */
    .admin-audit-log-filters {
        flex-direction: column;
    }
    .admin-audit-log-filter {
        flex: 1 1 auto;
    }
    .admin-audit-log-row {
        padding: 10px 12px;
        min-height: var(--tap, 44px);
    }
}

/* ─── Admin tick controls (TASK_26 / AC30 tick-controls slot) ───
   Sidebar-tab body painted by `frontend/static/js/admin/
   tick_controls.js`. Mounts inside the AC30 sidebar's
   `[data-admin-tab-panel="tick"]` host that the TASK_21 shell
   stamps; the painter writes a `.admin-tick-controls` root, a
   state row (tick chip + paused pill + processing chip), a
   buttons row (Pause / Unpause / Step), a jump row (n input +
   confirm input + Jump button), and a result line.

   Mobile breakpoint (TASK_29 / AC42) routes the tick-controls
   tab to an "Open on desktop" card on `< 640px` viewports
   because the dense button row + jump-N input doesn't survive
   a phone width; the media query stub at the bottom is what
   TASK_29 will flesh out. */

.admin-tick-controls {
    display: flex;
    flex-direction: column;
    gap: 12px;
    font-size: 13px;
}

.admin-tick-controls-state {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 8px;
    padding: 8px 10px;
    border: 1px solid rgba(255, 255, 255, 0.06);
    border-radius: var(--radius-md);
    background: rgba(0, 0, 0, 0.18);
}

.admin-tick-controls-tick {
    font-family: var(--font-mono, 'JetBrains Mono', monospace);
    font-size: 13px;
    color: var(--text, #e9e3d5);
    background: rgba(0, 0, 0, 0.36);
    padding: 3px 8px;
    border-radius: 3px;
}

.admin-tick-controls-paused {
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    padding: 3px 8px;
    border-radius: 3px;
    border: 1px solid currentColor;
    color: var(--text-dim, #a59c8a);
}
.admin-tick-controls-paused[data-tc-paused="true"] {
    color: var(--vermilion);
    background: rgba(168, 68, 44, 0.14);
}
.admin-tick-controls-paused[data-tc-paused="false"] {
    color: #6da06d;
}

.admin-tick-controls-processing {
    font-family: var(--font-mono, 'JetBrains Mono', monospace);
    font-size: 11px;
    color: #d3a13b;
    border: 1px solid #d3a13b;
    padding: 2px 6px;
    border-radius: 3px;
}

.admin-tick-controls-buttons {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
}

.admin-tick-controls-btn {
    padding: 6px 14px;
    background: transparent;
    color: var(--text, #e9e3d5);
    border: 1px solid var(--vermilion);
    border-radius: 4px;
    cursor: pointer;
    font-family: inherit;
    font-size: 13px;
    min-height: var(--tap-sm, 36px);
}
.admin-tick-controls-btn:hover:not(:disabled),
.admin-tick-controls-btn:focus-visible:not(:disabled) {
    color: #fff;
    background: rgba(168, 68, 44, 0.18);
    outline: none;
}
.admin-tick-controls-btn:disabled {
    opacity: 0.4;
    cursor: not-allowed;
    border-color: rgba(168, 68, 44, 0.32);
}

.admin-tick-controls-jump {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 8px;
    padding: 8px 10px;
    border: 1px solid rgba(255, 255, 255, 0.06);
    border-radius: var(--radius-md);
    background: rgba(0, 0, 0, 0.10);
}

.admin-tick-controls-jump-label,
.admin-tick-controls-jump-confirm-label {
    display: flex;
    align-items: center;
    gap: 6px;
    font-size: 12px;
    color: var(--text-dim, #a59c8a);
}

.admin-tick-controls-jump-n {
    width: 80px;
    background: rgba(0, 0, 0, 0.36);
    color: var(--text, #e9e3d5);
    border: 1px solid rgba(168, 68, 44, 0.32);
    border-radius: 4px;
    padding: 6px 8px;
    font-size: 13px;
    font-family: inherit;
}
.admin-tick-controls-jump-n:focus {
    outline: 2px solid var(--vermilion);
    outline-offset: -1px;
    border-color: var(--vermilion);
}

.admin-tick-controls-jump-confirm {
    width: 100px;
    background: rgba(0, 0, 0, 0.36);
    color: var(--vermilion);
    border: 1px solid var(--vermilion);
    border-radius: 4px;
    padding: 6px 8px;
    font-size: 13px;
    font-family: var(--font-mono, 'JetBrains Mono', monospace);
    text-transform: uppercase;
}
.admin-tick-controls-jump-confirm:focus {
    outline: 2px solid var(--vermilion);
    outline-offset: -1px;
}

.admin-tick-controls-result {
    font-size: 12px;
    color: var(--text-dim, #a59c8a);
    padding: 6px 10px;
    border: 1px solid rgba(255, 255, 255, 0.06);
    border-radius: 4px;
    background: rgba(0, 0, 0, 0.10);
}
.admin-tick-controls-result[data-tc-result="error"] {
    color: var(--vermilion);
    border-color: var(--vermilion);
    background: rgba(168, 68, 44, 0.10);
}
.admin-tick-controls-result[data-tc-result="ok"] {
    color: #6da06d;
    border-color: rgba(109, 160, 109, 0.36);
}

@media (prefers-reduced-motion: reduce) {
    .admin-tick-controls-btn,
    .admin-tick-controls-jump-n,
    .admin-tick-controls-jump-confirm {
        transition: none;
    }
}
html.force-reduced-motion .admin-tick-controls-btn,
html.force-reduced-motion .admin-tick-controls-jump-n,
html.force-reduced-motion .admin-tick-controls-jump-confirm {
    transition: none;
}

@media (max-width: 639px) {
    /* Mobile breakpoint stub — TASK_29 / AC42 routes the
       tick-controls tab to an "Open on desktop" card on phone
       widths. Until then we collapse the buttons + jump rows to
       one item per line + bump tap-target heights so finger
       presses land cleanly. */
    .admin-tick-controls-buttons {
        flex-direction: column;
    }
    .admin-tick-controls-btn {
        min-height: var(--tap, 44px);
        width: 100%;
    }
    .admin-tick-controls-jump {
        flex-direction: column;
        align-items: stretch;
    }
    .admin-tick-controls-jump-n {
        width: 100%;
    }
    .admin-tick-controls-jump-confirm {
        width: 100%;
    }
}

/* ─── Admin inspect tabs (TASK_27 / AC30 inspect slots) ─────────────────
   Sidebar-tab body painted by `frontend/static/js/admin/
   inspect_world.js` + the per-snapshot card renderers in the sibling
   `inspect_world_cards.js`. Mounts inside the AC30 sidebar's
   `[data-admin-tab-panel="world"]` AND `[data-admin-tab-panel="player"]`
   hosts that the TASK_21 shell stamps; the painter writes a
   `.admin-inspect` root with the `--world` / `--player` modifier
   matching the active tab, an intro line, one or more lookup forms
   (`.admin-inspect-form`), and per-result snapshot cards
   (`.admin-inspect-card[data-inspect-kind]`).

   Mobile breakpoint (TASK_29 / AC42) routes both inspect tabs to an
   "Open on desktop" card on `< 640px` viewports because the JSON
   `<pre>` dumps + dense lookup forms don't survive a phone width;
   the media query stub at the bottom is what TASK_29 will flesh
   out. */

.admin-inspect {
    display: flex;
    flex-direction: column;
    gap: 12px;
    font-size: 13px;
}

.admin-inspect-intro {
    font-size: 11px;
    color: var(--text-dim, #a59c8a);
    text-transform: uppercase;
    letter-spacing: 0.06em;
}

.admin-inspect-status {
    font-size: 12px;
    color: var(--text-dim, #a59c8a);
    padding: 6px 10px;
    border: 1px solid rgba(255, 255, 255, 0.06);
    border-radius: var(--radius-md);
    background: rgba(0, 0, 0, 0.10);
}
.admin-inspect-status[data-inspect-status="error"] {
    color: var(--vermilion);
    border-color: var(--vermilion);
    background: rgba(168, 68, 44, 0.10);
}
.admin-inspect-status[data-inspect-status="loading"] {
    color: #d3a13b;
    border-color: rgba(211, 161, 59, 0.36);
}
.admin-inspect-status[data-inspect-status="empty"] {
    color: var(--text-dim, #a59c8a);
    font-style: italic;
}

.admin-inspect-slot {
    display: flex;
    flex-direction: column;
    gap: 8px;
}

.admin-inspect-form {
    display: flex;
    flex-direction: column;
    gap: 6px;
    padding: 8px 10px;
    border: 1px solid rgba(255, 255, 255, 0.06);
    border-radius: var(--radius-md);
    background: rgba(0, 0, 0, 0.10);
}

.admin-inspect-form-title {
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--text-dim, #a59c8a);
}

.admin-inspect-form-row {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-end;
    gap: 8px;
}

.admin-inspect-form-label {
    display: flex;
    flex-direction: column;
    gap: 2px;
    font-size: 11px;
    color: var(--text-dim, #a59c8a);
}

.admin-inspect-form-label-text {
    font-family: var(--font-mono, 'JetBrains Mono', monospace);
    text-transform: lowercase;
}

.admin-inspect-form-input {
    width: 90px;
    background: rgba(0, 0, 0, 0.36);
    color: var(--text, #e9e3d5);
    border: 1px solid rgba(168, 68, 44, 0.32);
    border-radius: 4px;
    padding: 6px 8px;
    font-size: 13px;
    font-family: inherit;
}
.admin-inspect-form-input:focus {
    outline: 2px solid var(--vermilion);
    outline-offset: -1px;
    border-color: var(--vermilion);
}

.admin-inspect-form-submit {
    padding: 6px 14px;
    background: transparent;
    color: var(--text, #e9e3d5);
    border: 1px solid var(--vermilion);
    border-radius: 4px;
    cursor: pointer;
    font-family: inherit;
    font-size: 13px;
    min-height: var(--tap-sm, 36px);
}
.admin-inspect-form-submit:hover,
.admin-inspect-form-submit:focus-visible {
    color: #fff;
    background: rgba(168, 68, 44, 0.18);
    outline: none;
}

.admin-inspect-card {
    display: flex;
    flex-direction: column;
    gap: 8px;
    padding: 10px 12px;
    border: 1px solid rgba(255, 255, 255, 0.06);
    border-radius: var(--radius-md);
    background: rgba(0, 0, 0, 0.18);
}

.admin-inspect-card-title {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 6px;
}

.admin-inspect-card-name {
    font-size: 14px;
    color: var(--text, #e9e3d5);
    font-weight: 600;
}

.admin-inspect-tick {
    font-family: var(--font-mono, 'JetBrains Mono', monospace);
    font-size: 11px;
    color: var(--text-dim, #a59c8a);
    background: rgba(0, 0, 0, 0.36);
    padding: 2px 6px;
    border-radius: 3px;
}

.admin-inspect-paused {
    font-size: 10px;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    padding: 2px 6px;
    border-radius: 3px;
    border: 1px solid currentColor;
}
.admin-inspect-paused[data-inspect-paused="true"] {
    color: var(--vermilion);
    background: rgba(168, 68, 44, 0.14);
}
.admin-inspect-paused[data-inspect-paused="false"] {
    color: #6da06d;
}

.admin-inspect-chip {
    font-family: var(--font-mono, 'JetBrains Mono', monospace);
    font-size: 11px;
    color: var(--text-dim, #a59c8a);
    background: rgba(0, 0, 0, 0.32);
    padding: 2px 6px;
    border-radius: 3px;
    border: 1px solid rgba(255, 255, 255, 0.06);
}
.admin-inspect-chip[data-inspect-dim="1"] {
    opacity: 0.55;
    font-style: italic;
}

.admin-inspect-meta {
    display: flex;
    flex-wrap: wrap;
    gap: 6px 10px;
}

.admin-inspect-kv {
    display: inline-flex;
    align-items: baseline;
    gap: 4px;
    font-size: 11px;
}

.admin-inspect-kv-key {
    color: var(--text-dim, #a59c8a);
    text-transform: uppercase;
    letter-spacing: 0.04em;
}

.admin-inspect-kv-val {
    color: var(--text, #e9e3d5);
    font-family: var(--font-mono, 'JetBrains Mono', monospace);
}

.admin-inspect-json {
    font-size: 11px;
}

.admin-inspect-json-summary {
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--text-dim, #a59c8a);
    cursor: pointer;
    padding: 4px 6px;
    border-radius: 3px;
}
.admin-inspect-json-summary:hover {
    color: var(--text, #e9e3d5);
}

.admin-inspect-json-pre {
    font-family: var(--font-mono, 'JetBrains Mono', monospace);
    font-size: 11px;
    color: var(--text, #e9e3d5);
    background: rgba(0, 0, 0, 0.36);
    padding: 8px 10px;
    border-radius: 4px;
    border: 1px solid rgba(255, 255, 255, 0.04);
    overflow-x: auto;
    white-space: pre;
    margin: 4px 0 0 0;
}

@media (prefers-reduced-motion: reduce) {
    .admin-inspect-form-input,
    .admin-inspect-form-submit {
        transition: none;
    }
}
html.force-reduced-motion .admin-inspect-form-input,
html.force-reduced-motion .admin-inspect-form-submit {
    transition: none;
}

@media (max-width: 639px) {
    /* Mobile breakpoint stub — TASK_29 / AC42 routes the inspect
       tabs to an "Open on desktop" card on phone widths. Until
       then we collapse the form row and bump tap-target heights so
       finger presses land cleanly. */
    .admin-inspect-form-row {
        flex-direction: column;
        align-items: stretch;
    }
    .admin-inspect-form-input,
    .admin-inspect-form-submit {
        width: 100%;
        min-height: var(--tap, 44px);
    }
}


/* ─── Mobile-lite "Open on desktop" deep-link card (TASK_29 / AC42) ──
   Painted by `frontend/static/js/admin/mobile_lite.js::
   paintOpenOnDesktopCard` into any sidebar tab body / confirm modal
   slot that doesn't survive a phone width. Bulk ops + audit log +
   world inspect + player inspect + tick controls all route here on
   `(max-width: 639px)`. The card carries a glyph + title + body copy
   + the deep-link URL + a Copy-link button + a `<details>` "Why?"
   disclosure (UA-default expand). The chrome is layout-neutral —
   the sibling tabs and the confirm modal each provide their own
   host so the same painter fits every slot. */
.admin-open-on-desktop-card {
    display: flex;
    flex-direction: column;
    gap: 10px;
    padding: 16px;
    border: 1px solid var(--vermilion);
    border-radius: var(--radius-md);
    background: rgba(168, 68, 44, 0.06);
    color: var(--text, #e9e3d5);
    font-size: 14px;
}
.admin-open-on-desktop-glyph {
    font-size: 28px;
    line-height: 1;
    color: var(--vermilion);
    text-align: center;
}
.admin-open-on-desktop-title {
    font: 600 16px/1.2 var(--font-display, 'EB Garamond', serif);
    color: var(--vermilion);
    text-align: center;
    text-transform: uppercase;
    letter-spacing: 0.06em;
}
.admin-open-on-desktop-body {
    font-size: 13px;
    color: var(--text-dim, #a59c8a);
    line-height: 1.5;
    text-align: center;
}
.admin-open-on-desktop-link {
    display: flex;
    flex-direction: column;
    gap: 4px;
    padding: 8px 10px;
    border: 1px solid rgba(255, 255, 255, 0.06);
    border-radius: 4px;
    background: rgba(0, 0, 0, 0.36);
    overflow-x: auto;
}
.admin-open-on-desktop-link-text {
    font-family: var(--font-mono, 'JetBrains Mono', monospace);
    font-size: 12px;
    color: var(--text, #e9e3d5);
    word-break: break-all;
    white-space: pre-wrap;
}
.admin-open-on-desktop-copy {
    padding: 8px 14px;
    background: transparent;
    color: var(--text, #e9e3d5);
    border: 1px solid var(--vermilion);
    border-radius: 4px;
    cursor: pointer;
    font-family: inherit;
    font-size: 14px;
    min-height: var(--tap, 44px);
    align-self: stretch;
}
.admin-open-on-desktop-copy:hover,
.admin-open-on-desktop-copy:focus-visible {
    color: #fff;
    background: rgba(168, 68, 44, 0.18);
    outline: none;
}
.admin-open-on-desktop-status {
    font-size: 12px;
    color: var(--text-dim, #a59c8a);
    text-align: center;
    min-height: 1em;
}
.admin-open-on-desktop-status[data-admin-lite-status="ok"] {
    color: #6da06d;
}
.admin-open-on-desktop-status[data-admin-lite-status="error"] {
    color: var(--vermilion);
}
.admin-open-on-desktop-why {
    font-size: 12px;
    color: var(--text-dim, #a59c8a);
}
.admin-open-on-desktop-why-summary {
    cursor: pointer;
    padding: 4px 6px;
    border-radius: 3px;
}
.admin-open-on-desktop-why-summary:hover {
    color: var(--text, #e9e3d5);
}
.admin-open-on-desktop-why-body {
    margin-top: 6px;
    padding: 8px 10px;
    border-left: 2px solid rgba(168, 68, 44, 0.32);
    line-height: 1.5;
}

/* Mobile-lite confirm-modal bulk-op gate (TASK_29 / AC42). The
   modal's standard body blocks (params / dry-run / typed-confirm /
   result + Confirm button) hide; the dedicated `liteGate` slot
   reveals the AC42 card. The painter (`confirm_modal_lite.js`) does
   the JS-side hide; this rule pins the slot's reveal so the card
   inherits the modal's chrome (max-width / padding) cleanly. */
.admin-confirm-modal-lite-gate {
    display: flex;
    flex-direction: column;
    gap: 0;
}
.admin-confirm-modal-lite-gate[hidden] {
    display: none;
}

/* === icon-refactor TASK_02 / spec AC03 ===
 *
 * Shared `.icon` class for the bundled SVG icon set under
 * `/static/svg/icons/{lucide,custom}/`. Every <img> emitted by
 * `components/icon.js::icon()` / `iconHTML()` lands here. The
 * `currentColor` invariant on the SVGs themselves means this rule
 * only sets size + alignment; the parent's `color` cascades into the
 * stroke / fill.
 *
 * `data-tone` is the canonical opt-in for atlas accent colours —
 * `brass` for the brass-strong accent rail (action chips, primary CTAs)
 * and `patina` for the success-tinted variant (live-status dots,
 * positive deltas). Other tones can be set inline via `style="color: …"`
 * on the call site or via a wrapping class.
 *
 * Vertical alignment is `-0.15em` so a 16 px icon sits flush with the
 * text baseline of `--fs-body` (13 px) and `--fs-label` (11 px) —
 * matched by visual-regression diff against the Lucide reference set.
 * Reduced-motion users see no transition (the icon never animates), so
 * the `prefers-reduced-motion` block at the bottom of this file does
 * not need an entry. */
.icon {
    display: inline-block;
    vertical-align: -0.15em;
    color: currentColor;
    /* Defence-in-depth: prevents an oversized icon (size> hard cap) from
       blowing the parent's line height on a malformed call. */
    flex-shrink: 0;
}
.icon[data-tone="brass"]  { color: var(--accent); }
.icon[data-tone="patina"] { color: var(--patina); }


/* ─── clan-reputation-tiered TASK_05 / AC-B4 ──────────────────────── */
/* Clan-hall candidacy panel sits above the actions-grid in the Лагер
 * tab body when the tile is the viewer's clan-hall. Reuses the
 * canonical .atlas-card chrome with a dedicated `data-variant` so the
 * hint copy + button layout stay consistent with the panel-card
 * precedent (stockpile / building / clan).
 */
.atlas-card[data-variant="clan-hall"] {
    border-left: 4px solid var(--accent, #c8a653);
    margin-bottom: 12px;
}
.atlas-card[data-variant="clan-hall"] .clan-hall-panel-hint {
    margin: 4px 0 8px 0;
    color: var(--text-dim, #aaa);
}
.atlas-card[data-variant="clan-hall"] .clan-hall-panel-btn {
    display: inline-block;
    padding: 6px 12px;
    margin-top: 4px;
}
.atlas-card[data-variant="clan-hall"] .clan-hall-panel-btn-nominate {
    background: var(--accent, #c8a653);
    color: #1a1a1a;
    font-weight: 600;
}
.atlas-card[data-variant="clan-hall"] .clan-hall-panel-btn-retract {
    background: #555;
    color: #ddd;
}

/* Reputation gauge in the guild-panel header band (player guilds —
 * not the NPC clan_view gauge). Two-tier: outside_clan_rep when
 * unaffiliated, clan_inside_rep when in a clan. The candidate-chip
 * appears next to the label when value >= 50 inside-rep.
 */
.rep-gauge .rep-gauge-bar {
    border-radius: 2px;
    overflow: hidden;
}
.rep-gauge .rep-gauge-fill {
    transition: width 240ms ease-out;
}
@media (prefers-reduced-motion: reduce) {
    .rep-gauge .rep-gauge-fill { transition: none; }
}
.rep-gauge .rep-gauge-chip {
    font-style: italic;
}

/* ─── admin-contact-form TASK_06 — three contact panels ───────────────
 * Form + reporter "Мої звернення" + admin contact queue. Reuse the
 * canonical sheet-modal panel chrome so panel-open / Esc-priority /
 * mobile-drawer collapse all work without further wiring; the rules
 * below cover the panel-body layout (form rows + list rows + detail
 * blocks) plus the kind / status colour-cue badges.
 */

/* Form panel body */
.contact-form-body {
    padding: 12px 16px 16px 16px;
    display: flex;
    flex-direction: column;
    gap: 12px;
}
.contact-form-banner {
    padding: 8px 12px;
    border-radius: 4px;
    background: #5a2424;
    color: #ffd2c8;
    font-size: 14px;
    border-left: 4px solid #c83a3a;
}
.contact-form-row { display: flex; flex-direction: column; gap: 4px; }
.contact-form-label {
    font-size: 13px;
    color: var(--text-dim, #aaa);
    font-weight: 600;
}
.contact-form-input,
.contact-form-select,
.contact-form-textarea {
    background: #1a1a1a;
    border: 1px solid #444;
    color: #ddd;
    padding: 6px 8px;
    border-radius: 3px;
    font: inherit;
}
.contact-form-textarea { resize: vertical; min-height: 80px; }
.contact-form-cheating {
    padding: 8px 12px;
    border-left: 3px solid #c8a653;
    background: rgba(200, 166, 83, 0.06);
    display: flex;
    flex-direction: column;
    gap: 8px;
}
.contact-form-actions {
    display: flex;
    gap: 8px;
    align-items: center;
    margin-top: 4px;
}
.contact-form-mylink {
    background: transparent;
    border: 1px solid #444;
    color: #ccc;
}

/* Shared kind / status badges (reporter list + admin queue) */
.contact-mr-badge,
.admin-cq-badge {
    display: inline-block;
    padding: 1px 6px;
    border-radius: 3px;
    font-size: 11px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    border: 1px solid #444;
    color: #ddd;
}
.contact-mr-badge[data-kind="cheating"],
.admin-cq-badge[data-kind="cheating"] { background: #4a2a2a; color: #ffd2c8; border-color: #7a3a3a; }
.contact-mr-badge[data-kind="bug"],
.admin-cq-badge[data-kind="bug"] { background: #2a3a4a; color: #c8d8e8; border-color: #3a5a7a; }
.contact-mr-badge[data-kind="account"],
.admin-cq-badge[data-kind="account"] { background: #4a4a2a; color: #e8e0c8; border-color: #7a7a3a; }
.contact-mr-badge[data-kind="suggestion"],
.admin-cq-badge[data-kind="suggestion"] { background: #2a4a3a; color: #c8e8d8; border-color: #3a7a5a; }
.contact-mr-badge[data-kind="other"],
.admin-cq-badge[data-kind="other"] { background: #3a3a3a; color: #ccc; }
.contact-mr-badge[data-status="open"],
.admin-cq-badge[data-status="open"] { background: #2a4a3a; color: #c8e8d8; }
.contact-mr-badge[data-status="triaged"],
.admin-cq-badge[data-status="triaged"] { background: #4a4a2a; color: #e8e0c8; }
.contact-mr-badge[data-status="in_progress"],
.admin-cq-badge[data-status="in_progress"] { background: #2a3a4a; color: #c8d8e8; }
.contact-mr-badge[data-status="resolved"],
.admin-cq-badge[data-status="resolved"] { background: #2a4a2a; color: #c8e8c8; }
.contact-mr-badge[data-status="wontfix"],
.admin-cq-badge[data-status="wontfix"] { background: #4a2a2a; color: #ffd2c8; }
.contact-mr-badge[data-status="duplicate"],
.admin-cq-badge[data-status="duplicate"] { background: #3a3a3a; color: #ccc; }

/* "Мої звернення" panel — reporter list + detail */
.contact-my-reports-body { padding: 12px 16px 16px 16px; }
.contact-mr-toolbar { margin-bottom: 8px; }
.contact-mr-empty { padding: 12px; color: var(--text-dim, #aaa); font-style: italic; }
.contact-mr-error {
    padding: 10px 12px;
    background: #5a2424;
    color: #ffd2c8;
    border-radius: 3px;
    border-left: 4px solid #c83a3a;
}
.contact-mr-list { list-style: none; padding: 0; margin: 0; }
.contact-mr-row {
    padding: 8px 10px;
    border-bottom: 1px solid #2a2a2a;
    cursor: pointer;
    transition: background 120ms ease-out;
}
.contact-mr-row:hover { background: rgba(200, 166, 83, 0.06); }
.contact-mr-row-head {
    display: flex;
    gap: 8px;
    align-items: center;
    flex-wrap: wrap;
    margin-bottom: 4px;
}
/* mobile-ux-rework TASK_08 / AC24 — `--font-mono` token instead of the
 * bare `monospace` keyword. */
.contact-mr-id { color: var(--text-dim, #aaa); font-family: var(--font-mono); }
.contact-mr-date { color: var(--text-dim, #888); font-size: 12px; margin-left: auto; }
.contact-mr-subject { color: #ccc; font-size: 13px; }
.contact-mr-detail { padding: 4px 0; }
.contact-mr-detail-head { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin-bottom: 8px; }
.contact-mr-detail-subject { margin: 4px 0 12px 0; color: #e8e8e8; }
.contact-mr-detail-body {
    background: #1a1a1a;
    border-left: 3px solid #444;
    padding: 10px 12px;
    color: #ddd;
    border-radius: 3px;
    white-space: pre-wrap;
    word-break: break-word;
}
.contact-mr-detail-meta { margin-top: 8px; color: var(--text-dim, #888); font-size: 12px; }

/* Admin queue panel */
.admin-contact-queue-body { padding: 12px 16px 16px 16px; }
.admin-cq-filters {
    display: flex;
    gap: 12px;
    align-items: center;
    flex-wrap: wrap;
    margin-bottom: 12px;
}
.admin-cq-filter-label {
    display: flex;
    align-items: center;
    gap: 6px;
    font-size: 13px;
    color: var(--text-dim, #aaa);
}
.admin-cq-filter-toggle { gap: 6px; }
.admin-cq-filter-status,
.admin-cq-filter-kind {
    background: #1a1a1a;
    border: 1px solid #444;
    color: #ddd;
    padding: 4px 6px;
    border-radius: 3px;
}
.admin-cq-empty { padding: 12px; color: var(--text-dim, #aaa); font-style: italic; }
.admin-cq-error {
    padding: 10px 12px;
    background: #5a2424;
    color: #ffd2c8;
    border-radius: 3px;
    border-left: 4px solid #c83a3a;
}
.admin-cq-list { list-style: none; padding: 0; margin: 0; }
.admin-cq-row {
    padding: 8px 10px;
    border-bottom: 1px solid #2a2a2a;
    cursor: pointer;
    transition: background 120ms ease-out;
}
.admin-cq-row:hover { background: rgba(200, 166, 83, 0.06); }
.admin-cq-row-head {
    display: flex;
    gap: 8px;
    align-items: center;
    flex-wrap: wrap;
    margin-bottom: 4px;
    font-size: 12px;
}
/* mobile-ux-rework TASK_08 / AC24 — `--font-mono` token. */
.admin-cq-id { color: var(--text-dim, #aaa); font-family: var(--font-mono); }
.admin-cq-user { color: #c8d8e8; }
.admin-cq-world { color: var(--text-dim, #888); }
.admin-cq-date { color: var(--text-dim, #888); font-size: 12px; margin-left: auto; }
.admin-cq-subject { color: #ccc; font-size: 13px; }
.admin-cq-toolbar { margin-bottom: 8px; }
.admin-cq-detail { padding: 4px 0; }
.admin-cq-detail-head { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin-bottom: 8px; }
.admin-cq-detail-subject { margin: 4px 0 12px 0; color: #e8e8e8; }
.admin-cq-detail-body {
    background: #1a1a1a;
    border-left: 3px solid #444;
    padding: 10px 12px;
    color: #ddd;
    border-radius: 3px;
    white-space: pre-wrap;
    word-break: break-word;
    margin-bottom: 12px;
}
.admin-cq-meta {
    background: rgba(255, 255, 255, 0.02);
    padding: 8px 12px;
    border-radius: 3px;
    margin-bottom: 12px;
    font-size: 12px;
}
.admin-cq-meta-row { display: flex; gap: 8px; padding: 2px 0; }
.admin-cq-meta-key { color: var(--text-dim, #888); min-width: 100px; }
.admin-cq-meta-val { color: #ccc; word-break: break-all; }
.admin-cq-extra { margin-bottom: 12px; }
.admin-cq-extra summary { cursor: pointer; color: var(--accent, #c8a653); font-size: 13px; }
.admin-cq-extra-body {
    background: #1a1a1a;
    padding: 8px 12px;
    border-radius: 3px;
    color: #ccc;
    font-size: 12px;
    overflow-x: auto;
}
.admin-cq-transitions {
    display: flex;
    gap: 6px;
    align-items: center;
    flex-wrap: wrap;
    margin-bottom: 12px;
    padding: 8px;
    background: rgba(200, 166, 83, 0.06);
    border-radius: 3px;
}
.admin-cq-transitions-label { font-size: 13px; color: var(--text-dim, #aaa); }
.admin-cq-transitions-empty { color: var(--text-dim, #888); font-style: italic; padding: 8px; }
.admin-cq-notes { margin-top: 12px; }
.admin-cq-notes h5 { margin: 0 0 6px 0; color: #c8a653; font-size: 13px; text-transform: uppercase; }
.admin-cq-notes-empty { color: var(--text-dim, #888); font-style: italic; }
.admin-cq-note-list { list-style: none; padding: 0; margin: 0; }
.admin-cq-note {
    padding: 6px 10px;
    border-left: 3px solid #444;
    background: rgba(255, 255, 255, 0.02);
    margin-bottom: 6px;
}
.admin-cq-note-head { display: flex; gap: 8px; font-size: 11px; color: var(--text-dim, #888); margin-bottom: 2px; }
.admin-cq-note-author { color: #c8a653; }
.admin-cq-note-body { color: #ccc; font-size: 13px; white-space: pre-wrap; }
.admin-cq-note-form { margin-top: 12px; display: flex; flex-direction: column; gap: 6px; }
.admin-cq-note-input {
    background: #1a1a1a;
    border: 1px solid #444;
    color: #ddd;
    padding: 6px 8px;
    border-radius: 3px;
    font: inherit;
    resize: vertical;
}

/* admin-contact-form TASK_08 — public reporter↔admin chat thread.
 * Distinct from `admin-cq-note*` (admin-internal) by visual weight + role
 * tinting: reporter rows carry a blue accent, admin rows green. Reused
 * by both reporter ("Мої звернення") and admin ("Черга звернень")
 * panels via the shared `contact_thread_render.js` helper. */
.contact-thread { margin-top: 12px; }
.contact-thread-title { margin: 0 0 6px 0; color: #c8a653; font-size: 13px; text-transform: uppercase; }
.contact-thread-empty { color: var(--text-dim, #888); font-style: italic; padding: 6px 0; }
.contact-thread-list { list-style: none; padding: 0; margin: 0 0 8px 0; }
.contact-thread-msg {
    padding: 6px 10px;
    border-left: 3px solid #444;
    background: rgba(255, 255, 255, 0.02);
    margin-bottom: 6px;
}
.contact-thread-msg[data-author-kind="reporter"] {
    border-left-color: #4d8aff;
    background: rgba(77, 138, 255, 0.06);
}
.contact-thread-msg[data-author-kind="admin"] {
    border-left-color: #5fb56a;
    background: rgba(95, 181, 106, 0.06);
}
.contact-thread-msg-head { display: flex; gap: 8px; font-size: 11px; color: var(--text-dim, #888); margin-bottom: 2px; align-items: center; }
.contact-thread-role {
    text-transform: uppercase;
    font-weight: 600;
    font-size: 10px;
    letter-spacing: 0.5px;
}
.contact-thread-role[data-author-kind="reporter"] { color: #4d8aff; }
.contact-thread-role[data-author-kind="admin"] { color: #5fb56a; }
.contact-thread-author { color: #c8a653; }
.contact-thread-date { margin-left: auto; }
.contact-thread-body { color: #ccc; font-size: 13px; white-space: pre-wrap; }
.contact-thread-form { margin-top: 8px; display: flex; flex-direction: column; gap: 6px; }
.contact-thread-input {
    background: #1a1a1a;
    border: 1px solid #444;
    color: #ddd;
    padding: 6px 8px;
    border-radius: 3px;
    font: inherit;
    resize: vertical;
}
.contact-thread-submit { align-self: flex-start; }
.contact-thread-submit[aria-disabled="true"],
.contact-thread-submit[disabled] {
    opacity: 0.5;
    cursor: not-allowed;
}
.contact-thread-hint {
    font-size: 11px;
    color: var(--text-dim, #888);
    font-style: italic;
}

/* ─── chats-removal-external-links TASK_04 — guild external-link icon row ───
 * Per-guild external-community link strip rendered by
 * `guild_panel.js::renderGuildExternalLinks`. Sits between the
 * member-count span and the join button on every row in the guilds
 * list. Layout: flex row + 4 px gap; icons stay tight so a guild with
 * all seven URLs filled doesn't crowd the join button on narrow panels.
 * Per-platform colour accents follow each community's recognisable
 * brand hue (Discord blurple / Telegram cyan / Twitter ice / Reddit
 * orange / Matrix teal / YouTube red); the generic `.guild-ext-link`
 * styling stays neutral so an unknown future platform falls back to
 * the panel's text colour without a missing-token render.
 */
.guild-ext-links {
    display: flex;
    flex-direction: row;
    align-items: center;
    gap: 4px;
    flex-wrap: nowrap;
}
.guild-ext-link {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 22px;
    height: 22px;
    color: #b8b8b8;
    text-decoration: none;
    border: 1px solid transparent;
    border-radius: 3px;
    transition: color 120ms ease-out,
                border-color 120ms ease-out,
                background 120ms ease-out;
}
.guild-ext-link:hover,
.guild-ext-link:focus-visible {
    color: #fff;
    border-color: #555;
    background: #1f1f1f;
    outline: none;
}
.guild-ext-link[data-platform="discord"]:hover  { color: #5865f2; }
.guild-ext-link[data-platform="telegram"]:hover { color: #2aabee; }
.guild-ext-link[data-platform="website"]:hover  { color: #b8d8ff; }
.guild-ext-link[data-platform="twitter"]:hover  { color: #e7e9ea; }
.guild-ext-link[data-platform="reddit"]:hover   { color: #ff4500; }
.guild-ext-link[data-platform="matrix"]:hover   { color: #0dbd8b; }
.guild-ext-link[data-platform="youtube"]:hover  { color: #ff0033; }
@media (prefers-reduced-motion: reduce) {
    .guild-ext-link { transition: none; }
}

/* ─── skill-tree-progression: skill cards + tree grid + detail modal
   ─────────────────────────────────────────────────────────────────
   TASK_12 ships every skill-tree-namespace stylesheet entry in one
   block. TASK_10 + TASK_11 deferred their CSS to land here so the
   parent-component-installs-section-CSS pattern (mirrors
   ui-modal-redesign R8 + ui-tile-army-cards-rework T06 precedent)
   keeps the rules colocated.
*/

/* Skill card art panel — `skill_card.js::_buildArtSlot` mounts a
   square thumb above the body wrap. The slot reserves a 1:1 panel via
   aspect-ratio so the grid keeps a stable rhythm when one card has an
   illustration and a sibling falls through to the icon glyph. The
   `--locked` modifier desaturates the art per design.md
   "Card markup" — locked card art reads dim so the player's eye
   skips past the row to the unlocked options. */
.skill-card-art-slot {
    position: relative;
    width: 100%;
    aspect-ratio: 1 / 1;
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: hidden;
    border-radius: var(--radius-card, 8px);
    background: var(--surface-elev);
}
.skill-card-art {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}
.skill-card-icon-fallback {
    width: 60%;
    height: 60%;
    object-fit: contain;
    opacity: 0.85;
}
.skill-card-art-slot--locked .skill-card-art,
.skill-card-art-slot--locked .skill-card-icon-fallback {
    filter: grayscale(0.85) opacity(0.6);
}
.skill-card-lock-badge {
    position: absolute;
    top: var(--space-1, 4px);
    right: var(--space-1, 4px);
    width: 28px;
    height: 28px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border-radius: var(--radius-pill, 999px);
    background: rgba(20, 20, 24, 0.78);
    color: var(--ink-on-dark, #f6f3eb);
    font-size: 16px;
    line-height: 1;
    pointer-events: none;
}

/* Lock-reason chip — appended to `atlas-card-meta` when the card is
   locked. The accent edge + dim ink reads as a muted warning so the
   chip sits inside the meta row without stealing focus from the
   primary line. */
.atlas-meta-chip--lock {
    border-color: var(--color-warning, #b58530);
    background: rgba(181, 133, 48, 0.12);
}

/* Skill card root — desaturates the whole `.atlas-card` chrome on
   lock so the meta strip + secondary line read in lockstep with the
   art panel. The `aria-disabled` selector keeps the desaturation
   reachable even if `data-locked` drifts on a future refactor. */
.skill-card[data-locked="true"],
.skill-card[aria-disabled="true"] {
    opacity: 0.78;
}
.skill-card[data-locked="true"] .atlas-card-primary,
.skill-card[aria-disabled="true"] .atlas-card-primary {
    color: var(--ink-mute);
}

/* Skill tree grid — one section per category, each carrying the
   heading divider + the canonical `.atlas-card-grid` rule from the
   atlas-card section. The wrapper sits inside the UnitView Дерево
   tab body; the grid auto-fills 200 px tracks (mirrors the stockpile
   surface) so the cards tile cleanly across desktop widths and
   collapse to a single column on the mobile bottom-sheet. */
.skill-tree-grid {
    display: flex;
    flex-direction: column;
    gap: var(--space-3, 12px);
    padding: var(--space-2, 8px) 0;
}
.skill-tree-section {
    display: flex;
    flex-direction: column;
    gap: var(--space-2, 8px);
}
.skill-tree-section-heading {
    display: flex;
    align-items: baseline;
    gap: var(--space-2, 8px);
}
.skill-tree-section-count {
    font-family: var(--font-mono, monospace);
    color: var(--ink-mute, #8a857c);
    font-weight: normal;
    font-size: 0.85em;
}
.skill-tree-empty {
    margin: var(--space-3, 12px) 0;
}
.skill-tree-heading {
    margin-bottom: var(--space-2, 8px);
}

/* Skill detail modal — TASK_11 deferred its CSS to TASK_12 so the
   2-col desktop / stacked mobile layout lands here in lockstep with
   the tree-grid that opens it. The dialog host inherits the native
   `<dialog>` chrome from `modal/base.js`; this block adds the body
   layout + the per-section spacing. */
.skill-detail-modal {
    max-width: min(960px, 92vw);
    width: 92vw;
    padding: 0;
    border: 1px solid var(--border-firm, #4a4640);
    border-radius: var(--radius-card, 8px);
    background: var(--surface, #1c1a17);
    color: var(--ink, #f6f3eb);
}
.skill-detail-modal::backdrop {
    background: rgba(0, 0, 0, 0.6);
}
.skill-detail-header {
    display: flex;
    align-items: center;
    gap: var(--space-2, 8px);
    padding: var(--space-3, 12px) var(--space-4, 16px);
    border-bottom: 1px solid var(--rule, #2c2a26);
}
.skill-detail-title {
    flex: 1;
    margin: 0;
    font-family: var(--font-display, "EB Garamond", serif);
    font-size: var(--fs-h2, 1.4rem);
}
.skill-detail-back,
.skill-detail-close {
    min-width: var(--tap-sm, 36px);
    min-height: var(--tap-sm, 36px);
    border: 1px solid var(--border-firm, #4a4640);
    border-radius: var(--radius-pill, 999px);
    background: var(--surface-elev, #25221e);
    color: var(--ink, #f6f3eb);
    cursor: pointer;
}
.skill-detail-back.hidden {
    display: none;
}
.skill-detail-body {
    display: grid;
    grid-template-columns: 40% 60%;
    gap: var(--space-4, 16px);
    padding: var(--space-4, 16px);
}
.skill-detail-art {
    margin: 0;
    display: flex;
    align-items: flex-start;
    justify-content: center;
}
.skill-detail-art img {
    width: 100%;
    height: auto;
    object-fit: cover;
    border-radius: var(--radius-card, 8px);
}
.skill-detail-content {
    display: flex;
    flex-direction: column;
    gap: var(--space-3, 12px);
}
.skill-detail-effects {
    width: 100%;
    border-collapse: collapse;
    font-size: 0.95em;
}
.skill-detail-effects th,
.skill-detail-effects td {
    padding: var(--space-1, 4px) var(--space-2, 8px);
    text-align: left;
    border-bottom: 1px solid var(--rule, #2c2a26);
}
.skill-detail-effects tr[data-use-only="true"] td {
    color: var(--color-warning, #b58530);
    font-weight: 600;
}
.skill-detail-effects-use-only-badge {
    display: inline-block;
    margin-left: var(--space-2, 8px);
    padding: 0 var(--space-2, 8px);
    border-radius: var(--radius-pill, 999px);
    background: rgba(181, 133, 48, 0.18);
    color: var(--color-warning, #b58530);
    font-size: 0.8em;
    font-weight: 600;
}
.skill-detail-locks,
.skill-detail-prereqs {
    padding: var(--space-2, 8px);
    border-radius: var(--radius-card, 8px);
    background: var(--surface-elev, #25221e);
}
.skill-detail-learn {
    align-self: flex-start;
    padding: var(--space-2, 8px) var(--space-4, 16px);
    border: 1px solid var(--accent, #b58530);
    border-radius: var(--radius-pill, 999px);
    background: var(--accent, #b58530);
    color: var(--ink-on-accent, #1c1a17);
    font-weight: 600;
    cursor: pointer;
    min-height: var(--tap, 44px);
}
.skill-detail-learn[aria-disabled="true"],
.skill-detail-learn[disabled] {
    opacity: 0.55;
    cursor: not-allowed;
}

/* Stacked-on-mobile per design.md "Detail modal: 2-column on desktop
   ... single-column stacked on mobile". Below 720 px the body
   collapses to one column so the art reads above the content rather
   than next to it. */
@media (max-width: 719px) {
    .skill-detail-body {
        grid-template-columns: 1fr;
    }
}

/* TASK_13 — Filter row + keyboard nav focus indicator. Sits above
   the grid; a search input on the left + a chips strip on the right.
   The chips strip mirrors the `.tile-view-filter-chip` shape so the
   visual rhythm carries across the workspace. */
.skill-tree-filter {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-3, 12px);
    margin-bottom: var(--space-3, 12px);
}
.skill-tree-search {
    display: flex;
    flex: 1 1 220px;
    min-width: 200px;
    max-width: 360px;
    align-items: center;
}
.skill-tree-search-input {
    width: 100%;
    background: var(--bg-elev);
    border: 1px solid var(--border-strong);
    color: var(--text);
    border-radius: var(--radius-pill, 999px);
    padding: 6px 14px;
    font-family: var(--font-body);
    font-size: 16px;
    min-height: var(--tap-sm, 36px);
    transition: border-color 140ms ease, background 140ms ease;
}
.skill-tree-search-input::placeholder {
    color: var(--text-dim);
}
.skill-tree-search-input:focus-visible {
    outline: 2px solid var(--color-warning);
    outline-offset: 2px;
    border-color: var(--color-warning);
}
.skill-tree-filter-chips {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-1, 4px);
    list-style: none;
    margin: 0;
    padding: 0;
}
.skill-tree-filter-chip-wrap {
    display: inline-flex;
}
.skill-tree-filter-chip {
    background: var(--bg-elev);
    border: 1px solid var(--border-strong);
    color: var(--text-dim);
    border-radius: 999px;
    padding: 4px 12px;
    font-family: var(--font-body);
    font-size: var(--fs-label);
    cursor: pointer;
    min-height: var(--tap-sm, 36px);
    transition: border-color 140ms ease, color 140ms ease,
        background 140ms ease;
}
.skill-tree-filter-chip:hover {
    color: var(--text);
}
.skill-tree-filter-chip:focus-visible {
    outline: 2px solid var(--color-warning);
    outline-offset: 2px;
}
.skill-tree-filter-chip.is-active {
    border-color: var(--color-warning);
    color: var(--color-warning);
    background: rgba(255, 183, 0, 0.10);
}

/* TASK_13 — Empty filter state (catalogue has rows, but the active
   query/category narrows the visible set to zero). The "Очистити
   фільтр" CTA gives the player a one-tap escape route so they don't
   have to fish for the search box. */
.skill-tree-empty-filter {
    margin: var(--space-3, 12px) 0;
}
.skill-tree-filter-clear {
    align-self: flex-start;
}

/* TASK_13 — Keyboard focus indicator on Detail CTA buttons (the
   focusable surface the arrow-key reducer drives). Carries the same
   ochre outline as the filter chip focus to keep visual rhythm. */
.skill-card-detail:focus-visible {
    outline: 2px solid var(--color-warning);
    outline-offset: 2px;
}

@media (max-width: 639px) {
    .skill-tree-filter {
        flex-direction: column;
        align-items: stretch;
        gap: var(--space-2, 8px);
    }
    .skill-tree-search {
        max-width: none;
    }
}

/* ─── army-script-loops / TASK_07 — Скрипт tab body ─────────────────
 * Fullscreen ArmyView script panel (drag-reorder step list + per-kind
 * inline editor + state badge + paused banner + cycle counter).
 *
 * Class prefixes: `.asp-` (army-script panel) / `.script-state-*`
 * (state badge variants). Scoped under the canonical
 * `.army-view-tab-content.tab-script` to keep parent tabs untouched.
 */
.tab-script .asp-root {
    display: flex;
    flex-direction: column;
    gap: var(--space-3, 12px);
    padding: var(--space-3, 12px);
}
.asp-skeleton, .asp-locked, .asp-empty {
    padding: var(--space-3, 12px);
    color: var(--text-secondary, var(--text));
    text-align: center;
}
.asp-error {
    padding: var(--space-3, 12px);
    color: var(--color-error, var(--text));
    background: var(--bg-elev, transparent);
    border-radius: 4px;
    display: flex;
    gap: var(--space-2, 8px);
    align-items: center;
    justify-content: space-between;
}
.asp-empty-note { margin: 0 0 var(--space-3, 12px) 0; }

.asp-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: var(--space-2, 8px);
    border-bottom: 1px solid var(--border, transparent);
    padding-bottom: var(--space-2, 8px);
}
.asp-header-title { font-weight: 600; }
.asp-header-meta {
    display: flex;
    gap: var(--space-2, 8px);
    align-items: center;
}
.asp-state-badge {
    padding: 2px 8px;
    border-radius: 4px;
    font-size: 0.85em;
}
.asp-state-badge.script-state-running   { background: var(--color-success); color: var(--text-on-accent, #000); }
.asp-state-badge.script-state-paused    { background: var(--color-warning); color: var(--text-on-accent, #000); }
.asp-state-badge.script-state-inactive,
.asp-state-badge.script-state-draft     { background: var(--bg-elev, transparent); color: var(--text-secondary, var(--text)); }
.asp-cycles { font-size: 0.85em; color: var(--text-secondary, var(--text)); }

.asp-paused-banner {
    background: color-mix(in srgb, var(--color-warning) 15%, transparent);
    border-left: 3px solid var(--color-warning);
    padding: var(--space-2, 8px) var(--space-3, 12px);
    border-radius: 0 4px 4px 0;
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: var(--space-2, 8px);
}
.asp-paused-msg { font-size: 0.95em; }

.asp-step-list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: var(--space-2, 8px);
}
.asp-step {
    display: grid;
    grid-template-columns: auto auto 1fr auto auto;
    gap: var(--space-2, 8px);
    align-items: start;
    padding: var(--space-2, 8px);
    border: 1px solid var(--border, transparent);
    border-radius: 4px;
    background: var(--bg-elev, transparent);
}
.asp-step.asp--cursor {
    border-left: 3px solid var(--color-success);
    background: color-mix(in srgb, var(--color-success) 10%, var(--bg-elev, transparent));
}
.asp-step .step-drag {
    cursor: grab;
    user-select: none;
    color: var(--text-secondary, var(--text));
    padding: 0 var(--space-2, 8px);
}
.asp-step .step-drag:active { cursor: grabbing; }
.asp-step-icon {
    font-size: 1.2em;
    line-height: 1.2;
    width: 1.4em;
    text-align: center;
}
.asp-step-label { display: flex; flex-direction: column; gap: 2px; }
.asp-step-kind { font-weight: 500; }
.asp-step-summary { font-size: 0.85em; color: var(--text-secondary, var(--text)); }
.asp-step-editor-body {
    grid-column: 1 / -1;
    padding-top: var(--space-2, 8px);
    border-top: 1px dashed var(--border, transparent);
    margin-top: var(--space-2, 8px);
}
.asp-step-editor-row {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2, 8px);
    align-items: end;
}
.asp-step-editor-cell {
    display: flex;
    flex-direction: column;
    gap: 2px;
    min-width: 80px;
}
.asp-step-editor-label { font-size: 0.8em; color: var(--text-secondary, var(--text)); }
.asp-step-editor-input {
    padding: 2px 6px;
    background: var(--bg, transparent);
    border: 1px solid var(--border, transparent);
    border-radius: 3px;
    color: var(--text);
    font-size: 0.9em;
}
.asp-step-editor-note { font-style: italic; color: var(--text-secondary, var(--text)); }
.asp-step-editor-advanced { margin-top: var(--space-2, 8px); }
.asp-step-editor-advanced-summary {
    cursor: pointer;
    font-size: 0.85em;
    color: var(--text-secondary, var(--text));
}
.asp-step-empty {
    list-style: none;
    color: var(--text-secondary, var(--text));
    text-align: center;
    padding: var(--space-3, 12px);
}

.asp-add-step {
    display: flex;
    gap: var(--space-2, 8px);
    align-items: center;
}
.asp-add-step-cap { color: var(--color-warning); font-size: 0.85em; }
.asp-add-step-kind {
    padding: 2px 6px;
    background: var(--bg, transparent);
    border: 1px solid var(--border, transparent);
    border-radius: 3px;
    color: var(--text);
}

.asp-footer {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2, 8px);
    padding-top: var(--space-2, 8px);
    border-top: 1px solid var(--border, transparent);
}
.asp-action {
    padding: 4px 10px;
    background: var(--bg-elev, transparent);
    border: 1px solid var(--border, transparent);
    border-radius: 3px;
    color: var(--text);
    cursor: pointer;
    font-size: 0.9em;
}
.asp-action:hover:not(:disabled) { background: var(--brass-soft, var(--bg-elev)); }
.asp-action:disabled { opacity: 0.5; cursor: not-allowed; }
.asp-action-primary { background: var(--color-success); color: var(--text-on-accent, #000); }
.asp-action-warn    { background: var(--color-warning); color: var(--text-on-accent, #000); }
.asp-action-ghost   { background: transparent; }

.asp-script-list { font-size: 0.85em; color: var(--text-secondary, var(--text)); }

/* === victory-conditions / TASK_09 — full-screen victory overlay === */
/* Painted by `victory_overlay.js` when `world.victory` lands. The
 * world is paused server-side; the overlay is non-dismissable except
 * via the OK button (which reloads to `/`). z-index 10000 sits above
 * every panel + every modal so a duplicate dismiss path can't sneak
 * the player into a frozen world. */
.victory-overlay {
    position: fixed;
    inset: 0;
    z-index: 10000;
    background: rgba(0, 0, 0, 0.85);
    display: flex;
    align-items: center;
    justify-content: center;
    color: #fff;
}
.victory-card {
    background: #1a1a1a;
    padding: 40px 60px;
    border: 2px solid #ffcc00;
    text-align: center;
    max-width: 600px;
    box-shadow: 0 8px 40px rgba(0, 0, 0, 0.6);
}
.victory-card h1 {
    color: #ffcc00;
    font-size: 2.5rem;
    margin: 0 0 20px;
}
.victory-winner {
    font-size: 1.8rem;
    margin: 10px 0;
}
.victory-path {
    font-size: 1.2rem;
    opacity: 0.8;
}
.victory-stats {
    list-style: none;
    padding: 20px 0;
    text-align: left;
    margin: 0;
}
.victory-stats li {
    padding: 4px 0;
    border-bottom: 1px solid rgba(255, 204, 0, 0.15);
}
#victory-ok-btn {
    margin-top: 20px;
    padding: 12px 32px;
    background: #ffcc00;
    color: #000;
    border: 0;
    cursor: pointer;
    font-weight: bold;
    font-size: 1rem;
}
#victory-ok-btn:hover { background: #e6b800; }

/* tile-state-illustrations — hero illustration at top of
   #tile-details. Sized to fit the side-panel + mobile sheet. */
.tile-state-hero {
    display: block;
    width: 100%;
    max-width: 380px;
    height: auto;
    aspect-ratio: 1 / 1;
    margin: 0 auto var(--gap-md);
    border-radius: var(--radius-md);
    object-fit: cover;
    background: var(--ink-mute);   /* placeholder fill before load */
}
