/* ============================================================================= deck.js — the reusable engine for the Africa Multiple / Bayreuth deck. This file is GENERIC. You should not need to edit it per talk. Each deck only edits index.html: its slides, and the `window.DECK_CONFIG` block near the top of that file (presenter, talk metadata, contact links). What it does, in order: • fills every [data-contact] slot with icon links from DECK_CONFIG • initialises reveal.js (custom nav, no stock chrome) with plugins • builds the running head (current section) and persistent footer • builds the auto table-of-contents overlay from [data-toc] sections • keeps everything in sync on each slide change Keyboard: ←/→ navigate · G jump to slide (type a number, Enter) · T table of contents · O overview · F fullscreen · S speaker notes Ctrl/Cmd+Shift+F search across slides · ? help · Esc closes overlays ============================================================================= */ (function () { "use strict"; var CFG = window.DECK_CONFIG || {}; CFG.links = CFG.links || {}; /* ---- engine UI strings, language-aware (from ; default English). Add a language by extending I18N; decks opt in via . Every string the engine shows or announces lives here — a string spelled out at its call site is one a new language silently misses. -------------- */ var LANG = (CFG.lang || document.documentElement.lang || "en").slice(0, 2).toLowerCase(); var I18N = { en: { contents: "Contents", overview: "overview", close: "close", prev: "Previous slide", next: "Next slide", tocOpen: "Open table of contents", tocAria: "Table of contents", closeAria: "Close", deckNav: "Slide navigation", copyLink: "Copy link to this slide", linkCopied: "Link copied", imageViewer: "Image viewer", imageClose: "Close image", imageView: "View image full screen", imagePrev: "Previous image", imageNext: "Next image", imageLoading: "Loading image…", imageUnavailable: "Image unavailable. Try another image.", embedLoading: "Loading file…", embedError: "Could not load the file.", embedSource: "View the source", scrollRegion: "Scrollable excerpt", frameNetwork: "This website uses a network connection.", frameShow: "Show the site here", frameUnavailable: "Live view unavailable — it needs a network connection.", frameOpen: "Open the site" }, fr: { contents: "Sommaire", overview: "aperçu", close: "fermer", prev: "Diapo précédente", next: "Diapo suivante", tocOpen: "Ouvrir le sommaire", tocAria: "Sommaire", closeAria: "Fermer", deckNav: "Navigation des diapositives", copyLink: "Copier le lien de cette diapositive", linkCopied: "Lien copié", imageViewer: "Visionneuse d’images", imageClose: "Fermer l’image", imageView: "Afficher l’image en plein écran", imagePrev: "Image précédente", imageNext: "Image suivante", imageLoading: "Chargement de l’image…", imageUnavailable: "Image indisponible. Essayez une autre image.", embedLoading: "Chargement du fichier…", embedError: "Impossible de charger le fichier.", embedSource: "Voir la source", scrollRegion: "Extrait défilant", frameNetwork: "Ce site utilise une connexion réseau.", frameShow: "Afficher le site ici", frameUnavailable: "Aperçu en direct indisponible — une connexion réseau est requise.", frameOpen: "Ouvrir le site" } }; var STR = I18N[LANG] || I18N.en; /* Dev-mode flags: ?check outlines overflow + shows fit scales; ?no-fit (or ?audit) disables auto-fitting so authored overflow is visible raw. */ var CHECK_MODE = /[?&](check|audit)\b/.test(location.search); var NO_FIT = /[?&](no-fit|audit)\b/.test(location.search); var PARAMS = new URLSearchParams(location.search); var PRINT = PARAMS.has("print-pdf") || PARAMS.get("view") === "print"; var motionPreference = window.matchMedia("(prefers-reduced-motion: reduce)"); var fileEmbedsReady = Promise.resolve(); function leafSlides() { return Array.from(document.querySelectorAll(".reveal .slides section")) .filter(function (s) { return !s.querySelector(":scope > section") && !s.classList.contains("stack"); }); } function leafSlideFor(el) { return el && el.closest ? el.closest("section:not(.stack)") : null; } function imageSource(img) { return img.currentSrc || img.getAttribute("src") || img.getAttribute("data-src") || ""; } function loadImage(img) { if (img.dataset.srcset) img.srcset = img.dataset.srcset; if (!img.getAttribute("src") && img.dataset.src) img.src = img.dataset.src; if (!img.getAttribute("src") && !img.getAttribute("srcset")) return Promise.resolve(); return new Promise(function (resolve) { var timer = setTimeout(done, 10000); function done() { clearTimeout(timer); img.removeEventListener("load", done); img.removeEventListener("error", done); resolve(); } img.addEventListener("load", done); img.addEventListener("error", done); if (img.complete) done(); }).then(function () { return img.complete && img.naturalWidth && img.decode ? img.decode().catch(function () {}) : null; }); } async function settleSlides(slides) { await fileEmbedsReady; if (document.fonts) await document.fonts.ready; await Promise.all(slides.flatMap(function (s) { return Array.from(s.querySelectorAll("img")).map(loadImage); })); Reveal.layout(); fitReady = true; slides.forEach(function (s) { fitSlide(s, true); }); if (Reveal.isPrintView()) buildPrintImprints(); await new Promise(function (resolve) { requestAnimationFrame(function () { requestAnimationFrame(resolve); }); }); } window.DeckRuntime = { leafSlides: leafSlides, settle: function () { return settleSlides(Reveal.isPrintView() ? leafSlides() : [Reveal.getCurrentSlide()].filter(Boolean)); } }; // Folder this script lives in (e.g. .../shared/) so engine assets resolve no // matter how deep the talk page sits. Captured while currentScript is valid. var SCRIPT_BASE = (function () { var s = document.currentScript; if (!s) { var all = document.getElementsByTagName("script"); for (var i = 0; i < all.length; i++) if (/deck\.js(\?|$)/.test(all[i].src)) { s = all[i]; break; } } return s && s.src ? s.src.replace(/[^\/]*$/, "") : ""; })(); /* ---- inline icons (stroke icons inherit currentColor; ORCID keeps its mark) */ var ICON = { prev: '', next: '', toc: '', close:'', link: '', check:'', github:'', globe:'', mail: '', orcid:'', linkedin:'' }; function elem(html) { var t = document.createElement("template"); t.innerHTML = html.trim(); return t.content.firstElementChild; } /* Deck titles are author copy, and this engine builds its chrome by string concatenation. A title carrying &, <, or a quote would otherwise land in the markup as markup. */ function escapeHTML(s) { return String(s == null ? "" : s).replace(/[&<>"']/g, function (c) { return { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]; }); } function tidyUrl(u) { return String(u || "").replace(/^https?:\/\//, "").replace(/^www\./, "").replace(/\/$/, ""); } /* ---- contact links from DECK_CONFIG.links, into every [data-contact] slot */ function contactHTML() { var L = CFG.links, out = []; function row(href, icon, label) { if (!/^(https?:|mailto:)/i.test(href)) return ""; return '' + icon + "" + escapeHTML(label) + ""; } if (L.github) out.push(row(L.github, ICON.github, tidyUrl(L.github).replace(/^github\.com\//, ""))); if (L.website) out.push(row(L.website, ICON.globe, tidyUrl(L.website))); if (L.orcid) out.push(row(L.orcid, ICON.orcid, tidyUrl(L.orcid).replace(/^orcid\.org\//, ""))); if (L.linkedin)out.push(row(L.linkedin, ICON.linkedin, tidyUrl(L.linkedin).replace(/^linkedin\.com\/in\//, ""))); if (L.email) out.push(row("mailto:" + L.email, ICON.mail, L.email)); return out.join(""); } /* ---- footer (single persistent element, updated per slide) -------------- */ var footer, counterCur, counterTot, btnPrev, btnNext; function buildFooter(reveal) { var amLogo = CFG.logoMark || (SCRIPT_BASE + "logo-africamultiple.png"); footer = elem( '
" ); reveal.appendChild(footer); counterCur = footer.querySelector(".counter .cur"); counterTot = footer.querySelector(".counter .tot"); btnPrev = footer.querySelector(".prev"); btnNext = footer.querySelector(".next"); btnPrev.addEventListener("click", function () { Reveal.prev(); }); btnNext.addEventListener("click", function () { Reveal.next(); }); footer.querySelector(".toc-btn").addEventListener("click", toggleTOC); var copy = document.createElement("button"); copy.className = "deck-btn copy-link"; copy.type = "button"; copy.title = STR.copyLink; copy.setAttribute("aria-label", STR.copyLink); copy.innerHTML = ICON.link; var status = document.createElement("span"); status.className = "deck-status"; status.setAttribute("role", "status"); footer.querySelector(".deck-nav").append(copy, status); /* The status line is for screen readers only, so on its own a sighted presenter clicked and saw nothing happen. The button answers for itself: its icon turns to a tick for a moment. Both then reset, so a second copy is announced and shown again rather than landing on text already there. */ var copiedTimer = null; function resetCopied() { clearTimeout(copiedTimer); copiedTimer = null; copy.classList.remove("is-copied"); copy.innerHTML = ICON.link; status.textContent = ""; } copy.addEventListener("click", async function () { resetCopied(); var canonical = document.querySelector('link[rel="canonical"]'); var url = new URL(canonical ? canonical.href : location.href); url.search = ""; url.hash = "/" + encodeURIComponent(Reveal.getCurrentSlide().id); try { await navigator.clipboard.writeText(url.href); copy.classList.add("is-copied"); copy.innerHTML = ICON.check; status.textContent = STR.linkCopied; copiedTimer = setTimeout(resetCopied, 2400); } catch (error) { window.prompt(STR.copyLink, url.href); } }); } /* ---- counted vs annexe slides ------------------------------------------ A deck may keep backup material after its closing slide for questions, marked data-visibility="uncounted" (reveal's own attribute). Reveal still navigates to it and it still prints, but it must never inflate the folio the room reads. One folio vocabulary answers that everywhere the deck numbers itself — footer counter, contents list, printed imprint: counted slides run 1…N, annexe slides take an A-series of their own, and while an annexe slide is on screen the live counter holds at N / N rather than counting past its own total. The stronger data-visibility="hidden" needs nothing here — reveal removes those slides at init. */ function isUncounted(sec) { return !!sec && sec.getAttribute("data-visibility") === "uncounted"; } function pad2(n) { return String(n).padStart(2, "0"); } /* The counted folio at a horizontal index: {cur, total} over the counted slides alone. On an annexe slide cur is the last counted slide before it. */ function countedFolio(h) { var cur = 0, total = 0; Reveal.getHorizontalSlides().forEach(function (sec, i) { if (isUncounted(sec)) return; total++; if (i <= h) cur = total; }); return { cur: cur || 1, total: total || 1 }; } /* ---- per-page imprint (PDF export only) --------------------------------- */ /* On screen one persistent footer is right: it updates as you move. On paper there is nothing to update — every page is final and needs its own folio, so each printed page gets an imprint of its own instead. The PDF is the shareable record, so annexe pages print with the rest — but they are uncounted, so they cannot take a number out of the deck's own sequence. Counted pages read NN / TT against the counted total; annexe pages read A01, A02 … The alternative — repeating the closing page's "25 / 25" across every annexe page — would read as a printing fault where a plainly different mark reads as a decision, and this way the reader can see at a glance where the talk ended. */ function buildPrintImprints() { var pages = document.querySelectorAll(".reveal .slides .pdf-page"); if (!pages.length) return; var counted = 0; Array.prototype.forEach.call(pages, function (page) { if (!isUncounted(page.querySelector("section"))) counted++; }); var total = pad2(counted); var title = escapeHTML(CFG.talkShort || CFG.talkTitle || ""); if (CFG.venue) title += " · " + escapeHTML(CFG.venue); var n = 0, annexe = 0; Array.prototype.forEach.call(pages, function (page) { var sec = page.querySelector("section"); // The folios advance for every page, so a page that already carries an // imprint (a re-run of the pass) cannot shift the numbering behind it. var folio = isUncounted(sec) ? "A" + pad2(++annexe) : pad2(++n) + " / " + total; if (page.querySelector(".pdf-imprint")) return; var field = fieldOf(sec); var mark = elem( '" ); page.appendChild(mark); }); } /* ---- running head (current section, like a book) ------------------------ */ var runhead, runSec; var HERO = ["cover", "section", "closing", "statement", "media"]; function buildRunhead(reveal) { runhead = elem(''); reveal.appendChild(runhead); runSec = runhead.querySelector(".rh-sec"); } /* The running section is the latest section title / [data-toc] at or before the current slide — resolved by scanning forward so TOC jumps stay correct. */ function sectionLabelFor(h) { var hSlides = Reveal.getHorizontalSlides(), label = ""; for (var i = 0; i <= h && i < hSlides.length; i++) { var sec = hSlides[i]; if (sec.classList.contains("section")) { var t = sec.querySelector("h2"); label = (t ? t.textContent : sec.getAttribute("data-toc") || label).trim(); } else if (sec.getAttribute("data-toc")) { label = sec.getAttribute("data-toc"); } } return label; } /* ---- table-of-contents overlay ------------------------------------------ */ var overlay, tocItems = []; function buildTOC(reveal) { /* Reveal's scroll view (?view=scroll) re-parents every slide out of `.slides > section` — the selector getHorizontalSlides() reads — so the list below has nothing to enumerate: it built an empty overlay on one deck and a one-entry one on another, and the folio it printed could not agree with the (equally unreachable) footer counter. The chrome for that view hides the Contents button; leaving the overlay unbuilt is what stops the T key from opening a dialog listing a deck that isn't there. */ if (Reveal.isScrollView && Reveal.isScrollView()) return; var hSlides = Reveal.getHorizontalSlides(); var entries = []; // The right-hand folio is the same one the footer and the printed imprint // spend, so an annexe entry reads A01 rather than a number the counter on // that very slide would contradict. var counted = 0, annexe = 0; hSlides.forEach(function (sec, h) { var folio = isUncounted(sec) ? "A" + pad2(++annexe) : pad2(++counted); var label = sec.getAttribute("data-toc"); if (label) entries.push({ h: h, label: label, folio: folio }); }); if (!entries.length) return; // no TOC requested var rows = entries.map(function (e, i) { return '"; }).join(""); overlay = elem( '" ); reveal.appendChild(overlay); tocItems = Array.prototype.slice.call(overlay.querySelectorAll(".toc-item")); tocItems.forEach(function (btn) { btn.addEventListener("click", function () { Reveal.slide(parseInt(btn.getAttribute("data-h"), 10), 0); closeTOC(); }); }); overlay.querySelector(".toc-close").addEventListener("click", closeTOC); overlay.addEventListener("click", function (e) { if (e.target === overlay) closeTOC(); }); setDialogHidden(overlay, true); } /* Keep an overlay's accessibility state in sync with its visual state: `inert` (with aria-hidden fallback) while closed, interactive while open. */ function setDialogHidden(dialog, hiddenState) { if (!dialog) return; if ("inert" in dialog) dialog.inert = hiddenState; if (hiddenState) dialog.setAttribute("aria-hidden", "true"); else dialog.removeAttribute("aria-hidden"); } /* Focus an element once its dialog is actually visible: the overlays fade in via a visibility transition, and focus() is a no-op while the computed visibility is still hidden (the first frame after the class flips). */ function focusWhenVisible(el) { if (!el) return; requestAnimationFrame(function () { requestAnimationFrame(function () { el.focus(); }); }); } var tocLastFocus = null; function openTOC() { if (!overlay) return; tocLastFocus = document.activeElement; overlay.classList.add("open"); setDialogHidden(overlay, false); markCurrentTOC(); // Move focus into the dialog (current entry if any, else the first). focusWhenVisible(overlay.querySelector(".toc-item.current") || overlay.querySelector(".toc-item")); } function closeTOC() { if (!overlay) return; overlay.classList.remove("open"); setDialogHidden(overlay, true); if (tocLastFocus && tocLastFocus.focus) tocLastFocus.focus(); // restore focus to the trigger tocLastFocus = null; } function toggleTOC(){ if (overlay) (overlay.classList.contains("open") ? closeTOC() : openTOC()); } /* While the TOC is open, keys must act on the DIALOG, never on the deck behind it: Tab is trapped inside, arrows move between entries, Escape closes, and everything else is stopped before reveal's own key handler. Runs in the capture phase on document so it wins over Reveal. */ function tocKeydown(e) { if (!overlay || !overlay.classList.contains("open")) return; if (e.key === "Tab") { trapTOCFocus(e); e.stopPropagation(); return; } if (e.key === "Escape") { e.preventDefault(); e.stopPropagation(); closeTOC(); return; } // The dialog's own footer names T and O, so both have to work from inside // it rather than be swallowed with every other key: T is the same toggle // that opened the contents, and O hands over to reveal's overview. if (!e.ctrlKey && !e.metaKey && !e.altKey) { var letter = String(e.key || "").toLowerCase(); if (letter === "t" || letter === "o") { e.preventDefault(); e.stopPropagation(); closeTOC(); if (letter === "o") Reveal.toggleOverview(true); return; } } var f = Array.prototype.slice.call( overlay.querySelectorAll(".toc-item") ).filter(function (el) { return !el.disabled && el.offsetParent !== null; }); var idx = f.indexOf(document.activeElement); if (e.key === "ArrowDown" || e.key === "ArrowRight") { e.preventDefault(); if (f.length) f[(idx + 1 + f.length) % f.length].focus(); } else if (e.key === "ArrowUp" || e.key === "ArrowLeft") { e.preventDefault(); if (f.length) f[(idx - 1 + f.length) % f.length].focus(); } else if (e.key === "Home") { e.preventDefault(); if (f.length) f[0].focus(); } else if (e.key === "End") { e.preventDefault(); if (f.length) f[f.length - 1].focus(); } // Whatever the key, never let it drive the presentation behind the dialog. e.stopPropagation(); } /* Keep Tab inside the open dialog (simple focus trap). */ function trapTOCFocus(e) { if (e.key !== "Tab" || !overlay || !overlay.classList.contains("open")) return; var f = Array.prototype.slice.call( overlay.querySelectorAll(".toc-close, .toc-item") ).filter(function (el) { return !el.disabled && el.offsetParent !== null; }); if (!f.length) return; var first = f[0], last = f[f.length - 1], a = document.activeElement; if (e.shiftKey && (a === first || !overlay.contains(a))) { e.preventDefault(); last.focus(); } else if (!e.shiftKey && (a === last || !overlay.contains(a))) { e.preventDefault(); first.focus(); } } function markCurrentTOC() { if (!overlay) return; var h = Reveal.getIndices().h; var active = null; tocItems.forEach(function (btn) { var bh = parseInt(btn.getAttribute("data-h"), 10); if (bh <= h) active = btn; btn.classList.remove("current"); btn.removeAttribute("aria-current"); }); if (active) { active.classList.add("current"); active.setAttribute("aria-current", "page"); } } /* ---- per-slide sync ----------------------------------------------------- */ var DARK = ["section", "closing", "media"]; /* The dark fields split into two registers by luminance (see the on-dark token block in the theme). The footer and the viewport live outside the slide, so they cannot inherit the slide's register — deck.js mirrors it onto them as data-field="deep". Everything else dark stays on the bright defaults, which are safe on any field. */ function isDeepField(el) { if (!el) return false; if (el.classList.contains("section")) return el.classList.contains("navy"); return el.classList.contains("closing") || el.classList.contains("media"); } /* null on a paper slide, else the register the chrome over it must use. The live footer and the printed imprint both read it, so a slide can never be dark for one and light for the other. */ function fieldOf(el) { if (!el) return null; var attr = el.getAttribute("data-footer"); var dark = attr ? attr === "dark" : DARK.some(function (c) { return el.classList.contains(c); }); if (!dark) return null; return isDeepField(el) ? "deep" : "bright"; } function update() { var cur = Reveal.getCurrentSlide(); var h = Reveal.getIndices().h; var folio = countedFolio(h); if (counterCur) counterCur.textContent = pad2(folio.cur); if (counterTot) counterTot.textContent = pad2(folio.total); if (btnPrev) btnPrev.disabled = Reveal.isFirstSlide(); if (btnNext) btnNext.disabled = Reveal.isLastSlide(); var field = fieldOf(cur); var isDark = !!field; var isDeep = field === "deep"; var vp = document.querySelector(".reveal-viewport"); [footer, vp].forEach(function (el) { if (!el) return; el.classList.toggle(el === footer ? "on-dark" : "deck-dark", isDark); if (isDeep) el.setAttribute("data-field", "deep"); else el.removeAttribute("data-field"); }); // Running head: the current section on content slides; hidden on title pages. if (runhead) { var isHero = !!(cur && HERO.some(function (c) { return cur.classList.contains(c); })); runhead.hidden = isHero; if (!isHero && runSec) runSec.textContent = sectionLabelFor(h); } markCurrentTOC(); fitSlide(cur); animateCounts(cur); } /* ---- auto-fit: scale a slide's content down ONLY if it overflows the safe area (gold-rule clearance on hero slides; footer reserve below). Slides that already fit are never touched; an overflowing one is wrapped in an absolutely placed .fit box and scaled to fit. Runs once per slide, after webfonts settle so the measurement is real — and again (forced) when delayed content such as an image or a file embed resolves and changes the slide's height. Fitting is VISIBLE, not silent: every fitted slide carries data-fit with the applied scale; a scale below FIT_WARN logs a warning; below FIT_FAIL the slide is stamped data-fit-fail (a validation failure) unless the author explicitly allows it with a data-fit-allow attribute on the with the slim global from
shared/highlight.min.js (~40 KB), which is what every deck loads instead of
reveal's 921 KB bundled highlight plugin — that plugin is not vendored and
the engine registers no highlighter. A deck with no code slides simply
drops the script tag and this becomes a no-op. The data-highlighted guard
is hljs's own mark, so a second pass never re-highlights a block. */
function highlightAll() {
var hl = window.hljs;
if (!hl) return;
document.querySelectorAll(".reveal .slides pre code").forEach(function (code) {
if (code.dataset.highlighted) return;
try { hl.highlightElement(code); } catch (e) {}
});
}
/* ---- dev-only slide check (?check): outline any slide whose content spills
past the fixed 1280×720 canvas, and make auto-fitting VISIBLE — the banner
shows the applied data-fit scale for the current slide, amber below the
warning threshold, red below the failure threshold. Combine with ?no-fit
(or use ?audit, which implies both) to see authored overflow before any
scaling is applied. ------------------------------------------------------- */
var checkModeUpdate = null;
function enableCheckMode() {
var banner = elem('');
banner.hidden = true;
document.body.appendChild(banner);
function check() {
var s = Reveal.getCurrentSlide();
if (!s) return;
var fitted = s.hasAttribute("data-fit");
var k = fitted ? parseFloat(s.getAttribute("data-fit")) : 1;
// A fitted slide's layout boxes are unscaled (transforms don't change
// scrollHeight), so the raw overflow test only applies to unfitted slides.
var over = !fitted && (s.scrollHeight > s.clientHeight + 1 || s.scrollWidth > s.clientWidth + 1);
var fail = s.hasAttribute("data-fit-fail") || (fitted && k < FIT_FAIL && !s.hasAttribute("data-fit-allow"));
var warn = fitted && k < FIT_WARN;
var msg = "", color = "#c0392b";
if (over) msg = NO_FIT ? "Authored overflow (fitting disabled)" : "Slide overflows 1280×720";
else if (fitted) {
msg = "Auto-fitted ×" + k.toFixed(3) + (fail ? " — below " + FIT_FAIL : warn ? " — below " + FIT_WARN : "");
color = fail ? "#c0392b" : warn ? "#b9770e" : "#1e8449";
}
var outline = over || fail ? "#c0392b" : warn ? "#b9770e" : "";
s.style.outline = outline ? "3px solid " + outline : "";
s.style.outlineOffset = outline ? "-3px" : "";
banner.textContent = msg;
banner.style.background = color;
banner.hidden = !msg;
}
checkModeUpdate = check;
Reveal.on("slidechanged", check);
check();
}
function init() {
var reveal = document.querySelector(".reveal");
decorateChrome();
lazifyFrames(); // live iframes load only when their slide becomes visible
leafSlides().forEach(function (slide, i) {
if (slide.id) return;
var label = slide.dataset.toc || (slide.querySelector("h1,h2") || {}).textContent || "slide";
var id = label.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "slide";
var baseId = id, suffix = 2;
while (document.getElementById(id)) id = baseId + "-" + suffix++;
slide.id = id;
});
window.DeckRuntime.ready = Reveal.initialize({
width: 1280, height: 720, margin: 0,
minScale: 0.2, maxScale: 2.0,
// Never switch to reveal 6's scroll view on narrow screens: the theme is
// designed around a scaled 1280×720 canvas, and the automatic scroll mode
// (≤435 px wide) clips two-column slides on portrait phones.
scrollActivationWidth: null,
center: false, hash: true,
controls: false, progress: true, slideNumber: false,
transition: CFG.transition || "fade",
transitionSpeed: "default",
autoAnimate: !motionPreference.matches,
defaultTiming: CFG.defaultTiming || null,
totalTime: CFG.totalTime || null,
backgroundTransition: "fade",
overview: true, touch: true, keyboard: true,
// PDF export (?print-pdf): one printed page per slide. Reveal's default
// is Infinity, so any slide even 1px over the page height spilled onto
// extra pages — the "one slide, several pages" bug. The house style is a
// fixed 1280×720 screen per slide, so a hard cap of 1 is always right.
pdfSeparateFragments: false, pdfMaxPagesPerSlide: 1,
plugins: revealPlugins()
}).then(function () {
decorateSlides(); // fill [data-contact] slots
buildFooter(reveal);
buildRunhead(reveal);
buildTOC(reveal);
fileEmbedsReady = loadFileEmbeds();
initScrollRegions();
buildLightbox();
initFrameFallbacks();
highlightAll(); // highlight code via global hljs (works without the bundled plugin)
update();
// Late-loading media (lazy data-src images, embeds) can change a slide's
// height after the first fit pass — re-fit the slide when they arrive.
// Images only: iframes are reloaded by the browser whenever the fit
// wrapper reparents them, so refitting on iframe load would loop.
document.querySelector(".slides").addEventListener("load", function (e) {
if (e.target && e.target.nodeName === "IMG") refitAfterLoad(e.target);
}, true);
// If the tab hides mid-roll, rAF freezes; snap any counting numeral to its
// final value so none is ever left reading a partial count (or 0).
document.addEventListener("visibilitychange", function () {
if (!document.hidden) return;
document.querySelectorAll("[data-count]").forEach(function (el) {
if (el._countRAF) { cancelAnimationFrame(el._countRAF); el._countRAF = null; }
if (el._countTO) { clearTimeout(el._countTO); el._countTO = null; }
var done = el.getAttribute("data-count-text");
if (done !== null) el.textContent = done;
});
});
if (CHECK_MODE) enableCheckMode();
motionPreference.addEventListener("change", function () { Reveal.configure({ autoAnimate: !motionPreference.matches }); });
// Defensive relayout: recompute the scale once the window and webfonts
// have settled, in case the deck initialised before it had real size.
window.addEventListener("load", function () { Reveal.layout(); fitReady = true; fitSlide(Reveal.getCurrentSlide()); });
if (document.fonts && document.fonts.ready) document.fonts.ready.then(function () { Reveal.layout(); fitReady = true; fitSlide(Reveal.getCurrentSlide()); });
// Reveal's resize event only fires when its scale changes. A CSS
// breakpoint can change our safe area even at the same scale, so listen
// to the viewport itself. Other slides re-check their geometry on entry.
var resizeFitFrame = null;
window.addEventListener("resize", function () {
if (PRINT) return;
if (resizeFitFrame !== null) cancelAnimationFrame(resizeFitFrame);
resizeFitFrame = requestAnimationFrame(function () {
resizeFitFrame = null;
Reveal.layout();
fitSlide(Reveal.getCurrentSlide());
});
});
// PDF export (?print-pdf): every slide prints, so every slide needs the
// overflow fit — not just the current one. Re-fit them all (force: the
// pass may re-run once fonts settle or after reveal builds its
// .pdf-page layout, whichever lands last).
if (PRINT) {
var fitAllPages = function () {
if (!fitReady || !document.querySelector(".reveal .pdf-page")) return;
settleSlides(leafSlides());
};
Reveal.on("pdf-ready", fitAllPages);
if (document.fonts && document.fonts.ready) document.fonts.ready.then(fitAllPages);
}
Reveal.addKeyBinding({ keyCode: 84, key: "T", description: "Table of contents" }, toggleTOC);
// While the TOC dialog is open, all keys act on the dialog (capture phase,
// before reveal's own handler): Esc closes, arrows move, Tab is trapped.
document.addEventListener("keydown", tocKeydown, true);
});
Reveal.on("slidechanged", update);
Reveal.on("overviewshown", function () { if (footer) footer.style.opacity = "0"; });
Reveal.on("overviewhidden", function () { if (footer) footer.style.opacity = ""; });
}
function revealPlugins() {
var p = [];
if (window.RevealNotes) p.push(RevealNotes);
if (window.RevealZoom) p.push(RevealZoom);
if (window.RevealSearch) p.push(RevealSearch);
return p;
}
if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", init);
else init();
})();