← Aethelgard results

Aethelgard

Claude haiku 5.5 · full

Initial view of Aethelgard

Reviews & generation details

Reviews

No human review yet. Unreviewed does not mean unsuccessful.

Generation context

Task text captured before this run. Effective runtime system instructions were not captured.

Generation prompt · 0840347b81
Act as an elite frontend engineer and game designer. Your goal is to write a fully complete, production-ready, beautifully polished HTML/CSS/JS game. The game is a turn-based monster-collection and battling RPG inspired by classic retro handheld games, but reimagined with a modern, clean, glassmorphic UI. It must contain at least 1 hour of compelling gameplay, a deep combat system, a functional world progression, and an integrated local save state. CRITICAL: Do not use placeholders, "TODO" comments, or truncated code. Write every line of CSS, JSON data, and JavaScript logic completely. --- ### 1. THEME & LORE (Legally Distinct) - World Setting: Aethelgard, a world vibrating with elemental energy. - Creatures: "Resonants" — spirits manifested from elemental frequencies. - Trainers: "Conductors" — people who use harmonic tuning devices to capture and direct Resonants. - Capture Device: "Aether Prisms" — crystalline geometric matrices that harmonize with a Resonant's energy to store them safely. - Elements: * Pyro (Fire) - strong against Flora, weak against Hydro * Hydro (Water) - strong against Pyro, weak against Flora * Flora (Nature) - strong against Hydro, weak against Pyro * Zephyr (Wind) - strong against Earth, weak against Volt * Earth (Terra) - strong against Volt, weak against Zephyr * Volt (Electric) - strong against Zephyr, weak against Earth * Cosmos (Neutral/Void) - no type advantages/disadvantages --- ### 2. VISUAL DESIGN & UI SYSTEM Implement a highly polished, responsive interface using Tailwind CSS (via CDN) and custom CSS injected in a `<style>` block. - Aesthetics: Dark mode by default. Deep slate/violet background, frosted glass cards (backdrop-filter: blur), vibrant element-themed neon borders and glow effects (e.g., Pyro glows reddish-orange, Volt glows cyan/yellow). - Screen Transitions: Smooth sliding or fading transitions between the Main Menu, Overworld Navigation, Battle Screen, and Conductors Dex (Encyclopaedia). - Animations: - Screen shake effect when taking critical damage. - Flash effects for successful hits. - Particle drift for elemental status effects. - Bounce animations for active Resonants in battles. - Sound System: Write a lightweight Web Audio API synthesizer class to generate retro 8-bit sound effects directly in JS. Implement sounds for: UI Hover, Select, Hit (Physical), Hit (Elemental/Super Effective), Level Up, and Capture Success. --- ### 3. GAMEPLAY ENGINE & SYSTEMS #### A. Overworld & Exploration Map Instead of a complex 2D physics engine, design a gorgeous, interactive "Zone-based Node Map." - Zones: Players must unlock and progress through 4 distinct zones: 1. The Verdant Canopy (Levels 1-5, Flora/Zephyr heavy) 2. The Sunken Grotto (Levels 5-10, Hydro/Earth heavy) 3. The Obsidian Crags (Levels 10-15, Pyro/Volt heavy) 4. The Apex Pillar (Level 15+, Final Boss Gauntlet) - Mechanics: Within each zone, the player clicks nodes on a visual trail. Nodes can be: - Wild Encounters (Tall Grass) - Mystery Caches (Items, Currencies) - Wandering Conductors (NPC Battles) - Healing Springs (Restores team health) - Zone Gatekeeper (Boss battle required to open the next zone) #### B. The Combat System A tactical, turn-based 1v1 battle system: - Interface: Show both Resonants' level, HP bar (with color transitions from green -> yellow -> red), XP progress, status effects, and beautiful vector sprites/icons (constructed using styled SVG elements dynamically rendered based on the monster class). - Actions: - Attack: Choose from 4 unique moves dynamically unlocked via leveling. - Prisms: Attempt to capture wild Resonants (capture rate based on current HP remaining, status, and Prism tier). - Exchange: Swap the active Resonant with a conscious party member (up to 3 in active party). - Inventory: Use items (Vitals Elixir, Status Cleanser, Prism Boosters). - Combat Math: - Damage = (((2 * Level / 5 + 2) * MovePower * (Attack / Defense)) / 50 + 2) * TypeModifier * Random(0.85, 1.00) - Critical Hits: 10% chance to deal 1.5x damage. #### C. Database of Resonants Include exactly 18 fully budgeted Resonants (6 elemental families, each with a 3-tier evolutionary line). For example: - Emberkit (Pyro, Level 1) -> Pyrowolf (Pyro, Level 8) -> Ignisaur (Pyro, Level 15) - Droplet (Hydro, Level 1) -> Geoxus (Hydro, Level 8) -> Neptunian (Hydro, Level 15) - Seedling (Flora, Level 1) -> Thornback (Flora, Level 8) -> Bloomgoliath (Flora, Level 15) - Sparky (Volt, Level 1) -> Boltsaber (Volt, Level 8) -> Stormlord (Volt, Level 15) - Gusty (Zephyr, Level 1) -> Slipstream (Zephyr, Level 8) -> Zephyrus (Zephyr, Level 15) - Pebble (Earth, Level 1) -> Obsidian (Earth, Level 8) -> Terrasunder (Earth, Level 15) Each database entry must have: - Name, Type, Base Stats (HP, Attack, Defense, Speed), Sprite SVG parameters, Evolution Level, and an array of unlockable Moves (Move Name, Power, Accuracy, Element, Unlock Level). --- ### 4. PROGRESSION & SAVING - Leveling: Defeating wild Resonants or Conductors yields XP. Reaching XP thresholds increases stats and evolves the Resonant at level 8 and 15. - Economy & Shop: Earn "Aether Shards" from battles. Spend them at the Camp Shop to buy: - Harmonizer Prism (Basic Catch) - Hyper-Conductive Prism (High Catch Rate) - Core Restorer (Restores 50% HP) - Resonance Elixir (Heals all status conditions) - Save File: Automatically save state parsing the entire Player profile (Active party, inventory, unlocked zones, shards, and completed boss battles) to `localStorage` on every change. Offer a "Reset Progress" button in the menu. --- ### 5. EXECUTION & CODE ARCHITECTURE Structure guidelines: 1. 2. `<head>`: Tailwind CDN, Google Fonts (e.g., 'Inter', 'Press Start 2P' for retro headings), custom styled-components inside `<style>`. 3. `<body>`: Wrapper with dark-mode background and layout containers. 4. UI screens inside `<div id="app">` hidden/shown using JS state controllers (e.g., swapping classes like `hidden`). 5. JavaScript `<script>` block at the bottom: - Synth Manager class (Web Audio API) - Resonants Database array - Player Profile object (State) - Game Loop Engine (Handling map navigation, combat cycles, UI rendering, math, and Save/Load logic). DO NOT truncate any arrays. DO NOT use comments like `// insert all other monsters here`. Generate the complete code containing all 18 monsters, 3 zones, unique dialog for the 4 major bosses, and full HTML templates. Make the SVGs look premium, utilizing paths, polylines, gradients, and distinct features per element type.
Inspect original source 111,154 bytes · SHA-256 494fab4d5295
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Aethelgard — Resonance Conductor Chronicles</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700;800&family=Press+Start+2P&display=swap" rel="stylesheet">
<style>
:root{
  --bg1:#080616; --bg2:#1a1038; --line:rgba(255,255,255,.12);
  --txt:#ebe8ff; --mute:#9d95c7; --accent:#8b5cf6; --c:#8b5cf6;
  --ok:#34d399; --warn:#facc15; --bad:#f87171;
}
*{box-sizing:border-box}
html,body{height:100%;margin:0}
body{
  font-family:'Inter',system-ui,-apple-system,sans-serif;color:var(--txt);
  background:
    radial-gradient(1000px 640px at 85% -10%,rgba(139,92,246,.28),transparent 60%),
    radial-gradient(860px 600px at -10% 110%,rgba(14,116,144,.25),transparent 60%),
    linear-gradient(160deg,var(--bg1),var(--bg2) 60%,#0a1020);
  overflow:hidden;user-select:none;-webkit-user-select:none;
}
#app{position:fixed;inset:0}
.font-retro{font-family:'Press Start 2P',monospace;letter-spacing:.5px}
.screen{
  position:absolute;inset:0;display:flex;flex-direction:column;overflow:hidden;
  opacity:0;transform:translateX(48px);pointer-events:none;visibility:hidden;
  transition:opacity .45s ease,transform .5s cubic-bezier(.2,.8,.2,1),visibility .5s;
}
.screen.active{opacity:1;transform:none;pointer-events:auto;visibility:visible}
.screen.exit-left{transform:translateX(-48px)}
.glass{
  background:linear-gradient(145deg,rgba(255,255,255,.10),rgba(255,255,255,.025));
  backdrop-filter:blur(16px) saturate(140%);-webkit-backdrop-filter:blur(16px) saturate(140%);
  border:1px solid var(--line);border-radius:18px;
  box-shadow:0 12px 40px rgba(0,0,0,.35),inset 0 1px 0 rgba(255,255,255,.08);
}
.neon{border-color:var(--c)!important;box-shadow:0 0 22px -4px var(--c),inset 0 0 26px -14px var(--c)!important}
.t-pyro{--c:#ff5a2a} .t-hydro{--c:#2aa8ff} .t-flora{--c:#4ade80} .t-volt{--c:#facc15}
.t-zephyr{--c:#67e8f9} .t-earth{--c:#d6a25c} .t-cosmos{--c:#a78bfa}
.txt-c{color:var(--c)}
.small{font-size:10px}
.muted{color:var(--mute)}
.hidden-x{display:none!important}

/* Buttons */
.btn{
  position:relative;padding:11px 18px;border-radius:12px;border:1px solid var(--line);
  background:rgba(255,255,255,.06);color:var(--txt);font-weight:700;font-size:14px;cursor:pointer;
  transition:transform .15s ease,background .2s ease,box-shadow .2s ease,border-color .2s ease;
  backdrop-filter:blur(8px);outline:none;
}
.btn:hover:not(:disabled){transform:translateY(-2px);background:rgba(255,255,255,.14);border-color:rgba(255,255,255,.3);box-shadow:0 8px 24px -8px rgba(139,92,246,.6)}
.btn:active:not(:disabled){transform:translateY(0) scale(.98)}
.btn:disabled{opacity:.38;cursor:not-allowed}
.btn.big{padding:16px 22px;font-size:16px;min-width:280px}
.btn.primary{background:linear-gradient(135deg,rgba(139,92,246,.7),rgba(34,211,238,.45));border-color:rgba(255,255,255,.25)}
.btn.primary:hover:not(:disabled){background:linear-gradient(135deg,rgba(139,92,246,.95),rgba(34,211,238,.6))}
.btn.danger{background:linear-gradient(135deg,rgba(220,38,38,.55),rgba(249,115,22,.35))}
.btn.ghost{background:transparent}
.btn.small-btn{padding:6px 10px;font-size:12px;border-radius:9px}
.btn.move{text-align:left;display:flex;flex-direction:column;gap:3px;padding:10px 12px;border-left:4px solid var(--c)}
.btn.move .mt{font-size:11px;opacity:.8;font-weight:600}
.btn.move:hover:not(:disabled){box-shadow:0 0 22px -6px var(--c)}
.btn.item-btn{display:flex;justify-content:space-between;align-items:center;text-align:left}

/* Layout helpers */
.top-bar{display:flex;align-items:center;justify-content:space-between;padding:16px 22px;gap:12px}
.content{flex:1;overflow:auto;padding:8px 22px 22px}
.center-col{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:14px;position:relative;z-index:2;padding:20px}
.menu-stack{display:flex;flex-direction:column;gap:12px;margin-top:26px;align-items:center}
.title{font-size:44px;margin:0;background:linear-gradient(90deg,#ff7a45,#c084fc,#67e8f9,#facc15);-webkit-background-clip:text;background-clip:text;color:transparent;background-size:300% 100%;animation:titleShift 8s linear infinite;filter:drop-shadow(0 0 18px rgba(139,92,246,.45));text-align:center;line-height:1.5}
@keyframes titleShift{to{background-position:300% 0}}
.subtitle{color:var(--mute);letter-spacing:4px;text-transform:uppercase;font-size:12px;margin:0}
.logo-wrap{width:150px;height:150px;position:relative;display:grid;place-items:center}
.logo-glyph{width:120px;height:120px;border-radius:50%;border:2px solid rgba(255,255,255,.2);position:relative;animation:spin 16s linear infinite}
.logo-glyph::before,.logo-glyph::after{content:"";position:absolute;inset:14px;border-radius:50%;border:2px dashed rgba(103,232,249,.6)}
.logo-glyph::after{inset:30px;border:2px solid rgba(255,122,69,.7);transform:rotate(45deg);border-radius:12px}
@keyframes spin{to{transform:rotate(360deg)}}
.bg-orbs{position:absolute;inset:0;overflow:hidden;pointer-events:none;z-index:1}
.bg-orbs i{position:absolute;border-radius:50%;filter:blur(60px);opacity:.45;animation:float 14s ease-in-out infinite alternate}
.bg-orbs i:nth-child(1){width:340px;height:340px;background:#7c3aed;left:-80px;top:10%}
.bg-orbs i:nth-child(2){width:300px;height:300px;background:#0ea5e9;right:-60px;top:40%;animation-delay:-4s}
.bg-orbs i:nth-child(3){width:240px;height:240px;background:#f97316;left:40%;bottom:-80px;animation-delay:-8s}
@keyframes float{from{transform:translate(0,0) scale(1)}to{transform:translate(40px,-30px) scale(1.12)}}

/* HUD */
.hud{margin:14px 16px 0;padding:12px 18px;display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap}
.hud-right{display:flex;gap:10px;align-items:center;flex-wrap:wrap}
.pill{display:inline-flex;align-items:center;gap:8px;padding:7px 12px;border-radius:999px;background:rgba(0,0,0,.3);border:1px solid var(--line);font-weight:700;font-size:13px}
.zone-tabs{display:flex;gap:10px;padding:14px 16px 0;overflow-x:auto}
.ztab{padding:9px 14px;border-radius:12px;border:1px solid var(--line);background:rgba(255,255,255,.05);font-weight:700;font-size:13px;cursor:pointer;white-space:nowrap;color:var(--txt);transition:all .25s}
.ztab.on{background:rgba(255,255,255,.15);border-color:var(--zc,#fff);box-shadow:0 0 18px -4px var(--zc,#fff)}
.ztab:disabled{opacity:.35;cursor:not-allowed}
.world-body{flex:1;display:grid;grid-template-columns:minmax(0,1fr) 300px;gap:14px;padding:14px 16px;min-height:0}
.map{position:relative;overflow:hidden;min-height:300px;background:
  radial-gradient(circle at 30% 20%,var(--zglow,rgba(139,92,246,.25)),transparent 55%),
  linear-gradient(180deg,rgba(10,8,30,.6),rgba(10,8,30,.25))}
.map svg#trail{position:absolute;inset:0;width:100%;height:100%}
.map .trail-line{fill:none;stroke:rgba(255,255,255,.18);stroke-width:.6;stroke-dasharray:1.5 1.5;vector-effect:non-scaling-stroke}
.map .trail-done{fill:none;stroke:var(--zc,#fff);stroke-width:.9;vector-effect:non-scaling-stroke;filter:drop-shadow(0 0 4px var(--zc,#fff))}
#nodes{position:absolute;inset:0}
.node{position:absolute;width:74px;height:74px;margin:-37px 0 0 -37px;border-radius:50%;display:grid;place-items:center;cursor:pointer;
  background:rgba(10,8,30,.7);border:2px solid rgba(255,255,255,.2);transition:transform .25s,box-shadow .3s,border-color .3s}
.node svg{width:38px;height:38px}
.node .nlabel{position:absolute;top:80px;left:50%;transform:translateX(-50%);white-space:nowrap;font-size:11px;font-weight:700;color:var(--mute);background:rgba(0,0,0,.4);padding:3px 8px;border-radius:8px}
.node.done{border-color:rgba(52,211,153,.7);opacity:.7}
.node.done::after{content:"✓";position:absolute;top:-6px;right:-4px;width:20px;height:20px;border-radius:50%;background:var(--ok);color:#042;font-size:12px;font-weight:800;display:grid;place-items:center}
.node.locked{opacity:.4;cursor:not-allowed;filter:grayscale(.6)}
.node.current{border-color:var(--zc,#fff);box-shadow:0 0 24px -2px var(--zc,#fff);animation:pulseNode 1.6s ease-in-out infinite}
.node.current:hover{transform:scale(1.1)}
.node.gate.current{border-color:#f472b6;box-shadow:0 0 28px -2px #f472b6}
@keyframes pulseNode{0%,100%{transform:scale(1)}50%{transform:scale(1.07)}}
.zdeco i{position:absolute;width:6px;height:6px;border-radius:50%;background:var(--zc,#fff);opacity:.6;animation:floatUp 6s linear infinite;pointer-events:none}
@keyframes floatUp{0%{transform:translateY(0);opacity:0}15%{opacity:.7}100%{transform:translateY(-320px);opacity:0}}
.side{padding:16px;display:flex;flex-direction:column;gap:12px;overflow:auto}
.side h3{margin:0;font-size:13px;text-transform:uppercase;letter-spacing:2px;color:var(--mute)}
.mini{display:flex;gap:10px;align-items:center;padding:10px;border-radius:14px;background:rgba(0,0,0,.25);border:1px solid var(--line);margin-bottom:8px}
.mini .ms{width:54px;height:54px;flex:none}
.mini .mi{flex:1;min-width:0}
.mini .mn{font-weight:800;font-size:14px;display:flex;justify-content:space-between;gap:6px}
.bottom-nav{display:flex;gap:10px;padding:0 16px 16px;flex-wrap:wrap;justify-content:center}

/* HP / XP bars */
.hpbar{height:10px;border-radius:99px;background:rgba(0,0,0,.45);overflow:hidden;border:1px solid rgba(255,255,255,.08)}
.hpfill{height:100%;border-radius:99px;transition:width .7s cubic-bezier(.2,.8,.2,1),background-color .4s}
.xpbar{height:4px;border-radius:99px;background:rgba(0,0,0,.45);overflow:hidden;margin-top:6px}
.xpfill{height:100%;background:linear-gradient(90deg,#67e8f9,#a78bfa);transition:width .6s}
.hpnum{font-size:12px;font-weight:700;color:var(--mute);font-variant-numeric:tabular-nums}

/* Battle */
#s-battle{padding:0}
#arena{position:relative;flex:1;min-height:0;margin:10px 14px 0;border-radius:22px;overflow:hidden;
  background:
   radial-gradient(ellipse 60% 40% at 25% 85%,rgba(255,255,255,.08),transparent 70%),
   radial-gradient(ellipse 50% 50% at 75% 30%,rgba(139,92,246,.25),transparent 70%),
   linear-gradient(180deg,#140d2e,#0b0a1d 70%,#0c1830);
  border:1px solid var(--line);}
#arena::after{content:"";position:absolute;left:-10%;right:-10%;bottom:12%;height:24%;border-radius:50%;background:radial-gradient(closest-side,rgba(255,255,255,.07),transparent);pointer-events:none}
.bcard{position:absolute;width:min(330px,44%);padding:14px 16px;z-index:4}
#enemy-card{top:14px;right:16px}
#player-card{bottom:14px;right:16px}
.bcard .nm{display:flex;justify-content:space-between;align-items:center;gap:8px;font-weight:800;font-size:16px}
.bcard .tp{font-size:11px;font-weight:700;padding:3px 8px;border-radius:8px;background:rgba(0,0,0,.35);border:1px solid var(--c);color:var(--c);white-space:nowrap}
.bcard .st{display:flex;gap:6px;align-items:center;margin-top:6px;min-height:20px;font-size:11px;font-weight:700}
.sbadge{padding:2px 8px;border-radius:7px;font-size:10px;text-transform:uppercase;letter-spacing:1px}
.sbadge.burn{background:#ff5a2a;color:#fff}.sbadge.shock{background:#facc15;color:#222}.sbadge.root{background:#4ade80;color:#052}.sbadge.chill{background:#67e8f9;color:#034}
.sprite-wrap{position:absolute;z-index:3;display:grid;place-items:center}
#enemy-sprite{top:12%;right:15%;width:min(200px,27%);aspect-ratio:1}
#player-sprite{bottom:14%;left:12%;width:min(240px,31%);aspect-ratio:1}
.sprite-wrap svg{width:100%;height:100%;overflow:visible}
.sprite-wrap .shadow{position:absolute;bottom:-6%;left:10%;right:10%;height:12%;border-radius:50%;background:radial-gradient(closest-side,rgba(0,0,0,.6),transparent);z-index:-1}
.bounce{animation:bounce 1.6s ease-in-out infinite}
@keyframes bounce{0%,100%{transform:translateY(0)}50%{transform:translateY(-10px)}}
.enter-e{animation:slideInE .7s cubic-bezier(.2,.8,.2,1)}
.enter-p{animation:slideInP .7s cubic-bezier(.2,.8,.2,1)}
@keyframes slideInE{from{opacity:0;transform:translateX(60px)}to{opacity:1;transform:none}}
@keyframes slideInP{from{opacity:0;transform:translateX(-60px)}to{opacity:1;transform:none}}
.flash{animation:flash .45s steps(2,end)}
@keyframes flash{0%{filter:brightness(4) saturate(0)}50%{filter:brightness(2.2) drop-shadow(0 0 16px #fff)}100%{filter:none}}
.faint{animation:faintDrop .8s ease-in forwards}
@keyframes faintDrop{to{transform:translateY(60px);opacity:0}}
.shake{animation:shake .5s cubic-bezier(.36,.07,.19,.97) both}
@keyframes shake{10%,90%{transform:translate3d(-2px,0,0)}20%,80%{transform:translate3d(4px,0,0)}30%,50%,70%{transform:translate3d(-8px,0,0)}40%,60%{transform:translate3d(8px,0,0)}}
.crit-flash{position:absolute;inset:0;background:radial-gradient(circle,rgba(255,255,255,.35),transparent 70%);opacity:0;pointer-events:none;z-index:5}
.crit-flash.go{animation:critFade .6s ease-out}
@keyframes critFade{0%{opacity:1}100%{opacity:0}}
.fx-p{position:absolute;width:5px;height:5px;border-radius:50%;pointer-events:none;animation:drift 2.2s ease-out infinite;z-index:6}
@keyframes drift{0%{transform:translate(0,0) scale(1);opacity:0}20%{opacity:.9}100%{transform:translate(var(--dx),-70px) scale(.3);opacity:0}}
.status-fx{position:absolute;left:0;right:0;top:0;bottom:0;pointer-events:none;overflow:hidden;border-radius:18px}
.dmg-pop{position:absolute;z-index:8;font-weight:800;font-size:22px;pointer-events:none;animation:popUp 1s ease-out forwards;text-shadow:0 2px 10px rgba(0,0,0,.8)}
@keyframes popUp{0%{transform:translateY(0) scale(.6);opacity:0}15%{opacity:1;transform:translateY(-6px) scale(1.15)}100%{transform:translateY(-54px) scale(1);opacity:0}}
#battle-bottom{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:12px;padding:12px 14px 14px;height:248px;flex:none}
.log{padding:14px 16px;overflow:hidden;display:flex;flex-direction:column;justify-content:flex-end;gap:6px;font-size:14px;line-height:1.45}
.log p{margin:0;animation:logIn .35s ease;max-width:100%}
.log p.dim{opacity:.55}
.log p.sys{color:var(--warn)}
.log p.boss{color:#f9a8d4;font-style:italic}
@keyframes logIn{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}
#menu-box{padding:12px;display:flex;flex-direction:column;min-height:0;overflow:hidden}
#menu-main{display:grid;grid-template-columns:1fr 1fr;gap:10px;height:100%}
#menu-main .btn{font-size:15px;font-weight:800;letter-spacing:1px;text-transform:uppercase;display:flex;align-items:center;justify-content:center;gap:8px}
#menu-main .btn.fight{background:linear-gradient(135deg,rgba(255,90,42,.45),rgba(139,92,246,.35))}
#menu-sub{display:flex;flex-direction:column;gap:8px;height:100%;min-height:0}
#menu-sub .list{display:grid;grid-template-columns:1fr 1fr;gap:8px;overflow:auto;flex:1;min-height:0;padding-right:4px}
#menu-sub .list.one{grid-template-columns:1fr}
.menu-head{display:flex;justify-content:space-between;align-items:center;font-size:12px;color:var(--mute);font-weight:700}

/* Dex / Camp / Shop */
.grid{display:grid;gap:14px;grid-template-columns:repeat(auto-fill,minmax(190px,1fr))}
.dex-card{padding:14px;cursor:pointer;transition:transform .25s,box-shadow .3s,border-color .3s;text-align:center;position:relative}
.dex-card:hover{transform:translateY(-4px)}
.dex-card .dsp{width:110px;height:110px;margin:0 auto}
.dex-card .dsp svg{width:100%;height:100%}
.dex-card.unseen .dsp{filter:brightness(0) saturate(0) opacity(.5)}
.dex-card .dn{font-weight:800;margin-top:6px}
.dex-card .dno{font-size:11px;color:var(--mute);font-weight:700}
.dex-card .dtag{display:inline-block;margin-top:6px;font-size:10px;padding:2px 8px;border-radius:7px;border:1px solid var(--c);color:var(--c);font-weight:800;text-transform:uppercase}
.dex-layout{display:grid;grid-template-columns:minmax(0,1fr) 340px;gap:16px;min-height:0;flex:1;padding:0 22px 22px}
.dex-layout .content{padding:0;overflow:auto}
#dex-detail{padding:20px;overflow:auto;display:flex;flex-direction:column;gap:12px}
.stat-row{display:grid;grid-template-columns:54px 1fr 36px;gap:8px;align-items:center;font-size:12px;font-weight:700}
.stat-bar{height:8px;border-radius:99px;background:rgba(0,0,0,.4);overflow:hidden}
.stat-bar i{display:block;height:100%;border-radius:99px;background:linear-gradient(90deg,var(--c),#fff);transition:width .8s}
.move-row{display:flex;justify-content:space-between;gap:8px;padding:9px 12px;border-radius:12px;background:rgba(0,0,0,.25);border-left:3px solid var(--c);font-size:13px}
.camp-slot{padding:14px;min-height:190px;cursor:pointer;display:flex;flex-direction:column;gap:6px;align-items:center;text-align:center;transition:all .25s}
.camp-slot.sel{box-shadow:0 0 0 2px #fff,0 0 26px -2px #fff}
.camp-slot .csp{width:110px;height:110px}
.camp-slot .csp svg{width:100%;height:100%}
.camp-slot:hover{transform:translateY(-3px)}
.shop-row{display:flex;align-items:center;gap:14px;padding:14px 16px;margin-bottom:12px}
.shop-row .ico{width:56px;height:56px;border-radius:14px;display:grid;place-items:center;background:rgba(0,0,0,.35);border:1px solid var(--line);flex:none}
.shop-row .ico svg{width:34px;height:34px}
.shop-row .sb{flex:1;min-width:0}
.shop-row .sn{font-weight:800}
.shop-row .sd{font-size:12px;color:var(--mute);margin-top:2px}
.opt-row{display:flex;justify-content:space-between;align-items:center;gap:14px;padding:16px 18px;margin-bottom:12px}

/* Overlay / toast */
#overlay{position:fixed;inset:0;z-index:50;display:grid;place-items:center;background:rgba(5,3,15,.66);backdrop-filter:blur(6px);opacity:0;pointer-events:none;transition:opacity .35s}
#overlay.show{opacity:1;pointer-events:auto}
#overlay .panel{width:min(520px,92vw);padding:28px;text-align:center;display:flex;flex-direction:column;gap:14px;align-items:center;transform:scale(.94);transition:transform .35s cubic-bezier(.2,.8,.2,1)}
#overlay.show .panel{transform:none}
#overlay .otitle{font-size:16px;line-height:1.6}
#overlay .otext{color:var(--mute);line-height:1.6;font-size:15px}
#overlay .obtns{display:flex;gap:10px;flex-wrap:wrap;justify-content:center}
#toast{position:fixed;left:50%;top:22px;transform:translate(-50%,-160px);opacity:0;visibility:hidden;z-index:60;padding:12px 20px;border-radius:14px;font-weight:700;transition:transform .45s cubic-bezier(.2,.8,.2,1),opacity .3s,visibility .3s;pointer-events:none;max-width:90vw;text-align:center}
#toast.show{transform:translate(-50%,0);opacity:1;visibility:visible}
.hl{outline:2px solid rgba(255,255,255,.35);outline-offset:2px}
.pulse-soft{animation:pulseSoft 2.4s ease-in-out infinite}
@keyframes pulseSoft{0%,100%{opacity:.7}50%{opacity:1}}

@media (max-width:860px){
  .world-body{grid-template-columns:1fr;grid-template-rows:minmax(320px,1fr) auto}
  #battle-bottom{grid-template-columns:1fr;height:340px}
  .bcard{width:46%}
  .btn.big{min-width:220px}
  .dex-layout{grid-template-columns:1fr}
  #enemy-sprite{width:34%;top:20%}
  #player-sprite{width:38%;bottom:20%}
}
@media (max-width:560px){
  .title{font-size:26px}
  .bcard{width:50%;padding:10px}
  .bcard .nm{font-size:13px}
}
</style>
</head>
<body>
<div class="bg-orbs"><i></i><i></i><i></i></div>
<div id="app">

  <!-- ============ MAIN MENU ============ -->
  <section id="s-menu" class="screen active">
    <div class="center-col">
      <div class="logo-wrap"><div class="logo-glyph"></div></div>
      <p class="subtitle">The Resonance Conductor Chronicles</p>
      <h1 class="title font-retro">AETHELGARD</h1>
      <p class="muted" style="margin:0;text-align:center;max-width:520px;line-height:1.6">Tune the frequencies. Capture the spirits. Restore the harmony of a world vibrating with elemental energy.</p>
      <div class="menu-stack">
        <button class="btn big primary" id="btn-continue">Continue Journey</button>
        <button class="btn big" id="btn-new">New Journey</button>
        <button class="btn big" id="btn-dex-menu">Conductors Dex</button>
        <button class="btn big danger" id="btn-reset">Reset Progress</button>
      </div>
      <div id="menu-info" class="small muted" style="margin-top:8px;text-align:center"></div>
    </div>
  </section>

  <!-- ============ OVERWORLD ============ -->
  <section id="s-world" class="screen">
    <div class="glass hud">
      <div>
        <div class="font-retro" id="w-zone-name" style="font-size:12px;line-height:1.6">THE VERDANT CANOPY</div>
        <div class="muted small" id="w-zone-sub" style="margin-top:4px"></div>
      </div>
      <div class="hud-right">
        <span class="pill">✦ <b id="w-shards">0</b> Shards</span>
        <span class="pill">Party <b id="w-party-n">1</b>/3</span>
        <button class="btn small-btn" id="w-menu">Menu</button>
      </div>
    </div>
    <div class="zone-tabs" id="zone-tabs"></div>
    <div class="world-body">
      <div class="glass map" id="map">
        <svg id="trail" viewBox="0 0 100 100" preserveAspectRatio="none"></svg>
        <div class="zdeco" id="zdeco"></div>
        <div id="nodes"></div>
      </div>
      <div class="glass side">
        <h3>Party</h3>
        <div id="w-party"></div>
        <h3>Progress</h3>
        <div id="w-progress" class="small muted" style="line-height:1.6"></div>
        <h3>Pack</h3>
        <div id="w-pack" class="small" style="line-height:1.8"></div>
      </div>
    </div>
    <div class="bottom-nav">
      <button class="btn" id="w-camp">Camp &amp; Shop</button>
      <button class="btn" id="w-dex">Conductors Dex</button>
      <button class="btn" id="w-menu2">Main Menu</button>
    </div>
  </section>

  <!-- ============ BATTLE ============ -->
  <section id="s-battle" class="screen">
    <div class="top-bar" style="padding:10px 16px 0">
      <div>
        <div class="font-retro" id="b-title" style="font-size:11px;line-height:1.6">WILD ENCOUNTER</div>
        <div class="small muted" id="b-zone" style="margin-top:3px"></div>
      </div>
      <div class="hud-right"><span class="pill">✦ <b id="b-shards">0</b></span><span class="pill" id="b-prisms">Prisms: 0</span></div>
    </div>
    <div id="arena">
      <div class="status-fx" id="fx-layer"></div>
      <div class="crit-flash" id="crit-flash"></div>
      <div class="glass bcard" id="enemy-card"></div>
      <div class="glass bcard" id="player-card"></div>
      <div class="sprite-wrap" id="enemy-sprite"><div class="shadow"></div><div id="e-svg" style="width:100%;height:100%"></div></div>
      <div class="sprite-wrap" id="player-sprite"><div class="shadow"></div><div id="p-svg" style="width:100%;height:100%"></div></div>
    </div>
    <div id="battle-bottom">
      <div class="glass log" id="battle-log"></div>
      <div class="glass" id="menu-box">
        <div id="menu-main"></div>
        <div id="menu-sub" class="hidden-x"></div>
      </div>
    </div>
  </section>

  <!-- ============ CONDUCTORS DEX ============ -->
  <section id="s-dex" class="screen">
    <div class="top-bar">
      <div>
        <h2 class="font-retro" style="font-size:16px;margin:0;line-height:1.5">CONDUCTORS DEX</h2>
        <div class="small muted" id="dex-count" style="margin-top:6px"></div>
      </div>
      <button class="btn" id="dex-back">← Back</button>
    </div>
    <div class="dex-layout">
      <div class="content"><div class="grid" id="dex-grid"></div></div>
      <div class="glass" id="dex-detail"></div>
    </div>
  </section>

  <!-- ============ CAMP ============ -->
  <section id="s-camp" class="screen">
    <div class="top-bar">
      <div>
        <h2 class="font-retro" style="font-size:16px;margin:0;line-height:1.5">CAMP &amp; SUPPLY</h2>
        <div class="small muted" id="camp-sub" style="margin-top:6px"></div>
      </div>
      <div class="hud-right">
        <span class="pill">✦ <b id="camp-shards">0</b> Shards</span>
        <button class="btn" id="camp-back">← Back</button>
      </div>
    </div>
    <div style="padding:0 22px;display:flex;gap:10px">
      <button class="btn small-btn camp-tab" data-tab="party">Party &amp; Box</button>
      <button class="btn small-btn camp-tab" data-tab="shop">Aether Shop</button>
      <button class="btn small-btn camp-tab" data-tab="pack">Pack</button>
    </div>
    <div class="content" id="camp-content" style="margin-top:14px"></div>
  </section>

</div>

<div id="overlay"><div class="glass panel" id="o-panel">
  <div class="otitle font-retro" id="o-title"></div>
  <div class="otext" id="o-text"></div>
  <div class="obtns" id="o-btns"></div>
</div></div>

<div id="toast" class="glass"></div>
<button id="mute-btn" class="btn small-btn" title="Toggle sound" style="position:fixed;right:14px;bottom:14px;z-index:70">♪ On</button>

<script>
"use strict";
/* =====================================================================
   AETHELGARD — Resonance Conductor Chronicles
   ===================================================================== */

/* ---------- 1. ELEMENTS ---------- */
const TYPES = {
  pyro:   { name:'Pyro',   strong:'flora',  weak:'hydro'  },
  hydro:  { name:'Hydro',  strong:'pyro',   weak:'flora'  },
  flora:  { name:'Flora',  strong:'hydro',  weak:'pyro'   },
  zephyr: { name:'Zephyr', strong:'earth',  weak:'volt'   },
  earth:  { name:'Earth',  strong:'volt',   weak:'zephyr' },
  volt:   { name:'Volt',   strong:'zephyr', weak:'earth'  },
  cosmos: { name:'Cosmos', strong:null,     weak:null     }
};
const PAL = {
  pyro:   { c:'#ff5a2a', l:'#ffc38a', d:'#6b1a08', glow:'#ff5a2a', name:'Pyro'   },
  hydro:  { c:'#2aa8ff', l:'#b5ecff', d:'#0a2f63', glow:'#2aa8ff', name:'Hydro'  },
  flora:  { c:'#34c46a', l:'#d8ffb0', d:'#0d4a2a', glow:'#4ade80', name:'Flora'  },
  zephyr: { c:'#5ed8f0', l:'#ecfeff', d:'#124a6a', glow:'#67e8f9', name:'Zephyr' },
  earth:  { c:'#c08a4a', l:'#f3dcb0', d:'#4a2a12', glow:'#d6a25c', name:'Earth'  },
  volt:   { c:'#f5c518', l:'#fff9c4', d:'#5c4300', glow:'#facc15', name:'Volt'   },
  cosmos: { c:'#9b7bff', l:'#e0d4ff', d:'#2a145c', glow:'#a78bfa', name:'Cosmos' }
};
function effectiveness(atkType, defType){
  const t = TYPES[atkType];
  if (!t || !t.strong) return 1;
  if (t.strong === defType) return 2;
  if (t.weak === defType) return 0.5;
  return 1;
}

/* ---------- 2. MOVES ---------- */
function mv(n, p, a, t, u, s, c){ return { n, p, a, t, u, s:s||null, c:c||0 }; }

/* ---------- 3. RESONANT DATABASE (18 species, 6 families x 3 stages) ---------- */
const SP = {
  /* PYRO */
  emberkit: { id:'emberkit', no:1, name:'Emberkit', type:'pyro', stage:1, evo:8, next:'pyrowolf', shape:'beast',
    base:{hp:44,atk:52,def:40,spd:56},
    desc:'A restless kit whose tail embers flare whenever it senses a rival. Its warmth keeps the Canopy dens alive.',
    moves:[ mv('Cinder Nip',35,100,'pyro',1), mv('Tackle Burst',40,100,'cosmos',1), mv('Flame Snap',50,95,'pyro',3,'burn',0.15),
            mv('Scorch Pounce',60,90,'pyro',5), mv('Ember Lash',65,90,'pyro',7,'burn',0.1), mv('Ember Rush',75,85,'pyro',9) ] },
  pyrowolf: { id:'pyrowolf', no:2, name:'Pyrowolf', type:'pyro', stage:2, evo:15, next:'ignisaur', shape:'beast',
    base:{hp:62,atk:74,def:58,spd:74},
    desc:'Its mane burns with a steady flame. Pyrowolves hunt in pairs across the ash-dusted ridges.',
    moves:[ mv('Blaze Fang',55,95,'pyro',1,'burn',0.15), mv('Ash Gale',45,100,'cosmos',1), mv('Cinder Crash',65,90,'pyro',3),
            mv('Magma Howl',70,90,'pyro',8,'burn',0.2), mv('Solar Mane',80,90,'pyro',10), mv('Inferno Fang',95,80,'pyro',13,'burn',0.25) ] },
  ignisaur: { id:'ignisaur', no:3, name:'Ignisaur', type:'pyro', stage:3, evo:null, next:null, shape:'drake',
    base:{hp:84,atk:100,def:76,spd:94},
    desc:'A winged drake of pure combustion. Its roar can melt the frost from a mountain in a single breath.',
    moves:[ mv('Pyre Talon',60,95,'pyro',1,'burn',0.2), mv('Void Roar',70,95,'cosmos',1), mv('Ember Storm',85,90,'pyro',4,'burn',0.2),
            mv('Dragonfire Wing',90,88,'pyro',8), mv('Sunder Blaze',105,80,'pyro',12,'burn',0.25), mv('Eclipse Flare',115,75,'cosmos',16) ] },

  /* HYDRO */
  droplet: { id:'droplet', no:4, name:'Droplet', type:'hydro', stage:1, evo:8, next:'geoxus', shape:'blob',
    base:{hp:50,atk:44,def:52,spd:46},
    desc:'A shimmering bead of tidal resonance. It bounces across still ponds and sings to the moon.',
    moves:[ mv('Splash Jab',35,100,'hydro',1), mv('Bubble Slam',40,100,'cosmos',1), mv('Tidal Bash',50,95,'hydro',3,'chill',0.15),
            mv('Brine Jet',60,90,'hydro',5), mv('Undertow',70,90,'hydro',8,'chill',0.2), mv('Maelstrom Crash',85,85,'hydro',10) ] },
  geoxus: { id:'geoxus', no:5, name:'Geoxus', type:'hydro', stage:2, evo:15, next:'neptunian', shape:'serpent',
    base:{hp:68,atk:62,def:74,spd:58},
    desc:'A sea-serpent born from silt and rain. Its scales ring like struck bells when the tide turns.',
    moves:[ mv('Torrent Slam',55,95,'hydro',1), mv('Shell Shock',45,100,'cosmos',1), mv('Seafloor Quake',70,90,'earth',3),
            mv('Riptide Lash',70,90,'hydro',5,'chill',0.15), mv('Glacier Fang',75,90,'hydro',7,'chill',0.2), mv('Abyssal Crush',100,75,'cosmos',14) ] },
  neptunian: { id:'neptunian', no:6, name:'Neptunian', type:'hydro', stage:3, evo:null, next:null, shape:'drake',
    base:{hp:90,atk:84,def:98,spd:76},
    desc:'A tidal wyrm whose fins chart the ocean currents. Old sailors say it sings storms into being.',
    moves:[ mv('Coral Thrash',65,95,'hydro',1), mv('Tsunami Crash',90,85,'hydro',1), mv('Pearl Surge',75,90,'hydro',4,'chill',0.15),
            mv('Leviathan Coil',85,90,'cosmos',8), mv('Whirl Cascade',70,95,'zephyr',10), mv("Neptune's Wrath",110,75,'hydro',15,'chill',0.2) ] },

  /* FLORA */
  seedling: { id:'seedling', no:7, name:'Seedling', type:'flora', stage:1, evo:8, next:'thornback', shape:'blob',
    base:{hp:48,atk:46,def:50,spd:42},
    desc:'A sprout that stores sunlight in its crown. It hums softly when the wind passes through its leaves.',
    moves:[ mv('Vine Whip',40,100,'flora',1), mv('Pollen Pop',35,100,'cosmos',1), mv('Leaf Dart',50,95,'flora',3,'root',0.15),
            mv('Bramble Slap',60,90,'flora',5), mv('Spore Cloud',45,100,'flora',7,'root',0.25), mv('Verdant Bloom',70,90,'flora',9) ] },
  thornback: { id:'thornback', no:8, name:'Thornback', type:'flora', stage:2, evo:15, next:'bloomgoliath', shape:'beast',
    base:{hp:66,atk:68,def:72,spd:50},
    desc:'A bristling grove-beast whose spines hold dew that cures almost anything. Hunters respect it deeply.',
    moves:[ mv('Thorn Lash',55,95,'flora',1,'root',0.15), mv('Bark Bash',50,95,'earth',1), mv('Spore Cloud',45,100,'flora',5,'root',0.25),
            mv('Briar Surge',70,90,'flora',8), mv('Canopy Fury',85,85,'flora',11), mv('Pollen Storm',75,95,'flora',9,'root',0.15) ] },
  bloomgoliath: { id:'bloomgoliath', no:9, name:'Bloomgoliath', type:'flora', stage:3, evo:null, next:null, shape:'golem',
    base:{hp:88,atk:92,def:96,spd:66},
    desc:'A moss-clad colossus that walks the Canopy floor. Flowers the size of carts bloom wherever it rests.',
    moves:[ mv('Petal Storm',70,95,'flora',1), mv('Overgrow Slam',80,90,'earth',1), mv('Rootbind',60,100,'flora',3,'root',0.25),
            mv('Starseed Burst',100,80,'cosmos',12), mv('Gaia Crush',105,80,'earth',15), mv('Pollen Storm',75,95,'flora',8,'root',0.15) ] },

  /* VOLT */
  sparky: { id:'sparky', no:10, name:'Sparky', type:'volt', stage:1, evo:8, next:'boltsaber', shape:'beast',
    base:{hp:40,atk:50,def:38,spd:70},
    desc:'A jittery static-fox that leaves tiny lightning footprints. It loves metal and anything that glows.',
    moves:[ mv('Static Pinch',40,100,'volt',1), mv('Quick Zap',35,100,'cosmos',1), mv('Spark Fang',50,95,'volt',3,'shock',0.15),
            mv('Volt Dash',60,90,'volt',5), mv('Arc Bite',65,90,'volt',8,'shock',0.2), mv('Thunder Lunge',75,85,'volt',10) ] },
  boltsaber: { id:'boltsaber', no:11, name:'Boltsaber', type:'volt', stage:2, evo:15, next:'stormlord', shape:'drake',
    base:{hp:58,atk:78,def:56,spd:96},
    desc:'A lean, blade-tailed predator that strikes faster than thought. Its tail crackles before every lunge.',
    moves:[ mv('Lightning Slash',65,95,'volt',1), mv('Dash Strike',55,100,'zephyr',1), mv('Plasma Twin',70,90,'volt',4,'shock',0.15),
            mv('Circuit Ram',80,90,'cosmos',7), mv('Ion Fang',85,85,'volt',10,'shock',0.2), mv('Storm Barrage',95,80,'volt',14) ] },
  stormlord: { id:'stormlord', no:12, name:'Stormlord', type:'volt', stage:3, evo:null, next:null, shape:'serpent',
    base:{hp:80,atk:102,def:78,spd:122},
    desc:'A thundering serpent of cloud and copper. Where it coils, the sky itself splits open with light.',
    moves:[ mv('Thunderwrath',100,80,'volt',1), mv('Gale Lance',80,90,'zephyr',1), mv('Static Crown',90,90,'volt',4,'shock',0.25),
            mv('Cyclone Volt',90,85,'zephyr',8), mv('Galvanic Crush',110,75,'volt',12), mv('Eclipse Surge',100,80,'cosmos',16) ] },

  /* ZEPHYR */
  gusty: { id:'gusty', no:13, name:'Gusty', type:'zephyr', stage:1, evo:8, next:'slipstream', shape:'blob',
    base:{hp:42,atk:48,def:40,spd:66},
    desc:'A puff of playful wind with eyes. It tumbles through the Canopy, collecting seeds and mischief.',
    moves:[ mv('Breeze Slash',40,100,'zephyr',1), mv('Gust Puff',35,100,'cosmos',1), mv('Wind Scrape',50,95,'zephyr',3),
            mv('Cyclone Tap',60,90,'zephyr',5), mv('Sky Shear',70,90,'zephyr',8), mv('Tailwind Rush',80,85,'zephyr',10) ] },
  slipstream: { id:'slipstream', no:14, name:'Slipstream', type:'zephyr', stage:2, evo:15, next:'zephyrus', shape:'beast',
    base:{hp:60,atk:72,def:58,spd:90},
    desc:'A swift glider-fox that rides invisible air currents. It is said to carry messages between mountain peaks.',
    moves:[ mv('Vortex Rush',65,95,'zephyr',1), mv('Aerial Dive',60,100,'cosmos',1), mv('Gust Slam',75,90,'zephyr',4),
            mv('Earth Skim',70,90,'earth',6), mv('Tornado Lance',85,85,'zephyr',9), mv('Hurricane Edge',95,80,'zephyr',14) ] },
  zephyrus: { id:'zephyrus', no:15, name:'Zephyrus', type:'zephyr', stage:3, evo:null, next:null, shape:'drake',
    base:{hp:82,atk:96,def:80,spd:116},
    desc:'The wind-sovereign of the Canopy. Its wings unfurl into a hurricane, and every leaf bows in its wake.',
    moves:[ mv('Storm Cyclone',90,85,'zephyr',1), mv('Sky Coil',65,100,'zephyr',1), mv('Thermal Blade',75,90,'zephyr',4),
            mv('Void Gust',90,90,'cosmos',7), mv('Sonic Boom',80,90,'volt',10), mv('Apex Tempest',110,75,'zephyr',14) ] },

  /* EARTH */
  pebble: { id:'pebble', no:16, name:'Pebble', type:'earth', stage:1, evo:8, next:'obsidian', shape:'golem',
    base:{hp:56,atk:46,def:64,spd:30},
    desc:'A stubborn little boulder-spirit that rolls to sleep in riverbeds. Patient, unshakable, and surprisingly strong.',
    moves:[ mv('Rock Toss',40,100,'earth',1), mv('Dust Tap',35,100,'cosmos',1), mv('Stone Crush',50,95,'earth',3),
            mv('Quake Stamp',60,90,'earth',5), mv('Gravel Bite',65,90,'earth',8), mv('Tremor Bash',75,85,'earth',10) ] },
  obsidian: { id:'obsidian', no:17, name:'Obsidian', type:'earth', stage:2, evo:15, next:'terrasunder', shape:'shard',
    base:{hp:80,atk:66,def:92,spd:40},
    desc:'A jagged volcanic glass-spirit. Its edges are sharp enough to cut through steel and sorrow alike.',
    moves:[ mv('Obsidian Shard',65,95,'earth',1), mv('Heavy Plate',55,100,'cosmos',1), mv('Shatter Fall',70,90,'earth',4),
            mv('Magma Slam',80,90,'pyro',7,'burn',0.15), mv('Glass Storm',85,85,'earth',10), mv('Fault Break',95,80,'earth',14) ] },
  terrasunder: { id:'terrasunder', no:18, name:'Terrasunder', type:'earth', stage:3, evo:null, next:null, shape:'golem',
    base:{hp:104,atk:88,def:118,spd:52},
    desc:'A mountain that remembers when it was a continent. When it stands, the ground forgets how to be still.',
    moves:[ mv('Terra Break',100,85,'earth',1), mv('Tectonic Crown',80,95,'earth',1), mv('Ridge Fall',90,90,'earth',4),
            mv('Sundering Tremor',110,80,'cosmos',9), mv('Mountain Wrath',115,75,'earth',12), mv('Continent Smite',120,70,'earth',17) ] }
};
const STARTERS = ['emberkit','droplet','seedling'];
const SPECIES_IDS = Object.keys(SP);

/* ---------- 4. ITEMS ---------- */
const ITEMS = {
  harmonizer: { name:'Harmonizer Prism', desc:'Basic capture prism. Catch power x1.', price:40, tier:1, kind:'prism', icon:'prism' },
  hyper:      { name:'Hyper-Conductive Prism', desc:'High-tuning prism. Catch power x1.8.', price:140, tier:1.8, kind:'prism', icon:'prism2' },
  core:       { name:'Core Restorer', desc:'Restores 50% of a Resonant\'s max HP.', price:60, kind:'heal', icon:'core' },
  elixir:     { name:'Resonance Elixir', desc:'Heals every status condition.', price:50, kind:'cure', icon:'elixir' },
  vitals:     { name:'Vitals Elixir', desc:'Restores 40 HP. Found in caches.', price:0, kind:'heal40', icon:'vitals' },
  cleanser:   { name:'Status Cleanser', desc:'Cures a status condition. Found in caches.', price:0, kind:'cure', icon:'cleanser' },
  booster:    { name:'Prism Booster', desc:'In battle: next prism throw gains +0.6 tier.', price:0, kind:'booster', icon:'booster' }
};
const SHOP_ITEMS = ['harmonizer','hyper','core','elixir'];
const ITEM_ORDER = ['harmonizer','hyper','core','elixir','vitals','cleanser','booster'];

/* ---------- 5. TRAINERS & BOSSES ---------- */
const TRAINERS = {
  c_flora1:  { name:'Conductor Lyra', title:'Herb Tuner', col:'#4ade80', shards:80,
    intro:'Lyra: "Your Resonants look skittish. Let me show you what real tuning sounds like!"',
    defeat:'Lyra: "Hm. Your harmonics are cleaner than I expected. Take this."',
    team:[{sp:'seedling',l:4},{sp:'gusty',l:5}] },
  c_zephyr1: { name:'Scout Teren', title:'Wind Runner', col:'#67e8f9', shards:90,
    intro:'Teren: "Nobody passes the ridge without a duel. Ready?"',
    defeat:'Teren: "Fast enough, I see. The ridge is yours. Stay sharp."',
    team:[{sp:'gusty',l:6},{sp:'sparky',l:6},{sp:'emberkit',l:6}] },
  c_hydro1:  { name:'Diver Oru', title:'Tide Listener', col:'#2aa8ff', shards:120,
    intro:'Oru: "The grotto hears everything. It hears your footsteps, and it hears your doubts."',
    defeat:'Oru: "The tide turns. You have earned the grotto\'s quiet."',
    team:[{sp:'droplet',l:7},{sp:'geoxus',l:8},{sp:'pebble',l:7}] },
  c_earth1:  { name:'Miner Dagna', title:'Stone Caller', col:'#d6a25c', shards:130,
    intro:'Dagna: "Rocks don\'t lie, kid. Let\'s see whether your spirits can hold a line."',
    defeat:'Dagna: "Solid. Like I said, rocks don\'t lie."',
    team:[{sp:'pebble',l:8},{sp:'obsidian',l:8},{sp:'thornback',l:8}] },
  c_pyro1:   { name:'Smelter Rhyx', title:'Ember Forger', col:'#ff5a2a', shards:170,
    intro:'Rhyx: "The Crags forge the strong. Let\'s see what your Resonants are made of!"',
    defeat:'Rhyx: "Hot-blooded and cool-headed. That is rare. Keep the fire honest."',
    team:[{sp:'pyrowolf',l:12},{sp:'emberkit',l:11},{sp:'sparky',l:12}] },
  c_volt1:   { name:'Stormcaller Vesh', title:'Arc Virtuoso', col:'#facc15', shards:180,
    intro:'Vesh: "Thunder answers only to those who can keep up. Can you?"',
    defeat:'Vesh: "Current flows through you, Conductor. Impressive."',
    team:[{sp:'boltsaber',l:12},{sp:'sparky',l:12},{sp:'gusty',l:13}] },
  c_apex1:   { name:'Sentinel Oma', title:'Pillar Guard', col:'#c084fc', shards:260,
    intro:'Oma: "The pillar tests the worthy. Few have climbed this far."',
    defeat:'Oma: "Go on. The Apex waits, and she is not gentle."',
    team:[{sp:'bloomgoliath',l:16},{sp:'slipstream',l:16},{sp:'ignisaur',l:16}] },
  c_apex2:   { name:'Resonant Hollis', title:'Sky Anchor', col:'#67e8f9', shards:280,
    intro:'Hollis: "The air above is thin and cold. Let\'s see if you can breathe it."',
    defeat:'Hollis: "You breathe better than I did at your age."',
    team:[{sp:'neptunian',l:17},{sp:'stormlord',l:17},{sp:'obsidian',l:17}] },
  c_apex3:   { name:'Keeper Vael', title:'Last Warden', col:'#f472b6', shards:300,
    intro:'Vael: "Beyond me lies only the Apex. Prove yourself or turn back now."',
    defeat:'Vael: "Then the Apex is yours to face. Walk well."',
    team:[{sp:'terrasunder',l:18},{sp:'zephyrus',l:18},{sp:'ignisaur',l:17}] },
  /* ---- Gatekeeper bosses ---- */
  warden: { name:'Warden Verdalis', title:'Gatekeeper of the Canopy', col:'#4ade80', shards:400, boss:true,
    intro:'Verdalis: "I have guarded this canopy since the first seed fell. Show me your resolve, young Conductor!"',
    low:'Verdalis: "Hm... your harmony grows louder. I will not yield easily!"',
    defeat:'Verdalis: "The canopy opens to you. Go, and remember: every living thing hums a note."',
    team:[{sp:'thornback',l:5},{sp:'slipstream',l:6},{sp:'pyrowolf',l:6}] },
  maren:  { name:'Tide-Sovereign Maren', title:'Keeper of the Sunken Grotto', col:'#2aa8ff', shards:650, boss:true,
    intro:'Maren: "Beneath the waves, pressure teaches patience. Let us see if you have learned it."',
    low:'Maren: "The tide surges through me still! Do not mistake my calm for weakness!"',
    defeat:'Maren: "The grotto sings for you now. Carry its song to the Crags, Conductor."',
    team:[{sp:'geoxus',l:9},{sp:'droplet',l:9},{sp:'pebble',l:10}] },
  kaelos: { name:'Forgemaster Kaelos', title:'Lord of the Obsidian Crags', col:'#ff5a2a', shards:900, boss:true,
    intro:'Kaelos: "Every blade I forge is tempered by fire and storm. Test your edge against mine!"',
    low:'Kaelos: "Ha! My forge still burns hot. Strike harder, or be cooled!"',
    defeat:'Kaelos: "You have quenched my forge. The Apex Pillar awaits. Do not let it cool you."',
    team:[{sp:'pyrowolf',l:13},{sp:'boltsaber',l:13},{sp:'obsidian',l:14}] },
  ysolde: { name:'Apex Conductor Ysolde', title:'Voice of the Apex Pillar', col:'#a78bfa', shards:2000, boss:true, final:true,
    intro:'Ysolde: "I am the last note of Aethelgard. Every Resonant in this world answers to my song. Will yours answer to yours?"',
    low:'Ysolde: "Your harmony is... remarkable. But the Apex does not bow. Not yet!"',
    defeat:'Ysolde: "The song is complete... and so is yours. Aethelgard is in tune once more."',
    team:[{sp:'neptunian',l:18},{sp:'zephyrus',l:18},{sp:'terrasunder',l:19},{sp:'stormlord',l:20}] }
};

/* ---------- 6. ZONES ---------- */
const ZONES = [
  { id:'z1', name:'The Verdant Canopy', sub:'Levels 1–5 · Flora & Zephyr', col:'#4ade80', glow:'rgba(74,222,128,.22)', min:1, max:5,
    pool:['seedling','seedling','gusty','gusty','emberkit','sparky'], gate:'warden', gateLv:5,
    nodes:[
      {t:'wild',x:10,y:70,l:'Tall Grass'},
      {t:'cache',x:22,y:38,l:'Mossy Cache'},
      {t:'wild',x:34,y:62,l:'Fern Hollow'},
      {t:'conductor',x:46,y:30,tr:'c_flora1',l:'Lyra'},
      {t:'spring',x:58,y:60,l:'Verdant Spring'},
      {t:'wild',x:70,y:34,l:'Canopy Rise'},
      {t:'conductor',x:80,y:64,tr:'c_zephyr1',l:'Teren'},
      {t:'gate',x:90,y:30,tr:'warden',l:'Warden Verdalis'}
    ] },
  { id:'z2', name:'The Sunken Grotto', sub:'Levels 5–10 · Hydro & Earth', col:'#2aa8ff', glow:'rgba(42,168,255,.22)', min:5, max:10,
    pool:['droplet','droplet','pebble','pebble','geoxus','emberkit','gusty'], gate:'maren', gateLv:10,
    nodes:[
      {t:'wild',x:10,y:40,l:'Brine Pool'},
      {t:'cache',x:24,y:66,l:'Sunken Chest'},
      {t:'wild',x:36,y:34,l:'Coral Maze'},
      {t:'conductor',x:50,y:62,tr:'c_hydro1',l:'Oru'},
      {t:'spring',x:62,y:32,l:'Glowing Spring'},
      {t:'wild',x:72,y:60,l:'Kelp Forest'},
      {t:'conductor',x:82,y:34,tr:'c_earth1',l:'Dagna'},
      {t:'gate',x:90,y:66,tr:'maren',l:'Maren'}
    ] },
  { id:'z3', name:'The Obsidian Crags', sub:'Levels 10–15 · Pyro & Volt', col:'#ff5a2a', glow:'rgba(255,90,42,.22)', min:10, max:15,
    pool:['emberkit','pyrowolf','pyrowolf','sparky','boltsaber','obsidian','obsidian','pebble'], gate:'kaelos', gateLv:15,
    nodes:[
      {t:'wild',x:12,y:64,l:'Ash Fields'},
      {t:'cache',x:24,y:30,l:'Forge Cache'},
      {t:'wild',x:36,y:56,l:'Lava Shelf'},
      {t:'conductor',x:50,y:34,tr:'c_pyro1',l:'Rhyx'},
      {t:'spring',x:62,y:62,l:'Cooling Basin'},
      {t:'wild',x:74,y:36,l:'Thunder Ridge'},
      {t:'conductor',x:80,y:66,tr:'c_volt1',l:'Vesh'},
      {t:'gate',x:92,y:38,tr:'kaelos',l:'Forgemaster Kaelos'}
    ] },
  { id:'z4', name:'The Apex Pillar', sub:'Level 15+ · Final Gauntlet', col:'#a78bfa', glow:'rgba(167,139,250,.24)', min:15, max:20,
    pool:['ignisaur','neptunian','bloomgoliath','stormlord','terrasunder','zephyrus','ignisaur','stormlord'], gate:'ysolde', gateLv:20,
    nodes:[
      {t:'wild',x:10,y:60,l:'Cloud Stair'},
      {t:'conductor',x:24,y:34,tr:'c_apex1',l:'Oma'},
      {t:'wild',x:38,y:60,l:'Sky Bridge'},
      {t:'spring',x:50,y:32,l:'Pillar Spring'},
      {t:'conductor',x:62,y:62,tr:'c_apex2',l:'Hollis'},
      {t:'wild',x:74,y:34,l:'Storm Ledge'},
      {t:'conductor',x:82,y:62,tr:'c_apex3',l:'Vael'},
      {t:'gate',x:92,y:32,tr:'ysolde',l:'Apex Ysolde'}
    ] }
];
const ZONE_BY_ID = {}; ZONES.forEach((z,i)=>{ z.index=i; ZONE_BY_ID[z.id]=z; });

/* ---------- 7. UTILITIES ---------- */
const $ = id => document.getElementById(id);
const rand = (a,b) => a + Math.random()*(b-a);
const randInt = (a,b) => Math.floor(rand(a,b+1));
const pick = arr => arr[Math.floor(Math.random()*arr.length)];
const clamp = (v,a,b) => Math.max(a, Math.min(b, v));
const sleep = ms => new Promise(r => setTimeout(r, ms));
const esc = s => String(s).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));

/* ---------- 8. WEB AUDIO 8-BIT SYNTH ---------- */
class SynthManager {
  constructor(){ this.ctx = null; this.master = null; this.enabled = true; this.lastHover = 0; }
  init(){
    if (this.ctx) { if (this.ctx.state === 'suspended') this.ctx.resume(); return; }
    const AC = window.AudioContext || window.webkitAudioContext;
    if (!AC) return;
    this.ctx = new AC();
    this.master = this.ctx.createGain();
    this.master.gain.value = 0.22;
    this.master.connect(this.ctx.destination);
  }
  tone(freq, dur, type='square', vol=0.3, when=0, slideTo=null){
    if (!this.enabled || !this.ctx) return;
    const t = this.ctx.currentTime + when;
    const o = this.ctx.createOscillator();
    const g = this.ctx.createGain();
    o.type = type;
    o.frequency.setValueAtTime(freq, t);
    if (slideTo) o.frequency.exponentialRampToValueAtTime(Math.max(20, slideTo), t + dur);
    g.gain.setValueAtTime(0.0001, t);
    g.gain.exponentialRampToValueAtTime(vol, t + 0.01);
    g.gain.exponentialRampToValueAtTime(0.0001, t + dur);
    o.connect(g); g.connect(this.master);
    o.start(t); o.stop(t + dur + 0.05);
  }
  noise(dur, vol=0.35, when=0, cutoff=1800){
    if (!this.enabled || !this.ctx) return;
    const t = this.ctx.currentTime + when;
    const len = Math.floor(this.ctx.sampleRate * dur);
    const buf = this.ctx.createBuffer(1, len, this.ctx.sampleRate);
    const d = buf.getChannelData(0);
    for (let i = 0; i < len; i++) d[i] = (Math.random() * 2 - 1) * (1 - i / len);
    const src = this.ctx.createBufferSource();
    src.buffer = buf;
    const f = this.ctx.createBiquadFilter();
    f.type = 'lowpass'; f.frequency.value = cutoff;
    const g = this.ctx.createGain();
    g.gain.value = vol;
    src.connect(f); f.connect(g); g.connect(this.master);
    src.start(t);
  }
  hover(){
    const now = performance.now();
    if (now - this.lastHover < 70) return;
    this.lastHover = now;
    this.tone(880, 0.04, 'square', 0.07);
  }
  select(){ this.tone(523, 0.07, 'square', 0.16); this.tone(784, 0.09, 'square', 0.16, 0.06); }
  hit(){ this.noise(0.12, 0.3); this.tone(200, 0.14, 'square', 0.22, 0, 70); }
  superHit(){ this.noise(0.16, 0.38); this.tone(660, 0.1, 'sawtooth', 0.16, 0, 330); this.tone(990, 0.12, 'square', 0.14, 0.09); }
  crit(){ this.noise(0.22, 0.45); this.tone(1400, 0.2, 'square', 0.14, 0, 300); }
  levelUp(){ [523, 659, 784, 1047, 1318].forEach((f, i) => this.tone(f, 0.14, 'square', 0.18, i * 0.09)); }
  capture(){ [880, 1046, 1318, 1568, 2093].forEach((f, i) => this.tone(f, 0.12, 'triangle', 0.22, i * 0.05)); }
  prism(){ this.tone(300, 0.3, 'triangle', 0.2, 0, 1200); }
  fail(){ this.tone(420, 0.25, 'sawtooth', 0.16, 0, 110); }
  heal(){ [659, 784, 988].forEach((f, i) => this.tone(f, 0.12, 'triangle', 0.2, i * 0.08)); }
  evolve(){ [392, 523, 659, 784, 1047, 784, 1047].forEach((f, i) => this.tone(f, 0.16, 'triangle', 0.2, i * 0.11)); }
  fanfare(){ [523, 523, 523, 659, 523, 784].forEach((f, i) => this.tone(f, 0.18, 'square', 0.18, i * 0.14)); }
}
const SFX = new SynthManager();

/* ---------- 9. PROCEDURAL SVG ART ---------- */
let SVG_UID = 0;
function eyeSVG(x, y, r){
  return `<ellipse cx="${x}" cy="${y}" rx="${r}" ry="${r*1.15}" fill="#fff"/>` +
         `<ellipse cx="${x+r*0.25}" cy="${y+r*0.12}" rx="${r*0.52}" ry="${r*0.7}" fill="#120a24"/>` +
         `<circle cx="${x+r*0.42}" cy="${y-r*0.3}" r="${r*0.22}" fill="#fff"/>`;
}
const SHAPES = {
  beast:(id,p)=>`
    <path d="M136 124 C172 116 186 84 172 56 C168 46 158 52 160 64 C164 84 156 108 132 116 Z" fill="${p.c}"/>
    <path d="M136 124 C166 112 176 88 170 64" fill="none" stroke="${p.l}" stroke-width="3" opacity=".55"/>
    <g fill="${p.d}"><rect x="72" y="142" width="14" height="30" rx="7"/><rect x="92" y="146" width="14" height="28" rx="7"/><rect x="112" y="146" width="14" height="28" rx="7"/><rect x="132" y="142" width="14" height="30" rx="7"/></g>
    <ellipse cx="104" cy="124" rx="50" ry="32" fill="url(#${id}b)"/>
    <path d="M58 86 Q56 64 48 40 Q72 56 80 74 Z" fill="${p.c}"/>
    <path d="M78 74 Q86 56 98 36 Q100 62 94 80 Z" fill="${p.c}"/>
    <path d="M60 82 Q58 66 54 52 Q70 62 74 78 Z" fill="${p.l}" opacity=".6"/>
    <circle cx="66" cy="96" r="28" fill="url(#${id}b)"/>
    <ellipse cx="44" cy="106" rx="14" ry="10" fill="${p.l}"/>
    <ellipse cx="34" cy="102" rx="4" ry="3" fill="#1b1030"/>
    ${eyeSVG(60, 92, 6.5)}${eyeSVG(78, 90, 6.5)}
    <path d="M40 116 L45 122 L50 116" fill="#fff"/>
    <path d="M90 106 Q104 104 112 110" fill="none" stroke="${p.d}" stroke-width="2"/>`,
  drake:(id,p)=>`
    <path d="M104 104 C86 56 46 34 14 46 C34 60 52 70 62 92 C44 90 34 92 26 100 C50 104 68 112 84 120 Z" fill="url(#${id}w)" stroke="${p.c}" stroke-width="2"/>
    <path d="M150 126 C178 124 192 140 184 158 C178 170 166 160 170 150 C172 142 160 140 148 140 Z" fill="${p.c}"/>
    <path d="M70 124 C86 98 136 96 156 118 C166 132 150 150 124 150 L84 150 C74 148 66 136 70 124 Z" fill="url(#${id}b)"/>
    <rect x="92" y="140" width="16" height="30" rx="8" fill="${p.d}"/>
    <rect x="128" y="140" width="16" height="30" rx="8" fill="${p.d}"/>
    <path d="M80 112 C70 100 64 92 62 86 C56 74 50 68 42 66 L24 80 C36 84 46 96 56 100 C64 104 74 116 84 124 Z" fill="url(#${id}b)"/>
    <polygon points="54,76 50,56 64,72" fill="${p.l}"/>
    <path d="M26 80 L16 84 L26 90 Z" fill="${p.l}"/>
    ${eyeSVG(50, 84, 5.5)}
    <path d="M30 94 L44 96" stroke="#1b1030" stroke-width="2" stroke-linecap="round"/>`,
  blob:(id,p)=>`
    <path d="M100 22 C140 72 162 110 160 138 C158 166 134 180 100 180 C66 180 42 166 40 138 C38 110 60 72 100 22 Z" fill="url(#${id}b)"/>
    <ellipse cx="78" cy="88" rx="12" ry="22" fill="#fff" opacity=".32" transform="rotate(20 78 88)"/>
    ${eyeSVG(84, 124, 9)}${eyeSVG(116, 124, 9)}
    <path d="M90 146 Q100 156 110 146" fill="none" stroke="#1b1030" stroke-width="3" stroke-linecap="round"/>
    <ellipse cx="72" cy="140" rx="5" ry="3" fill="${p.l}" opacity=".5"/><ellipse cx="128" cy="140" rx="5" ry="3" fill="${p.l}" opacity=".5"/>`,
  serpent:(id,p)=>`
    <path d="M150 176 C112 160 150 120 112 104 C76 90 80 62 96 58 C116 52 110 30 76 34" fill="none" stroke="${p.d}" stroke-width="30" stroke-linecap="round"/>
    <path d="M150 176 C112 160 150 120 112 104 C76 90 80 62 96 58 C116 52 110 30 76 34" fill="none" stroke="${p.c}" stroke-width="22" stroke-linecap="round"/>
    <path d="M150 176 C112 160 150 120 112 104 C76 90 80 62 96 58 C116 52 110 30 76 34" fill="none" stroke="${p.l}" stroke-width="5" stroke-linecap="round" opacity=".45" transform="translate(-4 -4)"/>
    <path d="M52 38 L30 46 M30 46 L22 40 M30 46 L22 52" stroke="${p.glow}" stroke-width="2.5" fill="none" stroke-linecap="round"/>
    <circle cx="66" cy="34" r="22" fill="url(#${id}b)"/>
    ${eyeSVG(58, 30, 6)}${eyeSVG(76, 28, 6)}
    <path d="M60 44 L66 50 L72 44" fill="#fff"/>
    <path d="M86 20 Q88 12 96 12" fill="none" stroke="${p.l}" stroke-width="2" opacity=".6"/>`,
  golem:(id,p)=>`
    <rect x="28" y="92" width="30" height="66" rx="15" fill="${p.d}"/>
    <rect x="142" y="92" width="30" height="66" rx="15" fill="${p.d}"/>
    <rect x="52" y="60" width="96" height="112" rx="28" fill="url(#${id}b)"/>
    <polyline points="80,96 92,112 84,128 100,142" fill="none" stroke="${p.l}" stroke-width="2.5" opacity=".7"/>
    <polyline points="124,80 112,100 124,118" fill="none" stroke="${p.l}" stroke-width="2.5" opacity=".6"/>
    <rect x="72" y="28" width="56" height="40" rx="14" fill="url(#${id}b)"/>
    <rect x="84" y="42" width="11" height="9" rx="3" fill="${p.glow}" filter="url(#${id}q)"/>
    <rect x="105" y="42" width="11" height="9" rx="3" fill="${p.glow}" filter="url(#${id}q)"/>
    <circle cx="100" cy="122" r="12" fill="${p.glow}" opacity=".8" filter="url(#${id}q)"/>
    <path d="M78 150 L122 150" stroke="${p.l}" stroke-width="2" opacity=".5"/>`,
  shard:(id,p)=>`
    <polygon points="100,12 152,80 128,182 72,182 48,80" fill="url(#${id}b)"/>
    <polygon points="100,12 152,80 100,80" fill="#fff" opacity=".14"/>
    <polyline points="100,12 100,182" fill="none" stroke="#fff" stroke-width="1.5" opacity=".3"/>
    <polyline points="48,80 152,80" fill="none" stroke="#fff" stroke-width="1.5" opacity=".3"/>
    <polyline points="72,182 100,80 128,182" fill="none" stroke="${p.l}" stroke-width="1.2" opacity=".35"/>
    ${eyeSVG(84, 112, 6)}${eyeSVG(116, 112, 6)}
    <path d="M90 140 Q100 148 110 140" fill="none" stroke="#120a24" stroke-width="2.5" stroke-linecap="round"/>`
};
const DECO = {
  pyro:(id,p,st)=>({
    back:`<path d="M24 192 Q10 150 34 128 Q32 160 48 166 Q48 136 62 116 Q72 160 82 170 Q94 134 102 104 Q114 150 124 170 Q136 138 150 120 Q152 160 168 166 Q174 148 186 136 Q190 178 176 192 Z" fill="${p.c}" opacity=".7"/>
           <path d="M46 192 Q40 170 56 158 Q60 176 74 180 Q90 160 100 144 Q110 170 124 180 Q140 166 152 160 Q150 182 160 192 Z" fill="${p.l}" opacity=".45"/>`,
    front:`<g fill="${p.l}" opacity=".9"><circle cx="30" cy="60" r="2.5"/><circle cx="170" cy="50" r="2"/><circle cx="20" cy="110" r="1.8"/></g>` + (st>=2?`<path d="M14 30 L22 46 L30 30 L38 46 L46 30" fill="none" stroke="${p.l}" stroke-width="2" opacity=".6"/>`:'')
  }),
  hydro:(id,p,st)=>({
    back:`<ellipse cx="100" cy="178" rx="80" ry="12" fill="none" stroke="${p.l}" stroke-width="2" opacity=".5" stroke-dasharray="6 6"/>`,
    front:`<g><circle cx="22" cy="70" r="6" fill="${p.l}" opacity=".9"/><circle cx="20" cy="68" r="2" fill="#fff"/>
            <circle cx="180" cy="90" r="5" fill="${p.l}" opacity=".85"/><circle cx="178" cy="88" r="1.6" fill="#fff"/>
            <circle cx="32" cy="150" r="4" fill="${p.l}" opacity=".7"/></g>` + (st>=2?`<path d="M164 40 Q176 52 166 64" fill="none" stroke="${p.l}" stroke-width="2" opacity=".6"/>`:'')
  }),
  flora:(id,p,st)=>({
    back:`<g fill="${p.c}" opacity=".9"><ellipse cx="30" cy="148" rx="14" ry="7" transform="rotate(-35 30 148)"/><ellipse cx="172" cy="150" rx="14" ry="7" transform="rotate(35 172 150)"/></g>
          <path d="M30 148 L44 136 M172 150 L158 138" stroke="${p.l}" stroke-width="1.5" opacity=".7"/>`,
    front:`<g fill="${p.c}" opacity=".95"><ellipse cx="30" cy="40" rx="8" ry="4" transform="rotate(-40 30 40)"/><ellipse cx="170" cy="60" rx="8" ry="4" transform="rotate(40 170 60)"/></g>` + (st>=2?`<ellipse cx="22" cy="100" rx="7" ry="3.5" transform="rotate(-20 22 100)" fill="${p.l}" opacity=".8"/>`:'')
  }),
  volt:(id,p,st)=>({
    back:`<circle cx="100" cy="104" r="78" fill="none" stroke="${p.c}" stroke-width="1" opacity=".35" stroke-dasharray="3 9"/>`,
    front:`<polyline points="22,64 38,94 28,98 46,130" fill="none" stroke="${p.l}" stroke-width="3" stroke-linejoin="round" filter="url(#${id}q)"/>
           <polyline points="176,42 160,72 172,76 154,106" fill="none" stroke="${p.l}" stroke-width="3" stroke-linejoin="round" filter="url(#${id}q)"/>` + (st>=2?`<polyline points="150,170 168,150 158,146 176,126" fill="none" stroke="${p.c}" stroke-width="2.5" stroke-linejoin="round" filter="url(#${id}q)"/>`:'')
  }),
  zephyr:(id,p,st)=>({
    back:`<path d="M10 70 Q60 20 100 60 T190 56" fill="none" stroke="${p.l}" stroke-width="2.5" opacity=".5"/>
          <path d="M14 136 Q60 98 104 130 T190 124" fill="none" stroke="${p.l}" stroke-width="2" opacity=".4"/>`,
    front:`<g fill="#fff" opacity=".85"><circle cx="26" cy="110" r="2"/><circle cx="176" cy="88" r="2.5"/><circle cx="170" cy="168" r="2"/></g>` + (st>=2?`<path d="M18 176 Q48 160 60 182" fill="none" stroke="${p.l}" stroke-width="2" opacity=".6"/>`:'')
  }),
  earth:(id,p,st)=>({
    back:`<polygon points="22,180 38,156 58,168 52,186" fill="${p.d}"/>
          <polygon points="150,176 170,150 186,176 176,188" fill="${p.d}"/>`,
    front:`<polyline points="156,30 146,44 160,52" fill="none" stroke="${p.l}" stroke-width="1.6" opacity=".6"/>` + (st>=2?`<polygon points="26,84 34,70 42,84" fill="${p.c}" opacity=".9"/>`:'')
  }),
  cosmos:(id,p,st)=>({
    back:`<path d="M24 40 L26 48 L34 50 L26 52 L24 60 L22 52 L14 50 L22 48 Z" fill="${p.l}"/>
          <path d="M170 150 L172 158 L180 160 L172 162 L170 170 L168 162 L160 160 L168 158 Z" fill="${p.l}"/>
          <path d="M176 30 L177 35 L182 36 L177 37 L176 42 L175 37 L170 36 L175 35 Z" fill="${p.l}"/>`,
    front:`<ellipse cx="100" cy="112" rx="92" ry="22" fill="none" stroke="${p.glow}" stroke-width="1.5" stroke-dasharray="4 6" transform="rotate(-16 100 112)" opacity=".7"/>`
  })
};
function spriteSVG(spId, flip){
  const s = SP[spId]; const p = PAL[s.type];
  const id = 's' + (++SVG_UID);
  const d = DECO[s.type](id, p, s.stage);
  const body = SHAPES[s.shape](id, p);
  const halo = s.stage >= 3 ? `<circle cx="100" cy="100" r="96" fill="none" stroke="${p.glow}" stroke-width="1.5" stroke-dasharray="2 8" opacity=".55"/>` : '';
  const tf = flip ? 'translate(200 0) scale(-1 1)' : '';
  return `<svg viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
    <defs>
      <radialGradient id="${id}b" cx="38%" cy="30%" r="72%"><stop offset="0" stop-color="${p.l}"/><stop offset=".42" stop-color="${p.c}"/><stop offset="1" stop-color="${p.d}"/></radialGradient>
      <linearGradient id="${id}w" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#fff" stop-opacity=".85"/><stop offset="1" stop-color="${p.c}" stop-opacity=".25"/></linearGradient>
      <radialGradient id="${id}g"><stop offset="0" stop-color="${p.glow}" stop-opacity=".5"/><stop offset="1" stop-color="${p.glow}" stop-opacity="0"/></radialGradient>
      <filter id="${id}q" x="-50%" y="-50%" width="200%" height="200%"><feGaussianBlur stdDeviation="2.5" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
    </defs>
    <circle cx="100" cy="104" r="96" fill="url(#${id}g)"/>
    <g transform="${tf}">${d.back}${body}${d.front}${halo}</g>
  </svg>`;
}
const ICONS = {
  wild:`<path d="M8 34 Q10 20 6 12 M16 34 Q18 16 16 6 M24 34 Q26 18 30 10 M32 34 Q30 22 36 16" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round"/>`,
  cache:`<path d="M7 16 Q20 4 33 16" fill="none" stroke="currentColor" stroke-width="2.4"/><rect x="7" y="16" width="26" height="18" rx="3" fill="none" stroke="currentColor" stroke-width="2.4"/><path d="M7 22 H33" stroke="currentColor" stroke-width="2"/><rect x="18" y="20" width="4" height="6" fill="currentColor"/>`,
  conductor:`<circle cx="20" cy="13" r="6" fill="none" stroke="currentColor" stroke-width="2.4"/><path d="M8 34 Q8 22 20 22 Q32 22 32 34" fill="none" stroke="currentColor" stroke-width="2.4"/>`,
  spring:`<path d="M20 5 C26 14 31 20 31 26 A11 11 0 0 1 9 26 C9 20 14 14 20 5 Z" fill="none" stroke="currentColor" stroke-width="2.4"/><path d="M15 26 A5 5 0 0 0 20 31" fill="none" stroke="currentColor" stroke-width="2"/>`,
  gate:`<path d="M6 30 L8 12 L15 22 L20 8 L25 22 L32 12 L34 30 Z" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linejoin="round"/><rect x="6" y="30" width="28" height="4" fill="currentColor"/>`,
  prism:`<polygon points="20,4 36,16 28,36 12,36 4,16" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linejoin="round"/><polyline points="4,16 20,22 36,16" fill="none" stroke="currentColor" stroke-width="1.6"/><polyline points="20,4 20,22 28,36" fill="none" stroke="currentColor" stroke-width="1.4" opacity=".7"/>`,
  prism2:`<polygon points="20,4 36,16 28,36 12,36 4,16" fill="currentColor" opacity=".3" stroke="currentColor" stroke-width="2.4" stroke-linejoin="round"/><polygon points="20,10 30,18 24,30 16,30 10,18" fill="currentColor" opacity=".6"/><polyline points="4,16 20,22 36,16" fill="none" stroke="currentColor" stroke-width="1.6"/>`,
  core:`<circle cx="20" cy="20" r="14" fill="none" stroke="currentColor" stroke-width="2.4"/><path d="M20 12 V28 M12 20 H28" stroke="currentColor" stroke-width="3.2" stroke-linecap="round"/>`,
  elixir:`<path d="M16 6 H24 M17 6 V16 L8 32 Q7 36 12 36 H28 Q33 36 32 32 L23 16 V6" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linejoin="round"/><path d="M12 26 H28" stroke="currentColor" stroke-width="2"/><circle cx="18" cy="30" r="2" fill="currentColor"/>`,
  cleanser:`<path d="M10 30 C10 14 22 8 32 8 C32 20 26 32 10 30 Z" fill="none" stroke="currentColor" stroke-width="2.4"/><path d="M10 34 L24 18" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"/>`,
  vitals:`<path d="M20 34 C6 24 4 16 8 11 C12 6 18 8 20 13 C22 8 28 6 32 11 C36 16 34 24 20 34 Z" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linejoin="round"/><path d="M12 18 H16 L18 14 L22 22 L24 18 H28" fill="none" stroke="currentColor" stroke-width="2"/>`,
  booster:`<path d="M24 4 L10 22 H19 L15 36 L30 16 H21 Z" fill="currentColor" opacity=".9"/>`
};
function iconSVG(name, cls=''){ return `<svg viewBox="0 0 40 40" class="${cls}" aria-hidden="true">${ICONS[name]||''}</svg>`; }

/* ---------- 10. STATE, PERSISTENCE, STAT MATH ---------- */
const SAVE_KEY = 'aethelgard.save.v1';
const STATUS_NAME = { burn:'Burn', shock:'Shock', root:'Root', chill:'Chill' };
const STATUS_COLOR = { burn:'#ff5a2a', shock:'#facc15', root:'#4ade80', chill:'#67e8f9' };
const SCREEN_NAMES = ['menu','world','battle','dex','camp'];
let state = null;
let B = null;              // active battle
let fxTimer = null;
let campTab = 'party';
let campSel = null;
let dexSel = 'emberkit';
let dexReturn = 'menu';
let currentScreen = 'menu';

function freshProg(){ const p = {}; ZONES.forEach(z => p[z.id] = { pos: -1 }); return p; }
function defaultState(){
  return {
    v:1, party:[], box:[],
    inv:{ harmonizer:3, hyper:0, core:1, elixir:0, vitals:0, cleanser:0, booster:0 },
    shards:0, unlocked:1, zone:0, prog:freshProg(), bosses:{},
    dex:{ seen:{}, caught:{} }, nextUid:1, mute:false, complete:false
  };
}
function loadGame(){
  try{
    const raw = localStorage.getItem(SAVE_KEY);
    if (!raw) return null;
    const s = JSON.parse(raw);
    const d = defaultState();
    const m = Object.assign(d, s);
    m.inv = Object.assign(d.inv, s.inv || {});
    m.prog = Object.assign(freshProg(), s.prog || {});
    m.dex = Object.assign({ seen:{}, caught:{} }, s.dex || {});
    if (!Array.isArray(m.party) || m.party.length === 0) return null;
    m.zone = clamp(m.zone | 0, 0, m.unlocked - 1);
    return m;
  }catch(e){ return null; }
}
function hasSave(){ return !!localStorage.getItem(SAVE_KEY) && !!loadGame(); }
function saveGame(){
  if (!state) return;
  try { localStorage.setItem(SAVE_KEY, JSON.stringify(state)); } catch(e){}
  updateMenuInfo();
}
function newGame(starter){
  state = defaultState();
  const m = newMon(starter, 1);
  state.party.push(m);
  state.dex.caught[starter] = true;
  state.dex.seen[starter] = true;
  saveGame();
}

function maxHP(m){ return Math.floor(SP[m.sp].base.hp * m.lv / 25) + m.lv + 10; }
function stat(m, k){ return Math.floor(SP[m.sp].base[k] * m.lv / 10) + 10; }
function speedOf(m){ return stat(m,'spd') * (m.st === 'chill' ? 0.6 : 1); }
function xpNeed(lv){ return 2*lv*lv + 8*lv + 10; }
function yieldXP(lv, trainer){ return Math.floor(lv * 14 * (1 + lv/10) * (trainer ? 1.35 : 1)); }
function monName(m){ return SP[m.sp].name; }
function hpColor(pct){ return pct > 50 ? '#34d399' : (pct > 20 ? '#facc15' : '#f87171'); }

function refreshMoves(m){
  const all = SP[m.sp].moves;
  const idx = [];
  all.forEach((mo, i) => { if (mo.u <= m.lv) idx.push(i); });
  m.mi = idx.slice(-4);
  return all.filter(mo => mo.u === m.lv).map(mo => mo.n);
}
function normalizeEvo(m){
  while (SP[m.sp].evo && m.lv >= SP[m.sp].evo && SP[m.sp].next) m.sp = SP[m.sp].next;
}
function newMon(spId, lv){
  const m = { uid: state.nextUid++, sp: spId, lv: lv, xp: 0, hp: 0, st: null, mi: [] };
  normalizeEvo(m);
  refreshMoves(m);
  m.hp = maxHP(m);
  return m;
}
function gainXP(m, amt, loud){
  if (m.hp <= 0 || m.lv >= 20) return;
  m.xp += amt;
  if (loud) logMsg(`${monName(m)} gained ${amt} XP.`, 'dim');
  while (m.lv < 20 && m.xp >= xpNeed(m.lv)){
    m.xp -= xpNeed(m.lv);
    const old = maxHP(m);
    m.lv++;
    m.hp = Math.min(maxHP(m), m.hp + (maxHP(m) - old));
    logMsg(`${monName(m)} grew to Lv ${m.lv}!`, 'sys');
    SFX.levelUp();
    const learned = refreshMoves(m);
    learned.forEach(n => logMsg(`${monName(m)} learned ${n}!`, 'sys'));
    const sp = SP[m.sp];
    if (sp.evo && m.lv >= sp.evo && sp.next){
      const from = sp.name;
      m.sp = sp.next;
      refreshMoves(m);
      m.hp = Math.min(maxHP(m), m.hp);
      SFX.evolve();
      logMsg(`${from} evolved into ${SP[m.sp].name}!`, 'boss');
      state.dex.seen[m.sp] = true;
    }
  }
  if (m.lv >= 20) m.xp = 0;
}

/* ---------- 11. UI HELPERS ---------- */
function toast(msg, col){
  const t = $('toast');
  t.textContent = msg;
  t.style.color = col || '#fff';
  t.classList.add('show');
  clearTimeout(toast._t);
  toast._t = setTimeout(() => t.classList.remove('show'), 2300);
}
function dialog(title, html, buttons){
  return new Promise(res => {
    $('o-title').textContent = title;
    $('o-text').innerHTML = html;
    const box = $('o-btns');
    box.innerHTML = '';
    buttons.forEach((b, i) => {
      const el = document.createElement('button');
      el.className = 'btn ' + (b.cls || '');
      el.textContent = b.label;
      el.onclick = () => { $('overlay').classList.remove('show'); res(b.value !== undefined ? b.value : i); };
      box.appendChild(el);
    });
    $('overlay').classList.add('show');
  });
}
function showScreen(name){
  currentScreen = name;
  document.querySelectorAll('.screen').forEach(s => s.classList.remove('active'));
  $('s-' + name).classList.add('active');
}
function mkBtn(label, cls, fn, dis, sub){
  const b = document.createElement('button');
  b.className = 'btn ' + (cls || '');
  b.disabled = !!dis;
  b.innerHTML = label + (sub ? `<span class="mt">${sub}</span>` : '');
  b.onclick = fn;
  return b;
}
function barHTML(pct, col){
  return `<div class="hpbar"><div class="hpfill" style="width:${clamp(pct,0,100)}%;background:${col};box-shadow:0 0 10px ${col}"></div></div>`;
}
function monMiniHTML(m, extraCls){
  const max = maxHP(m);
  const pct = m.hp / max * 100;
  const c = PAL[SP[m.sp].type].c;
  return `<div class="mini ${extraCls||''}" style="--c:${c};border-color:${c}55;${m.hp<=0?'opacity:.45':''}">
    <div class="ms">${spriteSVG(m.sp, false)}</div>
    <div class="mi">
      <div class="mn"><span>${esc(monName(m))}</span><span class="small muted">Lv ${m.lv}</span></div>
      <div style="margin:6px 0 4px">${barHTML(pct, hpColor(pct))}</div>
      <div class="hpnum">${m.hp} / ${max} HP${m.st?` · <span style="color:${STATUS_COLOR[m.st]}">${STATUS_NAME[m.st]}</span>`:''}</div>
    </div></div>`;
}
function logMsg(text, cls){
  const log = $('battle-log');
  const p = document.createElement('p');
  if (cls) p.className = cls;
  p.textContent = text;
  log.appendChild(p);
  while (log.children.length > 6) log.removeChild(log.firstChild);
}
function updateMenuInfo(){
  const info = $('menu-info');
  const s = loadGame();
  $('btn-continue').disabled = !s;
  if (!s){ info.textContent = 'No saved journey found. Begin a new one.'; return; }
  info.textContent = `Party ${s.party.length}/3 · ✦ ${s.shards} Shards · Zones ${s.unlocked}/4 · Bosses ${Object.keys(s.bosses).length}/4 · Dex ${Object.keys(s.dex.caught).length}/18${s.complete?' · Harmony Restored':''}`;
}

/* ---------- 12. OVERWORLD ---------- */
function canClick(z, i){
  const zi = z.index;
  if (zi >= state.unlocked) return false;
  const pos = state.prog[z.id].pos;
  const n = z.nodes[i];
  if (i === pos + 1) return true;
  if (i <= pos && (n.t === 'wild' || n.t === 'spring')) return true;
  return false;
}
function advance(z, i){
  if (i > state.prog[z.id].pos) state.prog[z.id].pos = i;
}
function renderWorld(){
  const z = ZONES[state.zone];
  const root = $('s-world');
  root.style.setProperty('--zc', z.col);
  root.style.setProperty('--zglow', z.glow);
  const zn = $('w-zone-name');
  zn.textContent = z.name.toUpperCase();
  zn.style.color = z.col;
  $('w-zone-sub').textContent = z.sub;
  $('w-shards').textContent = state.shards;
  $('w-party-n').textContent = state.party.length;

  const tabs = $('zone-tabs');
  tabs.innerHTML = '';
  ZONES.forEach((zz, i) => {
    const b = document.createElement('button');
    b.className = 'ztab' + (i === state.zone ? ' on' : '');
    b.style.setProperty('--zc', zz.col);
    b.disabled = i >= state.unlocked;
    b.textContent = (i >= state.unlocked ? '🔒 ' : '') + (i+1) + '. ' + zz.name.replace('The ', '');
    b.onclick = () => { state.zone = i; saveGame(); renderWorld(); };
    tabs.appendChild(b);
  });

  const dec = $('zdeco');
  dec.innerHTML = '';
  for (let k = 0; k < 16; k++){
    const d = document.createElement('i');
    d.style.left = rand(2, 98) + '%';
    d.style.bottom = rand(-5, 20) + '%';
    d.style.animationDelay = (-rand(0, 6)).toFixed(2) + 's';
    d.style.animationDuration = rand(4, 8).toFixed(2) + 's';
    d.style.width = d.style.height = rand(3, 7).toFixed(1) + 'px';
    dec.appendChild(d);
  }

  const pos = state.prog[z.id].pos;
  const pts = z.nodes.map(n => `${n.x},${n.y}`).join(' ');
  const donePts = z.nodes.slice(0, Math.max(0, pos) + 1).map(n => `${n.x},${n.y}`).join(' ');
  $('trail').innerHTML = `<polyline class="trail-line" points="${pts}"/>` + (pos >= 0 ? `<polyline class="trail-done" points="${donePts}"/>` : '');

  const nodesEl = $('nodes');
  nodesEl.innerHTML = '';
  const colorFor = { wild: z.col, cache: '#facc15', conductor: '#fff', spring: '#67e8f9', gate: '#f472b6' };
  z.nodes.forEach((n, i) => {
    const cleared = i <= pos;
    const repeatable = n.t === 'wild' || n.t === 'spring';
    const click = canClick(z, i);
    let cls = 'node';
    if (n.t === 'gate') cls += ' gate';
    if (cleared && !repeatable) cls += ' done';
    if (click) cls += ' current';
    if (!click && !cleared) cls += ' locked';
    const el = document.createElement('div');
    el.className = cls;
    el.style.left = n.x + '%';
    el.style.top = n.y + '%';
    el.style.color = n.t === 'conductor' ? TRAINERS[n.tr].col : colorFor[n.t];
    el.style.setProperty('--zc', n.t === 'conductor' ? TRAINERS[n.tr].col : colorFor[n.t]);
    el.innerHTML = iconSVG(n.t) + `<span class="nlabel">${esc(n.l)}</span>`;
    el.title = n.l;
    el.onclick = () => handleNode(i);
    nodesEl.appendChild(el);
  });

  renderSideParty();
  $('w-progress').innerHTML = `Bosses felled: ${Object.keys(state.bosses).length}/4<br>Zones unlocked: ${state.unlocked}/4<br>Dex caught: ${Object.keys(state.dex.caught).length}/18${state.complete ? '<br><span style="color:#c4b5fd">Harmony restored.</span>' : ''}`;
  const packKeys = ITEM_ORDER.filter(k => state.inv[k] > 0);
  $('w-pack').innerHTML = packKeys.length ? packKeys.map(k => `<div>${ITEMS[k].name} × ${state.inv[k]}</div>`).join('') : '<span class="muted">Empty</span>';
}
function renderSideParty(){
  $('w-party').innerHTML = state.party.map(m => monMiniHTML(m)).join('');
}
async function handleNode(i){
  const z = ZONES[state.zone];
  if (!canClick(z, i)){ toast('Clear the trail ahead first.', '#fcd34d'); return; }
  const n = z.nodes[i];
  if (n.t === 'wild'){ advance(z, i); saveGame(); renderWorld(); return startBattle({ kind:'wild', zi:z.index, ni:i }); }
  if (n.t === 'conductor') return startBattle({ kind:'trainer', zi:z.index, ni:i, tr:n.tr });
  if (n.t === 'gate') return startBattle({ kind:'boss', zi:z.index, ni:i, tr:n.tr });
  if (n.t === 'spring'){
    state.party.forEach(m => { m.hp = maxHP(m); m.st = null; });
    advance(z, i);
    SFX.heal();
    saveGame();
    renderWorld();
    dialog('Healing Spring', 'The spring hums with restorative resonance. Your Resonants are fully healed and cured.', [{label:'Lovely'}]);
    return;
  }
  if (n.t === 'cache') return openCache(z, i);
}
async function openCache(z, i){
  advance(z, i);
  let html;
  if (Math.random() < 0.45){
    const amt = randInt(40, 90) + z.index * 25;
    state.shards += amt;
    html = `You crack open a mossy chest and find <b>${amt} Aether Shards</b>.`;
  } else {
    const table = [{k:'vitals',w:35},{k:'booster',w:25},{k:'cleanser',w:25},{k:'harmonizer',w:15}];
    let r = Math.random() * 100, pickK = 'vitals';
    for (const t of table){ if (r < t.w){ pickK = t.k; break; } r -= t.w; }
    state.inv[pickK]++;
    html = `Inside you find a <b>${ITEMS[pickK].name}</b>.`;
  }
  saveGame();
  renderWorld();
  await dialog('Mystery Cache', html, [{label:'Excellent'}]);
}

/* ---------- 13. BATTLE ENGINE ---------- */
function activeMon(){ return state.party[B.pi]; }
function aliveCount(){ return state.party.filter(m => m.hp > 0).length; }
function calcDamage(att, def, mv){
  const A = stat(att,'atk'), D = Math.max(1, stat(def,'def'));
  const base = ((2*att.lv/5 + 2) * mv.p * (A/D)) / 50 + 2;
  const eff = effectiveness(mv.t, SP[def.sp].type);
  const crit = Math.random() < 0.10;
  const rnd = rand(0.85, 1.0);
  const dmg = Math.max(1, Math.floor(base * eff * rnd * (crit ? 1.5 : 1)));
  return { dmg, eff, crit };
}
function usableMoves(m){ return (m.mi || []).map(i => SP[m.sp].moves[i]); }
function pickEnemyMove(e){
  const moves = usableMoves(e);
  if (!moves.length) return null;
  const p = activeMon();
  const scored = moves.map(mo => ({ mo, s: mo.p * effectiveness(mo.t, SP[p.sp].type) * (mo.a/100) * rand(0.75,1.25) }));
  scored.sort((a,b) => b.s - a.s);
  if (Math.random() < 0.25) return pick(moves);
  return scored[0].mo;
}
function sprFx(side){ return $(side === 'p' ? 'player-sprite' : 'enemy-sprite'); }
function popDamage(side, text, col){
  const host = $('arena');
  const el = document.createElement('div');
  el.className = 'dmg-pop';
  el.textContent = text;
  el.style.color = col || '#fff';
  const r = sprFx(side).getBoundingClientRect();
  const a = host.getBoundingClientRect();
  el.style.left = (r.left - a.left + r.width * 0.5 - 20) + 'px';
  el.style.top = (r.top - a.top + r.height * 0.1) + 'px';
  host.appendChild(el);
  setTimeout(() => el.remove(), 1000);
}
function flashSprite(side){
  const el = sprFx(side).querySelector('svg');
  if (!el) return;
  el.classList.remove('flash'); void el.offsetWidth; el.classList.add('flash');
}
function shakeArena(){
  const a = $('arena');
  a.classList.remove('shake'); void a.offsetWidth; a.classList.add('shake');
  setTimeout(() => a.classList.remove('shake'), 520);
}
function critFlash(){
  const c = $('crit-flash');
  c.classList.remove('go'); void c.offsetWidth; c.classList.add('go');
}
function faintAnim(side){
  const el = sprFx(side);
  el.classList.add('faint');
  return sleep(800);
}
function resetFaint(side){ sprFx(side).classList.remove('faint'); }
const wait = ms => sleep(ms);

async function startBattle(info){
  B = {
    kind: info.kind, zi: info.zi, ni: info.ni, tr: info.tr || null,
    team: [], ei: 0, e: null, pi: 0, busy: true, boost: false, bossWarned: false, done: false, wild: info.kind === 'wild'
  };
  let pi = state.party.findIndex(m => m.hp > 0);
  if (pi < 0){ state.party.forEach(m => m.hp = maxHP(m)); pi = 0; }
  B.pi = pi;
  const z = ZONES[info.zi];
  if (B.kind === 'wild'){
    const lv = clamp(randInt(z.min, z.max), 1, 20);
    const sp = pick(z.pool);
    const e = newMon(sp, lv);
    B.team = [e];
  } else {
    const tr = info.kind === 'boss' ? TRAINERS[info.tr] : TRAINERS[info.tr];
    B.team = tr.team.map(t => newMon(t.sp, t.l));
  }
  B.team.forEach(m => state.dex.seen[m.sp] = true);
  B.e = B.team[0];
  saveGame();

  showScreen('battle');
  $('battle-log').innerHTML = '';
  $('b-zone').textContent = z.name + ' · ' + z.sub;
  $('b-shards').textContent = state.shards;
  $('b-prisms').textContent = `Harmonizer ${state.inv.harmonizer} · Hyper ${state.inv.hyper}`;
  SFX.init();
  renderBattle(true);

  if (B.kind === 'wild'){
    $('b-title').textContent = 'WILD ENCOUNTER';
    $('b-title').style.color = z.col;
    logMsg(`A wild ${SP[B.e.sp].name} (Lv ${B.e.lv}) appeared!`, 'sys');
    await wait(500);
  } else {
    const tr = TRAINERS[info.tr];
    $('b-title').textContent = (B.kind === 'boss' ? 'GATEKEEPER · ' : 'CONDUCTOR · ') + tr.name.toUpperCase();
    $('b-title').style.color = tr.col;
    logMsg(tr.intro, B.kind === 'boss' ? 'boss' : 'sys');
    await wait(400);
    logMsg(`${tr.name} sends out ${SP[B.e.sp].name}!`, 'sys');
  }
  sprFx('e').classList.add('enter-e');
  sprFx('p').classList.add('enter-p');
  setTimeout(() => { sprFx('e').classList.remove('enter-e'); sprFx('p').classList.remove('enter-p'); }, 800);
  await wait(700);
  logMsg(`Go, ${SP[activeMon().sp].name}!`, 'dim');
  B.busy = false;
  showMenuMain();
  renderBattle();
}

function cardHTML(side){
  const isP = side === 'p';
  const m = isP ? activeMon() : B.e;
  const sp = SP[m.sp], pal = PAL[sp.type];
  const max = maxHP(m);
  const pct = m.hp / max * 100;
  const stHTML = m.st ? `<span class="sbadge ${m.st}">${STATUS_NAME[m.st]}</span>` : '';
  const xpHTML = isP ? `<div class="xpbar"><div class="xpfill" style="width:${clamp(m.xp / xpNeed(m.lv) * 100,0,100)}%"></div></div>` : '';
  return `<div class="nm"><span>${esc(sp.name)}</span><span class="small muted">Lv ${m.lv}</span></div>
    <div style="display:flex;gap:6px;align-items:center;margin-top:6px"><span class="tp">${pal.name}</span>${stHTML}</div>
    <div style="margin-top:10px" class="hpbar"><div class="hpfill" style="width:${pct}%;background:${hpColor(pct)};box-shadow:0 0 10px ${hpColor(pct)}"></div></div>
    <div class="hpnum" style="margin-top:4px;display:flex;justify-content:space-between"><span>HP ${m.hp} / ${max}</span>${isP ? `<span>XP ${Math.floor(m.xp)}/${xpNeed(m.lv)}</span>` : (B.kind!=='wild'?`<span>${B.team.filter(t=>t.hp>0).length}/${B.team.length} left</span>`:'')}</div>
    ${xpHTML}`;
}
function renderBattle(initial){
  const eCard = $('enemy-card'), pCard = $('player-card');
  const p = activeMon(), e = B.e;
  const eKey = e.uid, pKey = p.uid + ':' + p.sp;
  eCard.style.setProperty('--c', PAL[SP[e.sp].type].c);
  pCard.style.setProperty('--c', PAL[SP[p.sp].type].c);
  eCard.classList.toggle('neon', true);
  pCard.classList.toggle('neon', true);
  if (eCard.dataset.key !== String(eKey) || initial){ eCard.dataset.key = eKey; eCard.innerHTML = cardHTML('e'); }
  else eCard.innerHTML = cardHTML('e');
  pCard.innerHTML = cardHTML('p');
  if ($('e-svg').dataset.key !== String(eKey)){ $('e-svg').dataset.key = eKey; $('e-svg').innerHTML = spriteSVG(e.sp, true); }
  if ($('p-svg').dataset.key !== pKey){ $('p-svg').dataset.key = pKey; $('p-svg').innerHTML = spriteSVG(p.sp, false); }
  $('b-shards').textContent = state.shards;
  $('b-prisms').textContent = `Harmonizer ${state.inv.harmonizer} · Hyper ${state.inv.hyper}`;
  $('e-svg').classList.toggle('bounce', e.hp > 0);
  $('p-svg').classList.toggle('bounce', p.hp > 0);
  renderStatusFX();
}
let fxKey = '';
function renderStatusFX(){
  const layer = $('fx-layer');
  const key = [B.e.st || '', activeMon().st || ''].join('|');
  if (key === fxKey) return;
  fxKey = key;
  layer.innerHTML = '';
  const add = (st, left, top) => {
    if (!st) return;
    for (let i = 0; i < 9; i++){
      const d = document.createElement('div');
      d.className = 'fx-p';
      d.style.background = STATUS_COLOR[st];
      d.style.boxShadow = `0 0 10px ${STATUS_COLOR[st]}`;
      d.style.left = (left + rand(-8, 8)) + '%';
      d.style.top = (top + rand(-4, 4)) + '%';
      d.style.setProperty('--dx', rand(-18, 18).toFixed(1) + 'px');
      d.style.animationDelay = (-rand(0, 2.2)).toFixed(2) + 's';
      layer.appendChild(d);
    }
  };
  add(B.e.st, 74, 30);
  add(activeMon().st, 22, 70);
}
function showMenuMain(){
  $('menu-main').classList.remove('hidden-x');
  $('menu-sub').classList.add('hidden-x');
  const box = $('menu-main');
  box.innerHTML = '';
  const isWild = B.kind === 'wild';
  const fight = mkBtn('⚔ Attack', 'fight', () => { SFX.select(); showMoveMenu(); });
  const prism = mkBtn('✦ Prisms', '', () => { SFX.select(); showPrismMenu(); }, !isWild || B.busy, isWild ? `Harm ${state.inv.harmonizer} · Hyper ${state.inv.hyper}` : 'Wild Resonants only');
  const exch = mkBtn('⇄ Exchange', '', () => { SFX.select(); showSwitchMenu(false); }, false, 'Swap your active Resonant');
  const inv = mkBtn('◈ Inventory', '', () => { SFX.select(); showItemMenu(); }, false, 'Use an item');
  box.append(fight, prism, exch, inv);
  if (isWild){
    const flee = mkBtn('Retreat', 'ghost small-btn', () => { SFX.select(); retreat(); }, false);
    flee.style.gridColumn = '1 / -1';
    flee.style.padding = '6px';
    flee.style.fontSize = '12px';
    box.append(flee);
  }
}
function subMenu(title){
  $('menu-main').classList.add('hidden-x');
  const sub = $('menu-sub');
  sub.classList.remove('hidden-x');
  sub.innerHTML = `<div class="menu-head"><span>${title}</span><button class="btn small-btn" id="sub-back">← Back</button></div><div class="list" id="sub-list"></div>`;
  $('sub-back').onclick = () => { SFX.select(); showMenuMain(); };
  return $('sub-list');
}
function showMoveMenu(){
  const list = subMenu('CHOOSE A MOVE · ' + SP[activeMon().sp].name);
  const p = activeMon();
  const moves = usableMoves(p);
  moves.forEach((mo, i) => {
    const pal = PAL[mo.t] || PAL.cosmos;
    const b = document.createElement('button');
    b.className = 'btn move';
    b.style.setProperty('--c', pal.c);
    b.innerHTML = `<span>${esc(mo.n)}</span><span class="mt">${pal.name} · Pow ${mo.p} · Acc ${mo.a}${mo.s?' · '+STATUS_NAME[mo.s]:''}</span>`;
    b.onmouseenter = () => SFX.hover();
    b.onclick = () => { SFX.select(); playerAct({type:'move', mi: p.mi[i]}); };
    list.appendChild(b);
  });
  for (let k = moves.length; k < 4; k++){
    const b = document.createElement('button'); b.className = 'btn move'; b.disabled = true;
    b.innerHTML = `<span>— locked —</span><span class="mt">Unlocks by leveling</span>`;
    list.appendChild(b);
  }
}
function showPrismMenu(){
  const list = subMenu('AETHER PRISMS');
  const opts = [
    {k:'harmonizer', label:'Harmonizer Prism', sub:`Catch ×1.0 · owned ${state.inv.harmonizer}`},
    {k:'hyper', label:'Hyper-Conductive Prism', sub:`Catch ×1.8 · owned ${state.inv.hyper}`},
  ];
  opts.forEach(o => {
    const b = mkBtn(o.label, '', () => { SFX.select(); playerAct({type:'prism', key:o.k}); }, state.inv[o.k] <= 0, o.sub);
    b.classList.add('move'); b.style.setProperty('--c', '#67e8f9');
    b.style.width = '100%';
    list.appendChild(b);
  });
  const boost = mkBtn(B.boost ? 'Booster Active ✓' : 'Prism Booster', '', () => { SFX.select(); useBoosterInBattle(); }, B.boost || state.inv.booster <= 0, `+0.6 tier to next throw · owned ${state.inv.booster}`);
  boost.classList.add('move'); boost.style.setProperty('--c', '#facc15'); boost.style.width = '100%';
  list.appendChild(boost);
}
function useBoosterInBattle(){
  if (B.boost || state.inv.booster <= 0) return;
  state.inv.booster--; B.boost = true; saveGame();
  toast('Prism Booster charged!', '#facc15');
  showPrismMenu();
}
function showItemMenu(){
  const list = subMenu('INVENTORY');
  const p = activeMon();
  const keys = ['vitals','core','elixir','cleanser'];
  keys.forEach(k => {
    const it = ITEMS[k];
    const dis = state.inv[k] <= 0 || (k === 'vitals' || k === 'core' ? p.hp >= maxHP(p) : (k === 'elixir' || k === 'cleanser' ? !p.st : false));
    const b = mkBtn(`${iconSVG(it.icon)} ${it.name} <span class="muted">×${state.inv[k]}</span>`, 'item-btn', () => { SFX.select(); playerAct({type:'item', key:k}); }, dis, it.desc);
    b.classList.add('move'); b.style.setProperty('--c', '#67e8f9');
    b.style.width = '100%';
    b.style.flexDirection = 'column';
    list.appendChild(b);
  });
  const boost = mkBtn('Prism Booster', '', () => { SFX.select(); useBoosterInBattle(); }, state.inv.booster <= 0 || B.boost, `Charges next prism · owned ${state.inv.booster}`);
  boost.classList.add('move'); boost.style.setProperty('--c', '#facc15'); boost.style.width = '100%';
  list.appendChild(boost);
}
function showSwitchMenu(forced){
  const list = subMenu(forced ? 'CHOOSE A RESONANT TO SEND OUT' : 'EXCHANGE RESONANT');
  list.classList.add('one');
  state.party.forEach((m, i) => {
    const pct = m.hp / maxHP(m) * 100;
    const dis = m.hp <= 0 || i === B.pi;
    const b = document.createElement('button');
    b.className = 'btn';
    b.disabled = dis;
    b.style.textAlign = 'left';
    b.style.padding = '0';
    b.style.overflow = 'hidden';
    b.innerHTML = monMiniHTML(m) + (i === B.pi ? '<div class="small muted" style="padding:0 12px 8px">Active</div>' : '');
    b.onclick = () => { SFX.select(); if (forced) forcedSwitch(i); else playerAct({type:'switch', pi:i}); };
    list.appendChild(b);
  });
}
function hideSub(){ $('menu-sub').classList.add('hidden-x'); }
function setMenuEnabled(on){ $('menu-box').style.pointerEvents = on ? 'auto' : 'none'; $('menu-box').style.opacity = on ? 1 : .6; }

/* ---------- 14. TURN RESOLUTION ---------- */
const LUNGE_CSS = `
@keyframes lungeE{0%,100%{transform:translateX(0)}40%{transform:translate(-26px,8px) scale(1.06)}}
@keyframes lungeP{0%,100%{transform:translateX(0)}40%{transform:translate(26px,-8px) scale(1.06)}}
.lunge-e{animation:lungeE .45s ease-out}
.lunge-p{animation:lungeP .45s ease-out}`;
(function injectLunge(){ const s = document.createElement('style'); s.textContent = LUNGE_CSS; document.head.appendChild(s); })();

function lunge(side){
  const el = sprFx(side).querySelector('svg');
  if (!el) return;
  const cls = side === 'p' ? 'lunge-p' : 'lunge-e';
  el.classList.remove('lunge-e','lunge-p'); void el.offsetWidth; el.classList.add(cls);
  setTimeout(() => el.classList.remove(cls), 480);
}
function hideMenus(){ $('menu-main').classList.add('hidden-x'); $('menu-sub').classList.add('hidden-x'); }
function trainerName(){ return B.tr ? TRAINERS[B.tr].name : 'The wild Resonant'; }

async function strike(att, def, mo, side){
  const an = monName(att), dn = monName(def);
  if (att.st === 'shock' && Math.random() < 0.25){ logMsg(`${an} is numbed by shock and can't move!`, 'dim'); await wait(450); return; }
  if (att.st === 'root' && Math.random() < 0.15){ logMsg(`${an} is rooted in place!`, 'dim'); await wait(450); return; }
  logMsg(`${an} used ${mo.n}!`);
  lunge(side);
  await wait(260);
  if (Math.random() * 100 > mo.a){
    logMsg(`${an}'s ${mo.n} missed!`, 'dim');
    await wait(450);
    return;
  }
  const r = calcDamage(att, def, mo);
  const defSide = side === 'p' ? 'e' : 'p';
  def.hp = Math.max(0, def.hp - r.dmg);
  flashSprite(defSide);
  popDamage(defSide, '-' + r.dmg, r.crit ? '#fde047' : (r.eff > 1 ? '#fb923c' : '#fff'));
  if (r.crit){ SFX.crit(); critFlash(); logMsg('A critical resonance!', 'sys'); }
  else if (r.eff > 1) SFX.superHit();
  else SFX.hit();
  if (r.eff > 1) logMsg("It's super effective!", 'sys');
  else if (r.eff < 1) logMsg("It's not very effective...", 'dim');
  if (r.dmg >= maxHP(def) * 0.25 || r.crit) shakeArena();
  renderBattle();
  await wait(550);
  if (mo.s && def.hp > 0 && !def.st && Math.random() < mo.c){
    def.st = mo.s;
    SFX.tone(600, 0.12, 'triangle', 0.15, 0, 300);
    logMsg(`${dn} is afflicted with ${STATUS_NAME[mo.s]}!`, 'sys');
    renderBattle();
    await wait(380);
  }
}
async function faint(side){
  const who = side === 'p' ? activeMon() : B.e;
  logMsg(`${monName(who)} fainted!`, 'dim');
  SFX.fail();
  await faintAnim(side);
  resetFaint(side);
  renderBattle();
}
async function settle(){
  if (B.e.hp <= 0){
    await faint('e');
    const next = B.team.findIndex(t => t.hp > 0);
    if (next < 0){ await victory(); return true; }
    B.ei = next; B.e = B.team[next];
    if (B.kind !== 'wild') logMsg(`${trainerName()} sends out ${SP[B.e.sp].name}!`, 'sys');
    renderBattle(true);
    sprFx('e').classList.add('enter-e');
    setTimeout(() => sprFx('e').classList.remove('enter-e'), 800);
    await wait(700);
  }
  if (activeMon().hp <= 0){
    await faint('p');
    if (aliveCount() === 0){ await defeat(); return true; }
    B.busy = true;
    logMsg('Choose your next Resonant!', 'sys');
    showSwitchMenu(true);
    const back = $('sub-back'); if (back) back.style.display = 'none';
    return true;
  }
  if (B.kind === 'boss' && !B.bossWarned && B.e.hp > 0 && B.e.hp / maxHP(B.e) < 0.5 && B.team.length === 1){
    B.bossWarned = true;
    logMsg(TRAINERS[B.tr].low, 'boss');
    await wait(700);
  }
  return false;
}
async function enemyAttack(){
  if (B.e.hp <= 0) return;
  const mo = pickEnemyMove(B.e);
  if (!mo){ logMsg(`${monName(B.e)} waits...`, 'dim'); await wait(400); return; }
  await strike(B.e, activeMon(), mo, 'e');
}
async function endOfTurn(){
  const list = [['p', activeMon()], ['e', B.e]];
  for (const [side, x] of list){
    if (x.hp <= 0) continue;
    if (x.st === 'burn'){
      const d = Math.max(1, Math.floor(maxHP(x) * 0.06));
      x.hp = Math.max(0, x.hp - d);
      logMsg(`${monName(x)} is scorched by burn! (-${d})`, 'dim');
      popDamage(side, '-' + d, '#ff8a5c'); flashSprite(side);
      renderBattle(); await wait(450);
    } else if (x.st === 'root'){
      const d = Math.max(1, Math.floor(maxHP(x) * 0.05));
      x.hp = Math.max(0, x.hp - d);
      logMsg(`${monName(x)} is drained by roots! (-${d})`, 'dim');
      popDamage(side, '-' + d, '#86efac');
      renderBattle(); await wait(450);
    }
  }
  saveGame();
}
async function playerAct(a){
  if (B.busy || B.done) return;
  B.busy = true;
  hideMenus();
  const p = activeMon();
  if (a.type === 'switch'){
    logMsg(`Come back, ${monName(p)}!`, 'dim');
    B.pi = a.pi;
    renderBattle(true);
    sprFx('p').classList.add('enter-p');
    setTimeout(() => sprFx('p').classList.remove('enter-p'), 800);
    await wait(650);
    logMsg(`Go, ${monName(activeMon())}!`, 'sys');
    await wait(300);
    await enemyAttack();
  }
  else if (a.type === 'item'){
    await useItem(a.key);
    await enemyAttack();
  }
  else if (a.type === 'prism'){
    const ok = await throwPrism(a.key);
    if (ok) return;
    await enemyAttack();
  }
  else if (a.type === 'move'){
    const mo = SP[p.sp].moves[a.mi];
    const pFirst = speedOf(p) >= speedOf(B.e);
    if (pFirst){
      if (p.hp > 0 && B.e.hp > 0) await strike(p, B.e, mo, 'p');
      if (await settle()) return;
      if (B.e.hp > 0 && activeMon().hp > 0){ await enemyAttack(); }
    } else {
      await enemyAttack();
      if (await settle()) return;
      if (activeMon().hp > 0 && B.e.hp > 0) await strike(p, B.e, mo, 'p');
    }
  }
  if (await settle()) return;
  await endOfTurn();
  if (await settle()) return;
  B.busy = false;
  showMenuMain();
  renderBattle();
}
async function useItem(key){
  const p = activeMon();
  const it = ITEMS[key];
  if (key === 'vitals'){
    p.hp = Math.min(maxHP(p), p.hp + 40);
    logMsg(`${monName(p)} recovered with a Vitals Elixir.`, 'sys');
    popDamage('p', '+40', '#86efac');
  } else if (key === 'core'){
    const h = Math.floor(maxHP(p) * 0.5);
    p.hp = Math.min(maxHP(p), p.hp + h);
    logMsg(`Core Restorer healed ${monName(p)} for ${h} HP.`, 'sys');
    popDamage('p', '+' + h, '#86efac');
  } else if (key === 'elixir' || key === 'cleanser'){
    const had = p.st;
    p.st = null;
    logMsg(had ? `${monName(p)} was cured of ${STATUS_NAME[had]}.` : `${monName(p)} feels fine.`, 'sys');
  }
  state.inv[key]--;
  SFX.heal();
  flashSprite('p');
  saveGame();
  renderBattle();
  await wait(650);
}
async function throwPrism(key){
  if (B.kind !== 'wild') return false;
  const it = ITEMS[key];
  const tier = it.tier + (B.boost ? 0.6 : 0);
  B.boost = false;
  state.inv[key]--;
  logMsg(`You hurl a ${it.name}!`);
  SFX.prism();
  const e = B.e;
  await wait(800);
  const hpFrac = e.hp / maxHP(e);
  let rate = (1.05 - 0.9 * hpFrac) * 0.52 * tier;
  if (e.st) rate *= 1.4;
  rate = clamp(rate, 0.03, 0.95);
  const ok = Math.random() < rate;
  if (ok){
    if (state.party.length < 3) state.party.push(e); else state.box.push(e);
    state.dex.caught[e.sp] = true;
    state.dex.seen[e.sp] = true;
    SFX.capture();
    logMsg(`Captured ${monName(e)}!`, 'sys');
    B.done = true;
    saveGame();
    await wait(900);
    await dialog('Resonance Captured!', `<div style="width:120px;height:120px;margin:0 auto">${spriteSVG(e.sp, false)}</div><div>${esc(monName(e))} was sealed inside an Aether Prism and joined your ${state.party.length > 0 && state.party.includes(e) ? 'party' : 'box'}.</div>`, [{label:'Splendid'}]);
    leaveBattle();
    return true;
  }
  SFX.fail();
  logMsg('The Resonant shook free of the prism!', 'dim');
  renderBattle();
  await wait(550);
  saveGame();
  return false;
}
async function victory(){
  B.done = true;
  const z = ZONES[B.zi];
  const isTrainer = B.kind !== 'wild';
  const tr = B.tr ? TRAINERS[B.tr] : null;
  const alive = state.party.filter(m => m.hp > 0);
  let totalXP = 0;
  B.team.forEach(t => totalXP += yieldXP(t.lv, isTrainer));
  alive.forEach(m => gainXP(m, Math.floor(totalXP / (m === activeMon() ? 1 : 2)), m === activeMon()));
  let shards = isTrainer ? tr.shards : 12 + B.team[0].lv * 3;
  state.shards += shards;
  saveGame();
  SFX.fanfare();
  logMsg(`${isTrainer ? trainerName() + ' is defeated!' : 'The wild Resonant fled into the grass.'}`, 'sys');
  logMsg(`+${shards} Aether Shards`, 'sys');
  await wait(600);
  if (isTrainer){
    const alreadyCleared = state.prog[z.id].pos >= B.ni;
    advance(z, B.ni);
    if (B.kind === 'boss'){
      state.bosses[B.tr] = true;
      if (z.index + 1 < ZONES.length) state.unlocked = Math.max(state.unlocked, z.index + 2);
      if (B.tr === 'ysolde') state.complete = true;
      state.unlocked = clamp(state.unlocked, 1, 4);
    }
    saveGame();
    await dialog(B.kind === 'boss' ? 'Gatekeeper Defeated!' : 'Duel Won!', `<div class="otext" style="color:#e9d5ff;font-style:italic">${esc(tr.defeat)}</div><div class="muted">+${shards} Aether Shards${alreadyCleared ? '' : ''} · XP distributed.</div>${B.kind==='boss' && z.index+1 < ZONES.length ? `<div style="color:#67e8f9;font-weight:700">New zone unlocked: ${ZONES[z.index+1].name}</div>` : ''}`, [{label:'Continue'}]);
    if (B.tr === 'ysolde'){
      await dialog('Harmony Restored', 'Aethelgard hums in perfect resonance once more. Every Resonant in the realm answers your song. Your journey is complete — but the Canopy, the Grotto, the Crags and the Apex still welcome you to keep exploring.', [{label:'Keep Playing'}]);
    }
  } else {
    await dialog('Victory!', `Your Resonants gathered ${totalXP} XP and you collected ${shards} Aether Shards.`, [{label:'Continue'}]);
  }
  checkLevelsZoneUnlock();
  leaveBattle();
}
function checkLevelsZoneUnlock(){
  let u = 1;
  for (let i = 0; i < ZONES.length; i++){
    const z = ZONES[i];
    if (state.bosses[z.gate] && i + 1 < ZONES.length) u = Math.max(u, i + 2);
  }
  state.unlocked = clamp(Math.max(state.unlocked, u), 1, ZONES.length);
  saveGame();
}
async function defeat(){
  B.done = true;
  const lost = Math.floor(state.shards * 0.1);
  state.shards -= lost;
  state.party.forEach(m => { m.hp = maxHP(m); m.st = null; });
  saveGame();
  await dialog('Your Resonants Fell', `You retreat to camp with your party healed. ${lost ? `Lost ${lost} Aether Shards in the scramble.` : ''}`, [{label:'Return'}]);
  leaveBattle();
}
async function retreat(){
  if (B.kind !== 'wild' || B.busy) return;
  B.done = true;
  logMsg('You retreated safely.', 'dim');
  await wait(500);
  leaveBattle();
}
function forcedSwitch(i){
  B.pi = i;
  logMsg(`Go, ${monName(activeMon())}!`, 'sys');
  renderBattle(true);
  B.busy = false;
  showMenuMain();
  renderBattle();
}
function leaveBattle(){
  B.done = true;
  B.busy = false;
  saveGame();
  showScreen('world');
  renderWorld();
  updateMenuInfo();
}

/* ---------- 15. CONDUCTORS DEX ---------- */
function openDex(from){
  dexReturn = from;
  if (!state){ state = defaultState(); }
  showScreen('dex');
  renderDex();
}
function renderDex(){
  const grid = $('dex-grid');
  grid.innerHTML = '';
  const caught = Object.keys(state.dex.caught).length;
  const seen = Object.keys(state.dex.seen).length;
  $('dex-count').textContent = `Caught ${caught} / 18 · Seen ${seen} / 18`;
  SPECIES_IDS.forEach(id => {
    const s = SP[id];
    const seenIt = !!state.dex.seen[id];
    const cEl = document.createElement('div');
    cEl.className = 'glass dex-card t-' + s.type + (seenIt ? ' neon' : '') + (seenIt ? '' : ' unseen');
    cEl.style.setProperty('--c', PAL[s.type].c);
    cEl.innerHTML = `<div class="dsp">${seenIt ? spriteSVG(id, false) : '<div style="width:100%;height:100%;display:grid;place-items:center;font-size:40px;opacity:.4">?</div>'}</div>
      <div class="dn">${seenIt ? esc(s.name) : '???'}</div>
      <div class="dno">No. ${String(s.no).padStart(3,'0')}</div>
      <span class="dtag">${PAL[s.type].name}</span>`;
    cEl.onmouseenter = () => SFX.hover();
    cEl.onclick = () => { SFX.select(); dexSel = id; renderDexDetail(); };
    grid.appendChild(cEl);
  });
  renderDexDetail();
}
function renderDexDetail(){
  const id = dexSel, s = SP[id], pal = PAL[s.type];
  const det = $('dex-detail');
  det.style.setProperty('--c', pal.c);
  det.className = 'glass neon';
  det.style.setProperty('--c', pal.c);
  const seen = !!state.dex.seen[id];
  if (!seen){
    det.innerHTML = `<div class="font-retro" style="font-size:12px;line-height:1.6">???</div>
      <div class="muted">This Resonant has not yet been encountered.</div>`;
    return;
  }
  const evoLine = s.evo ? `Evolves at Lv ${s.evo}` : 'Final form';
  const chainPrev = Object.values(SP).find(x => x.next === id);
  const chain = [];
  if (chainPrev) chain.push(chainPrev.name);
  chain.push(s.name);
  if (s.next) chain.push(SP[s.next].name);
  const maxStat = 130;
  const stats = [['HP','hp'],['ATK','atk'],['DEF','def'],['SPD','spd']];
  det.innerHTML = `
    <div style="display:flex;gap:14px;align-items:center">
      <div style="width:120px;height:120px;flex:none">${spriteSVG(id, false)}</div>
      <div style="min-width:0">
        <div class="font-retro" style="font-size:11px;line-height:1.6">${esc(s.name)}</div>
        <div class="muted small" style="margin-top:6px">No. ${String(s.no).padStart(3,'0')} · Stage ${s.stage}</div>
        <span class="dtag" style="margin-top:8px">${pal.name}</span>
      </div>
    </div>
    <p class="muted" style="margin:0;line-height:1.6;font-size:13px">${esc(s.desc)}</p>
    <div class="small" style="color:var(--mute)">Line: ${chain.map(esc).join(' → ')} · ${evoLine}</div>
    <div style="display:flex;flex-direction:column;gap:8px;margin-top:4px">
      ${stats.map(([lab,k]) => `<div class="stat-row"><span>${lab}</span><div class="stat-bar"><i style="width:${clamp(s.base[k]/maxStat*100,0,100)}%"></i></div><span>${s.base[k]}</span></div>`).join('')}
    </div>
    <h3 class="small" style="margin:6px 0 0;color:var(--mute);letter-spacing:2px">MOVES</h3>
    ${s.moves.map(mo => { const mp = PAL[mo.t]||PAL.cosmos; return `<div class="move-row" style="--c:${mp.c}"><span>${esc(mo.n)}<br><span class="small muted">${mp.name}${mo.s?' · '+STATUS_NAME[mo.s]:''}</span></span><span class="small" style="text-align:right">Pow ${mo.p}<br>Acc ${mo.a}% · Lv ${mo.u}</span></div>`; }).join('')}
  `;
}

/* ---------- 16. CAMP / PARTY / SHOP / PACK ---------- */
function openCamp(){
  campSel = null;
  showScreen('camp');
  renderCamp();
}
function setCampTab(t){ campTab = t; campSel = null; renderCamp(); }
function renderCamp(){
  document.querySelectorAll('.camp-tab').forEach(b => {
    const on = b.dataset.tab === campTab;
    b.style.background = on ? 'rgba(139,92,246,.45)' : '';
    b.style.borderColor = on ? '#c4b5fd' : '';
  });
  $('camp-shards').textContent = state.shards;
  $('camp-sub').textContent = `Party ${state.party.length}/3 · Box ${state.box.length} · Zone ${state.zone+1}`;
  const c = $('camp-content');
  if (campTab === 'party') renderCampParty(c);
  else if (campTab === 'shop') renderCampShop(c);
  else renderCampPack(c);
}
function renderCampParty(c){
  const all = state.party.map((m, i) => ({ m, where:'party', i })).concat(state.box.map((m, i) => ({ m, where:'box', i })));
  c.innerHTML = `<div class="small muted" style="margin-bottom:10px">Select a Resonant, then select another to swap positions. Party members battle; the box stores the rest. Click Heal All for a free restoration at camp.</div>
    <div style="display:flex;gap:10px;margin-bottom:14px;flex-wrap:wrap"><button class="btn small-btn" id="heal-all">Heal All (free)</button></div>
    <h3 class="small muted" style="letter-spacing:2px;margin:6px 0">PARTY</h3>
    <div class="grid" id="party-grid" style="grid-template-columns:repeat(auto-fill,minmax(210px,1fr))"></div>
    <h3 class="small muted" style="letter-spacing:2px;margin:18px 0 6px">BOX (${state.box.length})</h3>
    <div class="grid" id="box-grid" style="grid-template-columns:repeat(auto-fill,minmax(210px,1fr))"></div>`;
  const pg = $('party-grid'), bg = $('box-grid');
  $('heal-all').onclick = () => { state.party.forEach(m => { m.hp = maxHP(m); m.st = null; }); SFX.heal(); toast('Party restored!', '#86efac'); saveGame(); renderCamp(); };
  state.party.forEach((m, i) => pg.appendChild(campCard(m, 'party', i)));
  for (let i = state.party.length; i < 3; i++){
    const d = document.createElement('div'); d.className = 'glass camp-slot'; d.style.opacity = .5;
    d.innerHTML = `<div class="muted" style="margin-top:50px">Empty slot</div>`;
    pg.appendChild(d);
  }
  if (!state.box.length) bg.innerHTML = '<div class="muted small">The box is empty.</div>';
  state.box.forEach((m, i) => bg.appendChild(campCard(m, 'box', i)));
}
function campCard(m, where, i){
  const pal = PAL[SP[m.sp].type];
  const max = maxHP(m);
  const pct = m.hp / max * 100;
  const el = document.createElement('div');
  el.className = 'glass camp-slot' + (campSel && campSel.where === where && campSel.i === i ? ' sel' : '');
  el.classList.add('neon');
  el.style.setProperty('--c', pal.c);
  el.innerHTML = `<div class="csp">${spriteSVG(m.sp, false)}</div>
    <div style="font-weight:800">${esc(monName(m))} <span class="small muted">Lv ${m.lv}</span></div>
    <div style="width:100%">${barHTML(pct, hpColor(pct))}</div>
    <div class="hpnum">${m.hp}/${max} HP${m.st?` · <span style="color:${STATUS_COLOR[m.st]}">${STATUS_NAME[m.st]}</span>`:''}</div>
    <div class="small muted">${where==='party'?'Party':'Box'} · ${pal.name}</div>`;
  el.onclick = () => onCampCardClick(where, i);
  el.onmouseenter = () => SFX.hover();
  return el;
}
function onCampCardClick(where, i){
  SFX.select();
  if (!campSel){ campSel = { where, i }; renderCamp(); return; }
  const a = campSel;
  campSel = null;
  if (a.where === where && a.i === i){ renderCamp(); return; }
  const arrA = a.where === 'party' ? state.party : state.box;
  const arrB = where === 'party' ? state.party : state.box;
  if (a.where === where){
    const t = arrA[a.i]; arrA[a.i] = arrA[i]; arrA[i] = t;
  } else {
    const A = arrA[a.i];
    const Bm = arrB[i];
    if (Bm){ arrA[a.i] = Bm; arrB[i] = A; }
    else { arrA.splice(a.i, 1); arrB.push(A); }
  }
  if (state.party.length === 0 && state.box.length){ state.party.push(state.box.shift()); }
  saveGame(); renderCamp();
}
function renderCampShop(c){
  c.innerHTML = `<div class="small muted" style="margin-bottom:14px">Spend Aether Shards on prisms and restoratives. Found items are kept in your pack.</div><div id="shop-list"></div>`;
  const list = $('shop-list');
  SHOP_ITEMS.forEach(k => {
    const it = ITEMS[k];
    const row = document.createElement('div');
    row.className = 'glass shop-row';
    row.innerHTML = `<div class="ico" style="color:${k.includes('prism') || k==='harmonizer' || k==='hyper' ? '#67e8f9' : (k==='core'?'#86efac':'#c4b5fd')}">${iconSVG(it.icon)}</div>
      <div class="sb"><div class="sn">${esc(it.name)}</div><div class="sd">${esc(it.desc)}</div><div class="sd" style="color:#e9d5ff">Owned: ${state.inv[k]}</div></div>
      <div style="text-align:right"><div style="font-weight:800;color:#fde68a;margin-bottom:6px">✦ ${it.price}</div></div>`;
    const btn = document.createElement('button');
    btn.className = 'btn small-btn primary';
    btn.textContent = 'Buy';
    btn.disabled = state.shards < it.price;
    btn.onclick = () => {
      if (state.shards < it.price){ SFX.fail(); return; }
      state.shards -= it.price; state.inv[k]++; SFX.select(); saveGame();
      toast(`Bought ${it.name}!`, '#86efac'); renderCamp();
    };
    row.querySelector('.sb').parentNode.lastElementChild.appendChild(btn);
    list.appendChild(row);
  });
}
function renderCampPack(c){
  c.innerHTML = `<div class="small muted" style="margin-bottom:14px">Items for use in battle and in the field.</div><div id="pack-list"></div>`;
  const list = $('pack-list');
  ITEM_ORDER.forEach(k => {
    const it = ITEMS[k];
    const row = document.createElement('div');
    row.className = 'glass shop-row';
    row.innerHTML = `<div class="ico" style="color:#c4b5fd">${iconSVG(it.icon)}</div>
      <div class="sb"><div class="sn">${esc(it.name)} <span class="muted">×${state.inv[k]}</span></div><div class="sd">${esc(it.desc)}</div></div>`;
    if (it.kind === 'heal' || it.kind === 'heal40' || it.kind === 'cure' || k === 'vitals'){
      const btn = document.createElement('button');
      btn.className = 'btn small-btn';
      btn.textContent = 'Use on Party Lead';
      btn.disabled = state.inv[k] <= 0;
      btn.onclick = () => useFieldItem(k);
      row.appendChild(btn);
    }
    list.appendChild(row);
  });
}
function useFieldItem(k){
  const p = state.party.find(m => m.hp > 0) || state.party[0];
  if (k === 'vitals'){ p.hp = Math.min(maxHP(p), p.hp + 40); }
  else if (k === 'core'){ p.hp = Math.min(maxHP(p), p.hp + Math.floor(maxHP(p)/2)); }
  else if (k === 'elixir' || k === 'cleanser'){ p.st = null; }
  state.inv[k]--;
  SFX.heal(); saveGame(); toast(`${monName(p)} was restored.`, '#86efac'); renderCamp();
}

/* ---------- 17. MENU, NAVIGATION & BOOT ---------- */
async function beginNewJourney(){
  const choice = await dialog('Choose Your First Resonant',
    'Every Conductor begins with a single companion. Your choice shapes your first battles.',
    [
      {label:'Emberkit · Pyro', value:'emberkit', cls:'danger'},
      {label:'Droplet · Hydro', value:'droplet', cls:'primary'},
      {label:'Seedling · Flora', value:'seedling', cls:''}
    ]);
  newGame(choice);
  state.shards = 60;
  state.inv.harmonizer = 5;
  state.inv.core = 2;
  saveGame();
  SFX.select();
  showScreen('world');
  renderWorld();
  toast('Your journey begins in the Verdant Canopy.', '#bbf7d0');
  dialog('Welcome, Conductor', 'Click the glowing nodes on the trail to explore. Tall Grass hides wild Resonants. Conductors challenge you to duels. Defeat the Gatekeeper to open the next zone.', [{label:'Let us go'}]);
}
function continueJourney(){
  const s = loadGame();
  if (!s){ toast('No journey to continue.', '#fca5a5'); return; }
  state = s;
  showScreen('world');
  renderWorld();
}
async function resetProgress(){
  const r = await dialog('Reset Progress?', 'This erases your party, box, pack, shards, zones and Dex. This cannot be undone.', [
    {label:'Erase Everything', cls:'danger', value:1},
    {label:'Cancel', value:0}
  ]);
  if (r !== 1) return;
  localStorage.removeItem(SAVE_KEY);
  state = null;
  updateMenuInfo();
  toast('Progress reset. A new journey awaits.', '#fca5a5');
}
function goMainMenu(){
  if (state) saveGame();
  showScreen('menu');
  updateMenuInfo();
}

function bindUI(){
  $('btn-continue').onclick = () => { SFX.select(); continueJourney(); };
  $('btn-new').onclick = () => { SFX.select(); if (hasSave()){ dialog('Overwrite Journey?', 'Starting a new journey will replace your saved progress.', [{label:'Begin Anew', cls:'danger', value:1},{label:'Cancel', value:0}]).then(v => { if (v === 1){ localStorage.removeItem(SAVE_KEY); beginNewJourney(); } }); } else beginNewJourney(); };
  $('btn-dex-menu').onclick = () => { SFX.select(); if (!state) state = defaultState(); openDex('menu'); };
  $('btn-reset').onclick = () => { SFX.select(); resetProgress(); };
  $('w-menu').onclick = () => { SFX.select(); goMainMenu(); };
  $('w-menu2').onclick = () => { SFX.select(); goMainMenu(); };
  $('w-camp').onclick = () => { SFX.select(); openCamp(); };
  $('w-dex').onclick = () => { SFX.select(); openDex('world'); };
  $('dex-back').onclick = () => { SFX.select(); if (dexReturn === 'menu'){ showScreen('menu'); updateMenuInfo(); } else { showScreen(state ? 'world' : 'menu'); if (state) renderWorld(); } };
  $('camp-back').onclick = () => { SFX.select(); showScreen('world'); renderWorld(); };
  document.querySelectorAll('.camp-tab').forEach(b => b.onclick = () => { SFX.select(); setCampTab(b.dataset.tab); });
  $('mute-btn').onclick = () => {
    SFX.init();
    SFX.enabled = !SFX.enabled;
    $('mute-btn').textContent = SFX.enabled ? '♪ On' : '♪ Off';
    if (SFX.enabled) SFX.select();
  };
  document.addEventListener('mouseover', e => {
    const b = e.target.closest && e.target.closest('.btn');
    if (b && !b.disabled && b !== window._lastHov){ window._lastHov = b; SFX.hover(); }
    if (!b) window._lastHov = null;
  });
  document.addEventListener('pointerdown', () => SFX.init(), { once:false, passive:true });
}

function boot(){
  bindUI();
  updateMenuInfo();
  if (hasSave()){
    /* keep menu active; user chooses Continue */
  }
  $('o-title').textContent = '';
}
document.addEventListener('DOMContentLoaded', boot);
if (document.readyState !== 'loading') boot();
</script>
</body>
</html>
<!-- agent-meta {"model":"anthropic/claude-haiku-5.5","provider":"openrouter","persona":"full","sessionId":"10380154-810c-46ed-9664-9b0dd2147719","tokensIn":990639,"tokensOut":227146,"tokensTotal":1217785,"cost":0.15913627000000002,"turns":17,"toolCalls":19,"failedToolCalls":0,"timestamp":"2026-10-08T06:58:27.645Z"} -->