/** * About-page rail active-numeral highlight. * * The Liner Notes layout carries one oversized Bodoni numeral per movement in a * persistent left rail. This module lights the numeral of whichever movement is * currently in view by toggling `.is-active` on the movement element; the CSS does * the colour transition. Pure progressive enhancement — the numerals render * statically in low-opacity navy without this, so a load failure or a no-JS client * degrades to a still-legible page. * * One observer at a time, re-pointed on each `observe` call. The active movement is * the one nearest the top of the viewport among those currently intersecting, which * keeps a single numeral lit during the scroll rather than flickering between * adjacent movements at the boundary. */ let observer: IntersectionObserver | null = null; let observed: Element[] = []; function refreshActive(): void { let best: Element | null = null; let bestTop = Number.POSITIVE_INFINITY; for (const el of observed) { const rect = el.getBoundingClientRect(); const viewportH = window.innerHeight || document.documentElement.clientHeight; // In view at all (any overlap with the viewport). const inView = rect.bottom > 0 && rect.top < viewportH; if (!inView) continue; // Prefer the movement whose top is closest to (but not far below) the fold. const distance = Math.abs(rect.top); if (distance < bestTop) { bestTop = distance; best = el; } } for (const el of observed) { el.classList.toggle('is-active', el === best); } } export function observe(...elements: Element[]): void { unobserve(); observed = elements.filter(Boolean); if (observed.length === 0) return; // IntersectionObserver only tells us *that* visibility changed; the actual // "which is nearest the fold" decision is recomputed from live rects so the // choice stays correct mid-scroll. observer = new IntersectionObserver(() => refreshActive(), { threshold: [0, 0.25, 0.5, 0.75, 1], }); for (const el of observed) observer.observe(el); // Seed once so the first movement lights before any scroll. refreshActive(); } export function unobserve(): void { observer?.disconnect(); observer = null; for (const el of observed) el.classList.remove('is-active'); observed = []; }