/* App entry */ const TWEAK_DEFAULTS = { "palette": "pine", "density": "regular", "marqueeSpeed": 50, "theme": "light", "fontPair": "instrument-inter" }; const PALETTES = { pine: { bg: "#FFFFFF", bg2: "#F3F7F0", ink: "#14281A", paper: "#FFFFFF", accent: "#1F6B3B", accentDeep: "#14512B" }, slate: { bg: "#FFFFFF", bg2: "#F3F7F0", ink: "#14281A", paper: "#FFFFFF", accent: "#1F6B3B", accentDeep: "#14512B" } }; const FONT_PAIRS = { "instrument-inter": { serif: '"Instrument Serif", Georgia, serif', sans: '"Inter Tight", "Helvetica Neue", Helvetica, Arial, sans-serif' }, "fraunces-dm": { serif: '"Fraunces", Georgia, serif', sans: '"DM Sans", "Helvetica Neue", Helvetica, Arial, sans-serif' }, "spectral-spaceg": { serif: '"Spectral", Georgia, serif', sans: '"Space Grotesk", "Helvetica Neue", Helvetica, Arial, sans-serif' } }; function applyTweaks(t) { const r = document.documentElement; const p = PALETTES[t.palette] || PALETTES.slate; if (t.theme !== "dark") { r.style.setProperty("--bg", p.bg); r.style.setProperty("--bg-2", p.bg2); r.style.setProperty("--ink", p.ink); r.style.setProperty("--paper", p.paper); r.style.setProperty("--accent", p.accent); r.style.setProperty("--accent-deep", p.accentDeep); } else { r.style.removeProperty("--bg"); r.style.removeProperty("--bg-2"); r.style.removeProperty("--ink"); r.style.removeProperty("--paper"); r.style.removeProperty("--accent"); r.style.removeProperty("--accent-deep"); } document.body.dataset.theme = t.theme; r.style.setProperty("--density", t.density === "compact" ? 0.78 : t.density === "spacious" ? 1.18 : 1); r.style.setProperty("--marquee-speed", `${t.marqueeSpeed}s`); const fp = FONT_PAIRS[t.fontPair] || FONT_PAIRS["instrument-inter"]; r.style.setProperty("--serif", fp.serif); r.style.setProperty("--sans", fp.sans); } const { motion } = window.Motion || { motion: {} }; const Reveal = ({ children, delay = 0, className = "" }) => { if (!window.Motion) return
{children}
; return ( {children} ); }; const StatCount = ({ to, suffix = "", prefix = "", decimal = false }) => { if (!window.Motion) return {prefix}{to}{suffix}; const ref = React.useRef(null); const inView = window.Motion.useInView(ref, { once: true, margin: "-20%" }); const [val, setVal] = React.useState(0); React.useEffect(() => { if (!inView) return; const controls = window.Motion.animate(0, to, { duration: 1.4, ease: [0.16, 1, 0.3, 1], onUpdate: (v) => setVal(v) }); return () => controls.stop(); }, [inView, to]); return {prefix}{decimal ? val.toFixed(1) : Math.round(val)}{suffix}; }; Object.assign(window, { Reveal, motion, StatCount }); const App = () => { const [tweaks, setTweaks] = window.useTweaks(TWEAK_DEFAULTS); React.useEffect(() => { applyTweaks(tweaks); }, [tweaks]); // Setup Lenis Smooth Scroll React.useEffect(() => { if (!window.Lenis) return; if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return; // Guard against a stray instance surviving a hot-reload / remount. if (window.lenis) { try { window.lenis.destroy(); } catch (e) { } window.lenis = null; } const isMobile = window.matchMedia('(max-width: 767px)').matches; // Note: when `lerp` is set, Lenis ignores `duration`; lerp governs the feel. // Lower lerp = softer/heavier; higher = snappier. Mobile keeps native touch // scrolling (syncTouch:false), so smoothing only applies to the wheel. // // 0.12 is roughly a 130ms time constant. Going one way that just reads as // "smooth", but on a reversal the position has to decelerate through zero // before it can head back — which is why the page felt like it lagged // behind the wheel specifically when scrolling up. 0.18 keeps the glide and // cuts the turnaround to something the hand reads as immediate. The wheel // multiplier goes back to 1: at 0.9 the page moved less than the input said // it should, which compounds the same "heavy" impression. const lenis = new window.Lenis({ lerp: isMobile ? 0.2 : 0.18, smoothWheel: true, syncTouch: false, // native, low-latency touch scrolling on mobile wheelMultiplier: 1, touchMultiplier: 1.1, infinite: false, autoResize: true }); window.lenis = lenis; // Keep GSAP ScrollTrigger in sync with Lenis-driven scroll position. let onScroll; if (window.ScrollTrigger) { onScroll = () => window.ScrollTrigger.update(); lenis.on('scroll', onScroll); } // Single RAF source of truth. Prefer GSAP's ticker (one shared loop); // fall back to a self-cancelling requestAnimationFrame loop otherwise. let rafId = 0; let tickerFn = null; if (window.gsap) { tickerFn = (time) => lenis.raf(time * 1000); // GSAP ticker time is in seconds window.gsap.ticker.add(tickerFn); window.gsap.ticker.lagSmoothing(0); } else { const raf = (time) => { lenis.raf(time); rafId = requestAnimationFrame(raf); }; rafId = requestAnimationFrame(raf); } return () => { if (onScroll) lenis.off('scroll', onScroll); if (tickerFn && window.gsap) window.gsap.ticker.remove(tickerFn); // fixes the RAF leak if (rafId) cancelAnimationFrame(rafId); lenis.destroy(); window.lenis = null; }; }, []); // Handle clean URLs for anchor links React.useEffect(() => { window.__nav = (e, targetId) => { e.preventDefault(); const targetEl = document.getElementById(targetId); if (window.lenis) { window.lenis.scrollTo(targetId === 'top' ? 0 : `#${targetId}`); } else { if (targetEl) { targetEl.scrollIntoView({ behavior: 'smooth' }); } else if (targetId === 'top') { window.scrollTo({ top: 0, behavior: 'smooth' }); } } }; }, []); return ( <>