style(about): redesign /about as numbered "Liner Notes" editorial spine

Replace Home-cloned section grammar with a numbered left rail (Bodoni
numerals, vertical spine, mono marginalia), an asymmetric content column,
and SVG waveform dividers. Adds a degrade-safe IntersectionObserver interop.
Copy verbatim.
This commit is contained in:
daniel-c-harvey
2026-06-17 20:04:00 -04:00
parent a210b2ded7
commit c8168564bb
3 changed files with 701 additions and 529 deletions
@@ -0,0 +1,65 @@
/**
* 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 = [];
}