1(() => { 2 'use strict'; 3 4 const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; 5 const loader = document.querySelector('.loader'); 6 const header = document.querySelector('[data-header]'); 7 const progress = document.querySelector('.scroll-progress span'); 8 const menuButton = document.querySelector('.menu-toggle'); 9 const nav = document.querySelector('.nav'); 10 const videoDialog = document.querySelector('[data-video-dialog]'); 11 const videoFrame = videoDialog.querySelector('iframe'); 12 const lightbox = document.querySelector('[data-lightbox]'); 13 const lightboxImage = lightbox.querySelector('figure img'); 14 const lightboxCaption = lightbox.querySelector('figcaption span'); 15 const lightboxCounter = lightbox.querySelector('figcaption strong'); 16 const galleryButtons = [...document.querySelectorAll('[data-gallery-index]')]; 17 let galleryIndex = 0; 18 let lastTrigger = null; 19 let ticking = false; 20 21 function sharedPrivacyButton() { 22 return document.getElementById('cookie-privacy-button'); 23 } 24 25 function hasMediaConsent() { 26 const privacyButton = sharedPrivacyButton(); 27 if (!privacyButton) return true; 28 return Boolean(videoFrame.getAttribute('src')); 29 } 30 31 const gallery = galleryButtons.map((button) => { 32 const image = button.querySelector('img'); 33 return { src: image.src, alt: image.alt }; 34 }); 35 36 function unlockPage() { 37 window.setTimeout(() => loader.classList.add('is-gone'), reducedMotion ? 0 : 1150); 38 } 39 40 if (document.readyState === 'complete') unlockPage(); 41 else window.addEventListener('load', unlockPage, { once: true }); 42 43 function updateScroll() { 44 const y = window.scrollY; 45 const max = Math.max(1, document.documentElement.scrollHeight - window.innerHeight); 46 document.documentElement.style.setProperty('--scroll-y', y.toFixed(1)); 47 progress.style.transform = `scaleX(${Math.min(1, y / max)})`; 48 header.classList.toggle('is-scrolled', y > 30); 49 ticking = false; 50 } 51 52 window.addEventListener('scroll', () => { 53 if (!ticking) { 54 window.requestAnimationFrame(updateScroll); 55 ticking = true; 56 } 57 }, { passive: true }); 58 updateScroll(); 59 60 const reveals = document.querySelectorAll('.reveal'); 61 if ('IntersectionObserver' in window && !reducedMotion) { 62 const observer = new IntersectionObserver((entries) => { 63 entries.forEach((entry) => { 64 if (entry.isIntersecting) { 65 entry.target.classList.add('is-visible'); 66 observer.unobserve(entry.target); 67 } 68 }); 69 }, { rootMargin: '0px 0px -8% 0px', threshold: 0.12 }); 70 reveals.forEach((element, index) => { 71 element.style.transitionDelay = `${Math.min(index % 3, 2) * 70}ms`; 72 observer.observe(element); 73 }); 74 } else { 75 reveals.forEach((element) => element.classList.add('is-visible')); 76 } 77 78 function setMenu(open) { 79 menuButton.setAttribute('aria-expanded', String(open)); 80 nav.classList.toggle('is-open', open); 81 document.body.classList.toggle('is-locked', open); 82 } 83 84 menuButton.addEventListener('click', () => setMenu(menuButton.getAttribute('aria-expanded') !== 'true')); 85 nav.querySelectorAll('a').forEach((link) => link.addEventListener('click', () => setMenu(false))); 86 87 function openVideo(id, trigger) { 88 if (!hasMediaConsent()) { 89 sharedPrivacyButton()?.click(); 90 return; 91 } 92 lastTrigger = trigger; 93 setMenu(false); 94 videoFrame.src = `https://www.youtube-nocookie.com/embed/${encodeURIComponent(id)}?autoplay=1&controls=1&enablejsapi=1&rel=0&modestbranding=1`; 95 videoDialog.showModal(); 96 document.body.classList.add('is-locked'); 97 } 98 99 function closeVideo() { 100 videoFrame.contentWindow?.postMessage(JSON.stringify({ event: 'command', func: 'stopVideo', args: '' }), '*'); 101 videoDialog.close(); 102 document.body.classList.remove('is-locked'); 103 lastTrigger?.focus(); 104 } 105 106 document.querySelectorAll('[data-video]').forEach((button) => { 107 button.addEventListener('click', () => openVideo(button.dataset.video, button)); 108 }); 109 videoDialog.querySelector('.dialog-close').addEventListener('click', closeVideo); 110 videoDialog.addEventListener('cancel', (event) => { event.preventDefault(); closeVideo(); }); 111 videoDialog.addEventListener('click', (event) => { if (event.target === videoDialog) closeVideo(); }); 112 113 document.getElementById('privacySettings')?.addEventListener('click', () => { 114 sharedPrivacyButton()?.click(); 115 }); 116 117 function renderGallery() { 118 const item = gallery[galleryIndex]; 119 lightboxImage.src = item.src; 120 lightboxImage.alt = item.alt; 121 lightboxCaption.textContent = item.alt; 122 lightboxCounter.textContent = `${String(galleryIndex + 1).padStart(2, '0')} / ${String(gallery.length).padStart(2, '0')}`; 123 } 124 125 function openGallery(index, trigger) { 126 galleryIndex = index; 127 lastTrigger = trigger; 128 renderGallery(); 129 lightbox.showModal(); 130 document.body.classList.add('is-locked'); 131 } 132 133 function stepGallery(direction) { 134 galleryIndex = (galleryIndex + direction + gallery.length) % gallery.length; 135 renderGallery(); 136 } 137 138 function closeGallery() { 139 lightbox.close(); 140 document.body.classList.remove('is-locked'); 141 lastTrigger?.focus(); 142 } 143 144 galleryButtons.forEach((button) => button.addEventListener('click', () => openGallery(Number(button.dataset.galleryIndex), button))); 145 lightbox.querySelector('.lightbox__nav--prev').addEventListener('click', () => stepGallery(-1)); 146 lightbox.querySelector('.lightbox__nav--next').addEventListener('click', () => stepGallery(1)); 147 lightbox.querySelector('.dialog-close').addEventListener('click', closeGallery); 148 lightbox.addEventListener('cancel', (event) => { event.preventDefault(); closeGallery(); }); 149 lightbox.addEventListener('click', (event) => { if (event.target === lightbox) closeGallery(); }); 150 lightbox.addEventListener('keydown', (event) => { 151 if (event.key === 'ArrowLeft') stepGallery(-1); 152 if (event.key === 'ArrowRight') stepGallery(1); 153 }); 154})();
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.