/* ===================== NEONFANG — global styles ===================== */
:root {
  --bg: #0a0a14;
  --panel: rgba(14, 16, 30, 0.92);
  --panel-edge: #2a2f52;
  --ink: #e8ecff;
  --muted: #8a93c8;
  --accent: #7df9ff;
  --accent-2: #ff5cf0;
  --gold: #ffd15c;
  --hp: #ff4d6d;
  --xp: #7df9ff;
  --danger: #ff3b6b;
  --font: 'Courier New', ui-monospace, monospace;
}

* { box-sizing: border-box; margin: 0; padding: 0; }

html, body {
  width: 100%; height: 100%;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--font);
  overflow: hidden;
  overscroll-behavior: none;
  -webkit-user-select: none; user-select: none;
  touch-action: none;
}

#game-root { position: fixed; inset: 0; }

canvas#game {
  display: block;
  width: 100%; height: 100%;
  image-rendering: pixelated;
  image-rendering: crisp-edges;
  background: #06060d;
}

.hidden { display: none !important; }

/* ===================== HUD ===================== */
#hud {
  position: absolute; inset: 0;
  pointer-events: none;
  padding: calc(12px + env(safe-area-inset-top, 0px))
           calc(56px + env(safe-area-inset-right, 0px))
           12px
           calc(12px + env(safe-area-inset-left, 0px));
  display: flex; flex-direction: column;
  font-size: 14px;
  text-shadow: 0 2px 0 rgba(0,0,0,0.6);
}
/* The top row wraps rather than starves. `#stage-name` grows with its text
   ("Dawnfield · 01:25 left"), and as a flex sibling of a `flex: 1` bar it used
   to eat the XP bar down to a 62px stub on a 390px phone — the run's primary
   progression readout, unreadable. The XP bar now holds a hard 160px floor and
   the place/countdown line drops to its own full-width line when the three
   items no longer fit. No breakpoint: the wrap happens exactly when it must. */
#hud-top { display: flex; align-items: center; flex-wrap: wrap; column-gap: 12px; row-gap: 2px; }
#xp-bar {
  position: relative; flex: 1 1 160px; min-width: 160px; height: 20px;
  background: #101430; border: 2px solid var(--panel-edge);
  border-radius: 4px; overflow: hidden;
}
#xp-fill {
  position: relative;
  height: 100%; width: 0%;
  background: linear-gradient(90deg, #2b6cff, var(--xp));
  transition: width 0.12s linear;
}
/* Fang tip on the leading edge of HUD fill bars — the brand bites forward. */
#xp-fill::after, #hp-fill::after, #boss-bar #boss-fill::after {
  content: ''; position: absolute; right: -7px; top: 0;
  width: 8px; height: 100%;
  background: inherit;
  clip-path: polygon(0 0, 100% 50%, 0 100%);
}
#xp-label {
  position: absolute; inset: 0; display: flex; align-items: center;
  justify-content: center; font-weight: bold; letter-spacing: 1px;
  color: #fff; font-size: 12px; padding: 0 4px; white-space: nowrap;
}
#timer { font-size: 22px; font-weight: bold; letter-spacing: 2px; min-width: 78px; text-align: center; flex: 0 0 auto; }
/* `flex: 0 1 auto` — it must never grow, or it takes the width back off the XP
   bar on desktop; it only needs to be allowed to fall to the next line. */
#stage-name { color: var(--accent); font-size: 12px; flex: 0 1 auto; min-width: 110px; text-align: right; opacity: 0.9; }

/* Same story one row down: the counters are wider than the gap left beside the
   HP bar on a phone, and each `icon value` pair was breaking mid-span so the
   numbers stacked under their glyphs. Pairs never break; the whole cluster
   drops to its own line when it can't sit beside the HP bar. */
#hud-stats { display: flex; align-items: center; flex-wrap: wrap; column-gap: 12px; row-gap: 4px; margin-top: 8px; }
#hp-bar {
  position: relative; width: 260px; max-width: 45vw; height: 18px;
  background: #2a0e18; border: 2px solid #55203a; border-radius: 4px; overflow: hidden;
}
#hp-fill { position: relative; height: 100%; width: 100%; background: linear-gradient(90deg, #ff2d55, #ff6f91); transition: width 0.15s linear; }
#hp-bar.low { border-color: var(--danger); animation: hpPulse 0.9s ease-in-out infinite; }
@keyframes hpPulse {
  0%, 100% { box-shadow: 0 0 0 rgba(255,45,85,0); }
  50% { box-shadow: 0 0 16px rgba(255,45,85,0.65); }
}
#hp-label {
  position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
  font-size: 11px; font-weight: bold; color: #fff;
}
/* The low-HP alarm was carried by the border turning --danger plus a pulse —
   both of which a red-blind player loses (#ff3b6b simulates to a dull khaki,
   and the pulse is off under reduced motion). The glyph is what survives. */
#hp-bar.low #hp-label::before { content: '⚠'; margin-right: 5px; }
#hud-counters {
  display: flex; flex-wrap: wrap; column-gap: 14px; row-gap: 2px;
  flex: 0 1 auto; min-width: 190px; font-size: 13px;
}
#hud-counters > span { white-space: nowrap; }
#gold { color: var(--gold); }
#element-badge { color: var(--accent); }

/* Weapons and passives are two slot rows sitting side by side, which on a
   390px phone ran 486px wide — three slots entirely off-screen and a fourth
   clipped, so the player could not see their own build. They now wrap: the
   passive row drops beneath the weapon row exactly when the pair no longer
   fits, and desktop/landscape keep the single-line layout untouched.
   `row-gap: 0` because each `.slot-row` already carries its own margin-top. */
#weapon-row { display: flex; flex-wrap: wrap; column-gap: 6px; row-gap: 0; margin-top: 8px; }
.weapon-chip {
  display: flex; align-items: center; gap: 4px;
  background: var(--panel); border: 1px solid var(--panel-edge);
  border-radius: 6px; padding: 3px 6px; font-size: 11px;
}
.weapon-chip img { width: 20px; height: 20px; image-rendering: pixelated; }
.weapon-chip .lvl { color: var(--gold); font-weight: bold; }
.weapon-chip.passive { opacity: 0.92; }

/* VS-style slot rows. Wrapping inside a row too, so a deep ultraweapon haul
   (the slot count is uncapped) still can't push a slot past the edge. */
.slot-row { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 4px; }
.slot {
  position: relative; width: 30px; height: 30px;
  display: flex; align-items: center; justify-content: center;
  background: var(--panel); border: 1px solid var(--panel-edge); border-radius: 6px;
}
.slot .slot-icon { font-size: 16px; line-height: 1; }
.slot .lvl { position: absolute; right: 1px; bottom: 0; font-size: 9px; color: var(--gold); font-weight: bold; }
/* Multiplication reads as a multiplier, not a level — "×3.05" needs the full
   width of the slot, so it sits as a strip across the bottom instead of a
   corner badge. */
.slot .lvl.mult {
  left: 0; right: 0; bottom: -1px; text-align: center;
  font-size: 8px; letter-spacing: -0.2px;
  background: rgba(12,14,28,0.78); border-radius: 0 0 5px 5px;
}
.slot.empty { background: rgba(20,22,40,0.5); border-style: dashed; opacity: 0.5; }
.slot.passive { border-color: #3a3f66; }
.slot.tag-multiply { border-color: #b98cff; }

#boss-bar {
  position: absolute; left: 50%; transform: translateX(-50%);
  bottom: 18px; width: min(560px, 80vw); height: 22px;
  background: #1a0620; border: 2px solid #6a1b6a; border-radius: 4px; overflow: hidden;
  animation: bossIn 0.45s cubic-bezier(0.18, 0.89, 0.32, 1.3) backwards;
}
@keyframes bossIn { from { opacity: 0; transform: translateX(-50%) translateY(26px) scaleX(0.6); } }
#boss-bar #boss-fill { position:absolute; inset:0; width: 100%; background: linear-gradient(90deg, #ff2d9b, #ff8a3d); transition: width 0.1s linear; }
#boss-bar #boss-name {
  position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
  z-index: 2; font-weight: bold; letter-spacing: 2px; font-size: 12px; color: #fff;
}

/* ===================== Overlays / menus ===================== */
#overlay {
  position: absolute; inset: 0;
  display: none; align-items: center; justify-content: center;
  background: rgba(4, 5, 12, 0.82);
  backdrop-filter: blur(3px);
  z-index: 10;
}
#overlay.show { display: flex; animation: overlayIn 0.2s ease-out; }
@keyframes overlayIn { from { background: rgba(4, 5, 12, 0); } }
/* Menus with the animated key-art vista behind them stay mostly clear. */
#overlay.scenic { background: rgba(4, 5, 12, 0.22); backdrop-filter: none; }
#overlay.scenic .panel { background: rgba(10, 12, 24, 0.84); }

.panel {
  background: var(--panel);
  border: 2px solid var(--panel-edge);
  border-radius: 14px;
  padding: 26px 30px;
  max-width: min(940px, 94vw);
  max-height: 92vh; overflow-y: auto;
  box-shadow: 0 0 60px rgba(125,249,255,0.08), inset 0 0 0 1px rgba(255,255,255,0.03);
  animation: panelIn 0.3s cubic-bezier(0.18, 0.89, 0.32, 1.18) backwards;
}
@keyframes panelIn { from { opacity: 0; transform: translateY(16px) scale(0.96); } }

/* ---- Pinned-footer panels ----
   The middle region scrolls; headings and the primary action row stay put.
   Used by the level-up screen and the world map, where a row added to the
   panel has repeatedly pushed the primary button below the fold. */
.pin-panel { display: flex; flex-direction: column; overflow: hidden; }
.pin-panel > * { flex: 0 0 auto; }
.pin-panel > .pin-scroll {
  flex: 0 1 auto; min-height: 48px;
  overflow-y: auto; padding: 8px 6px 10px;
}

/* ---- Scroll affordance ----
   Any internally-scrolling region gets a bottom fade while content is cut off
   below (the .has-more class is toggled from screens.js). Without it, panels
   have shipped with primary buttons silently below the fold and nothing
   signalling the panel scrolls. */
.panel::after, .pin-scroll::after, .ach-list::after, .md-scroll::after {
  content: ''; display: block; flex: 0 0 auto;
  position: sticky; bottom: -1px; height: 38px; margin-top: -38px;
  background: linear-gradient(to bottom, rgba(10,12,24,0), rgba(7,9,18,0.94));
  opacity: 0; transition: opacity 0.25s; pointer-events: none;
}
.panel.has-more::after, .pin-scroll.has-more::after, .ach-list.has-more::after, .md-scroll.has-more::after { opacity: 1; }
.title {
  font-size: 46px; font-weight: 900; letter-spacing: 4px; text-align: center;
  background: linear-gradient(90deg, var(--accent), var(--accent-2));
  -webkit-background-clip: text; background-clip: text; color: transparent;
  margin-bottom: 4px;
  filter: drop-shadow(0 0 18px rgba(125,249,255,0.3));
  animation: titleGlow 2.8s ease-in-out infinite;
}
@keyframes titleGlow {
  0%, 100% { filter: drop-shadow(0 0 14px rgba(125,249,255,0.25)); }
  50% { filter: drop-shadow(0 0 28px rgba(255,92,240,0.45)); }
}
.subtitle { text-align: center; color: var(--muted); margin-bottom: 20px; letter-spacing: 2px; font-size: 13px; }

/* NEONFANG pixel wordmark — cyan NEON, magenta FANG, neon-sign flicker. */
.title-wordmark {
  display: block; margin: 2px auto 6px;
  width: min(460px, 84vw); height: auto;
  image-rendering: pixelated; image-rendering: crisp-edges;
  filter: drop-shadow(0 0 10px rgba(125,249,255,0.45)) drop-shadow(0 0 26px rgba(255,92,240,0.35));
  animation: neonFlicker 5.2s linear infinite;
}
@keyframes neonFlicker {
  0%, 100% { opacity: 1; }
  41% { opacity: 1; } 42% { opacity: 0.82; } 43% { opacity: 1; }
  44% { opacity: 0.9; } 45% { opacity: 1; }
  71% { opacity: 1; } 72% { opacity: 0.9; } 73% { opacity: 1; }
}

/* Fang-tooth divider — the brand bite, used under headings. */
.fang-divider {
  height: 7px; width: min(280px, 62%); margin: 0 auto 12px;
  background:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='28' height='7'%3E%3Cpath d='M1 0h12L7 6z' fill='%237df9ff' fill-opacity='0.5'/%3E%3Cpath d='M15 0h12L21 6z' fill='%23ff5cf0' fill-opacity='0.5'/%3E%3C/svg%3E")
    repeat-x center top;
}

/* Title-screen wolf emblem — pixel key art with a breathing neon glow. */
.title-emblem {
  display: block; margin: 2px auto 8px;
  width: 132px; height: auto;
  image-rendering: pixelated; image-rendering: crisp-edges;
  filter: drop-shadow(0 0 14px rgba(125,249,255,0.5)) drop-shadow(0 0 30px rgba(255,92,240,0.35));
  animation: emblemFloat 3.4s ease-in-out infinite;
}
@keyframes emblemFloat {
  0%, 100% { transform: translateY(0); filter: drop-shadow(0 0 14px rgba(125,249,255,0.5)) drop-shadow(0 0 30px rgba(255,92,240,0.3)); }
  50% { transform: translateY(-7px); filter: drop-shadow(0 0 20px rgba(125,249,255,0.65)) drop-shadow(0 0 42px rgba(255,92,240,0.5)); }
}
.section-title { font-size: 20px; letter-spacing: 2px; margin: 6px 0 14px; color: var(--accent); text-align:center; }
/* Every section heading carries the fang-tooth brand divider. */
.section-title::after {
  content: ''; display: block;
  height: 7px; width: min(220px, 55%); margin: 9px auto 0;
  background:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='28' height='7'%3E%3Cpath d='M1 0h12L7 6z' fill='%237df9ff' fill-opacity='0.5'/%3E%3Cpath d='M15 0h12L21 6z' fill='%23ff5cf0' fill-opacity='0.5'/%3E%3C/svg%3E")
    repeat-x center top;
}

.btn {
  pointer-events: auto; cursor: pointer;
  position: relative; overflow: hidden;
  background: linear-gradient(180deg, #1a2050, #12163a);
  border: 2px solid var(--panel-edge); color: var(--ink);
  padding: 12px 22px; border-radius: 10px; font-family: var(--font);
  font-size: 16px; font-weight: bold; letter-spacing: 1px;
  transition: transform 0.08s, border-color 0.12s, box-shadow 0.12s;
}
.btn::after {
  content: ''; position: absolute; top: 0; left: -80%; width: 55%; height: 100%;
  background: linear-gradient(105deg, transparent, rgba(255,255,255,0.15), transparent);
  transform: skewX(-20deg); pointer-events: none; transition: left 0.45s ease-out;
}
.btn:hover { transform: translateY(-2px); border-color: var(--accent); box-shadow: 0 6px 24px rgba(125,249,255,0.2); }
.btn:hover::after { left: 125%; }
.btn:active { transform: translateY(0) scale(0.98); }
.btn:focus-visible, .card:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.gp-focus { outline: 2px solid var(--accent) !important; outline-offset: 2px; border-color: var(--accent) !important; }
.btn.primary { background: linear-gradient(180deg, #2b6cff, #1746c8); border-color: var(--accent); }
.btn.ghost { background: transparent; }
.btn-row { display: flex; gap: 12px; justify-content: center; flex-wrap: wrap; margin-top: 18px; }

/* Card grid (level-up + character select) */
.card-grid { display: flex; gap: 16px; justify-content: center; flex-wrap: wrap; }
.card {
  pointer-events: auto; cursor: pointer;
  position: relative;
  width: 190px; min-height: 230px;
  background: linear-gradient(180deg, #141a3e, #0e1230);
  border: 2px solid var(--panel-edge); border-radius: 12px;
  padding: 16px; display: flex; flex-direction: column; align-items: center; text-align: center;
  transition: transform 0.1s, border-color 0.12s, box-shadow 0.12s;
  animation: cardIn 0.34s cubic-bezier(0.18, 0.89, 0.32, 1.28) backwards;
}
@keyframes cardIn { from { opacity: 0; transform: translateY(24px) scale(0.92); } }
.card-grid .card:nth-child(2) { animation-delay: 0.06s; }
.card-grid .card:nth-child(3) { animation-delay: 0.12s; }
.card-grid .card:nth-child(4) { animation-delay: 0.18s; }
.card-grid .card:nth-child(5) { animation-delay: 0.24s; }
.card-grid .card:nth-child(6) { animation-delay: 0.30s; }
/* Number-key hint on level-up cards */
.card .card-key {
  position: absolute; top: 6px; left: 6px;
  width: 20px; height: 20px; border-radius: 5px;
  background: #1a2050; border: 1px solid var(--panel-edge);
  font-size: 11px; font-weight: bold; color: var(--muted);
  display: flex; align-items: center; justify-content: center;
}
.card:hover { transform: translateY(-6px) scale(1.02); border-color: var(--accent); box-shadow: 0 10px 30px rgba(125,249,255,0.22); }
/* Locked characters read as locked through the desaturated art and the 🔒 cost
   line — not by fading the copy into the background. At the old 0.5 the card's
   description sat at 2.6:1; the art carries the dimming now instead. */
.card.locked { opacity: 0.85; filter: grayscale(0.7); cursor: not-allowed; }
.card.locked .card-icon { opacity: 0.5; }
.card.locked:hover { transform: none; border-color: var(--panel-edge); box-shadow: none; }
.card .card-icon { width: 72px; height: 72px; image-rendering: pixelated; margin: 6px 0 10px; }
.card .card-icon-glyph { font-size: 44px; line-height: 1; margin: 8px 0 6px; }
.card .card-body { display: flex; flex-direction: column; align-items: center; flex: 1; min-width: 0; }
.card .card-body .card-desc { flex: 1; }
.card .card-name { font-size: 16px; font-weight: bold; letter-spacing: 1px; }
.card .card-tag { font-size: 10px; letter-spacing: 2px; margin-top: 2px; text-transform: uppercase; }
.card .card-desc { font-size: 12px; color: var(--muted); margin-top: 10px; line-height: 1.5; flex: 1; }
.card .card-lvl { font-size: 11px; color: var(--gold); margin-top: 8px; font-weight: bold; }

.tag-weapon { color: var(--accent); }
.tag-passive { color: var(--gold); }
.tag-element { color: var(--accent-2); }
.tag-evolution { color: #ff8a3d; }
.tag-multiply { color: #b98cff; }
.tag-ascend { color: #fff6d6; text-shadow: 0 0 7px rgba(255, 209, 92, 0.85); }

.rarity-common { border-color: #2a2f52; }
.rarity-rare { border-color: #3a8cff; box-shadow: 0 0 18px rgba(58,140,255,0.18); }
.rarity-epic { border-color: #b98cff; box-shadow: 0 0 22px rgba(185,140,255,0.24); animation: cardIn 0.34s cubic-bezier(0.18,0.89,0.32,1.28) backwards, epicPulse 1.8s ease-in-out 0.4s infinite; }
/* Rarity pips — the rank is a COUNT, not a hue. The rare (#3a8cff) and epic
   (#b98cff) frames simulate to the same blue under protanopia (ΔE 8), and
   epic's extra glow is an animation, so it is gone under reduced motion too.
   One / two / three diamonds is what a colour-blind player actually reads. */
.rarity-common::before, .rarity-rare::before, .rarity-epic::before {
  position: absolute; top: 7px; right: 8px;
  font-size: 9px; letter-spacing: 1px; line-height: 1;
  color: var(--muted); pointer-events: none;
}
.rarity-common::before { content: '◆'; }
.rarity-rare::before { content: '◆◆'; color: #7fb2ff; }
.rarity-epic::before { content: '◆◆◆'; color: #cbb0ff; }
@keyframes epicPulse {
  0%, 100% { box-shadow: 0 0 22px rgba(185,140,255,0.24); }
  50% { box-shadow: 0 0 36px rgba(185,140,255,0.5); }
}

/* Chest reveal */
.chest-pop { animation: chestPop 0.55s cubic-bezier(0.18, 0.89, 0.32, 1.4) backwards; }
@keyframes chestPop {
  0% { opacity: 0; transform: scale(0.2) rotate(-14deg); }
  60% { transform: scale(1.18) rotate(4deg); }
  100% { opacity: 1; transform: scale(1) rotate(0); }
}

.stat-line { display: flex; justify-content: space-between; width: 100%; font-size: 12px; color: var(--muted); padding: 2px 0; }
.stat-line b { color: var(--ink); }

.summary-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px 24px; margin: 14px 0; font-size: 14px; }
.summary-grid .k { color: var(--muted); }
.summary-grid .v { text-align: right; font-weight: bold; }

/* Per-weapon damage bars (pause / game-over) */
.dmg-list { display: flex; flex-direction: column; gap: 4px; max-width: 380px; margin: 4px auto 12px; }
.dmg-row { display: flex; align-items: center; gap: 8px; font-size: 12px; }
.dmg-row .dmg-icon { width: 20px; text-align: center; font-size: 14px; }
.dmg-row .dmg-name { width: 118px; text-align: left; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: var(--ink); }
.dmg-row .dmg-bar { flex: 1; height: 8px; background: #101430; border: 1px solid var(--panel-edge); border-radius: 4px; overflow: hidden; }
.dmg-row .dmg-bar > div { height: 100%; background: linear-gradient(90deg, #2b6cff, var(--accent)); }
.dmg-row:first-child .dmg-bar > div { background: linear-gradient(90deg, #c9902e, var(--gold)); }
.dmg-row .dmg-val { width: 52px; text-align: right; color: var(--muted); font-weight: bold; }

/* Collapsible character stats on the pause panel */
.pause-stats { margin: 4px auto 6px; }
.pause-stats summary { cursor: pointer; color: var(--muted); font-size: 12px; letter-spacing: 1px; }
.pause-stats summary:hover { color: var(--accent); }

.hint { text-align:center; color: var(--muted); font-size: 12px; margin-top: 14px; line-height: 1.6; }

/* ===================== The Fang Tree (meta skill tree) ===================== */
/* The whole constellation must be selectable WITHOUT scrolling. The tree's
   panel is viewport-sized, its scroll region is a flex column, and the tree
   flexes to whatever height the occasional Ascension/support rows leave —
   with the nodes sized in container-query units so they scale down with the
   canvas instead of clipping. (Fixed 52px nodes used to overlap badly on
   phones and push the bottom rank behind the detail card, which forced a
   scroll on the screen's core interaction.) */
.tree-panel { height: min(92vh, 740px); }
.tree-panel > .pin-scroll { flex: 1 1 auto; min-height: 140px; display: flex; flex-direction: column; }
.tree-wrap {
  position: relative;
  flex: 1 1 auto; min-height: 140px;
  width: min(640px, 100%);
  align-self: center;
  container-type: size;
  margin: 6px 0;
  background:
    radial-gradient(circle at 50% 10%, rgba(255,209,92,0.06), transparent 40%),
    radial-gradient(circle at 16% 80%, rgba(255,92,240,0.05), transparent 45%),
    radial-gradient(circle at 84% 80%, rgba(255,209,92,0.04), transparent 45%),
    #0c0f24;
  border: 1px solid var(--panel-edge); border-radius: 12px;
  overflow: hidden;
}
.tree-edges { position: absolute; inset: 0; width: 100%; height: 100%; }
.tree-edges .edge { stroke: #252a4e; stroke-width: 2; }
.tree-edges .edge.lit.eb-fang { stroke: rgba(255,92,240,0.75); }
.tree-edges .edge.lit.eb-hide { stroke: rgba(125,249,255,0.75); }
.tree-edges .edge.lit.eb-fortune { stroke: rgba(255,209,92,0.75); }
.tree-edges .edge.lit.eb-apex { stroke: rgba(255,255,255,0.8); }
.skill-node {
  position: absolute; transform: translate(-50%, -50%);
  /* cqmin: nodes track the smaller tree dimension, so a squeezed canvas gets
     proportionally smaller circles instead of overlapping ones. */
  width: clamp(28px, 12.5cqmin, 52px); height: clamp(28px, 12.5cqmin, 52px);
  border-radius: 50%;
  background: #131737; border: 2px solid var(--panel-edge);
  color: var(--ink); cursor: pointer; pointer-events: auto;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  font-family: var(--font); padding: 0;
  transition: transform 0.1s, box-shadow 0.12s, border-color 0.12s;
}
.skill-node .sn-icon { font-size: clamp(11px, 4.6cqmin, 18px); line-height: 1.1; }
.skill-node .sn-lvl { font-size: clamp(7px, 2.3cqmin, 9px); font-weight: bold; color: var(--muted); }
.skill-node:hover { transform: translate(-50%, -50%) scale(1.12); }
.skill-node.selected { outline: 2px solid var(--ink); outline-offset: 2px; }
.skill-node.locked { opacity: 0.45; filter: grayscale(0.6); }
.skill-node.b-fang { border-color: #a1418f; }
.skill-node.b-hide { border-color: #3a7f96; }
.skill-node.b-fortune { border-color: #a8893a; }
.skill-node.avail.b-fang { border-color: var(--accent-2); box-shadow: 0 0 14px rgba(255,92,240,0.35); }
.skill-node.avail.b-hide { border-color: var(--accent); box-shadow: 0 0 14px rgba(125,249,255,0.35); }
.skill-node.avail.b-fortune { border-color: var(--gold); box-shadow: 0 0 14px rgba(255,209,92,0.35); }
.skill-node.maxed { background: #1a2050; }
.skill-node.maxed .sn-lvl { color: var(--gold); }
.skill-node.keystone { width: clamp(34px, 14.5cqmin, 60px); height: clamp(34px, 14.5cqmin, 60px); border-width: 3px; }
.skill-node.apex {
  width: clamp(38px, 16.5cqmin, 68px); height: clamp(38px, 16.5cqmin, 68px);
  border-width: 3px; border-color: #fff;
  background: linear-gradient(180deg, #1a2050, #131737);
}
.skill-node.apex.avail { box-shadow: 0 0 22px rgba(255,255,255,0.4); animation: epicPulse 1.8s ease-in-out infinite; }
.skill-node.apex.maxed { border-color: var(--gold); box-shadow: 0 0 26px rgba(255,209,92,0.5); }
/* The tree panel is a .pin-panel (same contract as the world map): the
   constellation and the Ascension/support rows scroll, while the detail card
   and the BUY MAX / Back row stay on screen at every viewport. If space runs
   out the scroll region yields first, then the detail card. */
.pin-panel > #skill-detail { flex: 0 1 auto; overflow-y: auto; }
#skill-detail {
  min-height: 88px; width: min(640px, 100%); margin: 0 auto;
  background: #101430; border: 1px solid var(--panel-edge); border-radius: 10px;
  padding: 10px 14px; display: flex; flex-direction: column; gap: 6px;
  align-items: center; justify-content: center; text-align: center;
}
#skill-detail .sd-head { display: flex; align-items: center; gap: 8px; font-size: 15px; }
#skill-detail .sd-icon { font-size: 20px; }
#skill-detail .sd-lvl { color: var(--gold); font-size: 12px; font-weight: bold; }
#skill-detail .sd-desc { color: var(--muted); font-size: 12px; }
#skill-detail .sd-reqs { font-size: 11px; }
#skill-detail .btn { padding: 8px 18px; font-size: 13px; }
/* `width: min(640px, 88vw)` was measured against the viewport, not the panel
   it sits in, so on a 390px phone the row ran 25px past the panel's content
   box and clipped the ASCEND button's right edge. Sized to its container
   instead, and allowed to wrap so the primary action drops to its own centred
   line rather than being squeezed out of frame. */
.ascend-row {
  display: flex; align-items: center; justify-content: center;
  flex-wrap: wrap; gap: 12px;
  width: 100%; max-width: 640px; margin: 10px auto 0;
  background: linear-gradient(90deg, rgba(255,209,92,0.10), rgba(255,92,240,0.10));
  border: 1px solid var(--gold); border-radius: 10px; padding: 10px 14px;
}
/* The ☄ was inline in the 12px body copy and inherited --ink: a ~6px
   monochrome comet on a dark panel, effectively invisible, leaving the gold
   "ASCENSION READY" text to carry the whole signal. Promoted to a sized gold
   badge with the same glow the `.tag-ascend` prestige marker already uses, so
   the row's meaning is carried by the glyph *and* the label, not either alone.
   Decorative — the text says everything, so it's aria-hidden in the markup. */
.ascend-row .ascend-glyph {
  flex: 0 0 auto; align-self: flex-start; font-size: 28px; line-height: 1;
  color: var(--gold); text-shadow: 0 0 10px rgba(255, 209, 92, 0.85);
}
.ascend-row .ascend-info { flex: 1 1 190px; text-align: left; font-size: 12px; color: var(--ink); line-height: 1.5; }
.ascend-row #btn-ascend { flex: 0 0 auto; }
.ascend-row .ascend-info b { color: var(--gold); }
.slot.ultra { border-color: var(--gold); box-shadow: 0 0 8px rgba(255,209,92,0.35); }

/* ============ THE FANGLANDS — hex world map ============ */
.map-wrap {
  position: relative;
  width: min(720px, 90vw);
  /* The vh term keeps the panel's buttons on screen on short/landscape
     displays, with room for the reclamation banner above it. */
  height: min(430px, 62vw, 31vh);
  margin: 8px auto 6px;
  background:
    radial-gradient(circle at 12% 50%, rgba(125,255,156,0.06), transparent 34%),
    radial-gradient(circle at 44% 16%, rgba(140,180,255,0.06), transparent 32%),
    radial-gradient(circle at 44% 84%, rgba(255,154,92,0.06), transparent 32%),
    radial-gradient(circle at 84% 50%, rgba(195,156,255,0.07), transparent 34%),
    #0a0d1f;
  border: 1px solid var(--panel-edge); border-radius: 12px;
  overflow: hidden;
}
.hex-map { position: absolute; inset: 0; width: 100%; height: 100%; }

.hx-link { stroke: #1d2244; stroke-width: 2; }
.hx-link.open { stroke: #39406f; }
.hx-link.lit { stroke: rgba(125,249,255,0.5); stroke-width: 3; }

/* Each hex is a group of layers: .hx-base (the state fill/stroke), .hx-terrain
   (the region's texture signature), the siege hatch, then badges. State rules
   target polygon.hx-base only, so the overlay layers never inherit a state
   fill. Region tint comes from --rc. */
.hexnode { cursor: pointer; pointer-events: auto; }
.hexnode polygon.hx-base {
  fill: #11142c; stroke: #262c52; stroke-width: 2;
  transition: fill 0.12s, stroke 0.12s;
}
.hexnode .hx-icon {
  font-family: var(--font); font-size: 8px; font-weight: bold;
  text-anchor: middle; fill: var(--muted); pointer-events: none;
  user-select: none;
}
.hexnode.rg-meadow { --rc: #7dff9c; }
.hexnode.rg-cavern { --rc: #8cb4ff; }
.hexnode.rg-wastes { --rc: #ff9a5c; }
.hexnode.rg-void   { --rc: #a06bff; }

/* Region terrain signatures (grass / crystal facets / embers / starfield) —
   the texture is what keeps the four regions apart when their tints collapse
   under deuteranopia (Cavern's blue and the Void's violet in particular).
   Opacity scales with ownership: a whisper on unreached land, full voice on
   land you hold. Shape colours mirror the region tints above — keep in step
   with src/content/worldmap.js. */
.hexnode polygon.hx-terrain { stroke: none; pointer-events: none; opacity: 0.3; transition: opacity 0.12s; }
.hex-map .pt-meadow { stroke: #7dff9c; fill: none; stroke-width: 0.45; stroke-linecap: round; }
.hex-map .pt-cavern { stroke: #8cb4ff; fill: none; stroke-width: 0.4; }
.hex-map .pt-wastes-f { fill: #ff9a5c; }
.hex-map .pt-ember-f { fill: var(--gold); }
.hex-map .pt-void-f { fill: #c8b4ff; }
.hex-map .pt-void { stroke: #c8b4ff; fill: none; stroke-width: 0.3; }

/* Unreached land keeps only a whisper of its region colour — enough to read the
   shape of the world you're going to take, not enough to look conquerable. */
.hexnode.st-locked polygon.hx-base {
  fill: color-mix(in srgb, var(--rc) 5%, #0c0f22);
  stroke: color-mix(in srgb, var(--rc) 14%, #1a1e3c);
}
.hexnode.st-locked .hx-icon { fill: #525a92; }
.hexnode.st-locked polygon.hx-terrain { opacity: 0.08; }
.hexnode.st-gated polygon.hx-base { fill: #14172f; stroke: #3b4170; stroke-dasharray: 4 3; }
.hexnode.st-gated .hx-icon { fill: #6b73a6; }
.hexnode.st-gated polygon.hx-terrain { opacity: 0.14; }
.hexnode.st-available polygon.hx-base {
  fill: color-mix(in srgb, var(--rc) 14%, #11142c);
  stroke: var(--rc);
}
.hexnode.st-available .hx-icon { fill: var(--rc); }
/* Land you hold looks claimed: a solid saturated fill, a bright rim, and the
   terrain at full voice — conquered vs available reads at arm's length. */
.hexnode.st-conquered polygon.hx-base {
  fill: color-mix(in srgb, var(--rc) 40%, #0e1128);
  stroke: color-mix(in srgb, var(--rc) 75%, #ffffff); stroke-width: 2.5;
}
.hexnode.st-conquered .hx-icon { fill: #eaf0ff; }
.hexnode.st-conquered polygon.hx-terrain { opacity: 0.6; }
/* Ground the horde has taken back — loud, and impossible to mistake for yours.
   Red is NOT enough on its own: --danger and the warm region tints both
   simulate to the same khaki, so a held Wastes hex and a contested one land
   ΔE 8.8 apart under deuteranopia. The siege hatch is the signifier that
   survives — a texture no other hex state has — backed by an oversized ⚠. */
.hexnode.st-contested polygon.hx-base {
  fill: color-mix(in srgb, var(--danger) 30%, #1a0a16);
  stroke: var(--danger); stroke-width: 3.5;
}
.hexnode polygon.hx-hatch { fill: url(#hx-siege); stroke: none; pointer-events: none; }
.hexnode.st-contested polygon.hx-terrain { opacity: 0.12; }
.hexnode.st-contested .hx-icon { fill: #ffe7ec; font-size: 11px; }
@media (prefers-reduced-motion: no-preference) {
  .hexnode.st-contested polygon.hx-base { animation: hexSiege 1.3s ease-in-out infinite; }
}
@keyframes hexSiege { 50% { fill: color-mix(in srgb, var(--danger) 58%, #1a0a16); } }
/* Open frontier beckons: a slow breathing fill, one beat gentler than the
   contested alarm. Motion is a redundant cue — the state already reads from
   fill + stroke — so reduced-motion loses nothing but the shimmer. */
@media (prefers-reduced-motion: no-preference) {
  .hexnode.st-available polygon.hx-base { animation: hexBeckon 2.6s ease-in-out infinite; }
}
@keyframes hexBeckon { 50% { fill: color-mix(in srgb, var(--rc) 27%, #11142c); } }
/* The prizes read louder than the plain hexes. */
.hexnode.st-available.ty-lair polygon.hx-base,
.hexnode.st-available.ty-treasure polygon.hx-base { stroke-width: 3.5; }
.hexnode.st-available.ty-lair .hx-icon { fill: var(--gold); font-size: 10px; }
.hexnode:not(.st-locked):hover polygon.hx-base { fill: color-mix(in srgb, var(--rc) 40%, #11142c); }
.hexnode.sel polygon.hx-base { stroke: var(--ink); stroke-width: 3.5; }
.hexnode.gp-focus polygon.hx-base { stroke: var(--accent) !important; stroke-width: 4 !important; }
@media (prefers-reduced-motion: no-preference) {
  .hexnode.st-available.ty-lair polygon.hx-base { animation: hexPulse 2.2s ease-in-out infinite; }
}
@keyframes hexPulse { 50% { stroke: var(--gold); } }

/* The map panel packs a lot of rows; keep its footer tight so the buttons
   stay on screen on short displays. The panel is a .pin-panel: the map itself
   scrolls while the detail card (with MARCH/RECLAIM) and Blessings/Back stay
   pinned below it — those buttons have shipped clipped more than once. */
.map-panel { padding: 20px 26px; }
.map-panel .btn-row { margin-top: 10px; }
.map-panel .section-title { margin-bottom: 10px; }
.map-panel > .pin-scroll { padding-top: 0; overflow-x: hidden; }
/* The 90vw widths above are for the panel at full size; inside the pinned
   layout they must never overflow the scroller on narrow phones. */
.map-siege, .map-wrap, .map-hunts, .map-panel #map-detail { max-width: 100%; }
/* If space runs out, the map region yields first (shrink 4) and the detail
   card second — and the march button stays pinned inside the detail card so
   the primary action is never the thing that gets cut. But the map is the
   screen: it never yields below a workable viewport. On a 390×844 phone the
   siege + gauntlet banners and a tall pinned detail card once crushed it to a
   ~20px slit — the floor here guarantees ~200px of visible map under compact
   banners, and the overflow moves into the detail card, whose MARCH button is
   sticky and so never leaves the screen. */
.map-panel > .pin-scroll { flex: 0 4 auto; min-height: clamp(180px, 42vh, 380px); }
/* Roomy desktop viewports fit the whole map outright — the banners plus every
   hex row — so hold the scroller open far enough that nothing is cropped. */
@media (min-width: 700px) and (min-height: 700px) {
  /* 42vh, not 47vh: the extra map height came straight out of the detail
     card's allocation, and with an anomaly + threat row the sticky MARCH
     button then floated over the threat stepper until scrolled. */
  .map-panel > .pin-scroll { min-height: min(42vh, 430px); }
}
/* The detail card's CONTENT scrolls in .md-scroll; the march button is a
   flex sibling below it, so it stays on screen structurally instead of
   sticky-floating over the threat stepper / boon rows when the card is full. */
.pin-panel > #map-detail { flex: 0 1 auto; overflow: hidden; }
#map-detail .md-scroll {
  overflow-y: auto; min-height: 0; align-self: stretch;
  display: flex; flex-direction: column; gap: 6px; align-items: center;
  justify-content: safe center;
}
#map-detail .btn[id='btn-march'] { flex-shrink: 0; }

.map-legend {
  display: flex; flex-wrap: wrap; gap: 6px 16px; justify-content: center;
  margin: 6px auto 8px; font-size: 11px; color: var(--muted);
}
.map-legend .mp-region { display: inline-flex; align-items: center; gap: 6px; }
/* The swatch carries a per-region glyph as well as the region tint: Cavern and
   the Void sit close enough in hue that a colour-only key is weak under
   deuteranopia, so the glyph is what makes the key legible. The Void's tint
   (#a06bff, was #c39cff) is also pushed off Cavern's blue — at #c39cff the two
   regions simulated to the same colour (ΔE 0.4). Keep this value in step with
   the region def in src/content/worldmap.js. */
.map-legend .mp-region i {
  width: 13px; height: 13px; font-style: normal; font-size: 11px; line-height: 13px;
  text-align: center; color: var(--rc); opacity: 0.95;
}
.map-legend .mp-region b { color: var(--ink); }
.map-legend .mp-region.done { color: var(--gold); }
.map-legend .mp-region.done b { color: var(--gold); }
.map-legend .mp-region b.mp-warn { color: var(--danger); }

/* The reclamation banner above the map. The hazard hatch is load-bearing:
   without it this alarm banner and the gold WEEKLY GAUNTLET reward banner
   directly below it both simulate to the same khaki-bordered box. */
.map-siege {
  width: min(720px, 90vw); margin: 6px auto 0; padding: 6px 14px;
  border: 1px solid var(--danger); border-radius: 9px;
  background:
    repeating-linear-gradient(135deg, rgba(255,59,107,0.16) 0 7px, rgba(255,59,107,0) 7px 14px),
    linear-gradient(90deg, rgba(255,59,107,0.14), rgba(255,59,107,0.05));
  font-size: 12px; line-height: 1.5; color: var(--ink); text-align: center;
}
.map-siege b { color: var(--danger); }
.map-siege.calm {
  border-color: var(--panel-edge); background: rgba(255,255,255,0.02);
  color: var(--muted);
}
.map-siege.calm b { color: var(--accent); }

#map-detail {
  min-height: 88px; width: min(720px, 90vw); margin: 0 auto;
  background: #101430; border: 1px solid var(--panel-edge); border-radius: 10px;
  padding: 10px 16px; display: flex; flex-direction: column; gap: 6px;
  /* `safe` matters: when the card overflows and scrolls, plain `center` clips
     the head row (the hex's name) above the scrollable area — unreachable. */
  align-items: center; justify-content: safe center; text-align: center;
}
#map-detail .md-head { display: flex; align-items: center; gap: 9px; flex-wrap: wrap; justify-content: center; font-size: 16px; }
#map-detail .md-icon { font-size: 21px; }
#map-detail .md-type { font-size: 11px; letter-spacing: 1px; color: var(--accent); }
#map-detail .md-region { font-size: 11px; }
#map-detail .md-held { font-size: 10px; font-weight: bold; color: var(--gold); border: 1px solid var(--gold); border-radius: 4px; padding: 1px 5px; }
#map-detail .md-desc { color: var(--muted); font-size: 12px; line-height: 1.5; max-width: 520px; }
#map-detail .md-stats { display: flex; flex-wrap: wrap; gap: 4px 18px; justify-content: center; font-size: 12px; color: var(--muted); }
#map-detail .md-stats b { color: var(--ink); }
#map-detail .md-stats .md-bounty b { color: var(--gold); }
#map-detail .md-boon { font-size: 12px; color: #7dff9c; }
#map-detail .md-boon.dark { color: var(--muted); }
#map-detail .md-boon.dark b { color: var(--danger); }
/* Same hatch as the contested hex and the siege banner — one "this ground is
   under siege" texture. Without it this badge, the gold ✓ HELD badge and the
   gold ◬ ANOMALY badge are three identical khaki pills under deuteranopia. */
#map-detail .md-lost {
  font-size: 10px; font-weight: bold; color: var(--danger);
  border: 1px solid var(--danger); border-radius: 4px; padding: 1px 5px;
  background: repeating-linear-gradient(135deg, rgba(255,59,107,0.30) 0 4px, rgba(255,59,107,0) 4px 8px);
}
/* What the hex lends you for the run: a starting weapon + a stat buff. */
#map-detail .md-loadout {
  font-size: 12px; color: var(--muted);
  background: rgba(125,249,255,0.05); border: 1px solid #23305c;
  border-radius: 8px; padding: 5px 12px;
}
#map-detail .md-loadout b { color: var(--accent); }
#map-detail .md-mastery {
  font-size: 10px; font-weight: bold; color: var(--danger);
  background: rgba(255,59,107,0.14); border-radius: 4px; padding: 1px 6px;
}
/* Threat picker — the endgame difficulty dial. */
#map-detail .md-threat {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap; justify-content: center;
  background: rgba(255,59,107,0.06); border: 1px solid #46203a;
  border-radius: 8px; padding: 4px 12px;
}
#map-detail .md-threat .tbtn {
  padding: 2px 10px; font-size: 13px; line-height: 1.3; border-radius: 6px;
}
#map-detail .md-threat .tbtn:disabled { opacity: 0.62; }
#map-detail .md-threat-lbl { font-size: 12px; letter-spacing: 1px; color: var(--muted); min-width: 104px; }
#map-detail .md-threat-lbl b { color: var(--danger); font-size: 15px; }
#map-detail .md-threat-note { font-size: 11px; color: var(--muted); }
#map-detail .btn { padding: 9px 22px; font-size: 14px; }
/* Mastery numeral under a hex's glyph. */
.hexnode .hx-threat {
  font-family: var(--font); font-size: 5.5px; font-weight: bold;
  text-anchor: middle; fill: var(--danger); pointer-events: none; user-select: none;
}
.hexnode.st-contested .hx-threat { fill: #ffd7e0; }
/* Daily anomaly badge — a gold ring (the map's only circle, so the shape
   carries the meaning, not just the colour) around a haloed ◬. The ◬ alone
   was invisible at map scale; the ring is what you spot from across the room.
   The pulse is decorative — under reduced motion both sit at full opacity
   and the anomaly stays just as findable. */
.hexnode .hx-anom {
  font-family: var(--font); font-size: 6.4px; font-weight: bold;
  text-anchor: middle; fill: var(--gold); pointer-events: none; user-select: none;
  paint-order: stroke; stroke: #0a0d1f; stroke-width: 1.1px;
  animation: anomPulse 1.6s ease-in-out infinite;
}
.hexnode .hx-anom-ring {
  fill: none; stroke: var(--gold); stroke-width: 1.6; stroke-opacity: 0.9;
  pointer-events: none;
  animation: anomRing 1.6s ease-in-out infinite;
}
@keyframes anomPulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.45; } }
@keyframes anomRing { 0%, 100% { stroke-opacity: 0.9; } 50% { stroke-opacity: 0.3; } }
@media (prefers-reduced-motion: reduce) {
  .hexnode .hx-anom, .hexnode .hx-anom-ring { animation: none; }
}
/* Anomaly legend line under the map. */
.map-anomaly {
  width: min(720px, 90vw); margin: 2px auto 0;
  font-size: 12px; color: var(--muted); text-align: center;
}
.map-anomaly b { color: var(--gold); }
/* THE HUNT BOARD — the next three achievement bounties, below the map with
   the legend/anomaly context rows. Quiet panel-edge framing on purpose: the
   red siege and gold gauntlet banners above the map are alarms, this is a
   noticeboard. Rows keep to one line each (the goal text ellipsizes) so the
   strip stays naturally small and never crowds the pinned detail card. */
.map-hunts {
  width: min(720px, 90vw); margin: 6px auto 0; padding: 6px 14px 8px;
  border: 1px solid var(--panel-edge); border-radius: 9px;
  background: rgba(255,255,255,0.02);
  font-size: 12px; line-height: 1.5; color: var(--ink); text-align: left;
}
.map-hunts .mh-title {
  text-align: center; color: var(--accent); font-weight: bold;
  letter-spacing: 1px; margin-bottom: 2px;
}
.map-hunts .mh-sub { color: var(--muted); font-weight: normal; letter-spacing: 0; }
.hunt-row { display: flex; align-items: baseline; gap: 8px; min-width: 0; }
.hunt-row .hunt-icon { flex: none; }
.hunt-row .hunt-name { font-weight: bold; white-space: nowrap; }
.hunt-row .hunt-goal {
  color: var(--muted); flex: 1; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.hunt-row .hunt-coin { color: var(--gold); white-space: nowrap; }

/* The Weekly Gauntlet banner — its own entry on the map screen, not a hex. */
.map-gauntlet {
  width: min(720px, 90vw); margin: 6px auto 0; padding: 6px 14px;
  border: 1px solid var(--gold); border-radius: 9px;
  background: linear-gradient(90deg, rgba(255,209,92,0.12), rgba(255,209,92,0.03));
  font-size: 12px; line-height: 1.5; color: var(--ink); text-align: center;
  display: flex; align-items: center; justify-content: center; gap: 12px; flex-wrap: wrap;
}
.map-gauntlet b { color: var(--gold); }
.map-gauntlet .gt-clears { white-space: nowrap; }
.map-gauntlet .btn { padding: 5px 14px; font-size: 12px; }
/* Anomaly row on the hex detail card. */
#map-detail .md-anom {
  font-size: 12px; color: var(--muted);
  background: rgba(255,209,92,0.06); border: 1px solid #4a3c1e;
  border-radius: 8px; padding: 5px 12px;
}
#map-detail .md-anom b { color: var(--gold); }
/* RECLAIM. The red gradient simulates to khaki — i.e. to a gold "reward"
   button — so it carries the siege hatch too, and its label is a different
   word from MARCH rather than the same word in a different colour. */
.btn.primary.danger {
  background:
    repeating-linear-gradient(135deg, rgba(255,255,255,0.10) 0 6px, rgba(255,255,255,0) 6px 12px),
    linear-gradient(180deg, #c81f4a, #8e1236);
  border-color: var(--danger);
}
.toast.danger { border-color: var(--danger); }

/* ---- Narrow phones: the banners compress so the map doesn't ----
   The siege and gauntlet banners are context, not the centrepiece; on a
   390px-wide screen at full prose they ate 160px of the map's viewport.
   Tighter type and padding, and the gauntlet's trophy chips (best score,
   clears) step aside — the ENTER button and the one-attempt rule stay. */
@media (max-width: 480px) {
  .map-siege, .map-gauntlet { font-size: 11px; line-height: 1.35; padding: 4px 10px; }
  .map-hunts { font-size: 11px; line-height: 1.45; padding: 4px 10px 6px; }
  .map-gauntlet { gap: 8px; }
  .map-gauntlet .btn { padding: 4px 10px; font-size: 11px; }
  .map-gauntlet .gt-clears { display: none; }
  .map-anomaly { font-size: 11px; }
}

/* ---- Short viewports (landscape phones, ~844x390) ----
   The panel scrolls at 92vh, but nothing signals that, so on a landscape phone
   a first-time player saw the logo and blurb with PLAY entirely below the fold
   — and the world map's MARCH button was unreachable the same way. Compact the
   chrome so the primary action fits without scrolling. */
@media (max-height: 560px) {
  .panel { padding: 12px 18px; max-height: 96vh; }
  .title { font-size: 26px; margin-bottom: 0; }
  .title-emblem { display: none; }
  .title-wordmark { width: min(240px, 46vw); margin: 0 auto 2px; }
  .subtitle { margin-bottom: 6px; font-size: 11px; }
  .fang-divider { height: 5px; margin-bottom: 6px; }
  .section-title { font-size: 15px; margin: 2px 0 6px; }
  .section-title::after { margin-top: 4px; height: 5px; }
  .btn { padding: 7px 14px; font-size: 13px; }
  .btn-row { margin-top: 8px; gap: 8px; }
  .hint { font-size: 11px; line-height: 1.4; }
  .summary-grid { margin: 6px auto !important; font-size: 11px; }
  .card { min-height: 0; width: 150px; padding: 10px; }
  .map-wrap { height: min(430px, 62vw, 52vh); margin: 4px auto; }
  /* Short landscape screens can't afford the portrait map floor — the compact
     rules above already shrink the chrome instead. */
  .map-panel > .pin-scroll { min-height: 120px; }
  .map-legend { margin: 4px auto; }
  #map-detail { min-height: 0; padding: 7px 12px; gap: 4px; }
  #map-detail .md-desc { display: none; }
  .map-panel { padding: 10px 18px; }
  /* The tree flexes to the space left (see .tree-panel); no height override. */
  .ach-list, .shop-list { max-height: 40vh; }
}

/* Very short (landscape phones). The world-map panel carries the most rows of
   any screen; the detail card is already pinned outside the scroll region
   (see .pin-panel), so here we just shed its optional rows. */
@media (max-height: 440px) {
  .map-wrap { height: min(430px, 62vw, 34vh); }
  .map-panel > .pin-scroll { min-height: 150px; }
  .map-legend, .map-hunts { display: none; }
  #map-detail .md-loadout, #map-detail .md-threat-note { display: none; }
  .map-siege { padding: 4px 12px; font-size: 11px; }
}

/* Shop */
.shop-list { display: flex; flex-direction: column; gap: 8px; margin: 14px 0; }
.shop-row {
  display: flex; align-items: center; gap: 12px;
  background: #101430; border: 1px solid var(--panel-edge); border-radius: 10px; padding: 10px 12px;
}
.shop-icon { font-size: 24px; width: 34px; text-align: center; }
.shop-info { flex: 1; text-align: left; }
.shop-name { font-size: 14px; font-weight: bold; }
.shop-pips { font-size: 10px; letter-spacing: 1px; margin-left: 6px; }
.shop-desc { font-size: 12px; color: var(--muted); margin-top: 2px; }
.shop-buy { padding: 8px 14px; font-size: 13px; min-width: 76px; }
.shop-buy:disabled { opacity: 0.62; cursor: default; }
.shop-buy:disabled:hover { transform: none; border-color: var(--panel-edge); box-shadow: none; }

/* Achievements list */
.ach-list { display: flex; flex-direction: column; gap: 6px; margin: 14px 0; max-height: 60vh; overflow-y: auto; }
/* Locked rows (achievements + unclaimed Blessings) used to be the whole row at
   opacity 0.55, which put their text at 2.7:1 — the same copy reads 6.4:1 at
   full strength. The "not yours yet" hierarchy is carried by the recessed
   background, the muted text colour and the dimmed 🔒 glyph instead, so
   nothing legible is traded for it. */
.ach-row {
  display: flex; align-items: center; gap: 12px;
  background: #0d1026; border: 1px solid #232746; border-radius: 10px; padding: 8px 12px;
}
.ach-row .ach-name, .ach-row .ach-desc { color: var(--muted); }
.ach-row .ach-icon { opacity: 0.5; }
.ach-row.got { background: #101430; border-color: #3a8f6a; }
.ach-row.got .ach-name { color: var(--ink); }
.ach-row.got .ach-icon { opacity: 1; }
.ach-icon { font-size: 22px; width: 30px; text-align: center; }
/* Bestiary sprites: crisp pixel art in the icon slot; undiscovered entries are
   blacked-out silhouettes — the shape shows, the specifics are earned. */
.codex-icon { width: 34px; height: 34px; image-rendering: pixelated; object-fit: contain; flex: 0 0 auto; }
.codex-hidden { filter: brightness(0); opacity: 0.55; }
.ach-info { flex: 1; text-align: left; }
.ach-name { font-size: 14px; font-weight: bold; }
.ach-desc { font-size: 12px; color: var(--muted); margin-top: 1px; }
.ach-check { color: #7dff9c; font-weight: bold; width: 18px; text-align: center; }

/* Achievement toasts.
   A right-hand column, clear of the top-centre HUD cluster (timer, XP bar,
   combo counter) and of a results panel's headline — stacking them top-centre
   buried "YOU DIED" under five toasts on a player's first death. main.js
   queues them (max 3 visible) so the column never grows past three. */
#toast-wrap {
  position: absolute;
  top: calc(64px + env(safe-area-inset-top, 0px));
  right: calc(12px + env(safe-area-inset-right, 0px));
  z-index: 20; display: flex; flex-direction: column; gap: 8px; align-items: flex-end;
  pointer-events: none;
}
@media (max-width: 640px) {
  /* Phones: drop below the HUD, the combo counter and the announce line. The
     HUD's own bottom is measured and published by hud.js — it was a hardcoded
     232px, which the wrapped slot rows outgrew and the announce then collided
     with. The 90px band is what the two canvas-drawn lines below the HUD
     occupy at their largest (a max-scale streak plus an announce), and they
     are positioned off the very same measurement. `--hud-bottom` is already
     viewport-absolute, so it carries the safe-area inset with it. */
  #toast-wrap { top: calc(var(--hud-bottom, 175px) + 90px); }
  /* While a menu/results panel is open there is no live HUD to dodge, and the
     in-run slot above lands square on the panel's numbers (it buried the
     results grid on the audit's phone shots) — pin the column back to the
     top edge until the panel closes. */
  body.overlay-open #toast-wrap { top: calc(8px + env(safe-area-inset-top, 0px)); }
}
.toast {
  display: flex; align-items: center; gap: 10px;
  max-width: min(340px, 64vw); text-align: left;
  background: var(--panel); border: 2px solid var(--gold); border-radius: 10px;
  padding: 8px 16px; font-size: 13px; color: var(--ink);
  box-shadow: 0 6px 24px rgba(255,209,92,0.25);
  opacity: 0; transform: translateY(-16px); transition: opacity 0.35s, transform 0.35s;
}
.toast.show { opacity: 1; transform: translateY(0); }
.toast .toast-icon { font-size: 22px; }
.toast b { color: var(--gold); font-size: 11px; letter-spacing: 1px; text-transform: uppercase; }

/* Settings */
.set-row { display: flex; align-items: center; gap: 12px; padding: 10px 4px; border-bottom: 1px solid rgba(255,255,255,0.05); }
.set-row label { flex: 1; text-align: left; font-size: 14px; }
.set-row input[type="range"] { flex: 2; accent-color: var(--accent); height: 4px; }
.set-toggle { padding: 6px 16px; font-size: 13px; min-width: 70px; }
#set-vol-val { min-width: 44px; text-align: right; color: var(--accent); font-size: 13px; }

/* Save backup (settings): the export/import code boxes. Fixed short heights —
   the codes are long, but nobody reads them; they scroll inside their box so
   the panel's pinned Back button never moves. */
.save-ta {
  display: block; width: 100%; height: 58px; resize: none;
  background: #101430; color: var(--ink);
  border: 1px solid var(--panel-edge); border-radius: 8px;
  padding: 7px 10px; margin: 8px 0 0;
  font-family: var(--font); font-size: 11px; line-height: 1.4;
  word-break: break-all;
}
.save-ta:focus { outline: none; border-color: var(--accent); }
.save-ta::placeholder { color: var(--muted); }
.save-btns { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 8px; }
.save-btns .btn { padding: 7px 14px; font-size: 13px; }

/* Disabled buttons say so with a dashed edge, not by fading their label out of
   readability — at the old 0.4 the label sat at 3.3:1, and several of these
   buttons carry the only explanation of why they are disabled ("🔒 <gate>",
   "✗ Spent this week", "Reroll (0)"). */
button.btn:disabled { opacity: 0.62; cursor: default; border-style: dashed; }
button.btn:disabled:hover { transform: none; border-color: var(--panel-edge); box-shadow: none; }
kbd {
  background: #1a2050; border: 1px solid var(--panel-edge); border-radius: 4px;
  padding: 1px 6px; font-size: 12px; font-family: var(--font);
}

/* ===================== Run-start bite transition ===================== */
/* Two dark jaws with SVG fangs snap shut over the screen, then open onto
   the run. Triggered by main.js on startRun; skipped under reduced motion. */
#bite { position: absolute; inset: 0; z-index: 30; pointer-events: none; overflow: hidden; }
#bite .jaw {
  position: absolute; left: 0; right: 0; height: 54vh; background: #05050e;
  filter: drop-shadow(0 0 18px rgba(125,249,255,0.35));
}
#bite .jaw-top { top: 0; transform: translateY(-110%); z-index: 2; }  /* teeth interlock over the lower jaw */
#bite .jaw-bottom { bottom: 0; transform: translateY(110%); }
#bite .jaw-top::after, #bite .jaw-bottom::after {
  content: ''; position: absolute; left: 0; right: 0; height: 44px;
  background-repeat: repeat-x; background-size: auto 100%;
}
#bite .jaw-top::after {
  top: 100%;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='64' height='44'%3E%3Cpath d='M0 0h64L33 40z' fill='%23f6ffff'/%3E%3Cpath d='M8 0h48L33 32z' fill='%23dff6ff'/%3E%3C/svg%3E");
}
#bite .jaw-bottom::after {
  bottom: 100%;
  background-position-x: 32px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='64' height='44'%3E%3Cpath d='M0 44h64L33 4z' fill='%23f6ffff'/%3E%3Cpath d='M8 44h48L33 12z' fill='%23ffe6fb'/%3E%3C/svg%3E");
}
#bite.chomp .jaw-top { animation: jawTop 0.9s cubic-bezier(0.65, 0, 0.3, 1) both; }
#bite.chomp .jaw-bottom { animation: jawBottom 0.9s cubic-bezier(0.65, 0, 0.3, 1) both; }
@keyframes jawTop {
  0% { transform: translateY(-110%); }
  36%, 58% { transform: translateY(0); }
  100% { transform: translateY(-110%); }
}
@keyframes jawBottom {
  0% { transform: translateY(110%); }
  36%, 58% { transform: translateY(0); }
  100% { transform: translateY(110%); }
}

/* ===================== Tutorial hints ===================== */
#tut-hint {
  position: absolute; left: 50%; transform: translateX(-50%);
  bottom: calc(96px + env(safe-area-inset-bottom, 0px));
  max-width: min(480px, 88vw);
  background: var(--panel); border: 2px solid var(--accent); border-radius: 12px;
  padding: 12px 38px 10px 16px; text-align: center; z-index: 7;
  font-size: 14px; line-height: 1.5; color: var(--ink);
  box-shadow: 0 8px 30px rgba(125,249,255,0.18);
  pointer-events: auto; transition: opacity 0.25s;
}
#tut-hint.pop { animation: tutPop 0.35s cubic-bezier(0.18, 0.89, 0.32, 1.3); }
@keyframes tutPop { from { opacity: 0; transform: translateX(-50%) translateY(14px); } }
#tut-hint .tut-dots { margin-top: 8px; display: flex; gap: 5px; justify-content: center; }
#tut-hint .tut-dot { width: 7px; height: 7px; border-radius: 50%; background: #2a2f52; }
#tut-hint .tut-dot.on { background: var(--accent); }
#tut-skip {
  position: absolute; top: 4px; right: 4px; width: 24px; height: 24px;
  background: transparent; border: none; color: var(--muted); font-size: 13px;
  cursor: pointer; font-family: var(--font);
}
#tut-skip:hover { color: var(--ink); }

/* ===================== Touch joystick ===================== */
#touch-stick {
  position: absolute; width: 120px; height: 120px; border-radius: 50%;
  background: rgba(125,249,255,0.06); border: 2px solid rgba(125,249,255,0.25);
  z-index: 5; pointer-events: none;
}
#touch-knob {
  position: absolute; left: 50%; top: 50%; width: 52px; height: 52px;
  margin: -26px 0 0 -26px; border-radius: 50%;
  background: rgba(125,249,255,0.35); border: 2px solid var(--accent);
}

#btn-pause-touch {
  position: absolute; z-index: 6;
  top: calc(8px + env(safe-area-inset-top, 0px));
  right: calc(8px + env(safe-area-inset-right, 0px));
  width: 44px; height: 44px; border-radius: 10px;
  background: var(--panel); border: 2px solid var(--panel-edge); color: var(--ink);
  font-size: 18px; cursor: pointer; pointer-events: auto;
  display: flex; align-items: center; justify-content: center;
}
#btn-pause-touch:active { border-color: var(--accent); }

/* Damage/announce flash text handled on canvas; screen shake via transform */
@media (max-width: 640px) {
  .title { font-size: 34px; }
  .card { width: 150px; min-height: 200px; }
  #hp-bar { width: 180px; }
  /* Compact the title-screen chrome so Reset Save isn't clipped at the
     bottom of the panel on portrait phones. */
  .title-emblem { width: 96px; }
  .subtitle { margin-bottom: 12px; }
  /* Level-up and character-select cards become full-width horizontal rows:
     tall cards stacked most choices below the fold (three cards on the game's
     single most frequent decision; one character per screenful once the
     signature blurbs landed) — as rows, the options share one screen. */
  .levelup-grid, .select-grid { flex-direction: column; flex-wrap: nowrap; gap: 10px; align-items: stretch; }
  .levelup-grid .card, .select-grid .card {
    width: 100%; min-height: 0;
    flex-direction: row; text-align: left; align-items: center;
    gap: 12px; padding: 10px 12px 10px 32px;
  }
  .select-grid .card { padding-left: 12px; }        /* no number-key badge to clear */
  .levelup-grid .card .card-icon-glyph { font-size: 32px; margin: 0; }
  .select-grid .card .card-icon { width: 52px; height: 52px; margin: 0; flex: 0 0 auto; }
  .levelup-grid .card .card-body, .select-grid .card .card-body { align-items: flex-start; }
  .levelup-grid .card .card-desc, .select-grid .card .card-desc { margin-top: 4px; }
  .levelup-grid .card .card-key { top: 50%; left: 6px; transform: translateY(-50%); }
}

/* Respect the OS reduced-motion preference — keep UI functional, kill flourish. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.001s !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001s !important;
  }
}
