"use strict";
const Q = new URLSearchParams(location.search);
const DEV = Q.has("dev"); // dev test mode (?dev): every skin unlocked to preview - never set for shipped users
const GORILLA_TEST = Q.has("gorilla") || Q.get("gauntlet")==="gorilla"; // ?gorilla -> drop straight into GORILLA WARFARE for testing
const STANDALONE = (window.SC_STANDALONE===true) || (location.port!=="4747" && !/^(127\.0\.0\.1|localhost)$/.test(location.hostname)); // hosted web build (injected flag, or any non-hub host) vs in-app OBS hub; the hub always serves on :4747 so treat that port as in-app even via LAN IP
const WEB_TOP = STANDALONE && window.top === window.self; // the full-page Maxxtopia/itch surface; extension and OBS embeds stay untouched
const WEB_RENDER_PIXELS = 800000; // cap only the backing store; CSS/gameplay scale stays resolution-independent
const AB = STANDALONE ? "imported/" : "/imported/"; // asset base: relative when hosted standalone, absolute under the app hub
const SKIN_VIS = 1.70; // in-game skin visual scale (hitbox/collision UNCHANGED) - Diggy-tuned so bought skins read big enough to enjoy while flying
// PWA: register the service worker ONLY on the hosted standalone web build (Add-to-Home-Screen + offline).
// Skipped on the in-app/OBS hub so the desktop overlay path is untouched. Failures are swallowed.
if (STANDALONE && "serviceWorker" in navigator) {
  window.addEventListener("load", function(){ navigator.serviceWorker.register("sw.js").catch(function(){}); });
}
// PWA install affordance (hosted web build, top-level page only - never in the OBS hub or an embed iframe).
// Android/Chromium: capture beforeinstallprompt -> a real "Add to Home Screen" button. iOS Safari has no such
// event, so we show a Share-sheet hint instead. Hidden once launched as an installed app.
if (STANDALONE && window.top === window.self) {
  var _scInstalled = function(){ try{ return matchMedia("(display-mode: standalone)").matches || matchMedia("(display-mode: fullscreen)").matches || navigator.standalone === true; }catch(e){ return false; } };
  var _scDefer = null, _scIsIOS = /iphone|ipad|ipod/i.test(navigator.userAgent);
  var _scEl = function(){ return document.getElementById("scInstall"); };
  var _scTxt = function(t){ var b=_scEl(); if(b){ var s=b.querySelector("#scInstallTxt"); if(s) s.textContent=t; } };
  window.addEventListener("beforeinstallprompt", function(e){ e.preventDefault(); _scDefer=e; var b=_scEl(); if(b && !_scInstalled()){ _scTxt("Add to Home Screen"); b.classList.add("show"); } });
  window.addEventListener("appinstalled", function(){ var b=_scEl(); if(b) b.classList.remove("show"); _scDefer=null; });
  var _scWire = function(){ var b=_scEl(); if(!b) return;
    if(!_scInstalled() && _scIsIOS && !_scDefer){ _scTxt("Install: tap Share, then 'Add to Home Screen'"); b.classList.add("show"); }
    b.onclick = function(){ if(_scDefer){ b.classList.remove("show"); _scDefer.prompt(); _scDefer=null; }
      else if(_scIsIOS){ _scTxt("Tap the Share icon, then 'Add to Home Screen'"); } }; };
  if(document.readyState!=="loading") _scWire(); else window.addEventListener("DOMContentLoaded", _scWire);
}
const cv = document.getElementById("game"); let ctx = cv.getContext("2d", WEB_TOP ? {alpha:false, desynchronized:true} : undefined); // ctx is `let` so drawScapes can briefly retarget it at an offscreen buffer during a world crossfade (restored synchronously)
// Resolution independence. CRISP = retina sharpness (canvas pixels per CSS pixel - backing-store only).
// DPR = the GAMEPLAY unit scale = CRISP * a viewport-fit factor, so the SAME play area is shown at any
// window size. Below the reference design box the world zooms OUT (small window => you see the same world,
// not "everything whips by and the kitty flies off the top"); at/above it the fit is capped to 1 so DPR
// == CRISP and fullscreen/OBS is byte-for-byte unchanged. Every gameplay constant is in *DPR units, so
// scaling this one value rescales the whole game (speeds, sizes, gravity, camera, world spawn).
let CRISP=Math.min(2, window.devicePixelRatio||1);
// Reference "design window" the world scales to fit. Desktop = wide 1600x900 cabinet framing. PHONES use a
// much smaller box so the world doesn't zoom way out (tiny sprites + everything crawling = "hella slow"); a
// smaller reference = bigger sprites + punchier scroll. (IS_TOUCH is defined later, so detect inline here.)
const _touchDev = ('ontouchstart' in window) || navigator.maxTouchPoints > 0;
const REF_W = _touchDev ? 520 : 1600, REF_H = _touchDev ? 400 : 900; // phones: punchy zoom (the follow-camera keeps the kitty on screen, so we no longer need to reserve the full launch arc vertically)
let W=0, H=0, DPR=CRISP;
function resize(){
  // Measure the canvas's ACTUAL on-screen box. #game is position:fixed inset:0, so CSS keeps it exactly
  // full-screen; reading clientWidth/Height (NOT window.innerWidth/Height, and NOT overriding the element's
  // CSS size) dodges the iOS standalone-PWA bugs where innerHeight is wrong/stale -> the element ends up
  // shorter than the screen = "black pit" at the bottom, and where rotation doesn't refresh innerW/H ->
  // half the screen black in landscape. The element now always fills the screen via CSS; we only size the
  // backing store + gameplay scale to match what's actually rendered.
  const cw = cv.clientWidth  || innerWidth;
  const ch = cv.clientHeight || innerHeight;
  const nativeDpr = Math.min(2, window.devicePixelRatio||1);
  const nativePixels = Math.max(1, cw*ch*nativeDpr*nativeDpr);
  CRISP = WEB_TOP ? nativeDpr*Math.min(1, Math.sqrt(WEB_RENDER_PIXELS/nativePixels)) : nativeDpr;
  W = cv.width  = Math.floor(cw*CRISP);
  H = cv.height = Math.floor(ch*CRISP);
  const fit = Math.min(1, cw/REF_W, ch/REF_H); // contain both axes; never upscale past design
  DPR = CRISP*fit;
  // DPR changed mid-run (e.g. phone rotation) -> gameplay coords are stored in DPR-scaled px, but GROUND()=H-70*DPR
  // moves; rescale every live entity by the ratio so the cat/obstacles stay glued to the ground (no float / unfair hit).
  try{ const _od=resize._dpr||DPR; resize._dpr=DPR; const s=DPR/_od;
    if(s!==1 && s>0 && isFinite(s) && (state==="fly"||state==="aim"||state==="abduct"||state==="redeploy"||state==="infurno"||state==="gauntlet")){
      if(typeof kitty!=="undefined" && kitty){ ["x","y","px","py","vx","vy","r"].forEach(k=>{ if(typeof kitty[k]==="number") kitty[k]*=s; }); }
      cam*=s; camY*=s; worldX*=s; if(waterStart>0){ waterStart*=s; waterEnd*=s; }
      for(const o of obstacles){ ["x","y","w","h","r"].forEach(k=>{ if(typeof o[k]==="number") o[k]*=s; }); if(o.bob){ o.bob.amp*=s; o.bob.base*=s; } }
      for(const p of parts){ p.x*=s; p.y*=s; p.vx*=s; p.vy*=s; if(typeof p.r==="number") p.r*=s; }
      for(const q of popTexts){ q.x*=s; q.y*=s; }
      for(const arr of [infCoins,infHaz,gHaz]){ if(arr) for(const h of arr){ ["x","y","vx","vy","r","w","h"].forEach(k=>{ if(typeof h[k]==="number") h[k]*=s; }); } }
      if(savedRun){ savedRun.x*=s; savedRun.y*=s; savedRun.vx*=s; savedRun.vy*=s; savedRun.cam*=s; }
    } }catch(e){}
}
addEventListener("resize", resize);
addEventListener("orientationchange", function(){ resize(); setTimeout(resize, 250); setTimeout(resize, 600); }); // iOS reports new dims a tick AFTER the rotation event
if (window.visualViewport) visualViewport.addEventListener("resize", resize);
addEventListener("load", resize);
resize();

let cfg = { accent:"#22d3a0", gravity:0.42, power:54, flapPower:9, flaps:3 };
function applyCfg(c){ if(c) cfg=Object.assign(cfg,c); document.documentElement.style.setProperty("--ac", cfg.accent||"#22d3a0"); }

const SAVE="sm.spritecannon";
// --- cloud-save state declared BEFORE the first persist() (pid-init below) so persist's sync hook isn't in the TDZ ---
let syncCode=""; try{ syncCode=(localStorage.getItem("sm.spritecannon.synccode")||"").trim(); }catch{}
let _adopting=false, _syncPushT=null, _syncBusy=false;
const SC_SYNC_URL="https://spritecannon-lb.maxxtopia.workers.dev";
let save = { coins:0, best:0, owned:["default"], skin:"default", runs:0, style:{}, daily:{streak:0,lastClaim:0}, quests:null, totalDist:0, coinsEarned:0, rev:0 };
try{ const s=JSON.parse(localStorage.getItem(SAVE)); if(s) save=Object.assign(save,s); }catch{}
// REGATE MIGRATION: Superbob now needs 65,000m (was 35k); Spookyszn now needs a posted score >= 25,000m (was any posted score). Revoke from anyone granted under the OLD bars who hasn't hit the new ones - checkUnlocks() auto-re-grants once they qualify. Idempotent + self-healing (safe to run every load).
function _regateSecrets(){ if(!Array.isArray(save.owned)) return; const drop=[]; if(save.owned.includes("superbob") && (save.best||0)<65000) drop.push("superbob"); if(save.owned.includes("spookyszn") && (save.best||0)<25000) drop.push("spookyszn"); if(!drop.length) return; save.owned=save.owned.filter(id=>drop.indexOf(id)<0); if(drop.indexOf(save.skin)>=0) save.skin="default"; if(Array.isArray(save.seenUnlocks)) save.seenUnlocks=save.seenUnlocks.filter(id=>drop.indexOf(id)<0); }
_regateSecrets();
function persist(){ if(!_adopting){ save.rev=(save.rev||0)+1; } try{ localStorage.setItem(SAVE, JSON.stringify(save)); }catch{} if(!_adopting && syncCode) cloudPush(); } // every change bumps rev + (if linked) debounced-pushes to the cloud
if(!save.pid){ save.pid="p_"+Math.random().toString(36).slice(2,10)+Date.now().toString(36); persist(); } // stable anon id for code redemption (per-player dedup)

// ===== CLOUD SAVE: optional cross-device sync via a player "sync code" (reuses the LB worker; rev-based last-write-wins) =====
function setSyncCode(c){ syncCode=String(c||"").replace(/[^\w\-]/g,"").slice(0,32).toUpperCase(); try{ localStorage.setItem("sm.spritecannon.synccode",syncCode); }catch{} }
function genSyncCode(){ const A="ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; let s=""; for(let i=0;i<8;i++) s+=A[Math.floor(Math.random()*A.length)]; return s; }
function _baseSave(){ return {coins:0,best:0,owned:["default"],skin:"default",runs:0,style:{},daily:{streak:0,lastClaim:0},quests:null,totalDist:0,coinsEarned:0,rev:0,revDay:0,revBought:0}; }
function _afterAdopt(){ try{ _regateSecrets(); }catch(e){} try{ updateCoinHUD(); }catch(e){} try{ refreshStartCard(); }catch(e){} try{ if(state==="shop") buildShop(); }catch(e){} }
function adoptCloudSave(cloud,rev){ if(!cloud||typeof cloud!=="object") return; const pid=save.pid; _adopting=true; save=Object.assign(_baseSave(),cloud); save.rev=rev||cloud.rev||0; if(!save.pid) save.pid=pid; persist(); _adopting=false; _afterAdopt(); }
function _mergeSaves(a,b){ const o=Object.assign({},a,b); o.coins=Math.max(a.coins||0,b.coins||0); o.best=Math.max(a.best||0,b.best||0); o.runs=Math.max(a.runs||0,b.runs||0); o.totalDist=Math.max(a.totalDist||0,b.totalDist||0); o.coinsEarned=Math.max(a.coinsEarned||0,b.coinsEarned||0); o.owned=Array.from(new Set([].concat(a.owned||["default"],b.owned||["default"]))); o.style=Object.assign({},a.style||{},b.style||{}); o.daily=((a.daily&&a.daily.lastClaim||0)>=(b.daily&&b.daily.lastClaim||0))?a.daily:b.daily;
  // daily REVIVE cap must survive a link/merge (anti-cheat): keep the newest day, and if both saves are on that day keep the HIGHER spent count so linking a device can't reset the 10/day ceiling
  { const rd=Math.max(a.revDay||0,b.revDay||0); o.revDay=rd; o.revBought=((a.revDay||0)===rd&&(b.revDay||0)===rd)?Math.max(a.revBought||0,b.revBought||0):(((a.revDay||0)===rd)?(a.revBought||0):(b.revBought||0)); }
  o.rev=Math.max(a.rev||0,b.rev||0)+1; return o; }
function cloudPull(){ if(!syncCode) return; fetch(SC_SYNC_URL+"/save?code="+encodeURIComponent(syncCode)).then(r=>r.json()).then(d=>{ if(d&&d.ok&&d.found&&d.save&&(d.rev||0)>(save.rev||0)) adoptCloudSave(d.save,d.rev); }).catch(()=>{}); } // load: newer cloud wins
function cloudPushNow(){ if(!syncCode||_syncBusy) return; _syncBusy=true; fetch(SC_SYNC_URL+"/save?code="+encodeURIComponent(syncCode),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({code:syncCode,rev:save.rev||0,save:save})}).then(r=>r.json()).then(d=>{ _syncBusy=false; if(d&&d.stale&&d.save) adoptCloudSave(d.save,d.rev); }).catch(()=>{ _syncBusy=false; }); }
function cloudPush(){ if(!syncCode) return; clearTimeout(_syncPushT); _syncPushT=setTimeout(cloudPushNow,4000); } // debounced -> many changes = one write (free-tier friendly)
function linkSyncCode(code,onDone){ code=String(code||"").replace(/[^\w\-]/g,"").slice(0,32).toUpperCase(); if(!code){ if(onDone) onDone(false,"Enter a code"); return; }
  fetch(SC_SYNC_URL+"/save?code="+encodeURIComponent(code)).then(r=>r.json()).then(d=>{ _adopting=true; if(d&&d.ok&&d.found&&d.save){ save=_mergeSaves(save,Object.assign({rev:d.rev||0},d.save)); } else { save.rev=(save.rev||0)+1; } setSyncCode(code); persist(); _adopting=false; _afterAdopt(); cloudPushNow(); if(onDone) onDone(true,code); }).catch(()=>{ if(onDone) onDone(false,"Offline - try again"); }); } // one-time generous MERGE so linking never loses either device's progress
setTimeout(cloudPull, 700); // pull on load if already linked
addEventListener("visibilitychange", function(){ if(document.hidden && syncCode){ clearTimeout(_syncPushT); cloudPushNow(); } }); // flush a pending push when the tab hides

// ---- sound (baked picks; original Web Audio synthesis, master volume via settings gear) ----
let audioCtx=null, masterGain=null, vol=50, lastVol=50;
try{ const sv=localStorage.getItem("sm.spritecannon.vol"); if(sv!=null) vol=Math.max(0,Math.min(100,Number(sv)||0)); }catch{}
function actx(){ if(!audioCtx){ try{ audioCtx=new (window.AudioContext||window.webkitAudioContext)(); }catch(e){ return null; }
    masterGain=audioCtx.createGain(); masterGain.gain.value=vol/100; masterGain.connect(audioCtx.destination); }
  if(audioCtx.state==="suspended") audioCtx.resume(); return audioCtx; }
// iOS/mobile keep Web Audio locked until a REAL user gesture. Resume the context + play a 1-sample
// silent buffer on the first tap/click/key (the standard unlock), then start music. Without this = no sound on phones.
(function(){ let unlocked=false;
  function unlock(){ if(unlocked) return; const c=actx(); if(!c) return;
    if(c.state==="suspended") c.resume();
    try{ const b=c.createBuffer(1,1,22050), s=c.createBufferSource(); s.buffer=b; s.connect(c.destination); s.start(0); }catch(e){}
    if(c.state!=="suspended"){ unlocked=true; ["pointerdown","touchend","click","keydown"].forEach(function(ev){ removeEventListener(ev,unlock); });
      try{ if(typeof musicStart==="function") musicStart(); }catch(e){} } }
  ["pointerdown","touchend","click","keydown"].forEach(function(ev){ addEventListener(ev,unlock,{passive:true}); }); })();
function pctx(ctx){ return new Proxy(ctx,{ get(tg,p){ if(p==="destination") return masterGain; const v=tg[p]; return typeof v==="function"? v.bind(tg): v; } }); }
function snd(name){ if(vol<=0) return; const c=actx(); if(!c||!SND[name]) return; try{ SND[name](pctx(c)); }catch(e){} }
function noiseBuf(ctx,dur,shape){ const len=Math.max(1,Math.floor(ctx.sampleRate*dur)); const b=ctx.createBuffer(1,len,ctx.sampleRate); const d=b.getChannelData(0); for(let i=0;i<len;i++){ const x=i/len; d[i]=(Math.random()*2-1)*(shape?shape(x):1); } return b; }
// sustained CHARGE engine (audition pick: "Reactor Pulse") - power-driven riser while holding to charge the cannon
let chargeEngine=null;
function makeCharge(ctx,out){
  const t=ctx.currentTime;
  const g=ctx.createGain(); g.gain.value=0.30; g.connect(out); // charge volume (lowered again per Diggy)
  const tone=ctx.createOscillator(); tone.type="sawtooth"; tone.frequency.value=120;
  const tf=ctx.createBiquadFilter(); tf.type="lowpass"; tf.frequency.value=600; tf.Q.value=4;
  const pulse=ctx.createGain(); pulse.gain.value=0.0001; tone.connect(tf); tf.connect(pulse); pulse.connect(g);
  const sub=ctx.createOscillator(); sub.type="sine"; sub.frequency.value=60; const subPulse=ctx.createGain(); subPulse.gain.value=0.0001; sub.connect(subPulse); subPulse.connect(g);
  const lfo=ctx.createOscillator(); lfo.type="sine"; lfo.frequency.value=2;
  const lfg=ctx.createGain(); lfg.gain.value=0.0001; lfo.connect(lfg); lfg.connect(pulse.gain); lfg.connect(subPulse.gain);
  tone.start(t);sub.start(t);lfo.start(t);
  return {
    set:function(p){ const tt=ctx.currentTime, depth=0.06+p*0.11;
      tone.frequency.setTargetAtTime(110+p*360,tt,0.05); tf.frequency.setTargetAtTime(400+p*3000,tt,0.05); sub.frequency.setTargetAtTime(50+p*70,tt,0.05);
      pulse.gain.setTargetAtTime(depth,tt,0.05); subPulse.gain.setTargetAtTime(depth*0.7,tt,0.05);
      lfg.gain.setTargetAtTime(depth,tt,0.05);
      lfo.frequency.setTargetAtTime(2+p*p*20,tt,0.06); }, // pulse rate accelerates with power
    stop:function(){ const tt=ctx.currentTime; pulse.gain.setTargetAtTime(0.0001,tt,0.03); subPulse.gain.setTargetAtTime(0.0001,tt,0.03); lfg.gain.setTargetAtTime(0.0001,tt,0.03);
      const e=tt+0.25; [tone,sub,lfo].forEach(o=>{try{o.stop(e);}catch(_){}}); }
  };
}
function startCharge(){ if(vol<=0) return; const c=actx(); if(!c) return; stopCharge(); try{ chargeEngine=makeCharge(c,masterGain); }catch(e){ chargeEngine=null; } }
function stopCharge(){ if(chargeEngine){ try{ chargeEngine.stop(); }catch(e){} chargeEngine=null; } }
// coin combo "chime ladder": pitch climbs a semitone per pickup chained within 1s (research: cheap dopamine)
function coinChime(semis){ if(vol<=0) return; const c=actx(); if(!c) return; const t=c.currentTime, mul=Math.pow(2,(semis||0)/12);
  const g=c.createGain(); g.gain.setValueAtTime(0.0001,t); g.gain.exponentialRampToValueAtTime(0.26,t+0.006); g.gain.exponentialRampToValueAtTime(0.0001,t+0.16); g.connect(masterGain);
  const o=c.createOscillator(); o.type="triangle"; o.frequency.setValueAtTime(700*mul,t); o.frequency.exponentialRampToValueAtTime(1500*mul,t+0.09); o.connect(g); o.start(t); o.stop(t+0.18); }
// user-supplied LOCAL clips (drop your OWN stream-safe files into imported/spritecannon/sfx/ as bark.mp3 / purr.mp3) - local only, never shipped
const customAudio={ bark:null, purr:null };
function loadCustom(name){ const exts=["mp3","ogg","wav","m4a"]; let i=0; (function tryNext(){ if(i>=exts.length) return; const a=new Audio(AB+"spritecannon/sfx/"+name+"."+exts[i]); a.preload="auto"; a.addEventListener("canplaythrough",()=>{ customAudio[name]=a; },{once:true}); a.addEventListener("error",()=>{ i++; tryNext(); },{once:true}); })(); }
let barkInst=null;
function stopBark(){ if(barkInst){ try{ barkInst.pause(); barkInst.currentTime=0; }catch(e){} barkInst=null; } }
function playCustom(name,v){ const a=customAudio[name]; if(!a||vol<=0) return false; try{ const c=a.cloneNode(); c.volume=Math.max(0,Math.min(1,(v==null?0.85:v)*(vol/100))); c.play().catch(()=>{}); if(name==="bark"){ stopBark(); barkInst=c; setTimeout(()=>{ if(barkInst===c){ try{c.pause();}catch(e){} barkInst=null; } },1100); } }catch(e){} return true; }
// ---- background music: up to 4 songs (drop into imported/spritecannon/music/ as song1..song4) mapped to escalating distance tiers, crossfaded ----
const MUS={ on:true, vol:0.1, songs:[null,null,null,null], cur:-1, started:false }; // default 10% (chill bg level); returning users keep their saved musicVol below
try{ MUS.on=localStorage.getItem("sm.spritecannon.musicOn")!=="0"; const mv=localStorage.getItem("sm.spritecannon.musicVol"); if(mv!=null) MUS.vol=Math.max(0,Math.min(1,Number(mv)/100)); }catch{}
function loadSongs(){ for(let i=0;i<4;i++){ (function(idx){ const exts=["mp3","ogg","m4a","wav"]; let e=0; (function tn(){ if(e>=exts.length) return; const a=new Audio(AB+"spritecannon/music/song"+(idx+1)+"."+exts[e]); a.loop=true; a.preload="auto"; a.volume=0; a.addEventListener("canplaythrough",()=>{ MUS.songs[idx]=a; },{once:true}); a.addEventListener("error",()=>{ e++; tn(); },{once:true}); })(); })(i); } }
function songsLoaded(){ return MUS.songs.some(s=>s); }
function musicTierFor(dist){ if(!songsLoaded()) return -1; let want=Math.max(0,Math.min(3,Math.floor(dist/650))); while(want>=0&&!MUS.songs[want]) want--; if(want<0){ for(let i=0;i<4;i++) if(MUS.songs[i]) return i; } return want; }
function fadeAudio(a,to,ms,thenPause){ if(!a) return; const from=a.volume,steps=Math.max(1,Math.floor(ms/40)); let s=0; const iv=setInterval(()=>{ s++; a.volume=Math.max(0,Math.min(1,from+(to-from)*(s/steps))); if(s>=steps){ clearInterval(iv); if(thenPause){ try{a.pause();}catch(e){} } } },40); }
function setSong(idx){ if(idx<0||idx===MUS.cur) return; const prev=MUS.songs[MUS.cur],next=MUS.songs[idx]; MUS.cur=idx; if(prev) fadeAudio(prev,0,1400,true); if(next&&MUS.on&&MUS.vol>0){ try{ next.volume=0; next.play().catch(()=>{}); }catch(e){} fadeAudio(next,MUS.vol,1400,false); } }
function musicStart(){ if(MUS.started||!MUS.on||!songsLoaded()) return; MUS.started=true; MUS.cur=-1; setSong(musicTierFor(distM)); }
function musicStop(){ MUS.songs.forEach(a=>{ if(a){ try{ a.pause(); a.currentTime=0; a.volume=0; }catch(e){} } }); MUS.cur=-1; MUS.started=false; }
function musicUpdate(){ if(!MUS.on||!songsLoaded()) return; if(!MUS.started){ MUS.started=true; MUS.cur=-1; } const tier=musicTierFor(distM); if(tier>=0&&tier!==MUS.cur) setSong(tier); }
function setMusicVol(v01){ MUS.vol=Math.max(0,Math.min(1,v01)); try{ localStorage.setItem("sm.spritecannon.musicVol",Math.round(MUS.vol*100)); }catch{} const a=MUS.songs[MUS.cur]; if(a&&MUS.on) a.volume=MUS.vol; }
function setMusicOn(on){ MUS.on=on; try{ localStorage.setItem("sm.spritecannon.musicOn",on?"1":"0"); }catch{} if(!on) musicStop(); else { MUS.started=false; musicStart(); } }
function setVol(v){ vol=Math.max(0,Math.min(100, v|0)); try{ localStorage.setItem("sm.spritecannon.vol", vol); }catch{}
  if(masterGain) masterGain.gain.value=vol/100;
  const s=document.getElementById("volSlider"), lab=document.getElementById("volVal"), ic=document.getElementById("volIcon");
  if(s) s.value=vol; if(lab) lab.textContent=vol+"%"; if(ic) ic.innerHTML = vol<=0? "&#128263;" : (vol<50? "&#128265;" : "&#128266;"); }
function toggleMute(){ if(vol>0){ lastVol=vol; setVol(0); } else { setVol(lastVol||70); } }
const SND = {
  fire: function(ctx){ const t=ctx.currentTime, out=ctx.destination; // audition pick: "Artillery Boom"
    const ns=ctx.createBufferSource(); ns.buffer=noiseBuf(ctx,0.25,x=>Math.pow(1-x,1.4));
    const lp=ctx.createBiquadFilter(); lp.type="lowpass"; lp.frequency.setValueAtTime(3200,t); lp.frequency.exponentialRampToValueAtTime(450,t+0.22);
    const ng=ctx.createGain(); ng.gain.setValueAtTime(0.3,t); ng.gain.exponentialRampToValueAtTime(0.0001,t+0.24);
    ns.connect(lp).connect(ng).connect(out); ns.start(t); ns.stop(t+0.25);
    const o=ctx.createOscillator(); o.type="sine"; o.frequency.setValueAtTime(120,t); o.frequency.exponentialRampToValueAtTime(40,t+0.3);
    const og=ctx.createGain(); og.gain.setValueAtTime(0.0001,t); og.gain.linearRampToValueAtTime(0.3,t+0.01); og.gain.exponentialRampToValueAtTime(0.0001,t+0.4);
    o.connect(og).connect(out); o.start(t); o.stop(t+0.42);
    const rs=ctx.createBufferSource(); rs.buffer=noiseBuf(ctx,0.7);
    const rlp=ctx.createBiquadFilter(); rlp.type="lowpass"; rlp.frequency.value=220; rlp.Q.value=0.6;
    const rg=ctx.createGain(); rg.gain.setValueAtTime(0.0001,t+0.04); rg.gain.linearRampToValueAtTime(0.16,t+0.1); rg.gain.exponentialRampToValueAtTime(0.0001,t+0.7);
    rs.connect(rlp).connect(rg).connect(out); rs.start(t+0.04); rs.stop(t+0.74); },
  flap: function(ctx){ const t=ctx.currentTime,dur=0.16; const len=Math.floor(ctx.sampleRate*dur),buf=ctx.createBuffer(1,len,ctx.sampleRate),d=buf.getChannelData(0);for(let i=0;i<len;i++)d[i]=(Math.random()*2-1)*(i/len);const ns=ctx.createBufferSource();ns.buffer=buf;const bp=ctx.createBiquadFilter();bp.type="bandpass";bp.frequency.setValueAtTime(900,t);bp.frequency.exponentialRampToValueAtTime(3000,t+dur);bp.Q.value=2.0;const ng=ctx.createGain();ng.gain.setValueAtTime(0.0001,t);ng.gain.linearRampToValueAtTime(0.2,t+0.06);ng.gain.exponentialRampToValueAtTime(0.0001,t+dur);ns.connect(bp).connect(ng).connect(ctx.destination); ns.start(t);ns.stop(t+dur); },
  pad: function(ctx){ const t=ctx.currentTime; const body=ctx.createOscillator(),bg=ctx.createGain();body.type="sine";body.frequency.setValueAtTime(150,t);body.frequency.exponentialRampToValueAtTime(55,t+0.13);bg.gain.setValueAtTime(0.0001,t);bg.gain.linearRampToValueAtTime(0.28,t+0.008);bg.gain.exponentialRampToValueAtTime(0.0001,t+0.18);body.connect(bg);bg.connect(ctx.destination);body.start(t);body.stop(t+0.2); const boi=ctx.createOscillator(),bog=ctx.createGain();boi.type="triangle";boi.frequency.setValueAtTime(220,t+0.04);boi.frequency.exponentialRampToValueAtTime(560,t+0.11);boi.frequency.exponentialRampToValueAtTime(340,t+0.26);bog.gain.setValueAtTime(0.0001,t+0.04);bog.gain.linearRampToValueAtTime(0.18,t+0.06);bog.gain.exponentialRampToValueAtTime(0.0001,t+0.34);boi.connect(bog);bog.connect(ctx.destination);boi.start(t+0.04);boi.stop(t+0.36); },
  spring: function(ctx){ const t=ctx.currentTime; const nb=ctx.createBuffer(1,Math.floor(ctx.sampleRate*0.05),ctx.sampleRate); const nd=nb.getChannelData(0); for(let i=0;i<nd.length;i++){nd[i]=(Math.random()*2-1)*Math.pow(1-i/nd.length,2);} const ns=ctx.createBufferSource(); ns.buffer=nb; const nbp=ctx.createBiquadFilter(); nbp.type="bandpass"; nbp.frequency.value=3200; nbp.Q.value=6; const ng=ctx.createGain(); ng.gain.setValueAtTime(0.28,t); ng.gain.exponentialRampToValueAtTime(0.0001,t+0.05); ns.connect(nbp); nbp.connect(ng); ng.connect(ctx.destination); ns.start(t); ns.stop(t+0.05); const o=ctx.createOscillator(); o.type="square"; o.frequency.setValueAtTime(900,t); o.frequency.exponentialRampToValueAtTime(420,t+0.02); const og=ctx.createGain(); og.gain.setValueAtTime(0.0001,t); og.gain.exponentialRampToValueAtTime(0.18,t+0.005); og.gain.exponentialRampToValueAtTime(0.0001,t+0.08); o.connect(og); og.connect(ctx.destination); o.start(t); o.stop(t+0.09); const w=ctx.createOscillator(); w.type="sine"; w.frequency.setValueAtTime(300,t+0.04); w.frequency.exponentialRampToValueAtTime(1700,t+0.30); const wg=ctx.createGain(); wg.gain.setValueAtTime(0.0001,t+0.04); wg.gain.exponentialRampToValueAtTime(0.22,t+0.10); wg.gain.exponentialRampToValueAtTime(0.0001,t+0.32); w.connect(wg); wg.connect(ctx.destination); w.start(t+0.04); w.stop(t+0.33); },
  balloon: function(ctx){ const t=ctx.currentTime; const nb=ctx.createBuffer(1,Math.floor(ctx.sampleRate*0.025),ctx.sampleRate); const nd=nb.getChannelData(0); for(let i=0;i<nd.length;i++){nd[i]=(Math.random()*2-1)*Math.pow(1-i/nd.length,2);} const ns=ctx.createBufferSource(); ns.buffer=nb; const bp=ctx.createBiquadFilter(); bp.type="bandpass"; bp.frequency.value=1500; bp.Q.value=0.7; const ng=ctx.createGain(); ng.gain.setValueAtTime(0.3,t); ng.gain.exponentialRampToValueAtTime(0.0001,t+0.035); ns.connect(bp); bp.connect(ng); ng.connect(ctx.destination); ns.start(t); ns.stop(t+0.04); const o=ctx.createOscillator(); o.type="sine"; o.frequency.setValueAtTime(320,t); o.frequency.exponentialRampToValueAtTime(120,t+0.07); const og=ctx.createGain(); og.gain.setValueAtTime(0.0001,t); og.gain.exponentialRampToValueAtTime(0.26,t+0.006); og.gain.exponentialRampToValueAtTime(0.0001,t+0.1); o.connect(og); og.connect(ctx.destination); o.start(t); o.stop(t+0.11); [880,1320,1980].forEach(function(f,i){ const d=0.06+i*0.05; const s=ctx.createOscillator(); s.type="triangle"; s.frequency.setValueAtTime(f*0.85,t+d); s.frequency.exponentialRampToValueAtTime(f,t+d+0.05); const sg=ctx.createGain(); sg.gain.setValueAtTime(0.0001,t+d); sg.gain.exponentialRampToValueAtTime(0.16,t+d+0.012); sg.gain.exponentialRampToValueAtTime(0.0001,t+d+0.1); s.connect(sg); sg.connect(ctx.destination); s.start(t+d); s.stop(t+d+0.11); }); },
  coin: function(ctx){ const t=ctx.currentTime; const g=ctx.createGain();g.gain.setValueAtTime(0.0001,t);g.gain.exponentialRampToValueAtTime(0.29,t+0.006);g.gain.exponentialRampToValueAtTime(0.0001,t+0.16);g.connect(ctx.destination);const o=ctx.createOscillator();o.type="triangle";o.frequency.setValueAtTime(700,t);o.frequency.exponentialRampToValueAtTime(1500,t+0.09);o.connect(g);o.start(t);o.stop(t+0.18); },
  token: function(ctx){ const t=ctx.currentTime; [784,988,1319,1760].forEach(function(f,i){const dt=t+i*0.06;const o=ctx.createOscillator();o.type="triangle";o.frequency.setValueAtTime(f,dt);const g=ctx.createGain();g.gain.setValueAtTime(0.0001,dt);g.gain.exponentialRampToValueAtTime(0.24,dt+0.006);g.gain.exponentialRampToValueAtTime(0.0001,dt+0.18);o.connect(g);g.connect(ctx.destination);o.start(dt);o.stop(dt+0.2);}); },
  reboot: function(ctx){ const t=ctx.currentTime; const n1=ctx.createOscillator(), g1=ctx.createGain(); n1.type="square"; n1.frequency.setValueAtTime(784,t); g1.gain.setValueAtTime(0.0001,t); g1.gain.exponentialRampToValueAtTime(0.22,t+0.01); g1.gain.exponentialRampToValueAtTime(0.0001,t+0.12); n1.connect(g1); g1.connect(ctx.destination); n1.start(t); n1.stop(t+0.13); const n2=ctx.createOscillator(), g2=ctx.createGain(); n2.type="square"; n2.frequency.setValueAtTime(1319,t+0.10); g2.gain.setValueAtTime(0.0001,t+0.10); g2.gain.exponentialRampToValueAtTime(0.22,t+0.115); g2.gain.exponentialRampToValueAtTime(0.0001,t+0.24); n2.connect(g2); g2.connect(ctx.destination); n2.start(t+0.10); n2.stop(t+0.25); const p=ctx.createOscillator(), pg=ctx.createGain(); p.type="sine"; p.frequency.setValueAtTime(220,t+0.24); p.frequency.exponentialRampToValueAtTime(880,t+0.40); pg.gain.setValueAtTime(0.0001,t+0.24); pg.gain.exponentialRampToValueAtTime(0.3,t+0.28); pg.gain.exponentialRampToValueAtTime(0.0001,t+0.46); p.connect(pg); pg.connect(ctx.destination); p.start(t+0.24); p.stop(t+0.47); const len=Math.floor(ctx.sampleRate*0.08), buf=ctx.createBuffer(1,len,ctx.sampleRate), d=buf.getChannelData(0); for(let i=0;i<len;i++){ d[i]=(Math.random()*2-1)*(1-i/len); } const ns=ctx.createBufferSource(); ns.buffer=buf; const nf=ctx.createBiquadFilter(); nf.type="bandpass"; nf.frequency.value=1200; const ng=ctx.createGain(); ng.gain.setValueAtTime(0.18,t+0.26); ng.gain.exponentialRampToValueAtTime(0.0001,t+0.40); ns.connect(nf); nf.connect(ng); ng.connect(ctx.destination); ns.start(t+0.26); ns.stop(t+0.40); },
  spike: function(ctx){ const t=ctx.currentTime; function womp(start,f1,f2,peak){ const o=ctx.createOscillator(); const g=ctx.createGain(); o.type="sawtooth"; const lp=ctx.createBiquadFilter(); lp.type="lowpass"; lp.frequency.setValueAtTime(1200,start); o.frequency.setValueAtTime(f1,start); o.frequency.exponentialRampToValueAtTime(f2,start+0.16); g.gain.setValueAtTime(0.0001,start); g.gain.exponentialRampToValueAtTime(peak,start+0.03); g.gain.exponentialRampToValueAtTime(0.0001,start+0.20); o.connect(lp); lp.connect(g); g.connect(ctx.destination); o.start(start); o.stop(start+0.21); } womp(t,330,247,0.20); womp(t+0.20,262,196,0.20); const o=ctx.createOscillator(); const g=ctx.createGain(); o.type="triangle"; o.frequency.setValueAtTime(196,t+0.40); g.gain.setValueAtTime(0.0001,t+0.40); g.gain.exponentialRampToValueAtTime(0.18,t+0.43); g.gain.exponentialRampToValueAtTime(0.0001,t+0.66); o.connect(g); g.connect(ctx.destination); o.start(t+0.40); o.stop(t+0.66); },
  newbest: function(ctx){ const t=ctx.currentTime; const notes=[523.25,659.25,783.99,1046.5],starts=[0,0.1,0.2,0.34];for(let n=0;n<notes.length;n++){const st=t+starts[n],accent=n===notes.length-1;const osc=ctx.createOscillator(),g=ctx.createGain();osc.type="triangle";osc.frequency.setValueAtTime(notes[n],st);const peak=accent?0.3:0.2,dur=accent?0.4:0.16;g.gain.setValueAtTime(0.0001,st);g.gain.linearRampToValueAtTime(peak,st+0.015);g.gain.exponentialRampToValueAtTime(0.0001,st+dur);osc.connect(g);g.connect(ctx.destination);osc.start(st);osc.stop(st+dur+0.02);} },
  catnip: function(ctx){ const t=ctx.currentTime; // DAILY CATNIP claim (audition pick #6 "Glass Shimmer"): glassy upward glissando + bell overtones
    function tn(type,f0,f1,t0,dur,peak){ const o=ctx.createOscillator(),g=ctx.createGain(); o.type=type; o.frequency.setValueAtTime(f0,t0); if(f1&&f1!==f0) o.frequency.exponentialRampToValueAtTime(f1,t0+dur*0.9); g.gain.setValueAtTime(0.0001,t0); g.gain.exponentialRampToValueAtTime(peak,t0+0.01); g.gain.exponentialRampToValueAtTime(0.0001,t0+dur); o.connect(g); g.connect(ctx.destination); o.start(t0); o.stop(t0+dur+0.02); }
    tn("sine",440,1760,t,0.5,0.16); tn("sine",880,3520,t+0.04,0.46,0.10); [1318.5,1760,2217].forEach(function(f,i){ tn("sine",f,f,t+0.3+i*0.04,0.3,0.10); }); },
  buy: function(ctx){ const t=ctx.currentTime; // SHOP BUY/unlock (audition pick #7 "Soft Confirm Chime"): a gentle two-note positive ding
    function tn(f,t0,dur,peak){ const o=ctx.createOscillator(),g=ctx.createGain(); o.type="sine"; o.frequency.setValueAtTime(f,t0); g.gain.setValueAtTime(0.0001,t0); g.gain.exponentialRampToValueAtTime(peak,t0+0.01); g.gain.exponentialRampToValueAtTime(0.0001,t0+dur); o.connect(g); g.connect(ctx.destination); o.start(t0); o.stop(t0+dur+0.02); }
    tn(783.99,t,0.22,0.22); tn(1174.7,t+0.12,0.4,0.22); },
  ui: function(ctx){ const t=ctx.currentTime, out=ctx.destination; // audition pick: "Soft Thup" - warm muted tap
    const o=ctx.createOscillator(),g=ctx.createGain(),lp=ctx.createBiquadFilter();
    lp.type="lowpass"; lp.frequency.value=900;
    o.type="sine"; o.frequency.setValueAtTime(230,t); o.frequency.exponentialRampToValueAtTime(150,t+0.07);
    g.gain.setValueAtTime(0.0001,t); g.gain.linearRampToValueAtTime(0.34,t+0.008); g.gain.exponentialRampToValueAtTime(0.0001,t+0.11);
    o.connect(g).connect(lp).connect(out); o.start(t); o.stop(t+0.12); },
  axe: function(ctx){ const t=ctx.currentTime,out=ctx.destination; // BACCA axe throw: spinning blade whir + swish (was the death sound)
    for(let i=0;i<6;i++){ const st=t+i*0.05; const ns=ctx.createBufferSource(); ns.buffer=noiseBuf(ctx,0.045,x=>1-x); const bp=ctx.createBiquadFilter(); bp.type="bandpass"; bp.Q.value=6; bp.frequency.value=2600-i*180; const g=ctx.createGain(); const pk=0.14*(1-i*0.09); g.gain.setValueAtTime(0.0001,st); g.gain.linearRampToValueAtTime(pk,st+0.008); g.gain.exponentialRampToValueAtTime(0.0001,st+0.045); ns.connect(bp).connect(g).connect(out); ns.start(st); ns.stop(st+0.05); }
    const sw=ctx.createBufferSource(); sw.buffer=noiseBuf(ctx,0.3,x=>Math.sin(Math.PI*x)); const sbp=ctx.createBiquadFilter(); sbp.type="bandpass"; sbp.frequency.setValueAtTime(900,t); sbp.frequency.exponentialRampToValueAtTime(400,t+0.28); sbp.Q.value=0.8; const sg=ctx.createGain(); sg.gain.setValueAtTime(0.0001,t); sg.gain.linearRampToValueAtTime(0.14,t+0.08); sg.gain.exponentialRampToValueAtTime(0.0001,t+0.3); sw.connect(sbp).connect(sg).connect(out); sw.start(t); sw.stop(t+0.31); },
  screech: function(ctx){ const t=ctx.currentTime,out=ctx.destination; // SONIC SCREAM ability: rising sonic ring (was the death sound)
    const o=ctx.createOscillator(),g=ctx.createGain(); o.type="sine"; o.frequency.setValueAtTime(300,t); o.frequency.exponentialRampToValueAtTime(2400,t+0.3); g.gain.setValueAtTime(0.0001,t); g.gain.exponentialRampToValueAtTime(0.26,t+0.03); g.gain.exponentialRampToValueAtTime(0.0001,t+0.4); o.connect(g).connect(out); o.start(t); o.stop(t+0.41);
    const p=ctx.createOscillator(),pg=ctx.createGain(); p.type="triangle"; p.frequency.setValueAtTime(1760,t+0.24); pg.gain.setValueAtTime(0.0001,t+0.24); pg.gain.exponentialRampToValueAtTime(0.18,t+0.26); pg.gain.exponentialRampToValueAtTime(0.0001,t+0.5); p.connect(pg).connect(out); p.start(t+0.24); p.stop(t+0.51); },
  throw: function(ctx){ const t=ctx.currentTime,out=ctx.destination; // GORILLA hurls a boulder: heavy throw whoosh (was the death sound)
    const ns=ctx.createBufferSource(); ns.buffer=noiseBuf(ctx,0.34,x=>Math.sin(Math.PI*x)); const lp=ctx.createBiquadFilter(); lp.type="lowpass"; lp.frequency.setValueAtTime(1400,t); lp.frequency.exponentialRampToValueAtTime(300,t+0.32); const ng=ctx.createGain(); ng.gain.setValueAtTime(0.0001,t); ng.gain.linearRampToValueAtTime(0.3,t+0.09); ng.gain.exponentialRampToValueAtTime(0.0001,t+0.34); ns.connect(lp).connect(ng).connect(out); ns.start(t); ns.stop(t+0.35);
    const o=ctx.createOscillator(),g=ctx.createGain(); o.type="sine"; o.frequency.setValueAtTime(150,t); o.frequency.exponentialRampToValueAtTime(60,t+0.3); g.gain.setValueAtTime(0.0001,t); g.gain.linearRampToValueAtTime(0.2,t+0.04); g.gain.exponentialRampToValueAtTime(0.0001,t+0.34); o.connect(g).connect(out); o.start(t); o.stop(t+0.35); },
  error: function(ctx){ const t=ctx.currentTime,out=ctx.destination; // redeem-code fail: soft error buzz (was the death sound)
    function blip(s,f1,f2){ const o=ctx.createOscillator(),g=ctx.createGain(),lp=ctx.createBiquadFilter(); lp.type="lowpass"; lp.frequency.value=1100; o.type="square"; o.frequency.setValueAtTime(f1,s); o.frequency.exponentialRampToValueAtTime(f2,s+0.1); g.gain.setValueAtTime(0.0001,s); g.gain.linearRampToValueAtTime(0.18,s+0.01); g.gain.exponentialRampToValueAtTime(0.0001,s+0.13); o.connect(lp).connect(g).connect(out); o.start(s); o.stop(s+0.14); } blip(t,300,220); blip(t+0.14,220,165); }
};

let skinImgs={}, skinBox={}; // skinBox[id] = the creature's alpha bounding box in image px (so padding never makes a skin render small)
function _computeSkinBox(id,img){ try{
  const c=document.createElement("canvas"); c.width=img.width; c.height=img.height; const g=c.getContext("2d"); g.drawImage(img,0,0);
  const d=g.getImageData(0,0,img.width,img.height).data; let x0=img.width,y0=img.height,x1=0,y1=0,found=false;
  for(let y=0;y<img.height;y++){ const row=y*img.width; for(let x=0;x<img.width;x++){ if(d[(row+x)*4+3]>16){ found=true; if(x<x0)x0=x; if(x>x1)x1=x; if(y<y0)y0=y; if(y>y1)y1=y; } } }
  if(found) skinBox[id]={x:x0,y:y0,w:x1-x0+1,h:y1-y0+1};
}catch(e){} }
let skinList=[{id:"default",name:"Kitty",cost:0,img:null}];
// ability skins (cost more): a skin gets an ability + custom cost/name via imported/spritecannon/skins/abilities.json
const ABILITY_LABELS={ ninelives:"start +1 life", doubleflap:"+2 flaps", featherfall:"floaty fall", goldcat:"2x coins", rocketstart:"big launch", shield:"1-hit shield", magnet:"coin magnet", bouncy:"super bounce", greedy:"5x coins", revive:"up to 4 lives", turtle:"regenerating shell", magnetburst:"10s coin magnet", flamejet:"flame dash", windboost:"wind launch", chrome:"chrome plating (renews + deflects)", dragon:"draft relaunch (10s) + flap regen", lightning:"thunder dash", fallout:"NUKE (clears the screen)", teleport:"blink dash (phase-through, x3)", split:"Flying Raijin (warp any way)", crashout:"NITRO smash (recharges 15s)", spicyspeed:"SPICY SPEED", buildwall:"BUILD THE WALL", slowmo:"SLOWED & THROWED", screech:"SONIC SCREAM", GodSpeed:"GOD SPEED", godmode:"MAN OF STEEL", bartman:"hold to skate/fly + glider", waterimmune:"WATER WALKER", thedrop:"THE DROP", phantom:"ghost rush (5s speed+phase)", rage:"Spartan Rage", candy:"eat candy -> random power" };
const ABILITY_DESC={
  greedy:"Earns 5x coins from everything you collect.",
  revive:"Respawn on the spot up to 4 times before it's game over.",
  turtle:"A shell shield absorbs a crash without ending your run - then grows back every 10 seconds.",
  magnetburst:"Pulls in every nearby coin for a 10-second burst.",
  flamejet:"Ignites a flame dash that launches you up and forward, over hazards.",
  windboost:"Catches a gust for an extra-long launch off the cannon.",
  chrome:"Chrome plating deflects hazard hits into a forward shockwave - and a spent plate grows back every 14s (up to 3).",
  dragon:"Tap your ability key for a Dragon Draft relaunch (recharges every 6-7 seconds), and regrow a wing-flap every 3 seconds.",
  crashout:"NITRO! Smash straight through everything for a few seconds. One charge, recharges every 15s.",
  lightning:"Tap your ability key for a THUNDER DASH - a huge piercing forward burst. 5s cooldown.",
  fallout:"Drop the NUKE: wipe every hazard on screen into a coin blast, launch off the mushroom cloud, and go radioactive-invincible for a moment. Recharges every 12s.",
  teleport:"Blink dash forward through danger with a leap, phasing through hazards for a moment. Three charges, one refills every 5s.",
  split:"Flying Raijin (the Yellow Flash): warp a huge distance in ANY direction you hold - up, down, forward or diagonal - flashing through hazards. 2.5s cooldown.",
  screech:"SONIC SCREAM: a shockwave that destroys every hazard on the whole screen and rockets you forward. 8s cooldown.",
  ninelives:"Start every run with one extra life in reserve.",
  doubleflap:"Adds 2 extra flaps for more air time and control.",
  featherfall:"Fall slow and floaty, softening every descent.",
  goldcat:"Doubles the coins you earn from everything.",
  rocketstart:"Launches you out of the cannon with extra power.",
  shield:"A shield blocks the first hit that would end your run.",
  magnet:"Continuously draws nearby coins toward you.",
  bouncy:"Bounce higher off every surface for big rebounds.",
  phantom:"GHOST RUSH: turn intangible AND blaze at crazy speed, phasing through every hazard for 5 seconds. 12s cooldown.",
  rage:"Unleash Spartan Rage - a sweeping smash clears nearby hazards and launches you up and forward. 13s cooldown.",
  candy:"Crazy Slots (Kite): eat a mystery candy to SPIN the slot - it locks on a random numbered power (speed, ghost, glide, star, super-flight, gold, or the rare REAPER jackpot)."
};
function ab(){ const s=skinList.find(x=>x.id===save.skin); return (s&&s.ability)||null; }
function baseFlaps(){ const a=ab(); return cfg.flaps + (a==="doubleflap"?2:(a==="bartman"?1:0)); } // bartman: +1 bonus flap
function grav(){ const a=ab(); const lg=biomeAt(distM).lowGrav||1; return cfg.gravity*lg*(glideT>0?0.4:(a==="featherfall"||a==="bartman"?0.62:1)); } // lg = Zero-G world low-gravity multiplier; bartman: permanent floaty slow-fall
function skinImgFor(id){ const v=save.style&&save.style[id]; return (v&&skinImgs[v])?skinImgs[v]:skinImgs[id]; } // chosen style variant -> its image (fallback to base)
function skinBoxFor(id){ const v=save.style&&save.style[id]; return (v&&skinBox[v])?skinBox[v]:skinBox[id]; }
function coinAmt(n){ let m = ab()==="greedy"?5 : ab()==="goldcat"?2 : 1; if(candyGoldT>0) m*=3; return n*m; } // Spookyszn GOLD RUSH candy = 3x coins while active
function pickR(){ return kitty.r + (ab()==="magnet"?55*DPR:0); }
function segNear(ox,oy,reach){ const ax=kitty.px,ay=kitty.py,bx=kitty.x,by=kitty.y, dx=bx-ax,dy=by-ay, L2=dx*dx+dy*dy; let tt=L2>0?((ox-ax)*dx+(oy-ay)*dy)/L2:0; tt=tt<0?0:tt>1?1:tt; const cx=ax+dx*tt, cy=ay+dy*tt; return Math.hypot(ox-cx,oy-cy)<reach; } // swept: nearest point on the move-segment within reach -> never tunnel through a pickup
let waterStart=-1, waterEnd=-1, waterCd=0; // lethal water stretch (floats + diving boards only; touching water costs a life)
let slideT=0, slideGrace=0; // ridable park slide: while >0 you ride a boost ramp (forward accel + low glide), eject up at the end. slideGrace = brief hazard-immunity (ride + launch arc) so the slide can't fling you to a death you didn't steer into
function slideFx(x,y){ for(let i=0,n=Math.round(10*JF());i<n;i++) parts.push({x:x+rand(-10,10)*DPR,y:y+rand(-6,6)*DPR,vx:rand(-6,-1)*DPR,vy:rand(-2,2)*DPR,life:0.7,c:i%2?"#ffd36b":"#9be3ff",r:rand(2,4)*DPR}); }
function inWaterX(x){ return waterStart>0 && x>=waterStart && x<=waterEnd; }
function inWater(){ return inWaterX(kitty.x); }
function nearFloat(){ for(const o of obstacles){ if((o.type==="float"||o.type==="diveboard") && o.w && Math.max(kitty.px,kitty.x)>o.x-o.w/2 && Math.min(kitty.px,kitty.x)<o.x+o.w/2) return true; } return false; } // standing over a water platform -> safe landing, not a splash
function splashFx(x,y){ for(let i=0;i<22;i++) parts.push({x:x+rand(-14,14)*DPR,y,vx:rand(-5,5)*DPR,vy:rand(-9,-2)*DPR,life:1,c:i%2?"#bfeaff":"#7fd0ff",r:rand(2,5)*DPR}); }
function maxLives(){ return ab()==="revive"?4:3; }
// ---- ACTIVE abilities: fired via Shift key / on-screen ability button (Diggy's pick) ----
const ACTIVE_AB={ rocketstart:{label:"AXE",uses:Infinity,cd:10,dur:0}, magnetburst:{label:"MAGNET",uses:Infinity,cd:25,dur:10}, flamejet:{label:"FLAME",uses:Infinity,cd:1.5,dur:0}, windboost:{label:"WIND",uses:Infinity,cd:5,dur:0.9}, teleport:{label:"BLINK",uses:3,cd:0.3,dur:0}, split:{label:"RAIJIN",uses:Infinity,cd:2.5,dur:0}, fallout:{label:"NUKE",uses:Infinity,cd:12,dur:0}, crashout:{label:"NITRO",uses:Infinity,cd:15,dur:2.5}, dragon:{label:"DRAFT",uses:Infinity,cd:10,dur:0}, lightning:{label:"DASH",uses:Infinity,cd:5,dur:0},
  spicyspeed:{label:"SPICY",uses:5,cd:1.2,dur:0}, buildwall:{label:"WALL",uses:Infinity,cd:8,dur:0}, slowmo:{label:"SLOW",uses:Infinity,cd:18,dur:3.5}, screech:{label:"SCREAM",uses:Infinity,cd:8,dur:0}, thedrop:{label:"DROP",uses:Infinity,cd:8,dur:0}, GodSpeed:{label:"SUPER",uses:Infinity,cd:25,dur:4}, phantom:{label:"WRAITH",uses:Infinity,cd:12,dur:5}, rage:{label:"RAGE",uses:Infinity,cd:13,dur:0}, candy:{label:"CANDY",uses:Infinity,cd:9,dur:0} };
let abUses=0, abCd=0, magnetT=0, glideT=0, abRegen=0, flameTrailT=0, crashT=0, turtleRegen=0, chromeRegen=0; // abRegen: teleport regen; flameTrailT: inferno trail; crashT: nitro smash window; turtleRegen/chromeRegen: renewable-armor timers
let spicyT=0, slowT=0, phaseT=0, superT=0, superCoast=0, starT=0, flightMeter=1, abHeld=false, wallFx=null, shockwaves=[], portalFx=[], candyGoldT=0, candySpawnT=0, skateT=0; // spicy dash / slow-mo / GodSpeed super (+coast) / godmode metered flight / buildwall sweep + screech rings / portal warp swirls / candyGoldT = Spookyszn gold-rush timer / skateT = Bart skateboard dash
let abilityKey="ShiftLeft", capturingKey=false; try{ const _k=localStorage.getItem("sm.spritecannon.abkey"); if(_k) abilityKey=_k; }catch{}
function keyLabel(c){ if(!c) return "None"; return c.replace("ShiftLeft","L-Shift").replace("ShiftRight","R-Shift").replace("ControlLeft","L-Ctrl").replace("ControlRight","R-Ctrl").replace("AltLeft","L-Alt").replace("AltRight","R-Alt").replace(/^Key/,"").replace(/^Digit/,"").replace("ArrowUp","Up").replace("ArrowDown","Down").replace("ArrowLeft","Left").replace("ArrowRight","Right"); }
let dragonUsed=false, dragonRegen=0, staticMeter=0, overchargeT=0, falloutBounces=0, falloutCharge=0; // dragon / lightning / fallout
function piercing(){ return crashT>0 || superT>0 || phaseT>0 || starT>0 || slideGrace>0 || (ab()==="lightning" && overchargeT>0); } // blow through hazards: nitro = the active smash window (was speed-based = never died); lightning = overcharge; GodSpeed super = invincible; slideGrace = the slide launched you, so you smash through instead of dying
function shatterFx(o){ const cy=o.y-(o.h||30*DPR)*0.5; for(let i=0;i<15;i++) parts.push({x:o.x+rand(-8,8)*DPR,y:cy,vx:rand(-8,8)*DPR,vy:rand(-10,-1)*DPR,life:rand(0.4,0.95),c:(i%3===0)?"#6b7079":(i%2?"#c9ccd2":"#8a8f98"),r:rand(2,5.5)*DPR}); if(typeof dust==="function") dust(o.x,o.y); } // grey rubble burst: a crushed/destroyed hazard shatters into debris
// ---- SPOOKYSZN "CRAZY SLOTS" (HxH Kite): eat a candy -> spin the slot -> it LOCKS on a random numbered power ----
let candyRoll=null; // {t,spin,hold,result,applied} while a slot is spinning
const SLOTS=[
  {n:"SPEED",  c:"#ff5aa0", fn:()=>{ spicyT=Math.max(spicyT,2.8); kitty.vx=Math.max(kitty.vx, kitty.vx*1.5+6*DPR); }},           // 1 sugar-rush speed
  {n:"GHOST",  c:"#b59cff", fn:()=>{ phaseT=Math.max(phaseT,2.8); }},                                                            // 2 phase through hazards
  {n:"FLOAT",  c:"#7fe7ff", fn:()=>{ glideT=Math.max(glideT,3.6); kitty.vy=Math.min(kitty.vy,-6*DPR); flapsLeft=baseFlaps(); }}, // 3 low-grav glide
  {n:"STAR",   c:"#ffe27a", fn:()=>{ starT=Math.max(starT,4.2); kitty.vy=Math.min(kitty.vy,-4*DPR); flapsLeft=baseFlaps(); }},   // 4 invincible rainbow
  {n:"FLIGHT", c:"#8fd0ff", fn:()=>{ superT=Math.max(superT,3.6); flapsLeft=baseFlaps(); kitty.vy=Math.min(kitty.vy,-3*DPR); }}, // 5 man-of-steel free flight
  {n:"GOLD",   c:"#ffd34d", fn:()=>{ candyGoldT=6.5; }},                                                                         // 6 3x coins
  {n:"REAPER", c:"#c6ff5a", fn:()=>{ superT=Math.max(superT,4.8); starT=Math.max(starT,4.8); candyGoldT=9; flapsLeft=baseFlaps(); }} // 7 JACKPOT: fly + invincible + gold
];
function eatCandy(o){ if(candyRoll) return; // one spin at a time (like Kite's slot)
  const jackpot = Math.random()<0.11; // ~1-in-9 lands the REAPER, like the rare top slot
  candyRoll={ t:0, spin:0.8, hold:0.6, result: jackpot?6:(Math.random()*6)|0, applied:false };
  snd("token"); shake=Math.max(shake,8*DPR);
  for(let i=0;i<12;i++) parts.push({x:o.x,y:o.y,vx:rand(-5,5)*DPR,vy:rand(-6,1)*DPR,life:0.5,c:i%2?"#b06bff":"#ffd34d",r:rand(2,4)*DPR}); }
function applySlot(i){ const k=SLOTS[i], big=(i===6); k.fn(); pop(kitty.x,kitty.y-42*DPR, big?"REAPER!!":k.n+"!"); snd(big?"newbest":"reboot"); shake=Math.max(shake, big?24*DPR:12*DPR); hitstop=Math.max(hitstop, big?3:1);
  for(let j=0;j<(big?40:22);j++) parts.push({x:kitty.x,y:kitty.y,vx:rand(-7,7)*DPR,vy:rand(-8,2)*DPR,life:rand(0.4,1),c:j%2?k.c:"#ffffff",r:rand(2,5)*DPR}); }
// ---- BACCA axe: shift THROWS a Minecraft axe; shift again WARPS you to it mid-air (10s cooldown) ----
let baccaAxe=null;
function throwAxe(){ baccaAxe={ x:kitty.x+kitty.r*0.6, y:kitty.y, vx:Math.max(kitty.vx+13*DPR, 22*DPR), vy:-7*DPR, spin:0, t:0 }; snd("axe"); shake=Math.max(shake,8*DPR); pop(kitty.x,kitty.y-40*DPR,"AXE!"); for(let i=0;i<8;i++) parts.push({x:kitty.x,y:kitty.y,vx:rand(-3,3)*DPR,vy:rand(-3,3)*DPR,life:0.4,c:i%2?"#caa15a":"#c9ccd2",r:rand(1.5,3)*DPR}); }
function baccaTeleport(){ if(!baccaAxe) return; kitty.x=baccaAxe.x; kitty.y=Math.max(40*DPR,Math.min(GROUND()-kitty.r,baccaAxe.y)); kitty.vx=Math.max(kitty.vx, baccaAxe.vx*0.85, 16*DPR); kitty.vy=Math.min(kitty.vy,-6*DPR); kitty.px=kitty.x; phaseT=Math.max(phaseT,0.25); kitty.sx=1.4; kitty.sy=0.66; shake=Math.max(shake,10*DPR); hitstop=Math.max(hitstop,1); snd("token"); pop(kitty.x,kitty.y-40*DPR,"WARP!"); for(let i=0;i<20;i++) parts.push({x:kitty.x,y:kitty.y,vx:rand(-6,6)*DPR,vy:rand(-6,4)*DPR,life:0.55,c:i%2?"#caa15a":"#7fe7ff",r:rand(2,4)*DPR}); baccaAxe=null; }
function drawBaccaAxe(t){ const a=baccaAxe, ax=a.x-cam; ctx.save(); ctx.translate(ax,a.y); ctx.rotate(a.spin);
  ctx.shadowColor="rgba(200,210,220,.6)"; ctx.shadowBlur=8*DPR;
  ctx.fillStyle="#6b4a2a"; roundRect(-2.5*DPR,-4*DPR,5*DPR,20*DPR,2*DPR); ctx.fill(); // handle
  ctx.fillStyle="#c9ccd2"; ctx.beginPath(); ctx.moveTo(0,-14*DPR); ctx.lineTo(13*DPR,-9*DPR); ctx.quadraticCurveTo(16*DPR,-3*DPR,12*DPR,1*DPR); ctx.lineTo(0,-3*DPR); ctx.closePath(); ctx.fill(); // blade
  ctx.fillStyle="#8a8f98"; ctx.beginPath(); ctx.moveTo(0,-14*DPR); ctx.lineTo(-13*DPR,-9*DPR); ctx.quadraticCurveTo(-16*DPR,-3*DPR,-12*DPR,1*DPR); ctx.lineTo(0,-3*DPR); ctx.closePath(); ctx.fill(); // back blade
  ctx.shadowBlur=0; ctx.restore();
  ctx.save(); ctx.globalAlpha=0.5+0.4*Math.sin(t*6); ctx.fillStyle="#caa15a"; ctx.font=`800 ${9*DPR}px Orbitron`; ctx.textAlign="center"; ctx.fillText("WARP", ax, a.y-20*DPR); ctx.restore(); }
function drawSkateboard(t){ const sx=kitty.x-cam, sy=kitty.y+kitty.r*0.92*SKIN_VIS, w=kitty.r*2.0*SKIN_VIS; ctx.save(); ctx.translate(sx,sy); ctx.rotate(Math.sin(t*9)*0.12);
  ctx.fillStyle="#ffd34d"; for(const dx of [-w*0.34,w*0.34]){ ctx.beginPath(); ctx.arc(dx,6*DPR,5*DPR,0,7); ctx.fill(); ctx.strokeStyle="rgba(0,0,0,.4)"; ctx.lineWidth=1.4*DPR; ctx.stroke(); } // wheels
  ctx.fillStyle="#3a2a5a"; roundRect(-w/2,-4*DPR,w,8*DPR,4*DPR); ctx.fill(); // deck
  ctx.fillStyle="#ff5aa0"; ctx.fillRect(-w*0.42,-1.5*DPR,w*0.84,3*DPR); ctx.restore(); } // grip stripe
function falloutCriticalMass(){ falloutCharge=0; shake=Math.max(shake,22*DPR); hitstop=2; kitty.vx+=20*DPR; kitty.vy=Math.min(kitty.vy,-6*DPR); pop(kitty.x,kitty.y-52*DPR,"CRITICAL MASS!"); snd("reboot");
  for(const o of obstacles){ if(Math.abs(o.x-kitty.x)<360*DPR && (o.type==="spike"||o.type==="tnt"||o.type==="doghouse")) o.hit=true; } // clear nearby hazards
  for(let i=0;i<40;i++) parts.push({x:kitty.x,y:kitty.y,vx:rand(-10,10)*DPR,vy:rand(-10,10)*DPR,life:1,c:i%3?"#ff7a1a":(i%2?"#ffd34d":"#fff"),r:rand(2,6)*DPR}); }
function activeCfg(){ return ACTIVE_AB[ab()]||null; }
function activateAbility(){
  if(state!=="fly") return; const c=activeCfg(); if(!c) return;
  if(ab()==="rocketstart" && baccaAxe){ baccaTeleport(); return; } // 2nd AXE press = warp to the thrown axe (ignores cooldown)
  if(ab()==="candy"){ if(candyRoll || abCd>0) return; abCd=c.cd; eatCandy({x:kitty.x,y:kitty.y-18*DPR}); return; } // Spookyszn: the ability button summons + eats a candy on demand -> spins the Crazy Slots (candies still also drift in to catch)
  if(abCd>0 || abUses<=0) return;
  abCd=c.cd; if(c.uses!==Infinity) abUses--;
  const id=ab();
  if(id==="rocketstart"){ throwAxe(); }
  else if(id==="magnetburst"){ magnetT=c.dur; pop(kitty.x,kitty.y-44*DPR,"MAGNET!"); shake=Math.max(shake,6*DPR); snd("token"); }
  else if(id==="flamejet"){ kitty.vx=Math.max(kitty.vx+16*DPR, 22*DPR); kitty.vy=Math.min(kitty.vy-3*DPR,-6*DPR); kitty.sx=1.7; kitty.sy=0.58; shake=Math.max(shake,10*DPR); hitstop=Math.max(hitstop,1); snd("spring"); flameTrailT=1.15; pop(kitty.x,kitty.y-40*DPR,"FLAME!"); // FURTHER dash + always a little UP (never dives into a spike) + a long lingering fire stream
    for(let i=0;i<44;i++) parts.push({x:kitty.x-rand(0,42)*DPR,y:kitty.y+rand(-10,10)*DPR,vx:rand(-15,-2)*DPR,vy:rand(-3,3)*DPR,life:rand(0.5,1.15),c:(i%3===0)?"#ff3d1f":(i%2?"#ff7a1a":"#ffd34d"),r:rand(3,7)*DPR}); }
  else if(id==="windboost"){ kitty.vy=Math.min(kitty.vy,-15*DPR); kitty.vx+=2*DPR; glideT=c.dur; snd("flap");
    for(let i=0;i<16;i++) parts.push({x:kitty.x+rand(-12,12)*DPR,y:kitty.y+10*DPR,vx:rand(-2,2)*DPR,vy:rand(1,3.5)*DPR,life:0.7,c:"#bfefff",r:rand(2,4)*DPR}); }
  else if(id==="teleport"){ kitty.x+=300*DPR; kitty.y=Math.max(40*DPR,kitty.y-95*DPR); kitty.vy=Math.min(kitty.vy,-12*DPR); kitty.vx=Math.max(kitty.vx,17*DPR); kitty.px=kitty.x; phaseT=Math.max(phaseT,0.32); kitty.sy=0.66; kitty.sx=1.38; shake=Math.max(shake,8*DPR); hitstop=Math.max(hitstop,1); snd("token"); pop(kitty.x,kitty.y-30*DPR,"BLINK!"); // longer forward blink + higher leap + a brief phase-through so it RELIABLY dodges through danger
    for(let i=0;i<26;i++) parts.push({x:kitty.x-rand(0,180)*DPR,y:kitty.y+rand(-14,14)*DPR,vx:0,vy:0,life:0.55,c:i%2?"#b59cff":"#7fe7ff",r:rand(2,4.5)*DPR}); }
  else if(id==="dragon"){ abCd=6+Math.random(); kitty.vy=-cfg.power*0.62*DPR; kitty.vx+=2*DPR; kitty.sx=1.3; kitty.sy=0.7; shake=14*DPR; snd("reboot"); pop(kitty.x,kitty.y-44*DPR,"DRAGON DRAFT!"); for(let i=0;i<24;i++) parts.push({x:kitty.x,y:kitty.y+10*DPR,vx:rand(-4,4)*DPR,vy:rand(1,5)*DPR,life:0.8,c:i%2?"#ff7a1a":"#ffd34d",r:rand(2,5)*DPR}); } // dragon draft relaunch, fired by the ability keybind (once/run)
  else if(id==="lightning"){ overchargeT=1.4; kitty.vx=Math.min(62*DPR, kitty.vx+28*DPR); kitty.vy=Math.min(kitty.vy,-4*DPR); kitty.sx=1.7; kitty.sy=0.55; shake=Math.max(shake,16*DPR); hitstop=Math.max(hitstop,2); snd("reboot"); pop(kitty.x,kitty.y-52*DPR,"THUNDER DASH!"); for(let i=0;i<26;i++) parts.push({x:kitty.x-rand(0,30)*DPR,y:kitty.y+rand(-12,12)*DPR,vx:rand(-12,-2)*DPR,vy:rand(-3,3)*DPR,life:rand(0.4,0.8),c:i%2?"#7fe7ff":"#ffffff",r:rand(3,6)*DPR}); } // Thunder Dash on the ability key (5s cd): big piercing forward burst (capped) + pierce window
  else if(id==="crashout"){ crashT=c.dur; kitty.vx+=15*DPR; kitty.sx=1.6; kitty.sy=0.55; shake=Math.max(shake,12*DPR); hitstop=2; flashT=Math.max(flashT,0); snd("reboot"); pop(kitty.x,kitty.y-42*DPR,"NITRO!"); // active smash window: pierce everything for dur, then it ends (no more never-die)
    for(let i=0;i<26;i++) parts.push({x:kitty.x-rand(0,18)*DPR,y:kitty.y+rand(-10,10)*DPR,vx:rand(-11,-3)*DPR,vy:rand(-3,3)*DPR,life:rand(0.4,0.8),c:(i%3===0)?"#21e6ff":(i%2?"#7fe7ff":"#ffffff"),r:rand(3,6)*DPR}); }
  else if(id==="split"){ // FLYING RAIJIN (Minato, the Yellow Flash): warps the way you're already FLYING - flap up first to warp up, dive to warp down, level to warp forward. Nothing to aim, it follows your trajectory + flashes through hazards
    let dx=kitty.vx, dy=kitty.vy; if(dx<4*DPR) dx=8*DPR; // always carry you forward
    const m=Math.hypot(dx,dy)||1; dx/=m; dy/=m; const D=380*DPR;
    kitty.x=kitty.x+dx*D;
    kitty.y=Math.max(40*DPR, Math.min(GROUND()-kitty.r, kitty.y+dy*D));
    kitty.vx=Math.max(kitty.vx, dx*22*DPR + 9*DPR); kitty.vy=dy*20*DPR - 2*DPR; // keep flying along the warp line (tiny upward bias so you never warp straight into the floor)
    kitty.px=kitty.x; phaseT=Math.max(phaseT,0.3); kitty.sx=1.45; kitty.sy=0.64; shake=Math.max(shake,9*DPR); hitstop=Math.max(hitstop,1); flashT=Math.max(flashT,0.32); snd("token"); pop(kitty.x,kitty.y-46*DPR,"FLYING RAIJIN!"); // yellow-flash warp
    for(let i=0;i<30;i++) parts.push({x:kitty.x-dx*rand(0,170)*DPR,y:kitty.y-dy*rand(0,170)*DPR,vx:0,vy:0,life:0.55,c:(i%3===0)?"#fff2a8":(i%2?"#ffd34d":"#ffae2e"),r:rand(2,5)*DPR}); } // gold streak trailing the origin, Yellow Flash style
  else if(id==="spicyspeed"){ spicyT=1.5; kitty.vx=Math.max(kitty.vx,kitty.vx*1.6); kitty.sx=1.5; kitty.sy=0.62; shake=Math.max(shake,8*DPR); snd("spring"); pop(kitty.x,kitty.y-40*DPR,"SPICY!"); // forward burst (NOT invincible); ongoing boost + speed-lines applied each frame while spicyT>0
    for(let i=0;i<22;i++) parts.push({x:kitty.x-rand(0,22)*DPR,y:kitty.y+rand(-9,9)*DPR,vx:rand(-13,-3)*DPR,vy:rand(-2,2)*DPR,life:rand(0.3,0.6),c:(i%2?"#ff4d1f":"#ffae3c"),r:rand(2,5)*DPR}); }
  else if(id==="buildwall"){ const x0=kitty.x+120*DPR, x1=kitty.x+1300*DPR; let cleared=0;
    for(const o of obstacles){ if(o.hit||o.got||o.pop) continue; if((o.type==="spike"||o.type==="tnt"||o.type==="doghouse") && o.x>=x0 && o.x<=x1){ o.hit=true; cleared++; shatterFx(o); } } // golden brick-wall CRUSHES the hazards ahead (they shatter into rubble + vanish); pickups left alone
    wallFx={x0,x1,life:0.6}; shake=Math.max(shake,14*DPR); hitstop=Math.max(hitstop,2); snd("reboot"); pop(kitty.x,kitty.y-44*DPR,cleared?"WALL UP!":"WALL!"); }
  else if(id==="slowmo"){ slowT=3.5; shake=Math.max(shake,6*DPR); snd("reboot"); pop(kitty.x,kitty.y-44*DPR,"SLOW-MO!"); // bullet-time: only the world advance is scaled (see WS in update), player input stays full
    for(let i=0;i<20;i++) parts.push({x:kitty.x+rand(-20,20)*DPR,y:kitty.y+rand(-20,20)*DPR,vx:rand(-2,2)*DPR,vy:rand(-2,2)*DPR,life:0.7,c:i%2?"#b59cff":"#d6b8ff",r:rand(2,4)*DPR}); }
  else if(id==="screech"){ let cleared=0; const x0=cam-40*DPR, x1=cam+W+40*DPR;
    for(const o of obstacles){ if(o.hit||o.got||o.pop) continue; if((o.type==="spike"||o.type==="tnt"||o.type==="doghouse") && o.x>=x0 && o.x<=x1){ o.hit=true; cleared++; shatterFx(o); } } // SONIC SCREAM shatters EVERY hazard on screen
    shockwaves.push({x:kitty.x,y:kitty.y,r:14*DPR,life:1.25,scream:true}); shockwaves.push({x:kitty.x,y:kitty.y,r:42*DPR,life:1.45,scream:true}); shockwaves.push({x:kitty.x,y:kitty.y,r:74*DPR,life:1.6,scream:true}); // 3 expanding sound rings
    phaseT=Math.max(phaseT,0.45); kitty.vx=Math.min(62*DPR,kitty.vx+24*DPR); kitty.vy=Math.min(kitty.vy,-8*DPR); kitty.sx=1.75; kitty.sy=0.52; shake=Math.max(shake,26*DPR); hitstop=Math.max(hitstop,3); flashT=Math.max(flashT,0.4); snd("screech"); pop(kitty.x,kitty.y-54*DPR,"SCREECH!!");
    for(let i=0;i<32;i++){ const a=Math.random()*6.28, sp=rand(4,12)*DPR; parts.push({x:kitty.x,y:kitty.y,vx:Math.cos(a)*sp,vy:Math.sin(a)*sp,life:rand(0.4,0.95),c:i%3===0?"#c6b3ff":(i%2?"#7fe7ff":"#ffffff"),r:rand(2,5)*DPR}); } } // radial sonic burst (violet/cyan/white)
  else if(id==="thedrop"){ let cleared=0; const R=560*DPR; // THE DROP (tscott): bass-drop -> clear a BIG radius + big launch up/forward + rainbow impulse shockwave
    for(const o of obstacles){ if(o.hit||o.got||o.pop) continue; if((o.type==="spike"||o.type==="tnt"||o.type==="doghouse") && Math.abs(o.x-kitty.x)<R){ o.hit=true; cleared++; for(let i=0;i<8;i++) parts.push({x:o.x,y:o.y-(o.h||30*DPR)*0.5,vx:rand(-7,7)*DPR,vy:rand(-9,1)*DPR,life:0.8,c:i%2?"#b56bff":"#ffd34d",r:rand(2,5)*DPR}); } }
    shockwaves.push({x:kitty.x,y:kitty.y,r:16*DPR,life:1.2,rainbow:true}); shockwaves.push({x:kitty.x,y:kitty.y,r:60*DPR,life:1.35,rainbow:true}); kitty.vy=-27*DPR; kitty.vx=Math.min(62*DPR,kitty.vx+18*DPR); kitty.sx=1.7; kitty.sy=0.52; shake=Math.max(shake,26*DPR); hitstop=Math.max(hitstop,3); flashT=Math.max(flashT,0.35); snd("reboot"); pop(kitty.x,kitty.y-54*DPR,"THE DROP!");
    for(let i=0;i<28;i++) parts.push({x:kitty.x+rand(-30,30)*DPR,y:kitty.y+rand(-10,20)*DPR,vx:rand(-8,8)*DPR,vy:rand(-12,-2)*DPR,life:rand(0.4,0.9),c:(i%3?"#b56bff":"#ffd34d"),r:rand(2,5)*DPR}); }
  else if(id==="GodSpeed"){ superT=c.dur; kitty.vx=Math.min(62*DPR,Math.max(kitty.vx,52*DPR)); kitty.vy=Math.min(kitty.vy,-3*DPR); kitty.sx=1.6; kitty.sy=0.6; shake=Math.max(shake,16*DPR); hitstop=Math.max(hitstop,2); snd("reboot"); pop(kitty.x,kitty.y-52*DPR,"GOD SPEED!"); // invincible + near-max speed + free flight while superT>0 (sprite transforms in drawKitty)
    for(let i=0;i<30;i++) parts.push({x:kitty.x-rand(0,26)*DPR,y:kitty.y+rand(-12,12)*DPR,vx:rand(-12,-2)*DPR,vy:rand(-3,3)*DPR,life:rand(0.4,0.9),c:(i%2?"#ffd34d":"#fff2c0"),r:rand(3,6)*DPR}); }
  else if(id==="phantom"){ phaseT=c.dur; kitty.vx=Math.min(62*DPR,Math.max(kitty.vx,48*DPR)); kitty.vy=Math.min(kitty.vy,-4*DPR); kitty.sx=1.6; kitty.sy=0.6; shake=Math.max(shake,12*DPR); hitstop=Math.max(hitstop,2); snd("reboot"); pop(kitty.x,kitty.y-48*DPR,"GHOST RUSH!"); // ghost out: intangible + CRAZY SPEED, blaze through every hazard while phaseT>0
    for(let i=0;i<30;i++) parts.push({x:kitty.x-rand(0,30)*DPR,y:kitty.y+rand(-18,18)*DPR,vx:rand(-10,-2)*DPR,vy:rand(-3,3)*DPR,life:rand(0.4,0.95),c:(i%3?"#9b8cff":"#2c2350"),r:rand(2,5)*DPR}); }
  else if(id==="rage"){ let cleared=0; const R=340*DPR; // SPARTAN RAGE (kratos): sweep nearby hazards + launch up/forward
    for(const o of obstacles){ if(o.hit||o.got||o.pop) continue; if((o.type==="spike"||o.type==="tnt"||o.type==="doghouse") && Math.abs(o.x-kitty.x)<R){ o.hit=true; cleared++; for(let i=0;i<8;i++) parts.push({x:o.x,y:o.y-(o.h||30*DPR)*0.5,vx:rand(-7,7)*DPR,vy:rand(-9,1)*DPR,life:0.8,c:i%2?"#ff5a2a":"#ffd34d",r:rand(2,5)*DPR}); } }
    shockwaves.push({x:kitty.x,y:kitty.y,r:20*DPR,life:1}); kitty.vy=-19*DPR; kitty.vx=Math.min(62*DPR,kitty.vx+13*DPR); kitty.sx=1.6; kitty.sy=0.55; shake=Math.max(shake,20*DPR); hitstop=Math.max(hitstop,3); snd("reboot"); pop(kitty.x,kitty.y-54*DPR,"SPARTAN RAGE!");
    for(let i=0;i<28;i++) parts.push({x:kitty.x+rand(-30,30)*DPR,y:kitty.y+rand(-10,20)*DPR,vx:rand(-8,8)*DPR,vy:rand(-12,-2)*DPR,life:rand(0.4,0.9),c:(i%3?"#ff5a2a":"#ffd34d"),r:rand(2,5)*DPR}); }
  else if(id==="fallout"){ let cleared=0; const R=W+80*DPR; // FALLOUT = drop the NUKE: wipe the whole screen of hazards into a coin blast, mushroom-launch, and go radioactive-invincible for a moment
    for(const o of obstacles){ if(o.hit||o.got||o.pop) continue; if((o.type==="spike"||o.type==="tnt"||o.type==="doghouse") && Math.abs(o.x-kitty.x)<R){ o.hit=true; cleared++; runCoins+=coinAmt(6); for(let i=0;i<6;i++) parts.push({x:o.x,y:o.y-(o.h||30*DPR)*0.5,vx:rand(-6,6)*DPR,vy:rand(-9,1)*DPR,life:0.9,c:i%2?"#c6ff5a":"#ffd34d",r:rand(2,5)*DPR}); } }
    phaseT=Math.max(phaseT,1.4); shockwaves.push({x:kitty.x,y:kitty.y,r:24*DPR,life:1}); shockwaves.push({x:kitty.x,y:kitty.y,r:10*DPR,life:1});
    kitty.vy=-20*DPR; kitty.vx=Math.min(62*DPR,kitty.vx+16*DPR); kitty.sx=1.6; kitty.sy=0.55; shake=Math.max(shake,24*DPR); hitstop=Math.max(hitstop,3); flashT=Math.max(flashT,0.6); snd("reboot"); pop(kitty.x,kitty.y-54*DPR,cleared?"NUKE!  +"+cleared:"NUKE!");
    for(let i=0;i<40;i++) parts.push({x:kitty.x+rand(-30,30)*DPR,y:kitty.y+rand(-16,10)*DPR,vx:rand(-9,9)*DPR,vy:rand(-13,-2)*DPR,life:rand(0.5,1.1),c:(i%3===0)?"#c6ff5a":(i%2?"#ffd34d":"#ff7a1a"),r:rand(2,6)*DPR}); }
}
let shielded=false, chromePlates=0; // chrome: absorbs the first N hazard hits (chrome plating)
function hazard(){ if(shielded){ shielded=false; kitty.vy=-13*DPR; kitty.vx=Math.max(kitty.vx,5*DPR); shake=10*DPR; pop(kitty.x,kitty.y-30*DPR,"SHIELD!"); for(let i=0;i<16;i++) parts.push({x:kitty.x,y:kitty.y,vx:rand(-4,4)*DPR,vy:rand(-5,-1)*DPR,life:1,c:i%2?"#7fe7ff":"#fff",r:rand(2,4)*DPR}); snd("token"); return true; }
  if(ab()==="chrome" && chromePlates>0){ chromePlates--; chromeRegen=0; kitty.vy=Math.min(kitty.vy,-12*DPR); kitty.vx=Math.max(kitty.vx+10*DPR,14*DPR); shake=14*DPR; hitstop=Math.max(hitstop,1); shockwaves.push({x:kitty.x,y:kitty.y,r:16*DPR,life:1}); pop(kitty.x,kitty.y-30*DPR,"CHROME!"); for(let i=0;i<22;i++) parts.push({x:kitty.x,y:kitty.y,vx:rand(-6,6)*DPR,vy:rand(-6,-1)*DPR,life:0.8,c:i%2?"#eef4fa":"#9fb3c6",r:rand(2,4.5)*DPR}); snd("token"); return true; } // chrome plating: absorb -> DEFLECT forward with a shockwave (renewable, regen below)
  return false; }
let bgImgs=[], bgIdx=0;
function loadImg(src){ return new Promise(res=>{ const im=new Image(); im.onload=()=>res(im); im.onerror=()=>res(null); im.src=src; }); }
async function loadManifests(){
  try{ const r=await fetch(AB+"spritecannon/skins/manifest.json",{cache:"no-store"});
    if(r.ok){ const m=await r.json(); const _variants=[]; for(const p of (m.images||[])){
      const id=p.split("/").pop().replace(/\.[^.]+$/,""); const im=await loadImg(AB+p.replace(/^imported\//,""));
      if(!im) continue; skinImgs[id]=im; _computeSkinBox(id,im);
      if(id.includes("__")){ _variants.push(id); continue; } // base__style = a style variant, grouped under its base below (not a standalone skin)
      if(!skinList.find(s=>s.id===id)) skinList.push({id,name:id.replace(/[-_]/g," "),cost:250,img:im});
    }
    for(const vid of _variants){ const base=vid.split("__")[0], styleId=vid.split("__").slice(1).join("__"); const s=skinList.find(x=>x.id===base);
      if(s){ if(!s.styles) s.styles=[{id:base,name:"Default",img:s.img}]; s.styles.push({id:vid,name:styleId.replace(/[-_]/g," "),img:skinImgs[vid]}); } } }
  }catch{}
  try{ const r=await fetch(AB+"spritecannon/skins/abilities.json",{cache:"no-store"});
    if(r.ok){ const cfgA=await r.json(); for(const id in cfgA){ const s=skinList.find(x=>x.id===id); if(s){ const e=cfgA[id]; if(e.ability) s.ability=e.ability; if(e.cost!=null) s.cost=Number(e.cost)||s.cost; if(e.name) s.name=e.name; if(e.secret) s.secret=true; if(e.rarity) s.rarity=e.rarity; if(e.unlock) s.unlock=e.unlock; if(e.grant) s.grant=true; if(e.abilityDesc) s.abilityDesc=e.abilityDesc; } } } if(typeof checkUnlocks==="function") checkUnlocks(); // a returning player may already meet a grant condition
  }catch{}
  try{ const r=await fetch(AB+"spritecannon/backgrounds/manifest.json",{cache:"no-store"});
    if(r.ok){ const m=await r.json(); for(const p of (m.images||[])){ const im=await loadImg(AB+p.replace(/^imported\//,"")); if(im) bgImgs.push(im); } }
  }catch{}
}

const GROUND=()=>H-70*DPR;
let state="start";
let cam=0, camY=0; // camY = vertical follow-camera (pans up so a high-flying kitty stays on screen)
let kitty, obstacles=[], parts=[], shake=0, flapsLeft=0, comboN=0, comboT=0;
let runRevived=false, revivesUsed=0, reviveTimer=null; // paid-continue state (revivesUsed drives cost tier + per-run cap; revived runs ARE leaderboard-eligible now)
let _rivals=[], _rivalI=0; // Rival Ghost: board scores you can chase this run (ascending) + pointer to the next one to beat
let ufo=null, infurno=null, infurnoUsedThisRun=false, savedRun=null, infCoins=[], infHaz=[], ufoBiomeMark="";
let gauntlet=null, gorilla=null, gHaz=[], gauntletUsed={fire:false,water:false,zombie:false,gorilla:false}; // survival gauntlet (lethal secret levels)
let lives=0, rebootFx=null, stall=0;
let aimAngle=42, charging=false, power=0, powerDir=1, aimUp=false, aimDown=false, infLeft=false, infRight=false;
let runCoins=0, distM=0, worldX=0, hitstop=0, flashT=0, winFlashT=0, lastMile=0, popTexts=[], prevBest=0, beatBest=false, skimCd=0, bhopWin=0, bhopChain=0;

function rand(a,b){ return a+Math.random()*(b-a); }
function frac(v){ return v-Math.floor(v); }
function hashY(k){ return frac(Math.sin(k*12.9898)*43758.5453); }
function hashS(k){ return frac(Math.cos(k*7.131)*1234.567); }

function reset(){
  cam=0; camY=0; obstacles=[]; parts=[]; shake=0; runCoins=0; distM=0; stall=0; lives=0; rebootFx=null; runRevived=false; revivesUsed=0; _worldEntry=null; clearInterval(reviveTimer); // _worldEntry cleared so a ceremony from a prior run can't flash over the results card / next launch
  aimAngle=42; power=0; charging=false; powerDir=1; stopCharge(); comboN=0; { const _ab=document.getElementById("againBtn"); if(_ab) _ab.classList.remove("sc-pulse"); } flapsLeft=baseFlaps();
  ufo=null; infurno=null; infCoins=[]; infurnoUsedThisRun=false; ufoBiomeMark=""; shielded=(ab()==="shield"||ab()==="turtle"); chromePlates=(ab()==="chrome"?3:0); if(ab()==="ninelives"||ab()==="revive") lives=1; // revive/ninelives start the run with 1 heart already; chrome = 3 armor plates
  gauntlet=null; gorilla=null; gHaz=[]; gauntletUsed={fire:false,water:false,zombie:false,gorilla:false};
  { const _c=ACTIVE_AB[ab()]; abUses=_c?_c.uses:0; } abCd=0; abRegen=0; flameTrailT=0; crashT=0; magnetT=0; glideT=0; dragonUsed=false; dragonRegen=0; staticMeter=0; overchargeT=0; falloutBounces=0; falloutCharge=0;
  spicyT=0; slowT=0; phaseT=0; superT=0; superCoast=0; starT=0; flightMeter=1; abHeld=false; wallFx=null; shockwaves=[]; portalFx=[]; turtleRegen=0; chromeRegen=0; candyGoldT=0; candySpawnT=2.5; candyRoll=null; baccaAxe=null; skateT=0; // new-ability per-run state
  kitty={ x:90*DPR, y:GROUND()-30*DPR, vx:0, vy:0, r:22*DPR, rest:false, sx:1, sy:1, spin:0 };
  hitstop=0; flashT=0; winFlashT=0; lastMile=0; popTexts=[]; prevBest=save.best; beatBest=false; skimCd=0; bhopWin=0; bhopChain=0; stopBark();
  if(Math.random()<0.45){ const sM=rand(280,1100); waterStart=90*DPR+sM*24*DPR; waterEnd=waterStart+rand(2600,4200)*DPR; } else { waterStart=waterEnd=-1; } waterCd=0; // ~45% of runs: a lethal water stretch
  slideT=0; slideGrace=0;
  buildWorld(); aimKitty(); initRivals();
  if(bgImgs.length) bgIdx=(bgIdx+1)%bgImgs.length;
  state="aim";
  show("startCard",false); show("resultCard",false); show("shopCard",false);
  show("lbCard",false); show("confirmCard",false); show("skinDetailCard",false); show("questCard",false); show("rewardCard",false); show("worldsCard",false); show("reviveCard",false); if(typeof closeUnlock==="function") closeUnlock(); // R / reset must not start a run stranded behind a leftover modal (questCard/rewardCard/worldsCard included)
  if(!tutSeen){ const te=document.getElementById("tut"); if(te) te.classList.remove("hidden"); }
}
function aimKitty(){ const a=aimAngle*Math.PI/180, L=72*DPR; kitty.x=90*DPR+Math.cos(a)*L; kitty.y=GROUND()-Math.sin(a)*L-4*DPR; kitty.spin=0; }
// procedural endless world: keep spawning ahead of the kitten; type mix unlocks new items by distance
function spawnObstacleAt(x){
  const dm=(x-90*DPR)/(24*DPR), t=Math.random();
  const diff=Math.min(1, dm/2500); // difficulty ramps with distance: more hazards, fewer safe bounces
  if(inWaterX(x)){ // WATER stretch: floats THROUGHOUT (always something to land/bounce on) + occasional diving boards; water costs a life
    obstacles.push({type:"float",x,y:GROUND(),w:rand(70,90)*DPR,h:14*DPR});
    obstacles.push({type:"float",x:x+rand(130,210)*DPR,y:GROUND(),w:rand(64,84)*DPR,h:14*DPR}); // fill the gap so a float is always reachable
    if(t<0.22) obstacles.push({type:"diveboard",x:x+rand(50,100)*DPR,y:GROUND(),w:52*DPR,h:30*DPR});
    if(Math.random()<0.10) obstacles.push({type:"comet",x:x+rand(-40,60)*DPR,y:GROUND()-rand(150,300)*DPR,r:20*DPR,got:false});
    if(Math.random()<0.05) obstacles.push({type:"bolt",x:x+rand(-40,60)*DPR,y:GROUND()-rand(140,280)*DPR,r:18*DPR,got:false});
    return;
  }
  if(biomeAt(dm).floaty){ // ZERO-G WORLD: everything floats. Mid-air launch pads to chain through the sky,
    // hazards that bob up/down to dodge, and treasure strung through the air. Low gravity keeps you hanging.
    obstacles.push({type:"updraft",x,y:GROUND()-rand(120,400)*DPR,r:26*DPR,hit:false}); // floating high-launch pad (always one)
    if(t<0.62) obstacles.push({type:"balloon",x:x+rand(50,150)*DPR,y:GROUND()-rand(120,340)*DPR,r:26*DPR,pop:false}); // floating bounce-balloon
    if(dm>150 && t<0.5){ const by=GROUND()-rand(150,360)*DPR; obstacles.push({type:"spike",x:x+rand(-20,70)*DPR,y:by,w:50*DPR,h:30*DPR,hit:false,bob:{amp:rand(45,95)*DPR,spd:rand(0.7,1.4),ph:rand(0,6.283),base:by}}); } // bobbing hazard to dodge
    if(Math.random()<0.55) obstacles.push({type:"comet",x:x+rand(-50,90)*DPR,y:GROUND()-rand(150,400)*DPR,r:20*DPR,got:false}); // floating coins
    if(Math.random()<0.14) obstacles.push({type:"star",x:x+rand(-40,70)*DPR,y:GROUND()-rand(180,360)*DPR,r:22*DPR,got:false}); // rare invincibility
    if(Math.random()<0.12) obstacles.push({type:"token",x:x+rand(-50,80)*DPR,y:GROUND()-rand(130,330)*DPR,r:18*DPR,got:false}); // +life heart
    return;
  }
  const _bn=biomeAt(dm).name, _funland=_bn==="Funland", _desert=_bn==="Desert Purramids";
  let placedSlide=false; const _i0=obstacles.length; // mark this call's spawns -> safety pass at the end keeps hazards off bouncers
  if(_desert && Math.random()<0.34){ const n=3+Math.floor(Math.random()*4); // DESERT = treasure run: a buried-gold ARC of coins (variable count 3-6 = variable-ratio reward; collecting the line chains the combo / rising-pitch coin ladder)
    for(let i=0;i<n;i++){ const px=x+rand(-30,40)*DPR+i*rand(40,56)*DPR, py=GROUND()-(150+Math.sin(i/(n-1)*Math.PI)*160)*DPR; obstacles.push({type:"comet",x:px,y:py,r:20*DPR,got:false}); } }
  if(_funland && t<0.55){ obstacles.push({type:"slide",x,y:GROUND(),w:150*DPR,h:42*DPR}); placedSlide=true; } // Funland: ridable boost slide (suppresses the co-spawned tnt/doghouse below so no HIDDEN hazard sits inside the slide footprint)
  else if(t<0.30-0.14*diff) obstacles.push({type:"pad",x,y:GROUND(),w:90*DPR,h:18*DPR});
  else if(t<0.50-0.14*diff) obstacles.push({type:"spring",x,y:GROUND(),w:42*DPR,h:46*DPR});
  else if(t<0.72-0.10*diff) obstacles.push({type:"balloon",x,y:GROUND()-rand(150,360)*DPR,r:26*DPR,pop:false});
  else if(dm>700 && t<0.80) obstacles.push({type:"tramp",x,y:GROUND(),w:120*DPR,h:22*DPR}); // mega trampoline deeper in
  else obstacles.push({type:"spike",x,y:GROUND(),w:54*DPR,h:30*DPR,hit:false});
  if(!_funland && dm>800 && Math.random()<0.03) obstacles.push({type:"slide",x:x+rand(150,210)*DPR,y:GROUND(),w:150*DPR,h:42*DPR}); // rare ridable-slide treat outside Funland (offset far so it can't overlap)
  if(Math.random()<0.10) obstacles.push({type:"token",x:x+rand(-70,90)*DPR,y:GROUND()-rand(120,320)*DPR,r:18*DPR,got:false});     // hearts: +3% (Diggy)
  if(dm>200 && Math.random()<(_desert?0.05:0.013)) obstacles.push({type:"jackpot",x:x+rand(-30,70)*DPR,y:GROUND(),w:86*DPR,h:20*DPR,hit:false}); // RARE jackpot pad: bounce = variable big coin payout + fanfare (slot-machine reward). Desert = golden treasure theme -> ~4x denser.
  if(dm>300 && Math.random()<0.055) obstacles.push({type:"bolt",x:x+rand(-60,80)*DPR,y:GROUND()-rand(140,300)*DPR,r:18*DPR,got:false});     // stamina bolt: rare
  if(dm>350 && Math.random()<0.08) obstacles.push({type:"rocket",x:x+rand(-60,80)*DPR,y:GROUND()-rand(150,330)*DPR,r:22*DPR,hit:false});    // rocket: explodes, big launch
  if(dm>400 && Math.random()<0.05) obstacles.push({type:"updraft",x:x+rand(-60,80)*DPR,y:GROUND()-rand(150,340)*DPR,r:26*DPR,hit:false});   // updraft: floating, launches you HIGHER than a trampoline
  if(dm>250 && Math.random()<0.06) obstacles.push({type:"comet",x:x+rand(-60,80)*DPR,y:GROUND()-rand(170,360)*DPR,r:20*DPR,got:false});      // bonus coins
  if(dm>500 && Math.random()<0.012) obstacles.push({type:"star",x:x+rand(-40,70)*DPR,y:GROUND()-rand(170,360)*DPR,r:22*DPR,got:false});       // RARE Super Star power-up (temporary invincibility)
  if(dm>400 && Math.random()<0.017) obstacles.push({type:"portal",x:x+rand(-30,70)*DPR,y:GROUND()-rand(110,300)*DPR,r:32*DPR,used:false});    // PORTAL: fly through -> warp out HIGH up with a speed boost
  // HIGH-ALTITUDE treats: a steep/big launch used to fly into empty sky -> sometimes string balloons, hearts, flap-wings + coins WAY up so getting big air pays off
  if(dm>120 && Math.random()<0.32){ const hy=function(){ return GROUND()-rand(430,920)*DPR; };
    obstacles.push({type:"balloon",x:x+rand(-40,80)*DPR,y:hy(),r:26*DPR,pop:false});
    if(Math.random()<0.65) obstacles.push({type:"token",x:x+rand(40,170)*DPR,y:hy(),r:18*DPR,got:false});       // heart up high
    if(dm>300 && Math.random()<0.55) obstacles.push({type:"bolt",x:x+rand(-70,110)*DPR,y:hy(),r:18*DPR,got:false}); // flap "wings" up high
    if(Math.random()<0.55) obstacles.push({type:"comet",x:x+rand(60,190)*DPR,y:hy(),r:20*DPR,got:false}); }       // coins up high
  if(dm>500 && !placedSlide && Math.random()<0.08+0.14*diff) obstacles.push({type:"tnt",x:x+rand(-30,60)*DPR,y:GROUND(),w:44*DPR,h:40*DPR,hit:false}); // hazard: blows you up (not under a slide)
  if(dm>2000 && !placedSlide && Math.random()<0.05+0.13*Math.min(1,(dm-2000)/3000)) obstacles.push({type:"doghouse",x:x+rand(-20,50)*DPR,y:GROUND(),w:58*DPR,h:46*DPR,hit:false}); // brutal: land in it = death (not under a slide)
  // SAFETY: a hazard must never hide ON/UNDER a bouncer (unfair stealth death) -> nudge any tnt/doghouse spawned this call clear of pads/springs/tramps/slides/jackpots
  { const bnc=[]; for(let i=_i0;i<obstacles.length;i++){ const o=obstacles[i]; if(o.w && (o.type==="pad"||o.type==="spring"||o.type==="tramp"||o.type==="slide"||o.type==="jackpot")) bnc.push(o); }
    if(bnc.length) for(let i=_i0;i<obstacles.length;i++){ const o=obstacles[i]; if(o.type!=="tnt" && o.type!=="doghouse") continue; const hw=o.w/2;
      for(const b of bnc){ const bhw=b.w/2; if(Math.abs(o.x-b.x) < bhw+hw+10*DPR) o.x = b.x + (bhw+hw+14*DPR)*(o.x>=b.x?1:-1); } } }
}
function extendWorld(toX){ let guard=0; while(worldX<toX && guard++<500){ worldX+=rand(240,500)*DPR; spawnObstacleAt(worldX); } }
function buildWorld(){ obstacles=[]; worldX=460*DPR; extendWorld(90*DPR+6000*DPR); }

function press(){ if(state==="aim"){ charging=true; startCharge(); hideTut(); } else if(state==="fly"){ flap(); } else if(state==="infurno"){ if(infurno && !infurno.ready){ infurnoGo(); infurnoFlap(); } else infurnoFlap(); } else if(state==="gauntlet"){ if(gauntlet && !gauntlet.ready){ gauntletGo(); gauntletFlap(); } else gauntletFlap(); }
  else if(state==="start"||state==="result"){ reset(); } }
function release(){ if(state==="aim" && charging){ fire(); } }
function flap(){ if(kitty.rest) return;
  // BHOP: a well-timed press while skating / just-landed KEEPS (and slightly boosts) horizontal momentum + hops you up to clear objects. Skill = keep the rhythm off each landing; chaining builds a small speed bonus. Costs NO flap.
  if(bhopWin>0 && kitty.y+kitty.r > GROUND()-90*DPR){
    bhopChain=Math.min(bhopChain+1,6);
    const boost=1.03+bhopChain*0.006;
    kitty.vx=Math.min(62*DPR, Math.max(kitty.vx*boost, kitty.vx+2*DPR));
    kitty.vy=-13*DPR; kitty.sy=0.6; kitty.sx=1.5; shake=Math.max(shake,5*DPR); snd("spring");
    pop(kitty.x,kitty.y-34*DPR, bhopChain>1?("BHOP x"+bhopChain+"!"):"BHOP!");
    for(let i=0;i<10;i++) parts.push({x:kitty.x-8*DPR,y:kitty.y+kitty.r*0.6,vx:rand(-5,-1)*DPR,vy:rand(-1,2)*DPR,life:0.5,c:i%2?"#bfefff":"#ffffff",r:rand(2,4)*DPR});
    bhopWin=0; return; // one bhop per landing; window reopens on the next ground contact
  }
  // SUPER STAR: infinite wings (flaps never deplete while the star is lit)
  if(starT>0){ snd("flap"); kitty.vy=-cfg.flapPower*DPR*0.62 - 2; kitty.vx+=1.4*DPR;
    for(let i=0;i<10;i++) parts.push({x:kitty.x-10*DPR, y:kitty.y+10*DPR, vx:rand(-2,-0.4)*DPR, vy:rand(-1,1.6)*DPR, life:1, c:"#ffe36b", r:rand(1.5,3.5)*DPR}); shake=4*DPR; return; }
  if(flapsLeft>0){ flapsLeft--; snd("flap"); kitty.vy=-cfg.flapPower*DPR*0.62 - 2; kitty.vx+=1.4*DPR;
    for(let i=0;i<10;i++) parts.push({x:kitty.x-10*DPR, y:kitty.y+10*DPR, vx:rand(-2,-0.4)*DPR, vy:rand(-1,1.6)*DPR, life:1, c:"#bfefff", r:rand(1.5,3.5)*DPR}); shake=4*DPR; } }
function mbounce(base,bm){ const b=base*bm*DPR; return -Math.min(b + Math.abs(kitty.vy)*0.55, b*1.9); } // momentum-reactive bounce: faster fall -> higher bounce, capped at 1.9x so it can't runaway-combine with the speed cap
function fire(){ charging=false; stopCharge(); snd("fire"); musicStart(); const a=aimAngle*Math.PI/180; let p=(power/100)*cfg.power*DPR; if(ab()==="rocketstart") p*=1.35;
  kitty.vx=Math.cos(a)*p; kitty.vy=-Math.sin(a)*p; kitty.rest=false; state="fly"; shake=10*DPR;
  for(let i=0,n=Math.round(26*JF());i<n;i++) parts.push({x:kitty.x, y:kitty.y, vx:rand(-1,5)*DPR, vy:rand(-3,3)*DPR, life:1, c:i%2?"#ffd36b":"#ff8a3c", r:rand(2,5)*DPR}); }

addEventListener("keydown",(e)=>{
  if(e.repeat) return;
  { const _t=e.target; if(_t && (_t.tagName==="INPUT"||_t.tagName==="TEXTAREA"||_t.isContentEditable)) return; } // typing a name/code -> don't trigger r=restart / s=shop / space=fire
  if(capturingKey){ e.preventDefault(); const _RESV=["Space","ArrowUp","ArrowDown","ArrowLeft","ArrowRight","KeyR","KeyS","Escape"]; if(e.code!=="Escape" && _RESV.indexOf(e.code)<0) { abilityKey=e.code; try{ localStorage.setItem("sm.spritecannon.abkey",abilityKey); }catch{} } capturingKey=false; const b=document.getElementById("abKeyBtn"); if(b) b.textContent=keyLabel(abilityKey); return; } // reserved keys (Space/arrows/R/S) are used by game controls -> refuse to bind the ability to them (else the ability silently never fires)
  if(state==="gauntlet" && gauntlet && !gauntlet.ready){ gauntletGo(); } // PRESS ANY KEY TO START the survival level (then fall through so the key also does its normal action)
  if(state==="infurno" && infurno && !infurno.ready){ infurnoGo(); } // PRESS ANY KEY TO START Dante's Infurno
  if(e.code==="Space"){ e.preventDefault(); press(); }
  else if(e.code==="ArrowUp"||e.code==="KeyW"){ e.preventDefault(); if(state==="infurno"){ if(infurno && !infurno.ready) infurnoGo(); infurnoFlap(); } else aimUp=true; }   // W/Up also FLAPS in Infurno (full WASD there); mirrors arrows elsewhere
  else if(e.code==="ArrowDown"){ e.preventDefault(); aimDown=true; }
  else if(e.code==="ArrowLeft"||e.code==="KeyA"){ e.preventDefault(); infLeft=true; }
  else if(e.code==="ArrowRight"||e.code==="KeyD"){ e.preventDefault(); infRight=true; }
  else if(e.code==="KeyS"){ if(state==="aim"||state==="fly"||state==="infurno"||state==="gauntlet"){ e.preventDefault(); aimDown=true; } else toggleShop(); } // S = aim-down during play, shop-toggle in menus
  else if(e.key==="r"||e.key==="R"){ reset(); }
  else if(DEV && (e.key==="u"||e.key==="U")){ if(state==="fly") startAbduction(); } // DEV-only: force UFO abduction (gated so shipped viewers can't farm secret levels)
  else if(DEV && (e.key==="j"||e.key==="J")){ if(state==="fly") startGauntlet("fire"); }  // DEV-only: force TRIAL BY FIRE
  else if(DEV && (e.key==="k"||e.key==="K")){ if(state==="fly") startGauntlet("water"); } // DEV-only: force SHARK WATERS
  else if(DEV && (e.key==="z"||e.key==="Z")){ if(state==="fly") startGauntlet("zombie"); } // DEV-only: force GRAVE DANGER (zombie hands)
  else if(DEV && (e.key==="g"||e.key==="G")){ if(state==="fly") startGauntlet("gorilla"); } // DEV-only: force GORILLA WARFARE
  else if(e.code==="Escape"){ e.preventDefault(); // universal back / to menu
    if(!document.getElementById("skinDetailCard").classList.contains("hidden")) show("skinDetailCard",false);
    else if(!document.getElementById("confirmCard").classList.contains("hidden")) show("confirmCard",false);
    else if(state==="shop") closeShopFn();
    else if(state==="quest") closeQuests();
    else if(state==="worlds") closeWorlds();
    else if(state==="lb") closeLB();
    else if(state==="result"||state==="aim"){ charging=false; stopCharge(); show("resultCard",false); show("shopCard",false); show("lbCard",false); show("questCard",false); show("rewardCard",false); show("worldsCard",false); show("startCard",true); state="start"; } } // stop charge riser so it doesn't loop on the menu
  else if(e.code===abilityKey){ e.preventDefault(); abHeld=true; activateAbility(); } // abHeld powers MAN OF STEEL held flight; activateAbility fires the press-style actives
  else if(e.key==="m"||e.key==="M"){ toggleMute(); }
});
addEventListener("keyup",(e)=>{ if(e.code==="Space"){ e.preventDefault(); release(); } else if(e.code==="ArrowUp"||e.code==="KeyW"){ aimUp=false; } else if(e.code==="ArrowDown"||e.code==="KeyS"){ aimDown=false; } else if(e.code==="ArrowLeft"||e.code==="KeyA"){ infLeft=false; } else if(e.code==="ArrowRight"||e.code==="KeyD"){ infRight=false; } else if(e.code===abilityKey){ abHeld=false; } });
addEventListener("blur",()=>{ abHeld=false; aimUp=aimDown=infLeft=infRight=false; if(charging){ charging=false; if(typeof stopCharge==="function") stopCharge(); } }); // focus loss can swallow the keyup -> drop all held-input so godmode flight/aim/charge don't stick on alt-tab
cv.addEventListener("mousedown",(e)=>{ e.preventDefault(); press(); });
addEventListener("mouseup",()=>{ release(); });
// --- touch: tap-and-hold to charge/flap; preventDefault kills the synthetic mouse double-fire (and page scroll/zoom) ---
const IS_TOUCH = ('ontouchstart' in window) || navigator.maxTouchPoints>0;
if(IS_TOUCH){ document.body.classList.add("is-touch");
  cv.addEventListener("touchstart",(e)=>{ e.preventDefault(); press(); },{passive:false});
  cv.addEventListener("touchend",(e)=>{ e.preventDefault(); release(); },{passive:false});
  cv.addEventListener("touchcancel",()=>{ if(charging){ charging=false; if(typeof stopCharge==="function") stopCharge(); } else release(); }); } // a CANCELLED touch (palm/edge reject, notification shade, call) must abort the charge, not commit a launch (mirrors the window-blur cancel)

function update(){
  if(shake>0) shake*=0.86;
  if(abCd>0) abCd-=1/60; if(magnetT>0) magnetT-=1/60; if(glideT>0) glideT-=1/60; if(overchargeT>0) overchargeT-=1/60; if(waterCd>0) waterCd-=1/60; if(crashT>0) crashT-=1/60;
  if(skimCd>0) skimCd-=1/60;
  if(bhopWin>0){ bhopWin--; if(bhopWin===0) bhopChain=0; } // bhop timing window closing -> chain resets if you missed it
  if(slideGrace>0) slideGrace-=1/60; // slide hazard-immunity ticking down (set while riding; lingers into the launch)
  if(spicyT>0) spicyT-=1/60; if(slowT>0) slowT-=1/60; if(phaseT>0) phaseT-=1/60; if(superT>0){ superT-=1/60; if(superT<=0) superCoast=1.3; } if(superCoast>0) superCoast-=1/60; if(starT>0) starT-=1/60; if(candyGoldT>0) candyGoldT-=1/60; if(skateT>0) skateT-=1/60; // GodSpeed leaves a momentum-coast window; Super Star invincibility ticks down; candyGoldT = Spookyszn gold rush; skateT = Bart skate dash
  if(candyRoll){ candyRoll.t+=1/60; if(!candyRoll.applied && candyRoll.t>=candyRoll.spin){ candyRoll.applied=true; applySlot(candyRoll.result); } if(candyRoll.t>=candyRoll.spin+candyRoll.hold) candyRoll=null; } // CRAZY SLOTS: spin -> lock -> fire the rolled power
  if(wallFx){ wallFx.life-=1/60; if(wallFx.life<=0) wallFx=null; }
  if(shockwaves.length){ for(const s of shockwaves){ s.r+=14*DPR; s.life-=1/60*2; } shockwaves=shockwaves.filter(s=>s.life>0); }
  if(portalFx.length){ for(const pf of portalFx) pf.t-=1/60*1.5; portalFx=portalFx.filter(pf=>pf.t>0); }
  if(ab()==="teleport"){ const _cap=ACTIVE_AB.teleport.uses; if(abUses<_cap){ abRegen+=1/60; if(abRegen>=5){ abRegen=0; abUses=Math.min(_cap,abUses+1); } } else abRegen=0; } // zeropoint: regen one blink charge every 5s, cap 3
  if(ab()==="turtle" && state==="fly"){ if(!shielded){ turtleRegen+=1/60; if(turtleRegen>=10){ turtleRegen=0; shielded=true; snd("token"); pop(kitty.x,kitty.y-40*DPR,"SHELL BACK!"); for(let i=0;i<12;i++) parts.push({x:kitty.x+rand(-14,14)*DPR,y:kitty.y+rand(-14,14)*DPR,vx:rand(-2,2)*DPR,vy:rand(-3,1)*DPR,life:0.6,c:i%2?"#3ad17a":"#bff5cf",r:rand(2,4)*DPR}); } } else turtleRegen=0; } // Turtle: the shell shield grows back every 10s
  if(ab()==="chrome" && state==="fly"){ if(chromePlates<3){ chromeRegen+=1/60; if(chromeRegen>=14){ chromeRegen=0; chromePlates++; snd("token"); pop(kitty.x,kitty.y-40*DPR,"+PLATE"); for(let i=0;i<10;i++) parts.push({x:kitty.x+rand(-12,12)*DPR,y:kitty.y+rand(-12,12)*DPR,vx:0,vy:0,life:0.5,c:i%2?"#eef4fa":"#9fb3c6",r:rand(1.5,3)*DPR}); } } else chromeRegen=0; } // Chrome: a spent plate grows back every 14s (max 3)
  if(ab()==="spicyspeed"){ const _cap=ACTIVE_AB.spicyspeed.uses; if(abUses<_cap){ abRegen+=1/60; if(abRegen>=12){ abRegen=0; abUses=Math.min(_cap,abUses+1); } } else abRegen=0; } // SPICY SPEED: regen 1 charge every 12s once below max (starts only when below 5)
  if(flashT>0) flashT-=0.06;
  if(winFlashT>0) winFlashT-=0.03; // gold survival-celebration flash (lingers longer than the red danger flash)
  if(hitstop>0){ hitstop--; }
  else if(state==="aim"){ if(aimUp) aimAngle=Math.min(85,aimAngle+1.2); if(aimDown) aimAngle=Math.max(8,aimAngle-1.2); aimKitty(); if(charging){ power+=powerDir*2.4; if(power>=100){power=100;powerDir=-1;} if(power<=0){power=0;powerDir=1;} if(chargeEngine) chargeEngine.set(power/100); } }
  else if(state==="fly"){
    const WS = (slowT>0?0.55:1); // SLOW-MO bullet-time factor: world advance scaled; softened so the player feels less slowed (flap/aim already full speed)
    kitty.vy += grav()*DPR;
    if(starT>0){ kitty.vx=Math.min(62*DPR,Math.max(kitty.vx,48*DPR)); if(kitty.vy>7*DPR) kitty.vy=7*DPR; // SUPER STAR: speed floor + a floatier fall (infinite wings handled in flap) = a powered-up rush
      for(let i=0;i<2;i++){ const h=(performance.now()*0.4+i*180)%360; parts.push({x:kitty.x-rand(2,22)*DPR,y:kitty.y+rand(-9,9)*DPR,vx:rand(-11,-2)*DPR,vy:rand(-2,2)*DPR,life:rand(0.3,0.6),c:"hsl("+h+",100%,64%)",r:rand(2,4.5)*DPR}); } }
    if(superT>0){ kitty.vy -= grav()*DPR*0.95; if(kitty.vy>4*DPR) kitty.vy=4*DPR; if(kitty.vy<-13*DPR) kitty.vy=-13*DPR; kitty.vx=Math.min(62*DPR,Math.max(kitty.vx,50*DPR)); // GodSpeed: near-zero gravity free flight + near-max speed (capped)
      for(let i=0;i<3;i++) parts.push({x:kitty.x-rand(2,26)*DPR,y:kitty.y+rand(-10,10)*DPR,vx:rand(-12,-2)*DPR,vy:rand(-2,2)*DPR,life:rand(0.3,0.7),c:(i%2?"#ffd34d":"#fff2c0"),r:rand(2.5,6)*DPR}); } // gold aura/trail
    if(ab()==="godmode"){ if(abHeld && flightMeter>0){ kitty.vy -= grav()*DPR*1.85; if(kitty.vy<-9*DPR) kitty.vy=-9*DPR; flightMeter=Math.max(0,flightMeter-1/(60*1.5)); // MAN OF STEEL: metered flight (~2.5s of held thrust); survivable (drains, then normal gravity)
        if(parts.length<240) for(let i=0;i<2;i++) parts.push({x:kitty.x-rand(0,14)*DPR,y:kitty.y+rand(4,12)*DPR,vx:rand(-3,1)*DPR,vy:rand(1,4)*DPR,life:0.5,c:i%2?"#9fd0ff":"#ffffff",r:rand(2,4)*DPR}); }
      else flightMeter=Math.min(1,flightMeter+1/(60*3)); } // recharge over ~3s when not flying (drain ~1.5s)
    if(ab()==="bartman"){ if(abHeld && flightMeter>0){ kitty.vy -= grav()*DPR*1.7; if(kitty.vy<-9*DPR) kitty.vy=-9*DPR; kitty.vx=Math.min(62*DPR,Math.max(kitty.vx,34*DPR)); flightMeter=Math.max(0,flightMeter-1/(60*1.2)); if(parts.length<250) for(let i=0;i<2;i++) parts.push({x:kitty.x-rand(6,30)*DPR,y:kitty.y+kitty.r*0.7+rand(-4,4)*DPR,vx:rand(-11,-4)*DPR,vy:rand(-1,2)*DPR,life:0.35,c:i%2?"#ffd34d":"#7fe7ff",r:rand(1.5,3.5)*DPR}); } // BART SKATEBOARD: HOLD to ride + fly (metered, like Man of Steel) + skate speed; drains ~1.2s
      else flightMeter=Math.min(1,flightMeter+1/(60*3.8)); } // recharge when you let go (~3.8s, a bit slower)
    if(spicyT>0){ kitty.vx=Math.min(62*DPR,Math.max(kitty.vx,kitty.vx+1.4*DPR)); if(parts.length<240) for(let i=0;i<2;i++) parts.push({x:kitty.x-rand(6,30)*DPR,y:kitty.y+rand(-8,8)*DPR,vx:rand(-16,-6)*DPR,vy:0,life:0.3,c:i%2?"#ff4d1f":"#ffae3c",r:rand(1.5,3.5)*DPR}); } // SPICY SPEED: ongoing forward accel + speed-lines (not invincible)
    if(slideT>0){ slideT-=1/60; slideGrace=0.55; kitty.vx+=2.0*DPR; if(kitty.vy>3*DPR) kitty.vy=3*DPR; if(Math.random()<0.6) slideFx(kitty.x-10*DPR,kitty.y+kitty.r*0.5); // riding the slide: forward boost + low glide (vx still bounded by the hard cap below); slideGrace refreshed each ride frame so it lingers ~0.55s into the launch arc
      if(slideT<=0){ kitty.vy=Math.min(kitty.vy,-12*DPR); kitty.vx+=3*DPR; kitty.sy=0.6; kitty.sx=1.4; shake=Math.max(shake,8*DPR); pop(kitty.x,kitty.y-30*DPR,"WHEEE!"); snd("spring"); } } // slide ejects you up + forward at the end
    if(flameTrailT>0){ flameTrailT-=1/60; for(let i=0;i<3;i++) parts.push({x:kitty.x-rand(2,20)*DPR,y:kitty.y+rand(-7,7)*DPR,vx:rand(-6,-1)*DPR,vy:rand(-2,2)*DPR,life:rand(0.32,0.6),c:(i%2?"#ff7a1a":"#ffd34d"),r:rand(2.5,5)*DPR}); } // inferno flame trail
    if(crashT>0){ for(let i=0;i<5;i++) parts.push({x:kitty.x+rand(-kitty.r,kitty.r),y:kitty.y+rand(-kitty.r,kitty.r),vx:rand(-8,-1)*DPR,vy:rand(-3,1)*DPR,life:rand(0.3,0.6),c:(i%3===0)?"#ff3d1f":(i%2?"#ff7a1a":"#ffd34d"),r:rand(2.5,6)*DPR}); } // NITRO: kitty engulfed in flames while immune (smashing through)
    if(kitty.vx>30*DPR) kitty.vx*=(superCoast>0?0.998:0.992);   // SAFETY air-drag; GodSpeed momentum carries (gentler) for ~1.3s after super ends
    if(kitty.vx>62*DPR) kitty.vx=62*DPR;            // SAFETY: hard speed cap -> camera always keeps up, no glitched infinite runs/scores
    kitty.px=kitty.x; kitty.py=kitty.y; kitty.x += kitty.vx*WS; kitty.y += kitty.vy;  // px/py = pre-move pos for swept collision (anti-tunnel); *WS = slow-mo only slows forward world progress (cam/distM follow x), vertical input stays full
    kitty.spin += kitty.vx*0.0016;
    const sp=Math.hypot(kitty.vx,kitty.vy);
    kitty.sx += ((1+Math.min(.35,sp*0.012))-kitty.sx)*0.3; kitty.sy += ((1-Math.min(.28,sp*0.010))-kitty.sy)*0.3; // ease squash back
    // dragon: once-per-run wing-flap recovery when plummeting near the ground
    // (dragon draft is now player-triggered via flap() when out of flaps + falling, not auto)
    const g=GROUND();
    const fellThisFrame = kitty.vy>0; // capture descent BEFORE the ground clamp negates vy, so a fast fall still resolves obstacle collisions (anti-tunnel-Y: pads bounce + hazards stay lethal at speed)
    if(kitty.y+kitty.r>g){ kitty.y=g-kitty.r;
      const onPlat = inWater() && nearFloat(); // landing on a float/diveboard = safe (the float bounce below handles it), never a splash death
      if(onPlat){ /* water platform: no splash penalty; obstacle loop bounces you */ }
      else if(inWater() && !(ab()==="waterimmune" || superT>0 || phaseT>0 || starT>0)){ // splashed into the lethal water -> costs a life (not insta-kill); WATER WALKER / GodSpeed / WRAITH / SUPER STAR = immune (starT pierces hazards via piercing(), so it must skim water too -> consistent invincibility)
        if(waterCd<=0){ waterCd=0.9; flashT=1; shake=14*DPR; snd("spike"); splashFx(kitty.x,g);
          if(lives>0){ lives--; kitty.vy=-13*DPR; kitty.vx*=0.65; pop(kitty.x,kitty.y-42*DPR,"SPLASH! -1 life"); } else { tryEnd(); } }
        else { kitty.vy=Math.min(kitty.vy,-8*DPR); kitty.vx*=0.9; } // brief grace: bounce out, no extra loss
      } else if(inWater()){ // WATER WALKER / GodSpeed: skim the surface, harmless splash
        if(waterCd<=0){ waterCd=0.5; splashFx(kitty.x,g); } kitty.vy=Math.min(kitty.vy,-8*DPR); kitty.vx*=0.97;
      } else {
        bhopWin=Math.max(bhopWin,12); // touched the ground -> open the bhop timing window (a flap now = a momentum-keeping hop)
        const _falmo = ab()==="fallout" && falloutBounces<4; // fallout: first ~4 bounces keep + boost speed (nuclear pulse bounce)
        const _lowfric = _falmo || crashT>0 || (ab()==="lightning" && overchargeT>0);
        if(Math.abs(kitty.vy)>2.4*DPR){ kitty.vy*=-0.46; kitty.vx*=(_lowfric?0.985:0.74); if(_falmo){ falloutBounces++; kitty.vx*=1.04; } dust(kitty.x,g); shake=Math.min(8*DPR,Math.abs(kitty.vy)); }
        else { kitty.vy=0; kitty.vx*=(_lowfric?0.997:0.86); if(Math.abs(kitty.vx)<0.4*DPR){ tryEnd(); } }
      }
    }
    const pr=pickR(), bm=(ab()==="bouncy"?1.28:1), _bobT=performance.now()*0.001;
    for(const o of obstacles){
      if(o.bob){ o.y = o.bob.base + Math.sin(_bobT*o.bob.spd + o.bob.ph)*o.bob.amp; } // Zero-G: hazards drift up/down (collision follows, so you really must dodge)
      if(magnetT>0 && !o.got && !o.pop && (o.type==="token"||o.type==="bolt"||o.type==="comet"||o.type==="balloon")){ const _dx=kitty.x-o.x, _dy=kitty.y-o.y; if(Math.hypot(_dx,_dy)<460*DPR){ o.x+=_dx*0.2; o.y+=_dy*0.2; } } // magnetburst pull
      if(o.type==="token"){ if(!o.got && segNear(o.x,o.y,pr+o.r)){ o.got=true; lives=Math.min(maxLives(),lives+1); runCoins+=coinAmt(3); tokenFx(o.x,o.y); snd("token"); playCustom("purr",0.6); } continue; }
      if(o.type==="bolt"){ if(!o.got && segNear(o.x,o.y,pr+o.r)){ o.got=true; flapsLeft=baseFlaps(); runCoins+=coinAmt(2); boltFx(o.x,o.y); snd("token"); } continue; }
      if(o.type==="comet"){ if(!o.got && segNear(o.x,o.y,pr+o.r)){ o.got=true; const cb=bumpCombo(); runCoins+=coinAmt(20); popFx(o.x,o.y); pop(o.x,o.y,"+"+coinAmt(20)+(cb>1?"  x"+cb:"")); coinChime(Math.min(cb-1,12)); } continue; }
      if(o.type==="star"){ if(!o.got && segNear(o.x,o.y,pr+o.r)){ o.got=true; starT=Math.max(starT,5); kitty.vx=Math.min(62*DPR,Math.max(kitty.vx,50*DPR)); kitty.vy=Math.min(kitty.vy,-4*DPR); flapsLeft=baseFlaps(); shake=Math.max(shake,12*DPR); snd("token"); pop(o.x,o.y-12*DPR,"SUPER STAR!"); for(let i=0;i<28;i++){ const h=i/28*360; parts.push({x:o.x,y:o.y,vx:rand(-7,7)*DPR,vy:rand(-7,3)*DPR,life:rand(0.5,1),c:"hsl("+h+",100%,62%)",r:rand(2,5)*DPR}); } } continue; } // SUPER STAR: 5s invincible + SPEED rush + INFINITE WINGS (Diggy's buff)
      if(o.type==="candy"){ if(!o.got && segNear(o.x,o.y,pr+o.r)){ o.got=true; eatCandy(o); } continue; } // Spookyszn: eat -> random power
      if(o.type==="rocket"){ if(!o.hit && segNear(o.x,o.y,kitty.r+o.r)){ o.hit=true; kitty.vx+=24*DPR; kitty.vy=Math.min(kitty.vy,-12*DPR); kitty.sx=1.5; kitty.sy=0.7; shake=18*DPR; hitstop=3; runCoins+=coinAmt(4); rocketFx(o.x,o.y); pop(o.x,o.y,"BOOST!"); snd("reboot"); } continue; }
      if(o.type==="updraft"){ if(!o.hit && segNear(o.x,o.y,kitty.r+o.r)){ o.hit=true; kitty.vy=-32*DPR; kitty.vx+=4*DPR; kitty.sy=0.55; kitty.sx=1.5; shake=14*DPR; hitstop=2; updraftFx(o.x,o.y); pop(o.x,o.y,"WHOOSH!"); snd("spring"); } continue; } // floating updraft: launches you higher than a tramp
      if(o.type==="portal"){ if(!o.used && segNear(o.x,o.y,kitty.r+o.r*0.72)){ o.used=true; // PORTAL: vanish in -> pop out of a NEW portal HIGH on the screen with a movement boost
          const ny=GROUND()-rand(560,920)*DPR; portalFx.push({x:o.x,y:o.y,t:1,k:"in"});
          kitty.y=ny; kitty.vx=Math.max(kitty.vx,24*DPR)+rand(6,12)*DPR; kitty.vy=-rand(7,12)*DPR; kitty.sx=1.5; kitty.sy=0.6;
          portalFx.push({x:kitty.x,y:ny,t:1,k:"out"}); shake=16*DPR; hitstop=2; flashT=Math.max(flashT,0.4); snd("reboot"); pop(kitty.x,ny-34*DPR,"WARP!");
          for(let i=0;i<30;i++) parts.push({x:kitty.x,y:ny,vx:rand(-7,7)*DPR,vy:rand(-7,7)*DPR,life:rand(0.4,0.9),c:i%2?"#b06bff":"#27e6ff",r:rand(2,5)*DPR});
          for(let i=0;i<10;i++) parts.push({x:o.x,y:o.y,vx:rand(-4,4)*DPR,vy:rand(-4,4)*DPR,life:rand(0.3,0.6),c:i%2?"#b06bff":"#27e6ff",r:rand(2,4)*DPR}); } continue; }
      if(o.type==="balloon"){ if(!o.pop && segNear(o.x,o.y,pr+o.r)){ o.pop=true; kitty.vx+=13*DPR; kitty.vy=Math.min(kitty.vy-3*DPR,-9*DPR); kitty.sx=1.5; kitty.sy=0.7; shake=14*DPR; hitstop=2; runCoins+=coinAmt(5); grenadeFx(o.x,o.y); pop(o.x,o.y,"BLAST!"); snd("balloon"); } continue; } // grenade balloon: blast forward + a bit higher
      if(o.w && Math.max(kitty.px,kitty.x)>o.x-o.w/2 && Math.min(kitty.px,kitty.x)<o.x+o.w/2 && kitty.y+kitty.r>o.y-o.h && kitty.y-kitty.r<o.y && (kitty.vy>=0 || fellThisFrame)){ // swept-x + fellThisFrame: catches fast roll-past AND fast drops the ground-clamp already negated (anti-tunnel). The `kitty.y-kitty.r<o.y` lower bound confines the hit to the obstacle's band so an AIRBORNE bobbing spike (Zero-G) can't phantom-kill a cat flying entirely beneath it.
        // (Fallout is now an active NUKE on the ability key; the old passive charge-on-hit was removed)
        if(o.type==="pad"){ kitty.vy=mbounce(16,bm); kitty.vx+=2*DPR; if(kitty.vx<10*DPR){ kitty.vx=Math.max(kitty.vx,13*DPR); kitty.vy=Math.min(kitty.vy,-16*DPR); pop(kitty.x,kitty.y-34*DPR,"BOING!"); } kitty.sy=0.6; kitty.sx=1.4; dust(kitty.x,o.y); shake=8*DPR; snd("pad"); } // low-momentum rescue: a slow roll-in still launches you forward + up back into play
        else if(o.type==="jackpot"){ kitty.vy=mbounce(20,bm); kitty.vx+=3*DPR; kitty.sy=0.55; kitty.sx=1.5; dust(kitty.x,o.y); // golden bounce pad: bounces every time, pays a VARIABLE jackpot once
          if(!o.hit){ o.hit=true; const roll=Math.random(), mega=roll>=0.97; const amt= roll<0.78? coinAmt(80) : mega? coinAmt(1000) : coinAmt(250); runCoins+=amt; hitstop=2; const cb=bumpCombo();
            pop(o.x,o.y-32*DPR,(mega?"⭐ MEGA JACKPOT +":"JACKPOT +")+amt); jackpotFx(o.x,o.y,mega); coinChime(Math.min(cb-1,12)); snd("newbest"); }
          else { shake=Math.max(shake,8*DPR); snd("pad"); } }
        else if(o.type==="tramp"){ kitty.vy=mbounce(24,bm); kitty.vx+=3*DPR; if(kitty.vx<12*DPR){ kitty.vx=Math.max(kitty.vx,19*DPR); kitty.vy=Math.min(kitty.vy,-27*DPR); pop(kitty.x,kitty.y-36*DPR,"REBOUND!"); } kitty.sy=0.55; kitty.sx=1.5; dust(kitty.x,o.y); shake=11*DPR; snd("pad"); } // low-momentum rescue: even a dead roll-in relaunches you forward + up so a stall isn't an instant death
        else if(o.type==="float"){ kitty.vy=mbounce(15,bm); kitty.vx+=2*DPR; kitty.sy=0.6; kitty.sx=1.4; splashFx(kitty.x,o.y); shake=7*DPR; snd("pad"); } // water float platform
        else if(o.type==="diveboard"){ kitty.vy=mbounce(27,bm); kitty.vx+=4*DPR; kitty.sy=0.5; kitty.sx=1.5; dust(kitty.x,o.y); shake=12*DPR; snd("spring"); } // diving board: big launch
        else if(o.type==="spring"){ kitty.vy=mbounce(18,bm); kitty.vx+=12*DPR; kitty.sy=0.55; kitty.sx=1.5; dust(kitty.x,o.y); shake=10*DPR; snd("spring"); } // stronger spring: higher bounce + more forward pop (Diggy: "needs a little more boost")
        else if(o.type==="slide"){ if(slideT<=0){ slideT=0.75; kitty.vx=Math.max(kitty.vx,30*DPR); kitty.vy=-3*DPR; kitty.sy=0.7; kitty.sx=1.3; slideFx(kitty.x,o.y); shake=8*DPR; snd("pad"); pop(o.x,o.y-o.h,"SLIDE!"); } } // ridable park slide = boost ramp (guard: don't restack mid-ride)
        else if(o.type==="spike" && !o.hit){ o.hit=true; if(piercing()){ shake=Math.max(shake,8*DPR); kitty.vx*=0.8; kitty.vy*=0.92; } else if(!hazard()){ kitty.vx*=0.1; kitty.vy=0; shake=16*DPR; hitstop=5; flashT=1; snd("spike");
          for(let i=0;i<18;i++) parts.push({x:kitty.x,y:kitty.y,vx:rand(-3,3)*DPR,vy:rand(-4,0)*DPR,life:1,c:"#ff2d4f",r:rand(2,4)*DPR}); tryEnd(); } }
        else if(o.type==="tnt" && !o.hit){ o.hit=true; if(piercing()){ shake=Math.max(shake,8*DPR); kitty.vx*=0.8; kitty.vy*=0.92; } else if(!hazard()){ shake=22*DPR; hitstop=6; flashT=1; snd("spike");
          for(let i=0;i<46;i++) parts.push({x:o.x,y:o.y-o.h*0.5,vx:rand(-8,8)*DPR,vy:rand(-10,2)*DPR,life:1,c:i%3?"#ff7a1a":(i%2?"#ffd34d":"#ff2d4f"),r:rand(2,6)*DPR}); tryEnd(); } }
        else if(o.type==="doghouse" && !o.hit){ o.hit=true; if(piercing()){ shake=Math.max(shake,8*DPR); kitty.vx*=0.8; kitty.vy*=0.92; } else if(!hazard()){ shake=18*DPR; hitstop=6; flashT=1; if(!playCustom("bark")) snd("spike"); pop(o.x,o.y-o.h,"GULP!");
          for(let i=0;i<24;i++) parts.push({x:o.x,y:o.y-o.h*0.5,vx:rand(-4,4)*DPR,vy:rand(-5,0)*DPR,life:1,c:i%2?"#6b4a2a":"#caa15a",r:rand(2,5)*DPR}); tryEnd(); } }
      }
      // hazard-skim near-miss: cleared a GROUND hazard by a whisker (genuine only — tight clearance band, ascending or level, throttled, deep enough). A manufactured-win dopamine beat, never spammed.
      if(skimCd<=0 && distM>180 && !o.hit && (o.type==="spike"||o.type==="tnt"||o.type==="doghouse")){
        const _dx=Math.abs(kitty.x-o.x), _clr=(o.y-o.h)-(kitty.y+kitty.r);
        if(_dx < o.w*0.5+kitty.r*0.6 && _clr>2*DPR && _clr<20*DPR && kitty.vx>3*DPR && kitty.vy<6*DPR){
          skimCd=1.6; pop(kitty.x,kitty.y-46*DPR,"PHEW!"); snd("flap");
          for(let i=0;i<8;i++) parts.push({x:kitty.x,y:kitty.y+10*DPR,vx:rand(-3,3)*DPR,vy:rand(-1,2)*DPR,life:0.5,c:"#bfefff",r:rand(1.5,3)*DPR}); }
      }
    }
    // Keep the kitty anchored ~260 world-px from the left on wide screens, but never past ~24% of a NARROW
    // (portrait) screen -> on phones the cat no longer hugs the right/center, it sits left with room to see ahead.
    const anchorX=Math.min(260*DPR, W*0.24);
    cam += ((kitty.x-anchorX)-cam)*0.12; if(cam<0) cam=0;
    // NO vertical follow-camera on ANY device (Diggy wants PC behavior everywhere): the kitty soars up off the
    // top and you take in the whole world; an on-screen arrow + "Nm up" readout (below) shows how high it is.
    camY=0;
    const nd=Math.floor((kitty.x-90*DPR)/(24*DPR)); distM=Math.max(distM,nd);
    while(_rivalI<_rivals.length && distM>=_rivals[_rivalI].dist){ const r=_rivals[_rivalI]; _rivalI++; // Rival Ghost: overtook a real board score -> celebrate the climb
      pop(kitty.x, kitty.y-70*DPR, "PASSED "+r.name+"! #"+r.rank); snd("newbest"); shake=Math.max(shake,8*DPR); }
    if(distM>2000){ const _sp=Math.hypot(kitty.vx,kitty.vy); if(_sp>10*DPR && parts.length<240) parts.push({x:kitty.x-kitty.vx*0.4,y:kitty.y,vx:0,vy:-0.05*DPR,life:0.45,c:getAccent(0.45),r:(2+Math.random()*2.5)*DPR}); } // deep-run speed trail
    if(ab()==="lightning" && overchargeT>0 && parts.length<260){ for(let i=0;i<2;i++) parts.push({x:kitty.x-kitty.vx*0.35+rand(-6,6)*DPR,y:kitty.y+rand(-9,9)*DPR,vx:0,vy:0,life:0.4,c:i%2?"#7fe7ff":"#ffffff",r:(2.5+Math.random()*3)*DPR}); } // Thunder Dash lightning trail (dash fired via ability key, see activateAbility)
    if(ab()==="phantom" && phaseT>0){ kitty.vx=Math.min(62*DPR, Math.max(kitty.vx, 44*DPR)); if(parts.length<250 && Math.random()<0.6) parts.push({x:kitty.x-kitty.vx*0.3+rand(-6,6)*DPR,y:kitty.y+rand(-10,10)*DPR,vx:0,vy:0,life:0.35,c:Math.random()<0.5?"#9b8cff":"#c7bcff",r:rand(2,4)*DPR}); } // Phantom GHOST RUSH: sustained crazy speed while intangible
    if(ab()==="rocketstart"){ kitty.vx=Math.min(kitty.vx+0.09*DPR, 36*DPR); if(parts.length<240 && Math.random()<0.55) parts.push({x:kitty.x-kitty.vx*0.32+rand(-4,4)*DPR,y:kitty.y+rand(-5,5)*DPR,vx:rand(-4,-1)*DPR,vy:rand(-1,1)*DPR,life:rand(0.25,0.5),c:Math.random()<0.5?"#ff9a3c":"#ffd34d",r:rand(1.5,3.5)*DPR}); } // Bacca ROCKET passive: an afterburner that keeps building forward speed (capped) + a flame trail
    if(ab()==="candy"){ candySpawnT-=1/60; if(candySpawnT<=0){ candySpawnT=rand(2.6,4.2); obstacles.push({type:"candy",x:kitty.x+W+rand(160,420)*DPR,y:GROUND()-rand(130,540)*DPR,r:15*DPR,got:false}); } } // Spookyszn: mystery candies drift in -> eat to spin the Crazy Slots
    if(baccaAxe){ const a=baccaAxe; a.t+=1/60; a.vy+=grav()*DPR*0.85; a.x+=a.vx; a.y+=a.vy; a.spin+=0.55; const gg=GROUND()-6*DPR; if(a.y>gg){ a.y=gg; a.vy=0; a.vx*=0.5; } if(a.x<cam-160*DPR || a.t>6){ baccaAxe=null; } else if(parts.length<220 && Math.random()<0.4) parts.push({x:a.x,y:a.y,vx:0,vy:0,life:0.3,c:"#c9ccd2",r:rand(1,2)*DPR}); } // Bacca axe in flight (shift again = warp to it)
    if(ab()==="chrome" && chromePlates>0 && parts.length<240 && Math.random()<0.5){ const a=Math.random()*6.28, rr2=kitty.r+2*DPR; parts.push({x:kitty.x+Math.cos(a)*rr2,y:kitty.y+Math.sin(a)*rr2,vx:0,vy:0,life:0.25,c:Math.random()<0.5?"#ffffff":"#cfe0ee",r:rand(1.5,3)*DPR}); } // chrome plating: subtle metallic shimmer while plates remain
    if(ab()==="dragon"){ if(flapsLeft<baseFlaps()){ dragonRegen+=1/60; if(dragonRegen>=3){ dragonRegen=0; flapsLeft++; snd("flap"); pop(kitty.x,kitty.y-40*DPR,"+FLAP"); for(let i=0;i<10;i++) parts.push({x:kitty.x,y:kitty.y+6*DPR,vx:rand(-3,3)*DPR,vy:rand(-3,1)*DPR,life:0.5,c:i%2?"#ff9a3c":"#ffd34d",r:rand(2,4)*DPR}); } } else dragonRegen=0; } // dragon: regenerate a wing-flap every 3s (capped at base flaps)
    while(distM>=lastMile+250){ lastMile+=250; pop(kitty.x, kitty.y-44*DPR, "+"+lastMile+"m");
      if(lastMile===2000||lastMile===5000||lastMile===9000){ shake=Math.max(shake,14*DPR); snd("newbest"); pop(kitty.x, kitty.y-72*DPR, lastMile+"m!"); }
      if(lastMile===OVERDRIVE_D){ shake=Math.max(shake,18*DPR); hitstop=Math.max(hitstop,3); flashT=Math.max(flashT,0); snd("newbest"); bannerT=1.2; bannerName="OVERDRIVE"; pop(kitty.x, kitty.y-72*DPR, "OVERDRIVE!"); for(let i=0;i<30;i++) parts.push({x:kitty.x,y:kitty.y,vx:rand(-7,7)*DPR,vy:rand(-7,3)*DPR,life:0.9,c:i%3===0?"#7fe7ff":(i%2?"#b59cff":"#ffd34d"),r:rand(2,5)*DPR}); } } // OVERDRIVE style-shift entry ceremony
    if(!beatBest && prevBest>0 && distM>prevBest){ beatBest=true; bannerT=1; bannerName="NEW HIGH SCORE!"; snd("newbest"); }
    extendWorld(kitty.x+3500*DPR); musicUpdate();
    if(obstacles.length>260) obstacles=obstacles.filter(o=>o.x>cam-700*DPR);
    const cur=Math.hypot(kitty.vx,kitty.vy);
    if(cur<1.1*DPR) stall++; else stall=0;
    if(stall>72) tryEnd();
    if(state==="fly" && GORILLA_TEST && !gauntletUsed.gorilla && distM>30) startGauntlet("gorilla"); // ?gorilla test: jump straight into the boss
    if(state==="fly"){ const _bn=biomeAt(distM).name; if(_bn!==ufoBiomeMark){ ufoBiomeMark=_bn;
      if(!infurnoUsedThisRun && distM>1200 && Math.random()<0.01) startAbduction(); // 1% per new biome past 1200m -> abduction
      else if(distM>1800){ if(!gauntletUsed.fire && Math.random()<0.01) startGauntlet("fire"); else if(!gauntletUsed.water && Math.random()<0.01) startGauntlet("water"); else if(!gauntletUsed.zombie && Math.random()<0.01) startGauntlet("zombie"); else if(!gauntletUsed.gorilla && Math.random()<0.01) startGauntlet("gorilla"); } // 1% per new biome past 1800m, once per type
    } }
  }
  else if(state==="abduct"){
    ufo.k += 1/60;
    if(ufo.phase==="descend"){ // pull BOTH the UFO and the kitty into on-screen positions so the abduction is always visible (the kitty may have soared above the top of the screen)
      const uT=H*0.13, kT=H*0.46; ufo.ufoY += (uT-ufo.ufoY)*0.09; kitty.y += (kT-kitty.y)*0.11; kitty.vx*=0.9; kitty.vy=0;
      if(ufo.k>1.15){ ufo.phase="beam"; ufo.k=0; } }
    else if(ufo.phase==="beam"){ ufo.beam=Math.min(1,ufo.k/0.45); if(ufo.k>0.7){ ufo.phase="suck"; ufo.k=0; } }
    else if(ufo.phase==="suck"){ ufo.beam=1; kitty.y += ((ufo.ufoY+16*DPR)-kitty.y)*0.13; const sc=Math.max(0.18,1-ufo.k*0.8); kitty.sx=kitty.sy=sc; if(ufo.k>0.95) enterInfurno(); }
  }
  else if(state==="redeploy"){ // reverse abduction: the UFO carries you back down + a GET-READY beat before control resumes (no jarring auto-launch out of the secret level)
    ufo.k += 1/60;
    if(ufo.phase==="return"){ ufo.ufoY += (H*0.12-ufo.ufoY)*0.10; ufo.beam=Math.min(1,ufo.k/0.4);
      kitty.y += (ufo.targetY-kitty.y)*0.11; kitty.sx=kitty.sy=Math.min(1,0.2+ufo.k*0.95); kitty.vx=0; kitty.vy=0;
      if(ufo.k>1.2 && Math.abs(kitty.y-ufo.targetY)<34*DPR){ ufo.phase="ready"; ufo.k=0; bannerT=1.2; bannerName="GET READY"; } }
    else if(ufo.phase==="ready"){ kitty.sx=kitty.sy=1; ufo.beam=Math.max(0,1-ufo.k/0.5); ufo.ufoY-=3.2*DPR; // beam retracts, UFO lifts off, brief breathing room
      if(ufo.k>1.05){ kitty.vx=Math.max(ufo.savedVx,18*DPR)+4*DPR; kitty.vy=Math.min(ufo.savedVy,-2*DPR); kitty.rest=false; kitty.sx=1.5; kitty.sy=0.62; shake=Math.max(shake,12*DPR); hitstop=Math.max(hitstop,1); // the UFO TOSSES you back into play with real momentum + a boost (no more dead-drop stall)
        for(let i=0;i<22;i++) parts.push({x:kitty.x-rand(0,24)*DPR,y:kitty.y+rand(-10,10)*DPR,vx:rand(-12,-3)*DPR,vy:rand(-3,3)*DPR,life:rand(0.4,0.8),c:(i%3===0)?"#7fe7ff":(i%2?"#b59cff":"#ffd34d"),r:rand(2.5,5)*DPR});
        snd("spring"); ufo=null; state="fly"; bannerT=0.9; bannerName="GO!"; pop(kitty.x,kitty.y-40*DPR,"BOOST!"); } }
  }
  else if(state==="infurno"){
    if(!infurno.ready){ infurno.hold+=1/60; infurno.p=Math.min(1, infurno.hold/0.8); if(infurno.hold>=15) infurnoGo(); // WAIT on "press any key" (timer + fireballs paused) so Infurno never starts before you're ready; 15s fallback
      kitty.vy=0; kitty.y=(GROUND()-200*DPR)+Math.sin(infurno.hold*2.2)*6*DPR; kitty.spin+=0.01;
      const _imv=(infRight?1:0)-(infLeft?1:0); if(_imv!==0) kitty.x+=_imv*7*DPR; kitty.x=Math.max(40*DPR,Math.min(W-40*DPR,kitty.x));
    } else {
    infurno.t += 1/60; infurno.p=1;
    kitty.vy += 0.34*DPR; kitty.y += kitty.vy;
    { const _imv=(infRight?1:0)-(infLeft?1:0); if(_imv!==0) kitty.x += _imv*7*DPR; kitty.x=Math.max(40*DPR,Math.min(W-40*DPR,kitty.x)); } // L/R control in Infurno only (no auto-drift; the cat holds position when you're not steering)
    kitty.spin += 0.02;
    const g=GROUND();
    if(kitty.y>g-kitty.r){ kitty.y=g-kitty.r; kitty.vy*=-0.42; if(kitty.vy>-2*DPR) kitty.vy=-4*DPR; }
    if(kitty.y<46*DPR){ kitty.y=46*DPR; if(kitty.vy<0) kitty.vy=0; }
    const tl=INFURNO_DUR-infurno.t;
    if(tl>1.2 && Math.random()<0.12) infCoins.push({x:rand(50,W-50),y:-20*DPR,vy:rand(2.4,4.4)*DPR,r:rand(11,16)*DPR,amt:coinAmt((rand(6,13))|0),got:false});
    for(const c of infCoins){ if(c.got) continue; let dx=kitty.x-c.x, dy=kitty.y-c.y, d=Math.hypot(dx,dy);
      if(d<120*DPR){ c.x+=dx*0.09; c.y+=dy*0.09; } c.y+=c.vy; dx=kitty.x-c.x; dy=kitty.y-c.y;
      if(Math.hypot(dx,dy)<kitty.r*1.6+c.r){ c.got=true; const cb=bumpCombo(); infurno.coins+=c.amt; runCoins+=c.amt; coinChime(Math.min(cb-1,12)); popFx(c.x,c.y); } }
    infCoins=infCoins.filter(c=>!c.got && c.y<GROUND()+30*DPR);
    // LETHAL falling fireballs - dodge or your run ends (no longer a free jackpot)
    if(infurno.t>1.0 && tl>1.0 && Math.random()<0.05+infurno.t*0.005) infHaz.push({x:rand(40*DPR,W-40*DPR),y:-30*DPR,vy:rand(4.5,7.5)*DPR,r:rand(13,19)*DPR,wob:rand(0,6.28)});
    for(const h of infHaz){ h.y+=h.vy; h.x+=Math.sin(infurno.t*3+h.wob)*0.7*DPR; // was `t` (render-only var) -> ReferenceError in update() => froze the game when a fireball spawned
      if(Math.hypot(kitty.x-h.x,kitty.y-h.y) < kitty.r + h.r*0.7){ // BURNED -> run ends (coins grabbed so far are kept)
        shake=20*DPR; flashT=1; snd("spike");
        for(let i=0;i<28;i++) parts.push({x:kitty.x,y:kitty.y,vx:rand(-5,5)*DPR,vy:rand(-6,2)*DPR,life:1,c:i%2?"#ff2d4f":"#ff7a1a",r:rand(2,5)*DPR});
        if(savedRun){ cam=savedRun.cam; kitty.x=savedRun.x; kitty.y=Math.min(savedRun.y,GROUND()-40*DPR); kitty.vx=savedRun.vx; kitty.vy=savedRun.vy; }
        kitty.sx=kitty.sy=1; infurno=null; infCoins=[]; infHaz=[]; state="fly"; endRun(); return; } }
    infHaz=infHaz.filter(h=>h.y<GROUND()+40*DPR);
    if(infurno.t>=INFURNO_DUR) exitInfurno();
    }
  }
  else if(state==="gauntlet"){
    const G=gauntlet; if(!G.ready){ G.hold+=1/60; if(G.hold>=15) gauntletGo(); } else { G.t += 1/60; } // WAIT for "press any key" (timer paused, no hazards) so it never starts before you're ready; 15s fallback auto-start so it can't hang
    kitty.vy += 0.34*DPR; kitty.y += kitty.vy;
    { const _mv=(infRight?1:0)-(infLeft?1:0); kitty.x += _mv*7.5*DPR; kitty.x=Math.max(40*DPR,Math.min(W-40*DPR,kitty.x)); }
    const g=GROUND();
    if(kitty.y>g-kitty.r){ kitty.y=g-kitty.r; kitty.vy*=-0.42; if(kitty.vy>-2*DPR) kitty.vy=-4*DPR; }
    if(kitty.y<46*DPR){ kitty.y=46*DPR; if(kitty.vy<0) kitty.vy=0; }
    G.cd -= 1/60;
    if(G.type!=="gorilla" && G.ready && G.cd<=0 && G.t < G.dur-1.2){
      if(G.type==="fire"){
        const warn=Math.max(0.44, 0.55 - G.t*0.010); // telegraph tightens slightly as it heats up, never below a readable 0.44s (stays fair)
        const fx=rand(60*DPR, W-60*DPR);
        gHaz.push({ kind:"fire", x:fx, w:rand(52,82)*DPR, h:rand(240,470)*DPR, t:0, warn, dur:1.0, burst:false }); // bigger, more dramatic pillars (hitbox scales with these)
        if(G.t>3 && Math.random() < Math.min(0.5, 0.15 + (G.t-3)*0.05)){ // TWICE THE HEAT: once it's rolling, a SECOND pillar a fair gap away -> thread the lane or fly over both (prob ramps with time)
          let fx2=rand(60*DPR, W-60*DPR), tries=0; while(Math.abs(fx2-fx)<165*DPR && tries++<8) fx2=rand(60*DPR, W-60*DPR); // keep a survivable lane between the pair
          gHaz.push({ kind:"fire", x:fx2, w:rand(52,82)*DPR, h:rand(240,470)*DPR, t:rand(0,0.10), warn, dur:1.0, burst:false });
        }
        G.cd = Math.max(0.28, 0.70 - G.t*0.045); // pillars come faster and keep coming
      } else if(G.type==="zombie"){
        if(G.t>2 && Math.random()<0.4){ // WRAITH swoops from ABOVE past ~2s -> no safe altitude (floating up is no longer safe)
          const wx=Math.max(50*DPR,Math.min(W-50*DPR, kitty.x + rand(-140,140)*DPR));
          gHaz.push({ kind:"wraith", x:wx, y:-34*DPR, vx:rand(-1,1)*DPR, vy:rand(4.4,6.2)*DPR, r:24*DPR, t:0 });
          G.cd = Math.max(0.5, 1.0 - G.t*0.03);
        } else {
          const hx=rand(60*DPR, W-60*DPR);
          gHaz.push({ kind:"hand", x:hx, w:rand(54,84)*DPR, h:rand(170,320)*DPR, t:0, warn:0.6, dur:1.1, burst:false });
          for(let i=0;i<10;i++) parts.push({x:hx+rand(-20,20)*DPR,y:g,vx:rand(-2,2)*DPR,vy:rand(-4,-0.5)*DPR,life:0.6,c:i%2?"#5a4a34":"#2e2418",r:rand(2,5)*DPR});
          G.cd = Math.max(0.40, 0.90 - G.t*0.03);
        }
      } else {
        G.side ^= 1; const fromL = G.side===0;
        const sx = fromL ? -30*DPR : W+30*DPR, sy=g-10*DPR, grav=0.55*DPR;
        let vx, vy;
        if(Math.random()<0.5){ // HUNTER: leap computed to arrive at the cat's CURRENT position at apex -> no safe camping at the roof/corner (was: sit at the top = invincible). Aims where you WERE, so dodge by moving.
          const tX=Math.max(70*DPR,Math.min(W-70*DPR,kitty.x)), tY=Math.max(46*DPR, kitty.y-8*DPR);
          const rise=Math.max(60*DPR, sy - tY); vy=-Math.sqrt(2*grav*rise); const tAp=Math.abs(vy)/grav; vx=(tX-sx)/Math.max(1,tAp);
        } else { // ROAMER: low->mid arc at a random spot; dodge by rising over it
          const tx=rand(W*0.25, W*0.75); vx=(tx-sx)/rand(58,74); vy=rand(-15,-21)*DPR;
        }
        gHaz.push({ kind:"shark", x:sx, y:sy, vx, vy, r:26*DPR, t:0, side:fromL?1:-1 });
        for(let i=0;i<14;i++) parts.push({x:sx,y:g,vx:rand(-2,2)*DPR,vy:rand(-4,-0.5)*DPR,life:0.7,c:i%2?"#bfefff":"#7fd0ff",r:rand(2,4)*DPR});
        G.cd = Math.max(0.72, 1.30 - G.t*0.028);                                      // fewer sharks (was 0.40/0.95) = not "fast af"
      }
    }
    if(G.type==="gorilla" && G.ready && gorilla){ const GO=gorilla;
      const SCROLL=Math.min(4.2, 2.3 + G.t*0.028)*DPR; // MOVING LEVEL: scrolls at you (gentle ramp) -> pillars drift past, readable/fair
      GO.facing = kitty.x>=GO.x ? 1 : -1; GO.x += Math.sign(kitty.x-GO.x)*(3.4 + G.t*0.2)*DPR; GO.x=Math.max(GO.r*0.5,Math.min(W*0.46,GO.x)); // chases from BEHIND (capped to the left half) so it can't camp directly under you; falling back left into it = grabbed
      if(Math.abs(kitty.x-GO.x) < GO.r*0.8 && kitty.y > g - GO.r*1.25){ shake=24*DPR; flashT=1; snd("spike"); for(let i=0;i<30;i++) parts.push({x:kitty.x,y:kitty.y,vx:rand(-6,6)*DPR,vy:rand(-7,2)*DPR,life:1,c:i%2?"#6b4a2a":"#c0c0c8",r:rand(2,5)*DPR}); if(savedRun){ cam=savedRun.cam; kitty.x=savedRun.x; kitty.y=Math.min(savedRun.y,GROUND()-40*DPR); kitty.vx=savedRun.vx; kitty.vy=savedRun.vy; } kitty.sx=kitty.sy=1; gHaz=[]; gauntlet=null; gorilla=null; state="fly"; endRun(); return; } // only grabs when you dip LOW into its reach - the scrolling pillars are what stop camping now
      // scrolling temple PILLARS - fly through the gap; ceiling+floor columns so you can't camp any altitude
      GO.pcd = (GO.pcd||0) - 1/60;
      if(GO.pcd<=0){ const gapH=Math.max(230*DPR,(288 - G.t*1.6)*DPR), gyMax=Math.max(210*DPR, g-155*DPR-gapH/2), gapY=rand(120*DPR, Math.max(160*DPR,gyMax)); gHaz.push({kind:"pillar",x:W+70*DPR,gapY,gapH,w:rand(44,62)*DPR,vx:-SCROLL,t:0}); for(let ci=-1;ci<=1;ci++) gHaz.push({kind:"gcoin",x:W+70*DPR,y:gapY+ci*46*DPR,vx:-SCROLL,r:14*DPR,amt:3,got:false,t:0}); GO.pcd=Math.max(1.1, 1.9 - G.t*0.035); } // BIG forgiving gaps + more spacing; a coin trail through each gap rewards threading (banked into your run)
      GO.acd -= 1/60;
      if(GO.state==="walk"){ if(GO.acd<=0){ GO.state="beat"; GO.anim=0; GO.next = "pound"; snd("reboot"); } } // pound only - the aimed rock was an unavoidable hit while you're committed to a pillar gap // chest-beat TELEGRAPH before every attack (fair warning)
      else if(GO.state==="beat"){ GO.anim += (1/60)/0.7; if(GO.anim>=1){ GO.state=GO.next; GO.anim=0; } }
      else { GO.anim += (1/60)/(GO.state==="throw"?0.75:0.65);
        if(GO.anim>=1){
          if(GO.state==="throw"){ const sx=GO.x+GO.facing*GO.r*0.3, sy=g-GO.r*1.5, grav=0.5*DPR, rise=Math.max(70*DPR, sy-kitty.y), vy=-Math.sqrt(2*grav*rise), tAp=Math.abs(vy)/grav, vx=(kitty.x-sx)/Math.max(1,tAp); gHaz.push({kind:"rock",x:sx,y:sy,vx,vy,r:22*DPR,t:0,grav}); snd("throw"); shake=Math.max(shake,8*DPR); }
          else { gHaz.push({kind:"shock",x:GO.x,y:g,vx:9*DPR,r:22*DPR,t:0}); gHaz.push({kind:"shock",x:GO.x,y:g,vx:-9*DPR,r:22*DPR,t:0}); snd("reboot"); shake=Math.max(shake,16*DPR); for(let i=0;i<20;i++) parts.push({x:GO.x,y:g,vx:rand(-8,8)*DPR,vy:rand(-7,-1)*DPR,life:0.7,c:i%2?"#6b4a2a":"#8a7a5a",r:rand(2,5)*DPR}); }
          GO.state="walk"; GO.anim=0; GO.acd = Math.max(0.7, 1.7 - G.t*0.06); // attacks come more often as it rages
        }
      }
    }
    for(const h of gHaz){
      if(h.kind==="fire"){
        h.t += 1/60;
        if(!h.burst && h.t>=h.warn){ h.burst=true; shake=Math.max(shake,8*DPR); snd("flap");
          for(let i=0;i<16;i++) parts.push({x:h.x+rand(-h.w/2,h.w/2),y:g,vx:rand(-2,2)*DPR,vy:rand(-7,-2)*DPR,life:0.8,c:i%2?"#ff7a1a":"#ffd34d",r:rand(2,5)*DPR}); }
        if(h.t>=h.warn){
          const top = g - h.h*Math.min(1,(h.t-h.warn)/0.18);
          if(Math.abs(kitty.x-h.x) < h.w/2 + kitty.r*0.6 && kitty.y+kitty.r > top){
            shake=22*DPR; flashT=1; snd("spike");
            for(let i=0;i<30;i++) parts.push({x:kitty.x,y:kitty.y,vx:rand(-5,5)*DPR,vy:rand(-6,2)*DPR,life:1,c:i%2?"#ff2d4f":"#ff7a1a",r:rand(2,5)*DPR});
            if(savedRun){ cam=savedRun.cam; kitty.x=savedRun.x; kitty.y=Math.min(savedRun.y,GROUND()-40*DPR); kitty.vx=savedRun.vx; kitty.vy=savedRun.vy; }
            kitty.sx=kitty.sy=1; gHaz=[]; gauntlet=null; state="fly"; endRun(); return;
          }
        }
      } else if(h.kind==="hand"){
        h.t += 1/60;
        if(!h.burst && h.t>=h.warn){ h.burst=true; shake=Math.max(shake,8*DPR); for(let i=0;i<14;i++) parts.push({x:h.x+rand(-h.w/2,h.w/2),y:g,vx:rand(-2,2)*DPR,vy:rand(-6,-1)*DPR,life:0.7,c:i%2?"#5a4a34":"#2e2418",r:rand(2,5)*DPR}); } // Grave Danger: eruption sound removed (Diggy: not good) - visual burst only
        if(h.t>=h.warn){ const top = g - h.h*Math.min(1,(h.t-h.warn)/0.18);
          if(Math.abs(kitty.x-h.x) < h.w/2 + kitty.r*0.6 && kitty.y+kitty.r > top){ shake=22*DPR; flashT=1; snd("spike"); for(let i=0;i<30;i++) parts.push({x:kitty.x,y:kitty.y,vx:rand(-5,5)*DPR,vy:rand(-6,2)*DPR,life:1,c:i%2?"#7bae3a":"#3a2f22",r:rand(2,5)*DPR}); if(savedRun){ cam=savedRun.cam; kitty.x=savedRun.x; kitty.y=Math.min(savedRun.y,GROUND()-40*DPR); kitty.vx=savedRun.vx; kitty.vy=savedRun.vy; } kitty.sx=kitty.sy=1; gHaz=[]; gauntlet=null; state="fly"; endRun(); return; } }
      } else if(h.kind==="rock"){
        h.t += 1/60; h.vy += (h.grav||0.5*DPR); h.x += h.vx; h.y += h.vy;
        if(Math.hypot(kitty.x-h.x, kitty.y-h.y) < kitty.r + h.r*0.8){ shake=22*DPR; flashT=1; snd("spike"); for(let i=0;i<26;i++) parts.push({x:kitty.x,y:kitty.y,vx:rand(-5,5)*DPR,vy:rand(-6,2)*DPR,life:1,c:i%2?"#6b4a2a":"#a89880",r:rand(2,5)*DPR}); if(savedRun){ cam=savedRun.cam; kitty.x=savedRun.x; kitty.y=Math.min(savedRun.y,GROUND()-40*DPR); kitty.vx=savedRun.vx; kitty.vy=savedRun.vy; } kitty.sx=kitty.sy=1; gHaz=[]; gauntlet=null; gorilla=null; state="fly"; endRun(); return; }
      } else if(h.kind==="shock"){
        h.t += 1/60; h.x += h.vx;
        if(Math.abs(kitty.x-h.x) < h.r + kitty.r*0.4 && kitty.y+kitty.r > g - 30*DPR){ shake=20*DPR; flashT=1; snd("spike"); for(let i=0;i<24;i++) parts.push({x:kitty.x,y:g,vx:rand(-6,6)*DPR,vy:rand(-7,-1)*DPR,life:0.9,c:i%2?"#8a7a5a":"#c0c0c8",r:rand(2,5)*DPR}); if(savedRun){ cam=savedRun.cam; kitty.x=savedRun.x; kitty.y=Math.min(savedRun.y,GROUND()-40*DPR); kitty.vx=savedRun.vx; kitty.vy=savedRun.vy; } kitty.sx=kitty.sy=1; gHaz=[]; gauntlet=null; gorilla=null; state="fly"; endRun(); return; }
      } else if(h.kind==="wraith"){
        h.t += 1/60;
        h.vx += Math.sign(kitty.x-h.x)*0.06*DPR; h.vx=Math.max(-3.2*DPR,Math.min(3.2*DPR,h.vx)); // gentle homing -> floating still up top is no longer safe
        h.x += h.vx + Math.sin(h.t*4 + h.y*0.02)*0.9*DPR; h.y += h.vy;                            // eerie weave as it descends
        if(Math.hypot(kitty.x-h.x, kitty.y-h.y) < kitty.r + h.r*0.8){
          shake=22*DPR; flashT=1; snd("spike");
          for(let i=0;i<30;i++) parts.push({x:kitty.x,y:kitty.y,vx:rand(-5,5)*DPR,vy:rand(-6,2)*DPR,life:1,c:i%2?"#7bff9a":"#3a2f22",r:rand(2,5)*DPR});
          if(savedRun){ cam=savedRun.cam; kitty.x=savedRun.x; kitty.y=Math.min(savedRun.y,GROUND()-40*DPR); kitty.vx=savedRun.vx; kitty.vy=savedRun.vy; }
          kitty.sx=kitty.sy=1; gHaz=[]; gauntlet=null; state="fly"; endRun(); return;
        }
      } else if(h.kind==="pillar"){
        h.t += 1/60; h.x += h.vx;
        if(Math.abs(kitty.x-h.x) < h.w/2 + kitty.r*0.55 && (kitty.y-kitty.r < h.gapY-h.gapH/2 || kitty.y+kitty.r > h.gapY+h.gapH/2)){
          shake=22*DPR; flashT=1; snd("spike");
          for(let i=0;i<26;i++) parts.push({x:kitty.x,y:kitty.y,vx:rand(-5,5)*DPR,vy:rand(-6,2)*DPR,life:1,c:i%2?"#6b6157":"#3a2f22",r:rand(2,5)*DPR});
          if(savedRun){ cam=savedRun.cam; kitty.x=savedRun.x; kitty.y=Math.min(savedRun.y,GROUND()-40*DPR); kitty.vx=savedRun.vx; kitty.vy=savedRun.vy; }
          kitty.sx=kitty.sy=1; gHaz=[]; gauntlet=null; gorilla=null; state="fly"; endRun(); return;
        }
      } else if(h.kind==="gcoin"){
        h.t=(h.t||0)+1/60; h.x += h.vx;
        if(!h.got && Math.hypot(kitty.x-h.x, kitty.y-h.y) < kitty.r + h.r){ h.got=true; const cb=bumpCombo(); const amt=coinAmt(h.amt); runCoins+=amt; coinChime(Math.min(cb-1,12)); popFx(h.x,h.y); pop(h.x,h.y-20*DPR,"+"+amt); const ce=document.getElementById("coins"); if(ce) ce.textContent=save.coins+runCoins; }
      } else {
        h.t += 1/60; h.vy += 0.55*DPR; h.x += h.vx; h.y += h.vy;
        if(Math.hypot(kitty.x-h.x, kitty.y-h.y) < kitty.r + h.r*0.8){
          shake=22*DPR; flashT=1; snd("spike");
          for(let i=0;i<30;i++) parts.push({x:kitty.x,y:kitty.y,vx:rand(-5,5)*DPR,vy:rand(-6,2)*DPR,life:1,c:i%2?"#ff2d4f":"#bfefff",r:rand(2,5)*DPR});
          if(savedRun){ cam=savedRun.cam; kitty.x=savedRun.x; kitty.y=Math.min(savedRun.y,GROUND()-40*DPR); kitty.vx=savedRun.vx; kitty.vy=savedRun.vy; }
          kitty.sx=kitty.sy=1; gHaz=[]; gauntlet=null; state="fly"; endRun(); return;
        }
      }
    }
    gHaz=gHaz.filter(h=> (h.kind==="fire"||h.kind==="hand") ? h.t < h.warn+h.dur : (h.kind==="pillar") ? h.x>-100*DPR : (h.kind==="gcoin") ? (!h.got && h.x>-60*DPR) : (h.y < GROUND()+60*DPR && h.x>-80*DPR && h.x<W+80*DPR));
    if(G.t>=G.dur) endGauntlet(true);
  }
  for(const p of parts){ p.x+=p.vx; p.y+=p.vy; p.vy+=0.12*DPR; p.life-=0.022; }
  parts=parts.filter(p=>p.life>0);
  for(const q of popTexts){ q.y-=0.6*DPR; q.life-=0.012; }
  popTexts=popTexts.filter(q=>q.life>0);
  document.getElementById("dist").innerHTML=distM+'<span class="u">m</span>';
  document.getElementById("best").innerHTML=Math.max(save.best,distM)+'<span class="u">m</span>';
  document.getElementById("coins").textContent=save.coins+runCoins;
}
function dust(x,y){ for(let i=0,n=Math.round(10*JF());i<n;i++) parts.push({x:x+rand(-14,14)*DPR,y,vx:rand(-2,2)*DPR,vy:rand(-2.4,-.3)*DPR,life:1,c:"#d8d0c0",r:rand(2,4)*DPR}); }
function popFx(x,y){ for(let i=0;i<18;i++) parts.push({x,y,vx:rand(-4,4)*DPR,vy:rand(-4,4)*DPR,life:1,c:i%2?"#ffd36b":"#7fe7ff",r:rand(2,4.5)*DPR}); }
function tokenFx(x,y){ for(let i=0;i<22;i++) parts.push({x,y,vx:rand(-4,4)*DPR,vy:rand(-4,4)*DPR,life:1,c:i%2?"#22d3a0":"#bfffe9",r:rand(2,4.5)*DPR}); shake=6*DPR; }
function pop(x,y,txt){ popTexts.push({x,y,txt,life:1}); }
function JF(){ return 1+Math.min(1.5, distM/4000); } // juice factor: escalate fx the deeper the run goes
function bumpCombo(){ const now=performance.now(); if(now-comboT>1000) comboN=0; comboN++; comboT=now; return comboN; }
function grenadeFx(x,y){ for(let i=0,n=Math.round(26*JF());i<n;i++) parts.push({x,y,vx:rand(-6,9)*DPR,vy:rand(-5,5)*DPR,life:1,c:i%3?"#ff7a1a":(i%2?"#ffd34d":"#9a9a9a"),r:rand(2,5)*DPR}); shake=Math.max(shake,12*DPR); }
function boltFx(x,y){ for(let i=0;i<22;i++) parts.push({x,y,vx:rand(-5,5)*DPR,vy:rand(-5,5)*DPR,life:1,c:i%2?"#2b7bff":"#bfe0ff",r:rand(2,4)*DPR}); shake=7*DPR; pop(x,y,"FLAPS+"); }
function rocketFx(x,y){ for(let i=0,n=Math.round(34*JF());i<n;i++) parts.push({x,y,vx:rand(-7,9)*DPR,vy:rand(-7,5)*DPR,life:1,c:i%3?"#ff7a1a":(i%2?"#ffd34d":"#fff2c2"),r:rand(2,6)*DPR}); shake=16*DPR; }
function updraftFx(x,y){ for(let i=0,n=Math.round(24*JF());i<n;i++) parts.push({x:x+rand(-16,16)*DPR,y,vx:rand(-3,3)*DPR,vy:rand(-10,-3)*DPR,life:1,c:i%2?"#bfeaff":"#ffffff",r:rand(2,4.5)*DPR}); shake=Math.max(shake,10*DPR); }

function tryEnd(){ if(state!=="fly") return; if(lives>0) reboot(); else offerRevive(); }
// ===== PAID REVIVE: coin-bought continue when out of lives. Reuses the in-game "Redeem a coin code" economy (/sccoins). A revived run banks coins AND is leaderboard-eligible (Diggy's call): if the continued run beats your best it updates save.best and posts like any other. =====
const REVIVE_COSTS=[500,1200,1800], REVIVE_CAP=3, REVIVE_DAILY_CAP=25; // [TUNE] cost of the 1st/2nd/3rd continue in a run; REVIVE_DAILY_CAP = anti-cheat ceiling on TOTAL revives bought per local day
function reviveCost(){ return REVIVE_COSTS[Math.min(revivesUsed, REVIVE_COSTS.length-1)]; }
function revBoughtToday(){ const today=scDayIndex(); if(save.revDay!==today){ save.revDay=today; save.revBought=0; } return save.revBought||0; } // rolls over at local midnight (same boundary as daily catnip / quests)
function offerRevive(){
  const worth = distM >= Math.max(400, Math.floor(save.best*0.6)); // only offer the revive on a run WORTH saving -> short/early deaths restart instantly (keeps the fast keep-playing loop)
  if(!worth || revivesUsed>=REVIVE_CAP || revBoughtToday()>=REVIVE_DAILY_CAP || save.coins<reviveCost()){ endRun(); return; } // not worth it / per-run capped / DAILY cap hit / can't afford -> normal instant end
  state="dying"; const cost=reviveCost(); // "dying" freezes the sim (update() only advances in fly) while the prompt is up
  const c=document.getElementById("reviveCost"), b=document.getElementById("reviveBal"), d=document.getElementById("reviveDist");
  if(c) c.textContent=cost; if(b) b.textContent=save.coins; if(d) d.textContent=distM;
  show("reviveCard",true); snd("token");
  let t=6; const cd=document.getElementById("reviveCd"); if(cd) cd.textContent=t; // arcade auto-decline countdown = urgency
  clearInterval(reviveTimer); reviveTimer=setInterval(()=>{ t--; if(cd) cd.textContent=Math.max(0,t); if(t<=0) declineRevive(); },1000);
}
function doRevive(){ const cost=reviveCost(); if(save.coins<cost || revBoughtToday()>=REVIVE_DAILY_CAP){ declineRevive(); return; }
  clearInterval(reviveTimer); save.coins-=cost; revivesUsed++; save.revBought=revBoughtToday()+1; runRevived=true; persist(); updateCoinHUD();
  show("reviveCard",false); lives=1; state="fly"; reboot(); } // reboot() spends the life (->0) + relaunches from the ground to keep the SAME run going
function declineRevive(){ clearInterval(reviveTimer); show("reviveCard",false); endRun(); }
function reboot(){ lives--; stall=0; flapsLeft=baseFlaps(); if(ab()==="teleport") abUses=ACTIVE_AB.teleport.uses; snd("reboot"); stopBark(); rebootFx={ x:kitty.x, y:GROUND(), t:1 };
  kitty.y=GROUND()-30*DPR; kitty.rest=false; const a=46*Math.PI/180, p=cfg.power*0.82*DPR;
  kitty.vx=Math.cos(a)*p; kitty.vy=-Math.sin(a)*p; shake=16*DPR;
  for(let i=0;i<30;i++) parts.push({x:kitty.x,y:GROUND(),vx:rand(-1,5)*DPR,vy:rand(-5,1)*DPR,life:1,c:i%2?"#7fe7ff":"#ffffff",r:rand(2,5)*DPR}); }
function endRun(){
  if(state!=="fly" && state!=="dying") return; // "dying" = the revive prompt was up; a decline/timeout must still finalize the run (else you're stuck staring at a dead kitty)
  state="result";
  const coinMult = ab()==="greedy"?5:(ab()==="goldcat"?2:1); // Queen=5x, goldcat=2x
  const baseCoins = Math.floor(distM/23) + (coinMult>1 ? Math.round(runCoins/coinMult) : runCoins); // raw (pre-multiplier) total: distance bonus (1 coin / 23m - trimmed from 20m to make coins a tad rarer) + un-multiplied pickups
  const earned = baseCoins*coinMult; save.coins+=earned; runCoins=0; // multiplier now applies to EVERYTHING incl. the distance bonus (was: pickups only). Bank then ZERO runCoins (shop opens from result without reset())
  const wasBest=save.best, isBest=distM>0 && distM>wasBest; if(isBest) save.best=distM; // revived (paid-continue) runs DO count now (Diggy's call) -> a revived run that beats your best ranks like any other
  const hsBonus = isBest ? (wasBest>0 ? Math.min(1000, 150+Math.round((distM-wasBest)*0.5)) : 150) : 0; // beat your best -> bonus coins (scales with margin, first record = flat 150)
  if(hsBonus) save.coins+=hsBonus;
  save.runs=(save.runs||0)+1; save.totalDist=(save.totalDist||0)+distM; save.coinsEarned=(save.coinsEarned||0)+earned+hsBonus; // lifetime stats feed quests + secret-skin unlocks (save.runs was never incremented before)
  questOnRun(distM, earned+hsBonus); persist(); checkUnlocks(); // 1 coin / 20m + pickup bonus; then auto-grant/announce any secret skin whose achievement just completed
  const nearMiss=!isBest && prevBest>=120 && distM<prevBest && distM>=Math.floor(prevBest*0.9); // within 10% of a real best (>=120m so tiny scores don't spam it)
  document.getElementById("resultWhere").textContent=biomeAt(distM).name;
  { const _de=document.getElementById("resultDist"); if(_de){ _de.textContent="0"; setTimeout(function(){ countUp(_de, distM); }, 200); } } // hero count-up after panel scale-in
  const msgEl=document.getElementById("resultMsg"); msgEl.className="r-msg"+(isBest?" best":(nearMiss?" near":""));
  msgEl.textContent = isBest ? "NEW BEST! \u{1F3C6}" : (nearMiss ? "SO CLOSE — "+(prevBest-distM)+"m short!" : pickMsg(distM));
  const bestEl=document.getElementById("resultBest"); bestEl.className="r-best"+(isBest?" nb":""); bestEl.textContent= isBest? ("⭐ NEW RECORD  +"+hsBonus+" ⛁") : ("BEST "+save.best+"m");
  { const _ce=document.getElementById("resultCoins"); if(coinMult>1 && baseCoins>0) countUpMult(_ce, baseCoins, coinMult); else countUp(_ce, earned, "+"); }
  showResultRank(distM, isBest);
  document.getElementById("resultGoal").innerHTML=nextSkinTeaser();
  document.getElementById("best").innerHTML=save.best+'<span class="u">m</span>';
  const _ab=document.getElementById("againBtn"); if(_ab) _ab.classList.toggle("sc-pulse", nearMiss); // pulse AGAIN on a near-miss
  { const _rp=document.querySelector("#resultCard .rpanel"); if(_rp){ _rp.classList.toggle("is-best", isBest); _rp.classList.toggle("is-near", nearMiss); } } // gold-hero + burst on a new best
  setTimeout(function(){ snd(isBest?"newbest":"coin"); },170);
  if(nearMiss) setTimeout(function(){ try{ snd("spring"); }catch(e){} },360); // soft "so close" cue
  refreshQuestBtn(); // pulse the QUESTS button on the result card if a mission is ready to claim
  show("resultCard",true);
  if(isBest) rConfetti();
}
function rConfetti(){ const host=document.getElementById("resultCard"); if(!host) return;
  if(window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
  const old=host.querySelector(".r-confetti"); if(old) old.remove();
  const layer=document.createElement("div"); layer.className="r-confetti"; host.appendChild(layer);
  const ac=(getComputedStyle(document.documentElement).getPropertyValue("--ac")||"").trim()||"#22d3a0";
  const cols=[ac,"#ffd34d","#ffe27a","#ffffff"];
  for(let i=0;i<18;i++){ const p=document.createElement("i");
    const x=46+Math.random()*8, dx=(Math.random()*2-1)*180, rot=(Math.random()*2-1)*540, dur=1.1+Math.random()*0.8, delay=Math.random()*0.18;
    p.style.cssText="left:"+x+"%;top:34%;background:"+cols[i%cols.length]+";--dx:"+dx+"px;--rot:"+rot+"deg;animation-duration:"+dur+"s;animation-delay:"+delay+"s";
    layer.appendChild(p); }
  setTimeout(function(){ layer.remove(); }, 2200); }
function countUp(el,to,prefix){ if(!el) return; clearInterval(el._iv); let cur=0; const step=Math.max(1,Math.ceil(to/26)); el.textContent=(prefix||"")+"0"; if(to<=0) return;
  el._iv=setInterval(function(){ cur=Math.min(to,cur+step); el.textContent=(prefix||"")+cur; if(cur>=to) clearInterval(el._iv); },26); }
// Queen / goldcat showcase: count to the BASE total, then reveal "xN" and climb to the multiplied total (you SEE the multiplier pay off)
function countUpMult(el, base, mult){ if(!el) return; clearInterval(el._iv);
  const badge=document.getElementById("resultMult"); if(badge) badge.style.display="none";
  el.style.color=""; const final=base*mult; let cur=0; const step=Math.max(1,Math.ceil(base/20));
  el.textContent="+0";
  el._iv=setInterval(function(){ cur=Math.min(base,cur+step); el.textContent="+"+cur;
    if(cur>=base){ clearInterval(el._iv);
      if(badge){ badge.textContent=(mult===5?"\u{1F451} QUEEN'S CUT  x5":"x"+mult+" COINS"); badge.style.display="block";
        try{ badge.animate([{transform:"scale(.4)",opacity:0},{transform:"scale(1.18)",opacity:1},{transform:"scale(1)",opacity:1}],{duration:440,easing:"cubic-bezier(.22,1,.36,1)"}); }catch{} }
      el.style.color="#ffd34d"; try{ el.animate([{transform:"scale(1)"},{transform:"scale(1.35)"},{transform:"scale(1)"}],{duration:520,easing:"cubic-bezier(.22,1,.36,1)"}); }catch{}
      snd("newbest");
      let c2=base; const step2=Math.max(1,Math.ceil((final-base)/18));
      el._iv=setInterval(function(){ c2=Math.min(final,c2+step2); el.textContent="+"+c2; if(c2>=final) clearInterval(el._iv); },28);
    }
  },24); }
function nextSkinTeaser(){ let best=null; for(const sk of skinList){ if(sk.id==="default"||sk.secret||save.owned.includes(sk.id)) continue; if(best===null||sk.cost<best.cost) best=sk; }
  if(!best) return "✨ You own every Sprite!"; const need=best.cost-save.coins;
  return need<=0 ? ("✨ You can afford <b>"+best.name+"</b> — tap SHOP") : ("<b>"+need+"</b> coins to <b>"+best.name+"</b>"); }
function refreshStartCard(){ // best-score badge + collection teaser on the start screen (only when relevant)
  const bb=document.getElementById("scBestBadge"); if(bb){ if(save.best>0){ document.getElementById("scBestVal").textContent=save.best+"m"; bb.hidden=false; } else bb.hidden=true; }
  const cb=document.getElementById("scCollBadge"); if(cb){ const list=skinList.filter(s=>!s.secret); const owned=list.filter(s=>s.id==="default"||save.owned.includes(s.id)).length;
    if(list.length>1){ document.getElementById("scCollVal").textContent=owned+"/"+list.length; cb.hidden=false; } else cb.hidden=true; }
  refreshDaily(); refreshQuestBtn(); }
function pickMsg(d){ if(d<60) return "Barely cleared the cannon..."; if(d<200) return "Decent launch!"; if(d<400) return "Now we're flying!"; return "MONSTER distance!"; }

// ===== shared reward + coin helpers =====
function updateCoinHUD(){ const c=document.getElementById("coins"); if(c) c.textContent=save.coins+runCoins; }
function showReward(title, body, sound){ const rt=document.getElementById("rewardTitle"), rb=document.getElementById("rewardBody"); if(rt) rt.textContent=title; if(rb) rb.textContent=body; show("rewardCard",true); try{ snd(sound||"newbest"); }catch(e){} }
function jackpotFx(x,y,mega){ const n=mega?60:34; for(let i=0;i<n;i++) parts.push({x,y,vx:rand(-7,7)*DPR,vy:rand(-11,2)*DPR,life:rand(0.6,1.2),c:i%3?"#ffd34d":(i%2?"#fff2c0":"#ff8a3c"),r:rand(2,6)*DPR}); shake=Math.max(shake,(mega?20:13)*DPR); if(mega) flashT=Math.max(flashT,0.7); }

// ===== DAILY CATNIP: escalating daily-claim rewards, reset on a missed day (habit hook) =====
function scDayIndex(){ const d=new Date(); return Math.floor((d.getTime() - d.getTimezoneOffset()*60000)/86400000); } // local-day index
const DAILY_TIERS=[0,60,90,120,180,260,360]; // reward for streak day 1..6; day 7+ = guaranteed jackpot
function dailyReward(day){ return day>=7 ? 500 : (DAILY_TIERS[day]||60); }
function dailyState(){ const today=scDayIndex(), d=save.daily||{streak:0,lastClaim:0};
  if(d.lastClaim===today) return { claimed:true, day:d.streak||1, streak:d.streak||1 };
  const cont=(d.lastClaim===today-1), streak=cont ? (d.streak||0)+1 : 1; // consecutive day -> grow; gap -> reset to day 1
  return { claimed:false, day:streak, streak:streak }; }
function claimDaily(){ const st=dailyState(); if(st.claimed) return; const amt=dailyReward(st.day);
  save.daily={ streak:st.streak, lastClaim:scDayIndex() }; save.coins+=amt; persist(); updateCoinHUD();
  showReward("\u{1F43E} DAILY CATNIP", "Day "+st.streak+" streak · +"+amt+" coins"+(st.day>=7?"  — JACKPOT!":""), "catnip");
  refreshDaily(); }
function refreshDaily(){ const el=document.getElementById("scDaily"); if(!el) return; const st=dailyState();
  if(!st.claimed){ el.hidden=false; el.classList.add("sc-pulse"); el.style.cursor="pointer";
    el.innerHTML="\u{1F43E} DAILY CATNIP · Day "+st.day+" · +"+dailyReward(st.day)+" ⛁ — CLAIM"; }
  else { el.hidden=false; el.classList.remove("sc-pulse"); el.style.cursor="default";
    el.innerHTML="\u{1F43E} "+st.streak+"-day streak · back tomorrow for more"; } }

// ===== QUESTS: 3 rotating missions, coins for tasks; claim -> respawn a fresh one =====
const QUEST_DEFS=[
  { type:"dist_run",   pick:[300,500,800,1200,1800], reward:t=>Math.round(t*0.13)+20, label:q=>"Reach "+q.target+"m in one run" },
  { type:"runs",       pick:[3,5,8,12],              reward:t=>t*19,                  label:q=>"Play "+q.target+" runs" },
  { type:"coins",      pick:[600,1200,2500],         reward:t=>Math.round(t*0.17),    label:q=>"Earn "+q.target+" coins" },
  { type:"dist_total", pick:[2500,5000,9000],        reward:t=>Math.round(t*0.06),    label:q=>"Fly "+q.target+"m total" },
  { type:"skins",      pick:[1],                     reward:t=>150,                   label:q=>"Unlock a new skin" }
];
function genQuest(avoid){ const _maxSkins=skinList.filter(s=>s.id!=="default").length, _skinsDead=ownedSkinCount()>=_maxSkins; // a "skins" quest is uncompletable once every skin is owned -> never roll it then
  const _ok=d=>(!avoid||avoid.indexOf(d.type)<0) && !(d.type==="skins"&&_skinsDead);
  let pool=QUEST_DEFS.filter(_ok); if(!pool.length) pool=QUEST_DEFS.filter(d=>!(d.type==="skins"&&_skinsDead)); if(!pool.length) pool=QUEST_DEFS;
  const d=pool[Math.floor(Math.random()*pool.length)]; const base=d.pick[Math.floor(Math.random()*d.pick.length)];
  const q={ type:d.type, target:base, reward:d.reward(base), prog:0 };
  if(d.type==="skins"){ q.target=ownedSkinCount()+base; q.prog=ownedSkinCount(); }
  return q; }
function ensureQuests(){ const today=scDayIndex();
  if(save.questDay!==today || !Array.isArray(save.quests) || save.quests.length<3){ const qs=[]; while(qs.length<3) qs.push(genQuest(qs.map(q=>q.type))); save.quests=qs; save.questDay=today; persist(); } // exactly 3 quests per day; fresh set at local midnight
  for(const q of save.quests){ if(q.type==="skins" && !q.claimed) q.prog=Math.max(q.prog||0,ownedSkinCount()); } }
function questLabel(q){ const d=QUEST_DEFS.find(x=>x.type===q.type); return d?d.label(q):"Mission"; }
function questDone(q){ return (q.prog||0)>=q.target; }
function anyQuestDone(){ ensureQuests(); return save.quests.some(q=>!q.claimed && questDone(q)); }
function msToQuestReset(){ const d=new Date(); const ms=d.getTime()-d.getTimezoneOffset()*60000, dayMs=86400000; return dayMs-(ms%dayMs); } // ms to next LOCAL midnight (matches scDayIndex's day boundary)
function fmtCountdown(ms){ ms=Math.max(0,ms); const s=Math.floor(ms/1000),h=Math.floor(s/3600),m=Math.floor((s%3600)/60),ss=s%60; const p=function(n){return(n<10?"0":"")+n;}; return p(h)+":"+p(m)+":"+p(ss); }
function questOnRun(dist, earned){ ensureQuests();
  for(const q of save.quests){ if(q.claimed) continue;
    if(q.type==="dist_run") q.prog=Math.max(q.prog||0, dist);
    else if(q.type==="runs") q.prog=(q.prog||0)+1;
    else if(q.type==="coins") q.prog=(q.prog||0)+earned;
    else if(q.type==="dist_total") q.prog=(q.prog||0)+dist;
    else if(q.type==="skins") q.prog=ownedSkinCount();
  } persist(); }
function claimQuest(i){ ensureQuests(); const q=save.quests[i]; if(!q||q.claimed||!questDone(q)) return; const r=q.reward;
  save.coins+=r; q.claimed=true; persist(); updateCoinHUD();
  showReward("✅ QUEST COMPLETE", "+"+r+" coins", "catnip"); buildQuests(); refreshQuestBtn(); } // claimed for the day -> the slot now counts down to the next reset (no instant respawn)
function refreshQuestBtn(){ const claimable=anyQuestDone();
  ["questBtn","quest2Btn"].forEach(id=>{ const b=document.getElementById(id); if(b) b.classList.toggle("sc-pulse",claimable); }); }
function buildQuests(){ ensureQuests(); const host=document.getElementById("questList"); if(!host) return; host.innerHTML="";
  save.quests.forEach((q,i)=>{ const claimed=!!q.claimed, done=questDone(q), prog=Math.min(q.target,q.prog||0), pct=Math.round(100*prog/q.target);
    const row=document.createElement("div");
    row.style.cssText="text-align:left;background:rgba(255,255,255,.0"+(claimed?"3":"5")+");border-radius:12px;padding:12px 14px;margin:8px 0;box-shadow:inset 0 0 0 1px "+(claimed?"rgba(124,231,160,.32)":done?"rgba(255,211,77,.5)":"rgba(255,255,255,.08)")+(claimed?";opacity:.88":"");
    let foot;
    if(claimed) foot='<span style="font-size:12px;color:#7ce0a0;font-weight:700">✓ Claimed</span> <span class="q-cd" data-pre=" · new quests in " style="font-size:12px;color:#9fb0c9;font-weight:600"> · new quests in '+fmtCountdown(msToQuestReset())+'</span>';
    else if(done) foot='<button class="pill" data-i="'+i+'" style="pointer-events:auto;cursor:pointer;padding:5px 18px;font-size:13px;background:linear-gradient(180deg,#ffe27a,#ffc23d);color:#06140f;font-weight:800;border:none">⛁ CLAIM</button>';
    else foot='<span style="font-size:12px;color:#9fb0c9;font-weight:600">'+prog+' / '+q.target+'</span>';
    row.innerHTML='<div style="display:flex;justify-content:space-between;align-items:center;gap:10px;margin-bottom:7px">'
      +'<span style="font-weight:700;font-size:14px;color:'+(claimed?"#9fb0c9":"#eef3ff")+'">'+questLabel(q)+'</span>'
      +'<span style="font-family:Orbitron,sans-serif;font-weight:800;font-size:13px;color:#ffd34d;white-space:nowrap">+'+q.reward+' ⛁</span></div>'
      +'<div style="height:9px;border-radius:6px;background:rgba(0,0,0,.35);overflow:hidden"><div style="height:100%;width:'+(claimed?100:pct)+'%;border-radius:6px;background:'+(claimed?"linear-gradient(90deg,#7ce0a0,#34c46a)":done?"linear-gradient(90deg,#ffe27a,#ffc23d)":"linear-gradient(90deg,var(--ac),#7fe7ff)")+';transition:width .3s"></div></div>'
      +'<div style="margin-top:7px;min-height:22px">'+foot+'</div>';
    host.appendChild(row); });
  host.querySelectorAll("button[data-i]").forEach(b=>{ b.onclick=()=>{ snd("ui"); claimQuest(+b.dataset.i); }; });
  const sub=document.querySelector("#questCard .sc-shop-sub"); if(sub) sub.innerHTML='3 quests a day &middot; <span class="q-cd" data-pre="new set in ">new set in '+fmtCountdown(msToQuestReset())+'</span>'; }
function refreshQuestCountdowns(){ const tx=fmtCountdown(msToQuestReset()); document.querySelectorAll("#questCard .q-cd").forEach(function(el){ el.textContent=(el.getAttribute("data-pre")||"")+tx; }); if(msToQuestReset()<1500) buildQuests(); } // tick the live countdowns; rebuild when the day rolls
setInterval(function(){ if(state==="quest") refreshQuestCountdowns(); }, 1000);
let prevQuestState="start";
function openQuests(){ if(state!=="start"&&state!=="result") return; prevQuestState=state; snd("ui");
  show("startCard",false); show("resultCard",false); show("shopCard",false); show("lbCard",false);
  ensureQuests(); buildQuests(); show("questCard",true); state="quest"; }
function closeQuests(){ snd("ui"); show("questCard",false); state=prevQuestState; if(state==="result") show("resultCard",true); else { state="start"; show("startCard",true); } }

// ===== WORLDS / ATLAS: a trophy-road of every biome; reached worlds revealed, the next a named goal, deeper ones a mystery. Aim for the highest world. =====
let prevWorldsState="start";
function openWorlds(){ if(state!=="start"&&state!=="result") return; prevWorldsState=state; snd("ui");
  show("startCard",false); show("resultCard",false); show("shopCard",false); show("lbCard",false); show("questCard",false);
  buildWorlds(); show("worldsCard",true); state="worlds"; }
function closeWorlds(){ snd("ui"); show("worldsCard",false); state=prevWorldsState; if(state==="result") show("resultCard",true); else { state="start"; show("startCard",true); } }
const WORLD_THEME = { "Aurora":["🌌","Northern lights"], "Moonbase":["🌎","Moon base"], "Kino":["🎭","Haunted theater"], "Facility 115":["⚡","115 facility"], "Ruby Rabbit":["🐇","Noir nightclub"], "Shadowman":["👻","Shadow-lord void"], // icon + short theme so a reached world shows WHAT it is, not just a color
  "Ghetto":["\u{1F3D9}️","Neon city block"], "Humble":["\u{1F3E1}","Balloon festival"], "Noname":["\u{1F32B}️","Glitch void"], "Plu":["\u{1F9F8}","Planet Pluto"], "Blurred":["\u{1F52E}","Neon dreamscape"], "Private":["\u{1F942}","Castle & dragons"], "Rad":["\u{1F6F9}","Radical retro"],
  "Dawnpurr":["\u{1F304}","Sunrise meadow"],"Emberglide":["\u{1F525}","Fiery glide"],"Zero Purravity":["\u{1FA90}","Zero gravity"],
  "Sporebloom":["\u{1F344}","Alien spores"],"Meowshroom Hollow":["\u{1F344}","Mushroom hollow"],"Petal Purradise":["\u{1F338}","Sakura petals"],
  "Abyssline":["\u{1F30A}","Rainy deep"],"Sky Kingdom":["☁️","Floating sky"],"Elysium":["\u{1F3DB}️","Heavenly"],"Zoomies Zone":["🌀","Speed hills"],
  "Funland":["\u{1F3A1}","Night carnival"],"Voltloom":["⚡","Electric storm"],"Sunspurr":["\u{1F30B}","Molten lava"],
  "Candyland":["\u{1F36C}","Candy land"],"Starwhisker":["\u{1F30C}","Starfield"],"Sakura Grove":["\u{1F338}","Cherry grove"],
  "Rainbow Road":["\u{1F308}","Rainbow road"],"Jungle Ruins":["\u{1F334}","Jungle ruins"],"Vapurrwave":["\u{1F334}","Vaporwave"],
  "Mango Meowrage":["\u{1F305}","Sunset rage"],"Underwater Reef":["\u{1F420}","Coral reef"],"Synthwave Grid":["\u{1F310}","Synthwave grid"],
  "Downpurr":["\u{1F327}️","Downpour"],"Neon Cyber-Kitty":["\u{1F303}","Neon cyber-city"],"Voltcrest":["⚡","Storm crest"],
  "Clockwork Kitty":["⚙️","Clockwork"],"Bioluminescent Abyss":["\u{1F52E}","Glowing abyss"],"Crimson Dawn":["\u{1FA78}","Crimson clouds"],
  "Aurora Purrge":["\u{1F30C}","Aurora"],"Purrgatory":["\u{1F480}","Purrgatory"],"Glacier Meowl":["❄️","Glacier"],
  "Desert Purramids":["\u{1F3DC}️","Pyramids & sphinx"],"Cosmic Mewbula":["\u{1F30C}","Cosmic nebula"],"Emberfall Catyon":["\u{1F30B}","Lava canyon"],
  "Magmacore":["\u{1F30B}","Magma core"],"Storm Spurr":["\u{1F329}️","Storm spire"],"Stormclaw":["⚡","Lightning"],
  "Frozen Purrora":["❄️","Frozen aurora"],"Geometric Void":["\u{1F537}","Geometric void"],"The Hush Below":["\u{1F327}️","The hush"],
  "Singularitty":["⚫","Singularity"],"Hyperpurr":["\u{1F50A}","Neon rave"],"Gotham":["\u{1F303}","Noir city"],
  "Hollywood":["\u{1F3AC}","Hollywood"],"Neon Tokyo":["\u{1F3EE}","Neon Tokyo"],"Senpai Skies":["⛩️","Anime sky"] };
function buildWorlds(){
  const host=document.getElementById("worldsList"); if(!host) return; host.innerHTML="";
  const best=save.best||0, zs=ZONES.slice().sort((a,b)=>a.d-b.d);
  const reachedList=zs.filter(z=>best>=z.d), reachedCount=reachedList.length, highest=reachedList[reachedList.length-1]||zs[0];
  const nextIdx=zs.findIndex(z=>best<z.d); const nextZone=nextIdx>=0?zs[nextIdx]:null;
  const sub=document.getElementById("worldsSub");
  if(sub) sub.innerHTML="Highest: <b style='color:var(--ac)'>"+highest.name+"</b> &middot; Best <b>"+best.toLocaleString()+"m</b> &middot; "+reachedCount+"/"+zs.length+" worlds";
  let nextRow=null;
  zs.slice().reverse().forEach(z=>{ // DEEPEST at top = a peak to climb toward (Clash-Royale trophy road)
    const reached=best>=z.d, isNext=(z===nextZone);
    const grad="linear-gradient(135deg,"+z.sky[0]+" 0%,"+z.sky[1]+" 50%,"+z.sky[2]+" 100%)";
    const row=document.createElement("div");
    row.style.cssText="display:flex;align-items:center;gap:12px;padding:9px 12px;margin:7px 0;border-radius:12px;text-align:left;"
      +(isNext?"box-shadow:inset 0 0 0 2px var(--ac);background:rgba(255,255,255,.07);"
        :reached?"box-shadow:inset 0 0 0 1px rgba(255,255,255,.10);background:rgba(255,255,255,.045);"
        :"box-shadow:inset 0 0 0 1px rgba(255,255,255,.05);background:rgba(255,255,255,.02);opacity:.72;");
    const th=WORLD_THEME[z.name]||["\u{1F30D}",""];
    const swBg=(reached||isNext)?grad:"#0e1120", swRing=reached?z.ac:(isNext?"var(--ac)":"rgba(255,255,255,.12)");
    const icon=reached?th[0]:(isNext?th[0]:"&#128274;"); // reached + next show the theme emoji; locked = lock
    const nm=reached?z.name:(isNext?z.name:"? ? ?");
    const det=reached?((th[1]||"Reached")+" &middot; "+z.d.toLocaleString()+"m")
             :isNext?("<b style='color:var(--ac)'>"+(z.d-best).toLocaleString()+"m to go</b> &middot; "+(th[1]||""))
             :("Reach "+z.d.toLocaleString()+"m");
    row.innerHTML="<div style='width:46px;height:46px;border-radius:10px;flex:0 0 auto;background:"+swBg+";box-shadow:inset 0 0 0 2px "+swRing+";display:flex;align-items:center;justify-content:center;font-size:23px'>"+icon+"</div>"
      +"<div style='flex:1;min-width:0'><div style='font-family:Orbitron,sans-serif;font-weight:800;font-size:15px;color:"+(reached?"#eef3ff":isNext?"#fff":"#8290a8")+"'>"+nm+(reached?" <span style=\"color:#5cff9e\">&#10003;</span>":"")+"</div><div style='font-size:12px;font-weight:600;color:"+(isNext?"#cfe0ff":"#8290a8")+"'>"+det+"</div></div>";
    host.appendChild(row); if(isNext) nextRow=row;
  });
  if(nextRow) setTimeout(function(){ try{ nextRow.scrollIntoView({block:"center"}); }catch(e){} },30); // land on the player's next goal
}
// ---- ABDUCTION + DANTE'S INFURNO secret level (research: rare variable-ratio jackpot) ----
const INFURNO_DUR=14;
function startAbduction(){ if(state!=="fly") return; infurnoUsedThisRun=true;
  savedRun={ x:kitty.x, y:kitty.y, vx:kitty.vx, vy:kitty.vy, cam:cam };
  ufo={ phase:"descend", k:0, sx:kitty.x-cam, ufoY:-90*DPR, beam:0 };
  state="abduct"; charging=false; stopCharge(); snd("token");
  bannerT=2.4; bannerName="SECRET LEVEL UNLOCKED"; } // announce it (was silent) + the descend phase now pulls the kitty into view (see update: abduct)
function enterInfurno(){ state="infurno"; cam=0; infurno={ t:0, p:0, coins:0, ready:false, hold:0 }; infCoins=[]; infHaz=[]; ufo=null;
  kitty.x=W*0.5; kitty.y=GROUND()-200*DPR; kitty.vx=0; kitty.vy=0; kitty.sx=kitty.sy=1; kitty.spin=0; kitty.rest=false;
  comboN=0; shake=12*DPR; bannerT=1.8; bannerName="DANTE'S INFURNO"; }
function exitInfurno(){ const jp=infurno?infurno.coins:0;
  infurno=null; infCoins=[]; infHaz=[];
  if(savedRun){ cam=savedRun.cam; kitty.x=savedRun.x; }
  const tY = Math.max(H*0.30, Math.min(savedRun ? savedRun.y : GROUND()-180*DPR, GROUND()-120*DPR)); // clamp into a VISIBLE band so the redeploy always plays on-screen (you may have been abducted from way above the top) + a bit of air to react
  ufo={ phase:"return", k:0, sx:(kitty.x-cam), ufoY:-90*DPR, beam:0,
        savedVx:(savedRun?Math.max(savedRun.vx,16*DPR):18*DPR), savedVy:(savedRun?savedRun.vy:0), targetY:tY };
  kitty.y=-40*DPR; kitty.vx=0; kitty.vy=0; kitty.sx=kitty.sy=0.2; kitty.rest=false; // start above the top; the UFO lowers you into view (see update: redeploy)
  state="redeploy"; snd("token");
  bannerT=1.4; bannerName="REDEPLOYING"; pop(kitty.x, 80*DPR, "JACKPOT +"+jp); }
function infurnoFlap(){ kitty.vy=-9*DPR; snd("flap"); for(let i=0;i<8;i++) parts.push({x:kitty.x,y:kitty.y+10*DPR,vx:rand(-1.5,1.5)*DPR,vy:rand(0,2)*DPR,life:0.7,c:"#ffd36b",r:rand(1.5,3)*DPR}); }
function infurnoGo(){ if(!infurno||infurno.ready) return; infurno.ready=true; shake=Math.max(shake,10*DPR); } // "press any key to start" -> begin the jackpot timer + fireballs (no start cue)

// ---- SURVIVAL GAUNTLET secret levels (LETHAL: TRIAL BY FIRE / SHARK WATERS) ----
const GAUNTLET_DUR=12;
function startGauntlet(type){ if(state!=="fly") return; if(type!=="fire"&&type!=="water"&&type!=="zombie"&&type!=="gorilla") return;
  gauntletUsed[type]=true;
  savedRun={ x:kitty.x, y:kitty.y, vx:kitty.vx, vy:kitty.vy, cam:cam };
  cam=0; gHaz=[]; comboN=0;
  const name = type==="fire" ? "TRIAL BY FIRE" : type==="zombie" ? "GRAVE DANGER" : type==="gorilla" ? "GORILLA WARFARE" : "SHARK WATERS";
  gauntlet={ type, t:0, dur:GAUNTLET_DUR, name, cd:0.7, side:0, ready:false, hold:0 }; // waits on "press any key to start" (no auto-countdown) so it never starts before you're ready
  kitty.x=W*0.5; kitty.y=GROUND()-200*DPR; kitty.vx=0; kitty.vy=0; kitty.sx=kitty.sy=1; kitty.spin=0; kitty.rest=false;
  gorilla = type==="gorilla" ? { x:W*0.13, y:GROUND(), facing:1, state:"walk", anim:0, acd:1.6, r:92*DPR, pcd:1.1 } : null;
  if(type==="gorilla"){ kitty.x=W*0.6; }
  state="gauntlet"; charging=false; stopCharge();
  shake=14*DPR; bannerT=1.8; bannerName=name; // no start cue (Diggy: rather have none than the death sound)
}
function gauntletGo(){ if(!gauntlet||gauntlet.ready) return; gauntlet.ready=true; gauntlet.cd=0.7; shake=Math.max(shake,10*DPR); } // "press any key to start" -> begin the timer + hazards (no start cue)
function gauntletFlap(){ kitty.vy=-9*DPR; snd("flap");
  const c = (gauntlet&&gauntlet.type==="fire") ? "#ffb24d" : "#bfefff";
  for(let i=0;i<8;i++) parts.push({x:kitty.x,y:kitty.y+10*DPR,vx:rand(-1.5,1.5)*DPR,vy:rand(0,2)*DPR,life:0.7,c,r:rand(1.5,3)*DPR}); }
function endGauntlet(survived){
  if(!survived) return; // death path is handled inline (state="fly"; endRun()) on hazard hit
  const nm = gauntlet?gauntlet.name:"GAUNTLET";
  save.coins += 5000; save.coinsEarned=(save.coinsEarned||0)+5000; questOnRun(0,5000); persist(); // owner spec: flat 5000-coin survival bonus (routed through coinsEarned + an "earn coins" quest, like every other coin source)
  if(savedRun){ cam=savedRun.cam; kitty.x=savedRun.x; kitty.y=Math.min(savedRun.y,GROUND()-40*DPR); kitty.vx=Math.max(savedRun.vx,6*DPR); kitty.vy=savedRun.vy; }
  kitty.sx=kitty.sy=1; kitty.rest=false; gHaz=[]; gauntlet=null; state="fly";
  // BIG survival payoff: violent full-screen shake + punchy hit-stop + gold flash + expanding shockwave rings + gold burst
  shake=Math.max(shake,38*DPR); hitstop=Math.max(hitstop,5); winFlashT=1; snd("newbest");
  shockwaves.push({x:kitty.x,y:kitty.y,r:18*DPR,life:1}); shockwaves.push({x:kitty.x,y:kitty.y,r:70*DPR,life:1.15});
  bannerT=2.2; bannerName="SURVIVED: "+nm;
  pop(kitty.x, kitty.y-50*DPR, "5000 COIN BONUS GRANTED");
  for(let i=0;i<64;i++){ const a=Math.random()*6.283, sp=(3+Math.random()*11)*DPR; parts.push({x:kitty.x,y:kitty.y,vx:Math.cos(a)*sp,vy:Math.sin(a)*sp-3*DPR,life:rand(0.7,1.5),c:i%3?"#ffd34d":(i%2?"#fff2c0":"#7fe7ff"),r:rand(2,6)*DPR}); }
  document.getElementById("coins").textContent=save.coins+runCoins;
}
window.startGauntlet=startGauntlet;

// ---- rounded-rect path helpers (generic over a 2d context) ----
function rr(g,x,y,w,h,r){ r=Math.min(r,w/2,h/2); g.beginPath(); g.moveTo(x+r,y);
  g.arcTo(x+w,y,x+w,y+h,r); g.arcTo(x+w,y+h,x,y+h,r); g.arcTo(x,y+h,x,y,r); g.arcTo(x,y,x+w,y,r); g.closePath(); }
function roundRect(x,y,w,h,r){ rr(ctx,x,y,w,h,r); }
function drawHeart(g,cx,cy,s){ g.beginPath(); g.moveTo(cx,cy+s*0.32);
  g.bezierCurveTo(cx,cy-s*0.06, cx-s*0.62,cy-s*0.5, cx-s*0.62,cy-s*0.02);
  g.bezierCurveTo(cx-s*0.62,cy+s*0.34, cx,cy+s*0.56, cx,cy+s*0.82);
  g.bezierCurveTo(cx,cy+s*0.56, cx+s*0.62,cy+s*0.34, cx+s*0.62,cy-s*0.02);
  g.bezierCurveTo(cx+s*0.62,cy-s*0.5, cx,cy-s*0.06, cx,cy+s*0.32); g.closePath(); g.fill(); }

let ACX="#22d3a0";
function getAccent(a){ const m=/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})/i.exec(ACX); return m?`rgba(${parseInt(m[1],16)},${parseInt(m[2],16)},${parseInt(m[3],16)},${a})`:`rgba(34,211,160,${a})`; }
// ---- distance-driven biomes: the world's colours evolve continuously as you fly ----
const ZONES = [
  { name:"Dawnpurr",   d:0,     sky:["#6cb7f2","#a9d6ff","#dcefe0"], ac:"#7ed957", fx:"none" },
  { name:"Emberglide", d:1000,  sky:["#ff2d6b","#ff6b2c","#ffc23d"], ac:"#21e6ff", fx:"none" },
  { name:"Sporebloom", d:2000,  sky:["#b6ff1a","#3ae06b","#0ea5a0"], ac:"#ff2ec4", fx:"none" },
  { name:"Meowshroom Hollow", d:3000,  sky:["#2f9e76","#136055","#06222b"], ac:"#5cffcf", fx:"none" },
  { name:"Zoomies Zone",       d:3600,  sky:["#3aa8f0","#8fd4ff","#ffe6a6"], ac:"#ffd23a", fx:"none" },
  { name:"Abyssline",  d:4500,  sky:["#0b36c9","#0e7ae6","#16e0c8"], ac:"#ffe03d", fx:"rain" },
  { name:"Sky Kingdom", d:5500, sky:["#2f83db","#5fa9ea","#ffd27a"], ac:"#ffce5a", fx:"none" },
  { name:"Funland",    d:6250,  sky:["#0b0a2e","#180f3e","#281a52"], ac:"#ff4fa6", fx:"none" }, // NIGHT amusement park: dark navy->purple sky, hot-pink neon accent drives the glowing ride lights
  { name:"Voltloom",   d:7000,  sky:["#7a0fe0","#d518c9","#ff3d8a"], ac:"#2bffe6", fx:"lightning" },
  { name:"Sunspurr",   d:8000,  sky:["#ffb200","#ff7a00","#ff3d5e"], ac:"#27e1ff", fx:"lava" },
  { name:"Candyland",  d:9000,  sky:["#ffd6ec","#ffb4d8","#bfe9ff"], ac:"#ff5fa6", fx:"none" },
  { name:"Starwhisker",  d:10500, sky:["#1a0a47","#5a1ad6","#16d6ff"], ac:"#ff5cf0", fx:"none" },
  { name:"Sakura Grove", d:11500, sky:["#a98fd6","#e89bc7","#ffc9b0"], ac:"#ff9ecb", fx:"none" },
  { name:"Jungle Ruins",    d:13000, sky:["#4fb87a","#23734f","#0c2c26"], ac:"#ffd75c", fx:"none" },
  { name:"Mango Meowrage",    d:14500, sky:["#ff8a3d","#ff5a6e","#7b2ff7"], ac:"#fff15c", fx:"none" },
  { name:"Underwater Reef", d:15500, sky:["#27d8cf","#0f86c4","#08305f"], ac:"#22e6d0", fx:"none" },
  { name:"Downpurr",        d:17000, sky:["#2fd6ff","#2a7bd6","#16306b"], ac:"#a8f3ff", fx:"rain" },
  { name:"Neon Cyber-Kitty", d:18000, sky:["#241046","#52137f","#0b0a1f"], ac:"#27e0ff", fx:"none" },
  { name:"Voltcrest",       d:19500, sky:["#b14dff","#5e2bd6","#190a4d"], ac:"#fff04a", fx:"lightning" },
  { name:"Clockwork Kitty",  d:20500, sky:["#d9a85c","#9a5e2c","#241712"], ac:"#7fe0ff", fx:"none" },
  { name:"Aurora Purrge",    d:22000, sky:["#1de2b0","#2360c4","#171a4a"], ac:"#7dffd0", fx:"none" },
  { name:"Purrgatory", d:23000, sky:["#39463f","#352f4a","#0c0e13"], ac:"#8f7fc0", fx:"none" },
  { name:"Glacier Meowl",    d:24500, sky:["#9be7ff","#6a7bff","#262a66"], ac:"#e8fbff", fx:"none" },
  { name:"Desert Purramids", d:25500, sky:["#7fb8e0","#eab15c","#f4cf86"], ac:"#ffd27a", fx:"none" },
  { name:"Emberfall Catyon",d:27000, sky:["#ff6a2c","#c11e4a","#3a0a2a"], ac:"#ffd23f", fx:"lava" },
  { name:"Magmacore",       d:28000, sky:["#ff5a1f","#a1102b","#180306"], ac:"#ffcf3a", fx:"lava" },
  { name:"Storm Spurr",     d:29000, sky:["#3a4170","#222748","#070912"], ac:"#9fd6ff", fx:"lightning" },
  { name:"Stormclaw",      d:30500, sky:["#6f4dff","#2a1f7a","#0a0726"], ac:"#9affef", fx:"lightning" },
  { name:"Frozen Purrora",   d:31500, sky:["#0c1a44","#1f4f8f","#9fe0ef"], ac:"#7dffc4", fx:"none" },
  { name:"The Hush Below",  d:33000, sky:["#7a2bff","#3a0f8c","#0b0420"], ac:"#ff5ce0", fx:"rain" },
  { name:"Singularitty",     d:34000, sky:["#b026ff","#3d0a66","#040208"], ac:"#00f0ff", fx:"lightning" },
  { name:"Elysium",             d:5800,  sky:["#f2a02e","#ff8a5c","#ffd98a"], ac:"#ffcf5c", fx:"none" },
  { name:"Rainbow Road",        d:12500, sky:["#0b1038","#2a1c63","#04030e"], ac:"#5cf0ff", fx:"none" },
  { name:"Vapurrwave",           d:13800, sky:["#caa6f0","#ff9ed8","#8fe6e0"], ac:"#ff5fc8", fx:"none" },
  { name:"Synthwave Grid",      d:16500, sky:["#241046","#4a0e6e","#120627"], ac:"#ff2d95", fx:"none" },
  { name:"Bioluminescent Abyss",d:21000, sky:["#0a2b4e","#06182f","#020812"], ac:"#22e6ff", fx:"none" },
  { name:"Cosmic Mewbula",      d:26500, sky:["#241654","#120a33","#060314"], ac:"#35e8ff", fx:"none" },
  { name:"Geometric Void",      d:32000, sky:["#05070e","#0a1020","#0e1834"], ac:"#22e6ff", fx:"none" },
  // ZERO-G WORLD: low gravity + floating bounce-pads + bobbing hazards. Neon palette, fast-but-floaty.
  // lowGrav = gravity multiplier; floaty = spawn mid-air pads/hazards instead of the ground set.
  { name:"Zero Purravity",      d:1500,  sky:["#1a0a52","#6f1ad6","#0a0524"], ac:"#19f0ff", fx:"none", lowGrav:0.40, floaty:true },
  { name:"Petal Purradise",     d:4200,  sky:["#ff9ec7","#ffc1de","#ffe6f2"], ac:"#ff4f9e", fx:"none" }, // all-pink cherry-blossom world (Naruto-Sakura vibe; sakura/petals are generic)
  { name:"Crimson Dawn",        d:21500, sky:["#2a0810","#5e0e1a","#0a0306"], ac:"#ff2d4a", fx:"none" }, // ominous red clouds on dark crimson (Akatsuki-inspired but original shapes + non-identical shade)
  { name:"Hyperpurr",           d:36000, sky:["#12022e","#2a0540","#05010f"], ac:"#ff2bd0", fx:"none" }, // CHAOTIC neon rave: cycling color washes + sweeping lasers + pulsing EQ skyline (deep-run euphoric payoff; reduced-motion safe)
  { name:"Gotham",              d:38000, sky:["#0a0e1f","#0f1530","#05060f"], ac:"#5b8dff", fx:"none" }, // noir city: near-black skyscrapers, lit windows, neon billboards, searchlight
  { name:"Hollywood",           d:40000, sky:["#5bb8f0","#a8dcff","#ffe8c8"], ac:"#ff4f8a", fx:"none" }, // LA daytime: Hollywood Hills+sign, palms, Melrose boutiques + pink wall; floor = Walk of Fame stars
  { name:"Neon Tokyo",          d:42000, sky:["#0a0a2e","#1a0f3e","#2a1a4e"], ac:"#ff2d6b", fx:"none" }, // Shibuya night: Tokyo Tower, dense neon signboards, lanterns
  { name:"Senpai Skies",        d:44000, sky:["#2a5cff","#7fb0ff","#ffd9a8"], ac:"#ffe27a", fx:"none" }, // Makoto-Shinkai anime sky: lens-flare sun, dramatic clouds, torii + telephone poles
  { name:"Ghetto",              d:46000, sky:["#161a2c","#20263c","#0c0f1a"], ac:"#ff2bd0", fx:"none" }, // neon night city block: graffiti cat-tags, lit windows, streetlamps, boombox
  { name:"Humble",              d:48000, sky:["#ffce87","#ff9e63","#9ab8d8"], ac:"#ffcf6b", fx:"none" }, // cozy golden-hour countryside: cottage, hay bales, fireflies
  { name:"Noname",              d:50000, sky:["#aab0b8","#c0c6ce","#d6dade"], ac:"#9aa4b0", fx:"none" }, // liminal anonymous void: fog, floating "?" glyphs, blinking cursor
  { name:"Plu",                 d:52000, sky:["#0b0a22","#161036","#050310"], ac:"#8fd0ff", fx:"none" }, // plushie toy land: giant button-eyed stuffed animals, felt clouds, quilted hills
  { name:"Blurred",             d:54000, sky:["#1a0a3e","#3a1060","#0a0524"], ac:"#ff6ae0", fx:"none" }, // dreamy soft-focus bokeh
  { name:"Private",             d:56000, sky:["#0c0a20","#1c1030","#060510"], ac:"#ff7a2b", fx:"none" }, // luxe members-only speakeasy garden: gold neon, velvet rope, hedges
  { name:"Rad",                 d:58000, sky:["#2a0a45","#7a1a8a","#ff2b6b"], ac:"#00e6d0", fx:"none" }, // radical retro skate: striped sun, halftone, palms, lightning
  { name:"Ruby Rabbit",         d:60000, sky:["#0a0620","#1a0a3a","#05030f"], ac:"#ff2bd0", fx:"none" },
  { name:"Shadowman",           d:64000, sky:["#0a0618","#1a0a2e","#050310"], ac:"#a24dff", fx:"none" },
  { name:"Aurora",              d:70000, sky:["#0a1030","#12204a","#050818"], ac:"#4dffb0", fx:"none" },
  { name:"Moonbase",            d:72000, sky:["#02030a","#0a0a1e","#000005"], ac:"#5b9dff", fx:"none" },
  { name:"Kino",                d:8500, sky:["#1a0810","#2e0f18","#0a0308"], ac:"#ff3d5e", fx:"none" },
  { name:"Facility 115",        d:78000, sky:["#0e1418","#182228","#060a0d"], ac:"#3dff9e", fx:"none" },
];
ZONES.sort((a,b)=>a.d-b.d); // keep zones ordered by distance after appending the new environments
// World test-jump: ?world=<substring> (or legacy ?zerog) forces a world from the start, for previewing.
const _forceWorld = ((typeof Q!=="undefined" ? (Q.get("world") || (Q.has("zerog") ? "zero" : "")) : "") || "").toLowerCase();
function _forcedZone(){ if(!_forceWorld) return null; const nm=s=>s.toLowerCase().replace(/[^a-z0-9]/g,""); const fw=nm(_forceWorld); return ZONES.find(z=>nm(z.name)===fw) || ZONES.find(z=>z.name.toLowerCase().includes(_forceWorld)) || ZONES.find(z=>nm(z.name).includes(fw)) || null; } // space/punct-insensitive so ?world=ironcolossus matches "Iron Colossus" // exact name wins (so ?world=rad => "Rad", not "Petal Purradise"); substring is the fallback
function _h2r(h){ const n=parseInt(h.slice(1),16); return [n>>16&255,n>>8&255,n&255]; }
function _r2h(r,g,b){ return "#"+[r,g,b].map(v=>Math.max(0,Math.min(255,Math.round(v))).toString(16).padStart(2,"0")).join(""); }
function _mix(a,b,t){ const A=_h2r(a),B=_h2r(b); return _r2h(A[0]+(B[0]-A[0])*t,A[1]+(B[1]-A[1])*t,A[2]+(B[2]-A[2])*t); }
function _sm(t){ return t*t*(3-2*t); }
function _hx(n){ n=Math.max(0,Math.min(255,n|0)); return (n<16?"0":"")+n.toString(16); }
function hslHex(h,s,l){ h=((h%360)+360)%360; const c=(1-Math.abs(2*l-1))*s, x=c*(1-Math.abs((h/60)%2-1)), m=l-c/2; let r,g,b; if(h<60){r=c;g=x;b=0;}else if(h<120){r=x;g=c;b=0;}else if(h<180){r=0;g=c;b=x;}else if(h<240){r=0;g=x;b=c;}else if(h<300){r=x;g=0;b=c;}else{r=c;g=0;b=x;} return "#"+_hx((r+m)*255)+_hx((g+m)*255)+_hx((b+m)*255); }
const _PFX=["lightning","lava","rain","none","lightning","rain"];
const _NA=["Vor","Nyx","Zar","Hel","Quor","Aeth","Umbra","Pyre","Stryx","Thal","Mor","Kael"], _NB=["mire","grave","spire","reach","fall","crest","rift","gale","forge","hollow","verge","scar"];
function proceduralBiome(k){ const hue=(210+k*137.508)%360, darken=Math.min(0.12,k*0.004);
  return { name:_NA[((k%_NA.length)+_NA.length)%_NA.length]+_NB[(((k*7+3)%_NB.length)+_NB.length)%_NB.length],
    sky:[hslHex(hue,0.88,0.54-darken), hslHex(hue+20,0.92,0.38-darken), hslHex(hue+42,0.96,0.20-darken*0.6)],
    ac:hslHex(hue+180,0.97,0.64), fx:_PFX[((k%_PFX.length)+_PFX.length)%_PFX.length] }; }
function biomeAt(dist){ const last=ZONES.length-1;
  { const fz=_forcedZone(); if(fz){ return { name:fz.name, sky:fz.sky.slice(), ac:fz.ac, fx:fz.fx||"none", lowGrav:fz.lowGrav, floaty:fz.floaty }; } } // ?world=/?zerog test-jump
  if(dist<=ZONES[last].d){ let i=0; while(i<last && dist>=ZONES[i+1].d) i++; const A=ZONES[i], B=ZONES[Math.min(i+1,last)]; const t=B.d>A.d?_sm((dist-A.d)/(B.d-A.d)):0; const near=t<0.5?A:B;
    return { name:near.name, sky:[_mix(A.sky[0],B.sky[0],t),_mix(A.sky[1],B.sky[1],t),_mix(A.sky[2],B.sky[2],t)], ac:_mix(A.ac,B.ac,t), fx:near.fx||"none", lowGrav:near.lowGrav, floaty:near.floaty }; }
  const over=dist-ZONES[last].d, SP=1500, k=Math.floor(over/SP); const A=(k===0)?ZONES[last]:proceduralBiome(k-1), B=proceduralBiome(k); const t=_sm((over%SP)/SP); const near=t<0.5?A:B;
  return { name:near.name, sky:[_mix(A.sky[0],B.sky[0],t),_mix(A.sky[1],B.sky[1],t),_mix(A.sky[2],B.sky[2],t)], ac:_mix(A.ac,B.ac,t), fx:near.fx||"none", lowGrav:near.lowGrav, floaty:near.floaty }; }
let lastBiome="", bannerT=0, bannerName="";
// --- WORLD-ENTRY CEREMONY (world-change ONLY; the shared bannerT still handles OVERDRIVE / NEW HIGH SCORE / etc) ---
// Panel-less title event: radial accent bloom + shockwave ring + sparkle burst + per-letter overshoot stagger
// (white->accent gradient + glow) + a shine sweep across the glyphs. Frame-rate independent (progress from t-start).
let _worldEntry=null;
const _weOut =p=>1-(1-p)*(1-p);                              // ease-out quad
const _weBack=p=>{const s=1.70158; return 1+(s+1)*Math.pow(p-1,3)+s*Math.pow(p-1,2);}; // overshoot
function triggerWorldEntry(name, t, bm){
  const rm=_PREF_REDUCE;
  const sparks=[], n=rm?10:26;
  for(let i=0;i<n;i++){ sparks.push({ ang:Math.random()*6.283, reach:(60+Math.random()*220)*DPR, r:(1.4+Math.random()*3)*DPR, delay:Math.random()*0.14, ph:Math.random()*6.283, white:i%5===0 }); }
  _worldEntry={ name, start:t, ac:(bm&&bm.ac)||ACX||"#7fe9ff", dist:distM, theme:(typeof WORLD_THEME!=="undefined" && WORLD_THEME[name])||null, sparks };
  if(!rm) shake=Math.max(shake, 6*DPR);   // tiny arrival punch
  snd("catnip");                          // glassy shimmer = "new world"
}
function drawWorldEntry(t){
  if(!_worldEntry) return;
  const e=_worldEntry, DUR=2.6, age=t-e.start;
  if(age>=DUR){ _worldEntry=null; return; }
  const rm=_PREF_REDUCE;
  const p=age/DUR;
  let env=1;                                   // fade-in / hold / fade-out envelope
  if(age<0.14) env=age/0.14; else if(p>0.78) env=Math.max(0,(1-p)/0.22);
  const cx=W/2, cy=H*0.19;
  ctx.save(); ctx.textAlign="center"; ctx.textBaseline="alphabetic";
  // radial accent bloom (no panel)
  ctx.globalCompositeOperation="lighter";
  const bR=(40+250*_weOut(Math.min(1,age/0.5)))*DPR;
  const bg=ctx.createRadialGradient(cx,cy,0,cx,cy,bR);
  bg.addColorStop(0,getAccent(0.40*env)); bg.addColorStop(0.45,getAccent(0.12*env)); bg.addColorStop(1,getAccent(0));
  ctx.fillStyle=bg; ctx.fillRect(cx-bR,cy-bR,bR*2,bR*2);
  // shockwave ring
  const rp=Math.min(1,age/0.6);
  if(rp<1){ const rr=(18+300*_weOut(rp))*DPR; ctx.globalAlpha=(1-rp)*0.55*env; ctx.strokeStyle=e.ac; ctx.lineWidth=(3*(1-rp)+0.6)*DPR; ctx.beginPath(); ctx.arc(cx,cy,rr,0,6.283); ctx.stroke(); ctx.globalAlpha=1; }
  // sparkle burst
  for(const s of e.sparks){ const se=age-s.delay; if(se<0) continue; const sp=Math.min(1,se/1.1); if(sp>=1) continue;
    const d=s.reach*_weOut(sp), x=cx+Math.cos(s.ang)*d, y=cy+Math.sin(s.ang)*d+34*DPR*sp*sp, tw=0.6+0.4*Math.sin(t*20+s.ph);
    ctx.globalAlpha=(1-sp)*env*tw; ctx.fillStyle=s.white?"#ffffff":e.ac; const r=s.r*(1-0.4*sp);
    ctx.beginPath(); ctx.arc(x,y,r,0,6.283); ctx.fill(); }
  ctx.globalAlpha=1; ctx.globalCompositeOperation="source-over";
  // kicker
  const kp=Math.max(0,Math.min(1,(age-0.05)/0.3));
  ctx.globalAlpha=kp*env; ctx.fillStyle=e.ac; ctx.font=`700 ${12*DPR}px 'Saira Semi Condensed'`; ctx.fillText("N E W   W O R L D", cx, cy-26*DPR);
  // TITLE: per-letter overshoot stagger, white->accent gradient
  const chars=[...e.name.toUpperCase()]; // code-point split so widths[] and the draw loop stay in lockstep (astral-safe)
  let fs=40*DPR; ctx.font=`900 ${fs}px Orbitron`;
  let widths=[], total=0; for(const ch of chars){ const w=ctx.measureText(ch).width; widths.push(w); total+=w; }
  const maxW=W*0.86;
  if(total>maxW){ fs*=maxW/total; ctx.font=`900 ${fs}px Orbitron`; widths=[]; total=0; for(const ch of chars){ const w=ctx.measureText(ch).width; widths.push(w); total+=w; } }
  const titleY=cy+13*DPR;
  const gTitle=ctx.createLinearGradient(0,-fs*0.72,0,fs*0.14);
  gTitle.addColorStop(0,"#ffffff"); gTitle.addColorStop(0.55,"#ffffff"); gTitle.addColorStop(1,e.ac);
  const STEP=rm?0:0.045; let lx=cx-total/2;
  for(let i=0;i<chars.length;i++){ const cw=widths[i], px=lx+cw/2; lx+=cw;
    const lp=Math.max(0,Math.min(1,(age-(0.12+i*STEP))/0.4)); if(lp<=0) continue;
    const sc=rm?1:_weBack(lp), yo=rm?0:(1-lp)*18*DPR;
    ctx.save(); ctx.translate(px, titleY-yo); ctx.scale(sc,sc); ctx.globalAlpha=lp*env;
    ctx.shadowColor=e.ac; ctx.shadowBlur=16*DPR; ctx.fillStyle=gTitle; ctx.fillText(chars[i],0,0); ctx.restore(); }
  ctx.shadowBlur=0; ctx.globalAlpha=1;
  // shine sweep across the glyphs
  if(!rm && age>0.45 && age<1.25){ const shp=(age-0.45)/0.8, band=cx-total/2+shp*total;
    ctx.globalCompositeOperation="lighter"; ctx.font=`900 ${fs}px Orbitron`;
    const g=ctx.createLinearGradient(band-40*DPR,0,band+40*DPR,0);
    g.addColorStop(0,"rgba(255,255,255,0)"); g.addColorStop(0.5,`rgba(255,255,255,${0.5*env})`); g.addColorStop(1,"rgba(255,255,255,0)");
    ctx.fillStyle=g; ctx.fillText(chars.join(""),cx,titleY); ctx.globalCompositeOperation="source-over"; }
  // descriptor + distance
  const dp=Math.max(0,Math.min(1,(age-0.35)/0.4));
  ctx.globalAlpha=dp*env;
  const desc=(e.theme && e.theme[1]) ? e.theme[1] : "New biome";
  const emoji=e.theme ? e.theme[0]+"  " : "";
  ctx.fillStyle="rgba(255,255,255,.9)"; ctx.font=`700 ${13*DPR}px 'Saira Semi Condensed'`;
  ctx.fillText(emoji+desc.toUpperCase()+"   ·   "+e.dist+"M", cx, cy+40*DPR);
  ctx.restore();
}

// ---- background: user image (parallax) OR original Geometry-Dash-style neon world ----
function drawPoly(x,y,r,sides,rot,fill){ ctx.save(); ctx.translate(x,y); ctx.rotate(rot); ctx.beginPath();
  for(let i=0;i<sides;i++){ const a=i/sides*6.283-Math.PI/2; const px=Math.cos(a)*r, py=Math.sin(a)*r; i?ctx.lineTo(px,py):ctx.moveTo(px,py); }
  ctx.closePath(); ctx.fillStyle=fill; ctx.fill(); ctx.restore(); }
function silhouetteColor(hex,a,mul){ const n=parseInt(hex.slice(1),16);
  let r=(n>>16)&255,g=(n>>8)&255,b=n&255; mul=mul==null?1:mul;
  r=Math.min(255,r*mul|0); g=Math.min(255,g*mul|0); b=Math.min(255,b*mul|0);
  return 'rgba('+r+','+g+','+b+','+a+')'; }
// ===== ESCALATING VISUAL INTENSITY + DEEP-RUN STYLE-SHIFT (Geometry-Dash-style dopamine ramp) =====
// One master scalar ramps the WORLD richer/glowier the farther you fly. Layers on TOP of the biome
// system (ZONES/scapes untouched), drawn in the SKY band BEFORE obstacles/kitty -> foreground stays
// readable on OBS. Red stays damage-only (aurora hue set skips the danger band).
const INTEN_FULL=12000, OVERDRIVE_D=16000, OVERDRIVE_RAMP=2500;
let INTEN=0, ODX=0, _pulse=0; // set once per frame in frame(), read by the helpers below
function intensityAt(d){ const x=Math.max(0,Math.min(1,d/INTEN_FULL)); return x*x*(3-2*x); }
function overdriveAt(d){ const x=Math.max(0,Math.min(1,(d-OVERDRIVE_D)/OVERDRIVE_RAMP)); return x*x*(3-2*x); }
function pulseAt(t){ const hz=0.30+0.32*INTEN, s=Math.sin(t*6.2832*hz); return s*s; } // soft rhythmic breathe 0..1
function applySkyIntensity(t, biome){
  if(INTEN<=0.001 && ODX<=0.001) return;
  const G=GROUND();
  const a=0.12+0.26*INTEN; // saturation/contrast lift via 'overlay' (caps ~0.38, never crushes detail)
  const sg=ctx.createLinearGradient(0,0,0,G); sg.addColorStop(0,biome.sky[0]); sg.addColorStop(.5,biome.sky[1]); sg.addColorStop(1,biome.sky[2]);
  ctx.save(); ctx.globalCompositeOperation='overlay'; ctx.globalAlpha=a; ctx.fillStyle=sg; ctx.fillRect(0,0,W,G); ctx.restore();
  const pg=(0.05+0.16*INTEN)*(0.35+0.65*_pulse); // breathing accent glow from horizon (caps ~0.21)
  if(pg>0.004){ const h=H*0.55, hg=ctx.createLinearGradient(0,G,0,G-h); hg.addColorStop(0,getAccent(pg)); hg.addColorStop(1,getAccent(0));
    ctx.save(); ctx.globalCompositeOperation='lighter'; ctx.fillStyle=hg; ctx.fillRect(0,G-h,W,h); ctx.restore(); }
  ctx.globalAlpha=1;
}
const _ODHUES=[190,160,110,55,280,225]; // cyan/teal/green/gold/violet/blue - NO red band
function drawOverdriveSky(t, biome){
  if(ODX<=0.001) return; const G=GROUND(), band=G/_ODHUES.length, scroll=t*(0.10+0.18*INTEN), aMax=0.18*ODX;
  ctx.save(); ctx.globalCompositeOperation='lighter';
  for(let i=0;i<_ODHUES.length;i++){ const wob=Math.sin(scroll*6.2832+i*1.7), y=i*band+wob*band*0.35, h2=band*1.6;
    const lg=ctx.createLinearGradient(0,y,0,y+h2); lg.addColorStop(0,'rgba(0,0,0,0)'); lg.addColorStop(.5,hslHex((_ODHUES[i]+t*8)%360,0.95,0.60)); lg.addColorStop(1,'rgba(0,0,0,0)');
    ctx.globalAlpha=aMax*(0.45+0.55*(0.5+0.5*wob)); ctx.fillStyle=lg; ctx.fillRect(0,y,W,h2); }
  ctx.restore(); ctx.globalAlpha=1;
}

// ===== Evolving distant landscapes: a different parallax silhouette style per biome =====
function scapeStyle(name){ let h=2166136261>>>0; name=name||""; for(let i=0;i<name.length;i++){ h=Math.imul(h^name.charCodeAt(i),16777619)>>>0; } return h%6; }
function _scapeCols(f,gap,cb){ const shift=cam*f; const i0=Math.floor((shift-gap)/gap), i1=Math.ceil((shift+W+gap)/gap);
  for(let k=i0;k<=i1;k++) cb(k, k*gap-shift); }
function scapeCity(biome,horizon){
  const layers=[
    {kind:'ridge', f:0.10, gap:170, base:0.15, vary:0.09, sky:2, mul:1.05, fa:0.55, rim:0.05},
    {kind:'spire', f:0.26, gap:60,  base:0.16, vary:0.30, sky:1, mul:0.55, fa:0.60, rim:0.22},
    {kind:'hill',  f:0.46, gap:230, base:0.22, vary:0.15, sky:0, mul:0.45, fa:0.85, rim:0.16},
  ];
  for(const L of layers){
    const gap=L.gap*DPR, shift=cam*L.f;
    const i0=Math.floor((shift-gap)/gap), i1=Math.ceil((shift+W+gap)/gap);
    const baseH=L.base*horizon, varH=L.vary*horizon;
    const fill=silhouetteColor(biome.sky[L.sky], L.fa, L.mul);
    if(L.kind==='spire'){
      for(let k=i0;k<=i1;k++){
        const x=k*gap-shift;
        const w=gap*(0.34+0.34*hashS(k));
        const hh=baseH+varH*(0.12+0.88*hashY(k));
        const top=horizon-hh;
        ctx.fillStyle=fill; ctx.fillRect(x-w/2, top, w, hh);
        ctx.fillStyle=getAccent(L.rim); ctx.fillRect(x-w/2, top, w, 2*DPR);
      }
    }else{
      const hAt=(k)=> baseH+varH*(0.30*hashY(k)+0.45*hashS(k)+0.25*hashY(k+1));
      const pts=[];
      for(let k=i0;k<=i1;k++) pts.push([k*gap-shift, horizon-hAt(k)]);
      ctx.beginPath(); ctx.moveTo(pts[0][0], horizon); ctx.lineTo(pts[0][0], pts[0][1]);
      for(let j=1;j<pts.length;j++){ const mx=(pts[j-1][0]+pts[j][0])/2, my=(pts[j-1][1]+pts[j][1])/2; ctx.quadraticCurveTo(pts[j-1][0], pts[j-1][1], mx, my); }
      ctx.lineTo(pts[pts.length-1][0], pts[pts.length-1][1]); ctx.lineTo(pts[pts.length-1][0], horizon); ctx.closePath();
      ctx.fillStyle=fill; ctx.fill();
      ctx.beginPath(); ctx.moveTo(pts[0][0], pts[0][1]);
      for(let j=1;j<pts.length;j++){ const mx=(pts[j-1][0]+pts[j][0])/2, my=(pts[j-1][1]+pts[j][1])/2; ctx.quadraticCurveTo(pts[j-1][0], pts[j-1][1], mx, my); }
      ctx.lineWidth=1.5*DPR; ctx.strokeStyle=getAccent(L.rim); ctx.stroke();
    }
  }
}
function scapeMountains(biome,horizon){
  const layers=[
    {f:0.09, gap:140, base:0.18, vary:0.16, sky:2, mul:1.02, a:0.40, rim:0.05, cap:0.0},
    {f:0.22, gap:160, base:0.24, vary:0.22, sky:1, mul:0.62, a:0.52, rim:0.08, cap:0.16},
    {f:0.42, gap:200, base:0.30, vary:0.30, sky:1, mul:0.42, a:0.78, rim:0.10, cap:0.22},
  ];
  for(const L of layers){
    const gap=L.gap*DPR, shift=cam*L.f;
    const i0=Math.floor((shift-gap)/gap), i1=Math.ceil((shift+W+gap)/gap);
    const baseH=L.base*horizon, varH=L.vary*horizon;
    const peak=(k)=> baseH+varH*(0.18+0.82*hashY(k*1.7+L.f));
    const valley=(k)=> baseH*0.10 + varH*0.18*hashS(k);
    ctx.beginPath(); ctx.moveTo(i0*gap-shift, horizon);
    for(let k=i0;k<=i1;k++){ const x=k*gap-shift;
      ctx.lineTo(x-gap*0.5, horizon-valley(k));
      ctx.lineTo(x, horizon-peak(k)); }
    ctx.lineTo(i1*gap-shift, horizon); ctx.closePath();
    ctx.fillStyle=silhouetteColor(biome.sky[L.sky], L.a, L.mul); ctx.fill();
    if(L.cap>0){ ctx.fillStyle=silhouetteColor(biome.sky[0], L.cap, 1.5);
      for(let k=i0;k<=i1;k++){ const x=k*gap-shift, ph=horizon-peak(k); const cw=gap*0.18, ch=varH*0.16;
        ctx.beginPath(); ctx.moveTo(x,ph); ctx.lineTo(x-cw, ph+ch); ctx.lineTo(x+cw, ph+ch); ctx.closePath(); ctx.fill(); } }
    ctx.beginPath(); ctx.moveTo(i0*gap-shift, horizon-valley(i0));
    for(let k=i0;k<=i1;k++){ const x=k*gap-shift; ctx.lineTo(x-gap*0.5, horizon-valley(k)); ctx.lineTo(x, horizon-peak(k)); }
    ctx.lineWidth=1.2*DPR; ctx.strokeStyle=getAccent(L.rim); ctx.stroke();
  }
}
function scapeCrystals(biome,horizon){
  const layers=[
    {f:0.12, gap:84,  base:0.18, vary:0.26, sky:2, mul:1.04, a:0.34, facet:0.06, rim:0.10},
    {f:0.40, gap:104, base:0.24, vary:0.40, sky:1, mul:0.50, a:0.55, facet:0.10, rim:0.20},
  ];
  for(const L of layers){
    const gap=L.gap*DPR;
    const baseH=L.base*horizon, varH=L.vary*horizon;
    _scapeCols(L.f, gap, (k,x)=>{
      const cnt = hashS(k)>0.55?2:1;
      for(let c=0;c<cnt;c++){
        const o=(c? (hashY(k*3+c)-0.5)*gap*0.7 : 0);
        const cx=x+o;
        const w=gap*(0.16+0.16*hashS(k*5+c));
        const hh=baseH+varH*(0.10+0.90*hashY(k*7+c));
        const tipY=horizon-hh, midY=horizon-hh*0.46;
        ctx.beginPath();
        ctx.moveTo(cx, horizon); ctx.lineTo(cx-w, midY); ctx.lineTo(cx, tipY); ctx.lineTo(cx+w, midY); ctx.closePath();
        ctx.fillStyle=silhouetteColor(biome.sky[L.sky], L.a, L.mul); ctx.fill();
        ctx.beginPath();
        ctx.moveTo(cx, horizon); ctx.lineTo(cx, tipY); ctx.lineTo(cx+w, midY); ctx.closePath();
        ctx.fillStyle=getAccent(L.facet); ctx.fill();
        ctx.lineWidth=1*DPR; ctx.strokeStyle=getAccent(L.rim);
        ctx.beginPath(); ctx.moveTo(cx-w,midY); ctx.lineTo(cx,tipY); ctx.lineTo(cx+w,midY); ctx.stroke();
      }
    });
  }
}
function scapeIslands(biome,horizon,t){
  const layers=[
    {f:0.10, gap:150, lift:0.34, vary:0.16, sz:0.20, sky:2, mul:1.05, a:0.30, rim:0.06, bob:2.5},
    {f:0.34, gap:210, lift:0.52, vary:0.22, sz:0.27, sky:1, mul:0.55, a:0.48, rim:0.11, bob:4.0},
  ];
  for(const L of layers){
    const gap=L.gap*DPR;
    _scapeCols(L.f, gap, (k,x)=>{
      const w=gap*(L.sz+0.08*hashS(k));
      const ry=w*0.26;
      const lift=horizon*(0.24+L.lift*hashY(k));
      const cy=horizon-lift + Math.sin(t*0.5+k*1.3)*L.bob*DPR;
      const avail=(horizon-cy)-18*DPR;
      const drop=Math.max(w*0.6, avail*(0.5+0.32*hashS(k*3)));
      ctx.fillStyle=silhouetteColor(biome.sky[L.sky], L.a, L.mul);
      ctx.beginPath();
      ctx.ellipse(x,cy,w,ry,0,Math.PI,2*Math.PI,false);
      ctx.quadraticCurveTo(x+w*0.36, cy+drop*0.55, x, cy+drop);
      ctx.quadraticCurveTo(x-w*0.36, cy+drop*0.55, x-w, cy); ctx.closePath(); ctx.fill();
      ctx.beginPath(); ctx.ellipse(x,cy,w,ry,0,Math.PI,2*Math.PI,false);
      ctx.lineWidth=1.6*DPR; ctx.strokeStyle=getAccent(L.rim); ctx.stroke();
    });
  }
}
function scapeForest(biome,horizon){
  const layers=[
    {f:0.12, gap:40, base:0.14, vary:0.14, sky:2, mul:1.02, a:0.30, lw:2.0, rim:0.05},
    {f:0.30, gap:54, base:0.20, vary:0.18, sky:1, mul:0.55, a:0.46, lw:3.0, rim:0.08},
    {f:0.50, gap:88, base:0.27, vary:0.22, sky:1, mul:0.38, a:0.66, lw:4.2, rim:0.10},
  ];
  for(const L of layers){
    const gap=L.gap*DPR;
    const baseH=L.base*horizon, varH=L.vary*horizon;
    ctx.lineCap='round'; ctx.lineJoin='round';
    _scapeCols(L.f, gap, (k,x)=>{
      const hh=baseH+varH*(0.3+0.7*hashY(k));
      const lean=(hashS(k)-0.5)*0.5;
      const topX=x+lean*hh, topY=horizon-hh;
      ctx.strokeStyle=silhouetteColor(biome.sky[L.sky], L.a, L.mul);
      ctx.lineWidth=L.lw*DPR;
      ctx.beginPath(); ctx.moveTo(x,horizon);
      ctx.quadraticCurveTo(x+lean*hh*0.4, horizon-hh*0.55, topX, topY); ctx.stroke();
      ctx.lineWidth=Math.max(1,L.lw*0.5)*DPR;
      for(let b=0;b<3;b++){
        const f=0.42+b*0.18, bx=x+lean*hh*f, by=horizon-hh*f;
        const dir=(hashS(k*5+b)>0.5?1:-1);
        const bl=hh*(0.16+0.18*hashY(k*9+b));
        ctx.beginPath(); ctx.moveTo(bx,by);
        ctx.quadraticCurveTo(bx+dir*bl*0.5, by-bl*0.2, bx+dir*bl, by-bl*0.5); ctx.stroke();
      }
    });
  }
}
function scapeRuins(biome,horizon){
  const layers=[
    {f:0.08, gap:230, base:0.22, vary:0.18, sky:2, mul:1.04, a:0.34, rim:0.05, floors:0},
    {f:0.24, gap:120, base:0.20, vary:0.30, sky:1, mul:0.58, a:0.50, rim:0.08, floors:1},
    {f:0.46, gap:96,  base:0.26, vary:0.36, sky:1, mul:0.42, a:0.74, rim:0.12, floors:1},
  ];
  for(const L of layers){
    const gap=L.gap*DPR;
    const baseH=L.base*horizon, varH=L.vary*horizon;
    _scapeCols(L.f, gap, (k,x)=>{
      const w=gap*(0.52+0.30*hashS(k));
      const hh=baseH+varH*(0.10+0.90*hashY(k));
      const bx=x-w/2, top=horizon-hh;
      ctx.fillStyle=silhouetteColor(biome.sky[L.sky], L.a, L.mul);
      ctx.fillRect(bx, top, w, hh);
      const notch=hashY(k*11);
      if(notch>0.45){ const nw=w*(0.25+0.35*hashS(k*13)), nh=hh*(0.06+0.16*notch);
        const side=hashS(k*17)>0.5?bx+w-nw:bx;
        ctx.save(); ctx.globalCompositeOperation='destination-out';
        ctx.fillStyle='rgba(0,0,0,1)'; ctx.fillRect(side, top, nw, nh); ctx.restore();
      }
      if(L.floors){ ctx.strokeStyle=getAccent(L.rim); ctx.lineWidth=1*DPR;
        const rows=Math.max(2,Math.floor(hh/(16*DPR)));
        for(let r=1;r<rows;r++){ const fy=top+(hh/rows)*r; ctx.beginPath(); ctx.moveTo(bx+2*DPR,fy); ctx.lineTo(bx+w-2*DPR,fy); ctx.stroke(); } }
      ctx.fillStyle=getAccent(L.rim*1.4); ctx.fillRect(bx, top, w, 1.6*DPR);
    });
  }
}
// ---- CANDYLAND (reworked 06-30): vivid candy world - bright frosting hills w/ sprinkles, spinning peppermint lollipops + candy canes, drifting wrapped candies ----
function scapeCandyland(biome,horizon,t){
  const CANDY=["#ff5c8a","#ff9ec4","#7ad7ff","#ffd54d","#a0e86a","#c58bff"];
  // BIG floating GUMMY BEARS drifting high in the sky - glossy + lit-up so the sky isn't just empty
  (function(){ const GB=["#ff4d7e","#ffd23d","#4dc3ff","#8ee65a","#c58bff","#ff8ac0"], D=DPR;
    function gummy(x,y,s,col){ ctx.save(); ctx.translate(x,y);
      ctx.shadowColor=col; ctx.shadowBlur=26*D; ctx.fillStyle=col; ctx.globalAlpha=0.62;
      ctx.beginPath(); ctx.ellipse(0,s*0.15,s*0.52,s*0.62,0,0,6.283); ctx.fill();            // body
      ctx.beginPath(); ctx.arc(0,-s*0.55,s*0.42,0,6.283); ctx.fill();                          // head
      ctx.beginPath(); ctx.arc(-s*0.34,-s*0.84,s*0.17,0,6.283); ctx.fill(); ctx.beginPath(); ctx.arc(s*0.34,-s*0.84,s*0.17,0,6.283); ctx.fill(); // ears
      ctx.beginPath(); ctx.ellipse(-s*0.52,-s*0.02,s*0.19,s*0.28,0.5,0,6.283); ctx.fill(); ctx.beginPath(); ctx.ellipse(s*0.52,-s*0.02,s*0.19,s*0.28,-0.5,0,6.283); ctx.fill(); // arms
      ctx.beginPath(); ctx.ellipse(-s*0.27,s*0.66,s*0.24,s*0.2,0,0,6.283); ctx.fill(); ctx.beginPath(); ctx.ellipse(s*0.27,s*0.66,s*0.24,s*0.2,0,0,6.283); ctx.fill(); // legs
      ctx.shadowBlur=0; ctx.globalAlpha=0.9; ctx.fillStyle='rgba(255,255,255,0.8)';           // glossy shine
      ctx.beginPath(); ctx.ellipse(-s*0.14,-s*0.62,s*0.12,s*0.18,-0.4,0,6.283); ctx.fill();
      ctx.beginPath(); ctx.ellipse(-s*0.16,s*0.04,s*0.09,s*0.22,-0.3,0,6.283); ctx.fill();
      ctx.globalAlpha=0.55; ctx.fillStyle='rgba(70,20,55,0.9)';                                 // tiny eyes
      ctx.beginPath(); ctx.arc(-s*0.15,-s*0.55,s*0.05,0,6.283); ctx.fill(); ctx.beginPath(); ctx.arc(s*0.15,-s*0.55,s*0.05,0,6.283); ctx.fill();
      ctx.restore(); }
    const gap=520*D, f=0.06, drift=t*6*D, shift=cam*f+drift, i0=Math.floor((shift-gap)/gap), i1=Math.ceil((shift+W+gap)/gap);
    for(let k=i0;k<=i1;k++){ if(hashS(k*1.7+3)<0.4) continue; const x=k*gap-shift, s=(46+34*hashY(k*3))*D, y=horizon*(0.15+0.48*hashY(k*7))+Math.sin(t*0.4+k)*10*D; gummy(x,y,s,GB[Math.floor(hashS(k)*GB.length)%GB.length]); }
  })();
  const hills=[{f:0.09,h:0.14,gap:260,c:"#ffc8e6"},{f:0.20,h:0.20,gap:190,c:"#ff9ecb"}];
  for(const L of hills){ const gap=L.gap*DPR, shift=cam*L.f, i0=Math.floor((shift-gap)/gap), i1=Math.ceil((shift+W+gap)/gap);
    ctx.fillStyle=L.c; ctx.beginPath(); ctx.moveTo(i0*gap-shift,horizon);
    for(let k=i0;k<=i1;k++){ const x=k*gap-shift, hh=horizon*L.h*(0.7+0.5*hashY(k)); ctx.lineTo(x-gap*0.5,horizon); ctx.quadraticCurveTo(x-gap*0.28,horizon-hh,x,horizon-hh); ctx.quadraticCurveTo(x+gap*0.28,horizon-hh,x+gap*0.5,horizon); }
    ctx.lineTo(i1*gap-shift,horizon); ctx.closePath(); ctx.fill();
    for(let s=0;s<40;s++){ const sx=(((s*63*DPR-cam*L.f)%W)+W)%W, sy=horizon-horizon*L.h*hashY(s*3+1)*0.9; ctx.fillStyle=CANDY[s%CANDY.length]; ctx.save(); ctx.translate(sx,sy); ctx.rotate(s); ctx.fillRect(-3*DPR,-1*DPR,6*DPR,2*DPR); ctx.restore(); } }
  const gap=150*DPR, f=0.38, shift=cam*f, i0=Math.floor((shift-gap)/gap), i1=Math.ceil((shift+W+gap)/gap);
  ctx.lineCap='round';
  for(let k=i0;k<=i1;k++){ const x=k*gap-shift, hh=horizon*(0.24+0.14*hashY(k*3+1)), top=horizon-hh;
    if(hashS(k)>0.5){ const r=gap*(0.16+0.05*hashY(k*7)), cy=top+r;
      ctx.strokeStyle="#f3f3f7"; ctx.lineWidth=Math.max(4*DPR,gap*0.05); ctx.beginPath(); ctx.moveTo(x,horizon); ctx.lineTo(x,cy+r*0.6); ctx.stroke();
      ctx.save(); ctx.translate(x,cy); ctx.rotate(t*0.6+k);
      ctx.fillStyle="#fff"; ctx.beginPath(); ctx.arc(0,0,r,0,6.283); ctx.fill();
      ctx.fillStyle=(k&1)?"#ff3d6e":"#ff7ab0"; for(let a=0;a<6.283;a+=6.283/6){ ctx.beginPath(); ctx.moveTo(0,0); ctx.arc(0,0,r,a,a+6.283/12); ctx.closePath(); ctx.fill(); }
      ctx.strokeStyle="rgba(255,255,255,.9)"; ctx.lineWidth=2*DPR; ctx.beginPath(); ctx.arc(0,0,r,0,6.283); ctx.stroke(); ctx.restore();
    } else { const w=Math.max(6*DPR,gap*0.06), r=w*1.4;
      ctx.lineWidth=w; ctx.strokeStyle="#ffffff"; ctx.beginPath(); ctx.moveTo(x,horizon); ctx.lineTo(x,top+r); ctx.arc(x+r,top+r,r,Math.PI,Math.PI*1.5); ctx.stroke();
      ctx.strokeStyle="#ff3d5e"; ctx.lineWidth=w*0.5; for(let sY=top+r; sY<horizon-w; sY+=w*1.1){ ctx.beginPath(); ctx.moveTo(x-w*0.4,sY+w*0.5); ctx.lineTo(x+w*0.4,sY-w*0.5); ctx.stroke(); } } }
  const sg=160*DPR, sf=0.6, sdrift=t*18*DPR, sshift=cam*sf+sdrift, j0=Math.floor((sshift-sg)/sg), j1=Math.ceil((sshift+W+sg)/sg);
  for(let k=j0;k<=j1;k++){ const x=k*sg-sshift, y=horizon*(0.08+0.5*hashY(k*5+3))+Math.sin(t*0.8+k)*7*DPR, rot=k*1.3+t*0.4, c=CANDY[((k%CANDY.length)+CANDY.length)%CANDY.length];
    ctx.save(); ctx.translate(x,y); ctx.rotate(rot);
    ctx.fillStyle=c; rr(ctx,-6*DPR,-4*DPR,12*DPR,8*DPR,3*DPR); ctx.fill();
    ctx.beginPath(); ctx.moveTo(-6*DPR,0); ctx.lineTo(-11*DPR,-4*DPR); ctx.lineTo(-11*DPR,4*DPR); ctx.closePath(); ctx.fill();
    ctx.beginPath(); ctx.moveTo(6*DPR,0); ctx.lineTo(11*DPR,-4*DPR); ctx.lineTo(11*DPR,4*DPR); ctx.closePath(); ctx.fill();
    ctx.fillStyle="rgba(255,255,255,.5)"; rr(ctx,-3*DPR,-2*DPR,3*DPR,4*DPR,1*DPR); ctx.fill(); ctx.restore(); }
}
// ---- EMBERGLIDE (06-30 bespoke): blazing glide sky - low molten sun w/ bloom + glowing warm ember-canyon ridges w/ lava-vein cracks + rising sparks ----
function scapeEmberglide(biome,horizon,t){
  const sunX=W*0.72-cam*0.02, sunY=horizon*0.5, sunR=horizon*0.2;
  ctx.save(); ctx.globalCompositeOperation="lighter";
  const sg=ctx.createRadialGradient(sunX,sunY,0,sunX,sunY,sunR*2.6); sg.addColorStop(0,"rgba(255,240,180,0.95)"); sg.addColorStop(0.32,"rgba(255,150,40,0.65)"); sg.addColorStop(1,"rgba(255,60,20,0)");
  ctx.fillStyle=sg; ctx.beginPath(); ctx.arc(sunX,sunY,sunR*2.6,0,6.283); ctx.fill();
  ctx.fillStyle="rgba(255,228,155,0.92)"; ctx.beginPath(); ctx.arc(sunX,sunY,sunR*0.8,0,6.283); ctx.fill(); ctx.restore();
  const layers=[{f:0.08,h:0.30,gap:360,c0:"#5a1608",c1:"#a8320c",a:0.9},{f:0.20,h:0.46,gap:300,c0:"#340c05",c1:"#7a2208",a:1}];
  for(const L of layers){ const gap=L.gap*DPR, shift=cam*L.f, i0=Math.floor((shift-gap)/gap), i1=Math.ceil((shift+W+gap)/gap);
    ctx.beginPath(); ctx.moveTo(i0*gap-shift,horizon);
    for(let k=i0;k<=i1;k++){ const x=k*gap-shift, hh=horizon*L.h*(0.6+0.6*hashY(k*3)); ctx.lineTo(x-gap*0.5,horizon-hh*0.5); ctx.lineTo(x,horizon-hh); ctx.lineTo(x+gap*0.5,horizon-hh*0.6); }
    ctx.lineTo(i1*gap-shift,horizon); ctx.closePath();
    const rg=ctx.createLinearGradient(0,horizon*0.4,0,horizon); rg.addColorStop(0,L.c1); rg.addColorStop(1,L.c0); ctx.save(); ctx.globalAlpha=L.a; ctx.fillStyle=rg; ctx.fill(); ctx.restore();
    ctx.save(); ctx.globalCompositeOperation="lighter"; ctx.strokeStyle="rgba(255,140,40,0.5)"; ctx.lineWidth=2*DPR; ctx.lineCap="round";
    for(let k=i0;k<=i1;k++){ const x=k*gap-shift, hh=horizon*L.h*(0.6+0.6*hashY(k*3)); ctx.beginPath(); ctx.moveTo(x,horizon-hh); ctx.lineTo(x+gap*0.16,horizon-hh*0.5); ctx.lineTo(x-gap*0.1,horizon*0.98); ctx.stroke(); }
    ctx.restore(); }
  ctx.save(); ctx.globalCompositeOperation="lighter";
  for(let i=0;i<34;i++){ const span=W+80*DPR, x=(((i*79*DPR-cam*0.35)%span)+span)%span, y=horizon-((t*40*DPR+i*53*DPR)%(horizon*0.95)), a=0.6*(1-(horizon-y)/(horizon*0.95));
    ctx.fillStyle="rgba(255,"+(140+Math.floor(90*hashY(i)))+",50,"+Math.max(0,a)+")"; ctx.beginPath(); ctx.arc(x,y,(1.2+1.6*hashY(i*3))*DPR,0,6.283); ctx.fill(); } ctx.restore();
}
// ---- SUNSPURR (06-30 bespoke): molten hellscape - eclipsed sun w/ corona, lava-lake horizon glow, black volcanic ridges w/ cascading glowing lava rivers + fire motes ----
function scapeSunspurr(biome,horizon,t){
  ctx.save(); ctx.globalCompositeOperation="lighter"; const lg=ctx.createLinearGradient(0,horizon*0.68,0,horizon); lg.addColorStop(0,"rgba(255,90,20,0)"); lg.addColorStop(1,"rgba(255,150,40,0.5)"); ctx.fillStyle=lg; ctx.fillRect(0,horizon*0.68,W,horizon*0.32); ctx.restore();
  const sunX=W*0.5-cam*0.03, sunY=horizon*0.42, sunR=horizon*0.28;
  ctx.save(); ctx.globalCompositeOperation="lighter"; const sg=ctx.createRadialGradient(sunX,sunY,sunR*0.5,sunX,sunY,sunR*1.8); sg.addColorStop(0,"rgba(255,120,30,0)"); sg.addColorStop(0.5,"rgba(255,90,20,0.5)"); sg.addColorStop(1,"rgba(255,40,10,0)"); ctx.fillStyle=sg; ctx.beginPath(); ctx.arc(sunX,sunY,sunR*1.8,0,6.283); ctx.fill(); ctx.restore();
  ctx.fillStyle="#2a0a06"; ctx.beginPath(); ctx.arc(sunX,sunY,sunR,0,6.283); ctx.fill();
  ctx.save(); ctx.globalCompositeOperation="lighter"; ctx.strokeStyle="rgba(255,160,60,"+(0.6+0.3*Math.sin(t*1.3))+")"; ctx.lineWidth=2.5*DPR; ctx.beginPath(); ctx.arc(sunX,sunY,sunR,0,6.283); ctx.stroke(); ctx.restore();
  const layers=[{f:0.12,h:0.34,gap:340},{f:0.26,h:0.52,gap:280}];
  for(const L of layers){ const gap=L.gap*DPR, shift=cam*L.f, i0=Math.floor((shift-gap)/gap), i1=Math.ceil((shift+W+gap)/gap);
    ctx.fillStyle="#160604"; ctx.beginPath(); ctx.moveTo(i0*gap-shift,horizon);
    for(let k=i0;k<=i1;k++){ const x=k*gap-shift, hh=horizon*L.h*(0.7+0.5*hashY(k*3)); ctx.lineTo(x-gap*0.4,horizon-hh*0.4); ctx.lineTo(x,horizon-hh); ctx.lineTo(x+gap*0.4,horizon-hh*0.55); }
    ctx.lineTo(i1*gap-shift,horizon); ctx.closePath(); ctx.fill();
    ctx.save(); ctx.globalCompositeOperation="lighter";
    for(let k=i0;k<=i1;k++){ if(hashS(k*3)<0.5) continue; const x=k*gap-shift, hh=horizon*L.h*(0.7+0.5*hashY(k*3));
      const grd=ctx.createLinearGradient(x,horizon-hh,x,horizon); grd.addColorStop(0,"rgba(255,220,120,0.9)"); grd.addColorStop(0.5,"rgba(255,110,30,0.8)"); grd.addColorStop(1,"rgba(255,60,20,0.3)");
      ctx.strokeStyle=grd; ctx.lineWidth=(2.5+2*hashS(k))*DPR; ctx.lineCap="round"; ctx.beginPath(); ctx.moveTo(x,horizon-hh);
      let px=x, py=horizon-hh; for(let s=1;s<=5;s++){ px+=(hashS(k*7+s)-0.5)*gap*0.2; py+=hh*0.2; ctx.lineTo(px,py); } ctx.stroke(); }
    ctx.restore(); }
  ctx.save(); ctx.globalCompositeOperation="lighter"; for(let i=0;i<24;i++){ const span=W+60*DPR, x=(((i*91*DPR-cam*0.4)%span)+span)%span, y=horizon-(((t*30*DPR+i*61*DPR)%(horizon*0.9))); ctx.fillStyle="rgba(255,170,70,"+(0.5*(1-(horizon-y)/(horizon*0.9)))+")"; ctx.beginPath(); ctx.arc(x,y,(1.5+1.5*hashY(i*5))*DPR,0,6.283); ctx.fill(); } ctx.restore();
}
// ---- VOLTLOOM (06-30 bespoke): neon electric storm - charged sky, tesla pylons w/ glowing capacitor tips arcing forked bolts between them + flash ----
function scapeVoltloom(biome,horizon,t){
  ctx.save(); ctx.globalCompositeOperation="lighter"; const hg=ctx.createLinearGradient(0,horizon*0.55,0,horizon); hg.addColorStop(0,"rgba(120,30,220,0)"); hg.addColorStop(1,"rgba(180,40,200,0.35)"); ctx.fillStyle=hg; ctx.fillRect(0,horizon*0.55,W,horizon*0.45); ctx.restore();
  const fl=Math.sin(t*0.9)*Math.sin(t*2.7); if(fl>0.9){ ctx.save(); ctx.globalCompositeOperation="lighter"; ctx.fillStyle="rgba(200,180,255,"+((fl-0.9)*2)+")"; ctx.fillRect(0,0,W,horizon); ctx.restore(); }
  const layers=[{f:0.14,h:0.34,gap:210,a:0.55},{f:0.32,h:0.52,gap:180,a:0.9}];
  for(const L of layers){ const gap=L.gap*DPR, shift=cam*L.f, i0=Math.floor((shift-gap)/gap), i1=Math.ceil((shift+W+gap)/gap); let prevTip=null;
    for(let k=i0;k<=i1;k++){ const x=k*gap-shift, hh=horizon*L.h*(0.7+0.6*hashY(k*3)), top=horizon-hh, w=gap*0.1;
      ctx.fillStyle=silhouetteColor(biome.sky[2],L.a,0.7); ctx.beginPath(); ctx.moveTo(x-w,horizon); ctx.lineTo(x-w*0.3,top); ctx.lineTo(x+w*0.3,top); ctx.lineTo(x+w,horizon); ctx.closePath(); ctx.fill();
      const blink=0.5+0.5*Math.sin(t*3+k*1.7);
      ctx.save(); ctx.globalCompositeOperation="lighter"; const gl=ctx.createRadialGradient(x,top,0,x,top,10*DPR); gl.addColorStop(0,getAccent(0.9*blink)); gl.addColorStop(1,getAccent(0)); ctx.fillStyle=gl; ctx.beginPath(); ctx.arc(x,top,10*DPR,0,6.283); ctx.fill(); ctx.restore();
      if(prevTip && Math.sin(t*4+k*2.1)>0.3){ ctx.save(); ctx.globalCompositeOperation="lighter"; ctx.strokeStyle=getAccent(0.7); ctx.lineWidth=1.8*DPR; ctx.beginPath(); ctx.moveTo(prevTip.x,prevTip.y);
        const segs=5; for(let s=1;s<=segs;s++){ const f2=s/segs, mx=prevTip.x+(x-prevTip.x)*f2, my=prevTip.y+(top-prevTip.y)*f2+(hashS(k*9+s+Math.floor(t*10))-0.5)*22*DPR; ctx.lineTo(mx,my); } ctx.stroke(); ctx.restore(); }
      prevTip={x,y:top}; } }
}
// ---- SPOREBLOOM (06-30 bespoke): vivid Mario-style mushroom forest - bright candy-colored caps w/ white spots + creamy stalks + glowing halos + drifting spores ----
function scapeSporebloom(biome,horizon,t){
  const CAPS=["#ff3b3b","#ff8a2b","#ffd23b","#8a5cff","#2bd6ff","#ff5ca8"];
  { const gap=240*DPR, f=0.09, shift=cam*f, i0=Math.floor((shift-gap)/gap), i1=Math.ceil((shift+W+gap)/gap); ctx.fillStyle=silhouetteColor(biome.sky[2],0.4,1.3);
    ctx.beginPath(); ctx.moveTo(i0*gap-shift,horizon); for(let k=i0;k<=i1;k++){ const x=k*gap-shift, hh=horizon*(0.1+0.07*hashY(k)); ctx.lineTo(x-gap*0.5,horizon); ctx.quadraticCurveTo(x,horizon-hh,x+gap*0.5,horizon); } ctx.lineTo(i1*gap-shift,horizon); ctx.closePath(); ctx.fill(); }
  const layers=[{f:0.20,scale:0.7,gap:230,dim:0.75},{f:0.42,scale:1.0,gap:180,dim:1.0}];
  for(const L of layers){ const gap=L.gap*DPR, shift=cam*L.f, i0=Math.floor((shift-gap)/gap), i1=Math.ceil((shift+W+gap)/gap);
    for(let k=i0;k<=i1;k++){ if(hashS(k*3)<0.3) continue; const x=k*gap-shift;
      const stalkH=horizon*(0.16+0.14*hashY(k*5))*L.scale, top=horizon-stalkH;
      const capW=gap*(0.26+0.1*hashS(k*7))*L.scale, capH=capW*0.72, col=CAPS[((k%CAPS.length)+CAPS.length)%CAPS.length], bob=Math.sin(t*0.6+k*1.7)*4*DPR, sw=capW*0.28;
      ctx.fillStyle=silhouetteColor("#fff4e0",0.85*L.dim,1); ctx.beginPath(); ctx.moveTo(x-sw*0.5,horizon); ctx.quadraticCurveTo(x-sw*0.6,top+bob+capH*0.3,x-sw*0.4,top+bob+capH*0.1); ctx.lineTo(x+sw*0.4,top+bob+capH*0.1); ctx.quadraticCurveTo(x+sw*0.6,top+bob+capH*0.3,x+sw*0.5,horizon); ctx.closePath(); ctx.fill();
      ctx.save(); ctx.globalCompositeOperation="lighter"; const gl=ctx.createRadialGradient(x,top+bob,0,x,top+bob,capW*1.3); gl.addColorStop(0,silhouetteColor(col,0.35*L.dim,1.4)); gl.addColorStop(1,silhouetteColor(col,0,1)); ctx.fillStyle=gl; ctx.beginPath(); ctx.arc(x,top+bob,capW*1.3,0,6.283); ctx.fill(); ctx.restore();
      ctx.fillStyle=silhouetteColor(col,0.95*L.dim,1); ctx.beginPath(); ctx.ellipse(x,top+bob+capH*0.15,capW,capH,0,Math.PI,2*Math.PI); ctx.closePath(); ctx.fill();
      ctx.fillStyle=silhouetteColor("#ffe6c2",0.7*L.dim,1); ctx.beginPath(); ctx.ellipse(x,top+bob+capH*0.15,capW,capH*0.16,0,0,Math.PI); ctx.fill();
      ctx.fillStyle="rgba(255,255,255,"+(0.92*L.dim)+")"; for(let s=0;s<4;s++){ const sx=x+(hashS(k*11+s)-0.5)*capW*1.3, sy=top+bob+capH*0.15-hashY(k*13+s)*capH*0.7, sr=(3+3*hashS(k*17+s))*DPR*L.scale; if(Math.abs(sx-x)>capW*0.9) continue; ctx.beginPath(); ctx.ellipse(sx,sy,sr,sr*0.8,0,0,6.283); ctx.fill(); } } }
  ctx.save(); ctx.globalCompositeOperation="lighter"; for(let i=0;i<30;i++){ const span=horizon*1.05, x=(((hashY(i*1.9)*W-cam*0.5+Math.sin(t*0.5+i*2)*14*DPR)%W)+W)%W, y=horizon-((t*(10+16*hashS(i))*DPR+hashY(i*4.7)*span)%span), r=(1.2+1.8*hashS(i*3))*DPR, tw=0.4+0.6*(0.5+0.5*Math.sin(t*2+i)); ctx.fillStyle=getAccent(0.5*tw); ctx.beginPath(); ctx.arc(x,y,r,0,6.283); ctx.fill(); } ctx.restore();
}
// ---- SAKURA GROVE (06-30 bespoke): lush cherry-blossom grove - soft pink moon, big vivid blossom canopies (shadow+top-lit puffs) w/ sparkle highlights + glowing lanterns + drifting petals ----
function scapeSakuraGrove(biome,horizon,t){
  { const mx=W*0.22-cam*0.03, my=horizon*0.34, mr=horizon*0.16; ctx.save(); ctx.globalCompositeOperation="lighter"; const mg=ctx.createRadialGradient(mx,my,0,mx,my,mr*1.9); mg.addColorStop(0,"rgba(255,220,235,0.9)"); mg.addColorStop(0.5,"rgba(255,180,215,0.4)"); mg.addColorStop(1,"rgba(255,150,200,0)"); ctx.fillStyle=mg; ctx.beginPath(); ctx.arc(mx,my,mr*1.9,0,6.283); ctx.fill(); ctx.fillStyle="rgba(255,236,245,0.95)"; ctx.beginPath(); ctx.arc(mx,my,mr*0.7,0,6.283); ctx.fill(); ctx.restore(); }
  // strung LANTERN GARLANDS across the sky (replaces the old per-tree lanterns) - sagging wires w/ glowing paper lanterns
  { const D=DPR, f=0.16, gap=360*D, sag=42*D, shift=cam*f, i0=Math.floor((shift-gap)/gap), i1=Math.ceil((shift+W+gap)/gap), rows=[horizon*0.30, horizon*0.52], LC=["#ff6a8a","#ffd23d","#ff9a5a","#c58bff","#7ad7ff"];
    for(let ri=0;ri<rows.length;ri++){ const baseY=rows[ri]+Math.sin(t*0.2+ri)*3*D;
      ctx.strokeStyle="rgba(70,45,60,0.55)"; ctx.lineWidth=1.6*D; ctx.beginPath();
      for(let k=i0;k<=i1;k++){ const x0=k*gap-shift, x1=(k+1)*gap-shift; for(let s=0;s<=8;s++){ const tt=s/8, xx=x0+(x1-x0)*tt, yy=baseY+Math.sin(tt*Math.PI)*sag; (k===i0&&s===0)?ctx.moveTo(xx,yy):ctx.lineTo(xx,yy); } }
      ctx.stroke();
      for(let k=i0;k<=i1;k++){ for(let j=1;j<=3;j++){ const tt=j/4, x=k*gap-shift+gap*tt, y=baseY+Math.sin(tt*Math.PI)*sag, col=LC[((k*3+j)%LC.length+LC.length)%LC.length], s=(9+3*hashY(k*7+j))*D, gy=y+s*1.1;
          ctx.save(); ctx.globalCompositeOperation="lighter"; const gl=ctx.createRadialGradient(x,gy,0,x,gy,s*2.6); gl.addColorStop(0,col); gl.addColorStop(1,col+"00"); ctx.globalAlpha=0.55; ctx.fillStyle=gl; ctx.beginPath(); ctx.arc(x,gy,s*2.6,0,6.283); ctx.fill(); ctx.restore();
          ctx.strokeStyle="rgba(70,45,60,0.55)"; ctx.lineWidth=1.2*D; ctx.beginPath(); ctx.moveTo(x,y); ctx.lineTo(x,y+s*0.45); ctx.stroke();
          ctx.fillStyle=col; ctx.beginPath(); ctx.ellipse(x,gy,s*0.7,s*0.9,0,0,6.283); ctx.fill();
          ctx.fillStyle="rgba(255,255,255,0.4)"; ctx.beginPath(); ctx.ellipse(x-s*0.22,gy-s*0.22,s*0.18,s*0.3,0,0,6.283); ctx.fill();
          ctx.fillStyle="rgba(50,30,45,0.7)"; ctx.fillRect(x-s*0.34,gy-s*0.98,s*0.68,s*0.14); ctx.fillRect(x-s*0.34,gy+s*0.84,s*0.68,s*0.14);
        } }
    }
  }
  const layers=[{f:0.10,scale:0.7,gap:320,blos:"#ffbcdd",dim:0.6},{f:0.26,scale:1.0,gap:240,blos:"#ff8fc4",dim:1.0}];
  for(const L of layers){ const gap=L.gap*DPR, shift=cam*L.f, i0=Math.floor((shift-gap)/gap), i1=Math.ceil((shift+W+gap)/gap);
    for(let k=i0;k<=i1;k++){ const x=k*gap-shift, th=horizon*(0.34+0.14*hashY(k*3))*L.scale, top=horizon-th, tw=Math.max(3*DPR,gap*0.03);
      ctx.strokeStyle=silhouetteColor("#5a3a4a",0.85*L.dim,1); ctx.lineWidth=tw; ctx.lineCap="round"; ctx.beginPath(); ctx.moveTo(x,horizon); ctx.quadraticCurveTo(x+6*DPR,top+th*0.5,x,top+th*0.28); ctx.stroke();
      const cy=top+th*0.16, cw=gap*(0.36+0.08*hashS(k))*L.scale, puffs=[[0,0,1],[-0.5,0.12,0.7],[0.5,0.12,0.7],[-0.28,-0.18,0.62],[0.28,-0.18,0.62],[0,0.22,0.66]], sway=Math.sin(t*0.5+k)*3*DPR;
      for(const p of puffs){ ctx.fillStyle=silhouetteColor(L.blos,0.9*L.dim,0.82); ctx.beginPath(); ctx.arc(x+p[0]*cw+sway,cy+p[1]*cw,cw*0.5*p[2],0,6.283); ctx.fill(); }
      for(const p of puffs){ ctx.fillStyle=silhouetteColor(L.blos,0.9*L.dim,1.25); ctx.beginPath(); ctx.arc(x+p[0]*cw-cw*0.12+sway,cy+p[1]*cw-cw*0.12,cw*0.4*p[2],0,6.283); ctx.fill(); }
      ctx.save(); ctx.globalCompositeOperation="lighter"; for(let s=0;s<5;s++){ const sx=x+(hashS(k*11+s)-0.5)*cw*1.4, sy=cy+(hashY(k*13+s)-0.5)*cw*1.2; ctx.fillStyle="rgba(255,240,248,"+(0.3+0.3*Math.sin(t*2+k+s))+")"; ctx.beginPath(); ctx.arc(sx,sy,1.6*DPR,0,6.283); ctx.fill(); } ctx.restore();
    } }
  ctx.save(); for(let i=0;i<26;i++){ const span=horizon*1.1, x=(((hashY(i*2.3)*W-cam*0.5+Math.sin(t*0.6+i)*30*DPR)%W)+W)%W, y=((t*(14+10*hashS(i))*DPR+hashY(i*3.7)*span)%span), rot=t*1.5+i; ctx.save(); ctx.translate(x,y); ctx.rotate(rot); ctx.fillStyle="rgba(255,175,210,"+(0.5+0.3*hashS(i))+")"; ctx.beginPath(); ctx.ellipse(0,0,4*DPR,2*DPR,0,0,6.283); ctx.fill(); ctx.restore(); } ctx.restore();
}
function scapeReef(biome,horizon,t){
  {
    const gap=180*DPR, f=0.11, shift=cam*f;
    const i0=Math.floor((shift-gap)/gap), i1=Math.ceil((shift+W+gap)/gap);
    const baseH=0.10*horizon, varH=0.09*horizon;
    ctx.fillStyle=silhouetteColor(biome.sky[2],0.42,1.5);
    ctx.beginPath(); ctx.moveTo(i0*gap-shift, horizon);
    for(let k=i0;k<=i1;k++){ const x=k*gap-shift; const hh=baseH+varH*(0.4+0.6*hashY(k));
      ctx.lineTo(x-gap*0.5, horizon);
      ctx.quadraticCurveTo(x-gap*0.28, horizon-hh, x, horizon-hh);
      ctx.quadraticCurveTo(x+gap*0.28, horizon-hh, x+gap*0.5, horizon); }
    ctx.lineTo(i1*gap-shift, horizon); ctx.closePath(); ctx.fill();
  }
  {
    const gap=260*DPR, f=0.20, fdrift=t*10*DPR, shift=cam*f+fdrift;
    const i0=Math.floor((shift-gap)/gap), i1=Math.ceil((shift+W+gap)/gap);
    for(let k=i0;k<=i1;k++){
      const x=k*gap-shift;
      const y=horizon*(0.18+0.42*hashY(k*5)) + Math.sin(t*0.5+k)*5*DPR;
      const s=gap*(0.05+0.03*hashS(k));
      ctx.fillStyle=silhouetteColor(biome.sky[1],0.22,0.85);
      ctx.beginPath(); ctx.ellipse(x,y,s,s*0.5,0,0,6.283); ctx.fill();
      ctx.beginPath(); ctx.moveTo(x-s,y); ctx.lineTo(x-s*1.7,y-s*0.5); ctx.lineTo(x-s*1.7,y+s*0.5); ctx.closePath(); ctx.fill();
    }
  }
  {
    const gap=100*DPR, f=0.32, shift=cam*f;
    const i0=Math.floor((shift-gap)/gap), i1=Math.ceil((shift+W+gap)/gap);
    ctx.lineCap='round';
    for(let k=i0;k<=i1;k++){
      const x=k*gap-shift;
      const hh=horizon*(0.16+0.16*hashY(k*3));
      const fronds=3+Math.floor(hashS(k)*3);
      const sway=Math.sin(t*0.7+k)*0.10;
      ctx.strokeStyle=silhouetteColor(biome.sky[1],0.40,0.95); ctx.lineWidth=3*DPR;
      for(let fi=0;fi<fronds;fi++){
        const a=(fi/(fronds-1)-0.5)*1.1 + sway;
        const tx=x+Math.sin(a)*hh*0.7, ty=horizon-hh*(0.7+0.3*hashY(k*7+fi));
        ctx.beginPath(); ctx.moveTo(x,horizon);
        ctx.quadraticCurveTo(x+Math.sin(a)*hh*0.25, horizon-hh*0.5, tx, ty); ctx.stroke();
      }
      ctx.fillStyle=getAccent(0.16);
      ctx.beginPath(); ctx.arc(x, horizon-hh*0.95, 4*DPR, 0, 6.283); ctx.fill();
    }
  }
  {
    const gap=74*DPR, f=0.5, shift=cam*f;
    const i0=Math.floor((shift-gap)/gap), i1=Math.ceil((shift+W+gap)/gap);
    ctx.lineCap='round';
    for(let k=i0;k<=i1;k++){
      const x=k*gap-shift;
      const hh=horizon*(0.16+0.20*hashY(k*9));
      ctx.strokeStyle=silhouetteColor(biome.sky[1],0.34,0.7); ctx.lineWidth=(3+2*hashS(k))*DPR;
      const segs=4; ctx.beginPath(); ctx.moveTo(x,horizon);
      let px=x, py=horizon;
      for(let s=1;s<=segs;s++){
        const fr=s/segs; const sway=Math.sin(t*1.1 + k + fr*2.2)*hh*0.16*fr;
        const nx=x+sway, ny=horizon-hh*fr;
        ctx.quadraticCurveTo(px+sway*0.5, (py+ny)/2, nx, ny); px=nx; py=ny;
      }
      ctx.stroke();
    }
  }
  {
    const gap=110*DPR, f=0.45, shift=cam*f;
    const i0=Math.floor((shift-gap)/gap), i1=Math.ceil((shift+W+gap)/gap);
    const range=horizon*0.85;
    for(let k=i0;k<=i1;k++){
      const x=k*gap-shift + Math.sin(t*1.3+k)*8*DPR;
      const phase=(t*26*DPR + hashY(k*11)*range) % range;
      const y=horizon-phase;
      const r=(2+2.4*hashS(k*13))*DPR;
      ctx.strokeStyle=getAccent(0.20*(1-phase/range)+0.06); ctx.lineWidth=1.4*DPR;
      ctx.beginPath(); ctx.arc(x,y,r,0,6.283); ctx.stroke();
    }
  }
}
function scapeCyberCity(biome,horizon,t){
  {
    const vx=W*0.5, vy=horizon, top=horizon-horizon*0.30;
    ctx.strokeStyle=getAccent(0.05); ctx.lineWidth=1*DPR;
    for(let i=-7;i<=7;i++){ const fx=vx+i*W*0.085;
      ctx.beginPath(); ctx.moveTo(vx,vy); ctx.lineTo(fx,top); ctx.stroke(); }
    for(let r=1;r<=5;r++){ const fr=r/5; const off=((t*0.15)%1);
      const yy=vy-(vy-top)*Math.min(1,(fr+off*0.2)); ctx.strokeStyle=getAccent(0.05*(1-fr));
      ctx.beginPath(); ctx.moveTo(0,yy); ctx.lineTo(W,yy); ctx.stroke(); }
  }
  const layers=[
    {f:0.10, gap:92, base:0.20, vary:0.22, sky:2, mul:0.85, a:0.55, cols:2},
    {f:0.28, gap:66, base:0.24, vary:0.36, sky:1, mul:0.42, a:0.70, cols:3},
  ];
  for(const L of layers){
    const gap=L.gap*DPR, shift=cam*L.f;
    const i0=Math.floor((shift-gap)/gap), i1=Math.ceil((shift+W+gap)/gap);
    const baseH=L.base*horizon, varH=L.vary*horizon;
    for(let k=i0;k<=i1;k++){
      const w=gap*(0.5+0.32*hashS(k));
      const hh=baseH+varH*(0.12+0.88*hashY(k));
      const bx=k*gap-shift-w/2, top=horizon-hh;
      ctx.fillStyle=silhouetteColor(biome.sky[L.sky], L.a, L.mul);
      ctx.fillRect(bx, top, w, hh);
      ctx.fillStyle=getAccent(0.30); ctx.fillRect(bx, top, w, 1.8*DPR);
      if(hashY(k*17)>0.6){ ctx.fillStyle=getAccent(0.18); ctx.fillRect(bx+w*0.5-1*DPR, top-12*DPR, 2*DPR, 12*DPR); }
      const cols=L.cols, rows=Math.max(2,Math.floor(hh/(16*DPR)));
      const pad=w*0.16, cw=(w-pad*2)/cols, ch=12*DPR;
      for(let c=0;c<cols;c++) for(let r=0;r<rows;r++){
        const lit=hashS(k*31+r*7+c*3)>0.42;
        const al = lit ? (0.16+0.14*(0.5+0.5*Math.sin(t*1.6 + k + r*1.3 + c))) : 0.05;
        ctx.fillStyle=getAccent(al);
        ctx.fillRect(bx+pad+c*cw+cw*0.18, top+8*DPR+r*16*DPR, cw*0.64, ch*0.5);
      }
    }
  }
  for(let b=0;b<2;b++){
    const by=horizon*(0.18+b*0.16), bh=9*DPR;
    ctx.fillStyle=getAccent(0.05); ctx.fillRect(0,by,W,bh);
    const sp=240*DPR, drift=(t*(26+b*14)*DPR)%sp;
    for(let x=-sp+(sp-drift%sp); x<W+sp; x+=sp){
      ctx.fillStyle=getAccent(0.13);
      rr(ctx, x-70*DPR, by, 140*DPR, bh, bh*0.4); ctx.fill();
    }
    ctx.fillStyle=getAccent(0.10); ctx.fillRect(0,by,W,1*DPR);
  }
}
function scapeGraveyard(biome,horizon,t){
  {
    const mx=W*0.72 - cam*0.04, my=horizon*0.30, mr=44*DPR;
    const pulse=0.5+0.5*Math.sin(t*0.5);
    const g=ctx.createRadialGradient(mx,my,mr*0.3,mx,my,mr*2.6);
    g.addColorStop(0, silhouetteColor(biome.sky[0], 0.18+0.05*pulse, 2.0));
    g.addColorStop(1, silhouetteColor(biome.sky[0], 0, 2.0));
    ctx.fillStyle=g; ctx.beginPath(); ctx.arc(mx,my,mr*2.6,0,6.283); ctx.fill();
    ctx.fillStyle=silhouetteColor(biome.sky[0], 0.55, 2.1);
    ctx.beginPath(); ctx.arc(mx,my,mr,0,6.283); ctx.fill();
    ctx.fillStyle=silhouetteColor(biome.sky[0], 0.12, 1.4);
    ctx.beginPath(); ctx.arc(mx-mr*0.3,my-mr*0.2,mr*0.18,0,6.283); ctx.fill();
    ctx.beginPath(); ctx.arc(mx+mr*0.25,my+mr*0.25,mr*0.13,0,6.283); ctx.fill();
  }
  {
    const gap=200*DPR, f=0.10, shift=cam*f;
    const i0=Math.floor((shift-gap)/gap), i1=Math.ceil((shift+W+gap)/gap);
    const baseH=0.06*horizon, varH=0.05*horizon;
    ctx.fillStyle=silhouetteColor(biome.sky[2],0.5,1.6);
    ctx.beginPath(); ctx.moveTo(i0*gap-shift, horizon);
    for(let k=i0;k<=i1;k++){ const x=k*gap-shift; const hh=baseH+varH*(0.4+0.6*hashY(k));
      ctx.lineTo(x-gap*0.5,horizon); ctx.quadraticCurveTo(x, horizon-hh, x+gap*0.5, horizon); }
    ctx.lineTo(i1*gap-shift,horizon); ctx.closePath(); ctx.fill();
  }
  {
    const gap=150*DPR, f=0.26, shift=cam*f;
    const i0=Math.floor((shift-gap)/gap), i1=Math.ceil((shift+W+gap)/gap);
    ctx.lineCap='round';
    for(let k=i0;k<=i1;k++){
      if(hashS(k*3)<0.45) continue;
      const x=k*gap-shift;
      const hh=horizon*(0.20+0.16*hashY(k*5));
      const lean=(hashS(k)-0.5)*0.5;
      const topX=x+lean*hh, topY=horizon-hh;
      ctx.strokeStyle=silhouetteColor(biome.sky[2],0.5,0.7); ctx.lineWidth=4*DPR;
      ctx.beginPath(); ctx.moveTo(x,horizon);
      ctx.quadraticCurveTo(x+lean*hh*0.5, horizon-hh*0.55, topX, topY); ctx.stroke();
      ctx.lineWidth=2*DPR;
      for(let bch=0;bch<4;bch++){
        const fr=0.4+bch*0.16, bx=x+lean*hh*fr, by=horizon-hh*fr;
        const dir=(hashS(k*7+bch)>0.5?1:-1);
        const bl=hh*(0.14+0.16*hashY(k*11+bch));
        const sway=Math.sin(t*0.6+k+bch)*bl*0.06;
        ctx.beginPath(); ctx.moveTo(bx,by);
        ctx.quadraticCurveTo(bx+dir*bl*0.5, by-bl*0.3, bx+dir*bl+sway, by-bl*0.6); ctx.stroke();
      }
    }
  }
  {
    const gap=84*DPR, f=0.44, shift=cam*f;
    const i0=Math.floor((shift-gap)/gap), i1=Math.ceil((shift+W+gap)/gap);
    for(let k=i0;k<=i1;k++){
      const x=k*gap-shift;
      const kind=hashY(k*13);
      ctx.fillStyle=silhouetteColor(biome.sky[2],0.6,0.85);
      if(kind>0.7){
        const hh=horizon*(0.10+0.05*hashS(k)), w=5*DPR, top=horizon-hh;
        ctx.fillRect(x-w/2, top, w, hh);
        ctx.fillRect(x-w*1.8, top+hh*0.28, w*3.6, w);
      } else {
        const w=gap*(0.34+0.14*hashS(k)), hh=horizon*(0.08+0.06*hashY(k*7));
        const r=w/2, top=horizon-hh;
        ctx.beginPath(); ctx.moveTo(x-r,horizon); ctx.lineTo(x-r, top+r);
        ctx.arc(x, top+r, r, Math.PI, 0); ctx.lineTo(x+r,horizon); ctx.closePath(); ctx.fill();
      }
    }
  }
  {
    const gap=26*DPR, f=0.5, shift=cam*f;
    const i0=Math.floor((shift-gap)/gap), i1=Math.ceil((shift+W+gap)/gap);
    const fh=horizon*0.075, ftop=horizon-fh;
    ctx.fillStyle=silhouetteColor(biome.sky[2],0.45,0.55);
    ctx.fillRect(i0*gap-shift, ftop+fh*0.30, (i1-i0+1)*gap, 2.5*DPR);
    ctx.fillRect(i0*gap-shift, ftop+fh*0.70, (i1-i0+1)*gap, 2.5*DPR);
    for(let k=i0;k<=i1;k++){
      const x=k*gap-shift, w=6*DPR;
      ctx.fillRect(x-w/2, ftop, w, fh);
      ctx.beginPath(); ctx.moveTo(x-w/2,ftop); ctx.lineTo(x,ftop-7*DPR); ctx.lineTo(x+w/2,ftop); ctx.closePath(); ctx.fill();
    }
  }
  for(let b=0;b<3;b++){
    const fy=horizon - horizon*(0.02+b*0.06);
    const drift=t*(7+b*5)*DPR, sp=170*DPR;
    ctx.fillStyle=silhouetteColor(biome.sky[2], 0.05, 1.8);
    for(let x=-sp+(sp-drift%sp); x<W+sp; x+=sp){
      ctx.beginPath(); ctx.ellipse(x, fy, 130*DPR, 16*DPR, 0, 0, 6.283); ctx.fill();
    }
  }
}
function scapeSakura(biome,horizon,t){
  {
    const gap=540*DPR, f=0.07, shift=cam*f;
    const i0=Math.floor((shift-gap)/gap), i1=Math.ceil((shift+W+gap)/gap);
    for(let k=i0;k<=i1;k++){
      const x=k*gap-shift;
      const hh=horizon*(0.42+0.10*hashY(k));
      const bw=gap*(0.46+0.10*hashS(k));
      const top=horizon-hh, capH=hh*0.30;
      ctx.fillStyle=silhouetteColor(biome.sky[1], 0.34, 0.78);
      ctx.beginPath();
      ctx.moveTo(x-bw, horizon);
      ctx.quadraticCurveTo(x-bw*0.32, top+capH, x-bw*0.13, top);
      ctx.lineTo(x+bw*0.13, top);
      ctx.quadraticCurveTo(x+bw*0.32, top+capH, x+bw, horizon);
      ctx.closePath(); ctx.fill();
      ctx.fillStyle=silhouetteColor(biome.sky[0], 0.34, 2.2);
      ctx.beginPath();
      ctx.moveTo(x-bw*0.13, top); ctx.lineTo(x+bw*0.13, top);
      ctx.lineTo(x+bw*0.22, top+capH); ctx.lineTo(x+bw*0.10, top+capH*0.74);
      ctx.lineTo(x, top+capH*1.08); ctx.lineTo(x-bw*0.10, top+capH*0.70);
      ctx.lineTo(x-bw*0.22, top+capH);
      ctx.closePath(); ctx.fill();
    }
  }
  {
    const gap=250*DPR, f=0.24, shift=cam*f;
    const i0=Math.floor((shift-gap)/gap), i1=Math.ceil((shift+W+gap)/gap);
    for(let k=i0;k<=i1;k++){
      const x=k*gap-shift;
      const fill=silhouetteColor(biome.sky[1], 0.52, 0.55);
      if(hashS(k)>0.5){
        const baseH=horizon*(0.20+0.08*hashY(k)), top=horizon-baseH;
        const w=gap*0.30, tiers=4, rh=baseH*0.12;
        ctx.fillStyle=fill;
        ctx.fillRect(x-w*0.10, top+baseH*0.06, w*0.20, baseH*0.94);
        for(let ti=0;ti<tiers;ti++){
          const fr=ti/(tiers-1);
          const ty=top + (baseH*0.94)*fr + baseH*0.05;
          const rw=w*(0.32+0.68*fr);
          ctx.beginPath();
          ctx.moveTo(x-rw, ty);
          ctx.quadraticCurveTo(x, ty-rh*1.7, x+rw, ty);
          ctx.quadraticCurveTo(x, ty+rh*0.55, x-rw, ty);
          ctx.closePath(); ctx.fill();
        }
        ctx.fillRect(x-1.4*DPR, top-baseH*0.08, 2.8*DPR, baseH*0.08);
      } else {
        const gh=horizon*(0.13+0.05*hashY(k)), gw=gap*0.13, top=horizon-gh;
        const pw=Math.max(3.5*DPR, gw*0.20);
        ctx.fillStyle=fill;
        ctx.fillRect(x-gw, top, pw, gh);
        ctx.fillRect(x+gw-pw, top, pw, gh);
        ctx.fillRect(x-gw-pw*0.4, top+gh*0.30, gw*2+pw*0.8, pw*0.95);
        ctx.beginPath();
        ctx.moveTo(x-gw*1.5, top+pw*1.3);
        ctx.quadraticCurveTo(x-gw*1.45, top-pw*0.3, x, top-pw*0.6);
        ctx.quadraticCurveTo(x+gw*1.45, top-pw*0.3, x+gw*1.5, top+pw*1.3);
        ctx.lineTo(x+gw*1.28, top+pw*1.3);
        ctx.quadraticCurveTo(x, top+pw*0.4, x-gw*1.28, top+pw*1.3);
        ctx.closePath(); ctx.fill();
        ctx.fillStyle=getAccent(0.12);
        ctx.fillRect(x-gw*1.2, top-pw*0.1, gw*2.4, 1.3*DPR);
      }
    }
  }
  {
    const gap=150*DPR, f=0.46, shift=cam*f;
    const i0=Math.floor((shift-gap)/gap), i1=Math.ceil((shift+W+gap)/gap);
    ctx.lineCap='round';
    for(let k=i0;k<=i1;k++){
      const x=k*gap-shift;
      const hh=horizon*(0.16+0.10*hashY(k*3));
      const lean=(hashS(k)-0.5)*0.4;
      const topX=x+lean*hh, topY=horizon-hh;
      ctx.strokeStyle=silhouetteColor(biome.sky[1], 0.44, 0.40);
      ctx.lineWidth=4*DPR;
      ctx.beginPath(); ctx.moveTo(x,horizon);
      ctx.quadraticCurveTo(x+lean*hh*0.5, horizon-hh*0.6, topX, topY); ctx.stroke();
      const cr=hh*(0.52+0.14*hashS(k*5));
      ctx.fillStyle=getAccent(0.24);
      for(let b=0;b<7;b++){
        const ang=b/7*6.283 + k;
        const bx=topX+Math.cos(ang)*cr*0.55, by=topY-cr*0.25+Math.sin(ang)*cr*0.45;
        ctx.beginPath(); ctx.arc(bx,by, cr*(0.45+0.22*hashY(k*7+b)), 0, 6.283); ctx.fill();
      }
      ctx.fillStyle=getAccent(0.16);
      ctx.beginPath(); ctx.arc(topX, topY-cr*0.25, cr*0.7, 0, 6.283); ctx.fill();
    }
  }
  {
    const cols=46, f=0.5, span=horizon*1.04;
    for(let k=0;k<cols;k++){
      const baseX=hashY(k*2.1)*W;
      const sway=Math.sin(t*0.6 + k*1.7)*16*DPR;
      const x=(((baseX - cam*f + sway)%W)+W)%W;
      const speed=(22+30*hashS(k))*DPR;
      const y=((t*speed + hashY(k*5.3)*span)%span);
      const s=(2.4+2.0*hashS(k*3))*DPR, rot=t*1.2 + k;
      ctx.save(); ctx.translate(x,y); ctx.rotate(rot);
      ctx.fillStyle=getAccent(0.20+0.16*hashS(k*7));
      ctx.beginPath(); ctx.ellipse(0,0, s, s*0.55, 0, 0, 6.283); ctx.fill();
      ctx.restore();
    }
  }
}
function scapeDesert(biome,horizon,t){
  {
    const sx=W*0.30 - cam*0.03, sy=horizon*0.32, sr=52*DPR;
    const g=ctx.createRadialGradient(sx,sy,sr*0.3, sx,sy,sr*3.2);
    g.addColorStop(0, getAccent(0.30)); g.addColorStop(0.5, getAccent(0.10)); g.addColorStop(1, getAccent(0));
    ctx.fillStyle=g; ctx.beginPath(); ctx.arc(sx,sy,sr*3.2,0,6.283); ctx.fill();
    ctx.fillStyle=getAccent(0.32);
    ctx.beginPath(); ctx.arc(sx,sy,sr,0,6.283); ctx.fill();
  }
  {
    const gap=300*DPR, f=0.10, shift=cam*f;
    const i0=Math.floor((shift-gap)/gap), i1=Math.ceil((shift+W+gap)/gap);
    for(let k=i0;k<=i1;k++){
      const x=k*gap-shift;
      if(hashS(k)>0.30){
        const cnt=hashY(k*3)>0.6?2:1;
        for(let c=0;c<cnt;c++){
          const o=(c?(hashS(k*5)-0.5)*gap*0.5:0), px=x+o;
          const hh=horizon*(0.24+0.16*hashY(k*7+c)), bw=hh*0.92, top=horizon-hh;
          ctx.fillStyle=silhouetteColor(biome.sky[1], 0.42, 0.72);
          ctx.beginPath(); ctx.moveTo(px-bw, horizon); ctx.lineTo(px, top); ctx.lineTo(px+bw, horizon); ctx.closePath(); ctx.fill();
          ctx.fillStyle=silhouetteColor(biome.sky[2], 0.20, 1.35);
          ctx.beginPath(); ctx.moveTo(px-bw, horizon); ctx.lineTo(px, top); ctx.lineTo(px, horizon); ctx.closePath(); ctx.fill();
        }
      } else {
        // GREAT SPHINX silhouette: reclining body + outstretched front paws + nemes-headdress head (faces the direction of travel)
        const bh=horizon*0.14, bw=gap*0.32, baseY=horizon;
        ctx.fillStyle=silhouetteColor(biome.sky[1], 0.40, 0.70);
        rr(ctx, x-bw, baseY-bh*0.92, bw*1.85, bh*0.92, bh*0.16); ctx.fill();                 // haunches / body
        rr(ctx, x+bw*0.55, baseY-bh*0.36, bw*1.0, bh*0.36, bh*0.07); ctx.fill();             // outstretched front paws
        ctx.beginPath();                                                                      // chest rising to the head
        ctx.moveTo(x+bw*0.46, baseY-bh*0.36); ctx.lineTo(x+bw*0.58, baseY-bh*1.16);
        ctx.lineTo(x+bw*1.06, baseY-bh*1.16); ctx.lineTo(x+bw*1.22, baseY-bh*0.36); ctx.closePath(); ctx.fill();
        const hx=x+bw*0.84, hy=baseY-bh*1.16, hw=bw*0.52, hht=bh*0.62;                        // head + nemes headdress (trapezoid)
        ctx.beginPath();
        ctx.moveTo(hx-hw*0.55, hy); ctx.lineTo(hx-hw*0.34, hy-hht);
        ctx.lineTo(hx+hw*0.34, hy-hht); ctx.lineTo(hx+hw*0.64, hy); ctx.closePath(); ctx.fill();
        ctx.fillStyle=silhouetteColor(biome.sky[2], 0.30, 1.10);                              // face nub poking past the headdress
        rr(ctx, hx+hw*0.30, hy-hht*0.58, hw*0.36, hht*0.52, hht*0.10); ctx.fill();
      }
    }
  }
  {
    const gap=260*DPR, f=0.24, shift=cam*f;
    const i0=Math.floor((shift-gap)/gap), i1=Math.ceil((shift+W+gap)/gap);
    const baseH=0.12*horizon, varH=0.08*horizon;
    ctx.fillStyle=silhouetteColor(biome.sky[1], 0.38, 1.02);
    ctx.beginPath(); ctx.moveTo(i0*gap-shift, horizon);
    for(let k=i0;k<=i1;k++){ const x=k*gap-shift; const hh=baseH+varH*(0.4+0.6*hashY(k));
      ctx.lineTo(x-gap*0.5, horizon);
      ctx.quadraticCurveTo(x-gap*0.30, horizon-hh, x, horizon-hh*0.86);
      ctx.quadraticCurveTo(x+gap*0.30, horizon-hh*0.66, x+gap*0.5, horizon);
    }
    ctx.lineTo(i1*gap-shift, horizon); ctx.closePath(); ctx.fill();
  }
  {
    const gap=190*DPR, f=0.44, shift=cam*f;
    const i0=Math.floor((shift-gap)/gap), i1=Math.ceil((shift+W+gap)/gap);
    const baseH=0.13*horizon, varH=0.10*horizon;
    ctx.fillStyle=silhouetteColor(biome.sky[2], 0.40, 1.10);
    ctx.beginPath(); ctx.moveTo(i0*gap-shift, horizon);
    for(let k=i0;k<=i1;k++){ const x=k*gap-shift; const hh=baseH+varH*(0.4+0.6*hashS(k));
      ctx.lineTo(x-gap*0.5, horizon);
      ctx.quadraticCurveTo(x, horizon-hh, x+gap*0.5, horizon); }
    ctx.lineTo(i1*gap-shift, horizon); ctx.closePath(); ctx.fill();
    ctx.lineCap='round';
    for(let k=i0;k<=i1;k++){
      if(hashY(k*9)<0.68) continue;
      const x=k*gap-shift;
      const hh=horizon*(0.16+0.06*hashS(k*3));
      const lean=(hashS(k)-0.5)*0.3;
      const topX=x+lean*hh, topY=horizon-hh;
      ctx.strokeStyle=silhouetteColor(biome.sky[2], 0.5, 0.55); ctx.lineWidth=3.4*DPR;
      ctx.beginPath(); ctx.moveTo(x,horizon);
      ctx.quadraticCurveTo(x+lean*hh*0.6, horizon-hh*0.6, topX, topY); ctx.stroke();
      const fronds=6, sway=Math.sin(t*0.8+k)*0.06;
      ctx.lineWidth=2.2*DPR;
      for(let fi=0;fi<fronds;fi++){
        const a=(fi/(fronds-1)-0.5)*2.4 + sway, fl=hh*0.46;
        const fx=topX+Math.sin(a)*fl, fy=topY-Math.cos(a)*fl*0.5;
        ctx.beginPath(); ctx.moveTo(topX,topY);
        ctx.quadraticCurveTo(topX+Math.sin(a)*fl*0.5, topY-Math.cos(a)*fl*0.4-4*DPR, fx, fy+8*DPR); ctx.stroke();
      }
    }
  }
  {
    const bands=7;
    for(let b=0;b<bands;b++){
      const y=horizon - horizon*(0.04+0.20*hashY(b*3.1));
      const speed=(30+40*hashS(b))*DPR, sp=320*DPR, drift=(t*speed)%sp;
      const len=(120+80*hashY(b*5))*DPR;
      ctx.fillStyle=getAccent(0.05+0.04*hashS(b*7));
      for(let x=-sp+((sp-drift)%sp); x<W+sp; x+=sp){
        const yy=y+Math.sin(t*0.5+b+x*0.01)*4*DPR;
        ctx.beginPath(); ctx.ellipse(x, yy, len, 3.5*DPR, 0, 0, 6.283); ctx.fill();
      }
    }
  }
}
function scapeFrozen(biome,horizon,t){
  {
    const ribbons=3;
    for(let r=0;r<ribbons;r++){
      const baseY=horizon*(0.13+r*0.10), amp=horizon*(0.06+0.03*r);
      const a0=0.15+0.08*(0.5+0.5*Math.sin(t*0.5+r*1.3));
      ctx.lineCap='round';
      for(let pass=0;pass<2;pass++){
        ctx.strokeStyle=getAccent(a0*(pass?0.45:1));
        ctx.lineWidth=(14+10*r)*DPR*(pass?2.0:1);
        ctx.beginPath();
        for(let x=-40;x<=W+40;x+=24){
          const phase=t*0.4 + r*2.0;
          const y=baseY + Math.sin(x*0.004 + phase)*amp + Math.sin(x*0.011 - phase*1.6)*amp*0.4 - cam*0.02;
          x===-40?ctx.moveTo(x,y):ctx.lineTo(x,y);
        }
        ctx.stroke();
      }
      for(let sx=0; sx<W; sx+=70*DPR){
        const xx=sx + r*30*DPR;
        const yy=baseY + Math.sin(xx*0.004 + t*0.4 + r*2)*amp;
        const grd=ctx.createLinearGradient(0,yy,0,yy+amp*2.2);
        grd.addColorStop(0,getAccent(a0*0.8)); grd.addColorStop(1,getAccent(0));
        ctx.strokeStyle=grd; ctx.lineWidth=3*DPR;
        ctx.beginPath(); ctx.moveTo(xx,yy); ctx.lineTo(xx,yy+amp*2.2); ctx.stroke();
      }
    }
  }
  {
    const gap=240*DPR, f=0.10, shift=cam*f;
    const i0=Math.floor((shift-gap)/gap), i1=Math.ceil((shift+W+gap)/gap);
    for(let k=i0;k<=i1;k++){
      const x=k*gap-shift;
      const hh=horizon*(0.20+0.16*hashY(k)), w=gap*(0.40+0.16*hashS(k));
      const top=horizon-hh, tilt=(hashS(k)-0.5)*w*0.4;
      ctx.fillStyle=silhouetteColor(biome.sky[2], 0.34, 1.45);
      ctx.beginPath();
      ctx.moveTo(x-w, horizon); ctx.lineTo(x-w*0.5, top+hh*0.18); ctx.lineTo(x+tilt, top);
      ctx.lineTo(x+w*0.6, top+hh*0.30); ctx.lineTo(x+w, horizon); ctx.closePath(); ctx.fill();
      ctx.fillStyle=silhouetteColor(biome.sky[0], 0.16, 2.1);
      ctx.beginPath();
      ctx.moveTo(x+tilt, top); ctx.lineTo(x+w*0.6, top+hh*0.30); ctx.lineTo(x+w, horizon); ctx.lineTo(x+tilt, horizon);
      ctx.closePath(); ctx.fill();
    }
  }
  {
    const gap=130*DPR, f=0.32, shift=cam*f;
    const i0=Math.floor((shift-gap)/gap), i1=Math.ceil((shift+W+gap)/gap);
    for(let k=i0;k<=i1;k++){
      const x=k*gap-shift;
      const cnt=hashS(k)>0.6?2:1;
      for(let c=0;c<cnt;c++){
        const o=c?(hashY(k*3+c)-0.5)*gap*0.6:0, cx=x+o;
        const w=gap*(0.14+0.12*hashS(k*5+c)), hh=horizon*(0.16+0.30*hashY(k*7+c));
        const tip=horizon-hh, mid=horizon-hh*0.4;
        ctx.fillStyle=silhouetteColor(biome.sky[1], 0.42, 1.20);
        ctx.beginPath();
        ctx.moveTo(cx, horizon); ctx.lineTo(cx-w, mid); ctx.lineTo(cx, tip); ctx.lineTo(cx+w, mid); ctx.closePath(); ctx.fill();
        ctx.strokeStyle=getAccent(0.14); ctx.lineWidth=1.2*DPR;
        ctx.beginPath(); ctx.moveTo(cx,tip); ctx.lineTo(cx+w,mid); ctx.stroke();
      }
    }
  }
  {
    const gap=220*DPR, f=0.46, shift=cam*f;
    const i0=Math.floor((shift-gap)/gap), i1=Math.ceil((shift+W+gap)/gap);
    const baseH=0.07*horizon, varH=0.06*horizon;
    ctx.fillStyle=silhouetteColor(biome.sky[0], 0.30, 2.3);
    ctx.beginPath(); ctx.moveTo(i0*gap-shift, horizon);
    for(let k=i0;k<=i1;k++){ const x=k*gap-shift; const hh=baseH+varH*(0.4+0.6*hashY(k));
      ctx.lineTo(x-gap*0.5, horizon);
      ctx.quadraticCurveTo(x, horizon-hh, x+gap*0.5, horizon); }
    ctx.lineTo(i1*gap-shift, horizon); ctx.closePath(); ctx.fill();
  }
  {
    const cols=54, f=0.5, span=horizon*1.06;
    for(let k=0;k<cols;k++){
      const baseX=hashY(k*1.7)*W;
      const sway=Math.sin(t*0.5+k*2.1)*10*DPR;
      const x=(((baseX - cam*f + sway)%W)+W)%W;
      const speed=(14+18*hashS(k))*DPR;
      const y=((t*speed + hashY(k*4.3)*span)%span);
      const r=(1.2+1.6*hashS(k*3))*DPR;
      ctx.fillStyle=silhouetteColor(biome.sky[0], 0.30+0.25*hashS(k*5), 2.5);
      ctx.beginPath(); ctx.arc(x,y,r,0,6.283); ctx.fill();
    }
  }
}
function scapeSky(biome,horizon,t){
  const D=DPR;
  const SUNX=W*0.68 - cam*0.02, SUNY=horizon*0.36;
  const STONE=hslHex(214,0.44,0.33), STONED=hslHex(217,0.50,0.21);

  // (1) deepen the blue crown + lay a warm sun-haze -> destroys the "washed white"
  { ctx.save();
    const tg=ctx.createLinearGradient(0,0,0,horizon);
    tg.addColorStop(0.00, silhouetteColor(biome.sky[0],0.42,0.62));
    tg.addColorStop(0.55,'rgba(0,0,0,0)');
    ctx.fillStyle=tg; ctx.fillRect(0,0,W,horizon);
    ctx.globalCompositeOperation='lighter';
    const bg=ctx.createRadialGradient(SUNX,SUNY,0,SUNX,SUNY,horizon*1.25);
    bg.addColorStop(0,'rgba(255,240,196,0.40)');
    bg.addColorStop(0.4,'rgba(255,214,140,0.16)');
    bg.addColorStop(1,'rgba(255,200,120,0)');
    ctx.fillStyle=bg; ctx.fillRect(0,0,W,horizon);
    ctx.restore(); }

  // (2) high sun + slow god-ray beams (backlight for every silhouette)
  { ctx.save(); ctx.globalCompositeOperation='lighter';
    const rays=16, len=horizon*1.9;
    for(let i=0;i<rays;i++){
      const a=(i/rays)*6.283 + t*0.02 + Math.sin(t*0.2+i*1.3)*0.02;
      const half=0.008+0.010*hashY(i*3.7);
      const al=0.03+0.05*(0.5+0.5*Math.sin(t*0.5+i*1.7));
      ctx.fillStyle='rgba(255,232,170,'+al.toFixed(3)+')';
      ctx.beginPath(); ctx.moveTo(SUNX,SUNY);
      ctx.lineTo(SUNX+Math.cos(a-half)*len, SUNY+Math.sin(a-half)*len);
      ctx.lineTo(SUNX+Math.cos(a+half)*len, SUNY+Math.sin(a+half)*len);
      ctx.closePath(); ctx.fill(); }
    const cg=ctx.createRadialGradient(SUNX,SUNY,0,SUNX,SUNY,horizon*0.17);
    cg.addColorStop(0,'rgba(255,255,247,0.95)');
    cg.addColorStop(0.5,'rgba(255,240,186,0.50)');
    cg.addColorStop(1,'rgba(255,220,140,0)');
    ctx.fillStyle=cg; ctx.beginPath(); ctx.arc(SUNX,SUNY,horizon*0.17,0,6.283); ctx.fill();
    ctx.restore(); }

  // ---- shared helpers ----
  function haloBehind(x,y,r,a){ r=Math.max(1,r); ctx.save(); ctx.globalCompositeOperation='lighter';
    const hg=ctx.createRadialGradient(x,y,0,x,y,r);
    hg.addColorStop(0,'rgba(255,228,158,'+a+')'); hg.addColorStop(1,'rgba(255,228,158,0)');
    ctx.fillStyle=hg; ctx.beginPath(); ctx.arc(x,y,r,0,6.283); ctx.fill(); ctx.restore(); }

  function cloudPath(x,cy,cw,ch,lobes,seed){
    ctx.beginPath(); ctx.moveTo(x-cw,cy+ch*0.5);
    for(let l=0;l<lobes;l++){
      const lx=x-cw+(l/(lobes-1))*cw*2;
      const lr=ch*(0.6+0.5*hashS(seed+l))*Math.sin((l+0.5)/lobes*Math.PI)+ch*0.35;
      ctx.arc(lx,cy-lr*0.4,Math.max(1,lr),Math.PI,0,false); }
    ctx.lineTo(x+cw,cy+ch*0.5); ctx.closePath(); }

  function cloudBand(f,gap,yFrac,sizeFrac,drift,topA,botA,rimA){
    const g=gap*D, shift=cam*f+drift;
    const i0=Math.floor((shift-g)/g), i1=Math.ceil((shift+W+g)/g);
    for(let k=i0;k<=i1;k++){ if(hashY(k*1.3)<0.32) continue;
      const x=k*g-shift, cy=horizon*yFrac + Math.sin(t*0.2+k)*4*D;
      const cw=g*(sizeFrac+0.08*hashS(k)), ch=cw*(0.20+0.06*hashY(k));
      const lobes=4+Math.floor(hashY(k*3)*3);
      cloudPath(x,cy,cw,ch,lobes,k*3.1);
      const cgd=ctx.createLinearGradient(0,cy-ch,0,cy+ch*0.6);
      cgd.addColorStop(0,'rgba(255,248,226,'+topA+')');            // sunlit crown
      cgd.addColorStop(1, silhouetteColor(biome.sky[0],botA,0.95)); // cool blue shaded belly = depth, not flat white
      ctx.fillStyle=cgd; ctx.fill();
      ctx.save(); ctx.globalCompositeOperation='lighter';
      cloudPath(x,cy,cw,ch,lobes,k*3.1);
      ctx.strokeStyle='rgba(255,230,160,'+rimA+')'; ctx.lineWidth=2*D; ctx.lineJoin='round'; ctx.stroke();
      ctx.restore(); } }

  function pennant(px,py,ph,dir){ ph=Math.max(2,ph);
    const pw=ph*0.9*dir, wv=Math.sin(t*3+px*0.01)*ph*0.14;
    ctx.fillStyle=getAccent(0.92);
    ctx.beginPath(); ctx.moveTo(px,py); ctx.lineTo(px,py+ph);
    ctx.lineTo(px+pw, py+ph*0.5+wv); ctx.closePath(); ctx.fill(); }

  function castle(cx, base, w, h){
    const kw=Math.max(2,w*0.46), kh=Math.max(2,h), tw=Math.max(2,w*0.22), th=Math.max(2,h*1.28);
    ctx.fillStyle=STONE;
    ctx.fillRect(cx-w*0.5, base-th, tw, th);                 // left tower
    ctx.fillRect(cx+w*0.5-tw, base-th, tw, th);              // right tower
    ctx.fillRect(cx-kw/2, base-kh, kw, kh);                  // central keep
    const nb=4, nw=kw/(nb*2);                                // keep battlements
    for(let n=0;n<nb;n++) ctx.fillRect(cx-kw/2+(n*2)*nw, base-kh-nw*1.2, nw, nw*1.2);
    function roof(rx,ry,rw){ rw=Math.max(2,rw); ctx.beginPath(); ctx.moveTo(rx-rw/2,ry); ctx.lineTo(rx,ry-rw*1.1); ctx.lineTo(rx+rw/2,ry); ctx.closePath(); ctx.fill(); }
    roof(cx-w*0.5+tw/2, base-th, tw*1.35);
    roof(cx+w*0.5-tw/2, base-th, tw*1.35);
    const sw=Math.max(2,w*0.12), sh=Math.max(3,h*0.7);       // tall central spire
    ctx.fillRect(cx-sw/2, base-kh-sh, sw, sh);
    roof(cx, base-kh-sh, sw*1.5);
    const spTip=base-kh-sh-sw*1.5*1.1, twTipY=base-th-tw*1.35*1.1;
    // sun-facing (right) backlit rim
    ctx.save(); ctx.globalCompositeOperation='lighter';
    ctx.strokeStyle='rgba(255,234,168,0.85)'; ctx.lineWidth=2*D; ctx.lineJoin='round'; ctx.lineCap='round';
    ctx.beginPath(); ctx.moveTo(cx+w*0.5-tw, base-th*0.1); ctx.lineTo(cx+w*0.5-tw, base-th); ctx.lineTo(cx+w*0.5-tw/2, twTipY); ctx.stroke();
    ctx.beginPath(); ctx.moveTo(cx, spTip); ctx.lineTo(cx+sw/2, base-kh-sh); ctx.stroke();
    ctx.restore();
    // gold banners
    pennant(cx+sw/2, spTip, sh*0.16, 1);
    pennant(cx+w*0.5-tw/2+tw*0.6, twTipY, th*0.14, 1);
    pennant(cx-w*0.5+tw/2-tw*0.6, twTipY, th*0.14, -1);
    // twinkling spire glint
    ctx.save(); ctx.globalCompositeOperation='lighter';
    ctx.fillStyle=getAccent(0.5+0.4*(0.5+0.5*Math.sin(t*2+cx)));
    ctx.beginPath(); ctx.arc(cx, spTip, 2.2*D,0,6.283); ctx.fill(); ctx.restore(); }

  function island(x,y,s,mode){ s=Math.max(6,s);
    haloBehind(x, y-s*0.3, s*1.9, mode===2?0.26:0.16);
    ctx.fillStyle=STONED;                                    // rocky underside
    ctx.beginPath(); ctx.moveTo(x-s,y);
    ctx.quadraticCurveTo(x-s*0.35,y+s*1.5, x,y+s*2.1);
    ctx.quadraticCurveTo(x+s*0.35,y+s*1.5, x+s,y); ctx.closePath(); ctx.fill();
    ctx.beginPath(); ctx.moveTo(x+s*0.2,y+s*1.4);            // dangling shard
    ctx.quadraticCurveTo(x+s*0.4,y+s*2.3, x+s*0.5,y+s*2.7);
    ctx.quadraticCurveTo(x+s*0.62,y+s*2.1, x+s*0.55,y+s*1.5); ctx.closePath(); ctx.fill();
    ctx.fillStyle=STONE;                                     // plateau
    ctx.beginPath(); ctx.ellipse(x,y,s,Math.max(1,s*0.36),0,0,6.283); ctx.fill();
    if(mode>0) castle(x, y-s*0.10, s*(mode===2?1.55:0.95), s*(mode===2?1.55:0.85));
    ctx.save(); ctx.globalCompositeOperation='lighter';     // backlit top rim
    ctx.strokeStyle='rgba(255,232,165,'+(mode===2?0.85:0.6)+')'; ctx.lineWidth=2*D;
    ctx.beginPath(); ctx.ellipse(x,y,s*0.96,Math.max(1,s*0.34),0,Math.PI,2*Math.PI); ctx.stroke();
    ctx.restore(); }

  // (3) far wispy cloud shelf (high, faint)
  cloudBand(0.06, 440, 0.24, 0.40, t*4*D, 0.16, 0.09, 0.18);

  // (4) distant floating rocks (some crowned)
  _scapeCols(0.05, 640*D, (k,x)=>{ if(hashY(k*1.7)<0.5) return;
    const y=horizon*(0.30+0.15*hashS(k)) + Math.sin(t*0.15+k)*3*D;
    island(x, y, (24+18*hashY(k*3))*D, hashS(k*5)>0.62?1:0); });

  // (5) mid cloud platforms
  cloudBand(0.18, 380, 0.50, 0.44, t*8*D, 0.24, 0.15, 0.32);

  // (6) mid citadels on cloud-islands
  _scapeCols(0.12, 700*D, (k,x)=>{ if(hashS(k*2.1+3)<0.5) return;
    const y=horizon*(0.48+0.13*hashY(k*3)) + Math.sin(t*0.2+k)*4*D;
    island(x, y, (38+24*hashS(k*5))*D, 1); });

  // (7) HERO grand citadel (near, slow parallax; always one on screen)
  _scapeCols(0.09, 1500*D, (k,x)=>{
    const y=horizon*0.62 + Math.sin(t*0.16+k)*4*D;
    island(x, y, horizon*(0.15+0.03*hashY(k*3)), 2); });

  // (8) foreground sunlit clouds drifting in front
  cloudBand(0.34, 320, 0.84, 0.48, t*14*D, 0.34, 0.22, 0.50);

  // (9) distant birds
  { ctx.save(); ctx.strokeStyle='rgba(40,55,80,0.5)'; ctx.lineWidth=2*D; ctx.lineCap='round';
    for(let i=0;i<5;i++){ const span=W+80*D, spd=7+4*hashY(i);
      const bx=(((hashS(i*5)*W*1.3 - cam*0.20 - t*spd*D)%span)+span)%span-40*D;
      const by=horizon*(0.20+0.28*hashY(i*9)); const s=(4+2*hashS(i))*D, flap=Math.sin(t*5+i)*s*0.5;
      ctx.beginPath(); ctx.moveTo(bx-s,by); ctx.quadraticCurveTo(bx-s*0.4,by-flap-s*0.4,bx,by);
      ctx.quadraticCurveTo(bx+s*0.4,by-flap-s*0.4,bx+s,by); ctx.stroke(); }
    ctx.restore(); }
}
function scapeMeadow(biome,horizon,t){
  // CALM Minecraft-style starting world: drifting blocky clouds, soft far hills, blocky terraced grass + simple trees
  for(let layer=0; layer<2; layer++){ // drifting square clouds (two slow parallax layers)
    const f=0.05+layer*0.05, cy=horizon*0.22+layer*horizon*0.15, gap=(240+layer*90)*DPR, drift=(t*(7+layer*5))*DPR;
    _scapeCols(f, gap, (k,x)=>{ if(((k*131)>>>0)%5>=2) return; const bx=x-(drift%gap), w=(50+20*hashS(k))*DPR, h=(15+7*hashY(k))*DPR, by=cy+hashY(k*3)*26*DPR;
      ctx.fillStyle="rgba(255,255,255,"+(0.55-layer*0.18)+")";
      rr(ctx,bx,by,w,h,6*DPR); ctx.fill(); rr(ctx,bx+w*0.22,by-h*0.55,w*0.66,h*0.95,6*DPR); ctx.fill(); }); }
  const farTop=horizon-86*DPR; // soft far hills
  ctx.fillStyle=silhouetteColor(biome.ac,0.20,0.85); ctx.beginPath(); ctx.moveTo(0,horizon);
  for(let x=0;x<=W;x+=10*DPR){ const wx=x+cam*0.10, y=farTop+Math.sin(wx*0.0038)*20*DPR+Math.sin(wx*0.012)*7*DPR; ctx.lineTo(x,y); }
  ctx.lineTo(W,horizon); ctx.closePath(); ctx.fill();
  const gap=66*DPR, baseY=horizon-44*DPR; // mid blocky grass terraces + simple trees
  _scapeCols(0.34, gap, (k,x)=>{
    const step=Math.round(Math.sin(k*0.6)*1.3+Math.sin(k*0.27)*1.1)*14*DPR, top=baseY-step;
    ctx.fillStyle=(k&1)?"#5db84a":"#4ca23e"; ctx.fillRect(x,top,gap+1*DPR,horizon-top);
    ctx.fillStyle="rgba(190,255,175,.45)"; ctx.fillRect(x,top,gap+1*DPR,4*DPR);
    if(((k*73)>>>0)%4===0){ const tx=x+gap*0.5, ty=top;
      ctx.fillStyle="#6e4a2b"; ctx.fillRect(tx-4*DPR,ty-24*DPR,8*DPR,24*DPR);
      ctx.fillStyle="#3f8f37"; rr(ctx,tx-18*DPR,ty-50*DPR,36*DPR,32*DPR,7*DPR); ctx.fill();
      ctx.fillStyle="rgba(120,220,120,.5)"; rr(ctx,tx-14*DPR,ty-47*DPR,15*DPR,11*DPR,4*DPR); ctx.fill(); } }); }
function scapeMushroom(biome,horizon,t){
  { const gap=220*DPR, f=0.10, shift=cam*f;
    const i0=Math.floor((shift-gap)/gap), i1=Math.ceil((shift+W+gap)/gap);
    const baseH=0.08*horizon, varH=0.07*horizon;
    ctx.fillStyle=silhouetteColor(biome.sky[2],0.46,1.5);
    ctx.beginPath(); ctx.moveTo(i0*gap-shift, horizon);
    for(let k=i0;k<=i1;k++){ const x=k*gap-shift; const hh=baseH+varH*(0.4+0.6*hashY(k));
      ctx.lineTo(x-gap*0.5,horizon); ctx.quadraticCurveTo(x, horizon-hh, x+gap*0.5, horizon); }
    ctx.lineTo(i1*gap-shift,horizon); ctx.closePath(); ctx.fill(); }
  const layers=[
    {f:0.20, gap:300, base:0.18, vary:0.10, sky:1, mul:0.78, a:0.34, glow:0.10, bob:5},
    {f:0.42, gap:170, base:0.24, vary:0.16, sky:1, mul:0.52, a:0.55, glow:0.20, bob:8},
  ];
  for(const L of layers){
    const gap=L.gap*DPR; const baseH=L.base*horizon, varH=L.vary*horizon;
    _scapeCols(L.f, gap, (k,x)=>{
      const hh=baseH+varH*(0.2+0.8*hashY(k*3));
      const bob=Math.sin(t*0.6+k*1.7)*L.bob*DPR;
      const cx=x, top=horizon-hh+bob;
      const stalkW=gap*(0.07+0.03*hashS(k));
      const capW=gap*(0.22+0.10*hashS(k*5)), capH=capW*0.66;
      ctx.fillStyle=silhouetteColor(biome.sky[L.sky], L.a, L.mul);
      ctx.beginPath();
      ctx.moveTo(cx-stalkW, horizon);
      ctx.quadraticCurveTo(cx-stalkW*0.7, top+capH*0.6, cx-stalkW*0.5, top+capH*0.4);
      ctx.lineTo(cx+stalkW*0.5, top+capH*0.4);
      ctx.quadraticCurveTo(cx+stalkW*0.7, top+capH*0.6, cx+stalkW, horizon);
      ctx.closePath(); ctx.fill();
      const gl=ctx.createRadialGradient(cx, top+capH*0.4, capW*0.1, cx, top+capH*0.4, capW*1.2);
      gl.addColorStop(0, getAccent(L.glow)); gl.addColorStop(1, getAccent(0));
      ctx.fillStyle=gl; ctx.beginPath(); ctx.arc(cx, top+capH*0.4, capW*1.2, 0, 6.283); ctx.fill();
      ctx.fillStyle=silhouetteColor(biome.sky[L.sky], L.a+0.06, L.mul*0.9);
      ctx.beginPath(); ctx.ellipse(cx, top+capH, capW, capH, 0, Math.PI, 2*Math.PI, false); ctx.closePath(); ctx.fill();
      for(let s=0;s<3;s++){
        const sx=cx + (hashS(k*7+s)-0.5)*capW*1.2;
        const sy=top+capH - hashY(k*9+s)*capH*0.7;
        ctx.fillStyle=getAccent(0.18+0.12*(0.5+0.5*Math.sin(t*1.3+k+s)));
        ctx.beginPath(); ctx.arc(sx, sy, (1.5+1.5*hashS(k*11+s))*DPR, 0, 6.283); ctx.fill();
      }
    });
  }
  { const cols=44, f=0.5, span=horizon*1.05;
    for(let k=0;k<cols;k++){
      const baseX=hashY(k*1.9)*W;
      const sway=Math.sin(t*0.5+k*2.0)*14*DPR;
      const x=(((baseX - cam*f + sway)%W)+W)%W;
      const speed=(10+16*hashS(k))*DPR;
      const y=horizon-((t*speed + hashY(k*4.7)*span)%span);
      const r=(1.2+1.8*hashS(k*3))*DPR;
      const tw=0.4+0.6*(0.5+0.5*Math.sin(t*2+k));
      ctx.fillStyle=getAccent(0.16*tw+0.05);
      ctx.beginPath(); ctx.arc(x,y,r,0,6.283); ctx.fill();
    } }
}
function scapeJungle(biome,horizon,t){
  { const gap=240*DPR, f=0.09, shift=cam*f;
    const i0=Math.floor((shift-gap)/gap), i1=Math.ceil((shift+W+gap)/gap);
    const baseH=0.12*horizon, varH=0.07*horizon;
    ctx.fillStyle=silhouetteColor(biome.sky[2],0.40,1.4);
    ctx.beginPath(); ctx.moveTo(i0*gap-shift, horizon);
    for(let k=i0;k<=i1;k++){ const x=k*gap-shift; const hh=baseH+varH*(0.4+0.6*hashY(k));
      ctx.lineTo(x-gap*0.5,horizon);
      ctx.quadraticCurveTo(x-gap*0.28, horizon-hh, x, horizon-hh);
      ctx.quadraticCurveTo(x+gap*0.28, horizon-hh, x+gap*0.5, horizon); }
    ctx.lineTo(i1*gap-shift,horizon); ctx.closePath(); ctx.fill(); }
  { const gap=150*DPR, f=0.26, shift=cam*f;
    const i0=Math.floor((shift-gap)/gap), i1=Math.ceil((shift+W+gap)/gap);
    const baseH=0.18*horizon, varH=0.22*horizon;
    ctx.lineCap='round';
    for(let k=i0;k<=i1;k++){
      if(hashS(k*3)<0.4) continue;
      const x=k*gap-shift, w=gap*(0.46+0.22*hashS(k));
      const hh=baseH+varH*(0.1+0.9*hashY(k));
      const bx=x-w/2, top=horizon-hh;
      ctx.fillStyle=silhouetteColor(biome.sky[1],0.48,0.6);
      ctx.fillRect(bx, top, w, hh);
      const sw=w*(0.4+0.3*hashY(k*5));
      ctx.fillRect(bx + (hashS(k*7)>0.5?0:w-sw), top-hh*0.12, sw, hh*0.12);
      ctx.fillStyle=getAccent(0.10); ctx.fillRect(bx, top, w, 2*DPR);
      ctx.strokeStyle=silhouetteColor(biome.sky[1],0.42,0.85); ctx.lineWidth=2*DPR;
      const vines=2+Math.floor(hashS(k*9)*2);
      for(let v=0;v<vines;v++){
        const vx=bx + w*(0.2+0.6*hashY(k*11+v));
        const vl=hh*(0.3+0.5*hashS(k*13+v));
        const sway=Math.sin(t*0.8+k+v)*0.12;
        ctx.beginPath(); ctx.moveTo(vx, top);
        ctx.quadraticCurveTo(vx+sway*vl, top+vl*0.6, vx+sway*vl*1.6, top+vl); ctx.stroke();
      }
    } }
  { const gap=120*DPR, f=0.46, shift=cam*f;
    const i0=Math.floor((shift-gap)/gap), i1=Math.ceil((shift+W+gap)/gap);
    ctx.lineCap='round';
    for(let k=i0;k<=i1;k++){
      const x=k*gap-shift;
      const vl=horizon*(0.3+0.4*hashY(k*3)), amp=0.16;
      ctx.strokeStyle=silhouetteColor(biome.sky[1],0.5,0.5); ctx.lineWidth=3*DPR;
      ctx.beginPath(); ctx.moveTo(x,0);
      let px=x, py=0; const segs=4;
      for(let s=1;s<=segs;s++){ const fr=s/segs;
        const sx=x+Math.sin(t*0.7+k+fr*2)*amp*vl*fr, sy=vl*fr;
        ctx.quadraticCurveTo(px+(sx-px)*0.5, (py+sy)/2, sx, sy); px=sx; py=sy; }
      ctx.stroke();
      ctx.fillStyle=getAccent(0.10);
      ctx.beginPath(); ctx.ellipse(px,py, 6*DPR, 3*DPR, 0.6, 0, 6.283); ctx.fill();
    } }
  { const cols=40, f=0.5;
    for(let k=0;k<cols;k++){
      const x=(((hashY(k*2.3)*W - cam*f + Math.sin(t*0.6+k*1.5)*20*DPR)%W)+W)%W;
      const y=horizon*0.2 + hashS(k*3.1)*0.7*horizon + Math.sin(t*0.9+k*2.2)*12*DPR;
      const blink=0.5+0.5*Math.sin(t*3+k*2.5);
      const r=(1.3+1.0*hashS(k*5))*DPR;
      const gl=ctx.createRadialGradient(x,y,0,x,y,r*4);
      gl.addColorStop(0, getAccent(0.5*blink)); gl.addColorStop(1, getAccent(0));
      ctx.fillStyle=gl; ctx.beginPath(); ctx.arc(x,y,r*4,0,6.283); ctx.fill();
      ctx.fillStyle=getAccent(0.7*blink); ctx.beginPath(); ctx.arc(x,y,r,0,6.283); ctx.fill();
    } }
}
function _scapeGear(cx,cy,R,teeth,rot,fill,rim){
  ctx.save(); ctx.translate(cx,cy); ctx.rotate(rot);
  ctx.fillStyle=fill;
  const tw=R*0.30, th=R*0.28;
  for(let i=0;i<teeth;i++){ ctx.save(); ctx.rotate(i/teeth*6.283); ctx.fillRect(-tw/2,-R-th*0.45,tw,th); ctx.restore(); }
  ctx.beginPath(); ctx.arc(0,0,R,0,6.283); ctx.fill();
  ctx.strokeStyle=rim; ctx.lineWidth=2*DPR;
  ctx.beginPath(); ctx.arc(0,0,R*0.52,0,6.283); ctx.stroke();
  ctx.beginPath(); ctx.arc(0,0,R*0.16,0,6.283); ctx.stroke();
  for(let s=0;s<6;s++){ const a=s/6*6.283;
    ctx.beginPath(); ctx.moveTo(Math.cos(a)*R*0.18, Math.sin(a)*R*0.18); ctx.lineTo(Math.cos(a)*R*0.5, Math.sin(a)*R*0.5); ctx.stroke(); }
  ctx.restore();
}
function scapeClockwork(biome,horizon,t){
  { const gap=200*DPR, f=0.10, shift=cam*f;
    const i0=Math.floor((shift-gap)/gap), i1=Math.ceil((shift+W+gap)/gap);
    const baseH=0.12*horizon, varH=0.10*horizon;
    ctx.fillStyle=silhouetteColor(biome.sky[2],0.45,1.2);
    ctx.beginPath(); ctx.moveTo(i0*gap-shift, horizon);
    for(let k=i0;k<=i1;k++){ const x=k*gap-shift; const hh=baseH+varH*(0.4+0.6*hashY(k));
      ctx.lineTo(x-gap*0.5, horizon-hh*0.7); ctx.lineTo(x, horizon-hh); ctx.lineTo(x+gap*0.5, horizon-hh*0.6); }
    ctx.lineTo(i1*gap-shift,horizon); ctx.closePath(); ctx.fill(); }
  { const gap=140*DPR, f=0.26, shift=cam*f;
    const i0=Math.floor((shift-gap)/gap), i1=Math.ceil((shift+W+gap)/gap);
    const baseH=0.16*horizon, varH=0.22*horizon;
    for(let k=i0;k<=i1;k++){
      const x=k*gap-shift, w=gap*(0.55+0.28*hashS(k));
      const hh=baseH+varH*(0.12+0.88*hashY(k));
      const bx=x-w/2, top=horizon-hh;
      ctx.fillStyle=silhouetteColor(biome.sky[1],0.55,0.5);
      ctx.fillRect(bx, top, w, hh);
      const cols=2+Math.floor(hashS(k*5)*2), rows=Math.max(2,Math.floor(hh/(18*DPR)));
      for(let c=0;c<cols;c++) for(let r=0;r<rows;r++){
        const lit=hashS(k*31+r*7+c*3)>0.5;
        const al=lit?(0.14+0.12*(0.5+0.5*Math.sin(t*1.4+k+r+c))):0.04;
        ctx.fillStyle=getAccent(al);
        ctx.fillRect(bx+w*0.16+c*(w*0.68/cols), top+8*DPR+r*18*DPR, w*0.4/cols, 7*DPR);
      }
      const stx=bx + w*(hashY(k*7)>0.5?0.78:0.22), stW=w*0.14, stH=hh*0.5;
      ctx.fillStyle=silhouetteColor(biome.sky[1],0.6,0.42);
      ctx.fillRect(stx-stW/2, top-stH, stW, stH);
      for(let p=0;p<4;p++){
        const ph=(t*0.3 + p/4 + hashY(k*9)) % 1;
        const py=top-stH - ph*hh*0.8, pr=stW*(0.6+ph*1.8);
        ctx.fillStyle=silhouetteColor(biome.sky[2], 0.18*(1-ph), 1.8);
        ctx.beginPath(); ctx.arc(stx + Math.sin(ph*4+k)*stW, py, pr, 0, 6.283); ctx.fill();
      }
    } }
  { const gap=200*DPR, f=0.44, shift=cam*f;
    const i0=Math.floor((shift-gap)/gap), i1=Math.ceil((shift+W+gap)/gap);
    for(let k=i0;k<=i1;k++){
      if(hashS(k*3)<0.45) continue;
      const x=k*gap-shift, R=gap*(0.16+0.10*hashY(k)), cy=horizon - R*0.3;
      const teeth=8+Math.floor(hashS(k*5)*6), dir=hashY(k*7)>0.5?1:-1;
      const rot=t*0.5*dir*(0.6+hashS(k*9));
      _scapeGear(x, cy, R, teeth, rot, silhouetteColor(biome.sky[1],0.5,0.46), getAccent(0.16));
    } }
}
function scapeStormSpire(biome,horizon,t){
  { const gap=150*DPR, f=0.10, shift=cam*f;
    const i0=Math.floor((shift-gap)/gap), i1=Math.ceil((shift+W+gap)/gap);
    const baseH=0.16*horizon, varH=0.20*horizon;
    ctx.fillStyle=silhouetteColor(biome.sky[2],0.5,1.1);
    ctx.beginPath(); ctx.moveTo(i0*gap-shift, horizon);
    for(let k=i0;k<=i1;k++){ const x=k*gap-shift; const hh=baseH+varH*(0.3+0.7*hashY(k));
      ctx.lineTo(x-gap*0.5, horizon-baseH*0.3); ctx.lineTo(x, horizon-hh); }
    ctx.lineTo(i1*gap-shift, horizon); ctx.closePath(); ctx.fill(); }
  for(let b=0;b<3;b++){
    const fy=horizon - horizon*(0.30+b*0.16), drift=t*(10+b*7)*DPR, sp=220*DPR;
    ctx.fillStyle=silhouetteColor(biome.sky[2], 0.10, 1.5);
    for(let x=-sp+(sp-drift%sp); x<W+sp; x+=sp){
      ctx.beginPath(); ctx.ellipse(x, fy, 150*DPR, 18*DPR, 0, 0, 6.283); ctx.fill();
    }
  }
  const layers=[
    {f:0.24, gap:120, base:0.30, vary:0.34, sky:1, mul:0.50, a:0.62, beacon:6},
    {f:0.46, gap:160, base:0.40, vary:0.42, sky:1, mul:0.34, a:0.80, beacon:9},
  ];
  for(const L of layers){
    const gap=L.gap*DPR; const baseH=L.base*horizon, varH=L.vary*horizon;
    _scapeCols(L.f, gap, (k,x)=>{
      if(hashS(k*3)<0.35) return;
      const w=gap*(0.16+0.12*hashS(k));
      const hh=baseH+varH*(0.1+0.9*hashY(k*5));
      const top=horizon-hh, tipX=x+(hashS(k*7)-0.5)*w*0.6;
      ctx.fillStyle=silhouetteColor(biome.sky[L.sky], L.a, L.mul);
      ctx.beginPath();
      ctx.moveTo(x-w, horizon); ctx.lineTo(x-w*0.5, top+hh*0.18); ctx.lineTo(tipX, top);
      ctx.lineTo(x+w*0.5, top+hh*0.18); ctx.lineTo(x+w, horizon); ctx.closePath(); ctx.fill();
      const blink=0.4+0.6*(0.5+0.5*Math.sin(t*2.4+k*1.7));
      const ang=-Math.PI/2 + Math.sin(t*0.5 + k*1.3)*0.7, len=hh*1.4, spread=0.06;
      const bg=ctx.createLinearGradient(tipX, top, tipX+Math.cos(ang)*len, top+Math.sin(ang)*len);
      bg.addColorStop(0, getAccent(0.16*blink)); bg.addColorStop(1, getAccent(0));
      ctx.fillStyle=bg;
      ctx.beginPath(); ctx.moveTo(tipX, top);
      ctx.lineTo(tipX+Math.cos(ang-spread)*len, top+Math.sin(ang-spread)*len);
      ctx.lineTo(tipX+Math.cos(ang+spread)*len, top+Math.sin(ang+spread)*len);
      ctx.closePath(); ctx.fill();
      const gl=ctx.createRadialGradient(tipX, top, 0, tipX, top, L.beacon*DPR*1.6);
      gl.addColorStop(0, getAccent(0.6*blink)); gl.addColorStop(1, getAccent(0));
      ctx.fillStyle=gl; ctx.beginPath(); ctx.arc(tipX, top, L.beacon*DPR*1.6, 0, 6.283); ctx.fill();
      ctx.fillStyle=getAccent(0.8*blink); ctx.beginPath(); ctx.arc(tipX, top, 1.6*DPR, 0, 6.283); ctx.fill();
    });
  }
}
function scapeFunland(biome,horizon,t){
  // ---- NIGHT SKY: twinkling stars + glowing crescent moon ----
  { ctx.save(); ctx.globalCompositeOperation="lighter";
    for(let i=0;i<72;i++){ const px=(((hashS(i*1.7)*W*1.6 - cam*0.02)%W)+W)%W, py=hashY(i*2.3)*horizon*0.84, tw=0.5+0.5*Math.sin(t*2+i*1.3), a=0.22+0.6*tw*tw, r=(hashY(i*5)<0.12?1.9:1.0)*DPR;
      ctx.fillStyle="rgba(222,236,255,"+a+")"; ctx.beginPath(); ctx.arc(px,py,r,0,6.283); ctx.fill(); }
    ctx.restore(); }
  { const mx=W*0.80 - cam*0.03, my=horizon*0.24, mr=32*DPR;
    let mg=ctx.createRadialGradient(mx,my,mr*0.4,mx,my,mr*2.7); mg.addColorStop(0,"rgba(255,247,210,.45)"); mg.addColorStop(0.5,"rgba(255,230,170,.10)"); mg.addColorStop(1,"rgba(255,230,170,0)");
    ctx.fillStyle=mg; ctx.beginPath(); ctx.arc(mx,my,mr*2.7,0,6.283); ctx.fill();
    ctx.fillStyle="#fdf3cf"; ctx.beginPath(); ctx.arc(mx,my,mr,0,6.283); ctx.fill();
    ctx.fillStyle=silhouetteColor(biome.sky[0],1,0.7); ctx.beginPath(); ctx.arc(mx+mr*0.55,my-mr*0.32,mr*0.86,0,6.283); ctx.fill(); }
  // neon ride-light palette (cycles per bulb)
  const _NEON=["#ff3df0","#27e6ff","#ffe14d","#5cff9e","#ff5a3c","#b06bff"];
  const _glow=(gx,gy,gr,col,a)=>{ ctx.save(); ctx.globalCompositeOperation="lighter"; let g=ctx.createRadialGradient(gx,gy,0,gx,gy,gr); g.addColorStop(0,silhouetteColor(col,a,1.4)); g.addColorStop(1,silhouetteColor(col,0,1)); ctx.fillStyle=g; ctx.beginPath(); ctx.arc(gx,gy,gr,0,6.283); ctx.fill(); ctx.restore(); };
  // far rolling park grounds (distant haze layer)
  {
    const gap=240*DPR, f=0.10, shift=cam*f;
    const i0=Math.floor((shift-gap)/gap), i1=Math.ceil((shift+W+gap)/gap);
    const baseH=0.08*horizon, varH=0.06*horizon;
    ctx.fillStyle=silhouetteColor(biome.sky[2],0.40,1.30);
    ctx.beginPath(); ctx.moveTo(i0*gap-shift, horizon);
    for(let k=i0;k<=i1;k++){ const x=k*gap-shift; const hh=baseH+varH*(0.4+0.6*hashY(k));
      ctx.lineTo(x-gap*0.5, horizon);
      ctx.quadraticCurveTo(x-gap*0.30, horizon-hh, x, horizon-hh);
      ctx.quadraticCurveTo(x+gap*0.30, horizon-hh, x+gap*0.5, horizon);
    }
    ctx.lineTo(i1*gap-shift, horizon); ctx.closePath(); ctx.fill();
  }
  // striped big-top circus tents with pennant flags (mid-far)
  {
    const gap=300*DPR, f=0.20, shift=cam*f;
    const i0=Math.floor((shift-gap)/gap), i1=Math.ceil((shift+W+gap)/gap);
    for(let k=i0;k<=i1;k++){
      if(hashS(k*2+5)<0.45) continue;
      const x=k*gap-shift;
      const tw=gap*(0.30+0.12*hashY(k*3)), th=horizon*(0.20+0.12*hashS(k*7));
      const top=horizon-th, peak=top-th*0.45;
      ctx.fillStyle=silhouetteColor(biome.sky[1],0.40,1.05);
      ctx.beginPath(); ctx.moveTo(x-tw,horizon);
      ctx.quadraticCurveTo(x-tw*0.9, top, x, peak);
      ctx.quadraticCurveTo(x+tw*0.9, top, x+tw, horizon);
      ctx.closePath(); ctx.fill();
      ctx.strokeStyle=silhouetteColor(biome.sky[2],0.30,0.7); ctx.lineWidth=Math.max(3*DPR,tw*0.10); ctx.lineCap='round';
      for(let s=-2;s<=2;s++){ const sx=x+s*tw*0.36;
        ctx.beginPath(); ctx.moveTo(sx,horizon); ctx.quadraticCurveTo(x+s*tw*0.16, top+th*0.2, x, peak); ctx.stroke(); }
      ctx.strokeStyle=silhouetteColor(biome.sky[1],0.50,0.6); ctx.lineWidth=2*DPR;
      ctx.beginPath(); ctx.moveTo(x,peak); ctx.lineTo(x,peak-th*0.28); ctx.stroke();
      ctx.fillStyle=getAccent(0.26+0.20*(0.5+0.5*Math.sin(t*2.2+k)));
      ctx.beginPath(); ctx.moveTo(x,peak-th*0.28); ctx.lineTo(x+tw*0.22,peak-th*0.225); ctx.lineTo(x,peak-th*0.17); ctx.closePath(); ctx.fill();
    }
  }
  // ferris wheels + drop/swing towers (near-mid, animated)
  {
    const gap=520*DPR, f=0.30, shift=cam*f;
    const i0=Math.floor((shift-gap)/gap), i1=Math.ceil((shift+W+gap)/gap);
    for(let k=i0;k<=i1;k++){
      const x=k*gap-shift, pick=hashS(k*5+1);
      if(pick<0.42){
        const R=horizon*(0.26+0.10*hashY(k*3)), cyc=horizon-R-horizon*0.05;
        const rot=t*0.25*(hashS(k)>0.5?1:-1) + k;
        ctx.strokeStyle=silhouetteColor(biome.sky[1],0.42,0.85); ctx.lineWidth=4*DPR; ctx.lineCap='round';
        ctx.beginPath(); ctx.moveTo(x-R*0.5,horizon); ctx.lineTo(x,cyc); ctx.lineTo(x+R*0.5,horizon); ctx.stroke();
        ctx.lineWidth=3*DPR; ctx.strokeStyle=getAccent(0.55); // neon-glowing wheel rim at night
        ctx.beginPath(); ctx.arc(x,cyc,R,0,6.283); ctx.stroke();
        const N=10;
        for(let s=0;s<N;s++){
          const a=rot + s/N*6.283, ex=x+Math.cos(a)*R, ey=cyc+Math.sin(a)*R;
          ctx.strokeStyle=silhouetteColor(biome.sky[1],0.30,1.0); ctx.lineWidth=2*DPR;
          ctx.beginPath(); ctx.moveTo(x,cyc); ctx.lineTo(ex,ey); ctx.stroke();
          ctx.fillStyle=silhouetteColor(biome.sky[2],0.40,1.20);
          ctx.beginPath(); ctx.arc(ex,ey,R*0.10,0,6.283); ctx.fill();
          { const _c=_NEON[((s+k)%_NEON.length+_NEON.length)%_NEON.length], _b=0.5+0.5*Math.sin(t*4+s*1.3+k); _glow(ex,ey,9*DPR*(0.6+0.5*_b),_c,0.45+0.4*_b); ctx.fillStyle=silhouetteColor(_c,1,1.7); ctx.beginPath(); ctx.arc(ex,ey,2.6*DPR,0,6.283); ctx.fill(); }
        }
        ctx.fillStyle=silhouetteColor(biome.sky[1],0.50,0.9); ctx.beginPath(); ctx.arc(x,cyc,R*0.08,0,6.283); ctx.fill();
      }else if(pick<0.66){
        const th=horizon*(0.50+0.16*hashY(k*7)), tw=Math.max(8*DPR, horizon*0.03), top=horizon-th;
        ctx.fillStyle=silhouetteColor(biome.sky[1],0.40,0.95); ctx.fillRect(x-tw*0.5, top, tw, th);
        ctx.strokeStyle=silhouetteColor(biome.sky[1],0.26,1.2); ctx.lineWidth=2*DPR;
        for(let s=top; s<horizon; s+=th*0.14){ ctx.beginPath(); ctx.moveTo(x-tw*0.5,s); ctx.lineTo(x+tw*0.5,s+th*0.07); ctx.stroke(); }
        ctx.fillStyle=silhouetteColor(biome.sky[2],0.42,1.20);
        ctx.beginPath(); ctx.moveTo(x-tw,top); ctx.lineTo(x+tw,top); ctx.lineTo(x,top-tw*1.2); ctx.closePath(); ctx.fill();
        { const _b=0.5+0.5*Math.sin(t*3+k); _glow(x,top-tw*1.2,12*DPR*(0.6+0.5*_b),"#ff3df0",0.5+0.4*_b); ctx.fillStyle=silhouetteColor("#ff3df0",1,1.7); ctx.beginPath(); ctx.arc(x,top-tw*1.2,3.2*DPR,0,6.283); ctx.fill(); }
        const carY=top+th*(0.15+0.70*(0.5+0.5*Math.sin(t*0.9+k*1.3)));
        ctx.fillStyle=silhouetteColor(biome.sky[2],0.40,1.0); rr(ctx, x-tw*0.9, carY, tw*1.8, tw*0.7, 2*DPR); ctx.fill();
      }
    }
  }
  // roller-coaster track with hills + loops (near)
  {
    const f=0.42, shift=cam*f, period=560*DPR, baseY=horizon-horizon*0.06;
    const coY=wx=>{ const p=wx/period; return baseY - (horizon*0.20)*(0.5+0.5*Math.sin(p*6.283))*(0.6+0.4*Math.sin(p*2.0+1.3)); };
    ctx.strokeStyle=silhouetteColor(biome.sky[1],0.28,0.9); ctx.lineWidth=3*DPR;
    const step=46*DPR;
    for(let x=-((shift%step)+step)%step; x<W+step; x+=step){ const y=coY(x+shift);
      ctx.beginPath(); ctx.moveTo(x,y); ctx.lineTo(x,horizon); ctx.stroke(); }
    ctx.strokeStyle=silhouetteColor(biome.sky[1],0.50,1.25); ctx.lineWidth=4*DPR; ctx.lineCap='round';
    ctx.beginPath();
    for(let x=0;x<=W;x+=8*DPR){ const y=coY(x+shift); x?ctx.lineTo(x,y):ctx.moveTo(x,y); }
    ctx.stroke();
    const l0=Math.floor(shift/period)-1, l1=Math.ceil((shift+W)/period)+1;
    for(let k=l0;k<=l1;k++){
      if(hashS(k*3+2)<0.5) continue;
      const wx=k*period+period*0.5, x=wx-shift; if(x<-90*DPR||x>W+90*DPR) continue;
      const lr=horizon*0.11, ly=coY(wx)-lr;
      ctx.beginPath(); ctx.arc(x,ly,lr,0,6.283); ctx.stroke();
    }
  }
}
function drawFloorPark(biome,t){
  const D=DPR, gy=GROUND(), gh=H-gy;
  let g=ctx.createLinearGradient(0,gy,0,H); g.addColorStop(0,"#241a37"); g.addColorStop(1,"#120b1f"); ctx.fillStyle=g; ctx.fillRect(0,gy,W,gh); // NIGHT boardwalk: dark planks lit by neon
  ctx.save(); ctx.beginPath(); ctx.rect(0,gy,W,gh); ctx.clip();
  const plank=40*D, off=((cam)%plank+plank)%plank;
  for(let x=-off,i=0; x<W+plank; x+=plank,i++){ const col=Math.round((cam-off)/plank)+i;
    ctx.fillStyle=(col&1)?"rgba(255,235,200,.10)":"rgba(70,45,20,.12)"; ctx.fillRect(x,gy,plank,gh);
    ctx.fillStyle="rgba(40,25,10,.35)"; ctx.fillRect(x,gy,2*D,gh);
    ctx.strokeStyle="rgba(60,38,16,.18)"; ctx.lineWidth=1*D; const grx=x+plank*(0.4+0.3*hashS(col));
    ctx.beginPath(); ctx.moveTo(grx,gy); ctx.lineTo(grx+(hashY(col)-0.5)*8*D,H); ctx.stroke();
    ctx.fillStyle="rgba(30,18,8,.40)"; ctx.beginPath(); ctx.arc(x+plank*0.5,gy+gh*0.18,1.6*D,0,7); ctx.fill();
    ctx.beginPath(); ctx.arc(x+plank*0.5,H-gh*0.18,1.6*D,0,7); ctx.fill(); }
  const bandH=Math.min(gh,16*D), sw=22*D, so=((cam)%(sw*2)+sw*2)%(sw*2), hues=["#ff8fc6","#ffe27a","#8fd0ff","#a8f0a0"];
  for(let x=-so-bandH,i=0; x<W+bandH; x+=sw,i++){ const col=((Math.round((cam-so)/sw)+i)%4+4)%4;
    ctx.fillStyle=silhouetteColor(hues[col],0.85,1.0);
    ctx.beginPath(); ctx.moveTo(x,gy+bandH); ctx.lineTo(x+sw,gy+bandH); ctx.lineTo(x+sw+bandH,gy); ctx.lineTo(x+bandH,gy); ctx.closePath(); ctx.fill(); }
  ctx.fillStyle=getAccent(.7); ctx.fillRect(0,gy,W,2*D); // glowing neon curb line
  // neon string-lights running along the boardwalk edge (additive glow, cycling colors)
  const _NL=["#ff3df0","#27e6ff","#ffe14d","#5cff9e","#b06bff"], lspc=44*D, lo=((cam*1.0)%lspc+lspc)%lspc;
  ctx.save(); ctx.globalCompositeOperation="lighter";
  for(let x=-lo,i=0; x<W+lspc; x+=lspc,i++){ const col=Math.round((cam-lo)/lspc)+i, c=_NL[((col%5)+5)%5], tw=0.45+0.55*Math.sin(t*4+col*0.9), py=gy+5*D;
    let lg=ctx.createRadialGradient(x,py,0,x,py,11*D); lg.addColorStop(0,silhouetteColor(c,0.5+0.45*tw,1.5)); lg.addColorStop(1,silhouetteColor(c,0,1)); ctx.fillStyle=lg; ctx.beginPath(); ctx.arc(x,py,11*D,0,7); ctx.fill();
    ctx.fillStyle=silhouetteColor(c,1,1.7); ctx.beginPath(); ctx.arc(x,py,2.2*D,0,7); ctx.fill(); }
  ctx.restore();
  ctx.restore();
}
// ===== NEW ENVIRONMENTS (06-27, subagent-designed) =====
function scapeRainbowRoad(biome,horizon,t){
  const D=DPR;
  { const g=ctx.createLinearGradient(0,0,0,horizon); g.addColorStop(0.00,biome.sky[0]); g.addColorStop(0.55,biome.sky[1]); g.addColorStop(1.00,biome.sky[2]); ctx.fillStyle=g; ctx.fillRect(0,0,W,horizon); }
  ctx.save(); ctx.globalCompositeOperation='lighter';
  const starSheet=(f,gap,perCol,szMin,szMax,twAmp)=>{ const g=gap*D, shift=cam*f; const i0=Math.floor((shift-g)/g), i1=Math.ceil((shift+W+g)/g);
    for(let k=i0;k<=i1;k++){ for(let s=0;s<perCol;s++){ const id=k*13.17+s*4.31; const sx=k*g-shift+hashS(id)*g; const sy=hashY(id)*horizon*0.98; const ph=hashS(id*1.7)*6.283; const tw=0.5+0.5*Math.sin(t*(1.0+hashY(id*2.3)*1.6)+ph); const a=0.20+twAmp*tw; const sz=(szMin+(szMax-szMin)*hashY(id*3.1))*D*(0.7+0.6*tw); const tint=hashS(id*5.5);
      ctx.fillStyle = tint<0.70 ? 'rgba(255,255,255,'+a+')' : tint<0.88 ? getAccent(a) : silhouetteColor(biome.sky[1],a,2.6); ctx.beginPath(); ctx.arc(sx,sy,sz,0,6.283); ctx.fill(); } } };
  starSheet(0.06,46,3,0.5,1.2,0.50); starSheet(0.10,78,3,0.7,1.8,0.55); ctx.restore();
  { const gap=2400*D, f=0.045, shift=cam*f; const i0=Math.floor((shift-gap)/gap), i1=Math.ceil((shift+W+gap)/gap);
    for(let k=i0;k<=i1;k++){ if(hashS(k*2+1)<0.5) continue; const x=k*gap-shift+(hashY(k)*0.4-0.2)*gap; const y=horizon*(0.18+0.16*hashY(k*3))+Math.sin(t*0.05+k)*4*D; const R=horizon*(0.12+0.06*hashS(k*5)); const hh=(k*47)%360;
      const hg=ctx.createRadialGradient(x,y,R*0.5,x,y,R*2.4); hg.addColorStop(0,silhouetteColor(biome.sky[1],0.20,2.4)); hg.addColorStop(1,silhouetteColor(biome.sky[1],0,2.4)); ctx.fillStyle=hg; ctx.beginPath(); ctx.arc(x,y,R*2.4,0,6.283); ctx.fill();
      ctx.save(); ctx.globalCompositeOperation='lighter'; ctx.strokeStyle=getAccent(0.45); ctx.lineWidth=Math.max(2*D,R*0.10); ctx.beginPath(); ctx.ellipse(x,y,R*1.7,R*0.5,-0.4,Math.PI,2*Math.PI); ctx.stroke(); ctx.restore();
      const bg=ctx.createLinearGradient(x-R,y-R,x+R,y+R); bg.addColorStop(0,hslHex(hh,0.55,0.62)); bg.addColorStop(1,hslHex(hh+40,0.65,0.32)); ctx.fillStyle=bg; ctx.beginPath(); ctx.arc(x,y,R,0,6.283); ctx.fill();
      ctx.save(); ctx.beginPath(); ctx.arc(x,y,R,0,6.283); ctx.clip(); ctx.fillStyle='rgba(255,255,255,0.06)'; for(let bd=-2;bd<=2;bd++) ctx.fillRect(x-R,y+bd*R*0.34-R*0.05,R*2,R*0.10); ctx.fillStyle='rgba(0,0,0,0.12)'; ctx.beginPath(); ctx.arc(x+R*0.5,y+R*0.2,R,0,6.283); ctx.fill(); ctx.restore();
      ctx.save(); ctx.globalCompositeOperation='lighter'; ctx.strokeStyle=getAccent(0.85); ctx.lineWidth=Math.max(2*D,R*0.10); ctx.beginPath(); ctx.ellipse(x,y,R*1.7,R*0.5,-0.4,0,Math.PI); ctx.stroke(); ctx.restore(); } }
  { const gap=900*D, f=0.07, shift=cam*f; const i0=Math.floor((shift-gap)/gap), i1=Math.ceil((shift+W+gap)/gap);
    for(let k=i0;k<=i1;k++){ if(hashS(k*3+7)<0.55) continue; const x=k*gap-shift+hashY(k*2)*gap*0.5; const y=horizon*(0.12+0.30*hashS(k*9))+Math.sin(t*0.08+k)*5*D; const r=horizon*(0.018+0.016*hashY(k*4));
      ctx.fillStyle='rgba(206,210,235,0.88)'; ctx.beginPath(); ctx.arc(x,y,r,0,6.283); ctx.fill(); ctx.fillStyle='rgba(255,255,255,0.10)'; ctx.beginPath(); ctx.arc(x-r*0.3,y-r*0.3,r*0.55,0,6.283); ctx.fill(); ctx.fillStyle='rgba(40,44,72,0.25)'; ctx.beginPath(); ctx.arc(x+r*0.35,y+r*0.15,r*0.30,0,6.283); ctx.fill(); } }
  ctx.save(); ctx.globalCompositeOperation='lighter'; ctx.lineCap='round';
  for(let lane=0;lane<3;lane++){ const P=2.4+lane*0.7, dur=0.9; const idx=Math.floor(t/P)+lane*17; const local=t-Math.floor(t/P)*P; if(local>dur) continue; if(hashS(idx*1.7+lane)<0.40) continue; const pr=local/dur; const sx0=hashY(idx*2.3)*W*0.85; const sy0=hashS(idx*3.1)*horizon*0.5; const dirx=0.93, diry=0.34, travel=W*1.1; const len=160*D+hashS(idx*5.3)*120*D; const hx=sx0+dirx*travel*pr, hy=sy0+diry*travel*pr; const tx=hx-dirx*len, ty=hy-diry*len; const fade=Math.sin(pr*Math.PI); const gr=ctx.createLinearGradient(tx,ty,hx,hy); gr.addColorStop(0,'rgba(255,255,255,0)'); gr.addColorStop(1,getAccent(0.9*fade)); ctx.strokeStyle=gr; ctx.lineWidth=2.2*D; ctx.beginPath(); ctx.moveTo(tx,ty); ctx.lineTo(hx,hy); ctx.stroke(); ctx.fillStyle='rgba(255,255,255,'+(0.85*fade)+')'; ctx.beginPath(); ctx.arc(hx,hy,2.4*D,0,6.283); ctx.fill(); }
  ctx.restore();
  { const f=0.16, shift=cam*f; const amp=horizon*0.11, midY=horizon*0.52; const freq=2*Math.PI/(900*D); const yc=x=>midY+amp*Math.sin((x+shift)*freq)+amp*0.35*Math.sin((x+shift)*freq*2.3+1.1); const bands=7, bandTh=Math.max(3*D,horizon*0.013), step=18*D;
    ctx.save(); ctx.globalCompositeOperation='lighter'; ctx.lineCap='round'; ctx.lineJoin='round';
    ctx.lineWidth=bandTh*bands*1.6; ctx.strokeStyle=silhouetteColor(biome.sky[1],0.10,2.3); ctx.beginPath(); for(let x=-step;x<=W+step;x+=step){ const y=yc(x); x<=-step?ctx.moveTo(x,y):ctx.lineTo(x,y); } ctx.stroke();
    for(let b=0;b<bands;b++){ const off=(b-(bands-1)/2)*bandTh; const hue=(b/bands)*300; ctx.lineWidth=bandTh*1.15; ctx.strokeStyle=hslHex(hue,0.95,0.58); ctx.globalAlpha=0.15+0.05*Math.sin(t*1.6+b*0.6); ctx.beginPath(); for(let x=-step;x<=W+step;x+=step){ const y=yc(x)+off; x<=-step?ctx.moveTo(x,y):ctx.lineTo(x,y); } ctx.stroke(); }
    ctx.globalAlpha=1;
    for(let s=0;s<10;s++){ const xx=((t*180*D+s*(W+step)/10)%(W+step)); const y=yc(xx); const a=0.5+0.5*Math.sin(t*3+s); ctx.fillStyle='rgba(255,255,255,'+(0.08+0.16*a)+')'; ctx.beginPath(); ctx.arc(xx,y,bandTh*1.6,0,6.283); ctx.fill(); }
    { const gap=120*D, gs=cam*f; const i0=Math.floor((gs-gap)/gap), i1=Math.ceil((gs+W+gap)/gap); const edge=bandTh*((bands-1)/2)+6*D; for(let k=i0;k<=i1;k++){ const x=k*gap-gs, y=yc(x); const tw=0.5+0.5*Math.sin(t*2.4+k*0.7); ctx.fillStyle=getAccent(0.28+0.45*tw); ctx.beginPath(); ctx.arc(x,y-edge,2.4*D,0,6.283); ctx.fill(); ctx.beginPath(); ctx.arc(x,y+edge,2.4*D,0,6.283); ctx.fill(); } }
    ctx.restore(); }
  ctx.save(); ctx.globalCompositeOperation='lighter';
  { const gap=260*D, f=0.42, shift=cam*f; const i0=Math.floor((shift-gap)/gap), i1=Math.ceil((shift+W+gap)/gap); for(let k=i0;k<=i1;k++){ if(hashS(k*7+3)<0.45) continue; const x=k*gap-shift+hashY(k)*gap; const y=horizon*(0.10+0.7*hashS(k*11)); const r=(8+14*hashY(k*5))*D; const tw=0.5+0.5*Math.sin(t*1.3+k); const hue=(k*57+t*30)%360; const g=ctx.createRadialGradient(x,y,0,x,y,r); g.addColorStop(0,hslHex(hue,0.8,0.72)); g.addColorStop(1,'rgba(0,0,0,0)'); ctx.globalAlpha=0.05+0.07*tw; ctx.fillStyle=g; ctx.beginPath(); ctx.arc(x,y,r,0,6.283); ctx.fill(); } ctx.globalAlpha=1; }
  ctx.restore();
}
function drawFloorRainbowRoad(biome,t){
  const D=DPR, gy=GROUND(), gh=H-gy;
  let g=ctx.createLinearGradient(0,gy,0,H); g.addColorStop(0,biome.sky[2]); g.addColorStop(1,'#05030d'); ctx.fillStyle=g; ctx.fillRect(0,gy,W,gh);
  ctx.save(); ctx.beginPath(); ctx.rect(0,gy,W,gh); ctx.clip();
  ctx.save(); ctx.globalCompositeOperation='lighter'; ctx.strokeStyle=getAccent(0.7); ctx.lineWidth=2*D; ctx.beginPath(); ctx.moveTo(0,gy); ctx.lineTo(W,gy); ctx.stroke(); ctx.restore();
  ctx.save(); ctx.globalCompositeOperation='lighter'; const sp=54*D, off=((cam*0.5)%sp+sp)%sp; for(let x=-off,i=0;x<W+sp;x+=sp,i++){ const col=Math.round((cam*0.5-off)/sp)+i; for(let s=0;s<2;s++){ const id=col*5.7+s*2.1; const px=x+hashS(id)*sp, py=gy+(0.10+0.80*hashY(id))*gh; const tw=0.5+0.5*Math.sin(t*1.6+id); ctx.fillStyle='rgba(255,255,255,'+(0.07+0.16*tw)+')'; ctx.beginPath(); ctx.arc(px,py,1.2*D,0,6.283); ctx.fill(); } } ctx.restore();
  ctx.save(); ctx.globalCompositeOperation='lighter'; const cyc=gy+gh*0.5, bands=7, bandTh=Math.max(2*D,gh*0.03); for(let b=0;b<bands;b++){ const hue=(b/bands)*300, y=cyc+(b-(bands-1)/2)*bandTh; ctx.globalAlpha=0.10; ctx.strokeStyle=hslHex(hue,0.95,0.55); ctx.lineWidth=bandTh*1.1; ctx.beginPath(); ctx.moveTo(0,y); ctx.lineTo(W,y); ctx.stroke(); } ctx.globalAlpha=1;
  const chSp=110*D, cOff=((cam)%chSp+chSp)%chSp; ctx.lineCap='round'; for(let x=-cOff;x<W+chSp;x+=chSp){ const a=0.16+0.16*Math.sin(t*3-x*0.01); ctx.strokeStyle='rgba(255,255,255,'+a+')'; ctx.lineWidth=2*D; ctx.beginPath(); ctx.moveTo(x,cyc-bandTh*2); ctx.lineTo(x+18*D,cyc); ctx.lineTo(x,cyc+bandTh*2); ctx.stroke(); } ctx.restore(); ctx.restore();
}
function scapeSynthwave(biome,horizon,t){
  const D=DPR, vx=W*0.5, CY=hslHex(189,0.95,0.62);
  { const top=horizon-horizon*0.22; ctx.lineWidth=1*D; for(let i=-9;i<=9;i++){ const fx=vx+i*W*0.075; ctx.strokeStyle=getAccent(0.05+0.02*Math.abs(Math.sin(i*0.6+t*0.4))); ctx.beginPath(); ctx.moveTo(vx,horizon); ctx.lineTo(fx,top); ctx.stroke(); } for(let r=1;r<=4;r++){ const fr=r/4, off=(t*0.18)%1; const yy=horizon-(horizon-top)*Math.min(1,fr*0.9+off*0.12); ctx.strokeStyle=silhouetteColor(CY,0.06*(1-fr*0.6),1); ctx.beginPath(); ctx.moveTo(0,yy); ctx.lineTo(W,yy); ctx.stroke(); } }
  { const sunR=horizon*0.40, sx=vx, sy=horizon-sunR*0.32; const bloom=ctx.createRadialGradient(sx,sy,sunR*0.6,sx,sy,sunR*1.7); bloom.addColorStop(0,"rgba(255,80,170,0.20)"); bloom.addColorStop(1,"rgba(255,80,170,0)"); ctx.fillStyle=bloom; ctx.beginPath(); ctx.arc(sx,sy,sunR*1.7,0,6.283); ctx.fill();
    ctx.save(); ctx.beginPath(); ctx.rect(0,0,W,horizon+1*D); ctx.clip(); ctx.beginPath(); ctx.arc(sx,sy,sunR,0,6.283); ctx.clip(); const sg=ctx.createLinearGradient(0,sy-sunR,0,sy+sunR); sg.addColorStop(0.00,hslHex(322,0.95,0.62)); sg.addColorStop(0.45,hslHex(338,0.95,0.58)); sg.addColorStop(0.72,hslHex(24,0.98,0.58)); sg.addColorStop(1.00,hslHex(48,1.00,0.62)); ctx.fillStyle=sg; ctx.fillRect(sx-sunR,sy-sunR,sunR*2,sunR*2); ctx.fillStyle=biome.sky[2]; for(let i=0;i<16;i++){ const fr=i/15, gy=sy+sunR*(-0.04+fr*0.96); if(gy>horizon) break; ctx.fillRect(sx-sunR,gy,sunR*2,D*(0.8+fr*5.5)); } ctx.restore(); }
  { const gap=360*D, f=0.12, shift=cam*f+t*8*D; const i0=Math.floor((shift-gap)/gap), i1=Math.ceil((shift+W+gap)/gap); for(let k=i0;k<=i1;k++){ if(hashS(k*3+2)<0.5) continue; const x=k*gap-shift, cy=horizon*(0.16+0.34*hashY(k*5)); const cw=gap*(0.30+0.18*hashS(k*7)), ch=cw*0.16; const a=0.10+0.06*(0.5+0.5*Math.sin(t*0.8+k)); ctx.fillStyle=silhouetteColor(CY,a,1); for(let s=0;s<3;s++){ const ox=(s-1)*cw*0.5, ow=cw*(0.7-0.16*Math.abs(s-1)); rr(ctx,x+ox-ow*0.5,cy-ch*0.5,ow,ch,ch*0.5); ctx.fill(); } ctx.strokeStyle=getAccent(a*0.9); ctx.lineWidth=1.4*D; rr(ctx,x-cw*0.5,cy-ch*0.5,cw,ch,ch*0.5); ctx.stroke(); } }
  for(let pass=0;pass<2;pass++){ const f=0.16+pass*0.07, shift=cam*f, gap=(190-pass*30)*D, yBase=horizon; const i0=Math.floor((shift-gap)/gap), i1=Math.ceil((shift+W+gap)/gap); const baseH=horizon*(0.16+pass*0.10), varH=horizon*(0.16+pass*0.10); const peakY=k=>yBase-(baseH+varH*hashY(k*1.3+pass*9));
    ctx.fillStyle=silhouetteColor(biome.sky[2],pass?0.95:0.55,pass?0.5:1.6); ctx.beginPath(); ctx.moveTo(i0*gap-shift,yBase); for(let k=i0;k<=i1;k++){ const x=k*gap-shift; ctx.lineTo(x-gap*0.5,yBase); ctx.lineTo(x,peakY(k)); } ctx.lineTo(i1*gap-shift,yBase); ctx.closePath(); ctx.fill();
    ctx.strokeStyle=pass?getAccent(0.5):silhouetteColor(CY,0.42,1); ctx.lineWidth=(pass?2.2:1.6)*D; ctx.lineCap="round"; ctx.beginPath(); for(let k=i0;k<=i1;k++){ const x=k*gap-shift; if(k===i0) ctx.moveTo(x-gap*0.5,yBase); else ctx.lineTo(x-gap*0.5,yBase); ctx.lineTo(x,peakY(k)); } ctx.stroke();
    ctx.lineWidth=1*D; ctx.strokeStyle=silhouetteColor(pass?"#ff2d95":CY,0.15,1); for(let k=i0;k<=i1;k++){ const x=k*gap-shift, py=peakY(k); ctx.beginPath(); ctx.moveTo(x,py); ctx.lineTo(x,yBase); ctx.stroke(); } }
  { const gap=240*D, f=0.34, shift=cam*f; const i0=Math.floor((shift-gap)/gap), i1=Math.ceil((shift+W+gap)/gap); for(let k=i0;k<=i1;k++){ if(hashS(k*5+1)<0.55) continue; const x=k*gap-shift+(hashY(k)-0.5)*gap*0.4; const hh=horizon*(0.30+0.16*hashY(k*7)), lean=(hashS(k*3)-0.5)*0.4; const topX=x+lean*hh, topY=horizon-hh, sway=Math.sin(t*0.7+k)*hh*0.02, tx=topX+sway; ctx.strokeStyle=silhouetteColor(biome.sky[2],0.95,0.6); ctx.lineCap="round"; ctx.lineWidth=4.5*D; ctx.beginPath(); ctx.moveTo(x,horizon); ctx.quadraticCurveTo(x+lean*hh*0.4,horizon-hh*0.55,tx,topY); ctx.stroke(); ctx.lineWidth=3*D; for(let s=0;s<7;s++){ const dir=-0.9+s*0.3, fl=hh*(0.40+0.14*hashS(k*11+s)); const mx=tx+Math.sin(dir)*fl*0.6, my=topY-Math.cos(dir)*fl*0.4; const ex=tx+Math.sin(dir)*fl, ey=topY-Math.cos(dir)*fl*0.2+fl*0.35; ctx.beginPath(); ctx.moveTo(tx,topY); ctx.quadraticCurveTo(mx,my,ex,ey); ctx.stroke(); } } }
  { ctx.save(); ctx.globalAlpha=0.04; ctx.fillStyle=hslHex(190,0.6,0.7); for(let y=(Math.floor(t*30)%4)*D;y<horizon;y+=4*D) ctx.fillRect(0,y,W,1*D); ctx.restore(); }
}
function drawFloorSynthwave(biome,t){
  const D=DPR, g0=GROUND(), bh=H-g0, vx=W*0.5, CY=hslHex(188,0.95,0.6);
  let g=ctx.createLinearGradient(0,g0,0,H); g.addColorStop(0,silhouetteColor(biome.sky[2],1,1.5)); g.addColorStop(1,"#05010e"); ctx.fillStyle=g; ctx.fillRect(0,g0,W,bh);
  ctx.save(); ctx.beginPath(); ctx.rect(0,g0,W,bh); ctx.clip();
  const cols=14, spacing=W/cols; for(let i=-cols;i<=cols;i++){ const bx=vx+i*spacing*1.15, edge=1-Math.min(1,Math.abs(i)/cols); ctx.strokeStyle=getAccent(0.16+0.34*edge); ctx.lineWidth=(0.8+0.8*edge)*D; ctx.beginPath(); ctx.moveTo(vx,g0); ctx.lineTo(bx,H); ctx.stroke(); }
  const rows=18, phase=(cam*0.0016+t*0.9)%1; for(let i=0;i<rows;i++){ const fr=(i+phase)/rows, yy=g0+bh*fr*fr; ctx.strokeStyle=silhouetteColor(CY,0.10+0.55*fr,1); ctx.lineWidth=(0.6+1.8*fr)*D; ctx.beginPath(); ctx.moveTo(0,yy); ctx.lineTo(W,yy); ctx.stroke(); }
  ctx.restore();
  ctx.save(); ctx.globalCompositeOperation="lighter"; ctx.strokeStyle=getAccent(0.9); ctx.lineWidth=2.4*D; ctx.beginPath(); ctx.moveTo(0,g0); ctx.lineTo(W,g0); ctx.stroke(); const hg=ctx.createLinearGradient(0,g0-14*D,0,g0+14*D); hg.addColorStop(0,getAccent(0)); hg.addColorStop(0.5,getAccent(0.22)); hg.addColorStop(1,getAccent(0)); ctx.fillStyle=hg; ctx.fillRect(0,g0-14*D,W,28*D); ctx.restore();
}
function scapeNebula(biome,horizon,t){
  { ctx.save(); ctx.globalCompositeOperation='lighter'; const gap=520*DPR, f=0.08, shift=cam*f; const i0=Math.floor((shift-gap)/gap), i1=Math.ceil((shift+W+gap)/gap);
    for(let k=i0;k<=i1;k++){ const bx=k*gap-shift+(hashY(k*1.3)-0.5)*gap*0.4; for(let b=0;b<2;b++){ const drift=Math.sin(t*0.05+k*0.7+b*2.3)*18*DPR; const cx=bx+(hashY(k*5+b*3)-0.5)*gap*0.5+drift; const cy=horizon*(0.14+0.50*hashS(k*3+b*7)); const pulse=0.92+0.08*Math.sin(t*0.18+k+b*1.7); const R=horizon*(0.34+0.34*hashY(k*7+b))*pulse; const hue=180+160*hashS(k*11+b*3); const col=hslHex(hue,0.86,0.56); const a=0.06+0.06*hashY(k*13+b);
      const g=ctx.createRadialGradient(cx,cy,0,cx,cy,R); g.addColorStop(0,silhouetteColor(col,a,1.0)); g.addColorStop(0.45,silhouetteColor(col,a*0.45,1.0)); g.addColorStop(1,silhouetteColor(col,0,1.0)); ctx.fillStyle=g; ctx.beginPath(); ctx.arc(cx,cy,R,0,6.283); ctx.fill();
      const cg=ctx.createRadialGradient(cx,cy,0,cx,cy,R*0.4); cg.addColorStop(0,silhouetteColor(hslHex(hue+30,0.9,0.62),a*0.9,1.0)); cg.addColorStop(1,silhouetteColor(hslHex(hue+30,0.9,0.62),0,1.0)); ctx.fillStyle=cg; ctx.beginPath(); ctx.arc(cx,cy,R*0.4,0,6.283); ctx.fill(); } } ctx.restore(); }
  { ctx.save(); ctx.globalCompositeOperation='lighter'; const f=0.05, n=78, span=horizon*1.04; for(let k=0;k<n;k++){ const baseX=hashY(k*1.7)*W; const x=(((baseX-cam*f)%W)+W)%W; const y=hashS(k*2.3)*span; const tw=0.5+0.5*Math.sin(t*1.3+k*2.1+hashY(k*9)*6.283); const r=(0.5+0.6*hashS(k*3))*DPR; const a=0.18+0.42*tw; ctx.fillStyle=(k%6===0)?getAccent(a):'rgba(228,236,255,'+a+')'; ctx.beginPath(); ctx.arc(x,y,r,0,6.283); ctx.fill(); } ctx.restore(); }
  { const gap=1700*DPR, f=0.12, shift=cam*f; const i0=Math.floor((shift-gap)/gap), i1=Math.ceil((shift+W+gap)/gap);
    for(let k=i0;k<=i1;k++){ const x=k*gap-shift+(hashY(k*3.1)-0.5)*gap*0.4; const R=horizon*(0.15+0.06*hashS(k*5)); const cy=horizon*(0.30+0.22*hashY(k*7)); const hue=190+90*hashS(k*9); const tilt=-0.42;
      ctx.save(); ctx.translate(x,cy); ctx.rotate(tilt); ctx.scale(1,0.30); ctx.strokeStyle=getAccent(0.30); ctx.lineWidth=R*0.10; ctx.beginPath(); ctx.arc(0,0,R*1.8,0,6.283); ctx.stroke(); ctx.strokeStyle=silhouetteColor(biome.sky[0],0.42,2.4); ctx.lineWidth=R*0.05; ctx.beginPath(); ctx.arc(0,0,R*1.5,0,6.283); ctx.stroke(); ctx.restore();
      const pg=ctx.createLinearGradient(x-R,cy-R,x+R,cy+R); pg.addColorStop(0,silhouetteColor(hslHex(hue,0.62,0.60),0.62,1.0)); pg.addColorStop(1,silhouetteColor(biome.sky[2],0.72,1.3)); ctx.fillStyle=pg; ctx.beginPath(); ctx.arc(x,cy,R,0,6.283); ctx.fill();
      ctx.save(); ctx.beginPath(); ctx.arc(x,cy,R,0,6.283); ctx.clip(); for(let bnd=0;bnd<3;bnd++){ const by=cy-R*0.55+R*bnd*0.46; ctx.fillStyle=silhouetteColor(hslHex(hue+bnd*14,0.55,0.55),0.16,1.0); ctx.fillRect(x-R,by,R*2,R*0.22); } ctx.fillStyle=getAccent(0.10); ctx.beginPath(); ctx.arc(x-R*0.32,cy-R*0.30,R*0.7,0,6.283); ctx.fill(); ctx.restore();
      ctx.save(); ctx.translate(x,cy); ctx.rotate(tilt); ctx.scale(1,0.30); ctx.strokeStyle=getAccent(0.34); ctx.lineWidth=R*0.10; ctx.beginPath(); ctx.arc(0,0,R*1.8,0,Math.PI); ctx.stroke(); ctx.restore();
      for(let m=0;m<2;m++){ const ang=t*(0.18+0.10*m)+k*1.7+m*2.6; const mx=x+Math.cos(ang)*R*(2.3+0.5*m); const my=cy+Math.sin(ang)*R*0.72; const mr=R*(0.13+0.05*m); ctx.fillStyle=silhouetteColor(biome.sky[1],0.70,1.7+0.3*m); ctx.beginPath(); ctx.arc(mx,my,mr,0,6.283); ctx.fill(); ctx.fillStyle=getAccent(0.12); ctx.beginPath(); ctx.arc(mx-mr*0.3,my-mr*0.3,mr*0.6,0,6.283); ctx.fill(); } } }
  { ctx.save(); ctx.globalCompositeOperation='lighter'; const f=0.18, n=52, span=horizon*1.06; for(let k=0;k<n;k++){ const baseX=hashY(k*3.7+1)*W; const x=(((baseX-cam*f)%W)+W)%W; const y=hashS(k*4.1+2)*span; const tw=0.5+0.5*Math.sin(t*2.1+k*1.9+hashY(k*5)*6.283); const r=(0.9+1.0*hashS(k*3))*DPR; const a=0.30+0.55*tw; const bright=hashS(k*7)>0.7; ctx.fillStyle=(k%5===0)?getAccent(a):'rgba(240,246,255,'+a+')'; ctx.beginPath(); ctx.arc(x,y,r,0,6.283); ctx.fill(); if(bright){ const L=r*(3.0+1.5*tw); ctx.strokeStyle=(k%5===0)?getAccent(a*0.8):'rgba(240,246,255,'+(a*0.8)+')'; ctx.lineWidth=DPR; ctx.lineCap='round'; ctx.beginPath(); ctx.moveTo(x-L,y); ctx.lineTo(x+L,y); ctx.moveTo(x,y-L); ctx.lineTo(x,y+L); ctx.stroke(); } } ctx.restore(); }
  { ctx.save(); ctx.globalCompositeOperation='lighter'; ctx.lineCap='round'; for(let r=0;r<2;r++){ const baseY=horizon*(0.09+r*0.07), amp=horizon*(0.05+0.02*r); const a0=0.045+0.035*(0.5+0.5*Math.sin(t*0.4+r*1.7)); ctx.strokeStyle=getAccent(a0); ctx.lineWidth=(22+12*r)*DPR; ctx.beginPath(); for(let x=-40;x<=W+40;x+=30){ const phase=t*0.22+r*2.2; const y=baseY+Math.sin(x*0.003+phase)*amp+Math.sin(x*0.008-phase*1.5)*amp*0.4-cam*0.012; x===-40?ctx.moveTo(x,y):ctx.lineTo(x,y); } ctx.stroke(); } ctx.restore(); }
  { ctx.save(); ctx.globalCompositeOperation='lighter'; ctx.lineCap='round'; const period=5.5; for(let s=0;s<3;s++){ const tt=t/period+s*0.41; const ph=tt-Math.floor(tt); if(ph>0.16) continue; const cyc=Math.floor(tt); const sx=hashY(cyc*7+s*3)*W; const sy=hashS(cyc*5+s)*horizon*0.55; const len=(130+90*hashY(cyc*11+s))*DPR; const ang=-0.55+0.30*hashS(cyc*13+s); const p=ph/0.16, fade=Math.sin(p*Math.PI); const hx=sx+Math.cos(ang)*len*1.7*p, hy=sy+Math.sin(ang)*len*1.7*p; const tx=hx-Math.cos(ang)*len, ty=hy-Math.sin(ang)*len; const g=ctx.createLinearGradient(tx,ty,hx,hy); g.addColorStop(0,getAccent(0)); g.addColorStop(0.6,getAccent(0.5*fade)); g.addColorStop(1,'rgba(255,255,255,'+(0.85*fade)+')'); ctx.strokeStyle=g; ctx.lineWidth=2.4*DPR; ctx.beginPath(); ctx.moveTo(tx,ty); ctx.lineTo(hx,hy); ctx.stroke(); ctx.fillStyle='rgba(255,255,255,'+(0.9*fade)+')'; ctx.beginPath(); ctx.arc(hx,hy,2.2*DPR,0,6.283); ctx.fill(); } ctx.restore(); }
}
function scapeVaporwave(biome,horizon,t){
  { const f=0.08, shift=cam*f; const sx=((W*0.5-shift)%(W*1.6)+W*1.6)%(W*1.6)-W*0.3; const cy=horizon-horizon*0.46+Math.sin(t*0.4)*4*DPR; const sr=horizon*0.30; const gl=ctx.createRadialGradient(sx,cy,sr*0.4,sx,cy,sr*2.6); gl.addColorStop(0,getAccent(0.34)); gl.addColorStop(0.5,getAccent(0.12)); gl.addColorStop(1,getAccent(0)); ctx.fillStyle=gl; ctx.beginPath(); ctx.arc(sx,cy,sr*2.6,0,6.283); ctx.fill();
    ctx.save(); ctx.beginPath(); ctx.arc(sx,cy,sr,0,6.283); ctx.clip(); const sg=ctx.createLinearGradient(0,cy-sr,0,cy+sr); sg.addColorStop(0,hslHex(48,1,0.74)); sg.addColorStop(0.45,hslHex(330,0.92,0.68)); sg.addColorStop(1,hslHex(292,0.82,0.62)); ctx.fillStyle=sg; ctx.fillRect(sx-sr,cy-sr,sr*2,sr*1.05); let yy=cy+sr*0.05, bh=sr*0.085; while(yy<cy+sr){ const fr=(yy-cy)/sr, bw=bh*(1-fr*0.55), gap=bh*(0.35+fr*1.5); ctx.fillRect(sx-sr,yy,sr*2,Math.max(1,bw)); yy+=bw+gap; } ctx.restore(); }
  { const f=0.12, shift=cam*f, vx=W*0.5, ROWS=8; ctx.strokeStyle=getAccent(0.16); ctx.lineWidth=1*DPR; for(let i=1;i<=ROWS;i++){ const p=i/ROWS, y=horizon-horizon*0.34*p*p; ctx.globalAlpha=0.18*(1-p); ctx.beginPath(); ctx.moveTo(0,y); ctx.lineTo(W,y); ctx.stroke(); } const colW=90*DPR, off=((shift)%colW+colW)%colW; ctx.globalAlpha=0.14; for(let gx=-7;gx<=7;gx++){ const bx=vx+gx*colW-off; ctx.beginPath(); ctx.moveTo(bx,horizon); ctx.lineTo(vx+(bx-vx)*0.18,horizon-horizon*0.32); ctx.stroke(); } ctx.globalAlpha=1; }
  { const gap=360*DPR, f=0.18, shift=cam*f; const i0=Math.floor((shift-gap)/gap), i1=Math.ceil((shift+W+gap)/gap); const base=silhouetteColor(biome.sky[2],0.50,1.45), hi=silhouetteColor(biome.sky[1],0.42,1.7); for(let k=i0;k<=i1;k++){ if(hashS(k*2+11)<0.5) continue; const x=k*gap-shift+(hashY(k*3)-0.5)*gap*0.4; const s=horizon*(0.16+0.10*hashY(k*5)); const y=horizon*(0.30+0.30*hashS(k*7))+Math.sin(t*0.5+k*1.7)*6*DPR; ctx.save(); ctx.translate(x,y); ctx.fillStyle=base; ctx.beginPath(); ctx.moveTo(-s*0.62,s*0.95); ctx.quadraticCurveTo(-s*0.55,s*0.25,-s*0.26,s*0.18); ctx.lineTo(s*0.26,s*0.18); ctx.quadraticCurveTo(s*0.55,s*0.25,s*0.62,s*0.95); ctx.closePath(); ctx.fill(); ctx.fillRect(-s*0.16,-s*0.02,s*0.32,s*0.30); ctx.beginPath(); ctx.ellipse(0,-s*0.34,s*0.30,s*0.40,0,0,6.283); ctx.fill(); ctx.beginPath(); ctx.moveTo(s*0.28,-s*0.40); ctx.quadraticCurveTo(s*0.42,-s*0.34,s*0.27,-s*0.24); ctx.closePath(); ctx.fill(); ctx.fillStyle=hi; ctx.beginPath(); ctx.ellipse(-s*0.10,-s*0.42,s*0.12,s*0.20,-0.3,0,6.283); ctx.fill(); ctx.restore(); } }
  { const gap=300*DPR, f=0.30, shift=cam*f; const i0=Math.floor((shift-gap)/gap), i1=Math.ceil((shift+W+gap)/gap); ctx.lineCap='round'; for(let k=i0;k<=i1;k++){ if(hashY(k*4+3)<0.45) continue; const x=k*gap-shift; const hh=horizon*(0.26+0.12*hashS(k*3)); const lean=(hashS(k)-0.5)*0.4, topX=x+lean*hh, topY=horizon-hh; ctx.strokeStyle=silhouetteColor(biome.sky[0],0.5,0.72); ctx.lineWidth=4*DPR; ctx.beginPath(); ctx.moveTo(x,horizon); ctx.quadraticCurveTo(x+lean*hh*0.6,horizon-hh*0.6,topX,topY); ctx.stroke(); const fronds=7, sway=Math.sin(t*0.7+k)*0.07; ctx.lineWidth=3*DPR; for(let fi=0;fi<fronds;fi++){ const a=(fi/(fronds-1)-0.5)*2.6+sway, fl=hh*0.5; const fx=topX+Math.sin(a)*fl, fy=topY-Math.cos(a)*fl*0.5; ctx.beginPath(); ctx.moveTo(topX,topY); ctx.quadraticCurveTo(topX+Math.sin(a)*fl*0.5,topY-Math.cos(a)*fl*0.4-5*DPR,fx,fy+8*DPR); ctx.stroke(); } } }
  { const gap=240*DPR, f=0.42, shift=cam*f; const i0=Math.floor((shift-gap)/gap), i1=Math.ceil((shift+W+gap)/gap); for(let k=i0;k<=i1;k++){ if(hashS(k*6+2)<0.5) continue; const x=k*gap-shift+(hashY(k*2)-0.5)*gap*0.5; const y=horizon*(0.18+0.55*hashS(k*9))+Math.sin(t*0.6+k*2.1)*10*DPR; const r=horizon*(0.04+0.05*hashY(k*3)); const rot=t*0.3*(hashS(k)>0.5?1:-1)+k; const col=hashY(k*7)>0.5?silhouetteColor(biome.sky[1],0.32,1.6):getAccent(0.28); drawPoly(x,y,r,3,rot,col); ctx.fillStyle=getAccent(0.22+0.18*(0.5+0.5*Math.sin(t*2+k))); ctx.beginPath(); ctx.arc(x,y,1.8*DPR,0,6.283); ctx.fill(); } for(let b=0;b<3;b++){ const yb=(t*22+b*137)%(horizon*0.9); const a=0.05+0.04*(0.5+0.5*Math.sin(t*1.3+b)); const jit=Math.sin(t*9+b*3)*6*DPR; ctx.fillStyle=getAccent(a); ctx.fillRect(jit,yb,W,2.2*DPR); } }
}
function drawFloorVaporwave(biome,t){
  ctx.save(); const D=DPR, g0=GROUND(), bh=H-g0; let bg=ctx.createLinearGradient(0,g0,0,H); bg.addColorStop(0,"#2a1a4a"); bg.addColorStop(1,"#120a26"); ctx.fillStyle=bg; ctx.fillRect(0,g0,W,bh);
  const c1=silhouetteColor("#ff6fd0",0.85,1.05), c2=silhouetteColor("#3fe0d8",0.80,1.0); const SP=0.02, STEP=Math.max(2*D,bh/44);
  for(let y=g0;y<H;y+=STEP){ const y1=Math.min(H,y+STEP), yc=(y+y1)/2, dd=Math.max(0.5,yc-g0), depth=bh/dd; const row=Math.floor(depth+cam*SP), colW=dd*0.42; if(colW<4*D){ ctx.globalAlpha=0.5; ctx.fillStyle=(row&1)?c1:c2; ctx.fillRect(0,y,W,y1-y); ctx.globalAlpha=1; continue; } let n=Math.floor((-W/2)/colW)-1, sx=W/2+n*colW; for(;sx<W;sx+=colW,n++){ if(sx+colW<0) continue; ctx.fillStyle=((row+n)&1)?c1:c2; ctx.fillRect(sx,y,colW+1,y1-y); } }
  let hg=ctx.createLinearGradient(0,g0,0,g0+18*D); hg.addColorStop(0,getAccent(0.6)); hg.addColorStop(1,getAccent(0)); ctx.fillStyle=hg; ctx.fillRect(0,g0,W,18*D); ctx.fillStyle=getAccent(0.9); ctx.fillRect(0,g0,W,2.5*D); ctx.restore();
}
function scapeGeoVoid(biome,horizon,t){
  const TAU=6.28318530718; const n=parseInt((biome.ac||"#22e6ff").slice(1),16); const ar=(n>>16)&255, ag=(n>>8)&255, ab=n&255; function ac2(a){ return 'rgba('+ab+','+ar+','+ag+','+a+')'; }
  function strokePoly(x,y,r,sides,rot,stroke,lw){ ctx.save(); ctx.translate(x,y); ctx.rotate(rot); ctx.beginPath(); for(let i=0;i<sides;i++){ const a=i/sides*TAU-Math.PI/2, px=Math.cos(a)*r, py=Math.sin(a)*r; i?ctx.lineTo(px,py):ctx.moveTo(px,py); } ctx.closePath(); ctx.lineWidth=lw; ctx.strokeStyle=stroke; ctx.stroke(); ctx.restore(); }
  const beat=0.5+0.5*Math.sin(t*3.0);
  { const g=ctx.createLinearGradient(0,0,0,horizon); g.addColorStop(0,'rgba(2,4,9,0.55)'); g.addColorStop(1,silhouetteColor(biome.sky[2],0.42,0.55)); ctx.fillStyle=g; ctx.fillRect(0,0,W,horizon); }
  { const cx=W*0.5, cy=horizon*0.44, R=Math.max(W,horizon)*(0.42+0.10*beat); const g=ctx.createRadialGradient(cx,cy,0,cx,cy,R); g.addColorStop(0,getAccent(0.16+0.13*beat)); g.addColorStop(0.5,getAccent(0.045+0.035*beat)); g.addColorStop(1,getAccent(0)); ctx.fillStyle=g; ctx.fillRect(0,0,W,horizon); }
  { ctx.lineWidth=1*DPR; const gap=150*DPR, f=0.12, shift=cam*f; const i0=Math.floor((shift-gap)/gap), i1=Math.ceil((shift+W+gap)/gap); ctx.strokeStyle=ac2(0.05); for(let k=i0;k<=i1;k++){ const x=k*gap-shift; ctx.beginPath(); ctx.moveTo(x,0); ctx.lineTo(x,horizon); ctx.stroke(); } const rows=5, sp=horizon/rows, drift=(t*18*DPR)%sp; for(let r=0;r<=rows;r++){ const y=horizon-(r*sp+drift); ctx.strokeStyle=getAccent(0.05*(1-r/rows)*(0.6+0.4*beat)); ctx.beginPath(); ctx.moveTo(0,y); ctx.lineTo(W,y); ctx.stroke(); } }
  const PLY=[{f:0.15,gap:270,r:0.18,a:0.30,spin:0.10,fill:true,sides:[6,3],sky:1},{f:0.27,gap:310,r:0.15,a:0.55,spin:-0.16,fill:false,sides:[4,3],sky:1},{f:0.41,gap:370,r:0.21,a:0.85,spin:0.22,fill:false,sides:[4,6],sky:0}];
  for(let li=0;li<PLY.length;li++){ const L=PLY[li], gap=L.gap*DPR, shift=cam*L.f; const i0=Math.floor((shift-gap)/gap), i1=Math.ceil((shift+W+gap)/gap); for(let k=i0;k<=i1;k++){ const hy=hashY(k*3+li*11), hs=hashS(k*5+li*7); const x=k*gap-shift+(hs-0.5)*gap*0.40; const y=horizon*(0.12+0.72*hy); const sc=1+0.05*Math.sin(t*3.0+k+li); const r=horizon*L.r*(0.55+0.85*hs)*sc; const sides=L.sides[(hashY(k*13+li)>0.5)?0:1]; const rot=t*L.spin*(0.7+0.6*hs)+k*0.9; if(L.fill){ drawPoly(x,y,r,sides,rot,silhouetteColor(biome.sky[L.sky],L.a,0.85)); } else { const useB=hashS(k*9+li*4)>0.5, lw=Math.max(1.5*DPR,r*0.035), a=L.a*(0.7+0.3*beat); strokePoly(x,y,r,sides,rot,useB?ac2(a):getAccent(a),lw); strokePoly(x,y,r*0.60,sides,-rot*0.6,useB?ac2(a*0.4):getAccent(a*0.4),lw*0.7); } } }
  { const N=26, span=W+220*DPR; for(let p=0;p<N;p++){ const f=0.18+0.22*((p%5)/5), shift=cam*f; const x=((hashY(p*7)*span-shift)%span+span)%span-110*DPR; const y=horizon*(0.05+0.90*hashS(p*3))+Math.sin(t*0.6+p)*8*DPR; const r=(0.6+1.6*hashY(p*5))*DPR; const tw=0.4+0.6*(0.5+0.5*Math.sin(t*2.2+p*1.7)); ctx.fillStyle=(p&1)?getAccent(0.5*tw):ac2(0.5*tw); ctx.beginPath(); ctx.arc(x,y,r,0,TAU); ctx.fill(); } }
}
function scapeAbyss(biome,horizon,t){
  const glowAc=(a)=>silhouetteColor(biome.ac,a,1.0); const MAG=hslHex(304,0.92,0.62); const glowMag=(a)=>silhouetteColor(MAG,a,1.0);
  { const f=0.08, shift=cam*f, gap=440*DPR; const i0=Math.floor((shift-gap)/gap), i1=Math.ceil((shift+W+gap)/gap); ctx.save(); ctx.globalCompositeOperation='lighter'; for(let k=i0;k<=i1;k++){ const x=k*gap-shift; if(hashS(k*2+3)<0.30) continue; const sway=Math.sin(t*0.22+k*1.7)*46*DPR; const slant=(hashY(k)-0.5)*0.7*gap+120*DPR; const w=gap*(0.10+0.06*hashS(k*5)); const tx=x+sway, bx=x+sway+slant; const g=ctx.createLinearGradient(0,0,0,horizon); const al=0.05+0.05*hashY(k*3); g.addColorStop(0,silhouetteColor(biome.sky[0],al,1.25)); g.addColorStop(0.6,silhouetteColor(biome.sky[0],al*0.45,1.0)); g.addColorStop(1,silhouetteColor(biome.sky[0],0,1.0)); ctx.fillStyle=g; ctx.beginPath(); ctx.moveTo(tx-w,0); ctx.lineTo(tx+w,0); ctx.lineTo(bx+w*2.2,horizon); ctx.lineTo(bx-w*2.2,horizon); ctx.closePath(); ctx.fill(); } ctx.restore(); }
  { const f=0.12, shift=cam*f, gap=1500*DPR; const i0=Math.floor((shift-gap)/gap), i1=Math.ceil((shift+W+gap)/gap); for(let k=i0;k<=i1;k++){ if(hashS(k*7+1)<0.55) continue; const swim=t*7*DPR*(hashS(k)>0.5?1:-1); const x=k*gap-shift+swim; const y=horizon*(0.26+0.30*hashY(k*3))+Math.sin(t*0.18+k)*16*DPR; const L=horizon*(0.42+0.18*hashY(k*5)), Bh=L*0.30; const dir=(hashS(k)>0.5?1:-1); ctx.fillStyle=silhouetteColor(biome.sky[1],0.34,1.45); ctx.beginPath(); ctx.ellipse(x,y,L*0.5,Bh,0,0,6.283); ctx.fill(); ctx.beginPath(); ctx.moveTo(x-dir*L*0.5,y); ctx.lineTo(x-dir*L*0.72,y-Bh*1.1); ctx.lineTo(x-dir*L*0.60,y); ctx.lineTo(x-dir*L*0.72,y+Bh*1.1); ctx.closePath(); ctx.fill(); ctx.save(); ctx.globalCompositeOperation='lighter'; for(let s=-3;s<=3;s++){ const bx=x+dir*s*L*0.10, by=y+Bh*0.78; const tw=0.5+0.5*Math.sin(t*1.6+s*0.8+k); ctx.fillStyle=glowAc(0.10+0.16*tw); ctx.beginPath(); ctx.arc(bx,by,1.8*DPR,0,6.283); ctx.fill(); } const lx=x+dir*L*0.58, ly=y-Bh*0.3, lp=0.5+0.5*Math.sin(t*2.0+k); ctx.fillStyle=glowMag(0.18+0.30*lp); ctx.beginPath(); ctx.arc(lx,ly,3.4*DPR,0,6.283); ctx.fill(); ctx.restore(); } }
  { const f=0.18, shift=cam*f, gap=150*DPR; const i0=Math.floor((shift-gap)/gap), i1=Math.ceil((shift+W+gap)/gap); for(let k=i0;k<=i1;k++){ const x=k*gap-shift; const hh=horizon*(0.14+0.20*hashY(k*3)); const tw=gap*(0.16+0.08*hashS(k)); const sway=Math.sin(t*0.5+k)*tw*0.18; const top=horizon-hh; ctx.fillStyle=silhouetteColor(biome.sky[2],0.55,1.55); ctx.beginPath(); ctx.moveTo(x-tw,horizon); ctx.quadraticCurveTo(x-tw*0.5+sway,top+hh*0.4,x+sway,top); ctx.quadraticCurveTo(x+tw*0.5+sway,top+hh*0.4,x+tw,horizon); ctx.closePath(); ctx.fill(); ctx.save(); ctx.globalCompositeOperation='lighter'; const tw2=0.5+0.5*Math.sin(t*1.4+k*1.3); ctx.fillStyle=(hashS(k*5)>0.5?glowAc(0.18+0.22*tw2):glowMag(0.18+0.22*tw2)); ctx.beginPath(); ctx.arc(x+sway,top,3*DPR,0,6.283); ctx.fill(); ctx.restore(); } }
  { const f=0.26, shift=cam*f, gap=300*DPR; const i0=Math.floor((shift-gap)/gap), i1=Math.ceil((shift+W+gap)/gap); ctx.save(); ctx.globalCompositeOperation='lighter'; ctx.lineCap='round'; for(let k=i0;k<=i1;k++){ const depth=0.55+0.45*hashY(k*3); const x=k*gap-shift+Math.sin(t*0.4+k)*22*DPR; const baseY=horizon*(0.20+0.46*hashS(k*5)); const y=baseY+Math.sin(t*(0.5+0.3*hashY(k))+k*1.7)*18*DPR; const sz=gap*0.085*depth, pulse=1+0.12*Math.sin(t*1.6+k*1.1); const bw=sz*pulse, bh=sz*0.82/pulse; const cy=hashS(k*7)>0.5, glow=cy?glowAc:glowMag; ctx.fillStyle=glow(0.06*depth); ctx.beginPath(); ctx.ellipse(x,y,bw*1.9,bh*1.9,0,0,6.283); ctx.fill(); ctx.fillStyle=glow(0.16*depth); ctx.beginPath(); ctx.ellipse(x,y,bw,bh,0,Math.PI,2*Math.PI); ctx.quadraticCurveTo(x,y+bh*0.55,x-bw,y); ctx.closePath(); ctx.fill(); ctx.strokeStyle=glow(0.30*depth); ctx.lineWidth=2*DPR; ctx.beginPath(); ctx.ellipse(x,y,bw,bh,0,Math.PI,2*Math.PI); ctx.stroke(); ctx.fillStyle=glow(0.22*depth); ctx.beginPath(); ctx.arc(x,y-bh*0.2,bw*0.35*pulse,0,6.283); ctx.fill(); ctx.strokeStyle=glow(0.12*depth); ctx.lineWidth=1.6*DPR; const tn=4; for(let s=0;s<tn;s++){ const ox=(s/(tn-1)-0.5)*bw*1.3; const len=bh*(3.2+1.6*hashY(k*9+s)); ctx.beginPath(); ctx.moveTo(x+ox,y+bh*0.2); let px=x+ox, py=y+bh*0.2; for(let q=1;q<=4;q++){ const fr=q/4, wob=Math.sin(t*2.0+k+s*0.9+fr*3.0)*bw*0.28*fr; const nx=x+ox+wob, ny=y+bh*0.2+len*fr; ctx.quadraticCurveTo(px+wob*0.5,(py+ny)/2,nx,ny); px=nx; py=ny; } ctx.stroke(); } } ctx.restore(); }
  { ctx.save(); ctx.globalCompositeOperation='lighter'; const N=52; for(let i=0;i<N;i++){ const depth=0.3+0.7*hashY(i*3.1); const drift=cam*(0.06+0.30*depth); const px=((hashS(i)*W*1.4+i*97*DPR-drift+Math.sin(t*0.3+i)*16*DPR)%W+W)%W; const py=((hashY(i)*horizon*1.05+Math.sin(t*0.45+i*1.3)*14*DPR)%horizon+horizon)%horizon; const tw=0.5+0.5*Math.sin(t*(1.0+depth*1.4)+i*2.1); const r=(0.7+1.9*depth)*DPR; const c=(hashS(i*5)>0.66)?silhouetteColor(MAG,(0.08+0.26*tw)*depth,1.0):silhouetteColor(biome.ac,(0.10+0.30*tw)*depth,1.0); ctx.fillStyle=c; ctx.beginPath(); ctx.arc(px,py,r,0,6.283); ctx.fill(); } ctx.restore(); }
  { const f=0.40, shift=cam*f, gap=96*DPR; const i0=Math.floor((shift-gap)/gap), i1=Math.ceil((shift+W+gap)/gap); ctx.lineCap='round'; for(let k=i0;k<=i1;k++){ const x=k*gap-shift; const hh=horizon*(0.18+0.22*hashY(k*9)); ctx.strokeStyle=silhouetteColor(biome.sky[2],0.5,1.35); ctx.lineWidth=(4+2*hashS(k))*DPR; let px=x, py=horizon; ctx.beginPath(); ctx.moveTo(x,horizon); for(let s=1;s<=4;s++){ const fr=s/4, sway=Math.sin(t*1.0+k+fr*2.4)*hh*0.20*fr; const nx=x+sway, ny=horizon-hh*fr; ctx.quadraticCurveTo(px+sway*0.5,(py+ny)/2,nx,ny); px=nx; py=ny; } ctx.stroke(); ctx.save(); ctx.globalCompositeOperation='lighter'; const tw=0.5+0.5*Math.sin(t*1.8+k*1.4); ctx.fillStyle=glowAc(0.14+0.20*tw); ctx.beginPath(); ctx.arc(px,py,2.6*DPR,0,6.283); ctx.fill(); ctx.restore(); } }
}
function drawFloorAbyss(biome,t){
  const D=DPR, gy=GROUND(), gh=H-gy; let g=ctx.createLinearGradient(0,gy,0,H); g.addColorStop(0,silhouetteColor(biome.sky[2],1,1.4)); g.addColorStop(1,"#01040c"); ctx.fillStyle=g; ctx.fillRect(0,gy,W,gh); ctx.save(); ctx.beginPath(); ctx.rect(0,gy,W,gh); ctx.clip(); ctx.strokeStyle=getAccent(.45); ctx.lineWidth=2*D; ctx.beginPath(); ctx.moveTo(0,gy); ctx.lineTo(W,gy); ctx.stroke();
  { const gap=140*D, shift=cam, i0=Math.floor((shift-gap)/gap), i1=Math.ceil((shift+W+gap)/gap); ctx.fillStyle=silhouetteColor(biome.sky[2],0.6,0.7); ctx.beginPath(); ctx.moveTo(i0*gap-shift,H); for(let k=i0;k<=i1;k++){ const x=k*gap-shift; const hh=gh*(0.16+0.16*hashY(k)); ctx.lineTo(x-gap*0.5,gy+gh*0.5); ctx.quadraticCurveTo(x,gy+gh*0.5-hh,x+gap*0.5,gy+gh*0.5); } ctx.lineTo(i1*gap-shift,H); ctx.closePath(); ctx.fill(); }
  ctx.globalCompositeOperation='lighter'; const MAG=hslHex(304,0.92,0.62); const gap=64*D, shift=cam, i0=Math.floor((shift-gap)/gap), i1=Math.ceil((shift+W+gap)/gap); for(let k=i0;k<=i1;k++){ const x=k*gap-shift+(hashY(k)-0.5)*gap*0.6; const y=gy+gh*(0.30+0.62*hashS(k)); const tw=0.5+0.5*Math.sin(t*(1.4+hashY(k))+k*1.7); const r=(1.4+2.0*hashS(k*5))*D; const col=(hashS(k*3)>0.5)?silhouetteColor(biome.ac,0.12+0.30*tw,1.0):silhouetteColor(MAG,0.12+0.30*tw,1.0); ctx.fillStyle=col; ctx.beginPath(); ctx.arc(x,y,r,0,6.283); ctx.fill(); } ctx.restore();
}
function scapeZoomies(biome,horizon,t){
  const D=DPR;
  const SUNX=W*0.78 - cam*0.02, SUNY=horizon*0.24;
  { ctx.save();
    const tg=ctx.createLinearGradient(0,0,0,horizon);
    tg.addColorStop(0.00, silhouetteColor(biome.sky[0],0.42,0.70));
    tg.addColorStop(0.55,'rgba(0,0,0,0)');
    ctx.fillStyle=tg; ctx.fillRect(0,0,W,horizon);
    ctx.globalCompositeOperation='lighter';
    const bg=ctx.createRadialGradient(SUNX,SUNY,0,SUNX,SUNY,horizon*1.20);
    bg.addColorStop(0,'rgba(255,250,214,0.42)');
    bg.addColorStop(0.4,'rgba(255,240,172,0.16)');
    bg.addColorStop(1,'rgba(255,236,150,0)');
    ctx.fillStyle=bg; ctx.fillRect(0,0,W,horizon);
    ctx.restore(); }
  { ctx.save(); ctx.globalCompositeOperation='lighter';
    const cg=ctx.createRadialGradient(SUNX,SUNY,0,SUNX,SUNY,horizon*0.16);
    cg.addColorStop(0,'rgba(255,255,250,0.96)');
    cg.addColorStop(0.55,'rgba(255,246,196,0.50)');
    cg.addColorStop(1,'rgba(255,238,166,0)');
    ctx.fillStyle=cg; ctx.beginPath(); ctx.arc(SUNX,SUNY,horizon*0.16,0,6.283); ctx.fill();
    ctx.restore(); }
  function cloudPath(x,cy,cw,ch,lobes,seed){
    ctx.beginPath(); ctx.moveTo(x-cw,cy+ch*0.5);
    for(let l=0;l<lobes;l++){
      const lx=x-cw+(l/(lobes-1))*cw*2;
      const lr=ch*(0.66+0.5*hashS(seed+l))*Math.sin((l+0.5)/lobes*Math.PI)+ch*0.40;
      ctx.arc(lx,cy-lr*0.35,Math.max(1,lr),Math.PI,0,false); }
    ctx.lineTo(x+cw,cy+ch*0.5); ctx.closePath(); }
  function cloudBand(f,gap,yFrac,sizeFrac,drift,thresh){
    const g=gap*D, shift=cam*f+drift;
    const i0=Math.floor((shift-g)/g), i1=Math.ceil((shift+W+g)/g);
    for(let k=i0;k<=i1;k++){ if(hashY(k*1.3)<thresh) continue;
      const x=k*g-shift, cy=horizon*yFrac + Math.sin(t*0.2+k)*4*D;
      const cw=g*(sizeFrac+0.08*hashS(k)), ch=cw*(0.30+0.06*hashY(k));
      const lobes=4+Math.floor(hashY(k*3)*3);
      cloudPath(x,cy,cw,ch,lobes,k*3.1);
      const cgd=ctx.createLinearGradient(0,cy-ch,0,cy+ch*0.6);
      cgd.addColorStop(0,'rgba(255,255,255,0.98)');
      cgd.addColorStop(1, silhouetteColor(biome.sky[0],0.55,1.30));
      ctx.fillStyle=cgd; ctx.fill();
      ctx.save(); ctx.globalCompositeOperation='lighter';
      cloudPath(x,cy,cw,ch,lobes,k*3.1);
      ctx.strokeStyle='rgba(255,255,255,0.45)'; ctx.lineWidth=2*D; ctx.lineJoin='round'; ctx.stroke();
      ctx.restore(); } }
  function hillY(x,shift,topFrac,amp,freq){ const wx=(x+shift)*freq;
    return horizon*topFrac + Math.sin(wx)*amp + Math.sin(wx*2.3+1.7)*amp*0.4 + Math.sin(wx*0.5+4.1)*amp*0.6; }
  function hillPath(f,topFrac,amp,wl){ const shift=cam*f, freq=6.283/wl, step=Math.max(10*D,W/48);
    ctx.beginPath(); ctx.moveTo(-2*D,horizon+80*D); ctx.lineTo(-2*D,hillY(-2*D,shift,topFrac,amp,freq));
    for(let x=-2*D; x<=W+2*D; x+=step) ctx.lineTo(x,hillY(x,shift,topFrac,amp,freq));
    ctx.lineTo(W+2*D,horizon+80*D); ctx.closePath(); }
  function hillTop(f,topFrac,amp,wl,style,lw){ const shift=cam*f, freq=6.283/wl, step=Math.max(10*D,W/48);
    ctx.strokeStyle=style; ctx.lineWidth=lw; ctx.lineJoin='round'; ctx.beginPath();
    for(let x=-2*D,first=true; x<=W+2*D; x+=step){ const y=hillY(x,shift,topFrac,amp,freq);
      if(first){ctx.moveTo(x,y);first=false;} else ctx.lineTo(x,y); }
    ctx.stroke(); }
  cloudBand(0.05, 430, 0.20, 0.34, t*5*D, 0.42);
  cloudBand(0.12, 470, 0.32, 0.40, t*8*D, 0.40);
  hillPath(0.06, 0.55, horizon*0.05, 900*D); ctx.fillStyle=hslHex(158,0.42,0.34); ctx.fill();
  hillTop (0.06, 0.55, horizon*0.05, 900*D, 'rgba(190,255,200,0.35)', 2*D);
  { const gap=680*D, f=0.10, shift=cam*f;
    const i0=Math.floor((shift-gap)/gap), i1=Math.ceil((shift+W+gap)/gap);
    for(let k=i0;k<=i1;k++){ if(hashS(k*2.9+6)<0.62) continue;
      const x=k*gap-shift+40*D*hashY(k), topY=horizon*0.50, botY=horizon*0.84, wdt=(14+8*hashS(k))*D;
      const wg=ctx.createLinearGradient(0,topY,0,botY);
      wg.addColorStop(0,'rgba(214,242,255,0.88)'); wg.addColorStop(1,'rgba(150,210,255,0.55)');
      ctx.fillStyle=wg; ctx.fillRect(x-wdt*0.5,topY,wdt,botY-topY);
      ctx.save(); ctx.globalCompositeOperation='lighter';
      ctx.strokeStyle='rgba(255,255,255,0.5)'; ctx.lineWidth=2*D;
      for(let s=0;s<3;s++){ const yy=topY+(((t*90*D + s*(botY-topY)/3)%(botY-topY))); ctx.beginPath(); ctx.moveTo(x-wdt*0.28,yy); ctx.lineTo(x-wdt*0.28,yy+12*D); ctx.stroke(); }
      ctx.restore();
      ctx.fillStyle='rgba(255,255,255,0.72)'; ctx.beginPath(); ctx.ellipse(x,botY,wdt*0.9,5*D,0,0,6.283); ctx.fill(); } }
  hillPath(0.16, 0.64, horizon*0.06, 700*D); ctx.fillStyle=hslHex(132,0.52,0.40); ctx.fill();
  hillTop (0.16, 0.64, horizon*0.06, 700*D, 'rgba(180,255,190,0.45)', 2.5*D);
  { const gap=560*D, f=0.16, shift=cam*f;
    const i0=Math.floor((shift-gap)/gap), i1=Math.ceil((shift+W+gap)/gap);
    for(let k=i0;k<=i1;k++){ if(hashS(k*2.1+3)<0.58) continue;
      const x=k*gap-shift+50*D*hashY(k), r=Math.max(18*D,(34+26*hashS(k*3))*D);
      const cy=hillY(x,shift,0.64,horizon*0.06,6.283/(700*D)) - r - 6*D;
      ctx.strokeStyle=hslHex(120,0.48,0.30); ctx.lineWidth=r*0.16; ctx.lineCap='round';
      ctx.beginPath(); ctx.moveTo(x-r*0.9,cy+r+r*0.3); ctx.quadraticCurveTo(x-r*0.5,cy+r,x-r*0.05,cy+r); ctx.stroke();
      ctx.beginPath(); ctx.moveTo(x+r*0.9,cy+r+r*0.3); ctx.quadraticCurveTo(x+r*0.5,cy+r,x+r*0.05,cy+r); ctx.stroke();
      ctx.strokeStyle=hslHex(140,0.52,0.36); ctx.lineWidth=Math.max(3*D,r*0.34);
      ctx.beginPath(); ctx.arc(x,cy,r,0,6.283); ctx.stroke();
      ctx.strokeStyle=hslHex(42,0.55,0.62); ctx.lineWidth=Math.max(1.5*D,r*0.11);
      ctx.beginPath(); ctx.arc(x,cy,r,0,6.283); ctx.stroke();
      ctx.save(); ctx.globalCompositeOperation='lighter'; ctx.strokeStyle='rgba(255,255,255,0.25)'; ctx.lineWidth=Math.max(1,r*0.06);
      for(let a=0;a<8;a++){ const ang=a/8*6.283; ctx.beginPath(); ctx.moveTo(x+Math.cos(ang)*r*0.84,cy+Math.sin(ang)*r*0.84); ctx.lineTo(x+Math.cos(ang)*r*1.16,cy+Math.sin(ang)*r*1.16); ctx.stroke(); }
      ctx.restore(); } }
  hillPath(0.30, 0.72, horizon*0.055, 520*D); ctx.fillStyle=hslHex(108,0.62,0.46); ctx.fill();
  { ctx.save(); hillPath(0.30, 0.72, horizon*0.055, 520*D); ctx.clip();
    const tt=42*D, to=((cam*0.30)%tt+tt)%tt;
    for(let x=-to,i=0; x<W+tt; x+=tt,i++){ const col=Math.round((cam*0.30-to)/tt)+i;
      for(let r=0;r<7;r++){ if(((col+r)&1)) continue; ctx.fillStyle='rgba(255,255,255,0.07)'; ctx.fillRect(x, horizon*0.72 + r*tt, tt, tt); } }
    ctx.restore(); }
  hillTop(0.30, 0.72, horizon*0.055, 520*D, 'rgba(180,255,150,0.60)', 3*D);
  { const gap=360*D, f=0.30, shift=cam*f;
    const i0=Math.floor((shift-gap)/gap), i1=Math.ceil((shift+W+gap)/gap);
    for(let k=i0;k<=i1;k++){ if(hashS(k*1.7+4)<0.52) continue;
      const x=k*gap-shift+60*D*hashY(k);
      const base=hillY(x,shift,0.72,horizon*0.055,6.283/(520*D)), th=horizon*(0.16+0.10*hashS(k));
      const tx=x+8*D, ty=base-th;
      ctx.strokeStyle=hslHex(32,0.5,0.40); ctx.lineWidth=6*D; ctx.lineCap='round';
      ctx.beginPath(); ctx.moveTo(x,base); ctx.quadraticCurveTo(x+12*D,base-th*0.55,tx,ty); ctx.stroke();
      for(let fd=0;fd<7;fd++){ const a=-0.55+fd*0.5, sway=Math.sin(t*1.0+k+fd)*0.06;
        ctx.fillStyle=hslHex(122-fd*3,0.6,0.32+0.06*(fd&1));
        ctx.beginPath(); ctx.moveTo(tx,ty);
        ctx.quadraticCurveTo(tx+Math.cos(a+sway)*28*D, ty+Math.sin(a+sway)*28*D-10*D, tx+Math.cos(a+sway)*48*D, ty+Math.sin(a+sway)*48*D+8*D);
        ctx.lineTo(tx,ty+4*D); ctx.closePath(); ctx.fill(); }
      ctx.fillStyle=hslHex(30,0.5,0.28); for(let c=0;c<2;c++){ ctx.beginPath(); ctx.arc(tx+(c?4*D:-4*D),ty+5*D,3*D,0,6.283); ctx.fill(); } } }
  { const gap=300*D, f=0.34, shift=cam*f;
    const i0=Math.floor((shift-gap)/gap), i1=Math.ceil((shift+W+gap)/gap);
    for(let k=i0;k<=i1;k++){ if(hashS(k*3.7+1)<0.55) continue;
      const x=k*gap-shift+80*D*hashY(k), y=horizon*(0.30+0.32*hashY(k*5))+Math.sin(t*1.2+k)*8*D;
      const s=Math.max(4*D,(9+5*hashS(k*2))*D), spin=Math.abs(Math.cos(t*2.2+k*1.3)), w=Math.max(1.5*D,s*spin);
      ctx.save(); ctx.globalCompositeOperation='lighter';
      const gl=ctx.createRadialGradient(x,y,0,x,y,s*2.2);
      gl.addColorStop(0,'rgba(255,224,120,0.5)'); gl.addColorStop(1,'rgba(255,224,120,0)');
      ctx.fillStyle=gl; ctx.beginPath(); ctx.arc(x,y,s*2.2,0,6.283); ctx.fill();
      ctx.restore();
      ctx.strokeStyle=hslHex(46,0.95,0.55); ctx.lineWidth=Math.max(1.5*D,s*0.32); ctx.lineJoin='round';
      ctx.beginPath(); ctx.moveTo(x,y-s); ctx.lineTo(x+w,y); ctx.lineTo(x,y+s); ctx.lineTo(x-w,y); ctx.closePath(); ctx.stroke();
      ctx.fillStyle='rgba(255,255,240,'+(0.4+0.5*spin).toFixed(3)+')';
      ctx.beginPath(); ctx.arc(x-w*0.3,y-s*0.3,Math.max(1,s*0.18),0,6.283); ctx.fill(); } }
  { ctx.save(); ctx.globalCompositeOperation='lighter';
    for(let i=0;i<7;i++){ const span=W+140*D, spd=(120+60*hashY(i))*D;
      const sx=(((hashS(i*5)*span - cam*0.5 - t*spd)%span)+span)%span-70*D;
      const sy=horizon*(0.16+0.60*hashY(i*3)), len=(80+120*hashS(i))*D;
      const sg=ctx.createLinearGradient(sx,sy,sx+len,sy);
      sg.addColorStop(0,'rgba(255,255,255,0)');
      sg.addColorStop(0.5,'rgba(255,255,255,'+(0.10+0.10*hashY(i)).toFixed(3)+')');
      sg.addColorStop(1,'rgba(255,255,255,0)');
      ctx.fillStyle=sg; ctx.fillRect(sx,sy,len,(1.5+hashS(i)*1.5)*D); }
    ctx.restore(); }
}
function drawFloorZoomies(biome,t){
  const D=DPR, gy=GROUND(), gh=H-gy;
  let g=ctx.createLinearGradient(0,gy,0,H);
  g.addColorStop(0, hslHex(40,0.56,0.52));
  g.addColorStop(0.5, hslHex(34,0.55,0.42));
  g.addColorStop(1, hslHex(28,0.52,0.30));
  ctx.fillStyle=g; ctx.fillRect(0,gy,W,gh);
  ctx.save(); ctx.beginPath(); ctx.rect(0,gy,W,gh); ctx.clip();
  const capH=Math.min(gh*0.55, 28*D), dirtY=gy+capH;
  const dt=Math.max(14*D, 20*D), off=((cam)%dt+dt)%dt;
  const tanA=hslHex(40,0.58,0.60), tanB=hslHex(32,0.55,0.44);
  const rows=Math.ceil((H-dirtY)/dt)+3;
  for(let x=-off,i=0; x<W+dt; x+=dt,i++){
    const col=Math.round((cam-off)/dt)+i;
    const skew=(col&1)?dt*0.5:0;
    for(let r=-1;r<rows;r++){
      const by=dirtY - dt*0.5 + r*dt + skew;
      if(by>H) continue;
      ctx.fillStyle=((col+r)&1)? tanA : tanB;
      ctx.fillRect(x,by,dt+1,dt+1);
    }
    if(hashY(col*1.3)>0.62){ ctx.fillStyle='rgba(70,44,20,0.35)';
      ctx.beginPath(); ctx.arc(x+dt*0.5, dirtY+gh*0.45+(hashS(col)-0.5)*10*D, 3*D,0,6.283); ctx.fill(); }
  }
  const sh=ctx.createLinearGradient(0,dirtY,0,H);
  sh.addColorStop(0,'rgba(0,0,0,0)'); sh.addColorStop(1,'rgba(40,22,8,0.30)');
  ctx.fillStyle=sh; ctx.fillRect(0,dirtY,W,H-dirtY);
  const gt=Math.max(14*D, 26*D), goff=((cam)%gt+gt)%gt;
  const grnA=hslHex(104,0.64,0.48), grnB=hslHex(96,0.66,0.37);
  for(let x=-goff,i=0; x<W+gt; x+=gt,i++){ const col=Math.round((cam-goff)/gt)+i;
    ctx.fillStyle=(col&1)?grnA:grnB; ctx.fillRect(x,gy,gt,capH); }
  ctx.fillStyle='rgba(40,74,24,0.55)'; ctx.fillRect(0,dirtY-1.5*D,W,2*D);
  const sc=Math.max(12*D, 20*D), soff=((cam)%sc+sc)%sc;
  ctx.fillStyle=hslHex(100,0.70,0.52);
  for(let x=-soff-sc,i=0; x<W+sc; x+=sc,i++){ const col=Math.round((cam-soff)/sc)+i;
    ctx.beginPath(); ctx.arc(x+sc*0.5, gy+3*D, Math.max(1,sc*(0.52+0.06*hashS(col))), Math.PI,2*Math.PI,false); ctx.fill(); }
  ctx.fillStyle=hslHex(88,0.85,0.66); ctx.fillRect(0,gy,W,3*D);
  ctx.fillStyle=getAccent(0.55); ctx.fillRect(0,gy-1*D,W,1.5*D);
  for(let x=-soff,i=0; x<W+sc; x+=sc,i++){ const col=Math.round((cam-soff)/sc)+i;
    if(hashS(col*1.9)>0.5){ ctx.fillStyle=hslHex(84,0.9,0.72); ctx.fillRect(x+sc*(0.2+0.6*hashY(col)), gy-3*D, 2*D, 3*D); } }
  ctx.save(); ctx.globalCompositeOperation='lighter';
  for(let i=0;i<6;i++){ const px=(((i*151*D - cam*1.0)%W)+W)%W, tw=0.5+0.5*Math.sin(t*4+i*1.7);
    ctx.fillStyle='rgba(255,255,220,'+(0.10+0.18*tw).toFixed(3)+')'; ctx.fillRect(px, gy+capH*0.4, 30*D, 1.5*D); }
  ctx.restore();
  ctx.restore();
}
function scapeElysium(biome,horizon,t){
  const D=DPR;
  const SUNX=W*0.70-cam*0.02, SUNY=horizon*0.46;
  { ctx.save();
    const tg=ctx.createLinearGradient(0,0,0,horizon);
    tg.addColorStop(0, silhouetteColor(biome.sky[0],0.45,0.68));
    tg.addColorStop(0.45,'rgba(0,0,0,0)');
    ctx.fillStyle=tg; ctx.fillRect(0,0,W,horizon);
    ctx.globalCompositeOperation='lighter';
    const bg=ctx.createRadialGradient(SUNX,SUNY,0,SUNX,SUNY,horizon*1.15);
    bg.addColorStop(0,'rgba(255,214,138,0.40)');
    bg.addColorStop(0.4,'rgba(255,168,96,0.20)');
    bg.addColorStop(1,'rgba(255,150,90,0)');
    ctx.fillStyle=bg; ctx.fillRect(0,0,W,horizon);
    ctx.restore(); }
  { ctx.save(); ctx.globalCompositeOperation='lighter';
    const og=ctx.createRadialGradient(SUNX,SUNY,0,SUNX,SUNY,horizon*0.62);
    og.addColorStop(0,'rgba(255,236,180,0.55)');
    og.addColorStop(0.5,'rgba(255,190,110,0.22)');
    og.addColorStop(1,'rgba(255,170,90,0)');
    ctx.fillStyle=og; ctx.beginPath(); ctx.arc(SUNX,SUNY,horizon*0.62,0,6.283); ctx.fill();
    const rays=14, len=horizon*1.9;
    for(let i=0;i<rays;i++){
      const a=(i/rays)*6.283 + t*0.03 + Math.sin(t*0.25+i*1.3)*0.02;
      const half=0.010+0.010*hashY(i*3);
      const al=0.04+0.06*(0.5+0.5*Math.sin(t*0.6+i*1.7));
      ctx.fillStyle='rgba(255,224,150,'+al.toFixed(3)+')';
      ctx.beginPath(); ctx.moveTo(SUNX,SUNY);
      ctx.lineTo(SUNX+Math.cos(a-half)*len,SUNY+Math.sin(a-half)*len);
      ctx.lineTo(SUNX+Math.cos(a+half)*len,SUNY+Math.sin(a+half)*len);
      ctx.closePath(); ctx.fill(); }
    for(let r=0;r<3;r++){ const rr0=horizon*(0.16+r*0.07), pw=0.5+0.5*Math.sin(t*0.5+r);
      ctx.strokeStyle='rgba(255,240,190,'+(0.09+0.06*pw).toFixed(3)+')';
      ctx.lineWidth=(2+r)*D; ctx.beginPath(); ctx.arc(SUNX,SUNY,rr0,0,6.283); ctx.stroke(); }
    const cg=ctx.createRadialGradient(SUNX,SUNY,0,SUNX,SUNY,horizon*0.14);
    cg.addColorStop(0,'rgba(255,252,240,0.95)');
    cg.addColorStop(0.6,'rgba(255,232,168,0.60)');
    cg.addColorStop(1,'rgba(255,210,130,0)');
    ctx.fillStyle=cg; ctx.beginPath(); ctx.arc(SUNX,SUNY,horizon*0.14,0,6.283); ctx.fill();
    ctx.restore(); }
  function cloudPath(x,cy,cw,ch,lobes,seed){
    ctx.beginPath(); ctx.moveTo(x-cw,cy+ch*0.5);
    for(let l=0;l<lobes;l++){
      const lx=x-cw+(l/(lobes-1))*cw*2;
      const lr=ch*(0.6+0.5*hashS(seed+l))*Math.sin((l+0.5)/lobes*Math.PI)+ch*0.35;
      ctx.arc(lx,cy-lr*0.4,Math.max(1,lr),Math.PI,0,false); }
    ctx.lineTo(x+cw,cy+ch*0.5); ctx.closePath(); }
  function backCloud(x,cy,cw,ch,lobes,seed,darkA,rimA){
    cloudPath(x,cy,cw,ch,lobes,seed);
    ctx.fillStyle=silhouetteColor(hslHex(24,0.72,0.34),darkA,1); ctx.fill();
    ctx.save(); ctx.globalCompositeOperation='lighter';
    cloudPath(x,cy,cw,ch,lobes,seed);
    ctx.strokeStyle='rgba(255,228,150,'+rimA+')'; ctx.lineWidth=2.4*D; ctx.lineJoin='round'; ctx.stroke();
    ctx.restore(); }
  { const gap=430*D, f=0.05, drift=t*4*D, shift=cam*f+drift;
    const i0=Math.floor((shift-gap)/gap), i1=Math.ceil((shift+W+gap)/gap);
    for(let k=i0;k<=i1;k++){ if(hashY(k*1.3)<0.42) continue;
      const x=k*gap-shift, cy=horizon*(0.20+0.06*hashS(k))+Math.sin(t*0.18+k)*4*D;
      const cw=gap*(0.40+0.08*hashS(k)), ch=cw*(0.18+0.05*hashY(k));
      backCloud(x,cy,cw,ch,4+Math.floor(hashY(k*3)*3),k*3.1,0.34,0.34); } }
  { const gap=520*D, f=0.20, shift=cam*f;
    const i0=Math.floor((shift-gap)/gap), i1=Math.ceil((shift+W+gap)/gap);
    const dark=silhouetteColor(hslHex(26,0.60,0.30),0.94,1);
    for(let k=i0;k<=i1;k++){ if(hashS(k*2+5)<0.5) continue;
      const x=k*gap-shift, sc=0.8+0.5*hashY(k*3);
      const span=gap*0.30*sc, th=horizon*0.26*sc;
      const baseY=horizon*(0.50+0.14*hashS(k*7))+Math.sin(t*0.15+k)*3*D, top=baseY-th;
      ctx.save(); ctx.globalCompositeOperation='lighter';
      const hg=ctx.createRadialGradient(x,baseY-th*0.5,0,x,baseY-th*0.5,span*1.5);
      hg.addColorStop(0,'rgba(255,222,150,0.28)'); hg.addColorStop(1,'rgba(255,222,150,0)');
      ctx.fillStyle=hg; ctx.beginPath(); ctx.arc(x,baseY-th*0.5,span*1.5,0,6.283); ctx.fill();
      ctx.restore();
      const cols=5, colW=span/(cols*1.6);
      ctx.fillStyle=dark; rr(ctx,x-span*0.5-6*D,baseY-th*0.14,span+12*D,th*0.14,3*D); ctx.fill();
      for(let c=0;c<cols;c++){ const cx=x-span*0.5+(c+0.5)/cols*span;
        ctx.fillStyle=dark; ctx.fillRect(cx-colW*0.5,top+th*0.20,colW,th*0.66); }
      ctx.fillStyle=dark; ctx.fillRect(x-span*0.5-4*D,top+th*0.08,span+8*D,th*0.12);
      ctx.fillStyle=dark; ctx.beginPath(); ctx.moveTo(x-span*0.5-6*D,top+th*0.10);
      ctx.lineTo(x,top-th*0.16); ctx.lineTo(x+span*0.5+6*D,top+th*0.10); ctx.closePath(); ctx.fill();
      ctx.save(); ctx.globalCompositeOperation='lighter';
      ctx.strokeStyle='rgba(255,232,160,0.80)'; ctx.lineWidth=2*D; ctx.lineJoin='round';
      ctx.beginPath(); ctx.moveTo(x,top-th*0.16); ctx.lineTo(x+span*0.5+6*D,top+th*0.10); ctx.stroke();
      const rcx=x-span*0.5+(cols-0.5)/cols*span;
      ctx.beginPath(); ctx.moveTo(rcx+colW*0.5,top+th*0.20); ctx.lineTo(rcx+colW*0.5,top+th*0.86); ctx.stroke();
      ctx.restore(); } }
  { const gap=470*D, f=0.10, drift=t*7*D, shift=cam*f+drift;
    const i0=Math.floor((shift-gap)/gap), i1=Math.ceil((shift+W+gap)/gap);
    for(let k=i0;k<=i1;k++){ if(hashS(k*1.7+9)<0.42) continue;
      const x=k*gap-shift, cy=horizon*(0.32+0.10*hashY(k))+Math.sin(t*0.20+k)*4*D;
      const cw=gap*(0.42+0.08*hashS(k)), ch=cw*(0.20+0.05*hashY(k));
      backCloud(x,cy,cw,ch,4+Math.floor(hashS(k*3)*3),k*2.7,0.42,0.42); } }
  { const gap=560*D, f=0.16, drift=t*4*D, shift=cam*f+drift;
    const i0=Math.floor((shift-gap)/gap), i1=Math.ceil((shift+W+gap)/gap);
    for(let k=i0;k<=i1;k++){ if(hashS(k*2.3+7)<0.5) continue;
      const x=k*gap-shift, y=horizon*(0.30+0.28*hashY(k*5))+Math.sin(t*0.3+k)*6*D, s=(34+24*hashY(k*3))*D;
      ctx.save(); ctx.globalCompositeOperation='lighter';
      const hg=ctx.createRadialGradient(x,y,0,x,y,s*2.4);
      hg.addColorStop(0,'rgba(255,232,160,0.30)'); hg.addColorStop(1,'rgba(255,232,160,0)');
      ctx.fillStyle=hg; ctx.beginPath(); ctx.arc(x,y,s*2.4,0,6.283); ctx.fill(); ctx.restore();
      ctx.fillStyle=hslHex(24,0.55,0.24); ctx.beginPath(); ctx.moveTo(x-s,y);
      ctx.quadraticCurveTo(x-s*0.4,y+s*1.4,x,y+s*1.7);
      ctx.quadraticCurveTo(x+s*0.4,y+s*1.4,x+s,y); ctx.closePath(); ctx.fill();
      ctx.fillStyle=hslHex(112,0.55,0.40); ctx.beginPath(); ctx.ellipse(x,y,s,s*0.42,0,0,6.283); ctx.fill();
      ctx.fillStyle=hslHex(108,0.62,0.56); ctx.beginPath(); ctx.ellipse(x,y-s*0.12,s*0.86,s*0.30,0,Math.PI,2*Math.PI); ctx.fill();
      ctx.strokeStyle=hslHex(26,0.55,0.20); ctx.lineWidth=3*D; ctx.lineCap='round';
      ctx.beginPath(); ctx.moveTo(x+s*0.3,y-s*0.1); ctx.lineTo(x+s*0.3,y-s*0.5); ctx.stroke();
      ctx.fillStyle=hslHex(120,0.55,0.42); ctx.beginPath(); ctx.arc(x+s*0.3,y-s*0.62,s*0.30,0,6.283); ctx.fill();
      ctx.strokeStyle='rgba(190,230,255,0.55)'; ctx.lineWidth=2.5*D;
      ctx.beginPath(); ctx.moveTo(x-s*0.25,y+s*0.1); ctx.lineTo(x-s*0.25,y+s*1.9); ctx.stroke();
      ctx.save(); ctx.globalCompositeOperation='lighter';
      ctx.strokeStyle='rgba(255,230,158,0.75)'; ctx.lineWidth=2*D; ctx.lineCap='round';
      ctx.beginPath(); ctx.moveTo(x,y+s*1.7); ctx.quadraticCurveTo(x+s*0.4,y+s*1.4,x+s,y); ctx.stroke();
      ctx.beginPath(); ctx.ellipse(x,y-s*0.12,s*0.80,s*0.26,0,Math.PI,2*Math.PI); ctx.stroke();
      ctx.restore(); } }
  { const gap=340*D, f=0.42, drift=t*13*D, shift=cam*f+drift;
    const i0=Math.floor((shift-gap)/gap), i1=Math.ceil((shift+W+gap)/gap);
    ctx.save(); ctx.globalCompositeOperation='lighter';
    for(let k=i0;k<=i1;k++){ if(hashS(k*1.9+2)<0.34) continue;
      const x=k*gap-shift, cy=horizon*0.86+Math.sin(t*0.3+k)*5*D;
      const cw=gap*(0.46+0.08*hashY(k)), ch=cw*(0.22+0.06*hashS(k));
      cloudPath(x,cy,cw,ch,4+Math.floor(hashS(k*3)*3),k*1.9);
      const cgd=ctx.createLinearGradient(0,cy-ch,0,cy+ch*0.6);
      cgd.addColorStop(0,'rgba(255,230,158,0.30)'); cgd.addColorStop(1,'rgba(255,190,110,0.06)');
      ctx.fillStyle=cgd; ctx.fill(); }
    ctx.restore(); }
  { for(let i=0;i<5;i++){ const span=W+80*D, spd=8+5*hashY(i);
      const bx=(((hashS(i*5)*W*1.3-cam*0.18-t*spd*D)%span)+span)%span-40*D;
      const by=horizon*(0.18+0.30*hashY(i*9)); const s=(4+2*hashS(i))*D, flap=Math.sin(t*5+i)*s*0.5;
      ctx.strokeStyle='rgba(70,52,40,0.6)'; ctx.lineWidth=2*D; ctx.lineCap='round';
      ctx.beginPath(); ctx.moveTo(bx-s,by); ctx.quadraticCurveTo(bx-s*0.4,by-flap-s*0.4,bx,by);
      ctx.quadraticCurveTo(bx+s*0.4,by-flap-s*0.4,bx+s,by); ctx.stroke(); } }
  { ctx.save(); ctx.globalCompositeOperation='lighter';
    for(let i=0;i<24;i++){ const f=0.30+0.16*hashY(i), span=W+60*D;
      const px=(((hashS(i)*W*1.4+i*97*D)-cam*f)%span+span)%span-30*D;
      const py=horizon*(0.14+0.72*hashY(i*3))+Math.sin(t*0.6+i)*10*D;
      const tw=0.4+0.6*(0.5+0.5*Math.sin(t*1.5+i*2.1));
      if(i%5===0){ const fy=py+Math.cos(t*0.5+i)*6*D, s=(3+1.5*hashY(i*7))*D;
        ctx.fillStyle='rgba(255,244,196,'+(0.20+0.30*tw).toFixed(3)+')';
        ctx.beginPath(); ctx.ellipse(px,fy,s*0.45,s*1.3,Math.sin(t*0.7+i)*0.5,0,6.283); ctx.fill(); }
      else { ctx.fillStyle='rgba(255,230,150,'+(0.18+0.42*tw).toFixed(3)+')';
        ctx.beginPath(); ctx.arc(px,py,(1.3+1.2*hashY(i*7))*D,0,6.283); ctx.fill(); } }
    ctx.restore(); }
}
function drawFloorElysium(biome,t){
  const D=DPR, gy=GROUND(), gh=H-gy;
  let g=ctx.createLinearGradient(0,gy,0,H);
  g.addColorStop(0, hslHex(40,0.60,0.60));
  g.addColorStop(0.5, hslHex(34,0.62,0.46));
  g.addColorStop(1, hslHex(28,0.60,0.28));
  ctx.fillStyle=g; ctx.fillRect(0,gy,W,gh);
  ctx.save(); ctx.beginPath(); ctx.rect(0,gy,W,gh); ctx.clip();
  ctx.save(); ctx.globalCompositeOperation='lighter';
  const rg=ctx.createRadialGradient(W*0.70,gy,0,W*0.70,gy,Math.max(1,gh*2.4));
  rg.addColorStop(0,'rgba(255,224,150,0.50)');
  rg.addColorStop(0.5,'rgba(255,190,110,0.16)');
  rg.addColorStop(1,'rgba(255,180,100,0)');
  ctx.fillStyle=rg; ctx.fillRect(0,gy,W,gh); ctx.restore();
  const tile=86*D, off=((cam)%tile+tile)%tile;
  for(let x=-off,i=0;x<W+tile;x+=tile,i++){ const col=Math.round((cam-off)/tile)+i;
    ctx.fillStyle=(col&1)?'rgba(255,238,196,0.14)':'rgba(150,96,40,0.16)';
    ctx.fillRect(x,gy,tile,gh);
    ctx.strokeStyle='rgba(120,78,36,0.30)'; ctx.lineWidth=1.5*D;
    ctx.beginPath(); ctx.moveTo(x,gy); ctx.lineTo(x,H); ctx.stroke();
    ctx.strokeStyle='rgba(255,236,196,0.14)'; const vx=x+tile*(0.3+0.4*hashS(col));
    ctx.beginPath(); ctx.moveTo(vx,gy); ctx.lineTo(vx+(hashY(col)-0.5)*30*D,H); ctx.stroke(); }
  const cg=70*D, co=((cam)%cg+cg)%cg; ctx.fillStyle='rgba(255,236,190,0.85)';
  for(let x=-co-cg,i=0;x<W+cg;x+=cg,i++){ const col=Math.round((cam-co)/cg)+i;
    const r=cg*(0.5+0.18*hashY(col)); ctx.beginPath(); ctx.arc(x+cg*0.5,gy+2*D,r,Math.PI,2*Math.PI,false); ctx.fill(); }
  ctx.save(); ctx.globalCompositeOperation='lighter';
  const sg=ctx.createLinearGradient(0,gy-6*D,0,gy+10*D);
  sg.addColorStop(0,'rgba(255,240,190,0)'); sg.addColorStop(0.5,'rgba(255,238,180,0.80)'); sg.addColorStop(1,'rgba(255,238,180,0)');
  ctx.fillStyle=sg; ctx.fillRect(0,gy-6*D,W,16*D); ctx.restore();
  ctx.fillStyle=getAccent(0.6); ctx.fillRect(0,gy,W,2.5*D);
  for(let i=0;i<12;i++){ const px=(((i*151*D-cam)%W)+W)%W, py=gy+gh*(0.25+0.6*hashY(i)), tw=0.5+0.5*Math.sin(t*3+i*1.7);
    ctx.fillStyle='rgba(255,244,210,'+(0.20+0.50*tw).toFixed(3)+')';
    ctx.beginPath(); ctx.arc(px,py,2.2*D,0,6.283); ctx.fill(); }
  ctx.restore();
}
// CRIMSON DAWN scenery: ominous red clouds drifting on a dark crimson sky + a blood moon. Inspired by the
// Akatsuki red-cloud motif but with original lumpy shapes (not the trademarked cloud-with-white-outline).
function _redCloud(x,y,s,fill,rim,a){ const lobes=[[0,0,1],[-0.72,0.12,0.7],[0.72,0.12,0.72],[-0.36,-0.3,0.6],[0.42,-0.26,0.62],[1.18,0.18,0.5],[-1.18,0.2,0.48]];
  ctx.save(); ctx.globalAlpha=a; ctx.fillStyle=rim; for(const L of lobes){ ctx.beginPath(); ctx.arc(x+L[0]*s,y+L[1]*s,L[2]*s*1.14,0,6.283); ctx.fill(); }
  ctx.fillStyle=fill; for(const L of lobes){ ctx.beginPath(); ctx.arc(x+L[0]*s,y+L[1]*s,L[2]*s,0,6.283); ctx.fill(); } ctx.restore(); }
function scapeRedClouds(biome,horizon,t){
  const mx=W*0.74-cam*0.02, my=horizon*0.30; // blood moon
  ctx.save(); ctx.globalCompositeOperation='lighter'; const mg=ctx.createRadialGradient(mx,my,0,mx,my,horizon*0.55);
  mg.addColorStop(0,'rgba(255,60,72,0.32)'); mg.addColorStop(1,'rgba(120,10,22,0)'); ctx.fillStyle=mg; ctx.fillRect(0,0,W,horizon); ctx.restore();
  ctx.fillStyle='#7c0e1a'; ctx.beginPath(); ctx.arc(mx,my,horizon*0.13,0,6.283); ctx.fill();
  ctx.fillStyle=biome.sky?biome.sky[2]:'#0a0306'; ctx.beginPath(); ctx.arc(mx+horizon*0.05,my-horizon*0.028,horizon*0.115,0,6.283); ctx.fill(); // crescent shadow
  const layers=[{f:0.06,y:0.18,sz:0.10,n:5,a:0.85,c:'#b0141f',rim:'#360309',spd:6},
                {f:0.15,y:0.32,sz:0.14,n:5,a:0.93,c:'#dc1f30',rim:'#48060f',spd:11},
                {f:0.27,y:0.50,sz:0.18,n:4,a:0.98,c:'#ff2f44',rim:'#5a0a14',spd:18}];
  for(const L of layers){ const span=W*1.7, drift=cam*L.f + t*L.spd*DPR;
    for(let i=0;i<L.n*2;i++){ const baseX=hashS(i*3.1+L.f*97)*span; const x=(((baseX-drift)%span)+span)%span - span*0.3;
      const y=horizon*(L.y+(hashY(i*2.7)-0.5)*0.10)+Math.sin(t*0.4+i)*4*DPR; const s=horizon*L.sz*(0.7+0.6*hashY(i*5));
      _redCloud(x,y,s,L.c,L.rim,L.a); } }
}
// ---- HYPERPURR: chaotic neon rave (cycling color washes + sweeping lasers + pulsing EQ skyline + light motes). Reduced-motion safe; gameplay layer stays readable on top. ----
let _PREF_REDUCE=false; try{ _PREF_REDUCE = !!(window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches); }catch(e){}
function scapeHyperpurr(biome,horizon,t){
  const beat=Math.sin(t*Math.PI*2*2.0), pulse=0.5+0.5*beat, flash=_PREF_REDUCE?0.28:1.0; // ~120BPM; reduced-motion calms the flashing
  const HUES=[330,190,50,140,270,12], hueAt=i=>HUES[((i%HUES.length)+HUES.length)%HUES.length];
  // 1) full-screen cycling club-light color wash (beat-synced, subtle)
  { ctx.save(); ctx.globalCompositeOperation="lighter"; const h=(t*55)%360, a=(0.05+0.10*pulse)*flash;
    ctx.fillStyle="hsla("+h+",100%,55%,"+a+")"; ctx.fillRect(0,0,W,horizon);
    ctx.fillStyle="hsla("+((h+165)%360)+",100%,55%,"+(a*0.7)+")"; ctx.fillRect(0,horizon*0.45,W,horizon*0.6); ctx.restore(); }
  // 2) sweeping laser fan from a glowing pivot
  { const px=W*0.5, py=-horizon*0.12, N=7; ctx.save(); ctx.globalCompositeOperation="lighter";
    for(let i=0;i<N;i++){ const ang=-Math.PI/2+Math.sin(t*0.6+i*0.9)*1.15+(i-N/2)*0.05, len=horizon*2.1, ex=px+Math.cos(ang)*len, ey=py+Math.sin(ang)*len, hu=hueAt(i);
      const g=ctx.createLinearGradient(px,py,ex,ey); g.addColorStop(0,"hsla("+hu+",100%,65%,"+(0.30*flash)+")"); g.addColorStop(1,"hsla("+hu+",100%,65%,0)");
      ctx.strokeStyle=g; ctx.lineWidth=(2+3*pulse)*DPR; ctx.beginPath(); ctx.moveTo(px,py); ctx.lineTo(ex,ey); ctx.stroke(); }
    const dg=ctx.createRadialGradient(px,py,0,px,py,64*DPR); dg.addColorStop(0,"hsla("+((t*80)%360)+",100%,70%,"+(0.5*flash)+")"); dg.addColorStop(1,"hsla(0,0%,100%,0)"); ctx.fillStyle=dg; ctx.beginPath(); ctx.arc(px,py,64*DPR,0,6.283); ctx.fill(); ctx.restore(); }
  // 3) pulsing neon equalizer skyline (bars bounce to the beat)
  { const bw=46*DPR, off=((cam*0.5)%bw+bw)%bw; let bi=0;
    for(let x=-off; x<W+bw; x+=bw, bi++){ const col=Math.round((cam*0.5-off)/bw)+bi, c=hueAt(col), lv=0.28+0.55*(0.5+0.5*Math.sin(t*6+col*0.7)*(0.6+0.4*pulse)), bh=horizon*lv;
      ctx.fillStyle="rgba(8,4,18,0.85)"; ctx.fillRect(x+2*DPR, horizon-bh, bw-4*DPR, bh);
      ctx.save(); ctx.globalCompositeOperation="lighter";
      const bg=ctx.createLinearGradient(0,horizon-bh,0,horizon); bg.addColorStop(0,"hsla("+c+",100%,60%,"+(0.55*flash)+")"); bg.addColorStop(0.3,"hsla("+c+",100%,55%,"+(0.12*flash)+")"); bg.addColorStop(1,"hsla("+c+",100%,55%,0)");
      ctx.fillStyle=bg; ctx.fillRect(x+2*DPR, horizon-bh, bw-4*DPR, bh);
      ctx.fillStyle="hsla("+c+",100%,72%,"+(0.6+0.4*pulse)+")"; ctx.fillRect(x+2*DPR, horizon-bh-3*DPR, bw-4*DPR, 4*DPR); ctx.restore(); } }
  // 4) floating light motes rising
  { ctx.save(); ctx.globalCompositeOperation="lighter";
    for(let i=0;i<34;i++){ const span=W+60*DPR, x=(((i*87*DPR - cam*0.6)%span)+span)%span - 30*DPR, y=horizon-(((t*30*DPR + i*40*DPR)%(horizon*1.05))), c=hueAt(i+Math.floor(t*2)), r=(1.4+1.6*hashY(i*3))*DPR, a=Math.max(0.12,(0.3+0.5*Math.sin(t*3+i))*flash);
      ctx.fillStyle="hsla("+c+",100%,66%,"+a+")"; ctx.beginPath(); ctx.arc(x,y,r,0,6.283); ctx.fill(); } ctx.restore(); }
}
// ---- GOTHAM: brooding noir city. Big near-black skyscrapers, lit windows, blinking antennas, neon rooftop billboards, a sweeping searchlight. ----
function scapeGotham(biome,horizon,t){
  // hazy city glow + bat-signal searchlight
  { ctx.save(); ctx.globalCompositeOperation="lighter"; const gg=ctx.createRadialGradient(W*0.5,horizon,0,W*0.5,horizon,horizon*1.2); gg.addColorStop(0,"rgba(120,150,210,0.10)"); gg.addColorStop(1,"rgba(120,150,210,0)"); ctx.fillStyle=gg; ctx.fillRect(0,0,W,horizon);
    const px=W*0.22-cam*0.02, py=horizon*0.96, ang=-Math.PI/2+Math.sin(t*0.22)*0.5, len=horizon*1.5, sp=0.055;
    ctx.fillStyle="rgba(200,220,255,0.05)"; ctx.beginPath(); ctx.moveTo(px,py); ctx.lineTo(px+Math.cos(ang-sp)*len,py+Math.sin(ang-sp)*len); ctx.lineTo(px+Math.cos(ang+sp)*len,py+Math.sin(ang+sp)*len); ctx.closePath(); ctx.fill(); ctx.restore(); }
  const layers=[{f:0.08,hmin:0.30,hmax:0.55,gap:92,win:false,body:silhouetteColor(biome.sky[1]||"#0f1530",0.7,0.7)},
                {f:0.18,hmin:0.42,hmax:0.78,gap:124,win:true,body:"#0b0d1a"},
                {f:0.34,hmin:0.55,hmax:0.96,gap:154,win:true,body:"#06070f"}];
  for(let li=0; li<layers.length; li++){ const L=layers[li], gap=L.gap*DPR, shift=cam*L.f, i0=Math.floor((shift-gap)/gap), i1=Math.ceil((shift+W+gap)/gap);
    for(let k=i0;k<=i1;k++){ const x=k*gap-shift, w=gap*(0.72+0.2*hashS(k*1.7)), h=horizon*(L.hmin+(L.hmax-L.hmin)*hashY(k*2.3)), top=horizon-h, bx=x-w/2;
      ctx.fillStyle=L.body; ctx.fillRect(bx,top,w,h);
      if(hashS(k*5)>0.7){ ctx.fillRect(x-1.5*DPR, top-h*0.10, 3*DPR, h*0.10); ctx.save(); ctx.globalCompositeOperation="lighter"; ctx.fillStyle="rgba(255,60,60,"+(0.4+0.6*(0.5+0.5*Math.sin(t*4+k)))+")"; ctx.beginPath(); ctx.arc(x,top-h*0.10,2.6*DPR,0,6.283); ctx.fill(); ctx.restore(); } // blinking red antenna light
      if(L.win){ const cols=Math.max(2,Math.floor(w/(12*DPR))), rows=Math.max(3,Math.floor(h/(16*DPR))), cw=w/cols, rh=h/rows;
        for(let c2=0;c2<cols;c2++) for(let r2=0;r2<rows;r2++){ if(hashY(k*13+c2*7+r2*3.1)<0.55) continue;
          const flick=(hashS(k*3+c2+r2)>0.92)?(0.35+0.65*(0.5+0.5*Math.sin(t*9+c2+r2))):1, warm=hashS(k*2+c2*5+r2)>0.32;
          ctx.fillStyle= warm? "rgba(255,212,120,"+(0.55*flick)+")" : "rgba(150,200,255,"+(0.45*flick)+")"; ctx.fillRect(bx+c2*cw+cw*0.22, top+r2*rh+rh*0.20, cw*0.56, rh*0.55); }
        if(li>=2 && hashS(k*7+3)>0.6){ const bbw=w*0.72, bbh=h*0.13, bbx=x-bbw/2, bby=top-bbh-5*DPR, hue=((k*47)%360+t*18)%360;
          ctx.save(); ctx.globalCompositeOperation="lighter"; const bg=ctx.createLinearGradient(bbx,bby,bbx,bby+bbh); bg.addColorStop(0,"hsla("+hue+",100%,60%,0.5)"); bg.addColorStop(1,"hsla("+hue+",100%,50%,0.14)"); ctx.fillStyle=bg; ctx.fillRect(bbx,bby,bbw,bbh); ctx.restore();
          ctx.strokeStyle="hsla("+hue+",100%,72%,"+(0.6+0.4*Math.sin(t*5+k))+")"; ctx.lineWidth=2*DPR; ctx.strokeRect(bbx,bby,bbw,bbh);
          ctx.fillStyle="hsla("+hue+",100%,76%,0.85)"; for(let s=0;s<3;s++) ctx.fillRect(bbx+bbw*0.16+s*bbw*0.25, bby+bbh*0.4, bbw*0.15, bbh*0.2); } } } }
}
function drawFloorStreet(biome,t){ const D=DPR, gy=GROUND(), gh=H-gy;
  let g=ctx.createLinearGradient(0,gy,0,H); g.addColorStop(0,"#0c0e18"); g.addColorStop(1,"#05060d"); ctx.fillStyle=g; ctx.fillRect(0,gy,W,gh); // wet night asphalt
  ctx.save(); ctx.beginPath(); ctx.rect(0,gy,W,gh); ctx.clip();
  ctx.globalCompositeOperation="lighter"; // neon reflections shimmering on the wet road
  for(let i=0;i<16;i++){ const px=(((i*101*D - cam*0.9)%W)+W)%W, hue=((i*53)+t*18)%360, a=0.04+0.06*(0.5+0.5*Math.sin(t*2+i));
    const rg=ctx.createLinearGradient(px,gy,px,H); rg.addColorStop(0,"hsla("+hue+",100%,60%,"+a+")"); rg.addColorStop(1,"hsla("+hue+",100%,60%,0)"); ctx.fillStyle=rg; ctx.fillRect(px-6*D,gy,12*D,gh); }
  ctx.globalCompositeOperation="source-over";
  ctx.fillStyle="rgba(220,200,120,.45)"; const dl=46*D, doff=((cam*1.0)%dl+dl)%dl; for(let x=-doff; x<W; x+=dl) ctx.fillRect(x, gy+gh*0.5, dl*0.4, 3*D); // center-line dashes
  ctx.restore(); }
// ---- VOLCANO: erupting cones with glowing craters + lava streaks + rising embers (serves Emberglide/Sunspurr/Emberfall Catyon/Magmacore; recolors per palette) ----
function scapeVolcano(biome,horizon,t){
  { ctx.save(); ctx.globalCompositeOperation="lighter"; const g=ctx.createLinearGradient(0,horizon*0.5,0,horizon); g.addColorStop(0,getAccent(0)); g.addColorStop(1,getAccent(0.20)); ctx.fillStyle=g; ctx.fillRect(0,horizon*0.5,W,horizon*0.5); ctx.restore(); } // hot horizon haze
  const layers=[{f:0.10,a:0.5,h:0.42,gap:430},{f:0.22,a:0.82,h:0.64,gap:540}];
  for(const L of layers){ const gap=L.gap*DPR, shift=cam*L.f, i0=Math.floor((shift-gap)/gap), i1=Math.ceil((shift+W+gap)/gap);
    for(let k=i0;k<=i1;k++){ const x=k*gap-shift, vh=horizon*L.h*(0.7+0.5*hashY(k*3)), vw=vh*1.25, top=horizon-vh;
      ctx.fillStyle=silhouetteColor(biome.sky[2],L.a,0.5);
      ctx.beginPath(); ctx.moveTo(x-vw,horizon); ctx.lineTo(x-vw*0.18,top); ctx.lineTo(x+vw*0.18,top); ctx.lineTo(x+vw,horizon); ctx.closePath(); ctx.fill();
      ctx.save(); ctx.globalCompositeOperation="lighter"; const cg=ctx.createRadialGradient(x,top,0,x,top,vw*0.42); cg.addColorStop(0,"rgba(255,190,70,"+(0.5+0.3*Math.sin(t*1.5+k))+")"); cg.addColorStop(1,"rgba(255,80,20,0)"); ctx.fillStyle=cg; ctx.beginPath(); ctx.arc(x,top,vw*0.42,0,6.283); ctx.fill(); ctx.restore(); // glowing crater
      ctx.strokeStyle=getAccent(0.5); ctx.lineWidth=2*DPR; ctx.lineCap="round"; for(let s=-1;s<=1;s++){ ctx.beginPath(); ctx.moveTo(x+s*vw*0.10, top+vh*0.10); ctx.lineTo(x+s*vw*0.42, horizon); ctx.stroke(); } } } // lava streaks
  { ctx.save(); ctx.globalCompositeOperation="lighter"; for(let i=0;i<30;i++){ const span=W+60*DPR, x=(((i*73*DPR - cam*0.4)%span)+span)%span, y=horizon-(((t*42*DPR+i*47*DPR)%(horizon*0.92))), a=0.55*(1-(horizon-y)/(horizon*0.92));
    ctx.fillStyle="rgba(255,"+(120+Math.floor(90*hashY(i)))+",40,"+Math.max(0,a)+")"; ctx.beginPath(); ctx.arc(x,y,(1+1.4*hashY(i*3))*DPR,0,6.283); ctx.fill(); } ctx.restore(); } // rising embers
}
// ---- RAIN: low storm clouds + rain-veiled distant ridge + bg drizzle + lightning glow (serves Abyssline/Downpurr/The Hush Below; the fx:'rain' overlay adds foreground rain) ----
function scapeRain(biome,horizon,t){
  const layers=[{f:0.05,y:0.16,a:0.5,sz:0.10,n:5,spd:5},{f:0.12,y:0.30,a:0.72,sz:0.14,n:5,spd:9}];
  for(const L of layers){ const span=W*1.6, drift=cam*L.f+t*L.spd*DPR; ctx.fillStyle=silhouetteColor(biome.sky[2],L.a,0.65);
    for(let i=0;i<L.n*2;i++){ const bx=hashS(i*3.1+L.f*53)*span, x=(((bx-drift)%span)+span)%span-span*0.2, y=horizon*L.y+Math.sin(t*0.3+i)*4*DPR, s=horizon*L.sz*(0.7+0.6*hashY(i*5));
      ctx.beginPath(); ctx.ellipse(x,y,s*1.9,s,0,0,6.283); ctx.fill(); } } // clouds
  { const gap=200*DPR, shift=cam*0.18, i0=Math.floor((shift-gap)/gap), i1=Math.ceil((shift+W+gap)/gap); ctx.fillStyle=silhouetteColor(biome.sky[1],0.55,0.55);
    ctx.beginPath(); ctx.moveTo(i0*gap-shift,horizon); for(let k=i0;k<=i1;k++){ const x=k*gap-shift, h=horizon*(0.18+0.16*hashY(k)); ctx.lineTo(x,horizon-h); ctx.lineTo(x+gap*0.5,horizon-h*0.6); } ctx.lineTo(i1*gap-shift,horizon); ctx.closePath(); ctx.fill(); } // veiled ridge
  { ctx.save(); ctx.globalAlpha=0.16; ctx.strokeStyle=silhouetteColor(biome.sky[0],1,1.7); ctx.lineWidth=1.2*DPR;
    for(let i=0;i<44;i++){ const x=(((i*53*DPR - t*230*DPR)%W)+W)%W, y=((i*37*DPR + t*230*DPR)%horizon); ctx.beginPath(); ctx.moveTo(x,y); ctx.lineTo(x-4*DPR,y+16*DPR); ctx.stroke(); } ctx.restore(); } // bg drizzle
  { const fl=Math.sin(t*0.7)*Math.sin(t*3.3); if(fl>0.86){ ctx.save(); ctx.globalCompositeOperation="lighter"; ctx.fillStyle="rgba(180,200,255,"+((fl-0.86)*1.3)+")"; ctx.fillRect(0,0,W,horizon); ctx.restore(); } } // lightning flash
}
// ---- HOLLYWOOD: Hollywood Hills + sign, tall LA palms, a Melrose boutique row (pink wall + angel-wings murals). Floor = Walk of Fame. ----
function scapeHollywood(biome,horizon,t){
  { const f=0.05, shift=cam*f, ridge=horizon*0.60; ctx.fillStyle=silhouetteColor(biome.sky[2],0.5,0.78); // Hollywood Hills
    ctx.beginPath(); ctx.moveTo(0,horizon); for(let x=0;x<=W;x+=50*DPR){ const y=ridge+Math.sin((x+shift)*0.004)*22*DPR+Math.sin((x+shift)*0.011)*9*DPR; ctx.lineTo(x,y); } ctx.lineTo(W,horizon); ctx.closePath(); ctx.fill();
    const sx=(((W*0.55 - cam*f)%(W*2))+W*2)%(W*2)-W*0.5, sy=ridge-16*DPR; ctx.fillStyle="rgba(248,248,252,0.95)"; ctx.font="800 "+(15*DPR)+"px Arial,Helvetica,sans-serif"; ctx.textAlign="left"; ctx.textBaseline="alphabetic"; ctx.fillText("HOLLYWOOD", sx, sy); } // the sign
  { const gap=270*DPR, f=0.18, shift=cam*f, i0=Math.floor((shift-gap)/gap), i1=Math.ceil((shift+W+gap)/gap); // LA palms
    for(let k=i0;k<=i1;k++){ if(hashS(k*3)<0.42) continue; const x=k*gap-shift, th=horizon*(0.44+0.2*hashY(k*5)), top=horizon-th, hx=x+(hashS(k)-0.5)*22*DPR;
      ctx.strokeStyle=silhouetteColor(biome.sky[1],0.5,0.5); ctx.lineWidth=4*DPR; ctx.lineCap="round"; ctx.beginPath(); ctx.moveTo(x,horizon); ctx.quadraticCurveTo(x+6*DPR,top+th*0.4,hx,top); ctx.stroke();
      const sway=Math.sin(t*0.8+k)*0.08; ctx.lineWidth=3*DPR; for(let fi=0;fi<7;fi++){ const a=(fi/6-0.5)*2.6+sway, fl=th*0.30; ctx.beginPath(); ctx.moveTo(hx,top); ctx.quadraticCurveTo(hx+Math.sin(a)*fl*0.6,top-Math.cos(a)*fl*0.5,hx+Math.sin(a)*fl,top-Math.cos(a)*fl*0.4+10*DPR); ctx.stroke(); } } }
  { const gap=200*DPR, f=0.34, shift=cam*f, i0=Math.floor((shift-gap)/gap), i1=Math.ceil((shift+W+gap)/gap), cols=["#ff4f8a","#ffd24d","#4fc6ff","#7ee08a","#b06bff","#ff8a4f"]; // Melrose storefronts
    for(let k=i0;k<=i1;k++){ const x=k*gap-shift, w=gap*0.96, h=horizon*(0.30+0.12*hashY(k*2)), top=horizon-h, c=cols[((k%cols.length)+cols.length)%cols.length];
      ctx.fillStyle=silhouetteColor(c,0.88,1.05); ctx.fillRect(x,top,w,h);
      ctx.fillStyle=silhouetteColor(c,1,0.65); ctx.fillRect(x,top,w,11*DPR); ctx.fillStyle="rgba(255,255,255,.55)"; for(let s=0;s<Math.floor(w/(16*DPR));s++) if(s&1) ctx.fillRect(x+s*16*DPR,top,8*DPR,11*DPR); // awning
      ctx.fillStyle="rgba(18,22,38,.42)"; ctx.fillRect(x+w*0.12,top+h*0.45,w*0.76,h*0.5); // window
      if(hashS(k*7)>0.7){ ctx.strokeStyle="rgba(255,255,255,.85)"; ctx.lineWidth=2.5*DPR; const mx=x+w*0.5,my=top+h*0.42; for(const sgn of [-1,1]){ ctx.beginPath(); ctx.moveTo(mx,my); for(let fi=0;fi<5;fi++){ const a=fi/4*Math.PI*0.5; ctx.lineTo(mx+sgn*Math.cos(a)*w*0.18*(1-fi*0.05),my-Math.sin(a)*h*0.22); } ctx.stroke(); } } } } // angel-wings mural
}
function drawFloorWalkOfFame(biome,t){ const D=DPR, gy=GROUND(), gh=H-gy;
  let g=ctx.createLinearGradient(0,gy,0,H); g.addColorStop(0,"#3a3530"); g.addColorStop(1,"#241f1c"); ctx.fillStyle=g; ctx.fillRect(0,gy,W,gh); // charcoal terrazzo
  ctx.save(); ctx.beginPath(); ctx.rect(0,gy,W,gh); ctx.clip();
  for(let i=0;i<60;i++){ const px=(((i*61*D - cam)%W)+W)%W, py=gy+hashY(i*3)*gh; ctx.fillStyle="rgba(200,190,170,"+(0.05+0.06*hashS(i))+")"; ctx.beginPath(); ctx.arc(px,py,1.4*D,0,7); ctx.fill(); } // speckle
  const sp=150*D, off=((cam)%sp+sp)%sp;
  for(let x=-off; x<W+sp; x+=sp){ const cy=gy+gh*0.5, sz=42*D;
    ctx.fillStyle="#c66b7a"; rr(ctx,x-sz/2,cy-sz/2,sz,sz,6*D); ctx.fill(); ctx.strokeStyle="#d9a24a"; ctx.lineWidth=3*D; rr(ctx,x-sz/2,cy-sz/2,sz,sz,6*D); ctx.stroke();
    ctx.fillStyle="#f6d98a"; ctx.beginPath(); for(let s=0;s<10;s++){ const a=-Math.PI/2+s*Math.PI/5, r=(s&1)?sz*0.14:sz*0.30; const px=x+Math.cos(a)*r, py=cy+Math.sin(a)*r; s?ctx.lineTo(px,py):ctx.moveTo(px,py); } ctx.closePath(); ctx.fill(); } // brass-rimmed coral star
  ctx.restore(); }
// ---- NEON TOKYO: Tokyo Tower, dense neon signboards (mock kanji), lit windows, strung red lanterns. Floor = wet neon street. ----
function scapeTokyo(biome,horizon,t){
  { const f=0.08, tx=W*0.7-cam*f, th=horizon*0.78, base=horizon, topY=base-th, w=th*0.34; ctx.strokeStyle=silhouetteColor("#ff5a3c",0.85,1.05); ctx.lineWidth=3*DPR; ctx.lineCap="round"; // Tokyo Tower
    ctx.beginPath(); ctx.moveTo(tx-w/2,base); ctx.lineTo(tx-w*0.10,topY+th*0.15); ctx.moveTo(tx+w/2,base); ctx.lineTo(tx+w*0.10,topY+th*0.15); ctx.stroke();
    for(let s=0;s<6;s++){ const y0=base-(s/6)*th*0.85, y1=base-((s+1)/6)*th*0.85, ww0=w*(1-s/6*0.85), ww1=w*(1-(s+1)/6*0.85); ctx.beginPath(); ctx.moveTo(tx-ww0/2,y0); ctx.lineTo(tx+ww1/2,y1); ctx.moveTo(tx+ww0/2,y0); ctx.lineTo(tx-ww1/2,y1); ctx.moveTo(tx-ww1/2,y1); ctx.lineTo(tx+ww1/2,y1); ctx.stroke(); }
    ctx.beginPath(); ctx.moveTo(tx,topY+th*0.15); ctx.lineTo(tx,topY); ctx.stroke(); ctx.save(); ctx.globalCompositeOperation="lighter"; ctx.fillStyle="rgba(255,80,60,"+(0.4+0.6*(0.5+0.5*Math.sin(t*4)))+")"; ctx.beginPath(); ctx.arc(tx,topY,3*DPR,0,7); ctx.fill(); ctx.restore(); }
  { const gap=120*DPR, f=0.2, shift=cam*f, i0=Math.floor((shift-gap)/gap), i1=Math.ceil((shift+W+gap)/gap), NEON=["#ff2d6b","#27e6ff","#ffe14d","#5cff9e","#b06bff","#ff8a3c"]; // skyscrapers + neon
    for(let k=i0;k<=i1;k++){ const x=k*gap-shift, w=gap*(0.74+0.2*hashS(k*1.7)), h=horizon*(0.4+0.45*hashY(k*2.3)), top=horizon-h, bx=x-w/2;
      ctx.fillStyle=silhouetteColor(biome.sky[2],0.92,0.45); ctx.fillRect(bx,top,w,h);
      if(hashS(k*5)>0.4){ const c=NEON[((k%NEON.length)+NEON.length)%NEON.length], sgx=bx+w*(0.18+0.5*hashY(k*7)), sgw=8*DPR, n=3+Math.floor(hashY(k*3)*4), sgy=top+h*0.10;
        ctx.save(); ctx.globalCompositeOperation="lighter"; for(let s=0;s<n;s++){ const yy=sgy+s*11*DPR, fl=0.5+0.5*Math.sin(t*3+k+s); ctx.fillStyle=silhouetteColor(c,0.4+0.5*fl,1.45); ctx.fillRect(sgx,yy,sgw,8*DPR); } ctx.restore();
        ctx.strokeStyle=silhouetteColor(c,0.6,1.3); ctx.lineWidth=1.5*DPR; ctx.strokeRect(sgx-2*DPR,sgy-2*DPR,sgw+4*DPR,n*11*DPR); } // vertical signboard
      if(hashS(k*9)>0.55){ const c=NEON[((k*3%NEON.length)+NEON.length)%NEON.length]; ctx.save(); ctx.globalCompositeOperation="lighter"; ctx.fillStyle=silhouetteColor(c,0.5+0.3*Math.sin(t*4+k),1.3); ctx.fillRect(bx+w*0.1,top+h*0.5,w*0.8,5*DPR); ctx.restore(); }
      for(let wy=top+10*DPR; wy<horizon; wy+=14*DPR) for(let wx=bx+6*DPR; wx<bx+w-6*DPR; wx+=12*DPR){ if(hashY(k*13+wx*0.07+wy*0.07)<0.62) continue; ctx.fillStyle="rgba(255,220,150,0.5)"; ctx.fillRect(wx,wy,4*DPR,5*DPR); } } }
  { const f=0.4, shift=cam*f, sp=92*DPR, off=((shift)%sp+sp)%sp; for(let x=-off,i=0; x<W+sp; x+=sp,i++){ const ly=horizon*0.12+Math.sin(t*0.6+i)*4*DPR; // red lanterns
    ctx.save(); ctx.globalCompositeOperation="lighter"; ctx.fillStyle="rgba(255,120,80,.4)"; ctx.beginPath(); ctx.arc(x,ly,14*DPR,0,7); ctx.fill(); ctx.restore();
    ctx.fillStyle="#d62828"; ctx.beginPath(); ctx.ellipse(x,ly,9*DPR,12*DPR,0,0,7); ctx.fill(); ctx.fillStyle="rgba(255,180,80,.55)"; ctx.fillRect(x-9*DPR,ly-1*DPR,18*DPR,2*DPR); } }
}
// ---- SENPAI SKIES: Makoto-Shinkai dramatic anime sky. Lens-flare sun, volumetric clouds, distant town, torii gate + telephone poles, sparkles. Floor = grass. ----
function scapeAnime(biome,horizon,t){
  { const sx=W*0.72-cam*0.02, sy=horizon*0.30; ctx.save(); ctx.globalCompositeOperation="lighter"; // lens-flare sun
    let g=ctx.createRadialGradient(sx,sy,0,sx,sy,horizon*0.5); g.addColorStop(0,"rgba(255,250,230,0.9)"); g.addColorStop(0.2,"rgba(255,235,180,0.4)"); g.addColorStop(1,"rgba(255,220,150,0)"); ctx.fillStyle=g; ctx.beginPath(); ctx.arc(sx,sy,horizon*0.5,0,7); ctx.fill();
    ctx.fillStyle="rgba(255,253,245,0.95)"; ctx.beginPath(); ctx.arc(sx,sy,16*DPR,0,7); ctx.fill();
    for(let i=1;i<=4;i++){ const fx=sx+(W*0.5-sx)*(i/5), fy=sy+(horizon*0.7-sy)*(i/5); ctx.fillStyle="rgba(255,230,180,"+(0.12/i)+")"; ctx.beginPath(); ctx.arc(fx,fy,(6+i*4)*DPR,0,7); ctx.fill(); } ctx.restore(); }
  const layers=[{f:0.05,y:0.20,a:0.85,sz:0.13,n:4},{f:0.12,y:0.34,a:0.95,sz:0.17,n:4}]; // dramatic clouds
  for(const L of layers){ const span=W*1.6, drift=cam*L.f+t*3*DPR;
    for(let i=0;i<L.n*2;i++){ const bx=hashS(i*3.1+L.f*53)*span, x=(((bx-drift)%span)+span)%span-span*0.2, y=horizon*L.y+Math.sin(t*0.25+i)*5*DPR, s=horizon*L.sz*(0.7+0.6*hashY(i*5));
      ctx.fillStyle="rgba(255,255,255,"+(L.a*0.9)+")"; for(let p=0;p<4;p++){ const ox=(p-1.5)*s*0.7, oy=Math.sin(p)*s*0.2; ctx.beginPath(); ctx.arc(x+ox,y+oy,s*(0.6+0.2*hashY(i+p)),0,7); ctx.fill(); }
      ctx.save(); ctx.globalCompositeOperation="lighter"; ctx.fillStyle="rgba(255,220,150,0.25)"; ctx.beginPath(); ctx.arc(x+s*0.6,y-s*0.3,s*0.5,0,7); ctx.fill(); ctx.restore(); } }
  { const gap=120*DPR, shift=cam*0.16, i0=Math.floor((shift-gap)/gap), i1=Math.ceil((shift+W+gap)/gap); ctx.fillStyle=silhouetteColor(biome.sky[2],0.5,0.6); for(let k=i0;k<=i1;k++){ const x=k*gap-shift, h=horizon*(0.08+0.10*hashY(k)); ctx.fillRect(x-gap*0.4,horizon-h,gap*0.8,h); } } // distant town
  { const f=0.3, shift=cam*f, tx=(((W*0.4 - shift)%(W*2.2))+W*2.2)%(W*2.2)-W*0.6; // torii gate
    if(tx>-120*DPR && tx<W+120*DPR){ const th=horizon*0.34, top=horizon-th, w=th*0.7; ctx.fillStyle=silhouetteColor("#d83a3a",0.85,0.92);
      ctx.fillRect(tx-w/2-6*DPR,top,9*DPR,th); ctx.fillRect(tx+w/2-3*DPR,top,9*DPR,th); ctx.fillRect(tx-w/2-16*DPR,top-2*DPR,w+32*DPR,9*DPR); ctx.fillRect(tx-w/2-8*DPR,top+14*DPR,w+16*DPR,6*DPR); }
    const sp=300*DPR, off=((shift)%sp+sp)%sp; let prevX=null,prevY=null; // telephone poles + wires
    for(let x=-off; x<W+sp; x+=sp){ const ph=horizon*0.30, top=horizon-ph; ctx.fillStyle=silhouetteColor(biome.sky[2],0.6,0.5); ctx.fillRect(x-2*DPR,top,4*DPR,ph); ctx.fillRect(x-12*DPR,top+8*DPR,24*DPR,3*DPR); ctx.fillRect(x-9*DPR,top+16*DPR,18*DPR,3*DPR);
      if(prevX!==null){ ctx.strokeStyle=silhouetteColor(biome.sky[2],0.6,0.5); ctx.lineWidth=2*DPR; ctx.beginPath(); ctx.moveTo(prevX,prevY); ctx.quadraticCurveTo((prevX+x)/2,top+24*DPR,x,top+10*DPR); ctx.stroke(); } prevX=x; prevY=top+10*DPR; } }
  { ctx.save(); ctx.globalCompositeOperation="lighter"; for(let i=0;i<20;i++){ const x=(((i*97*DPR - cam*0.3)%W)+W)%W, y=hashY(i*3)*horizon*0.7, tw=0.5+0.5*Math.sin(t*3+i*1.7); ctx.fillStyle="rgba(255,255,240,"+(0.3*tw)+")"; ctx.beginPath(); ctx.arc(x,y,1.6*DPR,0,7); ctx.fill(); } ctx.restore(); } // sparkles
}
// ===== END NEW ENVIRONMENTS =====

/* ===== 7 NEW WORLDS (07-01): Ghetto Humble Noname Plu Blurred Private Rad ===== */
function scapeGhetto(biome,horizon,t){ const D=DPR;
  // moon glow high-right
  const mx=W*0.78-cam*0.02, my=horizon*0.32; ctx.save(); ctx.globalCompositeOperation='lighter'; const mg=ctx.createRadialGradient(mx,my,0,mx,my,horizon*0.55); mg.addColorStop(0,'rgba(255,220,180,0.5)'); mg.addColorStop(1,'rgba(255,220,180,0)'); ctx.fillStyle=mg; ctx.beginPath(); ctx.arc(mx,my,horizon*0.55,0,6.283); ctx.fill(); ctx.restore();
  ctx.fillStyle=silhouetteColor(biome.sky[0],0.85,1.15); ctx.beginPath(); ctx.arc(mx,my,horizon*0.09,0,6.283); ctx.fill();
  // FAR tower row (parallax 0.08)
  const far=(f,base,col,mul,lit)=>{ const gap=210*D,shift=cam*f,i0=Math.floor((shift-gap)/gap),i1=Math.ceil((shift+W+gap)/gap);
    for(let k=i0;k<=i1;k++){ const x=k*gap-shift, w=gap*(0.62+0.28*hashS(k)), bh=horizon*base*(0.55+0.7*hashY(k)), by=horizon; ctx.fillStyle=silhouetteColor(col,0.9,mul); ctx.fillRect(x,by-bh,w,bh);
      // windows
      const cols=Math.max(2,Math.floor(w/(14*D))), rows=Math.floor(bh/(20*D)); for(let a=0;a<cols;a++)for(let b=0;b<rows;b++){ const seed=k*97+a*13+b*7; if(hashS(seed)>lit) continue; const fl=hashY(seed*3); const hue=[35,300,140,20][Math.floor(hashY(seed)*4)%4]; const on=0.5+0.5*Math.sin(t*(1+fl*3)+seed); ctx.fillStyle=silhouetteColor(hslHex(hue,0.9,0.6),0.35+0.5*on,1.3); ctx.fillRect(x+4*D+a*(w-6*D)/cols, by-bh+6*D+b*(bh-8*D)/rows, (w-6*D)/cols-3*D, (bh-8*D)/rows-4*D); } } };
  far(0.08,0.5,biome.sky[1],0.85,0.55);
  far(0.2,0.72,biome.sky[2],1.0,0.6);
  // NEAR wall block on right with neon graffiti cat-tag
  const nf=0.34, ns=cam*nf, wallX=W*0.66-ns%(W*1.4), wy=horizon*0.42;
  for(let pass=0;pass<2;pass++){ const wx=wallX+pass*W*1.4; ctx.fillStyle=silhouetteColor(biome.sky[2],0.95,0.7); ctx.fillRect(wx,wy,W*0.5,horizon-wy);
    // graffiti cat tag glow
    const cx=wx+W*0.22, cy=wy+horizon*0.22, s=horizon*0.16, pulse=_PREF_REDUCE?0.85:0.65+0.35*Math.sin(t*2.2);
    ctx.save(); ctx.globalCompositeOperation='lighter'; ctx.strokeStyle=getAccent(0.8*pulse); ctx.lineWidth=3.5*D; ctx.lineCap='round'; ctx.shadowColor=getAccent(0.9); ctx.shadowBlur=18*D;
    ctx.beginPath(); ctx.arc(cx,cy,s*0.55,0.2,6.083); ctx.stroke(); // head
    ctx.beginPath(); ctx.moveTo(cx-s*0.42,cy-s*0.3); ctx.lineTo(cx-s*0.62,cy-s*0.72); ctx.lineTo(cx-s*0.18,cy-s*0.46); ctx.stroke(); // ear L
    ctx.beginPath(); ctx.moveTo(cx+s*0.42,cy-s*0.3); ctx.lineTo(cx+s*0.62,cy-s*0.72); ctx.lineTo(cx+s*0.18,cy-s*0.46); ctx.stroke(); // ear R
    ctx.lineWidth=2.5*D; ctx.beginPath(); ctx.arc(cx-s*0.2,cy-s*0.02,s*0.07,0,6.283); ctx.arc(cx+s*0.2,cy-s*0.02,s*0.07,0,6.283); ctx.stroke(); // eyes
    ctx.beginPath(); ctx.moveTo(cx-s*0.35,cy+s*0.18); ctx.lineTo(cx+s*0.35,cy+s*0.18); ctx.moveTo(cx-s*0.32,cy+s*0.28); ctx.lineTo(cx+s*0.32,cy+s*0.28); ctx.stroke(); // whiskers
    // drips
    for(let d=0;d<3;d++){ const dx=cx-s*0.4+d*s*0.4, dl=s*(0.2+0.5*hashY(d+pass*5)); ctx.lineWidth=2*D; ctx.beginPath(); ctx.moveTo(dx,cy+s*0.55); ctx.lineTo(dx,cy+s*0.55+dl); ctx.stroke(); }
    ctx.restore();
    // flickering streetlamp with warm pool
    const lx=wx+W*0.05, ly=wy+horizon*0.05, flick=_PREF_REDUCE?1:0.7+0.3*Math.sin(t*13+pass); ctx.save(); ctx.globalCompositeOperation='lighter'; const lg=ctx.createRadialGradient(lx,ly,0,lx,ly,horizon*0.28); lg.addColorStop(0,`rgba(255,170,80,${0.5*flick})`); lg.addColorStop(1,'rgba(255,170,80,0)'); ctx.fillStyle=lg; ctx.beginPath(); ctx.arc(lx,ly,horizon*0.28,0,6.283); ctx.fill(); ctx.restore(); }
  // chain-link fence hint (near, faint) + steam wisps
  ctx.save(); ctx.globalCompositeOperation='lighter';
  for(let s=0;s<5;s++){ const sx=((s*263*D - cam*0.28)%(W+200*D)+W+200*D)%(W+200*D)-100*D, wob=Math.sin(t*1.1+s*2)*8*D; const sg=ctx.createLinearGradient(sx,horizon,sx,horizon*0.55); sg.addColorStop(0,'rgba(200,210,230,0.16)'); sg.addColorStop(1,'rgba(200,210,230,0)'); ctx.fillStyle=sg; ctx.beginPath(); ctx.moveTo(sx-9*D,horizon); ctx.quadraticCurveTo(sx+wob,horizon*0.78,sx-6*D,horizon*0.55); ctx.quadraticCurveTo(sx-wob,horizon*0.78,sx+9*D,horizon); ctx.closePath(); ctx.fill(); }
  ctx.restore(); }

function drawFloorGhetto(biome,t){ const D=DPR, gy=GROUND(); let g=ctx.createLinearGradient(0,gy,0,H); g.addColorStop(0,silhouetteColor(biome.sky[2],1,0.55)); g.addColorStop(1,hslHex(220,0.15,0.08)); ctx.fillStyle=g; ctx.fillRect(0,gy,W,H-gy);
  ctx.save(); ctx.beginPath(); ctx.rect(0,gy,W,H-gy); ctx.clip();
  // road-paint dashes
  const dy=gy+(H-gy)*0.42, gap=90*D,shift=cam*0.45,i0=Math.floor((shift-gap)/gap),i1=Math.ceil((shift+W+gap)/gap); ctx.fillStyle='rgba(230,210,120,0.28)'; for(let k=i0;k<=i1;k++){ const x=k*gap-shift; ctx.fillRect(x,dy,gap*0.45,5*D); }
  // cracks
  ctx.strokeStyle='rgba(0,0,0,0.35)'; ctx.lineWidth=1.5*D; for(let c=0;c<7;c++){ const cx=((c*181*D-cam*0.45)%(W+120*D)+W+120*D)%(W+120*D)-60*D, cyy=gy+(H-gy)*(0.2+0.6*hashY(c)); ctx.beginPath(); ctx.moveTo(cx,cyy); ctx.lineTo(cx+30*D*hashS(c),cyy+18*D); ctx.lineTo(cx+14*D,cyy+40*D); ctx.stroke(); }
  // neon spray splatters
  ctx.save(); ctx.globalCompositeOperation='lighter'; for(let s=0;s<2;s++){ const px=((s*640*D-cam*0.45)%(W+300*D)+W+300*D)%(W+300*D)-150*D, py=gy+(H-gy)*(0.5+0.3*s), col=s?getAccent(0.5):'rgba(120,255,140,0.4)'; const sg=ctx.createRadialGradient(px,py,0,px,py,60*D); sg.addColorStop(0,col); sg.addColorStop(1,'rgba(0,0,0,0)'); ctx.fillStyle=sg; ctx.beginPath(); ctx.arc(px,py,60*D,0,6.283); ctx.fill(); }
  ctx.restore(); ctx.restore(); }
function scapeHumble(biome,horizon,t){
  const hy=horizon, cx=W*0.52, ac=biome.ac||getAccent(0);
  // ---- BIG glowing sun/moon halo behind the cat mountain ----
  const sunX=cx-cam*0.04, sunY=hy*0.62, sunR=Math.min(W,H)*0.20;
  ctx.save();ctx.globalCompositeOperation='lighter';
  const halo=ctx.createRadialGradient(sunX,sunY,sunR*0.2,sunX,sunY,sunR*2.4);
  halo.addColorStop(0,silhouetteColor(ac,0.9,1.6));halo.addColorStop(0.35,silhouetteColor('#ffcf6e',0.35,1));halo.addColorStop(1,'rgba(0,0,0,0)');
  ctx.fillStyle=halo;ctx.fillRect(0,0,W,hy*1.6);
  ctx.fillStyle=silhouetteColor('#fff2c8',0.95,1.2);ctx.beginPath();ctx.arc(sunX,sunY,sunR,0,7);ctx.fill();
  // ---- god-rays ----
  for(let r=0;r<10;r++){const a=r/10*Math.PI*2+t*0.05,len=sunR*3.2,wob=0.05+0.03*Math.sin(t*0.7+r);
    ctx.globalAlpha=0.06+0.04*Math.sin(t*0.9+r*1.7);ctx.fillStyle=silhouetteColor('#ffe6a0',0.5,1);
    ctx.beginPath();ctx.moveTo(sunX,sunY);ctx.lineTo(sunX+Math.cos(a-wob)*len,sunY+Math.sin(a-wob)*len);ctx.lineTo(sunX+Math.cos(a+wob)*len,sunY+Math.sin(a+wob)*len);ctx.fill();}
  ctx.restore();
  // ---- far misty foothills (parallax) ----
  for(let L=0;L<2;L++){const f=0.08+L*0.06,shift=cam*f,gap=520*DPR,base=hy*(0.78+L*0.09),amp=hy*(0.14-L*0.04);
    const i0=Math.floor((shift-gap)/gap),i1=Math.ceil((shift+W+gap)/gap);
    ctx.fillStyle=silhouetteColor(biome.sky[1],0.5-L*0.12,0.7+L*0.3);
    ctx.beginPath();ctx.moveTo(-100,hy*1.3);
    for(let k=i0;k<=i1;k++){const idx=((k%97)+97)%97,x=k*gap-shift,pk=base-amp*(0.5+0.5*hashY(idx));
      ctx.quadraticCurveTo(x-gap*0.5,pk,x,pk+amp*0.3);ctx.quadraticCurveTo(x+gap*0.4,base+amp*0.4,x+gap,pk+10*DPR);}
    ctx.lineTo(W+200,hy*1.3);ctx.closePath();ctx.fill();}
  // ---- HERO: sleeping cat-shaped mountain ----
  const br=1+0.012*Math.sin(t*0.55); // breathing
  ctx.save();ctx.translate(cx-cam*0.05,hy);ctx.scale(1,br);
  const mw=Math.min(W,H)*0.62,gy=(1.3-hy)*0; const dark=silhouetteColor(biome.sky[2]||'#241a3a',1,0.55);
  ctx.fillStyle=dark;ctx.beginPath();ctx.moveTo(-mw,0);
  ctx.quadraticCurveTo(-mw*0.8,-hy*0.30,-mw*0.62,-hy*0.34); // tail base
  ctx.quadraticCurveTo(-mw*0.5,-hy*0.55,-mw*0.36,-hy*0.30); // curled tail dip
  ctx.quadraticCurveTo(-mw*0.20,-hy*0.62,0,-hy*0.66);       // body hump
  ctx.quadraticCurveTo(mw*0.18,-hy*0.70,mw*0.30,-hy*0.55);  // toward head
  ctx.lineTo(mw*0.33,-hy*0.86);ctx.lineTo(mw*0.42,-hy*0.56);// EAR 1
  ctx.lineTo(mw*0.52,-hy*0.86);ctx.lineTo(mw*0.60,-hy*0.50);// EAR 2
  ctx.quadraticCurveTo(mw*0.78,-hy*0.30,mw,-hy*0.06);       // head/nose slope down
  ctx.lineTo(mw,0);ctx.closePath();ctx.fill();
  // shading rim-light on cat back
  ctx.save();ctx.globalCompositeOperation='lighter';
  ctx.strokeStyle=silhouetteColor(ac,0.5,1.5);ctx.lineWidth=3*DPR;
  ctx.beginPath();ctx.moveTo(-mw*0.20,-hy*0.62);ctx.quadraticCurveTo(mw*0.05,-hy*0.70,mw*0.30,-hy*0.56);ctx.stroke();ctx.restore();
  // closed eye + nose hint
  ctx.strokeStyle=silhouetteColor('#000',0.4,1);ctx.lineWidth=3*DPR;ctx.beginPath();
  ctx.arc(mw*0.70,-hy*0.24,mw*0.05,0.2,1.0);ctx.stroke();
  ctx.fillStyle=silhouetteColor('#000',0.35,1);ctx.beginPath();ctx.arc(mw*0.90,-hy*0.13,4*DPR,0,7);ctx.fill();
  ctx.restore();
  // ---- floating cliff island + torii shrine ----
  const ix=cx+W*0.30-cam*0.22, iy=hy*0.52+Math.sin(t*0.6)*4*DPR;
  ctx.fillStyle=silhouetteColor('#1a1330',0.9,0.7);ctx.beginPath();
  ctx.moveTo(ix-34*DPR,iy);ctx.quadraticCurveTo(ix,iy+8*DPR,ix+34*DPR,iy);ctx.lineTo(ix+14*DPR,iy+30*DPR);ctx.lineTo(ix-16*DPR,iy+26*DPR);ctx.closePath();ctx.fill();
  ctx.save();ctx.globalCompositeOperation='lighter';ctx.strokeStyle=silhouetteColor(ac,0.9,1.7);ctx.lineWidth=3.5*DPR;
  ctx.beginPath();ctx.moveTo(ix-16*DPR,iy-2*DPR);ctx.lineTo(ix-16*DPR,iy-26*DPR);ctx.moveTo(ix+16*DPR,iy-2*DPR);ctx.lineTo(ix+16*DPR,iy-26*DPR);
  ctx.moveTo(ix-24*DPR,iy-24*DPR);ctx.lineTo(ix+24*DPR,iy-24*DPR);ctx.moveTo(ix-20*DPR,iy-30*DPR);ctx.lineTo(ix+20*DPR,iy-30*DPR);ctx.stroke();ctx.restore();
  // ---- drifting spirit-lanterns ----
  ctx.save();ctx.globalCompositeOperation='lighter';
  for(let n=0;n<14;n++){const sx=((hashS(n)*1.4-0.2)*W - cam*(0.15+hashS(n+40)*0.2))%(W*1.4);
    const px=((sx%W)+W)%W, py=hy*(0.30+hashY(n)*0.55)+Math.sin(t*0.5+n)*8*DPR, rad=(3+hashS(n+7)*4)*DPR;
    const g=ctx.createRadialGradient(px,py,0,px,py,rad*4);g.addColorStop(0,silhouetteColor('#ffd27a',0.95,1.4));g.addColorStop(1,'rgba(0,0,0,0)');
    ctx.fillStyle=g;ctx.beginPath();ctx.arc(px,py,rad*4,0,7);ctx.fill();}
  ctx.restore();
  // ---- birds ----
  ctx.strokeStyle=silhouetteColor('#000',0.5,1);ctx.lineWidth=2*DPR;
  for(let b=0;b<4;b++){const bx=(cx+ (b*140-260)*DPR - cam*0.28 + t*22*DPR)%(W+200)-100, by=hy*(0.24+b*0.05)+Math.sin(t*1.2+b)*6*DPR,ws=(7+b)*DPR,fl=Math.sin(t*3+b)*3*DPR;
    ctx.beginPath();ctx.moveTo(bx-ws,by+fl);ctx.quadraticCurveTo(bx,by-4*DPR,bx+ws,by+fl);ctx.stroke();}
  // ---- sparkles ----
  ctx.save();ctx.globalCompositeOperation='lighter';
  for(let s=0;s<18;s++){const px=(hashS(s)*W - cam*0.3)%W,ax=((px%W)+W)%W,py=hy*hashY(s+3),tw=Math.sin(t*2.5+s*2);
    if(tw>0.3){ctx.globalAlpha=tw*0.8;ctx.fillStyle=silhouetteColor('#ffffff',1,1.5);ctx.beginPath();ctx.arc(ax,py,1.4*DPR,0,7);ctx.fill();}}
  ctx.restore();ctx.globalAlpha=1;
}

function drawFloorHumble(biome,t){
  const gt=GROUND(),ac=biome.ac||getAccent(0);
  // misty foothill grass gradient
  const g=ctx.createLinearGradient(0,gt,0,H);g.addColorStop(0,silhouetteColor(biome.sky[2]||'#2a1f4a',1,0.6));g.addColorStop(0.4,'#1c3a2e');g.addColorStop(1,'#0e1f1a');
  ctx.fillStyle=g;ctx.fillRect(0,gt,W,H-gt);
  // soft ground mist band
  ctx.save();ctx.globalCompositeOperation='lighter';
  const m=ctx.createLinearGradient(0,gt-8*DPR,0,gt+40*DPR);m.addColorStop(0,'rgba(0,0,0,0)');m.addColorStop(0.5,silhouetteColor(ac,0.16,1.3));m.addColorStop(1,'rgba(0,0,0,0)');
  ctx.fillStyle=m;ctx.fillRect(0,gt-8*DPR,W,50*DPR);ctx.restore();
  // glowing pebbles / lanterns along ground
  ctx.save();ctx.globalCompositeOperation='lighter';
  const gap=180*DPR,f=0.4,shift=cam*f,i0=Math.floor((shift-gap)/gap),i1=Math.ceil((shift+W+gap)/gap);
  for(let k=i0;k<=i1;k++){const idx=((k%53)+53)%53,x=k*gap-shift+hashY(idx)*80*DPR,y=gt+18*DPR+hashS(idx)*40*DPR,rad=(3+hashS(idx+5)*4)*DPR,pu=0.6+0.4*Math.sin(t*1.5+idx);
    const gg=ctx.createRadialGradient(x,y,0,x,y,rad*5);gg.addColorStop(0,silhouetteColor('#ffcf6e',0.9*pu,1.5));gg.addColorStop(1,'rgba(0,0,0,0)');
    ctx.fillStyle=gg;ctx.beginPath();ctx.arc(x,y,rad*5,0,7);ctx.fill();
    ctx.fillStyle=silhouetteColor('#fff3d0',0.9,1.4);ctx.beginPath();ctx.arc(x,y,rad*0.5,0,7);ctx.fill();}
  ctx.restore();
}
function scapeNoname(biome,horizon,t){
  const cy=Math.min(horizon*0.62,H*0.44); // vanishing / horizon center
  const cx=W*0.5;
  const calm=_PREF_REDUCE;
  const CY=hslHex(190,1,0.55), MG=hslHex(320,1,0.6), PU=hslHex(275,1,0.62), AG=hslHex(140,1,0.55);

  // 1. Deep dark void wash over the base sky (near-black vignette)
  ctx.save();
  const vg=ctx.createRadialGradient(cx,cy,0,cx,cy,W*0.9);
  vg.addColorStop(0,'rgba(6,2,20,0.15)');
  vg.addColorStop(0.55,'rgba(3,1,12,0.55)');
  vg.addColorStop(1,'rgba(0,0,4,0.9)');
  ctx.fillStyle=vg; ctx.fillRect(0,0,W,horizon);
  ctx.restore();

  // 1b. GLITCH datamosh color-bands: thin neon horizontal bars that jump via t
  ctx.save(); ctx.globalCompositeOperation='lighter';
  for(let i=0;i<14;i++){
    const seed=hashS(i*7.3), sy=hashY(i*3.1);
    const flick=calm?0.6:(Math.sin(t*(2+seed*6)+i)*0.5+0.5);
    if(flick<0.25) continue;
    const by=(sy*horizon + (calm?0:Math.sin(t*(0.6+seed)+i*2)*40*DPR))%horizon;
    const bh=(1+seed*3)*DPR;
    const bx=(hashS(i*1.7)*W + (calm?0:(t*(30+seed*120))%W))%W - W*0.2;
    const bw=(0.15+seed*0.5)*W;
    const hue=[190,320,275,140][i%4];
    ctx.fillStyle=hslHex(hue,1,0.55)+Math.round(flick*90+30).toString(16).padStart(2,'0');
    ctx.fillRect(bx,by,bw,bh);
  }
  ctx.restore();

  // 2. Receding NEON perspective corridor toward vanishing point
  ctx.save(); ctx.globalCompositeOperation='lighter'; ctx.lineWidth=1.2*DPR;
  const rays=16;
  for(let i=0;i<=rays;i++){
    const a=i/rays;
    const ex=(a*W); // edge point along bottom of sky band
    const hue=190+Math.sin(t*0.4+i)*40;
    ctx.strokeStyle=hslHex(hue,1,0.55)+'33';
    ctx.beginPath(); ctx.moveTo(cx,cy); ctx.lineTo(ex,horizon); ctx.stroke();
  }
  // receding horizontal rings that scroll toward viewer via t/cam
  const scroll=(t*0.25+cam*0.0006)%1;
  for(let i=0;i<9;i++){
    let d=(i+scroll)/9; // 0 near vanish .. 1 near viewer
    const yy=cy+(horizon-cy)*(d*d);
    const spread=(W*0.5)*(d*d);
    ctx.strokeStyle=CY+Math.round(40+d*120).toString(16).padStart(2,'0');
    ctx.beginPath(); ctx.moveTo(cx-spread,yy); ctx.lineTo(cx+spread,yy); ctx.stroke();
  }
  ctx.restore();

  // 6. Mysterious floating monolith silhouettes in the fog (far parallax)
  {const gap=340*DPR,f=0.08,shift=cam*f,i0=Math.floor((shift-gap)/gap),i1=Math.ceil((shift+W+gap)/gap);
   for(let k=i0;k<=i1;k++){
     const h=hashS(k*5.1); if(h<0.45) continue;
     const x=k*gap-shift, mw=(20+h*30)*DPR, mh=(90+h*130)*DPR;
     const by=horizon-mh*0.15+(calm?0:Math.sin(t*0.5+k)*8*DPR);
     ctx.save();
     ctx.fillStyle='rgba(4,1,16,0.8)';
     rr(ctx,x-mw/2,by-mh,mw,mh,4*DPR); ctx.fill();
     ctx.globalCompositeOperation='lighter'; ctx.strokeStyle=PU+'55'; ctx.lineWidth=1.4*DPR;
     rr(ctx,x-mw/2,by-mh,mw,mh,4*DPR); ctx.stroke();
     ctx.restore();
   }}

  // 3. Floating glowing glyphs with RGB-split chromatic aberration
  const glyphs=['?','???','[ REDACTED ]','?','404','NO?'];
  {const gap=300*DPR,f=0.22,shift=cam*f,i0=Math.floor((shift-gap)/gap),i1=Math.ceil((shift+W+gap)/gap);
   for(let k=i0;k<=i1;k++){
     const h=hashS(k*2.9); if(h<0.35) continue;
     const gi=Math.floor(hashY(k*4.4)*glyphs.length)%glyphs.length;
     const g=glyphs[((gi%glyphs.length)+glyphs.length)%glyphs.length];
     const x=k*gap-shift;
     const y=cy*0.5+hashS(k*6.6)*cy*0.9+(calm?0:Math.sin(t*(0.8+h)+k)*14*DPR);
     const sz=(22+h*30)*DPR;
     const off=(calm?1:(2+Math.sin(t*8+k)*3))*DPR;
     ctx.save(); ctx.globalCompositeOperation='lighter';
     ctx.font=`900 ${sz}px Orbitron`; ctx.textAlign='center'; ctx.textBaseline='middle';
     if(g==='[ REDACTED ]'){
       const bw=sz*4, bh=sz*0.9;
       ctx.globalCompositeOperation='source-over'; ctx.fillStyle='#000';
       rr(ctx,x-bw/2,y-bh/2,bw,bh,3*DPR); ctx.fill();
       ctx.globalCompositeOperation='lighter'; ctx.strokeStyle=MG+'cc'; ctx.lineWidth=1.5*DPR;
       rr(ctx,x-bw/2,y-bh/2,bw,bh,3*DPR); ctx.stroke();
     } else {
       ctx.fillStyle='rgba(255,40,60,0.7)'; ctx.fillText(g,x-off,y);
       ctx.fillStyle='rgba(0,230,255,0.7)'; ctx.fillText(g,x+off,y);
       ctx.fillStyle='rgba(255,255,255,0.9)'; ctx.fillText(g,x,y);
     }
     ctx.restore();
   }}

  // 5. drifting glowing STATIC specks
  ctx.save(); ctx.globalCompositeOperation='lighter';
  for(let i=0;i<40;i++){
    const s=hashS(i*11.7), sx=(hashY(i*2.2)*W+(calm?0:(t*(10+s*40)))%W)%W;
    const sy=(hashS(i*3.9)*horizon+(calm?0:Math.sin(t*2+i)*30*DPR))%horizon;
    const tw=calm?0.5:(Math.sin(t*(4+s*8)+i)*0.5+0.5);
    ctx.fillStyle=[CY,MG,AG,PU][i%4]+Math.round(tw*180).toString(16).padStart(2,'0');
    ctx.fillRect(sx,sy,1.5*DPR,1.5*DPR);
  }
  ctx.restore();

  // 4. HUGE pulsing central sign — flickering broken-neon 'NO NAME' + big '?'
  {const flick=calm?1:( (hashS(Math.floor(t*7))>0.18)? (0.6+Math.sin(t*20)*0.15+0.4):0.12 );
   const pulse=0.85+Math.sin(t*1.6)*0.15;
   ctx.save(); ctx.globalCompositeOperation='lighter';
   ctx.textAlign='center'; ctx.textBaseline='middle';
   // giant ghost ? behind
   ctx.font=`900 ${170*DPR*pulse}px Orbitron`;
   ctx.fillStyle=PU+Math.round(flick*50).toString(16).padStart(2,'0');
   ctx.fillText('?',cx,cy*0.95);
   // NO NAME neon with rgb split
   ctx.font=`900 ${44*DPR}px Orbitron`;
   const yy=cy*0.98, jx=calm?0:Math.sin(t*30)*2*DPR;
   ctx.fillStyle=`rgba(255,40,120,${0.5*flick})`; ctx.fillText('NO NAME',cx-3*DPR+jx,yy);
   ctx.fillStyle=`rgba(0,230,255,${0.5*flick})`; ctx.fillText('NO NAME',cx+3*DPR-jx,yy);
   ctx.fillStyle=`rgba(255,255,255,${0.85*flick})`; ctx.fillText('NO NAME',cx,yy);
   // neon glow halo
   ctx.shadowColor=MG; ctx.shadowBlur=30*DPR*flick;
   ctx.fillStyle=`rgba(255,43,208,${0.15*flick})`; ctx.fillText('NO NAME',cx,yy);
   ctx.restore();
  }

  // 5b. scanlines over the whole sky band
  ctx.save(); ctx.globalCompositeOperation='overlay';
  ctx.fillStyle='rgba(0,0,0,0.18)';
  const sh=3*DPR, off=calm?0:(t*20)%(sh*2);
  for(let y=-off;y<horizon;y+=sh*2){ ctx.fillRect(0,y,W,sh); }
  ctx.restore();
}

function drawFloorNoname(biome,t){
  const gy=GROUND(); const calm=_PREF_REDUCE;
  const cx=W*0.5;
  const CY=hslHex(190,1,0.55), MG=hslHex(320,1,0.6);

  // dark reflective base
  ctx.save();
  const fg=ctx.createLinearGradient(0,gy,0,H);
  fg.addColorStop(0,'rgba(4,1,16,0.85)');
  fg.addColorStop(1,'rgba(0,0,6,1)');
  ctx.fillStyle=fg; ctx.fillRect(0,gy,W,H-gy);
  ctx.restore();

  // faint neon reflection glow at the horizon line
  ctx.save(); ctx.globalCompositeOperation='lighter';
  const rg=ctx.createLinearGradient(0,gy,0,gy+80*DPR);
  rg.addColorStop(0,MG+'44'); rg.addColorStop(1,'rgba(0,0,0,0)');
  ctx.fillStyle=rg; ctx.fillRect(0,gy,W,80*DPR);
  ctx.restore();

  // perspective GRID floor receding to horizon
  ctx.save(); ctx.globalCompositeOperation='lighter'; ctx.lineWidth=1.2*DPR;
  // converging longitudinal lines
  const cols=14;
  for(let i=0;i<=cols;i++){
    const a=(i/cols-0.5); // -0.5..0.5
    const ex=cx+a*W*2.2; // spread at bottom
    ctx.strokeStyle=CY+'2e';
    ctx.beginPath(); ctx.moveTo(cx,gy); ctx.lineTo(ex,H); ctx.stroke();
  }
  // horizontal lines scrolling toward viewer
  const scroll=(t*0.4+cam*0.0008)%1;
  for(let i=0;i<10;i++){
    let d=(i+scroll)/10;
    const yy=gy+(H-gy)*(d*d);
    ctx.strokeStyle=MG+Math.round(40+d*140).toString(16).padStart(2,'0');
    ctx.beginPath(); ctx.moveTo(0,yy); ctx.lineTo(W,yy); ctx.stroke();
  }
  ctx.restore();

  // glitch scanlines + drifting static on the floor
  ctx.save(); ctx.globalCompositeOperation='lighter';
  for(let i=0;i<24;i++){
    const s=hashS(i*9.4), sx=(hashY(i*2.7)*W+(calm?0:(t*(15+s*50)))%W)%W;
    const sy=gy+((hashS(i*4.2)*(H-gy))+(calm?0:Math.sin(t*3+i)*20*DPR))%(H-gy);
    const tw=calm?0.5:(Math.sin(t*(5+s*6)+i)*0.5+0.5);
    ctx.fillStyle=(i%2?CY:MG)+Math.round(tw*150).toString(16).padStart(2,'0');
    ctx.fillRect(sx,sy,1.5*DPR,1.5*DPR);
  }
  // occasional glitch bar
  if(!calm){
    const gb=hashS(Math.floor(t*5));
    if(gb>0.6){ const by=gy+gb*(H-gy); ctx.fillStyle=CY+'55'; ctx.fillRect(0,by,W,2*DPR); }
  }
  ctx.restore();

  // dark scanline overlay
  ctx.save(); ctx.globalCompositeOperation='overlay'; ctx.fillStyle='rgba(0,0,0,0.2)';
  const sh=3*DPR, off=calm?0:(t*24)%(sh*2);
  for(let y=gy-off;y<H;y+=sh*2){ ctx.fillRect(0,y,W,sh); }
  ctx.restore();
}
function scapePlu(biome,horizon,t){
  const H0=horizon, ac=biome.ac||'#c9a', tt=_PREF_REDUCE?t*0.25:t;

  // ---- NEBULA: big soft additive gas clouds filling the whole sky ----
  ctx.save(); ctx.globalCompositeOperation='lighter';
  const neb=[
    {x:0.18,y:0.22,r:0.60,h:280,s:0.75,l:0.42,a:0.55},   // purple
    {x:0.72,y:0.30,r:0.66,h:190,s:0.80,l:0.42,a:0.48},   // teal
    {x:0.50,y:0.14,r:0.52,h:320,s:0.85,l:0.48,a:0.42},   // magenta
    {x:0.86,y:0.62,r:0.48,h:330,s:0.80,l:0.55,a:0.34},   // pink
    {x:0.08,y:0.66,r:0.50,h:250,s:0.70,l:0.40,a:0.38},   // indigo
    {x:0.40,y:0.55,r:0.44,h:210,s:0.75,l:0.45,a:0.30}    // blue
  ];
  for(let n=0;n<neb.length;n++){
    const c=neb[n], puls=0.85+0.15*Math.sin(tt*0.25+n*1.7);
    const cx=c.x*W - cam*0.03, cy=c.y*H0, rad=c.r*Math.max(W,H0)*puls;
    const g=ctx.createRadialGradient(cx,cy,0,cx,cy,rad);
    g.addColorStop(0, hslHex(c.h,c.s,c.l)+'');
    g.addColorStop(0, silhouetteColor(hslHex(c.h,c.s,c.l),c.a,1.2));
    g.addColorStop(0.5, silhouetteColor(hslHex(c.h,c.s,c.l*0.9),c.a*0.4,1));
    g.addColorStop(1, silhouetteColor(hslHex(c.h,c.s,c.l),0,1));
    ctx.fillStyle=g; ctx.beginPath(); ctx.arc(cx,cy,rad,0,7); ctx.fill();
  }
  ctx.restore();

  // ---- STARFIELD: 3 parallax depths, dense, twinkling ----
  const depths=[{n:150,f:0.03,sz:0.9,br:0.5},{n:110,f:0.10,sz:1.5,br:0.75},{n:70,f:0.20,sz:2.3,br:1}];
  for(let d=0;d<depths.length;d++){
    const D=depths[d], span=W*3, shift=(cam*D.f)%span;
    for(let i=0;i<D.n;i++){
      const k=d*777+i;
      let sx=(hashS(k)*span - shift); sx=((sx%span)+span)%span - W*0.5;
      const sy=hashY(k)*H0;
      const tw=0.45+0.55*Math.sin(tt*(1+hashS(k+3)*2)+i*1.3);
      const sz=(D.sz*(0.5+hashY(k+9)))*DPR;
      const hue=200+hashS(k+5)*140;
      ctx.save(); ctx.globalCompositeOperation='lighter';
      ctx.fillStyle=silhouetteColor(hslHex(hue,0.4,0.9),D.br*tw,1.3);
      ctx.beginPath(); ctx.arc(sx,sy,sz,0,7); ctx.fill();
      if(d===2 && tw>0.85){ // sparkle cross on bright near stars
        ctx.strokeStyle=silhouetteColor(hslHex(hue,0.3,0.95),(tw-0.85)*3,1);
        ctx.lineWidth=DPR; ctx.beginPath();
        ctx.moveTo(sx-sz*3,sy); ctx.lineTo(sx+sz*3,sy);
        ctx.moveTo(sx,sy-sz*3); ctx.lineTo(sx,sy+sz*3); ctx.stroke();
      }
      ctx.restore();
    }
  }

  // ---- DISTANT GALAXY SPIRAL (faint, for depth) ----
  ctx.save(); ctx.globalCompositeOperation='lighter';
  const gx=0.30*W-cam*0.05, gy=0.20*H0, gr=70*DPR;
  ctx.translate(gx,gy); ctx.rotate(tt*0.02+0.6); ctx.scale(1,0.42);
  for(let a=0;a<2;a++){ ctx.rotate(Math.PI);
    ctx.beginPath();
    for(let s=0;s<40;s++){ const ang=s*0.28, rr2=gr*(s/40);
      const px=Math.cos(ang)*rr2, py=Math.sin(ang)*rr2;
      s===0?ctx.moveTo(px,py):ctx.lineTo(px,py); }
    ctx.strokeStyle=silhouetteColor('#c9b6ff',0.18,1); ctx.lineWidth=3*DPR; ctx.stroke();
  }
  const gg=ctx.createRadialGradient(0,0,0,0,0,gr*0.5);
  gg.addColorStop(0,silhouetteColor('#fff0ff',0.5,1)); gg.addColorStop(1,silhouetteColor('#fff0ff',0,1));
  ctx.fillStyle=gg; ctx.beginPath(); ctx.arc(0,0,gr*0.5,0,7); ctx.fill();
  ctx.restore();

  // ---- DISTANT SUN: tiny but brilliant, lens flare ----
  const sunx=0.88*W-cam*0.02, suny=0.16*H0, sr=6*DPR;
  ctx.save(); ctx.globalCompositeOperation='lighter';
  const flick=0.9+0.1*Math.sin(tt*3);
  const sg=ctx.createRadialGradient(sunx,suny,0,sunx,suny,sr*9*flick);
  sg.addColorStop(0,'#ffffff'); sg.addColorStop(0.15,'#fff6d8');
  sg.addColorStop(0.4,silhouetteColor('#ffd27a',0.5,1)); sg.addColorStop(1,silhouetteColor('#ffd27a',0,1));
  ctx.fillStyle=sg; ctx.beginPath(); ctx.arc(sunx,suny,sr*9*flick,0,7); ctx.fill();
  ctx.strokeStyle=silhouetteColor('#fff6d8',0.6,1); ctx.lineWidth=1.5*DPR;
  for(let r=0;r<6;r++){ const ang=r*Math.PI/3+tt*0.1, L=sr*(7+2*Math.sin(tt*2+r));
    ctx.beginPath(); ctx.moveTo(sunx,suny); ctx.lineTo(sunx+Math.cos(ang)*L,suny+Math.sin(ang)*L); ctx.stroke(); }
  ctx.fillStyle='#ffffff'; ctx.beginPath(); ctx.arc(sunx,suny,sr,0,7); ctx.fill();
  ctx.restore();

  // ---- CHARON: small grey cratered moon ----
  const chx=0.20*W-cam*0.09, chy=0.44*H0, chr=26*DPR;
  ctx.save();
  const cg=ctx.createRadialGradient(chx-chr*0.35,chy-chr*0.35,chr*0.1,chx,chy,chr);
  cg.addColorStop(0,'#c8ccd6'); cg.addColorStop(0.6,'#8b909c'); cg.addColorStop(1,'#4a4e58');
  ctx.fillStyle=cg; ctx.beginPath(); ctx.arc(chx,chy,chr,0,7); ctx.fill();
  ctx.fillStyle='rgba(60,64,72,0.5)';
  for(let c=0;c<7;c++){ const ca=hashS(c*13)*7, cd=hashY(c*13)*chr*0.8;
    const px=chx+Math.cos(ca)*cd, py=chy+Math.sin(ca)*cd, cr=chr*(0.08+hashS(c*7)*0.14);
    ctx.beginPath(); ctx.arc(px,py,cr,0,7); ctx.fill(); }
  ctx.save(); ctx.globalCompositeOperation='lighter'; // rim light
  ctx.strokeStyle=silhouetteColor('#dfe6ff',0.4,1); ctx.lineWidth=1.5*DPR;
  ctx.beginPath(); ctx.arc(chx,chy,chr*0.97,-0.6,1.2); ctx.stroke(); ctx.restore();
  ctx.restore();

  // ---- PLUTO: signature planet with the heart ----
  const px=0.60*W-cam*0.06, py=0.36*H0, pr=Math.min(W,H0)*0.16;
  ctx.save();
  // atmospheric haze glow
  ctx.save(); ctx.globalCompositeOperation='lighter';
  const hz=ctx.createRadialGradient(px,py,pr*0.9,px,py,pr*1.35);
  hz.addColorStop(0,silhouetteColor('#9fd0ff',0.35,1)); hz.addColorStop(1,silhouetteColor('#9fd0ff',0,1));
  ctx.fillStyle=hz; ctx.beginPath(); ctx.arc(px,py,pr*1.35,0,7); ctx.fill(); ctx.restore();
  // body: tan + icy shading
  const pg=ctx.createRadialGradient(px-pr*0.35,py-pr*0.4,pr*0.1,px,py,pr);
  pg.addColorStop(0,'#f0e6d0'); pg.addColorStop(0.4,'#d8c3a0');
  pg.addColorStop(0.7,'#b89b76'); pg.addColorStop(1,'#6d5a44');
  ctx.fillStyle=pg; ctx.beginPath(); ctx.arc(px,py,pr,0,7); ctx.fill();
  // clip to sphere for surface details
  ctx.save(); ctx.beginPath(); ctx.arc(px,py,pr,0,7); ctx.clip();
  // icy blue-white patches
  ctx.fillStyle='rgba(210,228,245,0.55)';
  for(let s=0;s<6;s++){ const sa=hashS(s*17)*7, sd=hashY(s*17)*pr*0.85;
    const sxp=px+Math.cos(sa)*sd, syp=py+Math.sin(sa)*sd, srp=pr*(0.12+hashS(s*5)*0.22);
    ctx.beginPath(); ctx.ellipse(sxp,syp,srp,srp*0.7,sa,0,7); ctx.fill(); }
  // dark maculae
  ctx.fillStyle='rgba(70,52,40,0.4)';
  for(let s=0;s<4;s++){ const sa=hashS(s*29+3)*7, sd=hashY(s*29+3)*pr*0.8;
    ctx.beginPath(); ctx.arc(px+Math.cos(sa)*sd,py+Math.sin(sa)*sd,pr*0.14,0,7); ctx.fill(); }
  // TOMBAUGH REGIO — the iconic light heart
  ctx.save();
  ctx.translate(px+pr*0.12,py+pr*0.28); ctx.rotate(-0.15); const hs=pr*0.5;
  const hg=ctx.createRadialGradient(0,-hs*0.1,0,0,0,hs);
  hg.addColorStop(0,'#fdfbf4'); hg.addColorStop(0.7,'#eef0ea'); hg.addColorStop(1,'rgba(230,235,225,0.2)');
  ctx.fillStyle=hg;
  ctx.beginPath();
  ctx.moveTo(0,hs*0.85);
  ctx.bezierCurveTo(-hs*1.05,hs*0.1,-hs*0.55,-hs*0.75,0,-hs*0.2);
  ctx.bezierCurveTo(hs*0.55,-hs*0.75,hs*1.05,hs*0.1,0,hs*0.85);
  ctx.fill();
  ctx.restore();
  // terminator shadow
  const sh=ctx.createLinearGradient(px-pr,py-pr,px+pr,py+pr);
  sh.addColorStop(0,'rgba(0,0,0,0)'); sh.addColorStop(0.55,'rgba(10,6,20,0)');
  sh.addColorStop(1,'rgba(10,6,25,0.72)');
  ctx.fillStyle=sh; ctx.beginPath(); ctx.arc(px,py,pr,0,7); ctx.fill();
  ctx.restore(); // unclip
  // rim light
  ctx.save(); ctx.globalCompositeOperation='lighter';
  ctx.strokeStyle=silhouetteColor('#dff0ff',0.55,1); ctx.lineWidth=2*DPR;
  ctx.beginPath(); ctx.arc(px,py,pr*0.985,-1.4,0.5); ctx.stroke(); ctx.restore();
  ctx.restore();

  // ---- COMETS / shooting stars ----
  ctx.save(); ctx.globalCompositeOperation='lighter';
  for(let m=0;m<3;m++){
    const period=6+m*2.5, ph=((tt+m*3)%period)/period;
    if(ph>0.55) continue; // only visible part of cycle
    const prog=ph/0.55;
    const startx=(1.1-hashS(m*31)*0.3)*W, starty=(0.08+hashY(m*31)*0.4)*H0;
    const ang=2.4+hashS(m*7)*0.4;
    const dist=prog*W*0.9;
    const hx=startx+Math.cos(ang)*dist, hy=starty+Math.sin(ang)*dist;
    const tl=90*DPR*(0.6+hashS(m*3));
    const tx=hx-Math.cos(ang)*tl, ty=hy-Math.sin(ang)*tl;
    const tg=ctx.createLinearGradient(hx,hy,tx,ty);
    const chue=180+hashS(m*11)*160;
    tg.addColorStop(0,silhouetteColor('#ffffff',0.9,1));
    tg.addColorStop(0.3,silhouetteColor(hslHex(chue,0.6,0.75),0.5,1.2));
    tg.addColorStop(1,silhouetteColor(hslHex(chue,0.6,0.7),0,1));
    ctx.strokeStyle=tg; ctx.lineWidth=2.4*DPR; ctx.lineCap='round';
    ctx.beginPath(); ctx.moveTo(hx,hy); ctx.lineTo(tx,ty); ctx.stroke();
    ctx.fillStyle='#ffffff'; ctx.beginPath(); ctx.arc(hx,hy,2.2*DPR,0,7); ctx.fill();
  }
  ctx.restore();
}

function drawFloorPlu(biome,t){
  const gy=GROUND(), tt=_PREF_REDUCE?t*0.3:t, ac=biome.ac||'#bcd';

  // cold otherworldly ground glow above the surface
  ctx.save(); ctx.globalCompositeOperation='lighter';
  const glowG=ctx.createLinearGradient(0,gy-70*DPR,0,gy+20*DPR);
  glowG.addColorStop(0,silhouetteColor('#7fbfff',0,1));
  glowG.addColorStop(1,silhouetteColor('#a9d8ff',0.28,1));
  ctx.fillStyle=glowG; ctx.fillRect(0,gy-70*DPR,W,90*DPR); ctx.restore();

  // nitrogen-ice plains base
  const pg=ctx.createLinearGradient(0,gy,0,H);
  pg.addColorStop(0,'#dfeaf5'); pg.addColorStop(0.25,'#c3d4e6');
  pg.addColorStop(0.6,'#9fb2c9'); pg.addColorStop(1,'#6f809b');
  ctx.fillStyle=pg; ctx.fillRect(0,gy,W,H-gy);

  // tan mottled ice regions
  ctx.save(); ctx.beginPath(); ctx.rect(0,gy,W,H-gy); ctx.clip();
  ctx.fillStyle='rgba(200,178,140,0.22)';
  const gapT=280*DPR,fT=0.4,shiftT=cam*fT;
  const j0=Math.floor((shiftT-gapT)/gapT), j1=Math.ceil((shiftT+W+gapT)/gapT);
  for(let k=j0;k<=j1;k++){ const bx=k*gapT-shiftT;
    const by=gy+(30+hashY(k*3)*120)*DPR, br=(60+hashS(k*3)*80)*DPR;
    ctx.beginPath(); ctx.ellipse(bx+hashS(k)*100*DPR,by,br,br*0.5,0,0,7); ctx.fill(); }

  // cracked ice-sheet polygon texture (Sputnik Planitia cells)
  ctx.strokeStyle='rgba(90,110,140,0.45)'; ctx.lineWidth=1.4*DPR;
  const cg=200*DPR, fc=0.4, sc=cam*fc;
  const c0=Math.floor((sc-cg)/cg), c1=Math.ceil((sc+W+cg)/cg);
  for(let k=c0;k<=c1;k++){ const cx=k*cg-sc;
    ctx.beginPath();
    ctx.moveTo(cx,gy);
    ctx.lineTo(cx+(hashS(k)-0.5)*60*DPR, gy+(H-gy));
    ctx.stroke();
    // horizontal fracture bands
    const bandY=gy+(40+hashY(k*5)*160)*DPR;
    ctx.beginPath(); ctx.moveTo(cx-cg*0.5,bandY);
    ctx.lineTo(cx+cg*0.5, bandY+(hashS(k*7)-0.5)*30*DPR); ctx.stroke();
  }
  // subtle bright frost speckles
  ctx.fillStyle='rgba(255,255,255,0.5)';
  for(let i=0;i<40;i++){ const fx=((hashS(i*17)*W*2 - cam*0.4)%W+W)%W;
    const fy=gy+hashY(i*17)*(H-gy); ctx.fillRect(fx,fy,1.4*DPR,1.4*DPR); }
  ctx.restore();

  // jagged icy mountains along the top edge (water-ice peaks)
  const mg=170*DPR, fm=0.4, sm=cam*fm;
  const m0=Math.floor((sm-mg)/mg), m1=Math.ceil((sm+W+mg)/mg);
  for(let k=m0;k<=m1;k++){ const mx=k*mg-sm;
    const h=(40+hashS(k*9)*70)*DPR, w=mg*(0.7+hashY(k*9)*0.4);
    const peakx=mx+w*0.5;
    ctx.beginPath();
    ctx.moveTo(mx,gy+2);
    ctx.lineTo(peakx-(hashS(k)*8)*DPR, gy-h);
    ctx.lineTo(mx+w,gy+2);
    ctx.closePath();
    const mgd=ctx.createLinearGradient(0,gy-h,0,gy);
    mgd.addColorStop(0,'#f4f9ff'); mgd.addColorStop(0.5,'#c2d2e6'); mgd.addColorStop(1,'#8fa0ba');
    ctx.fillStyle=mgd; ctx.fill();
    // sunlit rim
    ctx.strokeStyle='rgba(255,255,255,0.7)'; ctx.lineWidth=1.4*DPR;
    ctx.beginPath(); ctx.moveTo(mx,gy+2); ctx.lineTo(peakx-(hashS(k)*8)*DPR,gy-h); ctx.stroke();
    // shadow side
    ctx.fillStyle='rgba(50,66,92,0.32)';
    ctx.beginPath(); ctx.moveTo(peakx-(hashS(k)*8)*DPR,gy-h);
    ctx.lineTo(mx+w,gy+2); ctx.lineTo(peakx,gy+2); ctx.closePath(); ctx.fill();
  }

  // crisp horizon highlight line
  ctx.save(); ctx.globalCompositeOperation='lighter';
  ctx.strokeStyle=silhouetteColor('#eaf4ff',0.6,1); ctx.lineWidth=1.5*DPR;
  ctx.beginPath(); ctx.moveTo(0,gy+1.5*DPR); ctx.lineTo(W,gy+1.5*DPR); ctx.stroke();
  ctx.restore();
}
function scapeBlurred(biome, horizon, t) {
  const ac = (typeof getAccent === 'function') ? getAccent(biome && biome.ac) : (biome && biome.ac) || '#ff4fd8';
  const cx = W * 0.30, cy = horizon * 0.36; // moon anchor (upper-left-ish, off the play lane)

  // ---------- 1) BRIGHT twinkling starfield ----------
  ctx.save();
  const shiftS = cam * 0.03;
  for (let s = 0; s < 150; s++) {
    const bx = ((hashS(s * 2.17) * (W + 600) - shiftS) % (W + 600) + (W + 600)) % (W + 600) - 300;
    const by = hashY(s * 3.31) * horizon * 0.95;
    const tw = 0.55 + 0.45 * Math.sin(t * (1.4 + hashS(s) * 2.2) + s * 1.7);
    const sz = (0.7 + hashS(s * 5.1) * 2.1) * DPR;
    const hue = 190 + hashS(s * 7.3) * 150; // cyan->magenta stars
    ctx.globalAlpha = 0.35 + tw * 0.65;
    ctx.fillStyle = hslHex(hue, 0.85, 0.85);
    ctx.beginPath(); ctx.arc(bx, by, sz, 0, 7); ctx.fill();
  }
  ctx.restore();

  // ---------- 2) VIVID AURORA ribbons ----------
  ctx.save();
  ctx.globalCompositeOperation = 'lighter';
  const auroraHues = [150, 175, 300, 195, 320];
  for (let a = 0; a < 5; a++) {
    const baseY = horizon * (0.14 + a * 0.075);
    const amp = horizon * (0.09 + hashS(a * 11.7) * 0.05);
    const hue = auroraHues[((a % auroraHues.length) + auroraHues.length) % auroraHues.length];
    const speed = 0.35 + a * 0.14;
    ctx.beginPath();
    ctx.moveTo(-40, baseY);
    for (let x = -40; x <= W + 40; x += 26 * DPR) {
      const y = baseY
        + Math.sin(x * 0.0021 + t * speed + a) * amp
        + Math.sin(x * 0.0055 - t * (speed * 0.7) + a * 2) * amp * 0.45;
      ctx.lineTo(x, y);
    }
    const g = ctx.createLinearGradient(0, baseY - amp, 0, baseY + amp + 120 * DPR);
    g.addColorStop(0, hslHex(hue, 1, 0.62));
    g.addColorStop(0.5, hslHex(hue + 20, 1, 0.5));
    g.addColorStop(1, hslHex(hue - 30, 1, 0.28));
    for (let x = W + 40; x >= -40; x -= 26 * DPR) {
      const y = baseY
        + Math.sin(x * 0.0021 + t * speed + a) * amp
        + Math.sin(x * 0.0055 - t * (speed * 0.7) + a * 2) * amp * 0.45
        + (80 + a * 26) * DPR;
      ctx.lineTo(x, y);
    }
    ctx.closePath();
    ctx.globalAlpha = 0.55;
    ctx.fillStyle = g;
    ctx.fill();
  }
  ctx.restore();

  // ---------- 1b) THE HERO MOON — god-ray beams ----------
  ctx.save();
  ctx.globalCompositeOperation = 'lighter';
  const rayN = 14;
  for (let r = 0; r < rayN; r++) {
    const ang = (r / rayN) * Math.PI * 2 + t * 0.05;
    const len = (260 + Math.sin(t * 0.7 + r) * 60) * DPR;
    const wdt = (10 + hashS(r * 4.4) * 22) * DPR;
    ctx.save();
    ctx.translate(cx, cy);
    ctx.rotate(ang);
    const rg = ctx.createLinearGradient(0, 0, len, 0);
    rg.addColorStop(0, 'rgba(255,250,225,0.5)');
    rg.addColorStop(1, 'rgba(255,240,200,0)');
    ctx.fillStyle = rg;
    ctx.globalAlpha = 0.4 + 0.25 * Math.sin(t * 1.1 + r * 2);
    ctx.beginPath();
    ctx.moveTo(0, -wdt);
    ctx.lineTo(len, -wdt * 0.15);
    ctx.lineTo(len, wdt * 0.15);
    ctx.lineTo(0, wdt);
    ctx.closePath(); ctx.fill();
    ctx.restore();
  }
  ctx.restore();

  // ---------- 1c) THE HERO MOON — luminous halo + core ----------
  const moonR = horizon * 0.16;
  ctx.save();
  ctx.globalCompositeOperation = 'lighter';
  const halo = ctx.createRadialGradient(cx, cy, moonR * 0.5, cx, cy, moonR * 4.2);
  halo.addColorStop(0, 'rgba(255,252,235,0.95)');
  halo.addColorStop(0.18, 'rgba(255,240,200,0.55)');
  halo.addColorStop(0.42, 'rgba(255,180,240,0.30)');
  halo.addColorStop(0.7, 'rgba(120,180,255,0.14)');
  halo.addColorStop(1, 'rgba(120,180,255,0)');
  ctx.fillStyle = halo;
  ctx.beginPath(); ctx.arc(cx, cy, moonR * 4.2, 0, 7); ctx.fill();
  ctx.restore();
  // crisp near-white core
  const core = ctx.createRadialGradient(cx - moonR * 0.3, cy - moonR * 0.3, moonR * 0.1, cx, cy, moonR);
  core.addColorStop(0, '#ffffff');
  core.addColorStop(0.6, '#fff6e2');
  core.addColorStop(0.88, '#ffe7c0');
  core.addColorStop(1, '#ffd28a');
  ctx.fillStyle = core;
  ctx.beginPath(); ctx.arc(cx, cy, moonR, 0, 7); ctx.fill();
  // subtle craters
  ctx.save();
  ctx.globalAlpha = 0.10; ctx.fillStyle = '#c9a86a';
  ctx.beginPath(); ctx.arc(cx + moonR * 0.3, cy + moonR * 0.1, moonR * 0.22, 0, 7); ctx.fill();
  ctx.beginPath(); ctx.arc(cx - moonR * 0.35, cy + moonR * 0.35, moonR * 0.14, 0, 7); ctx.fill();
  ctx.restore();

  // ---------- 4) Floating dream islands + neon archways ----------
  const gap = 360 * DPR, f = 0.2, shift = cam * f;
  const i0 = Math.floor((shift - gap) / gap), i1 = Math.ceil((shift + W + gap) / gap);
  for (let k = i0; k <= i1; k++) {
    const seed = ((k % 997) + 997) % 997;
    const x = k * gap - shift + (hashS(seed * 1.7) - 0.5) * 180 * DPR;
    const y = horizon * (0.30 + hashY(seed * 2.3) * 0.42) + Math.sin(t * 0.5 + seed) * 8 * DPR;
    const isArch = hashS(seed * 9.1) > 0.55;
    const iw = (70 + hashS(seed * 3.3) * 70) * DPR;
    const neon = hslHex(180 + hashS(seed * 5.5) * 160, 1, 0.6);
    if (isArch) {
      ctx.save();
      ctx.strokeStyle = neon; ctx.lineWidth = 4 * DPR; ctx.lineCap = 'round';
      ctx.shadowColor = neon; ctx.shadowBlur = 18 * DPR;
      ctx.beginPath(); ctx.arc(x, y + iw * 0.5, iw * 0.55, Math.PI * 1.05, Math.PI * 1.95); ctx.stroke();
      ctx.beginPath(); ctx.moveTo(x - iw * 0.5, y + iw * 0.5); ctx.lineTo(x - iw * 0.5, y + iw * 0.95); ctx.stroke();
      ctx.beginPath(); ctx.moveTo(x + iw * 0.5, y + iw * 0.5); ctx.lineTo(x + iw * 0.5, y + iw * 0.95); ctx.stroke();
      ctx.restore();
    } else {
      // island body (crisp silhouette) with glowing rim
      ctx.save();
      ctx.fillStyle = silhouetteColor(biome && biome.sky ? biome.sky[2] : '#2a0f4a', 0.92, 0.7);
      ctx.beginPath();
      ctx.moveTo(x - iw, y);
      ctx.quadraticCurveTo(x, y - iw * 0.32, x + iw, y);
      ctx.lineTo(x + iw * 0.45, y + iw * 0.75);
      ctx.quadraticCurveTo(x, y + iw * 1.05, x - iw * 0.45, y + iw * 0.75);
      ctx.closePath(); ctx.fill();
      ctx.strokeStyle = neon; ctx.lineWidth = 2.6 * DPR;
      ctx.shadowColor = neon; ctx.shadowBlur = 14 * DPR;
      ctx.beginPath();
      ctx.moveTo(x - iw, y);
      ctx.quadraticCurveTo(x, y - iw * 0.32, x + iw, y);
      ctx.stroke();
      ctx.restore();
      // glowing plant dots on top
      ctx.save();
      ctx.globalCompositeOperation = 'lighter';
      for (let p = 0; p < 3; p++) {
        const px = x + (hashS(seed * (p + 2)) - 0.5) * iw * 1.4;
        const py = y - hashS(seed * (p + 5)) * iw * 0.28;
        ctx.fillStyle = ac;
        ctx.globalAlpha = 0.8;
        ctx.beginPath(); ctx.arc(px, py, 3 * DPR, 0, 7); ctx.fill();
      }
      ctx.restore();
    }
  }

  // ---------- 3) HERO FLOATING WHALE(S) with neon outline + trail ----------
  const whales = [
    { f: 0.14, sc: 1.0, yy: 0.30, sp: 0.06, hue: 195 },
    { f: 0.09, sc: 0.6, yy: 0.52, sp: 0.045, hue: 315 }
  ];
  for (let wi = 0; wi < whales.length; wi++) {
    const wh = whales[wi];
    const period = W + 700 * DPR;
    const wx = ((t * 26 * DPR * (wi ? -1 : 1) - cam * wh.f) % period + period) % period - 200 * DPR;
    const wy = horizon * wh.yy + Math.sin(t * 0.5 + wi * 2) * 14 * DPR;
    const S = 150 * DPR * wh.sc;
    const dir = wi ? -1 : 1;
    const neon = hslHex(wh.hue, 1, 0.62);

    // particle trail
    ctx.save();
    ctx.globalCompositeOperation = 'lighter';
    for (let p = 0; p < 12; p++) {
      const tp = p / 12;
      const px = wx - dir * (0.55 + tp * 1.4) * S + Math.sin(t * 3 + p) * 4 * DPR;
      const py = wy + Math.sin(t * 2 + p * 1.3) * 8 * DPR;
      ctx.globalAlpha = (1 - tp) * 0.5;
      ctx.fillStyle = hslHex(wh.hue + tp * 40, 1, 0.7);
      ctx.beginPath(); ctx.arc(px, py, (4 - tp * 3) * DPR + 1, 0, 7); ctx.fill();
    }
    ctx.restore();

    // whale silhouette
    ctx.save();
    ctx.translate(wx, wy);
    ctx.scale(dir, 1);
    ctx.beginPath();
    ctx.moveTo(-0.55 * S, 0);
    ctx.bezierCurveTo(-0.35 * S, -0.28 * S, 0.25 * S, -0.30 * S, 0.5 * S, -0.05 * S); // back
    ctx.bezierCurveTo(0.62 * S, 0.0, 0.7 * S, -0.02 * S, 0.55 * S, 0.10 * S); // head
    ctx.bezierCurveTo(0.3 * S, 0.26 * S, -0.2 * S, 0.24 * S, -0.42 * S, 0.10 * S); // belly
    ctx.lineTo(-0.55 * S, 0.02 * S);
    ctx.lineTo(-0.78 * S, -0.20 * S); // tail top
    ctx.lineTo(-0.62 * S, 0.0);
    ctx.lineTo(-0.78 * S, 0.20 * S); // tail bottom
    ctx.closePath();
    const bod = ctx.createLinearGradient(0, -0.3 * S, 0, 0.26 * S);
    bod.addColorStop(0, hslHex(wh.hue, 0.85, 0.24));
    bod.addColorStop(1, hslHex(wh.hue - 20, 0.9, 0.12));
    ctx.fillStyle = bod; ctx.fill();
    ctx.strokeStyle = neon; ctx.lineWidth = 3 * DPR;
    ctx.shadowColor = neon; ctx.shadowBlur = 22 * DPR;
    ctx.stroke();
    // belly glow line + eye
    ctx.globalCompositeOperation = 'lighter';
    ctx.beginPath(); ctx.moveTo(-0.4 * S, 0.12 * S);
    ctx.quadraticCurveTo(0.1 * S, 0.22 * S, 0.45 * S, 0.09 * S);
    ctx.strokeStyle = hslHex(wh.hue + 30, 1, 0.75); ctx.lineWidth = 2 * DPR; ctx.stroke();
    ctx.fillStyle = '#ffffff';
    ctx.beginPath(); ctx.arc(0.46 * S, -0.02 * S, 3 * DPR * wh.sc + 1, 0, 7); ctx.fill();
    ctx.restore();
  }
}

function drawFloorBlurred(biome, t) {
  const g = GROUND();
  const ac = (typeof getAccent === 'function') ? getAccent(biome && biome.ac) : (biome && biome.ac) || '#ff4fd8';
  const cx = W * 0.30;

  // base mirror gradient — deep saturated water
  const wg = ctx.createLinearGradient(0, g, 0, H);
  wg.addColorStop(0, '#160a34');
  wg.addColorStop(0.4, '#1e0c46');
  wg.addColorStop(1, '#0a0522');
  ctx.fillStyle = wg;
  ctx.fillRect(0, g, W, H - g);

  // bright horizon rim line
  ctx.save();
  ctx.globalCompositeOperation = 'lighter';
  const rim = ctx.createLinearGradient(0, g - 4 * DPR, 0, g + 30 * DPR);
  rim.addColorStop(0, 'rgba(255,240,210,0.0)');
  rim.addColorStop(0.5, 'rgba(255,235,190,0.55)');
  rim.addColorStop(1, 'rgba(255,120,220,0.0)');
  ctx.fillStyle = rim;
  ctx.fillRect(0, g - 4 * DPR, W, 34 * DPR);
  ctx.restore();

  // ---------- vivid vertical reflection streaks ----------
  ctx.save();
  ctx.globalCompositeOperation = 'lighter';
  const streaks = [
    { x: cx, hue: 48, s: 0.55, bright: 0.9 },   // moon (gold)
    { x: cx - 40 * DPR, hue: 150, s: 0.9, bright: 0.5 },
    { x: cx + 60 * DPR, hue: 300, s: 0.9, bright: 0.5 },
    { x: W * 0.66, hue: 190, s: 0.9, bright: 0.45 },
    { x: W * 0.82, hue: 320, s: 0.9, bright: 0.4 }
  ];
  for (let i = 0; i < streaks.length; i++) {
    const st = streaks[i];
    const wobble = Math.sin(t * 1.3 + i) * 10 * DPR;
    const sw = (i === 0 ? 90 : 40) * DPR;
    for (let seg = 0; seg < 26; seg++) {
      const yy = g + seg * ((H - g) / 26);
      const wob = Math.sin(t * 2 + seg * 0.55 + i * 2) * (4 + seg * 0.7) * DPR;
      const a = st.bright * (1 - seg / 30) * (0.6 + 0.4 * Math.sin(t * 3 + seg + i));
      ctx.globalAlpha = Math.max(0, a);
      ctx.fillStyle = hslHex(st.hue, st.s, i === 0 ? 0.8 : 0.62);
      const w = sw * (1 + seg * 0.05);
      rr(ctx, st.x + wobble + wob - w / 2, yy, w, ((H - g) / 26) * 1.4, 6 * DPR);
      ctx.fill();
    }
  }
  ctx.restore();

  // ---------- animated ripple lines ----------
  ctx.save();
  ctx.globalCompositeOperation = 'lighter';
  for (let r = 0; r < 9; r++) {
    const yy = g + (r + 0.5) * ((H - g) / 9);
    const amp = 5 * DPR * (1 + r * 0.4);
    ctx.beginPath();
    for (let x = 0; x <= W; x += 22 * DPR) {
      const y = yy + Math.sin(x * 0.02 + t * 2 + r) * amp + Math.sin(x * 0.006 - t * 1.3) * amp * 0.6;
      if (x === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
    }
    ctx.globalAlpha = 0.18 + 0.12 * Math.sin(t + r);
    ctx.strokeStyle = hslHex(190 + r * 12, 0.9, 0.7);
    ctx.lineWidth = 2 * DPR;
    ctx.stroke();
  }
  ctx.restore();

  // sparkle glints on the water
  ctx.save();
  ctx.globalCompositeOperation = 'lighter';
  for (let s = 0; s < 40; s++) {
    const sx = hashS(s * 2.1) * W;
    const sy = g + hashY(s * 3.7) * (H - g);
    const tw = 0.5 + 0.5 * Math.sin(t * 3 + s * 2.3);
    ctx.globalAlpha = tw * 0.7;
    ctx.fillStyle = s % 3 === 0 ? '#fff2c8' : hslHex(180 + hashS(s) * 160, 1, 0.8);
    ctx.beginPath(); ctx.arc(sx, sy, (0.8 + hashS(s * 4.2) * 1.6) * DPR, 0, 7); ctx.fill();
  }
  ctx.restore();
}
function scapePrivate(biome,horizon,t){
  const acc=getAccent(1), stone='#3a3a4e', stoneD='#23232f', stoneL='#565672';
  const warm='#ffb347', gold='#ffd27a', fire='#ff6a1a';
  const tt=_PREF_REDUCE?0:t;

  // ---- moody sky wash (indigo->near black) on top of base ----
  const sg=ctx.createLinearGradient(0,0,0,horizon);
  sg.addColorStop(0,silhouetteColor('#1a1030',0.55,1));
  sg.addColorStop(0.6,silhouetteColor('#0c0a1c',0.45,1));
  sg.addColorStop(1,silhouetteColor('#050410',0.55,1));
  ctx.fillStyle=sg; ctx.fillRect(0,0,W,horizon);

  // ---- big blood moon ----
  const mx=W*0.74, my=horizon*0.34, mr=64*DPR;
  ctx.save(); ctx.globalCompositeOperation='lighter';
  const mg=ctx.createRadialGradient(mx,my,mr*0.2,mx,my,mr*3.2);
  mg.addColorStop(0,'rgba(255,120,70,0.55)');
  mg.addColorStop(0.35,'rgba(200,60,50,0.22)');
  mg.addColorStop(1,'rgba(120,20,40,0)');
  ctx.fillStyle=mg; ctx.beginPath(); ctx.arc(mx,my,mr*3.2,0,7); ctx.fill();
  ctx.restore();
  const md=ctx.createRadialGradient(mx-mr*0.3,my-mr*0.3,mr*0.2,mx,my,mr);
  md.addColorStop(0,'#ffcaa0'); md.addColorStop(0.5,'#f0784e'); md.addColorStop(1,'#a83a3f');
  ctx.fillStyle=md; ctx.beginPath(); ctx.arc(mx,my,mr,0,7); ctx.fill();
  // moon craters
  ctx.fillStyle='rgba(120,40,45,0.4)';
  for(let c=0;c<5;c++){const a=hashS(c*13)*7,rd=mr*(0.3+hashS(c*7)*0.5);
    ctx.beginPath();ctx.arc(mx+Math.cos(a)*rd,my+Math.sin(a)*rd,mr*(0.08+hashS(c*3)*0.1),0,7);ctx.fill();}

  // ---- dramatic clouds drifting across moon ----
  for(let c=0;c<7;c++){
    const cw=(180+hashS(c*5)*160)*DPR, cy=horizon*(0.18+hashS(c*9)*0.4);
    let cx=((c*260*DPR + tt*(6+hashS(c)*8)*DPR) % (W+cw*2))-cw;
    ctx.fillStyle=silhouetteColor('#0a0714',0.5+hashS(c*3)*0.2,1);
    ctx.beginPath();
    for(let b=0;b<5;b++){const bx=cx+b*cw*0.22, by=cy+Math.sin(b*1.3+c)*8*DPR;
      ctx.arc(bx,by,cw*(0.14+hashS(c*7+b)*0.1),0,7);}
    ctx.fill();
  }

  // ---- STARS ----
  ctx.save(); ctx.globalCompositeOperation='lighter';
  for(let s=0;s<60;s++){const sx=hashS(s*17)*W, sy=hashY(s*31)*horizon*0.8;
    const tw=0.3+0.7*(0.5+0.5*Math.sin(tt*2+s));
    ctx.fillStyle='rgba(255,230,200,'+(0.4*tw)+')';
    ctx.fillRect(sx,sy,1.4*DPR,1.4*DPR);}
  ctx.restore();

  // ===== helper: castle skyline layer =====
  function castleLayer(f,baseH,tint,ta,seed,winCol){
    const gap=300*DPR, shift=cam*f, i0=Math.floor((shift-gap)/gap), i1=Math.ceil((shift+W+gap)/gap);
    const gy=horizon;
    for(let k=i0;k<=i1;k++){
      const x=k*gap-shift;
      const h=(baseH+hashS(k*7+seed)*baseH*0.6);
      const w=gap*(0.62+hashS(k*3+seed)*0.2);
      ctx.fillStyle=silhouetteColor(tint,ta,1);
      // main keep body
      rr(ctx,x,gy-h,w,h+40*DPR,4*DPR); ctx.fill();
      // battlements on top
      const bw=w/6;
      for(let m=0;m<6;m+=2){ctx.fillRect(x+m*bw,gy-h-10*DPR,bw,12*DPR);}
      // twin towers with pointed spires
      for(let s=0;s<2;s++){
        const tx=x+(s?w-w*0.22:0), tw2=w*0.22, th=h*(1.15+hashS(k*11+s+seed)*0.35);
        ctx.fillStyle=silhouetteColor(tint,ta,1);
        rr(ctx,tx,gy-th,tw2,th+40*DPR,3*DPR); ctx.fill();
        // spire
        ctx.beginPath(); ctx.moveTo(tx-3*DPR,gy-th);
        ctx.lineTo(tx+tw2/2,gy-th-tw2*1.5); ctx.lineTo(tx+tw2+3*DPR,gy-th); ctx.closePath(); ctx.fill();
        // glowing window
        if(hashS(k*5+s+seed)>0.35){
          ctx.save(); ctx.globalCompositeOperation='lighter';
          const fl=0.6+0.4*Math.sin(tt*3+k+s);
          ctx.fillStyle=winCol.replace('A',(0.7*fl).toFixed(2));
          const wx=tx+tw2*0.3, wy=gy-th*0.55;
          rr(ctx,wx,wy,tw2*0.4,tw2*0.7,tw2*0.2); ctx.fill();
          ctx.restore();
        }
      }
    }
  }
  // 3 parallax depths
  castleLayer(0.06, 90*DPR, '#181428', 0.85, 100, 'rgba(255,180,90,A)');
  castleLayer(0.13, 120*DPR,'#221a34', 0.9, 40,  'rgba(255,200,110,A)');
  castleLayer(0.22, 150*DPR,'#2c2440', 0.95, 7,  'rgba(255,210,120,A)');

  // ===== WINGED STONE STATUES (gargoyles) on pedestals =====
  function gargoyle(gx,gy,sc,flip){
    ctx.save(); ctx.translate(gx,gy); if(flip)ctx.scale(-1,1); ctx.scale(sc,sc);
    // pedestal
    ctx.fillStyle=silhouetteColor(stoneD,1,1);
    rr(ctx,-26*DPR,0,52*DPR,26*DPR,3*DPR); ctx.fill();
    ctx.fillStyle=silhouetteColor(stone,1,1);
    rr(ctx,-20*DPR,-6*DPR,40*DPR,10*DPR,2*DPR); ctx.fill();
    // spread wings (behind body)
    const wf=6*Math.sin(tt*1.4)*DPR;
    ctx.fillStyle=silhouetteColor(stoneD,1,1);
    for(let s=0;s<2;s++){ ctx.save(); if(s)ctx.scale(-1,1);
      ctx.beginPath(); ctx.moveTo(0,-30*DPR);
      ctx.quadraticCurveTo(34*DPR,-56*DPR-wf,46*DPR,-20*DPR-wf);
      ctx.lineTo(40*DPR,-14*DPR);
      ctx.quadraticCurveTo(30*DPR,-30*DPR,18*DPR,-18*DPR);
      ctx.lineTo(24*DPR,-8*DPR);
      ctx.quadraticCurveTo(14*DPR,-18*DPR,4*DPR,-16*DPR);
      ctx.closePath(); ctx.fill(); ctx.restore(); }
    // crouching body
    ctx.fillStyle=silhouetteColor(stone,1,1);
    ctx.beginPath();
    ctx.moveTo(-14*DPR,-4*DPR);
    ctx.quadraticCurveTo(-16*DPR,-34*DPR,0,-40*DPR);
    ctx.quadraticCurveTo(16*DPR,-34*DPR,14*DPR,-4*DPR);
    ctx.closePath(); ctx.fill();
    // head + horns
    ctx.fillStyle=silhouetteColor(stoneL,1,1);
    ctx.beginPath(); ctx.arc(0,-42*DPR,8*DPR,0,7); ctx.fill();
    ctx.beginPath(); ctx.moveTo(-6*DPR,-46*DPR); ctx.lineTo(-10*DPR,-56*DPR); ctx.lineTo(-3*DPR,-48*DPR); ctx.fill();
    ctx.beginPath(); ctx.moveTo(6*DPR,-46*DPR); ctx.lineTo(10*DPR,-56*DPR); ctx.lineTo(3*DPR,-48*DPR); ctx.fill();
    // glowing eyes
    ctx.save(); ctx.globalCompositeOperation='lighter';
    ctx.fillStyle='rgba(255,120,40,0.9)';
    ctx.beginPath(); ctx.arc(-3*DPR,-43*DPR,1.6*DPR,0,7); ctx.arc(3*DPR,-43*DPR,1.6*DPR,0,7); ctx.fill();
    ctx.restore();
    ctx.restore();
  }
  // (winged statues removed - they read as "flying statues")

  // ===== TORCHES / brazier glow on walls =====
  function torch(tx,ty){
    const fl=_PREF_REDUCE?0.7:(0.55+0.45*Math.sin(tt*8+tx)*Math.cos(tt*5+ty));
    ctx.save(); ctx.globalCompositeOperation='lighter';
    const g=ctx.createRadialGradient(tx,ty,0,tx,ty,42*DPR*(0.85+fl*0.3));
    g.addColorStop(0,'rgba(255,200,110,'+(0.55*fl).toFixed(2)+')');
    g.addColorStop(0.4,'rgba(255,130,40,'+(0.28*fl).toFixed(2)+')');
    g.addColorStop(1,'rgba(255,90,20,0)');
    ctx.fillStyle=g; ctx.beginPath(); ctx.arc(tx,ty,42*DPR,0,7); ctx.fill();
    // flame teardrop
    ctx.fillStyle='rgba(255,220,120,'+(0.9*fl).toFixed(2)+')';
    ctx.beginPath(); ctx.moveTo(tx,ty-14*DPR*fl);
    ctx.quadraticCurveTo(tx+5*DPR,ty,tx,ty+5*DPR);
    ctx.quadraticCurveTo(tx-5*DPR,ty,tx,ty-14*DPR*fl); ctx.fill();
    ctx.restore();
  }
  for(let k=0;k<6;k++){ const tx=(k*220*DPR - (cam*0.22)% (220*DPR));
    torch(((tx%W)+W)%W, horizon*(0.62+hashS(k*9)*0.18)); }

  // ===== FIRE-BREATHING DRAGONS =====
  function dragon(seed,yBase,scale,speed,dir){
    const span=W+400*DPR;
    let dx=((tt*speed*DPR + seed*400*DPR)%span);
    if(dir<0)dx=span-dx;
    const dy=yBase+Math.sin(tt*0.7+seed)*24*DPR;
    const flap=Math.sin(tt*(_PREF_REDUCE?1.2:3.2)+seed);
    ctx.save(); ctx.translate(dx,dy); ctx.scale(dir*scale,scale);
    // tail
    ctx.strokeStyle=silhouetteColor('#1c1230',0.95,1); ctx.lineWidth=6*DPR; ctx.lineCap='round';
    ctx.beginPath(); ctx.moveTo(-6*DPR,0);
    ctx.quadraticCurveTo(-70*DPR,10*DPR+flap*6*DPR,-120*DPR,-6*DPR); ctx.stroke();
    // wings
    ctx.fillStyle=silhouetteColor('#241635',0.95,1);
    for(let s=0;s<2;s++){ ctx.save(); if(s)ctx.scale(1,-1);
      ctx.beginPath(); ctx.moveTo(-8*DPR,-2*DPR);
      ctx.quadraticCurveTo(-30*DPR,-46*DPR-flap*20*DPR,10*DPR,-52*DPR-flap*16*DPR);
      ctx.quadraticCurveTo(20*DPR,-24*DPR,24*DPR,-4*DPR);
      ctx.quadraticCurveTo(6*DPR,-16*DPR,-8*DPR,-2*DPR); ctx.closePath(); ctx.fill(); ctx.restore(); }
    // body
    ctx.fillStyle=silhouetteColor('#2c1c3d',1,1);
    ctx.beginPath(); ctx.ellipse(0,0,22*DPR,10*DPR,0,0,7); ctx.fill();
    // neck + head
    ctx.beginPath(); ctx.moveTo(16*DPR,-2*DPR);
    ctx.quadraticCurveTo(38*DPR,-14*DPR,50*DPR,-8*DPR);
    ctx.lineTo(56*DPR,-2*DPR); ctx.lineTo(48*DPR,2*DPR);
    ctx.quadraticCurveTo(36*DPR,2*DPR,18*DPR,4*DPR); ctx.closePath(); ctx.fill();
    // eye
    ctx.save(); ctx.globalCompositeOperation='lighter';
    ctx.fillStyle='rgba(255,180,60,0.95)'; ctx.beginPath(); ctx.arc(46*DPR,-6*DPR,2*DPR,0,7); ctx.fill();
    // ---- FIRE BREATH cone ----
    const fpow=_PREF_REDUCE?0.5:(0.5+0.5*Math.sin(tt*4+seed));
    const fx=58*DPR, fy=-1*DPR, flen=(70+fpow*40)*DPR;
    const fg=ctx.createRadialGradient(fx,fy,0,fx+flen*0.5,fy,flen);
    fg.addColorStop(0,'rgba(255,240,180,'+(0.9*fpow).toFixed(2)+')');
    fg.addColorStop(0.3,'rgba(255,150,40,'+(0.7*fpow).toFixed(2)+')');
    fg.addColorStop(0.7,'rgba(255,80,20,'+(0.35*fpow).toFixed(2)+')');
    fg.addColorStop(1,'rgba(200,30,10,0)');
    ctx.fillStyle=fg; ctx.beginPath();
    ctx.moveTo(fx,fy-4*DPR);
    ctx.quadraticCurveTo(fx+flen,fy-24*DPR*fpow, fx+flen,fy);
    ctx.quadraticCurveTo(fx+flen,fy+24*DPR*fpow, fx,fy+4*DPR);
    ctx.closePath(); ctx.fill();
    // embers
    for(let e=0;e<6;e++){const ep=(tt*1.5+e*0.5+seed)%1;
      ctx.fillStyle='rgba(255,'+(140+e*15)+',40,'+((1-ep)*0.7*fpow).toFixed(2)+')';
      ctx.beginPath(); ctx.arc(fx+flen*ep+ep*20*DPR, fy+Math.sin(e*2+tt*3)*10*DPR*ep, (2.5-ep*2)*DPR,0,7); ctx.fill();}
    ctx.restore();
    ctx.restore();
  }
  dragon(1.0, horizon*0.30, 1.25, 26, 1);
  dragon(2.4, horizon*0.5, 0.85, 18, -1);

  // ===== BATS =====
  ctx.save();
  for(let b=0;b<10;b++){
    const bx=((b*160*DPR + tt*30*DPR)%(W+80*DPR))-40*DPR;
    const by=horizon*(0.2+hashS(b*13)*0.5)+Math.sin(tt*2+b)*14*DPR;
    const wf=Math.sin(tt*10+b)*5*DPR;
    ctx.fillStyle=silhouetteColor('#080610',0.8,1);
    ctx.beginPath();
    ctx.moveTo(bx,by);
    ctx.quadraticCurveTo(bx-6*DPR,by-wf,bx-11*DPR,by);
    ctx.quadraticCurveTo(bx-6*DPR,by+2*DPR,bx,by+3*DPR);
    ctx.quadraticCurveTo(bx+6*DPR,by+2*DPR,bx+11*DPR,by);
    ctx.quadraticCurveTo(bx+6*DPR,by-wf,bx,by); ctx.fill();
  }
  ctx.restore();

  // ===== drifting embers/sparks =====
  ctx.save(); ctx.globalCompositeOperation='lighter';
  for(let e=0;e<26;e++){
    const ex=(hashS(e*7)*W + Math.sin(tt*0.5+e)*20*DPR);
    const ey=horizon - ((tt*(12+hashS(e*3)*20)*DPR + e*80*DPR)%(horizon));
    const al=0.4*(0.5+0.5*Math.sin(tt*3+e));
    ctx.fillStyle='rgba(255,'+(150+hashS(e)*80|0)+',60,'+al.toFixed(2)+')';
    ctx.beginPath(); ctx.arc(ex,ey,1.6*DPR,0,7); ctx.fill();
  }
  ctx.restore();
}

function drawFloorPrivate(biome,t){
  const gy=GROUND(); const tt=_PREF_REDUCE?0:t;
  // ---- dark stone ground base ----
  const g=ctx.createLinearGradient(0,gy,0,H);
  g.addColorStop(0,'#1c1830'); g.addColorStop(0.5,'#12101f'); g.addColorStop(1,'#08060f');
  ctx.fillStyle=g; ctx.fillRect(0,gy,W,H-gy);

  // ---- parapet / battlement edge silhouette along ground top ----
  ctx.fillStyle='#0c0a18';
  const bw=64*DPR, shift=(cam*0.4)%(bw*2);
  ctx.fillRect(0,gy,W,8*DPR);
  for(let x=-shift;x<W;x+=bw*2){ ctx.fillRect(x,gy-14*DPR,bw,16*DPR); }

  // ---- flagstone / cobble grid with perspective ----
  ctx.strokeStyle='rgba(90,80,120,0.22)'; ctx.lineWidth=1.5*DPR;
  const rows=8;
  for(let r=0;r<=rows;r++){
    const fr=r/rows; const yy=gy+ (H-gy)*fr*fr;
    ctx.beginPath(); ctx.moveTo(0,yy); ctx.lineTo(W,yy); ctx.stroke();
  }
  // vertical flagstone joints (converge toward center vanishing)
  const vx=W*0.5;
  for(let c=-8;c<=8;c++){
    ctx.beginPath();
    ctx.moveTo(vx + c*30*DPR - (cam*0.4)%(60*DPR), gy);
    ctx.lineTo(vx + c*160*DPR - (cam*0.4)%(60*DPR)*3.5, H);
    ctx.stroke();
  }

  // ---- warm torchlight glow pools on the floor ----
  ctx.save(); ctx.globalCompositeOperation='lighter';
  for(let k=0;k<5;k++){
    const px=(k*260*DPR - (cam*0.4)%(260*DPR)); const pxx=((px%W)+W)%W;
    const py=gy+(H-gy)*(0.25+hashS(k*11)*0.4);
    const fl=_PREF_REDUCE?0.7:(0.6+0.4*Math.sin(tt*7+k*2));
    const rad=110*DPR*(0.9+fl*0.2);
    const pg=ctx.createRadialGradient(pxx,py,0,pxx,py,rad);
    pg.addColorStop(0,'rgba(255,180,90,'+(0.28*fl).toFixed(2)+')');
    pg.addColorStop(0.5,'rgba(255,120,40,'+(0.12*fl).toFixed(2)+')');
    pg.addColorStop(1,'rgba(255,90,20,0)');
    ctx.fillStyle=pg; ctx.beginPath(); ctx.arc(pxx,py,rad,0,7); ctx.fill();
  }
  ctx.restore();

  // ---- central play-lane sheen (keep readable) ----
  const cg=ctx.createLinearGradient(0,gy,0,H);
  cg.addColorStop(0,'rgba(120,110,160,0.10)'); cg.addColorStop(1,'rgba(120,110,160,0)');
  ctx.fillStyle=cg; ctx.fillRect(W*0.28,gy,W*0.44,H-gy);

  // subtle top edge highlight
  ctx.fillStyle=getAccent(0.25); ctx.fillRect(0,gy,W,2*DPR);
}
function scapeRad(biome,horizon,t){ const D=DPR, red=_PREF_REDUCE;
  // --- Halftone dot pattern across sky (subtle grid) ---
  const dg=26*D, dsh=(cam*0.03)%dg; ctx.fillStyle='rgba(255,255,255,0.05)';
  for(let y=dg*0.5;y<horizon;y+=dg){ const fade=1-y/horizon; for(let x=-dsh; x<W+dg; x+=dg){ const r=(0.9+0.7*fade)*D; ctx.beginPath(); ctx.arc(x,y,r,0,6.283); ctx.fill(); } }
  // --- RETRO SUN: horizontal hot gradient stripes ---
  const sx=W*0.5-cam*0.02, sy=horizon*0.62, R=horizon*0.42;
  const hot=['#ffe93b','#ffb31e','#ff5ca8','#c93bff']; ctx.save();
  ctx.beginPath(); ctx.arc(sx,sy,R,0,6.283); ctx.clip();
  const N=14; for(let i=0;i<N;i++){ const yy=sy-R+ (i/N)*2*R, h=(2*R/N)+1; const c=hot[Math.min(hot.length-1,Math.floor(i/N*hot.length))]; ctx.fillStyle=c; const cut=i>N*0.5? (i-N*0.5)/(N*0.5) : 0; ctx.fillRect(sx-R, yy+cut*3*D, 2*R, h-cut*3*D); }
  ctx.restore();
  // sun glow
  ctx.save(); ctx.globalCompositeOperation='lighter'; const gg=ctx.createRadialGradient(sx,sy,R*0.3,sx,sy,R*1.5); gg.addColorStop(0,'rgba(255,120,190,0.35)'); gg.addColorStop(1,'rgba(255,120,190,0)'); ctx.fillStyle=gg; ctx.beginPath(); ctx.arc(sx,sy,R*1.5,0,6.283); ctx.fill(); ctx.restore();
  // --- Memphis squiggles + triangles floating (far layer) ---
  const mg=360*D,mf=0.09,ms=cam*mf,mi0=Math.floor((ms-mg)/mg),mi1=Math.ceil((ms+W+mg)/mg);
  for(let k=mi0;k<=mi1;k++){ const x=k*mg-ms, hy=horizon*(0.18+0.5*hashY(k*3)), col=['#3be0d0','#ffe93b','#ff5ca8'][k%3]; const bob=red?0:Math.sin(t*1.3+k)*6*D;
    if(k%2){ ctx.strokeStyle=col; ctx.lineWidth=3*D; ctx.beginPath(); for(let s=0;s<3;s++){ const px=x+s*10*D; ctx.moveTo(px,hy+bob); ctx.quadraticCurveTo(px+5*D,hy-8*D+bob,px+10*D,hy+bob);} ctx.stroke(); }
    else { ctx.fillStyle=col; ctx.beginPath(); ctx.moveTo(x,hy-9*D+bob); ctx.lineTo(x+9*D,hy+7*D+bob); ctx.lineTo(x-9*D,hy+7*D+bob); ctx.closePath(); ctx.fill(); } }
  // --- Neon lightning bolts scattered (mid) ---
  const bg=300*D,bf=0.16,bs=cam*bf,bi0=Math.floor((bs-bg)/bg),bi1=Math.ceil((bs+W+bg)/bg);
  for(let k=bi0;k<=bi1;k++){ if(hashS(k*7)<0.45) continue; const x=k*bg-bs+80*D*hashY(k*5), y=horizon*(0.3+0.4*hashS(k*2)), sc=(0.8+hashY(k))*D; const flick=red?1:(0.6+0.4*Math.abs(Math.sin(t*3+k))); ctx.save(); ctx.globalCompositeOperation='lighter'; ctx.fillStyle=`rgba(255,233,59,${0.85*flick})`; ctx.beginPath(); ctx.moveTo(x,y); ctx.lineTo(x+9*sc,y); ctx.lineTo(x+3*sc,y+11*sc); ctx.lineTo(x+11*sc,y+11*sc); ctx.lineTo(x-2*sc,y+30*sc); ctx.lineTo(x+2*sc,y+15*sc); ctx.lineTo(x-6*sc,y+15*sc); ctx.closePath(); ctx.fill(); ctx.restore(); }
  // --- Palm-tree silhouettes (near parallax) ---
  const pg=240*D,pf=0.32,psh=cam*pf,pi0=Math.floor((psh-pg)/pg),pi1=Math.ceil((psh+W+pg)/pg);
  for(let k=pi0;k<=pi1;k++){ const x=k*pg-psh+50*D*hashY(k), th=horizon*(0.42+0.25*hashS(k)); ctx.strokeStyle=silhouetteColor('#2a1140',0.85,1); ctx.lineWidth=6*D; ctx.beginPath(); ctx.moveTo(x,horizon); ctx.quadraticCurveTo(x+8*D,horizon-th*0.6,x+3*D,horizon-th); ctx.stroke(); ctx.fillStyle=silhouetteColor('#2a1140',0.85,1); const tx=x+3*D,ty=horizon-th; for(let f=0;f<6;f++){ const a=-0.4+f*0.55, sway=red?0:Math.sin(t*1.1+k+f)*0.06; ctx.beginPath(); ctx.moveTo(tx,ty); ctx.quadraticCurveTo(tx+Math.cos(a+sway)*26*D,ty+Math.sin(a+sway)*26*D-8*D, tx+Math.cos(a+sway)*40*D,ty+Math.sin(a+sway)*40*D+10*D); ctx.lineTo(tx,ty+3*D); ctx.closePath(); ctx.fill(); } }
  // --- RAD tag text ---
  ctx.save(); ctx.font=`900 ${Math.round(horizon*0.16)}px Arial Black, Arial, sans-serif`; ctx.textAlign='center'; ctx.textBaseline='middle'; const tgx=sx, tgy=sy-R*0.15; ctx.lineWidth=5*D; ctx.strokeStyle='#2a1140'; ctx.strokeText('RAD',tgx,tgy); ctx.fillStyle='#3be0d0'; ctx.fillText('RAD',tgx,tgy); ctx.restore();
}
function drawFloorRad(biome,t){ const D=DPR, gy=GROUND();
  // base bold gradient (teal->purple street)
  let g=ctx.createLinearGradient(0,gy,0,H); g.addColorStop(0,'#1b8f86'); g.addColorStop(0.5,'#5a2a8f'); g.addColorStop(1,'#2a1140'); ctx.fillStyle=g; ctx.fillRect(0,gy,W,H-gy);
  // halftone dots on floor (perspective, denser toward bottom)
  const fh=H-gy; ctx.fillStyle='rgba(255,255,255,0.06)'; const rowN=9; for(let r=0;r<rowN;r++){ const yy=gy+ (r/rowN)*(r/rowN)*fh, sp=(14+r*4)*D, off=(cam*0.28)%sp; for(let x=-off;x<W+sp;x+=sp){ ctx.beginPath(); ctx.arc(x,yy,(0.8+r*0.25)*D,0,6.283); ctx.fill(); } }
  // paint-splatter blobs
  const sg=200*D,sf=0.35,ss=cam*sf,si0=Math.floor((ss-sg)/sg),si1=Math.ceil((ss+W+sg)/sg);
  for(let k=si0;k<=si1;k++){ const x=k*sg-ss+120*D*hashY(k*3), y=gy+fh*(0.2+0.7*hashS(k*2)), rad=(10+18*hashY(k))*D, col=['#ff5ca8','#ffe93b','#3be0d0'][k%3]; ctx.fillStyle=col; ctx.globalAlpha=0.55; ctx.beginPath(); ctx.arc(x,y,rad,0,6.283); ctx.fill(); for(let d=0;d<4;d++){ const a=hashS(k*7+d)*6.283, dd=rad*(1.3+hashY(k+d)); ctx.beginPath(); ctx.arc(x+Math.cos(a)*dd,y+Math.sin(a)*dd,rad*0.28,0,6.283); ctx.fill(); } ctx.globalAlpha=1; }
  // thick center stripe (bright, glowing)
  const cx=W*0.5; ctx.save(); ctx.globalCompositeOperation='lighter'; const cw=Math.max(26*D,W*0.05); const cg=ctx.createLinearGradient(cx-cw,0,cx+cw,0); cg.addColorStop(0,'rgba(255,233,59,0)'); cg.addColorStop(0.5,'rgba(255,233,59,0.45)'); cg.addColorStop(1,'rgba(255,233,59,0)'); ctx.fillStyle=cg; ctx.fillRect(cx-cw,gy,cw*2,fh); ctx.restore();
  ctx.fillStyle='#ffe93b'; ctx.fillRect(cx-4*D,gy,8*D,fh);
  // dashed lane accents flanking center
  ctx.fillStyle='rgba(255,255,255,0.5)'; const dash=(cam*0.3)%(40*D); for(let side=-1;side<=1;side+=2){ for(let y=gy-dash;y<H;y+=40*D){ ctx.fillRect(cx+side*cw-3*D,y,6*D,18*D); } }
}

/* ===== ZOMBIES-HOMAGE MAPS wave1 (07-01) ===== */
function scapeRubyRabbit(biome,horizon,t){
  const gh=GROUND(), ac=biome.ac||getAccent(0);
  // --- eldritch purple sky wash over upper region ---
  ctx.save();
  const sg=ctx.createLinearGradient(0,0,0,horizon);
  sg.addColorStop(0,silhouetteColor('#1a0726',0.55,1));
  sg.addColorStop(1,silhouetteColor('#3a0f4a',0.35,1));
  ctx.fillStyle=sg; ctx.fillRect(0,0,W,horizon);
  ctx.restore();
  // --- drifting tentacle-wisps ---
  ctx.save(); ctx.globalCompositeOperation='lighter';
  for(let w=0;w<5;w++){
    const bx=(w*0.23+hashS(w*7))*W, by=horizon*(0.12+0.13*hashY(w*3));
    ctx.beginPath(); ctx.moveTo(bx,by);
    for(let s=1;s<=6;s++){
      const px=bx+Math.sin(t*0.3+w+s*0.7)*22*DPR*s*0.4, py=by+s*16*DPR;
      ctx.lineTo(px,py);
    }
    ctx.strokeStyle=silhouetteColor(hslHex(285,70,55),0.10,1);
    ctx.lineWidth=(7-w)*DPR; ctx.lineCap='round'; ctx.stroke();
  }
  ctx.restore();
  // --- HERO: three-headed beast where the moon would be ---
  const cx=W*0.5, cy=horizon*0.46, sway=Math.sin(t*0.4)*8*DPR, R=Math.min(W,horizon)*0.30;
  ctx.save(); ctx.globalCompositeOperation='lighter';
  const halo=ctx.createRadialGradient(cx,cy,0,cx,cy,R*1.9);
  halo.addColorStop(0,silhouetteColor(hslHex(300,80,55),0.30,1));
  halo.addColorStop(1,silhouetteColor(hslHex(300,80,55),0,1));
  ctx.fillStyle=halo; ctx.beginPath(); ctx.arc(cx,cy,R*1.9,0,7); ctx.fill();
  ctx.restore();
  const body='#0b0410';
  // hulking body mass
  ctx.save(); ctx.translate(sway,0); ctx.fillStyle=body;
  ctx.beginPath(); ctx.ellipse(cx,cy+R*0.55,R*1.25,R*0.85,0,0,7); ctx.fill();
  // three heads
  const heads=[[-R*0.85,-R*0.15,R*0.52],[0,-R*0.55,R*0.62],[R*0.85,-R*0.15,R*0.52]];
  for(let h=0;h<3;h++){
    const hx=cx+heads[h][0], hy=cy+heads[h][1], hr=heads[h][2], bob=Math.sin(t*0.6+h*1.3)*4*DPR;
    ctx.fillStyle=body; ctx.beginPath(); ctx.ellipse(hx,hy+bob,hr,hr*1.15,0,0,7); ctx.fill();
    // ears
    ctx.beginPath(); ctx.moveTo(hx-hr*0.7,hy+bob-hr*0.6); ctx.lineTo(hx-hr*0.35,hy+bob-hr*1.35); ctx.lineTo(hx-hr*0.05,hy+bob-hr*0.7); ctx.fill();
    ctx.beginPath(); ctx.moveTo(hx+hr*0.7,hy+bob-hr*0.6); ctx.lineTo(hx+hr*0.35,hy+bob-hr*1.35); ctx.lineTo(hx+hr*0.05,hy+bob-hr*0.7); ctx.fill();
    // glowing eyes
    ctx.save(); ctx.globalCompositeOperation='lighter';
    const eflk=0.7+0.3*Math.sin(t*3+h);
    for(let e=0;e<2;e++){
      const ex=hx+(e?1:-1)*hr*0.38, ey=hy+bob-hr*0.1;
      const eg=ctx.createRadialGradient(ex,ey,0,ex,ey,hr*0.5);
      eg.addColorStop(0,silhouetteColor(h===1?'#ff2d6f':hslHex(160,90,60),eflk,1));
      eg.addColorStop(1,silhouetteColor('#ff2d6f',0,1));
      ctx.fillStyle=eg; ctx.beginPath(); ctx.arc(ex,ey,hr*0.5,0,7); ctx.fill();
    }
    ctx.restore();
  }
  // central splitting maw
  const maw=cy-R*0.05, gape=(0.5+0.5*Math.sin(t*0.9))*R*0.28;
  ctx.fillStyle='#12000a'; ctx.beginPath();
  ctx.ellipse(cx,maw+R*0.3,R*0.22,gape,0,0,7); ctx.fill();
  ctx.save(); ctx.globalCompositeOperation='lighter';
  ctx.fillStyle=silhouetteColor('#ff2d6f',0.5,1);
  ctx.beginPath(); ctx.ellipse(cx,maw+R*0.3,R*0.10,gape*0.6,0,0,7); ctx.fill();
  ctx.restore();
  ctx.restore();
  // --- art-deco skyscrapers parallax ---
  const gap=300*DPR,f=0.2,shift=cam*f,i0=Math.floor((shift-gap)/gap),i1=Math.ceil((shift+W+gap)/gap);
  for(let k=i0;k<=i1;k++){
    const x=k*gap-shift, idx=((k%97)+97)%97, bw=(110+hashS(idx*5)*70)*DPR;
    const bh=(horizon*0.35+hashY(idx*11)*horizon*0.4);
    const by=gh-bh, col=silhouetteColor('#160a24',0.92,1);
    ctx.fillStyle=col;
    // stepped top
    ctx.fillRect(x,by,bw,bh);
    ctx.fillRect(x+bw*0.2,by-bh*0.10,bw*0.6,bh*0.10);
    ctx.fillRect(x+bw*0.38,by-bh*0.18,bw*0.24,bh*0.08);
    ctx.beginPath(); ctx.moveTo(x+bw*0.42,by-bh*0.18); ctx.lineTo(x+bw*0.5,by-bh*0.30); ctx.lineTo(x+bw*0.58,by-bh*0.18); ctx.fill();
    // lit windows
    ctx.save(); ctx.globalCompositeOperation='lighter';
    for(let wI=0;wI<10;wI++){
      if(hashS(idx*13+wI*3)>0.68){
        const wx=x+bw*(0.18+0.64*hashS(idx+wI)), wy=by+bh*(0.12+0.8*hashY(idx*2+wI));
        ctx.fillStyle=silhouetteColor(hashY(wI)>0.5?ac:'#ffcf6a',0.7,1);
        ctx.fillRect(wx,wy,4*DPR,6*DPR);
      }
    }
    ctx.restore();
  }
  // --- NEON NIGHTCLUB SIGN (rabbit outline + CLUB bar) ---
  const flk=(Math.sin(t*7)>-0.6&&Math.sin(t*2.3)>-0.9)?1:0.25;
  const sx=W*0.5-cam*0.28, sy=gh-horizon*0.30;
  ctx.save(); ctx.globalCompositeOperation='lighter'; ctx.globalAlpha=flk;
  ctx.strokeStyle=silhouetteColor(ac,0.9,1); ctx.lineWidth=3.5*DPR; ctx.lineCap='round'; ctx.lineJoin='round';
  ctx.shadowColor=ac; ctx.shadowBlur=14*DPR;
  const rs=26*DPR;
  ctx.beginPath();
  ctx.ellipse(sx,sy,rs*0.9,rs,0,0.3,Math.PI-0.3); // head arc
  ctx.moveTo(sx-rs*0.5,sy-rs*0.7); ctx.lineTo(sx-rs*0.75,sy-rs*1.9); ctx.lineTo(sx-rs*0.2,sy-rs*0.85); // ear L
  ctx.moveTo(sx+rs*0.5,sy-rs*0.7); ctx.lineTo(sx+rs*0.75,sy-rs*1.9); ctx.lineTo(sx+rs*0.2,sy-rs*0.85); // ear R
  ctx.stroke();
  ctx.font=`900 ${20*DPR}px Orbitron`; ctx.textAlign='center';
  ctx.fillStyle=silhouetteColor('#ff2d6f',0.95,1); ctx.shadowColor='#ff2d6f';
  ctx.fillText('CLUB',sx,sy+rs*1.9);
  ctx.restore();
  // --- diagonal rain ---
  ctx.save(); ctx.globalAlpha=0.35; ctx.strokeStyle=silhouetteColor('#bfd8ff',0.4,1); ctx.lineWidth=1.2*DPR;
  for(let r=0;r<70;r++){
    const rx=((hashS(r*5)*W*1.4)-((t*380*DPR+r*90)%(W*1.4)))+W*0.0;
    const ry=((t*760*DPR+hashY(r)*horizon*3)%(horizon+40*DPR));
    ctx.beginPath(); ctx.moveTo(rx,ry); ctx.lineTo(rx-9*DPR,ry+22*DPR); ctx.stroke();
  }
  ctx.restore();
  // --- warm street-lamp glow pools near ground ---
  ctx.save(); ctx.globalCompositeOperation='lighter';
  const lg=260*DPR,ls=cam*0.32,l0=Math.floor((ls-lg)/lg),l1=Math.ceil((ls+W+lg)/lg);
  for(let k=l0;k<=l1;k++){
    const x=k*lg-ls, gr=ctx.createRadialGradient(x,gh-6*DPR,0,x,gh-6*DPR,70*DPR);
    gr.addColorStop(0,silhouetteColor('#ffb347',0.28,1)); gr.addColorStop(1,silhouetteColor('#ffb347',0,1));
    ctx.fillStyle=gr; ctx.beginPath(); ctx.arc(x,gh-6*DPR,70*DPR,0,7); ctx.fill();
  }
  ctx.restore();
}

function drawFloorRubyRabbit(biome,t){
  const gh=GROUND(), ac=biome.ac||getAccent(0);
  // wet cobblestone base
  ctx.save();
  const fg=ctx.createLinearGradient(0,gh,0,H);
  fg.addColorStop(0,'#0d0616'); fg.addColorStop(1,'#050208');
  ctx.fillStyle=fg; ctx.fillRect(0,gh,W,H-gh);
  // cobble seams (perspective)
  ctx.strokeStyle=silhouetteColor('#2a1840',0.5,1); ctx.lineWidth=1*DPR;
  for(let r=1;r<7;r++){
    const y=gh+(H-gh)*(r/7)*(r/7);
    ctx.beginPath(); ctx.moveTo(0,y); ctx.lineTo(W,y); ctx.stroke();
  }
  const cob=90*DPR,cs=cam*0.5,c0=Math.floor((cs-cob)/cob),c1=Math.ceil((cs+W+cob)/cob);
  for(let k=c0;k<=c1;k++){
    const x=k*cob-cs; ctx.beginPath(); ctx.moveTo(x,gh); ctx.lineTo(x+(x-W*0.5)*0.4,H); ctx.stroke();
  }
  ctx.restore();
  // vertical neon reflections streaking down the wet street
  ctx.save(); ctx.globalCompositeOperation='lighter';
  const rg=210*DPR,rsh=cam*0.28,r0=Math.floor((rsh-rg)/rg),r1=Math.ceil((rsh+W+rg)/rg);
  for(let k=r0;k<=r1;k++){
    const idx=((k%53)+53)%53, x=k*rg-rsh+(hashS(idx*3)-0.5)*60*DPR;
    const col=hashY(idx)>0.5?ac:(hashY(idx*2)>0.5?'#ff2d6f':hslHex(165,90,60));
    const wob=Math.sin(t*2+idx)*4*DPR;
    const grd=ctx.createLinearGradient(x,gh,x+wob,H);
    grd.addColorStop(0,silhouetteColor(col,0.5,1)); grd.addColorStop(1,silhouetteColor(col,0,1));
    ctx.fillStyle=grd; ctx.fillRect(x-3*DPR,gh,7*DPR,H-gh);
  }
  ctx.restore();
  // rain ripple rings
  ctx.save(); ctx.globalCompositeOperation='lighter'; ctx.lineWidth=1.4*DPR;
  for(let p=0;p<16;p++){
    const ph=(t*0.9+hashS(p*7))%1, rad=ph*34*DPR;
    const px=hashS(p*3)*W, py=gh+(H-gh)*(0.15+0.8*hashY(p*5));
    ctx.strokeStyle=silhouetteColor('#bfd8ff',0.30*(1-ph),1);
    ctx.beginPath(); ctx.ellipse(px,py,rad,rad*0.35,0,0,7); ctx.stroke();
  }
  ctx.restore();
}
function scapeShadowman(biome,horizon,t){
  var cx=W*0.5, gy=GROUND();
  // 5. Swirling purple void backdrop
  ctx.save();
  ctx.globalCompositeOperation='lighter';
  for(var i=0;i<7;i++){
    var vx=cx-cam*0.08 + Math.sin(t*0.15+i*1.3)*W*0.22*(0.4+0.1*i);
    var vy=horizon*0.5 + Math.cos(t*0.11+i*0.9)*horizon*0.25;
    var vr=(90+i*46)*DPR;
    var g=ctx.createRadialGradient(vx,vy,0,vx,vy,vr);
    g.addColorStop(0,silhouetteColor(hslHex(275+i*8,0.85,0.5),0.18,1));
    g.addColorStop(1,silhouetteColor('#000000',0,1));
    ctx.fillStyle=g; ctx.beginPath(); ctx.arc(vx,vy,vr,0,7); ctx.fill();
  }
  ctx.restore();
  // purple lightning forks
  if(!_PREF_REDUCE){
    for(var L=0;L<2;L++){
      var seed=Math.floor(t*1.3)+L*17;
      if(hashS(seed)>0.55){
        ctx.save(); ctx.globalCompositeOperation='lighter';
        ctx.strokeStyle=silhouetteColor(hslHex(280,0.9,0.72),0.7,1);
        ctx.lineWidth=2*DPR; ctx.shadowColor=hslHex(285,0.9,0.6); ctx.shadowBlur=14*DPR;
        var lx=cx-cam*0.1+(hashS(seed*3)-0.5)*W*0.7, ly=horizon*0.1;
        ctx.beginPath(); ctx.moveTo(lx,ly);
        for(var s=1;s<7;s++){ lx+=(hashS(seed*3+s)-0.5)*40*DPR; ly+=horizon*0.11; ctx.lineTo(lx,ly);}
        ctx.stroke(); ctx.restore();
      }
    }
  }
  // drifting embers
  ctx.save(); ctx.globalCompositeOperation='lighter';
  for(var e=0;e<26;e++){
    var ex=((hashS(e*2.1)*W*1.4 - cam*0.12 + t*10*DPR)% (W*1.4)); if(ex<0)ex+=W*1.4;
    var ey=gy - (((hashS(e*3.3)*gy + t*22*DPR)%gy));
    var ea=0.35+0.5*Math.sin(t*3+e);
    ctx.fillStyle=silhouetteColor(hslHex(30,0.9,0.6),ea*0.6,1);
    ctx.beginPath(); ctx.arc(ex,ey,(1.2+hashS(e)*1.8)*DPR,0,7); ctx.fill();
  }
  ctx.restore();

  // ===== 1. HERO SHADOW FIGURE — HUGE, CENTERED =====
  var hx=cx - cam*0.06; // slight parallax, always visible
  var hb=Math.sin(t*0.6)*6*DPR;
  var topY=horizon*0.16+hb, botY=gy+40*DPR;
  var shH=botY-topY, shW=shH*0.42;
  // outer violet glow halo
  ctx.save(); ctx.globalCompositeOperation='lighter';
  var hg=ctx.createRadialGradient(hx,topY+shH*0.4,0,hx,topY+shH*0.4,shW*1.6);
  hg.addColorStop(0,silhouetteColor(hslHex(278,0.9,0.55),0.28,1));
  hg.addColorStop(1,silhouetteColor('#000',0,1));
  ctx.fillStyle=hg; ctx.beginPath(); ctx.arc(hx,topY+shH*0.4,shW*1.7,0,7); ctx.fill();
  ctx.restore();
  // body path (shoulders + hooded/horned head)
  function figPath(){
    ctx.beginPath();
    ctx.moveTo(hx-shW*0.9, botY);
    ctx.lineTo(hx-shW*1.05, topY+shH*0.42); // shoulder L
    ctx.quadraticCurveTo(hx-shW*0.72, topY+shH*0.30, hx-shW*0.5, topY+shH*0.28);
    // horn L
    ctx.lineTo(hx-shW*0.62, topY-shH*0.02);
    ctx.quadraticCurveTo(hx-shW*0.30, topY+shH*0.10, hx-shW*0.30, topY+shH*0.20);
    // hood top
    ctx.quadraticCurveTo(hx, topY-shH*0.06, hx+shW*0.30, topY+shH*0.20);
    // horn R
    ctx.quadraticCurveTo(hx+shW*0.30, topY+shH*0.10, hx+shW*0.62, topY-shH*0.02);
    ctx.lineTo(hx+shW*0.5, topY+shH*0.28);
    ctx.quadraticCurveTo(hx+shW*0.72, topY+shH*0.30, hx+shW*1.05, topY+shH*0.42);
    ctx.lineTo(hx+shW*0.9, botY);
    ctx.closePath();
  }
  // inner darkness fill
  var bg=ctx.createLinearGradient(hx,topY,hx,botY);
  bg.addColorStop(0,'#0a0510'); bg.addColorStop(1,'#050208');
  ctx.fillStyle=bg; figPath(); ctx.fill();
  // bright glowing rim/outline
  ctx.save();
  ctx.shadowColor=hslHex(282,0.95,0.6); ctx.shadowBlur=22*DPR;
  ctx.strokeStyle=silhouetteColor(hslHex(283,0.95,0.68),0.95,1); ctx.lineWidth=3.2*DPR;
  figPath(); ctx.stroke();
  ctx.shadowBlur=10*DPR; ctx.strokeStyle=silhouetteColor(hslHex(300,0.9,0.8),0.7,1); ctx.lineWidth=1.4*DPR;
  figPath(); ctx.stroke();
  ctx.restore();
  // two BIG glowing eyes
  ctx.save(); ctx.globalCompositeOperation='lighter';
  var eyY=topY+shH*0.17, eyDx=shW*0.20, er=shW*0.11*(1+0.08*Math.sin(t*4));
  for(var k=-1;k<=1;k+=2){
    var eg=ctx.createRadialGradient(hx+k*eyDx,eyY,0,hx+k*eyDx,eyY,er*2.4);
    eg.addColorStop(0,'#ffffff'); eg.addColorStop(0.35,hslHex(50,1,0.65));
    eg.addColorStop(1,silhouetteColor('#000',0,1));
    ctx.fillStyle=eg; ctx.beginPath(); ctx.arc(hx+k*eyDx,eyY,er*2.4,0,7); ctx.fill();
  }
  ctx.restore();
  // wispy smoke tendrils
  ctx.save(); ctx.globalCompositeOperation='lighter'; ctx.lineWidth=6*DPR; ctx.lineCap='round';
  for(var w=0;w<5;w++){
    var wx=hx+(w-2)*shW*0.5, wy=botY-shH*0.05;
    ctx.strokeStyle=silhouetteColor(hslHex(275,0.7,0.45),0.18,1);
    ctx.beginPath(); ctx.moveTo(wx,wy);
    ctx.quadraticCurveTo(wx+Math.sin(t*1.2+w)*30*DPR, wy-shH*0.25, wx+Math.sin(t*0.8+w)*50*DPR, wy-shH*0.5);
    ctx.stroke();
  }
  ctx.restore();

  // ===== 2. ONE-EYED GOLDEN TRIANGLE — upper center-right =====
  var tx=cx+W*0.24 - cam*0.1, ty=horizon*0.34+Math.sin(t*1.1)*10*DPR;
  var TS=Math.min(W,H)*0.16; // side length-ish
  var h3=TS*0.87;
  ctx.save();
  // radiating glow
  ctx.save(); ctx.globalCompositeOperation='lighter';
  var tg=ctx.createRadialGradient(tx,ty,0,tx,ty,TS*1.5);
  tg.addColorStop(0,silhouetteColor(hslHex(48,1,0.6),0.5,1));
  tg.addColorStop(1,silhouetteColor('#000',0,1));
  ctx.fillStyle=tg; ctx.beginPath(); ctx.arc(tx,ty,TS*1.5,0,7); ctx.fill();
  ctx.restore();
  // radiating rays
  ctx.save(); ctx.globalCompositeOperation='lighter'; ctx.strokeStyle=silhouetteColor(hslHex(50,1,0.7),0.25,1); ctx.lineWidth=2*DPR;
  for(var r=0;r<12;r++){ var a=r/12*6.283+t*0.2; ctx.beginPath(); ctx.moveTo(tx+Math.cos(a)*TS*0.9,ty+Math.sin(a)*TS*0.9); ctx.lineTo(tx+Math.cos(a)*TS*1.35,ty+Math.sin(a)*TS*1.35); ctx.stroke(); }
  ctx.restore();
  // triangle body (equilateral, apex up)
  var ax=tx, ay=ty-h3*0.6, blx=tx-TS*0.5, bly=ty+h3*0.4, brx=tx+TS*0.5, bry=ty+h3*0.4;
  function triPath(){ ctx.beginPath(); ctx.moveTo(ax,ay); ctx.lineTo(blx,bly); ctx.lineTo(brx,bry); ctx.closePath(); }
  var gg=ctx.createLinearGradient(ax,ay,tx,bly);
  gg.addColorStop(0,hslHex(52,1,0.72)); gg.addColorStop(1,hslHex(42,0.95,0.5));
  ctx.save(); ctx.shadowColor=hslHex(48,1,0.6); ctx.shadowBlur=20*DPR;
  ctx.fillStyle=gg; triPath(); ctx.fill(); ctx.restore();
  // brick-line etching
  ctx.save(); triPath(); ctx.clip();
  ctx.strokeStyle=silhouetteColor('#3a2600',0.4,1); ctx.lineWidth=1*DPR;
  for(var by=1;by<7;by++){ var yy=ay+(bly-ay)*by/7; ctx.beginPath(); ctx.moveTo(tx-TS,yy); ctx.lineTo(tx+TS,yy); ctx.stroke(); }
  ctx.restore();
  ctx.strokeStyle=hslHex(40,0.9,0.35); ctx.lineWidth=2*DPR; triPath(); ctx.stroke();
  // stick arms + legs (black)
  ctx.strokeStyle='#0a0a0a'; ctx.lineWidth=2.6*DPR; ctx.lineCap='round';
  var sw=Math.sin(t*2)*8*DPR;
  ctx.beginPath(); ctx.moveTo(blx+TS*0.1,bly-h3*0.15); ctx.lineTo(blx-TS*0.25,bly+sw); ctx.stroke();
  ctx.beginPath(); ctx.moveTo(brx-TS*0.1,bry-h3*0.15); ctx.lineTo(brx+TS*0.25,bry-sw); ctx.stroke();
  ctx.beginPath(); ctx.moveTo(tx-TS*0.14,bly); ctx.lineTo(tx-TS*0.2,bly+TS*0.4); ctx.stroke();
  ctx.beginPath(); ctx.moveTo(tx+TS*0.14,bry); ctx.lineTo(tx+TS*0.2,bry+TS*0.4); ctx.stroke();
  // central eye
  var blink=(Math.sin(t*1.7)>0.94)?0.12:1;
  ctx.fillStyle='#ffffff'; ctx.beginPath(); ctx.ellipse(tx,ty+h3*0.02,TS*0.26,TS*0.2*blink,0,0,7); ctx.fill();
  ctx.strokeStyle='#0a0a0a'; ctx.lineWidth=2*DPR; ctx.stroke();
  var dartx=Math.sin(t*0.9)*TS*0.1, darty=Math.cos(t*1.3)*TS*0.05;
  ctx.fillStyle='#111'; ctx.beginPath(); ctx.arc(tx+dartx,ty+h3*0.02+darty,TS*0.09*blink+0.5,0,7); ctx.fill();
  ctx.fillStyle='#fff'; ctx.beginPath(); ctx.arc(tx+dartx-TS*0.03,ty-h3*0.01+darty,TS*0.03,0,7); ctx.fill();
  // top hat
  ctx.fillStyle='#0a0a0a';
  rr(ctx,ax-TS*0.28,ay-2*DPR,TS*0.56,4*DPR,2*DPR); ctx.fill();
  rr(ctx,ax-TS*0.16,ay-TS*0.32,TS*0.32,TS*0.32,2*DPR); ctx.fill();
  ctx.strokeStyle=hslHex(48,1,0.6); ctx.lineWidth=3*DPR; ctx.beginPath(); ctx.moveTo(ax-TS*0.16,ay-TS*0.08); ctx.lineTo(ax+TS*0.16,ay-TS*0.08); ctx.stroke();
  // bow tie
  ctx.fillStyle=hslHex(0,0.8,0.4);
  ctx.beginPath(); ctx.moveTo(tx,bry-2*DPR); ctx.lineTo(tx-TS*0.14,bry+TS*0.06); ctx.lineTo(tx-TS*0.14,bry-TS*0.1); ctx.closePath(); ctx.fill();
  ctx.beginPath(); ctx.moveTo(tx,bry-2*DPR); ctx.lineTo(tx+TS*0.14,bry+TS*0.06); ctx.lineTo(tx+TS*0.14,bry-TS*0.1); ctx.closePath(); ctx.fill();
  ctx.restore();

  // ===== 3. Pentagram ritual circles =====
  function pentagram(px,py,pr,rot,col){
    ctx.save(); ctx.globalCompositeOperation='lighter';
    ctx.shadowColor=col; ctx.shadowBlur=14*DPR;
    ctx.strokeStyle=silhouetteColor(col,0.75,1); ctx.lineWidth=2*DPR;
    ctx.beginPath(); ctx.arc(px,py,pr,0,7); ctx.stroke();
    ctx.beginPath();
    for(var i=0;i<=5;i++){ var a=rot - 1.5708 + i*2*(4*Math.PI/5)/2*2/2; a=rot-1.5708+i*(4*Math.PI/5); var xx=px+Math.cos(a)*pr, yy=py+Math.sin(a)*pr; if(i===0)ctx.moveTo(xx,yy); else ctx.lineTo(xx,yy); }
    ctx.closePath(); ctx.stroke(); ctx.restore();
  }
  pentagram(cx-W*0.30-cam*0.12, horizon*0.5, Math.min(W,H)*0.07, t*0.3, hslHex(0,0.85,0.5));
  pentagram(cx+W*0.05-cam*0.12, horizon*0.22, Math.min(W,H)*0.05, -t*0.4, hslHex(280,0.9,0.6));
  // big seal
  pentagram(cx-W*0.02-cam*0.09, horizon*0.62, Math.min(W,H)*0.11, t*0.15, hslHex(275,0.9,0.55));

  // ===== 4. MANY floating candles =====
  for(var c=0;c<9;c++){
    var cX=(hashS(c*4.7)*W*1.3 - cam*0.16); cX=((cX%(W*1.3))+W*1.3)%(W*1.3);
    var baseY=horizon*0.3+hashS(c*2.9)*horizon*0.55;
    var cY=baseY+Math.sin(t*1.3+c*1.7)*14*DPR;
    var cw=6*DPR, ch=22*DPR;
    // wax
    ctx.fillStyle=silhouetteColor('#e8e0d0',0.85,1); rr(ctx,cX-cw/2,cY,cw,ch,2*DPR); ctx.fill();
    ctx.fillStyle=silhouetteColor('#c8bfa8',0.6,1); rr(ctx,cX-cw/2,cY,cw*0.4,ch,2*DPR); ctx.fill();
    // flame + glow
    var fl=1+0.25*Math.sin(t*9+c*3);
    ctx.save(); ctx.globalCompositeOperation='lighter';
    var fg=ctx.createRadialGradient(cX,cY-6*DPR,0,cX,cY-6*DPR,20*DPR*fl);
    fg.addColorStop(0,silhouetteColor(hslHex(40,1,0.7),0.9,1)); fg.addColorStop(1,silhouetteColor('#000',0,1));
    ctx.fillStyle=fg; ctx.beginPath(); ctx.arc(cX,cY-6*DPR,20*DPR*fl,0,7); ctx.fill();
    ctx.fillStyle=hslHex(45,1,0.75); ctx.beginPath(); ctx.ellipse(cX,cY-7*DPR,2.5*DPR,6*DPR*fl,0,0,7); ctx.fill();
    ctx.fillStyle=hslHex(20,1,0.55); ctx.beginPath(); ctx.ellipse(cX,cY-5*DPR,1.6*DPR,4*DPR*fl,0,0,7); ctx.fill();
    ctx.restore();
  }
}

function drawFloorShadowman(biome,t){
  var gy=GROUND();
  // cracked dark ground
  var fg=ctx.createLinearGradient(0,gy,0,H);
  fg.addColorStop(0,'#0d0715'); fg.addColorStop(1,'#050109');
  ctx.fillStyle=fg; ctx.fillRect(0,gy,W,H-gy);
  // cracks with purple seep
  ctx.save();
  for(var i=0;i<10;i++){
    var bx=((hashS(i*3.1)*W - cam*0.5)%W+W)%W;
    var by=gy+ (H-gy)*hashS(i*5.7);
    ctx.strokeStyle=silhouetteColor('#000',0.6,1); ctx.lineWidth=2*DPR;
    ctx.beginPath(); ctx.moveTo(bx,by);
    var px=bx,py=by;
    for(var s=0;s<4;s++){ px+=(hashS(i*7+s)-0.5)*60*DPR; py+=hashS(i*9+s)*20*DPR; ctx.lineTo(px,py);}
    ctx.stroke();
  }
  // purple energy seeping up from cracks
  ctx.globalCompositeOperation='lighter';
  for(var e=0;e<14;e++){
    var ex=((hashS(e*2.3)*W - cam*0.5)%W+W)%W;
    var ey=gy+ (H-gy)*hashS(e*4.1)*0.6;
    var ea=0.3+0.4*Math.sin(t*2+e);
    var eg=ctx.createRadialGradient(ex,ey,0,ex,ey,30*DPR);
    eg.addColorStop(0,silhouetteColor(hslHex(278,0.9,0.55),ea*0.5,1)); eg.addColorStop(1,silhouetteColor('#000',0,1));
    ctx.fillStyle=eg; ctx.beginPath(); ctx.arc(ex,ey,30*DPR,0,7); ctx.fill();
  }
  ctx.restore();
  // big glowing pentagram seal on floor
  var sx=W*0.5-cam*0.5, sy=gy+(H-gy)*0.55, sr=Math.min(W,(H-gy)*1.6)*0.22;
  ctx.save(); ctx.globalCompositeOperation='lighter';
  ctx.shadowColor=hslHex(0,0.85,0.5); ctx.shadowBlur=20*DPR;
  ctx.strokeStyle=silhouetteColor(hslHex(0,0.85,0.55),0.7,1); ctx.lineWidth=3*DPR;
  ctx.save(); ctx.translate(sx,sy); ctx.scale(1,0.4);
  ctx.beginPath(); ctx.arc(0,0,sr,0,7); ctx.stroke();
  ctx.beginPath(); ctx.arc(0,0,sr*0.8,0,7); ctx.stroke();
  var rot=t*0.1;
  ctx.strokeStyle=silhouetteColor(hslHex(278,0.9,0.6),0.75,1);
  ctx.beginPath();
  for(var i=0;i<=5;i++){ var a=rot-1.5708+i*(4*Math.PI/5); var xx=Math.cos(a)*sr*0.8, yy=Math.sin(a)*sr*0.8; if(i===0)ctx.moveTo(xx,yy); else ctx.lineTo(xx,yy);}
  ctx.closePath(); ctx.stroke();
  ctx.restore(); ctx.restore();
  // standing candles on floor
  for(var c=0;c<7;c++){
    var cX=((hashS(c*6.1)*W - cam*0.5)%W+W)%W;
    var cY=gy+(H-gy)*(0.2+hashS(c*3.3)*0.5);
    var cw=7*DPR, ch=26*DPR;
    ctx.fillStyle=silhouetteColor('#e8e0d0',0.9,1); rr(ctx,cX-cw/2,cY,cw,ch,2*DPR); ctx.fill();
    ctx.fillStyle=silhouetteColor('#6b5f48',0.7,1); ctx.fillRect(cX-cw/2,cY+ch,cw,3*DPR);
    var fl=1+0.3*Math.sin(t*10+c*2);
    ctx.save(); ctx.globalCompositeOperation='lighter';
    var fgg=ctx.createRadialGradient(cX,cY-7*DPR,0,cX,cY-7*DPR,22*DPR*fl);
    fgg.addColorStop(0,silhouetteColor(hslHex(38,1,0.68),0.95,1)); fgg.addColorStop(1,silhouetteColor('#000',0,1));
    ctx.fillStyle=fgg; ctx.beginPath(); ctx.arc(cX,cY-7*DPR,22*DPR*fl,0,7); ctx.fill();
    ctx.fillStyle=hslHex(45,1,0.75); ctx.beginPath(); ctx.ellipse(cX,cY-8*DPR,3*DPR,7*DPR*fl,0,0,7); ctx.fill();
    ctx.restore();
  }
}
function scapeAurora(biome, horizon, t){
  var gy = GROUND();
  // ---- dense starfield (drawn first, visible through translucent aurora) ----
  ctx.save();
  var STARS = 150;
  for(var s=0;s<STARS;s++){
    var sx = hashS(s*2+1)*W;
    var sy = hashY(s*3+2)*(horizon*0.98);
    var stw = 0.4 + hashS(s*5+3)*1.3;
    var tw = 0.45 + 0.55*Math.abs(Math.sin(t*0.8 + hashY(s*7+4)*6.28));
    ctx.globalAlpha = (0.25 + hashS(s+9)*0.7) * tw;
    ctx.fillStyle = hashS(s*11+5) > 0.85 ? '#bfe0ff' : '#ffffff';
    ctx.beginPath();
    ctx.arc((sx - cam*0.05), sy, stw*DPR, 0, 6.283);
    ctx.fill();
  }
  ctx.globalAlpha = 1;
  // ---- crisp moon ----
  var mx = W*0.80 - cam*0.06, my = horizon*0.26, mr = 20*DPR;
  var mg = ctx.createRadialGradient(mx, my, mr*0.2, mx, my, mr*3.4);
  mg.addColorStop(0,'rgba(220,235,255,0.55)');
  mg.addColorStop(1,'rgba(220,235,255,0)');
  ctx.fillStyle = mg; ctx.beginPath(); ctx.arc(mx,my,mr*3.4,0,6.283); ctx.fill();
  ctx.fillStyle = '#eaf1ff'; ctx.beginPath(); ctx.arc(mx,my,mr,0,6.283); ctx.fill();
  ctx.fillStyle = 'rgba(180,200,225,0.5)';
  ctx.beginPath(); ctx.arc(mx-mr*0.35,my-mr*0.2,mr*0.22,0,6.283); ctx.fill();
  ctx.beginPath(); ctx.arc(mx+mr*0.3,my+mr*0.25,mr*0.15,0,6.283); ctx.fill();
  ctx.restore();

  // ---- AURORA CURTAINS (many thin vertical rays along a wavy baseline) ----
  // helper via closure: draws one curtain of packed vertical rays
  function drawCurtain(cfg){
    var baseY = cfg.baseY, hueBot = cfg.hueBot, hueTop = cfg.hueTop;
    var maxH = cfg.maxH, aMul = cfg.aMul, depth = cfg.depth, seed = cfg.seed;
    var px = cam * depth;
    ctx.save();
    ctx.globalCompositeOperation = 'lighter';
    var stepW = 3.0*DPR;              // ray spacing
    var rayW = 4.2*DPR;               // ray width (overlap for softness)
    var startX = -60*DPR, endX = W + 60*DPR;
    var n = Math.floor((endX-startX)/stepW);
    for(var r=0;r<n;r++){
      var wx = startX + r*stepW - px%stepW;
      var u = (wx + px) / (W||1);
      // wavy fold baseline (fabric folding)
      var fold = Math.sin(u*3.4 + t*0.35 + seed) * (26*DPR)
               + Math.sin(u*7.9 - t*0.22 + seed*1.7) * (12*DPR);
      var by = baseY + fold;
      // per-ray height + brightness noise (shimmer)
      var nz = hashS((r*13 + Math.floor(seed*97)) );
      var nz2 = hashY((r*7 + Math.floor(seed*53)) );
      var shimmer = 0.55 + 0.45*Math.sin(t*1.6 + r*0.5 + nz2*6.28);
      var rh = maxH * (0.45 + 0.55*nz) * (0.75 + 0.25*shimmer);
      // fold-facing brightness: brighter where curtain faces us
      var face = 0.55 + 0.45*Math.cos(u*3.4 + t*0.35 + seed);
      var topY = by - rh;
      var g = ctx.createLinearGradient(0, by, 0, topY);
      var ab = (0.16 + 0.10*nz) * aMul * shimmer * face;   // bottom green alpha
      g.addColorStop(0.00, hslHex(198,80,60)+alpha(ab*0.5));  // faint cyan foot
      g.addColorStop(0.08, hslHex(hueBot,90,58)+alpha(ab*1.0));
      g.addColorStop(0.35, hslHex(hueBot,85,55)+alpha(ab*0.55));
      g.addColorStop(0.70, hslHex((hueBot+hueTop)/2,70,58)+alpha(ab*0.28));
      g.addColorStop(0.90, hslHex(hueTop,80,62)+alpha(ab*0.14));
      g.addColorStop(1.00, hslHex(hueTop,80,62)+alpha(0));
      ctx.fillStyle = g;
      ctx.fillRect(wx - rayW*0.5, topY, rayW, rh);
    }
    ctx.restore();
  }
  function alpha(a){ a = Math.max(0,Math.min(1,a)); var v=Math.round(a*255).toString(16); return v.length<2?'0'+v:v; }

  var band = horizon;
  if(!_PREF_REDUCE){
    // back faint purple curtain (depth)
    drawCurtain({baseY: band*0.52, maxH: band*0.42, hueBot:270, hueTop:320, aMul:0.6, depth:0.10, seed:2.1});
    // main green curtain
    drawCurtain({baseY: band*0.62, maxH: band*0.55, hueBot:145, hueTop:330, aMul:1.0, depth:0.16, seed:0.0});
    // front brighter narrower green-with-red-top curtain
    drawCurtain({baseY: band*0.70, maxH: band*0.40, hueBot:150, hueTop:340, aMul:0.85, depth:0.22, seed:4.7});
  } else {
    drawCurtain({baseY: band*0.62, maxH: band*0.5, hueBot:145, hueTop:330, aMul:0.8, depth:0.16, seed:0.0});
  }

  // ---- jagged snowy mountains + pines along horizon ----
  ctx.save();
  var mpx = cam*0.28;
  ctx.fillStyle = silhouetteColor('#0a1020', 0.92, 0.9);
  ctx.beginPath();
  ctx.moveTo(-40*DPR, gy);
  var peaks = 9, pw = (W+80*DPR)/peaks;
  ctx.lineTo(-40*DPR, gy - 30*DPR);
  for(var p=0;p<=peaks;p++){
    var pxp = -40*DPR + p*pw - mpx%pw;
    var ph = (55 + hashS(p*3+1)*95)*DPR;
    ctx.lineTo(pxp - pw*0.5, gy - 12*DPR);
    ctx.lineTo(pxp, gy - ph);
  }
  ctx.lineTo(W+40*DPR, gy); ctx.closePath(); ctx.fill();
  // snow caps
  ctx.fillStyle = 'rgba(200,220,240,0.14)';
  for(var q=0;q<=peaks;q++){
    var qx = -40*DPR + q*pw - mpx%pw;
    var qh = (55 + hashS(q*3+1)*95)*DPR;
    ctx.beginPath();
    ctx.moveTo(qx, gy-qh);
    ctx.lineTo(qx-9*DPR, gy-qh+16*DPR);
    ctx.lineTo(qx+9*DPR, gy-qh+16*DPR);
    ctx.closePath(); ctx.fill();
  }
  // pine trees (nearer, darker)
  ctx.fillStyle = silhouetteColor('#060a14', 0.95, 0.85);
  var pines = 14, tpx = cam*0.34;
  for(var tr=0;tr<pines;tr++){
    var txp = (hashS(tr*5+2)*(W+120*DPR)) - 60*DPR - tpx%(W+120*DPR);
    if(txp<-60*DPR) txp += (W+120*DPR);
    var th = (26 + hashY(tr*4+3)*30)*DPR, tw2 = th*0.42;
    ctx.beginPath();
    ctx.moveTo(txp, gy-th);
    ctx.lineTo(txp-tw2, gy-6*DPR);
    ctx.lineTo(txp+tw2, gy-6*DPR);
    ctx.closePath(); ctx.fill();
  }
  ctx.restore();
}

function drawFloorAurora(biome, t){
  var gy = GROUND();
  // ---- frozen ice/snow surface: dark blue-white ----
  var fg = ctx.createLinearGradient(0, gy, 0, H);
  fg.addColorStop(0, '#0c1626');
  fg.addColorStop(0.35, '#10203a');
  fg.addColorStop(1, '#1c3450');
  ctx.fillStyle = fg;
  ctx.fillRect(0, gy, W, H-gy);

  // ---- soft green aurora reflection (blurred vertical smears, low alpha) ----
  ctx.save();
  ctx.globalCompositeOperation = 'lighter';
  var smears = 26;
  for(var i=0;i<smears;i++){
    var sx = ((i/smears)*W + Math.sin(t*0.4+i*0.6)*24*DPR - cam*0.16);
    sx = ((sx % (W+80*DPR))+(W+80*DPR))%(W+80*DPR) - 40*DPR;
    var sw = (30+hashS(i*3+1)*70)*DPR;
    var sh = (40+hashY(i*2+2)*90)*DPR;
    var puls = 0.5+0.5*Math.sin(t*1.2 + i*0.7);
    var hue = hashS(i+7)>0.75 ? 320 : 150;
    var rg = ctx.createLinearGradient(0, gy, 0, gy+sh);
    rg.addColorStop(0, hslHex(hue,80,55)+ah((0.10+0.06*hashS(i+3))*puls));
    rg.addColorStop(0.5, hslHex(hue,75,52)+ah((0.05+0.03*hashY(i+4))*puls));
    rg.addColorStop(1, hslHex(hue,75,52)+ah(0));
    ctx.fillStyle = rg;
    ctx.fillRect(sx - sw*0.5, gy, sw, sh);
  }
  ctx.restore();
  function ah(a){ a=Math.max(0,Math.min(1,a)); var v=Math.round(a*255).toString(16); return v.length<2?'0'+v:v; }

  // ---- faint sparkle on ice ----
  ctx.save();
  var sp = 40;
  for(var k=0;k<sp;k++){
    var kx = hashS(k*4+1)*W;
    var ky = gy + hashY(k*3+2)*(H-gy);
    var tw = Math.abs(Math.sin(t*2.0 + hashS(k*6+5)*6.28));
    ctx.globalAlpha = 0.05 + tw*0.35*(1-(ky-gy)/(H-gy+1));
    ctx.fillStyle = k%5===0 ? '#bfe8d8' : '#dff0ff';
    ctx.beginPath(); ctx.arc(kx, ky, (0.5+hashY(k+8)*1.0)*DPR, 0, 6.283); ctx.fill();
  }
  ctx.globalAlpha = 1;
  // ---- drifting snow near surface ----
  var snow = 30;
  for(var f=0;f<snow;f++){
    var fx = (hashS(f*7+1)*W + Math.sin(t*0.6+f)*12*DPR);
    var fy = gy + ((hashY(f*5+2)*(H-gy)) + Math.sin(t*0.5+f*1.3)*6*DPR);
    ctx.globalAlpha = 0.10 + hashS(f+2)*0.22;
    ctx.fillStyle = '#eef6ff';
    ctx.beginPath(); ctx.arc(fx, fy, (0.7+hashY(f+4)*1.2)*DPR, 0, 6.283); ctx.fill();
  }
  ctx.globalAlpha = 1;
  ctx.restore();
}
function scapeMoonbase(biome,horizon,t){
  const hy=GROUND?GROUND():H*0.82, cx=W*0.5;
  // ---- deep space wash above horizon (darken toward black) ----
  ctx.save();
  const sg=ctx.createLinearGradient(0,0,0,hy);
  sg.addColorStop(0,'rgba(2,3,12,0.55)'); sg.addColorStop(1,'rgba(2,3,12,0)');
  ctx.fillStyle=sg; ctx.fillRect(0,0,W,hy);
  ctx.restore();
  // ---- MILKY-WAY band (soft diagonal glow) ----
  ctx.save(); ctx.globalCompositeOperation='lighter';
  ctx.translate(W*0.62,H*0.30); ctx.rotate(-0.5);
  const mw=ctx.createLinearGradient(-W,0,W,0);
  mw.addColorStop(0,'rgba(120,140,210,0)'); mw.addColorStop(0.5,'rgba(150,170,235,0.14)'); mw.addColorStop(1,'rgba(120,140,210,0)');
  ctx.fillStyle=mw; ctx.fillRect(-W,-H*0.16,2*W,H*0.32); ctx.restore();
  // ---- STARFIELD (parallax, twinkle) ----
  ctx.save();
  for(let i=0;i<140;i++){
    const f=0.06+((i%3)*0.03), sx=((hashS(i*2.1)*W*1.6 - cam*f)%(W*1.6)+W*1.6)%(W*1.6)-W*0.3;
    const sy=hashY(i*3.7)*hy*0.96, tw=0.4+0.6*Math.abs(Math.sin(t*1.3+i)), r=hashS(i*5.3)*1.3+0.3;
    ctx.globalAlpha=tw*(0.5+r*0.4); ctx.fillStyle=i%9===0?'#bfe0ff':'#ffffff';
    ctx.beginPath(); ctx.arc(sx,sy,r,0,7); ctx.fill();
  }
  ctx.restore();
  // ---- distant brilliant SUN ----
  const sunx=W*0.12-cam*0.05, suny=hy*0.30;
  ctx.save(); ctx.globalCompositeOperation='lighter';
  const sgl=ctx.createRadialGradient(sunx,suny,0,sunx,suny,60);
  sgl.addColorStop(0,'rgba(255,255,255,0.95)'); sgl.addColorStop(0.3,'rgba(255,250,220,0.5)'); sgl.addColorStop(1,'rgba(255,240,200,0)');
  ctx.fillStyle=sgl; ctx.beginPath(); ctx.arc(sunx,suny,60,0,7); ctx.fill();
  ctx.fillStyle='#fffef2'; ctx.beginPath(); ctx.arc(sunx,suny,7,0,7); ctx.fill(); ctx.restore();
  // ---- HERO: planet EARTH ----
  const ex=W*0.72-cam*0.05, ey=hy*0.34, R=Math.min(W,H)*0.20;
  ctx.save();
  // atmospheric rim glow
  ctx.globalCompositeOperation='lighter';
  const atm=ctx.createRadialGradient(ex,ey,R*0.82,ex,ey,R*1.28);
  atm.addColorStop(0,'rgba(90,170,255,0)'); atm.addColorStop(0.55,'rgba(90,175,255,0.35)'); atm.addColorStop(1,'rgba(120,200,255,0)');
  ctx.fillStyle=atm; ctx.beginPath(); ctx.arc(ex,ey,R*1.28,0,7); ctx.fill();
  ctx.globalCompositeOperation='source-over';
  // ocean sphere
  const oc=ctx.createRadialGradient(ex-R*0.35,ey-R*0.35,R*0.15,ex,ey,R);
  oc.addColorStop(0,'#3aa0e6'); oc.addColorStop(0.6,'#1667b8'); oc.addColorStop(1,'#0a3a78');
  ctx.fillStyle=oc; ctx.beginPath(); ctx.arc(ex,ey,R,0,7); ctx.fill();
  // clip to globe for continents + clouds
  ctx.save(); ctx.beginPath(); ctx.arc(ex,ey,R,0,7); ctx.clip();
  const spin=t*0.05;
  for(let c=0;c<7;c++){
    const a=c*0.9+spin, px=ex+Math.cos(a)*R*(0.2+hashS(c)*0.55), py=ey+Math.sin(c*2.3)*R*0.55;
    ctx.fillStyle=c%2?'#2f9e5a':'#3fae5f'; ctx.globalAlpha=0.9;
    ctx.beginPath(); ctx.ellipse(px,py,R*(0.16+hashS(c*3)*0.2),R*(0.12+hashY(c*3)*0.16),c,0,7); ctx.fill();
  }
  // white cloud swirls
  ctx.globalAlpha=0.5; ctx.fillStyle='#ffffff';
  for(let w=0;w<6;w++){
    const a=w*1.1-t*0.08, wx=ex+Math.cos(a)*R*0.6, wy=ey+Math.sin(w*1.7)*R*0.6;
    ctx.beginPath(); ctx.ellipse(wx,wy,R*0.28,R*0.09,a,0,7); ctx.fill();
  }
  // terminator shadow
  ctx.globalAlpha=1; const sh=ctx.createLinearGradient(ex-R,0,ex+R,0);
  sh.addColorStop(0,'rgba(0,0,10,0)'); sh.addColorStop(0.62,'rgba(0,0,15,0.15)'); sh.addColorStop(1,'rgba(0,0,20,0.72)');
  ctx.fillStyle=sh; ctx.fillRect(ex-R,ey-R,2*R,2*R);
  ctx.restore(); ctx.restore();
  // ---- shooting COMET ----
  const cp=(t*0.28)%6; if(cp<1.4){ const q=cp/1.4, hx=W*(1.05-q*0.8), hyc=hy*(0.12+q*0.28);
    ctx.save(); ctx.globalCompositeOperation='lighter'; ctx.strokeStyle='rgba(180,220,255,0.8)'; ctx.lineWidth=2;
    ctx.beginPath(); ctx.moveTo(hx,hyc); ctx.lineTo(hx+70,hyc-24); ctx.stroke();
    ctx.fillStyle='#eaf6ff'; ctx.beginPath(); ctx.arc(hx,hyc,2.4,0,7); ctx.fill(); ctx.restore(); }
  // ---- cratered lunar MOUNTAINS (parallax, 2 layers) ----
  const layer=(f,base,col,seed)=>{ ctx.save(); ctx.fillStyle=col; ctx.beginPath(); ctx.moveTo(0,hy);
    const step=90; for(let x=-100;x<=W+100;x+=step){ const wx=x, k=Math.floor((wx+cam*f)/step);
      const yh=base-(hashY(k*1.7+seed)*base*0.55+8); ctx.lineTo(wx - (cam*f)%step, hy-yh*0.0 - (base-yh)); }
    ctx.lineTo(W+100,hy); ctx.closePath(); ctx.fill(); ctx.restore(); };
  layer(0.14,H*0.16,'#3b3f4a',3.1); layer(0.24,H*0.11,'#2a2d36',7.7);
  // ---- DOME STATION on horizon ----
  ctx.save();
  const bf=0.3, bob=Math.sin(t*0.8)*0; // static
  const domes=[[0.30,34,'#7fe0ff'],[0.46,52,'#a6ff8f'],[0.60,30,'#ffcf6b']];
  for(let d=0;d<domes.length;d++){ const dm=domes[d], dx=W*dm[0]-((cam*bf)%W), dr=dm[1], dy=hy;
    ctx.fillStyle='#4a4f5c'; rr(ctx,dx-dr*1.05,dy-6,dr*2.1,10,3); ctx.fill();
    const dg=ctx.createLinearGradient(dx,dy-dr,dx,dy); dg.addColorStop(0,silhouetteColor(dm[2],0.85,1.2)); dg.addColorStop(1,'#2c3140');
    ctx.fillStyle=dg; ctx.beginPath(); ctx.arc(dx,dy-4,dr,Math.PI,0); ctx.fill();
    ctx.save(); ctx.globalCompositeOperation='lighter'; ctx.strokeStyle=dm[2]; ctx.globalAlpha=0.35; ctx.lineWidth=1;
    ctx.beginPath(); ctx.arc(dx,dy-4,dr,Math.PI,0); ctx.stroke();
    ctx.beginPath(); ctx.moveTo(dx-dr,dy-4); ctx.lineTo(dx+dr,dy-4); ctx.moveTo(dx,dy-4); ctx.lineTo(dx,dy-4-dr); ctx.stroke(); ctx.restore(); }
  // antenna towers + blinking lights
  for(let a=0;a<4;a++){ const ax=W*(0.18+a*0.2)-((cam*bf)%W), ah=26+hashS(a)*22;
    ctx.strokeStyle='#5a606e'; ctx.lineWidth=2; ctx.beginPath(); ctx.moveTo(ax,hy); ctx.lineTo(ax,hy-ah); ctx.stroke();
    const bl=Math.sin(t*2+a)>0.4; ctx.fillStyle=bl?'#ff5a5a':'#661c1c';
    if(bl){ ctx.save(); ctx.globalCompositeOperation='lighter'; ctx.fillStyle='rgba(255,90,90,0.6)'; ctx.beginPath(); ctx.arc(ax,hy-ah,4,0,7); ctx.fill(); ctx.restore(); }
    ctx.fillStyle=bl?'#ff7a7a':'#772222'; ctx.beginPath(); ctx.arc(ax,hy-ah,1.8,0,7); ctx.fill(); }
  ctx.restore();
  // ---- drifting satellite / astronaut speck ----
  const stx=(W*0.25+t*22)%(W*1.2)-W*0.1, sty=hy*0.5+Math.sin(t*0.6)*20;
  ctx.save(); ctx.fillStyle='#cfd6e2'; rr(ctx,stx-3,sty-2,6,4,1); ctx.fill();
  ctx.fillStyle='#4aa0ff'; ctx.fillRect(stx-9,sty-1,5,2); ctx.fillRect(stx+4,sty-1,5,2); ctx.restore();
}

function drawFloorMoonbase(biome,t){
  const hy=GROUND?GROUND():H*0.82;
  // ---- regolith gradient ----
  const g=ctx.createLinearGradient(0,hy,0,H);
  g.addColorStop(0,'#54596a'); g.addColorStop(0.5,'#3d4150'); g.addColorStop(1,'#24272f');
  ctx.fillStyle=g; ctx.fillRect(0,hy,W,H-hy);
  // ---- keep center lane readable: subtle dark vignette on edges only ----
  const scroll=cam*0.9;
  // ---- craters (parallax scroll) ----
  ctx.save();
  for(let i=0;i<26;i++){
    const f=0.5+((i%3)*0.18), gap=170, k=Math.floor((i*gap+scroll*f)/gap);
    const cxp=((i*gap - scroll*f)%(W+gap)+(W+gap))%(W+gap)-gap*0.5;
    const cyp=hy+18+hashY(k*2.3)* (H-hy)*0.8, cr=8+hashS(k*1.9)*26, edge=Math.abs(cxp-W*0.5)>W*0.22;
    ctx.globalAlpha=edge?0.9:0.4;
    // rim highlight
    ctx.fillStyle='#666b7d'; ctx.beginPath(); ctx.ellipse(cxp,cyp,cr,cr*0.42,0,0,7); ctx.fill();
    // basin
    ctx.fillStyle='#2b2e39'; ctx.beginPath(); ctx.ellipse(cxp,cyp+1,cr*0.8,cr*0.32,0,0,7); ctx.fill();
  }
  ctx.restore();
  // ---- scattered rocks ----
  ctx.save();
  for(let r=0;r<18;r++){ const f=0.7, gap=120, k=Math.floor((r*gap+scroll*f)/gap);
    const rx=((r*gap - scroll*f)%(W+gap)+(W+gap))%(W+gap)-gap*0.5, ry=hy+30+hashY(k*4.1)*(H-hy)*0.7, rs=3+hashS(k*3.3)*6;
    ctx.fillStyle='#5b6072'; ctx.beginPath(); ctx.moveTo(rx-rs,ry+rs*0.4); ctx.lineTo(rx-rs*0.3,ry-rs*0.6); ctx.lineTo(rx+rs*0.6,ry-rs*0.3); ctx.lineTo(rx+rs,ry+rs*0.4); ctx.closePath(); ctx.fill();
    ctx.fillStyle='#31343f'; ctx.fillRect(rx-rs,ry+rs*0.3,rs*2,2); }
  ctx.restore();
  // ---- faint bootprints down the lane ----
  ctx.save(); ctx.globalAlpha=0.16; ctx.fillStyle='#1c1e26';
  for(let b=0;b<10;b++){ const bx=W*0.5+((b%2)?7:-7)-((scroll*0.6)%80)+ (b*0)*0, by=hy+40+b*((H-hy)/10);
    ctx.beginPath(); ctx.ellipse(bx,by,4,7,0,0,7); ctx.fill(); }
  ctx.restore();
  // ---- glowing base panel + cable ----
  const px=W*0.5 - (scroll*0.6)%W, py=hy+12;
  ctx.save(); ctx.globalCompositeOperation='lighter';
  ctx.strokeStyle='rgba(90,220,255,0.5)'; ctx.lineWidth=2;
  ctx.beginPath(); ctx.moveTo(px-60,py+4); ctx.quadraticCurveTo(px,py+16,px+70,py+2); ctx.stroke();
  ctx.restore();
  ctx.save(); ctx.fillStyle='#2f3644'; rr(ctx,px-16,py,32,10,2); ctx.fill();
  const pulse=0.4+0.4*Math.abs(Math.sin(t*2));
  ctx.save(); ctx.globalCompositeOperation='lighter'; ctx.globalAlpha=pulse; ctx.fillStyle='#5adcff';
  rr(ctx,px-11,py+3,22,4,2); ctx.fill(); ctx.restore(); ctx.restore();
}
function scapeKino(biome,horizon,t){
  var g=GROUND(), ac=biome.ac, gold=hslHex(42,70,58), crim=hslHex(352,64,24);
  // ---- burning bombed CITY SKYLINE through broken back wall (deep parallax) ----
  var wf=0.08, wx=-cam*wf, hx=horizon-70;
  // fire glow bleeding through the wall gap
  ctx.save(); ctx.globalCompositeOperation='lighter';
  var fg=ctx.createRadialGradient(W*0.5+wx*0.2,hx+40,10,W*0.5+wx*0.2,hx+40,W*0.7);
  fg.addColorStop(0,silhouetteColor(hslHex(24,90,55),0.55,1)); fg.addColorStop(0.5,silhouetteColor(hslHex(14,85,40),0.22,1)); fg.addColorStop(1,'rgba(0,0,0,0)');
  ctx.fillStyle=fg; ctx.fillRect(0,hx-60,W,g-hx+80); ctx.restore();
  // skyline of ruined buildings inside the wall gap (clip to a jagged opening)
  ctx.save(); ctx.beginPath();
  ctx.moveTo(W*0.30+wx,hx+20); ctx.lineTo(W*0.34+wx,hx-30); ctx.lineTo(W*0.5+wx,hx-46); ctx.lineTo(W*0.66+wx,hx-26); ctx.lineTo(W*0.70+wx,hx+30); ctx.lineTo(W*0.66+wx,g-30); ctx.lineTo(W*0.34+wx,g-30); ctx.closePath(); ctx.clip();
  var sky2=ctx.createLinearGradient(0,hx-50,0,g); sky2.addColorStop(0,hslHex(18,60,14)); sky2.addColorStop(1,'#0a0508'); ctx.fillStyle=sky2; ctx.fillRect(0,hx-60,W,g);
  var bi=0; while(bi<10){ var bx=W*0.30+wx+hashS('bx'+bi)*W*0.42, bw=14+hashY('bw'+bi)*22, bh=30+hashY('bh'+bi)*70; ctx.fillStyle=silhouetteColor('#1a0d10',0.9,0.7); ctx.fillRect(bx,hx-20+hashY('by'+bi)*30,bw,bh);
    // window fire flicker
    if(((bi%3)+3)%3===0){ ctx.save(); ctx.globalCompositeOperation='lighter'; ctx.globalAlpha=0.4+0.4*Math.abs(Math.sin(t*3+bi)); ctx.fillStyle=hslHex(30,95,60); ctx.fillRect(bx+bw*0.3,hx-10+hashY('by'+bi)*30,4,4); ctx.restore(); } bi++; }
  // rising smoke columns
  ctx.save(); ctx.globalAlpha=0.3; var si=0; while(si<4){ var sxx=W*0.34+wx+si*W*0.1, sy=hx-10-((t*20+si*40)%120); ctx.fillStyle=silhouetteColor('#20161a',0.5,1); ctx.beginPath(); ctx.arc(sxx+Math.sin(t+si)*8,sy,14+si*3,0,7); ctx.fill(); si++; } ctx.restore();
  ctx.restore();
  // ---- ornate crumbling theater arches + balcony boxes (mid parallax) ----
  var af=0.18, axo=-cam*af;
  ctx.fillStyle=silhouetteColor(crim,0.85,0.6);
  var arc=0; while(arc<5){ var axx=(arc*W*0.28+axo)%(W*1.4)-W*0.2, ay=horizon-20, aw=W*0.2, ah=g-ay+40;
    rr(ctx,axx,ay,aw,ah,aw*0.5); ctx.fill();
    // gilded arch trim
    ctx.save(); ctx.globalCompositeOperation='lighter'; ctx.globalAlpha=0.35; ctx.strokeStyle=gold; ctx.lineWidth=3; ctx.beginPath(); ctx.arc(axx+aw/2,ay+aw*0.5,aw*0.5,Math.PI,0); ctx.stroke(); ctx.restore();
    // balcony box
    ctx.fillStyle=silhouetteColor(crim,0.9,0.45); rr(ctx,axx+aw*0.15,ay+ah*0.4,aw*0.7,ah*0.28,6); ctx.fill(); arc++; }
  // flickering wall sconces
  var sc=0; while(sc<4){ var scx=(sc*W*0.34+axo*1.1)%(W*1.3)-W*0.1, scy=horizon+30; ctx.save(); ctx.globalCompositeOperation='lighter'; var fl=0.5+0.5*Math.abs(Math.sin(t*5+sc*2.3)); ctx.globalAlpha=0.6*fl; var sg=ctx.createRadialGradient(scx,scy,1,scx,scy,26); sg.addColorStop(0,gold); sg.addColorStop(1,'rgba(0,0,0,0)'); ctx.fillStyle=sg; ctx.beginPath(); ctx.arc(scx,scy,26,0,7); ctx.fill(); ctx.restore(); sc++; }
  // ---- HERO: grand ruined STAGE with torn tattered RED CURTAINS parting ----
  var stf=0.3, sxo=-cam*stf, cx=W*0.5+sxo*0.2, part=40+Math.sin(t*0.6)*8;
  // proscenium gold frame top
  ctx.fillStyle=silhouetteColor(gold,0.5,0.9); rr(ctx,cx-W*0.42,horizon-30,W*0.84,26,8); ctx.fill();
  // curtains L/R with torn ragged bottom edge
  var side=0; while(side<2){ var dir=side===0?-1:1, cxo=cx+dir*(part), cw=W*0.30;
    var cg=ctx.createLinearGradient(cxo-dir*cw,0,cxo,0); cg.addColorStop(0,hslHex(352,60,14)); cg.addColorStop(0.5,hslHex(352,66,30)); cg.addColorStop(1,hslHex(352,55,18)); ctx.fillStyle=cg;
    ctx.beginPath(); ctx.moveTo(cxo,horizon-24); ctx.lineTo(cxo-dir*cw,horizon-24); ctx.lineTo(cxo-dir*cw,g+30);
    // tattered lower hem
    var tv=6; while(tv>=0){ var txx=cxo-dir*cw*(tv/6), ty=g-20+hashY('t'+side+tv)*40; ctx.lineTo(txx,ty); ctx.lineTo(txx+dir*cw/12,g-40-hashY('u'+side+tv)*20); tv--; }
    ctx.closePath(); ctx.fill();
    // vertical fold shading
    var fi=0; while(fi<5){ ctx.save(); ctx.globalAlpha=0.25; ctx.strokeStyle=fi%2?hslHex(352,50,10):gold; ctx.lineWidth=fi%2?8:2; ctx.beginPath(); var fxx=cxo-dir*cw*(fi/5); ctx.moveTo(fxx,horizon-20); ctx.lineTo(fxx-dir*6,g); ctx.stroke(); ctx.restore(); fi++; }
    // gold tie-back
    ctx.save(); ctx.globalCompositeOperation='lighter'; ctx.globalAlpha=0.5; ctx.fillStyle=gold; ctx.beginPath(); ctx.arc(cxo,horizon+(g-horizon)*0.45,10,0,7); ctx.fill(); ctx.restore(); side++; }
  // ---- shattered CHANDELIER hanging center ----
  var chx=W*0.5+sxo*0.15, chy=horizon-4, sway=Math.sin(t*0.9)*6;
  ctx.strokeStyle=silhouetteColor(gold,0.4,1); ctx.lineWidth=2; ctx.beginPath(); ctx.moveTo(chx,horizon-30); ctx.lineTo(chx+sway,chy+18); ctx.stroke();
  ctx.save(); ctx.globalCompositeOperation='lighter';
  var cr=0; while(cr<9){ var cang=cr/9*6.283, crx=chx+sway+Math.cos(cang)*22, cry=chy+18+Math.sin(cang)*10; ctx.globalAlpha=(cr%4?0.3:0.7)*(0.6+0.4*Math.sin(t*4+cr)); ctx.fillStyle=cr%4?gold:hslHex(48,90,80); ctx.beginPath(); ctx.arc(crx,cry,cr%4?2:3.5,0,7); ctx.fill(); cr++; } ctx.restore();
  // ---- lone dramatic SPOTLIGHT beam sweeping (animated) ----
  var swp=Math.sin(t*0.5)*W*0.16, topx=W*0.5+swp*0.4, botx=W*0.5+swp;
  ctx.save(); ctx.globalCompositeOperation='lighter';
  var bg=ctx.createLinearGradient(0,horizon-40,0,g+40); bg.addColorStop(0,silhouetteColor('#fff7e0',0.42,1)); bg.addColorStop(1,'rgba(255,240,200,0)');
  ctx.fillStyle=bg; ctx.beginPath(); ctx.moveTo(topx-16,horizon-38); ctx.lineTo(topx+16,horizon-38); ctx.lineTo(botx+70,g+30); ctx.lineTo(botx-70,g+30); ctx.closePath(); ctx.fill();
  // dust motes drifting in the beam
  var dm=0; while(dm<16){ var dph=(t*0.3+hashS('dm'+dm))%1, dmy=horizon-30+dph*(g-horizon+40), dmx=botx+(hashS('dx'+dm)-0.5)*120+Math.sin(t+dm)*6; ctx.globalAlpha=0.5*(1-dph)*(0.5+0.5*Math.sin(t*2+dm)); ctx.fillStyle='#fff'; ctx.beginPath(); ctx.arc(dmx,dmy,1.3,0,7); ctx.fill(); dm++; }
  ctx.restore();
  // ---- spooky film-projector scanline / flicker overlay ----
  ctx.save(); ctx.globalAlpha=0.05+0.04*Math.abs(Math.sin(t*11)); ctx.fillStyle='#000';
  var ln=0; while(ln<Math.ceil(g/4)){ ctx.fillRect(0,ln*4,W,2); ln++; } ctx.restore();
}

function drawFloorKino(biome,t){
  var g=GROUND(), gold=hslHex(42,70,55);
  // worn dark wooden STAGE planks
  var base=ctx.createLinearGradient(0,g,0,H); base.addColorStop(0,hslHex(24,32,12)); base.addColorStop(1,hslHex(20,30,6)); ctx.fillStyle=base; ctx.fillRect(0,g,W,H-g);
  var pf=0.3, pxo=-cam*pf%64;
  var pk=-1; while(pk<Math.ceil(W/64)+1){ var pxx=pk*64+pxo; ctx.strokeStyle=silhouetteColor('#000',0.4,1); ctx.lineWidth=2; ctx.beginPath(); ctx.moveTo(pxx,g); ctx.lineTo(pxx+18,H); ctx.stroke();
    // plank highlight
    ctx.strokeStyle=silhouetteColor(hslHex(28,30,26),0.3,1); ctx.lineWidth=1; ctx.beginPath(); ctx.moveTo(pxx+2,g); ctx.lineTo(pxx+20,H); ctx.stroke(); pk++; }
  // horizontal plank seams
  var hz=0; while(hz<3){ var hy=g+(H-g)*(0.3+hz*0.28); ctx.strokeStyle=silhouetteColor('#000',0.35,1); ctx.lineWidth=1.5; ctx.beginPath(); ctx.moveTo(0,hy); ctx.lineTo(W,hy); ctx.stroke(); hz++; }
  // red carpet strip down the center lane
  var cw=W*0.24, ccx=W*0.5; var cg=ctx.createLinearGradient(ccx-cw/2,0,ccx+cw/2,0);
  cg.addColorStop(0,hslHex(352,55,16)); cg.addColorStop(0.5,hslHex(352,60,28)); cg.addColorStop(1,hslHex(352,55,16)); ctx.fillStyle=cg;
  ctx.beginPath(); ctx.moveTo(ccx-cw*0.35,g); ctx.lineTo(ccx+cw*0.35,g); ctx.lineTo(ccx+cw*0.6,H); ctx.lineTo(ccx-cw*0.6,H); ctx.closePath(); ctx.fill();
  // gold carpet trim
  ctx.strokeStyle=silhouetteColor(gold,0.4,1); ctx.lineWidth=2; ctx.beginPath(); ctx.moveTo(ccx-cw*0.35,g); ctx.lineTo(ccx-cw*0.6,H); ctx.moveTo(ccx+cw*0.35,g); ctx.lineTo(ccx+cw*0.6,H); ctx.stroke();
  // spotlight pool on the stage floor (sweeps with beam)
  var swp=Math.sin(t*0.5)*W*0.16, poolx=W*0.5+swp, pooly=g+(H-g)*0.4;
  ctx.save(); ctx.globalCompositeOperation='lighter';
  var pg=ctx.createRadialGradient(poolx,pooly,4,poolx,pooly,W*0.22); pg.addColorStop(0,silhouetteColor('#fff2cc',0.3,1)); pg.addColorStop(1,'rgba(255,240,200,0)');
  ctx.fillStyle=pg; ctx.save(); ctx.translate(poolx,pooly); ctx.scale(1,0.4); ctx.beginPath(); ctx.arc(0,0,W*0.22,0,7); ctx.fill(); ctx.restore(); ctx.restore();
  // scattered debris / fallen chandelier shards
  if(!_PREF_REDUCE){ var db=0; while(db<14){ var dbx=(hashS('db'+db)*W - (cam*0.3)% W + W)%W, dby=g+8+hashY('dy'+db)*(H-g-10), ds=2+hashY('ds'+db)*5;
    ctx.fillStyle=silhouetteColor(((db%3)+3)%3===0?gold:'#000',0.5,1); ctx.save(); ctx.translate(dbx,dby); ctx.rotate(hashS('dr'+db)*6); ctx.fillRect(-ds/2,-ds/2,ds,ds*0.6); ctx.restore(); db++; } }
}
function scapeFacility115(biome,horizon,t){
  var acc=getAccent(biome.ac), BLU=hslHex((acc+150)%360,90,58), GRN=125, STEEL=hslHex(210,14,20);
  var pulse=0.5+0.5*Math.sin(t*2.2), flick=0.6+0.4*hashY(Math.floor(t*13));
  // ---- stormy sky flashes (far) ----
  if(pulse>0.72&&!_PREF_REDUCE){ ctx.save(); ctx.globalCompositeOperation='lighter';
    ctx.fillStyle=silhouetteColor(BLU,0.10*(pulse-0.72)*6,1.4); ctx.fillRect(0,0,W,horizon); ctx.restore(); }

  // ---- FAR factory skyline (f=0.10) ----
  var f0=0.10, bx0=-cam*f0, baseF=horizon+H*0.02;
  ctx.fillStyle=silhouetteColor(STEEL,0.55,0.7);
  for(var i=0;i<7;i++){ var gx=((i*260-((bx0%1820)+1820)%1820)); var bw=90+hashS(i*3)*70, bh=H*(0.14+hashY(i*5)*0.14);
    rr(ctx,gx,baseF-bh,bw,bh,3); ctx.fill();
    var sw=16+hashS(i)*10; rr(ctx,gx+bw*0.55,baseF-bh-H*0.10,sw,H*0.11,3); ctx.fill(); } // smokestacks

  // ---- MID industrial block: pipes, catwalks, riveted towers (f=0.20) ----
  var f1=0.20, bx1=-cam*f1, base=GROUND()-H*0.02;
  ctx.fillStyle=silhouetteColor(STEEL,0.92,1.05);
  for(var j=0;j<6;j++){ var tx=((j*300-((bx1%1800)+1800)%1800)); var tw=140+hashS(j*7)*60, th=H*(0.20+hashY(j*4)*0.16);
    rr(ctx,tx,base-th,tw,th,4); ctx.fill();
    ctx.strokeStyle=silhouetteColor(STEEL,0.6,1.6); ctx.lineWidth=2; ctx.strokeRect(tx+8,base-th+10,tw-16,th-16);
    var st=22+hashS(j*2)*14; rr(ctx,tx+tw*0.2,base-th-H*0.16,st,H*0.17,3); ctx.fill(); } // stacks
  // pipes across
  ctx.strokeStyle=silhouetteColor(STEEL,0.8,1.3); ctx.lineWidth=9; ctx.beginPath();
  var py=base-H*0.11+Math.sin(t*0.3)*1.5; ctx.moveTo(0,py); ctx.lineTo(W,py-H*0.02); ctx.stroke();
  // live BLUE LIQUID VIALS hanging from the pipes (spontaneous bubbling, NOT a clean loop)
  for(var w=0;w<4;w++){ var lx=((w*360-((bx1%1440)+1440)%1440))+120, ly=base-H*0.30, seed=w*13.7;
    var vw=15, vh=46, vx=lx-vw/2, vtop=ly-14, liqTop=vtop+vh*0.30+Math.sin(t*1.6+seed)*1.2;
    ctx.strokeStyle=silhouetteColor(STEEL,0.7,1.4); ctx.lineWidth=2; ctx.beginPath(); ctx.moveTo(lx,horizon); ctx.lineTo(lx,vtop-4); ctx.stroke();
    var wob=0.5+0.5*Math.max(0,Math.min(1, 0.5+0.4*Math.sin(t*1.7+seed)+0.25*Math.sin(t*5.3+seed*2)+0.2*(hashS(Math.floor(t*2.5)+w*7)-0.5)*2)); // irregular alive glow, no obvious cycle
    ctx.save(); ctx.globalCompositeOperation="lighter"; var vg=ctx.createRadialGradient(lx,liqTop+vh*0.35,0,lx,liqTop+vh*0.35,30);
    vg.addColorStop(0,silhouetteColor(hslHex(205,95,64),0.9*wob,1.6)); vg.addColorStop(1,silhouetteColor(hslHex(205,95,60),0,1));
    ctx.fillStyle=vg; ctx.beginPath(); ctx.arc(lx,liqTop+vh*0.35,30,0,7); ctx.fill(); ctx.restore();
    ctx.fillStyle="rgba(170,215,255,0.13)"; rr(ctx,vx,vtop,vw,vh,vw/2); ctx.fill();
    ctx.save(); ctx.beginPath(); rr(ctx,vx,vtop,vw,vh,vw/2); ctx.clip();
    var lg2=ctx.createLinearGradient(0,liqTop,0,vtop+vh); lg2.addColorStop(0,hslHex(200,95,60)); lg2.addColorStop(1,hslHex(214,95,40));
    ctx.fillStyle=lg2; ctx.fillRect(vx,liqTop,vw,vh); ctx.fillStyle=hslHex(190,95,80); ctx.fillRect(vx,liqTop,vw,2);
    ctx.save(); ctx.globalCompositeOperation="lighter";
    for(var b=0;b<4;b++){ var bs=w*17+b*5, spd=0.4+hashS(bs)*0.6, ph=t*spd+hashY(bs)*3, life=ph-Math.floor(ph);
      if(hashS(Math.floor(ph)+bs)>0.5){ var bbx=vx+3+hashS(bs*2)*(vw-6), bby=(vtop+vh-3)-life*(vtop+vh-3-liqTop-2); ctx.fillStyle="rgba(210,245,255,"+(0.85*(1-life)).toFixed(2)+")"; ctx.beginPath(); ctx.arc(bbx,bby,1+hashS(bs*3)*1.3,0,7); ctx.fill(); } }
    ctx.restore(); ctx.restore();
    ctx.fillStyle="rgba(255,255,255,0.3)"; rr(ctx,vx+2,vtop+3,2.5,vh*0.45,1.2); ctx.fill();
    ctx.fillStyle=silhouetteColor(STEEL,0.9,1.2); rr(ctx,vx-1,vtop-5,vw+2,6,2); ctx.fill(); }

  // ---- arcing TESLA lightning between mid towers ----
  if(!_PREF_REDUCE){ ctx.save(); ctx.globalCompositeOperation='lighter'; ctx.strokeStyle=silhouetteColor(BLU,0.85*flick,1.6);
    ctx.lineWidth=2.4; ctx.shadowColor=BLU; ctx.shadowBlur=14;
    for(var a=0;a<2;a++){ var ax=W*(0.22+a*0.4)+Math.sin(t*0.5+a)*20, ay=base-H*0.30, ex=ax+120, ey=base-H*0.36;
      ctx.beginPath(); ctx.moveTo(ax,ay); var seg=6;
      for(var s=1;s<=seg;s++){ var u=s/seg, jx=ax+(ex-ax)*u+(hashY(Math.floor(t*20)+s+a*9)-0.5)*40, jy=ay+(ey-ay)*u+(hashY(s*3+a)-0.5)*26; ctx.lineTo(jx,jy);} ctx.stroke(); }
    ctx.restore(); }

  // ---- HERO: glowing TELEPORTER PAD + rising beam (lower-center, f=0.30) ----
  var cx=W*0.5+(-cam*0.30-Math.round(-cam*0.30/W)*0)*0, hx=W*0.5, hy=GROUND()-H*0.015, rw=Math.min(W*0.24,230), rh=rw*0.34;
  // rising beam
  ctx.save(); ctx.globalCompositeOperation='lighter';
  var bg=ctx.createLinearGradient(hx,hy-H*0.62,hx,hy); bg.addColorStop(0,silhouetteColor(BLU,0,1));
  bg.addColorStop(0.6,silhouetteColor(BLU,0.20+0.14*pulse,1.4)); bg.addColorStop(1,silhouetteColor(BLU,0.5+0.3*pulse,1.6));
  ctx.fillStyle=bg; var bw2=rw*0.28*(0.8+0.25*pulse); ctx.beginPath();
  ctx.moveTo(hx-bw2*0.4,hy); ctx.lineTo(hx+bw2*0.4,hy); ctx.lineTo(hx+bw2,hy-H*0.62); ctx.lineTo(hx-bw2,hy-H*0.62); ctx.closePath(); ctx.fill();
  // pad glow pool
  var pg=ctx.createRadialGradient(hx,hy,0,hx,hy,rw*1.5); pg.addColorStop(0,silhouetteColor(BLU,0.6*pulse+0.25,1.6));
  pg.addColorStop(1,silhouetteColor(BLU,0,1)); ctx.fillStyle=pg; ctx.beginPath(); ctx.ellipse(hx,hy,rw*1.5,rh*1.6,0,0,7); ctx.fill();
  ctx.restore();
  // ring platform (stacked rings)
  for(var r=0;r<3;r++){ var ry=hy-r*rh*0.42, sc=1-r*0.14;
    ctx.fillStyle=silhouetteColor(STEEL,0.95,1.1+r*0.15); ctx.beginPath(); ctx.ellipse(hx,ry,rw*sc,rh*sc,0,0,7); ctx.fill();
    ctx.save(); ctx.globalCompositeOperation='lighter'; ctx.strokeStyle=silhouetteColor(BLU,0.9,1.6); ctx.lineWidth=3+pulse*2;
    ctx.shadowColor=BLU; ctx.shadowBlur=18; ctx.beginPath(); ctx.ellipse(hx,ry,rw*sc,rh*sc,0,0,7); ctx.stroke(); ctx.restore(); }
  // crackling arcs on pad
  if(!_PREF_REDUCE){ ctx.save(); ctx.globalCompositeOperation='lighter'; ctx.strokeStyle=silhouetteColor(hslHex(190,95,80),0.9*flick,1.7);
    ctx.lineWidth=1.8; ctx.shadowColor=BLU; ctx.shadowBlur=10;
    for(var k=0;k<5;k++){ var ang=t*1.3+k*1.256, r0=rw*0.2, r1=rw*0.9*(0.7+0.3*hashY(k+Math.floor(t*10)));
      ctx.beginPath(); ctx.moveTo(hx+Math.cos(ang)*r0,hy+Math.sin(ang)*r0*0.34);
      var mx=hx+Math.cos(ang)*r1*0.6+(hashY(k*2+Math.floor(t*15))-0.5)*24, my=hy+Math.sin(ang)*r1*0.34*0.6-14;
      ctx.lineTo(mx,my); ctx.lineTo(hx+Math.cos(ang)*r1,hy+Math.sin(ang)*r1*0.34-6); ctx.stroke(); }
    ctx.restore(); }

  // ---- fallen 115 METEOR with radioactive green glow + haze (f=0.28) ----
  var mx0=W*0.16-((-cam*0.28)%W), meX=W*0.14, meY=GROUND()+H*0.01, mr=Math.min(W*0.10,84);
  ctx.save(); ctx.globalCompositeOperation='lighter';
  var mg=ctx.createRadialGradient(meX,meY,0,meX,meY,mr*3.2); mg.addColorStop(0,silhouetteColor(hslHex(GRN,90,55),0.7*(0.7+0.3*pulse),1.6));
  mg.addColorStop(0.5,silhouetteColor(hslHex(GRN,85,45),0.28,1.3)); mg.addColorStop(1,silhouetteColor(hslHex(GRN,80,40),0,1));
  ctx.fillStyle=mg; ctx.beginPath(); ctx.arc(meX,meY,mr*3.2,0,7); ctx.fill(); ctx.restore();
  ctx.fillStyle=silhouetteColor(hslHex(120,20,10),1,0.5); ctx.beginPath(); ctx.arc(meX,meY,mr,0,7); ctx.fill();
  ctx.save(); ctx.globalCompositeOperation='lighter'; ctx.fillStyle=silhouetteColor(hslHex(GRN,95,65),0.9,1.7);
  for(var c=0;c<4;c++){ var cang=c*1.57+0.4; ctx.save(); ctx.translate(meX,meY); ctx.rotate(cang);
    rr(ctx,-2,-mr*0.85,4,mr*0.85,2); ctx.fill(); ctx.restore(); } // glowing cracks
  ctx.restore();
}

function drawFloorFacility115(biome,t){
  var acc=getAccent(biome.ac), BLU=hslHex((acc+150)%360,90,58), GRN=125, g=GROUND(), pulse=0.5+0.5*Math.sin(t*2.2);
  // dark grated concrete base
  var fg=ctx.createLinearGradient(0,g,0,H); fg.addColorStop(0,hslHex(210,12,13)); fg.addColorStop(1,hslHex(210,14,7));
  ctx.fillStyle=fg; ctx.fillRect(0,g,W,H-g);
  // grating slats
  ctx.strokeStyle=silhouetteColor(hslHex(210,10,4),0.7,1); ctx.lineWidth=2;
  for(var i=0;i<28;i++){ var gx=(i/28)*W; ctx.beginPath(); ctx.moveTo(gx,g); ctx.lineTo(gx+(gx-W/2)*0.25,H); ctx.stroke(); }
  ctx.strokeStyle=silhouetteColor(hslHex(210,10,18),0.5,1);
  for(var r=1;r<5;r++){ var ry=g+(H-g)*(r*r/25); ctx.beginPath(); ctx.moveTo(0,ry); ctx.lineTo(W,ry); ctx.stroke(); }
  // electric-blue conduit lines
  ctx.save(); ctx.globalCompositeOperation='lighter'; ctx.strokeStyle=silhouetteColor(BLU,0.55+0.2*pulse,1.5);
  ctx.lineWidth=3; ctx.shadowColor=BLU; ctx.shadowBlur=12;
  for(var c=0;c<3;c++){ var cx=W*(0.28+c*0.22); ctx.beginPath(); ctx.moveTo(cx,g); ctx.lineTo(cx+(cx-W/2)*0.3,H); ctx.stroke(); }
  ctx.restore();
  // glowing green energy cracks
  ctx.save(); ctx.globalCompositeOperation='lighter'; ctx.strokeStyle=silhouetteColor(hslHex(GRN,95,55),0.6+0.25*pulse,1.6);
  ctx.lineWidth=2.5; ctx.shadowColor=hslHex(GRN,90,50); ctx.shadowBlur=14;
  for(var k=0;k<4;k++){ var sx=W*(0.1+k*0.27); ctx.beginPath(); ctx.moveTo(sx,g);
    var yy=g, xx=sx; for(var s=0;s<4;s++){ yy+=(H-g)*0.28; xx+=(hashY(k*4+s)-0.5)*70; ctx.lineTo(xx,yy);} ctx.stroke(); }
  ctx.restore();
  // meteor floor glow bleed (matches scape meteor at left)
  ctx.save(); ctx.globalCompositeOperation='lighter';
  var mg=ctx.createRadialGradient(W*0.14,g,0,W*0.14,g,W*0.3); mg.addColorStop(0,silhouetteColor(hslHex(GRN,85,45),0.3*(0.7+0.3*pulse),1.4));
  mg.addColorStop(1,silhouetteColor(hslHex(GRN,80,40),0,1)); ctx.fillStyle=mg; ctx.fillRect(0,g,W*0.5,H-g); ctx.restore();
}
const THEMED_SCAPE = { "Dawnpurr":scapeMeadow, "Candyland":scapeCandyland, "Underwater Reef":scapeReef, "Neon Cyber-Kitty":scapeCyberCity, "Haunted Graveyard":scapeGraveyard, "Sakura Grove":scapeSakura, "Desert Purramids":scapeDesert, "Frozen Purrora":scapeFrozen, "Sky Kingdom":scapeSky, "Meowshroom Hollow":scapeMushroom, "Zoomies Zone":scapeZoomies, "Jungle Ruins":scapeJungle, "Clockwork Kitty":scapeClockwork, "Storm Spurr":scapeStormSpire, "Funland":scapeFunland, "Rainbow Road":scapeRainbowRoad, "Synthwave Grid":scapeSynthwave, "Cosmic Mewbula":scapeNebula, "Vapurrwave":scapeVaporwave, "Geometric Void":scapeGeoVoid, "Bioluminescent Abyss":scapeAbyss, "Elysium":scapeElysium, "Zero Purravity":scapeNebula, "Petal Purradise":scapeSakura, "Crimson Dawn":scapeRedClouds, "Hyperpurr":scapeHyperpurr, "Gotham":scapeGotham, "Hollywood":scapeHollywood, "Neon Tokyo":scapeTokyo, "Senpai Skies":scapeAnime, "Ghetto":scapeGhetto, "Humble":scapeHumble, "Noname":scapeNoname, "Plu":scapePlu, "Blurred":scapeBlurred, "Private":scapePrivate, "Rad":scapeRad, "Ruby Rabbit":scapeRubyRabbit, "Shadowman":scapeShadowman, "Aurora":scapeAurora, "Moonbase":scapeMoonbase, "Kino":scapeKino, "Facility 115":scapeFacility115 };
function _zoneObj(z){ return { name:z.name, sky:z.sky, ac:z.ac, fx:z.fx||"none" }; }
function zonePair(dist){ const last=ZONES.length-1; // the two biomes you're between + blend t (for crossfading the distant world)
  { const fz=_forcedZone(); if(fz){ return {A:_zoneObj(fz), B:_zoneObj(fz), tt:0}; } } // ?world=/?zerog: scenery matches the forced world
  if(dist<=ZONES[last].d){ let i=0; while(i<last && dist>=ZONES[i+1].d) i++; const A=ZONES[i], B=ZONES[Math.min(i+1,last)]; const tt=B.d>A.d?_sm((dist-A.d)/(B.d-A.d)):0; return {A:_zoneObj(A),B:_zoneObj(B),tt}; }
  const over=dist-ZONES[last].d, SP=1500, k=Math.floor(over/SP); const A=(k===0)?_zoneObj(ZONES[last]):proceduralBiome(k-1), B=proceduralBiome(k); const tt=_sm((over%SP)/SP); return {A:A,B:B,tt}; }
function _scapeFor(biome,horizon,t){
  const _ts=THEMED_SCAPE[biome.name]; if(_ts){ _ts(biome,horizon,t); return; } // themed levels override the hash styles
  switch(scapeStyle(biome.name)){
    case 0: scapeCity(biome,horizon); break;
    case 1: scapeMountains(biome,horizon); break;
    case 2: scapeCrystals(biome,horizon); break;
    case 3: scapeIslands(biome,horizon,t); break;
    case 4: scapeForest(biome,horizon); break;
    default: scapeRuins(biome,horizon); break;
  }
}
// --- offscreen double-buffer for the world crossfade (built lazily, resized with the canvas) ---
let _cfBufA=null,_cfBufB=null,_cfCtxA=null,_cfCtxB=null,_cfKeyA=null,_cfKeyB=null,_cfW=0,_cfH=0,_cfTick=0;
function _cfEnsure(){ if(!_cfBufA){ _cfBufA=document.createElement("canvas"); _cfBufB=document.createElement("canvas"); _cfCtxA=_cfBufA.getContext("2d"); _cfCtxB=_cfBufB.getContext("2d"); }
  if(_cfW!==cv.width||_cfH!==cv.height){ _cfW=_cfBufA.width=_cfBufB.width=cv.width; _cfH=_cfBufA.height=_cfBufB.height=cv.height; _cfKeyA=_cfKeyB=null; } }
function _renderScapeTo(bctx,biome,horizon,t){ const _o=ctx; try{ ctx=bctx; ctx.setTransform(1,0,0,1,0,0); ctx.clearRect(0,0,_cfW,_cfH); _scapeFor(biome,horizon,t); } finally { ctx=_o; } } // briefly retarget the global ctx at the buffer, restore no matter what
function drawScapes(t,biome){
  // SMOOTH WORLD TRANSITION: crossfade the upcoming world's scape in over a narrow band (tt 0.42->0.58) so you
  // WATCH the next world approach instead of it popping in. OUTSIDE the band = 1 scape drawn live (normal cost).
  // INSIDE the band we would need 2 heavy scapes/frame -> that dropped a render frame = the "lag on the fade".
  // Fix: cache each scape in its own offscreen buffer and re-render only ONE per frame (stagger); each frame just
  // blits the two buffers with alpha. Net per-frame cost during the fade ~= 1 scape render (same as normal), and
  // the staggered buffer is a single frame (16ms) stale = imperceptible for a far background. Smooth, no dip.
  const horizon=GROUND(), pr=zonePair(distM);
  const fade=_sm(Math.max(0,Math.min(1,(pr.tt-0.42)/0.16)));
  if(fade<=0){ _scapeFor(pr.A,horizon,t); return; }
  if(fade>=1){ _scapeFor(pr.B,horizon,t); return; }
  _cfEnsure();
  const keyA=pr.A&&pr.A.name, keyB=pr.B&&pr.B.name;
  if(_cfKeyA!==keyA){ _renderScapeTo(_cfCtxA,pr.A,horizon,t); _cfKeyA=keyA; } // fresh render on band entry / world change
  if(_cfKeyB!==keyB){ _renderScapeTo(_cfCtxB,pr.B,horizon,t); _cfKeyB=keyB; }
  if((++_cfTick)&1) _renderScapeTo(_cfCtxA,pr.A,horizon,t); else _renderScapeTo(_cfCtxB,pr.B,horizon,t); // keep parallax alive: refresh one buffer per frame
  ctx.save(); ctx.setTransform(1,0,0,1,0,0);
  ctx.globalAlpha=1-fade; ctx.drawImage(_cfBufA,0,0);
  ctx.globalAlpha=fade;   ctx.drawImage(_cfBufB,0,0);
  ctx.globalAlpha=1; ctx.restore();
}
// ---- biome-themed ambient flyers: UFO / helicopter / raven flock / balloon ----
function _flyerType(biome){
  const n = (biome && biome.name ? biome.name : '').toLowerCase();
  const fx = biome && biome.fx ? biome.fx : 'none';
  if (/dawn|meadow|calm|pasture|plain/.test(n)) return 'balloon'; // calm starting world
  if (fx === 'lava' || /fire|lava|ember|magma|dragon|forge|infurno|inferno/.test(n)) return 'dragon';
  if (/cyber|tech|city|neon|storm|circuit|grid|chrome/.test(n)) return 'jet';
  if (/cloud|aurora|sky|bright|kingdom|heaven|celest|day/.test(n)) return 'bison';
  if (/star|singular|aurora|whisk|void|cosmos|nebula|orbit|night|verge|astral/.test(n)) return 'ufo';
  if (fx === 'rain' || fx === 'lightning') return 'raven';
  if (/hush|storm|howl|abyss|down|gloom|dark|omin|raven|crow|murk/.test(n)) return 'raven';
  if (fx === 'lava') return 'heli';
  if (/sun|ember|magma|core|dawn|mango|mirage|spire|fall|canyon|city|tech|forge|industr/.test(n)) return 'heli';
  if (/bloom|spore|glacier|meadow|bright|day|sky|petal|garden/.test(n)) return 'balloon';
  let h=0; for(let i=0;i<n.length;i++) h=(h*31+n.charCodeAt(i))>>>0;
  return ['ufo','heli','raven','balloon'][h % 4];
}
function _ambUfo(x,y,s,t,i){
  ctx.save(); ctx.translate(x,y);
  ctx.fillStyle='rgba(18,24,42,0.34)'; ctx.beginPath(); ctx.ellipse(0,0, 13*s, 4.0*s, 0, 0, 7); ctx.fill();
  ctx.fillStyle='rgba(30,40,66,0.30)'; ctx.beginPath(); ctx.ellipse(0,-2.2*s, 6*s, 4.2*s, 0, Math.PI, 2*Math.PI, false); ctx.fill();
  ctx.strokeStyle=getAccent(0.16); ctx.lineWidth=1*DPR; ctx.beginPath(); ctx.ellipse(0,0, 13*s, 4.0*s, 0, 0, 7); ctx.stroke();
  const blink=0.30+0.70*(0.5+0.5*Math.sin(t*2.2+i*2.1)); const lr=2.4*s;
  const gl=ctx.createRadialGradient(0,3.2*s,0, 0,3.2*s, lr*3); gl.addColorStop(0, getAccent(0.55*blink)); gl.addColorStop(1, getAccent(0));
  ctx.fillStyle=gl; ctx.beginPath(); ctx.arc(0,3.2*s, lr*3, 0, 7); ctx.fill();
  ctx.fillStyle=getAccent(0.8*blink); ctx.beginPath(); ctx.arc(0,3.2*s, lr*0.5, 0, 7); ctx.fill();
  ctx.restore();
}
function _ambHeli(x,y,s,t,i,dir){
  ctx.save(); ctx.translate(x,y); ctx.scale(dir>0?1:-1,1);
  const body='rgba(20,26,40,0.40)';
  ctx.strokeStyle=body; ctx.lineWidth=2.2*s; ctx.lineCap='round';
  ctx.beginPath(); ctx.moveTo(2*s,0); ctx.lineTo(-12*s,-0.5*s); ctx.stroke();
  ctx.beginPath(); ctx.moveTo(-12*s,-0.5*s); ctx.lineTo(-14*s,-3*s); ctx.stroke();
  ctx.fillStyle=body; ctx.beginPath(); ctx.ellipse(3*s,0, 6.5*s, 3.6*s, 0, 0, 7); ctx.fill();
  ctx.fillStyle=getAccent(0.20); ctx.beginPath(); ctx.ellipse(6.5*s,0.4*s, 2.6*s, 2.0*s, 0, 0, 7); ctx.fill();
  ctx.strokeStyle=body; ctx.lineWidth=1*DPR; ctx.beginPath(); ctx.moveTo(-1*s,3.8*s); ctx.lineTo(8*s,3.8*s); ctx.stroke();
  ctx.beginPath(); ctx.moveTo(3*s,-3.4*s); ctx.lineTo(3*s,-5*s); ctx.stroke();
  const spin=Math.sin(t*9 + i*1.7); const rw=11*s*(0.35+0.65*Math.abs(spin));
  ctx.strokeStyle=getAccent(0.22+0.10*Math.abs(spin)); ctx.lineWidth=1.4*s;
  ctx.beginPath(); ctx.moveTo(3*s-rw,-5*s); ctx.lineTo(3*s+rw,-5*s); ctx.stroke();
  const tspin=0.5+0.5*Math.sin(t*16+i*3.0);
  ctx.strokeStyle=getAccent(0.14+0.14*tspin); ctx.lineWidth=1*DPR;
  ctx.beginPath(); ctx.moveTo(-14*s,-3*s-2*s*tspin); ctx.lineTo(-14*s,-3*s+2*s*tspin); ctx.stroke();
  ctx.restore();
}
function _ambRaven(x,y,s,t,i,dir){
  ctx.save(); ctx.translate(x,y); ctx.scale(dir>0?1:-1,1);
  const n = 2 + (Math.floor(hashS(i*3+5)*2));
  ctx.strokeStyle='rgba(54,62,84,0.62)'; ctx.lineCap='round'; ctx.lineJoin='round';
  for(let b=0;b<n;b++){
    const off = b - (n-1)/2; const bx = -off*7*s; const by = -Math.abs(off)*3.4*s;
    const flap = Math.sin(t*5.5 + i*1.3 + b*0.9); const up = 3.0*s + 2.6*s*flap; const span = 5.4*s;
    ctx.lineWidth = (1.3+0.35*Math.abs(off))*DPR;
    ctx.beginPath(); ctx.moveTo(bx-span, by-up*0.55); ctx.quadraticCurveTo(bx-1*s, by+0.5*s, bx, by); ctx.quadraticCurveTo(bx+1*s, by+0.5*s, bx+span, by-up*0.55); ctx.stroke();
  }
  ctx.restore();
}
function _ambBalloon(x,y,s,t,i){
  ctx.save();
  ctx.translate(x, y + Math.sin(t*0.7+i)*1.5*DPR);
  const R=7*s, neckY=R*0.62;
  ctx.beginPath(); ctx.moveTo(0,-R*1.95);                                   // teardrop envelope
  ctx.bezierCurveTo(R*1.2,-R*1.88, R*1.06,-R*0.12, R*0.34,neckY);
  ctx.bezierCurveTo(R*0.16,R*0.9, -R*0.16,R*0.9, -R*0.34,neckY);
  ctx.bezierCurveTo(-R*1.06,-R*0.12, -R*1.2,-R*1.88, 0,-R*1.95); ctx.closePath();
  const g=ctx.createRadialGradient(-R*0.4,-R*1.35,R*0.15, 0,-R*0.7,R*1.7);
  g.addColorStop(0, getAccent(0.5)); g.addColorStop(0.55, getAccent(0.34)); g.addColorStop(1,'rgba(18,24,38,0.36)');
  ctx.fillStyle=g; ctx.fill();
  ctx.strokeStyle=getAccent(0.16); ctx.lineWidth=0.8*DPR;                   // faint ribs
  ctx.beginPath(); ctx.moveTo(0,-R*1.95); ctx.bezierCurveTo(R*0.5,-R*1.4, R*0.5,-R*0.4, 0,neckY);
  ctx.moveTo(0,-R*1.95); ctx.bezierCurveTo(-R*0.5,-R*1.4, -R*0.5,-R*0.4, 0,neckY); ctx.stroke();
  ctx.fillStyle='rgba(255,255,255,0.16)'; ctx.beginPath(); ctx.ellipse(-R*0.42,-R*1.15,R*0.28,R*0.62,-0.35,0,7); ctx.fill(); // sheen
  ctx.fillStyle='rgba(26,20,14,0.5)'; ctx.beginPath();                      // basket
  ctx.moveTo(-1.7*s,neckY+2.2*s); ctx.lineTo(1.7*s,neckY+2.2*s); ctx.lineTo(1.3*s,neckY+4.6*s); ctx.lineTo(-1.3*s,neckY+4.6*s); ctx.closePath(); ctx.fill();
  ctx.strokeStyle='rgba(20,24,36,0.42)'; ctx.lineWidth=0.8*DPR;             // suspension lines
  ctx.beginPath(); ctx.moveTo(-R*0.34,neckY); ctx.lineTo(-1.5*s,neckY+2.2*s); ctx.moveTo(R*0.34,neckY); ctx.lineTo(1.5*s,neckY+2.2*s); ctx.stroke();
  ctx.restore();
}
function _ambDragon(x,y,s,t,i,dir){
  ctx.save(); ctx.translate(x,y); ctx.scale(dir>0?1:-1,1);
  const body='rgba(22,28,44,0.42)';
  const flap=Math.sin(t*4.2+i*1.4); // bat-wing beat
  ctx.strokeStyle=body; ctx.lineWidth=3.2*s; ctx.lineCap='round'; ctx.lineJoin='round';
  ctx.beginPath(); ctx.moveTo(9*s,0);
  ctx.quadraticCurveTo(2*s, 1.6*s*Math.sin(t*3+i), -4*s, 0.6*s);
  ctx.quadraticCurveTo(-11*s, -1.4*s, -16*s, 2.6*s*Math.sin(t*3+i+1.2));
  ctx.stroke();
  ctx.fillStyle=body; ctx.beginPath(); ctx.ellipse(10*s,0, 3.4*s, 2.2*s, 0, 0, 7); ctx.fill();
  ctx.beginPath(); ctx.moveTo(12.5*s,-1*s); ctx.lineTo(14.6*s,-2.2*s); ctx.lineTo(12.6*s,-2.4*s); ctx.closePath(); ctx.fill();
  const wy=-1*s - 4.2*s*Math.max(0,flap);
  ctx.fillStyle='rgba(18,24,40,0.36)'; ctx.beginPath();
  ctx.moveTo(1*s,-0.5*s);
  ctx.quadraticCurveTo(-2*s, wy-3*s, -7*s, wy);
  ctx.lineTo(-5*s, wy+1.5*s);
  ctx.quadraticCurveTo(-9*s, wy+1*s, -11*s, wy+4*s);
  ctx.lineTo(-3*s, 0.5*s); ctx.closePath(); ctx.fill();
  ctx.strokeStyle=getAccent(0.16); ctx.lineWidth=1*DPR;
  ctx.beginPath(); ctx.moveTo(9*s,-0.6*s); ctx.quadraticCurveTo(0,-1.2*s, -10*s,-0.4*s); ctx.stroke();
  const ep=0.5+0.5*Math.sin(t*2.0+i*1.9);
  const eg=ctx.createRadialGradient(15*s,-1.4*s,0, 15*s,-1.4*s, 3.4*s);
  eg.addColorStop(0,'rgba(255,176,72,'+(0.30*ep)+')'); eg.addColorStop(1,'rgba(255,176,72,0)');
  ctx.fillStyle=eg; ctx.beginPath(); ctx.arc(15*s,-1.4*s, 3.4*s, 0, 7); ctx.fill();
  ctx.restore();
}
function _ambJet(x,y,s,t,i,dir){
  ctx.save(); ctx.translate(x,y); ctx.scale(dir>0?1:-1,1);
  const body='rgba(20,26,40,0.40)';
  for(let c=0;c<3;c++){
    const cl=(18+c*10)*s, sh=0.10+0.06*(0.5+0.5*Math.sin(t*3-c*1.1+i));
    ctx.strokeStyle='rgba(232,238,248,'+sh+')'; ctx.lineWidth=(1.6-c*0.4)*s; ctx.lineCap='round';
    ctx.beginPath(); ctx.moveTo(-6*s,0); ctx.lineTo(-6*s-cl,0.2*c*s); ctx.stroke();
  }
  ctx.fillStyle=body; ctx.beginPath();
  ctx.moveTo(11*s,0);
  ctx.lineTo(-3*s,-1.4*s);
  ctx.lineTo(-7*s,-5.2*s);
  ctx.lineTo(-4*s,-1.0*s);
  ctx.lineTo(-7*s,0);
  ctx.lineTo(-4*s,1.0*s);
  ctx.lineTo(-7*s,5.2*s);
  ctx.lineTo(-3*s,1.4*s);
  ctx.closePath(); ctx.fill();
  ctx.beginPath(); ctx.moveTo(-5*s,0); ctx.lineTo(-7.5*s,-3*s); ctx.lineTo(-7*s,0); ctx.closePath(); ctx.fill();
  ctx.strokeStyle=getAccent(0.16); ctx.lineWidth=1*DPR;
  ctx.beginPath(); ctx.moveTo(11*s,0); ctx.lineTo(-3*s,-1.4*s); ctx.stroke();
  const bp=0.55+0.45*Math.sin(t*7+i*2.3);
  const bg=ctx.createRadialGradient(-7*s,0,0, -7*s,0, 4.2*s);
  bg.addColorStop(0, getAccent(0.5*bp)); bg.addColorStop(1, getAccent(0));
  ctx.fillStyle=bg; ctx.beginPath(); ctx.arc(-7*s,0, 4.2*s, 0, 7); ctx.fill();
  ctx.restore();
}
function _ambBison(x,y,s,t,i,dir){
  const bob=Math.sin(t*1.1+i*0.8)*1.6*DPR;
  ctx.save(); ctx.translate(x, y+bob); ctx.scale(dir>0?1:-1,1);
  const white='rgba(245,248,252,0.7)', shade='rgba(205,214,228,0.55)';
  ctx.fillStyle='rgba(120,134,160,0.22)'; ctx.beginPath(); ctx.ellipse(0,3.2*s, 12*s, 3.0*s, 0, 0, 7); ctx.fill();
  ctx.strokeStyle=shade; ctx.lineWidth=2.0*s; ctx.lineCap='round';
  for(let l=0;l<6;l++){
    const lx=(-8 + l*3.2)*s, sw=Math.sin(t*2.4 + l*1.05 + i)*1.8*s;
    ctx.beginPath(); ctx.moveTo(lx,2.4*s); ctx.lineTo(lx+sw,5.4*s); ctx.stroke();
  }
  ctx.fillStyle=white; ctx.beginPath(); ctx.ellipse(-1*s,0, 12*s, 6.2*s, 0, 0, 7); ctx.fill();
  ctx.fillStyle='rgba(120,86,54,0.6)'; ctx.beginPath(); ctx.ellipse(-2*s,-1.4*s, 6.5*s, 3.0*s, 0, 0, 7); ctx.fill();
  ctx.strokeStyle='rgba(96,68,42,0.55)'; ctx.lineWidth=1*DPR;
  ctx.beginPath(); ctx.moveTo(-7*s,-1.4*s); ctx.lineTo(3*s,-1.4*s); ctx.stroke();
  ctx.fillStyle=white; ctx.beginPath(); ctx.ellipse(10*s,0.4*s, 4.4*s, 4.0*s, 0, 0, 7); ctx.fill();
  ctx.strokeStyle=shade; ctx.lineWidth=1.6*s; ctx.lineCap='round';
  ctx.beginPath(); ctx.moveTo(11.5*s,-3.2*s); ctx.lineTo(13.4*s,-4.8*s); ctx.stroke();
  ctx.beginPath(); ctx.moveTo(8.6*s,-3.4*s); ctx.lineTo(8.0*s,-5.0*s); ctx.stroke();
  ctx.strokeStyle=getAccent(0.20); ctx.lineWidth=1.2*s;
  ctx.beginPath(); ctx.moveTo(10*s,-3.0*s); ctx.lineTo(10*s,1.2*s); ctx.moveTo(10*s,-3.0*s); ctx.lineTo(8.8*s,-1.6*s); ctx.moveTo(10*s,-3.0*s); ctx.lineTo(11.2*s,-1.6*s); ctx.stroke();
  ctx.fillStyle=white; ctx.beginPath(); ctx.ellipse(-12*s,-0.6*s, 2.6*s, 3.2*s, 0, 0, 7); ctx.fill();
  ctx.restore();
}
function drawAmbientFlyers(t, biome){
  const type=_flyerType(biome);
  const N=(type==='raven')?3:4, horizon=GROUND();
  for(let i=0;i<N;i++){
    const period=26+i*11+(i*i)%5; const ph=t/period + i*0.41 + hashY(i*9+3); const cyc=ph-Math.floor(ph);
    const span=0.62; if(cyc>span) continue; const cross=cyc/span; const dir=(i%2)?-1:1; const margin=90*DPR;
    const x = dir>0 ? -margin + cross*(W+2*margin) : (W+margin) - cross*(W+2*margin);
    const lane = hashY(i*13+7); const y = (0.08+0.34*lane)*horizon + Math.sin(t*0.5+i)*5*DPR;
    const s = (0.62+0.5*hashS(i*5+2))*DPR; const edge = Math.min(1, cross/0.10, (1-cross)/0.10);
    ctx.save(); ctx.globalAlpha=Math.max(0,edge);
    if(type==='heli')        _ambHeli(x,y,s,t,i,dir);
    else if(type==='dragon') _ambDragon(x,y,s,t,i,dir);
    else if(type==='jet')    _ambJet(x,y,s,t,i,dir);
    else if(type==='bison')  _ambBison(x,y,s,t,i,dir);
    else if(type==='raven')  _ambRaven(x,y,s,t,i,dir);
    else if(type==='balloon')_ambBalloon(x,y,s,t,i);
    else                     _ambUfo(x,y,s,t,i);
    ctx.restore();
  }
}
// ===== DANTE'S INFURNO secret level + abduction VFX =====
function _h(i){ const s = Math.sin(i*127.1+311.7)*43758.5453; return s - Math.floor(s); }
function _h2(i){ const s = Math.sin(i*269.5+183.3)*43758.5453; return s - Math.floor(s); }
function drawBigUfo(cx, cy, t, scale){
  const S = (scale||1) * DPR;
  const halfW = (70 * S) * 0.5;
  ctx.save();
  ctx.translate(cx, cy);
  ctx.save();
  ctx.globalCompositeOperation = 'lighter';
  const halo = ctx.createRadialGradient(0, 6*S, 0, 0, 6*S, halfW*2.2);
  halo.addColorStop(0, 'rgba(120,255,220,0.30)');
  halo.addColorStop(0.4, 'rgba(80,180,255,0.12)');
  halo.addColorStop(1, 'rgba(80,180,255,0)');
  ctx.fillStyle = halo;
  ctx.beginPath(); ctx.ellipse(0, 6*S, halfW*2.2, halfW*1.1, 0, 0, Math.PI*2); ctx.fill();
  ctx.restore();
  const discH = halfW*0.42, discY = 2*S;
  const g = ctx.createLinearGradient(0, discY-discH, 0, discY+discH);
  g.addColorStop(0, '#dfe7ef'); g.addColorStop(0.45, '#9aa8b6'); g.addColorStop(0.5, '#7b8896'); g.addColorStop(0.62, '#4a5560'); g.addColorStop(1, '#2a323b');
  ctx.fillStyle = g; ctx.beginPath(); ctx.ellipse(0, discY, halfW, discH, 0, 0, Math.PI*2); ctx.fill();
  ctx.lineWidth = 1.4*S; ctx.strokeStyle = 'rgba(230,245,255,0.5)';
  ctx.beginPath(); ctx.ellipse(0, discY-discH*0.12, halfW*0.98, discH*0.9, 0, Math.PI*1.05, Math.PI*1.95); ctx.stroke();
  const bg = ctx.createLinearGradient(0, discY, 0, discY+discH*1.6);
  bg.addColorStop(0, '#3a434d'); bg.addColorStop(1, '#1a1f25');
  ctx.fillStyle = bg; ctx.beginPath(); ctx.ellipse(0, discY+discH*0.35, halfW*0.78, discH*0.85, 0, 0, Math.PI); ctx.fill();
  const domeR = halfW*0.46, domeY = discY - discH*0.55;
  const dg = ctx.createRadialGradient(-domeR*0.3, domeY-domeR*0.5, domeR*0.1, 0, domeY, domeR*1.2);
  dg.addColorStop(0, 'rgba(190,255,240,0.95)'); dg.addColorStop(0.45, 'rgba(90,200,210,0.65)'); dg.addColorStop(1, 'rgba(40,90,120,0.4)');
  ctx.fillStyle = dg; ctx.beginPath(); ctx.ellipse(0, domeY, domeR, domeR*1.05, 0, Math.PI, Math.PI*2); ctx.fill();
  ctx.save(); ctx.globalCompositeOperation = 'lighter';
  const pulse = 0.55 + 0.45*Math.sin(t*3);
  const cg = ctx.createRadialGradient(0, domeY-domeR*0.2, 0, 0, domeY-domeR*0.2, domeR*0.9);
  cg.addColorStop(0, 'rgba(180,120,255,'+(0.5*pulse)+')'); cg.addColorStop(1, 'rgba(180,120,255,0)');
  ctx.fillStyle = cg; ctx.beginPath(); ctx.ellipse(0, domeY-domeR*0.2, domeR*0.8, domeR*0.8, 0, 0, Math.PI*2); ctx.fill();
  ctx.restore();
  ctx.fillStyle = 'rgba(255,255,255,0.5)';
  ctx.beginPath(); ctx.ellipse(-domeR*0.35, domeY-domeR*0.4, domeR*0.16, domeR*0.4, -0.5, 0, Math.PI*2); ctx.fill();
  ctx.save(); ctx.globalCompositeOperation = 'lighter';
  const N = 11, ringRx = halfW*0.86, ringRy = discH*0.82, ringY = discY+discH*0.28;
  const cols = [[120,255,200],[120,200,255],[200,120,255],[120,255,255]];
  for(let i=0;i<N;i++){
    const ph = i/N*Math.PI*2; if(Math.sin(ph) < -0.15) continue;
    const lx = Math.cos(ph)*ringRx, ly = ringY + Math.sin(ph)*ringRy;
    const cyc = (Math.sin(t*4 + i*0.9)*0.5+0.5), c = cols[i % cols.length], lr = (2.6 + 2.4*cyc)*S;
    const lg = ctx.createRadialGradient(lx, ly, 0, lx, ly, lr*2.4);
    lg.addColorStop(0, 'rgba('+c[0]+','+c[1]+','+c[2]+','+(0.95*cyc+0.2)+')'); lg.addColorStop(1, 'rgba('+c[0]+','+c[1]+','+c[2]+',0)');
    ctx.fillStyle = lg; ctx.beginPath(); ctx.arc(lx, ly, lr*2.4, 0, Math.PI*2); ctx.fill();
    ctx.fillStyle = 'rgba(255,255,255,'+(0.5*cyc+0.3)+')'; ctx.beginPath(); ctx.arc(lx, ly, lr*0.5, 0, Math.PI*2); ctx.fill();
  }
  ctx.restore();
  ctx.restore();
}
function drawTractorBeam(cx, topY, botY, p, t){
  if(p<=0) return; p = Math.max(0, Math.min(1, p));
  const H_ = botY - topY, topR = 14 * DPR * (0.6+0.4*p), botR = (90 * DPR) * (0.7+0.3*p);
  ctx.save(); ctx.globalCompositeOperation = 'lighter';
  const beamPulse = 0.85 + 0.15*Math.sin(t*6);
  const cg = ctx.createLinearGradient(0, topY, 0, botY);
  cg.addColorStop(0, 'rgba(140,255,220,'+(0.55*p*beamPulse)+')'); cg.addColorStop(0.5, 'rgba(90,220,255,'+(0.30*p*beamPulse)+')'); cg.addColorStop(1, 'rgba(120,255,200,'+(0.10*p)+')');
  ctx.fillStyle = cg; ctx.beginPath(); ctx.moveTo(cx-topR, topY); ctx.lineTo(cx+topR, topY); ctx.lineTo(cx+botR, botY); ctx.lineTo(cx-botR, botY); ctx.closePath(); ctx.fill();
  const ic = ctx.createLinearGradient(0, topY, 0, botY);
  ic.addColorStop(0, 'rgba(220,255,245,'+(0.7*p*beamPulse)+')'); ic.addColorStop(1, 'rgba(160,255,230,0)');
  ctx.fillStyle = ic; ctx.beginPath(); ctx.moveTo(cx-topR*0.5, topY); ctx.lineTo(cx+topR*0.5, topY); ctx.lineTo(cx+botR*0.45, botY); ctx.lineTo(cx-botR*0.45, botY); ctx.closePath(); ctx.fill();
  ctx.strokeStyle = 'rgba(200,255,235,'+(0.35*p)+')'; ctx.lineWidth = 1.5*DPR;
  ctx.beginPath(); ctx.moveTo(cx-topR,topY); ctx.lineTo(cx-botR,botY); ctx.moveTo(cx+topR,topY); ctx.lineTo(cx+botR,botY); ctx.stroke();
  const M = 26;
  for(let i=0;i<M;i++){
    const seed = _h(i), speed = 0.20 + _h2(i)*0.35; let ph = (seed + t*speed) % 1;
    const yfrac = 1 - ph, y = topY + yfrac*H_, curR = topR + (botR-topR)*yfrac;
    const swirl = Math.sin(t*2 + i*1.7 + yfrac*6) * curR * 0.7, x = cx + swirl;
    const fade = Math.sin(ph*Math.PI), sz = (1.4 + 2.6*_h(i+99)) * DPR * (0.5+0.5*yfrac), a = 0.8 * fade * p;
    const c = (i%3===0)?'180,255,200':(i%3===1)?'160,230,255':'210,170,255';
    const mg = ctx.createRadialGradient(x,y,0,x,y,sz*3); mg.addColorStop(0,'rgba('+c+','+a+')'); mg.addColorStop(1,'rgba('+c+',0)');
    ctx.fillStyle = mg; ctx.beginPath(); ctx.arc(x,y,sz*3,0,Math.PI*2); ctx.fill();
  }
  const poolPulse = 0.8 + 0.2*Math.sin(t*5+1);
  const pg = ctx.createRadialGradient(cx, botY, 0, cx, botY, botR*1.3);
  pg.addColorStop(0, 'rgba(230,255,245,'+(0.85*p*poolPulse)+')'); pg.addColorStop(0.35, 'rgba(130,255,210,'+(0.45*p)+')'); pg.addColorStop(1, 'rgba(110,220,255,0)');
  ctx.fillStyle = pg; ctx.beginPath(); ctx.ellipse(cx, botY, botR*1.3, botR*0.4, 0, 0, Math.PI*2); ctx.fill();
  ctx.fillStyle = 'rgba(255,255,255,'+(0.5*p*poolPulse)+')'; ctx.beginPath(); ctx.ellipse(cx, botY, botR*0.5, botR*0.16, 0, 0, Math.PI*2); ctx.fill();
  ctx.restore();
}
function drawInfurnoBg(t, p){
  p = Math.max(0, Math.min(1, p)); const gy = GROUND(); ctx.save();
  const sky = ctx.createLinearGradient(0,0,0,gy);
  sky.addColorStop(0, '#0a0204'); sky.addColorStop(0.35, '#2a0808'); sky.addColorStop(0.68, '#5c1606'); sky.addColorStop(0.86, '#a8360a'); sky.addColorStop(1, '#e0691a');
  ctx.fillStyle = sky; ctx.fillRect(0,0,W,gy);
  ctx.fillStyle = '#070202'; ctx.fillRect(0,gy,W,H-gy);
  ctx.save(); ctx.globalCompositeOperation = 'lighter';
  const hg = ctx.createRadialGradient(W*0.5, gy, 0, W*0.5, gy, W*0.7), breathe = 0.8 + 0.2*Math.sin(t*0.8);
  hg.addColorStop(0, 'rgba(255,150,40,'+(0.55*p*breathe)+')'); hg.addColorStop(0.4, 'rgba(220,90,20,'+(0.25*p)+')'); hg.addColorStop(1, 'rgba(150,40,10,0)');
  ctx.fillStyle = hg; ctx.beginPath(); ctx.ellipse(W*0.5, gy, W*0.7, gy*0.85, 0, 0, Math.PI*2); ctx.fill(); ctx.restore();
  function spireRange(baseY, scaleH, shade, count, seedOff, alpha){
    ctx.fillStyle = shade; ctx.globalAlpha = alpha; ctx.beginPath(); ctx.moveTo(0, baseY);
    for(let i=0;i<=count;i++){ const f = i/count, x = f*W, hh = (0.4 + _h(i+seedOff)*0.6) * scaleH, jag = (_h2(i*2+seedOff)*0.5) * scaleH;
      ctx.lineTo(x, baseY - hh); ctx.lineTo(x + W/count*0.5, baseY - hh*0.45 - jag*0.3); }
    ctx.lineTo(W, baseY); ctx.closePath(); ctx.fill(); ctx.globalAlpha = 1;
  }
  spireRange(gy*0.92, gy*0.42, '#1c0805', 9, 0, 0.7);
  ctx.save(); ctx.globalCompositeOperation='lighter';
  for(let i=0;i<4;i++){
    const ax = W*(0.18 + i*0.22) + Math.sin(t*0.3+i)*4*DPR, aw = (70+ i*10)*DPR, ah = (150+ (i%2)*40)*DPR, ay = gy*0.9;
    const arc = ctx.createLinearGradient(ax, ay-ah, ax, ay); arc.addColorStop(0,'rgba(255,120,30,0)'); arc.addColorStop(1,'rgba(255,140,40,'+(0.10*p)+')');
    ctx.fillStyle = arc; ctx.beginPath(); ctx.moveTo(ax-aw*0.5, ay); ctx.lineTo(ax-aw*0.5, ay-ah*0.6); ctx.quadraticCurveTo(ax, ay-ah, ax+aw*0.5, ay-ah*0.6); ctx.lineTo(ax+aw*0.5, ay); ctx.closePath(); ctx.fill();
  }
  ctx.restore();
  spireRange(gy*1.0, gy*0.30, '#0d0302', 13, 50, 0.95);
  const lavaTop = gy - 6*DPR, lavaBot = Math.min(H, gy + 26*DPR);
  const lg = ctx.createLinearGradient(0, lavaTop, 0, lavaBot);
  lg.addColorStop(0, '#ffe27a'); lg.addColorStop(0.25,'#ffb02e'); lg.addColorStop(0.6, '#e8500c'); lg.addColorStop(1, '#5c1404');
  ctx.fillStyle = lg; ctx.fillRect(0, lavaTop, W, lavaBot-lavaTop);
  ctx.save(); ctx.globalCompositeOperation='lighter';
  for(let i=0;i<40;i++){
    const bx = ((_h(i)*W) + Math.sin(t*0.5 + i)* 14*DPR) % W, by = lavaTop + (2 + _h2(i)*10)*DPR;
    const pop = Math.sin(t*2.2 + i*1.3)*0.5+0.5, br = (1.5 + 4*_h(i+7))*DPR * (0.5+0.5*pop);
    const bg2 = ctx.createRadialGradient(bx,by,0,bx,by,br*2.5); bg2.addColorStop(0,'rgba(255,230,150,'+(0.7*pop*p)+')'); bg2.addColorStop(0.5,'rgba(255,120,30,'+(0.3*pop*p)+')'); bg2.addColorStop(1,'rgba(255,80,20,0)');
    ctx.fillStyle=bg2; ctx.beginPath(); ctx.arc(bx,by,br*2.5,0,Math.PI*2); ctx.fill();
  }
  ctx.strokeStyle = 'rgba(255,235,160,'+(0.6*p)+')'; ctx.lineWidth = 2*DPR; ctx.beginPath();
  for(let x=0;x<=W;x+=8*DPR){ const yy = lavaTop + Math.sin(x*0.02 + t*1.5)*2.2*DPR + Math.sin(x*0.05 - t*2)*1.5*DPR; if(x===0) ctx.moveTo(x,yy); else ctx.lineTo(x,yy); }
  ctx.stroke(); ctx.restore();
  ctx.save(); ctx.globalCompositeOperation='lighter'; const EM = 70;
  for(let i=0;i<EM;i++){
    const seed = _h(i), seed2 = _h2(i), speed = 14 + seed2*40, life = 4 + seed*3, ph = ((t*speed/(life*40)) + seed) % 1;
    const startX = seed*W, sway = Math.sin(t*(0.6+seed2) + i)* (10+seed*22)*DPR, x = (startX + sway + W) % W, y = gy - ph*(gy*1.05);
    const fade = Math.sin(ph*Math.PI), sz = (0.8 + 1.8*seed2)*DPR, a = 0.85*fade*p;
    const cgc = Math.floor(150 + 80*seed), cb = Math.floor(30*seed2);
    const eg = ctx.createRadialGradient(x,y,0,x,y,sz*3); eg.addColorStop(0,'rgba(255,'+cgc+','+cb+','+a+')'); eg.addColorStop(1,'rgba(255,'+cgc+','+cb+',0)');
    ctx.fillStyle=eg; ctx.beginPath(); ctx.arc(x,y,sz*3,0,Math.PI*2); ctx.fill();
  }
  ctx.restore();
  ctx.save(); ctx.globalCompositeOperation='lighter'; const shimH = gy - gy*0.55;
  for(let i=0;i<3;i++){
    const yy = gy - (i+1)*shimH*0.22 + Math.sin(t*1.2 + i)*3*DPR;
    const sgrad = ctx.createLinearGradient(0,yy-20*DPR,0,yy+20*DPR); sgrad.addColorStop(0,'rgba(255,140,40,0)'); sgrad.addColorStop(0.5,'rgba(255,150,50,'+(0.05*p)+')'); sgrad.addColorStop(1,'rgba(255,140,40,0)');
    ctx.fillStyle=sgrad; ctx.fillRect(0,yy-20*DPR,W,40*DPR);
  }
  ctx.restore();
  if(p<1){ ctx.fillStyle='rgba(0,0,0,'+(1-p)+')'; ctx.fillRect(0,0,W,H); }
  ctx.restore();
}
function drawInfHazard(h,t){ // falling lethal fireball/meteor
  ctx.save();
  const tg=ctx.createLinearGradient(h.x,h.y-h.r*2.8,h.x,h.y);
  tg.addColorStop(0,"rgba(255,120,30,0)"); tg.addColorStop(1,"rgba(255,90,20,.5)");
  ctx.fillStyle=tg; ctx.beginPath(); ctx.moveTo(h.x-h.r*0.5,h.y); ctx.lineTo(h.x,h.y-h.r*2.8); ctx.lineTo(h.x+h.r*0.5,h.y); ctx.closePath(); ctx.fill();
  const fl=0.85+0.15*Math.sin(t*20+h.wob);
  const g=ctx.createRadialGradient(h.x,h.y,1,h.x,h.y,h.r*1.45*fl);
  g.addColorStop(0,"#fff7d6"); g.addColorStop(0.35,"#ffd14d"); g.addColorStop(0.7,"#ff7a1a"); g.addColorStop(1,"rgba(255,45,40,0)");
  ctx.fillStyle=g; ctx.beginPath(); ctx.arc(h.x,h.y,h.r*1.45*fl,0,7); ctx.fill();
  ctx.fillStyle="#ff3a2a"; ctx.beginPath(); ctx.arc(h.x,h.y,h.r*0.55,0,7); ctx.fill();
  ctx.restore();
}
function drawInfurnoCoin(x, y, r, t){
  ctx.save(); ctx.translate(x,y);
  ctx.save(); ctx.globalCompositeOperation='lighter';
  const gl = ctx.createRadialGradient(0,0,0,0,0,r*2); gl.addColorStop(0,'rgba(255,210,80,0.5)'); gl.addColorStop(1,'rgba(255,180,40,0)');
  ctx.fillStyle=gl; ctx.beginPath(); ctx.arc(0,0,r*2,0,Math.PI*2); ctx.fill(); ctx.restore();
  const spin = t*3 + x*0.05 + y*0.03, faceW = Math.max(0.12, Math.abs(Math.cos(spin)));
  ctx.fillStyle = '#a9750f'; ctx.beginPath(); ctx.ellipse(0,0, r*faceW + r*0.12, r, 0,0,Math.PI*2); ctx.fill();
  const fg = ctx.createLinearGradient(-r*faceW, -r, r*faceW, r);
  fg.addColorStop(0, '#fff1b0'); fg.addColorStop(0.45,'#ffce46'); fg.addColorStop(0.7, '#f5a623'); fg.addColorStop(1, '#b9760c');
  ctx.fillStyle = fg; ctx.beginPath(); ctx.ellipse(0,0, r*faceW, r, 0,0,Math.PI*2); ctx.fill();
  ctx.lineWidth = Math.max(1, r*0.08); ctx.strokeStyle = 'rgba(255,245,200,0.7)';
  ctx.beginPath(); ctx.ellipse(0,0, r*faceW*0.78, r*0.78, 0,0,Math.PI*2); ctx.stroke();
  ctx.fillStyle='rgba(180,110,10,0.55)'; ctx.beginPath();
  const spikes=5, outR=r*0.42, inR=r*0.18;
  for(let i=0;i<spikes*2;i++){ const rr=(i%2===0)?outR:inR, ang=-Math.PI/2 + i*Math.PI/spikes, px=Math.cos(ang)*rr*faceW, py=Math.sin(ang)*rr; if(i===0)ctx.moveTo(px,py); else ctx.lineTo(px,py); }
  ctx.closePath(); ctx.fill();
  ctx.save(); ctx.globalCompositeOperation='lighter';
  const sheenX = Math.sin(spin*1.3)*r*faceW*0.6;
  const sh = ctx.createRadialGradient(sheenX,-r*0.3,0,sheenX,-r*0.3,r*0.8); sh.addColorStop(0,'rgba(255,255,255,0.75)'); sh.addColorStop(1,'rgba(255,255,255,0)');
  ctx.fillStyle=sh; ctx.beginPath(); ctx.ellipse(sheenX,-r*0.3, r*faceW*0.5, r*0.5, 0,0,Math.PI*2); ctx.fill();
  const sp = Math.sin(t*4 + x)*0.5+0.5; ctx.fillStyle='rgba(255,255,255,'+(0.9*sp)+')';
  const spx=-r*0.3*faceW, spy=-r*0.45, sl=r*0.3*sp;
  ctx.beginPath(); ctx.moveTo(spx-sl,spy); ctx.lineTo(spx,spy-sl); ctx.lineTo(spx+sl,spy); ctx.lineTo(spx,spy+sl); ctx.closePath(); ctx.fill();
  ctx.restore(); ctx.restore();
}
// ---- biome weather FX + cinematic darkening (game-designer) ----
const _RN=120, _rx=new Float32Array(_RN), _rv=new Float32Array(_RN), _rl=new Float32Array(_RN), _ryy=new Float32Array(_RN);
for(let _i=0;_i<_RN;_i++){ _rx[_i]=Math.random(); _ryy[_i]=Math.random(); _rv[_i]=0.55+Math.random()*0.6; _rl[_i]=10+Math.random()*16; }
function _r1(n){ n=(n^61)^(n>>>16); n=n+(n<<3); n=n^(n>>>4); n=Math.imul(n,0x27d4eb2d); n=n^(n>>>15); return ((n>>>0)%100000)/100000; }
function fxRain(t,inten){ if(inten<=0) return; ctx.save(); ctx.lineCap="round"; ctx.strokeStyle="rgba(180,230,255,"+(0.35*inten)+")"; ctx.lineWidth=1.3*DPR; ctx.beginPath();
  for(let i=0;i<_RN;i++){ const cyc=(_ryy[i]+t*_rv[i]*0.9)%1; const y=cyc*(H+40)-20; const x=(_rx[i]%1)*(W+80)-40-y*0.06; const len=_rl[i]*DPR; ctx.moveTo(x,y); ctx.lineTo(x-len*0.32,y+len); } ctx.stroke(); ctx.restore(); }
function fxLightning(t,inten){ if(inten<=0) return; const g=GROUND(), PERIOD=3.2-1.2*Math.min(1,inten), local=t%PERIOD, strike=Math.floor(t/PERIOD), fd=0.32; if(local>fd) return; const k=1-(local/fd), flash=k*k;
  ctx.save(); ctx.fillStyle="rgba(235,235,255,"+(0.5*flash*inten)+")"; ctx.fillRect(0,0,W,H);
  const x0=(0.2+_r1(strike*3)*0.6)*W; ctx.lineCap="round"; ctx.lineJoin="round"; ctx.shadowColor="rgba(180,200,255,0.9)"; ctx.shadowBlur=18*DPR; ctx.strokeStyle="rgba(245,248,255,"+(0.95*(0.4+0.6*flash))+")"; ctx.lineWidth=2.6*DPR; ctx.beginPath(); let bx=x0; ctx.moveTo(bx,-10);
  for(let i=1;i<=9;i++){ const jit=(_r1(strike*97+i*31)-0.5)*(W*0.11); bx=x0+jit*(i/9)+(_r1(strike+i)-0.5)*26*DPR; ctx.lineTo(bx,(i/9)*(g+10)); } ctx.stroke(); ctx.restore(); }
function fxLava(t,inten){ if(inten<=0) return; const g=GROUND(), band=26*DPR, top=g-band; ctx.save();
  const gr=ctx.createLinearGradient(0,top,0,g+6*DPR); gr.addColorStop(0,"rgba(255,120,30,0)"); gr.addColorStop(0.55,"rgba(255,90,20,"+(0.55*inten)+")"); gr.addColorStop(1,"rgba(255,200,60,"+(0.9*inten)+")"); ctx.fillStyle=gr; ctx.fillRect(0,top,W,band+8*DPR);
  ctx.globalCompositeOperation="lighter"; for(let layer=0;layer<2;layer++){ const amp=(layer?5:8)*DPR, ph=t*(layer?1.7:1.1)+cam*0.0015, baseY=g-(layer?4:10)*DPR; ctx.beginPath(); ctx.moveTo(0,baseY); for(let x=0;x<=W;x+=14*DPR){ const y=baseY+Math.sin(x*0.018+ph)*amp+Math.sin(x*0.005-ph*0.7)*amp*0.5; ctx.lineTo(x,y); } ctx.lineTo(W,g+8*DPR); ctx.lineTo(0,g+8*DPR); ctx.closePath(); ctx.fillStyle=layer?"rgba(255,230,120,"+(0.30*inten)+")":"rgba(255,110,30,"+(0.35*inten)+")"; ctx.fill(); } ctx.restore(); }
function biomeFX(biome,t){ const inten=0.55+0.45*Math.min(1,distM/9000); if(biome.fx==="rain") fxRain(t,inten); else if(biome.fx==="lightning") fxLightning(t,inten); else if(biome.fx==="lava") fxLava(t,inten);
  const dk=0.5*Math.pow(Math.max(0,Math.min(1,(distM-5000)/9000)),2); if(dk>0){ const gr=ctx.createRadialGradient(W*0.5,H*0.45,H*0.2,W*0.5,H*0.5,H*0.85); gr.addColorStop(0,"rgba(4,2,12,0)"); gr.addColorStop(1,"rgba(4,2,12,"+dk+")"); ctx.fillStyle=gr; ctx.fillRect(0,0,W,H); } }
// upgraded dog house (game-designer): planked roof, bone, dark fanged mouth, glowing eyes
function drawDogHouse(x,o,t){ const w=o.w,h=o.h,gy=o.y,bx=x-w/2,bodyTop=gy-h,roofH=h*0.42,roofTop=bodyTop-roofH,cx=x; ctx.save();
  ctx.fillStyle="rgba(0,0,0,0.28)"; ctx.beginPath(); ctx.ellipse(cx,gy+2*DPR,w*0.6,6*DPR,0,0,Math.PI*2); ctx.fill();
  const bg=ctx.createLinearGradient(0,bodyTop,0,gy); bg.addColorStop(0,"#6b4a36"); bg.addColorStop(1,"#4a3122"); ctx.fillStyle=bg; ctx.fillRect(bx,bodyTop,w,h);
  ctx.strokeStyle="rgba(0,0,0,0.22)"; ctx.lineWidth=1*DPR; for(let py=bodyTop+h*0.28; py<gy; py+=h*0.26){ ctx.beginPath(); ctx.moveTo(bx+1,py); ctx.lineTo(bx+w-1,py); ctx.stroke(); }
  const oh=w*0.10; ctx.beginPath(); ctx.moveTo(bx-oh,bodyTop); ctx.lineTo(cx,roofTop); ctx.lineTo(bx+w+oh,bodyTop); ctx.closePath(); const rg=ctx.createLinearGradient(0,roofTop,0,bodyTop); rg.addColorStop(0,"#a23b3b"); rg.addColorStop(1,"#6f2222"); ctx.fillStyle=rg; ctx.fill();
  ctx.strokeStyle="#3f1414"; ctx.lineWidth=2*DPR; ctx.beginPath(); ctx.moveTo(bx-oh,bodyTop); ctx.lineTo(cx,roofTop); ctx.lineTo(bx+w+oh,bodyTop); ctx.stroke();
  ctx.save(); ctx.translate(cx,roofTop+roofH*0.5); ctx.fillStyle="#f3ead2"; const bl=w*0.16,br2=3*DPR; ctx.fillRect(-bl,-1.6*DPR,bl*2,3.2*DPR); for(const sx of [-bl,bl]) for(const sy of [-1,1]){ ctx.beginPath(); ctx.arc(sx,sy*3*DPR,br2,0,Math.PI*2); ctx.fill(); } ctx.restore();
  const mw=w*0.5,mh=h*0.66,mlx=cx-mw/2,mby=gy,mty=gy-mh; ctx.beginPath(); ctx.moveTo(mlx,mby); ctx.lineTo(mlx,mty+mw*0.4); ctx.quadraticCurveTo(mlx,mty,cx,mty); ctx.quadraticCurveTo(mlx+mw,mty,mlx+mw,mty+mw*0.4); ctx.lineTo(mlx+mw,mby); ctx.closePath();
  const mG=ctx.createRadialGradient(cx,mby,2*DPR,cx,mby,mh); mG.addColorStop(0,"#000000"); mG.addColorStop(0.7,"#0a0306"); mG.addColorStop(1,"#1a0608"); ctx.fillStyle=mG; ctx.fill();
  ctx.lineWidth=2.4*DPR; ctx.strokeStyle="rgba(255,70,70,0.85)"; ctx.setLineDash([6*DPR,5*DPR]); ctx.lineDashOffset=-(t*30)%1000; ctx.stroke(); ctx.setLineDash([]);
  ctx.save(); ctx.clip(); ctx.fillStyle="rgba(225,225,235,0.65)"; const tn=5,tw=mw/tn; for(let i=0;i<tn;i++){ const txc=mlx+tw*(i+0.5); ctx.beginPath(); ctx.moveTo(mlx+tw*i,mty+mw*0.32); ctx.lineTo(mlx+tw*(i+1),mty+mw*0.32); ctx.lineTo(txc,mty+mw*0.32+8*DPR); ctx.closePath(); ctx.fill(); ctx.beginPath(); ctx.moveTo(mlx+tw*i,mby); ctx.lineTo(mlx+tw*(i+1),mby); ctx.lineTo(txc,mby-7*DPR); ctx.closePath(); ctx.fill(); }
  const blink=(Math.sin(t*2.0)>-0.6)?1:0.15; ctx.fillStyle="rgba(255,90,60,"+(0.85*blink)+")"; ctx.beginPath(); ctx.arc(cx-mw*0.16,mty+mh*0.42,2.4*DPR,0,Math.PI*2); ctx.fill(); ctx.beginPath(); ctx.arc(cx+mw*0.16,mty+mh*0.42,2.4*DPR,0,Math.PI*2); ctx.fill(); ctx.restore();
  ctx.fillStyle="rgba(20,10,6,0.5)"; for(const pawx of [bx+w*0.12,bx+w*0.88]){ ctx.beginPath(); ctx.ellipse(pawx,gy-3*DPR,4*DPR,3*DPR,0,0,Math.PI*2); ctx.fill(); }
  ctx.restore(); }
function drawWaterOverlay(t){ if(waterStart>0){ const ws=waterStart-cam, we=waterEnd-cam; if(we>0 && ws<W){ const x0=Math.max(0,ws), x1=Math.min(W,we), g0=GROUND(); // lethal WATER stretch overlay (drawn in BOTH bg modes so it's never an invisible insta-hazard)
    ctx.save(); ctx.beginPath(); ctx.rect(x0,g0,x1-x0,H-g0); ctx.clip();
    let wg=ctx.createLinearGradient(0,g0,0,H); wg.addColorStop(0,"rgba(40,150,210,.94)"); wg.addColorStop(1,"rgba(10,55,105,.97)"); ctx.fillStyle=wg; ctx.fillRect(x0,g0,x1-x0,H-g0);
    for(let k=0;k<3;k++){ ctx.strokeStyle="rgba(200,240,255,"+(0.55-k*0.14)+")"; ctx.lineWidth=2*DPR; ctx.beginPath(); for(let x=x0-8*DPR;x<=x1+8*DPR;x+=8*DPR){ const yy=g0+6*DPR+k*11*DPR+Math.sin((x+cam)*0.03+t*3+k)*3*DPR; x<=x0-8*DPR?ctx.moveTo(x,yy):ctx.lineTo(x,yy); } ctx.stroke(); }
    ctx.restore();
    ctx.fillStyle="rgba(190,238,255,.8)"; if(ws>-4*DPR&&ws<W) ctx.fillRect(ws-2*DPR,g0-4*DPR,4*DPR,8*DPR); if(we>0&&we<W+4*DPR) ctx.fillRect(we-2*DPR,g0-4*DPR,4*DPR,8*DPR); // banks
  }} }
function drawBackground(t,biome){
  if(bgImgs.length){
    ctx.fillStyle="#0b1330"; ctx.fillRect(0,0,W,H);
    const im=bgImgs[bgIdx]; const s=Math.max(W/im.width, H/im.height); const iw=im.width*s, ih=im.height*s;
    const ox=-(cam*0.25)%iw; ctx.globalAlpha=.92;
    for(let x=ox-iw; x<W+iw; x+=iw) ctx.drawImage(im, x, H-ih, iw, ih);
    ctx.globalAlpha=1;
    ctx.fillStyle="#0c1326"; ctx.fillRect(0,GROUND(),W,H-GROUND());
    ctx.strokeStyle=getAccent(.6); ctx.lineWidth=2*DPR; ctx.beginPath(); ctx.moveTo(0,GROUND()); ctx.lineTo(W,GROUND()); ctx.stroke();
    drawWaterOverlay(t);
    return;
  }
  // biome sky (evolves with distance)
  const g=ctx.createLinearGradient(0,0,0,GROUND());
  g.addColorStop(0,biome.sky[0]); g.addColorStop(.5,biome.sky[1]); g.addColorStop(1,biome.sky[2]);
  ctx.fillStyle=g; ctx.fillRect(0,0,W,GROUND());
  drawOverdriveSky(t,biome);   // deep-run animated aurora wash (overdrive only)
  applySkyIntensity(t,biome);  // distance-ramped saturation/contrast lift + breathing accent glow
  drawScapes(t, biome);      // evolving distant landscape (style varies per biome)
  drawAmbientFlyers(t, biome);  // biome-themed flyers: UFO / helicopter / raven / balloon
  // driftmotes appear deeper in (>400m): glowing rising particles
  if(distM>400){ const n=Math.min(28+Math.round(20*INTEN), Math.floor((distM-400)/120)+6); const _rise=26*(1+0.6*INTEN);
    for(let i=0;i<n;i++){ const mx=(((i*137*DPR - cam*0.5)%W)+W)%W + Math.sin(t*0.7+i)*9*DPR;
      const my=GROUND() - ((t*_rise*DPR + i*97*DPR) % GROUND()); const r=(1.4+1.6*hashS(i))*DPR;
      ctx.fillStyle=getAccent(.5); ctx.beginPath(); ctx.arc(mx,my,r,0,7); ctx.fill(); } }
  drawFloor(biome,t); // themed per-biome floor (or default neon grid)
  if(waterStart>0){ const ws=waterStart-cam, we=waterEnd-cam; if(we>0 && ws<W){ const x0=Math.max(0,ws), x1=Math.min(W,we), g0=GROUND(); // lethal WATER stretch overlay
    ctx.save(); ctx.beginPath(); ctx.rect(x0,g0,x1-x0,H-g0); ctx.clip();
    let wg=ctx.createLinearGradient(0,g0,0,H); wg.addColorStop(0,"rgba(40,150,210,.94)"); wg.addColorStop(1,"rgba(10,55,105,.97)"); ctx.fillStyle=wg; ctx.fillRect(x0,g0,x1-x0,H-g0);
    for(let k=0;k<3;k++){ ctx.strokeStyle="rgba(200,240,255,"+(0.55-k*0.14)+")"; ctx.lineWidth=2*DPR; ctx.beginPath(); for(let x=x0-8*DPR;x<=x1+8*DPR;x+=8*DPR){ const yy=g0+6*DPR+k*11*DPR+Math.sin((x+cam)*0.03+t*3+k)*3*DPR; x<=x0-8*DPR?ctx.moveTo(x,yy):ctx.lineTo(x,yy); } ctx.stroke(); }
    ctx.restore();
    ctx.fillStyle="rgba(190,238,255,.8)"; if(ws>-4*DPR&&ws<W) ctx.fillRect(ws-2*DPR,g0-4*DPR,4*DPR,8*DPR); if(we>0&&we<W+4*DPR) ctx.fillRect(we-2*DPR,g0-4*DPR,4*DPR,8*DPR); // banks
  }}
}
const THEMED_FLOOR = {}; // biome name -> drawFloorX(biome,t); themed floors register here
THEMED_FLOOR["Rainbow Road"]=drawFloorRainbowRoad; THEMED_FLOOR["Synthwave Grid"]=drawFloorSynthwave; THEMED_FLOOR["Vapurrwave"]=drawFloorVaporwave; THEMED_FLOOR["Bioluminescent Abyss"]=drawFloorAbyss; THEMED_FLOOR["Elysium"]=drawFloorElysium; THEMED_FLOOR["Zoomies Zone"]=drawFloorZoomies;
THEMED_FLOOR["Zero Purravity"]=drawFloorSynthwave; // Zero-G = neon synthwave grid floor (no grass/trees)
THEMED_FLOOR["Petal Purradise"]=drawFloorCandy;     // all-pink sakura = candy floor (pink, no grass)
THEMED_FLOOR["Crimson Dawn"]=drawFloorAbyss;        // red-cloud world = dark void floor
function drawFloor(biome,t){ const f=THEMED_FLOOR[biome.name]; if(f){ f(biome,t); return; } drawFloorDefault(biome); }
function drawFloorDefault(biome){
  ctx.fillStyle="#0a1024"; ctx.fillRect(0,GROUND(),W,H-GROUND());
  ctx.strokeStyle=getAccent(.85); ctx.lineWidth=3*DPR; ctx.beginPath(); ctx.moveTo(0,GROUND()); ctx.lineTo(W,GROUND()); ctx.stroke();
  const sp=70*DPR, voff=cam%sp; ctx.strokeStyle=getAccent(.22); ctx.lineWidth=2*DPR;
  for(let x=-voff; x<W; x+=sp){ ctx.beginPath(); ctx.moveTo(x,GROUND()); ctx.lineTo((x-W/2)*1.5+W/2, H); ctx.stroke(); }
  for(let i=1;i<=4;i++){ const y=GROUND()+i*i*5*DPR; if(y>H) break; ctx.globalAlpha=.18; ctx.beginPath(); ctx.moveTo(0,y); ctx.lineTo(W,y); ctx.stroke(); }
  ctx.globalAlpha=1;
}

// ===== themed per-biome FLOOR renderers (geometric / urban) =====
function drawFloorChess(biome,t){
  ctx.save();
  const D=DPR, g0=GROUND(), bh=H-g0;
  const dark="#16161e", light="#efe9d6";
  ctx.fillStyle=dark; ctx.fillRect(0,g0,W,bh);
  const SP=0.014, STEP=Math.max(2*D, bh/40);
  for(let y=g0; y<H; y+=STEP){
    const y1=Math.min(H, y+STEP), yc=(y+y1)/2;
    const dd=Math.max(0.5, yc-g0); const depth=bh/dd;
    const row=Math.floor(depth + cam*SP);
    const colW=dd*0.5;
    if(colW<4*D){ ctx.fillStyle=(row&1)?"#34343d":"#23232b"; ctx.fillRect(0,y,W,y1-y); continue; }
    let n=Math.floor((-W/2)/colW)-1, sx=W/2+n*colW;
    for(; sx<W; sx+=colW, n++){ if(sx+colW<0) continue; ctx.fillStyle=((row+n)&1)?light:dark; ctx.fillRect(sx, y, colW+1, y1-y); }
  }
  ctx.fillStyle=getAccent(.45); ctx.fillRect(0,g0,W,2*D);
  ctx.restore();
}
function drawFloorRoad(biome,t){
  ctx.save();
  const D=DPR, g0=GROUND(), bh=H-g0;
  ctx.fillStyle=silhouetteColor(biome.sky[2],0.92,0.45); ctx.fillRect(0,g0,W,bh);
  const RW=W*0.60, A=0.62, GAP=0.85, zf=40;
  const projY=z=>g0+bh*(A/(z+A)), scl=z=>A/(z+A);
  const sf=scl(zf), yf=projY(zf);
  ctx.beginPath(); ctx.moveTo(W/2-RW*sf,yf); ctx.lineTo(W/2+RW*sf,yf); ctx.lineTo(W/2+RW,H); ctx.lineTo(W/2-RW,H); ctx.closePath();
  ctx.fillStyle="#1b1e25"; ctx.fill();
  const edge=lf=>{ const xb=W/2+lf*RW, xt=W/2+lf*RW*sf, wb=5*D, wt=Math.max(1,5*D*sf);
    ctx.beginPath(); ctx.moveTo(xt-lf*wt,yf); ctx.lineTo(xt,yf); ctx.lineTo(xb,H); ctx.lineTo(xb-lf*wb,H); ctx.closePath(); ctx.fillStyle="#e9e3d0"; ctx.fill(); };
  edge(-1); edge(1);
  const dash=(lf,col,wbase)=>{ const ph=(cam*0.0011)%GAP;
    for(let k=0;k<26;k++){ let z0=k*GAP-ph; if(z0<-GAP) continue; const z1=z0+GAP*0.52; if(z1<=0) continue; const zz0=Math.max(0,z0);
      const y0=projY(zz0), y1=projY(z1), s0=scl(zz0), s1=scl(z1); const cx0=W/2+lf*RW*s0, cx1=W/2+lf*RW*s1; const w0=Math.max(1,wbase*s0), w1=Math.max(1,wbase*s1);
      ctx.beginPath(); ctx.moveTo(cx0-w0,y0); ctx.lineTo(cx0+w0,y0); ctx.lineTo(cx1+w1,y1); ctx.lineTo(cx1-w1,y1); ctx.closePath(); ctx.fillStyle=col; ctx.fill(); } };
  dash(0,"#f0d878",4*D); dash(-0.5,"#cfd2cf",2.4*D); dash(0.5,"#cfd2cf",2.4*D);
  ctx.fillStyle=getAccent(.5); ctx.fillRect(0,g0,W,2*D);
  ctx.restore();
}
function drawFloorCircuit(biome,t){
  ctx.save();
  const D=DPR, g0=GROUND(), bh=H-g0;
  ctx.fillStyle=silhouetteColor(biome.sky[2],0.96,0.55); ctx.fillRect(0,g0,W,bh);
  const CELL=26*D, cols=Math.ceil(W/CELL)+2, rows=Math.ceil(bh/CELL)+1, ox=cam%CELL;
  ctx.lineCap="square";
  for(let r=0;r<rows;r++){ const gy=g0+r*CELL+CELL*0.5; if(gy>H) break;
    for(let c=-1;c<cols;c++){ const gx=c*CELL-ox+CELL*0.5; const wc=Math.floor((gx+cam)/CELL); const k=wc*0.137+r*3.71;
      if(hashS(k)>0.34){ ctx.strokeStyle=getAccent(0.22); ctx.lineWidth=2*D; ctx.beginPath(); ctx.moveTo(gx,gy); ctx.lineTo(gx+CELL,gy); ctx.stroke(); }
      if(hashY(k)>0.55){ ctx.strokeStyle=getAccent(0.16); ctx.lineWidth=2*D; const vy=gy+(hashS(k+1)>0.5?CELL:-CELL)*0.6; ctx.beginPath(); ctx.moveTo(gx,gy); ctx.lineTo(gx,vy); ctx.stroke(); }
      if(hashS(k*1.7)>0.40){ const lit=hashY(k*2)>0.66, pulse=0.5+0.5*Math.sin(t*2+wc+r);
        ctx.fillStyle=getAccent(0.28+(lit?0.5*pulse:0.10)); ctx.beginPath(); ctx.arc(gx,gy,(lit?2.8:2.0)*D,0,6.283); ctx.fill();
        if(lit){ const gl=ctx.createRadialGradient(gx,gy,0,gx,gy,7*D); gl.addColorStop(0,getAccent(0.30*pulse)); gl.addColorStop(1,getAccent(0)); ctx.fillStyle=gl; ctx.beginPath(); ctx.arc(gx,gy,7*D,0,6.283); ctx.fill(); } } } }
  ctx.fillStyle=getAccent(.7); ctx.fillRect(0,g0,W,2*D);
  ctx.restore();
}
function drawFloorMetal(biome,t){
  ctx.save();
  const D=DPR, g0=GROUND(), bh=H-g0;
  const grd=ctx.createLinearGradient(0,g0,0,H); grd.addColorStop(0,"#2b2f38"); grd.addColorStop(0.5,"#3c424e"); grd.addColorStop(1,"#23272f");
  ctx.fillStyle=grd; ctx.fillRect(0,g0,W,bh);
  ctx.globalAlpha=0.05;
  for(let y=g0+3*D;y<H;y+=4*D){ ctx.strokeStyle=(((y/(4*D))|0)&1)?"#ffffff":"#000000"; ctx.lineWidth=1; ctx.beginPath(); ctx.moveTo(0,y); ctx.lineTo(W,y); ctx.stroke(); }
  ctx.globalAlpha=1;
  const PW=110*D, ox=cam%PW, midY=g0+bh*0.5;
  const rivet=(rx,ry)=>{ ctx.fillStyle="#191c23"; ctx.beginPath(); ctx.arc(rx,ry,3.2*D,0,6.283); ctx.fill();
    const rg=ctx.createRadialGradient(rx-1*D,ry-1*D,0.5*D,rx,ry,3*D); rg.addColorStop(0,"#79808f"); rg.addColorStop(1,"#3a3f4a"); ctx.fillStyle=rg; ctx.beginPath(); ctx.arc(rx,ry,2.4*D,0,6.283); ctx.fill();
    ctx.fillStyle="rgba(255,255,255,.5)"; ctx.beginPath(); ctx.arc(rx-0.8*D,ry-0.8*D,0.7*D,0,6.283); ctx.fill(); };
  ctx.strokeStyle="#171a20"; ctx.lineWidth=3*D; ctx.beginPath(); ctx.moveTo(0,midY); ctx.lineTo(W,midY); ctx.stroke();
  ctx.strokeStyle="rgba(255,255,255,.06)"; ctx.lineWidth=1*D; ctx.beginPath(); ctx.moveTo(0,midY+2*D); ctx.lineTo(W,midY+2*D); ctx.stroke();
  for(let x=-ox; x<W+PW; x+=PW){ const wc=Math.round((x+cam)/PW);
    ctx.strokeStyle="#171a20"; ctx.lineWidth=3*D; ctx.beginPath(); ctx.moveTo(x,g0); ctx.lineTo(x,H); ctx.stroke();
    ctx.strokeStyle="rgba(255,255,255,.07)"; ctx.lineWidth=1*D; ctx.beginPath(); ctx.moveTo(x+2*D,g0); ctx.lineTo(x+2*D,H); ctx.stroke();
    rivet(x,g0+7*D); rivet(x,midY); rivet(x,H-7*D); rivet(x+PW*0.5,midY);
    if((wc&1)===0){ const lx=x+PW*0.5, ly=g0+bh*0.27, pulse=0.4+0.6*(0.5+0.5*Math.sin(t*1.6+wc));
      ctx.fillStyle="#15181e"; roundRect(lx-5*D,ly-4*D,10*D,8*D,2*D); ctx.fill();
      const gl=ctx.createRadialGradient(lx,ly,0,lx,ly,6*D); gl.addColorStop(0,getAccent(0.45*pulse)); gl.addColorStop(1,getAccent(0)); ctx.fillStyle=gl; ctx.beginPath(); ctx.arc(lx,ly,6*D,0,6.283); ctx.fill();
      ctx.fillStyle=getAccent(0.4+0.5*pulse); ctx.beginPath(); ctx.arc(lx,ly,1.8*D,0,6.283); ctx.fill(); } }
  ctx.restore();
}
function drawFloorChecker(biome,t){
  ctx.save();
  const D=DPR, g0=GROUND(), bh=H-g0;
  const c1=silhouetteColor(biome.sky[1],0.92,1.18), c2=silhouetteColor(biome.sky[2],0.96,0.62);
  ctx.fillStyle=c2; ctx.fillRect(0,g0,W,bh);
  const SP=0.02, STEP=Math.max(2*D, bh/44);
  for(let y=g0; y<H; y+=STEP){ const y1=Math.min(H, y+STEP), yc=(y+y1)/2; const dd=Math.max(0.5, yc-g0), depth=bh/dd; const row=Math.floor(depth + cam*SP); const colW=dd*0.42;
    if(colW<4*D){ ctx.globalAlpha=.5; ctx.fillStyle=(row&1)?c1:c2; ctx.fillRect(0,y,W,y1-y); ctx.globalAlpha=1; continue; }
    let n=Math.floor((-W/2)/colW)-1, sx=W/2+n*colW;
    for(; sx<W; sx+=colW, n++){ if(sx+colW<0) continue; ctx.fillStyle=((row+n)&1)?c1:c2; ctx.fillRect(sx, y, colW+1, y1-y); } }
  ctx.fillStyle=getAccent(.5); ctx.fillRect(0,g0,W,2*D);
  ctx.restore();
}
// ===== themed per-biome FLOOR renderers (natural / elemental) =====
function drawFloorGrass(biome,t){
  const D=DPR, gy=GROUND(), gh=H-gy;
  ctx.fillStyle="#6b4a2b"; ctx.fillRect(0,gy,W,gh);
  const tile=34*D, off=((cam)%tile+tile)%tile, capH=13*D, rowH=(gh-capH)/2;
  for(let x=-off,i=0; x<W+tile; x+=tile,i++){ const col=Math.round((cam-off)/tile)+i;
    for(let row=0; row<2; row++){ const by=gy+capH+row*rowH;
      ctx.fillStyle=silhouetteColor("#7a5530",1,0.82+0.24*hashS(col*2.3+row*7.7)); ctx.fillRect(x,by,tile,rowH);
      ctx.fillStyle="rgba(40,24,12,.55)"; ctx.fillRect(x,by,tile,1.5*D); ctx.fillRect(x,by,1.5*D,rowH);
      if(hashY(col*3.1+row*5.2)>0.55){ ctx.fillStyle="rgba(48,28,14,.6)"; ctx.fillRect(x+tile*(0.25+0.45*hashS(col+row)), by+rowH*(0.3+0.4*hashY(col*1.7+row)), 4*D,4*D); } } }
  ctx.fillStyle="#4ea63a"; ctx.fillRect(0,gy,W,capH);
  ctx.fillStyle="#6fce52"; ctx.fillRect(0,gy,W,4*D);
  for(let x=-off,i=0; x<W+tile; x+=tile,i++){ const col=Math.round((cam-off)/tile)+i;
    if(hashS(col*1.9)>0.4){ ctx.fillStyle="#3f8c30"; ctx.fillRect(x+tile*0.25, gy+capH, tile*0.5, 4*D); }
    ctx.fillStyle="#7fe060"; ctx.fillRect(x+tile*(0.2+0.6*hashY(col)), gy-3*D, 2*D, 3*D); }
}
function drawFloorCandy(biome,t){
  const D=DPR, gy=GROUND(), gh=H-gy;
  let g=ctx.createLinearGradient(0,gy,0,H); g.addColorStop(0,"#ffd1ec"); g.addColorStop(1,"#f7a8d8"); ctx.fillStyle=g; ctx.fillRect(0,gy,W,gh);
  ctx.save(); ctx.beginPath(); ctx.rect(0,gy,W,gh); ctx.clip();
  const sw=26*D, off=((cam)%(sw*2)+sw*2)%(sw*2); ctx.fillStyle="rgba(255,255,255,.5)";
  for(let x=-off-gh; x<W+gh; x+=sw*2){ ctx.beginPath(); ctx.moveTo(x,H); ctx.lineTo(x+sw,H); ctx.lineTo(x+sw+gh,gy); ctx.lineTo(x+gh,gy); ctx.closePath(); ctx.fill(); }
  ctx.restore();
  const tile=46*D, o2=((cam)%tile+tile)%tile, hues=["#ff8fc6","#a8e6ff","#fff39a","#c5a3ff"];
  for(let x=-o2,i=0; x<W+tile; x+=tile,i++){ const col=((Math.round((cam-o2)/tile)+i)%4+4)%4; const r=12*D+6*D*hashS(col*1.3+i*0.7), cx=x+tile*0.5, cy=gy+r*0.6, hue=hues[col];
    let rg=ctx.createRadialGradient(cx-r*0.35,cy-r*0.4,r*0.2,cx,cy,r); rg.addColorStop(0,"#ffffff"); rg.addColorStop(0.35,hue); rg.addColorStop(1,silhouetteColor(hue,1,0.7)); ctx.fillStyle=rg; ctx.beginPath(); ctx.arc(cx,cy,r,0,7); ctx.fill();
    ctx.fillStyle="rgba(255,255,255,.85)"; ctx.beginPath(); ctx.arc(cx-r*0.32,cy-r*0.36,r*0.22,0,7); ctx.fill(); }
}
function drawFloorLava(biome,t){
  const D=DPR, gy=GROUND(), gh=H-gy;
  let g=ctx.createLinearGradient(0,gy,0,H); g.addColorStop(0,"#2a2026"); g.addColorStop(1,"#141015"); ctx.fillStyle=g; ctx.fillRect(0,gy,W,gh);
  const tile=58*D, off=((cam)%tile+tile)%tile, flick=0.6+0.4*Math.sin(t*5.0);
  ctx.save(); ctx.beginPath(); ctx.rect(0,gy,W,gh); ctx.clip();
  for(let x=-off,i=0; x<W+tile; x+=tile,i++){ const col=Math.round((cam-off)/tile)+i; const jx=x+(hashY(col)-0.5)*6*D;
    ctx.fillStyle=silhouetteColor("#3a2c30",1,0.8+0.3*hashS(col*2.1)); ctx.fillRect(jx+2*D, gy+2*D, tile-4*D, gh-4*D);
    const cf=flick*(0.5+0.5*hashS(col*3.7+Math.floor(t*2)));
    ctx.save(); ctx.globalCompositeOperation="lighter"; // NO per-tile shadowBlur (was 10*D x ~25 tiles x2 strokes = the Sunspire/lava lag); the 'lighter' composite + bright strokes keep the glow
    ctx.strokeStyle="#ff7a1e"; ctx.lineWidth=3*D; ctx.globalAlpha=0.5+0.5*cf;
    ctx.beginPath(); ctx.moveTo(x,gy); for(let s=1;s<=4;s++){ ctx.lineTo(x+(hashY(col*1.7+s)-0.5)*14*D, gy+gh*s/4); } ctx.stroke();
    ctx.strokeStyle="#ffd27a"; ctx.lineWidth=1.2*D; ctx.stroke(); ctx.restore(); }
  ctx.save(); ctx.globalCompositeOperation="lighter"; ctx.globalAlpha=0.4+0.3*flick;
  ctx.strokeStyle="#ff8a2a"; ctx.lineWidth=2*D; ctx.shadowColor="#ffae3d"; ctx.shadowBlur=8*D; ctx.beginPath();
  for(let x=0;x<=W;x+=8*D){ const yy=gy+4*D+Math.sin((x+cam)*0.02)*2*D; x?ctx.lineTo(x,yy):ctx.moveTo(x,yy); } ctx.stroke(); ctx.restore();
  ctx.restore();
}
function drawFloorIce(biome,t){
  const D=DPR, gy=GROUND(), gh=H-gy;
  let g=ctx.createLinearGradient(0,gy,0,H); g.addColorStop(0,"#cfeeff"); g.addColorStop(1,"#8fc4e8"); ctx.fillStyle=g; ctx.fillRect(0,gy,W,gh);
  ctx.save(); ctx.beginPath(); ctx.rect(0,gy,W,gh); ctx.clip();
  const bandX=((-cam*0.6)%(W*1.5)+W*1.5)%(W*1.5)-W*0.25;
  let sg=ctx.createLinearGradient(bandX-60*D,0,bandX+60*D,0); sg.addColorStop(0,"rgba(255,255,255,0)"); sg.addColorStop(0.5,"rgba(255,255,255,.4)"); sg.addColorStop(1,"rgba(255,255,255,0)"); ctx.fillStyle=sg; ctx.fillRect(0,gy,W,gh*0.6);
  const tile=70*D, off=((cam)%tile+tile)%tile; ctx.strokeStyle="rgba(120,170,210,.6)"; ctx.lineWidth=1.4*D;
  for(let x=-off,i=0; x<W+tile; x+=tile,i++){ const col=Math.round((cam-off)/tile)+i, sx=x+tile*hashS(col), sy=gy+gh*0.2;
    ctx.beginPath(); ctx.moveTo(sx,sy); let xx=sx,yy=sy; for(let s=0;s<3;s++){ xx+=(hashY(col*2+s)-0.5)*40*D; yy+=gh*0.28; ctx.lineTo(xx,yy); } ctx.stroke();
    ctx.beginPath(); ctx.moveTo(sx,sy+gh*0.28); ctx.lineTo(sx+(hashS(col+9)-0.5)*36*D, sy+gh*0.55); ctx.stroke(); }
  ctx.fillStyle="rgba(255,255,255,.7)"; ctx.fillRect(0,gy,W,3*D);
  for(let i=0;i<10;i++){ const px=(((i*131*D - cam*0.9)%W)+W)%W, py=gy+gh*(0.2+0.6*hashY(i)), tw=0.5+0.5*Math.sin(t*3+i*1.7); ctx.fillStyle="rgba(255,255,255,"+(0.3+0.5*tw)+")"; ctx.fillRect(px,py,2.5*D,2.5*D); }
  ctx.restore();
}
function drawFloorSand(biome,t){
  const D=DPR, gy=GROUND(), gh=H-gy;
  let g=ctx.createLinearGradient(0,gy,0,H); g.addColorStop(0,"#f0d49a"); g.addColorStop(0.5,"#e0b46e"); g.addColorStop(1,"#bd8a4c"); ctx.fillStyle=g; ctx.fillRect(0,gy,W,gh);
  ctx.save(); ctx.beginPath(); ctx.rect(0,gy,W,gh); ctx.clip();
  // soft back dune band (parallax)
  ctx.fillStyle="#f4dca8"; ctx.beginPath(); ctx.moveTo(0,H); for(let x=0;x<=W;x+=12*D){ ctx.lineTo(x, gy+15*D+Math.sin((x+cam*0.5)*0.006)*11*D); } ctx.lineTo(W,H); ctx.closePath(); ctx.fill();
  // front dune crest
  const crest=x=>gy+8*D+Math.sin((x+cam*0.8)*0.012)*6*D+Math.sin((x+cam*0.8)*0.045)*2*D;
  ctx.fillStyle="#f0d6a0"; ctx.beginPath(); ctx.moveTo(0,H); for(let x=0;x<=W;x+=10*D){ ctx.lineTo(x,crest(x)); } ctx.lineTo(W,H); ctx.closePath(); ctx.fill();
  ctx.strokeStyle="rgba(150,110,60,.4)"; ctx.lineWidth=2*D; ctx.beginPath(); for(let x=0;x<=W;x+=10*D){ x?ctx.lineTo(x,crest(x)):ctx.moveTo(0,crest(0)); } ctx.stroke();
  // buried sandstone block course w/ carved glyph ticks (ancient ruins underfoot)
  const tile=120*D, off=((cam*0.9)%tile+tile)%tile, by=gy+gh*0.60, bh=gh*0.17;
  for(let x=-off; x<W+tile; x+=tile){
    ctx.fillStyle="rgba(120,86,44,.26)"; ctx.fillRect(x+3*D, by, tile-6*D, bh);
    ctx.strokeStyle="rgba(90,62,30,.34)"; ctx.lineWidth=1.4*D; ctx.strokeRect(x+3*D, by, tile-6*D, bh);
    ctx.fillStyle="rgba(90,62,30,.40)"; const gx=x+tile*0.5;
    ctx.fillRect(gx-11*D, by+bh*0.28, 4*D, 7*D); ctx.fillRect(gx+3*D, by+bh*0.28, 4*D, 7*D); ctx.fillRect(gx-5*D, by+bh*0.58, 9*D, 3*D);
  }
  // wind ripples over the sand
  ctx.strokeStyle="rgba(190,150,95,.45)"; ctx.lineWidth=1.2*D;
  for(let r=1;r<=4;r++){ const ry=gy+gh*(0.32+r*0.13); ctx.beginPath(); for(let x=0;x<=W;x+=8*D){ const yy=ry+Math.sin((x+cam*1.2+r*30)*0.06)*2.2*D; x?ctx.lineTo(x,yy):ctx.moveTo(0,yy); } ctx.stroke(); }
  // golden treasure glints twinkling in the sand (free-ambience dopamine; below the play line = zero readability cost)
  ctx.save(); ctx.globalCompositeOperation="lighter";
  for(let i=0;i<18;i++){ const span=W+80*D; const px=(((i*113*D - cam*0.8)%span)+span)%span-40*D, py=gy+gh*(0.22+0.66*hashY(i*3)); const tw=0.5+0.5*Math.sin(t*3+i*1.7), a=0.08+0.30*tw*tw;
    let cg=ctx.createRadialGradient(px,py,0.5*D,px,py,5*D); cg.addColorStop(0,"rgba(255,236,150,"+a+")"); cg.addColorStop(1,"rgba(255,236,150,0)"); ctx.fillStyle=cg; ctx.fillRect(px-5*D,py-5*D,10*D,10*D); }
  ctx.restore();
  ctx.restore();
}
function drawFloorReef(biome,t){
  const D=DPR, gy=GROUND(), gh=H-gy;
  let g=ctx.createLinearGradient(0,gy,0,H); g.addColorStop(0,"#cdb98a"); g.addColorStop(1,"#a99368"); ctx.fillStyle=g; ctx.fillRect(0,gy,W,gh);
  ctx.save(); ctx.beginPath(); ctx.rect(0,gy,W,gh); ctx.clip();
  ctx.save(); ctx.globalCompositeOperation="lighter";
  for(let i=0;i<14;i++){ const span=W+120*D; const px=(((i*97*D - cam*0.7)%span)+span)%span-60*D+Math.sin(t*0.8+i)*16*D; const py=gy+gh*(0.15+0.7*hashY(i)), a=0.06+0.05*(0.5+0.5*Math.sin(t*1.6+i*1.3));
    let cg=ctx.createRadialGradient(px,py,1*D,px,py,30*D); cg.addColorStop(0,"rgba(180,240,255,"+a+")"); cg.addColorStop(1,"rgba(180,240,255,0)"); ctx.fillStyle=cg; ctx.fillRect(px-30*D,py-30*D,60*D,60*D); }
  ctx.restore();
  ctx.strokeStyle="rgba(140,120,80,.35)"; ctx.lineWidth=1.2*D;
  for(let r=1;r<=3;r++){ const ry=gy+gh*(0.5+r*0.15); ctx.beginPath(); for(let x=0;x<=W;x+=8*D){ const yy=ry+Math.sin((x+cam*1.1+r*40)*0.05)*2*D; x?ctx.lineTo(x,yy):ctx.moveTo(0,yy); } ctx.stroke(); }
  const tile=52*D, off=((cam)%tile+tile)%tile, cols=["#ff7fae","#ff9d5c","#b98cff","#5cd0c0"];
  for(let x=-off,i=0; x<W+tile; x+=tile,i++){ const col=Math.round((cam-off)/tile)+i, cx=x+tile*(0.3+0.4*hashS(col)); const baseY=gy+10*D+gh*0.1*hashY(col), cc=cols[((col%4)+4)%4], hgt=14*D+14*D*hashS(col*1.7);
    ctx.strokeStyle=cc; ctx.lineWidth=4*D; ctx.lineCap="round"; ctx.beginPath(); ctx.moveTo(cx,baseY+hgt); ctx.lineTo(cx,baseY); ctx.moveTo(cx,baseY+hgt*0.5); ctx.lineTo(cx-7*D,baseY+hgt*0.1); ctx.moveTo(cx,baseY+hgt*0.6); ctx.lineTo(cx+8*D,baseY+hgt*0.15); ctx.stroke();
    ctx.fillStyle=silhouetteColor(cc,1,1.2); ctx.beginPath(); ctx.arc(cx,baseY,3*D,0,7); ctx.fill(); ctx.beginPath(); ctx.arc(cx-7*D,baseY+hgt*0.1,2.5*D,0,7); ctx.fill(); ctx.beginPath(); ctx.arc(cx+8*D,baseY+hgt*0.15,2.5*D,0,7); ctx.fill(); }
  ctx.restore();
}
// biome -> floor map
THEMED_FLOOR["Neon Cyber-Kitty"]=drawFloorCircuit; THEMED_FLOOR["Voltcrest"]=drawFloorCircuit;
THEMED_FLOOR["Clockwork Kitty"]=drawFloorMetal;
THEMED_FLOOR["Sky Kingdom"]=drawFloorChess;
THEMED_FLOOR["Mango Meowrage"]=drawFloorRoad;
THEMED_FLOOR["Starwhisker"]=drawFloorChecker;
THEMED_FLOOR["Dawnpurr"]=drawFloorGrass;
THEMED_FLOOR["Candyland"]=drawFloorCandy;
THEMED_FLOOR["Magmacore"]=drawFloorLava; THEMED_FLOOR["Sunspurr"]=drawFloorLava; THEMED_FLOOR["Emberfall Catyon"]=drawFloorLava;
THEMED_FLOOR["Frozen Purrora"]=drawFloorIce; THEMED_FLOOR["Glacier Meowl"]=drawFloorIce;
THEMED_FLOOR["Desert Purramids"]=drawFloorSand;
THEMED_FLOOR["Hyperpurr"]=drawFloorSynthwave; // neon dancefloor grid
THEMED_FLOOR["Gotham"]=drawFloorStreet;       // wet neon-reflected asphalt
THEMED_FLOOR["Aurora"]=drawFloorAurora; THEMED_FLOOR["Moonbase"]=drawFloorMoonbase; THEMED_FLOOR["Kino"]=drawFloorKino; THEMED_FLOOR["Facility 115"]=drawFloorFacility115;
THEMED_FLOOR["Ruby Rabbit"]=drawFloorRubyRabbit; THEMED_FLOOR["Shadowman"]=drawFloorShadowman;
THEMED_FLOOR["Ghetto"]=drawFloorGhetto; THEMED_FLOOR["Humble"]=drawFloorHumble; THEMED_FLOOR["Noname"]=drawFloorNoname; THEMED_FLOOR["Plu"]=drawFloorPlu; THEMED_FLOOR["Blurred"]=drawFloorBlurred; THEMED_FLOOR["Private"]=drawFloorPrivate; THEMED_FLOOR["Rad"]=drawFloorRad;
THEMED_FLOOR["Hollywood"]=drawFloorWalkOfFame; THEMED_FLOOR["Neon Tokyo"]=drawFloorStreet; THEMED_FLOOR["Senpai Skies"]=drawFloorGrass;
// remaining worlds: give each a themed floor (no more bare default) -- matched to theme
THEMED_FLOOR["Emberglide"]=drawFloorLava; THEMED_FLOOR["Voltloom"]=drawFloorCircuit; THEMED_FLOOR["Stormclaw"]=drawFloorCircuit; THEMED_FLOOR["Storm Spurr"]=drawFloorMetal;
THEMED_FLOOR["Sporebloom"]=drawFloorGrass; THEMED_FLOOR["Meowshroom Hollow"]=drawFloorGrass; THEMED_FLOOR["Jungle Ruins"]=drawFloorGrass; THEMED_FLOOR["Sakura Grove"]=drawFloorCandy;
THEMED_FLOOR["Abyssline"]=drawFloorReef; THEMED_FLOOR["Downpurr"]=drawFloorReef; THEMED_FLOOR["Aurora Purrge"]=drawFloorIce;
THEMED_FLOOR["Purrgatory"]=drawFloorAbyss; THEMED_FLOOR["Cosmic Mewbula"]=drawFloorAbyss; THEMED_FLOOR["The Hush Below"]=drawFloorAbyss; THEMED_FLOOR["Singularitty"]=drawFloorAbyss; THEMED_FLOOR["Geometric Void"]=drawFloorSynthwave;
THEMED_FLOOR["Underwater Reef"]=drawFloorReef;
THEMED_FLOOR["Funland"]=drawFloorPark;

// --- arcane-energy helpers (used by drawCannon) ---
function hash11(n){ const s=Math.sin(n*12.9898)*43758.5453; return s-Math.floor(s); }
function energyBolt(g,x1,y1,x2,y2,segs,jit,seed,w,col){
  g.lineWidth=w; g.strokeStyle=col; g.lineJoin="round"; g.lineCap="round";
  const dx=x2-x1, dy=y2-y1, len=Math.hypot(dx,dy)||1, nx=-dy/len, ny=dx/len;
  g.beginPath(); g.moveTo(x1,y1);
  for(let i=1;i<segs;i++){ const t=i/segs, off=(hash11(seed+i*7.3)-0.5)*jit;
    g.lineTo(x1+dx*t+nx*off, y1+dy*t+ny*off); }
  g.lineTo(x2,y2); g.stroke();
}
function drawCannon(){
  const D=DPR, bx=90*D-cam, by=GROUND(), now=performance.now()*0.001;
  const ang=aimAngle*Math.PI/180;
  const kick=Math.min(shake,10*D);                 // recoil/impact signal
  const recoil=-(kick/(10*D))*9*D;                 // push barrel back along its axis
  const breathe=0.5+0.5*Math.sin(now*1.6);         // slow idle "breathing" 0..1
  const energy=charging?(0.45+0.55*(power/100)):0.12; // arcane intensity: calm at rest, fierce at charge

  // ---------- ground shadow ----------
  ctx.save();
  ctx.fillStyle="rgba(0,0,0,.32)";
  ctx.beginPath(); ctx.ellipse(bx+6*D,by+6*D,46*D,11*D,0,0,Math.PI*2); ctx.fill();
  ctx.restore();

  // ---------- CARRIAGE / MOUNT (behind barrel) ----------
  ctx.save(); ctx.translate(bx,by);
  const wr=24*D;
  let wg=ctx.createRadialGradient(-6*D,-6*D,4*D,0,0,wr);
  wg.addColorStop(0,"#5a6378"); wg.addColorStop(.6,"#363c4c"); wg.addColorStop(1,"#191d27");
  ctx.fillStyle=wg; ctx.beginPath(); ctx.arc(0,0,wr,0,Math.PI*2); ctx.fill();
  ctx.lineWidth=3*D; ctx.strokeStyle="#10131b"; ctx.beginPath(); ctx.arc(0,0,wr-2*D,0,Math.PI*2); ctx.stroke();
  ctx.strokeStyle="#454c5e"; ctx.lineWidth=4*D;
  for(let i=0;i<6;i++){const a=now*0.3+i*Math.PI/3; ctx.beginPath(); ctx.moveTo(0,0); ctx.lineTo(Math.cos(a)*(wr-4*D),Math.sin(a)*(wr-4*D)); ctx.stroke();}
  let hg=ctx.createRadialGradient(-2*D,-2*D,1*D,0,0,8*D);
  hg.addColorStop(0,"#8b94a8"); hg.addColorStop(1,"#3a4154");
  ctx.fillStyle=hg; ctx.beginPath(); ctx.arc(0,0,8*D,0,Math.PI*2); ctx.fill();
  ctx.fillStyle=getAccent(.9); ctx.beginPath(); ctx.arc(0,0,2.6*D,0,Math.PI*2); ctx.fill();
  let bg=ctx.createLinearGradient(0,-8*D,0,2*D);
  bg.addColorStop(0,"#444b5e"); bg.addColorStop(1,"#23283a");
  ctx.fillStyle=bg;
  ctx.beginPath(); ctx.moveTo(-4*D,-6*D); ctx.lineTo(-40*D,4*D); ctx.lineTo(-34*D,9*D); ctx.lineTo(-2*D,2*D); ctx.closePath(); ctx.fill();
  ctx.restore();

  // ---------- BARREL (pivoting) ----------
  ctx.save(); ctx.translate(bx,by); ctx.rotate(-ang); ctx.translate(recoil,0);

  const len=64*D, h=15*D, x0=2*D;

  // ---------- ENERGY AURA (behind the metal) ----------
  {
    const aura=energy*(0.7+0.3*breathe);
    ctx.save(); ctx.globalCompositeOperation="lighter";
    let ag=ctx.createRadialGradient(len*0.5,0,2*D,len*0.5,0,len*0.95);
    ag.addColorStop(0,getAccent(0.20*aura));
    ag.addColorStop(0.5,`rgba(150,90,255,${0.14*aura})`);
    ag.addColorStop(1,getAccent(0));
    ctx.fillStyle=ag;
    ctx.beginPath(); ctx.ellipse(len*0.5,0,len*0.95,h*2.4,0,0,Math.PI*2); ctx.fill();
    ctx.restore();
  }

  ctx.fillStyle="rgba(0,0,0,.25)";
  roundRect(x0+3*D,-h+5*D,len+12*D,h*2,h); ctx.fill();

  // cylindrical body gradient (across thickness)
  let cg=ctx.createLinearGradient(0,-h,0,h);
  cg.addColorStop(0,"#20242f");
  cg.addColorStop(.30,"#5b6478");
  cg.addColorStop(.46,"#aeb7c9");
  cg.addColorStop(.60,"#6a7286");
  cg.addColorStop(1,"#171a23");
  ctx.fillStyle=cg;
  roundRect(x0,-h,len,h*2,h*0.7); ctx.fill();

  ctx.strokeStyle="rgba(255,255,255,.22)"; ctx.lineWidth=2*D;
  ctx.beginPath(); ctx.moveTo(x0+6*D,-h*0.55); ctx.lineTo(x0+len-6*D,-h*0.55); ctx.stroke();

  // reinforcing bands + rivets (middle band = ornate glowing brass)
  const bands=[16*D,34*D,52*D];
  const brassGlow=(charging?0.4:0.0)+(0.25+0.25*Math.sin(now*2));
  for(let bi=0;bi<bands.length;bi++){
    const bxp=bands[bi];
    if(bi===1){
      let bgr=ctx.createLinearGradient(0,-h,0,h);
      bgr.addColorStop(0,"#3a2a10"); bgr.addColorStop(.40,"#f4d089"); bgr.addColorStop(.52,"#d9a85c"); bgr.addColorStop(.62,"#9c6f30"); bgr.addColorStop(1,"#241608");
      ctx.fillStyle=bgr; roundRect(bxp-0.5*D,-h-2*D,8*D,h*2+4*D,3*D); ctx.fill();
      ctx.strokeStyle="rgba(255,236,190,.55)"; ctx.lineWidth=1*D;
      ctx.beginPath(); ctx.moveTo(bxp+1*D,-h*0.5); ctx.lineTo(bxp+1*D,h*0.5); ctx.stroke();
      ctx.save(); ctx.globalCompositeOperation="lighter";
      ctx.strokeStyle=`rgba(217,168,92,${0.35+0.5*Math.min(1,brassGlow)})`; ctx.lineWidth=1.6*D;
      roundRect(bxp-0.5*D,-h-2*D,8*D,h*2+4*D,3*D); ctx.stroke();
      ctx.restore();
      ctx.fillStyle="#ffe6b0"; ctx.beginPath(); ctx.arc(bxp+3.5*D,-h*0.55,1.7*D,0,Math.PI*2); ctx.fill();
      ctx.fillStyle="#6b4a1c"; ctx.beginPath(); ctx.arc(bxp+3.5*D,h*0.5,1.7*D,0,Math.PI*2); ctx.fill();
    } else {
      let bgr=ctx.createLinearGradient(0,-h,0,h);
      bgr.addColorStop(0,"#3a4053"); bgr.addColorStop(.45,"#cfd6e6"); bgr.addColorStop(.55,"#7b8398"); bgr.addColorStop(1,"#13161f");
      ctx.fillStyle=bgr; roundRect(bxp,-h-1.5*D,7*D,h*2+3*D,3*D); ctx.fill();
      ctx.fillStyle="#cdd4e4"; ctx.beginPath(); ctx.arc(bxp+3.5*D,-h*0.55,1.6*D,0,Math.PI*2); ctx.fill();
      ctx.fillStyle="#2a3040"; ctx.beginPath(); ctx.arc(bxp+3.5*D,h*0.5,1.6*D,0,Math.PI*2); ctx.fill();
    }
  }

  // ---------- GLOWING RUNES (pulse on offset phases) ----------
  {
    const runes=[{x:25*D,y:-h*0.12,t:0,p:0},{x:44*D,y:-h*0.05,t:1,p:2.1},{x:13*D,y:-h*0.18,t:2,p:4.2}];
    ctx.save(); ctx.globalCompositeOperation="lighter"; ctx.lineCap="round"; ctx.lineJoin="round";
    for(const ru of runes){
      const a=(charging?0.32:0.15)+(charging?0.55:0.12)*(0.5+0.5*Math.sin(now*3+ru.p));
      const col=(ru.t===1)?`rgba(150,90,255,${a})`:getAccent(a);
      ctx.strokeStyle=col; ctx.lineWidth=1.4*D; const s=3.4*D;
      ctx.beginPath();
      if(ru.t===0){ ctx.arc(ru.x,ru.y,s,0,Math.PI*2); ctx.moveTo(ru.x+s*0.45,ru.y); ctx.arc(ru.x,ru.y,s*0.45,0,Math.PI*2); }
      else if(ru.t===1){ ctx.moveTo(ru.x,ru.y-s); ctx.lineTo(ru.x+s*0.9,ru.y+s*0.7); ctx.lineTo(ru.x-s*0.9,ru.y+s*0.7); ctx.closePath(); }
      else { ctx.moveTo(ru.x-s,ru.y+s); ctx.lineTo(ru.x+s,ru.y-s); ctx.moveTo(ru.x-s*0.4,ru.y-s); ctx.lineTo(ru.x+s,ru.y-s*0.2); }
      ctx.stroke();
    }
    ctx.restore();
  }

  // ---------- FLARED MUZZLE ----------
  const mx=len+x0, mh=h*1.42;
  let mg=ctx.createLinearGradient(0,-mh,0,mh);
  mg.addColorStop(0,"#262b38"); mg.addColorStop(.42,"#9aa3b6"); mg.addColorStop(.55,"#5e6678"); mg.addColorStop(1,"#12151d");
  ctx.fillStyle=mg;
  ctx.beginPath();
  ctx.moveTo(mx-2*D,-h);
  ctx.lineTo(mx+10*D,-mh);
  ctx.lineTo(mx+10*D,mh);
  ctx.lineTo(mx-2*D,h);
  ctx.closePath(); ctx.fill();
  ctx.strokeStyle="#cdd4e4"; ctx.lineWidth=2.2*D;
  ctx.beginPath(); ctx.moveTo(mx+10*D,-mh); ctx.lineTo(mx+10*D,mh); ctx.stroke();
  // brass muzzle lip accent (ornate)
  ctx.save(); ctx.globalCompositeOperation="lighter";
  ctx.strokeStyle=`rgba(217,168,92,${0.30+0.4*energy})`; ctx.lineWidth=1.4*D;
  ctx.beginPath(); ctx.moveTo(mx+10*D,-mh); ctx.lineTo(mx+10*D,mh); ctx.stroke();
  ctx.restore();

  // ---------- ELECTRIC ARCS (crackle along barrel, flicker per-frame) ----------
  {
    const ft=Math.floor(now*22);                      // flicker clock
    let nArcs = charging ? Math.round(1+power/28) : 0;
    if(!charging && hash11(ft*1.7)<0.05) nArcs=1;      // rare faint idle spark
    if(nArcs>0){
      const nodes=[10*D,19.5*D,37.5*D,55.5*D,mx+6*D];  // rivet/band x anchors -> muzzle
      const bri=charging?(0.45+0.55*power/100):0.32;
      ctx.save(); ctx.globalCompositeOperation="lighter";
      for(let i=0;i<nArcs;i++){
        const seed=ft*3.1+i*9.7;
        const j=Math.floor(hash11(seed)*(nodes.length-1));
        const x1=nodes[j], x2=nodes[j+1];
        const y1=-h*(0.5+0.3*hash11(seed+1)), y2=-h*(0.5+0.3*hash11(seed+2));
        const cols=[getAccent(0.9*bri),`rgba(150,90,255,${0.8*bri})`,`rgba(190,245,255,${0.85*bri})`];
        energyBolt(ctx,x1,y1,x2,y2,6,h*1.1,seed,3.5*D,(i%2)?`rgba(150,90,255,${0.16*bri})`:getAccent(0.18*bri));
        energyBolt(ctx,x1,y1,x2,y2,6,h*1.1,seed,1.5*D,cols[i%3]);
      }
      ctx.restore();
    }
  }

  // ---------- GLOWING BORE -> SWIRLING VORTEX CORE (power-coupled) ----------
  const boreX=mx+9*D, glow=charging?(0.35+0.65*(power/100)):0.22;
  const pulse=charging?(0.85+0.15*Math.sin(now*9)):1;
  ctx.save();
  ctx.globalCompositeOperation="lighter";
  let bgrad=ctx.createRadialGradient(boreX,0,1*D,boreX,0,mh*1.15);
  bgrad.addColorStop(0,getAccent(0.95*glow*pulse));
  bgrad.addColorStop(0.4,`rgba(150,90,255,${0.45*glow*pulse})`);
  bgrad.addColorStop(0.7,getAccent(0.35*glow*pulse));
  bgrad.addColorStop(1,getAccent(0));
  ctx.fillStyle=bgrad;
  ctx.beginPath(); ctx.ellipse(boreX,0,mh*1.2,mh*1.05,0,0,Math.PI*2); ctx.fill();
  ctx.restore();
  let og=ctx.createRadialGradient(boreX,0,0.5*D,boreX,0,mh*0.78);
  og.addColorStop(0,getAccent(Math.min(1,glow*0.9*pulse)));
  og.addColorStop(0.55,"#0a0d14");
  og.addColorStop(1,"#05070b");
  ctx.fillStyle=og;
  ctx.beginPath(); ctx.ellipse(boreX,0,mh*0.62,mh*0.82,0,0,Math.PI*2); ctx.fill();
  {
    const vr=mh*0.7, spin=now*2.2, vi=glow*pulse;
    ctx.save(); ctx.globalCompositeOperation="lighter";
    for(let r=0;r<4;r++){
      const rr=vr*(0.28+r*0.22), a0=spin*(r%2?-1:1)+r*1.7;
      ctx.lineWidth=(2.2-r*0.25)*D;
      ctx.strokeStyle=(r%2)?`rgba(150,90,255,${0.5*vi})`:getAccent(0.62*vi);
      ctx.beginPath(); ctx.arc(boreX,0,rr,a0,a0+Math.PI*1.15); ctx.stroke();
    }
    ctx.lineWidth=1.6*D; ctx.strokeStyle=getAccent(0.7*vi);
    ctx.beginPath();
    for(let s=0;s<=40;s++){ const t=s/40, ar=spin*1.4+t*Math.PI*3.2, rr=vr*t;
      const sx=boreX+Math.cos(ar)*rr*0.95, sy=Math.sin(ar)*rr; s?ctx.lineTo(sx,sy):ctx.moveTo(sx,sy); }
    ctx.stroke();
    let hc=ctx.createRadialGradient(boreX,0,0.5*D,boreX,0,vr*0.5);
    hc.addColorStop(0,`rgba(255,255,255,${Math.min(1,0.9*vi)})`);
    hc.addColorStop(0.5,getAccent(0.6*vi));
    hc.addColorStop(1,getAccent(0));
    ctx.fillStyle=hc; ctx.beginPath(); ctx.arc(boreX,0,vr*0.5,0,Math.PI*2); ctx.fill();
    ctx.restore();
  }

  // ---------- CHARGE PARTICLES / MUZZLE FLASH ----------
  if(charging){
    const np=Math.round(4+power/14);
    for(let i=0;i<np;i++){
      const a=now*2+i*2.399, r=(mh*1.2)*((i*0.13+now*0.6)%1);
      const px=boreX-r*0.6, py=Math.sin(a)*mh*0.7*(1-r/(mh*1.2));
      const s=(1-r/(mh*1.3))*2.4*D*(0.5+power/120);
      ctx.fillStyle=getAccent(0.7*(1-r/(mh*1.3))*pulse);
      ctx.beginPath(); ctx.arc(px,py,Math.max(0.4,s),0,Math.PI*2); ctx.fill();
    }
  }
  if(kick>2*D){
    const f=kick/(10*D);
    ctx.save(); ctx.globalCompositeOperation="lighter";
    let fg=ctx.createRadialGradient(boreX+6*D,0,1*D,boreX+6*D,0,mh*2.6*f);
    fg.addColorStop(0,getAccent(0.95*f)); fg.addColorStop(0.3,getAccent(0.55*f)); fg.addColorStop(1,getAccent(0));
    ctx.fillStyle=fg; ctx.beginPath(); ctx.arc(boreX+6*D,0,mh*2.6*f,0,Math.PI*2); ctx.fill();
    ctx.strokeStyle=getAccent(0.85*f); ctx.lineWidth=2.5*D;
    for(let k=0;k<6;k++){const aa=k*Math.PI/3+now; ctx.beginPath(); ctx.moveTo(boreX+4*D,0); ctx.lineTo(boreX+4*D+Math.cos(aa)*mh*2.2*f,Math.sin(aa)*mh*2.2*f); ctx.stroke();}
    ctx.restore();
  }

  ctx.restore(); // end barrel

  // ---------- POWER METER ----------
  if(state==="aim" && charging){
    const mw=180*D, mh2=16*D, mxp=bx-mw/2, my=by-150*D, pr=mh2/2;
    ctx.save(); ctx.shadowColor=getAccent(0.6); ctx.shadowBlur=14*D;
    ctx.fillStyle="rgba(8,12,20,.78)"; roundRect(mxp,my,mw,mh2,pr); ctx.fill();
    ctx.restore();
    ctx.fillStyle="rgba(255,255,255,.05)"; roundRect(mxp+2*D,my+2*D,mw-4*D,mh2-4*D,pr-1*D); ctx.fill();
    const fw=(mw-4*D)*(power/100);
    ctx.save(); roundRect(mxp+2*D,my+2*D,Math.max(pr*1.4,fw),mh2-4*D,pr-1*D); ctx.clip();
    let pg=ctx.createLinearGradient(mxp,0,mxp+mw,0);
    pg.addColorStop(0,getAccent(0.55)); pg.addColorStop(0.7,getAccent(1)); pg.addColorStop(1,"#ffffff");
    ctx.fillStyle=pg; ctx.fillRect(mxp,my,mw,mh2);
    ctx.fillStyle="rgba(8,12,20,.55)";
    for(let s=1;s<10;s++){ctx.fillRect(mxp+(mw-4*D)*s/10,my,2*D,mh2);}
    ctx.restore();
    ctx.save(); ctx.globalCompositeOperation="lighter";
    const lex=mxp+2*D+fw;
    let leg=ctx.createRadialGradient(lex,my+mh2/2,1*D,lex,my+mh2/2,12*D);
    leg.addColorStop(0,getAccent(0.9)); leg.addColorStop(1,getAccent(0));
    ctx.fillStyle=leg; ctx.beginPath(); ctx.arc(lex,my+mh2/2,12*D,0,Math.PI*2); ctx.fill();
    ctx.restore();
    ctx.strokeStyle=getAccent(0.85); ctx.lineWidth=1.6*D; roundRect(mxp,my,mw,mh2,pr); ctx.stroke();
    ctx.fillStyle="#fff"; ctx.font=`700 ${12*D}px 'Saira Semi Condensed'`; ctx.textAlign="center";
    ctx.shadowColor=getAccent(0.9); ctx.shadowBlur=8*D;
    ctx.fillText("POWER",bx,my-7*D); ctx.shadowBlur=0;
  }
}
function drawRival(t){ if(_rivalI>=_rivals.length || state!=="fly") return; const r=_rivals[_rivalI]; // ghost flag at the next real board score you're chasing
  const wx=r.dist*24*DPR+90*DPR, x=wx-cam; if(x<-40*DPR||x>W+120*DPR) return; const g=GROUND();
  ctx.save();
  ctx.strokeStyle="rgba(180,220,255,.5)"; ctx.lineWidth=3*DPR; ctx.beginPath(); ctx.moveTo(x,g); ctx.lineTo(x,g-72*DPR); ctx.stroke(); // pole
  ctx.fillStyle="rgba(127,231,255,.85)"; ctx.beginPath(); ctx.moveTo(x,g-72*DPR); ctx.lineTo(x+34*DPR,g-62*DPR); ctx.lineTo(x,g-52*DPR); ctx.closePath(); ctx.fill(); // flag
  ctx.textAlign="center"; ctx.font=`800 ${12*DPR}px Orbitron`; ctx.fillStyle="#bfe0ff"; ctx.shadowColor="rgba(0,0,0,.6)"; ctx.shadowBlur=4*DPR;
  ctx.fillText("\u{1F47B} "+r.name+" #"+r.rank, x, g-82*DPR); ctx.restore(); }
function drawObstacles(t){
  for(const o of obstacles){ const x=o.x-cam; if(x<-140*DPR||x>W+140*DPR) continue;
    if((o.type==="spike"||o.type==="tnt"||o.type==="doghouse") && o.hit) continue; // an ability CRUSHED this hazard -> it's destroyed, stop drawing it (the shatter debris carries the break)
    if(o.type==="token"){ if(o.got) continue; const pulse=1+0.1*Math.sin(t*3+o.x); // big MAGENTA heart = life (shape-coded vs blue balloon)
      ctx.save(); ctx.translate(x,o.y); ctx.scale(pulse,pulse);
      ctx.strokeStyle="rgba(255,61,166,.4)"; ctx.lineWidth=3*DPR; ctx.beginPath(); ctx.arc(0,0,o.r*1.3,0,7); ctx.stroke();
      ctx.shadowColor="rgba(255,61,166,.9)"; ctx.shadowBlur=16*DPR;
      ctx.fillStyle="#ff3da6"; drawHeart(ctx,0,-o.r*0.12,o.r*1.35);
      ctx.shadowBlur=0; ctx.fillStyle="rgba(255,255,255,.5)"; drawHeart(ctx,-o.r*0.22,-o.r*0.38,o.r*0.45); ctx.restore(); continue; }
    if(o.type==="bolt"){ if(o.got) continue; const pl=1+0.12*Math.sin(t*5+o.x); ctx.save(); ctx.translate(x,o.y); // ANGEL WINGS = refill flaps (mirrors the flap HUD icon so players recognize it)
      let ag=ctx.createRadialGradient(0,0,2*DPR,0,0,o.r*2); ag.addColorStop(0,"rgba(255,255,255,.30)"); ag.addColorStop(1,"rgba(255,255,255,0)");
      ctx.fillStyle=ag; ctx.beginPath(); ctx.arc(0,0,o.r*2,0,7); ctx.fill();
      ctx.scale(pl,pl); drawWing(0,-o.r*0.15,o.r*1.35,true); // angel wings (cyan, same as the flap counter)
      ctx.restore(); continue; }
    if(o.type==="comet"){ if(o.got) continue; const pl=1+0.10*Math.sin(t*5+o.x); ctx.save(); ctx.translate(x,o.y); ctx.scale(pl,pl); // spinning gold COIN = coin bonus (matches the shop coin)
      const cr=o.r*0.82, wob=Math.max(0.12,Math.abs(Math.sin(t*2.6+o.x))); // wob = coin spin (face squashes toward edge-on)
      ctx.shadowColor="rgba(255,210,80,.8)"; ctx.shadowBlur=12*DPR;
      const cg=ctx.createLinearGradient(-cr,0,cr,0); cg.addColorStop(0,"#cf8f20"); cg.addColorStop(0.5,"#ffe488"); cg.addColorStop(1,"#d99a25");
      ctx.fillStyle=cg; ctx.beginPath(); ctx.ellipse(0,0,cr*wob,cr,0,0,7); ctx.fill();
      ctx.shadowBlur=0; ctx.strokeStyle="rgba(120,75,10,.5)"; ctx.lineWidth=2*DPR; ctx.stroke();
      ctx.strokeStyle="rgba(255,248,210,.85)"; ctx.lineWidth=1.4*DPR; ctx.beginPath(); ctx.ellipse(0,0,cr*wob*0.64,cr*0.64,0,0,7); ctx.stroke();
      if(wob>0.4){ ctx.fillStyle="rgba(255,255,255,.7)"; ctx.beginPath(); ctx.ellipse(-cr*wob*0.34,-cr*0.36,cr*wob*0.2,cr*0.16,0,0,7); ctx.fill(); }
      ctx.restore(); continue; }
    if(o.type==="star"){ if(o.got) continue; const pl=1+0.14*Math.sin(t*6+o.x); ctx.save(); ctx.translate(x,o.y); ctx.rotate(t*1.6); ctx.scale(pl,pl); // RARE Super Star power-up: rainbow shimmer star
      const sr=o.r*0.82, hue=(t*120+o.x)%360; ctx.shadowColor="hsla("+hue+",100%,60%,.9)"; ctx.shadowBlur=16*DPR;
      const sg=ctx.createLinearGradient(-sr,-sr,sr,sr); sg.addColorStop(0,"hsl("+hue+",100%,65%)"); sg.addColorStop(0.5,"#fff6c0"); sg.addColorStop(1,"hsl("+((hue+90)%360)+",100%,62%)");
      ctx.fillStyle=sg; ctx.beginPath(); for(let i=0;i<10;i++){ const a=i/10*Math.PI*2-Math.PI/2, rr2=(i%2?0.44:1)*sr; const px=Math.cos(a)*rr2, py=Math.sin(a)*rr2; i?ctx.lineTo(px,py):ctx.moveTo(px,py); } ctx.closePath(); ctx.fill();
      ctx.shadowBlur=0; ctx.strokeStyle="rgba(255,255,255,.9)"; ctx.lineWidth=1.4*DPR; ctx.stroke();
      ctx.fillStyle="rgba(255,255,255,.9)"; ctx.beginPath(); ctx.arc(-sr*0.2,-sr*0.22,sr*0.18,0,7); ctx.fill(); ctx.restore(); continue; }
    if(o.type==="candy"){ if(o.got) continue; const pl=1+0.12*Math.sin(t*4+o.x); ctx.save(); ctx.translate(x,o.y+Math.sin(t*2+o.x)*3*DPR); ctx.rotate(Math.sin(t*1.5+o.x)*0.25); ctx.scale(pl,pl); // MYSTERY spooky candy -> eating it SPINS the Crazy Slots
      ctx.shadowColor="#b06bff"; ctx.shadowBlur=15*DPR; ctx.fillStyle="#7a2fd6";
      ctx.beginPath(); ctx.ellipse(0,0,o.r*0.9,o.r*0.66,0,0,7); ctx.fill();
      ctx.beginPath(); ctx.moveTo(o.r*0.7,0); ctx.lineTo(o.r*1.45,-o.r*0.5); ctx.lineTo(o.r*1.45,o.r*0.5); ctx.closePath(); ctx.fill();
      ctx.beginPath(); ctx.moveTo(-o.r*0.7,0); ctx.lineTo(-o.r*1.45,-o.r*0.5); ctx.lineTo(-o.r*1.45,o.r*0.5); ctx.closePath(); ctx.fill();
      ctx.shadowBlur=0; ctx.strokeStyle="rgba(255,255,255,.75)"; ctx.lineWidth=2*DPR; ctx.beginPath(); ctx.ellipse(0,0,o.r*0.9,o.r*0.66,0,0,7); ctx.stroke();
      ctx.fillStyle="#fff"; ctx.font=`900 ${Math.round(o.r*0.95)}px Orbitron`; ctx.textAlign="center"; ctx.textBaseline="middle"; ctx.fillText("?",0,0); ctx.textBaseline="alphabetic"; ctx.restore(); continue; }
    if(o.type==="portal"){ if(o.used) continue; ctx.save(); ctx.translate(x,o.y); const sp=t*3+o.x; // PORTAL = warp gate; fly in -> pop out high with a boost
      ctx.shadowColor="rgba(150,90,255,.9)"; ctx.shadowBlur=18*DPR;
      let pg=ctx.createRadialGradient(0,0,o.r*0.25,0,0,o.r*1.5); pg.addColorStop(0,"rgba(180,120,255,0)"); pg.addColorStop(0.68,"rgba(150,90,255,.34)"); pg.addColorStop(1,"rgba(40,230,255,0)"); ctx.fillStyle=pg; ctx.beginPath(); ctx.arc(0,0,o.r*1.5,0,7); ctx.fill();
      ctx.shadowBlur=0; for(let i=0;i<3;i++){ const rr2=o.r*(1-i*0.27); ctx.strokeStyle=i%2?"rgba(40,230,255,.85)":"rgba(180,120,255,.85)"; ctx.lineWidth=3*DPR; ctx.beginPath(); ctx.ellipse(0,0,rr2,rr2*0.9,sp*(i%2?1:-1),0,5.6); ctx.stroke(); }
      ctx.fillStyle="rgba(240,230,255,.92)"; ctx.beginPath(); ctx.arc(0,0,o.r*0.2,0,7); ctx.fill(); ctx.restore(); continue; }
    if(o.type==="updraft"){ if(o.hit) continue; ctx.save(); ctx.translate(x,o.y); const sp=t*4+o.x; // UPDRAFT = floating boost, launches you high
      let ag=ctx.createRadialGradient(0,0,2*DPR,0,0,o.r*1.9); ag.addColorStop(0,"rgba(120,220,255,.45)"); ag.addColorStop(0.6,"rgba(90,200,255,.18)"); ag.addColorStop(1,"rgba(120,220,255,0)");
      ctx.fillStyle=ag; ctx.beginPath(); ctx.arc(0,0,o.r*1.9,0,7); ctx.fill();
      ctx.save(); ctx.globalCompositeOperation="lighter";
      for(let i=0;i<8;i++){ const ph=((sp*0.5+i/8)%1); const py=o.r*0.95 - ph*o.r*2.2, px=Math.sin(sp+i*1.4)*o.r*0.55*(1-ph*0.7), a=(1-ph)*0.7;
        ctx.fillStyle="rgba(180,240,255,"+a+")"; ctx.beginPath(); ctx.arc(px,py,(2.6-ph*1.6)*DPR,0,7); ctx.fill(); }
      ctx.restore();
      ctx.strokeStyle="rgba(225,250,255,.85)"; ctx.lineWidth=3*DPR; ctx.lineCap="round"; ctx.lineJoin="round";
      for(let c=0;c<3;c++){ const cy=-o.r*0.7+c*o.r*0.6 + Math.sin(sp - c)*2*DPR, w=o.r*0.5;
        ctx.globalAlpha=0.45+0.25*Math.sin(sp*2-c); ctx.beginPath(); ctx.moveTo(-w,cy+w*0.6); ctx.lineTo(0,cy); ctx.lineTo(w,cy+w*0.6); ctx.stroke(); }
      ctx.globalAlpha=1; ctx.restore(); continue; }
    if(o.type==="rocket"){ if(o.hit) continue; const fl=0.6+0.4*Math.sin(t*30+o.x); ctx.save(); ctx.translate(x,o.y); // ROCKET = explode + big launch
      ctx.fillStyle="rgba(255,150,30,"+(0.7+0.3*fl)+")"; ctx.beginPath(); ctx.moveTo(-o.r*0.4,o.r*0.7); ctx.lineTo(0,o.r*(1.3+0.5*fl)); ctx.lineTo(o.r*0.4,o.r*0.7); ctx.closePath(); ctx.fill();
      ctx.fillStyle="#e7ecf5"; roundRect(-o.r*0.45,-o.r*0.7,o.r*0.9,o.r*1.4,o.r*0.3); ctx.fill();
      ctx.fillStyle="#ff4d5e"; ctx.beginPath(); ctx.moveTo(-o.r*0.45,-o.r*0.5); ctx.lineTo(0,-o.r*1.25); ctx.lineTo(o.r*0.45,-o.r*0.5); ctx.closePath(); ctx.fill();
      ctx.beginPath(); ctx.moveTo(-o.r*0.45,o.r*0.4); ctx.lineTo(-o.r*0.85,o.r*0.78); ctx.lineTo(-o.r*0.45,o.r*0.7); ctx.closePath(); ctx.moveTo(o.r*0.45,o.r*0.4); ctx.lineTo(o.r*0.85,o.r*0.78); ctx.lineTo(o.r*0.45,o.r*0.7); ctx.closePath(); ctx.fill();
      ctx.fillStyle="#5cc8ff"; ctx.beginPath(); ctx.arc(0,-o.r*0.12,o.r*0.22,0,7); ctx.fill(); ctx.restore(); continue; }
    if(o.type==="float"){ const D=DPR, hw=o.w/2, bob=Math.sin(t*3+o.x)*2.5*D; const cyf=o.y-o.h*0.5+bob, rx=hw, ry=o.h+3*D, tube=ry*0.62, rxc=rx-tube*0.5, ryc=ry-tube*0.5; // FLOATIE = inflatable pool ring (bouncy water platform)
      ctx.fillStyle="rgba(0,0,0,.14)"; ctx.beginPath(); ctx.ellipse(x,o.y+3*D,rx*0.95,4*D,0,0,7); ctx.fill(); // water shadow
      ctx.lineCap="butt"; ctx.lineWidth=tube; const segs=8;
      for(let s=0;s<segs;s++){ ctx.strokeStyle=(s&1)?"#ff7ab8":"#fff4fa"; ctx.beginPath(); ctx.ellipse(x,cyf,rxc,ryc,0,s/segs*6.283,(s+1)/segs*6.283); ctx.stroke(); } // striped inflatable ring
      ctx.lineWidth=tube*0.28; ctx.strokeStyle="rgba(255,255,255,.6)"; ctx.beginPath(); ctx.ellipse(x,cyf,rxc,ryc,0,3.45,4.55); ctx.stroke(); // top gloss highlight
      ctx.strokeStyle="rgba(170,235,255,.45)"; ctx.lineWidth=2*D; ctx.beginPath(); ctx.ellipse(x,o.y+1*D,rx*1.06,5*D,0,0,7); ctx.stroke(); } // surface ripple
    else if(o.type==="diveboard"){ const hw=o.w/2, my=o.y-o.h; // DIVING BOARD = big launch
      ctx.fillStyle="#5a6678"; roundRect(x-hw,o.y-4*DPR,10*DPR,o.h+4*DPR,2*DPR); ctx.fill();
      let gd=ctx.createLinearGradient(0,my,0,my+10*DPR); gd.addColorStop(0,"#ffffff"); gd.addColorStop(1,"#cfe0ee");
      ctx.fillStyle=gd; roundRect(x-hw,my,o.w+8*DPR,9*DPR,4*DPR); ctx.fill();
      ctx.fillStyle="#ff9a3c"; roundRect(x+hw-2*DPR,my-1*DPR,12*DPR,11*DPR,3*DPR); ctx.fill();
      const dby=my-8*DPR + Math.sin(t*4+x)*3*DPR, dx2=x+hw+4*DPR; ctx.strokeStyle="#bfeaff"; ctx.lineWidth=3*DPR; ctx.lineJoin="round"; ctx.lineCap="round"; ctx.beginPath(); ctx.moveTo(dx2-8*DPR,dby+7*DPR); ctx.lineTo(dx2,dby); ctx.lineTo(dx2+8*DPR,dby+7*DPR); ctx.stroke(); }
    else if(o.type==="slide"){ const hw=o.w/2, my=o.y-o.h, D=DPR; // RIDABLE PARK SLIDE = boost ramp (curves up to the right, ejects forward+up)
      const P0x=x-hw, P0y=o.y-2*D, Pcx=x+hw*0.25, Pcy=o.y-2*D, P1x=x+hw, P1y=my;
      const qp=u=>{ const v=1-u; return [v*v*P0x+2*v*u*Pcx+u*u*P1x, v*v*P0y+2*v*u*Pcy+u*u*P1y]; };
      ctx.fillStyle="#7a5bd0"; const [s1x]=qp(0.18), [s2x]=qp(0.72); roundRect(s1x-4*D,o.y-o.h*0.34,8*D,o.h*0.34+6*D,3*D); ctx.fill(); roundRect(s2x-4*D,o.y-o.h*0.7,8*D,o.h*0.7+6*D,3*D); ctx.fill(); // support posts
      ctx.lineCap="round"; ctx.lineJoin="round";
      ctx.strokeStyle=getAccent(.9); ctx.lineWidth=15*D; ctx.beginPath(); ctx.moveTo(P0x,P0y); ctx.quadraticCurveTo(Pcx,Pcy,P1x,P1y); ctx.stroke(); // chute body
      ctx.strokeStyle="rgba(255,255,255,.75)"; ctx.lineWidth=4*D; ctx.beginPath(); ctx.moveTo(P0x,P0y-6*D); ctx.quadraticCurveTo(Pcx,Pcy-6*D,P1x,P1y-6*D); ctx.stroke(); // gloss highlight
      ctx.fillStyle="rgba(255,255,255,.55)"; for(let i=1;i<6;i++){ const [px,py]=qp(i/6); ctx.beginPath(); ctx.arc(px,py-6*D,2.2*D,0,7); ctx.fill(); } // chute studs
      const ax=P1x+8*D, ay=P1y-4*D + Math.sin(t*4+x)*3*D; ctx.strokeStyle="#ffd36b"; ctx.lineWidth=3.5*D; ctx.beginPath(); ctx.moveTo(ax-9*D,ay+8*D); ctx.lineTo(ax,ay); ctx.lineTo(ax-9*D,ay-8*D); ctx.stroke(); } // forward boost chevron
    else if(o.type==="pad"){ const hw=o.w/2, my=o.y-o.h; // GREEN trampoline (concave membrane + rounded up-chevron) - clearly a bouncer, not spikes
      ctx.fillStyle="#1c7a3f"; roundRect(x-hw,my-2*DPR,7*DPR,o.h+6*DPR,3*DPR); ctx.fill(); roundRect(x+hw-7*DPR,my-2*DPR,7*DPR,o.h+6*DPR,3*DPR); ctx.fill();
      ctx.lineCap="round";
      ctx.strokeStyle="rgba(62,224,127,.35)"; ctx.lineWidth=12*DPR; ctx.beginPath(); ctx.moveTo(x-hw+3*DPR,my+2*DPR); ctx.quadraticCurveTo(x,my+13*DPR,x+hw-3*DPR,my+2*DPR); ctx.stroke();
      ctx.strokeStyle="#3ee07f"; ctx.lineWidth=6*DPR; ctx.beginPath(); ctx.moveTo(x-hw+3*DPR,my+2*DPR); ctx.quadraticCurveTo(x,my+13*DPR,x+hw-3*DPR,my+2*DPR); ctx.stroke();
      const by=my-9*DPR + Math.sin(t*4+x)*3*DPR;
      ctx.strokeStyle="#eafff2"; ctx.lineWidth=3.5*DPR; ctx.lineJoin="round"; ctx.beginPath(); ctx.moveTo(x-9*DPR,by+8*DPR); ctx.lineTo(x,by); ctx.lineTo(x+9*DPR,by+8*DPR); ctx.stroke(); }
    else if(o.type==="jackpot"){ const hw=o.w/2, my=o.y-o.h; ctx.save(); if(o.hit) ctx.globalAlpha=0.55; // GOLDEN jackpot pad: glowing $ bouncer (variable big payout, once)
      ctx.fillStyle="#7a5a14"; roundRect(x-hw,my-2*DPR,8*DPR,o.h+6*DPR,3*DPR); ctx.fill(); roundRect(x+hw-8*DPR,my-2*DPR,8*DPR,o.h+6*DPR,3*DPR); ctx.fill();
      const glow=0.5+0.5*Math.sin(t*6+x); ctx.lineCap="round";
      ctx.strokeStyle="rgba(255,211,77,"+(0.28+glow*0.4)+")"; ctx.lineWidth=15*DPR; ctx.beginPath(); ctx.moveTo(x-hw+3*DPR,my+2*DPR); ctx.quadraticCurveTo(x,my+14*DPR,x+hw-3*DPR,my+2*DPR); ctx.stroke();
      ctx.strokeStyle="#ffd34d"; ctx.lineWidth=6*DPR; ctx.beginPath(); ctx.moveTo(x-hw+3*DPR,my+2*DPR); ctx.quadraticCurveTo(x,my+14*DPR,x+hw-3*DPR,my+2*DPR); ctx.stroke();
      if(!o.hit){ ctx.fillStyle="#fff7d6"; ctx.font="900 "+(15*DPR)+"px Orbitron"; ctx.textAlign="center"; ctx.fillText("$", x, my-7*DPR+Math.sin(t*4+x)*2*DPR); }
      ctx.restore(); ctx.globalAlpha=1; }
    else if(o.type==="spring"){ ctx.strokeStyle="#ffcf5a"; ctx.lineWidth=5*DPR; ctx.beginPath();
      for(let i=0;i<4;i++){ ctx.moveTo(x-14*DPR,o.y-i*11*DPR); ctx.lineTo(x+14*DPR,o.y-i*11*DPR-6*DPR); } ctx.stroke();
      ctx.fillStyle="#ffd96b"; roundRect(x-18*DPR,o.y-o.h-8*DPR,36*DPR,10*DPR,3*DPR); ctx.fill();
      ctx.fillStyle="rgba(255,255,255,.4)"; ctx.fillRect(x-15*DPR,o.y-o.h-6*DPR,30*DPR,2*DPR); }
    else if(o.type==="balloon"){ if(o.pop) continue;
      const R=o.r, bcy=o.y, neckY=bcy+R*0.98, knotY=bcy+R*1.12, attachY=o.y+o.r+18*DPR;
      ctx.beginPath(); ctx.moveTo(x, bcy-R*1.02);                                   // teardrop body
      ctx.bezierCurveTo(x+R*1.12, bcy-R*1.02, x+R*1.02, bcy+R*0.55, x+R*0.14, neckY);
      ctx.bezierCurveTo(x+R*0.05, bcy+R*1.02, x-R*0.05, bcy+R*1.02, x-R*0.14, neckY);
      ctx.bezierCurveTo(x-R*1.02, bcy+R*0.55, x-R*1.12, bcy-R*1.02, x, bcy-R*1.02); ctx.closePath();
      const bgrad=ctx.createRadialGradient(x-R*0.35, bcy-R*0.5, R*0.1, x+R*0.15, bcy+R*0.2, R*1.3);
      bgrad.addColorStop(0,"#c4e9ff"); bgrad.addColorStop(0.34,"#5cc0ff"); bgrad.addColorStop(0.72,"#2f8fe0"); bgrad.addColorStop(1,"#164e93");
      ctx.fillStyle=bgrad; ctx.fill();
      ctx.strokeStyle="rgba(120,190,245,.45)"; ctx.lineWidth=1.4*DPR; ctx.lineJoin="round"; ctx.stroke();
      const shx=x-R*0.34+Math.sin(t*1.5+o.x*0.02)*R*0.05, shy=bcy-R*0.52;               // glossy sheen
      const sh=ctx.createRadialGradient(shx, shy, 0, shx, shy, R*0.62);
      sh.addColorStop(0,"rgba(255,255,255,.85)"); sh.addColorStop(0.5,"rgba(255,255,255,.22)"); sh.addColorStop(1,"rgba(255,255,255,0)");
      ctx.fillStyle=sh; ctx.beginPath(); ctx.ellipse(shx, shy, R*0.34, R*0.52, -0.5, 0, 7); ctx.fill();
      ctx.fillStyle="rgba(255,255,255,.92)"; ctx.beginPath(); ctx.ellipse(x-R*0.44, bcy-R*0.6, R*0.1, R*0.16, -0.5, 0, 7); ctx.fill(); // catch-light
      ctx.fillStyle="#1f6fc0"; ctx.beginPath(); ctx.moveTo(x-R*0.14, neckY); ctx.lineTo(x, knotY); ctx.lineTo(x+R*0.14, neckY); ctx.closePath(); ctx.fill(); // knot
      const bsway=Math.sin(t*2+o.x*0.05)*2*DPR;                                         // swaying string down to the grenade
      ctx.strokeStyle="rgba(190,222,248,.6)"; ctx.lineWidth=1.6*DPR; ctx.lineCap="round";
      ctx.beginPath(); ctx.moveTo(x, knotY); ctx.quadraticCurveTo(x+bsway, (knotY+attachY)/2, x, attachY); ctx.stroke();
      // grenade dangling below: this balloon BLASTS you horizontally
      const ggy=o.y+o.r+18*DPR;
      ctx.fillStyle="#8a8f98"; ctx.fillRect(x-2.5*DPR,ggy-5*DPR,5*DPR,3*DPR);                       // cap
      ctx.strokeStyle="#c8ccd2"; ctx.lineWidth=1.4*DPR; ctx.beginPath(); ctx.moveTo(x+2*DPR,ggy-4*DPR); ctx.quadraticCurveTo(x+8*DPR,ggy-3*DPR,x+6*DPR,ggy+3*DPR); ctx.stroke(); // lever
      const gg=ctx.createLinearGradient(0,ggy-2*DPR,0,ggy+14*DPR); gg.addColorStop(0,"#4c6b45"); gg.addColorStop(1,"#2c4129");
      ctx.fillStyle=gg; ctx.beginPath(); ctx.ellipse(x,ggy+6*DPR,6.5*DPR,8*DPR,0,0,7); ctx.fill();    // body
      ctx.fillStyle="rgba(255,255,255,.22)"; ctx.beginPath(); ctx.ellipse(x-2*DPR,ggy+3*DPR,2*DPR,2.6*DPR,0,0,7); ctx.fill(); // highlight
      ctx.strokeStyle="rgba(0,0,0,.28)"; ctx.lineWidth=1*DPR; ctx.beginPath(); ctx.moveTo(x-5*DPR,ggy+5*DPR); ctx.lineTo(x+5*DPR,ggy+9*DPR); ctx.moveTo(x-5*DPR,ggy+9*DPR); ctx.lineTo(x+5*DPR,ggy+5*DPR); ctx.stroke(); }
    else if(o.type==="spike"){ ctx.fillStyle="rgba(255,45,79,.22)"; ctx.beginPath(); ctx.ellipse(x,o.y,o.w*0.6,7*DPR,0,0,7); ctx.fill(); // RED reserved for danger only
      ctx.fillStyle="#ff2d4f"; ctx.strokeStyle="rgba(0,0,0,.55)"; ctx.lineWidth=2*DPR; ctx.lineJoin="round";
      for(let i=0;i<3;i++){ ctx.beginPath(); ctx.moveTo(x-o.w/2+i*18*DPR,o.y); ctx.lineTo(x-o.w/2+i*18*DPR+9*DPR,o.y-o.h); ctx.lineTo(x-o.w/2+i*18*DPR+18*DPR,o.y); ctx.closePath(); ctx.fill(); ctx.stroke(); } }
    else if(o.type==="tramp"){ const hw=o.w/2, my=o.y-o.h; // MEGA green trampoline (double chevron = stronger)
      ctx.fillStyle="#1c7a3f"; roundRect(x-hw,my-2*DPR,9*DPR,o.h+8*DPR,3*DPR); ctx.fill(); roundRect(x+hw-9*DPR,my-2*DPR,9*DPR,o.h+8*DPR,3*DPR); ctx.fill();
      ctx.lineCap="round"; ctx.strokeStyle="rgba(93,255,158,.4)"; ctx.lineWidth=16*DPR; ctx.beginPath(); ctx.moveTo(x-hw+4*DPR,my+2*DPR); ctx.quadraticCurveTo(x,my+18*DPR,x+hw-4*DPR,my+2*DPR); ctx.stroke();
      ctx.strokeStyle="#5dff9e"; ctx.lineWidth=7*DPR; ctx.beginPath(); ctx.moveTo(x-hw+4*DPR,my+2*DPR); ctx.quadraticCurveTo(x,my+18*DPR,x+hw-4*DPR,my+2*DPR); ctx.stroke();
      const by=my-12*DPR+Math.sin(t*4+x)*4*DPR; ctx.strokeStyle="#eafff2"; ctx.lineWidth=4*DPR; ctx.lineJoin="round";
      ctx.beginPath(); ctx.moveTo(x-12*DPR,by+10*DPR); ctx.lineTo(x,by); ctx.lineTo(x+12*DPR,by+10*DPR); ctx.moveTo(x-12*DPR,by+19*DPR); ctx.lineTo(x,by+9*DPR); ctx.lineTo(x+12*DPR,by+19*DPR); ctx.stroke(); }
    else if(o.type==="tnt"){ const w=o.w,h=o.h,bx=x-w/2,by2=o.y-h; // TNT crate (hazard)
      ctx.fillStyle="#7a1f1f"; roundRect(bx,by2,w,h,4*DPR); ctx.fill();
      ctx.fillStyle="#b8332f"; ctx.fillRect(bx,by2+h*0.36,w,h*0.28);
      ctx.fillStyle="#ffe7c2"; ctx.font="900 "+(12*DPR)+"px Orbitron"; ctx.textAlign="center"; ctx.fillText("TNT", x, by2+h*0.62);
      ctx.strokeStyle="#caa15a"; ctx.lineWidth=2.5*DPR; ctx.lineCap="round"; ctx.beginPath(); ctx.moveTo(x,by2); ctx.quadraticCurveTo(x+8*DPR,by2-12*DPR,x+4*DPR,by2-18*DPR); ctx.stroke();
      const sp2=Math.sin(t*20)*0.5+0.5; ctx.fillStyle="#ffd34d"; ctx.beginPath(); ctx.arc(x+4*DPR,by2-18*DPR,(2+2*sp2)*DPR,0,7); ctx.fill(); }
    else if(o.type==="doghouse"){ drawDogHouse(x,o,t); }
  }
}
function drawReboot(){ if(!rebootFx) return; const x=rebootFx.x-cam, y=rebootFx.y;
  ctx.save(); ctx.globalAlpha=Math.max(0,rebootFx.t);
  ctx.fillStyle="#2a2440"; roundRect(x-34*DPR,y-44*DPR,68*DPR,44*DPR,8*DPR); ctx.fill();
  ctx.fillStyle=getAccent(.95); roundRect(x-34*DPR,y-50*DPR,68*DPR,8*DPR,4*DPR); ctx.fill();
  ctx.save(); ctx.translate(x+12*DPR,y-26*DPR); ctx.rotate(-46*Math.PI/180); ctx.fillStyle="#3a3358"; ctx.fillRect(0,-9*DPR,40*DPR,18*DPR); ctx.restore();
  ctx.fillStyle="#fff"; ctx.font=`800 ${11*DPR}px Orbitron`; ctx.textAlign="center"; ctx.fillText("REBOOT", x, y-58*DPR);
  ctx.restore(); rebootFx.t-=0.018; if(rebootFx.t<=0) rebootFx=null; }

// ---- the chunky original kitten (matches the sprite.png blob frame) ----
function drawKittyArt(g, r, col){
  col=col||{body:"#c8bcef", body2:"#a994e0", ear:"#f4a9c8", eye:"#1a1230", nose:"#f4a9c8"};
  g.lineJoin="round"; g.lineCap="round";
  // tail
  g.strokeStyle=col.body2; g.lineWidth=r*0.4;
  g.beginPath(); g.moveTo(r*0.82,r*0.5); g.quadraticCurveTo(r*1.5,r*0.2,r*1.22,-r*0.45); g.stroke();
  // chunky rounded body (the frame)
  const bw=1.06*r, top=-1.5*r, bot=0.92*r, rad=0.72*r;
  g.fillStyle=col.body; rr(g,-bw,top,bw*2,bot-top,rad); g.fill(); g.strokeStyle="rgba(18,12,36,.55)"; g.lineWidth=r*0.11; g.stroke();
  // belly highlight
  g.fillStyle=col.body2; g.globalAlpha=.45; rr(g,-bw*0.66,top+r*0.95,bw*1.32,bot-top-r*1.05,rad*0.7); g.fill(); g.globalAlpha=1;
  // ears
  g.fillStyle=col.body; for(const s of [-1,1]){ g.beginPath(); g.moveTo(s*0.6*r, top+0.2*r); g.lineTo(s*0.95*r, top-0.5*r); g.lineTo(s*0.16*r, top+0.06*r); g.closePath(); g.fill(); }
  g.fillStyle=col.ear; for(const s of [-1,1]){ g.beginPath(); g.moveTo(s*0.58*r, top+0.12*r); g.lineTo(s*0.8*r, top-0.32*r); g.lineTo(s*0.28*r, top+0.02*r); g.closePath(); g.fill(); }
  // face
  const ey=top+0.98*r;
  g.fillStyle=col.eye; g.beginPath(); g.arc(-0.42*r,ey,0.16*r,0,7); g.arc(0.42*r,ey,0.16*r,0,7); g.fill();
  g.fillStyle="#fff"; g.beginPath(); g.arc(-0.37*r,ey-0.05*r,0.055*r,0,7); g.arc(0.47*r,ey-0.05*r,0.055*r,0,7); g.fill();
  g.fillStyle=col.nose; g.beginPath(); g.moveTo(0,ey+0.34*r); g.lineTo(-0.12*r,ey+0.2*r); g.lineTo(0.12*r,ey+0.2*r); g.closePath(); g.fill();
  g.fillStyle="rgba(244,169,200,.4)"; g.beginPath(); g.arc(-0.72*r,ey+0.2*r,0.16*r,0,7); g.arc(0.72*r,ey+0.2*r,0.16*r,0,7); g.fill();
  g.strokeStyle="rgba(40,30,70,.4)"; g.lineWidth=r*0.045; for(const s of [-1,1]){ g.beginPath();
    g.moveTo(s*0.32*r,ey+0.3*r); g.lineTo(s*0.96*r,ey+0.22*r); g.moveTo(s*0.32*r,ey+0.42*r); g.lineTo(s*0.94*r,ey+0.46*r); g.stroke(); }
  // paws
  g.fillStyle=col.body; g.beginPath(); g.ellipse(-0.48*r,bot-0.04*r,0.34*r,0.24*r,0,0,7); g.ellipse(0.48*r,bot-0.04*r,0.34*r,0.24*r,0,0,7); g.fill();
}
function drawKitty(){
  if(state==="start"||state==="shop") return;
  const x=kitty.x-cam, y=kitty.y;
  const gy=GROUND(), sc=Math.max(0.25, 1-(gy-(y+kitty.r))/(420*DPR));
  ctx.fillStyle="rgba(0,0,0,.28)"; ctx.beginPath(); ctx.ellipse(x, gy-3*DPR, kitty.r*1.15*sc, kitty.r*0.3*sc, 0,0,7); ctx.fill();
  ctx.save(); ctx.translate(x,y); ctx.rotate(state==="fly"?kitty.spin:0); ctx.scale(kitty.sx,kitty.sy);
  if(ab()==="bartman" && state==="fly"){ // BARTHOLOMEW: hero CAPE stays flowing the whole time you're in play (billows bigger the faster he dives) + the bat-ear cowl "helmet"
    const r=kitty.r*SKIN_VIS, t2=performance.now()/160, fall=Math.max(0.25,Math.min(1,kitty.vy/(8*DPR))), up=r*(1.15+fall*2.0);
    const swayL=Math.sin(t2)*0.18*r, swayR=Math.sin(t2*1.15+0.8)*0.18*r, swayM=Math.sin(t2*0.9+2)*0.14*r;
    ctx.save();
    ctx.fillStyle="#141126"; // cape cloth
    ctx.beginPath(); ctx.moveTo(-r*0.46,-r*0.3);                       // left shoulder anchor
    ctx.quadraticCurveTo(-r*0.95+swayL, -up*0.55, -r*0.7+swayL, -up);  // up the left side
    ctx.quadraticCurveTo(-r*0.3+swayM, -up*0.86, 0, -up*1.02+swayM);   // wavy crest (left half)
    ctx.quadraticCurveTo(r*0.3+swayM, -up*0.86, r*0.7+swayR, -up);     // wavy crest (right half)
    ctx.quadraticCurveTo(r*0.95+swayR, -up*0.55, r*0.46,-r*0.3);       // down the right side
    ctx.quadraticCurveTo(0,-r*0.02, -r*0.46,-r*0.3); ctx.closePath(); ctx.fill();
    ctx.fillStyle="#2c2646"; ctx.globalAlpha=0.55;                     // inner-fold highlight
    ctx.beginPath(); ctx.moveTo(-r*0.2,-r*0.24); ctx.quadraticCurveTo(swayM,-up*0.8,r*0.2,-r*0.24); ctx.quadraticCurveTo(0,-r*0.02,-r*0.2,-r*0.24); ctx.closePath(); ctx.fill(); ctx.globalAlpha=1;
    ctx.restore();
  } // (cowl/helmet is drawn AFTER the sprite below, so it isn't hidden behind the big skin)
  if(phaseT>0){ ctx.globalAlpha=0.4+0.16*Math.sin(performance.now()*0.015); ctx.filter="brightness(.7) saturate(.5)"; } // PHASE: spectral translucent shimmer while intangible (drawKitty has no `t` param -> use the clock directly)
  const _super=(superT>0 && save.skin==="fastboi" && skinImgs["fastboi__super"]); // GodSpeed literally transforms the fastboi sprite while super is active (overrides the chosen style)
  const img=_super?skinImgs["fastboi__super"]:skinImgFor(save.skin);
  // NOTE: Super Star (starT) deliberately does NOT use a per-frame ctx.filter hue-rotate here anymore -
  // canvas filters force a slow software pass that tanked FPS, and since the game uses a FIXED per-frame
  // timestep, the FPS drop read as "everything went slow-mo" (worst in the small Twitch extension iframe).
  // The cheap rainbow RINGS + sparkles in drawPassiveHud() carry the star look at ~zero cost.
  if(img){ const bx=_super?skinBox["fastboi__super"]:skinBoxFor(save.skin), cw=bx?bx.w:img.width, ch=bx?bx.h:img.height, ccx=bx?(bx.x+bx.w/2):img.width/2, ccy=bx?(bx.y+bx.h/2):img.height/2;
    let s=(kitty.r*2.62*SKIN_VIS)/Math.sqrt(cw*ch); if(ch*s<kitty.r*2.05*SKIN_VIS) s=(kitty.r*2.05*SKIN_VIS)/ch; if(cw*s>kitty.r*3.6*SKIN_VIS) s=(kitty.r*3.6*SKIN_VIS)/cw; // area-norm + height floor (no skin too short, e.g. head-only gas mask) + width cap [SKIN_VIS scales visual only]
    ctx.drawImage(img,-ccx*s,-ccy*s,img.width*s,img.height*s); } // center on creature
  else drawKittyArt(ctx, kitty.r);
  if(ab()==="bartman" && state==="fly"){ const r=kitty.r*SKIN_VIS; ctx.save(); // little BAT HORNS on top of the head (no band/cowl, so they don't cover his hair)
    ctx.fillStyle="#141126"; ctx.strokeStyle="rgba(150,154,190,.45)"; ctx.lineWidth=r*0.045; ctx.lineJoin="round";
    for(const sgn of [-1,1]){ ctx.beginPath(); ctx.moveTo(sgn*r*0.30,-r*1.5); ctx.lineTo(sgn*r*0.46,-r*2.12); ctx.lineTo(sgn*r*0.58,-r*1.46); ctx.closePath(); ctx.fill(); ctx.stroke(); } // two little bat horns
    ctx.restore(); }
  if(phaseT>0){ ctx.globalAlpha=1; ctx.filter="none"; }
  ctx.restore();
}
function drawGauntletBg(type,t){
  const g=GROUND();
  if(type==="fire"){
    // ---- deep layered volcanic sky ----
    const sky=ctx.createLinearGradient(0,0,0,g);
    sky.addColorStop(0,"#0d0308"); sky.addColorStop(0.38,"#2a0708");
    sky.addColorStop(0.72,"#5c1305"); sky.addColorStop(1,"#93270699");
    ctx.fillStyle=sky; ctx.fillRect(0,0,W,g);
    ctx.save(); ctx.globalCompositeOperation="lighter";
    const glow=ctx.createRadialGradient(W*0.5,g,8*DPR,W*0.5,g,Math.max(1,W*0.8));
    glow.addColorStop(0,"rgba(255,140,45,.5)"); glow.addColorStop(0.45,"rgba(255,80,20,.2)"); glow.addColorStop(1,"rgba(255,60,10,0)");
    ctx.fillStyle=glow; ctx.fillRect(0,0,W,g);
    ctx.restore();
    // ---- distant erupting volcano silhouettes (deterministic) ----
    const nV=4;
    for(let i=0;i<nV;i++){
      const vx=((i+0.5)/nV)*W + (hashS(i*3+1)-0.5)*W*0.10;
      const vh=(0.15+hashY(i*2+1)*0.15)*g;
      const vw=Math.max(1,vh*(1.5+hashS(i+2)*0.9));
      const peakY=g-vh, cr=Math.max(1,vw*0.16);
      ctx.fillStyle="#180402";
      ctx.beginPath(); ctx.moveTo(vx-vw,g); ctx.lineTo(vx-cr,peakY); ctx.lineTo(vx+cr,peakY); ctx.lineTo(vx+vw,g); ctx.closePath(); ctx.fill();
      const pulse=0.55+0.45*Math.sin(t*1.6+i*1.7);
      ctx.save(); ctx.globalCompositeOperation="lighter";
      const cg=ctx.createRadialGradient(vx,peakY,1*DPR,vx,peakY,Math.max(1,cr*3));
      cg.addColorStop(0,"rgba(255,180,60,"+(0.5+0.35*pulse)+")"); cg.addColorStop(1,"rgba(255,90,20,0)");
      ctx.fillStyle=cg; ctx.beginPath(); ctx.arc(vx,peakY,Math.max(1,cr*3),0,7); ctx.fill();
      const side=(i%2)?1:-1;
      ctx.strokeStyle="rgba(255,120,30,"+(0.3+0.3*pulse)+")"; ctx.lineWidth=Math.max(1,2*DPR); ctx.lineCap="round";
      ctx.beginPath(); ctx.moveTo(vx+side*cr*0.5,peakY+cr*0.4);
      ctx.quadraticCurveTo(vx+side*vw*0.4, g-vh*0.4, vx+side*vw*0.55, g); ctx.stroke();
      ctx.restore();
    }
    // ---- molten lava floor ----
    const floor=ctx.createLinearGradient(0,g,0,H);
    floor.addColorStop(0,"#ff6a12"); floor.addColorStop(0.16,"#cf2f04"); floor.addColorStop(0.55,"#6e1403"); floor.addColorStop(1,"#280602");
    ctx.fillStyle=floor; ctx.fillRect(0,g,W,H-g);
    ctx.save(); ctx.globalCompositeOperation="lighter"; ctx.lineCap="round";
    for(let i=0;i<6;i++){
      const cy=g+(0.14+hashY(i+5)*0.72)*(H-g);
      const cx0=hashS(i+3)*W, dir=(i%2)?1:-1;
      ctx.strokeStyle="rgba(255,"+(110+((i*37)%110))+",30,"+(0.22+0.18*Math.sin(t*2+i))+")";
      ctx.lineWidth=Math.max(1,(1.2+hashS(i)*1.8)*DPR);
      ctx.beginPath();
      for(let s=0;s<=6;s++){ const px=cx0+s*W*0.09*dir, py=cy+Math.sin(px*0.02+t+i)*4*DPR; if(s===0)ctx.moveTo(px,py); else ctx.lineTo(px,py); }
      ctx.stroke();
    }
    ctx.restore();
    ctx.save(); ctx.globalCompositeOperation="lighter";
    const lip=ctx.createLinearGradient(0,g-24*DPR,0,g+12*DPR);
    lip.addColorStop(0,"rgba(255,190,50,0)"); lip.addColorStop(0.6,"rgba(255,120,20,.5)"); lip.addColorStop(1,"rgba(255,90,10,.8)");
    ctx.fillStyle=lip; ctx.beginPath(); ctx.moveTo(0,g+12*DPR);
    for(let x=0;x<=W;x+=14*DPR){ const y=g-(6+4*Math.sin(x*0.05+t*5)+3*Math.sin(x*0.11-t*7))*DPR; ctx.lineTo(x,y); }
    ctx.lineTo(W,g+12*DPR); ctx.closePath(); ctx.fill();
    ctx.restore();
    ctx.save(); ctx.globalCompositeOperation="lighter";
    for(let i=0;i<30;i++){
      const ex=(hashS(i)*W + i*53.7 + t*(12+(i%3)*10))%W;
      const cyc=(t*0.35+hashY(i))%1;
      const ey=g-cyc*g*0.92, drift=Math.sin(t*1.4+i)*10*DPR;
      const r=Math.max(1,(1.2+hashS(i+9)*2.2)*DPR);
      ctx.globalAlpha=(1-cyc)*0.5;
      ctx.fillStyle=i%3?"#ffce6a":"#ff7a1a";
      ctx.beginPath(); ctx.arc(ex+drift,ey,r,0,7); ctx.fill();
    }
    ctx.globalAlpha=1; ctx.restore();
    ctx.globalAlpha=1; ctx.globalCompositeOperation="source-over";
  } else if(type==="zombie"){
    const sky=ctx.createLinearGradient(0,0,0,g);
    sky.addColorStop(0,"#050810"); sky.addColorStop(0.42,"#0b1420");
    sky.addColorStop(0.78,"#16202a"); sky.addColorStop(1,"#1b2a22");
    ctx.fillStyle=sky; ctx.fillRect(0,0,W,g);
    const mx=W*0.72, my=g*0.30, mr=Math.max(1,Math.min(W,g)*0.13);
    ctx.save(); ctx.globalCompositeOperation="lighter";
    const mg=ctx.createRadialGradient(mx,my,mr*0.4,mx,my,Math.max(1,mr*3.4));
    mg.addColorStop(0,"rgba(255,130,60,.55)"); mg.addColorStop(0.4,"rgba(220,70,40,.20)"); mg.addColorStop(1,"rgba(180,40,30,0)");
    ctx.fillStyle=mg; ctx.beginPath(); ctx.arc(mx,my,Math.max(1,mr*3.4),0,7); ctx.fill();
    ctx.restore();
    const md=ctx.createRadialGradient(mx-mr*0.3,my-mr*0.3,mr*0.2,mx,my,mr);
    md.addColorStop(0,"#ffddab"); md.addColorStop(0.5,"#ff9b52"); md.addColorStop(1,"#d5562b");
    ctx.fillStyle=md; ctx.beginPath(); ctx.arc(mx,my,mr,0,7); ctx.fill();
    ctx.fillStyle="rgba(120,40,20,.22)";
    for(let i=0;i<5;i++){ const a=hashY(i+3)*7, rd=hashS(i+2)*mr*0.6, cr=Math.max(1,(0.08+hashS(i)*0.13)*mr);
      ctx.beginPath(); ctx.arc(mx+Math.cos(a)*rd,my+Math.sin(a)*rd,cr,0,7); ctx.fill(); }
    ctx.fillStyle="#080506";
    for(let i=0;i<5;i++){
      const bx=(-40*DPR + (t*(24+i*9)+hashS(i)*W)%(W+80*DPR));
      const by=g*0.18 + hashY(i)*g*0.34 + Math.sin(t*2+i)*6*DPR;
      const bw=Math.max(1,(6+i*1.6)*DPR), fl=Math.sin(t*11+i*2)*bw*0.5;
      ctx.beginPath(); ctx.moveTo(bx,by);
      ctx.quadraticCurveTo(bx-bw*0.6,by-bw*0.5-fl, bx-bw*1.5,by);
      ctx.quadraticCurveTo(bx-bw*0.6,by-bw*0.1, bx,by);
      ctx.quadraticCurveTo(bx+bw*0.6,by-bw*0.5-fl, bx+bw*1.5,by);
      ctx.quadraticCurveTo(bx+bw*0.6,by-bw*0.1, bx,by); ctx.fill();
    }
    const floor=ctx.createLinearGradient(0,g,0,H);
    floor.addColorStop(0,"#1a2118"); floor.addColorStop(0.5,"#111710"); floor.addColorStop(1,"#080b07");
    ctx.fillStyle=floor; ctx.fillRect(0,g,W,H-g);
    ctx.save(); ctx.globalCompositeOperation="lighter";
    const gg=ctx.createLinearGradient(0,g-54*DPR,0,g+8*DPR);
    gg.addColorStop(0,"rgba(60,200,90,0)"); gg.addColorStop(1,"rgba(70,220,110,.20)");
    ctx.fillStyle=gg; ctx.fillRect(0,g-54*DPR,W,62*DPR);
    for(let i=0;i<6;i++){
      const wx=(i*W/5 + t*(6+hashS(i)*10))%(W+60*DPR)-30*DPR;
      const wy=g-(8+hashY(i)*40)*DPR + Math.sin(t*0.9+i)*10*DPR;
      const wr=Math.max(1,(3+hashS(i+2)*4)*DPR);
      ctx.globalAlpha=0.2+0.2*Math.sin(t*1.5+i*2);
      ctx.fillStyle="#8dffa0"; ctx.beginPath(); ctx.arc(wx,wy,wr,0,7); ctx.fill();
    }
    ctx.globalAlpha=1; ctx.restore();
    ctx.strokeStyle="#080d08"; ctx.lineCap="round";
    const drawTree=(bx,scale)=>{
      const rec=(x,y,ang,len,w,seed)=>{
        if(len<7*DPR||w<0.6) return;
        const sway=Math.sin(t*1.1+seed)*0.05;
        const ex=x+Math.cos(ang+sway)*len, ey=y+Math.sin(ang+sway)*len;
        ctx.lineWidth=Math.max(1,w);
        const cxm=x+Math.cos(ang)*len*0.5+Math.sin(seed)*len*0.14, cym=y+Math.sin(ang)*len*0.5;
        ctx.beginPath(); ctx.moveTo(x,y); ctx.quadraticCurveTo(cxm,cym,ex,ey); ctx.stroke();
        rec(ex,ey,ang-0.52+hashS(seed)*0.24,len*0.72,w*0.66,seed+1.3);
        rec(ex,ey,ang+0.5-hashY(seed)*0.24,len*0.72,w*0.66,seed+2.7);
        if(hashS(seed+5)>0.4) rec(ex,ey,ang-0.08,len*0.58,w*0.5,seed+4.1);
      };
      rec(bx,g,-Math.PI/2,56*DPR*scale,Math.max(1,7*DPR*scale),bx*0.011);
    };
    drawTree(W*0.15,1.0); drawTree(W*0.45,0.66); drawTree(W*0.9,0.82);
    const drawGhoul=(fx,sc)=>{
      const sway=Math.sin(t*1.5+fx*0.01)*3*DPR, fy=g-4*DPR;
      ctx.fillStyle="#070c08";
      ctx.beginPath(); ctx.ellipse(fx+sway,fy-16*DPR*sc,Math.max(1,6*DPR*sc),Math.max(1,16*DPR*sc),0.2,0,7); ctx.fill();
      ctx.beginPath(); ctx.arc(fx+sway+3*DPR*sc,fy-30*DPR*sc,Math.max(1,5*DPR*sc),0,7); ctx.fill();
      ctx.strokeStyle="#070c08"; ctx.lineWidth=Math.max(1,3*DPR*sc);
      ctx.beginPath(); ctx.moveTo(fx+sway,fy-24*DPR*sc); ctx.lineTo(fx+sway+9*DPR*sc+Math.sin(t*2+fx)*2*DPR,fy-7*DPR*sc); ctx.stroke();
      ctx.beginPath(); ctx.moveTo(fx+sway,fy-3*DPR*sc); ctx.lineTo(fx+sway-3*DPR*sc,fy);
      ctx.moveTo(fx+sway+2*DPR*sc,fy-3*DPR*sc); ctx.lineTo(fx+sway+5*DPR*sc,fy); ctx.stroke();
    };
    drawGhoul(W*0.32,1.0); drawGhoul(W*0.61,0.8);
    for(let r=0;r<3;r++){
      const rowY=g-(2-r)*11*DPR, n=5+r*2, shade=28+r*20;
      for(let i=0;i<n;i++){
        const seed=r*10+i;
        const sx=((i+0.5)/n)*W + (hashS(seed)-0.5)*W*0.06;
        const sc=(0.55+r*0.28)*(0.8+hashY(seed)*0.5);
        const tw=Math.max(1,22*DPR*sc), th=Math.max(1,34*DPR*sc);
        const col="rgb("+shade+","+(shade+9)+","+(shade+5)+")";
        ctx.save(); ctx.translate(sx,rowY); ctx.rotate((hashS(seed+3)-0.5)*0.28);
        ctx.fillStyle=col;
        if(hashY(seed+7)>0.62){
          ctx.fillRect(-tw*0.14,-th,tw*0.28,th+18*DPR);
          ctx.fillRect(-tw*0.5,-th*0.66,tw,th*0.24);
        } else {
          rr(ctx,-tw/2,-th,tw,th+18*DPR,tw*0.5); ctx.fill();
          ctx.fillStyle="rgba(0,0,0,.28)"; ctx.fillRect(-tw*0.12,-th*0.72,Math.max(1,tw*0.06),th*0.5);
          if(r===2){ ctx.fillStyle="rgba(150,200,160,.12)"; ctx.fillRect(-tw*0.42,-th*0.9,Math.max(1,tw*0.08),th*0.7); }
        }
        ctx.restore();
      }
    }
    ctx.save(); ctx.globalCompositeOperation="lighter";
    for(let i=0;i<7;i++){
      const px=(i*W/6 + t*(10+hashS(i)*14))%(W+260*DPR)-130*DPR;
      const py=g-(2+hashY(i)*24)*DPR + Math.sin(t*0.7+i)*6*DPR;
      const pr=Math.max(1,(42+hashS(i+2)*64)*DPR);
      const fg=ctx.createRadialGradient(px,py,1*DPR,px,py,pr);
      fg.addColorStop(0,"rgba(150,180,162,.13)"); fg.addColorStop(1,"rgba(150,180,162,0)");
      ctx.fillStyle=fg; ctx.beginPath(); ctx.arc(px,py,pr,0,7); ctx.fill();
    }
    ctx.restore();
    ctx.globalAlpha=1; ctx.globalCompositeOperation="source-over";
  } else if(type==="gorilla"){
    // ===== GORILLA WARFARE — moon / deep-space arena (Cosmic Silverback) =====
    const sky=ctx.createLinearGradient(0,0,0,g);
    sky.addColorStop(0.00,"#05060f"); sky.addColorStop(0.55,"#0b1030"); sky.addColorStop(1.00,"#171b3c");
    ctx.fillStyle=sky; ctx.fillRect(0,0,W,g);
    // nebula wisps (soft colour, kept dim so gameplay stays readable)
    ctx.save(); ctx.globalCompositeOperation="lighter";
    for(let i=0;i<4;i++){ const nx=W*(0.18+i*0.24)+Math.sin(t*0.14+i)*20*DPR, ny=g*(0.18+hashY(i)*0.34), nr=Math.max(1,(150+hashS(i)*130)*DPR);
      const ng=ctx.createRadialGradient(nx,ny,1*DPR,nx,ny,nr); const col=i%2?"92,72,200":"200,58,92";
      ng.addColorStop(0,"rgba("+col+",.10)"); ng.addColorStop(1,"rgba("+col+",0)");
      ctx.fillStyle=ng; ctx.beginPath(); ctx.arc(nx,ny,nr,0,7); ctx.fill(); }
    ctx.restore();
    // starfield (deterministic + twinkle)
    for(let i=0;i<74;i++){ const sx=hashS(i)*W, sy=hashY(i)*g*0.94, big=hashY(i+3)>0.86?2:1;
      ctx.globalAlpha=(0.45+hashS(i+7)*0.55)*(0.5+0.5*Math.abs(Math.sin(t*1.4+i*1.3)));
      ctx.fillStyle=i%9?"#dfe6ff":"#ffdcc4"; ctx.fillRect(sx,sy,Math.max(1,big*DPR),Math.max(1,big*DPR)); }
    ctx.globalAlpha=1;
    // EARTH hanging in the sky
    const Ex=W*0.78, Ey=g*0.24, Er=Math.max(1,Math.min(W,g)*0.13);
    ctx.save(); ctx.globalCompositeOperation="lighter";
    const halo=ctx.createRadialGradient(Ex,Ey,Er*0.7,Ex,Ey,Er*1.9);
    halo.addColorStop(0,"rgba(90,150,255,.4)"); halo.addColorStop(1,"rgba(90,150,255,0)");
    ctx.fillStyle=halo; ctx.beginPath(); ctx.arc(Ex,Ey,Er*1.9,0,7); ctx.fill(); ctx.restore();
    const ed=ctx.createRadialGradient(Ex-Er*0.35,Ey-Er*0.35,Er*0.2,Ex,Ey,Er);
    ed.addColorStop(0,"#5aa8ff"); ed.addColorStop(0.68,"#2f6fd6"); ed.addColorStop(1,"#0f3268");
    ctx.fillStyle=ed; ctx.beginPath(); ctx.arc(Ex,Ey,Er,0,7); ctx.fill();
    ctx.save(); ctx.beginPath(); ctx.arc(Ex,Ey,Er,0,7); ctx.clip();
    ctx.fillStyle="#3c8a4e";
    for(let i=0;i<5;i++){ const a=hashY(i+1)*7, rd=hashS(i+2)*Er*0.66, cx=Ex+Math.cos(a)*rd, cy=Ey+Math.sin(a)*rd;
      ctx.beginPath(); ctx.ellipse(cx,cy,Math.max(1,Er*(0.16+hashS(i)*0.2)),Math.max(1,Er*(0.11+hashY(i)*0.15)),hashY(i)*3,0,7); ctx.fill(); }
    ctx.fillStyle="rgba(255,255,255,.20)";
    for(let i=0;i<4;i++){ const cx=Ex-Er+((t*6+i*Er*0.9)%(Er*2.4)), cy=Ey+(hashY(i)-0.5)*Er*1.2;
      ctx.beginPath(); ctx.ellipse(cx,cy,Math.max(1,Er*0.42),Math.max(1,Er*0.12),0,0,7); ctx.fill(); }
    ctx.fillStyle="rgba(6,10,26,.42)"; ctx.beginPath(); ctx.arc(Ex+Er*0.42,Ey+Er*0.2,Er*1.02,0,7); ctx.fill();
    ctx.restore();
    // distant grey moon ridges (horizon)
    for(let ridge=0;ridge<2;ridge++){
      const baseY=g*(0.66+ridge*0.08), amp=g*(0.1-ridge*0.03);
      ctx.beginPath(); ctx.moveTo(0,g);
      for(let x=0;x<=W;x+=Math.max(1,W/12)){ const k=x/W*12;
        const y=baseY - amp*(0.55+0.45*Math.sin(k*1.3+ridge*3)) - amp*0.3*Math.sin(k*2.7+ridge); ctx.lineTo(x,y); }
      ctx.lineTo(W,g); ctx.closePath();
      ctx.fillStyle=ridge?"#2b2c3a":"#212230"; ctx.fill();
    }
    // planted Soviet flag on the horizon (nod to the cosmonaut)
    const Fx=W*0.13, Fy=g*0.63;
    ctx.strokeStyle="#c6ccda"; ctx.lineWidth=Math.max(1,2.4*DPR); ctx.lineCap="round";
    ctx.beginPath(); ctx.moveTo(Fx,Fy); ctx.lineTo(Fx,Fy-46*DPR); ctx.stroke();
    ctx.fillStyle="#c81026"; ctx.beginPath(); ctx.moveTo(Fx,Fy-46*DPR); ctx.lineTo(Fx+30*DPR,Fy-40*DPR); ctx.lineTo(Fx,Fy-32*DPR); ctx.closePath(); ctx.fill();
    ctx.fillStyle="#e8c23a"; ctx.beginPath(); ctx.arc(Fx+9*DPR,Fy-39*DPR,Math.max(1,2.4*DPR),0,7); ctx.fill();
    // moon-surface floor + craters
    const floor=ctx.createLinearGradient(0,g,0,H);
    floor.addColorStop(0,"#4a4b58"); floor.addColorStop(0.5,"#2e2f3a"); floor.addColorStop(1,"#16171f");
    ctx.fillStyle=floor; ctx.fillRect(0,g,W,H-g);
    ctx.fillStyle="rgba(190,198,225,.16)"; ctx.fillRect(0,g,W,Math.max(1,3*DPR));
    for(let i=0;i<8;i++){ const rx=hashS(i+1)*W, ry=g+12*DPR+hashY(i+1)*(H-g)*0.55, rw=Math.max(1,(11+hashS(i+6)*26)*DPR);
      ctx.fillStyle="#20212b"; ctx.beginPath(); ctx.ellipse(rx,ry,rw,Math.max(1,rw*0.42),0,0,7); ctx.fill();
      ctx.fillStyle="rgba(255,255,255,.07)"; ctx.beginPath(); ctx.ellipse(rx-rw*0.28,ry-rw*0.2,Math.max(1,rw*0.55),Math.max(1,rw*0.2),0,0,7); ctx.fill(); }
    // drifting space dust
    ctx.save(); ctx.globalCompositeOperation="lighter";
    for(let i=0;i<10;i++){ const dx=(hashS(i)*W + t*(5+(i%3)*5))%W, dy=hashY(i)*g*0.85+Math.sin(t*0.9+i)*8*DPR;
      ctx.globalAlpha=0.2+0.3*Math.abs(Math.sin(t*1.6+i)); ctx.fillStyle="#aeb8e0";
      ctx.beginPath(); ctx.arc(dx,dy,Math.max(1,(1+hashS(i+2)*1.6)*DPR),0,7); ctx.fill(); }
    ctx.globalAlpha=1; ctx.restore();
    ctx.globalAlpha=1; ctx.globalCompositeOperation="source-over";
  } else {
    // ===== DEEP-OCEAN GAUNTLET =====
    const grd=ctx.createLinearGradient(0,0,0,H);
    grd.addColorStop(0.00,"#0f6f86"); grd.addColorStop(0.22,"#0a4f6e");
    grd.addColorStop(0.55,"#062f47"); grd.addColorStop(0.82,"#04182a");
    grd.addColorStop(1.00,"#020a15");
    ctx.fillStyle=grd; ctx.fillRect(0,0,W,H);
    ctx.save(); ctx.globalCompositeOperation="lighter";
    const surf=ctx.createLinearGradient(0,0,0,Math.max(1,H*0.16));
    surf.addColorStop(0,"rgba(120,235,255,0.22)"); surf.addColorStop(1,"rgba(120,235,255,0)");
    ctx.fillStyle=surf; ctx.fillRect(0,0,W,H*0.16); ctx.restore();
    ctx.save(); ctx.globalCompositeOperation="lighter";
    const nR=6, slant=W*0.14;
    for(let i=0;i<nR;i++){
      const bx=((i+0.5)/nR)*W + (hashS(i*2+1)-0.5)*W*0.14 + Math.sin(t*0.25+i)*22*DPR;
      const w=Math.max(1,(60+hashY(i*3+2)*90)*DPR);
      const a=0.05+0.05*(0.5+0.5*Math.sin(t*0.6+i*1.7));
      const rg=ctx.createLinearGradient(bx,0,bx-slant,H);
      rg.addColorStop(0,"rgba(150,240,255,"+a.toFixed(3)+")");
      rg.addColorStop(0.6,"rgba(90,200,235,"+(a*0.4).toFixed(3)+")");
      rg.addColorStop(1,"rgba(60,150,200,0)");
      ctx.fillStyle=rg;
      ctx.beginPath();
      ctx.moveTo(bx-w*0.5,0); ctx.lineTo(bx+w*0.5,0);
      ctx.lineTo(bx-slant+w*0.9,H); ctx.lineTo(bx-slant-w*0.9,H);
      ctx.closePath(); ctx.fill();
    }
    ctx.restore();
    ctx.save(); ctx.globalAlpha=0.5; ctx.fillStyle="rgba(6,26,40,0.55)";
    for(let s=0;s<3;s++){
      const speed=(10+s*6)*DPR, dir=(s%2?-1:1), span=W+220*DPR;
      const cx=(((t*speed*dir + s*W*0.4)%span)+span)%span - 110*DPR;
      const cy=H*(0.30+0.16*s)+Math.sin(t*0.5+s)*10*DPR;
      for(let f=0;f<7;f++){
        const fx=cx+Math.cos(f*1.3)*(30+f*7)*DPR*dir, fy=cy+Math.sin(f*2.1)*14*DPR, fs=(3+hashY(f+s*7)*2)*DPR;
        ctx.save(); ctx.translate(fx,fy); ctx.scale(dir,1);
        ctx.beginPath(); ctx.ellipse(0,0,Math.max(1,fs*1.6),Math.max(1,fs*0.7),0,0,7);
        ctx.moveTo(-fs*1.6,0); ctx.lineTo(-fs*2.4,-fs*0.7); ctx.lineTo(-fs*2.4,fs*0.7); ctx.closePath();
        ctx.fill(); ctx.restore();
      }
    }
    ctx.globalAlpha=1; ctx.restore();
    const floor=ctx.createLinearGradient(0,g-20*DPR,0,H);
    floor.addColorStop(0,"rgba(4,26,38,0)"); floor.addColorStop(0.4,"rgba(3,20,32,0.6)");
    floor.addColorStop(1,"rgba(2,10,18,0.95)");
    ctx.fillStyle=floor; ctx.fillRect(0,g-20*DPR,W,H-(g-20*DPR));
    ctx.save(); ctx.globalCompositeOperation="lighter";
    ctx.strokeStyle="rgba(90,200,225,0.10)"; ctx.lineWidth=Math.max(1,2*DPR);
    for(let r=0;r<4;r++){
      const y=g+(r+0.5)*((H-g)/4);
      ctx.beginPath();
      for(let x=0;x<=W;x+=14*DPR){ ctx.lineTo(x, y + Math.sin(x*0.05+t*1.5+r)*3*DPR + Math.sin(x*0.13-t*2+r*2)*2*DPR); }
      ctx.stroke();
    }
    ctx.restore();
    ctx.save();
    const kc=14, kgap=W/kc;
    for(let i=0;i<=kc;i++){
      const rx=i*kgap + (hashS(i*5+3)-0.5)*kgap*0.6;
      const hh=(90+hashY(i*3+1)*160)*DPR, isCoral=hashS(i*7+2)>0.62;
      ctx.fillStyle=isCoral?"rgba(10,40,52,0.85)":"rgba(6,30,40,0.8)";
      if(isCoral){
        ctx.save(); ctx.translate(rx,g+6*DPR); ctx.beginPath(); ctx.moveTo(0,0);
        for(let b=-2;b<=2;b++){ const ba=b*0.32+Math.sin(t*0.6+i)*0.05, bl=hh*0.5*(0.7+0.3*hashY(i*9+b+3));
          ctx.lineTo(Math.sin(ba)*bl,-Math.cos(ba)*bl); ctx.lineTo(Math.sin(ba+0.14)*bl*0.5,-Math.cos(ba+0.14)*bl*0.5); }
        ctx.closePath(); ctx.fill(); ctx.restore();
      } else {
        const sway=Math.sin(t*0.9+i*0.7)*18*DPR, w=Math.max(2,(6+hashY(i*11)*6)*DPR);
        ctx.beginPath(); ctx.moveTo(rx-w*0.5,g+6*DPR);
        ctx.quadraticCurveTo(rx-w*0.5+sway*0.5,g-hh*0.5,rx+sway,g-hh);
        ctx.quadraticCurveTo(rx+w*0.5+sway*0.5,g-hh*0.5,rx+w*0.5,g+6*DPR);
        ctx.closePath(); ctx.fill();
      }
    }
    ctx.restore();
    ctx.save(); ctx.globalCompositeOperation="lighter"; ctx.fillStyle="rgba(180,240,255,0.9)";
    for(let i=0;i<40;i++){
      const sp=(24+hashY(i*3+1)*40)*DPR, cyc=(t*sp/Math.max(1,H)+hashY(i*5))%1;
      const y=H-cyc*H, x=hashS(i*1.7+2)*W + Math.sin(t*1.4+i*1.3+cyc*6)*10*DPR;
      const r=Math.max(1,(1.2+hashS(i+7)*2.4)*DPR);
      ctx.globalAlpha=0.14+0.16*(1-cyc);
      ctx.beginPath(); ctx.arc(x,y,r,0,7); ctx.fill();
    }
    ctx.globalAlpha=1; ctx.restore();
    ctx.save();
    const vg=ctx.createRadialGradient(W*0.5,H*0.42,Math.max(1,H*0.2),W*0.5,H*0.5,Math.max(1,H*0.85));
    vg.addColorStop(0,"rgba(0,0,0,0)"); vg.addColorStop(1,"rgba(0,4,10,0.55)");
    ctx.fillStyle=vg; ctx.fillRect(0,0,W,H); ctx.restore();
    ctx.globalAlpha=1; ctx.globalCompositeOperation="source-over";
  }
}
function drawFlameColumn(h,t){
  const g=GROUND();
  if(h.t<h.warn){
    const prog=Math.max(0,Math.min(1,h.t/Math.max(0.0001,h.warn)));
    const pulse=0.5+0.5*Math.sin(t*20);
    const rw=Math.max(1,h.w*0.65);
    ctx.save(); ctx.globalCompositeOperation="lighter";
    const gl=ctx.createRadialGradient(h.x,g,2*DPR,h.x,g,Math.max(1,h.w*1.15));
    gl.addColorStop(0,"rgba(255,60,40,"+(0.4+0.4*pulse)+")");
    gl.addColorStop(0.5,"rgba(255,40,20,"+(0.16+0.14*pulse)+")");
    gl.addColorStop(1,"rgba(255,20,10,0)");
    ctx.fillStyle=gl; ctx.beginPath(); ctx.ellipse(h.x,g,Math.max(1,h.w*1.15),Math.max(1,14*DPR),0,0,7); ctx.fill();
    ctx.restore();
    ctx.save();
    ctx.globalAlpha=0.5+0.45*pulse;
    ctx.strokeStyle="#ff2d3f"; ctx.lineWidth=Math.max(1,3*DPR);
    ctx.beginPath(); ctx.ellipse(h.x,g-2*DPR,rw,Math.max(1,7*DPR),0,0,7); ctx.stroke();
    const rr=Math.max(1, h.w*(1.2-prog)*0.9 + 6*DPR);
    ctx.globalAlpha=0.3+0.4*(1-prog);
    ctx.setLineDash([6*DPR,5*DPR]); ctx.lineWidth=Math.max(1,2.4*DPR);
    ctx.beginPath(); ctx.ellipse(h.x,g-2*DPR,rr,Math.max(1,rr*0.32),0,0,7); ctx.stroke();
    ctx.setLineDash([]);
    ctx.restore();
    ctx.save(); ctx.globalAlpha=0.55+0.4*pulse; ctx.fillStyle="#ffd24a";
    for(let k=0;k<3;k++){
      const cy=g-(10+k*9)*DPR - pulse*4*DPR, cw=Math.max(1,h.w*0.28-k*2*DPR);
      ctx.beginPath();
      ctx.moveTo(h.x-cw,cy+5*DPR); ctx.lineTo(h.x,cy); ctx.lineTo(h.x+cw,cy+5*DPR);
      ctx.lineTo(h.x+cw,cy+9*DPR); ctx.lineTo(h.x,cy+4*DPR); ctx.lineTo(h.x-cw,cy+9*DPR);
      ctx.closePath(); ctx.fill();
    }
    ctx.restore();
    ctx.save(); ctx.globalCompositeOperation="lighter"; ctx.globalAlpha=0.1+0.12*pulse;
    const pf=ctx.createLinearGradient(0,g,0,g-42*DPR);
    pf.addColorStop(0,"rgba(255,120,30,.9)"); pf.addColorStop(1,"rgba(255,120,30,0)");
    ctx.fillStyle=pf; ctx.fillRect(h.x-h.w*0.2, g-42*DPR, Math.max(1,h.w*0.4), 42*DPR);
    ctx.restore();
    ctx.globalAlpha=1; ctx.globalCompositeOperation="source-over";
    return;
  }
  const rise=Math.min(1,(h.t-h.warn)/0.18);
  const hh=h.h*rise, top=g-hh, H2=Math.max(1,hh);
  ctx.save(); ctx.globalCompositeOperation="lighter";
  const base=ctx.createRadialGradient(h.x,g,2*DPR,h.x,g,Math.max(1,h.w*1.4));
  base.addColorStop(0,"rgba(255,165,45,.75)"); base.addColorStop(0.5,"rgba(255,80,20,.3)"); base.addColorStop(1,"rgba(255,60,10,0)");
  ctx.fillStyle=base; ctx.beginPath(); ctx.ellipse(h.x,g,Math.max(1,h.w*1.4),Math.max(1,20*DPR),0,0,7); ctx.fill();
  const halo=ctx.createLinearGradient(0,g,0,top);
  halo.addColorStop(0,"rgba(255,90,20,.45)"); halo.addColorStop(1,"rgba(255,90,20,0)");
  ctx.fillStyle=halo; ctx.fillRect(h.x-h.w*0.9, top, Math.max(1,h.w*1.8), hh);
  ctx.restore();
  ctx.save();
  const layers=[["#ff2a18",1.0],["#ff6a12",0.74],["#ffb52e",0.48],["#ffe98a",0.26],["#fffdf0",0.12]];
  for(const [col,sc] of layers){
    ctx.fillStyle=col;
    const w=Math.max(1,h.w*sc);
    ctx.beginPath(); ctx.moveTo(h.x-w/2, g);
    for(let y=0;y<=hh;y+=8*DPR){ const fy=g-y, k=y/H2, wob=Math.sin(y*0.045 - t*15 + h.x)*(w*0.22)*k; ctx.lineTo(h.x-w/2+wob, fy); }
    ctx.lineTo(h.x + Math.sin(t*12+h.x)*w*0.12, top - 5*DPR*sc);
    for(let y=hh;y>=0;y-=8*DPR){ const fy=g-y, k=y/H2, wob=Math.sin(y*0.045 - t*15 + h.x+1.7)*(w*0.22)*k; ctx.lineTo(h.x+w/2+wob, fy); }
    ctx.closePath(); ctx.fill();
  }
  ctx.restore();
  ctx.save(); ctx.globalCompositeOperation="lighter"; ctx.globalAlpha=0.55*rise;
  for(let k=0;k<3;k++){
    const tx=h.x+(k-1)*h.w*0.22, th=(9+8*Math.abs(Math.sin(t*9+k*2+h.x)))*DPR;
    const tg=ctx.createLinearGradient(0,top,0,top-th);
    tg.addColorStop(0,"rgba(255,220,120,.85)"); tg.addColorStop(1,"rgba(255,160,40,0)");
    ctx.fillStyle=tg;
    ctx.beginPath(); ctx.moveTo(tx-h.w*0.13,top);
    ctx.quadraticCurveTo(tx, top-th, tx+Math.sin(t*7+k)*h.w*0.1, top-th);
    ctx.quadraticCurveTo(tx, top-th*0.4, tx+h.w*0.13, top);
    ctx.closePath(); ctx.fill();
  }
  ctx.globalAlpha=1; ctx.restore();
  ctx.save(); ctx.globalCompositeOperation="lighter";
  for(let i=0;i<10;i++){
    const cyc=(t*0.9 + hashY(i+1) + (h.x%97)*0.01)%1;
    const ey=g - cyc*hh - 4*DPR;
    const ex=h.x + Math.sin(t*3 + i*1.3 + h.x)*(h.w*0.35);
    const r=Math.max(1,(1.2+hashS(i+1)*1.8)*DPR*(1-cyc*0.5));
    ctx.globalAlpha=(1-cyc)*0.8*rise;
    ctx.fillStyle=i%2?"#ffd27a":"#ff7a1a";
    ctx.beginPath(); ctx.arc(ex,ey,r,0,7); ctx.fill();
  }
  ctx.globalAlpha=1; ctx.restore();
  ctx.globalAlpha=1; ctx.globalCompositeOperation="source-over";
}
function drawZombieHand(h,t){
  const g=GROUND();
  const cx=h.x;
  const sd=(Math.abs(cx)*0.013)%1;
  if(h.t<h.warn){
    const prog=Math.max(0,Math.min(1,h.t/Math.max(0.0001,h.warn)));
    const pulse=0.5+0.5*Math.sin(t*18);
    ctx.save(); ctx.globalCompositeOperation="lighter";
    const gl=ctx.createRadialGradient(cx,g,2*DPR,cx,g,Math.max(1,h.w*1.1));
    gl.addColorStop(0,"rgba(120,240,110,"+(0.25+0.3*pulse)+")");
    gl.addColorStop(0.5,"rgba(80,200,90,"+(0.10+0.12*pulse)+")");
    gl.addColorStop(1,"rgba(60,180,80,0)");
    ctx.fillStyle=gl; ctx.beginPath(); ctx.ellipse(cx,g,Math.max(1,h.w*1.1),Math.max(1,13*DPR),0,0,7); ctx.fill();
    ctx.restore();
    const mh=Math.max(1,(4+prog*10)*DPR), mw=Math.max(1,h.w*0.55);
    const mgr=ctx.createLinearGradient(0,g-mh,0,g);
    mgr.addColorStop(0,"#4a3a26"); mgr.addColorStop(1,"#241a10");
    ctx.fillStyle=mgr;
    ctx.beginPath(); ctx.moveTo(cx-mw,g);
    for(let x=-mw;x<=mw;x+=6*DPR){ const by=g-mh*Math.cos((x/mw)*1.3)*(0.7+0.3*Math.sin(x*0.1+t*3)); ctx.lineTo(cx+x,by); }
    ctx.lineTo(cx+mw,g); ctx.closePath(); ctx.fill();
    ctx.save();
    ctx.strokeStyle="rgba(18,11,5,"+(0.5+0.3*prog)+")"; ctx.lineWidth=Math.max(1,2*DPR); ctx.lineCap="round";
    for(let k=0;k<5;k++){
      const a=(k/5)*Math.PI-Math.PI+(hashS(k+sd)-0.5)*0.4;
      const cl=h.w*(0.2+0.35*prog)*(0.6+hashY(k+sd)*0.6);
      ctx.beginPath(); ctx.moveTo(cx,g-2*DPR);
      ctx.lineTo(cx+Math.cos(a)*cl*0.5+(hashY(k)-0.5)*6*DPR,g-2*DPR);
      ctx.lineTo(cx+Math.cos(a)*cl,g-2*DPR-Math.abs(Math.sin(a))*cl*0.28); ctx.stroke();
    }
    ctx.restore();
    const poke=prog*8*DPR;
    for(let f=0;f<4;f++){
      const fx=cx+(f-1.5)*(h.w*0.20);
      const fhh=Math.max(1,poke*(0.5+0.5*Math.sin(f*1.4+sd*6))+2*DPR);
      const fww=Math.max(1,h.w*0.06);
      ctx.fillStyle="#5c6b48"; ctx.beginPath(); ctx.ellipse(fx,g-fhh*0.5,fww,fhh,0,0,7); ctx.fill();
      ctx.fillStyle="#2a3320"; ctx.beginPath(); ctx.arc(fx,g-fhh,Math.max(1,fww*0.5),0,7); ctx.fill();
    }
    ctx.save();
    ctx.globalAlpha=0.5+0.4*pulse; ctx.strokeStyle="#7dff5a"; ctx.lineWidth=Math.max(1,3*DPR);
    ctx.beginPath(); ctx.ellipse(cx,g-2*DPR,Math.max(1,h.w*0.6),Math.max(1,7*DPR),0,0,7); ctx.stroke();
    const rrad=Math.max(1,h.w*(1.2-prog)*0.9+6*DPR);
    ctx.globalAlpha=0.3+0.4*(1-prog);
    ctx.setLineDash([6*DPR,5*DPR]); ctx.lineWidth=Math.max(1,2.4*DPR);
    ctx.beginPath(); ctx.ellipse(cx,g-2*DPR,rrad,Math.max(1,rrad*0.32),0,0,7); ctx.stroke();
    ctx.setLineDash([]);
    ctx.restore();
    ctx.globalAlpha=1; ctx.globalCompositeOperation="source-over";
    return;
  }
  const dur=(h.dur||1.1);
  const life=h.t-h.warn;
  const rise=Math.min(1,life/0.18);
  const retract=1-Math.max(0,(life-(dur-0.22))/0.22);
  const grip=Math.max(0,rise*Math.min(1,Math.max(0,retract)));
  const hh=h.h*grip;
  const H2=Math.max(1,hh), top=g-hh;
  ctx.save(); ctx.globalCompositeOperation="lighter";
  const bg=ctx.createRadialGradient(cx,g,2*DPR,cx,g,Math.max(1,h.w*1.3));
  bg.addColorStop(0,"rgba(120,240,110,.5)"); bg.addColorStop(0.5,"rgba(70,190,90,.2)"); bg.addColorStop(1,"rgba(60,180,80,0)");
  ctx.fillStyle=bg; ctx.beginPath(); ctx.ellipse(cx,g,Math.max(1,h.w*1.3),Math.max(1,18*DPR),0,0,7); ctx.fill();
  ctx.restore();
  ctx.fillStyle="#2c2013";
  ctx.beginPath(); ctx.moveTo(cx-h.w*0.6,g);
  for(let x=-h.w*0.6;x<=h.w*0.6;x+=6*DPR){ ctx.lineTo(cx+x,g-Math.max(1,8*DPR)*Math.cos((x/(h.w*0.6))*1.3)); }
  ctx.lineTo(cx+h.w*0.6,g); ctx.closePath(); ctx.fill();
  ctx.save();
  for(let i=0;i<8;i++){
    const cyc=(life*1.4+hashY(i+sd))%1;
    const dx=cx+Math.sin(i*1.7+sd*6)*h.w*0.5*cyc, dy=g-cyc*hh*0.6-cyc*10*DPR;
    ctx.globalAlpha=(1-cyc)*0.7*rise; ctx.fillStyle=i%2?"#3a2c1a":"#4a3a26";
    ctx.beginPath(); ctx.arc(dx,dy,Math.max(1,(1.4+hashS(i)*2)*DPR),0,7); ctx.fill();
  }
  ctx.globalAlpha=1; ctx.restore();
  ctx.save();
  ctx.translate(cx,g); ctx.rotate(Math.sin(t*1.6+sd*6)*0.08*grip);
  const fw=Math.max(1,h.w*0.30), baseHW=Math.max(1,fw*0.62), wristHW=Math.max(1,fw*0.5);
  const palmR=Math.max(1,h.w*0.22);
  const palmY=-Math.max(0,hh-palmR);
  const skin=ctx.createLinearGradient(-fw,0,fw,0);
  skin.addColorStop(0,"#39422b"); skin.addColorStop(0.35,"#6b7a52"); skin.addColorStop(0.6,"#8a9a6b"); skin.addColorStop(1,"#39422b");
  ctx.fillStyle=skin;
  ctx.beginPath();
  ctx.moveTo(-baseHW,0);
  ctx.quadraticCurveTo(-wristHW-4*DPR,palmY*0.5,-wristHW,palmY);
  ctx.lineTo(wristHW,palmY);
  ctx.quadraticCurveTo(wristHW+4*DPR,palmY*0.5,baseHW,0);
  ctx.closePath(); ctx.fill();
  ctx.strokeStyle="rgba(230,228,210,.5)"; ctx.lineWidth=Math.max(1,2*DPR); ctx.lineCap="round";
  ctx.beginPath(); ctx.moveTo(-wristHW*0.3,palmY*0.25); ctx.lineTo(-wristHW*0.1,palmY*0.8); ctx.stroke();
  ctx.fillStyle="rgba(38,28,16,.6)";
  for(let i=0;i<4;i++){ const dy=palmY*(0.2+hashY(i+sd)*0.7), dxp=(hashS(i+sd)-0.5)*fw;
    ctx.beginPath(); ctx.arc(dxp,dy,Math.max(1,(2+hashS(i)*3)*DPR),0,7); ctx.fill(); }
  const slh=Math.max(1,Math.min(-palmY*0.45,h.w*0.6));
  ctx.fillStyle="#2a2622";
  ctx.beginPath(); ctx.moveTo(-baseHW-3*DPR,0); ctx.lineTo(-baseHW-2*DPR,-slh);
  ctx.lineTo(-baseHW*0.4,-slh-6*DPR); ctx.lineTo(0,-slh+2*DPR); ctx.lineTo(baseHW*0.5,-slh-7*DPR);
  ctx.lineTo(baseHW+2*DPR,-slh); ctx.lineTo(baseHW+3*DPR,0); ctx.closePath(); ctx.fill();
  ctx.fillStyle="#1c1916"; ctx.fillRect(-baseHW,-slh*0.5,Math.max(1,baseHW*2),Math.max(1,3*DPR));
  ctx.fillStyle="#7a8a5c"; ctx.beginPath(); ctx.ellipse(0,palmY,palmR,Math.max(1,palmR*0.82),0,0,7); ctx.fill();
  ctx.fillStyle="#5c6b48"; ctx.beginPath(); ctx.ellipse(0,palmY+palmR*0.2,Math.max(1,palmR*0.9),Math.max(1,palmR*0.5),0,0,7); ctx.fill();
  ctx.lineCap="round"; ctx.lineJoin="round";
  const spread=h.w*0.5, nF=4;
  for(let f=0;f<nF;f++){
    const fr=(f/(nF-1))-0.5;
    const baseX=fr*palmR;
    const tipXbase=fr*spread;
    const curl=0.5+0.5*Math.sin(t*7+f*1.5+sd*6);
    const twx=Math.sin(t*9+f*2+sd*6)*h.w*0.04*grip;
    const tipY=-hh*(0.90+0.08*Math.sin(t*5+f+sd*6));
    const tipX=tipXbase+twx-tipXbase*0.15*curl;
    const midX=(baseX+tipXbase)*0.5+tipXbase*0.14, midY=(palmY+tipY)*0.5;
    ctx.strokeStyle="#6b7a52"; ctx.lineWidth=Math.max(1,h.w*0.11*(1-f*0.02));
    ctx.beginPath(); ctx.moveTo(baseX,palmY); ctx.quadraticCurveTo(midX,midY,tipX,tipY); ctx.stroke();
    ctx.fillStyle="#e8e6d8";
    ctx.beginPath(); ctx.moveTo(tipX-2*DPR,tipY+4*DPR); ctx.lineTo(tipX+(fr>=0?3:-3)*DPR,tipY-5*DPR); ctx.lineTo(tipX+2*DPR,tipY+3*DPR); ctx.closePath(); ctx.fill();
  }
  ctx.strokeStyle="#6b7a52"; ctx.lineWidth=Math.max(1,h.w*0.12);
  ctx.beginPath(); ctx.moveTo(-palmR*0.6,palmY+palmR*0.2);
  ctx.quadraticCurveTo(-palmR*1.4,palmY,-palmR*1.2+Math.sin(t*8+sd*6)*4*DPR,palmY-palmR*0.9); ctx.stroke();
  ctx.fillStyle="#e8e6d8";
  ctx.beginPath(); const thx=-palmR*1.2+Math.sin(t*8+sd*6)*4*DPR, thy=palmY-palmR*0.9;
  ctx.moveTo(thx-3*DPR,thy+3*DPR); ctx.lineTo(thx-3*DPR,thy-4*DPR); ctx.lineTo(thx+2*DPR,thy-1*DPR); ctx.closePath(); ctx.fill();
  ctx.restore();
  ctx.globalAlpha=1; ctx.globalCompositeOperation="source-over";
}
function drawWraith(h,t){
  const r=Math.max(1,h.r);
  let ang=Math.atan2(h.vy||0,h.vx||0);
  if((Math.abs(h.vx||0)+Math.abs(h.vy||0))<1e-4) ang=Math.PI/2;
  const age=h.t||0;
  const ph=(h.x*0.017)%6.283;
  const bob=Math.sin(age*3+ph);
  const wob=Math.sin(age*4+ph), wob2=Math.sin(age*4+ph+2);
  const pulse=0.5+0.5*Math.sin(age*4.5+ph);
  ctx.save();
  ctx.translate(h.x,h.y);
  ctx.save(); ctx.globalCompositeOperation="lighter";
  const aur=ctx.createRadialGradient(0,0,r*0.2,0,0,Math.max(1,r*2.6));
  aur.addColorStop(0,"rgba(120,240,110,"+(0.22+0.10*pulse)+")");
  aur.addColorStop(0.45,"rgba(70,200,100,0.12)");
  aur.addColorStop(1,"rgba(50,170,80,0)");
  ctx.fillStyle=aur; ctx.beginPath(); ctx.arc(0,0,Math.max(1,r*2.6),0,7); ctx.fill();
  ctx.restore();
  ctx.rotate(ang);
  ctx.save(); ctx.globalCompositeOperation="lighter";
  ctx.strokeStyle="rgba(110,230,120,0.16)"; ctx.lineWidth=Math.max(1,1.4*DPR); ctx.lineCap="round";
  for(let i=0;i<3;i++){
    const yo=(i-1)*r*0.26, w1=Math.sin(age*5+i*2+ph), w2=Math.sin(age*4+i*3+ph);
    ctx.beginPath(); ctx.moveTo(-r*0.7,yo);
    ctx.quadraticCurveTo(-r*1.8,yo+w1*r*0.30,-r*3.1,yo+w2*r*0.55);
    ctx.stroke();
  }
  ctx.restore();
  const tipx=-r*(2.7+0.5*bob), tipy=r*0.22*wob2;
  ctx.beginPath();
  ctx.moveTo(r*1.05,0);
  ctx.quadraticCurveTo(r*0.70,-r*0.90, r*0.05,-r*0.90);
  ctx.quadraticCurveTo(-r*0.50,-r*0.90,-r*0.90,-r*0.58);
  ctx.quadraticCurveTo(-r*1.9,-r*(0.48+0.10*wob), tipx,tipy-r*0.15);
  ctx.quadraticCurveTo(-r*1.9, r*(0.48+0.10*wob2),-r*0.90, r*0.58);
  ctx.quadraticCurveTo(-r*0.50, r*0.90, r*0.05, r*0.90);
  ctx.quadraticCurveTo(r*0.70, r*0.90, r*1.05,0);
  ctx.closePath();
  const bodyG=ctx.createLinearGradient(r*1.0,0,tipx,0);
  bodyG.addColorStop(0,"rgba(24,46,30,0.82)");
  bodyG.addColorStop(0.5,"rgba(20,52,32,0.48)");
  bodyG.addColorStop(1,"rgba(30,80,45,0)");
  ctx.fillStyle=bodyG; ctx.fill();
  ctx.save(); ctx.globalCompositeOperation="lighter";
  ctx.strokeStyle="rgba(120,240,110,"+(0.22+0.16*pulse)+")"; ctx.lineWidth=Math.max(1,2*DPR);
  ctx.stroke();
  ctx.restore();
  ctx.fillStyle="rgba(4,10,6,0.86)";
  ctx.beginPath(); ctx.ellipse(r*0.45,0,Math.max(1,r*0.42),Math.max(1,r*0.52),0,0,7); ctx.fill();
  ctx.save(); ctx.globalCompositeOperation="lighter";
  for(let s=-1;s<=1;s+=2){
    const ex=r*0.60, ey=s*r*0.17;
    const eg=ctx.createRadialGradient(ex,ey,0,ex,ey,Math.max(1,r*0.42));
    eg.addColorStop(0,"rgba(200,255,155,"+(0.85+0.15*pulse)+")");
    eg.addColorStop(0.32,"rgba(120,240,110,0.75)");
    eg.addColorStop(1,"rgba(70,220,110,0)");
    ctx.fillStyle=eg; ctx.beginPath(); ctx.arc(ex,ey,Math.max(1,r*0.42),0,7); ctx.fill();
    ctx.fillStyle="#d6ffb0"; ctx.beginPath(); ctx.arc(ex,ey,Math.max(1,r*0.08),0,7); ctx.fill();
  }
  ctx.restore();
  ctx.save();
  ctx.strokeStyle="rgba(205,232,195,"+(0.50+0.22*pulse)+")"; ctx.lineWidth=Math.max(1,1.5*DPR); ctx.lineCap="round";
  const reach=r*(1.10+0.12*pulse);
  for(let s=-1;s<=1;s+=2){
    const rootx=r*0.12, rooty=s*r*0.60;
    for(let f=0;f<3;f++){
      const sp=(f-1), fx=reach, fy=s*r*0.26+sp*r*0.24;
      ctx.beginPath(); ctx.moveTo(rootx,rooty);
      ctx.quadraticCurveTo(r*0.70, s*r*0.46+sp*r*0.15, fx,fy);
      ctx.stroke();
      ctx.fillStyle="rgba(224,247,214,0.85)"; ctx.beginPath(); ctx.arc(fx,fy,Math.max(1,1.5*DPR),0,7); ctx.fill();
    }
  }
  ctx.restore();
  ctx.restore();
  ctx.globalAlpha=1; ctx.globalCompositeOperation="source-over";
}
function drawGorilla(gor,t){
  const u=Math.max(1,gor.r||1), facing=(gor.facing<0?-1:1);
  const st=gor.state||"walk", a=Math.max(0,Math.min(1,gor.anim||0));
  const R=(v)=>Math.max(1,v);
  const kf=(w,s)=>{ if(w<=s[0][0])return s[0][1];
    for(let i=1;i<s.length;i++){ if(w<=s[i][0]){ let z=(w-s[i-1][0])/Math.max(1e-4,s[i][0]-s[i-1][0]); z=z*z*(3-2*z);
      return s[i-1][1]+(s[i][1]-s[i-1][1])*z; } } return s[s.length-1][1]; };
  const L=(p,q,k)=>p+(q-p)*k;
  // ---- COSMIC SILVERBACK (Soviet cosmonaut, gorilla.jpeg) palette ----
  const suitHi="#c9cdd4", suit="#9aa1aa", suitSh="#6b727c", suitDk="#464b54";
  const fleshD="#2c2824", fleshM="#463f37", fleshHi="#5f554a", fleshDim="#322e29", fleshDimD="#211e18";
  const helmHi="#e7eaef", helmMid="#b7bcc5", helmDk="#787e88";
  const collar="#c3c9d3", collarDk="#6f7684";
  const furD="#211e22", furM="#332f36", skin="#4d4842", skinHi="#63594f";
  const fang="#efe9d6", gumC="#26121a", amber="#ffcf6a";
  const red="#ff3b30", redDk="#b3271f", cccpRed="#c81026", cccpYel="#e8c23a";
  const strapC="#2a2620";
  const outline="#0a0b0f";

  // ---- pose params ----
  let rear=0, surge=0, gait=0, lift=0, air=0, roar=0.15, slam=0;
  if(st==="walk"){ gait=t*5.6; air=Math.max(0,Math.sin(gait)); lift=air*0.52*u; surge=air*0.16*u; roar=0.1+air*0.22; }
  else if(st==="beat"){ rear=0.85; roar=0.4+0.6*Math.abs(Math.sin(a*Math.PI)); }
  else if(st==="throw"){ rear=0.6*Math.sin(Math.min(1,a/0.6)*Math.PI); roar=(a>0.5&&a<0.82)?1:0.2; }
  else if(st==="pound"){ rear=kf(a,[[0,0.12],[0.3,0.5],[0.5,0.05],[1,0.05]]); slam=kf(a,[[0,0],[0.34,0.15],[0.5,1],[0.72,0.5],[1,0]]); roar=(a>0.44&&a<0.62)?1:0.2; surge=kf(a,[[0,0],[0.5,0.35],[1,0.1]])*u; }

  // ---- anchors (quadruped charge -> reared, lerp by rear) ----
  const hipX=-0.82*u, hipY=(-1.14+rear*0.14)*u-lift;
  const shX=L(0.34,0.02,rear)*u+surge*0.4, shY=L(-1.62,-2.10,rear)*u-lift;
  const headX=L(1.20,0.34,rear)*u+surge, headY=L(-1.16,-2.40,rear)*u-lift;
  const chestX=L(0.74,0.16,rear)*u+surge*0.5, chestY=L(-0.84,-1.36,rear)*u-lift;
  const chestCy=chestY;

  // front hands (BOUND: reach forward+up mid-leap, plant on land)
  const chargeHX=(1.14+air*0.24)*u+surge, chargeHY=(-0.03-air*0.58)*u;
  let fhx,fhy;
  if(st==="beat"){ const bt=Math.sin(a*Math.PI*8); fhx=chestX+0.02*u-Math.max(0,bt)*0.18*u; fhy=chestY+0.16*u; }
  else if(st==="throw"){ fhx=kf(a,[[0,0.95],[0.42,-0.25],[0.7,1.55],[1,1.1]])*u; fhy=kf(a,[[0,-1.0],[0.42,-2.6],[0.7,-1.95],[1,-0.95]])*u; }
  else if(st==="pound"){ fhx=(0.98)*u+surge; fhy=kf(a,[[0,-1.15],[0.34,-2.35],[0.5,-0.02],[1,-0.05]])*u; }
  else { fhx=chargeHX; fhy=chargeHY; }
  const fh={x:fhx,y:fhy};
  const farHX=(1.02+air*0.24)*u+surge, farHY=(-0.03-air*0.5)*u;
  const farBeat=(st==="beat")?{x:chestX-0.16*u,y:chestY+0.12*u}:null;
  const nearFootX=(-0.5-air*0.22)*u, nearFootY=(-0.03-air*0.52)*u;
  const farFootX=(-0.74-air*0.22)*u, farFootY=(-0.03-air*0.46)*u;

  // ---- exposed-ape limb (bare forearm/leg + big hand/foot, grey suit cuff at top) ----
  const limb=(sx,sy,hx,hy,bend,w,endR,foot,dim)=>{
    const mx=(sx+hx)/2, my=(sy+hy)/2, dx=hx-sx, dy=hy-sy, len=Math.hypot(dx,dy)||1;
    const nx=-dy/len, ny=dx/len;
    const ex=mx+nx*bend, ey=my+ny*bend+0.04*u;
    const fill=dim?fleshDim:fleshM, shad=dim?fleshDimD:fleshD;
    ctx.lineCap="round"; ctx.lineJoin="round";
    ctx.strokeStyle=outline; ctx.lineWidth=R(w*1.24);
    ctx.beginPath(); ctx.moveTo(sx,sy); ctx.quadraticCurveTo(ex,ey,hx,hy); ctx.stroke();
    ctx.strokeStyle=fill; ctx.lineWidth=R(w);
    ctx.beginPath(); ctx.moveTo(sx,sy); ctx.quadraticCurveTo(ex,ey,hx,hy); ctx.stroke();
    ctx.strokeStyle=shad; ctx.globalAlpha=0.5; ctx.lineWidth=R(w*0.4);
    ctx.beginPath(); ctx.moveTo(sx+nx*w*0.28,sy+ny*w*0.28); ctx.quadraticCurveTo(ex+nx*w*0.28,ey+ny*w*0.28,hx+nx*w*0.28,hy+ny*w*0.28); ctx.stroke(); ctx.globalAlpha=1;
    if(!dim){ for(let k=0;k<3;k++){ const tt=0.34+k*0.2, it=1-tt; const px=it*it*sx+2*it*tt*ex+tt*tt*hx, py=it*it*sy+2*it*tt*ey+tt*tt*hy;
      ctx.fillStyle=fleshHi; ctx.globalAlpha=0.45; ctx.beginPath(); ctx.arc(px-nx*w*0.18,py-ny*w*0.18,R(w*0.13),0,7); ctx.fill();
      ctx.fillStyle=shad; ctx.beginPath(); ctx.arc(px+nx*w*0.16,py+ny*w*0.16,R(w*0.1),0,7); ctx.fill(); } ctx.globalAlpha=1; }
    // grey suit cuff band across the limb (thin, butt-capped so it doesn't blob)
    const cux=sx+(ex-sx)*0.3, cuy=sy+(ey-sy)*0.3;
    ctx.lineCap="butt";
    ctx.strokeStyle=outline; ctx.lineWidth=R(0.09*u); ctx.beginPath(); ctx.moveTo(cux-nx*w*0.62,cuy-ny*w*0.62); ctx.lineTo(cux+nx*w*0.62,cuy+ny*w*0.62); ctx.stroke();
    ctx.strokeStyle=dim?suitSh:collar; ctx.lineWidth=R(0.06*u); ctx.beginPath(); ctx.moveTo(cux-nx*w*0.6,cuy-ny*w*0.6); ctx.lineTo(cux+nx*w*0.6,cuy+ny*w*0.6); ctx.stroke();
    ctx.lineCap="round";
    // big bare ape hand / foot
    var kang=Math.atan2(hy-ey,hx-ex);
    ctx.save(); ctx.translate(hx,hy); ctx.rotate(kang);
    ctx.fillStyle=outline; ctx.beginPath(); ctx.arc(0,0,R(endR*1.12),0,7); ctx.fill();
    ctx.fillStyle=fill; ctx.beginPath(); ctx.arc(0,0,R(endR),0,7); ctx.fill();
    if(foot){ for(let k=-1;k<=2;k++){ ctx.fillStyle=outline; ctx.beginPath(); ctx.arc(endR*0.9,k*endR*0.34,R(endR*0.27),0,7); ctx.fill();
        ctx.fillStyle=fill; ctx.beginPath(); ctx.arc(endR*0.9,k*endR*0.34,R(endR*0.22),0,7); ctx.fill(); } }
    else { for(let k=-1;k<=1;k++){ ctx.fillStyle=outline; ctx.beginPath(); ctx.arc(endR*0.72,k*endR*0.44,R(endR*0.32),0,7); ctx.fill();
        ctx.fillStyle=fill; ctx.beginPath(); ctx.arc(endR*0.72,k*endR*0.44,R(endR*0.27),0,7); ctx.fill(); }
      ctx.fillStyle=shad; ctx.globalAlpha=0.45; for(let k=-1;k<=1;k++){ ctx.beginPath(); ctx.arc(endR*0.9,k*endR*0.44,R(endR*0.09),0,7); ctx.fill(); } ctx.globalAlpha=1; }
    if(!dim){ ctx.fillStyle=fleshHi; ctx.globalAlpha=0.4; ctx.beginPath(); ctx.arc(-endR*0.3,-endR*0.3,R(endR*0.26),0,7); ctx.fill(); ctx.globalAlpha=1; }
    ctx.restore();
  };

  // ---- ground shadow ----
  ctx.save(); ctx.fillStyle="rgba(0,0,0,.32)";
  ctx.beginPath(); ctx.ellipse(gor.x, GROUND()-2*DPR, R(u*2.0), R(u*0.34),0,0,7); ctx.fill();
  ctx.restore();

  ctx.save(); ctx.translate(gor.x, GROUND()); ctx.scale(facing,1);
  ctx.lineJoin="round"; ctx.lineCap="round";

  // motion streaks (charge)
  if(st==="walk"){ ctx.save(); ctx.strokeStyle="#ffffff"; ctx.lineCap="round";
    for(let i=0;i<5;i++){ const yy=-0.15*u-i*0.34*u, ln=(0.5+hashS(i)*0.6)*u; ctx.globalAlpha=(0.08+0.1*hashY(i))*(0.4+air); ctx.lineWidth=R((0.025+hashY(i)*0.03)*u);
      const off=((t*2.2+i*0.37)%1); const xx=-1.35*u-off*0.7*u;
      ctx.beginPath(); ctx.moveTo(xx,yy); ctx.lineTo(xx-ln,yy); ctx.stroke(); }
    ctx.globalAlpha=1; ctx.restore(); }

  // FAR limbs (behind body)
  limb(farFootX+0.1*u,hipY+0.1*u, farFootX,farFootY, 0.28*u, 0.42*u, 0.26*u, true, true);
  limb(shX-0.05*u,shY+0.1*u, farHX,farHY, -0.3*u, 0.48*u, 0.34*u, false, true);

  // ---- backpack (life support) ----
  ctx.save();
  const bpX=L((hipX+shX)/2-0.05*u, -0.35*u, rear), bpY=L(shY+0.42*u, shY, rear);
  ctx.translate(bpX, bpY);
  ctx.fillStyle=outline; rr(ctx,-0.42*u,-0.52*u,R(0.72*u),R(1.08*u),R(0.1*u)); ctx.fill();
  ctx.fillStyle=suitSh; rr(ctx,-0.38*u,-0.48*u,R(0.62*u),R(1.0*u),R(0.08*u)); ctx.fill();
  ctx.fillStyle=suitDk; rr(ctx,-0.3*u,-0.4*u,R(0.46*u),R(0.34*u),R(0.05*u)); ctx.fill();
  ctx.strokeStyle=suitDk; ctx.lineWidth=R(2*DPR); for(let k=0;k<3;k++){ ctx.beginPath(); ctx.moveTo(-0.34*u,-0.1*u+k*0.22*u); ctx.lineTo(0.28*u,-0.1*u+k*0.22*u); ctx.stroke(); }
  ctx.restore();

  // ---- grey quilted suit torso ----
  const torso=()=>{ ctx.beginPath();
    ctx.moveTo(hipX-0.34*u, hipY+0.60*u);
    ctx.quadraticCurveTo(hipX-0.54*u, hipY-0.05*u, hipX+0.02*u, shY+0.62*u);
    ctx.quadraticCurveTo(L(0.10,-0.12,rear)*u, shY+0.06*u, shX+0.30*u, shY+0.10*u);
    ctx.quadraticCurveTo(chestX+0.40*u, chestY+0.04*u, chestX+0.26*u, chestY+0.74*u);
    ctx.quadraticCurveTo(0.34*u, -0.10*u, hipX+0.46*u, -0.04*u);
    ctx.quadraticCurveTo(hipX+0.04*u, -0.02*u, hipX-0.34*u, hipY+0.60*u);
    ctx.closePath(); };
  torso(); ctx.strokeStyle=outline; ctx.lineWidth=R(5*DPR); ctx.stroke();
  const tg=ctx.createLinearGradient(0,shY,0,0); tg.addColorStop(0,suitHi); tg.addColorStop(0.5,suit); tg.addColorStop(1,suitSh);
  torso(); ctx.fillStyle=tg; ctx.fill();
  ctx.save(); torso(); ctx.clip();
  // quilting (horizontal seams)
  ctx.strokeStyle=suitDk; ctx.globalAlpha=0.4; ctx.lineWidth=R(1.8*DPR);
  for(let k=0;k<9;k++){ const yy=shY+0.2*u+k*0.28*u; ctx.beginPath(); ctx.moveTo(hipX-0.6*u,yy); ctx.quadraticCurveTo(0,yy+0.06*u,chestX+0.5*u,yy); ctx.stroke(); }
  ctx.globalAlpha=0.5; ctx.fillStyle=suitSh; ctx.beginPath(); ctx.ellipse(0.12*u,-0.08*u,R(1.0*u),R(0.5*u),0,0,7); ctx.fill();
  ctx.globalAlpha=1; ctx.restore();
  // vertical zipper down the chest/front
  ctx.strokeStyle=suitDk; ctx.lineWidth=R(3.4*DPR);
  ctx.beginPath(); ctx.moveTo(chestX+0.12*u,chestY+0.16*u); ctx.quadraticCurveTo(chestX+0.02*u,chestY+0.7*u,hipX+0.5*u,-0.06*u); ctx.stroke();
  ctx.strokeStyle=collar; ctx.globalAlpha=0.5; ctx.lineWidth=R(1.4*DPR); ctx.stroke(); ctx.globalAlpha=1;

  // NEAR hind leg
  limb(hipX+0.05*u,hipY+0.15*u, nearFootX,nearFootY, 0.3*u, 0.44*u, 0.28*u, true, false);

  // ---- harness straps + belly life-support port + CCCP flag patch ----
  ctx.strokeStyle=strapC; ctx.lineWidth=R(0.13*u); ctx.lineCap="round";
  ctx.beginPath(); ctx.moveTo(shX+0.24*u,shY+0.2*u); ctx.quadraticCurveTo(chestX+0.1*u,chestY+0.3*u,chestX-0.16*u,chestY+0.66*u); ctx.stroke();
  ctx.beginPath(); ctx.moveTo(L(0.05,-0.1,rear)*u,shY+0.28*u); ctx.quadraticCurveTo(chestX-0.05*u,chestY+0.34*u,chestX+0.2*u,chestY+0.64*u); ctx.stroke();
  // belly port
  ctx.save(); ctx.translate(chestX-0.02*u, chestY+0.56*u);
  ctx.fillStyle=outline; ctx.beginPath(); ctx.arc(0,0,R(0.2*u),0,7); ctx.fill();
  ctx.fillStyle=collar; ctx.beginPath(); ctx.arc(0,0,R(0.16*u),0,7); ctx.fill();
  ctx.fillStyle=collarDk; ctx.beginPath(); ctx.arc(0,0,R(0.12*u),0,7); ctx.fill();
  ctx.save(); ctx.globalCompositeOperation="lighter"; ctx.fillStyle=red; ctx.globalAlpha=0.6+0.3*Math.sin(t*3);
  ctx.beginPath(); ctx.arc(0,0,R(0.09*u),0,7); ctx.fill(); ctx.restore();
  ctx.fillStyle=redDk; ctx.beginPath(); ctx.arc(0,0,R(0.055*u),0,7); ctx.fill();
  ctx.strokeStyle=collarDk; ctx.lineWidth=R(0.09*u); ctx.beginPath(); ctx.moveTo(0,0.16*u); ctx.quadraticCurveTo(0.24*u,0.34*u,0.1*u,0.6*u); ctx.stroke();
  ctx.restore();
  // CCCP red flag patch on the front shoulder
  ctx.save(); ctx.translate(shX+0.16*u, shY+0.34*u);
  ctx.fillStyle=outline; rr(ctx,-0.15*u,-0.11*u,R(0.3*u),R(0.22*u),R(0.03*u)); ctx.fill();
  ctx.fillStyle=cccpRed; rr(ctx,-0.13*u,-0.09*u,R(0.26*u),R(0.18*u),R(0.02*u)); ctx.fill();
  ctx.fillStyle=cccpYel; ctx.beginPath(); ctx.arc(-0.03*u,-0.01*u,R(0.03*u),0,7); ctx.fill();
  ctx.strokeStyle=cccpYel; ctx.lineWidth=R(1.6*DPR); ctx.beginPath(); ctx.moveTo(-0.03*u,0.0*u); ctx.lineTo(0.03*u,0.05*u); ctx.moveTo(-0.03*u,0.0*u); ctx.lineTo(0.02*u,-0.04*u); ctx.stroke();
  ctx.restore();

  // ---- neck + helmet + open-visor face ----
  limb(shX+0.1*u,shY+0.1*u, headX,headY+0.4*u, (rear>0.3?0.0:0.14*u), 0.5*u, 0.02*u, false, false);
  ctx.save(); ctx.translate(headX, headY);
  // collar ring (neck)
  ctx.fillStyle=outline; ctx.beginPath(); ctx.ellipse(0,0.5*u,R(0.5*u),R(0.18*u),0,0,7); ctx.fill();
  ctx.fillStyle=collar; ctx.beginPath(); ctx.ellipse(0,0.46*u,R(0.44*u),R(0.13*u),0,0,7); ctx.fill();
  ctx.fillStyle=collarDk; ctx.beginPath(); ctx.ellipse(0,0.5*u,R(0.42*u),R(0.08*u),0,0,7); ctx.fill();
  // dark ape head
  ctx.fillStyle=outline; ctx.beginPath(); ctx.arc(0.02*u,0.0,R(0.56*u),0,7); ctx.fill();
  ctx.fillStyle=furD; ctx.beginPath(); ctx.arc(0.02*u,0.0,R(0.52*u),0,7); ctx.fill();
  // beard fur along the jaw
  ctx.fillStyle=furD; for(let k=0;k<7;k++){ const ang=Math.PI*(0.15+k*0.11); const rx=Math.cos(ang)*0.5*u, ry=0.2*u+Math.sin(ang)*0.42*u;
    ctx.beginPath(); ctx.arc(rx,ry,R((0.08+0.03*hashS(k))*u),0,7); ctx.fill(); }
  // muzzle
  ctx.fillStyle=skin; ctx.beginPath(); ctx.ellipse(0.16*u,0.2*u,R(0.28*u),R(0.22*u),0,0,7); ctx.fill();
  ctx.fillStyle=skinHi; ctx.globalAlpha=0.5; ctx.beginPath(); ctx.ellipse(0.16*u,0.12*u,R(0.2*u),R(0.1*u),0,0,7); ctx.fill(); ctx.globalAlpha=1;
  // heavy brow
  ctx.fillStyle=furM; ctx.beginPath();
  ctx.moveTo(-0.22*u,-0.02*u); ctx.quadraticCurveTo(0.12*u,-0.22*u,0.42*u,-0.01*u);
  ctx.quadraticCurveTo(0.38*u,0.1*u,0.12*u,0.02*u); ctx.quadraticCurveTo(-0.1*u,0.08*u,-0.22*u,-0.02*u); ctx.closePath(); ctx.fill();
  // eyes (amber, angry)
  ctx.fillStyle="#efe7d8"; ctx.beginPath(); ctx.ellipse(0.02*u,0.05*u,R(0.07*u),R(0.085*u),0,0,7); ctx.ellipse(0.28*u,0.06*u,R(0.07*u),R(0.085*u),0,0,7); ctx.fill();
  ctx.fillStyle=amber; ctx.beginPath(); ctx.arc(0.04*u,0.07*u,R(0.04*u),0,7); ctx.arc(0.3*u,0.08*u,R(0.04*u),0,7); ctx.fill();
  ctx.fillStyle="#1a1208"; ctx.beginPath(); ctx.arc(0.05*u,0.08*u,R(0.02*u),0,7); ctx.arc(0.31*u,0.09*u,R(0.02*u),0,7); ctx.fill();
  // nostrils
  ctx.fillStyle="#201812"; ctx.beginPath(); ctx.ellipse(0.1*u,0.22*u,R(0.032*u),R(0.022*u),0,0,7); ctx.ellipse(0.22*u,0.22*u,R(0.032*u),R(0.022*u),0,0,7); ctx.fill();
  // snarl
  if(roar>0.25){
    ctx.fillStyle=gumC; ctx.beginPath(); ctx.ellipse(0.16*u,0.36*u,R(0.17*u),R((0.05+roar*0.1)*u),0,0,7); ctx.fill();
    ctx.fillStyle=fang;
    for(let k=0;k<4;k++){ ctx.beginPath(); ctx.rect((0.03+k*0.06)*u,0.31*u,R(0.036*u),R(0.04*u)); ctx.fill(); }
    ctx.beginPath(); ctx.moveTo(0.02*u,0.31*u); ctx.lineTo(0.06*u,0.31*u); ctx.lineTo(0.04*u,0.45*u); ctx.closePath();
    ctx.moveTo(0.26*u,0.31*u); ctx.lineTo(0.3*u,0.31*u); ctx.lineTo(0.28*u,0.45*u); ctx.closePath(); ctx.fill();
  } else {
    ctx.strokeStyle="#201812"; ctx.lineWidth=R(2.6*DPR); ctx.lineCap="round";
    ctx.beginPath(); ctx.moveTo(0.02*u,0.36*u); ctx.quadraticCurveTo(0.16*u,0.3*u,0.3*u,0.36*u); ctx.stroke();
  }
  // METAL HELMET dome over the top/back (face left open)
  ctx.save();
  ctx.beginPath(); ctx.rect(-0.72*u,-0.78*u,1.5*u,0.72*u); ctx.clip();
  ctx.fillStyle=outline; ctx.beginPath(); ctx.arc(0.02*u,0.0,R(0.62*u),0,7); ctx.fill();
  const hmg=ctx.createLinearGradient(0,-0.62*u,0,0.05*u); hmg.addColorStop(0,helmHi); hmg.addColorStop(0.6,helmMid); hmg.addColorStop(1,helmDk);
  ctx.fillStyle=hmg; ctx.beginPath(); ctx.arc(0.02*u,0.0,R(0.58*u),0,7); ctx.fill();
  // yellow helmet band
  ctx.fillStyle=cccpYel; ctx.fillRect(-0.5*u,-0.42*u,1.05*u,0.13*u);
  ctx.fillStyle=helmDk; ctx.globalAlpha=0.3; ctx.fillRect(-0.5*u,-0.31*u,1.05*u,0.02*u); ctx.globalAlpha=1;
  // weather scuffs
  ctx.fillStyle=helmDk; ctx.globalAlpha=0.3; ctx.beginPath(); ctx.arc(-0.2*u,-0.5*u,R(0.06*u),0,7); ctx.arc(0.3*u,-0.44*u,R(0.05*u),0,7); ctx.fill(); ctx.globalAlpha=1;
  ctx.restore();
  // flipped-up visor frame (metal ring band over the brow)
  ctx.strokeStyle=helmMid; ctx.lineWidth=R(0.1*u);
  ctx.beginPath(); ctx.arc(0.02*u,0.0,R(0.58*u),Math.PI*1.02,Math.PI*1.98); ctx.stroke();
  ctx.strokeStyle=helmHi; ctx.globalAlpha=0.7; ctx.lineWidth=R(3*DPR);
  ctx.beginPath(); ctx.arc(0.02*u,0.0,R(0.55*u),Math.PI*1.15,Math.PI*1.6); ctx.stroke(); ctx.globalAlpha=1;
  ctx.restore();

  // NEAR front arm (reaching forward)
  limb(shX+0.08*u,shY+0.12*u, fh.x,fh.y, (st==="throw"||st==="beat"||st==="pound")?-0.4*u:-0.46*u, 0.56*u, 0.4*u, false, false);
  if(farBeat){ limb(shX-0.02*u,shY+0.14*u, farBeat.x,farBeat.y, -0.3*u, 0.5*u, 0.34*u, false, true); }

  // ---- FX ----
  if(st==="throw" && a<0.68){
    const rg=ctx.createRadialGradient(fh.x-0.1*u,fh.y-0.1*u,0.05*u,fh.x,fh.y,0.34*u);
    rg.addColorStop(0,"#9a8f80"); rg.addColorStop(1,"#463c34");
    ctx.fillStyle=rg; ctx.beginPath(); ctx.arc(fh.x,fh.y,R(0.32*u),0,7); ctx.fill();
    ctx.fillStyle="rgba(255,255,255,.2)"; ctx.beginPath(); ctx.arc(fh.x-0.1*u,fh.y-0.1*u,R(0.1*u),0,7); ctx.fill();
  }
  if(st==="beat"){ const bt=Math.sin(a*Math.PI*8);
    if(bt>0.85){ ctx.save(); ctx.globalCompositeOperation="lighter"; ctx.fillStyle="rgba(255,90,70,0.7)";
      ctx.beginPath(); ctx.arc(chestX,chestY+0.5*u,R(0.16*u),0,7); ctx.fill(); ctx.restore(); }
  }
  if(st==="pound" && a>0.46 && a<0.7){
    const p=(a-0.46)/0.24, al=1-p;
    ctx.save(); ctx.globalCompositeOperation="lighter"; ctx.fillStyle="rgba(255,210,140,"+(0.45*al).toFixed(3)+")";
    for(let i=0;i<10;i++){ const ang=(i/10)*6.283, rad=(0.5+p*1.5)*u*(0.7+hashS(i)*0.6);
      const dxp=Math.cos(ang)*rad, dyp=-Math.abs(Math.sin(ang))*rad*0.4;
      ctx.beginPath(); ctx.arc(0.5*u+dxp,-0.02*u+dyp,R((0.12+hashY(i)*0.12)*u*(0.5+p)),0,7); ctx.fill(); }
    ctx.restore();
    ctx.strokeStyle="rgba(60,40,20,"+(0.5*al).toFixed(3)+")"; ctx.lineWidth=R(3*DPR);
    for(let i=0;i<5;i++){ const ang=(i/5-0.5)*2.2;
      ctx.beginPath(); ctx.moveTo(0.5*u,-0.02*u); ctx.lineTo(0.5*u+Math.cos(ang)*1.6*u,-0.02*u+Math.abs(Math.sin(ang))*0.22*u); ctx.stroke(); }
  }
  ctx.restore();
  ctx.globalAlpha=1; ctx.globalCompositeOperation="source-over"; ctx.textBaseline="alphabetic";
}
function drawGCoin(h,t){
  if(h.got) return;
  const y=h.y+Math.sin((h.t||0)*4)*3*DPR, r=h.r;
  ctx.save(); ctx.globalCompositeOperation="lighter"; ctx.fillStyle="rgba(255,210,80,.28)"; ctx.beginPath(); ctx.arc(h.x,y,r*1.7,0,7); ctx.fill(); ctx.restore();
  const gr=ctx.createRadialGradient(h.x-r*0.3,y-r*0.3,r*0.2,h.x,y,r);
  gr.addColorStop(0,"#fff2b0"); gr.addColorStop(0.5,"#ffd24d"); gr.addColorStop(1,"#e09a1a");
  ctx.fillStyle=gr; ctx.beginPath(); ctx.arc(h.x,y,r,0,7); ctx.fill();
  ctx.strokeStyle="#c47f10"; ctx.lineWidth=Math.max(1,2*DPR); ctx.beginPath(); ctx.arc(h.x,y,r*0.66,0,7); ctx.stroke();
  ctx.fillStyle="rgba(255,255,255,.6)"; ctx.beginPath(); ctx.ellipse(h.x-r*0.3,y-r*0.35,r*0.28,r*0.15,-0.5,0,7); ctx.fill();
}
function drawPillar(h,t){
  const g=GROUND(), topB=h.gapY-h.gapH/2, botT=h.gapY+h.gapH/2, w=h.w;
  const grd=ctx.createLinearGradient(h.x-w/2,0,h.x+w/2,0); grd.addColorStop(0,"#2c2e36"); grd.addColorStop(0.5,"#666c7c"); grd.addColorStop(1,"#2c2e36");
  ctx.save(); ctx.fillStyle=grd; ctx.strokeStyle="#171922"; ctx.lineWidth=Math.max(1,3*DPR);
  rr(ctx,h.x-w/2,-24*DPR,w,topB+24*DPR,Math.max(1,7*DPR)); ctx.fill(); ctx.stroke();
  rr(ctx,h.x-w/2,botT,w,g-botT+24*DPR,Math.max(1,7*DPR)); ctx.fill(); ctx.stroke();
  ctx.strokeStyle="rgba(28,23,18,.5)"; ctx.lineWidth=Math.max(1,1.6*DPR);
  for(let y=topB-30*DPR;y>0;y-=34*DPR){ ctx.beginPath(); ctx.moveTo(h.x-w/2,y); ctx.lineTo(h.x+w/2,y); ctx.stroke(); }
  for(let y=botT+30*DPR;y<g;y+=34*DPR){ ctx.beginPath(); ctx.moveTo(h.x-w/2,y); ctx.lineTo(h.x+w/2,y); ctx.stroke(); }
  ctx.save(); ctx.globalCompositeOperation="lighter"; ctx.fillStyle="#40e6d4";
  ctx.beginPath(); ctx.ellipse(h.x,topB,w*0.6,10*DPR,0,0,7); ctx.fill();
  ctx.beginPath(); ctx.ellipse(h.x,botT,w*0.6,10*DPR,0,0,7); ctx.fill(); ctx.restore();
  ctx.fillStyle="rgba(255,255,255,.12)"; ctx.fillRect(h.x-w/2,-24*DPR,w*0.26,topB+24*DPR); ctx.fillRect(h.x-w/2,botT,w*0.26,g-botT);
  ctx.restore();
}
function drawRock(h,t){
  const r=Math.max(1,h.r);
  ctx.save(); ctx.translate(h.x,h.y); ctx.rotate((h.t||0)*4);
  const gr=ctx.createRadialGradient(-r*0.3,-r*0.3,r*0.2,0,0,r); gr.addColorStop(0,"#9a8f80"); gr.addColorStop(1,"#463c34");
  ctx.fillStyle=gr; ctx.beginPath(); for(let i=0;i<8;i++){ const a=i/8*6.283, rr2=r*(0.82+0.18*Math.sin(i*2.3)); if(i===0)ctx.moveTo(Math.cos(a)*rr2,Math.sin(a)*rr2); else ctx.lineTo(Math.cos(a)*rr2,Math.sin(a)*rr2); } ctx.closePath(); ctx.fill();
  ctx.strokeStyle="rgba(0,0,0,.35)"; ctx.lineWidth=Math.max(1,1.5*DPR); ctx.stroke();
  ctx.fillStyle="rgba(255,255,255,.18)"; ctx.beginPath(); ctx.arc(-r*0.32,-r*0.32,Math.max(1,r*0.26),0,7); ctx.fill();
  ctx.restore();
}
function drawShock(h,t){
  const g=GROUND(), r=Math.max(1,h.r);
  ctx.save(); ctx.lineCap="round";
  ctx.strokeStyle="#e8d8a8"; ctx.lineWidth=Math.max(1,5*DPR);
  ctx.beginPath(); ctx.arc(h.x, g, r, Math.PI*1.1, Math.PI*1.9); ctx.stroke();
  ctx.strokeStyle="rgba(255,255,255,.75)"; ctx.lineWidth=Math.max(1,2.5*DPR);
  ctx.beginPath(); ctx.arc(h.x, g, r*0.66, Math.PI*1.16, Math.PI*1.84); ctx.stroke();
  ctx.fillStyle="rgba(190,170,130,.5)"; ctx.beginPath(); ctx.ellipse(h.x, g-3*DPR, r*0.6, r*0.24, 0,0,7); ctx.fill();
  ctx.restore();
}
function drawShark(h,t){
  const r=Math.max(1,h.r);
  const ang=Math.atan2(h.vy,h.vx);
  const swim=Math.sin(t*10 + h.x*0.01);
  ctx.save();
  ctx.translate(h.x,h.y);
  ctx.rotate(ang);
  ctx.scale(h.side,1);
  ctx.save(); ctx.globalCompositeOperation="lighter";
  const wake=ctx.createLinearGradient(-r*4.2,0,-r*1.2,0);
  wake.addColorStop(0,"rgba(150,225,255,0)"); wake.addColorStop(1,"rgba(150,225,255,0.22)");
  ctx.fillStyle=wake; ctx.beginPath();
  ctx.moveTo(-r*1.2,-r*0.5); ctx.quadraticCurveTo(-r*3.6,-r*0.14,-r*4.2,0);
  ctx.quadraticCurveTo(-r*3.6,r*0.14,-r*1.2,r*0.5); ctx.closePath(); ctx.fill();
  ctx.restore();
  ctx.save(); ctx.globalCompositeOperation="lighter";
  const th=ctx.createRadialGradient(r*0.9,0,Math.max(1,r*0.1),r*0.9,0,Math.max(1,r*1.6));
  th.addColorStop(0,"rgba(255,40,40,0.28)"); th.addColorStop(1,"rgba(255,40,40,0)");
  ctx.fillStyle=th; ctx.beginPath(); ctx.arc(r*0.9,0,Math.max(1,r*1.6),0,7); ctx.fill();
  ctx.restore();
  const ty=swim*r*0.32;
  ctx.fillStyle="#3d4d5a"; ctx.beginPath();
  ctx.moveTo(-r*1.25,0); ctx.lineTo(-r*2.15,-r*0.85+ty);
  ctx.lineTo(-r*1.75,0); ctx.lineTo(-r*2.1,r*0.72+ty); ctx.closePath(); ctx.fill();
  const bg=ctx.createLinearGradient(0,-r*0.7,0,r*0.7);
  bg.addColorStop(0,"#556876"); bg.addColorStop(0.5,"#42535f");
  bg.addColorStop(0.55,"#3a4a56"); bg.addColorStop(1,"#d8e2e6");
  ctx.fillStyle=bg; ctx.beginPath();
  ctx.moveTo(-r*1.3,0);
  ctx.quadraticCurveTo(-r*0.6,-r*0.62, r*0.4,-r*0.5);
  ctx.quadraticCurveTo(r*1.2,-r*0.4, r*1.65,-r*0.06);
  ctx.quadraticCurveTo(r*1.45,r*0.12, r*1.05,r*0.34);
  ctx.quadraticCurveTo(r*0.3,r*0.6,-r*0.6,r*0.5);
  ctx.quadraticCurveTo(-r*1.0,r*0.36,-r*1.3,0);
  ctx.closePath(); ctx.fill();
  ctx.fillStyle="#37454f"; ctx.beginPath();
  ctx.moveTo(-r*0.15,-r*0.5); ctx.lineTo(r*0.2,-r*1.2); ctx.lineTo(r*0.5,-r*0.44); ctx.closePath(); ctx.fill();
  ctx.fillStyle="#2f3c45"; ctx.beginPath();
  ctx.moveTo(r*0.55,r*0.28); ctx.lineTo(r*0.9,r*0.95); ctx.lineTo(r*1.0,r*0.3); ctx.closePath(); ctx.fill();
  ctx.strokeStyle="rgba(20,28,34,0.75)"; ctx.lineWidth=Math.max(1,r*0.07); ctx.lineCap="round";
  for(let i=0;i<4;i++){ const gx=r*0.55-i*r*0.16; ctx.beginPath(); ctx.moveTo(gx,-r*0.24); ctx.quadraticCurveTo(gx-r*0.05,0,gx,r*0.24); ctx.stroke(); }
  ctx.fillStyle="#25141a"; ctx.beginPath();
  ctx.moveTo(r*1.62,-r*0.02); ctx.quadraticCurveTo(r*1.2,r*0.16,r*0.86,r*0.44);
  ctx.quadraticCurveTo(r*1.2,r*0.42,r*1.55,r*0.2); ctx.closePath(); ctx.fill();
  ctx.fillStyle="#ffffff";
  for(let i=0;i<5;i++){ const f=i/4;
    const ux=r*(1.55-0.62*f), uy=-r*0.02+r*0.2*f;
    ctx.beginPath(); ctx.moveTo(ux,uy); ctx.lineTo(ux-r*0.12,uy+r*0.14); ctx.lineTo(ux+r*0.04,uy+r*0.16); ctx.closePath(); ctx.fill();
    const lx=r*(1.5-0.6*f), ly=r*0.42-r*0.02*f;
    ctx.beginPath(); ctx.moveTo(lx,ly); ctx.lineTo(lx-r*0.1,ly-r*0.14); ctx.lineTo(lx+r*0.05,ly-r*0.15); ctx.closePath(); ctx.fill();
  }
  ctx.fillStyle="#0a0d10"; ctx.beginPath(); ctx.arc(r*0.92,-r*0.16,Math.max(1,r*0.14),0,7); ctx.fill();
  ctx.fillStyle="rgba(255,60,60,0.9)"; ctx.beginPath(); ctx.arc(r*0.95,-r*0.19,Math.max(1,r*0.05),0,7); ctx.fill();
  ctx.restore();
  ctx.globalAlpha=1; ctx.globalCompositeOperation="source-over";
}
function drawTrajectory(){ if(!(state==="aim"&&charging)) return; const a=aimAngle*Math.PI/180, p=(power/100)*cfg.power*DPR;
  let vx=Math.cos(a)*p, vy=-Math.sin(a)*p, px=kitty.x, py=kitty.y;
  for(let i=0;i<30;i++){ vy+=cfg.gravity*DPR*3; px+=vx*3; py+=vy*3; if(py>GROUND()-kitty.r) break;
    ctx.globalAlpha=Math.max(0,.55-i*0.016); ctx.fillStyle=getAccent(1); ctx.beginPath(); ctx.arc(px-cam,py,3*DPR,0,7); ctx.fill(); } ctx.globalAlpha=1; }
// Passive-ability feedback attached to the kitty so you can SEE what's happening
function drawPassiveHud(t){ const A=ab(); const sx=kitty.x-cam, sy=kitty.y;
  if(candyRoll){ const cr=candyRoll, spinning=cr.t<cr.spin, prog=Math.min(1,cr.t/cr.spin); // CRAZY SLOTS reel above the cat: numbers blur then LOCK on the rolled power
    const idx = spinning ? (Math.floor(performance.now()/(45+150*prog*prog))%SLOTS.length) : cr.result, k=SLOTS[idx];
    const boxY=sy-kitty.r-48*DPR, bw=56*DPR, bh=42*DPR; ctx.save(); ctx.textAlign="center";
    ctx.fillStyle="rgba(10,8,20,.88)"; roundRect(sx-bw/2, boxY-bh/2, bw, bh, 9*DPR); ctx.fill();
    ctx.strokeStyle=spinning?"rgba(255,255,255,.45)":k.c; ctx.lineWidth=3*DPR; roundRect(sx-bw/2, boxY-bh/2, bw, bh, 9*DPR); ctx.stroke();
    if(!spinning){ const gl=0.5+0.5*Math.sin(performance.now()*0.02); ctx.shadowColor=k.c; ctx.shadowBlur=(8+12*gl)*DPR; }
    ctx.fillStyle=k.c; ctx.font=`900 ${23*DPR}px Orbitron`; ctx.fillText(String(idx+1), sx, boxY+2*DPR); ctx.shadowBlur=0;
    ctx.fillStyle="#fff"; ctx.font=`800 ${9*DPR}px Orbitron`; ctx.fillText(spinning?"· · ·":k.n, sx, boxY+bh/2+11*DPR); ctx.restore(); }
  if(starT>0){ const hue=(performance.now()/3.5)%360; ctx.save(); ctx.globalCompositeOperation="lighter"; ctx.lineWidth=4*DPR;
    for(let k=0;k<3;k++){ ctx.strokeStyle="hsla("+((hue+k*120)%360)+",100%,62%,.8)"; ctx.beginPath(); ctx.arc(sx,sy,kitty.r+(8+k*5)*DPR+3*DPR*Math.sin(t*16+k),0,7); ctx.stroke(); }
    ctx.restore(); if(parts.length<260 && Math.random()<0.7){ const a2=Math.random()*7, rr2=kitty.r*(1+Math.random()*0.6); parts.push({x:kitty.x+Math.cos(a2)*rr2,y:kitty.y+Math.sin(a2)*rr2,vx:rand(-2,2)*DPR,vy:rand(-3,1)*DPR,life:0.5,c:"hsl("+((hue+Math.random()*360)%360)+",100%,65%)",r:rand(2,4)*DPR}); }
    ctx.save(); ctx.fillStyle="#fff"; ctx.font=`900 ${11*DPR}px Orbitron`; ctx.textAlign="center"; ctx.shadowColor="rgba(0,0,0,.6)"; ctx.shadowBlur=4*DPR; ctx.fillText("SUPER STAR", sx, sy-kitty.r-20*DPR); ctx.restore(); }
  if(superT>0){ const pu=0.5+0.5*Math.sin(t*18); ctx.save(); ctx.globalCompositeOperation="lighter"; ctx.strokeStyle="rgba(255,211,77,"+(0.5+0.4*pu)+")"; ctx.lineWidth=4*DPR; ctx.beginPath(); ctx.arc(sx,sy,kitty.r+10*DPR+pu*3*DPR,0,7); ctx.stroke(); ctx.restore();
    ctx.save(); ctx.fillStyle="#ffe27a"; ctx.font=`900 ${11*DPR}px Orbitron`; ctx.textAlign="center"; ctx.shadowColor="rgba(0,0,0,.6)"; ctx.shadowBlur=4*DPR; ctx.fillText("GOD SPEED", sx, sy-kitty.r-20*DPR); ctx.restore(); } // GodSpeed invincible aura
  if(spicyT>0){ ctx.save(); ctx.fillStyle="#ff7a1a"; ctx.font=`900 ${10*DPR}px Orbitron`; ctx.textAlign="center"; ctx.shadowColor="rgba(0,0,0,.6)"; ctx.shadowBlur=4*DPR; ctx.fillText("SPICY", sx, sy-kitty.r-30*DPR); ctx.restore(); }
  if(A==="godmode"||A==="bartman"){ const w=44*DPR,h=6*DPR,bx=sx-w/2,by=sy-kitty.r-22*DPR; ctx.save(); // MAN OF STEEL / BART SKATE flight meter (hold to fly/skate, drains; recharges when grounded)
    ctx.fillStyle="rgba(0,0,0,.5)"; roundRect(bx-1*DPR,by-1*DPR,w+2*DPR,h+2*DPR,4*DPR); ctx.fill();
    ctx.fillStyle="rgba(120,180,255,.28)"; roundRect(bx,by,w,h,3*DPR); ctx.fill();
    ctx.fillStyle=(abHeld&&flightMeter>0)?"#bfe3ff":"#6fb0ff"; roundRect(bx,by,Math.max(0,w*Math.min(1,flightMeter)),h,3*DPR); ctx.fill();
    ctx.fillStyle="rgba(207,224,238,.9)"; ctx.shadowColor="rgba(0,0,0,.6)"; ctx.shadowBlur=4*DPR; ctx.font=`800 ${9*DPR}px Orbitron`; ctx.textAlign="center"; ctx.fillText(A==="bartman"?"SKATE (hold ⚡)":"FLIGHT (hold ⚡)", sx, by-6*DPR); ctx.restore(); }
  if(crashT>0){ const pu=0.5+0.5*Math.sin(t*20); ctx.save(); ctx.globalCompositeOperation="lighter"; ctx.strokeStyle="rgba(255,140,40,"+(0.45+0.4*pu)+")"; ctx.lineWidth=4*DPR; ctx.beginPath(); ctx.arc(sx,sy,kitty.r+9*DPR+pu*3*DPR,0,7); ctx.stroke(); ctx.restore(); ctx.save(); ctx.fillStyle="#ffd34d"; ctx.font=`900 ${10*DPR}px Orbitron`; ctx.textAlign="center"; ctx.shadowColor="rgba(0,0,0,.6)"; ctx.shadowBlur=4*DPR; ctx.fillText("IMMUNE", sx, sy-kitty.r-18*DPR); ctx.restore(); } // NITRO active = flame ring + IMMUNE
  if(A==="lightning" && overchargeT>0){ const pu=0.55+0.45*Math.sin(t*16); ctx.save(); ctx.strokeStyle="rgba(127,231,255,"+pu+")"; ctx.lineWidth=3*DPR; ctx.beginPath(); ctx.arc(sx,sy,kitty.r+9*DPR,0,7); ctx.stroke();
    ctx.fillStyle="#7fe7ff"; ctx.font=`900 ${11*DPR}px Orbitron`; ctx.textAlign="center"; ctx.shadowColor="rgba(0,0,0,.6)"; ctx.shadowBlur=4*DPR; ctx.fillText("OVERCHARGE", sx, sy-kitty.r-20*DPR); ctx.restore(); } // dash pierce window ring (cooldown shown on the ability button)
  else if(A==="fallout"){ if(falloutBounces<4){ ctx.save(); ctx.textAlign="center"; const cy=sy-kitty.r-26*DPR; ctx.fillStyle="#c6ff5a"; ctx.shadowColor="rgba(0,0,0,.6)"; ctx.shadowBlur=4*DPR; ctx.font=`800 ${9*DPR}px Orbitron`; ctx.fillText("MOMENTUM", sx, cy-12*DPR); ctx.restore(); } } // NUKE cooldown shows on the ability button
  else if(A==="dragon"){ const w=40*DPR,h=5*DPR,bx=sx-w/2,by=sy-kitty.r-20*DPR; const full=flapsLeft>=baseFlaps(); ctx.save();
    if(!full){ ctx.fillStyle="rgba(0,0,0,.5)"; roundRect(bx-1*DPR,by-1*DPR,w+2*DPR,h+2*DPR,4*DPR); ctx.fill(); ctx.fillStyle="rgba(255,140,60,.3)"; roundRect(bx,by,w,h,3*DPR); ctx.fill(); ctx.fillStyle="#ff9a3c"; roundRect(bx,by,Math.max(0,w*Math.min(1,dragonRegen/3)),h,3*DPR); ctx.fill(); } // flap-regen progress
    if(abUses>0){ ctx.fillStyle="rgba(255,180,90,.9)"; ctx.font=`800 ${9*DPR}px Orbitron`; ctx.textAlign="center"; ctx.fillText("DRAFT READY (⚡)", sx, sy-kitty.r-28*DPR); } ctx.restore(); }
  // (Chrome ARMOR pips + Turtle SHELL icon now render above the HEARTS in the lives HUD - easier to read than floating over the cat)
}
function drawWing(wx,wy,s,on){
  ctx.save(); ctx.translate(wx,wy+s*0.22); ctx.shadowBlur=0; ctx.lineJoin="round"; ctx.lineCap="round";
  const col = on ? "#7fe7ff" : "rgba(255,255,255,.30)";
  // ANGEL WINGS: symmetric feathered pair + halo (sgn=+1 right, -1 left)
  function wing(sgn){
    // classic angel wing: smooth top edge sweeping up-and-out to a pointed tip,
    // then a 3-scallop feathered underside curving back to the body.
    ctx.beginPath();
    ctx.moveTo(0, s*0.30);
    ctx.quadraticCurveTo(sgn*s*0.55, -s*0.46, sgn*s*1.46, -s*0.98);   // top arc -> pointed tip
    ctx.quadraticCurveTo(sgn*s*1.30, -s*0.40, sgn*s*1.12, -s*0.52);   // tip feather
    ctx.quadraticCurveTo(sgn*s*1.16, -s*0.06, sgn*s*0.86, -s*0.18);   // feather 1
    ctx.quadraticCurveTo(sgn*s*0.90,  s*0.20, sgn*s*0.56,  s*0.06);   // feather 2
    ctx.quadraticCurveTo(sgn*s*0.58,  s*0.42, sgn*s*0.26,  s*0.28);   // feather 3
    ctx.quadraticCurveTo(sgn*s*0.12,  s*0.40, 0, s*0.30);             // back to body
    ctx.closePath();
  }
  function feathers(sgn){
    ctx.strokeStyle="rgba(8,22,38,.5)"; ctx.lineWidth=Math.max(0.7,s*0.06);
    ctx.beginPath();
    ctx.moveTo(sgn*s*0.30, -s*0.16); ctx.quadraticCurveTo(sgn*s*0.95, -s*0.46, sgn*s*1.34, -s*0.86); // top quill
    ctx.moveTo(sgn*s*0.30, -s*0.02); ctx.quadraticCurveTo(sgn*s*0.78, -s*0.22, sgn*s*1.02, -s*0.40);
    ctx.moveTo(sgn*s*0.28,  s*0.14); ctx.quadraticCurveTo(sgn*s*0.56, -s*0.02, sgn*s*0.74, -s*0.10);
    ctx.stroke();
  }
  function halo(){
    ctx.beginPath();
    ctx.ellipse(0, -s*1.02, s*0.42, s*0.18, 0, 0, Math.PI*2);
    ctx.lineWidth=Math.max(1.0, s*(on?0.16:0.14)); ctx.strokeStyle=col; ctx.stroke();
  }
  for(const sgn of [1,-1]){
    wing(sgn);
    if(on){ ctx.fillStyle=col; ctx.fill(); }
    else  { ctx.lineWidth=Math.max(1.2, s*0.16); ctx.strokeStyle=col; ctx.stroke(); }
  }
  if(on){ feathers(1); feathers(-1); }
  halo();
  ctx.restore();
}
// FIXED-TIMESTEP ACCUMULATOR (frame-rate independence). The sim (update()) advances in fixed 1/60s steps
// regardless of the display refresh rate. At 60Hz => exactly 1 step/frame = byte-identical to the old loop.
// At 144Hz it caps the sim to 60Hz (the old loop ran ~2.4x too fast there); under a lag spike it runs extra
// steps to catch up instead of playing in slow-motion (kills the recurring slow-mo bug class). Every timer &
// FX decay in update() is hardcoded 1/60 per call, so a true-60Hz cadence makes them all real-time correct.
const SIM_STEP=1000/60; // 16.667ms
let _simAcc=0, _simLast=0, _simInit=false;
// SPEED_TRIM globally paces the WHOLE sim (Diggy-tuned: 0.97 = 3% slower than true-60Hz = his "perfect pacing").
// Applied every step, so it slows flight/scroll/everything uniformly and stays frame-rate independent.
const SPEED_TRIM=0.97;
// "Game speed" setting. DEFAULT (1x) = the frame-rate-locked cadence (the feel Diggy likes; identical on every
// monitor). The old website build ran once-per-frame = refresh/60 too fast on a high-Hz monitor; once this build
// ships it REPLACES that, so the web version will feel like it does here too. 1.5/2/2.5 are opt-in faster runs.
const SPD_OPTS=[{v:1,l:"1x"},{v:1.5,l:"1.5x"},{v:2,l:"2x"},{v:2.5,l:"2.5x"}];
function _speedMul(){ const s=+save.speed||1; return (s>1 && (state==="fly"||state==="infurno"||state==="gauntlet"||state==="abduct")) ? s : 1; }
function stepSim(){
  const now=performance.now();
  if(!_simInit){ _simLast=now; _simInit=true; }
  if(document.hidden){ _simLast=now; _simAcc=0; return; } // matches the old !document.hidden gate; hold the clock so returning doesn't fast-forward a huge gap
  let elapsed=now-_simLast; _simLast=now;
  if(elapsed>250) elapsed=250;                 // spiral-of-death clamp: catch up at most ~15 steps in one frame
  _simAcc+=elapsed*SPEED_TRIM*_speedMul();
  let n=0; while(_simAcc>=SIM_STEP && n<20){ update(); _simAcc-=SIM_STEP; n++; } // remainder carries to next frame (smooths)
}
function frame(){
  const t=performance.now()*0.001;
  updateAbilityBtn();
  document.body.dataset.scState=state; // drives touch-control visibility (aim -> AIM pad, infurno/gauntlet -> MOVE pad)
  const biome=biomeAt(distM); ACX=biome.ac;
  INTEN=intensityAt(distM); ODX=overdriveAt(distM); _pulse=pulseAt(t); // escalating-intensity per-frame scalars
  document.documentElement.style.setProperty("--ac", biome.ac);
  if(state==="fly"){ if(biome.name!==lastBiome){ if(lastBiome!==""){ triggerWorldEntry(biome.name, t, biome); } lastBiome=biome.name; } }
  else lastBiome=biome.name;
  ctx.setTransform(1,0,0,1,0,0); ctx.clearRect(0,0,W,H);
  const sx=(Math.random()-0.5)*shake, sy=(Math.random()-0.5)*shake; ctx.translate(sx,sy);
  if(state==="infurno"){
    drawInfurnoBg(t, infurno?infurno.p:1);
    for(const c of infCoins){ if(!c.got) drawInfurnoCoin(c.x,c.y,c.r,t); }
    for(const h of infHaz){ drawInfHazard(h,t); }
    for(const p of parts){ ctx.globalAlpha=Math.max(0,p.life); ctx.fillStyle=p.c; ctx.beginPath(); ctx.arc(p.x,p.y,p.r,0,7); ctx.fill(); } ctx.globalAlpha=1;
    drawKitty();
    for(const q of popTexts){ ctx.globalAlpha=Math.max(0,q.life); ctx.fillStyle="#ffe9a8"; ctx.strokeStyle="rgba(0,0,0,.6)"; ctx.lineWidth=3*DPR; ctx.font=`900 ${15*DPR}px Orbitron`; ctx.textAlign="center"; ctx.strokeText(q.txt,q.x,q.y); ctx.fillText(q.txt,q.x,q.y); } ctx.globalAlpha=1;
    if(infurno){ ctx.save(); ctx.textAlign="center"; ctx.shadowColor="rgba(0,0,0,.7)"; ctx.shadowBlur=7*DPR;
      ctx.fillStyle="#ffd34d"; ctx.font=`900 ${24*DPR}px Orbitron`; ctx.fillText("JACKPOT  +"+infurno.coins, W/2, 44*DPR);
      const tl=Math.max(0,INFURNO_DUR-infurno.t); ctx.fillStyle="#fff"; ctx.font=`800 ${14*DPR}px Orbitron`; ctx.fillText(tl.toFixed(1)+"s", W/2, 66*DPR);
      ctx.fillStyle="rgba(255,255,255,.85)"; ctx.font=`700 ${13*DPR}px 'Saira Semi Condensed'`; ctx.fillText("TAP to fly  ·  ARROWS to move  ·  grab gold, DODGE the fire!", W/2, H-20*DPR); ctx.restore(); }
    if(bannerT>0){ ctx.save(); ctx.globalAlpha=Math.min(1,bannerT*1.5); ctx.textAlign="center"; const bw2=Math.min(W*0.8,520*DPR), cy=H*0.17;
      ctx.fillStyle="rgba(0,0,0,.5)"; roundRect(W/2-bw2/2, cy-28*DPR, bw2, 56*DPR, 12*DPR); ctx.fill();
      ctx.fillStyle="#ff9a3c"; ctx.font=`900 ${26*DPR}px Orbitron`; ctx.fillText(bannerName.toUpperCase(), W/2, cy+1*DPR);
      ctx.fillStyle="rgba(255,255,255,.85)"; ctx.font=`700 ${12*DPR}px 'Saira Semi Condensed'`; ctx.fillText("SECRET JACKPOT LEVEL", W/2, cy+20*DPR); ctx.restore(); bannerT-=0.006; }
    if(infurno && !infurno.ready){ ctx.save(); ctx.textAlign="center"; // INTRO: explain the jackpot + WAIT for the player to press any key (no auto-start)
      ctx.fillStyle="rgba(0,0,0,.5)"; ctx.fillRect(0,0,W,H); ctx.shadowColor="rgba(0,0,0,.7)"; ctx.shadowBlur=8*DPR;
      ctx.fillStyle="#ff9a3c"; ctx.font=`900 ${30*DPR}px Orbitron`; ctx.fillText("DANTE'S INFURNO", W/2, H*0.30);
      ctx.fillStyle="#fff"; ctx.font=`800 ${17*DPR}px 'Saira Semi Condensed'`;
      ctx.fillText("Grab the falling gold for a coin JACKPOT.", W/2, H*0.385);
      ctx.fillText("DODGE the fireballs - one burn ends the run!", W/2, H*0.43);
      ctx.fillStyle="rgba(255,255,255,.9)"; ctx.font=`700 ${14*DPR}px 'Saira Semi Condensed'`; ctx.fillText("TAP / SPACE to fly  ·  LEFT / RIGHT to move", W/2, H*0.48);
      const pulse=0.55+0.45*Math.sin(t*4.5); ctx.globalAlpha=pulse; ctx.fillStyle="#ffd34d"; ctx.font=`900 ${34*DPR}px Orbitron`;
      ctx.fillText("PRESS ANY KEY TO START", W/2, H*0.64); ctx.globalAlpha=1; ctx.restore(); }
    stepSim();
    requestAnimationFrame(frame);
    return;
  }
  if(state==="gauntlet"){
    const ty=gauntlet?gauntlet.type:"fire";
    drawGauntletBg(ty, t);
    if(ty==="gorilla" && gorilla) drawGorilla(gorilla,t);
    for(const h of gHaz){ if(h.kind==="fire") drawFlameColumn(h,t); else if(h.kind==="hand") drawZombieHand(h,t); else if(h.kind==="wraith") drawWraith(h,t); else if(h.kind==="rock") drawRock(h,t); else if(h.kind==="shock") drawShock(h,t); else if(h.kind==="pillar") drawPillar(h,t); else if(h.kind==="gcoin") drawGCoin(h,t); else drawShark(h,t); }
    for(const p of parts){ ctx.globalAlpha=Math.max(0,p.life); ctx.fillStyle=p.c; ctx.beginPath(); ctx.arc(p.x,p.y,p.r,0,7); ctx.fill(); } ctx.globalAlpha=1;
    drawKitty();
    for(const q of popTexts){ ctx.globalAlpha=Math.max(0,q.life); ctx.fillStyle="#ffe9a8"; ctx.strokeStyle="rgba(0,0,0,.6)"; ctx.lineWidth=3*DPR; ctx.font=`900 ${15*DPR}px Orbitron`; ctx.textAlign="center"; ctx.strokeText(q.txt,q.x,q.y); ctx.fillText(q.txt,q.x,q.y); } ctx.globalAlpha=1;
    if(gauntlet){ ctx.save(); ctx.textAlign="center"; ctx.shadowColor="rgba(0,0,0,.7)"; ctx.shadowBlur=7*DPR;
      const tl=Math.max(0,gauntlet.dur-gauntlet.t);
      ctx.fillStyle= ty==="fire"?"#ff6a2c":"#39c6ff"; ctx.font=`900 ${22*DPR}px Orbitron`; ctx.fillText(gauntlet.name, W/2, 44*DPR);
      ctx.fillStyle="#fff"; ctx.font=`900 ${20*DPR}px Orbitron`; ctx.fillText("SURVIVE  "+tl.toFixed(1)+"s", W/2, 70*DPR);
      ctx.fillStyle="rgba(255,255,255,.85)"; ctx.font=`700 ${13*DPR}px 'Saira Semi Condensed'`; ctx.fillText("TAP to flap  ·  ARROWS to move  ·  SURVIVE", W/2, H-20*DPR); ctx.restore(); }
    if(gauntlet && !gauntlet.ready){ ctx.save(); ctx.textAlign="center"; // INTRO: explain the level + WAIT for the player to press any key (no auto-start)
      ctx.fillStyle="rgba(0,0,0,.5)"; ctx.fillRect(0,0,W,H);
      ctx.shadowColor="rgba(0,0,0,.7)"; ctx.shadowBlur=8*DPR;
      ctx.fillStyle= ty==="fire"?"#ff6a2c":"#39c6ff"; ctx.font=`900 ${30*DPR}px Orbitron`; ctx.fillText(gauntlet.name, W/2, H*0.30);
      ctx.fillStyle="#fff"; ctx.font=`800 ${17*DPR}px 'Saira Semi Condensed'`;
      ctx.fillText(ty==="fire"?"Flame columns erupt from the ground.":ty==="zombie"?"Hands claw up from below — and wraiths dive from above.":ty==="gorilla"?"Outrun the silverback! Fly through the temple gaps.":"Sharks leap in from the sides.", W/2, H*0.385);
      ctx.fillText("DODGE everything and survive "+GAUNTLET_DUR+"s for a 5000-coin bonus!", W/2, H*0.43);
      ctx.fillStyle="rgba(255,255,255,.9)"; ctx.font=`700 ${14*DPR}px 'Saira Semi Condensed'`; ctx.fillText("TAP / SPACE to flap  ·  LEFT / RIGHT to move", W/2, H*0.48);
      const pulse=0.55+0.45*Math.sin(t*4.5); ctx.globalAlpha=pulse; ctx.fillStyle= ty==="fire"?"#ffd34d":"#bfefff"; ctx.font=`900 ${34*DPR}px Orbitron`;
      ctx.fillText("PRESS ANY KEY TO START", W/2, H*0.64); ctx.globalAlpha=1; ctx.restore(); }
    if(bannerT>0){ ctx.save(); ctx.globalAlpha=Math.min(1,bannerT*1.5); ctx.textAlign="center"; const bw2=Math.min(W*0.8,520*DPR), cy=H*0.17;
      ctx.fillStyle="rgba(0,0,0,.5)"; roundRect(W/2-bw2/2, cy-28*DPR, bw2, 56*DPR, 12*DPR); ctx.fill();
      ctx.fillStyle= ty==="fire"?"#ff5a2a":"#39c6ff"; ctx.font=`900 ${26*DPR}px Orbitron`; ctx.fillText(bannerName.toUpperCase(), W/2, cy+1*DPR);
      ctx.fillStyle="rgba(255,255,255,.85)"; ctx.font=`700 ${12*DPR}px 'Saira Semi Condensed'`; ctx.fillText("LETHAL SURVIVAL LEVEL", W/2, cy+20*DPR); ctx.restore(); bannerT-=0.006; }
    if(flashT>0){ ctx.fillStyle="rgba(255,40,70,"+Math.min(.4,flashT*0.4)+")"; ctx.fillRect(-30,-30,W+60,H+60); }
    if(winFlashT>0){ ctx.save(); ctx.globalCompositeOperation="lighter"; const wg=ctx.createRadialGradient(W/2,H/2,0,W/2,H/2,Math.max(W,H)*0.75); const wa=Math.min(0.6,winFlashT*0.6); wg.addColorStop(0,"rgba(255,235,150,"+wa+")"); wg.addColorStop(0.5,"rgba(255,200,70,"+(wa*0.55)+")"); wg.addColorStop(1,"rgba(255,180,50,0)"); ctx.fillStyle=wg; ctx.fillRect(-30,-30,W+60,H+60); ctx.restore(); }
    stepSim();
    requestAnimationFrame(frame);
    return;
  }
  // FOLLOW-CAMERA: pan the whole world layer up by camY so a high-flying kitty stays on screen. Fill the
  // revealed top strip with the sky's top colour first so there's no gap. HUD (arrows/banners/distance)
  // is drawn AFTER ctx.restore() so it stays screen-fixed.
  if(camY>0.5){ // paint a full-screen sky backdrop so panning up never reveals black; the translated world's
                // own sky/ground draws on top of it (and slides off the bottom as you climb).
    const sky=biome.sky||["#0a0a16","#0a0a16"]; const g=ctx.createLinearGradient(0,0,0,H);
    g.addColorStop(0, sky[0]); g.addColorStop(1, sky[Math.min(1,sky.length-1)]||sky[0]);
    ctx.fillStyle=g; ctx.fillRect(0,0,W,H); }
  ctx.save(); ctx.translate(0, camY);
  drawBackground(t,biome);
  drawObstacles(t);
  if(baccaAxe) drawBaccaAxe(t);
  drawRival(t);
  drawCannon();
  drawReboot();
  drawTrajectory();
  for(const p of parts){ ctx.globalAlpha=Math.max(0,p.life); ctx.fillStyle=p.c; ctx.beginPath(); ctx.arc(p.x-cam,p.y,p.r,0,7); ctx.fill(); } ctx.globalAlpha=1;
  if(wallFx){ const life=wallFx.life, a=Math.max(0,Math.min(1,life/0.6)), g0=GROUND(); const wx0=wallFx.x0-cam, wx1=wallFx.x1-cam, ww=wx1-wx0;
    const rise=Math.max(0,Math.min(1,(0.6-life)/0.12)), topY=g0-g0*rise; ctx.save(); ctx.globalAlpha=a; // BUILD THE WALL: a golden brick wall slams up ahead of you, then fades
    const bgg=ctx.createLinearGradient(wx0,0,wx1,0); bgg.addColorStop(0,"rgba(255,196,60,.16)"); bgg.addColorStop(0.85,"rgba(255,224,120,.30)"); bgg.addColorStop(1,"rgba(255,255,210,.5)"); ctx.fillStyle=bgg; ctx.fillRect(wx0,topY,ww,g0-topY);
    const bh=22*DPR, bw=46*DPR;
    for(let by=g0; by>topY; by-=bh){ const row=Math.round((g0-by)/bh), off=(row%2)?bw/2:0;
      for(let bx=wx0-off; bx<wx1; bx+=bw){ const xx=Math.max(wx0,bx), w=Math.min(wx1,bx+bw-3*DPR)-xx; if(w<=0) continue;
        const fgg=ctx.createLinearGradient(0,by-bh,0,by); fgg.addColorStop(0,"#ffd76b"); fgg.addColorStop(1,"#df9626"); ctx.fillStyle=fgg; ctx.fillRect(xx,by-bh+2*DPR,w,bh-3*DPR);
        ctx.fillStyle="rgba(255,255,214,.45)"; ctx.fillRect(xx,by-bh+2*DPR,w,2*DPR); ctx.fillStyle="rgba(110,64,8,.32)"; ctx.fillRect(xx,by-3*DPR,w,2*DPR); } }
    ctx.globalCompositeOperation="lighter"; const eg=ctx.createLinearGradient(wx1-32*DPR,0,wx1,0); eg.addColorStop(0,"rgba(255,240,160,0)"); eg.addColorStop(1,"rgba(255,255,220,"+(a*0.85)+")"); ctx.fillStyle=eg; ctx.fillRect(wx1-32*DPR,topY,32*DPR,g0-topY);
    ctx.restore(); }
  for(const s of shockwaves){ ctx.save(); ctx.globalAlpha=Math.max(0,s.life)*0.8; ctx.globalCompositeOperation="lighter";
    if(s.rainbow){ const hue=(performance.now()*0.22 + s.r*1.4)%360; // rainbow IMPULSE ring (THE DROP)
      ctx.strokeStyle="hsla("+hue+",100%,60%,.95)"; ctx.lineWidth=6*DPR; ctx.beginPath(); ctx.arc(s.x-cam,s.y,s.r,0,7); ctx.stroke();
      ctx.strokeStyle="hsla("+((hue+120)%360)+",100%,64%,.85)"; ctx.lineWidth=3.5*DPR; ctx.beginPath(); ctx.arc(s.x-cam,s.y,s.r*0.74,0,7); ctx.stroke();
      ctx.strokeStyle="rgba(255,255,255,.8)"; ctx.lineWidth=1.6*DPR; ctx.beginPath(); ctx.arc(s.x-cam,s.y,s.r*0.5,0,7); ctx.stroke(); }
    else if(s.scream){ const a=Math.max(0,s.life); // SONIC SCREAM: layered violet -> cyan -> white sound rings
      ctx.strokeStyle="rgba(150,120,255,"+(0.5*a)+")"; ctx.lineWidth=8*DPR; ctx.beginPath(); ctx.arc(s.x-cam,s.y,s.r,0,7); ctx.stroke();
      ctx.strokeStyle="rgba(127,231,255,"+(0.9*a)+")"; ctx.lineWidth=4*DPR; ctx.beginPath(); ctx.arc(s.x-cam,s.y,s.r,0,7); ctx.stroke();
      ctx.strokeStyle="rgba(255,255,255,"+(0.75*a)+")"; ctx.lineWidth=1.8*DPR; ctx.beginPath(); ctx.arc(s.x-cam,s.y,s.r*0.82,0,7); ctx.stroke(); }
    else { ctx.strokeStyle="rgba(127,231,255,.9)"; ctx.lineWidth=4*DPR; ctx.beginPath(); ctx.arc(s.x-cam,s.y,s.r,0,7); ctx.stroke(); ctx.strokeStyle="rgba(255,255,255,.7)"; ctx.lineWidth=2*DPR; ctx.beginPath(); ctx.arc(s.x-cam,s.y,s.r*0.7,0,7); ctx.stroke(); }
    ctx.restore(); } // SONIC SCREAM / rainbow-impulse shockwave rings
  for(const pf of portalFx){ const px=pf.x-cam, py=pf.y, life=Math.max(0,pf.t); ctx.save(); ctx.globalCompositeOperation="lighter"; ctx.globalAlpha=life; // portal warp swirls: "in" collapses, "out" pops open where you reappear
    const baseR=(pf.k==="out"?48:38)*DPR, r=pf.k==="out"?baseR*(1.5-life*1.0):baseR*life;
    for(let i=0;i<3;i++){ ctx.strokeStyle=i%2?"rgba(40,230,255,.9)":"rgba(180,120,255,.9)"; ctx.lineWidth=(4-i)*DPR; ctx.beginPath(); ctx.arc(px,py,Math.max(1,r*(1-i*0.22)),0,7); ctx.stroke(); }
    ctx.fillStyle="rgba(240,230,255,"+(life*0.8)+")"; ctx.beginPath(); ctx.arc(px,py,Math.max(1,r*0.2),0,7); ctx.fill(); ctx.restore(); }
  if(ab()==="bartman" && abHeld && flightMeter>0) drawSkateboard(t);
  drawKitty();
  if(state==="fly") drawPassiveHud(t); // ability feedback (lightning meter / fallout pips / dragon regen)
  biomeFX(biome,t);
  if((state==="abduct"||state==="redeploy") && ufo){ const cx=ufo.sx; drawTractorBeam(cx, ufo.ufoY+18*DPR, Math.min(GROUND(), kitty.y+12*DPR), ufo.beam, t); drawBigUfo(cx, ufo.ufoY, t, 1.15); }
  for(const q of popTexts){ ctx.globalAlpha=Math.max(0,q.life); ctx.fillStyle="#fff"; ctx.strokeStyle="rgba(0,0,0,.6)"; ctx.lineWidth=3*DPR; ctx.font=`900 ${15*DPR}px Orbitron`; ctx.textAlign="center"; ctx.strokeText(q.txt,q.x-cam,q.y); ctx.fillText(q.txt,q.x-cam,q.y); } ctx.globalAlpha=1;
  ctx.restore(); // end follow-camera; HUD below is screen-fixed
  // off-screen-up arrow: only if the kitty is STILL above the view after the camera pan (rare now)
  if(state==="fly" && (kitty.y+camY)<30*DPR){ const ax=Math.max(40*DPR,Math.min(W-40*DPR,kitty.x-cam)), alt=Math.max(0,Math.floor((30*DPR-(kitty.y+camY))/(24*DPR)));
    ctx.save(); ctx.textAlign="center"; ctx.fillStyle=biome.ac; ctx.beginPath(); ctx.moveTo(ax,12*DPR); ctx.lineTo(ax-12*DPR,32*DPR); ctx.lineTo(ax+12*DPR,32*DPR); ctx.closePath(); ctx.fill();
    ctx.fillStyle="#fff"; ctx.shadowColor="rgba(0,0,0,.7)"; ctx.shadowBlur=5*DPR; ctx.font=`800 ${13*DPR}px Orbitron`; ctx.fillText(alt+"m up", ax, 52*DPR); ctx.restore(); }
  if(state==="fly"){ ctx.save(); ctx.shadowColor="rgba(0,0,0,.7)"; ctx.shadowBlur=6*DPR; ctx.textAlign="left";
    ctx.font=`800 ${14*DPR}px Orbitron`; ctx.fillStyle="rgba(255,255,255,.7)"; ctx.fillText("LIVES", 22*DPR, H-48*DPR);
    const _hxx=82*DPR, _hyy=H-48*DPR, _ml=maxLives(); ctx.font=`800 ${16*DPR}px Orbitron`; ctx.fillStyle="rgba(255,255,255,.22)"; ctx.fillText("♥".repeat(_ml), _hxx, _hyy); ctx.fillStyle="#ff5470"; ctx.fillText("♥".repeat(Math.max(0,Math.min(_ml,lives))), _hxx, _hyy);
    ctx.fillStyle="#fff"; ctx.font=`800 ${13*DPR}px Orbitron`; ctx.fillText("FLAPS", 22*DPR, H-24*DPR);
    const _tf=baseFlaps(); for(let _w=0;_w<_tf;_w++) drawWing((104+_w*24)*DPR, H-30*DPR, 8*DPR, _w<flapsLeft);
    if(ab()==="chrome"){ const ay=H-72*DPR; ctx.fillStyle="rgba(207,224,238,.85)"; ctx.font=`800 ${13*DPR}px Orbitron`; ctx.fillText("ARMOR", 22*DPR, ay+5*DPR); // Chrome plates above the hearts
      for(let i=0;i<3;i++){ const px=(90+i*17)*DPR, on=i<chromePlates; if(on){ const sh=ctx.createLinearGradient(px-6*DPR,ay-6*DPR,px+6*DPR,ay+6*DPR); sh.addColorStop(0,"#ffffff"); sh.addColorStop(.5,"#b6c6d6"); sh.addColorStop(1,"#7f93a6"); ctx.fillStyle=sh; ctx.beginPath(); ctx.arc(px,ay,6.2*DPR,0,7); ctx.fill(); ctx.strokeStyle="rgba(255,255,255,.9)"; ctx.lineWidth=1.4*DPR; ctx.stroke(); } else { ctx.fillStyle="rgba(255,255,255,.14)"; ctx.beginPath(); ctx.arc(px,ay,5.6*DPR,0,7); ctx.fill(); ctx.strokeStyle="rgba(255,255,255,.3)"; ctx.lineWidth=1.2*DPR; ctx.stroke(); } } }
    else if(ab()==="turtle"){ const ay=H-72*DPR; ctx.fillStyle=shielded?"rgba(120,240,170,.9)":"rgba(160,200,175,.8)"; ctx.font=`800 ${13*DPR}px Orbitron`; ctx.fillText("SHELL", 22*DPR, ay+5*DPR); // Turtle shell above the hearts
      ctx.save(); ctx.translate(96*DPR,ay); if(shielded){ const g=ctx.createLinearGradient(0,-8*DPR,0,8*DPR); g.addColorStop(0,"#7ef0a6"); g.addColorStop(.55,"#33c86f"); g.addColorStop(1,"#1c8f4c"); ctx.fillStyle=g; ctx.beginPath(); ctx.arc(0,3*DPR,9*DPR,Math.PI,2*Math.PI); ctx.closePath(); ctx.fill(); ctx.strokeStyle="rgba(9,60,30,.85)"; ctx.lineWidth=1.4*DPR; ctx.stroke(); ctx.beginPath(); ctx.moveTo(0,-6*DPR); ctx.lineTo(0,3*DPR); ctx.moveTo(-5*DPR,-2*DPR); ctx.lineTo(-6*DPR,3*DPR); ctx.moveTo(5*DPR,-2*DPR); ctx.lineTo(6*DPR,3*DPR); ctx.stroke(); } else { ctx.strokeStyle="rgba(120,220,160,.35)"; ctx.lineWidth=1.5*DPR; ctx.beginPath(); ctx.arc(0,3*DPR,9*DPR,Math.PI,2*Math.PI); ctx.closePath(); ctx.stroke(); ctx.strokeStyle="#3ad17a"; ctx.lineWidth=2.6*DPR; ctx.lineCap="round"; ctx.beginPath(); ctx.arc(0,0,11*DPR,-Math.PI/2,-Math.PI/2+Math.min(1,turtleRegen/10)*2*Math.PI); ctx.stroke(); } ctx.restore(); }
    ctx.restore(); }
  if(bannerT>0){ ctx.save(); ctx.globalAlpha=Math.min(1,bannerT*1.5); ctx.textAlign="center";
    const isHS=bannerName==="NEW HIGH SCORE!", bw2=Math.min(W*0.7, 470*DPR), cy=H*0.17;
    ctx.fillStyle="rgba(0,0,0,.5)"; roundRect(W/2-bw2/2, cy-28*DPR, bw2, 56*DPR, 12*DPR); ctx.fill();
    ctx.fillStyle=isHS?"#ffd34d":biome.ac; ctx.font=`900 ${26*DPR}px Orbitron`; ctx.fillText(bannerName.toUpperCase(), W/2, cy+1*DPR);
    ctx.fillStyle="rgba(255,255,255,.85)"; ctx.font=`700 ${12*DPR}px 'Saira Semi Condensed'`; ctx.fillText((isHS?"★  ":"NEW BIOME  ·  ")+distM+"m", W/2, cy+20*DPR);
    ctx.restore(); bannerT-=0.006; }
  drawWorldEntry(t); // NEW-WORLD ceremony (world-change only; independent of bannerT)
  if(slowT>0){ const a=Math.min(1,slowT/0.5); ctx.save(); ctx.fillStyle="rgba(150,90,255,"+(a*(0.08+0.03*Math.sin(t*7)))+")"; ctx.fillRect(-30,-30,W+60,H+60); // SLOWED & THROWED woozy purple tint (lightened so the player isn't washed out)
    const vg=ctx.createRadialGradient(W/2,H/2,H*0.25,W/2,H/2,H*0.75); vg.addColorStop(0,"rgba(120,60,220,0)"); vg.addColorStop(1,"rgba(90,40,180,"+(a*0.20)+")"); ctx.fillStyle=vg; ctx.fillRect(0,0,W,H); ctx.restore(); }
  if(flashT>0){ ctx.fillStyle="rgba(255,40,70,"+Math.min(.4,flashT*0.4)+")"; ctx.fillRect(-30,-30,W+60,H+60); }
  stepSim();
  requestAnimationFrame(frame);
}
requestAnimationFrame(frame);

function show(id,on){ const el=document.getElementById(id); el.classList.toggle("hidden",!on); if(on&&id==="startCard"){ const sp=el.querySelector(".sc-startpanel"); if(sp) sp.classList.toggle("sc-seethru", Math.random()<0.5); refreshStartCard(); } } // ~50% of the time the start card goes see-through so the live meadow shows behind it
let prevState="start";
function toggleShop(){ if(state==="shop") closeShopFn(); else openShop(); }
// ===== RARITY SYSTEM (item-shop redesign) =====
const RARITY = {
  common:    { name:"COMMON",    c1:"#9fb0c9", c2:"#5d6b85", glow:"rgba(150,170,200,.30)" },
  rare:      { name:"RARE",      c1:"#37a6ff", c2:"#1f6dff", glow:"rgba(45,130,255,.45)"  },
  epic:      { name:"EPIC",      c1:"#c06bff", c2:"#7a35ff", glow:"rgba(150,80,255,.52)"  },
  legendary: { name:"LEGENDARY", c1:"#ffc24d", c2:"#ff8a1e", glow:"rgba(255,150,40,.55)"  },
  mythic:    { name:"MYTHIC",    c1:"#ffe39a", c2:"#b97bff", glow:"rgba(255,210,120,.55)" }
};
const RARITY_RANK = { common:1, rare:2, epic:3, legendary:4, mythic:5 };
function rarityOf(sk){
  if(sk.rarity && RARITY_RANK[sk.rarity]) return sk.rarity; // explicit rarity from abilities.json wins
  if(sk.secret) return "mythic";
  if(sk.id==="default" || !sk.cost) return "common";
  const c = sk.cost||0, hasAb = !!sk.ability;
  if(hasAb) return c>=6500 ? "legendary" : "epic";   // ability skins: epic, top-priced = legendary
  if(c>=4000) return "legendary";                     // premium cosmetic
  if(c>=2000) return "epic";
  return "rare";                                       // standard cosmetic (1000) = rare
}
// ---- secret-skin unlock engine (per-skin conditions from abilities.json) ----
function ownedAbilCount(){ return save.owned.filter(id=>{ const s=skinList.find(x=>x.id===id); return s&&s.ability; }).length; }
function ownedSkinCount(){ return save.owned.filter(id=>id!=="default").length; }
function unlockMet(sk){ const u=sk.unlock; if(!u) return ownedAbilCount()>=5; // legacy default
  const v=+u.value||0;
  switch(u.type){
    case "own_ability_skins": return ownedAbilCount()>=v;
    case "own_skins":         return ownedSkinCount()>=v;
    case "total_runs":        return (save.runs||0)>=v;
    case "reach_distance":    return (save.best||0)>=v;
    case "best_distance":     return (save.best||0)>=v;
    case "posted_score":      return !!save.posted && (save.best||0) >= (v||1);
    case "manual":            return false;
    default:                  return ownedAbilCount()>=v;
  }
}
function unlockHint(sk){ return (sk.unlock&&sk.unlock.label) ? sk.unlock.label : ""; } // empty label = no hint shown (mystery), by design
let _shopFilter="all", _shopSort="rarity";
function openShop(){ if(state!=="start"&&state!=="result") return; prevState=state; buildShop(); show("shopCard",true); state="shop"; } // no shop mid-run
function closeShopFn(){ show("shopCard",false); state=prevState; if(state==="start") show("startCard",true); else if(state==="result") show("resultCard",true); }
function pickableStyles(sk){ return (sk.styles||[]).filter(st=>st.id!=="fastboi__super"); } // fastboi__super is the GodSpeed transform, not a chooseable cosmetic
function buildShop(){
  const grid=document.getElementById("skinGrid"); grid.innerHTML="";
  document.getElementById("coins").textContent=save.coins+runCoins;
  const wEl=document.getElementById("shopWallet"); if(wEl) wEl.textContent=(save.coins+runCoins).toLocaleString();
  (function(){ // collection progress (goal-gradient dopamine)
    const nonSecret=skinList.filter(s=>!s.secret), secretList=skinList.filter(s=>s.secret);
    const ownNS=nonSecret.filter(s=>DEV||save.owned.includes(s.id)).length;
    const ownSec=secretList.filter(s=>DEV||save.owned.includes(s.id)).length;
    const remain=Math.max(0, nonSecret.length-ownNS);
    const pct=nonSecret.length? Math.round(100*ownNS/nonSecret.length):100, done=remain===0;
    const wrap=document.getElementById("shopProgress"); if(wrap) wrap.classList.toggle("done",done);
    const cEl=document.getElementById("shopProgCount"); if(cEl) cEl.textContent= done? "COMPLETE ✦" : (ownNS+" / "+nonSecret.length);
    const fEl=document.getElementById("shopProgFill"); if(fEl) fEl.style.width=pct+"%";
    const nEl=document.getElementById("shopProgNudge");
    if(nEl){ let s = done ? "Every sprite collected — you own them all" : (remain+" more to unlock everything");
      if(secretList.length) s += " · <span class='sec'>+ "+ownSec+"/"+secretList.length+" secret</span>"; nEl.innerHTML=s; }
  })();
  let ordered=skinList.slice();
  // only LOCKED secrets sink to the bottom; once a secret is owned/revealed it sorts by its real rarity like any skin
  const lockedSec=s=>!!s.secret && !(DEV||save.owned.includes(s.id)) && !unlockMet(s);
  if(_shopSort==="price"){
    ordered.sort((a,b)=>{ if(a.id==="default") return -1; if(b.id==="default") return 1;
      const la=lockedSec(a), lb=lockedSec(b); if(la&&!lb) return 1; if(lb&&!la) return -1; return (a.cost||0)-(b.cost||0); });
  } else {
    ordered.sort((a,b)=>{ if(a.id==="default") return -1; if(b.id==="default") return 1;
      const la=lockedSec(a), lb=lockedSec(b); if(la&&!lb) return 1; if(lb&&!la) return -1;
      const dr=RARITY_RANK[rarityOf(a)]-RARITY_RANK[rarityOf(b)]; if(dr) return dr; return (a.cost||0)-(b.cost||0); }); // ascending: least-rare on top -> rarest at the bottom
  }
  let shown=0;
  for(const sk of ordered){
    const owned=(DEV||save.owned.includes(sk.id)), sel=save.skin===sk.id;
    const locked = !!sk.secret && !owned && !unlockMet(sk);
    const affordable = !owned && !locked && (save.coins+runCoins)>=sk.cost;
    if(_shopFilter==="owned"   && !owned)      continue;
    if(_shopFilter==="afford"  && !affordable) continue;
    if(_shopFilter==="ability" && !sk.ability) continue;
    shown++;
    const rar=rarityOf(sk), pal=RARITY[rar];
    const el=document.createElement("div");
    el.className="skin"+(sel?" sel":"")+(locked?" locked":"")+(owned?" owned":"")+(affordable?" afford":"")+(sk.secret?" secret":"");
    el.dataset.rarity=rar;
    el.style.setProperty("--rc1",pal.c1); el.style.setProperty("--rc2",pal.c2); el.style.setProperty("--rg",pal.glow);
    const rib=document.createElement("div"); rib.className="sc-rar"; rib.textContent=pal.name; el.appendChild(rib);
    if(owned){ const bdg=document.createElement("div"); bdg.className="sc-badge"; bdg.textContent="✓"; el.appendChild(bdg); }
    const hideSecret = !!sk.secret && !owned && !unlockMet(sk);
    const thumb=document.createElement("div"); thumb.className="thumb";
    if(sk.img){ const c=document.createElement("canvas"); c.width=c.height=84; const cx=c.getContext("2d"); const img=skinImgFor(sk.id)||sk.img; // show the owner's chosen style variant
      const bx=skinBoxFor(sk.id), cw=bx?bx.w:img.width, ch=bx?bx.h:img.height, ccx=bx?(bx.x+bx.w/2):img.width/2, ccy=bx?(bx.y+bx.h/2):img.height/2;
      const area=Math.sqrt(cw*ch); let s=70/area; const big=Math.max(cw,ch)*s; if(big>80) s*=80/big;
      cx.imageSmoothingEnabled=true; cx.drawImage(img, 42-ccx*s, 42-ccy*s, img.width*s, img.height*s);
      if(hideSecret){ cx.globalCompositeOperation="source-atop"; cx.fillStyle="#03040a"; cx.fillRect(0,0,84,84); cx.globalCompositeOperation="source-over"; } // pokemon-style solid black silhouette (shape only, no colors)
      thumb.appendChild(c); }
    else { const c=document.createElement("canvas"); c.width=c.height=84; const cx=c.getContext("2d"); cx.translate(42,46); drawKittyArt(cx,21); thumb.appendChild(c); }
    el.appendChild(thumb);
    const nm=document.createElement("div"); nm.className="nm"; nm.textContent= hideSecret? "???" : sk.name; el.appendChild(nm);
    if(sk.ability && !hideSecret){ const abl=document.createElement("div"); abl.className="sc-ab"; abl.textContent="⚡ "+(ABILITY_LABELS[sk.ability]||sk.ability); el.appendChild(abl); }
    if(!hideSecret && pickableStyles(sk).length>1){ const stb=document.createElement("div"); stb.className="sc-styles-badge"; stb.textContent="◐ "+pickableStyles(sk).length+" STYLES"; el.appendChild(stb); } // surface that this skin has multiple chooseable looks
    const cost=document.createElement("div"); cost.className="cost"+(owned?" owned":"");
    if(sk.secret && locked){
      const line=document.createElement("div"); line.className="sc-line"; line.innerHTML="🔒 "+pal.name; cost.appendChild(line);
      const h=unlockHint(sk); if(h){ const tg=document.createElement("div"); tg.className="sc-togo"; tg.textContent=h; cost.appendChild(tg); }
    } else if(owned){
      const line=document.createElement("div"); line.className="sc-line"; line.textContent= sel?"EQUIPPED":"OWNED"; cost.appendChild(line);
    } else {
      const line=document.createElement("div"); line.className="sc-line"; line.innerHTML=(sk.cost>0? sk.cost+" <span class='sc-coin'>⛁</span>" : "UNLOCKED ✦"); cost.appendChild(line);
      const have=save.coins+runCoins; if(sk.cost>0 && have<sk.cost){ const tg=document.createElement("div"); tg.className="sc-togo"; tg.textContent=(sk.cost-have)+" to go"; cost.appendChild(tg); }
    }
    el.appendChild(cost);
    el.onclick=()=>{ snd("ui"); openSkinDetail(sk); };
    grid.appendChild(el);
  }
  if(!shown){ const e=document.createElement("div"); e.className="sc-empty"; e.textContent="Nothing here yet — keep flying to earn coins."; grid.appendChild(e); }
}
// ---- unlock celebration (anticipation -> reward juice) ----
let _unlockTO=null, _unlockRAF=null;
function _acHex(){ const v=getComputedStyle(document.documentElement).getPropertyValue('--ac').trim(); return v||"#22d3a0"; }
function drawUnlockSkin(skId){
  const cv=document.getElementById("unlockPreview"), D=Math.min(2,window.devicePixelRatio||1), SZ=200;
  cv.width=SZ*D; cv.height=SZ*D; cv.style.width=cv.style.height=SZ+"px";
  const g=cv.getContext("2d"); g.setTransform(1,0,0,1,0,0); g.clearRect(0,0,cv.width,cv.height);
  const img=skinImgs[skId];
  if(img){ const bx=skinBox[skId], cw=bx?bx.w:img.width, ch=bx?bx.h:img.height, ccx=bx?(bx.x+bx.w/2):img.width/2, ccy=bx?(bx.y+bx.h/2):img.height/2;
      const s=(SZ*D*0.62)/Math.max(cw,ch); g.imageSmoothingEnabled=true; g.drawImage(img, cv.width/2-ccx*s, cv.height/2-ccy*s, img.width*s, img.height*s); }
  else { const r=SZ*D*0.30; g.translate(cv.width/2, cv.height/2+0.29*r); drawKittyArt(g,r); }
}
function _burstConfetti(big){
  const cv=document.getElementById("unlockFx"), D=Math.min(2,window.devicePixelRatio||1);
  const _cw=cv.clientWidth||innerWidth, _ch=cv.clientHeight||innerHeight; // measure the actual full-screen box (CSS inset:0); innerWidth/Height are stale on iOS standalone PWAs -> off-center/clipped confetti
  cv.width=Math.floor(_cw*D); cv.height=Math.floor(_ch*D);
  const g=cv.getContext("2d"), cx=cv.width/2, cy=cv.height*0.42;
  const cols=[_acHex(), "#ffd34d", "#ffffff", "#fff2c0"], N=big?170:95, P=[];
  for(let i=0;i<N;i++){ const a=Math.random()*6.283, sp=(2+Math.random()*7)*D*(big?1.3:1);
    P.push({x:cx,y:cy,vx:Math.cos(a)*sp,vy:Math.sin(a)*sp-3*D,life:1,rot:Math.random()*6.283,vr:(Math.random()-.5)*.4, w:(3+Math.random()*4)*D, h:(5+Math.random()*7)*D, c:cols[i%cols.length]}); }
  const t0=performance.now(); cancelAnimationFrame(_unlockRAF);
  (function frame(now){ const dt=Math.min(40, now-(frame._l||now))/16.67; frame._l=now;
    g.clearRect(0,0,cv.width,cv.height); let alive=false;
    for(const p of P){ p.life-=0.012*dt; if(p.life<=0) continue; alive=true; p.vy+=0.12*D*dt; p.vx*=0.99; p.x+=p.vx*dt; p.y+=p.vy*dt; p.rot+=p.vr*dt;
      g.save(); g.globalAlpha=Math.max(0,Math.min(1,p.life)); g.translate(p.x,p.y); g.rotate(p.rot); g.fillStyle=p.c; g.fillRect(-p.w/2,-p.h/2,p.w,p.h); g.restore(); }
    if(alive && (now-t0)<2600) _unlockRAF=requestAnimationFrame(frame); else g.clearRect(0,0,cv.width,cv.height);
  })(t0);
}
function closeUnlock(){ clearTimeout(_unlockTO); cancelAnimationFrame(_unlockRAF); show("unlockCard",false); }
function celebrateUnlock(skId, availableOnly){
  const sk=skinList.find(s=>s.id===skId); if(!sk){ snd("buy"); return; }
  const nonSecret=skinList.filter(s=>!s.secret);
  const ownNS=nonSecret.filter(s=>save.owned.includes(s.id)).length;
  const complete = !sk.secret && nonSecret.length>1 && ownNS>=nonSecret.length;
  document.getElementById("unlockTitle").textContent = availableOnly ? "SECRET UNLOCKED!" : (complete ? "COLLECTION COMPLETE!" : "UNLOCKED!");
  document.getElementById("unlockName").textContent = availableOnly ? (sk.name+" — claim it in the Shop") : (sk.name+" Sprite");
  const abEl=document.getElementById("unlockAbility");
  if(sk.ability){ abEl.style.display="inline-block"; abEl.textContent="⚡ "+(ABILITY_LABELS[sk.ability]||sk.ability); } else abEl.style.display="none";
  const card=document.getElementById("unlockCard"); card.classList.toggle("complete", complete);
  drawUnlockSkin(skId); show("unlockCard",true);
  const sh=document.getElementById("unlockShow"); sh.style.animation="none"; void sh.offsetWidth; sh.style.animation="";
  snd("buy"); _burstConfetti(complete);
  clearTimeout(_unlockTO); _unlockTO=setTimeout(closeUnlock, complete?2400:1600);
}
// ---- SECRET SKIN unlocks: auto-GRANT the free ones + a "SECRET UNLOCKED!" popup (grant skins are added free; buy skins just announce they're now claimable) ----
let _unlockQ=[], _flushing=false;
function checkUnlocks(){ if(!Array.isArray(save.seenUnlocks)) save.seenUnlocks=[]; let changed=false;
  for(const sk of skinList){ if(!sk.secret || save.owned.includes(sk.id) || !unlockMet(sk)) continue;
    if(sk.grant){ save.owned.push(sk.id); changed=true; _unlockQ.push({id:sk.id, grant:true}); }                       // auto-grant FREE
    else if(!save.seenUnlocks.includes(sk.id)){ save.seenUnlocks.push(sk.id); changed=true; _unlockQ.push({id:sk.id, grant:false}); } } // buy skin: announce once
  if(changed){ persist(); try{ buildShop(); }catch(e){} }
  _flushUnlocks(); }
function _flushUnlocks(){ if(_flushing || !_unlockQ.length) return; _flushing=true; const u=_unlockQ.shift();
  setTimeout(()=>{ _flushing=false; try{ celebrateUnlock(u.id, !u.grant); }catch(e){} _flushUnlocks(); }, 1100); } // stagger multiple unlocks ~1.1s apart
document.getElementById("unlockCard").onclick=closeUnlock;
let _detailSk=null;
function openSkinDetail(sk){
  _detailSk=sk;
  const owned=(DEV||save.owned.includes(sk.id)), sel=save.skin===sk.id;
  const locked = !!sk.secret && !owned && !unlockMet(sk);
  const cv=document.getElementById("detailPreview"), g=cv.getContext("2d");
  g.setTransform(1,0,0,1,0,0); g.clearRect(0,0,cv.width,cv.height);
  const hideSecret = !!sk.secret && !owned && !unlockMet(sk); // greyed cutout until unlocked
  const rar=rarityOf(sk), pal=RARITY[rar];
  const dp=document.getElementById("detailPanel"); if(dp){ dp.style.setProperty("--rc1",pal.c1); dp.style.setProperty("--rc2",pal.c2); dp.style.setProperty("--rg",pal.glow); }
  document.getElementById("skinDetailCard").dataset.rarity=rar;
  const drb=document.getElementById("detailRarity"); if(drb) drb.textContent= pal.name;
  const img=(owned?skinImgFor(sk.id):skinImgs[sk.id]); // owned: preview the chosen style variant
  if(img){ const bx=(owned?skinBoxFor(sk.id):skinBox[sk.id]), cw=bx?bx.w:img.width, ch=bx?bx.h:img.height, ccx=bx?(bx.x+bx.w/2):img.width/2, ccy=bx?(bx.y+bx.h/2):img.height/2; // center on alpha content box (gas_mask/applejuice etc.)
    const pad=18, avail=cv.width-pad*2, s=Math.min(avail/cw, avail/ch); g.imageSmoothingEnabled=true; g.drawImage(img, cv.width/2-ccx*s, cv.height/2-ccy*s, img.width*s, img.height*s);
    if(hideSecret){ g.globalCompositeOperation="source-atop"; g.fillStyle="#03040a"; g.fillRect(0,0,cv.width,cv.height); g.globalCompositeOperation="source-over"; } } // pokemon-style solid black silhouette
  else { const r=58; g.translate(cv.width/2, cv.height/2 + 0.29*r); drawKittyArt(g,r); }
  document.getElementById("detailName").textContent = hideSecret ? "???" : (sk.name+" Sprite"); // full collectible name in the showcase (on-brand for Sprite Cannon)
  const abEl=document.getElementById("detailAbility");
  if(sk.ability && !hideSecret){ abEl.style.display="block"; document.getElementById("detailAbilityName").textContent="⚡ "+(ABILITY_LABELS[sk.ability]||sk.ability); document.getElementById("detailAbilityDesc").textContent= sk.abilityDesc || ABILITY_DESC[sk.ability] || ""; }
  else abEl.style.display="none";
  const costEl=document.getElementById("detailCost"); const buyBtn=document.getElementById("detailBuy"), eqBtn=document.getElementById("detailEquip");
  buyBtn.style.display="none"; eqBtn.style.display="none"; costEl.style.color="#ffd34d";
  if(owned){ costEl.style.color="#7fe9b8"; costEl.textContent= sel ? "SELECTED" : "OWNED"; if(!sel) eqBtn.style.display="inline-flex"; }
  else if(locked){ costEl.style.color="#cdd6ea"; const h=unlockHint(sk); costEl.textContent= h || "🔒 Locked"; }
  else { const have=save.coins+runCoins; buyBtn.style.display="inline-flex"; buyBtn.innerHTML= sk.cost>0 ? ("&#10003; BUY "+sk.cost+" ⛁") : "&#10003; CLAIM"; costEl.textContent= sk.cost>0 ? (have<sk.cost ? (sk.cost-have)+" coins to go" : sk.cost+" ⛁") : "Unlocked ✦"; if(sk.cost>0 && have<sk.cost) costEl.style.color="#cdd6ea"; }
  const stylesEl=document.getElementById("detailStyles"); // Fortnite-style edit-styles: clickable chips for owned skins with >1 variant
  if(stylesEl){ stylesEl.innerHTML="";
    const stys=pickableStyles(sk);
    if(owned && stys.length>1 && !hideSecret){ stylesEl.style.display="flex";
      const cur=(save.style&&save.style[sk.id])||sk.id;
      for(const st of stys){ const chip=document.createElement("div"); chip.className="sc-chip"+((st.id===cur)?" sel":""); chip.textContent=st.name;
        chip.onclick=()=>{ snd("ui"); save.style[sk.id]=st.id; persist(); openSkinDetail(sk); buildShop(); }; stylesEl.appendChild(chip); } }
    else stylesEl.style.display="none"; }
  show("skinDetailCard",true);
}
document.getElementById("detailBack").onclick=()=>{ snd("ui"); show("skinDetailCard",false); };
document.getElementById("detailEquip").onclick=()=>{ snd("ui"); const sk=_detailSk; if(sk && (sk.id==="default"||save.owned.includes(sk.id))){ save.skin=sk.id; persist(); buildShop(); } else if(sk && DEV){ save.skin=sk.id; buildShop(); } show("skinDetailCard",false); }; // owned -> equip+persist; DEV -> equip in-memory only to test abilities (NOT persisted, so no free skin once ?dev is removed)
document.getElementById("detailBuy").onclick=()=>{ const sk=_detailSk; if(!sk) return;
  if(save.coins>=sk.cost){ snd("ui"); show("skinDetailCard",false); askBuy(sk); }
  else { const ce=document.getElementById("detailCost"); if(ce){ ce.classList.remove("sc-shake"); void ce.offsetWidth; ce.classList.add("sc-shake"); } snd("ui"); } };

document.getElementById("playBtn").onclick=()=>{ snd("ui"); reset(); };
document.getElementById("shopBtn").onclick=()=>{ snd("ui"); openShop(); };
document.getElementById("againBtn").onclick=()=>{ snd("ui"); reset(); };
document.getElementById("shop2Btn").onclick=()=>{ snd("ui"); openShop(); };
document.getElementById("questBtn").onclick=openQuests;
document.getElementById("quest2Btn").onclick=openQuests;
document.getElementById("questClose").onclick=closeQuests;
document.getElementById("worldsBtn").onclick=openWorlds;
document.getElementById("worlds2Btn").onclick=openWorlds;
document.getElementById("worldsClose").onclick=closeWorlds;
{ const _sd=document.getElementById("scDaily"); if(_sd) _sd.onclick=()=>{ if(!dailyState().claimed) claimDaily(); }; }
// insta-restart: tapping the results page anywhere OUTSIDE a button/input launches the next run (the page itself stays; Space does this too via press())
{ const _rc=document.getElementById("resultCard"); if(_rc) _rc.addEventListener("click",(e)=>{ if(state!=="result") return; if(e.target.closest(".pill")||e.target.closest("button")||e.target.closest("input")||e.target.closest("#resultRank")) return; snd("ui"); reset(); }); } // #resultRank holds the first-run name prompt (label/whitespace had no guard) -> don't restart when tapping it
document.getElementById("closeShop").onclick=()=>{ snd("ui"); closeShopFn(); };
document.getElementById("closeShopTop").onclick=()=>{ snd("ui"); closeShopFn(); };
(function(){ const fb=document.getElementById("shopFilters");
  if(fb){ fb.querySelectorAll(".sc-filter[data-f]").forEach(b=>{ b.onclick=()=>{ snd("ui");
    fb.querySelectorAll(".sc-filter[data-f]").forEach(x=>x.classList.remove("active")); b.classList.add("active");
    _shopFilter=b.dataset.f; buildShop(); }; }); }
  const sb=document.getElementById("shopSortBtn");
  if(sb){ sb.onclick=()=>{ snd("ui"); _shopSort=(_shopSort==="rarity")?"price":"rarity"; sb.dataset.sort=_shopSort;
    const sp=sb.querySelector("span"); if(sp) sp.textContent=(_shopSort==="rarity"?"RARITY":"PRICE"); buildShop(); }; }
})();
let _pendingBuy=null;
function askBuy(sk){ _pendingBuy=sk; document.getElementById("confirmText").innerHTML= sk.cost>0 ? ("Spend <b style='color:#ffd34d'>"+sk.cost+"</b> coins on <b style='color:var(--ac)'>"+sk.name+"</b>?") : ("Unlock <b style='color:var(--ac)'>"+sk.name+"</b>?"); show("confirmCard",true); }
document.getElementById("confirmYes").onclick=()=>{ const sk=_pendingBuy; _pendingBuy=null; show("confirmCard",false); if(sk && save.coins>=sk.cost){ save.coins-=sk.cost; save.owned.push(sk.id); save.skin=sk.id; persist(); buildShop(); celebrateUnlock(sk.id); } };
document.getElementById("confirmNo").onclick=()=>{ snd("ui"); _pendingBuy=null; show("confirmCard",false); };
document.getElementById("restartBtn").onclick=()=>{ snd("ui"); reset(); };
document.getElementById("settingsBtn").onclick=()=>{ snd("ui"); document.getElementById("settingsPanel").classList.toggle("hidden"); };
const SC_CODE_URL="https://spritecannon-codes.maxxtopia.workers.dev/redeem";
const IS_TWITCH_EXT = !!(window.Twitch && window.Twitch.ext); // hide off-platform coin sales inside the Twitch extension (Twitch bans it)
async function redeemCode(inpId,msgId){ const inp=document.getElementById(inpId||"redeemInput"), msg=document.getElementById(msgId||"redeemMsg");
  const code=(inp.value||"").trim().toUpperCase(); if(!code) return;
  msg.style.color="#cdd6ea"; msg.textContent="Checking...";
  try{ const r=await fetch(SC_CODE_URL,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({code,player:save.pid})});
    const j=await r.json();
    if(j.ok){ save.coins+=j.coins; persist(); inp.value=""; msg.style.color="#7ee59a"; msg.textContent="+"+j.coins+" coins!"; snd("newbest");
      const c=document.getElementById("coins"); if(c) c.textContent=save.coins+runCoins; if(state==="shop") buildShop(); }
    else { msg.style.color="#ff8a8a"; msg.textContent=j.error||"Invalid code"; snd("error"); }
  }catch(e){ msg.style.color="#ff8a8a"; msg.textContent="Network error - try again"; }
}
{ const rb=document.getElementById("redeemBtn"); if(rb) rb.onclick=()=>{ snd("ui"); redeemCode(); };
  const ri=document.getElementById("redeemInput"); if(ri) ri.addEventListener("keydown",e=>{ if(e.key==="Enter") redeemCode(); }); }
// ===== GET COINS: bundle purchase via manual payment + Maxx approve (hidden inside the Twitch ext). =====
const SC_BUY = {
  api: "https://spritecannon-codes.maxxtopia.workers.dev",
  methods: [
    { id:"cashapp", label:"Cash App", handle:"$YOURCASHTAG", note:"Add a note: <b>for food</b>" },
    { id:"paypal", label:"PayPal", handle:"paypal.me/YOURNAME", note:"Send as <b>Friends &amp; Family</b> &mdash; note: <b>for food</b>" }
  ],
  bundles: [ {id:"b1",usd:3,coins:3000}, {id:"b2",usd:6,coins:7000}, {id:"b3",usd:12,coins:16000,best:true}, {id:"b4",usd:25,coins:36000} ]
};
let _buyBundle=null, _buyMethod=SC_BUY.methods[0];
function _appliedBuys(){ try{ return JSON.parse(localStorage.getItem("sm.spritecannon.buysApplied")||"[]"); }catch(e){ return []; } }
function _markApplied(id){ try{ const a=_appliedBuys(); if(a.indexOf(id)<0){ a.push(id); localStorage.setItem("sm.spritecannon.buysApplied",JSON.stringify(a.slice(-200))); } }catch(e){} }
function renderBuy(){ const bw=document.getElementById("buyBundles"); if(!bw) return; bw.innerHTML="";
  SC_BUY.bundles.forEach(b=>{ const sel=_buyBundle&&_buyBundle.id===b.id; const el=document.createElement("div");
    el.style.cssText="position:relative;cursor:pointer;border-radius:12px;padding:12px 6px;background:"+(sel?"rgba(255,215,106,.16)":"rgba(255,255,255,.05)")+";border:2px solid "+(sel?"#ffd76a":(b.best?"rgba(255,215,106,.45)":"rgba(255,255,255,.12)"));
    el.innerHTML="<div style='font:900 16px Orbitron;color:#ffd76a'>"+b.coins.toLocaleString()+"</div><div style='font-size:9px;color:#9aa7bd;margin:-1px 0 5px'>coins</div><div style='font:800 13px Orbitron;color:#fff'>$"+b.usd+"</div>"+(b.best?"<div style='position:absolute;top:-8px;left:50%;transform:translateX(-50%);background:#ffd76a;color:#2a1a04;font:800 7px Orbitron;padding:2px 6px;border-radius:6px;white-space:nowrap'>BEST</div>":"");
    el.onclick=()=>{ snd("ui"); _buyBundle=b; renderBuy(); renderPay(); const p=document.getElementById("buyPay"); if(p)p.style.display=""; };
    bw.appendChild(el); }); }
function renderPay(){ const mw=document.getElementById("buyMethods"); if(!mw) return; mw.innerHTML="";
  SC_BUY.methods.forEach(m=>{ const sel=_buyMethod.id===m.id; const b=document.createElement("div"); b.textContent=m.label;
    b.style.cssText="cursor:pointer;font:800 11px Orbitron;padding:6px 13px;border-radius:8px;color:"+(sel?"#08110a":"#cdd6ea")+";background:"+(sel?"#ffd76a":"rgba(255,255,255,.08)");
    b.onclick=()=>{ snd("ui"); _buyMethod=m; renderPay(); }; mw.appendChild(b); });
  const instr=document.getElementById("buyInstr");
  if(instr&&_buyBundle){ instr.innerHTML="Send <b style='color:#ffd76a'>$"+_buyBundle.usd+"</b> to <b style='color:#ffd76a'>"+_buyMethod.handle+"</b><br>"+_buyMethod.note+"<br><span style='font-size:11px;color:#9aa7bd'>Then tap below &mdash; Diggy releases your "+_buyBundle.coins.toLocaleString()+" coins.</span>"; } }
async function submitBuyClaim(){ if(!_buyBundle) return; const st=document.getElementById("buyStatus"); st.style.color="#cdd6ea"; st.textContent="Notifying Diggy...";
  try{ const r=await fetch(SC_BUY.api+"/purchase/claim",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({pid:save.pid,name:save.name||"",usd:_buyBundle.usd,coins:_buyBundle.coins,method:_buyMethod.id})});
    const j=await r.json();
    if(j.ok){ st.style.color="#7ee59a"; st.innerHTML="Sent! After you pay, Diggy approves it and your coins appear here automatically."; snd("newbest"); }
    else { st.style.color="#ff8a8a"; st.textContent=j.error||"Could not submit - try again"; } }
  catch(e){ st.style.color="#ff8a8a"; st.textContent="Payment system not live yet - try later"; } }
async function pollBuys(){ if(!save||!save.pid) return;
  try{ const r=await fetch(SC_BUY.api+"/purchase/status?pid="+encodeURIComponent(save.pid)); if(!r.ok) return; const j=await r.json();
    if(j&&j.grants&&j.grants.length){ const applied=_appliedBuys(); let total=0, ackIds=[];
      j.grants.forEach(g=>{ if(applied.indexOf(g.id)<0){ total+=g.coins; _markApplied(g.id); ackIds.push(g.id); } });
      if(total>0){ save.coins+=total; persist(); try{updateCoinHUD();}catch(e){} if(state==="shop"){try{buildShop();}catch(e){}}
        const st=document.getElementById("buyStatus"); if(st){ st.style.color="#7ee59a"; st.textContent="+"+total.toLocaleString()+" coins added! Thank you."; } snd("newbest");
        try{ fetch(SC_BUY.api+"/purchase/ack",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({pid:save.pid,ids:ackIds})}); }catch(e){} } } }
  catch(e){} }
function openCoins(){ show("coinsCard",true); _buyBundle=null; const p=document.getElementById("buyPay"); if(p)p.style.display="none"; const st=document.getElementById("buyStatus"); if(st)st.textContent=""; const m=document.getElementById("redeemMsg2"); if(m)m.textContent=""; renderBuy(); pollBuys(); }
function closeCoins(){ show("coinsCard",false); }
{ const gb=document.getElementById("getCoinsBtn");
  if(gb && !IS_TWITCH_EXT){ gb.onclick=()=>{ snd("ui"); openCoins(); };
    // button appears only once the worker reports payments ENABLED (Diggy set a real handle) - keeps placeholder handles off live
    fetch(SC_BUY.api+"/purchase/config").then(r=>r.json()).then(cfg=>{ if(cfg&&cfg.enabled){ if(cfg.methods&&cfg.methods.length)SC_BUY.methods=cfg.methods; if(cfg.bundles&&cfg.bundles.length)SC_BUY.bundles=cfg.bundles; _buyMethod=SC_BUY.methods[0]; gb.style.display=""; } }).catch(function(){}); }
  const pb=document.getElementById("buyPaidBtn"); if(pb) pb.onclick=()=>{ snd("ui"); submitBuyClaim(); };
  const rb2=document.getElementById("redeemBtn2"); if(rb2) rb2.onclick=()=>{ snd("ui"); redeemCode("redeemInput2","redeemMsg2"); };
  const ri2=document.getElementById("redeemInput2"); if(ri2) ri2.addEventListener("keydown",e=>{ if(e.key==="Enter") redeemCode("redeemInput2","redeemMsg2"); });
  const cc=document.getElementById("closeCoins"); if(cc) cc.onclick=()=>{ snd("ui"); closeCoins(); }; }
if(!IS_TWITCH_EXT){ setInterval(()=>{ const cc=document.getElementById("coinsCard"); if((cc&&!cc.classList.contains("hidden"))||state==="shop") pollBuys(); }, 15000); }
{ const rvb=document.getElementById("reviveBtn"); if(rvb) rvb.onclick=()=>{ snd("ui"); doRevive(); };
  const rvn=document.getElementById("reviveNo"); if(rvn) rvn.onclick=()=>{ snd("ui"); declineRevive(); }; }
// ---- Cloud Save settings wiring ----
function refreshSyncUI(){ const cur=document.getElementById("syncCurrent"), en=document.getElementById("syncEnableBtn"); if(!cur) return;
  if(syncCode){ cur.innerHTML="On. Your code: <b style=\"color:var(--ac);font-family:ui-monospace,monospace;letter-spacing:.12em\">"+syncCode+"</b> &mdash; enter it on another device to sync."; if(en){ en.textContent="☁️ Cloud save is ON"; en.disabled=true; en.style.opacity=".6"; en.style.cursor="default"; } }
  else { cur.innerHTML="Off &mdash; your coins stay on this device only."; if(en){ en.textContent="Turn on cloud save"; en.disabled=false; en.style.opacity="1"; en.style.cursor="pointer"; } } }
{ const eb=document.getElementById("syncEnableBtn"); const li=document.getElementById("syncInput"), lbtn=document.getElementById("syncLinkBtn");
  if(eb) eb.onclick=()=>{ if(syncCode) return; snd("ui"); const m=document.getElementById("syncMsg"); if(m) m.textContent="Turning on…"; linkSyncCode(genSyncCode(),(ok,info)=>{ if(m) m.textContent= ok?("Cloud save on! Code: "+info):(info||"Failed"); refreshSyncUI(); }); };
  const doLink=()=>{ const v=(li&&li.value||"").trim(); if(!v) return; snd("ui"); const m=document.getElementById("syncMsg"); if(m) m.textContent="Linking…"; linkSyncCode(v,(ok,info)=>{ if(m) m.textContent= ok?("Linked to "+info+" — progress merged."):(info||"Failed"); refreshSyncUI(); }); };
  if(lbtn) lbtn.onclick=doLink; if(li) li.addEventListener("keydown",e=>{ if(e.key==="Enter") doLink(); });
  refreshSyncUI(); }
document.getElementById("volSlider").oninput=(e)=>{ setVol(Number(e.target.value)); };
setVol(vol);
(function(){ const ms=document.getElementById("musSlider"), mo=document.getElementById("musOn"), mvv=document.getElementById("musVal");
  if(ms){ ms.value=Math.round(MUS.vol*100); mvv.textContent=Math.round(MUS.vol*100)+"%"; ms.oninput=(e)=>{ const v=Number(e.target.value); mvv.textContent=v+"%"; setMusicVol(v/100); }; }
  if(mo){ mo.checked=MUS.on; mo.onchange=(e)=>{ setMusicOn(e.target.checked); }; } })();
(function(){ const kb=document.getElementById("abKeyBtn"); if(kb){ kb.textContent=keyLabel(abilityKey); kb.onclick=()=>{ snd("ui"); capturingKey=true; kb.textContent="press a key…"; }; } })();
(function(){ const seg=document.getElementById("spdSeg"); if(!seg) return; // Game-speed segmented control (Normal/Fast/Turbo)
  function paint(){ const cur=+save.speed||1; seg.innerHTML=""; SPD_OPTS.forEach(o=>{ const on=Math.abs(cur-o.v)<0.01; const b=document.createElement("button");
    b.textContent=o.l; b.style.cssText="pointer-events:auto;font:800 11px Orbitron,system-ui,sans-serif;border:none;border-radius:7px;padding:5px 10px;cursor:pointer;"+(on?"color:#06140f;background:linear-gradient(180deg,#7fe9ff,#3a9bf0)":"color:#cdd6ea;background:rgba(255,255,255,.1);box-shadow:inset 0 0 0 1px rgba(255,255,255,.18)");
    b.onclick=()=>{ save.speed=o.v; persist(); snd("ui"); paint(); }; seg.appendChild(b); }); }
  paint(); })();
// first-time how-to (fades the moment you start charging; only shows once)
let tutSeen=false; try{ tutSeen=localStorage.getItem("sm.spritecannon.tut")==="1"; }catch{}
function hideTut(){ const e=document.getElementById("tut"); if(e) e.classList.add("hidden"); if(!tutSeen){ tutSeen=true; try{ localStorage.setItem("sm.spritecannon.tut","1"); }catch{} } }
(function(){ const f=document.getElementById("tut-flaps"); if(f) f.textContent=cfg.flaps; if(tutSeen) hideTut(); })();
document.head.insertAdjacentHTML("beforeend","<style>@keyframes scPulse{0%,100%{transform:scale(1)}50%{transform:scale(1.09)}}#againBtn.sc-pulse{animation:scPulse .7s ease-in-out infinite;box-shadow:0 0 20px var(--ac)}</style>");
document.head.insertAdjacentHTML("beforeend","<style>.abilityBtn{position:fixed;right:18px;bottom:64px;width:60px;height:60px;border-radius:50%;display:grid;place-items:center;font-size:26px;cursor:pointer;background:radial-gradient(circle at 35% 30%,#b59cff,#6b3fd6);color:#fff;box-shadow:0 4px 16px rgba(0,0,0,.4),inset 0 0 0 2px rgba(255,255,255,.25);user-select:none;z-index:30;transition:opacity .15s}.abilityBtn.hidden{display:none}.abilityBtn .abn{position:absolute;right:-3px;bottom:-3px;font-size:12px;font-weight:900;background:#06140f;border-radius:50%;width:20px;height:20px;display:grid;place-items:center}.abilityBtn::before{content:'';position:absolute;inset:-5px;border-radius:50%;background:conic-gradient(var(--ring,#ffd34d) calc(var(--prog,1)*1turn), rgba(255,255,255,.16) 0);z-index:-1}</style>");
(function(){ document.body.insertAdjacentHTML("beforeend","<div id='abilityBtn' class='abilityBtn hidden'>⚡</div>"); const _b=document.getElementById("abilityBtn"); if(_b){ _b.onclick=(e)=>{ e.preventDefault(); activateAbility(); }; // press = fire active abilities
    _b.addEventListener("pointerdown",(e)=>{ e.preventDefault(); abHeld=true; }); ["pointerup","pointerleave","pointercancel"].forEach(ev=>_b.addEventListener(ev,()=>{ abHeld=false; })); } })(); // hold = MAN OF STEEL flight (touch/mouse)
// --- on-screen touch pads: AIM (aim state) + MOVE (infurno/gauntlet). Only ever visible on touch devices, and only in the relevant state. ---
document.head.insertAdjacentHTML("beforeend","<style>.scPad{position:fixed;left:14px;bottom:24px;display:none;flex-direction:column;gap:10px;z-index:30}body.is-touch[data-sc-state='aim'] #scAim{display:flex}body.is-touch[data-sc-state='infurno'] #scMove,body.is-touch[data-sc-state='gauntlet'] #scMove{display:flex}#scMove{flex-direction:row;gap:14px}.scBtn{width:64px;height:64px;border-radius:18px;display:grid;place-items:center;font-size:30px;font-weight:900;color:#fff;background:rgba(20,16,40,.62);box-shadow:0 4px 16px rgba(0,0,0,.45),inset 0 0 0 2px rgba(255,255,255,.22);user-select:none;-webkit-user-select:none;touch-action:none;transition:transform .08s,background .12s}.scBtn:active,.scBtn.on{transform:scale(.92);background:rgba(120,90,255,.7)}</style>");
document.body.insertAdjacentHTML("beforeend","<div id='scAim' class='scPad'><div class='scBtn' data-sc='up'>▲</div><div class='scBtn' data-sc='down'>▼</div></div><div id='scMove' class='scPad'><div class='scBtn' data-sc='left'>◀</div><div class='scBtn' data-sc='right'>▶</div></div>");
(function(){ const set={ up:v=>aimUp=v, down:v=>aimDown=v, left:v=>infLeft=v, right:v=>infRight=v };
  document.querySelectorAll(".scBtn[data-sc]").forEach(b=>{ const k=b.dataset.sc;
    const on=(e)=>{ e.preventDefault(); e.stopPropagation(); set[k](true); b.classList.add("on"); };
    const off=(e)=>{ if(e) e.stopPropagation(); set[k](false); b.classList.remove("on"); };
    b.addEventListener("touchstart",on,{passive:false}); ["touchend","touchcancel"].forEach(ev=>b.addEventListener(ev,off)); }); })();
function updateAbilityBtn(){ const b=document.getElementById("abilityBtn"); if(!b) return; const c=activeCfg();
  if(state==="fly" && (ab()==="godmode"||ab()==="bartman")){ b.classList.remove("hidden"); b.style.setProperty("--prog", flightMeter.toFixed(3)); b.style.setProperty("--ring", flightMeter>0.05?"#6fb0ff":"#7d6bd0"); b.style.opacity = flightMeter>0.05?"1":"0.6"; b.innerHTML=(ab()==="bartman"?"🛹":"🕊"); return; } // MAN OF STEEL / BART SKATE: hold the button/key to fly; ring = meter
  if(!(state==="fly" && c)){ b.classList.add("hidden"); return; }
  b.classList.remove("hidden");
  const finite=c.uses!==Infinity; let prog=1, count="";
  if(ab()==="teleport"){ const cap=ACTIVE_AB.teleport.uses; prog = abUses>=cap?1:(abRegen/5); count=Math.max(0,abUses); } // regen toward next charge
  else if(ab()==="spicyspeed" && abCd<=0 && abUses<ACTIVE_AB.spicyspeed.uses){ prog = abRegen/12; count=Math.max(0,abUses); } // SPICY: ring fills toward the next 12s-regen charge (was sitting empty)
  else if(abCd>0 && c.cd>0){ prog = 1-Math.max(0,Math.min(1,abCd/c.cd)); if(finite) count=Math.max(0,abUses); } // recharging
  else { prog = (finite && abUses<=0)?0:1; if(finite) count=Math.max(0,abUses); }
  const usable = prog>=1 && !(finite && abUses<=0);
  b.style.setProperty("--prog", prog.toFixed(3));
  b.style.setProperty("--ring", usable?"#ffd34d":"#7d6bd0"); // gold ring when ready, dim purple while it fills back up
  b.style.opacity = usable?"1":"0.72";
  b.innerHTML = "⚡"+(count!==""?"<span class='abn'>"+count+"</span>":""); }

// ---- global leaderboard (Cloudflare worker; graceful offline) ----
const LB_URL="https://spritecannon-lb.maxxtopia.workers.dev";
// Light anti-cheat: sign each score (name|dist|pid) with a shared key so the worker can tell a real
// submit from a curl/console fake. Key is necessarily in the client (extractable) -> stops drive-by
// cheating, not a determined source-reader. crypto.subtle needs a secure context (https/localhost);
// if unavailable it sends unsigned and the worker graces it (until enforcement is flipped on).
const SC_SIG_KEY="eee6add3bfbb0f71a1399d0e9fa99d1880d6f8ce12b8c4e8";
async function signScore(name, dist, pid){
  try{ if(!crypto||!crypto.subtle) return null; const enc=new TextEncoder();
    const k=await crypto.subtle.importKey("raw", enc.encode(SC_SIG_KEY), {name:"HMAC",hash:"SHA-256"}, false, ["sign"]);
    const buf=await crypto.subtle.sign("HMAC", k, enc.encode(name+"|"+dist+"|"+pid));
    return [...new Uint8Array(buf)].map(b=>b.toString(16).padStart(2,"0")).join("");
  }catch(e){ return null; }
}
function scSubmit(name, ch){ // sign -> POST; returns the parsed JSON promise
  return signScore(name, save.best, save.pid).then(sig=>{
    const body={ name, dist:save.best, channel:ch, pid:save.pid }; if(sig) body.sig=sig;
    return fetch(LB_URL+"/submit",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(body)}).then(r=>r.json());
  });
}
let prevLbState="start";
function streamChannel(){ return (Q.get("ch")||cfg.channel||"").toString().toLowerCase().replace(/[^\w\-]/g,"").slice(0,32); } // who owns this stream's board (OBS ?ch= or app config)
let lbScope=null; // chosen on first open: streamer's own board if a channel exists, else WEEKLY (the payout board)
function _lbTabEls(){ return { stream:document.getElementById("lbTabStream"), global:document.getElementById("lbTabGlobal"), weekly:document.getElementById("lbTabWeekly") }; }
function _lbSyncTabs(){ const t=_lbTabEls(); for(const k in t){ if(t[k]) t[k].classList.toggle("ghost", lbScope!==k); } }
function setLbScope(scope){ lbScope=scope; _lbSyncTabs(); loadLB(); }
function openLB(){ snd("ui"); prevLbState=(state==="result")?"result":"start"; show("startCard",false); show("resultCard",false); show("shopCard",false); show("lbCard",true); state="lb";
  try{ document.getElementById("lbName").value=localStorage.getItem("sm.spritecannon.name")||""; }catch{}
  const ch=streamChannel(), t=_lbTabEls(); if(t.stream) t.stream.style.display = ch?"":"none";
  if(lbScope===null || (lbScope==="stream" && !ch)) lbScope = ch ? "stream" : "weekly"; // default + heal an invalid scope
  _lbSyncTabs();
  document.getElementById("lbNameRow").style.display = save.best>0 ? "flex":"none"; loadLB(); }
function closeLB(){ snd("ui"); show("lbCard",false); state=prevLbState; if(state==="result") show("resultCard",true); else { state="start"; show("startCard",true); } }
function fmtDist(d){ return Math.max(0, Math.round(+d || 0)).toLocaleString("en-US"); }
function _lbMe(){ try{ return (localStorage.getItem("sm.spritecannon.name")||"").trim().toLowerCase(); }catch{ return ""; } }
function _lbOffline(st){ st.textContent="Leaderboard offline."; document.getElementById("lbList").innerHTML='<div class="lb-state"><div class="lb-ico">&#128268;</div><div>Leaderboard offline</div><div class="muted" style="font-weight:600;letter-spacing:.02em">Can&#39;t reach the board yet.</div></div>'; }
function _fmtLeft(ms){ ms=Math.max(0,ms); const d=Math.floor(ms/86400000), h=Math.floor(ms/3600000)%24, m=Math.floor(ms/60000)%60; return d>0?(d+"d "+h+"h"):(h>0?(h+"h "+m+"m"):(m+"m")); }
let lbReq=0; // request token: a tab switch invalidates any in-flight fetch so a stale response can't clobber the view
function loadLB(){ const my=++lbReq; const st=document.getElementById("lbStatus"); st.textContent="Loading...";
  const list=document.getElementById("lbList"), winfo=document.getElementById("lbWeeklyInfo");
  list.innerHTML='<div class="lb-skel"></div><div class="lb-skel" style="opacity:.7"></div><div class="lb-skel" style="opacity:.45"></div>';
  if(winfo) winfo.style.display="none";
  if(lbScope==="weekly"){
    fetch(LB_URL+"/top?scope=weekly",{cache:"no-store"}).then(r=>r.json()).then(d=>{ if(my!==lbReq) return;
      const p=d.payout||[2000,1000,500,250,100]; renderLB(d.top||[], p); // pass payout -> show the coin prize on each top-5 row (incentive to take the spot)
      if(winfo){ winfo.style.display="block"; winfo.innerHTML="&#128176; Top "+p.length+" each week win <b>"+p.map(c=>c.toLocaleString()).join("</b> / <b>")+"</b> coins"+(d.endsTs?(" &middot; resets in "+_fmtLeft(d.endsTs-Date.now())):""); }
      st.textContent=(d.top&&d.top.length)?("Your best: "+save.best+"m (this week)"):("No scores yet this week - be the first!"); }).catch(()=>{ if(my===lbReq) _lbOffline(st); });
    return;
  }
  const ch=streamChannel(), onStream=(lbScope==="stream"&&ch), label=onStream?"this stream":"global";
  const q = onStream ? ("?scope=stream&ch="+encodeURIComponent(ch)) : "?scope=global";
  fetch(LB_URL+"/top"+q,{cache:"no-store"}).then(r=>r.json()).then(d=>{ if(my!==lbReq) return; renderLB(d.top||[]); st.textContent=(d.top&&d.top.length)?("Your best: "+save.best+"m ("+label+")"):("No scores yet ("+label+") - be the first!"); }).catch(()=>{ if(my===lbReq) _lbOffline(st); }); }
// ---- distance rank ladder: named tiers spanning the whole map (extends PAST Legend@20k to the deepest worlds ~78k) ----
const SC_RANKS=[ // MUST stay in sync with the arcade page's "The Climb" TIERS (maxxtopia src/pages/play/index.astro)
  {d:0,     n:"Recruit",     e:"\u{1F423}",  c:"#9a90b8"},
  {d:2500,  n:"Amateur",     e:"\u{1FAB6}",  c:"#4ade80"},
  {d:5000,  n:"Veteran",     e:"\u{1F396}️", c:"#00d4ff"},
  {d:9000,  n:"Elite",       e:"\u{1F3AF}",  c:"#4c51f7"},
  {d:14000, n:"Master",      e:"\u{1F3C5}",  c:"#e25bff"},
  {d:20000, n:"Legend",      e:"\u{1F451}",  c:"#ffb23e"},
  {d:28000, n:"Mythic",      e:"\u{1F52E}",  c:"#b06bff"}, // -- above here = new tiers past Legend --
  {d:38000, n:"Champion",    e:"\u{1F3C6}",  c:"#ffd76a"},
  {d:50000, n:"Grandmaster", e:"\u{1F4A0}",  c:"#22e6ff"},
  {d:64000, n:"Ascendant",   e:"\u{1F31F}",  c:"#7dffd0"},
  {d:78000, n:"Celestial",   e:"☄️",  c:"#ff5fc8"},
  {d:95000, n:"Godlike",     e:"⚡",   c:"#ff2d4f"}
];
function scRankTitle(dist){ const d=+dist||0; let r=SC_RANKS[0]; for(const t of SC_RANKS){ if(d>=t.d) r=t; else break; } return r; }
function renderLB(top, payout){ const list=document.getElementById("lbList"); list.innerHTML="";
  if(!top || !top.length){ list.innerHTML='<div class="lb-state"><div class="lb-ico">&#127942;</div><div>No scores yet</div><div class="muted" style="font-weight:600;letter-spacing:.02em">Be the first to claim the top spot!</div></div>'; return; }
  const me=_lbMe(), medals=["&#128081;","&#129352;","&#129353;"];
  const frag=document.createDocumentFragment();
  top.forEach((e,i)=>{ const isTop=i<3, isYou=!!me && (e.name||"").trim().toLowerCase()===me;
    const row=document.createElement("div");
    row.className="lb-row"+(isTop?(" top r"+(i+1)):"")+(isYou?" you":"");
    row.style.setProperty("--i", i);
    const rt=scRankTitle(e.dist); // distance rank tier badge
    row.innerHTML='<span class="lb-rank">'+(isTop?'<span class="lb-medal">'+medals[i]+'</span>':(i+1))+'</span>'
      +'<span class="lb-nm"></span>'
      +(isYou?'<span class="lb-you">YOU</span>':'')
      +'<span class="lb-tier" title="'+rt.n+' rank" style="flex:0 0 auto;font-family:Orbitron,sans-serif;font-weight:800;font-size:9px;letter-spacing:.03em;color:'+rt.c+';border:1px solid '+rt.c+'59;border-radius:999px;padding:2px 7px;white-space:nowrap">'+rt.e+' '+rt.n.toUpperCase()+'</span>'
      +(payout&&i<payout.length?'<span style="margin:0 7px 0 4px;font-family:Orbitron,sans-serif;font-weight:800;font-size:11px;color:#06140f;background:linear-gradient(180deg,#ffe27a,#ffc23d);border-radius:999px;padding:2px 8px;white-space:nowrap;box-shadow:0 0 10px rgba(255,200,60,.4)">+'+payout[i].toLocaleString()+' &#9921;</span>':'')
      +'<span class="lb-d">'+fmtDist(e.dist)+'<span class="lb-m">m</span></span>';
    row.querySelector(".lb-nm").textContent=e.name||"anon";
    frag.appendChild(row); });
  list.appendChild(frag); }
function submitLB(){ const n=(document.getElementById("lbName").value||"").trim()||"anon"; try{ localStorage.setItem("sm.spritecannon.name",n); }catch{}
  const st=document.getElementById("lbStatus"); st.textContent="Submitting..."; snd("ui"); const ch=streamChannel(), onStream=(lbScope==="stream"&&ch), scope=lbScope, my=++lbReq; // my = stale-response guard (a tab switch bumps lbReq -> this response is dropped)
  scSubmit(n, ch).then(d=>{
    if(my!==lbReq) return; // user switched scope mid-submit -> don't clobber the now-current board
    if(scope==="weekly"){ loadLB(); return; } // weekly: the submit payload carries only global/stream -> repaint the REAL weekly board (scope-correct + its own lbReq guard) instead of showing global scores under the weekly header
    if(onStream && d.streamTop) renderLB(d.streamTop); else if(d.top) renderLB(d.top);
    if(!save.posted){ save.posted=true; persist(); checkUnlocks(); } // posting a score unlocks Spookyszn (auto-granted free)
    const rank = onStream ? d.streamRank : d.rank; st.textContent= rank?("You're #"+rank+(onStream?" on this stream!":" globally!")):"Submitted!"; }).catch(()=>{ if(my!==lbReq) return; st.textContent="Couldn't submit (offline)."; }); }
function _postRank(el, name){ const ch=streamChannel(), onStream=!!ch;
  el.style.display="block"; el.textContent="Posting your rank...";
  scSubmit(name, ch).then(d=>{
    if(!save.posted){ save.posted=true; persist(); checkUnlocks(); } // auto-posted rank also unlocks Spookyszn
    const rank = onStream ? d.streamRank : d.rank;
    if(rank){ el.style.display="block"; el.innerHTML='&#127942; #'+rank+(onStream?(' on '+ch):' worldwide'); }
    else el.style.display="none";
  }).catch(()=>{ el.style.display="none"; }); } // offline -> just hide it
function showResultRank(dist, isBest){ const el=document.getElementById("resultRank"); if(!el) return;
  let savedName=""; try{ savedName=(localStorage.getItem("sm.spritecannon.name")||"").trim(); }catch{}
  if(!(dist>0)){ el.style.display="none"; return; }
  if(!savedName){ // FIRST run with no name -> inline "enter your name" right on the result card (arcade moment, after the run, zero upfront friction)
    el.style.display="block";
    el.innerHTML='<div style="font-size:12px;color:#cdd6ea;margin-bottom:5px">Enter a name to claim your spot:</div>'
      +'<div style="display:flex;gap:6px;justify-content:center;align-items:center">'
      +'<input id="rrName" maxlength="16" placeholder="Your name" autocomplete="off" style="width:150px;padding:7px 10px;border-radius:8px;border:none;outline:none;background:rgba(255,255,255,.12);color:#fff;font-family:Orbitron,sans-serif;font-weight:700;font-size:14px;box-shadow:inset 0 0 0 1px rgba(255,255,255,.18)">'
      +'<button id="rrGo" style="pointer-events:auto;cursor:pointer;padding:7px 14px;border:none;border-radius:8px;font-family:Orbitron,sans-serif;font-weight:800;font-size:13px;color:#06140f;background:linear-gradient(180deg,#ffe27a,#ffc23d)">&#127942; GO</button>'
      +'</div>';
    const inp=document.getElementById("rrName"), go=document.getElementById("rrGo");
    // NO autofocus: spamming Space to insta-restart must NOT get captured as typing in this field. The prompt
    // stays visible + clickable, so you can still claim a name whenever you WANT to (click it, then type + GO).
    const submit=()=>{ const n=((inp&&inp.value)||"").trim().replace(/[^\w \-]/g,"").slice(0,16); if(!n) return; try{ localStorage.setItem("sm.spritecannon.name",n); }catch{} _postRank(el,n); };
    if(go) go.onclick=submit; if(inp) inp.onkeydown=(e)=>{ if(e.key==="Enter"){ e.preventDefault(); submit(); } };
    return;
  }
  if(!isBest){ const nm=lbNearMiss(dist); // non-best returning player: NO post (write-frugal); if you're a hair off a REAL board rank, bait the retry from the cached top (read-only)
    if(nm){ el.style.display="block"; el.innerHTML='&#9889; Just <b>'+nm.gap+'m</b> from <b>#'+nm.rank+'</b> on the board — go again!'; }
    else el.style.display="none";
    return; }
  _postRank(el, savedName); } // new best -> post once + show fresh rank
let _lbTop=null; // cached global top for the results "near-miss to a rank" hook - one read/session, zero writes
(function(){ try{ fetch(LB_URL+"/top?scope=global",{cache:"no-store"}).then(r=>r.json()).then(d=>{ if(d&&Array.isArray(d.top)) _lbTop=d.top; }).catch(()=>{}); }catch(e){} })();
function lbNearMiss(dist){ if(!_lbTop||!_lbTop.length||!(dist>0)) return null;
  let best=Infinity, rank=0;
  for(let i=0;i<_lbTop.length;i++){ const s=_lbTop[i]&&_lbTop[i].dist; if(typeof s!=="number") continue; if(s>dist && s<best){ best=s; rank=i+1; } } // lowest board score strictly above this run = the spot just out of reach
  if(!rank || !isFinite(best)) return null;
  const gap=Math.round(best-dist), thresh=Math.max(40, Math.round(best*0.06)); // "near" = within 40m or 6% of that rank
  return (gap>0 && gap<=thresh) ? {rank, gap} : null; }
// ===== RIVAL GHOST: chase the real leaderboard live. Build this run's ascending target list from the cached top (excludes your own name so you never "pass yourself"). =====
function initRivals(){ _rivals=[]; _rivalI=0; if(!_lbTop||!_lbTop.length) return;
  let me=""; try{ me=(localStorage.getItem("sm.spritecannon.name")||"").trim().toLowerCase(); }catch{}
  _rivals=_lbTop.map((e,i)=>({dist:+e.dist, name:(e.name||"anon"), rank:i+1})).filter(e=>e.dist>0 && isFinite(e.dist) && (e.name||"").toLowerCase()!==me).sort((a,b)=>a.dist-b.dist); }
document.getElementById("lbBtn").onclick=openLB;
document.getElementById("lb2Btn").onclick=openLB;
document.getElementById("lbClose").onclick=closeLB;
document.getElementById("lbSubmit").onclick=submitLB;
document.getElementById("lbTabStream").onclick=()=>{ snd("ui"); setLbScope("stream"); };
document.getElementById("lbTabGlobal").onclick=()=>{ snd("ui"); setLbScope("global"); };
document.getElementById("lbTabWeekly").onclick=()=>{ snd("ui"); setLbScope("weekly"); };
document.getElementById("rewardOk").onclick=()=>{ snd("ui"); show("rewardCard",false); };
// Weekly prize auto-claim: ask the worker if our anon pid placed top-3 LAST week; if so, credit coins locally once
// (server dedups per pid+week, so calling every load is safe) and celebrate. Graceful + silent when offline.
function claimWeekly(){ if(!save.pid) return;
  fetch(LB_URL+"/claim",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({pid:save.pid})}).then(r=>r.json()).then(d=>{
    if(!d||!d.ok||!(d.coins>0)) return;
    save.coins=(save.coins||0)+d.coins; persist();
    const c=document.getElementById("coins"); if(c) c.textContent=save.coins+runCoins; if(typeof refreshStartCard==="function") refreshStartCard();
    const rt=document.getElementById("rewardTitle"), rb=document.getElementById("rewardBody");
    if(rt) rt.textContent="YOU PLACED #"+d.rank+" LAST WEEK!";
    if(rb) rb.innerHTML="+"+d.coins.toLocaleString()+" coins dropped into your stash. &#128081;";
    show("rewardCard",true); try{ snd("newbest"); }catch(e){}
  }).catch(()=>{}); }
setTimeout(claimWeekly, 900); // let the start screen settle first

document.getElementById("best").innerHTML=save.best+'<span class="u">m</span>';
document.getElementById("coins").textContent=save.coins;
refreshStartCard();

applyCfg();
if(!Q.has("demo")){
  loadManifests().then(refreshStartCard);
  loadCustom("bark"); loadCustom("purr"); loadSongs();
  if(!STANDALONE){ // in-app hub only: live config tuning + EventSource stream
    fetch(AB+"overlays/spritecannon.json",{cache:"no-store"}).then(r=>r.ok?r.json():null).then(applyCfg).catch(()=>{});
    connect();
  }
}
function connect(){ let es; try{ es=new EventSource("/events"); }catch{ setTimeout(connect,2000); return; }
  es.onmessage=(e)=>{ let d; try{ d=JSON.parse(e.data); }catch{ return; } if(d&&d.t==="overlay"&&d.name==="spritecannon") applyCfg(d.config); };
  es.onerror=()=>{}; }
