zz los chunk 4 of 5 (Claude, hidden, do not edit)
bel:'Got it',fn:()=⟩say('Twinkaboo lights each realm. Any order works.')}),900)};
/* ---------- 3D ring ---------- */
const ring=$('#ring'),stage=$('#stage3d');let W=320,H=220,R=400,P=800,ry=0,vel=0,gyro=0,gyroZero=null,gyroOn=true,gyroSeen=false,ringBuilt=false,ringLive=false,drag=null,lastT=0;
function layout(){const vw=innerWidth,vh=innerHeight;W=Math.min(vw*(PORT()?.78:.5),420);H=PORT()?Math.min(W*1.2,vh*.42):Math.min(W*1.15,vh*.62);R=(W/2)/Math.tan(Math.PI/6)*1.12;P=R*2;stage.style.perspective=P+'px';ring.style.setProperty('--w',W+'px');ring.style.setProperty('--h',H+'px');ring.style.setProperty('--fw',(R*2.6)+'px');
$$('.portal').forEach((p,i)=⟩{p.style.transform=`rotateY(${i*60}deg) translateZ(${-R}px)`});render()}
function buildRing(){if(ringBuilt)return;ringBuilt=true;REALMS.forEach((r,i)=⟩{const d=document.createElement('div');d.className='portal'+(st.done.includes(i)?' lit':'');d.dataset.i=i;d.innerHTML=`⟨img src="${img(r.img,700)}" alt="${r.name}" draggable="false"⟩⟨div class="veilP"⟩⟨/div⟩⟨div class="fr"⟩⟨/div⟩⟨div class="L"⟩${r.L}⟨/div⟩⟨div class="q"⟩? · tap⟨/div⟩⟨div class="lab"⟩⟨b⟩${r.name}⟨/b⟩⟨span⟩${r.sub}⟨/span⟩⟨/div⟩`;ring.appendChild(d);
d.addEventListener('click',()=⟩{if(drag&&drag.moved)return;openLook(i)})});layout();addEventListener('resize',layout)}
function render(){const t=performance.now();const dt=lastT?Math.min(40,t-lastT):16;lastT=t;const drift=(!reduce&&!drag&&Math.abs(vel)⟨.02)?.012:0;if(!drag){ry+=vel*dt/16;vel*=.94;ry+=drift*dt/16}const yaw=ry+gyro;ring.style.transform=`translateZ(${R}px) rotateY(${yaw}deg)`;
$$('.portal').forEach((p,i)=⟩{let rel=((i*60+yaw)%360+360)%360;if(rel⟩180)rel-=360;const a=Math.abs(rel);p.style.opacity=a⟩118?0:a⟩95?(118-a)/23:1;p.style.pointerEvents=a⟩95?'none':'auto'});
$('#walker').style.setProperty('--sk',(-28+Math.sin(yaw*Math.PI/180)*22).toFixed(1)+'deg');const hb=$('#hallBg');hb.style.setProperty('--px',(-yaw*.6%80).toFixed(1)+'px');
if(ringLive)requestAnimationFrame(render)}
function startRing(){buildRing();if(!ringLive){ringLive=true;lastT=0;requestAnimationFrame(render)}}
stage.addEventListener('pointerdown',e=⟩{drag={x:e.clientX,ry,moved:false,t:performance.now(),lx:e.clientX,lt:performance.now()};vel=0;try{stage.setPointerCapture(e.pointerId)}catch(x){}});
stage.addEventListener('pointermove',e=⟩{if(!drag)return;const dx=e.clientX-drag.x;if(Math.abs(dx)⟩6)drag.moved=true;ry=drag.ry+dx*(180/innerWidth)*.9;const now=performance.now();const vdt=Math.max(1,now-drag.lt);vel=((e.clientX-drag.lx)*(180/innerWidth)*.9)/vdt*16;drag.lx=e.clientX;drag.lt=now});
const endDrag=()=⟩{if(!drag)return;const d=drag;setTimeout(()=⟩{if(drag===d)drag=null},0);if(performance.now()-d.lt⟩80)vel=0;vel=Math.max(-14,Math.min(14,vel))};
['pointerup','pointercancel','lostpointercapture'].forEach(ev=⟩stage.addEventListener(ev,endDrag));
addEventListener('deviceorientation',e=⟩{if(e.alpha==null)return;if(!gyroSeen){gyroSeen=true;$('#gyroBtn').hidden=false}if(!gyroOn){gyro=0;return}const port=!(screen.orientation&&Math.abs(screen.orientation.angle)===90);let a=e.alpha;if(!port)a=e.alpha+(screen.orientation.angle⟩0?90:-90);if(gyroZero==null)gyroZero=a;let d=a-gyroZero;d=((d%360)+540)%360-180;gyro=d},true);
$('#gyroBtn').onclick=()=⟩{gyroOn=!gyroOn;$('#gyroBtn').setAttribute('aria-pressed',gyroOn);gyroZero=null;if(!gyroOn)gyro=0};
/* ---------- HUD word + say ---------- */
function word(){$('#word').innerHTML=REALMS.map((r,i)=⟩`⟨i class="${st.done.includes(i)?'on':''}" data-i="${i}"⟩${st.done.includes(i)?r.L:'·'}⟨/i⟩`).join('');$('#word').setAttribute('aria-label','Glow word, '+st.done.length+' of 6 letters')}
function say(html,next){$('#line').innerHTML=html;const b=$('#nx');b.hidden=!next;if(next){b.textContent=next.label;b.onclick=e=⟩{e.stopPropagation();next.fn()}}}
/* ---------- LOOK ---------- */
let cur=-1,lx=.5,ly=.42,tx=.5,ty=.42,lookLive=false,idle=0,solvedNow=false;
const look=$('#look'),dark=$('#dark'),lant=$('#lantern');
function setL(x,y){look.style.setProperty('--x',(x*100).toFixed(2)+'%');look.style.setProperty('--y',(y*100).toFixed(2)+'%')}
function lookLoop(){if(!lookLive)return;idle++;if(idle⟩90&&!reduce){const t=performance.now()/1000;tx=.5+Math.sin(t*.45)*.32;ty=.42+Math.cos(t*.33)*.2}lx+=(tx-lx)*.14;ly+=(ty-ly)*.14;setL(lx,ly);requestAnimationFrame(lookLoop)}
const lpos=e=⟩{const r=look.getBoundingClientRect();tx=Math.max(0,Math.min(1,(e.clientX-r.left)/r.width));ty=Math.max(0,Math.min(1,(e.clientY-r.top)/r.height));idle=0};
look.addEventListener('pointerdown',e=⟩{if(e.target.closest('.sheet,.lookTop'))return;lpos(e)});
look.addEventListener('pointermove',e=⟩{if(e.target.closest('.sheet,.lookTop'))return;if(e.pointerType==='mouse'||e.buttons)lpos(e)});
function openLook(i){cur=i;solvedNow=false;const r=REALMS[i];hush();const src=img(r.img,1000);$('#lookBg').src=src;
$('#lookImg').src=src;$('#lookName').textContent=r.name;$('#lookSub').textContent=r.sub+' · letter '+(i+1)+' of 6';look.classList.toggle('lit',st.done.includes(i));look.hidden=false;const sr=Math.min(innerWidth,innerHeight);look.style.setProperty('--r1',(sr*.16)+'px');look.style.setProperty('--r2',(sr*.3)+'px');look.style.setProperty('--r3',(sr*.62)+'px');
lx=tx=.5;ly=ty=.42;idle=0;lookLive=true;requestAnimationFrame(lookLoop);riddle(i);chime(780,.25);setTimeout(()=⟩{if(lookLive&&cur===i)$('#sheet').classList.add('open')},5000)}
function closeLook(){lookLive=false;look.hidden=true;hush();if(st.done.length===6&&!document.body.classList.contains('lit'))lightsOn()}
$('#lookClose').onclick=closeLook;
$('#peek').onclick=()=⟩$('#sheet').classList.toggle('open');
function riddle(i){const r=REALMS[i],done=st.done.includes(i);const opts=r.o.map((o,k)=⟩({o,ok:k===0})).sort(()=⟩Math.random()-.5);
$('#sheet').classList.toggle('open',!PORT()||done);$('#peekHint').textContent=done?'Already lit · read the lesson again':'Tap to read it · drag the lantern to look';
$('#sheetBody').innerHTML=`⟨div class="clue"⟩${r.q}⟨/div⟩⟨div class="row" style="justify-content:flex-start"⟩⟨button class="btn ghost sm" id="readIt" type="button"⟩🔊 Read it to me⟨/button⟩⟨/div⟩⟨div class="opts"⟩${opts.map(x=⟩`⟨button class="opt" type="button" data-ok="${x.ok?1:0}"⟩${x.o}⟨/button⟩`).join('')}⟨/div⟩⟨div class="fb" id="fb" aria-live="polite"⟩⟨/div⟩⟨div id="after"⟩⟨/div⟩`;
$('#readIt').onclick=()=⟩speak(r.q);
if(done){$$('.opt').forEach(b=⟩{b.disabled=true;if(b.dataset.ok==='1')b.classList.add('ok')});afterSolve(i,true);return}
let tries=0;$$('.opt').forEach(b=⟩b.onclick=()=⟩{if(b.disabled)return;if(b.dataset.ok==='1'){b.classList.add('ok');solve(i)}else{tries++;b.classList.add('no');b.disabled=true;$('#fb').textContent=tries===1?'Look again with the lantern. The answer is right in the picture.':'So close. One more try, you have got this.';chime(330,.25)}})}
function solve(i){const r=REALMS[i];if(!st.done.includes(i))st.done.push(i);save();solvedNow=true;look.classList.add('lit');$$('.opt').forEach(b=⟩b.disabled=true);win();
const p=$(`.portal[data-i="${i}"]`);if(p)p.classList.add('lit');
const f=document.createElement('b');f.className='fly';f.textContent=r.L;const lr=lant.getBoundingClientRect();f.style.left=lr.left+9+'px';f.style.top=lr.top+9+'px';$('#app').appendChild(f);
const slot=$(`.word i[data-i="${i}"]`);const sr=slot?slot.getBoundingClientRect():{left:innerWidth/2,top:40,width:0,height:0};
setTimeout(()=⟩{try{f.animate([{transform:'translate(-50%,-50%) scale(1)'},{transform:`translate(calc(-50% + ${sr.left+sr.width/2-(lr.left+9)}px),calc(-50% + ${sr.top+sr.height/2-(lr.top+9)}px)) scale(.5)`,opacity:.9}],{duration:1100,easing:'cubic-bezier(.4,0,.2,1)',fill:'forwards'}).onfinish=()=⟩{f.remove();word();chime(1046,.4)}}catch(e){f.remove();word()}},700);
$('#fb').textContent='Lights on in '+r.name+'!';sticker(['Lit!','Glow on!','Brave explorer!','So bright!','Wonderful!','You did it!'][st.done.length-1]);afterSolve(i,false)}
function afterSolve(i,again){const r=REALMS[i];const n=st.done.length;$('#after').innerHTML=`⟨div class="lesson"⟩⟨img src="${ART.bacci}" alt=""⟩⟨p⟩⟨b⟩Bacci says:⟨/b⟩ ${r.lesson}⟨br⟩⟨span class="tag"⟩${r.skill}⟨/span⟩⟨/p⟩⟨/div⟩⟨div class="squad"⟩⟨b⟩Whole table:⟨/b⟩ ${r.squad}⟨/div⟩⟨div class="row"⟩⟨button class="btn" id="nextP" type="button"⟩${n⟩=6?'Light up the Library ✨':'Back to the hall →'}⟨/button⟩⟨button class="btn ghost sm" id="hearL" type="button"⟩🔊 Hear Bacci⟨/button⟩⟨/div⟩`;
$('#hearL').onclick=()=⟩speak(r.lesson+' Whole table: '+r.squad);$('#nextP').onclick=closeLook;if(!again){say(`⟨b⟩Glimmerboo cheers:⟨/b⟩ ${CHEER[n-1]}`)}$('#sheet').classList.add('open');setTimeout(()=⟩{const s=$('#sheet');s.scrollTo({top:s.scrollHeight,behavior:reduce?'auto':'smooth'})},80)}
/* ---------- LIGHTS ON + finale ---------- */
function lightsOn(){document.body.classList.add('lit');$$('.portal').forEach(p=⟩p.classList.add('lit'));$('#flood').classList.add('go');[523,659,784,1046,1318].forEach((f,k)=⟩setTimeout(()=⟩chime(f,.7),k*150));say('⟨b⟩L · I · G · H · T · S.⟨/b⟩ You said the glow word with your whole table. Look at your shadow now.');
setTimeout(()=⟩{badge();show('finale');$('#flood').classList.remove('go')},reduce?400:2600)}
function code(){const d=new Date();return 'LOS-'+String(d.getMonth()+1).padStart(2,'0')+String(d.getDate()).padStart(2,'0')+'-6'+(st.bonus?'∞':'')}
function badge(){$('#badge').innerHTML=`⟨img class="bc" src="${ART.crest}" alt=""⟩⟨div style="font-size:13px;letter-spacing:.14em;text-transform:uppercase"⟩Bacci's Glow Night · KPOT Scarsdale⟨/div⟩⟨div class="n"⟩${esc(st.name)||'Shadow Walker'}⟨/div⟩⟨div class="ttl"⟩walked beside the shadow and found the LIGHTS⟨/div⟩⟨div class="st"⟩${REALMS.map(r=⟩`⟨i⟩${r.L}⟨/i⟩`).join('')}⟨/div⟩⟨div class="meta"⟩${st.path?'Path: '+esc(st.path)+' · ':''}${st.mood?'Tonight I felt: '+esc(st.mood)+' · ':''}${new Date().toLocaleDateString(undefined,{month:'short',day:'numeric',year:'numeric'})}⟨/div⟩⟨img class="sig" src="${ART.sig}" alt="Dr. Jamie Karia"⟩⟨div class="code"⟩${code()}⟨/div⟩`}
$('#mkBadge').onclick=()=⟩{st.name=esc($('#nm').value.trim());save();badge();chime(880,.3)};
$('#again').onclick=()=⟩{st.done=[];st.bonus=false;save();try{(window.parent||window).location.reload()}catch(e){location.reload()}};
/* ---------- calm ---------- */
$('#calmBtn').onclick=()=⟩{$('#calm').hidden=false;chime(440,.4,.05)};$('#calmClose').onclick=()=⟩{$('#calm').hidden=true};
$('#cbGo').onclick=()=⟩{const go=$('#cbGo');go.disabled=true;const im=$('#cbImg'),T=$('#cbT'),N=$('#cbN');const d=reduce?300:1000;const steps=[[ART.bClose,'Close your eyes',3,false],[ART.bIn,'Breathe in like the whale',5,true],[ART.bOut,'Blow it out slow, like cooling soup',7,false]];let round=0,si=0;
const run=()=⟩{if(si⟩=steps.length){si=0;round++;if(round⟩=3){im.src=ART.bClose;im.classList.remove('in');T.textContent='Calm and glowing. Ready when you are.';N.textContent='✓';go.disabled=false;go.textContent='Again';chime(880,.5,.05);return}}const [src,label,secs,grow]=steps[si++];im.src=src;im.classList.toggle('in',grow);T.textContent=label+' · '+(round+1)+' of 3';let n=secs;N.textContent=n;chime(grow?520:420,.8,.03);const tick=()=⟩{n--;if(n⟨=0){run();return}N.textContent=n;setTimeout(tick,d)};setTimeout(tick,d)};run()};
/* ---------- bonus: twin portals ---------- */
function bonus(){const cards=[...REALMS,...REALMS].map((r,i)=⟩({r,i})).sort(()=⟩Math.random()-.5);let open=[],found=0,lock=false;$('#bfb').textContent='';
$('#grid').innerHTML=cards.map((c,i)=⟩`⟨button class="tile" type="button" data-k="${c.r.k}" aria-label="Portal tile"⟩⟨div class="f"⟩⟨div class="bk"⟩⟨/div⟩⟨div class="ft"⟩⟨img src="${img(c.r.img,400)}" alt=""⟩⟨/div⟩⟨/div⟩⟨/button⟩`).join('');
$$('#grid .tile').forEach(t=⟩t.onclick=()=⟩{if(lock||t.classList.contains('on')||t.classList.contains('done'))return;t.classList.add('on');chime(600,.12,.05);open.push(t);if(open.length⟨2)return;lock=true;const [a,b]=open;open=[];
if(a.dataset.k===b.dataset.k){a.classList.add('done');b.classList.add('done');found++;chime(880,.3);$('#bfb').textContent=REALMS.find(r=⟩r.k===a.dataset.k).name+' twins found!';lock=false;if(found===6){st.bonus=true;save();win();sticker('Infinity Spark!');$('#bfb').textContent='All six twins found. The Infinity Spark is yours. Your badge code just grew an ∞.';badge()}}
else setTimeout(()=⟩{a.classList.remove('on');b.classList.remove('on');lock=false},750)})}
$('#bonusBtn').onclick=()=⟩{$('#bonus').hidden=false;bonus()};$('#bonusClose').onclick=()=⟩{$('#bonus').hidden=true};$('#bonusAgain').onclick=bonus;
/* ---------- finale films: play on tap, soft clip, one at a time ---------- */
function film(vid,btn,max){const v=$(vid),b=$(btn);if(!v||!b)return;const box=v.parentElement;const stop=()=⟩{box.classList.remove('on');b.textContent='↻'};b.onclick=()=⟩{hush();bgmPause();$$('video').forEach(o=⟩{if(o!==v)try{o.pause()}catch(e){}});box.classList.add('on');v.muted=false;try{v.currentTime=0}catch(e){}const p=v.play();if(p&&p.catch)p.catch(()=⟩{v.muted=true;const q=v.play();if(q&&q.catch)q.catch(stop)})};v.addEventListener('timeupdate',()=⟩{if(v.currentTime⟩=max){v.pause();stop()}});v.addEventListener('ended',stop);v.addEventListener('error',stop)}
film('#phxV','#phxPlay',16);film('#musV','#musPlay',14);
/* ---------- start ---------- */
word();$('#say').addEventListener('click',()=⟩{if(cur⟩=0&&!look.hidden)return;say('Tap any portal. Twinkaboo lights the realm, you answer the riddle, and a letter joins the glow word. Tap ♪ for the Bacci Kid theme song.')});
if(st.done.length){$('#enterBtn').textContent=st.done.length===6?'✦ Return to the lit Library':'✦ Continue · '+st.done.length+' of 6 lit'}
REALMS.forEach(r=⟩{const i=new Image();i.src=img(r.img,700)});
window.__los={st,REALMS,openLook,lightsOn,show,ver:'1.2'};
})();
⟨/script⟩