// Main app function App() { const [scrolled, setScrolled] = React.useState(false); React.useEffect(() => { const onScroll = () => setScrolled(window.scrollY > 16); onScroll(); window.addEventListener('scroll', onScroll); return () => window.removeEventListener('scroll', onScroll); }, []); // Scroll-reveal observer — defensive: content is visible by default in CSS, // we opt into the hide/animate behavior only when JS + IO are both available. React.useEffect(() => { const reduceMotion = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches; if (reduceMotion || typeof IntersectionObserver === 'undefined') return; const root = document.documentElement; root.classList.add('js-reveal'); const els = Array.from(document.querySelectorAll('.reveal')); const io = new IntersectionObserver((entries) => { entries.forEach(e => { if (e.isIntersecting) { e.target.classList.add('is-visible'); io.unobserve(e.target); } }); }, { rootMargin: '0px 0px -10% 0px', threshold: 0.01 }); // Immediately mark anything already within the viewport on first paint. const reveal = (el) => el.classList.add('is-visible'); requestAnimationFrame(() => { const vh = window.innerHeight || 800; els.forEach(el => { const r = el.getBoundingClientRect(); if (r.top < vh * 0.95) reveal(el); else io.observe(el); }); }); // Safety fallback: if anything is still hidden after 1.2s, reveal it. const fallback = setTimeout(() => { document.querySelectorAll('.reveal:not(.is-visible)').forEach(reveal); }, 1200); return () => { io.disconnect(); clearTimeout(fallback); }; }, []); return ( <>