Le HTML vendu tient en un bloc. Un cadre .st-progress-scroll enveloppe tout : c'est lui qui porte le fond #0a0a0f, la couleur du texte et la police (Inter, puis la police système), qui est position: relative pour servir de repère au libellé placé en absolu, et qui est une colonne flex à max-height: 100% — vos couleurs et votre police se posent sur cette seule règle. Dedans, .scroll-container#progress-scroll (width: 100%, height: 280px, overflow: hidden auto — pas de défilement horizontal, vertical automatique —, position: relative, scroll-behavior: smooth) contient d'abord la piste .st-progress-bar-track (4 px de haut, blanc à 8 %, position: sticky; top: 0, z-index: 20) et son remplissage .st-progress-bar-fill#progress-fill (width: 0%, dégradé #6366f1 → #a855f7 → #ec4899, coins droits arrondis de 2 px, transition: width 0.05s linear), puis le corps .progress-inner (padding: 24px 20px) : cinq h3 de 1 rem en blanc, cinq p de 0,8 rem en blanc à 50 % avec un interligne de 1,7, quatre .section-divider de 60 × 2 px. Leurs marges sont écrites en entier (margin: 0 0 12px pour les titres, 0 0 16px pour les paragraphes) : le rendu ne dépend pas d'un reset de la feuille de styles qui accueille le bloc. Comme la piste est dans l'élément qui défile, sticky la garde collée au bord haut pendant que le texte passe dessous — mesuré : la piste reste à y = 0 du conteneur en fin de course. Après le conteneur, toujours dans le cadre, un span.scroll-hint-label « Scrollez pour explorer » en 0,65 rem capitales, blanc à 30 %, calé à 8 px du bas du cadre, pulse de 0,3 à 0,7 d'opacité toutes les 2 s (@keyframes pulseHint). Les barres de défilement WebKit sont réduites à 4 px (::-webkit-scrollbar).
Le script fait une chose. Enfermé dans une fonction immédiatement invoquée — progressScroll et progressFill n'existent pas dans window et ne peuvent pas entrer en collision avec vos propres variables —, il récupère les deux éléments par leur id et, s'ils existent, pose un seul écouteur scroll sur le conteneur — pas de requestAnimationFrame, pas de boucle : rien ne tourne au repos. À chaque événement, scrollHeight − clientHeight donne la course totale en pixels (215 px pour le contenu vendu à 1 264 px de large, 454 px à 374 px, les paragraphes se repliant), scrollTop / course × 100 donne le pourcentage, écrit tel quel dans style.width. La transition de 50 ms lisse le pas entre deux événements. Mesuré dans Chromium sur une page vierge, un cran de molette de 60 px : scrollTop passe à 60, la largeur inline à 27.907%, la largeur rendue à 352,7 px sur une piste de 1 264 px — la proportion exacte, stabilisée sous 150 ms. Fin de course : 215 / 215, soit 100% ; retour en haut : 0%. Rien n'est calculé au chargement : tant qu'aucun événement scroll n'est survenu, style.width reste vide et la barre à 0 %.
Le pourcentage n'existe que comme largeur. La variable progress vaut bien 0 à 100, mais le code ne l'écrit nulle part en texte : le lecteur voit une barre, pas « 47 % ». L'afficher tient en une ligne à la suite de l'existante — label.textContent = Math.round(progress) + ' %' — et c'est ce que font les trois scènes ci-dessous. Trois choix d'écriture, vérifiés sur une page vierge. Le cadre .st-progress-scroll livre son propre fond : collé tel quel sur une page blanche, le bloc reste un rectangle sombre où titres, texte et piste se lisent (mesuré : aucun bloc de texte de la couleur du fond, sur page blanche comme sur page sombre) — sans ce fond, un texte blanc n'existerait que sur un site déjà sombre. Le même cadre est le repère du libellé : un élément en position: absolute se cale sur son ancêtre positionné le plus proche, et le cadre l'est ; le libellé tombe à 8 px du bas du bloc, sur la dernière ligne visible (mesuré : y = 268 dans un cadre de 280). Enfin le cadre est une colonne flex à max-height: 100% : posé dans une boîte de hauteur fixe plus basse que 280 px, le conteneur se réduit à la boîte au lieu de déborder (mesuré : dans une boîte de 250 px, conteneur à 250 px, piste et libellé visibles) ; sur une page sans contrainte de hauteur, ce max-height est sans effet. Le code vise en revanche deux id fixes : une seconde copie du bloc sur la même page a une barre qui ne bouge jamais (mesuré : style.width vide sur la seconde après 100 px de défilement).
Trois propriétés fixent le plancher : la syntaxe à deux valeurs overflow: hidden auto (Chrome 68, Firefox 61, Safari 13.1 — avant, la déclaration est ignorée et le conteneur ne défile pas), position: sticky (Chrome 56, Firefox 32, Safari 13) et, pour le seul défilement programmé, scroll-behavior (Safari 15.4). Le JavaScript est en ES2015 (const, fonction fléchée) : Chrome 49, Firefox 45, Safari 10. Zéro dépendance. La progression est calculée en JavaScript sur l'événement scroll, pas par animation-timeline : elle fonctionne sur Firefox, qui ne rend les animations pilotées au défilement en CSS natif dans aucune version.
Sans JavaScript, le conteneur défile normalement et la piste reste vide : une ligne grise de 4 px, aucune erreur. Sans la syntaxe overflow à deux valeurs (Safari avant 13.1), le conteneur ne défile pas : le texte déborde des 280 px et la barre ne bouge jamais — écrivez overflow-x: hidden; overflow-y: auto en deux déclarations pour couvrir ces versions.