/* Manifesto — sticky pinned, words light up tightly bound to scroll */ const Manifesto = () => { const ref = React.useRef(null); const wordsRef = React.useRef([]); const text = "We work after hours because that's when ideas breathe. No roadmaps, no quarterly noise — just careful builds, shipped when they're ready. Each project is a complete thought."; const words = text.split(" "); React.useEffect(() => { let raf = 0; const onScroll = () => { cancelAnimationFrame(raf); raf = requestAnimationFrame(() => { const el = ref.current; if (!el) return; const rect = el.getBoundingClientRect(); const vh = window.innerHeight; // total scroll distance over which the reveal plays const total = rect.height - vh; if (total <= 0) return; // progress: 0 when sticky stage first sticks (rect.top hits 0), // 1 when section is about to exit (rect.top = -total) const progress = Math.max(0, Math.min(1, -rect.top / total)); const eased = Math.min(1, progress / 0.85); const lit = Math.round(eased * words.length); wordsRef.current.forEach((node, i) => { if (!node) return; if (i < lit) node.classList.add("lit"); else node.classList.remove("lit"); }); }); }; window.addEventListener("scroll", onScroll, { passive: true }); window.addEventListener("resize", onScroll); onScroll(); return () => { window.removeEventListener("scroll", onScroll); window.removeEventListener("resize", onScroll); cancelAnimationFrame(raf); }; }, []); return ( Manifesto 01 {words.map((w, i) => { const isItalic = /^(after|hours|breathe|complete\.?)$/i.test(w); return ( (wordsRef.current[i] = el)} > {isItalic ? {w} : w} {" "} ); })} ); }; /* Projects index */ const PROJECTS = [ { n: "01", name: "FiveM Scripts", nameEm: "Scripts", desc: "Server-side systems and tools for FiveM. The first complete project under After-Hours.", tags: "Live · 6 scripts", soon: false, href: "fivem.html", }, { n: "02", name: "Untitled Tool", nameEm: "Tool", desc: "A small utility we keep coming back to. Quietly in development.", tags: "In progress", soon: true, }, { n: "03", name: "Notebook", nameEm: "Notebook", desc: "Notes, experiments, and writing. Public when it's ready.", tags: "Q3 · 2026", soon: true, }, { n: "04", name: "—", desc: "An idea waiting for the right night.", tags: "Reserved", soon: true, }, ]; const Projects = () => { return ( The index. 04 entries 01 live · 03 pending updated 2026 {PROJECTS.map((p) => ( { if (!p.href) e.preventDefault(); }} > {p.n} {p.nameEm ? <>{p.name.replace(p.nameEm, "")}{p.nameEm}> : p.name} {p.desc} {p.tags} {p.soon ? "·" : "↗"} ))} ); }; Object.assign(window, { Manifesto, Projects });