File size: 5,159 Bytes
3fd3412 587e3b6 3fd3412 587e3b6 3fd3412 587e3b6 3fd3412 587e3b6 3fd3412 587e3b6 3fd3412 587e3b6 3fd3412 587e3b6 3fd3412 587e3b6 3fd3412 587e3b6 3fd3412 587e3b6 3fd3412 587e3b6 3fd3412 587e3b6 3fd3412 587e3b6 3fd3412 587e3b6 3fd3412 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 | (() => {
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(); } });
})();
|