/* BinTat — dynamic FX layer: scroll progress, story rail, word reveals, parallax, scroll cue */ const { useEffect:useFxE, useRef:useFxR, useState:useFxS } = React; /* thin progress bar at the very top */ function ScrollProgress(){ const ref = useFxR(null); useFxE(()=>{ const el = ref.current; if(!el) return; let t=false; const f=()=>{ t=false; const max = document.documentElement.scrollHeight - innerHeight; el.style.transform = `scaleX(${max>0 ? Math.min(1, scrollY/max) : 0})`; }; const on=()=>{ if(!t){ t=true; requestAnimationFrame(f); } }; addEventListener('scroll', on, {passive:true}); addEventListener('resize', on, {passive:true}); f(); return ()=>{ removeEventListener('scroll', on); removeEventListener('resize', on); }; },[]); return
; } /* global scroll effects: hero parallax, [data-plx] parallax, body.is-scrolled */ function ScrollFX(){ useFxE(()=>{ if(window.matchMedia && matchMedia('(prefers-reduced-motion: reduce)').matches) return; let els=[], heroBg=null, ticking=false, raf=0; const collect=()=>{ els=[...document.querySelectorAll('[data-plx]')]; heroBg=document.querySelector('[data-hero-bg]'); }; collect(); const root=document.getElementById('root'); const mo=new MutationObserver(()=>{ collect(); onScroll(); }); if(root) mo.observe(root,{childList:true,subtree:true}); const frame=()=>{ ticking=false; const sc=scrollY, vh=innerHeight||800; document.body.classList.toggle('is-scrolled', sc>70); if(heroBg) heroBg.style.transform=`translate3d(0,${(sc*0.16).toFixed(1)}px,0) scale(1.08)`; for(const el of els){ const r=el.getBoundingClientRect(); if(r.bottom<-80||r.top>vh+80) continue; const c=(r.top+r.height/2)-vh/2; const sp=parseFloat(el.dataset.plx)||0.1; const y=Math.max(-30,Math.min(30,-(c*sp))); el.style.transform=`translate3d(0,${y.toFixed(1)}px,0) scale(1.13)`; } }; const onScroll=()=>{ if(!ticking){ ticking=true; raf=requestAnimationFrame(frame); } }; addEventListener('scroll', onScroll, {passive:true}); addEventListener('resize', onScroll, {passive:true}); frame(); return ()=>{ removeEventListener('scroll', onScroll); removeEventListener('resize', onScroll); mo.disconnect(); cancelAnimationFrame(raf); }; },[]); return null; } /* left chapter rail — the "fil conducteur" (home) */ function StoryRail({chapters}){ const [act, setAct] = useFxS(0); const fillRef = useFxR(null); useFxE(()=>{ let t=false; const f=()=>{ t=false; const mid=(innerHeight||800)*0.46; const tops=chapters.map(c=>{ const el=document.getElementById('ch-'+c.id); return el?el.getBoundingClientRect().top:1e9; }); let a=0; tops.forEach((tp,i)=>{ if(tp