AdvisoryBoardMeeting_june2026/assets/js/deck.js
Jonathan Hvid eca79752f4 deck: board-meeting revisions across the slide set
- Slide 1 + 19: restore the old Project Bifrost intro — a spanning
  aurora arc that draws in and breathes; welcome adds the meeting line,
  closing reuses the same treatment.
- Questions: strip to the quoted question only (no eyebrow/rule/rainbow),
  ~30% larger.
- Board: names ~2x, portraits ~half size.
- Mission: big centred Fenja, ink values beneath, Project Bifrost lockup;
  supporter caption enlarged and the equal-height logos pinned above the
  menu with more air.
- Pause: drop the colour wash.
- Architecture: remove accent highlight, label to top-right, +30% text,
  more side air, taller cards.
- New Demo and Roadmap solo-word slides (now 19 total; counter dynamic).
- Section dividers: Fenja logo instead of Bifrost; sov title de-emphasised.
- Meme: larger, captionless, with a quoted "Sovereign Cloud" title.
- Sandbox: big title, one cohesive paragraph, no rule, larger logos.
- Tighten dot-nav so all 19 dots stay on one row.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 13:48:48 +02:00

117 lines
4.4 KiB
JavaScript

// ─────────────────────────────────────────────────────────────
// 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 counterTotal = document.getElementById('counter-total');
const slides = Array.from(document.querySelectorAll('.slide'));
if (!deck || !nav || !slides.length) return;
const pad = (n) => String(n).padStart(2, '0');
// Keep the "/ NN" total in sync with however many slides exist.
if (counterTotal) counterTotal.textContent = pad(slides.length);
// ─── 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;
}
});
})();