diff --git a/assets/dist/intro-establishing.webp b/assets/dist/intro-establishing.webp new file mode 100644 index 0000000..230417d Binary files /dev/null and b/assets/dist/intro-establishing.webp differ diff --git a/assets/raw/intro-establishing.jpeg b/assets/raw/intro-establishing.jpeg new file mode 100755 index 0000000..d797053 Binary files /dev/null and b/assets/raw/intro-establishing.jpeg differ diff --git a/build.sh b/build.sh index b3284e3..2c92ab3 100755 --- a/build.sh +++ b/build.sh @@ -20,7 +20,8 @@ for f in pilot/arc.tpl.html pilot/manifest-docker.json pilot/manifest-cluster.js assets/dist/delivery-flow.svg assets/dist/docker-layers.webp \ assets/dist/course-II-cluster.webp assets/dist/course-00-surround.webp \ assets/dist/course-III-node.webp assets/dist/course-III-pod.webp \ - assets/dist/course-IV-traffic.webp assets/dist/course-V-orbit.webp; do + assets/dist/course-IV-traffic.webp assets/dist/course-V-orbit.webp \ + assets/dist/intro-establishing.webp; do [[ -f "$f" ]] || { printf 'missing source: %s\n' "$f" >&2; exit 1; } done diff --git a/pilot/arc.tpl.html b/pilot/arc.tpl.html index 2128f8a..dda2ddb 100644 --- a/pilot/arc.tpl.html +++ b/pilot/arc.tpl.html @@ -1,4 +1,5 @@
+

The Exploded Cluster · The Delivery Arc

The machine was the easy part.
Now watch how software reaches it.

The foundations first - what an image is, what a cluster is - then the toolchain @@ -782,7 +783,15 @@ a:hover,a:focus-visible{border-bottom-color:var(--cyan)} .purpose h2.kt{margin:0 0 1rem} b{color:var(--ink)} .mono{font-family:var(--mono);font-size:.9em;color:var(--cyan)} -.masthead{max-width:64ch;margin:0 auto;padding:16vh 24px 9vh;text-align:center} +.masthead{position:relative;max-width:64ch;margin:0 auto;padding:16vh 24px 9vh;text-align:center} +/* The archipelago is anchored BELOW the text block: the words never sit over machinery. + Top edge starts under the lede; the mask feathers both ends so it dissolves into the void. */ +.introbg{position:absolute;inset:52% -50vw -2vh;z-index:-1;overflow:hidden;pointer-events:none} +.introbg img{width:100%;height:100%;object-fit:cover;object-position:center 62%;opacity:.55; + -webkit-mask-image:linear-gradient(180deg,transparent 0%,#000 26%,#000 82%,transparent 100%); + mask-image:linear-gradient(180deg,transparent 0%,#000 26%,#000 82%,transparent 100%)} +@media (max-width:900px){.introbg{inset:58% -60vw -2vh}.introbg img{opacity:.4}} +.masthead > :not(.introbg){position:relative} .eyebrow{font-family:var(--mono);font-size:.72rem;letter-spacing:.22em;text-transform:uppercase; color:var(--cyan);margin:0 0 1.5rem} .masthead h1,.thesis{font-size:clamp(1.9rem,4.8vw,3.2rem);line-height:1.1;margin:0 0 1.3rem; @@ -831,12 +840,15 @@ b{color:var(--ink)} .mono{font-family:var(--mono);font-size:.9em;color:var(--cya gap:26px;align-items:center} .scene{position:relative;margin:0;line-height:0} .scene .flat{display:block;width:100%;height:auto} -.slab{position:absolute;inset:0;background-size:100% 100%;background-repeat:no-repeat} +.slab{position:absolute;inset:0;background-size:100% 100%;background-repeat:no-repeat; + transition:opacity var(--dur-base) ease} .legend{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:13px} -.legend li{padding-left:22px;border-left:1px solid var(--line); +.legend li{padding-left:22px;border-left:1px solid var(--line);cursor:default; opacity:.34;transition:opacity var(--dur-base) ease,border-color var(--dur-base) ease} -.legend li.lit{opacity:1;border-left-color:var(--cyan)} +.legend li.lit{opacity:.72} +.legend li.act{opacity:1;border-left-color:var(--cyan)} +.legend li:hover,.legend li:focus-visible{opacity:1} .legend .t{font-size:.92rem;font-weight:600;margin:0 0 .22rem;letter-spacing:-.01em} .legend .b{font-size:.76rem;line-height:1.5;color:var(--dim);margin:0} @@ -919,9 +931,11 @@ body:not(.fx) .slab{display:none} const flat = scene.querySelector(".flat"); const legend = sec.querySelector(".legend"); // legend text comes from repo JSON; build with DOM nodes + textContent, never innerHTML - if (legend) man.parts.forEach(p => { + if (legend) man.parts.forEach((p, i) => { if (!p.title && !p.blurb) return; const li = document.createElement("li"); + li.dataset.part = String(i); + li.tabIndex = 0; // keyboard users get the same link as the mouse const t = document.createElement("p"); t.className = "t"; t.textContent = p.title; const b = document.createElement("p"); b.className = "b"; b.textContent = p.blurb; li.append(t, b); legend.appendChild(li); @@ -953,6 +967,18 @@ body:not(.fx) .slab{display:none} } } + // ---- intro backdrop (class: parallax) ------------------------------------------------ + // Drifts up and fades as the index arrives. Parallax is disabled outright under reduced + // motion (house rule), which is why this sits after the RM check for its own animation. + const introBg = document.querySelector(".introbg img"); + if (introBg && !RM && window.gsap && window.ScrollTrigger) { + gsap.to(introBg, { + yPercent: -12, opacity: 0.12, ease: TOK.EASE.scrub, + scrollTrigger: { trigger: ".masthead", start: "top top", end: "bottom top", + scrub: TOK.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"); @@ -1002,8 +1028,37 @@ body:not(.fx) .slab{display:none} "translate3d(" + ox.toFixed(3) + "%," + oy.toFixed(3) + "%,0)" + extra; }); 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))); + // Object <-> text linkage: the row being read is the part that stands out. The scrub + // walks the list top to bottom; whichever row is current gets .act, its slab stays at + // full strength and the rest sit back. Reading position and visual focus never diverge. + if (n) { + let act = -1; + lis.forEach((li, i) => { + const on = t >= 0.9 * ((i + 0.5) / n); + li.classList.toggle("lit", on); + if (on) act = i; + }); + u.act = act; + if (u.pin == null) u.paint(); + } }; + // paint() applies the focus state: pinned (hover/focus) wins over the scroll position. + u.paint = () => { + const f = u.pin != null ? u.pin : u.act; + u.slabs.forEach((el, i) => { el.style.opacity = (f < 0 || i === f) ? "1" : "0.38"; }); + lis.forEach((li, i) => li.classList.toggle("act", i === f)); + }; + u.pin = null; u.act = -1; + // hover or keyboard focus on a row pins the link to that part - user-initiated, so it + // stays available even when the scrub is not moving. + lis.forEach((li, i) => { + const on = () => { u.pin = i; u.paint(); }; + const off = () => { u.pin = null; u.paint(); }; + li.addEventListener("mouseenter", on); + li.addEventListener("focus", on); + li.addEventListener("mouseleave", off); + li.addEventListener("blur", off); + }); }); const boot = () => { diff --git a/pilot/assemble-arc.mjs b/pilot/assemble-arc.mjs index 1197343..fee64bd 100644 --- a/pilot/assemble-arc.mjs +++ b/pilot/assemble-arc.mjs @@ -60,6 +60,7 @@ let app = tpl.slice(i + 8, j) let body = tpl.slice(0, i) + tpl.slice(j + 9); const heroes = { + __HERO_INTRO__: "intro-establishing.webp", __HERO_SURROUND__: "course-00-surround.webp", __HERO_NODE__: "course-III-node.webp", __HERO_POD__: "course-III-pod.webp", @@ -90,8 +91,9 @@ for (const [ph, file] of Object.entries(heroes)) { heroUrls[ph] = url; const dims = webpSize(buf); const dimAttr = dims ? ` width="${dims.w}" height="${dims.h}"` : ""; - // Course 00's hero is the first visible image -> load eager; the rest are below the fold. - const load = ph === "__HERO_SURROUND__" ? "eager" : "lazy"; + // The intro backdrop is the above-the-fold LCP candidate -> eager. Everything else, + // including Course 00's hero, sits below the fold and lazy-loads. + const load = ph === "__HERO_INTRO__" ? "eager" : "lazy"; body = body.replace(`src="${ph}"`, `src="${url}"${dimAttr} decoding="async" loading="${load}"`); } { diff --git a/pilot/assemble-arc.py b/pilot/assemble-arc.py index 1a6f30d..0e34067 100644 --- a/pilot/assemble-arc.py +++ b/pilot/assemble-arc.py @@ -26,6 +26,7 @@ out = (ROOT / "pilot/arc.tpl.html").read_text() \ .replace("__MAN_GITOPS__", man("gitops")) \ .replace("__MAN_SUPPLY__", man("supply")) \ .replace("__MAN_HELM__", man("helm")) \ + .replace("__HERO_INTRO__", b64("intro-establishing.webp")) \ .replace("__HERO_SURROUND__", b64("course-00-surround.webp")) \ .replace("__HERO_NODE__", b64("course-III-node.webp")) \ .replace("__HERO_POD__", b64("course-III-pod.webp")) \