const MOTION_QUERY = '(prefers-reduced-motion: reduce)'; function prefersReducedMotion() { return window.matchMedia(MOTION_QUERY).matches; } function enableEnhancement() { if (prefersReducedMotion() || typeof IntersectionObserver !== 'function') { document.documentElement.removeAttribute('data-motion'); return false; } document.documentElement.dataset.motion = 'enhance'; return true; } function activatePageOpen(enhance) { document.querySelectorAll('[data-motion="page-open"]').forEach((node) => { if (!enhance) { node.classList.add('is-active'); return; } requestAnimationFrame(() => { requestAnimationFrame(() => node.classList.add('is-active')); }); }); } function initializeMotionDelays() { const beatOrder = new Map([ ['seal', 0], ['heading', 0], ['title', 1], ['media', 2], ['cta', 3], ]); document.querySelectorAll('[data-motion="page-open"]').forEach((page) => { page.querySelectorAll('[data-motion-beat]').forEach((beat, index) => { const namedIndex = beatOrder.get(beat.dataset.motionBeat); beat.style.setProperty('--motion-index', String(Math.min(namedIndex ?? index, 3))); }); }); document.querySelectorAll('[data-reveal-group]').forEach((group) => { const nodes = Array.from(group.querySelectorAll('[data-reveal]')) .filter((node) => node.closest('[data-reveal-group]') === group); nodes.forEach((node, index) => { node.style.setProperty('--motion-index', String(Math.min(index, 3))); }); }); } function observeReveals(enhance) { const nodes = Array.from(document.querySelectorAll('[data-reveal]')); if (nodes.length === 0) { return; } if (!enhance || typeof IntersectionObserver !== 'function') { nodes.forEach((node) => node.classList.add('is-revealed')); return; } const observer = new IntersectionObserver( (entries) => { entries.forEach((entry) => { if (!entry.isIntersecting) { return; } entry.target.classList.add('is-revealed'); observer.unobserve(entry.target); }); }, { rootMargin: '0px 0px -12% 0px', threshold: 0.2, }, ); nodes.forEach((node) => observer.observe(node)); } function setupChapterIndex() { const index = document.querySelector('[data-chapter-index]'); const progress = document.querySelector('[data-chapter-progress] span'); const chapters = Array.from(document.querySelectorAll('[data-chapter]')); if (!index || chapters.length === 0) { return; } const links = Array.from(index.querySelectorAll('a[href^="#"]')); let framePending = false; const setActive = (id) => { links.forEach((link) => { const isCurrent = link.getAttribute('href') === `#${id}`; if (isCurrent) { link.setAttribute('aria-current', 'true'); } else { link.removeAttribute('aria-current'); } }); }; const updateProgress = (ratio) => { if (!progress) { return; } const clamped = Math.min(1, Math.max(0, ratio)); progress.style.setProperty('--chapter-progress', `${(clamped * 100).toFixed(2)}%`); }; const sync = () => { const marker = window.scrollY + Math.min(window.innerHeight * 0.35, 280); let current = chapters[0]; chapters.forEach((chapter) => { if (chapter.offsetTop <= marker) { current = chapter; } }); const heading = current.querySelector('[id$="-heading"]') || document.getElementById(`${current.dataset.chapter}-heading`); const headingId = heading?.id || current.getAttribute('aria-labelledby') || `${current.dataset.chapter}-heading`; setActive(headingId); const doc = document.documentElement; const max = Math.max(1, doc.scrollHeight - window.innerHeight); updateProgress(window.scrollY / max); }; const scheduleSync = () => { if (framePending) { return; } framePending = true; requestAnimationFrame(() => { framePending = false; sync(); }); }; setActive(chapters[0].getAttribute('aria-labelledby') || 'hero-heading'); sync(); window.addEventListener('scroll', scheduleSync, { passive: true }); window.addEventListener('resize', scheduleSync); } function bootMotion() { initializeMotionDelays(); const enhance = enableEnhancement(); activatePageOpen(enhance); observeReveals(enhance); setupChapterIndex(); window.matchMedia(MOTION_QUERY).addEventListener('change', (event) => { if (event.matches) { document.documentElement.removeAttribute('data-motion'); document.querySelectorAll('[data-motion="page-open"]').forEach((node) => { node.classList.add('is-active'); }); document.querySelectorAll('[data-reveal]').forEach((node) => { node.classList.add('is-revealed'); }); return; } document.documentElement.dataset.motion = 'enhance'; }); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', bootMotion, { once: true }); } else { bootMotion(); }