  :root{
    /* Dark shell, colors reworked only — layout/structure below is
       untouched. Cold Signal HUD accent (icy dominant + muted secondary
       + one danger red) layered on top of the ORIGINAL rarity ramp
       (gray/blue/violet/gold), which was deliberately left alone. */
    --paper:#0e0e0e; --paper-dim:#171917;
    --ink:#eef1ea; --ink-soft:#9aa0ab; --ink-faint:#5d6a65; --line:#262a26;
    /* --blue = shield/secondary, --red = danger, --cyan = dominant
       (HP/extraction/win-state), --green = dominant (kept as a second
       alias so existing HP-fill/dropZone-OK rules stay green-named in
       code but render the same dominant icy hue), --amber = gold
       (shared with epic-tier), --violet = rare-tier hue (loot glyph),
       --orange = ammo-loot glyph, distinct from danger red. */
    --blue:#4c5a5f; --red:#e2432f; --cyan:#dfe9ec; --amber:#ffc933;
    --green:#dfe9ec; --violet:#8b5cf6; --orange:#fb923c;
    --rarity-common:#8e8e8e; --rarity-uncommon:#60a5fa; --rarity-rare:#8b5cf6; --rarity-epic:#ffc933;
    /* In-run functional colors — each state gets its own hue so they
       never get confused for one another at a glance: shield=blue,
       sprint=amber, weapon/stamina overheat=hot-orange (NOT the same
       red as HP-critical), extraction=safe-green (NOT the same icy
       hue as HP/dominant). */
    --shield:#4ca4e9; --sprint:#ffb347; --heat:#ff6a3d; --safe:#34d399;
    /* Shield-MOD (attachment, LMG-only, Z-activation) — deliberately its
       own hue, not --amber (gold/epic-tier) and not --shield (the base
       gear-shield HUD segments above) — a bright, clean yellow so the
       riot-shield plate reads as its own distinct thing at a glance. */
    --shieldmod:#f5e050;
    /* Attachment-DERIVED weapon tier — separate palette from the standard
       item-rarity one above (attachments.js's ATTACHMENT_TIER_COLORS /
       SHIELD_TIER_COLORS). Uncommon/rare/epic are the same hex as
       --rarity-*; only common differs (green here vs. gray above) — a
       bare weapon with nothing slotted is plain gray (rarity-common), a
       weapon with real common-tier attachments equipped is green
       (attach-tier-common). */
    --attach-common:#22c55e;
    /* Whole Inventory overlay (weapon art, attachments, shield, equipment,
       cores, backpack + its aligned Backpack Slot) is scaled by
       an overall 1.2x factor off the original 34px cell / 3px gap
       baseline: 34px -> 41px, 3px gap -> 4px. Every slot in the overlay
       is built off these two variables so they all scale together. */
    --inv-cell: 41px;
    --inv-gap: 4px;
  }
  *{ box-sizing:border-box; }
  html,body{
    margin:0; padding:0; height:100%; width:100%; background:var(--paper); color:var(--ink);
    font-family:"SFMono-Regular","Cascadia Mono","JetBrains Mono","Courier New",monospace;
    overflow:hidden; user-select:none; -webkit-user-select:none; cursor:default;
  }
  #wrap{ position:fixed; inset:0; }
  #stage{ position:fixed; inset:0; }
  #gridFrame{ position:absolute; inset:0; cursor:crosshair; overflow:hidden; }
  #worldMap{ position:absolute; top:0; left:0; font-size:14px; line-height:14px; white-space:pre; will-change:transform; letter-spacing:5.6px; transform-origin:0 0; }
  #entityLayer{ position:absolute; inset:0; overflow:hidden; pointer-events:none; }
  .entityEl{ position:absolute; font-weight:700; transform:translate(-50%,-50%); font-size:14px; line-height:14px; }
  .bulletStreak{ position:absolute; width:16px; height:2px; border-radius:1px; pointer-events:none; z-index:2; }
  #playerBox{ position:absolute; top:50%; left:50%; width:8px; height:8px; margin:-4px 0 0 -4px; background:#000; z-index:3; }
  #staminaBarWorld{ position:absolute; width:44px; height:4px; background:var(--paper); border:1.5px solid var(--ink); box-shadow:0 0 4px rgba(0,0,0,0.8); z-index:4; display:none; }
  #staminaBarWorld .fill{ position:absolute; top:0; left:0; bottom:0; background:var(--sprint); color:var(--sprint); box-shadow:0 0 4px 1px currentColor; }
  #staminaBarWorld .fill.overheated{ background:var(--heat); color:var(--heat); animation:pulse 0.4s ease-in-out infinite; }

  /* VOLT heat bar / CHARGE hold bar — vertical siblings of the horizontal
     stamina/heat bar above (same 32×4px footprint, just rotated: 4 wide,
     32 tall). Positioned each frame in hud.js's renderHUD() on whichever
     side the CURSOR IS NOT ON (left/right of player, never above — stays
     clear of the horizontal bar's zone by construction, not by accident:
     that one sits ABOVE the player, these sit BESIDE it). Fill grows
     bottom-up (bottom:0, height set in JS), not left-to-right like the
     horizontal one. Volt gets a blue fill + the same pulse animation the
     horizontal bar's overheat state already uses, applied here when
     THIS weapon's own heat maxes out — never switches to red, stays
     blue always (see stepWeaponHeat()/combat.js). Charge gets a plain
     green fill, no overheat state exists for it at all. */
  #voltHeatBar, #chargeHoldBar{ position:absolute; width:4px; height:44px; background:var(--paper); border:1.5px solid var(--ink); box-shadow:0 0 4px rgba(0,0,0,0.8); z-index:4; display:none; }
  #voltHeatBar .fill, #chargeHoldBar .fill{ position:absolute; left:0; right:0; bottom:0; }
  #voltHeatBar .fill{ background:var(--shield); color:var(--shield); box-shadow:0 0 4px 1px currentColor; }
  #voltHeatBar .fill.overheated{ animation:pulse 0.4s ease-in-out infinite; background:var(--heat); color:var(--heat); }
  #chargeHoldBar .fill{ background:var(--green); color:var(--green); box-shadow:0 0 4px 1px currentColor; }
  #grid{ font-size:14px; line-height:14px; white-space:pre; }
  .c-wall{ color:#213b32; }
  .c-floor{ color:var(--ink-faint); }
  .c-zone-off{ color:#7fb8c2; }
  .c-zone-on{ color:var(--cyan); animation:pulse 1.1s ease-in-out infinite; }
  .c-player{ color:#000; font-weight:700; }
  .enemyBox{ position:absolute; width:16px; height:24px; margin:-12px 0 0 -8px; z-index:3; }
  .enemyBox.c-enemy{ background:var(--red); opacity:0.6; }
  .enemyBox.c-enemy-alert{ background:#e05555; opacity:1; }
  .c-bullet-p{ background:linear-gradient(90deg, transparent, #fff 40%, #fff 100%); box-shadow:0 0 4px 1px rgba(255,255,255,0.8); }
  .slideDustPuff{ position:absolute; width:8px; height:8px; margin:-4px 0 0 -4px; border-radius:50%; background:var(--ink-soft); pointer-events:none; }
  .c-bullet-e{ background:linear-gradient(90deg, transparent, var(--red) 40%, var(--red) 100%); box-shadow:0 0 4px 1px rgba(224,41,79,0.7); }
  .c-loot-gold{ color:var(--amber); }
  .c-loot-knife{ color:var(--ink-soft); }
  .c-loot-med{ color:var(--green); }
  .c-loot-weapon{ color:var(--violet); }
  .c-loot-ammo{ color:var(--orange); }
  /* Crate world object — FIXED pixel box (28x42), deliberately not derived
     from worldSize tile footprint or CELL_W/CELL_H tile-to-px scale. Only
     its center position (left/top, set per-frame) uses the tile scale;
     its SIZE never does — and now that size is per-CRATE-TYPE (boxSizePx),
     not a single shared constant, so width/height/background/border are
     all set inline per-instance (setupEntityEls) rather than fixed here. */
  .crateBox{ position:absolute; box-sizing:border-box; z-index:3; }
  .crateBoxImg{ position:absolute; inset:0; width:100%; height:100%; object-fit:contain; pointer-events:none; }
  .crateBoxGlyph{ position:absolute; inset:0; display:flex; align-items:center; justify-content:center;
    font-weight:700; pointer-events:none; }
  /* Locked-crate diagonal hazard stripes — an OVERLAY on top of the crate's
     own color/image/glyph (not a replacement), shown/hidden per-frame based
     on LOCK_STRIPE_VISIBLE_RANGE/LOCK_STRIPE_RANGE_ENABLED (crates-data.js)
     and whether this specific instance is still locked (needsKey()). */
  .crateLockStripes{ position:absolute; inset:0; pointer-events:none; display:none;
    background:repeating-linear-gradient(45deg, rgba(185,28,28,0.55) 0px, rgba(185,28,28,0.55) 4px,
      transparent 4px, transparent 9px); }
  .crateLockStripes.show{ display:block; }
  /* Enemy loot bag world object — FIXED pixel box, randomized per-instance
     between 14x21 and 21x14 (1x1.5 / 1.5x1 tiles, enemy-loot.js). Same
     absolute-positioned/centered-via-margin convention crateBox uses.
     Always shows the same shared UESC.png icon — no per-enemy-type art. */
  .lootBagBox{ position:absolute; box-sizing:border-box; z-index:3; }
  .lootBagImg{ position:absolute; inset:0; width:100%; height:100%; object-fit:contain; pointer-events:none; }
  /* Enemy shield visual — a sibling overlay on the enemy's own box, not a
     replacement of it. THREE distinct cues, not one reused for all three:
       - idle shimmer: always-on while shield hp>0, opacity scaled to
         hp/maxHp (so it fades toward nothing as the shield depletes, and
         fades back IN as regen refills it — same shield reappearing, not
         a new one), colored per tier (white/green/blue/purple/gold).
       - hitFlash: brief brightness pulse, shield absorbed a hit but stayed up.
       - breakFlash: bigger, longer pulse — ONLY the instant hp crosses to 0.
     Sizing/position matches enemyBox's own box+margin convention, just a
     few px larger so it reads as a halo around the enemy, not on top of it. */
  .enemyShieldGlow{ position:absolute; width:22px; height:30px; margin:-15px 0 0 -11px;
    border-radius:4px; pointer-events:none; z-index:2;
    transition:opacity 0.2s linear; }
  .enemyShieldGlow.hitFlash{ animation:shieldHitPulse 150ms ease-out; }
  .enemyShieldGlow.breakFlash{ animation:shieldBreakPulse 450ms ease-out; }
  @keyframes shieldHitPulse{ from{ filter:brightness(2.4); } to{ filter:brightness(1); } }
  @keyframes shieldBreakPulse{ 0%{ filter:brightness(3); transform:scale(1.35); opacity:1; } 100%{ filter:brightness(1); transform:scale(1); opacity:0; } }
  /* E-hold interact popup — radial (conic-gradient) fill ring encircling an
     "E" glyph, anchored over the target. Fill % is set inline per frame
     from state.interactHold.ms / INTERACT_HOLD_MS (interact.js). */
  #crateInteractPopup{ position:absolute; width:30px; height:30px; margin:-36px 0 0 -15px;
    z-index:7; display:none; pointer-events:none; }
  #crateInteractRing{ width:100%; height:100%; border-radius:50%;
    background:conic-gradient(var(--blue) 0deg, rgba(28,33,30,0.15) 0deg);
    display:flex; align-items:center; justify-content:center; }
  #crateInteractInner{ width:22px; height:22px; border-radius:50%; background:var(--paper);
    display:flex; align-items:center; justify-content:center; font-weight:700; font-size:12px; color:var(--ink); }
  /* Lock-prompt popup — shown INSTEAD of the E-ring when the nearest
     target is locked and the player doesn't have its key (no hold is
     possible, so no progress ring makes sense). Same anchor/sizing
     convention as the E-hold popup. */
  #lockPromptPopup{ position:absolute; width:30px; height:30px; margin:-36px 0 0 -15px;
    z-index:7; display:none; pointer-events:none; }
  #lockPromptIcon{ width:100%; height:100%; border-radius:50%; background:rgba(185,28,28,0.85);
    display:flex; align-items:center; justify-content:center; font-size:14px; color:var(--ink); }
  /* Small text callouts above either popup — "Use [Key]" (shown the whole
     hold, if the E-ring's target is a locked crate the player CAN open)
     or "Requires [Key]" (a 2s callout after tapping E on a locked target
     with no key — state.lockDeniedMsLeft, controls.js). */
  #interactLabel, #lockDeniedLabel{ position:absolute; transform:translate(-50%,-100%);
    z-index:7; display:none; pointer-events:none; font-size:10px; font-weight:700;
    white-space:nowrap; background:var(--paper); padding:1px 5px; border:1px solid var(--line); }
  #lockDeniedLabel{ color:var(--red); border-color:var(--red); }
  /* Crate inventory column — grid-template-columns is set inline per-open
     (varies by crate type's invSize), unlike bpGrid's fixed CSS. */
  .crateGrid{ display:grid; grid-auto-rows:var(--inv-cell); gap:var(--inv-gap); }
  #crateColEl{ border-left:1px solid var(--line); padding-left:20px; }
  @keyframes pulse{ 0%,100%{opacity:0.5;} 50%{opacity:1;} }

  /* ============================================================= */
  /* MENU SCREENS — Home / Prepare / Vault. Full-screen, opaque,    */
  /* sit above everything (raid canvas keeps rendering underneath   */
  /* but is fully covered while these are shown). Toggled via       */
  /* renderMenus() based on state.phase — see core script.          */
  /* ============================================================= */
  .menuScreen{ position:fixed; inset:0; background:var(--paper); z-index:20;
    display:none; flex-direction:column; font-family:inherit; color:var(--ink); }
  .menuScreen::after, #overlay::before, #inventoryPanel::before{
    content:""; position:absolute; inset:0; pointer-events:none;
    background:repeating-linear-gradient(to bottom, rgba(238,241,234,0.02) 0px, rgba(238,241,234,0.02) 1px, transparent 1px, transparent 3px);
  }
  .menuTitle{ font-size:14px; letter-spacing:3px; font-weight:700; text-align:center;
    padding:16px 0; border-bottom:1px solid var(--ink); position:relative; }
  .menuTitle::before, .menuTitle::after{
    content:""; position:absolute; bottom:-1px; width:1px; height:9px; background:var(--ink); }
  .menuTitle::before{ left:0; } .menuTitle::after{ right:0; }

  /* HOME */
  #homeBtnGrid{ flex:1; display:flex; flex-direction:column; align-items:center;
    justify-content:center; gap:10px; }
  .homeBtn{ width:220px; padding:12px; font-family:inherit; font-size:14px; letter-spacing:1px;
    text-transform:uppercase; background:transparent; border:1px solid var(--ink); color:var(--ink);
    cursor:pointer; }
  .homeBtn:hover{ background:var(--ink); color:var(--paper); }
  .homeBtn.disabled{ border-color:var(--ink-faint); color:var(--ink-faint); cursor:not-allowed; }
  .homeBtn.disabled:hover{ background:transparent; color:var(--ink-faint); }

  /* AUTH — the login/signup gate. Whole game sits behind this now
     (confirmed direction: required, not optional) — it's the first
     thing shown on boot whenever there's no existing Supabase session,
     and after a real sign-out. Same .menuScreen shell every other
     full-screen menu uses, just with a form instead of buttons. */
  #authFormWrap{ flex:1; display:flex; flex-direction:column; align-items:center;
    justify-content:center; gap:10px; }
  #authFormWrap input{ width:260px; padding:11px 12px; font-family:inherit; font-size:13px;
    background:transparent; border:1px solid var(--line); color:var(--ink); }
  #authFormWrap input:focus{ outline:none; border-color:var(--ink); }
  #authFormWrap input::placeholder{ color:var(--ink-faint); }
  #authSubmitBtn{ width:260px; padding:12px; font-family:inherit; font-size:14px; letter-spacing:1px;
    text-transform:uppercase; background:var(--ink); color:var(--paper); border:1px solid var(--ink);
    cursor:pointer; margin-top:4px; }
  #authSubmitBtn:hover{ opacity:0.85; }
  #authSubmitBtn:disabled{ opacity:0.4; cursor:not-allowed; }
  #authToggleModeBtn{ background:transparent; border:none; color:var(--ink-soft); font-family:inherit;
    font-size:12px; text-decoration:underline; cursor:pointer; margin-top:6px; }
  #authToggleModeBtn:hover{ color:var(--ink); }
  #authErrorText{ color:var(--red); font-size:12px; max-width:260px; text-align:center; min-height:14px; }

  /* Top banner — was shared across Prepare/Vault/Factions; only Vault's
     (Loadout's) is left now, both other screens retired into it. */
  #vaultTopBanner{ display:flex; align-items:center; gap:0; height:44px;
    padding:0 14px; border-bottom:1px solid var(--ink); flex-shrink:0; }
  #vaultTopBanner .brand{ font-weight:700; letter-spacing:2px; font-size:13px; margin-right:24px; }
  .navBtn{ font-size:11px; letter-spacing:1.5px; text-transform:uppercase; color:var(--ink-soft);
    padding:8px 16px; border:1px solid transparent; cursor:pointer; background:none; font-family:inherit; }
  .navBtn.active{ color:var(--ink); border:1px solid var(--line); background:var(--paper-dim); }
  .navBtn.disabled{ color:var(--ink-faint); cursor:not-allowed; }
  #vaultTopBanner .spacer{ flex:1; }

  /* FACTIONS — honest static mock, same convention World/Class already
     follow (no faction/contract/vendor/upgrade/reputation data model
     exists yet). Was its own standalone screen; retired and folded in
     here as a column, same as Prepare was. The tab row/expand
     interaction IS real, working JS (factions-ui.js) — only the faction
     data itself is fake. Icon-only collapsed tabs sit in a row; clicking
     one expands it to show the full name and swaps the panel below to
     that faction's Contracts/Vendor/Upgrades content — one faction open
     at a time, same accordion principle the editor's POI list uses.
     Per-faction colors (see FACTIONS.md) extend the existing dark-theme
     accent palette rather than introducing new hexes — each icon always
     carries its faction's color (even collapsed, so the row itself
     reads as color-coded at a glance). The color now extends to the
     WHOLE expanded folder, not just the tab — factions-ui.js mirrors
     the tab's faction-<key> class onto #factionPanel on click, so the
     panel picks up the same per-faction rules below: outline in the
     full color, background as a low-opacity (10%) tint of that same
     color — composited over the dark theme's near-black surface, that
     tint reads as a genuinely darker SHADE of the outline color, not a
     separate lighter tone next to it. */
  .vFactionsCol{ display:flex; flex-direction:column; }
  .factionTabRow{ display:flex; gap:4px; margin-bottom:1px; }
  .factionTab{ display:flex; align-items:center; gap:8px; height:40px; padding:0 12px;
    border:1px dashed var(--ink-faint); background:transparent; cursor:pointer;
    font-family:inherit; color:var(--ink-soft); flex-shrink:0; overflow:hidden; white-space:nowrap; }
  .factionTab-expanded{ border-style:solid; background:rgba(255,255,255,0.04); color:var(--ink); flex:1; }
  .factionTabIcon{ font-size:16px; line-height:1; flex-shrink:0; display:flex; }
  .factionTabIcon i{ color:var(--ink-faint); }
  .factionTabName{ font-size:11px; display:none; }
  .factionTab-expanded .factionTabName{ display:inline; }
  /* Each faction: tab icon color (always) + tab/panel outline+fill (only
     while that faction's faction-<key> class sits on the expanded tab
     AND, mirrored by JS, on #factionPanel). .factionPanel's own border/
     background (below) are the fallback for the sliver of time before
     JS runs, or if a faction somehow has no color rule. */
  .faction-ledger .factionTabIcon i{ color:var(--amber); }
  .faction-ledger.factionTab-expanded{ border-color:var(--amber); }
  .faction-ledger.factionPanel{ border-color:var(--amber); background:rgba(255,201,51,0.10); }
  .faction-aegis .factionTabIcon i{ color:var(--cyan); }
  .faction-aegis.factionTab-expanded{ border-color:var(--cyan); }
  .faction-aegis.factionPanel{ border-color:var(--cyan); background:rgba(223,233,236,0.10); }
  .faction-graft .factionTabIcon i{ color:var(--violet); }
  .faction-graft.factionTab-expanded{ border-color:var(--violet); }
  .faction-graft.factionPanel{ border-color:var(--violet); background:rgba(139,92,246,0.10); }
  .faction-kilnwatch .factionTabIcon i{ color:var(--orange); }
  .faction-kilnwatch.factionTab-expanded{ border-color:var(--orange); }
  .faction-kilnwatch.factionPanel{ border-color:var(--orange); background:rgba(251,146,60,0.10); }
  .faction-wolfpack .factionTabIcon i{ color:var(--red); }
  .faction-wolfpack.factionTab-expanded{ border-color:var(--red); }
  .faction-wolfpack.factionPanel{ border-color:var(--red); background:rgba(226,67,47,0.10); }
  .faction-synth .factionTabIcon i{ color:var(--blue); }
  .faction-synth.factionTab-expanded{ border-color:var(--blue); }
  .faction-synth.factionPanel{ border-color:var(--blue); background:rgba(76,90,95,0.10); }

  /* Contracts — vertical "bounty posting" cards, one .factionContractsGrid
     per faction (populated by factions-ui.js's renderFactionContracts()).
     Card border color is faction-scoped the SAME way the tab/panel colors
     above already are (.faction-<key> ancestor class on #factionPanel,
     set by selectFaction()) — no separate color system to keep in sync. */
  .factionContractsGrid{ display:flex; gap:13px; flex-wrap:wrap; margin:10px 0 21px; }
  .contractCard{ width:224px; background:rgba(255,255,255,0.03); border:1px solid var(--line);
    clip-path:polygon(10px 0,100% 0,100% calc(100% - 10px),calc(100% - 10px) 100%,0 100%,0 10px);
    padding:16px; display:flex; flex-direction:column; gap:10px; font-size:14px; }
  .contractCardEmpty{ color:var(--ink-faint); align-items:center; justify-content:center; text-align:center; }
  .faction-ledger .contractCard{ border-color:var(--amber); background:rgba(255,201,51,0.05); }
  .faction-aegis .contractCard{ border-color:var(--cyan); background:rgba(223,233,236,0.04); }
  .faction-graft .contractCard{ border-color:var(--violet); background:rgba(139,92,246,0.05); }
  .faction-kilnwatch .contractCard{ border-color:var(--orange); background:rgba(251,146,60,0.05); }
  .faction-wolfpack .contractCard{ border-color:var(--red); background:rgba(226,67,47,0.05); }
  .faction-synth .contractCard{ border-color:var(--blue); background:rgba(76,90,95,0.08); }
  .contractCardTop{ display:flex; justify-content:space-between; align-items:center; }
  .contractKindBadge{ font-size:12px; letter-spacing:1px; color:var(--ink-soft); border:1px solid var(--line); padding:3px 8px; }
  /* Kind badge is deliberately faction-INDEPENDENT — a STORY contract can
     belong to any faction, so it can't reuse a faction hue without
     implying ownership by whichever faction happens to share that color
     (confirmed fix: STORY no longer reuses Ledger's amber). Inverted
     neutral fill reads as "elevated" without borrowing any of the 6
     faction accents. SPECIAL intentionally left unstyled here — no event
     content exists yet to design against (see contracts-data.js). */
  .contractKindBadge.kind-story{ color:var(--paper); background:var(--ink); border-color:var(--ink); }
  .contractAcceptedTag{ font-size:12px; letter-spacing:1px; color:var(--safe); }
  .contractName{ color:var(--ink); font-size:17px; font-weight:700; line-height:1.3; }
  .contractDesc{ color:var(--ink-soft); line-height:1.5; }
  .contractBoons{ display:flex; flex-wrap:wrap; gap:5px; }
  .contractBoon{ font-size:12px; padding:3px 7px; border:1px solid var(--rarity-uncommon); color:var(--rarity-uncommon); }
  .contractRewards{ border-top:1px solid var(--line); padding-top:10px; color:var(--ink-soft); display:flex; flex-direction:column; gap:4px; }
  .contractRewardCr{ color:var(--amber); }
  .contractActionBtn{ margin-top:auto; background:transparent; border:1px solid var(--ink); color:var(--ink);
    font-family:inherit; font-size:13px; letter-spacing:1px; text-transform:uppercase; padding:8px; cursor:pointer; }
  .contractActionBtn.abandon{ border-color:var(--red); color:var(--red); }
  .contractRerollRow{ display:flex; justify-content:space-between; align-items:center; font-size:12px; color:var(--ink-faint); }
  .contractRerollBtn{ background:transparent; border:none; color:var(--ink-soft); font-family:inherit; font-size:12px; text-decoration:underline; cursor:pointer; padding:0; }

  /* Turn-in lifecycle — objective-met badge/button, and the fully
     turned-in card (reward grid + countdown). Kind badge stays
     faction-independent for the same reason kind-story does (see that
     rule above) — met/turned-in status isn't a "kind", it's orthogonal
     to rotator/story/special. */
  .contractAcceptedTag.contractMetTag{ color:var(--safe); }
  .contractActionBtn.turnin{ border-color:var(--safe); color:var(--safe); }
  .contractKindBadge.kind-turnedin{ color:#0e0e0e; background:var(--safe); border-color:var(--safe); }
  .contractRewardGrid{ display:grid; grid-template-columns:repeat(4,44px); grid-auto-rows:44px; gap:5px; }
  .contractRewardSlot{ width:44px; height:44px; position:relative; overflow:hidden; }
  .contractTurnInTimer{ font-size:12px; color:var(--red); }
  .contractTurnInTimer.contractTurnInUrgent{ color:var(--red); font-weight:700; }

  .factionPanel{ border:1px solid var(--line); background:var(--paper-dim); padding:14px; flex:1; overflow-y:auto; }
  .factionStanding{ font-size:10px; color:var(--ink-soft); }
  .factionSectionLabel{ font-size:10px; letter-spacing:2px; color:var(--ink-soft); text-transform:uppercase;
    border-bottom:1px solid var(--line); padding-bottom:6px; margin:14px 0 8px; }
  .factionSectionLabel:first-of-type{ margin-top:14px; }
  .factionListEmpty{ font-size:11px; color:var(--ink-faint); font-style:italic; line-height:1.5; }

  /* World/Class + player bar — now the FIRST column of Vault's own
     #vaultMain grid (was a separate #prepMain layout on its own screen).
     #prepMain/.prepCol/.prepColMiddle are gone — no separate grid needed,
     this content just flows inside the shared .vCol wrapper now, same as
     Equipped/Backpack does in the next column over. */
  .sectionLabel{ font-size:10px; letter-spacing:2px; color:var(--ink-soft); text-transform:uppercase;
    border-bottom:1px solid var(--line); padding-bottom:6px; margin-bottom:10px; }
  /* Header row variant — label + small tag(s) sharing one line (loadout
     totals, free-slot counter) instead of the label alone. */
  .sectionLabelRow{ display:flex; align-items:baseline; gap:10px;
    border-bottom:1px solid var(--line); padding-bottom:6px; margin-bottom:10px; }
  .sectionLabelRow .sectionLabel{ border-bottom:none; padding-bottom:0; margin-bottom:0; }
  .vTotalTag{ font-size:11px; color:var(--ink-soft); font-weight:600; }
  .vFreeCounter{ font-size:11px; color:var(--ink-faint); margin-left:auto; }
  /* World cards — restyled to match Vault's existing dashed-outline
     convention (.emptySlot/.itemArt.slotEmpty) instead of the old
     heavier solid-border + decorative corner-bracket treatment, since
     this content now lives on the same screen as the rest of Vault and
     should read as one cohesive design, not two stitched together.
     Unselected/locked = dashed, same as any other empty equip slot;
     selected = solid cyan border + faint tint, same accent color the
     timer/extraction UI already uses elsewhere. */
  .prepCard{ border:1px dashed var(--ink-faint); background:transparent; padding:10px; margin-bottom:8px; }
  .prepCard-selected{ border:1px solid var(--cyan); background:rgba(223,233,236,0.06); padding:10px; margin-bottom:8px; }
  .prepName{ font-size:12px; font-weight:700; }
  .prepMeta{ font-size:10px; color:var(--ink-soft); margin-top:2px; }
  .worldThumb{ width:100%; height:52px; border:1px dashed var(--ink-faint); background:transparent;
    display:flex; align-items:center; justify-content:center; font-size:10px; color:var(--ink-faint); margin-bottom:8px; }
  #prepBottomBar{ display:flex; align-items:center; justify-content:space-between; gap:10px;
    height:56px; padding:0 16px; border-top:1px solid var(--ink); flex-shrink:0; }
  .devToolLabel{ font-size:10px; color:var(--amber); }
  #loadWorldBtn{ font-size:11px; letter-spacing:1px; text-transform:uppercase; padding:10px 18px;
    border:1px solid var(--ink); background:var(--ink); color:var(--paper); cursor:pointer; }

  /* Player identity bar — class icon + full name + level. Name is still
     a hardcoded placeholder (no save/profile system exists yet); level,
     its progress bar, and the CR balance are real now, computed from the
     persistent vault (vault-store.js). */
  .prepPlayerBar{ display:flex; align-items:center; gap:14px; border:1px solid var(--line);
    background:var(--paper-dim); padding:12px 16px; }
  .prepPlayerIcon{ width:48px; height:48px; border:1px dashed var(--ink-faint); background:transparent;
    display:flex; align-items:center; justify-content:center; font-size:11px; font-weight:700;
    color:var(--ink); flex-shrink:0; }
  .prepPlayerInfo{ flex:1; display:flex; flex-direction:column; gap:6px; min-width:0; }
  .prepPlayerTopRow{ display:flex; align-items:baseline; justify-content:space-between; gap:10px; }
  .prepPlayerName{ font-size:16px; font-weight:700; }
  .prepPlayerCredits{ font-size:13px; color:var(--amber); font-weight:700; flex-shrink:0; }
  .prepPlayerLevelRow{ display:flex; align-items:center; gap:8px; }
  .prepPlayerLevel{ font-size:12px; color:var(--ink-soft); flex-shrink:0; }
  .prepXpBarTrack{ flex:1; height:6px; background:var(--paper); border:1px solid var(--line); overflow:hidden; }
  .prepXpBarFill{ height:100%; background:var(--amber); box-shadow:0 0 4px 1px rgba(255,201,51,0.5); }
  .prepXpBarText{ font-size:10px; color:var(--ink-faint); flex-shrink:0; white-space:nowrap; }

  /* Class selection — tiled cards with a monochrome glyph icon. Selection
     is still static/non-interactive (no real class system exists yet —
     see prepare-ui.js header note). Same dashed/cyan restyle as World's
     cards above, for the same reason. */
  .prepClassGrid{ display:grid; grid-template-columns:repeat(auto-fill, minmax(150px,1fr));
    gap:12px; align-content:start; }
  .prepClassCard{ border:1px dashed var(--ink-faint); background:transparent; padding:16px; text-align:center; }
  .prepClassCard-selected{ border:1px solid var(--cyan); background:rgba(223,233,236,0.06); padding:16px; text-align:center; }
  .prepClassIcon{ width:56px; height:56px; margin:0 auto 10px; border:1px dashed var(--ink-faint);
    background:transparent; display:flex; align-items:center; justify-content:center;
    font-size:13px; font-weight:700; color:var(--ink); }
  .prepClassName{ font-size:13px; font-weight:700; }
  .prepClassMeta{ font-size:10px; color:var(--ink-soft); margin-top:4px; }

  /* VAULT (LOADOUT) — real, persistent, interactive now
     (vault-ui.js/vault-store.js), not a static mock. FOUR columns now:
     World/Class + player bar (from the old standalone Prepare screen,
     retired), Factions (from the old standalone Factions screen, also
     retired — icon-tab row + expandable panel, factions-ui.js), Equipped
     +Backpack SIDE BY SIDE, and Vault storage.
     Equipped+Backpack AND Vault storage are both CONTENT-SIZED now
     (`max-content` / `calc(...)`), not flexible fr shares — this pushes
     Equipped+Backpack flush against Vault storage (was a 1.4fr share
     with slack space between it and the right edge's own content-sized
     column), which in turn frees that entire share for Factions, whose
     expanded panel actually benefits from the room (three real sections
     — Contracts/Vendor/Upgrades — vs. Equipped+Backpack's fixed, already-
     measured content width).
     Equipped+Backpack now uses its OWN cell size, --equip-cell
     (renderVaultScreen(), vault-ui.js) — a deliberate 1.2x multiple of
     Vault storage's --vault-cell, not the same value. This is an
     intentional, explicit split: one fixed ratio applied uniformly to
     Equipped tiles, weapon boxes, attach cells, AND Backpack's grid
     together, not the old problem of two sizes drifting apart from
     independent runtime measurements.
     World/Class : Factions = 0.8fr : 2.4fr — Factions picks up the
     share Equipped+Backpack no longer needs. Starting point, not
     measured — flag if it needs tuning once it's live. */
  #vaultMain{ flex:1; display:grid; grid-template-columns:0.8fr 2.4fr max-content calc(8 * var(--vault-cell) + 7 * var(--vault-gap) + 52px); gap:1px;
    background:var(--line); overflow:hidden;
    --vault-cell: 44px; --equip-cell: 52.8px; --vault-gap: 4px; }
  .vCol{ background:var(--paper); overflow-y:auto; overflow-x:hidden; min-width:0; padding:26px; }

  /* Equipped weapons — art box + real drag/drop attach cells (BUG FIX:
     used to be read-only here, same visual language as the in-raid
     Inventory overlay's weaponSlotRow, reused directly
     (weaponAttachCellsHtml() in hud.js). Weapon box width/height set
     inline per-render (renderVaultScreen()) off --vault-cell, not fixed. */
  .vWeaponRow{ display:flex; align-items:center; gap:8px; margin-bottom:10px; }
  /* Overrides the in-raid overlay's fixed --inv-cell sizing for
     weaponAttachCellsHtml()'s markup, ONLY here — --vault-attach-cell is
     computed fresh each render (vault-ui.js) to stay proportional to the
     Vault screen's own (now smaller) weapon box, since --inv-cell
     itself has no relationship to Vault's dynamic scaling at all.
     BUG FIX: this used to be `.vReadOnly`, which ALSO forced cursor:default
     on every cell/draggable and was never wired with any drag/drop
     handlers at all — equipping an attachment onto a Vault-equipped weapon
     was silently impossible. Split the sizing override (kept here) from
     the interactivity lockout (removed) — cursor:grab on occupied cells
     already comes from the base `.attachCell.attach-tier-*` rules below. */
  .vAttachScaled.weaponAttachGrid{ grid-template-columns:repeat(2, var(--vault-attach-cell, var(--inv-cell)));
    grid-auto-rows:var(--vault-attach-cell, var(--inv-cell)); gap:var(--vault-gap); }
  .vAttachScaled .attachCell{ font-size:8px; }

  /* Shield/Equipment/Cores/Pack — uniform compact tiles, same
     "2x1" shape convention the in-raid overlay uses, sized off the
     shared --vault-cell so everything on this whole screen reads as one
     consistent scale, Backpack's grid included. */
  .vEquipTile{ width:calc(2 * var(--equip-cell) + var(--vault-gap)); height:var(--equip-cell); }
  /* Pack — real 2x2 now, matching the pack item's actual invSize
     (was the same compact 2x1 shape as Shield/Equipment/Cores). Width
     stays the same 2-cell width; only height doubles. */
  .vEquipTile.vPackTile{ height:calc(2 * var(--equip-cell) + var(--vault-gap)); }
  /* Left column — World/Class + player bar. Flex column so .vClassBottom
     (margin-top:auto) can pin Class to the bottom edge instead of it
     sitting directly under World with empty space below. */
  .vLeftCol{ display:flex; flex-direction:column; }
  .vClassBottom{ margin-top:auto; }
  /* Middle column split — Equipped and Backpack side by side (was
     stacked, Backpack below Equipped). vMiddleCol overrides the normal
     .vCol block layout with a row flex of two sub-columns; each vSubCol
     keeps its own natural content width instead of stretching evenly,
     so Equipped's narrower rows and Backpack's grid don't force each
     other to an artificial equal half. */
  .vMiddleCol{ display:flex; gap:24px; align-items:flex-start; }
  .vSubCol{ flex:0 0 auto; }
  .vGearRow{ display:flex; gap:20px; align-items:flex-start; }
  .vGearGroup{ display:flex; flex-direction:column; gap:6px; }
  .vGearLabel{ font-size:11px; letter-spacing:1.5px; color:var(--ink-soft); text-transform:uppercase; }
  /* Cores — was accidentally left on --vault-gap (4px), out of step with
     Shield/Equipment's 20px .vGearRow spacing right above it. Matches now. */
  .vCoresRow{ display:flex; gap:20px; margin-top:6px; }

  /* On-hand Backpack + real Vault storage grids — both now share the
     SAME --vault-cell size (was each computing its own, Backpack's
     landing ~2x bigger since it has half as many columns in a
     similarly-wide column). Backpack naturally renders narrower than
     its column now, since 4 cols at --vault-cell size takes less room
     than 8 — that's expected, not a bug; the merged column isn't sized
     to force it edge-to-edge anymore. */
  .vGrid{ display:grid; gap:var(--vault-gap); }
  #vBpGrid{ grid-template-columns:repeat(4, var(--pack-cell)); grid-auto-rows:var(--pack-cell); }
  .vVaultGrid{ grid-template-columns:repeat(8, var(--vault-cell)); grid-auto-rows:var(--vault-cell); }

  /* Search box — dims (via .vFilteredOut below), never removes/repositions
     real items, so the auto-sorted contiguous layout never changes from
     typing a query. */
  .vSearchBox{ width:100%; box-sizing:border-box; padding:6px 9px; margin-bottom:10px;
    border:1px solid var(--line); background:var(--paper); font-size:12px; color:var(--ink);
    font-family:inherit; }
  .vSearchBox:focus{ outline:none; border-color:var(--ink-soft); }
  .vFilteredOut{ opacity:0.15; pointer-events:none; }

  /* Item-detail hover window — shows on hover, hides on mouseout
     (vault-ui.js's setupVaultDetailHover()), same pattern as the smaller
     stat tooltip it replaced. pointer-events:none so the panel itself
     can never trigger a mouseout on the item it's anchored next to. */
  .vDetailPanel{ position:fixed; z-index:20; padding:14px; border:1px solid var(--line);
    background:var(--paper-dim); width:260px; box-shadow:0 2px 10px rgba(0,0,0,0.12);
    pointer-events:none; }
  .vDetailArt{ display:flex; align-items:center; justify-content:center; margin:0 auto 8px;
    background:var(--paper); border:1px solid var(--ink-faint); }
  .vDetailName{ font-size:14px; font-weight:700; margin-bottom:4px; }
  .vDetailMeta{ font-size:11px; color:var(--ink-soft); margin-bottom:6px; }
  .vDetailStat{ font-size:11px; color:var(--ink); }
  /* Real slotted attachments (vault-ui.js's vaultDetailAttachmentsText())
     — a step down from .vDetailStat visually (softer color, small gap
     above) since it's supplementary info under the main stat line, not
     the primary fact being shown. Hidden via inline style, not display:
     none here, so JS can toggle it per-item (bare weapons/non-weapons
     have nothing to show). Shared by #vDetailCompat too (the reverse
     line — which weapons an attachment fits — same supplementary role,
     never visible at the same time as this one since they're gated on
     opposite item categories). */
  .vDetailAttachments{ font-size:11px; color:var(--ink-soft); margin-top:6px;
    padding-top:6px; border-top:1px solid var(--ink-faint); line-height:1.5; }
  /* The bolded name inside #vDetailCompat — the one weapon (of possibly
     several listed) that's both currently equipped and has room for
     this attachment right now (vaultDetailCompatibilityHtml()). Same
     accent color the drag-time compatibility highlight uses
     (.attachCompatTarget/.attachSlotTarget above), so "this is a real
     option for you right now" reads as one consistent signal across the
     whole feature. */
  .vDetailCompatEquipped{ color:var(--safe); font-weight:700; }

  /* Engine error banner — surfaces the actual JS error directly on screen
     so a crash during play is visible without needing devtools open. */
  #engineErrorBanner{ position:fixed; top:0; left:0; right:0; z-index:999; display:none;
    background:var(--red); color:#fff; font-family:inherit; font-size:12px; padding:8px 14px;
    white-space:pre-wrap; word-break:break-word; max-height:40vh; overflow-y:auto; }

  /* BUG FIX (pre-existing, not introduced by shield-mod work): top:50%;
     left:50% alone anchors this box's TOP-LEFT CORNER at the player's
     true center — transform-origin:0% 50% then computes its pivot as
     50% of THIS box's own height measured from THAT corner, landing
     1.25px (half of height:2.5px) below the true center. The old fix
     attempt (a -1.25px Y-translate in render.js's transform) only
     masked this at angle 0 — translate() inside `transform` shifts
     content, it doesn't move where transform-origin itself computes to,
     so at any other rotation angle the dash swung around the wrong
     pivot. margin-top:-1.25px (same trick #playerBox already uses
     correctly, above) makes the box symmetric around the anchor BEFORE
     any transform runs, so transform-origin:0% 50% now lands exactly on
     the true center at every angle. render.js's translate dropped its
     Y-component accordingly — it's no longer needed or correct. */
  .gunDash{ position:absolute; top:50%; left:50%; height:2.5px; margin-top:-1.25px; background:var(--ink); box-shadow:0 0 2px rgba(0,0,0,0.5); pointer-events:none; transform-origin:0% 50%; z-index:3; }

  /* Shield-mod (attachment) — wraps around the player as an open stroke
     arc facing the aim direction, not a filled plate at the muzzle (the
     earlier div/border-radius version — CSS can only fill shapes with
     border-radius, it can't produce a clean open arc, hence the switch
     to real SVG, same geometry the original design sketch used).
     Symmetric viewBox (-18 -18 36 36) means local (0,0) IS the SVG's own
     visual center, so simple margin-centering (matching #playerBox's own
     -half-width/-half-height trick) puts that center exactly on the
     true player position — no transform-origin override needed, unlike
     .gunDash above (default 50% 50% is already correct here). The path
     itself (ext-shoot-game.html) arcs from local (0,-14) to (0,14)
     through (14,0) — flat/open side faces local -x (backward, away from
     aim), curved side bulges toward local +x (outward along aim) —
     rotating the whole SVG by state.aimAngle (render.js) is the only
     per-frame change needed; the path's own `d` never changes. Hidden
     entirely (display:none, set in render.js) whenever the active
     weapon has no shield-mod attachment. Active/inactive swaps stroke
     style via class, same as before. */
  .shieldMod{ position:absolute; top:50%; left:50%; width:50px; height:50px; margin:-25px 0 0 -25px;
    pointer-events:none; z-index:3; }
  .shieldMod.smActive path{ stroke:var(--shieldmod); filter:drop-shadow(0 0 3px rgba(245,224,80,0.65)); }
  .shieldMod.smInactive path{ stroke:var(--ink-faint); stroke-dasharray:4 5; opacity:0.55; }

  /* Muzzle flash — single reusable element (not pooled, same as gunDash1/2)
     since only the player has a visible barrel to flash from. Outer
     (#muzzleFlash) is positioned/rotated via inline transform set once per
     shot (tryFire(), combat.js), same rotate-then-translate-along-local-axis
     trick gunDash uses. Inner (#muzzleFlashInner) carries the actual visual
     and the scale+opacity pop animation — kept as a separate element on
     purpose so the animation's transform can't clobber the outer's
     JS-set positioning transform (a single element trying to do both
     was the old limitation — animating transform always fully replaces
     whatever was set inline, so scale would have overwritten the
     rotate+translate and broken aim-tracking). Warm halo, not the old
     dark ring — that was tuned for a white world background that no
     longer exists post dark-theme rework; a dark halo on a dark
     background reads as nothing. */
  #muzzleFlash{ position:absolute; top:50%; left:50%; width:0; height:0; pointer-events:none; z-index:4; }
  #muzzleFlashInner{ position:absolute; top:0; left:0; width:8px; height:8px; margin:-4px 0 0 -4px;
    border-radius:50%; background:#fff; opacity:0; }
  #muzzleFlash.show #muzzleFlashInner{ animation:muzzleFlashPop 130ms ease-out; }
  @keyframes muzzleFlashPop{
    0%{ opacity:1; transform:scale(1.8); box-shadow:0 0 16px 6px rgba(255,214,120,0.95); }
    60%{ transform:scale(1); }
    100%{ opacity:0; transform:scale(0.8); box-shadow:0 0 2px 1px rgba(255,214,120,0); }
  }

  #targetIcon{
    position:absolute; width:22px; height:22px; margin:-11px 0 0 -11px;
    border:2.5px solid var(--red); border-radius:50%; opacity:0.9;
    box-shadow:0 0 4px rgba(224,41,79,0.6);
    pointer-events:none; z-index:3;
  }
  #targetIcon::before, #targetIcon::after{ content:""; position:absolute; background:var(--red); }
  #targetIcon::before{ left:50%; top:-7px; width:2px; height:7px; margin-left:-1px; }
  #targetIcon::after{ left:-7px; top:50%; width:7px; height:2px; margin-top:-1px; }

  /* floating HUD panels */
  .hud{ position:absolute; background:rgba(14,14,14,0.85); border:1px solid var(--line); z-index:4; }
  .hudLabel{ font-size:12px; letter-spacing:2px; color:var(--ink-soft); text-transform:uppercase; }
  .hudSub{ font-size:10px; color:#93a396; margin-top:1px; margin-bottom:8px; }

  #timerOverlay{ top:8px; left:50%; transform:translateX(-50%); padding:5.2px 18.2px; text-align:center; border-radius:9.1px; }
  #timerOverlay .v{ font-weight:700; font-size:19.5px; color:var(--cyan); letter-spacing:1px; }

  /* POI banner — "[ NAME ]", top-center, fades in/out via .show. Text
     color is set inline per-POI in hud.js (poi.color, chosen per-region
     in the editor), so no color is hardcoded here.

     Backing panel — same rgba(14,14,14,0.85) treatment the timer uses.
     Without it, since the banner sits at a fixed screen position while
     the world scrolls underneath the player, whatever material happens
     to render at that spot shows through — and since both the POI's
     text color and any material's ground color are arbitrary (picked
     independently in the editor), they can genuinely match and the text
     disappears into the ground. A real backdrop makes contrast
     guaranteed instead of coincidental. */
  #poiBanner{
    position:absolute; top:53px; left:50%; transform:translateX(-50%); z-index:8;
    background:rgba(14,14,14,0.85); border:1px solid var(--line); padding:5px 16px;
    font-size:20.8px; letter-spacing:3px; white-space:nowrap;
    opacity:0; pointer-events:none; transition:opacity 0.4s ease;
  }
  #poiBanner.show{ opacity:1; }
  #poiBanner .br{ color:var(--ink-faint); }

  /* Event feed — up to 3 single-line entries, positioned in JS each frame
     (hud.js's renderHUD) relative to the player/heat bar, not fixed via
     top/left here like the old #killFeed panel. Deliberately small/quiet
     — never wraps (white-space:nowrap + ellipsis as a safety net past
     the 50-char hard cap already enforced in combat.js's pushEventFeed). */
  #eventFeed{ position:absolute; inset:0; pointer-events:none; z-index:5; }
  .efLine{
    position:absolute; transform:translateX(-50%);
    max-width:230px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
    background:rgba(14,14,14,0.85); border:1px solid var(--line); border-left:2px solid var(--cyan);
    font-size:11px; font-weight:600; padding:1px 7px; text-align:center;
  }

  .weaponRow{ display:flex; flex-direction:column; gap:10px; align-items:flex-start; }
  /* Each weapon (or its empty slot) plus its attachment cells sit in one
     row, attachments to the RIGHT of the art — no toggle button/separate
     panel anymore, this is always visible for an equipped weapon. */
  .weaponSlotRow{ display:flex; align-items:center; gap:7px; }
  .emptySlot{ border:1px dashed var(--ink-faint); background:transparent; display:flex; align-items:center; justify-content:center; }

  /* Generic empty-outline placeholder for any slot with nothing equipped
     (base shield, base pack, equipment/cores before those systems
     exist) — dashed border, no fill, no placeholder text. */
  .itemArt.slotEmpty{ border:1px dashed var(--ink-faint); background:transparent; cursor:default; }

  /* Shield/Equipment/Cores equip-slot boxes are all a uniform,
     compact 2-cell-wide x 1-cell-tall rectangle — purely a UI shape for
     the dedicated equip slot; it's independent of the actual item's real
     inventory footprint (a shield is still 4x2 when it's sitting in the
     backpack grid — see items-data.js). The art just fills/crops into
     this smaller box (itemArtHtml's object-fit:fill still applies).
     Built off --inv-cell/--inv-gap so it scales with the rest of the
     Inventory overlay. */
  .shieldSlot{ display:grid; grid-template-columns:repeat(2,var(--inv-cell)); grid-auto-rows:var(--inv-cell); gap:var(--inv-gap); align-self:flex-start; width:calc(2 * var(--inv-cell) + var(--inv-gap)); height:var(--inv-cell); }
  .packSlot{ display:grid; grid-template-columns:repeat(2,var(--inv-cell)); grid-auto-rows:var(--inv-cell); gap:var(--inv-gap); width:calc(2 * var(--inv-cell) + var(--inv-gap)); height:calc(2 * var(--inv-cell) + var(--inv-gap)); align-self:flex-start; }
  /* Equipment (single grenade slot) sits beside Shield in one row. */
  .gearRow{ display:flex; gap:19px; align-items:flex-start; }
  .gearGroup{ display:flex; flex-direction:column; }
  .equipmentSlot{ width:calc(2 * var(--inv-cell) + var(--inv-gap)); height:var(--inv-cell); }
  /* Cores (CORE_SLOT_COUNT) — a flat row of
     empty-outline slots, structure-only until that system exists. Each
     slot is the same 2x1 shape as Shield/Equipment above, side by side. */
  .coreRow{ display:flex; gap:var(--inv-gap); }
  .coreRow .itemArt{ width:calc(2 * var(--inv-cell) + var(--inv-gap)); height:var(--inv-cell); }

  /* Backpack Slot (equipped pack) — moved into the left column, under
     Cores. Real 2x2 shape now, matching the pack item's actual invSize,
     instead of the old compact 2x1 fake shape that existed only to
     align with the grid's top-right corner. The backpack ITEM GRID
     itself stays in the right column, unaffected. */
  .bpGridWrap{ position:relative; }

  /* Cols fixed at 4 — every backpack tier (base + all 4 pack_* items)
     keeps 4 columns, only rows grow (4/6/8/10). Was repeat(6,...) to
     match the old deprecated assault_pack's 6-col shape.
     Whole Inventory overlay scaled by 1.2x per spec (34px -> --inv-cell,
     3px gap -> --inv-gap) — backpack grid, its aligned Backpack Slot, and
     every other equip slot in the overlay all share these two variables. */
  .bpGrid{ display:grid; grid-template-columns:repeat(4,var(--inv-cell)); grid-auto-rows:var(--inv-cell); gap:var(--inv-gap); }
  /* Sits directly below .bpGrid (same column count/cell size, wired up in
     hud.js) — grid-template-columns set inline per-render since cols can
     vary in theory same as bpGrid does, even though today it's always 4. */
  .bpFutureGrid{ display:grid; grid-auto-rows:var(--inv-cell); gap:var(--inv-gap); }
  .bpCell{ background:var(--paper-dim); border:1px solid var(--ink-faint); min-width:0; min-height:0; position:relative; }
  /* "Locked-ahead" preview cells — shown BELOW the real backpack grid
     (buildFutureBackpackGrid() in hud.js), representing rows the player
     doesn't have yet but could unlock with a better pack (up to the
     biggest pack tier's total row count). Dimmed/faded and NOT a
     dropZone — nothing can be placed here yet, this is preview only.
     The "+" lives here now, not on the real usable cells above. */
  .bpCellLocked{ background:var(--paper-dim); border:1px dashed var(--ink-faint); opacity:0.4; min-width:0; min-height:0; position:relative; }
  .bpCellLocked::after{
    content:"+"; position:absolute; inset:0; display:flex; align-items:center; justify-content:center;
    color:var(--ink-faint); font-size:17px; line-height:1; pointer-events:none;
  }
  .itemArt{
    background:var(--paper); border:1px solid var(--rarity-common); display:flex; align-items:center; justify-content:center;
    font-size:12px; line-height:1.05; white-space:pre; text-align:center; color:var(--ink); overflow:hidden;
    min-width:0; min-height:0; position:relative; cursor:grab;
  }
  .itemArt.rarity-common{ border-color:var(--rarity-common); background:rgba(142,142,142,0.10); }
  .itemArt.rarity-uncommon{ border-color:var(--rarity-uncommon); background:rgba(96,165,250,0.12); }
  .itemArt.rarity-rare{ border-color:var(--rarity-rare); background:rgba(139,92,246,0.13); }
  .itemArt.rarity-epic{ border-color:var(--rarity-epic); background:rgba(255,201,51,0.18); }
  /* Attachment-derived weapon tier — applied instead of the plain
     rarity-* classes on an EQUIPPED weapon's own art box once real
     attachments are slotted (see attachments.js's weaponTierClass()).
     "Shaded cell" per spec: border color + a light background tint at
     the same hue, not just an outline. */
  .itemArt.attach-tier-common{ border-color:var(--attach-common); background:rgba(34,197,94,0.10); }
  .itemArt.attach-tier-uncommon{ border-color:var(--rarity-uncommon); background:rgba(96,165,250,0.10); }
  .itemArt.attach-tier-rare{ border-color:var(--rarity-rare); background:rgba(139,92,246,0.10); }
  .itemArt.attach-tier-epic{ border-color:var(--rarity-epic); background:rgba(255,201,51,0.18); }
  .itemArt img{ width:100%; height:100%; object-fit:fill; display:block; pointer-events:none; }
  .itemArt .qty{ position:absolute; top:0; right:1px; font-size:9px; color:var(--ink-soft); background:rgba(14,14,14,0.8); line-height:1; }
  .itemArt.dragOver{ outline:2px solid var(--blue); }
  .dropZone.dragTargetOK{ outline:2px dashed var(--green); }
  .dropZone.dragTargetBad{ outline:2px dashed var(--red); }

  /* Attachment-compatibility highlight — shown for the FULL DURATION of
     an attachment drag (dragstart -> dragend, hud.js's
     onItemDragStart()/updateAttachmentCompatibilityHighlight()), across
     BOTH equipped weapon slots at once, on whichever screen the drag
     started on (in-raid Inventory overlay or Vault). Distinct from
     .dropZone.dragTargetOK above, which only lights up ON HOVER over one
     specific cell — this fires the moment the drag starts, before the
     player has hovered over anything, so incompatibility is obvious
     up front instead of only discovered after a rejected drop.
     .attachCompatTarget/.attachIncompatTarget go on the weapon's own
     item-art box; .attachIncompatTarget ALSO goes on its whole
     attach-cell grid (nothing more specific to point at when the
     answer's just "no"). For a compatible weapon, the STRONG signal
     moves to .attachSlotTarget on the ONE real destination cell instead
     of glowing the whole grid uniformly. */
  .attachCompatTarget{ outline:1px solid var(--safe); outline-offset:1px; }
  .attachIncompatTarget{ opacity:0.4; filter:grayscale(70%); }
  .attachSlotTarget{ outline:2px solid var(--safe); outline-offset:2px; box-shadow:0 0 6px 1px rgba(52,211,153,0.55); }

  /* Per-weapon attachment cells — sit inline to the RIGHT of the weapon
     art (.weaponSlotRow), always visible for an equipped weapon, real
     drag/drop. Only positions the weapon actually supports ever get a
     cell here (hud.js's weaponAttachCellsHtml skips duds entirely), so
     there's no ".dud" state to style anymore — unlike Vault's read-only
     grid (vault-ui.js), which still shows unsupported positions grayed
     out for reference. */
  .weaponAttachGrid{ display:grid; grid-template-columns:repeat(2,var(--inv-cell)); grid-auto-rows:var(--inv-cell); gap:var(--inv-gap); }
  .attachCell{ border:1px dashed var(--ink-faint); font-size:9px; letter-spacing:0.5px;
    display:flex; align-items:center; justify-content:center; color:var(--ink-faint); text-align:center;
    background:var(--paper); min-width:0; min-height:0; position:relative; }
  .attachCell.attach-tier-common{ border:1px solid var(--attach-common); background:rgba(34,197,94,0.12); color:var(--ink); cursor:grab; }
  .attachCell.attach-tier-uncommon{ border:1px solid var(--rarity-uncommon); background:rgba(96,165,250,0.12); color:var(--ink); cursor:grab; }
  .attachCell.attach-tier-rare{ border:1px solid var(--rarity-rare); background:rgba(139,92,246,0.12); color:var(--ink); cursor:grab; }
  .attachCell.attach-tier-epic{ border:1px solid var(--rarity-epic); background:rgba(255,201,51,0.20); color:var(--ink); cursor:grab; }

  #bottomStrip{ bottom:8px; left:50%; transform:translateX(-50%); display:flex; align-items:flex-end; gap:21px; background:transparent; border:none; }
  #itemPreview{ display:none; }
  #itemPreview .glyph{ height:38px; display:flex; align-items:center; justify-content:center; margin-bottom:4px; font-size:27px; color:var(--cyan); }
  #itemPreview .name{ font-weight:700; text-align:center; }
  #itemPreview .sub{ color:var(--ink-soft); text-align:center; font-size:13px; margin-top:4px; }

  /* VITALS — shield (chunky 4-segment blocks, real gaps, each segment =
     25% of equipped shield capacity, base included) + HP (thin single
     bar, deliberately different shape from shield so the two can't be
     confused) + fireteam-slot/name tag row (stubbed null — see
     hud.js renderHUD() — until co-op/MP exists). Matches the approved
     1:1 pull: no numeric x/y readout on either bar. */
  #vitals{ width:220px; background:rgba(14,14,14,0.85); border:1px solid var(--line); padding:10px 13px;
    clip-path:polygon(8px 0,100% 0,100% calc(100% - 8px),calc(100% - 8px) 100%,0 100%,0 8px); }
  .segRow{ display:flex; gap:2px; height:22px; }
  .seg{ flex:1; border:1px solid var(--line); background-color:var(--paper-dim);
    background-image:repeating-linear-gradient(45deg, rgba(238,241,234,0.06) 0px, rgba(238,241,234,0.06) 2px, transparent 2px, transparent 6px); }
  /* Filled-segment color follows the equipped shield's real tier
     (hud.js sets .tier-common/.tier-uncommon/.tier-rare/.tier-epic on
     .segRow) — same rarity-* variables the inventory cells use, so a
     rare shield reads visually consistent with a rare inventory item. */
  .tier-common .seg.filled{ background:var(--rarity-common); border-color:var(--rarity-common); background-image:none; }
  .tier-uncommon .seg.filled{ background:var(--rarity-uncommon); border-color:var(--rarity-uncommon); background-image:none; }
  .tier-rare .seg.filled{ background:var(--rarity-rare); border-color:var(--rarity-rare); background-image:none; }
  .tier-epic .seg.filled{ background:var(--rarity-epic); border-color:var(--rarity-epic); background-image:none; }

  .hpBar{ height:5px; background:var(--paper-dim); border:1px solid var(--ink-faint); margin-top:6px; position:relative; }
  .hpBar .fill{ position:absolute; inset:0; background:var(--green); }
  .hpBar .fill.low{ background:var(--red); }

  .tag{ display:flex; align-items:center; gap:8px; margin-top:12px; font-size:11px; }
  .tagIcon{ width:22px; height:22px; background:var(--paper-dim); border:1px solid var(--ink-faint); display:flex; align-items:center; justify-content:center; font-size:10px; color:var(--ink-faint); }
  .tag .id{ color:var(--ink-soft); }

  /* PACK SPACE + HOTBAR — slot 4 (left) and slot 5 (right) flank the
     pack-space readout on top; Q/F/V below are fully static and inert,
     reserved for equipment/ability use later (no JS wiring on purpose —
     see the comment in ext-shoot-game.html). Centered per request. */
  #packHotbar{ width:220px; background:rgba(14,14,14,0.85); border:1px solid var(--line); padding:10px 13px; text-align:center;
    clip-path:polygon(8px 0,100% 0,100% calc(100% - 8px),calc(100% - 8px) 100%,0 100%,0 8px); }
  #packTopRow{ display:flex; align-items:center; justify-content:center; gap:10px; margin-bottom:6px; }
  #packTopRow .packLabels{ flex:1; }
  #packHotbar .k{ font-size:12px; letter-spacing:1px; color:var(--ink-soft); text-transform:uppercase; }
  #packHotbar .v{ font-weight:700; margin:4px 0 0; letter-spacing:0.5px; }

  #hotbar{ display:flex; gap:5px; justify-content:center; }
  .hbSlot{ width:44px; height:44px; border:1px solid var(--line); background:var(--paper); font-size:13px; display:flex; align-items:center; justify-content:center; position:relative; color:var(--ink-soft); }
  .hbSlot .num{ position:absolute; top:1px; left:3px; font-size:10px; color:#9aa39a; }
  .hbSlot.active{ border-color:var(--cyan); color:var(--cyan); }
  .flankSlot{ width:44px; height:44px; flex-shrink:0; border:1px dashed var(--ink-faint); background:transparent;
    display:flex; align-items:center; justify-content:center; font-size:11px; color:var(--ink-faint); }

  /* AMMO — plain neutral icon chip (ammo-type short code, e.g. "LT" for
     ammo_light — see AMMO_TYPE_CODE in hud.js) anchoring the left side
     so the panel doesn't read as lopsided/empty; text column keeps the
     weapon name + MAG readout + spare-mag/reserve line, right-aligned. */
  #ammoBlock{ width:220px; display:flex; align-items:center; gap:12px; background:rgba(14,14,14,0.85); border:1px solid var(--line); padding:10px 13px;
    clip-path:polygon(8px 0,100% 0,100% calc(100% - 8px),calc(100% - 8px) 100%,0 100%,0 8px); }
  .ammoIconWrap{ position:relative; width:36px; height:36px; flex-shrink:0; }
  .ammoIcon{ width:36px; height:36px; border:1px solid var(--line); background:var(--paper-dim);
    color:var(--ink-soft); display:flex; align-items:center; justify-content:center; font-size:12px; font-weight:700; letter-spacing:0.5px; transition:color 0.15s; }
  .reloadOutlineSvg{ position:absolute; inset:0; pointer-events:none; }
  .reloadOutlineSvg rect{ transition:opacity 0.15s; }
  .ammoTextCol{ flex:1; text-align:right; min-width:0; }
  #ammoBlock .weapon{ font-size:11px; letter-spacing:1px; color:var(--ink-soft); margin-bottom:4px; }
  #ammoBlock .row{ display:flex; align-items:baseline; justify-content:flex-end; gap:10px; }
  #ammoBlock .mag{ font-size:9px; color:var(--ink-faint); letter-spacing:1.5px; }
  #ammoBlock .v{ font-size:30px; letter-spacing:1px; color:var(--ink); }
  #ammoBlock .v .u{ font-size:13px; color:var(--ink-soft); }
  #ammoBlock .reserve{ font-size:10px; color:var(--ink-soft); margin-top:2px; }

  /* inventory overlay: merged Inventory panel, toggled by "I". Panel
     chrome (padding/gaps) and label text are scaled by the same 1.2x
     factor as the slots themselves, scoped to #inventoryPanel only so
     other HUD panels (kill feed, item preview, etc.) that share the
     .hudLabel/.hudSub classes are unaffected. */
  #inventoryOverlay{ position:absolute; inset:0; display:none; align-items:center; justify-content:center; background:rgba(6,6,6,0.65); z-index:6; }
  #inventoryOverlay.open{ display:flex; }
  #inventoryPanel{ background:rgba(14,14,14,0.75); border:1px solid var(--line); padding:22px 28px; display:flex; flex-direction:column; gap:16px; backdrop-filter:blur(2px); position:relative; overflow:hidden; }
  #inventoryPanel .hudLabel{ font-size:14px; }
  #inventoryPanel .hudSub{ font-size:12px; }
  #inventoryColumns{ display:flex; gap:37px; align-items:flex-start; }
  .invCol{ display:flex; flex-direction:column; gap:6px; }

  /* Toast — surfaces pushLog() messages (equip rejections especially:
     "doesn't fit this weapon", "backpack full", etc.) that previously had
     nowhere to render at all. z-index above the Inventory overlay (6) so
     a rejection while the overlay's open is still visible. */
  #toast{
    position:absolute; top:52px; left:50%; transform:translate(-50%,-6px);
    z-index:9; background:rgba(6,6,6,0.92); color:var(--ink);
    padding:6px 14px 6px 12px; font-size:12px; letter-spacing:0.3px;
    border-left:2px solid var(--ink-faint);
    opacity:0; pointer-events:none; transition:opacity 0.2s ease, transform 0.2s ease;
  }
  #toast.show{ opacity:1; transform:translate(-50%,0); }

  /* In-raid Contracts tracker — top-right corner, toggled with J
     (controls.js). Compact HUD readout, deliberately distinct from the
     Loadout screen's full "bounty posting" cards (.contractCard) — this
     is a glance-at-while-playing panel, not a browsable board. */
  #contractsTracker{ top:16px; right:16px; padding:16px 20px; min-width:360px; max-width:440px; }
  #contractsTrackerHeader{ font-size:20px; letter-spacing:3px; color:var(--ink-soft);
    margin-bottom:12px; display:flex; justify-content:space-between; }
  #contractsTrackerHint{ color:var(--ink-faint); font-size:18px; }
  #contractsTrackerList{ display:flex; flex-direction:column; gap:12px; }
  .trackerEntry{ font-size:22px; }
  .trackerEntryName{ color:var(--ink); font-weight:700; }
  .trackerEntryProgress{ color:var(--ink-soft); font-size:20px; }
  .trackerEntryProgress.trackerEntryExtract{ color:var(--rarity-uncommon); }
  .trackerEntryProgress.trackerEntryCompleted{ color:var(--safe); }

  /* Stat tooltip — Name + StatLine for any item/attachment with
     showStatTooltip:true. Positioned in JS (hud.js) above whichever slot
     is hovered; pointer-events:none so it can never itself intercept the
     hover/drag it's describing. z-index above the Inventory overlay so it
     reads on top of everything else in there. */
  #itemTooltip{
    position:fixed; display:none; z-index:10; transform:translate(-50%,-100%);
    background:rgba(6,6,6,0.95); border:1px solid var(--ink-faint); color:var(--ink);
    padding:6px 10px; min-width:120px; max-width:220px; pointer-events:none;
    font-size:11px; line-height:1.4;
  }
  #itemTooltip .ttName{ font-weight:700; letter-spacing:0.3px; margin-bottom:2px; }
  #itemTooltip .ttStat{ color:#c7ccc6; }

  #overlay{ position:absolute; inset:0; background:rgba(10,10,10,0.96); display:flex; align-items:center; justify-content:center; flex-direction:column; text-align:center; z-index:10; padding:26px; overflow:hidden; }
  #overlay h1{ letter-spacing:5px; font-size:29px; margin:0 0 8px; color:var(--ink); }
  #overlay .lead{ color:var(--ink-soft); max-width:624px; font-size:16px; line-height:1.6; margin-bottom:18px; }
  #overlay .stats{ font-size:17px; margin-bottom:21px; line-height:1.8; }
  #overlay button{ background:transparent; border:1px solid var(--ink); color:var(--ink); padding:10px 23px; font-family:inherit; font-size:16px; letter-spacing:3px; cursor:pointer; text-transform:uppercase; }
  #overlay button + button{ margin-top:12px; }
  #overlay button:hover{ background:var(--ink); color:var(--paper); }
  #overlay.hidden{ display:none; }
  #overlay.dead h1{ color:var(--red); } #overlay.dead button{ border-color:var(--red); color:var(--red); } #overlay.dead button:hover{ background:var(--red); color:#fff; }
  #overlay.win h1{ color:var(--cyan); } #overlay.win button{ border-color:var(--cyan); color:var(--cyan); } #overlay.win button:hover{ background:var(--cyan); color:#fff; }

  /* End-of-raid report — multi-phase, phases STACK (hud.js's
     enterReport()/stepEndReport()/renderEndReport()), replaces the old
     single-popup dead/extracted screen. Inventory stays fixed on the
     right the whole time; a headline value block + secondary stat grid
     stack on the left, Continue button appears bottom-center once fully
     revealed. */
  #reportScreen{ width:100%; max-width:820px; }
  .reportSubline{ color:var(--ink-soft); font-size:13px; margin-top:2px; }
  #reportBody{ display:flex; gap:36px; align-items:flex-start; justify-content:center; margin-top:22px; text-align:left; }
  #reportLeft{ width:300px; padding-top:4px; }
  #reportRight{ background:rgba(14,14,14,0.75); border:1px solid var(--line); padding:16px; clip-path:polygon(10px 0,100% 0,100% calc(100% - 10px),calc(100% - 10px) 100%,0 100%,0 10px); }

  .reportLabel{ font-size:11px; letter-spacing:1.5px; color:var(--ink-soft); text-transform:uppercase; margin-bottom:4px; }
  .reportValue{ font-size:20px; font-weight:700; color:var(--ink); letter-spacing:1px; }
  #overlay.report.dead .reportValue{ color:var(--red); }
  #overlay.report.win .reportValue{ color:var(--cyan); }

  /* Headline — the one number every player looks for first, sized well
     above every other stat on the screen. */
  .reportHeadline{ margin-bottom:22px; padding-bottom:18px; border-bottom:1px solid var(--line); }
  .reportValueBig{ font-size:42px; line-height:1.1; }
  .reportAutosell{ margin-top:6px; font-size:13px; color:var(--ink-soft); }
  #overlay.report.win .reportAutosell{ color:var(--cyan); }

  /* Secondary stats — real (time/kills) and honest-placeholder
     (EXP/contracts) values side by side in one 2x2 grid instead of the
     old one-per-row stack, so the eye reads them as a group, not a
     staged drip of separate blocks. */
  .reportStatGrid{ display:grid; grid-template-columns:1fr 1fr; gap:14px 20px; }
  .reportStat .reportValue{ font-size:20px; }
  .reportStatPending .reportValue{ color:var(--ink-faint); }

  #reportEquipRow{ display:flex; gap:6px; margin-bottom:4px; flex-wrap:wrap; align-items:flex-end; }
  .reportItem{ border:1px solid var(--line); background:var(--paper); overflow:hidden; }
  /* Weapon — same wide rectangle the real in-raid weaponSlotRow art box
     uses (174x85, ~2:1), scaled down to report-panel size so a rifle's
     art isn't crushed into a square. */
  .reportItemWeapon{ width:87px; height:41px; }
  /* Shield — same 2-cell-wide x1-tall compact shape shieldSlot uses
     elsewhere, independent of the item's real 4x2 backpack footprint. */
  .reportItemWide{ width:calc(2 * 41px + 4px); height:41px; }
  /* Backpack (equipped pack) — same 2x2 shape packSlot uses. */
  .reportItemPack{ width:calc(2 * 41px + 4px); height:calc(2 * 41px + 4px); }
  /* Melee (knife) — genuinely a 1x1 item, stays a plain square. */
  .reportItemSquare{ width:41px; height:41px; }
  .reportEmpty{ color:var(--ink-faint); font-size:12px; }
  .reportSubCount{ color:var(--ink-faint); font-weight:400; text-transform:none; letter-spacing:0; margin-left:4px; }
  #reportContinueWrap{ margin-top:26px; min-height:44px; }

  /* Auto-sold item strip — small icon row under the "+X cr auto-sold"
     line, same compact chip look as reportEquipRow but square (autosell
     items are backpack stackables, not weapons/shields). */
  .reportAutosoldRow{ display:flex; gap:5px; margin-top:8px; flex-wrap:wrap; }
  .reportAutosoldItem{ position:relative; width:30px; height:30px; border:1px solid var(--line); background:var(--paper); }
  .reportAutosoldItem .qty{ position:absolute; bottom:0; right:1px; font-size:9px; color:var(--ink-soft); background:rgba(6,6,6,0.85); padding:0 2px; line-height:1.3; }

  .keyhint{ color:var(--ink-soft); font-size:13px; margin-top:5px; line-height:1.6; }
  .keyhint b{ color:var(--ink); }
  #flash{ position:absolute; inset:0; background:var(--red); opacity:0; pointer-events:none; mix-blend-mode:multiply; z-index:5; }
  #extractCountdown{
    position:absolute; left:50%; transform:translate(-50%,-50%); z-index:6; display:none;
    font-size:42px; font-weight:700; color:var(--safe); text-shadow:0 0 12px rgba(52,211,153,0.5);
  }
