/** * Asterion Bricks — main JS * * Vanilla JS, defer-loaded. Wraps everything in DOMContentLoaded. * Performance budget : initial bundle < 200 KB gzipped. * * Modules planned : * - Header scroll behavior (background swap after 80px) * - Mega menu trigger (hover desktop, click touch, ESC closes) * - Mobile drawer (full-screen overlay, accordion sections) * - Scroll-triggered fade-in (IntersectionObserver, threshold 0.15) * - Stat callouts count-up (1000ms ease-out) * - Hero video lazy-load + reduced-motion fallback to poster */ (function () { 'use strict'; /** * Detect reduced motion once and expose a flag for downstream modules. * Updated reactively if the user toggles the setting mid-session. */ const reducedMotionQuery = window.matchMedia('(prefers-reduced-motion: reduce)'); let prefersReducedMotion = reducedMotionQuery.matches; reducedMotionQuery.addEventListener('change', (e) => { prefersReducedMotion = e.matches; }); /** * Header scroll behavior. * After 80px scroll, header gets a `is-scrolled` class * (consumed by .av-header.is-scrolled rule in components.css). */ function initHeaderScroll() { const header = document.querySelector('.av-header'); if (!header) return; let ticking = false; const onScroll = () => { if (!ticking) { window.requestAnimationFrame(() => { header.classList.toggle('is-scrolled', window.scrollY > 80); ticking = false; }); ticking = true; } }; onScroll(); window.addEventListener('scroll', onScroll, { passive: true }); } /** * Scroll-triggered fade-in (Design Handoff section 5.3). * Sections fade-in + slide-up 16px on entry, threshold 0.15, trigger once. * Skipped entirely if user prefers reduced motion. */ function initScrollReveal() { if (prefersReducedMotion) return; const targets = document.querySelectorAll('[data-av-reveal]'); if (!targets.length) return; const observer = new IntersectionObserver((entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { entry.target.classList.add('is-revealed'); observer.unobserve(entry.target); } }); }, { threshold: 0.15, rootMargin: '0px 0px -10% 0px' }); targets.forEach((target) => observer.observe(target)); } /** * Stat count-up (Design Handoff section 5.3). * Element : target * Animates from 0 to target value over duration ms, ease-out. */ function initStatCountUp() { if (prefersReducedMotion) return; const stats = document.querySelectorAll('[data-av-countup]'); if (!stats.length) return; const observer = new IntersectionObserver((entries) => { entries.forEach((entry) => { if (!entry.isIntersecting) return; const el = entry.target; const target = parseFloat(el.dataset.avCountup) || 0; const duration = parseInt(el.dataset.avCountupDuration || '1000', 10); const start = performance.now(); const tick = (now) => { const elapsed = Math.min((now - start) / duration, 1); // ease-out cubic const eased = 1 - Math.pow(1 - elapsed, 3); el.textContent = Math.round(target * eased).toLocaleString(); if (elapsed < 1) requestAnimationFrame(tick); }; requestAnimationFrame(tick); observer.unobserve(el); }); }, { threshold: 0.5 }); stats.forEach((s) => observer.observe(s)); } /** * Mobile drawer toggle. * Trigger : * Drawer : */ function initMobileDrawer() { const toggle = document.querySelector('.av-drawer-toggle'); const drawer = document.getElementById('av-drawer'); if (!toggle || !drawer) return; const open = () => { drawer.classList.add('is-open'); toggle.setAttribute('aria-expanded', 'true'); document.body.style.overflow = 'hidden'; }; const close = () => { drawer.classList.remove('is-open'); toggle.setAttribute('aria-expanded', 'false'); document.body.style.overflow = ''; }; toggle.addEventListener('click', () => { drawer.classList.contains('is-open') ? close() : open(); }); document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && drawer.classList.contains('is-open')) close(); }); } /** * Boot. */ document.addEventListener('DOMContentLoaded', () => { initHeaderScroll(); initScrollReveal(); initStatCountUp(); initMobileDrawer(); }); })();