PageSourceSearch

https://rootevidence.com/home.js?v=c943d036

js rootevidence.com collected 2026-10-09 22:47:06 UTC 26,712 bytes, 622 lines download raw bytes

1/* ==========================================================================
2   home.js — HOME page behavior.
3   1) Silky sequential reveals: .rv elements fade+rise in (.in) as they enter
4      the viewport, staggered per section on the brand's --stagger beat. The
5      page is fully visible without JS (styles arm only once body.rv-armed is
6      set here); reduced-motion users and non-IO browsers get everything
7      immediately.
8   2) Guilloché hero background (.home-gbg — the gallery's GBG1 ported as a
9      fixed full-viewport layer): a gentle eased fade-IN on land, then a FAST
10      eased fade-OUT on scroll — opacity = loadK · (1-p)³ with p = y/(0.55·vh),
11      so the very first pixels of scroll produce visible fade and the field is
12      completely gone before the FIRE section arrives. The copy is NOT pinned
13      and NOT faded: it scrolls in normal flow (Aaron: scrolling must never
14      feel blocked). Below 0.02 the wrapper snaps to true 0 so the engine's
15      cssShown gate parks the surface and frees its animation slot; scrolling
16      back up resumes from the parked painted frame — no flash. Reduced
17      motion: no listener; home.css hides the layer entirely.
18   3) Financial Risk Retired counter: the dollar figure is PLAIN ink text
19      (no foil — restraint) counting $1.4M → $62.1M over ~210s (linear,
20      rAF), then looping. Text is painted at most ~7×/sec — enough for a
21      one-decimal readout; faster is pointless DOM churn. The assets figure
22      does NOT count with it: it holds at 194,755 and bumps +100–150 every
23      6–10s with a short ease. An IntersectionObserver pauses everything
24      while the card is offscreen. Reduced motion: static end state.
25   (The former #4 — light stack-card seal field sync — left with the emblem
26   seal when the stacking cards went plain: restraint.)
27   ========================================================================== */
28(function () {
29  'use strict';
30
31  var reduce = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
32  var hasIO = 'IntersectionObserver' in window;
33
34  /* ---------- 1 · reveals ---------- */
35  (function () {
36    var els = Array.prototype.slice.call(document.querySelectorAll('.rv'));
37    if (!els.length) return;
38
39    if (reduce || !hasIO) {
40      els.forEach(function (el) { el.classList.add('in'); });
41      return;
42    }
43
44    document.body.classList.add('rv-armed');
45
46    /* stagger: each element waits its turn within its own section (90ms = --stagger) */
47    var counts = [];
48    els.forEach(function (el) {
49      var sec = el.closest('section') || document.body;
50      var hit = null;
51      for (var i = 0; i < counts.length; i++) if (counts[i].sec === sec) { hit = counts[i]; break; }
52      if (!hit) { hit = { sec: sec, n: 0, step: parseInt(sec.getAttribute && sec.getAttribute('data-stagger'), 10) || 90 }; counts.push(hit); }
53      el.style.transitionDelay = (hit.n * hit.step) + 'ms';
54      hit.n++;
55    });
56
57    var io = new IntersectionObserver(function (entries) {
58      entries.forEach(function (en) {
59        if (!en.isIntersecting) return;
60        en.target.classList.add('in');
61        io.unobserve(en.target);
62      });
63    }, { threshold: 0.15, rootMargin: '0px 0px -8% 0px' });
64
65    els.forEach(function (el) { io.observe(el); });
66  })();
67
68  /* ---------- 2 · guilloché hero background (GBG10): land fade-in, fast
69     scroll fade-out ---------- */
70  (function () {
71    var bg = document.querySelector('.home-gbg');
72    if (!bg || reduce) return; /* reduced motion: home.css hides the layer */
73
74    var FADE_VH = 0.55;   /* fully gone by ~55% of one viewport height */
75    var IN_MS = 700;      /* the gentle land fade-in */
76
77    var loadK = 0;        /* 0→1 across the land fade-in */
78    var t0 = null, ticking = false;
79
80    /* scroll term: ease-OUT on the fade — (1-p)³ has slope -3 at p=0, so the
81       first pixels of scroll visibly bite, then the tail settles smoothly */
82    function scrollK() {
83      var vh = window.innerHeight || 1;
84      var y = window.scrollY || window.pageYOffset || 0;
85      var p = Math.min(1, Math.max(0, y / (vh * FADE_VH)));
86      var q = 1 - p;
87      return q * q * q;
88    }
89
90    function paint() {
91      ticking = false;
92      var o = loadK * scrollK();
93      /* snap the last 2% to TRUE zero: the engine's cssShown gate then parks
94         the surfaces (painted static frames kept) and frees their budget slots */
95      if (o < 0.02) o = 0;
96      bg.style.opacity = String(o);
97    }
98    function onScroll() {
99      if (ticking) return;
100      ticking = true;
101      window.requestAnimationFrame(paint);
102    }
103
104    function fadeIn(ts) {
105      if (t0 === null) t0 = ts;
106      var p = Math.min(1, (ts - t0) / IN_MS);
107      loadK = 1 - Math.pow(1 - p, 3);
108      paint();
109      if (p < 1) window.requestAnimationFrame(fadeIn);
110    }
111
112    window.addEventListener('scroll', onScroll, { passive: true });
113    window.requestAnimationFrame(fadeIn);
114  })();
115
116  /* ---------- 2b · bento: scan progress bar + trendline draw-in ---------- */
117  (function () {
118    /* scan bar: the SCAN PAGE's creep (2026-07-24, Aaron: "exactly the
119       same way") - scroll-scrubbed 8% -> 100% with the same outCubic
120       settle, driven by the bar's own travel through the lower two thirds
121       of the viewport. Reduced motion: the CSS 62% rest state. */
122    if (!reduce) {
123      var bars = Array.prototype.slice.call(document.querySelectorAll('[data-scanbar]'));
124      if (bars.length) {
125        var outCubicBar = function (t) { t = Math.min(1, Math.max(0, t)); return 1 - Math.pow(1 - t, 3); };
126        var lastW = [];
127        var rafBar = null;
128        var paintBars = function () {
129          rafBar = null;
130          var vh = window.innerHeight || 1;
131          bars.forEach(function (bar, i) {
132            var fill = bar.querySelector('.home-scanbar__fill');
133            if (!fill) return;
134            var r = bar.getBoundingClientRect();
135            if (r.bottom < -40 || r.top > vh + 40) return;
136            var w = 8 + 92 * outCubicBar((vh - r.top) / (vh * 0.66));
137            if (Math.abs((lastW[i] || 0) - w) > 0.05) { lastW[i] = w; fill.style.width = w + '%'; }
138          });
139        };
140        window.addEventListener('scroll', function () {
141          if (!rafBar) rafBar = requestAnimationFrame(paintBars);
142        }, { passive: true });
143        paintBars();
144      }
145    }
146
147    /* 'last scan cycle completed' = always yesterday (we scan daily) */
148    Array.prototype.forEach.call(document.querySelectorAll('[data-scan-yday]'), function (el) {
149      el.textContent = new Date(Date.now() - 864e5).toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
150    });
151
152    /* Warranty-card slot tuning (2026-07-22, Aaron: "the stripe sometimes
153       doesn't light up" - on the crowded homepage the stamp's four pri-4
154       sheets lost the animation-slot auction to earlier pri-4 surfaces,
155       unlike the one-section-at-a-time gallery):
156       - the edge stamp is the card's hero element -> PROMOTE to priority 2
157         (right behind the badge and the globe), so its sheets always hold
158         slots and answer the pointer exactly like the components page
159       - the whisper background laminate is garnish -> DEMOTE to priority 8
160         (the site.js underline pattern); silver-locked, it reads the same
161         parked or animated */
162    function bentoSlotTuning() {
163      Array.prototype.forEach.call(document.querySelectorAll('.home-bento__lam .hl'), function (el) {
164        if (el.__evdHolo) el.__evdHolo.priority = 8;
165      });
166      Array.prototype.forEach.call(document.querySelectorAll('.home-bento__card--warranty > .hl--seal, .home-firsts__card--warranty > .hl--seal'), function (el) {
167        if (el.__evdHolo) el.__evdHolo.priority = 2;
168      });
169      /* the FIRE-list stat icons must always animate (parked icons show one
170         dull static frame - Aaron wants them shining like the nav dropdown
171         icons, which always hold slots): icon priority 6 -> 3 */
172      Array.prototype.forEach.call(document.querySelectorAll('.home-fires .hl--icon'), function (el) {
173        if (el.__evdHolo) el.__evdHolo.priority = 3;
174      });
175    }
176    window.addEventListener('evd-holo-ready', bentoSlotTuning);
177    var lamT = 0;
178    window.addEventListener('resize', function () {
179      if (lamT) return;
180      lamT = setTimeout(function () { lamT = 0; bentoSlotTuning(); }, 400);
181    }, { passive: true });
182
183    /* trendline draw-in ([data-spark-draw]: the bento Reporting card AND the
184       3c board-slide): the curtain sweeps 0 -> 100% while the dot rides the
185       reveal edge along the polyline. Runs once per wrapper, on scroll-in.
186       Reduced motion: CSS shows everything fully drawn, dot stays at the
187       markup endpoint. */
188    if (reduce) return;
189    var DUR = 2600;
190    function ease(t) { return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2; }
191    Array.prototype.forEach.call(document.querySelectorAll('[data-spark-draw]'), function (wrap) {
192      var svg = wrap.querySelector('svg');
193      var poly = svg && svg.querySelector('polyline');
194      var reveal = wrap.querySelector('.sparkdraw-reveal');
195      /* the curtain's inner layer: the 3c slide keeps its hologram rule, the
196         bento carries a plain slate-900 masked line - both counter-translate
197         the same way */
198      var foil = reveal && reveal.firstElementChild;
199      var dot = wrap.querySelector('circle');
200      if (!poly || !reveal || !foil || !dot) return;
201      var pts = (poly.getAttribute('points') || '').trim().split(/\s+/).map(function (p) {
202        var a = p.split(',');
203        return [parseFloat(a[0]), parseFloat(a[1])];
204      }).filter(function (p) { return !isNaN(p[0]) && !isNaN(p[1]); });
205      if (pts.length < 2) return;
206      var vb = (svg.getAttribute('viewBox') || '0 0 320 96').split(/\s+/);
207      var vbW = parseFloat(vb[2]) || 320;
208      var first = pts[0], last = pts[pts.length - 1];
209      function yAt(x) {
210        if (x <= pts[0][0]) return pts[0][1];
211        for (var i = 1; i < pts.length; i++) {
212          if (x <= pts[i][0]) {
213            var a = pts[i - 1], b = pts[i];
214            var t = (x - a[0]) / ((b[0] - a[0]) || 1);
215            return a[1] + (b[1] - a[1]) * t;
216          }
217        }
218        return pts[pts.length - 1][1];
219      }
220      var io = new IntersectionObserver(function (en) {
221        if (!en[0].isIntersecting) return;
222        io.disconnect();
223        /* dot to the line's start before the first frame */
224        dot.setAttribute('cx', first[0]);
225        dot.setAttribute('cy', first[1]);
226        var t0 = null;
227        function frame(ts) {
228          if (t0 === null) t0 = ts;
229          var p = Math.min(1, (ts - t0) / DUR);
230          var r = ease(p);
231          var off = (1 - r) * 101;
232          reveal.style.transform = 'translateX(-' + off.toFixed(3) + '%)';
233          foil.style.transform = 'translateX(' + off.toFixed(3) + '%)';
234          /* the reveal edge in viewBox units; the dot clamps to the line */
235          var x = Math.max(first[0], Math.min(r * vbW, last[0]));
236          dot.setAttribute('cx', x.toFixed(1));
237          dot.setAttribute('cy', yAt(x).toFixed(1));
238          if (p < 1) { window.requestAnimationFrame(frame); }
239          else {
240            dot.setAttribute('cx', last[0]);
241            dot.setAttribute('cy', last[1]);
242          }
243        }
244        window.requestAnimationFrame(frame);
245      }, { threshold: 0.5 });
246      io.observe(wrap);
247    });
248  })();
249
250  /* ---------- 3 · Financial Risk Retired counter ---------- */
251  (function () {
252    var numEl = document.getElementById('frrNum');
253    var assetsEl = document.getElementById('frrAssets');
254    if (!numEl || !assetsEl) return;
255
256    var V0 = 1.4, V1 = 62.1;           /* dollars, in millions */
257    var DUR = 210000;                  /* ~210s per pass, then loop */
258    var PAINT_MS = 140;                /* ≤ ~7 text paints per second */
259    var ASSETS = 194755;               /* parked, never counts with the dollars */
260
261    function fmtM(v) { return v.toFixed(1) + 'M'; }
262    function fmtA(v) { return Math.round(v).toLocaleString('en-US'); }
263
264    if (reduce) { /* static end state */
265      numEl.textContent = fmtM(V1);
266      assetsEl.textContent = fmtA(ASSETS);
267      return;
268    }
269
270    /* --- the dollar steel counter: $1.4M → $62.1M, linear, loops --- */
271    var elapsed = 0, lastTs = null, lastPaint = -Infinity, raf = null;
272
273    function paint() {
274      var p = (elapsed % DUR) / DUR;   /* linear, loops back to $1.4M */
275      numEl.textContent = fmtM(V0 + (V1 - V0) * p);
276    }
277    function frame(ts) {
278      if (lastTs !== null) elapsed += ts - lastTs;
279      lastTs = ts;
280      if (ts - lastPaint >= PAINT_MS) { lastPaint = ts; paint(); }
281      raf = window.requestAnimationFrame(frame);
282    }
283
284    /* --- the assets figure: parked, occasional +100–150 bump, eased --- */
285    var assets = ASSETS, bumpTimer = null, bumpRaf = null;
286    assetsEl.textContent = fmtA(assets);
287
288    function easeOut(t) { return 1 - Math.pow(1 - t, 3); }
289    function scheduleBump() {
290      if (bumpTimer !== null) return;
291      bumpTimer = window.setTimeout(function () {
292        bumpTimer = null;
293        bump();
294      }, 6000 + Math.random() * 4000);   /* every 6–10s */
295    }
296    function bump() {
297      var from = assets;
298      var delta = 100 + Math.round(Math.random() * 50);   /* +100–150 */
299      var t0 = null;
300      assets = from + delta;
301      function step(ts) {
302        if (t0 === null) t0 = ts;
303        var p = Math.min(1, (ts - t0) / 600);              /* small ease, ~0.6s */
304        assetsEl.textContent = fmtA(from + delta * easeOut(p));
305        if (p < 1) { bumpRaf = window.requestAnimationFrame(step); }
306        else { bumpRaf = null; scheduleBump(); }
307      }
308      bumpRaf = window.requestAnimationFrame(step);
309    }
310
311    function start() {
312      if (raf === null) {
313        lastTs = null;                 /* don't count paused time */
314        raf = window.requestAnimationFrame(frame);
315      }
316      assetsEl.textContent = fmtA(assets);   /* settle any bump cut short by stop() */
317      scheduleBump();
318    }
319    function stop() {
320      if (raf !== null) { window.cancelAnimationFrame(raf); raf = null; }
321      if (bumpTimer !== null) { window.clearTimeout(bumpTimer); bumpTimer = null; }
322      if (bumpRaf !== null) { window.cancelAnimationFrame(bumpRaf); bumpRaf = null; }
323    }
324
325    if (hasIO) {
326      var card = numEl.closest('.home-frr') || numEl;
327      new IntersectionObserver(function (entries) {
328        entries.forEach(function (en) {
329          if (en.isIntersecting) start(); else stop();
330        });
331      }).observe(card);
332    } else {
333      start();
334    }
335  })();
336
337})();
338
339/* ==========================================================================
340   The map-claim beats (SHARED - home.js loads on the home AND surface pages, so every .sfmap beats slide on either page rides this one driver): three lines,
341   each on its own screen; crossfades are inline opacity on the phase
342   WRAPPERS only. Beat windows use smoothstep; the last beat keeps its
343   end state. data-stops on the section mirrors the settled fractions.
344   ========================================================================== */
345(function () {
346  var reduce = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
347  if (reduce || !('IntersectionObserver' in window)) return;
348  var secs = Array.prototype.slice.call(document.querySelectorAll('.sfmap'));
349  if (!secs.length) return;
350  document.body.classList.add('sf-scrub');
351
352  function ss(t) { t = Math.min(1, Math.max(0, t)); return t * t * (3 - 2 * t); }
353  function win(q, a, b, w) {
354    var o = ss((q - a) / w);
355    if (b !== null) o *= 1 - ss((q - b) / w);
356    return o;
357  }
358
359  var stages = secs.map(function (sec) {
360    return { el: sec, phases: Array.prototype.slice.call(sec.querySelectorAll('.sfmap__phase')), near: false, last: [] };
361  });
362
363  function paint(st) {
364    var vh = window.innerHeight || 1;
365    var r = st.el.getBoundingClientRect();
366    var range = r.height - vh;
367    var q = range > 0 ? (-r.top) / range : 1;
368    q = Math.min(1, Math.max(0, q));
369    /* beat windows derive from the phase count: n beats split the scroll
370       evenly, each eases out 0.08 before the next eases in; the last beat
371       keeps its end state */
372    var n = st.phases.length || 1;
373    var step = 1.08 / n;
374    var w = st.phases.map(function (_, i) {
375      var a = i * step - 0.08;
376      var b = (i === n - 1) ? null : (i + 1) * step - 0.16;
377      return win(q, a, b, 0.09);
378    });
379    st.phases.forEach(function (ph, i) {
380      if (Math.abs((st.last[i] || 0) - w[i]) > 0.001) { st.last[i] = w[i]; ph.style.opacity = w[i]; ph.style.pointerEvents = w[i] > 0.5 ? 'auto' : 'none'; /* EV-FIREPOP-PHASE */ }
381    });
382  }
383
384  var raf = null;
385  function frame() {
386    raf = null;
387    var again = false;
388    stages.forEach(function (st) { if (st.near) { paint(st); again = true; } });
389    if (again) raf = requestAnimationFrame(frame);
390  }
391  function kick() { if (!raf) raf = requestAnimationFrame(frame); }
392  var io = new IntersectionObserver(function (entries) {
393    entries.forEach(function (en) {
394      var st = stages.filter(function (s) { return s.el === en.target; })[0];
395      if (st) st.near = en.isIntersecting;
396    });
397    kick();
398  }, { rootMargin: '20% 0px 20% 0px' });
399  stages.forEach(function (st) { io.observe(st.el); paint(st); });
400  window.addEventListener('scroll', kick, { passive: true });
401})();
402
403/* ==========================================================================
404   THE BACKLOG STORY (home-prob) - custom pinned scrub stage. Six text
405   beats over a persistent table layer: build (header, then rows one by
406   one), the two medium-severity rows blink red, the zoom-out with rows
407   racing past the scroll (accelerating), and the closing line with more
408   red frames. Wrapper-only opacity + transform; the table DOM is built
409   once and only classes/level values change per frame.
410   ========================================================================== */
411(function () {
412  'use strict';
413  var reduce = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
414  if (reduce || !('IntersectionObserver' in window)) return;
415  var sec = document.querySelector('.home-prob');
416  if (!sec) return;
417  document.body.classList.add('sf-scrub');   /* shares the beats' armed class */
418
419  var phases = Array.prototype.slice.call(sec.querySelectorAll('.home-prob__phase'));
420  var wrapEl = sec.querySelector('.home-prob__tablewrap');
421  var tableEl = sec.querySelector('.home-prob__table');
422  var headEl = sec.querySelector('.home-prob__thead');
423  var trackEl = sec.querySelector('.home-prob__track');
424  var rowsAll = Array.prototype.slice.call(sec.querySelectorAll('.home-prob__row'));
425  var baseCount = rowsAll.length / 3;                    /* track holds the set x3 for the loop */
426  var baseRows = rowsAll.slice(0, baseCount);
427  var hlA = Array.prototype.slice.call(sec.querySelectorAll('.hp-hl-a')).slice(0, 2);
428  var hlB = Array.prototype.slice.call(sec.querySelectorAll('.hp-hl-b'));
429
430  function ss(t) { t = Math.min(1, Math.max(0, t)); return t * t * (3 - 2 * t); }
431  function win(q, a, b, w) {
432    var o = ss((q - a) / w);
433    if (b !== null) o *= 1 - ss((q - b) / w);
434    return o;
435  }
436
437  var last = {};
438  function setO(key, el, v) {
439    if (Math.abs((last[key] || 0) - v) < 0.001) return;
440    last[key] = v;
441    el.style.opacity = v;
442  }
443  function setCls(key, list, on) {
444    if (last[key] === on) return;
445    last[key] = on;
446    list.forEach(function (el) { el.classList.toggle('hl-on', on); });
447  }
448
449  var maskEl = sec.querySelector('.home-prob__mask');
450  function paint() {
451    var vh = window.innerHeight || 1;
452    var vw = window.innerWidth || 1;
453    var r = sec.getBoundingClientRect();
454    var range = r.height - vh;
455    var qRaw = range > 0 ? (-r.top) / range : 1;   /* UNclamped: the race keeps counting past the stage */
456    var q = Math.min(1, Math.max(0, qRaw));
457
458    /* text beats - "It never will." holds a long, slow window (Aaron:
459       it was a blink); the last beat keeps its end state */
460    var w = [
461      win(q, -0.05, 0.10, 0.05),
462      win(q, 0.13, 0.30, 0.08),
463      win(q, 0.34, 0.48, 0.05),
464      win(q, 0.52, 0.64, 0.05),
465      win(q, 0.68, 0.80, 0.05),
466      win(q, 0.84, null, 0.05)
467    ];
468    phases.forEach(function (ph, i) { setO('p' + i, ph, w[i]); });
469
470    /* table: fades in with beat 2 and stays; during the race it dims a
471       step further as it becomes pure background texture */
472    var z = ss((q - 0.68) / 0.10);
473    setO('tw', wrapEl, (0.55 - 0.17 * z) * ss((q - 0.335) / 0.03));
474
475    /* build: header first, then one row per step */
476    var built = Math.max(0, Math.floor((q - 0.35) / 0.008));
477    if (last.built !== built) {
478      last.built = built;
479      headEl.classList.toggle('on', built > 0);
480      rowsAll.forEach(function (row, i) { row.classList.toggle('on', i < built - 1); });
481    }
482
483    /* highlights: set A with beat 3, set B with the last beat */
484    setCls('hla', hlA, q >= 0.52);
485    setCls('hlb', hlB, q >= 0.84);
486
487    /* the takeover (beat 4 onward): the table's BOX grows to the full
488       viewport - width to 100vw, top to 0, mask to 100vh, corners
489       squared - while the glyph scale barely moves (1 -> 1.08), so the
490       rows keep reading as a long list edge to edge, top to bottom
491       (Aaron round 4: the earlier version blew the table out of the
492       screen). The race rides UNclamped progress and keeps scrolling
493       into the next slide. */
494    var scale = 1 + 0.08 * z;
495    if (Math.abs((last.sc || 1) - scale) > 0.002) {
496      last.sc = scale;
497      tableEl.style.transform = 'scale(' + scale + ')';
498    }
499    if (z > 0.001) {
500      var baseW = Math.min(1080, vw * 0.92);
501      var w = Math.round(baseW + (vw - baseW) * z);
502      var top = Math.round(vh * 0.54 * (1 - z));
503      var mh = Math.round(432 + (vh - 432) * z);
504      var rad = Math.round(16 * (1 - z));
505      if (last.bw !== w) { last.bw = w; wrapEl.style.width = w + 'px'; }
506      if (last.bt !== top) { last.bt = top; wrapEl.style.top = top + 'px'; }
507      if (last.mh !== mh) { last.mh = mh; maskEl.style.maxHeight = mh + 'px'; }
508      if (last.rad !== rad) { last.rad = rad; tableEl.style.borderRadius = rad + 'px'; }
509    } else if (last.bw !== undefined) {
510      last.bw = last.bt = last.mh = last.rad = undefined;
511      wrapEl.style.width = '';
512      wrapEl.style.top = '';
513      maskEl.style.maxHeight = '';
514      tableEl.style.borderRadius = '';
515    }
516    var race = Math.max(0, (qRaw - 0.68) / 0.32);
517    var setH = trackEl.scrollHeight / 3;
518    var y = (race * race) * setH * 6;                     /* accelerating; unbounded past the stage */
519    y = setH > 0 ? (y % setH) : 0;
520    if (Math.abs((last.y || 0) - y) > 0.5) {
521      last.y = y;
522      trackEl.style.transform = 'translateY(' + (-y) + 'px)';
523    }
524  }
525
526  var raf = null, near = false;
527  function frame() { raf = null; if (near) { paint(); raf = requestAnimationFrame(frame); } }
528  function kick() { if (!raf) raf = requestAnimationFrame(frame); }
529  var io = new IntersectionObserver(function (entries) {
530    entries.forEach(function (en) { near = en.isIntersecting; });
531    kick();
532  }, { rootMargin: '20% 0px 20% 0px' });
533  io.observe(sec);
534  window.addEventListener('scroll', kick, { passive: true });
535  paint();
536})();
537
538/* ==========================================================================
539   FOUNDER FILM lightbox (nav-home restructure): the watch button opens
540   the unmuted player; Esc / backdrop / the X close it and pause playback.
541   Reduced motion also stops the background loop.
542   ========================================================================== */
543(function () {
544  'use strict';
545  var sec = document.querySelector('.home-video');
546  if (!sec) return;
547  var reduce = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
548  var bg = sec.querySelector('.home-video__bg');
549  if (reduce && bg) { try { bg.pause(); } catch (e) {} }
550
551  var box = sec.querySelector('[data-video-lightbox]');
552  var player = box && box.querySelector('.home-video__player');
553  var openBtn = sec.querySelector('[data-video-open]');
554  if (!box || !player || !openBtn) return;
555
556  function open() {
557    box.hidden = false;
558    if (bg) { try { bg.pause(); } catch (e) {} }
559    try { player.currentTime = 0; player.play(); } catch (e) {}
560    sec.querySelector('.home-video__close').focus();
561  }
562  function close() {
563    try { player.pause(); } catch (e) {}
564    box.hidden = true;
565    if (bg && !reduce) { try { bg.play(); } catch (e) {} }
566    openBtn.focus();
567  }
568  openBtn.addEventListener('click', open);
569  box.querySelector('[data-video-close]').addEventListener('click', close);
570  box.addEventListener('click', function (e) { if (e.target === box) close(); });
571  document.addEventListener('keydown', function (e) { if (e.key === 'Escape' && !box.hidden) close(); });
572})();
573
574(function () {
575  'use strict';
576  var reduce = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
577  if (reduce) return;
578  var vids = [].slice.call(document.querySelectorAll('video[data-autoplay="inview"]'));
579  if (!vids.length) return;
580  var ARM_EVENTS = ['pointermove', 'pointerdown', 'touchstart', 'wheel', 'scroll', 'keydown'];
581  var hasIO = 'IntersectionObserver' in window;
582  var armed = false;
583  var visible = [];
584
585  function inView(v) { var r = v.getBoundingClientRect(); return r.bottom > 0 && r.top < window.innerHeight; }
586  function start(v) {
587    if (!v.paused) return;
588    v.muted = true;
589    var p = v.play();
590    if (p && p.catch) p.catch(function () {});
591  }
592  function startVisible() {
593    if (!armed) return;
594    if (!hasIO) visible = vids.filter(inView);
595    visible.forEach(start);
596  }
597  function arm() {
598    if (armed) return;
599    armed = true;
600    ARM_EVENTS.forEach(function (t) { window.removeEventListener(t, arm, true); });
601    startVisible();
602  }
603
604  if (hasIO) {
605    var io = new IntersectionObserver(function (entries) {
606      entries.forEach(function (e) {
607        var i = visible.indexOf(e.target);
608        if (e.isIntersecting) {
609          if (i === -1) visible.push(e.target);
610          if (armed) start(e.target);
611        } else if (i !== -1) {
612          visible.splice(i, 1);
613          try { e.target.pause(); } catch (err) {}
614        }
615      });
616    }, { threshold: 0.1 });
617    vids.forEach(function (v) { io.observe(v); });
618  }
619  ARM_EVENTS.forEach(function (t) { window.addEventListener(t, arm, { capture: true, passive: true }); });
620  document.addEventListener('visibilitychange', function () { if (!document.hidden) startVisible(); });
621  window.addEventListener('pageshow', startVisible);
622})();

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.