PageSourceSearch

https://www.formality.com/js/main.js

js formality.com collected 2026-10-08 05:59:56 UTC 24,353 bytes, 512 lines download raw bytes

1/* ==========================================================================
2   Formality — interactions (vanilla JS, sans dépendance)
3   Généré depuis Formality Home V2.dc.html. Amélioration progressive : la page reste
4   lisible et indexable sans JavaScript.
5   ========================================================================== */
6(function () {
7  'use strict';
8
9  var C = {
10    props: { ambientAnimations: true, loopSpeed: 1.3 },
11    gen: 0, timers: [], cleanup: [],
12
13  isStatic: function() {
14    return matchMedia('(prefers-reduced-motion: reduce)').matches || (this.props.ambientAnimations ?? true) === false;
15  },
16
17  sp: function() {
18    const v = +(this.props.loopSpeed ?? 1);
19    return isNaN(v) ? 1 : Math.min(2, Math.max(0.4, v));
20  },
21
22  q: function(s) { return document.querySelector(s); },
23
24  qa: function(s) { return [...document.querySelectorAll(s)]; },
25
26  after: function(ms, fn) { const gen = this.gen; this.timers.push(setTimeout(() => { if (gen === this.gen) fn(); }, ms)); },
27
28  lockScroll: function(on) {
29    const b = document.body;
30    if (on) {
31      this._scrollY = window.scrollY || 0;
32      b.style.top = -this._scrollY + 'px';
33      b.setAttribute('data-scroll-lock', '');
34    } else if (b.hasAttribute('data-scroll-lock')) {
35      b.removeAttribute('data-scroll-lock');
36      b.style.top = '';
37      window.scrollTo(0, this._scrollY || 0);
38    }
39  },
40
41  setupVideos: function() {
42    const onClick = (e) => {
43      const btn = e.target.closest('[data-yt]');
44      if (!btn) return;
45      const id = btn.getAttribute('data-yt');
46      const frame = btn.closest('[data-video]');
47      if (!id || !frame || frame.querySelector('iframe')) return;
48      e.preventDefault();
49      const iframe = document.createElement('iframe');
50      iframe.src = 'https://www.youtube.com/embed/' + id + '?autoplay=1&rel=0&modestbranding=1&playsinline=1';
51      iframe.title = btn.getAttribute('aria-label') || 'Témoignage vidéo';
52      iframe.allow = 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share';
53      iframe.allowFullscreen = true;
54      iframe.referrerPolicy = 'strict-origin-when-cross-origin';
55      iframe.setAttribute('style', 'position:absolute;inset:0;width:100%;height:100%;border:0;display:block');
56      frame.appendChild(iframe);
57    };
58    document.addEventListener('click', onClick);
59    this.cleanup.push(() => document.removeEventListener('click', onClick));
60  },
61
62  setupContactForm: function() {
63    const FORM_URL = 'https://share-eu1.hsforms.com/1GKQF3MSpRIOcE98fc4tz0Q2dkfbc';
64    const modal = this.q('[data-form-modal]');
65    if (!modal) return;
66    const body = modal.querySelector('[data-form-body]');
67    const open = () => {
68      if (!body.querySelector('iframe')) {
69        body.setAttribute('data-frame', '');
70        const iframe = document.createElement('iframe');
71        iframe.src = FORM_URL;
72        iframe.title = 'Prendre rendez-vous avec Formality';
73        iframe.setAttribute('style', 'width:100%;height:100%;border:0;display:block');
74        body.appendChild(iframe);
75      }
76      modal.style.display = 'flex';
77      C.lockScroll(true);
78    };
79    const close = () => {
80      modal.style.display = 'none';
81      C.lockScroll(false);
82    };
83    const onClick = (e) => {
84      const a = e.target.closest('a');
85      if (a && (a.hasAttribute('data-open-form') || a.textContent.indexOf('Parlons de vos enjeux') !== -1)) { e.preventDefault(); open(); return; }
86      if (e.target.closest('[data-form-close]') || e.target.hasAttribute('data-form-backdrop')) { e.preventDefault(); close(); }
87    };
88    const onKey = (e) => { if (e.key === 'Escape' && modal.style.display === 'flex') close(); };
89    document.addEventListener('click', onClick);
90    document.addEventListener('keydown', onKey);
91    this.cleanup.push(() => { document.removeEventListener('click', onClick); document.removeEventListener('keydown', onKey); });
92  },
93
94  setupLangMenu: function() {
95    const sw = this.q('[data-lang-switch]');
96    if (!sw) return;
97    const trigger = sw.querySelector('[data-lang-trigger]');
98    const set = (open) => {
99      if (open) sw.setAttribute('data-open', ''); else sw.removeAttribute('data-open');
100      if (trigger) trigger.setAttribute('aria-expanded', open ? 'true' : 'false');
101    };
102    if (trigger) trigger.addEventListener('click', (e) => { e.preventDefault(); set(!sw.hasAttribute('data-open')); });
103    document.addEventListener('click', (e) => { if (!sw.contains(e.target)) set(false); });
104    document.addEventListener('keydown', (e) => { if (e.key === 'Escape') set(false); });
105  },
106
107  setupMobileNav: function() {
108    const nav = this.q('[data-navbar]');
109    const btn = this.q('[data-nav-toggle]');
110    if (!nav || !btn) return;
111    const set = (open) => { if (open) nav.setAttribute('data-nav-open', ''); else nav.removeAttribute('data-nav-open'); btn.setAttribute('aria-expanded', open ? 'true' : 'false'); };
112    const toggle = (e) => { e.preventDefault(); set(!nav.hasAttribute('data-nav-open')); };
113    const onDoc = (e) => { if (nav.hasAttribute('data-nav-open') && !nav.contains(e.target)) set(false); };
114    const onLink = (e) => { if (e.target.closest('[data-navlinks] a')) set(false); };
115    btn.addEventListener('click', toggle);
116    document.addEventListener('click', onDoc);
117    nav.addEventListener('click', onLink);
118    this.cleanup.push(() => { btn.removeEventListener('click', toggle); document.removeEventListener('click', onDoc); nav.removeEventListener('click', onLink); });
119  },
120
121  componentDidMount: function() {
122    document.documentElement.lang = 'fr';
123    this.gen = 0; this.timers = []; this.cleanup = [];
124    const onScroll = () => {
125      const nav = this.q('[data-navbar]');
126      if (!nav) return;
127      const on = window.scrollY > 30;
128      nav.style.background = on ? 'rgba(11,3,65,.88)' : 'transparent';
129      nav.style.backdropFilter = on ? 'blur(16px)' : 'none';
130      nav.style.borderColor = on ? 'rgba(190,181,246,.26)' : 'transparent';
131    };
132    addEventListener('scroll', onScroll, { passive: true });
133    this.cleanup.push(() => removeEventListener('scroll', onScroll));
134    this.setupReveals();
135    this.setupClauses();
136    this.setupCounters();
137    this.setupFaq();
138    this.setupUseCases();
139    this.setupVideos();
140    this.setupContactForm();
141    this.setupMobileNav();
142    this.setupLangMenu();
143    this.initMotion();
144  },
145
146  stopMotion: function() { this.gen++; this.timers.forEach(clearTimeout); this.timers = []; },
147
148  initMotion: function() {
149    this.gen++;
150    const track = this.q('[data-tickertrack]');
151    if (track) track.style.animationPlayState = this.isStatic() ? 'paused' : 'running';
152    if (this.isStatic()) this.applyStatic();
153    else { this.startHero(); this.startPipe(); this.startAsk(); }
154  },
155
156  lineHit: function(l) { l.style.background = '#81EBC5'; l.style.boxShadow = '0 0 14px rgba(129,235,197,.5)'; },
157
158  lineMk: function(l) { l.style.background = '#81EBC5'; l.style.boxShadow = '0 0 12px rgba(129,235,197,.45)'; },
159
160  lineOff: function(l) { l.style.background = (l.dataset.k !== undefined) ? '#DDD9F1' : '#E7E5F2'; l.style.boxShadow = 'none'; },
161
162  stampOn: function(el, rot) { el.style.opacity = '1'; el.style.transform = 'scale(1) rotate(' + rot + 'deg)'; },
163
164  stampOff: function(el, scale, rot) { el.style.opacity = '0'; el.style.transform = 'scale(' + scale + ') rotate(' + rot + 'deg)'; },
165
166  fillSlot: function(slot, v) {
167    const val = slot.querySelector('[data-slot-v]');
168    val.textContent = v; val.style.color = '#F4F2FF'; slot.style.opacity = '1';
169  },
170
171  emptySlot: function(slot) {
172    const val = slot.querySelector('[data-slot-v]');
173    val.textContent = '\u00b7\u00b7\u00b7'; val.style.color = 'rgba(244,242,255,.82)'; slot.style.opacity = '.55';
174  },
175
176  litDot: function(d, on) {
177    d.style.borderColor = on ? '#81EBC5' : 'rgba(190,181,246,.5)';
178    d.style.color = on ? '#81EBC5' : 'rgba(244,242,255,.82)';
179    d.style.boxShadow = on ? '0 0 22px rgba(129,235,197,.35)' : 'none';
180  },
181
182  fillField: function(v) {
183    v.textContent = v.dataset.v;
184    v.style.color = '#0A0046'; v.style.fontWeight = '600';
185    v.style.background = 'rgba(129,235,197,.28)';
186    v.style.borderColor = 'rgba(129,235,197,.75)'; v.style.borderStyle = 'solid';
187    v.style.boxShadow = 'none';
188  },
189
190  emptyField: function(v) {
191    const ph = { 'Loyer annuel': '\u2039 loyer \u203a', 'Indexation': '\u2039 indexation \u203a', 'Prochaine \u00e9ch\u00e9ance': '\u2039 \u00e9ch\u00e9ance \u203a', 'Pr\u00e9avis': '\u2039 pr\u00e9avis \u203a' };
192    v.textContent = ph[v.dataset.k] || '\u2039 \u2026 \u203a';
193    v.style.color = '#6D5DD3'; v.style.fontWeight = '600';
194    v.style.background = 'rgba(190,181,246,.18)';
195    v.style.borderColor = 'rgba(190,181,246,.75)'; v.style.borderStyle = 'dashed';
196    v.style.boxShadow = 'none';
197  },
198
199  fieldGlow: function(v) {
200    v.style.boxShadow = '0 0 14px rgba(129,235,197,.6)';
201  },
202
203  stepState: function(el, state) {
204    const fill = el.querySelector('[data-stepfill]'), num = el.querySelector('[data-stepnum]'), lab = el.querySelector('[data-steplabel]');
205    const c = state === 'active' ? '#81EBC5' : '#F4F2FF';
206    fill.style.width = state === 'upcoming' ? '0%' : '100%';
207    num.style.color = c; lab.style.color = c;
208  },
209
210  applyStatic: function() {
211    const card = this.q('[data-contract]');
212    if (card) {
213      [...card.querySelectorAll('[data-field]')].forEach((f) => { this.fillField(f); this.fieldGlow(f); });
214      [...card.querySelectorAll('[data-m]')].forEach((l) => this.lineOff(l));
215      this.qa('[data-slot]').forEach((s) => {
216        const src = card.querySelector('[data-field][data-k="' + s.dataset.slot + '"]');
217        if (src) this.fillSlot(s, src.dataset.v);
218      });
219      const stamp = this.q('[data-stamp]'), cstamp = this.q('[data-cstamp]'), beam = this.q('[data-beam]'), ph = this.q('[data-cphase]'), fiche = this.q('[data-ficheind]');
220      if (stamp) this.stampOn(stamp, -4);
221      if (cstamp) this.stampOn(cstamp, -6);
222      if (beam) beam.style.opacity = '0';
223      if (ph) ph.textContent = 'Document sign\u00e9 \u00b7 PDF';
224      if (fiche) fiche.style.opacity = '1';
225      const chips = this.q('[data-scan]');
226      if (chips) chips.querySelectorAll('[data-chip]').forEach((c) => c.remove());
227    }
228    { const steps = [...this.qa('[data-step]')]; steps.forEach((el, k) => this.stepState(el, k < steps.length - 1 ? 'done' : 'active')); }
229    this.qa('[data-dot]').forEach((d) => this.litDot(d, true));
230    const pulse = this.q('[data-pulse]');
231    if (pulse) pulse.style.opacity = '0';
232    const pairs = this.askPairs();
233    const qEl = this.q('[data-asktext]'), ans = this.q('[data-askanswer]'), caret = this.q('[data-caret]');
234    if (qEl) { qEl.textContent = pairs[0].q; ans.innerHTML = pairs[0].a; ans.style.opacity = '1'; caret.style.display = 'none'; }
235  },
236
237  startHero: function() {
238    const gen = this.gen, self = this;
239    const scan = this.q('[data-scan]'), card = this.q('[data-contract]'), beam = this.q('[data-beam]'),
240      stamp = this.q('[data-stamp]'), cstamp = this.q('[data-cstamp]'), cPhase = this.q('[data-cphase]'),
241      fiche = this.q('[data-ficheind]');
242    if (!scan || !card) return;
243    const fields = [...card.querySelectorAll('[data-field]')];
244    const mLines = [...card.querySelectorAll('[data-m]')];
245    const steps = [...this.qa('[data-step]')];
246    const slots = {};
247    this.qa('[data-slot]').forEach((s) => { slots[s.dataset.slot] = s; });
248    const sp = this.sp(), DUR = 18000 / sp;
249    let start = null;
250    const fired = new Set(), events = new Set();
251    const geom = { h: 0, tops: [] };
252    beam.style.willChange = 'transform';
253
254    const setStep = (i) => steps.forEach((el, k) => self.stepState(el, k < i ? 'done' : k === i ? 'active' : 'upcoming'));
255
256    function reset() {
257      fired.clear(); events.clear();
258      fields.forEach((f) => self.emptyField(f));
259      mLines.forEach((l) => self.lineOff(l));
260      Object.values(slots).forEach((s) => self.emptySlot(s));
261      self.stampOff(stamp, .85, -4); self.stampOff(cstamp, 1.4, -6);
262      cPhase.textContent = 'Mod\u00e8le \u00b7 Champs \u00e0 compl\u00e9ter';
263      if (fiche) fiche.style.opacity = '.4';
264      beam.style.opacity = '0';
265      beam.style.transform = 'translateY(0px)';
266      geom.h = card.offsetHeight;
267      geom.tops = fields.map((f) => f.offsetTop);
268      setStep(0);
269      scan.querySelectorAll('[data-chip]').forEach((c) => c.remove());
270    }
271    function mkChip(text, left, top) {
272      const chip = document.createElement('div');
273      chip.setAttribute('data-chip', '');
274      chip.style.cssText = 'position:absolute;z-index:5;font-family:Montserrat,sans-serif;font-size:11px;background:#0A0046;color:#81EBC5;border:1px solid rgba(129,235,197,.5);padding:5px 10px;border-radius:4px;white-space:nowrap;transition:transform 1s cubic-bezier(.3,.7,.2,1),opacity .3s;box-shadow:0 8px 30px rgba(0,0,0,.5)';
275      chip.textContent = text;
276      chip.style.left = left + 'px'; chip.style.top = top + 'px';
277      scan.appendChild(chip);
278      return chip;
279    }
280    function chipAt(line, text, hold) {
281      const sR = scan.getBoundingClientRect(), lR = line.getBoundingClientRect();
282      const chip = mkChip(text, lR.left - sR.left, lR.top - sR.top - 10);
283      requestAnimationFrame(() => requestAnimationFrame(() => { chip.style.transform = 'translateY(-14px)'; }));
284      self.after(hold, () => { chip.style.opacity = '0'; self.after(350, () => chip.remove()); });
285    }
286    function fieldExtract(row) {
287      self.fieldGlow(row);
288      const k = row.dataset.k, v = row.dataset.v, slot = slots[k];
289      if (!slot) return;
290      const sR = scan.getBoundingClientRect(), lR = row.getBoundingClientRect(), tR = slot.getBoundingClientRect();
291      const chip = mkChip(k + ' : ' + v, lR.left - sR.left, lR.top - sR.top - 4);
292      requestAnimationFrame(() => requestAnimationFrame(() => {
293        chip.style.transform = 'translate(' + (tR.left - lR.left) + 'px,' + (tR.top - lR.top) + 'px)';
294      }));
295      self.after(1000 / sp, () => { chip.style.opacity = '0'; self.fillSlot(slot, v); self.after(350, () => chip.remove()); });
296    }
297    function once(key, fn) { if (!events.has(key)) { events.add(key); fn(); } }
298    function loop(now) {
299      if (gen !== self.gen) return;
300      if (start == null) start = now;
301      const t = ((now - start) % DUR) * sp;
302      if (t < 40 && events.size > 0) reset();
303      /* --- Phase 1 : remplissage du modèle --- */
304      if (t > 800)  once('f0', () => self.fillField(fields[0]));
305      if (t > 1700) once('f1', () => self.fillField(fields[1]));
306      if (t > 2600) once('f2', () => self.fillField(fields[2]));
307      if (t > 3500) once('f3', () => self.fillField(fields[3]));
308      /* --- Phase 2 : négociation IA --- */
309      if (t > 4600) once('p2', () => { setStep(1); cPhase.textContent = 'N\u00e9gociation \u00b7 Version 3'; });
310      if (t > 5200) once('m0', () => { self.lineMk(mLines[0]); chipAt(mLines[0], mLines[0].dataset.m, 2400 / sp); });
311      if (t > 6600) once('m1', () => { self.lineMk(mLines[1]); chipAt(mLines[1], mLines[1].dataset.m, 2400 / sp); });
312      /* --- Phase 3 : signature --- */
313      if (t > 8600) once('p3', () => setStep(2));
314      if (t > 9100) once('sig', () => { self.stampOn(cstamp, -6); cPhase.textContent = 'Document sign\u00e9 \u00b7 PDF'; mLines.forEach((l) => self.lineOff(l)); });
315      /* --- Phase 4 : extraction / analyse --- */
316      if (t > 10800 && t < 15800) {
317        once('p4', () => { setStep(3); if (fiche) fiche.style.opacity = '1'; });
318        const y = -50 + (geom.h + 70) * ((t - 10800) / 5000);
319        beam.style.transform = 'translateY(' + (y + 60) + 'px)'; beam.style.opacity = '1';
320        for (let i = 0; i < fields.length; i++) {
321          const f = fields[i];
322          if (!fired.has(f) && geom.tops[i] < y + 22) { fired.add(f); fieldExtract(f); }
323        }
324      } else if (t >= 15800) {
325        beam.style.opacity = '0';
326        if (t > 16600) once('valid', () => self.stampOn(stamp, -4));
327      }
328      requestAnimationFrame(loop);
329    }
330    reset();
331    requestAnimationFrame(loop);
332  },
333
334  startPipe: function() {
335    const gen = this.gen, self = this;
336    const pulse = this.q('[data-pulse]'), steps = this.qa('[data-dot]');
337    if (!pulse || !steps.length) return;
338    pulse.style.opacity = '1';
339    pulse.style.willChange = 'transform';
340    const host = pulse.parentElement;
341    let W = host.clientWidth;
342    const onRz = () => { W = host.clientWidth; };
343    addEventListener('resize', onRz);
344    this.cleanup.push(() => removeEventListener('resize', onRz));
345    const D = 7000 / this.sp();
346    let start = null;
347    function loop(now) {
348      if (gen !== self.gen) return;
349      if (start == null) start = now;
350      const u = ((now - start) % D) / D;
351      pulse.style.transform = 'translateX(' + (u * 0.92 * W) + 'px)';
352      steps.forEach((s, i) => {
353        const su = (i + 0.5) / steps.length;
354        self.litDot(s, u > su - 0.5 / steps.length);
355      });
356      requestAnimationFrame(loop);
357    }
358    requestAnimationFrame(loop);
359  },
360
361  askPairs: function() {
362    const P = (t) => '<p style="font-size:15.5px;color:rgba(244,242,255,.82);margin:0 0 16px">' + t + '</p>';
363    const B = (t) => '<b style="color:#81EBC5;font-weight:600">' + t + '</b>';
364    const S = (t) => '<span style="display:inline-flex;align-items:center;gap:8px;font-family:Montserrat,sans-serif;font-size:12px;color:#BEB5F6;border:1px solid rgba(190,181,246,.5);padding:6px 11px;margin:0 8px 8px 0"><i style="color:#81EBC5;font-style:normal">\u00a7</i>' + t + '</span>';
365    return [
366      { q: 'Quels contrats ont une indexation \u00e0 appliquer ce trimestre ?',
367        a: P(B('3 contrats concern\u00e9s') + " : le bail Rivoli, le contrat cadre Nexa et l'avenant n\u00b03 Meridian. Indexation \u00e0 appliquer avant le 30 septembre.") + S('Bail Rivoli \u00b7 art. 4.2') + S('Contrat cadre Nexa \u00b7 art. 7.1') + S('Avenant n\u00b03 \u00b7 annexe B') },
368      { q: 'Quelles \u00e9ch\u00e9ances arrivent dans les 90 proch
368ains jours ?',
369        a: P(B('5 \u00e9ch\u00e9ances identifi\u00e9es') + ' : 2 renouvellements tacites, 2 pr\u00e9avis \u00e0 notifier, 1 garantie bancaire \u00e0 renouveler. La plus proche est \u00e0 J-42.') + S('Bail commercial \u00b7 art. 3.4') + S('Pr\u00eat bancaire \u00b7 clause 9') },
370      { q: 'Quels engagements de plafonnement de loyer avons-nous sign\u00e9s ?',
371        a: P(B('2 clauses de plafonnement actives') + ', toutes deux limit\u00e9es \u00e0 la variation de l\u2019indice avec un cap de 3,5% par an.') + S('Bail Rivoli \u00b7 art. 4.3') + S('Bail Haussmann \u00b7 art. 5.1') }
372    ];
373  },
374
375  startAsk: function() {
376    const gen = this.gen, self = this;
377    const qEl = this.q('[data-asktext]'), ans = this.q('[data-askanswer]'), caret = this.q('[data-caret]');
378    if (!qEl) return;
379    caret.style.display = 'inline-block';
380    const pairs = this.askPairs();
381    const sp = this.sp();
382    const CPS = 26 * sp;
383    let pi = 0;
384    function cycle() {
385      if (gen !== self.gen) return;
386      const p = pairs[pi]; pi = (pi + 1) % pairs.length;
387      qEl.textContent = ''; ans.style.opacity = '0';
388      const t0 = performance.now();
389      let shown = -1;
390      function frame(now) {
391        if (gen !== self.gen) return;
392        const n = Math.min(p.q.length, Math.floor((now - t0) / 1000 * CPS));
393        if (n !== shown) { shown = n; qEl.textContent = p.q.slice(0, n); }
394        if (n < p.q.length) { requestAnimationFrame(frame); return; }
395        self.after(500, () => { ans.innerHTML = p.a; ans.style.opacity = '1'; });
396        self.after(6500 / sp, cycle);
397      }
398      requestAnimationFrame(frame);
399    }
400    cycle();
401  },
402
403  setupReveals: function() {
404    if (matchMedia('(prefers-reduced-motion: reduce)').matches) return;
405    const els = this.qa('[data-reveal]');
406    if (!('IntersectionObserver' in window)) { els.forEach((el) => { el.style.opacity = '1'; el.style.transform = 'none'; }); return; }
407    const groups = new Map();
408    els.forEach((el) => {
409      const p = el.parentElement;
410      if (!groups.has(p)) groups.set(p, []);
411      groups.get(p).push(el);
412    });
413    const io = new IntersectionObserver((es) => {
414      es.forEach((e) => {
415        if (e.isIntersecting) { e.target.style.opacity = '1'; e.target.style.transform = 'none'; io.unobserve(e.target); }
416      });
417    }, { threshold: .12 });
418    els.forEach((el) => {
419      const base = +(el.dataset.reveal || 0);
420      const sibs = groups.get(el.parentElement);
421      const i = Math.max(0, sibs.indexOf(el));
422      const delay = (base * 0.1 + i * 0.08).toFixed(2);
423      el.style.opacity = '0';
424      el.style.transform = 'translateY(30px)';
425      el.style.transition = 'opacity .85s cubic-bezier(.2,.7,.3,1) ' + delay + 's,transform .85s cubic-bezier(.2,.7,.3,1) ' + delay + 's';
426      io.observe(el);
427    });
428    this.cleanup.push(() => io.disconnect());
429  },
430
431  setupClauses: function() {
432    const els = this.qa('[data-uline]');
433    if (matchMedia('(prefers-reduced-motion: reduce)').matches || !('IntersectionObserver' in window)) { els.forEach((e) => e.classList.add('is-drawn')); return; }
434    const io = new IntersectionObserver((es) => {
435      es.forEach((e) => { if (e.isIntersecting) { e.target.classList.add('is-drawn'); io.unobserve(e.target); } });
436    }, { threshold: .6 });
437    els.forEach((e) => io.observe(e));
438    this.cleanup.push(() => io.disconnect());
439  },
440
441  setupCounters: function() {
442    const els = this.qa('[data-count]');
443    if (matchMedia('(prefers-reduced-motion: reduce)').matches || !('IntersectionObserver' in window)) {
444      els.forEach((el) => { el.textContent = el.dataset.count; });
445      return;
446    }
447    const io = new IntersectionObserver((es) => {
448      es.forEach((e) => {
449        if (!e.isIntersecting) return;
450        io.unobserve(e.target);
451        const el = e.target, end = +el.dataset.count, t0 = performance.now(), D = 1400;
452        const up = (now) => {
453          const u = Math.min((now - t0) / D, 1);
454          el.textContent = Math.round(end * (1 - Math.pow(1 - u, 3)));
455          if (u < 1) requestAnimationFrame(up);
456        };
457        requestAnimationFrame(up);
458      });
459    }, { threshold: .5 });
460    els.forEach((el) => { el.textContent = '0'; io.observe(el); });
461    this.cleanup.push(() => io.disconnect());
462  },
463
464  setupUseCases: function() {
465    const items = this.qa('[data-uc]');
466    items.forEach((item, idx) => {
467      const btn = item.querySelector('[data-uc-q]'), a = item.querySelector('[data-uc-a]'), x = item.querySelector('[data-uc-x]');
468      const set = (open) => {
469        item.dataset.open = open ? '1' : '';
470        a.style.maxHeight = open ? (a.scrollHeight + 24) + 'px' : '0px';
471        x.style.transform = 'rotate(' + (open ? 45 : 0) + 'deg)';
472        item.style.borderColor = open ? 'rgba(129,235,197,.55)' : 'rgba(190,181,246,.5)';
473      };
474      item._ucSet = set;
475      item.addEventListener('mouseenter', () => { items.forEach((it) => it._ucSet(it === item)); });
476      item.addEventListener('mouseleave', () => set(false));
477      btn.addEventListener('click', () => {
478        const willOpen = !item.dataset.open;
479        items.forEach((it) => it._ucSet(it === item ? willOpen : false));
480      });
481      set(false);
482    });
483  },
484
485  setupFaq: function() {
486    this.qa('[data-faq]').forEach((item, idx) => {
487      const btn = item.querySelector('[data-faq-q]'), a = item.querySelector('[data-faq-a]'), x = item.querySelector('[data-faq-x]');
488      const set = (open) => {
489        item.dataset.open = open ? '1' : '';
490        a.style.maxHeight = open ? (a.scrollHeight + 24) + 'px' : '0px';
491        x.style.transform = 'translateY(-50%) rotate(' + (open ? 45 : 0) + 'deg)';
492      };
493      btn.addEventListener('click', () => set(!item.dataset.open));
494      set(idx === 0);
495    });
496  }
497  };
498
499  function boot() {
500    try { C.componentDidMount(); }
501    catch (err) {
502      /* filet de sécurité : jamais de contenu invisible si un script échoue */
503      Array.prototype.forEach.call(document.querySelectorAll('[data-reveal]'), function (el) {
504        el.style.opacity = '1'; el.style.transform = 'none';
505      });
506      if (window.console && console.error) console.error(err);
507    }
508  }
509  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', boot);
510  else boot();
511  window.FormalitySite = C;
512})();

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.