* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
  -webkit-tap-highlight-color: transparent;
}

html,
body {
  width: 100%;
  height: 100%;
  overflow: hidden;
  background: #0c0f08;
  touch-action: manipulation;
  user-select: none;
  -webkit-user-select: none;
  font-family: "Courier New", ui-monospace, "Segoe UI Mono", monospace;
  overscroll-behavior: none;
  image-rendering: pixelated;
  image-rendering: crisp-edges;
  font-synthesis: none;
}

/* HUD density knob. --hud is a unitless scale (0.5…1) set live on resize by
   main.js so every pixel-art widget (labels, badges, slots) shrinks with the
   window on any platform. It applies to the *fixed* HUD kit only; full-screen
   panels and overlays size themselves to the viewport instead. The 1 here is
   just the pre-JS default. */
:root {
  --hud: 1;
  /* Vertical clearance left below the top bar cluster, in px (see media
     blocks below, where the top bar re-stacks into two rows on phones).
     Referenced by fixed HUD widgets; those widgets are zoom(scaled) so the
     offset they land at equals var×hud, matching the scaled top bar. */
  --topbar-h: 92px;
  /* ALWAYS-ON OBJECTIVE STRIP (Q5, round 10) — the #obj-strip band below the
     top bar. Four vars, all PRE-JS FALLBACKS ONLY exactly like --party-t /
     --timer-t further down: js/quest.js refreshStrip() measures the strip's
     REAL rect on every quest change / world entry / resize and overwrites the
     two `-live` values with the exact delta each consumer needs.
       --objstrip-h    zoom-local px — added to the `zoom: var(--hud)`
                       surfaces (#toasts, #quest-log) so they land 8 screen px
                       UNDER the strip at every HUD scale;
       --objstrip-hs   screen px — the same delta for the ONE un-zoomed
                       consumer (#ach-toasts).
     The `body.in-game.obj-strip-on` gate below is what selects them: outside
     the world — or with the flag off / no active quest — both resolve to 0px,
     so every consumer computes exactly today's value (byte-for-byte legacy
     HUD). The consumers that can MEASURE instead do: #event-timer through
     --timer-t, the co-op roster anchor and js/first.js guidePlace's floor all
     read the real #obj-strip rect (js/ui.js / js/first.js). */
  --objstrip-h-live: 0px;
  --objstrip-hs-live: 0px;
  --objstrip-h: 0px;
  --objstrip-hs: 0px;
  /* Reserved band under the top bar owned by the transient BOSS HP bar
     (#boss-hud-bar: `top: 58px` + its name line + the 10px bar + air). The
     objective strip never lands inside it — on a phone `--topbar-h × hud`
     would otherwise drop the strip's band to ~80px, i.e. straight under a
     boss bar that is only up during a fight. #obj-strip takes the max() of
     this lane and the normal toast line, so the two can never touch at any
     width (the real boss-bar box is measured at 360/390/1440 by
     _mmtest/objstrip-check.js). */
  --bossbar-lane: 102px;
  /* Event countdown ("GOLD RUSH 30s") sits just BELOW the top bar — the old
     50px was inside the 92px bar and overlaid the map-name cluster.
     --party-t / --timer-t below are PRE-JS FALLBACKS ONLY: js/ui.js
     syncPartyOffset() overwrites both from the measured #topbar bottom as
     soon as the HUD mounts (ResizeObserver + resize), which is what keeps
     the co-op roster and the countdown clear of the HP/buff box at every
     window size. */
  --timer-t: 100px;
  --party-t: 100px;
}

/* The objective strip owns the first line under the top bar (Q5, round 10):
   this is the GATE that selects the measured deltas — js/quest.js writes the
   `-live` values from the strip's real rect, and they only take effect while
   the world is up AND a quest is actually on screen. Everywhere else (menu,
   flag off, no active quest) both vars are 0px and every consumer below
   computes exactly today's value. */
body.in-game.obj-strip-on {
  --objstrip-h: var(--objstrip-h-live);
  --objstrip-hs: var(--objstrip-hs-live);
}

/* On phones the top bar switches from a 3-in-a-row strip to two stacked rows
   (left/right clusters on top, centered map name below) so the center label
   can never collide with the corner clusters. Offset vars follow the new
   stack height so toasts / quest log / minimap / towers sit clear of it. */
@media (max-width: 700px) {
  #topbar {
    grid-template-columns: 1fr auto;
    grid-template-areas:
      "left right"
      "center center";
    row-gap: 4px;
  }
  #player-info {
    grid-area: left;
  }
  #top-right {
    grid-area: right;
  }
  #map-name {
    grid-area: center;
    justify-self: center;
  }
  :root {
    --topbar-h: 132px;
    --timer-t: 140px;
    /* Pre-JS fallback: the two-row phone stack is ~77px to the map-name
       row bottom at hud scale; syncPartyOffset() replaces it immediately. */
    --party-t: 80px;
  }
}

/* Very short viewports (landscape phones / small laptops): cap the dock's
   center column so the ability row stays comfortably in reach. */
@media (max-height: 480px) {
  #dock {
    bottom: calc(6px + env(safe-area-inset-bottom));
  }
  .ability-btn {
    width: 44px;
    height: 44px;
  }
  #potion-slot,
  #quick-slot,
  #scroll-slot {
    width: 48px;
    height: 48px;
  }
  /* the ally slot rides every slot group (§4.1) and keeps its 42 px floor —
     the mobile-wrap acceptance says it never shrinks below 42 px, so the
     short-viewport compaction skips it */
  #ally-slot {
    width: 42px;
    height: 42px;
  }
}

#game {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  display: none;
  touch-action: none;
}

/* F15 / R3 per-tier resample rule (main.js resize toggles this class):
   `pixelated`/`crisp-edges` is inherited from html,body and is CORRECT where
   the backing store is an integer multiple of the CSS box (HIGH dpr 2.0 — the
   desktop art identity) or exactly 1:1 (LOW dpr 1.0, no resample at all).
   It is wrong where the store is a non-integer DOWN-scale (MED dpr 1.5), where
   nearest-neighbour is what produces the shimmer/crawl while the camera pans.
   One resample, smoothed only where a downscale is unavoidable. */
#game.resample-smooth {
  image-rendering: auto;
  -webkit-image-rendering: auto;
}

body:not(.in-game) #hud,
body:not(.in-game) #ability-bar,
body:not(.in-game) #potion-slot,
body:not(.in-game) #quick-slot,
body:not(.in-game) #scroll-slot,
body:not(.in-game) #ally-slot,
body:not(.in-game) #tip,
body:not(.in-game) #bottom-right {
  display: none !important;
}

body.in-game #game { display: block; }

#no-js {
  display: none;
}

html:not(.js) #no-js {
  display: flex;
  position: fixed;
  inset: 0;
  /* Start-aligned with `margin: auto` on the card so tall fallback content
     stays reachable by scrolling instead of being clipped off-screen. */
  align-items: flex-start;
  justify-content: center;
  overflow-y: auto;
  padding: 20px;
  color: #fff;
  background: #0c0f08;
  font-size: 18px;
  z-index: 50;
}

/* ---------- Pixel helpers ---------- */
.px-label {
  display: inline-block;
  vertical-align: middle;
  line-height: 0;
}

.px-label canvas {
  display: block;
}

.px-card {
  background: #1c2817;
  border: 3px solid #0a0d07;
  outline: 2px solid #3f5c33;
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.06), 4px 4px 0 rgba(0, 0, 0, 0.55);
  border-radius: 0;
}

.px-btn {
  pointer-events: auto;
  position: relative;
  color: #ffd23f;
  background: #2c4fa8;
  border: 2px solid #0a0d07;
  outline: 2px solid transparent;
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.25);
  border-radius: 0;
  font-family: inherit;
  font-weight: 900;
  letter-spacing: 1px;
  text-transform: uppercase;
  touch-action: manipulation;
  cursor: pointer;
  padding: 9px 12px;
  min-width: 44px;
  min-height: 40px;
  font-size: 12px;
  text-shadow: 2px 2px 0 rgba(0, 0, 0, 0.6);
  transition: transform 0.05s, filter 0.05s;
  /* Slightly notched pixel-art corners. */
  clip-path: polygon(
    6px 0, calc(100% - 6px) 0,
    100% 6px, 100% calc(100% - 6px),
    calc(100% - 6px) 100%, 6px 100%,
    0 calc(100% - 6px), 0 6px
  );
  filter: drop-shadow(2px 2px 0 rgba(0, 0, 0, 0.5));
}

.px-btn:hover {
  filter: drop-shadow(2px 2px 0 rgba(0, 0, 0, 0.5)) brightness(1.12);
}

.px-btn:active {
  transform: translate(2px, 2px);
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.25);
  filter: drop-shadow(0 0 0 rgba(0, 0, 0, 0.5));
}

/* ---------- HUD ---------- */
#hud {
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 10;
}

#topbar {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: start;
  gap: 8px;
  padding: calc(8px + env(safe-area-inset-top)) 10px 0 10px;
  pointer-events: none;
  z-index: 12;
}

#topbar > * {
  pointer-events: auto;
}

#player-info {
  justify-self: start;
  zoom: var(--hud);
  background: #1c2817;
  border: 3px solid #0a0d07;
  outline: 2px solid #3f5c33;
  box-shadow: 4px 4px 0 rgba(0, 0, 0, 0.55);
  border-radius: 0;
  padding: 6px 8px;
  /* Fixed footprint (Lead UI pass 2026-10-23): the box hugs the LV/bars
     content and NEVER grows for the buff chips — #potion-timer suppresses
     its intrinsic width below, so a wall of active buffs wraps into rows
     INSIDE the box instead of stretching it up to 46vw across the screen.
     224px = content (~202px) + padding, and guarantees two buff chips per
     row (longest real chip "Blood Oath 10s" ≈ 96px). The old
     max-width:46vw is gone: under zoom:var(--hud) it resolved inside the
     zoom space (min-width always won on phones), so it never constrained
     anything. */
  min-width: 224px;
}

#lvl {
  color: #ffd23f;
  font-size: 11px;
  font-weight: 900;
  letter-spacing: 1px;
  text-shadow: 2px 2px 0 #000;
}

.bar {
  position: relative;
  height: 12px;
  border-radius: 0;
  background: #0a0d07;
  border: 2px solid #0a0d07;
  margin-top: 4px;
}

.bar .fill {
  height: 100%;
  border-radius: 0;
  /* P3 / §4.C.5 (CTO review, ruling 2): the box is FULL WIDTH and ui.js
     clips the visible part with `clip-path: inset(0 <100-pct>% 0 0)`.
     `style.width` is a layout property — under sustained damage the HP/XP
     fills kept a layout pass alive every frame (measured: `#xp-fill .width`
     142 writes / 20 s = the LayoutCount floor in chaos `full`). clip-path is
     paint-only: writing it never schedules layout, and it interpolates the
     SAME numeric value `width` did (right edge = 100% − inset), so the
     stepped animation is unchanged — verified frame-by-frame in
     `_mmtest/perf-a52-cto-review/clip-transition-test.js` (max |width − clip|
     = 0.008 % of the box, both 7 distinct values under steps(6)). */
  width: 100%;
  transition: width 0.12s steps(6), clip-path 0.12s steps(6);
}

.bar.hp .fill {
  background: repeating-linear-gradient(90deg, #ff5d57 0 8px, #c13a34 8px 16px);
}

.bar.hp {
  height: 24px;
}

.bar.xp .fill {
  background: repeating-linear-gradient(90deg, #8a7bff 0 8px, #5e4bb0 8px 16px);
}

.bar.xp {
  height: 6px;
}

.bar span {
  position: absolute;
  inset: 0;
  z-index: 2; /* above the fill (z-index:1) — the HP number must never hide
                 behind the red bar (ghost-chip regression fix) */
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 8px;
  font-weight: 900;
  color: #fff;
  text-shadow: 1px 1px 0 #000;
  white-space: nowrap;
  overflow: hidden;
}

#hp-text {
  display: flex !important;
  align-items: center;
  justify-content: center;
  width: auto !important;
  height: auto !important;
  line-height: normal !important;
}

#gold {
  zoom: var(--hud);
  background: #1c2817;
  border: 3px solid #0a0d07;
  outline: 2px solid #7a5f14;
  box-shadow: 4px 4px 0 rgba(0, 0, 0, 0.55);
  border-radius: 0;
  padding: 6px 10px;
  color: #ffd23f;
  font-weight: 900;
  font-size: 14px;
  display: flex;
  align-items: center;
  gap: 6px;
  text-shadow: 2px 2px 0 #000;
}

#top-right {
  justify-self: end;
  display: flex;
  /* U2 (gap #18 hazard): the connection chip wraps onto its own line instead
     of crushing the currency chips on a narrow phone (390 px keeps every
     currency readable — they never shrink, they re-flow). */
  flex-wrap: wrap;
  justify-content: flex-end;
  align-items: flex-start;
  gap: 10px;
  max-width: 52vw;
}

#currencies-bar {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: 4px;
  align-items: center;
}

.coin-ico {
  color: #ffd23f;
  font-size: 10px;
}
.coin-ico.ess { color: #88ccff; }
.coin-ico.shd { color: #9b59b6; }
.coin-ico.orb { color: #e74c3c; }

#currencies-bar {
  display: flex;
  gap: 4px;
  align-items: center;
}

#gold, #essence, #shards, #orbs {
  zoom: var(--hud);
  background: #1c2817;
  border: 3px solid #0a0d07;
  box-shadow: 4px 4px 0 rgba(0, 0, 0, 0.55);
  border-radius: 0;
  padding: 6px 10px;
  font-weight: 900;
  font-size: 14px;
  display: flex;
  align-items: center;
  gap: 6px;
  text-shadow: 2px 2px 0 #000;
  white-space: nowrap;
}
#gold { color: #ffd23f; outline: 2px solid #7a5f14; }
#essence { color: #88ccff; outline: 2px solid #4488aa; }
#shards { color: #9b59b6; outline: 2px solid #6a3a80; }
#orbs { color: #e74c3c; outline: 2px solid #a02020; }

#room-code {
  zoom: var(--hud);
  align-self: center;
  background: #141d28;
  border: 3px solid #0a0d07;
  outline: 2px solid #3f5c8a;
  box-shadow: 4px 4px 0 rgba(0, 0, 0, 0.55);
  border-radius: 0;
  padding: 6px 8px;
  color: #5fc1ff;
  font-weight: 900;
  font-size: 13px;
  letter-spacing: 2px;
  text-shadow: 2px 2px 0 #000;
  white-space: nowrap;
}

/* ---- Tier-1 connection chip (U2, designs/disconnect-reconnect.md §5.2) -----
   The #room-code recipe (zoomed HUD chip, flat fill, hard black border, hard
   offset shadow) carrying the connection palette instead of the room blue.
   `--net` is the ONLY thing that moves between states — JS toggles an
   is-* class, the colour itself stays in CSS (§2.4: reuse shipped tokens).
   The visible label is painted px2 by js/netstate.js into .net-chip-paint;
   .net-chip-sr is the live region's text (STATE WORD ONLY) and is clipped out
   of layout, with the attempt counter in its aria-hidden sibling (gap #17).
   Blink = the shipped queue-pulse keyframes, disabled by the reduced-motion
   block under the strip rules (§5.10: keep the colour, drop the motion). */
.net-chip {
  --net: #3f5c8a; /* resting outline until a state class paints one */
  zoom: var(--hud);
  position: relative;
  align-self: center;
  display: flex;
  align-items: center;
  gap: 3px;
  background: #162110;
  border: 3px solid #0a0d07;
  outline: 2px solid var(--net);
  box-shadow: 4px 4px 0 rgba(0, 0, 0, 0.55);
  border-radius: 0;
  padding: 5px 8px;
  white-space: nowrap;
  text-shadow: 2px 2px 0 #000;
}
.net-chip-paint {
  display: inline-block;
  line-height: 1;
  image-rendering: pixelated;
}
.net-chip-paint canvas { display: inline-block; image-rendering: pixelated; }
/* Clip, don't remove: the word must stay in the accessibility tree — it IS
   what aria-live announces. */
.net-chip-sr {
  position: absolute;
  left: 0;
  top: 0;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
/* State palette — outline and label colour per the §5.2 table. */
.net-chip.is-connecting,
.net-chip.is-queued { --net: #5fc1ff; }
.net-chip.is-degraded,
.net-chip.is-reconnecting { --net: #ffd23f; }
.net-chip.is-failed { --net: #ff5d5d; }
.net-chip.is-online { --net: #57d46a; }
.net-chip.is-reconnecting,
.net-chip.is-queued { animation: queue-pulse 1.1s ease-in-out infinite; }

/* ---- U6 idle warning chip (designs/disconnect-reconnect.md §5.6, R4/gap #21)
   Same shipped chassis — the .net-chip class does all the work, this rule only
   paints the warn palette (`--net` = #ffd23f outline, same gold the DEGRADED/
   RECONNECTING states use) and marks the chip as tappable. The tap is a REAL
   keep-alive (js/netstate.js idleTap sends a routed frame the server counts as
   an action), which is why the painted "· TAP TO STAY" tail can promise it.
   Deliberately NO blink: the countdown is information, not decoration — the
   §5.10 reduced-motion block keeps it ticking as text and there is no
   animation here for that block to disable. `:active` is press feedback only
   (no hover-dependent affordance — mobile-safe). */
.net-chip.idle-chip {
  --net: #ffd23f;
  cursor: pointer;
}
.net-chip.idle-chip:active { transform: translate(1px, 1px); }

#map-name {
  justify-self: center;
  align-self: center;
  max-width: 100%;
  min-width: 0;
  zoom: var(--hud);
  background: #1c2817;
  border: 3px solid #0a0d07;
  outline: 2px solid #7a6a3a;
  box-shadow: 4px 4px 0 rgba(0, 0, 0, 0.55);
  border-radius: 0;
  padding: 4px 8px;
  color: #cfcba9;
  text-shadow: 2px 2px 0 #000;
  line-height: 1;
  text-align: center;
  width: fit-content;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

/* ---------- Bottom dock: three-zone grid that can never overlap ----------
   #dock is a full-width bottom strip with three grid columns:
   left = quick-use slots / fx bar, center = ability bar (+ tip above it),
   right = the button column. Each zone lives in its own column track, so the
   components physically cannot collide regardless of screen size — the grid
   track reserves space for each zone. */
#dock {
  position: fixed;
  left: 8px;
  right: 8px;
  bottom: calc(10px + env(safe-area-inset-bottom));
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
  align-items: end;
  gap: 8px;
  pointer-events: none;
  z-index: 15;
}

#dock-left {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 8px;
  justify-self: start;
  min-width: 0;
}

#dock-mid {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  min-width: 0;
  max-width: 46vw;
  pointer-events: none;
}

#dock-right {
  justify-self: end;
  min-width: 0;
  pointer-events: none;
}

/* #bottom-right keeps the collapsible column + its corner toggler; the zone
   is anchored to the dock; the column scrolls instead of climbing over other
   zones when it grows. */
#bottom-right {
  zoom: var(--hud);
  pointer-events: auto;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  max-width: 46vw;
  max-height: 56vh;
}

#right-clutch {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  overflow-y: auto;
  overflow-x: hidden;
  max-height: calc(56vh - 50px);
  opacity: 1;
  padding-bottom: 3px;
  scrollbar-width: none;
  transition: max-height 0.3s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.22s ease,
    padding-bottom 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}

#right-clutch::-webkit-scrollbar {
  display: none;
}

#bottom-right.collapsed #right-clutch {
  max-height: 0;
  opacity: 0;
  padding-bottom: 0;
  pointer-events: none;
}

/* Rubber-band settle — the end of the expand choreography in js/ui.js: after
   the fling back to the top the column overshoots the edge and springs down
   into rest. Content moves inside the (fixed) scroll clip, so the last button
   briefly rides up past the top edge exactly like a released band.
   `:not(.seq-loop)`: this ID-qualified `animation` would otherwise win the
   cascade over the guide's `.seq-loop` (js/seq.js glow law — the glow is
   CONSTANT) for the 260ms it runs, so a lit button never gets to blink off. */
@keyframes clutchSettle {
  0% { transform: translateY(-5px); }
  100% { transform: translateY(0); }
}

#right-clutch.settle > .hud-btn:not(.seq-loop) {
  animation: clutchSettle 0.24s ease-out 1;
}

@media (prefers-reduced-motion: reduce) {
  #right-clutch.settle > .hud-btn { animation: none; }
}

#ability-bar {
  zoom: var(--hud);
  pointer-events: auto;
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  align-items: flex-end;
  justify-content: center;
  max-width: 46vw;
}

.ability-btn {
  pointer-events: auto;
  position: relative;
  width: 54px;
  height: 54px;
  background: #1c2817;
  border: 3px solid #0a0d07;
  outline: 2px solid #4a6b3c;
  box-shadow: 3px 3px 0 rgba(0, 0, 0, 0.55);
  border-radius: 0;
  touch-action: none;
  overflow: hidden;
  padding: 0;
}

.ability-btn img {
  width: 100%;
  height: 100%;
  display: block;
  image-rendering: pixelated;
}

.ability-btn .cd {
  position: absolute;
  left: 0;
  bottom: 0;
  width: 100%;
  /* P3 / §4.C.5 (CTO review, ruling 2): the cooldown veil used to be driven
     by `style.height` (layout — `div.cd .height` was the single biggest
     layout-writer in chaos `full`, 184 writes / 20 s, and its `height`
     transition kept a layout pass alive for 0.1 s after every write). The box
     is now always full height and JS animates `transform: scaleY()` from the
     bottom edge instead — flat colour, no border/shadow/children, so the
     painted rectangle is identical (see designs/perf-a52-cto-review.md). */
  height: 100%;
  transform: scaleY(0);
  transform-origin: 50% 100%;
  background: rgba(0, 0, 0, 0.72);
  transition: transform 0.1s linear, height 0.1s linear;
}

.ability-btn .cd-num {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  color: #ffd23f;
  font-weight: 900;
  font-size: 15px;
  text-shadow: 2px 2px 0 #000;
}

.ability-btn .key {
  position: absolute;
  top: 1px;
  left: 3px;
  color: #a9bf9a;
  font-size: 9px;
  font-weight: 900;
}

/* Top-right badge: the shape you draw to cast whatever is in this slot
   (circle / triangle / Z / square / U for slots 1-5, N / W / ∩ for 6-8). */
.ability-btn .slot-shape {
  position: absolute;
  top: 1px;
  right: 2px;
  z-index: 3;
  pointer-events: none;
  image-rendering: pixelated;
}

.ability-btn .abi-lvl {
  position: absolute;
  bottom: 2px;
  right: 3px;
  color: #7dfc88;
  font-weight: 900;
}

.ability-btn .drawn {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  color: #7dfc88;
  font-size: 10px;
  font-weight: 900;
  background: rgba(0, 0, 0, 0.55);
}

.ability-btn.dim {
  opacity: 0.55;
}

#tip {
  zoom: var(--hud);
  color: rgba(255, 255, 255, 0.75);
  max-width: min(40vw, 560px);
  text-align: center;
  overflow: hidden;
  font-size: 10px;
  font-weight: 900;
  letter-spacing: 1px;
  text-shadow: 2px 2px 0 #000;
  pointer-events: none;
}

/* Per-character retirement of that beginner hint: it is visible only while
   the ACTIVE HERO has never killed a boss. js/ui.js refreshGlyphTip() toggles
   this class (hero load, new hero, co-op welcome, boss death) — the state it
   reads rides the character save, never a global localStorage key. Note the
   hint is hidden in co-op by `body.net-mode #tip` below regardless. */
#tip.retired {
  display: none;
}

/* ---------- Right buttons ---------- */
.hud-btn.hud-toggler {
  min-width: 48px;
  min-height: 42px;
  background: #26301c;
}

.hud-btn {
  pointer-events: auto;
  position: relative;
  font-weight: 900;
  letter-spacing: 1px;
  color: #fff;
  background: #2f4fa8;
  border: 2px solid #0a0d07;
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.25);
  border-radius: 0;
  padding: 11px 12px;
  font-size: 13px;
  min-width: 48px;
  min-height: 42px;
  touch-action: manipulation;
  margin-top: 8px;
  text-shadow: 2px 2px 0 rgba(0, 0, 0, 0.5);
  /* Same banner/pennant shape as the main-menu tabs. */
  clip-path: polygon(8% 0, 92% 0, 100% 100%, 0 100%);
  filter: drop-shadow(0 3px 0 #0a0d07);
}

.hud-btn:hover {
  filter: drop-shadow(0 3px 0 #0a0d07) brightness(1.1);
}

.hud-btn:active {
  transform: translate(2px, 2px);
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.25);
  filter: drop-shadow(0 0 0 #0a0d07);
}

.hud-btn.big {
  font-size: 14px;
  background: #b03c2b;
}

/* All HUD buttons share one shadow recipe and one gap; the per-button rules
   below only tint the face so the column reads as a single clutch. */
.hud-btn#btn-bag {
  background: #0f6b4f;
}

.hud-btn#btn-map {
  background: #1c4a6e;
}

.hud-btn.has {
  outline: 2px solid #e05ec0;
}

.hud-btn#btn-summon-ally {
  background: #2f6b2a;
}

.hud-btn#btn-summon-ally:disabled {
  opacity: 0.75;
}

.hud-btn#btn-summon-ally:disabled:active {
  transform: none;
  box-shadow: 0 3px 0 #0a0d07, inset 0 0 0 1px rgba(255, 255, 255, 0.28);
}

.hud-btn .px-label {
  min-width: 18px;
  text-align: center;
}

.hud-btn .px-label + .px-label {
  margin-left: 8px;
}

.hud-btn.big {
  min-width: 96px;
}

.hud-btn .count {
  position: absolute;
  top: -6px;
  right: -6px;
  background: #0a0d07;
  color: #ffd23f;
  border: 2px solid #ffd23f;
  font-size: 11px;
  min-width: 20px;
  height: 20px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 900;
}

/* ---------- Minimap ---------- */
#minimap-wrap {
  zoom: var(--hud);
  position: absolute;
  top: calc(var(--topbar-h) + 8px + env(safe-area-inset-top));
  right: 10px;
  pointer-events: none;
  opacity: 1;
  transition: opacity 0.15s steps(3, end);
}
#minimap-wrap.hide {
  opacity: 0;
  pointer-events: none;
}
#minimap {
  display: block;
  image-rendering: pixelated;
  image-rendering: crisp-edges;
  border: 2px solid #0a0d07;
  box-shadow: 0 4px 0 1px rgba(255, 255, 255, 0.1), 0 8px 0 2px rgba(0, 0, 0, 0.6);
}

/* ---------- Panels ---------- */
#panel-mask {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.55);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.15s;
  z-index: 20;
}

#panel-mask.show {
  opacity: 1;
  pointer-events: auto;
}

.panel {
  position: fixed;
  left: 50%;
  bottom: 0;
  transform: translate(-50%, 105%);
  width: min(560px, 96vw);
  max-height: 76vh;
  background: #1c2817;
  border: 4px solid #0a0d07;
  outline: 2px solid #3f5c33;
  color: #f2eeca;
  z-index: 21;
  transition: transform 0.18s steps(4);
  display: flex;
  flex-direction: column;
  box-shadow: 8px 0 0 rgba(0, 0, 0, 0.35), 0 -6px 0 rgba(0, 0, 0, 0.4);
  border-bottom: none;
  border-radius: 0;
  padding-bottom: env(safe-area-inset-bottom, 0px);
}

.panel.show {
  transform: translate(-50%, 0);
}

#panel-skills {
  width: min(720px, 96vw);
  max-height: 94vh;
}

.panel-head {
  position: relative;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  padding: 12px 14px 8px;
  border-bottom: 3px solid #0a0d07;
  background: repeating-linear-gradient(90deg, #223019 0 12px, #1a2613 12px 24px);
}

.panel-head h2,
.panel-head .panel-title {
  font-size: 15px;
  letter-spacing: 2px;
  color: #ffd23f;
  text-shadow: 2px 2px 0 #000;
}

.panel-head .pts {
  margin-left: auto;
  background: #0a0d07;
  color: #ffd23f;
  border: 2px solid #ffd23f;
  font-weight: 900;
  min-width: 24px;
  height: 24px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 13px;
}

.panel-head .pts.zero {
  border-color: #55604d;
  color: #55604d;
}

.panel-head .pts-label {
  font-size: 11px;
  color: #cfcba9;
}

.tree-tools {
  margin-left: auto;
  display: flex;
  gap: 6px;
}

.tree-tool {
  pointer-events: auto;
  cursor: pointer;
  font-weight: 900;
  color: #ffd23f;
  background: #162110;
  border: 2px solid #0a0d07;
  box-shadow: 2px 2px 0 rgba(0, 0, 0, 0.45);
  border-radius: 0;
  padding: 6px 10px;
  font-size: 12px;
  touch-action: manipulation;
}

.tree-tool:active {
  transform: translate(2px, 2px);
  box-shadow: 0 0 0 rgba(0, 0, 0, 0.45);
}

.tree-reset {
  color: #ff6a5a;
  border-color: #3a1510;
  font-size: 10px;
  padding: 6px 8px;
  letter-spacing: 0.5px;
}
#panel-skills .panel-x {
  position: static;
  order: 10;
}
#panel-skills #btn-tree-reset-all {
  order: 9;
}
.tree-reset:hover {
  color: #ff9090;
  border-color: #5a2520;
}

.tree-status {
  color: #9db18c;
  font-size: 11px;
  letter-spacing: 0.5px;
  padding: 3px 2px 7px;
  min-height: 18px;
}

.tree-canvas {
  width: 100%;
  height: min(66vh, 640px);
  background: #101a0c;
  border: 3px solid #0a0d07;
  outline: 2px solid #3f5c33;
  box-shadow: 4px 4px 0 rgba(0, 0, 0, 0.5);
  touch-action: none;
  display: block;
  cursor: grab;
}

.panel-body {
  padding: 10px 12px 16px;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: thin;
  scrollbar-color: #3f5c33 #0a0d07;
  min-height: 0;
  flex: 1;
}

.panel-body::-webkit-scrollbar {
  width: 8px;
}

.panel-body::-webkit-scrollbar-track {
  background: #0a0d07;
  border-left: 1px solid #1c2817;
}

.panel-body::-webkit-scrollbar-thumb {
  background: #3f5c33;
  border: 1px solid #0a0d07;
}

.panel-body::-webkit-scrollbar-thumb:hover {
  background: #57d46a;
}

/* Sticky header strip inside a scrolling .panel-body — inventory tabs +
   weapon/armor/trinket row, achievements tabs + category chips. It pins under
   the panel head while the list scrolls. The negative side margins cancel
   .panel-body's horizontal padding so the opaque backdrop covers the whole
   panel width and nothing shows through above it. */
.bag-sticky,
.ach-sticky {
  position: sticky;
  top: 0;
  z-index: 6;
  background: #1c2817;
  margin-left: -12px;
  margin-right: -12px;
  padding-left: 12px;
  padding-right: 12px;
  border-bottom: 2px solid #0a0d07;
}

/* Those two bodies start flush: a scrollport padding-top would keep the stuck
   strip 10px below the top edge, and list rows would show through that band.
   The gap moves onto the first in-flow element instead (.ach-sticky carries
   its own; the bag's search row keeps the original 10px). */
#panel-bag .panel-body,
#panel-achievements .panel-body {
  padding-top: 0;
}

.ach-sticky {
  padding-top: 10px;
}

.skill-row {
  display: flex;
  align-items: center;
  gap: 10px;
  background: #162110;
  border: 2px solid #0a0d07;
  box-shadow: 3px 3px 0 rgba(0, 0, 0, 0.4);
  border-radius: 0;
  padding: 9px 10px;
  margin-bottom: 8px;
}

.skill-info {
  flex: 1;
  min-width: 0;
}

.skill-name {
  font-weight: 900;
  font-size: 13px;
  display: flex;
  align-items: center;
  gap: 6px;
  letter-spacing: 1px;
}

.skill-icon {
  display: inline-block;
  width: 18px;
  height: 18px;
  vertical-align: middle;
}

.skill-icon img {
  width: 100%;
  height: 100%;
  image-rendering: pixelated;
}

.skill-desc {
  color: #9db18c;
  font-size: 11px;
  margin-top: 2px;
}

.skill-right {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 5px;
}

.skill-dots {
  display: flex;
  gap: 3px;
}

.skill-dots .dot {
  width: 8px;
  height: 8px;
  border-radius: 0;
  background: #2c3726;
  border: 1px solid #0a0d07;
}

.skill-dots .dot.on {
  background: #ffd23f;
}

.skill-btn {
  font-weight: 900;
  font-size: 15px;
  color: #1a1200;
  background: #ffd23f;
  border: 2px solid #0a0d07;
  box-shadow: 0 3px 0 #8a6a12;
  border-radius: 0;
  width: 34px;
  height: 30px;
  touch-action: manipulation;
  cursor: pointer;
}

.skill-btn:active:not(:disabled) {
  transform: translate(2px, 2px);
  box-shadow: 0 0 0 #8a6a12;
}

.skill-btn.locked {
  background: #2c3726;
  color: #69765f;
  box-shadow: 0 3px 0 #0a0d07;
}

.skill-btn.maxed {
  background: #2f7d2f;
  color: #d8ffd0;
  font-size: 12px;
}

.stat-row {
  display: flex;
  justify-content: space-between;
  padding: 7px 4px;
  border-bottom: 2px solid rgba(0, 0, 0, 0.35);
  font-size: 13px;
  gap: 10px;
}

.stat-row span:first-child {
  color: #cfcba9;
}

.stat-row span:last-child {
  color: #ffd23f;
  font-weight: 900;
}

/* ---------- Achievements panel ---------- */
#panel-achievements .panel-head {
  /* the close X floats top-right — keep the count chips clear of it */
  padding-right: 42px;
}

.ach-tabs {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  margin-bottom: 8px;
}

.ach-tab {
  pointer-events: auto;
  cursor: pointer;
  font-weight: 900;
  letter-spacing: 1px;
  color: #cfcba9;
  background: #162110;
  border: 2px solid #0a0d07;
  box-shadow: 2px 2px 0 rgba(0, 0, 0, 0.45);
  border-radius: 0;
  padding: 7px 10px;
  font-size: 11px;
  touch-action: manipulation;
  line-height: 1;
}

.ach-tab.on {
  background: #ffd23f;
  color: #1a1200;
  box-shadow: 0 2px 0 #8a6a12;
}

/* Category chips — same chrome as .bag-tab (they wrap instead of swiping, so
   the last chip can never be cut off at the panel edge). */
.ach-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  padding-bottom: 6px;
  margin-bottom: 4px;
}

.ach-chip {
  pointer-events: auto;
  cursor: pointer;
  font-weight: 900;
  letter-spacing: 1px;
  color: #cfcba9;
  background: #162110;
  border: 2px solid #0a0d07;
  box-shadow: 2px 2px 0 rgba(0, 0, 0, 0.45);
  border-radius: 0;
  padding: 7px 10px;
  font-size: 11px;
  line-height: 1;
  touch-action: manipulation;
}

.ach-chip.on {
  background: #ffd23f;
  color: #1a1200;
  box-shadow: 0 2px 0 #8a6a12;
}

/* Rarity row — same chip chrome, separated by a hard 2px rule so the two
   filter axes read as two rows (the game's standard border colour, not a
   translucent smudge — the artist review flagged rgba() as off-palette).
   The chips paint their label in the tier colour (js/achievements.js
   paintTabEl offColor); the selected chip flips to dark-on-gold like every
   other selected chip in the game, and keeps its tier identity via a
   tier-coloured frame instead of the generic amber. */
.ach-rar-chips {
  padding-top: 6px;
  margin-top: 2px;
  border-top: 2px solid #0a0d07;
}

/* Same hit size as the category chips above — the rarity row must not be the
   smallest tap target in the window (project UX rule: one-finger play). */
.ach-rar-chip {
  padding: 7px 10px;
}

/* Both chip rows live in the sticky strip; when a view clears them (Titles
   tab, login card) an empty row would still draw its padding + separator. */
#ach-chips:empty,
#ach-rar-chips:empty {
  display: none;
}

.ach-list {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.ach-row {
  background: #16210f;
  border: 2px solid #0a0d07;
  border-left: 4px solid #b0713c;
  padding: 7px 9px 8px;
}

.ach-row.tier-silver { border-left-color: #c9ccd1; }
.ach-row.tier-gold { border-left-color: #ffd23f; }
.ach-row.tier-platinum { border-left-color: #e5f2f2; }

.ach-row.got {
  background: #1d2c12;
  box-shadow: inset 0 0 0 1px rgba(125, 252, 136, 0.25);
}

.ach-head {
  display: flex;
  align-items: baseline;
  gap: 8px;
}

.ach-tier {
  flex: 0 0 auto;
  font-size: 9px;
  font-weight: 900;
  letter-spacing: 1.5px;
  text-transform: uppercase;
  color: #b0713c;
}

.ach-row.tier-silver .ach-tier { color: #c9ccd1; }
.ach-row.tier-gold .ach-tier { color: #ffd23f; }
.ach-row.tier-platinum .ach-tier { color: #e5f2f2; }

.ach-name {
  flex: 1 1 auto;
  font-size: 13px;
  font-weight: 900;
  color: #f2eeca;
  min-width: 0;
}

.ach-row.got .ach-name { color: #d8ffd0; }

.ach-desc {
  margin-top: 3px;
  font-size: 11px;
  line-height: 1.45;
  color: #9aa88a;
}

.ach-progress {
  margin-top: 5px;
  font-size: 11px;
  font-weight: 900;
  color: #ffd23f;
}

.ach-cap {
  margin-top: 5px;
  font-size: 11px;
  font-weight: 900;
  color: #cfcba9;
}

.ach-bar {
  margin-top: 4px;
  height: 6px;
  background: #0a0d07;
  border: 1px solid #3f5c33;
}

.ach-bar-fill {
  height: 100%;
  width: 0;
  background: #ffd23f;
}

.ach-earned {
  margin-top: 5px;
  font-size: 11px;
  font-weight: 900;
  letter-spacing: 0.5px;
  color: #7dfc88;
}

.ach-empty {
  padding: 10px 4px;
  font-size: 12px;
  color: #8a9a7a;
}

.ach-filter-note {
  /* horizontal padding 0: the note must align with the separator, the chips
     and the list rows (all flush left), not sit 4px right of them. */
  padding: 2px 0 4px;
  font-size: 11px;
  font-weight: 900;
  letter-spacing: 0.5px;
  color: #8a9a7a;
}

/* ---- titles tab ---- */
.ach-title-preview {
  padding: 7px 9px;
  margin-bottom: 8px;
  background: #0a0d07;
  border: 2px solid #3f5c33;
  font-size: 12px;
  font-weight: 900;
  color: #f2eeca;
}

.ach-title-none { color: #8a9a7a; }

.ach-title-nick { color: #ffd23f; }

.title-row {
  display: flex;
  align-items: center;
  gap: 8px;
  background: #16210f;
  border: 2px solid #0a0d07;
  border-left: 4px solid #55604d;
  padding: 7px 9px;
}

.title-row.got {
  border-left-color: #b9a7ff;
  background: #1d2c12;
}

.title-main {
  flex: 1 1 auto;
  min-width: 0;
}

.title-text {
  font-size: 13px;
  font-weight: 900;
}

.title-text.none { color: #cfcba9; }

.title-sub {
  display: block;
  margin-top: 2px;
  font-size: 10px;
  color: #9aa88a;
}

.title-equip {
  flex: 0 0 auto;
  min-height: 32px;
  padding: 6px 10px;
  font-size: 11px;
}

.title-equip.on {
  background: #2f4a1a;
  color: #7dfc88;
  cursor: default;
}

/* ---------- Inventory ---------- */
.bag-tabs {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  margin-bottom: 10px;
}

.bag-tab {
  pointer-events: auto;
  cursor: pointer;
  font-weight: 900;
  letter-spacing: 1px;
  color: #cfcba9;
  background: #162110;
  border: 2px solid #0a0d07;
  box-shadow: 2px 2px 0 rgba(0, 0, 0, 0.45);
  border-radius: 0;
  padding: 7px 10px;
  font-size: 11px;
  touch-action: manipulation;
  line-height: 1;
}

.bag-tab.on {
  background: #ffd23f;
  color: #1a1200;
  box-shadow: 0 2px 0 #8a6a12;
}

.bag-hint {
  color: #7dfc88;
  font-size: 11px;
  letter-spacing: 0.5px;
  padding: 4px 0;
}

#bag-equip {
  display: flex;
  gap: 6px;
  margin-bottom: 4px;
}

/* The tab strip sits directly above the two equipment rows inside the pinned
   .bag-sticky: keep that gap tight so the scrolling grid keeps its rows on a
   phone (measured in _mmtest/ally-equip-check.js). */
#bag-tabs {
  margin-bottom: 4px;
}

/* Ally equipment row: the SAME slots as the player row under an INLINE
   pixel-font caption — the caption shares the row instead of owning a second
   line. A standalone caption row plus roomy slots starved the scrolling grid
   on a 360×640 phone (see the MEASURE block in _mmtest/ally-equip-check.js:
   .bag-sticky must never eat the grid's rows). */
#bag-ally-equip {
  display: flex;
  flex-wrap: wrap;
  align-items: stretch;
  gap: 6px;
  margin-bottom: 4px;
}

#bag-ally-equip .ally-equip-cap {
  flex: 0 0 auto;
  align-self: center;
}

.bag-slot {
  flex: 1;
  text-align: center;
  background: #162110;
  border: 2px solid #0a0d07;
  box-shadow: 3px 3px 0 rgba(0, 0, 0, 0.4);
  padding: 2px 4px;
  /* 44px floor = the touch-target guideline, and the tallest content still
     fits (both equipment rows share this rule). */
  min-height: 44px;
}

.bag-slot .slot-name {
  color: #9db18c;
  font-size: 9px;
  line-height: 10px;
  letter-spacing: 1px;
}

.bag-slot.filled .slot-name {
  color: #ffd23f;
}

.bag-slot .slot-item {
  margin-top: 1px;
  font-size: 11px;
  line-height: 12px;
  font-weight: 900;
}
.equipped-unidentified {
  display: inline-block;
  margin-left: 4px;
  padding: 0 3px;
  color: #0a0d07;
  background: #ffd23f;
  font-size: 10px;
  line-height: 15px;
  vertical-align: middle;
}

.bag-slot img {
  width: 18px;
  height: 18px;
  image-rendering: pixelated;
  display: block;
  margin: 0 auto 1px;
}

#bag-grid {
  display: grid;
  grid-template-columns: repeat(6, 1fr);
  gap: 6px;
  /* Rarity frames are 2px OUTLINES outside the cell, so the first row's top
     frame needs breathing room below .bag-sticky (opaque, z-index 6) or the
     strip paints over it and the row looks frameless/cut off. */
  margin-top: 6px;
}

.bag-cell {
  aspect-ratio: 1;
  background: #0d130a;
  border: 2px solid #0a0d07;
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.05);
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  touch-action: manipulation;
  cursor: pointer;
}

.bag-cell img {
  width: 78%;
  height: 78%;
  image-rendering: pixelated;
}

.bag-cell .qty {
  position: absolute;
  bottom: 1px;
  right: 3px;
  color: #fff;
  font-size: 9px;
  font-weight: 900;
  text-shadow: 1px 1px 0 #000;
}

/* ── NEW badge ─────────────────────────────────────────────────────────────
   Top-right corner (the stack count owns the bottom-right, so the two never
   collide): a dark plate + pixel-font "NEW". It pulses ONCE when the window
   opens and stays static afterwards — looping motion across a grid of slots
   just steals attention from the game. Cleared when the window closes. */
.bag-cell .new-badge, .glyph-card .new-badge {
  position: absolute;
  top: 1px;
  right: 2px;
  padding: 1px 2px 2px;
  background: #0a0d07;
  border: 1px solid #ff5d5d;
  line-height: 0;
  pointer-events: none;
  z-index: 2;
  transform-origin: center;
}
.bag-cell .new-badge.pulse, .glyph-card .new-badge.pulse {
  animation: newPulse 0.4s steps(4) 1;
}
@keyframes newPulse {
  0% { transform: scale(1); }
  50% { transform: scale(1.45); }
  100% { transform: scale(1); }
}
@media (prefers-reduced-motion: reduce) {
  .bag-cell .new-badge.pulse, .glyph-card .new-badge.pulse { animation: none; }
}

/* ── Inventory sort button ────────────────────────────────────────────────
   One cycling button in the panel head (left of the X): pixel ▲▼ icon plus
   the current mode painted in the pixel font, so the mode is readable
   without tapping. Tall/wide enough for a thumb (WCAG 2.2 target size). */
#panel-bag .bag-sort {
  position: absolute;
  top: 5px;
  right: 34px;
  display: flex;
  align-items: center;
  gap: 6px;
  height: 30px;
  min-width: 58px;
  padding: 0 8px;
  background: #101a0c;
  border: 2px solid #3f5a32;
  border-radius: 0;
  cursor: pointer;
  z-index: 3;
}
#panel-bag .bag-sort img {
  display: block;
  image-rendering: pixelated;
}
#panel-bag .bag-sort .bag-sort-mode {
  line-height: 0;
}
#panel-bag .bag-sort:hover,
#panel-bag .bag-sort:focus-visible {
  border-color: #57d46a;
}
#panel-bag .bag-sort:active {
  transform: translateY(1px);
  background: #16240f;
}

.bag-cell.empty {
  opacity: 0.35;
}

/* ── Search ghost rows (spec §9 g) ── Worn gear surfaced ONLY by an active
   search (hero's 3 slots then the ally's 3): the same cell chassis with a
   pixel-font EQUIPPED/ALLY badge in the top-left instead of a stack count,
   no NEW badge, and the tap opens the slot detail. */
.bag-cell.ghost {
  opacity: 0.95;
}
.bag-cell .ghost-badge {
  position: absolute;
  top: 1px;
  left: 2px;
  padding: 1px 2px 2px;
  background: #0a0d07;
  border: 1px solid #3f5c33;
  line-height: 0;
  pointer-events: none;
  z-index: 2;
}

/* ── Short-viewport bag budget (spec §9 a) ── At 360×640 the sticky strip
   left ~94px of grid — 12px short of two full rows. Reclaim the whitespace,
   #panel-bag-scoped so no other panel moves: the body's bottom padding 16→8
   (+8), the grid's top margin 6→2 (+4) — the spec's "exactly 12px" — plus the
   search row's 10/8px margins → 4/4 (+10) so two FULL rows land in this
   browser too (the spec's own arithmetic was 2px brittle; measured
   `_mmtest/ally-equip-check.js` MEASURE). The tab strip is deliberately
   untouched (its chips are tap targets).
   POTION PANE (spec §9 a/i acceptance, CTO ruling 2026-10-27): the 5-button
   row's ~200px of content already scrolls inside the 120px cap — DRINK/DROP
   sit below the fold at 120 too — so the cap is NOT what keeps the buttons
   reachable. Lowering it 120→92 on short viewports returns 28px to the grid
   (visible 34→62px vs a 53px row = ≥1 FULL grid row with the pane open, the
   spec's §4.2 acceptance) at the cost of scroll extent only. Tall viewports
   keep 120px. */
@media (max-height: 700px) {
  #panel-bag .panel-body {
    padding-bottom: 8px;
  }
  #panel-bag #bag-grid {
    margin-top: 2px;
  }
  #panel-bag .bag-search {
    margin-top: 4px;
    margin-bottom: 4px;
  }
  #panel-bag #bag-detail {
    max-height: 92px;
  }
}

.rar-0 {
  outline: 2px solid #7d8490;
}
.rar-1 {
  outline: 2px solid #57d46a;
}
.rar-2 {
  outline: 2px solid #4aa3ff;
}
.rar-3 {
  outline: 2px solid #c05ef0;
}
.rar-4 {
  outline: 2px solid #ffb02e;
  box-shadow: 0 0 0 1px #ffd23f, 0 0 12px 2px rgba(255, 176, 46, 0.55);
}

#bag-detail {
  flex-shrink: 0;
  background: #162110;
  border: 2px solid #0a0d07;
  box-shadow: 3px 3px 0 rgba(0, 0, 0, 0.4);
  padding: 8px 10px;
  font-size: 11px;
  min-height: 40px;
  max-height: 120px;
  overflow-y: auto;
  /* Same pixelart scrollbar as .panel-body / .menu-frame / .chat-log … —
     the detail card is the ONE scroll surface inside a panel whose content
     (potion 5-button row, long stat lines) overflows the 120px cap, and it
     used to inherit the native OS scrollbar. */
  scrollbar-width: thin;
  scrollbar-color: #3f5c33 #0a0d07;
}

#bag-detail::-webkit-scrollbar {
  width: 8px;
}

#bag-detail::-webkit-scrollbar-track {
  background: #0a0d07;
  border-left: 1px solid #1c2817;
}

#bag-detail::-webkit-scrollbar-thumb {
  background: #3f5c33;
  border: 1px solid #0a0d07;
}

#bag-detail::-webkit-scrollbar-thumb:hover {
  background: #57d46a;
}

#bag-detail .d-name {
  font-weight: 900;
  letter-spacing: 1px;
}

#bag-detail .d-stats {
  color: #b8cda8;
}
#bag-detail .d-unidentified {
  margin-top: 4px;
  color: #ffd23f;
  font-weight: 900;
}

#bag-detail .d-act {
  color: #7dfc88;
}

#bag-detail .d-actions {
  display: flex;
  justify-content: flex-end;
  /* up to three sibling buttons on a gear row ([EQUIP] [EQUIP ALLY] [DROP]) —
     wrap cleanly instead of overflowing #bag-detail on a phone */
  flex-wrap: wrap;
  gap: 8px;
  margin-top: 6px;
}

.drop-btn {
  pointer-events: auto;
  cursor: pointer;
  font-weight: 900;
  letter-spacing: 1px;
  color: #fff;
  background: #b03c2b;
  border: 2px solid #0a0d07;
  box-shadow: 0 3px 0 #0a0d07, inset 0 0 0 1px rgba(255, 255, 255, 0.2);
  border-radius: 0;
  padding: 8px 14px;
  font-size: 12px;
  touch-action: manipulation;
  /* ≥44 px touch-target floor (spec §9 i) — the box grows vertically only,
     padding/font untouched. The potion detail row is a combat-time surface;
     every .drop-btn (potion/scroll/map/gear rows) shares the floor. */
  min-height: 44px;
}

/* Equip siblings of DROP — identical chassis/height/padding, only the fill
   differs (green = your hero, blue = your ally; DROP keeps its red).
   Contrast (WCAG, white 12px/900 text): DROP red #b03c2b 5.95:1, ally blue
   #2a6fc9 4.98:1, equip green #2d8038 4.93:1 (hue 128° vs the palette's
   #57d46a at 129°) — all above AA 4.5:1 (the previous green measured 4.09). */
.drop-btn.equip { background: #2d8038; }
.drop-btn.ally { background: #2a6fc9; }

.drop-btn:active {
  transform: translate(2px, 2px);
  box-shadow: 0 0 0 #0a0d07;
}

/* ---------- Toasts ---------- */
#toasts {
  zoom: var(--hud);
  position: fixed;
  /* + var(--objstrip-h) (Q5, round 10): with a quest active the objective
     strip owns the first line under the top bar, so the toast column starts
     BELOW it — 52 screen px on desktop, more on a phone where
     `--topbar-h × hud` lands the toast line above the strip. 0px = today's
     offset, byte-for-byte. */
  top: calc(var(--topbar-h) + 10px + var(--objstrip-h) + env(safe-area-inset-top));
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  z-index: 46;
  pointer-events: none;
}

.toast {
  background: #162110;
  border: 2px solid #0a0d07;
  outline: 2px solid #7a5f14;
  color: #ffe9a0;
  font-size: clamp(10px, 2.5vw, 12px);
  font-weight: 900;
  padding: 6px 10px;
  max-width: 88vw;
  border-radius: 0;
  box-shadow: 3px 3px 0 rgba(0, 0, 0, 0.5);
  letter-spacing: 1px;
  transition: opacity 0.35s, transform 0.35s;
}

.toast.out {
  opacity: 0;
  transform: translateY(-10px);
}

/* ---- Tier-2 persistent connection strip (U3, §5.3) ------------------------
   Same toast chrome (it inherits .toast's fill/border/shadow) with the gold
   warn outline, but it is a STATIC child of #toasts: js/netstate.js repaints
   it in place, it never rides the toast lifetime and is never auto-dismissed —
   only JOINED / a terminal record hide it (§5.1 tier rules). aria-hidden on
   the container: the Tier-1 chip is the single announcing voice (§5.10), so
   the countdown can tick without a screen reader following along.
   .conn-strip-text is a display:none mirror of the painted label: text
   consumers (tests, tooling) read the strip, the canvas carries no text. */
.conn-strip {
  outline: 2px solid #ffd23f;
  color: #ffe9a0;
  max-width: 88vw;
  letter-spacing: 1px;
}
.conn-strip-paint {
  display: inline-block;
  line-height: 1;
  image-rendering: pixelated;
}
.conn-strip-paint canvas { display: inline-block; image-rendering: pixelated; }
.conn-strip-text { display: none; }
.conn-strip.is-pulse { animation: queue-pulse 1.1s ease-in-out infinite; }

/* §5.10 reduced motion: every NEW animation in the connection ladder is a
   blink — drop the motion, keep the colour and the label (WCAG 2.3.3). The
   countdowns keep ticking as text; nothing else changes. */
@media (prefers-reduced-motion: reduce) {
  .net-chip,
  .conn-strip { animation: none !important; }
}

/* ---------- Achievement toasts ---------- */
/* Top-center, never tappable (pointer-events:none), stacked under the menu
   header but above the chat popups so a trophy is always readable. */
#ach-toasts {
  position: fixed;
  /* Not zoomed (unlike #toasts) — hence the SCREEN-px twin var. */
  top: calc(var(--topbar-h) + 10px + var(--objstrip-hs) + env(safe-area-inset-top));
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  z-index: 38;
  pointer-events: none;
  max-width: 92vw;
}
/* In the main menu the menu itself is z-index 40, which would bury the toast
   behind it — and the menu is exactly where the referral notice (and any other
   menu-side unlock) fires. Lift it above the menu only in that state; it stays
   below the namecard dialogs (95) and the reward notice (97). */
body:not(.in-game) #ach-toasts {
  z-index: 45;
}

.ach-toast {
  min-width: 190px;
  max-width: 88vw;
  padding: 7px 12px 8px 14px;
  background: #141b10;
  border: 2px solid #0a0d07;
  border-left-width: 6px;
  border-left-color: #b0713c;
  box-shadow: 3px 3px 0 rgba(0, 0, 0, 0.5);
  text-align: left;
  transition: opacity 0.35s, transform 0.35s;
}

.ach-toast.tier-silver { border-left-color: #c9ccd1; }
.ach-toast.tier-gold { border-left-color: #ffd23f; }
.ach-toast.tier-platinum { border-left-color: #e5f2f2; }

.ach-toast-tier {
  font-size: 9px;
  font-weight: 900;
  letter-spacing: 1.5px;
  color: #b0713c;
}
.ach-toast.tier-silver .ach-toast-tier { color: #c9ccd1; }
.ach-toast.tier-gold .ach-toast-tier { color: #ffd23f; }
.ach-toast.tier-platinum .ach-toast-tier { color: #e5f2f2; }

.ach-toast-name {
  font-size: clamp(11px, 2.6vw, 13px);
  font-weight: 900;
  color: #f2eeca;
  letter-spacing: 0.5px;
}

.ach-toast.out {
  opacity: 0;
  transform: translateY(-10px);
}

.hide {
  display: none !important;
}

/* Screen-reader-only text: stays in the accessibility tree (this is what
   aria-live regions announce) but out of layout — painted canvases carry no
   text, so live regions mirror their state word here (§5.10). */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  border: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* ---------- Level banner ---------- */
#level-banner {
  position: fixed;
  top: 22%;
  left: 50%;
  transform: translateX(-50%) scale(0.6);
  opacity: 0;
  z-index: 23;
  pointer-events: none;
  text-align: center;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
}

#level-banner canvas {
  image-rendering: pixelated;
  display: block;
  max-width: 90vw;
  max-height: 35vh;
  filter: drop-shadow(3px 3px 0 #2a1700) drop-shadow(6px 6px 0 rgba(0, 0, 0, 0.7));
}

#level-banner-main,
#level-banner-sub {
  line-height: 0;
  margin: 0;
  padding: 0;
}

#level-banner.show {
  animation: pop 2.4s steps(8) forwards;
}

@keyframes pop {
  0% {
    opacity: 0;
    transform: translateX(-50%) scale(0.4);
  }
  14% {
    opacity: 1;
    transform: translateX(-50%) scale(1.1);
  }
  30% {
    transform: translateX(-50%) scale(1);
  }
  80% {
    opacity: 1;
  }
  100% {
    opacity: 0;
    transform: translateX(-50%) scale(1);
  }
}

/* ---------- Boss banner ---------- */
#boss-banner {
  position: fixed;
  top: 15%;
  left: 0;
  width: 100%;
  z-index: 24;
  pointer-events: none;
  text-align: center;
  opacity: 0;
  transform: translateY(-200%);
  padding: 20px 0 32px;
}

#boss-banner.show {
  animation: bossSlam 3.5s steps(12) forwards;
}

#boss-banner-name {
  font-family: monospace;
  font-weight: 900;
  font-size: clamp(18px, 5vw, 36px);
  color: #ff5d5d;
  text-shadow: 2px 2px 0 #1a0a00, 4px 4px 0 rgba(0, 0, 0, 0.7);
  letter-spacing: 3px;
  line-height: 1.1;
  margin: 4px 0;
}

#boss-banner-name canvas {
  image-rendering: pixelated;
  display: inline-block;
  max-width: 90vw;
  max-height: 35vh;
  filter: drop-shadow(3px 3px 0 #1a0a00) drop-shadow(6px 6px 0 rgba(0, 0, 0, 0.8));
  width: auto;
  height: auto;
}

#boss-banner-sub {
  font-family: monospace;
  font-weight: 700;
  font-size: clamp(10px, 3vw, 18px);
  color: #ffd23f;
  text-shadow: 1px 1px 0 #1a0a00, 2px 2px 0 rgba(0, 0, 0, 0.7);
  letter-spacing: 6px;
  text-transform: uppercase;
}

#boss-banner-sub canvas {
  image-rendering: pixelated;
  display: inline-block;
  max-width: 80vw;
  max-height: 20vh;
}

@keyframes bossSlam {
  0% {
    opacity: 0;
    transform: translateY(-200%);
  }
  8% {
    opacity: 1;
    transform: translateY(0);
  }
  12% {
    transform: translateY(-5%);
  }
  16% {
    transform: translateY(0);
  }
  75% {
    opacity: 1;
  }
  100% {
    opacity: 0;
    transform: translateY(0);
  }
}

/* ---------- Reward popup ---------- */
#reward-popup {
  position: fixed;
  top: 28%;
  left: 50%;
  transform: translateX(-50%) scale(0.6);
  opacity: 0;
  z-index: 24;
  pointer-events: none;
  text-align: center;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
}
#reward-popup canvas {
  image-rendering: pixelated;
  display: inline-block;
  max-width: 90vw;
  max-height: 40vh;
  filter: drop-shadow(3px 3px 0 #1a0a00) drop-shadow(6px 6px 0 rgba(0, 0, 0, 0.7));
}
#reward-popup.show {
  animation: rewardPop 3s steps(8) forwards;
}
@keyframes rewardPop {
  0%   { opacity: 0; transform: translateX(-50%) scale(0.3); }
  10%  { opacity: 1; transform: translateX(-50%) scale(1.15); }
  20%  { transform: translateX(-50%) scale(1); }
  80%  { opacity: 1; }
  100% { opacity: 0; transform: translateX(-50%) scale(0.95); }
}
.rp-title { margin-bottom: 2px; }
.rp-name { margin-bottom: 6px; }
.rp-rewards { }

/* ---------- Event banner ---------- */
#event-banner {
  position: fixed;
  top: 20%;
  left: 50%;
  transform: translateX(-50%) scale(0.6);
  opacity: 0;
  z-index: 23;
  pointer-events: none;
  text-align: center;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
}
#event-banner canvas {
  image-rendering: pixelated;
  display: inline-block;
  max-width: 90vw;
  max-height: 40vh;
  filter: drop-shadow(3px 3px 0 #1a0a00) drop-shadow(6px 6px 0 rgba(0, 0, 0, 0.7));
}
#event-banner.show {
  animation: eventBannerPop 2.8s steps(8) forwards;
}
@keyframes eventBannerPop {
  0%   { opacity: 0; transform: translateX(-50%) scale(0.3); }
  8%   { opacity: 1; transform: translateX(-50%) scale(1.2); }
  18%  { transform: translateX(-50%) scale(1); }
  78%  { opacity: 1; }
  100% { opacity: 0; transform: translateX(-50%) scale(1); }
}

/* ---------- Event timer ---------- */
#event-timer {
  position: fixed;
  top: calc(var(--timer-t) + env(safe-area-inset-top));
  left: 50%;
  transform: translateX(-50%);
  z-index: 22;
  pointer-events: none;
  text-align: center;
  opacity: 0;
  transition: opacity 0.3s;
}
#event-timer.show {
  opacity: 1;
}
#event-timer canvas {
  image-rendering: pixelated;
  display: inline-block;
  max-width: 80vw;
  max-height: 30vh;
  filter: drop-shadow(2px 2px 0 rgba(0, 0, 0, 0.7));
}

/* ── Map Timer (countdown during map runs) ── */
#map-timer {
  text-align: center;
  margin-top: 2px;
  pointer-events: none;
}
#map-timer.hide {
  display: none;
}
#map-timer canvas {
  image-rendering: pixelated;
  display: inline-block;
  filter: drop-shadow(1px 1px 0 rgba(0, 0, 0, 0.8));
}
#map-timer.warn canvas {
  animation: timerPulse 0.5s steps(2) infinite;
}
#map-timer.warn {
  color: #ff5d5d;
}
@keyframes timerPulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.5; }
}

/* ---------- Quest log reward button ---------- */
.ql-reward-btn {
  display: block;
  width: 100%;
  margin-top: 6px;
  padding: 6px 0;
  font-family: inherit;
  font-size: 11px;
  font-weight: 900;
  letter-spacing: 2px;
  color: #1a1200;
  background: #57d46a;
  border: 2px solid #0a0d07;
  box-shadow: 0 3px 0 #2a6a2a;
  cursor: pointer;
  text-align: center;
  transition: transform 0.08s, box-shadow 0.08s;
}
.ql-reward-btn:active {
  transform: translate(0, 2px);
  box-shadow: 0 1px 0 #2a6a2a;
}

/* ---------- Reset-all confirmation popup ---------- */
#reset-overlay {
  position: fixed;
  inset: 0;
  background: rgba(10, 1, 1, 0.78);
  display: flex;
  align-items: center;
  justify-content: center;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.2s;
  z-index: 30;
  image-rendering: pixelated;
}
#reset-overlay.show {
  opacity: 1;
  pointer-events: auto;
}
/* Hidden overlays have to be *fully* inert, not just transparent: a child that
   re-enables pointer-events (the respawn button) would otherwise swallow taps
   aimed at whatever sits underneath it — which is how the glyph RESET ALL
   confirm window lost its RESET/CANCEL buttons to an invisible death card.
   visibility:hidden removes every descendant from hit-testing too. */
#reset-overlay:not(.show),
#death-overlay:not(.show) {
  visibility: hidden;
}
.reset-card {
  position: relative;
  text-align: center;
  color: #ffe0c0;
  background: #2b1712;
  background-image: linear-gradient(180deg, rgba(255, 255, 255, 0.05), transparent 40%);
  border: 4px solid #0a0d07;
  outline: 2px solid #c94a2e;
  box-shadow: 8px 8px 0 rgba(0, 0, 0, 0.65), inset 0 0 0 2px rgba(201, 74, 46, 0.35);
  border-radius: 0;
  padding: 20px 22px 18px;
  max-width: 88vw;
  max-height: 90vh;
  overflow-y: auto;
  image-rendering: pixelated;
  scrollbar-width: thin;
  scrollbar-color: #3f5c33 #0a0d07;
}
.reset-card::before,
.reset-card::after {
  content: "";
  position: absolute;
  width: 10px;
  height: 10px;
  background: #c94a2e;
  box-shadow: 0 0 0 2px #0a0d07;
  pointer-events: none;
}
/* Corner nubs sit flush with the padding box. They used to hang -2px over its
   edges, which extended scrollHeight/scrollWidth by 2px each and forced BOTH
   scrollbars onto this dialog even though its content always fits. */
.reset-card::before {
  top: 0;
  left: 0;
}
.reset-card::after {
  bottom: 0;
  right: 0;
}
/* Pixel-art scrollbar, only shown if content genuinely overflows (house
   style: 8px, #0a0d07 track, #3f5c33 thumb, #57d46a hover). */
.reset-card::-webkit-scrollbar {
  width: 8px;
  height: 8px;
}
.reset-card::-webkit-scrollbar-track {
  background: #0a0d07;
  border-left: 1px solid #1c2817;
}
.reset-card::-webkit-scrollbar-thumb {
  background: #3f5c33;
  border: 1px solid #0a0d07;
}
.reset-card::-webkit-scrollbar-thumb:hover {
  background: #57d46a;
}
.reset-title {
  font-size: 14px;
  font-weight: 900;
  letter-spacing: 2px;
  color: #ff6a5a;
  margin-bottom: 10px;
}
.reset-desc {
  font-size: 11px;
  line-height: 1.6;
  color: #d4b896;
  margin-bottom: 16px;
}
.reset-buttons {
  display: flex;
  gap: 12px;
  justify-content: center;
}
.reset-btn {
  font-family: inherit;
  font-size: 12px;
  font-weight: 900;
  letter-spacing: 1.5px;
  padding: 8px 22px;
  border: 3px solid #0a0d07;
  cursor: pointer;
  transition: transform 0.08s, box-shadow 0.08s;
}
.reset-yes {
  color: #1a0800;
  background: #ff6a5a;
  box-shadow: 0 3px 0 #8a2a1a;
}
.reset-yes:active {
  transform: translate(0, 2px);
  box-shadow: 0 1px 0 #8a2a1a;
}
.reset-no {
  color: #ffe0c0;
  background: #3a2a1a;
  box-shadow: 0 3px 0 #1a0e08;
}
.reset-no:active {
  transform: translate(0, 2px);
  box-shadow: 0 1px 0 #1a0e08;
}

/* ---------- Death overlay ---------- */
#death-overlay {
  position: fixed;
  inset: 0;
  background: rgba(10, 1, 1, 0.82);
  background-image: repeating-conic-gradient(rgba(255, 0, 0, 0.05) 0% 25%, transparent 0% 50%);
  background-size: 6px 6px;
  display: flex;
  align-items: center;
  justify-content: center;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.25s;
  z-index: 30;
  image-rendering: pixelated;
}

#death-overlay.show {
  opacity: 1;
  pointer-events: auto;
}

.death-card {
  position: relative;
  text-align: center;
  color: #ffe0c0;
  background: #2b1712;
  background-image: linear-gradient(180deg, rgba(255, 255, 255, 0.05), transparent 40%);
  border: 4px solid #0a0d07;
  outline: 2px solid #c94a2e;
  box-shadow: 8px 8px 0 rgba(0, 0, 0, 0.65), inset 0 0 0 2px rgba(201, 74, 46, 0.35);
  border-radius: 0;
  padding: 20px 22px 18px;
  width: min(320px, 88vw);
  overflow: hidden;
  image-rendering: pixelated;
}

/* pixel-art corner notches */
.death-card::before,
.death-card::after {
  content: "";
  position: absolute;
  width: 10px;
  height: 10px;
  background: #c94a2e;
  box-shadow: 0 0 0 2px #0a0d07;
  pointer-events: none;
}
.death-card::before {
  top: -4px;
  left: -4px;
}
.death-card::after {
  bottom: -4px;
  right: -4px;
}

.death-card .death-skull {
  display: block;
  margin: 0 auto 10px;
  width: 36px;
  height: 30px;
  image-rendering: pixelated;
  filter: drop-shadow(2px 2px 0 rgba(0, 0, 0, 0.6));
}

.death-card .death-title {
  font-size: 0;
  line-height: 1;
  margin: 0 auto 8px;
  display: inline-block;
}

.death-card .death-title canvas,
.death-card .death-btn canvas {
  max-width: 100%;
  height: auto;
}

.death-card .death-timer {
  display: block;
  margin: 12px auto 16px;
  image-rendering: pixelated;
  min-height: 40px;
}

.death-card h2 {
  font-size: 20px;
  letter-spacing: 2px;
  text-shadow: 2px 2px 0 #000;
  margin-bottom: 6px;
}

.death-card p {
  color: #c9a89a;
  margin-bottom: 16px;
  font-size: 12px;
}

.death-card .death-btn {
  pointer-events: auto;
  display: inline-block;
  min-width: 150px;
  font-size: 0;
  line-height: 0;
  text-align: center;
  color: #fff;
  background: #b03c2b;
  background-image: linear-gradient(180deg, rgba(255, 255, 255, 0.18), transparent 45%);
  border: 3px solid #0a0d07;
  box-shadow: 0 4px 0 #0a0d07, inset 0 0 0 1px rgba(255, 255, 255, 0.2), 4px 4px 0 rgba(0, 0, 0, 0.55);
  border-radius: 0;
  padding: 10px 18px;
  touch-action: manipulation;
  cursor: pointer;
  image-rendering: pixelated;
}

.death-card .death-btn:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}

.death-card .death-btn:active {
  transform: translate(2px, 2px);
  box-shadow: 0 0 0 #0a0d07, inset 0 0 0 1px rgba(255, 255, 255, 0.2);
}

/* ---------- Trade window ---------- */
#trade-overlay {
  position: fixed;
  inset: 0;
  z-index: 32;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(6, 8, 4, 0.62);
  background-image: repeating-conic-gradient(rgba(125, 252, 136, 0.05) 0% 25%, transparent 0% 50%);
  background-size: 6px 6px;
  image-rendering: pixelated;
}

#trade-overlay.hide {
  display: none;
}

.trade-card {
  position: relative;
  width: min(560px, 92vw);
  max-height: 85vh;
  display: flex;
  flex-direction: column;
  text-align: center;
  color: #f2eeca;
  background: #1c2817;
  border: 4px solid #0a0d07;
  outline: 3px solid #57d46a;
  box-shadow: 8px 8px 0 rgba(0, 0, 0, 0.65), inset 0 0 0 2px rgba(87, 212, 106, 0.2);
  border-radius: 0;
  padding: 20px 22px;
  image-rendering: pixelated;
  overflow: hidden;
}

.trade-header {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  margin-bottom: 12px;
  padding-bottom: 10px;
  border-bottom: 2px solid #0a0d07;
}

.trade-title {
  display: inline-block;
  margin: 0;
}

.trade-merchant-name {
  font-size: 14px;
  font-weight: 900;
  letter-spacing: 2px;
  text-transform: uppercase;
}

.trade-merchant-sub {
  font-size: 10px;
  color: #9db18c;
  letter-spacing: 1px;
  max-width: 260px;
  line-height: 1.3;
}

.trade-currencies {
  display: flex;
  justify-content: center;
  gap: 6px;
  flex-wrap: wrap;
  margin-top: 4px;
}

.currency-badge {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  padding: 3px 8px;
  background: #1c2817;
  border: 2px solid #0a0d07;
  outline: 2px solid #57d46a;
  font-size: 12px;
  font-weight: 900;
  letter-spacing: 0.5px;
  white-space: nowrap;
  text-shadow: 1px 1px 0 #000;
}

.trade-gold {
  display: inline-block;
  margin: 0 0 12px;
}

.trade-stock {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin-bottom: 14px;
}

.trade-stock {
  scrollbar-width: thin;
  scrollbar-color: #3f5c33 #0a0d07;
}

.trade-stock::-webkit-scrollbar {
  width: 8px;
}

.trade-stock::-webkit-scrollbar-track {
  background: #0a0d07;
  border-left: 1px solid #1c2817;
}

.trade-stock::-webkit-scrollbar-thumb {
  background: #3f5c33;
  border: 1px solid #0a0d07;
}

.trade-stock::-webkit-scrollbar-thumb:hover {
  background: #57d46a;
}

.trade-buy {
  width: 100%;
  max-width: 100%;
  overflow: hidden;
  white-space: nowrap;
  pointer-events: auto;
  color: #1a1200;
  background: #d9c24a;
  background-image: linear-gradient(180deg, rgba(255, 255, 255, 0.2), transparent 45%);
  border: 2px solid #0a0d07;
  box-shadow: 0 3px 0 #0a0d07;
  border-radius: 0;
  font-family: inherit;
  text-align: center;
  line-height: 0;
  padding: 10px 8px;
  touch-action: manipulation;
  cursor: pointer;
}

.trade-buy canvas {
  max-width: 100%;
  height: auto;
  image-rendering: pixelated;
}

.trade-buy:active {
  transform: translate(1px, 2px);
  box-shadow: 0 0 0 #0a0d07;
}

.trade-leave {
  flex-shrink: 0;
  background: #5e2f8a;
}

.trade-desc {
  margin-top: 4px;
  color: #cfcba9;
  font-size: 11px;
  letter-spacing: 1px;
  line-height: 1.5;
}

.trade-desc.hide {
  display: none;
}

.trade-cant-afford {
  opacity: 0.5;
}

.trade-cant-afford:hover {
  opacity: 0.75;
}

/* ---------- Editor ---------- */
.editor-root {
  position: fixed;
  inset: 0;
  z-index: 40;
  display: flex;
  flex-direction: column;
  background: rgba(10, 13, 8, 0.94);
  background-image: repeating-conic-gradient(rgba(87, 212, 106, 0.04) 0% 25%, transparent 0% 50%);
  background-size: 8px 8px;
  color: #f2eeca;
  image-rendering: pixelated;
}

.editor-root.hide {
  display: none;
}

.editor-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: 10px 14px;
  background: #1c2817;
  border-bottom: 4px solid #0a0d07;
  box-shadow: 6px 6px 0 rgba(0, 0, 0, 0.4);
  font-size: 16px;
  font-weight: 900;
  letter-spacing: 3px;
  color: #ffd23f;
}

.editor-tabs {
  display: flex;
  gap: 6px;
  padding: 8px 14px;
  background: #141d10;
  border-bottom: 3px solid #0a0d07;
  overflow-x: auto;
}

.ed-tab {
  pointer-events: auto;
  flex: 1;
  min-width: 96px;
  background: #223019;
  color: #cfcba9;
  border: 2px solid #0a0d07;
  box-shadow: 2px 2px 0 rgba(0, 0, 0, 0.45);
  border-radius: 0;
  font-family: inherit;
  font-weight: 900;
  letter-spacing: 2px;
  padding: 8px 6px;
  touch-action: manipulation;
  cursor: pointer;
}

.ed-tab.on {
  background: #3f5c33;
  color: #fff;
  outline: 2px solid #ffd23f;
}

.editor-body {
  flex: 1;
  display: flex;
  gap: 12px;
  padding: 12px 14px;
  min-height: 0;
}

.ed-list {
  width: 210px;
  min-width: 150px;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 4px;
  background: #141d10;
  border: 3px solid #0a0d07;
  box-shadow: 4px 4px 0 rgba(0, 0, 0, 0.5);
  padding: 8px;
}

.ed-item {
  pointer-events: auto;
  display: flex;
  align-items: center;
  gap: 8px;
  text-align: left;
  background: #1c2817;
  color: #cfcba9;
  border: 2px solid #0a0d07;
  border-radius: 0;
  font-family: inherit;
  font-size: 13px;
  letter-spacing: 0.5px;
  padding: 8px 10px;
  cursor: pointer;
  touch-action: manipulation;
}

.ed-item.on {
  background: #2f4a26;
  color: #fff;
  outline: 2px solid #57d46a;
}

.ed-chip-dot {
  width: 14px;
  height: 14px;
  border: 2px solid #0a0d07;
  flex: none;
}

.ed-cosmetic-preview {
  width: 36px;
  height: 36px;
  border: 2px solid #0a0d07;
  background: #1c2817;
  flex: none;
  image-rendering: pixelated;
}

.ed-item-add {
  margin-left: auto;
  flex: none;
  background: #2a4a6a;
  color: #ffffff;
  border: 2px solid #0a0d07;
  padding: 1px 5px;
  font-family: inherit;
  font-size: 9px;
  letter-spacing: 1px;
  cursor: pointer;
  touch-action: manipulation;
}
.ed-item-add:active {
  background: #57d46a;
  color: #1a1200;
}

.ed-sfx-drop {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 6px;
  min-height: 90px;
  padding: 14px 10px;
  background: #10160c;
  border: 3px dashed #3a4a2e;
  color: #cfcba9;
  text-align: center;
  cursor: pointer;
  user-select: none;
  touch-action: manipulation;
}
.ed-sfx-drop.over {
  border-color: #57d46a;
  background: #16220f;
}
.ed-sfx-drop.has {
  border-style: solid;
  border-color: #57d46a;
}
.ed-sfx-drop-title {
  font-size: 12px;
  letter-spacing: 1px;
  color: #ffffff;
  word-break: break-all;
}
.ed-sfx-drop-sub {
  font-size: 10px;
  letter-spacing: 1px;
  color: #8a9578;
}
.ed-sfx-row {
  display: flex;
  gap: 8px;
}

.ed-panel {
  flex: 1;
  min-width: 0;
  overflow-y: auto;
  background: #141d10;
  border: 3px solid #0a0d07;
  box-shadow: 4px 4px 0 rgba(0, 0, 0, 0.5);
  padding: 12px;
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.ed-sub {
  color: #ffd23f;
  font-size: 12px;
  letter-spacing: 1px;
  font-weight: 900;
}

.ed-art {
  background: #0a0d07;
  border: 3px solid #0a0d07;
  outline: 2px solid #3f5c33;
  padding: 10px;
  align-self: flex-start;
}

.ed-tools {
  display: flex;
  flex-wrap: wrap;
  gap: 5px;
  margin-bottom: 8px;
}

.ed-tool {
  pointer-events: auto;
  background: #223019;
  color: #cfcba9;
  border: 2px solid #0a0d07;
  border-radius: 0;
  font-family: inherit;
  font-size: 10px;
  font-weight: 900;
  letter-spacing: 1px;
  padding: 5px 8px;
  cursor: pointer;
}

.ed-tool.on {
  background: #ffd23f;
  color: #10150c;
}

.ed-canvas {
  display: block;
  image-rendering: pixelated;
  cursor: crosshair;
  touch-action: none;
  outline: 2px solid #0a0d07;
  max-width: 100%;
}

.ed-frame {
  display: inline-block;
  background: #0d130a;
  border: 3px solid #0a0d07;
  outline: 2px solid #3f5c33;
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.06), 3px 3px 0 rgba(0, 0, 0, 0.5);
  padding: 6px;
}

.ed-size {
  margin-top: 6px;
  color: #9db18c;
  font-size: 10px;
  letter-spacing: 1px;
  text-align: center;
}

.ed-save {
  background: #5e2f8a;
}

.ed-palwrap {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  margin-top: 8px;
}

.ed-chip {
  min-width: 22px;
  height: 22px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 2px solid #0a0d07;
  border-radius: 0;
  color: #fff;
  font-size: 10px;
  font-weight: 900;
  cursor: pointer;
  overflow: hidden;
  box-sizing: border-box;
  text-shadow: 1px 1px 0 #000;
}

.ed-chip.on {
  outline: 2px solid #ffd23f;
}

.ed-chip input {
  position: absolute;
  opacity: 0;
  pointer-events: none;
}

.ed-new {
  background: #3f5c33;
}

.ed-paltiles {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-top: 8px;
}

.ed-place {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  border: 2px solid #0a0d07;
  outline: 1px solid #3f5c33;
  background: #1c2817;
  color: #cfcba9;
  font-size: 11px;
  font-weight: 900;
  letter-spacing: 1px;
  padding: 4px 8px;
  cursor: pointer;
  touch-action: manipulation;
}

.ed-place input {
  width: 14px;
  height: 14px;
  accent-color: #57d46a;
  margin: 0;
  cursor: pointer;
}

.ed-place .eds-sw {
  width: 12px;
  height: 12px;
  outline: 1px solid #0a0d07;
  flex: none;
}

.ed-place .ed-count {
  color: #8f9a74;
  font-size: 10px;
  margin-left: 2px;
}

.ed-palprev {
  display: block;
  margin-top: 8px;
  align-self: flex-start;
  outline: 2px solid #0a0d07;
  image-rendering: pixelated;
}

.ed-form {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.ed-field {
  display: flex;
  align-items: center;
  gap: 8px;
}

.ed-fkey {
  width: 120px;
  color: #9db18c;
  font-size: 11px;
  letter-spacing: 1px;
  text-transform: uppercase;
  flex: none;
}

.ed-field input[type="number"],
.ed-field input[type="text"],
.ed-field select {
  flex: 1;
  min-width: 0;
  background: #0d130a;
  color: #fff;
  border: 2px solid #0a0d07;
  outline: 1px solid #4a6b3c;
  border-radius: 0;
  font-family: inherit;
  font-size: 12px;
  padding: 5px 8px;
}

.ed-field input[type="color"] {
  width: 40px;
  height: 26px;
  background: #0d130a;
  border: 2px solid #0a0d07;
  padding: 0;
  border-radius: 0;
}

.ed-note {
  color: #77836c;
  font-size: 10px;
  letter-spacing: 1px;
}

.ed-hint {
  color: #8a957f;
  font-size: 12px;
}

.ed-ops {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  padding: 10px 14px;
  background: #141d10;
  border-top: 3px solid #0a0d07;
}

.ed-op {
  background: #3f5c33;
}

.ed-footer {
  padding: 8px 14px;
  background: #0c1309;
  color: #6f7d63;
  font-size: 10px;
  letter-spacing: 2px;
  text-align: center;
  border-top: 2px solid #0a0d07;
}

.ed-close {
  background: #b03c2b;
}

@media (max-height: 520px) {
  .panel {
    max-height: 86vh;
  }
}

/* Mobile URL/toolbars shrink the visible viewport below the vh-based cap, so a
   panel's foot (and the glyph sheet anchored to it) could land off-screen with
   no way to scroll it back. dvh tracks the live viewport, keeping every panel
   fully visible while its chrome is expanded. */
@supports (height: 100dvh) {
  .panel {
    max-height: 76dvh;
  }
  #panel-skills {
    max-height: 94dvh;
  }
  @media (max-height: 520px) {
    .panel {
      max-height: 86dvh;
    }
  }
}

/* Small / landscape screens: tighten popup padding so cards fit the viewport */
@media (max-width: 400px), (max-height: 400px) {
  .death-card,
  .reset-card {
    padding: 14px 16px 12px;
  }
  .death-card h2 {
    font-size: 16px;
  }
  .death-card .death-btn {
    min-width: 120px;
    padding: 8px 14px;
  }
}

/* Narrow screens: stack the forge list over its detail pane so neither
   squeezes into sub-tap targets, and let panel headers wrap their tool row. */
@media (max-width: 560px) {
  .crafting-body {
    flex-direction: column;
  }
  .craft-detail {
    width: auto;
    flex: 1 1 42%;
    min-height: 0;
  }
  .craft-recipes-list {
    flex: 0 0 50%;
  }
  .panel-head h2,
  .panel-title {
    width: 100%;
  }
}

/* ---------- Character creation menu ---------- */
#menu {
  position: fixed;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  /* Visible from first paint so the menu (and its landing copy) can be the
     Largest Contentful Paint without waiting on ~42 deferred scripts. JS owns
     visibility from here on: show() adds .show, hide() adds body.in-game. */
  opacity: 1;
  pointer-events: none;
  transition: opacity 0.25s;
  z-index: 40;
  padding: 16px;
}

#menu.show {
  opacity: 1;
  pointer-events: auto;
}

/* Hides the menu during play — .show is absent in exactly this state. */
body.in-game #menu {
  opacity: 0;
  pointer-events: none;
}

#menu:not(.show) * {
  pointer-events: none !important;
}

/* The landing section is static HTML with plain <a> links — never gated on
   JS, so it stays usable even if the game bundle fails to boot.
   Scoped with body:not(.in-game): #menu is a fixed, full-viewport layer, so
   while playing it is only opacity:0 — an invisible link would still swallow
   taps meant for the game (glyphs, HUD, panels). */
body:not(.in-game) #menu:not(.show) .menu-about a {
  pointer-events: auto !important;
}

/* In-game NOTHING inside the menu may be hit-testable. The menu layer sits
   above the game (z-index 40), so a single re-enabled descendant is enough to
   block a tap: this blanket rule outranks every pointer-events rule that
   targets a menu child (the landing-link exception above stops matching in
   the first place, this is the belt to its braces). */
body.in-game #menu,
body.in-game #menu * {
  pointer-events: none !important;
}

#menu-bg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  image-rendering: pixelated;
  z-index: 0;
}

.menu-frame {
  position: relative;
  z-index: 1;
  width: min(440px, 96vw);
  min-height: min(620px, 90vh);
  max-height: 98vh;
  overflow-y: auto;
  overflow-x: hidden;
  text-align: center;
  color: #f2eeca;
  background: rgba(16, 24, 12, 0.94);
  background-image: repeating-conic-gradient(rgba(87, 212, 106, 0.04) 0% 25%, transparent 0% 50%);
  background-size: 8px 8px;
  border: 4px solid #0a0d07;
  outline: 2px solid #4a6b3c;
  /* Artistic non-rectangular frame: clipped corners, straight sides. */
  clip-path: polygon(
    12% 0, 88% 0,
    100% 10%, 100% 90%,
    88% 100%, 12% 100%,
    0 90%, 0 10%
  );
  filter: drop-shadow(6px 6px 0 rgba(0, 0, 0, 0.55)) drop-shadow(0 0 40px rgba(0, 0, 0, 0.6));
  padding: 18px 22px 28px;
  scrollbar-width: thin;
  scrollbar-color: #3f5c33 #0a0d07;
}

.menu-frame::-webkit-scrollbar {
  width: 8px;
}

.menu-frame::-webkit-scrollbar-track {
  background: #0a0d07;
  border-left: 1px solid #1c2817;
}

.menu-frame::-webkit-scrollbar-thumb {
  background: #3f5c33;
  border: 1px solid #0a0d07;
}

.menu-frame::-webkit-scrollbar-thumb:hover {
  background: #57d46a;
}

.gear-picker-card {
  scrollbar-width: thin;
  scrollbar-color: #3f5c33 #0a0d07;
}

.gear-picker-card::-webkit-scrollbar {
  width: 8px;
}

.gear-picker-card::-webkit-scrollbar-track {
  background: #0a0d07;
  border-left: 1px solid #1c2817;
}

.gear-picker-card::-webkit-scrollbar-thumb {
  background: #3f5c33;
  border: 1px solid #0a0d07;
}

.gear-picker-card::-webkit-scrollbar-thumb:hover {
  background: #57d46a;
}

.menu-title {
  margin-bottom: 10px;
}

.menu-title h1,
.menu-title .menu-title-mark {
  letter-spacing: 0;
  margin: 0;
  min-height: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
}

/* Until renderTitle() swaps in the bitmap canvas, the brand reads as plain
   text instead of an empty 44px gap. */
.menu-title .menu-title-mark {
  font-size: 30px;
  font-weight: bold;
  color: #ffd23f;
  text-shadow: 2px 2px 0 #000;
}

.menu-title .tag {
  color: #9db18c;
  font-size: 11px;
  letter-spacing: 4px;
  margin-top: 0;
}

.menu-tabs {
  display: flex;
  gap: 6px;
  justify-content: center;
  margin-bottom: 8px;
  border-bottom: 3px solid #0a0d07;
  padding-bottom: 8px;
}

/* Two rows of tabs. The 3px rule belongs UNDER BOTH rows, so row 1 drops its
   own border/padding and row 2 draws the single rule — otherwise the strip
   would end up with a double line. */
.menu-tabs.menu-tabs-row1 {
  margin-bottom: 0;
  border-bottom: none;
  padding-bottom: 0;
}

/* After login the Settings tab (the "Log in / Sign up" slot) and the Chat
   tab swap places — order only, the DOM order is unchanged (js/menu.js
   syncAuthStatus toggles .swap). Sound stays last. */
.menu-tabs.menu-tabs-row1.swap [data-tab="auth"] {
  order: 1;
}
.menu-tabs.menu-tabs-row1.swap #menu-sound-toggle {
  order: 2;
}

.menu-tabs.menu-tabs-row2 {
  margin-top: 6px;
  flex-wrap: wrap;
}

/* flex-basis 0 + grow lets ONE tab fill the whole row (so it reads as a
   deliberate second row); a second tab added later splits the row, and
   min-width wraps to two-per-row on narrow screens. Kept at the same ~42px
   height as row 1 so it stays a comfortable touch target on phones. */
.menu-tabs.menu-tabs-row2 .menu-tab {
  flex: 1 1 0;
  min-width: 46%;
}

.menu-tab {
  flex: 1;
  cursor: pointer;
  background: #162110;
  border: 2px solid #0a0d07;
  color: #9db18c;
  font-family: inherit;
  font-size: 11px;
  font-weight: 900;
  letter-spacing: 1px;
  padding: 10px 6px 8px;
  text-transform: uppercase;
  white-space: nowrap;
  box-shadow: 0 3px 0 #0a0d07;
  transition: transform 0.05s, background 0.1s;
  /* Banner / pennant tab shape. */
  clip-path: polygon(8% 0, 92% 0, 100% 100%, 0 100%);
}

.menu-tab:hover,
.menu-tab.on {
  background: #223019;
  color: #ffd23f;
  border-color: #ffd23f;
}

.menu-tab.on {
  background: #2c3f20;
  box-shadow: 0 3px 0 #ffd23f;
}

.menu-tab:active {
  transform: translateY(2px);
  box-shadow: 0 0 0 #0a0d07;
}

/* Phones: row 1 has four tabs and "LOG IN / SIGN UP" no longer fits next to
   "SINGLE PLAYER" on one line — the nowrap label overflowed its pennant and
   the Single player text got cut off on the left. Both labels now break onto
   two rows INSIDE the button frame (the labels carry a hard line break, see
   index.html / menu.js; white-space: pre-line honours it while still wrapping).
   Wider screens keep white-space: nowrap from the base rule, which collapses
   the break back into one line. */
@media (max-width: 600px) {
  .menu-tab {
    white-space: pre-line;
    line-height: 1.25;
    padding: 8px 6px;
    text-align: center;
    /* Size each tab to its own (two-line) label instead of an equal sliver of
       the row — otherwise "LOG IN /" wraps twice and the tab shows 4 rows.
       Row 2's explicit `flex: 1 1 0` rule keeps priority (higher specificity). */
    flex: 1 1 auto;
    letter-spacing: 0.5px;
  }
  .menu-tabs.menu-tabs-row1 {
    flex-wrap: wrap;
  }
}

.menu-sound-btn {
  cursor: pointer;
  background: #162110;
  border: 2px solid #0a0d07;
  color: #7dfc88;
  font-family: inherit;
  font-size: 11px;
  font-weight: 900;
  letter-spacing: 1px;
  padding: 10px 6px 8px;
  text-transform: uppercase;
  white-space: nowrap;
  box-shadow: 0 3px 0 #0a0d07;
  transition: transform 0.05s, background 0.1s;
  clip-path: polygon(8% 0, 92% 0, 100% 100%, 0 100%);
  line-height: 1;
  min-width: 28px;
  text-align: center;
  margin-left: auto;
}
.menu-sound-btn::before {
  content: none;
}
.menu-sound-btn:hover {
  background: #223019;
  color: #ffd23f;
  border-color: #ffd23f;
}
.menu-sound-btn:active {
  transform: translateY(2px);
  box-shadow: 0 0 0 #0a0d07;
}
.menu-sound-btn.muted {
  color: #ff5d5d;
}

.menu-panel {
  display: none;
  text-align: left;
  animation: panel-in 0.15s ease-out;
}

.menu-panel.show {
  /* Column layout lets the shown panel fill leftover space so its footer
     strip (tutorial link) pins to the bottom divider instead of floating
     with dead space between the lines. Children never shrink — the frame
     scrolls tall content as before. */
  display: flex;
  flex-direction: column;
  flex: 1 0 auto;
}
.menu-panel.show > * {
  flex-shrink: 0;
}
.menu-panels {
  display: flex;
  flex-direction: column;
}

@keyframes panel-in {
  from { opacity: 0; transform: translateY(6px); }
  to { opacity: 1; transform: translateY(0); }
}

/* ---------- FIRST-IMPRESSION FLOW (js/first.js + js/coach.js) -------------
   First visit only (fresh device + feature never seen + not automated).
   Every rule below is inert unless js/first.js adds #menu.first-run or
   removes .hide — a normal load has neither, so today's menu is untouched
   byte-for-byte. Kill switch: CONFIG.FIRSTIMP / localStorage pixoot_first=0.
   Pixel law: hard borders, hard shadows, steps() motion, transform+opacity
   only — no blur, no glass, no rounded corners, no flashing (the blast is a
   one-shot monotonic grow-then-shrink, well under 3 luminance flips/s). */

/* ROUND 8 (owner override of round 2): while the attract card owns the menu
   BOTH blocks go display:none — the panel block AND the two .menu-tabs rows
   (the sound toggle lives inside row 1, so it goes with it). The first
   screen is now title + tagline + the verb lines + PLAY (+ the cookie card
   and the below-fold About/legal links, which stay in flow underneath):
   FIRST IMPRESSION = PLAY ONLY, nothing else to tap. Reverted because
   clicking a tab while the cookie question was unanswered left the menu in a
   half state (see js/first.js onDocClick — every non-cookie, non-PLAY tap
   now settles the whole state machine in ONE place).

   .menu-panels stays display:none (it is buttons + panels — .click() and
   value writes still work on hidden elements, which the _mmtest suite
   relies on). Dropped by ONE real choice: a non-silent switchTab(), a tap
   outside the cookie card, or the PLAY button — then every rule in this
   block is inert again and today's menu is untouched byte-for-byte. */
#menu.first-run .menu-panels,
#menu.first-run .menu-tabs {
  display: none;
}

/* The two first-visit cards: hidden by the global .hide, in normal flow
   right after the title block (never a viewport overlay — the landing links
   must stay hit-testable). */
.first-card {
  margin: 6px 0 16px;
  padding: 14px 14px 16px;
  text-align: center;
}
.first-card .first-verb {
  display: block;
  margin: 2px 0 8px;
}
.first-card .first-sub {
  color: #9db18c;
  font-size: 11px;
  line-height: 1.5;
  margin: 2px 0 12px;
}
/* Painted sub-line = inline-block canvases, and the 5x7 font never wraps a
   line — stack them so the sentence reads as written (same rule as the coach
   copy in .coach .coach-copy .px-label). */
.first-card .first-sub .px-label {
  display: block;
}
/* ROUND 8 (items 1 + 3) — LEADING on every MULTI-LINE painted surface of the
   first-impression flow: a 4px MARGIN between the stacked canvases. It is the
   same 4px on all three, so a two-line chip, the coach's multi-line copy and
   the NAME YOUR HERO card's two rows read with identical spacing:
     .first-sub      the rename card's two painted rows (item 3)
     .coach-copy     the coach marks' multi-line copy (item 1)
     .seq-closer     the F5/F7/F8 chips + the F6 closer (item 1)
   A MARGIN, never line-height: .px-label keeps `line-height: 0` (a line-height
   on a zero-height inline-block only shifts the canvas box — no real gap, and
   a sub-pixel one would blur the pixel grid). 4px is a whole pixel step at
   every zoom and matches the 2px-pixel font's own row rhythm (4 = 2 rows).
   The `+ .px-label` sibling selector touches ONLY a 2nd, 3rd… row, so every
   single-line surface in the flow is byte-identical to round 7. */
.first-card .first-sub .px-label + .px-label,
.coach .coach-copy .px-label + .px-label,
.seq-closer .px-label + .px-label {
  margin-top: 4px;
}
.first-card .m-label {
  text-align: left;
}
.first-card input {
  width: 100%;
  font-family: inherit;
  background: #0d130a;
  color: #fff;
  border: 2px solid #0a0d07;
  outline: 2px solid #4a6b3c;
  padding: 10px;
  font-size: 15px;
  letter-spacing: 2px;
  text-align: center;
  text-transform: uppercase;
}
/* Primary PLAY / SAVE: the game's GREEN accent CTA (design D4) — >=56px tall
   (mobile target rule), keyboard-reachable (real <button>), label painted in
   #eaffd8 for contrast (#eaffd8 on #3f6b2a ≈ 5.8:1, >=4.5:1). */
.first-play {
  display: block;
  width: 100%;
  min-height: 56px;
  margin-top: 10px;
  background: #3f6b2a;
  border: 2px solid #57d46a;
  color: #eaffd8;
  outline-color: #0a0d07;
  text-align: center;
}
/* Ghost CO-OP: dark body + green — reads secondary to PLAY, same box. */
.first-coop {
  display: block;
  width: 100%;
  min-height: 46px;
  margin-top: 8px;
  background: #162110;
  color: #7dfc88;
  outline-color: #3f5c33;
  text-align: center;
}
/* KEEP <name>: both halves are painted px labels, so the row just centers
   them (the old inline HTML Courier half is gone — index.html). */
#rename-keep {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
}
/* The rename box's ✕ is the cookie card's OWN close chassis (.cc-x, unchanged
   — same 26px ink box, same geometry, same ::after pad past the 44px law), so
   the card is the only positioning context it needs: without this it would
   anchor to .menu-frame (the nearest positioned ancestor) and land in the
   frame's corner. js/first.js — the ✕ hides without consuming the offer. */
#rename-card {
  position: relative;
}
/* The rename card's error line: the shared .m-info box, painted red. */
#rename-error {
  color: #ff5d5d;
}

/* The self-pressing PLAY blast (F1): fixed vs the viewport (it lives outside
   .menu-frame, which has a filter + overflow that would contain/clip it),
   sized to the PLAY button's rect by js/first.js and driven through five
   steps() phases (round 6 — heavier, ON SCREEN, and actually skippable):

     POP     160ms  0 → 1.15 → 1
     SWELL   340ms  1 → k        (k = MAX(1, MIN(1.25, (vw-24)/w)) — never
                                  leaves the viewport, never blows past the
                                  card's neighbourhood; the old viewport-
                                  diagonal blow-up is GONE)
     HOLD    160ms  static       (this is the weight)
     PRESS   160ms  k → 1.08, background darkens
     ABSORB  240ms  1.08 → 0.06 IN PLACE, brightens, opacity to 0 at the end
     …then the existing scene.cover() dip (150 + 240 = 390ms)
   = 1060ms of animation + 390ms dip ≈ 1.45s, on top of the 400ms read beat
   (PRESS_MS in js/first.js — still the tunable). animation-fill-mode is
   forwards (NOT both): a delayed phase must not apply its from-state early
   and clobber the one that is actually running — every phase after the first
   is invisible until its own delay. */
#first-blast {
  position: fixed;
  display: none;
  z-index: 2;
  pointer-events: none;
  background: #3f6b2a;
  border: 2px solid #57d46a;
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.25);
  clip-path: polygon(
    6px 0, calc(100% - 6px) 0,
    100% 6px, 100% calc(100% - 6px),
    calc(100% - 6px) 100%, 6px 100%,
    0 calc(100% - 6px), 0 6px
  );
  color: #eaffd8;
  transform-origin: 50% 50%;
}
#first-blast.run {
  display: block;
  animation:
    firstPop    160ms steps(4, end) 0ms,
    firstSwell  340ms steps(6, end) 160ms,
    firstHold   160ms steps(1, end) 500ms,
    firstPress  160ms steps(4, end) 660ms,
    firstAbsorb 240ms steps(6, end) 820ms;
  animation-fill-mode: forwards;
}
@keyframes firstPop {
  0% { transform: scale(0); }
  70% { transform: scale(1.15); }
  100% { transform: scale(1); }
}
@keyframes firstSwell {
  from { transform: scale(1); }
  to { transform: scale(var(--k, 1.25)); }
}
/* The HOLD is a real beat: the button sits at full swell, dead still. */
@keyframes firstHold {
  from { transform: scale(var(--k, 1.25)); }
  to { transform: scale(var(--k, 1.25)); }
}
@keyframes firstPress {
  from { transform: scale(var(--k, 1.25)); background: #3f6b2a; }
  to { transform: scale(1.08); background: #2c4d1e; }
}
/* ABSORB: same place, no travel — it shrinks IN VIEW, brightens as it goes
   and only fades out on the very last step (fill-mode: forwards holds the
   invisible end state until js/first.js removes .run). */
@keyframes firstAbsorb {
  0% { transform: scale(1.08); background: #2c4d1e; opacity: 1; }
  55% { background: #57d46a; }
  100% { transform: scale(0.06); background: #7dff8d; opacity: 0; }
}
/* The card's own 200ms crossfade under reduced motion (the dance is skipped;
   the scene dip is already skipped by scene.reduced()/flashOff there). */
.first-card.first-fading {
  opacity: 0;
  transition: opacity 200ms linear;
}

/* Coach marks (js/coach.js) — #coach-host lives inside #hud (in-world only),
   above the dock (z 15) but below the menu layer (40) and the cookie card
   (94): a mark must never cover the consent buttons or a menu-time dialog.
   Host carries the anchor: .anchor-top (fixed under the topbar) or the
   bottom anchor, whose `bottom` px offset js/coach.js measures (dock height
   / consent-card height, so a mark always sits ABOVE the consent card).
   Copy is painted with PIXOOT.pf.setEl at show time — same lettering as
   #tip-adjacent HUD labels. Pointer-events pass through the host; only the
   ✕ re-enables them (>=44px, no hover-only affordance). */
#coach-host {
  position: fixed;
  left: 50%;
  /* ROUND 7: max-content is what lets a `left:50%` box really centre itself
     (shrink-to-fit would measure against only half the viewport), so the
     hero-anchored MOVE mark's box is centred like its placement rule says. */
  width: max-content;
  transform: translateX(-50%);
  z-index: 20;
  display: none;
  pointer-events: none;
  max-width: min(92vw, 420px);
  text-align: center;
}
#coach-host.show {
  display: block;
}
.coach {
  position: relative;
  padding: 12px 38px 13px 14px;
  animation: coachIn 180ms steps(3, end) 1;
}
.coach .coach-copy {
  display: block;
}
/* Painted labels are inline-blocks (px-label) and would run together on one
   line at >=429px — stack them so the copy reads as written. */
.coach .coach-copy .px-label {
  display: block;
}
/* ✕ CLOSE: the game's ink-box close (.cc-x chassis minus z-index, plus
   pointer-events:auto because the host is pointer-events:none). 26px box +
   the ::after pad keeps the real tap target >=44px. */
.coach-x {
  position: absolute;
  top: 6px;
  right: 6px;
  width: 26px;
  height: 26px;
  padding: 0;
  background: #0a0d07;
  border: 2px solid #55604d;
  color: #cfcba9;
  font-family: inherit;
  font-size: 13px;
  font-weight: 900;
  line-height: 22px;
  text-align: center;
  cursor: pointer;
  touch-action: manipulation;
  pointer-events: auto;
}
.coach-x:hover,
.coach-x:active { background: #131a0e; border-color: #55604d; color: #cfcba9; }
.coach-x::after { content: ""; position: absolute; inset: -9px; }
@keyframes coachIn {
  from { opacity: 0; transform: translateY(6px); }
  to { opacity: 1; transform: translateY(0); }
}
@media (prefers-reduced-motion: reduce) {
  /* Reduced motion = instant appear, no scale dance anywhere in the flow. */
  .coach { animation: none; }
  #first-blast.run { animation: none; }
}

/* The consent gate on the animated start (CONFIG.FIRSTIMP.gate): while the
   cookie question is unanswered PLAY sits HELD — static and visibly inert (a
   dimmed CTA, NEVER a spinner or a shimmer), and js/first.js keeps the real
   <button> `disabled` so a tap enters nothing. The label keeps its painted
   pixels; only the chassis dims. Released the instant consent resolves. */
.first-play.held {
  background: #26391c;
  border-color: #4a6b3c;
  color: #9fd7a8;
  cursor: default;
  animation: none;
}
.first-play.held .px-label { opacity: 0.7; }

/* EVERY guide text leaves with a 260ms steps() fade (js/coach.js endCurrent,
   js/seq.js hideChip/hideCloser) — the node stays mounted for the whole
   transition and is only removed when it lands. Applies to ✕, ESC,
   self-resolution, the step to the next mark and the menu return alike. */
#coach-host.leaving {
  opacity: 0;
  transition: opacity 260ms steps(6, end);
}

/* ---- F5/F6/F7/F8 sequence host (js/seq.js): the instruction card (the
   F5/F7/F8 chips + the F6 closer). ROUND 7 (Change A): HORIZONTALLY CENTRED
   (owner: "they should be middle and a bit above the character, between the
   character and the level-up popup") — `width: max-content` is what lets a
   `left:50%` box really centre itself instead of shrink-to-fitting against
   half the viewport; the VERTICAL offset is js/first.js guidePlace() (the
   hero's real screen position + the banner/topbar/head/dock clamps), applied
   on every 200ms tick and on resize. In-world only (inside #hud), above the
   dock (z 15) and the overlay (19), below every menu-time dialog.
   ROUND 6: there is NO TAP TO RESUME chip any more — sequences never pause,
   so no resume node, no resume selector, no button. ---- */
#seq-host {
  position: fixed;
  left: 50%;
  right: auto;
  width: max-content;
  /* ROUND 8: 340 → 380. Every chip now carries the ✕ gutter (12 + 38px of
     padding), and js/seq.js paintCue measures its copy against THIS cap.
     ROUND 9 (bug 1): the measure is the card's REAL content box,
     `min(0.84·vw, 380) − 56` (cap minus the 3px border on each side and the
     12 + 38px padding) — round 8 used `min(0.92·vw, 380) − 50`, which is up
     to 0.08·vw + 6 px wider than the box a card here can actually be, so the
     F6 closer's 27-char line (324px at px2) was admitted at phone widths
     whose content box held less: the centred canvas spilled out of the card
     and under the ✕. px2 now survives on every desktop width and drops to
     px1 exactly where the gutter would not fit. */
  max-width: min(84vw, 380px);
  transform: translateX(-50%);
  text-align: center;
  z-index: 20;
  display: block;
  pointer-events: none;
  bottom: calc(24px + env(safe-area-inset-bottom));
}
.seq-closer {
  pointer-events: auto;
  opacity: 0;
  transform: translateY(6px);
  transition: opacity 180ms steps(3, end), transform 180ms steps(3, end);
  position: relative;
  display: block;
  margin-top: 8px;
  /* ROUND 9 (bug 1): the card must ALWAYS contain its ✕. The box is 26px
     tall and sits 3px (border) + 6px (inset) below the card's top, so its
     bottom edge is 35px down — but a ONE-line card painted at px1 is only
     6 + 20 + 7 = 33px tall (the F6 closer drops to px1 below ~452px), and
     the ✕ then hung 2px OUT of the card ("sometimes off the instruction
     box"). 40px is the floor that keeps >= 2px of clearance under the box at
     every width, and it equals the 1-line px2 card's own height, so nothing
     changes at desktop widths (the 2-line chips are 58px). */
  min-height: 40px;
  /* ROUND 8 (item 7): the SAME 38px right gutter the coach card carries —
     every chip now carries the ✕ (item 8), so the 26px ink box + its pad has
     to sit NEXT TO the copy instead of on top of its last characters.
     Designer r9 NIT: left 12 → 14 so the copy's centre offset (38−14)/2 = 12
     matches the coach card and the SKIP confirm exactly (was 13). */
  padding: 12px 38px 12px 14px;
  text-align: center;
}
.seq-closer.in { opacity: 1; transform: translateY(0); }
.seq-closer .px-label { display: block; }
/* F8 abort ✕ (js/seq.js ensureX) — the coach ✕ chassis: a 26px ink box whose
   ::after pad keeps the real tap target >= 44px, pointer-events re-enabled on
   a host that passes everything through. ROUND 8 (item 8): it no longer
   dismisses — it asks SKIP THE TUTORIAL? first (js/seq.js askSkip). */
.seq-x {
  position: absolute;
  top: 6px;
  right: 6px;
  width: 26px;
  height: 26px;
  padding: 0;
  background: #0a0d07;
  border: 2px solid #55604d;
  color: #cfcba9;
  font-family: inherit;
  font-size: 13px;
  font-weight: 900;
  line-height: 22px;
  text-align: center;
  cursor: pointer;
  touch-action: manipulation;
  pointer-events: auto;
  z-index: 5;
}
/* ROUND 8 (item 7): HOVER PARITY with the game's other ink-box X (.cc-x on
   the cookie card, .coach-x on a mark) — the identical monochrome triple
   (bg #131a0e, border #55604d, glyph #cfcba9), the identical ::after pad and
   the identical pointer-events: the chip's X must be indistinguishable from
   every other X in the game. (.panel-x/.overlay-x keep their RED hover —
   that is the destructive-action style of a panel close, deliberately NOT
   the reference here: this X only opens a question.) Monochrome on :active
   too, so touch gets the same feedback as hover, and nothing about the
   affordance is hover-only (the box is always painted). */
.seq-x:hover,
.seq-x:active { background: #131a0e; border-color: #55604d; color: #cfcba9; }
.seq-x::after { content: ""; position: absolute; inset: -9px; }
/* F8 guided freeze overlay (js/seq.js): the dim + spotlight + gesture trail.
   Inside #hud (in-world only) at z 19 — above the dock (15) and the topbar
   (12), below #coach-host / #seq-host (20), and #level-banner (23) is a body
   layer so the LEVEL UP banner always stays above it. pointer-events: none is
   NON-NEGOTIABLE: the draw gesture must still reach #game underneath. */
#f8-overlay {
  position: fixed;
  inset: 0;
  /* ROUND 9 (bug 2): `inset: 0` ALONE DOES NOT SIZE A CANVAS. A <canvas> is
     a REPLACED element, so with `width/height: auto` its used size is its
     INTRINSIC size (the width/height ATTRIBUTES js/seq.js writes = viewport x
     devicePixelRatio) and the over-constrained `right/bottom` are dropped. At
     dpr 1 (the old desktop test profile, 1280x800) intrinsic === viewport, so
     it accidentally lined up; at dpr 2/3 (every real phone) the element was
     2-3x the screen — only its top-left corner was ON the screen, while
     ctx.setTransform(dpr) mapped every drawn coordinate x dpr, so the hero's
     lit disk and the popup hole landed at dpr x their intended place (the
     disk in the screen's bottom-right CORNER, the dim covering everything):
     "on mobile the dimmed area of the glyph instruction is just way off
     screen". 100% pins the layout box to the viewport exactly like #game's
     own rule; js/seq.js paintOverlay mirrors main.js resize() and sets the
     inline box too. Geometry: rect == viewport, backing == viewport x dpr. */
  width: 100%;
  height: 100%;
  z-index: 19;
  pointer-events: none;
  image-rendering: pixelated;
}
/* It clears with the same fade every guide text uses (260ms steps, 0ms under
   reduced motion — see the block below). */
#f8-overlay.f8-leaving {
  opacity: 0;
  transition: opacity 260ms steps(6, end);
}

/* ---- ROUND 8 (item 8) — SKIP THE TUTORIAL? ---------------------------------
   The chip's ✕ no longer dismisses: it raises this confirm, built from the
   repo's EXISTING confirm chassis (.delete-confirm-box / -title / -actions +
   .px-btn — the same box every DELETE HERO? / RESET GLYPH? / DISMANTLE?
   dialog uses, restyled nowhere), mounted INSIDE #seq-host so it is placed by
   the ONE shared guidePlace() rule: centred, ~40px above the hero.
   The chassis box is `position:fixed; top/left 50%; translate(-50%,-50%)` for
   the menu-time dialogs — here the HOST is the positioned element js/first.js
   moves, so the box drops back into normal flow (relative, no transform) and
   takes the host's width.
   [KEEP GOING] is the default (focused) target: Enter therefore CANCELS, Esc
   CANCELS too (a capture-phase key bound with the dialog, so it can never
   fall through to the panel/settings Escapes), and only a deliberate tap or a
   Tab onto [SKIP] + Enter spends the guide. Both buttons are >=44px tall
   (.px-btn's own min-height is 40 — lifted here for the touch law). */
#seq-host .skip-confirm {
  position: relative;
  top: auto;
  left: auto;
  right: auto;
  transform: none;
  width: 100%;
  min-width: 0;
  max-width: 100%;
  margin-top: 8px;
  pointer-events: auto;
  z-index: 1;
  /* ROUND 9 (bug 1): the SAME right-hand ✕ gutter the instruction chip's
     copy reserves (12 + 38), here 44px so the ✕ — 9px in from the card's
     outer edge, 26px wide — lands in clear space with >= 12px of daylight
     before the title. Same corner, same offset as every chip: the box keeps
     .delete-confirm-box's 3px border, so the ✕ centre is a constant
     22px from the card's top-right corner on both surfaces. Without the
     gutter the ✕ sat ON the title (the reason the chip carries 38px of
     right padding at all). Designer r9 NIT: the left gutter matched the
     right (44/44) so the question's CONTENT — title and both buttons —
     centres on the card's true centre instead of sitting 12px left of it
     (the asymmetric 20/44 would have done that), and it buys the buttons
     29px of slack at 360 instead of the zero they had. */
  padding: 16px 44px;
}
#seq-host .skip-confirm .px-btn {
  flex: 1 1 auto;
  min-height: 44px;
  min-width: 44px;
}
/* The guide's own copy is RELEASED while the question stands (one
   instruction at a time — the question IS the instruction), and comes back
   through stepEnter() the moment [KEEP GOING] lands. */
/* The card leaves the same way every other guide text does: with the fade. */
.seq-closer.seq-leaving {
  opacity: 0;
  transform: translateY(4px);
  transition: opacity 260ms steps(6, end), transform 260ms steps(6, end);
}

/* ---- F5/F6/F7 sequence highlights (js/seq.js): a GOLD RING is the whole
   vocabulary — highlight only, this file never presses anything. Exactly one
   step's worth is ever on screen (the sweep is the single exception: the 12
   clutch buttons it lights ARE the one gesture).

   ROUND 6 — THE GLOW LAW: every guided target carries .seq-hi (the cold
   static outline) PLUS .seq-loop, a CONSTANT loop that never stops and never
   goes fully off. It animates BOX-SHADOW ONLY — banded integer-px rings
   (2-3px #ffd23f band + a 2px #0a0d07 keyline + a low-alpha gold halo),
   never a blur, never a gradient, never a layout property, never a
   transform/filter on the target itself.

     1200ms · steps(4, end) · infinite = DIM → MID → HIGH → MID
     four 300ms HOLDS ≈ 0.83Hz, i.e. ~3 luminance jumps a second (the law's
     ceiling), each state a flat ring, each change one hard pixel step.
   The `.999` stops are what make each state a HOLD: the animation's steps()
   only ever rides the 0.012ms gap between them, so the loop never ramps and
   never sub-steps into a 13Hz flicker.

   `.seq-out` is the SAME loop for small controls with no interior room
   (.panel-x / .overlay-x): the rings sit OUTSIDE the box instead of inset. ---- */
.seq-hi { box-shadow: inset 0 0 0 2px #ffd23f; }
.seq-hi.seq-out { box-shadow: 0 0 0 2px #ffd23f; }
.seq-loop { animation: seqLoop 1200ms steps(4, end) infinite; }
.seq-loop.seq-out { animation-name: seqLoopOut; }
@keyframes seqLoop {
  0%, 24.999% {
    box-shadow: inset 0 0 0 2px rgba(255, 210, 63, 0.38), inset 0 0 0 4px #0a0d07,
                inset 0 0 0 7px rgba(255, 210, 63, 0.10);
  }
  25%, 49.999% {
    box-shadow: inset 0 0 0 2px rgba(255, 210, 63, 0.76), inset 0 0 0 4px #0a0d07,
                inset 0 0 0 7px rgba(255, 210, 63, 0.22);
  }
  50%, 74.999% {
    box-shadow: inset 0 0 0 3px #ffd23f, inset 0 0 0 5px #0a0d07,
                inset 0 0 0 8px rgba(255, 210, 63, 0.34);
  }
  75%, 99.999% {
    box-shadow: inset 0 0 0 2px rgba(255, 210, 63, 0.76), inset 0 0 0 4px #0a0d07,
                inset 0 0 0 7px rgba(255, 210, 63, 0.22);
  }
}
@keyframes seqLoopOut {
  0%, 24.999% {
    box-shadow: 0 0 0 2px rgba(255, 210, 63, 0.38), 0 0 0 4px #0a0d07,
                0 0 0 7px rgba(255, 210, 63, 0.10);
  }
  25%, 49.999% {
    box-shadow: 0 0 0 2px rgba(255, 210, 63, 0.76), 0 0 0 4px #0a0d07,
                0 0 0 7px rgba(255, 210, 63, 0.22);
  }
  50%, 74.999% {
    box-shadow: 0 0 0 3px #ffd23f, 0 0 0 5px #0a0d07,
                0 0 0 8px rgba(255, 210, 63, 0.34);
  }
  75%, 99.999% {
    box-shadow: 0 0 0 2px rgba(255, 210, 63, 0.76), 0 0 0 4px #0a0d07,
                0 0 0 7px rgba(255, 210, 63, 0.22);
  }
}
/* F5.3 — the bag's OWN hint line while the `row` beat carries the
   instruction: it leaves with the same 260ms steps() fade as every other
   guide text (30s with no progress), and comes back the moment the step
   re-lights. js/seq.js hintSync/hintRestore own the copy. */
#bag-detail .bag-hint { transition: opacity 260ms steps(6, end); }
#bag-detail .bag-hint.seq-fade { opacity: 0; }
/* SF1 — the F7 HELD toast is instruction too, so the 30s idle takes it with
   the same 260ms steps() fade (js/ui.js toastRelease(true)); a progress
   re-hold takes the fading node back instead of mounting a second one. */
.toast.seq-fade { opacity: 0; transition: opacity 260ms steps(6, end); }
/* F6.2 — ONE pass, 12 buttons × 90ms ≈ 1.1s, staggered with animation-delay
   (the --seq-i index js/seq.js writes), one iteration, no loop, no blur.
   fill-mode is deliberately none: a button is lit ONLY inside its own 90ms
   window and falls back to neutral the instant it ends. A sweep is not a
   flash — it never inherits .seq-loop. */
#right-clutch .hud-btn.seq-sweep {
  animation: seqSweep 90ms steps(1, end) calc(var(--seq-i, 0) * 90ms) 1;
}
@keyframes seqSweep {
  from { box-shadow: inset 0 0 0 2px #ffd23f; background: #2a3a1e; }
  to { box-shadow: inset 0 0 0 2px #ffd23f; background: #2a3a1e; }
}
@media (prefers-reduced-motion: reduce) {
  /* Reduced motion = the same states, no travel: the leave fades are
     instant and the sweep never runs — but the LOOP still has to READ as an
     affordance, so it freezes into a high-contrast DOUBLE ring (gold band +
     keyline + gold halo, never animated, never off). */
  #coach-host.leaving,
  .seq-closer,
  .seq-closer.seq-leaving,
  #f8-overlay.f8-leaving { transition: none; }
  .seq-loop {
    animation: none;
    box-shadow: inset 0 0 0 3px #ffd23f, inset 0 0 0 5px #0a0d07,
                inset 0 0 0 8px rgba(255, 210, 63, 0.6);
  }
  .seq-loop.seq-out {
    box-shadow: 0 0 0 3px #ffd23f, 0 0 0 5px #0a0d07,
                0 0 0 8px rgba(255, 210, 63, 0.6);
  }
  #right-clutch .hud-btn.seq-sweep { animation: none; }
}

.m-label {
  display: block;
  text-align: left;
  color: #cfcba9;
  font-size: 10px;
  letter-spacing: 1px;
  margin: 4px 0 2px;
}

#char-name {
  width: 100%;
  font-family: inherit;
  background: #0d130a;
  color: #fff;
  border: 2px solid #0a0d07;
  outline: 2px solid #4a6b3c;
  padding: 10px;
  font-size: 15px;
  letter-spacing: 2px;
  text-align: center;
  text-transform: uppercase;
}

.skin-row {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  justify-content: center;
  margin-bottom: 6px;
}

.skin-btn {
  pointer-events: auto;
  cursor: pointer;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  background: #162110;
  border: 2px solid #0a0d07;
  box-shadow: 2px 2px 0 rgba(0, 0, 0, 0.45);
  color: #cfcba9;
  font-size: 9px;
  font-weight: 900;
  letter-spacing: 1px;
  padding: 6px 8px;
  font-family: inherit;
  text-transform: uppercase;
  transition: transform 0.05s;
}

.skin-btn:active {
  transform: translateY(1px);
}

.skin-btn .swatch {
  width: 26px;
  height: 34px;
  border: 2px solid #0a0d07;
  display: block;
}

.skin-btn.on {
  outline: 2px solid #ffd23f;
  background: #223019;
}

.px-btn.wide {
  width: 100%;
  margin-top: 10px;
}

.px-btn.accent {
  background: #3f6b2a;
  color: #f2eeca;
  outline-color: #57d46a;
}

.px-btn.danger {
  background: #5a1e1e;
  color: #f2eeca;
  outline-color: #e05e5e;
}

.m-info {
  color: #9db18c;
  font-size: 10px;
  margin-top: 6px;
  min-height: 14px;
  line-height: 1.4;
}

.char-list,
.coop-rooms {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin-top: 6px;
  overflow-y: auto;
  scrollbar-width: thin;
  scrollbar-color: #3f5c33 #0a0d07;
}

/* Tall enough for 3 heroes so the origin badge of the last one is visible
   without an inner scroll; the menu frame itself scrolls if there are more. */
.char-list {
  max-height: 210px;
}

.coop-rooms {
  max-height: 140px;
}

.char-list::-webkit-scrollbar,
.coop-rooms::-webkit-scrollbar {
  width: 6px;
}

.char-list::-webkit-scrollbar-track,
.coop-rooms::-webkit-scrollbar-track {
  background: #0a0d07;
}

.char-list::-webkit-scrollbar-thumb,
.coop-rooms::-webkit-scrollbar-thumb {
  background: #3f5c33;
  border: 1px solid #0a0d07;
}

.char-row {
  cursor: pointer;
  /* GRID, never flex (defect D7): under `display:flex;
     justify-content:space-between` the portrait's x-position depended on HOW
     MANY children the row had — account rows have 3 (no UPLOAD arrow), local
     rows 4, the New Hero row 2 — so the portrait slid sideways between
     buckets and states. Every column is a FIXED width on purpose: with an
     `auto` action track the flexible first track (1fr) would absorb the
     difference and the portrait would still move between row variants.
     col1 = text (flexes), col2 = 40px portrait, col3 = 20px delete,
     col4 = 24px upload — identical for every row, so an empty action cell
     just reads as space. @media (pointer:coarse) below widens cols 3+4 to
     44px for real tap targets, still the same for every row on that device. */
  display: grid;
  grid-template-columns: minmax(0, 1fr) 40px 20px 24px;
  align-items: center;
  gap: 8px;
  width: 100%;
  text-align: left;
  background: #0d130a;
  border: 2px solid #0a0d07;
  outline: 1px solid #3f5c33;
  color: #f2eeca;
  font-family: inherit;
  padding: 6px 8px;
  transition: background 0.1s;
}

.char-row:hover {
  background: #14200f;
}

.char-row.on {
  outline: 2px solid #ffd23f;
  background: #162110;
}

.char-portrait {
  width: 40px;
  height: 48px;
  flex: 0 0 auto;
  background: #0d130a;
  border: 2px solid #0a0d07;
  outline: 1px solid #3f5c33;
  image-rendering: pixelated;
}

.char-left {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}

.char-name {
  font-size: 13px;
  font-weight: 900;
  letter-spacing: 1px;
  text-transform: uppercase;
  color: #ffd23f;
}

.char-meta {
  font-size: 10px;
  color: #9db18c;
  letter-spacing: 1px;
}

/* Origin badge: LOCAL = this device, ACCOUNT = cloud mirror. Kept as pure
   text + border so it renders identically on every platform font. */
.char-origin {
  display: inline-block;
  margin-left: 4px;
  padding: 1px 5px;
  font-size: 9px;
  line-height: 1.3;
  letter-spacing: 1px;
  text-transform: uppercase;
  border: 1px solid currentColor;
  vertical-align: middle;
  white-space: nowrap;
}

.char-origin.local {
  color: #57d46a;
  background: rgba(87, 212, 106, 0.08);
}

.char-origin.acct {
  color: #6fb7ff;
  background: rgba(111, 183, 255, 0.08);
}

/* Inline SVG icons — never platform-font glyphs (☁ / ⬂ / ⬆ / ◀ render as
   tofu, or at zero width, on devices missing those code points). */
.ico {
  display: inline-block;
  vertical-align: middle;
  flex: 0 0 auto;
}

/* OWNER FIX (A6): the badge is the bordered WORD alone — the old `.ico`
   (legacy SVG) and the redesign's `.px-ic` grid swap are no longer produced
   by js/menu.js buildCharMeta/menuPx.badge in EITHER mode, so their sizing
   rules are gone with them (no orphaned margins). */
.char-name .ico {
  width: 9px;
  height: 8px;
  margin-left: 5px;
}

.char-del {
  color: #e05e5e;
  font-size: 18px;
  font-weight: 900;
  padding: 0 4px;
  cursor: pointer;
  user-select: none;
  line-height: 1;
  /* The cell is a fixed grid track (see .char-row) — centre the glyph in it
     so every row's × sits on the same x, present or not. */
  text-align: center;
}
.char-mig {
  color: #57d46a;
  font-size: 16px;
  font-weight: 900;
  padding: 0 4px;
  cursor: pointer;
  user-select: none;
  line-height: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.char-mig .ico {
  width: 16px;
  height: 16px;
  display: block;
}
.char-mig:hover {
  color: #8afe9a;
}

/* ==========================================================================
   ROSTER FIRST-PAINT + PROGRESSIVE ARRIVAL  (menu first-paint design)
   --------------------------------------------------------------------------
   The roster used to be an EMPTY HOLE in the first paint: the list shipped
   with 0 height and only gained rows once JS finished the portrait pass, so
   the menu frame jumped — and a slow login pull left a frozen "Loading
   heroes…" line behind forever. Three cooperating pieces fix that:

   1. FIRST PAINT (index.html): both lists ship `.char-list.is-loading` with
      3 static `.char-skel` rows + an sr-only line, inside a CONSTANT-height
      `.roster-slot` status band. 3 x 64px + 2 x 6px gaps = 204px = the height
      the list has anyway, so the swap to real rows is a pixel-for-pixel
      replace (zero CLS for rosters of >= 3).
   2. ARRIVAL (js/menu.js): portrait canvases are CSS-boxed at 40x48 and baked
      on a 6 ms/frame rAF budget — pixels may land a frame late, geometry
      never moves.
   3. STATE (js/menu.js setRosterState): idle | busy | stalled | error writes
      copy into BOTH #sp-roster-status and #coop-roster-status, toggles both
      RETRY buttons, then re-renders. A pull in flight keeps the skeleton and
      suppresses the false "No account heroes yet" empty state; a definitive
      error with 0 rows replaces it with ONE .char-error row + a RETRY.

   INCIDENT LEVER — html.no-skel (localStorage pixoot_skel=0, parsed once in
   js/menu.js): hides every skeleton row and both status slots and drops the
   reserved 204px, restoring the pre-design empty state. Also settable live:
   document.documentElement.classList.add("no-skel"). NOTE: the lever hides
   the RETRY button along with the slot — clear it to get retry back.
   ========================================================================== */

/* 1. Reserved first-paint height — 3 rows x 64px + 2 x 6px list gaps. */
.char-list.is-loading {
  min-height: 204px;
}

/* 2. Skeleton rows. The BOX comes from .char-row / .char-portrait (40x48
      portrait cell, 2px #0a0d07 border, 1px #3f5c33 outline, #0d130a fill,
      64px total row height) — the skeleton only swaps the CONTENT, so the
      real row lands on exactly the same pixels. Visually inert = literally
      inert (no hover, no tap, no tab stop: it is a <div>, not a <button>). */
.char-row.char-skel {
  cursor: default;
  pointer-events: none;
  /* The PULSE: shipped @keyframes queue-pulse, no new keyframe vocabulary
     (frozen by the reduced-motion block below). */
  animation: queue-pulse 1.1s ease-in-out infinite;
}
.char-skel .char-left {
  gap: 2px;
}
.skel-bar {
  display: block;
  background: #2a3823;
}
.skel-bar.skel-name {
  width: 44%;
  height: 13px;
}
.skel-bar.skel-meta {
  width: 66%;
  height: 10px;
}
.skel-portrait {
  position: relative;
  width: 40px;
  height: 48px;
  background: #0d130a;
  border: 2px solid #0a0d07;
  outline: 1px solid #3f5c33;
}
/* Pixel silhouette: a 5x6 "hero" built from 4px box-shadow cells — no image,
   no SVG, no canvas, so it paints in the very first frame with zero JS. */
.skel-portrait::after {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  width: 4px;
  height: 4px;
  transform: translate(-10px, -12px);
  background: transparent;
  box-shadow:
    4px 0 0 #31412a, 8px 0 0 #31412a, 12px 0 0 #31412a,
    4px 4px 0 #31412a, 8px 4px 0 #31412a, 12px 4px 0 #31412a,
    0 8px 0 #31412a, 4px 8px 0 #31412a, 8px 8px 0 #31412a,
    12px 8px 0 #31412a, 16px 8px 0 #31412a,
    4px 12px 0 #31412a, 8px 12px 0 #31412a, 12px 12px 0 #31412a,
    4px 16px 0 #31412a, 8px 16px 0 #31412a, 12px 16px 0 #31412a,
    4px 20px 0 #31412a, 12px 20px 0 #31412a;
}
.skel-act {
  align-self: center;
  height: 16px;
  background: #18210f;
  border: 1px solid #24311a;
}

/* 3. Portrait arrival. .is-baking = canvas created, pixels still pending: the
      empty box PULSES on the shipped queue-pulse keyframes (deliberately no
      new keyframe for the pulse). .is-in = pixels landed, one 0.09s 2-step
      fade. The canvas box is CSS-fixed, so late pixels never reflow a row. */
.char-portrait.is-baking {
  animation: queue-pulse 1.1s ease-in-out infinite;
}
.char-portrait.is-in {
  animation: roster-fade-in 0.09s steps(2) 1;
}
/* The ONLY new keyframe in this block: a 0 -> 1 fade used by the portrait
   arrival above and by the delayed status reveal below. Everything that
   PULSES reuses the existing queue-pulse (see @keyframes queue-pulse). */
@keyframes roster-fade-in {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

/* 4. Status band — CONSTANT height across idle / busy / stalled / error /
      retry, so the list underneath never moves (zero CLS). The retry's
      .link-btn margin-top is flattened so the band is exactly its min-height. */
.roster-slot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  min-height: 28px;
  margin-top: 4px;
}
/* OWNER FIX (A2): when the CO-OP band is completely EMPTY (idle copy, RETRY
   hidden — js/menu.js syncCoopBandIdle() toggles .is-idle off the ONE roster
   state machine) it collapses to zero instead of reserving its constant
   28px, so the "CO-OP HERO" label sits ~8px (its own 2px + .char-list's 6px)
   above the first selector row. Any real content (busy / stalled / error /
   RETRY) drops the class and the constant-height zero-CLS contract holds.
   Scoped to the CO-OP slot only — #sp-roster-slot keeps today's band — and
   inert without html.menu-redesign (legacy byte-for-byte). */
html.menu-redesign #coop-roster-slot.is-idle {
  min-height: 0;
  margin-top: 0;
}
.roster-status {
  font-size: 10px;
  letter-spacing: 1px;
  line-height: 1.4;
  color: #9db18c;
}
.roster-retry {
  flex: 0 0 auto;
  margin-top: 0;
}
/* State colours: busy/stalled = gold, error = red; idle keeps the muted
   default and empty copy (nothing to announce). */
.roster-status.is-busy {
  color: #ffd23f;
}
.roster-status.is-error {
  color: #ff5d5d;
}

/* 5. First-paint reveal: the slot ships with .is-firstpaint (opacity 0) and
      only reveals once the boot is still running at +0.4s. A fast boot lands
      #menu.show first, the `html.js #menu:not(.show)` rule stops matching and
      the empty band never blinks — setRosterState() drops the class for good
      as soon as a real state is written. */
.roster-status.is-firstpaint {
  opacity: 0;
}
html.js #menu:not(.show) .roster-status.is-firstpaint {
  animation: roster-fade-in 0.09s steps(2) 0.4s both;
}

/* 6. Definitive pull failure with 0 rows (defect D5): ONE error row replaces
      the skeleton and the roster slot shows RETRY — never an endless
      "Loading heroes…", never a false empty state. */
.char-row.char-error {
  cursor: default;
  background: #1a0f0d;
  outline-color: #6b322c;
}
.char-row.char-error:hover {
  background: #1a0f0d;
}
.char-error .char-name {
  color: #ff5d5d;
}
.char-error .char-meta {
  color: #d89a94;
}

/* 7. Defect D6: sync() has always toggled `.disabled` on #btn-continue, but
      the class had NO styling — a "disabled" button looked live and still
      swallowed taps. Now also used while a roster pull is in flight. */
.px-btn.disabled,
.px-btn[aria-disabled="true"] {
  color: #8d9a7e;
  background: #1f2d4a;
  cursor: default;
  pointer-events: none;
  filter: grayscale(0.75) brightness(0.72) drop-shadow(2px 2px 0 rgba(0, 0, 0, 0.55));
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.08);
}

/* 8. Height settle used by menu.js endRosterLoading(): 120 ms in 6 steps
      reads as a deliberate pixel-art snap instead of a CSS ease. */
.char-list.is-settling {
  transition: height 120ms steps(6);
}

/* 9. Touch: the delete/upload cells are FIXED grid tracks, so they are widened
      TOGETHER to 44px here (a bare min-width would overflow the 20/24px track
      and cover the portrait). The status band grows to 44px with them so it
      stays constant on touch devices too — zero CLS, real tap targets, and
      every row variant still lands its portrait on the same x. */
@media (pointer: coarse) {
  .char-row {
    grid-template-columns: minmax(0, 1fr) 40px 44px 44px;
    touch-action: manipulation;
  }
  .char-del,
  .char-mig,
  .roster-retry {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 44px;
    min-height: 44px;
    padding: 0;
  }
  .roster-slot {
    min-height: 44px;
  }
}

/* 10. Reduced motion: every animation this design added is frozen — no
       skeleton pulse, no arrival fade, no delayed reveal, no height settle
       (menu.js checks the same query before animating the settle at all). */
@media (prefers-reduced-motion: reduce) {
  .char-skel,
  .char-portrait.is-baking,
  .char-portrait.is-in,
  .roster-status.is-firstpaint {
    animation: none !important;
  }
  .char-list.is-settling {
    transition: none !important;
  }
}

/* 11. INCIDENT LEVER — html.no-skel (localStorage pixoot_skel=0): back to the
       pre-design empty state, skeleton rows and status slots gone and with
       them the reserved 204px. Documented in the header comment above. */
html.no-skel .char-list.is-loading {
  min-height: 0;
}
html.no-skel .char-skel {
  display: none;
}
html.no-skel .roster-slot {
  display: none;
}

.link-btn {
  background: none;
  border: none;
  color: #6f7d63;
  font-family: inherit;
  font-size: 10px;
  text-decoration: underline;
  cursor: pointer;
  margin-top: 8px;
  letter-spacing: 1px;
}

#btn-save {
  background: #5e2f8a;
}

/* ---------- Co-op ---------- */
/* Top offset is MEASURED, not guessed: js/ui.js syncPartyOffset() writes
   --party-t (and --timer-t) from #topbar's real bottom whenever the header
   stack resizes — buff chips wrapping into rows, the phone two-row topbar
   stack, HUD zoom changes, safe-area insets. Units are zoom-local px: this
   padding sits inside zoom:var(--hud), so it renders as --party-t × hud
   screen px; the left inset is pre-divided by --hud so the rows land on the
   same 10 screen px column as the HP box. No env() here on purpose:
   #topbar's measured rect already includes the safe-area inset, and an
   env() inside zoom would double-count it scaled by hud (the old notch-phone
   overlap with ZERO buffs). content-box keeps the 42vh cap on the roster
   rows themselves, not rows + the offset padding. */
#party-bar {
  position: absolute;
  top: 0;
  left: 0;
  z-index: 11;
  box-sizing: content-box;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 4px;
  padding: var(--party-t) 0 0 calc(10px / var(--hud));
  zoom: var(--hud);
  pointer-events: none;
  max-height: 42vh;
  overflow-y: auto;
}

/* Lobby group roster (Co-op tab). One row per member; the leader row carries
   the star and everyone else gets a kick control when you lead. */
#coop-group-list {
  margin: 6px 0 8px;
  border: 2px solid #0a0d07;
  background: #10170d;
}

.group-member {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 7px 8px;
  border-bottom: 1px solid #1f2b18;
  font-size: 11px;
  font-weight: 900;
  letter-spacing: 0.5px;
  color: #cfcba9;
}
.group-member:last-child { border-bottom: none; }

.group-member-name {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.group-member-name.leader { color: #ffd23f; }

.group-kick {
  flex: 0 0 auto;
  min-height: 30px;
  padding: 4px 8px;
  font-size: 9px;
  background: #3a1515;
  border-color: #8b2020;
  color: #ff5d5d;
}
.group-kick:active { transform: translateY(1px); }

/* Leader-only: hand the party to someone else. Sits before Kick so the
   constructive action reads first. */
.group-promote {
  flex: 0 0 auto;
  min-height: 30px;
  padding: 4px 8px;
  font-size: 9px;
  background: #2c3f20;
  border-color: #57d46a;
  color: #7dfc88;
}
.group-promote:active { transform: translateY(1px); }
/* Offline member: the button is disabled (twin: the server's 409 offline) —
   without this rule the disabled control paints IDENTICAL to an enabled one,
   and the tap dies silently (no click event, so no explaining toast). The
   dimmed fill is the only affordance on the lever-off legacy path. */
.group-promote:disabled {
  background: #1a2116;
  border-color: #3f4a37;
  color: #6f7a5f;
  cursor: default;
}
.group-promote:not(:disabled):active { transform: translateY(1px); }

.group-member .group-promote + .group-kick { margin-left: 4px; }

/* Ready state chip — the pixelart READY button in the Co-op roster. Your own
   row is a toggle; the other members' chips are read-only indicators. */
.group-ready {
  flex: 0 0 auto;
  min-height: 30px;
  padding: 4px 8px;
  font-size: 9px;
  letter-spacing: 0.5px;
  background: #162110;
  border-color: #0a0d07;
  color: #9db18c;
}
.group-ready.on {
  background: #2c3f20;
  border-color: #57d46a;
  color: #7dfc88;
}
.group-ready:disabled { opacity: 1; cursor: default; }
.group-ready:not(:disabled):active { transform: translateY(1px); }

#btn-group-rejoin {
  margin-bottom: 6px;
  background: #16321f;
  border-color: #2f7d47;
  color: #7dfc88;
}

/* Non-leaders see the group verbs disabled — explained rather than dead. */
#btn-coop-create:disabled,
#btn-coop-join:disabled {
  opacity: 0.5;
  cursor: not-allowed;
  filter: grayscale(0.4);
}

#btn-group-leave {
  margin-bottom: 8px;
  background: #2a1c10;
  border-color: #6b4a22;
  color: #ffcf8a;
}

/* Roster header: only rendered when the party is bigger than one player. */
.party-head {
  width: 100%;
  padding: 3px 6px;
  margin-bottom: 2px;
  font-size: 9px;
  font-weight: 900;
  letter-spacing: 1px;
  text-transform: uppercase;
  color: #ffd23f;
  background: #162110;
  border: 2px solid #0a0d07;
  text-align: center;
}

.party-member {
  display: flex;
  align-items: center;
  gap: 6px;
  background: #141d10;
  border: 2px solid #0a0d07;
  outline: 1px solid #3f5c33;
  box-shadow: 3px 3px 0 rgba(0, 0, 0, 0.55);
  padding: 4px 7px;
  min-width: 150px;
  max-width: 55vw;
  pointer-events: auto;
}

.party-swatch {
  width: 11px;
  height: 11px;
  flex: 0 0 auto;
  border: 1px solid #0a0d07;
  outline: 1px solid rgba(255, 255, 255, 0.25);
}

.party-info {
  flex: 0 0 auto;
  color: #f2eeca;
  font-size: 8px;
  font-weight: 900;
  letter-spacing: 1px;
  text-shadow: 1px 1px 0 #000;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  max-width: 16ch;
}

/* Offline/menu rows append a STATE word after the name (" · OFFLINE") —
   exactly where the 16ch live-row cap ellipsizes, which used to hide the
   very word that says why the row is grey. Those rows carry no HP bar and no
   JOIN button, so the wider line still sits inside the row's min-width:150px
   box (content-bound rows top out at 55vw either way, and ui.js
   syncPartyOffset re-measures the real rect after every rebuild). */
.party-member.off .party-info {
  max-width: 26ch;
}

.party-hp {
  flex: 1 1 auto;
  height: 5px;
  min-width: 24px;
  border: 1px solid #0a0d07;
  background: #2a3520;
}

.party-member.self {
  outline-color: #ffd23f;
}

/* Parked in the menu: still in the group, not on the board — greyed out and
   without a JOIN button (there is nobody to teleport to). */
.party-member.off {
  opacity: 0.6;
  filter: grayscale(0.7);
}

.coop-row {
  display: flex;
  gap: 8px;
  margin-top: 6px;
  margin-bottom: 4px;
}

.coop-row .px-btn {
  flex: 1;
  margin: 0;
}

#coop input {
  width: 100%;
  margin-top: 3px;
  padding: 5px 6px;
  box-sizing: border-box;
  font-family: inherit;
  font-size: 14px;
  font-weight: 900;
  letter-spacing: 3px;
  text-align: center;
  color: #ffd23f;
  background: #0a0d07;
  border: 2px solid #0a0d07;
  outline: 2px solid #3f5c33;
  text-transform: uppercase;
}

#coop .m-check {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 11px;
  letter-spacing: 0.5px;
  cursor: pointer;
  user-select: none;
  margin-top: 10px;
}

#coop .m-check input {
  width: auto;
  margin: 0;
  padding: 0;
  outline: none;
  accent-color: #ffd23f;
}

.coop-rooms {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin: 4px 0 6px;
  max-height: 80px;
  overflow-y: auto;
}

.coop-rooms:empty {
  margin: 0;
  max-height: none;
}

.coop-rooms .coop-room {
  width: 100%;
  font-family: inherit;
  font-size: 12px;
  font-weight: 900;
  letter-spacing: 1px;
  text-align: left;
  color: #e8e2c0;
  background: #141a0e;
  border: 2px solid #3f5c33;
  padding: 8px 10px;
  cursor: pointer;
}

.coop-rooms .coop-room:hover {
  color: #ffd23f;
  border-color: #ffd23f;
}

#coop .coop-rooms:empty::before {
  content: "No public rooms yet.";
  font-size: 12px;
  color: #9aa4b2;
}

/* Lobby rows: code+count on top, hero names + levels underneath. */
.coop-rooms .coop-room {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.coop-room-head {
  font-size: 12px;
  font-weight: 900;
  letter-spacing: 1px;
}

.coop-room-heroes {
  font-size: 10px;
  font-weight: 400;
  letter-spacing: 0.5px;
  color: #8a9a7a;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.coop-room:hover .coop-room-heroes {
  color: #cfe4ff;
}

.coop-rooms-empty {
  font-size: 12px;
  color: #9aa4b2;
  padding: 4px 2px;
}

/* Lobby pager — one page = 10 rooms (server-enforced). */
.coop-page {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  margin-top: 6px;
}

.coop-page-btn {
  font-family: inherit;
  font-size: 11px;
  font-weight: 900;
  letter-spacing: 1px;
  color: #e8e2c0;
  background: #141a0e;
  border: 2px solid #3f5c33;
  padding: 4px 10px;
  cursor: pointer;
}

.coop-page-btn:hover:enabled {
  color: #ffd23f;
  border-color: #ffd23f;
}

.coop-page-btn:disabled {
  opacity: 0.35;
  cursor: default;
}

.coop-page-label {
  font-size: 10px;
  color: #577a4a;
  min-width: 72px;
  text-align: center;
  letter-spacing: 1px;
}

body.net-mode #tip {
  display: none;
}

/* ── Crafting overlay ── */
#crafting-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.55);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 22;
}

#crafting-overlay.hide {
  display: none;
}

.crafting-card {
  position: relative;
  width: min(520px, 92vw);
  max-height: 85vh;
  display: flex;
  flex-direction: column;
  text-align: center;
  color: #f2eeca;
  background: #1c2817;
  border: 4px solid #0a0d07;
  outline: 3px solid #d4a03f;
  box-shadow: 8px 8px 0 rgba(0, 0, 0, 0.65), inset 0 0 0 2px rgba(212, 160, 63, 0.2);
  border-radius: 0;
  padding: 16px 18px;
  image-rendering: pixelated;
  overflow: hidden;
}

.crafting-header {
  margin-bottom: 10px;
}

/* Title + tabs carry the exact .panel-title / .bag-tab chrome so the forge
   header reads the same as the inventory window. */
.crafting-title {
  font-size: 15px;
  letter-spacing: 2px;
  color: #ffd23f;
  text-shadow: 2px 2px 0 #000;
  margin-bottom: 8px;
  /* the card centers its text; the inventory title it mirrors is left-set */
  text-align: left;
}

.crafting-tabs {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
}

.craft-tab {
  pointer-events: auto;
  cursor: pointer;
  font-weight: 900;
  letter-spacing: 1px;
  color: #cfcba9;
  background: #162110;
  border: 2px solid #0a0d07;
  box-shadow: 2px 2px 0 rgba(0, 0, 0, 0.45);
  border-radius: 0;
  padding: 7px 10px;
  font-size: 11px;
  font-family: inherit;
  text-transform: uppercase;
  touch-action: manipulation;
  line-height: 1;
}

.craft-tab.on {
  background: #ffd23f;
  color: #1a1200;
  box-shadow: 0 2px 0 #8a6a12;
}

/* Bulk-dismantle row (DISMANTLE tab): one wrap row of rarity buttons —
   4 across on the 520px desktop card, 2+2 on a 320px phone. */
.dismantle-all-row {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  margin-top: 6px;
}

.bulk-btn {
  flex: 1 1 112px;
  min-width: 112px;
  min-height: 44px;
  padding: 9px 6px;
  font-size: 10px;
}

.bulk-btn[data-rarity="0"] { color: #9aa4b2; }
.bulk-btn[data-rarity="1"] { color: #57d46a; }
.bulk-btn[data-rarity="2"] { color: #4aa3ff; }
.bulk-btn[data-rarity="3"] { color: #c05ef0; }

.bulk-btn:disabled {
  cursor: default;
}

.crafting-body {
  display: flex;
  gap: 8px;
  flex: 1;
  min-height: 0;
  overflow: hidden;
}

.craft-recipes-list {
  flex: 1;
  overflow-y: auto;
  background: #0a0d07;
  border: 2px solid #3f5c33;
  padding: 4px;
  text-align: left;
  scrollbar-width: thin;
  scrollbar-color: #3f5c33 #0a0d07;
}

.craft-recipes-list::-webkit-scrollbar {
  width: 6px;
}

.craft-recipes-list::-webkit-scrollbar-track {
  background: #0a0d07;
}

.craft-recipes-list::-webkit-scrollbar-thumb {
  background: #3f5c33;
}

.craft-detail {
  width: 200px;
  flex-shrink: 0;
  overflow-y: auto;
  background: #0a0d07;
  border: 2px solid #3f5c33;
  padding: 8px;
  text-align: left;
  scrollbar-width: thin;
  scrollbar-color: #3f5c33 #0a0d07;
}

.craft-detail::-webkit-scrollbar {
  width: 6px;
}

.craft-detail::-webkit-scrollbar-track {
  background: #0a0d07;
}

.craft-detail::-webkit-scrollbar-thumb {
  background: #3f5c33;
}

.craft-row:hover {
  background: rgba(87, 212, 106, 0.1);
}

.craft-close {
  flex-shrink: 0;
  background: #5e2f8a;
}

/* ── Waypoint overlay ── */
#waypoint-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.55);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 22;
}

#waypoint-overlay.hide {
  display: none;
}

.waypoint-card {
  position: relative;
  width: min(280px, 88vw);
  max-height: 70vh;
  display: flex;
  flex-direction: column;
  color: #f2eeca;
  background: #1c2817;
  border: 4px solid #0a0d07;
  outline: 3px solid #5fc1ff;
  box-shadow: 8px 8px 0 rgba(0, 0, 0, 0.65);
  border-radius: 0;
  padding: 16px 18px;
  overflow: hidden;
}

.waypoint-title {
  font-size: 16px;
  color: #5fc1ff;
  text-shadow: 2px 2px 0 rgba(0, 0, 0, 0.5);
  text-align: center;
  letter-spacing: 2px;
  margin-bottom: 4px;
}

.waypoint-sub {
  font-size: 10px;
  color: #577a4a;
  text-align: center;
  margin-bottom: 10px;
}

.waypoint-list {
  flex: 1;
  overflow-y: auto;
  scrollbar-width: thin;
  scrollbar-color: #3f5c33 #0a0d07;
}

.waypoint-row:hover {
  background: rgba(95, 193, 255, 0.1);
}

.waypoint-close {
  flex-shrink: 0;
  background: #2a4a6a;
  margin-top: 8px;
  position: relative;
  z-index: 10;
}

/* ── Map run modifier readout (under the minimap) ── */
#mapmods {
  position: absolute;
  right: 0;
  top: calc(100% + 6px);
  width: 160px;
  color: #f2eeca;
  font-size: 9px;
  text-align: right;
  line-height: 1.4;
  pointer-events: none;
  text-shadow: 1px 1px 0 rgba(0, 0, 0, 0.75);
}
#mapmods .mm-title {
  color: #8a7bff;
  letter-spacing: 1px;
  margin-bottom: 2px;
}
#mapmods .mm-mod {
  color: #cfcba9;
}
#mapmods .mm-sub {
  color: #577a4a;
}

/* ── Map Device overlay ── */
#mapdevice-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.55);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 22;
}
#mapdevice-overlay.hide {
  display: none;
}
.mapdevice-card {
  position: relative;
  width: min(440px, 94vw);
  max-height: 85vh;
  display: flex;
  flex-direction: column;
  color: #f2eeca;
  background: #152016;
  border: 4px solid #0a0d07;
  outline: 3px solid #8a7bff;
  box-shadow: 8px 8px 0 rgba(0, 0, 0, 0.65);
  border-radius: 0;
  padding: 16px 18px;
  overflow: hidden;
}
.mapdevice-title {
  font-size: 16px;
  color: #8a7bff;
  text-shadow: 2px 2px 0 rgba(0, 0, 0, 0.5);
  text-align: center;
  letter-spacing: 2px;
  margin-bottom: 4px;
}
.mapdevice-sub {
  font-size: 10px;
  color: #577a4a;
  text-align: center;
  margin-bottom: 10px;
}
.mapdevice-list {
  flex: 1;
  overflow-y: auto;
  scrollbar-width: thin;
  scrollbar-color: #3f5c33 #0a0d07;
}
.mapdevice-row {
  background: #0d130a;
  border: 2px solid #0a0d07;
  margin-bottom: 6px;
  padding: 6px 8px;
  cursor: pointer;
  display: flex;
  align-items: center;
  gap: 8px;
}
.mapdevice-row:hover {
  background: rgba(138, 123, 255, 0.12);
}
.mapdevice-row .mapdevice-row-main {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 3px;
}
.mapdevice-row .mapdevice-name {
  font-size: 11px;
  line-height: 1.2;
  /* The name always carries BOTH numbers ("<Land> Map T<n> +<m>"), so it runs
     a few characters longer than it used to. On a 320px phone the longest land
     would clip behind an ellipsis and hide the +N — let it wrap to a second
     line instead (the row is a column stack, so the TIER line just follows). */
  white-space: normal;
  overflow-wrap: break-word;
}
.mapdevice-row .mapdevice-tier {
  font-size: 10px;
  line-height: 1.2;
  color: #ffd23f;
}
.mapdevice-activate {
  color: #ffd23f;
  background: #2c4fa8;
  border: 2px solid #0a0d07;
  outline: 2px solid transparent;
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.25);
  border-radius: 0;
  font-family: inherit;
  font-weight: 900;
  letter-spacing: 1px;
  text-transform: uppercase;
  touch-action: manipulation;
  cursor: pointer;
  padding: 8px 10px;
  min-width: 70px;
  min-height: 40px;
  font-size: 11px;
  text-shadow: 2px 2px 0 rgba(0, 0, 0, 0.6);
  transition: transform 0.05s, filter 0.05s;
  clip-path: polygon(
    5px 0, calc(100% - 5px) 0,
    100% 5px, 100% calc(100% - 5px),
    calc(100% - 5px) 100%, 5px 100%,
    0 calc(100% - 5px), 0 5px
  );
  filter: drop-shadow(2px 2px 0 rgba(0, 0, 0, 0.5));
  flex-shrink: 0;
}
.mapdevice-activate:hover {
  filter: drop-shadow(2px 2px 0 rgba(0, 0, 0, 0.5)) brightness(1.12);
}
.mapdevice-activate:active {
  transform: translate(2px, 2px);
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.25);
  filter: drop-shadow(0 0 0 rgba(0, 0, 0, 0.5));
}
.mapdevice-detail {
  margin-top: 6px;
  padding: 8px;
  background: #162110;
  border: 1px solid #0a0d07;
  font-size: 11px;
  color: #cfcba9;
  line-height: 1.5;
  min-height: 48px;
  flex-shrink: 0;
  overflow-y: auto;
  max-height: 20vh;
}
.mapdevice-detail .md-tip {
  color: #577a4a;
}
.mapdevice-detail .md-run {
  color: #ffd23f;
}
.mapdevice-close {
  flex-shrink: 0;
  background: #2a4a6a;
  margin-top: 10px;
  position: relative;
  z-index: 10;
}

/* ── Co-op MAP RUN INVITE card (js/net.js showMapInvite) ──
   NON-BLOCKING by design: NO backdrop element and NO position:fixed inset:0
   click-catcher anywhere in this family — #map-invite shrink-wraps the card
   and pointer-events are enabled on .map-invite-card ALONE (the root itself
   is pointer-events:none), so the game keeps running and a tap outside the
   card falls straight through to the world. The card is therefore an island:
   top-centre, compact, dismissible three ways (✕ / NOT NOW / countdown).

   Z-INDEX 18 — reasoned against the ladder AS MEASURED IN THE ROOT STACKING
   CONTEXT (the whole HUD kit — #topbar 12, #party-bar 11, the dock, the
   minimap and the obj-strip — lives INSIDE #hud at 10, so in the root it
   all paints as one layer at 10; only body-level siblings count separately):
     10 #hud (entire HUD + party bar) · 16 #quest-log · 19 #chat-pop ·
     20 #panel-mask · 21 .panel · 22 #mapdevice-overlay/#event-timer ·
     23 banners · 24 #reward-popup/#boss-banner · 26 #ingame-settings ·
     30 #reset/#death · 32 #trade · 38 #ach-toasts · 40 #menu ·
     46 #toasts · 88–97 the menu-time dialogs.
   15 (CTO round: was 18) sits ABOVE the whole HUD/party-bar family (the
   required "above the HUD" — #hud owns 10) but BELOW #quest-log (16): on
   phones the log is right-aligned 240px wide and overlaps this centred
   320px card, and at 18 the card covered the log's real tap targets
   (.qb-abandon × and friends). Everything else named as owning the screen
   still sits above the card: .panel + #panel-mask (20/21), #mapdevice
   (22), #ingame-settings (26), #death (30), the menu-time dialogs
   (88–97) — and the ticking invite survives all of them closing.
   The transient top-lane chrome is deliberately ABOVE this card instead of
   under it: banners (23/24), #event-timer (22), #toasts (46) and
   #ach-toasts (38 — which shares this card's exact top offset, see below)
   are ALL pointer-events:none, so a toast or a LEVEL-UP banner drawing
   across the title for its 1–3s never steals a JOIN/NOT NOW tap — whereas
   a z ABOVE them would permanently hide a celebration or the live event
   countdown behind this opaque card for the invite's whole lifetime.
   Transient-over-persistent beats the reverse. */
#map-invite {
  position: fixed;
  /* Screen-px lane, byte-identical to #ach-toasts — the house's ONE un-zoomed
     top-lane consumer: this card is viewport-sized overlay chrome, NOT part of
     the zoom:var(--hud) HUD kit, so it uses the SCREEN-px objstrip twin
     (--objstrip-hs, 0 unless body.in-game.obj-strip-on) and never the
     zoom-local --objstrip-h. Those are precisely the units js/quest.js
     writeStripOffsets() measures against (basePlain = topbar-h + 10 + env),
     so with a quest strip up the card lands on the strip's real bottom.
     --topbar-h is the NOMINAL (hud=1) bar height while the bar's children
     scale down with --hud ≤ 1: on a scaled phone this top sits LOWER than
     the bar's real bottom (extra clearance — never a collision), and at
     hud=1 it is exactly bar + 10px. */
  top: calc(var(--topbar-h) + 10px + var(--objstrip-hs) + env(safe-area-inset-top));
  left: 50%;
  transform: translateX(-50%);
  /* The ROOT never eats a tap — only the card below re-enables them. */
  pointer-events: none;
  /* 15, not 18 (CTO round fix): at 15 the card still clears the whole HUD
     kit (#hud 10 — party bar, dock, minimap all live INSIDE it) but sits
     BELOW the open quest log (16), which on phones is right-aligned
     240px wide and overlaps this centred 320px card — the card used to
     cover the log's real tap targets (.qb-abandon × etc). Everything
     transient that crosses the lane (#chat-pop 19, banners 23/24,
     reward 24, settings 26, death 30, trade 32, toasts 46, menu-time
     dialogs 88-97) stays ABOVE the card either way. */
  z-index: 15;
  image-rendering: pixelated;
}
#map-invite.hide {
  /* Redundant with the global .hide (display:none !important) — kept explicit
     the way #mapdevice-overlay.hide / #trade-overlay.hide are, so the
     show/hide contract of this overlay is readable in its own section. */
  display: none;
}
/* Fail-closed CSS twin of the js/net.js body.in-game gate: even if a stray
   frame ever dropped .hide outside the world, the card cannot paint over the
   main menu. (.cc-root runs the same rule in the opposite direction.) */
body:not(.in-game) #map-invite {
  display: none;
}

.map-invite-card {
  position: relative;
  pointer-events: auto;
  /* Compact on a 360px phone: 320 leaves 20px of world visible per side and
     keeps the ✕ clear of the title. */
  width: min(320px, calc(100vw - 24px));
  /* Short-viewport guard (landscape phones): the card is a fixed top-lane
     island, so it caps its OWN height to the lane it was placed in — the
     same `top` terms above minus a 205px reserve for the bottom dock — and
     scrolls INSIDE rather than growing over the ability bar. Measured: with
     this reserve the card's bottom lands at (100vh + 10px - env - 205) when
     the cap engages, which clears the dock CONTENT at every tested viewport
   (the #dock CONTAINER may be clipped by a few px on the shortest
   landscape — it is pointer-events:none, so only content matters); the
     natural height (~247px) stays UNDER the cap at 360×640 with up to five
     modifier lines, so the specified target shows the whole card — both
     buttons — with NO scrollbar (cardOverflowX/Y = 0 in the harness). The
     actions row is sticky-bottom (see below) so the buttons stay reachable
     on any viewport where the cap does engage. */
  max-height: calc(100vh - var(--topbar-h) - var(--objstrip-hs) - env(safe-area-inset-top) - 205px);
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: thin;
  scrollbar-color: #3f5c33 #0a0d07;
  /* Map-device family chassis (see .mapdevice-card / .namecard-box): flat
     dark-green fill, chunky near-black border, hard offset shadow, zero
     radius. The accent ring is a box-shadow (NOT .mapdevice-card's outline)
     because the idle pulse below animates box-shadow bands only — the house
     glow law. */
  color: #f2eeca;
  background: #152016;
  border: 4px solid #0a0d07;
  border-radius: 0;
  padding: 12px 14px 10px;
  text-align: center;
  image-rendering: pixelated;
  box-shadow: 0 0 0 2px #8a7bff, 8px 8px 0 rgba(0, 0, 0, 0.65);
  /* IDLE PULSE — glow law: BOX-SHADOW ONLY, banded integer-px rings on
     steps(4,end) with .999 hold-stops (the .seq-loop idiom), never a blur,
     gradient, transform or filter. 1200ms/4 = 300ms holds ≈ 0.83Hz, DIM →
     MID → HIGH → MID, never fully off; the 8px hard offset shadow rides in
     every frame (box-shadow animates as ONE list, so the constant must be
     restated per keyframe). HIGH adds a 2px #0a0d07 keyline OUTSIDE the
     purple band (later shadows paint behind earlier ones). */
  animation: mapInvitePulse 1200ms steps(4, end) infinite;
}
@keyframes mapInvitePulse {
  0%, 24.999% {
    box-shadow: 0 0 0 2px rgba(138, 123, 255, 0.35), 8px 8px 0 rgba(0, 0, 0, 0.65);
  }
  25%, 49.999% {
    box-shadow: 0 0 0 2px rgba(138, 123, 255, 0.72), 8px 8px 0 rgba(0, 0, 0, 0.65);
  }
  50%, 74.999% {
    box-shadow: 0 0 0 3px #8a7bff, 0 0 0 5px #0a0d07, 8px 8px 0 rgba(0, 0, 0, 0.65);
  }
  75%, 99.999% {
    box-shadow: 0 0 0 2px rgba(138, 123, 255, 0.72), 8px 8px 0 rgba(0, 0, 0, 0.65);
  }
}

/* ✕ CLOSE — the game's ink-box X (.cc-x chassis, styled by ID because the
   fixed DOM contract gives this button no class). 26px box, the invisible
   ::after pad grows the REAL tap target to 44px (inset -9px each side =
   26 + 18 = 44). Monochrome hover/active: the red X hover stays reserved
   for destructive actions (panel-x / .cc-x ruling).
   INSET 12px, NOT the house's usual 6px, for one non-obvious reason: this
   card is a SCROLL container (overflow-y:auto — the short-viewport guard in
   .map-invite-card above), and a scroll container clips + counts overflow at
   its PADDING box. At 6px the pad would poke 3px past that box on the top
   and right = 3px of horizontal scrollable overflow → a stray scrollbar on
   a card that should never scroll sideways. At 12px the pad spans exactly [3px … 47px]
   from each padding-box edge: fully contained, so the target stays a clean
   44×44 with zero overflow. */
#map-invite-x {
  position: absolute;
  top: 12px;
  right: 12px;
  z-index: 5;
  width: 26px;
  height: 26px;
  padding: 0;
  cursor: pointer;
  background: #0a0d07;
  border: 2px solid #55604d;
  color: #cfcba9;
  font-family: inherit;
  font-size: 13px;
  font-weight: 900;
  line-height: 22px;
  text-align: center;
  touch-action: manipulation;
}
#map-invite-x:hover,
#map-invite-x:active {
  background: #131a0e;
  border-color: #8a7bff;
  color: #cfcba9;
}
#map-invite-x::after {
  content: "";
  position: absolute;
  inset: -9px;
}
#map-invite-x:focus-visible {
  outline: 2px solid #ffd23f;
  outline-offset: 1px;
}

/* Title row: the map-device purple (.mapdevice-title's colour — this card IS
   the device's outreach). The tiny pixel map glyph is pure box-shadow art on
   a 2×2 base (no canvas, no image, no filter); ::after is an invisible
   spacer of the same footprint so the text stays optically centred despite
   the glyph hanging off its left. The side margin is SYMMETRIC (12px, not a
   right-only gutter): equal margins never shift a centred line, they only
   reserve wrap room — the ✕ clearance comes from the geometry (the line ends
   ~49px from the content edge while the ✕ box starts at 24px), so the static
   "MAP RUN INVITE" can never reach the button at any card width. */
#map-invite-title {
  font-size: 14px;
  font-weight: 900;
  letter-spacing: 2px;
  text-transform: uppercase;
  color: #8a7bff;
  text-shadow: 2px 2px 0 rgba(0, 0, 0, 0.55);
  margin: 0 12px 4px;
  line-height: 1.2;
}
#map-invite-title::before {
  content: "";
  display: inline-block;
  width: 2px;
  height: 2px;
  /* box-shadow art does NOT count toward layout — the layout box is only the
     2×2 base while the painted glyph spans 14px wide (base + 12px of shadow).
     So the reservation is done by hand: 12px of overhang + 6px of air = 18px,
     which starts the text 6px after the art's last pixel. The ::after mirror
     below is 2 + 18 = 20px so the visible text stays optically centred. */
  margin-right: 18px;
  /* vertical-align N lowers/raises the 2px box off the baseline; the art
     hangs 10px down from that box. N=8 puts the art EXACTLY on the cap-height
     band ([baseline-10 … baseline]) — it neither spills into the descenders
     nor grows the line box (the text's own ascent already reaches -12px). */
  vertical-align: 8px;
  background: #0a0d07;       /* pixel (0,0) = the top-left ink corner */
  image-rendering: pixelated;
  /* 7×5 pixels at 2px each = a 14×10 folded scroll: ink keyline rows top and
     bottom, parchment body, two darker fold columns, one red seal pixel. */
  box-shadow:
    2px 0 0 #0a0d07, 4px 0 0 #0a0d07, 6px 0 0 #0a0d07, 8px 0 0 #0a0d07, 10px 0 0 #0a0d07, 12px 0 0 #0a0d07,
    0 2px 0 #cbb87a, 2px 2px 0 #cbb87a, 4px 2px 0 #cbb87a, 6px 2px 0 #cbb87a, 8px 2px 0 #cbb87a, 10px 2px 0 #cbb87a, 12px 2px 0 #cbb87a,
    0 4px 0 #0a0d07, 2px 4px 0 #8a753c, 4px 4px 0 #cbb87a, 6px 4px 0 #b03c2b, 8px 4px 0 #cbb87a, 10px 4px 0 #8a753c, 12px 4px 0 #0a0d07,
    0 6px 0 #cbb87a, 2px 6px 0 #cbb87a, 4px 6px 0 #cbb87a, 6px 6px 0 #cbb87a, 8px 6px 0 #cbb87a, 10px 6px 0 #cbb87a, 12px 6px 0 #cbb87a,
    0 8px 0 #0a0d07, 2px 8px 0 #0a0d07, 4px 8px 0 #0a0d07, 6px 8px 0 #0a0d07, 8px 8px 0 #0a0d07, 10px 8px 0 #0a0d07, 12px 8px 0 #0a0d07;
}
#map-invite-title::after {
  content: "";
  display: inline-block;
  width: 20px;               /* = ::before's full inline advance (2 + 18) */
  height: 1px;
}

/* Copy lines. All three word-wrap (the JS may carry a ~40-char land or mod
   string) — break-word means a long token wraps instead of running the card
   sideways; at 10–11px Courier a 40-char line is ~264px inside the 288px
   content box, i.e. it fits or wraps, never scrolls. */
.map-invite-from {
  font-size: 10px;
  font-weight: 900;
  letter-spacing: 0.5px;
  color: #9db18c;
  line-height: 1.4;
  margin-bottom: 2px;
  overflow-wrap: break-word;
}
/* Map name: js/net.js stamps an inline RARITY colour here, so this rule
   deliberately sets NO colour of its own (inline always wins anyway; the
   inherit fallback only shows if the JS ever clears it). Weight + size +
   the hard dark shadow are what make ANY rarity ink legible on #152016. */
.map-invite-name {
  font-size: 13px;
  font-weight: 900;
  letter-spacing: 1px;
  line-height: 1.35;
  color: inherit;
  text-shadow: 2px 2px 0 rgba(0, 0, 0, 0.75);
  margin-bottom: 2px;
  overflow-wrap: break-word;
}
.map-invite-land {
  font-size: 11px;
  font-weight: 900;
  letter-spacing: 0.5px;
  color: #ffe9a0;
  line-height: 1.4;
  margin-bottom: 4px;
  overflow-wrap: break-word;
}
/* Modifiers: the JS joins one modLine per "\n", so pre-line turns those back
   into real lines (the default would collapse them into one run-on row).
   Left-aligned inside an inset plate — the .mapdevice-detail idiom — because
   a stacked list reads badly centred. :empty = the defensive twin: js/net.js
   always writes SOMETHING before it drops .hide, so an empty plate can only
   appear if a future writer forgets — then it vanishes instead of rendering
   a 14px-tall empty box. */
.map-invite-mods {
  font-size: 10px;
  line-height: 1.5;
  color: #cfcba9;
  text-align: left;
  white-space: pre-line;
  overflow-wrap: break-word;
  background: #0d130a;
  border: 2px solid #0a0d07;
  padding: 5px 7px;
  margin: 0 0 4px;
}
.map-invite-mods:empty { display: none; }
/* Timer: visually distinct because the JS rewrites it every second. The gold
   chip is the .toast chrome (dark plate + gold keyline + gold ink). The width
   is FIXED: js/net.js mapInviteMmss() emits M:SS for a sub-hour countdown, so
   the text is at most "59:59" = 5 monospace chars (5 × 7.2 + 5 letter-spacing
   ≈ 41px) which sits INSIDE min-width 62 (border-box: 41 + 16 pad + 4 border
   = 61) — every value from "0:00" to "59:59" renders at one identical width,
   and tabular-nums pins the digits to that grid, so a per-second update can
   never shift a pixel of layout. */
.map-invite-timer {
  display: inline-block;
  min-width: 62px;
  padding: 2px 8px;
  background: #0d130a;
  border: 2px solid #0a0d07;
  outline: 2px solid #7a5f14;
  color: #ffd23f;
  font-size: 12px;
  font-weight: 900;
  letter-spacing: 1px;
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
  text-align: center;
  margin-bottom: 2px;
}
.map-invite-timer:empty { display: none; }

/* Actions. Both come with .px-btn.wide from the fixed DOM contract (width
   100% + margin-top 10px) — inside this flex ROW the widths become equal
   halves via flex-basis 0, and the per-button margin-top collapses into the
   single row margin. min-height 44px is the touch law lifted above .px-btn's
   global 40 (the #conn-card precedent): JOIN and NOT NOW must both be
   >=44px on a phone. JOIN keeps the existing .px-btn.accent variant (green
   plate, #57d46a ring) — it already ships, nothing new is defined here.
   STICKY BOTTOM: only when the card's height cap engages (short/landscape
   viewports) does this row pin to the bottom of the scrollport — the copy
   above it scrolls while JOIN / NOT NOW stay on screen, so the card is never
   a sliver you cannot answer. With the cap disengaged (the specified
   360×640 portrait target, every desktop size) sticky is inert and the row
   renders in its normal position — byte-identical layout. The row carries the
   card's own fill + padding so scrolling copy can never peek beside it. */
.map-invite-actions {
  display: flex;
  gap: 8px;
  /* margin 4 + the row's 6px padding-top = the original 10px visual gap and
     the SAME row height (4 + 6 + 44 = 10 + 44) — the sticky fill changes
     nothing about the card's natural height. */
  margin-top: 4px;
  position: sticky;
  bottom: 0;
  background: #152016;
  padding-top: 6px;
}
.map-invite-actions .px-btn {
  flex: 1 1 0;
  width: auto;
  margin-top: 0;
  min-height: 44px;
  padding-left: 6px;
  padding-right: 6px;
}
.map-invite-actions .px-btn:focus-visible {
  outline-color: #ffd23f;
}

@media (prefers-reduced-motion: reduce) {
  /* Same states, no travel: the pulse freezes into the HIGH frame — a static
     banded double ring (purple band + ink keyline) that never animates and
     never goes off. */
  .map-invite-card {
    animation: none;
    box-shadow: 0 0 0 3px #8a7bff, 0 0 0 5px #0a0d07, 8px 8px 0 rgba(0, 0, 0, 0.65);
  }
}

/* SHORT SCREENS (landscape phones / small laptops, height <= 480px): the
   dock shrinks with them (the max-height:480 dock rules + the 56vh clutch
   cap), so the portrait reserve of 205px over-reserves here and would squint
   the card down to a 30px sliver. 128px is the measured reserve: at 640x360
   it leaves a 100px card = title (17) + the sticky action row (50) + their
   padding with 3px to spare, so the pinned row can never rise into the
   headline; the card still reaches no dock CONTENT (harness hits = 0 at
   640x360, 600x460 and 900x420) and the copy between title and row scrolls
   inside the card exactly as designed.

   FLOOR (CTO round fix): with the objective strip ON (--objstrip-hs 12,
   the default — the 3 starter quests auto-accept) the calc() loses another
   12px, dropping the cap BELOW the card's fixed chrome (border 8 + padding
   22 + title 17 + action row 50 = 97px) — at 640x360 the cap hit 88px and
   the opaque sticky row sliced through "MAP RUN INVITE" (measured overlap:
   title [190,170] vs actions [178,178] = 9px). `max(97px, calc(...))`
   floors the cap at the chrome height, so the worst case is a fully-scrolled
   card that always shows title + both buttons — never an overlap. Verified
   post-fix: 640x360 strip-on shows the title clear of the row, and the
   grown card bottom still clears dock CONTENT (dock container is
   pointer-events:none; content starts ~44px lower). */
@media (max-height: 480px) {
  .map-invite-card {
    max-height: max(97px, calc(100vh - var(--topbar-h) - var(--objstrip-hs) - env(safe-area-inset-top) - 128px));
  }
}

/* ── In-game settings overlay (HUD button "SETTINGS [ESC]") ── */
#ingame-settings {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.55);
  display: flex;
  align-items: center;
  justify-content: center;
  /* Above the HUD (10) and the map device (22); below the death/menu layers. */
  z-index: 26;
}
#ingame-settings.hide {
  display: none;
}
.igset-card {
  position: relative;
  display: flex;
  flex-direction: column;
  width: min(400px, 94vw);
  max-height: 86vh;
  color: #f2eeca;
  background: #152016;
  background-image: repeating-conic-gradient(rgba(87, 212, 106, 0.04) 0% 25%, transparent 0% 50%);
  background-size: 16px 16px;
  border: 4px solid #0a0d07;
  outline: 3px solid #3f5c33;
  box-shadow: 8px 8px 0 rgba(0, 0, 0, 0.65);
  border-radius: 0;
  padding: 22px 18px 16px;
}
.igset-title {
  font-size: 16px;
  font-weight: 900;
  color: #ffd23f;
  text-align: center;
  letter-spacing: 4px;
  text-shadow: 2px 2px 0 rgba(0, 0, 0, 0.5);
  margin-bottom: 12px;
  flex-shrink: 0;
}
/* Scroll body: only the section stack scrolls — the title and MAIN MENU stay
   put. Same pixel scrollbar as the other scroll views (.panel-body etc.), so a
   short screen scrolls this like every other list in the game. */
.igset-scroll {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior: contain;
  scrollbar-width: thin;
  scrollbar-color: #3f5c33 #0a0d07;
}
.igset-scroll::-webkit-scrollbar {
  width: 8px;
}
.igset-scroll::-webkit-scrollbar-track {
  background: #0a0d07;
  border-left: 1px solid #1c2817;
}
.igset-scroll::-webkit-scrollbar-thumb {
  background: #3f5c33;
  border: 1px solid #0a0d07;
}
.igset-scroll::-webkit-scrollbar-thumb:hover {
  background: #57d46a;
}
#set-main-menu {
  flex-shrink: 0;
  margin-top: 12px;
}
/* ── Section dropdowns (SOUNDS / GAMEPLAY / GRAPHICS) ──────────────────────
   The head is a full-width pixel button: section name in the pixel font on
   the left, a PIXOOT.px ▲▼-style caret on the right that flips when the
   section opens. One section open at a time (js/ui.js openSetSection) keeps
   the card small on phones. The head lights up on :hover / :focus / press
   AND while its section is open — hover alone read as "broken" on touch,
   where a tap leaves a sticky :hover only on heads that were actually
   tapped (the accordion collapses SOUNDS without ever tapping it), and the
   window can pop open under a stationary cursor that never re-evaluates
   :hover. Anchoring the light to the OPEN section gives every head the same
   highlight for every input method. */
.igset-drop {
  margin-bottom: 8px;
}
.igset-drop-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  width: 100%;
  min-height: 46px;
  padding: 0 12px;
  background: #23331c;
  /* The highlight lights the BORDER, not an outline: with the .px-btn
     clip-path in play, Chrome never painted an outline's TOP segment
     consistently (first-position heads lost it entirely, others depended on
     sub-pixel clip rounding — a classic "works on the middle buttons" bug).
     Borders paint inside the clip on all four sides, so hover / press /
     focus / open light the WHOLE ring on every head, every time. */
  border: 2px solid #0a0d07;
  outline: none;
}
.igset-drop-head:hover,
.igset-drop-head:active,
.igset-drop-head:focus-visible,
.igset-drop.open .igset-drop-head {
  border-color: #57d46a;
}
.igset-drop-head .px-label {
  display: block;
  line-height: 0;
}
.igset-caret {
  display: block;
  flex-shrink: 0;
  image-rendering: pixelated;
  transition: transform 0.14s steps(3);
}
.igset-drop.open .igset-caret {
  transform: rotate(180deg);
}
@media (prefers-reduced-motion: reduce) {
  .igset-caret {
    transition: none;
  }
}
.igset-drop-body {
  display: none;
  padding: 12px 2px 2px;
}
.igset-drop.open .igset-drop-body {
  display: block;
}
.igset-row {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 8px;
}
.igset-name {
  flex: 0 0 66px;
  width: 66px;
}
.igset-name .px-label {
  display: block;
}
.set-toggle {
  flex: 1 1 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 42px;
  background: #23331c;
  outline-color: #3f5c33;
}
.set-toggle.off {
  background: #2a1616;
  outline-color: #7a3a3a;
}
/* Cycle buttons print their whole mode on the button — keep the pixel label
   on one line even on narrow phones. */
.feel-cycle .px-label {
  white-space: nowrap;
}

/* ── Combat feel (designs/combat-experience.md §3.E) ────────────────────── */

/* HP chip-lag "ghost" (§3.E.3): a dark-red trail that chases the real fill
   over 0.4s after a loss, so burst damage reads at a glance. Sits under the
   fill; .bar is already position:relative. */
.bar .ghost {
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  width: 100%;
  /* == the legacy `width: 0` (invisible until ui.js writes the first value) */
  clip-path: inset(0 100% 0 0);
  background: #5a0f0f;
  z-index: 0;
  transition: width 0.4s linear, clip-path 0.4s linear;
}
.bar .fill {
  position: relative;
  z-index: 1;
}

/* Boss HUD bar (§3.E.4): top-center under the HUD row, max ~60% width. */
#boss-hud-bar {
  position: fixed;
  top: 58px;
  left: 50%;
  transform: translateX(-50%);
  width: min(58vw, 420px);
  z-index: 6;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.25s linear;
}
#boss-hud-bar.show {
  opacity: 1;
}
#boss-hud-bar.hide {
  display: none;
}
#boss-hud-name {
  display: block;
  text-align: center;
  margin-bottom: 3px;
}
.bar.boss-hp {
  height: 10px;
  margin-top: 0;
  border: 2px solid #0a0d07;
  outline: 1px solid #7a4a12;
  background: #241505;
}
.bar.boss-hp .fill {
  background: repeating-linear-gradient(90deg, #ff9d2e 0 6px, #e0762a 6px 12px);
}
.bar.boss-hp span {
  position: absolute;
  inset: 0;
  text-align: center;
  font-size: 8px;
  line-height: 12px;
  color: #0a0d07;
  font-weight: 900;
}

/* Low-HP vignette (§3.E.2): ~0.7 Hz breathing (1.43s — well under 3 flashes/s),
   opacity capped ~0.35; `vig-static` for prefers-reduced-motion users. */
#feel-vignette.vig-pulse {
  animation: feelVig 1.43s ease-in-out infinite;
}
#feel-vignette.vig-static {
  opacity: 0.26;
}
@keyframes feelVig {
  0%, 100% { opacity: 0.16; }
  50% { opacity: 0.35; }
}

/* Ability ready pop (§3.E.5): brief gold ring when a cooldown completes. */
.ability-btn.ready-pop {
  animation: readyPop 0.18s steps(3) 1;
  box-shadow: 0 0 0 2px #ffd23f, 2px 2px 0 rgba(0, 0, 0, 0.45);
}
@keyframes readyPop {
  0% { transform: scale(1); }
  50% { transform: scale(1.12); }
  100% { transform: scale(1); }
}

/* Photosensitivity + reduced-motion guards (§3.H): no animated flashing for
   players who asked for none; the vignette keeps its static form instead. */
@media (prefers-reduced-motion: reduce) {
  #feel-vignette.vig-pulse {
    animation: none;
    opacity: 0.26;
  }
  .ability-btn.ready-pop {
    animation: none;
  }
}
#set-main-menu {
  background: #3a2a12;
  border-color: #7a5a1e;
  color: #ffb53f;
  margin-top: 14px;
  min-height: 44px;
}

/* ── Pixel sliders (custom divs — never a native <input type=range>) ── */
.px-slider {
  position: relative;
  flex: 1 1 auto;
  height: 28px;
  cursor: pointer;
  touch-action: none;
  outline: none;
}
.px-slider:focus-visible {
  outline: 2px solid #ffd23f;
  outline-offset: 3px;
}
.px-slider-track {
  position: absolute;
  left: 0;
  right: 0;
  top: 9px;
  height: 10px;
  background: #0d130a;
  border: 2px solid #0a0d07;
  box-shadow: inset 0 2px 0 rgba(255, 255, 255, 0.12);
}
.px-slider-fill {
  height: 100%;
  width: 0;
  background: repeating-linear-gradient(90deg, #57d46a 0 8px, #3f9b3d 8px 10px);
}
.px-slider-handle {
  position: absolute;
  top: 2px;
  left: 2px;
  width: 14px;
  height: 24px;
  background: #ffd23f;
  border: 2px solid #0a0d07;
  box-shadow: 2px 2px 0 rgba(0, 0, 0, 0.5);
  pointer-events: none;
}

.craft-search {
  width: 100%;
  padding: 4px 8px;
  font-size: 11px;
  font-family: inherit;
  color: #cfcba9;
  background: #0a0d07;
  border: 2px solid #3f5c33;
  outline: none;
  margin-bottom: 6px;
  box-sizing: border-box;
}

.craft-search:focus {
  border-color: #d4a03f;
}

.bag-search {
  margin-top: 10px;
  margin-bottom: 8px;
}

.tree-search {
  margin-bottom: 6px;
}

/* ── Consumable bar ── */
#consumable-bar {
  display: none !important;
}

#consumable-bar.hide {
  display: none;
}

/* ── Quick-use slots (Q=potion1, E=potion2, R=scroll, A=ally/Shift+E) ── */
#potion-slot,
#quick-slot,
#scroll-slot,
#ally-slot {
  zoom: var(--hud);
  /* Containing block for the .qs-qty badge — without it the absolutely
     positioned counter resolves against #dock (position: fixed) and lands on
     the HUD collapse button instead of its own slot. */
  position: relative;
  width: 56px;
  height: 56px;
  background: #1c2817;
  border: 3px solid #0a0d07;
  box-shadow: 3px 3px 0 rgba(0, 0, 0, 0.55);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  pointer-events: auto;
  z-index: 16;
  image-rendering: pixelated;
  flex: 0 0 auto;
}
/* The ally slot is 75 % of the normal slot (spec §4.1) — 42 vs 56 — and never
   shrinks below that on a narrow phone: #dock-left is flex-wrap, so a full
   row wraps the slot to the next line instead of squeezing it. */
#ally-slot {
  width: 42px;
  height: 42px;
}
#potion-slot { outline: 2px solid #57d46a; }
#potion-slot:hover { outline-color: #7dfc88; }
#quick-slot { outline: 2px solid #e05ec0; }
#quick-slot:hover { outline-color: #ff80c0; }
#scroll-slot { outline: 2px solid #c05ef0; }
#scroll-slot:hover { outline-color: #d080ff; }
/* Ally blue — the same family as .drop-btn.ally (§4.1). */
#ally-slot { outline: 2px solid #2a6fc9; }
#ally-slot:hover { outline-color: #4a8fe0; }

#potion-slot:active,
#quick-slot:active,
#scroll-slot:active,
#ally-slot:active {
  transform: translate(2px, 2px);
  box-shadow: 0 0 0 rgba(0,0,0,0);
}

#potion-slot .qs-icon,
#quick-slot .qs-icon,
#scroll-slot .qs-icon {
  width: 40px;
  height: 40px;
  image-rendering: pixelated;
  pointer-events: none;
}
#ally-slot .qs-icon {
  width: 30px;
  height: 30px;
  image-rendering: pixelated;
  pointer-events: none;
}

#potion-slot .qs-qty,
#quick-slot .qs-qty,
#scroll-slot .qs-qty {
  position: absolute;
  bottom: -4px;
  right: -4px;
  background: #0a0d07;
  font-size: 10px;
  font-weight: 900;
  min-width: 18px;
  height: 16px;
  padding: 0 3px;
  display: flex;
  align-items: center;
  justify-content: center;
}
#ally-slot .qs-qty {
  position: absolute;
  bottom: -4px;
  right: -4px;
  background: #0a0d07;
  font-size: 9px;
  font-weight: 900;
  min-width: 16px;
  height: 14px;
  padding: 0 3px;
  display: flex;
  align-items: center;
  justify-content: center;
}
#potion-slot .qs-qty { color: #57d46a; border: 1px solid #57d46a; }
#quick-slot .qs-qty { color: #e05ec0; border: 1px solid #e05ec0; }
#scroll-slot .qs-qty { color: #c05ef0; border: 1px solid #c05ef0; }
#ally-slot .qs-qty { color: #4a8fe0; border: 1px solid #4a8fe0; }

#potion-slot .qs-empty,
#quick-slot .qs-empty,
#scroll-slot .qs-empty,
#ally-slot .qs-empty {
  color: #55604d;
  font-size: 10px;
  font-weight: 900;
  text-align: center;
  pointer-events: none;
}
/* Owner ruling (round 11): the EMPTY ally slot PRINTS ITS HOTKEY on two
   lines — SHIFT / +E — instead of a bare "A". Scoped to #ally-slot only:
   the Q/E/R slots keep their single letters. The HUD mono is ~0.6em per
   glyph, so "SHIFT" is 30px inside the 36px content box (42px minus the
   3px borders) — two centered lines fit the box at every width. */
#ally-slot .qs-empty {
  font-size: 10px;
  line-height: 1.15;
  white-space: pre;
}

/* ── Potion timer (active buffs below the HP bar) ──
   width:0 + min-width:100% is the intrinsic-width suppression trick: it
   stops the chip row's one-line max-content from widening the shrink-to-fit
   #player-info box (an `auto` grid column of #topbar), so the chip list
   wraps into rows inside the box instead of stretching it across the
   screen. Both elements live under zoom:var(--hud). */
#potion-timer {
  display: flex;
  gap: 4px;
  margin-top: 4px;
  flex-wrap: wrap;
  width: 0;
  min-width: 100%;
}

#potion-timer.hide {
  display: none;
}

.potion-timer-item {
  display: flex;
  align-items: center;
  gap: 3px;
  background: #0a0d07;
  border: 1px solid #3f5c33;
  padding: 1px 4px;
  font-size: 9px;
  font-weight: 900;
  color: #cfcba9;
  white-space: nowrap;
  /* Hard 2-chips-per-row cap (50% − half the 4px column gap): a chip can
     shrink (with the name ellipsis below) but can never push the row past
     the box, so the box width is a true invariant at every window size. */
  min-width: 0;
  max-width: calc(50% - 2px);
}

/* "+N" overflow chip when more than 6 effects are live — same chip chrome,
   gold count, so the buff area has a hard 3-row height ceiling. */
.potion-timer-item.pt-more {
  color: #ffd23f;
  justify-content: center;
}

.potion-timer-item .pt-icon {
  width: 10px;
  height: 10px;
  image-rendering: pixelated;
  flex-shrink: 0;
}

.potion-timer-item .pt-name {
  color: inherit;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.potion-timer-item .pt-time {
  color: #ffd23f;
}

.cons-slot {
  width: 32px;
  height: 32px;
  background: #1c2817;
  border: 2px solid #3f5c33;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  position: relative;
}

.cons-slot:hover {
  border-color: #57d46a;
}

.cons-slot:active {
  background: #2a3a1a;
}

.cons-qty {
  position: absolute;
  top: -4px;
  right: -4px;
  background: #0a0d07;
  color: #cfcba9;
  font-size: 8px;
  padding: 0 3px;
  border: 1px solid #3f5c33;
}

/* ── FX bar ── */
#fx-bar {
  display: flex;
  gap: 4px;
  z-index: 15;
}

#fx-bar.hide {
  display: none;
}

.fx-icon {
  width: 24px;
  height: 24px;
  border: 2px solid #0a0d07;
  display: flex;
  align-items: center;
  justify-content: center;
  position: relative;
  animation: fx-pulse 1s ease-in-out infinite;
}

@keyframes fx-pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.6; }
}

/* ═══════════════════════════════════════════════════════════════════
   QUEST SYSTEM UI
   ═══════════════════════════════════════════════════════════════════ */

/* ── Always-on OBJECTIVE STRIP (Q5, round 10) — one line under the top bar:
   `FIRST STEPS · 3/10 SLIMES`, painted with PIXOOT.pf like every other HUD
   label, tappable → the SAME quest.toggleLog() the Q button uses. Built on
   demand by js/quest.js (so CONFIG.QUEST.objStrip off = NO NODE AT ALL, not
   just a hidden one) and hidden by the global .hide while there is no active
   quest. It owns the first line under the bar: every surface that would land
   on it clears it through `--objstrip-h` (see :root), and the un-zoomed
   consumers measure this element's REAL rect instead.

   Placement: LEFT under the bar — the centre lane is the toast / event-timer /
   boss-bar column and the right lane is the minimap + quest log, so the left
   edge is the only one that never moves. js/quest.js narrows `right` to stop
   short of the minimap's real rect at every width (360/390/1440 verified by
   _mmtest/objstrip-check.js). */
#obj-strip {
  position: fixed;
  zoom: var(--hud);
  /* The toast line, floored by the boss bar's reserved lane — the strip can
     never land under a transient #boss-hud-bar (see --bossbar-lane). */
  top: calc(max(var(--topbar-h) + 10px, var(--bossbar-lane) / var(--hud)) + env(safe-area-inset-top));
  /* 10 SCREEN px in from the edge (the #party-bar trick): zoom scales the
     offset, so pre-dividing keeps it aligned with the HP box above it. */
  left: calc(10px / var(--hud));
  right: 10px;                 /* narrowed by js/quest.js to clear the minimap */
  z-index: 17;                 /* above #quest-log (16) so its own toggle stays tappable */
  box-sizing: border-box;
  display: flex;
  align-items: center;
  gap: 6px;
  /* >= 44 SCREEN px tap box at every HUD scale (--hud can be 0.5 on a short
     phone, so the box is pre-divided exactly like #party-bar's inset). */
  min-height: calc(44px / var(--hud));
  min-width: calc(44px / var(--hud));
  padding: 4px 10px;
  background: #1c2817;
  border: 3px solid #0a0d07;
  outline: 2px solid #7a5f14;
  box-shadow: 4px 4px 0 rgba(0, 0, 0, 0.55);
  border-radius: 0;
  color: #cfcba9;
  text-align: left;
  cursor: pointer;
  pointer-events: auto;
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
}
#obj-strip:active {
  transform: translate(2px, 2px);
  box-shadow: 2px 2px 0 rgba(0, 0, 0, 0.55);
}
#obj-strip:focus-visible {
  outline-color: #ffd23f;
}
/* Painted canvas + the display:none text mirror (the js/netstate.js conn-strip
   convention): the canvas carries the pixels, .obj-strip-text carries the real
   string for tests and tooling, and the button's aria-label carries it for
   screen readers. */
.obj-strip-paint {
  display: block;
  line-height: 0;
  image-rendering: pixelated;
  image-rendering: crisp-edges;
}
.obj-strip-paint canvas {
  display: block;
  image-rendering: pixelated;
  image-rendering: crisp-edges;
}
.obj-strip-text {
  display: none;
}

/* ── Quest toggle button (top-right, below currencies) ── */
.quest-toggle-btn {
  zoom: var(--hud);
  pointer-events: auto;
  position: relative;
  width: 38px;
  height: 38px;
  background: #1c2817;
  border: 3px solid #0a0d07;
  outline: 2px solid #7a5f14;
  box-shadow: 4px 4px 0 rgba(0, 0, 0, 0.55);
  border-radius: 0;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  margin-top: 8px;
  flex-shrink: 0;
  transition: background 0.1s;
}

.quest-toggle-btn.active {
  background: #2c4a18;
  outline-color: #57d46a;
}

.quest-toggle-btn:active {
  transform: translate(2px, 2px);
  box-shadow: 0 0 0 rgba(0, 0, 0, 0.55);
}

/* ── Quest log side panel ── */
#quest-log {
  zoom: var(--hud);
  position: fixed;
  /* Opens BELOW the strip (Q5): the strip stays tappable so a second tap on
     it closes the log again — the panel never sits on top of its own toggle. */
  top: calc(var(--topbar-h) + 10px + var(--objstrip-h) + env(safe-area-inset-top));
  right: 12px;
  width: 240px;
  max-height: 55vh;
  background: #0c1208;
  border: 3px solid #0a0d07;
  outline: 2px solid #3f5c33;
  box-shadow: 6px 6px 0 rgba(0, 0, 0, 0.6);
  border-radius: 0;
  z-index: 16;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  pointer-events: auto;
}

#quest-log.hide {
  display: none;
}

.ql-head {
  position: relative;
  background: #1c2817;
  border-bottom: 2px solid #0a0d07;
  padding: 6px 10px;
  flex-shrink: 0;
}

.ql-title {
  font-size: 11px;
  font-weight: 900;
  letter-spacing: 2px;
  color: #ffd23f;
  text-shadow: 2px 2px 0 rgba(0, 0, 0, 0.6);
}

.ql-list {
  flex: 1;
  overflow-y: auto;
  padding: 6px;
  scrollbar-width: thin;
  scrollbar-color: #3f5c33 #0a0d07;
}

.ql-list::-webkit-scrollbar {
  width: 6px;
}

.ql-list::-webkit-scrollbar-track {
  background: #0a0d07;
}

.ql-list::-webkit-scrollbar-thumb {
  background: #3f5c33;
  border: 1px solid #0a0d07;
}

.ql-card {
  background: #162110;
  border: 2px solid #0a0d07;
  box-shadow: 3px 3px 0 rgba(0, 0, 0, 0.4);
  padding: 8px 8px 6px;
  margin-bottom: 6px;
  border-radius: 0;
}

.ql-card.ql-done {
  border-color: #2a5a20;
  background: #1a2a14;
}

.ql-name {
  margin-bottom: 3px;
  line-height: 0;
}

.ql-name canvas {
  display: inline-block;
  vertical-align: top;
}

.ql-desc {
  font-size: 10px;
  color: #9db18c;
  margin-bottom: 5px;
  letter-spacing: 0.5px;
}

.ql-obj {
  font-size: 10px;
  padding: 2px 0;
  display: flex;
  align-items: center;
  gap: 4px;
}

.ql-prog {
  color: #cfcba9;
  font-weight: 700;
  letter-spacing: 0.5px;
}

.ql-prog-done {
  color: #57d46a;
}

.ql-complete-badge {
  margin-top: 4px;
  line-height: 0;
}

.ql-complete-badge canvas {
  display: inline-block;
  vertical-align: top;
}

.ql-empty {
  padding: 12px 6px;
  text-align: center;
  line-height: 0;
}

.ql-empty canvas {
  display: inline-block;
  vertical-align: top;
}

/* ── Quest Board overlay ── */
#questboard-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.55);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 22;
}

#questboard-overlay.hide {
  display: none;
}

.qb-card {
  position: relative;
  width: min(320px, 90vw);
  max-height: 72vh;
  display: flex;
  flex-direction: column;
  color: #f2eeca;
  background: #1c2817;
  border: 4px solid #0a0d07;
  outline: 3px solid #ffd23f;
  box-shadow: 8px 8px 0 rgba(0, 0, 0, 0.65);
  border-radius: 0;
  padding: 16px 18px;
  overflow: hidden;
}

.qb-title {
  font-size: 16px;
  color: #ffd23f;
  text-shadow: 2px 2px 0 rgba(0, 0, 0, 0.5);
  text-align: center;
  letter-spacing: 3px;
  margin-bottom: 4px;
  font-weight: 900;
}

.qb-sub {
  font-size: 10px;
  color: #577a4a;
  text-align: center;
  margin-bottom: 4px;
}

.qb-active-line {
  font-size: 10px;
  color: #9db18c;
  text-align: center;
  margin-bottom: 10px;
  letter-spacing: 1px;
}

.qb-active-line span {
  color: #ffd23f;
  font-weight: 900;
}

.qb-list {
  flex: 1;
  overflow-y: auto;
  scrollbar-width: thin;
  scrollbar-color: #3f5c33 #0a0d07;
}

.qb-list::-webkit-scrollbar {
  width: 6px;
}

.qb-list::-webkit-scrollbar-track {
  background: #0a0d07;
}

.qb-list::-webkit-scrollbar-thumb {
  background: #3f5c33;
}

.qb-hint {
  font-size: 10px;
  color: #6a8a5a;
  text-align: center;
  padding: 14px 6px;
  letter-spacing: 0.5px;
}

.qb-row {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px;
  margin-bottom: 6px;
  background: #162110;
  border: 2px solid #0a0d07;
  box-shadow: 3px 3px 0 rgba(0, 0, 0, 0.4);
  border-radius: 0;
}

.qb-info {
  flex: 1;
  min-width: 0;
}

.qb-name {
  margin-bottom: 2px;
  line-height: 0;
}

.qb-name canvas {
  display: inline-block;
  vertical-align: top;
}

.qb-desc {
  font-size: 10px;
  color: #9db18c;
  margin-bottom: 3px;
}

.qb-reward {
  font-size: 9px;
  color: #ffd23f;
  font-weight: 700;
  letter-spacing: 0.5px;
}

.qb-accept {
  pointer-events: auto;
  flex-shrink: 0;
  font-weight: 900;
  font-size: 10px;
  color: #1a1200;
  background: #57d46a;
  border: 2px solid #0a0d07;
  box-shadow: 0 3px 0 #2a6a2a;
  border-radius: 0;
  padding: 6px 10px;
  cursor: pointer;
  letter-spacing: 1px;
  touch-action: manipulation;
}

.qb-accept:active {
  transform: translate(2px, 2px);
  box-shadow: 0 0 0 #2a6a2a;
}

.qb-active {
  border-color: #2a5a20;
  background: #1a2a14;
}

.qb-abandon {
  pointer-events: auto;
  flex-shrink: 0;
  width: 22px;
  height: 22px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 16px;
  font-weight: 900;
  color: #8a3a3a;
  background: #2a1414;
  border: 2px solid #0a0d07;
  cursor: pointer;
  line-height: 1;
  touch-action: manipulation;
}

.qb-abandon:hover, .qb-abandon:active {
  color: #ff5d5d;
  background: #3a1a1a;
}

.ql-abandon {
  position: absolute;
  top: 4px;
  right: 6px;
  width: 18px;
  height: 18px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 14px;
  font-weight: 900;
  color: #6a3a3a;
  background: #1a0e0e;
  border: 1px solid #0a0d07;
  cursor: pointer;
  line-height: 1;
  touch-action: manipulation;
  border-radius: 0;
}

.ql-abandon:hover, .ql-abandon:active {
  color: #ff5d5d;
  background: #3a1a1a;
}

.ql-card {
  position: relative;
}

.qb-close {
  flex-shrink: 0;
  background: #2a4a6a;
  margin-top: 8px;
  position: relative;
  z-index: 10;
}

/* ── Close X buttons for panels and overlays ── */
.panel-x,
.overlay-x {
  position: absolute;
  top: 6px;
  right: 6px;
  width: 22px;
  height: 22px;
  background: #0a0d07;
  border: 2px solid #55604d;
  color: #cfcba9;
  font-size: 13px;
  font-weight: 900;
  line-height: 18px;
  text-align: center;
  cursor: pointer;
  z-index: 5;
  padding: 0;
  font-family: inherit;
}
.panel-x:hover,
.overlay-x:hover,
.hall-x:hover {
  background: #3a1a1a;
  border-color: #ff5d5d;
  color: #ff5d5d;
}

/* ══════════════════════════════════════════════════════════════════════
   HALL OF FAME (designs/leaderboard-gallery.html §5/§6/§7/§8/§9 — the
   approved mock). ONE #lboard-root node (js/hall.js) rendered into two
   hosts: #tab-hall (menu) and #hall-overlay (in-game, qb-card chassis).
   Laws baked in here: only the list scrolls · 44px pager (the lobby's 28px
   fail the touch law) · ≥44px everywhere · NO hover-dependent info ·
   medals always sit on a #0d130a well · reduced-motion = static end states
   · safe-area padding in the in-game host · ≤380px column/wrap rules.
   ════════════════════════════════════════════════════════════════════ */
#hall-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.55);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 22;
  /* in-game host: never sit under a notch / home indicator */
  padding: calc(12px + env(safe-area-inset-top)) calc(12px + env(safe-area-inset-right))
           calc(12px + env(safe-area-inset-bottom)) calc(12px + env(safe-area-inset-left));
}
#hall-overlay.hide { display: none; }

/* qb-card recipe (4px near-black border + 3px gold outline + 8px hard shadow)
   at the gallery's window size: min(420px, 94vw) × 78vh. */
.hall-card {
  position: relative;
  width: min(420px, 94vw);
  max-height: 78vh;
  display: flex;
  flex-direction: column;
  color: #f2eeca;
  background: #10170d;
  border: 4px solid #0a0d07;
  outline: 3px solid #ffd23f;
  box-shadow: 8px 8px 0 rgba(0, 0, 0, 0.65);
  border-radius: 0;
  padding: 8px;
  overflow: hidden;
}

.lboard-root {
  display: flex;
  flex-direction: column;
  gap: 6px;
  width: 100%;
  min-height: 0;
  max-height: 78vh;
  overflow: hidden;
}
/* ONLY the list may shrink: everything else is fixed chrome. Without this the
   column flex squeezes the me-row — a shrunk main axis makes its flex-wrap
   wrap into a SECOND COLUMN (the note lands beside the button and bleeds out
   of the card) — and the list collapses to whatever is left over. */
.lboard-root > * { flex-shrink: 0; }
/* inside the in-game card the CARD owns the 78vh cap, not the root */
.hall-card .lboard-root { max-height: none; flex: 1 1 auto; min-height: 0; }

/* ---- header ------------------------------------------------------------ */
.hall-head {
  position: relative;
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  padding-right: 26px;
}
.hall-head-main { flex: 1 1 100%; text-align: center; min-width: 0; }
.hall-title {
  font-size: 16px;
  color: #ffd23f;
  letter-spacing: 3px;
  font-weight: 900;
  text-shadow: 2px 2px 0 rgba(0, 0, 0, 0.5);
}
.hall-sub {
  font-size: 10px;
  color: #577a4a;
  letter-spacing: 1px;
  margin-top: 2px;
}
.hall-chip {
  font-size: 9px;
  color: #ffd23f;
  background: #162110;
  border: 1px solid #ffd23f;
  padding: 2px 6px;
  white-space: nowrap;
  letter-spacing: 1px;
}
.hall-upd { font-size: 10px; color: #577a4a; white-space: nowrap; }
.hall-x {
  position: absolute;
  /* top: 0, NOT -2 — .hall-head and .hall-pop-head sit flush against an
     overflow:hidden ANCESTOR (#lboard-root / .hall-pop), so a negative top
     put the top 2px of the button (its whole top border) OUTSIDE the clip
     edge and it rendered cut off. top: 0 lands the box exactly on the clip
     edge — fully painted, no layout shift anywhere else
     (.hall-modal .hall-x keeps its own 4px inset). */
  top: 0;
  right: 0;
  width: 26px;
  height: 26px;
  background: #0a0d07;
  border: 2px solid #55604d;
  color: #cfcba9;
  font-size: 14px;
  font-weight: 900;
  line-height: 20px;
  text-align: center;
  cursor: pointer;
  padding: 0;
  z-index: 6;
}
.hall-x:active { background: #3a1a1a; border-color: #ff5d5d; color: #ff5d5d; }

/* ---- guest banner (whole strip is a button) ---------------------------- */
.hall-banner {
  display: block;
  width: 100%;
  font-family: inherit;
  border: 0;
  border-top: 1px solid #ffd23f;
  border-bottom: 1px solid #ffd23f;
  background: #131a0e;
  text-align: center;
  padding: 8px 6px;
  cursor: pointer;
  min-height: 44px;
}
.hall-banner .b1 { display: block; font-size: 11px; color: #ffd23f; font-weight: 900; letter-spacing: 1px; }
.hall-banner .b2 { display: block; font-size: 10px; color: #9db18c; margin-top: 3px; }
.hall-banner:active { background: #1a2313; }
/* 429 / frozen-season banners reuse the strip with a silver (non-CTA) face */
.hall-banner.state {
  cursor: default;
  border-color: #c9ccd1;
  min-height: 0;
}
.hall-banner.state .b1 { color: #c9ccd1; }
.hall-banner.hide { display: none; }

/* ---- board chips (.chat-subtab chassis, wraps at 360px) ---------------- */
.hall-tabs {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  align-items: center;
}
.hall-tabs .hall-tab { min-height: 40px; }
.hall-seasons { display: flex; gap: 6px; margin-left: auto; }
.hall-tabs .hall-season { min-height: 32px; padding: 6px 8px; font-size: 10px; }

/* ---- list (THE ONLY scroller) ------------------------------------------ */
.hall-list {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  background: #0a0d07;
  /* 12px top = crown headroom: `.hall-crown { top:-9px }` (art review FIX 1 —
     with 4px the scroller clipped the crown's jewel tips/spike rows) */
  padding: 12px 4px 4px;
  display: flex;
  flex-direction: column;
  gap: 3px;
  scrollbar-width: thin;
  scrollbar-color: #3f5c33 #0a0d07;
}
.hall-list::-webkit-scrollbar { width: 6px; }
.hall-list::-webkit-scrollbar-track { background: #0a0d07; }
.hall-list::-webkit-scrollbar-thumb { background: #3f5c33; }

.hall-row {
  position: relative;
  display: flex;
  align-items: center;
  gap: 6px;
  background: #162110;
  border: 2px solid #0a0d07;
  box-shadow: 3px 3px 0 #0a0d07;
  /* 44px (v1.1): rows are TAPPABLE now — they open the placement popup, so
     they carry the same touch law as the pager. */
  min-height: 44px;
  padding: 2px 6px;
  cursor: pointer;
  touch-action: manipulation;
}
.hall-row:active { background: #1b2812; }
.hall-row:focus-visible { outline: 2px solid #57d46a; outline-offset: 1px; }
/* 28px medal/rank slot on the MANDATORY #0d130a well (gallery DON'T list) */
.hall-slot {
  width: 28px;
  min-width: 28px;
  height: 28px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: #0d130a;
  border: 1px solid #24301a;
}
.hall-slot canvas { image-rendering: pixelated; display: block; }
.hall-slot .hall-slotnum { font-size: 10px; color: #cfcba9; font-weight: 900; }
.hall-rank { font-size: 12px; color: #cfcba9; min-width: 20px; text-align: right; }
.hall-name {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 4px;
}
.hall-name canvas { image-rendering: pixelated; display: block; }
.hall-titlechip {
  font-size: 9px;
  color: #c9a63f;
  border: 1px solid #4a4326;
  padding: 1px 4px;
  white-space: nowrap;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
}
.hall-titlechip:empty { display: none; }
.hall-hero { font-size: 9px; color: #8a9a7a; white-space: nowrap; }
.hall-hero:empty { display: none; }
.hall-val { font-size: 12px; color: #ffd23f; white-space: nowrap; font-weight: 900; }
.hall-ex {
  font-size: 9px;
  color: #9db18c;
  white-space: nowrap;
  min-width: 44px;
  text-align: right;
}
.hall-dl { font-size: 10px; min-width: 16px; text-align: center; }
.hall-dl.up { color: #7dfc88; }
.hall-dl.dn { color: #ff5d5d; }
.hall-dl.eq { color: #577a4a; }
.hall-pip { font-size: 9px; line-height: 1; }
.hall-pip.server { color: #ffd23f; }
.hall-pip.client { color: #9aa; }

/* ---- PARTY group chips (PARTY tab ONLY — js/hall.js partyGroupEl) --------
   After the holder's name/hero line: the ic_party glyph (or the "·with·"
   text prefix when the sprite id has not shipped) + one dim chip per OTHER
   member, "+N" past 3 members. Static presentation: no hover-only info, no
   animation (reduced-motion blocks untouched), chips are aria-hidden (the
   row's composed aria-label carries the group), and the cluster can only
   GROW the row — .hall-row's 44px min-height floor is never reduced.
   ≤380px the cluster is forced onto its own line under the name. */
.hall-group {
  flex: 0 1 auto;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 4px;
  min-width: 0;
}
.hall-group canvas { image-rendering: pixelated; display: block; }
.hall-group-with {
  font-size: 9px;
  color: #577a4a;
  letter-spacing: 1px;
  white-space: nowrap;
}
.hall-mchip {
  font-size: 9px;
  line-height: 1.4;
  color: #7f9070;                 /* dim: the chips are context, not the stat */
  background: #0f1609;
  border: 1px solid #2f4028;
  padding: 1px 5px;
  max-width: 100%;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.hall-mchip.more {
  color: #9db18c;
  border-color: #3f5c33;
  font-weight: 900;
  letter-spacing: 1px;
}

/* top-3 treatments: #1 64px gold ring + crown notch + 34px medal ·
   #2/#3 52px + silver/bronze left bar · #4–10 plain 40px */
.hall-row.r1 {
  min-height: 64px;
  box-shadow: 0 0 0 2px #ffd23f, 3px 3px 0 #0a0d07;
}
.hall-row.r1 .hall-slot { width: 34px; min-width: 34px; height: 34px; }
.hall-row.r1 .hall-name { font-weight: 900; }
.hall-row.r2 { min-height: 52px; border-left: 6px solid #c0c0c0; }
.hall-row.r3 { min-height: 52px; border-left: 6px solid #cd7f32; }
.hall-row.r2 .hall-rank,
.hall-row.r3 .hall-rank { color: #e8e2c0; }
.hall-crown { position: absolute; top: -9px; left: 24px; line-height: 0; }
.hall-crown canvas { image-rendering: pixelated; display: block; }
.hall-row.hall-enter { animation: hallEnter 400ms steps(2, end) 1; }
@keyframes hallEnter {
  from { box-shadow: 0 0 0 2px #ffd23f, 3px 3px 0 #0a0d07; }
  to   { box-shadow: 3px 3px 0 #0a0d07; }
}

/* ---- footer hint / src-pip legend (printed once, never hover-only) ----- */
.hall-legend {
  font-size: 9px;
  color: #577a4a;
  text-align: center;
  letter-spacing: 0.5px;
  padding: 2px 4px;
}

/* ---- sticky "you" row (flex footer — NEVER position:sticky) ------------ */
.hall-me {
  display: flex;
  align-items: center;
  gap: 6px;
  flex-wrap: wrap;
  background: #162110;
  border: 2px solid #7dfc88;
  box-shadow: 3px 3px 0 #0a0d07;
  min-height: 44px;
  padding: 4px 6px;
}
.hall-me .hall-rk {
  font-size: 12px;
  color: #7dfc88;
  background: #0a2a12;
  border: 1px solid #7dfc88;
  padding: 1px 5px;
  white-space: nowrap;
}
.hall-me .hall-nm { flex: 1 1 auto; font-size: 12px; color: #7dfc88; min-width: 0; }
.hall-me .hall-nm canvas { image-rendering: pixelated; display: block; }
.hall-me .hall-val { color: #7dfc88; }
.hall-me .hall-dist { font-size: 9px; color: #9db18c; white-space: nowrap; }
.hall-me .hall-band {
  font-size: 9px;
  color: #9db18c;
  border: 1px solid #3f5c33;
  padding: 1px 4px;
  white-space: nowrap;
}
.hall-me .hall-note { flex: 1 1 100%; font-size: 10px; color: #9db18c; }
.hall-me.dim { border-color: #3f5c33; }
.hall-me.dim .hall-nm { color: #9db18c; }
.hall-me.ghost { border-color: #577a4a; background: #131a0e; opacity: 0.9; }
.hall-me.ghost .hall-nm { color: #9db18c; }
.hall-me.cta {
  border-color: #57d46a;
  flex-direction: column;
  align-items: stretch;
  gap: 4px;
  text-align: center;
}
.hall-me.hall-enter { animation: hallFlash 600ms steps(2, end) 1; }
@keyframes hallFlash {
  from { border-color: #ffd23f; }
  to   { border-color: #7dfc88; }
}
.hall-hide { display: none !important; }
/* one scrollable list only (429 dims it — the busy banner stays live) */
.hall-list.hall-busy { opacity: 0.4; }
/* ERROR: gallery §8 parity — 1px #ff5d5d hairline around the failed list */
.hall-list.hall-err { border: 1px solid #ff5d5d; }
/* CLOSE rides every state (chrome tail: legend + CLOSE) */
.hall-close { margin-top: 2px; width: 100%; }

/* ---- pager (44px — the lobby's 28px fails the touch law) --------------- */
.hall-pager {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  flex-wrap: wrap;
}
.hall-btn {
  font-family: inherit;
  font-size: 11px;
  font-weight: 900;
  letter-spacing: 1px;
  color: #e8e2c0;
  background: #141a0e;
  border: 2px solid #3f5c33;
  min-height: 44px;
  min-width: 44px;
  padding: 6px 10px;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  touch-action: manipulation;
}
.hall-btn:active { transform: translateY(2px); }
.hall-btn.accent { background: #3f6b2a; color: #eaffd8; border-color: #57d46a; }
.hall-btn[aria-disabled="true"], .hall-btn:disabled {
  opacity: 0.35;
  cursor: default;
}
.hall-btn canvas { image-rendering: pixelated; display: block; }
.hall-pgno { font-size: 10px; color: #577a4a; min-width: 90px; text-align: center; letter-spacing: 1px; }

/* ---- states ------------------------------------------------------------ */
.hall-skel {
  display: flex;
  flex-direction: column;
  gap: 3px;
}
.hall-skel-row {
  background: #162110;
  border: 2px solid #0a0d07;
  box-shadow: 3px 3px 0 #0a0d07;
  position: relative;
  overflow: hidden;
}
.hall-skel-row::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(90deg, transparent 0 33%, rgba(255, 210, 63, 0.14) 33% 66%, transparent 66% 100%);
  background-size: 300% 100%;
  animation: hallShimmer 1.2s steps(3, end) infinite;
}
@keyframes hallShimmer {
  from { background-position: 150% 0; }
  to   { background-position: -150% 0; }
}
.hall-state {
  text-align: center;
  padding: 16px 10px;
}
.hall-state-t { font-size: 12px; color: #ffd23f; font-weight: 900; letter-spacing: 1px; }
.hall-state-t.err { color: #ff5d5d; }
.hall-state-s { font-size: 10px; color: #577a4a; margin-top: 6px; }
.hall-state .hall-btn { margin-top: 10px; width: 100%; }
.hall-state .hall-banner { margin-top: 10px; text-align: left; }

/* ---- welcome-back modal (namecard chassis) ---------- */
.hall-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.72);
  z-index: 95;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 16px;
}
.hall-modal {
  position: relative;
  width: min(300px, 94vw);
  background: #10170d;
  border: 3px solid #0a0d07;
  box-shadow: 0 0 0 2px #ffd23f, 6px 6px 0 rgba(0, 0, 0, 0.6);
  padding: 16px 12px 14px;
  text-align: center;
}
.hall-modal .hall-modal-ic { display: flex; justify-content: center; margin-bottom: 6px; }
.hall-modal .hall-modal-ic canvas { image-rendering: pixelated; display: block; }
.hall-modal .hall-title { font-size: 14px; letter-spacing: 1px; }
.hall-modal .hall-modal-sub { font-size: 10px; color: #9db18c; margin: 6px 0 12px; line-height: 1.5; }
.hall-modal .hall-btn { width: 100%; }
.hall-modal .hall-x { top: 4px; right: 4px; }

/* ══ PLACEMENT POPUP (.hall-pop) — a row tap opens this INNER layer ═══════
   namecard/qb chassis (4px near-black border + 3px gold outline + hard
   shadow — the .hall-card recipe). Flow: header (medal · rank · Nick#1234567
   · <title> · hero · board · value · achieved) → character preview stage →
   EQUIPPED strip → ALLY strip (only when the snapshot carries ally gear) →
   GLYPHS strip → detail pane.
   Laws: no hover-only info anywhere · every control ≥44px · the detail pane
   sits to the RIGHT when the box is wide enough (flex-wrap does it with no
   breakpoint) and stacks under it when it is not · ≤380px drills INTO the
   detail with a BACK affordance · reduced motion = one static pose (the rAF
   loop is never started, js/hall.js paintStage/startPopFrame). */
.hall-pop-layer {
  position: absolute;
  inset: 0;
  z-index: 30;
  background: rgba(0, 0, 0, 0.6);
  display: flex;
  align-items: center;
  justify-content: center;
  /* same safe-area rule as #hall-overlay (in-game notch / home indicator) */
  padding: calc(12px + env(safe-area-inset-top)) calc(12px + env(safe-area-inset-right))
           calc(12px + env(safe-area-inset-bottom)) calc(12px + env(safe-area-inset-left));
}
.hall-pop {
  position: relative;
  width: 100%;
  max-width: 680px;
  max-height: 100%;
  min-height: 0;
  display: flex;
  flex-direction: column;
  color: #f2eeca;
  background: #10170d;
  border: 4px solid #0a0d07;
  outline: 3px solid #ffd23f;
  box-shadow: 4px 4px 0 rgba(0, 0, 0, 0.65);
  overflow: hidden;
}
/* the box is focused programmatically on open — keep the chassis outline
   (never the UA focus ring, which would eat the gold edge) */
.hall-pop:focus,
.hall-pop:focus-visible { outline: 3px solid #ffd23f; }

.hall-pop-head {
  position: relative;
  display: flex;
  align-items: center;
  gap: 6px;
  flex-wrap: wrap;
  padding: 8px 32px 8px 8px;
  border-bottom: 1px solid #24301a;
}
.hall-pop-medal {
  width: 28px;
  min-width: 28px;
  height: 28px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: #0d130a;
  border: 1px solid #24301a;
}
.hall-pop-medal canvas { image-rendering: pixelated; display: block; }
.hall-pop-rank { font-size: 12px; color: #cfcba9; font-weight: 900; }
.hall-pop-id {
  flex: 1 1 100%;
  min-width: 0;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 4px;
}
/* name plate: banner_placement tails hang past the box, only the crimson core
   is tiled to the measured width (03-popup-art §2.1); the caption is DOM text
   — art grids never carry baked lettering. */
.hall-pop-plate {
  position: relative;
  display: inline-flex;
  align-items: center;
  /* the name is centred INSIDE the plate (PO 2026-10-25) — the crimson core
     is tiled to the measured width, so the caption must not hug one edge */
  justify-content: center;
  min-height: 24px;
  /* half a character of extra air each side (13px px-font ≈ 8–9px a glyph):
     the text used to sit ON the plate's inner edges */
  padding: 0 12px;
  max-width: 100%;
  box-sizing: border-box;
}
.hall-pop-band {
  position: absolute;
  left: 0;
  top: 0;
  display: block;
  image-rendering: pixelated;
}
.hall-pop-name {
  position: relative;
  z-index: 1;
  font-size: 13px;
  color: #ffd23f;
  font-weight: 900;
  letter-spacing: 1px;
  /* dynamic box: the plate grows WITH the name (one line), and only at the
     container's edge does the name ellipsize — never spill over the art */
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  min-width: 0;
  max-width: 100%;
  /* letter-spacing adds a trailing gap after the last glyph; pull it back so
     the centred name is optically centred too */
  margin-right: -1px;
}
.hall-pop-hero { font-size: 9px; color: #8a9a7a; white-space: nowrap; }
.hall-pop-meta {
  flex: 1 1 100%;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  min-width: 0;
}
.hall-pop-board {
  font-size: 9px;
  color: #ffd23f;
  background: #162110;
  border: 1px solid #ffd23f;
  padding: 2px 6px;
  letter-spacing: 1px;
  white-space: nowrap;
}
.hall-pop-val { font-size: 12px; color: #ffd23f; font-weight: 900; white-space: nowrap; }
.hall-pop-chip { display: inline-flex; width: 20px; height: 20px; line-height: 0; }
.hall-pop-chip canvas { image-rendering: pixelated; display: block; }
.hall-pop-frozen { font-size: 9px; color: #c9a63f; font-weight: 900; letter-spacing: 1px; white-space: nowrap; }
.hall-pop-when { font-size: 9px; color: #577a4a; white-space: nowrap; }
/* .hall-pop-cap (the "◆ server-observed · captured at this placement" stage
   caption) was REMOVED with its element — PO 2026-10-25; the header's src pip
   + AT PLACEMENT chip carry provenance now. */

.hall-pop-body {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-wrap: wrap;
  align-content: flex-start;
  gap: 6px;
  padding: 8px;
  overflow-y: auto;
  scrollbar-width: thin;
  scrollbar-color: #3f5c33 #0a0d07;
}
.hall-pop-main { flex: 1 1 300px; min-width: 0; display: flex; flex-direction: column; gap: 8px; }

/* ---- preview stage (mtx.js drawPreview recipe) -------------------------- */
.hall-pop-stage {
  position: relative;
  height: 168px;
  background: #10170d;
  border: 2px solid #0a0d07;
  box-shadow: 3px 3px 0 rgba(0, 0, 0, 0.4);
  overflow: hidden;
}
/* the canvas BACKING store is stage×dpr: it must display at the stage's CSS
   size, not at its attribute size, or every dpr>1 render draws 2× too wide
   and gets clipped away at the right edge */
.hall-pop-stage canvas { display: block; width: 100%; height: 100%; image-rendering: pixelated; }
.hall-pop-fallback {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 4px;
  text-align: center;
  padding: 8px;
}
.hall-pop-fallback canvas { image-rendering: pixelated; display: block; }
.hall-pop-fallback .hall-skel { width: 70%; }
.hall-pop-qmark { font-size: 22px; font-weight: 900; color: #55604d; line-height: 1; }
.hall-pop-note { font-size: 10px; color: #9db18c; line-height: 1.5; }
.hall-pop-note.sub { color: #577a4a; }
.hall-pop-note.err { color: #ff5d5d; font-weight: 900; letter-spacing: 1px; }

/* ---- EQUIPPED / ALLY / GLYPHS strips -------------------------------------
   The ALLY strip reuses these classes 1:1 (same cells, same 44px targets);
   it is only ever built when snap.allyGear is non-empty (js/hall.js). */
.hall-pop-group { display: flex; flex-direction: column; gap: 4px; }
.hall-pop-grouphd { font-size: 9px; font-weight: 900; letter-spacing: 2px; color: #577a4a; }
.hall-pop-slots { display: flex; flex-wrap: wrap; gap: 6px; }
.hall-popslot {
  position: relative;
  min-width: 44px;
  min-height: 44px;
  padding: 2px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  background: #0d130a;
  border: 2px solid #0a0d07;
  box-shadow: 2px 2px 0 rgba(0, 0, 0, 0.45);
  color: #cfcba9;
  font-family: inherit;
  cursor: pointer;
  touch-action: manipulation;
}
.hall-popslot:active { transform: translateY(1px); }
.hall-popslot:focus-visible { outline: 2px solid #57d46a; outline-offset: 1px; }
.hall-popslot.sel { background: #1a2a10; box-shadow: 0 0 0 2px #ffcc44, 2px 2px 0 rgba(0, 0, 0, 0.45); }
/* the well = the artist's slot_gear/slot_glyph sprite at ×2 (22 → 44px), the
   item/glyph icon lands in its 16×16 core. The rarity tint rides the SPRITE's
   letter R at draw time (03-popup-art §3.1 "one or the other, never both") —
   the .rar-N CSS outline below is only the fallback when no art shipped. */
.hall-well {
  position: relative;
  width: 44px;
  height: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: #0a0d07;
}
.hall-well canvas.hall-wellart {
  position: absolute;
  left: 0;
  top: 0;
  width: 44px;
  height: 44px;
  display: block;
  image-rendering: pixelated;
  z-index: 0;
}
.hall-well img,
.hall-well canvas:not(.hall-wellart) {
  position: absolute;
  left: 6px;
  top: 6px;
  width: 32px;
  height: 32px;
  display: block;
  object-fit: contain;
  image-rendering: pixelated;
  z-index: 1;
}
.hall-pop-pips { display: flex; gap: 2px; }
.hall-pop-pip { width: 5px; height: 5px; background: #2a3a2a; }
.hall-pop-pip.on { background: #ffcc44; }

/* ---- popup PARTY block (full roster incl. the holder — DISPLAY ONLY) -----
   One line per member under the PARTY group head; the holder's `.self` line
   is the gold one. No button/hover affordance anywhere: members have their
   OWN board rows, so nothing here is tappable (js/hall.js renderPopParty
   builds spans/divs only). Legacy rows ship party:null → the block is not
   built at all. */
.hall-pop-party { display: flex; flex-direction: column; gap: 4px; }
.hall-pm {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  min-height: 32px;
  padding: 4px 6px;
  background: #131a0e;
  border: 1px solid #24301a;
  color: #9db18c;
  font-size: 11px;
}
.hall-pm.self {
  color: #ffd23f;
  background: #1b1a0e;
  border-color: #c9a63f;
  font-weight: 900;
}
.hall-pm-ic { display: inline-flex; line-height: 0; }
.hall-pm-ic canvas { image-rendering: pixelated; display: block; }
.hall-pm-nm { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.hall-pm-hero { font-size: 9px; color: #8a9a7a; white-space: nowrap; }

/* ---- detail pane — the inventory detail pane's anatomy ------------------ */
.hall-pop-detail {
  flex: 1 1 220px;
  min-width: 170px;
  background: #162110;
  border: 2px solid #0a0d07;
  box-shadow: 3px 3px 0 rgba(0, 0, 0, 0.4);
  padding: 8px 10px;
  font-size: 11px;
  min-height: 40px;
}
.hall-pop-detail .d-name { font-weight: 900; letter-spacing: 1px; }
.hall-pop-detail .d-stats { color: #b8cda8; }
.hall-pop-detail .d-unidentified { margin-top: 4px; color: #ffd23f; font-weight: 900; }
.hall-pop-detail .d-act { color: #7dfc88; margin-top: 4px; }

.hall-pop-foot { display: flex; gap: 6px; padding: 0 8px 8px; flex-wrap: wrap; }
.hall-pop-foot .hall-btn { flex: 1 1 auto; }
.hall-pop-foot .hall-pop-close { flex: 1 1 100%; }
.hall-pop.hall-drill .hall-pop-foot .hall-pop-close { flex: 1 1 auto; }
.hall-pop-back { display: none; }
.hall-pop.hall-drill .hall-pop-back { display: inline-flex; }

/* ---- ≤380px: extra col hides, distance + MY PAGE take their own rows ----
   The name column stays put: .hall-name is a wrapping flex box, so the
   <title> chip and the hero name fall UNDER the name canvas inside the
   column (the gallery's 360px rule) without moving the value off the row. */
@media (max-width: 380px) {
  .hall-ex { display: none; }
  /* PARTY chips leave the name line and take their own row underneath */
  .hall-group { flex: 1 1 100%; }
  .hall-title { font-size: 13px; letter-spacing: 2px; }
  .hall-rank { min-width: 16px; }
  .hall-me .hall-dist { flex: 1 1 100%; white-space: normal; }
  .hall-mypage { flex: 1 1 100%; }
  .hall-pgno { min-width: 72px; }
  /* placement popup: drill INTO the detail (stage + strips step aside) and
     give the pane the full box; BACK + CLOSE share the foot row */
  .hall-pop-stage { height: 150px; }
  .hall-pop.hall-drill .hall-pop-main { display: none; }
  .hall-pop-detail { min-width: 0; }
  .hall-pop-name { font-size: 12px; }
}

/* ---- reduced motion: every animation → its static end state ------------ */
@media (prefers-reduced-motion: reduce) {
  .hall-skel-row::after { animation: none; background: rgba(255, 210, 63, 0.10); }
  .hall-row.hall-enter { animation: none; box-shadow: 3px 3px 0 #0a0d07; }
  .hall-me.hall-enter { animation: none; border-color: #7dfc88; }
  .hall-btn:active { transform: none; }
  /* placement popup: the preview never animates (js/hall.js draws ONE static
     pose — the rAF loop is not started), so no transform/transition either */
  .hall-popslot:active { transform: none; }
  .hall-pop .hall-skel-row::after { animation: none; background: rgba(255, 210, 63, 0.10); }
}

/* ── Search input clear X ── */
.search-wrap {
  position: relative;
  width: 100%;
}
.search-wrap .craft-search {
  padding-right: 22px;
}
.search-clear {
  position: absolute;
  top: 50%;
  right: 4px;
  transform: translateY(-50%);
  width: 16px;
  height: 16px;
  background: none;
  border: none;
  color: #55604d;
  font-size: 13px;
  font-weight: 900;
  line-height: 16px;
  text-align: center;
  cursor: pointer;
  padding: 0;
  display: none;
  font-family: inherit;
}
.search-wrap.has-text .search-clear {
  display: block;
}
.search-clear:hover {
  color: #ff5d5d;
}

/* ---- Auth dialog ---- */
.auth-status {
  position: absolute;
  top: 8px;
  right: 12px;
  z-index: 10;
  display: flex;
  align-items: center;
  gap: 6px;
}
.auth-badge {
  font-family: "Press Start 2P", monospace;
  font-size: 9px;
  padding: 3px 8px;
  border-radius: 4px;
  white-space: nowrap;
}
.auth-badge.guest {
  background: #3a2a10;
  color: #ffd23f;
  border: 1px solid #6b5020;
}
.auth-badge.registered {
  background: #1a3a1a;
  color: #7ddf64;
  border: 1px solid #3f6b3f;
}
.auth-logout,
.auth-import,
.auth-upgrade {
  font-family: "Press Start 2P", monospace;
  font-size: 8px;
  padding: 3px 8px;
  border: 1px solid #4a6b3c;
  background: #1a2412;
  color: #a8c89a;
  cursor: pointer;
  border-radius: 3px;
  white-space: nowrap;
}
.auth-logout:hover,
.auth-import:hover,
.auth-upgrade:hover {
  background: #2a3a20;
  color: #d2f0c0;
}

.auth-dialog {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(0, 0, 0, 0.7);
}
.auth-dialog-inner {
  position: relative;
  background: #10180c;
  border: 3px solid #4a6b3c;
  outline: 2px solid #2a3a20;
  border-radius: 8px;
  padding: 20px 24px;
  width: min(340px, 90vw);
  text-align: center;
  font-family: "Press Start 2P", monospace;
  color: #f2eeca;
}
.auth-dialog-inner h3 {
  font-size: 14px;
  margin: 0 0 12px;
  color: #7ddf64;
}
.auth-tabs {
  display: flex;
  gap: 4px;
  margin-bottom: 12px;
}
.auth-tab {
  flex: 1;
  font-family: "Press Start 2P", monospace;
  font-size: 9px;
  padding: 6px 8px;
  border: 2px solid #3f5c33;
  background: #0d130a;
  color: #7a9a6a;
  cursor: pointer;
  border-radius: 4px;
}
.auth-tab.active {
  background: #2a4a20;
  color: #d2f0c0;
  border-color: #7ddf64;
}
.auth-form input {
  width: 100%;
  box-sizing: border-box;
  font-family: "Press Start 2P", monospace;
  font-size: 10px;
  padding: 8px 10px;
  margin-bottom: 8px;
  border: 2px solid #3f5c33;
  background: #0d130a;
  color: #f2eeca;
  border-radius: 4px;
}
.auth-form input::placeholder {
  color: #5a7a4a;
}
.auth-submit {
  width: 100%;
  font-family: "Press Start 2P", monospace;
  font-size: 11px;
  padding: 10px;
  border: 2px solid #7ddf64;
  background: #2a5a20;
  color: #d2f0c0;
  cursor: pointer;
  border-radius: 4px;
  margin-bottom: 8px;
}
.auth-submit:hover {
  background: #3a7a30;
}
.auth-or {
  font-size: 9px;
  color: #5a7a4a;
  margin: 4px 0;
}
.auth-google {
  width: 100%;
  font-family: "Press Start 2P", monospace;
  font-size: 9px;
  padding: 8px;
  border: 2px solid #4285f4;
  background: #1a2a4a;
  color: #8ab4f8;
  cursor: pointer;
  border-radius: 4px;
}
.auth-google:hover {
  background: #2a3a5a;
}
.auth-msg {
  font-size: 8px;
  color: #ffd23f;
  margin-top: 8px;
  min-height: 12px;
}
.auth-close {
  position: absolute;
  top: 8px;
  right: 10px;
  font-size: 16px;
  background: none;
  border: none;
  color: #7a9a6a;
  cursor: pointer;
}
.auth-close:hover {
  color: #ff5d5d;
}

/* Inline auth panel */
.auth-tabs-inline {
  display: flex;
  gap: 6px;
  margin-bottom: 10px;
}
.auth-tab-btn {
  flex: 1;
  font-family: "Press Start 2P", monospace;
  font-size: 9px;
  padding: 8px 6px;
  border: 2px solid #0a0d07;
  background: #162110;
  color: #9db18c;
  cursor: pointer;
  text-transform: uppercase;
  letter-spacing: 1px;
  box-shadow: 0 3px 0 #0a0d07;
  transition: transform 0.05s, background 0.1s;
  clip-path: polygon(
    4px 0, calc(100% - 4px) 0,
    100% 4px, 100% calc(100% - 4px),
    calc(100% - 4px) 100%, 4px 100%,
    0 calc(100% - 4px), 0 4px
  );
}
.auth-tab-btn:hover {
  background: #223019;
  color: #ffd23f;
  border-color: #ffd23f;
}
.auth-tab-btn.active {
  background: #2c3f20;
  color: #ffd23f;
  border-color: #ffd23f;
  box-shadow: 0 3px 0 #ffd23f;
}
.auth-tab-btn:active {
  transform: translateY(2px);
  box-shadow: 0 0 0 #0a0d07;
}
.auth-input,
#settings-nickname {
  width: 100%;
  box-sizing: border-box;
  font-family: "Press Start 2P", monospace;
  font-size: 10px;
  padding: 8px 10px;
  margin-bottom: 6px;
  border: 2px solid #0a0d07;
  outline: 2px solid #4a6b3c;
  background: #0d130a;
  color: #f2eeca;
  letter-spacing: 1px;
  clip-path: polygon(
    4px 0, calc(100% - 4px) 0,
    100% 4px, 100% calc(100% - 4px),
    calc(100% - 4px) 100%, 4px 100%,
    0 calc(100% - 4px), 0 4px
  );
}
.auth-input:focus,
#settings-nickname:focus {
  outline-color: #7ddf64;
  background: #101a0c;
}
.auth-input::placeholder,
#settings-nickname::placeholder {
  color: #5a7a4a;
}
/* The nickname sits in a flex row next to its SAVE button: undo the stacked-
   form sizing above so it stretches like the chat inputs do. */
.chat-input-row #settings-nickname {
  flex: 1 1 auto;
  width: auto;
  min-width: 0;
  min-height: 44px;
  margin-bottom: 0;
}
/* Password requirements checklist */
.pw-reqs {
  margin: 4px 0 6px;
  padding: 0;
}
.pw-reqs-title {
  font-family: "Press Start 2P", monospace;
  font-size: 7px;
  color: #9db18c;
  margin-bottom: 3px;
}
.pw-req {
  font-family: "Press Start 2P", monospace;
  font-size: 8px;
  color: #5a7a4a;
  padding: 3px 0;
  display: flex;
  align-items: center;
  gap: 8px;
}
.pw-req.met {
  color: #7ddf64;
}
.pw-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 14px;
  height: 14px;
  flex-shrink: 0;
  font-family: Arial, Helvetica, sans-serif;
  font-size: 12px;
  line-height: 1;
  font-weight: bold;
  color: #e05e5e;
}
.pw-req.met .pw-icon {
  color: #7ddf64;
}
.auth-or {
  text-align: center;
  font-family: "Press Start 2P", monospace;
  font-size: 8px;
  color: #5a7a4a;
  margin: 6px 0;
}
#auth-google {
  clip-path: polygon(
    6px 0, calc(100% - 6px) 0,
    100% 6px, 100% calc(100% - 6px),
    calc(100% - 6px) 100%, 6px 100%,
    0 calc(100% - 6px), 0 6px
  );
  box-shadow: inset 0 0 0 1px rgba(255,255,255,0.15), 2px 2px 0 rgba(0,0,0,0.5);
  transition: transform 0.05s, filter 0.05s;
}
#auth-google:hover {
  filter: brightness(1.15);
}
#auth-google:active {
  transform: translate(2px, 2px);
  box-shadow: inset 0 0 0 1px rgba(255,255,255,0.15);
}

/* Privacy & Terms checkboxes (signup only) */
.auth-legal {
  margin: 6px 0 4px;
}
.auth-legal-check {
  display: flex;
  align-items: flex-start;
  gap: 6px;
  font-family: "Press Start 2P", monospace;
  font-size: 7px;
  color: #9db18c;
  margin-bottom: 4px;
  cursor: pointer;
  line-height: 1.5;
}
.auth-legal-check input[type="checkbox"] {
  width: 12px;
  height: 12px;
  min-width: 12px;
  margin-top: 1px;
  accent-color: #7ddf64;
  cursor: pointer;
}
.auth-legal-check a {
  color: #7ddf64;
  text-decoration: underline;
}
.auth-legal-check a:hover {
  color: #aaff99;
}

/* Legal links — pinned to the true bottom-right corner of the window.
   Visible on the main menu only (in-game the HUD owns that corner). */
#legal-links {
  position: fixed;
  right: calc(6px + env(safe-area-inset-right));
  bottom: calc(6px + env(safe-area-inset-bottom));
  z-index: 45;
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 3px 6px;
  font-family: "Press Start 2P", monospace;
  font-size: 7px;
  line-height: 1.5;
}
#legal-links a {
  color: #7ddf64;
  text-decoration: underline;
}
#legal-links a:hover {
  color: #aaff99;
}
.legal-sep {
  color: #4a6b3c;
}
body.in-game #legal-links {
  display: none;
}

/* Tutorial link — bottom of the main frame, shown in BOTH the single-player and
   co-op tabs (one copy per tab panel, see .menu-tut-foot in index.html).
   The link strip is centered between its divider line and the frame edge:
   symmetric padding + flex-centered 44px tap target (no downward stretch). */
.menu-tut-foot {
  margin-top: auto; /* pin the strip to the section bottom divider */
  padding-top: 8px;
  padding-bottom: 4px;
  border-top: 2px solid #2a3a22;
  text-align: center;
  display: flex;
  justify-content: center;
  font-family: "Press Start 2P", monospace;
  font-size: 9px;
}
.menu-tut-foot a {
  color: #7ddf64;
  text-decoration: none;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 6px 10px;
  line-height: 1.6;
}
/* Touch: keep the full 44px tap target (still vertically centered). */
@media (pointer: coarse) {
  .menu-tut-foot a {
    min-height: 44px;
  }
}
.menu-tut-foot a:hover,
.menu-tut-foot a:focus {
  color: #aaff99;
  text-decoration: underline;
}

/* Mobile / touch devices — the 7px corner links land under the menu frame and
   are unmissable-small on phones, so the links are rendered as a footer at the
   bottom of the main menu frame instead (see #menu-legal in index.html).
   Desktop (fine pointer, wide or tall viewport) keeps the corner treatment. */
#menu-legal {
  display: none;
}
@media (max-width: 700px), ((max-height: 480px) and (pointer: coarse)) {
  #legal-links {
    display: none;
  }
  .menu-frame {
    display: flex;
    flex-direction: column;
    padding-bottom: 0;
  }
  .menu-panels {
    flex: 1 0 auto;
  }
  #menu-legal {
    display: flex;
    position: sticky;
    bottom: 0;
    align-items: center;
    justify-content: center;
    gap: 6px;
    /* ONE row, always. The three labels + dots are ~49 monospaced chars; at
       the old fixed 9px they needed ~440px and wrapped onto 2-3 rows on any
       phone, eating ~40px of menu height. Press Start 2P advances ~1em per
       char, so the font scales with the viewport width (1.65vw ~= 5.3px
       @320, 6.4px @390, capped at 8px from ~485px up) — one row by
       construction at any width, still legible (desktop corner links are
       7px). nowrap is safe BECAUSE the size tracks the width; if a device
       ever reports an odd width the strip shrinks rather than wraps. */
    flex-wrap: nowrap;
    white-space: nowrap;
    margin-top: auto;
    padding: 10px 8px calc(28px + env(safe-area-inset-bottom));
    background: #10180c;
    border-top: 2px solid #4a6b3c;
    font-family: "Press Start 2P", monospace;
    font-size: min(8px, 1.65vw);
    line-height: 1.6;
  }
  #menu-legal a {
    color: #7ddf64;
    text-decoration: underline;
    padding: 8px 3px;
    position: relative;
  }
  /* 44px tap targets WITHOUT growing the strip (mobile rule): the hit area
     extends past the ~6-8px font's box into the footer's own padding via an
     invisible overlay, so the row stays one thin line but every link is a
     full-height target. 46px, not 44: sub-pixel layout at some widths/DPRs
     rounds a 44px overlay down to 41-43 hit-testable pixels. */
  #menu-legal a::after {
    content: "";
    position: absolute;
    left: 0;
    right: 0;
    top: 50%;
    transform: translateY(-50%);
    height: 46px;
  }
  #menu-legal a:active {
    color: #aaff99;
  }
}

/* Delete account confirmation popup */
.delete-confirm-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(0,0,0,0.7);
  z-index: 9998;
}
.delete-confirm-box {
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  background: #1c2817;
  border: 3px solid #8b2020;
  outline: 2px solid #ff5d5d;
  box-shadow: 6px 6px 0 rgba(0,0,0,0.55);
  padding: 16px 20px;
  z-index: 9999;
  min-width: 280px;
  max-width: 90vw;
}
.delete-confirm-title {
  font-family: "Press Start 2P", monospace;
  font-size: 12px;
  color: #ff5d5d;
  text-shadow: 2px 2px 0 #000;
  margin-bottom: 10px;
  text-align: center;
}
.delete-confirm-text {
  font-family: "Press Start 2P", monospace;
  font-size: 9px;
  color: #cfcba9;
  line-height: 1.6;
  text-align: center;
  margin-bottom: 14px;
}
.delete-confirm-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  justify-content: center;
}

/* Bulk-dismantle confirm (forge card) reuses the delete-confirm chassis in
   the forge's amber palette. Scoped to #crafting-overlay so the glyph/hero
   red variants keep their own colors. */
#crafting-overlay .delete-confirm-box {
  background: #1c2817;
  border-color: #d4a03f;
  outline-color: #d4a03f;
}

#crafting-overlay .delete-confirm-title {
  color: #d4a03f;
}

/* Settings button (replaces the old note-glyph mute button) */
#btn-settings {
  background: #1c5a3a;
}

/* ---- Queue panel (inline in main menu, pixel-art style) ---- */
#coop-queue { image-rendering: pixelated; }
@keyframes queue-pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.5; }
}

/* ---- Glyph Book ---- */
/* Category tabs — the .bag-tab chrome (wrap instead of a fixed 6-column grid,
   so labels never clip; labels are painted as pixel-font canvases by
   js/glyphbook.js just like the inventory tabs). */
.glyph-cats { display: flex; flex-wrap: wrap; gap: 4px; padding: 4px 0 8px; }
.glyph-cat-btn { pointer-events: auto; cursor: pointer; font-weight: 900; letter-spacing: 1px; color: #cfcba9; background: #162110; border: 2px solid #0a0d07; box-shadow: 2px 2px 0 rgba(0, 0, 0, 0.45); border-radius: 0; padding: 7px 10px; font-size: 11px; line-height: 1; touch-action: manipulation; min-width: 0; }
.glyph-cat-btn.active { background: #ffd23f; color: #1a1200; box-shadow: 0 2px 0 #8a6a12; }
/* Toolbar row: search bar (same chassis as the inventory's) + EQUIPPED
   on/off filter. Wraps on narrow phones so neither control is squeezed. */
.glyph-toolbar { display: flex; flex-wrap: wrap; gap: 6px; align-items: stretch; padding: 2px 0 6px; }
.glyph-toolbar .search-wrap { flex: 1 1 200px; min-width: 0; }
.glyph-toolbar .craft-search { margin-bottom: 0; }
.glyph-eq-toggle { pointer-events: auto; cursor: pointer; font-weight: 900; letter-spacing: 1px; color: #cfcba9; background: #162110; border: 2px solid #0a0d07; box-shadow: 2px 2px 0 rgba(0, 0, 0, 0.45); border-radius: 0; padding: 7px 10px; font-size: 11px; line-height: 1; touch-action: manipulation; flex: 0 0 auto; min-height: 30px; }
/* Blue = the same colour the equipped card border uses, so the filter and
   the cards it shows read as one idea. */
.glyph-eq-toggle.active { background: #4aa3ff; color: #04101f; box-shadow: 0 2px 0 #17456e; }
.glyph-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 4px; flex: 1; min-height: 0; padding-bottom: 8px; align-content: start; overflow-y: auto; scrollbar-width: thin; scrollbar-color: #3a5a3a #0c0f08; }
.glyph-card { position: relative; background: #0d130a; border: 1px solid #2a3a2a; padding: 4px; text-align: center; cursor: pointer; transition: border-color 0.15s; clip-path: polygon(3px 0, calc(100% - 3px) 0, 100% 3px, 100% calc(100% - 3px), calc(100% - 3px) 100%, 3px 100%, 0 calc(100% - 3px), 0 3px); }
.glyph-card:hover { border-color: #7dfc88; }
.glyph-card.selected { border-color: #ffcc44; background: #1a2a10; }
.glyph-card.equipped { border-color: #4aa3ff; }
.glyph-card.locked { opacity: 0.35; }
.glyph-card-icon { width: 32px; height: 32px; margin: 0 auto 2px; image-rendering: pixelated; }
.glyph-card-name { font-family: "Press Start 2P", monospace; font-size: 9px; color: #cfcba9; line-height: 1.3; }
.glyph-card-tier { font-size: 5px; padding: 1px 3px; border-radius: 2px; margin-top: 2px; display: inline-block; }
/* Detail sheet sits IN the panel flow (not absolutely pinned to the panel's
   foot): as a normal flex child it can never fall outside the panel box, and
   it is the one and only scroll container for it — absolute sheets nested in
   the scrolling .panel-body scroll unreliably on mobile Safari. Its own
   overflow-y: auto scrolls the assign/slot buttons into reach; the panel's
   safe-area padding below it keeps them clear of the home indicator. */
.glyph-detail { position: relative; flex: 0 0 auto; width: 100%; max-height: 70%; background: #0c0f08; border-top: 2px solid #3a5a3a; padding: 12px; overflow-y: auto; -webkit-overflow-scrolling: touch; overscroll-behavior: contain; z-index: 10; }
.glyph-detail-name { font-family: "Press Start 2P", monospace; font-size: 14px; color: #f2eeca; margin-bottom: 4px; }
.glyph-detail-tier { font-size: 11px; padding: 2px 6px; border-radius: 3px; display: inline-block; margin-bottom: 6px; }
.glyph-detail-desc { font-family: "Press Start 2P", monospace; font-size: 10px; color: #9db18c; line-height: 1.6; margin-bottom: 8px; }
.glyph-detail-stats { font-family: "Press Start 2P", monospace; font-size: 10px; color: #88aa88; margin-bottom: 8px; }
.glyph-detail-level { font-family: "Press Start 2P", monospace; font-size: 11px; color: #ffcc44; margin-bottom: 4px; }
.glyph-detail-cost { font-family: "Press Start 2P", monospace; font-size: 10px; color: #aaaacc; margin-bottom: 8px; }
.glyph-detail-close { position: absolute; top: 4px; right: 8px; background: none; border: none; color: #ff5d5d; font-size: 16px; cursor: pointer; font-family: monospace; }
#tab-glyphs { position: relative; overflow: hidden; }
#panel-glyphs .panel-body { display: flex; flex-direction: column; padding-bottom: 0; }
.glyph-stat-row { display: flex; justify-content: space-between; padding: 2px 0; border-bottom: 1px solid rgba(0, 0, 0, 0.3); }
.glyph-stat-row span:first-child { color: #cfcba9; }
.glyph-stat-row span:last-child { color: #ffd23f; font-weight: 900; }
.glyph-equip-btn { display: block; width: 100%; padding: 6px 8px; margin-top: 4px; font-family: "Press Start 2P", monospace; font-size: 10px; color: #1a1200; background: #57d46a; border: 2px solid #0a0d07; box-shadow: 0 3px 0 #2a6a2a; cursor: pointer; text-align: center; }
.glyph-equip-btn:active { transform: translateY(2px); box-shadow: 0 0 0 #2a6a2a; }
.glyph-equip-btn.unequip { background: #b03c2b; color: #fff; box-shadow: 0 3px 0 #5a1a10; }
.glyph-equip-btn.unequip:active { box-shadow: 0 0 0 #5a1a10; }
.glyph-equip-btn.locked { background: #2c3726; color: #69765f; box-shadow: 0 3px 0 #0a0d07; cursor: not-allowed; }
.glyph-card-lvl { font-size: 7px; color: #ffcc44; margin-top: 2px; font-family: "Press Start 2P", monospace; }
.glyph-upgrade-btn {
  display: block;
  width: 100%;
  padding: 10px 12px;
  margin-top: 6px;
  font-family: "Press Start 2P", monospace;
  font-size: 11px;
  font-weight: 900;
  letter-spacing: 1px;
  text-transform: uppercase;
  text-align: center;
  color: #ffd23f;
  background: #2a5a2a;
  border: 2px solid #0a0d07;
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.25), 0 3px 0 #1a3a1a;
  clip-path: polygon(6px 0, calc(100% - 6px) 0, 100% 6px, 100% calc(100% - 6px), calc(100% - 6px) 100%, 6px 100%, 0 calc(100% - 6px), 0 6px);
  cursor: pointer;
  transition: transform 0.05s, filter 0.05s;
  filter: drop-shadow(2px 2px 0 rgba(0, 0, 0, 0.5));
}
.glyph-upgrade-btn:hover { filter: drop-shadow(2px 2px 0 rgba(0, 0, 0, 0.5)) brightness(1.12); }
.glyph-upgrade-btn:active { transform: translate(2px, 2px); filter: drop-shadow(0 0 0 rgba(0, 0, 0, 0.5)); }
.glyph-upgrade-btn:disabled { background: #1a2a1a; color: #55604d; box-shadow: 0 2px 0 #0a0d07; cursor: not-allowed; filter: none; opacity: 0.6; }
.glyph-upgrade-btn:disabled:hover { filter: none; }
.glyph-upgrade-btn:disabled:active { transform: none; }
.glyph-detail-cd { font-size: 9px; color: #aaaacc; margin-bottom: 4px; }
.glyph-sp-label { font-size: 7px; color: #88aa88; margin-left: 2px; }
.glyph-reset-all {
  pointer-events: auto;
  cursor: pointer;
  font-family: "Press Start 2P", monospace;
  font-weight: 900;
  color: #ff6a5a;
  background: #162110;
  border: 2px solid #3a1510;
  box-shadow: 2px 2px 0 rgba(0, 0, 0, 0.45);
  border-radius: 0;
  padding: 6px 8px;
  font-size: 10px;
  letter-spacing: 0.5px;
  touch-action: manipulation;
  margin-left: auto;
  margin-right: 24px;
  clip-path: polygon(3px 0, calc(100% - 3px) 0, 100% 3px, 100% calc(100% - 3px), calc(100% - 3px) 100%, 3px 100%, 0 calc(100% - 3px), 0 3px);
}
.glyph-reset-all:hover { color: #ff9090; border-color: #5a2520; }
.glyph-reset-all:active { transform: translate(2px, 2px); box-shadow: 0 0 0 rgba(0, 0, 0, 0.45); }
@media (max-width: 600px) {
  /* Phones: give the sheet the most room we can — it holds the slot assign
     buttons the player has to reach with one thumb. The grid above it keeps a
     row or two so the selected card stays in view. */
  .glyph-detail { border-top: 2px solid #3a5a3a; max-height: 78%; }
}


/* ---- menu chat tab ---------------------------------------------------------- */

.chat-chans {
  display: flex;
  gap: 6px;
  overflow-x: auto;
  padding-bottom: 6px;
  margin-bottom: 6px;
  -webkit-overflow-scrolling: touch;
  /* Same pixelart scrollbar as .chat-log / .panel-body / .menu-frame … */
  scrollbar-width: thin;
  scrollbar-color: #3f5c33 #0a0d07;
}

/* This strip scrolls horizontally, so the bar is sized with `height` (a
   vertical bar would use `width`). Colours match the vertical ones exactly. */
.chat-chans::-webkit-scrollbar {
  height: 6px;
}

.chat-chans::-webkit-scrollbar-track {
  background: #0a0d07;
}

.chat-chans::-webkit-scrollbar-thumb {
  background: #3f5c33;
  border: 1px solid #0a0d07;
}

.chat-chans::-webkit-scrollbar-thumb:hover {
  background: #4f7343;
}

.chat-chan {
  flex: 0 0 auto;
  cursor: pointer;
  pointer-events: auto;
  min-height: 38px;
  padding: 8px 12px;
  font-family: inherit;
  font-size: 10px;
  font-weight: 900;
  letter-spacing: 1px;
  text-transform: uppercase;
  color: #cfcba9;
  background: #162110;
  border: 2px solid #0a0d07;
  box-shadow: 2px 2px 0 rgba(0, 0, 0, 0.45);
}

.chat-chan.on {
  color: #0d130a;
  background: #ffd23f;
  border-color: #0a0d07;
}

.chat-log {
  height: min(42vh, 320px);
  min-height: 160px;
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  background: #0d130a;
  border: 2px solid #0a0d07;
  outline: 2px solid #4a6b3c;
  padding: 8px;
  margin-bottom: 6px;
  /* Same pixelart scrollbar as the rest of the menu (menu + HUD share this
     class, so the in-game chat scrolls identically). */
  scrollbar-width: thin;
  scrollbar-color: #3f5c33 #0a0d07;
}

.chat-log::-webkit-scrollbar {
  width: 6px;
}

.chat-log::-webkit-scrollbar-track {
  background: #0a0d07;
}

.chat-log::-webkit-scrollbar-thumb {
  background: #3f5c33;
  border: 1px solid #0a0d07;
}

.chat-log::-webkit-scrollbar-thumb:hover {
  background: #4f7343;
}

.chat-row {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  align-items: baseline;
  padding: 4px 0;
  border-bottom: 1px solid rgba(74, 107, 60, 0.25);
  font-size: 13px;
  line-height: 1.5;
}

.chat-time {
  color: #8899aa;
  font-size: 10px;
  flex: 0 0 auto;
}

.chat-name {
  color: #ffd23f;
  font-weight: 900;
  flex: 0 0 auto;
  /* Wider than before: room for an optional Supporter badge after the name.
     The nickname itself still truncates (see .chat-name-text). */
  max-width: 55%;
  white-space: nowrap;
  overflow: visible;
}

.chat-name-text {
  display: inline-block;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  vertical-align: bottom;
}

.chat-name::after {
  content: ":";
  color: #8899aa;
}

/* Equipped achievement title — rendered BEFORE the nickname (inside the same
   name flow), so it truncates together with the name instead of pushing it
   out of the row. Violet reads as "earned", never as the gold nickname. */
.chat-title {
  color: #b9a7ff;
  font-size: 10px;
  font-weight: 900;
  letter-spacing: 0.4px;
}

/* Supporter badge — shown after the nickname only for accounts that bought a
   cosmetics unlock and left the toggle on in Settings. */
.chat-badge {
  display: inline-block;
  margin-left: 5px;
  padding: 1px 5px;
  font-size: 9px;
  font-weight: 900;
  letter-spacing: 0.5px;
  line-height: 1.3;
  text-transform: uppercase;
  color: #2a1c00;
  background: linear-gradient(180deg, #ffe27a, #ffd23f);
  border: 1px solid #a8811a;
  vertical-align: 1px;
}

/* ---- clickable nickname (opens the action card) --------------------------- */
.chat-name-click {
  cursor: pointer;
  text-decoration: underline dotted rgba(255, 210, 63, 0.6) 1px;
  text-underline-offset: 3px;
}
.chat-name-click:hover,
.chat-name-click:focus-visible {
  color: #fff3c4;
  text-decoration-style: solid;
  outline: none;
}

/* ---- channel tag shown per row in the All view ---------------------------- */
.chat-chan-tag {
  flex: 0 0 auto;
  margin: 0 6px;
  padding: 1px 5px;
  font-size: 9px;
  font-weight: 900;
  letter-spacing: 0.5px;
  text-transform: uppercase;
  color: #0d130a;
  background: #88aaff;
  border: 1px solid #0a0d07;
  vertical-align: 1px;
}

/* ---- composer send-target chip + channel list -----------------------------
   The chip always names where the message will go, which is what makes an
   "All" read view safe to compose from. `.chat-input-row` itself is defined
   with the rest of the chat styles further down. */
.chat-target {
  flex: 0 0 auto;
  cursor: pointer;
  min-width: 74px;
  min-height: 40px;
  padding: 6px 8px;
  font-family: inherit;
  font-size: 10px;
  font-weight: 900;
  letter-spacing: 1px;
  text-transform: uppercase;
  color: #0d130a;
  background: #ffd23f;
  border: 2px solid #0a0d07;
  box-shadow: 0 3px 0 #0a0d07;
  touch-action: manipulation;
}
.chat-target.ro {
  background: #5b3a1a;
  color: #ffd9a8;
}
.chat-target:active {
  transform: translateY(2px);
  box-shadow: 0 1px 0 #0a0d07;
}

.chat-target-list {
  position: relative;
  display: flex;
  flex-wrap: wrap;
  gap: 5px;
  padding: 7px;
  margin-top: 6px;
  background: #10170d;
  border: 2px solid #0a0d07;
  box-shadow: 3px 3px 0 rgba(0, 0, 0, 0.5);
}
.chat-target-list.hide { display: none; }

.chat-target-item {
  cursor: pointer;
  min-height: 34px;
  padding: 7px 10px;
  font-family: inherit;
  font-size: 10px;
  font-weight: 900;
  letter-spacing: 1px;
  text-transform: uppercase;
  color: #cfcba9;
  background: #162110;
  border: 2px solid #0a0d07;
  touch-action: manipulation;
}
.chat-target-item.on {
  background: #ffd23f;
  color: #1a1200;
}
.chat-target-item:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}

/* ---- menu hero rosters: ACCOUNT / LOCAL sub-tabs (hero-split design) ----- */

.hero-subtabs {
  display: flex;
  gap: 6px;
  margin-bottom: 8px;
}

.hero-subtab {
  cursor: pointer;
  font-family: inherit;
  font-size: 11px;
  font-weight: 900;
  letter-spacing: 1px;
  padding: 10px 12px;
  min-height: 40px; /* one-thumb tap target on phones (project UX rule) */
  flex: 1 1 0;
  color: #88aa88;
  background: #162110;
  border: 2px solid #0a0d07;
  box-shadow: 0 3px 0 #0a0d07;
  touch-action: manipulation;
}

.hero-subtab.on {
  color: #0d130a;
  background: #ffd23f;
}

.hero-subtab:active {
  transform: translateY(2px);
  box-shadow: 0 1px 0 #0a0d07;
}

.hero-gate {
  padding: 10px 0 4px;
}

.hero-gate .m-info {
  margin-bottom: 8px;
}

.hero-empty {
  padding: 6px 2px;
  color: #9db18c;
}

/* ---- menu Chat tab: CHAT / FRIENDS sub-tabs -------------------------------- */

.chat-subtabs-row {
  display: flex;
  /* Small screens: let the JOIN DISCORD link wrap under the tabs instead of
     squeezing them — every control keeps its ≥44px-class tap target. */
  flex-wrap: wrap;
  gap: 6px;
  margin-bottom: 8px;
}

.chat-subtabs {
  display: flex;
  gap: 6px;
}

/* The JOIN DISCORD link (Chat tab) and the six social links above the About
   heading are styled byte-for-byte like a sub-tab (same box, same colors, same
   press feedback) but use their own classes so js/chat.js's `.chat-subtab`
   click/aria loops never treat them as tabs. */
.chat-subtab,
.chat-discord,
.social-btn {
  cursor: pointer;
  font-family: inherit;
  font-size: 11px;
  font-weight: 900;
  letter-spacing: 1px;
  padding: 8px 12px;
  color: #88aa88;
  background: #162110;
  border: 2px solid #0a0d07;
  box-shadow: 0 3px 0 #0a0d07;
  text-decoration: none;
  touch-action: manipulation;
}

.chat-subtab.on {
  color: #0d130a;
  background: #ffd23f;
}

.chat-subtab:active,
.chat-discord:active,
.social-btn:active {
  transform: translateY(2px);
  box-shadow: 0 1px 0 #0a0d07;
}

/* Monochrome brand mark: one path, painted with the button's own text color
   (currentColor) so it stays in the palette in every state. */
.chat-discord,
.social-btn {
  display: inline-flex;
  align-items: center;
  /* OWNER FIX (C3): icon + label centred as a pair inside the button — a
     no-op while the box is fit-content (it hugs the pair), but it keeps the
     pair centred the moment the button ever grows wider. */
  justify-content: center;
  gap: 6px;
}

.chat-discord-ic,
.social-ic {
  display: block;
  flex: none;
  width: 14px;
  height: 14px;
}

.chat-subtab-badge {
  display: inline-block;
  min-width: 16px;
  margin-left: 6px;
  padding: 1px 4px;
  font-size: 10px;
  color: #fff;
  background: #b03c2b;
  border: 1px solid #0a0d07;
}

.chat-subtab-badge.hide {
  display: none;
}

/* ---- friend list / requests ------------------------------------------------ */

.friends-empty {
  padding: 8px 2px;
  color: #88aa88;
  font-size: 11px;
}

.friend-label {
  margin: 6px 0 6px;
  font-size: 10px;
  letter-spacing: 1px;
  text-transform: uppercase;
  color: #88aa88;
}

.friend-row {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 7px 8px;
  margin-bottom: 6px;
  background: #162110;
  border: 2px solid #0a0d07;
  box-shadow: 2px 2px 0 rgba(0, 0, 0, 0.4);
}

.friend-row.pending {
  border-color: #6b5a2a;
}

/* FRIENDS tab: add-by-nickname row — one touch line, input + ADD button. */
.friend-add {
  display: flex;
  gap: 8px;
  align-items: stretch;
  margin-bottom: 4px;
}
.friend-add .friend-add-input {
  flex: 1 1 auto;
  width: auto;
  min-width: 0;
  margin-bottom: 0;
  min-height: 40px;
}
.friend-add .friend-add-btn {
  flex: 0 0 auto;
  padding: 8px 14px;
  min-width: 64px;
  min-height: 40px;
  margin-bottom: 0;
}

.friend-dot {
  flex: 0 0 auto;
  width: 12px;
  height: 12px;
  background: #5b6b52;
  border: 2px solid #0a0d07;
}

.friend-dot.on {
  background: #57d46a;
}

.friend-name {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  text-align: left;
  font-family: inherit;
  font-size: 12px;
  font-weight: 900;
  color: #cfcba9;
  background: none;
  border: none;
  cursor: pointer;
  padding: 4px 0;
}

.friend-name:active {
  color: #ffd23f;
}

.friend-state {
  flex: 0 0 auto;
  font-size: 9px;
  letter-spacing: 1px;
  text-transform: uppercase;
  color: #88aa88;
}

.friend-state.on {
  color: #57d46a;
}

.friend-group {
  flex: 0 0 auto;
  padding: 2px 5px;
  font-size: 8px;
  letter-spacing: 1px;
  color: #7dfc88;
  background: #1d3320;
  border: 1px solid #3f5c33;
}

.friend-actions {
  display: flex;
  gap: 6px;
  flex: 0 0 auto;
}

.friend-actions .px-btn {
  min-width: 0;
  padding: 7px 8px;
  font-size: 9px;
}

/* ---- pixel-art action card (name click) + group invite -------------------- */
.namecard-backdrop {
  position: absolute;
  inset: 0;
  background: rgba(0, 0, 0, 0.72);
}

#chat-namecard,
#group-invite,
#group-readycheck,
#conn-card {
  position: fixed;
  inset: 0;
  z-index: 95;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 16px;
}
#chat-namecard.hide,
#group-invite.hide,
#group-readycheck.hide,
#conn-card.hide { display: none; }

.namecard-box {
  position: relative;
  width: min(300px, 94vw);
  background: #10170d;
  border: 3px solid #0a0d07;
  box-shadow: 0 0 0 2px #ffd23f, 6px 6px 0 rgba(0, 0, 0, 0.6);
  padding: 14px 12px;
  text-align: center;
}
.namecard-title {
  color: #ffd23f;
  font-weight: 900;
  font-size: 14px;
  letter-spacing: 1px;
  text-transform: uppercase;
  word-break: break-word;
}
.namecard-sub {
  color: #7f8ea6;
  font-size: 10px;
  font-weight: 900;
  letter-spacing: 1px;
  text-transform: uppercase;
  margin: 4px 0 10px;
}
.namecard-text {
  color: #cfcba9;
  font-size: 12px;
  line-height: 1.5;
  margin: 6px 0 12px;
}
.namecard-actions {
  display: flex;
  flex-direction: column;
  gap: 7px;
}
.namecard-actions .px-btn {
  width: 100%;
  min-height: 42px;
}
/* U4 (§5.4): the connection card's actions are the only way out of a blocking
   overlay on a phone — 44px minimum touch target (WCAG 2.5.8 / Material). */
#conn-card .namecard-actions .px-btn {
  min-height: 44px;
}
/* Tier 4 carries ONE primary action; when the secondary button exists (idle's
   [MENU]) it stays visibly secondary. */
#conn-card .namecard-actions .px-btn.is-secondary {
  background: #1b2416;
  border-color: #0a0d07;
  color: #cfcba9;
}

/* ==========================================================================
   Refer a friend — js/referral.js
   Same chassis as the namecards (flat fill, hard black border, gold ring),
   widened + scrollable because this one carries a live pet preview, the link
   and an invite form. Two dialogs: the share window and the reward notice.
   Everything is a real, labelled control — no hover-only affordances.
   ========================================================================== */
#referral-window,
#referral-reward-modal {
  position: fixed;
  inset: 0;
  z-index: 96;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: max(16px, env(safe-area-inset-top)) 16px max(16px, env(safe-area-inset-bottom));
}
/* The reward notice always wins over an open share window / name card. */
#referral-reward-modal { z-index: 97; }
#referral-window.hide,
#referral-reward-modal.hide { display: none; }

.referral-backdrop {
  position: absolute;
  inset: 0;
  background: rgba(0, 0, 0, 0.78);
}
.referral-box {
  position: relative;
  width: min(360px, 96vw);
  max-height: 92vh;
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  background: #10170d;
  border: 3px solid #0a0d07;
  box-shadow: 0 0 0 2px #ffd23f, 6px 6px 0 rgba(0, 0, 0, 0.6);
  padding: 14px 12px 16px;
  text-align: center;
  scrollbar-width: thin;
  scrollbar-color: #3f5c33 #0a0d07;
}
/* The close button IS .overlay-x / .panel-x — same 22×22 box, same 13px glyph,
   same colors and hover, so it reads as the game's one X. The invisible
   ::after pad grows the tap target back to ~44px without changing the look. */
.referral-x {
  position: absolute;
  top: 6px;
  right: 6px;
  z-index: 5;
  width: 22px;
  height: 22px;
  padding: 0;
  cursor: pointer;
  background: #0a0d07;
  border: 2px solid #55604d;
  color: #cfcba9;
  font-family: inherit;
  font-size: 13px;
  font-weight: 900;
  line-height: 18px;
  text-align: center;
  touch-action: manipulation;
}
.referral-x:hover {
  background: #3a1a1a;
  border-color: #ff5d5d;
  color: #ff5d5d;
}
.referral-x:active {
  background: #3a1a1a;
  border-color: #ff5d5d;
  color: #ff5d5d;
}
.referral-x::after {
  content: "";
  position: absolute;
  inset: -11px;
}
/* The reward notice takes the X OUT of the corner: in normal flow, right-
   aligned above the title, so a long "Your friend joined Pixoot!" can never
   run underneath it (the corner version overlapped the text). */
#referral-reward-modal .referral-x {
  position: relative;
  top: auto;
  right: auto;
  display: block;
  margin: 0 0 6px auto;
}
.referral-center { text-align: center; margin: 14px 0; }
#referral-retry { margin-top: 8px; }

.referral-stage {
  border: 2px solid #0a0d07;
  background: #10170d;
  margin: 10px 0 6px;
}
#referral-stage {
  display: block;
  width: 100%;
  height: auto;
  image-rendering: pixelated;
}
.referral-reward {
  display: flex;
  align-items: center;
  text-align: left;
  background: #162110;
  border: 2px solid #0a0d07;
  outline: 2px solid #ffd23f;
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.06);
  padding: 8px 10px;
  margin: 10px 0 8px;
}
.referral-reward-name {
  color: #ffd23f;
  font-weight: 900;
  font-size: 13px;
  letter-spacing: 1px;
}
.referral-reward .m-info { margin-top: 4px; }
.referral-badge {
  color: #ffd23f;
  background: rgba(255, 210, 63, 0.08);
  margin-top: 6px;
}
.referral-steps {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin: 6px 0 12px;
  text-align: left;
}
.referral-step {
  display: flex;
  gap: 8px;
  color: #cfcba9;
  font-size: 11px;
  line-height: 1.45;
}
.referral-step-n {
  color: #ffd23f;
  font-weight: 900;
  flex: 0 0 auto;
  min-width: 14px;
}
.referral-thanks {
  background: #162110;
  border: 2px solid #0a0d07;
  outline: 2px solid #57d46a;
  padding: 10px;
  margin: 10px 0;
  text-align: left;
}
.referral-thanks-title {
  color: #ffd23f;
  font-weight: 900;
  font-size: 13px;
  letter-spacing: 1px;
  text-transform: uppercase;
  line-height: 1.4;
  margin-bottom: 6px;
}
/* The link / email inputs sit in .chat-input-row next to their buttons —
   same override the nickname row uses so they stretch instead of stacking. */
.referral-line {
  flex: 1 1 auto;
  width: auto;
  min-width: 0;
  min-height: 44px;
  margin-bottom: 0;
}
/* iOS zooms any input under 16px on focus — inside a fixed dialog that pushes
   SEND off screen. */
.referral-email {
  font-size: 16px;
  letter-spacing: 0;
}
.referral-msg {
  min-height: 14px;
  text-align: left;
}
.referral-msg.ok { color: #7dfc88; }
.referral-msg.err { color: #ff5d5d; }
.referral-msg.warn { color: #e0b458; }
.referral-note {
  text-align: left;
  margin-top: 6px;
}
#referral-window .px-btn,
#referral-reward-modal .px-btn { min-height: 44px; }
#referral-reward-modal .namecard-box { position: relative; }
#referral-settings-hint { margin: 2px 0 10px; }

/* ==========================================================================
   Cookie consent banner — js/consent.js (designs/cookie-consent.md §4)
   NON-modal bottom-centre card: no backdrop, no aria-modal — pointer events
   only reach the card itself, so tap-to-move gameplay stays usable behind
   it. z 94 sits above menu (40) / HUD (30) / promo (88) / mtx preview (90) /
   legal-links (45) and YIELDS to the menu-time dialogs at 95 (#chat-namecard,
   #group-invite, #group-readycheck) and the reward modal (97) — an unanswered
   banner must never swallow an invite's ACCEPT/DECLINE taps (review #3); the
   documented z ladder above still holds.
   ACCEPT ALL and REJECT ALL share .cc-btn with NO colour or size difference:
   equal prominence + one click each is a REGULATOR REQUIREMENT — never
   restyle one of them as the "primary" button. Nothing here is hover-only;
   every target is >=44px.
   ========================================================================== */
.cc-root {
  position: fixed;
  left: 50%;
  transform: translateX(-50%);
  bottom: calc(10px + env(safe-area-inset-bottom));
  width: min(460px, calc(100vw - 20px));
  /* 94, NOT 96: the menu-time dialogs (#chat-namecard, #group-invite,
     #group-readycheck) sit at 95 — an unanswered banner must never cover
     an incoming invite's ACCEPT/DECLINE on a small screen (review #3).
     Still far above #menu (40), #hud (10) and #legal-links (45). */
  z-index: 94;
  pointer-events: auto;
}
/* 701–959px desktop band: the centered card's right edge overlaps the
   bottom-right #legal-links strip (review #4) — lift the card clear of it.
   <=700px the strip is hidden by its own media query; >=960px there is no
   overlap. The lift is deterministic (strip = 7px font at bottom:6px). */
@media (min-width: 701px) and (max-width: 959px) {
  .cc-root { bottom: calc(30px + env(safe-area-inset-bottom)); }
}
/* The card belongs to the MAIN PAGE (design §4: a card over #menu). While
   the world is on screen it would sit over the canvas eating tap-to-move
   taps, so it steps away in-game and returns the moment the player exits to
   the menu — meanwhile gating stays fail-closed and a fresh visit shows it
   again if it was never answered. */
body.in-game .cc-root { display: none; }
/* FIRST-IMPRESSION FLOW exception (js/first.js, CONFIG.FIRSTIMP.consent):
   a FIRST-SESSION hero (the auto-created first-visit hero) that entered the
   world before the question was answered keeps the card on screen in-world —
   body.first-session exists only while that hero is in the world with no
   consent record, is removed on the pixoot:consent event and by every
   menu.show(). The general rule above is untouched for everyone else (one
   extra class in the selector, higher specificity — no !important needed). */
body.in-game.first-session .cc-root { display: block; }
/* Palette comes from .px-card (#1c2817 / #0a0d07 / #3f5c33 + hard shadow). */
.cc-card {
  position: relative;
  padding: 12px;
  text-align: left;
}
.cc-head {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 8px;
  padding-right: 32px; /* keep the title clear of the corner ✕ */
}
.cc-logo { width: 56px; height: 56px; image-rendering: pixelated; flex: 0 0 auto; }
.cc-title-wrap { min-width: 0; }
.cc-title {
  color: #ffd23f;
  font-size: 13px;
  font-weight: 900;
  letter-spacing: 1px;
  text-transform: uppercase;
}
.cc-sub { color: #9db18c; font-size: 10px; letter-spacing: 0.5px; margin-top: 2px; }
.cc-text { font-size: 11px; color: #f2eeca; line-height: 1.5; margin: 0 0 10px; }
.cc-btns { display: flex; gap: 8px; flex-wrap: wrap; }
.cc-btn { flex: 1 1 auto; min-height: 44px; }
/* CUSTOMIZE is not one of the two required equal-prominence choices, so it
   may read lighter — but it stays a full-size button (>=44px), never a link. */
.cc-btn-alt { background: #1e2f57; color: #8fa8dd; }
.cc-foot { margin-top: 8px; font-size: 10px; color: #9db18c; }
.cc-foot a { color: #ffd23f; }
/* ✕ CLOSE — monochrome (the red hover stays reserved for destructive
   actions), 26px box with an invisible ::after pad growing the real tap
   target past 44px, same trick as .referral-x. */
.cc-x {
  position: absolute;
  top: 6px;
  right: 6px;
  z-index: 5;
  width: 26px;
  height: 26px;
  padding: 0;
  background: #0a0d07;
  border: 2px solid #55604d;
  color: #cfcba9;
  font-family: inherit;
  font-size: 13px;
  font-weight: 900;
  line-height: 22px;
  text-align: center;
  cursor: pointer;
  touch-action: manipulation;
}
.cc-x:hover,
.cc-x:active { background: #131a0e; border-color: #55604d; color: #cfcba9; }
.cc-x::after { content: ""; position: absolute; inset: -9px; }
/* Per-category rows: the WHOLE row is the label of a real checkbox — one
   big touch target, boxes never pre-ticked (Planet49; JS fills them). */
.cc-row {
  display: flex;
  align-items: center;
  gap: 10px;
  border-top: 1px solid #3f5c33;
  padding: 8px 0;
  color: #f2eeca;
  font-size: 11px;
  cursor: pointer;
}
.cc-row-name { flex: 1 1 auto; color: #ffd23f; font-weight: 900; font-size: 11px; }
.cc-row-desc { color: #9db18c; font-weight: 400; font-size: 10px; line-height: 1.4; margin-top: 2px; }
.cc-check { width: 22px; height: 22px; flex: 0 0 auto; margin: 0; accent-color: #57d46a; cursor: pointer; }
.cc-always {
  flex: 0 0 auto;
  background: #10170d;
  border: 1px solid #3f5c33;
  color: #9db18c;
  font-size: 9px;
  letter-spacing: 1px;
  padding: 4px 6px;
}
/* Short screens: the settings layer scrolls INSIDE itself instead of
   covering half the phone (design risk B12). */
.cc-settings {
  max-height: min(50vh, 360px);
  overflow-y: auto;
  overscroll-behavior: contain;
}
/* Narrow phones: full-width card inside the insets, buttons stack
   full-width (each still >=44px). */
@media (max-width: 420px) {
  .cc-root { width: calc(100vw - 16px); }
  .cc-btn { flex-basis: 100%; }
}
/* Just-in-time Google consent notice in the auth panel — shown by
   js/menu.js googleJitConfirm() only while Google is not yet allowed. */
#auth-google-jit {
  background: #10170d;
  border: 2px solid #3f5c33;
  outline: 1px solid #0a0d07;
  padding: 8px;
}
.jit-text { font-size: 11px; color: #f2eeca; line-height: 1.45; margin-bottom: 6px; }
.jit-btns { display: flex; gap: 8px; flex-wrap: wrap; }
.jit-btn { flex: 1 1 auto; min-height: 44px; font-size: 11px; }

/* ==========================================================================
   Report a bug — js/bugreport.js
   Same window chassis as the referral dialog (flat fill, hard black border,
   gold ring, corner X) widened for a subject line and a real text area. The
   box scrolls on phones; both exits (corner X + CANCEL) are >=44px targets
   and nothing depends on hover. The honeypot is taken out of VIEW and out of
   the tab order — off-screen rather than display:none, because bots that
   skip invisible fields are exactly the ones we want to keep.
   ========================================================================== */
#bugreport-window {
  position: fixed;
  inset: 0;
  z-index: 96;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: max(16px, env(safe-area-inset-top)) 16px max(16px, env(safe-area-inset-bottom));
}
#bugreport-window.hide { display: none; }

.bugreport-box {
  position: relative;
  width: min(360px, 96vw);
  max-height: 92vh;
  /* X is hidden, not auto: the honeypot sits off to the left and would
     otherwise give this (already Y-scrolling) box a horizontal scrollbar. */
  overflow-x: hidden;
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  padding: 16px 12px 14px;
  scrollbar-width: thin;
  scrollbar-color: #3f5c33 #0a0d07;
}
.bugreport-box .m-label { margin-top: 8px; }
/* iOS zooms any input under 16px on focus; inside a fixed dialog that pushes
   SEND off screen. Same override the referral email input uses. */
.bugreport-box .auth-input,
.bugreport-box .bugreport-textarea {
  font-size: 16px;
  letter-spacing: 0;
}
.bugreport-textarea {
  display: block;
  min-height: 132px;
  line-height: 1.6;
  resize: vertical;
  overflow-wrap: anywhere;
  word-break: break-word;
}
.bugreport-count {
  text-align: right;
  color: #7f8ea6;
  font-size: 9px;
  letter-spacing: 1px;
  margin: -2px 0 6px;
}
.bugreport-count.warn { color: #e0b458; }
.bugreport-count.max { color: #ff5d5d; }
.bugreport-msg {
  min-height: 14px;
  text-align: left;
  margin-bottom: 6px;
}
.bugreport-msg.ok { color: #7dfc88; }
.bugreport-msg.err { color: #ff5d5d; }
.bugreport-note {
  text-align: left;
  margin-top: 8px;
  font-size: 10px;
}
#bugreport-window .px-btn { min-height: 44px; }
.bugreport-hp {
  position: absolute;
  left: -9999px;
  top: auto;
  width: 1px;
  height: 1px;
  overflow: hidden;
}


/* ==========================================================================
   Feedback + cosmetics promo popups — js/prompts.js
   Same chassis as the referral/bug-report windows (flat fill, hard black
   border, gold ring, corner X) at BODY level, sized for phones: max 420px,
   safe-area padding, everything >=44px and nothing hover-only. The feedback
   window sits at z 96. The weekly promo sits at z 88 — DELIBERATELY below
   .mtx-preview (z 90) so the shop's PREVIEW dialog opens above it, and above
   the menu (40) + HUD (30). Stars are whole-star toggles: gold #ffd23f when
   on, #6a6a5a outline when off, color transition, 44x44 tap targets.
   ========================================================================== */
#feedback-window,
#promo-window {
  position: fixed;
  inset: 0;
  z-index: 96;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: max(16px, env(safe-area-inset-top)) 16px max(16px, env(safe-area-inset-bottom));
}
/* Under .mtx-preview (90): PREVIEW inside the mounted shop stacks above the
   promo window itself. */
#promo-window { z-index: 88; }
#feedback-window.hide,
#promo-window.hide { display: none; }

.feedback-box,
.promo-box {
  position: relative;
  width: min(420px, 96vw);
  max-height: 92vh;
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  background: #10170d;
  border: 3px solid #0a0d07;
  box-shadow: 0 0 0 2px #ffd23f, 6px 6px 0 rgba(0, 0, 0, 0.6);
  padding: 14px 12px 16px;
  text-align: center;
  scrollbar-width: thin;
  scrollbar-color: #3f5c33 #0a0d07;
}
#feedback-window .px-btn,
#promo-window .px-btn { min-height: 44px; }
/* Disabled SUBMIT reads as disabled (no hover-only cue anywhere). */
#feedback-submit:disabled {
  cursor: not-allowed;
  filter: grayscale(0.7) brightness(0.65);
}
#feedback-submit:not(:disabled):active { transform: translateY(1px); }

/* ---- star rating: whole stars only, no pre-selection ---- */
#feedback-stars {
  display: flex;
  justify-content: center;
  gap: 4px;
  margin: 12px 0 8px;
}
#feedback-stars button {
  min-width: 44px;
  min-height: 44px;
  padding: 0;
  font-family: inherit;
  font-size: 28px;
  line-height: 40px;
  text-align: center;
  touch-action: manipulation;
  cursor: pointer;
  /* px-btn aesthetics: hard black border, squared outline, no radius. */
  background: #0a0d07;
  border: 2px solid #0a0d07;
  outline: 2px solid #6a6a5a;
  color: #6a6a5a;
  transition: color 0.12s ease, outline-color 0.12s ease, transform 0.05s;
}
#feedback-stars button.on {
  color: #ffd23f;
  outline-color: #ffd23f;
}
#feedback-stars button:focus-visible { outline-color: #ffd23f; }
#feedback-stars button:active { transform: translateY(1px); }

/* ---- optional comment (AFTER the stars) ---- */
#feedback-comment {
  display: block;
  width: 100%;
  min-height: 96px;
  margin-top: 4px;
  padding: 8px;
  font-family: inherit;
  /* 16px: iOS zooms anything smaller on focus and pushes CANCEL off screen. */
  font-size: 16px;
  letter-spacing: 0;
  line-height: 1.5;
  color: #cfcba9;
  background: #0a0d07;
  border: 2px solid #0a0d07;
  outline: 2px solid #55604d;
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.25);
  text-align: left;
  resize: vertical;
  overflow-wrap: anywhere;
  word-break: break-word;
}
#feedback-comment:focus { outline-color: #ffd23f; }
#feedback-comment::placeholder { color: #6f7a63; }
#feedback-counter {
  text-align: right;
  margin: 2px 0 6px;
}
#feedback-error {
  color: #ff5d5d;
  text-align: left;
  margin-bottom: 4px;
}
#feedback-error[hidden] { display: none; }

/* ---- weekly promo: the mounted shop scrolls inside the box ---- */
#promo-shop {
  min-height: 220px;
  max-height: 60vh;
  margin-top: 10px;
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  text-align: left;
  scrollbar-width: thin;
  scrollbar-color: #3f5c33 #0a0d07;
}


/* ==========================================================================
   Microtransactions (second row of menu tabs) — js/mtx.js
   Pixel-art styling matches the rest of the menu: flat fills, hard 2-3px
   black borders, no rounded corners, gold for "this costs money".
   Everything is sized for touch (>=40px targets) and never depends on hover.
   ========================================================================== */

.mtx-notice {
  min-height: 14px;
  margin-bottom: 8px;
}
.mtx-notice.ok { color: #7dfc88; }
.mtx-notice.err { color: #ff5d5d; }

.mtx-warn {
  border: 2px solid #ffd23f;
  background: #2c2610;
  padding: 10px;
  margin-bottom: 10px;
}
.mtx-warn-title {
  color: #ffd23f;
  font-weight: 900;
  font-size: 11px;
  letter-spacing: 1px;
  text-transform: uppercase;
  margin-bottom: 4px;
}

.mtx-pack {
  border: 2px solid #0a0d07;
  background: #162110;
  box-shadow: 3px 3px 0 #0a0d07;
  padding: 10px;
  margin-bottom: 12px;
  position: relative;
}
.mtx-pack::before {
  content: "BEST VALUE";
  position: absolute;
  top: -9px;
  right: 8px;
  background: #ffd23f;
  color: #1a1200;
  font-size: 8px;
  font-weight: 900;
  letter-spacing: 1px;
  padding: 2px 6px;
  border: 1px solid #0a0d07;
}
.mtx-pack.owned {
  border-color: #7dfc88;
}
.mtx-pack.owned::before {
  content: "OWNED";
  background: #7dfc88;
}
.mtx-pack-head { margin-bottom: 6px; }
.mtx-pack-name {
  color: #ffd23f;
  font-weight: 900;
  font-size: 14px;
  letter-spacing: 1px;
  text-transform: uppercase;
}
.mtx-pack-sub { color: #9db18c; font-size: 11px; line-height: 1.5; }

.mtx-price {
  color: #ffd23f;
  font-weight: 900;
  font-size: 20px;
  margin: 6px 0;
}
.mtx-price-note {
  color: #7f8ea6;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 1px;
  margin-left: 6px;
  text-transform: uppercase;
}

.mtx-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  align-items: center;
  margin: 6px 0;
}
.mtx-actions .px-btn { flex: 1 1 auto; min-height: 40px; }

.mtx-save {
  color: #7dfc88;
  font-size: 10px;
  font-weight: 900;
  letter-spacing: 0.5px;
  text-transform: uppercase;
}

.mtx-owned-flag {
  color: #7dfc88;
  font-size: 11px;
  font-weight: 900;
  letter-spacing: 0.5px;
  text-transform: uppercase;
  padding: 4px 0;
}

.mtx-note {
  color: #7f8ea6;
  font-size: 10px;
  letter-spacing: 0.5px;
  text-transform: uppercase;
  flex: 1 1 100%;
}

.mtx-grid {
  display: grid;
  gap: 8px;
}

.mtx-cat {
  border: 2px solid #0a0d07;
  background: #162110;
}
.mtx-cat.owned { border-color: #3f5c33; }

.mtx-cat-head {
  width: 100%;
  display: flex;
  align-items: center;
  gap: 8px;
  cursor: pointer;
  background: #1b2616;
  border: none;
  color: #cfcba9;
  font-family: inherit;
  font-weight: 900;
  font-size: 11px;
  letter-spacing: 1px;
  text-transform: uppercase;
  padding: 12px 10px;
  min-height: 44px;
  text-align: left;
}
.mtx-cat-head.open { background: #243118; color: #ffd23f; }
.mtx-cat-name { flex: 1 1 auto; }
.mtx-cat-count {
  color: #7f8ea6;
  font-size: 9px;
  font-weight: 700;
}
.mtx-cat-price {
  color: #ffd23f;
  font-size: 11px;
}
.mtx-cat.owned .mtx-cat-price { color: #7dfc88; }

.mtx-cat-actions { padding: 0 10px 6px; }

.mtx-items {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(76px, 1fr));
  gap: 6px;
  padding: 4px 10px 10px;
}

.mtx-item {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 3px;
  cursor: pointer;
  background: #10170d;
  border: 2px solid #0a0d07;
  color: #cfcba9;
  font-family: inherit;
  padding: 6px 3px;
  min-height: 84px;
  touch-action: manipulation;
}
.mtx-item:active { transform: translateY(1px); }
.mtx-item.locked { opacity: 0.72; }
.mtx-item-art {
  width: 44px;
  height: 44px;
  image-rendering: pixelated;
  display: block;
}
.mtx-item-name {
  font-size: 8px;
  font-weight: 900;
  letter-spacing: 0.3px;
  text-align: center;
  line-height: 1.2;
  max-width: 100%;
}
.mtx-item-flag {
  font-size: 7px;
  font-weight: 900;
  letter-spacing: 0.5px;
  color: #7f8ea6;
}
.mtx-item-flag:only-of-type { color: #7dfc88; }
.mtx-item:not(.locked) .mtx-item-flag { color: #7dfc88; }
.mtx-item.locked .mtx-item-flag { color: #ff8a5c; }

.mtx-foot {
  margin-top: 10px;
  font-size: 10px;
  line-height: 1.6;
}

/* ---- animated preview window ---- */

.mtx-preview {
  position: fixed;
  inset: 0;
  z-index: 90;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 16px;
}
.mtx-preview-backdrop {
  position: absolute;
  inset: 0;
  background: rgba(0, 0, 0, 0.78);
}
.mtx-preview-box {
  position: relative;
  width: min(360px, 96vw);
  max-height: 92vh;
  overflow-y: auto;
  background: #10170d;
  border: 3px solid #0a0d07;
  box-shadow: 0 0 0 2px #3f5c33, 6px 6px 0 rgba(0, 0, 0, 0.6);
  padding: 12px;
}
.mtx-preview-head {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  margin-bottom: 8px;
}
.mtx-preview-titles { flex: 1 1 auto; min-width: 0; }
.mtx-preview-name {
  color: #ffd23f;
  font-weight: 900;
  font-size: 14px;
  letter-spacing: 1px;
  text-transform: uppercase;
}
.mtx-preview-type {
  color: #7f8ea6;
  font-size: 10px;
  font-weight: 900;
  letter-spacing: 1px;
  text-transform: uppercase;
}
.mtx-preview-head .px-btn { min-width: 44px; min-height: 40px; }

.mtx-preview-stage {
  border: 2px solid #0a0d07;
  background: #10170d;
  margin-bottom: 8px;
}
#mtx-preview-canvas {
  display: block;
  width: 100%;
  height: auto;
  image-rendering: pixelated;
}

/* Index of every item in the current preview list (Supporter pack mode). */
.mtx-preview-list {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(96px, 1fr));
  gap: 4px;
  max-height: 132px;
  overflow-y: auto;
  padding: 6px;
  margin-bottom: 8px;
  background: #0b1109;
  border: 2px solid #0a0d07;
  -webkit-overflow-scrolling: touch;
}
.mtx-preview-list.hide { display: none; }

/* Pixel-art scrollbars — byte-for-byte the .menu-frame recipe (the menu
   reference): every scrollable in the preview/promo flow — the preview box,
   the Supporter-pack item index, and the in-game promo shop — must render
   the same 8px bar as the menu storefront, in both the webkit path (older
   Chrome) and the standard scrollbar-width/color path (Chrome 121+). */
.mtx-preview-box,
.mtx-preview-list,
#promo-shop,
.promo-box {
  scrollbar-width: thin;
  scrollbar-color: #3f5c33 #0a0d07;
}
.mtx-preview-box::-webkit-scrollbar,
.mtx-preview-list::-webkit-scrollbar,
#promo-shop::-webkit-scrollbar,
.promo-box::-webkit-scrollbar {
  width: 8px;
}
.mtx-preview-box::-webkit-scrollbar-track,
.mtx-preview-list::-webkit-scrollbar-track,
#promo-shop::-webkit-scrollbar-track,
.promo-box::-webkit-scrollbar-track {
  background: #0a0d07;
  border-left: 1px solid #1c2817;
}
.mtx-preview-box::-webkit-scrollbar-thumb,
.mtx-preview-list::-webkit-scrollbar-thumb,
#promo-shop::-webkit-scrollbar-thumb,
.promo-box::-webkit-scrollbar-thumb {
  background: #3f5c33;
  border: 1px solid #0a0d07;
}
.mtx-preview-box::-webkit-scrollbar-thumb:hover,
.mtx-preview-list::-webkit-scrollbar-thumb:hover,
#promo-shop::-webkit-scrollbar-thumb:hover,
.promo-box::-webkit-scrollbar-thumb:hover {
  background: #57d46a;
}

.mtx-preview-list-head {
  grid-column: 1 / -1;
  color: #ffd23f;
  font-size: 9px;
  font-weight: 900;
  letter-spacing: 1px;
  text-transform: uppercase;
  padding: 4px 2px 1px;
  border-bottom: 1px solid #1f2b18;
  margin-bottom: 1px;
}

.mtx-preview-list-item {
  cursor: pointer;
  background: #162110;
  border: 2px solid #0a0d07;
  color: #cfcba9;
  font-family: inherit;
  font-size: 9px;
  font-weight: 900;
  letter-spacing: 0.3px;
  padding: 7px 5px;
  min-height: 34px;
  text-align: left;
  line-height: 1.25;
  touch-action: manipulation;
}
.mtx-preview-list-item.on {
  background: #ffd23f;
  color: #1a1200;
  border-color: #0a0d07;
}
.mtx-preview-list-item:active { transform: translateY(1px); }
.mtx-preview-desc {
  font-size: 11px;
  line-height: 1.5;
  margin-bottom: 8px;
}
.mtx-preview-action {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  align-items: center;
  justify-content: center;
}
.mtx-preview-action .px-btn { min-height: 40px; padding-left: 14px; padding-right: 14px; }
.mtx-pv-nav {
  display: flex;
  align-items: center;
  gap: 8px;
  flex: 1 1 100%;
  justify-content: center;
}
.mtx-pv-arrow { min-width: 44px !important; min-height: 40px; font-size: 16px; }
.mtx-pv-pos {
  color: #7f8ea6;
  font-size: 10px;
  font-weight: 900;
  letter-spacing: 1px;
}

/* ---- Settings: Supporter badge toggle -------------------------------------
   The existing .m-check rules are scoped to #coop, so the settings copy gets
   its own identical treatment (same hit area, no hover dependency). */

.settings-check {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 11px;
  font-weight: 900;
  letter-spacing: 0.5px;
  text-transform: uppercase;
  cursor: pointer;
  user-select: none;
  margin: 4px 0 6px;
  color: #cfcba9;
  min-height: 40px;
}
.settings-check input {
  width: auto;
  margin: 0;
  padding: 0;
  outline: none;
  accent-color: #ffd23f;
  width: 18px;
  height: 18px;
  flex: 0 0 auto;
}

.chat-body {
  color: #e8e6cf;
  flex: 1 1 120px;
  min-width: 0;
  overflow-wrap: anywhere;
  word-break: break-word;
}

/* Shown inside the message log when chat is unavailable (logged out or the
   email is unverified) — a persistent explanation instead of a blank window. */
.chat-locked {
  display: block;
  padding: 14px 10px;
  text-align: center;
  font-family: 'Press Start 2P', monospace;
  font-size: 10px;
  line-height: 1.7;
  color: #e0b458;
  background: #1a1710;
  border: 2px dashed #5a4a20;
}

.chat-status {
  min-height: 16px;
  margin-bottom: 4px;
}

/* Just-in-time transparency notice above the chat input (GDPR Art. 13 —
   information must be given at the point of collection, not only in the
   policy page). */
.chat-notice {
  color: #7f8f70;
  font-size: 10px;
  line-height: 1.4;
  margin-top: 4px;
  margin-bottom: 6px;
}
.chat-notice a {
  color: #4ecca3;
}

.chat-input-row {
  display: flex;
  gap: 6px;
  align-items: stretch;
}

#chat-input,
#chat-hud-input {
  flex: 1 1 auto;
  min-width: 0;
  min-height: 44px;
  font-family: inherit;
  background: #0d130a;
  color: #fff;
  border: 2px solid #0a0d07;
  outline: 2px solid #4a6b3c;
  padding: 8px 10px;
  font-size: 15px;
}

#chat-input:disabled,
#chat-hud-input:disabled {
  opacity: 0.5;
}

.chat-send-btn {
  min-height: 44px;
  min-width: 74px;
}

.chat-send-btn:disabled {
  opacity: 0.5;
  cursor: default;
}

#chat-input:disabled,
#chat-hud-input:disabled {
  opacity: 0.55;
  cursor: default;
}

/* Settings → verified badge (email verification / Google sign-in) */
.verify-badge {
  display: inline-block;
  padding: 2px 6px;
  font-family: 'Press Start 2P', monospace;
  font-size: 9px;
  letter-spacing: 1px;
  text-transform: uppercase;
  border: 1px solid currentColor;
  line-height: 1.4;
  white-space: nowrap;
}

.verify-badge.ok {
  color: #57d46a;
  background: rgba(87, 212, 106, 0.08);
}

.verify-badge.bad {
  color: #e0b458;
  background: rgba(224, 180, 88, 0.08);
}

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

/* Style window: shop toggle + locked cosmetic cells */
#cosmetic-shop-btn {
  min-height: 40px;
  font-size: 10px;
  margin-bottom: 8px;
}

#cosmetic-shop {
  max-height: 100%;
  overflow-y: auto;
}

.bag-cell.locked {
  outline: 1px dashed rgba(224, 180, 88, 0.55);
  outline-offset: -1px;
}

.cos-lock {
  font-size: 8px;
  text-align: center;
  color: #e0b458;
  margin-top: 1px;
  letter-spacing: 1px;
}

.px-btn.verify-send {
  min-height: 20px;
  padding: 0 8px;
  font-size: 9px;
}

.px-btn.verify-send:disabled {
  opacity: 0.55;
  cursor: default;
}

.chat-older {
  display: block;
  width: 100%;
  cursor: pointer;
  pointer-events: auto;
  background: #162110;
  color: #7ddf64;
  border: 2px solid #0a0d07;
  font-family: inherit;
  font-size: 10px;
  font-weight: 900;
  letter-spacing: 1px;
  padding: 8px;
  margin-bottom: 6px;
  min-height: 36px;
}

/* ---- admin console (admin.html) ------------------------------------------- */

.admin-body {
  margin: 0;
  min-height: 100vh;
  background: #101508;
  color: #e8e6cf;
  font-family: inherit;
}

.admin-box {
  max-width: 340px;
  margin: 12vh auto 0;
  padding: 24px 20px;
  background: #162110;
  border: 3px solid #0a0d07;
  outline: 2px solid #4a6b3c;
  text-align: center;
}

.admin-box-title {
  font-size: 16px;
  font-weight: 900;
  letter-spacing: 3px;
  color: #ffd23f;
  margin-bottom: 4px;
}

.admin-box-sub {
  font-size: 11px;
  color: #8899aa;
  margin-bottom: 16px;
}

.admin-box input {
  width: 100%;
  box-sizing: border-box;
  min-height: 44px;
  margin-bottom: 10px;
  font-family: inherit;
  font-size: 15px;
  background: #0d130a;
  color: #fff;
  border: 2px solid #0a0d07;
  outline: 2px solid #4a6b3c;
  padding: 8px 10px;
}

.admin-head {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  align-items: center;
  padding: 12px 16px;
  background: #162110;
  border-bottom: 3px solid #0a0d07;
}

.admin-title {
  font-weight: 900;
  letter-spacing: 2px;
  color: #ffd23f;
  font-size: 13px;
}

.admin-nav {
  display: flex;
  gap: 6px;
  flex: 1 1 auto;
  overflow-x: auto;
  /* Mobile: the strip is the tab bar — give it momentum touch scrolling and
     keep it from growing the header (min-width:0 is required for a flex item
     with overflow to actually scroll instead of stretching the parent). */
  min-width: 0;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior-x: contain;
}

.admin-nav-btn {
  cursor: pointer;
  pointer-events: auto;
  /* Mobile: tab buttons must NEVER shrink inside the horizontally scrollable
     .admin-nav strip — a shrunken button wraps its label instead of the strip
     scrolling (flex items default to shrink:1 + min-content width). */
  flex: 0 0 auto;
  white-space: nowrap;
  min-height: 38px;
  padding: 7px 12px;
  font-family: inherit;
  font-size: 10px;
  font-weight: 900;
  letter-spacing: 1px;
  text-transform: uppercase;
  color: #cfcba9;
  background: #0d130a;
  border: 2px solid #0a0d07;
}

.admin-nav-btn.on {
  background: #ffd23f;
  color: #0d130a;
}

.admin-main {
  padding: 14px 16px 40px;
  max-width: 1200px;
  margin: 0 auto;
}

.admin-toolbar {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
  align-items: center;
  margin-bottom: 10px;
}

/* Sub-view switcher inside a section (Reports → Players / Technical). */
.admin-subtabs {
  display: flex;
  gap: 6px;
  flex-wrap: wrap;
  margin-bottom: 12px;
}

.admin-toolbar input,
.admin-toolbar select {
  min-height: 40px;
  font-family: inherit;
  font-size: 14px;
  background: #0d130a;
  color: #fff;
  border: 2px solid #0a0d07;
  outline: 2px solid #4a6b3c;
  padding: 6px 10px;
}

.admin-msg {
  min-height: 18px;
  font-size: 12px;
  color: #7dfc88;
  margin-bottom: 8px;
}

.admin-msg.err { color: #ff5d5d; }

.admin-table-wrap {
  overflow-x: auto;
  border: 2px solid #0a0d07;
  background: #0d130a;
}

.admin-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 12px;
  min-width: 720px;
}

.admin-table th,
.admin-table td {
  padding: 7px 8px;
  text-align: left;
  border-bottom: 1px solid rgba(74, 107, 60, 0.3);
  vertical-align: top;
}

.admin-table th {
  color: #ffd23f;
  font-size: 10px;
  letter-spacing: 1px;
  text-transform: uppercase;
  background: #162110;
  position: sticky;
  top: 0;
}

.admin-table tr.deleted td { opacity: 0.45; text-decoration: line-through; }

.admin-table td .px-btn {
  font-size: 9px;
  padding: 5px 7px;
  min-height: 30px;
  margin: 1px 2px 1px 0;
}

.admin-mono {
  font-family: ui-monospace, Menlo, Consolas, monospace;
  font-size: 11px;
  color: #8899aa;
}

/* Cosmetics unlock panel (admin Accounts -> "Unlocks") */
.admin-unlocks { margin-top: 6px; }

.admin-unlocks-now {
  font-size: 12px;
  color: #7dfc88;
  margin: 4px 0 10px;
  line-height: 1.5;
}

.admin-check {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 12px;
  color: #cfcba9;
  cursor: pointer;
  user-select: none;
  min-height: 32px;
}
.admin-check input {
  width: 16px;
  height: 16px;
  margin: 0;
  accent-color: #ffd23f;
  flex: 0 0 auto;
}

.admin-unlock-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: 2px 10px;
  margin: 6px 0 8px;
}

/* The unlock table lives inside a row cell, not the full-width browser, so it
   must not inherit the 720px min-width of the main admin tables. */
.admin-unlocks .admin-table { min-width: 460px; }

/* Spam log (admin "Spam" tab) — action badges + section title */
.admin-spam-title {
  font-weight: 700;
  letter-spacing: 1px;
  color: #ffd23f;
  font-size: 11px;
}

.admin-badge {
  display: inline-block;
  font: 700 10px/1 ui-monospace, Menlo, Consolas, monospace;
  letter-spacing: 1px;
  padding: 4px 6px;
  border-radius: 3px;
  border: 1px solid transparent;
}

.admin-badge.blocked {
  color: #ff6b6b;
  background: #2a1414;
  border-color: #5c2020;
}

.admin-badge.flagged {
  color: #f5b301;
  background: #2a2014;
  border-color: #5c4520;
}

.admin-badge.muted {
  color: #c084fc;
  background: #1f1430;
  border-color: #45205c;
}

.admin-input {
  width: 100%;
  box-sizing: border-box;
  min-height: 34px;
  font-family: inherit;
  font-size: 13px;
  background: #0d130a;
  color: #fff;
  border: 2px solid #2a3a4a;
  padding: 4px 6px;
}

.admin-json {
  width: 100%;
  box-sizing: border-box;
  min-height: 140px;
  font-family: ui-monospace, Menlo, Consolas, monospace;
  font-size: 12px;
  background: #0d130a;
  color: #e8e6cf;
  border: 2px solid #2a3a4a;
  padding: 6px;
}

.admin-sub {
  background: #131a0e;
  border: 2px solid #0a0d07;
  padding: 8px;
  margin: 6px 0 12px;
}

.admin-chat-log {
  max-height: 60vh;
  max-height: 60dvh; /* mobile: dvh tracks the real visible pane under the URL bar */
  overflow-y: auto;
}

.admin-compose {
  display: flex;
  gap: 8px;
  margin-bottom: 10px;
}

.admin-compose input { flex: 1 1 auto; min-width: 0; }

/* ---- in-game HUD chat (button above the potions) --------------------------- */

#chat-hud {
  position: fixed;
  left: 8px;
  bottom: calc(26px + env(safe-area-inset-bottom) + (150px * var(--hud)));
  width: min(94vw, 470px);
  max-height: min(58vh, 430px);
  display: flex;
  flex-direction: column;
  box-sizing: border-box;
  padding: 8px;
  background: rgba(13, 19, 10, 0.97);
  border: 3px solid #0a0d07;
  outline: 2px solid #4a6b3c;
  box-shadow: 4px 4px 0 rgba(0, 0, 0, 0.55);
  pointer-events: auto;
  z-index: 30;
}

#chat-hud.hide { display: none; }

#chat-hud .chat-chans {
  flex: 0 0 auto;
  margin-bottom: 6px;
  padding-bottom: 4px;
}

#chat-hud .chat-log {
  flex: 1 1 auto;
  height: min(34vh, 260px);
  min-height: 110px;
  max-height: none;
}

#chat-hud .chat-status {
  flex: 0 0 auto;
  color: #ffd23f;
}

#chat-hud .chat-input-row {
  flex: 0 0 auto;
}

/* Transparent in-game chat popup — the same box as #chat-hud, but see-through
   so it never hides the game, pointer-events:none so it can never swallow a
   tap, and scaled down on phones (the real toggleable window keeps its own,
   unchanged size). Hidden until a message arrives; one message keeps it up
   for 30s (POP_MS in js/chat.js). */
#chat-pop {
  position: fixed;
  left: 8px;
  bottom: calc(26px + env(safe-area-inset-bottom) + (150px * var(--hud)));
  width: min(94vw, 470px);
  max-height: min(58vh, 430px);
  box-sizing: border-box;
  padding: 8px;
  display: flex;
  flex-direction: column;
  background: rgba(13, 19, 10, 0.42);
  border: 3px solid rgba(10, 13, 7, 0.7);
  outline: 2px solid rgba(74, 107, 60, 0.55);
  box-shadow: 4px 4px 0 rgba(0, 0, 0, 0.3);
  pointer-events: none;
  z-index: 19;
  opacity: 0;
  visibility: hidden;
  transform-origin: left bottom;
  transform: scale(var(--chat-pop-scale, 1));
  transition: opacity 0.25s;
}

#chat-pop.show {
  opacity: 1;
  visibility: visible;
}

#chat-pop .chat-log {
  flex: 1 1 auto;
  height: min(34vh, 260px);
  min-height: 110px;
  max-height: none;
  margin-bottom: 0;
  background: rgba(13, 19, 10, 0.34);
  border-color: rgba(10, 13, 7, 0.6);
  outline-color: rgba(74, 107, 60, 0.4);
}

/* Phones: shrink the popup so it leaves the HUD alone. */
@media (max-width: 600px) {
  #chat-pop {
    --chat-pop-scale: 0.6;
  }
}

.chat-hud-row {
  flex: 0 0 100%;
  order: -1;
  display: flex;
  pointer-events: none;
}

#chat-hud-btn {
  pointer-events: auto;
  cursor: pointer;
  zoom: var(--hud);
  width: 52px;
  height: 52px;
  padding: 0;
  background: #1c2817;
  border: 3px solid #0a0d07;
  box-shadow: 3px 3px 0 rgba(0, 0, 0, 0.55);
  outline: 2px solid #ffd23f;
  display: flex;
  align-items: center;
  justify-content: center;
  image-rendering: pixelated;
}

#chat-hud-btn:hover { outline-color: #ffe27a; }
#chat-hud-btn:active { transform: translateY(1px); }

#chat-hud-btn.on {
  outline-color: #7dfc88;
  background: #26401d;
}

#chat-hud-gem {
  width: 32px;
  height: 32px;
  display: block;
  image-rendering: pixelated;
}

/* ---------- Crawlable landing content (SEO) ------------------------------
   Sits inside the scrollable .menu-frame as an "About Pixoot" section: real
   prose with a real <h1>/<h2> outline that players can read and crawlers can
   index without JavaScript. Left-aligned because .menu-frame is centred by
   default and long prose is unreadable when centred. */
.menu-about {
  /* Tight against the tutorial strip above so the link sits centered
     between the two section divider lines (see .menu-tut-foot). */
  margin-top: 4px;
  padding-top: 16px;
  border-top: 2px solid #2a3b22;
  text-align: left;
  font-size: 13px;
  line-height: 1.65;
  color: #c9d4bb;
}

/* Social row above the <h1>: line / buttons / line / title. The section's own
   border-top is the separator above the row and this border-bottom is the one
   under it, so the heading keeps the divider line it has always had.
   The frame is 440px wide and six buttons never fit one line, so the row
   wraps (1–3 rows by viewport) with an 8px gutter instead of squeezing. */
.about-social {
  display: flex;
  flex-wrap: wrap;
  /* OWNER FIX (C3): the six buttons were left-packed inside the wider About
     column (default flex-start) — the owner asked for CENTERED content. Each
     wrapped row now centres itself (the buttons are fit-content, so their own
     icon+label pairs are already centred inside their boxes); wrap behaviour
     is untouched. */
  justify-content: center;
  gap: 8px;
  margin: 0 0 14px;
  padding: 0 0 14px;
  border-bottom: 2px solid #2a3b22;
}

.menu-about h1 {
  font-size: 17px;
  line-height: 1.3;
  letter-spacing: 0.5px;
  color: #ffd23f;
  margin: 0 0 10px;
  text-shadow: 2px 2px 0 #000;
}

.menu-about h2 {
  font-size: 13px;
  letter-spacing: 1.5px;
  text-transform: uppercase;
  color: #57d46a;
  margin: 16px 0 6px;
  padding-bottom: 4px;
  border-bottom: 1px solid #2a3b22;
}

.menu-about p {
  margin: 0 0 8px;
}

.menu-about strong {
  color: #f2eeca;
}

.menu-about .about-links {
  list-style: none;
  margin: 0;
  padding: 0;
}

.menu-about .about-links li {
  margin: 0 0 6px;
}

.menu-about .about-links a {
  color: #7ee69a;
  text-decoration: none;
  border-bottom: 1px solid #2f5c3c;
}

.menu-about .about-links a:hover,
.menu-about .about-links a:focus {
  color: #a8f5c0;
  border-bottom-color: #57d46a;
}

/* ---------- <noscript> fallback (replaces the old one-line message) ----- */
.nojs-card {
  max-width: 640px;
  margin: auto;
  text-align: left;
  font-size: 15px;
  line-height: 1.6;
}

.nojs-card h2 {
  color: #ffd23f;
  font-size: 20px;
  margin: 0 0 12px;
}

.nojs-card p {
  color: #c9d4bb;
  margin: 0 0 12px;
}

.nojs-card a {
  color: #7ee69a;
}

.nojs-links {
  font-size: 14px;
}

/* Same double-tap-zoom guard for the menu's landing copy and any link the
   component rules above did not already cover. Low specificity on purpose —
   #game { touch-action: none } and the per-component rules outrank this. */
a,
button {
  touch-action: manipulation;
}

/* ---- Inventory search syntax hint ---------------------------------------
   Revealed only while the search box has focus, so the panel stays clean.
   Pure CSS (`:focus-within` + adjacent sibling) — no JS, no layout jump. */
.search-syntax {
  display: none;
  margin-top: 6px;
  padding: 8px 10px;
  background: #0a0d07;
  border: 1px solid #2a3b22;
  color: #9db18c;
  font-size: 11px;
  line-height: 1.7;
  text-align: left;
}
.search-wrap.bag-search:focus-within + .search-syntax {
  display: block;
}
.search-syntax div {
  margin: 0 0 3px;
}
.search-syntax div:last-child {
  margin-bottom: 0;
}
.search-syntax b {
  color: #ffd23f;
  font-weight: 700;
  letter-spacing: 1px;
  margin-right: 4px;
}
.search-syntax code {
  color: #7ee69a;
  background: #16200f;
  border: 1px solid #2a3b22;
  padding: 0 4px;
  margin-right: 3px;
  font-family: inherit;
}

/* ==========================================================================
   MENU REDESIGN — "MENU, BUT ALIVE" (designs/menu-redesign-gallery.html,
   approved). EVERY rule below is scoped to html.menu-redesign (raised by
   js/menu.js when CONFIG.MENU_REDESIGN.roster || .party is on; one lever
   localStorage pixoot_menugallery=0 zeroes all three flags in js/config.js
   and removes the class — today's menu byte-for-byte, since every legacy
   selector keeps its own rules above this block).

   1. Sticky ▶ PLAY + ⋯ action bar (roster select-then-act, gallery §3.4)
   2. .char-row becomes a HERO CARD: live preview well 112x104 (96x98 under
      400px), name + LV chip + origin badge, CLASS · WEAPON · MAP, XP bar
      quantised to 5% cells, loadout line when selected. Selection = INSET
      gold ring (outline-offset:-2px) so the list's overflow never clips it.
      The .char-skel rows mirror the SAME card geometry (min-height 116,
      well box), so the first-paint contract (.char-list.is-loading 3 rows,
      .roster-slot band, html.no-skel) keeps zero layout jump.
   3. Co-op party LINEUP stage (one canvas + DOM plates), replacing the
      text group list while the flag is on.
   4. Chrome: featured row-2 tabs (Microtransactions / Hall of Fame) and the
      minimal centered legal footer (TUTORIAL · PRIVACY · TERMS · COOKIE
      SETTINGS — the .mr-tut links ship hidden and only reveal here).
   ========================================================================== */

/* .mr-* elements ship with class "hide" or .mr-tut (display:none) in the
   legacy mode; nothing below ever matches without the root class. */
#sp-new {
  display: contents; /* legacy: the wrapper has ZERO layout effect */
}

/* ── 1 · action bar ─────────────────────────────────────────────────────── */
/* OWNER FIX (A4 + A5), gallery `.rm-bar.sticky`:
   A4 — #btn-play-hero flex:1 fills the row (label stays centered — buttons
   center their content), the ⋯ stays at its fixed 56px, no dead space.
   A5 — the bar COVERS the frame's top area when stuck:
     · no top margin (the old 8px gap let content peek above the stuck bar),
     · horizontal BLEED over the frame's 22px side padding (negative margin +
       matching padding, exactly like the gallery's calc(-1 * var(--fx))), so
       the background runs edge-to-edge and nothing peeks BESIDE it either —
       the roster scrolls cleanly UNDER the bar.
   Padding keeps the buttons on their old x/y lines at rest. */
html.menu-redesign #sp-actionbar.mr-bar {
  display: flex;
  gap: 8px;
  position: sticky;
  top: 0;
  z-index: 7;
  margin: 0 -22px;
  padding: 14px 22px 8px;
  background: #10180c;
  border-bottom: 2px solid #0a0d07;
}
html.menu-redesign #sp-actionbar.mr-bar #btn-play-hero {
  flex: 1;
}
.px-btn.sq {
  flex: 0 0 56px;
  min-width: 56px;
  font-size: 16px;
  line-height: 1;
}
.mr-more {
  position: absolute;
  /* The A5 bar bleeds 22px over the frame padding — pin the dropdown back to
     the ⋯ button's right edge (its old geometry), still opening below the bar. */
  right: 22px;
  top: calc(100% + 4px);
  z-index: 9;
  background: #1c2817;
  border: 3px solid #0a0d07;
  outline: 2px solid #3f5c33;
  padding: 6px;
  display: flex;
  flex-direction: column;
  gap: 4px;
  min-width: 170px;
}
/* OWNER FIX (E2, round 11c): while the bar is STUCK its padding-right grows
   22 → 52 (the symmetric corner-notch indent, see the .stuck rule below), but
   `right` is measured from the bar's PADDING box — without this rule the
   dropdown would open 30px right of the ⋯ button. right:52px lands the
   dropdown's right edge exactly on the stuck ⋯ button's right edge (measured
   |Δ| ≤ 0.2px at 1440/390/360), still opening below the bar and inside the
   frame. Rest state keeps the base 22px. */
html.menu-redesign #sp-actionbar.mr-bar.stuck .mr-more {
  right: 52px;
}
.mr-more .px-btn {
  font-size: 10px;
  padding: 7px 8px;
  min-height: 44px;
  /* OWNER FIX (C4): the two stacked overflow items (UPLOAD TO CLOUD /
     DELETE HERO) read left-aligned; the owner asked for CENTERED labels.
     Everything else (font-size, padding, the 44px touch law) is untouched. */
  text-align: center;
}
/* touch law: chrome relaxes to 34px ONLY on fine pointers >600px */
@media (pointer: coarse), (max-width: 600px) {
  .mr-more .px-btn {
    min-height: 44px;
  }
}

/* ── 2 · hero cards ─────────────────────────────────────────────────────── */
html.menu-redesign .char-row {
  grid-template-columns: 112px minmax(0, 1fr);
  gap: 8px;
  min-height: 116px;
  align-items: center;
}
html.menu-redesign .char-row .char-left {
  order: 2;
}
html.menu-redesign .char-row .char-portrait {
  order: 1;
  width: 112px;
  height: 104px;
}
/* Delete / upload live in the ⋯ overflow now (their click handlers are kept
   — js/menu.js's ⋯ menu dispatches a programmatic .click() on them, which
   works on display:none nodes). */
html.menu-redesign .char-row .char-del,
html.menu-redesign .char-row .char-mig {
  display: none;
}
html.menu-redesign .hc-r1 {
  display: flex;
  align-items: center;
  gap: 6px;
  flex-wrap: wrap;
  min-width: 0;
}
.hc-lv {
  font-size: 9px;
  font-weight: 900;
  letter-spacing: 1px;
  color: #ffd23f;
  background: #162110;
  border: 1px solid #3f5c33;
  padding: 1px 5px;
  white-space: nowrap;
}
html.menu-redesign .char-row .char-meta {
  display: block;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.hc-xp {
  display: block;
  height: 8px;
  background: #0a0d07;
  border: 1px solid #24301a;
  position: relative;
  overflow: hidden;
  margin-top: 2px;
}
.hc-xp i {
  display: block;
  height: 100%;
  background: #57d46a;
}
.hc-loadout {
  display: none;
  font-size: 9px;
  letter-spacing: 1px;
  color: #cfcba9;
  background: #162110;
  border: 1px solid #24301a;
  padding: 2px 4px;
  margin-top: 2px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* Never colour-only selection: gold inset ring + gold XP + the loadout line
   appearing — three signals. */
html.menu-redesign .char-row.on {
  outline-offset: -2px;
}
html.menu-redesign .char-row.sel {
  outline: 2px solid #ffd23f;
  outline-offset: -2px;
  background: #162110;
}
/* ONE selected card, EVER (owner 2026-10-28: "why is there 2 selected
   character in single player tab?"). This roster paints the SAME gold ring
   from TWO different states: `.on` = the LAST PLAYED hero (js/menu.js sync()
   → `c.active`) and `.sel` = the card the pinned ▶ PLAY actually launches
   (mrMarkSelection) — so tapping any hero that is not the last-played one
   ringed BOTH cards at once. The ring belongs to `.sel` alone: the active
   hero falls back to the plain card keyline and keeps its own signal, the ▶
   badge beside the name (sync(), `if (c.active) nm.appendChild(iconEl("play"))`).
   Scoped to #char-list on purpose: #coop-char-list uses `.on` AS its
   selection (refreshCoopList → selectCoop), so the co-op ring is untouched,
   and legacy mode (no html.menu-redesign, no `.sel` node ever) keeps the
   active hero ringed byte-for-byte. */
html.menu-redesign #char-list .char-row.on:not(.sel) {
  outline: 1px solid #3f5c33;
  outline-offset: 0;
  background: #0d130a;
}
html.menu-redesign .char-row.sel .hc-xp i {
  background: #ffd23f;
}
html.menu-redesign .char-row.sel .hc-loadout {
  display: block;
}
/* Skeleton rows mirror the SAME card geometry: the well box in column 1, the
   two bars in column 2, action cells gone — 3 rows x 116px + 2 x 6px = 360px
   reserved while loading, exactly what the list has anyway (zero CLS). */
html.menu-redesign .char-row.char-skel .char-left {
  order: 2;
}
html.menu-redesign .char-skel .skel-portrait {
  order: 1;
  width: 112px;
  height: 104px;
}
html.menu-redesign .char-skel .skel-act {
  display: none;
}
/* UX review fix 9: :not(.no-skel) — at equal specificity the later rule wins,
   and without it this overrode html.no-skel .char-list.is-loading (block 11
   of the first-paint design) so the lever stopped dropping the reserved
   360px. The lever is the master switch again. */
html.menu-redesign:not(.no-skel) .char-list.is-loading {
  min-height: 360px;
}
html.menu-redesign .char-list {
  max-height: 360px;
}
/* Narrow frame (window < 400px): the card re-lays out at a 96px well —
   js/menu.js matches the same breakpoint for the canvas' logical pixels. */
@media (max-width: 400px) {
  html.menu-redesign .char-row {
    grid-template-columns: 96px minmax(0, 1fr);
    gap: 6px;
  }
  html.menu-redesign .char-row .char-portrait,
  html.menu-redesign .char-skel .skel-portrait {
    width: 96px;
    height: 98px;
  }
  html.menu-redesign .char-name {
    font-size: 12px;
  }
}
/* Roster first: CONTINUE is retired (the active hero is pre-selected, so
   play is still one tap on the sticky PLAY bar). Elements stay in the DOM —
   every harness and the legacy mode keep seeing them. */
html.menu-redesign #btn-continue,
html.menu-redesign #continue-info {
  display: none;
}
html.menu-redesign #sp-new {
  display: none;
}
html.menu-redesign #sp-new.open {
  display: block;
}
/* Roster list moves ABOVE the create disclosure inside #sp-controls. */
html.menu-redesign #sp-roster-slot {
  order: 1;
}
html.menu-redesign #char-list {
  order: 2;
}
html.menu-redesign #btn-new-hero {
  order: 3;
}
html.menu-redesign #sp-new {
  order: 4;
}
html.menu-redesign #sp-controls {
  display: flex;
  flex-direction: column;
}
/* UX review fix 3: while the ACCOUNT login gate is up, the controls wrapper
   hides everything EXCEPT the constant-height roster status band (§3.5 — the
   band + RETRY must stay reachable, never masked by a bucket/auth flip).
   js/menu.js toggles .gated (not .hide) under the redesign; html.no-skel
   still wins on the slot itself (its rule is unscoped to this wrapper). */
html.menu-redesign #sp-controls.gated > *:not(#sp-roster-slot) {
  display: none;
}
/* UX review fix 6: the TWO new verbs carry the 44px touch floor on phones /
   coarse pointers (.px-btn's global 40px stays for every pre-existing
   button — this raises exactly #btn-play-hero and #btn-new-hero). */
@media (pointer: coarse), (max-width: 600px) {
  html.menu-redesign #btn-play-hero,
  html.menu-redesign #btn-new-hero,
  html.menu-redesign #coop-new-hero {
    min-height: 44px;
  }
}

/* ── 3 · co-op party lineup stage ───────────────────────────────────────── */
#coop-lineup canvas {
  display: block;
  width: 100%;
  height: 170px;
  background: #0f160c;
  border: 2px solid #0a0d07;
  outline: 1px solid #3f5c33;
  image-rendering: pixelated;
}
.mr-plates {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 4px;
  margin-top: 6px;
}
.mr-pm {
  background: #162110;
  border: 1px solid #24301a;
  padding: 4px 3px;
  text-align: center;
  display: flex;
  flex-direction: column;
  gap: 3px;
  min-width: 0;
}
.mr-pm .nm {
  font-size: 10px;
  font-weight: 900;
  letter-spacing: 0.5px;
  color: #f2eeca;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.mr-pm .mt {
  font-size: 9px;
  color: #9db18c;
  letter-spacing: 0.5px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.mr-pm .ld {
  font-size: 9px;
  color: #ffd23f;
  letter-spacing: 1px;
}
.mr-pm .ready {
  background: #162110;
  border: 2px solid #0a0d07;
  color: #9db18c;
  font-family: inherit;
  font-size: 9px;
  font-weight: 900;
  letter-spacing: 1px;
  min-height: 36px;
  cursor: pointer;
  padding: 3px;
}
.mr-pm .ready.on {
  background: #2c3f20;
  color: #7dfc88;
  box-shadow: inset 0 0 0 1px #57d46a;
}
/* OFFLINE presence (group roster): the read-only pill states OFFLINE instead
   of a ready verdict, dimmer than NOT READY — the plate's offline indication. */
.mr-pm .ready.off {
  background: #12180e;
  color: #7d8a74;
}
.mr-pm .ready:disabled {
  cursor: default;
}
.mr-pm .kx {
  background: none;
  border: 0;
  color: #e05e5e;
  font-family: inherit;
  font-size: 11px;
  font-weight: 900;
  letter-spacing: 1px;
  cursor: pointer;
  min-height: 26px;
}
/* PROMOTE (UX review fix 2): the redesign hides #coop-group-list, the only
   old binding of promoteMember — the leader now hands leadership from the
   plate itself, CONSTRUCTIVE FIRST (spec §4.1), same confirm + API path. */
.mr-pm .pr {
  background: none;
  border: 0;
  color: #ffd23f;
  font-family: inherit;
  font-size: 11px;
  font-weight: 900;
  letter-spacing: 1px;
  cursor: pointer;
  min-height: 26px;
}
/* An offline member cannot be promoted (server twin: 409 code:"offline") —
   the control greys out; KICK next to it stays live on purpose. */
.mr-pm .pr:disabled {
  color: #6f7a5f;
  cursor: default;
}
.mr-pm.ghost {
  border: 2px dashed #3f5c33;
  background: transparent;
  color: #7dfc88;
  justify-content: center;
  cursor: pointer;
  font-family: inherit;
  font-size: 10px;
  letter-spacing: 1px;
  min-height: 78px;
  text-align: center;
  padding: 4px 3px;
}
/* OWNER FIX (A10): invite-ghost hover parity with the gallery
   (menu-redesign-gallery.html: .pm.ghost:hover{background:#14200f}). */
.mr-pm.ghost:hover {
  background: #14200f;
}
@media (pointer: coarse), (max-width: 600px) {
  .mr-pm .ready {
    min-height: 44px;
  }
  .mr-pm .kx,
  .mr-pm .pr {
    min-height: 44px;
  }
}
/* The plates + stage replace the text list while the flag is on (the legacy
   rows stay in the DOM, driven by the exact same refreshGroupInfo code). */
html.menu-redesign #coop-group-list {
  display: none;
}

/* ── 4 · chrome — featured row-2 tabs + minimal legal footer ────────────── */
html.menu-redesign .menu-tabs.menu-tabs-row2 .menu-tab {
  background: #2a2410;
  color: #ffd23f;
  box-shadow: 0 3px 0 #0a0d07, inset 0 0 0 2px #6b5a20;
}
html.menu-redesign .menu-tabs.menu-tabs-row2 .menu-tab:hover {
  background: #3a3116;
  color: #ffe9a8;
}
html.menu-redesign .menu-tabs.menu-tabs-row2 .menu-tab.on {
  background: #3f3516;
  color: #ffe9a8;
  box-shadow: 0 3px 0 #ffd23f, inset 0 0 0 2px #ffd23f;
}
/* OWNER FIX (A1): the Microtransactions / Hall of Fame labels carry NO icon
   (js/menu.js menuPx.sweep stopped injecting coin/trophy) — center the plain
   label both axes inside the pennant so it reads middle-aligned. */
html.menu-redesign .menu-tabs.menu-tabs-row2 .menu-tab {
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
}
/* The legal footer is TUTORIAL · PRIVACY · TERMS · COOKIE SETTINGS — minimal
   and centered. The .mr-tut links ship display:none (legacy) and reveal
   here; the mobile strip scales down a notch so four labels + dots stay ONE
   nowrap row at 320px. */
.mr-tut {
  display: none;
}
html.menu-redesign .mr-tut {
  display: inline;
}
/* UX review fix 8: ONE TUTORIAL link — the minimal legal row above already
   carries it, so the legacy long-sentence foot hides while the redesign is
   on (both #tab-play and #tab-coop). Lever off = both back byte-for-byte. */
html.menu-redesign .menu-tut-foot {
  display: none;
}
/* OWNER FIX (A8) + (A5): ONE legal footer for the redesign on EVERY viewport
   — #menu-legal becomes the gallery's `.rm-foot`: centered single row
   TUTORIAL · PRIVACY · TERMS · COOKIE SETTINGS, sticky at the frame's bottom
   edge. The frame takes the gallery's `padding: 0 fx 0` recipe (checked by
   designs/menu-redesign-gallery-check.js: "frame has no top/bottom padding
   (sticky bars cover the edges)"): it loses its top/bottom padding under the
   redesign so the STICKY bars (A5 PLAY bar, A9 room block) and the footer
   cover the frame edges with no scrollport band for content to peek through
   — `.menu-title`'s new top margin keeps the rest-state 18px (flex column ⇒
   no margin collapse ⇒ same pixels at scroll 0). The mobile media block
   already runs this flex + padding-bottom:0 recipe; this extends it to the
   redesign on every viewport. The corner #legal-links strip is HIDDEN in the
   redesign so nothing peeks at the screen edge; legacy keeps both surfaces
   exactly as they are (its long labels live in index.html markup — the short
   labels are installed by js/menu.js mrWire under MR.on only). */
html.menu-redesign .menu-frame {
  display: flex;
  flex-direction: column;
  padding-top: 0;
  padding-bottom: 0;
}
/* gallery .rm-frame>*{flex-shrink:0} — the FRAME is the only scroller: its
   children keep their natural height and overflow INTO the scrollbar, never
   squeeze into it (a shrinking .menu-panels would slide the footer up). */
html.menu-redesign .menu-frame > * {
  flex-shrink: 0;
}
html.menu-redesign .menu-title {
  margin-top: 18px;
}
html.menu-redesign .menu-panels {
  flex: 1 0 auto;
}
html.menu-redesign #menu-legal {
  display: flex;
  position: sticky;
  bottom: 0;
  align-items: center;
  justify-content: center;
  gap: 6px;
  flex-wrap: nowrap;
  white-space: nowrap;
  margin-top: auto;
  /* OWNER FIX (C1, round 11c re-tight): the redesign frame already carries
     padding-bottom:0, so the old phone-only 28px base band below the links was
     a DEAD ~30px dark strip between the last link box and the frame's inner
     bottom edge. Round 11b left 10/12; the owner then asked (round 11c, "text
     a bit larger and the empty space smaller") for a TIGHTER strip with
     BIGGER type — 8/10 is the measured sweet spot: at the new font the link
     box is ~28.8px tall and the 46px ::after tap overlay reaches ~8.6px past
     it, so 8px top (over the 2px border it clears with 1.4px to spare) and
     10px bottom keep the overlay INSIDE the strip at every width (measured
     top-margin ≥1.3px, bottom-margin ≥1.3px at 360/390/1440), still houses
     the safe-area inset on notched phones, and shaves the divider→link gap
     12→10px and the link→frame-bottom gap 12→10px (strip 51.2→48.8px @1440).
     LEGACY (lever off / phone media block) keeps its 28px untouched. */
  padding: 8px 8px calc(10px + env(safe-area-inset-bottom));
  background: #10180c;
  border-top: 2px solid #4a6b3c;
  font-family: "Press Start 2P", monospace;
  /* OWNER FIX (E3, round 11c): bigger legal type. The gallery reference
     (.rm-foot/.rm-legal, designs/menu-redesign-gallery.html) is 9px flat; the
     38-glyph row (TUTORIAL · PRIVACY · TERMS · COOKIE SETTINGS) measures
     227px at 8px in the shipped font stack — vs 304px of content box at the
     360px floor (sweep in _mmtest/menu-ownerfix-c-check.js: fits with ~77px
     to spare, and ~61px even assuming a worst-case 0.6em monospace fallback
     advance) — so the cap is reachable on every supported width, while the
     vw term only shrinks it on sub-364px devices. One nowrap row is
     asserted at 360/390/1440 (scrollWidth <= clientWidth). */
  font-size: min(8px, 2.2vw);
  line-height: 1.6;
}
html.menu-redesign #menu-legal a {
  color: #7ddf64;
  text-decoration: underline;
  padding: 8px 3px;
  position: relative;
}
html.menu-redesign #menu-legal a:active {
  color: #aaff99;
}
/* 44px tap targets WITHOUT growing the strip (same invisible-overlay trick
   as the mobile block): only where taps exist. */
@media (pointer: coarse) {
  html.menu-redesign #menu-legal a::after {
    content: "";
    position: absolute;
    left: 0;
    right: 0;
    top: 50%;
    transform: translateY(-50%);
    height: 46px;
  }
}
html.menu-redesign #legal-links {
  display: none;
}

/* ═══ MENU REDESIGN — PIXEL CHROME ══════════════════════════════════════
   designs/_menu-redesign/10-pixelpass-art.md §1 (CTO ruling §1, route A):
   js/menu.js PIXOOT.menuPx swaps every chrome glyph (play ▶, overflow ⋯,
   close ✕ grey + red, account ☁, local ▣, leader ★, sound note on + muted,
   pager ◀ ▶, coin + trophy on the featured tabs) for a grid sprite baked by
   PIXOOT.art() into <img class="px-ic" alt="" aria-hidden="true"> at exactly
   rows*scale px. The ORIGINAL text never leaves the DOM: its glyph character
   is wrapped in .px-txt and retired HERE, so textContent stays byte-identical
   for every harness while the player sees only pixel art.
   Both selectors are scoped to html.menu-redesign AND the icons are only
   ever created while that class is up (js/menu.js MR.on), so
   localStorage pixoot_menugallery=0 restores today's menu with its Unicode
   glyphs and no orphaned sprite. Icons are static — reduced motion needs no
   branch — and every box is an integer pixel size, so no swap can reflow. */
html.menu-redesign .px-ic {
  display: inline-block;
  vertical-align: middle;
  image-rendering: pixelated;
  flex: none;
  max-width: none;
}
/* The glyph stays in the text (textContent contract) and only its PAINT
   is retired — the sprite sits in its place. */
html.menu-redesign .px-txt {
  display: none;
}
/* Sprite + label are ONE unbreakable box: Chrome starts a new line the moment
   the pair stops fitting, which would float the coin above its own label. */
html.menu-redesign .px-mark {
  white-space: nowrap;
}
/* .px-r / .px-l are only added when real text follows / precedes the sprite:
   an icon-only host must keep a ZERO margin, or the sound button's fixed
   28px box would resize between its two pre-baked notes (tab-row shift). */
html.menu-redesign .px-ic.px-r {
  margin-right: 4px;
}
html.menu-redesign .px-ic.px-l {
  margin-left: 4px;
}
html.menu-redesign #btn-play-hero .px-ic {
  margin-right: 6px;
}
/* OWNER FIX (A6): no pixel grid for the origin badge either (see the removed
   .char-origin .ico rule) — ACCOUNT / LOCAL ship as the coloured word only. */
/* The muted/open note sprite is 14px tall where the Unicode note's line box
   was 11px: pin the button to the sprite's OWN height from the first frame,
   so the tab row is already its final height when arm() swaps the glyph in
   (zero shift), and mute keeps one box (min-width:28px already covers the
   10px vs 14px WIDTH). Lever off = today's box, untouched. */
html.menu-redesign .menu-sound-btn {
  min-height: 36px;
}
/* OWNER FIX (C5): the note sprite is 18px wide where the button's CONTENT box
   is only 12px — min-width:28px is border-box (28 − 4 border − 12 padding)
   and the tab row flex-shrinks the button from its 34px natural width (the
   .menu-tab items sit at basis 0, so they hand all the negative space to this
   one). As an inline-block the img STARTS at the line start and overflows to
   the RIGHT (measured 1440x900: left gap 8px / right gap 2px off the button
   box = the sprite 3px off centre), and in the stretched phone row (47.5px
   tall) the line box stayed pinned to the top padding instead of centring.
   Flex centreing puts the ICON dead-centre on BOTH axes in BOTH states:
   · horizontal — justify-content:center centres the 18px item on the 12px
     content box (3px bleed each side ⇒ 5px/5px inside the 28px button),
   · vertical — align-items:center rides the row-stretched height.
   ZERO size change: the container's max-content is still 18 + 16 = 34px, so
   the tab row lays out byte-identically (icon-contract boot encodes untouched
   — no new bake), and a pre-arm Unicode glyph still centres as the anonymous
   flex item. Legacy (no html.menu-redesign) keeps .menu-sound-btn as a plain
   block with text-align:center — untouched. */
html.menu-redesign .menu-sound-btn {
  display: flex;
  align-items: center;
  justify-content: center;
}
/* Row-2 labels at phone widths: 11px + 0.5-1px tracking puts
   "Microtransactions" at 121px, which already fills its content box — adding
   the coin/trophy would push the pair past the pennant's diagonal and clip
   the label's tail (and, without .px-mark, break the sprite onto its own
   line). 10px / no tracking = 100px + a 14px mark = 114px inside the 119px
   box at 360px — one line, fully inside the banner. Desktop (>600px) keeps
   the shipped 11px/1px — its box is ~187px wide. */
@media (max-width: 600px) {
  html.menu-redesign .menu-tabs.menu-tabs-row2 .menu-tab {
    font-size: 10px;
    letter-spacing: 0;
    line-height: 16px;
  }
}
/* …and the ROW height is pinned: a 14px sprite inside a 12-14px line box
   grows the line by ~2px when arm() swaps it in, which would yank the whole
   strip down by 2px a second after load. min-height:36px is the sprite-era
   box in BOTH states (plain glyph -> sprite), so the swap moves nothing;
   today's box is 34px, +2px under this class only. */
html.menu-redesign .menu-tabs.menu-tabs-row2 .menu-tab {
  min-height: 36px;
}
/* ── OWNER FIX — CO-OP panel order (designs/menu-redesign-gallery.html §4.0):
      GROUP preview → ROOM CODE → CREATE/JOIN → PUBLIC ROOMS → bucket + hero
      selector LAST. Implemented with flex `order` instead of moving markup, so
      the LEGACY path (kill switch off) keeps today's DOM order byte-for-byte
      and every id/listener is untouched. Direct children only; anything added
      later lands last (order 20) until it is placed on purpose. */
html.menu-redesign #coop {
  display: flex;
  flex-direction: column;
}
html.menu-redesign #coop > * { order: 20; }
html.menu-redesign #coop > #coop-lineup             { order: 1; }
html.menu-redesign #coop > #btn-ready-check         { order: 2; }
html.menu-redesign #coop > #btn-group-rejoin        { order: 3; }
html.menu-redesign #coop > #btn-group-leave         { order: 4; }
html.menu-redesign #coop > #coop-group-list         { order: 5; }
/* Fallback ONLY: if the .mr-coop-sticky wrapper could not be built (stale
   markup), the three loose nodes keep their old slot; with the wrapper in
   place they are grandchildren and these never match. */
html.menu-redesign #coop > label[for="coop-code"]   { order: 6; }
html.menu-redesign #coop > #coop-code               { order: 7; }
html.menu-redesign #coop > .coop-row                { order: 8; }
html.menu-redesign #coop > #coop-group-info         { order: 9; }
html.menu-redesign #coop > #coop-status             { order: 10; }
html.menu-redesign #coop > #coop-queue              { order: 11; }
html.menu-redesign #coop > label.m-check[for="coop-public"] { order: 12; }
html.menu-redesign #coop > #coop-rooms-label        { order: 13; }
html.menu-redesign #coop > #public-rooms            { order: 14; }
html.menu-redesign #coop > #coop-hero-subtabs       { order: 15; }
html.menu-redesign #coop > #coop-account-gate       { order: 16; }
html.menu-redesign #coop > #coop-account-empty      { order: 17; }
html.menu-redesign #coop > #coop-local-empty        { order: 18; }
html.menu-redesign #coop > #coop-roster             { order: 19; }
/* ── OWNER FIX (A9) — ROOM CODE + CREATE/JOIN pin to the top while the
      co-op panel scrolls (gallery `.rm-sticky-top` §4.0: "pin to the top
      while the panel scrolls, exactly like the PLAY bar does in Single
      player"). js/menu.js mrCoopReorder() wraps the THREE existing nodes
      (label + #coop-code + .coop-row — ids and listeners untouched, it is a
      pure move) into this div under the MR.party flag ONLY, so the legacy
      DOM never sees it. The wrapper takes the old direct-child slot (order 6,
      where label/code/row were 6/7/8 — same visual sequence), bleeds the
      frame's 22px side padding like the A5 PLAY bar and carries an opaque
      background + border-bottom so the lineup stage scrolls cleanly UNDER
      it. `.m-label` loses its top margin inside the block (gallery:
      .rm-sticky-top .rm-label{margin-top:0}). */
html.menu-redesign #coop > .mr-coop-sticky {
  order: 6;
  position: sticky;
  top: 0;
  z-index: 7;
  margin: 0 -22px;
  padding: 10px 22px 8px;
  background: #10180c;
  border-bottom: 2px solid #0a0d07;
}
html.menu-redesign .mr-coop-sticky .m-label {
  margin-top: 0;
}
/* ── OWNER FIX (C2) — STUCK state: the frame's artistic clip-path
      polygon(12% 0, 88% 0, 100% 10%, …) cuts a diagonal across its TOP
      corners, and while a full-bleed band is PINNED at the scrollport's top
      its content sits inside that cut. Measured (clip edge x = 0.12W·(1 −
      y/0.1H), label text top y≈14):
        1440x900 (W440 H882) edge 44.4px vs content x 26 → "ROOM CODE" lost
          its first 18px and hit-tested to the page ("OM CODE"),
        1440x600 (W440 H588) edge 40.2 → −14.2, 390x844 (W358 H827) 35.7 →
          −9.7, 360x640 (W328 H627) 30.6 → −4.6,
        the PLAY bar's #btn-play-hero box (x 26, top y 18) had its left
          10–16px strip shaved the same way (its LABEL cleared by 91px).
      One fixed 30px indent clears EVERY one of those with ≥11px to spare
      (content x 56 vs the tallest measured edge 44.4), and it is PADDING, so
      the background still bleeds edge-to-edge (margin would break the A5/A9
      band checks). js/menu.js mrStuckWatch() toggles .stuck only while the
      band is really pinned — at rest the class is gone and today's layout is
      byte-identical. Specificity mirrors each base rule (both carry an id). */
html.menu-redesign #coop > .mr-coop-sticky.stuck,
html.menu-redesign #sp-actionbar.mr-bar.stuck {
  padding-left: 52px;   /* 22px bleed padding + 30px clear of the LEFT diagonal */
  /* ── OWNER FIX (E1 + E2, round 11c) — SYMMETRIC indent. Round 11b only
        indented the LEFT, leaving the stuck content box [52, W−22]:
          · E1 — the ROOM CODE input + CREATE/JOIN row sat 15–20px right of
            the frame's centre, and the input's RIGHT edge ran INTO the
            top-right corner notch (measured input right-clearance −1.4px
            @390x844 — the owner's "not centered" / "extends past the
            buttons" impression),
          · E2 — the play bar's ⋯ button (56px, right-aligned in the same
            box) was clipped by the mirror diagonal: top-right corner
            clearance −6px @1440x900 / −7.6 @390 / −2.1 @360 (4 corner/edge
            hit-tests fell through to the page).
        52/52 makes the content box [52, W−52] — the group centre IS the
        frame's content-area centre (measured |Δcentre| ≤ 0.2px after the
        fix; label / input / .coop-row keep identical left AND right edges
        since all three span the one content box) — while both diagonals
        keep ≥21px clearance at every tested size and the label keeps x=56
        (≥11.6px clear of the left diagonal, still "well-placed"). At rest
        the class is gone → today's 22/22 layout, byte-identical. */
  padding-right: 52px;
}
