assets/site.js
/* Progressive enhancement: the full archive remains readable without JavaScript. */
(() => {
const root = document.documentElement;
const toggle = document.querySelector('.mode-toggle');
function updateTheme() {
const light = root.dataset.mode === 'light';
toggle.textContent = light ? 'Dark mode' : 'Light mode';
toggle.setAttribute('aria-label', light ? 'Switch to dark mode' : 'Switch to light mode');
}
if (toggle) {
toggle.hidden = false;
updateTheme();
toggle.addEventListener('click', () => {
root.dataset.mode = root.dataset.mode === 'light' ? 'dark' : 'light';
try { localStorage.setItem('fact-mode', root.dataset.mode); } catch (_) { /* Storage is optional. */ }
updateTheme();
});
}
const spotlight = document.querySelector('.hero-spotlight');
if (spotlight) {
const slides = [...spotlight.querySelectorAll('.spotlight-slide')];
const choices = [...spotlight.querySelectorAll('[data-slide]')];
const pause = spotlight.querySelector('.spotlight-pause');
const motion = matchMedia('(prefers-reduced-motion: reduce)');
let active = 0, paused = motion.matches, hovering = false, focused = false, timer;
spotlight.querySelector('.spotlight-controls').hidden = false;
function show(index) {
active = index;
slides.forEach((slide,i)=>{slide.hidden=i!==active;});
choices.forEach((button,i)=>button.setAttribute('aria-pressed',String(i===active)));
}
function schedule() {
clearInterval(timer);
pause.textContent = paused ? 'Play' : 'Pause';
pause.setAttribute('aria-label', paused ? 'Start model rotation' : 'Pause model rotation');
if (!paused && !hovering && !focused && !document.hidden) {
timer=setInterval(()=>show((active+1)%slides.length),6500);
}
}
choices.forEach((button,i)=>button.addEventListener('click',()=>{paused=true;show(i);schedule();}));
pause.addEventListener('click',()=>{paused=!paused;schedule();});
spotlight.addEventListener('mouseenter',()=>{hovering=true;schedule();});
spotlight.addEventListener('mouseleave',()=>{hovering=false;schedule();});
spotlight.addEventListener('focusin',()=>{focused=true;schedule();});
spotlight.addEventListener('focusout',event=>{focused=spotlight.contains(event.relatedTarget);schedule();});
document.addEventListener('visibilitychange',schedule);
motion.addEventListener('change',()=>{if(motion.matches)paused=true;schedule();});
schedule();
}
// Local launcher; Ko-fi is requested only after an intentional open.
const support = document.querySelector('[data-support-open]');
const supportDialog = document.getElementById('support-dialog');
if (support && supportDialog && typeof supportDialog.showModal === 'function') {
const frame = supportDialog.querySelector('iframe');
const loading = supportDialog.querySelector('.support-loading');
support.setAttribute('role', 'button');
support.setAttribute('aria-haspopup', 'dialog');
support.setAttribute('aria-controls', supportDialog.id);
support.setAttribute('aria-expanded', 'false');
support.addEventListener('click', event => {
if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
event.preventDefault();
supportDialog.showModal();
support.setAttribute('aria-expanded', 'true');
if (!frame.hasAttribute('src')) {
loading.hidden = false;
frame.src = frame.dataset.src;
}
});
support.addEventListener('keydown', event => {
if (event.key === ' ') { event.preventDefault(); support.click(); }
});
frame.addEventListener('load', () => {
if (!frame.hasAttribute('src')) return;
loading.hidden = true;
frame.parentElement.setAttribute('data-loaded', '');
});
frame.addEventListener('error', () => { loading.textContent = 'Open Ko-fi using the link below.'; });
supportDialog.querySelector('.support-close').addEventListener('click', () => supportDialog.close());
supportDialog.addEventListener('close', () => support.setAttribute('aria-expanded', 'false'));
supportDialog.addEventListener('click', event => {
if (event.target !== supportDialog) return;
const r = supportDialog.getBoundingClientRect();
if (event.clientX < r.left || event.clientX > r.right || event.clientY < r.top || event.clientY > r.bottom) supportDialog.close();
});
}
const list = document.getElementById('rec-list');
if (!list) return;
const records = [...list.querySelectorAll('.rec')].map(el => ({el, text:el.textContent.toLowerCase()}));
const keys = ['domain', 'lens', 'stance', 'controversy'];
const filters = keys.map(key => ({key, el:document.getElementById('f-' + key)}));
const search = document.getElementById('f-search');
const more = document.getElementById('f-more');
let limit = 12;
function apply() {
const query = search.value.toLowerCase().trim();
const matches = records.filter(({el, text}) => (!query || text.includes(query)) && filters.every(({key,el:input}) => !input.value || el.dataset[key] === input.value));
const visible = new Set(matches.slice(0, limit));
records.forEach(record => { record.el.hidden = !visible.has(record); });
document.getElementById('f-count').textContent = Math.min(limit,matches.length) + ' of ' + matches.length + ' positions';
document.getElementById('f-empty').hidden = matches.length !== 0;
more.hidden = matches.length <= limit;
more.textContent = 'Show ' + Math.min(12, Math.max(0, matches.length-limit)) + ' more positions';
}
function reset() { filters.forEach(({el})=>{el.value='';}); search.value=''; limit=12; }
filters.forEach(({el})=>el.addEventListener('change',()=>{limit=12;apply();}));
search.addEventListener('input',()=>{limit=12;apply();});
document.getElementById('f-reset').addEventListener('click',()=>{reset();apply();search.focus();});
more.addEventListener('click',()=>{limit+=12;apply();});
// Lens segmented tabs drive the (then hidden) lens select; same filter state, app-like browsing.
const seg = document.getElementById('lens-seg');
const lensSelect = document.getElementById('f-lens');
if (seg && lensSelect) {
seg.hidden = false;
lensSelect.classList.add('seg-hidden');
seg.addEventListener('click', event => {
const button = event.target.closest('[data-lens-tab]');
if (!button) return;
seg.querySelectorAll('[data-lens-tab]').forEach(b => b.setAttribute('aria-pressed', b === button ? 'true' : 'false'));
lensSelect.value = button.dataset.lensTab;
lensSelect.dispatchEvent(new Event('change'));
});
}
function revealPosition() {
const index = records.findIndex(({el})=>'#'+el.id===location.hash);
if(index<0) return;
reset(); limit = Math.max(12, index+1); apply();
records[index].el.scrollIntoView({block:'start'});
}
apply(); revealPosition();
window.addEventListener('hashchange',revealPosition);
})();