// ─── Scroll reveal ───────────────────────────────────────── const reveals = document.querySelectorAll('.reveal'); const observer = new IntersectionObserver((entries) => { entries.forEach(e => { if (e.isIntersecting) { e.target.classList.add('visible'); observer.unobserve(e.target); } }); }, {threshold: 0.1, rootMargin: '0px 0px -40px 0px'}); reveals.forEach(el => observer.observe(el)); // ─── Active nav link on scroll ────────────────────────────── const sections = document.querySelectorAll('section[id]'); const navLinks = document.querySelectorAll('.nav-links a'); window.addEventListener('scroll', () => { let current = ''; sections.forEach(s => { if (window.scrollY >= s.offsetTop - 120) current = s.id; }); navLinks.forEach(a => { a.style.color = a.getAttribute('href') === `#${current}` ? 'var(--mauve)' : ''; }); }); // ─── Project filters ────────────────────────────────────────────── const filterChips = document.querySelectorAll('.filter-chip'); const projectCards = document.querySelectorAll('#project-grid .project-card'); const seeMoreButton = document.querySelector('#projects-see-more'); const DEFAULT_VISIBLE_PROJECTS = 9; if (filterChips.length > 0 && projectCards.length > 0) { let selectedFilter = 'all'; let isExpanded = false; const interactiveSelector = 'a, button, input, textarea, select, summary, [role="button"]'; const goToCardHref = (card, event) => { const href = card.dataset.href; if (!href) { return; } const openInNewTab = event.metaKey || event.ctrlKey || event.button === 1; if (openInNewTab) { window.open(href, '_blank', 'noopener'); return; } globalThis.location.href = href; }; const applyFilter = () => { const visibleCards = []; projectCards.forEach(card => { const tech = (card.dataset.tech || '').split(/\s+/).filter(Boolean); const shouldShow = selectedFilter === 'all' || tech.includes(selectedFilter); card.classList.toggle('is-hidden', !shouldShow); if (shouldShow) { card.classList.remove('is-collapsed'); visibleCards.push(card); } }); if (!isExpanded) { visibleCards.slice(DEFAULT_VISIBLE_PROJECTS).forEach(card => { card.classList.add('is-collapsed'); }); } if (seeMoreButton) { const canExpand = visibleCards.length > DEFAULT_VISIBLE_PROJECTS; seeMoreButton.hidden = !canExpand; seeMoreButton.setAttribute('aria-expanded', canExpand && isExpanded ? 'true' : 'false'); seeMoreButton.textContent = isExpanded ? 'See less' : 'See more'; } }; filterChips.forEach(chip => { chip.addEventListener('click', () => { selectedFilter = chip.dataset.filter || 'all'; isExpanded = false; filterChips.forEach(other => { other.classList.toggle('active', other === chip); }); applyFilter(); }); }); if (seeMoreButton) { seeMoreButton.addEventListener('click', () => { isExpanded = !isExpanded; applyFilter(); }); } projectCards.forEach(card => { if (!card.dataset.href) { return; } card.setAttribute('role', 'link'); card.setAttribute('tabindex', '0'); card.addEventListener('click', event => { const target = event.target; if (!(target instanceof Element)) { return; } // Let inline links and other controls inside the card handle their own clicks. if (target.closest(interactiveSelector) && target.closest(interactiveSelector) !== card) { return; } goToCardHref(card, event); }); card.addEventListener('keydown', event => { if (event.key !== 'Enter' && event.key !== ' ') { return; } const target = event.target; if (!(target instanceof Element)) { return; } if (target.closest(interactiveSelector) && target.closest(interactiveSelector) !== card) { return; } event.preventDefault(); goToCardHref(card, event); }); }); applyFilter(); }