/* Full palette rebuild (user: "totally different graphics, colors,
   shaders"): --gold is now a genuine Wickerson Studios orange, and every
   background/border tone leans violet-indigo instead of neutral
   charcoal - since --gold/--panel/--panel-border are referenced
   throughout the whole UI (headings, prices, borders), this one change
   threads the new "computational plane" identity through nearly every
   screen in the game for free. */
:root {
  --bg: #120e1f;
  --panel: #1a1530;
  --panel-border: #4a3a6b;
  --gold: #f36c21;
  --cyan: #4fd6ff;
  --magenta: #d633d6;
  --green: #4ade80;
  --red: #ef4444;
  --text: #e8e8f0;
  /* Bumped from #8a8aa0 - this color is used everywhere for secondary/
     caption text (row descriptions, sub-headers, log history, the
     controls reference), a lot of it at small 11-13px sizes, and the
     original shade sat close to the edge of comfortable contrast
     against --bg for text that small. Lightened enough to read more
     easily without pushing it all the way to --text and losing the
     visual hierarchy secondary text is meant to signal. */
  --dim: #9c9cb8;
  font-family: 'Georgia', 'Times New Roman', serif;
}

* { box-sizing: border-box; }

html, body {
  margin: 0; padding: 0; width: 100%; height: 100%;
  background: var(--bg); color: var(--text);
  overflow: hidden;
  overscroll-behavior: none;
  font-family: 'Segoe UI', system-ui, sans-serif;
}

#scene-container {
  position: fixed; inset: 0;
  cursor: grab;
}
#scene-container.dragging { cursor: grabbing; }
#scene-container canvas { display: block; }

.hidden { display: none !important; }

/* ---------------- Screen FX (vignette, hit flash, level-up flash) ---------------- */
#vignette {
  position: fixed; inset: 0; pointer-events: none; z-index: 5;
  box-shadow: inset 0 0 0 0 rgba(200,0,0,0);
  transition: box-shadow 0.4s ease;
}
#vignette.low-hp {
  box-shadow: inset 0 0 140px 40px rgba(220,20,20,0.55);
  animation: vignette-pulse 1.6s ease-in-out infinite;
}
@keyframes vignette-pulse {
  0%, 100% { box-shadow: inset 0 0 140px 40px rgba(220,20,20,0.45); }
  50% { box-shadow: inset 0 0 170px 60px rgba(220,20,20,0.75); }
}
/* MOBILE-ONLY (user report with a screenshot from an older phone: the
   whole screen - HUD text, minimap, icons, everything - rendered heavily
   blurred, specifically while "CRITICAL" (low HP) was showing). The
   desktop keyframes above animate the box-shadow's OWN blur-radius/
   spread values every frame (140px/40px <-> 170px/60px) - a well-known
   GPU-heavy anti-pattern, since the browser has to re-rasterize the
   entire blurred shadow from scratch on every single frame rather than
   just recompositing an existing layer. On a weaker/older device GPU
   already under load, that continuous re-rasterization is a very
   plausible cause of the reported full-screen blur smearing (worsened by
   this game's several nearby backdrop-filter:blur() elements - #hud-top,
   .stat-chip, the joystick base - all competing for the same compositing
   budget). Fixed on mobile by animating opacity instead - a cheap,
   compositor-only property that never requires re-rasterizing anything -
   against a single FIXED (non-animated) box-shadow blur/spread, keeping
   the same "pulsing intensity" read without the per-frame blur recompute.
   Desktop's own animation above is completely untouched. */
.touch-mode #vignette.low-hp {
  box-shadow: inset 0 0 155px 50px rgba(220,20,20,0.6);
  animation: vignette-pulse-opacity 1.6s ease-in-out infinite;
}
@keyframes vignette-pulse-opacity {
  0%, 100% { opacity: 0.75; }
  50% { opacity: 1; }
}
#fx-layer {
  position: fixed; inset: 0; pointer-events: none; z-index: 6;
  opacity: 0; background: rgba(220,30,30,0.35);
}
#fx-layer.flash { animation: fx-flash 0.28s ease-out; }
@keyframes fx-flash {
  0% { opacity: 0.55; }
  100% { opacity: 0; }
}

/* ---------------- HUD ---------------- */
#hud {
  position: fixed; left: 0; right: 0; bottom: 0; top: 0;
  pointer-events: none;
  display: flex; flex-direction: column; justify-content: space-between;
}
#hud-top {
  pointer-events: auto;
  position: relative;
  display: flex; align-items: flex-start; justify-content: space-between; gap: 18px;
  background: linear-gradient(to bottom, rgba(10,10,18,0.8), rgba(10,10,18,0.48));
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  padding: 12px 22px;
  border-bottom: 1px solid var(--panel-border);
  box-shadow: 0 2px 18px rgba(0,0,0,0.45);
}
/* Ornate frame: thin gold corner brackets over the HUD banner, giving the
   stat readout a "carved plaque" feel instead of a flat toolbar. A slow
   ambient pulse keeps the frame feeling alive rather than a static sticker. */
.hud-frame-corner {
  position: absolute; width: 16px; height: 16px; pointer-events: none;
  border: 2px solid var(--gold);
  animation: hud-corner-glow 3.6s ease-in-out infinite;
}
.hud-frame-corner.tl { top: 4px; left: 4px; border-right: none; border-bottom: none; }
.hud-frame-corner.tr { top: 4px; right: 4px; border-left: none; border-bottom: none; }
.hud-frame-corner.bl { bottom: -1px; left: 4px; border-right: none; border-top: none; }
.hud-frame-corner.br { bottom: -1px; right: 4px; border-left: none; border-top: none; }
@keyframes hud-corner-glow {
  0%, 100% { opacity: 0.55; filter: drop-shadow(0 0 0 rgba(230,199,0,0)); }
  50% { opacity: 0.95; filter: drop-shadow(0 0 4px rgba(230,199,0,0.7)); }
}

#hud-left { display: flex; align-items: center; gap: 12px; }
#hud-portrait {
  width: 48px; height: 48px; flex: none; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  background: radial-gradient(circle at 35% 30%, #2a2a40, #14141f);
  border: 2px solid var(--gold); box-shadow: 0 0 10px rgba(230,199,0,0.35), inset 0 0 8px rgba(0,0,0,0.6);
  font-size: 22px;
}
#hud-identity { display: flex; flex-direction: column; gap: 4px; }
#hud-bars { display: flex; flex-direction: column; gap: 4px; max-width: 300px; }
.bar-row { display: flex; align-items: center; gap: 6px; }
.bar-icon { width: 16px; text-align: center; font-size: 12px; }
.hp-icon { color: var(--red); }
.xp-icon { color: var(--cyan); }
.bar {
  position: relative; flex: 1; height: 14px;
  background: #1a1a26; border: 1px solid var(--panel-border); border-radius: 2px;
  overflow: hidden;
}
.bar-fill {
  height: 100%; width: 0%;
  transition: width 0.35s cubic-bezier(.2,.8,.3,1);
}
.hp-bar .bar-fill { background: linear-gradient(90deg, #7a1d1d, var(--red)); }
.xp-bar .bar-fill { background: linear-gradient(90deg, #1d5c7a, var(--cyan)); }
/* "Chip damage" trailing bar (classic RPG/fighting-game HP-bar technique)
   - sits BEHIND .bar-fill (position:absolute so it doesn't push .bar-fill
   out of normal flow; earlier in DOM order than .bar-fill so it paints
   underneath it in the same stacking context). updateBar (ui.js) sets
   BOTH layers to the same target width every update; this layer's
   transition is slower (0.6s vs .bar-fill's 0.35s above) so on a value
   DROP it lags behind and stays exposed past .bar-fill's new, shorter
   edge until it catches down - that shrinking sliver of exposed chip IS
   the "you just lost this much" cue. On a RISE the chip can never exceed
   fill's already-larger width, so it stays fully hidden - correct for HP
   gains, and for XP ticking up mid-level; it becomes visible on XP again
   specifically at level-up, when XP resets against a larger denominator
   and the bar's percentage can actually drop. */
.bar-chip {
  position: absolute; top: 0; left: 0; height: 100%; width: 0%;
  background: #fff; opacity: 0.6;
  transition: width 0.6s ease-out;
}
.bar-text {
  position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
  font-size: 10.5px; color: #fff; text-shadow: 0 1px 2px rgba(0,0,0,0.9); pointer-events: none;
}
#hud-name { font-weight: 700; color: var(--gold); font-size: 16px; letter-spacing: 0.3px; }
#hud-level-row { display: flex; align-items: baseline; gap: 10px; }
#hud-level { color: var(--cyan); font-size: 12.5px; font-weight: 700; }
#hud-chapter { color: var(--dim); font-style: italic; font-size: 12px; }

#hud-right { display: flex; flex-direction: column; align-items: flex-end; gap: 8px; }
#hud-icon-buttons { display: flex; gap: 6px; }
#hud-stats { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 6px; }
.stat-chip {
  display: flex; align-items: center; gap: 5px;
  background: rgba(255,255,255,0.06); border: 1px solid var(--panel-border); border-radius: 6px;
  backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);
  padding: 3px 9px; font-size: 12.5px; color: var(--text);
}
.chip-icon { font-size: 12px; opacity: 0.9; }
/* User request (from real gameplay footage): every stat chip - AC, ATK,
   gold, Charges, Focus - used the identical flat style, no visual way
   to tell them apart except reading the tiny icon. A left-edge accent
   per category (same "color by meaning" language as the combat log
   fix) lets a player recognize which stat changed out of the corner of
   their eye during a fight, without reading text. */
.chip-ac { border-left: 2px solid #5cc3ff; }
.chip-ac .chip-icon { filter: drop-shadow(0 0 2px #5cc3ff); }
.chip-atk { border-left: 2px solid var(--red); }
.chip-atk .chip-icon { filter: drop-shadow(0 0 2px var(--red)); }
.chip-gold { border-left: 2px solid var(--gold); }
.chip-gold .chip-icon { filter: drop-shadow(0 0 2px var(--gold)); }
#chip-charges { border-left: 2px solid var(--magenta); }
#chip-charges .chip-icon { filter: drop-shadow(0 0 2px var(--magenta)); }
.chip-focus { border-left: 2px solid #ff9d4d; }
.chip-focus .chip-icon { filter: drop-shadow(0 0 2px #ff9d4d); }
/* Low torch fuel warning (see updateHUD's own comment, ui.js) - reuses
   the same --red already used for #hud-status's low-HP text, plus a
   gentle pulse so it reads as an active warning, not just a color swap
   easy to miss mid-fight. */
#hud-torch.low-fuel { color: var(--red); font-weight: 700; animation: low-fuel-pulse 1.4s ease-in-out infinite; }
@keyframes low-fuel-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.55; } }
#hud-status { color: var(--red); font-size: 12.5px; min-height: 14px; font-weight: 700; letter-spacing: 0.5px; }

#hud-log {
  pointer-events: auto;
  align-self: flex-start;
  max-width: 480px;
  margin: 8px 0 0 16px;
  padding: 8px 12px;
  background: rgba(10,10,18,0.7);
  border: 1px solid var(--panel-border);
  border-radius: 2px;
  font-size: 12.5px;
  max-height: 34vh;
  overflow-y: auto;
  line-height: 1.5;
}
#hud-log div {
  color: var(--dim);
  opacity: 0.72;
  padding-left: 7px;
  border-left: 2px solid transparent;
}
#hud-log div:last-child { color: var(--text); opacity: 1; font-weight: 500; }
/* User request (grounded in an actual gameplay-footage review): a flat
   wall of same-color log text made it hard to tell a critical hit from
   routine flavor text from a damage warning at a glance. Declared AFTER
   the :last-child rule above so a category color always wins on the
   newest line too (equal selector specificity - later source order
   decides), not just plain white. */
#hud-log div.log-crit-good { color: var(--gold); border-left-color: var(--gold); font-weight: bold; }
#hud-log div.log-crit-bad { color: #ff5566; border-left-color: #ff5566; font-weight: bold; }
#hud-log div.log-victory { color: var(--green); border-left-color: var(--green); }
#hud-log div.log-levelup { color: var(--gold); border-left-color: var(--gold); font-weight: bold; }
#hud-log div.log-hurt { color: #ff8a8a; border-left-color: #ff5566; }
#hud-log div.log-attack { color: #ffb877; border-left-color: #ffb877; }
#hud-log div.log-loot { color: var(--cyan); border-left-color: var(--cyan); }
#hud-log div.log-warn { color: #d9a066; border-left-color: #d9a066; font-style: italic; }
.log-repeat {
  display: inline-block;
  font-size: 10px;
  font-weight: bold;
  color: var(--gold);
  background: rgba(249,163,63,0.14);
  border-radius: 3px;
  padding: 0 4px;
  margin-left: 2px;
}

/* All hotkeys are listed permanently at the bottom of the screen during
   gameplay, the same "always visible, no click needed" treatment the
   W/A/S/D movement hint already got - #controls-panel sits in normal
   document flow right below the hint row (not an absolutely-positioned
   popup floating over the 3D view the way it once did), so it's part of
   the persistent HUD footer rather than something that has to be
   summoned. #btn-controls-toggle still lets a player collapse it back
   down to just the one-line hint if they'd rather reclaim the vertical
   space - toggling is opt-in to HIDE now, not opt-in to reveal.

   ONE LINE PER CATEGORY, not a 4-row-tall grid (the original layout this
   replaced) - user feedback was that the always-visible panel was eating
   into the 3D gameplay view underneath it. Four compact rows (~90px
   total) instead of up to six stacked rows per column (~180px) roughly
   halves the footer's footprint, and the backgrounds below are
   noticeably more transparent than before (0.72/0.75, was 0.88/0.97) so
   the game world stays visibly readable THROUGH the panel too, not just
   around its edges. */
#hud-controls {
  pointer-events: auto;
  background: rgba(10,10,18,0.72);
  border-top: 1px solid var(--panel-border);
  padding: 4px 16px 6px;
  font-size: 11px;
  color: var(--dim);
  /* align-items:flex-start (was center) - #action-bar (below) is a
     SEPARATE fixed-position layer that's also bottom-anchored and
     horizontally centered, so a centered controls panel sat directly
     underneath the J/P/X ability icons and got visually covered by
     them. Left-aligning this panel and right-anchoring #action-bar
     (see its own rule) puts real horizontal daylight between the two
     instead of stacking them both through the same center column. */
  display: flex; flex-direction: column; align-items: flex-start; gap: 4px;
}
#hud-controls-row { display: flex; align-items: center; justify-content: flex-start; gap: 12px; }
#btn-controls-toggle {
  font-family: inherit; font-size: 11px; font-weight: 700;
  background: transparent; border: 1px solid var(--panel-border); border-radius: 2px;
  color: var(--dim); padding: 2px 8px; cursor: pointer; flex: none;
  transition: all 0.15s ease;
}
#btn-controls-toggle:hover, #btn-controls-toggle.active { border-color: var(--cyan); color: var(--cyan); }
.hud-controls-hint { color: var(--dim); }
#controls-panel {
  background: rgba(14,14,22,0.75);
  border: 1px solid var(--panel-border); border-radius: 2px;
  padding: 6px 16px;
  display: flex; flex-direction: column; gap: 2px;
  max-width: min(97vw, 1000px);
  animation: quick-menu-in 0.14s ease-out;
}
.controls-line {
  display: flex; align-items: baseline; flex-wrap: wrap; gap: 3px 12px;
  padding: 1px 0;
}
.cl-label {
  color: var(--cyan); font-size: 9px; text-transform: uppercase; letter-spacing: 0.5px;
  font-weight: 700; min-width: 58px; flex: none;
}
.ck-pair {
  display: inline-flex; align-items: baseline; gap: 5px; flex: none;
  font-size: 11px; color: var(--text); white-space: nowrap;
}
.ck-pair .ck {
  display: inline-block;
  color: var(--dim); font-weight: 700; font-size: 9.5px;
  border: 1px solid var(--panel-border); border-radius: 3px; padding: 0 4px;
}

/* ---------------- Full screens ---------------- */
.screen {
  position: fixed; inset: 0;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  background: radial-gradient(ellipse at center, #241c3d 0%, #0c0816 100%);
  padding: 24px;
  text-align: center;
  z-index: 20;
  /* BUGFIX: no overflow rule at all here meant a screen taller than the
     viewport (most visibly the class-selection grid - 7 cards, each with
     a heading and a full description, almost always collapsing to a
     single narrow column on a phone-width screen) had nowhere to go:
     justify-content:center overflows a fixed, non-scrolling box equally
     off BOTH edges, silently cropping off some cards with no way to
     reach them at all - "unable to select all classes." Every other
     .screen (title, ability scores, loading, game over) is short enough
     to never actually need this in practice, but the fix belongs on the
     shared base rule rather than a class-select-only patch, since ANY of
     them could in principle overflow a short enough device. */
  overflow-y: auto;
}
.screen h1 { font-size: 42px; margin: 0 0 8px; color: var(--gold); letter-spacing: 1px; }
.screen h2 { font-size: 26px; margin: 0 0 20px; color: var(--cyan); }
.subtitle { color: var(--dim); margin-bottom: 32px; max-width: 560px; }
.hint { color: var(--dim); font-size: 12px; margin-top: 24px; }

/* Real external links (blender.org, the official Manual, PolyHaven,
   Blender Guru's channel - user request: "sprinkle the url links
   throughout our game") - themed to match the game's own cyan accent
   instead of the browser's default blue/purple, so they read as an
   intentional part of the UI rather than an unstyled leftover. */
a { color: var(--cyan); text-decoration: underline; text-decoration-color: rgba(79, 214, 255, 0.4); }
a:hover, a:focus { color: var(--gold); text-decoration-color: var(--gold); }
a:visited { color: var(--cyan); }

/* ---------------- Title screen: Desktop/Mobile control mode toggle ---------------- */
#ui-mode-toggle {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 14px 0;
}
.ui-mode-label { color: var(--dim); font-size: 12px; }
.ui-mode-btn {
  font-family: inherit;
  /* Bumped from 12px/7px 14px unconditionally (not touch-mode-scoped) -
     this is the ONE control a touch player has to interact with BEFORE
     touch-mode is even active (it's how they turn it on), so it can't
     rely on .touch-mode CSS to become touch-friendly. */
  font-size: 14px;
  padding: 12px 20px;
  background: var(--panel);
  border: 1px solid var(--panel-border);
  color: var(--dim);
  border-radius: 2px;
  cursor: pointer;
  transition: all 0.15s;
}
.ui-mode-btn:hover { border-color: var(--cyan); color: var(--cyan); }
.ui-mode-btn.active { border-color: var(--gold); color: var(--gold); background: #2a2410; }

/* ---------------- Loading screen ---------------- */
#loading-spinner {
  width: 42px; height: 42px; margin-bottom: 20px;
  border: 3px solid var(--panel-border);
  border-top-color: var(--cyan);
  border-radius: 50%;
  animation: loading-spin 0.85s linear infinite;
}
@keyframes loading-spin { to { transform: rotate(360deg); } }
.accent { color: var(--cyan); }

.menu-btn {
  font-family: inherit;
  font-size: 16px;
  padding: 12px 28px;
  margin: 6px;
  background: var(--panel);
  border: 1px solid var(--panel-border);
  color: var(--text);
  border-radius: 2px;
  cursor: pointer;
  transition: all 0.15s;
}
.menu-btn:hover { border-color: var(--cyan); color: var(--cyan); background: #1c1c30; }
.menu-btn.primary { border-color: var(--gold); color: var(--gold); }
.menu-btn.primary:hover { background: #2a2410; }
.btn-row { display: flex; gap: 10px; margin-top: 16px; }

.card-grid {
  display: grid;
  /* 220px -> 160px - user request: "ensure that all classes are
     available to see in one window." At 220px minimum, any phone-width
     screen (portrait especially) could only ever fit a single column,
     turning 7 full class cards into a tall single-file list that needed
     heavy scrolling even after the .screen overflow fix made scrolling
     possible at all. 160px lets most phones fit 2 columns instead,
     roughly halving how far you need to scroll to see every class. */
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  gap: 12px;
  max-width: 900px;
  width: 100%;
}
.card {
  background: var(--panel);
  border: 1px solid var(--panel-border);
  border-left: 3px solid var(--card-accent, var(--panel-border));
  border-radius: 3px;
  padding: 16px;
  cursor: pointer;
  text-align: left;
  transition: all 0.15s;
}
.card:hover {
  border-color: var(--card-accent, var(--cyan));
  background: #1c1c30; transform: translateY(-2px);
  box-shadow: 0 0 0 1px var(--card-accent, var(--cyan)) inset, 0 6px 18px rgba(0,0,0,0.4);
}
/* Real Tab/keyboard navigation (user request) - these cards are plain
   divs (ui.js's makeRowsKeyboardFocusable adds the tabindex/role that
   makes them focusable at all), so unlike a native <button> there's no
   browser default focus ring to fall back on. Reuses :hover's own look
   (same border/lift/glow a mouse-hover already gives) plus a real
   outline, so Tabbing through the class/alignment/difficulty pickers is
   exactly as visible as pointing at them. :focus-visible, not :focus,
   so a mouse click doesn't draw the same ring a keyboard Tab does. */
.card:focus-visible {
  border-color: var(--card-accent, var(--cyan));
  background: #1c1c30; transform: translateY(-2px);
  box-shadow: 0 0 0 1px var(--card-accent, var(--cyan)) inset, 0 6px 18px rgba(0,0,0,0.4);
  outline: 2px solid var(--cyan);
  outline-offset: 2px;
}
.card h3 { margin: 0 0 6px; color: var(--gold); font-size: 17px; display: flex; align-items: center; gap: 8px; }
.card p { margin: 3px 0; font-size: 13px; color: var(--dim); }
.card .key { color: var(--cyan); font-weight: 700; margin-right: 6px; }
.card .class-icon { font-size: 16px; }

.stat-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 10px 24px;
  font-size: 15px;
  margin-bottom: 12px;
}
.stat-grid div span:first-child { color: var(--dim); margin-right: 6px; }
#ability-derived { color: var(--cyan); font-size: 13px; max-width: 520px; margin-bottom: 8px; }

/* ---------------- Modals (Shop/Inventory/Quiz/etc) ---------------- */
/* User request: "can any of these infinite series algorithmic formulas be
   incorporated... tessellation...?" - a real triangular tessellation (the
   same zigzag-of-equilateral-triangles construction Blender's own mesh
   triangulation produces) sits faintly behind every modal in the game,
   tying the UI chrome itself back to the game's subject matter rather
   than being pure flavor text. Encoded as an inline SVG data URI (no
   extra image file, no JS canvas generation needed) at very low stroke
   opacity so it reads as texture, never competes with modal content. */
#modal-overlay {
  position: fixed; inset: 0;
  background:
    rgba(4,4,8,0.82)
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='60' height='104' viewBox='0 0 60 104'%3E%3Cg fill='none' stroke='%23ffd54a' stroke-opacity='0.06' stroke-width='1'%3E%3Cpath d='M0 0L30 52L0 104M60 0L30 52L60 104'/%3E%3C/g%3E%3C/svg%3E");
  display: flex; align-items: center; justify-content: center;
  z-index: 30;
}
#modal-box {
  /* flex column, NOT the overflow-y:auto block this used to be - the
     close button (position:absolute, see #modal-close-btn below) needs
     to stay fixed in the corner regardless of how far the modal's own
     content scrolls, which means the SCROLLING has to happen on
     #modal-content specifically, not on modal-box itself (an absolutely-
     positioned child still scrolls away with its scrolling ancestor if
     that ancestor is the one with overflow:auto). flex + min-height:0 on
     the content child (below) is the standard, robust way to get a
     fixed-height container with a non-scrolling chrome element and an
     independently-scrolling content area, without hand-computing pixel
     heights that would drift out of sync with the padding/button size.

     `resize: both` gives every modal screen a native, browser-drawn grip
     in the bottom-right corner for free - no custom resize JS needed at
     all (initModalWindow in ui.js only has to listen for the resulting
     size change via ResizeObserver, to persist it). `overflow: hidden`
     is required for `resize` to take effect per spec, and is safe here
     specifically because #modal-content (below) already owns its own
     independent overflow-y:auto scrolling - modal-box itself never
     needed to scroll its own box, just to clip at its own edge.
     max-width/max-height are deliberately capped well short of the full
     viewport (92vw/92vh, not 100) so the 3D game world behind the modal
     always stays at least partly visible/framing it, keeping the
     gameplay viewport the single largest thing on screen even at the
     modal's maximum resized size. */
  position: relative;
  display: flex;
  flex-direction: column;
  background: rgba(20,20,31,0.88);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  border: 1px solid var(--panel-border);
  border-radius: 3px;
  padding: 24px 28px;
  min-width: 340px;
  min-height: 220px;
  max-width: 92vw;
  max-height: 92vh;
  box-shadow: 0 12px 40px rgba(0,0,0,0.6);
  resize: both;
  overflow: hidden;
}
#modal-content { overflow-y: auto; min-height: 0; }
/* A thin, always-visible grip bar at the very top of the modal - "bleeds"
   past modal-box's own padding via negative margins so it reaches flush
   to the box's top/left/right edges, reading as a real title bar rather
   than an indented strip. initModalWindow (ui.js) drags the whole modal
   by listening for pointer events on this element specifically, so
   clicks everywhere else in a modal (rows, buttons, quiz choices) are
   completely unaffected. Visible on every screen from the start, not
   just after a first drag, so the "this window can be moved" affordance
   is discoverable without needing a tooltip to be read first. */
/* touch-action:none on every drag handle AND resize grip below - without
   it, a touch browser's own scroll/pan gesture recognition can hijack the
   drag before pointermove events ever reach makeWindowDraggable's own
   handler (setPointerCapture + e.preventDefault() alone are not reliably
   enough to stop that on real mobile browsers), which is exactly the kind
   of thing that reads as "dragging doesn't really work on my phone." */
#modal-drag-handle, #unit-circle-drag-handle, #reference-sheets-drag-handle, #worksheet-drag-handle,
.touch-resize-grip {
  touch-action: none;
}
#modal-drag-handle {
  margin: -24px -28px 14px -28px;
  padding: 7px 0;
  text-align: center;
  color: var(--dim);
  font-size: 11px;
  letter-spacing: 3px;
  line-height: 1;
  cursor: move;
  background: rgba(255,255,255,0.03);
  border-bottom: 1px solid var(--panel-border);
  border-radius: 3px 3px 0 0;
  flex: none;
  user-select: none;
}
#modal-drag-handle:hover { color: var(--cyan); background: rgba(79,214,255,0.08); }
#modal-close-btn {
  /* top is 36px, not the original 10px - #modal-drag-handle (above) now
     occupies the top ~26px of the box via a negative-margin "bleed", so
     the close button is nudged down to clear it with a safe margin
     rather than risk overlapping it on browsers with slightly different
     font-metric-driven line-height rendering. */
  position: absolute; top: 36px; right: 10px; z-index: 2;
  width: 28px; height: 28px;
  display: flex; align-items: center; justify-content: center;
  background: rgba(255,255,255,0.06);
  border: 1px solid var(--panel-border);
  border-radius: 50%;
  color: var(--dim);
  font-size: 18px; line-height: 1; font-family: inherit;
  cursor: pointer;
  transition: all 0.15s ease;
}
#modal-close-btn:hover { color: var(--text); border-color: var(--cyan); background: rgba(79,214,255,0.14); transform: rotate(90deg); }
/* Entrance animation, replayed each time openModal() runs (see ui.js -
   toggling a class that's already applied wouldn't restart a CSS
   animation, so ui.js removes it, forces a reflow, then re-adds it) -
   combined with a persistent ambient glow pulse on the SAME class, since
   an element's `animation` shorthand can only come from one matching
   rule at a time: a plain `#modal-box{animation:...}` base rule would be
   entirely overridden by the more specific `.modal-anim` selector for as
   long as that class is applied (i.e. almost the whole time the modal is
   open), so the comma-separated multi-animation form is used instead to
   run both at once on one rule. */
#modal-overlay.modal-anim { animation: modal-overlay-in 0.18s ease-out; }
#modal-box.modal-anim {
  animation: modal-box-in 0.22s cubic-bezier(.22,.85,.32,1.15) 1, modal-box-glow 5s ease-in-out infinite;
}
@keyframes modal-overlay-in {
  from { background: rgba(4,4,8,0); }
  to { background: rgba(4,4,8,0.82); }
}
@keyframes modal-box-in {
  from { opacity: 0; transform: scale(0.92) translateY(10px); }
  to { opacity: 1; transform: scale(1) translateY(0); }
}
@keyframes modal-box-glow {
  0%, 100% { box-shadow: 0 12px 40px rgba(0,0,0,0.6), 0 0 0 rgba(230,199,0,0); }
  50% { box-shadow: 0 12px 40px rgba(0,0,0,0.6), 0 0 26px rgba(230,199,0,0.22); }
}

/* ---------------- Unit Circle reference panel ---------------- */
/* A standalone floating window (see index.html's comment) - same glassy
   chrome language as #modal-box (border/blur/shadow) but with NO
   overlay/backdrop parent to center it, since it has to be able to sit
   alongside #modal-box rather than inside it. Default position anchors
   to the upper-right (not centered) specifically so it doesn't overlap
   a simultaneously-open, centered quiz modal until the player drags one
   of them - both windows share the exact same drag/resize/persist
   mechanism (makeWindowDraggable, ui.js), just keyed to their own
   separate localStorage slots. z-index is 35, deliberately HIGHER than
   #modal-overlay's 30 - the entire point of this panel is to stay usable
   while a quiz modal is open, so it must render (and stay clickable)
   ABOVE the modal's own dark backdrop, not underneath it where it would
   be dimmed and unreadable right when it's most needed. */
#unit-circle-panel {
  position: fixed;
  top: 90px;
  right: 24px;
  z-index: 35;
  display: flex;
  flex-direction: column;
  background: rgba(20,20,31,0.92);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  border: 1px solid var(--panel-border);
  border-radius: 3px;
  padding: 10px 14px 14px;
  min-width: 280px;
  min-height: 280px;
  max-width: 92vw;
  max-height: 92vh;
  box-shadow: 0 12px 40px rgba(0,0,0,0.6);
  resize: both;
  overflow: hidden;
}
#unit-circle-drag-handle {
  margin: -10px -14px 8px -14px;
  padding: 6px 0;
  text-align: center;
  color: var(--dim);
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 2px;
  line-height: 1;
  cursor: move;
  background: rgba(255,255,255,0.03);
  border-bottom: 1px solid var(--panel-border);
  border-radius: 3px 3px 0 0;
  flex: none;
  user-select: none;
}
#unit-circle-drag-handle:hover { color: var(--cyan); background: rgba(79,214,255,0.08); }
#unit-circle-close-btn {
  position: absolute; top: 32px; right: 10px; z-index: 2;
  width: 24px; height: 24px;
  display: flex; align-items: center; justify-content: center;
  background: rgba(255,255,255,0.06);
  border: 1px solid var(--panel-border);
  border-radius: 50%;
  color: var(--dim);
  font-size: 16px; line-height: 1; font-family: inherit;
  cursor: pointer;
  transition: all 0.15s ease;
}
#unit-circle-close-btn:hover { color: var(--text); border-color: var(--cyan); background: rgba(79,214,255,0.14); transform: rotate(90deg); }
#unit-circle-canvas { width: 100%; height: 100%; min-height: 0; flex: 1; }

/* ---------------- Formula Reference panel ---------------- */
/* Same glassy floating-window chrome as #unit-circle-panel, anchored to
   the upper-LEFT by default (instead of upper-right) so the two panels
   don't overlap each other before the player ever drags either one -
   z-index 35 for the same "stay above the quiz modal's backdrop" reason
   documented on #unit-circle-panel above. */
#reference-sheets-panel {
  position: fixed;
  top: 90px;
  left: 24px;
  z-index: 35;
  display: flex;
  flex-direction: column;
  background: rgba(20,20,31,0.92);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  border: 1px solid var(--panel-border);
  border-radius: 3px;
  padding: 10px 14px 14px;
  width: 760px;
  height: 580px;
  min-width: 340px;
  min-height: 260px;
  max-width: 92vw;
  max-height: 92vh;
  box-shadow: 0 12px 40px rgba(0,0,0,0.6);
  resize: both;
  overflow: hidden;
}
#reference-sheets-drag-handle {
  margin: -10px -14px 8px -14px;
  padding: 6px 0;
  text-align: center;
  color: var(--dim);
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 2px;
  line-height: 1;
  cursor: move;
  background: rgba(255,255,255,0.03);
  border-bottom: 1px solid var(--panel-border);
  border-radius: 3px 3px 0 0;
  flex: none;
  user-select: none;
}
#reference-sheets-drag-handle:hover { color: var(--cyan); background: rgba(79,214,255,0.08); }
#reference-sheets-close-btn {
  position: absolute; top: 32px; right: 10px; z-index: 2;
  width: 24px; height: 24px;
  display: flex; align-items: center; justify-content: center;
  background: rgba(255,255,255,0.06);
  border: 1px solid var(--panel-border);
  border-radius: 50%;
  color: var(--dim);
  font-size: 16px; line-height: 1; font-family: inherit;
  cursor: pointer;
  transition: all 0.15s ease;
}
#reference-sheets-close-btn:hover { color: var(--text); border-color: var(--cyan); background: rgba(79,214,255,0.14); transform: rotate(90deg); }
#wing-floor-tabs {
  display: flex;
  gap: 4px;
  flex: none;
  margin-bottom: 8px;
  flex-wrap: wrap;
}
.ref-tab {
  flex: 1;
  padding: 6px 4px;
  background: rgba(255,255,255,0.04);
  border: 1px solid var(--panel-border);
  border-radius: 2px;
  color: var(--dim);
  font-family: inherit;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.5px;
  cursor: pointer;
  transition: all 0.15s ease;
}
.ref-tab:hover { color: var(--text); border-color: var(--cyan); }
.ref-tab.active { color: #05050a; background: var(--cyan); border-color: var(--cyan); }

/* ================ The Codex of Duchame (Command Reference) ================ */
/* A genuine rebuild, not a reskin - user feedback: "completely different,
   not to resemble anything like it currently... plain hate the aesthetics
   and content." Old design: a horizontal tab bar over a flat scrolling
   list of terse title/description bullet rows with click-to-reveal
   diagrams (felt like a spec sheet). New design: a real book - a
   left-hand illuminated index (grouped, not a flat tab row) beside a
   right-hand "page" that reads as continuous in-world prose with its
   diagrams always visible inline as manuscript illustrations, not hidden
   behind a button. Cinzel (display) + EB Garamond (body) - a real
   illuminated-manuscript typographic identity distinct from the rest of
   this game's Georgia/Segoe UI pairing, since this screen earns its own
   voice as "the one book in the game," not another HUD panel. Actual
   panel width/height set directly on #reference-sheets-panel above
   (760x580, up from the old panel's 460x500) - an ID selector there
   would otherwise out-specificity a same-purpose class rule here. */
#codex-body {
  flex: 1;
  min-height: 0;
  display: flex;
  gap: 0;
  overflow: hidden;
  border: 1px solid var(--panel-border);
  border-radius: 4px;
  background: #0c0918;
}
#codex-index {
  flex: none;
  width: 208px;
  overflow-y: auto;
  padding: 10px 8px;
  background: linear-gradient(180deg, rgba(243,108,33,0.05), transparent 12%), rgba(0,0,0,0.22);
  border-right: 1px solid var(--panel-border);
}
.codex-index-group {
  margin-bottom: 12px;
}
.codex-index-group-title {
  font-family: 'Cinzel', Georgia, serif;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 1.6px;
  text-transform: uppercase;
  color: var(--gold);
  opacity: 0.75;
  padding: 0 6px 4px;
  border-bottom: 1px solid rgba(243,108,33,0.25);
  margin-bottom: 4px;
}
.codex-index-item {
  display: block;
  width: 100%;
  text-align: left;
  background: none;
  border: none;
  border-left: 2px solid transparent;
  color: var(--dim);
  font-family: 'EB Garamond', Georgia, serif;
  font-size: 13px;
  line-height: 1.35;
  padding: 4px 8px;
  cursor: pointer;
  border-radius: 0 3px 3px 0;
  transition: all 0.15s ease;
}
.codex-index-item:hover { color: var(--text); background: rgba(255,255,255,0.04); border-left-color: var(--panel-border); }
.codex-index-item.active { color: var(--gold); background: rgba(243,108,33,0.12); border-left-color: var(--gold); font-weight: 600; }
#codex-page {
  flex: 1;
  min-width: 0;
  overflow-y: auto;
  padding: 22px 28px 30px;
  background:
    radial-gradient(ellipse at 20% -10%, rgba(243,108,33,0.07), transparent 45%),
    #120e1f;
}
.codex-eyebrow {
  font-family: 'Cinzel', Georgia, serif;
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: 2.2px;
  text-transform: uppercase;
  color: var(--cyan);
  opacity: 0.8;
  margin: 0 0 4px;
}
.codex-title {
  font-family: 'Cinzel', Georgia, serif;
  font-size: 26px;
  font-weight: 700;
  color: var(--text);
  margin: 0 0 4px;
  line-height: 1.15;
  text-wrap: balance;
}
.codex-title::first-letter {
  color: var(--gold);
  font-size: 1.35em;
}
.codex-deck {
  font-family: 'EB Garamond', Georgia, serif;
  font-style: italic;
  font-size: 14px;
  color: var(--dim);
  margin: 0 0 18px;
  padding-bottom: 14px;
  border-bottom: 1px solid var(--panel-border);
}
.codex-entry {
  display: flex;
  gap: 16px;
  align-items: flex-start;
  margin-bottom: 22px;
  padding-bottom: 20px;
  border-bottom: 1px dashed rgba(255,255,255,0.08);
}
.codex-entry:last-of-type { border-bottom: none; }
.codex-entry-art {
  flex: none;
  width: 128px;
  cursor: zoom-in;
  border-radius: 6px;
  overflow: hidden;
  border: 1px solid var(--panel-border);
  transition: border-color 0.15s ease, transform 0.15s ease;
}
.codex-entry-art:hover { border-color: var(--gold); transform: scale(1.02); }
.codex-entry-art svg { display: block; width: 100%; height: auto; }
.codex-entry-body { flex: 1; min-width: 0; }
.codex-entry-title {
  font-family: 'Cinzel', Georgia, serif;
  font-size: 15px;
  font-weight: 700;
  color: var(--gold);
  margin: 0 0 6px;
  letter-spacing: 0.3px;
}
.codex-entry-text {
  font-family: 'EB Garamond', Georgia, serif;
  font-size: 15.5px;
  line-height: 1.55;
  color: var(--text);
  margin: 0;
}
.codex-crossrefs {
  margin-top: 4px;
  padding-top: 16px;
  border-top: 1px solid var(--panel-border);
}
.codex-crossrefs-label {
  font-family: 'Cinzel', Georgia, serif;
  font-size: 10px;
  letter-spacing: 1.6px;
  text-transform: uppercase;
  color: var(--dim);
  margin: 0 0 8px;
}
.codex-chip {
  display: inline-block;
  background: rgba(79,214,255,0.08);
  border: 1px solid var(--panel-border);
  color: var(--cyan);
  font-family: 'EB Garamond', Georgia, serif;
  font-size: 12.5px;
  padding: 4px 11px;
  border-radius: 999px;
  margin: 0 6px 6px 0;
  cursor: pointer;
  transition: all 0.15s ease;
}
.codex-chip:hover { background: rgba(79,214,255,0.18); border-color: var(--cyan); }
.codex-closing {
  margin-top: 6px;
  font-family: 'EB Garamond', Georgia, serif;
  font-style: italic;
  font-size: 13px;
  color: var(--dim);
  opacity: 0.75;
}

/* ---------------- Command Reference demo lightbox ---------------- */
/* User feedback: the inline demo panel lives inside a 340-460px sidebar -
   too small to actually read the detail well. This shows the identical
   diagram at real size in a large, centered lightbox. Deliberately its
   own overlay (NOT #modal-overlay/openModal) - no game.paused coupling,
   same "stays open through play" design as the reference panel itself. */
#ref-demo-lightbox {
  position: fixed; inset: 0;
  background: rgba(4,4,8,0.86);
  display: flex; align-items: center; justify-content: center;
  z-index: 60;
  padding: 24px;
}
#ref-demo-lightbox-box {
  position: relative;
  background: rgba(20,20,31,0.96);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  border: 1.5px solid var(--gold);
  border-radius: 10px;
  padding: 28px 32px 24px;
  /* Now that the inline demo panel has no width cap of its own (see
     .ref-demo-panel), a maximized reference panel can already show the
     diagram wider than the old 640px lightbox cap - bumped so
     "enlarge" is still meaningfully bigger in that case too. */
  width: min(900px, 92vw);
  box-shadow: 0 20px 60px rgba(0,0,0,0.7);
}
#ref-demo-lightbox-close {
  position: absolute; top: 10px; right: 10px;
  width: 30px; height: 30px;
  display: flex; align-items: center; justify-content: center;
  background: rgba(255,255,255,0.06);
  border: 1px solid var(--panel-border);
  border-radius: 50%;
  color: var(--dim);
  font-size: 18px;
  cursor: pointer;
}
#ref-demo-lightbox-close:hover { color: var(--text); border-color: var(--cyan); background: rgba(79,214,255,0.14); }
#ref-demo-lightbox-crumb {
  font-size: 10.5px;
  font-weight: bold;
  letter-spacing: 0.5px;
  text-transform: uppercase;
  color: var(--dim);
  margin-bottom: 8px;
  padding-right: 30px;
}
#ref-demo-lightbox-key {
  display: inline-block;
  background: var(--gold);
  color: #05050a;
  font-family: "Cascadia Code", "Consolas", monospace;
  font-weight: bold;
  font-size: 14px;
  padding: 4px 12px;
  border-radius: 5px;
  margin-bottom: 10px;
}
#ref-demo-lightbox-label {
  font-size: 18px;
  font-weight: bold;
  color: var(--text);
  margin-bottom: 14px;
  padding-right: 30px;
  line-height: 1.35;
}
#ref-demo-lightbox-svg svg { display: block; width: 100%; height: auto; }
#ref-demo-lightbox-link {
  display: block;
  margin-top: 14px;
  padding-top: 12px;
  border-top: 1px solid var(--panel-border);
  color: var(--gold);
  font-weight: bold;
  font-size: 13px;
  text-decoration: none;
}
#ref-demo-lightbox-link:hover { text-decoration: underline; }

/* ---------------- Interactive Worksheet panel ---------------- */
/* Same floating-window chrome as #unit-circle-panel/#reference-sheets-panel
   (see their comments) - anchored bottom-left by default so all three
   reference-style panels can be open simultaneously (top-right/top-left/
   bottom-left) without starting out on top of one another. */
#worksheet-panel {
  position: fixed;
  bottom: 24px;
  left: 24px;
  z-index: 35;
  display: flex;
  flex-direction: column;
  background: rgba(20,20,31,0.92);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  border: 1px solid var(--panel-border);
  border-radius: 3px;
  padding: 10px 14px 14px;
  width: 380px;
  height: 460px;
  min-width: 300px;
  min-height: 320px;
  max-width: 92vw;
  max-height: 92vh;
  box-shadow: 0 12px 40px rgba(0,0,0,0.6);
  resize: both;
  overflow: hidden;
}
#worksheet-drag-handle {
  margin: -10px -14px 8px -14px;
  padding: 6px 0;
  text-align: center;
  color: var(--dim);
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 2px;
  line-height: 1;
  cursor: move;
  background: rgba(255,255,255,0.03);
  border-bottom: 1px solid var(--panel-border);
  border-radius: 3px 3px 0 0;
  flex: none;
  user-select: none;
}
#worksheet-drag-handle:hover { color: var(--cyan); background: rgba(79,214,255,0.08); }
#worksheet-close-btn {
  position: absolute; top: 32px; right: 10px; z-index: 2;
  width: 24px; height: 24px;
  display: flex; align-items: center; justify-content: center;
  background: rgba(255,255,255,0.06);
  border: 1px solid var(--panel-border);
  border-radius: 50%;
  color: var(--dim);
  font-size: 16px; line-height: 1; font-family: inherit;
  cursor: pointer;
  transition: all 0.15s ease;
}
#worksheet-close-btn:hover { color: var(--text); border-color: var(--cyan); background: rgba(79,214,255,0.14); transform: rotate(90deg); }
#worksheet-tabs { display: flex; gap: 4px; flex: none; margin-bottom: 8px; }
.ws-tab {
  flex: 1;
  padding: 6px 4px;
  background: rgba(255,255,255,0.04);
  border: 1px solid var(--panel-border);
  border-radius: 2px;
  color: var(--dim);
  font-family: inherit;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.5px;
  cursor: pointer;
  transition: all 0.15s ease;
}
.ws-tab:hover { color: var(--text); border-color: var(--cyan); }
.ws-tab.active { color: #05050a; background: var(--cyan); border-color: var(--cyan); }
#worksheet-scratch-tab { flex: 1; min-height: 0; display: flex; flex-direction: column; }
#worksheet-scratch-toolbar {
  display: flex;
  align-items: center;
  gap: 10px;
  flex: none;
  margin-bottom: 6px;
}
#worksheet-clear-btn {
  padding: 4px 10px;
  background: rgba(255,255,255,0.06);
  border: 1px solid var(--panel-border);
  border-radius: 2px;
  color: var(--text);
  font-family: inherit;
  font-size: 11px;
  cursor: pointer;
}
#worksheet-clear-btn:hover { border-color: var(--cyan); color: var(--cyan); }
.ws-hint { color: var(--dim); font-size: 10px; }
#worksheet-canvas {
  flex: 1;
  min-height: 0;
  width: 100%;
  background: #05050a;
  border: 1px solid var(--panel-border);
  border-radius: 2px;
  cursor: crosshair;
  touch-action: none;
}
#worksheet-calc-tab { flex: 1; min-height: 0; display: flex; flex-direction: column; }
#worksheet-calc-display {
  flex: none;
  background: #05050a;
  border: 1px solid var(--panel-border);
  border-radius: 2px;
  padding: 8px 10px;
  color: var(--text);
  font-family: "Cascadia Code", "Consolas", monospace;
  font-size: 15px;
  min-height: 20px;
  overflow-x: auto;
  white-space: nowrap;
}
#worksheet-calc-result {
  flex: none;
  color: var(--gold);
  font-family: "Cascadia Code", "Consolas", monospace;
  font-size: 13px;
  padding: 3px 10px 6px;
  min-height: 16px;
}
/* overflow-y:auto - user request: "all windows and popups must have full
   scrolling capabilities to select all functionality." Flex rows/buttons
   below have no explicit min-height, so at a small enough panel resize
   they'd otherwise refuse to shrink past their text's own intrinsic
   minimum and get silently clipped by the panel's overflow:hidden with
   no way to reach the cut-off rows - this guarantees every calculator
   button stays reachable by scrolling instead. */
#worksheet-calc-buttons { flex: 1; min-height: 0; display: flex; flex-direction: column; gap: 4px; overflow-y: auto; }
.calc-row { flex: 1; display: flex; gap: 4px; }
.calc-btn {
  flex: 1;
  background: rgba(255,255,255,0.05);
  border: 1px solid var(--panel-border);
  border-radius: 2px;
  color: var(--text);
  font-family: inherit;
  font-size: 13px;
  cursor: pointer;
  transition: all 0.1s ease;
}
.calc-btn:hover { border-color: var(--cyan); color: var(--cyan); background: rgba(79,214,255,0.08); }
.calc-btn.calc-op { color: var(--gold); }
.calc-btn.calc-wide { flex: 2; font-size: 11px; }
.calc-btn.active { background: var(--cyan); color: #05050a; border-color: var(--cyan); }

/* ---------------- Collapsible floating windows ---------------- */
/* User request: "all other windows can be collapsable" - the three
   "stays open while playing" reference panels (Unit Circle, Formula
   Reference, Worksheet) can now shrink down to just their title bar
   instead of either eating real screen space or being closed outright
   and losing their remembered position/size. Shared classes rather than
   three near-identical copies, since all three panels already use the
   exact same drag-handle/close-btn chrome (see each panel's own CSS
   section) - .window-content marks each panel's actual content
   element(s) (a canvas, a tabs+content pair, or three stacked tabs) so
   ONE rule hides whichever ones a given panel has, and
   .window-collapse-btn is the shared look for the new minimize button
   sitting just left of each panel's existing close button. JS side: see
   ui.js's makeWindowDraggable, which now also owns collapse state. */
.window-collapse-btn {
  position: absolute; top: 32px; right: 40px; z-index: 2;
  width: 24px; height: 24px;
  display: flex; align-items: center; justify-content: center;
  background: rgba(255,255,255,0.06);
  border: 1px solid var(--panel-border);
  border-radius: 50%;
  color: var(--dim);
  font-size: 16px; line-height: 1; font-family: inherit;
  cursor: pointer;
  transition: all 0.15s ease;
}
.window-collapse-btn:hover { color: var(--text); border-color: var(--cyan); background: rgba(79,214,255,0.14); }
/* User request (emphatic): "all windows should be able to be toggled on
   and off and collapsible 100%" - the main modal (#modal-box - Shop,
   Inventory, Character Sheet, Bestiary, Settings, quizzes, everything
   that isn't one of the three reference panels) is the 4th and last
   window type in the game and now gets the exact same treatment. Its
   own close button sits 4px lower than the smaller panels' (top:36px,
   not 32px - #modal-box's drag-handle bleeds slightly differently, see
   #modal-close-btn's own comment), so its collapse button needs its own
   position override rather than inheriting the shared default below. */
#modal-collapse-btn { top: 36px; right: 44px; }
.window-collapsed {
  height: 44px !important;
  min-height: 0 !important;
  resize: none !important;
  overflow: hidden;
}
.window-collapsed .window-content { display: none; }
/* The close/collapse buttons are position:absolute at top:32px (36px for
   the main modal), which assumes real content below the handle to sit at
   the top of - once collapsed (the box forced down to 44px total
   height), that would sit right at/past the bottom edge, so both buttons
   move up into the handle's own band instead. */
.window-collapsed #modal-close-btn,
.window-collapsed #modal-collapse-btn,
.window-collapsed #unit-circle-close-btn,
.window-collapsed #reference-sheets-close-btn,
.window-collapsed #worksheet-close-btn,
.window-collapsed .window-collapse-btn {
  top: 6px;
}

#modal-box h2 { margin: 0 0 4px; color: var(--gold); font-size: 20px; }
/* User request: "make better ui and ux unrealengine style please" -
   Unreal's own Details-panel section headers sit on a thin hairline
   divider rather than floating free; the uppercase/letter-spaced label
   style above was already close to that "technical panel" look, this
   just adds the missing underline to complete it. */
#modal-box h3 { color: var(--cyan); font-size: 14px; margin: 14px 0 6px; text-transform: uppercase; letter-spacing: 0.5px; padding-bottom: 4px; border-bottom: 1px solid var(--panel-border); }
#modal-box .sub { color: var(--dim); font-size: 13px; margin-bottom: 12px; }
#modal-box .row {
  display: flex; flex-wrap: wrap; justify-content: space-between; align-items: center;
  padding: 8px 10px; margin: 4px 0;
  border: 1px solid var(--panel-border);
  border-radius: 2px;
  cursor: pointer;
  font-size: 14px;
}
#modal-box .row:hover { border-color: var(--cyan); background: #1c1c30; }
/* BUGFIX (found from real gameplay footage - "Hedge Wizard130gpHire a
   glass-cannon..." all glued together with zero spacing, no line break at
   all). The row's markup puts a <br> between the price and desc spans,
   but a <br> does nothing inside a flex container - it's just another
   zero-width flex item, not a real line break. That accidentally still
   LOOKED right whenever there was enough leftover width for
   justify-content:space-between to spread across, but for a long enough
   name+price+desc combo (this row happened to be the longest in the
   list) there's no leftover space left to distribute, so every item
   renders flush against its neighbor. flex-basis:100% forces .desc onto
   its own line every time, matching what the <br> in the markup always
   implied it should do - no JS/markup change needed, this is purely the
   layout catching up to the content it was already written for. */
/* Real Tab/keyboard navigation (user request) - see .card:focus-visible's
   own comment for the full reasoning (plain divs, no native focus ring,
   :focus-visible not :focus). Every shop/inventory/model-library/
   bestiary/fast-travel list in the game is built from these rows, so
   this one rule covers all of them. */
#modal-box .row:focus-visible { border-color: var(--cyan); background: #1c1c30; outline: 2px solid var(--cyan); outline-offset: 2px; }
#modal-box .row .price { color: var(--gold); }
#modal-box .row .desc { color: var(--dim); font-size: 12px; flex-basis: 100%; margin-top: 2px; }
#modal-box .row.disabled { opacity: 0.45; cursor: not-allowed; }
#modal-box .close-row { text-align: center; margin-top: 14px; color: var(--dim); font-size: 12px; }
/* New-character intro tips (openIntroTips, ui.js) - a small step-progress
   dot row, same idea as any onboarding carousel. */
.intro-tip-dots { display: flex; gap: 6px; margin-top: 18px; }
.intro-tip-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--panel-border); }
.intro-tip-dot.active { background: var(--cyan); }
/* About / Elevator Pitch screen (openAboutScreen, ui.js). */
#modal-box .about-pitch p { font-size: 14px; line-height: 1.6; margin: 0 0 12px; }
#modal-box .about-pitch b { color: var(--gold); }
#modal-box .about-screenshots {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  gap: 10px; margin-top: 6px;
}
/* Real gameplay screenshots (img/*.jpg) - the first and only external
   image assets this procedurally-rendered game has ever needed, added
   once the user supplied real captures to replace the original
   placeholder boxes (see project history for that earlier stopgap).
   object-fit:cover so all three fill the same tidy 16:9 frame
   regardless of the source screenshot's exact aspect ratio, rather than
   stretching or letterboxing. */
#modal-box .about-shot {
  width: 100%; aspect-ratio: 16 / 9; object-fit: cover;
  border: 1px solid var(--panel-border); border-radius: 3px;
  display: block;
}
#modal-box .quiz-q { font-size: 16px; margin: 10px 0 16px; line-height: 1.5; }
#modal-box .quiz-choice {
  display: block; width: 100%; text-align: left;
  padding: 10px 14px; margin: 6px 0;
  background: #1a1a2a; border: 1px solid var(--panel-border); border-radius: 2px;
  color: var(--text); font-size: 14px; cursor: pointer;
}
#modal-box .quiz-choice:hover { border-color: var(--cyan); }
#modal-box .quiz-result.correct { color: var(--green); }
#modal-box .quiz-result.incorrect { color: var(--red); }
/* User request: "take this to the next exponential level of UI UX
   awesomeness" - picking an answer used to jump STRAIGHT to a whole
   separate result screen with zero feedback on the choice you actually
   clicked - no confirmation of what you picked, no visible "yes/no" beat,
   just an abrupt swap. This is the classic quiz-game pattern instead
   (Kahoot/Duolingo/etc.): the clicked answer flashes green or red in
   place FIRST, the real correct answer lights up green too if you missed
   it, then the modal transitions to the full explanation screen a beat
   later - see pickQuizChoice, ui.js. transition:all so the color/border
   change animates rather than snapping instantly. */
#modal-box .quiz-choice.locked { pointer-events: none; transition: background 0.18s ease, border-color 0.18s ease, color 0.18s ease; }
#modal-box .quiz-choice.locked:not(.picked-correct):not(.picked-incorrect):not(.reveal-correct) { opacity: 0.4; }
#modal-box .quiz-choice.picked-correct, #modal-box .quiz-choice.reveal-correct {
  background: rgba(70,200,110,0.18); border-color: var(--green); color: var(--green);
}
#modal-box .quiz-choice.picked-incorrect {
  background: rgba(220,70,70,0.18); border-color: var(--red); color: var(--red);
}
#modal-box .explanation { color: var(--dim); font-size: 13px; margin-top: 10px; line-height: 1.5; }
#modal-box .explanation-label { color: var(--cyan); font-weight: 700; font-style: normal; }
#modal-box .quiz-learn-more { margin-top: 12px; font-size: 12.5px; }
#modal-box .quiz-learn-more a { color: var(--cyan); text-decoration: none; }
#modal-box .quiz-learn-more a:hover { text-decoration: underline; }

/* ---------------- Quiz question diagrams ----------------
   Inline SVG (question.diagram in quizbank.js) rendered between the question
   text and the answer choices - hotkey badges, viewport/panel layouts, the
   modifier stack, UV unwrap/island layouts, shader node graphs, armature/
   weight-paint and keyframe timelines, sculpt brushes, light types, render/
   export pipelines, and Final Critique symptom->cause diagrams, one genuinely
   matched to each of the 163 questions in QUIZ_BANK (100% coverage). SVGs are authored with
   unitless coordinates in a fixed viewBox and no hardcoded colors other than
   var(...) references, so they scale responsively and automatically pick up
   the same theme palette as the rest of the modal. */
#modal-box .quiz-diagram {
  display: flex;
  justify-content: center;
  margin: 4px 0 14px;
  padding: 12px;
  background: rgba(255,255,255,0.03);
  border: 1px solid var(--panel-border);
  border-radius: 3px;
}
#modal-box .quiz-diagram svg { width: 100%; max-width: 320px; height: auto; display: block; }

/* ---------------- Pop Quiz HUD dressing ----------------
   The quiz-modal class is toggled on #modal-box only for quiz/exam screens (see
   ui.js openModal()/renderQuizQuestion()/renderQuizResult()), giving those specific
   screens a distinct "exam terminal" look - a robotic scanning-corner-bracket
   motif and a monospace tier badge - distinct from ordinary shop/menu modals. */
#modal-box.quiz-modal {
  border-color: var(--cyan);
  position: relative;
}
#modal-box.quiz-modal::before, #modal-box.quiz-modal::after {
  content: ''; position: absolute; width: 18px; height: 18px;
  border: 2px solid var(--cyan); pointer-events: none; opacity: 0.85;
}
#modal-box.quiz-modal::before { top: -1px; left: -1px; border-right: none; border-bottom: none; }
#modal-box.quiz-modal::after { bottom: -1px; right: -1px; border-left: none; border-top: none; }
#modal-box .quiz-tier-badge {
  font-family: 'Consolas', 'Courier New', monospace;
  font-size: 11px; letter-spacing: 0.8px; text-transform: uppercase;
  color: var(--cyan); margin-bottom: 8px;
  animation: quiz-badge-pulse 2.4s ease-in-out infinite;
}
@keyframes quiz-badge-pulse {
  0%, 100% { opacity: 0.65; }
  50% { opacity: 1; }
}
#modal-box table { width: 100%; border-collapse: collapse; font-size: 13px; }
#modal-box td, #modal-box th { padding: 4px 6px; text-align: left; border-bottom: 1px solid #26263a; }
#modal-box .bestiary-list { font-size: 13px; }
#modal-box .bestiary-list > .unknown { padding: 2px 0; }
/* A discovered entry now carries a real stat line and, where it applies,
   a plain-English explanation of its special attack (see
   BESTIARY_SPECIAL_TEXT, ui.js) - single-column with a bit of breathing
   room reads far better for that than the old bare-name 2-column list
   ever could once entries stopped being one line each. */
#modal-box .bestiary-entry { padding: 6px 0; border-bottom: 1px solid #26263a; }
#modal-box .bestiary-entry:last-child { border-bottom: none; }
#modal-box .bestiary-detail { color: var(--dim); font-size: 11.5px; margin-top: 2px; }
#modal-box .bestiary-tag {
  color: var(--gold); font-size: 10px; border: 1px solid var(--panel-border);
  border-radius: 2px; padding: 1px 5px; margin-left: 6px;
}

/* The Syllabus (openSyllabus, ui.js) - one entry per topic pool, each
   with a level badge, a one-line course description, and its Student
   Learning Outcomes as a real bulleted list (no <ul>/<li> styling
   existed anywhere else in this file before this screen needed it). */
#modal-box .syllabus-entry {
  border: 1px solid var(--panel-border); border-radius: 3px;
  padding: 10px 14px; margin: 10px 0;
}
#modal-box .syllabus-entry h3 {
  margin: 0 0 4px; display: flex; align-items: center; justify-content: space-between; gap: 10px;
}
#modal-box .syllabus-level {
  color: var(--gold); font-size: 10.5px; text-transform: none; letter-spacing: 0.3px;
  border: 1px solid var(--panel-border); border-radius: 2px; padding: 2px 7px; flex: none;
}
#modal-box .syllabus-desc { color: var(--dim); font-size: 12.5px; margin: 0 0 8px; font-style: italic; }
#modal-box .syllabus-outcomes { margin: 0; padding-left: 18px; }
#modal-box .syllabus-outcomes li { font-size: 13px; padding: 3px 0; line-height: 1.4; }
#modal-box .syllabus-link { margin-top: 8px; font-size: 12.5px; }
#modal-box .syllabus-link a { color: var(--cyan); text-decoration: none; }
#modal-box .syllabus-link a:hover { text-decoration: underline; }
#modal-box .syllabus-video-channel { margin: 0 0 14px; padding-bottom: 10px; font-size: 13px; }
#modal-box .syllabus-download-cta { margin: 0 0 14px; padding-bottom: 10px; border-bottom: 1px solid var(--panel-border); font-size: 13px; }
#modal-box .syllabus-download-cta a { color: var(--gold); font-weight: bold; }
#modal-box .known { color: var(--magenta); }
#modal-box .unknown { color: var(--dim); }
#overview-canvas { background: #05050a; border: 1px solid var(--panel-border); border-radius: 2px; }
#fortress-canvas { background: #05050a; border: 1px solid var(--panel-border); border-radius: 2px; display: block; margin: 10px 0; }

/* ---------------- Boss health bar ---------------- */
#boss-bar {
  position: fixed; top: 14px; left: 50%; transform: translateX(-50%);
  z-index: 8; pointer-events: none;
  width: min(560px, 70vw);
  text-align: center;
}
#boss-bar-name {
  font-weight: 700; color: var(--magenta); font-size: 15px; letter-spacing: 0.5px;
  text-shadow: 0 2px 6px rgba(0,0,0,0.9);
  margin-bottom: 4px;
}
.boss-hp-bar {
  height: 16px; border-color: var(--magenta);
  box-shadow: 0 0 16px rgba(214,51,214,0.5);
}
/* transition below is overridden to 'none' at runtime the first time
   animateBarFillSeries (ui.js) touches this element - width is JS-driven
   (the e-series snap easing) from then on, same as every other bar-fill. */
.boss-hp-bar .bar-fill { background: linear-gradient(90deg, #6b1d6b, var(--magenta)); transition: width 0.4s ease; }

/* ---------------- Combo counter ---------------- */
#combo-display {
  position: fixed; top: 90px; right: 24px; z-index: 8; pointer-events: none;
  text-align: right;
  transition: transform 0.15s ease;
}
#combo-count {
  display: block; font-size: 42px; font-weight: 800; color: var(--gold);
  text-shadow: 0 0 18px rgba(230,199,0,0.7), 0 2px 4px rgba(0,0,0,0.9);
  line-height: 1;
}
.combo-label { font-size: 11px; color: var(--dim); letter-spacing: 2px; }
#combo-display.pulse { animation: combo-pulse 0.25s ease; }
@keyframes combo-pulse { 0% { transform: scale(1.35); } 100% { transform: scale(1); } }

/* ---------------- Chapter title card ---------------- */
#chapter-card {
  position: fixed; inset: 0; z-index: 15; pointer-events: none;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  background: rgba(0,0,0,0);
  opacity: 0;
}
#chapter-card.show { animation: chapter-card-anim 2.6s ease forwards; }
#chapter-card-eyebrow {
  color: var(--cyan); letter-spacing: 4px; font-size: 13px; text-transform: uppercase;
  margin-bottom: 8px; opacity: 0.9;
}
#chapter-card-title {
  color: var(--gold); font-size: 34px; font-weight: 700; text-align: center;
  padding: 0 24px; text-shadow: 0 4px 20px rgba(0,0,0,0.9);
  max-width: 90vw;
}
@keyframes chapter-card-anim {
  0% { opacity: 0; background: rgba(0,0,0,0); }
  12% { opacity: 1; background: rgba(4,4,8,0.55); }
  75% { opacity: 1; background: rgba(4,4,8,0.55); }
  100% { opacity: 0; background: rgba(0,0,0,0); }
}

/* ---------------- Institute siege battle banner ---------------- */
/* Same "invisible via opacity, never via display:none" discipline as
   #chapter-card above (see the HTML comment there) - non-blocking, doesn't
   pause the game, just a few seconds of watching the numbers actually
   fight it out before the overlay clears itself. */
#siege-banner {
  position: fixed; inset: 0; z-index: 16; pointer-events: none;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 14px; background: rgba(0,0,0,0); opacity: 0;
}
#siege-banner.show { animation: siege-banner-anim 5.5s ease forwards; }
#siege-banner-title {
  color: var(--magenta); font-size: 26px; font-weight: 700; letter-spacing: 2px;
  text-transform: uppercase; text-shadow: 0 4px 20px rgba(0,0,0,0.9);
}
#siege-fortress-canvas {
  background: #05050a; border: 1px solid var(--panel-border); border-radius: 2px;
  box-shadow: 0 8px 28px rgba(0,0,0,0.6);
}
#siege-bars { display: flex; flex-direction: column; gap: 10px; width: min(80vw, 420px); }
.siege-row { display: flex; align-items: center; gap: 10px; }
.siege-label { width: 110px; font-size: 13px; color: var(--dim); text-align: right; flex: none; }
.siege-bar { height: 16px; }
.siege-row:first-child .bar-fill { background: linear-gradient(90deg, #1d5c3a, var(--green)); }
.siege-row:last-child .bar-fill { background: linear-gradient(90deg, #6b1d1d, var(--red)); }
.siege-value { width: 34px; flex: none; font-weight: 700; color: var(--text); }
#siege-result {
  font-size: 20px; font-weight: 700; text-shadow: 0 3px 14px rgba(0,0,0,0.9);
  opacity: 0; transform: translateY(6px); transition: opacity 0.4s ease, transform 0.4s ease;
}
#siege-result.show { opacity: 1; transform: translateY(0); }
#siege-result.won { color: var(--green); }
#siege-result.lost { color: var(--red); }
@keyframes siege-banner-anim {
  0% { opacity: 0; background: rgba(0,0,0,0); }
  8% { opacity: 1; background: rgba(4,4,8,0.6); }
  88% { opacity: 1; background: rgba(4,4,8,0.6); }
  100% { opacity: 0; background: rgba(0,0,0,0); }
}

/* ---------------- Toast notifications ---------------- */
#toast-container {
  position: fixed; top: 16px; left: 50%; transform: translateX(-50%);
  z-index: 25; pointer-events: none;
  display: flex; flex-direction: column; align-items: center; gap: 8px;
}
.toast {
  background: rgba(14,14,22,0.94); border: 1px solid var(--panel-border);
  border-left: 4px solid var(--cyan);
  border-radius: 2px; padding: 10px 18px;
  font-size: 13.5px; color: var(--text);
  box-shadow: 0 8px 24px rgba(0,0,0,0.5);
  animation: toast-in 0.25s ease, toast-out 0.4s ease 2.6s forwards;
  max-width: 420px;
}
.toast.achievement { border-left-color: var(--gold); }
.toast b { color: var(--gold); }
.toast.boss { border-left-color: var(--red); }
@keyframes toast-in { from { opacity: 0; transform: translateY(-12px); } to { opacity: 1; transform: translateY(0); } }
@keyframes toast-out { to { opacity: 0; transform: translateY(-8px); } }

/* Contextual "just-in-time" hints (UI.showHint, main.js) - the follow-up
   layer to the new-character intro tips: instead of ONLY dumping
   everything upfront, a few short, one-time explanations fire the first
   time something actually relevant happens (first hit landed, first item
   found, HP running low) - a well-established onboarding pattern
   (progressive disclosure) that sticks far better than a wall of text
   read before the player has any context for it. Reuses #toast-container/
   .toast's shell but needs real interactivity (a dismiss button) and a
   much longer resting time than a routine 3.2s toast, since there's
   actual explanation to read here, not just a one-line confirmation. */
.toast.hint {
  pointer-events: auto;
  border-left-color: var(--gold);
  max-width: 460px;
  padding: 12px 16px;
  animation: toast-in 0.25s ease, toast-out 0.4s ease 9.6s forwards;
}
.toast-hint-title { color: var(--gold); font-weight: 700; font-size: 13px; margin-bottom: 4px; }
.toast-hint-close {
  float: right;
  background: none; border: none; color: var(--dim);
  font-size: 15px; line-height: 1; cursor: pointer;
  padding: 0 0 0 10px;
}
.toast-hint-close:hover { color: var(--text); }

/* ---------------- Minimap ---------------- */
#minimap-container {
  position: fixed; top: 96px; left: 16px; z-index: 7;
  pointer-events: none;
  background: rgba(10,10,18,0.65);
  border: 1px solid var(--panel-border);
  border-radius: 50%;
  padding: 4px;
}
#minimap-canvas { display: block; border-radius: 50%; }

/* ---------------- Ability action bar ---------------- */
/* Anchored to the bottom-RIGHT (was horizontally centered via left:50%) -
   #hud-controls (the always-visible hotkey panel, style.css above) is
   also bottom-anchored and used to be centered too, so the two competed
   for the same screen-center real estate and this bar's icons (J/P/X)
   ended up sitting directly on top of the controls panel's text. Now
   the controls panel hugs the left and this bar hugs the right, with
   the 3D game world visible between them instead of both fighting over
   the middle. */
/* #action-bar-row owns the fixed anchor #action-bar used to hold
   directly - see index.html's own comment on why #btn-toggle-windows
   has to be a sibling of #action-bar, not a child of it. */
#action-bar-row {
  position: fixed; right: 24px; bottom: 36px;
  /* Was 8, then 10 (below the touch joystick zone's 9) - see the BUGFIX
     comment near #touch-joystick-zone's own rule for why a real,
     clickable control must always out-rank an invisible drag-capture
     region it happens to overlap. Bumped again (user request: "ensure
     that the netlify add doesn't block any icon functionality") as a
     defensive measure against a "Powered by Netlify" pill seen floating
     over these exact icons in real screenshots - confirmed by a full
     codebase search that this game creates no such element itself, so it
     must be coming from Netlify's own hosting layer (an opt-in site
     badge or a preview/dev toolbar), entirely outside this page's DOM
     and therefore outside anything a stylesheet here can reach or
     override. This bump can only help if it DOES turn out to share this
     page's own stacking context; kept deliberately modest (10->13, not
     higher) so it stays below #chapter-card/#siege-banner's z-index 15/16
     full-screen dim-outs (those cinematic transitions are SUPPOSED to
     visually cover these icons - jumping above them would have broken
     that) and further below #modal-overlay's 30, so a genuinely open
     modal still covers these icons exactly as it always has. The real,
     guaranteed fix is on the Netlify side: Site settings -> General ->
     "Netlify badge" -> off. */
  z-index: 13; display: flex; align-items: flex-end; gap: 8px; pointer-events: none;
}
#action-bar { display: flex; gap: 8px; pointer-events: none; }
.action-slot {
  pointer-events: auto;
  position: relative;
  width: 46px; height: 46px;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  background: linear-gradient(to bottom, rgba(28,28,44,0.85), rgba(16,16,24,0.85));
  backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);
  border: 1px solid var(--panel-border); border-radius: 3px;
  cursor: pointer; user-select: none;
  box-shadow: 0 2px 8px rgba(0,0,0,0.5);
  transition: border-color 0.15s ease, transform 0.1s ease;
}
.action-slot:hover:not(.disabled) { border-color: var(--gold); transform: translateY(-2px); }
.action-slot:active:not(.disabled) { transform: translateY(0); }
.action-slot .slot-icon { font-size: 18px; line-height: 1; z-index: 1; }
.action-slot .slot-key {
  position: absolute; top: 2px; left: 4px; font-size: 9px; font-weight: 700; color: var(--dim); z-index: 1;
}
.action-slot .slot-count {
  position: absolute; bottom: 2px; right: 4px; font-size: 10px; font-weight: 700; color: var(--gold);
  text-shadow: 0 1px 2px rgba(0,0,0,0.9); z-index: 1;
}
.action-slot.disabled { opacity: 0.4; cursor: default; filter: grayscale(0.6); }
.action-slot.disabled:hover { border-color: var(--panel-border); transform: none; }
/* Low-ammo warning (ui.js's updateRangedWeaponSlot) - reuses the same
   low-fuel-pulse keyframe already defined for #hud-torch.low-fuel, since
   both are the same "resource running low, not yet empty" warning shape. */
.action-slot.low-ammo:not(.disabled) { border-color: var(--red); animation: low-fuel-pulse 1.4s ease-in-out infinite; }
.action-slot.low-ammo:not(.disabled) .slot-count { color: var(--red); }
/* #btn-toggle-windows is a real <button> reusing .action-slot's look
   (every OTHER action-slot is a plain <div>) - resets the handful of
   default button chrome .action-slot doesn't already override itself. */
#btn-toggle-windows { padding: 0; margin: 0; font: inherit; background-color: transparent; }
#btn-toggle-windows:hover { border-color: var(--cyan); transform: translateY(-2px); }

/* Radial charge ring: a conic-gradient sweep (--frac, 0-100, set from
   ui.js) masked down to just a ring via a radial-gradient mask - the
   standard CSS "donut from a conic-gradient" technique. Sits behind the
   icon/key/count (they're pinned to z-index:1 above) so it reads as a
   cooldown/charge dial framing the ability rather than a separate
   element. Absent entirely for the unlimited Search action (see ui.js -
   there's no meaningful "charge fraction" for an action with no resource
   cost, so no ring element is even created for it). */
.slot-ring {
  position: absolute; inset: 3px; border-radius: 50%;
  background: conic-gradient(var(--gold) calc(var(--frac, 100) * 1%), rgba(255,255,255,0.12) 0);
  -webkit-mask: radial-gradient(farthest-side, transparent calc(100% - 3px), #000 calc(100% - 3px));
  mask: radial-gradient(farthest-side, transparent calc(100% - 3px), #000 calc(100% - 3px));
  transition: background 0.3s ease;
}
.action-slot.disabled .slot-ring { opacity: 0.5; }

/* ---------------- HUD icon buttons ---------------- */
.hud-icon-btn {
  pointer-events: auto;
  background: transparent; border: 1px solid var(--panel-border); border-radius: 2px;
  color: var(--text); font-size: 13px; padding: 2px 8px; cursor: pointer;
  margin-left: auto;
}
.hud-icon-btn:hover { border-color: var(--cyan); color: var(--cyan); }
#achv-count { color: var(--gold); }

/* ---------------- Custom tooltips ----------------
   Replaces the browser's native `title` tooltip (slow to appear, plain
   gray box, inconsistent across browsers) on the HUD icon buttons and
   stat chips with an on-brand styled one - pure CSS (content from the
   `data-tooltip` attribute, shown via a ::after pseudo-element on
   :hover), so there's no JS positioning logic that could get an edge
   case wrong. Anchored below-and-right-aligned rather than centered
   above: every element this is used on lives in the top-right cluster
   of the HUD (or a modal's own top-right corner), where a centered-
   above tooltip would frequently clip off the top or right edge of the
   viewport - right-aligning below is safe regardless of viewport width
   for everything currently using it. `aria-label` (already set
   alongside `data-tooltip` wherever this is used) keeps the accessible
   name for screen readers without triggering a second, redundant
   native tooltip on top of this one. */
[data-tooltip] { position: relative; }
[data-tooltip]::after {
  content: attr(data-tooltip);
  position: absolute; top: 100%; right: 0;
  margin-top: 6px;
  background: rgba(14,14,22,0.97);
  border: 1px solid var(--panel-border); border-radius: 2px;
  padding: 5px 9px;
  font-size: 11.5px; font-weight: 400; color: var(--text); text-transform: none; letter-spacing: normal;
  white-space: nowrap; text-align: left;
  opacity: 0; transform: translateY(-4px);
  pointer-events: none;
  transition: opacity 0.15s ease, transform 0.15s ease;
  transition-delay: 0.05s;
  box-shadow: 0 6px 18px rgba(0,0,0,0.5);
  z-index: 20;
}
[data-tooltip]:hover::after { opacity: 1; transform: translateY(0); }

/* ---------------- Quick-access menu dropdown ----------------
   Previously the ONLY way to reach Inventory/Character Sheet/Shop/
   Bestiary/Overview Map/Fast Travel/The Keep was to already know
   their individual keyboard shortcut - this single "hamburger" icon
   surfaces all seven in one discoverable, clickable place instead of
   requiring every player to memorize (or go read) a dense line of
   letters, while adding only ONE new icon to the HUD rather than seven. */
.hud-icon-wrap { position: relative; display: flex; }
#quick-menu {
  position: absolute; top: calc(100% + 8px); right: 0;
  background: rgba(14,14,22,0.97);
  border: 1px solid var(--panel-border); border-radius: 3px;
  padding: 6px; min-width: 230px;
  box-shadow: 0 10px 32px rgba(0,0,0,0.55);
  display: flex; flex-direction: column; gap: 2px;
  z-index: 12;
  animation: quick-menu-in 0.14s ease-out;
  /* User request: "all windows and popups must have full scrolling
     capabilities to select all functionality" - this menu has grown to
     13 entries with no cap at all, tall enough that on a short mobile
     landscape screen it could run off the bottom of the viewport with
     no way to ever reach the last few items (Quit to Title included).
     Capped and made independently scrollable so every entry stays
     reachable regardless of screen height. */
  /* dvh (dynamic viewport height) after the vh fallback - user request:
     "make it work for the size of the iPhone 13 screen." iOS Safari's
     address bar/toolbar can show or hide as you scroll/interact, and
     100vh has historically measured the LARGEST possible viewport (bar
     hidden) even while the bar is actually showing, letting a vh-based
     max-height claim more room than is really visible and get clipped
     under the toolbar. 100dvh tracks the CURRENTLY visible viewport
     instead - declared second so it overrides the vh line only in
     browsers that understand dvh, leaving the vh line as a harmless
     fallback everywhere else. */
  max-height: calc(100vh - 60px);
  max-height: calc(100dvh - 60px);
  overflow-y: auto;
}
/* MOBILE-ONLY (scoped to .touch-mode, the explicit Mobile UI layout
   choice - desktop's own #quick-menu rule above is completely untouched
   by anything below). User report, with screenshots: "the main
   environment is blocked and obscured with window popups" - on a real
   phone-width viewport, the desktop dropdown above (anchored top-right,
   min-width 230px, no cap on how tall it can grow) had enough room to
   grow both wide AND tall enough to visually collide with the minimap
   (top-left) and the combat log (also top-left, no z-index of its own)
   at once - the screenshots showed log text and menu labels literally
   overlapping/interleaved, illegible, with the menu's own bottom entries
   pushed out past the visible viewport. A small anchored dropdown simply
   doesn't have room to coexist with the rest of the HUD on a phone the
   way it does on a spacious desktop window.
   Fix: on mobile only, the menu stops being an anchored dropdown and
   becomes its own centered, capped-width, fully opaque panel - the exact
   same "you're in a menu now, here's a clear focused list" pattern the
   game's other screens (Shop/Inventory/Achievements, all going through
   #modal-box) already use successfully, just without touching the shared
   #modal-box system itself (this stays a pure CSS repositioning of the
   existing #quick-menu element - no JS/DOM changes, so desktop's
   click-outside-to-close and action-wiring logic in main.js is completely
   unaffected). The oversized box-shadow is the standard CSS-only
   "infinite backdrop" trick - a huge dark spread that reads as a dimmed
   background over the game world without needing a second DOM element. */
.touch-mode #quick-menu {
  position: fixed;
  top: 50%; left: 50%; right: auto;
  transform: translate(-50%, -50%);
  width: min(88vw, 340px);
  /* Same vh-then-dvh fallback pattern as the base #quick-menu rule above
     (a bare min()/max() mixing vh and dvh would invalidate the WHOLE
     declaration on a browser that doesn't recognize dvh, unlike two
     separate declarations where the unsupported second line is simply
     skipped). */
  max-height: 78vh;
  max-height: 78dvh;
  background: rgba(12,12,20,0.99);
  box-shadow: 0 0 0 9999px rgba(2,2,6,0.62), 0 10px 32px rgba(0,0,0,0.55);
  z-index: 50;
  /* The base #quick-menu rule's own animation (quick-menu-in) sets
     transform: translateY(...) at each keyframe, which would REPLACE
     (not add to) the translate(-50%,-50%) centering above, snapping the
     menu off-center the instant the animation ran. This more-specific
     rule overrides `animation` with a centering-aware keyframe instead. */
  animation: quick-menu-in-centered 0.14s ease-out;
}
@keyframes quick-menu-in-centered {
  from { opacity: 0; transform: translate(-50%, calc(-50% - 4px)); }
  to { opacity: 1; transform: translate(-50%, -50%); }
}
@keyframes quick-menu-in {
  from { opacity: 0; transform: translateY(-4px); }
  to { opacity: 1; transform: translateY(0); }
}
.quick-menu-item {
  pointer-events: auto;
  display: flex; align-items: center; gap: 9px;
  background: transparent; border: none; border-radius: 2px;
  color: var(--text); font-family: inherit; font-size: 13px;
  padding: 8px 10px; cursor: pointer; text-align: left; width: 100%;
}
.quick-menu-item:hover { background: rgba(79,214,255,0.12); color: var(--cyan); }
.qm-icon { font-size: 15px; width: 18px; text-align: center; flex: none; }
.qm-label { flex: 1; }
.qm-key {
  color: var(--dim); font-size: 10.5px; font-weight: 700; flex: none;
  border: 1px solid var(--panel-border); border-radius: 3px; padding: 1px 5px;
}

/* ---------------- Game-over achievement summary ---------------- */
.gameover-achv-list {
  columns: 2; font-size: 13px; color: var(--magenta);
  max-width: 480px; margin: 4px auto 0;
}
.gameover-achv-list div { break-inside: avoid; padding: 2px 0; text-align: left; }

/* ---------------- Settings modal ---------------- */
#modal-box .settings-row {
  display: flex; align-items: center; justify-content: space-between;
  gap: 16px; padding: 10px 4px; border-bottom: 1px solid #26263a;
}
#modal-box .settings-row label { font-size: 14px; color: var(--text); }
#modal-box .settings-row input[type="range"] { width: 160px; accent-color: var(--cyan); }
#modal-box .settings-row input[type="checkbox"] { width: 18px; height: 18px; accent-color: var(--cyan); }
#modal-box .settings-row select {
  background: #241c3d; color: var(--text); border: 1px solid #35355a;
  border-radius: 2px; padding: 5px 8px; font-size: 13px; font-family: inherit;
}
/* Tab-key navigation between these fields only just started working (see
   main.js's keydown handler - Tab used to get preventDefault()'d like
   every other key while a modal was open, silently blocking native
   focus-cycling) - an on-theme focus ring makes that now-real navigation
   actually visible, rather than relying on each browser's differently-
   styled default outline. :focus-visible (not plain :focus) specifically
   so a mouse click doesn't draw the same ring a keyboard Tab does. */
#modal-box .settings-row input:focus-visible,
#modal-box .settings-row select:focus-visible {
  outline: 2px solid var(--cyan);
  outline-offset: 2px;
}

/* ---------------- Achievements modal extras ---------------- */
/* Fibonacci spiral layout - see openAchievements (ui.js) for the actual
   placement math (sqrt-radius + a 45-degree/F(6) angle step, traced by
   the .achv-spiral-guide SVG line beneath the badges). Container is sized
   to the fixed 300x300 coordinate space that math lays badges out in. */
#modal-box .achv-spiral { position: relative; width: 300px; height: 300px; margin: 10px auto; }
#modal-box .achv-spiral-guide { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; }
#modal-box .achv-node {
  position: absolute; transform: translate(-50%, -50%);
  width: 34px; height: 34px; display: flex; align-items: center; justify-content: center;
  cursor: default; z-index: 1;
}
/* Each badge sits on its own dark circular plate (not a bare floating
   emoji) so the spiral reads as a cohesive string of nodes rather than
   scattered icons - unlocked ones get a warm gold ring + glow, matching
   this game's existing gold-accent-for-achievement language elsewhere. */
#modal-box .achv-node .achv-plate {
  width: 100%; height: 100%; border-radius: 50%; display: flex; align-items: center; justify-content: center;
  background: radial-gradient(circle at 35% 30%, #232330, #121218);
  border: 1.5px solid var(--gold); box-shadow: 0 0 10px rgba(255, 213, 74, 0.45), inset 0 0 6px rgba(0,0,0,0.6);
}
#modal-box .achv-node.locked .achv-plate {
  border-color: var(--panel-border); box-shadow: inset 0 0 6px rgba(0,0,0,0.6);
}
#modal-box .achv-node .achv-icon { font-size: 16px; }
#modal-box .achv-node.locked .achv-icon { opacity: 0.45; }
/* Name/desc only ever show as a hover tooltip - there's no room to print
   them at spiral scale with up to a few dozen badges packed in. */
#modal-box .achv-node .achv-tip {
  position: absolute; bottom: 130%; left: 50%; transform: translateX(-50%);
  background: #0d0d14; border: 1px solid var(--panel-border); border-radius: 4px;
  padding: 6px 9px; width: max-content; max-width: 180px; font-size: 11.5px;
  line-height: 1.35; color: var(--text); pointer-events: none;
  opacity: 0; visibility: hidden; transition: opacity 0.15s ease;
  z-index: 5;
}
#modal-box .achv-node:hover .achv-tip { opacity: 1; visibility: visible; }
#modal-box .achv-node .achv-name { font-weight: 700; color: var(--gold); }
#modal-box .achv-node.locked .achv-name { color: var(--dim); }
#modal-box .achv-node .achv-desc { color: var(--dim); }

/* ---------------- Progress Report modal ---------------- */
#modal-box .progress-row { display: flex; align-items: center; gap: 10px; margin: 7px 0; }
#modal-box .progress-label { flex: 0 0 38%; font-size: 12px; color: var(--text); line-height: 1.3; }
#modal-box .progress-bar-track {
  flex: 1;
  height: 14px;
  background: rgba(255,255,255,0.06);
  border: 1px solid var(--panel-border);
  border-radius: 3px;
  overflow: hidden;
}
#modal-box .progress-bar-fill { height: 100%; transition: width 0.3s ease; }
#modal-box .progress-pct { flex: 0 0 110px; font-size: 11px; color: var(--dim); text-align: right; }

/* ---------------- Certificate of Mastery modal ---------------- */
/* A real diploma treatment (double gold border, seal, serif display type)
   deliberately unlike every other dark/monospace-flavored panel in this
   game - this is a keepsake/reward screen, not a data readout, so it earns
   its own distinct visual register rather than reusing .progress-row's
   utilitarian bars. */
#modal-box .certificate {
  background: linear-gradient(155deg, #fdf8ec, #f3e9d2);
  color: #2a2013;
  border-radius: 4px;
  padding: 6px;
  margin: 4px 0 0;
}
#modal-box .certificate-border {
  border: 3px solid var(--gold);
  outline: 1px solid var(--gold);
  outline-offset: -8px;
  border-radius: 3px;
  padding: 26px 28px 20px;
  text-align: center;
}
#modal-box .certificate-seal { font-size: 40px; line-height: 1; margin-bottom: 4px; }
#modal-box .certificate-kicker { font-size: 11px; letter-spacing: 3px; text-transform: uppercase; color: #8a6d1f; }
#modal-box .certificate-title { font-family: Georgia, 'Times New Roman', serif; color: #6b4e12; font-size: 26px; margin: 4px 0 2px; }
#modal-box .certificate-sub { font-size: 12px; letter-spacing: 1px; text-transform: uppercase; color: #8a6d1f; margin-bottom: 14px; }
#modal-box .certificate-body { font-size: 13px; line-height: 1.6; margin: 6px auto; max-width: 480px; }
#modal-box .certificate-name { font-family: Georgia, 'Times New Roman', serif; font-size: 20px; color: #2a2013; margin: 10px 0; padding-bottom: 8px; border-bottom: 1px solid #c9b585; display: inline-block; }
#modal-box .certificate-date { font-size: 11px; color: #8a6d1f; margin-top: 14px; letter-spacing: 0.5px; }

/* ---------------- Best-run stats on title ---------------- */
#best-stats {
  margin: 4px 0 18px;
  font-size: 12.5px; color: var(--dim);
  display: flex; gap: 18px; flex-wrap: wrap; justify-content: center;
  max-width: 560px;
}
#best-stats b { color: var(--cyan); }

/* ---------------- Monster nameplates ---------------- */
#nameplate-container {
  position: fixed; inset: 0; z-index: 8; pointer-events: none;
  overflow: hidden;
}
.nameplate {
  /* Position + the -50%/-100% self-centering anchor are both set together
     from JS as one `transform` (see updateNameplates in ui.js) - GPU-
     compositable and, unlike left/top, doesn't force a layout recalc on
     every one-per-frame update. top/left stay at their 0 default and are
     never touched. */
  position: absolute; top: 0; left: 0;
  display: flex; flex-direction: column; align-items: center; gap: 2px;
  white-space: nowrap;
  will-change: transform;
}
.nameplate .np-name {
  font-size: 11px; font-weight: 700; color: var(--text);
  text-shadow: 0 1px 3px rgba(0,0,0,0.95);
}
.nameplate .np-bar {
  width: 64px; height: 5px; border-radius: 3px; overflow: hidden;
  background: rgba(10,10,18,0.75); border: 1px solid rgba(255,255,255,0.25);
}
.nameplate .np-bar-fill { height: 100%; transition: width 0.3s ease; }
.nameplate.elite .np-name { color: var(--gold); }
.nameplate.elite .np-bar-fill { background: var(--gold); }
.nameplate.legendary .np-name { color: #ff6b6b; }
.nameplate.legendary .np-bar-fill { background: #8b1a1a; }
.nameplate.boss .np-name { font-size: 13px; color: var(--magenta); }
.nameplate.boss .np-bar { width: 96px; height: 7px; }
.nameplate.boss .np-bar-fill { background: var(--magenta); }

/* ---------------- Off-screen threat indicators ---------------- */
#threat-indicator-container {
  position: fixed; inset: 0; z-index: 8; pointer-events: none; overflow: hidden;
}
.threat-arrow {
  position: absolute; top: 0; left: 0; width: 0; height: 0;
  border-left: 8px solid transparent; border-right: 8px solid transparent;
  border-bottom: 14px solid var(--dim);
  filter: drop-shadow(0 1px 3px rgba(0,0,0,0.85));
  will-change: transform;
  /* The triangle's apex sits at this element's local (0,0) - rotating
     around that same point (rather than the border-box center) keeps the
     tip pinned exactly at the anchored screen-edge position as it turns
     to track a moving off-screen threat. */
  transform-origin: 0 0;
}
.threat-arrow.elite { border-bottom-color: var(--gold); }
.threat-arrow.legendary { border-bottom-color: #ff6b6b; }
.threat-arrow.boss { border-bottom-color: var(--magenta); transform: scale(1.3); }
.threat-arrow.near { animation: threat-pulse 0.7s ease-in-out infinite; }
@keyframes threat-pulse {
  0%, 100% { opacity: 0.65; }
  50% { opacity: 1; }
}

/* =========================================================================
   Touch / mobile mode
   =========================================================================
   User request: "modify this program as a version 2.0 cell phone style
   application ... so that it works by touch screen only." main.js detects
   a coarse (touch-primary) pointer via matchMedia('(pointer: coarse)') at
   startup and toggles this class on <body> - everything below is either
   (a) new touch-only UI hidden on desktop, or (b) a touch-mode override of
   an existing desktop-first rule, never a replacement of the desktop
   layout itself, so mouse+keyboard play is completely unaffected. */

/* The virtual joystick zone - a large INVISIBLE touch-capture region
   (not the visible ring itself, see #touch-joystick-base below), hidden
   entirely on desktop (WASD covers movement there), shown only in
   touch-mode. Also replaces #hud-controls (the keyboard hotkey
   reference) in that same bottom-left screen real estate, since a list
   of KEYBOARD shortcuts is actively useless with no keyboard.
   Deliberately sized generously (45% width x 60% height) rather than
   tightly around the visible ring - the whole point of a FLOATING
   joystick is that the player's thumb can land anywhere in a big lenient
   area and the ring appears right there, instead of demanding a precise
   tap on a small fixed target. Sits above #scene-container in the DOM
   (see index.html) so every touch inside this zone is captured here
   first and never reaches the camera-drag listener underneath. */
#touch-joystick-zone {
  display: none;
  position: fixed;
  left: 0;
  bottom: 0;
  width: 45%;
  height: 60%;
  z-index: 9;
  touch-action: none;
}
/* BUGFIX (user: "menu icons and toggles and hotkeys don't seem to be
   working very well at all during mobile gameplay"): the zone above is
   deliberately huge (see its own comment) so a thumb can land anywhere
   nearby, but at z-index 9 it sat ABOVE #action-bar-row (z-index 8,
   below) in the stacking order - on a typical portrait phone the zone's
   65%-width footprint (see the portrait override below) geometrically
   covers most or all of where the ability/potion/search/"show all
   windows" icons sit bottom-right, so every tap in that overlap
   silently hit the invisible joystick zone FIRST and never reached the
   real button underneath, regardless of that button's own
   pointer-events:auto. Real, visible, individually-clickable controls
   must always win hit-testing over an invisible drag-capture region
   they happen to overlap - raising the action bar (and, defensively,
   the top HUD icon row, in case some future/odd viewport ever pushes it
   low enough to overlap too) above the joystick's z-index fixes this
   without touching the joystick's own size/shape/behavior at all: it
   still captures drags everywhere EXCEPT the small rectangles actual
   buttons occupy, which is exactly the correct behavior. */
/* z-index only affects a POSITIONED element - #hud-icon-buttons was
   static (no position set) so a bare z-index here would have silently
   done nothing at all. See #action-bar-row's own rule (below, in its
   original section) for the matching z-index bump - deliberately not
   duplicated as a separate rule here, since a LATER same-specificity
   rule always wins the cascade and this comment sits BEFORE that
   original rule in the file; editing z-index in place there is the only
   way to make it actually stick. */
#hud-icon-buttons { position: relative; z-index: 10; }
/* has-touch (NOT .touch-mode) gates the joystick's visibility - user
   request: "the desktop version moves very well on my phone, I like
   that version on mobile but I need to be able to move the character
   with touch." Touch CAPABILITY and the desktop/mobile UI LAYOUT choice
   are independent: a touch-only phone can validly prefer the denser
   desktop window/HUD layout while still needing SOME way to move the
   character, so the joystick shows whenever the device has touch input
   at all, regardless of which UI mode is currently active - see
   main.js's applyHasTouch. */
.has-touch #touch-joystick-zone { display: block; }
/* BUGFIX (user: "I need to be able to toggle open and closed every
   single window... work every icon/text function in the menu listing
   too"): this whole file has consistently drawn a line between
   .has-touch (real touch INPUT CAPABILITY, set once from
   matchMedia('pointer:coarse') regardless of chosen UI layout) and
   .touch-mode (the Desktop/Mobile UI LAYOUT preference, an explicit
   title-screen choice) ever since the joystick itself needed exactly
   that distinction (see its own comment below) - a touchscreen player
   who prefers the denser Desktop UI layout still has no keyboard and no
   mouse. That same distinction was correctly applied to the joystick's
   own visibility, but every OTHER touch-interaction rule in this file
   (tap-target sizing, touch-action on the 3D scene, tap-highlight/
   long-press-callout suppression, window close/collapse button sizing)
   was still gated on .touch-mode alone - meaning a player using Desktop
   UI mode on an actual touchscreen phone (a fully valid, even
   PREFERRED combination per this project's own history) got NONE of
   these accommodations: tiny 24px close/collapse buttons, no
   touch-action on the camera-drag surface (fighting the browser's own
   scroll/zoom gesture recognizer), and - likely the single most
   confusing symptom - a long press on any button just slightly too
   long could trigger the browser's native text-selection/callout menu
   instead of the tap, since nothing suppressed that outside Mobile UI
   mode. Every rule below that's genuinely about TOUCH INPUT QUALITY
   (not screen-width-driven layout density, which legitimately still
   belongs to .touch-mode) is re-gated on .has-touch instead. */
.has-touch #hud-controls { display: none; }
/* Portrait support - the 45%/60% split above was tuned assuming
   landscape's wide-short screen shape; in portrait, width is the
   constrained dimension and height is the generous one, so the
   proportions are swapped here to keep the zone a similarly comfortable
   ABSOLUTE thumb-reach size rather than becoming a tall, narrow sliver.
   BUGFIX (user report, confirmed by their own side-by-side comparison:
   "I am not able to turn around or turn at all in First-Person mode on
   Mobile" in portrait, but "I am able to navigate left and right in the
   landscape mode" on the very same device): #scene-container's own
   pointerdown/pointermove drag-to-look handler (main.js) sits UNDER this
   zone, which is a higher z-index (9) sibling covering the same screen
   region - any touch that lands inside the zone's bounds is captured as
   joystick/movement input and never reaches the look-rotation handler at
   all. Landscape's zone (45% width) apparently leaves enough of the
   screen clear for a look-drag to land outside it and work correctly;
   portrait's was a full 65% width, eating most of the lower-left HALF of
   the screen and taking look-turning down with it. Brought down to match
   landscape's already-working 45% width (height trimmed similarly, from
   42% to 36%, for the same reasoning) - still a generously large,
   lenient thumb target for the joystick itself (the whole reason this
   zone is oversized rather than a small fixed circle in the first
   place), just no longer covering so much of the screen that turning
   becomes unreachable. */
@media (orientation: portrait) {
  .has-touch #touch-joystick-zone { width: 45%; height: 36%; }
}
/* The visible ring - absolutely positioned WITHIN the zone above, its
   left/top set directly in JS to the exact touch-down point (clamped so
   it never renders partly off-screen), then faded in via the .active
   class. Invisible and non-interactive (pointer-events:none - the ZONE
   handles all the actual pointer tracking, not this decorative ring)
   until a touch actually starts. */
#touch-joystick-base {
  position: absolute;
  width: 116px;
  height: 116px;
  margin: -58px 0 0 -58px;
  border-radius: 50%;
  background: rgba(20,20,31,0.55);
  border: 2px solid var(--panel-border);
  backdrop-filter: blur(4px);
  -webkit-backdrop-filter: blur(4px);
  /* User request: "ensure that the navigation device is visible and
     unobtrusive in the mobile mode all the time" - previously 0 (fully
     invisible) until the first touch, which meant a first-time player had
     no idea a joystick even existed until they happened to touch the
     right part of the screen. Now shown at a low resting opacity at a
     fixed rest spot the instant mobile mode turns on (see main.js's
     positionRestJoystick), staying subtle/non-obstructive at rest and
     brightening to full opacity only while actually being steered. */
  opacity: 0.4;
  transition: opacity 0.12s ease;
  pointer-events: none;
}
#touch-joystick-base.active { opacity: 1; }
#touch-joystick-knob {
  position: absolute;
  top: 50%; left: 50%;
  width: 52px; height: 52px;
  margin: -26px 0 0 -26px;
  border-radius: 50%;
  background: linear-gradient(to bottom, rgba(79,214,255,0.55), rgba(79,214,255,0.25));
  border: 2px solid var(--cyan);
  /* Only the knob's OWN reposition (JS sets a fresh translate() on every
     pointermove) should be instant - this transition exists purely for
     the release snap-back to center, so dragging never feels laggy but
     letting go still reads as a deliberate spring-back rather than a cut. */
  transition: transform 0.12s ease-out;
  pointer-events: none;
}

/* Two HUD buttons with no other touch equivalent (the '1'/'2' quick
   hotkeys for camera view cycling and wireframe toggling) - every OTHER
   keyboard shortcut already has a button/action-bar-slot/quick-menu-item
   covering it; these two were the only remaining keyboard-only actions. */
.touch-only-btn { display: none; }
/* BUGFIX: was .touch-mode - the '1'/'2' camera-view/wireframe hotkeys
   have NO other touch equivalent, so a Desktop-UI-mode player on a
   touchscreen (no keyboard) had literally no way to trigger either,
   same class of bug as the rest of this section. */
.has-touch .touch-only-btn { display: flex; }

/* Larger tap targets for real touch input - the ~24-28px desktop
   hover-tuned HUD buttons are well under the ~44px minimum comfortable
   touch target size. Gated on .has-touch (touch CAPABILITY), not
   .touch-mode (the UI LAYOUT choice) - see the BUGFIX comment above
   #hud-controls for why: a touchscreen is a touchscreen regardless of
   which layout the player prefers. */
.has-touch .hud-icon-btn { padding: 8px 12px; font-size: 16px; }
.has-touch .action-slot { width: 58px; height: 58px; }
.has-touch .action-slot .slot-icon { font-size: 24px; }
.has-touch .quick-menu-item { padding: 12px 14px; }
.has-touch #btn-menu { padding: 8px 12px; font-size: 18px; }
/* User request: "ensure that all windows and screens and popups are
   optimized for excellent mobile and touch gameplay" / "all info and
   icons should be accessible and sized perfectly for mobile" - these
   were the remaining ~24-26px desktop-tuned controls still under the
   ~44px comfortable touch target size: the Formula Reference/Worksheet
   tab buttons, the calculator keypad, and the floating panels' close/
   collapse buttons (the close button already got this treatment for the
   main modal - #modal-close-btn - but not yet for the three smaller
   panels, which use their own separate buttons). */
.has-touch .ref-tab, .has-touch .ws-tab { padding: 12px 6px; font-size: 13px; }
/* BUGFIX (user report with a screenshot: "the demos are too cramped...
   the tabs are too large on the mobile device"). The .has-touch rule
   above was tuned for comfortable tapping in isolation, but the Command
   Reference panel has 14 tabs wrapping into ~5 rows (see
   #reference-sheets-tabs' flex-wrap) - at 12px vertical padding each,
   that's a LOT of cumulative height, and combined with this panel's own
   .touch-mode-shrunk resting height (66vh, see its own comment further
   up) it was eating most of the available space before the player ever
   reached a demo diagram. Scoped narrower than .has-touch (which also
   covers Desktop-UI-mode-on-touchscreen, where the panel ISN'T shrunk
   and this was never a problem) - only .touch-mode's own already-smaller
   panel needs its own smaller tabs to match. Still comfortably tappable,
   just no longer the majority of the panel's own height. */
.touch-mode .ref-tab { padding: 7px 4px; font-size: 10.5px; }
.has-touch .calc-btn { font-size: 16px; min-height: 40px; }
/* BUGFIX: was .touch-mode - these are the Unit Circle/Formula Reference/
   Worksheet panels' own close/collapse buttons, the exact controls the
   user reported as unable to "toggle open and closed" on mobile. In
   Desktop UI mode on a touchscreen these stayed at 24px, well under a
   comfortable tap target, AND (see the tap-highlight/callout fix
   further below) a touch held on them even slightly too long could
   trigger the browser's own text-selection callout instead of the tap. */
.has-touch .window-collapse-btn,
.has-touch #unit-circle-close-btn,
.has-touch #reference-sheets-close-btn,
.has-touch #worksheet-close-btn {
  width: 36px; height: 36px; font-size: 20px;
}
/* Both buttons grew from 24px to 36px above without moving - at the
   collapse button's original right:40px (sized for a 24px neighbor),
   the two would now overlap by 6px. Pushed out to keep a clear gap. */
.has-touch .window-collapse-btn { right: 52px; }
/* Native resize left ENABLED on touch (previously disabled here, on the
   reasoning that a ~15px grip is hard to grab with a fingertip) - user
   request: mobile should work "like the desktop app almost exactly...
   moveable, sizable... same functionality," so resize capability now
   matches desktop exactly rather than being a mobile-specific reduction,
   fiddly grip and all.

   ADDITIVE follow-up, not a reversal of that: the native CSS `resize`
   corner is a mouse-only browser affordance in practice on real mobile
   browsers (iOS Safari/Android Chrome don't offer a touch-drag
   equivalent for it, regardless of the property staying "enabled" here),
   so "same functionality" on an actual phone still meant no WORKING way
   to shrink an oversized window - the exact "blocked by windows and
   popups" complaint this fixes. .touch-resize-grip (index.html, one per
   floating window, wired in ui.js's makeWindowDraggable) is a second,
   custom-drawn resize handle that DOES work via touch (real
   pointerdown/pointermove, same mechanism the drag handle already uses),
   shown only in .has-touch alongside the still-present native corner -
   mouse users are completely unaffected. */
.touch-resize-grip {
  display: none;
  position: absolute; bottom: 4px; right: 4px; z-index: 2;
  width: 30px; height: 30px;
  align-items: center; justify-content: center;
  background: rgba(255,255,255,0.06);
  border: 1px solid var(--panel-border);
  border-radius: 2px;
  color: var(--dim);
  font-size: 15px; line-height: 1;
  cursor: nwse-resize;
  user-select: none;
  -webkit-user-select: none;
}
.has-touch .touch-resize-grip { display: flex; }
.window-collapsed .touch-resize-grip { display: none; }
.has-touch #action-bar-row {
  right: calc(24px + env(safe-area-inset-right));
  bottom: calc(36px + env(safe-area-inset-bottom));
  /* BUGFIX (user report with a screenshot: "the icon on the left bottom
     is cropped off on my older phone"). This row had no wrap handling at
     all, right-anchored with content growing left - it had 4 static
     icons (toggle-windows/toggle-log/toggle-hud/ranged-weapon) plus
     however many class-ability slots #action-bar adds (up to 3), each a
     .has-touch-sized 58px tap target plus gap: comfortably over 450px of
     total width needed, wider than almost any phone screen, guaranteeing
     the leftmost icon(s) get pushed off the left edge with no way to
     reach them. Same fix #hud-icon-buttons already uses for the
     identical overflow problem (see its own comment) - wrap onto a
     second line rather than clipping. wrap-REVERSE (not plain wrap)
     specifically because this row is BOTTOM-anchored: an overflow row
     needs to stack UPWARD into the game view, not downward off the
     bottom of the screen the way a top-anchored row's overflow correctly
     stacks down. */
  flex-wrap: wrap-reverse;
  justify-content: flex-end;
  max-width: calc(100vw - 24px - env(safe-area-inset-right) - env(safe-area-inset-left));
}
/* BUGFIX (same "icons don't seem to be working" report): #hud-top (the
   Menu/Mute/Settings/etc. icon row lives inside it, right-aligned) had
   NO safe-area treatment at all, unlike the action bar above - in
   landscape on a notched phone (an iPhone's notch/Dynamic Island sits to
   one side in landscape, not centered), the right-anchored icon cluster
   could sit partly under the notch/rounded corner: present in the DOM,
   visually there, but not reliably tappable that close to the physical
   edge. Padded on all three relevant sides (top too, for the rare case
   of a landscape rotation that puts the inset there instead). */
.has-touch #hud-top {
  padding-left: calc(22px + env(safe-area-inset-left));
  padding-right: calc(22px + env(safe-area-inset-right));
  padding-top: calc(12px + env(safe-area-inset-top));
}
/* #scene-container had no touch-action rule at all (every other
   touch-interactive surface in this file - the joystick zone, the
   worksheet canvas - already sets one). Without it, camera-drag and
   pinch-zoom on the scene fought the browser's own gesture recognizer
   (pan/scroll-intent detection, a delayed "is this a scroll?" hold-off)
   on top of our own pointermove math, reading as laggy/janky drag input
   on real touch hardware even though it worked fine when tested with a
   mouse. BUGFIX: was .touch-mode - this is real touch-hardware behavior
   that has nothing to do with which UI layout is chosen, exactly the
   bug class documented at the top of this section. Gated on .has-touch
   so desktop's cursor-drag feel (and text-selection elsewhere on the
   page, on genuinely non-touch hardware) is still untouched. */
.has-touch #scene-container { touch-action: none; }

/* User request (final word on this whole mobile-layout arc): "I want the
   mobile app to work like the desktop app almost exactly, except I want
   the character to be controllable by touching the screen... all icons
   and menus should be located the same way." This deliberately UNDOES
   most of the mobile-specific layout divergence built up over several
   earlier follow-ups this same session (a compact/shrunk stats banner,
   a collapse-to-nothing toggle for it, a scaled-down minimap, a capped/
   shrunk message log, and Settings/Menu pinned as floating icons escaping
   the normal HUD row) - all of that made touch-mode look and behave
   noticeably DIFFERENT from desktop, which is the opposite of what's
   wanted now. The stats banner, minimap, and message log are back to
   their plain shared (non-touch-mode-specific) sizing/position, same as
   desktop - the Quick Menu's old Hide Stats Bar/Minimap/Log entries are
   removed for the same reason (nothing hides on its own anymore, so
   there's nothing left to toggle back). What's KEPT from the mobile work:
   the joystick
   (gated on has-touch, not touch-mode - see main.js's applyHasTouch,
   unaffected by any of this), bigger tap targets on genuinely
   interactive controls (buttons/tabs/quiz choices) a few rules below,
   and native resize on the four draggable windows (re-enabled just
   below) - all of which make touch input WORK, without changing where
   anything is or how it's laid out. */
.qm-divider { height: 1px; background: var(--panel-border); margin: 4px 2px; }

/* User follow-up, explicitly reversing part of the "match desktop
   exactly" philosophy above: "please look at shrinking the
   minimap/top stats bar on mobile" - the desktop-sized minimap
   (150px) and identity/stat-chip banner ate a large fraction of a real
   phone's much shorter viewport, directly feeding the earlier "main
   environment is blocked and obscured" report alongside the quick-menu
   overlap fixed above. Scoped to .touch-mode (screen-width-driven
   layout density, same category .touch-mode .card above already
   belongs to - NOT .has-touch, which is reserved for touch-INPUT-
   quality fixes like tap-target sizing) so desktop is completely
   unaffected. Deliberately does NOT touch #hud-icon-buttons/
   .hud-icon-btn - those are real tap targets, already sized up by the
   .has-touch rules elsewhere in this section for comfortable touch
   accuracy, and shrinking them back down here would undo that. */
.touch-mode #minimap-container {
  top: 68px; left: 12px;
  width: 96px; height: 96px;
  padding: 3px;
}
.touch-mode #minimap-canvas { width: 90px; height: 90px; }
.touch-mode #hud-portrait { width: 34px; height: 34px; font-size: 16px; }
.touch-mode #hud-left { gap: 8px; }
.touch-mode #hud-name { font-size: 13px; }
.touch-mode #hud-level-row { gap: 6px; }
.touch-mode #hud-level { font-size: 10.5px; }
.touch-mode #hud-chapter { font-size: 10px; }
.touch-mode #hud-bars { max-width: 170px; gap: 3px; }
.touch-mode .bar { height: 10px; }
.touch-mode .bar-text { font-size: 9px; }
.touch-mode .stat-chip { padding: 2px 6px; font-size: 10.5px; gap: 3px; }
.touch-mode .chip-icon { font-size: 10px; }
.touch-mode #hud-stats { gap: 4px; }

/* User follow-up, with screenshots: "the game play is still obscured way
   too much by info windows" and "perhaps a Hide all windows button is in
   order too." The screenshots showed the always-on message log itself as
   the remaining big offender - unlike every other window in the game
   (Shop/Inventory/Unit Circle/Command Reference/Worksheet), it has no
   collapse/close control at all, and its desktop sizing (max-width 480px,
   max-height 34vh) eats a huge fraction of a phone's much shorter, much
   narrower viewport even shrunk down. Two changes, both .touch-mode only:
   (1) a smaller resting size so it obstructs less even while showing,
   (2) a real show/hide toggle (see #btn-toggle-log, index.html/main.js/
   UI.toggleLogVisibility) so the player can clear it entirely on demand,
   matching what every other window in the game could already do. */
.touch-mode #hud-log { max-width: 260px; max-height: 18vh; max-height: 18dvh; font-size: 11px; padding: 6px 9px; }
#hud-log.log-hidden { display: none; }
/* #btn-toggle-log (index.html) exists in the DOM on every platform (a
   genuine sibling of #action-bar, like #btn-toggle-windows right next to
   it - see index.html's own comment on why that has to be a sibling
   rather than a child) but only ever SHOWN here, in .touch-mode - desktop
   never sees or can interact with it, so desktop behavior is completely
   unaffected either way. */
#btn-toggle-log { display: none; }
.touch-mode #btn-toggle-log { display: flex; }
/* #btn-toggle-hud / #hud-top.hud-top-hidden - see UI.toggleHudTopVisibility
   (ui.js) for the full reasoning. Same "exists in the DOM everywhere, only
   ever shown in .touch-mode" pattern as #btn-toggle-log above. */
#btn-toggle-hud { display: none; }
.touch-mode #btn-toggle-hud { display: flex; }
#hud-top.hud-top-hidden { display: none; }

/* User follow-up, with older-phone screenshots: "if you can make those
   windows sizable that would be great too" (comparing against a newer
   phone where window resizing already felt good). The actual resize
   MECHANISM (the custom touch-draggable .touch-resize-grip, see
   makeWindowDraggable in ui.js) isn't screen-size-dependent - it works
   the same on any touch device. What IS screen-size-dependent is these
   two panels' DEFAULT starting dimensions: #reference-sheets-panel opens
   at a fixed 460x500px, #worksheet-panel at 380x460px - both authored
   against a desktop-sized viewport. On a small/older phone, max-width/
   max-height:92vw/92vh already clamps the RENDERED size down, but that
   means the panel opens already pinned at its ceiling, leaving the grip
   nothing left to grow INTO - "resizable" in name only when there's no
   room left to actually resize larger. Switching the default to
   viewport-relative units instead of a fixed desktop pixel value fixes
   this at the source: the panel now opens at a sensible fraction of
   WHATEVER screen it's on, old phone or new, with real headroom below
   the 92vw/92vh ceiling either way. #unit-circle-panel and #modal-box
   aren't touched here - neither has an explicit starting width/height at
   all (they already auto-size to their own content), so they were never
   affected by this in the first place. */
.touch-mode #reference-sheets-panel { top: 60px; left: 10px; width: 88vw; height: 66vh; height: 66dvh; }
.touch-mode #worksheet-panel { bottom: 10px; left: 10px; width: 88vw; height: 52vh; height: 52dvh; }

/* #hud-icon-buttons (Menu/Mute/Settings/Achievements/Unit Circle/Formula
   Reference/Worksheet, plus the two touch-only camera/wireframe buttons -
   9 buttons total) had no wrap rule, so at touch-mode's larger tap-target
   padding it could overflow past the right edge of the screen on
   anything narrower than a large tablet, leaving Settings/Achievements/
   Worksheet etc. un-tappable with no scrollbar or affordance hinting they
   existed at all. Wrapping right-aligned onto a second row keeps every
   button reachable regardless of viewport width. */
.has-touch #hud-icon-buttons { flex-wrap: wrap; justify-content: flex-end; max-width: min(66vw, 460px); }
/* BUGFIX: was .touch-mode - the MAIN modal's own close/collapse buttons
   (Shop, Inventory, Bestiary, Settings, every quiz - the single most
   frequently opened/closed window type in the game), same "toggle
   windows" bug the three smaller panels' buttons had above. */
.has-touch #modal-close-btn { width: 36px; height: 36px; font-size: 22px; }
.has-touch #modal-collapse-btn { width: 36px; height: 36px; font-size: 20px; }
/* Class-selection cards more compact in touch-mode too - user request:
   "ensure that all classes are available to see in one window." Smaller
   padding/text fits more of the 2-column grid (see .card-grid above) on
   screen at once without needing to scroll as far. Deliberately LEFT on
   .touch-mode (not switched to .has-touch like the rules above) - this
   is screen-width-driven layout density tied to the chosen UI mode's
   expected viewport, not a touch-input-quality fix. */
.touch-mode .card { padding: 10px 12px; }
.touch-mode .card h3 { font-size: 14px; margin-bottom: 3px; }
.touch-mode .card p { font-size: 11px; margin: 2px 0; }
/* Quiz answers are the single most-tapped control in the whole game -
   worth a dedicated bump beyond the generic HUD-button sizing above.
   BUGFIX: was .touch-mode - same bug class as everything else in this
   section; a Desktop-UI-mode touchscreen player taps these constantly. */
.has-touch #modal-box .quiz-choice { padding: 16px 14px; font-size: 15px; margin: 8px 0; }
.has-touch #modal-box .row { padding: 14px 12px; }

/* User request: "make the game playable vertically instead, that way I
   can see the game environment better" - portrait was previously hard-
   blocked here by a full-screen "rotate to landscape" prompt, on the
   reasoning that the wide HUD/action-bar/joystick layout wouldn't fit a
   portrait phone. That constraint is largely gone now that the HUD can
   shrink AND fully collapse (see the compact-mobile-HUD and HUD-collapse
   passes above) - taller/narrower actually shows MORE of the 3D
   environment vertically, which is the whole point being asked for here.
   Portrait-specific layout tweaks (mainly the joystick zone, the one
   element whose proportions were tuned assuming landscape's wide-short
   shape) live just below, under @media (orientation: portrait). */

/* Prevents the classic mobile-browser annoyances that actively fight a
   real-time game: double-tap-to-zoom on a fast double-click ability slot,
   the blue tap-highlight rectangle flashing on every touch, and long-press
   text-selection/callout menus popping up over game UI that was never
   meant to be selectable text in the first place. BUGFIX: was scoped to
   .touch-mode only - the exact bug class documented at the top of this
   section, and very likely the single biggest contributor to "toggling
   windows doesn't work well": a Desktop-UI-mode touchscreen player
   holding a close/collapse button even slightly too long (trivially easy
   on a phone, much harder to avoid than on a mouse) would trigger the
   browser's own text-selection callout INSTEAD of the tap being
   registered as a click, with no suppression in place to stop it. Gated
   on .has-touch instead so this applies to any real touchscreen
   regardless of chosen layout - genuinely non-touch (mouse/trackpad)
   users still select/copy log text and standard UI text normally either
   way, since .has-touch is never set for them at all. */
.has-touch, .has-touch * {
  -webkit-tap-highlight-color: transparent;
  -webkit-touch-callout: none;
}
.has-touch #hud, .has-touch #action-bar-row,
.has-touch #controls-panel {
  user-select: none;
  -webkit-user-select: none;
}
/* The joystick zone gets this regardless of UI mode (has-touch, not
   touch-mode) - it's now visible/usable in Desktop UI mode too on a
   touch-capable device, and a stray text-selection callout mid-drag
   would be just as unwelcome there as in Mobile mode. */
.has-touch #touch-joystick-zone {
  user-select: none;
  -webkit-user-select: none;
}

/* ---------------- Level Up 3D model + 3D Model Library (ui.js) ---------------- */
.levelup-model {
  margin-top: 16px;
  padding-top: 14px;
  border-top: 1px solid var(--panel-border);
  text-align: center;
}
.levelup-model h4 {
  margin: 0 0 8px;
  color: var(--gold);
  font-size: 14px;
}
.model-canvas {
  width: 100%;
  max-width: 280px;
  height: 200px;
  display: block;
  margin: 0 auto 10px;
  background: radial-gradient(circle at 50% 40%, #1c1c30, #10101c);
  border: 1px solid var(--panel-border);
  border-radius: 3px;
}
.model-caption {
  color: var(--dim);
  font-size: 12.5px;
  line-height: 1.5;
  max-width: 320px;
  margin: 0 auto 12px;
}
#modal-box .levelup-model .menu-btn {
  font-size: 13px;
  padding: 8px 16px;
}
.model-library-body {
  display: flex;
  gap: 18px;
  align-items: flex-start;
  flex-wrap: wrap;
}
.model-library-list {
  flex: 1 1 200px;
  min-width: 180px;
  max-height: 380px;
  overflow-y: auto;
}
#modal-box .model-library-row { font-size: 13px; }
#modal-box .model-library-row.active { border-color: var(--gold); background: #2a2410; }
.ref-section-header {
  margin: 10px 0 4px;
  padding-top: 8px;
  border-top: 1px solid var(--border, #3a3a4a);
  font-size: 12px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--gold);
  opacity: 0.85;
}
.model-library-body .levelup-model {
  flex: 1 1 260px;
  border-top: none;
  padding-top: 0;
  margin-top: 0;
}
@media (max-width: 640px) {
  .model-library-body { flex-direction: column; }
  .model-library-list { max-height: 160px; }
}

/* ---------------- Ability Scores screen: stats + live avatar preview ---------------- */
.abilities-layout {
  display: flex;
  gap: 24px;
  align-items: flex-start;
  justify-content: center;
  flex-wrap: wrap;
  margin: 10px 0;
}
.abilities-stats { flex: 1 1 260px; max-width: 340px; }
.abilities-avatar { flex: 0 0 auto; margin-top: 0; padding-top: 0; border-top: none; }
@media (max-width: 640px) {
  .abilities-layout { flex-direction: column; align-items: center; }
}
