← Aethelgard results

Aethelgard

Minimax m3 · composer

Initial view of Aethelgard

Reviews & generation details

Reviews

No human review yet. Unreviewed does not mean unsuccessful.

Generation context

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

Generation prompt · exact text unavailable

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

Inspect original source 145,228 bytes · SHA-256 0fb76c0cb1f5
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Aether Resonance — Conductors of Aethelgard</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&amp;family=Press+Start+2P&amp;display=swap">
<style>
/* ── css: core ── */
:root {
  --bg-0: #0a0612;
  --bg-1: #120822;
  --bg-2: #1c0f33;
  --ink: #f4f1ff;
  --ink-dim: #a89dc9;
  --ink-mute: #6e6390;
  --line: rgba(180, 160, 230, 0.12);
  --line-strong: rgba(200, 180, 255, 0.28);
  --glass: rgba(30, 18, 56, 0.55);
  --glass-2: rgba(20, 10, 40, 0.7);
  --pyro: #ff6a3d;
  --pyro-2: #ffb27a;
  --hydro: #4cc4ff;
  --hydro-2: #8ee3ff;
  --flora: #6cff8e;
  --flora-2: #c2ff9b;
  --volt: #ffe04a;
  --volt-2: #fff7a3;
  --zephyr: #c8b6ff;
  --zephyr-2: #e3d8ff;
  --earth: #c98b5b;
  --earth-2: #e6b58a;
  --cosmos: #d6a8ff;
  --cosmos-2: #f1d8ff;
  --good: #58e398;
  --bad: #ff6e7a;
}
* { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
html, body {
  margin: 0; padding: 0;
  height: 100%; width: 100%;
  background: radial-gradient(ellipse at top, #1d0d3b 0%, #0a0612 65%);
  color: var(--ink);
  font-family: 'Inter', system-ui, sans-serif;
  font-weight: 500;
  overflow: hidden;
  user-select: none;
}
body::before {
  content: '';
  position: fixed; inset: 0;
  background-image:
    radial-gradient(circle at 15% 25%, rgba(140, 80, 220, 0.18), transparent 40%),
    radial-gradient(circle at 85% 75%, rgba(80, 180, 220, 0.12), transparent 45%),
    radial-gradient(circle at 50% 50%, rgba(220, 100, 200, 0.06), transparent 55%);
  pointer-events: none; z-index: 0;
}
#app {
  position: relative; z-index: 1;
  width: 100%; height: 100vh;
  display: flex; align-items: center; justify-content: center;
  padding: 16px;
}
.screen {
  width: 100%; max-width: 1100px; height: 100%;
  display: flex; flex-direction: column;
  animation: fadeIn 0.4s ease;
  overflow: hidden;
}
.screen.hidden { display: none !important; }
@keyframes fadeIn {
  from { opacity: 0; transform: translateY(8px); }
  to { opacity: 1; transform: translateY(0); }
}
@keyframes fadeOut {
  from { opacity: 1; }
  to { opacity: 0; }
}
@keyframes pulse-glow {
  0%, 100% { box-shadow: 0 0 24px var(--glow), inset 0 0 12px rgba(255,255,255,0.05); }
  50% { box-shadow: 0 0 48px var(--glow), inset 0 0 20px rgba(255,255,255,0.1); }
}
@keyframes bounce-tiny {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(-6px); }
}
@keyframes float-y {
  0%, 100% { transform: translateY(0) rotate(0); }
  50% { transform: translateY(-10px) rotate(1deg); }
}
@keyframes shake {
  0%, 100% { transform: translate(0, 0); }
  20% { transform: translate(-8px, 4px); }
  40% { transform: translate(8px, -4px); }
  60% { transform: translate(-6px, 6px); }
  80% { transform: translate(6px, -2px); }
}
@keyframes flash {
  0% { background-color: rgba(255,255,255,0); }
  50% { background-color: rgba(255,255,255,0.7); }
  100% { background-color: rgba(255,255,255,0); }
}
@keyframes slide-up {
  from { transform: translateY(30px); opacity: 0; }
  to { transform: translateY(0); opacity: 1; }
}
@keyframes drift {
  0% { transform: translateY(0) translateX(0); opacity: 0; }
  10% { opacity: 0.8; }
  90% { opacity: 0.6; }
  100% { transform: translateY(-120px) translateX(20px); opacity: 0; }
}
@keyframes spin-slow {
  to { transform: rotate(360deg); }
}
@keyframes flicker {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.85; }
}
.font-pixel { font-family: 'Press Start 2P', monospace; letter-spacing: 0.5px; }
.text-pixel { font-family: 'Press Start 2P', monospace; font-size: 10px; letter-spacing: 0.5px; line-height: 1.7; }
.text-pixel-sm { font-family: 'Press Start 2P', monospace; font-size: 8px; letter-spacing: 0.5px; line-height: 1.7; }
.glass {
  background: var(--glass);
  backdrop-filter: blur(22px) saturate(140%);
  -webkit-backdrop-filter: blur(22px) saturate(140%);
  border: 1px solid var(--line-strong);
  border-radius: 18px;
  box-shadow: 0 12px 40px rgba(0,0,0,0.45), inset 0 1px 0 rgba(255,255,255,0.05);
}
.glass-deep {
  background: var(--glass-2);
  backdrop-filter: blur(26px) saturate(150%);
  border: 1px solid var(--line-strong);
  border-radius: 18px;
}
.toast-layer {
  position: fixed; top: 24px; left: 50%; transform: translateX(-50%);
  z-index: 9999; pointer-events: none;
  display: flex; flex-direction: column; gap: 8px; align-items: center;
}
.toast {
  padding: 10px 18px;
  background: rgba(20, 10, 40, 0.85);
  backdrop-filter: blur(12px);
  border: 1px solid var(--line-strong);
  border-radius: 999px;
  color: var(--ink);
  font-size: 13px; font-weight: 600;
  animation: slide-up 0.3s ease, fadeOut 0.4s ease 2.4s forwards;
  box-shadow: 0 8px 24px rgba(0,0,0,0.4);
}
.fx-layer {
  position: fixed; inset: 0; pointer-events: none; z-index: 500;
}
.fx-shake { animation: shake 0.45s ease; }
.fx-flash {
  position: fixed; inset: 0; pointer-events: none; z-index: 400;
  background: white; opacity: 0;
  animation: flash 0.35s ease;
}
.particle {
  position: absolute;
  width: 6px; height: 6px;
  border-radius: 50%;
  pointer-events: none;
  animation: drift 1.6s linear forwards;
}
.btn {
  position: relative;
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  padding: 12px 22px;
  background: linear-gradient(180deg, rgba(80, 50, 140, 0.55), rgba(50, 25, 100, 0.55));
  border: 1px solid var(--line-strong);
  border-radius: 12px;
  color: var(--ink); font-weight: 600; font-size: 14px;
  cursor: pointer;
  transition: transform 0.15s ease, box-shadow 0.2s ease, background 0.2s ease;
  box-shadow: 0 6px 18px rgba(0,0,0,0.4), inset 0 1px 0 rgba(255,255,255,0.08);
  font-family: 'Inter', sans-serif;
}
.btn:hover { transform: translateY(-2px); box-shadow: 0 10px 28px rgba(140, 100, 230, 0.4); background: linear-gradient(180deg, rgba(100, 60, 170, 0.7), rgba(60, 30, 120, 0.7)); }
.btn:active { transform: translateY(0); }
.btn:disabled { opacity: 0.45; cursor: not-allowed; transform: none !important; }
.btn-primary {
  background: linear-gradient(180deg, #7c4dff, #5028c8);
  border-color: rgba(180, 140, 255, 0.5);
}
.btn-primary:hover { background: linear-gradient(180deg, #8e5dff, #5e2eda); }
.btn-danger {
  background: linear-gradient(180deg, #d24a6a, #8e2444);
  border-color: rgba(255, 140, 170, 0.5);
}
.btn-success {
  background: linear-gradient(180deg, #4acb8a, #1f8a5c);
  border-color: rgba(140, 255, 200, 0.5);
}
.btn-sm { padding: 8px 14px; font-size: 12px; border-radius: 9px; }
.btn-icon { width: 38px; height: 38px; padding: 0; border-radius: 10px; }
.chip {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 4px 10px;
  border-radius: 999px;
  font-size: 10px; font-weight: 700;
  text-transform: uppercase; letter-spacing: 1px;
  border: 1px solid currentColor;
}
.scrollbox {
  overflow-y: auto; overflow-x: hidden;
  scrollbar-width: thin; scrollbar-color: rgba(180,160,230,0.3) transparent;
}
.scrollbox::-webkit-scrollbar { width: 6px; }
.scrollbox::-webkit-scrollbar-thumb { background: rgba(180,160,230,0.3); border-radius: 3px; }
.scrollbox::-webkit-scrollbar-track { background: transparent; }
.tag { display: inline-block; padding: 2px 8px; border-radius: 6px; font-size: 10px; font-weight: 700; letter-spacing: 0.5px; text-transform: uppercase; }
.hp-track { width: 100%; height: 10px; background: rgba(0,0,0,0.5); border-radius: 6px; overflow: hidden; border: 1px solid rgba(255,255,255,0.08); }
.hp-fill { height: 100%; transition: width 0.5s ease, background-color 0.3s ease; border-radius: 6px; }
.xp-track { width: 100%; height: 6px; background: rgba(0,0,0,0.5); border-radius: 4px; overflow: hidden; border: 1px solid rgba(255,255,255,0.05); }
.xp-fill { height: 100%; background: linear-gradient(90deg, #b48cff, #6bd4ff); transition: width 0.5s ease; }
.divider { height: 1px; background: var(--line); margin: 12px 0; }
.kbd {
  font-family: 'Press Start 2P', monospace; font-size: 9px;
  padding: 3px 6px; border-radius: 5px;
  background: rgba(0,0,0,0.4); border: 1px solid var(--line-strong);
}
/* ── css: screens ── */
/* ELEMENT THEME TOKENS */
.el-pyro { --glow: rgba(255, 106, 61, 0.6); --c1: var(--pyro); --c2: var(--pyro-2); }
.el-hydro { --glow: rgba(76, 196, 255, 0.6); --c1: var(--hydro); --c2: var(--hydro-2); }
.el-flora { --glow: rgba(108, 255, 142, 0.6); --c1: var(--flora); --c2: var(--flora-2); }
.el-volt { --glow: rgba(255, 224, 74, 0.6); --c1: var(--volt); --c2: var(--volt-2); }
.el-zephyr { --glow: rgba(200, 182, 255, 0.6); --c1: var(--zephyr); --c2: var(--zephyr-2); }
.el-earth { --glow: rgba(201, 139, 91, 0.6); --c1: var(--earth); --c2: var(--earth-2); }
.el-cosmos { --glow: rgba(214, 168, 255, 0.6); --c1: var(--cosmos); --c2: var(--cosmos-2); }
.type-pill {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 3px 9px; border-radius: 6px;
  font-family: 'Press Start 2P', monospace;
  font-size: 8px; font-weight: 400;
  letter-spacing: 0.5px;
  text-transform: uppercase;
  background: rgba(0,0,0,0.35);
  border: 1px solid currentColor;
  white-space: nowrap;
}
.type-pill .dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; box-shadow: 0 0 6px currentColor; }
.sprite-svg { display: block; }

/* MENU */
#screen-menu { justify-content: center; align-items: center; padding: 20px; }
.menu-logo-wrap { display: flex; flex-direction: column; align-items: center; gap: 6px; margin-bottom: 28px; animation: float-y 4s ease-in-out infinite; }
.menu-logo { width: 130px; height: 130px; filter: drop-shadow(0 0 32px rgba(180, 100, 255, 0.6)); }
.menu-title {
  font-family: 'Press Start 2P', monospace;
  font-size: 32px;
  background: linear-gradient(180deg, #d4b3ff 0%, #7c4dff 60%, #4a2a9e 100%);
  -webkit-background-clip: text; background-clip: text; color: transparent;
  letter-spacing: 2px;
  text-shadow: 0 0 32px rgba(180,100,255,0.4);
}
.menu-subtitle { font-family: 'Press Start 2P', monospace; font-size: 9px; color: var(--ink-dim); letter-spacing: 3px; }
.menu-buttons { display: flex; flex-direction: column; gap: 12px; min-width: 320px; }
.menu-meta { margin-top: 24px; font-size: 11px; color: var(--ink-mute); text-align: center; font-family: 'Press Start 2P', monospace; }

/* OVERWORLD */
.overworld-shell { height: 100%; display: flex; flex-direction: column; gap: 12px; padding: 8px; min-height: 0; }
.overworld-header { display: flex; align-items: center; justify-content: space-between; padding: 12px 18px; gap: 16px; flex-shrink: 0; }
.overworld-header .stats { display: flex; gap: 18px; align-items: center; }
.overworld-header .stat { display: flex; flex-direction: column; align-items: flex-start; }
.overworld-header .stat .label { font-size: 9px; color: var(--ink-mute); font-family: 'Press Start 2P', monospace; letter-spacing: 1px; text-transform: uppercase; }
.overworld-header .stat .val { font-size: 18px; font-weight: 700; color: var(--ink); }
.overworld-header .stat .val.shards { color: var(--volt); text-shadow: 0 0 12px rgba(255, 224, 74, 0.5); }
.zone-tabs { display: flex; gap: 8px; padding: 4px; overflow-x: auto; flex-shrink: 0; }
.zone-tab {
  flex-shrink: 0;
  padding: 10px 16px;
  border-radius: 10px;
  border: 1px solid var(--line);
  background: rgba(20, 10, 40, 0.5);
  cursor: pointer; transition: all 0.2s ease;
  font-size: 12px; font-weight: 600;
  white-space: nowrap;
}
.zone-tab .zt-name { display: block; font-family: 'Press Start 2P', monospace; font-size: 9px; color: var(--ink-dim); margin-bottom: 4px; }
.zone-tab .zt-range { display: block; font-size: 10px; color: var(--ink-mute); }
.zone-tab.locked { opacity: 0.4; cursor: not-allowed; }
.zone-tab.active { background: linear-gradient(180deg, rgba(124, 77, 255, 0.3), rgba(80, 40, 200, 0.3)); border-color: rgba(180,140,255,0.6); box-shadow: 0 0 16px rgba(124,77,255,0.4); }
.zone-tab.active .zt-name { color: var(--ink); }

.zone-canvas-wrap {
  position: relative;
  flex: 1; min-height: 0;
  border-radius: 18px;
  overflow: hidden;
  background: linear-gradient(180deg, rgba(20, 10, 40, 0.4), rgba(10, 5, 25, 0.6));
  border: 1px solid var(--line);
}
.zone-bg-decor { position: absolute; inset: 0; pointer-events: none; opacity: 0.35; }
.node-trail { position: absolute; inset: 0; width: 100%; height: 100%; }
.node-link { stroke: rgba(180,160,230,0.25); stroke-width: 2; stroke-dasharray: 5 5; fill: none; }
.node-link.complete { stroke: rgba(108, 255, 142, 0.6); stroke-dasharray: 0; stroke-width: 2.5; filter: drop-shadow(0 0 4px rgba(108,255,142,0.6)); }
.node-link.locked { stroke: rgba(120, 100, 160, 0.18); }
.node {
  position: absolute;
  width: 64px; height: 64px;
  transform: translate(-50%, -50%);
  border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  cursor: pointer;
  transition: transform 0.2s ease, box-shadow 0.2s ease;
  border: 2px solid var(--line-strong);
  background: rgba(20,10,40,0.7);
  backdrop-filter: blur(8px);
  font-size: 26px;
  z-index: 2;
}
.node:hover { transform: translate(-50%, -50%) scale(1.12); z-index: 3; }
.node.locked { opacity: 0.45; cursor: not-allowed; }
.node.locked:hover { transform: translate(-50%, -50%); }
.node.complete {
  border-color: var(--flora);
  background: radial-gradient(circle, rgba(108,255,142,0.4), rgba(20,10,40,0.7));
  box-shadow: 0 0 18px rgba(108,255,142,0.5);
}
.node.gate {
  width: 80px; height: 80px;
  border-color: var(--pyro);
  background: radial-gradient(circle, rgba(255,106,61,0.4), rgba(20,10,40,0.8));
  box-shadow: 0 0 24px rgba(255,106,61,0.5);
}
.node.gate.cleared { border-color: var(--flora); background: radial-gradient(circle, rgba(108,255,142,0.4), rgba(20,10,40,0.8)); box-shadow: 0 0 24px rgba(108,255,142,0.5); }
.node.current {
  animation: pulse-glow 1.6s ease-in-out infinite;
  --glow: rgba(255, 224, 74, 0.7);
  border-color: var(--volt);
  background: radial-gradient(circle, rgba(255,224,74,0.5), rgba(20,10,40,0.8));
}
.node-emoji { font-size: 26px; filter: drop-shadow(0 2px 4px rgba(0,0,0,0.6)); }
.node-label {
  position: absolute; top: 68px; left: 50%; transform: translateX(-50%);
  font-size: 9px; color: var(--ink-dim); font-family: 'Press Start 2P', monospace;
  white-space: nowrap; text-align: center; text-shadow: 0 1px 4px rgba(0,0,0,0.8);
  pointer-events: none;
}
.zone-info {
  position: absolute; top: 16px; left: 16px; right: 16px;
  display: flex; align-items: center; justify-content: space-between;
  pointer-events: none;
  z-index: 1;
}
.zone-info .zi-title { font-family: 'Press Start 2P', monospace; font-size: 14px; color: var(--ink); text-shadow: 0 2px 6px rgba(0,0,0,0.8); }
.zone-info .zi-progress { font-family: 'Press Start 2P', monospace; font-size: 9px; color: var(--ink-dim); background: rgba(0,0,0,0.5); padding: 4px 8px; border-radius: 6px; }

.overworld-footer {
  display: flex; gap: 12px; justify-content: center;
  padding: 8px;
  flex-shrink: 0;
}

/* BATTLE */
.battle-shell {
  height: 100%; display: flex; flex-direction: column;
  gap: 12px;
  padding: 8px;
  position: relative;
  min-height: 0;
}
.battle-arena {
  position: relative;
  flex: 1; min-height: 0;
  border-radius: 18px;
  overflow: hidden;
  border: 1px solid var(--line-strong);
  background: linear-gradient(180deg, var(--arena-sky, #1d1840) 0%, var(--arena-floor, #2a1858) 100%);
}
.arena-bg-decor { position: absolute; inset: 0; pointer-events: none; }
.battle-platform-enemy { position: absolute; top: 18%; right: 18%; width: 220px; height: 30px; border-radius: 50%; background: radial-gradient(ellipse, rgba(0,0,0,0.55), transparent 70%); filter: blur(4px); }
.battle-platform-player { position: absolute; bottom: 22%; left: 18%; width: 260px; height: 32px; border-radius: 50%; background: radial-gradient(ellipse, rgba(0,0,0,0.55), transparent 70%); filter: blur(4px); }
.battle-sprite-wrap {
  position: absolute;
  display: flex; align-items: center; justify-content: center;
  pointer-events: none;
  z-index: 1;
}
.battle-sprite-wrap.enemy { top: 6%; right: 8%; }
.battle-sprite-wrap.player { bottom: 12%; left: 8%; }
.battle-sprite {
  display: block;
  width: 160px; height: 160px;
  filter: drop-shadow(0 8px 24px var(--sprite-glow, rgba(0,0,0,0.5)));
  transition: transform 0.3s ease, filter 0.3s ease;
}
.battle-sprite-wrap.enemy .battle-sprite { animation: float-y 2.4s ease-in-out infinite; }
.battle-sprite-wrap.player .battle-sprite { animation: bounce-tiny 0.9s ease-in-out infinite; }
@keyframes lunge { 0% { transform: translateX(0) scale(1); } 40% { transform: translateX(60px) scale(1.1); } 100% { transform: translateX(0) scale(1); } }
.battle-sprite.take-hit { animation: hit-shake 0.5s ease; filter: drop-shadow(0 8px 24px rgba(255,80,80,0.7)) brightness(2); }
@keyframes hit-shake { 0%,100% { transform: translateX(0); } 20% { transform: translateX(-12px); } 40% { transform: translateX(12px); } 60% { transform: translateX(-8px); } 80% { transform: translateX(8px); } }
.battle-sprite.faint { animation: faint-fall 1s ease forwards; opacity: 0; transform: translateY(40px); }
@keyframes faint-fall { to { opacity: 0; transform: translateY(60px) rotate(20deg); } }

.battle-info-enemy {
  position: absolute; top: 8%; left: 6%;
  width: 280px; max-width: 45%;
  padding: 12px 16px;
  border-radius: 12px;
  background: rgba(10, 5, 25, 0.78);
  backdrop-filter: blur(10px);
  border: 1px solid var(--line-strong);
  z-index: 2;
}
.battle-info-player {
  position: absolute; bottom: 8%; right: 6%;
  width: 320px; max-width: 55%;
  padding: 14px 18px;
  border-radius: 12px;
  background: rgba(10, 5, 25, 0.88);
  backdrop-filter: blur(10px);
  border: 1px solid var(--line-strong);
  z-index: 2;
}
.bi-row { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 4px; }
.bi-name { font-family: 'Press Start 2P', monospace; font-size: 13px; font-weight: 400; color: var(--ink); }
.bi-level { font-family: 'Press Start 2P', monospace; font-size: 10px; color: var(--ink-dim); }
.bi-bar-wrap { display: flex; align-items: center; gap: 8px; }
.bi-hp-label { font-family: 'Press Start 2P', monospace; font-size: 8px; color: var(--ink-dim); }
.bi-hp-text { font-family: 'Press Start 2P', monospace; font-size: 9px; color: var(--ink); min-width: 64px; text-align: right; }

.battle-message {
  padding: 14px 18px;
  background: rgba(10, 5, 25, 0.92);
  backdrop-filter: blur(12px);
  border: 1px solid var(--line-strong);
  border-radius: 14px;
  min-height: 70px;
  display: flex; align-items: center; justify-content: space-between; gap: 16px;
  flex-shrink: 0;
}
.battle-message .msg-text { font-size: 14px; font-weight: 600; color: var(--ink); flex: 1; line-height: 1.5; }
.battle-message .msg-text .accent { color: var(--volt); text-shadow: 0 0 8px rgba(255,224,74,0.6); }
.battle-message .msg-text .bad { color: var(--bad); }
.battle-message .msg-text .good { color: var(--good); }
.battle-msg-arrow { font-size: 18px; color: var(--ink-dim); animation: blink 1s ease infinite; }
@keyframes blink { 50% { opacity: 0.2; } }

.battle-actions { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.battle-action-btn {
  padding: 14px;
  border-radius: 12px;
  background: linear-gradient(180deg, rgba(60, 40, 120, 0.6), rgba(30, 15, 80, 0.6));
  border: 1px solid var(--line-strong);
  color: var(--ink);
  cursor: pointer; font-weight: 700; font-size: 14px;
  display: flex; align-items: center; gap: 10px; justify-content: center;
  transition: all 0.15s ease;
  font-family: 'Inter', sans-serif;
}
.battle-action-btn:hover { transform: translateY(-2px); box-shadow: 0 6px 20px rgba(140, 100, 230, 0.4); }
.battle-action-btn .ico { font-size: 18px; }
.battle-action-btn.attack { background: linear-gradient(180deg, rgba(220, 80, 100, 0.4), rgba(140, 40, 70, 0.5)); border-color: rgba(255, 120, 140, 0.5); }
.battle-action-btn.prism { background: linear-gradient(180deg, rgba(80, 180, 255, 0.35), rgba(40, 100, 180, 0.45)); border-color: rgba(140, 200, 255, 0.5); }
.battle-action-btn.swap { background: linear-gradient(180deg, rgba(140, 200, 100, 0.35), rgba(80, 140, 60, 0.45)); border-color: rgba(180, 230, 140, 0.5); }
.battle-action-btn.inv { background: linear-gradient(180deg, rgba(200, 160, 80, 0.35), rgba(140, 100, 40, 0.45)); border-color: rgba(240, 200, 120, 0.5); }
.battle-action-btn.flee { background: linear-gradient(180deg, rgba(160, 100, 200, 0.35), rgba(100, 50, 160, 0.45)); border-color: rgba(200, 140, 240, 0.5); }

.move-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.move-btn {
  padding: 12px;
  border-radius: 10px;
  background: rgba(20, 10, 40, 0.7);
  border: 1px solid var(--line-strong);
  text-align: left;
  cursor: pointer;
  transition: all 0.15s ease;
  font-family: 'Inter', sans-serif;
  color: var(--ink);
}
.move-btn:hover { transform: translateY(-2px); border-color: var(--c1, var(--line-strong)); }
.move-btn .move-name { font-weight: 700; font-size: 13px; margin-bottom: 4px; }
.move-btn .move-meta { display: flex; gap: 8px; font-size: 10px; color: var(--ink-dim); font-family: 'Press Start 2P', monospace; flex-wrap: wrap; align-items: center; }
.move-btn.disabled { opacity: 0.5; cursor: not-allowed; }

.party-swap-grid { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 10px; }
.party-slot {
  padding: 12px;
  border-radius: 12px;
  background: rgba(20, 10, 40, 0.6);
  border: 1px solid var(--line);
  cursor: pointer;
  text-align: center;
  transition: all 0.15s ease;
  color: var(--ink);
  font-family: 'Inter', sans-serif;
}
.party-slot.active { border-color: var(--volt); box-shadow: 0 0 12px rgba(255,224,74,0.4); }
.party-slot.fainted { opacity: 0.4; }
.party-slot:hover:not(.fainted) { transform: translateY(-2px); border-color: var(--line-strong); }

.inv-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px; }
.inv-item {
  padding: 10px;
  border-radius: 10px;
  background: rgba(20, 10, 40, 0.6);
  border: 1px solid var(--line);
  cursor: pointer; transition: all 0.15s ease;
  display: flex; align-items: center; gap: 10px;
  color: var(--ink);
  font-family: 'Inter', sans-serif;
}
.inv-item:hover { border-color: var(--line-strong); transform: translateY(-2px); }
.inv-item .ico { font-size: 22px; }
.inv-item .name { font-weight: 700; font-size: 13px; }
.inv-item .qty { font-size: 11px; color: var(--ink-dim); font-family: 'Press Start 2P', monospace; }
/* ── css: extra ── */
/* DEX */
.dex-shell { display: grid; grid-template-rows: auto 1fr; gap: 16px; height: 100%; padding: 8px; }
.dex-header { display: flex; justify-content: space-between; align-items: center; padding: 8px 16px; }
.dex-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: 12px; padding: 12px; }
.dex-card {
  padding: 14px 10px;
  border-radius: 14px;
  background: rgba(20, 10, 40, 0.6);
  border: 1px solid var(--line);
  text-align: center;
  cursor: pointer;
  transition: all 0.2s ease;
  position: relative;
}
.dex-card:hover { transform: translateY(-3px); border-color: var(--line-strong); }
.dex-card.seen { background: rgba(40, 20, 70, 0.7); }
.dex-card.caught { border-color: var(--flora); background: rgba(30, 50, 40, 0.5); }
.dex-card .dex-sprite { width: 80px; height: 80px; margin: 0 auto; }
.dex-card .dex-num { position: absolute; top: 6px; left: 8px; font-size: 9px; color: var(--ink-mute); font-family: 'Press Start 2P', monospace; }
.dex-card .dex-name { font-size: 12px; font-weight: 700; margin-top: 4px; }
.dex-card .dex-type { font-size: 9px; color: var(--ink-dim); margin-top: 2px; font-family: 'Press Start 2P', monospace; }
.dex-card.unknown .dex-sprite { filter: brightness(0); }
.dex-card.unknown .dex-name { color: var(--ink-mute); }
.dex-card.unknown .dex-type::after { content: '?????'; }

/* SHOP */
.shop-shell { display: grid; grid-template-rows: auto 1fr; gap: 16px; height: 100%; padding: 8px; }
.shop-list { display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px; padding: 12px; overflow-y: auto; }
.shop-item {
  display: flex; align-items: center; gap: 14px;
  padding: 14px;
  border-radius: 14px;
  background: rgba(20, 10, 40, 0.6);
  border: 1px solid var(--line);
  transition: all 0.2s ease;
}
.shop-item:hover { border-color: var(--line-strong); transform: translateY(-2px); }
.shop-item .ico { font-size: 32px; }
.shop-item .info { flex: 1; }
.shop-item .info .name { font-weight: 700; font-size: 14px; }
.shop-item .info .desc { font-size: 11px; color: var(--ink-dim); margin-top: 2px; }
.shop-item .buy-btn { padding: 8px 12px; border-radius: 8px; background: linear-gradient(180deg, #7c4dff, #5028c8); border: 1px solid rgba(180, 140, 255, 0.5); color: white; cursor: pointer; font-weight: 700; font-size: 12px; }
.shop-item .buy-btn:disabled { opacity: 0.5; cursor: not-allowed; }
.shop-item .cost { display: block; font-size: 10px; color: var(--volt); margin-top: 2px; font-family: 'Press Start 2P', monospace; }

/* PARTY */
.party-shell { display: grid; grid-template-rows: auto 1fr; gap: 16px; height: 100%; padding: 8px; }
.party-list { display: flex; flex-direction: column; gap: 12px; padding: 12px; overflow-y: auto; }
.party-row {
  display: grid; grid-template-columns: 90px 1fr auto; gap: 14px; align-items: center;
  padding: 12px;
  border-radius: 14px;
  background: rgba(20, 10, 40, 0.6);
  border: 1px solid var(--line);
}
.party-row .party-sprite { width: 80px; height: 80px; }
.party-row .info .name { font-weight: 700; font-size: 15px; }
.party-row .info .stats { display: flex; gap: 12px; margin-top: 6px; font-size: 10px; color: var(--ink-dim); font-family: 'Press Start 2P', monospace; }
.party-row .info .stats span b { color: var(--ink); font-weight: 400; }
.party-row .info .moves { font-size: 11px; color: var(--ink-dim); margin-top: 4px; }

/* DIALOG / VICTORY / DEFEAT */
.dialog-shell { display: flex; align-items: center; justify-content: center; height: 100%; padding: 8px; }
.dialog-box {
  width: 100%; max-width: 700px;
  padding: 30px;
  border-radius: 20px;
  background: rgba(15, 8, 32, 0.92);
  border: 1px solid var(--line-strong);
  backdrop-filter: blur(20px);
}
.dialog-box .portrait { width: 100px; height: 100px; margin: 0 auto 18px; }
.dialog-box .speaker { font-family: 'Press Start 2P', monospace; font-size: 12px; color: var(--ink-dim); text-align: center; margin-bottom: 12px; }
.dialog-box .body { font-size: 15px; line-height: 1.7; color: var(--ink); text-align: center; margin-bottom: 24px; min-height: 80px; }
.dialog-box .actions { display: flex; justify-content: center; gap: 12px; }
.end-shell { display: flex; align-items: center; justify-content: center; height: 100%; padding: 8px; }
.end-card {
  width: 100%; max-width: 520px;
  padding: 36px;
  border-radius: 20px;
  text-align: center;
}
.end-card .end-title {
  font-family: 'Press Start 2P', monospace;
  font-size: 28px;
  margin-bottom: 16px;
}
.end-card.victory .end-title { color: var(--flora); text-shadow: 0 0 24px rgba(108,255,142,0.6); }
.end-card.defeat .end-title { color: var(--bad); text-shadow: 0 0 24px rgba(255,110,122,0.6); }
.end-card .end-body { font-size: 14px; color: var(--ink-dim); margin-bottom: 24px; line-height: 1.6; }
.end-card .rewards { display: flex; justify-content: center; gap: 18px; margin-bottom: 24px; flex-wrap: wrap; }
.reward-pill { padding: 8px 16px; border-radius: 10px; background: rgba(0,0,0,0.4); border: 1px solid var(--line-strong); font-size: 13px; }

/* SPRITE CENTERING HELPER */
.sprite-frame { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; }

/* STATUS ICONS */
.status-icon { font-size: 13px; }

/* RESPONSIVE */
@media (max-width: 760px) {
  #app { padding: 6px; }
  .menu-title { font-size: 22px; }
  .menu-buttons { min-width: 280px; }
  .battle-info-enemy { width: 220px; padding: 8px 10px; top: 4%; left: 4%; }
  .battle-info-player { width: 250px; padding: 10px 12px; bottom: 4%; right: 4%; }
  .battle-sprite-wrap.enemy { top: 10%; right: 4%; }
  .battle-sprite-wrap.player { bottom: 14%; left: 4%; }
  .battle-sprite { width: 110px; height: 110px; }
  .bi-name { font-size: 10px; }
  .zone-tab { padding: 8px 12px; }
  .zone-tab .zt-name { font-size: 8px; }
  .node { width: 52px; height: 52px; }
  .node.gate { width: 64px; height: 64px; }
  .node-label { font-size: 8px; top: 56px; }
  .dex-grid { grid-template-columns: repeat(auto-fill, minmax(110px, 1fr)); }
}
</style>
</head>
<body>
<div id="app"><div id="screen-menu" class="screen"></div>
<div id="screen-overworld" class="screen hidden"></div>
<div id="screen-battle" class="screen hidden"></div>
<div id="screen-dex" class="screen hidden"></div>
<div id="screen-shop" class="screen hidden"></div>
<div id="screen-party" class="screen hidden"></div>
<div id="screen-dialog" class="screen hidden"></div>
<div id="screen-victory" class="screen hidden"></div>
<div id="screen-defeat" class="screen hidden"></div>
<div id="toast-layer" class="toast-layer"></div>
<div id="fx-layer" class="fx-layer"></div></div>
<script>
'use strict';

// ── class: Synth ──
// Lightweight Web Audio synth — produces retro 8-bit SFX.
class Synth {
  constructor() {
    this.ctx = null;
    this.master = null;
    this.enabled = true;
  }
  init() {
    if (this.ctx) return;
    const AC = window.AudioContext || window.webkitAudioContext;
    if (!AC) { this.enabled = false; return; }
    this.ctx = new AC();
    this.master = this.ctx.createGain();
    this.master.gain.value = 0.25;
    this.master.connect(this.ctx.destination);
  }
  resume() {
    if (this.ctx && this.ctx.state === 'suspended') this.ctx.resume();
  }
  _osc(type, freq, dur, vol, sweep) {
    if (!this.enabled || !this.ctx) return;
    const t = this.ctx.currentTime;
    const o = this.ctx.createOscillator();
    const g = this.ctx.createGain();
    o.type = type;
    o.frequency.setValueAtTime(freq, t);
    if (sweep) o.frequency.exponentialRampToValueAtTime(Math.max(40, freq + sweep), t + dur);
    g.gain.setValueAtTime(0, t);
    g.gain.linearRampToValueAtTime(vol || 0.3, t + 0.01);
    g.gain.exponentialRampToValueAtTime(0.0001, t + dur);
    o.connect(g); g.connect(this.master);
    o.start(t); o.stop(t + dur + 0.05);
  }
  _noise(dur, vol, hp) {
    if (!this.enabled || !this.ctx) return;
    const t = this.ctx.currentTime;
    const buf = this.ctx.createBuffer(1, this.ctx.sampleRate * dur, this.ctx.sampleRate);
    const d = buf.getChannelData(0);
    for (let i = 0; i < d.length; i++) d[i] = (Math.random() * 2 - 1) * (1 - i / d.length);
    const src = this.ctx.createBufferSource(); src.buffer = buf;
    const filt = this.ctx.createBiquadFilter(); filt.type = 'highpass'; filt.frequency.value = hp || 800;
    const g = this.ctx.createGain(); g.gain.value = vol || 0.2;
    src.connect(filt); filt.connect(g); g.connect(this.master);
    src.start(t); src.stop(t + dur);
  }
  hover() { this._osc('square', 700, 0.05, 0.12); }
  select() { this._osc('square', 520, 0.08, 0.2); setTimeout(() => this._osc('square', 780, 0.08, 0.2), 60); }
  cancel() { this._osc('square', 320, 0.12, 0.18, -120); }
  hitPhysical() { this._noise(0.18, 0.32, 400); this._osc('square', 180, 0.08, 0.25, -80); }
  hitElemental() { this._noise(0.32, 0.28, 1200); this._osc('sawtooth', 540, 0.16, 0.22, 320); setTimeout(() => this._osc('sawtooth', 740, 0.14, 0.2, -200), 70); }
  hitSuper() { this._noise(0.45, 0.4, 600); this._osc('sawtooth', 320, 0.18, 0.3, 600); setTimeout(() => this._osc('square', 880, 0.2, 0.25, -300), 90); setTimeout(() => this._osc('triangle', 1100, 0.18, 0.18), 170); }
  levelUp() { [523, 659, 784, 1046].forEach((f, i) => setTimeout(() => this._osc('square', f, 0.14, 0.22), i * 80)); }
  captureSuccess() { [440, 660, 880, 1100, 1320].forEach((f, i) => setTimeout(() => this._osc('triangle', f, 0.16, 0.24), i * 70)); }
  captureFail() { this._osc('sawtooth', 220, 0.4, 0.2, -100); }
  faint() { this._osc('sawtooth', 240, 0.5, 0.22, -180); }
  victory() { [523, 659, 784, 1046, 1318].forEach((f, i) => setTimeout(() => this._osc('triangle', f, 0.18, 0.28), i * 110)); }
  defeat() { [392, 349, 311, 261].forEach((f, i) => setTimeout(() => this._osc('sawtooth', f, 0.32, 0.25), i * 180)); }
  menuConfirm() { this._osc('square', 660, 0.08, 0.22); setTimeout(() => this._osc('square', 990, 0.1, 0.22), 60); }
  click() { this._osc('square', 880, 0.06, 0.18); }
  error() { this._osc('square', 180, 0.18, 0.22); }
  evolve() { [262, 330, 392, 523, 659, 784, 1046].forEach((f, i) => setTimeout(() => this._osc('triangle', f, 0.22, 0.26), i * 90)); }
}

// ── const: synth ──
const synth = new Synth();

// ── const: MOVES ──
// Move database — keyed by id. Each Resonant references its unlocked moves.
const MOVES = {
  // PYRO
  ember:    { name: 'Ember',       type: 'pyro',   power: 35, acc: 95, pp: 25, desc: 'A weak flame strike.' },
  flame_fang: { name: 'Flame Fang', type: 'pyro',  power: 50, acc: 95, pp: 20, desc: 'Biting jaws of fire.' },
  inferno:  { name: 'Inferno',     type: 'pyro',   power: 70, acc: 85, pp: 15, desc: 'Engulfs foe in a wildfire.' },
  pyre_claw: { name: 'Pyre Claw',  type: 'pyro',   power: 55, acc: 100,pp: 15, desc: 'A claw coated in molten heat.' },
  eruption: { name: 'Eruption',    type: 'pyro',   power: 95, acc: 75, pp: 8,  desc: 'A devastating volcanic blast.' },
  flare_whip: { name: 'Flare Whip', type: 'pyro',  power: 60, acc: 90, pp: 12, desc: 'A burning lash of plasma.' },
  // HYDRO
  splash:   { name: 'Splash',      type: 'hydro',  power: 30, acc: 100,pp: 25, desc: 'A sloppy jet of water.' },
  aqua_jet: { name: 'Aqua Jet',    type: 'hydro',  power: 45, acc: 95, pp: 20, desc: 'A piercing high-pressure stream.' },
  tidal_slam: { name: 'Tidal Slam', type: 'hydro', power: 65, acc: 90, pp: 15, desc: 'A crushing wave of seawater.' },
  hydro_pump: { name: 'Hydro Pump', type: 'hydro', power: 80, acc: 80, pp: 10, desc: 'A torrential blast.' },
  maelstrom: { name: 'Maelstrom',  type: 'hydro',  power: 90, acc: 80, pp: 8,  desc: 'A swirling watery vortex.' },
  brine_strike: { name: 'Brine Strike', type: 'hydro', power: 55, acc: 95, pp: 15, desc: 'Salty corrosive blast.' },
  // FLORA
  vine_whip: { name: 'Vine Whip',  type: 'flora',  power: 35, acc: 100,pp: 25, desc: 'Striking vines lash the foe.' },
  razor_leaf: { name: 'Razor Leaf', type: 'flora', power: 50, acc: 95, pp: 20, desc: 'Sharp leaves slice through.' },
  thorn_volley: { name: 'Thorn Volley', type: 'flora', power: 60, acc: 90, pp: 15, desc: 'A barrage of bramble.' },
  bloom_burst: { name: 'Bloom Burst', type: 'flora', power: 75, acc: 85, pp: 12, desc: 'A blooming explosion of pollen.' },
  verdant_ruin: { name: 'Verdant Ruin', type: 'flora', power: 95, acc: 80, pp: 8, desc: 'The forest reclaims the foe.' },
  spores: { name: 'Spore Cloud', type: 'flora', power: 30, acc: 100, pp: 20, desc: 'Status: poison the foe.' },
  // ZEPHYR
  gust:     { name: 'Gust',        type: 'zephyr', power: 30, acc: 100,pp: 25, desc: 'A gusty wind attack.' },
  air_cutter: { name: 'Air Cutter', type: 'zephyr', power: 50, acc: 95, pp: 20, desc: 'Slicing blades of air.' },
  cyclone:  { name: 'Cyclone',     type: 'zephyr', power: 60, acc: 90, pp: 15, desc: 'A spinning vortex.' },
  skyfall:  { name: 'Skyfall',     type: 'zephyr', power: 80, acc: 85, pp: 10, desc: 'Strikes from above.' },
  stormwind: { name: 'Stormwind',  type: 'zephyr', power: 95, acc: 80, pp: 8,  desc: 'A cataclysmic gale.' },
  zephyr_song: { name: 'Zephyr Song', type: 'zephyr', power: 30, acc: 100, pp: 20, desc: 'Status: lull foe to sleep.' },
  // EARTH
  rock_throw: { name: 'Rock Throw', type: 'earth', power: 35, acc: 90, pp: 25, desc: 'Hurls a chunk of rock.' },
  mud_slap:   { name: 'Mud Slap',   type: 'earth', power: 45, acc: 100, pp: 20, desc: 'Slaps with mucky earth.' },
  stone_edge: { name: 'Stone Edge', type: 'earth', power: 65, acc: 85, pp: 15, desc: 'Sharp stones erupt below.' },
  quake:      { name: 'Quake',      type: 'earth', power: 80, acc: 90, pp: 10, desc: 'A shuddering ground tremor.' },
  tectonic_lash: { name: 'Tectonic Lash', type: 'earth', power: 95, acc: 80, pp: 8, desc: 'Splits the ground apart.' },
  sand_blast: { name: 'Sand Blast', type: 'earth', power: 40, acc: 100, pp: 20, desc: 'A blinding blast of grit.' },
  // VOLT
  spark:    { name: 'Spark',       type: 'volt',   power: 35, acc: 100,pp: 25, desc: 'A jolt of static.' },
  thundershock: { name: 'Thundershock', type: 'volt', power: 50, acc: 95, pp: 20, desc: 'A sharp shock.' },
  charge_beam: { name: 'Charge Beam', type: 'volt', power: 60, acc: 90, pp: 15, desc: 'A focused beam of energy.' },
  thunderbolt: { name: 'Thunderbolt', type: 'volt', power: 80, acc: 85, pp: 10, desc: 'A powerful electric blast.' },
  stormlord_call: { name: 'Stormlord Call', type: 'volt', power: 95, acc: 80, pp: 8, desc: 'Calls a storm of judgement.' },
  static_buzz: { name: 'Static Buzz', type: 'volt', power: 30, acc: 100, pp: 20, desc: 'Status: may paralyze.' },
  // NORMAL-ish / COSMOS
  tackle:   { name: 'Tackle',      type: 'cosmos', power: 30, acc: 100,pp: 30, desc: 'A basic body slam.' },
  bite:     { name: 'Bite',        type: 'cosmos', power: 45, acc: 100,pp: 25, desc: 'A vicious bite.' },
  slash:    { name: 'Slash',       type: 'cosmos', power: 60, acc: 95, pp: 20, desc: 'A sharp claw slash.' },
  crush:    { name: 'Crush',       type: 'cosmos', power: 75, acc: 90, pp: 15, desc: 'An overwhelming slam.' },
  void_ray: { name: 'Void Ray',    type: 'cosmos', power: 90, acc: 85, pp: 10, desc: 'A beam of pure void energy.' }
};

// ── const: STATUS_MOVES ──
const STATUS_MOVES = { spores: 'poison', zephyr_song: 'sleep', static_buzz: 'paralysis' };

// ── const: ELEMENT_INFO ──
// Element color palettes and type effectiveness table.
const ELEMENT_INFO = {
  pyro:   { name: 'Pyro',   color1: '#ff6a3d', color2: '#ffb27a', glow: 'rgba(255,106,61,0.6)', light: '#ff9966', dark: '#cc3a14', accent: '#ffe4b0' },
  hydro:  { name: 'Hydro',  color1: '#4cc4ff', color2: '#8ee3ff', glow: 'rgba(76,196,255,0.6)', light: '#a0e0ff', dark: '#1a78cc', accent: '#dff4ff' },
  flora:  { name: 'Flora',  color1: '#6cff8e', color2: '#c2ff9b', glow: 'rgba(108,255,142,0.6)', light: '#a0ffba', dark: '#2a8a4a', accent: '#e0ffd6' },
  volt:   { name: 'Volt',   color1: '#ffe04a', color2: '#fff7a3', glow: 'rgba(255,224,74,0.6)', light: '#ffec88', dark: '#cca820', accent: '#fffadc' },
  zephyr: { name: 'Zephyr', color1: '#c8b6ff', color2: '#e3d8ff', glow: 'rgba(200,182,255,0.6)', light: '#d8c8ff', dark: '#7864c8', accent: '#f0e8ff' },
  earth:  { name: 'Earth',  color1: '#c98b5b', color2: '#e6b58a', glow: 'rgba(201,139,91,0.6)', light: '#d8a47a', dark: '#7a4a26', accent: '#f0d8b8' },
  cosmos: { name: 'Cosmos', color1: '#d6a8ff', color2: '#f1d8ff', glow: 'rgba(214,168,255,0.6)', light: '#e0b8ff', dark: '#7a48b8', accent: '#f8e8ff' }
};

// ── const: TYPE_CHART ──
// type -> { strongAgainst: [], weakAgainst: [] }
const TYPE_CHART = {
  pyro:   { strong: ['flora'],  weak: ['hydro'] },
  hydro:  { strong: ['pyro'],   weak: ['flora'] },
  flora:  { strong: ['hydro'],  weak: ['pyro'] },
  volt:   { strong: ['zephyr'], weak: ['earth'] },
  earth:  { strong: ['volt'],   weak: ['zephyr'] },
  zephyr: { strong: ['earth'],  weak: ['volt'] },
  cosmos: { strong: [],         weak: [] }
};

// ── function: typeEffectiveness ──
function typeEffectiveness(attackerType, defenderType) {
  if (attackerType === 'cosmos' || defenderType === 'cosmos') return 1.0;
  const chart = TYPE_CHART[attackerType];
  if (chart.strong.includes(defenderType)) return 1.6;
  if (chart.weak.includes(defenderType)) return 0.6;
  return 1.0;
}

// ── function: elColors ──
// SVG sprite generator. Each Resonant has a "shape" key that drives distinct geometry.
// We use gradients, polylines, and layered paths to create a premium vector look.

function elColors(type) {
  return ELEMENT_INFO[type] || ELEMENT_INFO.cosmos;
}

// ── function: shadowEllipse ──
function shadowEllipse(color, opacity) {
  return `<ellipse cx="80" cy="148" rx="46" ry="6" fill="${color}" opacity="${opacity || 0.4}"/>`;
}

// ── const: SHAPE_BUILDERS ──
const SHAPE_BUILDERS = {
  // PYRO LINE
  emberkit: (c) => `
    ${shadowEllipse(c.color1, 0.5)}
    <defs><radialGradient id="g-ember" cx="50%" cy="50%"><stop offset="0%" stop-color="${c.color2}"/><stop offset="60%" stop-color="${c.color1}"/><stop offset="100%" stop-color="${c.dark}"/></radialGradient></defs>
    <ellipse cx="80" cy="90" rx="38" ry="42" fill="url(#g-ember)" stroke="${c.dark}" stroke-width="2"/>
    <path d="M55 95 Q50 75 65 70 Q70 60 80 65 Q90 60 95 70 Q110 75 105 95" fill="${c.color2}" opacity="0.9"/>
    <path d="M62 110 Q60 125 70 130 Q80 135 90 130 Q100 125 98 110" fill="${c.dark}" opacity="0.5"/>
    <ellipse cx="68" cy="85" rx="6" ry="9" fill="#fff" opacity="0.85"/>
    <ellipse cx="92" cy="85" rx="6" ry="9" fill="#fff" opacity="0.85"/>
    <circle cx="68" cy="86" r="3" fill="#1a0a00"/>
    <circle cx="92" cy="86" r="3" fill="#1a0a00"/>
    <path d="M50 130 L40 150 M110 130 L120 150 M80 132 L80 155" stroke="${c.color2}" stroke-width="3" stroke-linecap="round" opacity="0.9"/>
  `,
  pyrowolf: (c) => `
    ${shadowEllipse(c.color1, 0.5)}
    <defs><linearGradient id="g-pyrow" x1="0%" y1="0%" x2="0%" y2="100%"><stop offset="0%" stop-color="${c.color2}"/><stop offset="100%" stop-color="${c.dark}"/></linearGradient></defs>
    <path d="M30 110 Q30 70 60 60 Q70 45 90 50 Q110 45 120 60 Q150 70 150 110 Q150 140 130 145 L50 145 Q30 140 30 110 Z" fill="url(#g-pyrow)" stroke="${c.dark}" stroke-width="2"/>
    <path d="M50 45 L60 60 M90 40 L95 50 M120 45 L115 60" stroke="${c.color2}" stroke-width="3" stroke-linecap="round"/>
    <path d="M65 95 L80 100 L95 95 L88 105 L80 102 L72 105 Z" fill="${c.dark}"/>
    <ellipse cx="68" cy="80" rx="6" ry="8" fill="#fff"/>
    <ellipse cx="112" cy="80" rx="6" ry="8" fill="#fff"/>
    <circle cx="68" cy="82" r="3" fill="#1a0a00"/>
    <circle cx="112" cy="82" r="3" fill="#1a0a00"/>
    <path d="M50 130 L40 150 M130 130 L140 150" stroke="${c.color2}" stroke-width="4" stroke-linecap="round"/>
  `,
  ignisaur: (c) => `
    ${shadowEllipse(c.color1, 0.5)}
    <defs><linearGradient id="g-igni" x1="0%" y1="0%" x2="0%" y2="100%"><stop offset="0%" stop-color="${c.color2}"/><stop offset="50%" stop-color="${c.color1}"/><stop offset="100%" stop-color="${c.dark}"/></linearGradient></defs>
    <path d="M20 120 Q20 80 50 70 Q60 40 90 50 Q120 40 130 70 Q160 80 160 120 Q160 150 130 152 L50 152 Q20 150 20 120 Z" fill="url(#g-igni)" stroke="${c.dark}" stroke-width="2.5"/>
    <path d="M40 70 L50 50 L60 70 M75 50 L85 30 L95 50 M110 70 L120 50 L130 70" stroke="${c.color2}" stroke-width="4" stroke-linecap="round" fill="none"/>
    <path d="M55 95 Q70 105 90 95 Q110 105 125 95 L120 115 Q100 122 80 122 Q60 122 60 115 Z" fill="${c.dark}"/>
    <circle cx="62" cy="85" r="8" fill="#fff" stroke="${c.dark}" stroke-width="1.5"/>
    <circle cx="118" cy="85" r="8" fill="#fff" stroke="${c.dark}" stroke-width="1.5"/>
    <circle cx="62" cy="87" r="4" fill="#ffea00"/>
    <circle cx="118" cy="87" r="4" fill="#ffea00"/>
    <circle cx="62" cy="87" r="2" fill="#1a0a00"/>
    <circle cx="118" cy="87" r="2" fill="#1a0a00"/>
  `,
  // HYDRO LINE
  droplet: (c) => `
    ${shadowEllipse(c.color1, 0.5)}
    <defs><radialGradient id="g-drop" cx="50%" cy="40%"><stop offset="0%" stop-color="${c.color2}"/><stop offset="100%" stop-color="${c.dark}"/></radialGradient></defs>
    <path d="M80 30 Q120 80 120 110 Q120 145 80 145 Q40 145 40 110 Q40 80 80 30 Z" fill="url(#g-drop)" stroke="${c.dark}" stroke-width="2"/>
    <ellipse cx="65" cy="80" rx="10" ry="16" fill="#fff" opacity="0.5"/>
    <ellipse cx="70" cy="105" rx="6" ry="9" fill="#fff"/>
    <ellipse cx="90" cy="105" rx="6" ry="9" fill="#fff"/>
    <circle cx="70" cy="107" r="2.5" fill="#0a1a3a"/>
    <circle cx="90" cy="107" r="2.5" fill="#0a1a3a"/>
    <path d="M75 120 Q80 125 85 120" stroke="#0a1a3a" stroke-width="2" fill="none" stroke-linecap="round"/>
  `,
  geoxus: (c) => `
    ${shadowEllipse(c.color1, 0.5)}
    <defs><linearGradient id="g-geox" x1="0%" y1="0%" x2="0%" y2="100%"><stop offset="0%" stop-color="${c.color2}"/><stop offset="100%" stop-color="${c.dark}"/></linearGradient></defs>
    <path d="M80 30 Q60 50 50 70 Q35 90 35 115 Q35 145 80 148 Q125 145 125 115 Q125 90 110 70 Q100 50 80 30 Z" fill="url(#g-geox)" stroke="${c.dark}" stroke-width="2"/>
    <path d="M50 95 Q80 105 110 95" stroke="${c.accent}" stroke-width="2" fill="none" opacity="0.7"/>
    <path d="M60 100 L60 115 M80 100 L80 118 M100 100 L100 115" stroke="${c.accent}" stroke-width="2" opacity="0.6"/>
    <ellipse cx="65" cy="95" rx="6" ry="9" fill="#fff"/>
    <ellipse cx="95" cy="95" rx="6" ry="9" fill="#fff"/>
    <circle cx="65" cy="97" r="3" fill="#0a1a3a"/>
    <circle cx="95" cy="97" r="3" fill="#0a1a3a"/>
    <path d="M50 130 L40 152 M110 130 L120 152" stroke="${c.dark}" stroke-width="4" stroke-linecap="round"/>
  `,
  neptunian: (c) => `
    ${shadowEllipse(c.color1, 0.5)}
    <defs><linearGradient id="g-nept" x1="0%" y1="0%" x2="0%" y2="100%"><stop offset="0%" stop-color="${c.color2}"/><stop offset="100%" stop-color="${c.dark}"/></linearGradient></defs>
    <ellipse cx="80" cy="90" rx="55" ry="60" fill="url(#g-nept)" stroke="${c.dark}" stroke-width="2.5"/>
    <path d="M25 90 Q40 80 60 90 Q80 100 100 90 Q120 80 135 90" stroke="${c.accent}" stroke-width="2" fill="none" opacity="0.7"/>
    <path d="M25 70 Q40 60 60 70 Q80 80 100 70 Q120 60 135 70" stroke="${c.accent}" stroke-width="2" fill="none" opacity="0.7"/>
    <path d="M25 110 Q40 100 60 110 Q80 120 100 110 Q120 100 135 110" stroke="${c.accent}" stroke-width="2" fill="none" opacity="0.7"/>
    <path d="M80 30 L70 50 L80 45 L90 50 Z" fill="${c.color2}" stroke="${c.dark}" stroke-width="1.5"/>
    <ellipse cx="62" cy="88" rx="7" ry="10" fill="#fff"/>
    <ellipse cx="98" cy="88" rx="7" ry="10" fill="#fff"/>
    <circle cx="62" cy="90" r="3.5" fill="#0a1a3a"/>
    <circle cx="98" cy="90" r="3.5" fill="#0a1a3a"/>
    <path d="M70 115 Q80 122 90 115" stroke="#0a1a3a" stroke-width="2" fill="none"/>
  `,
  // FLORA LINE
  seedling: (c) => `
    ${shadowEllipse(c.color1, 0.5)}
    <defs><radialGradient id="g-seed" cx="50%" cy="40%"><stop offset="0%" stop-color="${c.color2}"/><stop offset="100%" stop-color="${c.dark}"/></radialGradient></defs>
    <ellipse cx="80" cy="110" rx="45" ry="35" fill="url(#g-seed)" stroke="${c.dark}" stroke-width="2"/>
    <path d="M80 80 Q70 60 50 55 Q60 75 80 80 Z" fill="${c.color2}" stroke="${c.dark}" stroke-width="1.5"/>
    <path d="M80 80 Q90 60 110 55 Q100 75 80 80 Z" fill="${c.color2}" stroke="${c.dark}" stroke-width="1.5"/>
    <path d="M80 80 L80 55" stroke="${c.dark}" stroke-width="2"/>
    <circle cx="80" cy="50" r="6" fill="${c.accent}"/>
    <ellipse cx="65" cy="100" rx="6" ry="9" fill="#fff"/>
    <ellipse cx="95" cy="100" rx="6" ry="9" fill="#fff"/>
    <circle cx="65" cy="102" r="3" fill="#0a3a1a"/>
    <circle cx="95" cy="102" r="3" fill="#0a3a1a"/>
    <path d="M70 125 Q80 130 90 125" stroke="#0a3a1a" stroke-width="2" fill="none"/>
  `,
  thornback: (c) => `
    ${shadowEllipse(c.color1, 0.5)}
    <defs><linearGradient id="g-thorn" x1="0%" y1="0%" x2="0%" y2="100%"><stop offset="0%" stop-color="${c.color2}"/><stop offset="100%" stop-color="${c.dark}"/></linearGradient></defs>
    <path d="M30 110 Q30 70 60 60 Q80 50 100 60 Q130 70 130 110 Q130 150 100 150 L60 150 Q30 150 30 110 Z" fill="url(#g-thorn)" stroke="${c.dark}" stroke-width="2"/>
    <path d="M50 50 L55 35 M70 45 L72 30 M90 45 L88 30 M110 50 L105 35" stroke="${c.color2}" stroke-width="3" stroke-linecap="round"/>
    <path d="M50 50 L42 65 M70 45 L70 30 M90 45 L90 30 M110 50 L118 65" stroke="${c.dark}" stroke-width="2"/>
    <ellipse cx="62" cy="95" rx="7" ry="10" fill="#fff"/>
    <ellipse cx="98" cy="95" rx="7" ry="10" fill="#fff"/>
    <circle cx="62" cy="97" r="3.5" fill="#0a3a1a"/>
    <circle cx="98" cy="97" r="3.5" fill="#0a3a1a"/>
    <path d="M55 130 L45 152 M105 130 L115 152" stroke="${c.dark}" stroke-width="4" stroke-linecap="round"/>
  `,
  bloomgoliath: (c) => `
    ${shadowEllipse(c.color1, 0.5)}
    <defs><radialGradient id="g-bloom" cx="50%" cy="50%"><stop offset="0%" stop-color="${c.accent}"/><stop offset="100%" stop-color="${c.dark}"/></radialGradient></defs>
    <circle cx="80" cy="90" r="55" fill="url(#g-bloom)" stroke="${c.dark}" stroke-width="2.5"/>
    <ellipse cx="50" cy="60" rx="20" ry="14" fill="${c.color2}" stroke="${c.dark}" stroke-width="1.5" transform="rotate(-30 50 60)"/>
    <ellipse cx="110" cy="60" rx="20" ry="14" fill="${c.color2}" stroke="${c.dark}" stroke-width="1.5" transform="rotate(30 110 60)"/>
    <ellipse cx="45" cy="95" rx="20" ry="14" fill="${c.color2}" stroke="${c.dark}" stroke-width="1.5" transform="rotate(-60 45 95)"/>
    <ellipse cx="115" cy="95" rx="20" ry="14" fill="${c.color2}" stroke="${c.dark}" stroke-width="1.5" transform="rotate(60 115 95)"/>
    <circle cx="80" cy="90" r="28" fill="${c.accent}" stroke="${c.dark}" stroke-width="1.5"/>
    <ellipse cx="72" cy="85" rx="5" ry="8" fill="#fff"/>
    <ellipse cx="88" cy="85" rx="5" ry="8" fill="#fff"/>
    <circle cx="72" cy="86" r="2.5" fill="#0a3a1a"/>
    <circle cx="88" cy="86" r="2.5" fill="#0a3a1a"/>
  `,
  // VOLT LINE
  sparky: (c) => `
    ${shadowEllipse(c.color1, 0.5)}
    <defs><radialGradient id="g-spark" cx="50%" cy="50%"><stop offset="0%" stop-color="${c.accent}"/><stop offset="100%" stop-color="${c.dark}"/></radialGradient></defs>
    <path d="M80 25 L60 80 L75 80 L60 130 L100 70 L85 70 L100 25 Z" fill="url(#g-spark)" stroke="${c.dark}" stroke-width="2"/>
    <path d="M65 60 L70 55 M85 50 L90 45 M75 100 L80 95" stroke="#fff" stroke-width="2" opacity="0.6"/>
    <ellipse cx="72" cy="78" rx="3" ry="4" fill="#1a1a00"/>
    <ellipse cx="88" cy="78" rx="3" ry="4" fill="#1a1a00"/>
  `,
  boltsaber: (c) => `
    ${shadowEllipse(c.color1, 0.5)}
    <defs><linearGradient id="g-bolt" x1="0%" y1="0%" x2="0%" y2="100%"><stop offset="0%" stop-color="${c.accent}"/><stop offset="100%" stop-color="${c.dark}"/></linearGradient></defs>
    <path d="M50 130 L40 80 Q35 55 60 50 L100 45 Q135 50 130 85 L120 130 Z" fill="url(#g-bolt)" stroke="${c.dark}" stroke-width="2"/>
    <path d="M60 50 L65 30 M85 45 L88 25 M110 50 L115 30" stroke="${c.color2}" stroke-width="2.5" stroke-linecap="round"/>
    <ellipse cx="68" cy="80" rx="6" ry="9" fill="#fff"/>
    <ellipse cx="100" cy="80" rx="6" ry="9" fill="#fff"/>
    <circle cx="68" cy="82" r="3" fill="#1a1a00"/>
    <circle cx="100" cy="82" r="3" fill="#1a1a00"/>
    <path d="M70 110 L80 105 L90 110" stroke="#1a1a00" stroke-width="2" fill="none"/>
    <path d="M50 130 L40 152 M120 130 L130 152" stroke="${c.dark}" stroke-width="4" stroke-linecap="round"/>
  `,
  stormlord: (c) => `
    ${shadowEllipse(c.color1, 0.5)}
    <defs><linearGradient id="g-storm" x1="0%" y1="0%" x2="0%" y2="100%"><stop offset="0%" stop-color="${c.accent}"/><stop offset="50%" stop-color="${c.color1}"/><stop offset="100%" stop-color="${c.dark}"/></linearGradient></defs>
    <path d="M30 110 Q25 70 60 60 Q80 30 110 50 Q140 60 140 100 Q140 140 110 145 L60 145 Q30 140 30 110 Z" fill="url(#g-storm)" stroke="${c.dark}" stroke-width="2.5"/>
    <path d="M30 75 L20 50 M55 50 L45 25 M90 35 L95 15 M120 50 L130 25 M145 80 L155 60" stroke="${c.color2}" stroke-width="3" stroke-linecap="round"/>
    <ellipse cx="62" cy="90" rx="8" ry="11" fill="#fff"/>
    <ellipse cx="108" cy="90" rx="8" ry="11" fill="#fff"/>
    <circle cx="62" cy="92" r="4" fill="#1a1a00"/>
    <circle cx="108" cy="92" r="4" fill="#1a1a00"/>
    <path d="M50 130 L40 152 M120 130 L130 152" stroke="${c.dark}" stroke-width="4" stroke-linecap="round"/>
    <path d="M70 120 L80 113 L90 120 L85 125 L80 122 L75 125 Z" fill="${c.dark}"/>
  `,
  // ZEPHYR LINE
  gusty: (c) => `
    ${shadowEllipse(c.color1, 0.5)}
    <defs><radialGradient id="g-gust" cx="50%" cy="50%"><stop offset="0%" stop-color="${c.color2}"/><stop offset="100%" stop-color="${c.dark}"/></radialGradient></defs>
    <path d="M30 90 Q30 65 50 60 Q60 40 80 45 Q100 40 110 60 Q130 65 130 90 Q130 115 110 120 L50 120 Q30 115 30 90 Z" fill="url(#g-gust)" stroke="${c.dark}" stroke-width="2"/>
    <path d="M40 80 Q30 75 25 80 M40 90 Q28 88 22 95 M120 80 Q130 75 135 80 M120 90 Q132 88 138 95" stroke="${c.color2}" stroke-width="2.5" fill="none" stroke-linecap="round" opacity="0.7"/>
    <ellipse cx="65" cy="80" rx="5" ry="7" fill="#fff"/>
    <ellipse cx="95" cy="80" rx="5" ry="7" fill="#fff"/>
    <circle cx="65" cy="82" r="2.5" fill="#1a0a3a"/>
    <circle cx="95" cy="82" r="2.5" fill="#1a0a3a"/>
    <path d="M50 120 L40 145 M110 120 L120 145" stroke="${c.color2}" stroke-width="3" stroke-linecap="round" opacity="0.8"/>
  `,
  slipstream: (c) => `
    ${shadowEllipse(c.color1, 0.5)}
    <defs><linearGradient id="g-slip" x1="0%" y1="0%" x2="0%" y2="100%"><stop offset="0%" stop-color="${c.color2}"/><stop offset="100%" stop-color="${c.dark}"/></linearGradient></defs>
    <path d="M80 20 Q40 50 35 90 Q30 130 80 145 Q130 130 125 90 Q120 50 80 20 Z" fill="url(#g-slip)" stroke="${c.dark}" stroke-width="2"/>
    <path d="M40 70 Q20 60 15 80 M120 70 Q140 60 145 80 M40 110 Q20 120 15 100 M120 110 Q140 120 145 100" stroke="${c.color2}" stroke-width="2.5" fill="none" stroke-linecap="round" opacity="0.7"/>
    <ellipse cx="65" cy="85" rx="6" ry="9" fill="#fff"/>
    <ellipse cx="95" cy="85" rx="6" ry="9" fill="#fff"/>
    <circle cx="65" cy="87" r="3" fill="#1a0a3a"/>
    <circle cx="95" cy="87" r="3" fill="#1a0a3a"/>
    <path d="M40 130 L30 155 M120 130 L130 155" stroke="${c.color2}" stroke-width="4" stroke-linecap="round"/>
  `,
  zephyrus: (c) => `
    ${shadowEllipse(c.color1, 0.5)}
    <defs><linearGradient id="g-zeph" x1="0%" y1="0%" x2="0%" y2="100%"><stop offset="0%" stop-color="${c.color2}"/><stop offset="100%" stop-color="${c.dark}"/></linearGradient></defs>
    <ellipse cx="80" cy="80" rx="50" ry="45" fill="url(#g-zeph)" stroke="${c.dark}" stroke-width="2.5"/>
    <path d="M30 60 Q20 50 30 40 M130 60 Q140 50 130 40 M40 100 Q20 100 15 90 M120 100 Q140 100 145 90" stroke="${c.color2}" stroke-width="3" fill="none" stroke-linecap="round"/>
    <path d="M50 30 L60 50 L45 55 L65 75 L50 80 L70 100 L60 115 M110 30 L100 50 L115 55 L95 75 L110 80 L90 100 L100 115" stroke="${c.accent}" stroke-width="2" fill="none" opacity="0.7"/>
    <ellipse cx="63" cy="80" rx="7" ry="10" fill="#fff"/>
    <ellipse cx="97" cy="80" rx="7" ry="10" fill="#fff"/>
    <circle cx="63" cy="82" r="3.5" fill="#1a0a3a"/>
    <circle cx="97" cy="82" r="3.5" fill="#1a0a3a"/>
  `,
  // EARTH LINE
  pebble: (c) => `
    ${shadowEllipse(c.color1, 0.5)}
    <defs><radialGradient id="g-peb" cx="40%" cy="40%"><stop offset="0%" stop-color="${c.color2}"/><stop offset="100%" stop-color="${c.dark}"/></radialGradient></defs>
    <path d="M50 70 Q40 50 60 40 Q80 30 100 40 Q120 50 110 70 Q130 80 130 110 Q130 140 100 145 L60 145 Q30 140 30 110 Q30 80 50 70 Z" fill="url(#g-peb)" stroke="${c.dark}" stroke-width="2"/>
    <ellipse cx="65" cy="100" rx="6" ry="9" fill="#fff"/>
    <ellipse cx="95" cy="100" rx="6" ry="9" fill="#fff"/>
    <circle cx="65" cy="102" r="3" fill="#1a0a00"/>
    <circle cx="95" cy="102" r="3" fill="#1a0a00"/>
    <path d="M72 122 L88 122" stroke="#1a0a00" stroke-width="2.5" stroke-linecap="round"/>
    <path d="M30 110 L20 95 L35 90 M130 110 L140 95 L125 90" stroke="${c.dark}" stroke-width="2" fill="none"/>
  `,
  obsidian: (c) => `
    ${shadowEllipse(c.color1, 0.5)}
    <defs><linearGradient id="g-obs" x1="0%" y1="0%" x2="0%" y2="100%"><stop offset="0%" stop-color="${c.color2}"/><stop offset="100%" stop-color="${c.dark}"/></linearGradient></defs>
    <path d="M30 100 L50 50 L80 30 L110 50 L130 100 L120 145 L40 145 Z" fill="url(#g-obs)" stroke="${c.dark}" stroke-width="2.5"/>
    <path d="M50 50 L80 30 L110 50" stroke="${c.accent}" stroke-width="2" fill="none" opacity="0.6"/>
    <path d="M55 90 L70 80 L80 90 L95 80 L105 90" stroke="${c.accent}" stroke-width="1.5" fill="none" opacity="0.6"/>
    <ellipse cx="65" cy="105" rx="6" ry="9" fill="#fff"/>
    <ellipse cx="95" cy="105" rx="6" ry="9" fill="#fff"/>
    <circle cx="65" cy="107" r="3" fill="#1a0a00"/>
    <circle cx="95" cy="107" r="3" fill="#1a0a00"/>
    <path d="M50 130 L40 152 M110 130 L120 152" stroke="${c.dark}" stroke-width="4" stroke-linecap="round"/>
  `,
  terrasunder: (c) => `
    ${shadowEllipse(c.color1, 0.5)}
    <defs><linearGradient id="g-terra" x1="0%" y1="0%" x2="0%" y2="100%"><stop offset="0%" stop-color="${c.color2}"/><stop offset="50%" stop-color="${c.color1}"/><stop offset="100%" stop-color="${c.dark}"/></linearGradient></defs>
    <path d="M20 110 L30 60 L55 30 L80 15 L105 30 L130 60 L140 110 L130 150 L30 150 Z" fill="url(#g-terra)" stroke="${c.dark}" stroke-width="3"/>
    <path d="M30 60 L55 30 L80 15 L105 30 L130 60" stroke="${c.accent}" stroke-width="2.5" fill="none" opacity="0.7"/>
    <path d="M40 90 L60 75 L80 90 L100 75 L120 90" stroke="${c.accent}" stroke-width="2" fill="none" opacity="0.5"/>
    <path d="M45 115 L65 105 L80 120 L95 105 L115 115" stroke="${c.accent}" stroke-width="2" fill="none" opacity="0.5"/>
    <ellipse cx="62" cy="105" rx="7" ry="10" fill="#fff"/>
    <ellipse cx="98" cy="105" rx="7" ry="10" fill="#fff"/>
    <circle cx="62" cy="107" r="3.5" fill="#1a0a00"/>
    <circle cx="98" cy="107" r="3.5" fill="#1a0a00"/>
    <path d="M65 130 L80 125 L95 130" stroke="#1a0a00" stroke-width="2.5" fill="none" stroke-linecap="round"/>
  `
};

// ── function: renderSprite ──
function renderSprite(shape, type, size, extraClass) {
  const c = elColors(type);
  const builder = SHAPE_BUILDERS[shape];
  const cls = 'sprite-svg' + (extraClass ? ' ' + extraClass : '');
  if (!builder) return '<svg class="' + cls + '" viewBox="0 0 160 160" width="' + (size || 160) + '" height="' + (size || 160) + '"></svg>';
  return '<svg class="' + cls + '" viewBox="0 0 160 160" width="' + (size || 160) + '" height="' + (size || 160) + '" xmlns="http://www.w3.org/2000/svg">' + builder(c) + '</svg>';
}

// ── const: RESONANTS ──
const RESONANTS = [
  // === PYRO ===
  {
    id: 'emberkit', name: 'Emberkit', type: 'pyro', shape: 'emberkit',
    baseStats: { hp: 38, atk: 32, def: 28, spd: 36 },
    moves: [
      { id: 'tackle',     unlock: 1 },
      { id: 'ember',      unlock: 1 },
      { id: 'flame_fang', unlock: 6 },
      { id: 'pyre_claw',  unlock: 12 }
    ],
    evolvesTo: 8, evolvesInto: 'pyrowolf',
    dex: 'A curious flame kitten that purrs warm embers. Found near hearths and lava vents.'
  },
  {
    id: 'pyrowolf', name: 'Pyrowolf', type: 'pyro', shape: 'pyrowolf',
    baseStats: { hp: 58, atk: 50, def: 42, spd: 55 },
    moves: [
      { id: 'ember',       unlock: 1 },
      { id: 'flame_fang',  unlock: 1 },
      { id: 'pyre_claw',   unlock: 1 },
      { id: 'flare_whip',  unlock: 10 },
      { id: 'inferno',     unlock: 14 }
    ],
    evolvesTo: 15, evolvesInto: 'ignisaur',
    dex: 'A prowling wolf wreathed in flame. Hunts in coordinated packs across volcanic flats.'
  },
  {
    id: 'ignisaur', name: 'Ignisaur', type: 'pyro', shape: 'ignisaur',
    baseStats: { hp: 85, atk: 78, def: 70, spd: 70 },
    moves: [
      { id: 'flame_fang',  unlock: 1 },
      { id: 'pyre_claw',   unlock: 1 },
      { id: 'flare_whip',  unlock: 1 },
      { id: 'inferno',     unlock: 1 },
      { id: 'eruption',    unlock: 18 }
    ],
    evolvesTo: null, evolvesInto: null,
    dex: 'A towering drake of living magma. Said to slumber beneath the Apex Pillar.'
  },
  // === HYDRO ===
  {
    id: 'droplet', name: 'Droplet', type: 'hydro', shape: 'droplet',
    baseStats: { hp: 40, atk: 30, def: 32, spd: 32 },
    moves: [
      { id: 'splash',   unlock: 1 },
      { id: 'tackle',   unlock: 1 },
      { id: 'aqua_jet', unlock: 6 },
      { id: 'brine_strike', unlock: 12 }
    ],
    evolvesTo: 8, evolvesInto: 'geoxus',
    dex: 'A teardrop-shaped spirit that drifts in rain puddles. Playful, but quick to splash.'
  },
  {
    id: 'geoxus', name: 'Geoxus', type: 'hydro', shape: 'geoxus',
    baseStats: { hp: 60, atk: 48, def: 50, spd: 45 },
    moves: [
      { id: 'aqua_jet',    unlock: 1 },
      { id: 'brine_strike', unlock: 1 },
      { id: 'tidal_slam',  unlock: 10 },
      { id: 'hydro_pump',  unlock: 14 }
    ],
    evolvesTo: 15, evolvesInto: 'neptunian',
    dex: 'A tidal guardian carved from the ocean itself. Commands undertows with a glance.'
  },
  {
    id: 'neptunian', name: 'Neptunian', type: 'hydro', shape: 'neptunian',
    baseStats: { hp: 88, atk: 72, def: 80, spd: 65 },
    moves: [
      { id: 'tidal_slam', unlock: 1 },
      { id: 'hydro_pump', unlock: 1 },
      { id: 'brine_strike', unlock: 1 },
      { id: 'maelstrom',  unlock: 18 }
    ],
    evolvesTo: null, evolvesInto: null,
    dex: 'A titan of the deep, crowned in coral and storm. Sings the Abyssal Anthem.'
  },
  // === FLORA ===
  {
    id: 'seedling', name: 'Seedling', type: 'flora', shape: 'seedling',
    baseStats: { hp: 42, atk: 30, def: 35, spd: 30 },
    moves: [
      { id: 'vine_whip', unlock: 1 },
      { id: 'tackle',    unlock: 1 },
      { id: 'razor_leaf', unlock: 6 },
      { id: 'spores',    unlock: 10 }
    ],
    evolvesTo: 8, evolvesInto: 'thornback',
    dex: 'A tiny sprout spirit with the curiosity of a sunbeam. Photosynthesizes kindness.'
  },
  {
    id: 'thornback', name: 'Thornback', type: 'flora', shape: 'thornback',
    baseStats: { hp: 62, atk: 52, def: 55, spd: 38 },
    moves: [
      { id: 'razor_leaf',   unlock: 1 },
      { id: 'vine_whip',    unlock: 1 },
      { id: 'thorn_volley', unlock: 10 },
      { id: 'bloom_burst',  unlock: 14 }
    ],
    evolvesTo: 15, evolvesInto: 'bloomgoliath',
    dex: 'A walking thicket. Its back bristles with rose-thorns that fire like cannons.'
  },
  {
    id: 'bloomgoliath', name: 'Bloomgoliath', type: 'flora', shape: 'bloomgoliath',
    baseStats: { hp: 90, atk: 75, def: 85, spd: 50 },
    moves: [
      { id: 'thorn_volley',  unlock: 1 },
      { id: 'bloom_burst',   unlock: 1 },
      { id: 'razor_leaf',    unlock: 1 },
      { id: 'verdant_ruin',  unlock: 18 }
    ],
    evolvesTo: null, evolvesInto: null,
    dex: 'A walking ancient grove. Petals fall like meteors when it stomps.'
  },
  // === ZEPHYR ===
  {
    id: 'gusty', name: 'Gusty', type: 'zephyr', shape: 'gusty',
    baseStats: { hp: 35, atk: 30, def: 28, spd: 42 },
    moves: [
      { id: 'gust',   unlock: 1 },
      { id: 'tackle', unlock: 1 },
      { id: 'air_cutter', unlock: 6 },
      { id: 'zephyr_song', unlock: 11 }
    ],
    evolvesTo: 8, evolvesInto: 'slipstream',
    dex: 'A wispy wisp that giggles on the breeze. Sneezes create small hurricanes.'
  },
  {
    id: 'slipstream', name: 'Slipstream', type: 'zephyr', shape: 'slipstream',
    baseStats: { hp: 55, atk: 48, def: 42, spd: 65 },
    moves: [
      { id: 'air_cutter',   unlock: 1 },
      { id: 'gust',         unlock: 1 },
      { id: 'cyclone',      unlock: 10 },
      { id: 'skyfall',      unlock: 14 }
    ],
    evolvesTo: 15, evolvesInto: 'zephyrus',
    dex: 'A serpent of pure wind. Cuts clouds in half just for fun.'
  },
  {
    id: 'zephyrus', name: 'Zephyrus', type: 'zephyr', shape: 'zephyrus',
    baseStats: { hp: 80, atk: 70, def: 65, spd: 95 },
    moves: [
      { id: 'cyclone',    unlock: 1 },
      { id: 'skyfall',    unlock: 1 },
      { id: 'air_cutter', unlock: 1 },
      { id: 'stormwind',  unlock: 18 }
    ],
    evolvesTo: null, evolvesInto: null,
    dex: 'A demigod of western winds. Whispers carry it across the world in an instant.'
  },
  // === EARTH ===
  {
    id: 'pebble', name: 'Pebble', type: 'earth', shape: 'pebble',
    baseStats: { hp: 45, atk: 36, def: 42, spd: 24 },
    moves: [
      { id: 'rock_throw', unlock: 1 },
      { id: 'tackle',     unlock: 1 },
      { id: 'mud_slap',   unlock: 6 },
      { id: 'sand_blast', unlock: 11 }
    ],
    evolvesTo: 8, evolvesInto: 'obsidian',
    dex: 'A rolling pebble with eyes. Disappears into the earth when startled.'
  },
  {
    id: 'obsidian', name: 'Obsidian', type: 'earth', shape: 'obsidian',
    baseStats: { hp: 65, atk: 58, def: 70, spd: 32 },
    moves: [
      { id: 'rock_throw', unlock: 1 },
      { id: 'mud_slap',   unlock: 1 },
      { id: 'stone_edge', unlock: 10 },
      { id: 'sand_blast', unlock: 1 }
    ],
    evolvesTo: 15, evolvesInto: 'terrasunder',
    dex: 'A glassy golem. Volcanic glass blades line its arms and shoulders.'
  },
  {
    id: 'terrasunder', name: 'Terrasunder', type: 'earth', shape: 'terrasunder',
    baseStats: { hp: 95, atk: 85, def: 105, spd: 38 },
    moves: [
      { id: 'stone_edge',     unlock: 1 },
      { id: 'mud_slap',       unlock: 1 },
      { id: 'quake',          unlock: 1 },
      { id: 'tectonic_lash',  unlock: 18 }
    ],
    evolvesTo: null, evolvesInto: null,
    dex: 'A mountain that walks. It splits valleys with every step.'
  },
  // === VOLT ===
  {
    id: 'sparky', name: 'Sparky', type: 'volt', shape: 'sparky',
    baseStats: { hp: 33, atk: 32, def: 26, spd: 45 },
    moves: [
      { id: 'spark',    unlock: 1 },
      { id: 'tackle',   unlock: 1 },
      { id: 'thundershock', unlock: 6 },
      { id: 'static_buzz',  unlock: 11 }
    ],
    evolvesTo: 8, evolvesInto: 'boltsaber',
    dex: 'A mouse-shaped spark. Hops along power lines and zaps anything rude.'
  },
  {
    id: 'boltsaber', name: 'Boltsaber', type: 'volt', shape: 'boltsaber',
    baseStats: { hp: 55, atk: 55, def: 42, spd: 70 },
    moves: [
      { id: 'thundershock', unlock: 1 },
      { id: 'spark',        unlock: 1 },
      { id: 'charge_beam',  unlock: 10 },
      { id: 'thunderbolt',  unlock: 14 }
    ],
    evolvesTo: 15, evolvesInto: 'stormlord',
    dex: 'A saber-toothed cat crackling with voltage. Hunts during thunderstorms.'
  },
  {
    id: 'stormlord', name: 'Stormlord', type: 'volt', shape: 'stormlord',
    baseStats: { hp: 78, atk: 78, def: 65, spd: 100 },
    moves: [
      { id: 'charge_beam',     unlock: 1 },
      { id: 'thunderbolt',     unlock: 1 },
      { id: 'static_buzz',     unlock: 1 },
      { id: 'stormlord_call',  unlock: 18 }
    ],
    evolvesTo: null, evolvesInto: null,
    dex: 'A living thunderhead. Roars in thunder and walks in lightning.'
  }
];

// ── const: RESONANTS_BY_ID ──
const RESONANTS_BY_ID = Object.fromEntries(RESONANTS.map(r => [r.id, r]));

// ── function: statsForSpecies ──
function statsForSpecies(species, level) {
  const b = species.baseStats;
  return {
    maxHp: Math.floor((b.hp * 2 + 31) * level / 100 + level + 10),
    atk:   Math.floor((b.atk * 2 + 31) * level / 100 + 5),
    def:   Math.floor((b.def * 2 + 31) * level / 100 + 5),
    spd:   Math.floor((b.spd * 2 + 31) * level / 100 + 5)
  };
}

// ── function: xpForLevel ──
function xpForLevel(level) {
  return Math.floor(Math.pow(level, 3) * 0.6 + level * 8);
}

// ── function: xpToNext ──
function xpToNext(level) {
  return xpForLevel(level + 1) - xpForLevel(level);
}

// ── const: ZONES ──
// Zone and node definitions — 4 zones, each a series of nodes on a path.
// Each node is positioned in % of the canvas. Connections form a directed path.
const ZONES = [
  {
    id: 'verdant',
    name: 'The Verdant Canopy',
    subtitle: 'A whispering forest of bioluminescent flora.',
    levelRange: '1 — 5',
    sky: 'linear-gradient(180deg, #0d2030 0%, #1d3a2a 100%)',
    floor: 'linear-gradient(180deg, transparent 65%, rgba(20,60,30,0.7) 100%)',
    skyColor: '#14322a',
    nodes: [
      { x: 8,  y: 50, type: 'start', label: 'Trailhead', icon: '🌿' },
      { x: 24, y: 38, type: 'wild',  label: 'Tall Grass', icon: '🌾', tier: 'basic' },
      { x: 24, y: 62, type: 'wild',  label: 'Tall Grass', icon: '🌾', tier: 'basic' },
      { x: 42, y: 50, type: 'cache', label: 'Mossy Cache', icon: '🎁' },
      { x: 58, y: 36, type: 'npc',   label: 'Conductor Vale', icon: '🎩', team: 'flora' },
      { x: 58, y: 64, type: 'wild',  label: 'Tall Grass', icon: '🌾', tier: 'mid' },
      { x: 75, y: 50, type: 'heal',  label: 'Healing Spring', icon: '💧' },
      { x: 92, y: 50, type: 'gate',  label: 'Verdant Warden', icon: '👑', boss: 'warden' }
    ],
    links: [[0,1],[0,2],[1,3],[2,3],[3,4],[3,5],[4,6],[5,6],[6,7]]
  },
  {
    id: 'sunken',
    name: 'The Sunken Grotto',
    subtitle: 'A flooded cavern echoing with tidal hymns.',
    levelRange: '5 — 10',
    sky: 'linear-gradient(180deg, #08182e 0%, #10345a 100%)',
    floor: 'linear-gradient(180deg, transparent 65%, rgba(15,40,80,0.7) 100%)',
    skyColor: '#0a2540',
    nodes: [
      { x: 8,  y: 50, type: 'start', label: 'Grotto Mouth', icon: '🌊' },
      { x: 22, y: 36, type: 'wild',  label: 'Tidepools', icon: '🐚', tier: 'mid' },
      { x: 22, y: 64, type: 'cache', label: 'Pearl Cache', icon: '🦪' },
      { x: 40, y: 50, type: 'wild',  label: 'Kelp Forest', icon: '🌊', tier: 'mid' },
      { x: 56, y: 36, type: 'npc',   label: 'Conductor Marin', icon: '🎩', team: 'hydro' },
      { x: 56, y: 64, type: 'wild',  label: 'Deep Trench', icon: '🐙', tier: 'rare' },
      { x: 72, y: 50, type: 'heal',  label: 'Luminous Spring', icon: '✨' },
      { x: 88, y: 50, type: 'gate',  label: 'Tidal Sovereign', icon: '🌊', boss: 'sovereign' }
    ],
    links: [[0,1],[0,2],[1,3],[2,3],[3,4],[3,5],[4,6],[5,6],[6,7]]
  },
  {
    id: 'crags',
    name: 'The Obsidian Crags',
    subtitle: 'A jagged range where thunder meets magma.',
    levelRange: '10 — 15',
    sky: 'linear-gradient(180deg, #1a0a0a 0%, #2a1018 100%)',
    floor: 'linear-gradient(180deg, transparent 65%, rgba(80,20,10,0.6) 100%)',
    skyColor: '#2a0a18',
    nodes: [
      { x: 8,  y: 50, type: 'start', label: 'Crag Foot', icon: '⛰️' },
      { x: 22, y: 38, type: 'wild',  label: 'Lava Fields', icon: '🌋', tier: 'mid' },
      { x: 22, y: 62, type: 'cache', label: 'Volcanic Cache', icon: '💎' },
      { x: 40, y: 50, type: 'wild',  label: 'Storm Spire', icon: '⚡', tier: 'rare' },
      { x: 56, y: 36, type: 'npc',   label: 'Conductor Vex', icon: '🎩', team: 'pyro' },
      { x: 56, y: 64, type: 'wild',  label: 'Obsidian Path', icon: '🪨', tier: 'mid' },
      { x: 72, y: 50, type: 'heal',  label: 'Magma Vent', icon: '🔥' },
      { x: 88, y: 50, type: 'gate',  label: 'Cinder Sovereign', icon: '👑', boss: 'cinder' }
    ],
    links: [[0,1],[0,2],[1,3],[2,3],[3,4],[3,5],[4,6],[5,6],[6,7]]
  },
  {
    id: 'apex',
    name: 'The Apex Pillar',
    subtitle: 'The culmination. A spire of pure resonance.',
    levelRange: '15+',
    sky: 'linear-gradient(180deg, #1a0a3a 0%, #000000 100%)',
    floor: 'linear-gradient(180deg, transparent 65%, rgba(60,20,90,0.7) 100%)',
    skyColor: '#1a0a3a',
    nodes: [
      { x: 8,  y: 50, type: 'start', label: 'Sanctum Gate', icon: '🏛️' },
      { x: 24, y: 50, type: 'wild',  label: 'Wraith Hall', icon: '👻', tier: 'elite' },
      { x: 42, y: 50, type: 'heal',  label: 'Aether Font', icon: '✨' },
      { x: 60, y: 50, type: 'wild',  label: 'Crown of Echoes', icon: '🔮', tier: 'elite' },
      { x: 78, y: 50, type: 'cache', label: 'Pillar Cache', icon: '💠' },
      { x: 92, y: 50, type: 'gate',  label: 'The Conductor Supreme', icon: '🌟', boss: 'apex' }
    ],
    links: [[0,1],[1,2],[2,3],[3,4],[4,5]]
  }
];

// ── const: BOSSES ──
// Boss definitions: a team of 1-3 Resonants.
const BOSSES = {
  warden: {
    name: 'Verdant Warden',
    title: 'Guardian of the Canopy',
    portrait: { type: 'flora', shape: 'bloomgoliath' },
    dialog: [
      'Ah, a young Conductor enters my grove.',
      'The forest judges those who would exploit it. Show me your heart — and your Resonants.',
      'Prepare yourself.'
    ],
    team: [{ species: 'thornback', level: 6 }, { species: 'bloomgoliath', level: 7 }],
    reward: { shards: 80, xp: 60 }
  },
  sovereign: {
    name: 'Tidal Sovereign',
    title: 'Lord of the Sunken Grotto',
    portrait: { type: 'hydro', shape: 'neptunian' },
    dialog: [
      'You have descended into the deep.',
      'The tides have whispered of your coming. I am the song they sing.',
      'Let us see if your bond runs deeper than the abyss.'
    ],
    team: [{ species: 'geoxus', level: 11 }, { species: 'neptunian', level: 12 }],
    reward: { shards: 160, xp: 110 }
  },
  cinder: {
    name: 'Cinder Sovereign',
    title: 'Master of the Obsidian Crags',
    portrait: { type: 'pyro', shape: 'ignisaur' },
    dialog: [
      'You climb the Crags? Bold. Or foolish.',
      'The mountain does not forgive the timid. Burn brightly, or be buried.'
    ],
    team: [{ species: 'ignisaur', level: 14 }, { species: 'stormlord', level: 15 }],
    reward: { shards: 280, xp: 180 }
  },
  apex: {
    name: 'The Conductor Supreme',
    title: 'Harmonist of Aethelgard',
    portrait: { type: 'cosmos', shape: 'stormlord' },
    dialog: [
      '...',
      'You have climbed the Pillar. Few have done so. None have climbed it alone.',
      'I have waited centuries for a resonance pure enough to harmonize with.',
      'Strike me with everything you are — and become whole.'
    ],
    team: [
      { species: 'neptunian', level: 17 },
      { species: 'ignisaur', level: 18 },
      { species: 'stormlord', level: 19 }
    ],
    reward: { shards: 600, xp: 400 }
  }
};

// ── const: ZONE_UNLOCKS ──
// Zone unlock requirements
const ZONE_UNLOCKS = {
  verdant: { level: 1, requiresClear: null },
  sunken:  { level: 5, requiresClear: 'verdant' },
  crags:   { level: 10, requiresClear: 'sunken' },
  apex:    { level: 15, requiresClear: 'crags' }
};

// ── const: SHOP_ITEMS ──
// Shop inventory
const SHOP_ITEMS = [
  { id: 'prism_basic',  name: 'Harmonizer Prism',      desc: 'A basic capture matrix. 1× capture rate.', cost: 50,  type: 'prism',  tier: 1, icon: '🔷' },
  { id: 'prism_super',  name: 'Hyper-Conductive Prism', desc: 'A premium prism. 1.7× capture rate.',     cost: 180, type: 'prism',  tier: 2, icon: '🔶' },
  { id: 'prism_ultra',  name: 'Eternal Aether Prism',   desc: 'A mythic prism. 2.5× capture rate.',     cost: 500, type: 'prism',  tier: 3, icon: '💎' },
  { id: 'heal_half',    name: 'Core Restorer',          desc: 'Restores 50% HP to a Resonant.',          cost: 80,  type: 'heal',   amount: 0.5, icon: '💖' },
  { id: 'heal_full',    name: 'Vital Elixir',           desc: 'Fully restores a Resonant.',              cost: 220, type: 'heal',   amount: 1.0, icon: '💗' },
  { id: 'cure',         name: 'Resonance Elixir',       desc: 'Cures all status conditions.',            cost: 90,  type: 'cure',   icon: '🧪' },
  { id: 'revive',       name: 'Phoenix Feather',        desc: 'Revives a fainted Resonant to half HP.',  cost: 250, type: 'revive', icon: '🪶' }
];

// ── const: SAVE_KEY ──
// Player state, save/load, and instance creation.
const SAVE_KEY = 'aether_resonance_save_v1';

// ── function: newPlayer ──
function newPlayer() {
  const ember = RESONANTS_BY_ID.emberkit;
  const stats = statsForSpecies(ember, 5);
  return {
    name: 'Conductor',
    shards: 30,
    party: [
      createResonantInstance(ember, 5, true)
    ],
    storage: [], // captured but not in active party
    inventory: { prism_basic: 5, prism_super: 0, prism_ultra: 0, heal_half: 2, heal_full: 0, cure: 1, revive: 0 },
    zoneProgress: {
      verdant: { currentNode: 0, completed: [] },
      sunken:  { currentNode: 0, completed: [] },
      crags:   { currentNode: 0, completed: [] },
      apex:    { currentNode: 0, completed: [] }
    },
    bossesCleared: [],
    dex: { seen: ['emberkit'], caught: ['emberkit'] },
    playtime: 0
  };
}

// ── function: createResonantInstance ──
function createResonantInstance(species, level, isLeader) {
  const s = statsForSpecies(species, level);
  return {
    uid: 'r' + Date.now() + Math.floor(Math.random() * 100000),
    speciesId: species.id,
    nickname: species.name,
    level: level,
    xp: 0,
    hp: s.maxHp,
    maxHp: s.maxHp,
    atk: s.atk,
    def: s.def,
    spd: s.spd,
    moves: species.moves.filter(m => m.unlock <= level).map(m => ({
      id: m.id,
      pp: MOVES[m.id].pp,
      maxPp: MOVES[m.id].pp
    })),
    status: null, // 'poison', 'sleep', 'paralysis', 'burn'
    isLeader: !!isLeader
  };
}

// ── function: savePlayer ──
function savePlayer() {
  try {
    localStorage.setItem(SAVE_KEY, JSON.stringify(STATE.player));
    return true;
  } catch (e) {
    console.error('Save failed', e);
    return false;
  }
}

// ── function: loadPlayer ──
function loadPlayer() {
  try {
    const raw = localStorage.getItem(SAVE_KEY);
    if (!raw) return null;
    const data = JSON.parse(raw);
    return data;
  } catch (e) {
    console.error('Load failed', e);
    return null;
  }
}

// ── function: resetPlayer ──
function resetPlayer() {
  localStorage.removeItem(SAVE_KEY);
  STATE.player = newPlayer();
}

// ── function: addShards ──
function addShards(n) {
  STATE.player.shards = Math.max(0, STATE.player.shards + n);
  savePlayer();
}

// ── function: addItem ──
function addItem(id, qty) {
  if (!STATE.player.inventory[id]) STATE.player.inventory[id] = 0;
  STATE.player.inventory[id] += qty;
  savePlayer();
}

// ── function: discoverResonant ──
function discoverResonant(speciesId) {
  if (!STATE.player.dex.seen.includes(speciesId)) {
    STATE.player.dex.seen.push(speciesId);
  }
}

// ── function: caughtResonant ──
function caughtResonant(speciesId) {
  if (!STATE.player.dex.caught.includes(speciesId)) {
    STATE.player.dex.caught.push(speciesId);
  }
  discoverResonant(speciesId);
}

// ── const: STATE ──
// Global UI state.
const STATE = {
  player: null,
  screen: 'menu',
  // battle state
  battle: null, // { type, enemy: { species, level, hp, maxHp, ... }, playerActive, log[], awaitingInput, turn, etc. }
  // input/submenus
  submenu: null, // 'moves' | 'party' | 'inventory' | null
  selectedItem: null,
  // overworld
  currentZone: 'verdant',
  // misc
  awaitingConfirm: null,
  lastInteraction: 0
};

// ── function: showScreen ──
function showScreen(name) {
  document.querySelectorAll('.screen').forEach(s => s.classList.add('hidden'));
  const el = document.getElementById('screen-' + name);
  if (el) el.classList.remove('hidden');
  STATE.screen = name;
}

// ── function: toast ──
function toast(msg, dur) {
  const layer = document.getElementById('toast-layer');
  if (!layer) return;
  const t = document.createElement('div');
  t.className = 'toast';
  t.textContent = msg;
  layer.appendChild(t);
  setTimeout(() => t.remove(), (dur || 2.8) * 1000);
}

// ── function: shakeElement ──
function shakeElement(el) {
  if (!el) return;
  el.classList.remove('fx-shake');
  void el.offsetWidth;
  el.classList.add('fx-shake');
  setTimeout(() => el.classList.remove('fx-shake'), 500);
}

// ── function: flashElement ──
function flashElement(el) {
  if (!el) return;
  const f = document.createElement('div');
  f.className = 'fx-flash';
  document.body.appendChild(f);
  setTimeout(() => f.remove(), 400);
}

// ── function: emitParticles ──
function emitParticles(color, count, originEl) {
  const layer = document.getElementById('fx-layer');
  if (!layer) return;
  const rect = originEl ? originEl.getBoundingClientRect() : { left: window.innerWidth/2 - 50, top: window.innerHeight/2 - 50, width: 100, height: 100 };
  for (let i = 0; i < count; i++) {
    const p = document.createElement('div');
    p.className = 'particle';
    p.style.left = (rect.left + Math.random() * rect.width) + 'px';
    p.style.top = (rect.top + Math.random() * rect.height) + 'px';
    p.style.background = color;
    p.style.boxShadow = '0 0 8px ' + color;
    p.style.animationDuration = (1.2 + Math.random() * 0.8) + 's';
    layer.appendChild(p);
    setTimeout(() => p.remove(), 2000);
  }
}

// ── function: elThemed ──
function elThemed(type, extraClass) {
  return 'el-' + type + (extraClass ? ' ' + extraClass : '');
}

// ── function: typePill ──
function typePill(type) {
  const info = ELEMENT_INFO[type];
  return `<span class="type-pill" style="color:${info.color1}"><span class="dot"></span>${info.name}</span>`;
}

// ── function: el ──
function el(type, content) {
  return `<div class="el-${type}">${content}</div>`;
}

// ── function: speciesForEncounter ──
// Encounter generation by zone/tier.
function speciesForEncounter(zoneId, tier) {
  // Each zone has a pool of species appropriate to its theme.
  const pool = {
    verdant: ['seedling', 'gusty', 'pebble'],
    sunken:  ['droplet', 'seedling', 'pebble', 'sparky'],
    crags:   ['emberkit', 'sparky', 'pebble', 'droplet'],
    apex:    ['ignisaur', 'neptunian', 'stormlord', 'zephyrus', 'terrasunder', 'bloomgoliath']
  }[zoneId] || ['emberkit'];
  const t = tier || 'basic';
  let candidates = pool;
  if (t === 'basic') {
    candidates = pool.filter(id => RESONANTS_BY_ID[id].evolvesTo !== null && RESONANTS_BY_ID[id].evolvesTo <= 8);
    if (!candidates.length) candidates = pool;
  } else if (t === 'mid') {
    candidates = pool.filter(id => RESONANTS_BY_ID[id].evolvesTo !== null);
  } else if (t === 'rare' || t === 'elite') {
    candidates = pool;
  }
  return RESONANTS_BY_ID[candidates[Math.floor(Math.random() * candidates.length)]];
}

// ── function: levelForZone ──
function levelForZone(zoneId, tier) {
  const ranges = {
    verdant: { basic: [2, 4],  mid: [4, 5] },
    sunken:  { basic: [6, 8],  mid: [8, 10], rare: [9, 10] },
    crags:   { basic: [10, 12],mid: [12, 14],rare: [13, 15] },
    apex:    { elite: [16, 19] }
  };
  const r = (ranges[zoneId] || {})[tier || 'basic'] || [1, 5];
  return r[0] + Math.floor(Math.random() * (r[1] - r[0] + 1));
}

// ── function: createEnemyEncounter ──
function createEnemyEncounter(zoneId, tier) {
  const species = speciesForEncounter(zoneId, tier);
  const level = levelForZone(zoneId, tier);
  return createResonantInstance(species, level, false);
}

// ── function: createBossEncounter ──
function createBossEncounter(bossId) {
  const boss = BOSSES[bossId];
  return {
    bossId,
    name: boss.name,
    team: boss.team.map(s => createResonantInstance(RESONANTS_BY_ID[s.species], s.level, false)),
    active: 0,
    dialog: boss.dialog
  };
}

// ── function: createNpcEncounter ──
function createNpcEncounter(zoneId, teamType) {
  const pool = {
    flora:   ['seedling', 'thornback'],
    hydro:   ['droplet', 'geoxus'],
    pyro:    ['emberkit', 'pyrowolf'],
    zephyr:  ['gusty', 'slipstream'],
    earth:   ['pebble', 'obsidian'],
    volt:    ['sparky', 'boltsaber']
  }[teamType] || ['emberkit'];
  const species = RESONANTS_BY_ID[pool[Math.floor(Math.random() * pool.length)]];
  const level = levelForZone(zoneId, 'mid') + 1;
  return {
    name: 'Wandering Conductor',
    team: [createResonantInstance(species, level, false), createResonantInstance(species, level, false)],
    active: 0,
    reward: { shards: 30, xp: 30 }
  };
}

// ── function: startBattle ──
// Battle engine: turn-based 1v1, with optional multi-enemy bosses.
function startBattle(opts) {
  // opts = { kind: 'wild'|'npc'|'boss', enemy, returnTo, captureEnabled, escapeEnabled, bossId?, zoneId?, tier? }
  const b = {
    kind: opts.kind,
    enemy: opts.enemy, // instance OR boss obj
    bossId: opts.bossId || null,
    zoneId: opts.zoneId || STATE.currentZone,
    tier: opts.tier || 'basic',
    returnTo: opts.returnTo || 'overworld',
    captureEnabled: opts.captureEnabled !== false,
    escapeEnabled: opts.escapeEnabled !== false,
    turn: 0, // 0 = player, 1 = enemy
    phase: 'await', // 'await' | 'anim' | 'between' | 'end'
    log: [],
    reward: opts.reward || { shards: 8, xp: 12 },
    bossDefeated: false
  };
  STATE.battle = b;
  showScreen('battle');
  synth.resume();
  battleLog('A wild encounter begins!');
  battleRender();
}

// ── function: getBattleEnemy ──
function getBattleEnemy() {
  const b = STATE.battle;
  if (!b) return null;
  if (b.kind === 'boss') return b.enemy.team[b.enemy.active];
  if (b.kind === 'npc')  return b.enemy.team[b.enemy.active];
  return b.enemy;
}

// ── function: getPlayerActive ──
function getPlayerActive() {
  const p = STATE.player.party;
  return p[0];
}

// ── function: nextHealthyInParty ──
function nextHealthyInParty() {
  return STATE.player.party.find(r => r.hp > 0);
}

// ── function: battleLog ──
function battleLog(text, cls) {
  const b = STATE.battle;
  if (!b) return;
  b.log.push({ text, cls: cls || '' });
  if (b.log.length > 8) b.log.shift();
}

// ── function: battleClearLog ──
function battleClearLog() {
  if (STATE.battle) STATE.battle.log = [];
}

// ── function: getMoveList ──
function getMoveList(resonantInstance) {
  const species = RESONANTS_BY_ID[resonantInstance.speciesId];
  return resonantInstance.moves.map(m => {
    const def = MOVES[m.id];
    return { ...def, id: m.id, pp: m.pp, maxPp: m.maxPp };
  });
}

// ── function: enemyChooseMove ──
function enemyChooseMove() {
  const enemy = getBattleEnemy();
  const moves = getMoveList(enemy).filter(m => m.pp > 0);
  if (!moves.length) return null;
  // Simple AI: prefer super-effective moves; else highest power.
  const player = getPlayerActive();
  if (!player) return moves[0];
  const playerType = RESONANTS_BY_ID[player.speciesId].type;
  let best = moves[0];
  let bestScore = -1;
  for (const m of moves) {
    const eff = typeEffectiveness(m.type, playerType);
    const score = m.power * eff + Math.random() * 5;
    if (score > bestScore) { bestScore = score; best = m; }
  }
  return best;
}

// ── function: tryAttack ──
function tryAttack(attacker, defender, move, attackerIsPlayer) {
  const defSpecies = RESONANTS_BY_ID[defender.speciesId];
  const atkSpecies = RESONANTS_BY_ID[attacker.speciesId];
  const logPrefix = (attackerIsPlayer ? 'Your ' : 'Enemy ') + atkSpecies.name;
  // Accuracy check
  if (Math.random() * 100 > move.acc) {
    battleLog(`${logPrefix} used <span class="accent">${move.name}</span>... but it missed!`, 'accent');
    return { hit: false };
  }
  // Status prevention
  if (attacker.status === 'sleep' && !attackerIsPlayer) {
    if (Math.random() < 0.5) {
      battleLog(`${logPrefix} is fast asleep.`, 'accent');
      return { hit: false };
    } else {
      battleLog(`${logPrefix} woke up!`, 'good');
      attacker.status = null;
    }
  }
  if (attacker.status === 'paralysis' && !attackerIsPlayer) {
    if (Math.random() < 0.25) {
      battleLog(`${logPrefix} is paralyzed and can't move!`, 'accent');
      return { hit: false };
    }
  }
  // PP cost
  const slot = attacker.moves.find(m => m.id === move.id);
  if (slot) slot.pp = Math.max(0, slot.pp - 1);

  // Status move?
  if (STATUS_MOVES[move.id]) {
    const status = STATUS_MOVES[move.id];
    const targetStatus = attackerIsPlayer ? defender.status : defender.status;
    if (targetStatus) {
      battleLog(`${logPrefix} used <span class="accent">${move.name}</span>... but it failed.`);
    } else {
      defender.status = status;
      const map = { poison: 'poisoned', sleep: 'put to sleep', paralysis: 'paralyzed' };
      battleLog(`${logPrefix} used <span class="accent">${move.name}</span>! ${defSpecies.name} is ${map[status]}!`, 'accent');
    }
    return { hit: true, status: true };
  }

  // Damage calc
  const atkStat = attacker.atk;
  const defStat = defender.def;
  const level = attacker.level;
  let power = move.power || 0;
  let damage = (((2 * level / 5 + 2) * power * (atkStat / defStat)) / 50 + 2);
  const eff = typeEffectiveness(move.type, defSpecies.type);
  damage *= eff;
  // Crit
  let crit = Math.random() < 0.10;
  if (crit) damage *= 1.5;
  // Random
  damage *= (0.85 + Math.random() * 0.15);
  // STAB
  if (move.type === atkSpecies.type) damage *= 1.15;
  damage = Math.max(1, Math.floor(damage));
  defender.hp = Math.max(0, defender.hp - damage);
  // FX feedback
  if (crit) battleLog(`${logPrefix} used <span class="accent">${move.name}</span>! A <span class="good">CRITICAL HIT</span>!`, 'crit');
  else if (eff > 1.4) battleLog(`${logPrefix} used <span class="accent">${move.name}</span>! It's <span class="good">super effective</span>!`, 'super');
  else if (eff < 0.7) battleLog(`${logPrefix} used <span class="accent">${move.name}</span>! It's not very effective...`, 'weak');
  else battleLog(`${logPrefix} used <span class="accent">${move.name}</span>!`, 'normal');
  battleLog(`${defSpecies.name} took <b>${damage}</b> damage.`, 'dmg');
  return { hit: true, dmg: damage, crit, eff, fainted: defender.hp <= 0 };
}

// ── function: applyEndOfTurnStatus ──
function applyEndOfTurnStatus(resonant, label) {
  if (!resonant || resonant.hp <= 0) return;
  if (resonant.status === 'poison') {
    const dmg = Math.max(1, Math.floor(resonant.maxHp / 8));
    resonant.hp = Math.max(0, resonant.hp - dmg);
    battleLog(`${label} is hurt by poison! (-${dmg})`, 'bad');
  }
}

// ── function: tryCapture ──
function tryCapture(prismTier) {
  const enemy = getBattleEnemy();
  const ballMods = { 1: 1.0, 2: 1.7, 3: 2.5 };
  const mod = ballMods[prismTier] || 1.0;
  const hpRatio = enemy.hp / enemy.maxHp;
  let rate = ((1 - hpRatio) * 0.7 + 0.1) * mod;
  if (enemy.status === 'sleep' || enemy.status === 'paralysis') rate *= 1.2;
  if (enemy.status === 'poison') rate *= 1.1;
  rate = Math.min(0.95, rate);
  battleLog(`You throw a Prism... (capture rate ${Math.round(rate*100)}%)`, 'accent');
  return rate;
}

// ── function: resolveCapture ──
function resolveCapture(rate) {
  const enemy = getBattleEnemy();
  return new Promise(resolve => {
    setTimeout(() => {
      if (Math.random() < rate) {
        synth.captureSuccess();
        battleLog(`<span class="good">Gotcha!</span> ${RESONANTS_BY_ID[enemy.speciesId].name} was captured!`, 'good');
        // Add to storage or party
        caughtResonant(enemy.speciesId);
        const inst = createResonantInstance(RESONANTS_BY_ID[enemy.speciesId], enemy.level, false);
        inst.hp = enemy.hp;
        inst.status = enemy.status;
        inst.nickname = RESONANTS_BY_ID[enemy.speciesId].name;
        if (STATE.player.party.length < 3) {
          STATE.player.party.push(inst);
          battleLog(`${inst.nickname} joined your active party!`, 'good');
        } else {
          STATE.player.storage.push(inst);
          battleLog(`${inst.nickname} was sent to your Storage.`, 'good');
        }
        // XP for the active
        const xpGained = 30 + enemy.level * 4;
        giveXpToActive(xpGained);
        addShards(10);
        STATE.battle.phase = 'end';
        STATE.battle.captured = true;
        battleRender();
        setTimeout(() => endBattle('captured'), 1200);
        resolve(true);
      } else {
        synth.captureFail();
        battleLog(`The Prism failed... ${RESONANTS_BY_ID[enemy.speciesId].name} broke free!`, 'bad');
        // Enemy gets a free attack
        setTimeout(() => {
          enemyTurn();
          resolve(false);
        }, 700);
      }
    }, 800);
  });
}

// ── function: enemyTurn ──
function enemyTurn() {
  const b = STATE.battle;
  if (!b || b.phase === 'end') return;
  const enemy = getBattleEnemy();
  if (!enemy || enemy.hp <= 0) {
    onEnemyFaint();
    return;
  }
  const move = enemyChooseMove();
  if (!move) {
    battleLog(`${RESONANTS_BY_ID[enemy.speciesId].name} has no moves!`, 'bad');
    playerTurnBegin();
    return;
  }
  battleRender();
  setTimeout(() => {
    const player = getPlayerActive();
    if (!player) { battleEndDefeat(); return; }
    if (player.status === 'sleep') {
      if (Math.random() < 0.5) {
        battleLog(`${RESONANTS_BY_ID[player.speciesId].name} is fast asleep.`, 'accent');
        applyEndOfTurnStatus(enemy, RESONANTS_BY_ID[enemy.speciesId].name);
        applyEndOfTurnStatus(player, RESONANTS_BY_ID[player.speciesId].name);
        if (enemy.hp <= 0) onEnemyFaint();
        else if (player.hp <= 0) onPlayerFaint();
        else playerTurnBegin();
        return;
      } else {
        battleLog(`${RESONANTS_BY_ID[player.speciesId].name} woke up!`, 'good');
        player.status = null;
      }
    }
    if (player.status === 'paralysis' && Math.random() < 0.25) {
      battleLog(`${RESONANTS_BY_ID[player.speciesId].name} is paralyzed!`, 'accent');
      applyEndOfTurnStatus(enemy, RESONANTS_BY_ID[enemy.speciesId].name);
      applyEndOfTurnStatus(player, RESONANTS_BY_ID[player.speciesId].name);
      if (enemy.hp <= 0) onEnemyFaint();
      else if (player.hp <= 0) onPlayerFaint();
      else playerTurnBegin();
      return;
    }
    const result = tryAttack(enemy, player, move, false);
    playHitSfx(result, RESONANTS_BY_ID[enemy.speciesId].type);
    playHitAnim('player', result);
    applyEndOfTurnStatus(player, RESONANTS_BY_ID[player.speciesId].name);
    battleRender();
    if (player.hp <= 0) {
      setTimeout(() => onPlayerFaint(), 700);
    } else {
      setTimeout(() => playerTurnBegin(), 700);
    }
  }, 350);
}

// ── function: playerTurnBegin ──
function playerTurnBegin() {
  const b = STATE.battle;
  if (!b) return;
  b.turn = 0;
  b.phase = 'await';
  battleRender();
}

// ── function: onEnemyFaint ──
function onEnemyFaint() {
  const b = STATE.battle;
  if (!b) return;
  const enemy = getBattleEnemy();
  if (enemy) {
    synth.faint();
    battleLog(`${RESONANTS_BY_ID[enemy.speciesId].name} fainted!`, 'good');
  }
  // Multi-enemy (boss / npc)
  if ((b.kind === 'boss' || b.kind === 'npc') && b.enemy.active < b.enemy.team.length - 1) {
    b.enemy.active += 1;
    battleLog(`The foe sends out the next!`, 'accent');
    setTimeout(() => { playerTurnBegin(); }, 1100);
  } else {
    // Victory
    const reward = b.kind === 'boss' ? BOSSES[b.bossId].reward
                  : b.kind === 'npc' ? b.enemy.reward
                  : b.reward;
    addShards(reward.shards);
    giveXpToActive(reward.xp);
    if (b.kind === 'boss') {
      if (!STATE.player.bossesCleared.includes(b.bossId)) {
        STATE.player.bossesCleared.push(b.bossId);
      }
      synth.victory();
    }
    battleLog(`You earned <b>${reward.shards} Aether Shards</b> and <b>${reward.xp} XP</b>!`, 'good');
    STATE.battle.phase = 'end';
    battleRender();
    setTimeout(() => endBattle('victory'), 1200);
  }
}

// ── function: onPlayerFaint ──
function onPlayerFaint() {
  const b = STATE.battle;
  if (!b) return;
  synth.faint();
  // Auto swap to next healthy
  const next = nextHealthyInParty();
  if (next) {
    STATE.player.party = STATE.player.party.filter(r => r !== getPlayerActive()).concat([next]);
    battleLog(`Go! ${RESONANTS_BY_ID[next.speciesId].name}!`, 'accent');
    setTimeout(() => enemyTurn(), 1000);
  } else {
    synth.defeat();
    battleLog('All your Resonants have fainted...', 'bad');
    STATE.battle.phase = 'end';
    battleRender();
    setTimeout(() => endBattle('defeat'), 1200);
  }
}

// ── function: giveXpToActive ──
function giveXpToActive(amount) {
  const active = getPlayerActive();
  if (!active) return;
  active.xp += amount;
  while (active.xp >= xpToNext(active.level)) {
    active.xp -= xpToNext(active.level);
    active.level += 1;
    const s = statsForSpecies(RESONANTS_BY_ID[active.speciesId], active.level);
    active.maxHp = s.maxHp; active.atk = s.atk; active.def = s.def; active.spd = s.spd;
    active.hp = s.maxHp;
    // New moves?
    const species = RESONANTS_BY_ID[active.speciesId];
    species.moves.forEach(m => {
      if (m.unlock === active.level && !active.moves.find(am => am.id === m.id)) {
        active.moves.push({ id: m.id, pp: MOVES[m.id].pp, maxPp: MOVES[m.id].pp });
        battleLog('<span class="good">' + active.nickname + ' learned ' + MOVES[m.id].name + '!</span>', 'good');
      }
    });
    synth.levelUp();
    battleLog('<span class="good">' + active.nickname + ' grew to Lv. ' + active.level + '!</span>', 'good');
    // Evolution
    if (species.evolvesTo && active.level >= species.evolvesTo && species.evolvesInto) {
      const newId = species.evolvesInto;
      battleLog('<span class="accent">' + active.nickname + ' is evolving into ' + RESONANTS_BY_ID[newId].name + '!</span>', 'accent');
      active.speciesId = newId;
      active.nickname = RESONANTS_BY_ID[newId].name;
      const newStats = statsForSpecies(RESONANTS_BY_ID[newId], active.level);
      active.maxHp = newStats.maxHp; active.atk = newStats.atk; active.def = newStats.def; active.spd = newStats.spd;
      active.hp = newStats.maxHp;
      const newMoves = RESONANTS_BY_ID[newId].moves.filter(m => m.unlock <= active.level);
      active.moves = newMoves.map(m => ({ id: m.id, pp: MOVES[m.id].pp, maxPp: MOVES[m.id].pp }));
      synth.evolve();
      battleLog('<span class="good">' + active.nickname + ' evolved!</span>', 'good');
    }
  }
  savePlayer();
}

// ── function: endBattle ──
function endBattle(result) {
  const b = STATE.battle;
  if (!b) return;
  if (result === 'victory' || result === 'captured') {
    if (b.origNodeIndex !== undefined) {
      const zp = STATE.player.zoneProgress[b.zoneId || STATE.currentZone];
      if (zp && !zp.completed.includes(b.origNodeIndex)) {
        zp.completed.push(b.origNodeIndex);
      }
    }
    savePlayer();
    STATE.battle = null;
    // Special victory for apex boss
    if (b.kind === 'boss' && b.bossId === 'apex') {
      renderFinalVictory();
      showScreen('victory');
    } else {
      showScreen('overworld');
      renderOverworld();
    }
  } else if (result === 'defeat') {
    showScreen('defeat');
    renderDefeat();
  } else if (result === 'fled') {
    STATE.battle = null;
    showScreen('overworld');
    renderOverworld();
  }
}

// ── function: battleEndDefeat ──
function battleEndDefeat() {
  showScreen('defeat');
  renderDefeat();
}

// ── function: renderMenu ──
// Menu screen renderer.
function renderMenu() {
  const screen = document.getElementById('screen-menu');
  const hasSave = !!loadPlayer();
  const logoSvg = `<svg viewBox="0 0 130 130" xmlns="http://www.w3.org/2000/svg">
    <defs>
      <radialGradient id="mlg" cx="50%" cy="50%"><stop offset="0%" stop-color="#e0c3ff"/><stop offset="60%" stop-color="#7c4dff"/><stop offset="100%" stop-color="#2a1858"/></radialGradient>
      <radialGradient id="mlg2" cx="50%" cy="50%"><stop offset="0%" stop-color="#fff7a3"/><stop offset="100%" stop-color="#ffe04a" stop-opacity="0"/></radialGradient>
    </defs>
    <polygon points="65,12 100,40 100,90 65,118 30,90 30,40" fill="url(#mlg)" stroke="#d4b3ff" stroke-width="1.5"/>
    <polygon points="65,12 100,40 65,68 30,40" fill="rgba(255,255,255,0.2)"/>
    <polygon points="65,68 100,40 100,90 65,118" fill="rgba(0,0,0,0.18)"/>
    <circle cx="65" cy="65" r="22" fill="url(#mlg2)"/>
    <circle cx="65" cy="65" r="6" fill="#fff7a3"/>
  </svg>`;
  screen.innerHTML = `
    <div class="menu-logo-wrap">
      <div class="menu-logo">${logoSvg}</div>
      <div class="menu-title">AETHER RESONANCE</div>
      <div class="menu-subtitle">CONDUCTORS OF AETHELGARD</div>
    </div>
    <div class="menu-buttons">
      ${hasSave
        ? `<button class="btn btn-primary" id="btn-continue">▶  Continue Journey</button>
           <button class="btn" id="btn-newgame">✦  New Journey</button>`
        : `<button class="btn btn-primary" id="btn-newgame">✦  Begin Journey</button>`
      }
      <button class="btn" id="btn-credits">ℹ  About</button>
    </div>
    <div class="menu-meta">v1.0 — auto-saves to local storage</div>
  `;
  const continueBtn = document.getElementById('btn-continue');
  if (continueBtn) continueBtn.addEventListener('click', () => { synth.menuConfirm(); enterGame(); });
  document.getElementById('btn-newgame').addEventListener('click', () => {
    synth.menuConfirm();
    if (hasSave) {
      showConfirmDialog('Start a new journey?', 'Your current save will be replaced.', () => {
        resetPlayer();
        enterGame();
      });
    } else {
      enterGame();
    }
  });
  document.getElementById('btn-credits').addEventListener('click', () => {
    synth.click();
    showDialog({
      speaker: 'Aether Resonance',
      body: 'A single-file HTML RPG built with vanilla JS and SVG vector art. All sound is synthesized live via the Web Audio API. Every Resonant, move, and zone is hand-crafted.',
      actions: [{ label: 'OK', cls: 'btn btn-primary' }]
    });
  });
}

// ── function: enterGame ──
function enterGame() {
  if (!STATE.player) {
    const loaded = loadPlayer();
    STATE.player = loaded || newPlayer();
    savePlayer();
  }
  showScreen('overworld');
  renderOverworld();
}

// ── function: showConfirmDialog ──
function showConfirmDialog(title, body, onYes) {
  const dlg = document.getElementById('screen-dialog');
  dlg.classList.remove('hidden');
  document.querySelectorAll('.screen').forEach(s => { if (s !== dlg) s.classList.add('hidden'); });
  dlg.innerHTML = `
    <div class="dialog-shell">
      <div class="dialog-box glass">
        <div class="speaker">${title}</div>
        <div class="body">${body}</div>
        <div class="actions">
          <button class="btn" id="cf-no">No</button>
          <button class="btn btn-primary" id="cf-yes">Yes</button>
        </div>
      </div>
    </div>`;
  document.getElementById('cf-yes').addEventListener('click', () => { synth.menuConfirm(); dlg.classList.add('hidden'); onYes(); });
  document.getElementById('cf-no').addEventListener('click', () => { synth.cancel(); dlg.classList.add('hidden'); showScreen(STATE._prevScreen || 'overworld'); });
}

// ── function: showDialog ──
function showDialog({ speaker, body, portrait, actions }) {
  const dlg = document.getElementById('screen-dialog');
  STATE._prevScreen = STATE.screen;
  dlg.classList.remove('hidden');
  document.querySelectorAll('.screen').forEach(s => { if (s !== dlg) s.classList.add('hidden'); });
  dlg.innerHTML = `
    <div class="dialog-shell">
      <div class="dialog-box glass">
        ${portrait ? `<div class="portrait">${portrait}</div>` : ''}
        <div class="speaker">${speaker}</div>
        <div class="body">${body}</div>
        <div class="actions">
          ${(actions || [{ label: 'Continue', cls: 'btn btn-primary' }]).map((a, i) => `<button class="${a.cls || 'btn'}" data-idx="${i}">${a.label}</button>`).join('')}
        </div>
      </div>
    </div>`;
  const acts = actions || [{ label: 'Continue', cls: 'btn btn-primary' }];
  dlg.querySelectorAll('button').forEach((btn, i) => {
    btn.addEventListener('click', () => {
      synth.click();
      if (acts[i].onClick) acts[i].onClick();
    });
  });
}

// ── function: renderOverworld ──
// Overworld screen renderer — node map.
function renderOverworld() {
  const screen = document.getElementById('screen-overworld');
  const zone = ZONES.find(z => z.id === STATE.currentZone) || ZONES[0];
  STATE.currentZone = zone.id;
  const zp = STATE.player.zoneProgress[zone.id];
  const isZoneUnlocked = (zid) => {
    const req = ZONE_UNLOCKS[zid];
    if (!req) return false;
    if (req.requiresClear && !STATE.player.bossesCleared.includes(req.requiresClear)) return false;
    return true;
  };

  const zoneTabs = ZONES.map(z => {
    const unlocked = isZoneUnlocked(z.id);
    const active = z.id === zone.id;
    return `<div class="zone-tab ${active ? 'active' : ''} ${unlocked ? '' : 'locked'}" data-zone="${z.id}">
      <span class="zt-name">${z.name.toUpperCase()}</span>
      <span class="zt-range">Lv. ${z.levelRange}${unlocked ? '' : ' — LOCKED'}</span>
    </div>`;
  }).join('');

  // Build nodes
  const nodesHtml = zone.nodes.map((node, i) => {
    const completed = zp.completed.includes(i);
    const isCurrent = !completed && (i === 0 || zp.completed.includes(i - 1) || zone.nodes.slice(0, i).every((n, idx) => zp.completed.includes(idx)));
    const isGate = node.type === 'gate';
    const cleared = isGate && STATE.player.bossesCleared.includes(node.boss);
    const cls = `node ${isCurrent ? 'current' : ''} ${completed ? 'complete' : ''} ${isGate ? 'gate' : ''} ${cleared ? 'cleared' : ''} ${!isCurrent && !completed ? 'locked' : ''}`;
    return `<div class="${cls}" data-node="${i}" style="left:${node.x}%; top:${node.y}%;">
      <div class="node-emoji">${node.icon}</div>
      <div class="node-label">${node.label}</div>
    </div>`;
  }).join('');

  // Build links
  const linksHtml = (zone.links || []).map(([a, b]) => {
    const na = zone.nodes[a], nb = zone.nodes[b];
    const linkComplete = zp.completed.includes(a) || zp.completed.includes(b);
    return `<line class="node-link ${linkComplete ? 'complete' : ''}" x1="${na.x}%" y1="${na.y}%" x2="${nb.x}%" y2="${nb.y}%"/>`;
  }).join('');

  // Zone background decoration
  const bgDecor = (() => {
    if (zone.id === 'verdant') return `<svg class="zone-bg-decor" viewBox="0 0 100 100" preserveAspectRatio="none">
      ${[...Array(20)].map(() => `<circle cx="${Math.random()*100}" cy="${Math.random()*100}" r="${0.3+Math.random()*0.7}" fill="#6cff8e" opacity="${0.3+Math.random()*0.4}"/>`).join('')}
      <path d="M0 80 Q30 70 50 80 Q70 90 100 80 L100 100 L0 100 Z" fill="#1d3a2a" opacity="0.5"/>
    </svg>`;
    if (zone.id === 'sunken') return `<svg class="zone-bg-decor" viewBox="0 0 100 100" preserveAspectRatio="none">
      <path d="M0 60 Q20 50 40 60 Q60 70 80 60 Q90 55 100 60" stroke="#4cc4ff" stroke-width="0.3" fill="none" opacity="0.6"/>
      <path d="M0 70 Q25 60 50 70 Q75 80 100 70" stroke="#4cc4ff" stroke-width="0.3" fill="none" opacity="0.5"/>
      <path d="M0 80 Q30 75 50 80 Q70 85 100 80" stroke="#4cc4ff" stroke-width="0.3" fill="none" opacity="0.4"/>
    </svg>`;
    if (zone.id === 'crags') return `<svg class="zone-bg-decor" viewBox="0 0 100 100" preserveAspectRatio="none">
      <path d="M0 70 L20 50 L30 70 L50 40 L70 70 L100 50 L100 100 L0 100 Z" fill="#1a0a0a" opacity="0.6"/>
      <path d="M0 85 L15 75 L25 85 L40 65 L55 85 L75 70 L100 85 L100 100 L0 100 Z" fill="#0a0505" opacity="0.7"/>
    </svg>`;
    if (zone.id === 'apex') return `<svg class="zone-bg-decor" viewBox="0 0 100 100" preserveAspectRatio="none">
      ${[...Array(40)].map(() => `<circle cx="${Math.random()*100}" cy="${Math.random()*80}" r="${0.1+Math.random()*0.4}" fill="#fff" opacity="${0.3+Math.random()*0.5}"/>`).join('')}
    </svg>`;
    return '';
  })();

  const completed = zp.completed.length;
  const total = zone.nodes.length;
  const partyTop = STATE.player.party[0] ? RESONANTS_BY_ID[STATE.player.party[0].speciesId] : null;

  screen.innerHTML = `
    <div class="overworld-shell">
      <div class="overworld-header glass">
        <div class="stats">
          <div class="stat">
            <div class="label">Conductor</div>
            <div class="val">${STATE.player.name}</div>
          </div>
          <div class="stat">
            <div class="label">Shards</div>
            <div class="val shards">✦ ${STATE.player.shards}</div>
          </div>
          ${partyTop ? `<div class="stat"><div class="label">Lead</div><div class="val" style="color:${ELEMENT_INFO[partyTop.type].color1}">${partyTop.name} Lv.${STATE.player.party[0].level}</div></div>` : ''}
        </div>
        <div style="display:flex; gap:6px;">
          <button class="btn btn-sm" id="btn-dex">📖 Dex</button>
          <button class="btn btn-sm" id="btn-party">🐾 Party</button>
          <button class="btn btn-sm" id="btn-shop">🛒 Shop</button>
          <button class="btn btn-sm btn-danger" id="btn-saveexit">💾 Save</button>
        </div>
      </div>
      <div class="zone-tabs glass">
        ${zoneTabs}
      </div>
      <div class="zone-canvas-wrap glass">
        ${bgDecor}
        <div class="zone-info">
          <div class="zi-title">${zone.name}</div>
          <div class="zi-progress">${completed} / ${total} nodes — ${zone.subtitle}</div>
        </div>
        <svg class="node-trail" viewBox="0 0 100 100" preserveAspectRatio="none">
          ${linksHtml}
        </svg>
        ${nodesHtml}
      </div>
      <div class="overworld-footer">
        <div style="font-size:11px; color:var(--ink-dim); font-family:'Press Start 2P', monospace;">CLICK A NODE TO EXPLORE</div>
      </div>
    </div>
  `;
  document.getElementById('screen-overworld').style.setProperty('--arena-sky', zone.skyColor);
  document.querySelector('.zone-canvas-wrap').style.background = zone.sky;

  // Wire zone tabs
  screen.querySelectorAll('.zone-tab').forEach(tab => {
    tab.addEventListener('click', () => {
      const zid = tab.dataset.zone;
      synth.click();
      if (tab.classList.contains('locked')) {
        const req = ZONE_UNLOCKS[zid];
        toast('Clear the previous Zone\'s Warden first!', 2.4);
        synth.error();
        return;
      }
      STATE.currentZone = zid;
      renderOverworld();
    });
  });

  // Wire nodes
  screen.querySelectorAll('.node').forEach(nodeEl => {
    nodeEl.addEventListener('mouseenter', () => synth.hover());
    nodeEl.addEventListener('click', () => {
      const idx = parseInt(nodeEl.dataset.node);
      const node = zone.nodes[idx];
      if (nodeEl.classList.contains('locked')) {
        synth.error();
        toast('Reach the previous node first!', 2);
        return;
      }
      synth.select();
      handleNode(zone, node, idx);
    });
  });

  // Wire top buttons
  document.getElementById('btn-dex').addEventListener('click', () => { synth.click(); renderDex(); showScreen('dex'); });
  document.getElementById('btn-party').addEventListener('click', () => { synth.click(); renderParty(); showScreen('party'); });
  document.getElementById('btn-shop').addEventListener('click', () => { synth.click(); renderShop(); showScreen('shop'); });
  document.getElementById('btn-saveexit').addEventListener('click', () => { synth.click(); savePlayer(); toast('Game saved.', 1.6); renderMenu(); showScreen('menu'); });
}

// ── function: handleNode ──
function handleNode(zone, node, idx) {
  const zp = STATE.player.zoneProgress[zone.id];
  switch (node.type) {
    case 'start': {
      // Mark the start complete and refresh
      if (!zp.completed.includes(idx)) zp.completed.push(idx);
      savePlayer();
      synth.click();
      renderOverworld();
      toast('Your journey begins. Pick a path!', 2.4);
      break;
    }
    case 'wild': {
      const enemy = createEnemyEncounter(zone.id, node.tier);
      STATE.battle = null;
      startBattle({
        kind: 'wild',
        enemy: enemy,
        returnTo: 'overworld',
        zoneId: zone.id,
        tier: node.tier || 'basic',
        reward: { shards: 8 + (node.tier === 'rare' ? 6 : node.tier === 'elite' ? 12 : 0) + Math.floor(enemy.level * 1.2), xp: 12 + enemy.level * 2 },
        captureEnabled: true,
        escapeEnabled: true
      });
      STATE.battle.origNodeIndex = idx;
      discoverResonant(enemy.speciesId);
      break;
    }
    case 'cache': {
      const rewards = [
        { shards: 25, item: null },
        { shards: 40, item: null },
        { shards: 0, item: 'prism_basic', qty: 2 },
        { shards: 0, item: 'heal_half', qty: 2 },
        { shards: 0, item: 'prism_super', qty: 1 },
        { shards: 60, item: null }
      ];
      const r = rewards[Math.floor(Math.random() * rewards.length)];
      let msg = 'Mystery Cache found! ';
      if (r.shards) { addShards(r.shards); msg += '+' + r.shards + ' Shards'; }
      if (r.item) { addItem(r.item, r.qty); const item = SHOP_ITEMS.find(i => i.id === r.item); msg += ' +' + r.qty + ' ' + item.name; }
      toast(msg, 3);
      synth.captureSuccess();
      if (!zp.completed.includes(idx)) zp.completed.push(idx);
      savePlayer();
      setTimeout(renderOverworld, 600);
      break;
    }
    case 'npc': {
      const npc = createNpcEncounter(zone.id, node.team);
      STATE.battle = null;
      startBattle({
        kind: 'npc',
        enemy: npc,
        returnTo: 'overworld',
        zoneId: zone.id,
        tier: 'mid',
        reward: npc.reward,
        captureEnabled: false,
        escapeEnabled: true
      });
      STATE.battle.origNodeIndex = idx;
      break;
    }
    case 'heal': {
      STATE.player.party.forEach(r => { r.hp = r.maxHp; r.status = null; });
      savePlayer();
      synth.levelUp();
      toast('The healing waters restore your team.', 2.4);
      if (!zp.completed.includes(idx)) zp.completed.push(idx);
      setTimeout(renderOverworld, 800);
      break;
    }
    case 'gate': {
      const bossId = node.boss;
      if (STATE.player.bossesCleared.includes(bossId)) {
        toast('This Warden has already fallen.', 2);
        if (!zp.completed.includes(idx)) zp.completed.push(idx);
        setTimeout(renderOverworld, 800);
        return;
      }
      const boss = BOSSES[bossId];
      const portraitSvg = renderSprite(boss.portrait.shape, boss.portrait.type, 100);
      let step = 0;
      const showNext = () => {
        if (step >= boss.dialog.length) {
          const bossEncounter = createBossEncounter(bossId);
          startBattle({
            kind: 'boss',
            enemy: bossEncounter,
            bossId: bossId,
            returnTo: 'overworld',
            captureEnabled: false,
            escapeEnabled: true,
            reward: boss.reward
          });
          STATE.battle.origNodeIndex = idx;
          return;
        }
        showDialog({
          speaker: boss.name + ' — ' + boss.title,
          body: boss.dialog[step],
          portrait: portraitSvg,
          actions: [{ label: 'Continue ▶', cls: 'btn btn-primary', onClick: () => { step++; showNext(); } }]
        });
      };
      showNext();
      break;
    }
  }
}

// ── function: battleRender ──
// Battle screen renderer.
function battleRender() {
  const screen = document.getElementById('screen-battle');
  const b = STATE.battle;
  if (!b) return;
  const player = getPlayerActive();
  const enemy = getBattleEnemy();
  if (!player || !enemy) return;
  const playerSpecies = RESONANTS_BY_ID[player.speciesId];
  const enemySpecies = RESONANTS_BY_ID[enemy.speciesId];
  const playerInfo = ELEMENT_INFO[playerSpecies.type];
  const enemyInfo = ELEMENT_INFO[enemySpecies.type];
  const log = b.log[b.log.length - 1] || { text: '...', cls: '' };
  const sub = STATE.submenu;
  const canAct = b.phase === 'await' && player.hp > 0;

  let actionsHtml = '';
  if (!sub) {
    actionsHtml = '<div class="battle-actions">' +
      '<button class="battle-action-btn attack" data-act="attack" ' + (canAct ? '' : 'disabled') + '><span class="ico">⚔️</span> Attack</button>' +
      '<button class="battle-action-btn prism" data-act="prism" ' + (canAct && b.captureEnabled ? '' : 'disabled') + '><span class="ico">🔷</span> Prism</button>' +
      '<button class="battle-action-btn swap" data-act="swap" ' + (canAct ? '' : 'disabled') + '><span class="ico">🔄</span> Exchange</button>' +
      '<button class="battle-action-btn inv" data-act="inv" ' + (canAct ? '' : 'disabled') + '><span class="ico">🎒</span> Items</button>' +
      '<button class="battle-action-btn flee" data-act="flee" ' + (canAct && b.escapeEnabled ? '' : 'disabled') + ' style="grid-column: 1 / -1;"><span class="ico">🏃</span> Flee</button>' +
      '</div>';
  } else if (sub === 'moves') {
    const moves = getMoveList(player);
    actionsHtml = '<div class="move-grid">' +
      moves.map((m, i) => {
        const c = ELEMENT_INFO[m.type];
        return '<button class="move-btn" data-move="' + i + '" ' + (m.pp > 0 ? '' : 'disabled') + '>' +
          '<div class="move-name" style="color:' + c.color1 + ';">' + m.name + '</div>' +
          '<div class="move-meta">' +
            typePill(m.type) +
            '<span>POW ' + m.power + '</span>' +
            '<span>ACC ' + m.acc + '</span>' +
            '<span>' + m.pp + '/' + m.maxPp + '</span>' +
          '</div></button>';
      }).join('') + '</div>' +
      '<div style="display:flex; justify-content:center; margin-top:8px;"><button class="btn btn-sm" data-act="back">◀ Back</button></div>';
  } else if (sub === 'party') {
    actionsHtml = '<div class="party-swap-grid">' +
      STATE.player.party.map((r, i) => {
        const sp = RESONANTS_BY_ID[r.speciesId];
        return '<div class="party-slot ' + (i === 0 ? 'active' : '') + ' ' + (r.hp <= 0 ? 'fainted' : '') + '" data-idx="' + i + '">' +
          '<div style="font-size:10px; color:var(--ink-dim);">Slot ' + (i+1) + '</div>' +
          '<div class="sprite-frame" style="height:64px;">' + renderSprite(sp.shape, sp.type, 56) + '</div>' +
          '<div style="font-weight:700; font-size:12px;">' + r.nickname + '</div>' +
          '<div style="font-size:10px; color:var(--ink-dim);">Lv. ' + r.level + ' • HP ' + r.hp + '/' + r.maxHp + '</div>' +
          (i === 0 ? '<div style="font-size:10px; color:var(--volt);">ACTIVE</div>' : '<div style="font-size:10px; color:var(--ink-dim);">Click to swap</div>') +
          '</div>';
      }).join('') + '</div>' +
      '<div style="display:flex; justify-content:center; margin-top:8px;"><button class="btn btn-sm" data-act="back">◀ Back</button></div>';
  } else if (sub === 'inventory') {
    const items = SHOP_ITEMS.filter(it => STATE.player.inventory[it.id] > 0);
    if (items.length) {
      actionsHtml = '<div class="inv-grid">' + items.map(it =>
        '<div class="inv-item" data-item="' + it.id + '">' +
          '<div class="ico">' + it.icon + '</div>' +
          '<div><div class="name">' + it.name + '</div><div class="qty">× ' + STATE.player.inventory[it.id] + '</div></div>' +
        '</div>'
      ).join('') + '</div>';
    } else {
      actionsHtml = '<div style="text-align:center; color:var(--ink-dim); padding:20px;">Your inventory is empty.</div>';
    }
    actionsHtml += '<div style="display:flex; justify-content:center; margin-top:8px;"><button class="btn btn-sm" data-act="back">◀ Back</button></div>';
  } else if (sub === 'prism') {
    const items = SHOP_ITEMS.filter(it => it.type === 'prism' && STATE.player.inventory[it.id] > 0);
    if (items.length) {
      actionsHtml = '<div class="inv-grid">' + items.map(it =>
        '<div class="inv-item" data-prism="' + it.tier + '">' +
          '<div class="ico">' + it.icon + '</div>' +
          '<div><div class="name">' + it.name + '</div><div class="qty">× ' + STATE.player.inventory[it.id] + '</div></div>' +
        '</div>'
      ).join('') + '</div>';
    } else {
      actionsHtml = '<div style="text-align:center; color:var(--ink-dim); padding:20px;">No Prisms in your inventory.</div>';
    }
    actionsHtml += '<div style="display:flex; justify-content:center; margin-top:8px;"><button class="btn btn-sm" data-act="back">◀ Back</button></div>';
  }

  const playerHpPct = Math.max(0, (player.hp / player.maxHp) * 100);
  const playerHpColor = playerHpPct > 60 ? '#58e398' : playerHpPct > 30 ? '#ffd24a' : '#ff6e7a';
  const xpPct = (player.xp / xpToNext(player.level)) * 100;
  const enemyHpPct = Math.max(0, (enemy.hp / enemy.maxHp) * 100);
  const enemyHpColor = enemyHpPct > 60 ? '#58e398' : enemyHpPct > 30 ? '#ffd24a' : '#ff6e7a';

  const enemyName = (b.kind === 'boss' || b.kind === 'npc') && b.enemy.name
    ? (b.enemy.team && b.enemy.team.length > 1 ? b.enemy.name : RESONANTS_BY_ID[enemy.speciesId].name)
    : RESONANTS_BY_ID[enemy.speciesId].name;
  const showEnemyTeamCount = (b.kind === 'boss' || b.kind === 'npc') && b.enemy.team && b.enemy.team.length > 1;

  screen.innerHTML = '<div class="battle-shell">' +
    '<div class="battle-arena">' +
      '<div class="arena-bg-decor"></div>' +
      '<div class="battle-platform-enemy"></div>' +
      '<div class="battle-platform-player"></div>' +
      '<div class="battle-sprite-wrap enemy" id="wrap-enemy">' + renderSprite(enemySpecies.shape, enemySpecies.type, 160, 'battle-sprite') + '</div>' +
      '<div class="battle-sprite-wrap player" id="wrap-player">' + renderSprite(playerSpecies.shape, playerSpecies.type, 160, 'battle-sprite') + '</div>' +
      '<div class="battle-info-enemy">' +
        '<div class="bi-row"><span class="bi-name">' + enemyName + '</span><span class="bi-level">Lv.' + enemy.level + '</span></div>' +
        '<div class="bi-bar-wrap">' +
          '<span class="bi-hp-label">HP</span>' +
          '<div class="hp-track" style="flex:1;"><div class="hp-fill" style="width:' + enemyHpPct + '%; background:' + enemyHpColor + ';"></div></div>' +
          '<span class="bi-hp-text">' + enemy.hp + ' / ' + enemy.maxHp + '</span>' +
        '</div>' +
        '<div style="margin-top:6px; display:flex; align-items:center; gap:6px;">' +
          typePill(enemySpecies.type) +
          (enemy.status ? '<span class="status-icon">' + statusIcon(enemy.status) + '</span>' : '') +
        '</div>' +
        (showEnemyTeamCount ? '<div style="margin-top:6px; font-size:10px; color:var(--ink-dim); font-family:Press Start 2P, monospace;">' + (b.enemy.active+1) + ' / ' + b.enemy.team.length + '</div>' : '') +
      '</div>' +
      '<div class="battle-info-player">' +
        '<div class="bi-row"><span class="bi-name">' + player.nickname + '</span><span class="bi-level">Lv.' + player.level + ' ' + (player.status ? statusIcon(player.status) : '') + '</span></div>' +
        '<div class="bi-bar-wrap">' +
          '<span class="bi-hp-label">HP</span>' +
          '<div class="hp-track" style="flex:1;"><div class="hp-fill" style="width:' + playerHpPct + '%; background:' + playerHpColor + ';"></div></div>' +
          '<span class="bi-hp-text">' + player.hp + ' / ' + player.maxHp + '</span>' +
        '</div>' +
        '<div class="bi-bar-wrap" style="margin-top:4px;">' +
          '<span class="bi-hp-label">XP</span>' +
          '<div class="xp-track" style="flex:1;"><div class="xp-fill" style="width:' + xpPct + '%;"></div></div>' +
          '<span class="bi-hp-text">' + player.xp + ' / ' + xpToNext(player.level) + '</span>' +
        '</div>' +
        '<div style="margin-top:6px; display:flex; align-items:center; gap:6px;">' + typePill(playerSpecies.type) + '</div>' +
      '</div>' +
    '</div>' +
    '<div>' +
      '<div class="battle-message"><div class="msg-text ' + log.cls + '">' + log.text + '</div>' + (b.phase === 'end' ? '' : '<div class="battle-msg-arrow">▶</div>') + '</div>' +
      '<div style="margin-top:10px;">' + actionsHtml + '</div>' +
    '</div>' +
  '</div>';

  const enemyWrap = document.getElementById('wrap-enemy');
  if (enemyWrap) enemyWrap.style.setProperty('--sprite-glow', enemyInfo.glow);
  const playerWrap = document.getElementById('wrap-player');
  if (playerWrap) playerWrap.style.setProperty('--sprite-glow', playerInfo.glow);

  screen.querySelectorAll('[data-act]').forEach(btn => btn.addEventListener('click', () => handleBattleAct(btn.dataset.act)));
  screen.querySelectorAll('[data-move]').forEach(btn => btn.addEventListener('click', () => handleBattleMove(parseInt(btn.dataset.move))));
  screen.querySelectorAll('.party-slot').forEach(btn => btn.addEventListener('click', () => handlePartySwap(parseInt(btn.dataset.idx))));
  screen.querySelectorAll('[data-item]').forEach(btn => btn.addEventListener('click', () => handleItemUse(btn.dataset.item)));
  screen.querySelectorAll('[data-prism]').forEach(btn => btn.addEventListener('click', () => handlePrismUse(parseInt(btn.dataset.prism))));

  const msg = screen.querySelector('.battle-message');
  if (msg && b.phase === 'end') {
    msg.addEventListener('click', () => synth.click());
  }
}

// ── function: statusIcon ──
function statusIcon(s) {
  if (s === 'poison') return '<span title="Poisoned" style="color:#a3e635;">☠</span>';
  if (s === 'sleep') return '<span title="Asleep" style="color:#a78bfa;">💤</span>';
  if (s === 'paralysis') return '<span title="Paralyzed" style="color:#ffe04a;">⚡</span>';
  return '';
}

// ── function: handleBattleAct ──
function handleBattleAct(act) {
  const b = STATE.battle;
  if (!b || b.phase !== 'await') return;
  if (act === 'attack') { STATE.submenu = 'moves'; synth.click(); battleRender(); }
  else if (act === 'prism') {
    if (!b.captureEnabled) { synth.error(); return; }
    STATE.submenu = 'prism'; synth.click(); battleRender();
  }
  else if (act === 'swap') { STATE.submenu = 'party'; synth.click(); battleRender(); }
  else if (act === 'inv')  { STATE.submenu = 'inventory'; synth.click(); battleRender(); }
  else if (act === 'flee') {
    if (!b.escapeEnabled) { synth.error(); return; }
    if (b.kind === 'boss') { toast('You cannot flee from a Warden!', 2); synth.error(); return; }
    if (b.kind === 'npc' && Math.random() < 0.5) {
      toast('Failed to escape!', 1.6); synth.error();
      b.phase = 'anim';
      setTimeout(() => enemyTurn(), 800);
      return;
    }
    synth.cancel(); toast('Got away safely!', 1.6);
    setTimeout(() => endBattle('fled'), 600);
  }
  else if (act === 'back') { STATE.submenu = null; synth.cancel(); battleRender(); }
}

// ── function: handleBattleMove ──
function handleBattleMove(idx) {
  const b = STATE.battle;
  if (!b || b.phase !== 'await') return;
  const player = getPlayerActive();
  const moves = getMoveList(player);
  const move = moves[idx];
  if (!move || move.pp <= 0) { synth.error(); return; }
  STATE.submenu = null;
  b.phase = 'anim';
  synth.click();
  const enemy = getBattleEnemy();
  const result = tryAttack(player, enemy, move, true);
  playHitSfx(result, RESONANTS_BY_ID[player.speciesId].type);
  playHitAnim('enemy', result, RESONANTS_BY_ID[player.speciesId].type);
  applyEndOfTurnStatus(enemy, RESONANTS_BY_ID[enemy.speciesId].name);
  battleRender();
  if (enemy.hp <= 0) setTimeout(() => onEnemyFaint(), 800);
  else setTimeout(() => enemyTurn(), 800);
}

// ── function: handlePartySwap ──
function handlePartySwap(idx) {
  const b = STATE.battle;
  if (!b || b.phase !== 'await') return;
  if (idx === 0) { STATE.submenu = null; synth.cancel(); battleRender(); return; }
  const target = STATE.player.party[idx];
  if (!target) return;
  if (target.hp <= 0) { synth.error(); toast('That Resonant has fainted!', 1.8); return; }
  const cur = STATE.player.party[0];
  STATE.player.party[0] = target;
  STATE.player.party[idx] = cur;
  synth.click();
  battleLog('You swapped in ' + target.nickname + '!', 'accent');
  STATE.submenu = null;
  b.phase = 'anim';
  battleRender();
  setTimeout(() => enemyTurn(), 700);
}

// ── function: handleItemUse ──
function handleItemUse(itemId) {
  const b = STATE.battle;
  if (!b || b.phase !== 'await') return;
  if (!STATE.player.inventory[itemId]) { synth.error(); return; }
  const item = SHOP_ITEMS.find(i => i.id === itemId);
  if (item.type === 'heal') {
    const target = getPlayerActive();
    if (target.hp <= 0) { toast('Use a Phoenix Feather first.', 1.6); return; }
    if (target.hp >= target.maxHp) { toast('HP is already full.', 1.6); return; }
    const heal = Math.floor(target.maxHp * item.amount);
    target.hp = Math.min(target.maxHp, target.hp + heal);
    STATE.player.inventory[itemId]--;
    synth.levelUp();
    battleLog('You used <span class="accent">' + item.name + '</span>. ' + target.nickname + ' restored ' + heal + ' HP.', 'good');
    STATE.submenu = null; b.phase = 'anim'; battleRender();
    setTimeout(() => enemyTurn(), 700);
  } else if (item.type === 'cure') {
    const target = getPlayerActive();
    if (!target.status) { toast('No status to cure.', 1.6); return; }
    target.status = null;
    STATE.player.inventory[itemId]--;
    synth.click();
    battleLog('You used <span class="accent">' + item.name + '</span>. Status cured!', 'good');
    STATE.submenu = null; b.phase = 'anim'; battleRender();
    setTimeout(() => enemyTurn(), 700);
  } else if (item.type === 'revive') {
    const dead = STATE.player.party.find(r => r.hp <= 0);
    if (!dead) { toast('No Resonant needs reviving.', 1.6); return; }
    dead.hp = Math.floor(dead.maxHp * 0.5);
    STATE.player.inventory[itemId]--;
    synth.levelUp();
    battleLog('<span class="accent">' + item.name + '</span> restores ' + dead.nickname + ' to half HP!', 'good');
    STATE.submenu = null; b.phase = 'anim'; battleRender();
    setTimeout(() => enemyTurn(), 700);
  } else { synth.error(); }
}

// ── function: handlePrismUse ──
function handlePrismUse(tier) {
  const b = STATE.battle;
  if (!b || b.phase !== 'await') return;
  if (b.kind !== 'wild') { synth.error(); toast('Only wild Resonants can be captured.', 2); return; }
  const prismItem = SHOP_ITEMS.find(i => i.type === 'prism' && i.tier === tier);
  if (!prismItem || !STATE.player.inventory[prismItem.id]) { synth.error(); return; }
  STATE.player.inventory[prismItem.id]--;
  STATE.submenu = null;
  b.phase = 'anim';
  battleRender();
  const rate = tryCapture(tier);
  resolveCapture(rate);
}

// ── function: playHitSfx ──
function playHitSfx(result, attackerType) {
  if (!result.hit) { synth.hitPhysical(); return; }
  if (result.status) { synth.hitElemental(); return; }
  if (result.crit) synth.hitSuper();
  else if (result.eff > 1.4) synth.hitSuper();
  else if (result.eff < 0.7) synth.hitPhysical();
  else if (attackerType !== 'cosmos') synth.hitElemental();
  else synth.hitPhysical();
}

// ── function: playHitAnim ──
function playHitAnim(targetSide, result, attackerType) {
  const id = 'wrap-' + targetSide;
  const el = document.getElementById(id);
  if (!el) return;
  const sprite = el.querySelector('.battle-sprite');
  if (!sprite) return;
  if (result.hit) {
    sprite.classList.add('take-hit');
    setTimeout(() => sprite.classList.remove('take-hit'), 500);
  }
  if (result.hit) {
    const type = attackerType || 'cosmos';
    const col = (ELEMENT_INFO[type] || ELEMENT_INFO.cosmos).color1;
    const count = result.crit ? 36 : (result.eff > 1.4 ? 28 : 14);
    emitParticles(col, count, el);
  }
  shakeElement(document.querySelector('.battle-arena'));
}

// ── function: renderDex ──
// Dex (encyclopedia) screen.
function renderDex() {
  const screen = document.getElementById('screen-dex');
  const seen = STATE.player.dex.seen;
  const caught = STATE.player.dex.caught;
  const cards = RESONANTS.map((sp, i) => {
    const isCaught = caught.includes(sp.id);
    const isSeen = seen.includes(sp.id) || isCaught;
    return '<div class="dex-card ' + (isCaught ? 'caught' : isSeen ? 'seen' : 'unknown') + '" data-id="' + sp.id + '">' +
      '<div class="dex-num">#' + String(i+1).padStart(3, '0') + '</div>' +
      '<div class="dex-sprite">' + (isSeen ? renderSprite(sp.shape, sp.type, 80) : '') + '</div>' +
      '<div class="dex-name">' + (isSeen ? sp.name : '???') + '</div>' +
      '<div class="dex-type">' + (isSeen ? typePill(sp.type) : '???') + '</div>' +
    '</div>';
  }).join('');
  screen.innerHTML = '<div class="dex-shell">' +
    '<div class="dex-header glass">' +
      '<div><div style="font-size:11px; color:var(--ink-dim); font-family:Press Start 2P, monospace;">CONDUCTOR\'S DEX</div><div style="font-size:20px; font-weight:700;">Resonant Encyclopaedia</div></div>' +
      '<div style="display:flex; gap:12px; align-items:center;">' +
        '<div style="text-align:center;"><div style="font-size:18px; font-weight:700;">' + caught.length + ' / ' + RESONANTS.length + '</div><div style="font-size:10px; color:var(--ink-dim); font-family:Press Start 2P, monospace;">CAUGHT</div></div>' +
        '<button class="btn btn-sm" id="dex-back">◀ Back</button>' +
      '</div>' +
    '</div>' +
    '<div class="dex-grid scrollbox glass">' + cards + '</div>' +
  '</div>';

  document.getElementById('dex-back').addEventListener('click', () => { synth.click(); showScreen('overworld'); });
  screen.querySelectorAll('.dex-card').forEach(card => {
    card.addEventListener('click', () => {
      const id = card.dataset.id;
      const isSeen = seen.includes(id) || caught.includes(id);
      if (!isSeen) { synth.error(); return; }
      synth.click();
      const sp = RESONANTS_BY_ID[id];
      showResonantDetails(sp);
    });
  });
}

// ── function: showResonantDetails ──
function showResonantDetails(sp) {
  const s = statsForSpecies(sp, 50);
  const caught = STATE.player.dex.caught.includes(sp.id);
  const moves = sp.moves.map(m => '<div style="display:flex; justify-content:space-between; padding:4px 0; border-bottom:1px solid var(--line);">' +
    '<div>' + typePill(MOVES[m.id].type) + ' <b>' + MOVES[m.id].name + '</b> <span style="color:var(--ink-mute); font-size:11px;">(Unlocks Lv. ' + m.unlock + ')</span></div>' +
    '<div style="color:var(--ink-dim); font-size:11px; font-family:Press Start 2P, monospace;">POW ' + MOVES[m.id].power + ' • ACC ' + MOVES[m.id].acc + '</div>' +
  '</div>').join('');
  showDialog({
    speaker: sp.name + ' — #' + sp.id.toUpperCase(),
    body: '<div style="text-align:center; margin-bottom:14px;">' + renderSprite(sp.shape, sp.type, 120) + '</div>' +
      '<div style="text-align:center; margin-bottom:10px;">' + typePill(sp.type) + (sp.evolvesTo ? ' <span style="color:var(--ink-dim);">→ evolves at Lv. ' + sp.evolvesTo + '</span>' : ' <span style="color:var(--volt);">✦ Final Form</span>') + '</div>' +
      '<p style="font-style:italic; color:var(--ink-dim); margin-bottom:12px;">"' + sp.dex + '"</p>' +
      '<div style="background:rgba(0,0,0,0.3); padding:10px; border-radius:8px; margin-bottom:12px;">' +
        '<div style="font-size:11px; color:var(--ink-dim); font-family:Press Start 2P, monospace; margin-bottom:6px;">BASE STATS (LV.50 ESTIMATE)</div>' +
        '<div style="display:grid; grid-template-columns:repeat(4,1fr); gap:8px; font-size:12px;">' +
          '<div><div style="color:var(--ink-dim);">HP</div><div style="font-weight:700;">' + s.maxHp + '</div></div>' +
          '<div><div style="color:var(--ink-dim);">ATK</div><div style="font-weight:700;">' + s.atk + '</div></div>' +
          '<div><div style="color:var(--ink-dim);">DEF</div><div style="font-weight:700;">' + s.def + '</div></div>' +
          '<div><div style="color:var(--ink-dim);">SPD</div><div style="font-weight:700;">' + s.spd + '</div></div>' +
        '</div>' +
      '</div>' +
      '<div style="font-size:11px; color:var(--ink-dim); font-family:Press Start 2P, monospace; margin-bottom:6px;">MOVES</div>' +
      moves +
      (caught ? '' : '<div style="margin-top:10px; color:var(--ink-dim); font-size:11px;">⚠ Not yet caught — stats are species base.</div>'),
    actions: [{ label: 'Close', cls: 'btn btn-primary' }]
  });
}

// ── function: renderShop ──
// Shop screen.
function renderShop() {
  const screen = document.getElementById('screen-shop');
  const items = SHOP_ITEMS.map(it =>
    '<div class="shop-item" data-id="' + it.id + '">' +
      '<div class="ico">' + it.icon + '</div>' +
      '<div class="info"><div class="name">' + it.name + '</div><div class="desc">' + it.desc + '</div></div>' +
      '<div><button class="buy-btn" data-buy="' + it.id + '">Buy</button><div class="cost">✦ ' + it.cost + '</div></div>' +
    '</div>'
  ).join('');
  screen.innerHTML = '<div class="shop-shell">' +
    '<div class="dex-header glass">' +
      '<div><div style="font-size:11px; color:var(--ink-dim); font-family:Press Start 2P, monospace;">AETHER MERCANTILE</div><div style="font-size:20px; font-weight:700;">Camp Shop</div></div>' +
      '<div style="display:flex; gap:14px; align-items:center;">' +
        '<div style="text-align:right;"><div style="font-size:11px; color:var(--ink-dim); font-family:Press Start 2P, monospace;">YOUR SHARDS</div><div style="font-size:22px; font-weight:700; color:var(--volt);">✦ ' + STATE.player.shards + '</div></div>' +
        '<button class="btn btn-sm" id="shop-back">◀ Back</button>' +
      '</div>' +
    '</div>' +
    '<div class="shop-list scrollbox glass">' + items + '</div>' +
  '</div>';
  document.getElementById('shop-back').addEventListener('click', () => { synth.click(); showScreen('overworld'); });
  screen.querySelectorAll('[data-buy]').forEach(btn => {
    btn.addEventListener('click', () => {
      const it = SHOP_ITEMS.find(i => i.id === btn.dataset.buy);
      if (STATE.player.shards < it.cost) { synth.error(); toast('Not enough Shards!', 1.6); return; }
      synth.menuConfirm();
      STATE.player.shards -= it.cost;
      addItem(it.id, 1);
      toast('Purchased ' + it.name + '!', 1.6);
      renderShop();
    });
  });
}

// ── function: renderParty ──
// Party screen.
function renderParty() {
  const screen = document.getElementById('screen-party');
  const rows = STATE.player.party.map((r, i) => {
    const sp = RESONANTS_BY_ID[r.speciesId];
    return '<div class="party-row">' +
      '<div class="party-sprite">' + renderSprite(sp.shape, sp.type, 80) + '</div>' +
      '<div class="info">' +
        '<div class="name" style="color:' + ELEMENT_INFO[sp.type].color1 + ';">' + r.nickname + ' ' + (i === 0 ? '<span class="tag" style="background:rgba(255,224,74,0.2); color:var(--volt);">LEAD</span>' : '') + '</div>' +
        '<div class="stats"><span>Lv. <b>' + r.level + '</b></span><span>HP <b>' + r.hp + '/' + r.maxHp + '</b></span><span>ATK <b>' + r.atk + '</b></span><span>DEF <b>' + r.def + '</b></span><span>SPD <b>' + r.spd + '</b></span></div>' +
        '<div class="moves">' + r.moves.map(m => '<span style="display:inline-block; padding:2px 6px; border-radius:4px; background:rgba(0,0,0,0.3); margin-right:4px; font-size:10px; color:' + ELEMENT_INFO[MOVES[m.id].type].color1 + ';">' + MOVES[m.id].name + ' ' + m.pp + '/' + m.maxPp + '</span>').join('') + '</div>' +
      '</div>' +
      '<div style="display:flex; flex-direction:column; gap:6px;">' +
        typePill(sp.type) +
        (r.status ? '<div style="color:var(--bad); font-size:11px;">' + statusIcon(r.status) + ' ' + r.status + '</div>' : '') +
        (i > 0 ? '<button class="btn btn-sm" data-promote="' + i + '">Make Lead</button>' : '') +
      '</div>' +
    '</div>';
  }).join('');
  const storage = STATE.player.storage.map((r, i) => {
    const sp = RESONANTS_BY_ID[r.speciesId];
    return '<div class="party-row">' +
      '<div class="party-sprite">' + renderSprite(sp.shape, sp.type, 60) + '</div>' +
      '<div class="info">' +
        '<div class="name" style="color:' + ELEMENT_INFO[sp.type].color1 + ';">' + r.nickname + '</div>' +
        '<div class="stats"><span>Lv. <b>' + r.level + '</b></span><span>HP <b>' + r.hp + '/' + r.maxHp + '</b></span></div>' +
      '</div>' +
      '<div><button class="btn btn-sm" data-swapin="' + i + '">Add to Party</button></div>' +
    '</div>';
  }).join('');
  screen.innerHTML = '<div class="party-shell">' +
    '<div class="dex-header glass">' +
      '<div><div style="font-size:11px; color:var(--ink-dim); font-family:Press Start 2P, monospace;">YOUR RESONANTS</div><div style="font-size:20px; font-weight:700;">Active Party & Storage</div></div>' +
      '<div style="display:flex; gap:12px; align-items:center;">' +
        '<div style="text-align:right;"><div style="font-size:11px; color:var(--ink-dim); font-family:Press Start 2P, monospace;">SHARDS</div><div style="font-size:18px; font-weight:700; color:var(--volt);">✦ ' + STATE.player.shards + '</div></div>' +
        '<button class="btn btn-sm" id="party-back">◀ Back</button>' +
      '</div>' +
    '</div>' +
    '<div class="party-list scrollbox glass">' +
      '<h3 style="font-family:Press Start 2P, monospace; font-size:11px; color:var(--ink-dim); margin:0 0 8px;">ACTIVE (' + STATE.player.party.length + '/3)</h3>' +
      rows +
      (storage ? '<h3 style="font-family:Press Start 2P, monospace; font-size:11px; color:var(--ink-dim); margin:14px 0 8px;">STORAGE (' + STATE.player.storage.length + ')</h3>' + storage : '') +
    '</div>' +
  '</div>';
  document.getElementById('party-back').addEventListener('click', () => { synth.click(); showScreen('overworld'); });
  screen.querySelectorAll('[data-promote]').forEach(btn => {
    btn.addEventListener('click', () => {
      const idx = parseInt(btn.dataset.promote);
      const r = STATE.player.party[idx];
      const old = STATE.player.party[0];
      STATE.player.party[0] = r;
      STATE.player.party[idx] = old;
      synth.menuConfirm();
      savePlayer();
      renderParty();
      toast(r.nickname + ' is now your lead!', 1.6);
    });
  });
  screen.querySelectorAll('[data-swapin]').forEach(btn => {
    btn.addEventListener('click', () => {
      const idx = parseInt(btn.dataset.swapin);
      if (STATE.player.party.length >= 3) { synth.error(); toast('Party is full. Remove one first.', 2); return; }
      const r = STATE.player.storage.splice(idx, 1)[0];
      STATE.player.party.push(r);
      synth.menuConfirm();
      savePlayer();
      renderParty();
      toast(r.nickname + ' joined your party!', 1.6);
    });
  });
}

// ── function: renderDefeat ──
// Defeat screen.
function renderDefeat() {
  const screen = document.getElementById('screen-defeat');
  screen.innerHTML = '<div class="end-shell"><div class="end-card glass defeat">' +
    '<div class="end-title">DEFEATED</div>' +
    '<div class="end-body">Your Resonants have fallen. You retreat to the nearest Aether Font to recover — but the journey is not over.</div>' +
    '<div style="display:flex; gap:10px; justify-content:center;">' +
      '<button class="btn btn-primary" id="def-continue">Recover &amp; Continue</button>' +
      '<button class="btn btn-danger" id="def-reset">Reset Progress</button>' +
    '</div>' +
  '</div></div>';
  document.getElementById('def-continue').addEventListener('click', () => {
    synth.menuConfirm();
    // Heal party to half
    STATE.player.party.forEach(r => { r.hp = Math.floor(r.maxHp * 0.5); r.status = null; });
    savePlayer();
    STATE.battle = null;
    showScreen('overworld');
    renderOverworld();
    toast('Recovered at the last Aether Font...', 2);
  });
  document.getElementById('def-reset').addEventListener('click', () => {
    showConfirmDialog('Reset all progress?', 'This will delete your save permanently.', () => {
      resetPlayer();
      showScreen('menu');
      renderMenu();
    });
  });
}

// ── function: boot ──
function boot() {
  const loaded = loadPlayer();
  if (loaded) STATE.player = loaded;
  const unlock = () => {
    synth.init();
    synth.resume();
    document.removeEventListener('click', unlock);
    document.removeEventListener('keydown', unlock);
  };
  document.addEventListener('click', unlock);
  document.addEventListener('keydown', unlock);
  showScreen('menu');
  renderMenu();
  document.body.addEventListener('mouseover', (e) => {
    if (e.target.closest('.btn, .battle-action-btn, .node, .move-btn, .dex-card, .shop-item, .zone-tab, .party-slot, .inv-item, .party-row')) {
      synth.hover();
    }
  });
  document.addEventListener('keydown', (e) => {
    if (e.key === 'Escape' || e.key === 'Backspace') {
      if (STATE.screen === 'battle' && STATE.battle && STATE.submenu) {
        STATE.submenu = null; synth.cancel(); battleRender();
      } else if (STATE.screen === 'dex' || STATE.screen === 'party' || STATE.screen === 'shop') {
        showScreen('overworld');
      } else if (STATE.screen === 'overworld') {
        showScreen('menu'); renderMenu();
      }
    }
  });
}

// ── function: renderFinalVictory ──
function renderFinalVictory() {
  const screen = document.getElementById('screen-victory');
  screen.innerHTML = '<div class="end-shell"><div class="end-card glass victory">' +
    '<div style="font-size:64px; margin-bottom:12px;">🌟</div>' +
    '<div class="end-title">AETHELGARD HARMONIZED</div>' +
    '<div class="end-body">You have climbed the Apex Pillar and answered the Conductor Supreme\'s call. The resonance of Aethelgard flows through you — you are whole.</div>' +
    '<div class="rewards">' +
      '<div class="reward-pill">★ Master Conductor</div>' +
      '<div class="reward-pill">★ All Zones Cleared</div>' +
      '<div class="reward-pill">★ ✦ ' + STATE.player.shards + ' Shards</div>' +
    '</div>' +
    '<div style="display:flex; gap:10px; justify-content:center;">' +
      '<button class="btn btn-primary" id="vict-continue">Back to Overworld</button>' +
      '<button class="btn btn-danger" id="vict-newgame">New Game+</button>' +
    '</div>' +
  '</div></div>';
  document.getElementById('vict-continue').addEventListener('click', () => { synth.menuConfirm(); showScreen('overworld'); renderOverworld(); });
  document.getElementById('vict-newgame').addEventListener('click', () => {
    showConfirmDialog('Begin New Game+?', 'Your current save will be replaced.', () => {
      resetPlayer();
      showScreen('menu');
      renderMenu();
    });
  });
}

// ── main ──
window.addEventListener('DOMContentLoaded', boot);
</script>
</body>
</html>