← Creature Collection results

Creature Collection

Deepseek v4.1 Flash · full

Initial view of Creature Collection

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 137,757 bytes · SHA-256 4d0f0dbc4ace
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover"/>
<meta name="theme-color" content="#05080f"/>
<title>Hollow Tide — Binder of Kindra</title>
<style>
/* ============================================================
   PALETTE + BASE
   ============================================================ */
:root{
  --bg:#05080f;
  --bg2:#0a1020;
  --ink:#e9f3ff;
  --dim:#94aecd;
  --dimmer:#5d7a99;
  --glass:rgba(16,26,44,.62);
  --glass2:rgba(10,18,32,.86);
  --stroke:rgba(140,200,255,.20);
  --stroke2:rgba(140,200,255,.38);
  --accent:#5fe6cf;
  --accent2:#a98bff;
  --hot:#ff8a5c;
  --good:#8df29a;
  --bad:#ff6b7d;
  --pyr:#ff7a45; --blm:#6fdc7a; --ril:#4fc9e8; --stn:#c8a06a;
  --gal:#a8e0ff; --aet:#f3d78a; --umb:#b17ce0;
  --r:14px; --r2:20px;
  --shadow:0 18px 50px rgba(0,0,0,.55);
  --font: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  --mono: ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;
}
*{box-sizing:border-box; -webkit-tap-highlight-color:transparent;}
html,body{height:100%;margin:0;padding:0;background:var(--bg);color:var(--ink);font-family:var(--font);overflow:hidden;}
body{user-select:none; -webkit-user-select:none;}
button{font-family:inherit;color:inherit;background:none;border:none;cursor:pointer;}
::-webkit-scrollbar{width:8px;height:8px}
::-webkit-scrollbar-thumb{background:rgba(140,200,255,.22);border-radius:8px}
::-webkit-scrollbar-track{background:transparent}

#stage{position:fixed;inset:0;overflow:hidden;background:radial-gradient(120% 100% at 50% 0%, #0d1730 0%, #05080f 62%, #02040a 100%);}
canvas#cv{position:absolute;inset:0;width:100%;height:100%;display:block;image-rendering:auto;}

/* ============================================================
   GENERIC FX
   ============================================================ */
#vignette{position:absolute;inset:0;pointer-events:none;opacity:0;transition:opacity .8s ease;
  background:radial-gradient(80% 70% at 50% 50%, transparent 30%, rgba(0,0,0,.85) 100%);}
#flash{position:absolute;inset:0;pointer-events:none;background:#fff;opacity:0;mix-blend-mode:screen;}
.shake{animation:shakeX .38s cubic-bezier(.36,.07,.19,.97) both;}
@keyframes shakeX{
  10%,90%{transform:translate(-2px,1px)} 20%,80%{transform:translate(4px,-2px)}
  30%,50%,70%{transform:translate(-6px,2px)} 40%,60%{transform:translate(6px,-1px)}
}
.fadein{animation:fadein .35s ease both}
@keyframes fadein{from{opacity:0}to{opacity:1}}
.hidden{display:none !important}

.glass{
  background:linear-gradient(150deg, rgba(28,44,74,.62), rgba(10,18,34,.78));
  backdrop-filter:blur(16px) saturate(1.25); -webkit-backdrop-filter:blur(16px) saturate(1.25);
  border:1px solid var(--stroke); border-radius:var(--r2);
  box-shadow:var(--shadow), inset 0 1px 0 rgba(255,255,255,.07);
}
.pill{display:inline-flex;align-items:center;gap:5px;padding:2px 9px;border-radius:999px;font-size:11px;
  font-weight:700;letter-spacing:.06em;text-transform:uppercase;border:1px solid currentColor;opacity:.95}

/* ============================================================
   TITLE SCREEN
   ============================================================ */
#title{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;
  gap:26px;z-index:60;overflow:hidden;
  background:radial-gradient(90% 80% at 50% 28%, #16234a 0%, #08101f 48%, #02040a 100%);}
.aurora{position:absolute;width:120vmax;height:60vmax;border-radius:50%;filter:blur(70px);opacity:.42;
  pointer-events:none;mix-blend-mode:screen;}
.a1{background:radial-gradient(closest-side,#3ad2c0,transparent 70%);animation:aur1 26s ease-in-out infinite alternate;top:-22vmax;left:-30vmax}
.a2{background:radial-gradient(closest-side,#8a5cff,transparent 70%);animation:aur2 32s ease-in-out infinite alternate;bottom:-26vmax;right:-28vmax}
.a3{background:radial-gradient(closest-side,#ff8a5c,transparent 72%);animation:aur3 40s ease-in-out infinite alternate;top:20vmax;left:30vmax;opacity:.2}
@keyframes aur1{from{transform:translate(0,0) scale(1)}to{transform:translate(14vmax,8vmax) scale(1.22)}}
@keyframes aur2{from{transform:translate(0,0) scale(1.1)}to{transform:translate(-16vmax,-6vmax) scale(.9)}}
@keyframes aur3{from{transform:translate(0,0)}to{transform:translate(-20vmax,12vmax)}}
#titleInner{position:relative;z-index:2;display:flex;flex-direction:column;align-items:center;gap:8px;text-align:center;padding:20px}
.logo-sup{font-size:12px;letter-spacing:.62em;text-transform:uppercase;color:var(--accent);opacity:.85;margin-left:.62em}
.logo{font-size:clamp(42px,10vw,88px);font-weight:800;letter-spacing:.06em;line-height:.95;margin:6px 0 2px;
  background:linear-gradient(180deg,#ffffff 0%,#a8e6ff 42%,#7d8cff 100%);
  -webkit-background-clip:text;background-clip:text;color:transparent;
  filter:drop-shadow(0 0 26px rgba(110,200,255,.45)) drop-shadow(0 6px 30px rgba(120,90,255,.3));}
.logo-sub{font-size:clamp(11px,2.4vw,15px);letter-spacing:.42em;text-transform:uppercase;color:var(--dim);margin-left:.42em}
.titleRule{width:min(560px,80vw);height:1px;margin:18px 0 4px;
  background:linear-gradient(90deg,transparent,rgba(140,200,255,.5),transparent);}
.titleBlurb{max-width:min(620px,86vw);font-size:13.5px;line-height:1.75;color:var(--dim);margin-top:6px}
#title btns{display:flex;gap:14px;flex-wrap:wrap;justify-content:center}
.menuBtns{display:flex;gap:14px;flex-wrap:wrap;justify-content:center;margin-top:14px}
.bigbtn{position:relative;padding:14px 34px;border-radius:999px;font-size:14px;font-weight:700;letter-spacing:.18em;
  text-transform:uppercase;color:var(--ink);border:1px solid var(--stroke2);
  background:linear-gradient(150deg,rgba(46,72,120,.6),rgba(12,22,40,.75));
  box-shadow:0 10px 30px rgba(0,0,0,.45), inset 0 1px 0 rgba(255,255,255,.1);
  transition:transform .16s ease, box-shadow .3s ease, border-color .3s ease;overflow:hidden}
.bigbtn:hover{transform:translateY(-2px);border-color:var(--accent);box-shadow:0 14px 36px rgba(60,220,200,.22), inset 0 1px 0 rgba(255,255,255,.14)}
.bigbtn:active{transform:translateY(0) scale(.98)}
.bigbtn.primary{background:linear-gradient(150deg,rgba(50,190,180,.85),rgba(28,80,140,.85));border-color:rgba(140,255,240,.5)}
.bigbtn[disabled]{opacity:.35;cursor:not-allowed;transform:none}
.titleLinks{display:flex;gap:22px;margin-top:18px;font-size:11.5px;letter-spacing:.2em;text-transform:uppercase;color:var(--dimmer2,#4e6684)}
.titleLinks button{color:#6c86a8;letter-spacing:.2em;font-size:11.5px;text-transform:uppercase}
.titleLinks button:hover{color:var(--accent)}

/* ============================================================
   HUD
   ============================================================ */
#hud{position:absolute;inset:0;pointer-events:none;z-index:20;}
#hud .hudbar{display:flex;justify-content:space-between;align-items:flex-start;padding:12px 14px;gap:10px}
.zoneTag{pointer-events:auto;background:var(--glass);backdrop-filter:blur(12px);border:1px solid var(--stroke);
  border-radius:999px;padding:7px 16px;font-size:12px;letter-spacing:.16em;text-transform:uppercase;color:var(--ink);
  box-shadow:0 8px 24px rgba(0,0,0,.4);display:flex;align-items:center;gap:9px}
.zoneTag i{width:8px;height:8px;border-radius:50%;background:var(--accent);box-shadow:0 0 12px var(--accent)}
.hudRight{display:flex;gap:8px;pointer-events:auto}
.chip{background:var(--glass);backdrop-filter:blur(12px);border:1px solid var(--stroke);border-radius:999px;
  padding:7px 14px;font-family:var(--mono);font-size:12.5px;letter-spacing:.06em;color:var(--aet);
  box-shadow:0 8px 24px rgba(0,0,0,.4)}
.iconbtn{width:38px;height:38px;border-radius:12px;background:var(--glass);backdrop-filter:blur(12px);
  border:1px solid var(--stroke);display:grid;place-items:center;font-size:16px;color:var(--ink);
  transition:transform .15s, border-color .2s}
.iconbtn:hover{border-color:var(--accent);transform:translateY(-1px)}
#partyStrip{position:absolute;left:12px;bottom:12px;display:flex;flex-direction:column;gap:6px;pointer-events:none}
.pstrip{display:flex;align-items:center;gap:8px;background:rgba(8,14,26,.6);backdrop-filter:blur(8px);
  border:1px solid var(--stroke);border-radius:10px;padding:4px 9px 4px 5px;min-width:150px}
.pstrip.dead{opacity:.42;filter:grayscale(.7)}
.pstrip .dot{width:16px;height:16px;border-radius:50%;flex:none;box-shadow:0 0 8px currentColor}
.pstrip .pname{font-size:11px;letter-spacing:.04em;flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.pstrip .plv{font-family:var(--mono);font-size:10px;color:var(--dim)}
.pbar{height:4px;border-radius:3px;background:rgba(255,255,255,.1);overflow:hidden;width:100%;margin-top:3px}
.pbar i{display:block;height:100%;border-radius:3px;transition:width .45s cubic-bezier(.2,.8,.2,1)}
#hint{position:absolute;left:50%;transform:translateX(-50%);bottom:16px;padding:9px 20px;border-radius:999px;
  background:var(--glass);backdrop-filter:blur(12px);border:1px solid var(--stroke);font-size:12.5px;
  letter-spacing:.05em;color:var(--ink);opacity:0;transition:opacity .4s;pointer-events:none;z-index:22}
#hint.on{opacity:1}

/* ============================================================
   TOUCH CONTROLS
   ============================================================ */
#touch{position:absolute;inset:auto 0 0 0;z-index:23;display:none;pointer-events:none;padding:0 16px 16px}
#touch.on{display:block}
.dpad{position:absolute;left:18px;bottom:18px;width:150px;height:150px;pointer-events:auto}
.dpad button{position:absolute;width:50px;height:50px;border-radius:14px;background:rgba(20,34,56,.66);
  backdrop-filter:blur(10px);border:1px solid var(--stroke2);display:grid;place-items:center;font-size:16px;color:#cfe6ff}
.dpad button:active{background:rgba(60,220,200,.35)}
.dpad .up{left:50px;top:0}.dpad .dn{left:50px;top:100px}.dpad .lf{left:0;top:50px}.dpad .rt{left:100px;top:50px}
.actionBtns{position:absolute;right:18px;bottom:26px;display:flex;gap:14px;pointer-events:auto;align-items:flex-end}
.rbtn{width:74px;height:74px;border-radius:50%;background:linear-gradient(150deg,rgba(50,190,180,.5),rgba(24,60,110,.66));
  backdrop-filter:blur(10px);border:1px solid rgba(140,255,240,.42);display:grid;place-items:center;
  font-size:11px;font-weight:700;letter-spacing:.1em;text-transform:uppercase;color:#dcfaff;text-align:center;line-height:1.2}
.rbtn:active{transform:scale(.94)}

/* toast */
#toastWrap{position:absolute;top:64px;left:50%;transform:translateX(-50%);z-index:70;display:flex;flex-direction:column;gap:8px;align-items:center;pointer-events:none}
.toast{padding:9px 20px;border-radius:999px;background:var(--glass2);border:1px solid var(--stroke2);
  font-size:12.5px;letter-spacing:.05em;animation:toastIn .3s ease both, toastOut .4s ease 1.8s both;white-space:nowrap;
  box-shadow:0 10px 30px rgba(0,0,0,.5)}
@keyframes toastIn{from{opacity:0;transform:translateY(-12px)}to{opacity:1;transform:none}}
@keyframes toastOut{to{opacity:0;transform:translateY(-10px)}}
/* dmg numbers */
.dmg{position:absolute;font-family:var(--mono);font-weight:800;font-size:24px;pointer-events:none;z-index:40;
  text-shadow:0 2px 10px rgba(0,0,0,.85);animation:dmgUp 1s cubic-bezier(.2,.7,.3,1) both}
.dmg.crit{font-size:34px;color:#ffd76a;text-shadow:0 0 16px rgba(255,190,60,.9),0 2px 10px #000}
.dmg.small{font-size:17px}
@keyframes dmgUp{0%{opacity:0;transform:translate(-50%,6px) scale(.6)}
  22%{opacity:1;transform:translate(-50%,-14px) scale(1.14)}
  100%{opacity:0;transform:translate(-50%,-62px) scale(1)}}
</style>
<style>
/* ============================================================
   BATTLE
   ============================================================ */
#battle{position:absolute;inset:0;z-index:30;pointer-events:none;}
#battle.on{pointer-events:auto}
#enemyRow{position:absolute;top:0;left:0;right:0;padding:10px 12px;display:flex;gap:10px;justify-content:center;flex-wrap:wrap}
.ecard{width:min(210px,30vw);background:var(--glass);backdrop-filter:blur(14px);border:1px solid var(--stroke);
  border-radius:var(--r);padding:9px 12px 10px;box-shadow:0 12px 34px rgba(0,0,0,.5);position:relative;transition:opacity .4s, transform .3s}
.ecard.fainted{opacity:.32;transform:scale(.94) translateY(-4px)}
.ecard.target{cursor:pointer;border-color:var(--accent);box-shadow:0 0 0 1px var(--accent),0 12px 34px rgba(0,0,0,.5)}
.ecard .row1{display:flex;align-items:baseline;justify-content:space-between;gap:8px}
.ecard .nm{font-size:12.5px;font-weight:700;letter-spacing:.03em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.ecard .lv{font-family:var(--mono);font-size:11px;color:var(--dim);flex:none}
.hpTrack{height:7px;border-radius:5px;background:rgba(4,10,20,.72);overflow:hidden;margin-top:7px;position:relative;
  box-shadow:inset 0 1px 2px rgba(0,0,0,.7)}
.hpFill{height:100%;border-radius:5px;transition:width .5s cubic-bezier(.2,.8,.2,1), background .3s;position:relative}
.hpFill::after{content:'';position:absolute;inset:0;background:linear-gradient(180deg,rgba(255,255,255,.42),transparent 60%);border-radius:5px}
.hpghost{position:absolute;top:0;left:0;bottom:0;background:rgba(255,255,255,.5);transition:width .8s ease .18s;border-radius:5px}
.ecard .row3{display:flex;align-items:center;justify-content:space-between;gap:6px;margin-top:6px}
.tpills{display:flex;gap:4px;flex-wrap:wrap}
.tpill{font-size:9.5px;font-weight:800;letter-spacing:.1em;text-transform:uppercase;padding:2px 7px;border-radius:999px;
  border:1px solid currentColor;opacity:.92}
.statuses{display:flex;gap:4px;flex-wrap:wrap;margin-top:5px;min-height:16px}
.st{font-size:9.5px;font-weight:800;letter-spacing:.08em;padding:2px 6px;border-radius:5px;background:rgba(0,0,0,.42);
  border:1px solid currentColor;text-transform:uppercase}

/* player card */
#playerCard{position:absolute;left:12px;bottom:12px;width:min(330px,44vw);z-index:31}
.pc-inner{background:var(--glass);backdrop-filter:blur(14px);border:1px solid var(--stroke);border-radius:var(--r);
  padding:10px 14px 12px;box-shadow:0 12px 34px rgba(0,0,0,.5)}
.pc-inner .row1{display:flex;align-items:baseline;justify-content:space-between}
.pc-inner .nm{font-size:14px;font-weight:700}
.pc-inner .lv{font-family:var(--mono);font-size:11.5px;color:var(--dim)}
.hpText{font-family:var(--mono);font-size:11px;color:var(--dim);margin-top:4px;display:flex;justify-content:space-between}
.expTrack{height:4px;border-radius:3px;background:rgba(4,10,20,.72);overflow:hidden;margin-top:6px}
.expFill{height:100%;background:linear-gradient(90deg,var(--accent2),var(--accent));transition:width .5s ease;border-radius:3px}

/* command panel */
#cmdPanel{position:absolute;right:12px;bottom:12px;width:min(430px,50vw);z-index:32}
#cmdPanel .logbox{background:var(--glass);backdrop-filter:blur(12px);border:1px solid var(--stroke);border-radius:var(--r);
  padding:8px 14px;font-size:12.5px;color:var(--dim);min-height:36px;display:flex;align-items:center;margin-bottom:8px;
  line-height:1.4}
#cmdPanel .logbox b{color:var(--ink);font-weight:700}
.cmdGrid{display:grid;grid-template-columns:1fr 1fr;gap:8px}
.cmdBtn{display:flex;align-items:center;gap:8px;padding:9px 12px;border-radius:11px;border:1px solid var(--stroke);
  background:linear-gradient(150deg,rgba(30,48,80,.7),rgba(10,18,34,.8));backdrop-filter:blur(10px);
  transition:transform .12s, border-color .2s, background .2s;text-align:left;min-height:44px}
.cmdBtn:hover{border-color:var(--accent);transform:translateY(-1px)}
.cmdBtn:active{transform:translateY(0) scale(.98)}
.cmdBtn:disabled{opacity:.34;transform:none;cursor:not-allowed}
.cmdBtn .mnm{font-size:12.5px;font-weight:700;flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.cmdBtn .meta{font-family:var(--mono);font-size:10px;color:var(--dim);flex:none}
.cmdBtn.sub{background:linear-gradient(150deg,rgba(24,38,66,.7),rgba(8,14,26,.8))}
.mdot{width:9px;height:9px;border-radius:50%;flex:none;box-shadow:0 0 8px currentColor}

/* lineup bottom bar */
#lineup{position:absolute;left:12px;top:0;transform:translateY(0);display:flex;gap:6px;flex-direction:column;padding-top:0}

/* ============================================================
   DIALOG / CUT-IN
   ============================================================ */
#dialog{position:absolute;left:0;right:0;bottom:0;z-index:45;padding:0 0 max(14px,env(safe-area-inset-bottom)) 0;display:flex;justify-content:center}
.dlgBox{width:min(860px,94vw);margin:0 12px 12px;padding:18px 22px 20px;
  background:linear-gradient(150deg, rgba(26,42,72,.82), rgba(8,14,28,.92));backdrop-filter:blur(18px);
  border:1px solid var(--stroke2);border-radius:var(--r2);box-shadow:var(--shadow);position:relative;overflow:hidden}
.dlgBox::before{content:'';position:absolute;left:0;top:0;bottom:0;width:4px;background:linear-gradient(180deg,var(--accent),var(--accent2))}
.dlgWho{font-size:11px;letter-spacing:.28em;text-transform:uppercase;color:var(--accent);margin-bottom:8px}
.dlgText{font-size:15px;line-height:1.75;color:#dceafc;min-height:56px}
.dlgNext{position:absolute;right:16px;bottom:12px;font-size:11px;letter-spacing:.2em;text-transform:uppercase;color:var(--dim);animation:blink 1.4s infinite}
@keyframes blink{0%,100%{opacity:.35}50%{opacity:1}}
#vignette.on{opacity:1}

/* ============================================================
   MODAL
   ============================================================ */
#modalWrap{position:absolute;inset:0;z-index:65;display:flex;align-items:center;justify-content:center;
  background:rgba(2,5,12,.62);backdrop-filter:blur(6px);animation:fadein .22s ease both}
#modal{width:min(880px,94vw);max-height:90vh;display:flex;flex-direction:column;overflow:hidden}
.mHead{display:flex;align-items:center;justify-content:space-between;padding:16px 22px;border-bottom:1px solid var(--stroke)}
.mHead h2{margin:0;font-size:15px;letter-spacing:.22em;text-transform:uppercase;font-weight:700}
.mTabs{display:flex;gap:6px;padding:12px 18px 0;flex-wrap:wrap}
.mTab{padding:8px 16px;border-radius:999px;font-size:11.5px;letter-spacing:.14em;text-transform:uppercase;
  border:1px solid transparent;color:var(--dim);transition:.2s}
.mTab:hover{color:var(--ink);background:rgba(255,255,255,.05)}
.mTab.sel{color:var(--ink);background:rgba(95,230,207,.14);border-color:rgba(95,230,207,.45)}
.mBody{padding:16px 18px 20px;overflow-y:auto;flex:1;min-height:180px}
.mGrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(168px,1fr));gap:10px}
.card{border-radius:var(--r);border:1px solid var(--stroke);background:linear-gradient(150deg,rgba(28,44,74,.5),rgba(9,16,30,.7));
  padding:11px 13px;transition:.2s;position:relative;overflow:hidden;cursor:pointer}
.card:hover{border-color:var(--stroke2);transform:translateY(-2px)}
.card .cn{font-size:13px;font-weight:700;display:flex;align-items:center;gap:7px}
.card .cl{font-size:11px;color:var(--dim);line-height:1.5;margin-top:6px}
.card .stats{display:grid;grid-template-columns:1fr 1fr;gap:3px 10px;margin-top:8px;font-family:var(--mono);font-size:10.5px;color:var(--dim)}
.card .stats b{color:var(--ink);font-weight:600}
.card.sel{border-color:var(--accent);box-shadow:0 0 0 1px var(--accent)}
.mRow{display:flex;align-items:center;gap:12px;padding:10px 12px;border-radius:12px;border:1px solid var(--stroke);
  margin-bottom:8px;background:rgba(12,20,36,.5);transition:.2s}
.mRow:hover{border-color:var(--stroke2)}
.mRow .grow{flex:1;min-width:0}
.mRow .t1{font-size:12.5px;font-weight:700}
.mRow .t2{font-size:11px;color:var(--dim);margin-top:3px;line-height:1.5}
.miniBtn{padding:7px 14px;border-radius:999px;border:1px solid var(--stroke2);font-size:11px;letter-spacing:.1em;
  text-transform:uppercase;transition:.18s;flex:none}
.miniBtn:hover{background:rgba(95,230,207,.16);border-color:var(--accent);color:#fff}
.miniBtn.danger:hover{background:rgba(255,107,125,.18);border-color:var(--bad)}
.bar{height:6px;border-radius:4px;background:rgba(4,10,20,.7);overflow:hidden;margin-top:6px}
.bar i{display:block;height:100%;border-radius:4px;transition:width .4s ease}
.empty{text-align:center;color:var(--dim);font-size:12.5px;padding:34px 10px;line-height:1.7}
.lore{font-size:12.5px;line-height:1.8;color:#b9cde6;font-style:italic}

/* capture minigame */
#capture{position:absolute;inset:0;z-index:48;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:18px;
  background:rgba(2,5,12,.55);backdrop-filter:blur(5px)}
.capTitle{font-size:12px;letter-spacing:.32em;text-transform:uppercase;color:var(--accent)}
.capTrack{width:min(520px,82vw);height:34px;border-radius:999px;position:relative;background:rgba(4,10,22,.85);
  border:1px solid var(--stroke2);overflow:hidden;box-shadow:inset 0 2px 8px rgba(0,0,0,.7)}
.capZone{position:absolute;top:0;bottom:0;background:linear-gradient(180deg,rgba(95,230,207,.32),rgba(95,230,207,.14));border-left:1px solid var(--accent);border-right:1px solid var(--accent)}
.capZone.core{background:linear-gradient(180deg,rgba(255,214,106,.5),rgba(255,214,106,.2));border-color:#ffd76a}
.capMark{position:absolute;top:-3px;bottom:-3px;width:4px;background:#fff;box-shadow:0 0 14px #fff,0 0 26px var(--accent);border-radius:2px;left:0}
.capHint2{font-size:11.5px;color:var(--dim);letter-spacing:.1em}
.capResult{font-size:15px;letter-spacing:.12em;text-transform:uppercase;font-weight:700}

/* save slots */
.slot{display:flex;align-items:center;gap:14px;padding:14px 16px;border-radius:var(--r);border:1px solid var(--stroke);
  background:linear-gradient(150deg,rgba(26,42,72,.5),rgba(9,16,30,.7));margin-bottom:10px;transition:.2s}
.slot:hover{border-color:var(--accent)}
.slot .sname{font-size:13px;font-weight:700}
.slot .smeta{font-size:11px;color:var(--dim);margin-top:4px;line-height:1.6}

/* map */
#mapCanvas{width:100%;height:260px;border-radius:var(--r);border:1px solid var(--stroke);background:rgba(4,8,18,.6)}

/* tutor bubble */
.tutBubble{position:absolute;z-index:44;max-width:250px;padding:10px 14px;border-radius:14px;
  background:var(--glass2);border:1px solid var(--accent);font-size:12px;line-height:1.55;color:var(--ink);
  box-shadow:0 10px 30px rgba(0,0,0,.6);animation:fadein .3s ease both}
.tutBubble::after{content:'';position:absolute;width:0;height:0}

@media (max-width:720px){
  #playerCard{width:calc(50vw - 18px)}
  #cmdPanel{width:calc(50vw - 18px)}
  .cmdGrid{grid-template-columns:1fr}
  .ecard{width:calc(33vw - 14px);padding:7px 9px 8px}
  .ecard .nm{font-size:11px}
  .pc-inner .nm{font-size:12.5px}
  #partyStrip{display:none}
  .logo-sub{letter-spacing:.24em}
}
</style>
</head>
<body>
<div id="stage">
  <canvas id="cv"></canvas>
  <div id="vignette"></div>
  <div id="flash"></div>

  <!-- ============ OVERWORLD HUD ============ -->
  <div id="hud" class="hidden">
    <div class="hudbar">
      <div class="zoneTag"><i id="zoneDot"></i><span id="zoneName">—</span></div>
      <div class="hudRight">
        <div class="chip" id="moteChip">◈ 0</div>
        <button class="iconbtn" id="btnMenu" title="Menu">☰</button>
      </div>
    </div>
    <div id="partyStrip"></div>
  </div>
  <div id="hint"></div>

  <!-- ============ TOUCH ============ -->
  <div id="touch">
    <div class="dpad">
      <button class="up" data-dir="up">▲</button>
      <button class="lf" data-dir="left">◀</button>
      <button class="rt" data-dir="right">▶</button>
      <button class="dn" data-dir="down">▼</button>
    </div>
    <div class="actionBtns">
      <button class="rbtn" id="touchAction">Inter<br>act</button>
    </div>
  </div>

  <!-- ============ BATTLE ============ -->
  <div id="battle" class="hidden">
    <div id="enemyRow"></div>
    <div id="playerCard" class="hidden">
      <div class="pc-inner">
        <div class="row1"><span class="nm" id="pName">—</span><span class="lv" id="pLv">Lv 1</span></div>
        <div class="hpTrack"><div class="hpghost" id="pGhost" style="width:100%"></div><div class="hpFill" id="pHp" style="width:100%;background:linear-gradient(90deg,#5fe6cf,#37c9a5)"></div></div>
        <div class="hpText"><span id="pHpText">0 / 0</span><span id="pStatus"></span></div>
        <div class="expTrack"><div class="expFill" id="pExp" style="width:0%"></div></div>
      </div>
    </div>
    <div id="cmdPanel" class="hidden">
      <div class="logbox" id="battleLog">—</div>
      <div class="cmdGrid" id="cmdGrid"></div>
    </div>
  </div>

  <!-- ============ DIALOG ============ -->
  <div id="dialog" class="hidden">
    <div class="dlgBox">
      <div class="dlgWho" id="dlgWho"></div>
      <div class="dlgText" id="dlgText"></div>
      <div class="dlgNext">▸ tap / space</div>
    </div>
  </div>

  <!-- ============ CAPTURE MINIGAME ============ -->
  <div id="capture" class="hidden">
    <div class="capTitle">Resonance Binding</div>
    <div class="capTrack" id="capTrack">
      <div class="capZone" id="capZone"></div>
      <div class="capZone core hidden" id="capCore"></div>
      <div class="capMark" id="capMark"></div>
    </div>
    <div class="capHint2" id="capHint2">Tap in the glow to steady the shard</div>
    <div class="capResult hidden" id="capResult"></div>
    <button class="bigbtn primary hidden" id="capBtn">Lock</button>
  </div>

  <!-- ============ MODAL ============ -->
  <div id="modalWrap" class="hidden"><div id="modal" class="glass"></div></div>

  <!-- ============ TITLE ============ -->
  <div id="title">
    <div class="aurora a1"></div><div class="aurora a2"></div><div class="aurora a3"></div>
    <div id="titleInner">
      <div class="logo-sup">Binder of</div>
      <div class="logo">HOLLOW TIDE</div>
      <div class="logo-sub">Chronicle of the Six Reaches</div>
      <div class="titleRule"></div>
      <div class="titleBlurb">
        The lattice that holds the world is unravelling, strand by strand. You are a Binder —
        one who walks the drowning reaches, coaxing <em>Kindra</em> from the drift and threading them
        back into the weave. Anchor what remains before the Tide takes the rest.
      </div>
      <div class="menuBtns">
        <button class="bigbtn primary" id="btnNew">New Binding</button>
        <button class="bigbtn" id="btnContinue">Continue</button>
      </div>
      <div class="titleLinks">
        <button id="btnSlots">Slots</button>
        <button id="btnHelp">Codex</button>
        <button id="btnMute0">♪ Sound</button>
      </div>
    </div>
  </div>

  <div id="toastWrap"></div>
</div>
<script>
"use strict";
/* ================================================================
   HOLLOW TIDE — an original Kindra-binding chronicle
   All lore, names, creatures, abilities and terminology are original.
   ================================================================ */
</script>
<script>
/* ================================================================
   PART 1 — DATA: affinities, abilities, creatures, items, zones
   ================================================================ */
/* ---------- AFFINITIES (7) ---------- */
const AFF_ORDER = ['Pyre','Bloom','Rill','Stone','Gale','Aether','Umbra'];
const AFF = {
  Pyre:  {c:'#ff7a45', lore:'Ember-heat. Kiln and cinder.'},
  Bloom: {c:'#6fdc7a', lore:'Growth-force. Root and spore.'},
  Rill:  {c:'#4fc9e8', lore:'Cold current. Brine and mist.'},
  Stone: {c:'#c8a06a', lore:'Slate-patience. Weight and strata.'},
  Gale:  {c:'#a8e0ff', lore:'Thin-air motion. Whirl and howl.'},
  Aether:{c:'#f3d78a', lore:'Lattice-light. The weave made bright.'},
  Umbra: {c:'#b17ce0', lore:'Hollow space. The unraveling itself.'}
};
/* effectiveness of ATTACKER affinity vs DEFENDER affinity */
const TBL = {
  Pyre:  {Bloom:1.55, Rill:0.70, Stone:0.70, Gale:1,   Aether:1,   Umbra:0.70},
  Bloom: {Rill:1.55,  Pyre:0.70, Gale:0.70,  Stone:1,   Aether:1,   Umbra:0.70},
  Rill:  {Pyre:1.55,  Bloom:0.70,Stone:0.70, Gale:1,    Aether:1,   Umbra:0.70},
  Stone: {Pyre:1.55,  Rill:1.55, Gale:0.70,  Bloom:1,   Aether:1,   Umbra:0.70},
  Gale:  {Stone:1.55, Bloom:1.55,Pyre:1,    Rill:1,    Aether:1,   Umbra:0.70},
  Aether:{Pyre:1.25, Bloom:1.25,Rill:1.25,Stone:1.25,Gale:1.25, Umbra:2.0},
  Umbra: {Pyre:1, Bloom:1, Rill:1, Stone:1, Gale:1, Aether:1, Umbra:1}
};
function typeEff(atkAff, defAffs){
  if(defAffs.length===1 && atkAff===defAffs[0]) return 1;
  let sum=0, n=0;
  for(const d of defAffs){
    if(d===atkAff) continue;
    sum += (TBL[atkAff] && TBL[atkAff][d] !== undefined) ? TBL[atkAff][d] : 1;
    n++;
  }
  return n ? sum/n : 1;
}

/* ---------- ABILITIES (60) ---------- */
const ABIL = {};
function A(id,name,aff,pow,acc,cost,eff,opt){
  ABIL[id] = Object.assign({id,name,aff,pow,acc,cost,eff:eff||'none',
    chance:opt&&opt.chance||0, hits:opt&&opt.hits||1, heal:opt&&opt.heal||0,
    drain:opt&&opt.drain||0, crit:opt&&opt.crit||0, stage:opt&&opt.stage||0,
    priority:!!(opt&&opt.priority), recoil:opt&&opt.recoil||0,
    desc:opt&&opt.desc||''}, opt||{});
}
/* Pyre */
A('cinderlash','Cinder Lash','Pyre',40,100,1,null,{desc:'A snapping loop of hot air.'});
A('emberfang','Ember Fang','Pyre',55,95,2,'burn',{chance:.20,desc:'Bites; embers cling to the wound.'});
A('scorchtrail','Scorchtrail','Pyre',70,90,3,'burn',{chance:.45,desc:'Drags a burning seam across the foe.'});
A('pyreclap','Pyre Clap','Pyre',85,85,4,'crit',{crit:.18,desc:'A concussive bloom of flame.'});
A('furnacebrand','Furnace Brand','Pyre',110,80,6,'burn',{chance:.35,desc:'Marks the foe with kiln-heat.'});
A('hearthmend','Hearthmend','Pyre',0,100,4,'heal',{heal:.42,desc:'Draws warmth inward, mending the self.'});
A('ashveil','Ashveil','Pyre',0,100,3,'barrier',{desc:'A curtain of grey that turns one blow.'});
A('flarestep','Flare Step','Pyre',45,100,3,null,{priority:true,desc:'Strikes first, trailing sparks.'});
/* Bloom */
A('sporeburst','Spore Burst','Bloom',40,100,1,null,{desc:'A puff of stinging seed.'});
A('rootcoil','Root Coil','Bloom',55,95,2,'frost',{chance:.25,desc:'Roots bind the limbs and sap the pace.'});
A('bramblelash','Bramble Lash','Bloom',34,90,3,'multi',{hits:2,desc:'Whips twice with thorned cord.'});
A('loombloom','Loom Bloom','Bloom',90,85,4,null,{desc:'A seed-pod detonates in green light.'});
A('sapdraw','Sap Draw','Bloom',50,95,2,'drain',{drain:.50,desc:'Drinks deep; mends the drinker.'});
A('photosynth','Photosynth','Bloom',0,100,3,'regen',{desc:'Locks into the light; heals each turn.'});
A('pollendrift','Pollen Drift','Bloom',35,100,2,'shock',{chance:.35,desc:'Clogging pollen scrambles the nerves.'});
A('thornvolley','Thorn Volley','Bloom',22,90,4,'multi',{hits:3,desc:'A triple spray of hardened thorns.'});
A('verdantgrasp','Verdant Grasp','Bloom',75,95,4,'drain',{drain:.40,desc:'The ground itself holds and feeds.'});
/* Rill */
A('droplet','Droplet','Rill',40,100,1,null,{desc:'A single cold bead, thrown hard.'});
A('tidalslap','Tidal Slap','Rill',55,95,2,null,{desc:'A wall of water, hand-shaped.'});
A('brineburst','Brine Burst','Rill',75,90,3,'frost',{chance:.20,desc:'Salt spray that stiffens the joints.'});
A('undertow','Undertow','Rill',95,85,5,null,{desc:'Slow, dragging, and impossible to shake.'});
A('mistveil','Mistveil','Rill',0,100,3,'barrier',{desc:'Thick fog swallows the next attack.'});
A('wellspring','Wellspring','Rill',0,100,5,'heal',{heal:.50,desc:'A spring of clean water rises.'});
A('frostjab','Frost Jab','Rill',50,100,2,'frost',{chance:.30,desc:'A needle of rime.'});
A('rillsteed','Rillsteed','Rill',0,100,3,'speedup',{stage:1,desc:'Rides the current; gains speed.'});
A('maelstrom','Maelstrom','Rill',120,75,7,'frost',{chance:.30,desc:'The whole deep turns once.'});
/* Stone */
A('pebbletoss','Pebble Toss','Stone',40,100,1,null,{desc:'Flicks a dense chip of slate.'});
A('cragfist','Crag Fist','Stone',60,90,2,null,{desc:'A fist of packed gravel.'});
A('slatecrush','Slate Crush','Stone',80,85,3,null,{desc:'Layered weight comes down flat.'});
A('faultline','Faultline','Stone',100,80,5,'crit',{crit:.10,desc:'The floor opens along an old seam.'});
A('bedrockguard','Bedrock Guard','Stone',0,100,3,'barrier',{desc:'Standing stone absorbs the blow.'});
A('geodeburst','Geode Burst','Stone',30,85,4,'multi',{hits:3,desc:'Crystals crack loose in a spray.'});
A('granitecradle','Granite Cradle','Stone',0,100,5,'heal',{heal:.40,desc:'Settles into stillness and recovers.'});
A('stonehide','Stonehide','Stone',0,100,3,'buffdef',{stage:1,desc:'Thickens the hide to strata.'});
A('avalanche','Avalanche','Stone',75,80,6,'recoil',{recoil:.25,desc:'Everything above comes down at once.'});
/* Gale */
A('gustnip','Gust Nip','Gale',40,100,1,null,{desc:'A quick, cutting puff.'});
A('zephyrstrike','Zephyr Strike','Gale',50,95,3,null,{priority:true,desc:'Always lands before the wind turns.'});
A('cycloclaw','Cyclo Claw','Gale',75,90,3,null,{desc:'Spiral of hardened air.'});
A('tempestfang','Tempest Fang','Gale',95,85,4,null,{desc:'A roaring bite of storm.'});
A('tailwind','Tailwind','Gale',0,100,3,'speedup',{stage:1,desc:'The world falls behind.'});
A('skyrend','Sky Rend','Gale',115,75,6,null,{desc:'Tears a white line from horizon to horizon.'});
A('downdraft','Downdraft','Gale',60,95,3,'frost',{chance:.35,desc:'Cold air shoves the foe down.'});
A('whirlfang','Whirl Fang','Gale',30,90,4,'multi',{hits:3,desc:'Three bites from three directions.'});
A('galeward','Galeward','Gale',0,100,3,'barrier',{desc:'A spinning shell of moving air.'});
/* Aether */
A('glimmerjab','Glimmer Jab','Aether',45,100,1,null,{desc:'A prick of woven light.'});
A('prismlance','Prism Lance','Aether',65,95,3,null,{desc:'Splits into seven colours and converges.'});
A('astralmark','Astral Mark','Aether',85,90,4,'crit',{crit:.22,desc:'Marks the seam that splits most easily.'});
A('luminbolt','Lumin Bolt','Aether',105,80,6,null,{desc:'A lance of raw lattice.'});
A('aetherveil','Aether Veil','Aether',0,100,3,'barrier',{desc:'Folds light around the body.'});
A('radiance','Radiance','Aether',0,100,5,'heal',{heal:.55,desc:'Bathes the binder\u2019s side in warm light.'});
A('quasarfall','Quasar Fall','Aether',135,70,8,'recoil',{recoil:.30,desc:'Blinding. Costs the caster as much.'});
A('starweave','Starweave','Aether',0,100,4,'buffatk',{stage:1,desc:'Threads the self with bright filament.'});
/* Umbra */
A('shadeprick','Shade Prick','Umbra',40,100,1,null,{desc:'A thin absence, pointed.'});
A('duskrend','Dusk Rend','Umbra',60,95,2,'shock',{chance:.20,desc:'Rips along the lattice seam.'});
A('hollowtouch','Hollow Touch','Umbra',80,90,3,'drain',{drain:.50,desc:'Takes what was there. Keeps it.'});
A('voidmaw','Void Maw','Umbra',100,80,5,null,{desc:'Opens a mouth where nothing should be.'});
A('nightgrasp','Night Grasp','Umbra',55,95,2,'shock',{chance:.35,desc:'Cold fingers close on the spine.'});
A('umbralveil','Umbral Veil','Umbra',0,100,3,'barrier',{desc:'A patch of not-there.'});
A('entropichush','Entropic Hush','Umbra',70,90,4,'frost',{chance:.40,desc:'Slow, quiet, and very cold.'});
A('hollowing','Hollowing','Umbra',120,75,7,'drain',{drain:.35,desc:'Unwrites a piece of the foe.'});

/* ---------- CREATURES (18 base + 9 transformed = 27) ---------- */
const CRE = {};
function C(id,name,affs,lore,hp,atk,def,spd,learn,evo){
  CRE[id] = {id,name,affs,lore,base:{hp,atk,def,spd},learn:learn||[],evo:evo||null,no:Object.keys(CRE).length+1};
}
/* -- Starters -- */
C('cindling','Cindling',['Pyre'],'A coal-hearted whelp that naps inside cold hearthstones. Its dreaming breath sets nearby ash to glowing.',
  45,52,40,48,[[1,'cinderlash'],[5,'emberfang'],[9,'ashveil'],[13,'pyreclap'],[19,'scorchtrail'],[27,'furnacebrand']],
  {lv:16,item:'emberglass',to:'pyrekiln'});
C('mossprig','Mossprig',['Bloom'],'A hand-sized sprout carrying a single lantern seed. It hums to roots until the roots hum back.',
  48,46,50,44,[[1,'sporeburst'],[5,'rootcoil'],[9,'photosynth'],[13,'bramblelash'],[19,'sapdraw'],[27,'loombloom']],
  {lv:15,item:'verdantseed',to:'bramblewarden'});
C('rillit','Rillit',['Rill'],'A bead of living water that keeps its own tide. It rearranges puddles into tidy spirals.',
  46,50,44,56,[[1,'droplet'],[5,'tidalslap'],[9,'rillsteed'],[13,'brineburst'],[19,'frostjab'],[27,'undertow']],
  {lv:16,item:'tidepearl',to:'cascadon'});
/* -- Wild base roster -- */
C('ashfen','Ashfen',['Pyre','Umbra'],'A peat-smoked wanderer that leaves grey footprints smouldering for three days.',
  60,58,48,55,[[1,'cinderlash'],[6,'shadeprick'],[12,'ashveil'],[18,'scorchtrail'],[24,'nightgrasp'],[32,'hollowing']],
  {lv:24,item:'emberglass',to:'cinderwraith'});
C('funglet','Funglet',['Bloom','Umbra'],'It grows only where a name has been forgotten. Its cap holds a small quiet darkness.',
  66,50,62,38,[[1,'sporeburst'],[6,'shadeprick'],[12,'pollendrift'],[18,'entropichush'],[24,'sapdraw'],[30,'voidmaw']]);
C('thistlecloak','Thistlecloak',['Bloom','Stone'],'A walking knot of dry thorn with a slate core. It folds shut when threatened and waits.',
  70,62,74,36,[[1,'sporeburst'],[6,'pebbletoss'],[12,'rootcoil'],[18,'stonehide'],[24,'bramblelash'],[30,'slatecrush']],
  {lv:26,item:'slatecore',to:'thornbastion'});
C('brinestrike','Brinestrike',['Rill','Gale'],'A salt-riding hunter that surfs on its own wake. Its cry sounds like a door opening.',
  58,64,46,76,[[1,'droplet'],[6,'gustnip'],[12,'zephyrstrike'],[18,'brineburst'],[24,'downdraft'],[30,'cycloclaw']]);
C('tidelurker','Tidelurker',['Rill','Stone'],'It sleeps under sand for years and wakes only when the tide forgets it.',
  74,70,78,40,[[1,'droplet'],[6,'pebbletoss'],[12,'mistveil'],[18,'slatecrush'],[24,'wellspring'],[30,'maelstrom']]);
C('wispwake','Wispwake',['Aether','Rill'],'A floating arc of light that traces the path of old rivers. It dims when watched.',
  54,62,48,70,[[1,'glimmerjab'],[6,'droplet'],[12,'prismlance'],[18,'mistveil'],[24,'astralmark'],[30,'luminbolt']]);
C('pebblume','Pebblume',['Stone'],'A round grey stone that grew a face because someone kept looking at it. It is very patient.',
  54,48,66,32,[[1,'pebbletoss'],[6,'stonehide'],[11,'cragfist'],[17,'bedrockguard'],[24,'slatecrush'],[30,'granitecradle']],
  {lv:17,item:'slatecore',to:'monolithine'});
C('geodex','Geodex',['Stone','Pyre'],'Its shell is a shell of shells; the heart is a furnace that never learned to close.',
  68,74,80,34,[[1,'pebbletoss'],[6,'cinderlash'],[12,'cragfist'],[18,'scorchtrail'],[24,'geodeburst'],[30,'faultline']]);
C('zephlet','Zephlet',['Gale'],'A puff of breath that escaped a dying word. It is always slightly ahead of you.',
  44,54,38,72,[[1,'gustnip'],[5,'tailwind'],[11,'cycloclaw'],[16,'zephyrstrike'],[22,'tempestfang'],[30,'skyrend']],
  {lv:16,item:'windvane',to:'tempestral'});
C('downwhirl','Downwhirl',['Gale','Rill'],'It spirals downward forever, collecting rain, and has never once touched ground.',
  60,66,50,80,[[1,'gustnip'],[6,'droplet'],[12,'downdraft'],[18,'cycloclaw'],[24,'brineburst'],[30,'skyrend']]);
C('cinderspire','Cinderspire',['Pyre','Gale'],'A column of hot updraft with a grudge. Birds have learned its outline by heart.',
  62,82,50,78,[[1,'cinderlash'],[6,'gustnip'],[12,'pyreclap'],[18,'cycloclaw'],[26,'furnacebrand'],[34,'skyrend']]);
C('skyshear','Skyshear',['Gale','Aether'],'It cuts the sky into two halves and then waits politely for them to rejoin.',
  64,84,54,92,[[1,'gustnip'],[8,'glimmerjab'],[14,'cycloclaw'],[20,'astralmark'],[28,'tempestfang'],[36,'skyrend']]);
C('duskocoon','Duskocoon',['Umbra','Bloom'],'It spun a shell out of everything it did not want to remember, and now it lives there.',
  72,58,76,46,[[1,'shadeprick'],[8,'sporeburst'],[14,'umbralveil'],[20,'entropichush'],[28,'hollowtouch'],[36,'voidmaw']]);
C('nullamb','Nullamb',['Umbra'],'A lamb-shaped absence that grazes on edges. Where it walks, lines stop meeting.',
  58,56,60,50,[[1,'shadeprick'],[6,'nightgrasp'],[12,'umbralveil'],[20,'duskrend'],[28,'hollowtouch'],[36,'voidmaw']],
  {lv:20,item:'voidglass',to:'hollowmaw'});
C('glimmoth','Glimmoth',['Aether'],'A moth of woven light that eats stray threads of the lattice and grows brighter for it.',
  52,58,46,66,[[1,'glimmerjab'],[8,'prismlance'],[14,'starweave'],[22,'astralmark'],[30,'luminbolt'],[38,'quasarfall']],
  {lv:22,item:'prismheart',to:'luminarch'});
/* -- Transformed forms (9) -- */
C('pyrekiln','Pyrekiln',['Pyre','Stone'],'Its ribcage is a fired brick dome. It swallows slate and exhales air that never cools.',
  78,88,70,62,[[1,'emberfang'],[1,'cragfist'],[22,'scorchtrail'],[26,'slatecrush'],[32,'furnacebrand'],[40,'avalanche']]);
C('cinderwraith','Cinderwraith',['Pyre','Umbra'],'It is what remains when a fire is put out too slowly. It remembers being warm.',
  84,92,66,82,[[1,'emberfang'],[1,'duskrend'],[26,'ashveil'],[30,'scorchtrail'],[36,'hollowing'],[44,'furnacebrand']]);
C('bramblewarden','Bramblewarden',['Bloom','Stone'],'A hedge given a job. It has not moved in a century and considers this a career.',
  86,80,88,40,[[1,'rootcoil'],[1,'stonehide'],[22,'bramblelash'],[28,'slatecrush'],[34,'verdantgrasp'],[42,'loombloom']]);
C('thornbastion','Thornbastion',['Bloom','Stone'],'A fortress that grew. Its gate is a mouth; its walls are patience.',
  100,86,100,42,[[1,'bramblelash'],[1,'bedrockguard'],[28,'verdantgrasp'],[34,'faultline'],[40,'loombloom'],[48,'granitecradle']]);
C('cascadon','Cascadon',['Rill','Gale'],'A waterfall that learned to stand upright and walk. Its roar arrives a moment before it does.',
  80,84,68,74,[[1,'tidalslap'],[1,'cycloclaw'],[22,'brineburst'],[28,'downdraft'],[34,'undertow'],[42,'maelstrom']]);
C('monolithine','Monolithine',['Stone'],'It was a mountain first. It is still deciding whether the walking is an improvement.',
  96,82,106,28,[[1,'cragfist'],[1,'bedrockguard'],[24,'slatecrush'],[30,'geodeburst'],[36,'faultline'],[44,'granitecradle']]);
C('tempestral','Tempestral',['Gale','Aether'],'A storm that has finished storming and is now simply travelling, looking for somewhere to arrive.',
  76,92,62,96,[[1,'cycloclaw'],[1,'prismlance'],[22,'tempestfang'],[28,'astralmark'],[36,'skyrend'],[44,'luminbolt']]);
C('hollowmaw','Hollowmaw',['Umbra'],'The inside of an absence, given teeth. It is not hungry. It is simply where things go.',
  92,96,82,66,[[1,'duskrend'],[1,'hollowtouch'],[26,'entropichush'],[32,'voidmaw'],[38,'hollowing'],[46,'voidmaw']]);
C('luminarch','Luminarch',['Aether'],'It has read the lattice closely enough to fold a corner of it over itself. It apologises later.',
  88,104,74,88,[[1,'prismlance'],[1,'astralmark'],[28,'luminbolt'],[34,'radiance'],[40,'starweave'],[48,'quasarfall']]);

/* ---------- GUARDIANS (bosses, not bindable) ---------- */
const GUARDIANS = {
  vulkarn:{id:'vulkarn',name:'Vulkarn',title:'the Kilnheart',affs:['Pyre','Stone'],lk:13,
    base:{hp:38,atk:34,def:58,spd:46},moves:['emberfang','cragfist','stonehide','scorchtrail'],boss:true,
    lore:'The first anchor. A furnace that grew a spine and refused to go out.'},
  vael:{id:'vael',name:'Rootsinger Vael',title:'',affs:['Bloom','Stone'],lk:19,
    base:{hp:48,atk:46,def:66,spd:58},moves:['verdantgrasp','slatecrush','photosynth','bramblelash'],boss:true,
    lore:'Sings the thicket into a single closed fist.'},
  seshen:{id:'seshen',name:'Marrowtide Seshen',title:'',affs:['Rill','Umbra'],lk:25,
    base:{hp:54,atk:60,def:74,spd:70},moves:['undertow','entropichush','wellspring','frostjab'],boss:true,
    lore:'Keeps the shelf-glass from shattering by holding it in her cold hands.'},
  obdun:{id:'obdun',name:'Slatewake Obdun',title:'',affs:['Stone','Gale'],lk:31,
    base:{hp:62,atk:72,def:96,spd:64},moves:['faultline','skyrend','bedrockguard','cycloclaw'],boss:true,
    lore:'Walks the ridge once a year, and the ridge remembers the shape of his feet.'},
  cirrus:{id:'cirrus',name:'Highhowl Cirrus',title:'',affs:['Gale','Aether'],lk:38,
    base:{hp:70,atk:88,def:82,spd:108},moves:['skyrend','luminbolt','tailwind','tempestfang'],boss:true,
    lore:'A voice with weather attached. It has been shouting the same syllable for a thousand years.'},
  nullsable:{id:'nullsable',name:'Nullsable',title:'the Hollow',affs:['Umbra','Aether'],lk:46,
    base:{hp:84,atk:98,def:104,spd:96},moves:['hollowing','quasarfall','voidmaw','entropichush'],boss:true,
    lore:'The Tide wearing a shape it borrowed. It is not cruel; it is only thorough.'}
};

/* ---------- ITEMS ---------- */
const ITEMS = {
  shard_dull:    {name:'Dull Shard',kind:'bind',tier:0.62,price:24,  desc:'Cloudy lattice sliver. Holds a weakened Kindra just long enough.'},
  shard_bright:  {name:'Bright Shard',kind:'bind',tier:1.00,price:70, desc:'A steady thread of light. Reliable binding.'},
  shard_flawless:{name:'Flawless Shard',kind:'bind',tier:1.60,price:190,desc:'A perfect length of the old weave. Very few slip it.'},
  draught_sun:   {name:'Sunmoss Draught',kind:'heal',amount:60,price:32,desc:'Restores 60 HP to one Kindra.'},
  tonic_deep:    {name:'Deepwell Tonic',kind:'heal',amount:200,price:95,desc:'Restores 200 HP to one Kindra.'},
  salve_ember:   {name:'Emberbud Salve',kind:'revive',amount:0.5,price:160,desc:'Wakes a fainted Kindra at half HP.'},
  lantern:       {name:'Lumen Lantern',kind:'tool',price:120,desc:'Quiets the drift. Wild encounters for 400 steps.'},
  bell:          {name:'Tidebell',kind:'tool',price:85,desc:'One guaranteed clean escape from any wild battle.'},
  emberglass:    {name:'Emberglass',kind:'catalyst',price:220,desc:'Catalyst. Draws fire-kinds through their next form.'},
  verdantseed:   {name:'Verdant Seed',kind:'catalyst',price:220,desc:'Catalyst. Draws growth-kinds through their next form.'},
  tidepearl:     {name:'Tidepearl',kind:'catalyst',price:220,desc:'Catalyst. Draws current-kinds through their next form.'},
  slatecore:     {name:'Slatecore',kind:'catalyst',price:240,desc:'Catalyst. Draws stone-kinds through their next form.'},
  windvane:      {name:'Windvane',kind:'catalyst',price:250,desc:'Catalyst. Draws air-kinds through their next form.'},
  voidglass:     {name:'Voidglass',kind:'catalyst',price:300,desc:'Catalyst. Draws hollow-kinds through their next form.'},
  prismheart:    {name:'Prism Heart',kind:'catalyst',price:360,desc:'Catalyst. Draws light-kinds through their next form.'}
};
const SHOP_STOCK = ['shard_dull','shard_bright','shard_flawless','draught_sun','tonic_deep','salve_ember',
  'lantern','bell','emberglass','verdantseed','tidepearl','slatecore','windvane','voidglass','prismheart'];

/* ---------- STATUS DEFS ---------- */
const STATUS = {
  burn:  {name:'Burn',   c:'#ff7a45', desc:'Loses HP each turn. ATK halved.'},
  frost: {name:'Frost',  c:'#4fc9e8', desc:'SPD halved.'},
  shock: {name:'Shock',  c:'#f3d78a', desc:'May lose the turn entirely.'},
  barrier:{name:'Barrier',c:'#8df29a',desc:'Deflects the next damaging hit.'},
  regen: {name:'Regen',  c:'#6fdc7a', desc:'Recovers HP each turn.'}
};

/* ---------- ZONES ---------- */
const ZONES = [
  {id:'cinderwake',name:'Cinderwake Flats',part:'ember',rate:.13,lv:[3,6],guard:'vulkarn',
   sky:['#2b0f1a','#6a2620','#c9552b'],ground:['#3a1c1e','#5a2a22'],drift:['#7a3a24','#b35a2c'],
   wall:'#241016',glow:'#ff8a5c',pool:['cindling','mossprig','rillit'],
   blurb:'Ash over hardpan. The air tastes of cold kilns.'},
  {id:'mirelume',name:'Mirelume Thicket',part:'spore',rate:.15,lv:[8,13],guard:'vael',
   sky:['#08201a','#13402f','#2f7a4a'],ground:['#12301f','#1c4630'],drift:['#255c34','#3d8a45'],
   wall:'#0a1d14',glow:'#8df29a',pool:['mossprig','funglet','thistlecloak','wispwake'],
   blurb:'Green light filtered through a hundred closed leaves.'},
  {id:'glasswater',name:'Glasswater Shelf',part:'bubble',rate:.14,lv:[14,19],guard:'seshen',
   sky:['#061a2c','#0f3c58','#2f86ad'],ground:['#0d2a3c','#164860'],drift:['#1d5f7d','#3aa0c0'],
   wall:'#061722',glow:'#4fc9e8',pool:['rillit','brinestrike','tidelurker','wispwake'],
   blurb:'A shallow sea held perfectly still, one finger-width deep.'},
  {id:'slatecradle',name:'Slatecradle Ridge',part:'dust',rate:.14,lv:[20,25],guard:'obdun',
   sky:['#1a1030','#3a2456','#7a5a3a'],ground:['#241a2e','#3a2a40'],drift:['#4a3550','#7a5a52'],
   wall:'#150d22',glow:'#c8a06a',pool:['pebblume','geodex','tidelurker','cinderspire'],
   blurb:'Stacked plates of stone, tilted like an unread book.'},
  {id:'howlspire',name:'Howlspire Reach',part:'snow',rate:.16,lv:[28,34],guard:'cirrus',
   sky:['#0a1a30','#1c3c66','#6ea8d8'],ground:['#132238','#1e3550'],drift:['#2c4a70','#5f8fc0'],
   wall:'#08121f',glow:'#a8e0ff',pool:['zephlet','downwhirl','cinderspire','skyshear'],
   blurb:'Thin air, thin ice, and a wind that never finishes a sentence.'},
  {id:'nullsable',name:'Nullsable Deep',part:'void',rate:.17,lv:[36,44],guard:'nullsable',
   sky:['#0c0618','#241040','#4a1f6a'],ground:['#160c26','#241436'],drift:['#331c4a','#5a2f7a'],
   wall:'#0a0412',glow:'#b17ce0',pool:['nullamb','duskocoon','glimmoth','skyshear'],
   blurb:'Where the weave has already come undone. Lines stop meeting here.'}
];

/* ---------- STORY BEATS ---------- */
const STORY = {
  intro:[
    {who:'The Hollow Tide',t:'You arrive between heartbeats, the way all Binders do — mid-stride, breath held, one boot still somewhere else.'},
    {who:'The Hollow Tide',t:'Six Reaches. Six anchors. Countless Kindra sleeping in the drift between them.'},
    {who:'Warden of the First Shrine',t:'Binder. The lattice is fraying at the edges and the Tide is patient, which is worse than being strong.'},
    {who:'Warden of the First Shrine',t:'Walk. Bind what answers you. Anchor the reaches before the Tide takes the rest. That is the whole of the work.'}
  ],
  before1:[{who:'Warden of the First Shrine',t:'Something heavy sleeps at the heart of the Flat. Wake it, and it will test whether you are worth the shard you carry.'}],
  after1:[{who:'The Hollow Tide',t:'One anchor, set. Somewhere far off, a strand stops vibrating.'},
          {who:'Warden of the Second Shrine',t:'The Thicket has opened. Rootsinger Vael keeps it shut. She will not be argued with — only unmade.'}],
  after2:[{who:'The Hollow Tide',t:'Two anchors. The drift thickens; the Kindra grow bolder, and stranger.'},
          {who:'Warden of the Third Shrine',t:'Glasswater next. Walk carefully — the shelf is a mirror, and you are not the first thing it has shown.'}],
  after3:[{who:'Warden of the Fourth Shrine',t:'Slatecradle. Obdun walks it once a year and steps where he likes. Prefer that you are not where he likes.'}],
  after4:[{who:'The Hollow Tide',t:'Four anchors set. Four strands singing again. The Tide shifts its weight.'},
          {who:'Warden of the Fourth Shrine',t:'You have done the impossible thing already. But Howlspire is screaming, and the Deep has begun to eat its own edges.'},
          {who:'Warden of the Fourth Shrine',t:'Two reaches remain, and they are worse than the four behind you. Go anyway.'}],
  after5:[{who:'Warden of the Sixth Shrine',t:'The Deep. Where the weave stopped. Whatever you bind there will not want to leave.'}],
  final:[
    {who:'Nullsable, the Hollow',t:'You are a knot. A small, stubborn knot in a thing that is very tired of knots.'},
    {who:'Nullsable, the Hollow',t:'I will undo you last. I want you to see it finished.'},
    {who:'The Hollow Tide',t:'...the lattice takes the thread. Six anchors singing. The fraying stops — not mended, but held.'},
    {who:'The Hollow Tide',t:'You are a Binder. That is the entire job, and it is never once finished. The reaches are yours to walk.'}
  ]
};
/* ================================================================
   PART 2 — CORE ENGINE: utils, state, save, audio
   ================================================================ */
const $ = s=>document.querySelector(s);
const $$ = s=>Array.from(document.querySelectorAll(s));
const el = (tag,cls,html)=>{const e=document.createElement(tag); if(cls)e.className=cls; if(html!=null)e.innerHTML=html; return e;};
const clamp=(v,a,b)=>v<a?a:v>b?b:v;
const rnd=(a,b)=>a+Math.random()*(b-a);
const ri=(a,b)=>Math.floor(rnd(a,b+1));
const pick=a=>a[Math.floor(Math.random()*a.length)];
function mulberry32(a){return function(){a|=0;a=a+0x6D2B79F5|0;let t=Math.imul(a^a>>>15,1|a);t=t+Math.imul(t^t>>>7,61|t)^t;return((t^t>>>14)>>>0)/4294967296;}}
const esc=s=>String(s).replace(/[&<>"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]));

/* ---------- STAT MATH ---------- */
function statAt(base,lv){ return Math.floor(base*(1+lv/38)); }
function hpAt(baseHp,lv){ return Math.floor(baseHp*2.5*Math.pow(Math.max(1,lv)/5,1.45))+40; }
function expToNext(lv){ return Math.floor(10+Math.pow(lv,1.75)*1.3); }
function weaveMult(pow,lv){ return pow/2.4; }
const TYPE_COLOR = a=>AFF[a]?AFF[a].c:'#fff';

/* ---------- GLOBAL STATE ---------- */
let S = null;
let screen = 'title';
let G = {};            /* transient engine state (maps, particles, camera) */
const SAVE_KEY = 'hollowtide.v1';
let audioCtx=null, audioMaster=null, musicOn=true, sfxOn=true, musicTimer=null, musicStep=0, padNodes=null;

function newCreature(sp,lv){
  const base = CRE[sp];
  if(!base) return null;
  const c = {uid:'k'+Math.random().toString(36).slice(2,9), sp, lv:clamp(lv|0,1,50), exp:0,
    affs:base.affs.slice(), known:[], moves:[], status:null, sTurns:0, stg:{atk:0,def:0,spd:0}};
  c.known = learnableAt(sp,c.lv);
  c.moves = c.known.slice(0,4);
  recalc(c,true);
  return c;
}
function learnableAt(sp,lv){
  const out=[];
  for(const [l,a] of (CRE[sp].learn||[])) if(l<=lv && out.indexOf(a)<0) out.push(a);
  if(!out.length) out.push(CRE[sp].learn&&CRE[sp].learn.length?CRE[sp].learn[0][1]:'cinderlash');
  return out;
}
function recalc(c,full){
  const b=CRE[c.sp].base;
  const oldMax = c.maxhp||1;
  c.maxhp = hpAt(b.hp,c.lv);
  if(full||c.hp==null) c.hp = c.maxhp;
  else c.hp = Math.min(c.hp + (c.maxhp-oldMax), c.maxhp);
  c.atk = statAt(b.atk,c.lv); c.def = statAt(b.def,c.lv); c.spd = statAt(b.spd,c.lv);
}
function name(c){ const b=CRE[c.sp]; if(b) return b.name; const g=GUARDIANS[c.sp]; return g?g.name:'?'; }
function isBoss(c){ return !!GUARDIANS[c.sp]; }
function affsOf(c){ return GUARDIANS[c.sp] ? GUARDIANS[c.sp].affs : CRE[c.sp].affs; }
function displayName(c){ return name(c); }

/* ---------- SAVE / LOAD ---------- */
function blankState(slot){
  return {
    slot:slot, created:Date.now(), played:0,
    motes:260, bag:{shard_dull:6,draught_sun:3,lantern:0,bell:1},
    party:[], storage:[], dex:{}, bound:{},
    zone:0, unlocked:[true,false,false,false,false,false],
    guards:{}, anchors:0, pos:{x:2,y:10}, healedZone:-1,
    story:0, flags:{tut:0,steps:0,lantern:0,post:false,started:false},
    lastShrine:'Warden of the First Shrine'
  };
}
function saveGame(slot,silent){
  try{
    const all = JSON.parse(localStorage.getItem(SAVE_KEY)||'{}');
    all['s'+slot] = JSON.parse(JSON.stringify(S));
    localStorage.setItem(SAVE_KEY, JSON.stringify(all));
    if(!silent) toast('Progress anchored.');
    return true;
  }catch(e){ if(!silent) toast('Could not write the lattice.'); return false; }
}
function listSaves(){ try{ return JSON.parse(localStorage.getItem(SAVE_KEY)||'{}'); }catch(e){ return {}; } }
function loadSlot(slot){
  const all=listSaves(); const d=all['s'+slot];
  if(!d) return false;
  S=Object.assign(blankState(slot),d);
  S.party = S.party.filter(c=>CRE[c.sp]||GUARDIANS[c.sp]);
  return true;
}

/* ---------- TOAST ---------- */
function toast(msg){
  const t=el('div','toast',esc(msg)); $('#toastWrap').appendChild(t);
  setTimeout(()=>t.remove(),2300);
}

/* ================================================================
   AUDIO — generative ambience + chiptune, all synthesized
   ================================================================ */
function initAudio(){
  if(audioCtx) return;
  try{
    audioCtx = new (window.AudioContext||window.webkitAudioContext)();
    audioMaster = audioCtx.createGain();
    audioMaster.gain.value = 0.5;
    audioMaster.connect(audioCtx.destination);
  }catch(e){ audioCtx=null; }
}
const SCALES = {
  cinderwake:[0,3,5,7,10], mirelume:[0,2,4,7,9], glasswater:[0,3,5,7,10],
  slatecradle:[0,2,3,7,8], howlspire:[0,2,4,7,11], nullsable:[0,1,5,6,10],
  battle:[0,3,5,6,10], title:[0,4,7,11,14]
};
const ROOTS = {cinderwake:110,mirelume:98,glasswater:123.47,slatecradle:92.5,howlspire:130.81,nullsable:82.41,battle:103.83,title:87.31};
function startPad(root){
  if(!audioCtx) return;
  stopPad();
  const g=audioCtx.createGain(); g.gain.value=0; g.connect(audioMaster);
  const filt=audioCtx.createBiquadFilter(); filt.type='lowpass'; filt.frequency.value=520; filt.Q.value=1.1;
  filt.connect(g);
  const o1=audioCtx.createOscillator(); o1.type='sawtooth'; o1.frequency.value=root/2;
  const o2=audioCtx.createOscillator(); o2.type='sine'; o2.frequency.value=root/2*1.005;
  const o3=audioCtx.createOscillator(); o3.type='triangle'; o3.frequency.value=root*0.75;
  const lfo=audioCtx.createOscillator(); lfo.frequency.value=0.07; const lg=audioCtx.createGain(); lg.gain.value=260;
  lfo.connect(lg); lg.connect(filt.frequency);
  [o1,o2,o3].forEach(o=>{o.connect(filt); o.start();});
  lfo.start();
  g.gain.linearRampToValueAtTime(0.10, audioCtx.currentTime+2.4);
  padNodes={g,o1,o2,o3,lfo};
}
function stopPad(){
  if(!padNodes||!audioCtx) return;
  const p=padNodes; padNodes=null;
  try{ p.g.gain.cancelScheduledValues(audioCtx.currentTime);
    p.g.gain.setValueAtTime(p.g.gain.value,audioCtx.currentTime);
    p.g.gain.linearRampToValueAtTime(0,audioCtx.currentTime+0.6);
    [p.o1,p.o2,p.o3,p.lfo].forEach(o=>o.stop(audioCtx.currentTime+0.7));
  }catch(e){}
}
function blip(freq,dur,type,vol){
  if(!audioCtx||!sfxOn) return;
  const o=audioCtx.createOscillator(), g=audioCtx.createGain();
  o.type=type||'square'; o.frequency.setValueAtTime(freq,audioCtx.currentTime);
  g.gain.setValueAtTime(0,audioCtx.currentTime);
  g.gain.linearRampToValueAtTime((vol||0.09),audioCtx.currentTime+0.008);
  g.gain.exponentialRampToValueAtTime(0.0001,audioCtx.currentTime+(dur||0.12));
  o.connect(g); g.connect(audioMaster); o.start(); o.stop(audioCtx.currentTime+(dur||0.12)+0.02);
}
function noiseBurst(dur,vol){
  if(!audioCtx||!sfxOn) return;
  const n=audioCtx.sampleRate*dur|0, buf=audioCtx.createBuffer(1,n,audioCtx.sampleRate), d=buf.getChannelData(0);
  for(let i=0;i<n;i++) d[i]=(Math.random()*2-1)*Math.pow(1-i/n,2.2);
  const src=audioCtx.createBufferSource(); src.buffer=buf;
  const g=audioCtx.createGain(); g.gain.value=vol||0.16;
  const f=audioCtx.createBiquadFilter(); f.type='bandpass'; f.frequency.value=900; f.Q.value=0.8;
  src.connect(f); f.connect(g); g.connect(audioMaster); src.start();
}
const SFX = {
  step:()=>blip(220+Math.random()*40,0.045,'triangle',0.03),
  ui:()=>blip(660,0.05,'square',0.04),
  confirm:()=>{blip(520,0.07,'square',0.06); setTimeout(()=>blip(780,0.09,'square',0.055),60);},
  cancel:()=>blip(300,0.08,'square',0.05),
  hit:(t)=>{noiseBurst(0.14,0.14); blip(t>1?180:300,0.12,'sawtooth',0.07);},
  crit:()=>{noiseBurst(0.22,0.2); blip(140,0.2,'sawtooth',0.1); setTimeout(()=>blip(90,0.24,'sawtooth',0.09),70);},
  weak:()=>{noiseBurst(0.09,0.08); blip(200,0.08,'triangle',0.04);},
  heal:()=>{blip(600,0.1,'sine',0.07); setTimeout(()=>blip(900,0.14,'sine',0.06),90); setTimeout(()=>blip(1180,0.2,'sine',0.05),180);},
  bind:()=>{blip(440,0.1,'triangle',0.07);[0,120,240,360].forEach((d,i)=>setTimeout(()=>blip(520+i*170,0.16,'sine',0.06),d));},
  fail:()=>{[0,90].forEach((d,i)=>setTimeout(()=>blip(320-i*90,0.16,'sawtooth',0.06),d));},
  levelup:()=>{[0,90,180,300].forEach((d,i)=>setTimeout(()=>blip(523*Math.pow(1.26,i),0.2,'triangle',0.07),d));},
  faint:()=>{[0,140,300].forEach((d,i)=>setTimeout(()=>blip(300-i*80,0.3,'sine',0.07),d));},
  encounter:()=>{[0,80,160,260].forEach((d,i)=>setTimeout(()=>blip([392,523,659,784][i],0.14,'square',0.07),d));},
  anchor:()=>{[0,110,230,380,560].forEach((d,i)=>setTimeout(()=>blip([262,330,392,494,587][i],0.34,'triangle',0.08),d));}
};
function musicLoop(){
  if(musicTimer) clearInterval(musicTimer);
  if(!musicOn||!audioCtx) return;
  musicStep=0;
  musicTimer=setInterval(()=>{
    if(!audioCtx||!musicOn) return;
    const key = (screen==='battle')?'battle':(screen==='title'?'title':(ZONES[S&&S.zone]?ZONES[S.zone].id:'cinderwake'));
    const sc = SCALES[key]||SCALES.cinderwake, root = ROOTS[key]||110;
    musicStep++;
    if(musicStep%2===0){
      const n = sc[(Math.floor(musicStep/2))%sc.length];
      const f = root*Math.pow(2,(n+ (musicStep%8===0?12:0))/12);
      if(sfxOn){ blip(f*2,0.16,'triangle',0.028); }
    }
    if(musicStep%8===0 && sfxOn){ blip(root/2,0.5,'sine',0.05); }
  },240);
}
function setMusic(on){
  musicOn=on;
  if(on){ initAudio(); if(audioCtx) audioCtx.resume(); startPad((ZONES[S&&S.zone]?ROOTS[ZONES[S.zone].id]:ROOTS.title)); musicLoop(); }
  else { stopPad(); if(musicTimer){clearInterval(musicTimer); musicTimer=null;} }
  $$('#btnMute0,#btnMuteM').forEach(b=>b.textContent = on?'♪ Sound On':'♪ Muted');
}

/* ================================================================
   CANVAS
   ================================================================ */
const cv=$('#cv'), ctx=cv.getContext('2d');
let VW=0,VH=0,DPR=1;
function resize(){
  DPR=Math.min(window.devicePixelRatio||1,2);
  VW=window.innerWidth; VH=window.innerHeight;
  cv.width=Math.floor(VW*DPR); cv.height=Math.floor(VH*DPR);
  ctx.setTransform(DPR,0,0,DPR,0,0);
}
window.addEventListener('resize',resize);

/* ---------- SCREEN SHAKE / FLASH ---------- */
function shake(power){
  const s=$('#stage'); s.classList.remove('shake'); void s.offsetWidth;
  s.style.setProperty('--sp',power||1); s.classList.add('shake');
  setTimeout(()=>s.classList.remove('shake'),420);
}
function flash(color,ms){
  const f=$('#flash'); f.style.background=color||'#fff'; f.style.transition='none'; f.style.opacity='.85';
  requestAnimationFrame(()=>{ f.style.transition='opacity '+(ms||260)+'ms ease'; f.style.opacity='0'; });
}
function floatText(txt,x,y,cls){
  const d=el('div','dmg '+(cls||''),txt);
  d.style.left=x+'px'; d.style.top=y+'px';
  $('#stage').appendChild(d); setTimeout(()=>d.remove(),1050);
}
/* ================================================================
   PART 3 — OVERWORLD: map, render, input
   ================================================================ */
const TILE={FLOOR:0,WALL:1,DRIFT:2,SHRINE:3,GUARD:4,EXIT:5};

function hashStr(s){ let h=2166136261; for(let i=0;i<s.length;i++){h^=s.charCodeAt(i); h=Math.imul(h,16777619);} return h>>>0; }

function makeMap(zi){
  const rng=mulberry32(9931+zi*7919);
  const W=30,H=20, g=new Uint8Array(W*H);
  const idx=(x,y)=>y*W+x;
  for(let y=0;y<H;y++) for(let x=0;x<W;x++) g[idx(x,y)] = (x===0||y===0||x===W-1||y===H-1)?TILE.WALL:TILE.FLOOR;
  /* scatter wall ridges */
  let placed=0, guard2=0;
  while(placed<30 && guard2++<600){
    const bx=2+Math.floor(rng()*(W-4)), by=2+Math.floor(rng()*(H-4));
    const len=1+Math.floor(rng()*4), horiz=rng()<0.5;
    for(let k=0;k<len;k++){
      const x=bx+(horiz?k:0), y=by+(horiz?0:k);
      if(x>1&&y>1&&x<W-2&&y<H-2){ if(g[idx(x,y)]!==TILE.WALL){g[idx(x,y)]=TILE.WALL; placed++;} }
    }
  }
  /* drift patches */
  for(let i=0;i<16;i++){
    const bx=2+Math.floor(rng()*(W-4)), by=2+Math.floor(rng()*(H-4));
    const r=1+Math.floor(rng()*2);
    for(let y=by-r;y<=by+r;y++) for(let x=bx-r;x<=bx+r;x++){
      if(x<1||y<1||x>W-2||y>H-2) continue;
      if(Math.hypot(x-bx,y-by) <= r+0.15) g[idx(x,y)]=TILE.DRIFT;
    }
  }
  const spawn={x:2,y:Math.floor(H/2)};
  const shrine={x:5,y:4};
  const guard={x:W-8,y:Math.floor(H/2)};
  const exit={x:W-2,y:Math.floor(H/2)};
  /* carve L corridors */
  function carve(a,b){
    let x=a.x,y=a.y;
    while(x!==b.x){ x+= x<b.x?1:-1; if(g[idx(x,y)]===TILE.WALL) g[idx(x,y)]=TILE.FLOOR; }
    while(y!==b.y){ y+= y<b.y?1:-1; if(g[idx(x,y)]===TILE.WALL) g[idx(x,y)]=TILE.FLOOR; }
  }
  carve(spawn,shrine); carve(spawn,guard); carve(guard,exit); carve(shrine,guard);
  /* clearing around guardian */
  for(let y=guard.y-2;y<=guard.y+2;y++) for(let x=guard.x-2;x<=guard.x+2;x++)
    if(x>0&&y>0&&x<W-1&&y<H-1 && Math.hypot(x-guard.x,y-guard.y)<=2.4) g[idx(x,y)]=TILE.DRIFT;
  g[idx(shrine.x,shrine.y)]=TILE.SHRINE;
  g[idx(guard.x,guard.y)]=TILE.GUARD;
  g[idx(exit.x,exit.y)]=TILE.EXIT;
  /* prune unreachable floor */
  const seen=new Uint8Array(W*H), q=[spawn]; seen[idx(spawn.x,spawn.y)]=1;
  while(q.length){
    const p=q.pop();
    for(const [dx,dy] of [[1,0],[-1,0],[0,1],[0,-1]]){
      const x=p.x+dx,y=p.y+dy;
      if(x<0||y<0||x>=W||y>=H) continue;
      if(seen[idx(x,y)]) continue;
      if(g[idx(x,y)]===TILE.WALL) continue;
      seen[idx(x,y)]=1; q.push({x,y});
    }
  }
  for(let i=0;i<W*H;i++) if(!seen[i] && g[i]!==TILE.WALL) g[i]=TILE.WALL;
  return {W,H,g,spawn,shrine,guard,exit};
}
function tileAt(m,x,y){ if(x<0||y<0||x>=m.W||y>=m.H) return TILE.WALL; return m.g[y*m.W+x]; }
function walkable(t){ return t!==TILE.WALL; }

/* ---------- ambient particles ---------- */
function makeParticles(zone,n){
  const arr=[];
  for(let i=0;i<n;i++) arr.push({
    x:Math.random(), y:Math.random(), s:rnd(0.6,2.6), sp:rnd(0.2,1),
    ph:Math.random()*6.283, drift:rnd(-1,1), a:rnd(0.25,0.85)
  });
  return arr;
}
const ZONE_PART = {
  ember:(p,t,w,h)=>({x:p.x*w+Math.sin(t*0.6+p.ph)*22, y:((p.y - t*0.045*p.sp)%1)*h, r:p.s*1.8, c:'#ff9a4c'}),
  spore:(p,t,w,h)=>({x:p.x*w+Math.sin(t*0.5+p.ph)*34, y:((p.y - t*0.022*p.sp)%1)*h, r:p.s*1.5, c:'#a8f0a0'}),
  bubble:(p,t,w,h)=>({x:p.x*w+Math.sin(t*0.35+p.ph)*18, y:((p.y - t*0.03*p.sp)%1)*h, r:p.s*1.6, c:'#8fd8ff'}),
  dust:(p,t,w,h)=>({x:((p.x + t*0.02*p.drift*p.sp)%1)*w, y:p.y*h+Math.sin(t*0.7+p.ph)*12, r:p.s*1.3, c:'#e0c89a'}),
  snow:(p,t,w,h)=>({x:p.x*w+Math.sin(t*0.4+p.ph)*30, y:((p.y + t*0.035*p.sp)%1)*h, r:p.s*1.7, c:'#dff2ff'}),
  void:(p,t,w,h)=>({x:p.x*w+Math.sin(t*0.25+p.ph)*46, y:p.y*h+Math.cos(t*0.3+p.ph*1.7)*26, r:p.s*1.4, c:'#c79cff'})
};

/* ---------- procedural beasts ---------- */
function drawBeast(c,x,y,size,affs,t,opts){
  opts=opts||{};
  const seed=hashStr(opts.seedKey||'x');
  const col=TYPE_COLOR(affs[0]), col2=TYPE_COLOR(affs[affs.length-1]);
  const form=seed%4;
  const lobes = form===1 ? 9+(seed%3) : (form===3 ? 4+(seed%3) : 5+(seed%4));
  const spike = form===1 ? 0.30 : (form===3 ? 0.20 : 0.13);
  const sqX = form===3 ? 0.82 : 1.0;
  const sqY = form===2 ? 1.22 : 0.92;
  const spin = form===3 ? -0.35 : 0;
  const bob=Math.sin(t*1.6+(seed%100))*size*0.045;
  ctx.save();
  ctx.translate(x,y+bob); ctx.rotate(spin);
  /* shadow */
  ctx.globalAlpha=0.34*(opts.alpha==null?1:opts.alpha);
  ctx.fillStyle='#000';
  ctx.beginPath(); ctx.ellipse(0,size*0.78-bob*0.5,size*0.6,size*0.16,0,0,6.283); ctx.fill();
  ctx.globalAlpha=(opts.alpha==null?1:opts.alpha);
  /* aura */
  const grd=ctx.createRadialGradient(0,0,size*0.1,0,0,size*1.5);
  grd.addColorStop(0,col+'88'); grd.addColorStop(0.5,col+'22'); grd.addColorStop(1,'transparent');
  ctx.fillStyle=grd; ctx.beginPath(); ctx.arc(0,0,size*1.5,0,6.283); ctx.fill();
  /* crest spikes */
  if(form===1||form===3){
    ctx.fillStyle=col2; ctx.globalAlpha=0.85;
    for(let i=-1;i<=1;i++){
      const bx0=i*size*0.34;
      ctx.beginPath();
      ctx.moveTo(bx0-size*0.11,-size*0.55);
      ctx.lineTo(bx0+ size*0.0, -size*(1.02+(i===0?0.14:0)));
      ctx.lineTo(bx0+size*0.13,-size*0.52);
      ctx.closePath(); ctx.fill();
    }
    ctx.globalAlpha=(opts.alpha==null?1:opts.alpha);
  }
  /* body blob */
  ctx.beginPath();
  for(let i=0;i<=72;i++){
    const a=i/72*6.283;
    const r=size*(0.90+spike*Math.sin(a*lobes+t*0.9+seed)+0.06*Math.sin(a*2-t*1.4));
    const px=Math.cos(a)*r*sqX, py=Math.sin(a)*r*sqY;
    i?ctx.lineTo(px,py):ctx.moveTo(px,py);
  }
  ctx.closePath();
  const bg=ctx.createLinearGradient(0,-size*sqY,0,size*sqY);
  bg.addColorStop(0,col2); bg.addColorStop(0.5,col); bg.addColorStop(1,'#0b1020');
  ctx.fillStyle=bg; ctx.fill();
  ctx.lineWidth=2.4; ctx.strokeStyle=col; ctx.stroke();
  /* belly */
  ctx.globalAlpha=0.22;
  const belly=ctx.createRadialGradient(0,size*0.34,2,0,size*0.34,size*0.55);
  belly.addColorStop(0,col2); belly.addColorStop(1,'transparent');
  ctx.fillStyle=belly; ctx.beginPath(); ctx.ellipse(0,size*0.34,size*0.45,size*0.3,0,0,6.283); ctx.fill();
  ctx.globalAlpha=(opts.alpha==null?1:opts.alpha);
  /* eyes */
  const ey=-size*(form===2?0.24:0.12), ex=size*0.34;
  ctx.fillStyle='#f6fbff';
  for(const s of [-1,1]){ ctx.beginPath(); ctx.ellipse(s*ex,ey,size*0.17,size*0.2,0,0,6.283); ctx.fill(); }
  ctx.fillStyle='#08101c';
  for(const s of [-1,1]){ ctx.beginPath(); ctx.ellipse(s*ex+Math.sin(t*0.8)*size*0.03,ey+size*0.03,size*0.085,size*0.11,0,0,6.283); ctx.fill(); }
  /* brow crest */
  ctx.strokeStyle=col2; ctx.lineWidth=3.2;
  ctx.beginPath(); ctx.moveTo(-size*0.5,-size*(sqY*0.42)); ctx.quadraticCurveTo(0,-size*0.8,size*0.5,-size*(sqY*0.42)); ctx.stroke();
  /* orbiting motes */
  for(let i=0;i<4;i++){
    const a=t*(0.7+i*0.22)+i*1.7+seed%7;
    const rr=size*(1.12+i*0.09);
    ctx.globalAlpha=0.55; ctx.fillStyle=col;
    ctx.beginPath(); ctx.arc(Math.cos(a)*rr,Math.sin(a*0.9)*rr*0.5,size*0.055,0,6.283); ctx.fill();
  }
  ctx.globalAlpha=1;
  ctx.restore();
}

/* ---------- world render ---------- */
let TS=44;
function drawWorld(dt){
  const zi=S.zone, z=ZONES[zi], m=G.maps[zi];
  const p=G.player;
  TS=clamp(Math.min(VW/15.5, VH/10.5), 30, 58);
  /* sky gradient */
  const sg=ctx.createLinearGradient(0,0,0,VH);
  sg.addColorStop(0,z.sky[0]); sg.addColorStop(0.55,z.sky[1]); sg.addColorStop(1,z.sky[2]);
  ctx.fillStyle=sg; ctx.fillRect(0,0,VW,VH);
  /* horizon glow */
  const hg=ctx.createRadialGradient(VW*0.5,VH*0.22,10,VW*0.5,VH*0.22,VH*0.8);
  hg.addColorStop(0,z.glow+'3a'); hg.addColorStop(1,'transparent');
  ctx.fillStyle=hg; ctx.fillRect(0,0,VW,VH);

  const camX=G.cam.x-VW/2, camY=G.cam.y-VH/2;
  const x0=Math.max(0,Math.floor(camX/TS)-1), x1=Math.min(m.W-1,Math.ceil((camX+VW)/TS));
  const y0=Math.max(0,Math.floor(camY/TS)-1), y1=Math.min(m.H-1,Math.ceil((camY+VH)/TS));
  ctx.save(); ctx.translate(-camX,-camY);
  for(let y=y0;y<=y1;y++) for(let x=x0;x<=x1;x++){
    const t=tileAt(m,x,y), px=x*TS, py=y*TS;
    const h=hashStr(x+','+y+zi), v=(h%100)/100;
    if(t===TILE.WALL){
      ctx.fillStyle=z.wall;
      roundRect(px+1.5,py+1.5,TS-3,TS-3,7); ctx.fill();
      ctx.fillStyle='rgba(255,255,255,'+(0.05+v*0.05)+')';
      roundRect(px+4,py+4,TS-8,TS*0.26,5); ctx.fill();
      ctx.strokeStyle='rgba(0,0,0,.5)'; ctx.lineWidth=1;
      roundRect(px+1.5,py+1.5,TS-3,TS-3,7); ctx.stroke();
    } else {
      const g=ctx.createLinearGradient(px,py,px,py+TS);
      g.addColorStop(0,z.ground[0]); g.addColorStop(1,z.ground[1]);
      ctx.fillStyle=g;
      roundRect(px+1.5,py+1.5,TS-3,TS-3,5); ctx.fill();
      ctx.fillStyle='rgba(255,255,255,'+(0.012+v*0.035)+')';
      roundRect(px+1.5,py+1.5,TS-3,TS-3,5); ctx.fill();
      if(t===TILE.DRIFT){
        const dg=ctx.createLinearGradient(px,py,px,py+TS);
        dg.addColorStop(0,z.drift[0]); dg.addColorStop(1,z.drift[1]);
        ctx.fillStyle=dg; roundRect(px+1,py+1,TS-2,TS-2,4); ctx.fill();
        ctx.strokeStyle=z.glow+'66'; ctx.lineWidth=1.4;
        for(let i=0;i<5;i++){
          const ox=px+TS*(0.16+i*0.17), sw=Math.sin(G.t*1.1+i*1.3+ (h%13))*TS*0.1;
          ctx.beginPath(); ctx.moveTo(ox,py+TS*0.82);
          ctx.quadraticCurveTo(ox+sw,py+TS*0.5, ox+sw*0.5,py+TS*0.2); ctx.stroke();
        }
        ctx.globalAlpha=0.22+0.16*Math.sin(G.t*2+(h%7)); ctx.fillStyle=z.glow;
        roundRect(px+1,py+1,TS-2,TS-2,4); ctx.fill(); ctx.globalAlpha=1;
      }
    }
  }
  /* shrine */
  const sx=m.shrine.x*TS+TS/2, sy=m.shrine.y*TS+TS/2;
  drawShrine(sx,sy,TS);
  /* exit portal */
  if(zi<5 || S.flags.post){
    const ex=m.exit.x*TS+TS/2, ey=m.exit.y*TS+TS/2;
    const unlocked = (zi+1<6 && S.unlocked[zi+1]);
    drawPortal(ex,ey,TS,unlocked);
  }
  /* guardian */
  if(!S.guards[z.guard]){
    const gx=m.guard.x*TS+TS/2, gy=m.guard.y*TS+TS/2;
    const gn=GUARDIANS[z.guard];
    drawBeast(null,gx,gy-TS*0.5,TS*0.85,gn.affs,G.t,{seedKey:gn.id});
    ctx.globalAlpha=0.5+0.3*Math.sin(G.t*2);
    ctx.fillStyle=z.glow;
    ctx.font='600 '+Math.round(TS*0.28)+'px '+getComputedStyle(document.body).fontFamily;
    ctx.textAlign='center';
    ctx.fillText('⚑ '+gn.name, gx, gy-TS*1.5);
    ctx.globalAlpha=1;
  }
  /* player */
  drawPlayer(p.cx,p.cy,TS);
  ctx.restore();
  drawParticles(z,dt,false);
  /* soft edge darkening */
  const eg=ctx.createRadialGradient(VW/2,VH/2,Math.min(VW,VH)*0.32,VW/2,VH/2,Math.max(VW,VH)*0.75);
  eg.addColorStop(0,'transparent'); eg.addColorStop(1,'rgba(0,0,0,.65)');
  ctx.fillStyle=eg; ctx.fillRect(0,0,VW,VH);
}
function roundRect(x,y,w,h,r){
  ctx.beginPath();
  ctx.moveTo(x+r,y); ctx.arcTo(x+w,y,x+w,y+h,r); ctx.arcTo(x+w,y+h,x,y+h,r);
  ctx.arcTo(x,y+h,x,y,r); ctx.arcTo(x,y,x+w,y,r); ctx.closePath();
}
function drawShrine(x,y,TS){
  ctx.save(); ctx.translate(x,y);
  const pulse=0.6+0.4*Math.sin(G.t*1.6);
  const g=ctx.createRadialGradient(0,-TS*0.1,4,0,-TS*0.1,TS*1.5);
  g.addColorStop(0,'rgba(160,240,255,'+(0.4*pulse)+')'); g.addColorStop(1,'transparent');
  ctx.fillStyle=g; ctx.beginPath(); ctx.arc(0,-TS*0.1,TS*1.5,0,6.283); ctx.fill();
  ctx.fillStyle='#1b2b44'; ctx.strokeStyle='#9fe9ff'; ctx.lineWidth=2;
  roundRect(-TS*0.22,-TS*0.95,TS*0.44,TS*0.95,TS*0.1); ctx.fill(); ctx.stroke();
  ctx.fillStyle='rgba(160,240,255,'+(0.5+0.5*pulse)+')';
  ctx.beginPath(); ctx.arc(0,-TS*0.62,TS*0.13,0,6.283); ctx.fill();
  ctx.restore();
}
function drawPortal(x,y,TS,unlocked){
  ctx.save(); ctx.translate(x,y);
  for(let i=0;i<3;i++){
    const a=G.t*(1.2+i*0.5)+i*2;
    ctx.strokeStyle=unlocked? 'rgba(150,235,255,.75)':'rgba(120,140,170,.35)';
    ctx.lineWidth=3-i*0.6;
    ctx.beginPath(); ctx.ellipse(0,0,TS*0.34+i*4,TS*0.5+i*3,a*0.2,0,6.283); ctx.stroke();
  }
  ctx.fillStyle=unlocked?'rgba(120,230,255,.28)':'rgba(90,110,140,.18)';
  ctx.beginPath(); ctx.ellipse(0,0,TS*0.3,TS*0.44,0,0,6.283); ctx.fill();
  ctx.restore();
}
function drawPlayer(x,y,TS){
  const p=G.player;
  const bob=Math.sin(G.t*7)*(p.moving?2.4:1.2);
  ctx.save(); ctx.translate(x,y+bob*0.4);
  const g=ctx.createRadialGradient(0,0,2,0,0,TS*0.95);
  g.addColorStop(0,'rgba(140,255,235,.55)'); g.addColorStop(1,'transparent');
  ctx.fillStyle=g; ctx.beginPath(); ctx.arc(0,0,TS*0.95,0,6.283); ctx.fill();
  ctx.fillStyle='#0d1a2c'; ctx.strokeStyle='#7ff0dc'; ctx.lineWidth=2.6;
  ctx.beginPath(); ctx.arc(0,0,TS*0.3,0,6.283); ctx.fill(); ctx.stroke();
  ctx.fillStyle='#c9fff4';
  const dx=p.dir.x, dy=p.dir.y;
  ctx.beginPath(); ctx.arc(dx*TS*0.14,dy*TS*0.14,TS*0.1,0,6.283); ctx.fill();
  ctx.fillStyle='#7ff0dc';
  ctx.beginPath(); ctx.arc(0,0,TS*0.07,0,6.283); ctx.fill();
  ctx.restore();
}
function drawParticles(z,dt,front){
  if(!G.parts[z.id]) G.parts[z.id]=makeParticles(z,70);
  const fn=ZONE_PART[z.part]||ZONE_PART.dust;
  ctx.save(); ctx.globalCompositeOperation='lighter';
  for(const p of G.parts[z.id]){
    const q=fn(p,G.t,VW,VH);
    ctx.globalAlpha=p.a*(front?0.9:0.55);
    ctx.fillStyle=q.c;
    ctx.beginPath(); ctx.arc(q.x,q.y,q.r,0,6.283); ctx.fill();
  }
  ctx.restore();
}
/* ---------- INPUT ---------- */
const keys={up:false,down:false,left:false,right:false};
let dpad={up:false,down:false,left:false,right:false};
function keyName(e){
  const k=e.key;
  if(k==='ArrowUp'||k==='w'||k==='W') return 'up';
  if(k==='ArrowDown'||k==='s'||k==='S') return 'down';
  if(k==='ArrowLeft'||k==='a'||k==='A') return 'left';
  if(k==='ArrowRight'||k==='d'||k==='D') return 'right';
  return null;
}
window.addEventListener('keydown',e=>{
  initAudio();
  const d=keyName(e); if(d){ keys[d]=true; e.preventDefault(); }
  else if(e.key===' '||e.key==='Enter'){
    e.preventDefault();
    if(!$('#dialog').classList.contains('hidden')) { advanceDialog(); return; }
    if(!$('#capture').classList.contains('hidden')) { captureLock(); return; }
    if(screen==='world') doInteract();
    else if(screen==='battle') battleConfirm();
  }
  else if(e.key==='Escape'||e.key==='m'||e.key==='M'){
    if(screen==='world') openMenu();
    else if(screen==='menu') closeMenu();
  }
});
window.addEventListener('keyup',e=>{ const d=keyName(e); if(d){ keys[d]=false; e.preventDefault(); } });
/* touch */
$$('.dpad button').forEach(b=>{
  const d=b.dataset.dir;
  const on=e=>{ e.preventDefault(); dpad[d]=true; };
  const off=e=>{ e.preventDefault(); dpad[d]=false; };
  b.addEventListener('touchstart',on,{passive:false}); b.addEventListener('touchend',off);
  b.addEventListener('touchcancel',off); b.addEventListener('mousedown',on); b.addEventListener('mouseup',off);
  b.addEventListener('mouseleave',off);
});
$('#touchAction').addEventListener('click',()=>{ initAudio(); if(screen==='world') doInteract(); else if(screen==='battle') battleConfirm(); });
$('#dialog').addEventListener('click',e=>{ e.stopPropagation(); advanceDialog(); });

/* ---------- WORLD UPDATE ---------- */
function enterZone(zi,opts){
  S.zone=zi; S.pos={x:2,y:10};
  const m=makeMap(zi); G.maps[zi]=m;
  if(!G.parts[ZONES[zi].id]) G.parts[ZONES[zi].id]=makeParticles(ZONES[zi],70);
  const sp = opts&&opts.entry==='start' ? {x:2,y:Math.floor(m.H/2)} : {x:2,y:Math.floor(m.H/2)};
  G.player={tx:sp.x,ty:sp.y,px:sp.x,py:sp.y,fx0:sp.x,fy0:sp.y,moving:false,prog:0,dir:{x:1,y:0},cx:0,cy:0};
  G.encCd=8;
  updatePlayerPixels(); G.cam.x=G.player.cx; G.cam.y=G.player.cy;
  $('#zoneName').textContent=ZONES[zi].name;
  $('#zoneDot').style.background=ZONES[zi].glow;
  $('#zoneDot').style.boxShadow='0 0 12px '+ZONES[zi].glow;
  if(audioCtx&&musicOn){ startPad(ROOTS[ZONES[zi].id]); }
  updatePartyStrip();
  showHint(ZONES[zi].blurb,2600);
}
function updatePlayerPixels(){
  const p=G.player;
  p.cx=(p.px)*TS; p.cy=(p.py)*TS;
}
function easeIO(t){ return t<0.5 ? 2*t*t : -1+(4-2*t)*t; }
function worldUpdate(dt){
  const p=G.player, m=G.maps[S.zone];
  if(!p||!m) return;
  m.W0=m.W;
  const spd=7.6;
  if(!p.moving){
    let dx=0,dy=0;
    const up=keys.up||dpad.up, dn=keys.down||dpad.down, lf=keys.left||dpad.left, rt=keys.right||dpad.right;
    if(up) dy=-1; else if(dn) dy=1; else if(lf) dx=-1; else if(rt) dx=1;
    if(dx||dy){
      p.dir={x:dx,y:dy};
      const nx=p.tx+dx, ny=p.ty+dy;
      if(walkable(tileAt(m,nx,ny))){
        p.fx0=p.tx; p.fy0=p.ty; p.tx=nx; p.ty=ny; p.moving=true; p.prog=0;
        SFX.step();
      } else { p.prog=0; }
    }
  }
  if(p.moving){
    p.prog=Math.min(1,p.prog+dt*spd);
    const e=easeIO(p.prog);
    p.px=p.fx0+(p.tx-p.fx0)*e; p.py=p.fy0+(p.ty-p.fy0)*e;
    if(p.prog>=1){ p.moving=false; p.px=p.tx; p.py=p.ty; onArrive(); }
  }
  updatePlayerPixels();
  /* camera */
  G.cam.x += (p.cx-G.cam.x)*Math.min(1,dt*9);
  G.cam.y += (p.cy-G.cam.y)*Math.min(1,dt*9);
  G.player.cx=p.cx; /* used by renderer already */ 
  G.cam.x=clamp(G.cam.x, VW/2, Math.max(VW/2, m.W*TS-VW/2));
  G.cam.y=clamp(G.cam.y, VH/2, Math.max(VH/2, m.H*TS-VH/2));
  if(G.encCd>0) G.encCd-=dt*7.6;
  if(S.flags.lantern>0){ S.flags.lantern-=Math.max(1,Math.round(dt*7.6)); if(S.flags.lantern<=0) { S.flags.lantern=0; toast('The lantern gutters out.'); } }
}
function onArrive(){
  const m=G.maps[S.zone], p=G.player;
  S.flags.steps=(S.flags.steps||0)+1; S.played+=1;
  const t=tileAt(m,p.tx,p.ty);
  if(t===TILE.EXIT){
    const next=S.zone+1;
    if(next<6 && S.unlocked[next]){ transitionTo(()=>{ doTravel(next); }); }
    else { showHint('The way ahead is sealed. An anchor still holds it shut.',2400); if(!S.guards[ZONES[S.zone].guard]) setTimeout(()=>challengeGuardian(),300); }
    return;
  }
  if(t===TILE.GUARD && !S.guards[ZONES[S.zone].guard]){ setTimeout(()=>challengeGuardian(),220); return; }
  if(t===TILE.SHRINE && G.shrineCd<=0){ G.shrineCd=3; openShrine(); return; }
  if(t===TILE.DRIFT && G.encCd<=0){
    const z=ZONES[S.zone];
    let rate=z.rate*(S.flags.lantern>0?0.35:1);
    if(Math.random()<rate){ startWild(); return; }
  }
  tutStep();
}
function doTravel(next){
  enterZone(next);
  const z=ZONES[next];
  if(S.story===next){ /* handled below */ }
  if(!S.flags['seen_'+z.id]){
    S.flags['seen_'+z.id]=true;
    const beat = STORY['enter'+next];
    if(beat) queueDialog(beat);
  }
  saveGame(S.slot,true);
}
function challengeGuardian(){
  const z=ZONES[S.zone], gn=GUARDIANS[z.guard];
  queueDialog([{who:gn.name, t:gn.lore}], ()=>{ startGuardian(gn); });
}
function openShrine(){
  SFX.heal();
  healParty();
  saveGame(S.slot,true);
  openShrineMenu();
}

/* ---------- interaction (action button) ---------- */
function doInteract(){
  const m=G.maps[S.zone], p=G.player;
  const cx=p.tx, cy=p.ty;
  const t=tileAt(m,cx,cy);
  if(t===TILE.SHRINE){ openShrine(); return; }
  if(t===TILE.GUARD && !S.guards[ZONES[S.zone].guard]){ challengeGuardian(); return; }
  if(t===TILE.EXIT){ onArrive(); return; }
  /* face tile */
  const fx=cx+p.dir.x, fy=cy+p.dir.y;
  if(tileAt(m,fx,fy)===TILE.SHRINE){ openShrine(); return; }
  showHint('Nothing here but drift and quiet.',1400);
}

/* ---------- hints / tutorial ---------- */
let hintTimer=null;
function showHint(txt,ms){
  const h=$('#hint'); h.textContent=txt; h.classList.add('on');
  if(hintTimer) clearTimeout(hintTimer);
  hintTimer=setTimeout(()=>h.classList.remove('on'), ms||2000);
}
let bubbleEl=null;
function showBubble(txt,ms,pos){
  if(bubbleEl) bubbleEl.remove();
  bubbleEl=el('div','tutBubble',esc(txt));
  bubbleEl.style.left=(pos&&pos.x||Math.max(16,VW/2-125))+'px';
  bubbleEl.style.top=(pos&&pos.y||VH*0.24)+'px';
  $('#stage').appendChild(bubbleEl);
  if(ms) setTimeout(()=>{ if(bubbleEl){bubbleEl.remove(); bubbleEl=null;} }, ms);
}
function tutStep(){
  const f=S.flags;
  if(f.tut===0 && f.steps>=2){ f.tut=1; showBubble('Good. The Reaches are walked tile by tile. Shimmering drift hides Kindra — step into it.',5200,{x:VW*0.5-150,y:VH*0.26}); }
  else if(f.tut===1 && f.steps>=5){ f.tut=2; showBubble('A shrine-stone stands to the north. Step onto it to mend your Kindra and anchor your progress.',5200,{x:VW*0.5-150,y:VH*0.26}); }
}

/* ---------- transition ---------- */
let transEl=null;
function transitionTo(fn){
  if(transEl) return;
  transEl=el('div','','');
  transEl.style.cssText='position:absolute;inset:0;background:#02040a;z-index:55;opacity:0;transition:opacity .38s ease;pointer-events:none';
  $('#stage').appendChild(transEl);
  requestAnimationFrame(()=>{ transEl.style.opacity='1'; });
  setTimeout(()=>{ fn(); requestAnimationFrame(()=>{ transEl.style.opacity='0'; });
    setTimeout(()=>{ transEl.remove(); transEl=null; },420); },430);
}

/* ---------- PARTY HELPERS ---------- */
function healParty(){ for(const c of S.party){ c.hp=c.maxhp; c.status=null; c.sTurns=0; c.stg={atk:0,def:0,spd:0}; } updatePartyStrip(); }
function firstAlive(){ return S.party.find(c=>c.hp>0)||null; }
function partyAliveCount(){ return S.party.filter(c=>c.hp>0).length; }
function updatePartyStrip(){
  const strip=$('#partyStrip'); if(!strip) return;
  strip.innerHTML='';
  for(const c of S.party){
    const d=el('div','pstrip'+(c.hp<=0?' dead':''));
    d.innerHTML='<div class="dot" style="background:'+TYPE_COLOR(affsOf(c)[0])+';color:'+TYPE_COLOR(affsOf(c)[0])+'"></div>'+
      '<div style="flex:1;min-width:0"><div class="pname">'+esc(name(c))+'</div><div class="bar pbar"><i style="width:'+
      Math.max(0,Math.round(c.hp/c.maxhp*100))+'%;background:'+hpColor(c.hp/c.maxhp)+'"></i></div></div>'+
      '<span class="plv">'+c.lv+'</span>';
    strip.appendChild(d);
  }
  $('#moteChip').textContent='◈ '+S.motes;
}
function hpColor(r){ return r>0.5?'linear-gradient(90deg,#5fe6cf,#37c9a5)': r>0.2?'linear-gradient(90deg,#ffd76a,#ffa53c)':'linear-gradient(90deg,#ff7d7d,#e04a5e)'; }
/* ================================================================
   PART 4 — BATTLE
   ================================================================ */
const B = {
  on:false, units:[], queue:[], ti:0, round:0, pi:0, awaiting:null,
  wild:true, boss:null, over:false, busy:false, pos:{}, fx:[], flee:0,
  pendingCapture:null, lastMove:null, turnSide:null
};
function activeC(){ return S.party[B.pi]; }
function effSpd(c){ return c.spd*(c.status==='frost'?0.5:1)*(1+0.5*(c.stg.spd||0)); }
function effAtk(c){ return c.atk*(c.status==='burn'?0.5:1)*(1+0.5*(c.stg.atk||0)); }
function effDef(c){ return c.def*(1+0.5*(c.stg.def||0)); }

function wildRoll(){
  const z=ZONES[S.zone];
  const alive=S.party.filter(c=>c.hp>0).length;
  const r=Math.random();
  let n=1;
  if(alive>=6) n = r<0.30?1:(r<0.72?2:3);
  else if(alive>=4) n = r<0.50?1:(r<0.88?2:3);
  else if(alive>=3) n = r<0.68?1:2;
  else if(alive>=2) n = r<0.82?1:2;
  else n = r<0.90?1:2;
  const post = S.flags.post;
  const pool=z.pool.slice();
  const out=[];
  for(let i=0;i<n;i++){
    const sp=pick(pool);
    let lv=ri(z.lv[0],z.lv[1]) + (post?8:0);
    if(S.guards[z.guard] && z.lv[1]-z.lv[0]>3) lv=ri(z.lv[0]+2,z.lv[1]+ (post?8:2));
    out.push(newCreature(sp,clamp(lv,2,50)));
  }
  return out;
}
function startWild(){
  initAudio(); SFX.encounter();
  B.on=true; B.wild=true; B.boss=null; B.over=false; B.busy=true; B.flee=0; B.round=0;
  B.pendingCapture=null; B.fx=[];
  B.enemies=wildRoll().map(c=>({c,side:'enemy'}));
  openBattleUI();
  broadcastTurn();
  if(S.flags.tut<3){ S.flags.tut=3; showHint('Weaken it, then Bind with a shard. Nothing binds at full strength.',5200); }
}
function startGuardian(gn){
  initAudio(); SFX.anchor();
  B.on=true; B.wild=false; B.boss=gn; B.over=false; B.busy=true; B.flee=0; B.round=0; B.fx=[];
  const gg_=gn;
  const c={uid:'boss'+gg_.id, sp:gg_.id, lv:gg_.lk, exp:0, affs:gg_.affs.slice(),
    known:gg_.moves.slice(), moves:gg_.moves.slice(), status:null, sTurns:0, stg:{atk:0,def:0,spd:0},
    maxhp:hpAt(gg_.base.hp,gg_.lk)+60, atk:statAt(gg_.base.atk,gg_.lk), def:statAt(gg_.base.def,gg_.lk),
    spd:statAt(gg_.base.spd,gg_.lk), boss:true};
  c.hp=c.maxhp;
  B.enemies=[{c,side:'enemy'}];
  openBattleUI();
  broadcastTurn();
}
function openBattleUI(){
  screen='battle';
  $('#battle').classList.remove('hidden'); $('#battle').classList.add('on');
  $('#playerCard').classList.remove('hidden');
  $('#cmdPanel').classList.remove('hidden');
  $('#hud').classList.add('hidden');
  /* choose first alive */
  B.pi=S.party.findIndex(c=>c.hp>0); if(B.pi<0) B.pi=0;
  layoutBattle(); renderEnemyCards(); renderPlayerCard(); renderCommands();
  log(B.wild?'Wild Kindra block the way!':(B.boss.name+', '+ (B.boss.title||'the anchor') +' holds this reach!'), true);
  musicLoop();
}
function layoutBattle(){
  const n=B.enemies.length;
  const y=VH*0.30;
  for(let i=0;i<n;i++){
    const x=VW*(n===1?0.33:0.24+ i*(0.52/(n-1||1)));
    B.pos['e'+i]={x, y: y + (i%2)*VH*0.05};
  }
  B.pos.p={x:VW*0.56,y:VH*0.58};
}
function renderEnemyCards(){
  const row=$('#enemyRow'); row.innerHTML='';
  B.enemies.forEach((u,i)=>{
    const c=u.c, r=c.hp/c.maxhp;
    const card=el('div','ecard'+(c.hp<=0?' fainted':''));
    card.innerHTML='<div class="row1"><span class="nm">'+esc(name(c))+'</span><span class="lv">Lv '+c.lv+'</span></div>'+
      '<div class="hpTrack"><div class="hpghost" style="width:'+Math.round(r*100)+'%"></div><div class="hpFill" style="width:'+Math.round(r*100)+'%;background:'+hpColor(r)+'"></div></div>'+
      '<div class="row3"><div class="tpills">'+affsOf(c).map(a=>'<span class="tpill" style="color:'+TYPE_COLOR(a)+'">'+a+'</span>').join('')+'</div></div>'+
      '<div class="statuses">'+statusTags(c)+'</div>';
    row.appendChild(card);
  });
  renderPlayerCard();
}
function statusTags(c){
  let s='';
  if(c.status) s+='<span class="st" style="color:'+STATUS[c.status].c+'">'+STATUS[c.status].name+'</span>';
  if(c.barrier) s+='<span class="st" style="color:'+STATUS.barrier.c+'">Barrier</span>';
  if(c.stg&&c.stg.atk>0) s+='<span class="st" style="color:#ffd76a">ATK+</span>';
  if(c.stg&&c.stg.def>0) s+='<span class="st" style="color:#c8a06a">DEF+</span>';
  if(c.stg&&c.stg.spd>0) s+='<span class="st" style="color:#a8e0ff">SPD+</span>';
  return s;
}
function renderPlayerCard(){
  const c=activeC(); if(!c) return;
  const r=Math.max(0,c.hp/c.maxhp);
  $('#pName').textContent=name(c);
  $('#pLv').textContent='Lv '+c.lv;
  const f=$('#pHp'); f.style.width=Math.round(r*100)+'%'; f.style.background=hpColor(r);
  $('#pGhost').style.width=Math.round(r*100)+'%';
  $('#pHpText').textContent=Math.max(0,Math.round(c.hp))+' / '+c.maxhp;
  $('#pStatus').innerHTML=statusTags(c);
  $('#pExp').style.width=Math.round(c.exp/expToNext(c.lv)*100)+'%';
}
function log(t,bold){
  $('#battleLog').innerHTML = bold? ('<b>'+esc(t)+'</b>') : esc(t);
}

/* ---------- TURN ENGINE ---------- */
function buildQueue(){
  const list=[];
  const ac=activeC();
  if(ac&&ac.hp>0) list.push({c:ac,side:'player',idx:B.pi});
  B.enemies.forEach((u,i)=>{ if(u.c.hp>0) list.push({c:u.c,side:'enemy',idx:i}); });
  list.sort((a,b)=>effSpd(b.c)-effSpd(a.c));
  return list;
}
function broadcastTurn(){
  B.round++;
  B.queue=buildQueue();
  B.ti=0;
  nextTurn();
}
function pruneQueue(){ B.queue=B.queue.filter(u=>u.c.hp>0); }
function nextTurn(){
  if(B.over) return;
  if(checkEnd()) return;
  const tok = (B.tok = (B.tok||0)+1);
  pruneQueue();
  if(B.ti>=B.queue.length){ setTimeout(()=>{ if(B.tok===tok) broadcastTurn(); },260); return; }
  const u=B.queue[B.ti];
  if(!u||u.c.hp<=0){ B.ti++; return nextTurn(); }
  B.turnSide=u.side;
  /* start-of-turn status */
  const skip=tickStatus(u);
  if(checkEnd()) return;
  if(skip){ log(name(u.c)+' is wracked by shock and loses the turn.'); renderEnemyCards(); return setTimeout(()=>{ if(B.tok!==tok) return; B.ti++; nextTurn(); },780); }
  B.busy=false;
  if(u.side==='player'){ B.awaiting='command'; renderCommands(); }
  else { B.awaiting=null; renderCommands(); setTimeout(()=>{ if(B.tok===tok) enemyAct(u); },470); }
}
function tickStatus(u){
  const c=u.c; let skip=false;
  if(c.status==='burn'){
    const d=Math.max(1,Math.floor(c.maxhp*0.0625));
    c.hp=Math.max(0,c.hp-d);
    floatAt(u,'-'+d,'#ff8a5c','small'); SFX.hit(0);
  } else if(c.status==='regen'){
    const d=Math.floor(c.maxhp*0.09);
    c.hp=Math.min(c.maxhp,c.hp+d);
    floatAt(u,'+'+d,'#8df29a','small'); SFX.heal();
  } else if(c.status==='shock'){
    if(Math.random()<0.32) skip=true;
  }
  if(c.sTurns>0){ c.sTurns--; if(c.sTurns<=0 && c.status){ c.status=null; } }
  if(u.side==='player'){ renderPlayerCard(); } renderEnemyCards();
  return skip;
}
function floatAt(u,txt,color,cls){
  const key = u.side==='player' ? 'p' : ('e'+u.idx);
  const p=B.pos[key]; if(!p) return;
  const d=el('div','dmg '+(cls||''),txt);
  d.style.color=color||'#fff'; d.style.left=p.x+'px'; d.style.top=(p.y-30)+'px';
  $('#stage').appendChild(d); setTimeout(()=>d.remove(),1050);
}
function checkEnd(){
  if(B.over) return true;
  const enemiesAlive=B.enemies.some(u=>u.c.hp>0);
  const partyAlive=S.party.some(c=>c.hp>0);
  if(!enemiesAlive){ endBattle(true); return true; }
  if(!partyAlive){ endBattle(false); return true; }
  return false;
}

/* ---------- COMMANDS ---------- */
function renderCommands(){
  const grid=$('#cmdGrid'); grid.innerHTML='';
  if(B.over||B.busy&&!B.awaiting){ return; }
  if(B.awaiting==='switch'){
    S.party.forEach((c,i)=>{
      const b=el('button','cmdBtn sub');
      const r=Math.max(0,c.hp/c.maxhp);
      b.innerHTML='<span class="mdot" style="background:'+TYPE_COLOR(affsOf(c)[0])+';color:'+TYPE_COLOR(affsOf(c)[0])+'"></span>'+
        '<span class="mnm">'+esc(name(c))+'</span><span class="meta">Lv'+c.lv+' '+(c.hp<=0?'·fainted':(Math.round(c.hp)+'hp'))+'</span>';
      b.disabled=c.hp<=0||i===B.pi;
      b.onclick=()=>{ SFX.ui(); doSwitch(i); };
      grid.appendChild(b);
    });
    const back=el('button','cmdBtn sub');
    back.innerHTML='<span class="mnm">Back</span>';
    back.onclick=()=>{ SFX.cancel(); B.awaiting='command'; renderCommands(); };
    grid.appendChild(back);
    return;
  }
  if(B.awaiting!=='command'){ return; }
  const c=activeC();
  c.moves.forEach(id=>{
    const ab=ABIL[id];
    if(!ab) return;
    const b=el('button','cmdBtn');
    b.innerHTML='<span class="mdot" style="background:'+TYPE_COLOR(ab.aff)+';color:'+TYPE_COLOR(ab.aff)+'"></span>'+
      '<span class="mnm">'+esc(ab.name)+'</span><span class="meta">'+(ab.pow?ab.pow:'—')+' · '+(ab.priority?'\u2605':'')+'</span>';
    b.onclick=()=>{ SFX.ui(); playerUse(id); };
    grid.appendChild(b);
  });
  const sw=el('button','cmdBtn sub');
  sw.innerHTML='<span class="mnm">Swap In</span><span class="meta">turn</span>';
  sw.onclick=()=>{ SFX.ui(); B.awaiting='switch'; renderCommands(); };
  grid.appendChild(sw);
  if(B.wild){
    const best=bestShard();
    const cap=el('button','cmdBtn sub');
    cap.innerHTML='<span class="mnm">Bind</span><span class="meta">'+(best?ITEMS[best].name.split(' ')[0]:'no shard')+'</span>';
    cap.disabled=!best;
    cap.onclick=()=>{ SFX.ui(); beginCapture(); };
    grid.appendChild(cap);
    const run=el('button','cmdBtn sub');
    run.innerHTML='<span class="mnm">Flee</span><span class="meta">'+Math.round(fleeChance()*100)+'%</span>';
    run.onclick=()=>{ SFX.ui(); tryFlee(); };
    grid.appendChild(run);
  } else {
    const note=el('button','cmdBtn sub');
    note.innerHTML='<span class="mnm">A Warden cannot be bound</span>';
    note.disabled=true; grid.appendChild(note);
  }
}
function battleConfirm(){ if(screen==='battle'&&B.awaiting==='command'){ /* no-op, choose a move */ } }
function bestShard(){
  if((S.bag.shard_flawless||0)>0) return 'shard_flawless';
  if((S.bag.shard_bright||0)>0) return 'shard_bright';
  if((S.bag.shard_dull||0)>0) return 'shard_dull';
  return null;
}
function playerUse(id){
  if(B.busy||B.over) return;
  const ab=ABIL[id];
  if(!ab) return;
  const u={c:activeC(),side:'player',idx:B.pi};
  const ti=B.enemies.findIndex(e=>e.c.hp>0);
  if(ti<0) return;
  const target={c:B.enemies[ti].c,side:'enemy',idx:ti};
  B.busy=true; B.awaiting=null; renderCommands();
  resolveAbility(u,target,ab,()=>{ B.ti++; nextTurn(); });
}
function doSwitch(i){
  if(B.busy||B.over) return;
  const c=S.party[i]; if(!c||c.hp<=0) return;
  B.busy=true;
  B.pi=i;
  log('You draw '+name(c)+' into the weave.');
  SFX.confirm();
  renderPlayerCard(); renderEnemyCards();
  setTimeout(()=>{ B.busy=false; B.ti++; nextTurn(); },620);
}
function tryFlee(){
  if(B.busy||B.over) return;
  if((S.bag.bell||0)>0){
    S.bag.bell--; SFX.confirm(); log('The tidebell rings once — the way opens.');
    return setTimeout(()=>endBattle(null),620);
  }
  B.flee++;
  const ch=fleeChance();
  if(Math.random()<ch){ SFX.confirm(); log('You slip back into the drift.'); setTimeout(()=>endBattle(null),620); }
  else { SFX.fail(); log('The Kindra close the gap — no way out yet.'); B.busy=true; setTimeout(()=>{ B.busy=false; B.ti++; nextTurn(); },700); }
}
function fleeChance(){
  const mine=effSpd(activeC());
  const best=B.enemies.reduce((m,u)=>Math.max(m,effSpd(u.c)),1);
  return clamp(0.35+0.4*(mine/best)+B.flee*0.12,0.05,0.95);
}

/* ---------- DAMAGE / RESOLUTION ---------- */
function resolveAbility(u,target,ab,done){
  const A=u.c, D=target.c;
  const pa=affsOf(A), pd=affsOf(D);
  const lines=[];
  /* non-damaging */
  if(ab.eff==='heal'){
    const amt=Math.floor(A.maxhp*ab.heal);
    A.hp=Math.min(A.maxhp,A.hp+amt);
    floatAt(u,'+'+amt,'#8df29a'); SFX.heal();
    fx(u,'heal','#8df29a');
    log(name(A)+' used '+ab.name+'. Warmth spreads.');
    syncUI(); return setTimeout(done,760);
  }
  if(ab.eff==='barrier'){ A.barrier=true; fx(u,'ring','#8df29a'); SFX.heal();
    log(name(A)+' raises a '+ab.name+' barrier.'); syncUI(); return setTimeout(done,700); }
  if(ab.eff==='regen'){ A.status='regen'; A.sTurns=4; fx(u,'ring','#6fdc7a'); SFX.heal();
    log(name(A)+' settles into '+ab.name+'.'); syncUI(); return setTimeout(done,700); }
  if(ab.eff==='speedup'){ A.stg.spd=Math.min(3,(A.stg.spd||0)+ab.stage); fx(u,'ring','#a8e0ff'); SFX.confirm();
    log(name(A)+'\u2019s pace quickens.'); syncUI(); return setTimeout(done,700); }
  if(ab.eff==='buffatk'){ A.stg.atk=Math.min(3,(A.stg.atk||0)+ab.stage); fx(u,'ring','#f3d78a'); SFX.confirm();
    log(name(A)+'\u2019s body brightens with force.'); syncUI(); return setTimeout(done,700); }
  if(ab.eff==='buffdef'){ A.stg.def=Math.min(3,(A.stg.def||0)+ab.stage); fx(u,'ring','#c8a06a'); SFX.confirm();
    log(name(A)+' thickens into strata.'); syncUI(); return setTimeout(done,700); }
  /* damaging */
  const hits=ab.hits||1;
  let total=0, effMul=1, anyCrit=false, missed=false;
  const hitList=[];
  for(let h=0;h<hits;h++){
    if(Math.random()*100 > (ab.acc||100)){ hitList.push({miss:true}); continue; }
    const eff=typeEff(ab.aff,pd);
    const stab=pa.indexOf(ab.aff)>=0?1.25:1;
    const crit=Math.random()<(0.0625+(ab.crit||0));
    const lv=A.lv;
    let dmg=Math.floor(0.4*lv*(effAtk(A)/Math.max(1,effDef(D)))*weaveMult(ab.pow,lv)*stab*eff*(crit?1.65:1));
    dmg+=ri(0,3);
    dmg=Math.max(1,dmg);
    if(D.barrier && h===0 && dmg>0){ D.barrier=false; dmg=0; hitList.push({barrier:true}); continue; }
    D.hp=Math.max(0,D.hp-dmg); total+=dmg; effMul=eff; if(crit)anyCrit=true;
    D.stg.def*=1; hitList.push({dmg,eff,crit});
    if(D.hp<=0) break;
  }
  fx(target,total>0?'hit':'miss',TYPE_COLOR(ab.aff));
  if(total>0){
    SFX[anyCrit?'crit':(effMul>1.5?'hit':(effMul<1?'weak':'hit'))](effMul);
    shake(anyCrit?1.6:(effMul>1.5?1.2:0.8));
    if(effMul>1.5) burst(target,TYPE_COLOR(ab.aff));
    if(anyCrit) flash('rgba(255,230,150,.5)',200);
  } else { SFX.fail(); }
  let msg=name(u.c)+' used '+ab.name+'.';
  if(total>0){
    const key=u.side==='player'?'p':('e'+u.idx);
    const tkey=target.side==='player'?'p':('e'+target.idx);
    const px=B.pos[tkey];
    const d=el('div','dmg'+(anyCrit?' crit':''),String(total));
    d.style.left=px.x+'px'; d.style.top=(px.y-34)+'px'; d.style.color=anyCrit?'#ffd76a':'#fff';
    $('#stage').appendChild(d); setTimeout(()=>d.remove(),1050);
    if(hits>1) msg+=' ('+hits+'\u00d7)';
    if(anyCrit) msg+=' A critical seam!';
    if(effMul>1.5) msg+=' It bites deep.';
    else if(effMul<1 && effMul>0) msg+=' It barely registers.';
    else if(effMul===0) msg+=' No effect at all.';
  } else { msg+=' It finds nothing.'; }
  log(msg);
  /* secondary effects */
  if(total>0 && ab.drain){
    const hl=Math.floor(total*ab.drain); A.hp=Math.min(A.maxhp,A.hp+hl);
    floatAt(u,'+'+hl,'#8df29a','small');
  }
  if(ab.recoil){
    const rl=Math.max(1,Math.floor(total*ab.recoil)); A.hp=Math.max(0,A.hp-rl);
    floatAt(u,'-'+rl,'#ff6b7d','small');
  }
  if(total>0 && ab.chance>0 && ab.eff && STATUS[ab.eff]){
    if(D.hp>0 && Math.random()<ab.chance){
      if(ab.eff==='shock' && D.status) { /* one status at a time */ }
      D.status=ab.eff; D.sTurns = (ab.eff==='burn'||ab.eff==='regen')?4:3;
      floatAt(target,STATUS[ab.eff].name,STATUS[ab.eff].c,'small');
    }
  }
  syncUI();
  setTimeout(()=>{
    if(D.hp<=0){ onFaint(target,()=>done()); }
    else done();
  },560);
}
function syncUI(){
  if(B.on){ renderPlayerCard(); renderEnemyCards(); }
  updatePartyStrip();
}
function onFaint(u,next){
  SFX.faint();
  const key=u.side==='player'?'p':('e'+u.idx);
  const p=B.pos[key];
  if(p) burst(u,'#ffffff');
  log(name(u.c)+' comes apart into drift.');
  renderEnemyCards(); updatePartyStrip();
  setTimeout(()=>{
    if(checkEnd()) return;
    if(u.side==='player'){
      /* force swap */
      const alive=S.party.findIndex(c=>c.hp>0);
      if(alive>=0){ B.pi=alive; B.awaiting=null; renderPlayerCard();
        log(name(S.party[alive])+' steps forward.'); }
    }
    next();
  },700);
}
function enemyAct(u){
  if(B.over||B.busy) return;
  B.busy=true;
  const c=u.c;
  const alive=S.party.filter(x=>x.hp>0);
  if(!alive.length) return checkEnd();
  const ac=activeC();
  const target={c:ac,side:'player',idx:B.pi};
  /* AI: score moves */
  const wantHeal = c.hp/c.maxhp<0.32;
  let bestId=c.moves[0], bestScore=-1e9;
  for(const id of c.moves){
    const ab=ABIL[id]; if(!ab) continue;
    if(ab.eff==='heal'){ if(!wantHeal&&Math.random()<0.75) continue; }
    if(ab.eff==='barrier'&&c.barrier) continue;
    let sc=ab.pow*1.0;
    if(ab.pow){
      const eff=typeEff(ab.aff,affsOf(ac));
      sc*= (eff===0?0.4:eff);
      if(ab.hits) sc*=1+ (ab.hits-1)*0.25;
      if(eff>1.5) sc*=1.5;
    } else {
      sc = (ab.eff==='heal') ? (wantHeal? 60:8) : 26;
    }
    sc*=0.85+Math.random()*0.3;
    if(sc>bestScore){ bestScore=sc; bestId=id; }
  }
  const ab=ABIL[bestId];
  setTimeout(()=>resolveAbility(u,target,ab,()=>{ B.busy=false; B.ti++; nextTurn(); }),240);
}

/* ---------- BATTLE FX ---------- */
function fx(u,type,color){
  const key=u.side==='player'?'p':('e'+u.idx);
  const p=B.pos[key]; if(!p) return;
  B.fx.push({x:p.x,y:p.y,t:0,dur:type==='hit'?0.5:0.7,type,color});
}
function burst(u,color){
  const key=u.side==='player'?'p':('e'+u.idx);
  const p=B.pos[key]; if(!p) return;
  for(let i=0;i<20;i++){
    const a=Math.random()*6.283, s=rnd(60,260);
    B.fx.push({x:p.x,y:p.y,vx:Math.cos(a)*s,vy:Math.sin(a)*s,t:0,dur:rnd(0.4,0.9),type:'spark',color});
  }
}
function drawBattle(dt){
  const z=ZONES[S.zone];
  const sg=ctx.createLinearGradient(0,0,0,VH);
  sg.addColorStop(0,'#04070e'); sg.addColorStop(0.4,z.sky[0]); sg.addColorStop(1,'#02040a');
  ctx.fillStyle=sg; ctx.fillRect(0,0,VW,VH);
  const hg=ctx.createRadialGradient(VW*0.5,VH*0.4,10,VW*0.5,VH*0.4,VH*0.9);
  hg.addColorStop(0,z.glow+'22'); hg.addColorStop(1,'transparent');
  ctx.fillStyle=hg; ctx.fillRect(0,0,VW,VH);
  drawParticles(z,dt,false);
  /* ground */
  const gg=ctx.createRadialGradient(VW*0.5,VH*0.72,10,VW*0.5,VH*0.72,VW*0.6);
  gg.addColorStop(0,'rgba(255,255,255,.07)'); gg.addColorStop(1,'transparent');
  ctx.fillStyle=gg; ctx.beginPath(); ctx.ellipse(VW*0.5,VH*0.72,VW*0.5,VH*0.2,0,0,6.283); ctx.fill();
  /* enemies */
  B.enemies.forEach((u,i)=>{
    if(u.c.hp<=0) return;
    const p=B.pos['e'+i]; if(!p) return;
    const size=Math.min(VW,VH)*0.11*(1+ (u.c.boss?0.55:0));
    drawBeast(u.c,p.x,p.y,size,affsOf(u.c),G.t,{seedKey:u.c.sp+(u.c.boss?'boss':'')});
    /* name tag */
    ctx.font='700 12px '+getComputedStyle(document.body).fontFamily;
    ctx.textAlign='center'; ctx.fillStyle='rgba(255,255,255,.85)';
    ctx.fillText(name(u.c), p.x, p.y+size*1.15);
  });
  /* player beast */
  const ac=activeC();
  if(ac&&ac.hp>0){
    const p=B.pos.p;
    drawBeast(ac,p.x,p.y,Math.min(VW,VH)*0.115,affsOf(ac),G.t,{seedKey:ac.sp});
    ctx.font='700 12px '+getComputedStyle(document.body).fontFamily;
    ctx.textAlign='center'; ctx.fillStyle='rgba(200,255,245,.9)';
    ctx.fillText(name(ac), p.x, p.y+Math.min(VW,VH)*0.115*1.15);
  }
  /* fx */
  for(let i=B.fx.length-1;i>=0;i--){
    const f=B.fx[i]; f.t+=dt;
    const k=f.t/f.dur;
    if(k>=1){ B.fx.splice(i,1); continue; }
    ctx.save();
    if(f.type==='spark'){
      f.x+=f.vx*dt; f.y+=f.vy*dt; f.vy+=260*dt;
      ctx.globalAlpha=1-k; ctx.fillStyle=f.color;
      ctx.beginPath(); ctx.arc(f.x,f.y,3.4*(1-k)+1,0,6.283); ctx.fill();
    } else if(f.type==='ring'||f.type==='heal'){
      ctx.globalAlpha=(1-k)*0.85; ctx.strokeStyle=f.color; ctx.lineWidth=4*(1-k)+1;
      ctx.beginPath(); ctx.arc(f.x,f.y,30+k*130,0,6.283); ctx.stroke();
    } else {
      ctx.globalAlpha=(1-k);
      const g=ctx.createRadialGradient(f.x,f.y,4,f.x,f.y,60+k*120);
      g.addColorStop(0,f.color); g.addColorStop(1,'transparent');
      ctx.fillStyle=g; ctx.beginPath(); ctx.arc(f.x,f.y,60+k*120,0,6.283); ctx.fill();
    }
    ctx.restore();
  }
  const eg=ctx.createRadialGradient(VW/2,VH/2,Math.min(VW,VH)*0.3,VW/2,VH/2,Math.max(VW,VH)*0.72);
  eg.addColorStop(0,'transparent'); eg.addColorStop(1,'rgba(0,0,0,.72)');
  ctx.fillStyle=eg; ctx.fillRect(0,0,VW,VH);
}
/* ================================================================
   PART 5 — BINDING MINIGAME + BATTLE CONCLUSION
   ================================================================ */
let capState=null;
function beginCapture(){
  if(B.busy||B.over) return;
  const shard=bestShard(); if(!shard) return;
  const target=B.enemies.find(u=>u.c.hp>0); if(!target) return;
  capState={shard, target, t:0, dir:1, speed:1.05+Math.random()*0.25, pos:0.5, locked:false, gave:false};
  const z=$('#capZone'), core=$('#capCore');
  const zw=0.24, cw=0.11;
  z.style.left=((0.5-zw/2)*100)+'%'; z.style.width=(zw*100)+'%';
  core.style.left=((0.5-cw/2)*100)+'%'; core.style.width=(cw*100)+'%';
  core.classList.remove('hidden');
  $('#capture').classList.remove('hidden');
  $('#capResult').classList.add('hidden');
  $('#capResult').textContent='';
  $('#capBtn').classList.remove('hidden');
  $('#capBtn').textContent='Lock';
  $('#capHint2').textContent='Tap at the heart of the glow to steady the shard';
  B.busy=true; B.awaiting=null; renderCommands();
}
function updateCapture(dt){
  if(!capState||capState.locked) return;
  capState.t+=dt;
  capState.pos += capState.dir*capState.speed*dt;
  if(capState.pos>1){capState.pos=1;capState.dir=-1;}
  if(capState.pos<0){capState.pos=0;capState.dir=1;}
  $('#capMark').style.left=(capState.pos*100)+'%';
}
function captureLock(){
  if(!capState||capState.locked) return;
  capState.locked=true;
  const d=Math.abs(capState.pos-0.5);
  let bonus=0.78, grade='Slipped';
  if(d<=0.055){ bonus=1.55; grade='Perfect'; }
  else if(d<=0.12){ bonus=1.28; grade='Centred'; }
  else if(d<=0.24){ bonus=1.02; grade='Held'; }
  else { bonus=0.78; grade='Slipped'; }
  SFX.confirm();
  const target=capState.target;
  const hpR=Math.max(0,target.c.hp/target.c.maxhp);
  const tier=ITEMS[capState.shard].tier;
  let chance=clamp((1-hpR*0.82)*0.75*tier*bonus,0.03,0.95);
  if(target.c.status) chance=clamp(chance*1.12,0.03,0.96);
  /* consume shard */
  S.bag[capState.shard]--; 
  const roll=Math.random();
  const ok = roll<chance;
  const res=$('#capResult'); res.classList.remove('hidden');
  $('#capBtn').classList.add('hidden'); $('#capHint2').textContent='';
  if(ok){
    res.style.color='#8df29a';
    res.textContent=grade+' \u2014 the shard holds!';
    SFX.bind();
    flash('rgba(150,255,230,.5)',320);
    doBind(target.c);
  } else {
    res.style.color='#ff8a9c';
    res.textContent=grade+' \u2014 it slips free ('+Math.round(chance*100)+'%)';
    SFX.fail();
  }
  setTimeout(()=>{
    $('#capture').classList.add('hidden');
    capState=null;
    if(ok){
      /* bound — ends the wild battle for this foe */
      target.c.hp=0;
      renderEnemyCards();
      if(checkEnd()) return;
      B.busy=false; B.ti++; nextTurn();
    } else {
      B.busy=false; B.ti++; nextTurn();
    }
  },1350);
}
function doBind(c){
  const entry={sp:c.sp,lv:c.lv,hp:c.hp,maxhp:c.maxhp,atk:c.atk,def:c.def,spd:c.spd,
    affs:c.affs.slice(),known:c.known.slice(),moves:c.moves.slice(),uid:c.uid,status:null,sTurns:0,stg:{atk:0,def:0,spd:0}};
  entry.hp=Math.max(1,entry.hp);
  S.bound[c.sp]=true;
  S.dex[c.sp]={seen:true,bound:true};
  for(const a of affsOf(c)) S.dex['aff_'+a]=true;
  if(S.party.length<6){ S.party.push(entry); toast(name(c)+' joins your weave.'); }
  else { S.storage.push(entry); toast(name(c)+' rests in the deepkeep.'); }
  updatePartyStrip();
  if(S.flags.tut<4){ S.flags.tut=4; setTimeout(()=>showBubble('Bound. Open the satchel (☰ or M) any time to review your Kindra, swap techniques, and reshape them at the right level.',6400,{x:VW*0.5-170,y:VH*0.24}),700); }
  checkObjectives();
}
function checkObjectives(){
  const n=Object.keys(S.bound).length;
  if(!S.flags.post && S.anchors>=4 && n>=8){ S.flags.post=true; return true; }
  return false;
}

/* ---------- END BATTLE ---------- */
function endBattle(win){
  if(B.over) return;
  B.over=true;
  const totalLv=B.enemies.reduce((a,u)=>a+u.c.lv,0);
  const bossDefeated = !!B.boss;
  const boss=G_guard();
  if(win===true){
    const expBase=Math.floor((4+totalLv*totalLv*0.34+totalLv*2.6));
    const motes=Math.floor(totalLv*2.4+(bossDefeated?120:0));
    S.motes+=motes;
    let lvUps=[], justPost=false;
    S.party.forEach(c=>{
      if(c.hp<=0) return;
      const rel=clamp(1+0.055*(totalLv/Math.max(1,B.enemies.length)-c.lv),0.35,2.2);
      const gain=Math.max(1,Math.floor(expBase*rel*(c===activeC()?1:0.6)));
      c.exp+=gain;
      let ups=0;
      while(c.lv<50 && c.exp>=expToNext(c.lv)){ c.exp-=expToNext(c.lv); c.lv++; ups++; recalc(c,false);
        const nl=learnableAt(c.sp,c.lv);
        for(const a of nl) if(c.known.indexOf(a)<0){ c.known.push(a); if(c.moves.length<4) c.moves.push(a); }
      }
      if(c.lv>=50) c.exp=0;
      if(ups>0 && c.hp>0){ const before=c.hp; recalc(c,false); c.hp=Math.min(c.maxhp, c.hp+Math.floor(c.maxhp*0.14*ups)); lvUps.push(c); }
    });
    if(lvUps.length){ SFX.levelup(); lvUps.forEach(c=>toast(name(c)+' reached Lv '+c.lv+'!')); }
    log('Victory. +'+motes+' motes, +'+expBase+' EXP.');
    if(bossDefeated){
      S.guards[ZONES[S.zone].guard]=true;
      S.anchors++;
      const zi=S.zone;
      if(zi+1<6) S.unlocked[zi+1]=true;
      SFX.anchor();
      justPost=checkObjectives();
    }
    updatePartyStrip(); renderPlayerCard(); renderEnemyCards();
    setTimeout(()=>closeBattleScreen(()=>{
      saveGame(S.slot,true);
      if(bossDefeated) afterGuardian(justPost);
      else if(justPost) toast('Post-game: the far reaches open.');
    }), 1500);
  } else if(win===false){
    log('Your weave comes apart...');
    setTimeout(()=>closeBattleScreen(()=>{
      const lost=Math.floor(S.motes*0.1); S.motes-=lost;
      healParty();
      S.party.forEach(c=>{ if(c.hp<=0) c.hp=Math.max(1,Math.floor(c.maxhp*0.35)); });
      updatePartyStrip();
      toast('You wake at the shrine. \u2212'+lost+' motes.');
      openShrineMenu(true);
    }), 1500);
  } else {
    closeBattleScreen(()=>{});
  }
}
function G_guard(){ return B.boss; }
function closeBattleScreen(after){
  B.on=false;
  $('#battle').classList.add('hidden'); $('#battle').classList.remove('on');
  $('#playerCard').classList.add('hidden'); $('#cmdPanel').classList.add('hidden');
  $('#hud').classList.remove('hidden');
  screen='world';
  G.encCd=10;
  musicLoop();
  if(after) after();
}
function afterGuardian(justPost){
  const zi=S.zone;
  const beats=[];
  const n=zi+1;
  if(justPost) beats.push({who:'The Hollow Tide',t:'Four anchors, eight threads. The weave has stopped bleeding \u2014 the far reaches are open, and the drift there runs deeper.'});
  if(n===1) beats.push(...STORY.after1);
  else if(n===2) beats.push(...STORY.after2);
  else if(n===3) beats.push(...STORY.after3);
  else if(n===4) beats.push(...STORY.after4);
  else if(n===5) beats.push(...STORY.after5);
  else if(n===6) beats.push(...STORY.final);
  if(zi+1<6 && S.unlocked[zi+1]) beats.push({who:'The Hollow Tide',t:'The way opens eastward: '+ZONES[zi+1].name+'.'});
  if(beats.length) queueDialog(beats,()=>{
    if(zi+1<6) showHint('A new reach is open. Travel east.',3600);
    else showHint('All six anchors are set. The reaches are yours.',4200);
  });
  saveGame(S.slot,true);
}

/* ================================================================
   PART 6 — DIALOGUE
   ================================================================ */
let dlgQueue=[], dlgAfter=null, dlgIdx=0, dlgTyping=null;
function queueDialog(beats,after){
  if(!beats||!beats.length){ if(after) after(); return; }
  dlgQueue=beats.slice(); dlgAfter=after||null; dlgIdx=0;
  $('#dialog').classList.remove('hidden');
  showBeat();
}
function showBeat(){
  const b=dlgQueue[dlgIdx];
  if(!b){ endDialog(); return; }
  const tints={ 'The Hollow Tide':'rgba(80,60,140,.28)', 'Nullsable, the Hollow':'rgba(120,40,140,.34)' };
  const v=$('#vignette');
  v.style.background='radial-gradient(80% 70% at 50% 50%, transparent 22%, '+ (tints[b.who]||'rgba(0,0,0,.85)') +' 100%)';
  v.classList.add('on');
  $('#dlgWho').textContent=b.who||'';
  const t=$('#dlgText'); t.textContent='';
  let i=0; const full=b.t;
  if(dlgTyping) clearInterval(dlgTyping);
  dlgTyping=setInterval(()=>{
    i+=1; t.textContent=full.slice(0,i);
    if(i>=full.length){ clearInterval(dlgTyping); dlgTyping=null; }
  },14);
}
function advanceDialog(){
  const full=dlgQueue[dlgIdx];
  if(!full) return endDialog();
  const t=$('#dlgText');
  if(dlgTyping){ clearInterval(dlgTyping); dlgTyping=null; t.textContent=full.t; return; }
  dlgIdx++;
  if(dlgIdx>=dlgQueue.length) endDialog(); else showBeat();
}
function endDialog(){
  if(dlgTyping){clearInterval(dlgTyping);dlgTyping=null;}
  dlgQueue=[]; dlgIdx=0;
  $('#dialog').classList.add('hidden');
  $('#vignette').classList.remove('on');
  const a=dlgAfter; dlgAfter=null;
  if(a) a();
}
/* ================================================================
   PART 7 — MENUS, CODEX, SHOP, MAP, SAVE
   ================================================================ */
const M={tab:'team',sub:null,shop:false,title:''};
let AT_SHRINE=false;
function affPills(affs){
  return '<span class="tpills">'+affs.map(a=>'<span class="tpill" style="color:'+TYPE_COLOR(a)+'">'+a+'</span>').join('')+'</span>';
}
function openModal(tab,title){
  M.tab=tab||'team'; M.sub=null; M.title=title||'Binder\u2019s Satchel';
  screen='menu';
  $('#modalWrap').classList.remove('hidden');
  renderModal();
}
function closeModal(){
  $('#modalWrap').classList.add('hidden');
  M.sub=null;
  if(!S){ showTitle(); return; }
  if(screen==='menu') screen='world';
  updatePartyStrip();
}
function openMenu(){ if(screen!=='world') return; SFX.ui(); AT_SHRINE=false; openModal('team','Binder\u2019s Satchel'); }
function closeMenu(){ SFX.cancel(); closeModal(); }
$('#btnMenu').onclick=openMenu;
$('#modalWrap').addEventListener('click',e=>{ if(e.target.id==='modalWrap') closeMenu(); });

function mBtn(label,fn,cls){
  const b=el('button','miniBtn '+(cls||''),esc(label));
  b.onclick=(ev)=>{ ev.stopPropagation(); SFX.ui(); fn(); };
  return b;
}
function renderModal(){
  const m=$('#modal');
  m.innerHTML='';
  const head=el('div','mHead');
  head.innerHTML='<h2>'+esc(M.title)+'</h2>';
  const close=mBtn('Close',()=>closeMenu());
  head.appendChild(close);
  m.appendChild(head);
  const tabs=el('div','mTabs');
  const TABS=[['team','Kindra'],['bag','Satchel'],['codex','Codex'],['map','Reaches'],['shop','Stillpoint Shoppe'],['save','Anchors'],['options','Options']];
  TABS.forEach(([id,label])=>{
    const t=el('button','mTab'+(M.tab===id?' sel':''),label);
    t.onclick=()=>{ SFX.ui(); M.tab=id; M.sub=null; renderModal(); };
    tabs.appendChild(t);
  });
  m.appendChild(tabs);
  const body=el('div','mBody');
  m.appendChild(body);
  ({team:renderTeam,bag:renderBag,codex:renderCodex,map:renderMap,shop:renderShop,save:renderSave,options:renderOptions,starter:renderStarter,help:renderHelp}[M.tab]||renderTeam)(body);
}
function renderTeam(body){
  if(M.sub){ return renderCreatureDetail(body,M.sub); }
  if(!S.party.length && !S.storage.length){ body.appendChild(el('div','empty','No Kindra bound yet. Find them in the drift.')); return; }
  body.appendChild(el('div','','<div style="font-size:11px;letter-spacing:.22em;text-transform:uppercase;color:var(--dim);margin-bottom:8px">Weave \u2014 '+S.party.length+'/6 active</div>'));
  S.party.forEach((c,i)=>body.appendChild(creatureRow(c,i)));
  if(S.storage.length){
    body.appendChild(el('div','','<div style="font-size:11px;letter-spacing:.22em;text-transform:uppercase;color:var(--dim);margin:16px 0 8px">Deepkeep \u2014 '+S.storage.length+' resting</div>'));
    S.storage.forEach((c,i)=>body.appendChild(creatureRow(c,-1-i,true)));
  }
}
function creatureRow(c,idx,stored){
  const row=el('div','mRow');
  const r=Math.max(0,c.hp/c.maxhp);
  row.innerHTML='<div class="dot" style="width:26px;height:26px;border-radius:50%;flex:none;background:'+TYPE_COLOR(affsOf(c)[0])+';box-shadow:0 0 14px '+TYPE_COLOR(affsOf(c)[0])+'"></div>'+
    '<div class="grow"><div class="t1">'+esc(name(c))+' <span style="font-family:var(--mono);font-size:11px;color:var(--dim)">Lv '+c.lv+'</span></div>'+
    '<div class="t2">'+affPills(affsOf(c))+' &nbsp; '+Math.max(0,Math.round(c.hp))+'/'+c.maxhp+' HP &nbsp;\u00b7&nbsp; '+c.moves.map(x=>ABIL[x]?ABIL[x].name:'?').join(', ')+'</div>'+
    '<div class="bar"><i style="width:'+Math.round(r*100)+'%;background:'+hpColor(r)+'"></i></div></div>';
  const b=mBtn(stored?'Retrieve':'Open',()=>{
    if(stored){
      if(S.party.length>=6){ toast('Your weave is full (6).'); return; }
      const k=S.storage.indexOf(c); S.storage.splice(k,1); S.party.push(c); renderModal();
    } else { M.sub=c.uid; renderModal(); }
  });
  row.appendChild(b);
  return row;
}
function renderCreatureDetail(body,uid){
  const c=S.party.find(x=>x.uid===uid)||S.storage.find(x=>x.uid===uid);
  if(!c){ M.sub=null; return renderTeam(body); }
  const base=CRE[c.sp];
  const head=el('div','');
  head.innerHTML='<div style="display:flex;gap:14px;align-items:center;margin-bottom:12px">'+
    '<div style="width:48px;height:48px;border-radius:50%;background:'+TYPE_COLOR(affsOf(c)[0])+';box-shadow:0 0 22px '+TYPE_COLOR(affsOf(c)[0])+';flex:none"></div>'+
    '<div><div style="font-size:17px;font-weight:700">'+esc(name(c))+'</div><div class="t2">'+affPills(affsOf(c))+' Lv '+c.lv+' \u00b7 '+Math.max(0,Math.round(c.hp))+'/'+c.maxhp+' HP \u00b7 EXP '+c.exp+'/'+expToNext(c.lv)+'</div></div></div>';
  body.appendChild(head);
  body.appendChild(el('div','lore',esc(base?base.lore:(GUARDIANS[c.sp]?GUARDIANS[c.sp].lore:''))));
  const st=el('div','card');
  st.style.marginTop='12px'; st.style.cursor='default';
  st.innerHTML='<div class="stats" style="grid-template-columns:repeat(3,1fr)">'+
    '<div>ATK <b>'+effAtk(c).toFixed(0)+'</b></div><div>DEF <b>'+effDef(c).toFixed(0)+'</b></div><div>SPD <b>'+effSpd(c).toFixed(0)+'</b></div>'+
    '<div>ATK base <b>'+c.atk+'</b></div><div>DEF base <b>'+c.def+'</b></div><div>SPD base <b>'+c.spd+'</b></div></div>';
  body.appendChild(st);
  /* moves */
  body.appendChild(el('div','','<div style="font-size:11px;letter-spacing:.22em;text-transform:uppercase;color:var(--dim);margin:18px 0 8px">Equipped Techniques (4)</div>'));
  c.moves.forEach((id,i)=>{
    const ab=ABIL[id]; if(!ab) return;
    const row=el('div','mRow');
    row.innerHTML='<span class="pill" style="color:'+TYPE_COLOR(ab.aff)+'">'+ab.aff+'</span>'+
      '<div class="grow"><div class="t1">'+esc(ab.name)+'</div><div class="t2">PWR '+(ab.pow||'\u2014')+' \u00b7 ACC '+ab.acc+' \u00b7 '+esc(ab.desc||ab.eff)+'</div></div>';
    const sw=mBtn('Replace',()=>{ M.sub='moves:'+c.uid+':'+i; renderModal(); });
    row.appendChild(sw);
    body.appendChild(row);
  });
  if(M.sub&&String(M.sub).indexOf('moves:')===0){
    const [,cu,mi]=String(M.sub).split(':');
    const cc=S.party.find(x=>x.uid===cu);
    if(cc){
      body.appendChild(el('div','','<div style="font-size:11px;letter-spacing:.22em;text-transform:uppercase;color:var(--accent);margin:18px 0 8px">Known pool \u2014 tap to equip in slot '+(parseInt(mi)+1)+'</div>'));
      cc.known.forEach(id=>{
        const ab=ABIL[id]; if(!ab) return;
        const row=el('div','mRow');
        row.innerHTML='<span class="pill" style="color:'+TYPE_COLOR(ab.aff)+'">'+ab.aff+'</span>'+
          '<div class="grow"><div class="t1">'+esc(ab.name)+'</div><div class="t2">PWR '+(ab.pow||'\u2014')+' \u00b7 '+esc(ab.desc||ab.eff)+'</div></div>';
        row.appendChild(mBtn(cc.moves.indexOf(id)>=0?'\u2713':'Equip',()=>{
          cc.moves[parseInt(mi)]=id; M.sub=cc.uid; renderModal();
        }));
        body.appendChild(row);
      });
      body.appendChild(mBtn('Back',()=>{ M.sub=cc.uid; renderModal(); }));
    }
  }
  /* transform */
  if(base&&base.evo){
    const e=base.evo;
    const canLv=c.lv>=e.lv, hasIt=(S.bag[e.item]||0)>0;
    const row=el('div','mRow');
    row.style.borderColor = (canLv&&hasIt)?'var(--accent)':'var(--stroke)';
    row.innerHTML='<div class="grow"><div class="t1">Reshape into '+esc(CRE[e.to].name)+'</div><div class="t2">'+
      'Requires Lv '+e.lv+(e.item?(' and one '+ITEMS[e.item].name):'')+'. '+
      (canLv?'Level met.':'Needs '+(e.lv-c.lv)+' more levels.')+' '+(hasIt||!e.item?'Catalyst ready.':'No '+ITEMS[e.item].name+' in satchel.')+'</div></div>';
    const b=mBtn('Reshape',()=>doEvolve(c),'');
    b.disabled=!(canLv&&hasIt);
    row.appendChild(b);
    body.appendChild(row);
  }
  const back=mBtn('Back to weave',()=>{ M.sub=null; renderModal(); });
  body.appendChild(el('div','','<div style="margin-top:18px"></div>')).appendChild(back);
}
function doEvolve(c){
  const base=CRE[c.sp]; if(!base||!base.evo) return;
  const e=base.evo;
  if(c.lv<e.lv) return;
  if(e.item){ if((S.bag[e.item]||0)<=0) return; S.bag[e.item]--; }
  const oldName=name(c);
  c.sp=e.to; c.affs=CRE[e.to].affs.slice();
  const nl=learnableAt(e.to,c.lv);
  for(const a of nl){ if(c.known.indexOf(a)<0){ c.known.push(a); if(c.moves.length<4) c.moves.push(a); } }
  recalc(c,false); c.hp=c.maxhp;
  SFX.levelup(); flash('rgba(180,255,240,.55)',420); shake(1.2);
  toast(oldName+' reshaped into '+name(c)+'!');
  S.dex[c.sp]={seen:true,bound:true};
  M.sub=c.uid; renderModal(); updatePartyStrip();
}
function renderBag(body){
  const keys=Object.keys(S.bag).filter(k=>(S.bag[k]||0)>0);
  body.appendChild(el('div','','<div style="font-size:11px;letter-spacing:.22em;text-transform:uppercase;color:var(--dim);margin-bottom:10px">Motes: '+S.motes+'</div>'));
  if(!keys.length){ body.appendChild(el('div','empty','Your satchel is empty.')); return; }
  keys.forEach(k=>{
    const it=ITEMS[k]; if(!it) return;
    const row=el('div','mRow');
    row.innerHTML='<div class="grow"><div class="t1">'+esc(it.name)+' &times; '+S.bag[k]+'</div><div class="t2">'+esc(it.desc)+'</div></div>';
    if(it.kind==='heal'||it.kind==='revive'){
      row.appendChild(mBtn('Use',()=>{ M.sub='use:'+k; renderModal(); }));
    } else if(it.kind==='tool'&&k==='lantern'){
      row.appendChild(mBtn('Light',()=>{ S.bag.lantern--; S.flags.lantern=400; toast('The lantern quiets the drift.'); renderModal(); }));
    }
    body.appendChild(row);
  });
  if(M.sub&&String(M.sub).indexOf('use:')===0){
    const k=String(M.sub).split(':')[1], it=ITEMS[k];
    body.appendChild(el('div','','<div style="font-size:11px;letter-spacing:.22em;text-transform:uppercase;color:var(--accent);margin:16px 0 8px">Choose a Kindra</div>'));
    S.party.forEach(c=>{
      const ok = it.kind==='revive' ? c.hp<=0 : c.hp>0 && c.hp<c.maxhp;
      const row=el('div','mRow');
      row.innerHTML='<div class="grow"><div class="t1">'+esc(name(c))+' Lv '+c.lv+'</div><div class="t2">'+Math.round(c.hp)+' / '+c.maxhp+' HP</div></div>';
      const b=mBtn('Apply',()=>{
        S.bag[k]--;
        if(it.kind==='heal'){ c.hp=Math.min(c.maxhp,c.hp+it.amount); }
        else { c.hp=Math.floor(c.maxhp*it.amount); }
        SFX.heal(); toast(name(c)+' mends.'); M.sub=null; renderModal(); updatePartyStrip();
      });
      b.disabled=!ok; row.appendChild(b); body.appendChild(row);
    });
    body.appendChild(mBtn('Back',()=>{ M.sub=null; renderModal(); }));
  }
}
function renderCodex(body){
  const seen=Object.keys(S.dex).filter(k=>k.indexOf('aff_')!==0);
  const all=Object.keys(CRE);
  body.appendChild(el('div','','<div style="font-size:11px;letter-spacing:.22em;text-transform:uppercase;color:var(--dim);margin-bottom:10px">Bound '+all.filter(s=>S.bound[s]).length+' / '+all.length+'  \u00b7  Anchors set '+S.anchors+' / 6  \u00b7  Objectives: 8 Kindra &amp; 4 anchors</div>'));
  const g=el('div','mGrid');
  all.forEach(sp=>{
    const bound=!!S.bound[sp];
    const c=el('div','card');
    c.style.cursor='default';
    const b=CRE[sp];
    if(bound){
      c.innerHTML='<div class="cn"><span class="dot" style="width:12px;height:12px;border-radius:50%;background:'+TYPE_COLOR(b.affs[0])+'"></span>'+esc(b.name)+'</div>'+
        '<div class="cl">'+affPills(b.affs)+'</div>'+
        '<div class="cl lore">'+esc(b.lore)+'</div>'+
        '<div class="stats"><div>HP <b>'+b.base.hp+'</b></div><div>ATK <b>'+b.base.atk+'</b></div><div>DEF <b>'+b.base.def+'</b></div><div>SPD <b>'+b.base.spd+'</b></div></div>'+
        (b.evo?'<div class="cl" style="color:var(--accent)">Reshapes at Lv '+b.evo.lv+' \u2192 '+esc(CRE[b.evo.to].name)+'</div>':'');
    } else {
      c.innerHTML='<div class="cn" style="color:var(--dim)"><span class="dot" style="width:12px;height:12px;border-radius:50%;background:#33445c"></span>???</div>'+
        '<div class="cl">Not yet bound. It waits somewhere in the drift.</div>';
    }
    g.appendChild(c);
  });
  body.appendChild(g);
}
function renderMap(body){
  const wrap=el('div','');
  wrap.appendChild(el('div','','<div style="font-size:11px;letter-spacing:.22em;text-transform:uppercase;color:var(--dim);margin-bottom:10px">The Six Reaches</div>'));
  const c=el('canvas'); c.id='mapCanvas'; c.width=Math.min(760,VW*0.82); c.height=260;
  wrap.appendChild(c);
  const list=el('div',''); list.style.marginTop='14px';
  ZONES.forEach((z,i)=>{
    const known=S.flags['seen_'+z.id];
    const row=el('div','mRow');
    row.style.opacity = S.unlocked[i]?1:0.45;
    row.innerHTML='<div class="dot" style="width:18px;height:18px;border-radius:50%;flex:none;background:'+z.glow+';box-shadow:0 0 12px '+z.glow+'"></div>'+
      '<div class="grow"><div class="t1">'+(i+1)+'. '+esc(known||S.unlocked[i]?z.name:'???' )+'</div>'+
      '<div class="t2">'+(S.guards[z.guard]?'\u2713 anchored':(S.unlocked[i]?'anchor holds: '+esc(GUARDIANS[z.guard].name):'sealed'))+
      ' \u00b7 wild Lv '+z.lv[0]+'\u2013'+z.lv[1]+(S.flags.post?' (+8 roaming)':'')+'</div></div>'+
      (S.unlocked[i]&&i!==S.zone? '<span class="pill" style="color:var(--accent)">travel</span>':'')+
      (i===S.zone?'<span class="pill" style="color:var(--ink)">here</span>':'');
    row.appendChild(mBtn('Go',()=>{ if(!S.unlocked[i]||i===S.zone) return; closeModal(); transitionTo(()=>enterZone(i)); }));
    list.appendChild(row);
  });
  wrap.appendChild(list);
  body.appendChild(wrap);
  /* draw node graph */
  const g=c.getContext('2d');
  const w=c.width,h=c.height;
  g.clearRect(0,0,w,h);
  const pts=ZONES.map((z,i)=>({x:60+ (i%3)*(w-120)/2, y: (i<3? h*0.3 : h*0.72), z}));
  g.strokeStyle='rgba(140,200,255,.28)'; g.lineWidth=2;
  g.setLineDash([6,7]);
  for(let i=0;i<5;i++){ g.beginPath(); g.moveTo(pts[i].x,pts[i].y); g.lineTo(pts[i+1].x,pts[i+1].y); g.stroke(); }
  g.setLineDash([]);
  pts.forEach((p,i)=>{
    const on=S.unlocked[i];
    g.globalAlpha=on?1:0.3;
    const grd=g.createRadialGradient(p.x,p.y,2,p.x,p.y,30);
    grd.addColorStop(0,p.z.glow); grd.addColorStop(1,'transparent');
    g.fillStyle=grd; g.beginPath(); g.arc(p.x,p.y,30,0,6.283); g.fill();
    g.fillStyle=on?p.z.glow:'#54627a'; g.beginPath(); g.arc(p.x,p.y,11,0,6.283); g.fill();
    if(i===S.zone){ g.strokeStyle='#fff'; g.lineWidth=2.5; g.beginPath(); g.arc(p.x,p.y,18,0,6.283); g.stroke(); }
    if(S.guards[p.z.guard]){ g.fillStyle='#8df29a'; g.font='bold 14px sans-serif'; g.textAlign='center'; g.fillText('\u2713',p.x,p.y+5); }
    g.globalAlpha=1;
    g.fillStyle='rgba(220,235,255,.85)'; g.font='600 10px sans-serif'; g.textAlign='center';
    g.fillText((i+1)+'. '+(on?p.z.name:'???'),p.x,p.y+40);
  });
}
function renderShop(body){
  if(!AT_SHRINE){
    body.appendChild(el('div','empty','The Warden\u2019s trade opens only at a stillpoint.\nStep onto a standing stone to buy and sell.</div>'));
    return;
  }
  body.appendChild(el('div','','<div style="font-size:12.5px;color:var(--dim);margin-bottom:12px">Motes: <b style="color:var(--aet)">'+S.motes+'</b> \u00b7 The Warden trades in salvage from the drift.</div>'));
  SHOP_STOCK.forEach(k=>{
    const it=ITEMS[k];
    const row=el('div','mRow');
    row.innerHTML='<div class="grow"><div class="t1">'+esc(it.name)+' <span style="color:var(--aet);font-family:var(--mono);font-size:11px">\u25c8 '+it.price+'</span></div>'+
      '<div class="t2">'+esc(it.desc)+'</div></div>';
    const b=mBtn('Buy',()=>{
      if(S.motes<it.price){ toast('Not enough motes.'); SFX.fail(); return; }
      S.motes-=it.price; S.bag[k]=(S.bag[k]||0)+1; SFX.confirm(); renderModal(); updatePartyStrip();
    });
    b.disabled=S.motes<it.price;
    row.appendChild(b);
    body.appendChild(row);
  });
}
function renderSave(body){
  const all=listSaves();
  const cur = S?S.slot:-1;
  for(let i=0;i<3;i++){
    const d=all['s'+i];
    const row=el('div','slot');
    row.innerHTML='<div class="grow"><div class="sname">Anchor Slot '+(i+1)+'</div><div class="smeta">'+
      (d? (ZONES[d.zone]?ZONES[d.zone].name:'?')+' \u00b7 '+d.anchors+' anchors \u00b7 '+Object.keys(d.bound||{}).length+' bound \u00b7 Level '+((d.party&&d.party[0])?d.party[0].lv:'?') : 'empty')+
      '</div></div>';
    const saveBtn=mBtn('Save here',()=>{ if(!S) return; S.slot=i; saveGame(i,false); renderModal(); });
    saveBtn.disabled=!S;
    const loadBtn=mBtn('Load',()=>{ if(loadSlot(i)){ closeModal(); enterZone(S.zone); updatePartyStrip(); toast('Lattice resumed.'); } });
    loadBtn.disabled=!d;
    row.appendChild(saveBtn); row.appendChild(loadBtn);
    body.appendChild(row);
  }
}
function renderOptions(body){
  const row=el('div','mRow');
  row.innerHTML='<div class="grow"><div class="t1">Ambience &amp; melody</div><div class="t2">Generative pads and chiptune, all synthesized live.</div></div>';
  row.appendChild(mBtn(musicOn?'On':'Off',()=>{ setMusic(!musicOn); renderModal(); }));
  body.appendChild(row);
  const row2=el('div','mRow');
  row2.innerHTML='<div class="grow"><div class="t1">Sound effects</div><div class="t2">Impacts, chimes and binding tones.</div></div>';
  row2.appendChild(mBtn(sfxOn?'On':'Off',()=>{ sfxOn=!sfxOn; renderModal(); }));
  body.appendChild(row2);
  const row3=el('div','mRow');
  row3.innerHTML='<div class="grow"><div class="t1">Return to title</div><div class="t2">Your progress is anchored at the last shrine.</div></div>';
  row3.appendChild(mBtn('Title',()=>{ saveGame(S.slot,true); closeModal(); showTitle(); }));
  body.appendChild(row3);
  body.appendChild(el('div','lore','You are a Binder. The Kindra are threads of a lattice that is coming loose. Bind them, teach them, reshape them, and set the six anchors before the Tide finishes its work.'));
}
/* ================================================================
   PART 8 — SHRINE, TITLE, STARTERS, HELP
   ================================================================ */
function openShrineMenu(fromDefeat){
  AT_SHRINE=true;
  healParty();
  saveGame(S.slot,true);
  if(!fromDefeat) SFX.heal();
  openModal('shop',fromDefeat?'You wake at the stillpoint':'Stillpoint \u2014 mended & anchored');
}
function renderStarter(body){
  body.appendChild(el('div','','<div class="lore" style="margin-bottom:14px">Three Kindra answer a new Binder. Each walks a different strand of the lattice. Choose one to begin.</div>'));
  const g=el('div','mGrid');
  ['cindling','mossprig','rillit'].forEach(sp=>{
    const b=CRE[sp];
    const card=el('div','card');
    card.innerHTML='<div class="cn"><span class="dot" style="width:13px;height:13px;border-radius:50%;background:'+TYPE_COLOR(b.affs[0])+';box-shadow:0 0 12px '+TYPE_COLOR(b.affs[0])+'"></span>'+esc(b.name)+'</div>'+
      '<div class="cl">'+affPills(b.affs)+'</div>'+
      '<div class="cl lore">'+esc(b.lore)+'</div>'+
      '<div class="stats"><div>HP <b>'+b.base.hp+'</b></div><div>ATK <b>'+b.base.atk+'</b></div><div>DEF <b>'+b.base.def+'</b></div><div>SPD <b>'+b.base.spd+'</b></div></div>'+
      '<div style="margin-top:10px">'+mBtn('Choose '+b.name,()=>startGame(sp)).outerHTML+'</div>';
    card.querySelector('button').onclick=()=>{ SFX.confirm(); startGame(sp); };
    g.appendChild(card);
  });
  body.appendChild(g);
  body.appendChild(el('div','','<div class="lore" style="margin-top:14px">Type walk: Pyre \u25b8 Bloom \u25b8 Rill \u25b8 Pyre. Stone bites Pyre and Rill. Gale bites Stone and Bloom. Aether cuts everything a little, and Umbra resists all but Aether.</div>'));
}
function renderHelp(body){
  const t=el('div','');
  const mech=[
    ['Affinities', 'Seven strands. Pyre beats Bloom beats Rill beats Pyre. Stone beats Pyre and Rill. Gale beats Stone and Bloom. Aether bites everything slightly harder. Umbra resists all attacks except Aether, which tears straight through it.'],
    ['The Weave (damage)', 'Damage = 0.4 \u00d7 Level \u00d7 ATK / DEF \u00d7 Weave-Multiplier, plus a small drift of randomness. Matching a technique\u2019s affinity to your Kindra\u2019s affinity grants a 25% resonance bonus. Critical hits land on a lucky seam.'],
    ['Turn order', 'Everything alive acts in Speed order each round. Choose a technique, swap a Kindra in (costs the turn), bind a wild Kindra, or flee.'],
    ['Condition', 'Burn sears each turn and halves Attack. Frost halves Speed. Shock may steal a whole turn. Barrier deflects one blow. Regen mends each turn. Growth techniques raise ATK, DEF or SPD.'],
    ['Binding', 'Weaken a wild Kindra, then spend a shard. A swinging marker appears \u2014 lock it at the heart of the glow for a full resonance bonus. Lower HP and finer shards bind far more reliably.'],
    ['Reshaping', 'Some Kindra cross into a second form at a set level, and need the matching catalyst from a stillpoint shoppe.'],
    ['Stillpoints', 'Standing stones mend your weave, anchor your progress, and open the Warden\u2019s trade.']
  ];
  mech.forEach(([h,d])=>{
    const row=el('div','mRow');
    row.innerHTML='<div class="grow"><div class="t1">'+esc(h)+'</div><div class="t2">'+esc(d)+'</div></div>';
    t.appendChild(row);
  });
  body.appendChild(t);
}
function startGame(starter){
  S=blankState(0);
  S.party=[newCreature(starter,5)];
  S.bound[starter]=true; S.dex[starter]={seen:true,bound:true};
  S.story=1; S.flags.started=true; S.flags.tut=0;
  M.sub=null;
  $('#modalWrap').classList.add('hidden');
  $('#title').classList.add('hidden');
  $('#hud').classList.remove('hidden');
  if(isTouch) $('#touch').classList.add('on');
  screen='world';
  enterZone(0,{entry:'start'});
  updatePartyStrip();
  setTimeout(()=>queueDialog(STORY.intro.concat(STORY.before1),()=>{
    showBubble('Move with WASD or the arrow keys. On a touch screen, use the pad.',6200,{x:VW*0.5-150,y:VH*0.26});
  }),480);
}
function showTitle(){
  screen='title'; B.on=false;
  $('#title').classList.remove('hidden');
  $('#hud').classList.add('hidden'); $('#touch').classList.remove('on');
  $('#battle').classList.add('hidden'); $('#playerCard').classList.add('hidden'); $('#cmdPanel').classList.add('hidden');
  $('#modalWrap').classList.add('hidden');
  $('#dialog').classList.add('hidden');
  const any=Object.keys(listSaves()).length>0;
  $('#btnContinue').disabled=!any;
  $('#btnContinue').style.opacity=any?1:.35;
  if(audioCtx&&musicOn) startPad(ROOTS.title);
}
function continueGame(){
  const all=listSaves();
  const keys=Object.keys(all);
  if(!keys.length) return;
  keys.sort((a,b)=>(all[b].created||0)-(all[a].created||0));
  const slot=parseInt(keys[0].slice(1));
  if(loadSlot(slot)){
    $('#title').classList.add('hidden');
    $('#hud').classList.remove('hidden');
    if(isTouch) $('#touch').classList.add('on');
    screen='world';
    enterZone(S.zone);
    updatePartyStrip();
    toast('Resumed at '+ZONES[S.zone].name+'.');
  }
}
function showSlots(){
  M.tab='save'; M.sub=null;
  $('#title').classList.add('hidden');
  screen='menu';
  $('#modalWrap').classList.remove('hidden');
  renderModal();
}
const isTouch = ('ontouchstart' in window) || Math.max(window.innerWidth,window.innerHeight)<900;

/* ================================================================
   PART 9 — MAIN LOOP & BOOT
   ================================================================ */
let last=0;
function loop(ts){
  const dt=Math.min(0.05,(ts-last)/1000)||0.016; last=ts;
  G.t+=dt;
  ctx.clearRect(0,0,VW,VH);
  if(screen==='battle'&&B.on){
    drawBattle(dt);
  } else if((screen==='world'||screen==='menu')&&S&&G.player){
    if(screen==='world') worldUpdate(dt);
    else { /* keep camera settled */ }
    if(G.shrineCd>0) G.shrineCd-=dt;
    drawWorld(dt);
  } else if(screen==='title'){
    /* title uses CSS aurora; clear only */
  }
  if(capState) updateCapture(dt);
  requestAnimationFrame(loop);
}
window.addEventListener('pointerdown',()=>{ initAudio(); if(audioCtx&&audioCtx.state==='suspended') audioCtx.resume(); },{once:false});
window.addEventListener('keydown',()=>{ initAudio(); if(audioCtx&&audioCtx.state==='suspended') audioCtx.resume(); },{once:false});

$('#btnNew').onclick=()=>{ initAudio(); SFX.confirm(); $('#title').classList.add('hidden');
  AT_SHRINE=false; screen='menu'; M.tab='starter'; M.title='Choose your first Kindra'; M.sub=null;
  $('#modalWrap').classList.remove('hidden'); renderModal(); };
$('#btnContinue').onclick=()=>{ initAudio(); SFX.confirm(); continueGame(); };
$('#btnSlots').onclick=()=>{ initAudio(); SFX.ui(); showSlots(); };
$('#btnHelp').onclick=()=>{ initAudio(); SFX.ui(); M.tab='help'; M.sub=null; M.title='Codex of the Weave';
  screen='menu'; $('#title').classList.add('hidden'); $('#modalWrap').classList.remove('hidden');
  const m=$('#modal'); m.innerHTML='';
  const head=el('div','mHead'); head.innerHTML='<h2>Codex of the Weave</h2>';
  head.appendChild(mBtn('Close',()=>{ closeModal(); showTitle(); }));
  m.appendChild(head); const body=el('div','mBody'); m.appendChild(body); renderHelp(body); };
$('#btnMute0').onclick=()=>{ setMusic(!musicOn); SFX.ui(); };

function boot(){
  resize();
  G={t:0,maps:{},parts:{},cam:{x:0,y:0},player:null,encCd:0,shrineCd:0};
  initAudio();
  setMusic(true);
  showTitle();
  requestAnimationFrame(loop);
}
boot();
</script>
</body>
</html>
<!-- agent-meta {"model":"deepseek/deepseek-v4.1-flash","provider":"openrouter","persona":"full","sessionId":"0a67b2a8-a9c4-4f97-a198-d9a10573edf5","tokensIn":22203833,"tokensOut":155455,"tokensTotal":22359288,"cost":0.20260519800000001,"turns":99,"toolCalls":98,"failedToolCalls":2,"timestamp":"2026-09-10T23:35:33.794Z"} -->