/*
  Shared terminal/cockpit theme - dark background, phosphor-green text,
  bordered panels reminiscent of an old Apple II/Atari-era console.
  Global (not a Blazor CSS-isolation *.razor.css file) because more than
  one page uses it: /play originated it, /lobby reuses it, and any future
  page should too rather than duplicating this theme locally.
*/

:root {
    --cockpit-bg: #01120a;
    --cockpit-panel-bg: #041c10;
    --cockpit-green: #33ff77;
    --cockpit-green-dim: #1c8a49;
    --cockpit-amber: #ffb000;
    --cockpit-red: #ff3b3b;
    --cockpit-font: 'Consolas', 'Courier New', monospace;

    /* Achievement tiers (docs/ACHIEVEMENTS-PLAN.md Phase 1): /profile's grid and the Promotions page's badge stamps. */
    --achievement-bronze: #d08a4c;
    --achievement-silver: #c9d3dc;
    --achievement-gold: #ffd54a;

    /* One step brighter than --cockpit-green, for the moment a control is
       under the pointer or holding focus - the base green is already the
       resting text color, so "lit up" needs somewhere above it to go. */
    --cockpit-green-bright: #8affb4;
    /* Phosphor bloom. Always used as a box-shadow/glow color, never as a
       fill, so it carries its own alpha. */
    --cockpit-glow: rgba(51, 255, 119, 0.35);
    /* .cockpit-panel-bg lifted just enough to read as "this one" when the
       pointer is over it, without looking like a selected state. */
    --cockpit-panel-hover-bg: #062716;

    /* Motion. Every transition in the cockpit reads its duration from one of
       these three rather than hardcoding its own, so the reduced-motion
       block at the bottom of this file can neutralize the whole UI in one
       place instead of each effect carrying its own media query.
       Fast = a press (must feel like the click itself, not a response to
       it), the default = hover/focus, slow = anything that sweeps. */
    --cockpit-dur-fast: 90ms;
    --cockpit-dur: 160ms;
    --cockpit-dur-slow: 320ms;
    --cockpit-ease: cubic-bezier(0.2, 0.7, 0.3, 1);

    /* Shared by every overlay in the app (CockpitSelect's dropdown, the
       arena Inspect bubble) - everything else stacks at 0/1, so this just
       needs to clear the highest of those, not compete with a real
       stacking context. */
    --cockpit-z-popup: 5;

    /* docs/RESULTS-MODAL-PLAN.md Phase 1 - the modal layer (CockpitModalHost's backdrop and panel) sits above every
       popup; the connection banner and TimeoutBanner sit above THAT (html.cockpit-modal-open lifts them - see
       .cockpit-connection-banner below), so Reconnect / Take back control stay reachable while a modal is open.
       Both stay below #blazor-error-ui (1000). docs/PLAN.md "Tunable Values". */
    --cockpit-z-modal: 50;
    --cockpit-z-banner: 60;

    /* The one deliberate departure from all-phosphor-green: a traditional
       loot-game rarity ladder (grey/green/blue/purple/orange), the first
       place this cockpit uses hues beyond its own accent. Shared across
       every 5-tier quality/rarity concept in the game - RosterQuality
       (Common..Legendary) and VehicleQualityTier (Beater..Legendary) map
       onto it by ladder position (RarityCssClass.cs); weapons have no such
       tier yet, so nothing here applies to them until that model exists.
       Uncommon deliberately reuses --cockpit-green rather than a second
       green - the cockpit's own accent color already sits exactly where
       "uncommon" belongs on this ladder. The others are picked to still
       read clearly against the near-black background rather than matching
       any specific game's exact hex values. */
    --rarity-common: #9aa3a8;
    --rarity-uncommon: var(--cockpit-green);
    --rarity-rare: #3fa9ff;
    --rarity-epic: #b266ff;
    --rarity-legendary: #ff9425;

    /* Crew Traits (docs/CREW-TRAITS-PLAN.md PR 2) - one color per skill for Shared/TraitChips.razor, reusing
       hues the rarity ladder already established (a trait chip is never on the same line as a rarity label,
       so reuse reads as "another accent," not a clash) plus the cockpit's own red for Driving (loss-of-control/
       collision risk already reads red everywhere else in this app). */
    --skill-driving: var(--cockpit-red);
    --skill-gunnery: var(--rarity-rare);
    --skill-mechanic: var(--cockpit-amber);
    --skill-navigator: var(--rarity-epic);

    /* Safe-area insets (App.razor's viewport meta has viewport-fit=cover, so on a notched phone the
       page runs under the notch / home indicator). Every edge-pinned box adds these to its own
       padding - 0 on every device without a notch, so those render exactly as before. Read through
       tokens (never env() directly) so a uitest can set one on <html> to simulate a notch, which no
       headless browser reports (tools/uitest/verify-modal-followups.mjs). */
    --cockpit-safe-top: env(safe-area-inset-top, 0px);
    --cockpit-safe-right: env(safe-area-inset-right, 0px);
    --cockpit-safe-bottom: env(safe-area-inset-bottom, 0px);
    --cockpit-safe-left: env(safe-area-inset-left, 0px);
}

/* ==========================================================================
   Mouse cursors (art: wwwroot/img/cursors, SVG sources in its src/, noted in docs/ART-STYLE.md "Mouse cursors"). The one place
   they are defined: every cursor rule in this file, a scoped .razor.css or an inline style says var(--cursor-*) (Web.ClientTests'
   CursorRulesTests holds that line); real text fields keep the system I-beam, `cursor: text`.

   - Why three layers instead of a plain line and an image-set line: `cursor: var(--x)` always parses, so a value an engine cannot
     use is only found out at computed-value time, when no earlier declaration is left to fall back to (the element just inherits).
     So the choice is made here, with @supports, before anything reads the property.
   - Why root-absolute URLs: a url() in a custom property has no one stylesheet it obviously belongs to, and this property is read from
     cockpit.css, from the scoped bundle in another folder (_content/...) and from inline styles. App.razor's <base href="/"> is
     hard-coded, so the app only ever runs at the site root; a path base would have to change these six names too.
   - Touch-only devices keep the bare keywords, no images, so what a tap reads is what it always read: iOS Safari synthesises a
     click on a plain element only when its cursor is `pointer` (see .cockpit-tooltip-trigger--chip).
   - 32 x 32 CSS px is the largest a browser reliably shows anywhere on the page; Chromium drops a larger one near the window edge.
     Hotspots are in that 32 x 32 space; the 64 px files are the same art for a 2x screen. Do not scale them up.
   ========================================================================== */
:root {
    --cursor-arrow: default;
    --cursor-action: pointer;
    --cursor-crosshair: crosshair;
    --cursor-grab: grab;
    --cursor-grabbing: grabbing;
    --cursor-not-allowed: not-allowed;
}

@media (hover: hover) {
    :root {
        --cursor-arrow: url("/img/cursors/arrow-32.png") 3 3, default;
        --cursor-action: url("/img/cursors/action-32.png") 11 2, pointer;
        --cursor-crosshair: url("/img/cursors/crosshair-32.png") 15 15, crosshair;
        --cursor-grab: url("/img/cursors/grab-32.png") 16 19, grab;
        --cursor-grabbing: url("/img/cursors/grabbing-32.png") 16 19, grabbing;
        --cursor-not-allowed: url("/img/cursors/not-allowed-32.png") 15 15, not-allowed;
    }

    @supports (cursor: image-set(url("x.png") 1x) 1 1, auto) {
        :root {
            --cursor-arrow: image-set(url("/img/cursors/arrow-32.png") 1x, url("/img/cursors/arrow-64.png") 2x) 3 3, url("/img/cursors/arrow-32.png") 3 3, default;
            --cursor-action: image-set(url("/img/cursors/action-32.png") 1x, url("/img/cursors/action-64.png") 2x) 11 2, url("/img/cursors/action-32.png") 11 2, pointer;
            --cursor-crosshair: image-set(url("/img/cursors/crosshair-32.png") 1x, url("/img/cursors/crosshair-64.png") 2x) 15 15, url("/img/cursors/crosshair-32.png") 15 15, crosshair;
            --cursor-grab: image-set(url("/img/cursors/grab-32.png") 1x, url("/img/cursors/grab-64.png") 2x) 16 19, url("/img/cursors/grab-32.png") 16 19, grab;
            --cursor-grabbing: image-set(url("/img/cursors/grabbing-32.png") 1x, url("/img/cursors/grabbing-64.png") 2x) 16 19, url("/img/cursors/grabbing-32.png") 16 19, grabbing;
            --cursor-not-allowed: image-set(url("/img/cursors/not-allowed-32.png") 1x, url("/img/cursors/not-allowed-64.png") 2x) 15 15, url("/img/cursors/not-allowed-32.png") 15 15, not-allowed;
        }
    }

    /* The arrow everywhere (html: `cursor` inherits), so plain text is not an I-beam. These base rules are hover-only, so an element
       that had no cursor rule before gets none on a touch screen (see above). They sit before every class rule on purpose: same
       specificity, later wins. */
    html {
        cursor: var(--cursor-arrow);
    }

    input,
    textarea,
    [contenteditable]:not([contenteditable="false"]),
    [role="textbox"] {
        cursor: text;
    }

    a[href],
    area[href],
    button,
    select,
    summary,
    input[type="button"],
    input[type="submit"],
    input[type="reset"],
    input[type="image"],
    input[type="file"],
    input[type="color"],
    input[type="checkbox"],
    input[type="radio"],
    input[type="range"],
    [role="button"],
    [role="link"],
    [role="tab"],
    [role="option"],
    [role="menuitem"],
    [role="switch"],
    [role="checkbox"],
    [role="radio"] {
        cursor: var(--cursor-action);
    }

    /* Separate, so an engine without :has() loses only this rule. */
    label:has(input[type="checkbox"]:not(:disabled), input[type="radio"]:not(:disabled)) {
        cursor: var(--cursor-action);
    }

    /* A label wrapped round a disabled box or radio: not the hand of the rule above, and not the arrow either. */
    label:has(input:disabled) {
        cursor: var(--cursor-not-allowed);
    }

    /* A row that selects its car on a click. Only hover-mode: on a touch screen its button is what takes the tap, as before. */
    .squad-row[data-mode]:not(.is-out) {
        cursor: var(--cursor-action);
    }

    /* !important: a control's class rule (or a parent's action cursor, through a label) must never bring the hand back on a disabled one. */
    :disabled,
    [aria-disabled="true"] {
        cursor: var(--cursor-not-allowed) !important;
    }
}

/* The map (CameraViewport.razor, Rendering/ViewportCursor). The arrow by default; these classes come from its pointer handlers.
   The hand over another car stays while nothing else applies: a press there picks the car. */
.car-hover-target {
    cursor: var(--cursor-action);
}

.camera-viewport--targeting,
.camera-viewport--targeting .car-hover-target {
    cursor: var(--cursor-crosshair);
}

.camera-viewport--handle,
.camera-viewport--handle .car-hover-target {
    cursor: var(--cursor-grab);
}

/* Everything: a drag crosses cars, the Recenter button and the Inspect bubble and is still a drag. Only for the length of a press. */
.camera-viewport--grabbing,
.camera-viewport--grabbing * {
    cursor: var(--cursor-grabbing) !important;
}


/* Every cockpit page fills the browser edge-to-edge - no default white
   page margin/background should ever show around .cockpit-shell. */
/* Controls are never double-tap-to-zoom targets: two quick taps on a button/link/field (the speed arrows, Ready, a turn's confirm) must act twice,
   not zoom the page on iOS. manipulation keeps scrolling and pinch-zoom. */
button, a, input, select, textarea, label, summary, [role="button"],
/* Tappable things that are not native controls: the custom select's popup rows, /play's opponent rows, and SVG maps (the Overworld's node circles). An
   element's effective touch-action is the intersection of its own and its ancestors' (up to a scroll container), so the page root covers everything else;
   the arena's CameraViewport sets touch-action:none inline, which still wins there. */
html, body, svg, [role="option"], .cockpit-select-option, .cockpit-opponent-row, .play-combatant-row, .play-combatant-detail {
    touch-action: manipulation;
}

html, body {
    margin: 0;
    padding: 0;
    height: 100%;
    background: var(--cockpit-bg);
}

/* This is where `.camera-viewport-svg * { pointer-events: none }` used to
   live. The bug it worked around was real - a pointer event's
   OffsetX/OffsetY are measured against event.target, the specific shape
   the finger landed on, not against the element the listener sits on, so
   a tap landing on a painted car outline got measured against that
   shape's own bounding box instead of the canvas (found on a real phone:
   the drag handle only answered taps offset to one side of the car).
   Switching every shape off was the cheapest fix at the time.

   It was also the wrong layer. The defect was the coordinate *source*,
   not the hit-testing: CameraViewport now reads ClientX/ClientY, which
   are viewport-relative and don't care what was hit, and subtracts the
   container's own origin (see ContainerPoint there). With that in place
   nothing needs the shapes switched off, so they're switched back on -
   which is what makes hovering and tapping things in the arena possible
   at all, and what the observation/inspect UI is built on. */

/* Turn playback moves each car 20 times a second (30 steps over 1.5s, each
   one a full Blazor re-render) and the cars visibly stuttered across the
   field as a result. Rather than re-rendering more often - which multiplies
   the cost of every panel on the page, not just the arena, and lands hardest
   on the phones this game is actually played on - the browser fills in the
   gaps between steps for free.

   --car-step is set per-car by the owning page to its own step interval, so
   a transition finishes exactly as the next step arrives. The 0ms default
   means a caller that hasn't set it gets today's snapping rather than a
   guessed cadence. `linear` because CurvedTurnPlayback has already paced the
   motion correctly; easing here would fight it and make constant speed look
   like it surges.

   .wreck-glyph is every out-of-action glyph (wrecked, exploded, drowned,
   overturned - docs/COLLISION-PHYSICS-PLAN.md Phase 1): the same placement
   so a wreck can glide once it can be pushed, without being counted as a
   live .car-glyph. */
.car-glyph,
.car-capsule,
.car-outline,
.wreck-glyph {
    /* `translate`/`rotate`, not `transform` - see CarGlyph.razor. The SVG
       transform attribute isn't mapped to the CSS transform property in
       WebKit at all, so transitioning `transform` here was a silent no-op
       for every Safari player. fill-box keeps rotation about the glyph's own
       centre rather than the viewBox corner that auto-fit moves each render. */
    transform-box: fill-box;
    transform-origin: center;
    transition: translate var(--car-step, 0ms) linear,
                rotate var(--car-step, 0ms) linear;
}

/* Everything that rides along with a car - its label and status icons, the hover ring and hover target, a shot's
   muzzle and impact flashes, a damage number (Rendering/CarFollow) - glides with it on the same transition, never
   rotating. Before this they jumped once per playback step beside a gliding glyph (docs/PLAN.md "Playback
   smoothness"). */
.car-follow {
    transition: translate var(--car-step, 0ms) linear;
}

/* Weapons fire (docs/WEAPON-MOUNT-ART-PLAN.md Phase 4a, D-22..D-25). Every shot is a frame - a group TurnPlaybackLayer lays from the
   barrel tip to where the shot ends (translate, rotate, scale by its length; the playback driver re-lays it every frame as both cars move)
   holding its family's look in a unit frame, (0,0) to (1,0) (Shared/ShotEffectShape). Strokes are non-scaling, so widths and the Vulcan's
   dashes are screen pixels at any zoom - nothing is dashed by a stroke-dasharray measured in metres any more (the Phase 3 hit tracer drew
   as zoom-dependent dashes because of exactly that: under vector-effect: non-scaling-stroke both engines measure dashes in screen px, and
   pathLength does not fix it - it scales by the user-space length first). A draw-on is a scaleX of the line from the tip instead, and a
   projectile a translateX along the frame, so neither needs a length at all.

   What tells a hit from a miss is the target end (D-23): a hit's frame ends on the target's centre, where the impact spark (below) and the
   damage label land; a miss's ends short of the target or past it (ShotEffects.MissOf) and is a little fainter. Colour and shape say the
   family (D-24) - and never only colour: a dashed burst, a thick streak, a travelling dot, a cone, an instant line.

   Nothing persists (D-25): every look's whole life is --fx-life (at most ~480 ms) from its own start, and a hit's impact ends by
   --fx-travel + 240 ms (at most 490 ms). The shots of one volley start --shot-delay apart (D-15, set inline); every animation fills both ways
   so a delayed shot holds its invisible first frame until it starts, and its last (invisible) frame after it ends. The elements stay in the
   DOM for the rest of the turn (keyed, so their animations run once), transparent. */
.fx-gun { --fx-life: 400ms; --fx-travel: 150ms; --fx-colour: #ffd84a; --fx-width: 2px; }
.fx-vulcan { --fx-life: 400ms; --fx-travel: 150ms; --fx-colour: #ffd84a; --fx-width: 2px; }
.fx-cannon { --fx-life: 380ms; --fx-travel: 120ms; --fx-colour: #ff9a3c; --fx-width: 4.5px; --fx-impact: #ff9a3c; }
.fx-rocket { --fx-life: 480ms; --fx-travel: 250ms; --fx-colour: #fff3b0; --fx-width: 3px; --fx-impact: #ff7a29; }
.fx-flame { --fx-life: 450ms; --fx-travel: 150ms; --fx-colour: #ff6a1a; --fx-width: 0px; --fx-impact: #ff6a1a; }
.fx-laser { --fx-life: 300ms; --fx-travel: 0ms; --fx-colour: #7ff7ff; --fx-width: 1.5px; --fx-flash: #9ffaff; --fx-impact: #7ff7ff; }
.fx-sidearm { --fx-life: 300ms; --fx-travel: 100ms; --fx-colour: #f4f4f4; --fx-width: 1.2px; --fx-impact: #f4f4f4; }

.shot-fx {
    animation: fx-life var(--fx-life, 400ms) linear var(--shot-delay, 0ms) both;
}

.shot-fx.is-miss {
    animation-name: fx-life-miss;
}

.shot-fx.fx-laser {
    animation-name: fx-life-laser;
}

.shot-fx.fx-laser.is-miss {
    animation-name: fx-life-laser-miss;
}

@keyframes fx-life {
    0% { opacity: 0; }
    0.1% { opacity: 1; }
    55% { opacity: 1; }
    100% { opacity: 0; }
}

@keyframes fx-life-miss {
    0% { opacity: 0; }
    0.1% { opacity: 0.8; }
    45% { opacity: 0.7; }
    100% { opacity: 0; }
}

@keyframes fx-life-laser {
    0% { opacity: 0; }
    0.1% { opacity: 1; }
    30% { opacity: 1; }
    100% { opacity: 0; }
}

@keyframes fx-life-laser-miss {
    0% { opacity: 0; }
    0.1% { opacity: 0.8; }
    30% { opacity: 0.75; }
    100% { opacity: 0; }
}

.shot-fx .fx-line,
.shot-fx .fx-casing,
.shot-fx .fx-glow,
.shot-fx .fx-smoke,
.shot-fx .fx-dot,
.shot-fx .fx-dot-casing {
    fill: none;
    stroke-linecap: round;
}

.shot-fx .fx-line {
    stroke: var(--fx-colour);
    stroke-width: var(--fx-width);
}

/* A dark casing under each line, so a pale tracer still reads on the salt flats and on sand. */
.shot-fx .fx-casing {
    stroke: rgba(0, 0, 0, 0.45);
    stroke-width: calc(var(--fx-width) + 2px);
}

.shot-fx.fx-vulcan .fx-line,
.shot-fx.fx-vulcan .fx-casing {
    /* The one deliberately dashed look (D-24): a burst. Screen px - the stroke is non-scaling. */
    stroke-dasharray: 7 5;
    stroke-linecap: butt;
}

.shot-fx.fx-laser .fx-glow {
    stroke: rgba(80, 235, 255, 0.35);
    stroke-width: 5px;
}

.shot-fx.fx-rocket .fx-smoke {
    stroke: rgba(205, 205, 205, 0.7);
    stroke-width: var(--fx-width);
}

.shot-fx.fx-rocket .fx-dot {
    stroke: var(--fx-colour);
    stroke-width: 7px;
}

.shot-fx.fx-rocket .fx-dot-casing {
    stroke: rgba(0, 0, 0, 0.55);
    stroke-width: 10px;
}

.shot-fx.fx-flame .fx-cone {
    fill: var(--fx-colour);
    fill-opacity: 0.4;
}

.shot-fx.fx-flame .fx-cone-core {
    fill: #ffd23f;
    fill-opacity: 0.55;
}

/* A line drawn on from the tip (fill-box origin at its left end - the tip - not the viewBox corner). */
.shot-fx .fx-grow {
    transform-box: fill-box;
    transform-origin: 0% 50%;
    animation: fx-grow var(--fx-travel, 150ms) linear var(--shot-delay, 0ms) both;
}

@keyframes fx-grow {
    from { transform: scaleX(0.001); }
    to { transform: scaleX(1); }
}

/* The flame's cone spreads out from the nozzle, longer and wider at once. */
.shot-fx .fx-spread {
    transform-box: fill-box;
    transform-origin: 0% 50%;
    animation: fx-spread var(--fx-travel, 150ms) ease-out var(--shot-delay, 0ms) both;
}

@keyframes fx-spread {
    from { transform: scale(0.05); }
    to { transform: scale(1); }
}

/* A projectile along the frame: 1px is the frame's whole length (the group is scaled by it). */
.shot-fx .fx-run {
    animation: fx-run var(--fx-travel, 250ms) linear var(--shot-delay, 0ms) both;
}

@keyframes fx-run {
    from { transform: translateX(0px); }
    to { transform: translateX(1px); }
}

/* The cannon's streak is a fifth of the frame long, so it runs to 0.8 and ends at the target. */
.shot-fx .fx-run-streak {
    animation: fx-run-streak var(--fx-travel, 120ms) linear var(--shot-delay, 0ms) both;
}

@keyframes fx-run-streak {
    from { transform: translateX(0px); }
    to { transform: translateX(0.8px); }
}

/* Muzzle flash and impact spark. Both expand and vanish - only the moment it happened. fill-box origins so each scales about its own
   centre and not about the viewBox corner, which auto-fit moves every render (the same trap CarGlyph avoids by staying on the transform
   attribute). The impact starts as the family's line or projectile reaches the target (--fx-travel), its own volley delay on top. */
.shot-muzzle,
.shot-impact {
    transform-box: fill-box;
    transform-origin: center;
}

.shot-muzzle {
    stroke: var(--fx-flash, #ffd479);
    animation: shot-flash 200ms ease-out var(--shot-delay, 0ms) both;
}

.shot-impact {
    stroke: var(--fx-impact, #ff8a7a);
    animation: shot-flash 240ms ease-out calc(var(--fx-travel, 150ms) + var(--shot-delay, 0ms)) both;
    opacity: 0;
}

@keyframes shot-flash {
    0% { transform: scale(0.4); opacity: 0; }
    0.1% { transform: scale(0.4); opacity: 0.95; }
    100% { transform: scale(2.2); opacity: 0; }
}

/* The legend's own layout: a small table of samples, each a dark strip of field with a shooter and a target. */
.shot-legend {
    margin-top: 12px;
}

.shot-legend-title {
    margin: 0 0 6px;
    font-size: 1rem;
}

.shot-legend-table {
    border-collapse: collapse;
}

.shot-legend-table th,
.shot-legend-table td {
    padding: 3px 8px 3px 0;
    text-align: left;
    vertical-align: middle;
    font-weight: normal;
}

.shot-legend-table thead th {
    font-size: 0.8rem;
    opacity: 0.75;
}

.shot-legend-sample {
    display: block;
    max-width: 100%;
    height: auto;
    border-radius: 3px;
}

.shot-legend-ground {
    fill: #3a3f44;
}

.shot-legend-car {
    fill: #9aa3ad;
}

/* /play's "?" legend (Cockpit/ShotEffectsLegend) draws the same frames and sparks, frozen at one moment of their run: 180 ms into a shot
   (a gun's line drawn, a rocket three-quarters of the way, a laser still bright) and 80 ms into a spark. */
.shot-legend .shot-fx,
.shot-legend .shot-fx * {
    animation-delay: -180ms !important;
    animation-play-state: paused !important;
}

.shot-legend .shot-impact {
    animation-delay: -80ms !important;
    animation-play-state: paused !important;
}

/* docs/WEAPON-MOUNT-ART-PLAN.md Phase 4b (D-27, D-28): the fire-check ticks on /play's planned path (Cockpit/FireCheckTicks) - a short
   white bar across the path at each moving leg's end, a white ring where the car stands still (its count beside it). Light, thin and
   colourless, so they never read as the risk-coloured waypoint dots; dimmed and dashed while Hold fire or the weapons hold is on. */
.fire-check-tick line,
.fire-check-tick circle {
    stroke: #f2f5f8;
    stroke-width: 2;
    stroke-linecap: round;
    vector-effect: non-scaling-stroke;
    fill: none;
    opacity: 0.85;
}

.fire-check-tick.is-stationary circle {
    stroke-width: 1.5;
}

.fire-check-count text {
    fill: #f2f5f8;
    opacity: 0.85;
    font-weight: bold;
}

.fire-check-ticks.is-held line,
.fire-check-ticks.is-held circle {
    opacity: 0.35;
    stroke-dasharray: 2 3;
}

.fire-check-ticks.is-held .fire-check-count text {
    opacity: 0.35;
}

.fire-check-legend {
    margin-top: 12px;
}

/* The field's top-right corner while planning (Play.razor's OverlayContent): the count of fire checks, the tooltip's trigger. */
.cockpit-fire-check-chip {
    position: absolute;
    top: 10px;
    right: 10px;
    z-index: 2;
    display: flex;
    align-items: center;
    min-height: 32px;
    padding: 0 10px;
    background: rgba(5, 12, 5, 0.82);
    border: 1px solid var(--cockpit-green-dim);
    color: #f2f5f8;
    font-size: 0.8rem;
    line-height: 1.3;
    white-space: nowrap;
}

.cockpit-fire-check-chip.is-held {
    opacity: 0.6;
}

.cockpit-fire-check-hint {
    display: inline-block;
    line-height: 1.3;
    text-decoration: underline dotted;
    text-underline-offset: 3px;
}

/* docs/WEAPON-MOUNT-ART-PLAN.md section 6 "Dispenser drops" (Phase 3): a short puff where a drop leaves the car's dispenser box (or the
   drop point when this observer sees no box). Grows and fades in ~300 ms; the hazard itself is drawn as before. */
.drop-puff {
    transform-box: fill-box;
    transform-origin: center;
    animation: drop-puff 300ms ease-out both;
}

@keyframes drop-puff {
    from { transform: scale(0.6); opacity: 0.9; }
    to { transform: scale(2.4); opacity: 0; }
}

/* Boot splash. Sits at the bottom of the stacking order; .cockpit-shell and
   the title screen both claim a layer above it and both paint an opaque
   background, so it is covered the instant a page renders rather than needing
   to be dismissed. */
.app-boot {
    position: fixed;
    inset: 0;
    z-index: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 10px;
    background: var(--cockpit-bg);
    font-family: var(--cockpit-font);
    color: var(--cockpit-green-dim);
}

.app-boot-title {
    font-size: clamp(18px, 4vw, 30px);
    letter-spacing: 8px;
    color: var(--cockpit-green);
    text-shadow: 0 0 18px var(--cockpit-glow);
}

.app-boot-status {
    font-size: 12px;
    letter-spacing: 3px;
    text-transform: uppercase;
}

/* Three dots that fill in, so a slow connection shows something alive rather
   than a frozen word. content, not opacity, so there is no layout shift. */
.app-boot-status::after {
    content: "";
    animation: app-boot-dots 1.2s steps(1, end) infinite;
}

@keyframes app-boot-dots {
    0% { content: ""; }
    25% { content: "."; }
    50% { content: ".."; }
    75% { content: "..."; }
}

/* docs/ART-STYLE.md Phase 3: a wind turbine's rotor (Shared/StandingLayer). Display only, and no C# runs for it. transform-box: fill-box keeps the origin on the hub, the centre of the rotor symbol's full circle,
   wherever the camera has put the viewBox: the default box is the warning in ArenaMapLayer. PropRenderMath.RotorSeconds is this duration, and its 108 steps are 12 frames a second.
   It keyframes the individual rotate property, not transform, for an engine detail: Chromium gives an animated transform its own compositor layer, which re-records paint across the page at each step during playback, and does not composite this one (measured in the doc). If an engine changes that, the look is the same and only the cost moves. */
.standing-rotor {
    transform-box: fill-box;
    transform-origin: center;
    animation: standing-rotor-turn 9s steps(108) infinite;
}

@keyframes standing-rotor-turn {
    to { rotate: 360deg; }
}

@media (prefers-reduced-motion: reduce) {
    .standing-rotor {
        animation: none;
    }
}

@media (prefers-reduced-motion: reduce) {
    .app-boot-status::after {
        animation: none;
        content: "...";
    }
}

/* A cell that just lost points. Brief, and on the cell itself rather than the
   whole panel - the point is to say *which* readout changed, which a panel
   flash would lose. Paired with a @key on the same token in Play.razor, since
   a class alone would not replay the animation on a second hit. */
.cockpit-cell.is-damaged {
    animation: cockpit-damage-flash 620ms ease-out;
}

@keyframes cockpit-damage-flash {
    0% { background: var(--cockpit-red); color: var(--cockpit-bg); }
    35% { background: var(--cockpit-red); color: var(--cockpit-bg); }
    100% { background: transparent; }
}

.cockpit-bar-track.is-damaged {
    animation: cockpit-damage-bar 620ms ease-out;
}

@keyframes cockpit-damage-bar {
    0% { border-color: var(--cockpit-red); box-shadow: 0 0 10px var(--cockpit-red); }
    100% { border-color: var(--cockpit-green-dim); box-shadow: none; }
}

/* ==========================================================================
   Armor table (docs/PLAN.md "Armor table and damage flash") - Cockpit/ArmorCells
   + ArmorHeaderCells inside a .armor-table: the Combatants/Standings tables on
   /watch, /hall (rail and broadcast), /dev/replay and /play's Game Over table.
   Five narrow right-aligned columns F B L R U. The wrapper is the size
   container: the rail's columns are narrow even on a desktop, so the layout
   answers to the TABLE's width, never the viewport's. Narrow, each row becomes
   two lines (a flex row that wraps after the armor): the car and its armor
   first, then the status (and the model / result / time) under it, so the
   numbers keep their columns and the status gets the whole width.
   ========================================================================== */
:root {
    /* One armor column's width: three digits and a little air. */
    --armor-col: 2.3em;
}

.armor-table-wrap {
    container: armor-table / inline-size;
    overflow-x: auto;
}

.armor-table {
    width: 100%;
    border-collapse: collapse;
}

.armor-table th {
    text-align: left;
    font-weight: normal;
    color: var(--cockpit-green-dim);
    white-space: nowrap;
}

.armor-table td,
.armor-table th {
    padding: 1px 4px 1px 0;
    vertical-align: top;
}

.armor-table .armor-head,
.armor-table .armor-cell {
    width: var(--armor-col);
    min-width: var(--armor-col);
    max-width: var(--armor-col);
    box-sizing: border-box;
    padding-left: 0;
    padding-right: 0.4em;
    text-align: right;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

.armor-table .armor-cell--unknown {
    width: auto;
    max-width: none;
    text-align: left;
    color: var(--cockpit-green-dim);
}

/* The header letters: a tooltip chip each (Front armor, ...), the chip's own underline kept but no extra padding. */
.armor-table .armor-head .cockpit-tooltip-wrap {
    display: inline-block;
}

/* The car name ellipsises before anything else does. */
.armor-table .armor-table-car {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    max-width: 0;
    width: 40%;
}

.armor-table--race .armor-table-status,
.armor-table--broadcast .armor-table-status {
    white-space: nowrap;
}

/* The first armor column gets a little room from whatever sits before it. */
.armor-table .armor-head[data-facing="Front"],
.armor-table .armor-cell[data-facing="Front"] {
    width: calc(var(--armor-col) + 0.3em);
    min-width: calc(var(--armor-col) + 0.3em);
    max-width: calc(var(--armor-col) + 0.3em);
}

/* A side at 0 is gone: dimmed, still legible. */
.armor-value.is-gone {
    opacity: 0.4;
}

/* A side that just lost points: red, held for the first ~35%, then back to its own colour. Re-created by its @key on every
   drop, so a second hit restarts it. --cockpit-armor-flash-duration is written inline by Cockpit/ArmorCells from its one
   tunable, ArmorFlash.FlashMilliseconds (docs/PLAN.md "Tunable Values"). */
.armor-value {
    display: inline-block;
}

.armor-value.is-flash {
    animation: armor-flash var(--cockpit-armor-flash-duration) ease-out;
}

@keyframes armor-flash {
    0%, 35% { color: var(--cockpit-red); opacity: 1; text-shadow: 0 0 6px rgba(255, 59, 59, 0.6); }
    100% { text-shadow: none; }
}

@keyframes armor-flash-static {
    0%, 100% { color: var(--cockpit-red); opacity: 1; }
}

/* Reduced motion: the same red for the same time, no fade - it switches off at the end (steps), it never animates. */
@media (prefers-reduced-motion: reduce) {
    .armor-value.is-flash {
        animation: armor-flash-static var(--cockpit-armor-flash-duration) step-end;
    }
}

/* Screen-reader-only header text (the broadcast's Car / Status headings - the letters are the only header it shows). */
.armor-sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
}

/* Narrow: two lines per car. The row becomes a wrapping flex line - [pos] car (flexible, ellipsised) + the five fixed armor
   columns, a zero-height break (tbody tr::after, order 3), then the row's other cells (.armor-table-sub: status, model, result,
   time, laps) on the second line. The header keeps the car heading and the letters; its other headings stay for screen
   readers only. The thresholds are where the one-line row stops fitting at the rail's font: a race status "P3 · 2.1 km to go",
   a model and a status word, a result and a time beside a car name and the five columns. */
@container armor-table (max-width: 399px) {
    .armor-table--race tr {
        display: flex;
        flex-wrap: wrap;
        align-items: baseline;
    }

    .armor-table--race tbody tr::after {
        content: "";
        order: 3;
        flex-basis: 100%;
        height: 0;
    }

    .armor-table--race tbody tr {
        padding-bottom: 3px;
    }

    .armor-table--race .armor-table-pos {
        order: 0;
        flex: none;
    }

    .armor-table--race .armor-table-car {
        order: 1;
        flex: 1 1 0;
        min-width: 0;
        max-width: none;
        width: auto;
    }

    .armor-table--race .armor-cell,
    .armor-table--race .armor-head {
        order: 2;
        flex: none;
    }

    .armor-table--race .armor-table-sub {
        order: 4;
        min-width: 0;
        padding-left: 0.6em;
    }

    .armor-table--race thead .armor-table-sub {
        position: absolute;
        width: 1px;
        height: 1px;
        padding: 0;
        overflow: hidden;
        clip: rect(0, 0, 0, 0);
    }

    .armor-table--race .armor-table-status {
        flex: 1 1 100%;
        white-space: normal;
    }
}

@container armor-table (max-width: 459px) {
    .armor-table--model tr {
        display: flex;
        flex-wrap: wrap;
        align-items: baseline;
    }

    .armor-table--model tbody tr::after {
        content: "";
        order: 3;
        flex-basis: 100%;
        height: 0;
    }

    .armor-table--model tbody tr {
        padding-bottom: 3px;
    }

    .armor-table--model .armor-table-pos {
        order: 0;
        flex: none;
    }

    .armor-table--model .armor-table-car {
        order: 1;
        flex: 1 1 0;
        min-width: 0;
        max-width: none;
        width: auto;
    }

    .armor-table--model .armor-cell,
    .armor-table--model .armor-head {
        order: 2;
        flex: none;
    }

    .armor-table--model .armor-table-sub {
        order: 4;
        min-width: 0;
        padding-left: 0.6em;
    }

    .armor-table--model thead .armor-table-sub {
        position: absolute;
        width: 1px;
        height: 1px;
        padding: 0;
        overflow: hidden;
        clip: rect(0, 0, 0, 0);
    }

    /* The model then the status on the second line, the status right-aligned under the armor. */
    .armor-table--model .armor-table-model {
        flex: 1 1 auto;
        color: var(--cockpit-green-dim);
    }

    .armor-table--model .armor-table-status {
        flex: 0 1 auto;
        margin-left: auto;
        text-align: right;
        padding-right: 0.45em;
    }
}

@container armor-table (max-width: 419px) {
    .armor-table--gameover tr {
        display: flex;
        flex-wrap: wrap;
        align-items: baseline;
    }

    .armor-table--gameover tbody tr::after {
        content: "";
        order: 3;
        flex-basis: 100%;
        height: 0;
    }

    .armor-table--gameover tbody tr {
        padding-bottom: 3px;
    }

    .armor-table--gameover .armor-table-pos {
        order: 0;
        flex: none;
    }

    .armor-table--gameover .armor-table-car {
        order: 1;
        flex: 1 1 0;
        min-width: 0;
        max-width: none;
        width: auto;
    }

    .armor-table--gameover .armor-cell,
    .armor-table--gameover .armor-head {
        order: 2;
        flex: none;
    }

    .armor-table--gameover .armor-table-sub {
        order: 4;
        min-width: 0;
        padding-left: 0.6em;
    }

    .armor-table--gameover thead .armor-table-sub {
        position: absolute;
        width: 1px;
        height: 1px;
        padding: 0;
        overflow: hidden;
        clip: rect(0, 0, 0, 0);
    }
}

/* /play's Combatants/Standings rows (docs/PLAN.md "Armor table and damage flash", /play): the same table and narrow layout as the
   spectator tables (.armor-table--model / --race above), plus what /play's rows always had - a click centres the camera, a live
   opponent's row rings its car on hover, an out car is struck through, this player's own row is a bold tinted band (kept in sight
   while the list scrolls, in the fit layout below) - and an opponent's components / damage line under its row
   (.play-combatant-detail). The wrap is not a scroll container here (the panel body is), or the own row's sticky band would stick
   to the wrap instead. */
.armor-table-wrap--play {
    overflow: visible;
}

/* Clear of the panel's scrollbar: an overlay scrollbar (macOS, iOS) sits over the panel body's right edge while the list
   scrolls, which is where the U column's numbers are. */
.cockpit-rail-scroll-panel .armor-table-wrap--play {
    padding-right: 8px;
}

/* A fixed layout: the rail's width sizes the table, never its text - a 32-character name or a long model + passenger token (both
   ellipsised, nowrap) would otherwise widen the table past the panel by its min-content. */
.armor-table--play {
    table-layout: fixed;
}

.armor-table--play tr.play-combatant-row,
.armor-table--play tr.play-combatant-detail {
    cursor: var(--cursor-action);
    transition: background-color var(--cockpit-dur) var(--cockpit-ease);
}

.armor-table--play tr.play-combatant-row.out {
    opacity: 0.45;
    text-decoration: line-through;
}

/* The race's slim own-row band: only in the fit layout, where the Standings panel scrolls inside itself (see the fit rules). */
.armor-table--play tr.play-own-band {
    display: none;
}

.armor-table--play tr.play-combatant-row.is-own {
    font-weight: 700;
    background-color: rgba(255, 255, 255, 0.08);
}

.armor-table--play .play-combatant-detail-cell {
    padding: 0 0 3px 0;
}

/* A little tighter than the spectator tables: the fit layout's tightest rail (a five-weapon car, or eight race cars, at 1000x600)
   leaves this panel the header and one or two rows, and the first must still be wholly in sight (verify-combat-fit.mjs). */
.armor-table--play td,
.armor-table--play th {
    padding-top: 0;
    padding-bottom: 0;
}

/* The numbers in the cockpit's own green, not the row's colour: the first seat's colour is red (TeamColors), so this player's own
   row - very often that seat - would hide the red flash. The car name keeps the colour. */
.armor-table--play .armor-value {
    color: var(--cockpit-green);
}

/* One hover band over a car's row and its detail row, whichever is pointed at. */
@media (hover: hover) {
    .armor-table--play tr.play-combatant-row.hoverable:hover,
    .armor-table--play tr.play-combatant-row.hoverable:hover + tr.play-combatant-detail,
    .armor-table--play tr.play-combatant-row.hoverable:has(+ tr.play-combatant-detail:hover),
    .armor-table--play tr.play-combatant-detail.hoverable:hover {
        background: rgba(51, 255, 119, 0.12);
    }
}

/* Narrow (the rail on every desktop and phone): the model cell ellipsises (its title has the whole text) so the status beside it
   stays whole; the detail line spans the row. */
@container armor-table (max-width: 459px) {
    .armor-table--play.armor-table--model .armor-table-model {
        flex: 1 1 0;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
    }

    .armor-table--play.armor-table--model .armor-table-status {
        flex: 0 1 auto;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
    }

    .armor-table--play .play-combatant-detail-cell {
        flex: 1 1 100%;
        min-width: 0;
    }
}

/* The power bar's own width used to snap between turns. */
.cockpit-bar-fill {
    transition: width var(--cockpit-dur-slow) var(--cockpit-ease);
}

/* The topbar phase readout. It changes text on every phase change and was
   otherwise indistinguishable from the turn counter beside it; @key'd on the
   phase in Play.razor so each new state announces itself once. */
.cockpit-phase {
    animation: cockpit-phase-in var(--cockpit-dur) var(--cockpit-ease);
}

.cockpit-phase.is-resolving { color: var(--cockpit-amber); }
.cockpit-phase.is-spectating { color: var(--cockpit-green-dim); }
.cockpit-phase.is-over { color: var(--cockpit-green-bright); }

@keyframes cockpit-phase-in {
    from { opacity: 0; transform: translateY(-4px); }
    to { opacity: 1; transform: none; }
}

@media (prefers-reduced-motion: reduce) {
    .cockpit-cell.is-damaged,
    .cockpit-bar-track.is-damaged,
    .cockpit-phase {
        animation: none;
    }

    .cockpit-bar-fill {
        transition: none;
    }
}

.cockpit-shell {
    font-family: var(--cockpit-font);
    color: var(--cockpit-green);
    background: var(--cockpit-bg);
    /* Above the boot splash - see .app-boot. */
    position: relative;
    z-index: 1;
    /* 10px, plus the safe-area inset on each edge (0 without a notch). The
       frame's border still runs edge to edge; only the content is kept clear. */
    padding: calc(10px + var(--cockpit-safe-top)) calc(10px + var(--cockpit-safe-right))
             calc(10px + var(--cockpit-safe-bottom)) calc(10px + var(--cockpit-safe-left));
    border: 2px solid var(--cockpit-green-dim);
    box-sizing: border-box;
    /* Fills the browser viewport rather than a fixed footprint - min-
       height (not height) so a page whose content genuinely needs more
       room (e.g. Garage's crew panels) still just scrolls normally
       rather than clipping. display:flex column is what lets /play's
       .cockpit-main claim the remaining vertical space below the topbar
       (see its own flex:1) and lets a lighter page like /lobby center
       its content in that same remaining space. */
    width: 100%;
    min-height: 100vh;
    display: flex;
    flex-direction: column;
}

.cockpit-topbar {
    display: flex;
    flex-wrap: wrap;
    justify-content: space-between;
    /* A floor on the gap between the left (Home/page name) and right
       (Prestige/status/menu) groups, and the row gap once a topbar with
       enough content wraps to two lines at a narrow width - without either,
       the two groups could sit flush against each other with no visible
       separation ("HOME ROSTERPRESTIGE" running together) rather than
       reflowing. Found live on a tablet, on /roster's heavier topbar (a
       Prestige bar plus an unlock count on the right, alongside Home/Lobby/
       Roster on the left) - every page sharing this shell benefits. */
    gap: 4px 10px;
    padding: 4px 10px;
    margin-bottom: 10px;
    border-bottom: 1px solid var(--cockpit-green-dim);
    letter-spacing: 2px;
    text-transform: uppercase;
}

/* Lobby topbar's Prestige readout (Arena progression Phase 3) - the rank
   number plus a small horizontal XP bar toward the next one. */
.lobby-prestige-badge {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    font-size: 12px;
}

.lobby-prestige-bar {
    display: inline-block;
    width: 48px;
    height: 6px;
    background: var(--cockpit-panel-bg);
    border: 1px solid var(--cockpit-green-dim);
}

/* The same bar at panel width - GameOver's Prestige XP bar and /roster's
   "next Prestige rank" panel, which are the same quantity as the badge's,
   just given room. A modifier rather than a second class so there is one
   XP bar in the app. */
.lobby-prestige-bar.is-wide {
    display: block;
    width: 100%;
    height: 8px;
}

.lobby-prestige-bar-fill {
    display: block;
    height: 100%;
    background: var(--cockpit-green);
}

/* Standard hamburger-menu affordance for reaching Settings without a
   keyboard (a tablet has no Escape key). Sized as a real tap target, not
   a text-sized cockpit-button - it needs to work on a phone. */
.cockpit-menu-button {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 32px;
    height: 32px;
    padding: 0;
    font-size: 20px;
    line-height: 1;
    background: transparent;
    color: var(--cockpit-green);
    border: 2px solid var(--cockpit-green);
    cursor: var(--cursor-action);
    flex-shrink: 0;
}

.cockpit-menu-button:hover {
    background: var(--cockpit-green-dim);
    color: var(--cockpit-bg);
    box-shadow: 0 0 8px var(--cockpit-glow);
}

.cockpit-menu-button:active {
    background: var(--cockpit-green);
    color: var(--cockpit-bg);
    box-shadow: none;
}

/* /play's Leave match, beside the menu button (it was a rail panel of its own - no room for it in the 1000x600 fit rail). As tall as
   the menu button, so the top bar does not grow. */
.cockpit-topbar-button {
    height: 32px;
    padding: 0 8px;
    font-size: 12px;
    white-space: nowrap;
    flex-shrink: 0;
}

.cockpit-topbar-label-short {
    display: none;
}

/* The status group wraps (Play.razor): the sentence may take its own line(s) and break inside, Leave match + Settings stay together. */
.cockpit-topbar-status {
    min-width: 0;
    max-width: 100%;
}

.cockpit-topbar-status > .cockpit-phase {
    min-width: 0;
    overflow-wrap: anywhere;
}

/* A phone: the short label ("Leave"), so Leave match and the Settings menu button always fit beside each other on screen. */
@media (max-width: 699px) {
    .cockpit-topbar-label-full {
        display: none;
    }

    .cockpit-topbar-label-short {
        display: inline;
    }
}

.cockpit-main {
    display: flex;
    gap: 12px;
    /* flex:1/min-height:0 claims the shell's remaining vertical space
       below the topbar (and above .cockpit-log, on /play) - stretch (not
       flex-start) is what hands .cockpit-field a genuine, non-circular
       height so the arena canvas inside it can size itself against a
       real number instead of a percentage with nothing definite to
       resolve against. Pages without a canvas (/lobby, /garage) are
       unaffected in practice - their own content just centers or flows
       normally in the same reclaimed space. */
    align-items: stretch;
    flex: 1;
    min-height: 0;
}

.cockpit-field {
    border: 1px solid var(--cockpit-green-dim);
    padding: 4px;
    background: #000;
    line-height: 0;
    max-width: 100%;
    /* Grows to fill whatever width .cockpit-main has left over (next to
       .cockpit-side's fixed 340px) and centers CameraViewport's own
       canvas within that space - CameraViewport.razor measures this
       element's own rendered rect (via viewport.js's getSquareSize) and
       explicitly sizes itself to the largest square that fits both axes.
       A no-op for pages with no canvas child. */
    display: flex;
    align-items: center;
    justify-content: center;
    flex: 1;
    min-width: 0;
    min-height: 0;
}

/* The body of a non-arena page (/overworld, /lobby...) - the space below
   the topbar. Declares itself a container so LayoutGrid's container
   queries have something to measure: a grid collapses to one column by
   how wide *this* is, not the viewport. Pages compose HStack/VStack/
   LayoutGrid inside it rather than writing flex/grid inline. */
.cockpit-page {
    flex: 1;
    min-width: 0;
    min-height: 0;
    padding: 16px;
    container-type: inline-size;
    container-name: cockpit-page;
    /* Same size the /play rail (.cockpit-side) reads at - the form pages
       used to borrow that class purely for this, and its fixed 340px width
       came along for the ride. */
    font-size: 15px;
}

/* A single centred form or notice (settings, a countdown, an error) -
   capped so it reads as a card on a wide screen, full width on a phone.
   Sizing, not layout: put it inside a VStack/LayoutGrid, don't nest more
   layout inside it. */
.cockpit-col-narrow {
    width: min(440px, 100%);
}

.cockpit-side {
    display: flex;
    flex-direction: column;
    gap: 8px;
    width: 340px;
    flex-shrink: 0;
    font-size: 15px;
}

.cockpit-weapon-row,
.cockpit-status-grid,
.cockpit-nav-row,
.cockpit-opponent-row {
    display: flex;
    justify-content: space-between;
}

/* Keeps the weapon/crew row's name column ("Machine Gun · front, fixed") on one line instead of wrapping
   to a second - at the fit layout's ~244px rail column (1000x600, PR #293's breakpoint) a wrap pushed the
   whole rail past the viewport (tools/uitest/verify-combat-fit.mjs's railScroll check). min-width: 0
   overrides flex's default auto min-width, which otherwise refuses to shrink a row below its text's natural
   width and defeats text-overflow entirely. gap keeps the stat column from crowding the ellipsis when the
   name truncates. The full name is still available via the span's title attribute, and this only clips the
   name text - the stat span (tier colour, damage/range/WRECKED) is untouched. */
.cockpit-weapon-row {
    gap: 8px;
}

.cockpit-weapon-name {
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
    min-width: 0;
}

.cockpit-weapon-row > span:last-child {
    flex-shrink: 0;
}

/* docs/CREW-TRAITS-PLAN.md PR 5 review remediation - a display name can run up to 32 characters
   (DisplayNameChanger.MaxLength) plus a team prefix and a "(Waymo)"/disconnected suffix, routinely much
   longer than the old bare "Waymo-2". The ORIGINAL version of this fix put the ellipsis on the STATUS span
   (last-child) and kept the name/model spans at their natural width - backwards, it turned out: a 32-char
   name is exactly the span that needs to shrink, while ACTIVE/OUT (plus the new COMPACT not-scouted token,
   see OpponentDamageSummary.NotScoutedToken) must never be the one silently clipped mid-word, since losing
   track of whether a car is still in the fight is worse than a clipped name a tooltip can recover. So the
   NAME is the one with min-width:0/overflow:hidden/ellipsis now (.cockpit-opponent-name, shared with
   InspectBubble's own two-span header below, which has the identical "32-char name vs. a 220px-wide bubble"
   problem).

   A SECOND pass (still review remediation, caught live with a real 32-char name rather than a short one)
   found the rail row's own model column needs the identical treatment, not nowrap/flex-shrink:0: at the
   rail's ~244px column, "Alexandria Thunderclap VonDraxxx" plus "Standard Sedan" plus "active · ? Cond" is
   more text than fits even with the status column held fixed - giving ONLY the name flex:1/min-width:0 meant
   it alone absorbed the entire deficit and shrank to a sliver with no visible ellipsis character at all (and,
   with no gap between columns, visually fused into the model text right next to it: "Standard Sedanactive").
   .cockpit-opponent-model now shares the same ellipsis treatment (rail only - InspectBubble has no model span
   in this row at all), so flexbox's normal shrink math (proportional to each item's own natural width)
   distributes the squeeze across BOTH text columns instead of one disappearing while the other stays full -
   the model name is short and from a small fixed catalog, so it only loses a character or two in practice;
   the long name gives up the most, same as before, just never all the way to nothing. The row's own `gap`
   keeps the three columns visually separated even at their tightest. */
.cockpit-opponent-name,
.cockpit-opponent-row--rail > .cockpit-opponent-model {
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
    min-width: 0;
    flex: 1 1 auto;
}

.cockpit-opponent-row--rail {
    gap: 6px;
}

/* The status span (ACTIVE/OUT, or - rail only - that plus a compact "? Cond"/"? Load" not-scouted token,
   OpponentDamageSummary.NotScoutedToken) never shrinks below its own natural width, in EITHER row - short and
   fixed in the bubble, short-by-design (a token, never the full sentence) in the rail. */
.cockpit-opponent-status {
    white-space: nowrap;
    flex-shrink: 0;
}

/* Every Combatants row centers the camera on that car when clicked,
   regardless of .out/.hoverable - a wreck still has a real position
   worth jumping to. Scoped to those two classes rather than the bare
   .cockpit-opponent-row, which InspectBubble.razor's own header row also
   uses (inert there only because the bubble is pointer-events:none) - a
   pointer cursor over a row that can't actually be clicked would be
   misleading even if harmless. */
.cockpit-opponent-row.hoverable,
.cockpit-opponent-row.out {
    cursor: var(--cursor-action);
}

.cockpit-status-grid {
    flex-wrap: wrap;
    row-gap: 4px;
}

/* The inspect bubble's Damage row: one item per line rather than a single
   "1 TIRE OUT | 1 WPN OUT" string, which read as a run-on in the bubble's
   narrow width. Same "- " marker as .cockpit-participant-list, indented a
   little further since this sits under its own "Damage" label line rather
   than starting a panel. */
.cockpit-damage-list {
    list-style: none;
    margin: 0;
    padding: 0 0 0 12px;
}

.cockpit-damage-list li::before {
    content: "- ";
    color: var(--cockpit-green-dim);
}

.cockpit-status-grid span {
    width: 48%;
}

/* docs/DROPPED-WEAPONS-PLAN.md's "Underbody armor" added a 5th cell to the Armor row's own grid - sized
   so it still wraps to the same row count as the 4-cell Tires row beside it (3-per-row narrow, same 2
   rows a 2-per-row/4-item grid already took; 5-per-row at the wide breakpoint below, still 1 row) rather
   than growing the rail past the /play fit layout's viewport-tall budget (tools/uitest/verify-combat-fit.mjs).
   Two classes on the same element for specificity over the plain .cockpit-status-grid span rule above,
   regardless of source order. */
.cockpit-status-grid.cockpit-status-grid--armor span {
    width: 31%;
}

/* docs/RACING-PLAN.md Phase 4 - the "Race" badge beside a race map in the /lobby and /watch arena pickers: a small
   chequered-flag-white outlined tag, inline BEFORE the label (a long label ellipsises at the end, and took a trailing
   badge with it - found live) so it rides along in the closed select and every option. */
.cockpit-race-badge {
    display: inline-block;
    margin-right: 6px;
    padding: 0 4px;
    border: 1px solid #f4f4f4;
    border-radius: 3px;
    color: #f4f4f4;
    font-size: 0.75em;
    line-height: 1.4;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    vertical-align: middle;
}

/* docs/RACING-PLAN.md Phase 4 - the player's own row in a race's Standings panel. A tinted band (same negative-margin
   trick as .hoverable below, so the row keeps its width) plus bold, so "where am I" is a glance in a list of eight. */
.cockpit-opponent-row.is-own {
    margin: 0 -4px;
    padding: 0 4px;
    font-weight: 700;
    background-color: rgba(255, 255, 255, 0.08);
    cursor: var(--cursor-action);
}

.cockpit-opponent-row.out,
.cockpit-weapon-row.out {
    opacity: 0.45;
    text-decoration: line-through;
}

/* An opponent row that answers back: pointing at it rings that car on the
   arena (Play.razor's _hoveredOpponentId). The negative margin lets the
   highlight sit slightly proud of the text without moving anything - the
   row lives in a fixed-width rail, so growing it would reflow the panel.
   Only rows for cars still in the match get this class. */
.cockpit-opponent-row.hoverable {
    margin: 0 -4px;
    padding: 0 4px;
    transition: background-color var(--cockpit-dur) var(--cockpit-ease);
}

/* /play's "center on my car" button (map UI polish) - top-left corner of
   the arena, the one corner CameraViewport doesn't already use (the scale
   bar is bottom-right, Recenter is bottom-center - see their own CSS in
   CameraViewport.razor.css). Declared here rather than there because this
   button is Play.razor's own OverlayContent markup, not CameraViewport's -
   /watch has no "my car" to center on, so it never renders this at all.
   Sized well past the 44px touch-target minimum on every screen (not just
   a narrow one) rather than branching on a breakpoint for a few extra
   pixels on desktop. */
.camera-center-button {
    position: absolute;
    top: 10px;
    left: 10px;
    z-index: 2;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 44px;
    height: 44px;
    padding: 0;
    background: rgba(1, 18, 10, 0.85);
}

/* The Inspect panel, floated over the car on the arena instead of the side
   rail - see InspectBubble.razor/BubblePlacement.cs. top is set inline as a
   plain percentage of CameraViewport's own container; left is computed
   below from the inline --bubble-left custom property (see its own comment
   there) so the bubble's actual pixel box - not just its anchor fraction -
   stays inside the container on any screen size. Everything else here is
   the fixed visual shell. pointer-events:none - it's a readout, must never
   block a click/hover reaching the arena underneath it. Default sits above
   the car (tail on the bottom edge, pointing down at it); .tail-top is
   BubblePlacement's flip for a car too close to the top of the screen to
   fit the bubble above it. */
.inspect-bubble {
    position: absolute;
    /* --bubble-left (InspectBubble.razor, BubblePlacement.LeftPercent) is only ever clamped to a FRACTION
       of the view (SideMarginPercent) - fine on a wide desktop canvas, but on a narrow phone viewport the
       container itself can be well under this bubble's own max-width (220px), so a percent-only clamp
       still let it hang off the right edge and add horizontal page scroll (found live at 390px). 100cqw is
       this same container's real width in CSS pixels (container-type:inline-size is set on
       .camera-viewport-focus specifically for this, alongside the notch's own identical trick just below) -
       clamping the pixel position against it, using half of max-width as the margin, keeps the bubble's
       actual rendered box on screen at any container size, not just a wide one. */
    left: clamp(110px, var(--bubble-left), calc(100cqw - 110px));
    transform: translate(-50%, calc(-100% - 12px));
    /* Glides with the car it points at between playback steps (--car-step, set inline; docs/PLAN.md "Playback
       smoothness"), as the camera and the car glyphs do. */
    transition: left var(--car-step, 0ms) linear, top var(--car-step, 0ms) linear;
    min-width: 160px;
    max-width: 220px;
    /* Was 70% transparent (30% opaque) - reported live as too opaque, hiding
       the arena and cars underneath a hovered/pinned car. Owner feedback,
       2026-09-25 (after the sparser-dots pass made terrain patterns easier
       to read through the bubble): the tradeoff had gone too far the other
       way - text over a dotted surface was hard to read at 30% opaque, so
       this is back up to 50%, plus a light backdrop-filter blur so the
       terrain dots directly behind the text soften into a wash rather than
       competing with the letterforms, while the map still shows through.
       Same color-mix-over-transparent idiom .cockpit-spinner's border
       already uses, so this fixed --cockpit-bg thins out the same way a
       per-theme rgba() literal couldn't without hand-updating if that
       variable ever changed. */
    background: color-mix(in srgb, var(--cockpit-bg) 50%, transparent);
    -webkit-backdrop-filter: blur(2px);
    backdrop-filter: blur(2px);
    border: 1px solid var(--cockpit-green);
    box-shadow: 0 0 8px var(--cockpit-glow);
    padding: 6px 8px;
    font-size: 12px;
    /* .cockpit-field (this element's ancestor) sets line-height:0 to kill
       the inline-SVG descender gap - inherited here it collapses every
       row's line box to nothing, stacking all the text on top of itself.
       .camera-recenter resets it the same way for the same reason. */
    line-height: normal;
    z-index: var(--cockpit-z-popup);
    pointer-events: none;
}

.inspect-bubble.tail-top {
    transform: translate(-50%, 12px);
}

/* docs/WEAPON-DAMAGE-REBALANCE-PLAN.md sidearm follow-up S4 - the armed-passenger token's own line under the Model row
   ("Passenger: Shotgun (tires)"). The bubble is pointer-events:none, so a title tooltip can't reach it and an ellipsis would lose
   the intel for good: it wraps instead. The bubble floats over the map, outside the fit layout's rail budget. */
.inspect-bubble-passenger {
    text-align: right;
}

/* The one clickable thing inside .inspect-bubble (map UI polish) - opts
   itself back into pointer-events so a tap/click reaches it even though
   the bubble itself is pointer-events:none. Sized for a comfortable phone
   tap target (the bubble travels with the car on a small screen too, and
   there's no hover fallback there). Small margin-top so it doesn't crowd
   the header row right above it. */
.inspect-bubble-target-button {
    pointer-events: auto;
    display: block;
    width: 100%;
    margin-top: 4px;
    min-height: 32px;
    font-size: 12px;
    padding: 4px 8px;
}

.inspect-bubble-target-button.is-active {
    background: var(--cockpit-green);
    color: var(--cockpit-bg);
}

/* The notch - a real child of .inspect-bubble now (not a ::after), so its
   `left` can be driven by --notch-offset (InspectBubble.razor, a fraction
   of the whole view set by BubblePlacement) instead of a fixed 50%: a car
   near/off the side edge clamps the bubble's own LeftPercent to stay
   readable, but the notch still has to track the car's true position past
   that clamp. `100cqw` is 1% of CameraViewport's own rendered width per
   unit (see its container-type:inline-size, set specifically for this) -
   the same "percent of the whole view" unit --notch-offset is already
   expressed in - converted here to real pixels and added to 50% of *this
   element's own containing block* (the bubble, since it's a child now),
   then clamped to [7px, 100%-7px] so the notch travels the bubble's
   entire rendered width tracking the car exactly, but can never render
   outside the bubble's own rounded border no matter how far the car's
   true position has diverged from the bubble's clamped anchor. A rotated
   square, half-overlapping the bubble's edge with only the two borders
   that end up facing outward drawn, the same "diamond with two borders"
   technique every CSS speech-bubble tail uses. No existing tail/tooltip
   precedent anywhere else in this codebase to diverge from or match. */
.inspect-bubble-notch {
    position: absolute;
    left: clamp(7px, calc(50% + var(--notch-offset, 0) * 100cqw), calc(100% - 7px));
    width: 10px;
    height: 10px;
    /* Same transparency as .inspect-bubble itself, so the notch reads as
       part of the bubble rather than an opaque patch stuck to it. */
    background: color-mix(in srgb, var(--cockpit-bg) 30%, transparent);
    transform: translateX(-50%) rotate(45deg);
}

.inspect-bubble:not(.tail-top) .inspect-bubble-notch {
    bottom: -6px;
    border-right: 1px solid var(--cockpit-green);
    border-bottom: 1px solid var(--cockpit-green);
}

.inspect-bubble.tail-top .inspect-bubble-notch {
    top: -6px;
    border-left: 1px solid var(--cockpit-green);
    border-top: 1px solid var(--cockpit-green);
}

/* The car is genuinely outside the visible view horizontally - a notch
   would have nothing on-screen left to point at, so BubblePlacement swaps
   it for this instead: a small arrowhead sitting right at the screen's
   own edge, its tip touching the edge exactly and its base fanning inward
   - a directional cue ("keep looking that way") rather than a mere
   marker. A classic CSS border-triangle: a zero-size box with only one
   border colored and the two perpendicular to it transparent, which
   mitres them into the taper. edge-left points left (border-right is the
   flat base, no border-left at all collapses that side to the tip) with
   no further shift needed, since the tip already sits exactly on the box's
   own left%:0 anchor; edge-right mirrors that (border-left is the base)
   but then has to shift itself left by its own border width, since a
   box's near-zero-width anchor is always its *left* edge regardless of
   which side the triangle's point ends up on. */
.inspect-bubble-edge-arrow {
    position: absolute;
    width: 0;
    height: 0;
    border-top: 16px solid transparent;
    border-bottom: 16px solid transparent;
    filter: drop-shadow(0 0 3px var(--cockpit-glow));
    z-index: var(--cockpit-z-popup);
    pointer-events: none;
}

.inspect-bubble-edge-arrow.edge-left {
    border-right: 14px solid var(--cockpit-green);
    transform: translateY(-50%);
}

.inspect-bubble-edge-arrow.edge-right {
    border-left: 14px solid var(--cockpit-green);
    transform: translate(-14px, -50%);
}

.cockpit-bar-track {
    background: #062513;
    border: 1px solid var(--cockpit-green-dim);
    height: 14px;
    width: 100%;
    position: relative;
}

.cockpit-bar-fill {
    background: var(--cockpit-green);
    height: 100%;
}

.cockpit-bar-label {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 11px;
    color: #eafff2;
    mix-blend-mode: difference;
}

.cockpit-log {
    margin-top: 10px;
    border: 1px solid var(--cockpit-green-dim);
    background: #000;
    padding: 6px 10px;
    height: 120px;
    overflow-y: auto;
    font-size: 14px;
}

/* The log is the one panel that actually scrolls, so it's the one place a
   default OS scrollbar shows up - a pale chrome strip down the side of an
   otherwise black terminal. `scrollbar-color` covers Firefox; the
   ::-webkit- rules cover Chrome/Edge/Safari, which ignore it. Applied to
   any cockpit scroll region, not just the log, so a future one matches. */
.cockpit-log,
.cockpit-scroll,
.cockpit-rail-scroll-panel > .cockpit-panel-body,
.hall-scroll-panel > .cockpit-panel-body,
.hall-recent-list,
.hall-shell .cockpit-side-group,
.replay-shell .cockpit-side-group {
    scrollbar-width: thin;
    scrollbar-color: var(--cockpit-green-dim) #000;
}

.cockpit-log::-webkit-scrollbar,
.cockpit-scroll::-webkit-scrollbar,
.cockpit-rail-scroll-panel > .cockpit-panel-body::-webkit-scrollbar,
.hall-scroll-panel > .cockpit-panel-body::-webkit-scrollbar,
.hall-recent-list::-webkit-scrollbar,
.hall-shell .cockpit-side-group::-webkit-scrollbar,
.replay-shell .cockpit-side-group::-webkit-scrollbar {
    width: 10px;
}

.cockpit-log::-webkit-scrollbar-track,
.cockpit-scroll::-webkit-scrollbar-track,
.cockpit-rail-scroll-panel > .cockpit-panel-body::-webkit-scrollbar-track,
.hall-scroll-panel > .cockpit-panel-body::-webkit-scrollbar-track,
.hall-recent-list::-webkit-scrollbar-track,
.hall-shell .cockpit-side-group::-webkit-scrollbar-track,
.replay-shell .cockpit-side-group::-webkit-scrollbar-track {
    background: #000;
    border-left: 1px solid var(--cockpit-green-dim);
}

.cockpit-log::-webkit-scrollbar-thumb,
.cockpit-scroll::-webkit-scrollbar-thumb,
.cockpit-rail-scroll-panel > .cockpit-panel-body::-webkit-scrollbar-thumb,
.hall-scroll-panel > .cockpit-panel-body::-webkit-scrollbar-thumb,
.hall-recent-list::-webkit-scrollbar-thumb,
.hall-shell .cockpit-side-group::-webkit-scrollbar-thumb,
.replay-shell .cockpit-side-group::-webkit-scrollbar-thumb {
    background: var(--cockpit-green-dim);
    border: 2px solid #000;
}

.cockpit-log::-webkit-scrollbar-thumb:hover,
.cockpit-scroll::-webkit-scrollbar-thumb:hover,
.cockpit-rail-scroll-panel > .cockpit-panel-body::-webkit-scrollbar-thumb:hover,
.hall-scroll-panel > .cockpit-panel-body::-webkit-scrollbar-thumb:hover,
.hall-recent-list::-webkit-scrollbar-thumb:hover,
.hall-shell .cockpit-side-group::-webkit-scrollbar-thumb:hover,
.replay-shell .cockpit-side-group::-webkit-scrollbar-thumb:hover {
    background: var(--cockpit-green);
}

/* Windows Chrome puts stepper arrows at both ends of a scrollbar and
   renders them in OS chrome that no scrollbar property reaches. Nothing
   else in this UI has them; dropping them also matches Firefox's thin
   scrollbar, so the two browsers agree. */
.cockpit-log::-webkit-scrollbar-button,
.cockpit-scroll::-webkit-scrollbar-button {
    display: none;
}

.cockpit-checkbox-row input[type="checkbox"] {
    accent-color: var(--cockpit-green);
}

/* An opt-in fully cockpit-drawn checkbox: accent-color above only tints the CHECKED state - an unchecked box is still
   the OS's white square, the kind of native control that "pops up unstyled" on an otherwise black terminal page. The
   arena builder uses this (tools/uitest/verify-builder-styles.mjs checks for it); other pages keep the accent-color
   look until they opt in. The tick is a clip-path, so it can take var(--cockpit-green) directly. */
input[type="checkbox"].cockpit-checkbox {
    appearance: none;
    -webkit-appearance: none;
    flex: none;
    width: 16px;
    height: 16px;
    margin: 0;
    border: 1px solid var(--cockpit-green);
    background: #000;
    display: inline-grid;
    place-content: center;
    cursor: var(--cursor-action);
}

input[type="checkbox"].cockpit-checkbox::before {
    content: "";
    width: 10px;
    height: 10px;
    background: var(--cockpit-green);
    clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%);
    transform: scale(0);
    transition: transform var(--cockpit-dur-fast, 90ms) var(--cockpit-ease, ease);
}

input[type="checkbox"].cockpit-checkbox:checked::before {
    transform: scale(1);
}

input[type="checkbox"].cockpit-checkbox:focus-visible {
    outline: 2px solid var(--cockpit-green-bright);
    outline-offset: 2px;
}

input[type="checkbox"].cockpit-checkbox:disabled {
    opacity: 0.4;
    cursor: var(--cursor-not-allowed);
}

.cockpit-log-line::before {
    content: "> ";
    color: var(--cockpit-green-dim);
}

/* The log was forty lines of identical green, which made "did I hit
   anything" a reading exercise rather than a glance. Colour is by event
   kind, and the palette is the one already in use on the field, so a red
   line in the log and a red marker on the arena mean the same thing. */
.cockpit-log-line.is-hit { color: #ff8a7a; }
.cockpit-log-line.is-miss { color: #7f8c8d; }
.cockpit-log-line.is-collision { color: #f6ad55; }
.cockpit-log-line.is-control { color: #ecc94b; }
.cockpit-log-line.is-impact { color: var(--cockpit-red); }
.cockpit-log-line.is-hazard { color: var(--cockpit-amber); }
/* Same blue as the drowned status icon/glyph (Rendering/SurfaceRenderColors.Drowned) - a zone-change note is
   almost always about water, and the colour link makes the connection obvious at a glance. */
.cockpit-log-line.is-terrain { color: #7ec8f5; }
/* docs/RACING-PLAN.md Phase 4 - gate crossings, finishes, "weapons free": the chequered-flag white of the gates
   themselves (ArenaLayer draws them #f4f4f4), so a race line never reads as damage or a hazard. */
.cockpit-log-line.is-race { color: #f4f4f4; }
/* docs/WEAPON-MOUNT-ART-PLAN.md Phase 4b - why one of your own weapons fired nothing: a quiet violet, never a damage or hazard colour. */
.cockpit-log-line.is-nofire { color: #b9a7e8; }
/* A shot's instant in the turn, in front of its line - dimmer than the line itself, tabular so the stamps line up. */
.cockpit-log-time {
    opacity: 0.7;
    font-variant-numeric: tabular-nums;
}

/* Turn separators are the log's structure rather than its content - dimmer
   and letter-spaced so they read as rules between turns, not as events. */
.cockpit-log-line.is-structural {
    color: var(--cockpit-green-dim);
    letter-spacing: 1px;
}

/* Age banding, newest first. Older lines recede rather than competing with
   what just happened; three coarse bands rather than a per-line opacity,
   because this list re-renders ~20 times a second during playback. */
.cockpit-log-line.is-older { opacity: 0.62; }
.cockpit-log-line.is-oldest { opacity: 0.38; }

/* The newest line arrives rather than appearing. Only the inserted element
   runs this - see the @key note in Play.razor - so the list doesn't
   re-animate wholesale every time a line lands. */
.cockpit-log-line {
    animation: cockpit-log-arrive var(--cockpit-dur) var(--cockpit-ease);
    transition: opacity var(--cockpit-dur-slow) var(--cockpit-ease);
}

@keyframes cockpit-log-arrive {
    from { opacity: 0; transform: translateX(-6px); }
    to { opacity: 1; transform: none; }
}

@media (prefers-reduced-motion: reduce) {
    .cockpit-log-line {
        animation: none;
        transition: none;
    }
}

.cockpit-select,
.cockpit-input,
.cockpit-button {
    font-family: var(--cockpit-font);
    background: #000;
    color: var(--cockpit-green);
    border: 1px solid var(--cockpit-green);
    padding: 3px 6px;
    box-sizing: border-box;
}

.cockpit-select,
.cockpit-input {
    /* Below 16px, iOS Safari auto-zooms the whole page on focus (these
       inherit 15px from .cockpit-side otherwise) - an explicit 16px here
       is imperceptible on desktop but stops that zoom on a phone. */
    font-size: 16px;
    transition: border-color var(--cockpit-dur) var(--cockpit-ease),
                box-shadow var(--cockpit-dur) var(--cockpit-ease);
}

/* Strips the native control chrome (macOS bevel + arrow) so the closed
   select reads as cockpit rather than OS UI - Safari draws its own bevel
   here whenever appearance isn't overridden. The open option list is still
   OS-drawn either way (Safari allows no CSS on <option> or the popup),
   so this only covers the closed control - see PLAN.md "Dropdowns don't
   match the cockpit". Arrow is two gradient triangles rather than an SVG
   so it can reference var(--cockpit-green) directly. */
.cockpit-select {
    appearance: none;
    -webkit-appearance: none;
    padding-right: 24px;
    background-image:
        linear-gradient(45deg, transparent 50%, var(--cockpit-green) 50%),
        linear-gradient(135deg, var(--cockpit-green) 50%, transparent 50%);
    background-position: right 13px top 55%, right 8px top 55%;
    background-size: 5px 5px, 5px 5px;
    background-repeat: no-repeat;
}

/* The browser's own focus ring is suppressed here and re-added by the
   :focus-visible block further down, so a click leaves only the phosphor
   glow while a keyboard tab still gets a real, visible ring. */
.cockpit-select:focus,
.cockpit-input:focus {
    outline: none;
    border-color: var(--cockpit-green-bright);
    box-shadow: 0 0 8px var(--cockpit-glow);
}

/* CockpitSelect - the div/button/ARIA-listbox replacement for <select>,
   built specifically because Safari allows no CSS on <option> or the open
   popup itself. .cockpit-select above already covers the trigger's border/
   background/color/padding/arrow/focus/hover (class-scoped, so it applies
   to the trigger <button> for free); everything here is what's new. */
.cockpit-select-wrap {
    display: inline-block;
    position: relative;
}

.cockpit-select-trigger {
    text-align: left;
    cursor: var(--cursor-action);
}

.cockpit-select-trigger:disabled {
    opacity: 0.4;
    cursor: var(--cursor-not-allowed);
}

/* A <button> sizes to its current content, unlike a native <select> which
   sizes to its widest possible <option> - without this the trigger would
   visibly jump width as the value changes (e.g. Garage's Role select:
   "Passenger" vs. "Gunner: Slot 1 (MachineGun)"). Stacking every item's
   content in the same grid cell makes the cell (and so the button) size
   to the widest one, while only the actually-selected item stays visible. */
.cockpit-select-display {
    display: grid;
    /* Widest item, but never wider than the room the trigger has. */
    grid-template-columns: minmax(0, max-content);
}

.cockpit-select-display > * {
    grid-area: 1 / 1;
}

/* A ghost wider than a narrow column would wrap and make the trigger two
   lines tall around a one-line value (the Garage's Buy a Weapon Type list,
   whose longest item is "Micromissile Launcher (1 space) - Prestige 2") -
   so ghosts stay one clipped line; only the visible value may wrap. */
.cockpit-select-ghost {
    visibility: hidden;
    white-space: nowrap;
    overflow: hidden;
}

.cockpit-select-popup {
    position: absolute;
    top: calc(100% + 2px);
    left: 0;
    min-width: 100%;
    width: max-content;
    max-height: 240px;
    overflow-y: auto;
    background: var(--cockpit-bg);
    border: 1px solid var(--cockpit-green);
    box-shadow: 0 0 8px var(--cockpit-glow);
    z-index: var(--cockpit-z-popup);
}

/* CockpitSelect OpenUpWhenClipped: no room below inside a clipping ancestor - the list opens upward (cockpit-modal.js flipSelectPopup). */
.cockpit-select-popup.is-above {
    top: auto;
    bottom: calc(100% + 2px);
}

.cockpit-select-option {
    padding: 4px 8px;
    cursor: var(--cursor-action);
}

.cockpit-select-option.is-highlighted {
    background: rgba(51, 255, 119, 0.12);
}

/* An option that exists but can't be chosen (CockpitSelect's IsDisabled) -
   struck through and dim. This replaces the per-call-site
   .roster-picker-overcap span RosterCrewPicker used to draw, back when the
   select itself had no way to say "no". No pointer cursor and no hover
   highlight: it must not look clickable. */
.cockpit-select-option.is-disabled {
    text-decoration: line-through;
    color: var(--cockpit-green-dim);
    opacity: 0.6;
    cursor: var(--cursor-not-allowed);
}

@media (hover: hover) {
    .cockpit-select-option:not(.is-disabled):hover {
        background: rgba(51, 255, 119, 0.12);
    }
}

.cockpit-button {
    text-transform: uppercase;
    letter-spacing: 1px;
    cursor: var(--cursor-action);
    transition: background-color var(--cockpit-dur) var(--cockpit-ease),
                color var(--cockpit-dur) var(--cockpit-ease),
                border-color var(--cockpit-dur) var(--cockpit-ease),
                box-shadow var(--cockpit-dur) var(--cockpit-ease),
                translate var(--cockpit-dur-fast) var(--cockpit-ease);
}

.cockpit-button:hover:not(:disabled) {
    background: var(--cockpit-green-dim);
    color: var(--cockpit-bg);
    box-shadow: 0 0 8px var(--cockpit-glow);
}

/* A terminal key going down: 1px, no scale, and fast enough to read as the
   click itself rather than a reaction to it.

   `translate` (the standalone property), not `transform` - .camera-recenter
   already owns `transform` for its translateX(-50%) centering, and a press
   rule here would otherwise clobber it and fling the button off-center.
   The two properties compose instead of competing. */
.cockpit-button:active:not(:disabled) {
    background: var(--cockpit-green);
    color: var(--cockpit-bg);
    box-shadow: none;
    translate: 0 1px;
    transition-duration: var(--cockpit-dur-fast);
}

.cockpit-button:disabled {
    opacity: 0.4;
    cursor: var(--cursor-not-allowed);
}

/* Hold Fire / drop-caltrop: a full-width toggle rather than a small
   checkbox+label, both for a bigger tap target and so "on" is unmistakable -
   same inverted-fill language as .cockpit-flag-button.is-ready. */
.cockpit-toggle-button {
    display: block;
    width: 100%;
    padding: 8px;
    text-align: center;
    border: 2px solid var(--cockpit-green);
}

.cockpit-toggle-button.is-active {
    background: var(--cockpit-green);
    color: var(--cockpit-bg);
    box-shadow: 0 0 8px var(--cockpit-glow);
}

/* Specificity (0,4,0), beating the shared .cockpit-button:hover rule's
   (0,3,0) - without this, hovering an active toggle (or a tap's sticky
   post-touch hover, since this rule isn't confined to @media (hover: hover))
   would wash the "on" state out to the plain hover fill. */
.cockpit-button.cockpit-toggle-button.is-active:hover {
    background: var(--cockpit-green);
    color: var(--cockpit-bg);
}

/* An armed dispenser's drop-time slider (docs/DROPPED-WEAPONS-PLAN.md Phase 0): the shared CockpitSpeedSlider
   track filling the row, its "at 2.5s" readout beside it. Any reversing warning sits on its own line below. */
.cockpit-drop-time {
    display: flex;
    align-items: center;
    gap: 8px;
    margin-top: 4px;
}

.cockpit-drop-time .cockpit-speed-slider {
    flex: 1;
    min-width: 0;
}

.cockpit-drop-time .cockpit-hint {
    white-space: nowrap;
}

.cockpit-checkbox-row {
    display: flex;
    align-items: center;
    gap: 6px;
    transition: color var(--cockpit-dur) var(--cockpit-ease);
}

.cockpit-checkbox-row label,
.cockpit-checkbox-row input[type="checkbox"] {
    cursor: var(--cursor-action);
}

/* A label beside its own disabled input (Overworld's "Bring on trip" with a full convoy). By the input just before it, not by the row, since
   a row can hold several (Settings' radios). */
.cockpit-checkbox-row input:disabled + label {
    cursor: var(--cursor-not-allowed);
}

/* The checkbox's UA-default margin (WebKit: ~3px top/bottom) otherwise makes ITS margin box the tallest
   flex item in the row - taller than the label text - so align-items: center alone doesn't tighten the
   row; zeroing it here does, harmlessly, everywhere .cockpit-checkbox-row is used. */
.cockpit-checkbox-row input[type="checkbox"] {
    margin: 0;
}

/* CockpitCarPanel's per-mount "Hold facing this turn" row (a turret mount only - the Fixed-mount baseline
   the check above tunes for never renders it) - content tools/uitest/verify-combat-fit.mjs's turret-car
   Planning pass added on top of an already zero-slack rail budget at 1000x600. Same technique as
   .cockpit-weapon-name above: single line, ellipsis over wrap (Spanish's "Mantener orientación este turno"
   is longer than English's label and was the one that visibly wrapped), full text still on the title
   attribute. The compound selector below drops this row's usual .cockpit-hint top margin - scoped to
   .cockpit-checkbox-row.cockpit-hint together, the only place that pairs them - since there's no spare
   margin left in the budget for it; other bare .cockpit-hint or .cockpit-checkbox-row usages are untouched. */
.cockpit-checkbox-row.cockpit-hint {
    margin-top: 0;
}

.cockpit-checkbox-label {
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
    min-width: 0;
}

/* docs/WEAPON-DAMAGE-REBALANCE-PLAN.md Phase 2b: CockpitCarPanel's collapsed shot-hold row - two or more loaded one-shots share ONE row
   ("Hold: [1] [2] [3] ...", each box named in full by its aria-label) instead of a hold row under each rocket, which pushed the 1000x600
   rail past the viewport (tools/uitest/verify-limited-shots.mjs's five-rocket pass). The label may ellipsise; the boxes never shrink. */
.cockpit-hold-shots .cockpit-hold-shot {
    display: inline-flex;
    align-items: center;
    gap: 2px;
    flex: none;
}

/* A commit-style button that owns its whole row rather than sitting at
   its text's natural width - the primary action of a panel. */
.cockpit-button-wide {
    display: block;
    width: 100%;
    padding: 8px;
    font-size: 15px;
}

/* A link drawn as a button (the "See leaderboards" new-tab links): no underline, the label centred like a button's. */
a.cockpit-link-button {
    display: inline-block;
    text-decoration: none;
    text-align: center;
}

a.cockpit-link-button.cockpit-button-wide {
    display: block;
}

/* A hint that must stay one line (a map's description runs to a paragraph): ellipsised, the full text in the element's title. */
.cockpit-hint--one-line {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* A styled stand-in for the browser's own file input (/watch's genome picker): the real <input type=file> sits visually hidden inside a
   .cockpit-button label, so a click or the keyboard still opens the picker, and :focus-within gives it the same keyboard ring as a button. */
.cockpit-file-button {
    position: relative;
    text-align: center;
    box-sizing: border-box;
}

/* .cockpit-field-row label (small caps, 12px) would otherwise win over .cockpit-button-wide on this label. */
.cockpit-field-row label.cockpit-file-button {
    font-size: 15px;
    letter-spacing: 1px;
    margin-bottom: 0;
}

.cockpit-file-button:has(.cockpit-file-input:disabled) {
    opacity: 0.4;
    cursor: var(--cursor-not-allowed);
}

/* The label is never :disabled itself, so .cockpit-button's :hover/:active:not(:disabled) still match it: keep it looking at rest. It takes the pointer (no pointer-events: none) so not-allowed can show. */
.cockpit-file-button:has(.cockpit-file-input:disabled):is(:hover, :active) {
    background: #000;
    color: var(--cockpit-green);
    box-shadow: none;
    translate: none;
}

/* A picker row whose control fills the rail (/watch's Scenario panel): the trigger takes the full width and a long value is cut with an
   ellipsis instead of wrapping to a second line; the full text is still in the open list. */
.cockpit-field-row--stretch .cockpit-select-wrap {
    display: block;
}

.cockpit-field-row--stretch .cockpit-select-trigger {
    width: 100%;
}

.cockpit-field-row--stretch .cockpit-select-display {
    grid-template-columns: minmax(0, 1fr);
}

.cockpit-field-row--stretch .cockpit-select-value {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* /play's Fire priority picker: a long display name (32 characters, plus a team prefix) is cut with an ellipsis on one line instead of
   wrapping the trigger to three - the full names are in the open list. With several opponents the picker sits inside a CockpitTooltip,
   whose "?" keeps its place beside it. */
.cockpit-fire-priority .cockpit-select-wrap {
    max-width: 100%;
    vertical-align: middle;
}

/* With several opponents the picker sits in a CockpitTooltip: its wrapper takes the panel's width as a flex row (picker, then
   its "?"), so the picker is as wide as its longest name allows - a percentage max-width inside the shrink-to-fit inline wrapper
   resolved against the wrapper itself and squeezed the trigger to "Wa..." (PR #534 review). */
.cockpit-fire-priority .cockpit-tooltip-wrap {
    display: flex;
    max-width: 100%;
}

.cockpit-fire-priority .cockpit-tooltip-trigger {
    min-width: 0;
}

/* The open list wins over the picker's own tooltip: a click both hovers and focuses the trigger, and the bubble (same z-layer,
   later in the DOM) would paint over the upward-opening list's lower options. */
.cockpit-tooltip-wrap:has(.cockpit-select-popup:not([hidden])) .cockpit-tooltip-bubble {
    display: none;
}

.cockpit-fire-priority .cockpit-select-trigger {
    max-width: 100%;
}

.cockpit-fire-priority .cockpit-select-value,
.cockpit-fire-priority .cockpit-select-ghost {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.cockpit-file-input {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    opacity: 0;
    overflow: hidden;
    cursor: var(--cursor-action);
}

/* The pseudo-element has its own UA cursor and does not inherit the input's. */
.cockpit-file-input::file-selector-button {
    cursor: var(--cursor-action);
}

.cockpit-file-input:disabled::file-selector-button {
    cursor: var(--cursor-not-allowed);
}

.cockpit-file-button:focus-within {
    outline: 2px solid var(--cockpit-green-bright);
    outline-offset: 2px;
}

.cockpit-speed-row {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 8px;
}

.cockpit-speed-row label {
    text-transform: uppercase;
    letter-spacing: 0.5px;
    font-size: 12px;
}

/* The big READY button: a checkered-flag banner with the label on a
   solid plate across the middle so it stays legible over the pattern.
   The checker is img/checker-tile.svg rather than a CSS gradient so the
   same artwork backs the button, race-flags.svg, and anything later. */
.cockpit-flag-button {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 10px;
    width: 100%;
    min-height: 64px;
    padding: 0;
    border: 3px solid var(--cockpit-green);
    background-color: #0d0d0d;
    background-image: url("../img/checker-tile.svg");
    background-size: 24px 24px;
    font-family: var(--cockpit-font);
    cursor: var(--cursor-action);
    transition: box-shadow var(--cockpit-dur) var(--cockpit-ease),
                border-color var(--cockpit-dur) var(--cockpit-ease),
                translate var(--cockpit-dur-fast) var(--cockpit-ease);
}

.cockpit-flag-button-label {
    display: flex;
    align-items: center;
    gap: 10px;
    /* Opaque, so the checker never runs behind the text. */
    background: var(--cockpit-bg);
    color: var(--cockpit-green);
    border-left: 2px solid var(--cockpit-green);
    border-right: 2px solid var(--cockpit-green);
    padding: 12px 20px;
    font-size: 22px;
    font-weight: bold;
    letter-spacing: 6px;
    text-transform: uppercase;
    white-space: nowrap;
    transition: background-color var(--cockpit-dur) var(--cockpit-ease),
                color var(--cockpit-dur) var(--cockpit-ease);
}

/* The label is nowrap, so "CREATE MATCH" (22px, 6px tracking) is 348px wide and overflows a 320px phone: tighten it below 360px. */
@media (max-width: 359px) {
    .cockpit-flag-button-label {
        font-size: 17px;
        letter-spacing: 3px;
        padding: 12px 10px;
        gap: 6px;
    }
}

.cockpit-flag-button-flags {
    /* Small enough to sit inside the 64px banner, big enough that the
       flags still read as flags rather than as a smudge. */
    height: 40px;
    width: auto;
    flex-shrink: 0;
    /* The artwork is black-on-white like a real race flag, so it needs a
       light ground: straight on the dark label plate, its black half -
       poles, and half the squares - simply disappears. */
    background: #ffffff;
    border-radius: 3px;
    padding: 2px 4px;
}

.cockpit-flag-button:hover:not(:disabled) .cockpit-flag-button-label {
    background: var(--cockpit-green);
    color: var(--cockpit-bg);
}

/* Hovering waves the flag: the checker scrolls one full tile (24px, the
   background-size above) so the loop is seamless. Only on hover - ambient
   motion on a resting button would pull the eye off the arena. */
.cockpit-flag-button:hover:not(:disabled) {
    box-shadow: 0 0 14px var(--cockpit-green-dim);
    animation: cockpit-checker-scroll 1.6s linear infinite;
}

@keyframes cockpit-checker-scroll {
    from { background-position: 0 0; }
    to { background-position: 24px 0; }
}

.cockpit-flag-button:active:not(:disabled) {
    translate: 0 1px;
    transition-duration: var(--cockpit-dur-fast);
}

.cockpit-flag-button:disabled {
    opacity: 0.35;
    cursor: var(--cursor-not-allowed);
    filter: grayscale(1);
}

/* Already committed - the button stays live (the build can still be
   edited and re-submitted) but reads as armed rather than as a
   call to action. */
.cockpit-flag-button.is-ready .cockpit-flag-button-label {
    background: var(--cockpit-green);
    color: var(--cockpit-bg);
}

/* ---- Countdowns ---------------------------------------------------------
   Two of them, sharing an urgency language but little else: the garage's
   3-2-1 before the arena (a three-beat event) and /play's auto-pilot
   takeover warning (a slow drain). */

/* A numeral inside a ring that sweeps away over each second. Both the
   numeral and the sweep are re-created every tick - Garage.razor puts an
   @key on the count - which is what restarts their animations. A CSS
   animation on an element that merely changes its text plays once and
   never again. */
.cockpit-countdown {
    position: relative;
    width: 170px;
    height: 170px;
    margin: 4px auto 0;
}

.cockpit-countdown-ring {
    width: 100%;
    height: 100%;
    /* Start the sweep at twelve o'clock rather than at three. */
    transform: rotate(-90deg);
}

.cockpit-countdown-track {
    fill: none;
    stroke: var(--cockpit-green-dim);
    stroke-width: 2;
    opacity: 0.5;
}

.cockpit-countdown-sweep {
    fill: none;
    stroke: var(--cockpit-green);
    stroke-width: 4;
    stroke-linecap: round;
    /* 2 * pi * 45, the radius in the 100x100 viewBox - the entire
       circumference as a single dash, so animating the offset wipes it
       away exactly once with no repeat. */
    stroke-dasharray: 282.743;
    animation: cockpit-countdown-sweep 1s linear forwards;
}

@keyframes cockpit-countdown-sweep {
    from { stroke-dashoffset: 0; }
    to { stroke-dashoffset: 282.743; }
}

.cockpit-countdown-number {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 62px;
    font-weight: bold;
    color: var(--cockpit-green);
    text-shadow: 0 0 20px var(--cockpit-glow);
    animation: cockpit-countdown-punch var(--cockpit-dur-slow) var(--cockpit-ease);
}

/* Arrives slightly oversized and settles, rather than growing into place -
   a numeral that lands reads as a clock tick, one that scales up reads as
   a zoom effect. */
@keyframes cockpit-countdown-punch {
    from { opacity: 0; transform: scale(1.6); }
    to { opacity: 1; transform: scale(1); }
}

/* The final beat. Stays green - this is the go signal, not a warning - and
   holds rather than sweeping, since nothing follows it. */
.cockpit-countdown.is-go .cockpit-countdown-number {
    font-size: 46px;
    letter-spacing: 4px;
}

.cockpit-countdown.is-go .cockpit-countdown-sweep {
    animation: none;
    stroke-dashoffset: 0;
}

/* /play's auto-pilot warning. .cockpit-connection-banner supplies the
   frame; this adds the depleting bar and three urgency tiers, which is the
   difference between "you have a minute" and "this is happening now" -
   information the old static amber strip carried only in its text. */
.cockpit-countdown-banner {
    position: relative;
    overflow: hidden;
    border-color: var(--cockpit-green-dim);
    color: var(--cockpit-green);
}

.cockpit-countdown-bar {
    position: absolute;
    left: 0;
    bottom: 0;
    height: 3px;
    background: var(--cockpit-green);
    /* 1s is not a motion token and must not become one: it matches the tick
       interval of the loop that re-renders the width, so a once-a-second
       step renders as a continuous drain. Driving the bar from the deadline
       this way is what avoids re-rendering the page at 60fps just to move a
       progress bar. Because it isn't a token, the reduced-motion block at
       the bottom of this file has to switch it off by hand. */
    transition: width 1s linear, background-color var(--cockpit-dur) var(--cockpit-ease);
}

.cockpit-countdown-banner.is-warning {
    border-color: var(--cockpit-amber);
    color: var(--cockpit-amber);
}

.cockpit-countdown-banner.is-warning .cockpit-countdown-bar {
    background: var(--cockpit-amber);
}

.cockpit-countdown-banner.is-critical {
    border-color: var(--cockpit-red);
    color: var(--cockpit-red);
    animation: cockpit-countdown-alarm 1s ease-in-out infinite;
}

.cockpit-countdown-banner.is-critical .cockpit-countdown-bar {
    background: var(--cockpit-red);
}

/* One pulse per second, in step with the countdown's own tick. */
@keyframes cockpit-countdown-alarm {
    0%, 100% { opacity: 1; }
    50% { opacity: 0.55; }
}

.cockpit-error {
    color: var(--cockpit-amber);
}

/* A danger zone (e.g. Profile's "Delete my game data"): the whole panel reads
   red instead of phosphor green so nobody mistakes an irreversible action for
   an ordinary settings block. Put this on a CockpitPanel's Class - the panel,
   its title/dashes, text, hints, inputs and buttons all take their color from
   these custom properties, so remapping them here restyles every child with no
   per-element rules (the panel's own text color is set explicitly, since body
   text inherits an already-resolved green from .cockpit-shell). */
.cockpit-danger {
    --cockpit-green: var(--cockpit-red);
    --cockpit-green-bright: #ff8a8a;
    --cockpit-green-dim: #a32626;
    --cockpit-glow: rgba(255, 59, 59, 0.35);
    --cockpit-panel-bg: #1c0606;
    --cockpit-panel-hover-bg: #270808;
    color: var(--cockpit-red);
}

/* The hover fill is --cockpit-green-dim (now a dark red) under dark text -
   too murky on a button that deletes everything. Full red instead. */
.cockpit-danger .cockpit-button:hover:not(:disabled) {
    background: var(--cockpit-red);
    color: var(--cockpit-bg);
}

/* Same color as .cockpit-error - amber reads as caution regardless of
   whether the underlying issue blocks Ready (error) or not (warning) -
   just a distinct class so the two concepts don't share a name. */
.cockpit-warning {
    color: var(--cockpit-amber);
}

/* docs/FIRE-AND-EXPLOSIONS-PLAN.md Phase 1 - the Car panel's ON FIRE line: a warning, but in red, since the car is
   taking damage every turn it stays lit (amber is reserved for "worth knowing", e.g. WEAPONS LOCKED). */
.cockpit-warning.cockpit-on-fire {
    color: var(--cockpit-red);
    font-weight: bold;
}

.cockpit-hint {
    color: var(--cockpit-green);
    font-size: 12px;
    margin-top: 4px;
}

/* Small inline indicator for a brief async wait inside a .cockpit-hint line
   (e.g. Lobby.razor's cold-start "checking for a saved sign-in" state) -
   a spinning ring rather than text alone, so a multi-second network round
   trip doesn't look identical to a static, already-resolved message. */
.cockpit-spinner {
    display: inline-block;
    width: 10px;
    height: 10px;
    margin-right: 4px;
    border: 2px solid color-mix(in srgb, var(--cockpit-green) 30%, transparent);
    border-top-color: var(--cockpit-green);
    border-radius: 50%;
    vertical-align: -1px;
    animation: cockpit-spin 0.7s linear infinite;
}

@keyframes cockpit-spin {
    to {
        transform: rotate(360deg);
    }
}

/* The result screen's win/lose sentence - reads as a headline against the
   plain body text of everything else in the Controls panel. */
.cockpit-result-headline {
    font-size: 16px;
    font-weight: bold;
    letter-spacing: 1px;
    text-transform: uppercase;
    margin-bottom: 8px;
}

/* Default link blue is the one bit of browser chrome that still shows
   through inside a phosphor terminal. */
.cockpit-link {
    color: var(--cockpit-green);
    text-decoration: underline;
    transition: color var(--cockpit-dur) var(--cockpit-ease),
                background-color var(--cockpit-dur) var(--cockpit-ease);
}

/* The QR itself has to stay dark-on-light to scan reliably, so the
   terminal styling goes on the frame around it: a green-bordered plate
   with a little white padding, which doubles as the quiet zone scanners
   need to find the code's edges. */
.cockpit-qr {
    width: 170px;
    margin: 8px auto 4px;
    padding: 6px;
    background: #ffffff;
    border: 2px solid var(--cockpit-green);
    line-height: 0;
}

.cockpit-qr svg {
    display: block;
    width: 100%;
    height: auto;
}

.cockpit-link:hover {
    color: var(--cockpit-bg);
    background: var(--cockpit-green);
    text-decoration: none;
}

.cockpit-connection-banner {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 10px;
    padding: 6px 10px;
    margin-bottom: 10px;
    border: 1px solid var(--cockpit-amber);
    color: var(--cockpit-amber);
    text-transform: uppercase;
    letter-spacing: 1px;
}

.cockpit-controls > div {
    margin-top: 6px;
}

.cockpit-controls > div:first-child {
    margin-top: 0;
}

.cockpit-field-row {
    margin-top: 8px;
}

.cockpit-field-row:first-child {
    margin-top: 0;
}

.cockpit-field-row label {
    display: block;
    text-transform: uppercase;
    letter-spacing: 0.5px;
    font-size: 12px;
    margin-bottom: 3px;
}

/* First tabbed panel in the app - Settings' General/Key Bindings split.
   No underline-style indicator; the active tab just gets the same
   inverted-fill "armed" look every other selected control in the cockpit
   uses (.cockpit-flag-button.is-ready, .cockpit-toggle-button.is-active). */
.cockpit-tab {
    font-family: var(--cockpit-font);
    padding: 6px 14px;
    text-transform: uppercase;
    letter-spacing: 0.5px;
    font-size: 12px;
    border: 2px solid var(--cockpit-green);
    background: transparent;
    color: var(--cockpit-green);
    cursor: var(--cursor-action);
}

/* Dimmed like a disabled .cockpit-button: the team-count row (/lobby, /watch) disables the counts a map does not allow, and they must not look pressable. */
.cockpit-tab:disabled {
    opacity: 0.4;
    cursor: var(--cursor-not-allowed);
}

.cockpit-tab.is-active {
    background: var(--cockpit-green);
    color: var(--cockpit-bg);
}

.cockpit-participant-list {
    list-style: none;
    padding: 0;
    margin: 0;
}

.cockpit-participant-list li {
    padding: 2px 0;
}

.room-ride-row {
    padding: 6px 0;
    border-bottom: 1px solid var(--cockpit-green-dim);
}

/* A text-style disclosure toggle (the lobby's "Match options"): a real <button> for keyboard and screen readers, drawn like a link -
   no browser-default fill. */
.cockpit-disclosure {
    background: none;
    border: 0;
    padding: 0;
    color: var(--cockpit-green);
    font: inherit;
    text-decoration: underline;
    cursor: var(--cursor-action);
}

.cockpit-disclosure:focus-visible {
    outline: 1px solid var(--cockpit-green);
    outline-offset: 2px;
}

/* The lobby top bar's The Pits > Match > Ride > Crew > Start trail (docs/TUTORIAL-PLAN.md Phase 3b): the current step lit, the ones behind
   the player in plain green, the ones ahead dim, so it reads as progress rather than a status word. Under 600 px the whole trail does not
   fit beside the menu, so only its "Step N of M · name" line shows. */
.lobby-trail {
    display: inline-flex;
    flex-wrap: wrap;
    gap: 6px;
    align-items: center;
}

.lobby-trail-step {
    opacity: 0.45;
}

.lobby-trail-step.is-done {
    opacity: 0.8;
}

.lobby-trail-compact {
    display: none;
    color: var(--cockpit-green);
}

@media (max-width: 600px) {
    /* The steps are only visually hidden, so a screen reader still hears the whole trail with its aria-current step (the compact line is
       aria-hidden: it repeats them). */
    .lobby-trail-step {
        position: absolute;
        width: 1px;
        height: 1px;
        margin: -1px;
        padding: 0;
        overflow: hidden;
        clip: rect(0 0 0 0);
        clip-path: inset(50%);
        white-space: nowrap;
        border: 0;
    }

    .lobby-trail-sep {
        display: none;
    }

    .lobby-trail-compact {
        display: inline;
    }
}

.lobby-trail-step.is-current {
    opacity: 1;
    color: var(--cockpit-green);
    text-shadow: 0 0 6px var(--cockpit-green);
}

.lobby-trail-sep {
    opacity: 0.45;
}

/* Arena Chrome balance: a labelled value of its own, not another word in the trail's uppercase run. */
.lobby-chrome {
    margin-left: 4px;
}

.lobby-chrome-label {
    opacity: 0.6;
    margin-right: 4px;
}

/* The Pits (docs/TUTORIAL-PLAN.md Phase 2): the /lobby front door's work area beside the conversation rail. */
.pits-title {
    margin: 0;
    font-size: 28px;
    letter-spacing: 3px;
    text-transform: uppercase;
    color: var(--cockpit-green);
    text-shadow: 0 0 8px var(--cockpit-green-dim);
}

.pits-subtitle {
    margin-top: 2px;
}

/* The name box needs no more than a long name's width - full width on a wide screen read as a page-wide form. */
.pits-identity {
    max-width: 520px;
}

/* One option inside a card (Quick bout, Custom match): a name row, a line saying what it does, its control. */
.pits-option {
    display: flex;
    flex-direction: column;
    gap: 6px;
}

.pits-option + .pits-option {
    margin-top: 14px;
    padding-top: 14px;
    border-top: 1px solid var(--cockpit-green-dim);
}

.pits-option-head {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 8px;
}

.pits-option-name,
.pits-option-heading {
    margin: 0;
}

.pits-option-name {
    font-size: 18px;
    font-weight: bold;
    color: var(--cockpit-green);
}

.pits-tag {
    border: 1px solid var(--cockpit-amber);
    color: var(--cockpit-amber);
    padding: 1px 6px;
    font-size: 12px;
    letter-spacing: 1px;
    text-transform: uppercase;
}

.pits-wide-button {
    width: 100%;
}

.pits-option-heading {
    font-size: inherit;
    font-weight: normal;
}

.pits-custom-toggle {
    min-width: 0;
}

/* The custom pickers use the card's width: two columns when there is room, the hints spanning both. */
.pits-options {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
    column-gap: 16px;
}

.pits-options > .cockpit-hint {
    grid-column: 1 / -1;
}

/* One big code box: match codes are short, upper-case, and typed from a friend's message. */
.pits-code-input {
    width: 100%;
    box-sizing: border-box;
    font-size: 26px;
    letter-spacing: 6px;
    text-align: center;
    text-transform: uppercase;
    padding: 10px 8px;
}

.pits-hall-line {
    margin-top: 12px;
}

.room-actions {
    margin-top: 10px;
}

.room-ready-badge {
    color: var(--cockpit-green);
    font-weight: bold;
}

/* The room as steps (docs/TUTORIAL-PLAN.md Phase 3a, Room/RoomSteps): one pane per step, the room strip above Ride and Crew, a footer.
   Since Phase 3b the conversation rail sits on the left of every step (ConversationLayout); the work area beside it is the old pane.
   Capped and centred rather than fixed-width, so a phone gets the whole width. */
.room-steps {
    width: min(1280px, 100%);
    margin: 0 auto;
    display: flex;
    flex-direction: column;
    gap: 10px;
    box-sizing: border-box;
}

.room-steps-work {
    display: flex;
    flex-direction: column;
    gap: 10px;
    min-width: 0;
}

/* Takes the focus when a step changes (the pressed button is gone); a ring around the whole pane would only distract. */
.room-steps:focus {
    outline: none;
}

/* The Match step (docs/TUTORIAL-PLAN.md Phase 3b, Pages/Lobby.razor MatchStep): arena cards with a map thumbnail, Division cards,
   segmented seats / mode / who can join, and the room's footer - sticky to the bottom on a phone so Next is always in reach. */
.match-step {
    display: flex;
    flex-direction: column;
    gap: 16px;
    min-width: 0;
}

.match-section {
    display: flex;
    flex-direction: column;
    gap: 6px;
    min-width: 0;
}

.match-label {
    margin: 0;
    font-size: 12px;
    font-weight: normal;
    text-transform: uppercase;
    letter-spacing: 1.5px;
    opacity: 0.75;
}

.match-dim {
    opacity: 0.6;
}

.match-arenas {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
    gap: 10px;
}

.match-arena-card,
.match-division-card {
    display: flex;
    flex-direction: column;
    gap: 3px;
    padding: 0;
    text-align: left;
    font: inherit;
    color: inherit;
    background: var(--cockpit-panel-bg);
    border: 1px solid var(--cockpit-green-dim);
    cursor: var(--cursor-action);
    min-width: 0;
}

.match-arena-card:hover:not(:disabled),
.match-division-card:hover:not([aria-disabled="true"]) {
    background: var(--cockpit-panel-hover-bg);
}

.match-arena-card:focus-visible,
.match-division-card:focus-visible {
    outline: 2px solid var(--cockpit-green-bright);
    outline-offset: 2px;
}

.match-arena-card.is-selected,
.match-division-card.is-selected {
    border-color: var(--cockpit-green);
    box-shadow: 0 0 0 2px var(--cockpit-green);
}

.match-arena-name,
.match-division-name {
    color: var(--cockpit-green);
    font-weight: bold;
    overflow-wrap: anywhere;
}

.match-arena-name {
    padding: 6px 8px 0;
}

.match-arena-meta {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 6px;
    padding: 0 8px 8px;
    font-size: 12px;
    opacity: 0.8;
}

/* The map thumbnail: the arena's own geometry (Lobby/ArenaThumbnail), drawn in the cockpit's greens. */
.arena-thumb {
    display: block;
    width: 100%;
    height: 90px;
    background: var(--cockpit-panel-hover-bg);
}

.arena-thumb-bounds {
    fill: rgba(51, 255, 119, 0.04);
    stroke: var(--cockpit-green-dim);
}

.arena-thumb-obstacle {
    stroke: var(--cockpit-green);
    stroke-linecap: round;
    opacity: 0.75;
}

.arena-thumb-gate {
    stroke: var(--cockpit-amber);
    stroke-linecap: round;
}

.arena-thumb-spawn {
    fill: var(--cockpit-green-bright);
}

.match-divisions {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
    gap: 10px;
}

.match-division-card {
    padding: 10px;
    min-height: 80px;
}

.match-division-card[aria-disabled="true"] {
    opacity: 0.5;
    cursor: var(--cursor-not-allowed);
}

.match-division-line {
    font-size: 12px;
    opacity: 0.85;
}

.match-division-lock {
    color: var(--cockpit-amber);
}

.match-segments {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
    gap: 16px;
}

/* Segmented buttons: the cockpit's tab look, joined, wrapping when a big map offers many seat counts. */
.match-seg {
    display: flex;
    flex-wrap: wrap;
    gap: 4px;
}

.match-seg .cockpit-tab {
    min-width: 44px;
    min-height: 44px;
}

@container conversation-layout (max-width: 599px) {
    /* A phone swipes through the arenas rather than scrolling past a dozen cards. */
    .match-arenas {
        grid-template-columns: none;
        grid-auto-flow: column;
        grid-auto-columns: 150px;
        overflow-x: auto;
        scroll-snap-type: x mandatory;
        padding-bottom: 6px;
    }

    .match-arena-card {
        scroll-snap-align: start;
    }

    .match-seg .cockpit-tab {
        flex: 1 1 auto;
    }
}

/* "Next: your ride" is longer than the room's "Next": a smaller label, and the column sized to it rather than capped. */
.room-step-footer.match-step-footer {
    grid-template-columns: auto minmax(0, 1fr) auto;
}

.match-step-footer .cockpit-flag-button-label {
    font-size: 16px;
    letter-spacing: 3px;
}

@media (max-width: 600px) {
    /* The phone footer: the summary on one line over Back | Next, so the sticky bar stays short. */
    .room-step-footer.match-step-footer {
        grid-template-columns: auto minmax(0, 1fr);
        grid-template-areas: "summary summary" "back next";
    }

    .match-step-footer .room-step-footer-back .cockpit-button {
        width: auto;
        min-height: 48px;
    }

    .match-step-footer .room-step-position {
        display: none;
    }

    .room-step-footer.match-step-footer {
        position: sticky;
        bottom: 0;
        z-index: 1;
        background: var(--cockpit-bg);
        padding-bottom: calc(8px + var(--cockpit-safe-bottom));
    }
}

.room-strip {
    display: flex;
    flex-direction: column;
    gap: 6px;
    padding: 8px 10px;
    border: 1px solid var(--cockpit-green-dim);
    background: rgba(51, 255, 119, 0.04);
}

.room-strip-invite {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 8px;
}

.room-strip-label {
    opacity: 0.6;
    text-transform: uppercase;
    letter-spacing: 1px;
    font-size: 12px;
}

.room-strip-code {
    font-size: 20px;
    letter-spacing: 4px;
    color: var(--cockpit-green);
}

.room-strip-link {
    flex: 1 1 220px;
    min-width: 0;
}

.room-strip-qr {
    margin: 4px 0;
}

.room-strip-seats {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    margin: 0;
    padding: 0;
    list-style: none;
}

.room-strip-seat {
    display: inline-flex;
    gap: 6px;
    align-items: baseline;
    padding: 2px 8px;
    border: 1px solid var(--cockpit-green-dim);
    font-size: 13px;
}

.room-strip-seat.is-ready {
    border-color: var(--cockpit-green);
}

.room-strip-seat.is-ready .room-strip-seat-state {
    color: var(--cockpit-green);
    font-weight: bold;
}

.room-strip-seat.is-not-ready .room-strip-seat-state {
    color: var(--cockpit-amber);
}

.room-strip-seat.is-open {
    border-style: dashed;
    opacity: 0.7;
}

.room-strip-seat.is-mine .room-strip-seat-name {
    font-weight: bold;
}

.room-strip-status {
    display: flex;
    flex-wrap: wrap;
    gap: 4px 12px;
}

/* Back | where you are | Next; reasons on their own row under it. Stacks on a phone, Next first so the thumb finds it. */
.room-step-footer {
    display: grid;
    grid-template-columns: auto 1fr minmax(160px, 240px);
    grid-template-areas: "back summary next";
    align-items: center;
    gap: 6px 12px;
    padding-top: 8px;
    border-top: 1px solid var(--cockpit-green-dim);
}

.room-step-footer-back { grid-area: back; }
.room-step-footer-summary { grid-area: summary; display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.room-step-footer-next { grid-area: next; }

.room-step-footer-next .cockpit-flag-button {
    min-height: 48px;
}

/* Why the primary button is off (Room/RoomBlockNotice). The notices sit directly above Next, spanning the footer; where there is room, beside it.
   While a real problem holds it off the footer sticks to the bottom: the crew list can run past the fold, and the reason must show the moment it
   becomes true (a quiet wait or instruction does not stick). The scroll padding, about the footer's tallest, keeps a Tabbed-to control from landing behind it. */
.room-step-footer-notices {
    grid-area: notices;
    display: flex;
    flex-direction: column;
    gap: 6px;
    min-width: 0;
}

.room-step-footer.has-notices {
    grid-template-areas: "notices notices notices" "back summary next";
}

@media (min-height: 560px) {
    .room-step-footer.has-notices:has(.room-block) {
        position: sticky;
        bottom: 0;
        z-index: 1;
        background: var(--cockpit-bg);
        padding-bottom: calc(8px + var(--cockpit-safe-bottom));
    }

    html:has(.room-step-footer .room-block) {
        scroll-padding-bottom: 220px;
    }
}

/* The Match step's phone footer is always stuck (above): too tall for that with a notice on a very short screen. */
@media (max-height: 559px) {
    .room-step-footer.match-step-footer.has-notices {
        position: static;
    }
}

@container conversation-layout (min-width: 1200px) {
    .room-step-footer.has-notices {
        grid-template-columns: auto minmax(100px, 1fr) minmax(0, 3fr) minmax(160px, 240px);
        grid-template-areas: "back summary notices next";
    }

    .room-step-footer.match-step-footer.has-notices {
        grid-template-columns: auto minmax(100px, 1fr) minmax(0, 3fr) auto;
    }
}

/* The cockpit's alarm red (--cockpit-red), not .cockpit-error's amber alone: this is the one thing stopping the player. */
.room-block {
    display: flex;
    align-items: flex-start;
    gap: 10px;
    padding: 8px 10px;
    border: 1px solid var(--cockpit-red);
    border-left-width: 4px;
    background: color-mix(in srgb, var(--cockpit-red) 14%, var(--cockpit-bg));
    color: var(--cockpit-red);
    overflow-wrap: anywhere;
}

.room-block-marker {
    flex: none;
    width: 20px;
    height: 20px;
    line-height: 20px;
    text-align: center;
    font-weight: bold;
    background: var(--cockpit-red);
    color: var(--cockpit-bg);
}

.room-block-text {
    min-width: 0;
}

.room-block-reason {
    font-weight: bold;
}

.room-block-fix {
    margin-top: 2px;
}

.room-step-footer-leave {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 6px 12px;
}

.room-step-position {
    text-transform: uppercase;
    letter-spacing: 1px;
    font-size: 12px;
    opacity: 0.7;
}

.room-step-summary {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

@media (max-width: 600px) {
    .room-step-footer {
        grid-template-columns: 1fr;
        grid-template-areas: "next" "summary" "back";
    }

    /* Stuck to a phone's bottom edge, so short: the reason over Back | Next, as the Match step's phone footer. */
    .room-step-footer.has-notices {
        grid-template-columns: auto minmax(0, 1fr);
        grid-template-areas: "notices notices" "back next";
    }

    .room-step-footer.has-notices .room-step-footer-summary {
        display: none;
    }

    .room-step-footer.has-notices .room-step-footer-back .cockpit-button {
        width: auto;
        min-height: 48px;
    }

    .room-step-footer-back .cockpit-button {
        width: 100%;
    }

    .room-step-footer.match-step-footer.has-notices {
        grid-template-columns: auto minmax(0, 1fr);
        grid-template-areas: "notices notices" "back next";
    }
}

.room-change-links {
    margin-top: 6px;
}

.room-crew-note {
    margin-bottom: 8px;
}

.cockpit-participant-list li::before {
    content: "- ";
    color: var(--cockpit-green-dim);
}

/* Ambient hover effects on things that aren't buttons. Gated on a device
   that actually has a pointer: a touch browser leaves :hover latched on
   whatever was tapped last until something else is tapped, so on a phone
   these would read as a stuck selection rather than as feedback. The
   button/input hovers above are deliberately left ungated - they predate
   this and a latched button hover is self-explanatory. */
@media (hover: hover) {
    .cockpit-checkbox-row:hover {
        color: var(--cockpit-green-bright);
    }

    .cockpit-select:hover:not(:disabled),
    .cockpit-input:hover:not(:disabled) {
        border-color: var(--cockpit-green-bright);
    }

    .cockpit-opponent-row.hoverable:hover {
        background: rgba(51, 255, 119, 0.12);
    }
}

/* Keyboard focus. Nothing in this UI had a focus ring at all - the browser
   default is suppressed on the inputs above, and CameraViewport used to set
   outline:none outright - so the whole cockpit was unnavigable by keyboard
   without watching the caret. :focus-visible (not :focus) is what keeps a
   mouse click from also painting a ring: the browser only matches it when
   focus arrived by keyboard. Declared after the :focus rules above so it
   wins for a keyboard focus that matches both. */
.cockpit-button:focus-visible,
.cockpit-flag-button:focus-visible,
.cockpit-select:focus-visible,
.cockpit-input:focus-visible,
.cockpit-link:focus-visible,
.cockpit-checkbox-row input[type="checkbox"]:focus-visible {
    outline: 2px solid var(--cockpit-green-bright);
    outline-offset: 2px;
}

/* One switch for the whole cockpit. Durations collapse to zero, which
   settles every transition instantly at its end state; the two things that
   aren't durations - an infinite loop and a press offset - have to be
   turned off by hand. */
@media (prefers-reduced-motion: reduce) {
    :root {
        --cockpit-dur-fast: 0ms;
        --cockpit-dur: 0ms;
        --cockpit-dur-slow: 0ms;
    }

    .cockpit-flag-button:hover:not(:disabled) {
        animation: none;
    }

    .cockpit-button:active:not(:disabled),
    .cockpit-flag-button:active:not(:disabled) {
        translate: none;
    }

    /* The countdowns. The numeral still changes and the bar still shrinks -
       the information is intact - they just stop moving to get there. The
       bar's transition is listed explicitly because its duration is a
       tick-sync value rather than a motion token, so zeroing the tokens
       doesn't reach it. */
    .cockpit-countdown-number,
    .cockpit-countdown-sweep,
    .cockpit-countdown-banner.is-critical {
        animation: none;
    }

    .cockpit-countdown-bar {
        transition: none;
    }

    /* Cars snap between playback steps again rather than gliding. The
       playback itself still runs - the turn still animates, and at the same
       pace - this only removes the browser's interpolation between frames.
       What rides on a car (labels, rings, flashes, tracer ends, the Inspect
       bubble) snaps with it, and viewbox-glide.js snaps the camera too. */
    .car-glyph,
    .car-capsule,
    .car-outline,
    .wreck-glyph,
    .car-follow,
    .inspect-bubble {
        transition: none;
    }

    /* Weapons fire loses its motion but keeps its meaning (docs/WEAPON-MOUNT-ART-PLAN.md D-25): each shot's look appears whole - no
       draw-on, no travel, the projectile at its end - for its family's own short life, then goes, so a hit (ending on the target, with a
       static spark) and a miss (ending off it) and the family all still read, and nothing sits on the field. The muzzle flash and the drop
       puff are pure motion and stay hidden. */
    .shot-fx,
    .shot-fx.fx-laser {
        animation-name: fx-static;
    }

    .shot-fx.is-miss,
    .shot-fx.fx-laser.is-miss {
        animation-name: fx-static-miss;
    }

    .shot-fx .fx-grow,
    .shot-fx .fx-spread {
        animation: none;
        transform: none;
    }

    .shot-fx .fx-run {
        animation: none;
        transform: translateX(1px);
    }

    .shot-fx .fx-run-streak {
        animation: none;
        transform: translateX(0.8px);
    }

    .shot-impact {
        animation-name: fx-static;
        transform: none;
    }

    .shot-muzzle,
    .drop-puff {
        display: none;
    }
}

@keyframes fx-static {
    0% { opacity: 0; }
    0.1% { opacity: 1; }
    99.9% { opacity: 1; }
    100% { opacity: 0; }
}

@keyframes fx-static-miss {
    0% { opacity: 0; }
    0.1% { opacity: 0.75; }
    99.9% { opacity: 0.75; }
    100% { opacity: 0; }
}

/* Phones and small tablets in portrait - desktop (above this) is
   untouched. .cockpit-main goes from side-by-side to stacked so nothing
   ever needs horizontal scrolling to reach the controls. */
@media (max-width: 700px) {
    .cockpit-shell {
        padding: calc(6px + var(--cockpit-safe-top)) calc(6px + var(--cockpit-safe-right))
                 calc(6px + var(--cockpit-safe-bottom)) calc(6px + var(--cockpit-safe-left));
    }

    .cockpit-main {
        flex-direction: column;
        /* Desktop's flex:1/stretch exists purely so the arena canvas can
           grow to fill remaining space next to the side panel - in a
           stacked column that same growth would fight the canvas's own
           aspect-ratio sizing (see .cockpit-field below), so mobile goes
           back to ordinary content-driven sizing. */
        flex: none;
        align-items: stretch;
    }

    .cockpit-field {
        flex: none;
        /* .cockpit-field's own width is still definite here (.cockpit-main's
           align-items:stretch fills it to the shell's full width), but its
           height would otherwise be content-driven (auto) in the stacked
           column - circular, since CameraViewport measures *this* element's
           rect to size itself. aspect-ratio derives a real, non-circular
           height from the already-definite width instead. */
        aspect-ratio: 1 / 1;
    }

    .cockpit-side {
        width: 100%;
    }

    .cockpit-log {
        height: 90px;
    }
}

/* The garage's Division-fit meter (docs/ARENA-FLOW-PLAN.md Phase 2): value + minimum Division, a target picker,
   a bar of value against the target's cap that turns amber near it and red over it, and one status line. (The
   Garage page makes the meter and the Save bar sticky together while the editor below scrolls.) */
.division-fit-meter {
    background: var(--cockpit-bg);
    border: 1px solid var(--cockpit-green-dim);
    padding: 8px 10px;
}

.division-fit-values {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    justify-content: space-between;
    gap: 4px 12px;
}

.division-fit-value {
    font-weight: bold;
}

.division-fit-bar {
    margin: 6px 0 4px;
}

.division-fit-meter.is-tight .cockpit-bar-fill {
    background: var(--cockpit-amber);
}

.division-fit-meter.is-tight .division-fit-status {
    color: var(--cockpit-amber);
}

.division-fit-meter.is-over {
    border-color: var(--cockpit-red);
}

.division-fit-meter.is-over .cockpit-bar-fill {
    background: var(--cockpit-red);
}

.division-fit-meter.is-over .division-fit-status {
    color: var(--cockpit-red);
    font-weight: bold;
}

/* ==========================================================================
   Fit layout (docs/ARENA-FLOW-PLAN.md Phase 1) - /play and /watch on a
   desktop or landscape tablet never scroll the page: the map, the panels and
   the log are all on screen at once.

   The markup is always .cockpit-side > (.cockpit-side-panels >
   .cockpit-side-group x2) + the log. Outside this media query the panels and
   groups are display:contents, so every panel is a direct child of the
   rail's flex column - exactly the old layout (and the phone stack).

   In fit mode the shell is exactly the viewport tall and the arena field is
   sized in container-query units from the row it sits in (cqh/cqw - see the
   note on .cockpit-main below for why not aspect-ratio); CameraViewport then
   squares the canvas inside it (viewport.js getSquareSize). The map is
   height-limited, so the spare width goes to the rail: two panel columns
   with the log filling what is left beneath them, and the rail always keeps
   >= 512px (two ~250px columns) when the screen is nearly square.
   In landscape it may leave 440px (the nested queries at the end of the block).
   The floor: 1000x600 in any orientation, or 900x530 in landscape. Below that -
   and on any browser without container query units (@supports) - the
   scrolling layout applies.
   ========================================================================== */
.cockpit-side-panels,
.cockpit-side-group {
    display: contents;
}

/* The soft turn timer, in the top bar (it used to be a full-width banner). */
.cockpit-turn-timer {
    color: var(--cockpit-green);
    white-space: nowrap;
}

/* On a phone the top bar wraps; a dangling separator at a line end reads as a glitch. */
@media (max-width: 899px) {
    .cockpit-turn-timer::after {
        display: none;
    }
}

.cockpit-turn-timer::after {
    content: "\00b7";
    margin-left: 10px;
    opacity: 0.6;
}

.cockpit-turn-timer.is-warning {
    color: var(--cockpit-amber);
}

.cockpit-turn-timer.is-critical {
    color: var(--cockpit-red);
}

/* Sub-heading inside the consolidated Car panel. 4px, not the 6px this used to be: a turret mount's own
   "Hold facing this turn" row (tools/uitest/verify-combat-fit.mjs's turret-car Planning pass) adds a line
   the Fixed-mount baseline never had, and the rail had no spare budget left at 1000x600 for it even after
   trimming that row itself (see .cockpit-checkbox-row.cockpit-hint/.cockpit-checkbox-label above) - the Car
   panel's own five inter-section gaps (Armor -> Tires -> Power -> Weapons -> Crew -> Components) were the
   nearest place to reclaim it, 2px each, scoped to this panel only (.cockpit-car-label is CockpitCarPanel's
   own class, used nowhere else). */
.cockpit-car-label {
    margin-top: 4px;
    font-size: 11px;
    letter-spacing: 1px;
    text-transform: uppercase;
    color: var(--cockpit-green-dim);
}

.cockpit-car-label:first-child {
    margin-top: 0;
}

/* The Components heading and its chips on one line when they fit (CockpitCarPanel). */
.cockpit-car-components {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    column-gap: 8px;
    margin-top: 4px;
}

.cockpit-car-components > .cockpit-car-label {
    margin-top: 0;
}

/* docs/FIRE-AND-EXPLOSIONS-PLAN.md Phase 2 - the Power heading shares its line with the fire extinguisher tag (no extra rail height). */
.cockpit-car-label--split {
    display: flex;
    justify-content: space-between;
    gap: 8px;
}

.cockpit-extinguisher-tag {
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
    min-width: 0;
    text-transform: none;
    letter-spacing: 0;
    color: var(--cockpit-green);
}

/* Razor drops the whitespace around an @if, so the status span spaces itself (PR #389 review). */
.cockpit-extinguisher-tag > span + span {
    margin-left: 0.4em;
}

.cockpit-extinguisher-tag.out {
    opacity: 0.45;
}

/* Only the name is struck through; the "lost" status stays readable. */
.cockpit-extinguisher-tag.out > span:first-child {
    text-decoration: line-through;
}

/* The scrolling side-by-side layout (701px and up, below the fit floor): the panel is the map's own square, not the rail's height, and it
   pins while the rail scrolls. The field used to stretch to the rail (876px of it) with the square canvas centred inside, so a Samsung tablet
   at 962x540 opened on a 150px empty band above a map running off the bottom of the screen. The whole map fits under the top bar (76px) and inside the safe area when the page opens:
   svh, not dvh, or the map would resize as a phone's address bar hides on scroll; the rail takes the width that leaves. */
@media (min-width: 701px) {
    .cockpit-shell--fit .cockpit-field {
        box-sizing: border-box;
        flex: 0 0 auto;
        width: min(calc(100% - 352px), calc(100vh - 76px - var(--cockpit-safe-top) - var(--cockpit-safe-bottom)));
        width: min(calc(100% - 352px), calc(100svh - 76px - var(--cockpit-safe-top) - var(--cockpit-safe-bottom)));
        aspect-ratio: 1 / 1;
        align-self: flex-start;
        position: sticky;
        top: calc(8px + var(--cockpit-safe-top));
    }

    .cockpit-shell--fit .cockpit-side {
        flex: 1 1 340px;
        width: auto;
        min-width: 0;
        /* WebKit counts something in a rail wider than 500px toward the page's scroll width (4-13px, nothing's rect reaches it). */
        overflow-x: clip;
    }
}

@supports (height: 100cqh) {
@media (min-width: 1000px) and (min-height: 600px), (min-width: 900px) and (min-height: 530px) and (orientation: landscape) {
    .cockpit-shell--fit {
        height: 100vh;
        height: 100dvh;
        min-height: 0;
        overflow: hidden;
    }

    /* .cockpit-main becomes a size container so the field can be sized in cqh/cqw units: an explicit
       height and width that do NOT depend on the arena inside it. (aspect-ratio + a stretched height
       looked equivalent, but WebKit derived the width from the content's own height - the canvas -
       so after a resize the map stayed at its old size and the ResizeObserver looped.) The field is
       as tall as the row and, at most, as wide as it is tall, always leaving the rail >= 512px;
       CameraViewport then squares the canvas inside it (min(w, h)). */
    .cockpit-shell--fit .cockpit-main {
        container-type: size;
        container-name: cockpit-main;
    }

    /* Both sides the same: the panel IS the canvas's square, so no band can open inside it when the rail's minimum makes the field narrower
       than the row is tall (1024x768, 1000x600); what the row has left over sits below the panel. */
    .cockpit-shell--fit .cockpit-field {
        --cockpit-fit-side: min(100cqh, calc(100cqw - var(--cockpit-fit-rail-reserve, 512px)));
        flex: none;
        box-sizing: border-box;
        height: var(--cockpit-fit-side);
        width: var(--cockpit-fit-side);
        position: static;
    }

    .cockpit-shell--fit .cockpit-side {
        flex: 1 1 0;
        width: auto;
        min-width: 0;
        min-height: 0;
        overflow: hidden;
    }

    .cockpit-shell--fit .cockpit-side-panels {
        display: grid;
        grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
        gap: 8px;
        align-items: start;
        flex: 0 1 auto;
        min-height: 0;
        /* Last-resort safety (many opponents, the Game Over results): the rail scrolls, the page never does. */
        overflow-y: auto;
    }

    .cockpit-shell--fit .cockpit-side-group {
        display: flex;
        flex-direction: column;
        gap: 8px;
        min-width: 0;
        container-type: inline-size;
    }

    /* /play (docs/ARENA-FLOW-PLAN.md Phase 1, "Combatants keeps an internal scroll for large matches"): each rail column is exactly the
       space above the log, and a long Combatants/Standings list scrolls inside its own panel rather than the whole rail - so the Car panel
       and every other panel stay put however many cars are seated. The rail's own overflow stays the last resort (the Game Over results
       in the Controls column). */
    .cockpit-shell--fit .cockpit-side-panels--columns {
        grid-template-rows: minmax(0, 1fr);
        align-items: stretch;
    }

    .cockpit-shell--fit .cockpit-side-panels--columns .cockpit-side-group {
        min-height: 0;
    }

    .cockpit-shell--fit .cockpit-side-panels--columns .cockpit-side-group > * {
        flex-shrink: 0;
    }

    .cockpit-shell--fit .cockpit-side-panels--columns .cockpit-side-group > .cockpit-rail-scroll-panel {
        flex: 0 1 auto;
        min-height: 0;
        display: flex;
        flex-direction: column;
    }

    .cockpit-shell--fit .cockpit-rail-scroll-panel > .cockpit-panel-body {
        min-height: 0;
        overflow-x: hidden;
        overflow-y: auto;
    }

    /* Outside the body (offset 0, 2px): the sticky own row sits on the body's inner edge and would cover an inset outline. */
    .cockpit-shell--fit .cockpit-rail-scroll-panel > .cockpit-panel-body:focus-visible {
        outline: 2px solid var(--cockpit-green-bright);
        outline-offset: 0;
    }

    /* A race's own Standings row stays in sight once the list scrolls past it (sticky at the top), on an opaque band. */
    .cockpit-shell--fit .cockpit-rail-scroll-panel .armor-table--play.armor-table--race tr.is-own {
        position: sticky;
        top: 0;
        z-index: 2;
        background: linear-gradient(rgba(255, 255, 255, 0.08), rgba(255, 255, 255, 0.08)), var(--cockpit-panel-bg);
    }

    /* ...and while it is still below the panel's bottom edge, a slim one-line band (name · place · distance or time, no armor -
       Geoff 2026-10-28) pins there instead (the full two-line row there covered the leader's row in an eight-car race at
       1000x600). CSS only: the band is the row right after the own row, sticky at the bottom, pulled up under the own row by its
       own height. Where the own row is in sight, or has scrolled past (stuck at the top), the band sits at its own place - hidden
       under the own row's opaque band, which paints above it - and only while the own row's place is below the edge does the band
       stick to the edge, on its own. Only in the narrow (flex-row) layout - a wide table's rows are table rows, which a flex band
       would break (found live: the eight-car race's rows stayed mis-sized after a resize through a wide rail). */
    @container armor-table (max-width: 399px) {
    .cockpit-shell--fit .cockpit-rail-scroll-panel .armor-table--play.armor-table--race tr.play-own-band {
        --play-own-band-height: 1.3em;
        display: flex;
        position: sticky;
        bottom: 0;
        z-index: 1;
        height: var(--play-own-band-height);
        line-height: var(--play-own-band-height);
        margin-top: calc(-1 * var(--play-own-band-height));
        padding: 0;
        font-weight: 700;
        cursor: var(--cursor-action);
        background: linear-gradient(rgba(255, 255, 255, 0.08), rgba(255, 255, 255, 0.08)), var(--cockpit-panel-bg);
    }

    .cockpit-shell--fit .cockpit-rail-scroll-panel .armor-table--play.armor-table--race tr.play-own-band::after {
        content: none;
    }

    .cockpit-shell--fit .cockpit-rail-scroll-panel .armor-table--play.armor-table--race .play-own-band-cell {
        display: block;
        flex: 1 1 100%;
        min-width: 0;
        padding: 0;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
    }
    }

    /* Armor/Tires read four (five for Armor, see cockpit-status-grid--armor above) across once a column is wide enough (they are 2x2/3 in a narrow one). */
    @container (min-width: 330px) {
        .cockpit-status-grid span {
            width: 24%;
        }

        .cockpit-status-grid.cockpit-status-grid--armor span {
            width: 19%;
        }
    }

    /* The log takes whatever the panels leave. Newest line first, so the newest is always visible. */
    .cockpit-shell--fit .cockpit-log {
        flex: 1 1 90px;
        min-height: 90px;
        height: auto;
        margin-top: 0;
    }

    /* /hall (Geoff 2026-10-20): the log keeps at least a third of the screen, and the rail itself never scrolls: each column is
       exactly the space above the log, Recent results (and, if the second column runs out of room, Bettors) scrolls inside its own
       panel, and every other panel keeps its height. */
    .cockpit-shell--fit.hall-shell .cockpit-log {
        flex: 1 0 33vh;
        min-height: 33vh;
    }

    .cockpit-shell--fit.hall-shell .cockpit-side-panels {
        overflow: hidden;
        grid-template-rows: minmax(0, 1fr);
        align-items: stretch;
    }

    /* Last resort, on a screen too short for the next match's card and the bet slip above the log (1000x600): that column
       scrolls, with the cockpit's scroll bar, rather than cutting the bet slip off. */
    .cockpit-shell--fit.hall-shell .cockpit-side-group {
        min-height: 0;
        overflow-x: hidden;
        overflow-y: auto;
    }

    .cockpit-shell--fit.hall-shell .cockpit-side-group > * {
        flex-shrink: 0;
    }

    .cockpit-shell--fit.hall-shell .cockpit-side-group > .hall-scroll-panel {
        flex: 0 1 auto;
        min-height: 0;
        display: flex;
        flex-direction: column;
    }

    .cockpit-shell--fit.hall-shell .hall-scroll-panel > .cockpit-panel-body {
        min-height: 0;
        overflow-y: auto;
    }

    .cockpit-shell--fit.hall-shell .hall-recent-list {
        max-height: none;
        overflow: visible;
    }

    /* docs/OBSERVABILITY-PLAN.md Phase 5 - /dev/replay: the AI decisions panel can be far taller than the screen, so each rail column
       scrolls on its own above a log that keeps a fifth of the screen, as /hall's do. */
    .cockpit-shell--fit.replay-shell .cockpit-log {
        flex: 1 0 20vh;
        min-height: 20vh;
    }

    .cockpit-shell--fit.replay-shell .cockpit-side-panels {
        overflow: hidden;
        grid-template-rows: minmax(0, 1fr);
        align-items: stretch;
    }

    .cockpit-shell--fit.replay-shell .cockpit-side-group {
        min-height: 0;
        overflow-x: hidden;
        overflow-y: auto;
    }

    .cockpit-shell--fit.replay-shell .cockpit-side-group > * {
        flex-shrink: 0;
    }

    /* docs/TUTORIAL-PLAN.md Phase 5 - the narrative-beat overlay covers both panel columns: laid over the grid (absolute, so it takes
       no grid cell and the groups keep their places), the groups kept under it (hidden, inert) so the rail keeps its size - never past
       the viewport, the map unmoved. The overlay scrolls inside itself on a short rail. */
    .cockpit-shell--fit .cockpit-side-panels.is-beat-open {
        position: relative;
        overflow-y: hidden;
    }

    .cockpit-shell--fit .cockpit-side-panels.is-beat-open > .cockpit-side-group {
        display: flex;
        visibility: hidden;
    }

    .cockpit-shell--fit .cockpit-side-panels.is-beat-open > .narrative-overlay {
        position: absolute;
        inset: 0;
        z-index: 1;
        overflow-y: auto;
    }

    /* docs/ARENA-FLOW-PLAN.md Phase 1 "Later". In landscape the map's width limit leaves the rail 440px, two ~215px columns, at every width, so a
       wider window never means a smaller map. */
    @media (orientation: landscape) {
        .cockpit-shell--fit {
            --cockpit-fit-rail-reserve: 452px;
        }

        .cockpit-shell--fit .cockpit-side-panels {
            grid-template-columns: repeat(auto-fit, minmax(212px, 1fr));
        }
    }

    /* Those columns are too tall for a short row (the owner's Galaxy Tab Active5 is 960x600 at display scale 2), so under 680 tall the chrome
       gives up height: the frame, the top bar and the panels' own padding, never a button. The log shrinks to its last lines (the newest is first). */
    @media (max-height: 679px) {
        .cockpit-shell--fit {
            padding-top: calc(4px + var(--cockpit-safe-top));
            padding-bottom: calc(4px + var(--cockpit-safe-bottom));
        }

        .cockpit-shell--fit .cockpit-topbar {
            padding-top: 0;
            padding-bottom: 2px;
            margin-bottom: 4px;
        }

        .cockpit-shell--fit .cockpit-main,
        .cockpit-shell--fit .cockpit-side,
        .cockpit-shell--fit .cockpit-side-panels,
        .cockpit-shell--fit .cockpit-side-group {
            gap: 4px;
        }

        .cockpit-shell--fit .cockpit-side .cockpit-panel-title {
            padding-top: 1px;
            padding-bottom: 1px;
        }

        .cockpit-shell--fit .cockpit-side .cockpit-panel-body {
            padding-top: 3px;
            padding-bottom: 3px;
        }

        .cockpit-shell--fit .cockpit-controls > div {
            margin-top: 3px;
        }

        .cockpit-shell--fit .cockpit-controls > div:first-child {
            margin-top: 0;
        }

        .cockpit-shell--fit .cockpit-log {
            flex-basis: 48px;
            min-height: 48px;
            padding-top: 3px;
            padding-bottom: 3px;
        }

    }

    @media (max-height: 599px) {
        .cockpit-shell--fit.hall-shell .cockpit-log {
            flex: 1 0 22vh;
            min-height: 22vh;
        }
    }
}
}
}

/* docs/TUTORIAL-PLAN.md Phase 5 - /play's narrative-beat overlay (Cockpit/NarrativeBeatOverlay): the turn is paused on a server-sent
   beat. In the scrolling (phone) layout it takes the panels' place at the top of the rail; the fit layout lays it over both columns
   (above). Never position: fixed - the map stays visible and a real modal still covers it. */
.cockpit-side-panels.is-beat-open > .cockpit-side-group {
    display: none;
}

.narrative-overlay {
    display: flex;
    flex-direction: column;
    gap: 8px;
    box-sizing: border-box;
    padding: 8px;
    background: var(--cockpit-panel-bg);
    border: 1px solid var(--cockpit-amber);
}

/* The rail's big art would push the replies out of a 1000x600 rail: the overlay keeps the name plate's avatar only. */
.cockpit-side-panels .narrative-overlay .conversation-art {
    display: none;
}

.narrative-overlay-paused {
    color: var(--cockpit-amber);
    font-size: 12px;
    text-transform: uppercase;
    letter-spacing: 1px;
}

.cockpit-turn-timer.is-paused {
    color: var(--cockpit-amber);
}

/* docs/OBSERVABILITY-PLAN.md Phase 5 - the replay's transport line. */
.replay-status {
    color: var(--cockpit-green);
    margin-bottom: 6px;
}

/* ==========================================================================
   Roster (Arena progression Phase 5) - /roster's grid, the "closest to
   unlocking" strip, the garage's per-seat crew picker, and the GameOver
   Prestige panel. See docs/ARENA-PROGRESSION.md "The grid" for the card
   states this styles (unlocked / locked-visible / hidden).
   ========================================================================== */

.character-portrait {
    width: 100%;
    height: 64px;
    display: flex;
    align-items: center;
    justify-content: center;
    border: 1px solid var(--cockpit-green-dim);
    background: #000;
}

.character-portrait-silhouette {
    width: 28px;
    height: 28px;
    fill: var(--cockpit-green-dim);
}

.character-portrait-question {
    font-size: 24px;
    color: var(--cockpit-green-dim);
}

/* docs/TUTORIAL-PLAN.md D-9 - the conversation rail's two portrait sizes, still the silhouette. The accent border comes
   from the rail's --conversation-accent when it is set (Conversation/ConversationRail.razor). */
.character-portrait.character-portrait--art {
    height: auto;
    aspect-ratio: 4 / 5;
    max-height: 220px;
    border-color: var(--conversation-accent, var(--cockpit-green-dim));
}

.character-portrait--art .character-portrait-silhouette {
    width: 55%;
    height: 55%;
}

.character-portrait.character-portrait--avatar {
    width: 36px;
    height: 36px;
    flex: none;
    border-radius: 50%;
    overflow: hidden;
    border-color: var(--conversation-accent, var(--cockpit-green-dim));
}

.character-portrait--avatar .character-portrait-silhouette {
    width: 22px;
    height: 22px;
}

/* wraps to 2x2 rather than overflowing horizontally - at the roster
   grid's 130px card floor (Roster.razor's LayoutGrid), the four columns'
   combined width doesn't quite fit alongside the card's own padding, and
   without wrap the row simply spilled past the card's edge into whatever
   sat beside/below it. Found live on a tablet. */
.skill-pips {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
}

.skill-pips-column {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 2px;
}

.skill-pips-squares {
    display: flex;
    gap: 2px;
}

.skill-pips-square {
    width: 7px;
    height: 7px;
    border: 1px solid var(--cockpit-green-dim);
    background: transparent;
}

.skill-pips-square.is-filled {
    background: var(--cockpit-green);
    border-color: var(--cockpit-green);
}

.skill-pips-letter {
    font-size: 9px;
    color: var(--cockpit-green-dim);
}

/* Colors the D/G/M/N letter (not the pip squares, which stay the plain
   cockpit green above) by the skill it labels, reusing the same
   --skill-driving/gunnery/mechanic/navigator vars (:root, near the Crew
   Traits comment) that .trait-chip-* keys off further down this file.
   Keyed by Shared/SkillPips.razor's SkillCssClass suffix, independent of
   the localized letter text itself. */
.skill-letter-driving { color: var(--skill-driving); }
.skill-letter-gunnery { color: var(--skill-gunnery); }
.skill-letter-mechanic { color: var(--skill-mechanic); }
.skill-letter-navigator { color: var(--skill-navigator); }

/* Rarity-colored text - the --rarity-* ladder (see :root), generic so it
   applies to a Roster character's Quality label, a vehicle's
   VehicleQualityTier, or anything else that lands on this same five-tier
   scale later. RarityCssClass.cs picks the suffix. */
.rarity-text-common { color: var(--rarity-common); }
.rarity-text-uncommon { color: var(--rarity-uncommon); }
.rarity-text-rare { color: var(--rarity-rare); }
.rarity-text-epic { color: var(--rarity-epic); }
.rarity-text-legendary {
    color: var(--rarity-legendary);
    text-shadow: 0 0 6px var(--rarity-legendary);
}

/* Crew Traits (docs/CREW-TRAITS-PLAN.md PR 2) - Shared/TraitChips.razor. Small pill-shaped chips, one per
   trait, coloured by the trait's own skill (--skill-* above); the effect sentence is the title/tooltip, not
   drawn here. Wraps rather than overflowing, same as .skill-pips right above it in the roster card. */
.trait-chips {
    display: flex;
    flex-wrap: wrap;
    gap: 4px;
    margin-top: 4px;
}

.trait-chip {
    font-size: 9px;
    line-height: 1.4;
    padding: 1px 6px;
    border-radius: 8px;
    border: 1px solid currentColor;
    white-space: nowrap;
}

.trait-chip-driving { color: var(--skill-driving); }
.trait-chip-gunnery { color: var(--skill-gunnery); }
.trait-chip-mechanic { color: var(--skill-mechanic); }
.trait-chip-navigator { color: var(--skill-navigator); }

/* The garage's per-seat Roster picker (Phase 5) - Driver's own "this is
   you" badge, and the struck-through/dimmed look for an unlocked-but-
   over-cap option (see RosterCrewPicker's own doc comment - picking one is
   silently ignored, so this is purely a visual "why can't I pick this"
   cue). */
.roster-primary-badge {
    margin-left: 8px;
    padding: 1px 6px;
    font-size: 10px;
    color: var(--cockpit-bg);
    background: var(--cockpit-amber);
    text-transform: uppercase;
    letter-spacing: 0.5px;
}

.roster-card {
    display: flex;
    flex-direction: column;
    gap: 4px;
    padding: 8px;
    border: 1px solid var(--cockpit-green-dim);
    background: var(--cockpit-panel-bg);
}

.roster-card.is-locked {
    opacity: 0.85;
}

.roster-card.is-hidden {
    border-style: dashed;
    border-color: var(--cockpit-green-dim);
    align-items: center;
    text-align: center;
}

/* Border color is rarity, not unlock state, for every non-hidden card -
   is-locked's own opacity above already dims it, and is-unlocked no
   longer needs a border-color of its own. Chained with .roster-card
   (not a bare .roster-card-rarity-* class) to outrank .roster-card's own
   base border-color at equal specificity. A hidden card never carries one
   of these (its Quality is redacted), so is-hidden's dim/dashed border
   above is untouched. */
.roster-card.roster-card-rarity-common { border-color: var(--rarity-common); }
.roster-card.roster-card-rarity-uncommon { border-color: var(--rarity-uncommon); }
.roster-card.roster-card-rarity-rare { border-color: var(--rarity-rare); }
.roster-card.roster-card-rarity-epic { border-color: var(--rarity-epic); }
.roster-card.roster-card-rarity-legendary {
    border-color: var(--rarity-legendary);
    box-shadow: 0 0 6px var(--rarity-legendary);
}

.roster-card-name {
    font-weight: bold;
    text-transform: uppercase;
    font-size: 13px;
}

.roster-card.is-locked .roster-card-name {
    color: var(--cockpit-green-dim);
}

/* The card's one control (Roster.razor): the name is a button whose ::after
   stretches over the whole card, so a click anywhere on it opens the
   character profile, and the button itself is the keyboard stop. The card's
   own tooltip triggers (trait chips, growth line) sit above the overlay so a
   tap on one still toggles its tooltip instead of opening the profile. */
.roster-card {
    position: relative;
}

.roster-card-open {
    appearance: none;
    background: none;
    border: 0;
    padding: 0;
    margin: 0;
    font-family: inherit;
    color: inherit;
    text-align: left;
    cursor: var(--cursor-action);
}

.roster-card-open::after {
    content: "";
    position: absolute;
    inset: 0;
}

/* The focus ring is drawn on the stretched ::after (the card's own box), so
   it needs no :has() support. */
.roster-card-open:focus-visible {
    outline: none;
}

.roster-card-open:focus-visible::after {
    outline: 2px solid var(--cockpit-amber);
    outline-offset: 2px;
}

/* A faint tint layered over the panel background (not replacing it), only
   where hover exists - a touch tap would otherwise leave it stuck on. */
@media (hover: hover) {
    .roster-card:has(.roster-card-open):hover {
        background-image: linear-gradient(rgba(51, 255, 119, 0.06), rgba(51, 255, 119, 0.06));
    }
}

.roster-card .cockpit-tooltip-wrap {
    position: relative;
    z-index: 1;
}

/* position: relative (and a locked card's opacity) make every card its own
   stacking context, which would trap an open tooltip bubble under the cards
   that follow it - raise the card holding an open bubble above its
   neighbours. */
.roster-card:has(.cockpit-tooltip-bubble:not([hidden])) {
    z-index: var(--cockpit-z-popup);
}

/* The character profile (Roster/CharacterProfile.razor) - the body of the
   modal a roster card opens: portrait beside name/rarity/pips/traits, then
   the bio and, for a locked character, its unlock condition. Stacks the
   portrait above the text at phone widths. */
.character-profile {
    display: grid;
    grid-template-columns: 112px minmax(0, 1fr);
    gap: 8px 14px;
    align-items: start;
}

.character-portrait.character-portrait--large {
    width: 112px;
    height: 112px;
}

.character-portrait--large .character-portrait-silhouette {
    width: 56px;
    height: 56px;
}

.character-profile-text {
    display: flex;
    flex-direction: column;
    gap: 6px;
    min-width: 0;
}

.character-profile-name {
    font-weight: bold;
    text-transform: uppercase;
    font-size: 16px;
    overflow-wrap: anywhere;
}

.character-profile-bio {
    grid-column: 1 / -1;
    margin: 4px 0 0;
    line-height: 1.45;
    font-style: italic;
}

.character-profile-unlock {
    grid-column: 1 / -1;
    display: flex;
    flex-direction: column;
    gap: 4px;
}

@media (max-width: 480px) {
    .character-profile {
        grid-template-columns: minmax(0, 1fr);
    }

    .character-portrait.character-portrait--large {
        width: 100%;
        height: 96px;
    }
}

.roster-card-condition {
    font-size: 11px;
    color: var(--cockpit-green-dim);
}

.roster-card-progress {
    height: 4px;
    background: #000;
    border: 1px solid var(--cockpit-green-dim);
}

.roster-card-progress-fill {
    display: block;
    height: 100%;
    background: var(--cockpit-green-dim);
}

/* Arena crew progression Phase 2 (docs/ARENA-CREW-PROGRESSION-PLAN.md
   decision 8) - "your growth" on an unlocked roster card. Amber (matching
   the app's existing --cockpit-amber accent, e.g. the Driver's own
   "PRIMARY" badge) rather than the unlock-condition bar's dim green, so a
   card's own earned-progress line/bar reads as a distinct kind of
   information from "how close to unlocking this" above it. */
.crew-growth-line {
    font-size: 10px;
    color: var(--cockpit-amber);
}

.crew-growth-caption {
    margin-top: 2px;
}

.roster-card-progress-fill.is-crew-growth {
    background: var(--cockpit-amber);
}

/* GameOver's Prestige breakdown lines (Phase 5) - additive lines in the
   ordinary log colour, multiplier lines dim, the total line separated by a
   dashed rule above it. */
.prestige-breakdown-line {
    display: flex;
    justify-content: flex-end;
    gap: 8px;
}

.prestige-breakdown-line.is-multiplier {
    color: var(--cockpit-green-dim);
}

.prestige-breakdown-total {
    display: flex;
    justify-content: flex-end;
    margin-top: 4px;
    padding-top: 4px;
    border-top: 1px dashed var(--cockpit-green-dim);
    font-weight: bold;
}

.prestige-rankup-banner {
    text-align: center;
    padding: 8px;
    margin: 8px 0;
    border: 1px solid var(--cockpit-amber);
    color: var(--cockpit-amber);
    text-shadow: 0 0 8px var(--cockpit-amber);
    letter-spacing: 2px;
    text-transform: uppercase;
}

/* GameOver's per-crew XP block (CrewResultPanel): one compact row per crew member, the level-up line in the
   same amber the rank-up banner and roster growth line use. */
.crew-result-row {
    padding: 4px 0;
    border-bottom: 1px dashed var(--cockpit-green-dim);
}

.crew-result-row:last-child {
    border-bottom: none;
}

.crew-result-name {
    font-weight: bold;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.crew-result-levelup {
    color: var(--cockpit-amber);
    text-shadow: 0 0 6px var(--cockpit-amber);
    font-weight: bold;
}

/* CockpitTooltip (docs/PLAN.md "styled tooltips") - replaces native title="..." for information
   that isn't otherwise visible. The wrap is inline so it drops into a flex/inline-flow trigger
   row (a speed button, a weapon-row span) without changing that row's own layout. */
.cockpit-tooltip-wrap {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    position: relative;
}

.cockpit-tooltip-trigger {
    display: inline-flex;
}

/* Chip mode only (CockpitTooltip's ActionTrigger="false", the default) - the trigger does
   nothing of its own, so its whole box is purely a tooltip hotspot: the arrow (a mouse opens it by
   hovering and a mouse click does nothing, TooltipPointerGate - so not the hand; there is no help
   art), and no accidental text selection on a tap-to-toggle
   (iOS's own press-and-hold text-selection callout is exactly what the tap model avoids). An
   action trigger (a real button/label) keeps its own native cursor/selection behavior instead. */
.cockpit-tooltip-trigger--chip {
    cursor: var(--cursor-arrow);
    user-select: none;
    -webkit-user-select: none;
}

@media (hover: hover) {
    .cockpit-tooltip-trigger--chip:hover {
        color: var(--cockpit-green-bright);
    }
}

/* PR #342 review item 3: iOS Safari only dispatches a synthetic click to a plain (non-form,
   non-link) element - this trigger span - when it has SOME indication of being tappable, and
   an arrow doesn't count; `cursor: pointer` does. Scoped to (hover: none) - a touch-only device -
   and the keyword on purpose, not var(--cursor-action): the one literal CursorRulesTests allows. */
@media (hover: none) {
    .cockpit-tooltip-trigger--chip {
        cursor: pointer;
    }
}

.cockpit-tooltip-trigger:focus-visible {
    outline: 1px solid var(--cockpit-green-bright);
    outline-offset: 2px;
}

/* The touch-reachable alternative for an Action-trigger tooltip (PlanSpeedControls' Max/Min
   speed buttons, CockpitCarPanel's "Hold facing this turn") - a tap on the trigger itself must
   still perform its own action, so it can't also open the tooltip; this is deliberately separate
   and tiny, never the thing a tap on the real control lands on. */
.cockpit-tooltip-help {
    box-sizing: border-box;
    font-family: var(--cockpit-font);
    background: #000;
    color: var(--cockpit-green-dim);
    border: 1px solid var(--cockpit-green-dim);
    border-radius: 50%;
    width: 18px;
    height: 18px;
    line-height: 1;
    padding: 0;
    font-size: 11px;
    cursor: var(--cursor-arrow);
    flex: none;
}

@media (hover: hover) {
    .cockpit-tooltip-help:hover {
        color: var(--cockpit-green-bright);
        border-color: var(--cockpit-green-bright);
    }
}

/* CockpitCarPanel's per-mount "Hold facing this turn" row, same zero-slack rail budget the
   .cockpit-checkbox-row.cockpit-hint margin rule above already fights at 1000x600 (tools/uitest/
   verify-combat-fit.mjs's turret-car Planning pass) - the default 18px "?" (taller than this
   row's own ~14px text line height, since box-sizing:border-box still counts the circle's full
   diameter) became the row's tallest flex item and pushed the rail 3px past its no-internal-
   scroll budget. Shrunk to match the row's own text line height exactly, scoped here rather than
   lowered app-wide - every other "?" (RosterBuilder's trait picker, PlanSpeedControls) has room
   to spare at the normal 18px size.
   Selector note (#341 follow-up, item 5): CockpitTooltip now wraps the WHOLE <label
   class="cockpit-checkbox-row">, not just the text span inside it (a <div role="tooltip"> and
   this "?" button can't legally live inside a <label> - invalid HTML, and it was polluting the
   label's own accessible name), so .cockpit-tooltip-help is a SIBLING of the trigger span that
   contains .cockpit-checkbox-row, never its descendant. :has() on the preceding sibling is what
   lets this still target only that row's own help button. */
.cockpit-tooltip-trigger:has(.cockpit-checkbox-row) ~ .cockpit-tooltip-help {
    width: 14px;
    height: 14px;
    font-size: 9px;
}

/* Same budget, one more wrinkle from the same restructuring: .cockpit-tooltip-wrap is the row's
   direct child now (replacing the <label>'s own display:flex, a block-level box with no inline
   formatting concerns), and .cockpit-tooltip-wrap is inline-flex - an INLINE-level box, which
   defaults to vertical-align:baseline and so reserves room below itself for the surrounding
   block's font descender (the classic "inline-block image gap" bug), even though its own flex
   content is only 14px tall. That alone pushed this row ~3.5px past budget (measured as extra
   space between the row and the next sibling, not the row's own height); vertical-align:top
   removes it the same way it fixes that gap anywhere else. */
.cockpit-tooltip-wrap:has(.cockpit-checkbox-row) {
    vertical-align: top;
}

.cockpit-tooltip-bubble {
    position: fixed;
    /* Set from TooltipPlacement.Compute's own output (CockpitTooltip.razor's BubbleStyle) -
       these are only the fallback/pre-measurement values. */
    left: 0;
    top: 0;
    z-index: var(--cockpit-z-popup);
    max-width: 260px;
    width: max-content;
    padding: 6px 10px;
    font-family: var(--cockpit-font);
    font-size: 0.85em;
    line-height: 1.35;
    white-space: normal;
    word-wrap: break-word;
    background: var(--cockpit-panel-bg);
    color: var(--cockpit-green);
    border: 1px solid var(--cockpit-green);
    box-shadow: 0 0 8px var(--cockpit-glow);
    /* A readout, not a control - never steals a click/hover meant for whatever's underneath or
       behind it. */
    pointer-events: none;
    /* Invisible until TooltipPlacement has actually positioned it (is-positioned) - otherwise it
       would flash at its pre-measurement left:0/top:0 for one frame every time it opens. */
    opacity: 0;
    transition: opacity var(--cockpit-dur-fast) var(--cockpit-ease);
}

.cockpit-tooltip-bubble.is-positioned {
    opacity: 1;
}


/* ==========================================================================
   Modals (docs/RESULTS-MODAL-PLAN.md Phase 1) - Cockpit/CockpitModalHost +
   CockpitModal. The host is a SIBLING of the page's .cockpit-shell, outside
   every container-type container (layout containment would clip a fixed
   element and trap its z-index). No transform or backdrop-filter on any
   ancestor of the panel once it has settled: fixed descendants (tooltips,
   a CockpitSelect popup in fixed mode) must stay viewport-positioned.
   ========================================================================== */

/* Scroll lock while a modal is open (cockpit-modal.js toggles the class). On the
   ROOT only: body is height:100%, so overflow:hidden on body too would clip the
   page to one viewport and snap a scrolled page (the Garage) back to the top -
   found by docs/RESULTS-MODAL-PLAN.md Phase 5a's verify-confirm-modals.mjs. With
   the root alone the viewport stops scrolling and keeps its offset. */
html.cockpit-modal-open {
    overflow: hidden;
}

/* ...without the page jumping sideways when a classic (space-taking)
   scrollbar disappears: keep its gutter. Only when the page HAD one
   (cockpit-modal.js adds the class) - on a page that never scrolled, a
   stable gutter would itself shift the layout. */
html.cockpit-modal-open.cockpit-modal-gutter {
    scrollbar-gutter: stable;
}

/* The shell normally makes its own stacking context (z-index:1, above the
   boot splash); while a modal is open it gives that up, so the banners
   inside it can be lifted above the modal layer. Painting order against
   .app-boot (z-index 0, earlier in the tree) is unchanged. */
html.cockpit-modal-open .cockpit-shell {
    z-index: auto;
}

html.cockpit-modal-open .cockpit-connection-banner {
    position: relative;
    z-index: var(--cockpit-z-banner);
    background: var(--cockpit-bg);
}

.cockpit-modal-host {
    display: contents;
}

/* A sibling of the panel's frame, never its ancestor - the blur lives here only. */
.cockpit-modal-backdrop {
    position: fixed;
    inset: 0;
    z-index: var(--cockpit-z-modal);
    background: color-mix(in srgb, var(--cockpit-bg) 70%, transparent);
    -webkit-backdrop-filter: blur(2px);
    backdrop-filter: blur(2px);
    animation: cockpit-modal-fade var(--cockpit-dur) var(--cockpit-ease);
}

/* Centres the panel without a transform on it. Clicks fall through the
   frame to the backdrop; only the panel takes them. */
.cockpit-modal-frame {
    position: fixed;
    inset: 0;
    z-index: var(--cockpit-z-modal);
    display: flex;
    align-items: center;
    justify-content: center;
    padding: calc(16px + var(--cockpit-safe-top)) calc(16px + var(--cockpit-safe-right))
             calc(16px + var(--cockpit-safe-bottom)) calc(16px + var(--cockpit-safe-left));
    box-sizing: border-box;
    pointer-events: none;
}

.cockpit-modal {
    pointer-events: auto;
    display: flex;
    flex-direction: column;
    max-height: 100%;
    box-sizing: border-box;
    font-family: var(--cockpit-font);
    font-size: 15px;
    color: var(--cockpit-green);
    background: var(--cockpit-panel-bg);
    border: 1px solid var(--cockpit-green);
    box-shadow: 0 0 18px var(--cockpit-glow);
    outline: none;
    /* fill-mode none: the transform is gone the moment the animation ends,
       so nothing inside is left inside a transformed containing block. */
    animation: cockpit-modal-enter var(--cockpit-dur-slow) var(--cockpit-ease);
    animation-fill-mode: none;
}

.cockpit-modal--dialog {
    width: min(440px, 100%);
}

.cockpit-modal--wide {
    width: min(720px, 100%);
}

.cockpit-modal-header {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 10px 12px;
    border-bottom: 1px solid var(--cockpit-green-dim);
}

.cockpit-modal-title {
    flex: 1;
    margin: 0;
    font-size: 15px;
    font-weight: normal;
    text-transform: uppercase;
    letter-spacing: 2px;
}

.cockpit-modal-close {
    min-width: 32px;
    min-height: 32px;
    padding: 0 6px;
    font-size: 16px;
    line-height: 1;
}

.cockpit-modal-body {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    padding: 12px;
}

.cockpit-modal-message {
    margin: 0;
}

.cockpit-modal-footer {
    display: flex;
    flex-wrap: wrap;
    justify-content: flex-end;
    gap: 8px;
    padding: 10px 12px;
    border-top: 1px solid var(--cockpit-green-dim);
}

.cockpit-modal-footer .cockpit-button {
    min-height: 36px;
    padding: 6px 12px;
}

/* Below ModalRules.SheetBreakpointPx (700px): a full-width bottom sheet with
   internal scroll. The sheet runs edge to edge; its own bottom padding keeps
   the footer above the home indicator, and the frame's top padding keeps a
   tall sheet below the notch (all 0 without one). */
.cockpit-modal-frame--sheet {
    align-items: flex-end;
    padding: var(--cockpit-safe-top) 0 0;
}

.cockpit-modal.cockpit-modal--sheet {
    width: 100%;
    max-height: calc(100vh - var(--cockpit-safe-top));
    max-height: calc(100dvh - var(--cockpit-safe-top));
    border-left: none;
    border-right: none;
    border-bottom: none;
    padding: 0 var(--cockpit-safe-right) var(--cockpit-safe-bottom) var(--cockpit-safe-left);
    animation-name: cockpit-modal-sheet-enter;
}

@keyframes cockpit-modal-fade {
    from { opacity: 0; }
    to { opacity: 1; }
}

@keyframes cockpit-modal-enter {
    from { opacity: 0; transform: translateY(12px); }
    to { opacity: 1; transform: none; }
}

@keyframes cockpit-modal-sheet-enter {
    from { opacity: 0; transform: translateY(40px); }
    to { opacity: 1; transform: none; }
}


/* ==========================================================================
   The 3-2-1-GO overlay (docs/RESULTS-MODAL-PLAN.md Phase 5b) -
   Room/StartingCountdownOverlay. A non-dismissible modal request whose
   ExtraClass (cockpit-countdown-overlay) lands on the backdrop and frame:
   a lighter backdrop so the room (and then the arena) stays visible, the
   panel centred even on a phone (no bottom sheet for a three-second beat),
   and on GO (is-go) the whole overlay fades out over the GO beat -
   StartingCountdown.GoBeatMilliseconds, 600ms, held for the first 40%.
   The fade is a fixed duration, not a --cockpit-dur token: zeroed under
   reduced motion it would hide GO at once, so reduced motion turns the
   fade off instead and GO simply holds until the overlay closes.
   ========================================================================== */

.cockpit-modal-backdrop.cockpit-countdown-overlay {
    background: color-mix(in srgb, var(--cockpit-bg) 45%, transparent);
}

.cockpit-modal-frame.cockpit-countdown-overlay {
    align-items: center;
    padding: 16px;
}

.cockpit-countdown-overlay .cockpit-modal {
    width: min(340px, 100%);
    border: 1px solid var(--cockpit-green);
}

.cockpit-countdown-overlay .cockpit-modal-header {
    justify-content: center;
    text-align: center;
}

.cockpit-countdown-hint {
    text-align: center;
}

.cockpit-modal-backdrop.cockpit-countdown-overlay.is-go,
.cockpit-countdown-overlay.is-go .cockpit-modal {
    animation: cockpit-countdown-go-out 600ms ease-in forwards;
}

@keyframes cockpit-countdown-go-out {
    0%, 40% { opacity: 1; }
    100% { opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
    .cockpit-modal-backdrop.cockpit-countdown-overlay.is-go,
    .cockpit-countdown-overlay.is-go .cockpit-modal {
        animation: none;
    }
}


/* /play's "?" keyboard cheat-sheet (docs/RESULTS-MODAL-PLAN.md Phase 5b) - Cockpit/KeyboardHelpSheet. */
.cockpit-keyboard-help {
    width: 100%;
    border-collapse: collapse;
}

.cockpit-keyboard-help th,
.cockpit-keyboard-help td {
    padding: 5px 4px;
    border-bottom: 1px solid color-mix(in srgb, var(--cockpit-green-dim) 50%, transparent);
    font-weight: normal;
    text-align: left;
}

.cockpit-keyboard-help td {
    text-align: right;
    white-space: nowrap;
}

.cockpit-keyboard-help kbd {
    display: inline-block;
    min-width: 1.6em;
    padding: 1px 6px;
    font-family: var(--cockpit-font);
    text-align: center;
    color: var(--cockpit-green-bright);
    border: 1px solid var(--cockpit-green-dim);
    border-radius: 3px;
}


/* ==========================================================================
   The finish ceremony (docs/RESULTS-MODAL-PLAN.md Phase 3) -
   Cockpit/FinishBeat (the headline over the arena) and Cockpit/FinishCeremony
   + FinishCeremonyFooter (the paged modal). The beat is a SIBLING of the
   page's .cockpit-shell like the modal host (a fixed element inside the fit
   layout's containers is clipped and z-trapped). Motion reads the
   --cockpit-dur* tokens, so prefers-reduced-motion settles it all at once;
   the C# timers (beat, count-up) check the same media query themselves.
   ========================================================================== */

/* Full-viewport and transparent but for a soft vignette - the arena stays
   visible behind the sentence; a click anywhere skips the beat. */
.cockpit-finish-beat {
    position: fixed;
    inset: 0;
    /* One above the modal host: SpectatorView writes the beat before the page's CockpitModalHost, so DOM order no longer
       puts it on top as it did when /watch wrote it last (docs/GAMBLING-HALL-PLAN.md Phase 3). */
    z-index: calc(var(--cockpit-z-modal) + 1);
    display: flex;
    align-items: center;
    justify-content: center;
    padding: calc(16px + var(--cockpit-safe-top)) calc(16px + var(--cockpit-safe-right))
             calc(16px + var(--cockpit-safe-bottom)) calc(16px + var(--cockpit-safe-left));
    box-sizing: border-box;
    cursor: var(--cursor-action);
    background: radial-gradient(ellipse at center, color-mix(in srgb, var(--cockpit-bg) 55%, transparent) 0%, transparent 70%);
    animation: cockpit-modal-fade var(--cockpit-dur) var(--cockpit-ease);
}

/* The 3-2-1-GO treatment (.cockpit-countdown-number): arrives oversized and
   settles, with the phosphor glow. Same sentence as the rail headline -
   uppercased here by CSS, never in the string. */
.cockpit-finish-beat-text {
    max-width: min(900px, 100%);
    padding: 14px 22px;
    font-family: var(--cockpit-font);
    font-size: clamp(22px, 4.6vw, 46px);
    font-weight: bold;
    line-height: 1.15;
    letter-spacing: 3px;
    text-align: center;
    text-transform: uppercase;
    color: var(--cockpit-green-bright);
    text-shadow: 0 0 20px var(--cockpit-glow), 0 0 4px var(--cockpit-bg);
    background: color-mix(in srgb, var(--cockpit-bg) 72%, transparent);
    border-top: 1px solid var(--cockpit-green);
    border-bottom: 1px solid var(--cockpit-green);
    box-shadow: 0 0 24px var(--cockpit-glow);
    animation: cockpit-countdown-punch var(--cockpit-dur-slow) var(--cockpit-ease);
}

.cockpit-ceremony {
    display: flex;
    flex-direction: column;
    gap: 10px;
}

.cockpit-ceremony-page {
    display: flex;
    flex-direction: column;
    gap: 8px;
    animation: cockpit-phase-in var(--cockpit-dur) var(--cockpit-ease);
}

.cockpit-ceremony-dots {
    display: flex;
    justify-content: center;
    gap: 8px;
}

.cockpit-ceremony-dot {
    width: 8px;
    height: 8px;
    border: 1px solid var(--cockpit-green-dim);
    border-radius: 50%;
    transition: background-color var(--cockpit-dur) var(--cockpit-ease), border-color var(--cockpit-dur) var(--cockpit-ease);
}

.cockpit-ceremony-dot.is-done {
    background: var(--cockpit-green-dim);
}

.cockpit-ceremony-dot.is-current {
    background: var(--cockpit-green);
    border-color: var(--cockpit-green);
    box-shadow: 0 0 6px var(--cockpit-glow);
}

.cockpit-ceremony-headline {
    font-size: 18px;
    font-weight: bold;
    letter-spacing: 1px;
    text-align: center;
    text-transform: uppercase;
    color: var(--cockpit-green-bright);
    text-shadow: 0 0 10px var(--cockpit-glow);
}

.cockpit-ceremony-subhead {
    margin-top: 4px;
    font-size: 12px;
    letter-spacing: 2px;
    text-transform: uppercase;
    color: var(--cockpit-green-dim);
}

.cockpit-ceremony-footline {
    text-align: center;
}

/* A car's colour, wherever the ceremony names a car. */
.cockpit-chip {
    display: inline-block;
    flex: none;
    width: 10px;
    height: 10px;
    border-radius: 2px;
    box-shadow: 0 0 0 1px var(--cockpit-bg);
}

/* Podium: three steps, first place in the middle. Bar heights are inline
   (FinishCeremonyPages' fractions, through Rendering.Inv). */
.cockpit-podium {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    align-items: end;
    gap: 8px;
}

.cockpit-podium-step {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 2px;
    min-width: 0;
    text-align: center;
}

.cockpit-podium-step.is-place-1 { order: 2; }
.cockpit-podium-step.is-place-2 { order: 1; }
.cockpit-podium-step.is-place-3 { order: 3; }

.cockpit-podium-car {
    display: flex;
    align-items: center;
    gap: 6px;
    max-width: 100%;
}

.cockpit-podium-name {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.cockpit-podium-headline {
    font-weight: bold;
}

.cockpit-podium-detail {
    font-size: 12px;
    color: var(--cockpit-green-dim);
}

.cockpit-podium-bar {
    display: flex;
    align-items: flex-start;
    justify-content: center;
    width: 100%;
    margin-top: 4px;
    padding-top: 4px;
    box-sizing: border-box;
    font-weight: bold;
    color: var(--cockpit-bg);
    background: var(--cockpit-green);
    box-shadow: 0 0 10px var(--cockpit-glow);
    transition: height var(--cockpit-dur-slow) var(--cockpit-ease);
}

.cockpit-podium-step.is-dnf .cockpit-podium-bar {
    background: var(--cockpit-green-dim);
    box-shadow: none;
}

.cockpit-podium-step.is-own .cockpit-podium-name,
.cockpit-ceremony-row.is-own .cockpit-ceremony-row-text,
.cockpit-winner.is-own .cockpit-winner-name {
    color: var(--cockpit-green-bright);
    font-weight: bold;
}

.cockpit-ceremony-team {
    display: flex;
    flex-wrap: wrap;
    justify-content: space-between;
    gap: 4px 10px;
    padding: 4px 8px;
    border: 1px solid var(--cockpit-green-dim);
}

.cockpit-ceremony-team.is-own {
    border-color: var(--cockpit-green);
}

/* Race order and the elimination timeline share one row shape:
   key (place / turn) | chip | text | status. */
.cockpit-ceremony-list {
    margin: 0;
    padding: 0;
    list-style: none;
}

.cockpit-ceremony-row {
    display: grid;
    grid-template-columns: 3em auto minmax(0, 1fr) auto;
    align-items: center;
    gap: 8px;
    padding: 3px 6px;
    border-left: 2px solid transparent;
}

.cockpit-ceremony-row.is-own {
    border-left-color: var(--cockpit-green-bright);
    background: var(--cockpit-panel-hover-bg);
}

.cockpit-ceremony-row.is-teammate {
    border-left-color: var(--cockpit-green-dim);
}

.cockpit-ceremony-row-key {
    color: var(--cockpit-green-dim);
}

.cockpit-ceremony-row-text {
    min-width: 0;
    overflow-wrap: anywhere;
}

.cockpit-ceremony-row-status {
    font-size: 12px;
    color: var(--cockpit-green-dim);
    text-align: right;
}

.cockpit-timeline .cockpit-ceremony-row.is-alive .cockpit-ceremony-row-text {
    color: var(--cockpit-green-bright);
}

.cockpit-winner-card {
    padding: 8px 10px;
    border: 1px solid var(--cockpit-green);
    box-shadow: 0 0 10px var(--cockpit-glow);
}

.cockpit-winner {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 8px;
}

.cockpit-winner.is-out {
    opacity: 0.7;
}

/* Award stamps punch in one after another (inline animation-delay). */
.cockpit-stamps {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
    gap: 8px;
}

.cockpit-stamp {
    display: flex;
    flex-direction: column;
    gap: 3px;
    padding: 8px;
    border: 2px solid var(--cockpit-green);
    box-shadow: inset 0 0 10px var(--cockpit-glow);
    /* fill-mode backwards, not both: a delayed stamp still waits invisible
       through its delay, but nothing of the keyframe survives the end. `both`
       left transform: scale(1) on the stamp for good, which made it the
       containing block for any position:fixed descendant - a trait chip's
       CockpitTooltip bubble (NEW CHARACTER card) landed inside the modal
       body's scroll area instead of the viewport, grew scrollbars and
       flickered as it re-measured (playtest, 2026-10-13). */
    animation: cockpit-countdown-punch var(--cockpit-dur-slow) var(--cockpit-ease) backwards;
}

.cockpit-stamp.is-own {
    border-color: var(--cockpit-green-bright);
}

.cockpit-stamp-title {
    font-weight: bold;
    letter-spacing: 2px;
    text-transform: uppercase;
    color: var(--cockpit-green-bright);
}

.cockpit-stamp-recipient {
    display: flex;
    align-items: center;
    gap: 6px;
    min-width: 0;
    overflow-wrap: anywhere;
}

.cockpit-stamp-number {
    font-size: 13px;
    color: var(--cockpit-green-dim);
}

.cockpit-stat-strip {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
    gap: 6px;
}

.cockpit-stat {
    padding: 6px 8px;
    border: 1px solid var(--cockpit-green-dim);
}

.cockpit-stat-value {
    font-size: 16px;
    font-weight: bold;
}

.cockpit-stat-label {
    font-size: 11px;
    letter-spacing: 1px;
    text-transform: uppercase;
    color: var(--cockpit-green-dim);
}

.cockpit-ceremony-tally {
    display: flex;
    align-items: center;
    gap: 6px;
    padding: 16px 0;
    justify-content: center;
}

.cockpit-payout {
    display: flex;
    flex-direction: column;
    gap: 4px;
}

.cockpit-payout-line {
    display: flex;
    justify-content: space-between;
    gap: 10px;
}

.cockpit-payout-line.is-total {
    padding-top: 4px;
    border-top: 1px dashed var(--cockpit-green-dim);
    font-size: 17px;
    font-weight: bold;
    color: var(--cockpit-green-bright);
}

.cockpit-payout-amount {
    font-variant-numeric: tabular-nums;
}

.cockpit-payout-amount.is-loss {
    color: var(--cockpit-amber);
}

.cockpit-payout-deferred {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 8px;
    padding: 6px 8px;
    color: var(--cockpit-amber);
    border: 1px solid var(--cockpit-amber);
}

/* The Prestige XP bar fills from the XP before the match to after it. */
.cockpit-xp-fill .lobby-prestige-bar-fill {
    transition: width calc(var(--cockpit-dur-slow) * 3) var(--cockpit-ease);
}

.cockpit-modal-footer .cockpit-button-primary {
    border-color: var(--cockpit-green-bright);
    box-shadow: 0 0 8px var(--cockpit-glow);
}

/* The ceremony's Promotions page (docs/RESULTS-MODAL-PLAN.md Phase 4): one stamp per promotion, punched in one after another
   (inline animation-delay, PromotionsPage.PunchStaggerMilliseconds apart, through Rendering.Inv). The stamps reuse .cockpit-stamp's
   punch - so the reduced-motion rule below covers them too. The PRESTIGE stamp spans the row; the others share it. */
.cockpit-promotions {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(min(200px, 100%), 1fr));
    gap: 8px;
}

.cockpit-promo-stamp {
    min-width: 0;
}

.cockpit-promo-stamp.is-prestige,
.cockpit-promo-stamp.is-ace {
    border-color: var(--cockpit-amber);
    box-shadow: inset 0 0 12px color-mix(in srgb, var(--cockpit-amber) 35%, transparent);
}

.cockpit-promo-stamp.is-prestige {
    grid-column: 1 / -1;
}

.cockpit-promo-stamp.is-prestige .cockpit-promo-title,
.cockpit-promo-stamp.is-ace .cockpit-promo-title {
    color: var(--cockpit-amber);
}

.cockpit-promo-title {
    font-size: 22px;
}

.cockpit-promo-subhead {
    margin-top: 4px;
    font-size: 12px;
    letter-spacing: 1px;
    text-transform: uppercase;
    color: var(--cockpit-green-dim);
}

.cockpit-promo-lines {
    display: grid;
    gap: 2px;
    margin: 0;
    padding-left: 18px;
    font-size: 13px;
    overflow-wrap: anywhere;
}

.cockpit-promo-lines .is-closed {
    color: var(--cockpit-amber);
    list-style: square;
}

.cockpit-promo-person {
    display: flex;
    align-items: center;
    gap: 8px;
    min-width: 0;
}

.cockpit-promo-person .character-portrait {
    flex: 0 0 56px;
    width: 56px;
    height: 56px;
}

.cockpit-promo-person-text {
    min-width: 0;
    overflow-wrap: anywhere;
}

/* docs/ACHIEVEMENTS-PLAN.md Phase 1 - a badge stamp ("Bronze · Battering Ram") and the Killer badge merged onto an ACE stamp,
   coloured by tier through the --achievement-* tokens. */
.cockpit-promo-stamp.is-achievement {
    --tier-color: var(--cockpit-green);
    border-color: var(--tier-color);
    box-shadow: inset 0 0 12px color-mix(in srgb, var(--tier-color) 35%, transparent);
}

.cockpit-promo-stamp.is-achievement .cockpit-promo-title,
.cockpit-promo-badge {
    color: var(--tier-color);
    overflow-wrap: anywhere;
}

.cockpit-promo-badge {
    font-weight: bold;
    letter-spacing: 2px;
    text-transform: uppercase;
    border-top: 1px solid var(--tier-color);
    padding-top: 4px;
    margin-top: 2px;
}

/* /profile's Achievements grid (Profile/AchievementRows). A cell is dim until a tier is earned, then takes its tier colour. */
.achievement-card {
    --tier-color: var(--cockpit-green-dim);
    display: flex;
    flex-direction: column;
    gap: 4px;
    min-width: 0;
    padding: 8px;
    border: 1px solid var(--tier-color);
    box-shadow: inset 0 0 8px color-mix(in srgb, var(--tier-color) 25%, transparent);
}

.achievement-card.is-none {
    opacity: 0.8;
}

.achievement-card-head {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: 6px;
    min-width: 0;
}

.achievement-card-name {
    font-weight: bold;
    overflow-wrap: anywhere;
}

.achievement-card-tier {
    flex: 0 0 auto;
    font-size: 11px;
    letter-spacing: 1px;
    text-transform: uppercase;
    color: var(--tier-color);
}

.achievement-card-description {
    font-size: 12px;
    color: var(--cockpit-green-dim);
    overflow-wrap: anywhere;
}

.achievement-card .cockpit-bar-fill {
    background: var(--tier-color);
}

.achievement-card.is-none .cockpit-bar-fill {
    background: var(--cockpit-green);
}

.achievement-card .cockpit-bar-track {
    height: 6px;
}

.achievement-card-foot {
    display: flex;
    flex-wrap: wrap;
    justify-content: space-between;
    gap: 2px 8px;
    font-size: 12px;
}

.achievement-card-earned {
    font-size: 11px;
    color: var(--cockpit-green-dim);
}

.achievement-card-earned.is-pending {
    font-style: italic;
}

/* /profile's layout (Pages/Profile.razor): the Achievements panel spans the right column's two rows, so the Delete panel (last in
   DOM order, for a phone) sits straight under the profile panel on a wide screen. */
.profile-layout {
    /* Row 1 is the profile panel's own height; the spanning Achievements panel's extra height goes to row 2 (fr tracks take a
       spanning item's surplus first), so Delete starts right under the profile panel instead of halfway down. */
    grid-template-rows: auto 1fr;
}

.profile-layout > * {
    align-self: start;
}

.profile-layout > .profile-achievements {
    grid-row: span 2;
}

/* docs/ACHIEVEMENTS-PLAN.md Phase 4 - /leaderboards (Pages/Leaderboards.razor) and /profile's Career panel. The board table fits a phone:
   a long display name is cut with an ellipsis, never widening the page; numbers sit right-aligned in tabular figures. */
.leaderboard-layout > * {
    align-self: start;
}

.leaderboard-tabs .cockpit-tab {
    padding: 5px 10px;
}

.leaderboard-status {
    margin: 8px 0;
}

.leaderboard-table,
.career-table {
    width: 100%;
    border-collapse: collapse;
    table-layout: fixed;
    margin-top: 8px;
    font-variant-numeric: tabular-nums;
}

.leaderboard-table th,
.career-table th {
    text-align: left;
    text-transform: uppercase;
    letter-spacing: 0.5px;
    font-size: 11px;
    font-weight: normal;
    color: var(--cockpit-green-dim);
    padding: 4px 6px;
    border-bottom: 1px solid var(--cockpit-green-dim);
}

.leaderboard-table td,
.career-table td,
.career-table tbody th {
    padding: 5px 6px;
    border-bottom: 1px solid rgba(51, 255, 119, 0.12);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.leaderboard-table .leaderboard-rank {
    width: 3.5em;
    text-align: right;
}

.leaderboard-table .leaderboard-value {
    width: 7.5em;
    text-align: right;
}

.leaderboard-table .leaderboard-matches {
    width: 5.5em;
    text-align: right;
}

.leaderboard-player {
    overflow: hidden;
    text-overflow: ellipsis;
}

.leaderboard-prestige {
    font-size: 11px;
    color: var(--cockpit-green-dim);
}

.leaderboard-row.is-own td {
    background: var(--cockpit-panel-hover-bg);
    color: var(--cockpit-green-bright);
}

.leaderboard-row.is-own .leaderboard-name {
    box-shadow: inset 3px 0 0 var(--cockpit-amber);
}

.leaderboard-gap td {
    text-align: center;
    color: var(--cockpit-green-dim);
    border-bottom: none;
    padding: 0;
}

.leaderboard-paging {
    margin-top: 10px;
}

@container (max-width: 480px) {
    .leaderboard-table .leaderboard-value {
        width: 6em;
    }

    .leaderboard-table .leaderboard-matches {
        width: 4em;
    }
}

.career-table-wrap {
    overflow-x: auto;
}

/* Auto layout: the Division names wrap rather than being cut, the numbers keep their width; the wrap scrolls only as a last resort. */
.career-table {
    table-layout: auto;
}

.career-table thead th {
    white-space: normal;
    vertical-align: bottom;
}

.career-table tbody th {
    text-transform: none;
    letter-spacing: 0;
    font-size: inherit;
    color: var(--cockpit-green);
    white-space: normal;
}

.career-table td {
    text-align: right;
}

.career-table th:not(:first-child) {
    text-align: right;
}

@container (max-width: 520px) {
    .career-table {
        font-size: 11px;
    }

    .career-table thead th {
        letter-spacing: 0;
        font-size: 10px;
        padding: 3px;
    }

    .career-table td,
    .career-table tbody th {
        padding: 4px 3px;
    }
}

.career-table tr.is-all-time th,
.career-table tr.is-all-time td {
    color: var(--cockpit-green-bright);
}

.career-forfeits {
    display: block;
    font-size: 11px;
    color: var(--cockpit-green-dim);
}

.career-recent {
    list-style: none;
    margin: 0;
    padding: 0;
}

.career-recent-row {
    display: grid;
    grid-template-columns: 6.5em minmax(0, 1fr) auto;
    gap: 0 8px;
    padding: 4px 0;
    border-bottom: 1px solid rgba(51, 255, 119, 0.12);
    font-size: 12px;
}

.career-recent-outcome {
    grid-column: 1;
    grid-row: 1 / span 2;
    align-self: center;
    text-transform: uppercase;
    letter-spacing: 0.5px;
}

.career-recent-row.is-won .career-recent-outcome {
    color: var(--cockpit-amber);
}

.career-recent-row.is-lost .career-recent-outcome,
.career-recent-row.is-forfeit .career-recent-outcome {
    color: var(--cockpit-green-dim);
}

.career-recent-map {
    grid-column: 2;
    grid-row: 1;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.career-recent-when {
    grid-column: 3;
    grid-row: 1;
    color: var(--cockpit-green-dim);
    text-align: right;
    white-space: nowrap;
}

.career-recent-detail {
    grid-column: 2 / span 2;
    grid-row: 2;
    color: var(--cockpit-green-dim);
    overflow-wrap: anywhere;
}

/* Two classes strong, over .cockpit-field-row label's block, uppercase field-label look. */
.cockpit-field-row .career-hide-toggle {
    display: flex;
    align-items: center;
    gap: 8px;
    cursor: var(--cursor-action);
    text-transform: none;
    letter-spacing: 0;
    font-size: 13px;
}

.career-hide-toggle input {
    accent-color: var(--cockpit-green);
    width: 16px;
    height: 16px;
}

/* After every rule that sets a default --tier-color, and two classes strong, so a tier always wins. */
:is(.achievement-card, .cockpit-promo-stamp, .cockpit-promo-badge).is-bronze { --tier-color: var(--achievement-bronze); }
:is(.achievement-card, .cockpit-promo-stamp, .cockpit-promo-badge).is-silver { --tier-color: var(--achievement-silver); }
:is(.achievement-card, .cockpit-promo-stamp, .cockpit-promo-badge).is-gold { --tier-color: var(--achievement-gold); }

/* A delayed stamp would sit invisible through its delay even with a zero
   duration - under reduced motion the stamps are simply there. */
@media (prefers-reduced-motion: reduce) {
    .cockpit-stamp,
    .cockpit-finish-beat-text {
        animation: none;
    }
}

/* ==========================================================================
   Gambling hall (docs/GAMBLING-HALL-PLAN.md Phase 4) - /hall's card table, odds buttons, bet slip and results.
   ========================================================================== */
.hall-card-table {
    width: 100%;
    border-collapse: collapse;
}

/* More than eight picks (a 12-car card): no body line under each car (Details has it), so the bet slip stays on screen beside the card. */
.hall-card-table.is-dense .hall-pick-sub {
    display: none;
}

.hall-card-table th {
    text-align: left;
    font-size: 11px;
    font-weight: normal;
    letter-spacing: 0.5px;
    text-transform: uppercase;
    color: var(--cockpit-green-dim);
}

.hall-card-table td {
    vertical-align: top;
    padding: 2px 2px 2px 0;
}

.hall-card-table th.hall-odds-col,
.hall-card-table td.hall-odds-col {
    text-align: right;
    white-space: nowrap;
    width: 1%;
}

/* The kinds' headers never run together ("TEAM WINTEAM PLACE"). */
.hall-card-table th.hall-odds-col {
    padding-left: 10px;
}

.hall-pick-name {
    font-weight: bold;
}

.hall-pick-sub {
    font-size: 12px;
    color: var(--cockpit-green-dim);
}

.hall-odds {
    font-family: var(--cockpit-font);
    font-size: 13px;
    min-width: 3.4em;
    padding: 1px 4px;
    border: 1px solid var(--cockpit-green-dim);
    background: transparent;
    color: var(--cockpit-green);
    cursor: var(--cursor-action);
}

.hall-odds:disabled {
    cursor: var(--cursor-not-allowed);
    border-color: transparent;
    color: var(--cockpit-green-dim);
}

.hall-odds.is-picked {
    background: var(--cockpit-green);
    color: var(--cockpit-bg);
}

.hall-details-toggle {
    font-family: var(--cockpit-font);
    font-size: 11px;
    background: transparent;
    border: none;
    color: var(--cockpit-green-dim);
    text-decoration: underline;
    cursor: var(--cursor-action);
    padding: 0 0 0 4px;
}

.hall-details {
    border-left: 2px solid var(--cockpit-green-dim);
    padding: 2px 0 4px 8px;
    margin-bottom: 4px;
}

.hall-crew-row {
    display: flex;
    align-items: center;
    gap: 8px;
    font-size: 13px;
}

.hall-line {
    font-size: 13px;
    margin-top: 2px;
}

.hall-won {
    color: var(--cockpit-green-bright);
}

.hall-lost {
    color: #7f8c8d;
}

.hall-stake-input {
    width: 6em;
}

.hall-bet-row {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 6px;
    font-size: 13px;
    margin-top: 3px;
}

/* The View log modal: the whole match, scrolling inside the modal. */
.hall-record-log {
    height: min(60vh, 520px);
    margin-top: 0;
}

/* docs/HALL-AUTOPLAY-PLAN.md Phase 2: the small badge an autoplay screen shows while it reconnects (the last frame stays). */
.hall-reconnecting {
    color: var(--cockpit-amber);
    border: 1px solid var(--cockpit-amber);
    padding: 0 6px;
    font-size: 12px;
}

.cockpit-button.is-on {
    background: var(--cockpit-green);
    color: var(--cockpit-bg);
}

/* ==========================================================================
   docs/HALL-AUTOPLAY-PLAN.md Phase 3 - the broadcast layout (/hall?autoplay=1&broadcast=1, /hall-screen.html?broadcast=1).
   One full-screen grid sized in vh/vw (no container queries, no :has, no aspect-ratio: OBS's browser source is an older Chromium),
   built for 1920x1080 and the same picture scaled at 1280x720. Nothing in it scrolls, and there is no control, link or tooltip.
   Band on top, the arena and the side column in the middle, the ticker and the join corner at the bottom. The side column is wide
   in the lobby (the card's tiles) and narrow while a match plays (the standings) - is-preview / is-live.
   ========================================================================== */
.hall-broadcast {
    --hb-font: clamp(11px, 1.85vh, 30px);
    --hb-gap: 1.1vh;
    --hb-side: 30vw;
    position: fixed;
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
    box-sizing: border-box;
    display: grid;
    grid-template-columns: minmax(0, 1fr) var(--hb-side);
    grid-template-rows: auto minmax(0, 1fr) auto;
    grid-template-areas:
        "band band"
        "field side"
        "foot foot";
    gap: var(--hb-gap);
    padding: var(--hb-gap);
    overflow: hidden;
    background: var(--cockpit-bg);
    color: var(--cockpit-green);
    font-family: var(--cockpit-font);
    font-size: var(--hb-font);
    line-height: 1.25;
}

.hall-broadcast.is-preview {
    --hb-side: 60vw;
}

.hall-broadcast *,
.hall-broadcast *::before,
.hall-broadcast *::after {
    box-sizing: border-box;
}

/* ---- the band */
.hall-broadcast-band {
    grid-area: band;
    min-width: 0;
}

.hall-broadcast-band-row {
    display: flex;
    align-items: center;
    gap: 2.2vh;
    min-width: 0;
    padding: 0.7vh 1.4vh;
    border: 1px solid var(--cockpit-green-dim);
    background: var(--cockpit-panel-bg);
}

.hall-broadcast-brand {
    flex: none;
    font-size: 1.3em;
    font-weight: bold;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--cockpit-green-bright);
}

.hall-broadcast-status {
    flex: none;
    display: flex;
    align-items: baseline;
    gap: 0.6em;
}

.hall-broadcast-status-label {
    padding: 0.1em 0.5em;
    background: var(--cockpit-green);
    color: var(--cockpit-bg);
    font-weight: bold;
    text-transform: uppercase;
}

.hall-broadcast-status-value {
    font-size: 1.35em;
    font-weight: bold;
    white-space: nowrap;
}

.hall-broadcast-match {
    flex: 1 1 auto;
    min-width: 0;
    text-align: right;
    color: var(--cockpit-green);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.hall-broadcast .hall-reconnecting {
    flex: none;
    font-size: 0.8em;
}

/* ---- the arena */
.hall-broadcast-field {
    grid-area: field;
    min-width: 0;
    min-height: 0;
    position: relative;
}

.hall-broadcast-viewport {
    position: absolute;
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
    border: 1px solid var(--cockpit-green-dim);
    background: #000;
    overflow: hidden;
}

.hall-broadcast-svg {
    display: block;
    pointer-events: none;
}

/* While the camera pans at one zoom (broadcast-viewport.js), what does not change during a turn goes on its own compositor layers, so the pan moves a raster instead of re-rastering every terrain glyph
   each frame (docs/ART-STYLE.md "What a camera move costs"). Chromium only; other engines ignore it. */
.hall-broadcast-svg.is-camera-panning .arena-static-map,
.hall-broadcast-svg.is-camera-panning [data-layer="standing"] {
    will-change: transform;
}

/* ---- the side column */
.hall-broadcast-side {
    grid-area: side;
    min-width: 0;
    min-height: 0;
    display: flex;
    flex-direction: column;
    gap: var(--hb-gap);
    overflow: hidden;
}

.hall-broadcast-host {
    display: flex;
    flex-direction: column;
    gap: var(--hb-gap);
    min-height: 0;
    flex: 0 1 auto;
}

.hall-broadcast.is-preview .hall-broadcast-host {
    flex: 1 1 auto;
}

.hall-broadcast-host:empty {
    display: none;
}

.hall-broadcast-panel {
    min-width: 0;
    min-height: 0;
    padding: 1vh 1.3vh;
    border: 1px solid var(--cockpit-green-dim);
    background: var(--cockpit-panel-bg);
    overflow: hidden;
}

.hall-broadcast-heading {
    margin: 0 0 0.6vh;
    font-size: 1em;
    font-weight: bold;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--cockpit-green-bright);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.hall-broadcast-line {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.hall-broadcast-more,
.hall-broadcast-quiet {
    color: var(--cockpit-green-dim);
}

/* the standings */
.hall-broadcast-standings {
    width: 100%;
    border-collapse: collapse;
    table-layout: fixed;
}

.hall-broadcast-standings td {
    padding: 0.25vh 0.4em 0.25vh 0;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    vertical-align: top;
}

/* First in the side column, above the host's panels (the bets riding on the match). */
.hall-broadcast-standings-panel {
    order: -1;
    flex: none;
}

.hall-broadcast-car {
    width: 30%;
    font-weight: bold;
}

/* The broadcast's armor (docs/PLAN.md "Armor table and damage flash"): five fixed columns, letters small above them. */
/* Still a fixed layout (the band's width, not the text, sizes the columns): the car a third, the five armor columns their own
   fixed width, the status the rest, ellipsised - a long Spanish status must never push the table past the screen. */
.hall-broadcast-standings.armor-table .armor-table-car {
    width: 32%;
}

/* The letters small, on the content - not the cell, whose em padding must match the numbers' cells to keep them aligned. */
.hall-broadcast-standings.armor-table thead th {
    padding-bottom: 0;
}

.hall-broadcast-standings.armor-table thead th > * {
    font-size: 0.7em;
}

.hall-broadcast-standings.armor-table .armor-table-status {
    display: table-cell;
    padding-left: 0.8em;
}

/* A settled match's bettor lines wrap rather than lose their end (who won what). */
.hall-broadcast-result .hall-broadcast-line {
    white-space: normal;
}

.hall-broadcast-standings tr.is-out td {
    opacity: 0.5;
}

/* the card */
.hall-broadcast-card {
    display: flex;
    flex-direction: column;
    gap: var(--hb-gap);
    min-height: 0;
    flex: 1 1 auto;
}

.hall-broadcast-card-panel {
    display: flex;
    flex-direction: column;
    flex: 1 1 auto;
}

.hall-broadcast-betting {
    margin-left: 1em;
    font-weight: normal;
    letter-spacing: 0;
    text-transform: none;
    color: var(--cockpit-amber);
}

.hall-broadcast-teams {
    display: flex;
    flex-wrap: wrap;
    gap: 0.3vh 2em;
    margin-bottom: 0.8vh;
}

.hall-broadcast-team {
    display: flex;
    gap: 0.8em;
    align-items: baseline;
}

.hall-broadcast-team-name {
    font-weight: bold;
}

.hall-broadcast-tiles {
    flex: 1 1 auto;
    min-height: 0;
    display: grid;
    grid-template-columns: repeat(var(--hall-tile-columns, 4), minmax(0, 1fr));
    grid-auto-rows: minmax(0, 1fr);
    gap: var(--hb-gap);
}

.hall-broadcast-tile {
    min-width: 0;
    min-height: 0;
    display: flex;
    flex-direction: column;
    gap: 0.4vh;
    padding: 0.8vh 1vh;
    border: 1px solid var(--cockpit-green-dim);
    border-top-width: 0.5vh;
    background: var(--cockpit-bg);
    overflow: hidden;
    font-size: 0.82em;
}

.hall-broadcast-tile > div {
    min-width: 0;
    flex: none;
}

/* Two rows of tiles (more than four cars): one line per weapon and per crew member, so a car fits half the height. */
.hall-broadcast-tiles.is-compact .hall-broadcast-tile-weapons > div {
    white-space: nowrap;
}

.hall-broadcast-tiles.is-compact .hall-broadcast-tile {
    gap: 0.2vh;
    font-size: 0.78em;
}

/* ...and each crew member on one line, the name cut short before the skills. */
.hall-broadcast-tiles.is-compact .hall-broadcast-crew-row {
    flex-wrap: nowrap;
}

/* Three rows of tiles (more than eight cars): the prices on one line straight under the name, so a car with many weapons loses its last crew row and never its odds. */
.hall-broadcast-tiles.is-compact.is-dense .hall-broadcast-tile-odds {
    font-size: 0.92em;
    order: -1;
    margin-top: 0;
    flex-wrap: nowrap;
    justify-content: space-between;
    gap: 0.4em;
    padding-top: 0;
    padding-bottom: 0.3vh;
    border-top: 0;
    border-bottom: 1px solid var(--cockpit-green-dim);
}

.hall-broadcast-tiles.is-dense .hall-broadcast-tile-head {
    order: -2;
}

.hall-broadcast-tile-head {
    display: flex;
    flex-direction: column;
    gap: 0.4vh;
    min-width: 0;
}

/* A two-row card: the name and the body on one line, and the odds closer together, so the tile fits half the height. */
.hall-broadcast-tiles.is-compact .hall-broadcast-tile-head {
    flex-direction: row;
    align-items: baseline;
    gap: 0.6em;
}

.hall-broadcast-tiles.is-compact .hall-broadcast-tile-name {
    flex: none;
    max-width: 60%;
}

.hall-broadcast-tiles.is-compact .hall-broadcast-tile-model {
    flex: 1 1 auto;
    min-width: 0;
}

.hall-broadcast-tiles.is-compact .hall-broadcast-tile-odds {
    gap: 0.2vh 0.7em;
}

.hall-broadcast-tile-name {
    font-size: 1.3em;
    font-weight: bold;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.hall-broadcast-tile-model {
    color: var(--cockpit-green-dim);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.hall-broadcast-tile-weapons > div {
    overflow: hidden;
    text-overflow: ellipsis;
}

.hall-broadcast-crew-name {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.hall-broadcast-ticker-idle {
    color: var(--cockpit-green-dim);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.hall-broadcast-tile-crew {
    display: flex;
    flex-direction: column;
    gap: 0.3vh;
}

.hall-broadcast-crew-row {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: 0.2vh 0.5em;
    min-width: 0;
}

.hall-broadcast-crew-name {
    min-width: 0;
    flex: 1 1 auto;
}

.hall-broadcast-skills {
    flex: none;
    display: flex;
    gap: 0.45em;
    white-space: nowrap;
}

.hall-broadcast-skill {
    font-weight: bold;
}

.hall-broadcast-tile-odds {
    margin-top: auto;
    display: flex;
    flex-wrap: wrap;
    gap: 0.2vh 1em;
    padding-top: 0.4vh;
    border-top: 1px solid var(--cockpit-green-dim);
}

.hall-broadcast-odds {
    white-space: nowrap;
    font-weight: bold;
    color: var(--cockpit-amber);
}

.hall-broadcast-odds-kind {
    font-weight: normal;
    color: var(--cockpit-green-dim);
}

.hall-broadcast-bettors {
    flex: none;
}

.hall-broadcast-bettor-list {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 0.2vh 2em;
}

.hall-broadcast.is-live .hall-broadcast-bettor-list {
    grid-template-columns: minmax(0, 1fr);
}

/* Under the cars (Geoff 2026-10-22): the recent results, then the bets. The wide lobby column sets the two side by side, recent results
   first, so the card keeps its height (stacked, a two-row card of eight cars was cut off); the narrow live column stacks them. */
.hall-broadcast-under {
    flex: none;
    display: flex;
    flex-direction: column;
    gap: var(--hb-gap);
    min-width: 0;
}

.hall-broadcast.is-preview .hall-broadcast-under {
    flex-direction: row;
    align-items: stretch;
}

.hall-broadcast.is-preview .hall-broadcast-under > * {
    flex: 1 1 0;
}

.hall-broadcast.is-preview .hall-broadcast-under .hall-broadcast-bettor-list {
    grid-template-columns: minmax(0, 1fr);
}

/* The recent results: one line per finished match, newest first. Each line keeps its winner and turns; a long map name is cut with an
   ellipsis first. */
.hall-broadcast-recent {
    flex: none;
}

.hall-broadcast-recent-list {
    display: flex;
    flex-direction: column;
    gap: 0.2vh;
}

.hall-broadcast-recent-row {
    display: flex;
    align-items: baseline;
    gap: 0.6em;
    min-width: 0;
    white-space: nowrap;
}

.hall-broadcast-recent-time,
.hall-broadcast-recent-turns {
    flex: none;
    font-size: 0.85em;
    color: var(--cockpit-green-dim);
}

/* The map takes what is left (cut first, down to 3em); the winner is cut only after that. */
.hall-broadcast-recent-map {
    flex: 1 1 0;
    min-width: 3em;
    overflow: hidden;
    text-overflow: ellipsis;
}

.hall-broadcast-recent-winner {
    flex: 0 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    font-weight: bold;
    color: var(--cockpit-amber);
}

/* What the winner went off at ("@ 4.9"): never cut; a longshot (HallRecentResults.LongshotOdds) is set bright on an amber chip. */
.hall-broadcast-recent-odds {
    flex: none;
    color: var(--cockpit-amber);
}

.hall-broadcast-recent-odds.is-longshot {
    padding: 0 0.3em;
    font-weight: bold;
    background: var(--cockpit-amber);
    color: var(--cockpit-bg);
}

.hall-broadcast-recent-winner.hall-broadcast-quiet {
    font-weight: normal;
    color: var(--cockpit-green-dim);
}

/* ---- the foot: ticker and join corner */
.hall-broadcast-foot {
    grid-area: foot;
    min-width: 0;
    display: flex;
    align-items: stretch;
    gap: var(--hb-gap);
    height: 16vh;
}

.hall-broadcast-ticker {
    flex: 1 1 auto;
    min-width: 0;
    display: flex;
    flex-direction: column;
    justify-content: center;
    gap: 0.3vh;
    padding: 0.6vh 1.3vh;
    border: 1px solid var(--cockpit-green-dim);
    background: var(--cockpit-panel-bg);
    overflow: hidden;
}

.hall-broadcast-ticker-line {
    font-size: 0.95em;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.hall-broadcast-ticker-line:first-child {
    font-size: 1.05em;
    font-weight: bold;
}

.hall-broadcast-ticker-line:nth-child(3) {
    opacity: 0.6;
}

.hall-broadcast-join {
    flex: none;
    display: flex;
    align-items: center;
    gap: 1.2vh;
    padding: 0.6vh 0.6vh 0.6vh 1.3vh;
    border: 1px solid var(--cockpit-green-dim);
    background: var(--cockpit-panel-bg);
}

.hall-broadcast-join-text {
    text-align: right;
}

.hall-broadcast-join-label {
    font-size: 0.85em;
    color: var(--cockpit-green-dim);
    white-space: nowrap;
}

.hall-broadcast-join-address {
    font-weight: bold;
    color: var(--cockpit-green-bright);
    white-space: nowrap;
}

/* About 15vh square: ~3 px per module at 720p, so a phone reads it off a scaled-down stream. */
.hall-broadcast-qr {
    height: 100%;
    width: 14.8vh;
    background: #fff;
}

.hall-broadcast-qr svg {
    display: block;
    width: 100%;
    height: 100%;
}

/* /hall's end-of-match banner, centred over the map (Geoff 2026-10-20). Display only: clicks pass through to the map. */
.hall-winner-banner {
    position: absolute;
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%);
    z-index: 5;
    min-width: min(320px, 80%);
    max-width: 90%;
    padding: 16px 24px;
    text-align: center;
    pointer-events: none;
    /* .cockpit-field sets line-height: 0 (for the SVG); text needs its own. */
    line-height: 1.4;
    background: rgba(0, 0, 0, 0.85);
    border: 2px solid var(--cockpit-green);
    box-shadow: 0 0 24px rgba(0, 255, 102, 0.35);
}

.hall-winner-title {
    font-size: 0.8rem;
    letter-spacing: 0.2em;
    text-transform: uppercase;
    opacity: 0.8;
}

.hall-winner-headline {
    margin: 6px 0 4px;
    font-size: 1.6rem;
    font-weight: bold;
    color: var(--cockpit-green);
}

.hall-winner-subtitle {
    margin-top: 10px;
    font-size: 0.75rem;
    letter-spacing: 0.15em;
    text-transform: uppercase;
    opacity: 0.8;
}

.hall-winner-line {
    margin-top: 2px;
}

/* /hall's Recent results scroll inside their panel rather than stretching the rail (Geoff 2026-10-20). */
.hall-recent-list {
    max-height: 30vh;
    overflow-y: auto;
}

/* The office screen's join corner (plain /hall?autoplay=1): the broadcast's QR code, small, in the hall panel. */
.hall-office-join {
    display: flex;
    align-items: center;
    gap: 10px;
    margin-top: 8px;
}

.hall-office-qr {
    flex: none;
    width: 112px;
    height: 112px;
    background: #fff;
}

.hall-office-qr svg {
    display: block;
    width: 100%;
    height: 100%;
}

/* Native number spinners can't take the cockpit's colours (Geoff 2026-10-20: the hall stake's arrows were unstyled), so they are
   hidden on the stake, which has its own preset buttons; the arrow keys still step the value. */
.hall-stake-input[type="number"] {
    -moz-appearance: textfield;
    appearance: textfield;
}

.hall-stake-input[type="number"]::-webkit-inner-spin-button,
.hall-stake-input[type="number"]::-webkit-outer-spin-button {
    -webkit-appearance: none;
    margin: 0;
}

/* ==========================================================================
   Dev dashboard (docs/OBSERVABILITY-PLAN.md Phase 2) - /dev/matches and /dev/matches/{id}. Wide tables scroll inside their own box so a
   phone never scrolls the page sideways.
   ========================================================================== */
.dev-filters {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(min(220px, 100%), 1fr));
    gap: 8px 16px;
    align-items: end;
    margin-bottom: 8px;
}

.dev-filters > .cockpit-field-row {
    margin-top: 0;
}

.dev-filters input[type="date"] {
    width: 100%;
    box-sizing: border-box;
}

.dev-col-centered {
    margin-inline: auto;
}

.dev-table-scroll {
    overflow-x: auto;
    max-width: 100%;
}

.dev-table-tall {
    max-height: 480px;
    overflow-y: auto;
}

.dev-table {
    width: 100%;
    min-width: 640px; /* on a phone the box scrolls sideways rather than squashing every column to a word */
    border-collapse: collapse;
    font-size: 13px;
}

.dev-table th {
    position: sticky;
    top: 0;
    background: var(--cockpit-panel-bg);
    text-align: left;
    font-size: 11px;
    font-weight: normal;
    letter-spacing: 0.5px;
    text-transform: uppercase;
    color: var(--cockpit-green-dim);
    white-space: nowrap;
    padding: 2px 6px;
}

.dev-table td {
    vertical-align: top;
    padding: 3px 6px;
    border-top: 1px solid rgba(51, 255, 119, 0.12);
}

.dev-table .dev-num {
    text-align: right;
    white-space: nowrap;
}

/* A number column followed by a text column: a gap so a right-aligned value never reads into the next column's text. */
.dev-table .dev-num + :not(.dev-num) {
    padding-left: 16px;
}

.dev-table td.cockpit-log-line {
    white-space: normal;
}

.dev-half-link {
    display: block;
    font-size: 12px;
}

/* docs/PLAN.md "AFK auto-pilot pacing": an auto-piloted match in /dev/matches. */
.dev-autopilot-badge {
    display: block;
    width: fit-content;
    margin-top: 2px;
    padding: 0 4px;
    font-size: 12px;
    color: var(--cockpit-amber);
    border: 1px solid var(--cockpit-amber);
    border-radius: 2px;
}

.dev-match-title {
    font-size: 18px;
    margin: 0 0 8px;
}

.dev-facts {
    display: grid;
    grid-template-columns: max-content 1fr;
    gap: 2px 12px;
    margin: 0 0 8px;
}

.dev-facts dt {
    color: var(--cockpit-green-dim);
}

.dev-facts dd {
    margin: 0;
    overflow-wrap: anywhere;
}

.dev-subheading {
    margin-top: 8px;
}

.dev-awards {
    margin: 4px 0 0;
    padding-left: 20px;
}

.cockpit-log.dev-log {
    height: auto;
    max-height: 520px;
}

/* A table of a few short columns (the timings): as wide as its content, not stretched across the panel. */
.dev-table.dev-table--compact {
    width: auto;
    min-width: 0;
}

/* docs/OBSERVABILITY-PLAN.md Phase 3a: a spawn the stats page flags (its corrected p-value is under 0.05). */
.dev-table tr.dev-row-flagged td {
    color: var(--cockpit-amber);
}

/* docs/OBSERVABILITY-PLAN.md D-13 (Phase 3b): the /dev overview's native-SVG trend charts (Dev/TrendChart.razor). The series colours are
   the cockpit tokens - one --chart-N slot per series - so the charts follow the theme like everything else. */
:root {
    --chart-1: var(--cockpit-green);
    --chart-2: var(--rarity-rare);
    --chart-3: var(--cockpit-amber);
    --chart-4: var(--rarity-epic);
    --chart-5: var(--cockpit-red);
    --chart-6: var(--rarity-common);
}

.trend-chart {
    min-width: 0;
}

.trend-chart-svg {
    display: block;
    width: 100%;
    height: auto;
    overflow: visible;
    font-family: var(--cockpit-font);
    touch-action: manipulation;
}

.trend-chart-svg:focus-visible {
    outline: 1px solid var(--cockpit-green-bright);
    outline-offset: 2px;
}

.trend-c1 { --trend-color: var(--chart-1); }
.trend-c2 { --trend-color: var(--chart-2); }
.trend-c3 { --trend-color: var(--chart-3); }
.trend-c4 { --trend-color: var(--chart-4); }
.trend-c5 { --trend-color: var(--chart-5); }
.trend-c6 { --trend-color: var(--chart-6); }

.trend-bar { fill: var(--trend-color); opacity: 0.85; }
.trend-line { fill: none; stroke: var(--trend-color); stroke-width: 1.6; stroke-linejoin: round; stroke-linecap: round; }
.trend-dot { fill: var(--trend-color); }
.trend-band { fill: var(--trend-color); opacity: 0.22; stroke: none; }
.trend-shade { fill: var(--cockpit-green); opacity: 0.07; }
.trend-grid { stroke: var(--cockpit-green-dim); stroke-width: 0.5; opacity: 0.35; }
.trend-axis { stroke: var(--cockpit-green-dim); stroke-width: 1; }
.trend-tick { fill: var(--cockpit-green-dim); font-size: 12px; }
.trend-marker { stroke: var(--cockpit-amber); stroke-width: 1; stroke-dasharray: 3 3; opacity: 0.8; }
.trend-selected { fill: var(--cockpit-green); opacity: 0.08; }
.trend-hit { fill: transparent; cursor: var(--cursor-crosshair); }

.trend-legend {
    display: flex;
    flex-wrap: wrap;
    gap: 4px 12px;
    font-size: 12px;
    margin-top: 4px;
}

.trend-legend-item {
    display: inline-flex;
    align-items: center;
    gap: 4px;
}

.trend-swatch {
    display: inline-block;
    width: 10px;
    height: 10px;
    background: var(--trend-color);
}

.trend-swatch--band {
    opacity: 0.4;
}

.trend-readout {
    min-height: 1.4em;
    font-size: 12px;
    margin-top: 4px;
    overflow-wrap: anywhere;
}

.trend-chart-hint {
    margin-top: 4px;
}

/* docs/OBSERVABILITY-PLAN.md Phase 8: /dev/hall's calibration plot (Dev/CalibrationChart.razor) and the headline notes. */
.calibration-svg {
    max-width: 420px;
}

.calibration-diagonal { fill: none; stroke: var(--cockpit-green-dim); stroke-width: 1; stroke-dasharray: 4 3; }
.calibration-point { --trend-color: var(--chart-1); }
.calibration-point--outside { --trend-color: var(--chart-5); }
.calibration-interval { stroke: var(--trend-color); stroke-width: 1.4; opacity: 0.7; }
.calibration-dot { fill: var(--trend-color); opacity: 0.9; }
.calibration-swatch { --trend-color: var(--chart-1); }
.calibration-swatch--outside { --trend-color: var(--chart-5); }

.dev-fact-note {
    display: block;
    font-size: 12px;
}

/* docs/OBSERVABILITY-PLAN.md Phase 6: /dev/health's sparklines (Dev/Sparkline.razor) - the trend charts' marks with the newest value above. */
.health-sparkline-head {
    display: flex;
    justify-content: flex-end;
    gap: 8px;
    flex-wrap: wrap;
    font-size: 12px;
    color: var(--cockpit-green-dim);
}

.health-sparkline-current {
    font-size: 16px;
    color: var(--cockpit-green);
    font-variant-numeric: tabular-nums;
}

/* docs/OBSERVABILITY-PLAN.md Phase 4: the match detail's AI decisions (Dev/AiTracePanel.razor) - candidates best first, their score terms as a
   diverging stacked bar in the trend charts' --chart-N colours. */
.dev-ai-summary {
    margin: 8px 0;
    padding-left: 20px;
    font-size: 13px;
}

.dev-ai-table td.dev-ai-terms {
    min-width: 220px;
    width: 55%;
}

.dev-ai-bar {
    display: block;
    width: 100%;
    height: 14px;
}

.dev-ai-zero { stroke: var(--cockpit-green-dim); stroke-width: 1; }
.dev-ai-segment { fill: var(--trend-color); opacity: 0.85; stroke: var(--cockpit-panel-bg); stroke-width: 0.5; }
.dev-ai-segment.dev-ai-jitter { opacity: 0.4; }

.dev-ai-terms-text {
    font-size: 11px;
    color: var(--cockpit-green-dim);
    white-space: normal;
}

.dev-ai-chosen td {
    color: var(--cockpit-green-bright);
}

.dev-ai-chosen-mark {
    color: var(--cockpit-amber);
    margin-left: 4px;
}

.dev-ai-disqualified {
    color: var(--cockpit-amber);
}

/* The /dev overview's range toggle: the chosen range reads as pressed (aria-pressed carries it for assistive tech). */
.cockpit-button.dev-range[aria-pressed="true"] {
    background: var(--cockpit-green-dim);
    color: var(--cockpit-bg);
}

/* docs/OBSERVABILITY-PLAN.md Phase 7: /dev/errors - one card per fingerprint, samples expanded below it. Stored text is plain text. */
.dev-table--narrow {
    min-width: 0;
}

.dev-error-group {
    border: 1px solid rgba(51, 255, 119, 0.18);
    padding: 8px;
    min-width: 0;
}

.dev-error-head {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 4px 10px;
}

.dev-error-origin {
    font-size: 11px;
    text-transform: uppercase;
    padding: 0 6px;
    border: 1px solid currentColor;
}

.dev-error-origin--client {
    color: var(--cockpit-red);
}

.dev-error-origin--server {
    color: var(--cockpit-amber);
}

.dev-error-title {
    color: var(--cockpit-green-bright);
    overflow-wrap: anywhere;
    flex: 1 1 12em;
}

.dev-error-count {
    font-variant-numeric: tabular-nums;
}

.dev-error-message,
.dev-error-fingerprint {
    margin: 4px 0;
    overflow-wrap: anywhere;
}

.dev-error-fingerprint {
    font-size: 12px;
    color: var(--cockpit-green-dim);
}

.dev-error-sample {
    border-top: 1px solid rgba(51, 255, 119, 0.12);
    margin-top: 8px;
    padding-top: 8px;
}

.dev-error-stack {
    max-width: 100%;
    max-height: 320px;
    overflow: auto;
    margin: 4px 0 0;
    padding: 6px;
    font-size: 12px;
    white-space: pre;
    background: var(--cockpit-bg);
}


/* ==========================================================================
   Toasts (docs/PLAN.md "Toasts") - Cockpit/CockpitToastHost, drawn inside
   the page's CockpitModalHost (a shell sibling, outside every container).
   No toast is drawn while a modal is open (the host hides them), so the
   layer only has to clear the page and the finish beat; below the banner.
   Desktop: bottom-right, clear of the page; on the fit layout (/play,
   /watch) bottom-LEFT, over the arena's corner - the rail (Commit turn, the
   log) is the right edge and the top bar the top. Below the modal sheet
   breakpoint: the top edge - under the top bar and any banner (cockpit-
   toast.js measures them) - full width less the gutters (/play's controls
   own the bottom). Every edge pads by --cockpit-safe-*.
   ========================================================================== */
:root {
    --cockpit-z-toast: 55;
    --cockpit-toast-width: 360px;
    --cockpit-toast-gutter: 16px;
}

.cockpit-toast-region {
    position: fixed;
    z-index: var(--cockpit-z-toast);
    right: calc(var(--cockpit-toast-gutter) + var(--cockpit-safe-right));
    bottom: calc(var(--cockpit-toast-gutter) + var(--cockpit-safe-bottom));
    width: min(var(--cockpit-toast-width), calc(100vw - 2 * var(--cockpit-toast-gutter) - var(--cockpit-safe-left) - var(--cockpit-safe-right)));
    display: flex;
    flex-direction: column;
    gap: 8px;
    /* The empty region never takes a click from the page under it - only a toast does. */
    pointer-events: none;
}

html:has(.cockpit-shell--fit) .cockpit-toast-region {
    right: auto;
    left: calc(var(--cockpit-toast-gutter) + var(--cockpit-safe-left));
}

@media (max-width: 699.98px) {
    .cockpit-toast-region,
    html:has(.cockpit-shell--fit) .cockpit-toast-region {
        --cockpit-toast-gutter: 8px;
        /* Below the top bar and any banner still on screen (cockpit-toast.js keeps --cockpit-toast-top at their lowest
           bottom edge while a toast is up), and never above the notch. */
        top: calc(max(var(--cockpit-toast-top, 0px), var(--cockpit-safe-top)) + var(--cockpit-toast-gutter));
        bottom: auto;
        left: calc(var(--cockpit-toast-gutter) + var(--cockpit-safe-left));
        right: calc(var(--cockpit-toast-gutter) + var(--cockpit-safe-right));
        width: auto;
        /* Newest nearest the edge it docks to. */
        flex-direction: column-reverse;
    }
}

.cockpit-toast {
    --cockpit-toast-accent: var(--cockpit-green);
    pointer-events: auto;
    display: flex;
    align-items: flex-start;
    gap: 10px;
    padding: 10px 8px 10px 12px;
    box-sizing: border-box;
    font-family: var(--cockpit-font);
    font-size: 14px;
    color: var(--cockpit-green);
    background: var(--cockpit-panel-bg);
    border: 1px solid var(--cockpit-toast-accent);
    border-left-width: 4px;
    box-shadow: 0 0 14px var(--cockpit-glow), 0 4px 16px rgba(0, 0, 0, 0.6);
    animation: cockpit-toast-enter var(--cockpit-dur-slow) var(--cockpit-ease);
    animation-fill-mode: none;
}

.cockpit-toast--success { --cockpit-toast-accent: var(--cockpit-green-bright); }
.cockpit-toast--achievement { --cockpit-toast-accent: var(--rarity-legendary); }
.cockpit-toast--warning { --cockpit-toast-accent: var(--cockpit-amber); }

.cockpit-toast-icon {
    flex: none;
    width: 1.6em;
    height: 1.6em;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    color: var(--cockpit-bg);
    background: var(--cockpit-toast-accent);
    border-radius: 50%;
    font-weight: bold;
}

.cockpit-toast-content {
    flex: 1 1 auto;
    min-width: 0;
    overflow-wrap: anywhere;
}

.cockpit-toast-title {
    display: block;
    color: var(--cockpit-toast-accent);
    text-transform: uppercase;
    letter-spacing: 1px;
}

/* The click-through: a link or a button that reads as the title, underlined so it says it goes somewhere. */
a.cockpit-toast-action,
button.cockpit-toast-action {
    padding: 0;
    font: inherit;
    text-align: left;
    background: none;
    border: none;
    cursor: var(--cursor-action);
    text-decoration: underline;
    text-underline-offset: 3px;
}

.cockpit-toast-action:focus-visible,
.cockpit-toast-dismiss:focus-visible {
    outline: 2px solid var(--cockpit-toast-accent);
    outline-offset: 2px;
}

.cockpit-toast-text {
    margin-top: 3px;
    color: var(--cockpit-green);
}

.cockpit-toast-dismiss {
    flex: none;
    min-width: 32px;
    min-height: 32px;
    margin: -6px 0 -6px 0;
    padding: 0 6px;
    font: inherit;
    font-size: 18px;
    line-height: 1;
    color: var(--cockpit-green);
    background: none;
    border: 1px solid transparent;
    cursor: var(--cursor-action);
}

.cockpit-toast-dismiss:hover {
    border-color: var(--cockpit-green-dim);
}

@keyframes cockpit-toast-enter {
    from { opacity: 0; transform: translateY(8px); }
    to { opacity: 1; transform: none; }
}

@keyframes cockpit-toast-fade {
    from { opacity: 0; }
    to { opacity: 1; }
}

/* Reduced motion: no slide - a quick fade. */
@media (prefers-reduced-motion: reduce) {
    .cockpit-toast {
        animation: cockpit-toast-fade 120ms linear;
    }
}

.cockpit-visually-hidden {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
}

/* docs/TEAM-COMMAND-PLAN.md Phase 3 - the Squad panel. The rail's panel shows on the fit layout, the chip row on the stacked / phone layout, never both (the
   same media query as .cockpit-shell--fit). One line per car and one status line, so the panel's height is fixed and the column still fits (verify-combat-fit.mjs). */
.cockpit-shell .cockpit-side .squad-rail {
    display: none;
    position: relative;
}

.cockpit-shell .cockpit-side .squad-chips {
    display: block;
    margin-bottom: 8px;
    font-size: 13px;
}

@media (min-width: 1000px) and (min-height: 600px), (min-width: 900px) and (min-height: 530px) and (orientation: landscape) {
    .cockpit-shell--fit .cockpit-side .squad-rail {
        display: block;
    }

    .cockpit-shell--fit .cockpit-side .squad-chips {
        display: none;
    }
}

.squad-panel-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
}

.squad-chips-title {
    color: var(--cockpit-green);
    font-weight: bold;
    letter-spacing: 1px;
    text-transform: uppercase;
}

.cockpit-shell--fit .squad-rail > .cockpit-panel-body {
    padding-top: 2px;
    padding-bottom: 3px;
}

/* 1000x600 (the fit layout's smallest): the Controls column has room for the cars but no more beside a running lead's plan; the turn log carries a dropped order, Combatants lists the independents. */
@media (max-height: 679px) {
    .squad-rail [data-mode="withturn"] [data-testid="squad-dropped"],
    .squad-rail [data-mode="withturn"] [data-testid="squad-independent"] {
        display: none;
    }

    /* Fire priority in the Car column gives up its title, so the first Combatants row stays on screen (the picker keeps its label and tooltip). */
    .cockpit-shell--fit .cockpit-side-group .is-beside-car .cockpit-panel-title {
        display: none;
    }

    .cockpit-shell--fit .cockpit-side-group .is-beside-car .cockpit-panel-body {
        padding-top: 3px;
        padding-bottom: 3px;
    }
}

.squad-rail .squad-follow-me-wrap {
    position: absolute;
    top: 1px;
    right: 4px;
    display: flex;
    align-items: center;
    background: var(--cockpit-panel-bg, #041c10);
    padding-left: 6px;
}

.squad-follow-me {
    min-height: 0;
    padding: 1px 6px;
    font-size: 11px;
    white-space: nowrap;
}

.squad-rows {
    display: flex;
    flex-direction: column;
    gap: 3px;
}

.squad-chips .squad-rows {
    flex-flow: row wrap;
    gap: 6px;
    margin-top: 4px;
}

.squad-row {
    position: relative;
    display: flex;
    align-items: center;
    gap: 6px;
    min-width: 0;
    padding-bottom: 3px;
}

.squad-chips .squad-row {
    flex: 1 1 160px;
    flex-wrap: wrap;
    box-sizing: border-box;
    padding: 4px 6px 6px;
    border: 1px solid var(--cockpit-green-dim);
    min-width: 0;
}

.squad-row.is-out {
    opacity: 0.55;
}

/* docs/TEAM-COMMAND-PLAN.md Phase 6 - the car the controls edit: a rule down the row's left edge, inside the row so the panel keeps its height. */
.squad-row.is-selected {
    box-shadow: inset 2px 0 0 var(--cockpit-amber, #f6ad55);
    padding-left: 5px;
    background: rgba(246, 173, 85, 0.08);
}

.squad-row-select {
    all: unset;
    box-sizing: border-box;
    cursor: var(--cursor-action);
}

.squad-row-select:focus-visible {
    outline: 1px solid currentColor;
    outline-offset: 1px;
}

.squad-row-select:disabled {
    cursor: var(--cursor-not-allowed);
}

.squad-row-lead .squad-row-select {
    flex: 1 1 auto;
}

.squad-row-mode {
    flex: none;
    padding: 0 3px;
    border: 1px solid currentColor;
    font-size: 10px;
    line-height: 1.4;
    text-transform: uppercase;
}

.squad-row-mode.is-you {
    color: var(--cockpit-amber, #f6ad55);
}

.squad-row-mode.is-ai {
    color: var(--cockpit-green-dim);
}

.squad-row-tick {
    flex: none;
    color: var(--cockpit-amber, #f6ad55);
    font-size: 11px;
    line-height: 1;
}

.squad-plan-row {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    align-items: center;
}

/* The Controls panel gives way before whatever is under it (Fire priority, or a driving squad's Squad panel), and scrolls inside itself with Commit first (never clipping those out of reach). A rejected waypoint's warnings are what fill it on a short column. */
@media (min-width: 1000px) and (min-height: 600px), (min-width: 900px) and (min-height: 530px) and (orientation: landscape) {
    .cockpit-shell--fit .cockpit-side-panels--columns .cockpit-side-group > .cockpit-controls-panel {
        flex: 0 1 auto;
        min-height: 0;
        display: flex;
        flex-direction: column;
    }

    .cockpit-shell--fit .cockpit-side-panels--columns .cockpit-controls-panel > .cockpit-panel-body {
        min-height: 0;
        overflow-x: hidden;
        overflow-y: auto;
    }

    /* Commit stays in view while the rest of the panel scrolls under it. */
    .cockpit-shell--fit .cockpit-side-panels--columns .cockpit-controls-panel .cockpit-commit-row {
        position: sticky;
        top: 0;
        z-index: 2;
        background: var(--cockpit-panel-bg, #041c10);
    }
}

/* 1000x600: a one-line Commit, Undo and Hand to AI on one row, tighter car rows - the room the lead's row and Hand to AI took. */
@media (max-height: 679px) {
    .cockpit-button-wide.squad-commit {
        padding: 5px 2px;
        font-size: 12px;
        letter-spacing: 0;
    }

    .squad-plan-row {
        flex-wrap: nowrap;
        gap: 4px;
    }

    .squad-plan-row .cockpit-button {
        padding: 2px 5px;
        font-size: 11px;
        letter-spacing: 0;
        white-space: nowrap;
    }

    .squad-plan-row .cockpit-tooltip-help {
        display: none;
    }

    .squad-rail .squad-rows {
        gap: 1px;
    }

    .squad-rail .squad-row {
        padding-bottom: 2px;
    }
}

.cockpit-ceremony-squad {
    width: 100%;
    border-collapse: collapse;
    font-size: 13px;
}

.cockpit-ceremony-squad th,
.cockpit-ceremony-squad td {
    padding: 2px 6px;
    text-align: left;
    font-weight: normal;
}

.cockpit-ceremony-squad thead th {
    color: var(--cockpit-green-dim);
    font-size: 11px;
    text-transform: uppercase;
}

.cockpit-ceremony-squad .is-number {
    text-align: right;
}

.cockpit-ceremony-squad tr.is-own {
    color: var(--cockpit-amber, #f6ad55);
}

.squad-row-swatch {
    flex: none;
    width: 8px;
    height: 8px;
    border-radius: 50%;
}

.squad-row-name {
    flex: 0 1 34%;
    min-width: 0;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
}

.squad-row-order {
    display: flex;
    flex: 1 1 0;
    min-width: 0;
}

.squad-chips .squad-row-order {
    flex: 1 1 100%;
}

.squad-row-order .cockpit-select-trigger {
    min-height: 0;
    padding-top: 0;
    padding-bottom: 0;
    font-size: 12px;
    line-height: 1.5;
}

.squad-row-order .cockpit-select-display {
    display: block;
    min-width: 0;
    overflow: hidden;
}

.squad-row-order .cockpit-select-ghost {
    /* The trigger's width is the row's, not its longest option's. */
    display: none;
}

.squad-row-order .cockpit-select-value {
    display: block;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
}

.squad-row-order .cockpit-select-popup {
    right: 0;
    left: auto;
    min-width: 170px;
    max-height: 200px;
}

.squad-row-move {
    flex: none;
    min-height: 0;
    padding: 1px 6px;
    font-size: 11px;
}

.squad-row-out {
    margin-left: auto;
    font-size: 12px;
}

.squad-row-health {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    height: 2px;
    background: rgba(255, 255, 255, 0.12);
}

.squad-row-health > span {
    display: block;
    height: 100%;
}

.squad-one-line {
    min-width: 0;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
}

.squad-chips .squad-one-line {
    flex: 1 1 100%;
    white-space: normal;
}

/* A Hold here waiting for its point: over the map, top-centre (Center on car holds the top-left corner, the fire-check chip the top-right). */
.squad-map-hint {
    position: absolute;
    top: 52px;
    left: 50%;
    transform: translateX(-50%);
    z-index: 3;
    display: flex;
    align-items: center;
    gap: 8px;
    max-width: calc(100% - 20px);
    box-sizing: border-box;
    padding: 4px 8px;
    background: rgba(5, 12, 5, 0.9);
    border: 1px solid var(--cockpit-warning, #ffb000);
    color: var(--cockpit-warning, #ffb000);
    font-size: 0.8rem;
    line-height: 1.3;
    white-space: nowrap;
}

.squad-map-hint > span {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* A phone: the whole sentence (a blocked point says why), on as many lines as it takes. */
@media (max-width: 699px) {
    .squad-map-hint {
        width: calc(100% - 20px);
        white-space: normal;
    }

    .squad-map-hint > span {
        overflow: visible;
    }
}

/* docs/TUTORIAL-CONTENT.md T-16: the tour's planning hint takes two or three lines of the Controls column, which a short, narrow fit layout does not have spare (it would clip Fire priority under it).
   Nothing is hidden: "This move" and the speed readout each say something no other row does. The column packs tighter instead, for the two turns the hint is up. */
@media (min-width: 900px) and (min-height: 530px) and (orientation: landscape) and (max-width: 1200px) and (max-height: 700px), (min-width: 1000px) and (min-height: 600px) and (max-width: 1200px) and (max-height: 700px) {
    .cockpit-controls:has([data-testid="tutorial-plan-hint"]) > div {
        margin-top: 3px;
    }

    .cockpit-controls:has([data-testid="tutorial-plan-hint"]) > div:first-child {
        margin-top: 0;
    }

    .cockpit-controls:has([data-testid="tutorial-plan-hint"]) .cockpit-hint {
        margin-top: 2px;
    }

    .cockpit-controls:has([data-testid="tutorial-plan-hint"]) .cockpit-button-wide {
        padding: 4px 8px;
    }
}
