← Aethelgard results

Aethelgard

Grok 4.5 · legacy

Initial view of Aethelgard

Reviews & generation details

Reviews

No human review yet. Unreviewed does not mean unsuccessful.

Generation context

Imported historical artifact. Its exact original instructions and lineage are not known.

Generation prompt · exact text unavailable

See the current prompt for reference. It may differ from the instructions used to generate this artifact.

Inspect original source 111,413 bytes · SHA-256 afaec3671335
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Aethelgard — Resonant Conductors</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Press+Start+2P&display=swap" rel="stylesheet">
<script>
tailwind.config = {
  theme: {
    extend: {
      fontFamily: {
        inter: ['Inter', 'sans-serif'],
        retro: ['"Press Start 2P"', 'monospace']
      },
      colors: {
        aether: {
          900: '#0b0a14',
          800: '#12111f',
          700: '#1a1830',
          600: '#252240',
          500: '#3d3860'
        }
      }
    }
  }
}
</script>
<style>
:root {
  --pyro: #ff6b35; --hydro: #3b9eff; --flora: #4ade80;
  --zephyr: #a5f3fc; --earth: #d4a373; --volt: #fde047;
  --cosmos: #c084fc;
}
* { box-sizing: border-box; }
body {
  font-family: 'Inter', sans-serif;
  background: radial-gradient(ellipse at 20% 20%, #1a1035 0%, #0b0a14 50%),
              radial-gradient(ellipse at 80% 80%, #0f1a2e 0%, #0b0a14 50%);
  background-color: #0b0a14;
  min-height: 100vh;
  overflow-x: hidden;
  color: #e2e8f0;
}
.glass {
  background: rgba(255,255,255,0.05);
  backdrop-filter: blur(16px);
  -webkit-backdrop-filter: blur(16px);
  border: 1px solid rgba(255,255,255,0.1);
  box-shadow: 0 8px 32px rgba(0,0,0,0.4);
}
.glass-strong {
  background: rgba(15,12,30,0.85);
  backdrop-filter: blur(24px);
  -webkit-backdrop-filter: blur(24px);
  border: 1px solid rgba(255,255,255,0.12);
  box-shadow: 0 12px 40px rgba(0,0,0,0.5);
}
.neon-pyro { border-color: var(--pyro); box-shadow: 0 0 12px rgba(255,107,53,0.4), inset 0 0 12px rgba(255,107,53,0.05); }
.neon-hydro { border-color: var(--hydro); box-shadow: 0 0 12px rgba(59,158,255,0.4), inset 0 0 12px rgba(59,158,255,0.05); }
.neon-flora { border-color: var(--flora); box-shadow: 0 0 12px rgba(74,222,128,0.4), inset 0 0 12px rgba(74,222,128,0.05); }
.neon-zephyr { border-color: var(--zephyr); box-shadow: 0 0 12px rgba(165,243,252,0.4), inset 0 0 12px rgba(165,243,252,0.05); }
.neon-earth { border-color: var(--earth); box-shadow: 0 0 12px rgba(212,163,115,0.4), inset 0 0 12px rgba(212,163,115,0.05); }
.neon-volt { border-color: var(--volt); box-shadow: 0 0 12px rgba(253,224,71,0.4), inset 0 0 12px rgba(253,224,71,0.05); }
.neon-cosmos { border-color: var(--cosmos); box-shadow: 0 0 12px rgba(192,132,252,0.4), inset 0 0 12px rgba(192,132,252,0.05); }
.text-pyro { color: var(--pyro); } .text-hydro { color: var(--hydro); }
.text-flora { color: var(--flora); } .text-zephyr { color: var(--zephyr); }
.text-earth { color: var(--earth); } .text-volt { color: var(--volt); }
.text-cosmos { color: var(--cosmos); }
.bg-pyro { background: var(--pyro); } .bg-hydro { background: var(--hydro); }
.bg-flora { background: var(--flora); } .bg-zephyr { background: var(--zephyr); }
.bg-earth { background: var(--earth); } .bg-volt { background: var(--volt); }
.bg-cosmos { background: var(--cosmos); }
.screen { display: none; opacity: 0; transition: opacity 0.35s ease; }
.screen.active { display: flex; opacity: 1; animation: fadeIn 0.4s ease; }
@keyframes fadeIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
@keyframes bounceSoft {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(-8px); }
}
.bounce-active { animation: bounceSoft 1.4s ease-in-out infinite; }
@keyframes shake {
  0%, 100% { transform: translateX(0); }
  10%, 30%, 50%, 70%, 90% { transform: translateX(-6px); }
  20%, 40%, 60%, 80% { transform: translateX(6px); }
}
.shake { animation: shake 0.4s ease; }
@keyframes flashHit {
  0% { filter: brightness(1); }
  30% { filter: brightness(2.5) saturate(1.5); }
  100% { filter: brightness(1); }
}
.flash-hit { animation: flashHit 0.35s ease; }
@keyframes pulseGlow {
  0%, 100% { box-shadow: 0 0 8px currentColor; }
  50% { box-shadow: 0 0 20px currentColor, 0 0 40px currentColor; }
}
.hp-bar { transition: width 0.6s ease, background-color 0.4s ease; }
.xp-bar { transition: width 0.4s ease; }
.btn-game {
  transition: all 0.2s ease;
  position: relative;
  overflow: hidden;
}
.btn-game:hover:not(:disabled) {
  transform: translateY(-2px);
  filter: brightness(1.15);
}
.btn-game:active:not(:disabled) { transform: translateY(0); }
.btn-game:disabled { opacity: 0.4; cursor: not-allowed; }
.node-dot {
  transition: all 0.25s ease;
  cursor: pointer;
}
.node-dot:hover:not(.locked) {
  transform: scale(1.25);
  filter: brightness(1.3);
}
.node-dot.locked { opacity: 0.35; cursor: not-allowed; }
.node-dot.visited { opacity: 0.7; }
.node-dot.current { animation: pulseGlow 1.5s ease infinite; }
.zone-path {
  stroke: rgba(255,255,255,0.15);
  stroke-width: 2;
  stroke-dasharray: 6 4;
  fill: none;
}
.particle {
  position: absolute;
  border-radius: 50%;
  pointer-events: none;
  animation: drift 2s ease-out forwards;
}
@keyframes drift {
  0% { opacity: 1; transform: translate(0,0) scale(1); }
  100% { opacity: 0; transform: translate(var(--dx), var(--dy)) scale(0.2); }
}
.modal-overlay {
  background: rgba(0,0,0,0.65);
  backdrop-filter: blur(6px);
}
.scroll-thin::-webkit-scrollbar { width: 6px; }
.scroll-thin::-webkit-scrollbar-track { background: transparent; }
.scroll-thin::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.15); border-radius: 3px; }
.dex-card { transition: transform 0.2s, box-shadow 0.2s; }
.dex-card:hover { transform: translateY(-4px); }
.type-badge {
  font-size: 0.65rem;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  font-weight: 700;
  padding: 2px 8px;
  border-radius: 999px;
  border: 1px solid currentColor;
  background: rgba(0,0,0,0.3);
}
.battle-log { font-size: 0.8rem; line-height: 1.5; }
.battle-log .log-entry { animation: fadeIn 0.3s ease; padding: 2px 0; }
.title-glow {
  text-shadow: 0 0 20px rgba(192,132,252,0.6), 0 0 40px rgba(192,132,252,0.3);
}
.menu-bg-orbs {
  position: fixed; inset: 0; overflow: hidden; pointer-events: none; z-index: 0;
}
.orb {
  position: absolute; border-radius: 50%;
  filter: blur(60px); opacity: 0.25;
  animation: orbFloat 12s ease-in-out infinite alternate;
}
@keyframes orbFloat {
  0% { transform: translate(0,0) scale(1); }
  100% { transform: translate(30px,-40px) scale(1.15); }
}
#toast {
  position: fixed; top: 1.25rem; left: 50%; transform: translateX(-50%) translateY(-120%);
  z-index: 9999; transition: transform 0.35s cubic-bezier(0.34,1.56,0.64,1);
  pointer-events: none;
}
#toast.show { transform: translateX(-50%) translateY(0); }
.capture-shake {
  animation: captureShake 0.5s ease infinite;
}
@keyframes captureShake {
  0%, 100% { transform: rotate(0deg); }
  25% { transform: rotate(-12deg); }
  75% { transform: rotate(12deg); }
}
.stat-bar-bg { background: rgba(255,255,255,0.08); border-radius: 4px; height: 6px; overflow: hidden; }
.stat-bar-fill { height: 100%; border-radius: 4px; transition: width 0.4s ease; }
@media (max-width: 768px) {
  .font-retro { font-size: 0.55rem !important; letter-spacing: 0; }
}
</style>
</head>
<body class="font-inter">
<div id="toast" class="glass-strong px-5 py-3 rounded-xl text-sm font-semibold"></div>

<div class="menu-bg-orbs">
  <div class="orb" style="width:300px;height:300px;background:#7c3aed;top:10%;left:5%;"></div>
  <div class="orb" style="width:250px;height:250px;background:#2563eb;top:60%;right:10%;animation-delay:-4s;"></div>
  <div class="orb" style="width:200px;height:200px;background:#dc2626;bottom:10%;left:40%;animation-delay:-8s;"></div>
</div>

<div id="app" class="relative z-10 min-h-screen flex flex-col">

  <!-- ========== MAIN MENU ========== -->
  <div id="screen-menu" class="screen active flex-col items-center justify-center min-h-screen p-6">
    <div class="text-center mb-10">
      <div class="text-xs text-violet-400 tracking-[0.3em] uppercase mb-3 font-semibold">Elemental Frequencies Awaken</div>
      <h1 class="font-retro text-2xl md:text-4xl title-glow text-white mb-4 leading-relaxed">AETHELGARD</h1>
      <p class="text-slate-400 text-sm md:text-base max-w-md mx-auto">Resonant spirits await their Conductor. Tune the frequencies. Capture the unknown. Conquer the Apex.</p>
    </div>
    <div class="glass-strong rounded-2xl p-8 w-full max-w-sm space-y-3">
      <button onclick="Game.showScreen('world')" class="btn-game w-full py-3.5 rounded-xl bg-gradient-to-r from-violet-600 to-indigo-600 text-white font-bold text-sm tracking-wide shadow-lg shadow-violet-900/50" onmouseenter="SFX.hover()">
        ▶ Continue Journey
      </button>
      <button onclick="Game.newGame()" class="btn-game w-full py-3.5 rounded-xl glass text-white font-semibold text-sm" onmouseenter="SFX.hover()">
        ✦ New Conductor
      </button>
      <button onclick="Game.showScreen('dex')" class="btn-game w-full py-3 rounded-xl glass text-slate-300 font-medium text-sm" onmouseenter="SFX.hover()">
        📖 Conductor's Dex
      </button>
      <button onclick="Game.showShop()" class="btn-game w-full py-3 rounded-xl glass text-slate-300 font-medium text-sm" onmouseenter="SFX.hover()">
        🏪 Camp Shop
      </button>
      <button onclick="Game.resetProgress()" class="btn-game w-full py-2.5 rounded-xl text-red-400/70 hover:text-red-400 text-xs font-medium mt-2" onmouseenter="SFX.hover()">
        Reset Progress
      </button>
    </div>
    <div class="mt-6 text-xs text-slate-600">v1.0 · Aethelgard Resonants</div>
  </div>

  <!-- ========== OVERWORLD ========== -->
  <div id="screen-world" class="screen flex-col min-h-screen">
    <header class="glass-strong sticky top-0 z-50 px-4 py-3 flex items-center justify-between gap-3">
      <button onclick="Game.showScreen('menu')" class="btn-game text-slate-400 hover:text-white text-sm px-2" onmouseenter="SFX.hover()">☰</button>
      <div class="flex items-center gap-4 text-xs md:text-sm">
        <span class="text-amber-300 font-semibold" id="hud-shards">◆ 0</span>
        <span class="text-slate-400" id="hud-zone">Verdant Canopy</span>
      </div>
      <div class="flex gap-2">
        <button onclick="Game.showScreen('party')" class="btn-game glass px-3 py-1.5 rounded-lg text-xs font-semibold" onmouseenter="SFX.hover()">Party</button>
        <button onclick="Game.showScreen('dex')" class="btn-game glass px-3 py-1.5 rounded-lg text-xs font-semibold" onmouseenter="SFX.hover()">Dex</button>
        <button onclick="Game.showShop()" class="btn-game glass px-3 py-1.5 rounded-lg text-xs font-semibold" onmouseenter="SFX.hover()">Shop</button>
      </div>
    </header>

    <div class="flex-1 p-4 md:p-6 max-w-4xl mx-auto w-full">
      <div class="flex gap-2 mb-4 overflow-x-auto pb-1">
        <button data-zone="0" class="zone-tab btn-game glass px-4 py-2 rounded-xl text-xs font-bold whitespace-nowrap neon-flora" onclick="Game.selectZone(0)" onmouseenter="SFX.hover()">🌲 Canopy</button>
        <button data-zone="1" class="zone-tab btn-game glass px-4 py-2 rounded-xl text-xs font-bold whitespace-nowrap" onclick="Game.selectZone(1)" onmouseenter="SFX.hover()">🌊 Grotto</button>
        <button data-zone="2" class="zone-tab btn-game glass px-4 py-2 rounded-xl text-xs font-bold whitespace-nowrap" onclick="Game.selectZone(2)" onmouseenter="SFX.hover()">🌋 Crags</button>
        <button data-zone="3" class="zone-tab btn-game glass px-4 py-2 rounded-xl text-xs font-bold whitespace-nowrap" onclick="Game.selectZone(3)" onmouseenter="SFX.hover()">⛰️ Apex</button>
      </div>

      <div class="glass-strong rounded-2xl p-4 md:p-6 mb-4">
        <h2 class="font-retro text-xs md:text-sm text-white mb-1" id="zone-title">The Verdant Canopy</h2>
        <p class="text-slate-400 text-xs mb-4" id="zone-desc">Lush rainforest humming with Flora and Zephyr frequencies.</p>
        <div class="relative w-full" style="height:340px;" id="map-container">
          <svg id="map-svg" class="absolute inset-0 w-full h-full" viewBox="0 0 600 340" preserveAspectRatio="xMidYMid meet"></svg>
          <div id="map-nodes" class="absolute inset-0"></div>
        </div>
      </div>

      <div class="glass rounded-xl p-4">
        <h3 class="text-xs font-bold text-slate-400 uppercase tracking-wider mb-3">Active Party</h3>
        <div id="party-preview" class="flex gap-3 overflow-x-auto"></div>
      </div>
    </div>
  </div>

  <!-- ========== BATTLE ========== -->
  <div id="screen-battle" class="screen flex-col min-h-screen">
    <div class="flex-1 flex flex-col max-w-3xl mx-auto w-full p-3 md:p-4" id="battle-container">
      <!-- Enemy -->
      <div class="glass-strong rounded-2xl p-4 mb-3" id="enemy-panel">
        <div class="flex items-start justify-between gap-3">
          <div class="flex-1 min-w-0">
            <div class="flex items-center gap-2 mb-1">
              <span class="font-bold text-white text-sm md:text-base" id="enemy-name">Wild Emberkit</span>
              <span class="text-xs text-slate-400" id="enemy-level">Lv.3</span>
              <span class="type-badge" id="enemy-type">Pyro</span>
            </div>
            <div class="flex items-center gap-2 mb-1">
              <div class="flex-1 h-3 rounded-full bg-black/40 overflow-hidden">
                <div class="hp-bar h-full rounded-full bg-emerald-400" id="enemy-hp-bar" style="width:100%"></div>
              </div>
              <span class="text-xs text-slate-400 font-mono w-20 text-right" id="enemy-hp-text">30/30</span>
            </div>
            <div id="enemy-status" class="flex gap-1 flex-wrap min-h-[18px]"></div>
          </div>
          <div id="enemy-sprite" class="w-24 h-24 md:w-28 md:h-28 flex-shrink-0 bounce-active"></div>
        </div>
      </div>

      <!-- Battle Log -->
      <div class="glass rounded-xl p-3 mb-3 h-24 overflow-y-auto scroll-thin battle-log" id="battle-log"></div>

      <!-- Player Resonant -->
      <div class="glass-strong rounded-2xl p-4 mb-3" id="player-panel">
        <div class="flex items-start justify-between gap-3">
          <div id="player-sprite" class="w-24 h-24 md:w-28 md:h-28 flex-shrink-0 bounce-active"></div>
          <div class="flex-1 min-w-0">
            <div class="flex items-center gap-2 mb-1 justify-end">
              <span class="type-badge" id="player-type">Flora</span>
              <span class="text-xs text-slate-400" id="player-level">Lv.5</span>
              <span class="font-bold text-white text-sm md:text-base" id="player-name">Seedling</span>
            </div>
            <div class="flex items-center gap-2 mb-1">
              <span class="text-xs text-slate-400 font-mono w-20" id="player-hp-text">45/45</span>
              <div class="flex-1 h-3 rounded-full bg-black/40 overflow-hidden">
                <div class="hp-bar h-full rounded-full bg-emerald-400" id="player-hp-bar" style="width:100%"></div>
              </div>
            </div>
            <div class="flex items-center gap-2 mb-1">
              <span class="text-[10px] text-violet-400 w-20">XP</span>
              <div class="flex-1 h-1.5 rounded-full bg-black/40 overflow-hidden">
                <div class="xp-bar h-full rounded-full bg-violet-500" id="player-xp-bar" style="width:40%"></div>
              </div>
            </div>
            <div id="player-status" class="flex gap-1 flex-wrap justify-end min-h-[18px]"></div>
          </div>
        </div>
      </div>

      <!-- Actions -->
      <div id="battle-actions" class="glass-strong rounded-2xl p-3">
        <div id="action-main" class="grid grid-cols-2 gap-2">
          <button onclick="Battle.showMoves()" class="btn-game py-3 rounded-xl bg-gradient-to-r from-rose-600 to-orange-600 text-white font-bold text-sm" onmouseenter="SFX.hover()">⚔ Attack</button>
          <button onclick="Battle.showPrisms()" class="btn-game py-3 rounded-xl bg-gradient-to-r from-cyan-600 to-blue-600 text-white font-bold text-sm" id="btn-capture" onmouseenter="SFX.hover()">💎 Prism</button>
          <button onclick="Battle.showExchange()" class="btn-game py-3 rounded-xl glass text-white font-semibold text-sm" onmouseenter="SFX.hover()">🔄 Exchange</button>
          <button onclick="Battle.showItems()" class="btn-game py-3 rounded-xl glass text-white font-semibold text-sm" onmouseenter="SFX.hover()">🎒 Inventory</button>
        </div>
        <div id="action-moves" class="hidden grid grid-cols-1 sm:grid-cols-2 gap-2"></div>
        <div id="action-prisms" class="hidden grid grid-cols-1 gap-2"></div>
        <div id="action-exchange" class="hidden grid grid-cols-1 gap-2"></div>
        <div id="action-items" class="hidden grid grid-cols-1 gap-2"></div>
        <button id="btn-back" onclick="Battle.showMain()" class="hidden btn-game mt-2 w-full py-2 rounded-lg text-xs text-slate-400 hover:text-white" onmouseenter="SFX.hover()">← Back</button>
      </div>
    </div>
  </div>

  <!-- ========== PARTY ========== -->
  <div id="screen-party" class="screen flex-col min-h-screen">
    <header class="glass-strong sticky top-0 z-50 px-4 py-3 flex items-center gap-3">
      <button onclick="Game.showScreen('world')" class="btn-game text-slate-400 hover:text-white text-sm" onmouseenter="SFX.hover()">← Back</button>
      <h2 class="font-retro text-xs text-white">PARTY</h2>
      <span class="ml-auto text-xs text-slate-400" id="party-count">0/3 Active</span>
    </header>
    <div class="flex-1 p-4 max-w-2xl mx-auto w-full">
      <div class="mb-4">
        <h3 class="text-xs font-bold text-slate-400 uppercase tracking-wider mb-2">Active Party (max 3)</h3>
        <div id="party-active" class="space-y-3"></div>
      </div>
      <div>
        <h3 class="text-xs font-bold text-slate-400 uppercase tracking-wider mb-2">Resonance Vault</h3>
        <div id="party-vault" class="space-y-2"></div>
      </div>
    </div>
  </div>

  <!-- ========== DEX ========== -->
  <div id="screen-dex" class="screen flex-col min-h-screen">
    <header class="glass-strong sticky top-0 z-50 px-4 py-3 flex items-center gap-3">
      <button onclick="Game.showScreen(Player.party.length ? 'world' : 'menu')" class="btn-game text-slate-400 hover:text-white text-sm" onmouseenter="SFX.hover()">← Back</button>
      <h2 class="font-retro text-xs text-white">CONDUCTOR'S DEX</h2>
      <span class="ml-auto text-xs text-slate-400" id="dex-count">Seen 0/18</span>
    </header>
    <div class="flex-1 p-4 max-w-4xl mx-auto w-full">
      <div class="flex flex-wrap gap-2 mb-4" id="dex-filters">
        <button onclick="Game.filterDex('all')" class="btn-game glass px-3 py-1 rounded-lg text-xs font-bold active-filter" data-filter="all" onmouseenter="SFX.hover()">All</button>
        <button onclick="Game.filterDex('Pyro')" class="btn-game glass px-3 py-1 rounded-lg text-xs font-bold text-pyro" data-filter="Pyro" onmouseenter="SFX.hover()">Pyro</button>
        <button onclick="Game.filterDex('Hydro')" class="btn-game glass px-3 py-1 rounded-lg text-xs font-bold text-hydro" data-filter="Hydro" onmouseenter="SFX.hover()">Hydro</button>
        <button onclick="Game.filterDex('Flora')" class="btn-game glass px-3 py-1 rounded-lg text-xs font-bold text-flora" data-filter="Flora" onmouseenter="SFX.hover()">Flora</button>
        <button onclick="Game.filterDex('Zephyr')" class="btn-game glass px-3 py-1 rounded-lg text-xs font-bold text-zephyr" data-filter="Zephyr" onmouseenter="SFX.hover()">Zephyr</button>
        <button onclick="Game.filterDex('Earth')" class="btn-game glass px-3 py-1 rounded-lg text-xs font-bold text-earth" data-filter="Earth" onmouseenter="SFX.hover()">Earth</button>
        <button onclick="Game.filterDex('Volt')" class="btn-game glass px-3 py-1 rounded-lg text-xs font-bold text-volt" data-filter="Volt" onmouseenter="SFX.hover()">Volt</button>
      </div>
      <div id="dex-grid" class="grid grid-cols-2 md:grid-cols-3 gap-3"></div>
    </div>
  </div>

  <!-- ========== SHOP MODAL ========== -->
  <div id="modal-shop" class="fixed inset-0 z-[100] hidden items-center justify-center p-4 modal-overlay">
    <div class="glass-strong rounded-2xl p-6 w-full max-w-md max-h-[85vh] overflow-y-auto scroll-thin">
      <div class="flex items-center justify-between mb-4">
        <h2 class="font-retro text-xs text-white">CAMP SHOP</h2>
        <button onclick="Game.closeShop()" class="text-slate-400 hover:text-white text-lg" onmouseenter="SFX.hover()">✕</button>
      </div>
      <p class="text-amber-300 text-sm font-semibold mb-4" id="shop-shards">◆ 0 Aether Shards</p>
      <div id="shop-items" class="space-y-3"></div>
    </div>
  </div>

  <!-- ========== DIALOG MODAL ========== -->
  <div id="modal-dialog" class="fixed inset-0 z-[100] hidden items-center justify-center p-4 modal-overlay">
    <div class="glass-strong rounded-2xl p-6 w-full max-w-md">
      <div id="dialog-portrait" class="w-16 h-16 mx-auto mb-3"></div>
      <h3 class="font-bold text-center text-white mb-2" id="dialog-title">Gatekeeper</h3>
      <p class="text-slate-300 text-sm text-center mb-5 leading-relaxed" id="dialog-text"></p>
      <div id="dialog-actions" class="flex gap-2 justify-center"></div>
    </div>
  </div>

  <!-- ========== STARTER SELECT ========== -->
  <div id="modal-starter" class="fixed inset-0 z-[100] hidden items-center justify-center p-4 modal-overlay">
    <div class="glass-strong rounded-2xl p-6 w-full max-w-lg">
      <h2 class="font-retro text-xs text-center text-white mb-2">CHOOSE YOUR FIRST RESONANT</h2>
      <p class="text-slate-400 text-xs text-center mb-6">The frequencies call to you. Select a companion.</p>
      <div class="grid grid-cols-3 gap-3" id="starter-options"></div>
    </div>
  </div>

  <!-- ========== RESULT MODAL ========== -->
  <div id="modal-result" class="fixed inset-0 z-[100] hidden items-center justify-center p-4 modal-overlay">
    <div class="glass-strong rounded-2xl p-6 w-full max-w-sm text-center">
      <div id="result-icon" class="text-4xl mb-3"></div>
      <h3 class="font-bold text-white text-lg mb-2" id="result-title"></h3>
      <p class="text-slate-300 text-sm mb-4" id="result-text"></p>
      <div id="result-rewards" class="text-xs text-slate-400 mb-4 space-y-1"></div>
      <button onclick="Game.closeResult()" class="btn-game w-full py-3 rounded-xl bg-gradient-to-r from-violet-600 to-indigo-600 text-white font-bold text-sm" onmouseenter="SFX.hover()">Continue</button>
    </div>
  </div>

  <!-- ========== DETAILS MODAL ========== -->
  <div id="modal-details" class="fixed inset-0 z-[100] hidden items-center justify-center p-4 modal-overlay">
    <div class="glass-strong rounded-2xl p-6 w-full max-w-md max-h-[90vh] overflow-y-auto scroll-thin">
      <div class="flex justify-end"><button onclick="Game.closeDetails()" class="text-slate-400 hover:text-white" onmouseenter="SFX.hover()">✕</button></div>
      <div id="details-content"></div>
    </div>
  </div>

</div>

<script>
// ============================================================
// WEB AUDIO SYNTH
// ============================================================
const SFX = {
  ctx: null,
  init() {
    if (!this.ctx) this.ctx = new (window.AudioContext || window.webkitAudioContext)();
    if (this.ctx.state === 'suspended') this.ctx.resume();
  },
  tone(freq, dur, type = 'square', vol = 0.08, slide = 0) {
    this.init();
    const t = this.ctx.currentTime;
    const o = this.ctx.createOscillator();
    const g = this.ctx.createGain();
    o.type = type;
    o.frequency.setValueAtTime(freq, t);
    if (slide) o.frequency.linearRampToValueAtTime(freq + slide, t + dur);
    g.gain.setValueAtTime(vol, t);
    g.gain.exponentialRampToValueAtTime(0.001, t + dur);
    o.connect(g); g.connect(this.ctx.destination);
    o.start(t); o.stop(t + dur);
  },
  hover() { this.tone(600, 0.04, 'sine', 0.03); },
  select() { this.tone(440, 0.06, 'square', 0.06); this.tone(660, 0.08, 'square', 0.04); },
  hitPhysical() { this.tone(150, 0.1, 'sawtooth', 0.1, -80); this.tone(80, 0.15, 'square', 0.06); },
  hitElemental() {
    this.tone(520, 0.08, 'square', 0.08);
    setTimeout(() => this.tone(780, 0.1, 'square', 0.07), 40);
    setTimeout(() => this.tone(1040, 0.12, 'sine', 0.05), 80);
  },
  levelUp() {
    [523, 659, 784, 1047].forEach((f, i) => setTimeout(() => this.tone(f, 0.15, 'square', 0.07), i * 100));
  },
  capture() {
    [400, 500, 600, 800, 1000].forEach((f, i) => setTimeout(() => this.tone(f, 0.12, 'sine', 0.06), i * 80));
  },
  fail() { this.tone(200, 0.2, 'sawtooth', 0.08, -100); },
  heal() { this.tone(440, 0.1, 'sine', 0.05); this.tone(660, 0.15, 'sine', 0.04); }
};

// ============================================================
// TYPE CHART
// ============================================================
const TYPE_CHART = {
  Pyro:   { Flora: 2, Hydro: 0.5, Pyro: 0.5 },
  Hydro:  { Pyro: 2, Flora: 0.5, Hydro: 0.5 },
  Flora:  { Hydro: 2, Pyro: 0.5, Flora: 0.5 },
  Zephyr: { Earth: 2, Volt: 0.5, Zephyr: 0.5 },
  Earth:  { Volt: 2, Zephyr: 0.5, Earth: 0.5 },
  Volt:   { Zephyr: 2, Earth: 0.5, Volt: 0.5 },
  Cosmos: {}
};

function typeMod(atkType, defType) {
  return (TYPE_CHART[atkType] && TYPE_CHART[atkType][defType]) || 1;
}

const ELEMENT_COLORS = {
  Pyro: '#ff6b35', Hydro: '#3b9eff', Flora: '#4ade80',
  Zephyr: '#a5f3fc', Earth: '#d4a373', Volt: '#fde047', Cosmos: '#c084fc'
};

// ============================================================
// MOVES DATABASE
// ============================================================
const MOVES = {
  spark_snap:     { name: 'Spark Snap', power: 40, accuracy: 100, element: 'Pyro', effect: null },
  flame_sever:    { name: 'Flame Sever', power: 65, accuracy: 95, element: 'Pyro', effect: null },
  inferno_crash:  { name: 'Inferno Crash', power: 90, accuracy: 85, element: 'Pyro', effect: null },
  ash_veil:       { name: 'Ash Veil', power: 30, accuracy: 100, element: 'Pyro', effect: 'atkDown' },
  solar_burst:    { name: 'Solar Burst', power: 110, accuracy: 80, element: 'Pyro', effect: null },
  ember_coil:     { name: 'Ember Coil', power: 50, accuracy: 100, element: 'Pyro', effect: null },

  aqua_dart:      { name: 'Aqua Dart', power: 40, accuracy: 100, element: 'Hydro', effect: null },
  tide_lash:      { name: 'Tide Lash', power: 65, accuracy: 95, element: 'Hydro', effect: null },
  maelstrom:      { name: 'Maelstrom', power: 90, accuracy: 85, element: 'Hydro', effect: null },
  mist_shroud:    { name: 'Mist Shroud', power: 30, accuracy: 100, element: 'Hydro', effect: 'spdDown' },
  abyssal_crush:  { name: 'Abyssal Crush', power: 110, accuracy: 80, element: 'Hydro', effect: null },
  ripple_wave:    { name: 'Ripple Wave', power: 50, accuracy: 100, element: 'Hydro', effect: null },

  vine_whip:      { name: 'Vine Whip', power: 40, accuracy: 100, element: 'Flora', effect: null },
  thorn_barrage:  { name: 'Thorn Barrage', power: 65, accuracy: 95, element: 'Flora', effect: null },
  bloom_crash:    { name: 'Bloom Crash', power: 90, accuracy: 85, element: 'Flora', effect: null },
  pollen_haze:    { name: 'Pollen Haze', power: 30, accuracy: 100, element: 'Flora', effect: 'defDown' },
  world_root:     { name: 'World Root', power: 110, accuracy: 80, element: 'Flora', effect: null },
  seed_burst:     { name: 'Seed Burst', power: 50, accuracy: 100, element: 'Flora', effect: null },

  gust_cut:       { name: 'Gust Cut', power: 40, accuracy: 100, element: 'Zephyr', effect: null },
  cyclone_edge:   { name: 'Cyclone Edge', power: 65, accuracy: 95, element: 'Zephyr', effect: null },
  sky_rend:       { name: 'Sky Rend', power: 90, accuracy: 85, element: 'Zephyr', effect: null },
  wind_shear:     { name: 'Wind Shear', power: 30, accuracy: 100, element: 'Zephyr', effect: 'spdDown' },
  aether_tempest: { name: 'Aether Tempest', power: 110, accuracy: 80, element: 'Zephyr', effect: null },
  breeze_slash:   { name: 'Breeze Slash', power: 50, accuracy: 100, element: 'Zephyr', effect: null },

  pebble_shot:    { name: 'Pebble Shot', power: 40, accuracy: 100, element: 'Earth', effect: null },
  quake_spike:    { name: 'Quake Spike', power: 65, accuracy: 95, element: 'Earth', effect: null },
  tectonic_slam:  { name: 'Tectonic Slam', power: 90, accuracy: 85, element: 'Earth', effect: null },
  stone_guard:    { name: 'Stone Guard', power: 30, accuracy: 100, element: 'Earth', effect: 'defUp' },
  continental:    { name: 'Continental Crush', power: 110, accuracy: 80, element: 'Earth', effect: null },
  dust_burst:     { name: 'Dust Burst', power: 50, accuracy: 100, element: 'Earth', effect: null },

  static_pin:     { name: 'Static Pin', power: 40, accuracy: 100, element: 'Volt', effect: null },
  arc_blade:      { name: 'Arc Blade', power: 65, accuracy: 95, element: 'Volt', effect: null },
  thunder_crown:  { name: 'Thunder Crown', power: 90, accuracy: 85, element: 'Volt', effect: null },
  shock_bind:     { name: 'Shock Bind', power: 30, accuracy: 100, element: 'Volt', effect: 'spdDown' },
  plasma_nova:    { name: 'Plasma Nova', power: 110, accuracy: 80, element: 'Volt', effect: null },
  spark_rush:     { name: 'Spark Rush', power: 50, accuracy: 100, element: 'Volt', effect: null },

  void_tap:       { name: 'Void Tap', power: 45, accuracy: 100, element: 'Cosmos', effect: null },
  star_pulse:     { name: 'Star Pulse', power: 70, accuracy: 90, element: 'Cosmos', effect: null },
  null_wave:      { name: 'Null Wave', power: 55, accuracy: 100, element: 'Cosmos', effect: null },
  eclipse_beam:   { name: 'Eclipse Beam', power: 100, accuracy: 85, element: 'Cosmos', effect: null }
};

// ============================================================
// RESONANTS DATABASE (18)
// ============================================================
const SPECIES = [
  // --- PYRO LINE ---
  {
    id: 'emberkit', name: 'Emberkit', type: 'Pyro', stage: 1, evoLevel: 8, evoTo: 'pyrowolf',
    base: { hp: 42, atk: 52, def: 38, spd: 48 },
    moves: [
      { id: 'spark_snap', unlock: 1 }, { id: 'ember_coil', unlock: 3 },
      { id: 'ash_veil', unlock: 5 }, { id: 'flame_sever', unlock: 7 }
    ],
    sprite: { body: 'fox', ears: true, tail: 'flame', eyes: 'fierce' },
    desc: 'A fledgling flame spirit. Sparks dance along its velvet fur as it paces restlessly.'
  },
  {
    id: 'pyrowolf', name: 'Pyrowolf', type: 'Pyro', stage: 2, evoLevel: 15, evoTo: 'ignisaur',
    base: { hp: 62, atk: 78, def: 55, spd: 70 },
    moves: [
      { id: 'spark_snap', unlock: 1 }, { id: 'flame_sever', unlock: 1 },
      { id: 'ash_veil', unlock: 1 }, { id: 'inferno_crash', unlock: 10 },
      { id: 'ember_coil', unlock: 1 }
    ],
    sprite: { body: 'wolf', ears: true, tail: 'flame', eyes: 'fierce', mane: true },
    desc: 'Its howl ignites dry brush. Conductors revere the Pyrowolf as a sign of the wildfire season.'
  },
  {
    id: 'ignisaur', name: 'Ignisaur', type: 'Pyro', stage: 3, evoLevel: null, evoTo: null,
    base: { hp: 85, atk: 105, def: 78, spd: 88 },
    moves: [
      { id: 'flame_sever', unlock: 1 }, { id: 'inferno_crash', unlock: 1 },
      { id: 'solar_burst', unlock: 15 }, { id: 'ash_veil', unlock: 1 },
      { id: 'ember_coil', unlock: 1 }
    ],
    sprite: { body: 'drake', horns: true, tail: 'flame', eyes: 'fierce', wings: 'small', scales: true },
    desc: 'An apex flame Resonant. Magma seeps between its ebon scales; the air warps around it.'
  },
  // --- HYDRO LINE ---
  {
    id: 'droplet', name: 'Droplet', type: 'Hydro', stage: 1, evoLevel: 8, evoTo: 'geoxus',
    base: { hp: 48, atk: 40, def: 45, spd: 42 },
    moves: [
      { id: 'aqua_dart', unlock: 1 }, { id: 'ripple_wave', unlock: 3 },
      { id: 'mist_shroud', unlock: 5 }, { id: 'tide_lash', unlock: 7 }
    ],
    sprite: { body: 'blob', eyes: 'gentle', fins: true, bubbles: true },
    desc: 'A soft amorphous spirit of pure spring water. Its laughter sounds like rain on leaves.'
  },
  {
    id: 'geoxus', name: 'Geoxus', type: 'Hydro', stage: 2, evoLevel: 15, evoTo: 'neptunian',
    base: { hp: 72, atk: 60, def: 70, spd: 55 },
    moves: [
      { id: 'aqua_dart', unlock: 1 }, { id: 'tide_lash', unlock: 1 },
      { id: 'mist_shroud', unlock: 1 }, { id: 'maelstrom', unlock: 10 },
      { id: 'ripple_wave', unlock: 1 }
    ],
    sprite: { body: 'turtle', shell: true, eyes: 'wise', fins: true },
    desc: 'Its shell is polished river-stone. Slow to vacant ire, but an immovable wall of tide.'
  },
  {
    id: 'neptunian', name: 'Neptunian', type: 'Hydro', stage: 3, evoLevel: null, evoTo: null,
    base: { hp: 95, atk: 82, def: 95, spd: 72 },
    moves: [
      { id: 'tide_lash', unlock: 1 }, { id: 'maelstrom', unlock: 1 },
      { id: 'abyssal_crush', unlock: 15 }, { id: 'mist_shroud', unlock: 1 },
      { id: 'ripple_wave', unlock: 1 }
    ],
    sprite: { body: 'serpent', horns: true, eyes: 'wise', fins: true, crown: true },
    desc: 'Sovereign of sunken depths. Coral and pearl lace its magnificent coiled form.'
  },
  // --- FLORA LINE ---
  {
    id: 'seedling', name: 'Seedling', type: 'Flora', stage: 1, evoLevel: 8, evoTo: 'thornback',
    base: { hp: 45, atk: 45, def: 48, spd: 40 },
    moves: [
      { id: 'vine_whip', unlock: 1 }, { id: 'seed_burst', unlock: 3 },
      { id: 'pollen_haze', unlock: 5 }, { id: 'thorn_barrage', unlock: 7 }
    ],
    sprite: { body: 'sprout', leaves: true, eyes: 'gentle', flower: true },
    desc: 'A tiny seed-spirit. Sunlight makes it glow with contentment. Soft vines coil around its ankles.'
  },
  {
    id: 'thornback', name: 'Thornback', type: 'Flora', stage: 2, evoLevel: 15, evoTo: 'bloomgoliath',
    base: { hp: 68, atk: 72, def: 72, spd: 52 },
    moves: [
      { id: 'vine_whip', unlock: 1 }, { id: 'thorn_barrage', unlock: 1 },
      { id: 'pollen_haze', unlock: 1 }, { id: 'bloom_crash', unlock: 10 },
      { id: 'seed_burst', unlock: 1 }
    ],
    sprite: { body: 'beast', spikes: true, eyes: 'fierce', leaves: true, flower: false },
    desc: 'Armored in living bark and needle-thorns. It goes still as a shrub until prey nears.'
  },
  {
    id: 'bloomgoliath', name: 'Bloomgoliath', type: 'Flora', stage: 3, evoLevel: null, evoTo: null,
    base: { hp: 100, atk: 92, def: 98, spd: 60 },
    moves: [
      { id: 'thorn_barrage', unlock: 1 }, { id: 'bloom_crash', unlock: 1 },
      { id: 'world_root', unlock: 15 }, { id: 'pollen_haze', unlock: 1 },
      { id: 'seed_burst', unlock: 1 }
    ],
    sprite: { body: 'goliath', leaves: true, eyes: 'wise', flower: true, horns: 'branch' },
    desc: 'A walking grove. Forests bloom in its footsteps; older pedestrians call it the Heart-Tree made flesh.'
  },
  // --- ZEPHYR LINE ---
  {
    id: 'gusty', name: 'Gusty', type: 'Zephyr', stage: 1, evoLevel: 8, evoTo: 'slipstream',
    base: { hp: 38, atk: 48, def: 32, spd: 60 },
    moves: [
      { id: 'gust_cut', unlock: 1 }, { id: 'breeze_slash', unlock: 3 },
      { id: 'wind_shear', unlock: 5 }, { id: 'cyclone_edge', unlock: 7 }
    ],
    sprite: { body: 'bird', wings: true, eyes: 'fierce', tail: 'feather' },
    desc: 'Always in motion. A Gusty rests only mid-cloud, and hates sealed rooms.'
  },
  {
    id: 'slipstream', name: 'Slipstream', type: 'Zephyr', stage: 2, evoLevel: 15, evoTo: 'zephyrus',
    base: { hp: 55, atk: 72, def: 48, spd: 90 },
    moves: [
      { id: 'gust_cut', unlock: 1 }, { id: 'cyclone_edge', unlock: 1 },
      { id: 'wind_shear', unlock: 1 }, { id: 'sky_rend', unlock: 10 },
      { id: 'breeze_slash', unlock: 1 }
    ],
    sprite: { body: 'raptor', wings: true, eyes: 'fierce', tail: 'feather', crest: true },
    desc: 'It becomes near-invisible while diving. Culinary hunters once wagered over spotting one mid-hunt.'
  },
  {
    id: 'zephyrus', name: 'Zephyrus', type: 'Zephyr', stage: 3, evoLevel: null, evoTo: null,
    base: { hp: 72, atk: 95, def: 65, spd: 115 },
    moves: [
      { id: 'cyclone_edge', unlock: 1 }, { id: 'sky_rend', unlock: 1 },
      { id: 'aether_tempest', unlock: 15 }, { id: 'wind_shear', unlock: 1 },
      { id: 'breeze_slash', unlock: 1 }
    ],
    sprite: { body: 'phoenix', wings: 'large', eyes: 'fierce', tail: 'feather', halo: true },
    desc: 'A god-wind of the high atmosphere. Storm fronts rewrite themselves when a Zephyrus passes.'
  },
  // --- EARTH LINE ---
  {
    id: 'pebble', name: 'Pebble', type: 'Earth', stage: 1, evoLevel: 8, evoTo: 'obsidian',
    base: { hp: 50, atk: 42, def: 55, spd: 30 },
    moves: [
      { id: 'pebble_shot', unlock: 1 }, { id: 'dust_burst', unlock: 3 },
      { id: 'stone_guard', unlock: 5 }, { id: 'quake_spike', unlock: 7 }
    ],
    sprite: { body: 'rock', eyes: 'gentle', crystals: true },
    desc: 'A lively stone chip with two bright gem-eyes. It stacks itself into little cairns for fun.'
  },
  {
    id: 'obsidian', name: 'Obsidian', type: 'Earth', stage: 2, evoLevel: 15, evoTo: 'terrasunder',
    base: { hp: 75, atk: 68, def: 85, spd: 42 },
    moves: [
      { id: 'pebble_shot', unlock: 1 }, { id: 'quake_spike', unlock: 1 },
      { id: 'stone_guard', unlock: 1 }, { id: 'tectonic_slam', unlock: 10 },
      { id: 'dust_burst', unlock: 1 }
    ],
    sprite: { body: 'golem', eyes: 'fierce', crystals: true, arms: true },
    desc: 'Volcanic glass plated across its body. Blades shatter on impact with its folded arms.'
  },
  {
    id: 'terrasunder', name: 'Terrasunder', type: 'Earth', stage: 3, evoLevel: null, evoTo: null,
    base: { hp: 105, atk: 90, def: 110, spd: 50 },
    moves: [
      { id: 'quake_spike', unlock: 1 }, { id: 'tectonic_slam', unlock: 1 },
      { id: 'continental', unlock: 15 }, { id: 'stone_guard', unlock: 1 },
      { id: 'dust_burst', unlock: 1 }
    ],
    sprite: { body: 'titan', eyes: 'wise', crystals: true, arms: true, horns: true },
    desc: 'Mountains form ridges along its spine. A step from a Terrasunder rewrites regional maps.'
  },
  // --- VOLT LINE ---
  {
    id: 'sparky', name: 'Sparky', type: 'Volt', stage: 1, evoLevel: 8, evoTo: 'boltsaber',
    base: { hp: 40, atk: 55, def: 35, spd: 55 },
    moves: [
      { id: 'static_pin', unlock: 1 }, { id: 'spark_rush', unlock: 3 },
      { id: 'shock_bind', unlock: 5 }, { id: 'arc_blade', unlock: 7 }
    ],
    sprite: { body: 'critter', ears: true, eyes: 'fierce', sparks: true, tail: 'bolt' },
    desc: 'Static clings to everything it touches. Hair stands on end when a Sparky is near.'
  },
  {
    id: 'boltsaber', name: 'Boltsaber', type: 'Volt', stage: 2, evoLevel: 15, evoTo: 'stormlord',
    base: { hp: 58, atk: 82, def: 52, spd: 80 },
    moves: [
      { id: 'static_pin', unlock: 1 }, { id: 'arc_blade', unlock: 1 },
      { id: 'shock_bind', unlock: 1 }, { id: 'thunder_crown', unlock: 10 },
      { id: 'spark_rush', unlock: 1 }
    ],
    sprite: { body: 'feline', ears: true, eyes: 'fierce', sparks: true, tail: 'bolt', mane: true },
    desc: 'Lightning tattoos its sleek form. A Boltsaber can outrun audible thunder.'
  },
  {
    id: 'stormlord', name: 'Stormlord', type: 'Volt', stage: 3, evoLevel: null, evoTo: null,
    base: { hp: 78, atk: 108, def: 70, spd: 100 },
    moves: [
      { id: 'arc_blade', unlock: 1 }, { id: 'thunder_crown', unlock: 1 },
      { id: 'plasma_nova', unlock: 15 }, { id: 'shock_bind', unlock: 1 },
      { id: 'spark_rush', unlock: 1 }
    ],
    sprite: { body: 'beast', horns: true, eyes: 'fierce', sparks: true, tail: 'bolt', mane: true, crown: true },
    desc: 'A living thunderhead. The Stormlord\'s roar is the crack of a thousand chains of lightning.'
  }
];

const SPECIES_MAP = {};
SPECIES.forEach(s => SPECIES_MAP[s.id] = s);

// ============================================================
// SVG SPRITE GENERATOR
// ============================================================
function makeSprite(species, size = 96) {
  const t = species.type;
  const c = ELEMENT_COLORS[t];
  const c2 = shadeColor(c, -30);
  const c3 = shadeColor(c, 40);
  const sp = species.sprite;
  const id = 'g' + Math.random().toString(36).slice(2, 8);

  let bodyPaths = '';
  let extras = '';

  if (sp.body === 'fox' || sp.body === 'wolf' || sp.body === 'feline' || sp.body === 'critter') {
    bodyPaths = `
      <ellipse cx="48" cy="55" rx="28" ry="22" fill="url(#${id}b)"/>
      <ellipse cx="48" cy="38" rx="18" ry="16" fill="url(#${id}b)"/>
      <ellipse cx="38" cy="35" rx="3.5" ry="4" fill="#1a1a2e"/>
      <ellipse cx="58" cy="35" rx="3.5" ry="4" fill="#1a1a2e"/>
      <circle cx="39" cy="34" r="1.2" fill="#fff"/>
      <circle cx="59" cy="34" r="1.2" fill="#fff"/>
      <ellipse cx="48" cy="42" rx="3" ry="2" fill="${c2}"/>`;
    if (sp.ears) {
      extras += `<polygon points="32,28 28,12 40,26" fill="url(#${id}b)" stroke="${c2}" stroke-width="1"/>
                 <polygon points="64,28 68,12 56,26" fill="url(#${id}b)" stroke="${c2}" stroke-width="1"/>
                 <polygon points="33,26 30,15 38,25" fill="${c3}" opacity="0.6"/>
                 <polygon points="63,26 66,15 58,25" fill="${c3}" opacity="0.6"/>`;
    }
    if (sp.tail === 'flame' || sp.tail === 'bolt') {
      extras += `<path d="M72,58 Q90,40 85,25 Q80,45 75,55 Z" fill="url(#${id}t)" opacity="0.9"/>`;
    }
    if (sp.mane) {
      extras += `<path d="M30,40 Q20,30 25,48 Q22,55 32,52" fill="${c3}" opacity="0.7"/>
                 <path d="M66,40 Q76,30 71,48 Q74,55 64,52" fill="${c3}" opacity="0.7"/>`;
    }
  } else if (sp.body === 'drake' || sp.body === 'beast' || sp.body === 'titan' || sp.body === 'goliath') {
    bodyPaths = `
      <ellipse cx="48" cy="58" rx="32" ry="24" fill="url(#${id}b)"/>
      <ellipse cx="48" cy="36" rx="20" ry="18" fill="url(#${id}b)"/>
      <ellipse cx="38" cy="33" rx="4" ry="4.5" fill="#1a1a2e"/>
      <ellipse cx="58" cy="33" rx="4" ry="4.5" fill="#1a1a2e"/>
      <circle cx="39.5" cy="32" r="1.5" fill="#fff"/>
      <circle cx="59.5" cy="32" r="1.5" fill="#fff"/>
      <path d="M42,42 L48,46 L54,42" fill="none" stroke="${c2}" stroke-width="1.5" stroke-linecap="round"/>`;
    if (sp.horns || sp.horns === 'branch') {
      extras += `<path d="M34,24 Q28,8 36,20" fill="none" stroke="${c3}" stroke-width="3" stroke-linecap="round"/>
                 <path d="M62,24 Q68,8 60,20" fill="none" stroke="${c3}" stroke-width="3" stroke-linecap="round"/>`;
    }
    if (sp.wings === 'small' || sp.wings === 'large' || sp.wings === true) {
      const ws = sp.wings === 'large' ? 1.3 : 1;
      extras += `<path d="M20,50 Q5,30 18,45 Q8,55 22,55" fill="${c}" opacity="0.5" transform="scale(${ws})"/>
                 <path d="M76,50 Q91,30 78,45 Q88,55 74,55" fill="${c}" opacity="0.5" transform="scale(${ws})"/>`;
    }
    if (sp.tail === 'flame') {
      extras += `<path d="M75,65 Q95,50 90,30 Q88,55 78,68 Z" fill="url(#${id}t)"/>`;
    }
    if (sp.scales) {
      extras += `<circle cx="40" cy="55" r="3" fill="${c2}" opacity="0.4"/><circle cx="50" cy="60" r="3" fill="${c2}" opacity="0.4"/><circle cx="58" cy="54" r="3" fill="${c2}" opacity="0.4"/>`;
    }
    if (sp.spikes) {
      extras += `<polygon points="30,48 26,38 36,48" fill="${c2}"/><polygon points="48,45 48,34 54,46" fill="${c2}"/><polygon points="66,48 70,38 60,48" fill="${c2}"/>`;
    }
    if (sp.leaves) {
      extras += `<ellipse cx="25" cy="40" rx="8" ry="4" fill="${c3}" opacity="0.7" transform="rotate(-30 25 40)"/>
                 <ellipse cx="72" cy="42" rx="8" ry="4" fill="${c3}" opacity="0.7" transform="rotate(30 72 42)"/>`;
    }
    if (sp.flower) {
      extras += `<circle cx="48" cy="20" r="5" fill="${c3}"/><circle cx="48" cy="20" r="2.5" fill="${c}"/>`;
    }
    if (sp.crystals) {
      extras += `<polygon points="48,12 42,28 54,28" fill="${c3}" opacity="0.8" stroke="${c}" stroke-width="1"/>
                 <polygon points="28,45 24,58 34,55" fill="${c3}" opacity="0.6"/>
                 <polygon points="70,45 76,58 64,55" fill="${c3}" opacity="0.6"/>`;
    }
    if (sp.arms) {
      extras += `<rect x="12" y="50" width="12" height="20" rx="5" fill="url(#${id}b)"/>
                 <rect x="72" y="50" width="12" height="20" rx="5" fill="url(#${id}b)"/>`;
    }
    if (sp.crown) {
      extras += `<polygon points="40,18 44,8 48,16 52,8 56,18" fill="${c3}" stroke="${c}" stroke-width="1"/>`;
    }
    if (sp.sparks) {
      extras += `<line x1="15" y1="30" x2="22" y2="38" stroke="${c3}" stroke-width="2"/>
                 <line x1="80" y1="28" x2="74" y2="36" stroke="${c3}" stroke-width="2"/>
                 <line x1="20" y1="60" x2="14" y2="70" stroke="${c3}" stroke-width="1.5"/>`;
    }
  } else if (sp.body === 'blob' || sp.body === 'sprout' || sp.body === 'rock') {
    bodyPaths = `
      <ellipse cx="48" cy="52" rx="26" ry="28" fill="url(#${id}b)"/>
      <ellipse cx="38" cy="48" rx="4" ry="5" fill="#1a1a2e"/>
      <ellipse cx="58" cy="48" rx="4" ry="5" fill="#1a1a2e"/>
      <circle cx="39.5" cy="47" r="1.5" fill="#fff"/>
      <circle cx="59.5" cy="47" r="1.5" fill="#fff"/>
      <path d="M42,58 Q48,64 54,58" fill="none" stroke="${c2}" stroke-width="1.5" stroke-linecap="round"/>`;
    if (sp.fins) {
      extras += `<ellipse cx="22" cy="55" rx="8" ry="5" fill="${c}" opacity="0.6" transform="rotate(-20 22 55)"/>
                 <ellipse cx="74" cy="55" rx="8" ry="5" fill="${c}" opacity="0.6" transform="rotate(20 74 55)"/>`;
    }
    if (sp.bubbles) {
      extras += `<circle cx="30" cy="30" r="3" fill="${c3}" opacity="0.5"/><circle cx="68" cy="25" r="4" fill="${c3}" opacity="0.4"/><circle cx="55" cy="18" r="2" fill="${c3}" opacity="0.5"/>`;
    }
    if (sp.leaves || sp.flower) {
      extras += `<path d="M48,24 Q38,8 48,14 Q58,8 48,24" fill="${c3}"/>`;
      if (sp.flower) extras += `<circle cx="48" cy="12" r="4" fill="${shadeColor(c, 60)}"/><circle cx="48" cy="12" r="2" fill="${c}"/>`;
    }
    if (sp.crystals) {
      extras += `<polygon points="48,18 43,32 53,32" fill="${c3}" opacity="0.85"/>
                 <polygon points="32,40 28,52 38,48" fill="${c3}" opacity="0.6"/>
                 <polygon points="64,40 68,52 58,48" fill="${c3}" opacity="0.6"/>`;
    }
  } else if (sp.body === 'turtle') {
    bodyPaths = `
      <ellipse cx="48" cy="58" rx="30" ry="20" fill="url(#${id}b)"/>
      <ellipse cx="48" cy="50" rx="24" ry="16" fill="${c2}" opacity="0.5"/>
      <ellipse cx="48" cy="35" rx="14" ry="12" fill="url(#${id}b)"/>
      <ellipse cx="42" cy="33" rx="3" ry="3.5" fill="#1a1a2e"/>
      <ellipse cx="54" cy="33" rx="3" ry="3.5" fill="#1a1a2e"/>
      <circle cx="43" cy="32" r="1" fill="#fff"/>
      <circle cx="55" cy="32" r="1" fill="#fff"/>`;
    if (sp.shell) {
      extras += `<path d="M28,50 Q48,30 68,50" fill="none" stroke="${c3}" stroke-width="2"/>
                 <path d="M32,55 Q48,40 64,55" fill="none" stroke="${c3}" stroke-width="1.5"/>
                 <line x1="48" y1="38" x2="48" y2="58" stroke="${c3}" stroke-width="1.5"/>
                 <line x1="36" y1="48" x2="60" y2="48" stroke="${c3}" stroke-width="1.5"/>`;
    }
    if (sp.fins) {
      extras += `<ellipse cx="18" cy="60" rx="10" ry="5" fill="${c}" opacity="0.5"/><ellipse cx="78" cy="60" rx="10" ry="5" fill="${c}" opacity="0.5"/>`;
    }
  } else if (sp.body === 'serpent') {
    bodyPaths = `
      <path d="M30,70 Q20,50 35,40 Q50,30 48,20 Q46,45 55,50 Q70,55 65,75 Q50,80 30,70" fill="url(#${id}b)"/>
      <ellipse cx="48" cy="22" rx="12" ry="12" fill="url(#${id}b)"/>
      <ellipse cx="43" cy="20" rx="3" ry="3.5" fill="#1a1a2e"/>
      <ellipse cx="53" cy="20" rx="3" ry="3.5" fill="#1a1a2e"/>
      <circle cx="44" cy="19" r="1" fill="#fff"/>
      <circle cx="54" cy="19" r="1" fill="#fff"/>`;
    if (sp.crown) extras += `<polygon points="42,10 46,2 48,8 50,2 54,10" fill="${c3}"/>`;
    if (sp.horns) extras += `<path d="M38,14 L32,4" stroke="${c3}" stroke-width="2.5" stroke-linecap="round"/><path d="M58,14 L64,4" stroke="${c3}" stroke-width="2.5" stroke-linecap="round"/>`;
    if (sp.fins) extras += `<path d="M25,50 Q15,45 22,55" fill="${c}" opacity="0.5"/><path d="M60,45 Q75,40 62,52" fill="${c}" opacity="0.5"/>`;
  } else if (sp.body === 'bird' || sp.body === 'raptor' || sp.body === 'phoenix') {
    bodyPaths = `
      <ellipse cx="48" cy="55" rx="20" ry="22" fill="url(#${id}b)"/>
      <ellipse cx="48" cy="35" rx="14" ry="14" fill="url(#${id}b)"/>
      <ellipse cx="43" cy="33" rx="3" ry="3.5" fill="#1a1a2e"/>
      <ellipse cx="53" cy="33" rx="3" ry="3.5" fill="#1a1a2e"/>
      <circle cx="44" cy="32" r="1" fill="#fff"/>
      <circle cx="54" cy="32" r="1" fill="#fff"/>
      <polygon points="48,42 44,46 52,46" fill="${c2}"/>`;
    if (sp.wings || sp.wings === 'large') {
      extras += `<path d="M28,50 Q5,35 20,55 Q10,65 30,58" fill="url(#${id}b)" opacity="0.85"/>
                 <path d="M68,50 Q91,35 76,55 Q86,65 66,58" fill="url(#${id}b)" opacity="0.85"/>
                 <path d="M28,50 Q10,40 22,52" fill="none" stroke="${c3}" stroke-width="1" opacity="0.5"/>
                 <path d="M68,50 Q86,40 74,52" fill="none" stroke="${c3}" stroke-width="1" opacity="0.5"/>`;
    }
    if (sp.tail === 'feather') {
      extras += `<path d="M48,75 Q40,90 38,95" fill="none" stroke="${c}" stroke-width="3" stroke-linecap="round"/>
                 <path d="M48,75 Q48,92 50,96" fill="none" stroke="${c3}" stroke-width="3" stroke-linecap="round"/>
                 <path d="M48,75 Q56,90 58,95" fill="none" stroke="${c}" stroke-width="3" stroke-linecap="round"/>`;
    }
    if (sp.crest) extras += `<path d="M48,22 Q48,8 42,14" fill="none" stroke="${c3}" stroke-width="2.5"/><path d="M48,22 Q52,6 55,14" fill="none" stroke="${c3}" stroke-width="2"/>`;
    if (sp.halo) extras += `<ellipse cx="48" cy="14" rx="16" ry="5" fill="none" stroke="${c3}" stroke-width="2" opacity="0.7"/>`;
  } else {
    bodyPaths = `
      <ellipse cx="48" cy="50" rx="26" ry="28" fill="url(#${id}b)"/>
      <ellipse cx="38" cy="45" rx="4" ry="5" fill="#1a1a2e"/>
      <ellipse cx="58" cy="45" rx="4" ry="5" fill="#1a1a2e"/>
      <circle cx="39.5" cy="44" r="1.5" fill="#fff"/>
      <circle cx="59.5" cy="44" r="1.5" fill="#fff"/>`;
  }

  return `<svg viewBox="0 0 96 96" width="${size}" height="${size}" xmlns="http://www.w3.org/2000/svg">
    <defs>
      <radialGradient id="${id}b" cx="40%" cy="35%" r="65%">
        <stop offset="0%" stop-color="${c3}"/>
        <stop offset="60%" stop-color="${c}"/>
        <stop offset="100%" stop-color="${c2}"/>
      </radialGradient>
      <linearGradient id="${id}t" x1="0" y1="1" x2="1" y2="0">
        <stop offset="0%" stop-color="${c2}"/>
        <stop offset="50%" stop-color="${c}"/>
        <stop offset="100%" stop-color="${c3}"/>
      </linearGradient>
      <filter id="${id}g"><feGaussianBlur stdDeviation="2.5" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
    </defs>
    <circle cx="48" cy="50" r="40" fill="${c}" opacity="0.08"/>
    <g filter="url(#${id}g)">
      ${bodyPaths}
      ${extras}
    </g>
  </svg>`;
}

function shadeColor(hex, pct) {
  const num = parseInt(hex.slice(1), 16);
  let r = (num >> 16) + pct, g = ((num >> 8) & 0xff) + pct, b = (num & 0xff) + pct;
  r = Math.max(0, Math.min(255, r));
  g = Math.max(0, Math.min(255, g));
  b = Math.max(0, Math.min(255, b));
  return '#' + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
}

// ============================================================
// INSTANCE HELPERS
// ============================================================
function createResonant(speciesId, level) {
  const sp = SPECIES_MAP[speciesId];
  if (!sp) return null;
  const stats = calcStats(sp, level);
  const moves = getMovesForLevel(sp, level);
  return {
    uid: 'r_' + Date.now() + '_' + Math.random().toString(36).slice(2, 7),
    speciesId,
    name: sp.name,
    type: sp.type,
    level,
    xp: 0,
    xpToNext: xpForLevel(level),
    maxHp: stats.hp,
    hp: stats.hp,
    atk: stats.atk,
    def: stats.def,
    spd: stats.spd,
    moves,
    status: null,
    stages: { atk: 0, def: 0, spd: 0 }
  };
}

function calcStats(sp, level) {
  return {
    hp:  Math.floor(((sp.base.hp * 2 * level) / 100) + level + 10),
    atk: Math.floor(((sp.base.atk * 2 * level) / 100) + 5),
    def: Math.floor(((sp.base.def * 2 * level) / 100) + 5),
    spd: Math.floor(((sp.base.spd * 2 * level) / 100) + 5)
  };
}

function getMovesForLevel(sp, level) {
  return sp.moves
    .filter(m => m.unlock <= level)
    .slice(-4)
    .map(m => m.id);
}

function xpForLevel(level) {
  return Math.floor(level * level * 1.2 + 10);
}

function xpYield(enemyLevel, isTrainer) {
  return Math.floor((enemyLevel * 8 + 15) * (isTrainer ? 1.6 : 1));
}

function cloneResonant(r) {
  return JSON.parse(JSON.stringify(r));
}

function refreshStats(r) {
  const sp = SPECIES_MAP[r.speciesId];
  const s = calcStats(sp, r.level);
  const ratio = r.hp / r.maxHp;
  r.maxHp = s.hp; r.atk = s.atk; r.def = s.def; r.spd = s.spd;
  r.hp = Math.max(1, Math.floor(r.maxHp * ratio));
  r.moves = getMovesForLevel(sp, r.level);
  r.name = sp.name;
  r.type = sp.type;
}

function stageMult(stage) {
  return Math.max(0.25, Math.min(4, (2 + stage) / 2));
}

// ============================================================
// ZONES & NODES
// ============================================================
const ZONES = [
  {
    id: 0, name: 'The Verdant Canopy', short: 'Canopy',
    desc: 'Lush rainforest humming with Flora and Zephyr frequencies. Soft light filters through ancient boughs.',
    unlockLevel: 1, levels: [1, 5], neon: 'flora',
    wildPool: ['seedling', 'gusty', 'emberkit', 'droplet', 'pebble', 'sparky'],
    boss: {
      name: 'Grove Warden Lysara',
      intro: 'The Canopy does not yield to the impatient. Show me your harmony, Conductor — or become compost for my gardens.',
      defeat: 'Your resonance is pure. The Grotto path opens; watch the water carefully...',
      team: [{ id: 'thornback', level: 6 }, { id: 'slipstream', level: 5 }, { id: 'seedling', level: 6 }],
      shards: 80
    },
    nodes: [
      { id: 0, x: 50, y: 280, type: 'start', label: 'Camp' },
      { id: 1, x: 140, y: 240, type: 'wild', label: 'Tall Reed' },
      { id: 2, x: 120, y: 160, type: 'cache', label: 'Moss Cache' },
      { id: 3, x: 220, y: 200, type: 'wild', label: 'Fern Patch' },
      { id: 4, x: 250, y: 120, type: 'heal', label: 'Spring' },
      { id: 5, x: 320, y: 180, type: 'trainer', label: 'Sproutling Maya', trainer: { name: 'Sproutling Maya', dialog: 'My seedlings are tougher than they look!', team: [{ id: 'seedling', level: 3 }, { id: 'droplet', level: 2 }], shards: 25 } },
      { id: 6, x: 340, y: 90, type: 'wild', label: 'Canopy Edge' },
      { id: 7, x: 420, y: 140, type: 'cache', label: 'Hollow Log' },
      { id: 8, x: 400, y: 220, type: 'wild', label: 'Root Maze' },
      { id: 9, x: 480, y: 100, type: 'trainer', label: 'Windrunner Kai', trainer: { name: 'Windrunner Kai', dialog: 'Catch me if your Resonants can fly!', team: [{ id: 'gusty', level: 4 }, { id: 'sparky', level: 3 }], shards: 30 } },
      { id: 10, x: 520, y: 180, type: 'wild', label: 'Deep Thicket' },
      { id: 11, x: 550, y: 60, type: 'boss', label: 'Gatekeeper' }
    ],
    connections: [[0,1],[1,2],[1,3],[2,4],[3,4],[3,5],[4,6],[5,6],[5,8],[6,7],[7,9],[8,10],[9,11],[10,11],[6,9]]
  },
  {
    id: 1, name: 'The Sunken Grotto', short: 'Grotto',
    desc: 'Bioluminescent caves of standing water. Hydro and Earth frequencies pulse through the stone.',
    unlockLevel: 5, levels: [5, 10], neon: 'hydro',
    wildPool: ['droplet', 'geoxus', 'pebble', 'obsidian', 'seedling', 'thornback', 'gusty'],
    boss: {
      name: 'Tide Priest Orin',
      intro: 'These waters have drowned greater Conductors than you. Prove your current is stronger than the undertow.',
      defeat: 'The tide accepts you. Beyond these caves, the Crags burn — do not wash away.',
      team: [{ id: 'geoxus', level: 10 }, { id: 'obsidian', level: 9 }, { id: 'neptunian', level: 8 }],
      shards: 150
    },
    nodes: [
      { id: 0, x: 40, y: 170, type: 'start', label: 'Cave Mouth' },
      { id: 1, x: 120, y: 220, type: 'wild', label: 'Shallow Pool' },
      { id: 2, x: 130, y: 110, type: 'wild', label: 'Stalactite Hall' },
      { id: 3, x: 200, y: 160, type: 'heal', label: 'Mineral Spring' },
      { id: 4, x: 240, y: 250, type: 'cache', label: 'Sunken Chest' },
      { id: 5, x: 280, y: 100, type: 'trainer', label: 'Pearl Diver Ren', trainer: { name: 'Pearl Diver Ren', dialog: 'From the depths I rise — can you weather the pressure?', team: [{ id: 'droplet', level: 6 }, { id: 'geoxus', level: 7 }, { id: 'gusty', level: 5 }], shards: 45 } },
      { id: 6, x: 320, y: 190, type: 'wild', label: 'Dark Current' },
      { id: 7, x: 380, y: 80, type: 'wild', label: 'Echo Chamber' },
      { id: 8, x: 390, y: 240, type: 'trainer', label: 'Stonehewn Vera', trainer: { name: 'Stonehewn Vera', dialog: 'My netherstone Resonants never yield!', team: [{ id: 'pebble', level: 7 }, { id: 'obsidian', level: 8 }], shards: 50 } },
      { id: 9, x: 450, y: 150, type: 'cache', label: 'Crystal Vein' },
      { id: 10, x: 500, y: 100, type: 'wild', label: 'Abyss Mouth' },
      { id: 11, x: 540, y: 200, type: 'boss', label: 'Tide Priest' }
    ],
    connections: [[0,1],[0,2],[1,3],[1,4],[2,3],[2,5],[3,6],[4,6],[4,8],[5,7],[6,8],[6,9],[7,10],[8,11],[9,10],[9,11],[10,11]]
  },
  {
    id: 2, name: 'The Obsidian Crags', short: 'Crags',
    desc: 'Scorched ridgelines where magma rivers glow. Pyro and Volt energies crackle in the ash-choked air.',
    unlockLevel: 10, levels: [10, 15], neon: 'pyro',
    wildPool: ['emberkit', 'pyrowolf', 'sparky', 'boltsaber', 'obsidian', 'gusty', 'slipstream'],
    boss: {
      name: 'Cinder Baron Vex',
      intro: 'Ash and lightning answer to me. Your little tuning fork is a toy. Let me show you real power.',
      defeat: 'Impossible... the fire bows to you. Climb the Apex Pillar, if you dare claim the summit.',
      team: [{ id: 'pyrowolf', level: 14 }, { id: 'boltsaber', level: 13 }, { id: 'ignisaur', level: 12 }],
      shards: 250
    },
    nodes: [
      { id: 0, x: 50, y: 250, type: 'start', label: 'Ash Path' },
      { id: 1, x: 130, y: 200, type: 'wild', label: 'Ember Fields' },
      { id: 2, x: 110, y: 100, type: 'cache', label: 'Scorched Relic' },
      { id: 3, x: 200, y: 140, type: 'wild', label: 'Lava Bridge' },
      { id: 4, x: 220, y: 260, type: 'heal', label: 'Cooling Grotto' },
      { id: 5, x: 280, y: 80, type: 'trainer', label: 'Spark Smith Juno', trainer: { name: 'Spark Smith Juno', dialog: 'I forge Resonants in living lightning!', team: [{ id: 'sparky', level: 11 }, { id: 'boltsaber', level: 12 }, { id: 'emberkit', level: 10 }], shards: 70 } },
      { id: 6, x: 300, y: 180, type: 'wild', label: 'Crag Spire' },
      { id: 7, x: 360, y: 250, type: 'wild', label: 'Magma Basin' },
      { id: 8, x: 380, y: 120, type: 'trainer', label: 'Ash Dancer Pyra', trainer: { name: 'Ash Dancer Pyra', dialog: 'Dance with me through the firestorm!', team: [{ id: 'pyrowolf', level: 12 }, { id: 'slipstream', level: 11 }], shards: 75 } },
      { id: 9, x: 450, y: 200, type: 'cache', label: 'Magnetic Ore' },
      { id: 10, x: 480, y: 90, type: 'wild', label: 'Thunder Peak' },
      { id: 11, x: 540, y: 160, type: 'boss', label: 'Cinder Baron' }
    ],
    connections: [[0,1],[1,2],[1,3],[1,4],[2,5],[3,5],[3,6],[4,6],[4,7],[5,8],[6,7],[6,8],[7,9],[8,10],[8,9],[9,11],[10,11]]
  },
  {
    id: 3, name: 'The Apex Pillar', short: 'Apex',
    desc: 'A needle of living crystal that pierces the sky. Every elemental frequency converges here. Only masters climb its steps.',
    unlockLevel: 15, levels: [15, 20], neon: 'cosmos',
    wildPool: ['ignisaur', 'neptunian', 'bloomgoliath', 'zephyrus', 'terrasunder', 'stormlord', 'pyrowolf', 'boltsaber'],
    boss: {
      name: 'Apex Sovereign Nyx',
      intro: 'So. A Conductor reaches the Pillar. I am the silence between all frequencies — the void that underwrites creation. Show me a song worth remembering.',
      defeat: 'Beautiful. Your harmony spans every octave of Aethelgard. The world is yours to shape, Conductor. The frequencies sing your name.',
      team: [{ id: 'stormlord', level: 18 }, { id: 'ignisaur', level: 17 }, { id: 'zephyrus', level: 18 }, { id: 'bloomgoliath', level: 17 }],
      shards: 500
    },
    nodes: [
      { id: 0, x: 300, y: 300, type: 'start', label: 'Base Camp' },
      { id: 1, x: 200, y: 250, type: 'wild', label: 'Crystal Step A' },
      { id: 2, x: 400, y: 250, type: 'wild', label: 'Crystal Step B' },
      { id: 3, x: 300, y: 220, type: 'heal', label: 'Aether Pool' },
      { id: 4, x: 150, y: 180, type: 'trainer', label: 'Echo Knight Sol', trainer: { name: 'Echo Knight Sol', dialog: 'The Pillar tests all who climb. I am its first voice.', team: [{ id: 'neptunian', level: 15 }, { id: 'terrasunder', level: 15 }], shards: 100 } },
      { id: 5, x: 450, y: 180, type: 'trainer', label: 'Null Adherent Ira', trainer: { name: 'Null Adherent Ira', dialog: 'Silence is the purest note. Weather mine!', team: [{ id: 'stormlord', level: 16 }, { id: 'ignisaur', level: 15 }], shards: 110 } },
      { id: 6, x: 300, y: 150, type: 'cache', label: 'Summit Cache' },
      { id: 7, x: 220, y: 100, type: 'wild', label: 'Sky Terrace' },
      { id: 8, x: 380, y: 100, type: 'wild', label: 'Storm Terrace' },
      { id: 9, x: 300, y: 50, type: 'boss', label: 'Apex Sovereign' }
    ],
    connections: [[0,1],[0,2],[1,3],[2,3],[1,4],[2,5],[3,6],[4,6],[5,6],[4,7],[5,8],[6,7],[6,8],[7,9],[8,9]]
  }
];

// ============================================================
// ITEMS
// ============================================================
const SHOP = [
  { id: 'prism_basic', name: 'Harmonizer Prism', desc: 'Basic Aether Prism. Moderate capture chance.', price: 20, icon: '◇' },
  { id: 'prism_hyper', name: 'Hyper-Conductive Prism', desc: 'Advanced prism with high capture rate.', price: 60, icon: '◆' },
  { id: 'heal_core', name: 'Core Restorer', desc: 'Restores 50% of a Resonant\'s max HP.', price: 30, icon: '❤' },
  { id: 'heal_status', name: 'Resonance Elixir', desc: 'Cleanses all status conditions.', price: 25, icon: '✨' },
  { id: 'heal_full', name: 'Full Harmonic Draught', desc: 'Fully restores HP of one Resonant.', price: 55, icon: '💜' }
];

// ============================================================
// PLAYER STATE
// ============================================================
const Player = {
  name: 'Conductor',
  party: [],
  vault: [],
  inventory: { prism_basic: 5, prism_hyper: 1, heal_core: 3, heal_status: 2, heal_full: 1 },
  shards: 50,
  unlockedZones: [0],
  currentZone: 0,
  completedNodes: {},
  defeatedBosses: [],
  seen: {},
  caught: {},
  activeIndex: 0,
  flags: {}
};

function defaultPlayer() {
  return {
    name: 'Conductor',
    party: [],
    vault: [],
    inventory: { prism_basic: 5, prism_hyper: 1, heal_core: 3, heal_status: 2, heal_full: 1 },
    shards: 50,
    unlockedZones: [0],
    currentZone: 0,
    completedNodes: {},
    defeatedBosses: [],
    seen: {},
    caught: {},
    activeIndex: 0,
    flags: {}
  };
}

function saveGame() {
  try {
    localStorage.setItem('aethelgard_save', JSON.stringify(Player));
  } catch (e) { console.warn('Save failed', e); }
}

function loadGame() {
  try {
    const raw = localStorage.getItem('aethelgard_save');
    if (!raw) return false;
    const data = JSON.parse(raw);
    Object.keys(Player).forEach(k => delete Player[k]);
    Object.assign(Player, data);
    return true;
  } catch (e) { return false; }
}

// ============================================================
// GAME CONTROLLER
// ============================================================
const Game = {
  currentScreen: 'menu',
  dexFilter: 'all',
  pendingResult: null,

  init() {
    const loaded = loadGame();
    if (!loaded || !Player.party.length) {
      // wait for new game
    }
    this.renderDex();
    this.updateHUD();
  },

  showScreen(name) {
    SFX.select();
    document.querySelectorAll('.screen').forEach(s => s.classList.remove('active'));
    const el = document.getElementById('screen-' + name);
    if (el) {
      el.classList.add('active');
      this.currentScreen = name;
    }
    if (name === 'world') { this.renderWorld(); this.renderPartyPreview(); }
    if (name === 'party') this.renderParty();
    if (name === 'dex') this.renderDex();
    if (name === 'menu') this.updateHUD();
  },

  newGame() {
    SFX.select();
    Object.assign(Player, defaultPlayer());
    this.showStarterSelect();
  },

  resetProgress() {
    if (confirm('Reset all progress? This cannot be undone.')) {
      localStorage.removeItem('aethelgard_save');
      Object.assign(Player, defaultPlayer());
      this.showScreen('menu');
      this.toast('Progress reset.');
    }
  },

  showStarterSelect() {
    const modal = document.getElementById('modal-starter');
    const box = document.getElementById('starter-options');
    const starters = ['emberkit', 'droplet', 'seedling'];
    box.innerHTML = starters.map(id => {
      const sp = SPECIES_MAP[id];
      return `<button class="btn-game glass-strong rounded-xl p-3 neon-${sp.type.toLowerCase()} text-center" onclick="Game.pickStarter('${id}')" onmouseenter="SFX.hover()">
        <div class="flex justify-center mb-2">${makeSprite(sp, 72)}</div>
        <div class="font-bold text-sm text-white">${sp.name}</div>
        <div class="type-badge text-${sp.type.toLowerCase()} mt-1 inline-block">${sp.type}</div>
      </button>`;
    }).join('');
    modal.classList.remove('hidden');
    modal.classList.add('flex');
  },

  pickStarter(id) {
    SFX.select();
    const r = createResonant(id, 5);
    Player.party = [r];
    Player.seen[id] = true;
    Player.caught[id] = true;
    Player.activeIndex = 0;
    document.getElementById('modal-starter').classList.add('hidden');
    document.getElementById('modal-starter').classList.remove('flex');
    saveGame();
    this.toast(`${r.name} joined your party!`);
    this.showScreen('world');
  },

  updateHUD() {
    const sh = document.getElementById('hud-shards');
    if (sh) sh.textContent = '◆ ' + Player.shards;
    const z = ZONES[Player.currentZone];
    const hz = document.getElementById('hud-zone');
    if (hz && z) hz.textContent = z.short;
  },

  selectZone(idx) {
    if (!Player.unlockedZones.includes(idx)) {
      this.toast('Zone locked. Defeat the previous Gatekeeper!');
      SFX.fail();
      return;
    }
    SFX.select();
    Player.currentZone = idx;
    saveGame();
    this.renderWorld();
  },

  renderWorld() {
    this.updateHUD();
    const zone = ZONES[Player.currentZone];
    document.getElementById('zone-title').textContent = zone.name;
    document.getElementById('zone-desc').textContent = zone.desc;

    document.querySelectorAll('.zone-tab').forEach((tab, i) => {
      tab.classList.remove('neon-flora', 'neon-hydro', 'neon-pyro', 'neon-cosmos', 'opacity-40');
      if (!Player.unlockedZones.includes(i)) tab.classList.add('opacity-40');
      if (i === Player.currentZone) {
        const nmap = ['flora', 'hydro', 'pyro', 'cosmos'];
        tab.classList.add('neon-' + nmap[i]);
      }
    });

    const svg = document.getElementById('map-svg');
    const nodesEl = document.getElementById('map-nodes');
    const key = 'z' + zone.id;
    if (!Player.completedNodes[key]) Player.completedNodes[key] = {};

    // connections
    let paths = '';
    zone.connections.forEach(([a, b]) => {
      const na = zone.nodes[a], nb = zone.nodes[b];
      paths += `<line x1="${na.x}" y1="${na.y}" x2="${nb.x}" y2="${nb.y}" class="zone-path"/>`;
    });
    svg.innerHTML = paths;

    // Determine reachable: start always, then adjacent to any completed or start
    const completed = Player.completedNodes[key];
    const reachable = new Set([0]);
    let changed = true;
    while (changed) {
      changed = false;
      zone.connections.forEach(([a, b]) => {
        if ((reachable.has(a) && (a === 0 || completed[a])) || (reachable.has(b) && (b === 0 || completed[b]))) {
          if (!reachable.has(a)) { reachable.add(a); changed = true; }
          if (!reachable.has(b)) { reachable.add(b); changed = true; }
        }
        // also if either is completed, neighbour is reachable
        if (completed[a] || a === 0) { if (!reachable.has(b)) { reachable.add(b); changed = true; } }
        if (completed[b] || b === 0) { if (!reachable.has(a)) { reachable.add(a); changed = true; } }
      });
    }

    const icons = { start: '⛺', wild: '🌿', cache: '📦', trainer: '👤', heal: '💧', boss: '👑' };
    const colors = { start: '#a78bfa', wild: '#4ade80', cache: '#fbbf24', trainer: '#f472b6', heal: '#38bdf8', boss: '#f87171' };

    nodesEl.innerHTML = zone.nodes.map(n => {
      const locked = !reachable.has(n.id);
      const done = !!completed[n.id];
      const isBossDone = n.type === 'boss' && Player.defeatedBosses.includes(zone.id);
      const pctX = (n.x / 600) * 100;
      const pctY = (n.y / 340) * 100;
      let cls = 'node-dot absolute flex flex-col items-center';
      if (locked) cls += ' locked';
      if (done || isBossDone) cls += ' visited';
      const col = colors[n.type] || '#fff';
      return `<div class="${cls}" style="left:${pctX}%;top:${pctY}%;transform:translate(-50%,-50%);"
        onclick="Game.clickNode(${n.id})" onmouseenter="SFX.hover()">
        <div class="w-10 h-10 rounded-full flex items-center justify-center text-lg glass-strong"
          style="border:2px solid ${col};box-shadow:0 0 12px ${col}66;${locked ? 'filter:grayscale(1);' : ''}">
          ${icons[n.type] || '•'}
        </div>
        <span class="text-[9px] mt-1 text-slate-400 font-medium whitespace-nowrap max-w-[70px] truncate text-center">${n.label}</span>
      </div>`;
    }).join('');

    this.renderPartyPreview();
  },

  clickNode(nodeId) {
    const zone = ZONES[Player.currentZone];
    const node = zone.nodes.find(n => n.id === nodeId);
    if (!node) return;

    const key = 'z' + zone.id;
    const completed = Player.completedNodes[key] || {};

    // reachability check
    const reachable = new Set([0]);
    let changed = true;
    while (changed) {
      changed = false;
      zone.connections.forEach(([a, b]) => {
        if (completed[a] || a === 0) { if (!reachable.has(b)) { reachable.add(b); changed = true; } }
        if (completed[b] || b === 0) { if (!reachable.has(a)) { reachable.add(a); changed = true; } }
      });
    }
    if (!reachable.has(nodeId)) {
      this.toast('Path not yet opened.');
      SFX.fail();
      return;
    }

    if (!Player.party.some(r => r.hp > 0) && node.type !== 'heal' && node.type !== 'start') {
      this.toast('All Resonants fainted! Visit a Healing Spring.');
      SFX.fail();
      return;
    }

    SFX.select();

    switch (node.type) {
      case 'start':
        this.toast('Camp: Your journey resumes here.');
        break;
      case 'wild':
        this.startWildBattle(zone);
        this.markNode(nodeId);
        break;
      case 'cache':
        this.openCache(nodeId);
        break;
      case 'heal':
        this.healParty();
        this.markNode(nodeId);
        break;
      case 'trainer':
        this.showDialog(node.trainer.name, node.trainer.dialog, [
          { label: 'Battle!', fn: () => { this.closeDialog(); this.startTrainerBattle(node.trainer, nodeId); } },
          { label: 'Not now', fn: () => this.closeDialog() }
        ]);
        break;
      case 'boss':
        if (Player.defeatedBosses.includes(zone.id)) {
          this.toast('You have already bested this Gatekeeper.');
          break;
        }
        this.showDialog(zone.boss.name, zone.boss.intro, [
          { label: 'Challenge!', fn: () => { this.closeDialog(); this.startBossBattle(zone); } },
          { label: 'Retreat', fn: () => this.closeDialog() }
        ]);
        break;
    }
  },

  markNode(nodeId) {
    const key = 'z' + Player.currentZone;
    if (!Player.completedNodes[key]) Player.completedNodes[key] = {};
    Player.completedNodes[key][nodeId] = true;
    saveGame();
  },

  openCache(nodeId) {
    const key = 'z' + Player.currentZone;
    if (Player.completedNodes[key] && Player.completedNodes[key][nodeId]) {
      this.toast('Already looted.');
      return;
    }
    this.markNode(nodeId);
    const roll = Math.random();
    let msg = '', rewards = [];
    if (roll < 0.35) {
      const amt = 15 + Math.floor(Math.random() * 30) + Player.currentZone * 10;
      Player.shards += amt;
      rewards.push(`◆ ${amt} Aether Shards`);
      msg = 'You found a cache of shimmering shards!';
    } else if (roll < 0.6) {
      Player.inventory.prism_basic = (Player.inventory.prism_basic || 0) + 2;
      rewards.push('◇ Harmonizer Prism x2');
      msg = 'Prisms hum within the cache...';
    } else if (roll < 0.8) {
      Player.inventory.heal_core = (Player.inventory.heal_core || 0) + 2;
      rewards.push('❤ Core Restorer x2');
      msg = 'Medical supplies, carefully sealed.';
    } else {
      Player.inventory.prism_hyper = (Player.inventory.prism_hyper || 0) + 1;
      Player.shards += 20;
      rewards.push('◆ Hyper-Conductive Prism x1', '◆ 20 Aether Shards');
      msg = 'A rare haul! Fortune smiles.';
    }
    saveGame();
    this.showResult('📦', 'Cache Opened', msg, rewards);
  },

  healParty() {
    Player.party.forEach(r => { r.hp = r.maxHp; r.status = null; r.stages = { atk: 0, def: 0, spd: 0 }; });
    Player.vault.forEach(r => { r.hp = r.maxHp; r.status = null; r.stages = { atk: 0, def: 0, spd: 0 }; });
    SFX.heal();
    saveGame();
    this.toast('The spring restores all Resonants!');
    this.renderPartyPreview();
  },

  startWildBattle(zone) {
    const [lo, hi] = zone.levels;
    const level = lo + Math.floor(Math.random() * (hi - lo + 1));
    const pool = zone.wildPool;
    const id = pool[Math.floor(Math.random() * pool.length)];
    // sometimes evolved forms based on level
    let finalId = id;
    const sp = SPECIES_MAP[id];
    if (sp.evoTo && level >= sp.evoLevel) {
      finalId = sp.evoTo;
      const sp2 = SPECIES_MAP[finalId];
      if (sp2.evoTo && level >= sp2.evoLevel) finalId = sp2.evoTo;
    }
    const enemy = createResonant(finalId, level);
    Player.seen[finalId] = true;
    saveGame();
    Battle.start({ type: 'wild', enemy, enemies: [enemy], enemyIndex: 0, reward: 'Wild ' + enemy.name });
  },

  startTrainerBattle(trainer, nodeId) {
    const enemies = trainer.team.map(t => createResonant(t.id, t.level));
    enemies.forEach(e => { Player.seen[e.speciesId] = true; });
    saveGame();
    Battle.start({
      type: 'trainer',
      enemy: enemies[0],
      enemies,
      enemyIndex: 0,
      title: trainer.name,
      trainer,
      nodeId,
      shards: trainer.shards
    });
  },

  startBossBattle(zone) {
    const enemies = zone.boss.team.map(t => createResonant(t.id, t.level));
    enemies.forEach(e => { Player.seen[e.speciesId] = true; });
    saveGame();
    Battle.start({
      type: 'boss',
      enemy: enemies[0],
      enemies,
      enemyIndex: 0,
      title: zone.boss.name,
      zone,
      shards: zone.boss.shards
    });
  },

  renderPartyPreview() {
    const el = document.getElementById('party-preview');
    if (!el) return;
    if (!Player.party.length) {
      el.innerHTML = '<p class="text-slate-500 text-xs">No Resonants in party.</p>';
      return;
    }
    el.innerHTML = Player.party.map((r, i) => {
      const sp = SPECIES_MAP[r.speciesId];
      const pct = Math.floor((r.hp / r.maxHp) * 100);
      const hpCol = pct > 50 ? '#4ade80' : pct > 25 ? '#fbbf24' : '#f87171';
      return `<div class="glass rounded-xl p-2 min-w-[100px] flex-shrink-0 ${i === Player.activeIndex ? 'neon-' + r.type.toLowerCase() : ''}" onclick="Game.showDetails(${i},'party')">
        <div class="flex justify-center">${makeSprite(sp, 48)}</div>
        <div class="text-[10px] font-bold text-center text-white mt-1 truncate">${r.name}</div>
        <div class="text-[9px] text-center text-slate-400">Lv.${r.level}</div>
        <div class="h-1.5 rounded-full bg-black/40 mt-1 overflow-hidden">
          <div class="h-full rounded-full" style="width:${pct}%;background:${hpCol}"></div>
        </div>
      </div>`;
    }).join('');
  },

  renderParty() {
    document.getElementById('party-count').textContent = `${Player.party.length}/3 Active · Vault ${Player.vault.length}`;
    const act = document.getElementById('party-active');
    const vault = document.getElementById('party-vault');

    act.innerHTML = Player.party.map((r, i) => this.partyCard(r, i, 'party')).join('') ||
      '<p class="text-slate-500 text-sm">Empty party</p>';
    vault.innerHTML = Player.vault.map((r, i) => this.partyCard(r, i, 'vault')).join('') ||
      '<p class="text-slate-500 text-sm">Vault is empty. Capture wild Resonants!</p>';
  },

  partyCard(r, i, source) {
    const sp = SPECIES_MAP[r.speciesId];
    const pct = Math.floor((r.hp / r.maxHp) * 100);
    const hpCol = pct > 50 ? '#4ade80' : pct > 25 ? '#fbbf24' : '#f87171';
    const isActive = source === 'party' && i === Player.activeIndex;
    let actions = '';
    if (source === 'party') {
      actions = `
        <button onclick="event.stopPropagation();Game.setActive(${i})" class="btn-game text-[10px] px-2 py-1 rounded glass" onmouseenter="SFX.hover()">Lead</button>
        ${Player.party.length > 1 ? `<button onclick="event.stopPropagation();Game.toVault(${i})" class="btn-game text-[10px] px-2 py-1 rounded glass" onmouseenter="SFX.hover()">Vault</button>` : ''}
      `;
    } else {
      actions = `
        ${Player.party.length < 3 ? `<button onclick="event.stopPropagation();Game.toParty(${i})" class="btn-game text-[10px] px-2 py-1 rounded glass" onmouseenter="SFX.hover()">To Party</button>` : '<span class="text-[10px] text-slate-500">Party full</span>'}
      `;
    }
    return `<div class="glass-strong rounded-xl p-3 flex items-center gap-3 ${isActive ? 'neon-' + r.type.toLowerCase() : ''}" onclick="Game.showDetails(${i},'${source}')">
      <div class="flex-shrink-0">${makeSprite(sp, 56)}</div>
      <div class="flex-1 min-w-0">
        <div class="flex items-center gap-2">
          <span class="font-bold text-sm text-white">${r.name}</span>
          <span class="type-badge text-${r.type.toLowerCase()}">${r.type}</span>
          <span class="text-xs text-slate-400">Lv.${r.level}</span>
        </div>
        <div class="flex items-center gap-2 mt-1">
          <div class="flex-1 h-2 rounded-full bg-black/40 overflow-hidden">
            <div class="h-full rounded-full" style="width:${pct}%;background:${hpCol}"></div>
          </div>
          <span class="text-[10px] font-mono text-slate-400">${r.hp}/${r.maxHp}</span>
        </div>
        <div class="text-[10px] text-slate-500 mt-1">ATK ${r.atk} · DEF ${r.def} · SPD ${r.spd}</div>
      </div>
      <div class="flex flex-col gap-1">${actions}</div>
    </div>`;
  },

  setActive(i) {
    Player.activeIndex = i;
    SFX.select();
    saveGame();
    this.renderParty();
    this.renderPartyPreview();
  },

  toVault(i) {
    if (Player.party.length <= 1) return;
    const r = Player.party.splice(i, 1)[0];
    Player.vault.push(r);
    if (Player.activeIndex >= Player.party.length) Player.activeIndex = 0;
    SFX.select();
    saveGame();
    this.renderParty();
  },

  toParty(i) {
    if (Player.party.length >= 3) return;
    const r = Player.vault.splice(i, 1)[0];
    Player.party.push(r);
    SFX.select();
    saveGame();
    this.renderParty();
  },

  showDetails(i, source) {
    const r = source === 'party' ? Player.party[i] : Player.vault[i];
    if (!r) return;
    SFX.select();
    const sp = SPECIES_MAP[r.speciesId];
    const movesHtml = r.moves.map(mid => {
      const m = MOVES[mid];
      return `<div class="glass rounded-lg px-3 py-2 flex justify-between items-center">
        <span class="text-sm font-semibold text-white">${m.name}</span>
        <span class="type-badge text-${m.element.toLowerCase()}">${m.element}</span>
        <span class="text-xs text-slate-400">PWR ${m.power}</span>
      </div>`;
    }).join('');
    const evo = sp.evoTo ? `<p class="text-xs text-slate-400 mt-2">Evolves into <span class="text-violet-300 font-semibold">${SPECIES_MAP[sp.evoTo].name}</span> at Lv.${sp.evoLevel}</p>` : '<p class="text-xs text-slate-500 mt-2">Final form</p>';
    document.getElementById('details-content').innerHTML = `
      <div class="text-center mb-4">
        <div class="flex justify-center mb-2">${makeSprite(sp, 100)}</div>
        <h3 class="font-bold text-xl text-white">${r.name}</h3>
        <div class="flex justify-center gap-2 mt-1">
          <span class="type-badge text-${r.type.toLowerCase()}">${r.type}</span>
          <span class="text-sm text-slate-400">Lv.${r.level}</span>
        </div>
        <p class="text-xs text-slate-400 mt-2 leading-relaxed">${sp.desc}</p>
        ${evo}
      </div>
      <div class="space-y-2 mb-4">
        ${[['HP', r.hp + '/' + r.maxHp, r.maxHp, 120], ['ATK', r.atk, r.atk, 120], ['DEF', r.def, r.def, 120], ['SPD', r.spd, r.spd, 120]].map(([label, val, num, max]) => `
          <div class="flex items-center gap-2 text-xs">
            <span class="w-8 text-slate-400 font-bold">${label}</span>
            <div class="flex-1 stat-bar-bg"><div class="stat-bar-fill bg-${r.type.toLowerCase()}" style="width:${Math.min(100, (num/max)*100)}%;opacity:0.7"></div></div>
            <span class="w-14 text-right font-mono text-slate-300">${val}</span>
          </div>
        `).join('')}
      </div>
      <div class="text-xs font-bold text-slate-400 uppercase mb-2">Moves</div>
      <div class="space-y-1.5">${movesHtml}</div>
      <div class="mt-3 text-xs text-slate-500">XP: ${r.xp}/${r.xpToNext}</div>
    `;
    const modal = document.getElementById('modal-details');
    modal.classList.remove('hidden');
    modal.classList.add('flex');
  },

  closeDetails() {
    document.getElementById('modal-details').classList.add('hidden');
    document.getElementById('modal-details').classList.remove('flex');
  },

  // ---- DEX ----
  filterDex(type) {
    this.dexFilter = type;
    SFX.hover();
    this.renderDex();
  },

  renderDex() {
    const seen = Object.keys(Player.seen || {}).length;
    document.getElementById('dex-count').textContent = `Seen ${seen}/18 · Caught ${Object.keys(Player.caught || {}).length}/18`;
    document.querySelectorAll('#dex-filters button').forEach(b => {
      b.classList.toggle('ring-1', b.dataset.filter === this.dexFilter);
      b.classList.toggle('ring-white/30', b.dataset.filter === this.dexFilter);
    });
    const grid = document.getElementById('dex-grid');
    const list = SPECIES.filter(s => this.dexFilter === 'all' || s.type === this.dexFilter);
    grid.innerHTML = list.map(sp => {
      const known = Player.seen && Player.seen[sp.id];
      const owned = Player.caught && Player.caught[sp.id];
      if (!known) {
        return `<div class="dex-card glass rounded-xl p-3 text-center opacity-40">
          <div class="h-16 flex items-center justify-center text-3xl text-slate-600">?</div>
          <div class="text-xs text-slate-500 font-bold mt-1">???</div>
          <div class="text-[10px] text-slate-600">Unknown</div>
        </div>`;
      }
      return `<div class="dex-card glass-strong rounded-xl p-3 text-center neon-${sp.type.toLowerCase()} cursor-pointer" onclick="Game.showDexDetail('${sp.id}')" onmouseenter="SFX.hover()">
        <div class="flex justify-center">${makeSprite(sp, 64)}</div>
        <div class="text-xs font-bold text-white mt-1">${sp.name}</div>
        <div class="flex justify-center gap-1 mt-1">
          <span class="type-badge text-${sp.type.toLowerCase()}">${sp.type}</span>
          ${owned ? '<span class="text-[9px] text-emerald-400">✓</span>' : ''}
        </div>
        <div class="text-[10px] text-slate-500 mt-1">Stage ${sp.stage}</div>
      </div>`;
    }).join('');
  },

  showDexDetail(id) {
    const sp = SPECIES_MAP[id];
    if (!sp || !Player.seen[id]) return;
    SFX.select();
    const movesHtml = sp.moves.map(m => {
      const mv = MOVES[m.id];
      return `<div class="text-xs flex justify-between glass rounded px-2 py-1">
        <span>${mv.name}</span><span class="text-slate-400">Lv.${m.unlock} · ${mv.power}pwr</span>
      </div>`;
    }).join('');
    document.getElementById('details-content').innerHTML = `
      <div class="text-center">
        <div class="flex justify-center">${makeSprite(sp, 110)}</div>
        <h3 class="font-bold text-xl text-white mt-2">${sp.name}</h3>
        <span class="type-badge text-${sp.type.toLowerCase()}">${sp.type}</span>
        <p class="text-xs text-slate-400 mt-3 leading-relaxed">${sp.desc}</p>
      </div>
      <div class="grid grid-cols-4 gap-2 my-4 text-center">
        ${[['HP', sp.base.hp],['ATK', sp.base.atk],['DEF', sp.base.def],['SPD', sp.base.spd]].map(([l,v]) =>
          `<div class="glass rounded-lg p-2"><div class="text-[10px] text-slate-400">${l}</div><div class="font-bold text-white">${v}</div></div>`
        ).join('')}
      </div>
      ${sp.evoTo ? `<p class="text-xs text-center text-violet-300 mb-3">Evolves → ${SPECIES_MAP[sp.evoTo].name} @ Lv.${sp.evoLevel}</p>` : '<p class="text-xs text-center text-slate-500 mb-3">Final Evolution</p>'}
      <div class="space-y-1">${movesHtml}</div>
    `;
    const modal = document.getElementById('modal-details');
    modal.classList.remove('hidden');
    modal.classList.add('flex');
  },

  // ---- SHOP ----
  showShop() {
    SFX.select();
    document.getElementById('shop-shards').textContent = `◆ ${Player.shards} Aether Shards`;
    document.getElementById('shop-items').innerHTML = SHOP.map(item => {
      const owned = Player.inventory[item.id] || 0;
      return `<div class="glass rounded-xl p-3 flex items-center gap-3">
        <div class="text-2xl w-10 text-center">${item.icon}</div>
        <div class="flex-1">
          <div class="font-semibold text-sm text-white">${item.name}</div>
          <div class="text-[11px] text-slate-400">${item.desc}</div>
          <div class="text-[10px] text-slate-500 mt-0.5">Owned: ${owned}</div>
        </div>
        <button onclick="Game.buyItem('${item.id}')" class="btn-game glass px-3 py-2 rounded-lg text-xs font-bold text-amber-300" onmouseenter="SFX.hover()">
          ◆ ${item.price}
        </button>
      </div>`;
    }).join('');
    const m = document.getElementById('modal-shop');
    m.classList.remove('hidden');
    m.classList.add('flex');
  },

  closeShop() {
    const m = document.getElementById('modal-shop');
    m.classList.add('hidden');
    m.classList.remove('flex');
  },

  buyItem(id) {
    const item = SHOP.find(s => s.id === id);
    if (!item) return;
    if (Player.shards < item.price) {
      this.toast('Not enough Aether Shards!');
      SFX.fail();
      return;
    }
    Player.shards -= item.price;
    Player.inventory[id] = (Player.inventory[id] || 0) + 1;
    SFX.select();
    saveGame();
    this.updateHUD();
    this.showShop();
    this.toast(`Purchased ${item.name}!`);
  },

  // ---- DIALOG / RESULT ----
  showDialog(title, text, actions) {
    document.getElementById('dialog-title').textContent = title;
    document.getElementById('dialog-text').textContent = text;
    document.getElementById('dialog-portrait').innerHTML = '';
    document.getElementById('dialog-actions').innerHTML = actions.map((a, i) =>
      `<button class="btn-game px-4 py-2 rounded-xl ${i === 0 ? 'bg-gradient-to-r from-violet-600 to-indigo-600 text-white font-bold' : 'glass text-slate-300'} text-sm" data-idx="${i}" onmouseenter="SFX.hover()">${a.label}</button>`
    ).join('');
    document.getElementById('dialog-actions').querySelectorAll('button').forEach((btn, i) => {
      btn.onclick = () => actions[i].fn();
    });
    const m = document.getElementById('modal-dialog');
    m.classList.remove('hidden');
    m.classList.add('flex');
  },

  closeDialog() {
    const m = document.getElementById('modal-dialog');
    m.classList.add('hidden');
    m.classList.remove('flex');
  },

  showResult(icon, title, text, rewards) {
    document.getElementById('result-icon').textContent = icon;
    document.getElementById('result-title').textContent = title;
    document.getElementById('result-text').textContent = text;
    document.getElementById('result-rewards').innerHTML = (rewards || []).map(r => `<div>${r}</div>`).join('');
    const m = document.getElementById('modal-result');
    m.classList.remove('hidden');
    m.classList.add('flex');
  },

  closeResult() {
    const m = document.getElementById('modal-result');
    m.classList.add('hidden');
    m.classList.remove('flex');
    if (this.pendingResult) {
      const fn = this.pendingResult;
      this.pendingResult = null;
      fn();
    }
  },

  toast(msg) {
    const t = document.getElementById('toast');
    t.textContent = msg;
    t.classList.add('show');
    clearTimeout(this._toastTimer);
    this._toastTimer = setTimeout(() => t.classList.remove('show'), 2500);
  }
};

// ============================================================
// BATTLE ENGINE
// ============================================================
const Battle = {
  state: null,
  busy: false,

  start(cfg) {
    // Reset stage mods
    Player.party.forEach(r => { r.stages = { atk: 0, def: 0, spd: 0 }; });
    let activeIdx = Player.activeIndex;
    if (Player.party[activeIdx].hp <= 0) {
      activeIdx = Player.party.findIndex(r => r.hp > 0);
      if (activeIdx < 0) {
        Game.toast('No conscious Resonants!');
        return;
      }
      Player.activeIndex = activeIdx;
    }

    this.state = {
      ...cfg,
      player: Player.party[Player.activeIndex],
      turn: 0,
      over: false,
      canCapture: cfg.type === 'wild'
    };
    this.busy = false;
    Game.showScreen('battle');
    this.render();
    this.showMain();
    this.log(`A ${cfg.title} appeared!`);
    if (cfg.type === 'wild') {
      this.log(`Wild ${cfg.enemy.name} (Lv.${cfg.enemy.level}) wants to battle!`);
    } else {
      this.log(`${cfg.title} challenges you!`);
    }
  },

  log(msg) {
    const el = document.getElementById('battle-log');
    const entry = document.createElement('div');
    entry.className = 'log-entry text-slate-300';
    entry.textContent = msg;
    el.appendChild(entry);
    el.scrollTop = el.scrollHeight;
  },

  clearLog() {
    document.getElementById('battle-log').innerHTML = '';
  },

  render() {
    const s = this.state;
    if (!s) return;
    const p = s.player;
    const e = s.enemy;
    const psp = SPECIES_MAP[p.speciesId];
    const esp = SPECIES_MAP[e.speciesId];

    // Enemy
    document.getElementById('enemy-name').textContent = (s.type === 'wild' ? 'Wild ' : '') + e.name;
    document.getElementById('enemy-level').textContent = 'Lv.' + e.level;
    const et = document.getElementById('enemy-type');
    et.textContent = e.type;
    et.className = 'type-badge text-' + e.type.toLowerCase();
    this.setHpBar('enemy', e.hp, e.maxHp);
    document.getElementById('enemy-sprite').innerHTML = makeSprite(esp, 104);
    document.getElementById('enemy-status').innerHTML = this.statusHtml(e);

    // Player
    document.getElementById('player-name').textContent = p.name;
    document.getElementById('player-level').textContent = 'Lv.' + p.level;
    const pt = document.getElementById('player-type');
    pt.textContent = p.type;
    pt.className = 'type-badge text-' + p.type.toLowerCase();
    this.setHpBar('player', p.hp, p.maxHp);
    document.getElementById('player-sprite').innerHTML = makeSprite(psp, 104);
    document.getElementById('player-status').innerHTML = this.statusHtml(p);
    const xpPct = Math.min(100, Math.floor((p.xp / p.xpToNext) * 100));
    document.getElementById('player-xp-bar').style.width = xpPct + '%';

    // Capture button
    document.getElementById('btn-capture').style.display = s.canCapture ? '' : 'none';

    // Neon borders
    document.getElementById('enemy-panel').className = 'glass-strong rounded-2xl p-4 mb-3 neon-' + e.type.toLowerCase();
    document.getElementById('player-panel').className = 'glass-strong rounded-2xl p-4 mb-3 neon-' + p.type.toLowerCase();
  },

  setHpBar(who, hp, max) {
    const pct = Math.max(0, Math.min(100, Math.floor((hp / max) * 100)));
    const bar = document.getElementById(who + '-hp-bar');
    const text = document.getElementById(who + '-hp-text');
    bar.style.width = pct + '%';
    let col = '#4ade80';
    if (pct <= 25) col = '#f87171';
    else if (pct <= 50) col = '#fbbf24';
    bar.style.background = col;
    text.textContent = `${Math.max(0, hp)}/${max}`;
  },

  statusHtml(r) {
    const tags = [];
    if (r.status) tags.push(`<span class="text-[10px] px-1.5 py-0.5 rounded bg-purple-900/50 text-purple-300">${r.status}</span>`);
    if (r.stages) {
      if (r.stages.atk !== 0) tags.push(`<span class="text-[10px] px-1.5 py-0.5 rounded bg-red-900/40 text-red-300">ATK${r.stages.atk > 0 ? '+' : ''}${r.stages.atk}</span>`);
      if (r.stages.def !== 0) tags.push(`<span class="text-[10px] px-1.5 py-0.5 rounded bg-blue-900/40 text-blue-300">DEF${r.stages.def > 0 ? '+' : ''}${r.stages.def}</span>`);
      if (r.stages.spd !== 0) tags.push(`<span class="text-[10px] px-1.5 py-0.5 rounded bg-cyan-900/40 text-cyan-300">SPD${r.stages.spd > 0 ? '+' : ''}${r.stages.spd}</span>`);
    }
    return tags.join('');
  },

  showMain() {
    ['action-main', 'action-moves', 'action-prisms', 'action-exchange', 'action-items'].forEach(id => {
      document.getElementById(id).classList.add('hidden');
    });
    document.getElementById('action-main').classList.remove('hidden');
    document.getElementById('btn-back').classList.add('hidden');
  },

  showMoves() {
    SFX.select();
    const p = this.state.player;
    const box = document.getElementById('action-moves');
    box.innerHTML = p.moves.map(mid => {
      const m = MOVES[mid];
      return `<button class="btn-game glass neon-${m.element.toLowerCase()} rounded-xl p-3 text-left" onclick="Battle.useMove('${mid}')" onmouseenter="SFX.hover()">
        <div class="flex justify-between items-center">
          <span class="font-bold text-sm text-white">${m.name}</span>
          <span class="type-badge text-${m.element.toLowerCase()}">${m.element}</span>
        </div>
        <div class="text-[11px] text-slate-400 mt-1">Power ${m.power} · Acc ${m.accuracy}%</div>
      </button>`;
    }).join('');
    this._showSub('action-moves');
  },

  showPrisms() {
    SFX.select();
    if (!this.state.canCapture) {
      Game.toast('Cannot capture trainer Resonants!');
      return;
    }
    const box = document.getElementById('action-prisms');
    const items = [
      { id: 'prism_basic', name: 'Harmonizer Prism', mult: 1.5 },
      { id: 'prism_hyper', name: 'Hyper-Conductive Prism', mult: 2.5 }
    ];
    box.innerHTML = items.map(it => {
      const count = Player.inventory[it.id] || 0;
      return `<button class="btn-game glass rounded-xl p-3 text-left ${count ? '' : 'opacity-40'}" ${count ? `onclick="Battle.tryCapture('${it.id}',${it.mult})"` : 'disabled'} onmouseenter="SFX.hover()">
        <div class="font-bold text-sm text-white">${it.name}</div>
        <div class="text-[11px] text-slate-400">Owned: ${count} · Capture x${it.mult}</div>
      </button>`;
    }).join('');
    this._showSub('action-prisms');
  },

  showExchange() {
    SFX.select();
    const box = document.getElementById('action-exchange');
    box.innerHTML = Player.party.map((r, i) => {
      const fainted = r.hp <= 0;
      const active = i === Player.activeIndex;
      return `<button class="btn-game glass rounded-xl p-3 text-left flex items-center gap-3 ${fainted || active ? 'opacity-40' : ''}"
        ${fainted || active ? 'disabled' : `onclick="Battle.swapTo(${i})"`} onmouseenter="SFX.hover()">
        ${makeSprite(SPECIES_MAP[r.speciesId], 40)}
        <div>
          <div class="font-bold text-sm text-white">${r.name} ${active ? '(Active)' : ''}</div>
          <div class="text-[11px] text-slate-400">Lv.${r.level} · HP ${r.hp}/${r.maxHp}${fainted ? ' · Fainted' : ''}</div>
        </div>
      </button>`;
    }).join('');
    this._showSub('action-exchange');
  },

  showItems() {
    SFX.select();
    const box = document.getElementById('action-items');
    const usable = [
      { id: 'heal_core', name: 'Core Restorer', desc: 'Restore 50% HP' },
      { id: 'heal_full', name: 'Full Harmonic Draught', desc: 'Fully restore HP' },
      { id: 'heal_status', name: 'Resonance Elixir', desc: 'Clear status & stages' }
    ];
    box.innerHTML = usable.map(it => {
      const count = Player.inventory[it.id] || 0;
      return `<button class="btn-game glass rounded-xl p-3 text-left ${count ? '' : 'opacity-40'}"
        ${count ? `onclick="Battle.useItem('${it.id}')"` : 'disabled'} onmouseenter="SFX.hover()">
        <div class="font-bold text-sm text-white">${it.name}</div>
        <div class="text-[11px] text-slate-400">${it.desc} · Owned: ${count}</div>
      </button>`;
    }).join('');
    this._showSub('action-items');
  },

  _showSub(id) {
    ['action-main', 'action-moves', 'action-prisms', 'action-exchange', 'action-items'].forEach(x => {
      document.getElementById(x).classList.add('hidden');
    });
    document.getElementById(id).classList.remove('hidden');
    document.getElementById(id).classList.add(id === 'action-moves' ? 'grid' : 'grid');
    document.getElementById('btn-back').classList.remove('hidden');
  },

  // ---- ACTIONS ----
  async useMove(moveId) {
    if (this.busy || this.state.over) return;
    this.busy = true;
    this.showMain();
    const move = MOVES[moveId];
    const p = this.state.player;
    const e = this.state.enemy;

    // Speed check for turn order
    const pSpd = p.spd * stageMult(p.stages.spd);
    const eSpd = e.spd * stageMult(e.stages.spd);
    const playerFirst = pSpd >= eSpd;

    if (playerFirst) {
      await this.execMove(p, e, move, true);
      if (e.hp <= 0) { await this.onEnemyFaint(); this.busy = false; return; }
      await this.delay(400);
      await this.enemyTurn();
      if (p.hp <= 0) { await this.onPlayerFaint(); this.busy = false; return; }
    } else {
      await this.enemyTurn();
      if (p.hp <= 0) { await this.onPlayerFaint(); this.busy = false; return; }
      await this.delay(400);
      await this.execMove(p, e, move, true);
      if (e.hp <= 0) { await this.onEnemyFaint(); this.busy = false; return; }
    }

    this.render();
    this.busy = false;
  },

  async execMove(attacker, defender, move, isPlayer) {
    this.log(`${attacker.name} used ${move.name}!`);

    // Accuracy
    if (Math.random() * 100 > move.accuracy) {
      this.log('But it missed!');
      SFX.fail();
      await this.delay(400);
      return;
    }

    // Effect moves (stat changes)
    if (move.effect && move.power <= 30) {
      await this.applyEffect(move.effect, attacker, defender, isPlayer);
      return;
    }

    const atkStat = attacker.atk * stageMult(attacker.stages.atk || 0);
    const defStat = defender.def * stageMult(defender.stages.def || 0);
    let dmg = (((2 * attacker.level / 5 + 2) * move.power * (atkStat / defStat)) / 50 + 2);
    const mod = typeMod(move.element, defender.type);
    dmg *= mod;
    const rand = 0.85 + Math.random() * 0.15;
    dmg *= rand;
    const crit = Math.random() < 0.1;
    if (crit) dmg *= 1.5;
    dmg = Math.max(1, Math.floor(dmg));

    defender.hp = Math.max(0, defender.hp - dmg);

    // VFX
    const panel = document.getElementById(isPlayer ? 'enemy-panel' : 'player-panel');
    const sprite = document.getElementById(isPlayer ? 'enemy-sprite' : 'player-sprite');
    if (crit || mod > 1) {
      panel.classList.add('shake');
      SFX.hitElemental();
    } else {
      SFX.hitPhysical();
    }
    sprite.classList.add('flash-hit');
    this.spawnParticles(sprite, ELEMENT_COLORS[move.element]);
    setTimeout(() => { panel.classList.remove('shake'); sprite.classList.remove('flash-hit'); }, 400);

    let msg = `Dealt ${dmg} damage!`;
    if (crit) msg += ' Critical hit!';
    if (mod > 1) msg += ' It\'s super effective!';
    if (mod < 1 && mod > 0) msg += ' It\'s not very effective...';
    this.log(msg);

    if (move.effect && move.power > 30) {
      await this.applyEffect(move.effect, attacker, defender, isPlayer);
    }

    this.render();
    await this.delay(350);
  },

  async applyEffect(effect, attacker, defender, isPlayer) {
    const target = (effect === 'defUp' || effect.endsWith('Up')) ? attacker : defender;
    const who = target === attacker ? attacker.name : defender.name;
    if (effect === 'atkDown') {
      target.stages.atk = Math.max(-3, target.stages.atk - 1);
      this.log(`${who}'s Attack fell!`);
    } else if (effect === 'defDown') {
      target.stages.def = Math.max(-3, target.stages.def - 1);
      this.log(`${who}'s Defense fell!`);
    } else if (effect === 'spdDown') {
      target.stages.spd = Math.max(-3, target.stages.spd - 1);
      this.log(`${who}'s Speed fell!`);
    } else if (effect === 'defUp') {
      target.stages.def = Math.min(3, target.stages.def + 1);
      this.log(`${who}'s Defense rose!`);
    }
    SFX.hover();
    this.render();
    await this.delay(300);
  },

  async enemyTurn() {
    const e = this.state.enemy;
    const p = this.state.player;
    if (e.hp <= 0) return;

    // Pick best move or random
    const moves = e.moves.map(id => MOVES[id]).filter(Boolean);
    let best = moves[0];
    let bestScore = -1;
    moves.forEach(m => {
      let score = m.power * typeMod(m.element, p.type);
      score *= 0.85 + Math.random() * 0.3;
      if (score > bestScore) { bestScore = score; best = m; }
    });
    await this.execMove(e, p, best, false);
  },

  async tryCapture(prismId, mult) {
    if (this.busy || this.state.over) return;
    this.busy = true;
    this.showMain();

    if ((Player.inventory[prismId] || 0) <= 0) {
      Game.toast('No prisms left!');
      this.busy = false;
      return;
    }
    Player.inventory[prismId]--;
    saveGame();

    const e = this.state.enemy;
    this.log(`Threw a Prism at ${e.name}!`);

    // Capture formula inspired by classic games
    const hpFactor = 1 - (e.hp / e.maxHp) * 0.6;
    const baseRate = 0.25 + (hpFactor * 0.45);
    const statusBonus = e.status ? 1.3 : 1;
    const rate = Math.min(0.92, baseRate * mult * statusBonus * (1 - e.level * 0.01));

    // Shake animation
    const sprite = document.getElementById('enemy-sprite');
    sprite.classList.add('capture-shake');
    await this.delay(600);
    sprite.classList.remove('capture-shake');

    if (Math.random() < rate) {
      SFX.capture();
      this.log(`${e.name} was harmonized into the Prism!`);
      Player.caught[e.speciesId] = true;
      Player.seen[e.speciesId] = true;
      const captured = cloneResonant(e);
      captured.uid = 'r_' + Date.now() + '_' + Math.random().toString(36).slice(2, 7);
      captured.stages = { atk: 0, def: 0, spd: 0 };
      captured.status = null;
      if (Player.party.length < 3) {
        Player.party.push(captured);
        this.log(`${e.name} joined your active party!`);
      } else {
        Player.vault.push(captured);
        this.log(`${e.name} was stored in the Resonance Vault.`);
      }
      saveGame();
      this.state.over = true;
      await this.delay(600);
      Game.showResult('💎', 'Capture Success!', `${e.name} Lv.${e.level} is now yours!`, [
        `Type: ${e.type}`,
        Player.party.find(r => r.uid === captured.uid) ? 'Added to Active Party' : 'Stored in Vault'
      ]);
      Game.pendingResult = () => {
        Game.showScreen('world');
        Game.renderWorld();
      };
    } else {
      SFX.fail();
      this.log(`Oh no! ${e.name} broke free!`);
      await this.delay(400);
      await this.enemyTurn();
      if (this.state.player.hp <= 0) {
        await this.onPlayerFaint();
      }
      this.render();
    }
    this.busy = false;
  },

  async swapTo(i) {
    if (this.busy || this.state.over) return;
    if (Player.party[i].hp <= 0 || i === Player.activeIndex) return;
    this.busy = true;
    this.showMain();
    Player.activeIndex = i;
    this.state.player = Player.party[i];
    this.state.player.stages = this.state.player.stages || { atk: 0, def: 0, spd: 0 };
    this.log(`Go, ${this.state.player.name}!`);
    SFX.select();
    this.render();
    await this.delay(400);
    await this.enemyTurn();
    if (this.state.player.hp <= 0) await this.onPlayerFaint();
    this.render();
    this.busy = false;
  },

  async useItem(itemId) {
    if (this.busy || this.state.over) return;
    if ((Player.inventory[itemId] || 0) <= 0) return;
    this.busy = true;
    this.showMain();
    Player.inventory[itemId]--;
    const p = this.state.player;

    if (itemId === 'heal_core') {
      const heal = Math.floor(p.maxHp * 0.5);
      p.hp = Math.min(p.maxHp, p.hp + heal);
      this.log(`Used Core Restorer! Restored ${heal} HP.`);
      SFX.heal();
    } else if (itemId === 'heal_full') {
      const heal = p.maxHp - p.hp;
      p.hp = p.maxHp;
      this.log(`Used Full Harmonic Draught! Fully restored (${heal} HP).`);
      SFX.heal();
    } else if (itemId === 'heal_status') {
      p.status = null;
      p.stages = { atk: 0, def: 0, spd: 0 };
      this.log('Used Resonance Elixir! Status cleansed.');
      SFX.heal();
    }
    saveGame();
    this.render();
    await this.delay(400);
    await this.enemyTurn();
    if (this.state.player.hp <= 0) await this.onPlayerFaint();
    this.render();
    this.busy = false;
  },

  async onEnemyFaint() {
    const s = this.state;
    this.log(`${s.enemy.name} was defeated!`);
    SFX.hitElemental();

    // More enemies in trainer/boss battles?
    if (s.enemies && s.enemyIndex < s.enemies.length - 1) {
      s.enemyIndex++;
      s.enemy = s.enemies[s.enemyIndex];
      this.log(`${s.title} sends out ${s.enemy.name}!`);
      this.render();
      await this.delay(600);
      return;
    }

    // Victory
    s.over = true;
    const xp = xpYield(s.enemies ? s.enemies.reduce((a, e) => a + e.level, 0) / s.enemies.length : s.enemy.level, s.type !== 'wild');
    const shardGain = s.shards || (8 + s.enemy.level * 2 + Math.floor(Math.random() * 10));
    Player.shards += shardGain;

    // Give XP to active and a bit to others
    const events = [];
    for (const r of Player.party) {
      if (r.hp <= 0 && r !== s.player) continue;
      const gain = r === s.player ? xp : Math.floor(xp * 0.4);
      if (gain <= 0) continue;
      const msgs = await this.grantXP(r, gain);
      events.push(...msgs);
    }

    if (s.type === 'trainer' && s.nodeId != null) {
      Game.markNode(s.nodeId);
    }
    if (s.type === 'boss' && s.zone) {
      Player.defeatedBosses.push(s.zone.id);
      Game.markNode(s.zone.nodes.find(n => n.type === 'boss').id);
      // Unlock next zone
      const next = s.zone.id + 1;
      if (next < ZONES.length && !Player.unlockedZones.includes(next)) {
        Player.unlockedZones.push(next);
        events.push(`Zone unlocked: ${ZONES[next].name}!`);
      }
      // Boss defeat dialog stored
      events.push(s.zone.boss.defeat);
    }

    saveGame();
    await this.delay(500);

    const rewards = [`+${Math.floor(xp)} XP`, `◆ ${shardGain} Aether Shards`, ...events];
    const title = s.type === 'boss' ? 'Gatekeeper Defeated!' : s.type === 'trainer' ? 'Victory!' : 'Wild Resonant Defeated!';
    const icon = s.type === 'boss' ? '👑' : '⭐';
    Game.showResult(icon, title, `You defeated ${s.title}!`, rewards);
    Game.pendingResult = () => {
      // Check party still ok
      Game.showScreen('world');
      Game.renderWorld();
    };
  },

  async grantXP(r, amount) {
    const msgs = [];
    r.xp += amount;
    msgs.push(`${r.name} gained ${amount} XP`);
    while (r.xp >= r.xpToNext && r.level < 25) {
      r.xp -= r.xpToNext;
      r.level++;
      r.xpToNext = xpForLevel(r.level);
      refreshStats(r);
      r.hp = r.maxHp;
      msgs.push(`${r.name} reached Lv.${r.level}!`);
      SFX.levelUp();

      // Evolution
      const sp = SPECIES_MAP[r.speciesId];
      if (sp.evoTo && sp.evoLevel && r.level >= sp.evoLevel) {
        const oldName = r.name;
        r.speciesId = sp.evoTo;
        refreshStats(r);
        r.hp = r.maxHp;
        Player.seen[r.speciesId] = true;
        Player.caught[r.speciesId] = true;
        msgs.push(`${oldName} evolved into ${r.name}!`);
        SFX.levelUp();
      }
    }
    return msgs;
  },

  async onPlayerFaint() {
    this.log(`${this.state.player.name} fainted!`);
    SFX.fail();
    const aliveIdx = Player.party.findIndex(r => r.hp > 0);
    if (aliveIdx >= 0) {
      this.busy = true;
      this._forcedSwap = true;
      this.log('Choose your next Resonant!');
      this.render();
      this.showExchange();
      return;
    }
    this.state.over = true;
    this.log('All Resonants have fainted...');
    await this.delay(800);
    const loss = Math.floor(Player.shards * 0.1);
    Player.shards = Math.max(0, Player.shards - loss);
    Player.party.forEach(r => { r.hp = Math.max(1, Math.floor(r.maxHp * 0.3)); r.status = null; });
    saveGame();
    Game.showResult('💀', 'Defeat...', 'Your party was overwhelmed. You retreat to recover.', [
      loss > 0 ? `Lost ◆ ${loss} Aether Shards` : 'No shards lost',
      'Party partially restored'
    ]);
    Game.pendingResult = () => {
      Game.showScreen('world');
      Game.renderWorld();
    };
  },

  spawnParticles(el, color) {
    const rect = el.getBoundingClientRect();
    const container = document.getElementById('battle-container');
    for (let i = 0; i < 8; i++) {
      const p = document.createElement('div');
      p.className = 'particle';
      const size = 4 + Math.random() * 6;
      p.style.width = size + 'px';
      p.style.height = size + 'px';
      p.style.background = color;
      p.style.left = (rect.left + rect.width / 2 - container.getBoundingClientRect().left) + 'px';
      p.style.top = (rect.top + rect.height / 2 - container.getBoundingClientRect().top) + 'px';
      p.style.setProperty('--dx', (Math.random() - 0.5) * 80 + 'px');
      p.style.setProperty('--dy', (Math.random() - 0.5) * 80 - 20 + 'px');
      container.appendChild(p);
      setTimeout(() => p.remove(), 2000);
    }
  },

  delay(ms) {
    return new Promise(r => setTimeout(r, ms));
  }
};

const _origSwapTo = Battle.swapTo.bind(Battle);
Battle.swapTo = async function(i) {
  if (this._forcedSwap) {
    if (Player.party[i].hp <= 0) return;
    this._forcedSwap = false;
    this.showMain();
    Player.activeIndex = i;
    this.state.player = Player.party[i];
    this.state.player.stages = this.state.player.stages || { atk: 0, def: 0, spd: 0 };
    this.log(`Go, ${this.state.player.name}!`);
    SFX.select();
    this.render();
    this.busy = false;
    return;
  }
  return _origSwapTo(i);
};

// ============================================================
// BOOT
// ============================================================
document.addEventListener('click', () => SFX.init(), { once: true });
document.addEventListener('DOMContentLoaded', () => {
  Game.init();
  // If saved party exists, HUD ready; menu shows continue
  if (Player.party && Player.party.length) {
    Game.updateHUD();
  }
});
</script>
</body>
</html>