/**
 * War 2050 - Game UI Components
 * Cards, buttons, stats, and interactive elements
 */

/* =================================================================
   CARDS
   ================================================================= */

.card {
    background: var(--color-primary-700);
    border: 1px solid rgba(212, 175, 55, 0.1);
    border-radius: var(--radius-lg);
    padding: var(--space-5);
    transition: all var(--transition-base);
}

.card:hover {
    border-color: rgba(212, 175, 55, 0.2);
    box-shadow: var(--shadow-md);
}

.card-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: var(--space-4);
}

.card-title {
    font-family: var(--font-heading);
    font-size: var(--text-lg);
    font-weight: var(--font-semibold);
    color: var(--color-text-primary);
}

.card-icon {
    width: 48px;
    height: 48px;
    display: flex;
    align-items: center;
    justify-content: center;
    background: rgba(212, 175, 55, 0.1);
    border-radius: var(--radius-md);
    color: var(--color-accent-gold);
}

.card-body {
    color: var(--color-text-secondary);
}

.card-footer {
    margin-top: var(--space-4);
    padding-top: var(--space-4);
    border-top: 1px solid rgba(212, 175, 55, 0.1);
}

/* =================================================================
   STAT CARDS
   ================================================================= */

.stat-card {
    background: var(--color-primary-700);
    border: 1px solid rgba(212, 175, 55, 0.1);
    border-radius: var(--radius-lg);
    padding: var(--space-5);
}

.stat-card-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: var(--space-3);
}

.stat-label {
    font-size: var(--text-sm);
    color: var(--color-text-muted);
    text-transform: uppercase;
    letter-spacing: 0.05em;
}

.stat-icon {
    width: 32px;
    height: 32px;
    color: var(--color-accent-gold);
}

.stat-value {
    font-family: var(--font-display);
    font-size: var(--text-3xl);
    font-weight: var(--font-bold);
    color: var(--color-text-primary);
    margin-bottom: var(--space-2);
}

.stat-change {
    font-size: var(--text-sm);
    display: flex;
    align-items: center;
    gap: var(--space-1);
}

.stat-change.positive { color: var(--color-accent-green); }
.stat-change.negative { color: var(--color-accent-red); }

/* =================================================================
   BUTTONS
   ================================================================= */

.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-2);
    padding: var(--space-3) var(--space-5);
    font-family: var(--font-heading);
    font-size: var(--text-sm);
    font-weight: var(--font-semibold);
    border-radius: var(--radius-md);
    transition: all var(--transition-fast);
    cursor: pointer;
}

.btn-primary {
    background: var(--color-accent-gold);
    color: var(--color-primary-900);
}

.btn-primary:hover {
    background: var(--color-accent-gold-light);
    box-shadow: var(--shadow-glow-gold);
}

.btn-secondary {
    background: var(--color-primary-600);
    color: var(--color-text-primary);
    border: 1px solid rgba(212, 175, 55, 0.2);
}

.btn-secondary:hover {
    background: var(--color-primary-500);
    border-color: var(--color-accent-gold);
}

.btn-ghost {
    background: transparent;
    color: var(--color-text-secondary);
}

.btn-ghost:hover {
    background: rgba(212, 175, 55, 0.1);
    color: var(--color-accent-gold);
}

.btn-danger {
    background: var(--color-accent-red);
    color: white;
}

.btn-danger:hover {
    background: var(--color-accent-red-light);
}

.btn-sm {
    padding: var(--space-2) var(--space-3);
    font-size: var(--text-xs);
}

.btn-lg {
    padding: var(--space-4) var(--space-6);
    font-size: var(--text-base);
}

.btn-icon {
    /* 44x44 CSS px minimum touch target (WCAG 2.5.5) - was 40x40, which is
       the padded hit area for a 16px icon that docs/HUD-LAYOUT.md §7 calls
       out by name as under the minimum. */
    width: 44px;
    height: 44px;
    padding: 0;
}

.btn,
.btn-sm,
.btn-gold,
.btn-outline,
.btn-danger-outline,
.popup-tab {
    min-height: 44px;
}

/* =================================================================
   BADGES
   ================================================================= */

.badge {
    display: inline-flex;
    align-items: center;
    gap: var(--space-1);
    padding: var(--space-1) var(--space-2);
    font-size: var(--text-xs);
    font-weight: var(--font-semibold);
    border-radius: var(--radius-sm);
    text-transform: uppercase;
    letter-spacing: 0.05em;
}

.badge-gold {
    background: rgba(212, 175, 55, 0.2);
    color: var(--color-accent-gold);
}

.badge-cyan {
    background: rgba(6, 182, 212, 0.2);
    color: var(--color-accent-cyan);
}

.badge-green {
    background: rgba(5, 150, 105, 0.2);
    color: var(--color-accent-green);
}

.badge-red {
    background: rgba(220, 38, 38, 0.2);
    color: var(--color-accent-red);
}

/* =================================================================
   USER PROFILE WIDGET
   ================================================================= */

.user-profile {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    padding: var(--space-2) var(--space-3);
    border-radius: var(--radius-md);
    cursor: pointer;
    transition: background var(--transition-fast);
}

.user-profile:hover {
    background: rgba(212, 175, 55, 0.1);
}

.user-avatar {
    width: 40px;
    height: 40px;
    border-radius: 50%;
    background: var(--color-primary-600);
    border: 2px solid var(--color-accent-gold);
    object-fit: cover;
}

.user-info {
    display: flex;
    flex-direction: column;
}

.user-name {
    font-size: var(--text-sm);
    font-weight: var(--font-semibold);
    color: var(--color-text-primary);
}

.user-rank {
    font-size: var(--text-xs);
    color: var(--color-accent-gold);
    text-transform: uppercase;
}

/* =================================================================
   RESOURCE BAR
   ================================================================= */

.resource-bar {
    display: flex;
    align-items: center;
    gap: var(--space-4);
}

.resource-item {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-2) var(--space-3);
    background: var(--color-primary-700);
    border-radius: var(--radius-md);
}

.resource-icon {
    width: 20px;
    height: 20px;
}

.resource-icon.gold { color: var(--color-accent-gold); }
.resource-icon.energy { color: var(--color-accent-cyan); }
.resource-icon.materials { color: var(--color-accent-green); }
.resource-icon.bonds { color: var(--color-accent-bond); }

.resource-value {
    font-family: var(--font-display);
    font-size: var(--text-sm);
    font-weight: var(--font-bold);
    color: var(--color-text-primary);
}

/* =================================================================
   PROGRESS BARS
   ================================================================= */

.progress {
    height: 8px;
    background: var(--color-primary-600);
    border-radius: var(--radius-sm);
    overflow: hidden;
}

.progress-bar {
    height: 100%;
    background: var(--color-accent-gold);
    border-radius: var(--radius-sm);
    transition: width var(--transition-base);
}

.progress-bar.cyan { background: var(--color-accent-cyan); }
.progress-bar.green { background: var(--color-accent-green); }
.progress-bar.red { background: var(--color-accent-red); }

/* =================================================================
   NOTIFICATIONS / TOASTS
   ================================================================= */

.toast-container {
    position: fixed;
    /* Anchored top-left below .top-left-hud instead of sharing the
       top-right corner with the life bar - see docs/HUD-LAYOUT.md §4.4,
       which explicitly anticipates this constant being re-measured
       whenever the profile block's rendered height changes.

       Re-measured: .top-left-hud now stacks profile 42px + 3px gap +
       resource row ~28px + 3px gap + the .player-progress block (two
       ~11px rows, 3px gap, 5px padding top and bottom = ~35px, budgeted
       at 42px for font fallback) = ~118px worst case. Anchored at
       top: var(--space-4) (16px) that puts its bottom edge at ~134px,
       and it only shrinks at the 1024/640 breakpoints. 16 + 128 = 144px
       clears that with ~10px to spare in the worst case, and more at
       every narrower breakpoint. */
    top: calc(var(--space-4) + 128px);
    left: var(--space-4);
    z-index: var(--z-notification);
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
}

.toast {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    padding: var(--space-3) var(--space-4);
    background: var(--color-primary-700);
    border: 1px solid rgba(212, 175, 55, 0.2);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-lg);
    animation: slideIn 0.3s ease;
}

.toast.success { border-color: var(--color-accent-green); }
.toast.error { border-color: var(--color-accent-red); }
.toast.warning { border-color: var(--color-accent-orange); }
.toast.info { border-color: var(--color-accent-cyan); }

@keyframes slideIn {
    /* Was translateX(100%) -> 0 (entering from the right), matching the old
       top-right anchor. The container moved to top-left per §4.4, so the
       entry direction flips to match - entering from the left edge. */
    from {
        transform: translateX(-100%);
        opacity: 0;
    }
    to {
        transform: translateX(0);
        opacity: 1;
    }
}

/* Phone: top-left real estate is scarcer than the old top-right corner,
   so only the most recent toast stays visible; the rest queue instead of
   stacking down toward centre stage (docs/HUD-LAYOUT.md §5, Phone). */
@media (max-width: 640px) {
    .toast-container .toast:not(:first-child) {
        display: none;
    }
}

/* =================================================================
   LOADING STATES
   ================================================================= */

.loading-spinner {
    width: 24px;
    height: 24px;
    border: 2px solid var(--color-primary-600);
    border-top-color: var(--color-accent-gold);
    border-radius: 50%;
    animation: spin 0.8s linear infinite;
}

@keyframes spin {
    to { transform: rotate(360deg); }
}

.skeleton {
    background: linear-gradient(
        90deg,
        var(--color-primary-700) 25%,
        var(--color-primary-600) 50%,
        var(--color-primary-700) 75%
    );
    background-size: 200% 100%;
    animation: shimmer 1.5s infinite;
    border-radius: var(--radius-sm);
}

@keyframes shimmer {
    0% { background-position: 200% 0; }
    100% { background-position: -200% 0; }
}

/* =================================================================
   OVERSEER COMMUNICATION BOX - Speech Bubble Style
   ================================================================= */

.overseer-box {
    position: relative;
    width: clamp(220px, 25vw, 330px);
    background: rgba(10, 15, 26, 0.92);
    border: 1px solid var(--color-accent-gold);
    border-radius: var(--radius-md);
    padding: 12px;
    box-shadow: 0 0 20px rgba(212, 175, 55, 0.15),
                inset 0 0 30px rgba(0, 0, 0, 0.3);
}

.overseer-header {
    display: flex;
    align-items: center;
    /* The name moved into the portrait, so the badge is alone here
       and gets the full width rather than sharing it. */
    justify-content: flex-start;
    gap: 8px;
    margin-bottom: 8px;
    /* The badge is white-space: nowrap and the label has 2px of letter
       spacing, so on a narrow phone the header was wider than the box that
       contains it and "DEATH IMMINENT" ran off the right edge of the
       screen. Region B is width-capped in layout.css; this is the header
       agreeing to live inside that cap. */
    min-width: 0;
    /* Both label and badge are now at the 12px floor rather than 8.8px and
       7.2px, so on the narrowest phone the two can want more width than the
       box has. Wrapping is the only outcome that keeps both fully readable -
       truncating "EXPENDABLE" to "EXPEND..." loses the word, and that badge is
       the Overseer's verdict on the player, not decoration. It only wraps
       when it has to; at 390px and up it stays on one line. */
    flex-wrap: wrap;
    row-gap: 4px;
}

.overseer-label {
    flex-shrink: 0;
}

/* The Overseer's portrait.
 *
 * This was a 28px circular avatar in the header — a thumbnail beside a label.
 * That is fine for a static icon and useless for a living one: the source is
 * 720x720, so 28px threw away 98% of it, and the slow drift that makes him
 * read as awake is invisible at that size. He is meant to be a presence the
 * player is aware of, so the portrait leads the box rather than decorating it.
 *
 * SQUARE IS NOT A STYLE CHOICE. The video frame is 1280x720 but the picture
 * inside it is a 720x720 square at x 280..1000, black pillarbox either side.
 * With object-fit: cover a square container shows exactly that span — full
 * bleed, and the "Veo" watermark at x 1200..1260 falls outside it. Any wider
 * ratio reintroduces the pillarbox (and past ~1.56:1, the watermark itself).
 * Taller-than-wide is safe but crops his face. Keep it 1:1. */
/* Deliberately frameless: no border, no background, no clipping. The presence
 * component draws its own circular ring and glow, and a square frame around
 * that reads as two competing borders — worse, `overflow: hidden` here clips
 * the component's ring into a stray arc at the corners. This element only
 * reserves the space; whatever mounts inside owns its own look. */
.overseer-portrait {
    position: relative;
    /* One width, so the dormant rule below can halve it without knowing
       which breakpoint set it. */
    /* The ceiling, not the width. A viewport-derived figure cannot know how
       wide the box actually is — region B caps it well below 42vw on a
       phone — so using it directly made the portrait 173px inside a 153px
       content box. An overflowing block cannot be centred by auto margins:
       they resolve to zero on the left and the excess is pushed right,
       which is what put him off-centre. */
    --portrait-w: clamp(120px, 13vw, 180px);
    width: 100%;
    max-width: var(--portrait-w);
    transition: width var(--transition-slow);
    aspect-ratio: 1 / 1;
    margin: 0 auto 10px;
    display: flex;
    align-items: center;
    justify-content: center;
}

.overseer-portrait > .overseer-portrait-still,
.overseer-portrait > video,
.overseer-portrait > .op-presence {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
}

/* The fallback carries the frame the component would otherwise have drawn, so
   losing the video changes what moves, not how he is presented. */
.overseer-portrait-still {
    border-radius: 12px;
    background: #05070d;
    /* A flat echo of the component's steel frame, so losing the video changes
       what moves and not how he is mounted. Deliberately simpler — this is the
       degraded path and it should not out-render the real one. */
    padding: 6px;
    background-image: linear-gradient(145deg, #4a515c, #767d88 12%, #363c45 38%,
                                      #2b3038 62%, #565d68 88%, #333942);
    box-shadow:
        inset 0 1px 0 rgba(255, 255, 255, 0.3),
        inset 0 -1px 0 rgba(0, 0, 0, 0.5),
        inset 0 0 0 1px rgba(0, 0, 0, 0.4);
}

/* Set only once the mount has demonstrably inserted a player — see
   mountPortrait() in overseer.js. Until then the still is what shows. */
.overseer-portrait.is-live .overseer-portrait-still {
    display: none;
}

/* Dormant: he has not spoken for a while, so he gets out of the way.
   Halving the WIDTH rather than scaling him means the box actually reflows
   smaller and the HUD gives the screen back — a transform would shrink the
   picture and leave the hole it came from. Set from overseer.js; see the
   idle timer there for what counts as inactive. */
.overseer-portrait.is-dormant {
    /* Absolute, because the box is min-content when dormant and a
       percentage would have nothing to resolve against — the circularity
       that collapsed the portrait to 72x36. */
    width: calc(var(--portrait-w) / 2);
    max-width: none;
}

/* The nameplate, directly ABOVE the portrait.
   It used to sit on the picture, which meant its legibility had to survive
   video whose brightness changes every second — hence the opaque plate it
   needed. Above the frame it is simply text on the panel, so the plate is gone
   and the face is unobstructed. */
.overseer-nameplate {
    display: block;
    margin-bottom: 6px;
    font-family: var(--font-display);
    font-size: var(--text-xs);
    font-weight: var(--font-bold);
    letter-spacing: 0.18em;
    text-align: center;
    color: var(--color-accent-gold);
}

.overseer-label {
    font-family: var(--font-display);
    /* 10.4px -> 12px floor. Orbitron caps at 2px of tracking were costing more
       width than the extra 1.6px of size does, so the tracking pays for it. */
    font-size: var(--text-xs);
    line-height: 1.2;
    color: var(--color-accent-gold);
    letter-spacing: 0.06em;
    text-transform: uppercase;
}

.overseer-badge {
    /* Centred under the portrait rather than shoved to the right
       edge: the portrait is centred, and when he shrinks a
       right-aligned chip leaves the whole left half of the box
       empty. */
    margin-inline: auto;
    /* The word "Status" above it was redundant — a one-word verdict in the
       Overseer's own chip is self-evidently his assessment, and the label was
       costing a line and widening the chip for nothing. */
    display: flex;
    align-items: center;
    justify-content: center;
    text-align: center;
    padding: 3px 8px;
    background: rgba(212, 175, 55, 0.15);
    color: var(--color-accent-gold);
    font-family: var(--font-display);
    /* 8.8px, and 7.2px on a phone - the smallest text anywhere in the HUD,
       set in caps with wide tracking. 12px floor, tracking halved to pay
       for it. */
    font-size: var(--text-xs);
    line-height: 1.25;
    font-weight: var(--font-bold);
    letter-spacing: 0.04em;
    text-transform: uppercase;
    border-radius: var(--radius-sm);
    /* Wraps rather than truncates. "Status: EXPENDABLE" is the longest
       string this ever carries and it does not fit a phone on one line;
       "STATUS: EXPEND..." loses the only word that matters, and that word
       is the Overseer's verdict on the player rather than decoration. */
    white-space: normal;
    min-width: 0;
}

/* Badge color states (set by updateLifeBar in app.js) */
.overseer-badge.badge-red {
    background: rgba(185, 28, 28, 0.25);
    /* Was --color-accent-red-light, which is 4.45:1 against this translucent
       fill composited over the box - under AA. This is 6.0:1. */
    color: var(--color-accent-red-bright);
}

.overseer-badge.badge-orange {
    background: rgba(234, 88, 12, 0.2);
    color: #f97316;
}

/* Bounded, not fixed. At a fixed 48px this held 2.4 lines of --text-sm, while
   the lines routinely run past 140 characters — so most of what the Overseer
   said was cut off, and the box is the only place several things are ever
   reported. It grows to five lines and scrolls beyond that, which is the
   varying height HUD-LAYOUT.md 4.4 already describes. */
.overseer-message {
    /* No min-height. The box used to hold 48px open whether or not he had
     anything to say, so an empty Overseer reserved three lines of screen
     for nothing. It now collapses to nothing and grows with the text,
     which also means the whole panel tracks the portrait as it shrinks
     and expands instead of standing at one height regardless. */
    max-height: 98px;
    overflow-y: auto;
    overflow-x: hidden;
    /* Both must be set HERE, not only on .overseer-text. The text is an
       inline span, so each line box is struck from THIS block's font-size and
       line-height and can only grow from there. With the block left at the
       inherited 16px while the span was 10px on a phone, the strut was 21.6px
       around 10px text — every message came out double-spaced. */
    font-size: var(--text-sm);
    line-height: 1.35;
    word-wrap: break-word;
    overflow-wrap: break-word;
}

/* Scrollbar styling for message area */
.overseer-message::-webkit-scrollbar {
    width: 3px;
}

.overseer-message::-webkit-scrollbar-track {
    background: transparent;
}

.overseer-message::-webkit-scrollbar-thumb {
    background: rgba(212, 175, 55, 0.3);
    border-radius: 2px;
}

.overseer-text {
    font-family: var(--font-heading);
    font-size: var(--text-sm);
    color: rgba(255, 255, 255, 0.85);
    /* Matches the block's strut above — two different values here produced
       lines whose spacing depended on which one won. */
    line-height: 1.35;
}

/* Typing cursor */
.overseer-text::after {
    content: '';
    display: inline;
    border-right: 2px solid var(--color-accent-gold);
    margin-left: 1px;
    animation: blink 0.8s infinite;
}

.overseer-text.cursor-hidden::after {
    display: none;
}

/* =================================================================
   LIFE BAR (Standalone)
   ================================================================= */

.life-bar-container {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-2) var(--space-3);
    background: linear-gradient(
        135deg,
        rgba(127, 29, 29, 0.2) 0%,
        rgba(5, 8, 15, 0.95) 50%,
        rgba(127, 29, 29, 0.15) 100%
    );
    border: 1px solid rgba(185, 28, 28, 0.8);
    border-radius: var(--radius-md);
    min-width: 264px;
    max-width: 330px;
    box-shadow:
        0 0 10px rgba(127, 29, 29, 0.3),
        inset 0 0 15px rgba(0, 0, 0, 0.5);
}

.life-bar-label {
    font-family: var(--font-display);
    font-size: var(--text-xs);
    line-height: 1.2;
    font-weight: var(--font-bold);
    color: var(--color-accent-red-bright);
    letter-spacing: 0.06em;
    /* The bar floats over the game canvas, whose brightness changes as the
       player pans, so the label carries its own separation rather than relying
       on the panel behind it. */
    text-shadow: 0 1px 3px rgba(0, 0, 0, 0.95), 0 0 8px rgba(0, 0, 0, 0.7);
}

.life-bar {
    flex: 1;
    min-width: 80px;
    height: 16px;
    background: rgba(0, 0, 0, 0.6);
    border-radius: var(--radius-sm);
    position: relative;
    overflow: hidden;
    border: 1px solid rgba(127, 29, 29, 0.4);
}

.life-bar-fill {
    height: 100%;
    background: linear-gradient(90deg, #7f1d1d, #dc2626);
    border-radius: var(--radius-sm);
    transition: width 1s ease;
    position: relative;
    /* Top highlight lifts the fill off the black track so the two are
       distinguishable even at the darker end of the state ramp. */
    box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.22);
}

/* Bright leading edge — makes the exact fill level readable at a glance
   instead of fading into the track. */
.life-bar-fill::after {
    content: '';
    position: absolute;
    top: 0;
    right: 0;
    width: 2px;
    height: 100%;
    background: rgba(255, 255, 255, 0.75);
    box-shadow: 0 0 6px rgba(255, 255, 255, 0.5);
}

.life-bar-fill.critical {
    animation: pulse-critical 1s ease-in-out infinite;
}

@keyframes pulse-critical {
    0%, 100% { opacity: 1; }
    50% { opacity: 0.6; }
}

.life-bar-text {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    font-family: var(--font-display);
    font-size: var(--text-xs);
    /* body's 1.6 line-height makes a 12px readout 19px tall inside a 16px
       track that clips - the percentage lost its ascenders and descenders. */
    line-height: 1;
    font-weight: var(--font-bold);
    color: #fff;
    /* Reads over both the filled and empty halves of the track, which can be
       any state colour from dark red to bright green. A tight dark rim plus a
       soft halo covers both without an outline that would look heavy at 12px. */
    text-shadow:
        0 1px 0 rgba(0, 0, 0, 0.95),
        0 -1px 0 rgba(0, 0, 0, 0.95),
        1px 0 0 rgba(0, 0, 0, 0.95),
        -1px 0 0 rgba(0, 0, 0, 0.95),
        0 0 7px rgba(0, 0, 0, 0.85);
    letter-spacing: 0.04em;
}

.life-bar-rate {
    font-family: var(--font-mono, monospace);
    /* Was overridden down to 10px on phone; holds at the floor now. */
    font-size: var(--text-xs);
    line-height: 1.2;
    white-space: nowrap;
    color: var(--color-accent-red-bright);
    text-shadow: 0 1px 3px rgba(0, 0, 0, 0.95), 0 0 8px rgba(0, 0, 0, 0.7);
}

/* Was #6b7280 at 0.7 opacity, which composites to 2.56:1 over the bar - the
   readout that tells the player their life drain has stopped was the least
   legible thing in region B, and a contrast audit cannot see it because the
   dimming is opacity rather than colour. The italic still carries the state. */
.life-bar-rate.rate-paused {
    color: var(--color-text-muted);
    font-style: italic;
}

/* =================================================================
   LIFE BAR COLOR STATES (based on percentage)
   Red: <25%, Orange: 25-50%, Yellow: 50-70%,
   Lime-green: 70-90%, Dark-green: 90-100%
   ================================================================= */

/* Red state: < 25% (critical) */
.life-bar-container.state-red {
    background: linear-gradient(135deg, rgba(127, 29, 29, 0.25) 0%, rgba(5, 8, 15, 0.95) 50%, rgba(127, 29, 29, 0.2) 100%);
    border-color: rgba(153, 27, 27, 0.9);
    animation: lifeBarPulseRed 1.5s ease-in-out infinite;
}

.life-bar-container.state-red .life-bar-label,
.life-bar-container.state-red .life-bar-rate {
    color: #f87171;
}

.life-bar-container.state-red .life-bar-fill {
    background: linear-gradient(90deg, #450a0a, #dc2626);
}

@keyframes lifeBarPulseRed {
    0%, 100% {
        box-shadow: 0 0 8px rgba(127, 29, 29, 0.3), inset 0 0 15px rgba(0, 0, 0, 0.5);
    }
    50% {
        box-shadow: 0 0 12px rgba(127, 29, 29, 0.4), inset 0 0 15px rgba(0, 0, 0, 0.5);
    }
}

/* Orange state: 25% - 50% */
.life-bar-container.state-orange {
    background: linear-gradient(135deg, rgba(124, 45, 18, 0.2) 0%, rgba(5, 8, 15, 0.95) 50%, rgba(124, 45, 18, 0.15) 100%);
    border-color: rgba(154, 52, 18, 0.8);
    animation: lifeBarPulseOrange 2.5s ease-in-out infinite;
}

.life-bar-container.state-orange .life-bar-label,
.life-bar-container.state-orange .life-bar-rate {
    color: #fb923c;
}

.life-bar-container.state-orange .life-bar-fill {
    background: linear-gradient(90deg, #431407, #ea580c);
}

@keyframes lifeBarPulseOrange {
    0%, 100% {
        box-shadow: 0 0 8px rgba(124, 45, 18, 0.25), inset 0 0 15px rgba(0, 0, 0, 0.5);
    }
    50% {
        box-shadow: 0 0 12px rgba(124, 45, 18, 0.35), inset 0 0 15px rgba(0, 0, 0, 0.5);
    }
}

/* Yellow state: 50% - 70% */
.life-bar-container.state-yellow {
    background: linear-gradient(135deg, rgba(113, 63, 18, 0.15) 0%, rgba(5, 8, 15, 0.95) 50%, rgba(113, 63, 18, 0.1) 100%);
    border-color: rgba(133, 77, 14, 0.7);
}

.life-bar-container.state-yellow .life-bar-label,
.life-bar-container.state-yellow .life-bar-rate {
    color: #facc15;
}

.life-bar-container.state-yellow .life-bar-fill {
    background: linear-gradient(90deg, #422006, #eab308);
}

/* Lime-green state: 70% - 90% */
.life-bar-container.state-lime {
    background: linear-gradient(135deg, rgba(63, 98, 18, 0.15) 0%, rgba(5, 8, 15, 0.95) 50%, rgba(63, 98, 18, 0.1) 100%);
    border-color: rgba(77, 124, 15, 0.7);
}

.life-bar-container.state-lime .life-bar-label,
.life-bar-container.state-lime .life-bar-rate {
    color: #a3e635;
}

.life-bar-container.state-lime .life-bar-fill {
    background: linear-gradient(90deg, #1a2e05, #84cc16);
}

/* Dark-green state: 90% - 100% */
.life-bar-container.state-green {
    background: linear-gradient(135deg, rgba(20, 83, 45, 0.15) 0%, rgba(5, 8, 15, 0.95) 50%, rgba(20, 83, 45, 0.1) 100%);
    border-color: rgba(21, 128, 61, 0.7);
}

.life-bar-container.state-green .life-bar-label,
.life-bar-container.state-green .life-bar-rate {
    color: #4ade80;
}

.life-bar-container.state-green .life-bar-fill {
    background: linear-gradient(90deg, #052e16, #22c55e);
}

/* =================================================================
   GAME CANVAS CONTAINER
   ================================================================= */

.game-canvas-container {
    flex: 1;
    background: var(--color-primary-900);
    border: 1px solid rgba(212, 175, 55, 0.1);
    border-radius: var(--radius-lg);
    position: relative;
    overflow: hidden;
    min-height: 400px;
}

.game-canvas-container canvas {
    display: block;
    width: 100%;
    height: 100%;
}

.canvas-loading {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--space-3);
    color: var(--color-text-muted);
}

.canvas-loading .loading-spinner {
    width: 40px;
    height: 40px;
}

.canvas-loading.hidden {
    display: none;
}

/* =================================================================
   ACTION BAR (Bottom)
   ================================================================= */

.action-bar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: var(--space-3) 0;
    margin-top: var(--space-4);
    border-top: 1px solid rgba(212, 175, 55, 0.1);
}

.action-bar-left,
.action-bar-right {
    display: flex;
    align-items: center;
    gap: var(--space-2);
}

/* =================================================================
   RESPONSIVE ADJUSTMENTS
   ================================================================= */

@media (max-width: 1024px) {
    /* Life bar stays visible at all screen sizes in full-screen game mode */

    .resource-bar {
        gap: var(--space-2);
    }

    /* .game-canvas-container / .action-bar / bare .action-bar-left,-right
       belong to an older, non-fullscreen layout not present in the current
       index.html (the live HUD uses .action-buttons-floating, guarded
       against wrapping in layout.css). Left in place, just folded into the
       single 1024 breakpoint along with everything else that used to fire
       at the now-retired 768px threshold. */
    .game-canvas-container {
        min-height: 300px;
    }

    .action-bar {
        flex-wrap: wrap;
        gap: var(--space-2);
    }

    .action-bar-left,
    .action-bar-right {
        flex-wrap: wrap;
    }

    /* Overseer box mobile adjustments */
    .overseer-box {
        width: clamp(180px, 30vw, 264px);
        padding: 10px;
    }

    /* Fill the box rather than sitting as an inset thumbnail inside it. On a
       phone the box is the narrowest thing on screen and the space below it
       is empty canvas, so holding the portrait well inside its own container
       wasted the one dimension there was plenty of. */
    .overseer-portrait {
        --portrait-w: clamp(120px, 42vw, 180px);
    }

    .overseer-text {
        font-size: var(--text-xs);
    }

    /* Font-size tracks .overseer-text at every breakpoint — see the note on
       the base rule; the block sets the strut, so they cannot diverge. */
    .overseer-message {
        font-size: var(--text-xs);
        max-height: 84px;
    }

    /* Life bar mobile adjustments */
    .life-bar-container {
        padding: var(--space-1) var(--space-2);
        gap: var(--space-1);
        min-width: 198px;
        max-width: 264px;
    }

    /* Was 14px. .life-bar-text now sits at the 12px floor instead of 9px and
       the track has overflow:hidden, so anything under 16px clips the
       percentage the bar exists to report. */
    .life-bar {
        height: 16px;
    }
}

@media (max-width: 640px) {
    /* Overseer box small mobile */
    .overseer-box {
        width: clamp(165px, 50vw, 240px);
        padding: 8px;
    }

    .overseer-portrait {
        --portrait-w: clamp(120px, 42vw, 180px);
        margin-bottom: 8px;
    }

    /* Region B carried the four smallest styles in the product here: the
       Overseer label at 8.8px, his badge at 7.2px, his own dialogue at 10px
       and the life percentage at 9px. All four now inherit the 12px floor
       from their base rules, so the phone block only spends width and height,
       never size. */
    .overseer-label {
        letter-spacing: 0.04em;
    }

    .overseer-badge {
        padding: 1px 5px;
        letter-spacing: 0.02em;
    }

    /* 12px at line-height 1.35 is a 16.2px line, so the five lines the base
       rule promises need 81px rather than 72. That is the one place a region
       B box grew on purpose - 9px, all of it downward. */
    .overseer-message {
        max-height: 81px;
    }

    .life-bar-container {
        padding: 4px 6px;
        gap: 4px;
        min-width: 154px;
        max-width: 220px;
    }

    .life-bar-label {
        display: none;
    }
}

/* =================================================================
   PROFILE POPUP
   ================================================================= */

.popup-overlay {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background: rgba(0, 0, 0, 0.85);
    display: flex;
    align-items: center;
    justify-content: center;
    z-index: var(--z-modal);
    opacity: 0;
    visibility: hidden;
    transition: opacity var(--transition-base), visibility var(--transition-base);
}

.popup-overlay.active {
    opacity: 1;
    visibility: visible;
}

.popup-overlay.hidden {
    display: none;
}

@media (prefers-reduced-motion: reduce) {
    /* Swap the opacity/visibility fade for an instant toggle - docs/HUD-LAYOUT.md §7. */
    .popup-overlay {
        transition: none;
    }
}

.popup-content {
    /* Under the gradient, not instead of it. A contrast audit resolves an
       effective background from background-color only, so a panel painted
       purely by background-image reports its ancestor's colour - here the
       85%-black overlay - and every colour decision made against it is made
       against the wrong number. Declaring the flat equivalent makes the
       measurement true and gives the panel a fallback fill. */
    background: linear-gradient(135deg, var(--color-primary-800) 0%, var(--color-primary-700) 100%);
    /* Must follow the shorthand - `background:` resets background-color to
       transparent, which is how the panel had none to begin with. */
    background-color: var(--color-primary-700);
    border: 1px solid var(--color-accent-gold);
    border-radius: var(--radius-lg);
    width: 90%;
    max-width: 500px;
    max-height: 80vh;
    overflow-y: auto;
    position: relative;
    box-shadow: 0 0 40px rgba(212, 175, 55, 0.2);
}

.popup-close {
    position: absolute;
    top: var(--space-4);
    right: var(--space-4);
    display: flex;
    align-items: center;
    justify-content: center;
    /* 44x44 touch target around the visible glyph (WCAG 2.5.5) */
    min-width: 44px;
    min-height: 44px;
    background: none;
    border: none;
    color: var(--color-text-muted);
    font-size: 24px;
    cursor: pointer;
    transition: color var(--transition-fast);
    z-index: 10; /* stacking within .popup-content, not the region ladder - exempt per §2 */
    line-height: 1;
}

.popup-close:hover {
    color: var(--color-accent-gold);
}

/* Popup Tabs */
.popup-tabs {
    display: flex;
    border-bottom: 1px solid rgba(212, 175, 55, 0.2);
    padding: 0 var(--space-5);
}

.popup-tab {
    background: none;
    border: none;
    color: var(--color-text-muted);
    font-family: var(--font-display);
    font-size: var(--text-sm);
    padding: var(--space-4) var(--space-5);
    cursor: pointer;
    position: relative;
    transition: color var(--transition-fast);
}

.popup-tab:hover {
    color: var(--color-text-primary);
}

.popup-tab.active {
    color: var(--color-accent-gold);
}

.popup-tab.active::after {
    content: '';
    position: absolute;
    bottom: -1px;
    left: 0;
    width: 100%;
    height: 2px;
    background: var(--color-accent-gold);
}

/* Popup Tab Content */
.popup-tab-content {
    display: none;
    padding: var(--space-6);
}

.popup-tab-content.active {
    display: block;
}

.popup-tab-content h2 {
    font-family: var(--font-display);
    font-size: var(--text-lg);
    color: var(--color-accent-gold);
    margin-bottom: var(--space-5);
    letter-spacing: 0.1em;
}

/* Profile Section */
.profile-section {
    margin-bottom: var(--space-6);
}

.profile-section label {
    display: block;
    font-family: var(--font-heading);
    font-size: var(--text-xs);
    color: var(--color-text-muted);
    margin-bottom: var(--space-2);
    letter-spacing: 0.1em;
    text-transform: uppercase;
}

.profile-name-row {
    display: flex;
    gap: var(--space-3);
}

.profile-input {
    flex: 1;
    background: rgba(0, 0, 0, 0.3);
    border: 1px solid rgba(212, 175, 55, 0.2);
    border-radius: var(--radius-md);
    padding: var(--space-3) var(--space-4);
    color: var(--color-text-primary);
    font-family: var(--font-body);
    font-size: var(--text-sm);
    transition: border-color var(--transition-fast);
}

.profile-input:focus {
    outline: none;
    border-color: var(--color-accent-gold);
}

.profile-hint {
    font-size: var(--text-xs);
    color: var(--color-text-muted);
    margin-top: var(--space-2);
}

/* Button with cost - Gold variant */
.btn-gold {
    background: linear-gradient(135deg, var(--color-accent-gold) 0%, var(--color-accent-gold-dark) 100%);
    /* Same reason as .popup-content, and this is the specific element the
       audit reported at 1.08:1 for "CHANGE (250 materials)". Checked by eye
       first, as docs/TYPE-LEGIBILITY.md requires for the sub-2:1 rows: it is
       near-black on gold and genuinely reads at 6.8:1. The declaration exists
       so the tool measures what the eye sees. Must follow the shorthand. */
    background-color: var(--color-accent-gold-dark);
    border: none;
    color: var(--color-primary-900);
    font-family: var(--font-heading);
    font-size: var(--text-xs);
    font-weight: var(--font-semibold);
    padding: var(--space-3) var(--space-4);
    border-radius: var(--radius-md);
    cursor: pointer;
    transition: transform var(--transition-fast), box-shadow var(--transition-fast);
    white-space: nowrap;
}

.btn-gold:hover {
    transform: translateY(-1px);
    box-shadow: var(--shadow-glow-gold);
}

.btn-gold:disabled {
    opacity: 0.5;
    cursor: not-allowed;
    transform: none;
}

.btn-gold .cost {
    opacity: 0.8;
    font-size: var(--text-xs);
    font-weight: var(--font-normal);
}

/* Profile Stats Grid */
.profile-stats {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: var(--space-4);
    padding: var(--space-4);
    background: rgba(0, 0, 0, 0.2);
    border-radius: var(--radius-md);
}

.profile-stats .stat-item {
    text-align: center;
}

.profile-stats .stat-label {
    display: block;
    font-family: var(--font-heading);
    font-size: var(--text-xs);
    color: var(--color-text-muted);
    letter-spacing: 0.1em;
    margin-bottom: var(--space-1);
}

.profile-stats .stat-value {
    font-family: var(--font-display);
    font-size: var(--text-lg);
    color: var(--color-text-primary);
    margin: 0;
}

/* Storyline List */
.storyline-list {
    display: flex;
    flex-direction: column;
    gap: var(--space-4);
}

.storyline-item {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: var(--space-4);
    background: rgba(0, 0, 0, 0.2);
    border: 1px solid rgba(212, 175, 55, 0.2);
    border-radius: var(--radius-md);
    transition: border-color var(--transition-fast);
}

.storyline-item:hover {
    border-color: var(--color-accent-gold);
}

.storyline-info h3 {
    font-family: var(--font-display);
    font-size: var(--text-sm);
    color: var(--color-text-primary);
    margin-bottom: var(--space-1);
}

.storyline-info p {
    font-size: var(--text-xs);
    color: var(--color-text-muted);
    margin: 0;
}

/* Outline button variant */
.btn-outline {
    background: transparent;
    border: 1px solid var(--color-accent-gold);
    color: var(--color-accent-gold);
    font-family: var(--font-heading);
    font-size: var(--text-xs);
    padding: var(--space-2) var(--space-4);
    border-radius: var(--radius-md);
    cursor: pointer;
    transition: all var(--transition-fast);
}

.btn-outline:hover {
    background: var(--color-accent-gold);
    color: var(--color-primary-900);
}

/* Danger outline button (for logout) */
/* --color-accent-red as text on the profile panel measures 3.07:1 - the audit
   reported 4.29:1 only because it was resolving the background as the
   85%-black overlay rather than the panel (now fixed above). Logout is the
   one destructive control in the game and it was the dimmest thing on the
   panel; the brighter red is 5.35:1 and still unmistakably a danger colour. */
.btn-danger-outline {
    background: transparent;
    border: 1px solid var(--color-accent-red-light);
    color: var(--color-accent-red-bright);
    font-family: var(--font-heading);
    font-size: var(--text-sm);
    font-weight: var(--font-semibold);
    padding: var(--space-3) var(--space-5);
    border-radius: var(--radius-md);
    cursor: pointer;
    transition: all var(--transition-fast);
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    width: 100%;
    justify-content: center;
}

.btn-danger-outline:hover {
    background: var(--color-accent-red);
    /* Was --color-primary-900: near-black on --color-accent-red is 3.97:1, so
       the button lost contrast the moment it was hovered. White is 4.61:1. */
    color: var(--color-text-primary);
}

/* Profile logout section */
.profile-logout {
    margin-top: var(--space-6);
    padding-top: var(--space-5);
    border-top: 1px solid rgba(212, 175, 55, 0.2);
}

/* Profile popup responsive */
@media (max-width: 640px) {
    .popup-content {
        width: 95%;
        max-height: 90vh;
    }

    .profile-name-row {
        flex-direction: column;
    }

    .btn-gold {
        width: 100%;
    }

    .storyline-item {
        flex-direction: column;
        align-items: flex-start;
        gap: var(--space-3);
    }

    .btn-replay {
        width: 100%;
    }
}

/* --- badge lines, and a box that only takes the room it needs ---------- */

.overseer-badge-value {
    font-size: var(--text-xs);
    font-weight: var(--font-bold);
    letter-spacing: 0.04em;
    line-height: 1.15;
}

/* An empty message contributes nothing — no height, and no gap above it.
   :empty is exact here because overseer.js clears textContent rather than
   writing a space, which is also why presentTop() had to learn to clear the
   box at all. */
.overseer-message:has(.overseer-text:empty) {
    max-height: 0;
    margin: 0;
}

.overseer-header {
    transition: margin-bottom var(--transition-base);
}

/* Nothing to say: the header stops reserving the gap that separated it from
   text that is not there. */
.overseer-box:has(.overseer-text:empty) .overseer-header {
    margin-bottom: 0;
}

/* Dormant: the box narrows to what is actually in it.
   Halving the portrait without narrowing its container just moved the wasted
   space from the picture to the panel around it. fit-content shrinks to the
   widest remaining child — the badge — and margin-left: auto keeps the whole
   thing pinned to the right edge as it does. The max-width still applies, so
   a long message expands it again. */
.overseer-box:has(.overseer-portrait.is-dormant) {
    width: fit-content;
    min-width: 0;
    margin-left: auto;
}

.overseer-box:has(.overseer-portrait.is-dormant) .overseer-header {
    justify-content: center;
}

/* =================================================================
   REGION A — LEVEL BADGE + COMPACT PROGRESS STRIP
   Research and the decision behind this shape: docs/PROGRESSION-DISPLAY.md.
   Markup in game/index.html, painted by game/js/progression.js.

   Two rules of this file's ownership, stated because the selectors are
   split across three stylesheets and that is not obvious:

   - The .player-progress CONTAINER keeps its chrome from
     game/css/hudPanels.css (fill, gold hairline, radius, blur, the
     240 / 210 / 100% width steps). Nothing here restates it, so region
     A's WIDTH is unchanged by this pass and §6's clear-centre figure
     moves only because the block got shorter.
   - hudPanels.css's .pp-row / .pp-key / .pp-track / .pp-fill / .pp-value
     rules are now dead — the two-row markup they styled is gone. They
     are left in place because that file belongs to another lane.

   HEIGHT is the whole point. Before: two rows of key + track + value,
   3px gap, 5px padding = ~43px. After: one row = ~26px.
   ================================================================= */

/* -----------------------------------------------------------------
   The level, in the player card

   Moved out of the progress block entirely. A level is an identity
   fact — it belongs next to the callsign and the rank, the way Clash
   Royale's King Level and PSN's level number sit on the profile — not
   next to a bar, where it was paying for a whole 15px row to say one
   number that changes a few times a week.

   Costs no height: the card is 28px of avatar against 30px of name +
   rank, and the badge is shorter than both.
   ----------------------------------------------------------------- */

.user-profile-floating .user-info {
    /* Pushes the badge to the trailing edge of the card and lets the
       callsign ellipsis rather than shove it out. Same specificity as
       game/css/layout.css's rule; components.css is loaded after it.

       `contain: inline-size` is the load-bearing one and it is not
       decoration. .top-left-hud is a shrink-to-fit inline-flex column, so
       its width is the widest child's MAX-CONTENT — which means a long
       callsign plus the new badge would widen the whole of region A, and
       region A's width is what §6's clear-centre figure is computed from.
       Containment drops this element's intrinsic contribution to zero: the
       card takes the width the progress block below it already sets, and
       the callsign ellipsis inside it. Measured at 412: without this,
       region A goes 218.7px -> capped; with it, unchanged. */
    contain: inline-size;
    flex: 1 1 0;
    min-width: 0;
}

.user-profile-floating .user-name,
.user-profile-floating .user-rank {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.user-level {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    gap: 0;
    padding: 2px 7px;
    border: 1px solid rgba(212, 175, 55, 0.45);
    border-radius: var(--radius-sm);
    background: rgba(212, 175, 55, 0.12);
    font-family: var(--font-display);
    color: var(--color-accent-gold);
    line-height: 1;
}

/* `.user-level-key` — the "LVL" word — is gone, and so is the 9px rule that
   carried it.

   Two normative documents say no rendered text below 12px "ever"
   (variables.css's --text-xs note, and docs/TYPE-LEGIBILITY.md rule 1), and
   this was the one survivor, justified in a comment as a legend rather than a
   readout. That is a real distinction and it was still the wrong call: the
   gold badge with a number in it reads as a level without being told, so the
   word was three letters of sub-floor type buying nothing.

   Deleted rather than raised, and the rule is NOT amended. A stated, audited
   floor with one documented exception is a floor with a precedent attached to
   it. */

.user-level-num {
    font-size: 15px;
    font-weight: var(--font-bold);
    letter-spacing: 0.01em;
}

/* -----------------------------------------------------------------
   The progress strip — two meters, one row

   Both progressions stay on screen at all times. Nothing is behind a
   tap: see the doc's §4 on what progressive disclosure would cost.

   Each meter is  lead + rail + to-go number. The rail shows how far
   ALONG; the number shows how far TO GO. Non-redundant on purpose —
   printing 70% next to a 70%-full bar spends a whole column saying
   what the bar already said.
   ----------------------------------------------------------------- */

.pp-strip {
    display: flex;
    align-items: center;
    gap: 8px;
    min-width: 0;
}

.pp-meter {
    display: flex;
    align-items: center;
    gap: 4px;
    /* Equal halves, both allowed to shrink to the rail's floor. */
    flex: 1 1 0;
    min-width: 0;
}

.pp-lead {
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    min-width: 16px;
    height: 16px;
    font-family: var(--font-display);
    font-size: 13px;
    line-height: 1;
    font-weight: var(--font-bold);
    letter-spacing: 0.02em;
}

.pp-meter-xp .pp-lead { color: var(--color-accent-gold); }
.pp-meter-rank .pp-lead { color: var(--color-accent-cyan); }

.pp-rail {
    position: relative;
    display: block;
    /* The rail is the only elastic column in the strip; the lead and the
       number are fixed, because they are the parts that must not clip.
       28px is the floor at which a two-colour bar still separates fill
       from trough at a glance. Measured at 412: each rail gets ~40px. */
    flex: 1 1 auto;
    min-width: 28px;
    height: 6px;
    background: rgba(10, 15, 26, 0.9);
    border: 1px solid rgba(212, 175, 55, 0.15);
    border-radius: 3px;
    overflow: hidden;
}

.pp-rail-fill {
    display: block;
    height: 100%;
    width: 0;
    border-radius: 2px;
    transition: width var(--transition-slow);
}

.pp-meter-xp .pp-rail-fill {
    background: linear-gradient(90deg, var(--color-accent-gold-dark), var(--color-accent-gold-light));
}

.pp-meter-rank .pp-rail-fill {
    background: linear-gradient(90deg, var(--color-accent-cyan), var(--color-accent-cyan-light));
}

.pp-togo {
    flex-shrink: 0;
    font-family: var(--font-mono, monospace);
    /* 13px, not the 12px the rest of region A uses: this is the single
       readout the whole block exists for and 12px was called too small
       in dense panels. Large values are abbreviated (1.2k) rather than
       given more columns — see formatCompact in progression.js. */
    font-size: 13px;
    line-height: 1.15;
    /* --color-text-secondary, never --color-text-muted: muted measures
       4.03:1 on this fill and this is a persistent readout. The same
       rule the expedition strip's count follows. */
    color: var(--color-text-secondary);
    font-variant-numeric: tabular-nums;
}

/* At top rank the second rail is decorative — it is always full — so it
   is desaturated to a flat cyan rather than dimmed. State is never
   expressed with opacity here (docs/TYPE-LEGIBILITY.md). */
.player-progress.is-max-rank .pp-meter-rank .pp-rail-fill {
    background: var(--color-accent-cyan);
}

.player-progress.is-max-rank .pp-togo {
    color: var(--color-accent-cyan-light);
}

@media (max-width: 640px) {
    /* Region A is 56% of the viewport here — ~219px at 412 — and the
       block spans all of it. The strip loses the gap between meters and
       the rails absorb the rest; nothing is dropped, because dropping
       one of two progressions is the one outcome this pass must not
       produce. */
    .pp-strip { gap: 5px; }
    .pp-meter { gap: 3px; }

    /* Last thing to give, and only below 640. Region A is ~190px wide at a
       360px viewport and the two numbers plus two leads are ~120px of that
       on their own. A 22px rail is a coarse bar, but the number beside it
       is exact and 13px — and the alternative was dropping one of the two
       progressions, which is the outcome this pass must not produce. */
    .pp-rail { min-width: 22px; }
}

@media (prefers-reduced-motion: reduce) {
    /* hudPanels.css's §7 block names .pp-fill, which no longer exists. */
    .pp-rail-fill {
        transition: none;
    }

    /* The life bar's two warning pulses were the only animation in the product
       that ignored this preference — infinite, 1.5s and 2.5s, in the corner a
       player has to read status from. The Overseer's scrub is suppressed
       correctly; these were missed.

       DELETION rather than replacement, and only because the state does not
       depend on the motion: `state-red` and `state-orange` already carry
       themselves in the border colour, the fill colour and the label. The
       spec's word is "removes, reduces, OR REPLACES", so anywhere the motion
       IS the signal, substitute a cross-fade instead of doing this. */
    .life-bar-container.state-red,
    .life-bar-container.state-orange {
        animation: none;
    }
}

/* --- items added 2026-09-09 --------------------------------------------- */

/* Dormant: min-content, not fit-content.
   fit-content sizes to max-content, which is the badge's full unwrapped width
   ("EXPENDABLE"), so the box stayed as wide as the widest status word while
   the portrait inside it had halved — the padding looked enormous because the
   box was sized for something that was no longer there. min-content sizes to
   the longest single word instead, which is what actually has to fit. */
.overseer-box:has(.overseer-portrait.is-dormant) {
    width: min-content;
}

/* The life bar now lives in region A, under the player card, so it stops
   being pinned to the right and stretches with its new column. */
.top-left-hud .life-bar-container {
    margin-left: 0;
    width: 100%;
    max-width: none;
    min-width: 0;
}

/* Per-structure handbook dot. Sits inside the row button, so its own click is
   stopped in buildMenu.js before the row can start a placement. */
.bm-help {
    flex-shrink: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 22px;
    height: 22px;
    margin-left: 6px;
    border: 1px solid rgba(212, 175, 55, 0.45);
    border-radius: 50%;
    font-family: var(--font-display);
    font-size: var(--text-xs);
    font-weight: var(--font-bold);
    line-height: 1;
    color: var(--color-accent-gold);
    background: rgba(212, 175, 55, 0.1);
    cursor: help;
}

.bm-help:hover,
.bm-help:focus-visible {
    background: rgba(212, 175, 55, 0.25);
    border-color: var(--color-accent-gold);
}

/* The whole box reads as one column centred on him. The message was
   left-aligned against a centred portrait, which made the panel look like two
   unrelated things stacked. */
.overseer-box {
    text-align: center;
}

.overseer-message {
    text-align: center;
}

/* =================================================================
   TROOP-LOSS CONFIRMATION  (js/troopLossModal.js)

   The dialog shown before a disassembly destroys units the player
   cannot get back. It reuses .popup-overlay / .popup-content for the
   backdrop, geometry and gold frame; only the inside is new.

   Two rules from docs/TYPE-LEGIBILITY.md are load-bearing here rather
   than incidental:

   - Nothing under 12px. The smallest thing in this dialog is the
     options list at 13px, because it is the part that says what each
     button will actually do.
   - No state expressed as opacity. Every line is at full alpha; the
     "quiet" line is quiet by colour, on a measured tier. A faded
     sentence explaining an irreversible loss is the exact failure the
     audit found in "NO FACILITY" at 1.61:1.

   Red is --color-accent-red-dark, not --color-accent-red-light: white
   on the light red measures 3.58:1, which is what made the alert badge
   the worst contrast row in the HUD.
   ================================================================= */

.tl-dialog {
    padding: var(--space-6);
    max-width: 460px;
    /* Red rather than the shared gold, because this frame is the one
       that means "something is about to be destroyed". */
    border-color: var(--color-accent-red-bright);
    box-shadow: 0 0 40px rgba(220, 38, 38, 0.25);
}

.tl-title {
    margin: 0 0 var(--space-3);
    font-size: 20px;
    line-height: 1.25;
    color: var(--color-accent-red-bright);
    letter-spacing: 0.02em;
}

.tl-lead {
    margin: 0 0 var(--space-3);
    font-size: 15px;
    line-height: 1.5;
    color: var(--color-text-primary);
}

.tl-lead strong {
    color: var(--color-text-gold);
}

/* The server's own sentence. Boxed so it reads as the system speaking
   rather than as more of the paragraph above it. */
.tl-warning {
    margin: 0 0 var(--space-4);
    padding: var(--space-3);
    font-size: 14px;
    line-height: 1.5;
    color: var(--color-text-primary);
    background: rgba(185, 28, 28, 0.18);
    border-left: 3px solid var(--color-accent-red-bright);
    border-radius: var(--radius-sm);
}

.tl-warning:empty {
    display: none;
}

.tl-options {
    margin: 0 0 var(--space-5);
    padding-left: var(--space-5);
    font-size: 13px;
    line-height: 1.55;
    color: var(--color-text-secondary);
}

.tl-options li + li {
    margin-top: var(--space-2);
}

.tl-options strong {
    color: var(--color-text-primary);
}

.tl-actions {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-3);
    justify-content: flex-end;
}

.tl-btn {
    /* 44px minimum touch target (WCAG 2.5.5), same as .popup-close. */
    min-height: 44px;
    padding: 0 var(--space-5);
    font-family: inherit;
    font-size: 14px;
    font-weight: 600;
    color: var(--color-text-primary);
    background: var(--color-primary-600);
    border: 1px solid var(--color-primary-500);
    border-radius: var(--radius-md);
    cursor: pointer;
    transition: background var(--transition-fast), border-color var(--transition-fast);
}

.tl-btn:hover {
    background: var(--color-primary-500);
}

.tl-btn:focus-visible {
    outline: 2px solid var(--color-accent-cyan-light);
    outline-offset: 2px;
}

.tl-btn-danger {
    background: var(--color-accent-red-dark);
    border-color: var(--color-accent-red-bright);
}

.tl-btn-danger:hover {
    background: var(--color-accent-red);
}

/* The destructive button goes last in the DOM and stays last visually,
   so on a narrow screen the wrap puts "Go back" under the thumb first. */
@media (max-width: 420px) {
    .tl-actions {
        flex-direction: column-reverse;
    }

    .tl-btn {
        width: 100%;
    }
}

/* Dormant: the frame and nothing else.
   He has said nothing for a minute, so the panel around him is chrome for
   content that is not there — the name, the verdict, the empty message line
   and the box itself all go, leaving the steel frame in the corner. Everything
   returns the moment he speaks, because these are all display/visibility
   rules rather than anything that has to be rebuilt. */
.overseer-box:has(.overseer-portrait.is-dormant) {
    padding: 0;
    background: none;
    border: none;
    box-shadow: none;
}

.overseer-box:has(.overseer-portrait.is-dormant) .overseer-nameplate,
.overseer-box:has(.overseer-portrait.is-dormant) .overseer-header,
.overseer-box:has(.overseer-portrait.is-dormant) .overseer-message {
    display: none;
}

/* The portrait carried a bottom margin to separate it from the header that is
   no longer there. */
.overseer-box:has(.overseer-portrait.is-dormant) .overseer-portrait {
    margin-bottom: 0;
}

/* ---------------------------------------------------------------------------
   Rental confirmation. The dialog that stands between a player and spending
   War Bonds, which are bought with real money — so it is the one screen that
   most needs to look like the game rather than like the browser. It replaced
   window.confirm(); see js/rentConfirm.js for the rules it follows.
   Reuses .popup-overlay / .popup-content, as troopLossModal does.
   --------------------------------------------------------------------------- */
.rc-dialog {
    max-width: 380px;
    text-align: left;
}

.rc-kicker {
    margin: 0 0 2px;
    font-family: var(--font-display, 'Orbitron', sans-serif);
    font-size: var(--text-xs);
    letter-spacing: 1.2px;
    text-transform: uppercase;
    color: var(--color-accent-bond, #ece7da);
}

.rc-title {
    margin: 0 0 8px;
    font-family: var(--font-display, 'Orbitron', sans-serif);
    font-size: var(--text-lg);
    color: var(--color-text-primary, #e2e8f0);
}

.rc-lead {
    margin: 0 0 14px;
    font-size: var(--text-sm);
    line-height: 1.45;
    color: var(--color-text-secondary, #94a3b8);
}

/* The ledger: cost, holdings, and what is left. A price on its own is not a
   decision — what a player wants to know is what they will have afterwards.
   Printed, not rendered. This is the block that states the sum being taken, so
   it is the block drawn as the instrument itself: off-white stock, black
   impression, engraved hairlines between the entries. Everything around it
   stays the dark panel it sits in, which is what makes the slip read as a
   separate physical thing laid on the table rather than as another section. */
.rc-ledger {
    margin: 0 0 14px;
    padding: 11px 13px;
    border: 1px solid var(--color-bond-rule, rgba(10, 11, 13, 0.22));
    border-radius: 2px;
    background: var(--color-bond-paper, #ece7da);
    box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.55);
}

.rc-line {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    padding: 4px 0;
}

.rc-line dt {
    font-size: var(--text-sm);
    color: var(--color-bond-ink-soft, rgba(10, 11, 13, 0.62));
}

.rc-line dd {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    margin: 0;
    font-family: var(--font-display, 'Orbitron', sans-serif);
    font-size: var(--text-sm);
    color: var(--color-bond-ink, #0a0b0d);
}

/* The rule above the closing figure, as a ledger has under its column. */
.rc-after {
    margin-top: 4px;
    padding-top: 8px;
    border-top: 1px solid var(--color-bond-rule, rgba(10, 11, 13, 0.22));
}

.rc-after dd { font-weight: 700; }

.rc-cost { font-weight: 700; }
/* Red ink on paper, not screen red — #ef4444 on off-white is a glare rather
   than a warning, and the warning is the point. */
.rc-short { color: #9b1c1c; }

.rc-save {
    margin-left: 6px;
    padding: 1px 5px;
    border-radius: 2px;
    font-size: 12px;
    color: var(--color-bond-paper, #ece7da);
    background: var(--color-bond-ink, #0a0b0d);
}

.rc-bond {
    width: 14px;
    height: 14px;
    flex-shrink: 0;
}

.rc-note {
    margin: 0 0 16px;
    font-size: 12px;
    line-height: 1.45;
    color: var(--color-text-secondary, #94a3b8);
}

.rc-actions {
    display: flex;
    gap: 8px;
}

.rc-btn {
    flex: 1 1 0;
    padding: 11px 12px;
    font-family: var(--font-display, 'Orbitron', sans-serif);
    font-size: var(--text-sm);
    letter-spacing: 0.5px;
    border-radius: 4px;
    border: 1px solid var(--color-primary-600, #24406b);
    background: var(--color-primary-800, #101c33);
    color: var(--color-text-primary, #e2e8f0);
    cursor: pointer;
}

/* The paying button is the same stock as the slip above it, so the eye follows
   paper from the sum to the act of agreeing to it. Cancel stays a dark panel
   control — the two are told apart by material, not only by position. */
.rc-btn-buy {
    border-color: var(--color-bond-rule, rgba(10, 11, 13, 0.22));
    background: var(--color-bond-paper, #ece7da);
    color: var(--color-bond-ink, #0a0b0d);
    box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.55);
    font-weight: 700;
}

.rc-btn-buy:hover,
.rc-btn-buy:focus-visible {
    background: var(--color-bond-paper-lit, #f7f4ec);
    border-color: var(--color-bond-ink, #0a0b0d);
}

/* Unaffordable. It stops being paper: the instrument is not on offer, and
   leaving it printed would say it is. Not opacity — TYPE-LEGIBILITY rule 2,
   state must not be carried by it. */
.rc-btn-buy[disabled] {
    cursor: not-allowed;
    border-color: var(--color-primary-600, #24406b);
    background: var(--color-primary-700, #1a2744);
    color: var(--color-text-secondary, #94a3b8);
    box-shadow: none;
}

/* The "do not ask me again" box on a bond confirm. Sits under the ledger and
   above the buttons, because it changes what the NEXT purchase does, not this
   one — and it is only ever armed when the player actually pays. */
.rc-remember {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    margin: 0.75rem 0 0;
    font-size: 0.84rem;
    color: var(--color-text-dim, #94a3b8);
    cursor: pointer;
}

.rc-remember input {
    width: 1rem;
    height: 1rem;
    accent-color: var(--color-gold, #d4af37);
    cursor: pointer;
}

/* --- The War Bonds chip -------------------------------------------------
   It sat fourth in a row of four identical resource readouts, so bonds read
   as a fourth gatherable the player was failing to collect — which is exactly
   what the comment in index.html was written to prevent, and the chip drifted
   into it anyway. It also measured 27x23, under WCAG 2.2 AA's 24px floor, and
   its only label lived in a `title` attribute that never fires on a phone.

   The paper treatment is the answer already in the product: bonds are drawn
   as a document, not as a hue, and a paper chip among dark ones is instantly
   not-a-resource without borrowing gold, cyan or green (taken by the three
   real resources) or red (danger). */
/* Specificity note: `.resource-bar-floating .resource-item` in layout.css is
   two classes and lands later in the cascade, so a single-class rule here lost
   to it and the chip kept the dark resource fill. Matched rather than fought
   with !important — the bar owns the row's geometry and should keep doing so;
   only this one chip's surface is being claimed. */
.resource-bar-floating .resource-item-bonds,
.resource-item-bonds {
    background: var(--color-bond-paper);
    border: 1px solid var(--color-bond-ink);
    color: var(--color-bond-ink);
    /* 44 tall including padding, so the target meets the token the project
       already defines and does not apply here. */
    min-height: var(--touch-target);
    padding-left: var(--space-2);
    padding-right: var(--space-2);
    gap: var(--space-1);
}

.resource-item-bonds .resource-icon,
.resource-item-bonds .resource-value {
    color: var(--color-bond-ink);
}

/* A shop needs a way in that says so. The chip was one of only two entrances
   to the storefront in the entire codebase, and it looked like a meter. */
.resource-item-bonds::after {
    content: '+';
    font-family: var(--font-display);
    font-weight: var(--font-bold);
    font-size: var(--text-sm);
    line-height: 1;
    padding-left: var(--space-1);
    border-left: 1px solid var(--color-bond-ink-soft);
    color: var(--color-bond-ink);
}

/* The life bar is tappable — it is the only route to the survival panel and
   its daily objectives — and measured 26px tall. Same treatment: the hit area
   grows, the bar does not move. */
.life-bar-container {
    position: relative;
}

.life-bar-container::after {
    content: '';
    position: absolute;
    top: 50%;
    left: 0;
    right: 0;
    height: max(100%, var(--touch-target));
    transform: translateY(-50%);
    z-index: 0;
}

/* The life bar's badge sits on the bar itself, which is the only route to the
   survival panel and the daily objectives behind it. */
.life-bar-container { position: relative; }

.life-bar-container .hud-badge {
    position: absolute;
    top: -6px;
    right: -4px;
    z-index: 1;
}

/* --- The Overseer's relay ----------------------------------------------
   A panel covers his card from the chin down at phone size, and the beats
   worth having fire precisely when a panel is open, because that is where
   the player performs the verb he is remarking on.

   Styled as HIM, not as a toast. The gold rule and the display face are the
   same devices his card uses; a toast is the game's plain instrumental voice
   and borrowing it would make him sound like the status bar. */
.overseer-relay {
    position: fixed;
    left: var(--space-3);
    right: var(--space-3);
    /* Above the region-D panel slot, so the thing that covers him is exactly
       the thing this sits on top of. */
    bottom: calc(var(--region-d-max-height, 60vh) + var(--space-2));
    z-index: var(--z-panel);

    padding: var(--space-2) var(--space-3);
    background: var(--color-primary-900);
    border-left: 3px solid var(--color-accent-gold);
    border-radius: var(--radius-sm);
    box-shadow: 0 6px 20px rgba(0, 0, 0, 0.55);

    font-family: var(--font-body);
    font-size: var(--text-xs);
    line-height: 1.5;
    color: var(--color-text-secondary);
    /* Long enough to read, short enough not to become a second panel. */
    max-height: 4.5em;
    overflow: hidden;
}

@media (prefers-reduced-motion: no-preference) {
    .overseer-relay { animation: relayIn var(--transition-base) ease-out; }
}

@keyframes relayIn {
    from { opacity: 0; transform: translateY(4px); }
    to   { opacity: 1; transform: none; }
}

/* --- Motion, in the three places it earns its place ---------------------
   The product had almost none: two animations on an idle base screen, one of
   them the Overseer's video scrub. That is not automatically wrong, and the
   answer is not more of it — motion pays when something CHANGES IDENTITY OR
   POSITION, because the eye can track a thing that moved instead of having to
   re-find a thing that teleported. Everywhere else it is cost without return.

   Durations come from the tokens that already exist. --transition-fast /
   -base / -slow at 150/250/400ms already sit on the same ladder every major
   platform publishes, so nothing new is invented here.

   Reduced motion REPLACES rather than deletes, which is what the spec
   actually says — "removes, reduces, or replaces". A value that ticks becomes
   a value that sets; a settle becomes a flash. The information survives; only
   the movement goes. The one place deletion is right is the life pulse, and
   only because border, fill and label already carry that state. */

/* 1. A resource value that CHANGED. A number that swaps tells you nothing; a
      number that moves tells you it moved and roughly by how much. */
.resource-value.is-changed {
    animation: resourceTick var(--transition-base) ease-out;
}

@keyframes resourceTick {
    0%   { transform: translateY(-3px); opacity: 0.55; }
    100% { transform: none; opacity: 1; }
}

/* 2. A structure that has just been COMMITTED to the ground. This is also the
      answer to placement having had no confirmation at all beyond a toast. */
.placement-banner.is-committed {
    animation: placementSettle var(--transition-fast) ease-out;
}

@keyframes placementSettle {
    0%   { transform: translateX(-50%) scale(1.03); }
    100% { transform: translateX(-50%) scale(1); }
}

/* 3. A panel LEAVING. `hpSlideUp` already exists for the entrance and is the
      right length; panels simply vanished, so the eye had nowhere to follow
      them to and the next screen arrived without a transition. */
.hud-panel.is-closing {
    animation: hpSlideDown var(--transition-fast) ease-in forwards;
}

@keyframes hpSlideDown {
    from { transform: none; opacity: 1; }
    to   { transform: translateY(12px); opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
    /* Replacements, not removals. */
    .resource-value.is-changed {
        animation: resourceFlash var(--transition-fast) ease-out;
    }
    .placement-banner.is-committed { animation: none; }
    .hud-panel.is-closing {
        animation: hpFade var(--transition-fast) ease-in forwards;
    }
}

html.reduce-motion .resource-value.is-changed {
    animation: resourceFlash var(--transition-fast) ease-out;
}
html.reduce-motion .hud-panel.is-closing {
    animation: hpFade var(--transition-fast) ease-in forwards;
}

/* Opacity only: the spec is explicit that a change of colour, blur or opacity
   is not "motion animation" at all, because nothing changes size, shape or
   position. This is the substitution MDN itself gives as the worked example. */
@keyframes resourceFlash {
    0%   { opacity: 0.5; }
    100% { opacity: 1; }
}

@keyframes hpFade {
    from { opacity: 1; }
    to   { opacity: 0; }
}
