Three layers inside .sbm-scene (100% wide, max-width: 430px, 260px tall, overflow: hidden, --p: 0). On top, .sbm-scroll: a transparent layer in position: absolute; inset: 0, overflow-y: auto, z-index: 3, holding a .sbm-track of height: 340% — 884 px of content for a 260 px window, i.e. 624 px of travel as measured. Underneath, .sbm-stage (z-index: 1, pointer-events: none, aria-hidden) stacks .sbm-photo (blue → violet → pink gradient, centered caption) and .sbm-slats, a flex column of eight span.sbm-slat in flex: 1 1 0% — 32.5 px each — whose rgb(13, 13, 22) background is the scene's own: closed, the whole thing is a flat panel. The JavaScript (17 lines, ES5) listens to scroll passively on #sbmScroll, batches through requestAnimationFrame and writes --p = scrollTop / (scrollHeight − clientHeight), rounded to four decimals, on the scene. No SVG anywhere: the name comes from the catalog, the mask is made of stacked spans.
Each slat carries its rank inline (--k: 0 to 7) and a single rule does everything: transform: scaleY(clamp(0, 1 − (var(--p) × 9 − var(--k)), 1)), transform-origin: 50% 50%. Slat k stays at 1 while --p < k/9 and reaches 0 at (k + 1)/9: 69 px of scrolling per slat, strictly one after the other — measured at --p = 0.5, slats 0 to 3 are at 0, slat 4 at 0.500 (16.3 px tall out of 32.5), slats 5 to 7 at 1. The centered origin means the slat pinches toward its midline: the photo shows through both edges of the band, not from the bottom like a rising blind. The multiplier 9 for eight slats reserves the last ninth (--p from 0.889 to 1, 69 px) where nothing moves except the caption: opacity: clamp(0, (var(--p) − .75) × 4, 1), invisible until 468 px of scrolling, full at 624. The “Scrollez ici ↓” hint follows calc(1 − var(--p) × 3): gone at a third of the travel (208 px).
Measured in Chromium on a blank page: no console error, no seam between slats at --p = 0 (a uniform pixel column). The scroll layer covers everything (elementFromPoint returns .sbm-track): the wheel is captured over 624 px, then the page resumes — chaining measured, the page had not moved at mid-travel and advanced 200 px after the end stop. From the keyboard, the zone (tabindex="0", role="region") responds: arrow down = 40 px (--p + 0.064), space = 227 px. Two limits: --p is only recomputed on the next scroll — after going from 260 to 400 px tall with scrollTop = 312, it stays at 0.5000 while the true ratio is 0.325 (no resize listener); and scrollbar-width: thin leaves a thin bar on the right edge of the photo wherever scrollbars are classic (Windows, Linux — 0 px measured on macOS). The inline style="--p: 0.0000;" in the sold HTML is a trace of the extraction, identical to the initial state. 1.6 KB of CSS, 0.56 KB of JS.
Requires clamp() and CSS variables inside calc(), inset, transform, Element.closest() and requestAnimationFrame; ES5 JavaScript, zero dependencies. Everything is classic CSS driven by a scroll listener: unlike the category's 45 “native CSS” effects (Scroll-Driven Fade, View Timeline Scale, Clip Path Reveal…) built on animation-timeline, which no version of Firefox animates, this one works in Firefox.
Chrome 87+✓ Full
Firefox 75+✓ Full (focus outline from 85)
Safari 14.1+✓ Full (focus outline from 15.4)
Edge 87+✓ Full
Mobile iOS 14.5+✓ Full (finger scrolling)
Android Chrome✓ Full (finger scrolling)
Without JavaScript, --p stays at 0: closed panel, hint displayed, the zone scrolls for nothing and nothing is revealed — plan a class set by the script (or a noscript block) that opens the slats. Without clamp() (browsers older than 2020), the transform declaration is ignored: slats frozen, closed. Without :focus-visible, the rule is dropped and the browser draws its default outline.