/* 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 && }
setTweak('accent', v)} />
setTweak('displayFont', v)} />
setTweak('heroImage', v)} />
setTweak('grain', v)} />
);
}
ReactDOM.createRoot(document.getElementById('root')).render();