const { useState, useEffect, useRef } = React; /* Scroll reveal: fade + rise + blur, once. */ function useReveal(delay = 0) { const ref = useRef(null); const [shown, setShown] = useState(false); useEffect(() => { if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) { setShown(true); return; } const el = ref.current; if (!el) return; const io = new IntersectionObserver((es) => { es.forEach((e) => { if (e.isIntersecting) { setTimeout(() => setShown(true), delay); io.disconnect(); } }); }, { threshold: 0.12, rootMargin: "0px 0px -8% 0px" }); io.observe(el); return () => io.disconnect(); }, [delay]); return [ref, shown]; } function Reveal({ children, delay = 0, y = 22, as = "div", style, ...rest }) { const [ref, shown] = useReveal(delay); const Tag = as; return React.createElement(Tag, { ref, ...rest, style: { opacity: shown ? 1 : 0, transform: shown ? "none" : `translateY(${y}px)`, filter: shown ? "blur(0)" : "blur(6px)", transition: "opacity .7s cubic-bezier(.2,.7,.3,1), transform .7s cubic-bezier(.2,.7,.3,1), filter .7s cubic-bezier(.2,.7,.3,1)", ...style } }, children); } /* Count up to a target once visible. Preserves suffixes like K and +. */ function Counter({ value, duration = 1200 }) { const m = String(value).match(/^([\d.]+)(.*)$/); const target = m ? parseFloat(m[1]) : 0; const suffix = m ? m[2] : ""; const decimals = m && m[1].includes(".") ? 1 : 0; const [ref, shown] = useReveal(0); const [n, setN] = useState(0); useEffect(() => { if (!shown) return; if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) { setN(target); return; } let raf, t0; const step = (t) => { if (!t0) t0 = t; const p = Math.min(1, (t - t0) / duration); setN(target * (1 - Math.pow(1 - p, 3))); if (p < 1) raf = requestAnimationFrame(step); }; raf = requestAnimationFrame(step); return () => cancelAnimationFrame(raf); }, [shown, target, duration]); return {n.toFixed(decimals)}{suffix}; } /* Parallax translate driven by scroll position. */ function useParallax(strength = 0.12) { const ref = useRef(null); useEffect(() => { if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return; let raf = 0; const onScroll = () => { if (raf) return; raf = requestAnimationFrame(() => { raf = 0; const el = ref.current; if (!el) return; const r = el.getBoundingClientRect(); const mid = r.top + r.height / 2 - window.innerHeight / 2; el.style.transform = `translate3d(0, ${(-mid * strength).toFixed(1)}px, 0)`; }); }; onScroll(); window.addEventListener("scroll", onScroll, { passive: true }); return () => { window.removeEventListener("scroll", onScroll); if (raf) cancelAnimationFrame(raf); }; }, [strength]); return ref; } /* Pointer-following glow, confined to the element. */ function GlowSurface({ children, radius = 420, style, ...rest }) { const ref = useRef(null); const onMove = (e) => { const el = ref.current; if (!el) return; const r = el.getBoundingClientRect(); el.style.setProperty("--gx", `${e.clientX - r.left}px`); el.style.setProperty("--gy", `${e.clientY - r.top}px`); el.style.setProperty("--go", "1"); }; const onLeave = () => { const el = ref.current; if (el) el.style.setProperty("--go", "0"); }; return (