PageSourceSearch

https://premluczak.work/assets/js/preloader.js?v=KkMfWCQIHLux8_UP

js premluczak.work collected 2026-10-04 08:04:02 UTC 27,876 bytes, 514 lines download raw bytes

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.