// Shared public-site behavior: scroll-reveal animations and the mobile // nav toggle. Loaded by both index.php and menu.php. // Reveal-on-scroll (() => { if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return; const els = document.querySelectorAll('.reveal'); if (!('IntersectionObserver' in window)) { els.forEach((el) => el.classList.add('is-visible')); return; } const io = new IntersectionObserver((entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { entry.target.classList.add('is-visible'); io.unobserve(entry.target); } }); }, { threshold: 0.15 }); els.forEach((el) => io.observe(el)); })(); // Mobile nav toggle (() => { const toggle = document.getElementById('nav-toggle'); const nav = document.getElementById('primary-nav'); if (!toggle || !nav) return; function closeNav() { nav.classList.remove('is-open'); toggle.setAttribute('aria-expanded', 'false'); } function openNav() { nav.classList.add('is-open'); toggle.setAttribute('aria-expanded', 'true'); } toggle.addEventListener('click', () => { if (nav.classList.contains('is-open')) { closeNav(); } else { openNav(); } }); // Close after following a link nav.querySelectorAll('a').forEach((link) => { link.addEventListener('click', closeNav); }); // Close on Escape window.addEventListener('keydown', (e) => { if (e.key === 'Escape') closeNav(); }); // Close on outside click document.addEventListener('click', (e) => { if (!nav.classList.contains('is-open')) return; if (nav.contains(e.target) || toggle.contains(e.target)) return; closeNav(); }); })(); // Wrap-aware separator dots (announcement banner, hero tagline row). // Hides a "ยท" only when its two flanking elements wrap onto different // visual lines. There's no CSS way to detect "same line as sibling," and // a fixed viewport breakpoint can't substitute for it either, since // wrapping depends on the announcement's own text length, not just // screen width. (() => { const seps = document.querySelectorAll('.wrap-sep'); if (!seps.length) return; function sameLine(a, b) { const ra = a.getBoundingClientRect(); const rb = b.getBoundingClientRect(); return ra.top < rb.bottom && rb.top < ra.bottom; } // Hiding a separator removes it from flow (see .wrap-sep-hidden in // styles.css โ€” needs to, so a hidden dot never skews centering by still // reserving its space), which means one separator's hide/show can shift // where a *later* one's neighbors land โ€” e.g. the tagline row's second // dot needs to see the first dot's decision already applied, or it may // judge its own neighbors' line-sharing from a layout that's since // changed underneath it. So this deliberately processes separators one // at a time, in DOM order, applying each decision before measuring the // next: reset just this one to visible (so its own contribution is // never argued away by whatever its own current hidden state already // happens to be โ€” otherwise "hidden" would be self-confirming, since a // hidden dot's neighbors always look like they share a line), measure, // decide, apply, then move on with that change already in effect. function sync() { seps.forEach((sep) => { const prev = sep.previousElementSibling; const next = sep.nextElementSibling; if (!prev || !next) return; sep.classList.remove('wrap-sep-hidden'); sep.classList.toggle('wrap-sep-hidden', !sameLine(prev, next)); }); } sync(); let resizeTimer; window.addEventListener('resize', () => { clearTimeout(resizeTimer); resizeTimer = setTimeout(sync, 100); }); if (document.fonts && document.fonts.ready) { document.fonts.ready.then(sync); } })();