/* ============================================================
   PIXEL IDLE RPG — commercial-grade retro shell
   Palette: Endesga-32 + gold trim · Dark fantasy pixel aesthetic
   ------------------------------------------------------------
   Layout model
     Shell is a single grid column: HUD · main · footer, with a
     position:fixed dock pinned to the bottom. --dock-h reserves the
     body's bottom padding so nothing ever hides behind it.

     The dock and the character sub-tabs are BOTH made of real .tab-btn
     elements, because game.js binds and badges that class. The shell uses
     :has() to react to whichever .tab-panel game.js unhid — that is what
     keeps the dock's HEROES slot lit while a sub-tab is open, and what
     shows the sub-tab row only for the character group. No extra JS.
   ------------------------------------------------------------
   Contents
     1.  Reset
     2.  Design tokens
     3.  Base / page
     4.  Pixel primitives (panel, buttons, gold trim)
     5.  Top HUD (avatar ring · wave tracker · currency pills)
     6.  Language toggle
     7.  Stage: canvas, CRT stack + option, combat overlay, hit text
     8.  Skill dock (three 64x64 pixel slots, radial cooldown)
     9.  Sidebar sub-tabs, buy dock, panels
     9b. Bottom dock
    10.  Shop rows / upgrade cards
    10b. Marketplace, chat, arena + the rival inspection sheet
    11.  Inventory, rarity, item tooltips
    12.  Blacksmith / Forge
    13.  Skill orb (SKILLS tab icon)
    14.  Artifacts / prestige
    15.  Loot notifications
    16.  Settings modal (CSS-only toggles)
    16b. Progression: speed, tower, save I/O
    16c. Audio, enrage, achievements
    16d. Town hub
    16e. Party roster
    17.  Utilities   (.hidden is required by game.js)
    18.  Responsive
    19.  Reduced motion
    ---- the Metin2-flavoured expansion pack ----
    26a. The constellation WINDOW (drag bar, resize grip, hover card)
    26b. Efsun (enchant bonus) + the +7 enhancement aura
    26c. Evcil hayvan — the companion pet block
    26d. Zindanlar — the raid board
    26e. Günlük görevler — the daily bounty board
    26f. The AFK chest and the milestone bonus line
   ============================================================ */

/* ---------- 1. Reset ---------- */
*,
*::before,
*::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
  border-radius: 0; /* pixel art: never round a corner */
}

/* ---------- 2. Design tokens ---------- */
:root {
  /* Endesga-32 */
  --e-brick:#be4a2f; --e-rust:#d77643; --e-cream:#ead4aa; --e-sand:#e4a672;
  --e-clay:#b86f50;  --e-brown:#733e39; --e-ink:#3e2731;  --e-crimson:#a22633;
  --e-red:#e43b44;   --e-orange:#f77622; --e-amber:#feae34; --e-yellow:#fee761;
  --e-green:#63c74d; --e-forest:#3e8948; --e-pine:#265c42; --e-deep:#193c3e;
  --e-blue:#124e89;  --e-sky:#0099db;   --e-cyan:#2ce8f5;  --e-white:#ffffff;
  --e-silver:#c0cbdc;--e-grey:#8b9bb4;  --e-steel:#5a6988; --e-slate:#3a4466;
  --e-navy:#262b44;  --e-black:#181425; --e-hot:#ff0044;   --e-purple:#68386c;
  --e-mauve:#b55088; --e-rose:#f6757a;  --e-skin:#e8b796;  --e-tan:#c28569;

  /* ---------- 16-bit JRPG dark slate theme ----------
     Three colours carry the whole redesign, and everything below is expressed in
     terms of them rather than of the Endesga swatches directly, so re-theming is
     a three-line edit:

       --jrpg-bg     #1a1c23  the page ground
       --jrpg-panel  #2d3242  the inner panel plate
       --jrpg-gold   #e2b714  the accent

     The Endesga-32 ramp above is kept because the CANVAS art is drawn from it —
     sprites, biomes and particles all index those names, so they are not
     interchangeable with the UI theme and must not be folded into it. */
  --jrpg-bg:#1a1c23;
  --jrpg-bg2:#14161c;             /* one step darker, for wells and tracks */
  --jrpg-panel:#2d3242;
  --jrpg-panel2:#363c50;          /* raised face inside a panel */
  --jrpg-gold:#e2b714;
  --jrpg-gold-hi:#ffdf5e;
  --jrpg-edge:#0d0f14;            /* the 2px pixel border */
  --jrpg-bevel-hi:rgba(255,255,255,.10);
  --jrpg-bevel-lo:rgba(0,0,0,.42);

  /* semantic — now mapped onto the theme above */
  --page:var(--jrpg-bg);
  --panel-bg:var(--jrpg-panel);
  --panel-bg2:var(--jrpg-panel2);
  --line:var(--jrpg-edge);
  --bevel-hi:var(--jrpg-bevel-hi);
  --bevel-lo:var(--jrpg-bevel-lo);
  --txt:var(--e-silver);
  --txt-strong:var(--e-white);
  --txt-dim:#8b93a8;
  --trim:var(--jrpg-gold);        /* gold trim */
  --trim-hi:var(--jrpg-gold-hi);

  /* the 2px pixel frame every container wears, plus its bevel */
  --pxframe:0 0 0 2px var(--jrpg-edge);
  --pxbevel:inset 2px 2px 0 var(--jrpg-bevel-hi), inset -2px -2px 0 var(--jrpg-bevel-lo);

  /* accents used by the HUD chrome */
  --xp-a:var(--e-cyan);           /* level ring + level chip */
  --alert:var(--e-red);           /* notification badges */

  /* rhythm — every gap in the shell comes from these three */
  --gap-sm:6px;
  --gap-md:9px;
  --gap-lg:12px;

  /* shell metrics; --dock-h also reserves the body's bottom padding */
  --dock-h:70px;
  --rail:352px;                   /* sidebar width on desktop */

  /* reusable pixel elevation, so panels and buttons stay consistent */
  --bevel:inset 3px 3px 0 rgba(255,255,255,.08), inset -3px -3px 0 rgba(0,0,0,.3);
  --ring:0 0 0 3px var(--line);
  --lift:0 4px 0 0 var(--line);

  /* ---------- pixel type stack ----------
     Press Start 2P first, because it IS the game's look. Silkscreen and
     Pixelify Sans follow it purely to supply the glyphs it does not have:
     Press Start 2P is Latin-only, so Turkish (ş ğ ı İ ç ö ü) and every other
     latin-ext character fell through to 'Courier New' one glyph at a time.
     Font fallback is per-glyph, so this list alone fixes the mixed-font words —
     the Turkish letters now come from a pixel face.

     Courier New stays at the end as the offline/no-network path: opened straight
     off the disk with no webfonts, the whole UI degrades to one consistent
     monospace rather than to a mixture. */
  --font-pixel:'Press Start 2P','Silkscreen','Pixelify Sans','Courier New',monospace;
  --font-body:ui-monospace,'Lucida Console',Consolas,'Courier New',monospace;
}

/* ---------- Turkish glyph handling ----------
   Two separate problems, both of which show up as "broken Turkish".

   1. SUBSTITUTED GLYPHS — handled by the fallback list above.

   2. WRONG-CASE GLYPHS — Turkish is one of the two locales where Latin casing is
      not universal: uppercase of `i` is `İ` (not `I`), and lowercase of `I` is
      `ı` (not `i`). A browser only knows to do that if the text is tagged as
      Turkish, so every `text-transform: uppercase` in this sheet depends on the
      lang attribute being right. game.js sets <html lang> from the language
      toggle and the .t spans carry their own lang, so both paths are covered —
      this block just makes the dependency explicit and guards the descenders,
      which is where the remaining clipping came from: ş, ğ and ç all drop below
      the baseline, and a pixel font at 6px with the default line-height had no
      room for the tail. */
:lang(tr) {
  /* never let the browser fake a bold/italic for a face that has none — a
     synthesised pixel glyph is what produces the smeared look */
  font-synthesis: none;
}
/* Any pixel-font label can hold a descender in Turkish, so no pixel-font run is
   allowed a line-height under 1.5. Individual rules may raise it, never lower it.
   `:lang(tr)` (not `[lang=tr]`) so it follows <html lang> AND the per-span lang
   the .t pattern sets, without caring which one supplied it. */
:lang(tr) .pill-label,
:lang(tr) .zone-chapter,
:lang(tr) .zone-name,
:lang(tr) .lv-badge,
:lang(tr) .wave-word,
:lang(tr) .tower-label,
:lang(tr) .sub-txt,
:lang(tr) .dock-txt,
:lang(tr) .auto-label,
:lang(tr) .skill-name { line-height: 1.65; }

/* ---------- 3. Base / page ---------- */
html, body { min-height: 100%; }

body {
  background-color: var(--page);
  /* A flat slate ground with a faint scanline and one soft overhead pool. The old
     background was a purple radial wash; the JRPG theme wants the panels to be
     the brightest thing on screen, so the page is deliberately dull. */
  background-image:
    repeating-linear-gradient(0deg, rgba(255,255,255,.016) 0 1px, transparent 1px 4px),
    radial-gradient(120% 80% at 50% -20%, #262a35 0%, #1f222b 45%, var(--jrpg-bg) 100%);
  background-attachment: fixed;
  color: var(--txt);
  font-family: var(--font-body);
  font-size: 13px;
  line-height: 1.5;
  letter-spacing: .3px;
  padding: var(--gap-lg);
  /* the fixed dock must never cover the last row of content */
  padding-bottom: calc(var(--dock-h) + var(--gap-lg) + env(safe-area-inset-bottom, 0px));
  overflow-x: hidden;
}

.app { max-width: 1180px; margin: 0 auto; display: grid; gap: var(--gap-lg); }

h1, h2 {
  font-family: var(--font-pixel);
  font-weight: 400;
  color: var(--txt-strong);
}
h1 { font-size: 15px; line-height: 1.4; text-shadow: 0 3px 0 var(--e-black), 0 0 14px rgba(254,174,52,.25); }
h2 { font-size: 11px; letter-spacing: 1.5px; color: var(--e-cream); text-shadow: 0 2px 0 var(--e-black); }

canvas { display: block; image-rendering: pixelated; image-rendering: crisp-edges; }
::selection { background: var(--e-yellow); color: var(--e-black); }

/* bilingual labels: CSS mirrors html[data-lang], which applyI18n() maintains */
.t > [lang] { font-style: normal; }
.t > [lang="tr"] { display: none; }
html[data-lang="tr"] .t > [lang="en"] { display: none; }
html[data-lang="tr"] .t > [lang="tr"] { display: inline; }

/* ---------- 4. Pixel primitives ---------- */
/* Every container wears the same 2px pixel frame plus a one-step bevel. Two
   tokens, so the whole shell is consistent and re-theming is central. */
.panel {
  position: relative;
  background: var(--panel-bg);
  padding: 12px;
  box-shadow:
    var(--pxframe),
    var(--pxbevel),
    0 4px 0 0 rgba(0, 0, 0, .38);
}

/* thin gold trim along the top edge of major panels */
.panel::before {
  content: '';
  position: absolute;
  left: 3px; right: 3px; top: 0;
  height: 2px;
  background: linear-gradient(90deg, transparent, var(--trim), transparent);
  opacity: .55;
  pointer-events: none;
}

.icon-btn, .buy-btn, .tab-btn, .lang-btn, .skill-btn, .inv-btn {
  font-family: var(--font-pixel);
  border: 0;
  cursor: pointer;
  color: var(--txt-strong);
  text-transform: uppercase;
  -webkit-tap-highlight-color: transparent;
}

/* Compact 2px-framed toggle. Square-ish and small so a rail of six of them still
   fits beside the currency pills. */
.icon-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--panel-bg2);
  font-size: 10px;
  letter-spacing: 1px;
  padding: 7px 8px;
  min-height: 30px;
  min-width: 30px;
  box-shadow:
    inset 0 2px 0 rgba(255, 255, 255, .13),
    inset 0 -2px 0 rgba(0, 0, 0, .34),
    var(--pxframe),
    0 3px 0 0 var(--jrpg-edge);
  transition: transform .08s steps(2), filter .12s linear;
}
.icon-btn:hover { filter: brightness(1.18) saturate(1.1); transform: translateY(-2px); }
.icon-btn:active { transform: translateY(2px); }
.icon-btn.is-danger { background: var(--e-crimson); }
.icon-btn.is-cog { font-size: 15px; padding: 8px 11px; background: var(--e-brown); color: var(--trim-hi); }
.icon-btn.is-close { font-size: 16px; min-height: 30px; padding: 4px 10px; background: var(--e-steel); }

.icon-btn:focus-visible, .buy-btn:focus-visible, .tab-btn:focus-visible,
.lang-btn:focus-visible, .skill-btn:focus-visible, .inv-btn:focus-visible,
.forge-slot:focus-visible, .artifact-slot:focus-visible {
  outline: 3px solid var(--trim-hi);
  outline-offset: 3px;
}

/* ---------- 5. Top HUD ----------
   Three zones on one flex row: avatar · stage tracker · currency + utilities.

   This used to be `grid-template-columns: auto minmax(0,1fr) auto`, and the
   right-hand `auto` track is what clipped the currency pills and the utility
   icons: an `auto` track sizes to content and then refuses to shrink, so once the
   rail grew past the space available it simply overflowed the panel instead of
   wrapping.

   Flex fixes it because the zones can be given different shrink behaviour:

     left    fixed content (portrait, level badge, XP) — never shrinks
     centre  the elastic one: flex 1 with min-width 0 so long biome names
             ellipsise instead of pushing the rail off the edge
     right   never shrinks either, but the ROW is allowed to wrap, so on a
             narrow viewport the rail drops to its own line at full width
             rather than being cut off

   min-width: 0 on every zone is load-bearing: a flex item's default
   min-width:auto refuses to shrink below its content, which is the other half of
   the original overflow. */
.hud {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--gap-md) var(--gap-lg);
  padding: var(--gap-md) var(--gap-lg);
}

/* --- left: avatar frame with level ring --- */
.hud-left { flex: 0 0 auto; display: flex; align-items: center; gap: 10px; min-width: 0; }

/*
  The ring is a conic-gradient disc; .av-plate sits on top of its middle and is
  opaque, which is what turns the disc into an annulus. That is deliberately
  done with layering rather than mask-composite, because the prefixed
  -webkit-mask composite keywords differ from the standard ones.
  game.js writes --p (0-100) once per HUD tick.
*/
.avatar {
  --p: 0;
  position: relative;
  width: 58px;
  height: 58px;
  flex: 0 0 auto;
}
.av-ring {
  position: absolute;
  inset: 0;
  background:
    conic-gradient(from -90deg,
      var(--xp-a) 0 calc(var(--p) * 1%),
      rgba(0, 0, 0, .55) calc(var(--p) * 1%) 100%);
  box-shadow: 0 0 0 2px var(--line), 0 0 12px rgba(44, 232, 245, .28);
}
.av-plate {
  position: absolute;
  inset: 7px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: linear-gradient(180deg, var(--panel-bg2), var(--e-black));
  box-shadow: inset 0 0 0 2px var(--line), inset 2px 2px 0 rgba(255, 255, 255, .09);
}
.av-face { font-size: 21px; line-height: 1; filter: drop-shadow(0 2px 0 rgba(0, 0, 0, .6)); }

/* class sigil, bottom-left; level chip, bottom-right */
.av-class {
  position: absolute;
  left: -3px;
  bottom: -3px;
  font-size: 11px;
  line-height: 1;
  padding: 2px;
  background: var(--e-slate);
  box-shadow: 0 0 0 2px var(--line);
}
/* NOTE: .av-lv is gone — the level moved out of the portrait and onto its own
   .lv-badge plate beneath it, so the number is readable at 6.5px instead of being
   a 20px chip hanging off the ring's corner. */

/* the hero box: framed portrait with the level badge welded to its base */
.hero-box { position: relative; flex: 0 0 auto; display: flex; flex-direction: column; align-items: center; gap: 4px; }
.lv-badge {
  display: inline-flex;
  align-items: baseline;
  gap: 3px;
  padding: 2px 6px;
  font-family: var(--font-pixel);
  font-size: 6.5px;
  line-height: 1.6;
  letter-spacing: .8px;
  color: var(--jrpg-edge);
  background: var(--jrpg-gold);
  box-shadow: var(--pxframe), inset 0 2px 0 rgba(255, 255, 255, .3);
}
.lv-badge b { font-weight: 400; font-size: 8px; }

.hud-id { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.hud-name {
  font-family: var(--font-pixel);
  font-size: 9px;
  letter-spacing: 1px;
  line-height: 1.6;
  color: var(--txt-strong);
  text-shadow: 0 2px 0 var(--jrpg-edge);
}
/* the segmented EXP strip: narrow, 20 chunks, green.
   Compound `.bar.bar-xp` on purpose — this rule sits ABOVE the base `.bar` block
   in the file, and at equal specificity source order would let `.bar`'s height
   win. The extra class makes the override order-independent. */
.bar.bar-xp { --seg: 20; width: 132px; max-width: 100%; height: 9px; }
.fill-xp { background: var(--e-green); }

.hud-meta {
  display: flex;
  align-items: baseline;
  gap: 8px;
  font-family: var(--font-pixel);
  font-size: 6.5px;
  letter-spacing: 1px;
  line-height: 1.7;
  color: var(--txt-dim);
}
.hud-meta b { font-weight: 400; color: var(--e-cream); }
.hud-xp { color: var(--e-green); }
.hud-best { display: inline-flex; align-items: baseline; gap: 4px; white-space: nowrap; }

/* --- centre: chapter / zone / wave --- */
.hud-mid {
  /* the elastic zone: 260px is the basis it would LIKE, but flex-shrink keeps it
     collapsible so the rail never gets pushed out of the panel */
  flex: 1 1 260px;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 5px;
  min-width: 0;
  padding: 0 var(--gap-md);
}
/* "BÖLÜM 1" · "BOŞLUK ENGİNİ" — the chapter tag is a gold plate so the zone name
   beside it can be the long, ellipsising half. */
.zone-line { display: flex; align-items: center; gap: 8px; min-width: 0; }
.zone-chapter {
  flex: 0 0 auto;
  padding: 2px 6px;
  font-family: var(--font-pixel);
  font-size: 6px;
  letter-spacing: 1.4px;
  line-height: 1.7;
  color: var(--jrpg-edge);
  background: var(--jrpg-gold);
  box-shadow: var(--pxframe);
  white-space: nowrap;
}
.zone-name {
  min-width: 0;
  font-family: var(--font-pixel);
  font-size: 8px;
  letter-spacing: .6px;
  line-height: 1.7;
  color: var(--jrpg-gold-hi);
  text-shadow: 0 2px 0 var(--jrpg-edge);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.wave-line { display: flex; align-items: baseline; gap: 8px; min-width: 0; }
.wave-word { font-family: var(--font-pixel); font-size: 6.5px; letter-spacing: 2px; line-height: 1.7; color: var(--txt-dim); }
.wave-num {
  font-family: var(--font-pixel);
  font-size: 18px;
  font-weight: 400;
  line-height: 1.05;
  color: var(--txt-strong);
  text-shadow: 0 3px 0 var(--jrpg-edge);
}
.wave-step {
  margin-left: auto;
  font-family: var(--font-pixel);
  font-size: 7px;
  line-height: 1.7;
  color: var(--jrpg-gold);
  white-space: nowrap;
}

/* Zone progress bar: one segment per wave in the CURRENT zone, so the divider
   count changes with depth (five early, fifteen deep) rather than always being
   five. The skull lights on the boss run. */
.wave-track {
  position: relative;
  height: 12px;
  background: var(--e-black);
  box-shadow: 0 0 0 2px var(--line), inset 2px 2px 0 rgba(0, 0, 0, .6);
}
.wave-fill {
  height: 100%;
  width: 0;
  background:
    repeating-linear-gradient(90deg, rgba(255, 255, 255, .16) 0 2px, transparent 2px 6px),
    linear-gradient(180deg, var(--e-cyan), var(--e-sky));
  box-shadow: 0 0 10px rgba(44, 232, 245, .5);
  transition: width .3s cubic-bezier(.2, .8, .2, 1);
}
/* Divider overlay. game.js rebuilds its children whenever the zone changes
   width, so the notch count always equals (waves in this zone - 1). It sits on
   top of the fill and must not eat pointer events. Absolutely positioned over
   the whole track so a child's `left: 37.5%` still resolves against the track. */
.wave-notches {
  position: absolute;
  inset: 0;
  display: block;
  pointer-events: none;
}
.wave-notch {
  position: absolute;
  top: 0;
  width: 2px;
  height: 100%;
  background: var(--e-black);
  opacity: .85;
}
.wave-skull {
  position: absolute;
  right: -7px;
  top: 50%;
  transform: translateY(-50%);
  font-size: 13px;
  line-height: 1;
  filter: grayscale(1) brightness(.55);
  transition: filter .25s linear;
}
/* game.js toggles .is-boss while the wave in progress IS the boss wave */
.wave-track.is-boss .wave-fill {
  background:
    repeating-linear-gradient(90deg, rgba(255, 255, 255, .18) 0 2px, transparent 2px 6px),
    linear-gradient(180deg, var(--e-red), var(--e-crimson));
  box-shadow: 0 0 12px rgba(228, 59, 68, .6);
}
.wave-track.is-boss .wave-skull {
  filter: none;
  animation: skull-throb 1.1s ease-in-out infinite;
}
@keyframes skull-throb {
  0%, 100% { transform: translateY(-50%) scale(1); }
  50%      { transform: translateY(-50%) scale(1.22); }
}

/* NOTE: .boss-chip / .chip-label / .chip-val are gone with the boss countdown
   they displayed. The Tower still has a clock, and it still has a readout — in
   the Tower panel (.tower-timer), not in the header. */

/* --- right: currency pills + utility rail ---
   flex: 0 0 auto plus margin-left:auto pins the rail to the right edge while the
   centre zone absorbs the slack. Nothing inside the rail is allowed to shrink
   (flex-shrink: 0) — a half-width gold counter is worse than a wrapped row — so
   when there genuinely is not enough space the ROW wraps instead, and at the
   breakpoints below the rail takes a line of its own. */
.hud-right {
  flex: 0 0 auto;
  margin-left: auto;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 6px;
  min-width: 0;
  max-width: 100%;
}
.pill-row, .util-row { gap: 6px; }
.pill-row { display: flex; flex-wrap: wrap; justify-content: flex-end; }
.pill-row > *, .util-row > * { flex-shrink: 0; }

/*
  CURRENCY PILL — clipping fix.

  "ALTIN 1.45K" used to collide with both the coin icon and the pill's own right
  border. Three separate causes, all fixed here:

    1. `min-width: 0` on .pill-body let the flex item shrink BELOW its content,
       so a five-glyph value like "1.45K" was squeezed under the label. It is
       now `fit-content`, which refuses to compress the text at all.
    2. `padding: 4px 8px 4px 7px` gave the value 8px of clearance on the right;
       the pixel font's advance width plus its 2px text-shadow needed more, so
       the last glyph sat on the frame. Now a symmetric `4px 12px`.
    3. `line-height: 1.15` on an 11px pixel face is shorter than the glyph box,
       so the value's cap-height overlapped the label sitting above it. 1.45
       gives each row its own band.

  `flex-shrink: 0` and `min-width: fit-content` are stated on the pill itself as
  well: the row wraps to a second line rather than ever compressing a counter.
*/
.pill {
  display: flex;
  align-items: center;
  gap: 8px;
  flex: 0 0 auto;
  flex-shrink: 0;
  min-width: fit-content;
  padding: 4px 12px;
  overflow: visible;
  background: linear-gradient(180deg, var(--jrpg-panel2) 0%, var(--jrpg-panel) 100%);
  box-shadow:
    var(--pxframe),
    inset 2px 2px 0 rgba(255, 255, 255, .08),
    inset 0 -2px 0 rgba(0, 0, 0, .38),
    inset 4px 0 0 var(--c-accent);
}
.pill-body {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 3px;
  flex: 0 0 auto;
  min-width: fit-content;
  overflow: visible;
}
.pill-label {
  font-family: var(--font-pixel);
  font-size: 6px;
  letter-spacing: 1.4px;
  /* Turkish labels (CEVHER, MİTİK) carry a dotted capital and descenders; a
     tight default line-height clipped them inside the pill */
  line-height: 1.6;
  color: var(--txt-dim);
  white-space: nowrap;
  overflow: visible;
}
.pill-val {
  display: block;
  font-family: var(--font-pixel);
  font-size: 11px;
  font-weight: 400;
  /* room for the glyph box AND the 2px drop shadow beneath it */
  line-height: 1.45;
  /* the pixel font's last glyph carries no side bearing of its own */
  margin-right: 2px;
  color: var(--c-accent);
  text-shadow: 0 2px 0 rgba(0, 0, 0, .7);
  white-space: nowrap;
  overflow: visible;
}

/*
  Pixel currency icons, drawn entirely with box-shadow on a 3px cell grid. The
  element itself is one transparent cell at (0,0); every lit cell is a shadow
  offset by a multiple of 3px, so the art stays crisp at any zoom.
*/
.pill-ico {
  width: 3px;
  height: 3px;
  flex: 0 0 auto;
  /* box-shadow never affects layout, so the margin reserves the 15x15 footprint
     the shadow grid actually paints (3px cell + 12px of cells to the right/below) */
  margin: 0 12px 12px 0;
  background: transparent;
}
.pill.is-gold   { --c-accent: var(--jrpg-gold-hi); }
.pill.is-gem    { --c-accent: var(--e-cyan); }
.pill.is-mythic { --c-accent: var(--e-mauve); }

/* coin */
.pill.is-gold .pill-ico {
  box-shadow:
     3px  0 0 var(--e-amber),  6px  0 0 var(--e-amber),  9px  0 0 var(--e-amber),
     0    3px 0 var(--e-amber), 3px  3px 0 var(--e-yellow), 6px 3px 0 var(--e-yellow),
     9px  3px 0 var(--e-yellow), 12px 3px 0 var(--e-amber),
     0    6px 0 var(--e-amber), 3px  6px 0 var(--e-yellow), 6px 6px 0 #fff,
     9px  6px 0 var(--e-yellow), 12px 6px 0 var(--e-amber),
     0    9px 0 var(--e-amber), 3px  9px 0 var(--e-yellow), 6px 9px 0 var(--e-yellow),
     9px  9px 0 var(--e-yellow), 12px 9px 0 var(--e-amber),
     3px 12px 0 var(--e-amber), 6px 12px 0 var(--e-amber), 9px 12px 0 var(--e-amber);
  filter: drop-shadow(0 0 5px rgba(254, 231, 97, .7));
}
/* gem */
.pill.is-gem .pill-ico {
  box-shadow:
     6px  0 0 var(--e-sky),
     3px  3px 0 var(--e-sky), 6px 3px 0 var(--e-cyan), 9px 3px 0 var(--e-sky),
     0    6px 0 var(--e-sky), 3px 6px 0 var(--e-cyan), 6px 6px 0 #fff,
     9px  6px 0 var(--e-cyan), 12px 6px 0 var(--e-sky),
     3px  9px 0 var(--e-sky), 6px 9px 0 var(--e-cyan), 9px 9px 0 var(--e-sky),
     6px 12px 0 var(--e-sky);
  filter: drop-shadow(0 0 5px rgba(44, 232, 245, .7));
}
/* shard */
.pill.is-mythic .pill-ico {
  box-shadow:
     6px  0 0 #fff,
     3px  3px 0 var(--e-purple), 6px 3px 0 var(--e-cyan), 9px 3px 0 var(--e-purple),
     3px  6px 0 var(--e-purple), 6px 6px 0 var(--e-cyan), 9px 6px 0 var(--e-purple),
     3px  9px 0 var(--e-purple), 6px 9px 0 var(--e-cyan), 9px 9px 0 var(--e-purple),
     6px 12px 0 var(--e-mauve);
  filter: drop-shadow(0 0 5px rgba(181, 80, 136, .8));
}

.util-row { display: flex; align-items: center; flex-wrap: wrap; justify-content: flex-end; }

/* ---------- 6. Language toggle ---------- */
.lang-toggle {
  display: flex;
  background: var(--jrpg-bg2);
  padding: 2px;
  box-shadow: var(--pxframe), inset 0 2px 0 rgba(0, 0, 0, .4);
}
.lang-btn {
  background: transparent;
  color: var(--txt-dim);
  font-size: 8px;
  letter-spacing: 1px;
  padding: 6px 7px;
  min-width: 28px;
  transition: background .2s ease, color .2s ease;
}
.lang-btn:hover { color: var(--txt-strong); }
.lang-btn.active { background: var(--e-cyan); color: var(--jrpg-edge); box-shadow: inset 0 -2px 0 rgba(0, 0, 0, .28); }

/* ---------- 7. Stage ---------- */
.main { display: grid; grid-template-columns: minmax(0,1fr) var(--rail); gap: var(--gap-lg); align-items: start; }
.stage-column { min-width: 0; display: grid; gap: 10px; }

.stage {
  padding: 6px;
  background: var(--jrpg-bg2);
  box-shadow: var(--pxframe), var(--pxbevel), 0 5px 0 0 rgba(0, 0, 0, .45);
}
.stage::before { opacity: .8; }
/* The arena is centred inside its frame and capped, so at 1280px the canvas does
   not span the whole panel edge-to-edge while the (centred) combat dock sits
   beneath it — the two were visibly out of alignment before. */
.stage-inner {
  position: relative;
  line-height: 0;
  margin: 0 auto;
  width: 100%;
  max-width: 960px;
  background: #0b0912;
  box-shadow: inset 0 0 0 2px var(--jrpg-edge);
  overflow: hidden;
}
/* --stage-ar is written by applyRes() from the live backing-store size, so the
   box follows the chosen resolution's aspect and the absolutely-positioned
   overlay layer (boss bar, HP stack, hit text) stays glued to the world rect.
   The fallback keeps older/JS-less rendering at the 16:9 design ratio. */
#game-canvas {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: var(--stage-ar, 16 / 9);
  image-rendering: pixelated;
  image-rendering: crisp-edges;
}

/* ---------- resolution picker (top bar) ---------- */
.res-ctl {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 3px 6px;
  background: rgba(0, 0, 0, .34);
  box-shadow: inset 0 0 0 2px var(--line);
  cursor: pointer;
}
.res-ico { font-size: 11px; line-height: 1; filter: saturate(.7); }
.res-select {
  font-family: var(--font-pixel);
  font-size: 6px;
  letter-spacing: .5px;
  padding: 4px 3px;
  color: var(--txt-strong);
  background: transparent;
  border: 0;
  cursor: pointer;
  -webkit-appearance: none;
  appearance: none;
}
.res-select:focus-visible { outline: 2px solid var(--trim-hi); outline-offset: 1px; }
.res-select option { background: var(--e-ink); color: var(--txt-strong); font-family: inherit; }
.res-note {
  font-family: var(--font-pixel);
  font-size: 5.5px;
  letter-spacing: .4px;
  color: var(--txt-dim);
  white-space: nowrap;
}
/* the readout is the first thing worth dropping when the bar gets tight */
@media (max-width: 900px) { .res-note { display: none; } }
@media (max-width: 620px) { .res-ico { display: none; } }

/* ---------- HUD responsiveness ----------
   Four stages of graceful degradation, worst case last. The ordering matters:
   each stage gives up the least useful thing still on screen, and the counters
   themselves — the numbers a player actually reads — survive to the end. */

/* 1. The rail no longer fits beside the centre zone: give it its own full-width
      row and spread it out, rather than letting it clip. */
@media (max-width: 1180px) {
  .hud-right {
    flex: 1 1 100%;
    margin-left: 0;
    flex-direction: row;
    flex-wrap: wrap;
    align-items: center;
    justify-content: flex-end;
    gap: 6px;
  }
  .hud-right .pill-row { margin-right: auto; justify-content: flex-start; }
}

/* 2. Tighter: drop the pill captions and keep the values. A gold coin icon with
      a number next to it does not need the word GOLD over it. */
@media (max-width: 780px) {
  .hud { gap: var(--gap-sm) var(--gap-md); padding: var(--gap-sm) var(--gap-md); }
  .pill { padding: 4px 7px; gap: 5px; }
  .pill-label { display: none; }
  .pill-val { font-size: 10px; }
}

/* 3. The portrait's text block is the largest thing left that repeats
      information available elsewhere (level is on the badge, XP is on the ring). */
@media (max-width: 620px) {
  .hud-id { display: none; }
  .hud-mid { flex-basis: 180px; padding: 0; }
  .pill-ico { display: none; }
}

/* 4. Last resort: stack the zones. Nothing is hidden that has not already been
      hidden above, so this is purely a reflow. */
@media (max-width: 460px) {
  .hud-left { flex: 1 1 100%; }
  .hud-mid { flex: 1 1 100%; }
  .hud-right { justify-content: flex-start; }
  .hud-right .pill-row { margin-right: 0; }
}


/* --- CRT filter stack --- */
.crt-scan, .crt-mask, .crt-flicker, .crt-vig {
  position: absolute;
  inset: 0;
  pointer-events: none;
}
.crt-scan { background: repeating-linear-gradient(180deg, rgba(0,0,0,.20) 0 2px, rgba(0,0,0,0) 2px 4px); }
.crt-mask {
  background: repeating-linear-gradient(90deg,
    rgba(255,0,68,.05) 0 1px, rgba(44,232,245,.05) 1px 2px, rgba(99,199,77,.05) 2px 3px);
}
.crt-flicker { background: rgba(192,203,220,.035); animation: crt-flick 5.5s steps(2, end) infinite; }
.crt-vig {
  box-shadow:
    inset 0 0 46px rgba(11,9,18,.75),
    inset 0 0 120px rgba(11,9,18,.5),
    inset 0 0 8px rgba(0,0,0,.9);
}
@keyframes crt-flick {
  0%, 96%, 100% { opacity: .5; }
  97% { opacity: .95; }
  98% { opacity: .3; }
}

/* --- Render mode ---
   game.js owns this now: it stamps `gfx-crt` (or `gfx-crisp`) on <body> from the
   GRAPHICS panel and writes the choice straight to localStorage. CRISP is the
   default, so the filter layers are opt-in rather than opt-out.

   The vignette is deliberately excluded from the toggle: it frames the art
   rather than filtering it, so it stays on in both modes. */
body:not(.gfx-crt) .crt-scan,
body:not(.gfx-crt) .crt-mask,
body:not(.gfx-crt) .crt-flicker { display: none; }

/* Crisp mode leans the other way: hard nearest-neighbour everywhere and a
   slightly tighter vignette, so the pixel grid is the whole picture. */
body.gfx-crisp canvas { image-rendering: pixelated; image-rendering: crisp-edges; }
body.gfx-crisp .crt-vig {
  box-shadow:
    inset 0 0 30px rgba(11,9,18,.55),
    inset 0 0 90px rgba(11,9,18,.34);
}
/* CRT mode pushes the grille and glow a little harder than the base stack. */
body.gfx-crt .crt-scan {
  background: repeating-linear-gradient(180deg, rgba(0,0,0,.30) 0 2px, rgba(0,0,0,0) 2px 4px);
}
body.gfx-crt .crt-mask {
  background: repeating-linear-gradient(90deg,
    rgba(255,0,68,.075) 0 1px, rgba(44,232,245,.075) 1px 2px, rgba(99,199,77,.075) 2px 3px);
}
body.gfx-crt .stage-inner,
body.gfx-crt .town-stage { box-shadow: 0 0 0 3px #0d0a14, inset 0 0 0 2px var(--e-slate), 0 0 22px rgba(44,232,245,.10); }

.stage-overlay { position: absolute; inset: 0; pointer-events: none; line-height: 1.4; }

/* combat bars — the hero and enemy plates take 12 chunks each */
.bar-stack { position: absolute; left: 8px; right: 8px; bottom: 8px; display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.bar-head { display: flex; align-items: baseline; justify-content: space-between; gap: 6px; margin-bottom: 3px; }
.bar-name, .bar-text { font-family: var(--font-pixel); font-size: 7px; letter-spacing: 1px; line-height: 1.6; text-shadow: 0 2px 0 #000; }
.bar-name { color: var(--e-white); }
.bar-text { color: var(--e-silver); }

/* ---------- segmented arcade bars ----------
   EVERY progress bar in the game is segmented, and it is done in one place.

   The mechanism: .bar is the well, .bar-fill is the coloured mass, and .bar::after
   lays a comb of 1px opaque teeth over the top. Because the teeth are painted
   ABOVE the fill and match the well's colour, a continuous fill reads as discrete
   chunks with no extra elements and — crucially — no JS involvement: game.js
   still writes exactly one width per bar, as it always did.

   --seg is the segment count, so a bar can be re-cut per instance: the hero and
   enemy bars use 12, the boss plate 10, the EXP strip 20. */
.bar {
  --seg: 12;
  position: relative;
  height: 12px;
  background: var(--jrpg-bg2);
  box-shadow: var(--pxframe), inset 0 2px 0 rgba(0, 0, 0, .55);
  overflow: hidden;
}
.bar-fill {
  height: 100%;
  width: 100%;
  transition: width .22s steps(10, end);
  box-shadow: inset 0 3px 0 rgba(255, 255, 255, .24), inset 0 -3px 0 rgba(0, 0, 0, .3);
}
/* the comb. calc(100% / var(--seg)) is one segment; the last 1px of each is a
   tooth, which is what produces the 1px gap between chunks. */
.bar::after {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: repeating-linear-gradient(90deg,
    transparent 0 calc(100% / var(--seg) - 1px),
    var(--jrpg-bg2) calc(100% / var(--seg) - 1px) calc(100% / var(--seg)));
}
.fill-player { background: var(--e-green); }
.fill-enemy  { background: var(--e-red); }
.fill-boss   { background: var(--e-hot); }
.fill-forge  { background: var(--jrpg-gold); }

/* ---------- boss banner ----------
   Centred at the top of the canvas, wearing a 2px pixel frame and an ornate
   crest. The crest is the ::before of the panel rather than markup, so the boss
   plate stays a two-element structure that game.js can keep writing blind. */
.boss-panel {
  position: absolute;
  left: 50%; top: 14px;
  transform: translateX(-50%);
  width: min(330px, 80%);
  padding: 9px 12px 9px;
  background: linear-gradient(180deg, #2a2030 0%, #1d1826 100%);
  box-shadow:
    0 0 0 2px var(--jrpg-edge),
    0 0 0 4px var(--e-hot),
    inset 2px 2px 0 rgba(255, 255, 255, .09),
    inset -2px -2px 0 rgba(0, 0, 0, .5),
    0 5px 0 rgba(0, 0, 0, .5);
  text-align: center;
}
/* the crest: a skull on a notched plate, straddling the top frame */
.boss-panel::before {
  content: '\1F480';
  position: absolute;
  top: -13px;
  left: 50%;
  transform: translateX(-50%);
  width: 26px;
  height: 22px;
  font-size: 13px;
  line-height: 22px;
  text-align: center;
  background: var(--e-hot);
  color: #1a0a10;
  /* the notch: clipping the lower corners turns the plate into a crest rather
     than a floating box */
  clip-path: polygon(18% 0, 82% 0, 100% 26%, 86% 100%, 14% 100%, 0 26%);
  box-shadow: 0 0 0 2px var(--jrpg-edge);
}
.boss-name {
  font-family: var(--font-pixel);
  font-size: 8px;
  letter-spacing: 2px;
  line-height: 1.7;
  color: var(--jrpg-gold-hi);
  text-shadow: 0 2px 0 #000;
  margin: 3px 0 6px;
  overflow-wrap: break-word;
}
/* the boss plate is cut into 10 chunky segments — compound for the same
   order-independence reason as .bar.bar-xp above */
.bar.bar-boss { --seg: 10; height: 15px; }

/* floating message */
.game-msg {
  position: absolute;
  left: 50%; top: 34%;
  transform: translate(-50%, 10px);
  width: max-content;
  max-width: 88%;
  padding: 8px 12px;
  background: rgba(24,20,37,.9);
  box-shadow: 0 0 0 3px var(--trim-hi);
  font-family: var(--font-pixel);
  font-size: 9px;
  line-height: 1.6;
  letter-spacing: 1px;
  color: var(--trim-hi);
  text-align: center;
  opacity: 0;
  transition: opacity .25s ease, transform .25s ease;
}
.game-msg.visible { opacity: 1; transform: translate(-50%, 0); }

/* DOM hit-text layer (percentage positioned by JS).
   Bold multi-layer drop shadow so numbers stay legible over any biome, and a
   two-stage pop on crits: overshoot, settle, then drift up and fade. */
.hit-layer { position: absolute; inset: 0; pointer-events: none; z-index: 3; }
.hit-text {
  position: absolute;
  transform: translate(-50%, 0);
  font-family: var(--font-pixel);
  font-size: 10px;
  line-height: 1;
  color: var(--e-rose);
  /* stacked 1px offsets read as a hard pixel outline, not a blur */
  text-shadow:
    1px 0 0 #000, -1px 0 0 #000, 0 1px 0 #000, 0 -1px 0 #000,
    0 3px 0 rgba(0, 0, 0, .55), 0 0 9px rgba(0, 0, 0, .75);
  animation: hit-rise .9s cubic-bezier(.2, .8, .2, 1) forwards;
  will-change: transform, opacity;
}
.hit-text.is-crit {
  font-size: 16px;
  color: var(--trim-hi);
  text-shadow:
    1px 0 0 #000, -1px 0 0 #000, 0 1px 0 #000, 0 -1px 0 #000,
    0 3px 0 var(--e-brick), 0 0 14px rgba(254, 231, 97, .85);
  animation: hit-pop 1.05s cubic-bezier(.2, .8, .2, 1) forwards;
}
.hit-text.is-heal { color: var(--e-green); }
.hit-text.is-block { color: var(--e-cyan); }
@keyframes hit-rise {
  0%   { opacity: 0; transform: translate(-50%, 6px) scale(.8); }
  18%  { opacity: 1; transform: translate(-50%, 0) scale(1.1); }
  30%  { transform: translate(-50%, -4px) scale(1); }
  100% { opacity: 0; transform: translate(-50%, -34px) scale(1); }
}
@keyframes hit-pop {
  0%   { opacity: 0; transform: translate(-50%, 8px) scale(.5) rotate(-6deg); }
  14%  { opacity: 1; transform: translate(-50%, -2px) scale(1.5) rotate(3deg); }
  26%  { transform: translate(-50%, -6px) scale(1.05) rotate(-1deg); }
  38%  { transform: translate(-50%, -9px) scale(1.2); }
  100% { opacity: 0; transform: translate(-50%, -46px) scale(1); }
}

/* ---------- 8. Skill dock — three square slots + an auto-cast switch ----------
   The dock used to stretch the slots across the whole stage width (a 1fr grid),
   which at 1280px made three enormous lozenges. It is now CENTRED and the slots
   are a fixed square size, so the dock reads the same at every resolution.

   Two rows: the buff strip spans the top, then one centred row holding the three
   slots and the switch. */
.skill-dock {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  padding: var(--gap-md);
}
.dock-row {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 14px;
  flex-wrap: wrap;
}
.buff-strip { align-self: stretch; }
.skill-bar { display: flex; align-items: center; justify-content: center; gap: 12px; }

/* ---------------------------------------------------------------------------
   SKILL SLOT — 64x64 pixel-art plate, not an HTML button.

   Every trace of the browser's button chrome is stripped (appearance, border,
   background, font inheritance) and replaced with the four things that make a
   slot read as inventory furniture rather than as a control:

     ground   a flat #1a1c23 well — the theme's own page colour, so the slot
              reads as a HOLE cut in the dock rather than a raised lozenge
     frame    a real 2px solid #00ffff border on all four sides
     bevel    inset highlight top/left + inset shade bottom/right, 3px each, so
              the well has depth without a gradient
     shell    a 2px black keyline outside the cyan, which is what stops the
              cyan from bleeding into the dock's own panel colour

   No clip-path, no border-radius: the square IS the shape, and box-shadow is
   used for the bevel so the cyan glow can spill outside the box.
   --------------------------------------------------------------------------- */
.skill-btn {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  width: 64px;
  height: 64px;
  min-width: 64px;
  min-height: 64px;
  flex: 0 0 auto;
  padding: 0;
  margin: 0;
  cursor: pointer;
  overflow: visible;
  /* kill the UA button entirely */
  -webkit-appearance: none;
  appearance: none;
  font: inherit;
  color: var(--txt-strong);
  /* the square dark well + its cyan frame */
  background: #1a1c23;
  border: 2px solid #00ffff;
  box-shadow:
    /* inset bevels: lit from the top-left, shaded to the bottom-right */
    inset 3px 3px 0 rgba(255, 255, 255, .14),
    inset -3px -3px 0 rgba(0, 0, 0, .62),
    inset 0 0 0 1px rgba(0, 255, 255, .18),
    /* black keyline outside the cyan so the frame never bleeds into the dock */
    0 0 0 2px var(--jrpg-edge),
    0 3px 0 0 rgba(0, 0, 0, .5);
  transition: transform .08s steps(2), filter .15s linear, box-shadow .18s linear;
}
/* Ready-state CYAN border glow. Driven by .is-ready from updateHUD, so it lights
   only when the skill can actually be pressed — a dock where everything always
   glows tells the player nothing. */
.skill-btn.is-ready {
  border-color: #7cf6ff;
  box-shadow:
    inset 3px 3px 0 rgba(255, 255, 255, .18),
    inset -3px -3px 0 rgba(0, 0, 0, .58),
    inset 0 0 0 1px rgba(0, 255, 255, .3),
    0 0 0 2px var(--jrpg-edge),
    0 0 14px 2px rgba(0, 255, 255, .5),
    0 3px 0 0 rgba(0, 0, 0, .5);
  animation: slot-glow 1.9s ease-in-out infinite;
}
@keyframes slot-glow {
  0%, 100% {
    box-shadow:
      inset 3px 3px 0 rgba(255, 255, 255, .16),
      inset -3px -3px 0 rgba(0, 0, 0, .6),
      inset 0 0 0 1px rgba(0, 255, 255, .22),
      0 0 0 2px var(--jrpg-edge),
      0 0 9px 1px rgba(0, 255, 255, .34),
      0 3px 0 0 rgba(0, 0, 0, .5);
  }
  50% {
    box-shadow:
      inset 3px 3px 0 rgba(255, 255, 255, .22),
      inset -3px -3px 0 rgba(0, 0, 0, .56),
      inset 0 0 0 1px rgba(0, 255, 255, .38),
      0 0 0 2px var(--jrpg-edge),
      0 0 20px 5px rgba(0, 255, 255, .62),
      0 3px 0 0 rgba(0, 0, 0, .5);
  }
}
@media (prefers-reduced-motion: reduce) {
  .skill-btn.is-ready { animation: none; }
}
.skill-btn:hover { filter: brightness(1.16) saturate(1.08); transform: translateY(-2px); }
.skill-btn:active { transform: translateY(2px); }
.skill-btn:focus-visible { outline: 3px solid var(--trim-hi); outline-offset: 4px; }

.skill-icon {
  font-size: 22px;
  line-height: 1;
  filter: drop-shadow(0 2px 0 rgba(0, 0, 0, .7));
  pointer-events: none;
}
.skill-name {
  max-width: 60px;
  font-family: var(--font-pixel);
  font-size: 5px;
  letter-spacing: .5px;
  line-height: 1.6;
  text-align: center;
  color: var(--e-cream);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  pointer-events: none;
}
/* Hotkey badge, bracketed like the brief asks: [1] [2] [3], absolutely
   positioned at the TOP LEFT of the square. Nudged out by 4px on both axes so it
   straddles the cyan frame and reads as a tab welded onto the slot rather than a
   label floating inside it. */
.skill-key {
  position: absolute;
  top: -4px;
  left: -4px;
  z-index: 4;
  padding: 1px 3px;
  font-family: var(--font-pixel);
  font-size: 6px;
  letter-spacing: .5px;
  line-height: 1.5;
  color: #00ffff;
  background: var(--jrpg-edge);
  box-shadow: 0 0 0 2px #00ffff, 0 2px 0 rgba(0, 0, 0, .5);
  pointer-events: none;
}
.skill-key::before { content: '['; }
.skill-key::after { content: ']'; }

/*
  CIRCULAR radial cooldown sweep, inset inside the square slot. game.js sets
  --cd (0-100) every HUD tick and the conic gradient wipes from 12 o'clock; the
  circular mask is what makes it read as a clock face rather than a corner-filling
  wedge, which is what a conic gradient on a bare square looks like.
*/
.skill-btn::before {
  content: '';
  position: absolute;
  /* relative to the PADDING box, so this already sits inside the 2px cyan
     frame — 4px keeps the bevel highlight readable around the clock face */
  inset: 4px;
  z-index: 1;
  pointer-events: none;
  border-radius: 50%;
  background: conic-gradient(from -90deg,
    rgba(8, 6, 14, .82) calc(var(--cd, 0) * 1%), transparent 0);
}
/* the countdown readout — empty (and therefore invisible) when off cooldown */
.skill-cd {
  position: absolute;
  left: 50%;
  bottom: 16%;
  transform: translateX(-50%);
  z-index: 2;
  font-family: var(--font-pixel);
  font-size: 7px;
  letter-spacing: .4px;
  line-height: 1;
  color: var(--e-white);
  text-shadow: 0 0 4px rgba(0, 0, 0, .9), 0 2px 0 rgba(0, 0, 0, .7);
  pointer-events: none;
}
.skill-btn.on-cd { filter: grayscale(.55) brightness(.82); }
/* while counting down, the label gives way to the number */
.skill-btn.on-cd .skill-name { opacity: 0; }
/* Active (toggled-on) skill keeps the square well and swaps the frame to gold, so
   "running" and "ready" are told apart by COLOUR rather than by shape. */
.skill-btn.active-skill {
  background: #1c2a1e;
  border-color: var(--trim-hi);
  box-shadow:
    inset 3px 3px 0 rgba(255, 255, 255, .18),
    inset -3px -3px 0 rgba(0, 0, 0, .55),
    inset 0 0 0 1px rgba(254, 231, 97, .3),
    0 0 0 2px var(--jrpg-edge),
    0 0 14px 2px rgba(254, 231, 97, .45),
    0 3px 0 0 rgba(0, 0, 0, .5);
  animation: slot-live 1.2s ease-in-out infinite;
}
@keyframes slot-live {
  0%, 100% { filter: brightness(1); }
  50%      { filter: brightness(1.2); }
}

/* ---------- active buff / aura strip ----------
   One chip per running effect, above the slots. Skill auras, town draughts and
   the Aegis absorb pool each get their own accent so the source is readable
   without hovering. Scrolls sideways rather than wrapping, so a stack of eight
   buffs never pushes the skill slots down mid-fight. */
.buff-strip {
  display: flex;
  align-items: center;
  gap: 5px;
  min-height: 22px;
  padding: 2px 1px;
  overflow-x: auto;
  overflow-y: hidden;
  scrollbar-width: none;
}
.buff-strip::-webkit-scrollbar { height: 0; }
.buff-strip.is-empty { display: none; }
.buff-chip {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  flex: 0 0 auto;
  padding: 3px 6px;
  background: rgba(0, 0, 0, .38);
  box-shadow: inset 0 0 0 2px var(--line);
  animation: buff-in .18s steps(3);
}
.buff-chip-ico { font-size: 10px; line-height: 1; }
.buff-chip-val {
  font-family: var(--font-pixel);
  font-size: 6px;
  font-weight: 400;
  letter-spacing: .4px;
  color: var(--e-cream);
}
.buff-chip.is-skill { box-shadow: inset 0 0 0 2px var(--e-green); }
.buff-chip.is-skill .buff-chip-val { color: var(--e-green); }
.buff-chip.is-town { box-shadow: inset 0 0 0 2px var(--e-amber); }
.buff-chip.is-town .buff-chip-val { color: var(--trim-hi); }
.buff-chip.is-ward { box-shadow: inset 0 0 0 2px var(--e-sky); }
.buff-chip.is-ward .buff-chip-val { color: var(--e-cyan); }
@keyframes buff-in {
  from { opacity: 0; transform: translateY(4px); }
  to   { opacity: 1; transform: translateY(0); }
}
@media (prefers-reduced-motion: reduce) {
  .buff-chip { animation: none; }
}

/* ---------- AUTO-CAST toggle switch ----------
   Was a fourth slot that looked exactly like a skill but behaved like a
   checkbox. It is now an actual switch, which is what it always was: a persistent
   on/off state, not a thing you press to fire.

   Lives outside #skill-bar because buildSkillBar() wipes that container's
   innerHTML. game.js keeps driving #auto-skill and toggling .is-locked on this
   wrapper, so none of its wiring changed. */
.auto-switch {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 7px;
  flex: 0 0 auto;
  padding: 7px 10px;
  cursor: pointer;
  background: rgba(0, 0, 0, .34);
  box-shadow: 0 0 0 2px var(--e-black), 0 0 0 4px #2f3b5e, inset 0 2px 0 rgba(255, 255, 255, .06);
  transition: box-shadow .18s linear, filter .18s linear;
  -webkit-tap-highlight-color: transparent;
}
/* the real control, kept in the accessibility tree but visually replaced */
.auto-switch input {
  position: absolute;
  opacity: 0;
  width: 1px;
  height: 1px;
  pointer-events: none;
}
.auto-key {
  font-family: var(--font-pixel);
  font-size: 5.5px;
  letter-spacing: .5px;
  line-height: 1.5;
  color: var(--e-cyan);
}
.auto-key::before { content: '['; }
.auto-key::after { content: ']'; }
/* The status LED: a 6px pixel lamp that is dark when off and pulses when
   auto-cast is actually firing, so the dock says at a glance whether the hero is
   casting on his own. */
.auto-led {
  width: 6px;
  height: 6px;
  flex: 0 0 auto;
  background: #2a2f3c;
  box-shadow: 0 0 0 1px var(--jrpg-edge), inset 0 1px 0 rgba(255, 255, 255, .12);
  transition: background .18s linear, box-shadow .18s linear;
}
.auto-label {
  font-family: var(--font-pixel);
  font-size: 6px;
  letter-spacing: .8px;
  line-height: 1.6;
  color: var(--txt-dim);
  transition: color .18s linear;
}
/* track + knob, both on the pixel grid (no border-radius) */
.auto-track {
  position: relative;
  width: 28px;
  height: 14px;
  flex: 0 0 auto;
  background: #141827;
  box-shadow: inset 0 0 0 2px #3a4466;
  transition: box-shadow .18s linear, background .18s linear;
}
.auto-knob {
  position: absolute;
  top: 2px;
  left: 2px;
  width: 10px;
  height: 10px;
  background: var(--e-steel);
  transition: transform .16s steps(3), background .18s linear;
}
.auto-switch:hover { filter: brightness(1.14); }
.auto-switch:focus-within {
  box-shadow: 0 0 0 2px var(--e-black), 0 0 0 4px var(--trim-hi), inset 0 2px 0 rgba(255, 255, 255, .06);
}

/* ---- on ---- */
.auto-switch:has(input:checked) .auto-track {
  background: #0b3a63;
  box-shadow: inset 0 0 0 2px var(--e-cyan);
}
.auto-switch:has(input:checked) .auto-knob {
  transform: translateX(14px);
  background: var(--e-cyan);
}
.auto-switch:has(input:checked) .auto-label { color: var(--e-cyan); }
.auto-switch:has(input:checked) {
  box-shadow:
    0 0 0 2px var(--jrpg-edge),
    0 0 0 4px var(--e-cyan),
    0 0 10px 1px rgba(44, 232, 245, .35),
    inset 0 2px 0 rgba(255, 255, 255, .06);
}
/* the LED lights and breathes only while auto-cast is live */
.auto-switch:has(input:checked) .auto-led {
  background: var(--e-cyan);
  box-shadow: 0 0 0 1px var(--jrpg-edge), 0 0 7px 2px rgba(44, 232, 245, .75);
  animation: led-pulse 1.15s ease-in-out infinite;
}
@keyframes led-pulse {
  0%, 100% { opacity: 1;   box-shadow: 0 0 0 1px var(--jrpg-edge), 0 0 5px 1px rgba(44, 232, 245, .6); }
  50%      { opacity: .55; box-shadow: 0 0 0 1px var(--jrpg-edge), 0 0 11px 3px rgba(44, 232, 245, .95); }
}
@media (prefers-reduced-motion: reduce) {
  .auto-switch:has(input:checked) .auto-led { animation: none; }
}

/* ---- locked until the unlock level ---- */
.auto-switch.is-locked { filter: grayscale(1) brightness(.62); cursor: not-allowed; }
.auto-switch.is-locked:hover { filter: grayscale(1) brightness(.62); }

/* :has() is well supported now, but a browser without it would leave the switch
   permanently in its off state. This fallback drives the same visuals off the
   checkbox's sibling order instead, which needs no :has(). */
.auto-switch input:checked ~ .auto-track { background: #0b3a63; box-shadow: inset 0 0 0 2px var(--e-cyan); }
.auto-switch input:checked ~ .auto-track .auto-knob { transform: translateX(14px); background: var(--e-cyan); }
.auto-switch input:checked ~ .auto-label { color: var(--e-cyan); }
.auto-switch input:checked ~ .auto-led {
  background: var(--e-cyan);
  box-shadow: 0 0 0 1px var(--jrpg-edge), 0 0 7px 2px rgba(44, 232, 245, .75);
}

@media (max-width: 460px) {
  .auto-label { display: none; }
}

/* ---------- 9. Sidebar: sub-tabs, buy dock, panels ---------- */
.sidebar { min-width: 0; display: grid; gap: var(--gap-lg); }
.shop { padding: 0; overflow: visible; }

/* --- character sub-tabs ---
   Only revealed while one of the five character panels is on screen. :has()
   lets the shell react to which .tab-panel game.js unhid, with no JS of its own. */
.sub-tabs {
  display: none;
  grid-template-columns: repeat(5, minmax(0, 1fr));
  gap: 3px;
  padding: var(--gap-sm);
  background: var(--e-black);
}
body:has(#tab-attributes:not(.hidden)) .sub-tabs,
body:has(#tab-heroes:not(.hidden)) .sub-tabs,
body:has(#tab-companions:not(.hidden)) .sub-tabs,
body:has(#tab-tower:not(.hidden)) .sub-tabs,
body:has(#tab-dungeon:not(.hidden)) .sub-tabs { display: grid; }

/* Taller than they were, and stacked rather than inline. At 6px the Turkish
   labels (ÖZELLİK, YOLDAŞ) are long enough that icon-beside-text forced them to
   ellipsise; putting the icon above the label gives the text the full button
   width and the extra height keeps the descenders off the bottom edge. */
.sub-btn {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 4px;
  min-height: 50px;
  padding: 9px 5px 8px;
  background: rgba(255, 255, 255, .04);
  color: var(--txt-dim);
  font-size: 6px;
  letter-spacing: .4px;
  line-height: 1.6;
  text-align: center;
  transition: background .18s linear, color .18s linear, transform .08s steps(2);
}
.sub-txt { display: block; max-width: 100%; overflow-wrap: break-word; }
.sub-ico { font-size: 11px; line-height: 1; filter: grayscale(.6); transition: filter .18s linear; }
.sub-btn:hover { color: var(--txt-strong); background: rgba(255, 255, 255, .09); }
.sub-btn:hover .sub-ico { filter: none; }
.sub-btn:active { transform: translateY(2px); }
.sub-btn.active {
  background: var(--panel-bg2);
  color: var(--trim-hi);
  box-shadow: inset 0 -3px 0 var(--trim-hi), inset 0 2px 0 rgba(255, 255, 255, .1);
}
.sub-btn.active .sub-ico { filter: none; }

/* --- buy-amount dock, floating just above the active view --- */
.buy-dock {
  position: relative;
  z-index: 2;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--gap-md);
  margin: 0 var(--gap-md) calc(var(--gap-md) * -1);
  padding: var(--gap-sm) var(--gap-md);
  background: linear-gradient(180deg, #262d49, #14111f);
  box-shadow:
    0 0 0 3px var(--line),
    inset 0 3px 0 rgba(255, 255, 255, .09),
    inset 0 -3px 0 rgba(0, 0, 0, .45),
    0 5px 0 0 rgba(0, 0, 0, .35);
  transform: translateY(calc(var(--gap-md) * -0.5));
}
.dock-label { font-family: var(--font-pixel); font-size: 6.5px; letter-spacing: 1.5px; color: var(--txt-dim); }
.mult-ctl { display: flex; gap: 3px; }
.mult-btn {
  font-family: var(--font-pixel);
  border: 0;
  cursor: pointer;
  background: var(--e-slate);
  color: var(--txt-dim);
  font-size: 7px;
  letter-spacing: .5px;
  padding: 7px 8px;
  min-width: 36px;
  box-shadow: inset 0 2px 0 rgba(255, 255, 255, .12), inset 0 -3px 0 rgba(0, 0, 0, .34), 0 0 0 2px var(--line);
  transition: background .16s linear, color .16s linear, transform .08s steps(2);
}
.mult-btn:hover { color: var(--txt-strong); filter: brightness(1.18); }
.mult-btn:active { transform: translateY(2px); }
.mult-btn.active {
  background: var(--e-cyan);
  color: var(--e-black);
  box-shadow: inset 0 2px 0 rgba(255, 255, 255, .3), inset 0 -3px 0 rgba(0, 0, 0, .3), 0 0 0 2px var(--line), 0 0 12px rgba(44, 232, 245, .55);
}
.mult-btn.is-max.active {
  background: var(--trim-hi);
  box-shadow: inset 0 2px 0 rgba(255, 255, 255, .35), inset 0 -3px 0 rgba(0, 0, 0, .3), 0 0 0 2px var(--line), 0 0 12px rgba(254, 231, 97, .6);
}
.mult-btn:focus-visible { outline: 3px solid var(--trim-hi); outline-offset: 2px; }

/* --- notification badge: red dot when a tab holds an affordable action --- */
.tab-badge {
  position: absolute;
  top: 5px;
  right: 5px;
  width: 9px;
  height: 9px;
  background: var(--alert);
  box-shadow: 0 0 0 2px var(--e-black), 0 0 8px rgba(228, 59, 68, .85);
  opacity: 0;
  transform: scale(.4);
  transition: opacity .18s linear, transform .18s cubic-bezier(.2,.8,.2,1);
  pointer-events: none;
}
.tab-btn.has-alert .tab-badge { opacity: 1; transform: scale(1); animation: badge-pip 1.5s ease-in-out infinite; }
@keyframes badge-pip {
  0%, 100% { box-shadow: 0 0 0 2px var(--e-black), 0 0 5px rgba(228, 59, 68, .7); }
  50%      { box-shadow: 0 0 0 2px var(--e-black), 0 0 13px rgba(246, 117, 122, 1); }
}
/* the badge stays visible on the open tab but stops shouting */
.tab-btn.active.has-alert .tab-badge { animation: none; background: var(--e-rose); }

/* The panel body scrolls, but its ceiling is now viewport-relative instead of a
   flat 486px. That fixed number was cutting the bottom off the taller panels —
   the Tower's relic-bonus block and the talent hints under it — on any window
   shorter than about 700px, with no way to reach them. */
.tab-body {
  /* A flex COLUMN, not a block. Panels are laid out one under another and each
     keeps its own natural height (see .tab-body > .tab-panel below); when the
     stack is taller than the scrollport the panel scrolls instead of its blocks
     being compressed into one another. */
  display: flex;
  flex-direction: column;
  align-items: stretch;
  padding: var(--gap-lg);
  max-height: clamp(320px, 62vh, 640px);
  overflow-y: auto;
  /* momentum scrolling on touch, and never a horizontal bar.
     This stays `hidden` on purpose. `overflow-x: visible` is NOT available
     here: CSS computes `visible` to `auto` on one axis as soon as the other
     axis scrolls, so asking for visible would only add a horizontal scrollbar
     and would still clip. Tooltips escape this box by being cloned into
     #tip-layer on <body> instead — see initTipPortal() in game.js. */
  overflow-x: hidden;
  -webkit-overflow-scrolling: touch;
}
/* A flex item defaults to flex-shrink: 1, which is what let a tall panel — the
   forge, with its anvil + success meter + actions + sockets + gems — get
   squeezed toward the scrollport height so its blocks bled into each other.
   Panels now refuse to shrink and simply overflow into the scrollbar. */
.tab-body > .tab-panel {
  flex: 0 0 auto;
  min-height: fit-content;
  min-width: 0;
}
.tab-panel:not(.hidden) { animation: panel-in .24s cubic-bezier(.2,.8,.2,1) both; }
@keyframes panel-in {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 1; transform: translateY(0); }
}

.tab-body::-webkit-scrollbar { width: 10px; }
.tab-body::-webkit-scrollbar-track { background: var(--e-black); }
.tab-body::-webkit-scrollbar-thumb { background: var(--e-steel); box-shadow: inset 2px 2px 0 rgba(255,255,255,.14); }
.tab-body { scrollbar-width: thin; scrollbar-color: var(--e-steel) var(--e-black); }

.section-rule { height: 3px; margin: 14px 0; background: linear-gradient(90deg, transparent, var(--e-steel), transparent); }

/* ---------- 9c. Marketplace, arena and the chat dock ----------
   All three are new surfaces built out of the existing pixel vocabulary — the
   same 2px frames, the same .buy-btn, the same .item-tip — so nothing here
   introduces a second visual language. */

.mkt-mode {
  font-family: var(--font-pixel);
  font-size: 6px;
  letter-spacing: .8px;
  line-height: 1.7;
  color: var(--jrpg-gold);
  white-space: nowrap;
}
.mkt-tabs { display: flex; gap: 3px; margin-bottom: 9px; }
.mkt-tab {
  flex: 1 1 0;
  min-width: 0;
  padding: 9px 6px;
  border: 0;
  cursor: pointer;
  background: rgba(255, 255, 255, .04);
  color: var(--txt-dim);
  font-family: var(--font-pixel);
  font-size: 6px;
  letter-spacing: .6px;
  line-height: 1.7;
  transition: background .18s linear, color .18s linear;
}
.mkt-tab:hover { color: var(--txt-strong); background: rgba(255, 255, 255, .08); }
.mkt-tab.active {
  background: var(--panel-bg2);
  color: var(--jrpg-gold-hi);
  box-shadow: inset 0 -3px 0 var(--jrpg-gold);
}

.mkt-filters { display: flex; flex-wrap: wrap; gap: 5px; margin-bottom: 8px; }
.mkt-search {
  flex: 1 1 120px;
  min-width: 0;
  padding: 7px 8px;
  font-family: inherit;
  font-size: 10px;
  color: var(--txt-strong);
  background: var(--jrpg-bg2);
  border: 0;
  box-shadow: inset 0 0 0 2px var(--jrpg-edge);
}
.mkt-search:focus-visible { outline: none; box-shadow: inset 0 0 0 2px var(--jrpg-gold); }
.mkt-select { flex: 0 1 auto; width: auto; min-width: 84px; margin-top: 0; }
.mkt-filters .inv-btn { flex: 0 0 auto; }

.mkt-status { min-height: 1.5em; margin-bottom: 7px; font-size: 9.5px; line-height: 1.6; color: var(--txt-dim); }
.mkt-status.is-win { color: var(--e-green); }
.mkt-status.is-fail { color: var(--e-rose); }

.mkt-body { display: grid; gap: 8px; }
.mkt-list { display: grid; gap: 6px; }

/* One listing. position:relative so the .item-tip has something to anchor to. */
.mkt-row {
  position: relative;
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
  gap: 8px;
  padding: 7px 8px;
  background: rgba(0, 0, 0, .26);
  box-shadow: inset 0 0 0 2px var(--jrpg-edge), inset 2px 2px 0 rgba(255, 255, 255, .04);
}
.mkt-row:hover { filter: brightness(1.12); }
/* The rarity classes already set --r elsewhere; borrow it for the left edge.
   Spelled out rather than matched with [class*='rar-'] — the classes are
   .r-common … .r-mythic, so a substring match on 'rar-' silently never fires and
   every listing on the board loses its rarity rule. */
.mkt-row:is(.r-common, .r-uncommon, .r-rare, .r-epic, .r-legendary, .r-mythic) {
  box-shadow: inset 0 0 0 2px var(--jrpg-edge), inset 4px 0 0 var(--r, var(--e-steel));
}
.mkt-ico { display: flex; align-items: center; justify-content: center; width: 30px; height: 30px; font-size: 15px; }
.mkt-ico canvas { display: block; image-rendering: pixelated; width: 30px; height: 30px; }
.mkt-row-body { min-width: 0; }
/* Declared explicitly rather than inherited from <div>: the arena's rival rows
   are <button> elements, whose content model is phrasing-only, so these two are
   spans there and divs in the market. Both must lay out the same. */
.mkt-row-name {
  display: block;
  font-family: var(--font-pixel);
  font-size: 6.5px;
  letter-spacing: .4px;
  line-height: 1.7;
  color: var(--txt-strong);
  overflow-wrap: break-word;
}
.mkt-row-sub { display: block; margin-top: 3px; font-size: 9px; line-height: 1.55; color: var(--txt-dim); overflow-wrap: break-word; }
.mkt-row-foot { display: flex; flex-direction: column; align-items: flex-end; gap: 5px; flex: 0 0 auto; }
.mkt-price {
  font-family: var(--font-pixel);
  font-size: 7px;
  line-height: 1.6;
  color: var(--jrpg-gold-hi);
  white-space: nowrap;
}
/* the row's tooltip opens above and is right-aligned, clear of the buy button */
.mkt-row > .item-tip { bottom: calc(100% + 6px); left: auto; right: 0; transform: translateY(4px); }
.mkt-row:hover > .item-tip,
.mkt-row:focus-within > .item-tip { opacity: 1; visibility: visible; transform: translateY(0); }

.mkt-pager { display: flex; align-items: center; justify-content: center; gap: 8px; }
.mkt-pager .inv-btn { min-width: 44px; }
.mkt-page-at { font-family: var(--font-pixel); font-size: 6.5px; line-height: 1.7; color: var(--txt-dim); }

.mkt-ledger {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  flex-wrap: wrap;
  padding: 8px 9px;
  font-size: 9.5px;
  line-height: 1.6;
  color: var(--txt-strong);
  background: rgba(0, 0, 0, .28);
  box-shadow: inset 0 0 0 2px var(--jrpg-edge);
}
.mkt-compose {
  display: grid;
  gap: 6px;
  padding: 8px 9px;
  background: rgba(0, 0, 0, .22);
  box-shadow: inset 0 0 0 2px var(--jrpg-edge);
}
.mkt-kind { display: flex; gap: 4px; }
.mkt-kind-btn {
  flex: 1 1 0;
  padding: 6px 5px;
  border: 0;
  cursor: pointer;
  background: rgba(255, 255, 255, .05);
  color: var(--txt-dim);
  font-family: var(--font-pixel);
  font-size: 6px;
  line-height: 1.7;
}
.mkt-kind-btn.active { background: var(--jrpg-gold); color: var(--jrpg-edge); }
.mkt-compose-foot { display: flex; align-items: center; justify-content: space-between; gap: 8px; flex-wrap: wrap; }
.mkt-fee-note { font-size: 9px; line-height: 1.6; color: var(--txt-dim); }

/* ---- arena ---- */
.ar-stats { display: flex; flex-wrap: wrap; gap: 5px; margin-bottom: 9px; }
.ar-stat {
  display: flex;
  flex-direction: column;
  gap: 2px;
  flex: 1 1 68px;
  min-width: 0;
  padding: 6px 7px;
  background: rgba(0, 0, 0, .26);
  box-shadow: inset 0 0 0 2px var(--jrpg-edge);
}
.ar-stat-k { font-family: var(--font-pixel); font-size: 5.5px; letter-spacing: .8px; line-height: 1.7; color: var(--txt-dim); }
.ar-stat b { font-family: var(--font-pixel); font-size: 8px; font-weight: 400; line-height: 1.5; color: var(--jrpg-gold-hi); }
.ar-me, .ar-report {
  padding: 7px 9px;
  font-size: 9.5px;
  line-height: 1.6;
  background: rgba(0, 0, 0, .24);
  box-shadow: inset 0 0 0 2px var(--jrpg-edge);
  overflow-wrap: break-word;
}
.ar-me { color: var(--txt-dim); }
.ar-report.is-win { color: var(--e-green); box-shadow: inset 0 0 0 2px var(--e-forest); }
.ar-report.is-fail { color: var(--e-rose); box-shadow: inset 0 0 0 2px var(--e-crimson); }

/* Arena points get the gold treatment the rating does not: rating is plumbing
   (it only picks opponents), points are the score. */
.ar-stat.is-ap { box-shadow: inset 0 0 0 2px var(--jrpg-gold); }
.ar-stat.is-ap b { color: var(--jrpg-gold-hi); }

/* ---- chips: who you are fighting, and what beating them pays ----
   Both are chips rather than sentences because a ladder is SCANNED. The kind
   chip answers "is this the house or a person" — colour-coded rather than
   labelled twice, since the row already carries a helm or a shield — and the
   difficulty chip states the reward BEFORE the ticket is spent, which is the
   only reason the tiers are worth making legible at all. */
.ar-kind, .ar-diff {
  display: inline-block;
  padding: 2px 4px;
  background: var(--e-slate);
  color: var(--e-white);
  font-family: var(--font-pixel);
  font-size: 5px;
  letter-spacing: .4px;
  line-height: 1.7;
  white-space: nowrap;
  vertical-align: middle;
}
.ar-kind { margin-right: 4px; }
.ar-kind.is-npc { background: var(--e-purple); color: var(--e-cream); }
.ar-kind.is-player { background: var(--e-sky); color: var(--e-white); }
/* easy → green, medium → amber, hard → crimson: the ramp a player already
   reads on every other danger cue in the game */
.ar-diff.is-d0 { background: var(--e-pine); color: var(--e-green); }
.ar-diff.is-d1 { background: var(--e-brown); color: var(--e-amber); }
.ar-diff.is-d2 { background: var(--e-crimson); color: var(--e-cream); }
/* A stable row is house furniture, not another player's listing — given a
   left rule in the house colour so the two pools separate down the column. */
.mkt-row.is-gladiator { box-shadow: inset 0 0 0 2px var(--jrpg-edge), inset 4px 0 0 var(--e-purple); }

/* ---- the travelling merchant ----
   Their listings sit in the same column as real players', so the brand has to
   read before the item name does. Gold on ink, which is the badge vocabulary
   the rest of the shell already uses for "the game is speaking". */
.mkt-tag {
  display: inline-block;
  margin-right: 3px;
  padding: 2px 4px;
  background: var(--jrpg-gold);
  color: var(--jrpg-edge);
  font-family: var(--font-pixel);
  font-size: 5px;
  letter-spacing: .4px;
  line-height: 1.7;
  white-space: nowrap;
  vertical-align: middle;
}
.mkt-row.is-merchant { box-shadow: inset 0 0 0 2px var(--jrpg-gold), inset 2px 2px 0 rgba(255, 255, 255, .04); }
/* The rarity keeps the left edge on a merchant row that carries an item — the
   rarity IS the thing being sold, and the gold outer frame already says who is
   selling it, so the two markers use different edges and never compete. */
.mkt-row.is-merchant:is(.r-common, .r-uncommon, .r-rare, .r-epic, .r-legendary, .r-mythic) {
  box-shadow: inset 0 0 0 2px var(--jrpg-gold), inset 4px 0 0 var(--r, var(--e-steel));
}

/* ---------------------------------------------------------------------------
   RIVAL PROFILE — the inspection screen

   Clicking an opponent row opens #rival-modal instead of fighting immediately.
   The layout deliberately borrows the player's OWN character sheet furniture:
   the same .equip-slot plates, the same .item-tip cards, the same rarity ramp.
   That is the whole point of an inspect screen — you are reading a rival's sheet
   in the format you already read your own, so a comparison needs no translation.

   `.mkt-row.is-rival` marks the list rows as inspectable rather than as buy
   rows: the whole row becomes the hit target and grows an "INSPECT" affordance,
   and its own hover tooltip is suppressed so it cannot fight the modal.
   --------------------------------------------------------------------------- */
/* The list row is a real <button> now, so the UA's own button chrome — border,
   buttonface background, its own font stack, centred text — all has to be
   stripped before .mkt-row's grid can take over. */
.mkt-row.is-rival {
  width: 100%;
  cursor: pointer;
  text-align: left;
  font: inherit;
  color: inherit;
  border: 0;
  -webkit-appearance: none;
  appearance: none;
  -webkit-tap-highlight-color: transparent;
}
.mkt-row.is-rival:focus-visible { outline: 3px solid var(--trim-hi); outline-offset: 2px; }
.mkt-row.is-rival > .item-tip { display: none; }
.ar-inspect {
  font-family: var(--font-pixel);
  font-size: 5.5px;
  letter-spacing: 1px;
  line-height: 1.7;
  color: var(--e-cyan);
  white-space: nowrap;
}
.mkt-row.is-rival:hover .ar-inspect { color: var(--trim-hi); }

/* Wider than a plain dialog: four gear plates plus a stat table side by side is
   the minimum that reads as a character sheet rather than as a list. */
.modal.is-rival {
  max-width: min(560px, calc(100vw - 24px));
  display: grid;
  grid-template-rows: auto 1fr auto;
  gap: 10px;
  max-height: calc(100vh - 32px);
}
/* the scrolling middle — overflow-x stays visible so the gear tooltips, which
   open sideways out of the slot row, are not clipped by the scroller */
.rv-body {
  display: grid;
  gap: 10px;
  min-height: 0;
  overflow-y: auto;
  overflow-x: hidden;
  padding: 2px;
}

/* --- identity banner --- */
.rv-head {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 9px 10px;
  background: rgba(0, 0, 0, .3);
  box-shadow: inset 0 0 0 2px var(--jrpg-edge), inset 4px 0 0 var(--e-cyan);
}
.rv-portrait {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 46px;
  height: 46px;
  flex: 0 0 auto;
  font-size: 24px;
  line-height: 1;
  background: linear-gradient(180deg, var(--jrpg-panel2), var(--e-black));
  box-shadow: 0 0 0 2px var(--e-cyan), inset 2px 2px 0 rgba(255, 255, 255, .1);
}
/* the procedural weapon icon, blitted at 3x with smoothing off */
.rv-portrait canvas { display: block; image-rendering: pixelated; width: 30px; height: 30px; }
.rv-id { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.rv-name {
  font-family: var(--font-pixel);
  font-size: 9px;
  letter-spacing: .8px;
  line-height: 1.7;
  color: var(--txt-strong);
  text-shadow: 0 2px 0 var(--jrpg-edge);
  overflow-wrap: break-word;
}
.rv-tags { display: flex; flex-wrap: wrap; align-items: center; gap: 5px; }
.rv-tag {
  padding: 2px 6px;
  font-family: var(--font-pixel);
  font-size: 5.5px;
  letter-spacing: 1px;
  line-height: 1.7;
  color: var(--jrpg-edge);
  background: var(--e-steel);
  box-shadow: 0 0 0 2px var(--jrpg-edge);
  white-space: nowrap;
}
.rv-tag.is-tier { background: var(--jrpg-gold); }
.rv-tag.is-rating { background: var(--e-cyan); }
.rv-tag.is-power { background: var(--e-mauve); color: var(--e-white); }

/* --- section heads, shared by the gear row and the stat table --- */
.rv-sec {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px;
  padding-bottom: 2px;
  font-family: var(--font-pixel);
  font-size: 6.5px;
  letter-spacing: 1.2px;
  line-height: 1.7;
  color: var(--txt-dim);
  box-shadow: inset 0 -2px 0 var(--e-slate);
}
.rv-sec b { font-weight: 400; color: var(--jrpg-gold-hi); }

/* --- the four equipped pieces ---
   Same .equip-slot element the backpack uses, so paintSlot() and fillTip() can
   render a rival's gear with no second code path. overflow:visible on the grid
   is load-bearing: the tooltip has to be able to leave the row. */
.rv-gear {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 7px;
  overflow: visible;
}
.rv-gear .equip-slot {
  width: auto;
  min-height: 74px;
  cursor: help;
}
/* Tooltips from the outer two plates would run off the dialog if they stayed
   centred, so the first and last anchor to their own inner edge instead. */
.rv-gear > :first-child .item-tip { left: 0; transform: translateX(0) translateY(4px); }
.rv-gear > :first-child:hover .item-tip,
.rv-gear > :first-child:focus-visible .item-tip { transform: translateX(0) translateY(0); }
.rv-gear > :last-child .item-tip { left: auto; right: 0; transform: translateX(0) translateY(4px); }
.rv-gear > :last-child:hover .item-tip,
.rv-gear > :last-child:focus-visible .item-tip { transform: translateX(0) translateY(0); }

/* --- computed stat table ---
   Four rows, each label · value · a delta against the player's own number, so
   "can I win this" is answerable without leaving the screen. */
.rv-stats { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 6px; }
.rv-stat {
  display: flex;
  flex-direction: column;
  gap: 3px;
  min-width: 0;
  padding: 7px 8px;
  background: rgba(0, 0, 0, .28);
  box-shadow: inset 0 0 0 2px var(--jrpg-edge), inset 3px 0 0 var(--rv-accent, var(--e-steel));
}
.rv-stat-k {
  font-family: var(--font-pixel);
  font-size: 5.5px;
  letter-spacing: 1px;
  line-height: 1.7;
  color: var(--txt-dim);
  white-space: normal;
  overflow-wrap: break-word;
}
.rv-stat-row { display: flex; align-items: baseline; justify-content: space-between; gap: 6px; min-width: 0; }
.rv-stat-v {
  font-family: var(--font-pixel);
  font-size: 10px;
  font-weight: 400;
  line-height: 1.45;
  color: var(--rv-accent, var(--e-cream));
  white-space: nowrap;
}
/* the comparison arrow reads from the PLAYER's point of view: green means the
   player is ahead on that line, red means the rival is */
.rv-delta {
  font-family: var(--font-pixel);
  font-size: 5.5px;
  letter-spacing: .4px;
  line-height: 1.7;
  white-space: nowrap;
}
.rv-delta.is-up { color: var(--e-green); }
.rv-delta.is-down { color: var(--e-rose); }
.rv-stat.is-hp { --rv-accent: var(--e-red); }
.rv-stat.is-dmg { --rv-accent: var(--trim-hi); }
.rv-stat.is-armor { --rv-accent: var(--e-sky); }
.rv-stat.is-aps { --rv-accent: var(--e-green); }

/* set lines the rival has completed — the one piece of information the four
   tooltips cannot tell you individually */
.rv-sets { display: grid; gap: 5px; }
.rv-set {
  padding: 6px 8px;
  font-size: 9px;
  line-height: 1.6;
  color: var(--e-cyan);
  background: rgba(0, 0, 0, .24);
  box-shadow: inset 0 0 0 2px var(--e-deep);
  overflow-wrap: break-word;
}

/* --- verdict line + action row --- */
.rv-odds {
  padding: 8px 9px;
  font-size: 9.5px;
  line-height: 1.6;
  text-align: center;
  background: rgba(0, 0, 0, .3);
  box-shadow: inset 0 0 0 2px var(--jrpg-edge);
  overflow-wrap: break-word;
}
.rv-odds.is-win { color: var(--e-green); box-shadow: inset 0 0 0 2px var(--e-forest); }
.rv-odds.is-fail { color: var(--e-rose); box-shadow: inset 0 0 0 2px var(--e-crimson); }
.rv-actions { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 7px; }
.rv-actions .inv-btn { width: 100%; }
.rv-note { min-height: 1.4em; font-size: 9px; line-height: 1.6; text-align: center; color: var(--txt-dim); }
.rv-note.is-win { color: var(--e-green); }
.rv-note.is-fail { color: var(--e-rose); }

@media (max-width: 520px) {
  /* two-up gear plates and a single stat column: four 74px plates cannot stay
     square across a phone dialog */
  .rv-gear { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .rv-gear .item-tip { left: 0; right: auto; transform: translateX(0) translateY(4px); }
  .rv-gear .equip-slot:hover .item-tip,
  .rv-gear .equip-slot:focus-visible .item-tip { transform: translateX(0) translateY(0); }
  .rv-stats { grid-template-columns: minmax(0, 1fr); }
  .rv-actions { grid-template-columns: minmax(0, 1fr); }
}

/* ---------------------------------------------------------------------------
   SPOILS OF VICTORY — #loot-modal

   The arena's payout screen. Four numbers, then the gear.

   Everything on it is ALREADY BANKED before it appears (see duelEnd), so this is
   a receipt, not a claim form — which is why nothing in here is a required
   interaction and the dialog closes any way the player likes.

   The four stats get a gold frame and the piece rows reuse .r-* so a legendary
   drop reads the same colour here as it will in the backpack a second later.
   --------------------------------------------------------------------------- */
.modal.is-loot { max-width: min(440px, calc(100vw - 24px)); }
.loot-body { display: grid; gap: var(--gap-md); }
.loot-sub {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  font-size: 9.5px;
  line-height: 1.6;
  color: var(--txt-dim);
}
.loot-tier {
  padding: 2px 5px;
  background: var(--e-crimson);
  color: var(--e-cream);
  font-family: var(--font-pixel);
  font-size: 5px;
  letter-spacing: .5px;
  line-height: 1.7;
}

.loot-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 6px; }
.loot-stat {
  display: flex;
  flex-direction: column;
  gap: 3px;
  min-width: 0;
  padding: 7px 6px;
  text-align: center;
  background: rgba(0, 0, 0, .3);
  box-shadow: inset 0 0 0 2px var(--jrpg-edge);
}
.loot-k {
  font-family: var(--font-pixel);
  font-size: 5px;
  letter-spacing: .5px;
  line-height: 1.7;
  color: var(--txt-dim);
  overflow-wrap: break-word;
}
.loot-stat b {
  font-family: var(--font-pixel);
  font-size: 9px;
  font-weight: 400;
  line-height: 1.5;
  color: var(--txt-strong);
}
.loot-stat.is-gold   { box-shadow: inset 0 0 0 2px var(--jrpg-gold); }
.loot-stat.is-gold b { color: var(--jrpg-gold-hi); }
.loot-stat.is-ap b     { color: var(--e-cyan); }
.loot-stat.is-token b  { color: var(--e-mauve); }
.loot-stat.is-rating b { color: var(--e-green); }

.loot-sec {
  padding-bottom: 4px;
  border-bottom: 2px solid var(--jrpg-edge);
  font-family: var(--font-pixel);
  font-size: 6px;
  letter-spacing: 1px;
  line-height: 1.7;
  color: var(--trim);
}
.loot-items { display: grid; gap: 5px; max-height: 208px; overflow-y: auto; scrollbar-width: thin; }
.loot-none { padding: 8px; font-size: 9.5px; line-height: 1.6; color: var(--txt-dim); }
.loot-item {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
  gap: 8px;
  padding: 6px 7px;
  background: rgba(0, 0, 0, .28);
  /* same left rule as a market row, so a rarity reads identically everywhere */
  box-shadow: inset 0 0 0 2px var(--jrpg-edge), inset 4px 0 0 var(--r, var(--e-steel));
  animation: loot-row .26s cubic-bezier(.2, .8, .2, 1) both;
}
.loot-item-ico { display: flex; width: 30px; height: 30px; align-items: center; justify-content: center; }
.loot-item-ico canvas { display: block; image-rendering: pixelated; width: 30px; height: 30px; }
.loot-item-txt { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.loot-item-txt b {
  font-family: var(--font-pixel);
  font-size: 7px;
  font-weight: 400;
  line-height: 1.6;
  color: var(--r, var(--txt-strong));
  overflow-wrap: break-word;
}
.loot-item-txt small { font-size: 9px; line-height: 1.55; color: var(--txt-dim); overflow-wrap: break-word; }
.loot-item-rar {
  padding: 3px 5px;
  background: var(--r, var(--e-steel));
  color: var(--r-ink, var(--jrpg-edge));
  font-family: var(--font-pixel);
  font-size: 6px;
  line-height: 1.6;
}
@keyframes loot-row { from { opacity: 0; transform: translateX(-8px); } to { opacity: 1; transform: none; } }
@media (prefers-reduced-motion: reduce) { .loot-item { animation: none; } }

/* Only shown when a full backpack forced a piece to be auto-sold. Stated in the
   warning colour because it is the one line here the player might want to act
   on — by making room before the next fight. */
.loot-note {
  padding: 6px 7px;
  font-size: 9px;
  line-height: 1.6;
  color: var(--e-amber);
  background: rgba(0, 0, 0, .28);
  box-shadow: inset 0 0 0 2px var(--e-brown);
  overflow-wrap: break-word;
}
.loot-actions { display: grid; }
.loot-actions .inv-btn { width: 100%; }

@media (max-width: 460px) {
  .loot-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* ---------------------------------------------------------------------------
   LEADERBOARD — #lb-modal

   One dialog, two boards, one table shape. GEAR SCORE has four columns and
   ARENA POINTS has six, so the grid is declared per board (.is-power /
   .is-arena) and every row — header included — inherits it. That is what keeps a
   header cell over its column without a single hard-coded width.

   Rank is right-aligned and tabular, numbers are right-aligned and tabular, and
   the name column is the only elastic one: a ranking is read by running an eye
   DOWN a column, and proportional digits make that column wobble.

   The podium is three flat pixel plates rather than a gradient trophy. Gold,
   silver and bronze already mean first, second and third to everyone; the badge
   only has to be unmistakable at 11px.
   --------------------------------------------------------------------------- */
.modal.is-board { max-width: min(560px, calc(100vw - 24px)); }
/* .modal-head is space-between; the transport label belongs beside the title, not
   floated out into the middle of the header. */
.modal.is-board .modal-head #lb-mode { margin-right: auto; }
.lb-tabs { display: flex; flex-wrap: wrap; align-items: center; gap: 4px; margin-bottom: var(--gap-md); }
.lb-tab {
  padding: 6px 8px;
  border: 0;
  cursor: pointer;
  background: rgba(255, 255, 255, .04);
  color: var(--txt-dim);
  font-family: var(--font-pixel);
  font-size: 6px;
  letter-spacing: .6px;
  line-height: 1.7;
}
.lb-tab:hover { color: var(--txt-strong); background: rgba(255, 255, 255, .08); }
.lb-tab.active {
  background: var(--jrpg-gold);
  color: var(--jrpg-edge);
  box-shadow: inset 0 0 0 2px var(--jrpg-edge);
}
.lb-tab:focus-visible { outline: 3px solid var(--trim-hi); outline-offset: 2px; }
.lb-refresh { margin-left: auto; flex: 0 0 auto; }

.lb-body { min-height: 120px; max-height: min(52vh, 420px); overflow-y: auto; scrollbar-width: thin; }
.lb-offline {
  margin-bottom: 7px;
  padding: 6px 7px;
  font-size: 9px;
  line-height: 1.6;
  color: var(--e-amber);
  background: rgba(0, 0, 0, .28);
  box-shadow: inset 0 0 0 2px var(--e-brown);
}
.lb-table { display: grid; gap: 4px; }
.lb-row {
  display: grid;
  align-items: center;
  gap: 7px;
  padding: 6px 7px;
  background: rgba(0, 0, 0, .26);
  box-shadow: inset 0 0 0 2px var(--jrpg-edge);
}
/* the two column sets — every row in a board, header included, uses one of these */
.lb-table.is-power .lb-row { grid-template-columns: 34px minmax(0, 1fr) 44px 74px; }
.lb-table.is-arena .lb-row { grid-template-columns: 34px minmax(0, 1fr) 44px 34px 34px 56px; }
.lb-row.is-head {
  background: none;
  box-shadow: none;
  padding-bottom: 2px;
  border-bottom: 2px solid var(--jrpg-edge);
}
.lb-row.is-head .lb-cell {
  font-family: var(--font-pixel);
  font-size: 5px;
  letter-spacing: .6px;
  line-height: 1.7;
  color: var(--txt-dim);
}
.lb-cell {
  min-width: 0;
  font-size: 9.5px;
  line-height: 1.55;
  color: var(--txt);
  text-align: right;
  font-variant-numeric: tabular-nums;
  overflow-wrap: break-word;
}
.lb-cell.is-rank, .lb-row.is-head .lb-cell:first-child { text-align: center; }
.lb-cell.is-who, .lb-row.is-head .lb-cell:nth-child(2) { text-align: left; }
.lb-cell.is-rank {
  font-family: var(--font-pixel);
  font-size: 7px;
  line-height: 1.6;
  color: var(--txt-dim);
}
.lb-cell.is-who { display: flex; flex-wrap: wrap; align-items: baseline; gap: 5px; }
.lb-cell.is-who b {
  font-family: var(--font-pixel);
  font-size: 7px;
  font-weight: 400;
  line-height: 1.6;
  color: var(--txt-strong);
  overflow-wrap: anywhere;
}
.lb-cell.is-power  { font-family: var(--font-pixel); font-size: 7px; line-height: 1.6; color: var(--jrpg-gold-hi); }
.lb-cell.is-points { font-family: var(--font-pixel); font-size: 7px; line-height: 1.6; color: var(--e-cyan); }
.lb-cell.is-lvl  { color: var(--txt-strong); }
.lb-cell.is-win  { color: var(--e-green); }
.lb-cell.is-loss { color: var(--e-rose); }
.lb-cell.is-rate { color: var(--txt-strong); }

.lb-you, .lb-unranked {
  padding: 1px 4px;
  font-family: var(--font-pixel);
  font-size: 5px;
  letter-spacing: .4px;
  line-height: 1.7;
}
.lb-you { background: var(--e-green); color: var(--jrpg-edge); }
.lb-unranked { background: var(--e-slate); color: var(--txt-dim); }

/* Your own row, wherever it lands — the one row a player is actually looking for. */
.lb-row.is-me {
  background: rgba(99, 199, 77, .12);
  box-shadow: inset 0 0 0 2px var(--e-forest);
}

/* ---- the podium ---- */
.lb-row.is-podium { background: rgba(226, 183, 20, .07); }
.lb-row.is-p1 { box-shadow: inset 0 0 0 2px var(--jrpg-gold); }
.lb-row.is-p2 { box-shadow: inset 0 0 0 2px var(--e-silver); }
.lb-row.is-p3 { box-shadow: inset 0 0 0 2px var(--e-clay); }
/* is-me wins over the podium: the player's own row matters more to them than
   its colour does, and a first-place player still gets the 🥇 badge */
.lb-row.is-podium.is-me { box-shadow: inset 0 0 0 2px var(--e-forest); }
.lb-badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  font-size: 11px;
  line-height: 1;
  background: var(--e-slate);
  box-shadow: inset 0 0 0 2px var(--jrpg-edge);
}
.lb-badge.is-p1 { background: var(--jrpg-gold); }
.lb-badge.is-p2 { background: var(--e-silver); }
.lb-badge.is-p3 { background: var(--e-clay); }

.lb-note {
  margin-top: var(--gap-md);
  font-size: 9px;
  line-height: 1.6;
  color: var(--txt-dim);
  overflow-wrap: break-word;
}

@media (max-width: 520px) {
  /* Level and win-rate are the two columns a phone can afford to lose: rank,
     name and the score itself are what the board is FOR. */
  .lb-table.is-power .lb-row { grid-template-columns: 30px minmax(0, 1fr) 64px; }
  .lb-table.is-arena .lb-row { grid-template-columns: 30px minmax(0, 1fr) 40px 30px 30px; }
  .lb-table.is-power .lb-cell.is-lvl,
  .lb-table.is-power .lb-row.is-head .lb-cell:nth-child(3),
  .lb-table.is-arena .lb-cell.is-rate,
  .lb-table.is-arena .lb-row.is-head .lb-cell:nth-child(6) { display: none; }
}

/* ---------------------------------------------------------------------------
   PATCH NOTES — #patch-modal

   A changelog, so it is typographic rather than decorative: the version and
   date recede, the release title carries the eye down the page, and the body
   text is the only thing at full contrast.

   The newest release is marked .is-latest and given the gold rule, because on
   the one boot where this dialog opens by itself, "what changed just now" is the
   only question being asked. Older releases stay readable below it rather than
   being collapsed — the list is short, and a reader who came in via 📜 is
   usually looking for something from further back.
   --------------------------------------------------------------------------- */
.modal.is-notes { max-width: min(560px, calc(100vw - 24px)); }
.pn-version {
  margin-left: auto;
  padding: 3px 6px;
  background: var(--e-slate);
  color: var(--jrpg-gold-hi);
  font-family: var(--font-pixel);
  font-size: 6px;
  letter-spacing: .5px;
  line-height: 1.7;
  white-space: nowrap;
}
.pn-body {
  display: grid;
  gap: var(--gap-lg);
  max-height: min(56vh, 460px);
  overflow-y: auto;
  scrollbar-width: thin;
}
.pn-rel { display: grid; gap: 7px; }
.pn-rel-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 6px;
  padding-bottom: 5px;
  margin: 0;
  border-bottom: 2px solid var(--jrpg-edge);
}
.pn-rel.is-latest .pn-rel-head { border-bottom-color: var(--jrpg-gold); }
.pn-ver {
  padding: 2px 5px;
  background: var(--e-slate);
  color: var(--txt-strong);
  font-family: var(--font-pixel);
  font-size: 5.5px;
  letter-spacing: .5px;
  line-height: 1.7;
}
.pn-rel.is-latest .pn-ver { background: var(--jrpg-gold); color: var(--jrpg-edge); }
.pn-rel-title {
  font-family: var(--font-pixel);
  font-size: 8px;
  letter-spacing: .5px;
  line-height: 1.6;
  color: var(--trim-hi);
  overflow-wrap: break-word;
}
.pn-date { margin-left: auto; font-size: 9px; line-height: 1.6; color: var(--txt-dim); white-space: nowrap; }

.pn-list { display: grid; gap: 6px; margin: 0; padding: 0; list-style: none; }
.pn-item {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  align-items: start;
  gap: 8px;
  padding: 7px 8px;
  background: rgba(0, 0, 0, .26);
  box-shadow: inset 0 0 0 2px var(--jrpg-edge);
}
.pn-item.is-new { box-shadow: inset 0 0 0 2px var(--jrpg-edge), inset 4px 0 0 var(--e-green); }
.pn-item.is-fix { box-shadow: inset 0 0 0 2px var(--jrpg-edge), inset 4px 0 0 var(--e-sky); }
.pn-item.is-bal { box-shadow: inset 0 0 0 2px var(--jrpg-edge), inset 4px 0 0 var(--e-amber); }
.pn-badge {
  padding: 3px 5px;
  font-family: var(--font-pixel);
  font-size: 5px;
  letter-spacing: .4px;
  line-height: 1.7;
  white-space: nowrap;
  color: var(--jrpg-edge);
  background: var(--e-grey);
}
.pn-badge.is-new { background: var(--e-green); }
.pn-badge.is-fix { background: var(--e-sky); color: var(--e-white); }
.pn-badge.is-bal { background: var(--e-amber); }
.pn-text { font-size: 10px; line-height: 1.65; color: var(--txt); overflow-wrap: break-word; }

.pn-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--gap-md);
  margin-top: var(--gap-md);
}
.pn-foot { flex: 1 1 160px; min-width: 0; font-size: 9px; line-height: 1.6; color: var(--txt-dim); }
.pn-actions .inv-btn { flex: 0 0 auto; }

@media (max-width: 460px) {
  .pn-item { grid-template-columns: minmax(0, 1fr); }
  .pn-badge { justify-self: start; }
  .pn-actions .inv-btn { width: 100%; }
}

/* ---- chat dock ----
   Fixed, bottom-left, above the nav dock. Collapsed it is just the toggle. */
.chat-dock {
  position: fixed;
  left: 8px;
  bottom: calc(var(--dock-h) + 8px + env(safe-area-inset-bottom, 0px));
  z-index: 68;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  /* No gap: the toggle-to-panel spacing is the panel's own margin-top instead, so
     it can collapse WITH the panel. A flex gap would survive the collapse and
     leave the toggle floating 5px above where it belongs. */
  gap: 0;
  max-width: min(340px, calc(100vw - 16px));
  pointer-events: none;   /* the gap between toggle and panel stays click-through */
}
.chat-dock > * { pointer-events: auto; }
.chat-toggle {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 7px 10px;
  border: 0;
  cursor: pointer;
  background: var(--panel-bg2);
  color: var(--txt-strong);
  font-family: var(--font-pixel);
  font-size: 6px;
  letter-spacing: .8px;
  line-height: 1.7;
  box-shadow: var(--pxframe), inset 0 2px 0 rgba(255, 255, 255, .1), 0 3px 0 var(--jrpg-edge);
}
.chat-toggle:hover { filter: brightness(1.15); }
.chat-toggle-ico { font-size: 11px; line-height: 1; }
.chat-dock.is-open .chat-toggle { background: var(--jrpg-gold); color: var(--jrpg-edge); }

/* The panel collapses with a real transition rather than display:none, and the
   toggle's state is remembered — see chatToggle() / chatWritePref() in game.js.

   max-height carries the motion because the panel's true height depends on how
   the hint line wraps; the cap sits well above anything it can reach, and the
   opacity and lift are what actually read as movement, so the curve never looks
   like a slow height crawl.

   visibility is switched at the FAR end of the collapse so a hidden panel cannot
   be tabbed into, and overflow is clipped during the slide for a clean edge —
   .is-settled releases the clip once the animation finishes, which is what lets
   an item tooltip escape upward while the dock sits open. */
.chat-panel {
  width: min(340px, calc(100vw - 16px));
  margin-top: 0;
  max-height: 0;
  opacity: 0;
  transform: translateY(8px);
  overflow: hidden;
  visibility: hidden;
  background: var(--panel-bg);
  box-shadow: var(--pxframe), var(--pxbevel), 0 4px 0 rgba(0, 0, 0, .4);
  transition:
    max-height .28s cubic-bezier(.2, .8, .3, 1),
    margin-top .28s cubic-bezier(.2, .8, .3, 1),
    transform .28s cubic-bezier(.2, .8, .3, 1),
    opacity .18s ease,
    visibility 0s linear .28s;
}
.chat-dock.is-open .chat-panel {
  margin-top: 5px;
  max-height: 480px;
  opacity: 1;
  transform: none;
  visibility: visible;
  transition:
    max-height .28s cubic-bezier(.2, .8, .3, 1),
    margin-top .28s cubic-bezier(.2, .8, .3, 1),
    transform .28s cubic-bezier(.2, .8, .3, 1),
    opacity .22s ease .04s,
    visibility 0s linear 0s;
}
.chat-dock.is-open.is-settled .chat-panel { overflow: visible; }

.chat-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 6px;
  padding: 5px 6px;
  background: var(--jrpg-bg2);
}
.chat-chs { display: flex; gap: 2px; min-width: 0; flex-wrap: wrap; }
.chat-ch {
  padding: 5px 6px;
  border: 0;
  cursor: pointer;
  background: rgba(255, 255, 255, .04);
  color: var(--txt-dim);
  font-family: var(--font-pixel);
  font-size: 5.5px;
  letter-spacing: .5px;
  line-height: 1.7;
}
.chat-ch:hover { color: var(--txt-strong); }
.chat-ch.active { background: var(--jrpg-gold); color: var(--jrpg-edge); }
.chat-mode { font-family: var(--font-pixel); font-size: 5px; line-height: 1.7; color: var(--txt-dim); white-space: nowrap; }

.chat-log {
  height: 168px;
  overflow-y: auto;
  padding: 7px 8px;
  font-size: 10px;
  line-height: 1.6;
  background: var(--jrpg-bg2);
  scrollbar-width: thin;
}
.chat-empty { color: var(--txt-dim); font-size: 9.5px; }
.chat-line { margin-bottom: 4px; overflow-wrap: break-word; }
.chat-who { font-weight: 400; color: var(--jrpg-gold); }
.chat-line.is-sys .chat-who { color: var(--e-cyan); }
.chat-line.is-sys .chat-text { color: var(--e-sky); }
.chat-line.is-mine .chat-who { color: var(--e-green); }
.chat-text { color: var(--txt); }

/* Timestamp. Deliberately the quietest thing on the line — it is reference
   information you scan for once, not something you read every message — and
   tabular figures so a column of them does not shimmer as the digits change. */
.chat-time {
  margin-right: 4px;
  font-family: var(--font-pixel);
  font-size: 5px;
  line-height: 1.7;
  color: var(--txt-dim);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* Unread counts. The room tab grows a pip; the collapsed dock's toggle pulses,
   because with the panel shut the tab pips are not on screen at all and the
   toggle is the only surface left to raise a hand with. */
.chat-unread {
  display: inline-block;
  min-width: 11px;
  margin-left: 4px;
  padding: 1px 2px;
  background: var(--alert);
  color: var(--e-white);
  font-family: var(--font-pixel);
  font-size: 5px;
  line-height: 1.6;
  text-align: center;
}
.chat-ch.active .chat-unread { background: var(--jrpg-edge); color: var(--jrpg-gold-hi); }
.chat-toggle.has-unread {
  background: var(--e-crimson);
  color: var(--e-white);
  animation: chat-ping 1.6s ease-in-out infinite;
}
@keyframes chat-ping {
  0%, 100% { box-shadow: var(--pxframe); }
  50%      { box-shadow: var(--pxframe), 0 0 0 3px rgba(228, 59, 68, .34); }
}
@media (prefers-reduced-motion: reduce) {
  .chat-toggle.has-unread { animation: none; }
}

/* A line the SERVER sent, as against one this client wrote for itself. Given a
   left rule and lifted off the log background, so an official alert is never
   mistaken for the game talking to itself. */
.chat-line.is-srv {
  margin: 5px 0;
  padding: 4px 6px;
  background: rgba(46, 34, 47, .6);
  box-shadow: inset 3px 0 0 var(--e-cyan);
}
.chat-line.is-srv .chat-who { color: var(--e-cyan); }
.chat-line.is-srv .chat-text { color: var(--txt-strong); }

/* /broadcast — the loudest thing the log can say, and the one that also raised a
   banner over the whole screen. */
.chat-line.is-ann {
  background: rgba(62, 42, 12, .7);
  box-shadow: inset 3px 0 0 var(--jrpg-gold-hi);
}
.chat-line.is-ann .chat-who { color: var(--jrpg-gold-hi); }
.chat-line.is-ann .chat-text { color: var(--e-yellow); }

/* ---- the owner's plate ----
   Pixel-framed with stacked box-shadows rather than a border: a 1px border
   anti-aliases at fractional zoom and reads as a smudge, while stacked hard
   shadows stay crisp at any scale — the same trick the rest of the chrome uses. */
.chat-badge {
  display: inline-block;
  margin-right: 4px;
  padding: 2px 4px;
  vertical-align: 1px;
  font-family: var(--font-pixel);
  font-size: 5.5px;
  letter-spacing: .5px;
  line-height: 1.6;
  color: var(--e-black);
  background: linear-gradient(180deg, var(--jrpg-gold-hi) 0 50%, var(--jrpg-gold) 50% 100%);
  box-shadow:
    0 0 0 1px var(--jrpg-edge),
    0 0 0 2px var(--e-amber),
    0 2px 0 rgba(0, 0, 0, .45),
    0 0 10px rgba(226, 183, 20, .5);
  text-shadow: 0 1px 0 rgba(255, 255, 255, .35);
  animation: founder-gleam 2.8s steps(1, end) infinite;
}
/* Stepped rather than eased, so it blinks like a two-frame sprite instead of
   breathing like a CSS glow. */
@keyframes founder-gleam {
  0%, 88% { filter: none; }
  92%     { filter: brightness(1.4); }
  96%     { filter: none; }
}
.chat-line.is-founder .chat-who {
  color: var(--jrpg-gold-hi);
  text-shadow: 0 0 6px rgba(226, 183, 20, .45);
}

/* an item link inside a message: a coloured chip carrying a real tooltip */
.chat-link {
  position: relative;
  display: inline-block;
  margin-left: 4px;
  padding: 0 3px;
  cursor: pointer;
  color: var(--r, var(--e-cream));
  background: rgba(0, 0, 0, .35);
  box-shadow: inset 0 0 0 1px var(--r, var(--e-steel));
}
.chat-link > .item-tip { bottom: calc(100% + 6px); left: 0; right: auto; transform: translateY(4px); }
.chat-link:hover > .item-tip,
.chat-link:focus-visible > .item-tip,
/* pinned by a click — hover is not available on a touch screen, and inspecting a
   linked item is the entire reason the chip exists */
.chat-link.is-tip-open > .item-tip { opacity: 1; visibility: visible; transform: translateY(0); }

.chat-hint { padding: 4px 8px 0; font-size: 8.5px; line-height: 1.5; color: var(--txt-dim); }
/* "You are not in a guild" — a rule, stated quietly */
.chat-hint.is-lock { color: var(--e-rose); }
.chat-hint.is-admin { color: var(--jrpg-gold-hi); }
/* a transient refusal from chatSay(): the cooldown, a rejected command. Louder
   than the standing hint because it answers something the player just did. */
.chat-hint.is-warn { color: var(--e-amber); }

.chat-compose { display: flex; align-items: center; gap: 5px; padding: 6px; }
/* Locked composer: [sistem] is generated, and [lonca] has nobody on the other end
   until a guild exists. Visibly disabled rather than hidden — a control that
   vanishes reads as a bug, one that is greyed reads as a rule. */
.chat-compose.is-locked { opacity: .6; }
.chat-compose.is-locked .buy-btn { cursor: not-allowed; filter: grayscale(.75); }
.chat-attach {
  flex: 0 0 auto;
  min-width: 16px;
  padding: 3px 4px;
  text-align: center;
  font-family: var(--font-pixel);
  font-size: 6px;
  line-height: 1.6;
  color: var(--jrpg-edge);
  background: var(--e-cyan);
}
.chat-input {
  flex: 1 1 auto;
  min-width: 0;
  padding: 7px 8px;
  font-family: inherit;
  font-size: 10px;
  color: var(--txt-strong);
  background: var(--jrpg-bg2);
  border: 0;
  box-shadow: inset 0 0 0 2px var(--jrpg-edge);
}
.chat-input:focus-visible { outline: none; box-shadow: inset 0 0 0 2px var(--jrpg-gold); }
.chat-input:disabled { cursor: not-allowed; color: var(--txt-dim); }

/* the chat would sit on top of the town dialogue on a phone, so it steps aside */
@media (max-width: 700px) {
  .chat-dock { max-width: calc(100vw - 16px); }
  .chat-log { height: 132px; }
}

/* ---- broadcast banner ----
   The owner's /broadcast, fixed across the top-centre of the viewport. z-index
   clears the nav dock and the chat but stays under the modals: an announcement
   must never cover the sign-in wall. pointer-events:none throughout, because it
   is an interruption the player never has to dismiss. */
.announce-banner {
  position: fixed;
  top: calc(8px + env(safe-area-inset-top, 0px));
  left: 50%;
  z-index: 96;
  display: flex;
  align-items: center;
  gap: 8px;
  max-width: min(680px, calc(100vw - 24px));
  padding: 9px 14px;
  pointer-events: none;
  opacity: 0;
  transform: translate(-50%, -140%);
  background: linear-gradient(180deg, rgba(62, 42, 12, .97), rgba(24, 20, 37, .97));
  box-shadow:
    0 0 0 2px var(--jrpg-edge),
    0 0 0 5px var(--jrpg-gold),
    0 0 0 7px var(--jrpg-edge),
    0 6px 0 rgba(0, 0, 0, .5),
    0 0 40px rgba(226, 183, 20, .35);
}
.announce-banner.is-live { animation: ann-drop .42s cubic-bezier(.16, 1.25, .4, 1) both; }
.announce-banner.is-live.is-out { animation: ann-lift .65s ease both; }

.announce-tag {
  flex: 0 0 auto;
  padding: 3px 6px;
  font-family: var(--font-pixel);
  font-size: 6px;
  letter-spacing: 1px;
  line-height: 1.7;
  color: var(--e-black);
  background: var(--jrpg-gold-hi);
  box-shadow: 0 0 0 1px var(--jrpg-edge);
}
.announce-text {
  min-width: 0;
  font-size: 11px;
  line-height: 1.55;
  color: var(--e-yellow);
  overflow-wrap: break-word;
  text-shadow: 0 2px 0 rgba(0, 0, 0, .6);
}

/* Drops in and overshoots once. The impact frame is what makes it read as
   announced rather than merely faded in. */
@keyframes ann-drop {
  0%   { opacity: 0; transform: translate(-50%, -140%); }
  60%  { opacity: 1; transform: translate(-50%, 6%); }
  80%  { transform: translate(-50%, -4%); }
  100% { opacity: 1; transform: translate(-50%, 0); }
}
@keyframes ann-lift {
  0%   { opacity: 1; transform: translate(-50%, 0); }
  100% { opacity: 0; transform: translate(-50%, -130%); }
}

@media (max-width: 700px) {
  .announce-banner { gap: 6px; padding: 7px 10px; }
  .announce-text { font-size: 10px; }
  .announce-tag { font-size: 5.5px; }
}

@media (prefers-reduced-motion: reduce) {
  /* The collapse still has to HAPPEN, so only the travel is dropped: the panel
     cuts to its state and cross-fades instead of sliding. */
  .chat-panel,
  .chat-dock.is-open .chat-panel {
    transform: none;
    transition: opacity .12s linear, visibility 0s linear;
  }
  .chat-badge { animation: none; }
  .announce-banner.is-live { animation: none; opacity: 1; transform: translate(-50%, 0); }
  .announce-banner.is-live.is-out { animation: none; opacity: 0; transform: translate(-50%, 0); }
}

/* ---------- 9b. Bottom dock ---------- */
.dock {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 70;
  display: grid;
  /* 8 slots: heroes, items, forge, talents, market, arena, town, settings */
  grid-template-columns: repeat(8, minmax(0, 1fr));
  gap: 2px;
  padding: 5px 5px calc(5px + env(safe-area-inset-bottom, 0px));
  background: linear-gradient(180deg, var(--jrpg-panel) 0%, #14161c 78%);
  box-shadow:
    0 -3px 0 0 var(--line),
    0 -5px 0 0 rgba(254, 174, 52, .22),
    0 -14px 26px rgba(0, 0, 0, .5);
}

.dock-btn {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 4px;
  /* taller, with real horizontal padding: ENVANTER and YETENEKLER are long
     enough at 6px that 2px of side padding put them against the divider */
  min-height: 62px;
  padding: 9px 6px;
  border: 0;
  cursor: pointer;
  background: transparent;
  color: var(--txt-dim);
  font-family: var(--font-pixel);
  font-size: 6px;
  letter-spacing: .4px;
  text-transform: uppercase;
  -webkit-tap-highlight-color: transparent;
  transition: color .18s linear, background .18s linear, transform .08s steps(2);
}
.dock-ico {
  font-size: 18px;
  line-height: 1;
  filter: grayscale(.6) brightness(.9);
  transition: filter .18s linear, transform .18s cubic-bezier(.2,.8,.2,1);
}
.dock-txt { line-height: 1.6; text-align: center; max-width: 100%; overflow-wrap: break-word; }
.dock-btn:hover { color: var(--txt-strong); background: rgba(255, 255, 255, .06); }
.dock-btn:hover .dock-ico { filter: none; transform: translateY(-2px) scale(1.06); }
.dock-btn:active { transform: translateY(2px); }
.dock-btn:focus-visible { outline: 3px solid var(--trim-hi); outline-offset: -3px; }

/* active slot: lit label, raised icon, gold underline that grows from the centre */
.dock-btn::after {
  content: '';
  position: absolute;
  left: 6px; right: 6px; top: 0;
  height: 4px;
  background: var(--trim-hi);
  box-shadow: 0 0 10px rgba(254, 231, 97, .7);
  transform: scaleX(0);
  transition: transform .24s cubic-bezier(.2,.8,.2,1);
}
.dock-btn.active,
body:has(#tab-attributes:not(.hidden)) .dock-btn[data-tab="heroes"],
body:has(#tab-companions:not(.hidden)) .dock-btn[data-tab="heroes"],
body:has(#tab-dungeon:not(.hidden)) .dock-btn[data-tab="heroes"],
body:has(#tab-tower:not(.hidden)) .dock-btn[data-tab="heroes"] {
  color: var(--trim-hi);
  background: rgba(254, 174, 52, .1);
}
.dock-btn.active::after,
body:has(#tab-attributes:not(.hidden)) .dock-btn[data-tab="heroes"]::after,
body:has(#tab-companions:not(.hidden)) .dock-btn[data-tab="heroes"]::after,
body:has(#tab-dungeon:not(.hidden)) .dock-btn[data-tab="heroes"]::after,
body:has(#tab-tower:not(.hidden)) .dock-btn[data-tab="heroes"]::after { transform: scaleX(1); }
.dock-btn.active .dock-ico,
body:has(#tab-attributes:not(.hidden)) .dock-btn[data-tab="heroes"] .dock-ico,
body:has(#tab-companions:not(.hidden)) .dock-btn[data-tab="heroes"] .dock-ico,
body:has(#tab-dungeon:not(.hidden)) .dock-btn[data-tab="heroes"] .dock-ico,
body:has(#tab-tower:not(.hidden)) .dock-btn[data-tab="heroes"] .dock-ico {
  filter: none;
  transform: translateY(-1px);
}

/* roll any character sub-tab alert up onto the HEROES slot, so a badge is never
   hidden behind a collapsed group */
body:has(.sub-tabs .tab-btn.has-alert) .dock-btn[data-tab="heroes"] .tab-badge {
  opacity: 1;
  transform: scale(1);
  animation: badge-pip 1.5s ease-in-out infinite;
}

/* The settings slot is a real <button> that opens a JS-driven modal, not a tab
   and no longer a <label> for a checkbox. `body.is-paused` is set by the modal
   controller, so the slot lights up for as long as a dialog is open. */
.dock-btn.is-settings { color: var(--txt-dim); }
body.is-paused .dock-btn.is-settings {
  color: var(--trim-hi);
  background: rgba(254, 174, 52, .1);
}
body.is-paused .dock-btn.is-settings::after { transform: scaleX(1); }

/* ---------- 10. Shop rows (game.js) ----------
   game.js builds these from rowMarkup(), so the structure is fixed:
   .row-head > .row-icon + (.row-name + .row-desc), then
   .row-bottom > (.row-level|.row-state) + .buy-btn > .row-cost           */
.shop-row {
  position: relative;
  padding: var(--gap-md);
  margin-bottom: var(--gap-md);
  background: linear-gradient(180deg, #414c73, var(--panel-bg2) 60%, #313a5c);
  box-shadow: var(--ring), var(--bevel), 0 4px 0 0 rgba(0, 0, 0, .35);
  transition: transform .08s steps(2), filter .15s linear, box-shadow .15s linear;
}
.shop-row:hover { filter: brightness(1.09); }
/* tactile depression: the whole card sinks and loses its drop edge */
.shop-row:active { transform: translateY(2px); box-shadow: var(--ring), var(--bevel); }
.shop-row:last-child { margin-bottom: 0; }
.shop-row.is-max { filter: saturate(.6) brightness(.9); }
.shop-row.is-max::after {
  content: '';
  position: absolute;
  inset: 0;
  box-shadow: inset 0 0 0 3px var(--trim);
  pointer-events: none;
}

.row-head { display: flex; align-items: center; gap: var(--gap-md); margin-bottom: var(--gap-md); }
.row-icon {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  flex: 0 0 auto;
  font-size: 17px;
  line-height: 1;
  background: rgba(0, 0, 0, .3);
  box-shadow: inset 0 0 0 2px var(--line), inset 2px 2px 0 rgba(255, 255, 255, .08);
  filter: drop-shadow(0 2px 0 rgba(0, 0, 0, .5));
}
.row-name { font-family: var(--font-pixel); font-size: 8px; letter-spacing: 1px; color: var(--txt-strong); line-height: 1.5; }
.row-desc { font-size: 10px; color: var(--txt-dim); margin-top: 4px; }
.row-bottom { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.row-level, .row-state { font-family: var(--font-pixel); font-size: 8px; letter-spacing: 1px; color: var(--e-cyan); }
.row-state { color: var(--e-green); }
.row-cost { color: var(--trim-hi); }
.eq-title { font-family: var(--font-pixel); font-size: 8px; letter-spacing: 1.5px; color: var(--trim); margin: 4px 0 9px; }
.eq-title:not(:first-child) { margin-top: 14px; }

.buy-btn {
  background: linear-gradient(180deg, #6fbf5b, var(--e-forest) 60%, #2f6b38);
  font-size: 8px;
  letter-spacing: 1px;
  padding: 9px 10px;
  min-height: 34px;
  color: var(--e-white);
  box-shadow:
    inset 0 3px 0 rgba(255,255,255,.2),
    inset 0 -3px 0 rgba(0,0,0,.34),
    var(--ring),
    var(--lift);
  transition: transform .08s steps(2), filter .15s linear, box-shadow .1s linear;
}
.buy-btn:hover:not(:disabled) { filter: brightness(1.16); transform: translateY(-2px); }
/* pressed: sink onto the drop edge so the button visibly bottoms out */
.buy-btn:active:not(:disabled) {
  transform: translateY(2px);
  box-shadow: inset 0 3px 0 rgba(255,255,255,.14), inset 0 -2px 0 rgba(0,0,0,.4), var(--ring);
}
.buy-btn:disabled { background: var(--e-steel); filter: grayscale(.7) brightness(.72); cursor: not-allowed; transform: none; }
.buy-btn.is-prestige { width: 100%; background: linear-gradient(180deg, #9a52a0, var(--e-purple) 60%, #4b2750); padding: 12px; }

/* Descriptive copy inside a panel. Every one of these is free text that gets
   noticeably longer in Turkish, so all of them get a real line-height, room to
   grow (height stays auto) and word wrapping — no fixed heights anywhere. */
.panel-note {
  font-size: 10px;
  line-height: 1.7;
  color: var(--txt-dim);
  margin-bottom: 11px;
  overflow-wrap: break-word;
}
.companion-list { display: grid; gap: 9px; }
.companion-list:empty { display: none; }
.empty-state {
  padding: 16px 10px;
  text-align: center;
  font-family: var(--font-pixel);
  font-size: 8px;
  line-height: 1.9;
  letter-spacing: 1px;
  color: var(--e-steel);
  background: rgba(0,0,0,.24);
  box-shadow: inset 0 0 0 2px var(--e-slate);
  /* #tower-lock puts a full sentence in here ("Sınav Kulesi Lv 20'de açılır"),
     so it has to wrap and grow rather than sit on one clipped line */
  overflow-wrap: break-word;
  height: auto;
}

/* ---------- 11. Inventory, rarity, tooltips ---------- */
.r-common    { --r:#8b9bb4; --r-glow:rgba(139,155,180,.40); --r-ink:#3a4466; }
.r-uncommon  { --r:#63c74d; --r-glow:rgba(99,199,77,.50);   --r-ink:#193c3e; }
.r-rare      { --r:#0099db; --r-glow:rgba(0,153,219,.55);   --r-ink:#124e89; }
.r-epic      { --r:#b55088; --r-glow:rgba(181,80,136,.55);  --r-ink:#68386c; }
.r-legendary { --r:#f77622; --r-glow:rgba(254,231,97,.60);  --r-ink:#733e39; }
/* mythic is the 0.02% tier — it gets the brightest ring and a live shimmer */
.r-mythic    { --r:#2ce8f5; --r-glow:rgba(44,232,245,.75);  --r-ink:#68386c; }
.r-mythic .rarity-badge,
.inv-slot.r-mythic, .equip-slot.r-mythic, .forge-slot.r-mythic {
  animation: mythic-shimmer 1.8s ease-in-out infinite;
}
@keyframes mythic-shimmer {
  0%, 100% { box-shadow: 0 0 0 3px var(--r), 0 0 10px var(--r-glow); }
  50%      { box-shadow: 0 0 0 3px #fff, 0 0 20px var(--r-glow); }
}

.inv-head { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; margin: 2px 0 8px; }
.inv-title { font-family: var(--font-pixel); font-size: 8px; letter-spacing: 1.5px; color: var(--trim); }
.inv-count { font-family: var(--font-pixel); font-size: 8px; color: var(--txt-dim); }
.inv-count b { font-weight: 400; color: var(--e-cyan); }
.inv-hint { font-size: 9px; color: var(--e-steel); }

.equip-row { display: grid; grid-template-columns: repeat(4, minmax(0,1fr)); gap: 6px; margin-bottom: 14px; }
.inv-grid {
  display: grid;
  grid-template-columns: repeat(8, minmax(0, 1fr));
  gap: 5px;
  max-width: 100%;
  margin: 0 auto 12px;
}

/* --- category filter strip ---
   Five tabs share this row: ALL / WEAPONS / ARMOR / MATERIALS / POTIONS
   (TÜMÜ / SİLAHLAR / ZIRHLAR / MALZEME / İKSİRLER).

   It is a plain wrapping flex row, stated explicitly so nothing can collapse it:
   `position: relative` and `height: auto` mean a tab can never be taken out of
   flow and stacked into the same line box as its neighbours.

   The tabs also no longer shrink below their own label. They were
   `flex: 1 1 auto; min-width: 0` with `overflow: hidden; text-overflow: ellipsis`
   — at 6.5px, five Turkish labels in a ~290px strip were each clipped to two or
   three glyphs, and five ellipsised pixel-font stubs 4px apart read as one
   run-together block. `min-width: max-content` makes each tab size to its text
   and the row WRAP to a second line when it has to. */
.inv-cats {
  position: relative;
  display: flex;
  flex-wrap: wrap;
  align-items: stretch;
  gap: 4px;
  height: auto;
  min-height: fit-content;
  padding: 3px;
  margin-bottom: 8px;
  background: var(--e-black);
  box-shadow: inset 0 0 0 2px var(--e-slate), inset 0 3px 0 rgba(0, 0, 0, .45);
}
.inv-cat {
  position: relative;
  flex: 1 1 auto;
  min-width: max-content;
  height: auto;
  padding: 6px 5px;
  border: 0;
  background: transparent;
  color: var(--e-steel);
  font-family: var(--font-pixel);
  font-size: 6.5px;
  line-height: 1.5;
  letter-spacing: .6px;
  white-space: nowrap;
  cursor: pointer;
  transition: background .14s linear, color .14s linear;
}
.inv-cat:hover { color: var(--e-silver); background: rgba(255, 255, 255, .05); }
.inv-cat:focus-visible { outline: 3px solid var(--trim-hi); outline-offset: 1px; }
.inv-cat.active {
  color: var(--e-black);
  background: var(--trim);
  box-shadow: inset 0 -3px 0 rgba(0, 0, 0, .28);
}

/* An item filtered out by the active category stays in place but recedes, so
   slot indices remain stable and drag-to-reorder keeps working under a filter. */
.inv-slot.is-filtered { opacity: .26; filter: saturate(.3); }
.inv-slot.is-filtered .item-tip { display: none; }

/* --- materials / consumables tally view --- */
.inv-stash {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 6px;
  margin-bottom: 12px;
}
.stash-cell {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: 7px;
  padding: 8px 9px;
  background: rgba(0, 0, 0, .26);
  box-shadow: 0 0 0 2px var(--line), inset 2px 2px 0 rgba(255, 255, 255, .05);
  opacity: .5;
}
.stash-cell.has-any {
  opacity: 1;
  box-shadow: 0 0 0 2px var(--stash-accent, var(--e-steel)), inset 2px 2px 0 rgba(255, 255, 255, .07);
}
.stash-ico { font-size: 15px; line-height: 1; }
.stash-name { font-size: 9.5px; line-height: 1.4; color: var(--txt-dim); min-width: 0; }
.stash-val { font-family: var(--font-pixel); font-size: 8px; font-weight: 400; color: var(--stash-accent, var(--e-cream)); }

.equip-slot, .inv-slot, .forge-slot, .artifact-slot {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: 0;
  cursor: pointer;
  color: var(--txt);
  background: var(--e-black);
  box-shadow:
    0 0 0 3px var(--e-slate),
    inset 2px 2px 0 rgba(255,255,255,.06),
    inset -2px -2px 0 rgba(0,0,0,.45);
  transition: transform .1s steps(2), filter .14s linear, box-shadow .16s linear;
}
.equip-slot { flex-direction: column; gap: 3px; padding: 7px 3px 5px; }
/* 8 columns in a ~320px rail leaves ~36px per cell, so the old 44px floor would
   break the square. aspect-ratio keeps them square at whatever width they get;
   the floor only guards against a pathologically narrow rail. */
.inv-slot { aspect-ratio: 1/1; min-height: 28px; }

.equip-slot:hover, .inv-slot:hover, .forge-slot:hover, .artifact-slot:hover { transform: translateY(-2px); filter: brightness(1.16); }
.equip-slot:active, .inv-slot:active { transform: translateY(1px); }

.equip-slot.has-item, .inv-slot.has-item, .forge-slot.has-item {
  background: var(--r-ink, var(--e-slate));
  box-shadow:
    0 0 0 3px var(--r, var(--e-steel)),
    0 0 12px var(--r-glow, transparent),
    inset 2px 2px 0 rgba(255,255,255,.12),
    inset -2px -2px 0 rgba(0,0,0,.40);
}
.equip-slot.is-selected, .inv-slot.is-selected {
  transform: translateY(-2px);
  box-shadow: 0 0 0 3px var(--trim-hi), 0 0 16px rgba(254,231,97,.62), inset 2px 2px 0 rgba(255,255,255,.18);
}

.slot-icon { font-size: 18px; line-height: 1; filter: grayscale(1) opacity(.42); }
.equip-slot.has-item .slot-icon, .inv-slot.has-item .slot-icon, .forge-slot.has-item .slot-icon { filter: none; }
.inv-slot.is-empty .slot-icon:empty::before { content: '·'; font-size: 20px; color: var(--e-slate); }
.slot-label { font-family: var(--font-pixel); font-size: 6px; letter-spacing: .5px; color: var(--txt-dim); }
.equip-slot.has-item .slot-label { color: var(--e-white); }

.rarity-badge {
  position: absolute;
  top: -3px; right: -3px;
  display: none;
  min-width: 13px;
  padding: 1px 3px;
  font-family: var(--font-pixel);
  font-size: 6px;
  line-height: 1.7;
  text-align: center;
  color: var(--e-black);
  background: var(--r, var(--e-grey));
  box-shadow: 0 0 0 2px var(--e-black);
}
.has-item .rarity-badge { display: block; }

.equip-slot.r-legendary.has-item, .inv-slot.r-legendary.has-item, .forge-slot.r-legendary.has-item {
  animation: legend-pulse 1.5s ease-in-out infinite;
}
@keyframes legend-pulse {
  0%, 100% { box-shadow: 0 0 0 3px #f77622, 0 0 8px rgba(254,231,97,.45), inset 2px 2px 0 rgba(255,255,255,.12); }
  50%      { box-shadow: 0 0 0 3px #fee761, 0 0 20px rgba(254,231,97,.90), inset 2px 2px 0 rgba(255,255,255,.22); }
}

/* --- tooltips (items, forge preview) --- */
/* Tooltips size themselves to their content now.

   The old rule was a hard `width: 190px`, which clipped long item names —
   "Usta işi Deri Zırh", "Yıpranmış İlmek" — because .tip-name is a pixel font
   with letter-spacing inside a fixed box that could not grow. The fix is three
   parts: a MIN width so short tips still look deliberate, a MAX width so a long
   set bonus does not stretch across the panel, and real wrapping so anything
   longer than one line breaks instead of overflowing. Height is left to auto,
   which is what lets the socket and drop-source lines extend the box. */
.item-tip {
  position: absolute;
  left: 50%;
  bottom: calc(100% + 9px);
  z-index: 30;
  display: flex;
  flex-direction: column;
  min-width: 240px;
  width: max-content;
  max-width: min(320px, calc(100vw - 28px));
  height: auto;
  padding: 8px 9px;
  text-align: left;
  background: var(--e-black);
  box-shadow: 0 0 0 3px var(--r, var(--e-steel)), 0 6px 0 rgba(0,0,0,.45);
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transform: translateX(-50%) translateY(4px);
  transition: opacity .14s ease, transform .14s ease, visibility .14s;
  /* the actual overflow fix: wrap on words, and break a single unbreakable
     token rather than letting it punch through the border */
  white-space: normal;
  overflow-wrap: break-word;
  word-break: break-word;
}
/* every line inside a tip inherits the same wrapping contract */
.item-tip > * {
  max-width: 100%;
  white-space: normal;
  overflow-wrap: break-word;
}

.equip-slot:hover .item-tip, .equip-slot:focus-visible .item-tip,
.inv-slot:hover .item-tip,   .inv-slot:focus-visible .item-tip,
.forge-slot:hover .item-tip, .forge-slot:focus-visible .item-tip,
.artifact-slot:hover .item-tip, .artifact-slot:focus-visible .item-tip {
  opacity: 1;
  visibility: visible;
  transform: translateX(-50%) translateY(0);
}
.inv-slot.is-empty .item-tip { display: none; }

/* ---------- resource guide cards ----------
   Same tooltip shell as an item card, hung off any counter carrying data-res:
   the header pills, the town purse cells and the backpack MATERIALS tally.
   Shown on hover AND on focus, since the pills are keyboard-reachable. */
[data-res] { position: relative; }
[data-res] > .is-res-tip {
  left: auto;
  right: 0;
  bottom: calc(100% + 8px);
  transform: translateY(4px);
  min-width: 232px;
  box-shadow: 0 0 0 3px var(--e-steel), 0 6px 0 rgba(0, 0, 0, .45);
}
[data-res]:hover > .is-res-tip,
[data-res]:focus-visible > .is-res-tip,
[data-res]:focus-within > .is-res-tip {
  opacity: 1;
  visibility: visible;
  transform: translateY(0);
}
.tip-res-use { color: var(--e-cream, var(--txt-strong)); }
/* the town purse sits at the top of its panel, so its cards drop downward
   instead of being clipped by the panel edge */
.mat-cell > .is-res-tip { top: calc(100% + 8px); bottom: auto; }
.mat-cell:first-child > .is-res-tip { right: auto; left: 0; }

/* keep tips inside .tab-body, which clips on both axes.
   The nth-child arithmetic tracks the grid width: 8 columns now, so the first
   ROW is -n+8 and the edge columns are 8n+1 / 8n. */
.equip-row .item-tip, .artifact-row .item-tip { top: calc(100% + 9px); bottom: auto; }
.inv-grid .inv-slot:nth-child(-n+8) .item-tip { top: calc(100% + 9px); bottom: auto; }
.equip-row .equip-slot:first-child .item-tip,
.artifact-row .artifact-slot:first-child .item-tip,
.inv-grid .inv-slot:nth-child(8n+1) .item-tip,
.inv-grid .inv-slot:nth-child(8n+2) .item-tip { left: 0; transform: translateY(4px); }
.equip-row .equip-slot:first-child:hover .item-tip,
.artifact-row .artifact-slot:first-child:hover .item-tip,
.inv-grid .inv-slot:nth-child(8n+1):hover .item-tip,
.inv-grid .inv-slot:nth-child(8n+1):focus-visible .item-tip,
.inv-grid .inv-slot:nth-child(8n+2):hover .item-tip,
.inv-grid .inv-slot:nth-child(8n+2):focus-visible .item-tip { transform: translateY(0); }
.equip-row .equip-slot:last-child .item-tip,
.artifact-row .artifact-slot:last-child .item-tip,
.inv-grid .inv-slot:nth-child(8n) .item-tip,
.inv-grid .inv-slot:nth-child(8n+7) .item-tip { right: 0; left: auto; transform: translateY(4px); }
.equip-row .equip-slot:last-child:hover .item-tip,
.artifact-row .artifact-slot:last-child:hover .item-tip,
.inv-grid .inv-slot:nth-child(8n):hover .item-tip,
.inv-grid .inv-slot:nth-child(8n):focus-visible .item-tip,
.inv-grid .inv-slot:nth-child(8n+7):hover .item-tip,
.inv-grid .inv-slot:nth-child(8n+7):focus-visible .item-tip { transform: translateY(0); }

.tip-name { display: block; margin-bottom: 5px; font-family: var(--font-pixel); font-size: 8px; line-height: 1.6; color: var(--r, var(--e-white)); }
.tip-slot { display: block; margin-bottom: 6px; font-size: 9px; letter-spacing: 1px; text-transform: uppercase; color: var(--txt-dim); }

/* rarity badge + meta row inside an item tooltip */
.tip-badge {
  display: inline-block;
  margin-right: 6px;
  padding: 2px 5px;
  font-family: var(--font-pixel);
  font-size: 6px;
  letter-spacing: 1px;
  color: var(--e-black);
  background: var(--r, var(--e-grey));
  box-shadow: 0 0 8px var(--r-glow, transparent);
}
.tip-meta { color: var(--txt-dim); }

/* affix rows: label left, comparison arrow right */
.tip-line {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
  font-size: 10px;
  line-height: 1.7;
  color: var(--e-silver);
}
.stat-up { color: var(--e-green); }
/* an affix the equipped piece has and this one does not */
.stat-lost { color: var(--e-steel); }
.stat-arrow {
  flex: 0 0 auto;
  font-family: var(--font-pixel);
  font-size: 6.5px;
  letter-spacing: .5px;
}
.stat-arrow.is-up { color: var(--e-green); text-shadow: 0 0 6px rgba(99, 199, 77, .5); }
.stat-arrow.is-down { color: var(--e-rose); text-shadow: 0 0 6px rgba(246, 117, 122, .5); }

/* --- gear family + set lines ---
   The family line says what the piece IS (a dagger is fast whatever it rolled);
   the set block says what it could become. Inactive set tiers stay dim so the
   difference between "have it" and "would have it" is readable at a glance. */
.tip-family {
  display: block;
  margin: 0 0 6px;
  padding-bottom: 5px;
  border-bottom: 2px solid var(--e-slate);
  font-size: 9.5px;
  line-height: 1.6;
  color: var(--trim);
}
.tip-set-name {
  display: block;
  margin: 7px 0 3px;
  padding-top: 6px;
  border-top: 2px solid var(--e-slate);
  font-family: var(--font-pixel);
  font-size: 7px;
  letter-spacing: 1px;
  color: var(--trim-hi);
}
.tip-set {
  display: block;
  font-size: 9.5px;
  line-height: 1.7;
  color: var(--e-steel);
}
.tip-set.is-on { color: var(--e-green); text-shadow: 0 0 6px rgba(99, 199, 77, .4); }

/* a set piece is marked in the corner of its slot, independent of rarity */
.inv-slot.has-set::after,
.equip-slot.has-set::after {
  content: '';
  position: absolute;
  top: 3px;
  left: 3px;
  width: 6px;
  height: 6px;
  background: var(--trim-hi);
  box-shadow: 0 0 7px rgba(254, 231, 97, .8);
  pointer-events: none;
}

.tip-sell {
  display: block;
  margin-top: 6px;
  font-size: 9.5px;
  line-height: 1.6;
  color: var(--trim-hi);
}
.stat-down { color: var(--e-rose); }
.tip-cmp { display: block; margin-top: 6px; padding-top: 5px; border-top: 2px solid var(--e-slate); font-size: 9px; line-height: 1.6; color: var(--trim); }
.tip-cmp.is-up { color: var(--e-green); }
.tip-cmp.is-down { color: var(--e-rose); }

/* socket contents and provenance, both added to the tooltip body */
.tip-sockets { color: var(--e-sky); }
.tip-source {
  margin-top: 5px;
  padding-top: 5px;
  border-top: 2px solid var(--e-slate);
  color: var(--e-sand, var(--txt-dim));
  font-style: normal;
}

/* --- inventory actions --- */
.inv-actions { display: grid; grid-template-columns: repeat(2, minmax(0,1fr)); gap: 8px; }
.inv-btn {
  min-height: 40px;
  padding: 10px 6px;
  border: 0;
  cursor: pointer;
  font-size: 7.5px;
  line-height: 1.7;
  letter-spacing: 1px;
  color: var(--e-white);
  background: var(--e-steel);
  box-shadow:
    inset 0 3px 0 rgba(255,255,255,.16),
    inset 0 -3px 0 rgba(0,0,0,.34),
    0 0 0 3px var(--line),
    0 4px 0 0 var(--line);
  transition: transform .08s steps(2), filter .15s linear;
}
.inv-btn:hover:not(:disabled) { filter: brightness(1.18); transform: translateY(-2px); }
.inv-btn:active:not(:disabled) { transform: translateY(2px); }
.inv-btn[disabled] { filter: grayscale(.7) brightness(.68); cursor: not-allowed; transform: none; }
.inv-btn.is-sell { background: var(--e-brown); }
.inv-btn.is-auto { background: var(--e-forest); }
.inv-btn.is-forge { background: var(--e-brick); }
.inv-btn.is-craft { background: var(--e-blue); }

/* ---------- 12. Blacksmith / Forge ---------- */
/*
  OVERFLOW CONTRACT for this whole panel.

  The instructional line ("Ekipmanı +1'den +9'a yükselt…") was being cut off
  along its TOP edge. Two causes, both addressed here rather than by nudging a
  margin:

    1. .forge is a grid inside a scrolling .tab-body. A grid item with a fixed
       or implicit row height clips its first text line when the pixel font's
       ascender exceeds the line box. Every container below is therefore
       `height: auto` + `min-height: fit-content` + `overflow: visible`, so the
       box grows to the text instead of the text being cropped to the box.
    2. The note sat flush against the panel's own top padding with no line-box
       leading of its own. It now carries real padding and a 1.75 line-height.

  game.js also resets .tab-body's scrollTop when a tab is opened — a panel
  inherited the previous tab's scroll offset, which is what made the first line
  look permanently cut off rather than merely scrolled away.
*/
#tab-forge,
.forge,
.forge-meter,
.forge-info {
  height: auto;
  min-height: fit-content;
  overflow: visible;
}
/* Nothing in the forge column may be compressed to make the panel fit.
   `.forge` is a grid so its rows already size to content, but .tab-body is a
   flex column now and these blocks are flex/grid items in more than one context
   — the default `flex-shrink: 1` is exactly what let the anvil, the success
   meter and the action row squeeze into each other and bleed out of their
   frames. They stack at full height and the panel scrolls instead. */
#tab-forge,
.forge,
.forge-anvil,
.forge-meter,
.forge-info,
.forge-actions,
.forge-costs,
.sock-row,
.gem-row,
.forge-log {
  flex-shrink: 0;
}
.forge-anvil,
.forge-actions,
.sock-row,
.gem-row {
  min-height: fit-content;
}
#tab-forge { padding-top: 2px; }
#tab-forge > .panel-note {
  height: auto;
  min-height: fit-content;
  overflow: visible;
  padding: 4px 2px 6px;
  line-height: 1.75;
  white-space: normal;
  overflow-wrap: break-word;
}
.forge { display: grid; gap: 12px; }

.forge-anvil {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  height: auto;
  min-height: fit-content;
  padding: 18px 10px 12px;
  background:
    radial-gradient(60% 45% at 50% 96%, rgba(247,118,34,.35), transparent 70%),
    rgba(0,0,0,.34);
  box-shadow: 0 0 0 3px var(--line), inset 2px 2px 0 rgba(255,255,255,.05);
  /* Was `hidden`, purely to keep the embers inside the box — but that also
     cropped the forge slot's own item tooltip, which opens UPWARD out of this
     container. The clip moved onto .embers instead, which is the only thing that
     actually needed it. */
  overflow: visible;
}

.forge-slot { width: 62px; height: 62px; z-index: 2; }
.forge-plus {
  position: absolute;
  bottom: -4px; left: 50%;
  transform: translateX(-50%);
  padding: 1px 5px;
  font-family: var(--font-pixel);
  font-size: 8px;
  color: var(--e-black);
  background: var(--trim-hi);
  box-shadow: 0 0 0 2px var(--e-black);
}
.forge-slot.is-empty .forge-plus { display: none; }

/* CSS-drawn anvil */
.anvil-base {
  position: relative;
  width: 92px;
  height: 26px;
  background: var(--e-steel);
  box-shadow:
    0 0 0 3px var(--e-black),
    inset 0 3px 0 rgba(255,255,255,.16),
    inset 0 -4px 0 rgba(0,0,0,.4);
}
.anvil-base::after {
  content: '';
  position: absolute;
  left: 50%; bottom: -16px;
  transform: translateX(-50%);
  width: 46px; height: 14px;
  background: var(--e-slate);
  box-shadow: 0 0 0 3px var(--e-black), inset 0 3px 0 rgba(255,255,255,.1);
}

/* burning embers — this is the box that needs the clip, not its parent */
.embers { position: absolute; inset: 0; overflow: hidden; pointer-events: none; }
.embers span {
  position: absolute;
  bottom: 8px;
  width: 3px; height: 3px;
  background: var(--e-orange);
  box-shadow: 0 0 6px var(--e-yellow);
  animation: ember-rise 3.2s linear infinite;
}
.embers span:nth-child(1) { left: 18%; animation-delay: 0s; }
.embers span:nth-child(2) { left: 34%; animation-delay: .7s; background: var(--e-yellow); }
.embers span:nth-child(3) { left: 52%; animation-delay: 1.4s; }
.embers span:nth-child(4) { left: 68%; animation-delay: 2.1s; background: var(--e-amber); }
.embers span:nth-child(5) { left: 84%; animation-delay: 2.7s; }
@keyframes ember-rise {
  0%   { opacity: 0; transform: translateY(0) scale(1); }
  15%  { opacity: 1; }
  100% { opacity: 0; transform: translateY(-72px) translateX(6px) scale(.4); }
}

/* The forge info column. `.forge-info` is an alias on the same element so the
   readout block has a name of its own; both selectors are live.

   Overflow fixes here mirror the tooltip ones: the grid tracks are allowed to
   shrink (minmax(0,...) via min-width:0), every text node wraps, and nothing
   has a fixed height — a long Turkish label like "BAŞARI ORANI" or a three-line
   forge message extends the box instead of spilling out of it. */
.forge-meter, .forge-info { display: grid; gap: 7px; min-width: 0; }
.forge-meter-head, .forge-info-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px;
  min-width: 0;
  font-family: var(--font-pixel);
  font-size: 8px;
  letter-spacing: 1px;
  line-height: 1.7;
  color: var(--txt-dim);
  white-space: normal;
  overflow-wrap: break-word;
}
.forge-meter-head > span { min-width: 0; }
.forge-meter-head b { font-weight: 400; font-size: 11px; color: var(--e-green); flex: 0 0 auto; }
.bar-forge { height: 14px; }
.forge-meter.is-risky .forge-meter-head b { color: var(--e-amber); }
.forge-meter.is-danger .forge-meter-head b { color: var(--e-red); }
.forge-meter.is-risky .fill-forge { background: var(--e-orange); }
.forge-meter.is-danger .fill-forge { background: var(--e-red); }

.forge-costs { display: flex; gap: 7px; flex-wrap: wrap; min-width: 0; }
.cost-chip {
  display: flex;
  align-items: center;
  gap: 6px;
  min-width: 0;
  padding: 5px 8px;
  background: rgba(0,0,0,.3);
  box-shadow: 0 0 0 2px var(--line), inset 2px 2px 0 rgba(255,255,255,.05);
  font-family: var(--font-pixel);
  font-size: 7px;
  letter-spacing: 1px;
  line-height: 1.7;
  color: var(--txt-dim);
  white-space: normal;
  overflow-wrap: break-word;
}
.cost-chip b { font-weight: 400; font-size: 9px; }
.cost-chip.is-ore b { color: var(--e-sky); }
.cost-chip.is-gold b { color: var(--trim-hi); }
.cost-chip.is-short b { color: var(--e-red); }

.forge-actions { display: grid; grid-template-columns: repeat(3, minmax(0,1fr)); gap: 7px; }
/* the forge message is free text and can be two lines in Turkish */
.forge-log {
  min-height: 1.4em;
  height: auto;
  font-size: 10px;
  line-height: 1.55;
  text-align: center;
  color: var(--trim);
  white-space: normal;
  overflow-wrap: break-word;
}
.forge-log.is-fail { color: var(--e-rose); }
.forge-log.is-win { color: var(--e-green); }

/* --- socketing & gem inscription ---
   One pip per socket. A set pip is filled with its gem's colour and is a real
   button (click to pry); an open pip is disabled; a ghost pip marks a socket
   this rarity could still take. */
.sock-row {
  display: flex;
  align-items: center;
  gap: 7px;
  min-height: 30px;
  margin-bottom: 8px;
  padding: 6px 8px;
  background: rgba(0, 0, 0, .26);
  box-shadow: 0 0 0 2px var(--line), inset 2px 2px 0 rgba(255, 255, 255, .05);
}
.sock-pip {
  flex: 0 0 auto;
  width: 20px;
  height: 20px;
  border: 0;
  padding: 0;
  background: var(--e-black);
  box-shadow: inset 0 0 0 2px var(--e-slate);
  transition: box-shadow .14s linear, transform .1s steps(2);
}
.sock-pip.is-set {
  background: var(--sock-col, var(--e-cyan));
  box-shadow: inset 0 0 0 2px var(--e-black), 0 0 9px var(--sock-col, var(--e-cyan));
  cursor: pointer;
}
.sock-pip.is-set:hover { transform: translateY(-2px); }
.sock-pip.is-open { box-shadow: inset 0 0 0 2px var(--trim); }
.sock-pip.is-ghost { opacity: .35; box-shadow: inset 0 0 0 2px var(--e-steel); }
.sock-pip:focus-visible { outline: 3px solid var(--trim-hi); outline-offset: 2px; }
.sock-none { font-size: 9.5px; color: var(--txt-dim); }

.inv-btn.is-socket { width: 100%; }

.gem-row { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 6px; }
.gem-chip {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: 6px;
  padding: 7px 8px;
  border: 0;
  cursor: pointer;
  background: rgba(0, 0, 0, .26);
  box-shadow: 0 0 0 2px var(--gem-col, var(--e-steel)), inset 2px 2px 0 rgba(255, 255, 255, .05);
  transition: filter .14s linear, transform .1s steps(2);
}
.gem-chip:hover:not(:disabled) { filter: brightness(1.16); transform: translateY(-2px); }
.gem-chip:disabled { cursor: default; opacity: .45; }
.gem-chip:focus-visible { outline: 3px solid var(--trim-hi); outline-offset: 2px; }
.gem-dot {
  width: 12px;
  height: 12px;
  background: var(--gem-col, var(--e-cyan));
  box-shadow: 0 0 8px var(--gem-col, var(--e-cyan));
}
.gem-lab { font-family: var(--font-pixel); font-size: 6.5px; letter-spacing: .5px; color: var(--e-cream); text-align: left; }
.gem-cnt { font-family: var(--font-pixel); font-size: 7px; font-weight: 400; color: var(--gem-col, var(--e-cyan)); }

/* ---------- 13. Skill orb (SKILLS tab icon) ----------
   The three-branch talent grid this section used to dress is gone: the SKILLS tab
   hosts the passive constellation instead, so `.talent-tree`, `.talent-branch`,
   `.talent-node`, `.node-ico`, `.node-rank`, `.branch-head` and their state rules
   went with the markup they styled.

   What replaces them is one glyph, worn in two places — the dock's SKILLS slot and
   the panel's own header — so the button and the surface it opens read as the same
   thing. It is drawn rather than set as an emoji: a blue energy crystal, a diamond
   with a lit facet and a soft aura, which no font can render inconsistently and
   which picks up the dock's grayscale-when-inactive treatment for free.
   ---------------------------------------------------------------------- */
.skill-orb {
  position: relative;
  display: inline-block;
  width: 15px;
  height: 15px;
  flex: 0 0 auto;
  /* the crystal body: brightest at the top-left facet, deepening to the base.
     Stops run white -> cyan -> sky -> blue, i.e. strictly light to dark, so the
     diamond reads as one lit solid rather than two bands. */
  background: linear-gradient(135deg,
    #ffffff 0 14%,
    var(--e-cyan) 14% 42%,
    var(--e-sky) 42% 68%,
    var(--e-blue) 68% 100%);
  /* cut with a clip rather than a rotation so the edges land on whole device
     pixels at the two sizes this is used at */
  clip-path: polygon(50% 0, 100% 50%, 50% 100%, 0 50%);
  box-shadow: 0 0 9px rgba(44, 232, 245, .55);
  animation: skill-orb-pulse 2.6s ease-in-out infinite;
}
/* the aura: a larger diamond bleeding out behind the body */
.skill-orb::before {
  content: '';
  position: absolute;
  left: -4px; top: -4px; right: -4px; bottom: -4px;
  background: radial-gradient(circle at 50% 50%, rgba(44, 232, 245, .42), transparent 68%);
  clip-path: polygon(50% 0, 100% 50%, 50% 100%, 0 50%);
  pointer-events: none;
}
/* the energy half of the crystal: one lit pixel core */
.skill-orb::after {
  content: '';
  position: absolute;
  left: 50%; top: 50%;
  width: 3px; height: 3px;
  margin: -1.5px 0 0 -1.5px;
  background: #ffffff;
  box-shadow: 0 0 6px #ffffff;
  pointer-events: none;
}
@keyframes skill-orb-pulse {
  0%, 100% { box-shadow: 0 0 9px rgba(44, 232, 245, .55); }
  50%      { box-shadow: 0 0 15px rgba(44, 232, 245, .9); }
}
/* In the dock the glyph stands in for an emoji, so it takes the icon metrics and
   inherits .dock-ico's grayscale-when-inactive / lit-when-active filter. */
.dock-ico.skill-orb { width: 17px; height: 17px; }
/* In the panel header it sits on the title's baseline run. */
.inv-title .skill-orb { margin-right: 6px; vertical-align: -2px; }

/* ---- the SKILLS slot and the panel it opens wear the crystal's colour ----
   Every other dock slot lights gold. This one lights cyan, for the same reason
   its icon is a crystal instead of an emoji: the button and the board behind it
   have to read as one object. Follows the .is-town-tab precedent — override the
   two accent properties, inherit the rest of the dock's behaviour. */
.dock-btn.is-skill-tab.active {
  color: var(--e-cyan);
  background: rgba(44, 232, 245, .1);
}
.dock-btn.is-skill-tab::after {
  background: var(--e-cyan);
  box-shadow: 0 0 10px rgba(44, 232, 245, .7);
}
.is-skill-head .inv-title {
  color: var(--e-cyan);
  text-shadow: 0 0 8px rgba(44, 232, 245, .35);
}
/* A hairline in the same colour ties the header to the stage below it, which is
   the darkest box in the panel and otherwise floats free of its own title. */
.is-skill-head {
  padding-bottom: 6px;
  box-shadow: inset 0 -2px 0 rgba(44, 232, 245, .22);
}

/* Centred small print under a panel body. Named for the talent grid it was born
   under; that grid is gone and the Tower's relic note is the remaining user, so
   the name is now generic. */
.panel-hint {
  font-size: 9.5px;
  line-height: 1.7;
  text-align: center;
  color: var(--e-steel);
  overflow-wrap: break-word;
}

/* ---------- 14. Artifacts / prestige ---------- */
.artifact-row { display: grid; grid-template-columns: repeat(3, minmax(0,1fr)); gap: 7px; margin-bottom: 4px; }
.artifact-slot { min-height: 52px; }
.artifact-slot.is-empty .slot-icon { filter: grayscale(1) opacity(.3); }

.prestige-grid { display: grid; grid-template-columns: repeat(3, minmax(0,1fr)); gap: 7px; margin-bottom: 11px; }
.prestige-cell { padding: 9px 5px; text-align: center; background: rgba(0,0,0,.28); box-shadow: 0 0 0 2px var(--line), inset 2px 2px 0 rgba(255,255,255,.05); }
.prestige-label { display: block; font-family: var(--font-pixel); font-size: 7px; letter-spacing: 1px; color: var(--txt-dim); margin-bottom: 6px; }
.prestige-value { font-family: var(--font-pixel); font-size: 11px; font-weight: 400; color: var(--e-mauve); }
.prestige-req { margin-top: 9px; text-align: center; font-size: 10px; color: var(--e-sand); min-height: 1.3em; }

/* ---------- 15. Loot notifications ---------- */
.loot-popups {
  position: fixed;
  top: 14px; right: 14px;
  z-index: 70;
  display: flex;
  flex-direction: column;
  gap: 8px;
  width: min(286px, 74vw);
  pointer-events: none;
}
.loot-pop {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 8px 10px;
  background: var(--panel-bg);
  box-shadow: 0 0 0 3px var(--r, var(--e-steel)), 0 0 15px var(--r-glow, transparent), inset 2px 2px 0 rgba(255,255,255,.08);
  animation: loot-in .3s cubic-bezier(.2,.8,.2,1) both;
}
.loot-pop.r-legendary { animation: loot-in .3s cubic-bezier(.2,.8,.2,1) both, legend-pulse 1.5s ease-in-out .3s infinite; }
.loot-pop.is-out { animation: loot-out .28s ease-in both; }
.loot-icon { flex: 0 0 auto; font-size: 17px; line-height: 1; }
.loot-text { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.loot-text b { font-family: var(--font-pixel); font-size: 8px; font-weight: 400; line-height: 1.5; color: var(--r, var(--e-white)); }
.loot-text i { font-style: normal; font-size: 9.5px; color: var(--txt-dim); }
@keyframes loot-in  { from { opacity: 0; transform: translateX(40px) scale(.94); } to { opacity: 1; transform: none; } }
@keyframes loot-out { from { opacity: 1; transform: none; } to { opacity: 0; transform: translateX(40px) scale(.94); } }

/* ---------- 16. Settings modal (JS-driven) ----------
   game.js's modal controller adds and removes `.hidden`; there is no checkbox
   in the markup any more. z-index must clear `.menu` (120) or the title-screen
   SETTINGS button opens the panel behind the title screen — which is exactly
   what the old CSS-only version did. */
.modal-back {
  position: fixed;
  inset: 0;
  z-index: 10000;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 16px;
}
/* `.hidden` is `display: none !important`, so an open modal is simply one
   without it. All three dialogs share this single rule. */
.modal-scrim {
  position: absolute;
  inset: 0;
  border: 0;
  padding: 0;
  background: rgba(10, 5, 14, .78);
  cursor: pointer;
}
.modal { position: relative; z-index: 1; max-width: 420px; width: 100%; margin: 0; animation: modal-in .22s cubic-bezier(.2,.8,.2,1) both; }
@keyframes modal-in { from { opacity: 0; transform: translateY(10px) scale(.97); } to { opacity: 1; transform: none; } }

/* ---------- passive tree panel ----------
   The board was a dialog once; it is the SKILLS tab now, so the sizing problem
   inverted. A dialog is a box you give a height to and divide up; a tab panel is
   a column inside a scrolling `.tab-body`, where nothing above the stage has a
   definite height to take a fraction OF. So the stage names its own height and
   `#tree-canvas { height: 100% }` finally has something to resolve against —
   without it the canvas measures 0 tall and game.js falls back to 640x420.

   clamp() rather than a fixed number because the board is the panel's whole
   reason to exist: give it half the viewport on a laptop, but never so much on a
   tall monitor that the toolbar and the info line scroll out of reach. */
.is-skill-panel {
  /* The dialog's `gap: 10px` was the ONLY thing separating the toolbar, the
     stage, the info box and the hint — none of them carries a margin. A plain
     block panel would have stacked all four flush, so the column comes with the
     board. `.hidden` still wins: it is display:none !important. */
  display: flex;
  flex-direction: column;
  gap: 10px;
}
/* .inv-head brings a 8px bottom margin of its own; the column's gap does that
   job now, and the two together read as a hole under the title. */
.is-skill-panel .inv-head { margin: 2px 0 0; }
.is-skill-panel .tree-stage { height: clamp(280px, 52vh, 560px); }
/* The toolbar wraps to two rows in the sidebar's width far more often than it did
   at the dialog's 920px, so the rows need to stay legible when they do. */
.is-skill-panel .tree-bar { row-gap: 6px; }
.tree-bar {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
  padding: 7px 9px;
  background: rgba(0, 0, 0, .28);
  box-shadow: inset 0 0 0 2px var(--line);
}
/* The point counter is no longer part of the toolbar: as a tab, the panel has an
   `.inv-head` of its own, and the pool now sits in its `.inv-count` slot where
   every other panel puts its running total. `.tree-pts*` went with the dialog. */
.tree-spent, .tree-zoom {
  font-family: var(--font-pixel);
  font-size: 6.5px;
  letter-spacing: .5px;
  color: var(--txt-dim);
}
.tree-zoom { margin-left: auto; }
.tree-bar .inv-btn { flex: 0 0 auto; }
.tree-bar .inv-btn:disabled { opacity: .45; cursor: not-allowed; filter: grayscale(.7); }

/* ---- search ----
   Sits in the toolbar and takes the slack, so the board's filter is the widest
   thing in the row rather than an afterthought bolted onto the end. */
.tree-search-wrap {
  position: relative;
  flex: 1 1 150px;
  min-width: 110px;
  display: flex;
  align-items: center;
}
.tree-search {
  width: 100%;
  min-width: 0;
  padding: 6px 22px 6px 8px;
  font-family: inherit;
  font-size: 10px;
  color: var(--txt-strong);
  background: var(--jrpg-bg2);
  border: 0;
  box-shadow: inset 0 0 0 2px var(--jrpg-edge);
}
.tree-search::placeholder { color: var(--txt-dim); }
.tree-search:focus-visible { outline: none; box-shadow: inset 0 0 0 2px var(--jrpg-gold); }
/* the browser's own search affordances do not match this UI at all */
.tree-search::-webkit-search-cancel-button,
.tree-search::-webkit-search-decoration { -webkit-appearance: none; appearance: none; }
.tree-search-x {
  position: absolute;
  right: 3px;
  padding: 0 4px;
  border: 0;
  cursor: pointer;
  background: none;
  color: var(--txt-dim);
  font-size: 13px;
  line-height: 1;
}
.tree-search-x:hover { color: var(--e-rose); }
.tree-search-hits,
.tree-respec-cost {
  flex: 0 0 auto;
  font-family: var(--font-pixel);
  font-size: 6px;
  letter-spacing: .5px;
  line-height: 1.7;
  color: var(--trim-hi);
}
/* one class for "the number in front of you is not affordable / not found" */
.tree-search-hits.is-poor,
.tree-respec-cost.is-poor { color: var(--e-rose); }

/* The canvas fills the stage; game.js sizes its backing store to the element's
   own box times the device pixel ratio, so the board stays sharp when zoomed. */
.tree-stage {
  position: relative;
  min-height: 200px;
  /* matches the canvas's own darkest stop, so the 0x0 first frame before the
     modal has been measured does not flash a lighter rectangle */
  background: #06050e;
  box-shadow: inset 0 0 0 2px var(--line);
  overflow: hidden;
}
#tree-canvas {
  display: block;
  width: 100%;
  height: 100%;
  /* the board is vector-drawn, not pixel art — smoothing is correct here */
  image-rendering: auto;
  cursor: grab;
  touch-action: none;   /* the board owns drag; the page must not scroll under it */
}
#tree-canvas:active { cursor: grabbing; }
#tree-canvas:focus-visible { outline: 3px solid var(--trim-hi); outline-offset: -3px; }

/* ---- tier legend ----
   Overlaid on the stage bottom-left. pointer-events:none throughout: it sits on
   top of a draggable surface and must never eat a pan that starts under it. */
.tree-legend {
  position: absolute;
  left: 7px;
  bottom: 6px;
  display: flex;
  flex-wrap: wrap;
  gap: 9px;
  padding: 5px 7px;
  pointer-events: none;
  background: rgba(6, 5, 14, .72);
  box-shadow: inset 0 0 0 1px rgba(120, 142, 200, .22);
}
.tree-lg {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-family: var(--font-pixel);
  font-size: 5px;
  letter-spacing: .5px;
  line-height: 1.7;
  color: var(--txt-dim);
}
/* Each swatch is the same silhouette the canvas draws for that tier, so the
   legend teaches the board rather than describing it. */
.tree-lg i { display: block; background: currentColor; }
.tree-lg.is-minor { color: var(--e-steel); }
.tree-lg.is-minor i { width: 7px; height: 7px; border-radius: 50%; }
.tree-lg.is-notable { color: var(--e-mauve, var(--e-cream)); }
.tree-lg.is-notable i {
  width: 10px; height: 10px; border-radius: 50%;
  background: none;
  box-shadow: inset 0 0 0 2px currentColor, 0 0 0 2px rgba(255, 255, 255, .18);
}
.tree-lg.is-keystone { color: var(--e-yellow); }
.tree-lg.is-keystone i {
  width: 11px; height: 11px;
  clip-path: polygon(50% 0, 93% 25%, 93% 75%, 50% 100%, 7% 75%, 7% 25%);
}

.tree-info {
  display: grid;
  gap: 4px;
  min-height: 54px;
  padding: 8px 9px;
  background: rgba(0, 0, 0, .3);
  box-shadow: inset 0 0 0 2px var(--line);
}
.tree-info-head { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; }
/* The tier badge. game.js writes its colour inline from the node's own branch
   colour, so only the shape and weight live here. */
.tree-info-tier {
  flex: 0 0 auto;
  padding: 2px 5px;
  font-family: var(--font-pixel);
  font-size: 5px;
  letter-spacing: .8px;
  line-height: 1.7;
  color: var(--txt-dim);
  background: rgba(0, 0, 0, .45);
  box-shadow: inset 0 0 0 1px currentColor;
}
.tree-info-tier.is-keystone { box-shadow: inset 0 0 0 1px currentColor, 0 0 8px rgba(254, 231, 97, .35); }
.tree-info-name {
  font-family: var(--font-pixel);
  font-size: 8px;
  font-weight: 400;
  letter-spacing: .8px;
  line-height: 1.6;
  color: var(--e-cream);
  overflow-wrap: break-word;
}
.tree-info-desc { font-size: 10px; line-height: 1.55; color: var(--txt-strong); overflow-wrap: break-word; }
.tree-info-cost {
  font-family: var(--font-pixel);
  font-size: 6.5px;
  letter-spacing: .5px;
  color: var(--trim-hi);
}
.tree-hint { font-size: 9px; line-height: 1.5; color: var(--txt-dim); text-align: center; }

/* The utility rail's tree opener is gone with the dialog — the dock's SKILLS slot
   is the only door now, so `.icon-btn.is-tree` has nothing left to paint. */

@media (max-width: 620px) {
  /* A phone's board is short enough that half the viewport reads as cramped;
     give it a bigger share and drop the furniture that explains it instead. */
  .is-skill-panel .tree-stage { height: clamp(240px, 58vh, 420px); }
  .tree-zoom { display: none; }
  .tree-hint { display: none; }
  /* the legend costs more of a phone's board than it explains */
  .tree-legend { display: none; }
  .tree-search-wrap { flex: 1 1 100%; order: 9; }
  .tree-search-hits { order: 10; }
  .tree-respec-cost { order: 11; }
}

.modal-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 12px; }
.modal-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 12px; }

.meta { display: grid; gap: 8px; }
.meta-row { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 7px 9px; background: rgba(0,0,0,.24); }
.meta-label { font-family: var(--font-pixel); font-size: 7px; letter-spacing: 1.5px; color: var(--txt-dim); }
.meta-value { font-family: var(--font-pixel); font-size: 9px; font-weight: 400; color: var(--e-cream); }

/* --- option row with a pixel switch (state is driven by .is-on from game.js) --- */
.opt-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--gap-lg);
  padding: var(--gap-md);
  margin-bottom: 8px;
  cursor: pointer;
  background: rgba(0, 0, 0, .26);
  box-shadow: 0 0 0 2px var(--line), inset 2px 2px 0 rgba(255, 255, 255, .05);
  transition: filter .15s linear, transform .08s steps(2);
}
.opt-row:hover { filter: brightness(1.14); }
.opt-row:active { transform: translateY(2px); }
.opt-row:focus-visible { outline: 3px solid var(--trim-hi); outline-offset: 2px; }
.opt-text { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.opt-name { font-family: var(--font-pixel); font-size: 7px; letter-spacing: 1px; line-height: 1.6; color: var(--e-cream); }
.opt-desc { font-size: 9.5px; line-height: 1.5; color: var(--txt-dim); }
.opt-switch {
  position: relative;
  flex: 0 0 auto;
  width: 42px;
  height: 22px;
  background: var(--e-black);
  box-shadow: inset 0 0 0 2px var(--e-slate), inset 0 3px 0 rgba(0, 0, 0, .5);
  transition: background .18s linear, box-shadow .18s linear;
}
.opt-knob {
  position: absolute;
  top: 3px;
  left: 3px;
  width: 16px;
  height: 16px;
  background: var(--e-steel);
  box-shadow: inset 0 2px 0 rgba(255, 255, 255, .22), inset 0 -2px 0 rgba(0, 0, 0, .35);
  transition: transform .18s cubic-bezier(.2,.8,.2,1), background .18s linear;
}
.opt-row.is-on .opt-switch {
  background: rgba(44, 232, 245, .22);
  box-shadow: inset 0 0 0 2px var(--e-cyan), inset 0 3px 0 rgba(0, 0, 0, .3), 0 0 10px rgba(44, 232, 245, .4);
}
.opt-row.is-on .opt-knob { transform: translateX(20px); background: var(--e-cyan); }

/* --- segmented graphics control ---
   Same anatomy as .opt-row, but the right-hand side is a set of mutually
   exclusive buttons instead of a single switch. */
.gfx-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--gap-lg);
  padding: var(--gap-md);
  margin-bottom: 8px;
  background: rgba(0, 0, 0, .26);
  box-shadow: 0 0 0 2px var(--line), inset 2px 2px 0 rgba(255, 255, 255, .05);
}
.gfx-seg {
  display: flex;
  flex: 0 0 auto;
  padding: 3px;
  background: var(--e-black);
  box-shadow: inset 0 0 0 2px var(--e-slate), inset 0 3px 0 rgba(0, 0, 0, .45);
}
/* Five resolution options do not fit one row on a narrow settings panel, so this
   variant is allowed to wrap instead of overflowing its box. */
.gfx-seg.is-wide { flex-wrap: wrap; justify-content: flex-end; max-width: 240px; }
.gfx-seg.is-wide .gfx-btn { min-width: 0; padding: 6px 6px; font-size: 6px; }
.gfx-btn {
  min-width: 40px;
  padding: 6px 7px;
  border: 0;
  background: transparent;
  color: var(--e-steel);
  font-family: var(--font-pixel);
  font-size: 7px;
  letter-spacing: .8px;
  cursor: pointer;
  transition: background .14s linear, color .14s linear, box-shadow .14s linear;
}
.gfx-btn:hover { color: var(--e-silver); background: rgba(255, 255, 255, .05); }
.gfx-btn:focus-visible { outline: 3px solid var(--trim-hi); outline-offset: 1px; }
.gfx-btn.active {
  color: var(--e-black);
  background: var(--e-cyan);
  box-shadow: inset 0 -3px 0 rgba(0, 0, 0, .28), 0 0 10px rgba(44, 232, 245, .45);
}

/* The settings modal now carries the whole graphics panel, so it needs to be
   able to scroll on a short viewport instead of overflowing it. */
#settings-modal .modal {
  max-height: calc(100vh - 32px);
  overflow-y: auto;
  overscroll-behavior: contain;
}

.foot { text-align: center; font-size: 9.5px; letter-spacing: .5px; color: var(--e-steel); padding: 2px 0 18px; }
.noscript {
  max-width: 1180px; margin: 0 auto; padding: 12px;
  background: var(--e-crimson); color: var(--e-white);
  font-family: var(--font-pixel); font-size: 9px; text-align: center;
}

/* ---------- 16b. Progression pack: speed, auto-skill, tower, save I/O ---------- */

/* --- level-gated speed control --- */
/* Compact speed selector. Four tiers are kept — 1.5x and 3x are unlockable
   content and dropping them to match a "1x / 2x" brief would delete progression —
   but each button is now small enough that the whole group reads as one chip. */
.speed-ctl {
  display: flex;
  background: var(--jrpg-bg2);
  padding: 2px;
  box-shadow: var(--pxframe), inset 0 2px 0 rgba(0, 0, 0, .4);
}
.speed-btn {
  position: relative;
  font-family: var(--font-pixel);
  font-size: 6.5px;
  letter-spacing: .4px;
  line-height: 1.6;
  padding: 6px 5px;
  min-width: 26px;
  border: 0;
  cursor: pointer;
  color: var(--txt-dim);
  background: transparent;
  text-transform: uppercase;
  transition: background .2s ease, color .2s ease;
}
.speed-btn:hover { color: var(--txt-strong); }
.speed-btn.active { background: var(--jrpg-gold); color: var(--jrpg-edge); box-shadow: inset 0 -2px 0 rgba(0, 0, 0, .28); }
.speed-btn.is-locked { color: #4d5568; cursor: not-allowed; }
.speed-btn.is-locked::after {
  content: '\1F512';
  position: absolute;
  top: -2px; right: -1px;
  font-size: 7px;
  filter: grayscale(1);
}
.speed-btn:focus-visible { outline: 3px solid var(--trim-hi); outline-offset: 2px; }

/* shared lock tooltip */
.lock-tip {
  position: absolute;
  left: 50%; top: calc(100% + 8px);
  transform: translateX(-50%) translateY(4px);
  z-index: 40;
  width: max-content;
  max-width: 190px;
  padding: 6px 8px;
  background: var(--e-black);
  box-shadow: 0 0 0 3px var(--e-steel);
  font-family: var(--font-body);
  font-size: 9.5px;
  line-height: 1.5;
  letter-spacing: 0;
  text-transform: none;
  color: var(--e-sand);
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transition: opacity .14s ease, transform .14s ease, visibility .14s;
}
.speed-btn.is-locked:hover .lock-tip,
.speed-btn.is-locked:focus-visible .lock-tip,
.auto-switch.is-locked:hover .lock-tip,
.auto-switch.is-locked:focus-within .lock-tip {
  opacity: 1;
  visibility: visible;
  transform: translateX(-50%) translateY(0);
}
.speed-ctl .speed-btn:last-child .lock-tip { left: auto; right: 0; transform: translateY(4px); }
.speed-ctl .speed-btn:last-child.is-locked:hover .lock-tip { transform: translateY(0); }

/* The auto-cast switch sits at the right end of the dock row, so its "unlocks at
   Lv 15" tip opens upward and right-aligned rather than off the edge. */
.auto-switch .lock-tip { top: auto; bottom: calc(100% + 8px); left: auto; right: 0; transform: translateY(-4px); }
.auto-switch.is-locked:hover .lock-tip,
.auto-switch.is-locked:focus-within .lock-tip { transform: translateY(0); }

/* --- tower of trials ---
   `min-height` rather than `height` throughout: the relic list grows as relics
   are won, and the Turkish strings are the longest in the game. */
.tower { display: grid; gap: 10px; }
.tower-head { display: flex; align-items: flex-end; justify-content: space-between; gap: 8px; flex-wrap: wrap; }
.tower-floor { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.tower-label {
  font-family: var(--font-pixel);
  font-size: 7px;
  letter-spacing: 1.5px;
  line-height: 1.7;
  color: var(--txt-dim);
  overflow-wrap: break-word;
}
.tower-floor b { font-family: var(--font-pixel); font-size: 20px; font-weight: 400; color: var(--e-cyan); }
.tower-floor.is-best { text-align: right; }
.tower-floor.is-best b { font-size: 14px; color: var(--trim-hi); }
.tower-of { font-family: var(--font-pixel); font-size: 9px; color: var(--e-steel); }

.bar-tower { height: 14px; }
.fill-tower { background: var(--e-sky); }

.tower-timer {
  font-family: var(--font-pixel);
  font-size: 14px;
  text-align: center;
  letter-spacing: 2px;
  color: var(--e-silver);
  padding: 7px;
  background: rgba(0, 0, 0, .3);
  box-shadow: inset 0 0 0 2px var(--e-slate);
}
.tower-timer.is-hot { color: var(--e-red); animation: tower-tick .5s steps(2, end) infinite; }
@keyframes tower-tick { 0%, 100% { opacity: 1; } 50% { opacity: .5; } }

.inv-btn.is-tower { width: 100%; background: var(--e-blue); padding: 12px; }
.tower-bonus {
  font-size: 10px;
  line-height: 1.75;
  color: var(--e-cream);
  text-align: center;
  min-height: 1.4em;
  height: auto;
  overflow-wrap: break-word;
}

/* --- welcome back modal --- */
/* display comes from .modal-back; `.hidden` alone hides it now */
.wb-time { font-size: 10px; text-align: center; color: var(--txt-dim); margin-bottom: 12px; }
.wb-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; }
.wb-cell { padding: 11px 5px; text-align: center; background: rgba(0, 0, 0, .3); box-shadow: 0 0 0 2px var(--line), inset 2px 2px 0 rgba(255, 255, 255, .05); }
.wb-label { display: block; font-family: var(--font-pixel); font-size: 7px; letter-spacing: 1px; color: var(--txt-dim); margin-bottom: 7px; }
.wb-value { font-family: var(--font-pixel); font-size: 13px; font-weight: 400; }
.wb-value.is-gold { color: var(--trim-hi); }
.wb-value.is-ore { color: var(--e-sky); }
.wb-value.is-xp { color: var(--e-green); }
.wb-note { margin-top: 11px; font-size: 9.5px; text-align: center; color: var(--e-steel); }

/* --- save export / import --- */
.io-box {
  width: 100%;
  padding: 8px;
  font-family: var(--font-body);
  font-size: 9.5px;
  line-height: 1.5;
  color: var(--e-silver);
  background: var(--e-black);
  border: 0;
  resize: vertical;
  box-shadow: inset 0 0 0 2px var(--e-slate);
  word-break: break-all;
}
.io-box:focus-visible { outline: 3px solid var(--trim-hi); outline-offset: 2px; }
.io-row { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; margin-top: 8px; }
.io-status { min-height: 1.3em; margin-top: 8px; font-size: 9.5px; text-align: center; color: var(--e-sand); }
.io-status.is-ok { color: var(--e-green); }
.io-status.is-bad { color: var(--e-rose); }

/* ---------- 16c. Polish pack: audio toggles, enrage, achievements ---------- */

.icon-btn.is-audio { background: var(--e-deep); font-size: 13px; padding: 8px 10px; }
.icon-btn.is-audio.is-off { filter: grayscale(1) brightness(.62); }
.icon-btn.is-trophy { background: var(--e-purple); font-size: 13px; padding: 8px 10px; }
/* Leaderboard and patch notes, sharing the trophy's metrics so the utility rail
   stays one height. Distinct grounds only — the emoji carries the meaning. */
.icon-btn.is-board { background: var(--e-blue); font-size: 13px; padding: 8px 10px; }
.icon-btn.is-notes { background: var(--e-brown); font-size: 13px; padding: 8px 10px; }

/* --- boss enrage overlay --- */
.enrage-veil {
  position: absolute;
  inset: 0;
  pointer-events: none;
  opacity: 0;
  box-shadow: inset 0 0 50px rgba(255, 0, 68, .85), inset 0 0 120px rgba(255, 0, 68, .45);
  transition: opacity .3s ease;
}
.enrage-veil.is-on { opacity: 1; animation: enrage-pulse 1.1s ease-in-out infinite; }
@keyframes enrage-pulse {
  0%, 100% { opacity: .45; }
  50%      { opacity: 1; }
}

/* --- revival fade ---
   Covers the arena for the REVIVE_TIME the hero spends down. The point is to make
   dying read as a beat in the loop rather than as a failure state: it dips to
   near-black, shows the reviving message through it, and lifts. game.js toggles
   .is-on from hero.dead, and the timing here matches REVIVE_TIME (1.5s). */
.revive-veil {
  position: absolute;
  inset: 0;
  pointer-events: none;
  opacity: 0;
  background: radial-gradient(120% 90% at 50% 50%, rgba(90, 12, 24, .55), rgba(8, 6, 12, .92));
  transition: opacity .45s ease;
}
.revive-veil.is-on { opacity: 1; animation: revive-dip 1.5s ease-in-out 1 forwards; }
@keyframes revive-dip {
  0%   { opacity: 0; }
  22%  { opacity: 1; }
  70%  { opacity: 1; }
  100% { opacity: .25; }
}
@media (prefers-reduced-motion: reduce) {
  .revive-veil.is-on { animation: none; opacity: .8; }
}

/* ---------- 16c.2 THE LIVE ARENA — duel banner + dock lock ----------

   game.js draws the duel itself (fighters, floating name plates, HP bars, hit
   art) straight onto the canvas. Only two things are DOM, and both are here
   because they want the pixel webfont and a real entry animation:

     .arena-banner   the VS plate on the way in, then the result card
     .arena-lock     the plate that covers the skill dock for the duration

   body.is-arena is on for exactly as long as duel.on is true, which is what lets
   the lock plate appear and disappear with no extra JS.
   --------------------------------------------------------------------------- */

.arena-banner {
  position: absolute;
  left: 50%;
  top: 30%;
  transform: translate(-50%, -50%);
  z-index: 4;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 7px;
  width: max-content;
  max-width: 92%;
  padding: 14px 26px;
  text-align: center;
  font-family: var(--font-pixel);
  /* the plate: a dark slab with a gold keyline, same furniture as .game-msg but
     heavier, because this one is the headline rather than a status line */
  background:
    linear-gradient(180deg, rgba(24, 20, 37, .95), rgba(13, 15, 20, .97));
  box-shadow:
    0 0 0 2px var(--jrpg-edge),
    0 0 0 5px var(--trim),
    0 0 0 7px var(--jrpg-edge),
    0 0 34px rgba(0, 0, 0, .75);
  animation: ab-slam .34s cubic-bezier(.16, 1.3, .4, 1) both;
}

.ab-kicker {
  font-size: 8px;
  letter-spacing: 2px;
  color: var(--txt-dim);
  /* a long rival name must not stretch the plate off the stage */
  max-width: 26ch;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.ab-title {
  font-size: 30px;
  line-height: 1;
  letter-spacing: 3px;
  color: var(--trim-hi);
  /* stacked 1px offsets read as a hard pixel outline, never a blur */
  text-shadow:
    2px 0 0 var(--jrpg-edge), -2px 0 0 var(--jrpg-edge),
    0 2px 0 var(--jrpg-edge), 0 -2px 0 var(--jrpg-edge),
    0 6px 0 rgba(0, 0, 0, .6);
}
.ab-sub {
  font-size: 8.5px;
  line-height: 1.7;
  letter-spacing: 1px;
  color: var(--txt);
}
.ab-reward {
  font-size: 10px;
  letter-spacing: 1.5px;
  padding: 5px 12px;
  color: var(--e-black);
  background: var(--trim);
  box-shadow: 0 0 0 2px var(--jrpg-edge);
}
/* an empty reward slot must not leave a floating gold chip behind — the VS state
   fills it with the rival's tier, the result states with the payout */
.ab-reward:empty { display: none; }

/* --- the three states --- */
.arena-banner.is-vs .ab-title { color: var(--e-cyan); font-size: 34px; }
.arena-banner.is-vs .ab-reward { background: var(--e-sky); color: var(--e-white); }

.arena-banner.is-win {
  box-shadow:
    0 0 0 2px var(--jrpg-edge),
    0 0 0 5px var(--e-yellow),
    0 0 0 7px var(--jrpg-edge),
    0 0 40px rgba(254, 231, 97, .35);
}
.arena-banner.is-win .ab-title { color: var(--e-yellow); }
.arena-banner.is-win .ab-reward { background: var(--e-green); color: var(--e-black); }

.arena-banner.is-lose {
  box-shadow:
    0 0 0 2px var(--jrpg-edge),
    0 0 0 5px var(--e-crimson),
    0 0 0 7px var(--jrpg-edge),
    0 0 40px rgba(162, 38, 51, .4);
}
.arena-banner.is-lose .ab-title { color: var(--e-rose); }
.arena-banner.is-lose .ab-reward { background: var(--e-crimson); color: var(--e-white); }

/* Slams in from slightly above and overshoots once — the impact frame is what
   makes a result feel announced rather than faded in. */
@keyframes ab-slam {
  0%   { opacity: 0; transform: translate(-50%, -50%) scale(1.5); }
  55%  { opacity: 1; transform: translate(-50%, -50%) scale(.94); }
  78%  { transform: translate(-50%, -50%) scale(1.04); }
  100% { opacity: 1; transform: translate(-50%, -50%) scale(1); }
}

/* --- the arena tints the whole stage, so the mode change reads instantly --- */
body.is-arena .stage-inner { box-shadow: inset 0 0 0 3px var(--trim), inset 0 0 70px rgba(0, 0, 0, .5); }
/* the boss plate and the enrage veil belong to a wave and have no business over
   a duel — game.js hides the plate, and this stops a stale veil bleeding through */
body.is-arena .enrage-veil,
body.is-arena .revive-veil { opacity: 0 !important; animation: none !important; }

/* --- the dock lock ---
   Sits over the whole skill dock while a duel runs. The buttons underneath are
   genuinely disabled and castSkill() genuinely refuses, so this plate reports a
   real state rather than merely covering the controls. */
.arena-lock {
  display: none;
  position: absolute;
  inset: 0;
  z-index: 5;
  align-items: center;
  justify-content: center;
  gap: 10px;
  font-family: var(--font-pixel);
  font-size: 9px;
  letter-spacing: 2px;
  color: var(--trim-hi);
  background: rgba(13, 15, 20, .88);
  box-shadow: inset 0 0 0 2px var(--trim);
}
.al-icon { font-size: 15px; animation: al-tick 1.4s steps(2) infinite; }
@keyframes al-tick {
  0%, 100% { transform: rotate(-14deg); }
  50%      { transform: rotate(14deg); }
}
.skill-dock { position: relative; }
body.is-arena .arena-lock { display: flex; }
/* the dock behind the plate is inert; grey it so a hotkey press reads as refused
   rather than as broken */
body.is-arena .skill-bar,
body.is-arena .auto-switch,
body.is-arena .buff-strip { filter: grayscale(1) brightness(.55); }

@media (prefers-reduced-motion: reduce) {
  .arena-banner { animation: none; }
  .al-icon { animation: none; }
}

@media (max-width: 700px) {
  .arena-banner { padding: 10px 16px; gap: 5px; }
  .ab-title { font-size: 21px; letter-spacing: 2px; }
  .arena-banner.is-vs .ab-title { font-size: 24px; }
  .ab-sub, .ab-kicker { font-size: 7.5px; }
  .ab-reward { font-size: 8.5px; padding: 4px 8px; }
  .arena-lock { font-size: 7.5px; letter-spacing: 1px; }
}

/* --- achievement toasts (bottom-right) --- */
.ach-toasts {
  position: fixed;
  right: 14px;
  bottom: 14px;
  z-index: 75;
  display: flex;
  flex-direction: column;
  gap: 8px;
  width: min(300px, 78vw);
  pointer-events: none;
}
.ach-toast {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 9px 11px;
  background: var(--panel-bg);
  box-shadow:
    0 0 0 3px var(--trim),
    0 0 18px rgba(254, 174, 52, .45),
    inset 2px 2px 0 rgba(255, 255, 255, .1);
  animation: ach-in .34s cubic-bezier(.2, .8, .2, 1) both;
}
.ach-toast.is-out { animation: ach-out .3s ease-in both; }
.ach-ico { flex: 0 0 auto; font-size: 19px; line-height: 1; }
.ach-body { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.ach-kicker { font-family: var(--font-pixel); font-size: 6.5px; letter-spacing: 1.5px; color: var(--trim-hi); }
.ach-name { font-family: var(--font-pixel); font-size: 8px; line-height: 1.5; color: var(--e-white); }
.ach-prize { font-size: 9.5px; color: var(--e-green); }
@keyframes ach-in  { from { opacity: 0; transform: translateX(46px) scale(.94); } to { opacity: 1; transform: none; } }
@keyframes ach-out { from { opacity: 1; transform: none; } to { opacity: 0; transform: translateX(46px) scale(.94); } }

/* --- achievements modal --- */
/* display comes from .modal-back; `.hidden` alone hides it now */
.modal.is-tall { max-width: 460px; }
.ach-count { font-family: var(--font-pixel); font-size: 10px; text-align: center; color: var(--txt-dim); margin-bottom: 11px; }
.ach-count b { font-weight: 400; color: var(--trim-hi); }

.ach-list { display: grid; gap: 7px; max-height: 52vh; overflow-y: auto; padding-right: 3px; }
.ach-list::-webkit-scrollbar { width: 10px; }
.ach-list::-webkit-scrollbar-track { background: var(--e-black); }
.ach-list::-webkit-scrollbar-thumb { background: var(--e-steel); box-shadow: inset 2px 2px 0 rgba(255, 255, 255, .14); }
.ach-list { scrollbar-width: thin; scrollbar-color: var(--e-steel) var(--e-black); }

.ach-row {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 8px 9px;
  background: rgba(0, 0, 0, .26);
  box-shadow: 0 0 0 2px var(--line), inset 2px 2px 0 rgba(255, 255, 255, .04);
}
.ach-row.is-done {
  background: rgba(254, 174, 52, .12);
  box-shadow: 0 0 0 2px var(--trim), inset 2px 2px 0 rgba(255, 255, 255, .08);
}
.ach-row .ach-ico { font-size: 16px; filter: grayscale(1) opacity(.45); }
.ach-row.is-done .ach-ico { filter: none; }
.ach-row .ach-name { font-size: 7.5px; color: var(--txt-dim); }
.ach-row.is-done .ach-name { color: var(--e-white); }
.ach-desc { font-size: 9.5px; color: var(--e-steel); margin-top: 3px; }
.ach-row.is-done .ach-desc { color: var(--e-cream); }
.ach-tick { margin-left: auto; flex: 0 0 auto; font-family: var(--font-pixel); font-size: 9px; color: var(--e-steel); }
.ach-row.is-done .ach-tick { color: var(--e-green); }

/* ---------- 16e. Party roster ---------- */
.party-head { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; margin: 2px 0 8px; }

/* --- formation strip --- */
.party-formation {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 5px;
  margin-bottom: 4px;
}
.party-slot {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 3px;
  padding: 8px 3px 6px;
  border: 0;
  cursor: pointer;
  background: rgba(0, 0, 0, .32);
  box-shadow:
    0 0 0 2px var(--line),
    inset 2px 2px 0 rgba(255, 255, 255, .06),
    inset 0 -4px 0 var(--h-accent, var(--e-steel));
  transition: filter .14s linear, transform .1s steps(2);
}
.party-slot:hover { filter: brightness(1.16); transform: translateY(-2px); }
.party-slot:focus-visible { outline: 3px solid var(--trim-hi); outline-offset: 2px; }
.party-ico { font-size: 17px; line-height: 1; }
.party-name {
  font-family: var(--font-pixel);
  font-size: 5.5px;
  letter-spacing: .3px;
  line-height: 1.5;
  text-align: center;
  color: var(--txt-dim);
}
.party-lv { font-family: var(--font-pixel); font-size: 7px; font-weight: 400; color: var(--h-accent, var(--txt-strong)); }
.party-slot.is-empty { filter: grayscale(1) brightness(.6); }
.party-slot.is-empty .party-lv { color: var(--e-steel); font-size: 5.5px; }

/* one accent per class, shared by the strip and the roster rows */
.is-knight { --h-accent: var(--e-silver); }
.is-ranger { --h-accent: var(--e-green); }
.is-mage   { --h-accent: var(--e-sky); }
.is-cleric { --h-accent: var(--trim-hi); }

/* --- roster rows --- */
.hero-list { display: grid; gap: 9px; }
.hero-row {
  padding: 9px;
  background: var(--panel-bg2);
  box-shadow:
    0 0 0 3px var(--line),
    inset 3px 3px 0 rgba(255, 255, 255, .08),
    inset -3px -3px 0 rgba(0, 0, 0, .3),
    inset 5px 0 0 var(--h-accent, var(--e-steel));
  transition: filter .15s linear;
}
.hero-row:hover { filter: brightness(1.07); }
.hero-row.is-empty { box-shadow: 0 0 0 3px var(--line), inset 3px 3px 0 rgba(255,255,255,.04), inset 5px 0 0 var(--e-steel); }
.hero-row.is-empty .hero-port { filter: grayscale(1) opacity(.5); }
.hero-row.is-maxed { box-shadow: 0 0 0 3px var(--trim), inset 3px 3px 0 rgba(255,255,255,.09), inset 5px 0 0 var(--h-accent); }

.hero-head { display: flex; align-items: flex-start; gap: 9px; margin-bottom: 8px; }
.hero-port { font-size: 21px; line-height: 1.1; flex: 0 0 auto; }
.hero-id { min-width: 0; flex: 1 1 auto; }
.hero-name { font-family: var(--font-pixel); font-size: 8px; letter-spacing: .8px; line-height: 1.6; color: var(--txt-strong); }
.hero-role {
  display: inline-block;
  margin-top: 4px;
  padding: 2px 5px;
  font-family: var(--font-pixel);
  font-size: 5.5px;
  letter-spacing: 1px;
  color: var(--e-black);
  background: var(--h-accent, var(--e-steel));
}
.hero-stat { margin-top: 5px; font-size: 10px; line-height: 1.55; color: var(--txt-dim); }
.hero-lv {
  flex: 0 0 auto;
  font-family: var(--font-pixel);
  font-size: 9px;
  font-weight: 400;
  color: var(--h-accent, var(--e-cyan));
}

/* --- milestone pips ---
   A real progress bar behind five perk gates: the fill shows how far the hero
   has climbed toward the next milestone, the pips show which perks are banked. */
.ms-track {
  position: relative;
  display: flex;
  gap: 3px;
  margin-bottom: 8px;
  padding: 3px;
  background: var(--e-black);
  box-shadow: inset 0 0 0 2px rgba(255, 255, 255, .05);
}
.ms-pip {
  position: relative;
  z-index: 1;
  flex: 1 1 0;
  padding: 5px 0;
  text-align: center;
  font-family: var(--font-pixel);
  font-size: 6px;
  letter-spacing: .3px;
  color: var(--e-steel);
  background: rgba(255, 255, 255, .04);
  cursor: help;
  transition: color .16s linear, background .16s linear, box-shadow .16s linear;
}
.ms-pip.is-on {
  color: var(--e-black);
  background: var(--h-accent, var(--trim-hi));
  box-shadow: 0 0 0 1px var(--line), 0 0 9px rgba(254, 231, 97, .45);
}
/* the next gate breathes so the target is obvious at a glance */
.ms-pip:not(.is-on) + .ms-pip:not(.is-on) { animation: none; }
.ms-pip.is-on + .ms-pip:not(.is-on),
.ms-track > .ms-pip:first-child:not(.is-on) {
  color: var(--e-cream);
  background: rgba(255, 255, 255, .1);
  box-shadow: inset 0 0 0 2px var(--h-accent, var(--trim));
}
.hero-next {
  margin-bottom: 8px;
  font-size: 9.5px;
  line-height: 1.5;
  color: var(--e-cream);
}
.hero-row.is-maxed .hero-next { color: var(--trim-hi); }

.hero-foot { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.hero-cost { font-family: var(--font-pixel); font-size: 7px; letter-spacing: .5px; line-height: 1.7; color: var(--trim-hi); }
.buy-btn.is-hero { flex: 0 0 auto; background: var(--e-blue); font-size: 7px; padding: 9px 11px; min-height: 32px; }
.hero-row.is-empty .buy-btn.is-hero { background: var(--e-purple); }

/* ---------- 16d. Town hub: village mode, nodes, NPC dialogue ---------- */

/* --- town mode ---
   game.js puts .is-town on <body> while the TOWN tab is active. The stage column
   folds away and the sidebar takes the full grid, which is what turns a 348px
   panel into a proper village screen without moving it in the DOM. */
body.is-town .main { grid-template-columns: minmax(0, 1fr); }
body.is-town .stage-column { display: none; }
body.is-town .tab-body { max-height: none; }
.dock-btn.is-town-tab.active { color: var(--e-cream); }

.town-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  margin-bottom: 10px;
}
.inv-btn.is-back { background: var(--e-steel); font-size: 7px; padding: 8px 10px; min-height: 30px; }

/* --- material purse --- */
.town-mats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(86px, 1fr));
  gap: 6px;
  margin-bottom: 10px;
}
.mat-cell {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 6px 8px;
  background: rgba(0, 0, 0, .34);
  box-shadow:
    0 0 0 2px var(--line),
    inset 2px 2px 0 rgba(255, 255, 255, .05),
    inset 4px 0 0 var(--mat-accent, var(--e-steel));
}
.mat-ico { font-size: 13px; line-height: 1; flex: 0 0 auto; }
.mat-label { font-family: var(--font-pixel); font-size: 6px; letter-spacing: 1px; color: var(--txt-dim); }
.mat-val {
  margin-left: auto;
  font-family: var(--font-pixel);
  font-size: 9px;
  font-weight: 400;
  color: var(--mat-accent, var(--txt-strong));
}
.mat-cell.is-wood     { --mat-accent: var(--e-clay); }
.mat-cell.is-stone    { --mat-accent: var(--e-grey); }
.mat-cell.is-obsidian { --mat-accent: var(--e-mauve); }
.mat-cell.is-mythic   { --mat-accent: var(--e-cyan); }
.mat-cell.is-token    { --mat-accent: var(--e-amber); }
.mat-cell.is-map      { --mat-accent: var(--e-green); }

/* --- backdrop + interactive nodes --- */
.town-stage {
  position: relative;
  line-height: 0;
  background: #0b0912;
  overflow: hidden;
  box-shadow: 0 0 0 3px var(--line), inset 0 0 0 2px var(--e-slate), 0 5px 0 0 rgba(0, 0, 0, .42);
}
.town-stage canvas { width: 100%; height: auto; }
.town-stage .crt-scan, .town-stage .crt-vig { z-index: 2; }

.town-nodes { position: absolute; inset: 0; z-index: 5; }

.town-node {
  position: absolute;
  transform: translateX(-50%);
  background: transparent;
  border: 0;
  padding: 0;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: filter .14s linear, transform .1s steps(2);
}
.town-node::before {
  /* the hit plate itself stays invisible until hovered, so the pixel art reads */
  content: '';
  position: absolute;
  inset: -3px;
  box-shadow: inset 0 0 0 2px transparent;
  transition: box-shadow .14s linear, background .14s linear;
}
.town-node:hover { transform: translateX(-50%) translateY(-3px); }
.town-node:hover::before { box-shadow: inset 0 0 0 2px var(--trim-hi); background: rgba(254, 231, 97, .1); }
.town-node:focus-visible { outline: 3px solid var(--trim-hi); outline-offset: 2px; }
.town-node.is-ruin { filter: grayscale(.5) brightness(.9); }

.node-plate {
  position: absolute;
  left: 50%;
  top: 100%;
  transform: translateX(-50%);
  margin-top: 5px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  padding: 4px 6px;
  white-space: nowrap;
  background: rgba(24, 20, 37, .84);
  box-shadow: 0 0 0 2px var(--line), inset 0 2px 0 rgba(255, 255, 255, .08);
  pointer-events: none;
}
.node-title { font-family: var(--font-pixel); font-size: 6px; letter-spacing: .6px; color: var(--e-cream); }
.node-lv { font-family: var(--font-pixel); font-size: 6px; color: var(--e-cyan); }
.town-node.is-ruin .node-lv { color: var(--e-steel); }
.town-node.is-maxed .node-lv { color: var(--trim-hi); }

/* an affordable upgrade gets a soft pulse — the one nudge the village gives you */
.town-node.is-ready .node-plate {
  box-shadow: 0 0 0 2px var(--trim), 0 0 12px rgba(254, 174, 52, .5);
  animation: node-pulse 1.9s ease-in-out infinite;
}
@keyframes node-pulse {
  0%, 100% { box-shadow: 0 0 0 2px var(--trim), 0 0 8px rgba(254, 174, 52, .35); }
  50%      { box-shadow: 0 0 0 2px var(--trim-hi), 0 0 16px rgba(254, 231, 97, .65); }
}

/* --- active buff strip --- */
.buff-row { display: flex; flex-wrap: wrap; gap: 6px; margin: 10px 0 0; }
.buff-chip {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 5px 8px;
  background: rgba(0, 0, 0, .36);
  box-shadow: 0 0 0 2px var(--buff-accent, var(--e-steel)), inset 2px 2px 0 rgba(255, 255, 255, .06);
}
.buff-ico { font-size: 12px; line-height: 1; }
.buff-name { font-family: var(--font-pixel); font-size: 6px; letter-spacing: .8px; color: var(--e-cream); }
.buff-time { font-family: var(--font-pixel); font-size: 7px; font-weight: 400; color: var(--buff-accent, var(--txt-strong)); }
.buff-chip.is-rested { --buff-accent: var(--e-green); }
.buff-chip.is-eatk   { --buff-accent: var(--e-red); }
.buff-chip.is-ehp    { --buff-accent: var(--e-rose); }
.buff-chip.is-ehaste { --buff-accent: var(--e-cyan); }
.buff-chip.is-steal  { --buff-accent: var(--e-crimson); }
.buff-chip.is-ward   { --buff-accent: var(--e-sky); }
.buff-chip.is-drop   { --buff-accent: var(--trim-hi); }

/* --- NPC dialogue box --- */
.dialog {
  margin-top: 10px;
  padding: 10px;
  background: var(--panel-bg2);
  box-shadow:
    0 0 0 3px var(--line),
    inset 3px 3px 0 rgba(255, 255, 255, .08),
    inset -3px -3px 0 rgba(0, 0, 0, .32);
  animation: dlg-in .2s cubic-bezier(.2, .8, .2, 1) both;
}
@keyframes dlg-in { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }

.dlg-top { display: flex; align-items: flex-start; gap: 10px; }
.dlg-portrait {
  flex: 0 0 auto;
  padding: 3px;
  background: var(--e-black);
  box-shadow: 0 0 0 2px var(--trim), inset 0 0 0 2px var(--e-slate);
}
.dlg-portrait canvas {
  display: block;
  width: 64px;
  height: 64px;
  image-rendering: pixelated;
  image-rendering: crisp-edges;
}
.dlg-body { min-width: 0; flex: 1 1 auto; }
.dlg-name {
  display: block;
  font-family: var(--font-pixel);
  font-size: 8px;
  letter-spacing: 1px;
  line-height: 1.6;
  color: var(--trim-hi);
  text-shadow: 0 2px 0 var(--e-black);
}
.dlg-text {
  margin-top: 7px;
  font-size: 11px;
  line-height: 1.65;
  color: var(--e-cream);
  /* a caret-height min stops the box jumping while the typewriter runs */
  min-height: 3.3em;
}
.dlg-log { margin-top: 8px; font-size: 10px; min-height: 1.4em; color: var(--txt-dim); }
.dlg-log.is-win { color: var(--e-green); }
.dlg-log.is-fail { color: var(--e-rose); }

.dlg-heading {
  margin: 12px 0 7px;
  padding-bottom: 5px;
  font-family: var(--font-pixel);
  font-size: 7px;
  letter-spacing: 1.2px;
  line-height: 1.7;
  color: var(--trim);
  box-shadow: inset 0 -2px 0 var(--e-slate);
}
.dlg-heading:first-child { margin-top: 8px; }

.dlg-actions { max-height: 340px; overflow-y: auto; padding-right: 3px; }
.dlg-actions::-webkit-scrollbar { width: 10px; }
.dlg-actions::-webkit-scrollbar-track { background: var(--e-black); }
.dlg-actions::-webkit-scrollbar-thumb { background: var(--e-steel); box-shadow: inset 2px 2px 0 rgba(255, 255, 255, .14); }
.dlg-actions { scrollbar-width: thin; scrollbar-color: var(--e-steel) var(--e-black); }

.dlg-row {
  padding: 8px;
  margin-bottom: 7px;
  background: rgba(0, 0, 0, .28);
  box-shadow: 0 0 0 2px var(--line), inset 2px 2px 0 rgba(255, 255, 255, .05);
  transition: filter .14s linear;
}
.dlg-row:hover { filter: brightness(1.12); }
.dlg-row:last-child { margin-bottom: 0; }
.dlg-row.is-done { opacity: .58; }
.dlg-row.is-ready { box-shadow: 0 0 0 2px var(--e-green), inset 2px 2px 0 rgba(255, 255, 255, .07); }

.dlg-row-head { display: flex; align-items: flex-start; gap: 8px; margin-bottom: 8px; }
.dlg-row-ico { font-size: 15px; line-height: 1.2; flex: 0 0 auto; }
.dlg-row-body { min-width: 0; }
.dlg-row-name { font-family: var(--font-pixel); font-size: 7px; letter-spacing: .6px; line-height: 1.7; color: var(--txt-strong); }
.dlg-row-sub { margin-top: 4px; font-size: 10px; line-height: 1.55; color: var(--txt-dim); }
.dlg-row-foot { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.dlg-cost { font-family: var(--font-pixel); font-size: 6.5px; letter-spacing: .5px; line-height: 1.7; color: var(--trim-hi); }
.dlg-row-foot .buy-btn { flex: 0 0 auto; font-size: 7px; padding: 8px 10px; min-height: 30px; }
.dlg-row-foot .buy-btn.is-drop { background: var(--e-crimson); }

/* ---------- the market shelf clock ----------
   The stock rotates on the real clock, so the counter has to say when. The bar is
   the countdown made visual — it drains left to right across the hour, and goes
   amber for the last five minutes, because the length of a short bar is hard to
   read at a glance while a colour change is not. Sits between the shelf heading
   and the goods, which is where the eye already passes on the way down. */
.mkt-clock {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 8px;
  margin-bottom: 7px;
  background: rgba(0, 0, 0, .28);
  box-shadow: 0 0 0 2px var(--line), inset 2px 2px 0 rgba(255, 255, 255, .05);
}
.mkt-clock-lab {
  flex: 0 0 auto;
  font-family: var(--font-pixel);
  font-size: 6px;
  letter-spacing: .5px;
  line-height: 1.7;
  color: var(--trim-hi);
}
.mkt-clock-bar {
  position: relative;
  flex: 1 1 auto;
  min-width: 40px;
  height: 8px;
  overflow: hidden;
  background: var(--e-black);
  box-shadow: inset 0 0 0 2px var(--e-slate);
}
.mkt-clock-fill {
  display: block;
  width: 100%;
  height: 100%;
  background: var(--e-cyan);
  /* stepped, not smooth: the whole UI is pixel art and a sub-pixel ease on an
     8px bar only reads as blur */
  transition: width .25s steps(8, end);
}
.mkt-clock.is-soon .mkt-clock-fill { background: var(--e-amber); }
.mkt-clock.is-soon .mkt-clock-lab { color: var(--e-amber); }

@media (prefers-reduced-motion: reduce) {
  .mkt-clock-fill { transition: none; }
}

/* ---------- auction house: the consign row ----------
   The only dialogue row with real form controls in it. Both fields are full
   width so the row stays usable at phone width, where the dialogue column is
   narrow enough that a side-by-side select and number box would collide. */
.dlg-row.is-ah-sell .dlg-row-body { width: 100%; }

.ah-select,
.ah-price {
  display: block;
  width: 100%;
  margin-top: 6px;
  padding: 7px 8px;
  font-family: inherit;
  font-size: 10px;
  line-height: 1.4;
  color: var(--txt-strong);
  background: rgba(0, 0, 0, .42);
  border: 0;
  box-shadow: inset 0 0 0 2px var(--line);
  -webkit-appearance: none;
  appearance: none;
}
.ah-select { cursor: pointer; }
/* the arrow the appearance reset removed, drawn back as a chevron */
.ah-select {
  background-image: linear-gradient(45deg, transparent 50%, var(--trim-hi) 50%),
                    linear-gradient(135deg, var(--trim-hi) 50%, transparent 50%);
  background-position: right 12px center, right 7px center;
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat;
  padding-right: 24px;
}
.ah-select option { background: var(--e-ink, #16121f); color: var(--txt-strong); }

.ah-select:focus-visible,
.ah-price:focus-visible {
  outline: none;
  box-shadow: inset 0 0 0 2px var(--trim-hi);
}

.ah-price-wrap { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.ah-price { flex: 1 1 120px; min-width: 0; }
/* the spinners are useless at this size and steal width */
.ah-price::-webkit-outer-spin-button,
.ah-price::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.ah-price[type='number'] { -moz-appearance: textfield; appearance: textfield; }

.ah-fee {
  flex: 0 0 auto;
  font-family: var(--font-pixel);
  font-size: 6.5px;
  letter-spacing: .5px;
  line-height: 1.7;
  color: var(--txt-dim);
}

/* the gear-wall readout borrows .empty-state and turns it red */
.empty-state.is-gate {
  color: var(--e-rose);
  background: rgba(228, 59, 68, .12);
  box-shadow: inset 0 0 0 2px var(--e-crimson);
  font-size: 7px;
  line-height: 2;
}

/* ---------- 17. Utilities ---------- */
/* game.js toggles this on panels and the boss banner — keep it. */
.hidden { display: none !important; }

/* ---------- 16f. Title / main menu ---------- */
.menu {
  position: fixed;
  inset: 0;
  z-index: 120;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px 16px;
  background:
    radial-gradient(120% 80% at 50% 0, rgba(104, 56, 108, .5), transparent 60%),
    linear-gradient(180deg, #1b1730 0%, #12101c 55%, #0a0812 100%);
  /* game.js drives the fade with .is-out, then hides the node on transitionend */
  opacity: 1;
  transition: opacity .55s ease, transform .55s cubic-bezier(.2, .8, .2, 1);
}
.menu.is-out { opacity: 0; transform: scale(1.06); pointer-events: none; }

/* drifting starfield built from two repeating gradients — no assets */
.menu-stars {
  position: absolute;
  inset: -20%;
  pointer-events: none;
  opacity: .5;
  background-image:
    radial-gradient(1px 1px at 20% 30%, #fff 99%, transparent),
    radial-gradient(1px 1px at 70% 15%, var(--e-cyan) 99%, transparent),
    radial-gradient(1px 1px at 45% 70%, #fff 99%, transparent),
    radial-gradient(1px 1px at 85% 55%, var(--e-mauve) 99%, transparent),
    radial-gradient(1px 1px at 10% 85%, #fff 99%, transparent);
  background-size: 220px 220px, 300px 300px, 180px 180px, 260px 260px, 340px 340px;
  animation: stars-drift 60s linear infinite;
}
@keyframes stars-drift { to { transform: translate3d(-220px, -180px, 0); } }

.menu-inner {
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 18px;
  text-align: center;
}

/* --- pulsing logo: three stacked words, each on its own beat --- */
.menu-logo {
  display: flex;
  flex-direction: column;
  gap: 4px;
  margin: 0;
  font-family: var(--font-pixel);
  line-height: 1.15;
}
.logo-line {
  font-size: clamp(30px, 9vw, 62px);
  letter-spacing: 4px;
  color: var(--trim-hi);
  /* stacked hard shadows read as a chunky arcade bevel */
  text-shadow:
    3px 3px 0 var(--e-brick),
    6px 6px 0 var(--e-ink),
    0 0 26px rgba(254, 174, 52, .55);
  animation: logo-beat 2.4s ease-in-out infinite;
}
.logo-line.is-b { color: var(--e-cyan); text-shadow: 3px 3px 0 var(--e-blue), 6px 6px 0 var(--e-ink), 0 0 26px rgba(44, 232, 245, .5); animation-delay: .18s; }
.logo-line.is-c { color: var(--e-rose); text-shadow: 3px 3px 0 var(--e-crimson), 6px 6px 0 var(--e-ink), 0 0 26px rgba(246, 117, 122, .5); animation-delay: .36s; }
@keyframes logo-beat {
  0%, 100% { transform: translateY(0) scale(1); }
  50%      { transform: translateY(-5px) scale(1.035); }
}

.menu-tag {
  font-family: var(--font-pixel);
  font-size: 8px;
  letter-spacing: 3px;
  color: var(--e-mauve);
}

.menu-actions { display: flex; flex-direction: column; gap: 10px; width: min(300px, 82vw); margin-top: 6px; }
.menu-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  padding: 15px 18px;
  border: 0;
  cursor: pointer;
  font-family: var(--font-pixel);
  font-size: 11px;
  letter-spacing: 2px;
  text-transform: uppercase;
  color: var(--e-white);
  background: linear-gradient(180deg, #6fbf5b, var(--e-forest) 60%, #2f6b38);
  box-shadow:
    inset 0 3px 0 rgba(255, 255, 255, .22),
    inset 0 -4px 0 rgba(0, 0, 0, .38),
    var(--ring),
    0 6px 0 0 var(--line);
  transition: transform .08s steps(2), filter .15s linear, box-shadow .1s linear;
}
.mb-ico { font-size: 12px; line-height: 1; }
.menu-btn:hover { filter: brightness(1.16); transform: translateY(-2px); }
.menu-btn:active {
  transform: translateY(3px);
  box-shadow: inset 0 3px 0 rgba(255, 255, 255, .16), inset 0 -2px 0 rgba(0, 0, 0, .4), var(--ring);
}
.menu-btn:focus-visible { outline: 3px solid var(--trim-hi); outline-offset: 3px; }
.menu-btn.is-new { background: linear-gradient(180deg, #9a52a0, var(--e-purple) 60%, #4b2750); }
.menu-btn.is-set { background: linear-gradient(180deg, #6e7ea6, var(--e-steel) 60%, #333c5e); }

.menu-save {
  font-family: var(--font-pixel);
  font-size: 7px;
  line-height: 1.9;
  letter-spacing: 1px;
  color: var(--e-steel);
  max-width: 320px;
}


/* ---------- 18. Responsive ---------- */

/* The HUD's own breakpoints now live beside the .hud rules in section 5, where
   the flex behaviour they modify is defined — the stack that used to be here
   drove `grid-template-columns` and `grid-column`, both of which became inert
   the moment .hud stopped being a grid. What is left here is the surrounding
   layout, which is still grid. */

@media (max-width: 1080px) {
  .main { grid-template-columns: minmax(0,1fr); }
  /* the sidebar is full-width now, so let its panels run as tall as they like */
  .tab-body { max-height: none; }
}

@media (max-width: 700px) {
  /* three equal pills read better than three ragged ones once they have a row
     to themselves (see the 1180px rule that puts them there) */
  .pill-row { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .pill { padding: 5px 7px; }
  .pill-val { font-size: 10px; }
}

@media (max-width: 560px) {
  body {
    font-size: 12px;
    padding: 8px;
    padding-bottom: calc(var(--dock-h) + 8px + env(safe-area-inset-bottom, 0px));
  }
  h1, h2 { font-size: 11px; }
  .panel { padding: 10px; }
  .hud { padding: 10px; }
  .bar-stack { grid-template-columns: minmax(0,1fr); gap: 5px; }

  /* avatar and wave type shrink together so the HUD keeps one rhythm */
  .avatar { width: 50px; height: 50px; }
  .av-face { font-size: 18px; }
  .wave-num { font-size: 15px; }
  .zone-name { font-size: 7px; }

  /* Eight slots will not fit side by side on a phone, and letting the grid wrap
     to a second row would double the dock's height — which --dock-h has already
     reserved as body padding, so the last row of content would go under it.
     One scrolling row instead: the height stays fixed and every tab stays
     reachable. */
  .dock {
    grid-template-columns: none;
    grid-auto-flow: column;
    grid-auto-columns: minmax(62px, 1fr);
    overflow-x: auto;
    overflow-y: hidden;
    scrollbar-width: none;
  }
  .dock::-webkit-scrollbar { height: 0; }
  .dock-btn { min-height: 50px; font-size: 5.5px; padding: 6px 1px; }
  .dock-ico { font-size: 16px; }
  .buy-dock { flex-wrap: wrap; justify-content: center; }
  /* the base .sub-btn is already a column; this just tightens it for phones */
  .sub-btn { gap: 3px; font-size: 5.5px; min-height: 44px; padding: 7px 3px; }

  .wb-grid { grid-template-columns: minmax(0, 1fr); }
  .io-row { grid-template-columns: minmax(0, 1fr); }
  .speed-btn { min-width: 28px; padding: 6px 4px; font-size: 7px; }
  .equip-row { grid-template-columns: repeat(2, minmax(0,1fr)); }
  .inv-actions { grid-template-columns: minmax(0,1fr); }
  .forge-actions { grid-template-columns: minmax(0,1fr); }
  /* Tooltips on a phone: cap the width against the VIEWPORT, never with a fixed
     `width`. A hard width here was re-introducing exactly the clipping the base
     rule was rewritten to fix — a long Turkish item name ("Usta işi Deri Zırh")
     needs to wrap, and it can only wrap if the box is allowed to size itself. */
  .item-tip {
    min-width: 0;
    width: max-content;
    max-width: calc(100vw - 24px);
  }
  /* both stacks sit above the fixed dock so nothing hides behind it */
  .loot-popups { top: auto; bottom: calc(var(--dock-h) + 46px); right: 8px; left: 8px; width: auto; }
  .ach-toasts { right: 8px; left: 8px; bottom: calc(var(--dock-h) + 6px); width: auto; }
  .prestige-grid { grid-template-columns: minmax(0,1fr); }

  /* skills stay on one row — they are the primary combat control */
  .skill-dock { padding: 8px; gap: 8px; }
  .dock-row { gap: 10px; }
  .skill-bar { gap: 7px; }
  /* still square, still framed — only the edge length shrinks, and min-width /
     min-height have to come down with it or the base 64px floor wins */
  .skill-btn { width: 56px; height: 56px; min-width: 56px; min-height: 56px; }
  .skill-icon { font-size: 15px; }
  .auto-switch { padding: 6px 8px; gap: 5px; }

  /* town: the name plates would collide at phone width, so hide all but the
     level and let the pixel art carry the identification */
  .town-mats { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .node-title { display: none; }
  .node-plate { padding: 2px 4px; margin-top: 2px; }
  .dlg-portrait canvas { width: 48px; height: 48px; }
  .dlg-top { gap: 8px; }
  .dlg-row-foot { flex-direction: column; align-items: stretch; gap: 6px; }
  .dlg-row-foot .buy-btn { width: 100%; }
  .dlg-actions { max-height: none; }

  /* roster: two-up formation strip, stacked buy row */
  .party-formation { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .hero-foot { flex-direction: column; align-items: stretch; gap: 6px; }
  .buy-btn.is-hero { width: 100%; }
  .ms-pip { font-size: 5.5px; }
}

@media (max-width: 400px) {
  /* Two-up grid for the counters. `grid-template-columns` was already here but
     .pill-row is display:flex, so the declaration did nothing and the three
     pills kept flexing on one line — which is what squeezed the gold value. The
     display switch is the half that was missing. */
  .pill-row {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    justify-content: stretch;
  }
  /* a grid item may not shrink below its content, so the pill stays legible */
  .pill { min-width: 0; padding: 4px 10px; }
  .pill.is-mythic { grid-column: 1 / -1; }
  .hud-right { align-items: stretch; width: 100%; }
  .dock-txt { display: none; }
  .dock-btn { min-height: 44px; }
  .dock-ico { font-size: 19px; }
  /* the CSS-drawn crystal is not a font glyph, so it scales on its own axis */
  .dock-ico.skill-orb { width: 19px; height: 19px; }
  .hud-xp { display: none; }
  /* 40 slots at 8-across is unusable on a phone — 5 columns keeps them tappable
     and the grid simply grows to 8 rows instead. */
  .inv-grid { grid-template-columns: repeat(5, minmax(0, 1fr)); }
  .inv-stash { grid-template-columns: 1fr; }
  /* Smaller glyphs and tighter padding, but still `min-width: max-content` from
     the base rule — on a phone the five tabs wrap onto two rows rather than
     being ellipsised into unreadable stubs. */
  .inv-cat { font-size: 6px; padding: 6px 3px; }
}

/* ---------- 19. Reduced motion ---------- */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
  }
  .crt-flicker { display: none; }
  .embers { display: none; }
  .icon-btn:hover,
  .buy-btn:hover:not(:disabled),
  .skill-btn:hover,
  .shop-row:hover,
  .inv-btn:hover,
  .equip-slot:hover,
  .inv-slot:hover,
  .forge-slot:hover,
  .artifact-slot:hover,
  .equip-slot.is-selected,
  .inv-slot.is-selected,
  .town-node:hover,
  .dlg-row:hover,
  .party-slot:hover,
  .skill-btn:hover,
  .auto-switch:hover,
  .dock-btn:hover .dock-ico,
  .mult-btn:active {
    transform: none;
  }
  .town-node.is-ready .node-plate { animation: none; }
  .tab-btn.has-alert .tab-badge { animation: none; }
  .dock-btn[data-tab="heroes"] .tab-badge { animation: none; }
  .wave-track.is-boss .wave-skull { animation: none; }
  .skill-btn.active-skill { animation: none; }
  .wave-fill { transition: none; }
}

/* ---------- 20. First-run tutorial ----------
   A "spotlight" here is really a vignette: one dim rectangle covering the whole
   viewport, with a transparent HOLE over the element the current step explains.
   The hole is NOT a clip-path — translating a clip against a scrolled page is
   brittle. Instead the dim is four edge panes that surround the target rect,
   laid out by game.js. That reads identically and survives scrolls and resizes
   because every pane is computed from the same getBoundingClientRect. */
.tut-root { position: fixed; inset: 0; z-index: 9999; pointer-events: none; }
.tut-root.hidden { display: none; }
.tut-card { pointer-events: auto; }
.tut-dim {
  position: absolute;
  inset: 0;
  z-index: 0;
  background: rgba(10, 5, 14, .62);
  cursor: pointer;
  pointer-events: auto;
}
/* the hole panes: each is a rect filling one side of the spotlight. game.js
   sets left/top/width/height per pane each time a step is shown. They take
   clicks like the plain dim does, so clicking anywhere outside the highlighted
   element advances — but the element ITSELF stays clickable, which is what
   makes a step like "open the backpack" actually followable. */
.tut-spot { display: none; position: absolute; inset: 0; z-index: 1; }
.tut-spot .pane {
  position: absolute;
  background: rgba(10, 5, 14, .62);
  pointer-events: auto;
  cursor: pointer;
}
.tut-frame {
  position: absolute;
  z-index: 2;
  pointer-events: none;
  display: none;
  box-shadow:
    0 0 0 3px var(--trim-hi),
    0 0 0 7px rgba(0, 0, 0, .5),
    0 0 34px rgba(254, 231, 97, .4);
  animation: tut-pulse 1.8s ease-in-out infinite;
}
@keyframes tut-pulse {
  0%, 100% { box-shadow: 0 0 0 3px var(--trim-hi), 0 0 0 7px rgba(0,0,0,.5), 0 0 22px rgba(254,231,97,.3); }
  50%      { box-shadow: 0 0 0 3px var(--trim-hi), 0 0 0 7px rgba(0,0,0,.5), 0 0 44px rgba(254,231,97,.58); }
}
.tut-card {
  position: absolute;
  left: 50%;
  bottom: 26px;
  transform: translateX(-50%);
  z-index: 3;
  width: min(520px, calc(100vw - 32px));
  padding: 14px 16px;
  background: var(--panel-bg);
  box-shadow: 0 0 0 3px var(--line), 0 0 0 5px var(--e-slate), inset 0 3px 0 rgba(255, 255, 255, .08), 0 14px 0 rgba(0, 0, 0, .4);
}
.tut-card::before {
  content: '';
  position: absolute;
  left: 50%;
  bottom: 100%;
  transform: translateX(-50%);
  width: 0;
  height: 0;
  border: 0 solid transparent;
  border-bottom: 10px solid var(--e-slate);
  border-left: 9px solid transparent;
  border-right: 9px solid transparent;
}
.tut-text {
  font-family: var(--font-pixel);
  font-size: 8px;
  line-height: 1.9;
  letter-spacing: .6px;
  color: var(--e-cream);
  margin-bottom: 12px;
}
.tut-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
}
.tut-step {
  font-family: var(--font-pixel);
  font-size: 7px;
  color: var(--e-steel);
}
.tut-foot .inv-btn {
  flex: 0 0 auto;
  width: auto;
  padding: 8px 14px;
}

/* ---------- 20. Tooltip portal layer ----------
   WHY A PORTAL AND NOT A Z-INDEX.

   .tab-body is a scrollport (`overflow-y: auto` + `overflow-x: hidden`), so an
   absolutely positioned tooltip inside it is clipped on all four edges no matter
   how high its z-index is — z-index orders painting, it does not defeat a
   parent's clip. The nth-child flip rules further up only ever mitigated this:
   they hard-code an 8-column grid (the phone breakpoint makes .inv-grid five
   columns, so the arithmetic flips the wrong tips), and they cannot help at all
   once the panel is scrolled, because the anchor can then sit flush against the
   top edge of the scrollport with its tip opening upward into clipped space.

   Switching the tip to `position: fixed` does not escape either. The hovered
   slot carries BOTH `transform: translateY(-2px)` and `filter: brightness(1.16)`
   on :hover, and `.tab-panel:not(.hidden)` keeps `transform: translateY(0)` from
   panel-in's `both` fill mode. Any of those makes that ancestor the containing
   block for fixed-position descendants, which puts .tab-body straight back into
   the clip chain.

   So the tip is CLONED into this layer, which hangs off <body> where no ancestor
   is transformed or clipped, and is positioned from the anchor's
   getBoundingClientRect() by initTipPortal() in game.js. The original tip stays
   exactly where it is — render code calls node.querySelector('.item-tip')
   constantly — and is only muted while its clone is on screen. */
#tip-layer {
  position: fixed;
  inset: 0;
  z-index: 999999;
  pointer-events: none;
}
/* The clone owns its own position. `!important` here is not decoration: the
   base .item-tip rule and the nth-child flip rules further up both set
   left/right/top/bottom/transform, and the clone must ignore all of them. */
#tip-layer .item-tip {
  position: absolute !important;
  left: 0;
  top: 0;
  right: auto !important;
  bottom: auto !important;
  z-index: 999999 !important;
  margin: 0 !important;
  opacity: 1 !important;
  visibility: visible !important;
  transform: none !important;
  transition: none !important;
  pointer-events: none !important;
  max-width: min(320px, calc(100vw - 16px)) !important;
}
/* The in-place tip stops PAINTING while its clone is on screen, so a tooltip is
   never drawn twice — but it is muted with opacity alone, not `visibility`.
   It keeps `visibility: visible` from the :hover / :focus-visible rule and so
   stays in the accessibility tree exactly as it was before the portal existed;
   #tip-layer is aria-hidden precisely so the clone does not duplicate it. */
.has-tip-portal > .item-tip {
  opacity: 0 !important;
}

/* ==========================================================================
   24. THE OVERHAUL — BLACKSMITH PICKER, ANTI-BOSS STONES, SELL PROTECTION
       AND THE GUILD

   Four features share this block because they share a visual language: a dense
   grid of .inv-slot cells with a state tag laid over the ones that cannot be
   used. The blacksmith and the marketplace picker are the same widget with
   different rules, so the cell styling is written once and specialised twice.
   ========================================================================== */

/* ---------- 24a. "ENVANTERDEN SEÇ" — the blacksmith's backpack picker ----------
   Six columns rather than the backpack's eight: this grid sits in a narrower
   column and the cells carry a + level and a socket strip that need the room. */
.fg-picker {
  display: grid;
  grid-template-columns: repeat(6, minmax(0, 1fr));
  gap: 5px;
  max-height: 240px;
  overflow-y: auto;
  overflow-x: hidden;
  padding: 6px;
  margin-bottom: 12px;
  background: rgba(0, 0, 0, .26);
  box-shadow: 0 0 0 2px var(--line), inset 2px 2px 0 rgba(255, 255, 255, .05);
  scrollbar-width: thin;
}
.fg-picker:empty { display: none; }
.fg-picker > .sock-none { grid-column: 1 / -1; padding: 10px 4px; text-align: center; }

.fg-cell { position: relative; cursor: pointer; }
/* the selected piece is the one on the anvil — gold trim, matching every other
   "this is the active thing" affordance in the shell */
.fg-cell.is-selected {
  box-shadow: inset 0 0 0 3px var(--trim), 0 0 12px rgba(254, 174, 52, .45);
  transform: translateY(-2px);
}
.fg-cell-plus {
  position: absolute;
  top: 1px;
  left: 2px;
  font-family: var(--font-pixel);
  font-size: 6px;
  line-height: 1;
  color: var(--e-amber);
  text-shadow: 1px 1px 0 var(--e-black);
  pointer-events: none;
  z-index: 3;
}
/* one dot per cut socket, along the bottom edge of the cell */
.fg-cell-socks {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 2px;
  display: flex;
  justify-content: center;
  gap: 2px;
  pointer-events: none;
  z-index: 3;
}
.fg-sock {
  width: 4px;
  height: 4px;
  background: transparent;
  box-shadow: inset 0 0 0 1px var(--e-steel);
}
.fg-sock.is-set { box-shadow: none; }

/* ---------- 24b. The worn-gear strip / the unequip gate ----------
   Shown only while something is equipped. Its whole job is to answer "why is my
   sword not in the picker", so the KULLANILIYOR tag and the UNEQUIP button sit
   on the same row as the item that is blocked. */
.fg-worn-head .inv-title { color: var(--e-rose); }
.fg-worn {
  display: flex;
  flex-direction: column;
  gap: 5px;
  margin-bottom: 12px;
}
.fg-worn.hidden, .fg-worn-head.hidden { display: none; }
.fg-worn-row {
  display: grid;
  grid-template-columns: auto 1fr auto auto;
  align-items: center;
  gap: 8px;
  padding: 6px 8px;
  background: rgba(0, 0, 0, .3);
  box-shadow: 0 0 0 2px var(--line), inset 2px 2px 0 rgba(255, 255, 255, .04);
}
/* the rarity classes already exist for slots; reuse their colour as a left edge */
.fg-worn-row.r-common   { border-left: 3px solid var(--e-silver); }
.fg-worn-row.r-uncommon { border-left: 3px solid var(--e-green); }
.fg-worn-row.r-rare     { border-left: 3px solid var(--e-sky); }
.fg-worn-row.r-epic     { border-left: 3px solid var(--e-mauve); }
.fg-worn-row.r-legendary { border-left: 3px solid var(--e-amber); }
.fg-worn-row.r-mythic   { border-left: 3px solid var(--e-hot); }
.fg-worn-ico {
  display: grid;
  place-items: center;
  width: 30px;
  height: 30px;
  background: var(--e-black);
  box-shadow: inset 0 0 0 2px var(--e-slate);
}
.fg-worn-ico canvas { display: block; image-rendering: pixelated; }
.fg-worn-body { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.fg-worn-name {
  font-family: var(--font-pixel);
  font-size: 6.5px;
  font-weight: 400;
  letter-spacing: .4px;
  color: var(--e-cream);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.fg-worn-sub { font-size: 9px; color: var(--txt-dim); }
/* the same red "in use" tag the sell picker uses, so one visual means one thing */
.fg-worn-tag,
.mkt-eq-tag {
  font-family: var(--font-pixel);
  font-size: 5.5px;
  letter-spacing: .5px;
  line-height: 1;
  padding: 4px 5px;
  color: var(--e-white);
  background: var(--e-crimson);
  box-shadow: inset 0 0 0 1px var(--e-red), 0 0 8px rgba(228, 59, 68, .5);
  white-space: nowrap;
}
.fg-worn-btn { flex: 0 0 auto; }

/* ---------- 24c. Anti-boss stones ----------
   The stone tray is the gem tray with a warmer frame and a chiselled pip, so the
   two families read as siblings rather than as one mechanic in two places. */
.stone-row { margin-top: 2px; }
.gem-chip.is-stone {
  box-shadow: 0 0 0 2px var(--gem-col, var(--e-amber)),
              inset 2px 2px 0 rgba(255, 255, 255, .06),
              0 0 10px rgba(254, 174, 52, .18);
}
.gem-chip.is-stone .gem-dot {
  /* a stone is cut, not faceted — a clipped hexagon rather than a square */
  clip-path: polygon(50% 0, 100% 28%, 100% 72%, 50% 100%, 0 72%, 0 28%);
  width: 13px;
  height: 13px;
}
.gem-chip.is-stone .gem-lab { color: var(--e-amber); }
/* a socketed stone gets the same hexagon in the socket row */
.sock-pip.is-stone {
  clip-path: polygon(50% 0, 100% 28%, 100% 72%, 50% 100%, 0 72%, 0 28%);
}

/* ---------- 24d. The marketplace sell picker ----------
   Lists the backpack AND the four worn pieces. A worn cell is dimmed, wears the
   red KULLANILIYOR tag and is a disabled button, so the protection is visible
   before it is enforced. */
.mkt-pick-head {
  font-family: var(--font-pixel);
  font-size: 6px;
  letter-spacing: .5px;
  color: var(--e-cream);
  margin: 2px 0 6px;
}
.mkt-pick {
  display: grid;
  grid-template-columns: repeat(6, minmax(0, 1fr));
  gap: 5px;
  max-height: 210px;
  overflow-y: auto;
  overflow-x: hidden;
  padding: 6px;
  margin-bottom: 8px;
  background: rgba(0, 0, 0, .26);
  box-shadow: 0 0 0 2px var(--line), inset 2px 2px 0 rgba(255, 255, 255, .05);
  scrollbar-width: thin;
}
.mkt-pick > .sock-none { grid-column: 1 / -1; padding: 10px 4px; text-align: center; }
.mkt-pick-cell { position: relative; cursor: pointer; }
.mkt-pick-cell.is-selected {
  box-shadow: inset 0 0 0 3px var(--trim), 0 0 12px rgba(254, 174, 52, .45);
  transform: translateY(-2px);
}
/* the locked (equipped) state: desaturated, red-framed, and not hoverable */
.mkt-pick-cell.is-locked {
  cursor: not-allowed;
  filter: saturate(.35) brightness(.72);
  box-shadow: inset 0 0 0 2px var(--e-crimson);
}
.mkt-pick-cell.is-locked:hover { transform: none; }
.mkt-eq-tag {
  position: absolute;
  left: 50%;
  bottom: 2px;
  transform: translateX(-50%) rotate(-4deg);
  pointer-events: none;
  z-index: 4;
  /* the cells are small, so the tag has to shrink to fit inside one */
  font-size: 4.5px;
  padding: 3px 3px;
  max-width: calc(100% - 4px);
  overflow: hidden;
  text-overflow: clip;
}
/* a locked cell must not out-shout the grid: kill its hover card's compare block */
.mkt-pick-cell.is-locked .item-tip { filter: none; }

/* ---------- 24e. The guild (Lonca) ----------
   Two dialogue-row variants: the founding form, which carries the only text
   input in the town UI, and the banner that replaces it once founded. */
.dlg-row.is-guild-found,
.dlg-row.is-guild-banner {
  box-shadow: 0 0 0 2px var(--trim), inset 2px 2px 0 rgba(255, 255, 255, .05);
}
.dlg-row.is-guild-banner .dlg-row-name { color: var(--e-amber); }
.gd-name {
  width: 100%;
  margin-top: 6px;
  padding: 7px 8px;
  font-family: var(--font-pixel);
  font-size: 7px;
  letter-spacing: .4px;
  color: var(--e-cream);
  background: var(--e-black);
  border: 0;
  box-shadow: inset 0 0 0 2px var(--e-slate), inset 0 3px 0 rgba(0, 0, 0, .45);
}
.gd-name::placeholder { color: var(--txt-dim); }
.gd-name:focus {
  outline: none;
  box-shadow: inset 0 0 0 2px var(--trim), inset 0 3px 0 rgba(0, 0, 0, .45);
}

/* ---------- 24f. Narrow screens ----------
   Both pickers drop to four columns before their cells go below the ~28px the
   icon needs, and the worn strip lets its button wrap under the name. */
@media (max-width: 720px) {
  .fg-picker, .mkt-pick { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}
@media (max-width: 480px) {
  .fg-picker, .mkt-pick { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .fg-worn-row { grid-template-columns: auto 1fr; row-gap: 6px; }
  .fg-worn-tag, .fg-worn-btn { grid-column: 2; justify-self: start; }
}

/* ---------- 25. Account: login / register / forgot password ----------
   The auth dialog reuses .modal-back, .panel.modal and .inv-btn wholesale, so
   only the form internals are defined here. Everything is built from the same
   tokens as the rest of the game: --font-pixel labels, the 2px --jrpg-edge
   frame, an inset well for every field.

   One deliberate deviation from the pixel-font house style: the INPUTS use
   --font-body, not --font-pixel. 'Press Start 2P' has no lowercase worth the
   name at 10px and no dotted i, which makes a typed password or a Turkish email
   address genuinely unreadable as you type it. Labels stay pixel; values stay
   legible. */
.modal.is-auth { max-width: 380px; }

/* The head's own flex is `space-between`, so the toggle and the × have to travel
   as ONE child or the toggle would be pushed into the middle of the row. */
.auth-head-tools { display: flex; align-items: center; gap: 8px; flex: 0 0 auto; }

/* Tighter than the settings toggle: it shares a 380px-wide head with a pixel-font
   title, and at the full 8px/7px padding the two would collide at 360px. */
.lang-toggle.is-compact .lang-btn {
  font-size: 7px;
  padding: 5px 6px;
  min-width: 24px;
}

/* While the hard gate is up the × is `disabled` and reads as dead weight beside a
   live control, so it is taken out of the paint entirely. Not `display:none` on
   the button itself — the gate toggles `disabled`, and keying the rule off that
   keeps the two in sync with no extra class to forget. */
.modal-back.is-hardgate .auth-head-tools .is-close { display: none; }

.auth-pane { display: grid; gap: 12px; }

.auth-field { display: grid; gap: 5px; }

.auth-label {
  font-family: var(--font-pixel);
  font-size: 7px;
  line-height: 1.7;
  letter-spacing: 1.3px;
  color: var(--txt-dim);
}

.auth-input {
  width: 100%;
  padding: 9px 10px;
  font-family: var(--font-body);
  font-size: 13px;
  line-height: 1.4;
  color: var(--txt-strong);
  background: var(--jrpg-bg2);
  border: 0;
  box-shadow: inset 0 0 0 2px var(--jrpg-edge), inset 0 3px 0 rgba(0, 0, 0, .38);
}
.auth-input::placeholder { color: var(--txt-dim); }
.auth-input:focus,
.auth-input:focus-visible {
  outline: none;
  box-shadow: inset 0 0 0 2px var(--trim), inset 0 3px 0 rgba(0, 0, 0, .38);
}
/* A field the server rejected. game.js adds this and clears it on the next
   keystroke, so it marks "this is what failed", not "this is invalid now". */
.auth-input.is-bad {
  box-shadow: inset 0 0 0 2px var(--e-red), inset 0 3px 0 rgba(0, 0, 0, .38);
}

.auth-hint,
.auth-blurb {
  font-family: var(--font-body);
  font-size: 11px;
  line-height: 1.55;
  color: var(--txt-dim);
}
.auth-blurb { margin: 0 0 2px; }

/* Reserves its line height while empty so the form does not jump the first time
   a message appears — the submit button would otherwise slide out from under a
   finger mid-tap. */
.auth-msg {
  min-height: 2.6em;
  margin: 0 0 4px;
  font-family: var(--font-body);
  font-size: 11px;
  line-height: 1.55;
  color: var(--txt-dim);
}
.auth-msg.is-bad  { color: var(--e-rose); }
.auth-msg.is-good { color: var(--e-green); }
.auth-msg.is-busy { color: var(--e-amber); }

.auth-actions { margin-top: 2px; }
.auth-actions .inv-btn { width: 100%; }

.auth-links {
  display: flex;
  flex-wrap: wrap;
  gap: 4px 14px;
  justify-content: space-between;
}

/* Text buttons, not links: they switch panes rather than navigate, and a real
   <a href> here would lie to anyone using a screen reader. */
.auth-link {
  padding: 4px 0;
  font-family: var(--font-body);
  font-size: 11px;
  color: var(--e-sky);
  background: none;
  border: 0;
  text-decoration: underline;
  text-underline-offset: 2px;
  cursor: pointer;
}
.auth-link:hover { color: var(--e-cyan); }
.auth-link:focus-visible { outline: 2px solid var(--trim-hi); outline-offset: 2px; }
.auth-link.is-quiet { color: var(--txt-dim); }
.auth-link.is-quiet:hover { color: var(--txt); }

/* ---------- THE HARD GATE ----------
   Sign-in is mandatory, so while `is-authgate` is on <body> nothing behind the
   dialog may be seen, heard or reached.

   visibility, NOT display: `applyRes()` in `auto` mode measures the canvas's own
   box on every resize, and a display:none subtree measures zero — which would
   leave the stage sized 0x0 the moment the gate came down. visibility:hidden
   keeps the layout box intact while still removing the subtree from the paint
   and from the tab order. */
body.is-authgate .app,
body.is-authgate .menu {
  visibility: hidden !important;
  pointer-events: none !important;
}

/* Opaque, not the usual 78% wash — there is nothing behind this one the player
   is meant to see, and `default` says the scrim is not a way out. */
.modal-back.is-hardgate .modal-scrim {
  background: var(--jrpg-bg);
  cursor: default;
}

/* The × belongs to the signed-in account view of this same dialog. It is also
   `disabled` in JS, which is what actually keeps it out of the focus trap; this
   only stops it from being seen. */
.modal-back.is-hardgate .icon-btn.is-close { display: none; }

/* Auth.ungate() fades the wall out rather than snapping it away. */
.modal-back.is-fading {
  opacity: 0;
  pointer-events: none;
  transition: opacity .32s ease;
}

.auth-gate-foot {
  margin-top: 10px;
  padding-top: 10px;
  border-top: 2px solid var(--jrpg-edge);
  display: grid;
  justify-items: center;
}

/* Dev-only reset link (AUTH_DEV_LINKS=1). Loud amber precisely so it can never
   be mistaken for production chrome. */
.auth-dev {
  margin: 10px 0 0;
  padding: 8px 9px;
  background: rgba(254, 174, 52, .1);
  box-shadow: inset 0 0 0 2px var(--e-amber);
  font-family: var(--font-body);
  font-size: 10px;
  line-height: 1.6;
  word-break: break-all;
}
.auth-dev .t {
  display: block;
  font-family: var(--font-pixel);
  font-size: 7px;
  letter-spacing: 1.2px;
  color: var(--e-amber);
}
.auth-dev a { color: var(--e-cyan); }

/* ---------- 25b. Account chrome outside the dialog ---------- */

/* Title-screen SIGN IN button. Teal so it reads as a service action rather than
   a third way into the game. */
.menu-btn.is-account { background: linear-gradient(180deg, #2aa9c4, #12718c 60%, #0c4d61); }

.menu-acct {
  min-height: 1px;
  font-family: var(--font-pixel);
  font-size: 7px;
  line-height: 1.9;
  letter-spacing: 1px;
  color: var(--e-cyan);
  max-width: 320px;
}

/* Settings-panel account block */
.acct-block { display: grid; gap: 8px; }
#acct-state { color: var(--e-cyan); }

/* ---------- 25c. reset.html ----------
   The standalone recovery page loads this same stylesheet but has no .app
   shell, so it needs its own centring — three rules, rather than a second
   stylesheet that would drift out of sync. */
.reset-page {
  min-height: 100vh;
  display: grid;
  place-items: center;
  padding: 20px;
  background: var(--page);
}
.reset-wrap { width: 100%; max-width: 380px; }
.reset-page .modal { animation: none; }   /* nothing opened it, so nothing slides */

@media (max-width: 420px) {
  .auth-links { flex-direction: column; gap: 2px; }
}

/* ============================================================
   26. THE METIN2-FLAVOURED EXPANSION PACK

   Six screens, one stylesheet section, in the order the player meets them:
   the constellation window, efsun and the enhancement aura, the companion pet,
   the raid board, the daily bounties, and the AFK chest.

   Two rules held throughout, both inherited from the rest of this file:

     1. Colour comes from the markup, not from a selector per variant. Every
        list game.js builds sets a custom property on the row (--pc for a pet,
        --dc for a dungeon, --tt for a tree tooltip) out of the game's own
        palette, and the rules below read that property. Adding a fifth pet is
        then a data change, not a stylesheet change.
     2. Nothing here has a fixed height. Turkish runs longer than English in
        almost every one of these strings, so every box grows instead.
   ============================================================ */

/* ---------- 26a. The constellation WINDOW ----------
   The board left the sidebar because breathable constellations do not fit a
   320px column at any zoom — and once it was a window it needed the two things
   a sidebar cannot offer: a size the player picks and a position they pick.

   `.modal.is-tree` therefore overrides the shared `.modal` 420px cap outright.
   It is a FLEX COLUMN with the stage as the only growing child, which is what
   lets a drag on the grip make the canvas bigger rather than making the toolbar
   taller. width/height are inline-styled by treeWinApply(), so everything here
   is a default the player is free to leave behind. */
.modal.is-tree {
  display: flex;
  flex-direction: column;
  gap: 10px;
  width: min(1080px, calc(100vw - 32px));
  max-width: none;
  height: min(760px, calc(100vh - 32px));
  /* the grip is positioned against this box */
  padding-bottom: 14px;
}

/* The drag bar. `cursor: move` is the affordance; `touch-action: none` is the
   requirement — without it a touch drag scrolls the page out from under the
   pointer capture. user-select stops a fast drag from selecting the heading. */
.modal-head.is-grab {
  cursor: move;
  margin: -2px -2px 0;
  padding: 6px 8px;
  background: rgba(0, 0, 0, .26);
  box-shadow: inset 0 0 0 2px var(--line);
  touch-action: none;
  user-select: none;
  -webkit-user-select: none;
}
.modal-head.is-grab.is-dragging { background: rgba(254, 174, 52, .12); }
.modal-head.is-grab h2 { margin-right: auto; }
/* the close button is a button again inside a bar that eats pointerdown */
.modal-head.is-grab button { cursor: pointer; }

.tree-points-wrap {
  font-family: var(--font-pixel);
  font-size: 8px;
  letter-spacing: 1px;
  color: var(--txt-dim);
}
.tree-points-wrap b { font-weight: 400; font-size: 11px; color: var(--e-cyan); }

/* The resize grip: three stepped pixel rules in the corner, which is the
   universal "drag me" mark and costs no image. */
.modal-grip {
  position: absolute;
  right: 0;
  bottom: 0;
  width: 18px;
  height: 18px;
  cursor: nwse-resize;
  touch-action: none;
  background:
    linear-gradient(135deg, transparent 46%, var(--e-steel) 46%, var(--e-steel) 58%, transparent 58%),
    linear-gradient(135deg, transparent 66%, var(--e-steel) 66%, var(--e-steel) 78%, transparent 78%),
    linear-gradient(135deg, transparent 86%, var(--e-steel) 86%, var(--e-steel) 98%, transparent 98%);
}
.modal-grip:hover, .modal-grip.is-dragging { filter: brightness(1.6); }

/* The stage is the ONLY growing child, so the window's height lands on the
   canvas. min-height keeps it from collapsing to nothing when the toolbar wraps
   to three rows on a narrow window. */
.modal.is-tree .tree-stage { flex: 1 1 auto; min-height: 220px; height: auto; }
.modal.is-tree .tree-bar { row-gap: 6px; }
.modal.is-tree .tree-info { flex: 0 0 auto; }

/* the +/- pair reads as one control with the percentage between them */
.tree-zoomers { display: inline-flex; align-items: center; gap: 4px; margin-left: auto; }
.tree-zoomers .tree-zoom { margin-left: 0; min-width: 40px; text-align: center; }
.icon-btn.is-zoom {
  min-width: 26px;
  min-height: 26px;
  padding: 2px 7px;
  font-family: var(--font-pixel);
  font-size: 11px;
  background: var(--e-slate);
  color: var(--trim-hi);
}

/* --- the SKILLS tab launcher --- */
.inv-btn.is-tree-open {
  width: 100%;
  padding: 14px 10px;
  background: linear-gradient(180deg, #6b4a9c, #4a2f74 60%, #2f1d4d);
  color: var(--trim-hi);
  font-size: 8px;
  letter-spacing: 1.5px;
}
.inv-btn.is-tree-open:hover:not(:disabled) { filter: brightness(1.2) saturate(1.15); }

/* --- the hover card ---
   Positioned by treeTipShow() in stage coordinates, so left/top are inline and
   this rule only paints. --tt is the hovered node's own colour: one property,
   set from the palette, drives the frame, the name and the branch line. */
.tree-tip {
  position: absolute;
  z-index: 40;
  display: flex;
  flex-direction: column;
  gap: 3px;
  min-width: 210px;
  max-width: min(320px, calc(100vw - 28px));
  padding: 9px 10px;
  background: rgba(6, 5, 14, .97);
  box-shadow: 0 0 0 3px var(--tt, var(--e-steel)), 0 6px 0 rgba(0, 0, 0, .5);
  pointer-events: none;   /* it sits on a draggable surface; never eat a pan */
}
.tree-tip-branch {
  font-family: var(--font-pixel);
  font-size: 6.5px;
  letter-spacing: 1.5px;
  color: var(--tt, var(--e-steel));
  opacity: .85;
}
.tree-tip-name {
  font-family: var(--font-pixel);
  font-size: 8.5px;
  line-height: 1.6;
  color: var(--e-white);
  overflow-wrap: break-word;
}
.tree-tip-meta { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; margin: 2px 0 3px; }
.tree-tip-tier {
  padding: 2px 5px;
  font-family: var(--font-pixel);
  font-size: 6px;
  letter-spacing: 1px;
  color: var(--e-black);
  background: var(--e-steel);
}
.tree-tip-tier.is-notable { background: var(--e-mauve); }
.tree-tip-tier.is-keystone {
  background: var(--e-yellow);
  box-shadow: 0 0 9px rgba(254, 231, 97, .55);
}
.tree-tip-rank { font-family: var(--font-pixel); font-size: 6.5px; letter-spacing: 1px; color: var(--txt-dim); }
.tree-tip-line { font-size: 10px; line-height: 1.65; color: var(--e-silver); }
.tree-tip-cost {
  margin-top: 3px;
  padding-top: 5px;
  border-top: 2px solid var(--e-slate);
  font-size: 9.5px;
  color: var(--trim);
}
/* the verdict: what a click would do RIGHT NOW, coloured by whether it can */
.tree-tip-state { font-family: var(--font-pixel); font-size: 6.5px; letter-spacing: 1px; line-height: 1.8; }
.tree-tip-state.is-ready { color: var(--e-green); text-shadow: 0 0 7px rgba(99, 199, 77, .5); }
.tree-tip-state.is-max { color: var(--trim-hi); }
.tree-tip-state.is-locked { color: var(--e-steel); }
.tree-tip-state.is-poor { color: var(--e-rose); }

/* ---------- 26b. Efsun + the +7 enhancement aura ---------- */

/* --- the blacksmith's efsun rows --- */
.efsun-box {
  display: flex;
  flex-direction: column;
  gap: 3px;
  padding: 8px 9px;
  background: rgba(104, 56, 108, .16);
  box-shadow: inset 0 0 0 2px var(--e-purple);
}
.efsun-line {
  font-size: 10px;
  line-height: 1.65;
  color: var(--e-mauve);
  overflow-wrap: break-word;
}
.efsun-line::before { content: '◈ '; color: var(--e-rose); }
/* the empty state doubles as the explanation, so it wraps like body copy */
.efsun-none { font-size: 9.5px; line-height: 1.7; color: var(--txt-dim); overflow-wrap: break-word; }

.cost-chip.is-dust b { color: var(--e-rose); }
.inv-btn.is-efsun {
  width: 100%;
  background: linear-gradient(180deg, #8d4f9e, #68386c 60%, #46224a);
  color: var(--e-cream);
}
.inv-btn.is-efsun .lbl { font-weight: 400; }
/* a toggled-on action (the active pet, mainly) reads as pressed, not as new */
.inv-btn.is-on { background: var(--e-forest); box-shadow: inset 0 0 0 2px var(--trim-hi); }

/* --- efsun and enhancement inside an item tooltip --- */
.tip-efsun-name {
  display: block;
  margin: 7px 0 3px;
  padding-top: 6px;
  border-top: 2px solid var(--e-purple);
  font-family: var(--font-pixel);
  font-size: 7px;
  letter-spacing: 1px;
  color: var(--e-mauve);
}
.tip-line.tip-efsun { color: var(--e-rose); }
.tip-line.tip-enh { color: var(--trim); }
.tip-line.tip-enh.is-aura { color: var(--trim-hi); text-shadow: 0 0 8px rgba(254, 231, 97, .55); }
.tip-enh-aura {
  font-family: var(--font-pixel);
  font-size: 6.5px;
  letter-spacing: 1px;
  line-height: 1.9;
  color: var(--e-orange);
}

/* --- THE AURA ON THE SLOT ITSELF ---
   `data-plus` is written for every upgraded piece, `has-enh` only from +7 up.
   That split is the whole design: a +4 gets its number in the corner and
   nothing else, a +7 gets the number AND a burning ring.

   The ring is layered ON TOP of the rarity shadow rather than replacing it, so
   a mythic +9 is still mythic-coloured — the aura says "upgraded", the ring
   says "rare", and the two never fight over the same pixel. */
.equip-slot[data-plus]::before,
.inv-slot[data-plus]::before,
.forge-slot[data-plus]::before {
  content: attr(data-plus);
  position: absolute;
  bottom: -3px;
  left: -3px;
  z-index: 2;
  padding: 1px 3px;
  font-family: var(--font-pixel);
  font-size: 6px;
  line-height: 1.7;
  color: var(--e-black);
  background: var(--trim);
  box-shadow: 0 0 0 2px var(--e-black);
  pointer-events: none;
}
.equip-slot.has-enh[data-plus]::before,
.inv-slot.has-enh[data-plus]::before,
.forge-slot.has-enh[data-plus]::before {
  color: var(--e-black);
  background: var(--trim-hi);
  box-shadow: 0 0 0 2px var(--e-black), 0 0 10px rgba(254, 231, 97, .85);
}
.equip-slot.has-enh.has-item,
.inv-slot.has-enh.has-item,
.forge-slot.has-enh.has-item {
  animation: enh-aura 1.7s ease-in-out infinite;
}
@keyframes enh-aura {
  0%, 100% {
    box-shadow:
      0 0 0 3px var(--r, var(--e-steel)),
      0 0 10px var(--r-glow, transparent),
      0 0 14px rgba(254, 174, 52, .40),
      inset 2px 2px 0 rgba(255, 255, 255, .12),
      inset -2px -2px 0 rgba(0, 0, 0, .40);
  }
  50% {
    box-shadow:
      0 0 0 3px var(--r, var(--e-steel)),
      0 0 10px var(--r-glow, transparent),
      0 0 26px rgba(254, 231, 97, .92),
      inset 2px 2px 0 rgba(255, 255, 255, .22),
      inset -2px -2px 0 rgba(0, 0, 0, .40);
  }
}

/* ---------- 26c. Evcil hayvan — the companion pet ----------
   Rows are icon · body · actions, and the body is the only flexible track so a
   long Turkish description wraps instead of squeezing the buttons. --pc is the
   pet's palette colour, set on the row by renderPets(). */
.pet-current {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px;
  margin-bottom: 9px;
  padding: 7px 9px;
  background: rgba(0, 0, 0, .26);
  box-shadow: inset 0 0 0 2px var(--line);
}
.pet-current-label { font-family: var(--font-pixel); font-size: 6.5px; letter-spacing: 1px; color: var(--txt-dim); }
.pet-current b { font-family: var(--font-pixel); font-size: 8px; font-weight: 400; color: var(--trim-hi); }

.pet-list { display: grid; gap: 8px; }
.pet-row {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
  gap: 9px;
  padding: 9px;
  background: var(--panel-bg2);
  box-shadow: var(--pxframe), inset 2px 2px 0 rgba(255, 255, 255, .05);
}
/* a locked pet is legible but plainly not yours yet */
.pet-row.is-locked { filter: grayscale(.55) brightness(.86); }
.pet-row.is-active { box-shadow: 0 0 0 3px var(--pc, var(--trim-hi)), inset 2px 2px 0 rgba(255, 255, 255, .08); }
.pet-ico { font-size: 22px; line-height: 1; filter: drop-shadow(0 0 7px var(--pc, transparent)); }
.pet-body { min-width: 0; display: grid; gap: 3px; }
.pet-name {
  font-family: var(--font-pixel);
  font-size: 8px;
  line-height: 1.6;
  color: var(--pc, var(--e-white));
  overflow-wrap: break-word;
}
.pet-lv {
  margin-left: 6px;
  padding: 1px 4px;
  font-size: 6px;
  letter-spacing: 1px;
  color: var(--e-black);
  background: var(--pc, var(--e-steel));
}
.pet-desc { font-size: 9.5px; line-height: 1.6; color: var(--txt-dim); overflow-wrap: break-word; }
.pet-bonus { font-size: 9.5px; line-height: 1.6; color: var(--e-green); overflow-wrap: break-word; }
.pet-need { font-size: 9px; color: var(--e-steel); }
.pet-acts { display: grid; gap: 5px; align-content: center; }
.pet-acts .inv-btn { min-height: 28px; padding: 6px 9px; font-size: 6.5px; }
.bar-pet { height: 8px; --seg: 10; }
.fill-pet { background: var(--pc, var(--e-green)); }

/* ---------- 26d. Zindanlar — the raid board ----------
   Shaped like the tower's panel on purpose: a raid IS a timed boss floor with a
   different door, and a player who has used one should recognise the other. */
.dun-head { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; margin-bottom: 9px; }
.dun-stat {
  padding: 9px 5px;
  text-align: center;
  background: rgba(0, 0, 0, .3);
  box-shadow: 0 0 0 2px var(--line), inset 2px 2px 0 rgba(255, 255, 255, .05);
}
.dun-label { display: block; margin-bottom: 6px; font-family: var(--font-pixel); font-size: 6.5px; letter-spacing: 1px; color: var(--txt-dim); }
.dun-stat b { font-family: var(--font-pixel); font-size: 13px; font-weight: 400; color: var(--e-cyan); }
.dun-stat.is-timer b { color: var(--trim-hi); }
/* the last fifteen seconds of a raid, pulsed — same tell as the tower's clock */
.dun-stat.is-timer b.is-hot { color: var(--e-red); animation: tower-tick .8s steps(2) infinite; }

.dun-state {
  margin-bottom: 9px;
  font-family: var(--font-pixel);
  font-size: 7px;
  letter-spacing: 1px;
  line-height: 1.9;
  text-align: center;
  color: var(--e-steel);
}

.dun-list { display: grid; gap: 8px; margin-bottom: 11px; }
.dun-row {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  align-items: start;
  gap: 9px;
  padding: 9px;
  background: var(--panel-bg2);
  box-shadow: var(--pxframe), inset 2px 2px 0 rgba(255, 255, 255, .05);
  border-left: 4px solid var(--dc, var(--e-steel));
}
.dun-row.is-locked { filter: grayscale(.6) brightness(.82); }
.dun-ico { font-size: 22px; line-height: 1.2; filter: drop-shadow(0 0 8px var(--dc, transparent)); }
.dun-body { min-width: 0; display: grid; gap: 3px; }
.dun-name {
  font-family: var(--font-pixel);
  font-size: 8px;
  line-height: 1.6;
  color: var(--dc, var(--e-white));
  overflow-wrap: break-word;
}
.dun-clears { margin-left: 6px; font-size: 6px; letter-spacing: 1px; color: var(--txt-dim); }
.dun-desc { font-size: 9.5px; line-height: 1.6; color: var(--txt-dim); overflow-wrap: break-word; }
.dun-reward { font-size: 9.5px; line-height: 1.6; color: var(--trim); overflow-wrap: break-word; }
/* the button spans both columns so a long "ENTER · 3 Keys" never squeezes the
   description into a two-character-wide ribbon */
.dun-acts { grid-column: 1 / -1; }
.dun-acts .inv-btn { width: 100%; }

/* The raid panel closes on a footnote rather than opening on one, so the
   margin has to flip: a plain .panel-note pushes space DOWN, which at the end
   of a panel is a gap against nothing. This pushes up instead and draws the
   same hairline the section rules use, so the hint reads as an aside to the
   list above it rather than as a heading for empty space below. */
.panel-note.is-hint {
  margin: 12px 0 0;
  padding-top: 10px;
  border-top: 2px solid var(--line);
  font-size: 9px;
}

/* ---------- 26e. Günlük görevler — the daily bounty board ---------- */
.daily-list { display: grid; gap: 7px; max-height: 52vh; overflow-y: auto; padding-right: 3px; }
.daily-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: 9px;
  padding: 9px;
  background: var(--panel-bg2);
  box-shadow: var(--pxframe), inset 2px 2px 0 rgba(255, 255, 255, .05);
}
/* three states, three weights: open is plain, finished glows, claimed recedes */
.daily-row.is-done { box-shadow: 0 0 0 3px var(--e-green), inset 2px 2px 0 rgba(255, 255, 255, .08); }
.daily-row.is-taken { filter: grayscale(.65) brightness(.8); }
.daily-body { min-width: 0; display: grid; gap: 4px; }
.daily-name { font-size: 10px; line-height: 1.6; color: var(--txt-strong); overflow-wrap: break-word; }
.daily-prog { font-family: var(--font-pixel); font-size: 6.5px; letter-spacing: 1px; color: var(--txt-dim); }
.daily-reward { font-size: 9.5px; line-height: 1.6; color: var(--trim); overflow-wrap: break-word; }
.daily-row .inv-btn { min-height: 30px; padding: 7px 10px; font-size: 6.5px; }
.bar-daily { height: 8px; --seg: 10; }
.fill-daily { background: var(--e-green); }
.daily-row.is-taken .fill-daily { background: var(--e-steel); }
.daily-note { margin-top: 10px; font-size: 9.5px; line-height: 1.6; text-align: center; color: var(--e-steel); }

/* The header opener carries its ready-count as data-count, written by the HUD
   tick. Empty string means no pip — `:not([data-count=""])` is what keeps the
   corner clean when there is nothing to claim. */
.icon-btn.is-daily { position: relative; font-size: 14px; background: var(--e-pine); }
.icon-btn.is-daily:not([data-count=""])::after {
  content: attr(data-count);
  position: absolute;
  top: -5px;
  right: -5px;
  min-width: 14px;
  padding: 1px 3px;
  font-family: var(--font-pixel);
  font-size: 6px;
  line-height: 1.8;
  text-align: center;
  color: var(--e-black);
  background: var(--alert);
  box-shadow: 0 0 0 2px var(--e-black);
  animation: badge-pip 1.5s ease-in-out infinite;
}

/* ---------- 26f. The AFK chest and the milestone bonus ---------- */
.wb-chest {
  margin-top: 12px;
  padding: 10px 9px 11px;
  background: rgba(254, 174, 52, .08);
  box-shadow: inset 0 0 0 2px var(--trim);
}
.wb-chest-head {
  display: block;
  margin-bottom: 9px;
  font-family: var(--font-pixel);
  font-size: 7px;
  letter-spacing: 1.5px;
  text-align: center;
  color: var(--trim-hi);
}
.wb-chest-ico { font-size: 13px; margin-right: 5px; vertical-align: -1px; }
/* four cells, not the welcome grid's three */
.wb-grid.is-chest { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.wb-grid.is-chest .wb-value { font-size: 11px; }
.wb-value.is-dust { color: var(--e-rose); }
.wb-value.is-key { color: var(--e-cyan); }
.wb-value.is-food { color: var(--e-green); }
.wb-value.is-mythic { color: var(--e-mauve); }
.wb-note.is-chest { margin-top: 9px; font-size: 9px; }

/* Account-wide milestone bonuses, under the achievement count. Written by
   renderAchBonus() as one line; it says "permanent" by being gold. */
.ach-bonus {
  margin-bottom: 11px;
  padding: 8px 9px;
  font-size: 9.5px;
  line-height: 1.7;
  text-align: center;
  color: var(--trim);
  background: rgba(254, 174, 52, .08);
  box-shadow: inset 0 0 0 2px var(--trim);
  overflow-wrap: break-word;
}

/* ---------- 26g. Narrow screens ----------
   The window has no room to be a window on a phone: it fills the viewport, the
   grip goes away (there is nothing left to resize toward) and the rows that were
   three columns become two. */
@media (max-width: 620px) {
  .modal.is-tree {
    width: calc(100vw - 16px);
    height: calc(100vh - 16px);
  }
  .modal-grip { display: none; }
  .modal.is-tree .tree-zoomers { margin-left: 0; }
  .pet-row { grid-template-columns: auto minmax(0, 1fr); }
  .pet-acts { grid-column: 1 / -1; grid-auto-flow: column; }
  .pet-acts .inv-btn { width: 100%; }
  .wb-grid.is-chest { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .sub-tabs { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

/* ---------- 26h. Reduced motion ----------
   The aura and the bounty pip are the two things added here that move on their
   own. Both carry information in their COLOUR as well as their motion, so
   freezing them at full strength loses nothing. */
@media (prefers-reduced-motion: reduce) {
  .equip-slot.has-enh.has-item,
  .inv-slot.has-enh.has-item,
  .forge-slot.has-enh.has-item {
    animation: none;
    box-shadow:
      0 0 0 3px var(--r, var(--e-steel)),
      0 0 20px rgba(254, 231, 97, .7),
      inset 2px 2px 0 rgba(255, 255, 255, .16),
      inset -2px -2px 0 rgba(0, 0, 0, .40);
  }
  .icon-btn.is-daily:not([data-count=""])::after { animation: none; }
  .dun-stat.is-timer b.is-hot { animation: none; }
}



