← Creature Collection results

Creature Collection

Minimax m3 · middle

Initial view of Creature Collection

Reviews & generation details

Reviews

Human review · Jul 17, 2026 · rubric 1

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

The reviewer marked this result as broken. It remains in the archive.

Visual design
Unscored
UX / feel
Unscored
Works
Unscored
Prompt spirit
Unscored
Delight
Unscored

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 170,374 bytes · SHA-256 aab2376b95bf
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<title>Aethermere: Whispers of the Hollow</title>
<style>
:root{
  --bg-0:#07060f;
  --bg-1:#0f0c1f;
  --bg-2:#1a1530;
  --bg-3:#241b40;
  --ink-0:#e8e3ff;
  --ink-1:#b6a8e0;
  --ink-2:#7a6ba8;
  --lumen:#7fffe5;
  --lumen-dim:#3fb5a0;
  --cinder:#ff8a5b;
  --flora:#7cd66a;
  --tidal:#5ec3ff;
  --storm:#cba6ff;
  --crag:#e0c891;
  --shade:#a48cff;
  --crimson:#ff5a7a;
  --gold:#ffd76b;
  --good:#7cf2a8;
  --bad:#ff6e6e;
  --glass: rgba(20, 16, 40, 0.62);
  --glass-strong: rgba(14, 10, 28, 0.85);
  --glass-border: rgba(180, 160, 240, 0.18);
  --glass-border-bright: rgba(180, 200, 255, 0.32);
  --shadow-soft: 0 12px 40px rgba(0,0,0,0.45);
  --r-sm:8px; --r-md:14px; --r-lg:22px; --r-xl:32px;
  --f-display: 'Segoe UI', 'SF Pro Display', system-ui, -apple-system, sans-serif;
  --f-mono: ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
}
*{box-sizing:border-box;margin:0;padding:0;-webkit-tap-highlight-color:transparent}
html,body{width:100%;height:100%;overflow:hidden;background:#000;color:var(--ink-0);font-family:var(--f-display);user-select:none;-webkit-user-select:none}
#app{position:relative;width:100vw;height:100vh;overflow:hidden;background:radial-gradient(ellipse at center, var(--bg-2) 0%, var(--bg-0) 75%)}
#game-canvas{position:absolute;inset:0;width:100%;height:100%;display:block;image-rendering:auto}
#ui-layer{position:absolute;inset:0;pointer-events:none;z-index:5}
#ui-layer > *{pointer-events:auto}

/* === Title screen === */
#title-screen{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;background:radial-gradient(ellipse at 50% 30%, #2a1f4f 0%, #0a0617 70%);z-index:50;overflow:hidden}
#title-screen::before{content:'';position:absolute;inset:0;background:
  radial-gradient(2px 2px at 20% 30%, var(--lumen), transparent),
  radial-gradient(1px 1px at 70% 60%, #fff, transparent),
  radial-gradient(1.5px 1.5px at 40% 80%, var(--storm), transparent),
  radial-gradient(1px 1px at 85% 20%, #fff, transparent),
  radial-gradient(2px 2px at 55% 45%, var(--cinder), transparent),
  radial-gradient(1px 1px at 15% 65%, #fff, transparent),
  radial-gradient(1.5px 1.5px at 90% 75%, var(--tidal), transparent);opacity:.7;animation:stars-twinkle 6s ease-in-out infinite}
@keyframes stars-twinkle{0%,100%{opacity:.5}50%{opacity:.9}}
#title-content{position:relative;text-align:center;z-index:2;padding:20px}
.title-mark{font-size:clamp(38px,7vw,72px);font-weight:200;letter-spacing:.18em;color:transparent;background:linear-gradient(135deg, var(--lumen) 0%, var(--storm) 50%, var(--cinder) 100%);-webkit-background-clip:text;background-clip:text;margin-bottom:6px;text-shadow:0 0 40px rgba(127,255,229,.3)}
.title-sub{font-size:clamp(12px,1.6vw,16px);letter-spacing:.4em;color:var(--ink-1);margin-bottom:48px;font-weight:300}
.title-tagline{font-size:clamp(11px,1.4vw,14px);color:var(--ink-2);font-style:italic;margin-bottom:36px;max-width:520px;line-height:1.6}
.title-btn{display:block;margin:10px auto;padding:14px 38px;min-width:240px;background:linear-gradient(135deg, rgba(127,255,229,.12), rgba(196,166,255,.12));border:1px solid var(--glass-border-bright);color:var(--ink-0);font-family:var(--f-display);font-size:15px;letter-spacing:.18em;border-radius:var(--r-md);cursor:pointer;backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);transition:all .25s ease;font-weight:500}
.title-btn:hover{background:linear-gradient(135deg, rgba(127,255,229,.25), rgba(196,166,255,.25));border-color:var(--lumen);transform:translateY(-2px);box-shadow:0 8px 24px rgba(127,255,229,.25)}
.title-btn:active{transform:translateY(0)}
.title-btn.primary{background:linear-gradient(135deg, var(--lumen), var(--storm));color:#0a0617;font-weight:600;border:none}
.title-btn.primary:hover{box-shadow:0 8px 32px rgba(127,255,229,.5)}
.title-credits{position:absolute;bottom:18px;left:0;right:0;text-align:center;font-size:10px;color:var(--ink-2);letter-spacing:.3em}

/* === HUD overlay === */
#hud{position:absolute;top:0;left:0;right:0;padding:14px 18px;display:flex;justify-content:space-between;align-items:center;pointer-events:none;z-index:10;background:linear-gradient(180deg, rgba(7,6,15,.55) 0%, transparent 100%)}
#hud > *{pointer-events:auto}
.hud-zone{font-size:12px;letter-spacing:.25em;color:var(--ink-1);text-transform:uppercase}
.hud-zone strong{color:var(--lumen);font-weight:500;display:block;font-size:15px;letter-spacing:.12em}
.hud-right{display:flex;gap:10px}
.hud-btn{width:42px;height:42px;border-radius:50%;background:var(--glass);border:1px solid var(--glass-border);color:var(--ink-0);cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:18px;backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);transition:all .2s}
.hud-btn:hover{background:rgba(40,30,70,.85);border-color:var(--lumen);transform:scale(1.06)}

/* === Touch controls === */
#touch-controls{position:absolute;bottom:20px;left:20px;width:160px;height:160px;display:none;z-index:12;touch-action:none}
.touch-pad{position:relative;width:100%;height:100%;border-radius:50%;background:rgba(20,16,40,.5);border:1px solid var(--glass-border);backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px)}
.touch-btn{position:absolute;width:46px;height:46px;border-radius:50%;background:rgba(127,255,229,.15);border:1px solid var(--lumen-dim);color:var(--lumen);font-size:22px;display:flex;align-items:center;justify-content:center;cursor:pointer}
.touch-btn.up{top:6px;left:50%;transform:translateX(-50%)}
.touch-btn.down{bottom:6px;left:50%;transform:translateX(-50%)}
.touch-btn.left{left:6px;top:50%;transform:translateY(-50%)}
.touch-btn.right{right:6px;top:50%;transform:translateY(-50%)}

/* === Modal === */
.modal-backdrop{position:absolute;inset:0;background:rgba(0,0,0,.55);backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px);z-index:40;display:flex;align-items:center;justify-content:center;padding:20px;animation:fadein .2s ease}
@keyframes fadein{from{opacity:0}to{opacity:1}}
.modal{background:var(--glass-strong);border:1px solid var(--glass-border-bright);border-radius:var(--r-xl);padding:24px;max-width:min(92vw, 720px);max-height:90vh;overflow-y:auto;box-shadow:var(--shadow-soft);backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);position:relative;animation:rise .3s cubic-bezier(.2,.8,.3,1)}
@keyframes rise{from{opacity:0;transform:translateY(20px) scale(.96)}to{opacity:1;transform:translateY(0) scale(1)}}
.modal.wide{max-width:min(94vw, 920px)}
.modal h2{font-size:20px;letter-spacing:.15em;color:var(--lumen);font-weight:500;margin-bottom:18px;text-transform:uppercase}
.modal h3{font-size:14px;letter-spacing:.12em;color:var(--ink-1);margin:14px 0 8px;text-transform:uppercase}
.modal-close{position:absolute;top:14px;right:14px;width:34px;height:34px;border-radius:50%;background:transparent;border:1px solid var(--glass-border);color:var(--ink-1);cursor:pointer;font-size:16px;display:flex;align-items:center;justify-content:center;transition:all .2s}
.modal-close:hover{background:rgba(255,90,122,.2);border-color:var(--crimson);color:var(--crimson)}

/* === Dialogue / cut-in === */
#dialogue-box{position:absolute;bottom:30px;left:50%;transform:translateX(-50%);width:min(92vw, 720px);background:var(--glass-strong);border:1px solid var(--glass-border-bright);border-radius:var(--r-lg);padding:20px 24px;z-index:30;backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);display:none;animation:rise .3s ease}
#dialogue-box.show{display:block}
.dialogue-speaker{font-size:11px;letter-spacing:.3em;color:var(--lumen);text-transform:uppercase;margin-bottom:6px}
.dialogue-text{font-size:15px;line-height:1.6;color:var(--ink-0);min-height:48px}
.dialogue-text::after{content:'▌';color:var(--lumen);animation:blink 1s infinite;margin-left:2px}
@keyframes blink{0%,49%{opacity:1}50%,100%{opacity:0}}
.dialogue-hint{font-size:10px;color:var(--ink-2);letter-spacing:.2em;text-align:right;margin-top:10px}

/* === Battle screen === */
#battle-screen{position:absolute;inset:0;display:none;z-index:25;background:linear-gradient(180deg, #1a0f2e 0%, #2a1a4a 50%, #1f1438 100%);overflow:hidden}
#battle-screen.show{display:flex;flex-direction:column}
#battle-canvas{position:absolute;inset:0;width:100%;height:100%}
.battle-bottom{position:absolute;bottom:0;left:0;right:0;background:linear-gradient(180deg, transparent, rgba(7,6,15,.85) 30%);padding:20px;z-index:3}
#battle-info{display:flex;justify-content:space-between;align-items:flex-end;margin-bottom:14px;gap:14px;flex-wrap:wrap}
.battle-enemy-info{flex:1;min-width:0}
.battle-party-info{flex:1;min-width:0;display:flex;flex-direction:column;align-items:flex-end;gap:6px}
.battle-name{font-size:13px;letter-spacing:.15em;color:var(--ink-1);text-transform:uppercase;margin-bottom:4px}
.battle-name strong{color:var(--ink-0);font-weight:600;letter-spacing:.1em}
.battle-level{font-size:11px;color:var(--lumen);margin-left:8px;letter-spacing:.1em}
.hp-bar-wrap{width:100%;max-width:280px;height:14px;background:rgba(0,0,0,.6);border:1px solid var(--glass-border);border-radius:7px;overflow:hidden;position:relative}
.hp-bar{height:100%;background:linear-gradient(90deg, #4af2a4, #2ad080);transition:width .55s cubic-bezier(.2,.8,.3,1);border-radius:7px;position:relative;overflow:hidden}
.hp-bar.low{background:linear-gradient(90deg, #ffb45a, #ff7e3a)}
.hp-bar.critical{background:linear-gradient(90deg, #ff5a7a, #ff3050);animation:hpflash 1s infinite}
@keyframes hpflash{0%,100%{opacity:1}50%{opacity:.6}}
.hp-bar::after{content:'';position:absolute;top:0;left:0;right:0;height:50%;background:linear-gradient(180deg, rgba(255,255,255,.25), transparent)}
.hp-text{font-size:11px;color:var(--ink-1);margin-top:3px;letter-spacing:.05em;font-family:var(--f-mono)}
.battle-menu{display:grid;grid-template-columns:repeat(4,1fr);gap:8px;max-width:520px;margin:0 auto}
.battle-btn{padding:12px 8px;background:var(--glass);border:1px solid var(--glass-border);border-radius:var(--r-md);color:var(--ink-0);font-family:var(--f-display);font-size:13px;letter-spacing:.1em;cursor:pointer;transition:all .2s;text-transform:uppercase;backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);position:relative;overflow:hidden}
.battle-btn:hover:not(:disabled){background:rgba(127,255,229,.18);border-color:var(--lumen);transform:translateY(-2px)}
.battle-btn:disabled{opacity:.4;cursor:not-allowed}
.battle-btn .pp{font-size:10px;color:var(--lumen);display:block;margin-top:3px;letter-spacing:.05em}
#battle-message{position:absolute;top:30%;left:50%;transform:translate(-50%,-50%);text-align:center;font-size:18px;color:var(--ink-0);text-shadow:0 2px 12px rgba(0,0,0,.8);z-index:4;pointer-events:none;letter-spacing:.1em;line-height:1.5;max-width:80vw;animation:msgpop .4s ease}
@keyframes msgpop{from{opacity:0;transform:translate(-50%,-30%) scale(.85)}to{opacity:1;transform:translate(-50%,-50%) scale(1)}}
.dmg-num{position:absolute;font-weight:700;font-size:24px;color:var(--crimson);text-shadow:0 2px 8px rgba(0,0,0,.9);pointer-events:none;z-index:5;letter-spacing:.05em;animation:dmgfly 1.2s ease-out forwards}
.dmg-num.crit{color:var(--gold);font-size:32px}
.dmg-num.heal{color:var(--good)}
.dmg-num.miss{color:var(--ink-2);font-size:18px}
.dmg-num.se{color:var(--gold);font-size:30px}
@keyframes dmgfly{0%{opacity:0;transform:translateY(20px) scale(.5)}20%{opacity:1;transform:translateY(0) scale(1.2)}100%{opacity:0;transform:translateY(-60px) scale(.9)}}

/* === Capture minigame === */
#capture-modal{position:absolute;inset:0;background:rgba(0,0,0,.7);backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);z-index:35;display:none;align-items:center;justify-content:center;flex-direction:column}
#capture-modal.show{display:flex}
#capture-rings{position:relative;width:240px;height:240px}
.capture-ring{position:absolute;inset:0;border-radius:50%;border:4px solid;display:flex;align-items:center;justify-content:center;font-size:14px;color:var(--ink-0);font-weight:600;letter-spacing:.1em;cursor:pointer;transition:transform .05s}
.capture-ring.outer{border-color:var(--lumen);transform:scale(var(--ring-scale, 1))}
.capture-ring.inner{border-color:var(--gold);transform:scale(calc(var(--ring-scale, 1) * 0.6))}
.capture-ring.hit{background:rgba(127,255,229,.4);transform:scale(.95)}
#capture-hint{margin-top:20px;color:var(--ink-1);font-size:13px;letter-spacing:.15em;text-align:center;line-height:1.6}
#capture-timer{width:200px;height:6px;background:rgba(0,0,0,.5);border-radius:3px;margin-top:12px;overflow:hidden}
#capture-timer-fill{height:100%;background:linear-gradient(90deg, var(--lumen), var(--gold));transition:width .1s linear;width:100%}

/* === Menu screens === */
.menu-tabs{display:flex;gap:6px;margin-bottom:18px;flex-wrap:wrap;border-bottom:1px solid var(--glass-border);padding-bottom:12px}
.menu-tab{padding:8px 16px;background:transparent;border:1px solid transparent;border-radius:var(--r-sm);color:var(--ink-1);cursor:pointer;font-size:12px;letter-spacing:.15em;text-transform:uppercase;transition:all .2s}
.menu-tab:hover{color:var(--ink-0)}
.menu-tab.active{background:rgba(127,255,229,.15);border-color:var(--lumen-dim);color:var(--lumen)}

/* Party grid */
.party-grid{display:grid;grid-template-columns:repeat(auto-fill, minmax(160px, 1fr));gap:12px}
.party-card{background:var(--glass);border:1px solid var(--glass-border);border-radius:var(--r-md);padding:14px;transition:all .2s;cursor:pointer;position:relative;overflow:hidden}
.party-card:hover{border-color:var(--lumen);transform:translateY(-2px)}
.party-card.active{border-color:var(--lumen);box-shadow:0 0 24px rgba(127,255,229,.2)}
.party-card.fainted{opacity:.5;filter:grayscale(.6)}
.party-card .pc-name{font-size:14px;color:var(--ink-0);font-weight:600;letter-spacing:.05em}
.party-card .pc-lvl{font-size:11px;color:var(--lumen);margin-left:6px}
.party-card .pc-type{font-size:10px;color:var(--ink-1);letter-spacing:.1em;margin-top:2px;text-transform:uppercase}
.party-card .pc-mini{width:64px;height:64px;margin:8px auto;display:block}
.party-card .pc-hp{height:6px;background:rgba(0,0,0,.5);border-radius:3px;overflow:hidden;margin-top:8px}
.party-card .pc-hp-fill{height:100%;background:linear-gradient(90deg, #4af2a4, #2ad080);transition:width .3s}

/* Bestiary */
.bestiary-grid{display:grid;grid-template-columns:repeat(auto-fill, minmax(110px, 1fr));gap:10px}
.bestiary-card{background:var(--glass);border:1px solid var(--glass-border);border-radius:var(--r-md);padding:10px;text-align:center;transition:all .2s;cursor:default}
.bestiary-card.seen{border-color:var(--glass-border-bright)}
.bestiary-card.caught{border-color:var(--lumen);box-shadow:0 0 12px rgba(127,255,229,.15)}
.bestiary-card.unknown{opacity:.4;filter:grayscale(1)}
.bestiary-card .bc-mini{width:60px;height:60px;margin:0 auto 6px;display:block}
.bestiary-card .bc-name{font-size:11px;color:var(--ink-0);font-weight:500;letter-spacing:.05em}
.bestiary-card.unknown .bc-name{color:var(--ink-2)}

/* Items list */
.items-list{display:flex;flex-direction:column;gap:6px;max-height:60vh;overflow-y:auto;padding-right:6px}
.item-row{display:flex;align-items:center;justify-content:space-between;padding:10px 14px;background:var(--glass);border:1px solid var(--glass-border);border-radius:var(--r-md);transition:all .2s;cursor:default}
.item-row:hover{border-color:var(--lumen-dim)}
.item-name{font-size:13px;color:var(--ink-0)}
.item-desc{font-size:10px;color:var(--ink-2);margin-top:2px;letter-spacing:.05em}
.item-qty{font-family:var(--f-mono);color:var(--lumen);font-size:13px}

/* Map */
.zone-tabs{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:14px}
.zone-tab{padding:8px 12px;background:var(--glass);border:1px solid var(--glass-border);border-radius:var(--r-sm);color:var(--ink-1);cursor:pointer;font-size:11px;letter-spacing:.1em;transition:all .2s}
.zone-tab.active{background:rgba(127,255,229,.18);border-color:var(--lumen);color:var(--lumen)}
.zone-tab.locked{opacity:.4;cursor:not-allowed}
.map-canvas-wrap{width:100%;aspect-ratio:16/10;background:rgba(0,0,0,.3);border:1px solid var(--glass-border);border-radius:var(--r-md);position:relative;overflow:hidden}
#map-canvas{width:100%;height:100%;display:block}

/* Starter select */
.starter-grid{display:grid;grid-template-columns:repeat(auto-fit, minmax(180px, 1fr));gap:14px;margin-top:18px}
.starter-card{background:var(--glass);border:1px solid var(--glass-border);border-radius:var(--r-lg);padding:18px;text-align:center;cursor:pointer;transition:all .25s}
.starter-card:hover{border-color:var(--lumen);transform:translateY(-4px);box-shadow:0 12px 32px rgba(127,255,229,.2)}
.starter-card .sc-art{width:120px;height:120px;margin:0 auto 10px;display:block}
.starter-card .sc-name{font-size:16px;color:var(--ink-0);font-weight:600;letter-spacing:.05em}
.starter-card .sc-type{font-size:11px;color:var(--lumen);letter-spacing:.15em;margin:4px 0;text-transform:uppercase}
.starter-card .sc-lore{font-size:12px;color:var(--ink-1);line-height:1.5;margin-top:8px;font-style:italic}

/* Floating damage on overworld */
.ow-pop{position:absolute;font-size:11px;color:var(--ink-1);letter-spacing:.1em;background:rgba(0,0,0,.6);padding:3px 8px;border-radius:10px;pointer-events:none;z-index:8;animation:owpop 1.4s ease-out forwards;white-space:nowrap}
@keyframes owpop{0%{opacity:0;transform:translateY(0) scale(.8)}20%{opacity:1;transform:translateY(-10px) scale(1)}100%{opacity:0;transform:translateY(-50px) scale(.9)}}

/* Scrollbars */
::-webkit-scrollbar{width:8px;height:8px}
::-webkit-scrollbar-track{background:rgba(0,0,0,.2);border-radius:4px}
::-webkit-scrollbar-thumb{background:var(--ink-2);border-radius:4px}
::-webkit-scrollbar-thumb:hover{background:var(--lumen-dim)}

/* Tooltip */
.tooltip{position:relative}
.tooltip::after{content:attr(data-tip);position:absolute;bottom:calc(100% + 8px);left:50%;transform:translateX(-50%);background:var(--bg-1);color:var(--ink-0);padding:5px 10px;border-radius:6px;font-size:11px;white-space:nowrap;opacity:0;pointer-events:none;transition:opacity .2s;border:1px solid var(--glass-border);z-index:100;letter-spacing:.05em}
.tooltip:hover::after{opacity:1}

/* Status badges */
.status-badge{display:inline-block;padding:2px 8px;border-radius:10px;font-size:10px;letter-spacing:.1em;margin-right:4px;background:rgba(127,255,229,.15);color:var(--lumen);border:1px solid var(--lumen-dim);text-transform:uppercase}
.status-badge.burn{background:rgba(255,90,90,.15);color:#ff8a8a;border-color:#ff6868}
.status-badge.frost{background:rgba(120,200,255,.15);color:#9cdcff;border-color:#6cc4ff}
.status-badge.shock{background:rgba(255,234,120,.15);color:#ffea78;border-color:#ffd86e}
.status-badge.regen{background:rgba(124,242,168,.15);color:#7cf2a8;border-color:#5fdb90}
.status-badge.barrier{background:rgba(127,255,229,.15);color:var(--lumen);border-color:var(--lumen)}

/* Toast */
#toast{position:absolute;top:80px;left:50%;transform:translateX(-50%);background:var(--glass-strong);border:1px solid var(--glass-border-bright);padding:10px 18px;border-radius:var(--r-md);font-size:13px;letter-spacing:.1em;color:var(--lumen);z-index:60;opacity:0;transition:opacity .3s, transform .3s;backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);box-shadow:var(--shadow-soft);pointer-events:none}
#toast.show{opacity:1;transform:translateX(-50%) translateY(0)}
#toast.hide{transform:translateX(-50%) translateY(-20px)}

/* Choice modal */
.choice-list{display:flex;flex-direction:column;gap:8px}
.choice-btn{padding:14px 18px;background:var(--glass);border:1px solid var(--glass-border);border-radius:var(--r-md);color:var(--ink-0);text-align:left;cursor:pointer;transition:all .2s}
.choice-btn:hover{background:rgba(127,255,229,.15);border-color:var(--lumen);transform:translateX(4px)}
.choice-btn .cb-cost{color:var(--lumen);font-size:11px;letter-spacing:.1em;margin-top:4px;display:block}

/* Story vignette */
.vignette{position:absolute;inset:0;z-index:45;display:none;align-items:center;justify-content:center;padding:30px;pointer-events:none}
.vignette.show{display:flex;animation:vig-in .8s ease}
@keyframes vig-in{from{opacity:0}to{opacity:1}}
.vignette-text{max-width:600px;text-align:center;font-size:18px;line-height:1.8;color:var(--ink-0);text-shadow:0 2px 20px rgba(0,0,0,.9);letter-spacing:.05em;padding:30px;background:rgba(0,0,0,.4);border-radius:var(--r-lg);backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px);border:1px solid var(--glass-border)}
.vignette-text .vt-stage{display:block;font-size:11px;letter-spacing:.4em;color:var(--lumen);margin-bottom:14px;text-transform:uppercase}

@media (max-width: 720px){
  .battle-menu{grid-template-columns:repeat(2,1fr);max-width:none}
  #touch-controls{display:block}
  .modal{max-height:85vh;padding:18px}
  .hud-zone{font-size:10px}
  .hud-zone strong{font-size:12px}
  .title-mark{font-size:36px}
  .vignette-text{font-size:14px}
}
@media (max-height: 600px){
  #touch-controls{display:none}
}
</style>
</head>
<body>
<div id="app">
  <canvas id="game-canvas"></canvas>
  <div id="ui-layer"></div>
</div>
<script>

/* ============================================================
   AETHERMERE: WHISPERS OF THE HOLLOW
   An original creature-collecting tale
   ============================================================ */

'use strict';

// ---------- Utilities ----------
const rand = (min, max) => Math.random() * (max - min) + min;
const randInt = (min, max) => Math.floor(rand(min, max + 1));
const choice = arr => arr[randInt(0, arr.length - 1)];
const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
const pick = arr => arr[Math.floor(Math.random() * arr.length)];
const fmt = n => Math.floor(n).toString().padStart(4, ' ');

// ---------- Type System ----------
const TYPES = ['Cinder', 'Flora', 'Tidal', 'Storm', 'Crag', 'Lumen', 'Shade'];

// Type chart per spec:
// Type A > B > C > A  (A=Cinder, B=Flora, C=Tidal)
// D > A & C           (D=Storm)
// E > D & B           (E=Crag)
// F beats all slightly (F=Lumen)
// G resists all, weak to F (G=Shade)
const TYPE_CHART = {
  Cinder: { Flora: 2, Tidal: 0.5, Storm: 0.5, Crag: 1, Lumen: 1, Shade: 1 },
  Flora:  { Tidal: 2, Cinder: 0.5, Crag: 0.5, Storm: 1, Lumen: 1, Shade: 1 },
  Tidal:  { Cinder: 2, Flora: 0.5, Storm: 0.5, Crag: 1, Lumen: 1, Shade: 1 },
  Storm:  { Cinder: 2, Tidal: 2, Crag: 0.5, Flora: 1, Lumen: 1, Shade: 1 },
  Crag:   { Storm: 2, Flora: 2, Cinder: 1, Tidal: 1, Lumen: 1, Shade: 1 },
  Lumen:  { Cinder: 1.4, Flora: 1.4, Tidal: 1.4, Storm: 1.4, Crag: 1.4, Shade: 2 },
  Shade:  { Cinder: 0.5, Flora: 0.5, Tidal: 0.5, Storm: 0.5, Crag: 0.5, Lumen: 0.5 }
};
const TYPE_COLORS = {
  Cinder: '#ff8a5b', Flora: '#7cd66a', Tidal: '#5ec3ff',
  Storm: '#cba6ff', Crag: '#e0c891', Lumen: '#7fffe5', Shade: '#a48cff'
};

// ---------- Abilities (40+) ----------
const ABILITIES = {
  emberpaw:    { name: 'Emberpaw',    type: 'Cinder', power: 40, acc: 100, pp: 20, cost: 5,  fx: null, desc: 'A flickering claw of cinder.' },
  flamewake:   { name: 'Flamewake',   type: 'Cinder', power: 70, acc: 95,  pp: 12, cost: 12, fx: 'burn', chance: 0.25, desc: 'Scorches the foe. May burn.' },
  pyrepulse:   { name: 'Pyre Pulse',  type: 'Cinder', power: 60, acc: 100, pp: 14, cost: 10, fx: 'crit', desc: 'Searing flare with high crit rate.' },
  ashveil:     { name: 'Ash Veil',    type: 'Cinder', power: 0,  acc: 100, pp: 18, cost: 8,  fx: 'defup', desc: 'Shrouds in ash. Raises DEF.' },
  searstorm:   { name: 'Searstorm',   type: 'Cinder', power: 80, acc: 85,  pp: 8,  cost: 16, fx: 'burn', chance: 0.4, desc: 'Storm of embers. May burn.' },
  cinderfang:  { name: 'Cinderfang',  type: 'Cinder', power: 50, acc: 100, pp: 16, cost: 7,  fx: null, desc: 'Fanged strike wreathed in cinder.' },

  vinewhip:    { name: 'Vinewhip',    type: 'Flora',  power: 40, acc: 100, pp: 20, cost: 5,  fx: null, desc: 'A lashing vine tendril.' },
  bloomburst:  { name: 'Bloom Burst', type: 'Flora',  power: 65, acc: 100, pp: 12, cost: 11, fx: 'regen', desc: 'Floral surge. Heals user slightly.' },
  thornvolley: { name: 'Thorn Volley',type: 'Flora',  power: 25, acc: 95,  pp: 14, cost: 9,  fx: 'multihit', hits: 3, desc: 'Three thorn strikes.' },
  petalstorm:  { name: 'Petalstorm',  type: 'Flora',  power: 75, acc: 90,  pp: 10, cost: 14, fx: null, desc: 'A whirling storm of razor petals.' },
  rootmend:    { name: 'Root Mend',   type: 'Flora',  power: 0,  acc: 100, pp: 12, cost: 10, fx: 'heal', amt: 0.5, desc: 'Restores half max HP.' },
  barkplate:   { name: 'Bark Plate',  type: 'Flora',  power: 0,  acc: 100, pp: 18, cost: 8,  fx: 'defup', desc: 'Hardens bark. Raises DEF.' },

  tidesplash:  { name: 'Tidesplash',  type: 'Tidal',  power: 40, acc: 100, pp: 20, cost: 5,  fx: null, desc: 'A crashing wave.' },
  hydrolance:  { name: 'Hydro Lance', type: 'Tidal',  power: 70, acc: 95,  pp: 12, cost: 12, fx: null, desc: 'Piercing jet of water.' },
  brinesnare:  { name: 'Brine Snare', type: 'Tidal',  power: 50, acc: 100, pp: 16, cost: 8,  fx: 'frost', chance: 0.3, desc: 'Icy brine. May frost.' },
  riptide:     { name: 'Riptide',     type: 'Tidal',  power: 55, acc: 100, pp: 14, cost: 10, fx: 'multihit', hits: 2, desc: 'Two surging waves.' },
  aquaveil:    { name: 'Aqua Veil',   type: 'Tidal',  power: 0,  acc: 100, pp: 14, cost: 9,  fx: 'heal', amt: 0.4, desc: 'A soothing mist. Heals HP.' },
  deepdrain:   { name: 'Deepdrain',   type: 'Tidal',  power: 45, acc: 100, pp: 12, cost: 9,  fx: 'leech', desc: 'Drains foe life.' },

  sparkshot:   { name: 'Sparkshot',   type: 'Storm',  power: 40, acc: 100, pp: 20, cost: 5,  fx: null, desc: 'A quick jolt.' },
  thunder:     { name: 'Thunderclap', type: 'Storm',  power: 75, acc: 90,  pp: 10, cost: 14, fx: 'shock', chance: 0.3, desc: 'Crackling bolt. May shock.' },
  voltrush:    { name: 'Volt Rush',   type: 'Storm',  power: 35, acc: 100, pp: 14, cost: 8,  fx: 'priority', desc: 'Strikes first with electric speed.' },
  tempest:     { name: 'Tempest',     type: 'Storm',  power: 80, acc: 85,  pp: 8,  cost: 16, fx: 'shock', chance: 0.2, desc: 'Raging storm. May shock.' },
  galvanic:    { name: 'Galvanic',    type: 'Storm',  power: 0,  acc: 100, pp: 18, cost: 8,  fx: 'speedup', desc: 'Raises SPD.' },
  staticshell: { name: 'Static Shell',type: 'Storm',  power: 0,  acc: 100, pp: 16, cost: 7,  fx: 'barrier', desc: 'Erects a barrier. One free hit.' },

  pebblepunch: { name: 'Pebblepunch', type: 'Crag',   power: 40, acc: 100, pp: 20, cost: 5,  fx: null, desc: 'A small but firm strike.' },
  stonefall:   { name: 'Stonefall',   type: 'Crag',   power: 70, acc: 95,  pp: 12, cost: 12, fx: null, desc: 'Crushing rockfall.' },
  crystallance:{ name: 'Crystal Lance',type:'Crag',   power: 85, acc: 90,  pp: 8,  cost: 16, fx: null, desc: 'A piercing crystal spear.' },
  boulderbash: { name: 'Boulder Bash',type: 'Crag',   power: 95, acc: 80,  pp: 6,  cost: 20, fx: 'selfslow', desc: 'Massive slam. User loses SPD.' },
  graniteskin: { name: 'Granite Skin',type: 'Crag',   power: 0,  acc: 100, pp: 18, cost: 8,  fx: 'defup', desc: 'Stoneskin. Raises DEF.' },
  quakestomp:  { name: 'Quake Stomp', type: 'Crag',   power: 75, acc: 100, pp: 10, cost: 14, fx: null, desc: 'Shatters the ground beneath all.' },

  glimmer:     { name: 'Glimmer',     type: 'Lumen',  power: 50, acc: 100, pp: 16, cost: 7,  fx: null, desc: 'A radiant beam of light.' },
  photonlance: { name: 'Photon Lance',type: 'Lumen',  power: 80, acc: 95,  pp: 10, cost: 14, fx: null, desc: 'A piercing lance of pure light.' },
  spectrum:    { name: 'Spectrum',    type: 'Lumen',  power: 75, acc: 100, pp: 12, cost: 12, fx: 'crit', desc: 'Refracted beam with high crit.' },
  refract:     { name: 'Refract',     type: 'Lumen',  power: 0,  acc: 100, pp: 16, cost: 8,  fx: 'barrier', desc: 'Bends light. One free hit.' },
  brightheal:  { name: 'Bright Mend', type: 'Lumen',  power: 0,  acc: 100, pp: 12, cost: 10, fx: 'heal', amt: 0.5, desc: 'Warm light. Heals HP.' },
  auroraveil:  { name: 'Aurora Veil', type: 'Lumen',  power: 0,  acc: 100, pp: 10, cost: 12, fx: 'allup', desc: 'Raises ATK, DEF, SPD.' },

  umbralash:   { name: 'Umbralash',   type: 'Shade',  power: 50, acc: 100, pp: 16, cost: 7,  fx: null, desc: 'A claw wreathed in shadow.' },
  souldrain:   { name: 'Soul Drain',  type: 'Shade',  power: 45, acc: 100, pp: 12, cost: 9,  fx: 'leech', desc: 'Steals foe life essence.' },
  voidpulse:   { name: 'Void Pulse',  type: 'Shade',  power: 70, acc: 95,  pp: 12, cost: 12, fx: null, desc: 'A pulse of nothingness.' },
  shadowmist:  { name: 'Shadowmist',  type: 'Shade',  power: 0,  acc: 100, pp: 16, cost: 8,  fx: 'barrier', desc: 'Vanishes. One free hit.' },
  eclipse:     { name: 'Eclipse',     type: 'Shade',  power: 90, acc: 85,  pp: 6,  cost: 18, fx: 'defup', desc: 'Dark eclipse. High power.' },
  hexbind:     { name: 'Hexbind',     type: 'Shade',  power: 55, acc: 95,  pp: 14, cost: 10, fx: 'curse', desc: 'A draining hex. Damages over time.' },

  tackle:      { name: 'Tackle',      type: null,     power: 35, acc: 100, pp: 25, cost: 4,  fx: null, desc: 'A basic charge.' },
  bite:        { name: 'Bite',        type: null,     power: 50, acc: 100, pp: 18, cost: 6,  fx: null, desc: 'A snapping bite.' },
  quickstrike: { name: 'Quickstrike', type: null,     power: 30, acc: 100, pp: 16, cost: 6,  fx: 'priority', desc: 'Strikes first.' },
  focus:       { name: 'Focus',       type: null,     power: 0,  acc: 100, pp: 20, cost: 6,  fx: 'critup', desc: 'Raises crit rate next turn.' }
};
const ABILITY_IDS = Object.keys(ABILITIES);
console.log('Abilities loaded:', ABILITY_IDS.length);


// ---------- Creature Catalog (18 base + 9 transformed = 27) ----------
// Each creature: id, name, types[], base stats, learnset, lore, art (drawing fn), evolves
const CREATURES = {
  // Starters (zone 1)
  cinderkin: {
    id: 'cinderkin', name: 'Cinderkin', types: ['Cinder'],
    base: { hp: 38, atk: 47, def: 38, spd: 42 },
    learnset: { 1: ['tackle', 'emberpaw'], 6: ['cinderfang'], 12: ['flamewake'], 20: ['pyrepulse'], 30: ['searstorm'] },
    lore: 'A small ember sprite that flickers when it dreams.',
    evolves: { to: 'volcoynx', at: 22, item: 'embercatalyst' },
    shape: 'cinder'
  },
  lumipup: {
    id: 'lumipup', name: 'Lumipup', types: ['Lumen'],
    base: { hp: 40, atk: 38, def: 42, spd: 50 },
    learnset: { 1: ['tackle', 'glimmer'], 6: ['auroraveil'], 12: ['photonlance'], 20: ['spectrum'], 30: ['brightheal'] },
    lore: 'A pale fox that leaves a trail of fading starlight.',
    evolves: { to: 'astralswan', at: 22, item: 'lumencatalyst' },
    shape: 'lumen'
  },
  aqualing: {
    id: 'aqualing', name: 'Aqualing', types: ['Tidal'],
    base: { hp: 42, atk: 42, def: 44, spd: 40 },
    learnset: { 1: ['tackle', 'tidesplash'], 6: ['aquaveil'], 12: ['hydrolance'], 20: ['riptide'], 30: ['brinesnare'] },
    lore: 'A glassy tadpole that hums in harmony with the tide.',
    evolves: { to: 'tidewarden', at: 22, item: 'tidalcatalyst' },
    shape: 'tidal'
  },
  // Zone 1 Embergrove
  thornkin: {
    id: 'thornkin', name: 'Thornkin', types: ['Flora'],
    base: { hp: 44, atk: 48, def: 46, spd: 36 },
    learnset: { 1: ['tackle', 'vinewhip'], 8: ['thornvolley'], 16: ['barkplate'], 24: ['petalstorm'] },
    lore: 'A walking thicket with a laugh like snapping twigs.',
    evolves: { to: 'forestlord', at: 26, item: 'floracatalyst' },
    shape: 'flora'
  },
  pebblump: {
    id: 'pebblump', name: 'Pebblump', types: ['Crag'],
    base: { hp: 50, atk: 44, def: 56, spd: 28 },
    learnset: { 1: ['tackle', 'pebblepunch'], 8: ['graniteskin'], 16: ['stonefall'], 24: ['boulderbash'] },
    lore: 'A rolling stone that only stops when it hears music.',
    evolves: { to: 'stonearch', at: 28, item: 'cragcatalyst' },
    shape: 'crag'
  },
  sparkimp: {
    id: 'sparkimp', name: 'Sparkimp', types: ['Storm'],
    base: { hp: 36, atk: 42, def: 34, spd: 52 },
    learnset: { 1: ['sparkshot', 'quickstrike'], 8: ['voltrush'], 16: ['galvanic'], 24: ['thunder'] },
    lore: 'A jittery sprite that hums in 5/4 time.',
    evolves: null, shape: 'storm'
  },
  // Zone 2 Tidalcoast
  coralyn: {
    id: 'coralyn', name: 'Coralyn', types: ['Tidal', 'Flora'],
    base: { hp: 46, atk: 44, def: 50, spd: 36 },
    learnset: { 1: ['tidesplash', 'vinewhip'], 9: ['bloomburst'], 18: ['riptide'], 27: ['petalstorm'] },
    lore: 'A coral that sings when the moon is waning.',
    evolves: { to: 'reeflord', at: 28, item: 'tidalcatalyst' },
    shape: 'coral'
  },
  brinewraith: {
    id: 'brinewraith', name: 'Brinewraith', types: ['Tidal', 'Shade'],
    base: { hp: 42, atk: 50, def: 38, spd: 46 },
    learnset: { 1: ['tidesplash', 'umbralash'], 9: ['souldrain'], 18: ['hydrolance'], 27: ['voidpulse'] },
    lore: 'A drowned thing that has forgotten its name.',
    evolves: null, shape: 'wraith'
  },
  // Zone 3 Voltfields
  voltwing: {
    id: 'voltwing', name: 'Voltwing', types: ['Storm', 'Lumen'],
    base: { hp: 42, atk: 48, def: 40, spd: 54 },
    learnset: { 1: ['sparkshot', 'glimmer'], 9: ['voltrush'], 18: ['photonlance'], 27: ['tempest'] },
    lore: 'A stormbird whose feathers hum with static.',
    evolves: null, shape: 'wing'
  },
  forgebeetle: {
    id: 'forgebeetle', name: 'Forgebeetle', types: ['Cinder', 'Crag'],
    base: { hp: 52, atk: 52, def: 54, spd: 26 },
    learnset: { 1: ['emberpaw', 'pebblepunch'], 9: ['stonefall'], 18: ['flamewake'], 27: ['crystallance'] },
    lore: 'A beetle of magma and iron that beats its wings slowly.',
    evolves: null, shape: 'beetle'
  },
  // Zone 4 Lumenglass
  crystoid: {
    id: 'crystoid', name: 'Crystoid', types: ['Lumen', 'Crag'],
    base: { hp: 44, atk: 46, def: 56, spd: 38 },
    learnset: { 1: ['glimmer', 'pebblepunch'], 9: ['refract'], 18: ['photonlance'], 27: ['crystallance'] },
    lore: 'A living geode that chimes when struck.',
    evolves: { to: 'prismcrystal', at: 30, item: 'lumencatalyst' },
    shape: 'crystal'
  },
  specter: {
    id: 'specter', name: 'Specter', types: ['Shade', 'Lumen'],
    base: { hp: 40, atk: 48, def: 38, spd: 50 },
    learnset: { 1: ['umbralash', 'glimmer'], 9: ['shadowmist'], 18: ['voidpulse'], 27: ['photonlance'] },
    lore: 'A wisp that cannot decide if it is a star or a memory.',
    evolves: null, shape: 'specter'
  },
  // Zone 5 Prismpeaks
  prismoth: {
    id: 'prismoth', name: 'Prismoth', types: ['Lumen'],
    base: { hp: 38, atk: 50, def: 36, spd: 56 },
    learnset: { 1: ['glimmer', 'quickstrike'], 9: ['spectrum'], 18: ['auroraveil'], 27: ['photonlance'] },
    lore: 'A moth whose wings hold every color of dawn.',
    evolves: null, shape: 'moth'
  },
  // Zone 6 Shadehollow
  umbraclaw: {
    id: 'umbraclaw', name: 'Umbraclaw', types: ['Shade'],
    base: { hp: 44, atk: 56, def: 38, spd: 50 },
    learnset: { 1: ['umbralash', 'bite'], 9: ['shadowmist'], 18: ['eclipse'], 27: ['voidpulse'] },
    lore: 'A clawed terror that walks only where no lantern burns.',
    evolves: { to: 'shadequeen', at: 30, item: 'shadecatalyst' },
    shape: 'umbra'
  },
  hollowkin: {
    id: 'hollowkin', name: 'Hollowkin', types: ['Shade', 'Crag'],
    base: { hp: 50, atk: 50, def: 52, spd: 32 },
    learnset: { 1: ['umbralash', 'pebblepunch'], 9: ['hexbind'], 18: ['stonefall'], 27: ['eclipse'] },
    lore: 'A sentinel carved by no hand, left in a forgotten hall.',
    evolves: null, shape: 'golem'
  },
  // Extra
  mossback: {
    id: 'mossback', name: 'Mossback', types: ['Flora', 'Crag'],
    base: { hp: 54, atk: 42, def: 56, spd: 28 },
    learnset: { 1: ['vinewhip', 'pebblepunch'], 9: ['barkplate'], 18: ['bloomburst'], 27: ['quakestomp'] },
    lore: 'A patient grazer with a forest on its shoulders.',
    evolves: null, shape: 'mossback'
  },
  drizztip: {
    id: 'drizztip', name: 'Drizztip', types: ['Tidal', 'Storm'],
    base: { hp: 40, atk: 44, def: 38, spd: 48 },
    learnset: { 1: ['tidesplash', 'sparkshot'], 9: ['riptide'], 18: ['voltrush'], 27: ['thunder'] },
    lore: 'A seahorse with lightning running through its mane.',
    evolves: null, shape: 'seahorse'
  },
  ashlung: {
    id: 'ashlung', name: 'Ashlung', types: ['Cinder', 'Shade'],
    base: { hp: 46, atk: 54, def: 40, spd: 42 },
    learnset: { 1: ['emberpaw', 'umbralash'], 9: ['souldrain'], 18: ['flamewake'], 27: ['eclipse'] },
    lore: 'A wolf that hunts by the embers of extinguished hearths.',
    evolves: null, shape: 'wolf'
  },
  // Transformations (9)
  volcoynx: {
    id: 'volcoynx', name: 'Volcoynx', types: ['Cinder', 'Storm'],
    base: { hp: 60, atk: 72, def: 56, spd: 76 },
    learnset: { 22: ['searstorm', 'voltrush'], 30: ['tempest'], 40: ['eclipse'] },
    lore: 'A cinder-eyed lynx that sparks when it purrs.',
    isTransformation: true, evolves: null, shape: 'cinder'
  },
  astralswan: {
    id: 'astralswan', name: 'Astralswan', types: ['Lumen', 'Shade'],
    base: { hp: 64, atk: 60, def: 64, spd: 80 },
    learnset: { 22: ['photonlance', 'shadowmist'], 30: ['spectrum'], 40: ['eclipse'] },
    lore: 'A swan of light that swims through the night sky.',
    isTransformation: true, evolves: null, shape: 'lumen'
  },
  tidewarden: {
    id: 'tidewarden', name: 'Tidewarden', types: ['Tidal', 'Crag'],
    base: { hp: 70, atk: 66, def: 72, spd: 60 },
    learnset: { 22: ['hydrolance', 'quakestomp'], 30: ['boulderbash'], 40: ['crystallance'] },
    lore: 'A reef-turtle whose shell holds a thousand tides.',
    isTransformation: true, evolves: null, shape: 'tidal'
  },
  forestlord: {
    id: 'forestlord', name: 'Forestlord', types: ['Flora', 'Crag'],
    base: { hp: 78, atk: 70, def: 76, spd: 50 },
    learnset: { 26: ['petalstorm', 'quakestomp'], 34: ['crystallance'], 42: ['boulderbash'] },
    lore: 'A walking wood with mountains on its brow.',
    isTransformation: true, evolves: null, shape: 'flora'
  },
  stonearch: {
    id: 'stonearch', name: 'Stonearch', types: ['Crag', 'Lumen'],
    base: { hp: 84, atk: 70, def: 88, spd: 42 },
    learnset: { 28: ['crystallance', 'photonlance'], 36: ['boulderbash'], 44: ['spectrum'] },
    lore: 'A glowing gate carved by patient centuries.',
    isTransformation: true, evolves: null, shape: 'crag'
  },
  reeflord: {
    id: 'reeflord', name: 'Reeflord', types: ['Tidal', 'Flora'],
    base: { hp: 72, atk: 66, def: 78, spd: 58 },
    learnset: { 28: ['riptide', 'bloomburst'], 36: ['petalstorm'], 44: ['hydrolance'] },
    lore: 'A coral mind the size of a city block.',
    isTransformation: true, evolves: null, shape: 'coral'
  },
  thunderhead: {
    id: 'thunderhead', name: 'Thunderhead', types: ['Storm', 'Lumen'],
    base: { hp: 70, atk: 78, def: 64, spd: 88 },
    learnset: { 30: ['tempest', 'photonlance'], 38: ['eclipse'], 46: ['spectrum'] },
    lore: 'A small god that walks only when the sky is bruised.',
    isTransformation: true, evolves: null, shape: 'storm'
  },
  prismcrystal: {
    id: 'prismcrystal', name: 'Prismcrystal', types: ['Lumen', 'Shade'],
    base: { hp: 68, atk: 72, def: 82, spd: 70 },
    learnset: { 30: ['spectrum', 'voidpulse'], 38: ['auroraveil'], 46: ['eclipse'] },
    lore: 'A geometry of light and absence that thinks in seven colors.',
    isTransformation: true, evolves: null, shape: 'crystal'
  },
  shadequeen: {
    id: 'shadequeen', name: 'Shadequeen', types: ['Shade'],
    base: { hp: 74, atk: 86, def: 64, spd: 84 },
    learnset: { 30: ['eclipse', 'souldrain'], 38: ['voidpulse'], 46: ['hexbind'] },
    lore: 'A monarch of dusk; the lanterns dim when she passes.',
    isTransformation: true, evolves: null, shape: 'umbra'
  }
};
const CREATURE_IDS = Object.keys(CREATURES);
const STARTERS = ['cinderkin', 'lumipup', 'aqualing'];

console.log('Creatures loaded:', CREATURE_IDS.length);


// ---------- Items ----------
const ITEMS = {
  // Collection items (3 tiers)
  wrappingshard: { name: 'Wrapping Shard', type: 'collection', tier: 1, desc: 'A splinter of woven light. Catches weakened foes.', catchBonus: 0.18, price: 50 },
  wovenlattice:  { name: 'Woven Lattice',  type: 'collection', tier: 2, desc: 'A lattice of folded light. Steadier bond.', catchBonus: 0.32, price: 200 },
  echoframe:     { name: 'Echo Frame',     type: 'collection', tier: 3, desc: 'A ring that holds a small space. Strong bond.', catchBonus: 0.5,  price: 800 },
  // Restoration
  mossdrop:      { name: 'Mossdrop',       type: 'heal', amt: 0.3, desc: 'Heals 30% of max HP.', price: 30 },
  dawnvial:      { name: 'Dawn Vial',      type: 'heal', amt: 0.6, desc: 'Heals 60% of max HP.', price: 90 },
  elixirlume:    { name: 'Elixir of Lume', type: 'heal', amt: 1.0, desc: 'Fully restores HP.',  price: 250 },
  wakebell:      { name: 'Wakebell',       type: 'revive', amt: 0.5, desc: 'Revives a fainted creature, half HP.', price: 200 },
  // Catalysts
  embercatalyst: { name: 'Ember Catalyst', type: 'catalyst', for: 'Cinder', desc: 'Catalyst for Cinder evolutions.', price: 500 },
  lumencatalyst: { name: 'Lumen Catalyst', type: 'catalyst', for: 'Lumen',  desc: 'Catalyst for Lumen evolutions.', price: 500 },
  tidalcatalyst: { name: 'Tidal Catalyst', type: 'catalyst', for: 'Tidal',  desc: 'Catalyst for Tidal evolutions.', price: 500 },
  floracatalyst: { name: 'Flora Catalyst', type: 'catalyst', for: 'Flora',  desc: 'Catalyst for Flora evolutions.', price: 500 },
  cragcatalyst:  { name: 'Crag Catalyst',  type: 'catalyst', for: 'Crag',   desc: 'Catalyst for Crag evolutions.', price: 500 },
  shadecatalyst: { name: 'Shade Catalyst', type: 'catalyst', for: 'Shade',  desc: 'Catalyst for Shade evolutions.', price: 500 },
  prismcatalyst: { name: 'Prism Catalyst', type: 'universal', desc: 'Universal evolution catalyst.', price: 2000 },
  // Field
  lurepebble:    { name: 'Lure Pebble',    type: 'field', desc: 'Raises wild encounter rate briefly.', price: 80 },
  wardchime:     { name: 'Ward Chime',     type: 'field', desc: 'Lowers encounter rate briefly.', price: 80 }
};

// ---------- Zones ----------
// 6 zones with tile-based overworlds
const ZONES = [
  {
    id: 0, name: 'Embergrove', tagline: 'Where the first embers still remember the sun.',
    bgGradient: ['#3a0f0f', '#1f0820', '#0d0410'],
    accent: '#ff8a5b', particle: 'ember',
    encounterPool: [
      { c: 'cinderkin', w: 30 }, { c: 'thornkin', w: 30 },
      { c: 'pebblump', w: 20 }, { c: 'sparkimp', w: 15 },
      { c: 'mossback', w: 5 }
    ],
    encounterRate: 0.10, levelRange: [3, 7],
    map: 'embergrove', anchor: { c: 'ashlung', level: 10, name: 'The Hearthwarden' },
    story: 'Embergrove',
    storyNode: 'The first grove burns in hues of remembered daybreak. A path of soft cinder leads to a shrine where a small flame sits patient as a held breath.'
  },
  {
    id: 1, name: 'Tidalcoast', tagline: 'A shoreline that hums to itself between the waves.',
    bgGradient: ['#0a2240', '#102a4a', '#04101e'],
    accent: '#5ec3ff', particle: 'bubble',
    encounterPool: [
      { c: 'aqualing', w: 30 }, { c: 'coralyn', w: 25 },
      { c: 'brinewraith', w: 15 }, { c: 'drizztip', w: 15 },
      { c: 'thornkin', w: 10 }, { c: 'mossback', w: 5 }
    ],
    encounterRate: 0.10, levelRange: [8, 13],
    map: 'tidalcoast', anchor: { c: 'brinewraith', level: 15, name: 'The Drowning Bell' },
    story: 'Tidalcoast',
    storyNode: 'The water here sings in three-part harmony. At the cliff, a bell made of glass and kelp marks the boundary between the still and the moving.'
  },
  {
    id: 2, name: 'Voltfields', tagline: 'Grass that listens for thunder, sky that remembers lightning.',
    bgGradient: ['#1f1240', '#160a30', '#0a0418'],
    accent: '#cba6ff', particle: 'spark',
    encounterPool: [
      { c: 'sparkimp', w: 25 }, { c: 'voltwing', w: 25 },
      { c: 'forgebeetle', w: 20 }, { c: 'drizztip', w: 15 },
      { c: 'cinderkin', w: 10 }, { c: 'brinewraith', w: 5 }
    ],
    encounterRate: 0.11, levelRange: [14, 19],
    map: 'voltfields', anchor: { c: 'voltwing', level: 21, name: 'The Storm-Hollow' },
    story: 'Voltfields',
    storyNode: 'The lightning here does not strike. It visits. Beneath each bolt is a small pool where sky-colored fish will one day surface.'
  },
  {
    id: 3, name: 'Lumenglass', tagline: 'A garden of frozen music, lit from within.',
    bgGradient: ['#0f2a3a', '#0a1a30', '#040a18'],
    accent: '#7fffe5', particle: 'dust',
    encounterPool: [
      { c: 'crystoid', w: 30 }, { c: 'prismoth', w: 25 },
      { c: 'specter', w: 20 }, { c: 'voltwing', w: 15 },
      { c: 'forgebeetle', w: 10 }
    ],
    encounterRate: 0.11, levelRange: [20, 26],
    map: 'lumenglass', anchor: null,
    story: 'Lumenglass',
    storyNode: 'No sun crosses this sky. Instead, every shard of glass remembers a different dawn, and the air tastes faintly of oranges.'
  },
  {
    id: 4, name: 'Prismpeaks', tagline: 'Where the mountains are made of folded light.',
    bgGradient: ['#3a1850', '#1f0c34', '#0a0418'],
    accent: '#7fffe5', particle: 'crystal',
    encounterPool: [
      { c: 'prismoth', w: 30 }, { c: 'crystoid', w: 25 },
      { c: 'specter', w: 20 }, { c: 'voltwing', w: 15 },
      { c: 'umbraclaw', w: 10 }
    ],
    encounterRate: 0.12, levelRange: [27, 33],
    map: 'prismpeaks', anchor: { c: 'prismoth', level: 32, name: 'The Refracting Throne' },
    story: 'Prismpeaks',
    storyNode: 'Each peak is a frozen chord. Climbers who listen find that one of them always sings the song they were humming as a child.'
  },
  {
    id: 5, name: 'Shadehollow', tagline: 'The final garden, where the lanterns do not go.',
    bgGradient: ['#100820', '#08041a', '#02020a'],
    accent: '#a48cff', particle: 'shade',
    encounterPool: [
      { c: 'umbraclaw', w: 30 }, { c: 'hollowkin', w: 25 },
      { c: 'specter', w: 20 }, { c: 'prismoth', w: 15 },
      { c: 'ashlung', w: 10 }
    ],
    encounterRate: 0.13, levelRange: [34, 44],
    map: 'shadehollow', anchor: { c: 'umbraclaw', level: 42, name: 'The Hollow Monarch' },
    story: 'Shadehollow',
    storyNode: 'In the deepest hall, a throne of broken lanterns awaits a guest. The hollows are polite. They simply never sleep.'
  }
];

// ---------- Zone Map Data (tile-based) ----------
// Tiles: 0=path, 1=grass, 2=water, 3=tree, 4=rock, 5=shrine, 6=anchor, 7=brush, 8=wall, 9=portal
// 20 wide x 15 tall
function buildZoneMap(zoneId) {
  // We'll generate zone maps programmatically with consistent features
  const W = 20, H = 15;
  const map = [];
  for (let y = 0; y < H; y++) {
    const row = [];
    for (let x = 0; x < W; x++) {
      row.push(1); // default grass
    }
    map.push(row);
  }
  // Borders
  for (let x = 0; x < W; x++) { map[0][x] = 8; map[H-1][x] = 8; }
  for (let y = 0; y < H; y++) { map[y][0] = 8; map[y][W-1] = 8; }
  
  // Generate a meandering path
  let cx = 1, cy = Math.floor(H/2);
  map[cy][cx] = 0;
  while (cx < W-2) {
    const dir = Math.random();
    if (dir < 0.5) { cx++; map[cy][cx] = 0; }
    else if (dir < 0.75 && cy > 2) { cy--; map[cy][cx] = 0; }
    else if (cy < H-3) { cy++; map[cy][cx] = 0; }
    if (Math.random() < 0.15 && cx < W-3) { cx++; map[cy][cx] = 0; }
  }
  
  // Zone-specific features
  if (zoneId === 0) { // Embergrove - cinder trees
    for (let i = 0; i < 18; i++) {
      const x = randInt(2, W-3), y = randInt(1, H-2);
      if (map[y][x] === 1) map[y][x] = 3;
    }
    for (let i = 0; i < 6; i++) {
      const x = randInt(2, W-3), y = randInt(1, H-2);
      if (map[y][x] === 1) map[y][x] = 4;
    }
  } else if (zoneId === 1) { // Tidalcoast - water
    for (let y = 2; y < 6; y++) {
      for (let x = 1; x < W-1; x++) {
        if (Math.random() < 0.5 && map[y][x] === 1) map[y][x] = 2;
      }
    }
    for (let i = 0; i < 10; i++) {
      const x = randInt(2, W-3), y = randInt(7, H-2);
      if (map[y][x] === 1) map[y][x] = 3;
    }
  } else if (zoneId === 2) { // Voltfields - rocks + grass
    for (let i = 0; i < 15; i++) {
      const x = randInt(2, W-3), y = randInt(1, H-2);
      if (map[y][x] === 1) map[y][x] = 4;
    }
    for (let i = 0; i < 12; i++) {
      const x = randInt(2, W-3), y = randInt(1, H-2);
      if (map[y][x] === 1) map[y][x] = 3;
    }
  } else if (zoneId === 3) { // Lumenglass - crystal trees
    for (let i = 0; i < 12; i++) {
      const x = randInt(2, W-3), y = randInt(1, H-2);
      if (map[y][x] === 1) map[y][x] = 3;
    }
    for (let i = 0; i < 8; i++) {
      const x = randInt(2, W-3), y = randInt(1, H-2);
      if (map[y][x] === 1) map[y][x] = 4;
    }
  } else if (zoneId === 4) { // Prismpeaks - mountains
    for (let i = 0; i < 20; i++) {
      const x = randInt(2, W-3), y = randInt(1, H-2);
      if (map[y][x] === 1) map[y][x] = 4;
    }
  } else if (zoneId === 5) { // Shadehollow - shades
    for (let i = 0; i < 22; i++) {
      const x = randInt(2, W-3), y = randInt(1, H-2);
      if (map[y][x] === 1) map[y][x] = 7;
    }
  }
  
  // Find a path cell for player start
  let startX = 1, startY = Math.floor(H/2);
  for (let y = 0; y < H; y++) {
    for (let x = 0; x < W; x++) {
      if (map[y][x] === 0) { startX = x; startY = y; y = H; break; }
    }
  }
  
  // Place shrine (always near center, on path)
  let shrineX = Math.floor(W/2), shrineY = Math.floor(H/2);
  for (let r = 0; r < 8; r++) {
    for (let dy = -r; dy <= r; dy++) for (let dx = -r; dx <= r; dx++) {
      const x = shrineX + dx, y = shrineY + dy;
      if (x > 1 && x < W-1 && y > 1 && y < H-1 && map[y][x] === 0) {
        map[y][x] = 5; shrineX = x; shrineY = y; r = 99; dy = 99; dx = 99;
      }
    }
  }
  
  // Place portal at start (for zone travel)
  let portalX = 1, portalY = Math.floor(H/2);
  for (let dy = -2; dy <= 2; dy++) {
    const y = Math.floor(H/2) + dy;
    if (y > 0 && y < H-1 && map[y][1] === 0) { portalY = y; break; }
  }
  map[portalY][1] = 9;
  
  // Place anchor (boss) at end of path
  let anchorX = W-2, anchorY = Math.floor(H/2);
  map[anchorY][anchorX] = 6;
  
  return { tiles: map, w: W, h: H, startX, startY, shrineX, shrineY, anchorX, anchorY, portalX, portalY };
}

console.log('Zones loaded:', ZONES.length);


// ---------- Audio System (Web Audio API) ----------
const Audio = (() => {
  let ctx = null;
  let masterGain = null;
  let musicGain = null;
  let sfxGain = null;
  let musicNodes = [];
  let musicInterval = null;
  let muted = false;
  
  function init() {
    if (ctx) return;
    try {
      ctx = new (window.AudioContext || window.webkitAudioContext)();
      masterGain = ctx.createGain();
      masterGain.connect(ctx.destination);
      musicGain = ctx.createGain();
      musicGain.gain.value = 0.18;
      musicGain.connect(masterGain);
      sfxGain = ctx.createGain();
      sfxGain.gain.value = 0.32;
      sfxGain.connect(masterGain);
    } catch (e) {
      console.warn('Audio init failed', e);
    }
  }
  
  function resume() {
    if (ctx && ctx.state === 'suspended') ctx.resume();
  }
  
  function setMuted(m) {
    muted = m;
    if (masterGain) masterGain.gain.value = m ? 0 : 1;
  }
  
  // Play a single tone
  function tone(freq, dur, type = 'sine', vol = 0.4, dest = sfxGain) {
    if (!ctx || muted) return;
    const osc = ctx.createOscillator();
    const g = ctx.createGain();
    osc.type = type;
    osc.frequency.value = freq;
    g.gain.setValueAtTime(0, ctx.currentTime);
    g.gain.linearRampToValueAtTime(vol, ctx.currentTime + 0.01);
    g.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + dur);
    osc.connect(g);
    g.connect(dest);
    osc.start();
    osc.stop(ctx.currentTime + dur + 0.05);
  }
  
  // SFX
  const sfx = {
    hit: () => { tone(180, 0.12, 'square', 0.3); tone(140, 0.08, 'sawtooth', 0.2); },
    crit: () => { tone(420, 0.1, 'square', 0.4); setTimeout(() => tone(280, 0.15, 'square', 0.35), 50); },
    super: () => { tone(520, 0.12, 'triangle', 0.4); setTimeout(() => tone(680, 0.15, 'sine', 0.4), 80); },
    miss: () => { tone(220, 0.2, 'sine', 0.2); },
    heal: () => { tone(660, 0.2, 'sine', 0.3); setTimeout(() => tone(880, 0.25, 'sine', 0.3), 100); },
    capture: () => { tone(440, 0.15, 'sine', 0.4); setTimeout(() => tone(660, 0.15, 'sine', 0.4), 100); setTimeout(() => tone(880, 0.3, 'sine', 0.4), 200); },
    capturefail: () => { tone(330, 0.2, 'square', 0.3); setTimeout(() => tone(220, 0.3, 'square', 0.3), 150); },
    levelup: () => { [523, 659, 784, 1047].forEach((f, i) => setTimeout(() => tone(f, 0.18, 'sine', 0.4), i * 90)); },
    menu: () => { tone(800, 0.06, 'square', 0.25); },
    confirm: () => { tone(660, 0.08, 'sine', 0.35); setTimeout(() => tone(880, 0.1, 'sine', 0.35), 60); },
    cancel: () => { tone(330, 0.1, 'square', 0.25); setTimeout(() => tone(220, 0.1, 'square', 0.25), 80); },
    victory: () => { [523, 659, 784, 1047, 1319].forEach((f, i) => setTimeout(() => tone(f, 0.2, 'sine', 0.4), i * 80)); },
    fanf: () => { [392, 523, 659, 784, 1047].forEach((f, i) => setTimeout(() => tone(f, 0.18, 'triangle', 0.45), i * 110)); },
    encounter: () => { tone(110, 0.15, 'square', 0.3); setTimeout(() => tone(70, 0.3, 'square', 0.4), 100); },
    burn: () => { tone(220, 0.15, 'sawtooth', 0.25); },
    shock: () => { tone(1200, 0.08, 'square', 0.3); setTimeout(() => tone(800, 0.08, 'square', 0.3), 60); },
    step: () => { tone(140 + Math.random() * 20, 0.04, 'square', 0.12); }
  };
  
  // Music: simple looping melody
  const melodies = {
    // Note frequencies
    overworld: {
      bpm: 95,
      lead: [
        // A simple, hopeful melody
        {n:'E4',d:0.5},{n:'G4',d:0.5},{n:'A4',d:0.5},{n:'B4',d:1.0},
        {n:'A4',d:0.5},{n:'G4',d:0.5},{n:'E4',d:1.0},
        {n:'D4',d:0.5},{n:'E4',d:0.5},{n:'G4',d:0.5},{n:'A4',d:1.0},
        {n:'G4',d:0.5},{n:'E4',d:0.5},{n:'D4',d:1.5}
      ],
      pad: [ // Bass line
        {n:'A2',d:2.0},{n:'E3',d:2.0},{n:'F3',d:2.0},{n:'D3',d:2.0}
      ]
    },
    battle: {
      bpm: 130,
      lead: [
        {n:'E4',d:0.25},{n:'E4',d:0.25},{n:'G4',d:0.5},
        {n:'E4',d:0.25},{n:'A4',d:0.5},{n:'G4',d:0.5},
        {n:'E4',d:0.25},{n:'E4',d:0.25},{n:'B4',d:0.5},
        {n:'A4',d:0.5},{n:'G4',d:0.5},{n:'E4',d:0.5}
      ],
      pad: [
        {n:'A2',d:1.0},{n:'A2',d:1.0},{n:'E3',d:1.0},{n:'E3',d:1.0}
      ]
    },
    title: {
      bpm: 75,
      lead: [
        {n:'A4',d:1.0},{n:'E5',d:1.0},{n:'D5',d:1.0},{n:'C5',d:1.0},
        {n:'B4',d:1.0},{n:'A4',d:2.0}
      ],
      pad: [
        {n:'A2',d:4.0},{n:'E3',d:4.0}
      ]
    }
  };
  
  const NOTE_FREQS = {
    'C2':65.41,'D2':73.42,'E2':82.41,'F2':87.31,'G2':98.00,'A2':110.00,'B2':123.47,
    'C3':130.81,'D3':146.83,'E3':164.81,'F3':174.61,'G3':196.00,'A3':220.00,'B3':246.94,
    'C4':261.63,'D4':293.66,'E4':329.63,'F4':349.23,'G4':392.00,'A4':440.00,'B4':493.88,
    'C5':523.25,'D5':587.33,'E5':659.25,'F5':698.46,'G5':783.99,'A5':880.00,'B5':987.77,
    'C6':1046.50,'D6':1174.66,'E6':1318.51
  };
  
  let currentMusic = null;
  let musicTime = 0;
  let musicTimeouts = [];
  
  function stopMusic() {
    musicTimeouts.forEach(t => clearTimeout(t));
    musicTimeouts = [];
    if (musicInterval) clearInterval(musicInterval);
    musicInterval = null;
  }
  
  function playMusic(name) {
    if (!ctx) return;
    if (currentMusic === name) return;
    stopMusic();
    currentMusic = name;
    const m = melodies[name];
    if (!m) return;
    const beatMs = 60000 / m.bpm;
    
    function scheduleMelody(melody, type, vol) {
      let t = 0;
      melody.forEach(note => {
        const freq = NOTE_FREQS[note.n];
        if (!freq) { t += note.d; return; }
        const to = setTimeout(() => {
          if (muted) return;
          const osc = ctx.createOscillator();
          const g = ctx.createGain();
          osc.type = type;
          osc.frequency.value = freq;
          g.gain.setValueAtTime(0, ctx.currentTime);
          g.gain.linearRampToValueAtTime(vol, ctx.currentTime + 0.02);
          g.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + note.d * (beatMs/1000) * 0.9);
          osc.connect(g);
          g.connect(musicGain);
          osc.start();
          osc.stop(ctx.currentTime + note.d * (beatMs/1000));
        }, t * beatMs);
        musicTimeouts.push(to);
        t += note.d;
      });
      return t * beatMs;
    }
    
    // Lead
    const leadDur = scheduleMelody(m.lead, 'triangle', 0.18);
    // Pad (slower)
    setTimeout(() => scheduleMelody(m.pad, 'sine', 0.10), 100);
    // Loop
    const loopDur = Math.max(leadDur, 4000);
    const loopId = setInterval(() => {
      if (currentMusic !== name) return;
      scheduleMelody(m.lead, 'triangle', 0.18);
      scheduleMelody(m.pad, 'sine', 0.10);
    }, loopDur);
    musicInterval = loopId;
  }
  
  return { init, resume, setMuted, sfx, playMusic, stopMusic, tone, isMuted: () => muted };
})();


// ---------- Game State ----------
const Game = {
  state: 'title', // title, starter, tutorial, exploration, battle, dialogue, menu, gameover, victory
  party: [],      // 6 creatures
  storage: [],    // caught creatures not in party (max 30)
  inventory: {},
  gold: 100,
  seen: new Set(),
  caught: new Set(),
  currentZone: 0,
  unlockedZones: 1,
  playerX: 1,
  playerY: 7,
  playerDir: 'down',
  playerStep: 0,
  zoneMap: null,
  defeatedAnchors: new Set(),
  stepsTaken: 0,
  totalEncounters: 0,
  totalCaught: 0,
  storyStage: 0, // 0-3
  tutorialStep: 0,
  awaitingInput: false,
  hasSave: false,
  activeCreature: 0, // index in party
  activeBattle: null,
  menuOpen: false,
  muted: false,
  paused: false,
  transitions: { fade: 0, dir: 0 },
  postGame: false,
  finalVictory: false
};

const MAX_LEVEL = 50;
const MAX_PARTY = 6;

// Create a creature instance from species
function makeCreature(speciesId, level = 5) {
  const s = CREATURES[speciesId];
  if (!s) return null;
  const inst = {
    id: Math.random().toString(36).slice(2, 9),
    species: speciesId,
    level: level,
    exp: 0,
    hp: 0, maxHp: 0,
    atk: 0, def: 0, spd: 0,
    types: [...s.types],
    abilityIds: [], // 4 abilities
    pp: {}, // ability id -> current pp
    status: null, // 'burn' | 'frost' | 'shock' | 'barrier' | 'regen' | 'curse'
    statMods: { atk: 0, def: 0, spd: 0 },
    name: s.name,
    transformations: 0
  };
  recalcStats(inst);
  // Pick 4 abilities
  const known = getKnownAbilities(speciesId, level);
  inst.abilityIds = known.slice(-4);
  if (inst.abilityIds.length < 4) {
    // pad with tackle
    while (inst.abilityIds.length < 4) inst.abilityIds.push('tackle');
  }
  // Init PP
  for (const ab of inst.abilityIds) {
    inst.pp[ab] = ABILITIES[ab]?.pp ?? 10;
  }
  inst.maxHp = inst.hp;
  return inst;
}

function recalcStats(inst) {
  const s = CREATURES[inst.species];
  if (!s) return;
  const lvl = inst.level;
  inst.maxHp = Math.floor(s.base.hp + lvl * 2.4 + Math.pow(lvl, 1.2) * 1.1);
  inst.atk = Math.floor(s.base.atk + lvl * 1.6);
  inst.def = Math.floor(s.base.def + lvl * 1.5);
  inst.spd = Math.floor(s.base.spd + lvl * 1.4);
  inst.hp = inst.maxHp;
}

function getKnownAbilities(speciesId, level) {
  const s = CREATURES[speciesId];
  const known = [];
  const levels = Object.keys(s.learnset).map(Number).sort((a,b) => a-b);
  for (const lv of levels) {
    if (lv <= level) {
      for (const ab of s.learnset[lv]) {
        if (!known.includes(ab)) known.push(ab);
      }
    }
  }
  return known;
}

function addExp(inst, amount) {
  if (inst.level >= MAX_LEVEL) return { leveled: false, learned: null, evo: null };
  inst.exp += amount;
  const need = expToNext(inst.level);
  let result = { leveled: false, learned: null, evo: null };
  while (inst.exp >= expToNext(inst.level) && inst.level < MAX_LEVEL) {
    inst.exp -= expToNext(inst.level);
    inst.level++;
    recalcStats(inst);
    result.leveled = true;
    // Learn new ability
    const s = CREATURES[inst.species];
    if (s.learnset[inst.level]) {
      for (const ab of s.learnset[inst.level]) {
        if (!inst.abilityIds.includes(ab) && inst.abilityIds.length < 4) {
          inst.abilityIds.push(ab);
          result.learned = ab;
          break;
        }
      }
    }
    // Check evolution
    if (s.evolves && inst.level >= s.evolves.at && !result.evo) {
      // Will be triggered separately with catalyst
    }
  }
  return result;
}

function expToNext(level) {
  // Smooth curve
  return Math.floor(8 + Math.pow(level, 1.45) * 4.5);
}

// ---------- Save / Load ----------
function saveGame() {
  try {
    const data = {
      party: Game.party,
      storage: Game.storage,
      inventory: Game.inventory,
      gold: Game.gold,
      seen: Array.from(Game.seen),
      caught: Array.from(Game.caught),
      currentZone: Game.currentZone,
      unlockedZones: Game.unlockedZones,
      playerX: Game.playerX,
      playerY: Game.playerY,
      defeatedAnchors: Array.from(Game.defeatedAnchors),
      stepsTaken: Game.stepsTaken,
      totalEncounters: Game.totalEncounters,
      totalCaught: Game.totalCaught,
      storyStage: Game.storyStage,
      timestamp: Date.now()
    };
    localStorage.setItem('aethermere_save_1', JSON.stringify(data));
    Game.hasSave = true;
    return true;
  } catch (e) { console.warn('Save failed', e); return false; }
}

function loadGame() {
  try {
    const raw = localStorage.getItem('aethermere_save_1');
    if (!raw) return null;
    const data = JSON.parse(raw);
    Game.party = data.party || [];
    Game.storage = data.storage || [];
    Game.inventory = data.inventory || {};
    Game.gold = data.gold || 0;
    Game.seen = new Set(data.seen || []);
    Game.caught = new Set(data.caught || []);
    Game.currentZone = data.currentZone || 0;
    Game.unlockedZones = data.unlockedZones || 1;
    Game.playerX = data.playerX ?? 1;
    Game.playerY = data.playerY ?? 7;
    Game.defeatedAnchors = new Set(data.defeatedAnchors || []);
    Game.stepsTaken = data.stepsTaken || 0;
    Game.totalEncounters = data.totalEncounters || 0;
    Game.totalCaught = data.totalCaught || 0;
    Game.storyStage = data.storyStage || 0;
    Game.hasSave = true;
    return data;
  } catch (e) { console.warn('Load failed', e); return null; }
}

function hasSave() {
  try { return !!localStorage.getItem('aethermere_save_1'); } catch (e) { return false; }
}

function deleteSave() {
  try { localStorage.removeItem('aethermere_save_1'); Game.hasSave = false; } catch (e) {}
}

// ---------- Inventory init ----------
function initInventory() {
  const inv = {};
  inv['wrappingshard'] = 5;
  inv['mossdrop'] = 3;
  Game.inventory = inv;
}


// ---------- Canvas / Renderer ----------
const canvas = document.getElementById('game-canvas');
const ctx = canvas.getContext('2d');
let dpr = Math.max(1, window.devicePixelRatio || 1);
let viewW = 0, viewH = 0;

function resize() {
  dpr = Math.max(1, window.devicePixelRatio || 1);
  const rect = canvas.getBoundingClientRect();
  viewW = rect.width; viewH = rect.height;
  canvas.width = viewW * dpr;
  canvas.height = viewH * dpr;
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
}
window.addEventListener('resize', resize);
resize();

// Tile rendering
let TILE_SIZE = 48;
function getTileSize() {
  // Fit to view
  const tw = Math.floor(viewW / 20);
  const th = Math.floor(viewH / 15);
  return Math.max(20, Math.min(tw, th));
}

// ---------- Particle System ----------
const particles = [];
function addParticle(p) { particles.push({ ...p, life: p.life ?? 1, age: 0 }); }

function updateParticles(dt) {
  for (let i = particles.length - 1; i >= 0; i--) {
    const p = particles[i];
    p.age += dt;
    if (p.age >= p.life) { particles.splice(i, 1); continue; }
    p.x += p.vx * dt;
    p.y += p.vy * dt;
    p.vx *= 0.98;
    p.vy *= 0.98;
    if (p.gravity) p.vy += p.gravity * dt;
  }
}

function drawParticles() {
  for (const p of particles) {
    const t = p.age / p.life;
    const alpha = p.alpha ?? (1 - t);
    ctx.save();
    ctx.globalAlpha = clamp(alpha, 0, 1);
    ctx.fillStyle = p.color;
    if (p.glow) { ctx.shadowColor = p.color; ctx.shadowBlur = p.glow; }
    ctx.beginPath();
    const size = (p.size ?? 4) * (p.shrink ? 1 - t : 1) * (p.grow ? 0.5 + t * 0.5 : 1);
    ctx.arc(p.x, p.y, Math.max(0.5, size), 0, Math.PI * 2);
    ctx.fill();
    ctx.restore();
  }
}

function emitZoneParticles(zone) {
  const z = ZONES[zone];
  if (!z) return;
  const w = viewW, h = viewH;
  const pType = z.particle;
  const colorMap = {
    ember: ['#ff8a5b', '#ff6e3a', '#ffae7f'],
    bubble: ['#5ec3ff', '#9cdcff', '#bce5ff'],
    spark: ['#cba6ff', '#e1c8ff', '#7fffe5'],
    dust: ['#7fffe5', '#bce5ff', '#ffffff'],
    crystal: ['#7fffe5', '#cba6ff', '#e1c8ff'],
    shade: ['#a48cff', '#7a6ba8', '#3a2860']
  };
  const colors = colorMap[pType] || ['#7fffe5'];
  // Add ambient particles
  if (Math.random() < 0.4) {
    addParticle({
      x: rand(0, w), y: rand(0, h),
      vx: rand(-8, 8), vy: pType === 'bubble' ? rand(-20, -5) : rand(-12, -2),
      color: pick(colors),
      size: rand(1.5, 4), life: rand(3, 6), alpha: 0.5, glow: 6
    });
  }
}

// ---------- Overworld Rendering ----------
const camera = { x: 0, y: 0, targetX: 0, targetY: 0 };

function drawOverworld(dt) {
  const z = ZONES[Game.currentZone];
  const map = Game.zoneMap;
  if (!z || !map) return;
  
  // Background gradient
  const grad = ctx.createLinearGradient(0, 0, 0, viewH);
  grad.addColorStop(0, z.bgGradient[0]);
  grad.addColorStop(0.5, z.bgGradient[1]);
  grad.addColorStop(1, z.bgGradient[2]);
  ctx.fillStyle = grad;
  ctx.fillRect(0, 0, viewW, viewH);
  
  TILE_SIZE = getTileSize();
  const offsetX = (viewW - map.w * TILE_SIZE) / 2 - camera.x;
  const offsetY = (viewH - map.h * TILE_SIZE) / 2 - camera.y;
  
  // Emit ambient particles
  emitZoneParticles(Game.currentZone);
  
  // Draw tiles
  for (let y = 0; y < map.h; y++) {
    for (let x = 0; x < map.w; x++) {
      const t = map.tiles[y][x];
      const sx = offsetX + x * TILE_SIZE;
      const sy = offsetY + y * TILE_SIZE;
      drawTile(t, sx, sy, TILE_SIZE, z);
    }
  }
  
  // Draw player
  const px = offsetX + Game.playerX * TILE_SIZE;
  const py = offsetY + Game.playerY * TILE_SIZE;
  drawPlayer(px, py, TILE_SIZE);
  
  // Vignette
  const vg = ctx.createRadialGradient(viewW/2, viewH/2, viewW*0.3, viewW/2, viewH/2, viewW*0.7);
  vg.addColorStop(0, 'transparent');
  vg.addColorStop(1, 'rgba(0,0,0,0.55)');
  ctx.fillStyle = vg;
  ctx.fillRect(0, 0, viewW, viewH);
  
  drawParticles();
  
  // Pause overlay
  if (Game.paused) {
    ctx.fillStyle = 'rgba(0,0,0,0.5)';
    ctx.fillRect(0, 0, viewW, viewH);
  }
}

function drawTile(type, x, y, s, zone) {
  const cx = x + s/2, cy = y + s/2;
  const t = performance.now() / 1000;
  switch (type) {
    case 0: // path
      ctx.fillStyle = '#3a3050';
      ctx.fillRect(x, y, s, s);
      ctx.fillStyle = '#4a3e60';
      ctx.fillRect(x + 2, y + 2, s - 4, s - 4);
      break;
    case 1: // grass
      ctx.fillStyle = '#1f2a1a';
      ctx.fillRect(x, y, s, s);
      ctx.fillStyle = '#2a3a22';
      for (let i = 0; i < 3; i++) {
        const gx = x + ((i * 17 + 3) % s);
        const gy = y + ((i * 23 + 5) % s);
        ctx.fillRect(gx, gy, 2, 4);
      }
      // Subtle color variation per zone
      if (zone.accent) {
        ctx.fillStyle = zone.accent + '22';
        ctx.fillRect(x, y, s, s);
      }
      break;
    case 2: // water
      ctx.fillStyle = '#0e2a48';
      ctx.fillRect(x, y, s, s);
      ctx.fillStyle = '#1a3e64';
      const wo = Math.sin(t * 2 + x * 0.1 + y * 0.1) * 2;
      ctx.fillRect(x, y + s*0.4 + wo, s, 2);
      ctx.fillRect(x, y + s*0.7 - wo, s, 2);
      ctx.fillStyle = 'rgba(127,255,229,0.15)';
      ctx.fillRect(x, y + s*0.3, s, 1);
      break;
    case 3: // tree
      ctx.fillStyle = '#1f2a1a';
      ctx.fillRect(x, y, s, s);
      ctx.fillStyle = '#3a2818';
      ctx.fillRect(x + s*0.4, y + s*0.5, s*0.2, s*0.5);
      ctx.fillStyle = zone.accent || '#2a4a28';
      ctx.beginPath();
      ctx.arc(x + s*0.5, y + s*0.4, s*0.4, 0, Math.PI*2);
      ctx.fill();
      ctx.fillStyle = (zone.accent || '#3a6a3a') + '88';
      ctx.beginPath();
      ctx.arc(x + s*0.5, y + s*0.35, s*0.3, 0, Math.PI*2);
      ctx.fill();
      break;
    case 4: // rock
      ctx.fillStyle = '#1a1a24';
      ctx.fillRect(x, y, s, s);
      ctx.fillStyle = '#4a4a5a';
      ctx.beginPath();
      ctx.moveTo(x + s*0.2, y + s*0.7);
      ctx.lineTo(x + s*0.4, y + s*0.3);
      ctx.lineTo(x + s*0.6, y + s*0.4);
      ctx.lineTo(x + s*0.85, y + s*0.65);
      ctx.lineTo(x + s*0.7, y + s*0.85);
      ctx.closePath();
      ctx.fill();
      ctx.fillStyle = '#6a6a7a';
      ctx.beginPath();
      ctx.moveTo(x + s*0.4, y + s*0.3);
      ctx.lineTo(x + s*0.6, y + s*0.4);
      ctx.lineTo(x + s*0.5, y + s*0.55);
      ctx.closePath();
      ctx.fill();
      break;
    case 5: // shrine
      ctx.fillStyle = '#1a1428';
      ctx.fillRect(x, y, s, s);
      // base
      ctx.fillStyle = '#3a3060';
      ctx.fillRect(x + s*0.2, y + s*0.55, s*0.6, s*0.35);
      // pillar
      ctx.fillStyle = '#5a4a8a';
      ctx.fillRect(x + s*0.35, y + s*0.3, s*0.3, s*0.3);
      // flame
      const pulse = 0.7 + Math.sin(t * 3) * 0.3;
      ctx.fillStyle = `rgba(127,255,229,${pulse})`;
      ctx.shadowColor = '#7fffe5'; ctx.shadowBlur = 16;
      ctx.beginPath();
      ctx.arc(x + s*0.5, y + s*0.28, s*0.08 * pulse, 0, Math.PI*2);
      ctx.fill();
      ctx.shadowBlur = 0;
      break;
    case 6: // anchor (boss)
      ctx.fillStyle = '#1a0814';
      ctx.fillRect(x, y, s, s);
      ctx.fillStyle = 'rgba(0,0,0,0.4)';
      ctx.fillRect(x, y, s, s);
      const glow = 0.5 + Math.sin(t * 2) * 0.5;
      ctx.fillStyle = `rgba(164,90,255,${glow})`;
      ctx.shadowColor = '#a45aff'; ctx.shadowBlur = 20;
      ctx.beginPath();
      ctx.arc(x + s*0.5, y + s*0.5, s*0.35, 0, Math.PI*2);
      ctx.fill();
      ctx.shadowBlur = 0;
      ctx.fillStyle = '#1a0a28';
      ctx.beginPath();
      ctx.arc(x + s*0.5, y + s*0.5, s*0.18, 0, Math.PI*2);
      ctx.fill();
      // Marker
      ctx.fillStyle = '#fff';
      ctx.font = `${s*0.25}px sans-serif`;
      ctx.textAlign = 'center';
      ctx.fillText('▲', x + s*0.5, y + s*0.6);
      break;
    case 7: // brush (shades)
      ctx.fillStyle = '#0e0820';
      ctx.fillRect(x, y, s, s);
      ctx.fillStyle = 'rgba(164,140,255,0.3)';
      for (let i = 0; i < 5; i++) {
        const bx = x + (i * 7 + 2) % s;
        const by = y + (i * 11 + 3) % s;
        const bo = Math.sin(t * 1.5 + i) * 2;
        ctx.fillRect(bx, by + bo, 3, 3);
      }
      break;
    case 9: // portal
      ctx.fillStyle = '#1a1428';
      ctx.fillRect(x, y, s, s);
      const rot = t * 1.2;
      ctx.save();
      ctx.translate(x + s/2, y + s/2);
      ctx.rotate(rot);
      ctx.strokeStyle = '#7fffe5';
      ctx.lineWidth = 2;
      ctx.shadowColor = '#7fffe5'; ctx.shadowBlur = 12;
      for (let i = 0; i < 3; i++) {
        ctx.beginPath();
        ctx.ellipse(0, 0, s*0.35 - i*4, s*0.12 - i*2, 0, 0, Math.PI*2);
        ctx.stroke();
      }
      ctx.shadowBlur = 0;
      ctx.fillStyle = '#7fffe5';
      ctx.beginPath();
      ctx.arc(0, 0, 3, 0, Math.PI*2);
      ctx.fill();
      ctx.restore();
      break;
    case 8: // wall
      ctx.fillStyle = '#0a0414';
      ctx.fillRect(x, y, s, s);
      break;
  }
}

function drawPlayer(x, y, s) {
  const cx = x + s/2, cy = y + s/2;
  const t = performance.now() / 100;
  const bob = Math.sin(t) * (Game.playerStep > 0 ? 2 : 0);
  // Shadow
  ctx.fillStyle = 'rgba(0,0,0,0.4)';
  ctx.beginPath();
  ctx.ellipse(cx, y + s*0.85, s*0.25, s*0.08, 0, 0, Math.PI*2);
  ctx.fill();
  // Body
  ctx.fillStyle = '#5a4a8a';
  ctx.beginPath();
  ctx.arc(cx, cy - s*0.1 + bob, s*0.22, 0, Math.PI*2);
  ctx.fill();
  // Cloak
  ctx.fillStyle = '#a48cff';
  ctx.beginPath();
  ctx.moveTo(cx - s*0.2, cy + bob);
  ctx.lineTo(cx, cy + s*0.32 + bob);
  ctx.lineTo(cx + s*0.2, cy + bob);
  ctx.closePath();
  ctx.fill();
  // Hood highlight
  ctx.fillStyle = '#cba6ff';
  ctx.beginPath();
  ctx.arc(cx, cy - s*0.18 + bob, s*0.12, 0, Math.PI*2);
  ctx.fill();
  // Glow
  ctx.shadowColor = '#7fffe5'; ctx.shadowBlur = 12;
  ctx.fillStyle = '#7fffe5';
  ctx.beginPath();
  ctx.arc(cx, cy - s*0.15 + bob, 2, 0, Math.PI*2);
  ctx.fill();
  ctx.shadowBlur = 0;
}


// ---------- Creature Art Drawing ----------
// Each creature has a procedural drawing function
function drawCreatureArt(ctx, species, x, y, size, palette) {
  const fn = ART[species] || ART.default;
  fn(ctx, x, y, size, palette);
}

const ART = {
  default: (ctx, x, y, s) => {
    ctx.fillStyle = '#888';
    ctx.beginPath();
    ctx.arc(x, y, s*0.3, 0, Math.PI*2);
    ctx.fill();
  },
  cinder: (ctx, x, y, s) => {
    const t = performance.now() / 600;
    const c1 = '#ff6e3a', c2 = '#ffae7f', c3 = '#7fffe5';
    // Body
    ctx.fillStyle = c1;
    ctx.beginPath();
    ctx.ellipse(x, y, s*0.35, s*0.3, 0, 0, Math.PI*2);
    ctx.fill();
    // Head
    ctx.beginPath();
    ctx.arc(x, y - s*0.2, s*0.22, 0, Math.PI*2);
    ctx.fill();
    // Ears (fox)
    ctx.fillStyle = c2;
    ctx.beginPath();
    ctx.moveTo(x - s*0.2, y - s*0.3);
    ctx.lineTo(x - s*0.1, y - s*0.5);
    ctx.lineTo(x - s*0.05, y - s*0.3);
    ctx.closePath(); ctx.fill();
    ctx.beginPath();
    ctx.moveTo(x + s*0.2, y - s*0.3);
    ctx.lineTo(x + s*0.1, y - s*0.5);
    ctx.lineTo(x + s*0.05, y - s*0.3);
    ctx.closePath(); ctx.fill();
    // Eyes
    ctx.fillStyle = c3;
    ctx.shadowColor = c3; ctx.shadowBlur = 8;
    ctx.beginPath();
    ctx.arc(x - s*0.08, y - s*0.22, 3, 0, Math.PI*2);
    ctx.arc(x + s*0.08, y - s*0.22, 3, 0, Math.PI*2);
    ctx.fill();
    ctx.shadowBlur = 0;
    // Ember sparks
    for (let i = 0; i < 4; i++) {
      const sx = x + Math.sin(t + i * 1.7) * s*0.4;
      const sy = y + Math.cos(t * 1.3 + i) * s*0.4;
      ctx.fillStyle = c2;
      ctx.beginPath();
      ctx.arc(sx, sy, 2, 0, Math.PI*2);
      ctx.fill();
    }
  },
  lumen: (ctx, x, y, s) => {
    const t = performance.now() / 800;
    const c1 = '#7fffe5', c2 = '#bce5ff', c3 = '#fff';
    // Body
    ctx.fillStyle = c1;
    ctx.beginPath();
    ctx.ellipse(x, y, s*0.32, s*0.26, 0, 0, Math.PI*2);
    ctx.fill();
    // Head
    ctx.beginPath();
    ctx.arc(x, y - s*0.18, s*0.2, 0, Math.PI*2);
    ctx.fill();
    // Ears (long, like a fennec)
    ctx.fillStyle = c2;
    ctx.beginPath();
    ctx.moveTo(x - s*0.15, y - s*0.25);
    ctx.lineTo(x - s*0.25, y - s*0.5);
    ctx.lineTo(x - s*0.05, y - s*0.3);
    ctx.closePath(); ctx.fill();
    ctx.beginPath();
    ctx.moveTo(x + s*0.15, y - s*0.25);
    ctx.lineTo(x + s*0.25, y - s*0.5);
    ctx.lineTo(x + s*0.05, y - s*0.3);
    ctx.closePath(); ctx.fill();
    // Eyes
    ctx.fillStyle = c3;
    ctx.shadowColor = c1; ctx.shadowBlur = 10;
    ctx.beginPath();
    ctx.arc(x - s*0.07, y - s*0.2, 3, 0, Math.PI*2);
    ctx.arc(x + s*0.07, y - s*0.2, 3, 0, Math.PI*2);
    ctx.fill();
    ctx.shadowBlur = 0;
    // Star trail
    for (let i = 0; i < 6; i++) {
      const a = t + i * (Math.PI*2/6);
      const sx = x + Math.cos(a) * s*0.4;
      const sy = y + Math.sin(a) * s*0.4;
      ctx.fillStyle = c2;
      ctx.globalAlpha = 0.6 + Math.sin(t * 2 + i) * 0.4;
      ctx.beginPath();
      ctx.arc(sx, sy, 1.5, 0, Math.PI*2);
      ctx.fill();
    }
    ctx.globalAlpha = 1;
  },
  tidal: (ctx, x, y, s) => {
    const t = performance.now() / 500;
    const c1 = '#5ec3ff', c2 = '#9cdcff', c3 = '#0a2240';
    // Body (round)
    ctx.fillStyle = c1;
    ctx.beginPath();
    ctx.arc(x, y, s*0.32, 0, Math.PI*2);
    ctx.fill();
    // Tail (curl)
    ctx.strokeStyle = c1; ctx.lineWidth = s*0.1;
    ctx.lineCap = 'round';
    ctx.beginPath();
    ctx.moveTo(x + s*0.2, y + s*0.05);
    ctx.quadraticCurveTo(x + s*0.45, y - s*0.1, x + s*0.3, y - s*0.3);
    ctx.stroke();
    // Eyes
    ctx.fillStyle = c3;
    ctx.beginPath();
    ctx.arc(x - s*0.1, y - s*0.05, 3, 0, Math.PI*2);
    ctx.arc(x + s*0.1, y - s*0.05, 3, 0, Math.PI*2);
    ctx.fill();
    // Bubbles
    for (let i = 0; i < 3; i++) {
      const bx = x - s*0.3 + i * s*0.2;
      const by = y - s*0.4 - Math.sin(t * 2 + i) * 4;
      ctx.strokeStyle = c2; ctx.lineWidth = 1;
      ctx.beginPath();
      ctx.arc(bx, by, 3, 0, Math.PI*2);
      ctx.stroke();
    }
  },
  flora: (ctx, x, y, s) => {
    const t = performance.now() / 700;
    const c1 = '#7cd66a', c2 = '#a8e090', c3 = '#5a3a18';
    // Body (round, leafy)
    ctx.fillStyle = c1;
    ctx.beginPath();
    ctx.arc(x, y, s*0.32, 0, Math.PI*2);
    ctx.fill();
    // Leaves around
    for (let i = 0; i < 6; i++) {
      const a = i * Math.PI*2/6 + t * 0.1;
      const lx = x + Math.cos(a) * s*0.3;
      const ly = y + Math.sin(a) * s*0.3;
      ctx.fillStyle = c2;
      ctx.beginPath();
      ctx.ellipse(lx, ly, s*0.12, s*0.06, a, 0, Math.PI*2);
      ctx.fill();
    }
    // Eyes
    ctx.fillStyle = c3;
    ctx.beginPath();
    ctx.arc(x - s*0.1, y - s*0.05, 3, 0, Math.PI*2);
    ctx.arc(x + s*0.1, y - s*0.05, 3, 0, Math.PI*2);
    ctx.fill();
    // Flower on top
    ctx.fillStyle = '#ffae7f';
    for (let i = 0; i < 5; i++) {
      const a = i * Math.PI*2/5;
      const fx = x + Math.cos(a) * s*0.12;
      const fy = y - s*0.35 + Math.sin(a) * s*0.06;
      ctx.beginPath();
      ctx.arc(fx, fy, 4, 0, Math.PI*2);
      ctx.fill();
    }
    ctx.fillStyle = '#ffd76b';
    ctx.beginPath();
    ctx.arc(x, y - s*0.35, 3, 0, Math.PI*2);
    ctx.fill();
  },
  storm: (ctx, x, y, s) => {
    const t = performance.now() / 300;
    const c1 = '#cba6ff', c2 = '#7fffe5', c3 = '#3a2860';
    // Body (angular)
    ctx.fillStyle = c1;
    ctx.beginPath();
    ctx.moveTo(x - s*0.3, y + s*0.1);
    ctx.lineTo(x - s*0.15, y - s*0.3);
    ctx.lineTo(x + s*0.15, y - s*0.3);
    ctx.lineTo(x + s*0.3, y + s*0.1);
    ctx.lineTo(x + s*0.15, y + s*0.3);
    ctx.lineTo(x - s*0.15, y + s*0.3);
    ctx.closePath();
    ctx.fill();
    // Eye
    ctx.fillStyle = c2;
    ctx.shadowColor = c2; ctx.shadowBlur = 8;
    ctx.beginPath();
    ctx.arc(x, y - s*0.05, 4, 0, Math.PI*2);
    ctx.fill();
    ctx.shadowBlur = 0;
    // Sparks
    for (let i = 0; i < 3; i++) {
      const a = t + i * 2;
      const sx = x + Math.cos(a) * s*0.4;
      const sy = y + Math.sin(a) * s*0.4;
      ctx.strokeStyle = c2; ctx.lineWidth = 1.5;
      ctx.beginPath();
      ctx.moveTo(sx - 3, sy); ctx.lineTo(sx + 3, sy);
      ctx.moveTo(sx, sy - 3); ctx.lineTo(sx, sy + 3);
      ctx.stroke();
    }
  },
  crag: (ctx, x, y, s) => {
    const t = performance.now() / 1000;
    const c1 = '#e0c891', c2 = '#a08860', c3 = '#5a4830';
    // Body (chunky)
    ctx.fillStyle = c1;
    ctx.beginPath();
    ctx.ellipse(x, y + s*0.05, s*0.36, s*0.28, 0, 0, Math.PI*2);
    ctx.fill();
    // Head
    ctx.beginPath();
    ctx.arc(x, y - s*0.2, s*0.2, 0, Math.PI*2);
    ctx.fill();
    // Eyes
    ctx.fillStyle = c3;
    ctx.beginPath();
    ctx.arc(x - s*0.08, y - s*0.22, 2.5, 0, Math.PI*2);
    ctx.arc(x + s*0.08, y - s*0.22, 2.5, 0, Math.PI*2);
    ctx.fill();
    // Crystal on top
    ctx.fillStyle = c2;
    ctx.beginPath();
    ctx.moveTo(x - s*0.1, y - s*0.35);
    ctx.lineTo(x, y - s*0.5);
    ctx.lineTo(x + s*0.1, y - s*0.35);
    ctx.closePath();
    ctx.fill();
    // Body markings
    ctx.fillStyle = c2;
    ctx.fillRect(x - s*0.2, y, s*0.4, 2);
    ctx.fillRect(x - s*0.15, y + s*0.1, s*0.3, 2);
  },
  shade: (ctx, x, y, s) => {
    const t = performance.now() / 700;
    const c1 = '#a48cff', c2 = '#3a2860', c3 = '#7fffe5';
    // Body
    ctx.fillStyle = c1;
    ctx.beginPath();
    ctx.ellipse(x, y, s*0.32, s*0.28, 0, 0, Math.PI*2);
    ctx.fill();
    // Head
    ctx.beginPath();
    ctx.arc(x, y - s*0.18, s*0.2, 0, Math.PI*2);
    ctx.fill();
    // Eyes
    ctx.fillStyle = c3;
    ctx.shadowColor = c3; ctx.shadowBlur = 8;
    ctx.beginPath();
    ctx.arc(x - s*0.08, y - s*0.2, 3, 0, Math.PI*2);
    ctx.arc(x + s*0.08, y - s*0.2, 3, 0, Math.PI*2);
    ctx.fill();
    ctx.shadowBlur = 0;
    // Tail (flickering)
    ctx.strokeStyle = c1; ctx.lineWidth = s*0.08;
    ctx.lineCap = 'round';
    const tw = Math.sin(t) * 5;
    ctx.beginPath();
    ctx.moveTo(x - s*0.25, y + s*0.1);
    ctx.quadraticCurveTo(x - s*0.4, y - s*0.1 + tw, x - s*0.3, y - s*0.35);
    ctx.stroke();
  },
  coral: (ctx, x, y, s) => {
    const t = performance.now() / 1000;
    const c1 = '#ff8aaa', c2 = '#5ec3ff', c3 = '#7cd66a';
    // Coral body
    ctx.fillStyle = c1;
    ctx.beginPath();
    ctx.arc(x, y, s*0.3, 0, Math.PI*2);
    ctx.fill();
    // Branches
    for (let i = 0; i < 5; i++) {
      const a = i * Math.PI*2/5 - Math.PI/2;
      const bx = x + Math.cos(a) * s*0.3;
      const by = y + Math.sin(a) * s*0.3;
      ctx.fillStyle = c1;
      ctx.beginPath();
      ctx.moveTo(bx, by);
      ctx.lineTo(bx + Math.cos(a) * s*0.15, by + Math.sin(a) * s*0.15);
      ctx.lineTo(bx + Math.cos(a + 0.3) * s*0.15, by + Math.sin(a + 0.3) * s*0.15);
      ctx.closePath();
      ctx.fill();
    }
    // Eyes
    ctx.fillStyle = c2;
    ctx.beginPath();
    ctx.arc(x - s*0.08, y - s*0.05, 3, 0, Math.PI*2);
    ctx.arc(x + s*0.08, y - s*0.05, 3, 0, Math.PI*2);
    ctx.fill();
    // Sparkles
    ctx.fillStyle = c3;
    for (let i = 0; i < 3; i++) {
      const a = t + i * 2.1;
      const sx = x + Math.cos(a) * s*0.45;
      const sy = y + Math.sin(a) * s*0.45;
      ctx.beginPath();
      ctx.arc(sx, sy, 1.5, 0, Math.PI*2);
      ctx.fill();
    }
  },
  wraith: (ctx, x, y, s) => {
    const t = performance.now() / 500;
    const c1 = '#5ec3ff', c2 = '#a48cff', c3 = '#fff';
    // Wisp body
    ctx.fillStyle = c1;
    ctx.globalAlpha = 0.6;
    ctx.beginPath();
    ctx.ellipse(x, y, s*0.28, s*0.35, 0, 0, Math.PI*2);
    ctx.fill();
    ctx.globalAlpha = 0.8;
    // Wisps trailing
    for (let i = 0; i < 3; i++) {
      const wy = y + s*0.2 + i * s*0.1;
      const w = s*0.15 - i * s*0.04;
      ctx.fillStyle = i % 2 ? c1 : c2;
      ctx.beginPath();
      ctx.ellipse(x + Math.sin(t + i) * 3, wy, w, s*0.06, 0, 0, Math.PI*2);
      ctx.fill();
    }
    ctx.globalAlpha = 1;
    // Eyes
    ctx.fillStyle = c3;
    ctx.shadowColor = c3; ctx.shadowBlur = 8;
    ctx.beginPath();
    ctx.arc(x - s*0.08, y - s*0.1, 2.5, 0, Math.PI*2);
    ctx.arc(x + s*0.08, y - s*0.1, 2.5, 0, Math.PI*2);
    ctx.fill();
    ctx.shadowBlur = 0;
  },
  wing: (ctx, x, y, s) => {
    const t = performance.now() / 200;
    const c1 = '#cba6ff', c2 = '#7fffe5', c3 = '#5a3a8a';
    const flap = Math.sin(t) * 8;
    // Body
    ctx.fillStyle = c1;
    ctx.beginPath();
    ctx.ellipse(x, y, s*0.2, s*0.3, 0, 0, Math.PI*2);
    ctx.fill();
    // Wings
    ctx.fillStyle = c3;
    ctx.beginPath();
    ctx.moveTo(x - s*0.15, y);
    ctx.quadraticCurveTo(x - s*0.4, y - s*0.2 + flap, x - s*0.3, y + s*0.1 + flap);
    ctx.closePath(); ctx.fill();
    ctx.beginPath();
    ctx.moveTo(x + s*0.15, y);
    ctx.quadraticCurveTo(x + s*0.4, y - s*0.2 + flap, x + s*0.3, y + s*0.1 + flap);
    ctx.closePath(); ctx.fill();
    // Eye
    ctx.fillStyle = c2;
    ctx.shadowColor = c2; ctx.shadowBlur = 6;
    ctx.beginPath();
    ctx.arc(x, y - s*0.1, 3, 0, Math.PI*2);
    ctx.fill();
    ctx.shadowBlur = 0;
  },
  beetle: (ctx, x, y, s) => {
    const t = performance.now() / 400;
    const c1 = '#5a3a18', c2 = '#ff8a5b', c3 = '#3a2810';
    // Shell
    ctx.fillStyle = c1;
    ctx.beginPath();
    ctx.ellipse(x, y, s*0.32, s*0.28, 0, 0, Math.PI*2);
    ctx.fill();
    // Carapace split
    ctx.strokeStyle = c3; ctx.lineWidth = 2;
    ctx.beginPath();
    ctx.moveTo(x, y - s*0.28);
    ctx.lineTo(x, y + s*0.28);
    ctx.stroke();
    // Glowing cracks
    ctx.strokeStyle = c2; ctx.lineWidth = 1.5;
    ctx.shadowColor = c2; ctx.shadowBlur = 6;
    ctx.beginPath();
    ctx.moveTo(x - s*0.15, y - s*0.15);
    ctx.lineTo(x - s*0.05, y);
    ctx.lineTo(x - s*0.15, y + s*0.15);
    ctx.stroke();
    ctx.beginPath();
    ctx.moveTo(x + s*0.15, y - s*0.15);
    ctx.lineTo(x + s*0.05, y);
    ctx.lineTo(x + s*0.15, y + s*0.15);
    ctx.stroke();
    ctx.shadowBlur = 0;
    // Legs
    ctx.strokeStyle = c3; ctx.lineWidth = s*0.06;
    ctx.beginPath();
    ctx.moveTo(x - s*0.3, y); ctx.lineTo(x - s*0.45, y - s*0.1);
    ctx.moveTo(x + s*0.3, y); ctx.lineTo(x + s*0.45, y - s*0.1);
    ctx.moveTo(x - s*0.3, y + s*0.1); ctx.lineTo(x - s*0.45, y + s*0.2);
    ctx.moveTo(x + s*0.3, y + s*0.1); ctx.lineTo(x + s*0.45, y + s*0.2);
    ctx.stroke();
  },
  crystal: (ctx, x, y, s) => {
    const t = performance.now() / 1000;
    const c1 = '#7fffe5', c2 = '#5ec3ff', c3 = '#3a6a8a';
    // Crystal body
    ctx.fillStyle = c1;
    ctx.beginPath();
    ctx.moveTo(x, y - s*0.35);
    ctx.lineTo(x + s*0.3, y - s*0.1);
    ctx.lineTo(x + s*0.25, y + s*0.25);
    ctx.lineTo(x - s*0.25, y + s*0.25);
    ctx.lineTo(x - s*0.3, y - s*0.1);
    ctx.closePath();
    ctx.fill();
    // Inner shine
    ctx.fillStyle = c2;
    ctx.globalAlpha = 0.5 + Math.sin(t) * 0.3;
    ctx.beginPath();
    ctx.moveTo(x, y - s*0.25);
    ctx.lineTo(x + s*0.15, y);
    ctx.lineTo(x, y + s*0.15);
    ctx.lineTo(x - s*0.15, y);
    ctx.closePath();
    ctx.fill();
    ctx.globalAlpha = 1;
    // Edge
    ctx.strokeStyle = c3; ctx.lineWidth = 1.5;
    ctx.beginPath();
    ctx.moveTo(x, y - s*0.35);
    ctx.lineTo(x + s*0.3, y - s*0.1);
    ctx.lineTo(x + s*0.25, y + s*0.25);
    ctx.lineTo(x - s*0.25, y + s*0.25);
    ctx.lineTo(x - s*0.3, y - s*0.1);
    ctx.closePath();
    ctx.stroke();
    // Sparkles
    for (let i = 0; i < 3; i++) {
      const a = t * 2 + i * 2;
      const sx = x + Math.cos(a) * s*0.4;
      const sy = y + Math.sin(a) * s*0.4;
      ctx.fillStyle = c1;
      ctx.beginPath();
      ctx.arc(sx, sy, 1.5, 0, Math.PI*2);
      ctx.fill();
    }
  },
  specter: (ctx, x, y, s) => {
    const t = performance.now() / 600;
    const c1 = '#a48cff', c2 = '#7fffe5', c3 = '#fff';
    // Ghost
    ctx.fillStyle = c1;
    ctx.globalAlpha = 0.7;
    ctx.beginPath();
    ctx.moveTo(x - s*0.25, y + s*0.1);
    ctx.quadraticCurveTo(x - s*0.3, y - s*0.3, x, y - s*0.3);
    ctx.quadraticCurveTo(x + s*0.3, y - s*0.3, x + s*0.25, y + s*0.1);
    // Wavy bottom
    for (let i = 0; i < 5; i++) {
      const wx = x - s*0.25 + i * s*0.1;
      const wy = y + s*0.1 + Math.sin(t * 2 + i) * 4 + s*0.1;
      ctx.lineTo(wx + s*0.05, wy);
    }
    ctx.closePath();
    ctx.fill();
    ctx.globalAlpha = 1;
    // Eyes
    ctx.fillStyle = c2;
    ctx.shadowColor = c2; ctx.shadowBlur = 8;
    ctx.beginPath();
    ctx.arc(x - s*0.1, y - s*0.05, 3, 0, Math.PI*2);
    ctx.arc(x + s*0.1, y - s*0.05, 3, 0, Math.PI*2);
    ctx.fill();
    ctx.shadowBlur = 0;
  },
  moth: (ctx, x, y, s) => {
    const t = performance.now() / 250;
    const c1 = '#7fffe5', c2 = '#cba6ff', c3 = '#ff6ba6';
    // Body
    ctx.fillStyle = '#5a3a8a';
    ctx.beginPath();
    ctx.ellipse(x, y, s*0.1, s*0.3, 0, 0, Math.PI*2);
    ctx.fill();
    // Wings
    const flap = Math.sin(t) * 5;
    ctx.fillStyle = c1;
    ctx.beginPath();
    ctx.moveTo(x, y);
    ctx.quadraticCurveTo(x - s*0.4, y - s*0.2 + flap, x - s*0.3, y + s*0.2);
    ctx.closePath(); ctx.fill();
    ctx.beginPath();
    ctx.moveTo(x, y);
    ctx.quadraticCurveTo(x + s*0.4, y - s*0.2 + flap, x + s*0.3, y + s*0.2);
    ctx.closePath(); ctx.fill();
    // Patterns
    ctx.fillStyle = c2;
    ctx.beginPath();
    ctx.arc(x - s*0.2, y - s*0.05 + flap/2, s*0.05, 0, Math.PI*2);
    ctx.arc(x + s*0.2, y - s*0.05 + flap/2, s*0.05, 0, Math.PI*2);
    ctx.fill();
    ctx.fillStyle = c3;
    ctx.beginPath();
    ctx.arc(x - s*0.25, y + s*0.05 + flap/2, s*0.04, 0, Math.PI*2);
    ctx.arc(x + s*0.25, y + s*0.05 + flap/2, s*0.04, 0, Math.PI*2);
    ctx.fill();
  },
  umbra: (ctx, x, y, s) => {
    const t = performance.now() / 400;
    const c1 = '#a48cff', c2 = '#3a2860', c3 = '#ff5a7a';
    // Body (large)
    ctx.fillStyle = c1;
    ctx.beginPath();
    ctx.ellipse(x, y, s*0.35, s*0.32, 0, 0, Math.PI*2);
    ctx.fill();
    // Claws
    ctx.fillStyle = c2;
    for (let side = -1; side <= 1; side += 2) {
      for (let i = 0; i < 3; i++) {
        const a = -Math.PI/2 + side * (i - 1) * 0.3;
        const cx = x + Math.cos(a) * s*0.32;
        const cy = y + Math.sin(a) * s*0.32;
        const tx = x + Math.cos(a) * s*0.45;
        const ty = y + Math.sin(a) * s*0.45;
        ctx.strokeStyle = c2; ctx.lineWidth = s*0.05;
        ctx.lineCap = 'round';
        ctx.beginPath();
        ctx.moveTo(cx, cy);
        ctx.lineTo(tx, ty);
        ctx.stroke();
      }
    }
    // Eyes
    ctx.fillStyle = c3;
    ctx.shadowColor = c3; ctx.shadowBlur = 10;
    ctx.beginPath();
    ctx.arc(x - s*0.1, y - s*0.1, 3, 0, Math.PI*2);
    ctx.arc(x + s*0.1, y - s*0.1, 3, 0, Math.PI*2);
    ctx.fill();
    ctx.shadowBlur = 0;
    // Ears
    ctx.fillStyle = c1;
    ctx.beginPath();
    ctx.moveTo(x - s*0.2, y - s*0.25);
    ctx.lineTo(x - s*0.1, y - s*0.4);
    ctx.lineTo(x - s*0.05, y - s*0.25);
    ctx.closePath(); ctx.fill();
    ctx.beginPath();
    ctx.moveTo(x + s*0.2, y - s*0.25);
    ctx.lineTo(x + s*0.1, y - s*0.4);
    ctx.lineTo(x + s*0.05, y - s*0.25);
    ctx.closePath(); ctx.fill();
  },
  golem: (ctx, x, y, s) => {
    const t = performance.now() / 800;
    const c1 = '#4a4060', c2 = '#2a2440', c3 = '#a48cff';
    // Blocky body
    ctx.fillStyle = c1;
    ctx.fillRect(x - s*0.3, y - s*0.1, s*0.6, s*0.4);
    // Head
    ctx.fillRect(x - s*0.2, y - s*0.3, s*0.4, s*0.2);
    // Cracks (glow)
    ctx.strokeStyle = c3; ctx.lineWidth = 1.5;
    ctx.shadowColor = c3; ctx.shadowBlur = 6;
    ctx.beginPath();
    ctx.moveTo(x - s*0.15, y - s*0.05);
    ctx.lineTo(x, y + s*0.05);
    ctx.lineTo(x + s*0.15, y - s*0.05);
    ctx.moveTo(x - s*0.2, y + s*0.15);
    ctx.lineTo(x - s*0.05, y + s*0.25);
    ctx.stroke();
    ctx.shadowBlur = 0;
    // Eyes
    ctx.fillStyle = c3;
    ctx.shadowColor = c3; ctx.shadowBlur = 8;
    ctx.fillRect(x - s*0.12, y - s*0.22, 4, 4);
    ctx.fillRect(x + s*0.08, y - s*0.22, 4, 4);
    ctx.shadowBlur = 0;
  },
  mossback: (ctx, x, y, s) => {
    const t = performance.now() / 800;
    const c1 = '#7cd66a', c2 = '#5a8a4a', c3 = '#4a3a2a';
    // Tortoise shell
    ctx.fillStyle = c3;
    ctx.beginPath();
    ctx.ellipse(x, y, s*0.35, s*0.25, 0, 0, Math.PI*2);
    ctx.fill();
    // Moss
    for (let i = 0; i < 8; i++) {
      const a = i * Math.PI*2/8;
      const r = s*0.3;
      const mx = x + Math.cos(a) * r * 0.7;
      const my = y + Math.sin(a) * r * 0.5;
      ctx.fillStyle = i % 2 ? c1 : c2;
      ctx.beginPath();
      ctx.arc(mx, my, s*0.08, 0, Math.PI*2);
      ctx.fill();
    }
    // Head
    ctx.fillStyle = c1;
    ctx.beginPath();
    ctx.arc(x - s*0.3, y, s*0.12, 0, Math.PI*2);
    ctx.fill();
    // Eye
    ctx.fillStyle = c3;
    ctx.beginPath();
    ctx.arc(x - s*0.32, y - s*0.03, 2, 0, Math.PI*2);
    ctx.fill();
  },
  seahorse: (ctx, x, y, s) => {
    const t = performance.now() / 500;
    const c1 = '#5ec3ff', c2 = '#cba6ff', c3 = '#fff';
    // Seahorse body
    ctx.fillStyle = c1;
    ctx.beginPath();
    ctx.moveTo(x, y - s*0.35);
    ctx.quadraticCurveTo(x + s*0.15, y - s*0.2, x + s*0.1, y + s*0.1);
    ctx.quadraticCurveTo(x + s*0.05, y + s*0.3, x - s*0.15, y + s*0.25);
    ctx.quadraticCurveTo(x - s*0.2, y, x - s*0.1, y - s*0.1);
    ctx.quadraticCurveTo(x, y - s*0.2, x, y - s*0.35);
    ctx.fill();
    // Mane (lightning)
    ctx.strokeStyle = c2; ctx.lineWidth = 1.5;
    ctx.shadowColor = c2; ctx.shadowBlur = 4;
    const bolt = Math.sin(t * 3) * 2;
    ctx.beginPath();
    ctx.moveTo(x - s*0.05, y - s*0.35);
    ctx.lineTo(x + s*0.05 + bolt, y - s*0.25);
    ctx.lineTo(x - s*0.05, y - s*0.15);
    ctx.stroke();
    ctx.shadowBlur = 0;
    // Eye
    ctx.fillStyle = c3;
    ctx.beginPath();
    ctx.arc(x - s*0.02, y - s*0.2, 2, 0, Math.PI*2);
    ctx.fill();
  },
  wolf: (ctx, x, y, s) => {
    const t = performance.now() / 500;
    const c1 = '#3a2020', c2 = '#ff8a5b', c3 = '#ff5a3a';
    // Body
    ctx.fillStyle = c1;
    ctx.beginPath();
    ctx.ellipse(x, y, s*0.35, s*0.25, 0, 0, Math.PI*2);
    ctx.fill();
    // Head
    ctx.beginPath();
    ctx.arc(x - s*0.2, y - s*0.1, s*0.18, 0, Math.PI*2);
    ctx.fill();
    // Ears
    ctx.beginPath();
    ctx.moveTo(x - s*0.32, y - s*0.25);
    ctx.lineTo(x - s*0.25, y - s*0.4);
    ctx.lineTo(x - s*0.2, y - s*0.22);
    ctx.closePath(); ctx.fill();
    ctx.beginPath();
    ctx.moveTo(x - s*0.1, y - s*0.22);
    ctx.lineTo(x - s*0.05, y - s*0.4);
    ctx.lineTo(x, y - s*0.25);
    ctx.closePath(); ctx.fill();
    // Ember eyes
    ctx.fillStyle = c2;
    ctx.shadowColor = c2; ctx.shadowBlur = 8;
    ctx.beginPath();
    ctx.arc(x - s*0.25, y - s*0.12, 2.5, 0, Math.PI*2);
    ctx.arc(x - s*0.15, y - s*0.12, 2.5, 0, Math.PI*2);
    ctx.fill();
    ctx.shadowBlur = 0;
    // Tail
    ctx.fillStyle = c1;
    ctx.beginPath();
    ctx.moveTo(x + s*0.3, y);
    ctx.quadraticCurveTo(x + s*0.45, y - s*0.1, x + s*0.4, y - s*0.3);
    ctx.lineTo(x + s*0.3, y - s*0.2);
    ctx.closePath(); ctx.fill();
  }
};


// ---------- Battle System ----------
const Battle = {
  active: false,
  enemies: [],
  party: [],
  activeIdx: 0,
  turnQueue: [],
  turnIdx: 0,
  log: [],
  state: 'menu', // menu, anim, message, done, capture
  message: '',
  shake: 0,
  flashAlpha: 0,
  flashColor: '#fff',
  bgOffset: 0,
  particles: [],
  selectedAbility: -1,
  capturedThisBattle: false,
  expAwarded: 0,
  awaitingNext: false,
  
  start(encounter) {
    this.active = true;
    this.capturedThisBattle = false;
    this.expAwarded = 0;
    this.state = 'menu';
    this.shake = 0;
    this.flashAlpha = 0;
    this.particles = [];
    this.log = [];
    this.selectedAbility = -1;
    this.awaitingNext = false;
    // Build party copies
    this.party = Game.party.map(c => ({
      ...c, hp: c.hp, maxHp: c.maxHp, status: null, statMods: { atk: 0, def: 0, spd: 0 },
      barrier: false, abilityIds: [...c.abilityIds]
    }));
    this.activeIdx = this.party.findIndex(c => c.hp > 0);
    if (this.activeIdx < 0) this.activeIdx = 0;
    // Build enemies
    this.enemies = encounter.enemies.map(e => {
      const inst = makeCreature(e.species, e.level);
      inst.isAnchor = e.isAnchor || false;
      inst.shakeT = 0;
      inst.faintT = 0;
      return inst;
    });
    // Auto select first ability
    this.selectedAbility = 0;
    // Build turn queue
    this.buildTurnQueue();
    Audio.playMusic('battle');
    this.message = `A wild ${this.enemies[0].name} appears!`;
    this.addLog(this.message);
    Audio.sfx.encounter();
  },
  
  buildTurnQueue() {
    const all = [];
    this.party.forEach((c, i) => {
      if (c.hp > 0) all.push({ kind: 'player', idx: i, spd: c.spd, acted: false });
    });
    this.enemies.forEach((e, i) => {
      if (e.hp > 0) all.push({ kind: 'enemy', idx: i, spd: e.spd, acted: false });
    });
    const effSpd = (a) => {
      const c = a.kind === 'player' ? this.party[a.idx] : this.enemies[a.idx];
      let s = c.spd;
      if (c.status === 'frost') s *= 0.5;
      return s;
    };
    all.sort((a, b) => effSpd(b) - effSpd(a));
    this.turnQueue = all;
    this.turnIdx = 0;
  },
  
  currentActor() {
    return this.turnQueue[this.turnIdx];
  },
  
  addLog(msg) {
    this.log.push(msg);
    this.message = msg;
  },
  
  async playerAction(action) {
    if (this.state !== 'menu') return;
    this.state = 'anim';
    Audio.sfx.confirm();
    if (action === 'fight') {
      // Show ability select
      this.state = 'ability';
      return;
    } else if (action === 'bag') {
      this.state = 'bag';
      return;
    } else if (action === 'party') {
      this.state = 'party';
      return;
    } else if (action === 'run') {
      // Mark actor as acted
      const actor = this.currentActor();
      if (actor) actor.acted = true;
      if (this.enemies.some(e => e.isAnchor)) {
        this.addLog('You cannot flee from a zone anchor!');
        this.state = 'message';
        setTimeout(() => this.nextTurn(), 1200);
        return;
      }
      if (Math.random() < 0.7) {
        this.addLog('You fled the battle.');
        this.state = 'message';
        setTimeout(() => this.end(false), 1200);
        return;
      } else {
        this.addLog("Couldn't escape!");
        this.state = 'message';
        setTimeout(() => this.nextTurn(), 1500);
        return;
      }
    }
  },
  
  async useAbility(abilId) {
    if (this.state !== 'ability') return;
    const actor = this.party[this.activeIdx];
    const ab = ABILITIES[abilId];
    if (!ab) return;
    if (!actor.pp) actor.pp = {};
    if ((actor.pp[abilId] ?? 0) <= 0) {
      this.addLog(`${actor.name} has no energy left for ${ab.name}!`);
      Audio.sfx.cancel();
      this.state = 'message';
      setTimeout(() => this.nextTurn(), 1000);
      return;
    }
    this.state = 'anim';
    actor.pp[abilId]--;
    Audio.sfx.confirm();
    this.addLog(`${actor.name} uses ${ab.name}!`);
    await this.delay(600);
    await this.executeMove(actor, ab, this.enemies[0], 'enemy', 0);
    this.afterAction();
  },
  
  useItem(itemId) {
    if (this.state !== 'bag') return;
    const it = ITEMS[itemId];
    if (!it || !Game.inventory[itemId] || Game.inventory[itemId] <= 0) return;
    if (it.type === 'collection') {
      Game.inventory[itemId]--;
      this.state = 'capture';
      UI.openCaptureModal(this.enemies[0], itemId);
      return;
    } else if (it.type === 'heal') {
      Game.inventory[itemId]--;
      const actor = this.party[this.activeIdx];
      const heal = Math.floor(actor.maxHp * it.amt);
      actor.hp = Math.min(actor.maxHp, actor.hp + heal);
      this.addLog(`${actor.name} restored ${heal} HP.`);
      Audio.sfx.heal();
      // Particles
      for (let i = 0; i < 12; i++) {
        const a = rand(0, Math.PI*2);
        this.spawnParticle(actor.x || viewW*0.25, actor.y || viewH*0.6, a, '#7cf2a8');
      }
      this.afterAction();
    } else if (it.type === 'revive') {
      // Find fainted
      const target = this.party.find(c => c.hp <= 0);
      if (!target) { this.addLog('No one needs reviving.'); this.state = 'menu'; return; }
      Game.inventory[itemId]--;
      target.hp = Math.floor(target.maxHp * it.amt);
      this.addLog(`${target.name} was revived!`);
      Audio.sfx.heal();
      this.afterAction();
    }
  },
  
  switchParty(idx) {
    if (this.state !== 'party') return;
    if (idx === this.activeIdx) { this.state = 'menu'; return; }
    const c = this.party[idx];
    if (c.hp <= 0) { this.addLog(`${c.name} is unable to battle.`); return; }
    this.activeIdx = idx;
    this.addLog(`Go, ${c.name}!`);
    Audio.sfx.confirm();
    this.afterAction();
  },
  
  afterAction() {
    // Mark actor as acted
    const actor = this.currentActor();
    if (actor) actor.acted = true;
    this.state = 'message';
    setTimeout(() => this.nextTurn(), 900);
  },
  
  nextTurn() {
    // End battle check
    if (this.party.every(c => c.hp <= 0)) {
      this.addLog('Your party has fallen...');
      this.state = 'message';
      setTimeout(() => this.handleDefeat(), 1500);
      return;
    }
    if (this.enemies.every(e => e.hp <= 0)) {
      this.addLog('Victory!');
      this.state = 'message';
      Audio.sfx.victory();
      setTimeout(() => this.handleVictory(), 1200);
      return;
    }
    // Advance to next actor
    this.turnIdx++;
    while (this.turnIdx < this.turnQueue.length) {
      const a = this.turnQueue[this.turnIdx];
      if (a.acted) { this.turnIdx++; continue; }
      if (a.kind === 'player') {
        if (this.party[a.idx].hp <= 0) { a.acted = true; this.turnIdx++; continue; }
        // Player turn
        this.activeIdx = a.idx;
        this.state = 'menu';
        return;
      } else {
        if (this.enemies[a.idx].hp <= 0) { a.acted = true; this.turnIdx++; continue; }
        // Enemy turn
        this.enemyTurn(a.idx);
        return;
      }
    }
    // All acted - new round
    this.turnQueue.forEach(a => a.acted = false);
    this.turnIdx = 0;
    // Apply end-of-round effects
    this.applyEndOfRound();
    // Re-sort for new turn order
    this.buildTurnQueue();
    this.nextTurn();
  },
  
  applyEndOfRound() {
    for (const c of [...this.party, ...this.enemies]) {
      if (c.hp <= 0) continue;
      // Burn
      if (c.status === 'burn') {
        const dmg = Math.max(1, Math.floor(c.maxHp * 0.06));
        c.hp = Math.max(0, c.hp - dmg);
        this.addLog(`${c.name} is hurt by burn. (-${dmg})`);
      }
      // Regen
      if (c.status === 'regen') {
        const heal = Math.max(1, Math.floor(c.maxHp * 0.08));
        c.hp = Math.min(c.maxHp, c.hp + heal);
        this.addLog(`${c.name} regenerated ${heal} HP.`);
      }
      // Curse
      if (c.status === 'curse') {
        const dmg = Math.max(1, Math.floor(c.maxHp * 0.05));
        c.hp = Math.max(0, c.hp - dmg);
        this.addLog(`${c.name} suffers the curse. (-${dmg})`);
      }
    }
  },
  
  async enemyTurn(idx) {
    this.state = 'anim';
    const e = this.enemies[idx];
    // Pick a random ability
    const abId = pick(e.abilityIds);
    const ab = ABILITIES[abId];
    // Pick a target - first alive
    const alive = this.party.filter(c => c.hp > 0);
    if (alive.length === 0) { this.nextTurn(); return; }
    const target = alive[0];
    this.addLog(`${e.name} uses ${ab.name}!`);
    await this.delay(600);
    await this.executeMove(e, ab, target, 'player', 0);
    this.afterAction();
  },
  
  async executeMove(attacker, abil, target, side, targetIdx) {
    if (!abil) return;
    // Accuracy check
    if (Math.random() * 100 > abil.acc) {
      this.addLog(`${attacker.name}'s attack missed!`);
      Audio.sfx.miss();
      this.spawnMissText(target);
      return;
    }
    
    // Status: shock - chance to skip
    if (attacker.status === 'shock' && Math.random() < 0.3) {
      this.addLog(`${attacker.name} is shocked and cannot move!`);
      Audio.sfx.shock();
      this.spawnEffect(attacker.x || viewW*0.5, attacker.y || viewH*0.5, '#ffea78');
      return;
    }
    
    // Barrier
    if (target.status === 'barrier') {
      target.status = null;
      this.addLog(`${target.name}'s barrier deflected the attack!`);
      this.spawnEffect(target.x || viewW*0.5, target.y || viewH*0.5, '#7fffe5');
      return;
    }
    
    // Damage calc
    let mult = 1;
    if (abil.type) {
      // Check both attacker types
      for (const atkType of attacker.types || []) {
        const t = TYPE_CHART[atkType] || {};
        if (t[target.types[0]] !== undefined) mult *= t[target.types[0]];
        if (target.types[1] && t[target.types[1]] !== undefined) mult *= t[target.types[1]];
      }
    }
    const isCrit = Math.random() < 0.0625 || abil.fx === 'crit';
    const stab = (abil.type && attacker.types.includes(abil.type)) ? 1.25 : 1;
    const level = attacker.level;
    const atk = attacker.atk * (1 + (attacker.statMods?.atk || 0));
    const def = target.def * (1 + (target.statMods?.def || 0));
    const burnHalf = attacker.status === 'burn' ? 0.5 : 1;
    // Damage formula: (level*0.4 + 3) * power/35 * atk/def * multipliers
    const baseScalar = level * 0.4 + 3;
    const powerFactor = abil.power / 35;
    const ratio = atk / Math.max(1, def);
    let dmg = baseScalar * powerFactor * ratio * mult * stab * burnHalf;
    if (isCrit) dmg *= 1.5;
    dmg = Math.floor(dmg) + randInt(2, 4);
    if (dmg < 1 && abil.power > 0) dmg = 1;
    if (mult >= 2) {
      this.addLog(`It's super effective!`);
      Audio.sfx.super();
    } else if (mult < 0.75) {
      this.addLog(`It's not very effective...`);
    }
    if (isCrit) { this.addLog(`A critical hit!`); Audio.sfx.crit(); this.shake = 12; }
    if (abil.fx === 'priority') this.addLog(`${attacker.name} moves first!`);
    
    // Animate
    await this.animateAttack(attacker, target, abil);
    
    if (dmg > 0) {
      target.hp = Math.max(0, target.hp - dmg);
      this.spawnDamageText(target, dmg, isCrit, mult >= 2);
      Audio.sfx.hit();
      this.shake = Math.max(this.shake, isCrit ? 14 : 6);
      if (mult >= 2) {
        // Burst particles
        for (let i = 0; i < 14; i++) {
          const a = rand(0, Math.PI*2);
          this.spawnParticle(target.x || viewW*0.5, target.y || viewH*0.5, a, '#ffd76b', 5);
        }
      }
    }
    
    // Multi-hit
    if (abil.fx === 'multihit' && abil.hits) {
      for (let h = 1; h < abil.hits; h++) {
        await this.delay(200);
        if (target.hp <= 0) break;
        const subDmg = Math.floor(dmg * 0.6);
        target.hp = Math.max(0, target.hp - subDmg);
        this.addLog(`Hit ${h+1}!`);
        this.spawnDamageText(target, subDmg, false, false, true);
        Audio.sfx.hit();
        this.shake = Math.max(this.shake, 4);
      }
    }
    
    // Apply special effects
    await this.applyEffect(attacker, target, abil, side);
  },
  
  async applyEffect(attacker, target, abil, side) {
    if (!abil.fx) return;
    const r = Math.random();
    
    switch (abil.fx) {
      case 'burn':
        if (target.status == null && (abil.chance == null || r < abil.chance)) {
          target.status = 'burn';
          this.addLog(`${target.name} was burned!`);
          this.spawnEffect(target.x || viewW*0.5, target.y || viewH*0.5, '#ff6e3a');
          Audio.sfx.burn();
        }
        break;
      case 'frost':
        if (target.status == null && (abil.chance == null || r < abil.chance)) {
          target.status = 'frost';
          this.addLog(`${target.name} was frostbitten!`);
          this.spawnEffect(target.x || viewW*0.5, target.y || viewH*0.5, '#9cdcff');
        }
        break;
      case 'shock':
        if (target.status == null && (abil.chance == null || r < abil.chance)) {
          target.status = 'shock';
          this.addLog(`${target.name} was shocked!`);
          this.spawnEffect(target.x || viewW*0.5, target.y || viewH*0.5, '#ffea78');
          Audio.sfx.shock();
        }
        break;
      case 'regen':
        attacker.status = 'regen';
        this.addLog(`${attacker.name} is regenerating.`);
        this.spawnEffect(attacker.x || viewW*0.5, attacker.y || viewH*0.5, '#7cf2a8');
        break;
      case 'heal':
        if (attacker.hp > 0) {
          const amt = Math.floor(attacker.maxHp * (abil.amt || 0.4));
          attacker.hp = Math.min(attacker.maxHp, attacker.hp + amt);
          this.addLog(`${attacker.name} restored ${amt} HP.`);
          this.spawnEffect(attacker.x || viewW*0.5, attacker.y || viewH*0.5, '#7cf2a8');
          Audio.sfx.heal();
        }
        break;
      case 'leech':
        if (attacker.hp > 0 && target.hp > 0) {
          const steal = Math.max(1, Math.floor(dmg * 0.5));
          attacker.hp = Math.min(attacker.maxHp, attacker.hp + steal);
          this.addLog(`${attacker.name} drained ${steal} HP.`);
        }
        break;
      case 'defup':
        attacker.statMods.def = (attacker.statMods.def || 0) + 1;
        this.addLog(`${attacker.name}'s DEF rose.`);
        this.spawnEffect(attacker.x || viewW*0.5, attacker.y || viewH*0.5, '#a48cff');
        break;
      case 'speedup':
        attacker.statMods.spd = (attacker.statMods.spd || 0) + 1;
        this.addLog(`${attacker.name}'s SPD rose.`);
        this.spawnEffect(attacker.x || viewW*0.5, attacker.y || viewH*0.5, '#5ec3ff');
        break;
      case 'critup':
        attacker.statMods.crit = (attacker.statMods.crit || 0) + 1;
        this.addLog(`${attacker.name} is focusing.`);
        break;
      case 'barrier':
        target.status = 'barrier';
        this.addLog(`${target.name} raised a barrier.`);
        this.spawnEffect(target.x || viewW*0.5, target.y || viewH*0.5, '#7fffe5');
        break;
      case 'curse':
        target.status = 'curse';
        this.addLog(`${target.name} is cursed.`);
        this.spawnEffect(target.x || viewW*0.5, target.y || viewH*0.5, '#a48cff');
        break;
      case 'selfslow':
        attacker.statMods.spd = (attacker.statMods.spd || 0) - 1;
        this.addLog(`${attacker.name}'s SPD fell.`);
        break;
      case 'allup':
        attacker.statMods.atk = (attacker.statMods.atk || 0) + 1;
        attacker.statMods.def = (attacker.statMods.def || 0) + 1;
        attacker.statMods.spd = (attacker.statMods.spd || 0) + 1;
        this.addLog(`${attacker.name}'s stats rose.`);
        this.spawnEffect(attacker.x || viewW*0.5, attacker.y || viewH*0.5, '#ffd76b');
        break;
    }
  },
  
  async animateAttack(attacker, target, abil) {
    // Type-based particle effects
    const type = abil.type;
    const colorMap = {
      Cinder: '#ff8a5b', Flora: '#7cd66a', Tidal: '#5ec3ff',
      Storm: '#cba6ff', Crag: '#e0c891', Lumen: '#7fffe5', Shade: '#a48cff'
    };
    const c = type ? colorMap[type] : '#fff';
    // Atk lunge
    attacker.lunge = 1;
    await this.delay(150);
    attacker.lunge = 0;
    // Effect particles at target
    for (let i = 0; i < 8; i++) {
      const a = rand(0, Math.PI*2);
      this.spawnParticle(target.x || viewW*0.5, target.y || viewH*0.5, a, c, 4);
    }
  },
  
  delay(ms) { return new Promise(r => setTimeout(r, ms)); },
  
  spawnParticle(x, y, angle, color, speed = 100) {
    this.particles.push({
      x, y, age: 0, life: rand(0.5, 1),
      vx: Math.cos(angle) * speed, vy: Math.sin(angle) * speed,
      color, size: rand(2, 5), gravity: 80
    });
  },
  
  spawnDamageText(target, dmg, isCrit, isSE, isMulti = false) {
    const el = document.createElement('div');
    el.className = 'dmg-num' + (isCrit ? ' crit' : '') + (isSE ? ' se' : '');
    el.textContent = (isCrit ? '✦ ' : '') + '-' + dmg + (isMulti ? '!' : '');
    el.style.left = ((target.x || viewW*0.5) + rand(-30, 30)) + 'px';
    el.style.top = ((target.y || viewH*0.5) - 30) + 'px';
    document.getElementById('ui-layer').appendChild(el);
    setTimeout(() => el.remove(), 1300);
  },
  
  spawnMissText(target) {
    const el = document.createElement('div');
    el.className = 'dmg-num miss';
    el.textContent = 'MISS';
    el.style.left = ((target.x || viewW*0.5)) + 'px';
    el.style.top = ((target.y || viewH*0.5) - 30) + 'px';
    document.getElementById('ui-layer').appendChild(el);
    setTimeout(() => el.remove(), 1300);
  },
  
  spawnEffect(x, y, color) {
    for (let i = 0; i < 10; i++) {
      const a = rand(0, Math.PI*2);
      this.particles.push({
        x, y, age: 0, life: rand(0.6, 1.2),
        vx: Math.cos(a) * 80, vy: Math.sin(a) * 80,
        color, size: rand(3, 6), gravity: 0
      });
    }
  },
  
  handleVictory() {
    // Calculate EXP
    let totalExp = 0;
    let totalGold = 0;
    for (const e of this.enemies) {
      const level = e.level;
      totalExp += Math.floor(8 + level * 3 + level * level * 0.1);
      totalGold += Math.floor(5 + level * 2 + (e.isAnchor ? 200 : 0));
    }
    this.expAwarded = totalExp;
    // Gold
    Game.gold += totalGold;
    this.addLog(`You gained ${totalGold} gold.`);
    // Award to active
    const active = Game.party[this.activeIdx];
    if (active && active.hp > 0) {
      const res = addExp(active, totalExp);
      this.addLog(`${active.name} gained ${totalExp} EXP.`);
      if (res.leveled) {
        this.addLog(`${active.name} grew to level ${active.level}!`);
        Audio.sfx.levelup();
      }
      if (res.learned) {
        this.addLog(`${active.name} learned ${ABILITIES[res.learned].name}!`);
        if (!active.abilityIds.includes(res.learned)) active.abilityIds.push(res.learned);
      }
    }
    // Distribute some to others
    const perEach = Math.floor(totalExp * 0.3);
    for (let i = 0; i < Game.party.length; i++) {
      if (i === this.activeIdx) continue;
      const c = Game.party[i];
      if (c && c.hp > 0) addExp(c, perEach);
    }
    // Mark as seen
    for (const e of this.enemies) {
      Game.seen.add(e.species);
      if (e.isAnchor) {
        Game.defeatedAnchors.add(`${Game.currentZone}_${e.species}_${e.level}`);
        // Unlock next zone
        Game.unlockedZones = Math.max(Game.unlockedZones, Game.currentZone + 2);
        Audio.sfx.fanf();
        this.addLog('A new path opens...');
      }
    }
    // Update party HP, PP, status
    for (let i = 0; i < Game.party.length; i++) {
      if (Game.party[i] && this.party[i]) {
        Game.party[i].hp = this.party[i].hp;
        if (this.party[i].pp) Game.party[i].pp = {...this.party[i].pp};
        if (this.party[i].status) Game.party[i].status = this.party[i].status;
      }
    }
    // Story progress
    updateStoryProgress();
    // Save
    saveGame();
    this.end(true);
  },
  
  handleDefeat() {
    // Wipe to last shrine
    Game.gold = Math.floor(Game.gold * 0.5);
    // Heal party
    for (const c of Game.party) {
      if (c) c.hp = 1;
    }
    saveGame();
    this.end(false);
  },
  
  end(victory) {
    this.active = false;
    Game.state = 'exploration';
    Audio.playMusic('overworld');
    hideBattleUI();
    // Remove any lingering battle message
    document.getElementById('battle-message')?.remove();
  }
};

function updateStoryProgress() {
  // Stage 0: tutorial done
  // Stage 1: first anchor defeated
  // Stage 2: 2 anchors defeated
  // Stage 3: 4 anchors defeated
  const defeated = Game.defeatedAnchors.size;
  let newStage = 0;
  if (Game.storyStage === 0 && defeated >= 1) newStage = 1;
  if (Game.storyStage === 1 && defeated >= 2) newStage = 2;
  if (Game.storyStage < 3 && defeated >= 4) newStage = 3;
  if (newStage > Game.storyStage) {
    Game.storyStage = newStage;
    UI.showStory(newStage);
  }
  // Post-game: harder spawns
  Game.postGame = defeated >= 4;
}

// Check victory condition: 8+ unique creatures caught
function checkVictoryCondition() {
  if (Game.caught.size >= 8 && !Game.finalVictory) {
    Game.finalVictory = true;
    setTimeout(() => {
      UI.showDialogue('Wisp', 'You have bound eight creatures. The lanterns along the Hollow have begun to hum together. The path that was hidden is no longer hidden. Walk well, Binder.', () => {});
    }, 500);
  }
}


// ---------- Battle Renderer ----------
function drawBattle(dt) {
  // Sky / background per zone
  const z = ZONES[Game.currentZone] || ZONES[0];
  const grad = ctx.createLinearGradient(0, 0, 0, viewH);
  grad.addColorStop(0, z.bgGradient[0]);
  grad.addColorStop(0.6, z.bgGradient[1]);
  grad.addColorStop(1, z.bgGradient[2]);
  ctx.fillStyle = grad;
  ctx.fillRect(0, 0, viewW, viewH);
  
  // Floor
  ctx.fillStyle = 'rgba(0,0,0,0.3)';
  ctx.fillRect(0, viewH*0.65, viewW, viewH*0.35);
  
  // Platforms
  drawPlatform(viewW*0.78, viewH*0.55, 80); // enemy
  drawPlatform(viewW*0.25, viewH*0.7, 110); // player
  
  // Set positions
  for (let i = 0; i < Battle.enemies.length; i++) {
    const e = Battle.enemies[i];
    e.x = viewW*0.78 - i * 30;
    e.y = viewH*0.45 - i * 15;
  }
  if (Battle.party[Battle.activeIdx]) {
    Battle.party[Battle.activeIdx].x = viewW*0.25;
    Battle.party[Battle.activeIdx].y = viewH*0.6;
  }
  
  // Draw enemies
  for (let i = 0; i < Battle.enemies.length; i++) {
    const e = Battle.enemies[i];
    if (e.hp <= 0) continue;
    const sx = e.lunge ? e.x - 20 : e.x;
    const sy = e.y;
    const size = e.isAnchor ? 90 : 70;
    if (e.shakeT > 0) {
      e.shakeT -= dt;
    }
    const shx = e.shakeT > 0 ? rand(-3, 3) : 0;
    drawCreatureArt(ctx, e.species, sx + shx, sy, size);
    // HP bar
    if (i === 0 || Battle.enemies.length === 1) {
      drawBattleHpBar(sx - 60, sy - size*0.7, 120, 8, e.hp, e.maxHp, e.name, e.level, e.isAnchor);
    }
  }
  
  // Draw active party
  const p = Battle.party[Battle.activeIdx];
  if (p && p.hp > 0) {
    const sx = p.lunge ? p.x + 20 : p.x;
    drawCreatureArt(ctx, p.species, sx, p.y, 100);
    drawBattleHpBar(sx - 70, p.y - 80, 140, 12, p.hp, p.maxHp, p.name, p.level, false, true);
    // Status badges
    if (p.status) {
      ctx.font = '11px sans-serif';
      ctx.fillStyle = '#fff';
      ctx.textAlign = 'center';
      ctx.fillText('[' + p.status.toUpperCase() + ']', sx, p.y - 90);
    }
  }
  
  // Particles
  for (let i = Battle.particles.length - 1; i >= 0; i--) {
    const p = Battle.particles[i];
    p.age += dt;
    if (p.age >= p.life) { Battle.particles.splice(i, 1); continue; }
    p.x += p.vx * dt;
    p.y += p.vy * dt;
    p.vx *= 0.95; p.vy *= 0.95;
    if (p.gravity) p.vy += p.gravity * dt;
    const t = p.age / p.life;
    ctx.save();
    ctx.globalAlpha = 1 - t;
    ctx.fillStyle = p.color;
    ctx.shadowColor = p.color; ctx.shadowBlur = 6;
    ctx.beginPath();
    ctx.arc(p.x, p.y, p.size * (1 - t * 0.5), 0, Math.PI*2);
    ctx.fill();
    ctx.restore();
  }
  
  // Flash
  if (Battle.flashAlpha > 0) {
    ctx.fillStyle = Battle.flashColor;
    ctx.globalAlpha = Battle.flashAlpha;
    ctx.fillRect(0, 0, viewW, viewH);
    ctx.globalAlpha = 1;
    Battle.flashAlpha -= dt * 3;
  }
  
  // Shake
  if (Battle.shake > 0) {
    Battle.shake -= dt * 30;
  }
}

function drawPlatform(x, y, w) {
  ctx.fillStyle = 'rgba(0,0,0,0.5)';
  ctx.beginPath();
  ctx.ellipse(x, y, w, 12, 0, 0, Math.PI*2);
  ctx.fill();
  ctx.fillStyle = 'rgba(127,255,229,0.15)';
  ctx.beginPath();
  ctx.ellipse(x, y, w*0.7, 6, 0, 0, Math.PI*2);
  ctx.fill();
}

function drawBattleHpBar(x, y, w, h, hp, max, name, level, big, isPlayer) {
  const pct = Math.max(0, hp / max);
  // Backdrop
  ctx.fillStyle = 'rgba(0,0,0,0.7)';
  ctx.fillRect(x - 1, y - 1, w + 2, h + 2);
  ctx.fillStyle = '#1a1530';
  ctx.fillRect(x, y, w, h);
  // Fill
  let color = '#4af2a4';
  if (pct < 0.5) color = '#ffb45a';
  if (pct < 0.2) color = '#ff5a7a';
  ctx.fillStyle = color;
  ctx.fillRect(x, y, w * pct, h);
  // Name
  if (name) {
    ctx.font = 'bold 12px sans-serif';
    ctx.fillStyle = '#fff';
    ctx.textAlign = 'left';
    ctx.fillText(name + (level != null ? ' L' + level : ''), x, y - 4);
  }
  if (isPlayer) {
    ctx.font = '10px monospace';
    ctx.fillStyle = '#b6a8e0';
    ctx.textAlign = 'right';
    ctx.fillText(Math.floor(hp) + '/' + Math.floor(max), x + w, y + h + 12);
  }
}



// ---------- UI System ----------
const UI = {
  currentMenu: null,
  
  // Open a modal
  openModal(html, wide = false) {
    this.closeModal();
    const bd = document.createElement('div');
    bd.className = 'modal-backdrop';
    bd.id = 'modal-bd';
    const m = document.createElement('div');
    m.className = 'modal' + (wide ? ' wide' : '');
    m.id = 'modal-content';
    m.innerHTML = html + '<button class="modal-close" data-action="close">✕</button>';
    bd.appendChild(m);
    document.getElementById('ui-layer').appendChild(bd);
    bd.addEventListener('click', e => {
      if (e.target === bd) this.closeModal();
    });
    m.querySelector('.modal-close').addEventListener('click', () => this.closeModal());
    return m;
  },
  
  closeModal() {
    const bd = document.getElementById('modal-bd');
    if (bd) bd.remove();
    this.currentMenu = null;
  },
  
  showToast(text, duration = 2000) {
    let t = document.getElementById('toast');
    if (!t) {
      t = document.createElement('div');
      t.id = 'toast';
      document.getElementById('ui-layer').appendChild(t);
    }
    t.textContent = text;
    t.classList.add('show');
    t.classList.remove('hide');
    clearTimeout(this._toastT);
    this._toastT = setTimeout(() => {
      t.classList.add('hide');
      t.classList.remove('show');
    }, duration);
  },
  
  showDialogue(speaker, text, onDone) {
    this.closeModal();
    if (this._dlg) this._dlg.remove();
    const prevState = Game.state;
    Game.state = 'dialogue';
    this._dlg = document.createElement('div');
    this._dlg.id = 'dialogue-box';
    this._dlg.className = 'show';
    this._dlg.innerHTML = `<div class="dialogue-speaker">${speaker}</div><div class="dialogue-text"></div><div class="dialogue-hint">▼ tap or press E to continue</div>`;
    document.getElementById('ui-layer').appendChild(this._dlg);
    this._typeText(text, () => {
      // Wait for click or E
      const handler = () => {
        this._dlg.removeEventListener('click', handler);
        this._dlg.remove();
        if (Game.state === 'dialogue') Game.state = prevState || 'exploration';
        if (onDone) onDone();
      };
      this._dlg.addEventListener('click', handler);
      const keyHandler = (e) => {
        if (e.key.toLowerCase() === 'e' || e.key === ' ' || e.key === 'Enter') {
          window.removeEventListener('keydown', keyHandler);
          handler();
        }
      };
      window.addEventListener('keydown', keyHandler);
    });
  },
  
  _typeText(text, onDone) {
    const el = this._dlg.querySelector('.dialogue-text');
    let i = 0;
    el.textContent = '';
    const tick = () => {
      if (i < text.length) {
        el.textContent += text[i++];
        setTimeout(tick, 22);
      } else {
        if (onDone) onDone();
      }
    };
    tick();
  },
  
  showVignette(stage) {
    const stages = {
      1: { tag: 'Chapter I', text: 'A child walked the Embergrove with a lantern made of foxfire. The first anchor stirred. The lantern did not go out.' },
      2: { tag: 'Chapter II', text: 'Two of the four anchors have fallen. The path to Lumenglass shimmers, and the binder hears a new chord beneath the dust.' },
      3: { tag: 'Chapter III', text: 'The hollows are quiet now. The lanterns light themselves. Somewhere, a door unlocks from the inside.' }
    };
    const data = stages[stage];
    if (!data) return;
    const v = document.createElement('div');
    v.className = 'vignette show';
    v.innerHTML = `<div class="vignette-text"><span class="vt-stage">${data.tag}</span>${data.text}</div>`;
    document.getElementById('ui-layer').appendChild(v);
    Audio.sfx.fanf();
    setTimeout(() => {
      v.style.transition = 'opacity 1s';
      v.style.opacity = '0';
      setTimeout(() => v.remove(), 1000);
    }, 4500);
  },
  
  showStory(stage) { this.showVignette(stage); },
  
  // Capture modal
  openCaptureModal(creature, itemId) {
    const tier = ITEMS[itemId].tier;
    const baseChance = 0.3 + (creature.hp / creature.maxHp < 0.2 ? 0.2 : 0) + ITEMS[itemId].catchBonus + tier * 0.05;
    
    const html = `
      <div id="capture-modal" class="show">
        <div id="capture-rings">
          <div class="capture-ring outer"></div>
          <div class="capture-ring inner"></div>
        </div>
        <div id="capture-hint">
          Tap the rings as they pass through the center!<br>
          <span style="color:var(--lumen)">${creature.name}</span> · HP ${Math.floor(creature.hp)}/${creature.maxHp}
        </div>
        <div id="capture-timer"><div id="capture-timer-fill"></div></div>
      </div>
    `;
    document.getElementById('ui-layer').insertAdjacentHTML('beforeend', html);
    this._captureState = { tier, baseChance, creature, itemId, hits: 0, total: 0, time: 4.0, ring: 1, lastRingChange: 0 };
    // Shrinking ring animation
    let scale = 1.5;
    let innerScale = 0.4;
    let shrinking = true;
    let lastT = performance.now();
    const updateRing = () => {
      const now = performance.now();
      const dt = (now - lastT) / 1000;
      lastT = now;
      if (shrinking) {
        scale -= 0.6 * dt;
        if (scale < 0.5) { shrinking = false; }
      } else {
        scale += 0.6 * dt;
        if (scale > 1.5) { shrinking = true; }
      }
      document.querySelector('.capture-ring.outer').style.setProperty('--ring-scale', scale);
    };
    this._captureInterval = setInterval(updateRing, 16);
    this._captureTimer = setInterval(() => {
      this._captureState.time -= 0.1;
      document.getElementById('capture-timer-fill').style.width = (this._captureState.time / 4.0 * 100) + '%';
      if (this._captureState.time <= 0) this.finishCapture();
    }, 100);
    
    // Click handlers
    const rings = document.querySelectorAll('.capture-ring');
    rings.forEach(ring => {
      ring.addEventListener('click', () => {
        if (this._captureState.time <= 0) return;
        const isOuter = ring.classList.contains('outer');
        const scaleVal = parseFloat(getComputedStyle(ring).getPropertyValue('--ring-scale'));
        // Hit if scale is between 0.9 and 1.1
        const hit = scaleVal > 0.85 && scaleVal < 1.15;
        ring.classList.add('hit');
        setTimeout(() => ring.classList.remove('hit'), 100);
        this._captureState.hits += hit ? 1 : 0;
        this._captureState.total += 1;
        Audio.sfx.confirm();
        // Update hint
        document.getElementById('capture-hint').innerHTML = `Hits: ${this._captureState.hits}/${this._captureState.total}`;
      });
    });
  },
  
  finishCapture() {
    clearInterval(this._captureInterval);
    clearInterval(this._captureTimer);
    const m = document.getElementById('capture-modal');
    if (m) m.remove();
    const s = this._captureState;
    if (s.hits === 0) s.hits = 0;
    const accuracy = s.total > 0 ? s.hits / s.total : 0;
    const hpFactor = 1 - (s.creature.hp / s.creature.maxHp);
    const finalChance = s.baseChance + accuracy * 0.3 + hpFactor * 0.2;
    const success = Math.random() < finalChance;
    if (success) {
      // Captured!
      const species = s.creature.species;
      Game.seen.add(species);
      Game.caught.add(species);
      const c = makeCreature(species, s.creature.level);
      c.hp = c.maxHp;
      // Add to storage
      if (Game.party.length < MAX_PARTY) {
        Game.party.push(c);
      } else {
        Game.storage.push(c);
      }
      Battle.capturedThisBattle = true;
      Audio.sfx.capture();
      this.showToast(`✨ ${s.creature.name} was bound!`);
      Battle.addLog(`${s.creature.name} was bound to your party!`);
      // Set enemy hp to 0
      s.creature.hp = 0;
      // Mark all enemies for removal
      for (const e of Battle.enemies) e.hp = 0;
      // Animate
      this.captureBurst(s.creature);
      const isFirstCatch = Game.totalCaught === 0;
      Game.totalCaught++;
      checkVictoryCondition();
      setTimeout(() => {
        Battle.end(true);
        saveGame();
        if (isFirstCatch) {
          setTimeout(() => {
            UI.showDialogue('Wisp', 'You have made your first binding. The creature will follow you now. Rest at the shrine when you are tired, and seek the anchor at the world\'s edge.', () => {});
          }, 800);
        }
      }, 1800);
    } else {
      Audio.sfx.capturefail();
      this.showToast('The bond broke loose!');
      Battle.addLog('The bond failed!');
      // Use a turn
      Battle.afterAction();
    }
  },
  
  captureBurst(c) {
    const center = { x: viewW*0.78, y: viewH*0.45 };
    for (let i = 0; i < 40; i++) {
      const a = (i / 40) * Math.PI * 2;
      Battle.particles.push({
        x: center.x, y: center.y, age: 0, life: 1.2,
        vx: Math.cos(a) * 200, vy: Math.sin(a) * 200,
        color: '#7fffe5', size: 4, gravity: 0
      });
    }
  },
  
  // Open specific menus
  openPartyMenu(onSelect) {
    this.closeModal();
    const html = `
      <h2>Your Party</h2>
      <div class="party-grid" id="party-grid"></div>
    `;
    const m = this.openModal(html, true);
    const grid = m.querySelector('#party-grid');
    Game.party.forEach((c, i) => {
      const card = document.createElement('div');
      card.className = 'party-card' + (i === Game.activeCreature ? ' active' : '') + (c.hp <= 0 ? ' fainted' : '');
      const canvas = document.createElement('canvas');
      canvas.className = 'pc-mini';
      canvas.width = 64; canvas.height = 64;
      card.appendChild(canvas);
      const name = document.createElement('div');
      name.className = 'pc-name';
      name.textContent = c.name + (CREATURES[c.species].isTransformation ? ' ★' : '');
      card.appendChild(name);
      const lvl = document.createElement('span');
      lvl.className = 'pc-lvl';
      lvl.textContent = 'Lv' + c.level;
      name.appendChild(lvl);
      const type = document.createElement('div');
      type.className = 'pc-type';
      type.textContent = CREATURES[c.species].types.join(' / ');
      card.appendChild(type);
      const hp = document.createElement('div');
      hp.className = 'pc-hp';
      const fill = document.createElement('div');
      fill.className = 'pc-hp-fill';
      fill.style.width = (c.hp / c.maxHp * 100) + '%';
      hp.appendChild(fill);
      card.appendChild(hp);
      // Draw art
      const cctx = canvas.getContext('2d');
      drawCreatureArt(cctx, c.species, 32, 32, 28);
      card.addEventListener('click', () => {
        if (onSelect) onSelect(i);
        else {
          // Open detail
          this.openCreatureDetail(i);
        }
      });
      grid.appendChild(card);
    });
  },
  
  openCreatureDetail(idx) {
    const c = Game.party[idx];
    const s = CREATURES[c.species];
    const html = `
      <h2>${c.name}</h2>
      <div style="display:grid;grid-template-columns:1fr 1fr;gap:18px;align-items:start">
        <div>
          <canvas id="cd-art" width="180" height="180" style="background:rgba(0,0,0,.3);border-radius:14px;display:block;margin:0 auto"></canvas>
          <p style="color:var(--ink-1);font-style:italic;margin-top:12px;font-size:13px;line-height:1.5">${s.lore}</p>
        </div>
        <div>
          <h3>Profile</h3>
          <div style="display:grid;grid-template-columns:auto 1fr;gap:6px 14px;font-size:13px">
            <span style="color:var(--ink-1)">Type</span><span style="color:var(--lumen)">${s.types.join(' / ')}</span>
            <span style="color:var(--ink-1)">Level</span><span>${c.level} / ${MAX_LEVEL}</span>
            <span style="color:var(--ink-1)">EXP</span><span>${c.exp} / ${expToNext(c.level)}</span>
          </div>
          <h3>Stats</h3>
          <div style="display:grid;grid-template-columns:auto 1fr;gap:6px 14px;font-size:13px">
            <span style="color:var(--ink-1)">HP</span><span>${c.hp} / ${c.maxHp}</span>
            <span style="color:var(--ink-1)">ATK</span><span>${c.atk}</span>
            <span style="color:var(--ink-1)">DEF</span><span>${c.def}</span>
            <span style="color:var(--ink-1)">SPD</span><span>${c.spd}</span>
          </div>
          <h3>Abilities</h3>
          <div style="display:flex;flex-direction:column;gap:6px">
            ${c.abilityIds.map(ab => {
              const a = ABILITIES[ab];
              if (!a) return '';
              return `<div style="display:flex;justify-content:space-between;padding:8px 10px;background:rgba(0,0,0,.3);border-radius:8px;font-size:12px">
                <span><span style="color:${TYPE_COLORS[a.type]||'#fff'}">${a.name}</span><br><span style="color:var(--ink-2);font-size:10px">${a.desc}</span></span>
                <span style="color:var(--ink-1);font-family:var(--f-mono)">PWR ${a.power}</span>
              </div>`;
            }).join('')}
          </div>
          ${s.evolves && c.level >= s.evolves.at ? `
            <h3>Evolution</h3>
            <div style="padding:12px;background:rgba(127,255,229,.1);border:1px solid var(--lumen-dim);border-radius:8px;font-size:12px">
              <div>Can transform into <strong style="color:var(--lumen)">${CREATURES[s.evolves.to].name}</strong></div>
              <div style="color:var(--ink-1);margin-top:6px">Requires: <strong>${ITEMS[s.evolves.item]?.name || s.evolves.item}</strong></div>
              <button class="battle-btn" data-evo="${idx}" style="margin-top:8px;width:100%">Transform</button>
            </div>
          ` : ''}
        </div>
      </div>
    `;
    const m = this.openModal(html, true);
    const ac = m.querySelector('#cd-art');
    const actx = ac.getContext('2d');
    drawCreatureArt(actx, c.species, 90, 90, 80);
    // Evo button
    const evoBtn = m.querySelector('[data-evo]');
    if (evoBtn) {
      evoBtn.addEventListener('click', () => {
        const item = CREATURES[c.species].evolves.item;
        if (Game.inventory[item] && Game.inventory[item] > 0) {
          Game.inventory[item]--;
          this.evolveCreature(idx);
          this.closeModal();
        } else {
          this.showToast(`You need ${ITEMS[item]?.name || item}.`);
        }
      });
    }
  },
  
  evolveCreature(idx) {
    const c = Game.party[idx];
    const s = CREATURES[c.species];
    const target = s.evolves.to;
    const targetS = CREATURES[target];
    this.showDialogue('The Hollow', `${c.name} trembles. The light gathers. A new shape emerges.`, () => {
      const oldName = c.name;
      c.species = target;
      c.name = targetS.name;
      c.types = [...targetS.types];
      // Re-learn abilities
      const known = getKnownAbilities(target, c.level);
      c.abilityIds = known.slice(-4);
      if (c.abilityIds.length < 4) while (c.abilityIds.length < 4) c.abilityIds.push('tackle');
      recalcStats(c);
      c.hp = c.maxHp;
      c.transformations = (c.transformations || 0) + 1;
      Audio.sfx.fanf();
      this.showToast(`✨ ${oldName} became ${c.name}!`);
      // Burst
      for (let i = 0; i < 30; i++) {
        const a = rand(0, Math.PI*2);
        Battle.particles.push({
          x: viewW/2, y: viewH/2, age: 0, life: 1.5,
          vx: Math.cos(a) * 200, vy: Math.sin(a) * 200,
          color: '#7fffe5', size: 4, gravity: 0
        });
      }
    });
  },
  
  openBagMenu() {
    this.closeModal();
    const items = Object.entries(Game.inventory).filter(([k, v]) => v > 0);
    const html = `
      <h2>Inventory</h2>
      <div style="margin-bottom:12px;color:var(--ink-1);font-size:12px">Gold: <span style="color:var(--gold)">${Game.gold}</span> ◆</div>
      <div class="items-list" id="items-list"></div>
    `;
    const m = this.openModal(html);
    const list = m.querySelector('#items-list');
    if (items.length === 0) {
      list.innerHTML = '<div style="color:var(--ink-2);text-align:center;padding:20px">No items.</div>';
    }
    items.forEach(([id, qty]) => {
      const it = ITEMS[id];
      if (!it) return;
      const row = document.createElement('div');
      row.className = 'item-row';
      row.innerHTML = `<div>
        <div class="item-name">${it.name}</div>
        <div class="item-desc">${it.desc}</div>
      </div><div class="item-qty">×${qty}</div>`;
      list.appendChild(row);
    });
  },
  
  openBestiary() {
    this.closeModal();
    const html = `<h2>Bestiary</h2>
      <div style="color:var(--ink-1);font-size:12px;margin-bottom:14px">Discovered: ${Game.caught.size} / ${CREATURE_IDS.length}</div>
      <div class="bestiary-grid" id="bestiary-grid"></div>`;
    const m = this.openModal(html, true);
    const grid = m.querySelector('#bestiary-grid');
    // Group: base creatures first, then transformations
    const ordered = [...CREATURE_IDS].sort((a, b) => {
      const sa = CREATURES[a], sb = CREATURES[b];
      return (sa.isTransformation ? 1 : 0) - (sb.isTransformation ? 1 : 0);
    });
    for (const id of ordered) {
      const s = CREATURES[id];
      const known = Game.seen.has(id);
      const caught = Game.caught.has(id);
      const card = document.createElement('div');
      card.className = 'bestiary-card ' + (caught ? 'caught' : known ? 'seen' : 'unknown');
      const canvas = document.createElement('canvas');
      canvas.className = 'bc-mini';
      canvas.width = 60; canvas.height = 60;
      card.appendChild(canvas);
      const name = document.createElement('div');
      name.className = 'bc-name';
      name.textContent = known ? s.name : '???';
      card.appendChild(name);
      if (known) {
        const cctx = canvas.getContext('2d');
        drawCreatureArt(cctx, id, 30, 30, 26);
        card.addEventListener('click', () => {
          this.openBestiaryDetail(id);
        });
      } else {
        const cctx = canvas.getContext('2d');
        cctx.fillStyle = '#444';
        cctx.beginPath();
        cctx.arc(30, 30, 20, 0, Math.PI*2);
        cctx.fill();
        cctx.fillStyle = '#222';
        cctx.font = '20px sans-serif';
        cctx.textAlign = 'center';
        cctx.fillText('?', 30, 38);
      }
      grid.appendChild(card);
    }
  },
  
  openBestiaryDetail(id) {
    const s = CREATURES[id];
    const caught = Game.caught.has(id);
    const html = `
      <h2>${s.name}${s.isTransformation ? ' ★' : ''}</h2>
      <div style="display:grid;grid-template-columns:1fr 1fr;gap:18px">
        <div>
          <canvas id="bd-art" width="160" height="160" style="background:rgba(0,0,0,.3);border-radius:14px;display:block;margin:0 auto"></canvas>
          <p style="color:var(--ink-1);font-style:italic;margin-top:12px;font-size:13px;line-height:1.5">${s.lore}</p>
        </div>
        <div>
          <h3>Profile</h3>
          <div style="display:grid;grid-template-columns:auto 1fr;gap:6px 14px;font-size:13px">
            <span style="color:var(--ink-1)">Type</span><span style="color:var(--lumen)">${s.types.join(' / ')}</span>
            <span style="color:var(--ink-1)">Form</span><span>${s.isTransformation ? 'Transformed' : 'Base'}</span>
            <span style="color:var(--ink-1)">Status</span><span style="color:${caught?'var(--good)':'var(--gold)'}">${caught ? 'Bound' : (Game.seen.has(id) ? 'Encountered' : 'Unknown')}</span>
          </div>
          <h3>Base Stats</h3>
          <div style="display:grid;grid-template-columns:auto 1fr;gap:6px 14px;font-size:13px">
            <span style="color:var(--ink-1)">HP</span><span>${s.base.hp}</span>
            <span style="color:var(--ink-1)">ATK</span><span>${s.base.atk}</span>
            <span style="color:var(--ink-1)">DEF</span><span>${s.base.def}</span>
            <span style="color:var(--ink-1)">SPD</span><span>${s.base.spd}</span>
          </div>
        </div>
      </div>
    `;
    const m = this.openModal(html);
    const ac = m.querySelector('#bd-art');
    const actx = ac.getContext('2d');
    drawCreatureArt(actx, id, 80, 80, 70);
  },
  
  openMap() {
    this.closeModal();
    const html = `<h2>World Map</h2>
      <div class="zone-tabs" id="zone-tabs"></div>
      <div class="map-canvas-wrap"><canvas id="map-canvas"></canvas></div>
      <p style="color:var(--ink-1);font-size:13px;margin-top:14px;line-height:1.6" id="map-desc"></p>`;
    const m = this.openModal(html, true);
    const tabs = m.querySelector('#zone-tabs');
    const desc = m.querySelector('#map-desc');
    let active = Game.currentZone;
    const drawMapZone = (idx) => {
      const cnv = m.querySelector('#map-canvas');
      const c = cnv.getContext('2d');
      const w = cnv.width = cnv.offsetWidth, h = cnv.height = cnv.offsetHeight;
      const z = ZONES[idx];
      const grad = c.createLinearGradient(0, 0, 0, h);
      grad.addColorStop(0, z.bgGradient[0]);
      grad.addColorStop(1, z.bgGradient[2]);
      c.fillStyle = grad;
      c.fillRect(0, 0, w, h);
      // Draw a stylized map
      const isUnlocked = idx < Game.unlockedZones;
      c.fillStyle = isUnlocked ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.4)';
      c.fillRect(10, 10, w-20, h-20);
      // Place markers
      const mx = w*0.5, my = h*0.45;
      c.fillStyle = isUnlocked ? z.accent : '#555';
      c.beginPath();
      c.arc(mx, my, 30, 0, Math.PI*2);
      c.fill();
      c.fillStyle = '#fff';
      c.font = 'bold 14px sans-serif';
      c.textAlign = 'center';
      c.fillText(z.name, mx, my+5);
      // Zone # 
      c.font = '11px sans-serif';
      c.fillStyle = '#888';
      c.fillText('Zone ' + (idx+1), mx, my+22);
      // Anchor
      if (z.anchor) {
        const defeated = Game.defeatedAnchors.has(`${idx}_${z.anchor.c}_${z.anchor.level}`);
        c.fillStyle = defeated ? '#7cf2a8' : (isUnlocked ? '#ff5a7a' : '#444');
        c.beginPath();
        c.arc(mx + 80, my - 30, 10, 0, Math.PI*2);
        c.fill();
        c.fillStyle = '#fff';
        c.font = '9px sans-serif';
        c.fillText(defeated ? '✓' : 'A', mx + 80, my - 27);
      }
      // Lock icon
      if (!isUnlocked) {
        c.fillStyle = '#888';
        c.font = '24px sans-serif';
        c.fillText('🔒', w-40, 40);
      }
      // Connections
      if (idx < 5) {
        c.strokeStyle = isUnlocked ? 'rgba(127,255,229,0.5)' : 'rgba(255,255,255,0.1)';
        c.lineWidth = 2;
        c.setLineDash([5, 5]);
        c.beginPath();
        c.moveTo(mx + 30, my);
        c.lineTo(w - 30, h/2);
        c.stroke();
        c.setLineDash([]);
      }
      desc.textContent = z.tagline + (z.anchor ? ` — Anchor: ${z.anchor.name}` : ' — No anchor here.');
    };
    for (let i = 0; i < ZONES.length; i++) {
      const t = document.createElement('div');
      t.className = 'zone-tab' + (i === active ? ' active' : '') + (i >= Game.unlockedZones ? ' locked' : '');
      t.textContent = ZONES[i].name;
      t.addEventListener('click', () => {
        if (i >= Game.unlockedZones) return;
        m.querySelectorAll('.zone-tab').forEach(x => x.classList.remove('active'));
        t.classList.add('active');
        drawMapZone(i);
      });
      tabs.appendChild(t);
    }
    drawMapZone(active);
  },
  
  openShrine() {
    this.closeModal();
    const html = `<h2>Shrine of ${ZONES[Game.currentZone].name}</h2>
      <p style="color:var(--ink-1);font-size:13px;line-height:1.6">A small flame burns here. The lanterns do not speak, but the wind carries memory.</p>
      <div class="choice-list" style="margin-top:18px">
        <button class="choice-btn" data-act="heal">Rest by the flame
          <span class="cb-cost">Heal all party to full HP</span>
        </button>
        <button class="choice-btn" data-act="shop">Browse the wandering merchant
          <span class="cb-cost">Spend gold for supplies</span>
        </button>
        <button class="choice-btn" data-act="save">Commune with the shrine
          <span class="cb-cost">Save your progress</span>
        </button>
        <button class="choice-btn" data-act="evolve">Evolution chamber
          <span class="cb-cost">Transform eligible creatures</span>
        </button>
      </div>`;
    const m = this.openModal(html);
    m.querySelectorAll('[data-act]').forEach(b => {
      b.addEventListener('click', () => {
        const act = b.dataset.act;
        if (act === 'heal') {
          for (const c of Game.party) {
            if (!c) continue;
            c.hp = c.maxHp;
            // Restore PP
            if (!c.pp) c.pp = {};
            for (const ab of c.abilityIds) {
              c.pp[ab] = ABILITIES[ab]?.pp ?? 10;
            }
            // Clear status
            c.status = null;
            c.statMods = { atk: 0, def: 0, spd: 0 };
          }
          Audio.sfx.heal();
          this.showToast('Your party feels restored.');
          saveGame();
          this.closeModal();
        } else if (act === 'save') {
          saveGame();
          this.showToast('The shrine remembers.');
          Audio.sfx.confirm();
        } else if (act === 'evolve') {
          this.openEvolutionList();
        } else if (act === 'shop') {
          this.openShop();
        }
      });
    });
  },
  
  openEvolutionList() {
    this.closeModal();
    const eligible = [];
    for (let i = 0; i < Game.party.length; i++) {
      const c = Game.party[i];
      if (!c) continue;
      const s = CREATURES[c.species];
      if (s.evolves && c.level >= s.evolves.at) {
        eligible.push({ idx: i, c, item: s.evolves.item, target: s.evolves.to });
      }
    }
    if (eligible.length === 0) {
      this.openModal('<h2>Evolution Chamber</h2><p style="color:var(--ink-1)">No creatures are ready to transform yet. Train them further.</p>');
      return;
    }
    let html = '<h2>Evolution Chamber</h2><div class="choice-list">';
    for (const e of eligible) {
      const has = Game.inventory[e.item] && Game.inventory[e.item] > 0;
      html += `<button class="choice-btn" data-evo="${e.idx}">
        <strong>${e.c.name}</strong> → <span style="color:var(--lumen)">${CREATURES[e.target].name}</span>
        <span class="cb-cost">${ITEMS[e.item]?.name || e.item} ${has ? '✓' : '✗ (need ' + (Game.inventory[e.item] || 0) + ')'}</span>
      </button>`;
    }
    html += '</div>';
    const m = this.openModal(html);
    m.querySelectorAll('[data-evo]').forEach(b => {
      b.addEventListener('click', () => {
        const idx = parseInt(b.dataset.evo);
        const c = Game.party[idx];
        const s = CREATURES[c.species];
        if (Game.inventory[s.evolves.item] && Game.inventory[s.evolves.item] > 0) {
          Game.inventory[s.evolves.item]--;
          this.evolveCreature(idx);
          this.closeModal();
        } else {
          this.showToast('You lack the catalyst.');
        }
      });
    });
  },
  
  openShop() {
    this.closeModal();
    const stock = [
      { id: 'wrappingshard', qty: 3 }, { id: 'wovenlattice', qty: 2 },
      { id: 'mossdrop', qty: 5 }, { id: 'dawnvial', qty: 3 }, { id: 'elixirlume', qty: 1 },
      { id: 'wakebell', qty: 1 }, { id: 'lurepebble', qty: 2 }, { id: 'wardchime', qty: 2 },
      { id: 'embercatalyst', qty: 1 }, { id: 'lumencatalyst', qty: 1 },
      { id: 'tidalcatalyst', qty: 1 }, { id: 'floracatalyst', qty: 1 },
      { id: 'cragcatalyst', qty: 1 }, { id: 'shadecatalyst', qty: 1 },
      { id: 'prismcatalyst', qty: 1 }
    ];
    let html = `<h2>Wandering Merchant</h2>
      <p style="color:var(--gold);font-size:14px;margin-bottom:14px">Gold: ${Game.gold} ◆</p>
      <div class="choice-list">`;
    for (const s of stock) {
      const it = ITEMS[s.id];
      if (!it || !it.price) continue;
      html += `<button class="choice-btn" data-buy="${s.id}">
        <strong>${it.name}</strong> — <span style="color:var(--gold)">${it.price}◆</span>
        <span class="cb-cost">${it.desc}</span>
      </button>`;
    }
    html += '</div>';
    const m = this.openModal(html);
    m.querySelectorAll('[data-buy]').forEach(b => {
      b.addEventListener('click', () => {
        const id = b.dataset.buy;
        const it = ITEMS[id];
        if (Game.gold >= it.price) {
          Game.gold -= it.price;
          Game.inventory[id] = (Game.inventory[id] || 0) + 1;
          Audio.sfx.confirm();
          this.showToast(`Bought ${it.name}.`);
          this.openShop(); // refresh
        } else {
          this.showToast('Not enough gold.');
          Audio.sfx.cancel();
        }
      });
    });
  },
  
  openStarterSelect() {
    this.closeModal();
    const html = `<h2>Choose Your First Companion</h2>
      <p style="color:var(--ink-1);font-size:13px;line-height:1.6">The lantern calls for a partner. Each path sings in a different key.</p>
      <div class="starter-grid" id="starter-grid"></div>`;
    const m = this.openModal(html, true);
    const grid = m.querySelector('#starter-grid');
    for (const id of STARTERS) {
      const s = CREATURES[id];
      const card = document.createElement('div');
      card.className = 'starter-card';
      const canvas = document.createElement('canvas');
      canvas.className = 'sc-art';
      canvas.width = 120; canvas.height = 120;
      card.appendChild(canvas);
      const cctx = canvas.getContext('2d');
      drawCreatureArt(cctx, id, 60, 60, 56);
      const name = document.createElement('div');
      name.className = 'sc-name';
      name.textContent = s.name;
      card.appendChild(name);
      const type = document.createElement('div');
      type.className = 'sc-type';
      type.textContent = s.types.join(' / ');
      card.appendChild(type);
      const lore = document.createElement('div');
      lore.className = 'sc-lore';
      lore.textContent = s.lore;
      card.appendChild(lore);
      card.addEventListener('click', () => {
        const c = makeCreature(id, 5);
        Game.party = [c];
        Game.seen.add(id);
        Game.caught.add(id);
        initInventory();
        saveGame();
        Game.state = 'tutorial';
        this.closeModal();
        startTutorial();
      });
      grid.appendChild(card);
    }
  }
};


// ---------- Exploration Logic ----------
const Exploration = {
  encounterSteps: 0,
  
  tryMove(dx, dy) {
    if (Game.state !== 'exploration' || Game.menuOpen || Game.paused) return;
    const m = Game.zoneMap;
    const nx = Game.playerX + dx;
    const ny = Game.playerY + dy;
    if (nx < 0 || ny < 0 || nx >= m.w || ny >= m.h) return;
    const t = m.tiles[ny][nx];
    if (t === 8 || t === 3 || t === 4 || t === 2) return; // walls/blocks/water
    Game.playerX = nx; Game.playerY = ny;
    Game.stepsTaken++;
    Game.playerStep = 6;
    if (dx > 0) Game.playerDir = 'right';
    else if (dx < 0) Game.playerDir = 'left';
    else if (dy < 0) Game.playerDir = 'up';
    else if (dy > 0) Game.playerDir = 'down';
    Audio.sfx.step();
    
    // Check tile
    if (t === 5) {
      // Shrine
      UI.openShrine();
      return;
    } else if (t === 6) {
      // Anchor
      this.startAnchor();
      return;
    } else if (t === 9) {
      // Portal
      this.openPortal();
      return;
    }
    
    // Random encounter
    const z = ZONES[Game.currentZone];
    if (t === 1 || t === 0 || t === 7) {
      // Lower rate on path
      const rate = z.encounterRate * (t === 0 ? 0.5 : 1);
      this.encounterSteps++;
      if (this.encounterSteps >= 8 && Math.random() < rate) {
        this.encounterSteps = 0;
        this.startWildEncounter();
      }
    }
  },
  
  startWildEncounter() {
    const z = ZONES[Game.currentZone];
    // Pick a creature
    const total = z.encounterPool.reduce((s, p) => s + p.w, 0);
    let r = Math.random() * total;
    let species = z.encounterPool[0].c;
    for (const p of z.encounterPool) {
      r -= p.w;
      if (r <= 0) { species = p.c; break; }
    }
    let lvl = randInt(z.levelRange[0], z.levelRange[1]);
    // Post-game: harder spawns (higher levels)
    if (Game.postGame) {
      lvl = Math.min(MAX_LEVEL, lvl + 8 + Math.floor(Math.random() * 5));
    }
    const isFirst = Game.totalEncounters === 0;
    Game.state = 'battle';
    Battle.start({ enemies: [{ species, level: lvl, isAnchor: false }] });
    showBattleUI();
    Game.totalEncounters++;
    if (isFirst) {
      setTimeout(() => {
        UI.showDialogue('Wisp', 'A wild creature appears! Press 1 to choose Fight, or click the button. Pick an ability. The stronger its type advantage, the more it hurts.', () => {
          // do nothing, battle continues
        });
      }, 1500);
    }
  },
  
  startAnchor() {
    const z = ZONES[Game.currentZone];
    if (!z.anchor) return;
    if (Game.defeatedAnchors.has(`${Game.currentZone}_${z.anchor.c}_${z.anchor.level}`)) {
      UI.showToast('The anchor has been quieted. The path is open.');
      return;
    }
    UI.showDialogue('The Anchor', z.anchor.name + ' stirs. The hollows gather close.', () => {
      Game.state = 'battle';
      Battle.start({ enemies: [{ species: z.anchor.c, level: z.anchor.level, isAnchor: true }] });
      showBattleUI();
      Game.totalEncounters++;
    });
  },
  
  openPortal() {
    // Build travel choices
    const unlocked = [];
    for (let i = 0; i < ZONES.length; i++) {
      if (i < Game.unlockedZones) unlocked.push(i);
    }
    let html = `<h2>Path of the Hollow</h2>
      <p style="color:var(--ink-1);font-size:13px;line-height:1.6">A small rift in the air, humming with a color you've seen before.</p>
      <div class="choice-list" style="margin-top:14px">`;
    for (const idx of unlocked) {
      const z = ZONES[idx];
      const cur = idx === Game.currentZone;
      html += `<button class="choice-btn" data-zone="${idx}" ${cur?'disabled style="opacity:.5"':''}>
        <strong style="color:${z.accent}">${z.name}</strong>
        <span class="cb-cost">${cur ? 'Current zone' : z.tagline}</span>
      </button>`;
    }
    html += `</div>`;
    const m = UI.openModal(html);
    m.querySelectorAll('[data-zone]').forEach(b => {
      b.addEventListener('click', () => {
        const idx = parseInt(b.dataset.zone);
        if (idx === Game.currentZone) return;
        Game.currentZone = idx;
        Game.zoneMap = buildZoneMap(idx);
        Game.playerX = Game.zoneMap.startX;
        Game.playerY = Game.zoneMap.startY;
        Exploration.encounterSteps = 0;
        updateHUD();
        saveGame();
        UI.closeModal();
        Audio.sfx.confirm();
        UI.showToast(`Arrived at ${ZONES[idx].name}.`);
      });
    });
  }
};

// ---------- Battle UI ----------
function showBattleUI() {
  // Hide exploration HUD, show battle
  document.getElementById('hud')?.style.setProperty('display', 'none');
  document.getElementById('touch-controls')?.style.setProperty('display', 'none');
  UI.closeModal();
  // Build battle UI
  const ui = document.getElementById('ui-layer');
  let battle = document.getElementById('battle-screen');
  if (!battle) {
    battle = document.createElement('div');
    battle.id = 'battle-screen';
    battle.innerHTML = `<div class="battle-bottom">
      <div id="battle-info"></div>
      <div id="battle-menu-area"></div>
    </div>`;
    ui.appendChild(battle);
  }
  battle.classList.add('show');
  renderBattleUI();
}

function hideBattleUI() {
  const b = document.getElementById('battle-screen');
  if (b) b.classList.remove('show');
  document.getElementById('hud')?.style.removeProperty('display');
  if ('ontouchstart' in window) document.getElementById('touch-controls')?.style.removeProperty('display');
}

function renderBattleUI() {
  const info = document.getElementById('battle-info');
  if (!info) return;
  // Enemy info
  const e = Battle.enemies[0];
  if (e) {
    info.innerHTML = `
      <div class="battle-enemy-info">
        <div class="battle-name"><strong>${e.name}</strong><span class="battle-level">Lv${e.level}</span></div>
        <div class="hp-bar-wrap"><div class="hp-bar ${e.hp/e.maxHp<0.2?'critical':e.hp/e.maxHp<0.5?'low':''}" style="width:${(e.hp/e.maxHp*100)}%"></div></div>
        <div class="hp-text">${Math.floor(e.hp)} / ${e.maxHp}</div>
      </div>
      <div class="battle-party-info" id="party-info"></div>
    `;
    const pi = info.querySelector('#party-info');
    Battle.party.forEach((c, i) => {
      const row = document.createElement('div');
      row.style.cssText = 'display:flex;align-items:center;gap:8px;width:100%;max-width:280px;justify-content:flex-end;cursor:pointer';
      row.innerHTML = `
        <span style="font-size:12px;${i===Battle.activeIdx?'color:var(--lumen)':'color:var(--ink-1)'};min-width:80px;text-align:right">${c.name} Lv${c.level}</span>
        <div class="hp-bar-wrap" style="width:120px"><div class="hp-bar ${c.hp/c.maxHp<0.2?'critical':c.hp/c.maxHp<0.5?'low':''}" style="width:${(c.hp/c.maxHp*100)}%"></div></div>
      `;
      row.addEventListener('click', () => switchParty(i));
      pi.appendChild(row);
    });
  }
  // Menu
  const menu = document.getElementById('battle-menu-area');
  renderBattleMenu(menu);
}

function renderBattleMenu(menu) {
  if (!menu) return;
  if (Battle.state === 'menu') {
    menu.innerHTML = `<div class="battle-menu">
      <button class="battle-btn" data-act="fight">⚔ Fight</button>
      <button class="battle-btn" data-act="bag">🎒 Bag</button>
      <button class="battle-btn" data-act="party">✦ Party</button>
      <button class="battle-btn" data-act="run">⤴ Run</button>
    </div>`;
    menu.querySelectorAll('[data-act]').forEach(b => {
      b.addEventListener('click', () => Battle.playerAction(b.dataset.act));
    });
  } else if (Battle.state === 'ability') {
    const p = Battle.party[Battle.activeIdx];
    const abilities = p.abilityIds;
    menu.innerHTML = `<div class="battle-menu" style="grid-template-columns:repeat(2,1fr);max-width:none">
      ${abilities.map((ab, i) => {
        const a = ABILITIES[ab];
        const cur = p.pp?.[ab] ?? 0;
        const max = a.pp;
        const noPP = cur <= 0;
        return `<button class="battle-btn" data-ab="${ab}" ${noPP?'disabled':''}>${a.name}<span class="pp">PWR ${a.power} · ${cur}/${max}</span></button>`;
      }).join('')}
      <button class="battle-btn" data-ab="back" style="grid-column:span 2;background:rgba(255,90,122,.1)">← Back</button>
    </div>`;
    menu.querySelectorAll('[data-ab]').forEach(b => {
      b.addEventListener('click', () => {
        if (b.dataset.ab === 'back') { Battle.state = 'menu'; renderBattleMenu(menu); }
        else Battle.useAbility(b.dataset.ab);
      });
    });
  } else if (Battle.state === 'bag') {
    const items = Object.entries(Game.inventory).filter(([k, v]) => v > 0 && (ITEMS[k]?.type === 'collection' || ITEMS[k]?.type === 'heal' || ITEMS[k]?.type === 'revive'));
    menu.innerHTML = `<div class="battle-menu" style="grid-template-columns:1fr;max-width:280px;margin:0 auto">
      ${items.length === 0 ? '<div style="color:var(--ink-2);text-align:center;padding:12px">No usable items.</div>' : ''}
      ${items.map(([id, qty]) => {
        const it = ITEMS[id];
        return `<button class="battle-btn" data-item="${id}">${it.name}<span class="pp">×${qty}</span></button>`;
      }).join('')}
      <button class="battle-btn" data-item="back" style="background:rgba(255,90,122,.1)">← Back</button>
    </div>`;
    menu.querySelectorAll('[data-item]').forEach(b => {
      b.addEventListener('click', () => {
        if (b.dataset.item === 'back') { Battle.state = 'menu'; renderBattleMenu(menu); }
        else Battle.useItem(b.dataset.item);
      });
    });
  } else if (Battle.state === 'party') {
    menu.innerHTML = `<div class="battle-menu" style="grid-template-columns:1fr;max-width:340px;margin:0 auto">
      ${Battle.party.map((c, i) => `
        <button class="battle-btn" data-p="${i}" ${c.hp<=0?'disabled':''} ${i===Battle.activeIdx?'style="border-color:var(--lumen)"':''}>
          ${c.name} Lv${c.level} — ${Math.floor(c.hp)}/${c.maxHp} HP ${i===Battle.activeIdx?'(active)':''}
        </button>
      `).join('')}
      <button class="battle-btn" data-p="back" style="background:rgba(255,90,122,.1)">← Back</button>
    </div>`;
    menu.querySelectorAll('[data-p]').forEach(b => {
      b.addEventListener('click', () => {
        if (b.dataset.p === 'back') { Battle.state = 'menu'; renderBattleMenu(menu); }
        else Battle.switchParty(parseInt(b.dataset.p));
      });
    });
  } else {
    // Anim/message - hide menu
    menu.innerHTML = '';
  }
}

function switchParty(idx) {
  if (Battle.state !== 'menu') return;
  if (idx === Battle.activeIdx) return;
  if (Battle.party[idx].hp <= 0) return;
  Battle.activeIdx = idx;
  Battle.addLog(`Switched to ${Battle.party[idx].name}!`);
  Battle.afterAction();
  renderBattleUI();
}

function updateBattleUI() {
  renderBattleUI();
  // Floating message
  if (Battle.message) {
    let msgEl = document.getElementById('battle-message');
    if (!msgEl) {
      msgEl = document.createElement('div');
      msgEl.id = 'battle-message';
      document.getElementById('ui-layer').appendChild(msgEl);
    }
    msgEl.innerHTML = Battle.message;
  }
}

// ---------- HUD ----------
function buildHUD() {
  if (document.getElementById('hud')) {
    updateHUD();
    return;
  }
  const ui = document.getElementById('ui-layer');
  ui.innerHTML = `
    <div id="hud">
      <div class="hud-zone">Zone ${Game.currentZone+1}<strong>${ZONES[Game.currentZone]?.name || 'Unknown'}</strong></div>
      <div class="hud-right">
        <button class="hud-btn tooltip" data-tip="Party" id="btn-party">✦</button>
        <button class="hud-btn tooltip" data-tip="Bag" id="btn-bag">▣</button>
        <button class="hud-btn tooltip" data-tip="Bestiary" id="btn-bestiary">◉</button>
        <button class="hud-btn tooltip" data-tip="Map" id="btn-map">◈</button>
        <button class="hud-btn tooltip" data-tip="Save" id="btn-save">⊕</button>
        <button class="hud-btn tooltip" data-tip="Sound" id="btn-sound">♪</button>
        <button class="hud-btn tooltip" data-tip="Menu" id="btn-menu">≡</button>
      </div>
    </div>
    <div id="touch-controls">
      <div class="touch-pad">
        <div class="touch-btn up" data-dir="up">▲</div>
        <div class="touch-btn down" data-dir="down">▼</div>
        <div class="touch-btn left" data-dir="left">◀</div>
        <div class="touch-btn right" data-dir="right">▶</div>
      </div>
    </div>
  `;
  document.getElementById('btn-party').addEventListener('click', () => UI.openPartyMenu());
  document.getElementById('btn-bag').addEventListener('click', () => UI.openBagMenu());
  document.getElementById('btn-bestiary').addEventListener('click', () => UI.openBestiary());
  document.getElementById('btn-map').addEventListener('click', () => UI.openMap());
  document.getElementById('btn-save').addEventListener('click', () => { saveGame(); UI.showToast('Progress saved.'); Audio.sfx.confirm(); });
  document.getElementById('btn-sound').addEventListener('click', () => {
    Game.muted = !Game.muted;
    Audio.setMuted(Game.muted);
    document.getElementById('btn-sound').textContent = Game.muted ? '✕' : '♪';
    document.getElementById('btn-sound').style.color = Game.muted ? 'var(--bad)' : '';
    Audio.sfx.menu();
  });
  document.getElementById('btn-menu').addEventListener('click', () => {
    if (UI.currentMenu === 'pause') UI.closeModal();
    else openPauseMenu();
  });
  document.querySelectorAll('.touch-btn').forEach(b => {
    b.addEventListener('touchstart', e => { e.preventDefault(); const d = b.dataset.dir; moveFromDir(d); });
    b.addEventListener('mousedown', e => { e.preventDefault(); const d = b.dataset.dir; moveFromDir(d); });
  });
}

function openPauseMenu() {
  UI.currentMenu = 'pause';
  const html = `<h2>Menu</h2>
    <div class="choice-list">
      <button class="choice-btn" data-pm="resume">Resume
        <span class="cb-cost">Close this menu</span>
      </button>
      <button class="choice-btn" data-pm="save">Save
        <span class="cb-cost">Write your progress</span>
      </button>
      <button class="choice-btn" data-pm="load">Title Screen
        <span class="cb-cost">Return to title (saves automatically)</span>
      </button>
      <button class="choice-btn" data-pm="erase">Erase Save
        <span class="cb-cost">Start a new binding</span>
      </button>
    </div>`;
  const m = UI.openModal(html);
  m.querySelectorAll('[data-pm]').forEach(b => {
    b.addEventListener('click', () => {
      const act = b.dataset.pm;
      if (act === 'resume') { UI.closeModal(); }
      else if (act === 'save') { saveGame(); UI.showToast('Saved.'); Audio.sfx.confirm(); UI.closeModal(); }
      else if (act === 'load') { saveGame(); UI.closeModal(); showTitle(); }
      else if (act === 'erase') {
        if (confirm('Erase your save? This cannot be undone.')) {
          deleteSave();
          UI.closeModal();
          showTitle();
        }
      }
    });
  });
}

function moveFromDir(d) {
  if (d === 'up') Exploration.tryMove(0, -1);
  else if (d === 'down') Exploration.tryMove(0, 1);
  else if (d === 'left') Exploration.tryMove(-1, 0);
  else if (d === 'right') Exploration.tryMove(1, 0);
}

function updateHUD() {
  const z = ZONES[Game.currentZone];
  const hudZone = document.querySelector('.hud-zone');
  if (hudZone && z) {
    hudZone.innerHTML = `Zone ${Game.currentZone+1}<strong>${z.name}</strong>`;
  }
}

// ---------- Tutorial ----------
function startTutorial() {
  Game.state = 'tutorial';
  Game.tutorialStep = 0;
  buildHUD();
  Game.zoneMap = buildZoneMap(0);
  Game.playerX = Game.zoneMap.startX;
  Game.playerY = Game.zoneMap.startY;
  Audio.playMusic('overworld');
  UI.showDialogue('Wisp', 'Welcome to Aethermere, Binder. The lantern hears you, but the world is still asleep. Try moving with the WASD or arrow keys.', () => {
    Game.tutorialStep = 1;
    Game.state = 'exploration';
  });
}



// ---------- Title Screen ----------
function showTitle() {
  Game.state = 'title';
  Game.hasSave = hasSave();
  document.getElementById('ui-layer').innerHTML = `
    <div id="title-screen">
      <div id="title-content">
        <div class="title-mark">AETHERMERE</div>
        <div class="title-sub">Whispers of the Hollow</div>
        <div class="title-tagline">A world broken into color. Six zones. Four anchors. One lantern in your palm.</div>
        <button class="title-btn primary" id="btn-new">✦ New Binding</button>
        ${Game.hasSave ? '<button class="title-btn" id="btn-cont">◈ Continue</button>' : ''}
        <button class="title-btn" id="btn-erase" style="font-size:11px;opacity:.5;margin-top:6px">Erase Save</button>
      </div>
      <div class="title-credits">v1.0 · Original creation · No external assets</div>
    </div>
  `;
  Audio.stopMusic();
  Audio.playMusic('title');
  document.getElementById('btn-new').addEventListener('click', () => {
    Audio.resume();
    Audio.sfx.confirm();
    UI.openStarterSelect();
  });
  if (Game.hasSave) {
    document.getElementById('btn-cont').addEventListener('click', () => {
      Audio.resume();
      Audio.sfx.confirm();
      const data = loadGame();
      if (data) {
        Game.zoneMap = buildZoneMap(Game.currentZone);
        // Validate player position
        const m = Game.zoneMap;
        if (Game.playerX < 0 || Game.playerY < 0 || Game.playerX >= m.w || Game.playerY >= m.h) {
          Game.playerX = m.startX; Game.playerY = m.startY;
        }
        const t = m.tiles[Game.playerY] && m.tiles[Game.playerX] ? m.tiles[Game.playerY][Game.playerX] : null;
        if (t === null || t === undefined || t === 8 || t === 3 || t === 4 || t === 2) {
          Game.playerX = m.startX; Game.playerY = m.startY;
        }
        // Ensure all creatures have pp
        for (const c of Game.party) {
          if (!c) continue;
          if (!c.pp) c.pp = {};
          for (const ab of c.abilityIds) {
            if (c.pp[ab] == null) c.pp[ab] = ABILITIES[ab]?.pp ?? 10;
          }
        }
        buildHUD();
        Game.state = 'exploration';
        Audio.playMusic('overworld');
        UI.showToast('Welcome back, Binder.');
      }
    });
  }
  document.getElementById('btn-erase')?.addEventListener('click', () => {
    if (confirm('Erase your save? This cannot be undone.')) {
      deleteSave();
      showTitle();
    }
  });
}

// ---------- Input ----------
const keys = {};
window.addEventListener('keydown', e => {
  const k = e.key.toLowerCase();
  keys[k] = true;
  if (Game.state === 'exploration' || Game.state === 'tutorial') {
    if (k === 'w' || k === 'arrowup') Exploration.tryMove(0, -1);
    else if (k === 's' || k === 'arrowdown') Exploration.tryMove(0, 1);
    else if (k === 'a' || k === 'arrowleft') Exploration.tryMove(-1, 0);
    else if (k === 'd' || k === 'arrowright') Exploration.tryMove(1, 0);
    else if (k === 'e') {
      // Interact with tile
      const m = Game.zoneMap;
      const t = m.tiles[Game.playerY][Game.playerX];
      if (t === 5) UI.openShrine();
      else if (t === 6) Exploration.startAnchor();
      else if (t === 9) Exploration.openPortal();
    }
    else if (k === 'escape') {
      if (UI.currentMenu === 'pause') UI.closeModal();
      else openPauseMenu();
    }
  } else if (Game.state === 'battle' || Battle.active) {
    if (k === '1') Battle.playerAction('fight');
    else if (k === '2') Battle.playerAction('bag');
    else if (k === '3') Battle.playerAction('party');
    else if (k === '4') Battle.playerAction('run');
    else if (k === 'escape') {
      if (Battle.state === 'ability') Battle.state = 'menu';
      else if (Battle.state === 'bag') Battle.state = 'menu';
      else if (Battle.state === 'party') Battle.state = 'menu';
      renderBattleUI();
    }
  } else if (k === 'enter' || k === ' ') {
    if (Game.state === 'title') {
      document.getElementById('btn-new')?.click();
    }
  }
});
window.addEventListener('keyup', e => { keys[e.key.toLowerCase()] = false; });

// ---------- Main Loop ----------
let lastTime = 0;
let uiUpdateAccum = 0;

function loop(time) {
  const dt = Math.min(0.05, (time - lastTime) / 1000 || 0);
  lastTime = time;
  
  // Update
  if (Game.state === 'exploration' || Game.state === 'tutorial') {
    if (Game.playerStep > 0) Game.playerStep -= dt * 30;
  }
  if (Game.state === 'battle' && Battle.active) {
    if (Battle.shake > 0) Battle.shake = Math.max(0, Battle.shake - dt * 30);
    // Render UI less frequently to avoid flicker
  }
  updateParticles(dt);
  
  // Draw
  ctx.save();
  let shakeX = 0, shakeY = 0;
  if (Game.state === 'battle' && Battle.shake > 0) {
    shakeX = (Math.random() - 0.5) * Battle.shake;
    shakeY = (Math.random() - 0.5) * Battle.shake;
  }
  ctx.translate(shakeX, shakeY);
  
  if (Game.state === 'title') {
    // Animated bg
    const t = time / 1000;
    const grad = ctx.createLinearGradient(0, 0, 0, viewH);
    grad.addColorStop(0, `hsl(${(t*10)%360}, 30%, 15%)`);
    grad.addColorStop(1, '#07060f');
    ctx.fillStyle = grad;
    ctx.fillRect(0, 0, viewW, viewH);
    // Stars
    for (let i = 0; i < 80; i++) {
      const x = (i * 137.5 + t * 5) % viewW;
      const y = (i * 71.3) % viewH;
      const s = (Math.sin(t + i) * 0.5 + 0.5) * 2;
      ctx.fillStyle = `rgba(255,255,255,${0.4 + Math.sin(t*2 + i)*0.3})`;
      ctx.beginPath();
      ctx.arc(x, y, s, 0, Math.PI*2);
      ctx.fill();
    }
  } else if (Game.state === 'battle' && Battle.active) {
    drawBattle(dt);
  } else if (Game.state === 'exploration' || Game.state === 'tutorial') {
    drawOverworld(dt);
  }
  ctx.restore();
  
  // Update battle UI periodically
  if (Game.state === 'battle' && Battle.active) {
    uiUpdateAccum += dt;
    if (uiUpdateAccum > 0.15) {
      uiUpdateAccum = 0;
      updateBattleUI();
    }
  }
  
  requestAnimationFrame(loop);
}

// ---------- Init ----------
function init() {
  resize();
  Audio.init();
  showTitle();
  requestAnimationFrame(loop);
}

// Resize handling
window.addEventListener('orientationchange', () => setTimeout(resize, 100));
window.addEventListener('resize', resize);

// First user gesture to enable audio
const enableAudio = () => {
  Audio.resume();
  window.removeEventListener('click', enableAudio);
  window.removeEventListener('touchstart', enableAudio);
  window.removeEventListener('keydown', enableAudio);
};
window.addEventListener('click', enableAudio);
window.addEventListener('touchstart', enableAudio);
window.addEventListener('keydown', enableAudio);

// Start
init();
</script>
</body>
</html>
<!-- agent-meta {"model":"minimax/minimax-m3","provider":"openrouter","persona":"middle","sessionId":"009aa85d-fae4-4d0d-9021-9d1ac26253b8","tokensIn":6168736,"tokensOut":95858,"tokensTotal":6264594,"turns":80,"toolCalls":79,"failedToolCalls":1,"timestamp":"2026-07-12T21:22:14.341Z"} -->