diff --git a/pilot/arc.tpl.html b/pilot/arc.tpl.html
index c5cf0e4..a8bf644 100644
--- a/pilot/arc.tpl.html
+++ b/pilot/arc.tpl.html
@@ -921,17 +921,17 @@ body:not(.fx) .slab{display:none}
units.push({ sec, man, scene, flat, legend });
});
- // Reduced motion: leave the flat exploded hero in place (it IS the end state), no slabs, no scrub.
+ // Reduced motion / no GSAP: leave the flat exploded hero in place (it IS the end state).
if (RM || !window.gsap || !window.ScrollTrigger) return;
document.body.classList.add("fx");
gsap.registerPlugin(ScrollTrigger);
- const wc = (u, on) => u.slabs && u.slabs.forEach(el => el.style.willChange = on ? "transform" : "auto");
+ const wc = (u, on) => u.slabs.forEach(el => el.style.willChange = on ? "transform" : "auto");
- // Build a scene's slabs lazily, just before it approaches the viewport. The flat image stays the
- // fallback until the slabs are appended, so a scene that never initialises still reads correctly.
- const initScene = u => {
- if (u.slabs) return;
+ // Build every scene's slabs up front so pinned ScrollTriggers reserve their pin-spacing in the
+ // document flow from the start. Creating pins lazily reflows the page and floats a scene over the
+ // following section. The flat hero stays the fallback until its slabs exist.
+ units.forEach(u => {
u.slabs = u.man.parts.map(p => {
const d = document.createElement("div");
d.className = "slab";
@@ -941,6 +941,7 @@ body:not(.fx) .slab{display:none}
u.scene.appendChild(d);
return d;
});
+ u.flat.style.visibility = "hidden";
const lis = u.legend ? [...u.legend.children] : [];
const n = lis.length;
u.apply = t => {
@@ -954,42 +955,37 @@ body:not(.fx) .slab{display:none}
u.scene.style.transform = "scale(" + (0.95 + 0.05 * t).toFixed(4) + ")";
if (n) lis.forEach((li, i) => li.classList.toggle("lit", t >= 0.9 * ((i + 0.5) / n)));
};
- u.flat.style.visibility = "hidden";
-
- const desktop = document.documentElement.scrollHeight - innerHeight > 200 &&
- matchMedia("(min-width: 901px)").matches;
- if (desktop) {
- u.mode = "pin+scrub";
- ScrollTrigger.create({
- trigger: u.sec.querySelector(".pin"),
- start: "top top", end: "+=1800", pin: true, scrub: 0.6,
- onToggle: self => wc(u, self.isActive),
- onUpdate: self => u.apply(self.progress),
- onRefreshInit: () => u.apply(0)
- });
- u.apply(0);
- } else {
- u.mode = "assemble";
- u.sec.querySelector(".pin").style.minHeight = "0";
- u.apply(0); wc(u, true);
- gsap.to({ v: 0 }, { v: 1, duration: 1.6, ease: "power3.out",
- onUpdate: function () { u.apply(this.targets()[0].v); },
- onComplete: () => wc(u, false) });
- }
- ScrollTrigger.refresh();
- };
+ });
const boot = () => {
- // initialise each scene shortly before it reaches the viewport; keep the flat image until then
- const near = new IntersectionObserver((es, obs) => {
- es.forEach(e => {
- if (!e.isIntersecting) return;
- const u = units.find(x => x.sec === e.target);
- if (u) initScene(u);
- obs.unobserve(e.target);
- });
- }, { rootMargin: "400px 0px 400px 0px" });
- units.forEach(u => near.observe(u.sec));
+ const desktop = document.documentElement.scrollHeight - innerHeight > 200 &&
+ matchMedia("(min-width: 901px)").matches;
+ units.forEach(u => {
+ if (desktop) {
+ u.mode = "pin+scrub";
+ ScrollTrigger.create({
+ trigger: u.sec.querySelector(".pin"),
+ start: "top top", end: "+=1800", pin: true, scrub: 0.6,
+ onToggle: self => wc(u, self.isActive),
+ onUpdate: self => u.apply(self.progress),
+ onRefreshInit: () => u.apply(0)
+ });
+ u.apply(0);
+ } else {
+ u.mode = "assemble";
+ u.sec.querySelector(".pin").style.minHeight = "0";
+ u.apply(0);
+ // assemble once, when the scene scrolls in; keep will-change only for the tween's life
+ ScrollTrigger.create({
+ trigger: u.scene, start: "top 80%", once: true,
+ onEnter: () => { wc(u, true); gsap.to({ v: 0 }, {
+ v: 1, duration: 1.6, ease: "power3.out",
+ onUpdate: function () { u.apply(this.targets()[0].v); },
+ onComplete: () => wc(u, false) }); }
+ });
+ }
+ });
+ ScrollTrigger.refresh();
};
// the aperitif terminal types its transcript once, when it first enters view
document.querySelectorAll(".crt").forEach(crt => {
@@ -1002,7 +998,7 @@ body:not(.fx) .slab{display:none}
io.observe(crt);
});
- Promise.all(units.map(u => u.flat.decode ? u.flat.decode().catch(() => {}) : Promise.resolve()))
- .then(() => requestAnimationFrame(() => requestAnimationFrame(boot)));
+ // boot on the next frame; do not gate on decoding lazy images (that stalls with loading=lazy)
+ requestAnimationFrame(() => requestAnimationFrame(boot));
})();