/* ============================================================ * banner-loader.js — Sovereign Plugin / AlbertLane.net * Wires up top banner, watermarks, side panel, and modals. * * CSS vars written to :root: * --banner-h Exact height of #pubBanner * --header-clearance Banner + sub-watermark strip + breathing room * * Page customisation (optional, add to
): * * → Inserts into sub-banner text. Defaults to "Digital Fraud Audit Series". * * Current-page detection: * Compares window.location.pathname against every .series-link[href]. * Adds .current to matching link and auto-opens its accordion. * Removes any hard-coded .current classes first. * * Albert Lane · AlbertLane.net · SEC No. 17684-273-411-436 * ============================================================ */ (function () { 'use strict'; /* ── Watermark text ───────────────────────────────────── */ var seriesMeta = document.querySelector('meta[name="series-watermark"]'); var series = seriesMeta ? seriesMeta.getAttribute('content') : 'Digital Fraud Audit Series'; var wmText = 'Albert Lane · Gemini · Anthropic’s Claude · ' + series + ' · SEC No. 17684-273-411-436 · AlbertLane.net'; var subWm = document.getElementById('subWatermark'); if (subWm) subWm.textContent = wmText; /* ── Banner height → CSS vars ─────────────────────────── */ var banner = document.getElementById('pubBanner'); var root = document.documentElement; function updateBannerMetrics() { if (!banner) return; var h = banner.offsetHeight; var subH = subWm ? (subWm.offsetHeight || 22) : 22; root.style.setProperty('--banner-h', h + 'px'); root.style.setProperty('--header-clearance', (h + subH + 10) + 'px'); if (subWm) subWm.style.top = h + 'px'; } updateBannerMetrics(); if (window.ResizeObserver && banner) { new ResizeObserver(updateBannerMetrics).observe(banner); } window.addEventListener('resize', updateBannerMetrics); /* ── Touch-safe tap handler (prevents ghost-click double-fire) */ function onTap(el, fn) { if (!el) return; var pending = false; el.addEventListener('touchend', function (e) { e.preventDefault(); pending = true; fn(); setTimeout(function () { pending = false; }, 600); }, { passive: false }); el.addEventListener('click', function () { if (!pending) fn(); }); } /* ── Sidebar open / close ─────────────────────────────── */ var sidebar = document.getElementById('sidebar'); var overlay = document.getElementById('navOverlay'); var navTab = document.getElementById('navTab'); var closeBtn = document.getElementById('sidebarClose'); function openSidebar() { if (!sidebar) return; sidebar.classList.add('open'); if (overlay) overlay.classList.add('open'); sidebar.setAttribute('aria-hidden', 'false'); if (navTab) navTab.setAttribute('aria-expanded', 'true'); document.body.style.overflow = 'hidden'; if (closeBtn) closeBtn.focus(); } function closeSidebar() { if (!sidebar) return; sidebar.classList.remove('open'); if (overlay) overlay.classList.remove('open'); sidebar.setAttribute('aria-hidden', 'true'); if (navTab) navTab.setAttribute('aria-expanded', 'false'); document.body.style.overflow = ''; if (navTab) navTab.focus(); } onTap(navTab, openSidebar); onTap(closeBtn, closeSidebar); if (overlay) overlay.addEventListener('click', closeSidebar); /* ── Smooth accordion helpers (scrollHeight-driven) ──── */ function openAccordion(links) { if (!links) return; links.style.maxHeight = links.scrollHeight + 'px'; /* After transition completes: set to 'none' so dynamic content resizes freely */ links.addEventListener('transitionend', function h() { if (links.style.maxHeight !== '0px') links.style.maxHeight = 'none'; links.removeEventListener('transitionend', h); }); } function closeAccordion(links) { if (!links) return; /* Snap from 'none' to explicit px so transition has a start point */ links.style.maxHeight = links.scrollHeight + 'px'; /* Double rAF ensures the browser commits the px value before animating */ requestAnimationFrame(function () { requestAnimationFrame(function () { links.style.maxHeight = '0'; }); }); } /* ── Series accordion toggles ─────────────────────────── */ document.querySelectorAll('.series-toggle').forEach(function (btn) { onTap(btn, function () { var acc = btn.closest('.series-accordion'); var links = acc ? acc.querySelector('.series-links') : null; var isOpen = acc ? acc.classList.contains('open') : false; if (isOpen) { if (acc) acc.classList.remove('open'); btn.setAttribute('aria-expanded', 'false'); closeAccordion(links); } else { if (acc) acc.classList.add('open'); btn.setAttribute('aria-expanded', 'true'); openAccordion(links); } }); }); /* ── Auto-mark current page + open its accordion ──────── */ var path = window.location.pathname; var normPath = path.endsWith('/') ? path + 'index.html' : path; /* Clear any hard-coded .current before we set our own */ document.querySelectorAll('.series-link.current').forEach(function (l) { l.classList.remove('current'); }); document.querySelectorAll('.series-link').forEach(function (link) { var href = link.getAttribute('href'); if (!href) return; try { var abs = new URL(href, window.location.href).pathname; var normHref = abs.endsWith('/') ? abs + 'index.html' : abs; if (normHref === normPath || abs === path) { link.classList.add('current'); var acc = link.closest('.series-accordion'); if (acc && !acc.classList.contains('open')) { acc.classList.add('open'); var toggle = acc.querySelector('.series-toggle'); if (toggle) toggle.setAttribute('aria-expanded', 'true'); var links = acc.querySelector('.series-links'); requestAnimationFrame(function () { openAccordion(links); }); } } } catch (e) { /* ignore malformed hrefs */ } }); /* ── Sidebar tools panel ──────────────────────────────── */ var toolsToggle = document.getElementById('toolsToggle'); var sidebarTools = document.getElementById('sidebarTools'); var toolsLinks = document.getElementById('toolsLinks'); if (toolsToggle && sidebarTools) { onTap(toolsToggle, function () { var isOpen = sidebarTools.classList.contains('open'); if (isOpen) { sidebarTools.classList.remove('open'); toolsToggle.setAttribute('aria-expanded', 'false'); closeAccordion(toolsLinks); } else { sidebarTools.classList.add('open'); toolsToggle.setAttribute('aria-expanded', 'true'); openAccordion(toolsLinks); } }); } /* ── NKA popup ────────────────────────────────────────── */ var nkaModal = document.getElementById('nkaModal'); var nkaContent = document.getElementById('nkaContent'); var nkaClose = document.getElementById('nkaClose'); var nkaBtn = document.getElementById('nkaBtn'); var nkaLoaded = false; function openNka() { if (!nkaModal) return; nkaModal.classList.add('open'); nkaModal.setAttribute('aria-hidden', 'false'); document.body.style.overflow = 'hidden'; if (!nkaLoaded && nkaContent) { fetch('/assets/private/nka-popup.html') .then(function (r) { return r.ok ? r.text() : Promise.reject(r.status); }) .then(function (html) { var doc = new DOMParser().parseFromString(html, 'text/html'); doc.querySelectorAll('script, iframe, object, embed, style, link, meta').forEach(function (s) { s.remove(); }); var URI_ATTRS = /^(?:href|src|action|formaction|xlink:href)$/i; var UNSAFE_SCHEME = /^\s*(?:javascript|data|vbscript):/i; doc.querySelectorAll('*').forEach(function (el) { Array.from(el.attributes).forEach(function (a) { if (/^on/i.test(a.name) || a.name.toLowerCase() === 'srcdoc' || a.name.toLowerCase() === 'style') { el.removeAttribute(a.name); } else if (URI_ATTRS.test(a.name) && UNSAFE_SCHEME.test(a.value)) { el.removeAttribute(a.name); } }); }); nkaContent.innerHTML = ''; Array.from(doc.body.childNodes).forEach(function (n) { nkaContent.appendChild(document.adoptNode(n)); }); nkaLoaded = true; }) .catch(function () { if (nkaContent) nkaContent.textContent = 'Content temporarily unavailable.'; }); } if (nkaClose) nkaClose.focus(); } function closeNka() { if (!nkaModal) return; nkaModal.classList.remove('open'); nkaModal.setAttribute('aria-hidden', 'true'); document.body.style.overflow = ''; if (nkaBtn) nkaBtn.focus(); } onTap(nkaBtn, openNka); onTap(nkaClose, closeNka); if (nkaModal) { nkaModal.addEventListener('click', function (e) { if (e.target === nkaModal) closeNka(); }); } /* ── Escape key ───────────────────────────────────────── */ document.addEventListener('keydown', function (e) { if (e.key !== 'Escape') return; if (nkaModal && nkaModal.classList.contains('open')) { closeNka(); return; } if (sidebar && sidebar.classList.contains('open')) closeSidebar(); }); })();