* { box-sizing: border-box; }

:root {
  /* Warm bronze/leather/parchment palette (WoW-flavored chrome). --accent-2
     stays purple/arcane on purpose - used for "available"/magic accents, and
     reads fine against the warm browns the same way epic-purple pops in WoW. */
  --bg: #15100a;
  --bg-panel: #211a10;
  --bg-panel-2: #2d2312;
  --border: #6b4a26;
  --border-light: #a9793a;
  --text: #ecdfc4;
  --text-dim: #ab9878;
  --accent: #d4af5a;
  --accent-2: #8a5fd6;
  --hp: #c23b3b;
  --hp-bg: #3a1414;
  --good: #5cae6e;
  --bad: #c23b3b;
  --font: 'Georgia', 'Cambria', serif;
}

html, body {
  margin: 0;
  padding: 0;
  background: radial-gradient(ellipse at top, #2a1f10 0%, #100b06 70%);
  color: var(--text);
  font-family: var(--font);
  min-height: 100vh;
}

#app {
  max-width: 720px;
  margin: 0 auto;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  padding: 16px;
}

button {
  font-family: var(--font);
  cursor: pointer;
  text-align: center;
}

/* ---------- Title / Class select ---------- */
.center-screen {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  gap: 18px;
  padding: 20px;
}

.game-title {
  font-size: clamp(1.9rem, 9vw, 2.6rem);
  letter-spacing: 3px;
  font-variant: small-caps;
  color: var(--accent);
  text-shadow: 0 2px 12px rgba(212, 175, 90, 0.45), 0 1px 0 rgba(0,0,0,0.6);
  margin: 0;
}

.game-subtitle {
  color: var(--text-dim);
  margin-top: -10px;
  font-style: italic;
}

.meta-stats {
  display: flex;
  gap: 18px;
  flex-wrap: wrap;
  justify-content: center;
  color: var(--text-dim);
  font-size: 0.85rem;
}

/* The title screen's own button column - a fixed uniform width (rather than
   each button sizing to its own label length) so "Begin Adventure" and
   "Change Difficulty (Normal)" read as one consistent column, not a
   staggered stack of different widths. */
.title-btn { width: 300px; max-width: 100%; }
.title-skip-tutorials {
  display: flex; align-items: center; gap: 8px;
  margin-top: 14px;
  font-size: 0.8rem;
  color: var(--text-dim);
  cursor: pointer;
}

/* ---------- Ornate buttons (beveled metal/wood, carved-text look) ---------- */
.btn-primary {
  background: linear-gradient(180deg, #f0d68a 0%, #d4af5a 45%, #a3792f 100%);
  color: #2a1a0a;
  border: 2px solid #5c3a18;
  border-radius: 4px;
  padding: 13px 30px;
  font-size: 1.05rem;
  font-weight: bold;
  letter-spacing: 0.4px;
  min-height: 44px;
  text-shadow: 0 1px 0 rgba(255,255,255,0.4);
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.45), inset 0 -3px 6px rgba(0,0,0,0.22), 0 3px 0 #3d2510, 0 5px 10px rgba(0,0,0,0.45);
  transition: transform 0.08s ease, box-shadow 0.15s ease;
}
.btn-primary:hover { transform: translateY(-1px); box-shadow: inset 0 1px 0 rgba(255,255,255,0.5), inset 0 -3px 6px rgba(0,0,0,0.22), 0 4px 0 #3d2510, 0 0 14px rgba(212,175,90,0.55); }
.btn-primary:active { transform: translateY(2px); box-shadow: inset 0 2px 5px rgba(0,0,0,0.35), 0 1px 0 #3d2510; }
.btn-primary:disabled { opacity: 0.45; cursor: not-allowed; transform: none; filter: grayscale(0.4); }

.btn-secondary {
  background: linear-gradient(180deg, #4a3823 0%, #2e2213 100%);
  color: var(--text);
  border: 2px solid var(--border);
  border-radius: 4px;
  padding: 11px 24px;
  min-height: 44px;
  text-shadow: 0 1px 1px rgba(0,0,0,0.7);
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.07), 0 2px 0 #150f08, 0 4px 6px rgba(0,0,0,0.3);
  transition: border-color 0.15s ease, transform 0.08s ease;
}
.btn-secondary:hover { border-color: var(--accent); transform: translateY(-1px); }
.btn-secondary:active { transform: translateY(1px); }

.class-grid {
  display: flex;
  gap: 14px;
  flex-wrap: wrap;
  justify-content: center;
}

.class-card {
  background: linear-gradient(180deg, var(--bg-panel) 0%, var(--bg-panel-2) 100%);
  border: 2px solid var(--border);
  border-radius: 8px;
  padding: 16px;
  width: 190px;
  cursor: pointer;
  transition: border-color 0.15s ease, transform 0.1s ease;
  color: var(--text);
  text-align: center;
  display: block;
  box-shadow: inset 0 0 0 1px rgba(212,175,90,0.1), 0 3px 8px rgba(0,0,0,0.35);
}
.class-card:hover { border-color: var(--accent-2); transform: translateY(-2px); }
.class-card.selected { border-color: var(--accent); box-shadow: inset 0 0 0 1px rgba(212,175,90,0.25), 0 0 14px rgba(212,175,90,0.4); }
.class-card .class-icon { font-size: 2.2rem; }
.class-card h3 { margin: 6px 0 4px; }
.class-card p { font-size: 0.8rem; color: var(--text-dim); margin: 4px 0; }
.class-card .class-stats { font-size: 0.78rem; color: var(--accent); margin-top: 8px; }
.class-card .class-stat-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 3px 12px;
  font-size: 0.78rem;
  font-variant-numeric: tabular-nums;
  color: var(--accent);
  margin-top: 10px;
  padding-top: 8px;
  border-top: 1px solid rgba(212,175,90,0.15);
}
.class-card.locked { cursor: not-allowed; opacity: 0.55; filter: grayscale(0.6); }
.class-card.locked:hover { transform: none; border-color: var(--border); }

/* Two confirm buttons, shown one at a time by breakpoint (see showClassSelect):
   .venture-forth-btn is inserted as a DOM sibling right after whichever card
   is selected - flex-basis:100% in the wrapping .class-grid forces it onto
   its own row immediately below that card, so the confirm tap stays right
   under the thumb on one-handed mobile play, where a single column of cards
   makes a full-width break read as natural. .venture-forth-fixed is the
   plain static button below the whole grid, for desktop - the one-handed
   reach problem doesn't exist there, and breaking a wide multi-column grid
   with a full-width bar mid-scroll reads as a layout glitch, not an
   affordance. Hidden by default; the mobile breakpoint below flips which
   one shows. */
.venture-forth-btn { display: none; flex-basis: 100%; width: 100%; max-width: 420px; margin: 0 auto; }
.venture-forth-fixed { max-width: 420px; margin: 4px auto 0; }

@media (max-width: 480px) {
  .venture-forth-btn { display: block; }
  .venture-forth-fixed { display: none; }
}

/* ---------- Header (persistent during run) ---------- */
.hud {
  display: flex;
  align-items: center;
  justify-content: space-between;
  background: linear-gradient(180deg, var(--bg-panel) 0%, var(--bg-panel-2) 100%);
  border: 2px solid var(--border);
  border-radius: 6px;
  padding: 10px 14px;
  margin-bottom: 12px;
  flex-wrap: wrap;
  gap: 10px;
  box-shadow: inset 0 0 0 1px rgba(212,175,90,0.08), 0 2px 6px rgba(0,0,0,0.3);
}

.hud-player {
  display: flex;
  align-items: center;
  gap: 10px;
}

/* Wraps .hp-bar-wrap (not just the bar itself) so the floating crit text
   (see .floating-crit below) can sit right above the bar without being
   clipped by the bar's own overflow:hidden. */
.hp-bar-container { position: relative; }
.hp-bar-wrap {
  width: 140px;
  height: 14px;
  margin: 0 auto;
  background: var(--hp-bg);
  border-radius: 4px;
  overflow: hidden;
  border: 1px solid var(--border);
  box-shadow: inset 0 1px 3px rgba(0,0,0,0.5);
}
.hp-bar-fill { height: 100%; background: linear-gradient(90deg, var(--hp), #e56a6a); transition: width 0.3s ease; }
.hp-label { font-size: 0.72rem; color: var(--text-dim); margin-top: 2px; }

/* Health bar "just got hit" feedback (see Combat.resolveAfterPlayerHit and
   resolveEnemyTurn, and the hpFlash handling in renderCombatScreen) - a
   quick shake plus a red flash on the bar itself, both scaled by
   --flash-intensity (set inline per-hit from how big a bite the damage took
   out of that side's own max HP), so a huge crit visibly rattles the bar
   while a glancing hit barely nudges it. */
@keyframes hpBarShakeFlash {
  0% { transform: translateX(0); box-shadow: inset 0 1px 3px rgba(0,0,0,0.5); }
  15% { transform: translateX(calc(-6px * var(--flash-intensity, 1))); box-shadow: inset 0 1px 3px rgba(0,0,0,0.5), 0 0 calc(10px * var(--flash-intensity, 1)) rgba(255,40,40,0.9); }
  30% { transform: translateX(calc(5px * var(--flash-intensity, 1))); }
  45% { transform: translateX(calc(-4px * var(--flash-intensity, 1))); }
  60% { transform: translateX(calc(2px * var(--flash-intensity, 1))); box-shadow: inset 0 1px 3px rgba(0,0,0,0.5), 0 0 calc(4px * var(--flash-intensity, 1)) rgba(255,40,40,0.5); }
  100% { transform: translateX(0); box-shadow: inset 0 1px 3px rgba(0,0,0,0.5); }
}
.hp-bar-wrap.hp-bar-hit { animation: hpBarShakeFlash 0.45s ease-out; }
@keyframes hpBarFillFlash {
  0% { filter: brightness(1) saturate(1); }
  20% { filter: brightness(calc(1.3 + 1.3 * var(--flash-intensity, 1))) saturate(1.4); }
  100% { filter: brightness(1) saturate(1); }
}
.hp-bar-wrap.hp-bar-hit .hp-bar-fill { animation: hpBarFillFlash 0.45s ease-out; }

.hud-gold { color: var(--accent); font-weight: bold; }
/* Opens showRunRelicsModal - was previously an icon strip rendered inline
   here, which overflowed the HUD row once a long run had picked up a
   dozen+ distinct relics. */
.hud-relics-btn {
  background: var(--bg-panel-2);
  border: 1px solid var(--border);
  border-radius: 4px;
  padding: 6px 10px;
  color: var(--accent);
  font-family: inherit;
  font-weight: bold;
  font-size: 0.85rem;
  cursor: pointer;
  transition: border-color 0.15s ease;
}
.hud-relics-btn:hover { border-color: var(--accent-2); }
.relic-icon { position: relative; display: inline-flex; }
.hud-curses { display: flex; gap: 4px; font-size: 1.1rem; filter: drop-shadow(0 0 3px rgba(192,57,43,0.7)); }
.hud-buffs { display: flex; gap: 4px; font-size: 1.1rem; filter: drop-shadow(0 0 3px rgba(76,175,125,0.7)); }
.hud-act { font-size: 0.78rem; color: var(--text-dim); }

/* ---------- Map: fixed "3rd person" perspective viewport (no scrolling -
   the camera stays anchored on the player, the road recedes into the
   distance instead). A soft gradient fades the horizon at the top. ---------- */
.map-container {
  border: 2px solid var(--border);
  border-radius: 6px;
  background: radial-gradient(ellipse at 50% 15%, #3a2a18 0%, var(--bg-panel) 65%);
  position: relative;
  overflow: hidden;
  box-shadow: inset 0 0 0 1px rgba(212,175,90,0.08);
}
.map-container::before {
  content: '';
  position: absolute;
  top: 0; left: 0; right: 0;
  height: 90px;
  background: linear-gradient(to bottom, rgba(0,0,0,0.5) 0%, rgba(0,0,0,0) 100%);
  pointer-events: none;
  z-index: 4;
}
/* The impact beat when a charging node and the traveler meet (see
   shakeScreen in map.js) - the whole viewport jolts once, distinct from
   either combatant's own attack-swing animation. */
.map-container.map-impact-shake { animation: mapImpactShake 0.32s ease-out; }
@keyframes mapImpactShake {
  0% { transform: translate(0, 0); }
  15% { transform: translate(-7px, 3px); }
  32% { transform: translate(6px, -4px); }
  50% { transform: translate(-5px, 3px); }
  68% { transform: translate(4px, -2px); }
  84% { transform: translate(-2px, 1px); }
  100% { transform: translate(0, 0); }
}
@media (prefers-reduced-motion: reduce) {
  .map-container.map-impact-shake { animation: none; }
}

.map-scroll { position: relative; width: 320px; max-width: 100%; margin: 0 auto; }

/* The zone's PixelLab-generated top-down overworld map (see
   renderZoneTopdownMap in map.js) - fills .map-container exactly (a fixed
   320x500 viewport, not a scrollable one), landmarks confined to its own top
   quarter so they stay clear of the node graph below. background-position
   keeps that top edge anchored so cover-cropping only ever eats into the
   open ground at the bottom, never the landmarks. */
.map-topdown-bg {
  position: absolute; inset: 0;
  background-size: cover;
  background-position: top center;
  image-rendering: pixelated;
  z-index: 0;
}
/* Brief zoom-and-fade travel cues (see leaveForegroundTrees/
   bloomForegroundTrees in map.js) standing in for the old walk animation -
   "leaving" pushes the world away as you set out, "blooming" brings the new
   stretch back into focus on arrival. */
.map-topdown-bg.map-topdown-bg-leaving { animation: topdownBgLeave 0.5s ease-in forwards; }
.map-topdown-bg.map-topdown-bg-blooming { animation: topdownBgBloom 0.6s cubic-bezier(0.3, 1.2, 0.5, 1) forwards; }
@keyframes topdownBgLeave {
  0% { transform: scale(1); opacity: 1; }
  100% { transform: scale(1.12); opacity: 0.35; }
}
@keyframes topdownBgBloom {
  0% { transform: scale(1.12); opacity: 0.35; }
  100% { transform: scale(1); opacity: 1; }
}
.map-theme-label {
  position: absolute;
  top: 6px;
  left: 10px;
  font-size: 0.68rem;
  color: rgba(255,255,255,0.65);
  text-shadow: 0 1px 2px #000, 0 0 4px #000;
  pointer-events: none;
  z-index: 3;
}

.map-svg { position: absolute; top: 0; left: 0; z-index: 1; }
.map-line { stroke: #3a2a18; stroke-width: 8; stroke-linecap: round; opacity: 0.5; }
.map-line.active {
  stroke: var(--accent-2);
  stroke-width: 8;
  stroke-linecap: round;
  opacity: 0.85;
  stroke-dasharray: 5 8;
  animation: pathFlow 1.4s linear infinite;
}
@keyframes pathFlow { to { stroke-dashoffset: -26; } }
.map-line-hit { stroke: transparent; stroke-width: 22; cursor: pointer; }
/* Applied to every node/line except the one just picked (see animateTravel
   in map.js) - the road not taken falls away while the chosen node travels
   in to meet the player, instead of just vanishing on the next render. */
.map-fading-out { transition: opacity 0.3s ease; opacity: 0 !important; }

/* The traveler marker: your class's own sprite, always dead center - the
   chosen node travels in to meet it rather than the other way around (see
   animateTravel in map.js). Kept above every node, including the one
   currently charging in (animateTravel bumps that node's own z-index to 5
   while it travels, so its art doesn't fall behind other still-fading
   nodes/lines mid-animation) - the player should always read as standing
   in front of the enemy on this screen, not the other way around. */
.map-traveler {
  position: absolute;
  transform: translate(-50%, -100%) scale(var(--traveler-scale, 1));
  transform-origin: 50% 100%;
  pointer-events: none;
  z-index: 6;
}
.map-traveler svg, .map-traveler img { image-rendering: pixelated; filter: drop-shadow(0 3px 3px rgba(0,0,0,0.6)); }
.map-traveler.facing-left svg, .map-traveler.facing-left img { transform: scaleX(-1); }
/* Played once on arrival (see animateTravel in map.js) - the landing hop
   after the walk cycle below, buying time for the treeline/path bloom-in to
   actually be seen before the screen cuts to the encounter. */
.map-traveler.jumping svg, .map-traveler.jumping img { animation: travelerJump 0.6s ease-out 1; }
.map-traveler.jumping.facing-left svg, .map-traveler.jumping.facing-left img { animation: travelerJumpLeft 0.6s ease-out 1; }
@keyframes travelerJump {
  0% { transform: translateY(0) rotate(0deg) scale(1, 1); }
  25% { transform: translateY(-6px) rotate(-4deg) scale(0.94, 1.08); }
  55% { transform: translateY(-24px) rotate(3deg) scale(1.04, 0.96); }
  80% { transform: translateY(-2px) rotate(-2deg) scale(0.97, 1.05); }
  100% { transform: translateY(0) rotate(0deg) scale(1, 1); }
}
@keyframes travelerJumpLeft {
  0% { transform: scaleX(-1) translateY(0) rotate(0deg) scale(1, 1); }
  25% { transform: scaleX(-1) translateY(-6px) rotate(4deg) scale(0.94, 1.08); }
  55% { transform: scaleX(-1) translateY(-24px) rotate(-3deg) scale(1.04, 0.96); }
  80% { transform: scaleX(-1) translateY(-2px) rotate(2deg) scale(0.97, 1.05); }
  100% { transform: scaleX(-1) translateY(0) rotate(0deg) scale(1, 1); }
}
/* Two more traveler states, both purely CSS (no per-class/weapon sprite
   frames needed - one shared animation works on every class/armor/weapon/
   mount/pet combo since it just transforms whatever's already rendered):
   a marching bob+lean while a chosen node travels in to meet the player
   (animateTravel), and a slower idle sway while just standing at the
   current node between choices. Exactly one of idle-bob/walking/jumping is
   ever active at a time (map.js swaps the class) so their `transform`
   animations never fight each other. Applies to every element inside
   .map-traveler (rider, mount, pet alike - see renderCompanionRig), so a
   mounted/companioned traveler bobs together as one group. */
.map-traveler.walking svg, .map-traveler.walking img { animation: travelerWalk 0.42s ease-in-out infinite; }
.map-traveler.walking.facing-left svg, .map-traveler.walking.facing-left img { animation: travelerWalkLeft 0.42s ease-in-out infinite; }
@keyframes travelerWalk {
  0%, 100% { transform: translateY(0) rotate(0deg); }
  25% { transform: translateY(-3px) rotate(-3deg); }
  50% { transform: translateY(-0.5px) rotate(0deg); }
  75% { transform: translateY(-3px) rotate(3deg); }
}
@keyframes travelerWalkLeft {
  0%, 100% { transform: scaleX(-1) translateY(0) rotate(0deg); }
  25% { transform: scaleX(-1) translateY(-3px) rotate(3deg); }
  50% { transform: scaleX(-1) translateY(-0.5px) rotate(0deg); }
  75% { transform: scaleX(-1) translateY(-3px) rotate(-3deg); }
}
.map-traveler.idle-bob svg, .map-traveler.idle-bob img { animation: travelerIdleBob 2.2s ease-in-out infinite; }
.map-traveler.idle-bob.facing-left svg, .map-traveler.idle-bob.facing-left img { animation: travelerIdleBobLeft 2.2s ease-in-out infinite; }
@keyframes travelerIdleBob {
  0%, 100% { transform: translateY(0) rotate(0deg); }
  50% { transform: translateY(-2px) rotate(0.6deg); }
}
@keyframes travelerIdleBobLeft {
  0%, 100% { transform: scaleX(-1) translateY(0) rotate(0deg); }
  50% { transform: scaleX(-1) translateY(-2px) rotate(-0.6deg); }
}
@media (prefers-reduced-motion: reduce) {
  .map-traveler.idle-bob svg, .map-traveler.idle-bob img,
  .map-traveler.idle-bob.facing-left svg, .map-traveler.idle-bob.facing-left img { animation: none; }
}

.map-node {
  position: absolute;
  z-index: 2;
  transform: translate(-50%, -50%) scale(var(--node-scale, 1));
  transition: left 0.2s ease, top 0.2s ease;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  background: var(--bg-panel-2);
  border: 2px solid var(--border);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1.1rem;
  color: var(--text-dim);
}
.map-node.visited { opacity: 0.45; border-color: var(--good); }
.map-node.current { border-color: var(--accent); box-shadow: 0 0 10px rgba(212,175,90,0.6); }
.map-node.available {
  cursor: pointer;
  border-color: var(--accent-2);
  box-shadow: 0 0 8px rgba(138,95,214,0.5);
}
.map-node.available:hover { border-color: var(--accent); transform: translate(-50%, -50%) scale(calc(var(--node-scale, 1) * 1.12)); }
.map-node.boss { width: 48px; height: 48px; font-size: 1.4rem; }
.map-node.fogged .map-node-icon { opacity: 0.55; font-weight: bold; font-family: Georgia, serif; }
.map-node.fogged.available { animation: fogPulse 2.2s ease-in-out infinite; }
/* The actual creature/theme art (see nodePreviewArt, map.js) replacing the
   generic icon+circle wherever it's known - sized closer to the traveler's
   own on-map sprite (renderCompanionRig, 78-100px) rather than shrunk into
   the small type-icon circle. The node's own circle becomes a ground plate
   at the art's feet (the ::after below) instead of a porthole clipping the
   whole sprite, so a tall/wide creature can stand on it rather than being
   masked into a coin portrait. */
.map-node.has-art {
  width: 56px;
  height: 56px;
  background: none;
  border: none;
  box-shadow: none;
  overflow: visible;
}
/* The ground plate itself - a flattened ellipse at the sprite's feet rather
   than a ring around its whole body. Still carries the available/current/
   visited color cues the plain circle used to (see the modifiers below),
   just as a floor glow instead of a full border. */
.map-node.has-art::after {
  content: '';
  position: absolute;
  left: 50%;
  bottom: 2px;
  width: 82%;
  height: 26%;
  transform: translateX(-50%);
  border-radius: 50%;
  background: radial-gradient(ellipse at center, rgba(20,15,10,0.55) 0%, rgba(20,15,10,0.3) 55%, transparent 80%);
  border: 2px solid var(--border);
  z-index: -1;
}
.map-node.has-art.available::after { border-color: var(--accent-2); box-shadow: 0 0 8px rgba(138,95,214,0.5); }
.map-node.has-art.current::after { border-color: var(--accent); box-shadow: 0 0 10px rgba(212,175,90,0.6); }
.map-node.has-art.visited::after { border-color: var(--good); opacity: 0.7; }
/* height/max-width default to the densest reachable-row tier (4 simultaneous
   choices) - the current/boss node (no --node-art-w/-h set, see renderMap in
   map.js) falls back to these same values since it never shares a row with
   other choices to crowd. A sparser reachable row sets bigger values inline
   per ART_SIZE_TIERS so its one or two choices can run up to ~2x this size -
   "same size as the player character or up to 100% larger" - without
   crowding a denser row. */
.map-node-art {
  position: absolute;
  left: 50%;
  bottom: 3px;
  transform: translateX(-50%);
  width: auto;
  height: var(--node-art-h, 156%);
  max-width: var(--node-art-w, 190%);
  object-fit: contain;
  image-rendering: pixelated;
  filter: drop-shadow(0 3px 2px rgba(0,0,0,0.5));
  pointer-events: none;
}
/* World Event map-node icon (see the isWorldEvent branch in renderMap,
   map.js) - a small spinning "earth" rendered entirely in CSS (gradient
   sphere + a scrolling continent layer clipped to the circle) rather than
   shrinking down the full encounter screen's own scene art, which read as
   mismatched at this size. Sits in the plain round node circle like the
   emoji icon it replaces, not the has-art ground-plate treatment. */
.map-node-planet {
  position: relative;
  display: block;
  width: 26px;
  height: 26px;
  border-radius: 50%;
  overflow: hidden;
  background: radial-gradient(circle at 35% 30%, #7ec2ee 0%, #2f72ab 45%, #143a5c 100%);
  box-shadow: inset -4px -4px 6px rgba(0,0,0,0.45), inset 2px 2px 4px rgba(255,255,255,0.25), 0 0 8px rgba(90,170,230,0.5);
  animation: mapPlanetGlow 3s ease-in-out infinite;
}
.map-node-planet-surface {
  position: absolute;
  inset: -15% -70% -15% -15%;
  background-image:
    radial-gradient(ellipse 26% 40% at 15% 35%, rgba(88,163,70,0.85) 0%, transparent 72%),
    radial-gradient(ellipse 20% 30% at 45% 68%, rgba(76,148,58,0.8) 0%, transparent 72%),
    radial-gradient(ellipse 18% 26% at 75% 25%, rgba(96,170,78,0.75) 0%, transparent 72%),
    radial-gradient(ellipse 16% 24% at 95% 55%, rgba(88,163,70,0.7) 0%, transparent 72%);
  background-repeat: repeat-x;
  background-size: 55% 100%;
  animation: mapPlanetSpin 7s linear infinite;
}
@keyframes mapPlanetSpin {
  from { background-position: 0% 0; }
  to { background-position: -110% 0; }
}
@keyframes mapPlanetGlow {
  0%, 100% { box-shadow: inset -4px -4px 6px rgba(0,0,0,0.45), inset 2px 2px 4px rgba(255,255,255,0.25), 0 0 8px rgba(90,170,230,0.5); }
  50% { box-shadow: inset -4px -4px 6px rgba(0,0,0,0.45), inset 2px 2px 4px rgba(255,255,255,0.25), 0 0 14px rgba(90,170,230,0.85); }
}
@keyframes fogPulse {
  0%, 100% { box-shadow: 0 0 8px rgba(138,95,214,0.5); }
  50% { box-shadow: 0 0 13px rgba(138,95,214,0.8); }
}

/* The map key, collapsed under a single centered toggle (native
   <details>/<summary>, same no-JS pattern as .shop-category) instead of a
   permanently-visible row of 15 icon/label pairs eating vertical space
   above the map's own action buttons. */
.map-legend-details {
  margin-top: 10px;
  text-align: center;
}
.map-legend-details summary {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  cursor: pointer;
  list-style: none;
  padding: 6px 16px;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--bg-panel-2);
  color: var(--text-dim);
  font-size: 0.78rem;
  font-weight: 600;
  transition: border-color 0.15s ease, color 0.15s ease;
}
.map-legend-details summary::-webkit-details-marker { display: none; }
.map-legend-details summary::after { content: '▸'; transition: transform 0.15s ease; }
.map-legend-details[open] summary::after { transform: rotate(90deg); }
.map-legend-details summary:hover { border-color: var(--accent); color: var(--text); }
.map-legend {
  display: flex;
  justify-content: center;
  gap: 10px 16px;
  flex-wrap: wrap;
  font-size: 0.72rem;
  color: var(--text-dim);
  margin-top: 10px;
  padding: 12px;
  background: var(--bg-panel-2);
  border: 1px solid var(--border);
  border-radius: 8px;
}

.btn-abandon-run { margin-top: 10px; width: 100%; opacity: 0.85; }
.btn-abandon-run:hover { opacity: 1; border-color: var(--bad); }

/* Collapsible Sanctuary shop categories (native <details>/<summary> - no JS
   needed to toggle) so a ~90-entry relic list doesn't dominate the screen. */
.shop-category {
  background: var(--bg-panel-2);
  border: 1px solid var(--border);
  border-radius: 6px;
  margin-bottom: 10px;
}
.shop-category summary {
  cursor: pointer;
  padding: 12px 14px;
  font-weight: 600;
  color: var(--accent);
  list-style: none;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
}
.shop-category summary::-webkit-details-marker { display: none; }
.shop-category summary::after { content: '▸'; color: var(--text-dim); flex-shrink: 0; transition: transform 0.15s ease; }
.shop-category[open] summary::after { transform: rotate(90deg); }
.shop-category[open] summary { border-bottom: 1px solid var(--border); }
.shop-category > .gear-row { margin: 8px 12px; }
.shop-category > .gear-row:last-child { margin-bottom: 12px; }
.shop-category > p.small-text { margin: 0 14px 12px; }

/* A tab-level category list entry (Inventory/Professions/Shop) - opens
   showListModal instead of expanding an inline <details> section. */
.category-btn {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  background: var(--bg-panel-2);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 12px 14px;
  margin-bottom: 10px;
  color: var(--text);
  font-family: inherit;
  font-size: 0.95rem;
  cursor: pointer;
  text-align: left;
  transition: border-color 0.15s ease, transform 0.08s ease;
}
.category-btn:hover { border-color: var(--accent-2); }
.category-btn:active { transform: translateY(1px); }
.category-btn-icon { font-size: 1.2rem; flex-shrink: 0; }
.category-btn-label { flex: 1; font-weight: 600; color: var(--accent); }


/* ---------- Panels (event / rest / shop / treasure / gameover) ---------- */
.panel {
  background: linear-gradient(180deg, var(--bg-panel) 0%, var(--bg-panel-2) 100%);
  border: 2px solid var(--border);
  border-radius: 8px;
  padding: 20px;
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: 14px;
  position: relative;
  box-shadow: inset 0 0 0 1px rgba(212,175,90,0.1), 0 4px 12px rgba(0,0,0,0.35);
}
.panel::before, .panel::after {
  content: '';
  position: absolute;
  top: -5px;
  width: 9px;
  height: 9px;
  background: var(--accent);
  transform: rotate(45deg);
  box-shadow: 0 0 6px rgba(212,175,90,0.6);
  pointer-events: none;
}
.panel::before { left: -5px; }
.panel::after { right: -5px; }

.panel h2 {
  margin: 0;
  color: var(--accent);
  font-variant: small-caps;
  letter-spacing: 1.5px;
  text-shadow: 0 1px 0 rgba(0,0,0,0.5);
}
.panel p.flavor { color: var(--text-dim); line-height: 1.5; }

.choice-list { display: flex; flex-direction: column; gap: 8px; }
.choice-btn {
  background: var(--bg-panel-2);
  border: 1px solid var(--border);
  color: var(--text);
  padding: 14px;
  border-radius: 4px;
  text-align: left;
  font-size: 0.95rem;
  min-height: 44px;
  transition: border-color 0.15s ease, background 0.15s ease;
}
.choice-btn:hover:not(:disabled) { border-color: var(--accent-2); background: var(--bg-panel); }
.choice-btn:disabled { opacity: 0.4; cursor: not-allowed; }

.outcome-box {
  background: var(--bg-panel-2);
  border-left: 3px solid var(--accent);
  padding: 12px 14px;
  border-radius: 4px;
  font-size: 0.9rem;
}

.relic-choice-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  gap: 10px;
}
.relic-card {
  background: var(--bg-panel-2);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 14px 10px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  text-align: center;
  transition: border-color 0.15s ease, background 0.15s ease, transform 0.15s ease;
}
.relic-card:hover { border-color: var(--accent); background: var(--bg-panel); transform: translateY(-2px); }
.relic-card .relic-icon { font-size: 1.8rem; }
.relic-card .relic-name { color: var(--accent-2); font-weight: 600; font-size: 0.9rem; }

.taming-encounter { border-color: var(--accent-2); }

.journal-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(96px, 1fr));
  gap: 8px;
  margin-bottom: 4px;
}
.journal-entry {
  background: var(--bg-panel-2);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 10px 6px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  text-align: center;
}
.journal-entry .journal-icon { font-size: 1.6rem; }
.journal-entry .journal-name { font-weight: 600; font-size: 0.82rem; }
.journal-entry.unknown { opacity: 0.5; }
.journal-entry.unknown .journal-icon { filter: grayscale(1); }
.journal-entry.unknown .journal-name { color: var(--text-dim); }
.quest-row .xp-bar-wrap { width: 100%; max-width: 200px; }
.profession-row.profession-active { border-left: 3px solid var(--accent); box-shadow: inset 0 0 0 1px rgba(212,175,90,0.15); }
.talent-row.talent-locked { opacity: 0.5; }

.shop-item, .rest-option {
  display: flex;
  align-items: center;
  justify-content: space-between;
  background: var(--bg-panel-2);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 10px 12px;
  gap: 10px;
  flex-wrap: wrap;
}
.shop-item .desc, .rest-option .desc { display: flex; align-items: center; gap: 10px; }
.shop-item .price { color: var(--accent); font-weight: bold; }

/* ---------- Combat ---------- */
.combat-arena {
  display: flex;
  justify-content: space-between;
  gap: 16px;
  align-items: flex-start;
  position: relative;
  border-radius: 8px;
  overflow: hidden;
  padding: 10px;
  margin: -10px -10px 0;
}
/* The current act's own zone map art (see .map-topdown-bg, map.js) reused
   as the combat backdrop, so a fight visibly happens IN that zone rather
   than on a flat panel background - set inline per-render (renderCombatScreen,
   main.js) since the theme depends on Game.act. A soft dark scrim on top
   keeps the HP bars/labels readable over busy artwork without hiding it. */
.combat-arena-bg {
  position: absolute;
  inset: 0;
  background-size: cover;
  background-position: top center;
  image-rendering: pixelated;
  z-index: 0;
}
.combat-arena-bg::after {
  content: '';
  position: absolute;
  inset: 0;
  background: rgba(10,7,3,0.32);
}
.combatant {
  position: relative;
  z-index: 1;
  flex: 1;
  min-width: 0;
  background: rgba(45,35,18,0.85);
  border: 2px solid var(--border);
  border-radius: 6px;
  /* Extra top clearance (vs. 14px on every other side) so a 3-line
     nameplate (long custom name + a long title - see .nameplate) always
     has headroom above this box's own top edge, even for a boss sprite
     tall enough to leave little empty space above it in the portrait box. */
  padding: 34px 14px 14px;
  text-align: center;
  box-shadow: inset 0 0 0 1px rgba(212,175,90,0.08);
}
.combatant .portrait { display: flex; justify-content: center; align-items: flex-end; height: 108px; position: relative; }
/* max-width+height:auto (not just image-rendering) so a narrow mobile
   combatant column shrinks the sprite proportionally instead of squashing
   it sideways - a flex item with an explicit width/height SVG child only
   shrinks the main-axis dimension by default, distorting the aspect ratio. */
.combatant .portrait svg, .combatant .portrait img { image-rendering: pixelated; max-width: 100%; height: auto; }

/* A companion "row": an equipped mount walks just behind/beside the
   character (not ridden - it's its own creature, trotting along), and an
   equipped pet trots alongside on the other side, smaller. All three are a
   simple bottom-aligned flex row so they read as traveling together rather
   than overlapping. Shared between the map traveler and the combat portrait
   via renderCompanionRig() in progression.js. */
.companion-row { display: flex; align-items: flex-end; gap: 3px; min-width: 0; }
.companion-row svg, .companion-row img { display: block; image-rendering: pixelated; max-width: 100%; height: auto; }
.companion-mount svg, .companion-pet svg, .companion-mount img, .companion-pet img { filter: drop-shadow(0 2px 2px rgba(0,0,0,0.5)); }
.companion-mount { opacity: 0.92; }
.companion-mount, .companion-pet { position: relative; }

/* A recruited companion "acting" in sync with the player's own turn (see
   renderCompanionParty/renderRecruitedCompanionRig in main.js) - a quick
   lunge-and-settle, since the player can't individually direct them. */
.companion-rider.companion-pulse { animation: companionPulse 0.4s ease; }
@keyframes companionPulse {
  0%, 100% { transform: translateX(0); }
  40% { transform: translateX(4px) scale(1.08); }
}
.companion-card { opacity: 0.95; }

/* An equipped pet/mount landing its own hit alongside the player's action
   (see Combat.resolveCompanionAttacks) - a lunge toward the enemy plus a
   role-colored glow (--companion-glow, set inline per role - see
   COMPANION_ROLE_GLOW in data.js and renderCompanionRig), so a Tank/DPS/
   Healer pet reads as visually distinct from a plain one mid-fight. */
@keyframes companionAttackLunge {
  0%, 100% { transform: translateX(0); filter: drop-shadow(0 2px 2px rgba(0,0,0,0.5)); }
  40% { transform: translateX(8px) scale(1.15); filter: drop-shadow(0 0 6px var(--companion-glow, var(--accent))) drop-shadow(0 0 10px var(--companion-glow, var(--accent))) brightness(1.2); }
}
.companion-mount.companion-acting, .companion-pet.companion-acting { animation: companionAttackLunge 0.5s ease; }

/* Every character/creature sprite in this game (see CREATURE_ART_IDS,
   characterSpriteFor) is drawn facing left - correct as-is for the enemy,
   who stands on the right side of the arena facing the player, but wrong
   for the player's own side (class body, mount, pet), which would otherwise
   face away from the enemy instead of toward it - mirrored here so the two
   combatants visibly face each other. Applied to the img/svg itself rather
   than the .companion-rider/-mount/-pet wrapper so it doesn't fight the
   lunge animation's own `transform` above. */
.combatant.player .companion-rider img, .combatant.player .companion-rider svg,
.combatant.player .companion-mount img, .combatant.player .companion-mount svg,
.combatant.player .companion-pet img, .combatant.player .companion-pet svg {
  transform: scaleX(-1);
}

/* A WoW-style floating nameplate above the character's own head (not the
   mount/pet) - see renderCompanionRig() in progression.js. */
.companion-rider { position: relative; }
/* Wraps instead of a single nowrap line, and caps its width well below any
   combatant column's own width - a long custom name plus an equipped title
   (see displayCharacterName in progression.js) could otherwise render 200px+
   wide on one line, which a narrow mobile combatant column (or the mount/pet
   name tags' much narrower wrappers) can't contain, pushing part of the text
   off the actual browser viewport. Centering (left:50%/translateX(-50%)) still
   keeps it aligned above whichever sprite it's nested against - it just wraps
   to 2-3 short lines instead of drifting wide when the name is long. */
.nameplate {
  position: absolute;
  top: -8px;
  left: 50%;
  transform: translateX(-50%);
  width: max-content;
  max-width: min(120px, 38vw);
  white-space: normal;
  overflow-wrap: break-word;
  text-align: center;
  line-height: 1.15;
  /* Scales with the combatant column's own width (same min(...,vw) proxy
     the max-width above uses) instead of a fixed size, so it shrinks
     gracefully on a narrow phone instead of forcing a long name+title to
     wrap onto extra lines and push higher above the sprite. */
  font-size: clamp(0.58rem, 2.6vw, 0.68rem);
  font-weight: 700;
  color: #ffd94a;
  text-shadow: 0 1px 2px #000, 0 0 4px #000, 0 0 4px #000;
  pointer-events: none;
}
/* A named mount/pet's nameplate (see companion-mount/companion-pet above) -
   same idea, scaled down to match their smaller sprite. */
.nameplate-small { font-size: clamp(0.48rem, 2.1vw, 0.56rem); top: -6px; max-width: min(80px, 30vw); }
.combatant.enemy.elite { border-color: var(--bad); }
.combatant.enemy.boss { border-color: var(--accent); }
.combatant.enemy.spectral { border-color: var(--accent-2); box-shadow: 0 0 14px rgba(138,95,214,0.5); }
.combatant.enemy.spectral .portrait svg, .combatant.enemy.spectral .portrait img { filter: hue-rotate(200deg) saturate(1.4) brightness(1.15) drop-shadow(0 0 6px rgba(138,95,214,0.7)); opacity: 0.92; }

.panel.legendary-encounter { border-color: var(--accent); animation: legendaryPanelPulse 2.4s ease-in-out infinite; }
@keyframes legendaryPanelPulse {
  0%, 100% { box-shadow: inset 0 0 0 1px rgba(212,175,90,0.1), 0 0 12px rgba(212,175,90,0.3); }
  50% { box-shadow: inset 0 0 0 1px rgba(212,175,90,0.15), 0 0 26px rgba(212,175,90,0.6); }
}

/* Jess's encounter (see enterWitchJess in main.js) - a soft magenta/orange
   panel pulse echoing her own glow, plus the portrait itself pulsing a
   brighter, tighter glow so she visibly radiates rather than just being lit
   from outside. */
.panel.witch-jess-encounter { border-color: #e08a3c; animation: jessPanelPulse 3s ease-in-out infinite; }
@keyframes jessPanelPulse {
  0%, 100% { box-shadow: inset 0 0 0 1px rgba(224,138,60,0.12), 0 0 14px rgba(161,60,224,0.35); }
  50% { box-shadow: inset 0 0 0 1px rgba(224,138,60,0.18), 0 0 28px rgba(224,138,60,0.55); }
}
/* The 7 RARE_NPCS encounters (see enterRareNpc in main.js) - a plain,
   un-animated portrait (unlike Jess's magical glow, which is hers alone) in
   a mildly accented panel so each still reads as a special one-off. */
.panel.rare-npc-encounter { border-color: var(--accent-2); }
.npc-portrait-wrap { text-align: center; margin-bottom: 4px; }
.npc-portrait {
  width: 160px;
  height: auto;
  image-rendering: pixelated;
  filter: drop-shadow(0 4px 6px rgba(0,0,0,0.6));
}
.jess-portrait-wrap { text-align: center; margin-bottom: 4px; }
.jess-portrait {
  width: 160px;
  height: auto;
  image-rendering: pixelated;
  animation: jessGlow 2.6s ease-in-out infinite;
}
@keyframes jessGlow {
  0%, 100% { filter: brightness(1) drop-shadow(0 0 10px rgba(224,138,60,0.6)) drop-shadow(0 0 18px rgba(161,60,224,0.4)); }
  50% { filter: brightness(1.12) drop-shadow(0 0 18px rgba(224,138,60,0.85)) drop-shadow(0 0 30px rgba(161,60,224,0.6)); }
}
.gauntlet-banner {
  text-align: center;
  color: var(--accent);
  font-weight: bold;
  letter-spacing: 1px;
  font-size: 0.85rem;
  margin-bottom: 4px;
}

/* Raid party strip (the player's own portrait stays in .combat-arena as
   usual) - shows the two Ghosts standing in for real party members. */
.raid-party {
  display: flex;
  justify-content: center;
  gap: 14px;
  margin-bottom: 10px;
}
.raid-ghost {
  display: flex;
  align-items: center;
  gap: 6px;
  background: var(--bg-panel-2);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 6px 10px;
  opacity: 0.85;
}
.raid-row { border-left: 3px solid var(--accent-2); }


/* Legendary WEAPON: only the weapon's own <g> (sprite-weapon-layer, tagged
   legendary-weapon-glow) glows and shines - the rest of the character is
   untouched. Sparkles are positioned near where weapons sit in the sprite
   (upper-right quadrant), wherever it's drawn (HUD, class select, combat, Sanctuary). */
.legendary-aura { position: relative; display: inline-block; line-height: 0; }
.legendary-aura svg .legendary-weapon-glow { animation: legendaryWeaponGlow 1.6s ease-in-out infinite; transform-origin: 85% 70%; transform-box: fill-box; }
@keyframes legendaryWeaponGlow {
  0%, 100% { filter: drop-shadow(0 0 2px #d4af5a) drop-shadow(0 0 4px rgba(212,175,90,0.6)) brightness(1); transform: scale(1); }
  50% { filter: drop-shadow(0 0 5px #d4af5a) drop-shadow(0 0 11px rgba(255,224,153,0.95)) brightness(1.35); transform: scale(1.05); }
}
.legendary-sparkle {
  position: absolute;
  color: #fff2c2;
  font-size: 10px;
  text-shadow: 0 0 4px #d4af5a, 0 0 2px #fff;
  pointer-events: none;
  opacity: 0;
  animation: legendarySparkle 1.8s ease-in-out infinite;
}
.legendary-sparkle.w1 { top: 14%; right: 1%; animation-delay: 0s; }
.legendary-sparkle.w2 { top: 52%; right: -2%; animation-delay: 0.8s; }
@keyframes legendarySparkle {
  0%, 40%, 100% { opacity: 0; transform: scale(0.5) rotate(0deg); }
  20% { opacity: 1; transform: scale(1.15) rotate(15deg); }
}

.sprite-mini { height: 36px; flex-shrink: 0; display: flex; align-items: flex-end; }
.sprite-mini svg, .sprite-mini img { image-rendering: pixelated; }
.class-icon { display: flex; justify-content: center; margin-bottom: 4px; }
.class-icon svg, .class-icon img { image-rendering: pixelated; }

/* Battle animations - the wrapping .portrait element is a fresh DOM node on
   every combat re-render, so these keyframes simply autoplay on mount. */
@keyframes lungeRight { 0% { transform: translateX(0); } 40% { transform: translateX(16px) scale(1.06); } 100% { transform: translateX(0); } }
@keyframes lungeLeft { 0% { transform: translateX(0); } 40% { transform: translateX(-16px) scale(1.06); } 100% { transform: translateX(0); } }
@keyframes hitShake { 0%, 100% { transform: translateX(0); } 20% { transform: translateX(-6px); } 40% { transform: translateX(6px); } 60% { transform: translateX(-4px); } 80% { transform: translateX(4px); } }
@keyframes deathFade { 0% { opacity: 1; transform: translateY(0) rotate(0deg); } 100% { opacity: 0.25; transform: translateY(10px) rotate(8deg); } }
@keyframes healGlow { 0%, 100% { filter: brightness(1); } 50% { filter: brightness(1.7) drop-shadow(0 0 6px var(--good)); } }

.combatant.player .portrait.anim-attack { animation: lungeRight 0.5s ease; }
.combatant.enemy .portrait.anim-attack { animation: lungeLeft 0.5s ease; }
.portrait.anim-death { animation: deathFade 0.6s ease forwards; }
.portrait.anim-heal { animation: healGlow 0.5s ease; }
/* Enemies flash a red damage glow on every hit (not just crits) - a colored
   drop-shadow "halo" around the sprite's silhouette rather than recoloring
   the whole sprite, so pixel-art detail stays readable. */
.portrait.anim-hit {
  animation: hitShake 0.4s ease;
  filter: brightness(1.3) drop-shadow(0 0 6px #ff2b2b) drop-shadow(0 0 12px #ff2b2b);
}

/* Class ability ("Skill" button) glow - one shared pulse/scale keyframe,
   colored per class via --ability-glow (see classAbilityVfx in main.js).
   Falls back to the old generic accent-colored pulse if a class ever isn't
   in that mapping. */
@keyframes classAbilityGlow {
  0%, 100% { transform: scale(1); filter: brightness(1) drop-shadow(0 0 0 transparent); }
  45% { transform: scale(1.2); filter: brightness(1.55) drop-shadow(0 0 8px var(--ability-glow, var(--accent-2))) drop-shadow(0 0 16px var(--ability-glow, var(--accent-2))); }
}
.portrait[class*="anim-skill-"] { animation: classAbilityGlow 0.65s ease; }
.portrait.anim-skill-rage { --ability-glow: #ff3b3b; }
.portrait.anim-skill-holy { --ability-glow: #fff4c2; }
.portrait.anim-skill-shadow { --ability-glow: #6a3fa0; }
.portrait.anim-skill-shine { --ability-glow: #ffe680; }
.portrait.anim-skill-nature { --ability-glow: #4caf50; }
.portrait.anim-skill-fel { --ability-glow: #9b30ff; }
.portrait.anim-skill-arcane { --ability-glow: #3fa9ff; }
.portrait.anim-skill-default { --ability-glow: var(--accent-2); }

/* Basic Attack gets the same class-flavor glow as Skill, layered onto the
   lunge (see animClass in main.js) - every physical class's own Attack now
   reads distinctly instead of a plain colorless lunge. Mage/Warlock never
   carry this class (see magicBallColor) since their Attack throws a
   projectile instead of lunging at all, so there's no lunge to tint. */
.combatant.player .portrait[class*="anim-attack-"] { animation: lungeRight 0.5s ease, classAbilityGlow 0.5s ease; }
.portrait.anim-attack-rage { --ability-glow: #ff3b3b; }
.portrait.anim-attack-holy { --ability-glow: #fff4c2; }
.portrait.anim-attack-shadow { --ability-glow: #6a3fa0; }
.portrait.anim-attack-shine { --ability-glow: #ffe680; }
.portrait.anim-attack-nature { --ability-glow: #4caf50; }
.portrait.anim-attack-fel { --ability-glow: #9b30ff; }
.portrait.anim-attack-arcane { --ability-glow: #3fa9ff; }
.portrait.anim-attack-default { --ability-glow: var(--accent-2); }

/* Mage/Warlock's basic Attack throws a small glowing orb across the arena
   instead of the melee lunge (see animClass in main.js, which suppresses
   anim-attack for these two classes so this doesn't play alongside it). The
   scale/opacity spike near the end of the keyframe reads as the orb bursting
   on impact with the enemy. */
.magic-ball {
  position: absolute;
  top: 46%;
  left: 8%;
  width: 14px;
  height: 14px;
  border-radius: 50%;
  background: radial-gradient(circle, #fff 0%, var(--ball-color) 45%, transparent 75%);
  box-shadow: 0 0 8px var(--ball-color), 0 0 16px var(--ball-color);
  animation: magicBallTravel 0.55s ease-in forwards;
  pointer-events: none;
  z-index: 5;
}
@keyframes magicBallTravel {
  0% { left: 8%; opacity: 1; transform: translateY(0) scale(1); }
  65% { left: 80%; opacity: 1; transform: translateY(-4px) scale(1); }
  82% { left: 84%; opacity: 1; transform: translateY(-4px) scale(2.4); }
  100% { left: 84%; opacity: 0; transform: translateY(-4px) scale(3); }
}

/* A ranged weapon Attack (see pickAttackWeaponSlot in combat.js - only fires
   when the character actually has a bow-visual item equipped in their ranged
   slot) shoots a small fletched shaft across the arena instead of lunging,
   mirroring the magic-ball projectile's own positioning/timing convention. */
.arrow-shot {
  position: absolute;
  top: 47%;
  left: 8%;
  width: 26px;
  height: 5px;
  border-radius: 2px;
  background: linear-gradient(90deg, transparent 0%, #c9a876 45%, #8a5a2e 100%);
  box-shadow: 0 1px 2px rgba(0,0,0,0.4);
  animation: arrowTravel 0.4s ease-in forwards;
  pointer-events: none;
  z-index: 5;
}
@keyframes arrowTravel {
  0% { left: 8%; opacity: 1; transform: translateY(0) scaleX(1); }
  75% { left: 78%; opacity: 1; transform: translateY(-3px) scaleX(1); }
  100% { left: 84%; opacity: 0; transform: translateY(-3px) scaleX(1.8); }
}

/* Fireball's own projectile - warm flame gradient, a flicker layered on top
   of its travel arc, and a soft trailing glow (::after) - visually distinct
   from the plain solid-color .magic-ball every other skill/attack uses.
   Keyed to the spell being cast (see isFireballCast in renderCombatScreen),
   not the caster's class, since Fireball is equippable by any class. */
.fireball-projectile {
  position: absolute;
  top: 45%;
  left: 8%;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  background: radial-gradient(circle, #fff6c2 0%, #ffb347 35%, #ff5a1f 65%, #c2260c 85%, transparent 100%);
  box-shadow: 0 0 10px #ff7a1f, 0 0 22px #ff4400, 0 0 34px #ff2200;
  animation: fireballTravel 0.6s ease-in forwards, fireballFlicker 0.12s ease-in-out infinite;
  pointer-events: none;
  z-index: 5;
}
.fireball-projectile::after {
  content: '';
  position: absolute;
  inset: -6px;
  border-radius: 50%;
  background: radial-gradient(circle, rgba(255,150,40,0.55) 0%, transparent 70%);
  animation: fireballTrail 0.6s ease-in forwards;
}
@keyframes fireballTravel {
  0% { left: 8%; opacity: 1; transform: translateY(0) scale(1) rotate(0deg); }
  60% { left: 78%; opacity: 1; transform: translateY(-6px) scale(1.1) rotate(180deg); }
  80% { left: 84%; opacity: 1; transform: translateY(-4px) scale(2.6) rotate(220deg); }
  100% { left: 84%; opacity: 0; transform: translateY(-4px) scale(3.4) rotate(260deg); }
}
@keyframes fireballFlicker {
  0%, 100% { filter: brightness(1) saturate(1); }
  50% { filter: brightness(1.35) saturate(1.3); }
}
@keyframes fireballTrail {
  0% { opacity: 0.8; transform: scale(0.6); }
  100% { opacity: 0; transform: scale(2.2); }
}

/* Critical-hit floating combat text - WoW-style "Critical!" callout that
   pops up over the enemy's portrait and fades as it rises. */
.floating-crit {
  position: absolute;
  top: -4px;
  left: 50%;
  transform: translateX(-50%);
  color: #ff3b3b;
  font-weight: 900;
  font-size: 0.95rem;
  line-height: 1.15;
  text-align: center;
  text-shadow: 0 0 3px #000, 0 0 7px #ff3b3b;
  pointer-events: none;
  z-index: 6;
  white-space: nowrap;
  animation: floatCritUp 1.1s ease-out forwards;
}
@keyframes floatCritUp {
  0% { opacity: 0; transform: translate(-50%, 0) scale(0.6); }
  15% { opacity: 1; transform: translate(-50%, -6px) scale(1.15); }
  30% { transform: translate(-50%, -10px) scale(1); }
  100% { opacity: 0; transform: translate(-50%, -46px) scale(1); }
}

/* AUTO's small "here's what just happened" toast (see showAutoToast in
   main.js) - stands in for the full-screen victory/reward panel so a fully
   automated run never needs a click. Fixed to the viewport (not the app
   root) so it survives whatever screen re-renders underneath it. Docked to
   the BOTTOM of the viewport rather than the top - every screen this fires
   on (combat, events, world events, taming...) puts its artwork and
   dialogue/log up top and its action buttons right after, so anchoring low
   is the one spot that stays clear of both regardless of which screen is
   underneath. */
.auto-toast {
  position: fixed;
  bottom: max(14px, env(safe-area-inset-bottom, 0px));
  left: 50%;
  transform: translateX(-50%);
  max-width: 90vw;
  background: linear-gradient(180deg, var(--bg-panel) 0%, var(--bg-panel-2) 100%);
  border: 2px solid var(--accent);
  border-radius: 8px;
  padding: 10px 16px;
  text-align: center;
  box-shadow: 0 4px 16px rgba(0,0,0,0.5), 0 0 12px rgba(212,175,90,0.4);
  z-index: 300;
  animation: autoToastIn 0.25s ease-out;
  pointer-events: none;
}
.auto-toast.auto-toast-hide { animation: autoToastOut 0.4s ease-in forwards; }
@keyframes autoToastIn { from { opacity: 0; transform: translate(-50%, 12px); } to { opacity: 1; transform: translate(-50%, 0); } }
@keyframes autoToastOut { from { opacity: 1; } to { opacity: 0; transform: translate(-50%, 8px); } }

/* Kyle the Bard's in-run tutorial (see maybeShowTutorial/showKyleOverlay in
   main.js) - a full-screen tap-catcher (so ANY tap dismisses it, not just a
   close button) with his portrait and a speech-bubble-style textbox docked
   to the bottom, over whatever screen it was called from. */
.kyle-overlay {
  position: fixed; inset: 0; z-index: 500;
  background: rgba(0,0,0,0.55);
  display: flex; align-items: flex-end; justify-content: center;
  padding: 16px;
  cursor: pointer;
}
.kyle-card {
  display: flex; align-items: flex-end; gap: 10px;
  max-width: 480px;
  animation: kyleSlideUp 0.35s ease-out;
}
.kyle-portrait {
  width: 108px; height: auto;
  image-rendering: pixelated;
  filter: drop-shadow(0 4px 6px rgba(0,0,0,0.6));
  flex-shrink: 0;
}
.kyle-textbox {
  background: var(--bg-panel);
  border: 2px solid var(--accent);
  border-radius: 8px;
  padding: 12px 14px;
  box-shadow: 0 4px 16px rgba(0,0,0,0.5);
  cursor: default;
}
.kyle-name { color: var(--accent); font-weight: bold; margin-bottom: 4px; }
.kyle-textbox p { margin: 0 0 6px; font-size: 0.85rem; line-height: 1.4; }
.kyle-textbox p:last-of-type { margin-bottom: 0; }
.kyle-hint { margin-top: 8px; font-size: 0.7rem; color: var(--text-dim); text-align: right; }
@keyframes kyleSlideUp { from { transform: translateY(30px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }

.auto-toggle {
  display: flex;
  align-items: center;
  gap: 6px;
  align-self: flex-start;
  font-size: 0.78rem;
  letter-spacing: 1px;
  color: var(--text-dim);
  cursor: pointer;
  user-select: none;
  min-height: 32px;
}
.auto-toggle input { accent-color: var(--accent-2); width: 18px; height: 18px; cursor: pointer; }
.hud .auto-toggle { align-self: center; }
/* Groups Auto Combat/Auto Dialogue/Auto Shop into one compact column - as a
   single flex item the HUD's own row can wrap around it as a unit instead
   of 3 separate toggles spreading out across the row individually. */
.auto-toggle-group { display: flex; flex-direction: column; gap: 2px; align-self: center; }
.auto-toggle-group .auto-toggle { min-height: unset; align-self: flex-start; }

.combat-log {
  background: #0c0906;
  border: 1px solid var(--border);
  border-radius: 4px;
  padding: 10px 12px;
  height: 120px;
  overflow-y: auto;
  font-size: 0.85rem;
  color: var(--text-dim);
  display: flex;
  flex-direction: column;
  gap: 3px;
}

.combat-actions { display: flex; flex-wrap: wrap; gap: 8px; }
.combat-actions button {
  flex: 1;
  min-width: 100px;
  min-height: 44px;
  padding: 10px;
  border-radius: 4px;
  border: 2px solid var(--border);
  background: linear-gradient(180deg, var(--bg-panel-2), var(--bg-panel));
  color: var(--text);
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.05);
  transition: border-color 0.15s ease, transform 0.08s ease;
}
.combat-actions button:hover:not(:disabled) { border-color: var(--accent-2); transform: translateY(-1px); }
.combat-actions button:disabled { opacity: 0.35; cursor: not-allowed; transform: none; }

.item-row { display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; }
/* Previously just a 1px border on a background barely lighter than the
   panel behind it - easy to lose against the dark brown combat backdrop.
   Now matches the same defined, lifted look as the Attack/Skill/Flee
   buttons (gradient + shadow + a brighter border) so items read as clearly
   clickable, with a bit more padding/weight on the label itself. */
.item-chip {
  background: linear-gradient(180deg, var(--bg-panel-2), var(--bg-panel));
  border: 2px solid var(--border-light);
  border-radius: 8px;
  padding: 10px 16px;
  font-size: 0.85rem;
  font-weight: 600;
  color: var(--text);
  min-height: 40px;
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.06), 0 2px 4px rgba(0,0,0,0.35);
  transition: border-color 0.15s ease, transform 0.08s ease;
}
.item-chip:hover:not(:disabled) { border-color: var(--accent); transform: translateY(-1px); }
.item-chip:disabled { opacity: 0.35; cursor: not-allowed; transform: none; }

/* ---------- Game over / victory ---------- */
.result-title { font-size: 2rem; font-variant: small-caps; letter-spacing: 1px; }
.result-title.victory { color: var(--good); }
.result-title.defeat { color: var(--bad); }

.small-text { font-size: 0.78rem; color: var(--text-dim); }

/* ---------- Sanctuary (persistent hub) ---------- */
.sanctuary {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.sanctuary-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 8px;
}
.sanctuary-header h2 { margin: 0; color: var(--accent); font-variant: small-caps; letter-spacing: 1.5px; }

.bank-summary {
  display: flex;
  gap: 14px;
  font-size: 0.8rem;
  color: var(--text-dim);
  flex-wrap: wrap;
}

.class-tabs, .sanctuary-tabs {
  display: flex;
  gap: 6px;
  flex-wrap: wrap;
}

.class-tab, .tab-btn {
  background: var(--bg-panel-2);
  border: 2px solid var(--border);
  color: var(--text-dim);
  border-radius: 4px;
  padding: 8px 12px;
  font-size: 0.82rem;
  min-height: 40px;
  display: flex;
  align-items: center;
  gap: 6px;
  transition: border-color 0.15s ease;
}
.class-tab.active, .tab-btn.active { border-color: var(--accent); color: var(--text); box-shadow: inset 0 0 0 1px rgba(212,175,90,0.15); }
.tab-btn-glow { box-shadow: 0 0 8px 2px rgba(212,175,90,0.65); border-color: var(--accent); animation: tabGlowPulse 1.6s ease-in-out infinite; }
.tab-btn-cheat { border-color: #7a3b3b; color: #d98a8a; }
.tab-btn-cheat.active { border-color: #d98a8a; color: #fff; }
.save-status { margin-top: 12px; min-height: 1.2em; }
.save-spinner { display: inline-block; animation: saveSpin 0.8s linear infinite; }
@keyframes saveSpin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
.save-done { color: var(--good); animation: saveDoneFade 2.2s ease-in forwards; }
@keyframes saveDoneFade { 0%, 65% { opacity: 1; } 100% { opacity: 0; } }
@keyframes tabGlowPulse {
  0%, 100% { box-shadow: 0 0 6px 1px rgba(212,175,90,0.5); }
  50% { box-shadow: 0 0 12px 4px rgba(212,175,90,0.9); }
}

.sanctuary-xp-bar {
  display: flex;
  align-items: center;
  gap: 10px;
  background: var(--bg-panel-2);
  border: 1px solid var(--border);
  border-radius: 4px;
  padding: 6px 12px;
  margin: 4px 0;
  flex-wrap: wrap;
}
.sanctuary-xp-bar .xp-bar-wrap { flex: 1; min-width: 120px; }

.class-picker {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  background: var(--bg-panel-2);
  border: 2px solid var(--border);
  color: var(--text);
  border-radius: 4px;
  padding: 8px 10px;
  min-height: 40px;
  font-family: inherit;
  font-size: 0.9rem;
  cursor: pointer;
  text-align: left;
  transition: border-color 0.15s ease;
}
.class-picker:hover { border-color: var(--accent); }
.class-picker-label { flex: 1; }
.class-picker-caret { color: var(--text-dim); }

.house-feed { display: flex; flex-direction: column; gap: 6px; align-items: stretch; min-width: 160px; }
.house-row { flex-wrap: wrap; }
.companion-name-input {
  display: block;
  margin-top: 5px;
  background: var(--bg-panel-2);
  border: 2px solid var(--border);
  color: var(--text);
  border-radius: 4px;
  padding: 5px 8px;
  font-size: 0.8rem;
  font-family: inherit;
  width: 160px;
  max-width: 100%;
}
.companion-name-input:focus { outline: none; border-color: var(--accent); }

.sanctuary-body {
  background: var(--bg-panel);
  border: 2px solid var(--border);
  border-radius: 6px;
  padding: 16px;
  flex: 1;
  overflow-y: auto;
  max-height: 55vh;
  box-shadow: inset 0 0 0 1px rgba(212,175,90,0.08);
}
.sanctuary-body h4 { color: var(--accent); margin: 14px 0 8px; font-variant: small-caps; letter-spacing: 1px; }
.sanctuary-body h4:first-child { margin-top: 0; }

.char-info-bar h3 { margin: 0 0 6px; }
.char-info-bar .btn-secondary { margin-top: 10px; }

.char-customize {
  margin-top: 16px;
  padding-top: 14px;
  padding-bottom: 14px;
  border-top: 1px solid var(--border);
  border-bottom: 1px solid var(--border);
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.char-customize h4 { margin: 0; }
.customize-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  max-width: 320px;
}
.customize-row span { color: var(--text-dim); font-size: 0.85rem; }
.customize-row input[type="text"] {
  background: var(--bg-panel-2);
  border: 2px solid var(--border);
  color: var(--text);
  border-radius: 4px;
  padding: 7px 10px;
  font-size: 0.85rem;
  font-family: inherit;
  width: 140px;
}
.customize-row input[type="text"]:focus { outline: none; border-color: var(--accent); }
.customize-row input[type="color"] {
  width: 44px;
  height: 32px;
  padding: 0;
  border: 2px solid var(--border);
  border-radius: 4px;
  background: none;
  cursor: pointer;
}

.xp-bar-wrap {
  width: 220px;
  max-width: 100%;
  height: 10px;
  background: var(--bg-panel-2);
  border: 1px solid var(--border);
  border-radius: 4px;
  overflow: hidden;
  margin: 6px 0 2px;
  box-shadow: inset 0 1px 3px rgba(0,0,0,0.5);
}
.xp-bar-fill { height: 100%; background: linear-gradient(90deg, var(--accent-2), #a98fe8); }

.stat-grid {
  display: grid;
  grid-template-columns: repeat(2, auto);
  gap: 4px 16px;
  font-size: 0.85rem;
  color: var(--accent);
  margin: 8px 0;
}

.gear-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  background: var(--bg-panel-2);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 8px 12px;
  margin-bottom: 6px;
  gap: 10px;
  flex-wrap: wrap;
}
.gear-row .desc { display: flex; align-items: center; gap: 10px; }
.spell-icon img { image-rendering: pixelated; display: block; }
.btn-icon { image-rendering: pixelated; vertical-align: -4px; margin-right: 5px; }
.gear-icon { image-rendering: pixelated; vertical-align: -6px; }
.gear-row button { min-height: 40px; }

::-webkit-scrollbar { width: 8px; }
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }

/* ---------- Armory (WoW-style paperdoll) ---------- */
/* Column/portrait widths scale fluidly with the viewport (clamp, not a fixed
   breakpoint) so the three columns always shrink together and stay on one
   row - flex-wrap previously let the left column + portrait fit one line
   while the right column alone wrapped onto its own centered row below,
   which read as broken rather than "responsive" on real phones. nowrap
   backs that up: even the clamp() floors (218px total) fit comfortably
   inside the narrowest real phone viewport. */
.armory-layout {
  display: flex;
  align-items: flex-start;
  justify-content: center;
  gap: clamp(6px, 2vw, 10px);
  flex-wrap: nowrap;
}
.armory-column { display: flex; flex-direction: column; gap: 6px; width: clamp(62px, 21vw, 96px); flex-shrink: 0; }
/* flex:1 (not shrink-to-fit) so the character info block (see
   renderCharInfoBlock/centerExtraHtml) has real width to lay out text in,
   rather than being squeezed to the portrait's own narrow clamp width - the
   portrait and weapon row stay centered within it via align-items. */
.armory-center { display: flex; flex-direction: column; align-items: center; gap: 8px; min-width: 0; flex: 1; }
.armory-center .char-info-bar,
.armory-center .char-customize,
.armory-center .customize-row { width: 100%; max-width: 340px; }
.title-select-btn { max-width: 240px; font-size: 0.8rem; color: var(--accent); border-color: var(--accent); }
.armory-portrait {
  width: clamp(86px, 28vw, 140px);
  min-height: 150px;
  display: flex;
  align-items: flex-end;
  justify-content: center;
  background: radial-gradient(ellipse at 50% 20%, #3a2a18 0%, var(--bg-panel) 70%);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 8px;
}
.armory-portrait svg, .armory-portrait img { image-rendering: pixelated; max-width: 100%; height: auto; }
/* Always a single row, main hand/off hand/ranged side by side, on every
   viewport - a phone-width .armory-center only has ~130px to give it (the
   two equipment columns on either side eat the rest), so the per-slot floor
   here is deliberately much smaller than the equipment slots' own floor
   rather than letting the row wrap onto its own stacked column. */
.armory-weapon-row { display: flex; flex-wrap: nowrap; justify-content: center; gap: 4px; max-width: 100%; }
.armory-weapon-row .armory-slot { width: clamp(36px, 18vw, 90px); flex-shrink: 1; }
.armory-slot {
  background: var(--bg-panel-2);
  border: 2px solid var(--border);
  border-radius: 6px;
  padding: 6px 4px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  text-align: center;
  cursor: pointer;
  transition: transform 0.15s ease;
}
.armory-slot:hover { transform: translateY(-1px); }
.armory-slot-icon { font-size: 1.3rem; }
.armory-slot-label { font-size: 0.65rem; color: var(--text-dim); line-height: 1.2; }
.armory-slot-selected { box-shadow: 0 0 0 2px var(--accent); }
.armory-slot-upgrade { animation: armoryUpgradePulse 1.6s ease-in-out infinite; }
@keyframes armoryUpgradePulse {
  0%, 100% { box-shadow: 0 0 5px 1px rgba(76,175,125,0.55); }
  50% { box-shadow: 0 0 11px 3px rgba(76,175,125,0.95); }
}

/* Inline slot picker (see renderSlotPicker in main.js) + stat comparisons */
.modal-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0,0,0,0.6);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 200;
  padding: 20px;
  animation: modalFadeIn 0.15s ease-out;
}
@keyframes modalFadeIn { from { opacity: 0; } to { opacity: 1; } }
.modal-panel {
  max-width: 480px;
  width: 100%;
  max-height: 85vh;
  overflow-y: auto;
  box-shadow: 0 8px 32px rgba(0,0,0,0.5);
  animation: modalPopIn 0.18s ease-out;
}
@keyframes modalPopIn { from { transform: scale(0.95); opacity: 0; } to { transform: scale(1); opacity: 1; } }
.armory-picker { margin: 0; }

/* First-time discovery popup (see queueDiscovery/showNextDiscovery in
   main.js) - a bigger, more ceremonial reveal than the plain list modals
   above: large centered art, a kind label, lore, and a stat/ability
   summary, for the first time a pet/mount/weapon/armor/spell is obtained
   or an enemy is encountered. */
.discovery-popup { max-width: 380px; text-align: center; }
.discovery-kind {
  display: inline-block;
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--accent);
  background: rgba(212,175,90,0.12);
  border: 1px solid rgba(212,175,90,0.35);
  border-radius: 20px;
  padding: 3px 12px;
  margin-bottom: 10px;
}
.discovery-art {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 128px;
  margin-bottom: 10px;
  /* Baseline for a plain-emoji icon (the 9 hand-placed GEAR_TEMPLATES and
     every LEGENDARY_ITEMS entry use a bare emoji, not an image) - scales it
     up to fill the art area instead of rendering at normal text size. */
  font-size: 4.5rem;
  line-height: 1;
}
.discovery-art img { max-width: 100%; }
/* Gear/spell icons that ARE pre-built <img> tags (the procedural gear pool's
   gearIconImg() output, spell icons) - scaled up here rather than
   re-fetching a larger asset, since none exists at a bigger size. */
.discovery-art .gear-icon { width: 96px !important; height: 96px !important; }
.discovery-popup h3 { margin: 4px 0 2px; text-wrap: balance; }
.discovery-stats {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 6px;
  margin: 10px 0;
}
.discovery-stats span {
  background: var(--bg-panel-2);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 3px 10px;
  font-size: 0.78rem;
}
.discovery-popup .btn-primary { margin-top: 8px; }

/* ---------- Spellbook (WoW-style page, see renderSpellbookPage) ---------- */
.spellbook-page {
  background: radial-gradient(ellipse at 50% 0%, rgba(212,175,90,0.06) 0%, transparent 60%), var(--bg-panel-2);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 14px;
}
.spellbook-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(56px, 1fr));
  gap: 8px;
  margin: 10px 0 14px;
}
.spellbook-slot {
  position: relative;
  background: var(--bg-panel);
  border: 2px solid var(--border);
  border-radius: 8px;
  padding: 6px 4px 4px;
  display: flex;
  flex-direction: column;
  align-items: center;
  cursor: pointer;
  transition: transform 0.12s ease, border-color 0.12s ease;
}
.spellbook-slot:hover { transform: translateY(-2px); border-color: var(--accent-2); }
.spellbook-slot img { image-rendering: pixelated; }
.spellbook-slot-selected { border-color: var(--accent); box-shadow: 0 0 8px rgba(212,175,90,0.5); }
.spellbook-slot-active { background: rgba(212,175,90,0.1); }
.spellbook-slot-level {
  font-size: 0.62rem;
  color: var(--text-dim);
  margin-top: 2px;
}
.spellbook-slot-innate {
  position: absolute;
  top: -6px;
  right: -4px;
  font-size: 0.75rem;
  color: var(--accent);
  text-shadow: 0 1px 2px #000;
}
.spellbook-detail {
  display: flex;
  align-items: center;
  gap: 12px;
  background: var(--bg-panel);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 10px 14px;
}
.spellbook-detail-icon { image-rendering: pixelated; flex-shrink: 0; border-radius: 6px; border: 2px solid var(--border); }
.spellbook-detail-text { flex: 1; min-width: 0; }
.spellbook-detail-text h4 { margin: 0 0 2px; }
.spellbook-detail-text p { margin: 0; }
@media (max-width: 480px) {
  .spellbook-detail { flex-wrap: wrap; }
  .spellbook-detail .btn-secondary { width: 100%; }
}

.tutorial-steps { display: flex; flex-direction: column; gap: 12px; margin-top: 10px; }
.tutorial-step { display: flex; gap: 10px; align-items: flex-start; }
.tutorial-step-icon { font-size: 1.4rem; flex-shrink: 0; line-height: 1.3; }
.stat-compare { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 4px; font-size: 0.78rem; }
.stat-compare .stat-better { color: var(--good); }
.stat-compare .stat-worse { color: var(--bad); }
.stat-compare .stat-equal { color: var(--text-dim); }

@media (max-width: 480px) {
  .armory-portrait { min-height: 110px; }
}

/* ---------- Mobile ---------- */
@media (max-width: 480px) {
  #app { padding: 10px; }
  .panel { padding: 16px; }
  .combat-arena { gap: 10px; }
  .combatant { padding: 10px; }
  .class-card { width: 100%; max-width: 320px; }
  .hp-bar-wrap { width: 100px; }
  .combat-actions button { min-width: 90px; font-size: 0.92rem; }
  .stat-grid { grid-template-columns: 1fr; }
  .sanctuary-header { flex-direction: column; align-items: flex-start; }
  .bank-summary { font-size: 0.75rem; gap: 10px; }
  .btn-primary, .btn-secondary { width: 100%; }
  .center-screen .btn-primary, .center-screen .btn-secondary { max-width: 320px; }
}
