(() => { 'use strict'; const nav = document.getElementById('nav'); const burger = document.getElementById('burger'); const navLinks = document.querySelector('.nav__links'); const yearEl = document.getElementById('year'); const parallaxEls = Array.from(document.querySelectorAll('[data-parallax]')); const revealEls = Array.from(document.querySelectorAll('.reveal')); if (yearEl) yearEl.textContent = new Date().getFullYear(); /* ---- Sticky / scrolled nav ---- */ const onScroll = () => { if (window.scrollY > 30) nav.classList.add('is-scrolled'); else nav.classList.remove('is-scrolled'); updateParallax(); }; /* ---- Mobile menu toggle ---- */ if (burger && navLinks) { burger.addEventListener('click', () => { const open = navLinks.classList.toggle('is-open'); burger.classList.toggle('is-open', open); burger.setAttribute('aria-expanded', String(open)); }); navLinks.querySelectorAll('a').forEach((a) => a.addEventListener('click', () => { navLinks.classList.remove('is-open'); burger.classList.remove('is-open'); burger.setAttribute('aria-expanded', 'false'); }) ); } /* ---- Parallax (rAF-throttled) ---- */ let ticking = false; const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches; function updateParallax() { if (reduced) return; if (ticking) return; ticking = true; requestAnimationFrame(() => { const vh = window.innerHeight; const vw = window.innerWidth; parallaxEls.forEach((el) => { const rect = el.getBoundingClientRect(); // Only process elements roughly in / near the viewport if (rect.bottom < -vh || rect.top > vh * 2) return; const speed = parseFloat(el.dataset.parallax) || 0.2; // Distance of element center from viewport center const center = rect.top + rect.height / 2 - vh / 2; const offset = -center * speed; // Use translate3d for GPU acceleration el.style.transform = `translate3d(0, ${offset.toFixed(1)}px, 0)`; }); ticking = false; }); } /* ---- Scroll reveal via IntersectionObserver ---- */ if ('IntersectionObserver' in window) { const io = new IntersectionObserver( (entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { entry.target.classList.add('is-visible'); io.unobserve(entry.target); } }); }, { threshold: 0.15, rootMargin: '0px 0px -60px 0px' } ); revealEls.forEach((el) => io.observe(el)); } else { revealEls.forEach((el) => el.classList.add('is-visible')); } /* ---- Smooth-scroll offset for fixed nav ---- */ document.querySelectorAll('a[href^="#"]').forEach((a) => { a.addEventListener('click', (e) => { const id = a.getAttribute('href'); if (id.length < 2) return; const target = document.querySelector(id); if (!target) return; e.preventDefault(); const navH = nav.offsetHeight; const top = target.getBoundingClientRect().top + window.scrollY - navH + 4; window.scrollTo({ top, behavior: 'smooth' }); }); }); /* ---- Listeners ---- */ window.addEventListener('scroll', onScroll, { passive: true }); window.addEventListener('resize', updateParallax); // initial paint onScroll(); updateParallax(); })();