ui/survivalhud.razor.scss
// SURVIVAL HUD
//
// Offsets mirror cl_hud_t5.lua's own anchors, converted from its 1920-wide
// reference to px here — the ScreenPanel is set to ConsistentHeight scaling, so
// these stay put across resolutions the same way the original's *scale does.
//
// ⚠️ Default font throughout. The original uses "Black Ops 1" for mainfont and
// smallfont; those are addon font files we cannot reference yet. Sizes and
// weights are picked to survive the swap without relayout.

.hud
{
    position: absolute;
    width: 100%;
    height: 100%;
    pointer-events: none;          // never eat clicks — the tool gun fires through it
    font-family: Inter;            // the shipped face; stands in for Black Ops 1
    color: white;

    // ── BOTTOM LEFT — health ─────────────────────────────────────────────────
    //
    // The original does have a left-hand health bar — it is what
    // nz_hud_health_style 1 switches to (wr = 34*scale in cl_hud_t5.lua) — so
    // this is the skin's other supported side, not an invention.

    .left
    {
        position: absolute;
        left: 40px;
        bottom: 60px;
        flex-direction: column;
    }

    .left
    {
        gap: 10px;
    }

    // One row per meter: icon, then bar. Both icons occupy the SAME width so
    // the two bars line up on their left edge — a 2px stagger between stacked
    // bars is the kind of thing that looks broken without being obviously wrong.
    .meter
    {
        flex-direction: row;
        align-items: center;
        gap: 10px;
    }

    .hp-icon,
    .boot,
    .banana,
    .armor-icon,
    .charge-icon,
    .focus-icon,
    .streak-icon
    {
        width: 26px;
        justify-content: center;
        align-items: center;
    }

    // Medical cross. Sized and coloured to read as a label for the bar, not as
    // a button — it is never interactive.
    .hp-icon
    {
        font-size: 34px;
        font-weight: 700;
        color: rgba( 235, 235, 235, 0.9 );
        text-shadow: 0px 2px 5px rgba( 0, 0, 0, 0.9 );

        // Nudge: a "+" glyph sits high in its line box, so it reads as
        // floating above the bar's centre unless pulled down.
        position: relative;
        top: -2px;
    }

    // Boot, built from two blocks — a shaft and a sole forming an L.
    //
    // ⚠️ NOT a glyph. Inter ships no boot, and an emoji (👢) falls back to a
    // blank box or tofu depending on the platform's font chain. Two divs always
    // render, at any size, on any machine.
    .boot
    {
        height: 26px;
        position: relative;

        .shaft,
        .foot
        {
            position: absolute;
            background-color: rgba( 235, 235, 235, 0.9 );
        }

        .shaft
        {
            left: 5px;
            top: 3px;
            width: 9px;
            height: 14px;
        }

        // Wider than the shaft and rounded at the toe, which is what reads as
        // a boot rather than an L.
        .foot
        {
            left: 5px;
            top: 15px;
            width: 17px;
            height: 7px;
            border-radius: 0px 3px 3px 0px;
        }
    }

    // ── BOTTOM RIGHT — round beside the weapon ───────────────────────────────

    .round
    {
        color: rgba( 235, 60, 45, 1 );          // the CoD blood red
        text-shadow: 0px 2px 6px rgba( 0, 0, 0, 0.9 );

        // ── between rounds ───────────────────────────────────────────────
        //
        // White and breathing until the next number appears. The colour change
        // is the signal that the wave is over; the pulse is what stops it
        // reading as a HUD element that simply went pale.
        //
        // ⚠️ `ease-in-out` IS the sine. A linear 20→100→20 has a visible corner
        // at each end where the direction reverses; ease-in-out eases through
        // both extremes, which is what a sine wave looks like and what was
        // asked for. Writing the curve by hand in keyframe steps would be a
        // worse approximation of the same thing.
        &.prep
        {
            color: rgba( 255, 255, 255, 1 );
            animation: round-breathe 1.6s infinite;
            animation-timing-function: ease-in-out;
        }

        // ── changing number ──────────────────────────────────────────────
        //
        // ⛔ TWO SEPARATE ANIMATIONS, NOT A CROSS-FADE. The glyph is swapped in
        // C# at the moment opacity reaches zero, so these never overlap — one
        // element goes fully out, changes, and comes back. A true cross-fade
        // would need two elements stacked, and at this size the overlap would
        // read as a double image rather than a blend.
        //
        // ⚠️ `forwards` on the fade-out so it HOLDS at zero. Without it the
        // number snaps back to full opacity on the last frame, which is a
        // flash exactly where the swap happens.
        &.fade-out
        {
            animation: round-fade-out 0.35s;
            animation-fill-mode: forwards;
            animation-timing-function: ease-in;
        }

        &.fade-in
        {
            animation: round-fade-in 0.35s;
            animation-timing-function: ease-out;
        }

        // Nudged down and left. Offsets rather than margins on purpose — the
        // weapon block sits in the same flex row and must not move, and a
        // margin here would reflow it.
        position: relative;
        top: 10px;
        right: 14px;

        // Tally and digits are DIFFERENT SIZES on purpose. Five strokes at digit
        // size runs far wider than a two-digit number and unbalances the corner;
        // the original sidesteps this by drawing a 150px tally sprite against
        // per-digit sprites.
        &.tally
        {
            font-size: 64px;
            font-weight: 500;
            letter-spacing: 6px;
        }

        &.digits
        {
            font-size: 72px;
            font-weight: 700;
            letter-spacing: 2px;
        }
    }

    .right
    {
        position: absolute;
        right: 40px;
        bottom: 48px;
        flex-direction: column;
        align-items: flex-end;

        // Lifts the points clear of the round/weapon row. The weapon block is
        // the anchor — it must not move — so all the spacing is spent above it.
        gap: 62px;
    }

    // Round and weapon share a row so the weapon keeps its exact position and
    // the round grows leftward from it. align-items: flex-end puts the round's
    // baseline on the ammo's, rather than floating it against the taller box.
    .lower
    {
        flex-direction: row;
        align-items: flex-end;
        gap: 26px;
    }

    // Team roster ---------------------------------------------------------------
    // ⚠️ THE SAME READING ORDER AS `.points` — a face, then a number — one size
    // down, with the name to the LEFT of the face so the two blocks line up on
    // their right edge. `.right` is `align-items: flex-end`, so everything here
    // grows leftward from the edge the big score sits on.
    //
    // ⛔ AND IT LIVES ABOVE `.points` IN THE MARKUP. See the razor comment: the
    // popup panel's offsets are hardcoded against this column's bottom, so any
    // height added BELOW the score moves the score and not the popups.
    .mate
    {
        flex-direction: row;
        align-items: center;
        gap: 10px;
        margin-bottom: 10px;

        // So the "+N" can be positioned against the ROW rather than the column.
        position: relative;

        .portrait
        {
            width: 42px;
            height: 42px;
            border-radius: 4px;
            overflow: hidden;
            background-color: rgba( 0, 0, 0, 0.35 );

            .face { width: 100%; height: 100%; }
        }

        .pts
        {
            font-size: 26px;
            color: rgba( 255, 235, 150, 0.85 );

            // Same reasoning as `.points .value`'s fixed width, at this scale:
            // right-aligned digits in a proportional face jitter the row's left
            // edge on every award, which drags the face with it.
            min-width: 92px;
            text-align: right;
            font-variant-numeric: tabular-nums;
        }

        // ⚠️ RISES OVER THE NUMBER IT BELONGS TO. Anchored to this row's top-right,
        // which is where `.pts` is — the previous `right: 100px` put it out to the
        // left of the face, over nothing.
        .gain
        {
            position: absolute;
            top: -16px;
            right: 4px;
            font-size: 20px;
            color: rgba( 190, 255, 170, 1 );
            animation: mate-gain 1.4s ease-out forwards;
            pointer-events: none;
        }
    }

    @keyframes mate-gain
    {
        0%   { opacity: 0; transform: translateY( 6px ); }
        15%  { opacity: 1; transform: translateY( 0px ); }
        70%  { opacity: 1; transform: translateY( -6px ); }
        100% { opacity: 0; transform: translateY( -14px ); }
    }

    // Points ------------------------------------------------------------------
    .points
    {
        flex-direction: row;
        align-items: center;
        gap: 10px;

        // Kept so anything absolutely positioned inside the points block
        // measures against IT rather than the whole bottom-right column.
        // (The "+50" popups used to live here and needed this; they are their
        // own panel now — see PointsPopupHud.)
        position: relative;


        .value
        {
            // ⛔ FIXED WIDTH, OR THE WHOLE BLOCK MOVES ON EVERY AWARD. `.right`
            // is `align-items: flex-end`, so `.points` is right-aligned and grows
            // LEFTWARD. This div renders the live score in a proportional face,
            // where "1" is narrower than "8" — so almost any change in the number
            // changes this width, which moves the block's left edge, which moves
            // the portrait, which moves every popup anchored to it.
            //
            // ⚠️ THAT IS THE "THEY ALL STUTTER AND GO LEFT" REPORT. It was never
            // the animation: the numbers were being carried sideways by the
            // counter reflowing underneath them. Nothing about the popups could
            // have fixed it.
            //
            // 160px holds six digits at this size, so the layout is stable across
            // any realistic score.
            min-width: 160px;
            text-align: right;

            font-size: 46px;
            font-weight: 700;
            color: rgba( 255, 232, 150, 1 );     // warm gold, as in BO1
            text-shadow: 0px 2px 6px rgba( 0, 0, 0, 0.9 );
        }

        // Square frame around the portrait image. Set PortraitImage on the
        // component to use custom art; empty falls back to the Steam avatar.
        .portrait
        {
            width: 52px;
            height: 52px;
            overflow: hidden;
            background-color: rgba( 0, 0, 0, 0.55 );
            border: 2px solid rgba( 255, 232, 150, 0.75 );

            .face
            {
                width: 100%;
                height: 100%;

                // Fill the square whatever the source aspect is, rather than
                // letterboxing an avatar inside its own frame.
                background-size: cover;
                background-position: center;
            }
        }
    }

    // Health ------------------------------------------------------------------
    // 256x24 in the original; kept proportional.
    // ⚠️ The empty state has to be legible. At 0 health the fill has no width,
    // so all that is left is this backing — and a translucent black box on a
    // black-and-grey map is invisible. It read as "the health bar disappeared"
    // rather than "you have no health". Opaque backing plus a light border, so
    // the EMPTY bar is still obviously a bar.
    // Armor -------------------------------------------------------------------
    //
    // ⚠️ THE THREE SEGMENTS TOGETHER ARE 256px — the same as .health and .stamina.
    // The container is pinned to that width and the segments flex-grow into it, so
    // the gaps come OUT of the total rather than being added on top. Sizing each
    // segment to a hard 82px instead would make the row 266px wide once the two 5px
    // gaps were counted, and the stack would sit visibly ragged against the two bars
    // below it.
    .armor
    {
        width: 256px;
        height: 18px;
        flex-direction: row;
        gap: 5px;

        .seg
        {
            flex-grow: 1;
            height: 100%;
            background-color: rgba( 0, 0, 0, 0.8 );
        }

        // Owned. Bright border and a live fill.
        .seg.unlocked
        {
            border: 2px solid rgba( 255, 255, 255, 0.3 );
        }

        // ⚠️ NOT HIDDEN, DIMMED. A tier you have not bought still draws, because
        // the empty slot is what tells you the vest can be upgraded at all. Hiding
        // them would make a tier-1 player's bar look like a full bar a third the
        // width, which reads as damage rather than as headroom.
        .seg.locked
        {
            border: 2px solid rgba( 255, 255, 255, 0.10 );
            background-color: rgba( 0, 0, 0, 0.45 );
        }

        .fill
        {
            height: 100%;
            // Steel blue, so it is not confused with health's white or stamina's
            // pale blue at a glance.
            background-color: rgba( 120, 175, 225, 0.95 );
            transition: width 0.12s ease-out;
        }
    }

    // A shield, built from divs. Same reasoning as the boot: Inter ships no shield
    // glyph and an emoji falls back to tofu. A square with its bottom corners
    // rounded hard reads as a shield at this size.
    .armor-icon
    {
        height: 26px;
        justify-content: center;
        align-items: center;

        .shield
        {
            width: 15px;
            height: 17px;
            background-color: rgba( 235, 235, 235, 0.9 );
            border-radius: 3px 3px 8px 8px;
        }
    }

    // Plates in reserve, right of the bar.
    .plates
    {
        flex-direction: row;
        align-items: center;
        gap: 5px;
        margin-left: 4px;

        .plate-icon
        {
            width: 9px;
            height: 14px;
            background-color: rgba( 120, 175, 225, 0.95 );
            border-radius: 2px;
        }

        .plate-count
        {
            font-size: 17px;
            font-weight: 700;
            color: rgba( 235, 235, 235, 0.95 );
            text-shadow: 0px 2px 5px rgba( 0, 0, 0, 0.9 );
        }

        // Carrying none: dimmed rather than hidden, so the slot's existence is
        // still visible and "go find plates" is legible.
        &.empty
        {
            .plate-icon { background-color: rgba( 255, 255, 255, 0.22 ); }
            .plate-count { color: rgba( 235, 235, 235, 0.45 ); }
        }
    }

    .health
    {
        width: 256px;
        height: 18px;
        background-color: rgba( 0, 0, 0, 0.8 );
        border: 2px solid rgba( 255, 255, 255, 0.3 );

        // ⚠️ POSITIONED, so Leech's overheal below lays itself over the fill rather than somewhere up the tree.
        position: relative;

        // ⚠️ LEECH'S OVERHEAL (2026-10-04): gold over the fill, from the left, on the bar's own scale.
        .over
        {
            position: absolute;
            left: 0px;
            top: 0px;
            height: 100%;
            background-color: rgba( 255, 206, 84, 0.95 );
        }

        .fill
        {
            height: 100%;
            background-color: rgba( 235, 235, 235, 0.95 );
            transition: width 0.12s ease-out, background-color 0.2s linear;
        }

        &.low .fill
        {
            background-color: rgba( 220, 45, 35, 1 );
        }
    }

    // Stamina ----------------------------------------------------------------
    // Same dimensions as health. The two are told apart by their ICON and FILL
    // COLOUR rather than by size — keep these three values in step with
    // .health above, or the stacked pair goes ragged down the right edge.
    // ⛔ A BANANA FROM ONE ROTATED BLOCK, for the reason `.boot` above already documents: the
    // shipped font has no banana glyph and an emoji falls back to a blank box. A curved crescent is
    // not worth a background image, so this is a rounded rectangle tilted off-axis — enough to read
    // as fruit next to a cross and a boot, which is all the label has to do.
    .banana
    {
        height: 26px;
        position: relative;

        .peel
        {
            position: absolute;
            left: 6px;
            top: 3px;
            width: 9px;
            height: 20px;

            background-color: rgba( 255, 237, 115, 0.95 );

            // Fat on one side, thin on the other, which is what makes it read as a
            // crescent rather than as a capsule.
            border-radius: 2px 7px 3px 7px;
            transform: rotate( 22deg );
        }
    }

    // ⚠️ THE SAME 256px WIDTH AND 18px HEIGHT AS HEALTH AND STAMINA, so the three stack as one
    // column. A bar that agreed on colour but not on geometry would read as a different kind of
    // thing sitting underneath them.
    .place-charge
    {
        width: 256px;
        height: 18px;
        background-color: rgba( 0, 0, 0, 0.8 );
        border: 2px solid rgba( 255, 255, 255, 0.3 );

        .fill
        {
            height: 100%;

            // The placeables' own light yellow, so the bar and the thing it places are
            // obviously the same system.
            background-color: rgba( 255, 237, 115, 0.9 );
            transition: width 0.1s linear, background-color 0.2s linear;
        }

        // ⛔ FULL IS THE ONLY STATE WORTH A COLOUR CHANGE, and it is the opposite of health and
        // stamina, where the highlighted state is the BAD one. Here full means the key will work,
        // which is the single thing the player is waiting to be told — so the border lights too,
        // not just the fill, because a fill at 100% has no edge left to distinguish it.
        &.ready
        {
            border-color: rgba( 255, 237, 115, 0.9 );

            .fill
            {
                background-color: rgba( 255, 252, 205, 1 );
            }
        }

        // ⚠️ PAUSED: one of yours is out and kills do not charge the next (2026-10-04). Dim, so it reads as waiting rather
        // than broken.
        &.paused
        {
            border-color: rgba( 255, 255, 255, 0.12 );

            .fill
            {
                background-color: rgba( 255, 237, 115, 0.3 );
            }
        }
    }

    .stamina
    {
        width: 256px;
        height: 18px;
        background-color: rgba( 0, 0, 0, 0.8 );
        border: 2px solid rgba( 255, 255, 255, 0.3 );

        .fill
        {
            height: 100%;
            background-color: rgba( 190, 205, 225, 0.9 );
            transition: width 0.1s linear, background-color 0.2s linear;
        }

        // Empty, not merely low: this is the moment sprint is actually blocked,
        // so it earns a colour change the way "nearly empty" does not.
        &.low .fill
        {
            background-color: rgba( 225, 150, 40, 1 );
        }
    }

    // Weapon ------------------------------------------------------------------
    .weapon
    {
        flex-direction: column;
        align-items: flex-end;

        // The weapon name and, when the gun has an ammo mod, its badge beside it.
        //
        // ⚠️ ROW-REVERSE, NOT ROW. This column is right-aligned, so the name's
        // right edge is the line everything else on the column lines up against. Putting
        // the badge after the name in flow order would push the name left by the badge's
        // width and only when a mod is held — the name would visibly shift when you
        // bought one. Reversed, the badge hangs off the left and the name never moves.
        .name-row
        {
            flex-direction: row-reverse;
            align-items: center;
            justify-content: flex-start;
            gap: 7px;
        }

        .name
        {
            font-size: 22px;
            font-weight: 600;
            letter-spacing: 2px;

            // ⚠️ MIRRORED IN C# AS SurvivalHud.NeutralNameColor, which now writes this
            // inline on every build. This declaration is the fallback for the frame before the
            // first build; the two must agree or a Common weapon changes colour on spawn.
            color: rgba( 225, 225, 225, 0.9 );
            text-shadow: 0px 2px 5px rgba( 0, 0, 0, 0.9 );
        }

        // The ammo mod badge — upstream's own HUD artwork.
        //
        // ⚠️ THE DEFAULT STATE IS DIM, because a mod on cooldown is the common
        // case for a 10s recharge. The lit state is the exception and so it is the one
        // that gets full opacity, not the other way round.
        //
        // ⚠️ SIZED TO THE CAP HEIGHT of the 22px name, not to the name's line
        // box. The source icons are 227x227 with generous transparent padding, so a badge
        // matching the full line height reads as much smaller than the text next to it.
        .mod-badge
        {
            width: 26px;
            height: 26px;
            opacity: 0.45;
            transition: opacity 0.15s ease-out;

            // Ready: full brightness.
            &.ready { opacity: 1; }

            // ⛔ AN UNBUILT MOD IS DESATURATED AND DIMMER whatever else it is, so a
            // catalogue entry cannot be mistaken for a working mod that happens to be
            // recharging. Five of the six are still catalogue entries.
            &.unwired
            {
                opacity: 0.3;
                filter: saturate( 0 );
            }
        }

        .ammo
        {
            flex-direction: row;
            align-items: baseline;
            gap: 6px;

            // ⚠️ THE SALVAGE CHIP OPTS OUT OF THE BASELINE. This row is
            // baseline-aligned so the 46px clip and the smaller reserve sit on one
            // line — but the salvage chip is a nested flex box whose own baseline
            // comes from its text, which would hang it off the bottom of a 46px
            // number. Centring it against that number is what reads as "beside".
            //
            // ⚠️ EXTRA RIGHT MARGIN ON TOP OF THE 6px GAP, because the chip is
            // a different kind of thing from the two numbers it now shares a row
            // with. Without it the salvage count reads as part of the ammo figure.
            .salvage
            {
                align-self: center;
                margin-right: 6px;
            }

            // ⚠️ THE WEAPON TECH TAG (2026-10-04): Select Fire's mode and the per-class augments' readouts, beside the clip.
            .techtag
            {
                align-self: center;
                margin-right: 6px;
                font-size: 15px;
                font-weight: 700;
                letter-spacing: 2px;
                color: rgba( 150, 200, 255, 0.85 );

                &.flash { color: rgba( 255, 255, 255, 1 ); }
                &.ready { color: rgba( 255, 200, 80, 1 ); }
            }
            text-shadow: 0px 2px 6px rgba( 0, 0, 0, 0.9 );

            // Magazine is the number you act on, so it carries the weight.
            .clip
            {
                font-size: 46px;
                font-weight: 700;
            }

            .sep
            {
                font-size: 28px;
                color: rgba( 255, 255, 255, 0.45 );
            }

            .reserve
            {
                font-size: 28px;
                font-weight: 500;
                color: rgba( 255, 255, 255, 0.65 );
            }
        }

        .reloading
        {
            font-size: 16px;
            font-weight: 600;
            letter-spacing: 3px;
            color: rgba( 255, 200, 80, 1 );
            text-shadow: 0px 2px 5px rgba( 0, 0, 0, 0.9 );
        }

        // Grenades, under the ammo.
        //
        // ⚠️ Right-aligned with the ammo above it rather than centred — this corner
        // reads bottom-up along a right edge, and a centred row would break that line.
        //
        // ⚠️ Deliberately smaller and dimmer than the clip count. It is equipment you
        // check between fights, not the number you watch while shooting; matching the
        // ammo's weight would give the corner two things competing to be read first.
        .grenades
        {
            flex-direction: row;
            align-items: center;
            justify-content: flex-end;
            margin-top: 6px;
            gap: 6px;

            .nade-icon
            {
                font-size: 20px;
                color: rgba( 120, 200, 120, 0.9 );
                text-shadow: 0px 2px 5px rgba( 0, 0, 0, 0.9 );
            }

            .nade-count
            {
                font-size: 26px;
                font-weight: bold;
                color: rgba( 255, 255, 255, 0.85 );
                text-shadow: 0px 2px 5px rgba( 0, 0, 0, 0.9 );
            }

            // ⛔ THE COOK STATE HAS TO BE LOUD. A grenade whose fuse is running in
            // your hand kills you in five seconds; this is the only warning, so it
            // goes red and pulses rather than shading a little warmer.
            &.cooking
            {
                .nade-icon, .nade-count
                {
                    color: rgba( 255, 90, 70, 1 );
                }

                animation: nade-pulse 0.4s infinite;
            }
        }
    }
}

@keyframes nade-pulse
{
    0%   { opacity: 1; }
    50%  { opacity: 0.35; }
    100% { opacity: 1; }
}


// ⚠️ 20% to 100%, as asked — the low end is deliberately NOT 0. A number that
// vanishes completely between rounds reads as the HUD dropping out; keeping a
// fifth of it visible makes it a pulse rather than a blink.
@keyframes round-breathe
{
    0%   { opacity: 0.2; }
    50%  { opacity: 1; }
    100% { opacity: 0.2; }
}

@keyframes round-fade-out
{
    0%   { opacity: 1; }
    100% { opacity: 0; }
}

@keyframes round-fade-in
{
    0%   { opacity: 0; }
    100% { opacity: 1; }
}

// ⛔ THE CLOSING BRACE ABOVE WAS MISSING, and it cost every rule that follows. A
// `@keyframes` block only accepts percentage selectors, so the parser hit `.charge` and
// reported `Expected %` - then swallowed `.charge`, `.charge-icon`, `.focus`, `.focus-mult`,
// `.focus-icon` and `.streak-icon` into the keyframes body, where none of them applied to
// anything. The trigger-charge bar, the Focus bar and both their icons have been UNSTYLED
// since the day they were added: 26px content-sized boxes rather than 256x18 bars.
//
// ⚠ THE ENGINE SAID SO, ONCE, AT STARTUP: "Skipping malformed rule in
// /ui/survivalhud.razor.scss: Expected % [:563]". It scrolled past unread while the bar was
// diagnosed by eye from a screenshot instead - INSTRUCTIONS.md §7 exactly, and §6 on top of
// it. `read_console` with no filter would have answered this in one call.

    // Trigger charge (Double Tap M3) ------------------------------------------
    //
    // ⚠️ SAME 256x18 AS HEALTH AND STAMINA, and it has to be — the three stack in one
    // column and any difference in width shows as a ragged right edge. Keep these three
    // values in step with `.health` and `.stamina` above, which already carry that warning
    // for each other.
    //
    // ⛔ THE FILL HAS NO COLOUR HERE. Both the width and the background are written from
    // `SurvivalHud.TickTriggerCharge` every frame, because the charge moves too fast for
    // this panel's deliberately coarse BuildHash to track. A colour declared here would be
    // overwritten on the first frame and would only ever be visible for that one frame,
    // which is worse than no declaration at all — it would read as the authority.
    .charge
    {
        width: 256px;
        height: 18px;
        background-color: rgba( 0, 0, 0, 0.8 );
        border: 2px solid rgba( 255, 255, 255, 0.3 );

        .fill
        {
            height: 100%;

            // ⚠️ NO `transition` ON WIDTH, UNLIKE HEALTH'S 0.12s. Health changes in steps
            // — a hit lands and the bar jumps — so easing it reads as smoothing. This one
            // is already continuous at frame rate, and a transition chasing a value that
            // moves every frame lags permanently behind it: the bar would read empty while
            // the gun was still hitting hard.
            //
            // ⚠️ The COLOUR does transition, because that changes in three discrete steps
            // and a hard snap between bands reads as a flicker.
            transition: background-color 0.15s linear;
        }
    }

    // A ringed dot — a charge/cell mark, built from two blocks for the same reason the
    // boot is (Inter ships no suitable glyph and emoji fall back to tofu).
    .charge-icon
    {
        height: 26px;
        position: relative;
        justify-content: center;
        align-items: center;

        .ring
        {
            width: 17px;
            height: 17px;
            border-radius: 9px;
            border: 2px solid rgba( 235, 235, 235, 0.9 );
        }

        // ⚠️ ABSOLUTE AND CENTRED ON THE RING, not a child of it — a child would be laid
        // out inside the ring's content box and pushed off-centre by its own border.
        .dot
        {
            position: absolute;
            left: 9px;
            top: 9px;
            width: 8px;
            height: 8px;
            border-radius: 4px;
            background-color: rgba( 235, 235, 235, 0.9 );
        }
    }
    // Deadshot Focus (M4) -----------------------------------------------------
    //
    // ⚠️ NARROWER THAN THE OTHER METERS, at 200px against their 256, because this row
    // carries a MULTIPLIER LABEL to its right and the whole left column has to keep one
    // right edge. 200 + gap + the label lands on the same 256 the bars above use.
    //
    // ⛔ THE FILL HAS NO COLOUR HERE, same as `.charge`. Both the width and the background
    // are written from `SurvivalHud.TickFocus`; a colour declared here would be overwritten
    // on the first frame and would only ever be visible for that one frame, which is worse
    // than no declaration because it reads as the authority.
    .focus
    {
        // ⛔ 256 x 18, THE SAME AS `.health` AND `.stamina`, AND THE LABEL MOVED INSIDE
        // TO PAY FOR IT. The note above records why this was 200: 200 + gap + a 46px label
        // came to the same 256 the bars above use, so the whole left column kept one right
        // edge. Widening the bar with the label still outside would have pushed this one row
        // 46px past every other - so the label is now absolutely positioned inside the bar's
        // right end. Same 256 edge, full-size bar, label still readable.
        width: 256px;
        height: 18px;

        // ⚠ NEEDED BY THE LABEL, which is `position: absolute` against this box. Without
        // it the label resolves against the whole HUD and lands nowhere near the bar.
        position: relative;
        background-color: rgba( 0, 0, 0, 0.8 );
        border: 2px solid rgba( 255, 255, 255, 0.3 );

        .fill
        {
            height: 100%;

            // ⚠️ A transition HERE but not on the charge bar, and the difference is real:
            // Focus moves in DISCRETE STEPS (one headshot kill), so easing reads as the bar
            // filling. The charge bar is already continuous at frame rate and a transition
            // would lag permanently behind it.
            transition: width 0.18s ease-out, background-color 0.2s linear;
        }
    }

    // ⚠️ TABULAR-ISH BY FIXED WIDTH rather than by font feature. The label runs from "×1"
    // to "×5" and back to "×1"; letting it size to content would shift the whole row
    // sideways on every kill.
    // ⛔ `.focus-mult` REMOVED, NOT LEFT AS DEAD CSS. The Focus bar carries no number any
    // more, so the rule matched nothing — and an unused selector in this file is exactly what
    // hid two unclosed `@keyframes` for weeks: rules that never applied and never complained.
    // If a figure comes back, it comes back with a fresh rule and a reason.

    // A stacked chevron pair — "building up", and visually distinct from the charge ring
    // so the two augment bars are never mistaken for each other at a glance.
    .focus-icon
    {
        height: 26px;
        position: relative;

        .chev
        {
            position: absolute;
            width: 13px;
            height: 4px;
            background-color: rgba( 235, 235, 235, 0.9 );
            left: 7px;
        }

        // ⚠️ Two bars rather than two rotated Vs. A rotate would need transform support
        // this layout system has only been proven to have for the salvage diamond, and a
        // pair of stacked dashes reads as "stacking" perfectly well.
        .chev.a { top: 8px; }
        .chev.b { top: 15px; width: 9px; left: 9px; }
    }
    // Vigor Rush Killstreak (M4) ----------------------------------------------
    //
    // ⚠️ THE BAR ITSELF REUSES `.focus`, because it IS the same object — a streak with a
    // multiplier label. Only the icon differs, so two streak bars stacked together stay
    // tellable apart. A third copy of the bar rules is how one of them drifts.
    .streak-icon
    {
        height: 26px;
        justify-content: center;
        align-items: center;

        // A solid square against Focus's stacked dashes — the most distinguishable shape
        // available without depending on transform support.
        .pip
        {
            width: 13px;
            height: 13px;
            background-color: rgba( 235, 235, 235, 0.9 );
        }
    }

// ── PERKS — bottom centre ────────────────────────────────────────────────────
//
// A single centred row that grows outward as perks are bought.
//
// ⚠️ CENTRED BY THE PARENT'S FLEX, and it works here where it did NOT for the
// Wunderfizz panel, the powerup banner or the perk grid. The difference is the
// CHILDREN: those cases centred an ABSOLUTELY POSITIONED child, which opts out
// of the parent's flex entirely. These icons are in normal flow, so
// justify-content applies to them normally.
//
// ⛔ NO NEGATIVE-MARGIN TRICK HERE, deliberately. That fix needs a KNOWN width —
// this row's width changes with every perk bought, so a fixed margin would only
// be centred at one perk count and drift at every other.
.perks
{
    position: absolute;
    bottom: 24px;
    left: 0px;
    width: 100%;

    flex-direction: row;
    align-items: center;
    justify-content: center;

    .perk
    {
        width: 44px;
        height: 44px;
        margin-left: 5px;
        margin-right: 5px;

        // Sits over gameplay, so it needs to read against a bright floor as well
        // as a dark one — a soft dark drop keeps the edges defined either way.
        text-shadow: 0px 2px 6px rgba( 0, 0, 0, 0.9 );
    }
}

// ── NAPALM IGNITION ──────────────────────────────────────────────────────────
//
// Horizontally centred, sat BELOW the crosshair, and faded out over 2s to match
// the original's `fadefac = (napalmdecay - CurTime()) / 2`.
//
// ⛔ THE FADE IS CSS, NOT C#. Driving opacity from the remaining time would mean
// rebuilding this whole panel every frame the icon is visible — and it is only
// ever visible mid-firefight, when the HUD can least afford it. The panel emits
// the element once and the animation runs itself.
//
// ⚠️ NOT ON THE CROSSHAIR, BY REQUEST. The original draws it dead centre, which
// puts a 64px icon over whatever you are currently shooting at — exactly the
// moment it fires. Half the size and dropped to 66% of screen height: still on
// the vertical axis where the eye already is, clear of the reticle, and above the
// bottom HUD furniture.
//
// ⚠️ THE NEGATIVE MARGINS ARE HALF THE BOX, so they have to move WITH the size.
// They are what actually places the icon — `left`/`top` position its top-left
// corner, and the margins pull it back by half to centre the box on that point.
// Halving the box and leaving them at -32px would have shifted it up and left by
// 16px each, which reads as "the position is also wrong" rather than as a missed
// edit.
.ignite
{
    position: absolute;
    left: 50%;
    top: 66%;
    margin-left: -16px;
    margin-top: -16px;
    width: 32px;
    height: 32px;

    animation: nz-ignite 2s linear forwards;

    img { width: 32px; height: 32px; }
}

// Holds full opacity briefly, then falls away — the original's alpha is
// 300*fadefac against a 255 ceiling, so it is pinned for the first third.
@keyframes nz-ignite
{
    0%   { opacity: 1; transform: scale( 1.35 ); }
    18%  { opacity: 1; transform: scale( 1 ); }
    100% { opacity: 0; transform: scale( 1 ); }
}

// ⛔ AND THE SAME BUG AGAIN, twenty lines further down: this brace was missing too, so
// `.salvage` was parsed as a keyframe step and the salvage counter has never had a style
// either. Two independent instances of one mistake in one file, both reported by the engine
// at startup and both unread.

    // Ammo mod -----------------------------------------------------------------
    // Salvage ------------------------------------------------------------------
    // Under the ammo, right-aligned with it. Green, matching the outline on the
    // salvage pickup itself so the floor loot and the counter read as one system.
    .salvage
    {
        flex-direction: row;
        align-items: center;
        justify-content: flex-end;
        gap: 6px;

        .salvage-icon
        {
            width: 11px;
            height: 11px;
            background-color: rgba( 60, 210, 90, 0.95 );
            // A diamond, so it is distinct from the plate's rectangle at a glance.
            transform: rotate( 45deg );
        }

        .salvage-count
        {
            font-size: 20px;
            font-weight: 700;
            color: rgba( 235, 235, 235, 0.95 );
            text-shadow: 0px 2px 5px rgba( 0, 0, 0, 0.9 );
        }
    }

// ═══ BASALT — THE CARVED HUD (`HudTheme`, `MapConfig.HudTheme = "basalt"`) ═══════════════════════════════════════════════════
//
// Asked for as *"geometrical and ancient looking but with modern light strips"*, then *"make it more ancient … ancient with
// accents basically"* and *"make sure everything is in the correct place"* (2026-09-27). Carved stone tablets, bone and ember,
// and a thin white light strip here and there as the only modern thing — the way the map's own strips are the only
// manufactured thing in it.
//
// ⛔ A LOOK, NEVER A LAYOUT — nothing here may move a panel, and `ui_panel_dump` in both looks is how that was checked:
//   * NO TABLETS (2026-09-27, later) — *"the background for the hud elements take too much space so i want to remove
//     them"*. The stone slabs behind the vitals, the score and the weapon are gone; the grooves, the sockets and the
//     colours stay.
//   * THE BARS KEEP THEIR 2px BORDERS — only the colours change, so each is still 256 x 18.
//   * THE FRAMES ON THE PERKS AND THE TEAM-MATES' FACES ARE BOX-SHADOW RINGS, which take no room: a border would come out of
//     the icon inside the box.
//   * THE FACE IS CINZEL ON EVERY WORD (2026-09-27, later), on Inter's line box, so no row grows taller — the one thing
//     here that can move a word, because it is wider than Inter on mixed-case text: a long weapon name reaches further.
//     No size, weight or spacing changes but the Roman round's own.
//
// ⚠️ `.hud.theme-basalt` IN FRONT OF EVERY RULE, so each is more specific than the ordinary rule it overrides — none of it
// depends on coming later in the file. That holds for the rules outside the `.hud` block too (`.charge`, `.focus`, the
// icons, `.salvage`, `.perks`): each is a descendant of the root that wears the class.

$stone: rgba( 46, 42, 37, 0.9 );
$stone-hi: rgba( 92, 83, 72, 1 );
$stone-lo: rgba( 17, 14, 11, 1 );
$groove: rgba( 15, 12, 9, 0.95 );
$groove-lip: rgba( 96, 86, 74, 1 );
$bone: rgba( 228, 217, 195, 1 );
$bone-dim: rgba( 160, 148, 130, 1 );
$ember: rgba( 222, 108, 46, 1 );
$ember-hot: rgba( 248, 150, 72, 1 );
$lite: rgba( 234, 246, 255, 1 );

.hud.theme-basalt
{
    // ── the face: the map's, Cinzel, on every word ────────────────────────────────────────────────────────────────────────
    // *"i want the font for this map to affect the ammo and weapon name …"* (2026-09-27). On the root, so all of it inherits:
    // the score, the weapon's name and ammo, the grenades, the salvage, the team-mates' points (the use prompt names its own
    // box, in UsePromptPanel.razor.scss). ⛔ ON INTER'S LINE BOX (0.898, for the reasons `.round` gives below): every row
    // here is sized around a line of Inter. Cinzel-Bold is the one face shipped, so every weight draws bold.
    font-family: Cinzel;
    line-height: 0.898;

    // ── the bars: grooves cut into the stone ─────────────────────────────────────────────────────────────────────────────
    .health, .stamina, .place-charge, .charge, .focus
    {
        background-color: $groove;
        border: 2px solid $stone-lo;
        border-right-color: $groove-lip;
        border-bottom-color: $groove-lip;
    }

    // health: molten ember, the heat under the rock
    .health .fill { background-color: rgba( 196, 86, 34, 1 ); }
    .health.low .fill { background-color: rgba( 246, 72, 32, 1 ); }

    // stamina: weathered bone
    .stamina .fill { background-color: rgba( 176, 165, 146, 0.95 ); }
    .stamina.low .fill { background-color: $ember; }

    // the banana's charge: old gold
    .place-charge .fill { background-color: rgba( 204, 166, 92, 0.95 ); }
    .place-charge.ready { border-color: $ember-hot; }
    .place-charge.ready .fill { background-color: rgba( 242, 206, 132, 1 ); }
    .place-charge.paused { border-color: rgba( 255, 255, 255, 0.12 ); }
    .place-charge.paused .fill { background-color: rgba( 204, 166, 92, 0.3 ); }

    // armor: slate
    .armor .seg { background-color: $groove; }
    .armor .seg.unlocked
    {
        border: 2px solid $stone-lo;
        border-right-color: $groove-lip;
        border-bottom-color: $groove-lip;
    }
    .armor .seg.locked
    {
        border: 2px solid rgba( 70, 63, 55, 0.55 );
        background-color: rgba( 15, 12, 9, 0.5 );
    }
    .armor .fill { background-color: rgba( 132, 148, 152, 0.95 ); }
    .plates .plate-icon { background-color: rgba( 132, 148, 152, 0.95 ); }
    .plates .plate-count { color: $bone; }
    .plates.empty .plate-icon { background-color: rgba( 92, 83, 72, 0.6 ); }
    .plates.empty .plate-count { color: $bone-dim; }

    // the icons, in bone
    .hp-icon { color: $bone; }
    .boot .shaft, .boot .foot, .armor-icon .shield, .charge-icon .dot, .focus-icon .chev, .streak-icon .pip
    {
        background-color: $bone;
    }
    .charge-icon .ring { border-color: $bone; }
    .banana .peel { background-color: rgba( 222, 190, 110, 0.95 ); }

    // ── the score: old gold ──────────────────────────────────────────────────────────────────────────────────────────────
    .points .value { color: rgba( 226, 204, 158, 1 ); }

    .points .portrait
    {
        background-color: $groove;
        border-color: $stone-hi;
        border-right-color: $stone-lo;
        border-bottom-color: $stone-lo;
    }

    .mate .portrait { box-shadow: 0px 0px 0px 1px $stone-hi; }
    .mate .pts { color: rgba( 226, 204, 158, 0.9 ); }

    // ── the round: carved ember numerals, in Cinzel ──────────────────────────────────────────────────────────────────────
    // Cinzel is Roman inscription capitals (`Assets/fonts/Cinzel-Bold.ttf`, SIL OFL 1.1, with `Cinzel-OFL.txt` beside it). The
    // user picked it from an in-game preview (2026-09-27). It is the one weight, Bold, so every round rule here asks for 700.
    //
    // ⛔ ITS LINE BOX IS PINNED TO INTER'S: 1.211 of the size, where Cinzel's own is 1.348. `.lower` is as tall as its tallest
    // child, which is normally the weapon (90px). At 72px Cinzel's own line (98px) would outgrow it, and the row would lift
    // the whole right-hand column, the score included, about 7px through every round from 10. Pinned, the round's box is
    // exactly the ordinary HUD's at every size, so nothing around it can move.
    //
    // ⛔ AND THE PIN IS 0.898, NOT 1.211 (corrected 2026-09-27, later). s&box reads a unitless or percentage line-height as a
    // share of the FONT'S OWN line, not of its size, so 1.211 made this 1.211 x 1.348 = 1.63 of the size: a 72px round 118px
    // tall where Inter's is 88. 0.898 is 1.211 / 1.348. `text_measure` shows it (Cinzel 72px: 118 at 1.211, 88 at 0.898;
    // Inter, 88), and so did the Arsenal's 47px title in the running game: 78px at 1.211, where Inter's is 58.
    .round
    {
        font-family: Cinzel;
        font-weight: 700;
        line-height: 0.898;
        color: $ember;
        text-shadow: 0px 2px 4px rgba( 0, 0, 0, 1 );
    }

    .round.prep { color: $bone; }

    // ⚠️ ROMAN FOR ROUNDS I TO IX ONLY (`HudTheme.RomanUpTo`) — "VIII" is the longest, four glyphs — so the ordinary tally's
    // own 64px. From round 10 the digits take the ordinary `.digits` rule, in ember.
    .round.roman
    {
        font-size: 64px;
        font-weight: 700;
        letter-spacing: 4px;
    }

    // ── the weapon: bone numbers ─────────────────────────────────────────────────────────────────────────────────────────
    .weapon .ammo .clip { color: $bone; }
    .weapon .ammo .sep { color: $bone-dim; }
    .weapon .ammo .reserve { color: $bone-dim; }
    .weapon .reloading { color: $ember-hot; }
    .weapon .ammo .techtag { color: $bone-dim; }
    .weapon .ammo .techtag.flash { color: $bone; }
    .weapon .ammo .techtag.ready { color: $ember-hot; }

    // ⚠️ THE GRENADES ARE THE WEAPON'S SIBLING in the markup (in `.lower`), not its child — so these are not nested under it
    .grenades .nade-icon { color: $ember; }
    .grenades .nade-count { color: $bone; }
    .grenades.cooking .nade-icon, .grenades.cooking .nade-count { color: rgba( 255, 80, 50, 1 ); }

    // salvage: jade
    .salvage .salvage-icon { background-color: rgba( 96, 170, 120, 0.95 ); }
    .salvage .salvage-count { color: $bone; }

    // ── the perks, each set in a stone socket ────────────────────────────────────────────────────────────────────────────
    .perks .perk
    {
        background-color: $stone;
        box-shadow: 0px 0px 0px 2px $stone-hi;
    }
}