Pure CSS — animation-timeline: scroll() for progress,
view() for reveal-on-enter. No scroll event listeners.
The panel below is its own scroll container (overflow: auto).
The lime bar tracks scroll progress via
animation-timeline: scroll(nearest).
The blue side meter uses a named
scroll-timeline-name: --article-scroll on the same scroller.
Cards animate with view() timelines as they enter, cover, or exit the scrollport.
animation-range: entry 0% entry 100% — runs while the card
crosses the scrollport edge. Classic reveal pattern, zero JS.
animation-range: cover 0% cover 50% — animates while the
element occupies the scrollport, not just at the boundary.
Each card owns its own anonymous view() timeline.
No IntersectionObserver, no requestAnimationFrame loop.
Only transform and opacity here — properties
the browser can promote and animate off the main thread.
animation-range: exit 0% exit 100% — runs as the card
leaves the scrollport. Scroll back up to see it restore.
Progress bar should read 100%. Side meter fills completely. All driven by CSS scroll timelines — not JavaScript.
Scroll inside the panel ↑↓ · Named timeline powers the blue meter
/* Named scroll timeline on the scroller */ .scroll-panel { scroll-timeline-name: --article-scroll; } /* Progress bar — anonymous scroll() on nearest ancestor */ .progress-bar { animation: grow linear; animation-timeline: scroll(nearest block); } /* Side meter — named timeline reference */ .side-meter-fill { animation: grow-v linear; animation-timeline: --article-scroll; } /* Card reveal — view() + animation-range */ .card { animation: reveal linear both; animation-timeline: view(block); animation-range: entry 0% entry 100%; }