/* Studio 76 — app shell, tweaks, motion orchestration. */ const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{ "accent": "#C2552B", "displayFont": "Archivo", "heroImage": "noir-a", "heroLayout": "split", "heroDark": true, "grain": true }/*EDITMODE-END*/; const DISPLAY_FONTS = { 'Archivo': { family: "'Archivo', sans-serif", weight: 900, tracking: '-0.02em' }, 'Big Shoulders Display':{ family: "'Big Shoulders Display', sans-serif",weight: 800, tracking: '0.005em' }, 'Anton': { family: "'Anton', sans-serif", weight: 400, tracking: '0.005em' }, }; function useScrollReveal(dep) { React.useEffect(() => { // Enable the entrance only if the rendering timeline is actually live. // A throttled/offscreen iframe freezes WAAPI, so .finished never resolves // and we leave content fully visible (no opacity:0 trap). let cancelled = false; try { const probe = document.createElement('div'); probe.style.cssText = 'position:fixed;width:1px;height:1px;opacity:0;pointer-events:none;left:-9px;top:-9px'; document.body.appendChild(probe); const anim = probe.animate([{ opacity: 0 }, { opacity: 1 }], { duration: 30 }); anim.finished.then(() => { if (!cancelled) document.documentElement.classList.add('reveal-on'); probe.remove(); check(); }).catch(() => probe.remove()); } catch (e) { /* no WAAPI → stay visible */ } let raf = 0; function check() { raf = 0; const vh = window.innerHeight; document.querySelectorAll('.reveal:not(.in)').forEach((el) => { const r = el.getBoundingClientRect(); if (r.top < vh * 0.9 && r.bottom > 0) el.classList.add('in'); }); } const onScroll = () => { if (!raf) raf = requestAnimationFrame(check); }; check(); const t1 = setTimeout(check, 200); const t2 = setTimeout(check, 800); window.addEventListener('scroll', onScroll, { passive: true }); window.addEventListener('resize', onScroll); return () => { cancelled = true; window.removeEventListener('scroll', onScroll); window.removeEventListener('resize', onScroll); clearTimeout(t1); clearTimeout(t2); if (raf) cancelAnimationFrame(raf); }; }, [dep]); } function App() { const [t, setTweak] = useTweaks(TWEAK_DEFAULTS); // apply theme tweaks to :root React.useEffect(() => { const r = document.documentElement.style; r.setProperty('--accent', t.accent); const df = DISPLAY_FONTS[t.displayFont] || DISPLAY_FONTS['Archivo']; r.setProperty('--font-display', df.family); r.setProperty('--display-weight', df.weight); r.setProperty('--display-tracking', df.tracking); }, [t.accent, t.displayFont]); useScrollReveal(JSON.stringify([t.heroLayout, t.heroImage, t.heroDark])); // ?hero=ivory|noir-a|noir-b — shareable hero-variant preview, overrides the tweak const heroPick = React.useMemo(() => { const q = new URLSearchParams(window.location.search).get('hero'); return ['noir-a', 'noir-b', 'ivory'].indexOf(q) >= 0 ? q : t.heroImage; }, [t.heroImage]); const begin = React.useCallback(() => { const el = document.getElementById('studio'); if (el) window.scrollTo({ top: el.getBoundingClientRect().top + window.scrollY - 70, behavior: 'smooth' }); }, []); return ( {t.grain &&
}