diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index b5276e5..a08b91e 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -1171,6 +1171,12 @@ img { max-width: 100%; display: block; } transform: translateY(0); } +/* Deferred kits: once mounted, never stay invisible if JS timing races */ +[data-deferred="ready"] .reveal { + opacity: 1; + transform: translateY(0); +} + .deferred-skeleton { height: 120px; margin: 24px auto; diff --git a/frontend/src/components/HomeClient.tsx b/frontend/src/components/HomeClient.tsx index 9d11247..95c7910 100644 --- a/frontend/src/components/HomeClient.tsx +++ b/frontend/src/components/HomeClient.tsx @@ -129,9 +129,13 @@ export default function HomeClient(props: Props) { }, []); useEffect(() => { - // Above-fold reveal only — deferred sections self-reveal via WhenVisible - const nodes = document.querySelectorAll(".reveal:not(.visible)"); - if (!nodes.length) return; + // Above-fold reveal; keep watching briefly for late client mounts + const mark = (root: ParentNode = document) => { + root.querySelectorAll(".reveal:not(.visible)").forEach((el, i) => { + (el as HTMLElement).style.transitionDelay = `${(i % 6) * 0.08}s`; + }); + }; + mark(); const observer = new IntersectionObserver( (entries) => entries.forEach((e) => { if (e.isIntersecting) { @@ -139,13 +143,18 @@ export default function HomeClient(props: Props) { observer.unobserve(e.target); } }), - { threshold: 0.1, rootMargin: "0px 0px -40px 0px" } + { threshold: 0.08, rootMargin: "0px 0px -40px 0px" } ); - nodes.forEach((el, i) => { - (el as HTMLElement).style.transitionDelay = `${(i % 6) * 0.08}s`; - observer.observe(el); - }); - return () => observer.disconnect(); + const observeAll = () => { + document.querySelectorAll(".reveal:not(.visible)").forEach((el) => observer.observe(el)); + }; + observeAll(); + const mo = new MutationObserver(() => observeAll()); + mo.observe(document.body, { childList: true, subtree: true }); + return () => { + observer.disconnect(); + mo.disconnect(); + }; }, []); const toggleCompare = (slug: string) => { diff --git a/frontend/src/components/WhenVisible.tsx b/frontend/src/components/WhenVisible.tsx index 2759b98..0842b2e 100644 --- a/frontend/src/components/WhenVisible.tsx +++ b/frontend/src/components/WhenVisible.tsx @@ -29,7 +29,7 @@ export default function WhenVisible({ io.disconnect(); } }, - { rootMargin, threshold: 0.01 } + { rootMargin, threshold: 0 } ); io.observe(el); return () => io.disconnect(); @@ -37,10 +37,24 @@ export default function WhenVisible({ useEffect(() => { if (!show || !ref.current) return; - // Reveal animations for deferred sections without a global MutationObserver - ref.current.querySelectorAll(".reveal:not(.visible)").forEach((node) => { - node.classList.add("visible"); - }); + const root = ref.current; + + const markVisible = () => { + root.querySelectorAll(".reveal:not(.visible)").forEach((node) => { + node.classList.add("visible"); + }); + }; + + // Lazy chunks mount after show=true — watch subtree + retry briefly + markVisible(); + const mo = new MutationObserver(markVisible); + mo.observe(root, { childList: true, subtree: true }); + const timers = [50, 200, 600, 1500].map((ms) => window.setTimeout(markVisible, ms)); + + return () => { + mo.disconnect(); + timers.forEach(clearTimeout); + }; }, [show]); return (