diff --git a/apps/landing-page/app/_components/home-enhancer.astro b/apps/landing-page/app/_components/home-enhancer.astro index b39e60ec79f..ea67c27fe4d 100644 --- a/apps/landing-page/app/_components/home-enhancer.astro +++ b/apps/landing-page/app/_components/home-enhancer.astro @@ -10,8 +10,8 @@ * - Headroom-style sticky header (hide on scroll down, reveal on up) * - Live GitHub stars + latest release version injection * - Live Wire contributors row (replaces the static fallback) - * - IntersectionObserver-driven `data-reveal` choreography (without - * this every section sits at `opacity: 0` and the page looks blank) + * - IntersectionObserver-driven `data-reveal` choreography, progressively + * enhanced so content stays visible when JavaScript is unavailable * * Inlining this script in only one of the entry points causes the * "blank landing page" bug on localized homepages, so we centralize it @@ -300,17 +300,25 @@ return; } - const observer = new IntersectionObserver( - (entries) => { - for (const entry of entries) { - if (!entry.isIntersecting) continue; - entry.target.dataset.revealed = 'true'; - observer.unobserve(entry.target); - } - }, - { threshold: 0.12, rootMargin: '0px 0px -8% 0px' }, - ); + let observer; + try { + observer = new IntersectionObserver( + (entries) => { + for (const entry of entries) { + if (!entry.isIntersecting) continue; + entry.target.dataset.revealed = 'true'; + observer.unobserve(entry.target); + } + }, + { threshold: 0.12, rootMargin: '0px 0px -8% 0px' }, + ); - for (const el of elements) observer.observe(el); + for (const el of elements) observer.observe(el); + document.documentElement.classList.add('reveal-ready'); + } catch (error) { + observer?.disconnect(); + document.documentElement.classList.remove('reveal-ready'); + console.error('Reveal animation initialization failed', error); + } })(); diff --git a/apps/landing-page/app/globals.css b/apps/landing-page/app/globals.css index 593789b9cd7..4e43f27c2ec 100644 --- a/apps/landing-page/app/globals.css +++ b/apps/landing-page/app/globals.css @@ -1906,7 +1906,8 @@ a.nav-mega-col-head.is-active { * its `data-reveal`, so the IntersectionObserver in home-enhancer.astro flips it * to `data-revealed='true'` on scroll-in and the cascade begins. The keyframes * mirror the component's blur(10→5→0) + opacity(0→.5→1) + translateY steps. */ -.hero h1.hero-title[data-reveal] { +.hero h1.hero-title[data-reveal], +.reveal-ready .hero h1.hero-title[data-reveal] { /* Let the per-word blur be the whole motion: cancel the generic block reveal translate/opacity so the frame itself doesn't slide or fade. */ opacity: 1; @@ -1916,12 +1917,17 @@ a.nav-mega-col-head.is-active { } .blur-word { display: inline-block; + opacity: 1; + filter: none; + transform: none; +} +.reveal-ready .blur-word { opacity: 0; filter: blur(10px); transform: translateY(-50px); will-change: transform, filter, opacity; } -.hero-title[data-revealed='true'] .blur-word { +.reveal-ready .hero-title[data-revealed='true'] .blur-word { animation: blurTextIn 0.38s cubic-bezier(0.23, 1, 0.32, 1) both; animation-delay: calc(var(--i, 0) * 0.035s); } @@ -5113,7 +5119,7 @@ section.tight { padding: 90px 0; } pointer-events: none; } /* The product window rises up from below when the CTA scrolls into view. */ -.cta-window[data-reveal] { +.reveal-ready .cta-window[data-reveal]:not([data-revealed='true']) { translate: 0 96px; } .cta-dance-inner { @@ -5834,28 +5840,35 @@ footer .container { /* ---------- scroll-reveal motion ---------- * - * Driven by `app/_components/reveal-root.tsx`. Elements with - * `data-reveal` start hidden + offset; the observer sets - * `data-revealed='true'` once they enter the viewport, triggering - * the transition. + * Content is visible by default. Once the homepage observer is successfully + * bound, it adds `.reveal-ready` to opt unrevealed elements into the hidden + * start state. The observer then sets `data-revealed='true'` on viewport entry. + * If JavaScript is disabled or reveal setup fails, the class is never added and + * the complete page remains visible. * * Uses `translate` / `scale` longhand properties (not `transform`) so * that elements like `.work-card` keep their static `transform: rotate()` * intact while still translating in. */ [data-reveal] { - opacity: 0; - translate: 0 28px; + opacity: 1; + translate: 0 0; + scale: 1; transition: opacity 900ms cubic-bezier(0.22, 1, 0.36, 1) var(--reveal-delay, 0ms), translate 900ms cubic-bezier(0.22, 1, 0.36, 1) var(--reveal-delay, 0ms), scale 900ms cubic-bezier(0.22, 1, 0.36, 1) var(--reveal-delay, 0ms); + will-change: auto; +} +.reveal-ready [data-reveal]:not([data-revealed='true']) { + opacity: 0; + translate: 0 28px; will-change: opacity, translate, scale; } -[data-reveal='left'] { translate: -36px 0; } -[data-reveal='right'] { translate: 36px 0; } -[data-reveal='scale'] { translate: 0 0; scale: 0.96; } -[data-reveal='rise-lg'] { translate: 0 64px; scale: 0.985; } +.reveal-ready [data-reveal='left']:not([data-revealed='true']) { translate: -36px 0; } +.reveal-ready [data-reveal='right']:not([data-revealed='true']) { translate: 36px 0; } +.reveal-ready [data-reveal='scale']:not([data-revealed='true']) { translate: 0 0; scale: 0.96; } +.reveal-ready [data-reveal='rise-lg']:not([data-revealed='true']) { translate: 0 64px; scale: 0.985; } [data-reveal][data-revealed='true'] { opacity: 1; translate: 0 0; @@ -6202,7 +6215,8 @@ footer .container { border-radius: 10px; } /* cancel the translate-up reveal so the static image isn't shifted */ - .cta-window[data-reveal] { translate: 0 0; } + .cta-window[data-reveal], + .reveal-ready .cta-window[data-reveal]:not([data-revealed='true']) { translate: 0 0; } /* Copy block (title → tag → buttons) leads the stack; the window follows via its base order:1 (inner stays at the default order:0). */ .cta-dance-inner { padding: 0; align-items: center; } diff --git a/apps/landing-page/app/pages/index.astro b/apps/landing-page/app/pages/index.astro index 638c3465bf8..d5612642a44 100644 --- a/apps/landing-page/app/pages/index.astro +++ b/apps/landing-page/app/pages/index.astro @@ -1809,18 +1809,26 @@ const cobeEnhancerUrl = `/enhancers/cobe.js?v=${ return; } - const observer = new IntersectionObserver( - (entries) => { - for (const entry of entries) { - if (!entry.isIntersecting) continue; - entry.target.dataset.revealed = 'true'; - observer.unobserve(entry.target); - } - }, - { threshold: 0.12, rootMargin: '0px 0px -8% 0px' }, - ); + let observer; + try { + observer = new IntersectionObserver( + (entries) => { + for (const entry of entries) { + if (!entry.isIntersecting) continue; + entry.target.dataset.revealed = 'true'; + observer.unobserve(entry.target); + } + }, + { threshold: 0.12, rootMargin: '0px 0px -8% 0px' }, + ); - for (const el of elements) observer.observe(el); + for (const el of elements) observer.observe(el); + document.documentElement.classList.add('reveal-ready'); + } catch (error) { + observer?.disconnect(); + document.documentElement.classList.remove('reveal-ready'); + console.error('Reveal animation initialization failed', error); + } })();