diff --git a/apps/web/src/app/globals.css b/apps/web/src/app/globals.css index 69f7e12..24a0ba2 100644 --- a/apps/web/src/app/globals.css +++ b/apps/web/src/app/globals.css @@ -234,6 +234,24 @@ @keyframes splash-zoom { 0% { transform: scale(1); } 50% { transform: scale(1.06); } 100% { transform: scale(1.14); } } .splash-zoom { animation: splash-zoom 0.8s ease-in-out forwards; } -/* Blinking terminal cursor inside the nebula monitor (opacity only = compositor). */ -@keyframes nebula-cursor-blink { 0%, 49% { opacity: 0.85; } 50%, 100% { opacity: 0; } } -.nebula-cursor { animation: nebula-cursor-blink 1.1s steps(1, end) infinite; } \ No newline at end of file +/* Tiny terminal inside the nebula monitor: type "such" once, backspace, then a blinking + caret on the left. width via steps() is the typewriter; border-right is the caret. */ +.nebula-term { + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-size: 9px; + line-height: 1; + letter-spacing: 0.5px; + white-space: nowrap; + overflow: hidden; + width: 0; + border-right: 1.5px solid currentColor; + animation: nebula-type 5s steps(4, end) forwards, nebula-caret 0.9s steps(1, end) infinite; +} +@keyframes nebula-type { + 0% { width: 0; } + 16% { width: 4ch; } + 60% { width: 4ch; } + 74% { width: 0; } + 100% { width: 0; } +} +@keyframes nebula-caret { 50% { border-color: transparent; } } \ No newline at end of file diff --git a/packages/ui/src/procedural/nebula-field.tsx b/packages/ui/src/procedural/nebula-field.tsx index 00c00b6..8e43b91 100644 --- a/packages/ui/src/procedural/nebula-field.tsx +++ b/packages/ui/src/procedural/nebula-field.tsx @@ -105,8 +105,8 @@ export const NebulaField = ({ className, density = 8, themeMode = 'dark', positi // mousemove, so there is no mobile cost. const pointerXRaw = useMotionValue(0); const pointerYRaw = useMotionValue(0); - const pointerX = useSpring(pointerXRaw, { stiffness: 60, damping: 18, mass: 0.4 }); - const pointerY = useSpring(pointerYRaw, { stiffness: 60, damping: 18, mass: 0.4 }); + const pointerX = useSpring(pointerXRaw, { stiffness: 40, damping: 22, mass: 0.6 }); + const pointerY = useSpring(pointerYRaw, { stiffness: 40, damping: 22, mass: 0.6 }); // Monitor position: just to the left of the hero title, on every screen. The extra // top room the hero gets on mobile (see page.tsx) keeps this clear of the header. @@ -118,8 +118,9 @@ export const NebulaField = ({ className, density = 8, themeMode = 'dark', positi const targetX = computerX + 1.5; const targetY = computerY + 1.5; - // Fewer, slightly thinner streamers on mobile for a cleaner read on a small screen. - const effectiveDensity = isMobile ? 5 : density; + // Same streamer count on every screen. Reducing it on mobile made them too sparse + // to read (they looked like there were none). + const effectiveDensity = density; // Check for reduced motion preference useEffect(() => { @@ -144,7 +145,7 @@ export const NebulaField = ({ className, density = 8, themeMode = 'dark', positi // Pointer parallax driver (desktop only). Spring-smoothed via pointerX/Y above. useEffect(() => { if (isMobile) return; - const MAX = 12; // px of nudge at the screen edges + const MAX = 6; // px of nudge at the screen edges (subtle, spring-eased) const onMove = (e: MouseEvent) => { pointerXRaw.set(((e.clientX / window.innerWidth) - 0.5) * 2 * MAX); pointerYRaw.set(((e.clientY / window.innerHeight) - 0.5) * 2 * MAX); @@ -189,13 +190,13 @@ export const NebulaField = ({ className, density = 8, themeMode = 'dark', positi setMonitorColor(colors[Math.floor(Math.random() * colors.length)]); monitorControls.start({ - scale: [1, 1.1, 1], + scale: [1, 1.14, 1], filter: [ "brightness(1) drop-shadow(0 0 0px rgba(0,0,0,0))", - "brightness(1.3) drop-shadow(0 0 12px currentColor)", + "brightness(1.7) drop-shadow(0 0 18px currentColor)", "brightness(1) drop-shadow(0 0 0px rgba(0,0,0,0))" ], - transition: { duration: 1.5, ease: "easeOut" } + transition: { duration: 0.9, ease: "easeOut" } }); }, [monitorControls, themeMode]); @@ -265,13 +266,9 @@ export const NebulaField = ({ className, density = 8, themeMode = 'dark', positi > - {/* Live-terminal blink inside the screen. CSS opacity only, so it runs on the - compositor for essentially no main-thread cost. */} - @@ -287,7 +284,7 @@ export const NebulaField = ({ className, density = 8, themeMode = 'dark', positi d={d} themeMode={themeMode} onHit={handleHit} - width={(themeMode === 'light' ? 0.08 : 0.12) * (isMobile ? 0.85 : 1)} + width={themeMode === 'light' ? 0.08 : 0.12} index={i} /> ))}