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.