← Creature Collection results

Creature Collection

Minimax m3 · full

Initial view of Creature Collection

Browser check: Artifact runtime error. This is a technical check, not a quality review.

Reviews & generation details

Reviews

Human review · Jul 17, 2026 · rubric 1

Historical path-based review. Association with these exact bytes is unverified.

Visual design
4 / 5
UX / feel
3 / 5
Works
2 / 5
Prompt spirit
4 / 5
Delight
2 / 5

Generation context

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

Generation prompt · exact text unavailable

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

Inspect original source 145,068 bytes · SHA-256 974465c76fcb
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" />
<title>Aethelgard: Veilbound</title>
<style>
:root{
  --cinder:#ff7a45; --cinder-glow:#ffb88a;
  --tide:#4cc9f0;  --tide-glow:#9be8ff;
  --verdant:#7ee07a; --verdant-glow:#c5f0a5;
  --volt:#f5e042; --volt-glow:#fff5a8;
  --terra:#c08a5a; --terra-glow:#e8c79a;
  --lumen:#fff5d6; --lumen-glow:#fff8e7;
  --umbral:#7c5cff; --umbral-glow:#b6a4ff;
  --ink:#0c0f1a; --ink-2:#161b2c; --ink-3:#202845;
  --glass:rgba(20,24,40,0.55);
  --glass-2:rgba(255,255,255,0.08);
  --line:rgba(255,255,255,0.14);
  --txt:#eef2ff; --txt-dim:#a3acd1;
  --good:#7ee07a; --bad:#ff6b6b; --warn:#f5c542;
  --shadow:0 6px 24px rgba(0,0,0,0.4);
}
*{box-sizing:border-box;margin:0;padding:0;-webkit-tap-highlight-color:transparent}
html,body{width:100%;height:100%;overflow:hidden;background:#07091a;color:var(--txt);font-family:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;letter-spacing:.02em;user-select:none}
body{position:relative}
#stage{position:fixed;inset:0;display:flex;align-items:center;justify-content:center;background:radial-gradient(1200px 800px at 50% 30%, #1a2247 0%, #0a0d1f 60%, #050714 100%)}
#game{position:absolute;inset:0;width:100%;height:100%;display:block;image-rendering:pixelated;cursor:default}
#ui{position:absolute;inset:0;pointer-events:none}
#ui > *{pointer-events:auto}
.hud{position:absolute;left:12px;top:10px;display:flex;gap:8px;align-items:center;z-index:10}
.chip{background:var(--glass);backdrop-filter:blur(10px) saturate(140%);-webkit-backdrop-filter:blur(10px) saturate(140%);border:1px solid var(--line);border-radius:14px;padding:6px 12px;font-size:12px;color:var(--txt);box-shadow:var(--shadow);display:inline-flex;align-items:center;gap:8px}
.chip b{color:var(--lumen-glow);font-weight:600}
.chip .dot{width:8px;height:8px;border-radius:50%;background:var(--lumen);box-shadow:0 0 10px var(--lumen-glow)}
.pill{border-radius:999px}
.btn{cursor:pointer;background:linear-gradient(180deg, rgba(255,255,255,0.10), rgba(255,255,255,0.03));border:1px solid var(--line);color:var(--txt);font-size:14px;padding:10px 16px;border-radius:12px;transition:transform .12s ease, background .12s ease, box-shadow .12s ease;backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px)}
.btn:hover{background:linear-gradient(180deg, rgba(255,255,255,0.18), rgba(255,255,255,0.05));transform:translateY(-1px);box-shadow:0 6px 18px rgba(0,0,0,0.35)}
.btn:active{transform:translateY(0)}
.btn.primary{background:linear-gradient(180deg, #ffd97a, #f5a142);color:#1a1407;border-color:#ffd97a;box-shadow:0 8px 22px rgba(245,180,80,0.4);font-weight:700}
.btn.primary:hover{background:linear-gradient(180deg, #ffe39a, #ffb65a)}
.btn.ghost{background:transparent;border-color:rgba(255,255,255,0.2);color:var(--txt-dim)}
.btn.danger{background:linear-gradient(180deg, #ff8a8a, #d34a4a);color:#1a0606;border-color:#ff8a8a}
.btn.tiny{padding:4px 8px;font-size:11px;border-radius:8px}
.btn-row{display:flex;gap:8px;flex-wrap:wrap}
.row{display:flex;gap:10px;align-items:center}
.col{display:flex;flex-direction:column;gap:10px}
.gap-4{gap:4px}.gap-8{gap:8px}.gap-12{gap:12px}.gap-16{gap:16px}
.center{display:flex;align-items:center;justify-content:center}
.grow{flex:1}
.scroll{overflow:auto;scrollbar-width:thin}
.scroll::-webkit-scrollbar{width:8px;height:8px}
.scroll::-webkit-scrollbar-thumb{background:rgba(255,255,255,0.15);border-radius:4px}
.modal-back{position:absolute;inset:0;background:radial-gradient(800px 500px at 50% 50%, rgba(40,30,80,0.5), rgba(0,0,0,0.7));backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);display:flex;align-items:center;justify-content:center;z-index:50;animation:fade .2s ease}
@keyframes fade{from{opacity:0}to{opacity:1}}
@keyframes pop{from{transform:scale(.92);opacity:0}to{transform:scale(1);opacity:1}}
@keyframes rise{from{transform:translateY(8px);opacity:0}to{transform:translateY(0);opacity:1}}
.modal{background:linear-gradient(180deg, rgba(40,50,90,0.7), rgba(20,24,40,0.85));border:1px solid var(--line);border-radius:18px;box-shadow:0 24px 60px rgba(0,0,0,0.6), inset 0 1px 0 rgba(255,255,255,0.07);padding:22px;max-width:min(900px,92vw);max-height:min(82vh,720px);width:auto;animation:pop .22s cubic-bezier(.2,.9,.3,1.3);overflow:hidden;display:flex;flex-direction:column}
.modal h2{font-size:18px;letter-spacing:.06em;text-transform:uppercase;color:var(--lumen-glow);margin-bottom:4px}
.modal .sub{color:var(--txt-dim);font-size:12px;margin-bottom:14px}
.modal .body{display:flex;flex-direction:column;gap:10px;min-height:0;flex:1}
.modal .foot{display:flex;gap:8px;justify-content:flex-end;margin-top:12px}
.tabs{display:flex;gap:4px;background:rgba(0,0,0,0.25);border:1px solid var(--line);border-radius:10px;padding:4px;margin-bottom:10px}
.tab{padding:6px 12px;border-radius:8px;font-size:12px;color:var(--txt-dim);cursor:pointer;letter-spacing:.04em;text-transform:uppercase}
.tab.active{background:linear-gradient(180deg, rgba(255,255,255,0.12), rgba(255,255,255,0.04));color:var(--lumen-glow)}
.title-screen{position:absolute;inset:0;z-index:80;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;background:radial-gradient(800px 600px at 50% 40%, #2a3260 0%, #0c0f1f 60%, #050714 100%);animation:fade .4s ease}
.title-screen h1{font-size:clamp(36px, 6vw, 64px);font-weight:300;letter-spacing:.12em;color:var(--lumen-glow);text-shadow:0 0 30px rgba(255,245,200,0.5), 0 0 60px rgba(255,245,200,0.2);margin-bottom:6px}
.title-screen h1 .accent{color:#ffd97a;font-weight:600}
.title-screen .tag{color:var(--txt-dim);font-size:13px;letter-spacing:.18em;text-transform:uppercase;margin-bottom:34px}
.title-screen .stack{display:flex;flex-direction:column;gap:10px;min-width:240px}
.title-screen .lore{position:absolute;bottom:24px;left:0;right:0;text-align:center;color:var(--txt-dim);font-size:11px;letter-spacing:.2em;text-transform:uppercase;opacity:.6}
.title-screen .stars{position:absolute;inset:0;overflow:hidden;pointer-events:none}
.star{position:absolute;width:2px;height:2px;background:#fff;border-radius:50%;opacity:.6;animation:twinkle 3s infinite ease-in-out}
@keyframes twinkle{0%,100%{opacity:.2;transform:scale(.8)}50%{opacity:1;transform:scale(1.4)}}
.dialogue{position:absolute;left:50%;bottom:20px;transform:translateX(-50%);max-width:min(720px,92vw);background:linear-gradient(180deg, rgba(20,24,40,0.85), rgba(10,14,30,0.9));border:1px solid var(--line);border-radius:14px;padding:14px 18px;backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);z-index:20;animation:rise .25s ease;box-shadow:var(--shadow)}
.dialogue .who{font-size:11px;letter-spacing:.18em;text-transform:uppercase;color:var(--lumen-glow);margin-bottom:6px}
.dialogue .what{font-size:14px;line-height:1.5}
.dialogue .next{position:absolute;right:14px;bottom:10px;color:var(--txt-dim);font-size:11px;letter-spacing:.16em}
.touch-pad{position:absolute;left:14px;bottom:14px;display:grid;grid-template-columns:repeat(3,44px);grid-template-rows:repeat(3,44px);gap:4px;z-index:15}
.touch-pad .tb{background:rgba(20,24,40,0.6);border:1px solid var(--line);border-radius:10px;display:flex;align-items:center;justify-content:center;color:var(--txt);font-size:18px;cursor:pointer;user-select:none;backdrop-filter:blur(4px)}
.touch-pad .tb:active{background:rgba(255,255,255,0.15)}
.touch-actions{position:absolute;right:14px;bottom:14px;display:flex;flex-direction:column;gap:6px;z-index:15}
.touch-actions .tb{width:64px;height:44px;background:rgba(20,24,40,0.6);border:1px solid var(--line);border-radius:10px;color:var(--txt);font-size:12px;font-weight:600;display:flex;align-items:center;justify-content:center;cursor:pointer;backdrop-filter:blur(4px)}
.touch-actions .tb:active{background:rgba(255,255,255,0.15)}
.toast{position:absolute;left:50%;top:30%;transform:translateX(-50%);background:rgba(20,24,40,0.9);border:1px solid var(--line);border-radius:12px;padding:10px 16px;font-size:13px;color:var(--lumen-glow);letter-spacing:.06em;text-transform:uppercase;z-index:60;animation:rise .25s ease, fadeout .8s ease 1.4s forwards;box-shadow:var(--shadow)}
@keyframes fadeout{to{opacity:0;transform:translate(-50%,-20px)}}
.flash{position:absolute;inset:0;pointer-events:none;z-index:30;opacity:0}
.flash.crit{background:radial-gradient(circle at 50% 50%, rgba(255,255,255,0.7), transparent 60%);animation:flashCrit .35s ease}
.flash.hit{background:radial-gradient(circle at 50% 50%, rgba(255,120,120,0.3), transparent 60%);animation:flashHit .25s ease}
.flash.heal{background:radial-gradient(circle at 50% 50%, rgba(120,255,160,0.3), transparent 60%);animation:flashHeal .35s ease}
@keyframes flashCrit{0%{opacity:0}20%{opacity:1}100%{opacity:0}}
@keyframes flashHit{0%{opacity:0}30%{opacity:1}100%{opacity:0}}
@keyframes flashHeal{0%{opacity:0}30%{opacity:1}100%{opacity:0}}
.shake{animation:shake .3s ease}
@keyframes shake{0%,100%{transform:translate(0,0)}20%{transform:translate(-4px,2px)}40%{transform:translate(4px,-2px)}60%{transform:translate(-3px,-2px)}80%{transform:translate(3px,1px)}}
.vignette{position:absolute;inset:0;pointer-events:none;z-index:5;mix-blend-mode:multiply;opacity:.65}
.bar{height:8px;background:rgba(0,0,0,0.45);border:1px solid var(--line);border-radius:6px;overflow:hidden;position:relative}
.bar > i{display:block;height:100%;background:linear-gradient(90deg, #7ee07a, #b6f0a0);transition:width .4s cubic-bezier(.2,.7,.2,1)}
.bar.warn > i{background:linear-gradient(90deg, #f5c542, #ffe28a)}
.bar.bad > i{background:linear-gradient(90deg, #ff6b6b, #ff9c9c)}
.bar.lumen > i{background:linear-gradient(90deg, #ffd97a, #fff5d6)}
.bar.energy > i{background:linear-gradient(90deg, #6cc6ff, #b6e8ff)}
.bar.exp > i{background:linear-gradient(90deg, #c9a4ff, #ecd9ff)}
.small{font-size:11px;color:var(--txt-dim)}
.swatch{display:inline-block;width:10px;height:10px;border-radius:50%;vertical-align:middle;margin-right:4px;box-shadow:0 0 8px currentColor}
.battle{position:absolute;inset:0;display:flex;flex-direction:column;z-index:40;background:linear-gradient(180deg, rgba(8,10,30,0.4), rgba(0,0,0,0.6))}
.battle-stage{flex:1;position:relative;overflow:hidden}
.battle-ui{display:flex;align-items:flex-end;gap:10px;padding:12px;background:linear-gradient(180deg, transparent, rgba(0,0,0,0.6))}
.enemy-card,.player-card{background:var(--glass);backdrop-filter:blur(10px) saturate(140%);-webkit-backdrop-filter:blur(10px) saturate(140%);border:1px solid var(--line);border-radius:14px;padding:10px 14px;box-shadow:var(--shadow);min-width:200px}
.enemy-card{align-self:flex-start;margin-left:24px}
.player-card{align-self:flex-end;margin-right:24px}
.battle-actions{position:absolute;left:0;right:0;bottom:0;padding:14px;background:linear-gradient(180deg, transparent, rgba(0,0,0,0.75));display:flex;flex-direction:column;gap:10px;align-items:center;z-index:45}
.battle-actions .menu{display:grid;grid-template-columns:repeat(2, minmax(120px, 220px));gap:8px;width:100%;max-width:520px}
.target-pick{position:absolute;inset:0;z-index:46;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,0.5);backdrop-filter:blur(2px)}
.text-bubble{position:absolute;left:50%;top:14px;transform:translateX(-50%);background:rgba(20,24,40,0.85);border:1px solid var(--line);border-radius:10px;padding:8px 14px;font-size:13px;color:var(--lumen-glow);z-index:48;animation:rise .2s ease;max-width:80%;text-align:center}
.icon-crit{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);font-size:48px;font-weight:700;color:#ffd97a;text-shadow:0 0 30px rgba(255,180,40,0.8);z-index:49;pointer-events:none;animation:critpop .6s ease forwards}
@keyframes critpop{0%{transform:translate(-50%,-50%) scale(.3);opacity:0}30%{transform:translate(-50%,-50%) scale(1.4);opacity:1}100%{transform:translate(-50%,-50%) scale(1);opacity:0}}
.battle-sprite{position:absolute;image-rendering:pixelated;will-change:transform}
.cryst{position:absolute;width:16px;height:16px;background:radial-gradient(circle at 30% 30%, #fff, rgba(255,255,255,0) 60%);border-radius:50%;pointer-events:none;mix-blend-mode:screen}
.glyph{width:22px;height:22px;border-radius:6px;display:inline-block;vertical-align:middle;background:rgba(0,0,0,0.4);border:1px solid var(--line);position:relative;overflow:hidden}
.glyph::after{content:"";position:absolute;inset:2px;border-radius:4px;background:currentColor;opacity:.85;filter:blur(0px)}
.mini-map{position:relative;width:100%;aspect-ratio:1/1;max-width:340px;background:rgba(0,0,0,0.4);border:1px solid var(--line);border-radius:14px;overflow:hidden;margin:0 auto}
.mini-map canvas{width:100%;height:100%;display:block}
.creature-list{display:grid;grid-template-columns:repeat(auto-fill, minmax(150px, 1fr));gap:10px}
.creature-tile{background:linear-gradient(180deg, rgba(255,255,255,0.06), rgba(255,255,255,0.02));border:1px solid var(--line);border-radius:12px;padding:10px;display:flex;flex-direction:column;gap:6px;transition:transform .15s ease}
.creature-tile:hover{transform:translateY(-2px)}
.creature-tile .top{display:flex;justify-content:space-between;align-items:center;gap:8px}
.creature-tile .name{font-weight:600;font-size:13px}
.creature-tile .lvl{font-size:11px;color:var(--txt-dim)}
.creature-tile .lore{font-size:11px;color:var(--txt-dim);line-height:1.3;min-height:30px}
.creature-tile .stats{display:grid;grid-template-columns:repeat(3,1fr);gap:4px;font-size:10px;color:var(--txt-dim)}
.creature-tile .stat b{color:var(--txt);font-weight:600}
.mini-sprite{width:42px;height:42px;background:rgba(0,0,0,0.4);border:1px solid var(--line);border-radius:10px;position:relative;overflow:hidden;flex-shrink:0}
.tooltip{position:absolute;background:rgba(10,14,30,0.95);border:1px solid var(--line);border-radius:8px;padding:6px 10px;font-size:11px;color:var(--txt);z-index:70;pointer-events:none;box-shadow:var(--shadow);white-space:nowrap}
.move-list{display:grid;grid-template-columns:repeat(auto-fill, minmax(170px, 1fr));gap:8px}
.move-tile{background:linear-gradient(180deg, rgba(255,255,255,0.08), rgba(255,255,255,0.02));border:1px solid var(--line);border-radius:10px;padding:8px 10px;cursor:pointer;transition:transform .12s, background .12s;position:relative}
.move-tile:hover{background:linear-gradient(180deg, rgba(255,255,255,0.16), rgba(255,255,255,0.04));transform:translateY(-1px)}
.move-tile.disabled{opacity:0.5;cursor:not-allowed}
.move-tile .nm{font-weight:600;font-size:13px;display:flex;justify-content:space-between;align-items:center;gap:8px}
.move-tile .pp{font-size:10px;color:var(--txt-dim);background:rgba(0,0,0,0.3);padding:2px 6px;border-radius:6px}
.move-tile .meta{font-size:10px;color:var(--txt-dim);margin-top:4px;display:flex;gap:8px;flex-wrap:wrap}
.tag{font-size:10px;padding:2px 6px;border-radius:6px;background:rgba(0,0,0,0.35);border:1px solid var(--line);color:var(--txt-dim);letter-spacing:.04em;text-transform:uppercase}
.tag.type-cinder{color:var(--cinder-glow);border-color:rgba(255,180,140,0.4)}
.tag.type-tide{color:var(--tide-glow);border-color:rgba(180,230,255,0.4)}
.tag.type-verdant{color:var(--verdant-glow);border-color:rgba(180,240,180,0.4)}
.tag.type-volt{color:var(--volt-glow);border-color:rgba(255,240,160,0.4)}
.tag.type-terra{color:var(--terra-glow);border-color:rgba(230,200,160,0.4)}
.tag.type-lumen{color:#fff5d6;border-color:rgba(255,245,200,0.5)}
.tag.type-umbral{color:var(--umbral-glow);border-color:rgba(180,160,255,0.4)}
.tag.status{color:var(--warn);border-color:rgba(245,200,80,0.4)}
.tag.buff{color:var(--good);border-color:rgba(120,230,120,0.4)}
.tag.debuff{color:var(--bad);border-color:rgba(255,140,140,0.4)}
.tag.priority{border-color:rgba(255,200,100,0.4);color:#ffd97a}
.capture-zone{position:absolute;inset:0;background:radial-gradient(500px 400px at 50% 50%, rgba(255,200,80,0.2), rgba(0,0,0,0.7));backdrop-filter:blur(3px);display:flex;align-items:center;justify-content:center;flex-direction:column;gap:14px;z-index:50;animation:fade .2s ease}
.capture-ring{width:240px;height:240px;border-radius:50%;border:4px solid rgba(255,255,255,0.2);position:relative;display:flex;align-items:center;justify-content:center}
.capture-ring::before{content:"";position:absolute;inset:0;border-radius:50%;border:4px solid transparent;border-top-color:#ffd97a;border-right-color:#fff5d6;animation:spin 1.2s linear infinite}
.capture-ring::after{content:"";position:absolute;inset:14px;border-radius:50%;border:3px solid rgba(255,255,255,0.18);border-bottom-color:#7ee07a;animation:spin 1.6s linear infinite reverse}
@keyframes spin{to{transform:rotate(360deg)}}
.capture-target{position:absolute;width:60px;height:60px;border-radius:50%;background:radial-gradient(circle at 50% 50%, rgba(255,200,80,0.9), rgba(255,200,80,0.2) 60%, transparent);box-shadow:0 0 30px rgba(255,200,80,0.7);animation:bob 1.2s ease-in-out infinite}
@keyframes bob{0%,100%{transform:translateY(-4px)}50%{transform:translateY(4px)}}
.capture-meter{width:260px;height:18px;background:rgba(0,0,0,0.55);border:1px solid var(--line);border-radius:10px;overflow:hidden;position:relative}
.capture-meter .fill{position:absolute;left:0;top:0;bottom:0;width:0;background:linear-gradient(90deg, #ffd97a, #fff5d6);transition:width .15s ease;box-shadow:inset 0 0 12px rgba(255,255,255,0.3)}
.capture-meter .marker{position:absolute;top:-4px;bottom:-4px;width:3px;background:#fff;box-shadow:0 0 10px #fff;border-radius:2px}
.flavor{position:absolute;left:14px;bottom:14px;font-size:11px;color:var(--txt-dim);letter-spacing:.1em;text-transform:uppercase;z-index:15;background:var(--glass);border:1px solid var(--line);border-radius:10px;padding:4px 10px;backdrop-filter:blur(6px)}
#pause-menu{position:absolute;right:14px;top:54px;display:flex;flex-direction:column;gap:6px;z-index:14}
#pause-menu .btn{padding:6px 12px;font-size:12px}
.divider{height:1px;background:linear-gradient(90deg, transparent, var(--line), transparent);margin:6px 0}
.icon-btn{width:34px;height:34px;background:var(--glass);border:1px solid var(--line);border-radius:10px;color:var(--txt);cursor:pointer;display:inline-flex;align-items:center;justify-content:center;font-size:14px;backdrop-filter:blur(6px)}
.icon-btn:hover{background:rgba(255,255,255,0.12)}
.story-vignette{position:absolute;inset:0;pointer-events:none;z-index:25;mix-blend-mode:multiply;opacity:.7;transition:opacity .6s ease}
.hidden{display:none !important}
@media (max-width:700px){
  .hud{font-size:10px}
  .modal{max-height:90vh;padding:16px}
  .creature-list{grid-template-columns:repeat(2,1fr)}
  .move-list{grid-template-columns:repeat(2,1fr)}
  .touch-pad,.touch-actions{display:grid}
  .battle-ui{flex-direction:column-reverse;align-items:stretch}
  .enemy-card,.player-card{margin:0;min-width:0}
}
</style>
</head>
<body>
<div id="stage">
  <canvas id="game" width="1280" height="720"></canvas>
  <div id="ui">
    <div id="hud" class="hud hidden">
      <div class="chip pill"><span class="dot"></span><span id="hud-zone">Cinder Glades</span></div>
      <div class="chip"><b id="hud-party">1</b>/6 party</div>
      <div class="chip"><b id="hud-coins">0</b> aether</div>
      <div class="chip"><b id="hud-time">—</b></div>
    </div>
    <div id="pause-menu" class="hidden">
      <button class="btn tiny" data-act="bag">Bag</button>
      <button class="btn tiny" data-act="team">Team</button>
      <button class="btn tiny" data-act="pedia">Codex</button>
      <button class="btn tiny" data-act="map">Map</button>
      <button class="btn tiny" data-act="save">Save</button>
      <button class="btn tiny" data-act="mute">Mute</button>
      <button class="btn tiny" data-act="quit">Title</button>
    </div>
    <div id="flavor" class="flavor hidden"></div>
    <div id="touch-pad" class="touch-pad hidden">
      <div class="tb" data-k="up">▲</div>
      <div class="tb" data-k="left">◀</div>
      <div class="tb" data-k="down">▼</div>
      <div class="tb" data-k="right">▶</div>
      <div class="tb" data-k="act">A</div>
      <div class="tb" data-k="bag">B</div>
      <div class="tb" data-k="cancel">×</div>
      <div class="tb" data-k="enter">↵</div>
      <div class="tb" data-k="pause">‖</div>
    </div>
    <div id="touch-actions" class="touch-actions hidden"></div>
  </div>
  <div id="title-screen" class="title-screen">
    <div class="stars" id="title-stars"></div>
    <h1>Aethelgard<span class="accent"> · </span>Veilbound</h1>
    <div class="tag">A small RPG about collecting and being chosen</div>
    <div class="stack">
      <button class="btn primary" id="btn-new">New Binding</button>
      <button class="btn" id="btn-cont">Continue</button>
    </div>
    <div class="lore">WASD / Arrows · Space · E · Esc</div>
  </div>
</div>

<script>
'use strict';
/* ===== CONSTANTS ===== */
const T = { CINDER:'Cinder', TIDE:'Tide', VERDANT:'Verdant', VOLT:'Volt', TERRA:'Terra', LUMEN:'Lumen', UMBRAL:'Umbral' };
const TYPE_COLORS = {
  Cinder:'#ff7a45', Tide:'#4cc9f0', Verdant:'#7ee07a', Volt:'#f5e042', Terra:'#c08a5a', Lumen:'#fff5d6', Umbral:'#7c5cff'
};
const TYPE_GLOW = {
  Cinder:'#ffb88a', Tide:'#9be8ff', Verdant:'#c5f0a5', Volt:'#fff5a8', Terra:'#e8c79a', Lumen:'#fff8e7', Umbral:'#b6a4ff'
};
const TYPE_CHART = {
  Cinder:  { Tide:2, Verdant:0.5, Volt:0.5, Terra:1, Lumen:0.5, Umbral:1 },
  Tide:    { Verdant:2, Cinder:0.5, Volt:1, Terra:0.5, Lumen:0.5, Umbral:1 },
  Verdant: { Cinder:2, Tide:0.5, Volt:1, Terra:0.5, Lumen:0.5, Umbral:1 },
  Volt:    { Cinder:2, Verdant:2, Terra:0.5, Tide:1, Lumen:0.5, Umbral:1 },
  Terra:   { Volt:2, Tide:2, Cinder:1, Verdant:1, Lumen:0.5, Umbral:1 },
  Lumen:   { Cinder:1.5, Tide:1.5, Verdant:1.5, Volt:1.5, Terra:1.5, Umbral:2 },
  Umbral:  { Cinder:0.5, Tide:0.5, Verdant:0.5, Volt:0.5, Terra:0.5, Lumen:2 }
};
const TYPE_LIST = ['Cinder','Tide','Verdant','Volt','Terra','Lumen','Umbral'];
const TILE = 40;
const MAX_PARTY = 6;
const MAX_LEVEL = 50;

/* ===== RNG ===== */
let _seed = (Date.now() & 0xffffffff) >>> 0;
function srand(s){ _seed = s>>>0; }
function rnd(){ // mulberry32
  _seed |= 0; _seed = (_seed + 0x6D2B79F5) | 0;
  let t = Math.imul(_seed ^ (_seed >>> 15), 1 | _seed);
  t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
  return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
}
function rint(a,b){ return Math.floor(rnd()*(b-a+1))+a; }
function pick(arr){ return arr[Math.floor(rnd()*arr.length)]; }
function chance(p){ return rnd() < p; }
function clamp(v,a,b){ return v<a?a:v>b?b:v; }
function lerp(a,b,t){ return a + (b-a)*t; }

/* ===== AUDIO ===== */
const Audio = (() => {
  let ctx = null, master = null, musicGain = null, sfxGain = null;
  let muted = false, musicNodes = [], musicTimer = 0, musicOn = false;
  function init(){
    if(ctx) return;
    try {
      ctx = new (window.AudioContext||window.webkitAudioContext)();
      master = ctx.createGain(); master.gain.value = 0.5; master.connect(ctx.destination);
      musicGain = ctx.createGain(); musicGain.gain.value = 0.18; musicGain.connect(master);
      sfxGain = ctx.createGain(); sfxGain.gain.value = 0.6; sfxGain.connect(master);
    } catch(e){ console.warn('Audio init failed',e); }
  }
  function ensure(){ if(ctx && ctx.state==='suspended') ctx.resume(); }
  function setMuted(m){ muted=m; if(musicGain) musicGain.gain.value = m?0:0.18; }
  function tone(freq, dur, type='sine', vol=0.3, slide=0){
    if(muted||!ctx) return;
    ensure();
    const t = ctx.currentTime;
    const o = ctx.createOscillator(); o.type = type; o.frequency.value = freq;
    if(slide) o.frequency.setValueAtTime(freq, t); o.frequency.linearRampToValueAtTime(freq+slide, t+dur);
    const g = ctx.createGain(); g.gain.value = 0; g.gain.linearRampToValueAtTime(vol, t+0.01); g.gain.exponentialRampToValueAtTime(0.0001, t+dur);
    o.connect(g); g.connect(sfxGain); o.start(t); o.stop(t+dur+0.02);
  }
  function noise(dur, vol=0.2, hp=400){
    if(muted||!ctx) return;
    ensure();
    const t = ctx.currentTime;
    const buf = ctx.createBuffer(1, Math.floor(ctx.sampleRate*dur), ctx.sampleRate);
    const d = buf.getChannelData(0);
    for(let i=0;i<d.length;i++) d[i] = (rnd()*2-1) * (1 - i/d.length);
    const src = ctx.createBufferSource(); src.buffer = buf;
    const g = ctx.createGain(); g.gain.value = vol;
    const f = ctx.createBiquadFilter(); f.type='highpass'; f.frequency.value = hp;
    src.connect(f); f.connect(g); g.connect(sfxGain);
    src.start(t);
  }
  const sfx = {
    select(){ tone(660,0.06,'square',0.18); tone(880,0.06,'square',0.18,200); },
    confirm(){ tone(523,0.08,'triangle',0.22); setTimeout(()=>tone(784,0.10,'triangle',0.22),60); },
    back(){ tone(440,0.08,'triangle',0.18,-80); },
    hit(){ noise(0.12,0.18,300); tone(180,0.18,'square',0.2,-80); },
    crit(){ tone(900,0.1,'square',0.25,300); setTimeout(()=>tone(1300,0.18,'square',0.25,-200),50); },
    heal(){ tone(523,0.1,'sine',0.18); setTimeout(()=>tone(659,0.12,'sine',0.18),80); setTimeout(()=>tone(784,0.18,'sine',0.18),160); },
    fire(){ noise(0.2,0.18,200); tone(420,0.18,'sawtooth',0.18,80); },
    water(){ tone(660,0.18,'sine',0.16,-200); },
    nature(){ tone(523,0.1,'triangle',0.16); setTimeout(()=>tone(659,0.12,'triangle',0.16),60); },
    volt(){ tone(1100,0.08,'square',0.2); setTimeout(()=>tone(880,0.06,'square',0.18),40); noise(0.08,0.12,800); },
    terra(){ tone(140,0.18,'square',0.22,40); },
    lumen(){ tone(880,0.18,'sine',0.16,300); setTimeout(()=>tone(1175,0.2,'sine',0.16),100); },
    umbral(){ tone(220,0.22,'sine',0.16,-60); setTimeout(()=>tone(165,0.22,'sine',0.16),80); },
    superEff(){ tone(660,0.08,'square',0.22); setTimeout(()=>tone(880,0.08,'square',0.22),60); setTimeout(()=>tone(1320,0.14,'square',0.22),120); },
    notEff(){ tone(330,0.12,'triangle',0.16,-40); },
    capture(){ tone(523,0.1,'sine',0.18); setTimeout(()=>tone(784,0.1,'sine',0.18),80); setTimeout(()=>tone(1046,0.2,'sine',0.18),160); },
    escape(){ tone(440,0.1,'sine',0.18,-80); },
    level(){ tone(523,0.08,'square',0.2); setTimeout(()=>tone(659,0.08,'square',0.2),70); setTimeout(()=>tone(784,0.08,'square',0.2),140); setTimeout(()=>tone(1046,0.2,'square',0.2),210); },
    shrine(){ tone(523,0.18,'sine',0.18); setTimeout(()=>tone(659,0.18,'sine',0.18),120); setTimeout(()=>tone(784,0.22,'sine',0.18),240); }
  };
  /* simple chiptune loop */
  const loops = {
    overworld: { tempo:120, lead:[
      [523,1],[659,1],[784,1],[1046,2],[784,1],[659,1],[523,2],
      [587,1],[698,1],[880,1],[1046,2],[880,1],[698,1],[587,2]
    ], bass:[
      [131,2],[165,2],[196,2],[165,2],[131,2],[165,2],[196,2],[165,2]
    ]},
    battle: { tempo:160, lead:[
      [392,0.5],[440,0.5],[494,0.5],[523,1],[392,0.5],[440,0.5],[494,0.5],[523,1],
      [466,0.5],[440,0.5],[392,0.5],[370,1],[466,0.5],[440,0.5],[392,0.5],[370,1]
    ], bass:[
      [98,1],[98,1],[98,1],[98,1],[98,1],[98,1],[98,1],[98,1]
    ]},
    shrine: { tempo:80, lead:[
      [392,2],[523,2],[587,2],[659,4],
      [523,2],[587,2],[659,2],[784,4]
    ], bass:[
      [131,4],[165,4],[196,4],[165,4]
    ]}
  };
  let curLoop = null, loopIdx = 0, loopStep = 0;
  function playLoop(name){
    if(musicOn && curLoop === name) return;
    curLoop = name; loopIdx = 0; loopStep = 0; musicOn = !!name;
    if(!name) return;
    if(muted) return;
    scheduleMusic();
  }
  function scheduleMusic(){
    if(!musicOn || !curLoop || !ctx) return;
    const L = loops[curLoop]; if(!L) return;
    const beat = 60 / L.tempo;
    // schedule a few beats ahead
    const lookahead = 0.3;
    while(musicTimer < ctx.currentTime + lookahead){
      const stepBeat = loopStep % L.lead.length;
      const l = L.lead[stepBeat];
      const dur = l[1] * beat;
      const o = ctx.createOscillator(); o.type='square'; o.frequency.value = l[0];
      const g = ctx.createGain(); g.gain.value = 0; g.gain.linearRampToValueAtTime(0.07, musicTimer+0.01); g.gain.linearRampToValueAtTime(0, musicTimer+dur-0.02);
      o.connect(g); g.connect(musicGain);
      o.start(musicTimer); o.stop(musicTimer+dur);
      // bass
      if(loopStep % 2 === 0 && L.bass){
        const b = L.bass[(loopStep/2) % L.bass.length];
        const bdur = b[1] * beat;
        const bo = ctx.createOscillator(); bo.type='triangle'; bo.frequency.value = b[0];
        const bg = ctx.createGain(); bg.gain.value = 0; bg.gain.linearRampToValueAtTime(0.10, musicTimer+0.01); bg.gain.linearRampToValueAtTime(0, musicTimer+bdur-0.05);
        bo.connect(bg); bg.connect(musicGain);
        bo.start(musicTimer); bo.stop(musicTimer+bdur);
      }
      musicTimer += dur;
      loopStep++;
    }
    setTimeout(scheduleMusic, 250);
  }
  function startMusic(name){ playLoop(name); }
  function stopMusic(){ curLoop = null; musicOn = false; }
  return { init, setMuted, sfx, startMusic, stopMusic, get muted(){return muted;} };
})();

/* ===== ABILITIES ===== */
/* power, accuracy, cost(PP), affinity, effect { kind, chance, ...params } */
const ABILITIES = {
  // Cinder
  emberFlick:   { name:'Ember Flick',   type:T.CINDER,   power:35, acc:95, pp:18, effect:{} },
  cinderJab:    { name:'Cinder Jab',    type:T.CINDER,   power:50, acc:90, pp:12, effect:{} },
  flameGlyph:   { name:'Flame Glyph',   type:T.CINDER,   power:55, acc:100, pp:10, effect:{kind:'burn',chance:25} },
  pyreRing:     { name:'Pyre Ring',     type:T.CINDER,   power:60, acc:85, pp:8, effect:{kind:'burn',chance:40} },
  phoenixDrift: { name:'Phoenix Drift', type:T.CINDER,   power:75, acc:90, pp:6, effect:{kind:'recoil',pct:25} },
  flareLash:    { name:'Flare Lash',    type:T.CINDER,   power:85, acc:85, pp:5, effect:{kind:'burn',chance:30} },
  // Tide
  rippleToss:   { name:'Ripple Toss',   type:T.TIDE,     power:35, acc:95, pp:18, effect:{} },
  brineLash:    { name:'Brine Lash',    type:T.TIDE,     power:50, acc:90, pp:12, effect:{} },
  tideSurge:    { name:'Tide Surge',    type:T.TIDE,     power:55, acc:100, pp:10, effect:{kind:'frost',chance:20} },
  crushingDepth:{ name:'Crushing Depth',type:T.TIDE,     power:70, acc:85, pp:8, effect:{kind:'frost',chance:35} },
  riptide:      { name:'Riptide',       type:T.TIDE,     power:60, acc:100, pp:8, effect:{kind:'multihit',hits:2} },
  maelstrom:    { name:'Maelstrom',     type:T.TIDE,     power:85, acc:80, pp:5, effect:{kind:'frost',chance:50} },
  // Verdant
  leafCut:      { name:'Leaf Cut',      type:T.VERDANT,  power:35, acc:95, pp:18, effect:{} },
  vineSnap:     { name:'Vine Snap',     type:T.VERDANT,  power:50, acc:90, pp:12, effect:{} },
  thornVolley:  { name:'Thorn Volley',  type:T.VERDANT,  power:55, acc:100, pp:10, effect:{kind:'multihit',hits:2} },
  rootHold:     { name:'Root Hold',     type:T.VERDANT,  power:50, acc:85, pp:8, effect:{kind:'speed',chance:100,delta:-2} },
  heartOfGrove: { name:'Heart of Grove',type:T.VERDANT,  power:0,  acc:100, pp:6, effect:{kind:'heal',pct:50} },
  briarStorm:   { name:'Briar Storm',   type:T.VERDANT,  power:85, acc:85, pp:5, effect:{kind:'speed',chance:30,delta:-1} },
  // Volt
  spark:        { name:'Spark',         type:T.VOLT,     power:35, acc:95, pp:18, effect:{} },
  joltingArc:   { name:'Jolting Arc',   type:T.VOLT,     power:50, acc:90, pp:12, effect:{kind:'shock',chance:20} },
  skybreak:     { name:'Skybreak',      type:T.VOLT,     power:55, acc:100, pp:10, effect:{kind:'shock',chance:30} },
  voltBurst:    { name:'Volt Burst',    type:T.VOLT,     power:60, acc:85, pp:8, effect:{kind:'shock',chance:40} },
  stormCrown:   { name:'Storm Crown',   type:T.VOLT,     power:75, acc:90, pp:6, effect:{kind:'priority'} },
  thunderPike:  { name:'Thunder Pike',  type:T.VOLT,     power:85, acc:85, pp:5, effect:{kind:'shock',chance:50} },
  // Terra
  stoneThrow:   { name:'Stone Throw',   type:T.TERRA,    power:35, acc:95, pp:18, effect:{} },
  dustKick:     { name:'Dust Kick',     type:T.TERRA,    power:50, acc:90, pp:12, effect:{} },
  earthenGuard: { name:'Earthen Guard', type:T.TERRA,    power:0,  acc:100, pp:10, effect:{kind:'barrier'} },
  crumble:      { name:'Crumble',       type:T.TERRA,    power:55, acc:100, pp:10, effect:{kind:'defDown',chance:30,delta:-1} },
  quakeStep:    { name:'Quake Step',    type:T.TERRA,    power:70, acc:85, pp:8, effect:{kind:'defDown',chance:50,delta:-1} },
  tectonicLash: { name:'Tectonic Lash', type:T.TERRA,    power:85, acc:80, pp:5, effect:{kind:'recoil',pct:25} },
  // Lumen
  gleamPulse:   { name:'Gleam Pulse',   type:T.LUMEN,    power:40, acc:100, pp:18, effect:{} },
  dawnspear:    { name:'Dawnspear',     type:T.LUMEN,    power:55, acc:95, pp:12, effect:{} },
  solace:       { name:'Solace',        type:T.LUMEN,    power:0,  acc:100, pp:8, effect:{kind:'heal',pct:40} },
  radiantVeil:  { name:'Radiant Veil',  type:T.LUMEN,    power:0,  acc:100, pp:8, effect:{kind:'regen',turns:4} },
  halo:         { name:'Halo',          type:T.LUMEN,    power:60, acc:100, pp:10, effect:{kind:'heal',pct:25} },
  solarCrown:   { name:'Solar Crown',   type:T.LUMEN,    power:85, acc:85, pp:5, effect:{kind:'heal',pct:25} },
  // Umbral
  shadowTap:    { name:'Shadow Tap',    type:T.UMBRAL,   power:35, acc:100, pp:18, effect:{} },
  gloomCut:     { name:'Gloom Cut',     type:T.UMBRAL,   power:50, acc:95, pp:12, effect:{} },
  voidstep:     { name:'Voidstep',      type:T.UMBRAL,   power:0,  acc:100, pp:8, effect:{kind:'priority'} },
  hexSiphon:    { name:'Hex Siphon',    type:T.UMBRAL,   power:55, acc:100, pp:10, effect:{kind:'lifesteal',pct:50} },
  eclipse:      { name:'Eclipse',       type:T.UMBRAL,   power:75, acc:90, pp:6, effect:{kind:'defDown',chance:30,delta:-1} },
  nightfall:    { name:'Nightfall',     type:T.UMBRAL,   power:85, acc:85, pp:5, effect:{kind:'speed',chance:30,delta:-2} },
  // Universal
  toughen:      { name:'Toughen',       type:null,        power:0,  acc:100, pp:12, effect:{kind:'defUp',chance:100,delta:1} },
  focusRush:    { name:'Focus Rush',    type:null,        power:0,  acc:100, pp:10, effect:{kind:'spdUp',chance:100,delta:1} },
  bulkUp:       { name:'Bulk Up',       type:null,        power:0,  acc:100, pp:8,  effect:{kind:'atkUp',chance:100,delta:1} },
  meditate:     { name:'Meditate',      type:null,        power:0,  acc:100, pp:6,  effect:{kind:'heal',pct:33} },
  broadside:    { name:'Broadside',     type:null,        power:55, acc:100, pp:10, effect:{kind:'multihit',hits:2} },
  pierceThrow:  { name:'Pierce Throw',  type:null,        power:55, acc:95,  pp:10, effect:{kind:'pierce'} }
};
const ABIL_LIST = Object.keys(ABILITIES);
const STATUS_EFFECT_KINDS = new Set(['burn','frost','shock','barrier','regen']);

/* ===== CREATURES ===== */
/* baseStats: [hp, atk, def, spd] */
/* type2 = optional secondary (very rare, only if needed) */
const CREATURES = {
  /* Starters (zone 1) */
  pyrlin:     { name:'Pyrlin',     type:T.CINDER,   base:[42,52,40,55], learnset:[['emberFlick',1],['cinderJab',5],['flameGlyph',10],['toughen',15],['pyreRing',22],['focusRush',28],['flareLash',36],['phoenixDrift',44]], lore:'A ember-sprite born where a fallen star touched the Cinder Glades. Warm to the touch, but shy of strangers.', transform:{into:'infernox', atLevel:32, catalyst:'cinderStone'} },
  aquave:     { name:'Aquave',     type:T.TIDE,     base:[44,48,46,52], learnset:[['rippleToss',1],['brineLash',5],['tideSurge',10],['toughen',15],['crushingDepth',22],['bulkUp',28],['riptide',36],['maelstrom',44]], lore:'A droplet of the First Sea given eyes and curiosity. It hums lullabies to tidepools at dusk.', transform:null },
  leafkin:    { name:'Leafkin',    type:T.VERDANT,  base:[46,50,48,50], learnset:[['leafCut',1],['vineSnap',5],['rootHold',10],['meditate',15],['thornVolley',22],['toughen',28],['heartOfGrove',36],['briarStorm',44]], lore:'A seedling that sprouted from a lullaby sung into loam. Its leaves chime when wind passes through.', transform:null },

  /* Zone 1: Cinder Glades */
  emberpup:   { name:'Emberpup',   type:T.CINDER,   base:[40,54,38,55], learnset:[['spark',1],['emberFlick',6],['cinderJab',12],['flameGlyph',20],['pyreRing',30],['focusRush',40]], lore:'Packs of Emberpups race through the Glades, leaving brief comet-trails in the dry grass.', transform:{into:'cindermaw', atLevel:34, catalyst:'cinderStone'} },

  /* Zone 2: Tideglass Coves */
  coralimp:   { name:'Coralimp',   type:T.TIDE,     base:[38,46,52,46], learnset:[['rippleToss',1],['brineLash',6],['tideSurge',12],['thornVolley',18],['crushingDepth',28],['toughen',38]], lore:'Built by patient tides, the Coralimp watches from reef-windows with a single violet eye.', transform:null },
  frostfin:   { name:'Frostfin',   type:T.TIDE,     base:[44,52,42,52], learnset:[['rippleToss',1],['brineLash',6],['tideSurge',12],['riptide',20],['crushingDepth',30],['frostbiteCharm',38]], lore:'Where cold currents meet warm shallows, Frostfin glides like a ribbon of silver dawn.', transform:null },

  /* Zone 3: Verdant Thicket */
  bramblin:   { name:'Bramblin',   type:T.VERDANT,  base:[48,50,52,42], learnset:[['leafCut',1],['vineSnap',6],['rootHold',12],['broadside',18],['briarStorm',28],['bulkUp',38]], lore:'Bramblin appear where old paths are forgotten. They remember every footstep, even yours.', transform:{into:'wildheart', atLevel:34, catalyst:'verdantMoss'} },
  vinewhip:   { name:'Vinewhip',   type:T.VERDANT,  type2:T.LUMEN,  base:[42,54,40,58], learnset:[['vineSnap',1],['leafCut',6],['gleamPulse',12],['focusRush',18],['briarStorm',30],['radiantVeil',40]], lore:'Coiled around moonlit trellises, the Vinewhip measures the worth of passing travelers.', transform:null },

  /* Zone 4: Volt Spires */
  sparkat:    { name:'Sparkat',    type:T.VOLT,     base:[36,48,38,64], learnset:[['spark',1],['joltingArc',6],['skybreak',12],['focusRush',18],['voltBurst',28],['thunderPike',40]], lore:'A cat with a crown of static. It naps atop spires, raining tiny sparks into clouds.', transform:{into:'stormfang', atLevel:32, catalyst:'voltCoil'} },
  boltwing:   { name:'Boltwing',   type:T.VOLT,     type2:T.LUMEN,  base:[42,46,40,68], learnset:[['spark',1],['joltingArc',6],['skybreak',12],['gleamPulse',18],['stormCrown',28],['solarCrown',40]], lore:'Said to carry messages between the Spires. Its feathers leave a glow on the morning wind.', transform:null },
  zapling:    { name:'Zapling',    type:T.VOLT,     base:[40,46,44,50], learnset:[['spark',1],['joltingArc',6],['skybreak',12],['toughen',18],['voltBurst',28],['thunderPike',40]], lore:'A pond-frog that swallowed a thunder-lozenge. Its croak startles the rain.', transform:null },

  /* Zone 5: Terra Crags */
  pebblit:    { name:'Pebblit',    type:T.TERRA,    base:[50,46,60,30], learnset:[['stoneThrow',1],['dustKick',6],['earthenGuard',12],['crumble',18],['toughen',28],['tectonicLash',40]], lore:'A pile of small stones that decided, one morning, to be a creature. Quiet, but firm.', transform:{into:'boulderon', atLevel:34, catalyst:'terraShard'} },
  dustmite:   { name:'Dustmite',   type:T.TERRA,    base:[36,52,38,62], learnset:[['dustKick',1],['stoneThrow',6],['crumble',12],['focusRush',18],['quakeStep',28],['tectonicLash',40]], lore:'Drifts across Crags in spirals, polishing bones it finds. It whistles an old mining song.', transform:null },
  moleling:   { name:'Moleling',   type:T.TERRA,    type2:T.UMBRAL, base:[48,48,52,40], learnset:[['stoneThrow',1],['dustKick',6],['shadowTap',12],['earthenGuard',18],['hexSiphon',28],['tectonicLash',40]], lore:'A burrowing thing that knows the names of every root beneath the Crags.', transform:null },

  /* Zone 6: Lumen Sanctum */
  glintling:  { name:'Glintling',  type:T.LUMEN,    base:[40,52,40,60], learnset:[['gleamPulse',1],['dawnspear',6],['solace',12],['halo',18],['radiantVeil',28],['solarCrown',40]], lore:'A mote of pure dawn. It cannot lie, but it loves telling the truth with a smile.', transform:{into:'radiantkin', atLevel:32, catalyst:'lumenPrism'} },
  lumibun:    { name:'Lumibun',    type:T.LUMEN,    type2:T.VERDANT, base:[44,46,46,56], learnset:[['gleamPulse',1],['leafCut',6],['dawnspear',12],['meditate',18],['halo',28],['heartOfGrove',40]], lore:'A rabbit of soft starlight that mends broken pathways as it hops.', transform:null },
  shadeling:  { name:'Shadeling',  type:T.UMBRAL,   base:[40,52,38,60], learnset:[['shadowTap',1],['gloomCut',6],['voidstep',12],['hexSiphon',18],['eclipse',28],['nightfall',40]], lore:'Not a creature, exactly — more a question the dusk asks itself. The answer is gentle.', transform:{into:'voidwalker', atLevel:34, catalyst:'umbralAsh'} },
  pyrelisk:   { name:'Pyrelisk',   type:T.CINDER,   type2:T.UMBRAL, base:[46,58,42,48], learnset:[['emberFlick',1],['gloomCut',6],['cinderJab',12],['flameGlyph',18],['eclipse',28],['phoenixDrift',40]], lore:'A basilisk hatched from a coal that fell into a forgotten shrine. Its gaze is a long memory.', transform:null },

  /* Hidden / post-game */
  duskwraith: { name:'Duskwraith', type:T.UMBRAL,   type2:T.LUMEN, base:[48,56,44,52], learnset:[['shadowTap',1],['gleamPulse',6],['eclipse',12],['halo',18],['hexSiphon',28],['solarCrown',40]], lore:'Found only where day and night argue. It walks the seam without choosing a side.', transform:null },

  /* Transformed forms */
  infernox:   { name:'Infernox',   type:T.CINDER,   base:[68,84,70,80], learnset:[['flareLash',1],['phoenixDrift',1],['solarCrown',1],['toughen',20],['bulkUp',35]], lore:'Pyrlin reborn in a corona of starfire. It remembers the meadow, but no longer fits inside it.', transform:null },
  cindermaw:  { name:'Cindermaw',  type:T.CINDER,   base:[66,86,66,82], learnset:[['flareLash',1],['phoenixDrift',1],['tectonicLash',1],['focusRush',20],['bulkUp',35]], lore:'Once a pup, now a furnace with a fang. It weeps sparks when the wind is right.', transform:null },
  wildheart:  { name:'Wildheart',  type:T.VERDANT,  base:[74,76,82,68], learnset:[['briarStorm',1],['heartOfGrove',1],['tectonicLash',1],['toughen',20],['bulkUp',35]], lore:'The forest\'s memory given roots and hunger. It will outlast the trees.', transform:null },
  stormfang:  { name:'Stormfang',  type:T.VOLT,     base:[60,76,60,98], learnset:[['thunderPike',1],['stormCrown',1],['flareLash',1],['focusRush',20],['bulkUp',35]], lore:'A bolt with teeth. The sky pauses when Stormfang yawns.', transform:null },
  boulderon:  { name:'Boulderon',  type:T.TERRA,    base:[86,72,98,38], learnset:[['tectonicLash',1],['earthenGuard',1],['solarCrown',1],['toughen',20],['bulkUp',35]], lore:'A walking hill. Birds mistake its shoulders for fresh land.', transform:null },
  radiantkin: { name:'Radiantkin', type:T.LUMEN,    base:[64,80,68,90], learnset:[['solarCrown',1],['halo',1],['flareLash',1],['focusRush',20],['toughen',35]], lore:'A small sun with kind eyes. It hums the song the dawn first learned.', transform:null },
  voidwalker: { name:'Voidwalker', type:T.UMBRAL,   base:[62,82,62,90], learnset:[['nightfall',1],['eclipse',1],['voidstep',1],['focusRush',20],['bulkUp',35]], lore:'Where the Voidwalker stands, the world politely steps aside.', transform:null },
  tidecaller: { name:'Tidecaller', type:T.TIDE,     base:[66,70,72,80], learnset:[['maelstrom',1],['riptide',1],['solarCrown',1],['focusRush',20],['bulkUp',35]], lore:'Aquave grown into the ocean\'s memory. Where it walks, the sea follows a step behind.', transform:null },
  stormbeak:  { name:'Stormbeak',  type:T.VOLT,     type2:T.LUMEN, base:[62,70,64,96], learnset:[['thunderPike',1],['stormCrown',1],['solarCrown',1],['focusRush',20],['bulkUp',35]], lore:'Boltwing with the storm settled in its bones. It carries messages between dawns now.', transform:null }
};
const CREAT_LIST = Object.keys(CREATURES);
const STARTER_IDS = ['pyrlin','aquave','leafkin'];

/* ===== ITEMS ===== */
const ITEMS = {
  wispOrb:    { name:'Wisp Orb',     desc:'A glass bead holding a tame ember. Use in battle to attempt a collection.', kind:'capture', power:1.0, price:50 },
  gleamOrb:   { name:'Gleam Orb',    desc:'A silvered sphere that hums when near a creature. Improves collection odds.', kind:'capture', power:1.5, price:200 },
  lattice:    { name:'Aetheric Lattice', desc:'A lattice of woven starlight. Triples collection power — for the worthy.', kind:'capture', power:3.0, price:1000 },
  potion:     { name:'Moonpetal Tonic', desc:'Restores 40 HP to a creature.', kind:'heal', power:40, price:80 },
  superPotion:{ name:'Sunroot Drop',    desc:'Fully restores one creature\'s HP and clears minor ailments.', kind:'healFull', price:300 },
  revive:     { name:'Dawnfeather',     desc:'Revives a fainted creature to half HP.', kind:'revive', power:0.5, price:400 },
  cinderStone:{ name:'Cinder Stone',    desc:'A petrified ember. Required to catalyze some transformations.', kind:'catalyst', price:600 },
  verdantMoss:{ name:'Verdant Moss',    desc:'Soft moss that hums of the forest\'s first breath.', kind:'catalyst', price:600 },
  voltCoil:   { name:'Volt Coil',       desc:'A wire of captured lightning, neatly coiled.', kind:'catalyst', price:600 },
  terraShard: { name:'Terra Shard',     desc:'A fragment of the first mountain.', kind:'catalyst', price:600 },
  lumenPrism: { name:'Lumen Prism',     desc:'A prism that splits a single dawn into a small rainbow.', kind:'catalyst', price:800 },
  umbralAsh:  { name:'Umbral Ash',      desc:'Cold ash that remembers the shape of what burned.', kind:'catalyst', price:800 },
  repel:      { name:'Repel Charm',     desc:'For a short time, weaker wilds avoid you.', kind:'repel', turns:200, price:200 }
};
const ITEM_LIST = Object.keys(ITEMS);

/* ===== ZONES ===== */
/* Each zone: id, name, palette, encounter table (creature ids with weights), boss, ambient (particle params), connections (unlock to next) */
const ZONES = {
  glades: {
    id:'glades', name:'Cinder Glades', order:1,
    gradient:['#4a1a08','#1a0a05'], accent:'#ff7a45',
    ambient:{kind:'ember', count:30, colors:['#ff7a45','#ffb070','#ffd97a']},
    wilds:[
      {id:'pyrlin', w:30}, {id:'aquave', w:15}, {id:'leafkin', w:15},
      {id:'emberpup', w:30}, {id:'vinewhip', w:10}
    ],
    boss:{ id:'pyrelisk', level:14, story:1 },
    rate: 0.18, minLv:2, maxLv:8,
    next:'cove',
    storyNode:'A wagon of broken starlight, half-buried in warm ash. The Veil is thin here.'
  },
  cove: {
    id:'cove', name:'Tideglass Coves', order:2,
    gradient:['#0e2a4a','#06121f'], accent:'#4cc9f0',
    ambient:{kind:'bubble', count:24, colors:['#9be8ff','#4cc9f0','#fff']},
    wilds:[
      {id:'aquave', w:25}, {id:'pyrlin', w:10}, {id:'coralimp', w:30},
      {id:'frostfin', w:20}, {id:'leafkin', w:15}
    ],
    boss:{ id:'frostfin', level:22, story:2, evolve:true },  /* a titan frostfin */
    rate: 0.18, minLv:10, maxLv:18,
    next:'thicket',
    storyNode:'Bells made of glass sing under the water, calling the lost home.'
  },
  thicket: {
    id:'thicket', name:'Verdant Thicket', order:3,
    gradient:['#0e2a14','#061a0a'], accent:'#7ee07a',
    ambient:{kind:'spore', count:22, colors:['#c5f0a5','#7ee07a','#fff5d6']},
    wilds:[
      {id:'leafkin', w:25}, {id:'bramblin', w:30}, {id:'vinewhip', w:15},
      {id:'coralimp', w:10}, {id:'frostfin', w:10}, {id:'aquave', w:10}
    ],
    boss:{ id:'wildheart', level:30, story:3, boss:false },  /* a wild bramblin transform */
    rate: 0.18, minLv:18, maxLv:26,
    next:'spires',
    storyNode:'A tree older than names, asking the wind to remember it tomorrow.'
  },
  spires: {
    id:'spires', name:'Volt Spires', order:4,
    gradient:['#1a1a30','#0a0a1a'], accent:'#f5e042',
    ambient:{kind:'spark', count:30, colors:['#fff5a8','#f5e042','#fff']},
    wilds:[
      {id:'sparkat', w:30}, {id:'boltwing', w:20}, {id:'zapling', w:20},
      {id:'vinewhip', w:10}, {id:'pebblit', w:10}, {id:'bramblin', w:10}
    ],
    boss:{ id:'stormfang', level:38, story:3, boss:false },
    rate: 0.20, minLv:26, maxLv:34,
    next:'crags',
    storyNode:'The Spires hum a chord older than weather. Birds refuse to land here.'
  },
  crags: {
    id:'crags', name:'Terra Crags', order:5,
    gradient:['#2a1a0a','#0a0805'], accent:'#c08a5a',
    ambient:{kind:'dust', count:20, colors:['#e8c79a','#c08a5a','#fff5d6']},
    wilds:[
      {id:'pebblit', w:30}, {id:'dustmite', w:25}, {id:'moleling', w:15},
      {id:'sparkat', w:10}, {id:'zapling', w:10}, {id:'emberpup', w:10}
    ],
    boss:{ id:'boulderon', level:44, story:4 },
    rate: 0.20, minLv:32, maxLv:42,
    next:'sanctum',
    storyNode:'A door carved from a single mountain. It only opens for those who have come on foot.'
  },
  sanctum: {
    id:'sanctum', name:'Lumen Sanctum', order:6,
    gradient:['#1a1430','#08051a'], accent:'#fff5d6',
    ambient:{kind:'dust', count:36, colors:['#fff5d6','#ecd9ff','#fff']},
    wilds:[
      {id:'glintling', w:25}, {id:'lumibun', w:20}, {id:'shadeling', w:20},
      {id:'duskwraith', w:10}, {id:'pyrelisk', w:10}, {id:'boltwing', w:10}, {id:'moleling', w:5}
    ],
    boss:{ id:'voidwalker', level:50, story:4, final:true },
    rate: 0.22, minLv:38, maxLv:50,
    next:null,
    storyNode:'The first dawn lives here, patient as a held breath. Bring it gently home.'
  }
};
const ZONE_LIST = ['glades','cove','thicket','spires','crags','sanctum'];

/* ===== STAT CALC ===== */
function statFor(creatId, level, statIdx){
  const c = CREATURES[creatId];
  if(!c) return 1;
  const base = c.base[statIdx];
  const atkBonus = (statIdx===0) ? 4 : (statIdx===3) ? 1.6 : 1.4;
  return Math.floor(base + (level-1) * atkBonus);
}
function makeBattleCreature(creatId, level){
  const c = CREATURES[creatId];
  const maxHp = statFor(creatId, level, 0);
  const moves = c.learnset.filter(([k,lv])=>lv<=level).map(([k])=>k).slice(0,4);
  if(!moves.length) moves.push(c.learnset[0][0]);
  const abilityPp = Object.fromEntries(Object.keys(ABILITIES).map(k=>[k,0]));
  for(const m of moves) abilityPp[m] = ABILITIES[m].pp;
  return {
    id: creatId, name:c.name, type:c.type, type2:c.type2||null,
    level, exp:0, expToNext: expToNext(level),
    maxHp, hp: maxHp,
    atk: statFor(creatId, level, 1),
    def: statFor(creatId, level, 2),
    spd: statFor(creatId, level, 3),
    abilityPp,
    moves,
    transforms: c.transform,
    status: {},
    atkMod: 0, defMod: 0, spdMod: 0, accMod: 0,
    shielded: false, hasFled:false, faint:false
  };
}
function expToNext(level){ return Math.floor(8 + level*5 + Math.pow(level,1.4)*1.5); }
function refreshPp(bc){
  const learnset = CREATURES[bc.id].learnset;
  bc.moves.forEach(mv => { bc.abilityPp[mv] = ABILITIES[mv].pp; });
}
function levelUp(bc){
  if(bc.level >= MAX_LEVEL) return false;
  bc.level++;
  const newMax = statFor(bc.id, bc.level, 0);
  const oldMax = bc.maxHp;
  bc.maxHp = newMax;
  bc.hp = Math.min(bc.maxHp, bc.hp + (newMax - oldMax));
  bc.atk = statFor(bc.id, bc.level, 1);
  bc.def = statFor(bc.id, bc.level, 2);
  bc.spd = statFor(bc.id, bc.level, 3);
  bc.expToNext = expToNext(bc.level);
  // learn moves
  const learnset = CREATURES[bc.id].learnset;
  for(const [k,lv] of learnset){
    if(lv === bc.level){
      if(bc.moves.length < 4){
        bc.moves.push(k); refreshPp(bc);
        Game.notify(bc.name + ' learned ' + ABILITIES[k].name + '!');
      } else {
        Game.offerMoveReplace(bc, k);
        break;
      }
    }
  }
  return true;
}
function learnableAt(creatId, level){
  return CREATURES[creatId].learnset.filter(([k,lv])=>lv===level).map(([k])=>k);
}

/* ===== MAPS (tile arrays) ===== */
/* Legend: . ground, # wall, ~ water, , tall grass (encounters), S shrine, N npc, G guardian/boss, E exit (forward), R retreat, P player start */
const MAPS = {
  glades: [
    "################################",
    "#P..........#......,~,,,......E#",
    "#...........#......,~~~,,.....#",
    "#..##.##....#.....,,,~~~,......#",
    "#...........#....,,,,,~~......#",
    "#....NN.....#..................#",
    "#...........#....,,,~~~,,......#",
    "#..,,,~~~...#....,~~~~~,.......#",
    "#..,~~~,...##....,~~,,,........#",
    "#..,,,.....##..................#",
    "#...........#....SS............#",
    "#....G......#....S.............#",
    "#...........#....S.............#",
    "#...........#..................#",
    "################################"
  ],
  cove: [
    "################################",
    "#P..............#...E..........#",
    "#..~~~~~~~~~~~~.#..............#",
    "#..~~~~..~~~~...#..............#",
    "#..~~~~..~~~~...#....,~~~~~,...#",
    "#........~~~~...#....,~~~~~,...#",
    "#..~~~~..~~~~...#....,~~~~~,...#",
    "#..~~~~..~~~~NN.#....,~~~~~,...#",
    "#..~~~~..~~~~...#..............#",
    "#..~~~~..~~~~...#....SS........#",
    "#..~~~~..~~~~...#....S.........#",
    "#........~~~~...#....S.........#",
    "#..~~~~~~~~~~~~.#....G.........#",
    "#...............#..............#",
    "################################"
  ],
  thicket: [
    "################################",
    "#P..........#........E.........#",
    "#..##.##....#........##.##....#",
    "#...........#........##.##....#",
    "#....NN.....#........##.##....#",
    "#...........#........##.##....#",
    "#..,,,~~~...#........##.##....#",
    "#..,~~~,...##........##.##....#",
    "#..,,,.....##..................#",
    "#...........#........##.##....#",
    "#....SS.....#........##.##....#",
    "#....S......#........##.##....#",
    "#....S......#........G.........#",
    "#...........#..................#",
    "################################"
  ],
  spires: [
    "################################",
    "#P...........#...E.............#",
    "#..##.##....##..........,,,....#",
    "#...........#..........,~~~,...#",
    "#..NN.......#..........,,,....#",
    "#...........#..........,~~~...#",
    "#..,,,~~~...#..........,,,....#",
    "#..,~~~,...##..........,~~~...#",
    "#..,,,.....##..........,,,....#",
    "#...........#..........,~~~...#",
    "#....SS.....#..........,,,....#",
    "#....S......#..........,~~~...#",
    "#....S......#..........G.......#",
    "#...........#.................#",
    "################################"
  ],
  crags: [
    "################################",
    "#P..........#....E.............#",
    "#..##.##....#..........~~......#",
    "#...........#..........~~......#",
    "#..NN.......#..........~~......#",
    "#...........#..........~~......#",
    "#..,,,~~~...#..........~~......#",
    "#..,~~~,...##..........~~......#",
    "#..,,,.....##..........~~......#",
    "#...........#..........~~......#",
    "#....SS.....#..........~~......#",
    "#....S......#..........~~......#",
    "#....S......#..........G.......#",
    "#...........#..........~~......#",
    "################################"
  ],
  sanctum: [
    "################################",
    "#P..........#....E.............#",
    "#..##.##....#..................#",
    "#...........#..................#",
    "#..NN.......#..................#",
    "#...........#..................#",
    "#..,,,~~~...#..................#",
    "#..,~~~,...##..................#",
    "#..,,,.....##..................#",
    "#...........#..................#",
    "#....SS.....#..................#",
    "#....S......#..................#",
    "#....S......#..................#",
    "#...........#....G.............#",
    "################################"
  ]
};

/* ===== STORY ===== */
const STORY = {
  1:{ who:'A Voice in the Ash', what:'A whisper from a broken star says: "Carry them gently. They are learning your name."', color:'#3a0e08' },
  2:{ who:'A Voice in the Tide', what:'The sea hums back, "The Veil is thinned where you walk. You are making a path that will remember you."', color:'#0a2440' },
  3:{ who:'A Voice in the Thicket', what:'The forest exhales. "Eight will choose you. Four will hold the world steady. Be patient at the door."', color:'#0a2814' },
  4:{ who:'A Voice in the Dawn', what:'The Sanctum brightens. "Bring the first dawn home. It has been waiting since the morning we lost."', color:'#241240' }
};

/* ===== GAME STATE ===== */
const Game = {
  state:'title', /* title | overworld | battle | dialog | menu | tutorial */
  player: { x:1, y:1, dir:0, walkT:0, stepPhase:0, moveAccum:0 },
  zone: 'glades',
  party: [], /* active party up to 6 */
  storage: [], /* boxed creatures */
  codex: {}, /* id -> {seen:true, caught:true} */
  bag: { potion:5, wispOrb:8, superPotion:1, revive:0, gleamOrb:0, lattice:0 },
  bagKinds: ['wispOrb','gleamOrb','lattice','potion','superPotion','revive','cinderStone','verdantMoss','voltCoil','terraShard','lumenPrism','umbralAsh','repel'],
  coins: 0,
  repelCount: 0,
  stepsInGrass: 0,
  tutorialStep: 0,
  tutorialFlag: false,
  flags: {}, /* quest flags */
  storyStage: 0,
  bossDefeated: {}, /* zoneId -> true */
  zoneUnlocked: { glades:true },
  shrineSpawn: 'glades',
  playTime: 0, /* seconds */
  startTime: 0,
  showPause: false,
  modal: null, /* current modal name */
  pendingDialog: null, /* queue of {who, what, color, onClose} */
  encounterCooldown: 0,
  shake: 0,
  paused: false,
  toasts: [],
  init(){
    this.bindUI();
    this.bindInput();
    this.start();
  },
  start(){
    document.getElementById('title-screen')?.remove();
    showTitle();
  },
  notify(msg, t=1800){
    const el = document.createElement('div');
    el.className = 'toast';
    el.textContent = msg;
    document.getElementById('ui').appendChild(el);
    setTimeout(()=>el.remove(), t+400);
  },
  offerMoveReplace(bc, newKey){ /* called via dialog if 4 moves already known */
    this.pendingDialog = {
      who: bc.name,
      what: 'Wants to learn ' + ABILITIES[newKey].name + '. Replace which move?',
      choices: bc.moves.map((m,i)=>({label:'Forget ' + ABILITIES[m].name, onPick:()=>{ bc.moves[i] = newKey; refreshPp(bc); this.notify('Forgot ' + ABILITIES[m].name + '!'); this.advanceDialog(); }}))
    };
    this.showDialog();
  },
  advanceDialog(){ this.pendingDialog.choices ? this.showDialog() : this.closeDialog(); }
};
window.Game = Game;

/* ===== CANVAS / RENDER ===== */
const canvas = document.getElementById('game');
const ctx2d = canvas.getContext('2d');
let VW = 1280, VH = 720;
function resize(){
  const r = canvas.getBoundingClientRect();
  const dpr = Math.min(window.devicePixelRatio||1, 2);
  canvas.width = Math.floor(r.width*dpr);
  canvas.height = Math.floor(r.height*dpr);
  VW = r.width; VH = r.height;
  ctx2d.setTransform(dpr,0,0,dpr,0,0);
  ctx2d.imageSmoothingEnabled = false;
}
window.addEventListener('resize', resize);
resize();

/* ===== PARTICLES ===== */
const Particles = {
  list: [],
  spawn(x,y,opts){
    const o = Object.assign({count:6,color:'#fff',life:0.4,size:2,spd:60,grav:0,spread:Math.PI*2}, opts||{});
    for(let i=0;i<o.count;i++){
      const a = o.spread===Math.PI*2 ? rnd()*o.spread : o.spread[0] + rnd()*(o.spread[1]-o.spread[0]);
      this.list.push({
        x,y,
        vx: Math.cos(a)*(o.spd*(0.5+rnd()*0.6)),
        vy: Math.sin(a)*(o.spd*(0.5+rnd()*0.6)),
        life:o.life*(0.6+rnd()*0.6), max:o.life,
        color:o.color, size:o.size*(0.6+rnd()*0.6), grav:o.grav
      });
    }
  },
  ambient:[],
  setAmbient(zone){
    this.ambient = [];
    const z = ZONES[zone];
    if(!z) return;
    const am = z.ambient;
    for(let i=0;i<am.count;i++){
      this.ambient.push({
        x: rnd()*VW, y: rnd()*VH,
        vx: (rnd()-0.5)*8, vy: -4 - rnd()*8,
        life: 4 + rnd()*4, max: 4+rnd()*4,
        color: pick(am.colors), size: 1+rnd()*2,
        kind: am.kind
      });
    }
  },
  updateAmbient(dt){
    for(const p of this.ambient){
      p.x += p.vx*dt; p.y += p.vy*dt;
      p.life -= dt;
      if(p.life<=0 || p.y<-10){
        p.x = rnd()*VW; p.y = VH + rnd()*30;
        p.vx = (rnd()-0.5)*8; p.vy = -4 - rnd()*8;
        p.life = p.max; p.color = pick(ZONES[Game.zone].ambient.colors);
      }
    }
  },
  update(dt){
    this.updateAmbient(dt);
    for(let i=this.list.length-1;i>=0;i--){
      const p = this.list[i];
      p.x += p.vx*dt; p.y += p.vy*dt;
      p.vy += (p.grav||0)*dt;
      p.life -= dt;
      if(p.life<=0) this.list.splice(i,1);
    }
  },
  draw(ctx){
    for(const p of this.ambient){
      const a = clamp(p.life/p.max, 0, 1);
      ctx.globalAlpha = a*0.6;
      ctx.fillStyle = p.color;
      ctx.beginPath(); ctx.arc(p.x,p.y,p.size,0,Math.PI*2); ctx.fill();
    }
    for(const p of this.list){
      const a = clamp(p.life/p.max, 0, 1);
      ctx.globalAlpha = a;
      ctx.fillStyle = p.color;
      ctx.beginPath(); ctx.arc(p.x,p.y,p.size*a,0,Math.PI*2); ctx.fill();
    }
    ctx.globalAlpha = 1;
  }
};

/* ===== SAVE / LOAD ===== */
const SAVE_KEY = 'aethelgard_veilbound_save_v1';
function saveGame(slot=0){
  try {
    const data = {
      v:1, ts:Date.now(),
      player: Game.player, zone: Game.zone,
      party: Game.party, storage: Game.storage,
      codex: Game.codex, bag: Game.bag, coins: Game.coins,
      repelCount: Game.repelCount,
      flags: Game.flags, storyStage: Game.storyStage,
      bossDefeated: Game.bossDefeated, zoneUnlocked: Game.zoneUnlocked,
      shrineSpawn: Game.shrineSpawn, playTime: Game.playTime,
      tutorialFlag: Game.tutorialFlag
    };
    localStorage.setItem(SAVE_KEY + '_' + slot, JSON.stringify(data));
    return true;
  } catch(e){ console.warn('Save failed',e); return false; }
}
function loadGame(slot=0){
  try {
    const raw = localStorage.getItem(SAVE_KEY + '_' + slot);
    if(!raw) return null;
    const data = JSON.parse(raw);
    Object.assign(Game.player, data.player);
    Game.zone = data.zone;
    Game.party = data.party; Game.storage = data.storage;
    Game.codex = data.codex; Game.bag = data.bag;
    Game.coins = data.coins; Game.repelCount = data.repelCount||0;
    Game.flags = data.flags||{}; Game.storyStage = data.storyStage||0;
    Game.bossDefeated = data.bossDefeated||{};
    Game.zoneUnlocked = data.zoneUnlocked||{glades:true};
    Game.shrineSpawn = data.shrineSpawn||'glades';
    Game.playTime = data.playTime||0;
    Game.tutorialFlag = !!data.tutorialFlag;
    return data;
  } catch(e){ console.warn('Load failed',e); return null; }
}
function hasSave(){ for(let i=0;i<3;i++) if(localStorage.getItem(SAVE_KEY+'_'+i)) return true; return false; }

/* ===== INPUT ===== */
const Input = {
  keys: {},
  pressed: {}, /* single-frame press */
  bind(){
    window.addEventListener('keydown', e => {
      Audio.init();
      const k = e.key.toLowerCase();
      if(!this.keys[k]) this.pressed[k] = true;
      this.keys[k] = true;
      if(['arrowup','arrowdown','arrowleft','arrowright',' '].includes(k)) e.preventDefault();
    });
    window.addEventListener('keyup', e => { this.keys[e.key.toLowerCase()] = false; });
    /* touch buttons */
    document.querySelectorAll('#touch-pad .tb').forEach(el => {
      const k = el.dataset.k;
      const map = {up:'arrowup',down:'arrowdown',left:'arrowleft',right:'arrowright',act:'e',bag:'b',cancel:'escape',enter:'enter',pause:'escape'};
      const key = map[k] || k;
      const down = e => { e.preventDefault(); Audio.init(); if(!this.keys[key]) this.pressed[key] = true; this.keys[key] = true; };
      const up = e => { e.preventDefault(); this.keys[key] = false; };
      el.addEventListener('touchstart', down, {passive:false});
      el.addEventListener('mousedown', down);
      el.addEventListener('touchend', up, {passive:false});
      el.addEventListener('mouseup', up);
      el.addEventListener('touchcancel', up, {passive:false});
      el.addEventListener('mouseleave', up);
    });
    /* click on canvas to start audio */
    canvas.addEventListener('pointerdown', ()=>{ Audio.init(); });
  },
  consume(k){ const v = !!this.pressed[k]; this.pressed[k] = false; return v; },
  isDown(k){ return !!this.keys[k]; }
};
Input.bind();

/* ===== MAP HELPERS ===== */
function getMap(zoneId){ return MAPS[zoneId]; }
function tileAt(map, x, y){
  if(y<0||x<0||y>=map.length||x>=map[0].length) return '#';
  return map[y][x];
}
function findFirst(map, ch){
  for(let y=0;y<map.length;y++) for(let x=0;x<map[y].length;x++) if(map[y][x]===ch) return {x,y};
  return null;
}

/* ===== OVERWORLD RENDER ===== */
function drawOverworld(dt){
  const zone = ZONES[Game.zone];
  const map = getMap(Game.zone);
  /* background gradient */
  const grad = ctx2d.createLinearGradient(0,0,0,VH);
  grad.addColorStop(0, zone.gradient[0]);
  grad.addColorStop(1, zone.gradient[1]);
  ctx2d.fillStyle = grad;
  ctx2d.fillRect(0,0,VW,VH);
  /* soft radial accent */
  const rg = ctx2d.createRadialGradient(VW/2,VH/2,40,VW/2,VH/2,Math.max(VW,VH)*0.7);
  rg.addColorStop(0, zone.accent + '22');
  rg.addColorStop(1, '#00000000');
  ctx2d.fillStyle = rg; ctx2d.fillRect(0,0,VW,VH);
  /* map area: 32x15 grid, 40px each, centered */
  const mapW = map[0].length * TILE, mapH = map.length * TILE;
  const ox = Math.floor((VW-mapW)/2), oy = Math.floor((VH-mapH)/2) + 20;
  /* draw tiles */
  for(let y=0;y<map.length;y++){
    for(let x=0;x<map[y].length;x++){
      const ch = map[y][x];
      const px = ox + x*TILE, py = oy + y*TILE;
      drawTile(ch, px, py, x, y, zone, map);
    }
  }
  /* draw entities */
  for(let y=0;y<map.length;y++){
    for(let x=0;x<map[y].length;x++){
      const ch = map[y][x];
      const px = ox + x*TILE + TILE/2, py = oy + y*TILE + TILE/2;
      if(ch==='N') drawNPC(px, py, zone);
      else if(ch==='S') drawShrine(px, py, zone);
      else if(ch==='G') drawGuardian(px, py, zone);
      else if(ch==='E') drawExit(px, py, zone, x, y, map);
    }
  }
  /* draw player */
  const ppx = ox + Game.player.x*TILE + TILE/2;
  const ppy = oy + Game.player.y*TILE + TILE/2;
  drawPlayer(ppx, ppy, Game.player.dir, Game.player.stepPhase, Game.zone);
  /* particles on top */
  Particles.draw(ctx2d);
  /* vignette */
  ctx2d.fillStyle = 'rgba(0,0,0,0.25)';
  ctx2d.fillRect(0,0,VW,40);
  ctx2d.fillRect(0,VH-40,VW,40);
  /* zone name in bottom strip */
  ctx2d.fillStyle = 'rgba(0,0,0,0.5)';
  ctx2d.fillRect(0, VH-40, VW, 40);
  ctx2d.fillStyle = '#eef2ff';
  ctx2d.font = '12px ui-sans-serif, system-ui';
  ctx2d.textAlign = 'left';
  ctx2d.fillText(zone.name, 14, VH-15);
  ctx2d.textAlign = 'right';
  if(Game.flags.tutorialActive){
    ctx2d.fillStyle = '#ffd97a';
    ctx2d.fillText('Tutorial: ' + (TUTORIAL[Game.tutorialStep]?.hint || ''), VW-14, VH-15);
  } else {
    ctx2d.fillStyle = '#a3acd1';
    ctx2d.fillText('WASD/Arrows to move · E to act · Esc to pause', VW-14, VH-15);
  }
  /* shake */
  if(Game.shake > 0){
    Game.shake -= dt;
  }
  /* store ox/oy for game logic */
  Game._ox = ox; Game._oy = oy;
}

const TUTORIAL = [
  {hint:'Use WASD or arrow keys to move.'},
  {hint:'Walk into tall grass to find wild creatures.'},
  {hint:'In battle, choose a move. Press E or Space to confirm.'},
  {hint:'Try capturing with a Wisp Orb after weakening a creature.'}
];

function drawTile(ch, x, y, tx, ty, zone, map){
  const base = zone.accent;
  const ctx = ctx2d;
  if(ch === '#'){
    /* wall: layered stone */
    const g = ctx.createLinearGradient(x,y,x,y+TILE);
    g.addColorStop(0, '#0a0a14');
    g.addColorStop(1, '#1a1a26');
    ctx.fillStyle = g; ctx.fillRect(x,y,TILE,TILE);
    /* brick lines */
    ctx.strokeStyle = 'rgba(255,255,255,0.04)';
    ctx.lineWidth = 1;
    ctx.strokeRect(x+0.5,y+0.5,TILE-1,TILE-1);
    ctx.beginPath();
    ctx.moveTo(x, y+TILE/2); ctx.lineTo(x+TILE, y+TILE/2);
    ctx.moveTo(x+TILE/2, y); ctx.lineTo(x+TILE/2, y+TILE/2);
    ctx.moveTo(x+TILE/4, y+TILE/2); ctx.lineTo(x+TILE/4, y+TILE);
    ctx.moveTo(x+3*TILE/4, y+TILE/2); ctx.lineTo(x+3*TILE/4, y+TILE);
    ctx.stroke();
    /* top highlight */
    ctx.fillStyle = 'rgba(255,255,255,0.05)';
    ctx.fillRect(x, y, TILE, 2);
  }
  else if(ch === '.'){
    /* ground */
    const g = ctx.createRadialGradient(x+TILE/2,y+TILE/2,4,x+TILE/2,y+TILE/2,TILE);
    g.addColorStop(0, shade(base, 0.18));
    g.addColorStop(1, shade(base, -0.32));
    ctx.fillStyle = g; ctx.fillRect(x,y,TILE,TILE);
    /* speckle */
    if(((tx*7+ty*13) % 5) === 0){
      ctx.fillStyle = 'rgba(255,255,255,0.05)';
      ctx.fillRect(x+8+(tx*5)%24, y+6+(ty*7)%28, 2, 2);
    }
  }
  else if(ch === ','){
    /* tall grass */
    const g = ctx.createLinearGradient(x,y,x,y+TILE);
    g.addColorStop(0, shade(base, 0.35));
    g.addColorStop(1, shade(base, -0.15));
    ctx.fillStyle = g; ctx.fillRect(x,y,TILE,TILE);
    /* blades */
    ctx.fillStyle = shade(base, 0.55);
    for(let i=0;i<6;i++){
      const bx = x + 4 + (i*7 + tx) % (TILE-8);
      const by = y + 16 + (i*5 + ty) % 14;
      ctx.beginPath();
      ctx.moveTo(bx, by+12); ctx.lineTo(bx+2, by); ctx.lineTo(bx+4, by+12); ctx.closePath();
      ctx.fill();
    }
    ctx.fillStyle = 'rgba(255,255,255,0.06)';
    ctx.fillRect(x, y, TILE, 1);
  }
  else if(ch === '~'){
    /* water */
    const g = ctx.createLinearGradient(x,y,x,y+TILE);
    g.addColorStop(0, shade(base, 0.30));
    g.addColorStop(1, shade(base, -0.20));
    ctx.fillStyle = g; ctx.fillRect(x,y,TILE,TILE);
    /* ripples */
    ctx.strokeStyle = 'rgba(255,255,255,0.18)';
    ctx.lineWidth = 1;
    const t = (performance.now()/1000 + tx*0.4 + ty*0.2);
    ctx.beginPath();
    ctx.arc(x+TILE/2, y+TILE/2, 8 + Math.sin(t)*2, 0, Math.PI*2);
    ctx.stroke();
  }
  else if(ch === 'S'){
    /* shrine floor */
    const g = ctx.createRadialGradient(x+TILE/2,y+TILE/2,2,x+TILE/2,y+TILE/2,TILE);
    g.addColorStop(0, '#fff5d6');
    g.addColorStop(1, shade(base, -0.4));
    ctx.fillStyle = g; ctx.fillRect(x,y,TILE,TILE);
    ctx.fillStyle = 'rgba(255,245,200,0.15)';
    ctx.fillRect(x,y,TILE,TILE);
  }
  else if(ch === 'E'){
    /* exit: dark portal pad */
    const g = ctx.createRadialGradient(x+TILE/2,y+TILE/2,2,x+TILE/2,y+TILE/2,TILE/2);
    g.addColorStop(0, '#fff5d6');
    g.addColorStop(0.6, '#c9a4ff');
    g.addColorStop(1, '#0a0a1a');
    ctx.fillStyle = g; ctx.fillRect(x,y,TILE,TILE);
  }
}
function shade(hex, amt){
  /* hex like #rrggbb, amt in -1..1 */
  const c = hex.replace('#','');
  const r = parseInt(c.substr(0,2),16), g = parseInt(c.substr(2,2),16), b = parseInt(c.substr(4,2),16);
  const f = amt<0 ? 1+amt : 1;
  const a = Math.abs(amt);
  const nr = Math.max(0,Math.min(255, amt<0 ? r*f : r + (255-r)*a));
  const ng = Math.max(0,Math.min(255, amt<0 ? g*f : g + (255-g)*a));
  const nb = Math.max(0,Math.min(255, amt<0 ? b*f : b + (255-b)*a));
  return '#' + [nr,ng,nb].map(v=>Math.floor(v).toString(16).padStart(2,'0')).join('');
}
function drawPlayer(cx, cy, dir, stepPhase, zone){
  const ctx = ctx2d;
  ctx.save();
  ctx.translate(cx, cy);
  /* shadow */
  ctx.fillStyle = 'rgba(0,0,0,0.4)';
  ctx.beginPath(); ctx.ellipse(0, 14, 12, 4, 0, 0, Math.PI*2); ctx.fill();
  /* bob */
  const bob = Math.sin(stepPhase*Math.PI*2) * 1.5;
  ctx.translate(0, bob);
  /* cloak body */
  ctx.fillStyle = '#3a2a4a';
  ctx.beginPath();
  ctx.moveTo(-10, 14);
  ctx.lineTo(-12, 2);
  ctx.lineTo(-6, -8);
  ctx.lineTo(6, -8);
  ctx.lineTo(12, 2);
  ctx.lineTo(10, 14);
  ctx.closePath(); ctx.fill();
  /* cloak highlight */
  ctx.fillStyle = '#5a3a6a';
  ctx.beginPath();
  ctx.moveTo(-6, -8);
  ctx.lineTo(0, 0);
  ctx.lineTo(6, -8);
  ctx.closePath(); ctx.fill();
  /* head */
  ctx.fillStyle = '#f0d8b0';
  ctx.beginPath(); ctx.arc(0, -14, 7, 0, Math.PI*2); ctx.fill();
  /* hood/hat */
  ctx.fillStyle = '#2a1a3a';
  ctx.beginPath();
  ctx.moveTo(-8, -16);
  ctx.quadraticCurveTo(0, -28, 8, -16);
  ctx.lineTo(7, -10);
  ctx.lineTo(-7, -10);
  ctx.closePath(); ctx.fill();
  /* eyes */
  ctx.fillStyle = '#fff';
  if(dir === 0){ ctx.fillRect(-4, -14, 2, 2); ctx.fillRect(2, -14, 2, 2); }
  else if(dir === 2){ ctx.fillRect(-3, -14, 2, 2); ctx.fillRect(3, -14, 2, 2); }
  else if(dir === 3){ ctx.fillRect(-4, -14, 2, 2); ctx.fillRect(3, -14, 2, 2); }
  else { ctx.fillRect(-3, -14, 2, 2); ctx.fillRect(4, -14, 2, 2); }
  /* staff: depending on dir, lean */
  ctx.save();
  ctx.rotate((dir-1)*0.15);
  ctx.fillStyle = '#6a4a3a';
  ctx.fillRect(11, -22, 2, 30);
  ctx.fillStyle = '#7ee0d6';
  ctx.beginPath(); ctx.arc(12, -24, 3, 0, Math.PI*2); ctx.fill();
  ctx.fillStyle = 'rgba(126,224,214,0.4)';
  ctx.beginPath(); ctx.arc(12, -24, 6, 0, Math.PI*2); ctx.fill();
  ctx.restore();
  ctx.restore();
}
function drawNPC(cx, cy, zone){
  const ctx = ctx2d;
  ctx.save(); ctx.translate(cx, cy);
  /* soft glow */
  const g = ctx.createRadialGradient(0,0,1,0,0,28);
  g.addColorStop(0, '#fff5d6'); g.addColorStop(1, '#fff5d600');
  ctx.fillStyle = g; ctx.beginPath(); ctx.arc(0,0,28,0,Math.PI*2); ctx.fill();
  /* body */
  ctx.fillStyle = '#5a3a2a';
  ctx.beginPath();
  ctx.moveTo(-10,14); ctx.lineTo(-12,2); ctx.lineTo(-6,-8); ctx.lineTo(6,-8); ctx.lineTo(12,2); ctx.lineTo(10,14); ctx.closePath();
  ctx.fill();
  /* face */
  ctx.fillStyle = '#f0d8b0';
  ctx.beginPath(); ctx.arc(0,-14,7,0,Math.PI*2); ctx.fill();
  /* question mark */
  ctx.fillStyle = '#fff5d6';
  ctx.font = 'bold 14px ui-sans-serif';
  ctx.textAlign = 'center';
  ctx.fillText('?', 0, -28);
  ctx.restore();
}
function drawShrine(cx, cy, zone){
  const ctx = ctx2d;
  const t = performance.now()/1000;
  ctx.save(); ctx.translate(cx, cy);
  /* pillar */
  ctx.fillStyle = '#2a2a3a';
  ctx.fillRect(-3, -8, 6, 18);
  /* crystal */
  const s = 5 + Math.sin(t*2)*1;
  ctx.fillStyle = '#fff5d6';
  ctx.beginPath();
  ctx.moveTo(0, -8 - s);
  ctx.lineTo(s, -8);
  ctx.lineTo(0, -8 + s);
  ctx.lineTo(-s, -8);
  ctx.closePath(); ctx.fill();
  /* glow */
  const g = ctx.createRadialGradient(0, -8, 1, 0, -8, 18);
  g.addColorStop(0, 'rgba(255,245,200,0.6)');
  g.addColorStop(1, 'rgba(255,245,200,0)');
  ctx.fillStyle = g; ctx.beginPath(); ctx.arc(0,-8,18,0,Math.PI*2); ctx.fill();
  /* base */
  ctx.fillStyle = '#3a3a4a';
  ctx.fillRect(-10, 10, 20, 3);
  ctx.restore();
}
function drawGuardian(cx, cy, zone){
  const ctx = ctx2d;
  const t = performance.now()/1000;
  ctx.save(); ctx.translate(cx, cy);
  /* aura */
  const a = 0.5 + Math.sin(t*1.5)*0.2;
  const g = ctx.createRadialGradient(0,0,4,0,0,28);
  g.addColorStop(0, 'rgba(255,80,80,'+a+')');
  g.addColorStop(1, 'rgba(255,80,80,0)');
  ctx.fillStyle = g; ctx.beginPath(); ctx.arc(0,0,28,0,Math.PI*2); ctx.fill();
  /* body */
  ctx.fillStyle = '#2a0a0a';
  ctx.beginPath();
  ctx.moveTo(-12,14); ctx.lineTo(-14,2); ctx.lineTo(-8,-10); ctx.lineTo(8,-10); ctx.lineTo(14,2); ctx.lineTo(12,14); ctx.closePath();
  ctx.fill();
  /* eyes */
  ctx.fillStyle = '#ff5050';
  ctx.shadowColor = '#ff5050'; ctx.shadowBlur = 8;
  ctx.fillRect(-5, -4, 3, 3); ctx.fillRect(2, -4, 3, 3);
  ctx.shadowBlur = 0;
  ctx.restore();
}
function drawExit(cx, cy, zone, tx, ty, map){
  const ctx = ctx2d;
  const t = performance.now()/1000;
  ctx.save(); ctx.translate(cx, cy);
  const r = 12 + Math.sin(t*2)*2;
  const g = ctx.createRadialGradient(0,0,1,0,0,r);
  g.addColorStop(0, 'rgba(255,245,200,0.6)');
  g.addColorStop(0.5, 'rgba(201,164,255,0.3)');
  g.addColorStop(1, 'rgba(0,0,0,0)');
  ctx.fillStyle = g; ctx.beginPath(); ctx.arc(0,0,r,0,Math.PI*2); ctx.fill();
  ctx.fillStyle = '#fff5d6';
  ctx.beginPath(); ctx.arc(0,0,4,0,Math.PI*2); ctx.fill();
  /* arrow */
  ctx.strokeStyle = '#fff5d6';
  ctx.lineWidth = 1.5;
  ctx.beginPath();
  ctx.moveTo(-6, 0); ctx.lineTo(6, 0);
  ctx.moveTo(3, -3); ctx.lineTo(6, 0); ctx.lineTo(3, 3);
  ctx.stroke();
  ctx.restore();
}

/* ===== OVERWORLD UPDATE ===== */
function updateOverworld(dt){
  if(Game.paused) return;
  if(Game.modal) return;
  /* decrement encounter cooldown */
  if(Game.encounterCooldown > 0) Game.encounterCooldown -= dt;
  if(Game.repelCount > 0) Game.repelCount -= dt;
  /* movement */
  let dx=0, dy=0;
  if(Input.isDown('arrowup') || Input.isDown('w')){ dy=-1; Game.player.dir=0; }
  else if(Input.isDown('arrowdown') || Input.isDown('s')){ dy=1; Game.player.dir=2; }
  else if(Input.isDown('arrowleft') || Input.isDown('a')){ dx=-1; Game.player.dir=3; }
  else if(Input.isDown('arrowright') || Input.isDown('d')){ dx=1; Game.player.dir=1; }
  const speed = 5; /* tiles per sec */
  if(dx||dy){
    Game.player.moveAccum += dt * speed;
    const steps = Math.floor(Game.player.moveAccum);
    Game.player.moveAccum -= steps;
    Game.player.stepPhase = (Game.player.stepPhase + dt*speed) % 1;
    let nx = Game.player.x, ny = Game.player.y;
    for(let s=0; s<steps; s++){
      const tx = nx+dx, ty = ny+dy;
      if(canWalk(tx, ty)){
        nx = tx; ny = ty;
        onStep(nx, ny);
      } else {
        break;
      }
    }
    Game.player.x = nx; Game.player.y = ny;
  } else {
    Game.player.stepPhase = 0;
  }
  /* action */
  if(Input.consume('e') || Input.consume(' ')){
    Audio.sfx.select();
    tryInteract();
  }
  /* pause */
  if(Input.consume('escape')){
    openPause();
  }
  /* bag */
  if(Input.consume('b')){
    openBag();
  }
}
function canWalk(x, y){
  const map = getMap(Game.zone);
  const t = tileAt(map, x, y);
  return t !== '#' && t !== '~';
}
function onStep(x, y){
  const map = getMap(Game.zone);
  const t = tileAt(map, x, y);
  if(t === ','){
    Game.stepsInGrass++;
    if(Game.encounterCooldown <= 0 && Game.repelCount <= 0 && Game.party.length){
      const zone = ZONES[Game.zone];
      if(chance(zone.rate)){
        triggerEncounter();
      }
    }
  }
  if(t === 'E'){
    /* zone exit: handled via interaction (E to advance) */
  }
}
function tryInteract(){
  const {x, y} = Game.player;
  const dirs = [[0,-1],[0,1],[-1,0],[1,0]];
  for(const [dx,dy] of dirs){
    const tx = x+dx, ty = y+dy;
    if(tx<0||ty<0) continue;
    const map = getMap(Game.zone);
    const t = tileAt(map, tx, ty);
    if(t === 'S'){ openShrine(); return; }
    if(t === 'N'){ openNPC(tx, ty); return; }
    if(t === 'G'){ openGuardian(tx, ty); return; }
    if(t === 'E'){ advanceZone(); return; }
  }
  /* also: action on current tile */
  const t = tileAt(getMap(Game.zone), x, y);
  if(t === 'S'){ openShrine(); return; }
  if(t === 'N'){ openNPC(x, y); return; }
  if(t === 'G'){ openGuardian(x, y); return; }
  if(t === 'E'){ advanceZone(); return; }
}
function advanceZone(){
  const z = ZONES[Game.zone];
  if(!z.next) return;
  if(!Game.zoneUnlocked[z.next]){ Game.notify('A barrier of Veil-light holds you back. The Guardian of '+z.name+' must fall.'); return; }
  Game.zone = z.next;
  Game.player.x = 1; Game.player.y = 1;
  Particles.setAmbient(Game.zone);
  Audio.startMusic(z.id === 'sanctum' ? 'shrine' : 'overworld');
  Game.notify('Entered '+ZONES[Game.zone].name+'!');
}
function openNPC(x, y){ showDialog({
  who:'Wandering Veil-Keeper',
  what: zoneTip(Game.zone),
  onClose: ()=>{ Game.paused = false; }
}); Game.paused = true; }
function zoneTip(z){
  switch(z){
    case 'glades': return '"The Cinder Glades are warm at the knee and cool at the heart. Listen for the hum of stones."';
    case 'cove': return '"The Coves will sing you a song. The first one is always a welcome, the second is a warning."';
    case 'thicket': return '"Don\'t pull a vine. The vines pull back."';
    case 'spires': return '"Don\'t chase the lightning. Walk under it instead."';
    case 'crags': return '"A door in the mountain opens only once. Bring no fear."';
    case 'sanctum': return '"The first dawn is patient. It has waited this long; it can wait for you."';
  }
  return '...';
}
function openShrine(){
  /* heal party */
  Audio.sfx.shrine();
  let healed = 0;
  for(const bc of Game.party){ if(bc.hp < bc.maxHp) healed += (bc.maxHp - bc.hp); bc.hp = bc.maxHp; bc.status = {}; bc.atkMod=0; bc.defMod=0; bc.spdMod=0; bc.accMod=0; }
  Game.notify('Shrine restores the party.');
  /* open shop + save prompt */
  setTimeout(()=>{
    openShop();
  }, 600);
  /* auto-save */
  saveGame(0);
}
function openShop(){
  const modal = document.createElement('div');
  modal.className = 'modal-back';
  modal.innerHTML = `<div class="modal" style="max-width:520px">
    <h2>Shrine of the Veil</h2>
    <div class="sub">Rest, purchase supplies, and continue your journey.</div>
    <div class="body">
      <div class="chip pill"><b>${Game.coins}</b> aether</div>
      <div class="divider"></div>
      <div class="small">You can buy consumables, capture items, and catalysts here.</div>
      <div id="shop-list" class="col gap-8"></div>
    </div>
    <div class="foot">
      <button class="btn tiny" data-act="save">Save</button>
      <button class="btn primary" data-act="close">Continue</button>
    </div>
  </div>`;
  document.getElementById('ui').appendChild(modal);
  const list = modal.querySelector('#shop-list');
  const offerings = [
    {id:'potion', qty:1, price:60},
    {id:'superPotion', qty:1, price:240},
    {id:'revive', qty:1, price:300},
    {id:'wispOrb', qty:1, price:40},
    {id:'gleamOrb', qty:1, price:160},
    {id:'lattice', qty:1, price:800},
    {id:'repel', qty:1, price:160},
    {id:'cinderStone', qty:1, price:500},
    {id:'verdantMoss', qty:1, price:500},
    {id:'voltCoil', qty:1, price:500},
    {id:'terraShard', qty:1, price:500},
    {id:'lumenPrism', qty:1, price:700},
    {id:'umbralAsh', qty:1, price:700}
  ];
  for(const o of offerings){
    const it = ITEMS[o.id];
    const row = document.createElement('div');
    row.className = 'row';
    row.style.justifyContent = 'space-between';
    row.style.background = 'rgba(255,255,255,0.04)';
    row.style.border = '1px solid var(--line)';
    row.style.borderRadius = '10px';
    row.style.padding = '6px 10px';
    row.innerHTML = `<div><b>${it.name}</b><div class="small">${it.desc}</div></div>
      <div class="row gap-8"><span class="small">${o.price} aether</span><button class="btn tiny">Buy</button></div>`;
    row.querySelector('button').addEventListener('click', ()=>{
      if(Game.coins < o.price){ Audio.sfx.notEff?.(); Game.notify('Not enough aether.'); return; }
      Game.coins -= o.price;
      Game.bag[o.id] = (Game.bag[o.id]||0) + 1;
      Audio.sfx.confirm();
      Game.notify('Acquired ' + it.name + '.');
      modal.querySelector('.chip b').textContent = Game.coins;
    });
    list.appendChild(row);
  }
  modal.addEventListener('click', e => {
    const a = e.target.dataset?.act;
    if(a === 'close'){ Audio.sfx.confirm(); modal.remove(); Game.paused = false; }
    if(a === 'save'){ saveGame(0); Game.notify('Game saved.'); Audio.sfx.confirm(); }
  });
  Game.paused = true;
}
function openGuardian(x, y){
  const z = ZONES[Game.zone];
  Audio.startMusic('battle');
  Game.state = 'battle';
  startBattle({ boss:true, zone:Game.zone, story:z.boss.story });
}
function triggerEncounter(){
  const zone = ZONES[Game.zone];
  if(!zone.wilds.length) return;
  /* weighted pick */
  const tot = zone.wilds.reduce((s,w)=>s+w.w,0);
  let r = rnd()*tot, picked = zone.wilds[0].id;
  for(const w of zone.wilds){ if(r < w.w){ picked = w.id; break; } r -= w.w; }
  Game.encounterCooldown = 1.2;
  Game.state = 'battle';
  Audio.startMusic('battle');
  const lvl = rint(zone.minLv, zone.maxLv);
  startBattle({ wild:true, creatures:[{id:picked, level:lvl}], zone:Game.zone });
}

/* ===== BATTLE ===== */
const Battle = {
  enemies: [],
  playerActive: 0,
  turnQueue: [],
  turnIndex: 0,
  busy: false,
  log: [],
  phase: 'choose', /* choose | execute | between | capture | over | switch */
  result: null,
  pending: null,
  captureTarget: null,
  bubbleText: '',
  bubbleTimer: 0,
  shake: 0,
  flash: null,
  damageNumbers: [],
  switchMode: false,
  crit: false,
  vignetteColor: null,
  vignetteTimer: 0,
  start(opts){
    this.enemies = [];
    this.playerActive = Game.party.findIndex(c=>c.hp>0);
    if(this.playerActive<0) this.playerActive = 0;
    if(opts.boss){
      const z = ZONES[opts.zone];
      const id = z.boss.id;
      const lvl = z.boss.level;
      const bc = makeBattleCreature(id, lvl);
      bc.boss = true;
      bc.maxHp = Math.floor(bc.maxHp * 1.2);
      bc.hp = bc.maxHp;
      bc.atk = Math.floor(bc.atk * 1.15);
      bc.def = Math.floor(bc.def * 1.1);
      this.enemies.push(bc);
      this.bossStory = z.boss.story;
    } else {
      const count = chance(0.25) ? 2 : 1;
      for(const w of opts.creatures){
        const bc = makeBattleCreature(w.id, w.level);
        this.enemies.push(bc);
        if(this.enemies.length>=count) break;
      }
      if(!this.enemies.length) this.enemies.push(makeBattleCreature(opts.creatures[0].id, opts.creatures[0].level));
    }
    /* mark codex */
    for(const e of this.enemies){
      if(!Game.codex[e.id]) Game.codex[e.id] = {seen:true};
    }
    this.turnQueue = [];
    for(let i=0;i<Game.party.length;i++) if(Game.party[i].hp>0) this.turnQueue.push({side:'p', idx:i});
    for(let i=0;i<this.enemies.length;i++) if(this.enemies[i].hp>0) this.turnQueue.push({side:'e', idx:i});
    this.turnQueue.sort((a,b) => {
      const A = a.side==='p' ? Game.party[a.idx] : this.enemies[a.idx];
      const B = b.side==='p' ? Game.party[b.idx] : this.enemies[b.idx];
      return B.spd - A.spd;
    });
    this.turnIndex = 0;
    this.busy = false;
    this.phase = 'between';
    this.bubbleText = 'A wild encounter!';
    this.bubbleTimer = 1.2;
    this.damageNumbers = [];
    this.result = null;
    this.captureTarget = null;
    this.pending = null;
    this.bossStory = null;
    this._endPending = false;
  },
  activeCreature(){
    const t = this.turnQueue[this.turnIndex];
    if(!t) return null;
    return t.side === 'p' ? Game.party[t.idx] : this.enemies[t.idx];
  },
  currentMove(){ return this.pending; },
  setMove(idx){
    const bc = Game.party[this.playerActive];
    if(!bc || !bc.moves[idx]) return;
    this.pending = bc.moves[idx];
    this.bubbleText = bc.name + ' used ' + ABILITIES[this.pending].name + '?';
    this.bubbleTimer = 0.6;
  },
  switchTo(idx){
    if(idx<0||idx>=Game.party.length) return;
    if(idx === this.playerActive){ this.phase='choose'; return; }
    if(Game.party[idx].hp<=0){ Audio.sfx.notEff?.(); return; }
    /* free switch */
    Game.party[this.playerActive].atkMod=0; Game.party[this.playerActive].defMod=0; Game.party[this.playerActive].spdMod=0;
    this.playerActive = idx;
    this.bubbleText = 'Go, '+Game.party[idx].name+'!';
    this.bubbleTimer = 0.8;
    Audio.sfx.confirm();
    this.phase='between'; this.busy=true;
    this.pending = { switch:true, idx };
    setTimeout(()=>this.executeTurn(), 500);
  },
  executeTurn(){
    const t = this.turnQueue[this.turnIndex];
    if(!t){ this.checkEnd(); return; }
    if(t.side === 'e'){
      /* enemy AI */
      const e = this.enemies[t.idx];
      if(!e || e.hp<=0){ this.nextTurn(); return; }
      const target = this.pickEnemyTarget();
      const move = this.chooseEnemyMove(e);
      this.attack(e, target, move, ()=>this.nextTurn());
    } else {
      /* player turn: pending set by menu */
      if(this.pending && this.pending.switch){
        this.nextTurn();
      } else if(this.pending){
        const attacker = Game.party[this.playerActive];
        if(!attacker || attacker.hp<=0){ this.nextTurn(); return; }
        const move = this.pending;
        const target = this.pickPlayerTarget(move);
        if(!target){ this.phase='choose'; return; }
        this.attack(attacker, target, move, ()=>this.nextTurn());
      } else {
        this.phase='choose';
      }
    }
  },
  chooseEnemyMove(e){
    const learnset = CREATURES[e.id].learnset.map(([k])=>k).filter(k=>ABILITIES[k] && e.abilityPp[k]>0);
    const choices = learnset.length ? learnset : (e.moves || []).filter(m=>e.abilityPp[m]>0);
    if(!choices.length) return null;
    return pick(choices);
  },
  pickEnemyTarget(){
    const alive = Game.party.map((c,i)=>({c,i})).filter(o=>o.c.hp>0);
    if(!alive.length) return null;
    return alive[0].i;
  },
  pickPlayerTarget(move){
    /* single-target damage: pick lowest HP% enemy */
    const move_ = ABILITIES[move];
    if(move_.power === 0) return this.playerActive; /* self */
    const alive = this.enemies.map((c,i)=>({c,i})).filter(o=>o.c.hp>0);
    if(!alive.length) return 0;
    alive.sort((a,b)=>a.c.hp/a.c.maxHp - b.c.hp/b.c.maxHp);
    return alive[0].i;
  },
  attack(attacker, targetIdx, moveKey, done){
    this.phase='execute'; this.busy=true;
    const move = ABILITIES[moveKey];
    if(!move){ done(); return; }
    const isEnemy = this.turnQueue[this.turnIndex].side==='e';
    const target = isEnemy ? Game.party[targetIdx] : this.enemies[targetIdx];
    if(!target || target.hp<=0){ done(); return; }
    attacker.abilityPp[moveKey]--;
    /* accuracy check */
    if(move.acc < 100 && rnd()*100 > move.acc){
      this.bubbleText = attacker.name + '\'s '+move.name+' missed!';
      this.bubbleTimer = 1.0;
      Audio.sfx.notEff?.();
      setTimeout(done, 700);
      return;
    }
    /* priority effects */
    const isPriority = move.effect && move.effect.kind === 'priority';
    /* play SFX */
    if(move.type) Audio.sfx[move.type.toLowerCase()]?.();
    /* type matchup */
    let mult = 1;
    if(move.type){
      mult = (TYPE_CHART[move.type] && TYPE_CHART[move.type][target.type]) || 1;
      if(target.type2 && TYPE_CHART[move.type] && TYPE_CHART[move.type][target.type2] !== undefined){
        const m2 = TYPE_CHART[move.type][target.type2];
        mult = m2 > mult ? m2 : (mult > m2 ? mult : mult); /* keep higher */
      }
    }
    const stab = (move.type === attacker.type || (attacker.type2 && move.type === attacker.type2)) ? 1.25 : 1;
    const crit = chance(0.0625) ? 1.5 : 1;
    this.crit = crit>1;
    /* modifiers */
    let def = Math.max(1, target.def + (target.defMod||0)*2);
    let atk = Math.max(1, attacker.atk + (attacker.atkMod||0)*2);
    if(target.status?.frost) atk = Math.floor(atk*0.75);
    if(attacker.status?.burn) atk = Math.floor(atk*0.5);
    let variance = 0.85 + rnd()*0.30;
    let pierce = move.effect?.kind === 'pierce' ? 0.6 : 1;
    let dmg = (0.4 * attacker.level * atk / def * mult * stab * crit * variance * pierce);
    if(move.power === 0) dmg = 0;
    /* multi-hit */
    const hits = (move.effect?.kind === 'multihit') ? move.effect.hits : 1;
    let totalDmg = 0;
    let i = 0;
    const doHit = () => {
      if(i>=hits || target.hp<=0){ afterAll(); return; }
      const t = i;
      const hitDmg = Math.max(1, Math.floor(dmg));
      target.hp = Math.max(0, target.hp - hitDmg);
      totalDmg += hitDmg;
      this.damageNumbers.push({ x: 0, y:0, value:hitDmg, type: move.type, mult, crit: crit>1, t:1.0, side: isEnemy?'p':'e', target: targetIdx });
      Audio.sfx.hit();
      this.shake = 0.18;
      if(crit>1 && t===0){ this.flash = 'crit'; Audio.sfx.crit(); }
      if(mult>1 && t===0){ this.flash = 'hit'; }
      if(mult<1 && t===0){ this.flash='hit'; }
      i++;
      setTimeout(doHit, 220);
    };
    const afterAll = () => {
      /* status effects */
      if(target.hp>0 && move.effect){
        const e = move.effect;
        if(e.kind === 'burn' && chance(e.chance/100)) this.applyStatus(target, 'burn');
        if(e.kind === 'frost' && chance(e.chance/100)) this.applyStatus(target, 'frost');
        if(e.kind === 'shock' && chance(e.chance/100)) this.applyStatus(target, 'shock');
        if(e.kind === 'barrier') this.applyStatus(target, 'barrier');
        if(e.kind === 'regen') this.applyStatus(target, 'regen', e.turns);
        if(e.kind === 'heal'){
          const amt = Math.floor(attacker.maxHp * e.pct/100);
          attacker.hp = Math.min(attacker.maxHp, attacker.hp+amt);
          this.bubbleText = attacker.name + ' restored '+amt+' HP!';
          this.bubbleTimer = 1.0;
          Audio.sfx.heal();
          this.flash='heal';
        }
        if(e.kind === 'atkUp'){ attacker.atkMod += e.delta; this.bubbleText = attacker.name + "'s ATK rose!"; this.bubbleTimer=0.9; }
        if(e.kind === 'defUp'){ target.defMod += e.delta; this.bubbleText = target.name + "'s DEF rose!"; this.bubbleTimer=0.9; }
        if(e.kind === 'defDown'){ if(chance(e.chance/100)){ target.defMod += e.delta; this.bubbleText = target.name + "'s DEF fell!"; this.bubbleTimer=0.9; } }
        if(e.kind === 'spdUp'){ attacker.spdMod += e.delta; this.bubbleText = attacker.name + "'s SPD rose!"; this.bubbleTimer=0.9; }
        if(e.kind === 'speed' && e.delta<0){ if(chance(e.chance/100)){ target.spdMod += e.delta; this.bubbleText = target.name + "'s SPD fell!"; this.bubbleTimer=0.9; } }
        if(e.kind === 'lifesteal'){
          const heal = Math.floor(totalDmg * e.pct/100);
          attacker.hp = Math.min(attacker.maxHp, attacker.hp+heal);
        }
        if(e.kind === 'recoil'){
          const rec = Math.floor(totalDmg * e.pct/100);
          attacker.hp = Math.max(1, attacker.hp - rec);
          this.bubbleText = attacker.name + ' is hurt by recoil!';
          this.bubbleTimer = 0.8;
        }
      }
      setTimeout(()=>{ if(target.hp<=0) this.onFaint(target); done(); }, 350);
    };
    setTimeout(doHit, 200);
  },
  applyStatus(target, kind, turns=4){
    target.status = target.status || {};
    if(target.status[kind]) return;
    target.status[kind] = turns;
    const map = {burn:'burned', frost:'frozen', shock:'shocked', barrier:'raised a barrier', regen:'is regenerating'};
    this.bubbleText = target.name + ' ' + (map[kind]||'was affected') + '!';
    this.bubbleTimer = 0.9;
  },
  onFaint(target){
    Audio.sfx.notEff?.();
    Particles.spawn(0,0,{count:14, color:'#fff', life:0.6, size:3, spd:120, grav:80, spread:Math.PI*2});
  },
  endOfTurnStatuses(actor){
    /* called at start of actor's turn */
    if(!actor.status) return false;
    let skip = false;
    for(const k in actor.status){
      if(k === 'burn'){
        const d = Math.max(1, Math.floor(actor.maxHp/16));
        actor.hp = Math.max(0, actor.hp - d);
        this.bubbleText = actor.name + ' is hurt by burn!';
        this.bubbleTimer = 0.8;
      }
      if(k === 'frost'){
        if(chance(0.25)) skip = true;
        this.bubbleText = actor.name + ' is frostbitten!';
        this.bubbleTimer = 0.7;
      }
      if(k === 'shock'){
        if(chance(0.30)){ skip = true; this.bubbleText = actor.name + ' is paralyzed!'; this.bubbleTimer=0.7; }
      }
      if(k === 'regen'){
        const d = Math.max(1, Math.floor(actor.maxHp/12));
        actor.hp = Math.min(actor.maxHp, actor.hp + d);
        this.bubbleText = actor.name + ' regenerates!';
        this.bubbleTimer = 0.7;
      }
      actor.status[k]--;
      if(actor.status[k] <= 0) delete actor.status[k];
    }
    return skip;
  },
  nextTurn(){
    this.turnIndex++;
    if(this.turnIndex >= this.turnQueue.length){
      /* round end: reset turn queue */
      this.turnQueue = [];
      for(let i=0;i<Game.party.length;i++) if(Game.party[i].hp>0) this.turnQueue.push({side:'p', idx:i});
      for(let i=0;i<this.enemies.length;i++) if(this.enemies[i].hp>0) this.turnQueue.push({side:'e', idx:i});
      this.turnQueue.sort((a,b) => {
        const A = a.side==='p' ? Game.party[a.idx] : this.enemies[a.idx];
        const B = b.side==='p' ? Game.party[b.idx] : this.enemies[b.idx];
        return B.spd - A.spd;
      });
      this.turnIndex = 0;
    }
    if(this.checkEnd()) return;
    const t = this.turnQueue[this.turnIndex];
    if(!t) return;
    const actor = t.side==='p' ? Game.party[t.idx] : this.enemies[t.idx];
    if(!actor || actor.hp<=0){ this.nextTurn(); return; }
    /* if barrier, set shielded */
    actor.shielded = !!actor.status?.barrier;
    if(actor.shielded && t.side==='p'){
      this.bubbleText = actor.name + ' is shielded!';
      this.bubbleTimer = 0.6;
    }
    if(t.side==='p'){
      this.playerActive = t.idx;
      const skip = this.endOfTurnStatuses(actor);
      if(skip){ this.bubbleText = actor.name + ' can\'t move!'; this.bubbleTimer = 0.8; this.pending={skip:true}; this.phase='execute'; setTimeout(()=>this.nextTurn(), 800); return; }
      this.phase='choose';
      this.pending=null;
      this.bubbleText = 'Choose an action.';
      this.bubbleTimer = 0.5;
    } else {
      const skip = this.endOfTurnStatuses(actor);
      if(skip){ this.pending={skip:true}; setTimeout(()=>this.nextTurn(), 800); return; }
      this.executeTurn();
    }
  },
  checkEnd(){
    const pAlive = Game.party.some(c=>c.hp>0);
    const eAlive = this.enemies.some(e=>e.hp>0);
    if(!pAlive){ this.result='lose'; this.phase='over'; return true; }
    if(!eAlive){ this.result='win'; this.phase='over'; return true; }
    return false;
  },
  onBattleEnd(){
    if(this.result === 'win'){
      /* exp, capture chance, codex */
      for(const e of this.enemies){
        const expGain = Math.floor(20 + e.level*8 + (e.boss?50:0));
        for(const bc of Game.party){ if(bc.hp>0){ bc.exp += expGain; let safety=20; while(bc.exp >= bc.expToNext && safety-->0) { bc.exp -= bc.expToNext; const ok = levelUp(bc); if(!ok) { bc.exp = 0; break; } Audio.sfx.level(); Game.notify(bc.name+' reached level '+bc.level+'!'); } } }
        Game.coins += 5 + e.level*2 + (e.boss?100:0);
        if(e.boss){
          Game.bossDefeated[Game.zone] = true;
          const z = ZONES[Game.zone];
          if(z.next && !Game.zoneUnlocked[z.next]){
            Game.zoneUnlocked[z.next] = true;
            Game.notify(z.name+' is pacified. A path opens onward.');
            if(z.boss.story) triggerStory(z.boss.story);
          }
          if(z.boss.final){
            triggerStory(4);
            setTimeout(()=>triggerFinale(), 800);
          }
        }
      }
      if(!this.wasWild){
        /* triggered by story */
      }
    } else if(this.result === 'lose'){
      /* wake at last shrine, half gold */
      Game.coins = Math.floor(Game.coins/2);
      Game.zone = Game.shrineSpawn;
      const p = findFirst(getMap(Game.zone), 'P');
      if(p){ Game.player.x = p.x; Game.player.y = p.y; }
      else { Game.player.x = 1; Game.player.y = 1; }
      for(const bc of Game.party){ bc.hp = bc.maxHp; bc.status = {}; }
      Game.notify('Woke at the last shrine.');
    }
    Game.state = 'overworld';
    Audio.startMusic(Game.zone === 'sanctum' ? 'shrine' : 'overworld');
    Particles.setAmbient(Game.zone);
  }
};
function startBattle(opts){
  Battle.wasWild = !!opts.wild;
  Battle.start(opts);
}

/* ===== BATTLE RENDER ===== */
function drawBattleScene(ctx, t){
  const zone = ZONES[Game.zone];
  /* background gradient */
  const grad = ctx.createLinearGradient(0,0,0,VH);
  grad.addColorStop(0, zone.gradient[0]);
  grad.addColorStop(1, zone.gradient[1]);
  ctx.fillStyle = grad;
  ctx.fillRect(0,0,VW,VH);
  /* soft ground bands */
  ctx.fillStyle = 'rgba(0,0,0,0.25)';
  ctx.beginPath();
  ctx.ellipse(VW*0.75, VH*0.7, 200, 36, 0, 0, Math.PI*2);
  ctx.fill();
  ctx.beginPath();
  ctx.ellipse(VW*0.25, VH*0.85, 220, 40, 0, 0, Math.PI*2);
  ctx.fill();
  /* radial highlight */
  const rg = ctx.createRadialGradient(VW*0.5, VH*0.4, 50, VW*0.5, VH*0.4, Math.max(VW,VH)*0.7);
  rg.addColorStop(0, zone.accent + '33');
  rg.addColorStop(1, '#00000000');
  ctx.fillStyle = rg; ctx.fillRect(0,0,VW,VH);
  /* draw enemies */
  for(let i=0;i<Battle.enemies.length;i++){
    const e = Battle.enemies[i];
    if(e.hp<=0) continue;
    const cx = VW*0.72 - (Battle.enemies.length-1)*60 + i*120;
    const cy = VH*0.45;
    drawCreatureSprite(ctx, e.id, cx, cy, 1.4, e.type, e.boss ? t*2 : t*1.2, e.hp/e.maxHp, e.boss);
  }
  /* draw player creature */
  const pc = Game.party[Battle.playerActive];
  if(pc && pc.hp>0){
    const cx = VW*0.28, cy = VH*0.62;
    drawCreatureSprite(ctx, pc.id, cx, cy, 1.3, pc.type, -t*1.2, pc.hp/pc.maxHp, false);
  }
  /* particles */
  Particles.draw(ctx);
  /* damage numbers */
  for(let i=Battle.damageNumbers.length-1;i>=0;i--){
    const d = Battle.damageNumbers[i];
    d.t -= 0.025;
    if(d.t<=0){ Battle.damageNumbers.splice(i,1); continue; }
    const x = d.side==='e' ? VW*0.72 : VW*0.28;
    const y = VH*0.45 + (1-d.t)*30;
    ctx.globalAlpha = d.t;
    ctx.font = 'bold ' + (d.crit?28:20) + 'px ui-sans-serif';
    ctx.textAlign = 'center';
    if(d.crit){
      ctx.fillStyle = '#ffd97a';
      ctx.strokeStyle = '#000'; ctx.lineWidth = 3;
      ctx.strokeText(d.value + '!', x, y);
      ctx.fillText(d.value + '!', x, y);
    } else {
      const c = d.mult > 1 ? '#7ee07a' : d.mult < 1 ? '#a3acd1' : '#fff';
      ctx.fillStyle = c;
      ctx.strokeStyle = '#000'; ctx.lineWidth = 2;
      ctx.strokeText(d.value, x, y);
      ctx.fillText(d.value, x, y);
    }
    ctx.globalAlpha = 1;
  }
  /* bubble text */
  if(Battle.bubbleText && Battle.bubbleTimer > 0){
    ctx.fillStyle = 'rgba(20,24,40,0.85)';
    const w = ctx.measureText(Battle.bubbleText).width + 32;
    ctx.fillStyle = 'rgba(20,24,40,0.92)';
    roundRect(ctx, VW/2 - w/2, 14, w, 36, 10);
    ctx.fillStyle = '#fff5d6';
    ctx.font = '14px ui-sans-serif';
    ctx.textAlign = 'center';
    ctx.fillText(Battle.bubbleText, VW/2, 38);
  }
  /* crit flash */
  if(Battle.flash === 'crit'){
    ctx.fillStyle = 'rgba(255,255,255,0.5)';
    ctx.fillRect(0,0,VW,VH);
    Battle.flash = null;
  } else if(Battle.flash === 'hit'){
    ctx.fillStyle = 'rgba(255,200,100,0.15)';
    ctx.fillRect(0,0,VW,VH);
    Battle.flash = null;
  } else if(Battle.flash === 'heal'){
    ctx.fillStyle = 'rgba(120,255,160,0.15)';
    ctx.fillRect(0,0,VW,VH);
    Battle.flash = null;
  }
  /* shake */
  if(Battle.shake > 0){
    Battle.shake -= 0.04;
  }
  /* cards */
  drawBattleCards(ctx);
  /* capture overlay */
  if(Battle.phase === 'capture' || Battle.captureTarget){
    drawCapture(ctx, t);
  }
  /* turn banner */
  if(Battle.phase === 'between' && !Battle.busy){
    /* hint */
  }
}
function drawBattleCards(ctx){
  /* enemy card */
  for(let i=0;i<Battle.enemies.length;i++){
    const e = Battle.enemies[i];
    if(e.hp<=0) continue;
    const x = VW*0.55 + i*100, y = 60;
    ctx.fillStyle = 'rgba(20,24,40,0.7)';
    roundRect(ctx, x, y, 200, 70, 10); ctx.fill();
    ctx.strokeStyle = TYPE_COLORS[e.type]; ctx.lineWidth = 2;
    roundRect(ctx, x, y, 200, 70, 10); ctx.stroke();
    ctx.fillStyle = '#fff'; ctx.font = 'bold 14px ui-sans-serif'; ctx.textAlign='left';
    ctx.fillText(e.name + (e.boss?' ★':''), x+10, y+22);
    ctx.fillStyle = '#a3acd1'; ctx.font = '10px ui-sans-serif';
    ctx.fillText('Lv ' + e.level + ' · ' + e.type, x+10, y+38);
    /* hp bar */
    const pct = Math.max(0, e.hp/e.maxHp);
    ctx.fillStyle = 'rgba(0,0,0,0.5)';
    roundRect(ctx, x+10, y+44, 180, 8, 4); ctx.fill();
    ctx.fillStyle = pct > 0.5 ? '#7ee07a' : pct > 0.25 ? '#f5c542' : '#ff6b6b';
    roundRect(ctx, x+10, y+44, 180*pct, 8, 4); ctx.fill();
    /* status icons */
    if(e.status){
      let sx = x+10;
      for(const k in e.status){
        ctx.fillStyle = '#ffd97a';
        ctx.font = '9px ui-sans-serif';
        ctx.fillText(k.toUpperCase().slice(0,3), sx, y+62);
        sx += 30;
      }
    }
  }
  /* player card */
  for(let i=0;i<Game.party.length;i++){
    const bc = Game.party[i];
    const x = 14 + i*100, y = VH-150;
    ctx.fillStyle = 'rgba(20,24,40,0.7)';
    roundRect(ctx, x, y, 92, 80, 10); ctx.fill();
    if(i === Battle.playerActive){
      ctx.strokeStyle = '#fff5d6'; ctx.lineWidth = 2;
      roundRect(ctx, x, y, 92, 80, 10); ctx.stroke();
    }
    ctx.fillStyle = '#fff'; ctx.font = 'bold 11px ui-sans-serif'; ctx.textAlign='left';
    ctx.fillText(bc.name.slice(0,8), x+6, y+16);
    ctx.fillStyle = '#a3acd1'; ctx.font = '9px ui-sans-serif';
    ctx.fillText('Lv'+bc.level, x+72, y+16);
    const pct = Math.max(0, bc.hp/bc.maxHp);
    ctx.fillStyle = 'rgba(0,0,0,0.5)';
    roundRect(ctx, x+6, y+22, 80, 6, 3); ctx.fill();
    ctx.fillStyle = bc.hp>0 ? (pct>0.5?'#7ee07a':pct>0.25?'#f5c542':'#ff6b6b') : '#444';
    roundRect(ctx, x+6, y+22, 80*pct, 6, 3); ctx.fill();
    ctx.fillStyle = '#a3acd1'; ctx.font='8px ui-sans-serif';
    ctx.fillText(bc.hp + '/' + bc.maxHp, x+6, y+38);
    /* exp */
    ctx.fillStyle = 'rgba(0,0,0,0.5)';
    roundRect(ctx, x+6, y+44, 80, 4, 2); ctx.fill();
    ctx.fillStyle = '#c9a4ff';
    roundRect(ctx, x+6, y+44, 80*(bc.exp/bc.expToNext), 4, 2); ctx.fill();
    /* status */
    if(bc.status){
      let sx = x+6;
      for(const k in bc.status){
        ctx.fillStyle = '#ffd97a'; ctx.font='8px ui-sans-serif';
        ctx.fillText(k.toUpperCase().slice(0,2), sx, y+58);
        sx += 18;
      }
    }
    if(bc.hp<=0){
      ctx.fillStyle = 'rgba(0,0,0,0.6)';
      roundRect(ctx, x, y, 92, 80, 10); ctx.fill();
      ctx.fillStyle = '#ff6b6b'; ctx.font='bold 12px ui-sans-serif';
      ctx.textAlign='center';
      ctx.fillText('FAINT', x+46, y+45);
      ctx.textAlign='left';
    }
  }
}
function roundRect(ctx, x, y, w, h, r){
  ctx.beginPath();
  ctx.moveTo(x+r,y);
  ctx.lineTo(x+w-r,y); ctx.quadraticCurveTo(x+w,y,x+w,y+r);
  ctx.lineTo(x+w,y+h-r); ctx.quadraticCurveTo(x+w,y+h,x+w-r,y+h);
  ctx.lineTo(x+r,y+h); ctx.quadraticCurveTo(x,y+h,x,y+h-r);
  ctx.lineTo(x,y+r); ctx.quadraticCurveTo(x,y,x+r,y);
  ctx.closePath();
}
function drawCreatureSprite(ctx, id, cx, cy, scale, type, anim, hpPct, boss){
  ctx.save();
  ctx.translate(cx, cy + Math.sin(anim)*4);
  const s = 28 * scale;
  const c = TYPE_COLORS[type] || '#fff';
  const g = TYPE_GLOW[type] || '#fff';
  /* aura */
  const auraR = s*1.6 + Math.sin(anim*0.5)*4;
  const aura = ctx.createRadialGradient(0,0,1,0,0,auraR);
  aura.addColorStop(0, c + '66');
  aura.addColorStop(1, c + '00');
  ctx.fillStyle = aura; ctx.beginPath(); ctx.arc(0,0,auraR,0,Math.PI*2); ctx.fill();
  /* body */
  ctx.fillStyle = shade(c, -0.3);
  ctx.beginPath();
  ctx.ellipse(0, 6, s*0.9, s*0.8, 0, 0, Math.PI*2); ctx.fill();
  /* head */
  ctx.fillStyle = c;
  ctx.beginPath(); ctx.arc(0, -s*0.3, s*0.7, 0, Math.PI*2); ctx.fill();
  /* eyes */
  ctx.fillStyle = '#fff';
  ctx.beginPath(); ctx.arc(-s*0.25, -s*0.35, s*0.12, 0, Math.PI*2); ctx.fill();
  ctx.beginPath(); ctx.arc(s*0.25, -s*0.35, s*0.12, 0, Math.PI*2); ctx.fill();
  ctx.fillStyle = '#000';
  ctx.beginPath(); ctx.arc(-s*0.22, -s*0.32, s*0.06, 0, Math.PI*2); ctx.fill();
  ctx.beginPath(); ctx.arc(s*0.28, -s*0.32, s*0.06, 0, Math.PI*2); ctx.fill();
  /* mouth */
  ctx.strokeStyle = '#000'; ctx.lineWidth = 2;
  ctx.beginPath();
  ctx.arc(0, -s*0.1, s*0.2, 0.1*Math.PI, 0.9*Math.PI); ctx.stroke();
  /* horn/spike top */
  ctx.fillStyle = g;
  ctx.beginPath();
  ctx.moveTo(-s*0.2, -s*0.9);
  ctx.lineTo(0, -s*1.3 - Math.abs(Math.sin(anim*0.7))*4);
  ctx.lineTo(s*0.2, -s*0.9);
  ctx.closePath(); ctx.fill();
  /* small details by type */
  if(type === 'Cinder'){
    ctx.fillStyle = '#ffd97a';
    for(let i=0;i<3;i++){
      const a = anim*0.5 + i*Math.PI*2/3;
      const fx = Math.cos(a)*s*0.5, fy = -s*0.3 + Math.sin(a*2)*s*0.2;
      ctx.beginPath(); ctx.arc(fx, fy, 2, 0, Math.PI*2); ctx.fill();
    }
  } else if(type === 'Tide'){
    ctx.fillStyle = '#9be8ff';
    for(let i=0;i<3;i++){
      ctx.beginPath(); ctx.arc(-s*0.6+i*s*0.6, s*0.5+Math.sin(anim+i)*2, 2, 0, Math.PI*2); ctx.fill();
    }
  } else if(type === 'Verdant'){
    ctx.fillStyle = '#c5f0a5';
    ctx.beginPath();
    ctx.ellipse(-s*0.6, -s*0.6, 4, 8, 0.5, 0, Math.PI*2); ctx.fill();
    ctx.beginPath();
    ctx.ellipse(s*0.55, -s*0.5, 4, 8, -0.3, 0, Math.PI*2); ctx.fill();
  } else if(type === 'Volt'){
    ctx.strokeStyle = '#fff5a8'; ctx.lineWidth = 1.5;
    for(let i=0;i<2;i++){
      const x1 = (rnd()-0.5)*s*0.8, y1 = -s*0.2;
      ctx.beginPath();
      ctx.moveTo(x1, y1);
      ctx.lineTo(x1 + (rnd()-0.5)*10, y1-8);
      ctx.lineTo(x1 + (rnd()-0.5)*14, y1-4);
      ctx.stroke();
    }
  } else if(type === 'Terra'){
    ctx.fillStyle = '#e8c79a';
    ctx.fillRect(-s*0.6, s*0.2, 6, 4);
    ctx.fillRect(s*0.3, s*0.4, 5, 5);
  } else if(type === 'Lumen'){
    ctx.fillStyle = '#fff8e7';
    for(let i=0;i<6;i++){
      const a = anim + i*Math.PI*2/6;
      const fx = Math.cos(a*2)*s*0.5, fy = Math.sin(a*2)*s*0.5;
      ctx.beginPath(); ctx.arc(fx, fy, 1.5, 0, Math.PI*2); ctx.fill();
    }
  } else if(type === 'Umbral'){
    ctx.fillStyle = 'rgba(20,10,40,0.5)';
    ctx.beginPath();
    ctx.arc(s*0.5, s*0.2, s*0.3, 0, Math.PI*2); ctx.fill();
  }
  /* boss crown */
  if(boss){
    ctx.fillStyle = '#ffd97a';
    ctx.beginPath();
    ctx.moveTo(-s*0.5, -s*0.7);
    ctx.lineTo(-s*0.3, -s*1.0);
    ctx.lineTo(-s*0.1, -s*0.7);
    ctx.lineTo(s*0.1, -s*1.0);
    ctx.lineTo(s*0.3, -s*0.7);
    ctx.lineTo(s*0.5, -s*1.0);
    ctx.lineTo(s*0.5, -s*0.6);
    ctx.lineTo(-s*0.5, -s*0.6);
    ctx.closePath(); ctx.fill();
  }
  /* hp bar above */
  if(hpPct < 1){
    ctx.fillStyle = 'rgba(0,0,0,0.6)';
    roundRect(ctx, -s*0.8, -s*1.5, s*1.6, 6, 3); ctx.fill();
    ctx.fillStyle = hpPct > 0.5 ? '#7ee07a' : hpPct > 0.25 ? '#f5c542' : '#ff6b6b';
    roundRect(ctx, -s*0.8, -s*1.5, s*1.6*Math.max(0,hpPct), 6, 3); ctx.fill();
  }
  ctx.restore();
}
function drawCapture(ctx, t){
  ctx.fillStyle = 'rgba(0,0,0,0.5)';
  ctx.fillRect(0,0,VW,VH);
  ctx.fillStyle = 'rgba(255,200,80,0.08)';
  ctx.fillRect(0,0,VW,VH);
  /* ring */
  const cx = VW/2, cy = VH*0.42;
  const r = 100;
  ctx.save(); ctx.translate(cx, cy);
  ctx.strokeStyle = 'rgba(255,255,255,0.4)';
  ctx.lineWidth = 4;
  ctx.beginPath(); ctx.arc(0,0,r,0,Math.PI*2); ctx.stroke();
  ctx.strokeStyle = '#ffd97a';
  ctx.lineWidth = 4;
  for(let i=0;i<3;i++){
    const a = (t*1.5 + i*Math.PI*2/3);
    ctx.beginPath();
    ctx.arc(0,0,r, a, a+1.2);
    ctx.stroke();
  }
  ctx.strokeStyle = '#fff5d6';
  ctx.beginPath();
  ctx.arc(0,0,r, t*2, t*2+1.5);
  ctx.stroke();
  /* target moves in circle */
  const target = Battle.captureTarget;
  if(target){
    const tx = Math.cos(t*1.8) * (r - 12);
    const ty = Math.sin(t*1.8) * (r - 12);
    ctx.fillStyle = '#fff5d6';
    ctx.beginPath(); ctx.arc(tx, ty, 10, 0, Math.PI*2); ctx.fill();
    ctx.fillStyle = 'rgba(255,245,200,0.5)';
    ctx.beginPath(); ctx.arc(tx, ty, 18, 0, Math.PI*2); ctx.fill();
  }
  ctx.restore();
  /* meter */
  const mY = VH*0.7;
  ctx.fillStyle = 'rgba(20,24,40,0.85)';
  roundRect(ctx, VW/2 - 130, mY - 12, 260, 24, 12); ctx.fill();
  if(target){
    const fill = target.captureFill || 0;
    ctx.fillStyle = '#ffd97a';
    roundRect(ctx, VW/2 - 124, mY - 6, 248 * (fill/100), 12, 6); ctx.fill();
  }
  ctx.fillStyle = '#fff5d6';
  ctx.font = 'bold 16px ui-sans-serif';
  ctx.textAlign = 'center';
  ctx.fillText('TAP TO ATTEMPT COLLECTION', VW/2, mY - 22);
  ctx.font = '12px ui-sans-serif';
  ctx.fillStyle = '#a3acd1';
  ctx.fillText('Press E, Space, or click to throw your orb', VW/2, mY + 30);
}

/* ===== BATTLE INPUT + UI ===== */
const BattleUI = {
  root: null,
  mode: 'main', /* main | moves | switch | bag | pick */
  init(){
    if(this.root) return;
    const d = document.createElement('div');
    d.id = 'battle-ui';
    d.style.cssText = 'position:absolute;left:0;right:0;bottom:0;padding:14px;display:flex;flex-direction:column;gap:8px;align-items:center;z-index:45;pointer-events:none;';
    d.innerHTML = `
      <div id="battle-action-grid" style="display:grid;grid-template-columns:repeat(2,minmax(140px,220px));gap:8px;width:100%;max-width:520px;pointer-events:auto"></div>
      <div id="battle-move-grid" class="move-list" style="display:none;width:100%;max-width:520px;pointer-events:auto;background:rgba(20,24,40,0.9);border:1px solid var(--line);border-radius:14px;padding:8px"></div>
    `;
    document.getElementById('ui').appendChild(d);
    this.root = d;
  },
  show(){
    this.init();
    this.root.style.display = 'flex';
  },
  hide(){
    if(this.root) this.root.style.display = 'none';
    this.mode = 'main';
  },
  render(){
    this.init();
    const act = this.root.querySelector('#battle-action-grid');
    const mov = this.root.querySelector('#battle-move-grid');
    const phase = Battle.phase;
    const bc = Game.party[Battle.playerActive];
    if(phase === 'choose' && bc && bc.hp>0){
      if(this.mode === 'main'){
        act.style.display = 'grid';
        mov.style.display = 'none';
        act.innerHTML = `
          <button class="btn primary" data-act="fight">Fight</button>
          <button class="btn" data-act="bag">Bag</button>
          <button class="btn" data-act="switch">Switch</button>
          <button class="btn" data-act="run">${Battle.wasWild ? 'Flee' : 'Run'}</button>
        `;
        act.querySelectorAll('button').forEach(b => {
          b.addEventListener('click', () => this.onAction(b.dataset.act));
        });
      }
    } else if(phase === 'execute' || phase === 'over'){
      act.style.display = 'none';
      mov.style.display = 'none';
      this.mode = 'main';
    } else {
      act.style.display = 'none';
      /* keep mov display based on mode */
      if(this.mode === 'main') mov.style.display = 'none';
    }
  },
  showMoves(){
    const act = this.root.querySelector('#battle-action-grid');
    const mov = this.root.querySelector('#battle-move-grid');
    act.style.display = 'none';
    mov.style.display = 'grid';
    this.mode = 'moves';
    const bc = Game.party[Battle.playerActive];
    if(!bc){ return; }
    mov.innerHTML = '';
    bc.moves.forEach((mv, i) => {
      const a = ABILITIES[mv];
      const tile = document.createElement('div');
      tile.className = 'move-tile' + (bc.abilityPp[mv]<=0 ? ' disabled' : '');
      const ppClass = bc.abilityPp[mv]<=0 ? 'bad' : '';
      const typeTag = a.type ? `<span class="tag type-${a.type.toLowerCase()}">${a.type}</span>` : '<span class="tag">—</span>';
      const effTag = a.effect && a.effect.kind ? `<span class="tag ${a.effect.kind==='heal'||a.effect.kind==='regen'||a.effect.kind==='atkUp'||a.effect.kind==='defUp'||a.effect.kind==='spdUp'?'buff':a.effect.kind==='recoil'||a.effect.kind==='defDown'||a.effect.kind==='speed'?'debuff':'status'}">${a.effect.kind}</span>` : '';
      const pri = a.effect && a.effect.kind==='priority' ? '<span class="tag priority">fast</span>' : '';
      tile.innerHTML = `
        <div class="nm">${a.name} <span class="pp ${ppClass}">${bc.abilityPp[mv]}/${a.pp}</span></div>
        <div class="meta">${a.power?`PWR ${a.power}`:'—'} · ACC ${a.acc} ${typeTag} ${effTag} ${pri}</div>
      `;
      tile.addEventListener('click', () => {
        if(bc.abilityPp[mv]<=0) return;
        Audio.sfx.confirm();
        Battle.setMove(i);
        Battle.phase = 'execute';
        Battle.busy = true;
        this.mode = 'main';
        setTimeout(()=>Battle.executeTurn(), 200);
      });
      mov.appendChild(tile);
    });
  },
  showSwitch(){
    const act = this.root.querySelector('#battle-action-grid');
    const mov = this.root.querySelector('#battle-move-grid');
    act.style.display = 'none';
    mov.style.display = 'grid';
    this.mode = 'switch';
    mov.innerHTML = '';
    Game.party.forEach((bc, i) => {
      const tile = document.createElement('div');
      tile.className = 'move-tile' + (bc.hp<=0 ? ' disabled' : '');
      tile.innerHTML = `
        <div class="nm">${bc.name} <span class="pp">${bc.hp>0?`HP ${bc.hp}/${bc.maxHp}`:'FAINT'}</span></div>
        <div class="meta">Lv ${bc.level} · ${bc.type}</div>
      `;
      tile.addEventListener('click', () => {
        if(bc.hp<=0) return;
        this.mode = 'main';
        Battle.switchTo(i);
      });
      mov.appendChild(tile);
    });
  },
  showBag(){
    const act = this.root.querySelector('#battle-action-grid');
    const mov = this.root.querySelector('#battle-move-grid');
    act.style.display = 'none';
    mov.style.display = 'grid';
    this.mode = 'bag';
    mov.innerHTML = '';
    /* back button */
    const back = document.createElement('div');
    back.className = 'move-tile';
    back.innerHTML = '<div class="nm">← Back</div>';
    back.addEventListener('click', () => { this.mode = 'main'; this.render(); });
    mov.appendChild(back);
    const offer = [
      {id:'potion', act:'heal', amt:40},
      {id:'superPotion', act:'healFull'},
      {id:'revive', act:'revive'},
      {id:'wispOrb', act:'capture', power:1.0},
      {id:'gleamOrb', act:'capture', power:1.5},
      {id:'lattice', act:'capture', power:3.0}
    ];
    offer.forEach(o => {
      const cnt = Game.bag[o.id]||0;
      if(cnt<=0) return;
      const it = ITEMS[o.id];
      const tile = document.createElement('div');
      tile.className = 'move-tile';
      tile.innerHTML = `
        <div class="nm">${it.name} <span class="pp">×${cnt}</span></div>
        <div class="meta">${it.desc}</div>
      `;
      tile.addEventListener('click', () => {
        if(o.act === 'heal'){
          const alive = Game.party.filter(c=>c.hp>0);
          if(!alive.length) return;
          pickPartyTarget('Use on which creature?', (idx) => {
            Game.bag[o.id]--;
            Game.party[idx].hp = Math.min(Game.party[idx].maxHp, Game.party[idx].hp + o.amt);
            Game.notify(Game.party[idx].name + ' restored ' + o.amt + ' HP!');
            Audio.sfx.heal();
            endPlayerAction();
          });
        } else if(o.act === 'healFull'){
          const alive = Game.party.filter(c=>c.hp>0 && c.hp < c.maxHp);
          if(!alive.length) return;
          pickPartyTarget('Use on which creature?', (idx) => {
            Game.bag[o.id]--;
            Game.party[idx].hp = Game.party[idx].maxHp;
            Game.party[idx].status = {};
            Game.notify(Game.party[idx].name + ' fully restored!');
            Audio.sfx.heal();
            endPlayerAction();
          });
        } else if(o.act === 'revive'){
          const fainted = Game.party.filter(c=>c.hp<=0);
          if(!fainted.length){ Game.notify('No fainted creatures.'); return; }
          pickPartyTarget('Revive which creature?', (idx) => {
            Game.bag[o.id]--;
            Game.party[idx].hp = Math.floor(Game.party[idx].maxHp/2);
            Game.notify(Game.party[idx].name + ' was revived!');
            Audio.sfx.heal();
            endPlayerAction();
          });
        } else if(o.act === 'capture'){
          Game.bag[o.id]--;
          attemptCapture(o.power, ITEMS[o.id]);
        }
      });
      mov.appendChild(tile);
    });
  },
  onAction(act){
    Audio.sfx.select();
    if(act === 'fight'){ this.showMoves(); }
    else if(act === 'bag'){ this.showBag(); }
    else if(act === 'switch'){ this.showSwitch(); }
    else if(act === 'run'){
      if(Battle.wasWild){
        if(chance(0.7)){
          Audio.sfx.escape();
          Battle.bubbleText = 'Got away safely!';
          Battle.bubbleTimer = 1.0;
          Battle.phase = 'over'; Battle.result = 'flee';
          setTimeout(()=>Battle.onBattleEnd(), 800);
        } else {
          Battle.bubbleText = 'Can\'t escape!';
          Battle.bubbleTimer = 0.8;
          Audio.sfx.notEff?.();
          /* treat as turn: skip to next turn */
          Battle.pending = { skip:true };
          Battle.phase = 'execute';
          setTimeout(()=>Battle.executeTurn(), 800);
        }
      } else {
        Game.notify('You can\'t flee from this battle.');
      }
    }
  }
};
function endPlayerAction(){
  Battle.phase = 'execute';
  Battle.busy = true;
  Battle.pending = { skip:true };
  setTimeout(()=>Battle.executeTurn(), 400);
}
function pickPartyTarget(label, onPick){
  const act = document.getElementById('battle-action-grid');
  const mov = document.getElementById('battle-move-grid');
  act.style.display = 'none';
  mov.style.display = 'grid';
  mov.innerHTML = `<div class="small" style="padding:6px 10px;color:#fff5d6">${label}</div>`;
  const back = document.createElement('div');
  back.className = 'move-tile';
  back.innerHTML = '<div class="nm">← Back</div>';
  back.addEventListener('click', () => BattleUI.showBag());
  mov.appendChild(back);
  Game.party.forEach((bc, i) => {
    const tile = document.createElement('div');
    tile.className = 'move-tile' + (bc.hp<=0 && label.indexOf('Revive')<0 ? ' disabled' : '');
    tile.innerHTML = `<div class="nm">${bc.name} <span class="pp">${bc.hp>0?bc.hp+'/'+bc.maxHp:'FAINT'}</span></div>`;
    tile.addEventListener('click', () => {
      if(bc.hp<=0 && label.indexOf('Revive')<0) return;
      onPick(i);
    });
    mov.appendChild(tile);
  });
}

/* ===== CAPTURE ===== */
function attemptCapture(itemPower, item){
  if(!Battle.wasWild){ Game.notify('Cannot capture guardians this way.'); return; }
  Audio.sfx.capture();
  const target = Battle.enemies.find(e=>e.hp>0);
  if(!target){ return; }
  Battle.captureTarget = { target, itemPower, item, captureFill:0, attempts:0 };
  Battle.phase = 'capture';
  Battle.bubbleText = 'You throw a ' + item.name + '!';
  Battle.bubbleTimer = 1.0;
}
function updateCapture(dt){
  if(Battle.phase !== 'capture' || !Battle.captureTarget) return;
  const t = Battle.captureTarget;
  /* attempt happens when player presses E/space */
}
function performCaptureAttempt(){
  if(!Battle.captureTarget) return;
  const t = Battle.captureTarget;
  const target = t.target;
  const hpFrac = target.hp / target.maxHp;
  const baseChance = 0.20 + (1 - hpFrac) * 0.6;
  const chance_ = baseChance * t.itemPower;
  const caught = rnd() < chance_;
  t.attempts++;
  /* visual: shake target */
  if(caught){
    Audio.sfx.capture();
    Battle.bubbleText = target.name + ' was caught!';
    Battle.bubbleTimer = 1.5;
    /* add to party or storage */
    if(Game.party.length < MAX_PARTY){
      Game.party.push(target);
    } else {
      Game.storage.push(target);
    }
    Game.codex[target.id] = Game.codex[target.id] || {};
    Game.codex[target.id].caught = true;
    /* add to codex if any un-seen */
    Battle.phase = 'over';
    Battle.result = 'win'; /* mark as win but no exp/coins */
    Battle.captureTarget = null;
    setTimeout(()=>Battle.onBattleEnd(), 1200);
  } else {
    Audio.sfx.notEff?.();
    Battle.bubbleText = target.name + ' broke free!';
    Battle.bubbleTimer = 1.0;
    if(t.attempts >= 3){
      Battle.phase = 'execute';
      Battle.busy = true;
      Battle.pending = { skip:true };
      Battle.captureTarget = null;
      setTimeout(()=>Battle.executeTurn(), 600);
    }
  }
}

/* ===== STORY ===== */
function triggerStory(stage){
  if(Game.storyStage >= stage) return;
  Game.storyStage = stage;
  const s = STORY[stage];
  showDialog({ who:s.who, what:s.what, color:s.color, onClose:()=>{} });
}
function triggerFinale(){
  showDialog({ who:'A Voice in the Dawn', what:'The world remembers you. The first dawn rests in your hand. You have done what was asked. The path is open — but the Veil still hums, and the wilds are patient.', onClose:()=>{
    Game.flags.finaleDone = true;
    Game.notify('Post-game: wilds grow stronger. The journey continues.');
  }});
}

/* ===== DIALOG ===== */
function showDialog(opts){
  Game.modal = 'dialog';
  const el = document.createElement('div');
  el.className = 'dialogue';
  el.innerHTML = `
    <div class="who">${opts.who||''}</div>
    <div class="what">${opts.what||''}</div>
    <div class="next">E / Space to continue</div>
  `;
  document.getElementById('ui').appendChild(el);
  const close = () => {
    el.remove();
    Game.modal = null;
    Audio.sfx.confirm();
    if(opts.onClose) opts.onClose();
  };
  el.addEventListener('click', close);
  const handler = (e) => {
    if((e.key==='e' || e.key===' ' || e.key==='Enter') && Game.modal==='dialog'){
      window.removeEventListener('keydown', handler);
      close();
    }
  };
  window.addEventListener('keydown', handler);
}
function closeDialog(){
  document.querySelectorAll('.dialogue').forEach(d=>d.remove());
  Game.modal = null;
}

/* ===== PAUSE / MENUS ===== */
function openPause(){
  Game.showPause = !Game.showPause;
  document.getElementById('pause-menu').classList.toggle('hidden', !Game.showPause);
}
function openBag(){ openMenu('bag'); }
function openTeam(){ openMenu('team'); }
function openPedia(){ openMenu('pedia'); }
function openMap(){ openMenu('map'); }
function openMenu(name){
  if(Game.modal) return;
  Game.modal = name;
  Audio.sfx.select();
  const modal = document.createElement('div');
  modal.className = 'modal-back';
  modal.dataset.name = name;
  document.getElementById('ui').appendChild(modal);
  const close = () => { modal.remove(); Game.modal = null; };
  const inner = document.createElement('div');
  inner.className = 'modal';
  modal.appendChild(inner);
  if(name === 'bag'){ buildBag(inner, close); }
  else if(name === 'team'){ buildTeam(inner, close); }
  else if(name === 'pedia'){ buildPedia(inner, close); }
  else if(name === 'map'){ buildMap(inner, close); }
  modal.addEventListener('click', e => { if(e.target === modal) close(); });
  /* escape */
  const handler = (e) => { if(e.key==='escape' && Game.modal===name){ window.removeEventListener('keydown', handler); close(); } };
  window.addEventListener('keydown', handler);
}
function buildBag(inner, close){
  inner.innerHTML = `<h2>Traveler's Pack</h2><div class="sub">Items, catalysts, and tools.</div><div class="body scroll" id="bag-body"></div><div class="foot"><button class="btn tiny" id="use-potion">Use Moonpetal Tonic</button><button class="btn" data-close>Close</button></div>`;
  inner.querySelector('[data-close]').addEventListener('click', close);
  const body = inner.querySelector('#bag-body');
  for(const id of Game.bagKinds){
    const cnt = Game.bag[id]||0;
    if(cnt<=0 && !ITEMS[id]) continue;
    const it = ITEMS[id];
    if(!it) continue;
    const row = document.createElement('div');
    row.className = 'row';
    row.style.justifyContent = 'space-between';
    row.style.background = 'rgba(255,255,255,0.04)';
    row.style.border = '1px solid var(--line)';
    row.style.borderRadius = '10px';
    row.style.padding = '6px 10px';
    row.innerHTML = `<div><b>${it.name}</b><div class="small">${it.desc}</div></div><div class="row gap-8"><span class="small">×${cnt}</span>${it.kind==='capture'||it.kind==='heal'||it.kind==='healFull'||it.kind==='revive'?'<button class="btn tiny" data-use="'+id+'">Use</button>':''}</div>`;
    body.appendChild(row);
  }
  body.querySelectorAll('[data-use]').forEach(b => b.addEventListener('click', () => {
    const id = b.dataset.use; const it = ITEMS[id];
    if((Game.bag[id]||0)<=0){ Game.notify('None left.'); return; }
    if(it.kind === 'capture'){
      Game.notify('Capture items are used in battle.');
      return;
    }
    if(it.kind === 'heal' || it.kind === 'healFull'){
      const alive = Game.party.filter(c=>c.hp>0 && (it.kind==='healFull' ? c.hp < c.maxHp : true));
      if(!alive.length){ Game.notify('No one needs it.'); return; }
      pickForMenu(alive, (idx) => {
        Game.bag[id]--;
        Game.party[idx].hp = Math.min(Game.party[idx].maxHp, Game.party[idx].hp + it.power);
        if(it.kind === 'healFull'){ Game.party[idx].hp = Game.party[idx].maxHp; Game.party[idx].status = {}; }
        Audio.sfx.heal();
        Game.notify(Game.party[idx].name + ' restored.');
        close(); setTimeout(()=>openBag(), 50);
      });
    } else if(it.kind === 'revive'){
      const f = Game.party.filter(c=>c.hp<=0);
      if(!f.length){ Game.notify('No fainted.'); return; }
      pickForMenu(f, (idx) => {
        Game.bag[id]--;
        Game.party[idx].hp = Math.floor(Game.party[idx].maxHp*it.power);
        Audio.sfx.heal();
        Game.notify(Game.party[idx].name + ' revived.');
        close(); setTimeout(()=>openBag(), 50);
      });
    }
  }));
  /* catalyst usage */
  inner.querySelector('#use-potion').addEventListener('click', () => {
    if((Game.bag.potion||0)<=0){ Game.notify('None left.'); return; }
    const alive = Game.party.filter(c=>c.hp>0);
    if(!alive.length) return;
    pickForMenu(alive, (idx) => {
      Game.bag.potion--;
      Game.party[idx].hp = Math.min(Game.party[idx].maxHp, Game.party[idx].hp + 40);
      Audio.sfx.heal();
      Game.notify(Game.party[idx].name + ' restored 40 HP.');
      close(); setTimeout(()=>openBag(), 50);
    });
  });
}
function pickForMenu(list, cb){
  const m = document.createElement('div');
  m.className = 'modal-back';
  m.innerHTML = `<div class="modal" style="max-width:480px"><h2>Choose</h2><div class="body scroll" id="pick-body"></div><div class="foot"><button class="btn" id="cancel">Cancel</button></div></div>`;
  document.getElementById('ui').appendChild(m);
  const body = m.querySelector('#pick-body');
  list.forEach((bc, i) => {
    const realIdx = Game.party.indexOf(bc);
    const t = document.createElement('div');
    t.className = 'move-tile';
    t.innerHTML = `<div class="nm">${bc.name} <span class="pp">${bc.hp>0?bc.hp+'/'+bc.maxHp:'FAINT'}</span></div><div class="meta">Lv ${bc.level} · ${bc.type}</div>`;
    t.addEventListener('click', () => { m.remove(); cb(realIdx); });
    body.appendChild(t);
  });
  m.querySelector('#cancel').addEventListener('click', () => m.remove());
}
function buildTeam(inner, close){
  inner.innerHTML = `<h2>Your Veilbound</h2><div class="sub">Active party and storage. Click a creature to switch, transform, or reorder.</div><div class="body scroll" id="team-body"></div><div class="foot"><button class="btn" data-close>Close</button></div>`;
  inner.querySelector('[data-close]').addEventListener('click', close);
  const body = inner.querySelector('#team-body');
  const renderOne = (bc, isStorage) => {
    const t = document.createElement('div');
    t.className = 'creature-tile';
    const tr = CREATURES[bc.id].transform;
    const canTrans = tr && !isStorage && bc.level >= tr.atLevel && (Game.bag[tr.catalyst]||0)>0 && !isStorage;
    t.innerHTML = `
      <div class="top">
        <div>
          <div class="name">${bc.name}</div>
          <div class="lvl">Lv ${bc.level} · ${bc.type}${bc.type2?'/'+bc.type2:''}</div>
        </div>
        <div class="small">HP ${bc.hp}/${bc.maxHp}</div>
      </div>
      <div class="lore">${CREATURES[bc.id].lore}</div>
      <div class="stats">
        <div class="stat">ATK <b>${bc.atk}</b></div>
        <div class="stat">DEF <b>${bc.def}</b></div>
        <div class="stat">SPD <b>${bc.spd}</b></div>
      </div>
      <div class="bar" style="margin-top:6px"><i style="width:${100*bc.hp/bc.maxHp}%"></i></div>
      <div class="bar exp" style="margin-top:4px"><i style="width:${100*bc.exp/bc.expToNext}%"></i></div>
      <div class="small">EXP ${bc.exp}/${bc.expToNext}</div>
      <div class="row gap-8" style="margin-top:6px;flex-wrap:wrap">
        <span class="tag type-${bc.type.toLowerCase()}">${bc.type}</span>
        ${bc.moves.map(m=>`<span class="tag">${ABILITIES[m].name}</span>`).join('')}
      </div>
      ${canTrans?`<button class="btn primary tiny" style="margin-top:8px" data-trans="${bc.id}">Transform using ${ITEMS[tr.catalyst].name}</button>`:''}
      ${!isStorage?`<button class="btn tiny" style="margin-top:6px" data-swap="${bc.id}">To Storage</button>`:''}
      ${isStorage?`<button class="btn tiny" style="margin-top:6px" data-recall="${bc.id}">To Party</button>`:''}
    `;
    t.querySelector('[data-trans]')?.addEventListener('click', () => {
      const tr2 = CREATURES[bc.id].transform;
      Game.bag[tr2.catalyst]--;
      const newBc = makeBattleCreature(tr2.into, bc.level);
      newBc.exp = bc.exp;
      const i = Game.party.indexOf(bc);
      Game.party[i] = newBc;
      Audio.sfx.level();
      Game.notify(bc.name + ' transforms into ' + newBc.name + '!');
      close(); setTimeout(()=>openTeam(), 50);
    });
    t.querySelector('[data-swap]')?.addEventListener('click', () => {
      const i = Game.party.indexOf(bc);
      if(Game.party.length<=1){ Game.notify('Cannot store your only creature.'); return; }
      Game.storage.push(bc);
      Game.party.splice(i,1);
      close(); setTimeout(()=>openTeam(), 50);
    });
    t.querySelector('[data-recall]')?.addEventListener('click', () => {
      const i = Game.storage.indexOf(bc);
      if(Game.party.length>=MAX_PARTY){ Game.notify('Party is full.'); return; }
      Game.party.push(bc);
      Game.storage.splice(i,1);
      close(); setTimeout(()=>openTeam(), 50);
    });
    return t;
  };
  /* active */
  const head = document.createElement('div');
  head.innerHTML = '<div class="small" style="margin:6px 2px">Active party</div>';
  body.appendChild(head);
  for(const bc of Game.party) body.appendChild(renderOne(bc, false));
  if(Game.storage.length){
    const head2 = document.createElement('div');
    head2.innerHTML = '<div class="small" style="margin:14px 2px 6px">Storage</div>';
    body.appendChild(head2);
    for(const bc of Game.storage) body.appendChild(renderOne(bc, true));
  }
}
function buildPedia(inner, close){
  inner.innerHTML = `<h2>Veil Codex</h2><div class="sub">All creatures you have seen.</div><div class="body scroll" id="pedia-body"></div><div class="foot"><button class="btn" data-close>Close</button></div>`;
  inner.querySelector('[data-close]').addEventListener('click', close);
  const body = inner.querySelector('#pedia-body');
  const seen = Object.keys(Game.codex).filter(k=>Game.codex[k]);
  /* show all known + locked */
  for(const id of CREAT_LIST){
    const c = CREATURES[id];
    const t = document.createElement('div');
    t.className = 'creature-tile';
    const known = !!Game.codex[id];
    t.innerHTML = `
      <div class="top">
        <div>
          <div class="name">${known?c.name:'???'}</div>
          <div class="lvl">${known?c.type:'???'}</div>
        </div>
        <div class="small">${known?(Game.codex[id].caught?'Caught':'Seen'):'Unknown'}</div>
      </div>
      <div class="lore">${known?c.lore:'You have not yet met this creature. Wander the Veil to learn more.'}</div>
      <div class="row gap-8" style="margin-top:6px;flex-wrap:wrap">
        ${known?`<span class="tag type-${c.type.toLowerCase()}">${c.type}</span>`:''}
        ${known?c.base.map((s,i)=>`<span class="tag">${['HP','ATK','DEF','SPD'][i]} ${s}</span>`).join(''):''}
      </div>
    `;
    body.appendChild(t);
  }
}
function buildMap(inner, close){
  inner.innerHTML = `<h2>World of Aethelgard</h2><div class="sub">Zones you've discovered.</div><div class="body scroll" id="map-body"></div><div class="foot"><button class="btn" data-close>Close</button></div>`;
  inner.querySelector('[data-close]').addEventListener('click', close);
  const body = inner.querySelector('#map-body');
  body.innerHTML = '<div class="mini-map"><canvas id="mmc" width="320" height="320"></canvas></div>';
  const c = body.querySelector('#mmc');
  const cc = c.getContext('2d');
  cc.fillStyle = '#0a0a1a'; cc.fillRect(0,0,320,320);
  const W = 320, H = 320;
  /* place zones in a hex-ish layout */
  const positions = {
    glades:    {x:80, y:240},
    cove:      {x:160, y:200},
    thicket:   {x:240, y:240},
    spires:    {x:80,  y:120},
    crags:     {x:160, y:80},
    sanctum:   {x:240, y:120}
  };
  const links = [
    ['glades','cove'], ['cove','thicket'], ['thicket','spires'],
    ['spires','crags'], ['crags','sanctum'], ['sanctum','thicket'],
    ['cove','spires']
  ];
  for(const [a,b] of links){
    const pa = positions[a], pb = positions[b];
    if(!Game.zoneUnlocked[a] || !Game.zoneUnlocked[b]) continue;
    cc.strokeStyle = '#fff5d655'; cc.lineWidth = 2;
    cc.beginPath(); cc.moveTo(pa.x, pa.y); cc.lineTo(pb.x, pb.y); cc.stroke();
  }
  for(const id of ZONE_LIST){
    const z = ZONES[id]; const p = positions[id];
    const here = Game.zone === id;
    const unlocked = Game.zoneUnlocked[id];
    cc.fillStyle = unlocked ? z.accent : '#333';
    cc.beginPath(); cc.arc(p.x, p.y, here?10:7, 0, Math.PI*2); cc.fill();
    if(here){ cc.strokeStyle = '#fff'; cc.lineWidth = 2; cc.beginPath(); cc.arc(p.x, p.y, 14, 0, Math.PI*2); cc.stroke(); }
    cc.fillStyle = unlocked ? '#fff' : '#777';
    cc.font = '10px ui-sans-serif'; cc.textAlign = 'center';
    cc.fillText(z.name, p.x, p.y+22);
  }
  /* legend */
  const legend = document.createElement('div');
  legend.style.marginTop = '10px';
  legend.innerHTML = '<div class="small">Currently: <b>'+ZONES[Game.zone].name+'</b></div>'+
    (Game.bossDefeated[Game.zone]?'<div class="small" style="color:#7ee07a">Guardian defeated.</div>':'<div class="small" style="color:#a3acd1">Guardian still wanders here.</div>');
  body.appendChild(legend);
}

/* ===== TITLE SCREEN ===== */
function showTitle(){
  Game.state = 'title';
  const ts = document.getElementById('title-screen');
  ts.style.display = 'flex';
  document.getElementById('hud').classList.add('hidden');
  document.getElementById('pause-menu').classList.add('hidden');
  document.getElementById('touch-pad').classList.add('hidden');
  document.getElementById('touch-actions').classList.add('hidden');
  /* stars */
  const sc = document.getElementById('title-stars');
  sc.innerHTML = '';
  for(let i=0;i<60;i++){
    const s = document.createElement('div');
    s.className = 'star';
    s.style.left = rnd()*100 + '%';
    s.style.top = rnd()*100 + '%';
    s.style.animationDelay = (rnd()*3) + 's';
    s.style.width = (1+rnd()*2) + 'px';
    s.style.height = s.style.width;
    sc.appendChild(s);
  }
  document.getElementById('btn-new').onclick = () => { Audio.init(); startNewGame(); };
  const cont = document.getElementById('btn-cont');
  if(hasSave()){
    cont.classList.remove('hidden');
  } else {
    cont.classList.add('hidden');
  }
  cont.onclick = () => { Audio.init(); const data = loadGame(0); if(data) { enterOverworld(); } else Game.notify('No save found.'); };
}
function startNewGame(){
  /* pick starter */
  Game.state = 'pickStarter';
  const ts = document.getElementById('title-screen');
  ts.style.display = 'none';
  const modal = document.createElement('div');
  modal.className = 'modal-back';
  modal.innerHTML = `<div class="modal" style="max-width:720px">
    <h2>Choose a Companion</h2>
    <div class="sub">A first creature to walk the Veil with you.</div>
    <div class="body" id="starter-body"></div>
    <div class="foot"><span class="small">Three companions, three different paths. You can catch more as you go.</span></div>
  </div>`;
  document.getElementById('ui').appendChild(modal);
  const body = modal.querySelector('#starter-body');
  body.style.display = 'grid';
  body.style.gridTemplateColumns = 'repeat(3, 1fr)';
  body.style.gap = '10px';
  for(const id of STARTER_IDS){
    const c = CREATURES[id];
    const t = document.createElement('div');
    t.className = 'creature-tile';
    t.style.cursor = 'pointer';
    t.innerHTML = `
      <div class="top">
        <div>
          <div class="name">${c.name}</div>
          <div class="lvl">Lv 5 · ${c.type}</div>
        </div>
        <span class="tag type-${c.type.toLowerCase()}">${c.type}</span>
      </div>
      <div class="lore">${c.lore}</div>
      <div class="stats">
        <div class="stat">HP <b>${c.base[0]+4*4}</b></div>
        <div class="stat">ATK <b>${c.base[1]+4*1.4}</b></div>
        <div class="stat">DEF <b>${c.base[2]+4*1.4}</b></div>
        <div class="stat">SPD <b>${c.base[3]+4*1.6}</b></div>
      </div>
      <button class="btn primary tiny" style="margin-top:8px" data-pick="${id}">Choose</button>
    `;
    body.appendChild(t);
  }
  body.querySelectorAll('[data-pick]').forEach(b => b.addEventListener('click', () => {
    const id = b.dataset.pick;
    const starter = makeBattleCreature(id, 5);
    Game.party = [starter];
    Game.storage = [];
    Game.codex = {}; Game.codex[id] = {seen:true, caught:true};
    Game.bag = { potion:3, superPotion:1, wispOrb:8, gleamOrb:0, lattice:0, revive:1, repel:1 };
    Game.coins = 200;
    Game.repelCount = 0;
    Game.stepsInGrass = 0;
    Game.tutorialStep = 0;
    Game.tutorialFlag = true;
    Game.flags = { tutorialActive:true };
    Game.storyStage = 0;
    Game.bossDefeated = {};
    Game.zoneUnlocked = { glades:true };
    Game.shrineSpawn = 'glades';
    Game.zone = 'glades';
    Game.player = { x:1, y:1, dir:0, walkT:0, stepPhase:0, moveAccum:0 };
    modal.remove();
    saveGame(0);
    triggerStory(1);
    enterOverworld();
    setTimeout(()=>{
      showDialog({ who:'A Voice in the Ash', what:'Move with W/A/S/D or the arrow keys. Press E to act — talk, pray at a shrine, or face a guardian. Press Esc to open the menu.' });
    }, 4000);
  }));
}
function enterOverworld(){
  Game.state = 'overworld';
  document.getElementById('title-screen').style.display = 'none';
  document.getElementById('hud').classList.remove('hidden');
  document.getElementById('touch-pad').classList.remove('hidden');
  document.getElementById('touch-actions').classList.remove('hidden');
  Particles.setAmbient(Game.zone);
  Audio.startMusic(Game.zone === 'sanctum' ? 'shrine' : 'overworld');
  BattleUI.hide();
  updateHUD();
}
function updateHUD(){
  document.getElementById('hud-zone').textContent = ZONES[Game.zone].name;
  document.getElementById('hud-party').textContent = Game.party.length;
  document.getElementById('hud-coins').textContent = Game.coins;
  const mm = Math.floor(Game.playTime/60).toString().padStart(2,'0');
  const ss = Math.floor(Game.playTime%60).toString().padStart(2,'0');
  document.getElementById('hud-time').textContent = mm+':'+ss;
}

/* ===== BATTLE INPUT ===== */
function updateBattle(dt){
  if(Game.paused) return;
  if(Battle.phase === 'over'){
    /* wait for visual to finish, then return to overworld */
    if(!Battle._endPending){
      Battle._endPending = true;
      setTimeout(()=>{
        BattleUI.hide();
        Battle.onBattleEnd();
        Battle._endPending = false;
      }, 1500);
    }
    return;
  }
  if(Battle.phase === 'capture'){
    if(Input.consume('e') || Input.consume(' ')){
      performCaptureAttempt();
    }
    return;
  }
  if(Battle.busy) return;
  if(Battle.phase === 'choose'){
    /* the BattleUI handles clicks; but we also support number keys 1-4 for moves */
    for(let i=1;i<=4;i++){
      if(Input.consume(''+i) || Input.consume('digit'+i)){
        const bc = Game.party[Battle.playerActive];
        if(bc && bc.moves[i-1] && bc.abilityPp[bc.moves[i-1]]>0){
          Audio.sfx.confirm();
          Battle.setMove(i-1);
          Battle.phase = 'execute';
          Battle.busy = true;
          setTimeout(()=>Battle.executeTurn(), 200);
        }
      }
    }
  }
  if(Battle.phase === 'between' && !Battle.busy){
    /* advance to first turn */
    Battle.busy = true;
    setTimeout(()=>{
      Battle.busy = false;
      Battle.executeTurn();
    }, 700);
  }
}

/* ===== MAIN LOOP ===== */
let last = performance.now();
function loop(now){
  const dt = Math.min(0.05, (now - last)/1000);
  last = now;
  if(Game.state !== 'title'){
    Game.playTime += dt;
    updateHUD();
  }
  /* clear with subtle background */
  ctx2d.fillStyle = '#04060f';
  ctx2d.fillRect(0,0,VW,VH);
  /* shake */
  const shake = (Game.shake > 0 ? Game.shake : 0) + (Battle.shake > 0 ? Battle.shake : 0);
  ctx2d.save();
  if(shake > 0){
    const s = shake * 6;
    ctx2d.translate((rnd()-0.5)*s, (rnd()-0.5)*s);
    Game.shake = Math.max(0, Game.shake - dt*2);
    Battle.shake = Math.max(0, Battle.shake - dt*2);
  }
  if(Game.state === 'overworld' || Game.state === 'menu'){
    updateOverworld(dt);
    drawOverworld(dt);
  } else if(Game.state === 'battle'){
    if(Battle.phase !== 'over' && Battle.phase !== 'capture' || true){
      drawBattleScene(ctx2d, performance.now()/1000);
    }
    updateBattle(dt);
    /* update bubble timer */
    if(Battle.bubbleTimer > 0){ Battle.bubbleTimer -= dt; if(Battle.bubbleTimer<0){ Battle.bubbleText=''; } }
    BattleUI.render();
    BattleUI.show();
  } else if(Game.state === 'pickStarter' || Game.state === 'title'){
    /* title screen background */
    const grad = ctx2d.createLinearGradient(0,0,0,VH);
    grad.addColorStop(0, '#2a3260');
    grad.addColorStop(1, '#0a0a1a');
    ctx2d.fillStyle = grad; ctx2d.fillRect(0,0,VW,VH);
    /* stars */
    for(let i=0;i<60;i++){
      const x = (i*97 + now/100) % VW;
      const y = (i*53 + now/50) % VH;
      const a = 0.3 + Math.sin(now/1000 + i)*0.4;
      ctx2d.globalAlpha = a;
      ctx2d.fillStyle = '#fff5d6';
      ctx2d.beginPath(); ctx2d.arc(x, y, 1, 0, Math.PI*2); ctx2d.fill();
    }
    ctx2d.globalAlpha = 1;
  }
  Particles.update(dt);
  ctx2d.restore();
  requestAnimationFrame(loop);
}

/* ===== BIND UI / START ===== */
document.getElementById('pause-menu').addEventListener('click', e => {
  const a = e.target.dataset?.act;
  if(!a) return;
  if(a === 'bag') openBag();
  else if(a === 'team') openTeam();
  else if(a === 'pedia') openPedia();
  else if(a === 'map') openMap();
  else if(a === 'save'){ saveGame(0); Game.notify('Game saved.'); Audio.sfx.confirm(); }
  else if(a === 'mute'){ Audio.setMuted(!Audio.muted); Game.notify(Audio.muted?'Muted.':'Unmuted.'); }
  else if(a === 'quit'){ showTitle(); }
});

/* hide pause menu initially */
document.getElementById('pause-menu').classList.add('hidden');
document.getElementById('hud').classList.add('hidden');

/* show title on load */
showTitle();
requestAnimationFrame(loop);

/* Ensure page doesn't scroll on keys */
window.addEventListener('keydown', e => {
  if(['ArrowUp','ArrowDown','ArrowLeft','ArrowRight',' '].includes(e.key)) e.preventDefault();
});

/* Click handlers on canvas for touch fallback */
canvas.addEventListener('click', e => {
  if(Game.state === 'battle' && Battle.phase === 'capture'){
    performCaptureAttempt();
  }
});

</script>
</body>
</html>
<!-- agent-meta {"model":"minimax/minimax-m3","provider":"openrouter","persona":"full","sessionId":"66656dbd-0294-46ee-881d-d139d51aa732","tokensIn":12860147,"tokensOut":102660,"tokensTotal":12962807,"turns":99,"toolCalls":98,"failedToolCalls":0,"timestamp":"2026-07-12T19:47:53.434Z"} -->