  * { margin: 0; padding: 0; box-sizing: border-box; }
  body {
    background: #0a0a0a;
    font-family: 'Courier New', monospace;
    min-height: 100vh;
    display: flex;
    justify-content: center;
    align-items: flex-start;
    padding: 18px 0 24px;
    overflow-y: auto;
    color: #34d399;
  }

  .game-container {
    position: relative;
    display: flex;
    flex-direction: column;
    gap: 20px;
  }

  .main-toolbar {
    display: flex;
    justify-content: flex-end;
  }

  .main-toolbar .ghost-btn {
    padding: 6px 10px;
    font-size: 9px;
    letter-spacing: 1px;
  }

  .game-container {
    background: #121214;
    /* Rigging "chassis" LED target — the one and only border this
       recolors (the outer frame). Set by applyRigTheme() in
       equipment.js; falls back to the original #34d399 green when no
       chassis LED is installed. */
    border: 2px solid var(--sb-chassis-color, #34d399);
    border-radius: 16px;
    padding: 28px;
    width: 1100px;
    max-width: 98vw;
    box-shadow: 0 0 60px rgba(52, 211, 153, 0.15);
    zoom: 0.9;
  }

  /* Sits exactly on top of .game-container's own 2px border above (both
     use border-box sizing via the universal `*` rule, and the same
     border-radius, so they align pixel-for-pixel). Transparent border
     when idle - the real border shows through untouched. Once a chassis
     LED is installed, applyRigTheme() gives this element the same
     border color AND runs applyLedFx() on it (same function used for
     the nameplate text and every icon), so it picks up the exact same
     filter-based glow/animation for free instead of needing its own
     bespoke keyframes. */
  .chassis-led-frame {
    position: absolute;
    inset: 0;
    border-radius: 16px;
    border: 2px solid transparent;
    pointer-events: none;
    z-index: 25;
  }

  h2 { font-size: 12px; letter-spacing: 4px; color: #4a4a5e; font-weight: normal; }

  /* --- TICKER FEED ---
     Sits between .top-row and .bottom-row (see index.html). One message
     is shown at a time: idle/invisible -> gradient wipe-in -> hold ->
     fade-out -> next message. js/ticker.js drives it by toggling
     .revealing / .fading on #tickerText and setting --ticker-grad-a/b
     per event type before each reveal (see TYPE_GRADIENTS there). */
  .ticker-bar {
    height: 34px;
    background: #0d0d12;
    border: 1px solid #1a1a2e;
    border-radius: 10px;
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: hidden;
    padding: 0 20px;
    /* .game-container's flex gap (20px) applies uniformly above and
       below every row, including this one - these negative margins
       pull the ticker closer to the panels on both sides without
       touching that gap value globally (which would also spread the
       top-row/bottom-row panels themselves further apart). Adjust to
       taste; -10px roughly halves the visible gap on each side. */
    margin: -12px 15px;
  }

  .ticker-text {
    font-size: 13px;
    font-weight: 700;
    letter-spacing: 1.25px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    max-width: 100%;
    /* Idle state: clip-path hides the entire string (0 width revealed),
       so nothing is drawn at all - reads as "the same colour as the
       bar" without needing a literal color match that would only hold
       for one particular background. Opacity stays at 1 here on
       purpose; visibility is controlled by clip-path during the
       reveal, and only drops via opacity during the fade-out below. */
    opacity: 1;
    color: transparent;
    background-image: linear-gradient(90deg, var(--ticker-grad-a, #34d399) 0%, var(--ticker-grad-b, #34d399) 100%);
    background-size: 100% 100%;
    -webkit-background-clip: text;
    background-clip: text;
    clip-path: inset(0 100% 0 0);
  }

  .ticker-text.revealing {
    animation: ticker-reveal 1.1s cubic-bezier(0.4, 0, 0.2, 1) forwards;
  }

  .ticker-text.fading {
    animation: ticker-fade 0.8s ease forwards;
  }

  /* Horizontal wipe: the right inset shrinks from 100% (nothing shown)
     to 0% (fully shown), sweeping the gradient into view left-to-right.
     Opacity is untouched here - only the clip moves. */
  @keyframes ticker-reveal {
    from { clip-path: inset(0 100% 0 0); }
    to   { clip-path: inset(0 0% 0 0); }
  }

  /* Straightforward fade once fully revealed - clip-path stays open,
     opacity carries the exit. */
  @keyframes ticker-fade {
    from { opacity: 1; }
    to   { opacity: 0; }
  }

  /* --- TOP ROW --- */
  .top-row { display: flex; flex-direction: column; gap: 14px; }

  .character-mega-panel {
    background: #0d0d12;
    border: 1px solid #1a1a2e;
    border-radius: 16px;
    padding: 16px;
  }
  .character-panels { display: flex; align-items: stretch; height: 500px; position: relative; isolation: isolate; z-index: 0; }

  /* No longer its own bordered box — it's the header strip of
     .character-mega-panel now, separated by a hairline instead of a
     second border+gap. */
  .character-topbar {
    display: flex;
    flex-direction: column;
    gap: 8px;
    padding-bottom: 12px;
    margin-bottom: 16px;
    border-bottom: 1px solid #1a1a2e;
    position: relative;
    z-index: 20;
    background: #0d0d12;
  }
  .topbar-row-main {
    position: relative;
    z-index: 0;
    display: flex;
    justify-content: space-between;
    align-items: flex-start;
    gap: 20px;
  }
  .topbar-left-col {
    display: flex;
    flex-direction: column;
    gap: 8px;
    align-items: flex-start;
  }
  /* The self-contained stat widget: name, vault/wallet, fatigue, hp — its
     own bordered card rather than loose items along the topbar. */
  .character-widget {
    display: flex;
    flex-direction: column;
    gap: 2px;
    border: 0px solid #1a1a2e;
    border-radius: 10px;
    padding: 0px 23px;
    min-width: 220px;
  }
  .widget-currency-row { display: flex; flex-direction: column; gap: 4px; }
  /* Engraved SHADOWBOUND wordmark filling the dead space below the
     widget/logout row. Color sits almost flush with the panel
     background — the illusion of depth comes entirely from the two
     text-shadows: a faint green glow below (light catching the lower
     lip of the carved groove) and a hard dark shadow above (the upper
     edge in shadow), rather than from the text itself standing out. */
  .game-wordmark {
    /* Absolutely positioned within .topbar-row-main (its nearest
       positioned ancestor) so it fills the row as a background layer
       without ever adding to the row's height. z-index: -1 paints it
       behind the row's normal-flow children (the widget, the logout
       button) rather than on top of them, so anything opaque in that
       row — like .character-widget's own background — naturally masks
       the wordmark wherever the two overlap, with zero extra markup or
       clipping logic needed. */
    position: absolute;
    inset: 0;
    z-index: -1;
    display: flex;
    align-items: center;
    justify-content: center;
    pointer-events: none;
    text-align: center;
    font-family: 'Cinzel', 'Courier New', serif;
    font-weight: 700;
    font-size: 50px;
    letter-spacing: 4px;
    color: #31384b;
    opacity: 0.35;

    text-shadow:
      0 1px 0 rgba(255,255,255,0.04),
      0 -1px 0 rgba(0,0,0,0.95),
      0 0 12px rgba(167,139,250,0.08);
    background:
      radial-gradient(
        ellipse at center,
        rgba(167,139,250,0.12) 0%,
        rgba(167,139,250,0.04) 40%,
        transparent 70%
  );
    user-select: none;
    animation: wordmark-glow 5s ease-in-out infinite;
  }
  /* Slow ember-like breathing glow behind the carved text. Only the two
     outer (blur) shadow layers change between keyframes — the carved
     highlight/shadow pair (first two layers) stays fixed so the "engraved"
     illusion is never disturbed, just gently backlit. All keyframes keep
     the same number of shadow layers so the browser can interpolate
     between them smoothly. */
  @keyframes wordmark-glow {
    0%, 100% {
      text-shadow:
        0 1px 1px rgba(167, 139, 250, 0.26),
        0 -1px 1px rgba(0, 0, 0, 0.19),
        0 0 18px rgba(167, 139, 250, 0.28),
        0 0 34px rgba(167, 139, 250, 0.1);
    }
    50% {
      text-shadow:
        0 1px 1px rgba(167, 139, 250, 0.26),
        0 -1px 1px rgba(0, 0, 0, 0.19),
        0 0 26px rgba(167, 139, 250, 0.42),
        0 0 52px rgba(167, 139, 250, 0.28);
    }
  }
  .topbar-name-wrap { position: relative; display: inline-flex; min-width: 0; }
  /* Guard against long/uncapped names breaking the row — truncates
     gracefully instead of wrapping or pushing the stats/logout around. */
  .topbar-name {
    max-width: 220px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    /* Rigging "nameplate" LED target — set by applyRigTheme() in
       equipment.js. Fallback matches h2's own default color (#4a4a5e)
       exactly, so nothing changes visually until an LED is installed -
       "inherit" would've picked up body's green instead, since this
       selector's higher specificity always beats the plain h2 rule. */
    color: var(--sb-name-color, #4a4a5e);
  }
  /* Invisible click target sitting exactly over the leading "⟡" glyph
     in .topbar-name — see the HTML comment above this button for why
     it's an overlay rather than a real child of the h2. Nudge the
     width if your font metrics put the diamond somewhere slightly
     different than ~16px in. */
  .secret-gender-toggle {
    position: absolute;
    inset: 0 auto 0 0;
    width: 16px;
    background: transparent;
    border: none;
    padding: 0;
    cursor: pointer;
  }
  /* VAULT / WALLET / HP on one line, FATIGUE on the line below —
     the tiered "step down" look, all centered as one stat cluster. */
  .topbar-stats { display: flex; flex-direction: column; gap: 6px; align-items: center; }
  .stat-line { display: flex; align-items: center; gap: 16px; }
  /* Standard offscreen-but-still-clickable pattern: keeps the legacy
     buttons functional for the bridge script without showing them. */
  .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;
  }

  .auth-screen {
    position: absolute;
    inset: 0;
    background: rgba(5, 5, 8, 0.88);
    backdrop-filter: blur(6px);
    display: flex;
    align-items: center;
    justify-content: center;
    z-index: 20;
    border-radius: 16px;
  }
  .auth-screen-card {
    width: min(420px, 92vw);
    background: #0d0d12;
    border: 1px solid #1a1a2e;
    border-radius: 18px;
    padding: 22px;
    display: flex;
    flex-direction: column;
    gap: 12px;
    box-shadow: 0 0 60px rgba(52, 211, 153, 0.12);
  }
  .auth-header { display: flex; justify-content: space-between; align-items: center; gap: 8px; }
  .auth-form { display: flex; flex-direction: column; gap: 8px; }
  .auth-form input {
    background: #0a0a0f;
    border: 1px solid #1a1a2e;
    border-radius: 8px;
    padding: 9px 10px;
    color: #34d399;
    font-family: 'Courier New', monospace;
  }
  .auth-form input::placeholder { color: #4a4a5e; }
  .auth-actions { display: flex; gap: 8px; }
  .auth-actions button { flex: 1; padding: 8px 10px; font-size: 11px; letter-spacing: 1px; }
  .ghost-btn {
    background: transparent;
    border: 1px solid #7a7a8e;
    color: #7a7a8e;
    padding: 6px 10px;
    font-size: 9px;
    letter-spacing: 1px;
  }
  .ghost-btn:hover { background: #7a7a8e; color: #0a0a0a; }
  .auth-error { min-height: 14px; color: #f87171; font-size: 10px; }
  .auth-status { font-size: 11px; color: #7a7a8e; line-height: 1.5; }
  .auth-hint { font-size: 10px; color: #4a4a5e; line-height: 1.5; }
  .auth-authenticated .auth-screen-card { padding-top: 16px; padding-bottom: 16px; }
  .auth-authenticated .auth-form { display: none; }
  .auth-authenticated .auth-error { display: none; }
  .auth-authenticated { display: none; }
  .avatar-header h2 { display: flex; align-items: center; gap: 8px; }
  .avatar-header .avatar-player-name { color: #34d399; }
  .avatar-gender-toggle {
    display: inline-flex;
    gap: 4px;
    margin: 6px 0 6px;
  }
  .avatar-gender-btn {
    border: 1px solid #2a2a3e;
    background: #0a0a0f;
    color: #7a7a8e;
    font-size: 11px;
    line-height: 1;
    padding: 2px 7px;
    border-radius: 999px;
    letter-spacing: 0;
    font-weight: bold;
    min-width: 24px;
  }
  .avatar-gender-btn:hover {
    border-color: #34d399;
    color: #34d399;
    background: #0f1d1a;
    box-shadow: 0 0 10px rgba(52, 211, 153, 0.2);
  }
  .avatar-gender-btn.active,
  .avatar-gender-btn[aria-pressed="true"] {
    border-color: #34d399;
    color: #07110c;
    background: #34d399;
    box-shadow: 0 0 12px rgba(52, 211, 153, 0.28);
  }

  /* --- AVATAR PANEL --- */
  .avatar-panel { width: 400px; flex-shrink: 0; height: 100%; padding-right: 20px; }
  /* .avatar-header no longer used in markup — its contents now live in
     .character-topbar above the panels row — kept here only in case any
     script still queries it. */
  .avatar-header { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 10px; gap: 20px; }
  .hp-group { display: flex; flex-direction: column; gap: 4px; }
  .hp-line, .fatigue-readout { display: flex; align-items: center; gap: 8px; }
  .hp-label, .fatigue-label { font-size: 11px; color: #4a4a5e; width: 65px; flex-shrink: 0; }
  .hp-value {
    font-size: 11px;
    color: #e5e7eb;
    opacity: 0;
    transition: opacity 0.2s ease;
    pointer-events: none;
  }
  .hpbar:hover + .hp-value {
    opacity: 1;
  }
  .currency-readouts { display: flex; gap: 12px; flex-wrap: wrap; }
  .gold-readout { font-size: 11px; color: #4a4a5e; letter-spacing: 1px; }
  .gold-readout span { color: #dcdcdc; font-weight: bold; }
  .hpbar, .fatigue-bar { width: 80px; height: 6px; background: #0e1625; border-radius: 3px; overflow: hidden; border: 1px solid #112131; }
  .hpfill { height: 100%; background: #34d399; transition: width 0.3s; }
  .fatigue-fill { width: 100%; height: 100%; background: #60a5fa; transition: width 0.3s ease, background 0.3s ease; }

  .avatar-body { display: flex; gap: 14px; height: 100%; }

  /* --- EQUIP SLOTS (now a loadout column beside the avatar) --- */
  .slots-column {
    display: flex;
    flex-direction: column;
    justify-content: center;
    gap: 10px;
    flex-shrink: 0;
  }
  .equip-slot {
    width: 62px;
    padding: 7px 4px 6px;
    border-radius: 8px;
    border: 1px dashed #2a2a3e;
    background: rgba(10, 10, 15, 0.5);
    cursor: pointer;
    transition: 0.2s;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 4px;
    position: relative;
  }
  .equip-slot .icon-wrap {
    width: 26px;
    height: 26px;
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: visible;
  }
  .equip-slot .icon-wrap svg {
    overflow: visible;
    transform: scale(1.8) translateY(25%);
  }
  .equip-slot .icon-wrap.holo-icon svg {
    --holo-base: scale(1.8) translateY(25%);
  }

  /* Consumable slots: smaller size, different accent */
  .equip-slot[data-slot^="consumable"] {
    width: 28px;
    height: 28px;
    padding: 4px;
    flex-direction: row;
    justify-content: center;
    border-color: #2a2a3e; /* Default blue for consumables */
  }
  .equip-slot[data-slot^="consumable"] .icon-wrap {
    width: 20px;
    height: 20px;
  }
  .equip-slot[data-slot^="consumable"] .icon-wrap svg {
    transform: scale(1.4) translateY(15%);
  }
  .equip-slot[data-slot^="consumable"] .icon-wrap.holo-icon svg {
    --holo-base: scale(1.4) translateY(15%);
  }
  .equip-slot[data-slot^="consumable"] .slot-label {
    display: none; /* No labels for small slots */
  }
  .equip-slot[data-slot^="consumable"] .slot-familiarity {
    display: none; /* No familiarity for consumables */
  }
  .equip-slot[data-slot^="consumable"] .holo-beam {
    width: 35px;
    height: 28px;
  }

  .consumable-row {
    display: flex;
    gap: 6px;
    justify-content: center;
  }
  .equip-slot .slot-label {
    font-size: 9px;
    letter-spacing: 1px;
    color: #3a3a4a;
  }
  /* keeps the label's height reserved (so the slot doesn't reflow)
     but hides the text once an item is filling the icon above it */
  .equip-slot.filled .slot-label { visibility: hidden; }
  .slot-familiarity {
    width: 100%;
    height: 4px;
    background: #13131c;
    border-radius: 999px;
    overflow: hidden;
    margin-top: 2px;
  }
  .slot-familiarity .familiarity-fill {
    height: 100%;
    background: linear-gradient(90deg, #34d399, #38bdf8);
    transition: width 0.2s ease;
  }
  /* Slot border state, low -> high priority (later rule wins ties since
     all of these share the same 0,2,0 specificity):
       default dashed  <  filled (rarity color, solid)  <  hover (dotted green)  <  selected/led-target (gold, further below)
     Rarity comes in as --slot-rarity-color, a custom property set per-slot
     in equipment.js, rather than a plain inline border-color — inline
     border-color would out-rank every rule here regardless of order,
     which is why hover/selected used to get silently overridden on
     filled slots. */
  .equip-slot.filled { border-style: solid; border-color: var(--slot-rarity-color, #2a2a3e); }
  .equip-slot:hover { border-style: dashed; border-color: #34d399; box-shadow: 0 0 15px rgba(52, 211, 153, 0.25); }

  /* --- RIGGING LED TARGETS (Rigging/System tab only) ---
     Two small target boxes pinned to the top of the avatar render box,
     let you point an owned LED cosmetic at the nameplate or at the
     panel/terminal "chassis" instead of a piece of gear. Hidden by
     default; toggled to display:flex by renderRigSlots() only while
     uiMode === 'system'. Reuses .equip-slot as a base (border, hover,
     .led-target/.filled states already work unmodified) with a smaller
     footprint and absolute placement layered above the avatar art. */
  .rig-slots {
    display: none;
    position: absolute;
    top: 10px;
    left: 10px;
    right: auto;
    z-index: 6; /* above avatar-stack and the hit/block flash overlays (z-index: 5) */
    justify-content: flex-start;
    gap: 10px;
    pointer-events: none;
  }
  .rig-slots .rig-slot {
    pointer-events: auto;
    width: 56px;
    background: rgba(10, 10, 15, 0.75);
    backdrop-filter: blur(3px);
  }
  /* Same asset-based LED icon (premium/led.svg, via Game.Items.renderIcon)
     the System tab's inventory cards already use, just tinted dim/grey
     while the slot is empty and the item's own LED color once it's
     installed — matching the pattern real gear slots use for their own
     icon-wrap (see .equip-slot .icon-wrap svg above), not a flat swatch. */
  .rig-slot .icon-wrap {
    width: 26px;
    height: 26px;
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: visible;
  }
  .rig-slot .icon-wrap svg {
    overflow: visible;
    transform: scale(1.5) translateY(10%);
  }
  .rig-slot .icon-wrap.holo-icon svg {
    --holo-base: scale(1.5) translateY(10%);
  }
  .rig-slot .slot-label {
    font-size: 8px;
  }

  .avatar-stage {
    position: relative;
    flex: 1;
    background: #0d0d12;
    border-radius: 20px;
    /* Not a chassis LED target — only the outer game-container frame
       and the terminal screen/scanlines react to the chassis LED.
       Left as the original static color. */
    border: 1px solid #1a1a2e;
    padding: 10px;
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: hidden;

    /* --- EASY CUTOUT ADJUSTMENT CONTROLS ---
       If the rasterized background effects cutout is not lining up perfectly with
       your avatar wrapper, you can tweak these three variables right here in CSS!
       No JS edits needed.
    */
    --cutout-offset-x: 0px; /* Shift cutout left/right (e.g. -5px or 10px) */
    --cutout-offset-y: 1px; /* Shift cutout up/down (e.g. -10px or 15px) */
    --cutout-scale: 1;    /* Resize the cutout (e.g. 0.95 or 1.1) */
  }

  .avatar-stack {
    position: relative;
    width: 96%;
    height: 96%;
    transform: translateY(38%) scale(1.13);
    transform-origin: 50% 52%;
  }
  .avatar-layer {
    position: absolute;
    inset: 0;
    pointer-events: none;
  }
  .avatar-layer svg {
    width: 100%;
    height: 100%;
    display: block;
  }

  /* --- INVENTORY --- */
  /* Shared positioning parent for the background combat stage (layer B:
     splat zone + mirrored enemy panel, always present, lower z-index)
     and the inventory panel (layer A, higher z-index, slides via
     transform to reveal what's underneath it). Both children are
     position: absolute; inset: 0 relative to THIS element rather than
     .character-panels — so they stay confined to the inventory column
     instead of overlapping .avatar-panel to their left. overflow: hidden
     here is what clips the inventory panel's retract animation so it
     can't slide past the header — without this, translateY(-110%) just
     moves the box wholesale with nothing to clip it. */
  .inventory-column {
    position: relative;
    flex: 1;
    height: 100%;
    min-height: 0;
    overflow: hidden;
  }
  .inventory-panel {
    position: absolute;
    inset: 0;
    display: flex;
    flex-direction: column;
    min-height: 0;
    padding-left: 20px;
    border-left: 1px solid rgba(255, 255, 255, 0.05);
    z-index: 1;
    /* Smoked/tinted glass, not true see-through: .combat-stage-row sits
       directly behind this (the splat-plate + arrow/wand/gun chain),
       and that's only meant to be visible once this panel retracts
       off-screen for combat — not bleeding through while browsing
       inventory. So alpha stays high (barely anything legible passes
       through) while a heavy blur + tint close to the page's #0a0a0a
       (rather than the flatter #0d0d12 card color) still sells the
       glass depth via blur/sheen rather than literal transparency. */
    background: rgba(9, 9, 12, 0.93);
    backdrop-filter: blur(24px) saturate(110%);
    -webkit-backdrop-filter: blur(24px) saturate(110%);
    box-shadow:
      inset 0 1px 0 rgba(255, 255, 255, 0.03),
      inset 0 0 50px rgba(0, 0, 0, 0.45);
    /* Bounce fix: the old curve's 1.56 was a "back" easing value that
       deliberately overshoots past 100% before settling — that
       overshoot was the bounce. This curve decelerates with no
       overshoot. */
    transition: transform 0.5s cubic-bezier(0.4, 0, 0.2, 1);
  }

  .inventory-panel.retracted {
    transform: translateY(-110%);
    pointer-events: none;
  }

  /* Background combat stage — revealed when .inventory-panel retracts.
     Mirrors .avatar-body's layout exactly (same widths/gaps, just
     flipped left-right) so .enemy-viewport ends up truly identical to
     .avatar-stage in both size and relative position, not just
     eyeballed close. */
  .combat-stage-row {
    position: absolute;
    inset: 0;
    z-index: 0;
    display: flex;
    align-items: stretch;
    /* combat-splat-zone used to be the flex:1 child in front of
       .enemy-panel here, which is what pushed enemy-panel to the right
       edge. Now that combat-splat-zone lives elsewhere (see below),
       .enemy-panel is the only child left, so justify-content: flex-end
       is what keeps it pinned to the same right-edge spot instead of
       drifting to hug the left edge of this row. */
    justify-content: flex-end;
  }

  /* combat-splat-zone (the hit-splat/miss plate) now lives in the HTML
     as its own sibling of .avatar-panel / .inventory-column, directly
     inside .character-panels (which already has position: relative
     from the isolation/z-index rule above) — rather than sharing
     .enemy-panel's box the way it used to. On desktop this rule puts
     it right back in its old visual spot, floating between the two
     400px side panels exactly as before. On mobile, the media query
     further down overrides this into a normal full-width row instead. */
  .combat-splat-zone {
    position: absolute;
    top: 0;
    bottom: 0;
    left: 400px; /* avatar-panel's width */
    right: 400px; /* enemy-panel's width */
    z-index: 0;
    pointer-events: none;
  }

  /* Bolted chrome plate housing the splat chain — reads as a physical
     panel fixed to the terminal rather than a floating glow. Sized in
     px rather than around content since the three chains (arrow/wand/
     gun) have different natural widths and we don't want the plate
     resizing when the weapon type swaps.

     Horizontal position is clamped rather than a plain 50%/translate
     centering: .combat-splat-zone's rendered width is just whatever's
     left over after the two fixed 400px side panels, which can easily
     shrink below this plate's 210px width (game-container's
     max-width: 98vw makes that the common case, not an edge case).
     A naive centered transform would then bleed the plate equally
     past BOTH edges of the zone — and the left edge is
     .inventory-column's own overflow:hidden boundary, which hard-clips
     whatever crosses it, unlike the right side (still inside
     .inventory-column, so it just tucks behind .enemy-panel). Flooring
     the offset at 0px means the plate only ever overflows rightward
     once the zone gets tight, avoiding the clip entirely. */
  .splat-plate {
    position: absolute;
    top: 50%;
    left: max(0px, calc(50% - 105px)); /* 105px = half the plate's own 210px width */
    transform: translateY(-50%);
    width: 210px;
    height: 78px;
    border-radius: 18px;
    background: linear-gradient(180deg, #2e2e38 5%, #17171d 10%, #0b0b0f 100%);
    border: 3px solid #1e1e24;
    box-shadow:
      inset 0 1px 0 rgba(255, 255, 255, 0.08),
      inset 0 -2px 5px rgba(0, 0, 0, 0.6),
      0 4px 16px rgba(0, 0, 0, 0.55);
    display: flex;
    align-items: center;
    justify-content: center;
  }

  /* Corner screws — domed radial-gradient head + a rotated dark line
     for the slot, so each reads as a fastener rather than a dot. */
  .splat-screw {
    position: absolute;
    width: 9px;
    height: 9px;
    border-radius: 50%;
    background: radial-gradient(circle at 35% 30%, #7a7a82, #1e1e24 60%, #1c1c22 100%);
    box-shadow: inset 0 0 1px rgba(0, 0, 0, 0.85), 0 1px 1px rgba(255, 255, 255, 0.15);
  }
  .splat-screw::after {
    content: "";
    position: absolute;
    top: 50%;
    left: 50%;
    width: 6px;
    height: 1px;
    background: rgba(0, 0, 0, 0.75);
    transform: translate(-50%, -50%) rotate(35deg);
  }
  .splat-screw.tl { top: 7px; left: 7px; }
  .splat-screw.tr { top: 7px; right: 7px; }
  .splat-screw.bl { bottom: 7px; left: 7px; }
  .splat-screw.br { bottom: 7px; right: 7px; }

  /* Single tuning point, same variables as before. */
  .splat-track {
    --splat-scale: 1.1;
    --splat-x: 0px;
    --splat-y: -1px;
    --splat-arrow-width: 34px;
    --splat-arrow-height: 34px;
    --splat-cutout-scale: 1;
    --splat-cutout-x: 0px;
    --splat-cutout-y: 0px;
    --splat-arrow-overlap: -12px;
    --splat-start-delay: 90ms;
    --splat-step-ms: 130ms;
    position: relative;
    width: max-content;
    height: var(--splat-arrow-height);
    transform: translate(var(--splat-x), var(--splat-y)) scale(var(--splat-scale));
  }

  /* Only one chain is shown at a time, picked by data-weapon on
     #splatTrack ("arrow" | "wand" | "gun" — combat.js just needs to
     set this attribute wherever it currently knows the equipped
     weapon; defaults to arrow if unset). */
  .splat-chain {
    display: none;
    align-items: center;
    height: 100%;
  }
  .splat-track:not([data-weapon="wand"]):not([data-weapon="gun"]) .splat-chain-arrow { display: flex; }
  .splat-track[data-weapon="wand"] .splat-chain-wand { display: flex; }
  .splat-track[data-weapon="gun"] .splat-chain-gun { display: flex; }

  /* ============ ARROW (default) ============ */
  .splat-arrow {
    position: relative;
    width: var(--splat-arrow-width);
    height: 100%;
    margin-left: var(--splat-arrow-overlap);
    flex-shrink: 0;
  }
  .splat-arrow:first-child { margin-left: 0; }
  .splat-arrow.flip { transform: scaleX(-1); }

  .arrow-glow {
    position: absolute;
    top: var(--splat-cutout-y);
    left: var(--splat-cutout-x);
    width: 100%;
    height: 100%;
    transform: scale(var(--splat-cutout-scale));
    --glow-color: #f87171;
    background: linear-gradient(90deg, transparent 0%, var(--glow-color) 45%, #fff 50%, var(--glow-color) 55%, transparent 100%);
    background-size: 300% 100%;
    --arrow-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1818 1432'%3E%3Cpath fill='white' d='M600,172 L600,172 L340,770 L622,1410 L76,762 Z M932,760 L932,760 L932,350 L1034,452 L1510,544 L1034,644 Z M936,1224 L936,1224 L936,814 L1038,916 L1514,1008 L1038,1110 Z M944,176 L944,176 L684,774 L966,1412 L420,766 Z M600,172'/%3E%3C/svg%3E");
    mask-image: var(--arrow-mask);
    mask-repeat: no-repeat;
    mask-position: center;
    mask-size: 100% 100%;
    -webkit-mask-image: var(--arrow-mask);
    -webkit-mask-repeat: no-repeat;
    -webkit-mask-position: center;
    -webkit-mask-size: 100% 100%;
    opacity: 0;
    filter: drop-shadow(0 0 6px var(--glow-color)) drop-shadow(0 0 14px var(--glow-color));
  }

  .arrow-glass {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    display: block;
    object-fit: contain;
    object-position: center;
    max-width: 100%;
    max-height: 100%;
    pointer-events: none;
    opacity: 0.6;
    transform: none;
  }

  /* ============ WAND ============ */
  /* Same 6-slot alternating structure as arrows: plain = "(" facing
     the player, .flip = ")" facing the enemy. Purple by default; swaps
     to the shared miss color below like every other weapon type, so
     "didn't land" reads the same regardless of what's equipped. */
  .splat-wand {
    position: relative;
    width: calc(var(--splat-arrow-width) * 0.6);
    height: 100%;
    margin-left: calc(var(--splat-arrow-overlap) * 0.4);
    flex-shrink: 0;
    display: flex;
    align-items: center;
    justify-content: center;
  }
  .splat-wand:first-child { margin-left: 0; }
  .wand-glyph {
    font-family: Georgia, serif;
    font-size: calc(var(--splat-arrow-height) * 0.85);
    line-height: 1;
    color: #a855f7;
    opacity: 0;
    text-shadow: 0 0 6px #a855f7, 0 0 16px #c084fc;
  }

  /* ============ GUN ============ */
  /* Each bullet is a real tapered cartridge silhouette (flat base,
     pointed nose) via clip-path. Two things matter here:
     1) The slot is wider than the glyph (not the other way around),
        so 5 separately-spaced shots read as a spray, not one bleeding
        merged wedge that looks like a laser beam.
     2) The base (plain, unflipped) glyph's nose points LEFT. Only
        .flip copies light up for the player's own attack, and
        .flip = scaleX(-1) mirrors that nose to point RIGHT (toward
        the enemy) — nose-toward-target instead of nose-toward-shooter.
        Plain copies (nose still left) light up for the enemy's
        counter-attack, correctly pointing back at the player. */
  .splat-bullet {
    position: relative;
    width: 15px;
    height: 100%;
    margin-left: 2px;
    flex-shrink: 0;
  }
  .splat-bullet:first-child { margin-left: 0; }
  .splat-bullet.flip { transform: scaleX(-1); }

  .splat-bullet::after {
    content: "";
    position: absolute;
    top: 50%;
    left: 50%;
    width: 11px;
    height: 5px;
    transform: translate(-50%, -50%);
    background: linear-gradient(90deg, #fde68a 0%, #facc15 45%, #eab308 100%);
    /* Flat base on the right, tapered nose to a point on the left. */
    clip-path: polygon(100% 12%, 100% 88%, 42% 88%, 0% 50%, 42% 12%);
    opacity: 0;
    filter: drop-shadow(0 0 4px #facc15) drop-shadow(0 0 9px #fde047);
  }

  /* ============ SWEEP ANIMATIONS ============ */
  @keyframes arrow-sweep {
    0% { opacity: 0; background-position: -110% 0; }
    40% { opacity: 1; background-position: 55% 0; }
    70% { opacity: 0.2; background-position: 130% 0; }
    100% { opacity: 0; background-position: 210% 0; }
  }
  @keyframes arrow-sweep-reverse {
    0% { opacity: 0; background-position: 210% 0; }
    40% { opacity: 1; background-position: 55% 0; }
    70% { opacity: 0.2; background-position: -20% 0; }
    100% { opacity: 0; background-position: -110% 0; }
  }
  @keyframes glow-pulse {
    0% { opacity: 0; }
    40% { opacity: 1; }
    70% { opacity: 0.25; }
    100% { opacity: 0; }
  }

  /* Miss color is shared across all three weapon chains — #38bdf8 is
     the app's actual cyan (matches the block/deflect edge-glow below),
     not the greener #00ffd5 this used to be pinned to. */
  .splat-track.lit.miss .arrow-glow { --glow-color: #38bdf8; }
  .splat-track.lit.enemy .arrow-glow { --glow-color: #f87171; }

  .splat-track.lit.miss .wand-glyph { color: #38bdf8; text-shadow: 0 0 6px #38bdf8, 0 0 16px #7dd3fc; }
  .splat-track.lit.miss .splat-bullet::after {
    background: linear-gradient(90deg, #0284c7 0%, #38bdf8 55%, #7dd3fc 100%);
    filter: drop-shadow(0 0 5px #38bdf8) drop-shadow(0 0 11px #7dd3fc);
  }

  .splat-track.lit:not(.enemy) .splat-arrow.flip .arrow-glow { animation: arrow-sweep 520ms ease-out both; }
  .splat-track.lit.enemy .splat-arrow:not(.flip) .arrow-glow { animation: arrow-sweep-reverse 520ms ease-out both; }
  .splat-track.lit:not(.enemy) .splat-arrow:nth-child(2) .arrow-glow { animation-delay: var(--splat-start-delay); }
  .splat-track.lit:not(.enemy) .splat-arrow:nth-child(4) .arrow-glow { animation-delay: calc(var(--splat-start-delay) + var(--splat-step-ms)); }
  .splat-track.lit:not(.enemy) .splat-arrow:nth-child(6) .arrow-glow { animation-delay: calc(var(--splat-start-delay) + var(--splat-step-ms) * 2); }
  .splat-track.lit.enemy .splat-arrow:nth-child(5) .arrow-glow { animation-delay: var(--splat-start-delay); }
  .splat-track.lit.enemy .splat-arrow:nth-child(3) .arrow-glow { animation-delay: calc(var(--splat-start-delay) + var(--splat-step-ms)); }
  .splat-track.lit.enemy .splat-arrow:nth-child(1) .arrow-glow { animation-delay: calc(var(--splat-start-delay) + var(--splat-step-ms) * 2); }

  .splat-track.lit:not(.enemy) .splat-wand.flip .wand-glyph { animation: glow-pulse 520ms ease-out both; }
  .splat-track.lit.enemy .splat-wand:not(.flip) .wand-glyph { animation: glow-pulse 520ms ease-out both; }
  .splat-track.lit:not(.enemy) .splat-wand:nth-child(2) .wand-glyph { animation-delay: var(--splat-start-delay); }
  .splat-track.lit:not(.enemy) .splat-wand:nth-child(4) .wand-glyph { animation-delay: calc(var(--splat-start-delay) + var(--splat-step-ms)); }
  .splat-track.lit:not(.enemy) .splat-wand:nth-child(6) .wand-glyph { animation-delay: calc(var(--splat-start-delay) + var(--splat-step-ms) * 2); }
  .splat-track.lit.enemy .splat-wand:nth-child(5) .wand-glyph { animation-delay: var(--splat-start-delay); }
  .splat-track.lit.enemy .splat-wand:nth-child(3) .wand-glyph { animation-delay: calc(var(--splat-start-delay) + var(--splat-step-ms)); }
  .splat-track.lit.enemy .splat-wand:nth-child(1) .wand-glyph { animation-delay: calc(var(--splat-start-delay) + var(--splat-step-ms) * 2); }

  /* Burst-fire timing: 5 rounds per side instead of 3. Uses a shorter,
     tighter step so the whole volley reads as one rapid burst rather
     than 5 evenly-spaced single shots — tighten/loosen via
     --splat-burst-step-ms below. Requires 10 .splat-bullet elements in
     the markup (5 flip + 5 plain, alternating child order 1-10) to
     match — see note where this variable is declared. */
  .splat-track {
    /* 4 gaps across 5 shots = 260ms total, identical to the arrow/wand
       chase's 2-gap 130ms*2 window in combat.js (SPLAT_STEP_MS *
       SPLAT_ARROW_STEPS) — the burst just fits more shots into the
       same time budget instead of needing new JS timing constants. */
    --splat-burst-step-ms: 65ms;
  }
  .splat-track.lit:not(.enemy) .splat-bullet.flip::after { animation: glow-pulse 420ms ease-out both; }
  .splat-track.lit.enemy .splat-bullet:not(.flip)::after { animation: glow-pulse 420ms ease-out both; }
  .splat-track.lit:not(.enemy) .splat-bullet:nth-child(2)::after  { animation-delay: var(--splat-start-delay); }
  .splat-track.lit:not(.enemy) .splat-bullet:nth-child(4)::after  { animation-delay: calc(var(--splat-start-delay) + var(--splat-burst-step-ms)); }
  .splat-track.lit:not(.enemy) .splat-bullet:nth-child(6)::after  { animation-delay: calc(var(--splat-start-delay) + var(--splat-burst-step-ms) * 2); }
  .splat-track.lit:not(.enemy) .splat-bullet:nth-child(8)::after  { animation-delay: calc(var(--splat-start-delay) + var(--splat-burst-step-ms) * 3); }
  .splat-track.lit:not(.enemy) .splat-bullet:nth-child(10)::after { animation-delay: calc(var(--splat-start-delay) + var(--splat-burst-step-ms) * 4); }
  .splat-track.lit.enemy .splat-bullet:nth-child(9)::after { animation-delay: var(--splat-start-delay); }
  .splat-track.lit.enemy .splat-bullet:nth-child(7)::after { animation-delay: calc(var(--splat-start-delay) + var(--splat-burst-step-ms)); }
  .splat-track.lit.enemy .splat-bullet:nth-child(5)::after { animation-delay: calc(var(--splat-start-delay) + var(--splat-burst-step-ms) * 2); }
  .splat-track.lit.enemy .splat-bullet:nth-child(3)::after { animation-delay: calc(var(--splat-start-delay) + var(--splat-burst-step-ms) * 3); }
  .splat-track.lit.enemy .splat-bullet:nth-child(1)::after { animation-delay: calc(var(--splat-start-delay) + var(--splat-burst-step-ms) * 4); }

  /* Triggered by combat.js flashSplat/shakeBox on #enemyViewport (player
     lands a hit) or #avatarWrapper (backfire / enemy counter-attack). */
  @keyframes combat-shake {
    0% { transform: translateX(0); }
    20% { transform: translateX(-6px) rotate(-1deg); }
    40% { transform: translateX(5px) rotate(1deg); }
    60% { transform: translateX(-4px); }
    80% { transform: translateX(3px); }
    100% { transform: translateX(0); }
  }
  .combat-shake {
    animation: combat-shake 0.28s ease;
  }

  /* HIT FLASH: quick red interference flash over the silhouette on a
     landed hit — meant to read as "damage taken right here" while the
     HP bar ticks down alongside it. Lives on the same bordered,
     overflow:hidden boxes .combat-shake already targets (#enemyViewport
     / #avatarWrapper, i.e. .enemy-viewport / .avatar-stage), so the
     flash is naturally clipped to the silhouette's frame with no extra
     markup. Two background layers on the same ::after: a repeating
     stripe pattern (the "scanlines") that slides downward, over a flat
     red tint. mix-blend-mode: screen so it reads as light interference
     against the dark panel background rather than a flat color card
     sitting on top of the art. Trigger by toggling .hit-flash alongside
     .combat-shake — remove/re-add the same way (see shakeBox in
     combat.js) so it can restart mid-flash on a fast follow-up hit. */
  .avatar-stage::after,
  .enemy-viewport::after {
    content: "";
    position: absolute;
    inset: 0;
    z-index: 5;
    pointer-events: none;
    opacity: 0;
    mix-blend-mode: screen;
    background:
      repeating-linear-gradient(
        0deg,
        rgba(255, 255, 255, 0.5) 0px,
        rgba(255, 255, 255, 0.5) 1px,
        transparent 1px,
        transparent 4px
      ),
      rgba(248, 113, 113, 0.6);
    background-size: 100% 8px, 100% 100%;
    background-position: 0 -30%, 0 0;
  }
  @keyframes hit-flash-burst {
    0% { opacity: 0; background-position: 0 -30%, 0 0; }
    20% { opacity: 0.95; }
    55% { opacity: 0.5; background-position: 0 110%, 0 0; }
    100% { opacity: 0; background-position: 0 160%, 0 0; }
  }
  .avatar-stage.hit-flash::after,
  .enemy-viewport.hit-flash::after {
    animation: hit-flash-burst 300ms ease-out both;
  }

  /* BLOCK / MISS EDGE GLOW: a soft light along the inner edge facing the
     attack, reading as "defended" rather than "hit" — used when a swing
     doesn't land. A thin gradient strip on ::before (kept separate from
     ::after above so both can run independently/simultaneously if ever
     needed), positioned on whichever side actually faces the incoming
     attack: left edge for #enemyViewport (attacks travel left-to-right,
     toward the enemy — see .splat-arrow direction), right edge for
     #avatarWrapper (the enemy's counter travels right-to-left, back
     toward the player). Cyan to match the "connected"/idle splat color
     rather than the red hit tone, so it reads as a distinct, cooler
     "deflected" beat instead of a weaker hit. */
  .avatar-stage::before,
  .enemy-viewport::before {
    content: "";
    position: absolute;
    top: 0;
    bottom: 0;
    width: 14px;
    z-index: 5;
    pointer-events: none;
    opacity: 0;
    box-shadow: 0 0 18px 4px rgba(56, 189, 248, 0.55);
  }
  .enemy-viewport::before {
    left: 0;
    background: linear-gradient(90deg, rgba(56, 189, 248, 0.85), transparent);
  }
  .avatar-stage::before {
    right: 0;
    background: linear-gradient(270deg, rgba(56, 189, 248, 0.85), transparent);
  }
  @keyframes block-edge-glow {
    0% { opacity: 0; }
    30% { opacity: 1; }
    100% { opacity: 0; }
  }
  .avatar-stage.block-flash::before,
  .enemy-viewport.block-flash::before {
    animation: block-edge-glow 420ms ease-out both;
  }

  /* Mirrors .avatar-panel: same width, same padding (flipped side),
     same internal gap. .enemy-panel-spacer mirrors .slots-column's
     width so .enemy-viewport lands in the same relative position
     .avatar-stage does, just flipped — genuine mirror, not a guess. */
  .enemy-panel {
    display: flex;
    align-items: stretch;
    gap: 14px; /* mirrors .avatar-body's gap: 14px */
    height: 100%;
    flex-shrink: 0;
    width: 400px; /* mirrors .avatar-panel's width: 400px */
    padding-left: 20px; /* mirrors .avatar-panel's padding-right: 20px */
  }

  .enemy-panel-spacer {
    flex-shrink: 0;
    width: 62px; /* mirrors .equip-slot's width: 62px (slots-column's
                    effective width, since slots stack vertically) */
  }

  .enemy-viewport {
    position: relative;
    flex: 1;
    min-width: 0;
    background: #0a0a0f;
    border-radius: 20px;
    /* Not a chassis LED target — see .avatar-stage above. */
    border: 1px solid #1a1a2e;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    pointer-events: none;
    overflow: hidden;
  }
  .enemy-art-wrap {
    flex: 1;
    width: 100%;
    display: flex;
    align-items: center;
    justify-content: center;
    position: relative;
  }
  .enemy-art-wrap svg {
    /* Relative instead of a fixed 320px, so it scales with whatever
       size .enemy-viewport ends up being (which now matches
       .avatar-stage automatically) rather than floating at a fixed
       size inside a flexible box. */
    width: 80%;
    height: 80%;
    /* Per-monster scale/offset/glow overrides come from enemy-meta.js/json
       via applyEnemyMetadata() in combat.js, set as inline custom
       properties on this element. Fallbacks below reproduce the old
       fixed single-layer-glow look exactly, so any monster without a
       table entry yet renders identically to before this change. */
    transform:
      translate(var(--enemy-offset-x, 0px), var(--enemy-offset-y, 0px))
      scale(var(--enemy-scale, 1))
      rotate(var(--enemy-rotate, 0deg));
    filter:
      drop-shadow(0 0 var(--enemy-glow1, 20px) currentColor)
      drop-shadow(0 0 var(--enemy-glow2, 0px) currentColor);
  }
  .enemy-art-wrap svg #outline,
  .enemy-art-wrap svg .outline {
    stroke: currentColor;
    stroke-width: var(--enemy-stroke-width, 0.5);
    fill: none;
    vector-effect: non-scaling-stroke;
  }
  .enemy-art-wrap svg #mask,
  .enemy-art-wrap svg .mask {
    fill: #0a0a0f;
    stroke: none;
  }
  .enemy-viewport-overlay {
    position: absolute;
    top: 20px;
    left: 20px;
    display: flex;
    flex-direction: column;
    gap: 6px;
    pointer-events: none;
    z-index: 6;
  }
  .enemy-viewport-overlay .enemy-name-label {
    font-size: 14px;
    letter-spacing: 2px;
    color: #f87171;
    text-transform: uppercase;
  }
  .enemy-viewport-overlay .hpbar {
    width: 180px;
    height: 8px;
  }
  .enemy-viewport-placeholder {
    color: #3a3a4a;
    font-size: 10px;
    letter-spacing: 2px;
  }
  .scroll-region {
    position: relative;
    min-height: 0;
  }
  .inv-scroll-region {
    display: flex;
    flex: 1;
    min-height: 0;
    width: 100%;
  }
  .shop-scroll-region,
  .tome-scroll-region {
    display: flex;
    flex: 1;
    min-height: 0;
  }
  #invList {
    margin-top: 5px;
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 8px;
    align-content: start;
    overflow-y: auto;
    flex: 1;
    min-height: 0;
    width: 100%;
    transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1);
  }
  .inv-item {
    background: #0a0a0f;
    border: 1px solid #1a1a2e;
    border-radius: 8px;
    padding: 7px 6px;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 3px;
    min-height: 96px;
    cursor: pointer;
    transition: 0.2s;
    position: relative;
  }
  .inv-item:hover { border-color: #34d399; box-shadow: 0 0 12px rgba(52, 211, 153, 0.2); }
  .inv-item.selected { border-color: #fbbf24; box-shadow: 0 0 12px rgba(251, 195, 59, 0.35); }
  .inv-icon {
    width: 28px;
    height: 26px;
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: visible;
  }
  .inv-icon svg {
    overflow: visible;
    transform: scale(1.8) translateY(70%);
  }

  /* ---------- HOLOGRAPHIC ITEM ICON ---------- */
  /* Add class="inv-icon holo-icon" (or "icon-wrap holo-icon" on equip slots)
     and drop a .holo-beam + .holo-projector + .holo-scan div alongside the
     svg icon, e.g.:
       <div class="inv-icon holo-icon">
         <div class="holo-beam"></div>
         <div class="holo-projector"></div>
         ${iconSvg}
         <div class="holo-scan"></div>
       </div>
  */
  .holo-icon { position: relative; }

  /* Beam/projector live inside this clipping layer instead of inside the
     card itself. This lets the beam get contained to the card's rounded
     border without also clipping the icon SVG (which needs to overflow
     its own tiny box thanks to its scale/translate transform) — the icon
     is a sibling of .holo-clip, never a descendant, so nothing about it
     is ever clipped. */
  .holo-clip {
    position: absolute;
    inset: 0;
    overflow: hidden;
    border-radius: inherit;
    pointer-events: none;
    z-index: 1;
  }

  /* --holo-color is set inline per item (equipment.js) from the item's
     resolved color — which itself comes from the item's rarity tier
     (see RARITIES in items.js) unless the item overrides it directly.
     Falls back to the original cyan if nothing sets it. */
  .holo-projector {
    position: absolute;
    bottom: -12px;
    left: 50%;
    width: 5px;
    height: 5px;
    transform: translateX(-50%);
    border-radius: 50%;
    background: var(--holo-color, #00ffd5);
    box-shadow: 0 0 4px var(--holo-color, #00ffd5), 0 0 10px var(--holo-color, #00ffd5);
    z-index: 1;
    pointer-events: none;
  }

  .holo-beam {
    position: absolute;
    bottom: -8px;
    left: 50%;
    width: 55px;
    height: 42px;
    transform: translateX(-50%);
    clip-path: polygon(44% 100%, 56% 100%, 100% 0%, 0% 0%);
    background: linear-gradient(to top,
      color-mix(in srgb, var(--holo-color, #00ffd5) 45%, transparent),
      color-mix(in srgb, var(--holo-color, #00ffd5) 8%, transparent),
      transparent);
    filter: blur(1.5px);
    opacity: .65;
    z-index: 1;
    pointer-events: none;
  }

  /* Inventory cards: the projector/beam live in .holo-clip, a sibling of
     .inv-icon that spans the full card and clips to it, so they can reach
     down under the sell button without also clipping the icon SVG. */
  .inv-item .holo-clip .holo-beam {
    bottom: -20px;
    width: 60px;
    height: 74px;
  }
  .inv-item .holo-clip .holo-projector {
    bottom: -15px;
  }

  /* --holo-base captures whatever base transform the icon svg already
     needs (scale + translateY), so the float animation adds to it
     instead of overwriting it. Default matches .inv-icon svg; the
     equip-slot override lives near .equip-slot .icon-wrap svg above. */
  .holo-icon svg {
    --holo-base: scale(1.8) translateY(70%);
    position: relative;
    z-index: 5;
    animation: holoFloat 2.8s ease-in-out infinite, holoFlicker 6s linear infinite;
    filter: drop-shadow(0 0 5px currentColor);
  }

  @keyframes holoFloat {
    0%, 100% { transform: var(--holo-base); }
    50%      { transform: var(--holo-base) translateY(-5px); }
  }

  @keyframes holoFlicker {
    0%, 96%, 100% { opacity: 1; }
    97%           { opacity: .5; }
    98%           { opacity: .85; }
    99%           { opacity: .65; }
  }

  .holo-icon .holo-scan {
    position: absolute;
    inset: -4px -8px;
    overflow: hidden;
    pointer-events: none;
    opacity: .25;
    border-radius: 4px;
    z-index: 6;
  }

  .holo-icon .holo-scan::before {
    content: "";
    position: absolute;
    inset: -100% 0;
    background: repeating-linear-gradient(
      to bottom,
      transparent 0px,
      transparent 3px,
      rgba(255,255,255,.18) 4px,
      transparent 5px
    );
    animation: holoScan 5s linear infinite;
  }

  @keyframes holoScan {
    from { transform: translateY(0%); }
    to   { transform: translateY(50%); }
  }

  /* Shop thumbnails sit in a short horizontal row (icon | name | button),
     not a tall card, so the beam needs to be shallow and wide instead of
     tall and narrow, or it bleeds into the next row in the grid. */
  /* .holo-clip now spans the full .shop-item row (like inventory cards do),
     so it clips the beam/projector to the row's rounded border without
     ever touching the icon SVG — that lives in .shop-thumb, a separate
     sibling untouched by this overflow:hidden. Since the clip box is the
     whole row now (not the tiny icon box), "left: 50%" would center the
     beam under the row's midpoint instead of under the icon, so it's
     pinned to the icon's actual horizontal position instead. */
  .shop-item .holo-clip .holo-beam {
    left: 24px;
    bottom: -10px;
    width: 40px;
    height: 34px;
    clip-path: polygon(42% 100%, 58% 100%, 100% 0%, 0% 0%);
  }
  .shop-item .holo-clip .holo-projector {
    left: 24px;
    bottom: -15px;
  }
  .inv-name {
    position: absolute;
    top: 6px;
    left: 50%;
    transform: translateX(-50%);
    width: calc(100% - 12px);
    text-transform: uppercase;
    text-align: center;
    font-size: 11px;
    z-index: 2;
    color:#b8d7ff;
    text-shadow:
        -1px -1px 0 #090912,
         1px -1px 0 #090912,
        -1px  1px 0 #090912,
         1px  1px 0 #090912;
}
  .inv-sell-btn {
    margin-top: auto;
    padding: 3px 7px;
    font-size: 8px;
    letter-spacing: 1px;
    border-width: 1px;
    border-color: #fbbf24;
    color: #fbbf24;
    border-radius: 5px;
    background: transparent;
    margin-bottom: 1px;
    position: relative;
    z-index: 2;
    opacity: 50%;
  }
  .inv-sell-btn:hover {
    background: #fbbf24;
    color: #0a0a0a;
    box-shadow: 0 0 14px rgba(251, 191, 36, 0.25);
    opacity: 50%;
  }
  .inv-sell-btn.inv-sell-btn--ghost {
    display: none;
    pointer-events: none;
  }
  .inv-empty { grid-column: 1 / -1; font-size: 11px; color: #3a3a4a; text-align: center; padding: 20px 0; }
  .inventory-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; gap: 10px; }

  /* Tab row that replaced the plain "⌘ INVENTORY" header - it IS the
     header now. Three equal-width tabs (Gear/Appearance/System) each
     get their own accent color so the active mode reads at a glance;
     colors deliberately echo existing accents used elsewhere (teal =
     primary/gear, violet = rarity/cosmetic, gold = overclock/system). */
  /* Small row for the Gear-tab sort toggle, sitting between the mode
     tabs and the item grid. Hidden outside Gear mode (see JS) since
     type-sorting only makes sense for equippable gear, not the
     Appearance/System inventories which use their own layouts. */
  .inv-toolbar {
    display: flex;
    justify-content: flex-end;
    margin-bottom: 8px;
  }
  .inv-sort-btn {
    padding: 5px 9px;
    font-size: 9px;
    letter-spacing: 1px;
  }
  /* Pressed/active = currently sorted by type, echoing the teal accent
     Gear mode already uses elsewhere (.inv-tab[data-mode="gear"].active). */
  .inv-sort-btn[aria-pressed="true"] {
    border-color: #34d399;
    color: #a7f3d0;
    background: rgba(52, 211, 153, 0.14);
  }
  .inv-sort-btn[aria-pressed="true"]:hover { background: rgba(52, 211, 153, 0.28); color: #a7f3d0; }

  .inv-tabs {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 6px;
    margin-bottom: 14px;
  }
  .inv-tab {
    border: 1px solid #1f2937;
    background: #0a0a0f;
    color: #9ca3af;
    border-radius: 7px;
    padding: 8px 4px;
    font-size: 9px;
    letter-spacing: 1.5px;
    text-align: center;
    cursor: pointer;
    transition: border-color 0.15s ease, color 0.15s ease, box-shadow 0.15s ease, background 0.15s ease;
  }
  .inv-tab:hover { border-color: #4a4a5e; color: #cbd5e1; }

  /* Overclock mode locks every tab but Gear (only gear is overclockable —
     see updateInvTabLockState() in equipment.js). Greyed out, no pointer,
     and the hover rule above is explicitly neutralized here rather than
     relying on `disabled` alone to suppress it — same specificity as
     `.inv-tab:hover` (one class + one pseudo-class each), so source order
     is what makes this win, and the extra `.inv-tab.inv-tab--locked:hover`
     variant guards against that order ever getting shuffled later. */
  .inv-tab--locked {
    cursor: not-allowed;
    opacity: 0.35;
    color: #6b7280;
    border-color: #1f2937;
    background: #0a0a0f;
    box-shadow: none;
  }
  .inv-tab--locked:hover,
  .inv-tab.inv-tab--locked:hover {
    border-color: #1f2937;
    color: #6b7280;
    background: #0a0a0f;
    box-shadow: none;
  }
  .inv-tab[data-mode="gear"].active {
    border-color: #34d399;
    color: #a7f3d0;
    background: rgba(52, 211, 153, 0.14);
    box-shadow: 0 0 12px rgba(52, 211, 153, 0.2);
  }
  .inv-tab[data-mode="appearance"].active {
    border-color: #a78bfa;
    color: #ddd6fe;
    background: rgba(167, 139, 250, 0.14);
    box-shadow: 0 0 12px rgba(167, 139, 250, 0.2);
  }
  .inv-tab[data-mode="system"].active {
    border-color: #fbbf24;
    color: #fde68a;
    background: rgba(251, 191, 36, 0.14);
    box-shadow: 0 0 12px rgba(251, 191, 36, 0.2);
  }

  /* Appearance tab: an owned item currently in use as a transmog
     reference for its slot gets a violet highlight so it reads as
     "active" the same way .equip-slot.filled does in Gear mode. */
  .inv-item.transmog-active {
    border-color: #a78bfa;
    box-shadow: 0 0 10px rgba(167, 139, 250, 0.35);
  }
  .inv-transmog-hint {
    font-size: 9px;
    color: #6b7280;
    text-align: center;
    margin-top: 2px;
  }
  .inv-mode-hint {
    font-size: 11px;
    color: #6b7280;
    padding: 2px 2px 12px;
  }

  /* System tab: LED/paint swatches for whichever loadout slot is
     currently targeted (click a slot on the left in System mode to
     change the target - see .equip-slot.led-target). */
  .led-swatch-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(52px, 1fr));
    gap: 10px;
  }
  .led-swatch {
    width: 100%;
    aspect-ratio: 1;
    border-radius: 8px;
    border: 2px solid #1a1a2e;
    cursor: pointer;
    transition: border-color 0.15s ease, box-shadow 0.15s ease;
  }
  .led-swatch:hover { border-color: #e5e7eb; }
  .led-swatch.active { border-color: #f9fafb; box-shadow: 0 0 10px currentColor; }
  .led-swatch.none-swatch {
    background: #0a0a0f;
    display: flex;
    align-items: center;
    justify-content: center;
    color: #6b7280;
    font-size: 9px;
    letter-spacing: 1px;
  }

  /* Highlights whichever equip-slot is the current LED target while
     System mode is active - same visual language as .filled's
     border/glow, just gold instead of per-item-color. This is the
     "selected" state and, by source order, outranks both .filled's
     rarity color and :hover's green above. Dashed rather than solid on
     purpose: a Legendary item's rarity color is this same gold, so a
     solid border here would be indistinguishable from "filled with a
     Legendary" - dashed (plus a softer, two-layer glow instead of one
     heavy layer) keeps "selected" reading as its own state. */
  .equip-slot.led-target {
    border-style: dashed;
    border-color: #fbbf24;
    box-shadow: 0 0 8px rgba(251, 191, 36, 0.35), 0 0 16px rgba(251, 191, 36, 0.18);
  }

  .shop-toggle-btn, #shopToggleBtn { border-color: #60a5fa; color: #60a5fa; }
  .shop-toggle-btn:hover, #shopToggleBtn:hover { background: #60a5fa; color: #0a0a0a; }
  #overclockBtn { border-color: #fbbf24; color: #fbbf24; }
  #overclockBtn:hover { background: #fbbf24; color: #0a0a0a; }
  #tomeLedgerBtn { border-color: #ef4444; color: #ef4444; }
  #tomeLedgerBtn:hover { background: #ef4444; color: #0a0a0a; }
  .shop-panel { margin-top: 18px; border-top: 1px solid #1a1a2e; padding-top: 14px; display: none; }
  .shop-panel.open { display: flex; flex-direction: column; min-height: 0; }
  .tome-panel { margin-top: 14px; border-top: 1px solid #1a1a2e; padding-top: 14px; display: none; }
  .tome-panel.open { display: flex; flex-direction: column; min-height: 0; }
  .shop-header { display: flex; justify-content: space-between; align-items: center; gap: 12px; margin-bottom: 10px; }
  .shop-header h3 { font-size: 10px; letter-spacing: 2px; margin: 0; color: #4a4a5e; }
  .shop-hint { font-size: 12px; color: #3a3a4a; }
  #shopList {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 18px 10px;
    width: 100%;
    min-height: 0;
    padding-top: 14px;
  }
  .tome-current {
    border: 1px solid #2a1020;
    background: linear-gradient(180deg, rgba(127, 29, 29, 0.22), rgba(10, 10, 15, 0.92));
    border-radius: 9px;
    padding: 9px;
    margin-bottom: 9px;
  }
  .tome-current .tome-title {
    color: #fca5a5;
    font-size: 10px;
    letter-spacing: 1px;
    margin-bottom: 4px;
  }
  .tome-current .tome-time {
    color: #cbd5e1;
    font-size: 9px;
    line-height: 1.6;
  }
  .tome-subtabs {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 7px;
    margin-bottom: 8px;
  }
  .tome-subtab {
    border: 1px solid #1f2937;
    background: #0a0a0f;
    color: #9ca3af;
    border-radius: 7px;
    padding: 6px 8px;
    font-size: 8px;
    letter-spacing: 1px;
  }
  .tome-subtab:hover {
    border-color: #f87171;
    color: #fca5a5;
  }
  .tome-subtab.active {
    border-color: #ef4444;
    color: #fecaca;
    background: rgba(127, 29, 29, 0.24);
    box-shadow: 0 0 12px rgba(239, 68, 68, 0.2);
  }
  #tomeList {
    max-height: 200px;
    overflow-y: auto;
    display: flex;
    flex-direction: column;
    gap: 8px;
    padding-right: 4px;
    width: 100%;
  }
  .tome-entry {
    border: 1px solid #1a1a2e;
    border-radius: 8px;
    background: #0a0a0f;
    padding: 8px;
    font-size: 11px;
    line-height: 1.5;
    color: #e5e7eb;
  }
  .tome-entry .tome-rank {
    color: #fda4af;
    font-size: 9px;
  }
  .tome-entry .tome-title { color: #fca5a5; }
  .tome-entry .tome-time { color: #9ca3af; font-size: 9px; }

  .inventory-panel.tome-mode .inv-scroll-region,
  .inventory-panel.tome-mode .inv-tabs,
  .inventory-panel.tome-mode #invList {
    display: none;
  }

  .inventory-panel.shop-mode .shop-panel,
  .inventory-panel.tome-mode .tome-panel {
    margin-top: 10px;
    padding-top: 10px;
    min-height: 0;
    overflow: hidden;
  }

  .inventory-panel.shop-mode #invList {
    height: 100%;
    overflow-y: auto;
    grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
    gap: 8px;
  }

  .inventory-panel.shop-mode .inv-scroll-region {
    flex: 0 0 44%;
    max-height: 44%;
    min-height: 0;
    width: 100%;
  }

  .inventory-panel.shop-mode .shop-panel {
    flex: 1;
    display: flex;
    flex-direction: column;
  }

  .inventory-panel.tome-mode .tome-panel {
    flex: 1;
    margin-top: 0;
    padding-top: 0;
    border-top: 0;
  }

  .inventory-panel.shop-mode #shopList {
    flex: 1;
    min-height: 0;
    overflow-y: auto;
    align-content: start;
    padding-right: 4px;
  }

  .inventory-panel.tome-mode .tome-scroll-region {
    flex: 1;
    min-height: 0;
  }

  .inventory-panel.tome-mode #tomeList {
    flex: 1;
    min-height: 0;
    max-height: none;
  }

  #invList,
  #shopList,
  #tomeList {
    scrollbar-width: none;
    -ms-overflow-style: none;
  }

  #invList::-webkit-scrollbar,
  #shopList::-webkit-scrollbar,
  #tomeList::-webkit-scrollbar {
    width: 0;
    height: 0;
  }

  .scroll-cue {
    position: absolute;
    right: 4px;
    font-size: 10px;
    color: #34d399;
    text-shadow: 0 0 8px rgba(52, 211, 153, 0.7);
    pointer-events: none;
    z-index: 4;
    display: none;
  }
  .scroll-cue.visible {
    display: block;
  }
  .scroll-cue.bottom {
    bottom: 3px;
  }
  .scroll-cue.top {
    top: 3px;
  }
  .tome-empty { color: #3a3a4a; font-size: 10px; text-align: center; padding: 12px 0; }
  .shop-item { display: flex; justify-content: space-between; align-items: center; gap: 10px; background: #0a0a0f; border: 1px solid #1a1a2e; border-radius: 10px; padding: 10px; font-size: 10px; position: relative; overflow: visible; }
  .shop-item.owned { border-color: #34d399; }
  .shop-thumb {
    width: 28px;
    height: 28px;
    flex-shrink: 0;
    display: flex;
    align-items: center;
    justify-content: center;  
  }
  .shop-thumb svg {
    overflow: visible;
    transform: scale(1.8) translateY(-4%);
  }
  .shop-thumb.holo-icon svg {
    --holo-base: scale(1.8) translateY(-4%);
  }
  .shop-item .shop-info { display: flex; flex-direction: column; gap: 4px; }
  .shop-item .shop-info { flex: 1; min-width: 0; }
  .shop-item .shop-name { color: #e5e7eb; }
  .shop-item .shop-price { color: #fbbf24; }
  .shop-item button { min-width: 80px; padding: 6px 8px; font-size: 9px; letter-spacing: 1px; }
  .shop-item button[disabled] { opacity: 0.35; cursor: default; }
  /* .inv-hint removed - tab bar (.inv-tabs) doubles as the header now, and
     the click-to-equip / click-to-unequip hint text went with it. */

  /* --- BOTTOM ROW: terminal feed (left) + dice pit (bottom right) --- */
  .bottom-row {
    display: flex;
    gap: 20px;
    align-items: stretch;
    padding: 20px;
    border: 1px solid rgba(255, 255, 255, 0.1);
    border-radius: 20px;
    background: rgba(0, 0, 0, 0.15);
    box-shadow: inset 0 2px 10px rgba(0, 0, 0, 0.45);
  }

  .log-panel-wrap {
    flex: 1;
    display: flex;
    flex-direction: column;
    gap: 10px;
    min-width: 0;
  }
  .panel-label {
    margin: 0;
    flex-shrink: 0;
  }

  .log-panel {
    flex: 1;
    display: flex;
    flex-direction: column;
    gap: 10px;
    background: #0d0d12;
    /* Border stays static — the chassis LED only drives the CRT inset
       glow (below) and the scanlines (.log-panel::after), i.e. "the
       screen", not the terminal's frame. */
    border: 1px solid #1E1F1F;
    border-radius: 16px;
    padding: 16px;
    overflow: hidden;
    position: relative;
    isolation: isolate;
    box-shadow:
      0 0 0 1px rgba(0, 0, 0, 0.4),
      0 8px 24px rgba(0, 0, 0, 0.35);
  }

  /* Sits above the CRT glass sheen (::before, z-index 2) and carries
     both the inset screen glow and the scanlines together - both need
     to hue-shift in lockstep with the same LED effect (rainbow, pulse,
     ...), and filter-based animation only shifts what's actually
     painted on the element it's applied to. A real element rather than
     a (now-removed) ::after pseudo-element, so applyRigTheme() can run
     applyLedFx() on it directly - same trick as .chassis-led-frame. */
  .terminal-led-frame {
    position: absolute;
    inset: 0;
    z-index: 3;
    pointer-events: none;
    border-radius: inherit;
    box-shadow:
      inset 0 0 18px var(--sb-chassis-glow, rgba(52, 211, 153, 0.3)),
      inset 0 0 4px var(--sb-chassis-glow-soft, rgba(52, 211, 153, 0.15));
    background: repeating-linear-gradient(
      to bottom,
      rgba(0, 0, 0, 0) 0px,
      rgba(0, 0, 0, 0) 2px,
      var(--sb-chassis-scan, rgba(52, 211, 153, 0.13)) 2px,
      var(--sb-chassis-scan, rgba(52, 211, 153, 0.13)) 4px
    );
    background-size: 100% 4px;
    animation: crt-scan-drift 0.5s steps(8) infinite;
    opacity: 0.85;
  }

  /* CRT glass overlay: faint curved sheen + vignette, sits above content but ignores clicks */
  .log-panel::before {
    content: "";
    position: absolute;
    inset: 0;
    z-index: 2;
    pointer-events: none;
    border-radius: inherit;
    background:
      radial-gradient(ellipse at 30% 20%, rgba(255, 255, 255, 0.06) 0%, rgba(255, 255, 255, 0) 45%),
      radial-gradient(ellipse at center, rgba(0, 0, 0, 0) 55%, rgba(0, 0, 0, 0.35) 100%);
    mix-blend-mode: screen;
  }

  /* Scanlines used to live in a ::after pseudo-element here - moved
     into .terminal-led-frame (a real element, see above) so the same
     applyLedFx() call that animates the glow can animate the scanline
     tint too, in the same pass. */

  @keyframes crt-scan-drift {
    from { background-position: 0 0; }
    to { background-position: 0 4px; }
  }

  /* --- ENEMY VIEWPORT ANIMATIONS (Stage 6) --- */
    /* Composed with the per-monster --enemy-offset-x/-y and --enemy-scale
      custom properties (set from enemy-meta.js/json in applyEnemyMetadata(),
     see .enemy-art-wrap svg above) so a running pose animation's transform
     keyframes don't overwrite them. A CSS animation's transform fully
     replaces any static transform on the same element for as long as it
     runs, so without this, any monster with a pose (i.e. almost all of
     them, since Ground is the default) silently ignored its scale/offset
     the entire time — only a monster with no pose animation ever showed
     those settings. */
  .enemy-hover {
    animation: enemy-hover-float 3s ease-in-out infinite;
  }
  @keyframes enemy-hover-float {
    0%, 100% { transform: translate(var(--enemy-offset-x, 0px), var(--enemy-offset-y, 0px)) scale(var(--enemy-scale, 1)) rotate(var(--enemy-rotate, 0deg)) translateY(0); }
    50% { transform: translate(var(--enemy-offset-x, 0px), var(--enemy-offset-y, 0px)) scale(var(--enemy-scale, 1)) rotate(var(--enemy-rotate, 0deg)) translateY(-15px); }
  }

  .enemy-ground {
    animation: enemy-ground-breath 4s ease-in-out infinite;
    transform-origin: bottom center;
  }
  @keyframes enemy-ground-breath {
    0%, 100% { transform: translate(var(--enemy-offset-x, 0px), var(--enemy-offset-y, 0px)) scale(var(--enemy-scale, 1)) rotate(var(--enemy-rotate, 0deg)) scale(1); }
    50% { transform: translate(var(--enemy-offset-x, 0px), var(--enemy-offset-y, 0px)) scale(var(--enemy-scale, 1)) rotate(var(--enemy-rotate, 0deg)) scale(1.03); }
  }

  /* GLITCH FX (Stage 7) */
  .glitch-fx {
    position: relative;
  }
  .glitch-fx::before,
  .glitch-fx::after {
    content: "";
    position: absolute;
    width: 4px;
    height: 4px;
    background: currentColor;
    opacity: 0;
    z-index: 10;
    pointer-events: none;
  }
  .glitch-fx::before {
    left: var(--fizz-x1, 10%);
    top: var(--fizz-y1, 20%);
    animation: digital-fizz 2s steps(1) infinite;
  }
  .glitch-fx::after {
    left: var(--fizz-x2, 80%);
    top: var(--fizz-y2, 70%);
    animation: digital-fizz 2.5s steps(1) infinite 0.5s;
  }

  @keyframes digital-fizz {
    0%, 95% { opacity: 0; }
    96%, 98% { opacity: 0.8; transform: scaleX(4) translateX(2px); }
    97% { opacity: 1; transform: scaleX(2) translateX(-2px); }
    99% { opacity: 0.5; }
  }

  /* OVERLAY FX (Stage 8) — independent of glitch-fx and of each other; any
     combination can be layered on one monster. All use currentColor so
     they automatically follow --enemy-color the same way glitch-fx does,
     and none of them touch `transform`, so they compose freely with
     .enemy-hover / .enemy-ground without needing the offset/scale
     workaround those two required. */

  /* Ember rise: two drifting sparks. --ember-x1/y1/-x2/y2 are box-relative
     % start points, same convention as --fizz-x1 etc — place them on the
     art itself (e.g. near a foot or a wingtip), not in empty space. */
  .ember-fx { position: relative; }
  .ember-fx::before,
  .ember-fx::after {
    content: "";
    position: absolute;
    width: 3px;
    height: 3px;
    border-radius: 50%;
    background: currentColor;
    box-shadow: 0 0 4px 1px currentColor;
    opacity: 0;
    z-index: 9;
    pointer-events: none;
  }
  .ember-fx::before {
    left: var(--ember-x1, 30%);
    top: var(--ember-y1, 70%);
    animation: ember-rise 2.6s ease-in infinite;
  }
  .ember-fx::after {
    left: var(--ember-x2, 65%);
    top: var(--ember-y2, 75%);
    animation: ember-rise 3.2s ease-in infinite 1.1s;
  }
  @keyframes ember-rise {
    0% { opacity: 0; transform: translateY(0) scale(1); }
    10% { opacity: 0.9; }
    80% { opacity: 0.45; }
    100% { opacity: 0; transform: translateY(-70px) scale(0.4); }
  }

  /* Static flicker: a scanline texture that flickers in and out across
     the whole box — a TV-static / signal-corruption read, not tied to
     any single point on the art so it needs no placement. */
  .static-fx { position: relative; }
  .static-fx::after {
    content: "";
    position: absolute;
    inset: 0;
    background: repeating-linear-gradient(
      0deg, currentColor 0px, currentColor 1px,
      transparent 1px, transparent 3px
    );
    mix-blend-mode: overlay;
    opacity: 0;
    z-index: 9;
    pointer-events: none;
    animation: static-flicker 2.4s steps(1) infinite;
  }
  @keyframes static-flicker {
    0%, 100% { opacity: 0; }
    4% { opacity: 0.35; }
    6% { opacity: 0.05; }
    9% { opacity: 0.4; }
    11% { opacity: 0; }
    52% { opacity: 0.25; }
    54% { opacity: 0; }
  }

  /* Aura pulse: a soft ring expanding outward from center and fading —
     reads as a heartbeat / mana-pulse, also box-centered so no placement
     needed. */
  .pulse-fx { position: relative; }
  .pulse-fx::after {
    content: "";
    position: absolute;
    left: 50%;
    top: var(--pulse-y, 50%);
    width: 40%;
    height: 40%;
    border-radius: 50%;
    border: 1px solid currentColor;
    transform: translate(-50%, -50%) scale(0.6);
    opacity: 0;
    z-index: 8;
    pointer-events: none;
    animation: aura-pulse 2.8s ease-out infinite;
  }
  @keyframes aura-pulse {
    0% { opacity: 0.6; transform: translate(-50%, -50%) scale(0.6); }
    70%, 100% { opacity: 0; transform: translate(-50%, -50%) scale(2.1); }
  }

  .fx-void-bloom { position: relative; }
  .fx-void-bloom::before {
    content: "";
    position: absolute;
    left: 50%;
    top: 55%;
    width: 140%;
    height: 80%;
    transform: translate(-50%, -50%);
    background: radial-gradient(ellipse at center, rgba(125, 211, 252, 0.14) 0%, rgba(167, 139, 250, 0.1) 35%, transparent 70%);
    filter: blur(10px);
    animation: void-bloom-pulse 3.3s ease-in-out infinite;
    pointer-events: none;
    z-index: 7;
  }
  @keyframes void-bloom-pulse {
    0%, 100% { opacity: 0.35; transform: translate(-50%, -50%) scale(0.92); }
    50% { opacity: 0.95; transform: translate(-50%, -50%) scale(1.08); }
  }

  .fx-arc-storm { position: relative; }
  .fx-arc-storm::before,
  .fx-arc-storm::after {
    content: "";
    position: absolute;
    inset: 14% 18%;
    border: 1px solid rgba(125, 211, 252, 0.75);
    border-radius: 999px;
    opacity: 0;
    pointer-events: none;
    z-index: 9;
  }
  .fx-arc-storm::before { animation: arc-storm-ring 2.1s linear infinite; }
  .fx-arc-storm::after { animation: arc-storm-ring 2.1s linear infinite 1s; }
  @keyframes arc-storm-ring {
    0% { opacity: 0; transform: scale(0.5) rotate(0deg); }
    20% { opacity: 0.85; }
    100% { opacity: 0; transform: scale(1.4) rotate(180deg); }
  }

  .fx-rune-field { position: relative; }
  .fx-rune-field::before {
    content: "◇ ◇ ◇";
    position: absolute;
    left: 50%;
    bottom: 8%;
    transform: translateX(-50%);
    letter-spacing: 8px;
    font-size: 10px;
    color: currentColor;
    opacity: 0.7;
    animation: rune-drift 2.8s ease-in-out infinite;
    pointer-events: none;
    z-index: 10;
  }
  @keyframes rune-drift {
    0%, 100% { opacity: 0.25; transform: translateX(-50%) translateY(0); }
    50% { opacity: 0.95; transform: translateX(-50%) translateY(-14px); }
  }

  .fx-ghost-trail svg { animation: ghost-trail-wobble 2.4s ease-in-out infinite; }
  .fx-ghost-trail::after {
    content: "";
    position: absolute;
    inset: 0;
    background: radial-gradient(circle at 45% 60%, rgba(255, 255, 255, 0.2), transparent 55%);
    mix-blend-mode: screen;
    opacity: 0;
    animation: ghost-trail-halo 2.4s ease-in-out infinite;
    pointer-events: none;
    z-index: 7;
  }
  @keyframes ghost-trail-wobble {
    0%, 100% { transform: translate(var(--enemy-offset-x, 0px), var(--enemy-offset-y, 0px)) scale(var(--enemy-scale, 1)) rotate(var(--enemy-rotate, 0deg)); }
    50% { transform: translate(calc(var(--enemy-offset-x, 0px) + 4px), calc(var(--enemy-offset-y, 0px) - 5px)) scale(var(--enemy-scale, 1)) rotate(calc(var(--enemy-rotate, 0deg) + 1.5deg)); }
  }
  @keyframes ghost-trail-halo {
    0%, 100% { opacity: 0.15; }
    50% { opacity: 0.65; }
  }

  .fx-data-drip { position: relative; }
  .fx-data-drip::before,
  .fx-data-drip::after {
    content: "";
    position: absolute;
    top: -12px;
    width: 2px;
    height: 48px;
    background: linear-gradient(to bottom, rgba(125, 211, 252, 0), rgba(125, 211, 252, 0.95), rgba(125, 211, 252, 0));
    opacity: 0;
    pointer-events: none;
    z-index: 9;
  }
  .fx-data-drip::before { left: 32%; animation: data-drip-fall 1.8s linear infinite; }
  .fx-data-drip::after { left: 68%; animation: data-drip-fall 2.2s linear infinite 0.7s; }
  @keyframes data-drip-fall {
    0% { opacity: 0; transform: translateY(-20px); }
    15% { opacity: 0.9; }
    100% { opacity: 0; transform: translateY(360px); }
  }

  /* Chromatic corrupt: a brief red/cyan channel-split flash on the SVG
     itself (filter-based, so it has to live on the svg, not the wrapper —
     ::before/::after don't render reliably on SVG-namespace elements, see
     the glitch-fx note above). It replays the base glow drop-shadows every
     frame and layers the split shadows on top only during the flash beats,
     so the constant glow never disappears when this fires. */
  .corrupt-fx svg {
    animation: chroma-corrupt 3.4s steps(1) infinite;
  }
  @keyframes chroma-corrupt {
    0%, 92%, 100% {
      filter:
        drop-shadow(0 0 var(--enemy-glow1, 6px) var(--enemy-glow-rgba, rgba(248,113,113,1)))
        drop-shadow(0 0 var(--enemy-glow2, 14px) var(--enemy-glow-rgba, rgba(248,113,113,1)));
    }
    93% {
      filter:
        drop-shadow(0 0 var(--enemy-glow1, 6px) var(--enemy-glow-rgba, rgba(248,113,113,1)))
        drop-shadow(0 0 var(--enemy-glow2, 14px) var(--enemy-glow-rgba, rgba(248,113,113,1)))
        drop-shadow(3px 0 0 rgba(255,60,60,0.8))
        drop-shadow(-3px 0 0 rgba(60,255,255,0.8));
    }
    94% {
      filter:
        drop-shadow(0 0 var(--enemy-glow1, 6px) var(--enemy-glow-rgba, rgba(248,113,113,1)))
        drop-shadow(0 0 var(--enemy-glow2, 14px) var(--enemy-glow-rgba, rgba(248,113,113,1)))
        drop-shadow(-4px 0 0 rgba(255,60,60,0.8))
        drop-shadow(4px 0 0 rgba(60,255,255,0.8));
    }
    95% {
      filter:
        drop-shadow(0 0 var(--enemy-glow1, 6px) var(--enemy-glow-rgba, rgba(248,113,113,1)))
        drop-shadow(0 0 var(--enemy-glow2, 14px) var(--enemy-glow-rgba, rgba(248,113,113,1)));
    }
  }

  /* Optional metadata-driven enemy death FX (played client-side when a
     kill resolves, before the combat teardown hides the viewport). */
  .death-fx-explode svg {
    animation: enemy-death-explode 620ms ease-out forwards;
    transform-origin: center center;
  }
  @keyframes enemy-death-explode {
    0% {
      opacity: 1;
      filter: contrast(1);
    }
    40% {
      opacity: 1;
      transform: translate(var(--enemy-offset-x, 0px), var(--enemy-offset-y, 0px)) scale(calc(var(--enemy-scale, 1) * 1.14));
      filter: brightness(1.8);
    }
    100% {
      opacity: 0;
      transform: translate(var(--enemy-offset-x, 0px), calc(var(--enemy-offset-y, 0px) - 22px)) scale(calc(var(--enemy-scale, 1) * 0.32));
      filter: blur(2px) brightness(2.2);
    }
  }

  .death-fx-scanline {
    position: relative;
  }
  .death-fx-scanline::after {
    content: "";
    position: absolute;
    inset: 0;
    pointer-events: none;
    background:
      repeating-linear-gradient(0deg, rgba(255, 255, 255, 0.55) 0px, rgba(255, 255, 255, 0.55) 1px, transparent 1px, transparent 4px),
      rgba(248, 113, 113, 0.45);
    mix-blend-mode: screen;
    animation: enemy-death-scanline 760ms ease-out forwards;
    z-index: 12;
  }
  .death-fx-scanline svg {
    animation: enemy-death-scanline-art 760ms ease-out forwards;
  }
  @keyframes enemy-death-scanline {
    0% {
      opacity: 0;
      background-position: 0 -20%;
    }
    25% {
      opacity: 1;
    }
    100% {
      opacity: 0;
      background-position: 0 140%;
    }
  }
  @keyframes enemy-death-scanline-art {
    0% {
      opacity: 1;
    }
    100% {
      opacity: 0;
      transform: translate(var(--enemy-offset-x, 0px), var(--enemy-offset-y, 0px)) scale(var(--enemy-scale, 1));
    }
  }

  .death-fx-shatter svg {
    animation: enemy-death-shatter 720ms ease-out forwards;
    transform-origin: center center;
  }
  @keyframes enemy-death-shatter {
    0% { opacity: 1; clip-path: inset(0 0 0 0); }
    35% { clip-path: inset(10% -8% 20% -8%); transform: translate(var(--enemy-offset-x, 0px), var(--enemy-offset-y, 0px)) scale(calc(var(--enemy-scale, 1) * 1.08)) rotate(calc(var(--enemy-rotate, 0deg) + 4deg)); }
    100% { opacity: 0; clip-path: inset(100% -45% 0 -45%); transform: translate(var(--enemy-offset-x, 0px), calc(var(--enemy-offset-y, 0px) - 14px)) scale(calc(var(--enemy-scale, 1) * 0.42)) rotate(calc(var(--enemy-rotate, 0deg) + 28deg)); }
  }

  .death-fx-phase-melt svg { animation: enemy-death-phase-melt 840ms ease-out forwards; }
  @keyframes enemy-death-phase-melt {
    0% { opacity: 1; filter: blur(0) brightness(1); }
    45% { opacity: 0.9; filter: blur(1.4px) brightness(1.8); transform: translate(var(--enemy-offset-x, 0px), var(--enemy-offset-y, 0px)) scale(calc(var(--enemy-scale, 1) * 1.05)) rotate(var(--enemy-rotate, 0deg)); }
    100% { opacity: 0; filter: blur(6px) brightness(2.5); transform: translate(calc(var(--enemy-offset-x, 0px) + 18px), calc(var(--enemy-offset-y, 0px) + 18px)) scale(calc(var(--enemy-scale, 1) * 0.2)) rotate(calc(var(--enemy-rotate, 0deg) - 14deg)); }
  }

  .death-fx-glitch-collapse svg { animation: enemy-death-glitch-collapse 680ms steps(2, end) forwards; }
  @keyframes enemy-death-glitch-collapse {
    0% { opacity: 1; filter: contrast(1.1); }
    25% { transform: translate(calc(var(--enemy-offset-x, 0px) + 4px), calc(var(--enemy-offset-y, 0px) - 2px)) scale(var(--enemy-scale, 1)) rotate(calc(var(--enemy-rotate, 0deg) + 2deg)); }
    55% { transform: translate(calc(var(--enemy-offset-x, 0px) - 6px), calc(var(--enemy-offset-y, 0px) + 3px)) scale(calc(var(--enemy-scale, 1) * 1.12)) rotate(calc(var(--enemy-rotate, 0deg) - 3deg)); }
    100% { opacity: 0; transform: translate(var(--enemy-offset-x, 0px), calc(var(--enemy-offset-y, 0px) + 26px)) scale(calc(var(--enemy-scale, 1) * 0.1)) rotate(calc(var(--enemy-rotate, 0deg) + 14deg)); }
  }

  .death-fx-void-burn { position: relative; }
  .death-fx-void-burn::after {
    content: "";
    position: absolute;
    inset: 0;
    background: radial-gradient(circle at center, rgba(255, 255, 255, 0.5), rgba(167, 139, 250, 0.35) 40%, rgba(0, 0, 0, 0.9) 72%);
    animation: enemy-death-void-burn 900ms ease-out forwards;
    pointer-events: none;
    z-index: 12;
  }
  .death-fx-void-burn svg { animation: enemy-death-void-burn-art 900ms ease-out forwards; }
  @keyframes enemy-death-void-burn {
    0% { opacity: 0; }
    35% { opacity: 1; }
    100% { opacity: 0; }
  }
  @keyframes enemy-death-void-burn-art {
    0% { opacity: 1; }
    100% { opacity: 0; transform: translate(var(--enemy-offset-x, 0px), var(--enemy-offset-y, 0px)) scale(calc(var(--enemy-scale, 1) * 0.02)); }
  }

  /* Pinned top region: location/enemy name (always populated) + tier tally.
     Never scrolls, never shrinks. Flows straight into the log below —
     no divider here, the log-header and #log read as one continuous surface. */
  .log-header {
    flex-shrink: 0;
    display: flex;
    flex-direction: column;
    gap: 8px;
    opacity: 80%;
  }

  .enemy-bar {
    display: none; /* Extracted to viewport Stage 5 */
    align-items: center;
    justify-content: flex-start;
    gap: 14px;
    min-height: 26px;
    font-size: 12px;
    color: #f87171;
    white-space: nowrap;
    overflow: hidden;
    opacity: 60%;
  }
  .enemy-bar.is-empty {
    visibility: hidden;
  }
  .enemy-bar.is-defeat {
    overflow: visible;
    padding-left: 10px;
  }
  .enemy-bar.is-defeat .hpbar {
    display: none;
  }
  .enemy-bar.is-defeat #enemyName {
    max-width: none;
    overflow: visible;
    font-size: 30px;
    line-height: 1;
    color: #ef4444;
    text-shadow: 0 0 8px rgba(239, 68, 68, 0.9), 0 0 22px rgba(239, 68, 68, 0.55);
    animation: skull-pulse 1.6s ease-in-out infinite;
  }
  @keyframes skull-pulse {
    0%, 100% { transform: scale(1); opacity: 0.85; }
    50% { transform: scale(1.18); opacity: 1; }
  }
  .enemy-core {
    display: flex;
    align-items: center;
    gap: 10px;
    min-width: 0;
    flex: 1;
  }
  #enemyName {
    max-width: 180px;
    overflow: hidden;
    text-overflow: ellipsis;
  }
  .enemy-bar .hpbar { width: 120px; height: 8px; }

  .header-top-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 14px;
  }

  /* Always-on location/tier label. Flexes to fill the row, tier indicator
     sits to its right. Never hidden. */
  .location-bar {
    display: flex;
    align-items: center;
    flex: 1;
    min-width: 0;
    min-height: 18px;
    font-size: 11px;
    letter-spacing: 1px;
    color: #34d399;
    white-space: nowrap;
    overflow: hidden;
  }
  #locationName {
    overflow: hidden;
    text-overflow: ellipsis;
    opacity: 70%;
  }

  .tier-indicator {
    flex-shrink: 0;
    min-height: 24px;
    display: flex;
    align-items: center;
  }
  #tierTally {
    width: 92px;
    height: 20px;
    overflow: visible;
  }
  .tier-tally-shadow {
    fill: none;
    stroke: rgba(10, 10, 15, 0.9);
    stroke-width: 4.4;
    stroke-linecap: round;
    opacity: 0.95;
    filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0.85));
    transform: translate(0.9px, 0.9px);
  }
  .tier-tally-stroke {
    fill: none;
    stroke: #ef4444;
    stroke-width: 2.35;
    stroke-linecap: round;
    filter: drop-shadow(0 0 6px rgba(239, 68, 68, 0.95));
  }
  .tier-tally-stroke.is-drawing {
    stroke-dasharray: 1;
    stroke-dashoffset: 1;
    animation: tally-burn-in 220ms ease-out forwards;
  }
  @keyframes tally-burn-in {
    from {
      stroke-dashoffset: 1;
      opacity: 0.3;
    }
    to {
      stroke-dashoffset: 0;
      opacity: 1;
    }
  }

  #log {
    flex: 0 0 auto;
    height: 100px; /* ~4 lines at 14px / line-height 1.7 */
    font-size: 14px;
    line-height: 1.7;
    white-space: pre-wrap;
    padding: 4px 2px;
    overflow-y: auto;
    overflow-x: hidden;
    scrollbar-width: none;
    -ms-overflow-style: none;
    -webkit-overflow-scrolling: touch;
    overscroll-behavior: contain;
    touch-action: pan-y;
    opacity: 60%;
  }
  #log::-webkit-scrollbar { display: none; }

  /* --- LOG GLITCH EFFECTS --- */
  .death-skull {
    display: inline-block;
    font-size: 42px;
    color: #ef4444;
    text-shadow: 0 0 15px #ef4444, 0 0 30px #ef4444;
    animation: death-skull-pulse 1.8s ease-in-out infinite;
    margin-top: 8px;
    line-height: 1;
    vertical-align: middle;
  }

  .critical-hit-label {
    color: #fbbf24;
    text-shadow: 0 0 6px rgba(251, 191, 36, 0.75), 0 0 12px rgba(251, 191, 36, 0.4);
    font-weight: 700;
    letter-spacing: 0.6px;
  }

  @keyframes death-skull-pulse {
    0%, 100% { transform: scale(1); opacity: 1; text-shadow: 0 0 15px #ef4444, 0 0 30px #ef4444; }
    50% { transform: scale(1.15); opacity: 0.8; text-shadow: 0 0 25px #ef4444, 0 0 50px #ef4444; }
  }

  /* Mistyped-letter glitch: stays the standard log colour (inherits from
     #log / body), just gets a quick swipe/blur when it corrects itself. */
  .glitch-char {
    display: inline-block;
  }
  .glitch-swipe {
    animation: glitch-swipe-anim 0.15s steps(2, end);
  }
  @keyframes glitch-swipe-anim {
    0%   { transform: translateX(-2px); filter: blur(1px); }
    100% { transform: translateX(0);    filter: blur(0); }
  }

  /* Fake-word glitch: the intrusive message, in red, fading into the real word. */
  .glitch-word {
    color: #ef4444;
    text-shadow: 0 0 6px rgba(239, 68, 68, 0.75);
    transition: opacity 0.35s ease;
    opacity: 1;
  }
  .glitch-fade-out { opacity: 0; }
  .glitch-fade-in {
    opacity: 1;
    color: inherit;
    text-shadow: none;
  }
  .icon { width: 60px; height: 30px; vertical-align: -10px; color: #FFFFFF; }

  /* --- BLACKMARKET (Auction House entry point) --- */

  .blackmarket-btn {
    flex-shrink: 0;
    background: none;
    border: none;
    color: #ef4444;
    font-size: 15px;
    line-height: 1;
    cursor: pointer;
    padding: 2px 4px;
    opacity: 0.55;
    text-shadow: 0 0 6px rgba(239, 68, 68, 0.7);
    transition: opacity 0.2s ease, transform 0.2s ease;
  }
  .blackmarket-btn:hover { opacity: 1; transform: scale(1.12); }
  .blackmarket-btn:disabled { opacity: 0.3; cursor: default; transform: none; }

  /* Covers the whole .game-container — sits above .chassis-led-frame
     (z-index: 25) so nothing in the game UI can show through — and
     unrolls downward from the very top of it.

     inset: -30px (= .game-container's 28px padding + 2px border) pushes
     this element's own edge out to sit flush with .game-container's
     OUTER edge, i.e. right on top of the base chassis border, instead of
     stopping at the padding edge the way inset:0 would. Combined with
     the border below, that's how the whole window's border reads as red
     while blackmarket is open — we're painting over the base border,
     not recoloring var(--sb-chassis-color). */
  .blackmarket-overlay {
    position: absolute;
    inset: -30px;
    z-index: 40;
    border-radius: 16px; /* matches .game-container's own outer corner radius */
    border: 2px solid #ef4444;
    box-shadow: 0 0 30px rgba(239, 68, 68, 0.35), inset 0 0 30px rgba(239, 68, 68, 0.06);
    background: rgba(6, 6, 10, 0.94);
    backdrop-filter: blur(1px);
    transform: scaleY(0);
    transform-origin: top;
    transition: transform 0.45s cubic-bezier(0.65, 0, 0.35, 1);
    pointer-events: none;
    overflow: hidden;
    display: flex;
    flex-direction: column;
  }
  .blackmarket-overlay.open {
    transform: scaleY(1);
    pointer-events: auto;
  }

  .blackmarket-skull-flash {
    position: absolute;
    inset: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 14px;
    z-index: 2;
    /* Purely decorative — without this, the invisible flash layer sits on
       top of .blackmarket-content (even at opacity: 0) and silently
       swallows every click meant for the close button underneath it. */
    pointer-events: none;
  }
  .blackmarket-skull-flash .skull-glyph {
    font-size: 96px;
    line-height: 1;
    color: #ef4444;
    text-shadow: 0 0 18px #ef4444, 0 0 40px rgba(239, 68, 68, 0.7);
    opacity: 0;
  }
  .blackmarket-skull-flash .skull-label {
    font-size: 14px;
    letter-spacing: 2px;
    color: #ef4444;
    text-shadow: 0 0 8px rgba(239, 68, 68, 0.8);
    opacity: 0;
  }
  /* 3 flashes: one flicker keyframe (below) repeated via iteration-count
     rather than hand-encoding 3 flashes into one timeline — tune the
     flash by changing SKULL_FLASH_DURATION_MS in js/blackmarket.js
     (must stay equal to this animation-duration) and/or the iteration
     count in both places. */
  .blackmarket-skull-flash.flash .skull-glyph {
    animation: blackmarket-skull-flicker 0.7s steps(1, end) 3 forwards;
  }
  .blackmarket-skull-flash.flash .skull-label {
    animation: blackmarket-label-pulse 2.1s ease-in-out forwards;
  }
  /* Deliberately jittery/uneven timing + a couple of horizontal jumps —
     reads as corrupted rather than a clean fade. */
  @keyframes blackmarket-skull-flicker {
    0%   { opacity: 1;   transform: translateX(0); }
    15%  { opacity: 0;   transform: translateX(0); }
    30%  { opacity: 1;   transform: translateX(-6px); }
    45%  { opacity: 0.2; transform: translateX(4px); }
    60%  { opacity: 1;   transform: translateX(0); }
    85%  { opacity: 0.6; transform: translateX(-2px); }
    100% { opacity: 0;   transform: translateX(0); }
  }
  @keyframes blackmarket-label-pulse {
    0%   { opacity: 0; }
    8%   { opacity: 1; }
    92%  { opacity: 1; }
    100% { opacity: 0; }
  }

  .blackmarket-content {
    position: relative;
    z-index: 1;
    width: 100%;
    height: 100%;
    min-height: 0;
    display: flex;
    flex-direction: column;
    padding: 16px 18px;
    opacity: 0;
    transition: opacity 0.35s ease;
  }
  .blackmarket-content.visible { opacity: 1; }

  /* Row below the header: listings pushed to the left, route-runner
     docked on the right, with .blackmarket-divider between them. */
  .blackmarket-main {
    flex: 1;
    min-height: 0;
    display: flex;
    gap: 14px;
  }

  .blackmarket-divider {
    flex-shrink: 0;
    width: 1px;
    align-self: stretch;
    background: linear-gradient(to bottom, transparent, rgba(239, 68, 68, 0.5), transparent);
  }

  /* Standalone route-runner.html, embedded as-is via iframe so it can
     keep being edited on its own without touching this file. */
  .blackmarket-side {
    flex: 1;
    min-width: 0;
    min-height: 0;
    display: flex;
  }
  .route-runner-frame {
    flex: 1;
    width: 100%;
    height: 100%;
    border: none;
    border-radius: 8px;
    background: #05080a; /* route-runner.html's own --bg, avoids a white flash while it loads */
  }

  .blackmarket-header {
    flex-shrink: 0;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    margin-bottom: 8px;
  }
  .blackmarket-header h3 {
    margin: 0;
    font-size: 13px;
    letter-spacing: 1px;
    color: #ef4444;
    text-shadow: 0 0 8px rgba(239, 68, 68, 0.6);
  }
  .blackmarket-close-btn {
    background: none;
    border: none;
    color: inherit;
    opacity: 0.6;
    cursor: pointer;
    font-size: 13px;
    padding: 2px 6px;
  }
  .blackmarket-close-btn:hover { opacity: 1; }

  .blackmarket-body {
    flex: 0 0 340px;
    max-width: 42%;
    min-height: 0;
    display: flex;
    flex-direction: column;
    gap: 8px;
    font-size: 12px;
    color: #cbd5e1;
  }

  .auction-list-form {
    flex-shrink: 0;
    display: flex;
    gap: 6px;
  }
  .auction-item-select,
  .auction-price-input {
    background: #0a0a0f;
    border: 1px solid #1a1a2e;
    border-radius: 6px;
    color: #cbd5e1;
    font-size: 11px;
    padding: 5px 6px;
  }
  .auction-item-select { flex: 1; min-width: 0; }
  .auction-price-input { width: 90px; }
  .auction-list-btn { flex-shrink: 0; padding: 5px 10px; font-size: 11px; }

  .auction-status {
    flex-shrink: 0;
    min-height: 14px;
    font-size: 11px;
    color: #ef4444;
  }
  .auction-status.ok { color: #4ade80; }

  .auction-scroll-region {
    display: flex;
    flex: 1;
    min-height: 0;
  }
  #auctionList {
    display: flex;
    flex-direction: column;
    gap: 6px;
    overflow-y: auto;
    flex: 1;
    min-height: 0;
    width: 100%;
  }
  .auction-empty {
    color: #3a3a4a;
    font-size: 11px;
    letter-spacing: 1px;
    text-align: center;
    padding: 16px 0;
  }

  .auction-row {
    background: #0a0a0f;
    border: 1px solid #1a1a2e;
    border-radius: 8px;
    padding: 7px 8px;
    display: flex;
    flex-direction: column;
    gap: 4px;
  }
  .auction-row.mine { border-color: rgba(239, 68, 68, 0.4); }
  .auction-row-top {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 8px;
  }
  .auction-item-name { color: #e2e8f0; font-size: 12px; }
  .auction-timer { font-size: 10px; color: #94a3b8; letter-spacing: 0.5px; white-space: nowrap; }
  .auction-timer.ending-soon { color: #ef4444; }
  .auction-row-mid {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    font-size: 10px;
    color: #94a3b8;
  }
  .auction-row-bottom { display: flex; gap: 6px; align-items: center; }
  .auction-bid-input {
    width: 70px;
    background: #06060a;
    border: 1px solid #1a1a2e;
    border-radius: 6px;
    color: #cbd5e1;
    font-size: 11px;
    padding: 4px 6px;
  }
  .auction-bid-btn,
  .auction-cancel-btn {
    flex-shrink: 0;
    padding: 4px 8px;
    font-size: 10px;
  }
  .auction-cancel-btn { color: #ef4444; border-color: rgba(239, 68, 68, 0.5); }
  .auction-tag {
    font-size: 9px;
    letter-spacing: 1px;
    color: #4ade80;
  }

  /* Pinned bottom region: action buttons. Never scrolls, never shrinks. */
  .btn-row {
    flex-shrink: 0;
    display: flex;
    gap: 8px;
    flex-wrap: nowrap;
    padding-top: 10px;
    border-top: 1px solid rgba(255, 255, 255, 0.00);
  }
  .btn-row.single-return {
    justify-content: center;
  }
  .btn-row button {
    flex: 1 1 0;
    min-width: 0;
    padding: 10px 12px;
    font-size: 12px;
    letter-spacing: 1px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    opacity: 40%;
  }
  .btn-row.single-return button {
    flex: 0 0 auto;
  }
  button {
    background: transparent;
    border: 2px solid #34d399;
    color: #34d399;
    padding: 10px 24px;
    font-family: 'Courier New', monospace;
    font-size: 13px;
    font-weight: bold;
    cursor: pointer;
    transition: 0.2s;
    letter-spacing: 2px;
    border-radius: 6px;
    /* Fixes a mobile bug: a tap-and-hold-then-release was triggering the
       browser's long-press menu (e.g. "Search") instead of the button's
       click, because the button's text was selectable. Disabling
       selection and iOS's callout menu on every button removes that
       browser gesture so only the game's own click handler fires. */
    -webkit-touch-callout: none;
    -webkit-user-select: none;
    user-select: none;
  }
  button:hover { background: #34d399; color: #0a0a0a; box-shadow: 0 0 30px rgba(52, 211, 153, 0.3); }
  button:disabled { opacity: 0.4; cursor: default; pointer-events: none; }
  #fleeBtn { border-color: #7a7a8e; color: #7a7a8e; }
  #fleeBtn:hover { background: #7a7a8e; color: #0a0a0a; }
  #goDeeperBtn { border-color: #a78bfa; color: #a78bfa; }
  #goDeeperBtn:hover { background: #a78bfa; color: #0a0a0a; }

  #goDeeperBtn.tome-challenge { border-color: #ef4444; color: #ef4444; }
  #goDeeperBtn.tome-challenge:hover { background: #ef4444; color: #0a0a0a; }

  /* Footer strip: cosmetic shop entry point (future Stripe MTX). Sits below
     the main box as its own element, not part of the bordered card. */
  .log-footer {
    flex-shrink: 0;
    background: #1a1a2e;
    border: none;
    padding: 8px 14px;
    border-radius: 8px;
    font-family: 'Courier New', monospace;
    font-size: 10px;
    font-weight: normal;
    letter-spacing: normal;
    color: #4a4a5e;
    text-align: center;
    min-height: 34px;
    cursor: pointer;
    transition: 0.2s;
  }
  .log-footer:hover {
    background: #24243a;
    color: #6a6a80;
    box-shadow: none;
  }
  .log-footer span { color: #ec4899; }

  /* --- DICE PIT (bowl-shaped, bottom right) --- */
  .dice-panel-wrap {
    flex-shrink: 0;
    width: 320px;
    display: flex;
    flex-direction: column;
    gap: 10px;
  }

  .dice-pit {
    flex: 1;
    width: 100%;
    background: #0d0d12;
    /* Not a chassis LED target — see .avatar-stage above. */
    border: 1px solid #1E1F1F;
    border-radius: 16px;
    padding: 16px;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    overflow: hidden;
    box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.4), 0 8px 24px rgba(0, 0, 0, 0.35);
  }

  .pit-bowl {
    position: relative;
    width: 100%;
    height: 180px;
    border-radius: 50% / 38%;
    background: radial-gradient(ellipse at center, #050506 0%, #0d0d12 65%, #121214 100%);
    box-shadow:
      inset 0 10px 26px rgba(0, 0, 0, 0.75),
      inset 0 -6px 14px rgba(52, 211, 153, 0.05),
      0 0 0 1px #1a1a2e;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 26px;
    overflow: visible;
  }

  /* The dice pit's contents are now a live three.js <canvas>,
     injected by js/dice.js. It just needs to fill the bowl. */
  #diceCanvas {
    width: 100%;
    height: 100%;
    display: block;
  }

  .die-result {
    font-size: 12px;
    color: #4a4a5e;
    margin-top: 12px;
    text-align: center;
  }
  .die-result .highlight { color: #fbbf24; font-weight: bold; }

  /* ============================================================
     TOUCH / MOBILE ADJUSTMENTS
     Targets touch devices via `pointer: coarse` rather than a
     width breakpoint, since the page has no <meta viewport> tag
     and phones report a fake ~980px width regardless of the real
     screen size (see chat) — a width-based @media wouldn't fire
     reliably. pointer: coarse fires based on actual input
     hardware, so it works either way.
     ============================================================ */
  @media (hover: none) and (pointer: coarse) {
    /* Stack terminal feed above dice pit instead of side-by-side.
       DOM order already has .log-panel-wrap before .dice-panel-wrap,
       so no `order` property needed — just switch the axis and let
       the dice panel go full width instead of its fixed 320px. */
    .bottom-row {
      flex-direction: column;
    }
    .dice-panel-wrap {
      width: 50%;
      margin: 0 auto;
    }
    /* PREVIOUS ATTEMPT (removed): making combat-splat-zone position:relative
       and width:100% turned it into a third item in .character-panels'
       flex ROW — which only has room budgeted for the avatar box and
       inventory box, so it squeezed into the enemy box's spot instead
       of dropping to its own line. That's what broke enemy/inventory.

       FIX: keep combat-splat-zone position: absolute (so it's taken out
       of the flex row entirely — it can't disturb avatar-panel /
       inventory-column's layout at all), but re-target which ancestor
       it's absolute *relative to*. On desktop it's positioned against
       .character-panels (the 500px-tall row) to sit between the two
       side panels. Here, .character-panels is switched to
       position: static, so the browser walks up to the next positioned
       ancestor instead — .character-mega-panel, made position: relative
       below — landing combat-splat-zone in the extra bottom padding
       we reserve on that outer box instead of inside the fixed-height
       row. This doesn't touch avatar-panel/inventory-column/enemy-panel
       at all, so their internal 500px math (which their absolutely
       positioned children rely on) stays completely undisturbed. */
    .character-mega-panel {
      position: relative;
      padding-bottom: 120px;
    }
    .character-panels {
      position: static;
    }
    .combat-splat-zone {
      top: auto;
      left: 16px;
      right: 16px;
      bottom: 14px;
      height: 90px;
    }
  }
