// ───────────────────────────────────────────────────────────── // deck.js — Bifrost Advisory Board presentation runtime // // 1. Builds the bottom dot-nav from the slides in the DOM (one // dot per slide, Danish label from data-label). // 2. Scroll-snap is CSS-driven; here we keep the active dot + // slide counter in sync via an IntersectionObserver, and // smooth-scroll to a slide when its dot is clicked. // 3. Adds .in-view to the active slide so its .reveal children // animate in. // 4. Keyboard: ↑/↓, PageUp/PageDown, Home/End jump between slides. // // No external libraries; no inline scripts (CSP-friendly). // ───────────────────────────────────────────────────────────── (function () { 'use strict'; // Mark that JS is live — the .reveal hiding rule is gated on this, so a // failed/blocked script leaves the deck fully visible rather than blank. document.documentElement.classList.add('js'); const deck = document.getElementById('deck'); const nav = document.getElementById('dot-nav'); const counter = document.getElementById('counter-now'); const slides = Array.from(document.querySelectorAll('.slide')); if (!deck || !nav || !slides.length) return; const pad = (n) => String(n).padStart(2, '0'); // ─── Build the dot-nav ─────────────────────────────────────── const dots = slides.map((slide, i) => { const btn = document.createElement('button'); btn.className = 'dot-btn'; btn.type = 'button'; btn.dataset.index = String(i); btn.setAttribute('aria-label', `${i + 1}. ${slide.dataset.label || ''}`.trim()); const label = document.createElement('span'); label.className = 'label'; label.textContent = slide.dataset.label || `Slide ${i + 1}`; const dot = document.createElement('span'); dot.className = 'dot'; btn.appendChild(label); btn.appendChild(dot); btn.addEventListener('click', () => goTo(i)); nav.appendChild(btn); return btn; }); let activeIndex = 0; function setActive(i) { if (i === activeIndex) return; activeIndex = i; dots.forEach((d, j) => d.classList.toggle('is-active', j === i)); if (counter) counter.textContent = pad(i + 1); } function goTo(i) { const target = slides[i]; if (target) target.scrollIntoView({ behavior: 'smooth', block: 'start' }); } // ─── Active-slide tracking + reveal ────────────────────────── const io = new IntersectionObserver((entries) => { entries.forEach((entry) => { const i = slides.indexOf(entry.target); if (entry.isIntersecting) { entry.target.classList.add('in-view'); // A slide counts as "active" once it owns most of the viewport. if (entry.intersectionRatio >= 0.5) setActive(i); } }); }, { root: deck, threshold: [0, 0.5, 0.9] }); slides.forEach((s) => io.observe(s)); // First slide is visible immediately on load. slides[0].classList.add('in-view'); setActive(0); if (counter) counter.textContent = pad(1); // ─── Keyboard navigation ───────────────────────────────────── window.addEventListener('keydown', (e) => { // Ignore when a control/input has focus (none here, but future-proof). const tag = (e.target && e.target.tagName) || ''; if (tag === 'INPUT' || tag === 'TEXTAREA') return; switch (e.key) { case 'ArrowDown': case 'PageDown': case ' ': e.preventDefault(); goTo(Math.min(activeIndex + 1, slides.length - 1)); break; case 'ArrowUp': case 'PageUp': e.preventDefault(); goTo(Math.max(activeIndex - 1, 0)); break; case 'Home': e.preventDefault(); goTo(0); break; case 'End': e.preventDefault(); goTo(slides.length - 1); break; } }); })();