PageSourceSearch

https://thefiveclub.co/assets/render.576ba4eb.js

js thefiveclub.co collected 2026-09-24 13:55:48 UTC 25,497 bytes, 609 lines download raw bytes

1/* Runtime de rendu de la landing page.
2 *
3 * Le balisage de index.html est le gabarit du design, conservé tel quel. Il
4 * utilise quatre constructions absentes du HTML brut, que ce fichier résout :
5 *
6 *   {{ chemin.vers.valeur }}   interpolation, en texte comme en attribut
7 *   <sc-for list="{{ items }}" as="x"> … </sc-for>   répétition
8 *   onClick / onSubmit="{{ handler }}"               liaison d'événement
9 *   style-hover="css"                                style au survol
10 *
11 * Deux points importants pour les performances et le SEO :
12 *
13 * 1. Les gestionnaires ne sont pas attachés élément par élément. Le rendu émet
14 *    un chemin résolu (`data-on-click="faq.2.toggle"`) et un seul écouteur
15 *    délégué sur le conteneur le résout dans le scope courant. Résultat : le
16 *    HTML pré-rendu par tools/prerender.mjs est interactif immédiatement, sans
17 *    reconstruction du DOM au chargement — l'élément LCP n'est jamais remplacé.
18 *
19 * 2. Les révélations au défilement s'appuient sur IntersectionObserver et ne
20 *    s'activent que si JavaScript tourne : sans JS, ou pour un robot, tout le
21 *    contenu reste visible.
22 */
23(function (global) {
24  'use strict';
25
26  var EXPR = /\{\{\s*([\w.$]+)\s*\}\}/g;
27  var SINGLE = /^\s*\{\{\s*([\w.$]+)\s*\}\}\s*$/;
28  var SVG_NS = 'http://www.w3.org/2000/svg';
29  var EVENT_ATTR = {
30    onclick: 'data-on-click',
31    onsubmit: 'data-on-submit',
32    onchange: 'data-on-change',
33  };
34
35  function lookup(scope, path) {
36    var parts = path.split('.');
37    var value = scope;
38    for (var i = 0; i < parts.length; i++) {
39      if (value == null) return undefined;
40      value = value[parts[i]];
41    }
42    return value;
43  }
44
45  function interpolate(text, scope, aliases) {
46    if (text.indexOf('{{') === -1) return text;
47    return text.replace(EXPR, function (_, path) {
48      var value = lookup(scope, resolvePath(path, aliases));
49      return value == null ? '' : String(value);
50    });
51  }
52
53  /* `aliases` traduit une variable de boucle en chemin concret dans le scope
54     racine : { q: 'faq.2' } transforme `q.toggle` en `faq.2.toggle`. */
55  function resolvePath(path, aliases) {
56    var dot = path.indexOf('.');
57    var head = dot === -1 ? path : path.slice(0, dot);
58    if (!Object.prototype.hasOwnProperty.call(aliases, head)) return path;
59    return aliases[head] + (dot === -1 ? '' : path.slice(dot));
60  }
61
62  function build(templateNode, scope, aliases, out, ns, doc) {
63    var nodes = templateNode.childNodes;
64    for (var i = 0; i < nodes.length; i++) {
65      var node = nodes[i];
66
67      if (node.nodeType === 3) {
68        out.appendChild(doc.createTextNode(interpolate(node.nodeValue, scope, aliases)));
69        continue;
70      }
71      if (node.nodeType !== 1) continue;
72
73      var tag = node.tagName.toLowerCase();
74
75      if (tag === 'sc-for') {
76        var m = SINGLE.exec(node.getAttribute('list') || '');
77        if (!m) continue;
78        var listPath = resolvePath(m[1], aliases);
79        var list = lookup(scope, listPath);
80        var as = node.getAttribute('as') || 'item';
81        if (Array.isArray(list)) {
82          for (var j = 0; j < list.length; j++) {
83            var inner = Object.create(aliases);
84            inner[as] = listPath + '.' + j;
85            build(node, scope, inner, out, ns, doc);
86          }
87        }
88        continue;
89      }
90
91      // Gabarit de vignette du bundler : hors page.
92      if (tag === 'template') continue;
93
94      var childNs = ns || (tag === 'svg' ? SVG_NS : null);
95      var el = childNs ? doc.createElementNS(childNs, tag) : doc.createElement(tag);
96
97      var attrs = node.attributes;
98      for (var k = 0; k < attrs.length; k++) {
99        var name = attrs[k].name;
100        var raw = attrs[k].value;
101
102        if (name === 'onclick' || name === 'onsubmit' || name === 'onchange') {
103          var h = SINGLE.exec(raw);
104          if (h) {
105            el.setAttribute(EVENT_ATTR[name], resolvePath(h[1], aliases));
106          }
107          continue;
108        }
109
110        /* `<textarea value="…">` n'existe pas en HTML : la valeur d'un textarea
111           est son contenu. Le canvas l'écrit comme un input, on le traduit. */
112        if (name === 'value' && tag === 'textarea') {
113          el.appendChild(doc.createTextNode(interpolate(raw, scope, aliases)));
114          continue;
115        }
116        if (name === 'style-hover') {
117          el.setAttribute('data-hover', interpolate(raw, scope, aliases));
118          continue;
119        }
120        el.setAttribute(name, interpolate(raw, scope, aliases));
121      }
122
123      build(node, scope, aliases, el, childNs, doc);
124      out.appendChild(el);
125    }
126  }
127
128  /* ── Ci-dessous : navigateur uniquement ─────────────────────────────────*/
129
130  function bindHover(root) {
131    var targets = root.querySelectorAll('[data-hover]');
132    for (var i = 0; i < targets.length; i++) {
133      (function (el) {
134        var base = el.getAttribute('style') || '';
135        var hover = el.getAttribute('data-hover');
136        el.removeAttribute('data-hover');
137        el.addEventListener('mouseenter', function () { el.setAttribute('style', base + ';' + hover); });
138        el.addEventListener('mouseleave', function () { el.setAttribute('style', base); });
139        el.addEventListener('focus', function () { el.setAttribute('style', base + ';' + hover); });
140        el.addEventListener('blur', function () { el.setAttribute('style', base); });
141      })(targets[i]);
142    }
143  }
144
145  function captureInputs(root) {
146    var inputs = root.querySelectorAll('input, textarea');
147    var state = { values: [], focused: -1, start: 0, end: 0 };
148    for (var i = 0; i < inputs.length; i++) {
149      state.values.push(inputs[i].value);
150      if (inputs[i] === document.activeElement) {
151        state.focused = i;
152        try { state.start = inputs[i].selectionStart;
152 state.end = inputs[i].selectionEnd; } catch (e) {}
153      }
154    }
155    return state;
156  }
157
158  function restoreInputs(root, state) {
159    var inputs = root.querySelectorAll('input, textarea');
160    for (var i = 0; i < inputs.length && i < state.values.length; i++) {
161      inputs[i].value = state.values[i];
162    }
163    if (state.focused > -1 && inputs[state.focused]) {
164      var el = inputs[state.focused];
165      el.focus();
166      try { el.setSelectionRange(state.start, state.end); } catch (e) {}
167    }
168  }
169
170  function resolveLinks(root, pages) {
171    var links = root.querySelectorAll('a[href$=".dc.html"]');
172    for (var i = 0; i < links.length; i++) {
173      var href = links[i].getAttribute('href');
174      links[i].setAttribute('href', pages[href] || '#');
175      if (!pages[href]) links[i].setAttribute('data-page-pending', href);
176    }
177  }
178
179  /* Apparitions au défilement. La classe n'est posée que si JS tourne, donc le
180     contenu reste visible pour un robot ou sans JavaScript. Le décalage entre
181     éléments frères se fait par transition-delay, jamais par un changement de
182     mise en page : rien ne bouge, seuls opacity et transform varient. */
183  /* Photos de fond : posées au moment utile.
184
185     Les photos de la page sont des `background-image`. Un fond écrit en style
186     inline part au téléchargement dès que l'élément entre dans le document,
187     sans considération de position : les dix-neuf photos du bas de page
188     partaient avec le hero. L'importateur les a déplacées dans `data-bg` ;
189     cette fonction les repose en style quand l'élément approche de la fenêtre.
190
191     La marge de 600 px fait démarrer le téléchargement bien avant que la
192     photo ne soit à l'écran : au rythme d'un défilement normal, elle est
193     arrivée quand le lecteur y parvient.
194
195     Deux garde-fous, parce qu'une photo manquante est pire qu'une photo
196     tardive : sans IntersectionObserver, tout est posé d'emblée ; et si
197     l'observateur reste muet alors que des éléments sont dans la fenêtre —
198     onglet d'arrière-plan, webview inhabituelle — un filet pose tout deux
199     secondes après le chargement. */
200  function bindFonds(root) {
201    /* Signale à la page que le chargement différé est pris en charge. Sans ce
202       drapeau, le filet inline de la coquille pose toutes les photos d'un
203       coup — c'est ce qui doit arriver si ce fichier est périmé ou absent. */
204    document.documentElement.setAttribute('data-fonds', 'auto');
205    var restants = root.querySelectorAll('[data-bg]');
206    if (!restants.length) return;
207
208    function poser(el) {
209      var v = el.getAttribute('data-bg');
210      if (!v) return;
211      el.style.backgroundImage = v;
212      el.removeAttribute('data-bg');
213    }
214
215    if (typeof IntersectionObserver === 'undefined') {
216      for (var i = 0; i < restants.length; i++) poser(restants[i]);
217      return;
218    }
219
220    var io = new IntersectionObserver(function (entries) {
221      for (var k = 0; k < entries.length; k++) {
222        if (!entries[k].isIntersecting) continue;
223        poser(entries[k].target);
224        io.unobserve(entries[k].target);
225      }
226    }, { rootMargin: '600px 0px' });
227
228    /* Ce qui est déjà à l'écran est posé tout de suite, sans passer par
229       l'observateur. Sans cela, un changement de langue — qui reconstruit le
230       document — retirerait les fonds visibles le temps d'un rendu avant que
231       l'observateur ne les rende.
232
233       Les positions sont toutes relevées d'abord, les styles écrits ensuite.
234       Alterner lecture et écriture force le navigateur à recalculer la mise en
235       page à chaque tour de boucle ; en deux temps, il ne la calcule qu'une
236       fois. */
237    var visibles = [];
238    var horsEcran = [];
239    var limite = window.innerHeight + 600;
240    for (var j = 0; j < restants.length; j++) {
241      var r = restants[j].getBoundingClientRect();
242      (r.top < limite && r.bottom > -600 ? visibles : horsEcran).push(restants[j]);
243    }
244    for (var v = 0; v < visibles.length; v++) poser(visibles[v]);
245    for (var o = 0; o < horsEcran.length; o++) io.observe(horsEcran[o]);
246
247    function poserTout() {
248      var reste = root.querySelectorAll('[data-bg]');
249      for (var n = 0; n < reste.length; n++) poser(reste[n]);
250      io.disconnect();
251    }
252
253    document.addEventListener('visibilitychange', function () {
254      if (document.visibilityState !== 'visible') return;
255      var reste = root.querySelectorAll('[data-bg]');
256      var aPoser = [];
257      var seuil = window.innerHeight + 600;
258      for (var n = 0; n < reste.length; n++) {
259        var r = reste[n].getBoundingClientRect();
260        if (r.top < seuil && r.bottom > -600) aPoser.push(reste[n]);
261      }
262      for (var k = 0; k < aPoser.length; k++) poser(aPoser[k]);
263    });
264
265    setTimeout(function () {
266      var reste = root.querySelectorAll('[data-bg]');
267      if (!reste.length) return;
268      // l'observateur a-t-il livré quoi que ce soit ?
269      for (var n = 0; n < reste.length; n++) {
270        var r = reste[n].getBoundingClientRect();
271        if (r.top < window.innerHeight && r.bottom > 0) return poserTout();
272      }
273    }, 2000);
274  }
275
276  function bindReveal(root) {
277    if (!('IntersectionObserver' in window)) return;
278    if (window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
279
280    var targets = root.querySelectorAll('[data-reveal]');
281    var pending = [];
282
283    /* Premier passage, avant que .reveal-ready ne pose l'état masqué : tout ce
284       qui est déjà visible est marqué révélé d'emblée. Ces éléments ne passent
285       donc jamais par opacity: 0 — rien ne clignote au chargement et le LCP
286       n'attend aucune animation. */
287    /* Positions relevées en une fois, classes posées ensuite : une seule
288       mise en page calculée au lieu d'une par élément. */
289    var dejaVus = [];
290    var plafond = window.innerHeight * 0.9;
291    for (var i = 0; i < targets.length; i++) {
292      if (targets[i].getBoundingClientRect().top < plafond) dejaVus.push(targets[i]);
293      else pending.push(targets[i]);
294    }
295    for (var d = 0; d < dejaVus.length; d++) dejaVus[d].classList.add('is-revealed');
296
297    document.documentElement.classList.add('reveal-ready');
298    if (!pending.length) return;
299
300    /* Sens du défilement. L'apparition « Baseline » fait monter le bloc depuis
301       le bas : jouée alors que le lecteur remonte, elle va à contresens et se
302       remarque. Dans ce cas le bloc s'affiche sans animation — jamais caché. */
303    var dernierY = window.scrollY || 0;
304    var versLeBas = true;
305    window.addEventListener('scroll', function () {
306      var y = window.scrollY || 0;
307      if (y !== dernierY) { versLeBas = y > dernierY; dernierY = y; }
308    }, { passive: true });
309
310    var io = new IntersectionObserver(function (entries) {
311      for (var i = 0; i < entries.length; i++) {
312        if (!entries[i].isIntersecting) continue;
313        var cible = entries[i].target;
314        if (cible.getAttribute('data-reveal') === 'baseline' && !versLeBas) {
315          cible.classList.add('sans-animation');
316        }
317        cible.classList.add('is-revealed');
318        io.unobserve(cible);
319      }
320    }, { rootMargin: '0px 0px -8% 0px', threshold: 0.08 });
321
322    var groups = new Map();
323    for (var j = 0; j < pending.length; j++) {
324      var el = pending[j];
325      var key = el.parentNode;
326      var n = groups.get(key) || 0;
327      groups.set(key, n + 1);
328      if (n > 0) el.style.transitionDelay = Math.min(n * 70, 280) + 'ms';
329      io.observe(el);
330    }
331
332    /* Un onglet masqué n'exécute pas l'étape de rendu : l'IntersectionObserver
333       n'y délivre aucun callback (vérifié — un observateur témoin ne reçoit
334       rien non plus). Une page ouverte en arrière-plan doit donc rattraper son
335       retard au moment où elle devient visible. */
336    function balayer() {
337      var reste = false;
338      var aReveler = [];
339      var hauteur = window.innerHeight;
340      for (var i = 0; i < pending.length; i++) {
341        var el = pending[i];
342        if (el.classList.contains('is-revealed')) continue;
343        var r = el.getBoundingClientRect();
344        if (r.top < hauteur && r.bottom > 0) aReveler.push(el);
345        else reste = true;
346      }
347      for (var k = 0; k < aReveler.length; k++) {
348        if (aReveler[k].getAttribute('data-reveal') === 'baseline' && !versLeBas) {
349          aReveler[k].classList.add('sans-animation');
350        }
351        aReveler[k].classList.add('is-revealed');
352      }
353      return reste;
354    }
355
356    /* Filet au défilement.
357
358       L'observateur suffit dans le cas nominal, mais rien ne garantit qu'il
359       parle : onglet mis en veille au chargement, webview inhabituelle,
360       extension qui le neutralise. Le contrôle à deux secondes ne rattrape que
361       ce qui est déjà à l'écran à cet instant — une section atteinte plus tard
362       restait alors masquée pour de bon, ce qui est exactement ce qu'il ne faut
363       jamais laisser arriver à du contenu.
364
365       Ce balayage est adossé au défilement, donc au geste même qui devrait
366       faire apparaître le bloc. Il est calé sur le rendu pour n'être calculé
367       qu'une fois par image, et se retire dès qu'il n'y a plus rien à révéler. */
368    var dernierBalayage = 0;
369    function surDefilement() {
370      /* Cadencé sur l'horloge, pas sur le rendu. `requestAnimationFrame` ne
371         s'exécute pas dans un document qui n'est pas peint — or c'est
372         précisément la situation où l'observateur se tait aussi. Un filet qui
373         partage la panne qu'il doit rattraper ne sert à rien. */
374      var maintenant = Date.now();
375      if (maintenant - dernierBalayage < 100) return;
376      dernierBalayage = maintenant;
377      if (!balayer()) window.removeEventListener('scroll', surDefilement);
378    }
379    window.addEventListener('scroll', surDefilement, { passive: true });
380
381    document.addEventListener('visibilitychange', function () {
382      if (document.visibilityState === 'visible') balayer();
383    });
384
385    /* Filet de sécurité : le contenu ne doit jamais rester invisible. Si, deux
386       secondes après l'affichage, un élément entré dans la fenêtre n'a toujours
387       pas été révélé, c'est que l'observateur ne délivre pas ses callbacks
388       (webview inhabituelle, extension). On révèle alors tout d'un coup — on
389       perd l'animation, jamais le contenu. Le compte à rebours ne démarre que
390       lorsque la page est visible, pour ne pas désactiver l'observateur d'un
391       onglet d'arrière-plan qui n'a simplement pas encore eu son tour. */
392    function armerFilet() {
393      setTimeout(function () {
394        if (document.visibilityState !== 'visible') return armerFilet();
395        var bloque = pending.some(function (el) {
396          if (el.classList.contains('is-revealed')) return false;
397          var r = el.getBoundingClientRect();
398          return r.top < window.innerHeight && r.bottom > 0;
399        });
400        if (!bloque) return;
401        io.disconnect();
402        for (var k = 0; k < pending.length; k++) {
403          pending[k].style.transitionDelay = '';
404          pending[k].classList.add('is-revealed');
405        }
406      }, 2000);
407    }
408    armerFilet();
409  }
410
411  /* Barre de téléchargement.
412
413     Elle n'apparaît qu'une fois le premier écran passé : plus tôt, elle
414     recouvrirait le bouton principal du hero sur un téléphone. Elle se ferme,
415     et le refus est retenu une semaine — un promo qui revient à chaque page est
416     un promo qu'on finit par contourner ; un promo qu'on ne peut plus jamais
417     revoir n'est plus un promo du tout.
418
419     Tout est facultatif : si ce fichier ne s'exécute pas, la barre reste
420     masquée. C'est un ajout, pas un contenu. */
421  var MEMOIRE_FERMETURE = 7 * 24 * 60 * 60 * 1000;   // une semaine
422
423  function bindBarreApp(root) {
424    var barre = root.querySelector('[data-barre-app]');
425    if (!barre || barre.dataset.branchee) return;
426    barre.dataset.branchee = '1';
427
428    /* Le refus est retenu, mais il expire au bout d'une semaine.
429
430       Il était retenu pour toujours : un seul clic sur la croix, et plus rien
431       ne ramenait la barre sur ce navigateur — ni une nouvelle visite, ni une
432       nouvelle version du site. Une valeur d'essai posée un jour condamnait
433       l'élément sans que rien ne le signale.
434
435       Les anciennes valeurs ('fermee') ne se lisent pas comme une date : elles
436       sont donc traitées comme expirées, ce qui rend la barre à ceux qui
437       l'avaient fermée du temps où c'était sans retour. */
438    try {
439      var ferme = parseInt(localStorage.getItem('tfc-barre-app'), 10);
440      if (ferme > 0 && Date.now() - ferme < MEMOIRE_FERMETURE) return;
441    } catch (e) {}
442
443    var fermer = barre.querySelector('[data-barre-fermer]');
444    if (fermer) {
445      fermer.addEventListener('click', function () {
446        barre.classList.remove('est-visible');
447        try { localStorage.setItem('tfc-barre-app', String(Date.now())); } catch (e) {}
448        setTimeout(function () { barre.hidden = true; }, 450);
449      });
450    }
451
452    var hero = root.querySelector('#top');
453    function seuil() {
454      return hero ? hero.getBoundingClientRect().height * 0.6 : window.innerHeight * 0.6;
455    }
456    function ajuster() {
457      var montre = (window.scrollY || 0) > seuil();
458      if (montre === barre.classList.contains('est-visible')) return;
459      if (montre) {
460        barre.hidden = false;
461        barre.setAttribute('aria-hidden', 'false');
462        /* Un calcul de style doit s'intercaler entre l'affichage et la classe,
463           sinon le glissement n'a pas d'état de départ et il est sauté.
464
465           Lire une métrique force ce calcul sur-le-champ, de façon synchrone.
466           `requestAnimationFrame` le faisait aussi — mais il ne s'exécute pas
467           dans un document qui n'est pas peint : onglet ouvert en arrière-plan,
468           fenêtre réduite, page restaurée au démarrage. La barre était alors
469           démasquée sans jamais recevoir sa classe, donc restait glissée hors
470           de l'écran. Invisible, et sans rien pour le dire. */
471        void barre.offsetHeight;
472        barre.classList.add('est-visible');
473      } else {
474        barre.classList.remove('est-visible');
475        barre.setAttribute('aria-hidden', 'true');
476      }
477    }
478    window.addEventListener('scroll', ajuster, { passive: true });
479    ajuster();
480  }
481
482  var FOCUSABLE = 'a[href], button:not([disabled]), input:not([disabled]), select, textarea, [tabindex]:not([tabindex="-1"])';
483
484  function mount(options) {
485    var template = document.getElementById(options.template);
486    var host = document.getElementById(options.mount);
487    var pages = options.pages || {};
488    var pending = false;
489    var currentScope = options.scope();
490    var lastFocus = null;
491
492    function paint() {
493      pending = false;
494      currentScope = options.scope();
495      var next = document.createDocumentFragment();
496      build(template.content, currentScope, Object.create(null), next, null, document);
497      bindHover(next);
498      resolveLinks(next, pages);
499
500      var carried = captureInputs(host);
501      host.textContent = '';
502      host.appendChild(next);
503      restoreInputs(host, carried);
504      afterPaint('paint');
505    }
506
507    /* Le DOM pré-rendu est déjà correct : on ne le reconstruit pas, on se
508       contente de le préparer (survol, liens, révélations). */
509    function adopt() {
510      bindHover(host);
511      resolveLinks(host, pages);
512      afterPaint('adopt');
513    }
514
515    function afterPaint(mode) {
516      // Observable pour le diagnostic : `adopt` = le DOM pre-rendu a ete
517      // conserve tel quel (chemin nominal, l'element LCP n'est pas remplace),
518      // `paint` = il a fallu reconstruire (changement de langue, interaction).
519      document.documentElement.setAttribute('data-hydration', mode);
520      bindFonds(host);
521      bindReveal(host);
522      bindBarreApp(host);
523      manageDialog();
524    }
525
526    /* Modale : focus initial, focus restitué à la fermeture, et arrière-plan
527       masqué aux technologies d'assistance pendant qu'elle est ouverte. */
528    function manageDialog() {
529      var dialog = host.querySelector('[role="dialog"]');
530      if (!dialog) return;
531      var open = getComputedStyle(dialog).display !== 'none';
532      var siblings = host.firstElementChild ? host.firstElementChild.children : [];
533
534      for (var i = 0; i < siblings.length; i++) {
535        if (siblings[i] === dialog) continue;
536        if (open) siblings[i].setAttribute('aria-hidden', 'true');
537        else siblings[i].removeAttribute('aria-hidden');
538      }
539
540      if (open && !dialog.contains(document.activeElement)) {
541        if (!lastFocus) lastFocus = document.activeElement;
542        var first = dialog.querySelector(FOCUSABLE);
543        if (first) first.focus();
544      } else if (!open && lastFocus) {
545        if (document.contains(lastFocus)) lastFocus.focus();
546        lastFocus = null;
547      }
548    }
549
550    // Piège de focus : Tab reste dans la modale tant qu'elle est ouverte.
551    document.addEventListener('keydown', function (event) {
552      if (event.key !== 'Tab') return;
553      var dialog = host.querySelector('[role="dialog"]');
554      if (!dialog || getComputedStyle(dialog).display === 'none') return;
555      var items = dialog.querySelectorAll(FOCUSABLE);
556      if (!items.length) return;
557      var first = items[0], last = items[items.length - 1];
558      if (event.shiftKey && document.activeElement === first) { last.focus(); event.preventDefault(); }
559      else if (!event.shiftKey && document.activeElement === last) { first.focus(); event.preventDefault(); }
560    });
561
562    // Écouteurs délégués : un seul pour toute la page, valables aussi bien sur
563    // le DOM pré-rendu que sur le DOM reconstruit.
564    function delegate(type, attr) {
565      host.addEventListener(type, function (event) {
566        var el = event.target.closest ? event.target.closest('[' + attr + ']') : null;
567        if (!el || !host.contains(el)) return;
568        var fn = lookup(currentScope, el.getAttribute(attr));
569        if (typeof fn === 'function') fn(event);
570      });
571    }
572    delegate('click', 'data-on-click');
573    delegate('submit', 'data-on-submit');
574    /* `input` plutôt que `change` : c'est la sémantique du onChange du canvas
575       (React), et la saisie reste fluide — captureInputs/restoreInputs
576       reposent la valeur et le curseur après chaque re-rendu. */
577    delegate('input', 'data-on-change');
578
579    function schedule() {
580      if (pending) return;
581      pending = true;
582      Promise.resolve().then(paint);
583    }
584
585    if (options.prerenderedLang && options.langOf && options.langOf() === options.prerenderedLang
586        && host.firstElementChild) {
587      adopt();
588    } else {
589      paint();
590    }
591
592    /* La page n'existe pas encore quand le navigateur traiterait un lien
593       profond /#faq. On l'honore ici, en respectant le scroll-margin-top des
594       sections (qui dégage la barre collante). */
595    function jumpToHash() {
596      if (location.hash.length < 2) return;
597      var target = document.getElementById(decodeURIComponent(location.hash.slice(1)));
598      if (!target) return;
599      var clearance = parseFloat(getComputedStyle(target).scrollMarginTop) || 0;
600      window.scrollTo(0, window.scrollY + target.getBoundingClientRect().top - clearance);
601    }
602    jumpToHash();
603    window.addEventListener('load', function () { if (window.scrollY === 0) jumpToHash(); });
604
605    return { render: paint, schedule: schedule };
606  }
607
608  global.Runtime = { mount: mount, build: build, lookup: lookup };
609})(typeof globalThis !== 'undefined' ? globalThis : this);

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.