diff --git a/apps/web/src/app/globals.css b/apps/web/src/app/globals.css index e34c840..fe51f43 100644 --- a/apps/web/src/app/globals.css +++ b/apps/web/src/app/globals.css @@ -227,16 +227,4 @@ transition-duration: 0.01ms !important; scroll-behavior: auto !important; } -} - -/* Splash hero idle animations as CSS (browser/compositor driven) instead of - framer-motion, so they cost no JS main-thread time. The reduced-motion block - above neutralizes them. */ -@keyframes splash-float { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-8px); } } -@keyframes splash-breath { 0%, 100% { opacity: 0.9; } 50% { opacity: 1; } } -@keyframes splash-ring-a { from { stroke-dashoffset: 0; } to { stroke-dashoffset: -1; } } -@keyframes splash-ring-b { from { stroke-dashoffset: 0; } to { stroke-dashoffset: 1; } } -.splash-float { animation: splash-float 6s ease-in-out infinite; } -.splash-breath { animation: splash-breath 5s ease-in-out infinite; } -.splash-ring-a { animation: splash-ring-a 5s linear infinite; } -.splash-ring-b { animation: splash-ring-b 7s linear infinite; } +} \ No newline at end of file diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx index 1b172e7..a22d70f 100644 --- a/apps/web/src/app/page.tsx +++ b/apps/web/src/app/page.tsx @@ -22,6 +22,10 @@ export default function Home() { // header, bottom nav, and background take over. Scrolling back up lands on the hero, // never the splash again. const [entered, setEntered] = useState(false); + // `splashUp` keeps the splash mounted through its fade-out; `leaving` triggers the + // cross-fade overlay (button path only). + const [splashUp, setSplashUp] = useState(true); + const [leaving, setLeaving] = useState(false); const splashRef = useRef(null); const restoreScroll = useRef(0); @@ -31,6 +35,7 @@ export default function Home() { useIsoLayoutEffect(() => { if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) { setEntered(true); + setSplashUp(false); } }, []); @@ -49,6 +54,7 @@ export default function Home() { if (!entry.isIntersecting) { restoreScroll.current = Math.max(0, window.scrollY - el.offsetHeight); setEntered(true); + setSplashUp(false); // scrolled past: just unmount, no overlay fade needed } }, { threshold: 0 }, @@ -85,6 +91,10 @@ export default function Home() { const enterFromButton = () => { restoreScroll.current = 0; setEntered(true); + setLeaving(true); + // Keep the splash mounted briefly as a fixed, fading overlay so it cross-fades + // into the revealed content instead of hard-cutting. + setTimeout(() => setSplashUp(false), 800); }; return ( @@ -92,7 +102,7 @@ export default function Home() {
- {!entered && } + {splashUp && }

- {/* steady bounding box, Cherenkov cyan */} + {/* steady bounding box, Cherenkov cyan (static: the circling rings were + removed because stroke-dashoffset is non-composited and destabilized + mobile LCP; the pulsing glow behind the logo carries the motion) */} - {/* two Cherenkov glow segments circling the ring in opposite directions */} - {animate && ( - <> - - - - )} {/* letters: static so they're a stable LCP candidate */} {/* S */} @@ -64,13 +49,21 @@ function SuchMark({ animate }: { animate: boolean }) { ); } -export function HeroSplash({ onEnter, sectionRef }: { onEnter: () => void; sectionRef?: any }) { +export function HeroSplash({ onEnter, sectionRef, leaving }: { onEnter: () => void; sectionRef?: any; leaving?: boolean }) { const prefersReduced = useReducedMotion(); - const animate = !prefersReduced; const [entering, setEntering] = useState(false); + const [fadeOut, setFadeOut] = useState(false); const dispRef = useRef(null); const turbRef = useRef(null); + // 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. + useEffect(() => { + if (!leaving) return; + const id = requestAnimationFrame(() => setFadeOut(true)); + return () => cancelAnimationFrame(id); + }, [leaving]); + const handleEnter = () => { if (entering) return; if (prefersReduced) { @@ -101,7 +94,11 @@ export function HeroSplash({ onEnter, sectionRef }: { onEnter: () => void; secti
{/* Click-to-enter: soft Cherenkov wash. The ripple itself is the SVG water filter refracting the logo (driven by handleEnter's rAF loop). */} @@ -139,10 +136,10 @@ export function HeroSplash({ onEnter, sectionRef }: { onEnter: () => void; secti - + diff --git a/packages/ui/src/procedural/nebula-field.tsx b/packages/ui/src/procedural/nebula-field.tsx index 4eebb2a..312090c 100644 --- a/packages/ui/src/procedural/nebula-field.tsx +++ b/packages/ui/src/procedural/nebula-field.tsx @@ -92,8 +92,10 @@ export const NebulaField = ({ className, density = 8, themeMode = 'dark', positi // Position calculations const computerX = position?.x ?? 18; const computerY = position?.y ?? 18; - const targetX = computerX + 2.5; - const targetY = computerY + 2.5; + // Aim the streamers at the monitor's screen: slightly up-and-left of the icon's + // geometric center. Tweak these two offsets to move the convergence point. + const targetX = computerX - 1; + const targetY = computerY - 1.5; // Check for reduced motion preference useEffect(() => {