/* Single external script (no inline JS anywhere → strict script-src 'self'). Loaded blocking in so the theme is set before first paint (no flash). */ (function () { // --- Pre-paint theme ----------------------------------------------------- try { var stored = localStorage.getItem("theme"); var theme = stored || (window.matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark"); document.documentElement.dataset.theme = theme; } catch (e) { document.documentElement.dataset.theme = "dark"; } function onReady(fn) { if (document.readyState !== "loading") fn(); else document.addEventListener("DOMContentLoaded", fn); } onReady(function () { // --- Theme toggle ------------------------------------------------------ var btn = document.getElementById("theme-toggle"); if (btn) { btn.addEventListener("click", function () { var root = document.documentElement; var next = root.dataset.theme === "light" ? "dark" : "light"; root.dataset.theme = next; try { localStorage.setItem("theme", next); } catch (e) { /* ignore */ } }); } // --- Reveal on scroll (progressive enhancement, motion-aware) ---------- var reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches; var targets = document.querySelectorAll("[data-reveal]"); if (!reduce && "IntersectionObserver" in window) { targets.forEach(function (el) { el.classList.add("reveal"); }); var io = new IntersectionObserver( function (entries) { entries.forEach(function (entry) { if (entry.isIntersecting) { entry.target.classList.add("is-visible"); io.unobserve(entry.target); } }); }, { rootMargin: "0px 0px -10% 0px", threshold: 0.1 }, ); targets.forEach(function (el) { io.observe(el); }); } // --- Scroll-coupled background pan ------------------------------------- // The Bat-Computer backdrop pans vertically in lock-step with scroll — // moving ONLY while you scroll, still at rest. Plain scroll listener so it // works in every browser (unlike CSS animation-timeline: scroll(), which is // Chromium-only). rAF-throttled; sets a CSS var the transform reads. var bgImgs = document.querySelectorAll(".site-bg__img"); if (bgImgs.length) { var ticking = false; function updateBg() { ticking = false; var doc = document.documentElement; var max = doc.scrollHeight - window.innerHeight; var p = max > 0 ? Math.min(1, Math.max(0, window.scrollY / max)) : 0; // +half a range at the top → -half at the bottom: scrolling down pans // the camera down the board. Range kept within the image's overhang so // an edge never shows (image is 215% tall → 57.5vh spare each side). var shift = (0.5 - p) * window.innerHeight * 0.7; for (var i = 0; i < bgImgs.length; i++) { bgImgs[i].style.setProperty("--bg-shift", shift.toFixed(1) + "px"); } } function onScroll() { if (!ticking) { ticking = true; window.requestAnimationFrame(updateBg); } } window.addEventListener("scroll", onScroll, { passive: true }); window.addEventListener("resize", onScroll, { passive: true }); updateBg(); } }); })();