// Smooth scroll for navigation links function getHeaderHeight() { const header = document.querySelector('header'); return header ? header.offsetHeight : 80; } function smoothScrollToSection(hash) { const target = document.querySelector(hash); if (!target) return; // Recalculate header height at the moment of scrolling const headerHeight = getHeaderHeight(); // Use getBoundingClientRect for robust position calc across layouts const targetTopRelative = target.getBoundingClientRect().top; const absoluteTargetTop = window.scrollY + targetTopRelative; // Exact offset: align section start just below the header bottom const top = Math.max(absoluteTargetTop - headerHeight, 0); window.scrollTo({ top, behavior: 'smooth' }); } function updateURLHash(hash) { // Update URL hash without triggering scroll if (hash && hash !== '#') { history.pushState(null, null, hash); } } function scrollToTop() { // Remove hash from URL and scroll to top history.pushState(null, null, window.location.pathname); window.scrollTo({ top: 0, behavior: 'smooth' }); } document.querySelectorAll('nav a[href^="#"]').forEach(anchor => { anchor.addEventListener('click', function (e) { e.preventDefault(); // If mobile menu is open, close it first so header height is correct const siteNavEl = document.getElementById('site-nav'); const menuBtn = document.getElementById('menu-toggle'); const wasOpen = siteNavEl && siteNavEl.classList.contains('open'); if (wasOpen) { siteNavEl.classList.remove('open'); if (menuBtn) menuBtn.setAttribute('aria-expanded', 'false'); // Wait for next frame so layout updates and header height is correct requestAnimationFrame(() => { smoothScrollToSection(this.getAttribute('href')); updateURLHash(this.getAttribute('href')); }); } else { smoothScrollToSection(this.getAttribute('href')); updateURLHash(this.getAttribute('href')); } }); }); // Add active class to navigation based on scroll position const sections = document.querySelectorAll('section[id]'); const navLinks = document.querySelectorAll('nav a'); function setActiveNav() { let current = ''; const headerHeight = getHeaderHeight(); sections.forEach(section => { const sectionTop = section.offsetTop; const sectionHeight = section.clientHeight; // Account for fixed header in active link detection if (window.scrollY + headerHeight + 30 >= sectionTop && window.scrollY < sectionTop + sectionHeight) { current = section.getAttribute('id'); } }); navLinks.forEach(link => { link.classList.remove('active'); if (link.getAttribute('href') === `#${current}`) { link.classList.add('active'); } }); } window.addEventListener('scroll', setActiveNav); window.addEventListener('load', setActiveNav); // Adjust on resize (header height can change) window.addEventListener('resize', () => { // Re-run active link calculation to keep alignment correct setActiveNav(); }); // Handle direct hash navigation (e.g., page load with #section) window.addEventListener('load', () => { if (location.hash) { // Use the same precise scroll logic on initial load smoothScrollToSection(location.hash); } }); // Lightbox for screenshots (desktop only behavior) const lightbox = document.getElementById('lightbox'); const lightboxImg = document.getElementById('lightbox-image'); const lightboxCaption = document.getElementById('lightbox-caption'); function openLightbox(src, caption) { if (!lightbox) return; lightboxImg.src = src; lightboxCaption.textContent = caption || ''; lightbox.setAttribute('aria-hidden', 'false'); document.body.style.overflow = 'hidden'; } function closeLightbox() { if (!lightbox) return; lightbox.setAttribute('aria-hidden', 'true'); lightboxImg.src = ''; document.body.style.overflow = ''; } // Delegate clicks on screenshots document.addEventListener('click', (e) => { const img = e.target.closest('.screenshot img'); if (img) { // Only open overlay on wider screens if (window.innerWidth >= 768) { openLightbox(img.src, img.alt || img.nextElementSibling?.textContent || ''); e.preventDefault(); } } if (e.target.matches('[data-close]')) { closeLightbox(); } }); // Close on ESC window.addEventListener('keydown', (e) => { if (e.key === 'Escape') closeLightbox(); }); // Mobile burger menu const menuToggle = document.getElementById('menu-toggle'); const siteNav = document.getElementById('site-nav'); if (menuToggle && siteNav) { menuToggle.addEventListener('click', () => { const isOpen = siteNav.classList.toggle('open'); menuToggle.setAttribute('aria-expanded', isOpen ? 'true' : 'false'); }); // Close menu after navigating siteNav.querySelectorAll('a').forEach(link => { link.addEventListener('click', () => { if (siteNav.classList.contains('open')) { siteNav.classList.remove('open'); menuToggle.setAttribute('aria-expanded', 'false'); } }); }); } // Handle SPORE title click to scroll to top and remove hash const sporeTitle = document.querySelector('.brand h1'); if (sporeTitle) { sporeTitle.addEventListener('click', scrollToTop); sporeTitle.style.cursor = 'pointer'; }