1/** 2 * PRELOADER v4 â minimal instant-text preloader. 3 * Sequence: 4 * 1. #preloader-text is visible in the markup instantly (no JS wait). 5 * 2. Soft fade-in (opts into a CSS transition, then triggers it a frame 6 * later so it actually animates instead of snapping). 7 * 3. Short hold, then text + the shared #particles-load-scrim 8 * (particle-morph.hbs) fade out together; preloader:done dispatched 9 * once fully hidden â same contract every prior version used. 10 * See partials/preloader.hbs for the full design note. 11 */ 12 13(function () { 14 'use strict'; 15 16 // Shared "at least one full cycle" gate for the indeterminate loading bar 17 // (main.css .progress-bar-track â the single-streak grow/collapse 18 // sequence, shared by #preloader-progress-bar here AND .scroll-progress 19 // during regular page navigation, see page-transition.js). Defined here, 20 // ABOVE this file's own early-return below, specifically so it's 21 // registered on EVERY page â page-transition.js loads after this file 22 // (default.hbs script order) and needs this on every navigation, not just 23 // the homepage. Without a minimum hold, a fast page load or navigation 24 // can call the "done" trigger before the bar has completed even one 25 // visible sequence â reads as a flicker, not a loading indicator. 26 // â MUST be >= bar-grow's animation-duration in main.css (currently 27 // 0.9s). That is not just cosmetic pacing: the .is-complete exit assumes 28 // it starts from a settled scaleX(1), and this gate is the only thing 29 // guaranteeing the one-shot grow has actually finished before the exit 30 // can be applied. Drop it below the grow duration and a fast load will 31 // cut the grow off mid-way and jump. Update both together. 32 window.__BAR_CYCLE_MS = 900; 33 window.__barMinCycleRelease = function (startTime, release) { 34 var elapsed = Date.now() - startTime; 35 var remaining = window.__BAR_CYCLE_MS - elapsed; 36 if (remaining <= 0) { release(); return; } 37 setTimeout(release, remaining); 38 }; 39 40 // Only run on homepage (preloader element present) 41 if (!document.getElementById('preloader')) return; 42 43 // Skip path: arrived via an in-site click (e.g. the logo), not a fresh 44 // landing/refresh. page-transition.js navigates via window.location.href 45 // (a real page load, not a client-side route swap), so document.referrer 46 // reliably reflects the previous page when it's same-origin. Replaces 47 // the old "once per localStorage session" check (see commented-out 48 // block below) â that flag stopped being read by the anti-flash script 49 // in default.hbs's <head> too; keep both in sync if this logic changes. 50 // 51 // isReload override (2026-08-06): document.referrer alone can't tell a 52 // refresh apart from an in-site click earlier in this tab's history â a 53 // browser reload PRESERVES the referrer from the page's original 54 // navigation, it doesn't clear it. So once you'd reached this page via 55 // an internal link even once, cameFromSameSite stayed true on every 56 // later refresh of that tab, permanently skipping the preloader (bug 57 // report: the loading bar never showed on refresh). Navigation Timing's 58 // own `type` is the correct signal for "was this specific request a 59 // reload" â checked first and allowed to override the referrer 60 // heuristic. Same override added to default.hbs's two copies of this 61 // check; keep all three in sync. 62 let isReload = false; 63 try { 64 const navEntry = performance.getEntriesByType && performance.getEntriesByType('navigation')[0]; 65 isReload = navEntry 66 ? navEntry.type === 'reload' 67 : !!(performance.navigation && performance.navigation.type === performance.navigation.TYPE_RELOAD); 68 } catch (e) { 69 isReload = false; 70 } 71 72 let cameFromSameSite = false; 73 try { 74 cameFromSameSite = !isReload && !!document.referrer && new URL(document.referrer).origin === window.location.origin; 75 } catch (e) { 76 cameFromSameSite = false; 77 } 78 79 if (cameFromSameSite) { 80 console.log('[preloader] SKIP PATH â arrived via in-site navigation (referrer: ' + document.referrer + '), skipping animation'); 81 const el = document.getElementById('preloader'); 82 if (el) el.style.display = 'none'; 83 // Reveal #particles-load-scrim HERE too, not just #preloader. 84 // 85 // The scrim is markup rendered by {{#is "index"}} â a TEMPLATE-level 86 // guard evaluated server-side on every request to "/", with no way to 87 // distinguish a fresh landing from a curtain-return (page-transition.js's 88 // runCurtainExit ends in a real `location.href` navigation, same as 89 // typing the URL). So the scrim exists and starts at its base 90 // opacity:1 on this path exactly as much as on a fresh load â but 91 // until this fix, only the FULL-RUN branch below (_finish(), later in 92 // this file) ever revealed it. This skip-path predates the scrim 93 // entirely and never touched it. 94 // 95 // Left alone, the scrim stayed opaque (z-index 999998, above .home/ 96 // .hero but still below nav's 10001) until a much slower FALLBACK in 97 // particle-morph.hbs (~line 725, gated on `!window.__preloaderRunning`) 98 // happened to fire once the particle system's own async THREE.js/GLB 99 // boot chain finished â that gate's own comment wrongly assumed it 100 // never runs on pages with #preloader (i.e. the homepage); this 101 // cameFromSameSite branch proves it does, since __preloaderRunning is 102 // never set on this path either. Reported symptom matched exactly: 103 // nav genuinely painting at opacity:1/visible per computed style, but 104 // invisible on screen â confirmed live via screenshot, a fully opaque 105 // frame with nothing on it â until that fallback eventually cleared 106 // the scrim, sometimes noticeably late (post-page boot is slower than 107 // About's), reading as "not visible, then fades in." 108 const scrim = document.getElementById('particles-load-scrim'); 109 if (scrim) { 110 scrim.classList.add('is-revealed'); 111 scrim.style.opacity = '0'; 112 } 113 document.documentElement.classList.remove('preloading'); 114 document.documentElement.classList.add('page-ready'); 115 window.__preloaderSkipped = true; 116 setTimeout(() => { 117 console.log('[preloader] preloader:done dispatched (skip path)'); 118 // Sticky flag â see particle-animation-loop.js's constructor, which 119 // checks this before deciding whether to even wait for the event.
120 // Without it, late subscribers (the particle system's own async 121 // bootstrap chain finishes well after this fires) miss the event 122 // entirely and fall back to a 20s timer for bloom to ever turn on. 123 window.__preloaderDoneFired = true; 124 window.dispatchEvent(new CustomEvent('preloader:done')); 125 }, 0); 126 return; 127 } 128 console.log('[preloader] FULL RUN â showing preloader (fresh landing/refresh)'); 129 window.__preloaderRunning = true; 130 131 /* Show preloader only once per session 132 if (localStorage.getItem('preloader_seen')) { 133 console.log('[preloader] SKIP PATH â preloader_seen in localStorage, skipping animation'); 134 const el = document.getElementById('preloader'); 135 if (el) el.style.display = 'none'; 136 document.documentElement.classList.add('page-ready'); 137 window.__preloaderSkipped = true; 138 setTimeout(() => { 139 console.log('[preloader] preloader:done dispatched (skip path)'); 140 window.dispatchEvent(new CustomEvent('preloader:done')); 141 }, 0); 142 return; 143 } 144 console.log('[preloader] FULL RUN â showing preloader'); 145 localStorage.setItem('preloader_seen', '1'); 146 window.__preloaderRunning = true; 147*/ 148 // Ensure nav is hidden during preloader 149 document.documentElement.classList.add('preloading'); 150 151 // Reveal page when preloader finishes (preloading class already on html from head script) 152 window.addEventListener('preloader:done', () => { 153 // Keep preloading class until nav is ready to show (page-ready added) 154 // Nav fades in after hero entrance completes, not immediately 155 const heroEl = document.querySelector('.intro'); 156 let _navShown = false; 157 const addPageReady = () => { 158 if (_navShown) return; 159 _navShown = true; 160 document.documentElement.classList.remove('preloading'); 161 document.documentElement.classList.add('page-ready'); 162 }; 163 if (heroEl) { 164 heroEl.addEventListener('heroEntranceDone', addPageReady, { once: true }); 165 setTimeout(addPageReady, 1200); // fallback if event doesn't fire 166 } else { 167 setTimeout(addPageReady, 800); 168 } 169 }, { once: true }); 170 171 // âââ Minimal text preloader âââââââââââââââââââââââââââââââââââââââââââââ 172 // Sequence: text is visible instantly in the markup (no opacity:0 default 173 // â see main.css's comment on .preloader-text), then this adds the 174 // fade-in class + triggers it a frame later so the CSS transition actually 175 // runs instead of snapping. HOLD_MS after that, fade the text and the 176 // shared #particles-load-scrim (particle-morph.hbs) out TOGETHER, then 177 // _hide() the preloader shell and dispatch preloader:done â same as every 178 // prior version, so the 5 files that key off __preloaderSkipped / 179 // __preloaderDoneFired / 'preloader:done' keep working unchanged. 180 // Nominal fade-in length, used only by the failsafe timer below. The 181 // REAL entrance is `preloaderFadeIn 0.4s ease-out` on #preloader 182 // (main.css) â the .pl-text-fade-in transition this used to name never 183 // actually produces a visible fade, since the container animating is 184 // what the viewer sees. Kept at 500 as a deliberately generous margin, 185 // not as a value that must match anything. 186 const FADE_IN_MS = 500; 187 // FAILSAFE MARGIN ONLY â no longer a deliberate hold. The scrim exit is 188 // triggered by the entrance animation actually completing (see the 189 // constructor's getAnimations check); this extra slack exists purely so 190 // the backup timer can't beat that to the punch on a normal load, and 191 // only takes over if the animation never resolves (reduced-motion 192 // zeroing the duration, backgrounded tab). 193 const HOLD_MS = 500; 194 // DELIBERATE dwell (unlike HOLD_MS above, which is failsafe slack): how 195 // long "Opportunity lives in complexity" stays on screen AFTER its 196 // entrance animation has finished, before the scrim starts its exit. 197 // Added 2026-09-24 per explicit request to let the line be read rather 198 // than glimpsed. Applies to the real path (the getAnimations/finished 199 // branch in the constructor) â the failsafe timer below adds it too, so 200 // neither route can cut the dwell short. 201 const TEXT_DWELL_MS = 1000; 202 // Longer than the 0.4s entrance ON PURPOSE. An exit that matches its 203 // entrance beat-for-beat reads as clipped, because it is competing with 204 // the scrim's own 600ms slide for the viewer's attention rather than 205 // receding behind it; stretching it past both the entrance and the slide 206 // lets the text recede as the scrim carries it away instead of blinking 207 // out mid-movement. Paired with power2.out below (mirroring the 208 // entrance's ease-out), this is the "smoother, longer, matching" exit. 209 const FADE_OUT_MS = 800; 210 const SCRIM_SLIDE_MS = 600; // MUST match .particles-load-scrim's transition-duration (main.css) 211 212 class Preloader { 213 constructor() { 214 this.preloader = document.getElementById('preloader'); 215 this.text = document.getElementById('preloader-text'); 216 this.progressBar = document.getElementById('preloader-progress-bar'); 217 this.scrim = document.getElementById('particles-load-scrim'); 218 219 if (!this.preloader || !this.text) return; 220 221 // Bar becomes visible the instant this constructor runs (CSS handles 222 // the chase from its own 0% keyframe) â anchors __barMinCycleRelease's 223 // "at least one full cycle" gate in _finish() below. 224 this._barStartTime = Date.now(); 225 226 // Text is already opacity:1 in its base CSS state (renders instantly, 227 // no JS dependency). Opting it INTO the fade-in transition here, then 228 // flipping .is-visible on the next frame, is what makes it visibly
229 // soft-fade rather than being present from t=0 with no motion at all. 230 this.text.classList.add('pl-text-fade-in'); 231 requestAnimationFrame(() => { 232 requestAnimationFrame(() => { 233 this.text.classList.add('is-visible'); 234 }); 235 }); 236 237 // The scrim's exit is triggered by the TEXT'S OWN fade-in finishing â 238 // driven off the real `transitionend`, not a timer that merely hopes 239 // to match .pl-text-fade-in's CSS duration (main.css). Keying off the 240 // actual event means the two stay in sync automatically if that 241 // duration is ever retuned in CSS, instead of silently drifting apart 242 // the way a hardcoded FADE_IN_MS constant would. 243 // 244 // Guarded on propertyName: this element only transitions opacity 245 // today, but a future transition on any other property would 246 // otherwise fire this early. 247 // 248 // The timer below is now a FAILSAFE, not the mechanism â transitionend 249 // legitimately never fires in several cases (a zeroed duration under 250 // prefers-reduced-motion, the element being display:none'd mid-fade, 251 // some backgrounded-tab conditions), and the preloader must never be 252 // able to strand the page behind a scrim that has no other way to 253 // leave. Both paths funnel into the same _finishing-guarded _finish(), 254 // so whichever wins the race, the other is a no-op. 255 // NOTE the target: #preloader's own `preloaderFadeIn` ANIMATION 256 // (main.css ~607, 0.4s ease-out forwards) â NOT the text's 257 // .pl-text-fade-in transition. Traced live: the text's computed 258 // opacity is 1 from first paint and never transitions, because the 259 // CONTAINER is what actually fades (preloaderOp climbing 0 -> 1 while 260 // textOp stayed pinned at 1). The text's own transition only ever 261 // fires on the way OUT, when _finish()'s GSAP tween writes an inline 262 // opacity that finally beats the class rule. So the visible 263 // "Opportunity lives in complexity" fade-in the viewer sees is this 264 // animation, and animationend on it is the correct "the text has 265 // finished appearing" signal. 266 // ALREADY-FINISHED CHECK comes first, and is the common case â not an 267 // edge case. preloaderFadeIn is a CSS animation that starts at first 268 // paint, while this constructor runs on DOMContentLoaded; on a normal 269 // load the 0.4s animation is long over by then (traced: animation 270 // ends 586ms, constructor runs 726ms, with getAnimations() already 271 // reporting playState "finished"). Attaching a listener alone would 272 // therefore wait forever and leave the failsafe timer to do all the 273 // work â exactly the 3.1s desync this replaced. Querying the live 274 // animation state instead of trusting the event to still be coming is 275 // what makes this correct regardless of which side of the race the 276 // constructor lands on. 277 const fadeInAnim = this.preloader.getAnimations 278 ? this.preloader.getAnimations().find(a => a.animationName === 'preloaderFadeIn') 279 : null; 280 281 // Both branches below go through this rather than calling _finish() 282 // directly, so the dwell applies however the entrance resolved. 283 const finishAfterDwell = () => { 284 this._dwellTimer = setTimeout(() => this._finish(), TEXT_DWELL_MS); 285 }; 286 287 if (!fadeInAnim || fadeInAnim.playState === 'finished') { 288 // Fade-in already done (or no such animation at all â reduced 289 // motion, unsupported getAnimations): the text has finished 290 // appearing, so the dwell starts now. 291 finishAfterDwell(); 292 } else { 293 // Still running â wait for it to land. `finished` is a promise on 294 // the Animation object itself, which (unlike an animationend 295 // listener) resolves correctly even if the animation completes 296 // between this check and the next tick. 297 fadeInAnim.finished.then(finishAfterDwell).catch(() => {}); 298 } 299 300 // Failsafe in BOTH branches â the promise above can reject (animation 301 // cancelled) or, on a stalled/backgrounded tab, simply never settle, 302 // and the page must never be left behind a scrim with no other way 303 // out. Harmless when the normal path wins: _finish() is guarded by 304 // its own _finishing flag, so whichever fires second is a no-op. 305 // + TEXT_DWELL_MS so the failsafe stays a failsafe: without it this 306 // would fire before the deliberate dwell above had elapsed and cut 307 // the line short on every normal load, making the backup path the 308 // effective one again. 309 this._holdTimer = setTimeout(() => this._finish(), FADE_IN_MS + HOLD_MS + TEXT_DWELL_MS); 310 } 311 312 _finish() { 313 if (this._finishing) return; 314 this._finishing = true; 315 316 // preloader:done dispatched HERE â at the moment the scrim starts its
317 // slide â not from _hide() 500ms later as before. That gap is what 318 // caused hero content set static by scroll-scrub-anim.js's 319 // isFreshPreloaderRun branch (entranceTl.progress(1), gated on this 320 // exact event) to only become visible ~700ms into the scrim's 321 // slide-away, well AFTER the transform had finished uncovering the 322 // hero â measured: scrim fully off-screen at 6275ms, hero content 323 // still opacity:0 until 6343ms. The scrim was sliding over nothing, 324 // then the hero popped in afterward â the opposite of the intended 325 // "scrim reveals already-visible content" effect. 326 // 327 // Every one of this event's other 3 listeners (main.js's hero-image 328 // wait + entrance chain, particle-animation-loop.js's bloom composer 329 // init, and this file's own nav-reveal listener below) only cares 330 // about "is the hero/particle system ready to be acted on", not 331 // "has the preloader visually finished fading" â firing ~500ms 332 // earlier makes the signal MORE accurate to what it's named, not 333 // less. _hide() still runs on its own timing afterward (shell 334 // display:none, scrim pointer-events) â it just no longer owns the 335 // event dispatch. 336 window.__preloaderDoneFired = true; // sticky flag â see other dispatch points' comment 337 window.dispatchEvent(new CustomEvent('preloader:done')); 338 339 // Stops the CSS chase and holds a full bright bar for this handoff 340 // beat (main.css .progress-bar-track.is-complete), gated through 341 // __barMinCycleRelease so the chase always completes at least one 342 // full loop before stopping â same helper page-transition.js uses 343 // for .scroll-progress during regular navigation. 344 if (this.progressBar) { 345 window.__barMinCycleRelease(this._barStartTime, () => { 346 this.progressBar.classList.add('is-complete'); 347 }); 348 } 349 350 const safetyTimer = setTimeout(() => { this._hide(); }, 4000); 351 352 // Scrim: SLIDE, not fade. .particles-load-scrim.is-revealed (main.css) 353 // translates it up by its own 150vh height over SCRIM_SLIDE_MS â its 354 // own CSS transition, not GSAP, since the motion is a plain transform 355 // with nothing else to coordinate. Longer than FADE_OUT_MS (500ms) on 356 // purpose: the slide is the visible reveal now, so it gets the 357 // slower, more deliberate duration; text/preloader-shell still fade 358 // on the original faster beat alongside it. 359 // 360 // Opacity is written AFTER the slide finishes, not alongside it â 361 // opacity and transform are independent properties, so setting 362 // opacity:0 immediately made the whole element invisible at once 363 // regardless of the transform, which silently defeated the slide 364 // entirely (confirmed via a frame-by-frame trace: opacity read 0 on 365 // literally every sampled frame, including mid-slide at -925px). 366 // Every other consumer of this element (the fade-in trigger and 6s 367 // failsafe in particle-morph.hbs, __preloaderRunning's gate) only 368 // cares about the terminal "is this scrim gone" value, not when it's 369 // reached, so deferring it to match the real end of the transform is 370 // free â it just has to actually happen after, not before. 371 // The slide itself is GATED ON THE HERO BEING READY â it must never 372 // uncover a page whose content has not been put in place yet. 373 // 374 // This file now loads early in <body> (default.hbs, right after 375 // <main>), well before scroll-scrub-anim.js (~1799, behind three.js 376 // and GSAP). That early boot is what makes the fade-in -> slide 377 // handoff instant, but it also means that at this point the hero has 378 // very likely NOT been made visible yet: scroll-scrub-anim.js sets it 379 // static and then fires 'hero:ready'. Sliding regardless produced 380 // exactly the bug this whole sequence exists to avoid â the scrim 381 // wiping across an empty page, hero popping in ~260ms after the slide 382 // had already finished (traced: slide done 1641ms, hero visible 383 // 1902ms). 384 // 385 // __heroReady is checked first for the same late-subscriber reason 386 // preloader:done needs its own sticky flag: the hero may already be 387 // ready by the time we get here, and CustomEvents do not replay. 388 const startScrimSlide = () => { 389 if (!this.scrim || this._scrimSliding) return; 390 this._scrimSliding = true; 391 this.scrim.classList.add('is-revealed'); 392 setTimeout(() => { if (this.scrim) this.scrim.style.opacity = '0'; }, SCRIM_SLIDE_MS); 393 }; 394 395 if (this.scrim) { 396 if (window.__heroReady) { 397 startScrimSlide(); 398 } else { 399 window.addEventListener('hero:ready', startScrimSlide, { once: true }); 400 // Failsafe: the hero signal only exists on the fresh-preloader 401 // path in scroll-scrub-anim.js. If that file errors, is absent, 402 // or takes an unexpected branch, the scrim must still leave â 403 // this is a veil, and a veil that can outlive its trigger is 404 // strictly worse than no veil at all (LOADING.md §9). 405 setTimeout(startScrimSlide, 3000); 406 } 407 } 408 409 // Fades the CONTAINER only, not the text as well. 410 // 411 // #preloader-text is a child of #preloader, so a tween on each meant 412 // the text's effective opacity was the PRODUCT of the two (0.5 * 0.5 413 // = 0.25 at the midpoint, not 0.5) â it visibly vanished well ahead 414 // of the container it sits on, which is a large part of what read as 415 // an abrupt exit. One tween on the parent fades the whole preloader, 416 // text included, at a single honest rate. 417 // 418 // Easing MIRRORS the fade-in rather than opposing it: the entrance is 419 // `preloaderFadeIn 0.4s ease-out` (main.css), which decelerates into 420 // place; this was `power2.in`, which accelerates away â the two 421 // together gave a soft arrival and a sharp departure. power2.out is
422 // ease-out's GSAP equivalent, so both halves now ease the same way. 423 // RELEASE THE ENTRANCE ANIMATION FIRST â load-bearing, not cleanup. 424 // 425 // .preloader carries `animation: preloaderFadeIn 0.4s ease-out 426 // forwards` (main.css ~607). A finished animation with fill-mode 427 // `forwards` keeps applying its final keyframe (opacity: 1), and in 428 // the CSS cascade that beats an inline style â so every fade-out 429 // written here was silently ignored. Traced directly: GSAP's inline 430 // opacity descended correctly (0.963 -> 0.60 -> ...) while the 431 // COMPUTED opacity stayed pinned at 1 for the whole tween, and the 432 // preloader only disappeared when _hide() applied display:none â 433 // a hard cut, never a fade, no matter what duration or easing was 434 // set here. Clearing `animation` drops that forwards fill and lets 435 // the tween actually take effect. 436 // Opacity is pinned to 1 in the same tick the animation is dropped: 437 // .preloader's own base rule is `opacity: 0` (the pre-animation 438 // state), so removing the forwards fill without this would expose 439 // that 0 for a frame â the preloader blinking out instantly, which 440 // is the very thing being fixed. 441 this.preloader.style.opacity = '1';
442 this.preloader.style.animation = 'none'; 443 444 if (typeof gsap !== 'undefined') { 445 gsap.to(this.preloader, { 446 opacity: 0, 447 duration: FADE_OUT_MS / 1000, 448 ease: 'power2.out', 449 onComplete: () => { clearTimeout(safetyTimer); this._hide(); }, 450 }); 451 } else { 452 this.preloader.style.transition = 'opacity ' + (FADE_OUT_MS / 1000) + 's ease-out'; 453 this.preloader.style.opacity = '0'; 454 setTimeout(() => { clearTimeout(safetyTimer); this._hide(); }, FADE_OUT_MS); 455 } 456 } 457 458 _hide() { 459 if (this._hidden) return; 460 this._hidden = true; 461 if (this.preloader) { 462 this.preloader.style.cssText = 'display:none !important'; 463 } 464 // Scrim belongs to particle-morph.hbs, not this file. pointer-events 465 // is safe to force here immediately (the scrim is already inert, 466 // this just makes it explicit) â opacity is NOT: _hide() fires at 467 // FADE_OUT_MS (500ms), but the slide (_finish(), above) runs for 468 // SCRIM_SLIDE_MS (600ms) and only sets opacity:0 once that completes. 469 // Writing opacity:0 here too, 100ms earlier, would cut the slide 470 // short â the scrim vanishing via opacity before its transform has 471 // finished, instead of the transform being what makes it leave. 472 // _finish()'s own deferred write is the one guarantee here; nothing 473 // else needs to duplicate it. 474 if (this.scrim) { 475 this.scrim.style.pointerEvents = 'none'; 476 } 477 478 // preloader:done is now dispatched from _finish() (above, at the 479 // moment the scrim starts its slide) â NOT here. This function is 480 // purely cleanup (shell display:none, scrim pointer-events) by the 481 // time it runs; every listener has already acted on the event. 482 } 483 } 484 485 function boot() { new Preloader(); } 486 487 // Boot as soon as BOTH elements this needs actually exist, rather than 488 // waiting for DOMContentLoaded. 489 // 490 // Waiting for DOMContentLoaded used to be mandatory: #particles-load-scrim 491 // was declared in particle-morph.hbs, near the end of <body>, behind ~96 492 // blocking <script> tags (three.js, GSAP, string-tune, all external CDNs). 493 // That pushed the event â and so this constructor â to ~3959ms on a cold 494 // dev-server load, while the preloader's own fade-in had already finished 495 // at 2404ms. The text just sat there for ~1.5s of dead time before the 496 // scrim could even begin to leave. 497 // 498 // The scrim is now the first element in <body> (default.hbs) and 499 // #preloader comes from index.hbs's `{{> preloader}}` at the top of 500 // <main>, so by the time THIS file executes (default.hbs ~1369) both are 501 // already parsed and the whole wait disappears. The readyState fallback 502 // stays for safety: if either element is somehow missing at this point 503 // (markup reordered, partial not included), defer to DOMContentLoaded 504 // rather than construct against nulls â Preloader's own constructor also 505 // bails on a missing #preloader, so a genuinely absent element is still 506 // handled, just later. 507 if (document.getElementById('preloader') && document.getElementById('particles-load-scrim')) { 508 boot(); 509 } else if (document.readyState === 'loading') { 510 document.addEventListener('DOMContentLoaded', boot); 511 } else { 512 boot(); 513 } 514})();
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.