Trois couches dans .sbm-scene (100 % de large, max-width: 430px, 260px de haut, overflow: hidden, --p: 0). Au-dessus, .sbm-scroll : un calque transparent en position: absolute; inset: 0, overflow-y: auto, z-index: 3, qui contient une piste .sbm-track de height: 340% — 884 px de contenu pour 260 px de fenêtre, soit 624 px de course mesurés. Dessous, .sbm-stage (z-index: 1, pointer-events: none, aria-hidden) empile .sbm-photo (dégradé bleu → violet → rose, légende centrée) et .sbm-slats, une colonne flex de huit span.sbm-slat en flex: 1 1 0% — 32,5 px chacun — dont le fond rgb(13, 13, 22) est celui de la scène : fermé, l'ensemble est un panneau uni. Le JavaScript (17 lignes, ES5) écoute scroll en passif sur #sbmScroll, regroupe par requestAnimationFrame et écrit --p = scrollTop / (scrollHeight − clientHeight), arrondi à quatre décimales, sur la scène. Aucun SVG nulle part : le nom vient du catalogue, le masque est fait de span empilés.
Chaque lamelle porte son rang en ligne (--k: 0 à 7) et une seule règle fait tout : transform: scaleY(clamp(0, 1 − (var(--p) × 9 − var(--k)), 1)), transform-origin: 50% 50%. La lamelle k reste à 1 tant que --p < k/9 et atteint 0 à (k + 1)/9 : 69 px de défilement par lamelle, strictement l'une après l'autre — mesuré à --p = 0,5, les lamelles 0 à 3 sont à 0, la lamelle 4 à 0,500 (16,3 px de haut sur 32,5), les lamelles 5 à 7 à 1. L'origine au centre fait que la lamelle se pince vers sa ligne médiane : la photo apparaît par les deux bords de la bande, pas par le bas comme un store qui remonte. Le multiplicateur 9 pour huit lamelles réserve le dernier neuvième (--p de 0,889 à 1, 69 px) où plus rien ne bouge sauf la légende : opacity: clamp(0, (var(--p) − .75) × 4, 1), invisible jusqu'à 468 px de défilement, pleine à 624. L'indice « Scrollez ici ↓ » suit calc(1 − var(--p) × 3) : éteint au tiers de la course (208 px).
Mesuré dans Chromium sur une page vierge : aucune erreur console, aucune couture entre lamelles à --p = 0 (colonne de pixels uniforme). Le calque de défilement couvre tout (elementFromPoint renvoie .sbm-track) : la molette est capturée sur 624 px puis la page reprend — enchaînement mesuré, la page n'a pas bougé à mi-course et a avancé de 200 px après la butée. Au clavier, la zone (tabindex="0", role="region") répond : flèche bas = 40 px (--p + 0,064), espace = 227 px. Deux limites : --p n'est recalculé qu'au prochain scroll — après un passage de 260 à 400 px de haut avec scrollTop = 312, il reste à 0,5000 alors que le vrai rapport est 0,325 (pas d'écouteur resize) ; et scrollbar-width: thin laisse une barre fine sur le bord droit de la photo là où les barres sont classiques (Windows, Linux — 0 px mesuré sur macOS). Le style="--p: 0.0000;" en ligne du HTML vendu est une trace de l'extraction, identique à l'état initial. 1,6 Ko de CSS, 0,56 Ko de JS.
Requiert clamp() et les variables CSS dans calc(), inset, transform, Element.closest() et requestAnimationFrame ; JavaScript ES5, zéro dépendance. Tout est du CSS classique piloté par un écouteur scroll : contrairement aux 45 effets « CSS natif » de la catégorie (Scroll-Driven Fade, View Timeline Scale, Clip Path Reveal…) bâtis sur animation-timeline, qu'aucune version de Firefox n'anime, celui-ci fonctionne sur Firefox.
Sans JavaScript, --p reste à 0 : panneau fermé, indice affiché, la zone défile dans le vide et rien n'est révélé — prévoyez une classe posée par le script (ou un bloc noscript) qui ouvre les lamelles. Sans clamp() (navigateurs antérieurs à 2020), la déclaration transform est ignorée : lamelles figées, fermées. Sans :focus-visible, la règle est écartée et le navigateur dessine son contour par défaut.