/* Motion — two signature moments only: the hero headline reveal and counting stats. Everything else on the page is a calm scroll-fade. */ const prefersReduced = () => window.matchMedia('(prefers-reduced-motion: reduce)').matches; /* Count a number up when it scrolls into view. Parses "+1500" / "+1M" / "98%" and preserves the prefix/suffix exactly as written. */ function useCountUp(ref, raw, duration = 1600) { React.useEffect(() => { const el = ref.current; if (!el) return; const m = String(raw).match(/^([^\d]*)([\d.]+)(.*)$/); if (!m || prefersReduced()) return; const [, prefix, numStr, suffix] = m; const target = parseFloat(numStr); const decimals = (numStr.split('.')[1] || '').length; let raf, started = false; const io = new IntersectionObserver((entries) => { entries.forEach((e) => { if (!e.isIntersecting || started) return; started = true; const t0 = performance.now(); const tick = (now) => { const p = Math.min(1, (now - t0) / duration); const eased = 1 - Math.pow(1 - p, 3); const val = target * eased; el.textContent = prefix + val.toFixed(decimals) + suffix; if (p < 1) raf = requestAnimationFrame(tick); }; el.textContent = prefix + '0' + suffix; raf = requestAnimationFrame(tick); io.unobserve(e.target); }); }, { threshold: 0.5 }); io.observe(el); return () => { io.disconnect(); if (raf) cancelAnimationFrame(raf); }; }, [raw]); } function CountUp({ value, className }) { const ref = React.useRef(null); useCountUp(ref, value); return {value}; } /* Split a string into per-word spans so CSS can cascade a reveal across them. The inter-word space is its OWN element: a trailing space inside an inline-block with overflow:hidden gets collapsed away by the browser, which runs the words together. */ function SplitWords({ text, className, delay = 0, step = 55 }) { const words = String(text).split(' '); return ( {words.map((w, i) => ( {w} {i < words.length - 1 ? : null} ))} ); } /* Duration from the track's real width, so the belt always moves at a constant px/second no matter how many chips it holds. */ function useMarquee(speed = 55) { const ref = React.useRef(null); React.useEffect(() => { const el = ref.current; if (!el) return; const apply = () => { const span = el.scrollWidth / 3; if (span > 0) el.style.animationDuration = (span / speed).toFixed(2) + 's'; }; apply(); const ro = new ResizeObserver(apply); ro.observe(el); return () => ro.disconnect(); }, [speed]); return ref; } Object.assign(window, { useMarquee, useCountUp, CountUp, SplitWords, prefersReduced });