1/* Spotlight for the homepage outcome ledger (partials/home/panel-journey.html). 2 * 3 * Keeps the row nearest the viewport's reading line at full contrast and dims 4 * the rest, so a reader meets one outcome per beat instead of six bodies at 5 * once. Rows also rise in the first time they enter the viewport. 6 * 7 * Progressive: this script only ever ADDS state. Without it -- no JavaScript, 8 * or a visitor who prefers reduced motion -- the stylesheet leaves every row at 9 * full contrast and nothing moves. The dimming is gated on the `is-spotlit` 10 * class this script sets, never on the markup. 11 * 12 * Deliberately not GSAP: this is an IntersectionObserver for the reveal and one 13 * rAF-throttled scroll handler for the spotlight, which is inside the motion 14 * stack design.md sanctions. ScrollTrigger buys nothing here that a pinned 15 * header and a nearest-row test do not already give. 16 */ 17(function () { 18 'use strict'; 19 20 var root = document.querySelector('[data-ledger]'); 21 if (!root) return; 22 23 if (window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches) return; 24 25 var rows = Array.prototype.slice.call(root.querySelectorAll('[data-ledger-row]')); 26 if (rows.length < 2) return; 27 28 root.classList.add('is-spotlit'); 29 30 /* 1. Reveal: each row rises in once, the first time it enters the viewport. */ 31 if ('IntersectionObserver' in window) { 32 var io = new IntersectionObserver(function (entries) { 33 entries.forEach(function (entry) { 34 if (!entry.isIntersecting) return; 35 entry.target.classList.add('is-seen'); 36 io.unobserve(entry.target); 37 }); 38 }, { rootMargin: '0px 0px -12% 0px' }); 39 rows.forEach(function (row) { io.observe(row); }); 40 } else { 41 rows.forEach(function (row) { row.classList.add('is-seen'); }); 42 } 43 44 /* 2. Spotlight. A row is lit until its TOP edge passes the anchor, then the 45 * next one lights. The anchor is the scroll position at which the header 46 * STICKS: the header sits `padding-top` below the row's top inside its 47 * column, and pins when that natural position reaches its sticky `top`, 48 * so the row's top is at (sticky top - padding) at that instant. Row 1 49 * therefore lets go exactly as the header locks, and every later row lets 50 * go at the same line. A fixed pixel line, never a fraction of the 51 * viewport: a fraction sits lower on a taller window and made the first 52 * row let go while still mid-screen. 53 * While the list is still arriving, the first row is lit; once every row 54 * has passed the anchor, the last one stays lit. */ 55 var pin = root.querySelector('.ledger__pin'); 56 var headCol = pin ? pin.parentElement : null; 57 var offset = parseFloat(root.getAttribute('data-reading-offset')); 58 if (isNaN(offset)) offset = 0; 59
60 function anchor() { 61 var top = pin ? parseFloat(window.getComputedStyle(pin).top) : NaN; 62 if (isNaN(top)) return 96 + offset; // header not pinned (narrow viewports) 63 var pad = headCol ? parseFloat(window.getComputedStyle(headCol).paddingTop) : 0; 64 return top - (isNaN(pad) ? 0 : pad) + offset; 65 } 66 67 var frame = 0; 68 69 function pick() { 70 frame = 0; 71 var line = anchor(); 72 var best = rows[rows.length - 1]; 73 74 for (var i = 0; i < rows.length; i++) { 75 if (rows[i].getBoundingClientRect().top >= line) { best = rows[i]; break; } 76 } 77 78 rows.forEach(function (row) { 79 row.classList.toggle('is-active', row === best); 80 }); 81 } 82 83 function schedule() { 84 if (!frame) frame = window.requestAnimationFrame(pick); 85 } 86 87 window.addEventListener('scroll', schedule, { passive: true }); 88 window.addEventListener('resize', schedule); 89 pick(); 90})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.