| (() => { |
| const $ = (s, root = document) => root.querySelector(s); |
| const $$ = (s, root = document) => [...root.querySelectorAll(s)]; |
| const header = $('#siteHeader'); |
| const progress = $('#scrollProgress'); |
| const bottomCta = $('#bottomCta'); |
| const toTop = $('#toTop'); |
| const menu = $('#mobileMenu'); |
| const menuToggle = $('#menuToggle'); |
| const accessPanel = $('#accessPanel'); |
| const root = document.documentElement; |
|
|
| const setLock = (locked) => document.body.classList.toggle('menu-lock', locked); |
|
|
| function onScroll(){ |
| const y = window.scrollY; |
| const h = Math.max(1, document.documentElement.scrollHeight - window.innerHeight); |
| header?.classList.toggle('scrolled', y > 18); |
| if(progress) progress.style.width = `${Math.min(100, (y / h) * 100)}%`; |
|
|
| const ctaSection = $('#cta'); |
| const footer = $('#contact'); |
| const ctaTop = ctaSection ? ctaSection.getBoundingClientRect().top + y : Infinity; |
| const footerTop = footer ? footer.getBoundingClientRect().top + y : Infinity; |
| const shouldShow = y > window.innerHeight * .78 && y + window.innerHeight < Math.min(ctaTop + 120, footerTop); |
| bottomCta?.classList.toggle('show', shouldShow); |
| toTop?.classList.toggle('show', y > 650); |
| } |
| addEventListener('scroll', onScroll, {passive:true}); |
| addEventListener('resize', onScroll, {passive:true}); |
| onScroll(); |
|
|
| if('IntersectionObserver' in window){ |
| const revealObserver = new IntersectionObserver(entries => { |
| entries.forEach(entry => { |
| if(entry.isIntersecting){ |
| entry.target.classList.add('in'); |
| revealObserver.unobserve(entry.target); |
| } |
| }); |
| }, {threshold:.12, rootMargin:'0px 0px -30px 0px'}); |
| $$('.reveal').forEach(el => revealObserver.observe(el)); |
|
|
| const sections = $$('main section[id]'); |
| const navLinks = $$('.nav-links a'); |
| const activeObserver = new IntersectionObserver(entries => { |
| entries.forEach(entry => { |
| if(entry.isIntersecting){ |
| navLinks.forEach(a => a.classList.toggle('active', a.getAttribute('href') === `#${entry.target.id}`)); |
| } |
| }); |
| }, {rootMargin:'-40% 0px -50% 0px'}); |
| sections.forEach(section => activeObserver.observe(section)); |
| } else { |
| $$('.reveal').forEach(el => el.classList.add('in')); |
| } |
|
|
| function openMenu(){ |
| menu?.classList.add('open'); |
| menu?.setAttribute('aria-hidden','false'); |
| menuToggle?.setAttribute('aria-expanded','true'); |
| setLock(true); |
| } |
| function closeMenu(){ |
| menu?.classList.remove('open'); |
| menu?.setAttribute('aria-hidden','true'); |
| menuToggle?.setAttribute('aria-expanded','false'); |
| if(!accessPanel?.classList.contains('open')) setLock(false); |
| } |
| menuToggle?.addEventListener('click', openMenu); |
| $('#closeMenu')?.addEventListener('click', closeMenu); |
| menu?.addEventListener('click', e => { if(e.target === menu || e.target.closest('a')) closeMenu(); }); |
|
|
| function openAccess(){ |
| accessPanel?.classList.add('open'); |
| accessPanel?.setAttribute('aria-hidden','false'); |
| setLock(true); |
| } |
| function closeAccess(){ |
| accessPanel?.classList.remove('open'); |
| accessPanel?.setAttribute('aria-hidden','true'); |
| if(!menu?.classList.contains('open')) setLock(false); |
| } |
| $$('.access-open').forEach(btn => btn.addEventListener('click', openAccess)); |
| $('#closeAccess')?.addEventListener('click', closeAccess); |
| accessPanel?.addEventListener('click', e => { if(e.target === accessPanel) closeAccess(); }); |
|
|
| let saved = {}; |
| try { saved = JSON.parse(localStorage.getItem('psrcAccess') || '{}'); } catch(_) {} |
| if(saved.scale) root.style.setProperty('--font-scale', saved.scale); |
| if(saved.contrast) document.body.classList.add('high-contrast'); |
| if(saved.motion) document.body.classList.add('reduce-motion'); |
|
|
| function saveAccess(){ |
| localStorage.setItem('psrcAccess', JSON.stringify({ |
| scale: getComputedStyle(root).getPropertyValue('--font-scale').trim() || '1', |
| contrast: document.body.classList.contains('high-contrast'), |
| motion: document.body.classList.contains('reduce-motion') |
| })); |
| } |
|
|
| accessPanel?.addEventListener('click', e => { |
| const action = e.target.dataset.access; |
| if(!action) return; |
| const scale = parseFloat(getComputedStyle(root).getPropertyValue('--font-scale')) || 1; |
| if(action === 'fontPlus') root.style.setProperty('--font-scale', Math.min(1.18, scale + .06)); |
| if(action === 'fontMinus') root.style.setProperty('--font-scale', Math.max(.92, scale - .06)); |
| if(action === 'contrast') document.body.classList.toggle('high-contrast'); |
| if(action === 'motion') document.body.classList.toggle('reduce-motion'); |
| if(action === 'reset'){ |
| root.style.setProperty('--font-scale', 1); |
| document.body.classList.remove('high-contrast','reduce-motion'); |
| localStorage.removeItem('psrcAccess'); |
| return; |
| } |
| saveAccess(); |
| }); |
|
|
| toTop?.addEventListener('click', () => window.scrollTo({top:0, behavior: document.body.classList.contains('reduce-motion') ? 'auto' : 'smooth'})); |
| addEventListener('keydown', e => { if(e.key === 'Escape'){ closeMenu(); closeAccess(); } }); |
| })(); |
|
|