1document.addEventListener('DOMContentLoaded', () => { 2 const navbar = document.getElementById('navbar'); 3 const navToggle = document.getElementById('navToggle'); 4 const primaryNav = document.getElementById('primaryNav'); 5 const navLinks = document.querySelectorAll('.nav-link'); 6 const backToTop = document.getElementById('backToTop'); 7 const sections = document.querySelectorAll('main section[id]'); 8 const contactForm = document.getElementById('contactForm'); 9 const formNote = document.getElementById('formNote'); 10 const yearEl = document.getElementById('year'); 11 12 if (yearEl) yearEl.textContent = new Date().getFullYear(); 13 14 // Navbar background + back-to-top visibility on scroll 15 const onScroll = () => { 16 const scrolled = window.scrollY > 20; 17 navbar.classList.toggle('scrolled', scrolled); 18 backToTop.classList.toggle('show', window.scrollY > 500); 19 }; 20 onScroll(); 21 window.addEventListener('scroll', onScroll, { passive: true }); 22 23 // Mobile menu toggle 24 navToggle.addEventListener('click', () => { 25 const isOpen = primaryNav.classList.toggle('open'); 26 navToggle.classList.toggle('open', isOpen); 27 navToggle.setAttribute('aria-expanded', String(isOpen)); 28 }); 29 30 // Close mobile menu after tapping a link 31 navLinks.forEach((link) => { 32 link.addEventListener('click', () => { 33 primaryNav.classList.remove('open'); 34 navToggle.classList.remove('open'); 35 navToggle.setAttribute('aria-expanded', 'false'); 36 }); 37 }); 38 39 // Back to top: animated manually so it scrolls smoothly across browsers instead of 40 // relying on native smooth-scroll, which some browsers silently skip. 41 function smoothScrollToTop(duration = 600) { 42 const startY = window.scrollY; 43 if (startY === 0) return; 44 if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) { 45 window.scrollTo(0, 0); 46 return; 47 } 48 const start = performance.now(); 49 function step(now) { 50 const progress = Math.min((now - start) / duration, 1); 51 const eased = 1 - Math.pow(1 - progress, 3); 52 // behavior: 'auto' forces an instant jump per frame; the global CSS 53 // scroll-behavior: smooth would otherwise turn each frame into its own 54 // native smooth-scroll animation, fighting this manual easing. 55 window.scrollTo({ top: startY * (1 - eased), left: 0, behavior: 'auto' }); 56 if (progress < 1) requestAnimationFrame(step); 57 } 58 requestAnimationFrame(step); 59 } 60 61 backToTop.addEventListener('click', () => smoothScrollToTop()); 62 63 // Highlight active nav link based on section in view 64 const observer = new IntersectionObserver( 65 (entries) => { 66 entries.forEach((entry) => { 67 if (entry.isIntersecting) { 68 const id = entry.target.getAttribute('id'); 69 navLinks.forEach((link) => { 70 link.classList.toggle('active', link.getAttribute('href') === `#${id}`); 71 }); 72 } 73 }); 74 }, 75 { rootMargin: '-45% 0px -50% 0px', threshold: 0 } 76 ); 77 sections.forEach((section) => observer.observe(section)); 78 79 // Submits to Netlify Forms via AJAX so the page doesn't reload; requires the site to be deployed on Netlify. 80 if (contactForm) { 81 contactForm.addEventListener('submit', (e) => { 82 e.preventDefault(); 83 const body = new URLSearchParams(new FormData(contactForm)).toString(); 84 fetch('/', { 85 method: 'POST', 86 headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, 87 body, 88 }) 89 .then(() => { 90 formNote.textContent = "Thanks! We'll get back to you within one business day."; 91 contactForm.reset(); 92 }) 93 .catch(() => { 94 formNote.textContent = 'Something went wrong. Please email us at [email protected] instead.'; 95 }); 96 }); 97 } 98});
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.