← Creature Collection results

Creature Collection

GPT 5.6 Sol · composer

Initial view of Creature Collection

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

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 45,152 bytes · SHA-256 af585f415392
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Veyra: The Lumen Binding</title>
<style>
/* ── css: base ── */
*{box-sizing:border-box} :root{--ink:#eefcff;--muted:#a9c2cc;--cyan:#70f6dc;--violet:#a987ff;--glass:rgba(8,22,32,.72);--line:rgba(184,246,255,.18);--danger:#ff6e86} html,body,#game{margin:0;width:100%;height:100%;overflow:hidden;background:#061018;color:var(--ink);font-family:ui-rounded,"Segoe UI",sans-serif}button{font:inherit;color:inherit;background:rgba(18,43,55,.82);border:1px solid var(--line);border-radius:12px;padding:11px 16px;cursor:pointer;letter-spacing:.09em;font-size:12px;font-weight:800;transition:.2s}button:hover{transform:translateY(-2px);border-color:var(--cyan);box-shadow:0 8px 28px rgba(77,245,214,.12)}button:disabled{opacity:.4;pointer-events:none}button.primary{background:linear-gradient(135deg,#57dfca,#7774ec);color:#041316;border:0}canvas{display:block}.eyebrow{font-size:10px;letter-spacing:.28em;color:var(--cyan);font-weight:900}.overlay{position:absolute;inset:0;display:none;z-index:20}.overlay.show{display:flex}.glass{background:linear-gradient(145deg,rgba(17,38,51,.92),rgba(6,17,26,.94));border:1px solid var(--line);box-shadow:0 30px 100px #0009,inset 0 1px #fff1;backdrop-filter:blur(18px);border-radius:24px}.wide{width:min(980px,92vw);padding:36px;margin:auto}
/* ── css: world ── */
#scene{width:100%;height:100%}#hud{position:absolute;left:24px;top:20px;z-index:3;pointer-events:none}.brand{font-weight:1000;letter-spacing:.2em;font-size:18px}.brand small{display:block;font-size:8px;color:var(--cyan);margin-top:3px}.brand:before{content:'◈';color:var(--cyan);margin-right:8px}#zonePill{margin-top:17px;display:inline-block;padding:7px 11px;border:1px solid var(--line);background:#071721aa;border-radius:30px;font-size:11px;letter-spacing:.12em}#objective{font-size:11px;color:var(--muted);margin-top:8px}#dock{position:absolute;z-index:5;bottom:18px;left:50%;transform:translateX(-50%);display:flex;gap:6px;padding:7px;background:#06131bd9;border:1px solid var(--line);border-radius:18px;backdrop-filter:blur(12px)}#dock button{padding:9px 12px;font-size:9px}#controls{display:none;position:absolute;z-index:6;bottom:80px;left:15px;gap:6px;align-items:end}#controls button{width:48px;height:45px;padding:0;margin:2px;touch-action:none}.act{border-radius:50%!important;margin-left:12px!important;color:var(--cyan)}#toast{position:absolute;z-index:50;top:80px;left:50%;transform:translate(-50%,-20px);padding:10px 18px;border-radius:40px;background:#071720e8;border:1px solid var(--line);opacity:0;transition:.3s;font-size:12px}#toast.on{opacity:1;transform:translate(-50%,0)}#flash{position:absolute;inset:0;pointer-events:none;z-index:60;opacity:0;background:white} @media(max-width:700px){#controls{display:flex}#dock{right:8px;left:auto;transform:none;bottom:10px}#dock button{padding:8px 6px;font-size:0}#dock button:after{content:'•';font-size:18px}#hud{left:14px;top:12px}.brand{font-size:14px}}
/* ── css: screens ── */
#titleScreen{align-items:center;justify-content:center;flex-direction:column;text-align:center;background:radial-gradient(circle at 50% 45%,#163e4d 0,#081a27 40%,#030a10 80%);overflow:hidden}#titleScreen:before,#titleScreen:after{content:'';position:absolute;width:70vmin;height:70vmin;border:1px solid #75ffe629;border-radius:45%;animation:orbit 22s linear infinite}#titleScreen:after{width:50vmin;height:50vmin;animation-direction:reverse;animation-duration:14s}.sigil{width:80px;height:80px;position:relative}.sigil i{position:absolute;inset:9px;border:1px solid var(--cyan);transform:rotate(45deg);animation:pulse 3s infinite}.sigil i:nth-child(2){inset:19px;animation-delay:.4s}.sigil i:nth-child(3){inset:29px;animation-delay:.8s}h1{font-size:clamp(64px,12vw,150px);letter-spacing:.18em;margin:3px 0 -10px;text-indent:.18em;line-height:.9;background:linear-gradient(180deg,#fff,#7df3df 70%,#4c8f9c);color:transparent;background-clip:text;filter:drop-shadow(0 0 28px #43d9c555)}#titleScreen h2{font-size:13px;letter-spacing:.55em;color:#b8e6e1}#titleScreen .lead{max-width:570px;color:var(--muted);line-height:1.7;font-size:14px;padding:0 20px}.titleBtns{display:flex;gap:10px;margin-top:15px}.hint{position:absolute;bottom:22px;font-size:8px;letter-spacing:.25em;color:#78929c}@keyframes orbit{to{transform:rotate(360deg)}}@keyframes pulse{50%{opacity:.25;transform:rotate(135deg) scale(.8)}}#starter{background:radial-gradient(circle,#193c47,#030b12);align-items:center}#starterCards{display:grid;grid-template-columns:repeat(3,1fr);gap:16px;margin-top:25px}.starterCard{padding:20px;background:#ffffff08;border:1px solid var(--line);border-radius:18px;text-align:left}.starterCard canvas{width:100%;height:140px}.starterCard h3{font-size:22px;margin:10px 0 4px}.starterCard p{color:var(--muted);font-size:12px;line-height:1.5}.starterCard button{width:100%}@media(max-width:650px){#starterCards{grid-template-columns:1fr}.starterCard canvas{height:70px}.wide{padding:20px;max-height:96vh;overflow:auto}}
/* ── css: battle ── */
#battle{background:#07121c;overflow:hidden}#battleCanvas{position:absolute;width:100%;height:100%}#battleTop{position:absolute;top:18px;left:22px;right:22px;display:flex;justify-content:space-between;font-size:10px;letter-spacing:.2em}.combatCard{position:absolute;width:min(310px,42vw);padding:13px 15px;background:#06131de6;border:1px solid var(--line);border-radius:15px;backdrop-filter:blur(10px)}.combatCard.enemy{right:5%;top:11%}.combatCard.ally{left:5%;bottom:24%}.combatCard h3{margin:0 0 8px;font-size:14px}.combatCard small{float:right;color:var(--muted)}.hpTrack{height:7px;background:#ffffff12;border-radius:9px;overflow:hidden}.hpFill{height:100%;background:linear-gradient(90deg,#4ed7a0,#9cffc4);transition:width .6s}.hpFill.low{background:linear-gradient(90deg,#ff496f,#ffb254)}.hpText{font-size:9px;color:var(--muted);margin-top:5px}#battleLog{position:absolute;left:4%;bottom:16%;width:42%;min-height:48px;padding:13px;border-left:2px solid var(--cyan);font-size:12px;line-height:1.5;background:#051019aa}#battleActions,#moveGrid{position:absolute;right:3%;bottom:4%;width:49%;display:grid;grid-template-columns:repeat(3,1fr);gap:7px}#battleActions button{min-height:48px}#moveGrid{display:none;grid-template-columns:repeat(2,1fr)}#moveGrid button{text-align:left}.moveMeta{display:block;color:var(--muted);font-size:8px;margin-top:5px}#rhythm{display:none;position:absolute;inset:0;background:#02080dcc;align-items:center;justify-content:center;flex-direction:column;z-index:10}#rhythm.show{display:flex}#rhythm .ring{position:absolute;border:2px solid var(--cyan);border-radius:50%}.outer{width:260px;height:260px;animation:shrink 1.5s linear infinite}.target{width:90px;height:90px}#rhythmTap{width:70px;height:70px;border-radius:50%;z-index:2}@keyframes shrink{from{transform:scale(1.7);opacity:0}20%{opacity:1}to{transform:scale(.2);opacity:.2}}@media(max-width:650px){#battleActions,#moveGrid{right:3%;width:94%;bottom:3%}.combatCard.ally{bottom:35%}#battleLog{bottom:25%;width:92%}}
/* ── css: panels ── */
#panel{background:#02080dcc;padding:4vh 4vw}.panel{width:100%;height:100%;padding:22px 26px;overflow:hidden}.panel header{display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid var(--line)}.panel h2{font-size:30px;margin:0 0 15px}.panel header button{font-size:24px;padding:4px 12px}#panelTabs{display:flex;gap:7px;margin:15px 0}#panelBody{height:calc(100% - 120px);overflow:auto;padding-right:5px}.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(230px,1fr));gap:12px}.entry{border:1px solid var(--line);border-radius:15px;padding:14px;background:#ffffff06}.entry h3{margin:4px 0}.entry p{font-size:11px;color:var(--muted);line-height:1.5}.aff{display:inline-block;padding:3px 7px;border-radius:10px;background:#57d9c922;font-size:8px;color:var(--cyan);margin-right:4px}.statline{display:flex;justify-content:space-between;font-size:10px;color:var(--muted);margin-top:8px}.dialogBox{position:absolute;bottom:7%;left:8%;right:8%;padding:24px 28px;background:#071520ef;border:1px solid var(--line);border-radius:22px;box-shadow:0 20px 80px #000}.dialogBox #speaker{font-size:9px;letter-spacing:.25em;color:var(--cyan)}#dialogText{font-size:18px;line-height:1.55;max-width:850px}.dialogBox button{float:right}.vignette{position:absolute;inset:0;background:radial-gradient(circle at 30% 35%,#3d776faa,#07131b 65%)}
/* ── css: clickfix ── */
#titleScreen:before,#titleScreen:after{pointer-events:none}#titleScreen>*{position:relative;z-index:1}.starterCard{position:relative;z-index:2}
</style>
</head>
<body>
<main id="game"><canvas id="scene" aria-label="Game world"></canvas><div id="hud"><div class="brand">VEYRA <small>THE LUMEN BINDING</small></div><div id="zonePill">Glasswake Verge</div><div id="objective">Follow the listening light</div></div><div id="toast"></div><div id="controls"><button data-key="up">▲</button><div><button data-key="left">◀</button><button data-key="down">▼</button><button data-key="right">▶</button></div><button data-key="act" class="act">✦</button></div><nav id="dock"><button data-panel="team">TEAM</button><button data-panel="bag">SATCHEL</button><button data-panel="atlas">ATLAS</button><button data-panel="map">MAP</button><button id="saveBtn">SAVE</button><button id="muteBtn">♫</button></nav><section id="titleScreen" class="overlay show"><div class="sigil"><i></i><i></i><i></i></div><p class="eyebrow">AN ORIGINAL CREATURE ODYSSEY</p><h1>VEYRA</h1><h2>THE LUMEN BINDING</h2><p class="lead">The sky has forgotten its shape. Bind the living echoes, steady four Anchors, and teach the horizon to remember.</p><div class="titleBtns"><button id="newBtn" class="primary">NEW BINDING</button><button id="continueBtn">CONTINUE</button></div><p class="hint">WASD / ARROWS TO MOVE · ENTER TO INTERACT · M OPENS MENU</p></section><section id="starter" class="overlay"><div class="glass wide"><p class="eyebrow">CHOOSE A FIRST ECHO</p><h2>The listening light divides into three voices.</h2><div id="starterCards"></div></div></section><section id="dialogue" class="overlay"><div class="vignette"></div><div class="dialogBox"><p id="speaker">THE LANTERN WARDEN</p><div id="dialogText"></div><button id="dialogNext" class="primary">CONTINUE</button></div></section><section id="battle" class="overlay"><canvas id="battleCanvas"></canvas><div id="battleTop"><span id="battleLabel">WILD RESONANCE</span><button id="battleMute">♫</button></div><div id="enemyCard" class="combatCard enemy"></div><div id="allyCard" class="combatCard ally"></div><div id="battleLog">The air gathers a voice...</div><div id="battleActions"><button data-act="moves">ABILITIES</button><button data-act="bind">BIND</button><button data-act="switch">SWITCH</button><button data-act="bag">ITEM</button><button data-act="flee">WITHDRAW</button></div><div id="moveGrid"></div><div id="rhythm"><p>Tap when the rings align</p><div class="ring outer"></div><div class="ring target"></div><button id="rhythmTap">BIND</button></div></section><section id="panel" class="overlay"><div class="glass panel"><header><div><p class="eyebrow" id="panelKicker">WAYFARER RECORD</p><h2 id="panelTitle">Team</h2></div><button id="panelClose">×</button></header><div id="panelTabs"></div><div id="panelBody"></div></div></section><div id="flash"></div></main>
<script>
'use strict';

// ── const: AFFINITIES ──
const AFFINITIES = ['Cinder','Tideglass','Verdure','Volt','Gloom','Astral','Bastion'];

// ── const: AFF_COLORS ──
const AFF_COLORS = {Cinder:'#ff765f',Tideglass:'#54d9ed',Verdure:'#74e29b',Volt:'#f4dd63',Gloom:'#b783ef',Astral:'#ff9ed8',Bastion:'#a7bac5'};

// ── const: ZONES ──
const ZONES = [
{name:'Glasswake Verge',grad:['#102e3b','#36766e'],rate:.07,guardian:'The Hush Anchor',bonus:'+5% binding clarity'},
{name:'Emberfold Steps',grad:['#431c2c','#b84c42'],rate:.09,guardian:'The Sear Anchor',bonus:'+5% party vigor'},
{name:'Mosslight Hollows',grad:['#102d29','#497a42'],rate:.11,guardian:'The Root Anchor',bonus:'+5% gained insight'},
{name:'Stormscript Shelf',grad:['#18264b','#6253a6'],rate:.12,guardian:'The Gale Anchor',bonus:'+5% ability precision'},
{name:'Nocturne Basin',grad:['#19152d','#563c69'],rate:.14,guardian:'The Veil Regent',bonus:'Rare echoes emerge'},
{name:'Starless Crown',grad:['#090e1e','#294d65'],rate:.16,guardian:'The Unwritten Choir',bonus:'The horizon remembers'}];

// ── const: ABILITY_NAMES ──
const ABILITY_NAMES = ['Coal Whisper','Glass Current','Briar Knock','Static Quill','Dusk Nibble','Comet Thread','Slate Guard','Flare Arc','Undertow Bell','Pollen Spiral','Quick Jolt','Shade Fold','Lumen Lance','Granite Hum','Searing Waltz','Rime Sip','Thorn Chorus','Pulse Snare','Umbral Wink','Orbit Bloom','Iron Patience','Ashen Vow','Mirror Rain','Rootbound','Sky Crackle','Night Orchard','Nova Pin','Wall of Hours','Cinder Many','Tidal Mend','Verdant Haste','Shock Chime','Gloom Leech','Astral Step','Bastion Break','Scorch Script','Frost Lattice','Regen Rhyme','Priority Gleam','Prism Torrent','Ember Crown','Moonwell','Vine Vault','Thunderwake','Null Murmur','Star Chorus','Citadel Skin','Sevenfold Ray'];

// ── const: ABILITIES ──
const ABILITIES = Object.fromEntries(ABILITY_NAMES.map((name,i)=>[name,{name,aff:AFFINITIES[i%7],power:i%7===1?0:18+(i%6)*7,acc:78+(i%5)*5,pp:4+(i%4)*2,effect:['burn','frost','regen','shock','drain','priority','barrier','boost','multi','heal'][i%10]}]));

// ── const: CREATURE_BASE ──
const CREATURE_BASE = [
['Flintail','A coal-hearted burrower that writes warm circles beneath cold soil.','Cinder',48,15,10,12],['Ripplefin','Its glassy fins collect forgotten rain-songs and release them at twilight.','Tideglass',52,11,13,12],['Mosswisp','A walking knot of green light; tiny gardens wake in its footprints.','Verdure',55,10,14,9],['Quillvolt','It stores storms in hollow spines, humming before the clouds arrive.','Volt',44,16,9,16],['Nimveil','A soft-edged prowler stitched from the shadows between lanterns.','Gloom',46,14,10,15],['Orbilume','Its orbiting motes are said to be punctuation from an unfinished sky.','Astral',45,15,9,17],['Cairnkin','Patient as a buried century, it remembers every road laid over it.','Bastion',65,9,18,6],['Pyrrindle','A ribbon-bodied spark that nests in cooling kiln bells.','Cinder',49,17,9,14],['Dewgongle','It rings its translucent throat to call mist across dry ravines.','Tideglass',58,10,15,8],['Bramblet','Bashful and barbed, it gifts ripe glowberries to quiet travelers.','Verdure',54,13,13,10],['Zappet','A restless tuft of charge that races its own thunderclap.','Volt',43,15,8,19],['Murkmoth','Its powder makes old memories briefly visible in the air.','Gloom',47,13,11,15],['Astrolisk','It watches seven directions at once, though none point north.','Astral',51,16,10,13],['Bulwarkle','Loose stones gather around it whenever danger draws near.','Bastion',67,10,19,5],['Cindervane','A proud heat-feather whose turns leave glowing calligraphy.','Cinder',50,18,9,15],['Tidepurl','A curious drop-creature that knots streams into silver loops.','Tideglass',56,11,14,11],['Sporeling','It trades dreams with mushrooms through a network of silver roots.','Verdure',53,12,13,12],['Veyrush','A rare horizon-swimmer visible only where two colors of sky meet.','Astral',60,17,13,18]];

// ── const: TRANSFORMS ──
const TRANSFORMS = [['Flintail','Pyrolynx'],['Ripplefin','Chorallune'],['Mosswisp','Groveid'],['Quillvolt','Tempestrill'],['Nimveil','Umbraxis'],['Orbilume','Cosmora'],['Cairnkin','Monolithen'],['Zappet','Boltrix'],['Astrolisk','Zenithra']];

// ── const: CREATURES ──
const CREATURES = (()=>{const out=CREATURE_BASE.map((c,i)=>({id:i,name:c[0],lore:c[1],aff:[c[2]],base:{hp:c[3],atk:c[4],def:c[5],spd:c[6]},learn:[ABILITY_NAMES[i%20],ABILITY_NAMES[(i+7)%40],ABILITY_NAMES[(i+15)%40],ABILITY_NAMES[(i+24)%40],ABILITY_NAMES[(i+34)%48]],transform:TRANSFORMS.find(x=>x[0]===c[0])?.[1]||null,at:18+(i%5)*3})); TRANSFORMS.forEach((t,j)=>{const b=out.find(x=>x.name===t[0]);out.push({id:18+j,name:t[1],lore:`${b.lore} In transformed form, its resonance has opened into a rarer and more deliberate pattern.`,aff:[b.aff[0],AFFINITIES[(j+3)%7]],base:{hp:b.base.hp+22,atk:b.base.atk+8,def:b.base.def+7,spd:b.base.spd+5},learn:b.learn,from:t[0],transform:null,at:50})});return out})();

// ── function: makeEcho ──
function makeEcho(id,level=5){const s=CREATURES[id];const scale=1+(level-1)*.075;const max=Math.floor(s.base.hp*scale);return {uid:Date.now()+Math.random(),id,name:s.name,level,xp:0,maxHp:max,hp:max,atk:Math.floor(s.base.atk*scale),def:Math.floor(s.base.def*scale),spd:Math.floor(s.base.spd*scale),aff:s.aff,status:{},moves:s.learn.slice(0,4).map(n=>({...ABILITIES[n],cur:ABILITIES[n].pp}))}}

// ── function: newState ──
function newState(){return {slot:1,zone:0,x:3,y:5,facing:'up',party:[],collection:[],inventory:{'Hush Orb':8,'Prism Orb':2,'Crown Orb':0,'Mend Dew':5,'Wake Salve':2,'Ember Seed':1,'Moon Shard':0,'Root Key':0},anchors:0,story:0,tutorial:0,steps:0,post:false,options:{mute:false},lastShrine:{zone:0,x:3,y:5}}}

// ── let: G ──
let G=newState();

// ── let: mode ──
let mode='title';

// ── let: battle ──
let battle=null;

// ── let: keys ──
let keys={};

// ── let: worldParticles ──
let worldParticles=[];

// ── let: audio ──
let audio=null;

// ── let: lastTime ──
let lastTime=0;

// ── function: mult ──
function mult(moveAff,targetAff){let m=1;const a=moveAff,t=targetAff[0];if((a==='Cinder'&&t==='Tideglass')||(a==='Tideglass'&&t==='Verdure')||(a==='Verdure'&&t==='Cinder'))m=1.5;if(a==='Volt'&&(t==='Cinder'||t==='Verdure'))m=1.5;if(a==='Gloom'&&(t==='Volt'||t==='Tideglass'))m=1.5;if(a==='Astral'&&t!=='Bastion')m=1.15;if(t==='Bastion')m=a==='Astral'?1.5:.72;return m}

// ── function: saveGame ──
function saveGame(manual=false){try{localStorage.setItem('veyra-binding',JSON.stringify(G));toast(manual?'Binding safely inscribed.':'Rest point inscribed.')}catch(e){toast('The local inscription failed.') }}

// ── function: loadGame ──
function loadGame(){try{const s=JSON.parse(localStorage.getItem('veyra-binding'));if(s){G=s;G.party.forEach(e=>e.moves.forEach(m=>m.cur??=m.pp));return true}}catch(e){}return false}

// ── function: show ──
function show(id,on=true){document.getElementById(id).classList.toggle('show',on)}

// ── function: toast ──
function toast(s){const e=document.getElementById('toast');e.textContent=s;e.classList.add('on');clearTimeout(toast.t);toast.t=setTimeout(()=>e.classList.remove('on'),2100)}

// ── function: rng ──
function rng(a,b){return a+Math.random()*(b-a)}

// ── function: statsHTML ──
function statsHTML(e){const pct=Math.max(0,e.hp/e.maxHp*100);return `<h3>${e.name} <small>LV ${e.level} · ${e.aff.join('/')}</small></h3><div class="hpTrack"><div class="hpFill ${pct<30?'low':''}" style="width:${pct}%"></div></div><div class="hpText">${Math.max(0,e.hp)} / ${e.maxHp} &nbsp; ${Object.keys(e.status).join(' · ')}</div>`}

// ── function: tone ──
function tone(freq=220,d=.1,type='sine'){if(G.options.mute)return;try{audio??=new (window.AudioContext||window.webkitAudioContext)();const o=audio.createOscillator(),g=audio.createGain();o.type=type;o.frequency.value=freq;g.gain.setValueAtTime(.04,audio.currentTime);g.gain.exponentialRampToValueAtTime(.001,audio.currentTime+d);o.connect(g).connect(audio.destination);o.start();o.stop(audio.currentTime+d)}catch(e){}}

// ── function: ambient ──
function ambient(){if(mode==='world'&&!G.options.mute){tone(90+G.zone*18,.8,'sine');if(Math.random()<.5)tone(180+Math.random()*90,.5,'triangle')}setTimeout(ambient,2200)}

// ── function: fitCanvas ──
function fitCanvas(c){const d=devicePixelRatio||1,w=c.clientWidth,h=c.clientHeight;if(c.width!==w*d||c.height!==h*d){c.width=w*d;c.height=h*d}const x=c.getContext('2d');x.setTransform(d,0,0,d,0,0);return {x,w,h}}

// ── function: drawEcho ──
function drawEcho(ctx,e,x,y,s=1,t=0){const c=AFF_COLORS[e.aff[0]],id=e.id;ctx.save();ctx.translate(x,y+Math.sin(t*.004+id)*5);ctx.scale(s,s);ctx.shadowBlur=25;ctx.shadowColor=c;ctx.fillStyle=c;ctx.globalAlpha=.18;ctx.beginPath();ctx.ellipse(0,16,42,12,0,0,7);ctx.fill();ctx.globalAlpha=1;ctx.fillStyle=c;ctx.beginPath();const horns=id%3;ctx.moveTo(-25,-20);if(horns)ctx.lineTo(-34,-48);ctx.lineTo(-11,-29);ctx.quadraticCurveTo(0,-45,18,-30);if(horns===2)ctx.lineTo(35,-48);ctx.lineTo(28,-15);ctx.quadraticCurveTo(39,10,20,28);ctx.quadraticCurveTo(0,40,-24,26);ctx.quadraticCurveTo(-39,5,-25,-20);ctx.fill();ctx.fillStyle='#07131c';ctx.beginPath();ctx.arc(-10,-7,4,0,7);ctx.arc(12,-7,4,0,7);ctx.fill();ctx.fillStyle='#fff';ctx.beginPath();ctx.arc(-9,-8,1.3,0,7);ctx.arc(13,-8,1.3,0,7);ctx.fill();ctx.strokeStyle='#ffffff88';ctx.lineWidth=2;for(let i=0;i<id%5;i++){ctx.beginPath();ctx.arc(0,2,15+i*4,i,4+i);ctx.stroke()}ctx.restore()}

// ── function: drawWorld ──
function drawWorld(t){const c=document.getElementById('scene'),{x,w,h}=fitCanvas(c),z=ZONES[G.zone];const gr=x.createLinearGradient(0,0,w,h);gr.addColorStop(0,z.grad[0]);gr.addColorStop(1,z.grad[1]);x.fillStyle=gr;x.fillRect(0,0,w,h);x.globalAlpha=.16;for(let i=0;i<70;i++){const px=(i*137+t*(.006+G.zone*.002))%w,py=(i*i*43+t*.01)%h;x.fillStyle=i%3?'#fff':AFF_COLORS[AFFINITIES[G.zone%7]];x.beginPath();x.arc(px,py,1+(i%3),0,7);x.fill()}x.globalAlpha=1;const tile=Math.min(w/14,h/10),ox=(w-tile*12)/2,oy=(h-tile*8)/2;x.strokeStyle='#ffffff08';for(let yy=0;yy<8;yy++)for(let xx=0;xx<12;xx++){x.strokeRect(ox+xx*tile,oy+yy*tile,tile,tile);if((xx*7+yy*11+G.zone)%13===0){x.fillStyle='#ffffff0b';x.beginPath();x.arc(ox+(xx+.5)*tile,oy+(yy+.5)*tile,tile*.22,0,7);x.fill()}}const nodes=[{x:2,y:6,c:'#83ffdf',icon:'◇'},{x:6,y:3,c:'#ffb4e2',icon:'◌'},{x:10,y:2,c:'#ffdc76',icon:'⬡'}];nodes.forEach((n,i)=>{x.shadowBlur=22;x.shadowColor=n.c;x.fillStyle=n.c;x.font=`${tile*.45}px sans-serif`;x.textAlign='center';x.fillText(n.icon,ox+(n.x+.5)*tile,oy+(n.y+.65)*tile);x.shadowBlur=0;x.fillStyle='#ffffffaa';x.font='9px sans-serif';x.fillText(['SHRINE','MEMORY','ANCHOR'][i],ox+(n.x+.5)*tile,oy+(n.y+1)*tile)});const px=ox+(G.x+.5)*tile,py=oy+(G.y+.5)*tile;x.shadowBlur=25;x.shadowColor='#7fffe7';x.fillStyle='#eafffb';x.beginPath();x.arc(px,py,tile*.17,0,7);x.fill();x.strokeStyle='#7fffe7';x.beginPath();x.arc(px,py,tile*.27+Math.sin(t*.006)*3,0,7);x.stroke();x.shadowBlur=0;requestAnimationFrame(loop)}

// ── function: loop ──
function loop(t){lastTime=t;if(mode==='world')drawWorld(t);else if(mode==='battle')drawBattle(t)}

// ── function: updateWorldHud ──
function updateWorldHud(){document.getElementById('zonePill').textContent=`${G.zone+1} / 6 · ${ZONES[G.zone].name}`;document.getElementById('objective').textContent=G.anchors<4?`Steady ${4-G.anchors} more Anchor${4-G.anchors===1?'':'s'} · ${G.collection.length}/8 echoes`:'The Crown is calling'}

// ── function: move ──
function move(dx,dy){if(mode!=='world')return;let nx=G.x+dx,ny=G.y+dy;if(nx<0&&G.zone>0){G.zone--;nx=11}else if(nx>11&&G.zone<5&&G.zone<=G.anchors){G.zone++;nx=0;storyCheck()}else if(nx<0||nx>11){toast(G.zone===5?'The horizon ends here.':'The Anchor seals the way.');return}if(ny<0||ny>7)return;G.x=nx;G.y=ny;G.steps++;updateWorldHud();tone(120+G.zone*15,.035,'triangle');if(G.tutorial===2){if(G.steps>=3){G.tutorial=3;const foe=makeEcho(3,3);foe.hp=Math.floor(foe.maxHp*.65);startBattle([foe],false);battleLog('Tutorial: choose ABILITIES. When its light is low, choose BIND and tap as the rings align.')}return}if(Math.random()<ZONES[G.zone].rate&&(G.x!==2||G.y!==6))startWild()}

// ── function: interact ──
function interact(){if(mode!=='world')return;if(Math.abs(G.x-2)+Math.abs(G.y-6)<=1)restShrine();else if(Math.abs(G.x-6)+Math.abs(G.y-3)<=1)storyCheck(true);else if(Math.abs(G.x-10)+Math.abs(G.y-2)<=1)startGuardian();else toast('Only the distant wind answers.')}

// ── function: restShrine ──
function restShrine(){G.party.forEach(e=>{e.hp=e.maxHp;e.status={};e.moves.forEach(m=>m.cur=m.pp)});G.lastShrine={zone:G.zone,x:2,y:6};G.inventory['Hush Orb']+=2;saveGame();toast('The shrine restores every voice. +2 Hush Orbs')}

// ── function: storyCheck ──
function storyCheck(force=false){const stages=[['WARDEN ORO','You carry a voice beside your own. Move gently; the wild echoes answer footsteps with challenge. We bind them not as possessions, but as promises.'],['THE VERGE REMEMBERS','One Anchor steadies. Beyond it, the Emberfold burns with sentences no one has finished. Eight distinct voices may be enough to read them.'],['THE HOLLOW CHOIR','The Anchors are not locks. They are tuning forks, holding the world to a shape it chose long ago. You are changing the chord.'],['THE UNWRITTEN SKY','Four Anchors answer. Gather eight names, then climb the Starless Crown and face the silence that has been teaching the storms.']];const idx=Math.min(3,G.anchors);if(force||idx>G.story){G.story=Math.max(G.story,idx);openDialogue(stages[idx][0],stages[idx][1])}}

// ── function: openDialogue ──
function openDialogue(speaker,text){mode='dialog';document.getElementById('speaker').textContent=speaker;document.getElementById('dialogText').textContent=text;show('dialogue');tone(260,.5,'sine')}

// ── function: startWild ──
function startWild(){const count=Math.random()<.22+G.zone*.04?2+(Math.random()<.25?1:0):1;const pool=Math.min(17,4+G.zone*3);const enemies=Array.from({length:count},()=>makeEcho(Math.floor(Math.random()*pool),Math.min(45,4+G.zone*5+Math.floor(Math.random()*5)+(G.post?8:0))));startBattle(enemies,false)}

// ── function: startGuardian ──
function startGuardian(){if(G.zone<4&&G.anchors>G.zone){toast('This Anchor already sings steadily.');return}const id=18+Math.min(8,G.zone);const enemy=makeEcho(id,10+G.zone*7);enemy.name=G.zone<4?ZONES[G.zone].guardian:enemy.name;enemy.maxHp=Math.floor(enemy.maxHp*1.6);enemy.hp=enemy.maxHp;startBattle([enemy],true)}

// ── function: startBattle ──
function startBattle(enemies,boss=false){if(!G.party.some(e=>e.hp>0)){restShrine();return}battle={enemies,index:0,ally:G.party.findIndex(e=>e.hp>0),boss,busy:false,turn:0};mode='battle';show('battle');document.getElementById('battleLabel').textContent=boss?'ANCHOR RESONANCE':'WILD RESONANCE';document.getElementById('battleActions').style.display='grid';document.getElementById('moveGrid').style.display='none';refreshBattle();tone(boss?90:150,.6,'sawtooth');requestAnimationFrame(loop)}

// ── function: activeAlly ──
function activeAlly(){return G.party[battle.ally]}

// ── function: activeEnemy ──
function activeEnemy(){return battle.enemies[battle.index]}

// ── function: refreshBattle ──
function refreshBattle(){if(!battle)return;document.getElementById('enemyCard').innerHTML=statsHTML(activeEnemy());document.getElementById('allyCard').innerHTML=statsHTML(activeAlly())}

// ── function: battleLog ──
function battleLog(s){document.getElementById('battleLog').textContent=s}

// ── function: drawBattle ──
function drawBattle(t){const c=document.getElementById('battleCanvas'),{x,w,h}=fitCanvas(c),z=ZONES[G.zone];const g=x.createLinearGradient(0,0,0,h);g.addColorStop(0,z.grad[1]);g.addColorStop(1,'#06101a');x.fillStyle=g;x.fillRect(0,0,w,h);x.fillStyle='#ffffff08';for(let i=0;i<16;i++){x.beginPath();x.arc((i*113+t*.012)%w,h*.42+Math.sin(i+t*.001)*80,30+i%4*13,0,7);x.fill()}if(battle){drawEcho(x,activeEnemy(),w*.72,h*.35,Math.min(2,w/550),t);drawEcho(x,activeAlly(),w*.25,h*.62,Math.min(2.3,w/500),t)}requestAnimationFrame(loop)}

// ── function: showMoves ──
function showMoves(){if(!battle||battle.busy)return;const g=document.getElementById('moveGrid'),a=activeAlly();g.innerHTML='';a.moves.forEach((m,i)=>{const b=document.createElement('button');b.innerHTML=`${m.name}<span class="moveMeta">${m.aff} · ${m.cur}/${m.pp} · PWR ${m.power}</span>`;b.style.borderColor=AFF_COLORS[m.aff]+'66';b.disabled=m.cur<=0;b.onclick=()=>playerMove(i);g.appendChild(b)});document.getElementById('battleActions').style.display='none';g.style.display='grid'}

// ── function: applyMove ──
function applyMove(att,def,m){m.cur=Math.max(0,m.cur-1);if(Math.random()*100>m.acc)return {text:`${att.name}'s ${m.name} dissolves wide.`,dmg:0};let crit=Math.random()<.09,mul=mult(m.aff,def.aff),stab=att.aff.includes(m.aff)?1.25:1;let atk=att.atk*(att.status.burn?.turns?0.5:1);let dmg=m.power?Math.max(1,Math.floor(.4*att.level*atk/Math.max(1,def.def)*mul*stab*(crit?1.5:1)+rng(1,5))):0;if(def.status.barrier&&dmg){dmg=0;delete def.status.barrier}def.hp=Math.max(0,def.hp-dmg);const hit=Math.random()<.45;if(hit&&m.effect==='burn')def.status.burn={turns:3};if(hit&&m.effect==='frost')def.status.frost={turns:3};if(hit&&m.effect==='shock')def.status.shock={turns:2};if(m.effect==='barrier')att.status.barrier={turns:9};if(m.effect==='regen')att.status.regen={turns:3};if(m.effect==='heal')att.hp=Math.min(att.maxHp,att.hp+Math.floor(att.maxHp*.25));if(m.effect==='drain')att.hp=Math.min(att.maxHp,att.hp+Math.floor(dmg/2));if(m.effect==='boost')att.spd=Math.floor(att.spd*1.15);if(m.effect==='multi'&&dmg){const extra=Math.floor(dmg*.55);def.hp=Math.max(0,def.hp-extra);dmg+=extra}if(mul>1.2){document.getElementById('flash').animate([{opacity:.35},{opacity:0}],{duration:400});tone(480,.12,'square')}else tone(230,.09,'triangle');return {text:`${att.name} casts ${m.name}! ${dmg?`${dmg} resonance${crit?' — CRITICAL':''}${mul>1.2?' — piercing!':''}`:'Its pattern shifts.'}`,dmg}}

// ── function: statusTick ──
function statusTick(e){if(e.status.burn){e.hp=Math.max(0,e.hp-Math.max(1,Math.floor(e.maxHp*.06)));if(--e.status.burn.turns<=0)delete e.status.burn}if(e.status.regen){e.hp=Math.min(e.maxHp,e.hp+Math.max(1,Math.floor(e.maxHp*.06)));if(--e.status.regen.turns<=0)delete e.status.regen}['frost','shock'].forEach(s=>{if(e.status[s]&&--e.status[s].turns<=0)delete e.status[s]})}

// ── function: playerMove ──
function playerMove(i){if(battle.busy)return;const a=activeAlly(),e=activeEnemy(),m=a.moves[i];battle.busy=true;document.getElementById('moveGrid').style.display='none';const enemyMove=e.moves.find(q=>q.cur>0)||e.moves[0];const pPri=m.effect==='priority'?99:a.spd*(a.status.frost?0.5:1),ePri=enemyMove.effect==='priority'?99:e.spd*(e.status.frost?0.5:1);const order=pPri>=ePri?[[a,e,m,true],[e,a,enemyMove,false]]:[[e,a,enemyMove,false],[a,e,m,true]];resolveOrder(order,0)}

// ── function: resolveOrder ──
function resolveOrder(order,i){if(i>=order.length){statusTick(activeAlly());statusTick(activeEnemy());refreshBattle();if(checkBattleEnd())return;battle.busy=false;document.getElementById('battleActions').style.display='grid';return}const [att,def,m,isPlayer]=order[i];if(att.hp<=0)return resolveOrder(order,i+1);if(att.status.shock&&Math.random()<.3){battleLog(`${att.name} loses the beat to shock.`);return setTimeout(()=>resolveOrder(order,i+1),700)}const r=applyMove(att,def,m);battleLog(r.text);refreshBattle();setTimeout(()=>{if(def.hp<=0){checkBattleEnd()}else resolveOrder(order,i+1)},850)}

// ── function: checkBattleEnd ──
function checkBattleEnd(){const e=activeEnemy(),a=activeAlly();if(e.hp<=0){grantExp(e);if(battle.index<battle.enemies.length-1){battle.index++;battleLog(`Another echo approaches: ${activeEnemy().name}.`);refreshBattle();setTimeout(()=>{battle.busy=false;document.getElementById('battleActions').style.display='grid'},900);return true}victory();return true}if(a.hp<=0){const next=G.party.findIndex((q,i)=>i!==battle.ally&&q.hp>0);if(next>=0){battle.ally=next;refreshBattle();battleLog(`${activeAlly().name} takes the vow.`);battle.busy=false;document.getElementById('battleActions').style.display='grid'}else defeat();return true}return false}

// ── function: grantExp ──
function grantExp(e){const a=activeAlly();const gain=Math.floor(e.level*12*(e.level/Math.max(1,a.level))*(G.anchors>=3?1.05:1));a.xp+=gain;while(a.level<50&&a.xp>=a.level*25){a.xp-=a.level*25;a.level++;a.maxHp+=Math.floor(CREATURES[a.id].base.hp*.075);a.hp=a.maxHp;a.atk++;a.def++;a.spd++;toast(`${a.name} rises to level ${a.level}!`)} }

// ── function: victory ──
function victory(){let narrative=false;if(battle.boss&&G.zone<4&&G.anchors===G.zone){G.anchors++;G.inventory['Prism Orb']++;toast(`${ZONES[G.zone].guardian} steadied! ${ZONES[G.zone].bonus}`);narrative=true;if(G.anchors===4&&G.collection.length>=8){G.post=true;G.inventory['Crown Orb']+=3}}endBattle('Victory. The gathered voices grow clearer.');if(narrative)setTimeout(()=>{if(G.post)openDialogue('THE HORIZON, REMEMBERING','Eight names answer four steady Anchors. The sky folds into a shape of its own choosing—not restored, but renewed. Harder wild resonances now roam every zone; your binding may continue without end.');else storyCheck()},1400)}

// ── function: defeat ──
function defeat(){G.zone=G.lastShrine.zone;G.x=G.lastShrine.x;G.y=G.lastShrine.y;G.party.forEach(e=>{e.hp=e.maxHp;e.status={}});endBattle('Your binding frays. The shrine recalls you.')}

// ── function: endBattle ──
function endBattle(msg){battleLog(msg);setTimeout(()=>{show('battle',false);mode='world';battle=null;updateWorldHud();requestAnimationFrame(loop)},1100)}

// ── function: flee ──
function flee(){if(battle?.boss){battleLog('The Anchor seals every retreat.');return}if(Math.random()<.75)endBattle('You withdraw between two heartbeats.');else{battleLog('The way folds shut!');setTimeout(enemyOnly,600)}}

// ── function: enemyOnly ──
function enemyOnly(){if(!battle)return;const e=activeEnemy(),a=activeAlly(),m=e.moves.find(q=>q.cur>0)||e.moves[0];applyMove(e,a,m);refreshBattle();if(!checkBattleEnd())battle.busy=false}

// ── function: startBind ──
function startBind(){if(battle?.boss){battleLog('Anchors cannot be bound. They must be steadied.');return}document.getElementById('rhythm').classList.add('show');battle.rhythmStart=performance.now()}

// ── function: tapBind ──
function tapBind(){const elapsed=(performance.now()-battle.rhythmStart)%1500,ring=Math.abs(elapsed/1500-.78);const skill=Math.max(0,1-ring*4);const e=activeEnemy();const item=G.inventory['Crown Orb']?'Crown Orb':G.inventory['Prism Orb']?'Prism Orb':'Hush Orb';if(!G.inventory[item]){document.getElementById('rhythm').classList.remove('show');battleLog('No binding vessels remain.');return}G.inventory[item]--;const quality=item==='Crown Orb'?.5:item==='Prism Orb'?.28:.12;const chance=Math.min(.94,.12+(1-e.hp/e.maxHp)*.62+quality+skill*.18);document.getElementById('rhythm').classList.remove('show');if(Math.random()<chance){if(!G.collection.includes(e.id))G.collection.push(e.id);const caught=makeEcho(e.id,e.level);if(G.party.length<6)G.party.push(caught);if(G.anchors>=4&&G.collection.length>=8&&!G.post){G.post=true;G.inventory['Crown Orb']+=3;toast('The horizon remembers! Post-binding wilds awaken.')}battleLog(`${e.name} accepts the binding! ${G.party.length<=6?'It joins your chorus.':'Its name enters the Atlas.'}`);tone(620,.5,'sine');setTimeout(()=>endBattle('A new promise is written.'),900)}else{battleLog(`${e.name} refuses the vessel (${Math.floor(chance*100)}% clarity).`);battle.busy=true;setTimeout(enemyOnly,800)}}

// ── function: renderStarters ──
function renderStarters(){const box=document.getElementById('starterCards');box.innerHTML='';[0,1,2].forEach(id=>{const s=CREATURES[id],d=document.createElement('div');d.className='starterCard';d.innerHTML=`<canvas></canvas><span class="aff">${s.aff[0]}</span><h3>${s.name}</h3><p>${s.lore}</p><div class="statline"><b>HP ${s.base.hp}</b><b>ATK ${s.base.atk}</b><b>DEF ${s.base.def}</b><b>SPD ${s.base.spd}</b></div><button class="primary">BIND ${s.name.toUpperCase()}</button>`;d.querySelector('button').onclick=()=>chooseStarter(id);box.appendChild(d);const c=d.querySelector('canvas'),ctx=c.getContext('2d');c.width=300;c.height=140;drawEcho(ctx,{id,aff:s.aff},150,82,1.25,0)})}

// ── function: chooseStarter ──
function chooseStarter(id){const e=makeEcho(id,5);G.party=[e];G.collection=[id];show('starter',false);mode='dialog';openDialogue('WARDEN ORO',`This is ${e.name}. Its voice now travels beside yours. Walk with WASD or the arrow keys. Seek wild echoes, weaken them in turn-bound contests, then use a binding vessel when their light runs low.`);G.tutorial=1;saveGame()}

// ── function: beginNew ──
function beginNew(){G=newState();show('titleScreen',false);show('starter');mode='starter';renderStarters();tone(330,.5)}

// ── function: continueGame ──
function continueGame(){if(!loadGame()){toast('No binding has been inscribed yet.');return}show('titleScreen',false);mode='world';updateWorldHud();requestAnimationFrame(loop)}

// ── function: openPanel ──
function openPanel(type){if(mode!=='world'&&mode!=='battle')return;const prev=mode;mode='panel';show('panel');const titles={team:'Bound Chorus',bag:'Wayfarer Satchel',atlas:'Echo Atlas',map:'Horizon Map',switch:'Choose a Voice'};document.getElementById('panelTitle').textContent=titles[type]||type;document.getElementById('panel').dataset.prev=prev;const body=document.getElementById('panelBody');if(type==='team'||type==='switch')renderTeam(body,type==='switch');if(type==='bag')renderBag(body);if(type==='atlas')renderAtlas(body);if(type==='map')renderMap(body)}

// ── function: closePanel ──
function closePanel(){show('panel',false);const prev=document.getElementById('panel').dataset.prev;mode=prev==='battle'&&battle?'battle':'world';if(mode==='battle')requestAnimationFrame(loop);else requestAnimationFrame(loop)}

// ── function: renderTeam ──
function renderTeam(body,switching=false){body.innerHTML='<div class="grid"></div>';const g=body.firstChild;G.party.forEach((e,i)=>{const s=CREATURES[e.id],d=document.createElement('div');d.className='entry';d.innerHTML=`<span class="aff">${e.aff.join(' · ')}</span><h3>${e.name} <small>LV ${e.level}</small></h3>${statsHTML(e)}<p>${s.lore}</p><div class="statline"><b>ATK ${e.atk}</b><b>DEF ${e.def}</b><b>SPD ${e.spd}</b><b>EXP ${e.xp}/${e.level*25}</b></div>`;const btn=document.createElement('button');if(switching){btn.textContent='SEND FORTH';btn.disabled=e.hp<=0||i===battle?.ally;btn.onclick=()=>{battle.ally=i;closePanel();refreshBattle();battleLog(`${e.name} takes the leading voice.`);battle.busy=true;setTimeout(enemyOnly,650)}}else if(s.transform){btn.textContent=`TRANSFORM · LV ${s.at}`;btn.disabled=e.level<s.at||!G.inventory['Ember Seed'];btn.onclick=()=>transformEcho(i)}else{btn.textContent='FORM COMPLETE';btn.disabled=true}d.appendChild(btn);g.appendChild(d)})}

// ── function: transformEcho ──
function transformEcho(i){const e=G.party[i],s=CREATURES[e.id],to=CREATURES.find(q=>q.name===s.transform);if(!to)return;G.inventory['Ember Seed']--;const n=makeEcho(to.id,e.level);n.xp=e.xp;G.party[i]=n;if(!G.collection.includes(to.id))G.collection.push(to.id);toast(`${e.name} opens into ${n.name}!`);renderTeam(document.getElementById('panelBody'));tone(700,.8,'sine')}

// ── function: renderBag ──
function renderBag(body){const near=G.x===2&&G.y===6;body.innerHTML=`${near?'<div class="entry"><h3>Shrine Exchange</h3><p>The shrine shapes field supplies from spare resonance.</p><button id="buyOrb">SHAPE HUSH ORB · free blessing</button> <button id="buySeed">SHAPE EMBER SEED · trade 3 Hush Orbs</button></div>':''}<div class="grid">${Object.entries(G.inventory).map(([k,v])=>`<div class="entry"><span class="aff">${['Hush Orb','Prism Orb','Crown Orb'].includes(k)?'BINDING':'SUPPLY'}</span><h3>${k}</h3><p>${itemLore(k)}</p><b>× ${v}</b></div>`).join('')}</div>`;if(near){body.querySelector('#buyOrb').onclick=()=>{G.inventory['Hush Orb']++;renderBag(body)};body.querySelector('#buySeed').onclick=()=>{if(G.inventory['Hush Orb']>=3){G.inventory['Hush Orb']-=3;G.inventory['Ember Seed']++;renderBag(body)}else toast('Three Hush Orbs are required.')}}}

// ── function: itemLore ──
function itemLore(k){return {'Hush Orb':'A quiet vessel for weakened wild echoes.','Prism Orb':'A clearer vessel with improved binding quality.','Crown Orb':'A near-perfect vessel tuned by four Anchors.','Mend Dew':'Restores a wounded voice.','Wake Salve':'Clears harsh conditions.','Ember Seed':'Catalyst for a mature echo to open its second form.','Moon Shard':'A cool transformation catalyst.','Root Key':'A field tool that loosens sealed paths.'}[k]||'A useful remnant.'}

// ── function: renderAtlas ──
function renderAtlas(body){body.innerHTML=`<p>${G.collection.length} / 27 names recorded · Objective: 8 unique names</p><div class="grid">${CREATURES.map((s,i)=>{const known=G.collection.includes(i);return `<div class="entry" style="opacity:${known?1:.35}"><span class="aff">${known?s.aff.join(' · '):'UNHEARD'}</span><h3>${known?s.name:'Unbound Pattern'}</h3><p>${known?s.lore:'Its voice has not yet entered your binding.'}</p>${known?`<div class="statline"><b>HP ${s.base.hp}</b><b>ATK ${s.base.atk}</b><b>DEF ${s.base.def}</b><b>SPD ${s.base.spd}</b></div>`:''}</div>`}).join('')}</div>`}

// ── function: renderMap ──
function renderMap(body){body.innerHTML=`<div class="grid">${ZONES.map((z,i)=>`<div class="entry" style="border-color:${z.grad[1]};opacity:${i<=G.anchors?1:.4}"><span class="aff">ZONE ${i+1}</span><h3>${z.name}</h3><p>Wild rate ${Math.round(z.rate*100)}% · ${z.guardian}</p><b>${i<G.anchors?'STEADIED':i===G.zone?'YOU ARE HERE':i<=G.anchors?'OPEN':'SEALED'}</b></div>`).join('')}</div><div class="entry"><h3>Binding Laws</h3><p>Cinder pierces Tideglass, Tideglass pierces Verdure, Verdure pierces Cinder. Volt pierces Cinder and Verdure. Gloom pierces Volt and Tideglass. Astral carries a slight edge over all ordinary patterns. Bastion resists every voice except Astral.</p></div>`}

// ── function: useBattleItem ──
function useBattleItem(){const a=activeAlly();if(G.inventory['Mend Dew']>0){G.inventory['Mend Dew']--;a.hp=Math.min(a.maxHp,a.hp+Math.floor(a.maxHp*.45));battleLog('Mend Dew restores the leading voice.');refreshBattle();battle.busy=true;setTimeout(enemyOnly,700)}else battleLog('No Mend Dew remains.')}

// ── function: toggleMute ──
function toggleMute(){G.options.mute=!G.options.mute;document.querySelectorAll('#muteBtn,#battleMute').forEach(b=>b.textContent=G.options.mute?'♩':'♫');toast(G.options.mute?'Sound folded away.':'Sound returns.')}

// ── function: handleKey ──
function handleKey(e){const k=e.key.toLowerCase();if(['arrowup','arrowdown','arrowleft','arrowright',' '].includes(k))e.preventDefault();if(mode==='world'){if(k==='w'||k==='arrowup')move(0,-1);if(k==='s'||k==='arrowdown')move(0,1);if(k==='a'||k==='arrowleft')move(-1,0);if(k==='d'||k==='arrowright')move(1,0);if(k==='enter'||k===' ')interact();if(k==='m'||k==='escape')openPanel('team')}else if(mode==='panel'&&k==='escape')closePanel();else if(mode==='dialog'&&(k==='enter'||k===' '))advanceDialog()}

// ── function: advanceDialog ──
function advanceDialog(){show('dialogue',false);mode='world';if(G.tutorial===1){G.tutorial=2;toast('Tutorial: move three tiles to draw a wild echo.')}requestAnimationFrame(loop)}

// ── main ──
document.getElementById('newBtn').onclick=beginNew;
document.getElementById('continueBtn').onclick=continueGame;
document.getElementById('continueBtn').disabled=!localStorage.getItem('veyra-binding');
document.getElementById('dialogNext').onclick=advanceDialog;
document.getElementById('panelClose').onclick=closePanel;
document.getElementById('saveBtn').onclick=()=>saveGame(true);
document.getElementById('muteBtn').onclick=toggleMute;
document.getElementById('battleMute').onclick=toggleMute;
document.getElementById('rhythmTap').onclick=tapBind;
document.addEventListener('keydown',handleKey);
document.querySelectorAll('#dock [data-panel]').forEach(b=>b.onclick=()=>openPanel(b.dataset.panel));
document.querySelectorAll('#battleActions [data-act]').forEach(b=>b.onclick=()=>{const a=b.dataset.act;if(a==='moves')showMoves();if(a==='bind')startBind();if(a==='switch')openPanel('switch');if(a==='bag')useBattleItem();if(a==='flee')flee()});
document.querySelectorAll('#controls [data-key]').forEach(b=>{const act=()=>{const k=b.dataset.key;if(k==='up')move(0,-1);if(k==='down')move(0,1);if(k==='left')move(-1,0);if(k==='right')move(1,0);if(k==='act')interact()};b.addEventListener('pointerdown',e=>{e.preventDefault();act()})});
window.addEventListener('resize',()=>{if(mode==='world'||mode==='battle')requestAnimationFrame(loop)});
ambient();
</script>
</body>
</html>
<!-- agent-meta {"model":"openai/gpt-5.6-sol","provider":"openrouter","persona":"composer","sessionId":"ea73aaaf-724c-4b9c-a301-ebae16b96388","tokensIn":519357,"tokensOut":18582,"tokensTotal":537939,"turns":25,"toolCalls":30,"failedToolCalls":1,"timestamp":"2026-07-14T04:01:52.950Z"} -->