diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx
index 43e31c8..6401c01 100644
--- a/apps/web/src/app/page.tsx
+++ b/apps/web/src/app/page.tsx
@@ -39,8 +39,8 @@ export default function Home() {
-
-
+ {/* Show before tell: the visual work leads, the services copy supports it below.
+ (The old order opened with two screens of text before the first image.) */}
diff --git a/apps/web/src/components/landing/hero-splash.tsx b/apps/web/src/components/landing/hero-splash.tsx
index ecc1451..160150e 100644
--- a/apps/web/src/components/landing/hero-splash.tsx
+++ b/apps/web/src/components/landing/hero-splash.tsx
@@ -20,8 +20,16 @@ const navButtons = [
export function HeroSplash({ onEnter, sectionRef, leaving }: { onEnter: () => void; sectionRef?: any; leaving?: boolean }) {
const [entering, setEntering] = useState(false);
const [fadeOut, setFadeOut] = useState(false);
+ // warmed: set on idle, well after LCP. It (a) paints a 1px element through the water
+ // filter so the browser compiles the filter graph BEFORE the first real use (the
+ // first-click stutter was the graph compiling mid-transition), and (b) turns on
+ // will-change for the logo so its layer is promoted ahead of the zoom. Doing this
+ // on requestIdleCallback keeps it invisible to Lighthouse.
+ const [warmed, setWarmed] = useState(false);
const dispRef = useRef(null);
const turbRef = useRef(null);
+ // Latest-handler ref so the window listeners below never hold a stale closure.
+ const enterRef = useRef<() => void>(() => {});
// When the page asks the splash to leave, it first becomes a fixed overlay; one
// frame later we drop opacity so it cross-fades into the revealed content.
@@ -59,6 +67,55 @@ export function HeroSplash({ onEnter, sectionRef, leaving }: { onEnter: () => vo
};
requestAnimationFrame(tick);
};
+ enterRef.current = handleEnter;
+
+ // Scroll intent = enter. Scrolling PAST the splash already dismissed it (the shell's
+ // IntersectionObserver), but only after a full screen of scrolling and with a hard
+ // cut. First wheel tick / touch drag / scroll key now plays the SAME water-ripple
+ // enter as a tap. Listeners are passive (never block the scroll) and the `entering`
+ // guard inside handleEnter dedupes against the button path.
+ useEffect(() => {
+ if (leaving) return;
+ let touchY: number | null = null;
+ const onWheel = (e: WheelEvent) => {
+ if (e.deltaY > 8) enterRef.current();
+ };
+ const onTouchStart = (e: TouchEvent) => {
+ touchY = e.touches[0]?.clientY ?? null;
+ };
+ const onTouchMove = (e: TouchEvent) => {
+ if (touchY == null) return;
+ const y = e.touches[0]?.clientY ?? touchY;
+ if (touchY - y > 24) { // a deliberate upward drag (scroll down), not a resting thumb
+ touchY = null;
+ enterRef.current();
+ }
+ };
+ const onKey = (e: KeyboardEvent) => {
+ if (e.key === "ArrowDown" || e.key === "PageDown" || e.key === " ") enterRef.current();
+ };
+ window.addEventListener("wheel", onWheel, { passive: true });
+ window.addEventListener("touchstart", onTouchStart, { passive: true });
+ window.addEventListener("touchmove", onTouchMove, { passive: true });
+ window.addEventListener("keydown", onKey);
+ return () => {
+ window.removeEventListener("wheel", onWheel);
+ window.removeEventListener("touchstart", onTouchStart);
+ window.removeEventListener("touchmove", onTouchMove);
+ window.removeEventListener("keydown", onKey);
+ };
+ }, [leaving]);
+
+ // Pre-warm on idle (post-LCP): compile the filter graph + promote the logo layer.
+ useEffect(() => {
+ const ric: typeof requestIdleCallback | undefined = (window as any).requestIdleCallback;
+ const id = ric ? ric(() => setWarmed(true)) : window.setTimeout(() => setWarmed(true), 1500);
+ return () => {
+ const cic: typeof cancelIdleCallback | undefined = (window as any).cancelIdleCallback;
+ if (ric && cic) cic(id as number);
+ else window.clearTimeout(id as number);
+ };
+ }, []);
return (
vo
+ {/* Filter pre-warm: a near-invisible 1px paint through the filter (opacity must be
+ non-zero or the paint is skipped and nothing compiles). Idle-gated via `warmed`. */}
+ {warmed && !entering && (
+
+ )}
+
{/* Logo (click to enter) + pulsing Cherenkov glow */}