/* 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 (
<>
>
);
};
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render();