diff --git a/src/app/globals.css b/src/app/globals.css index 5652c899d..ce2a4c235 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1,6 +1,6 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; +@import "tailwindcss/base"; +@import "tailwindcss/components"; +@import "tailwindcss/utilities"; :root { color-scheme: light; @@ -67,6 +67,12 @@ body { transition: background-color 200ms ease, color 200ms ease; } +/* Strip body background on landing page so the particle canvas (z-index: 0) is visible */ +body:has(.lnd-root) { + background: #050505 !important; + background-color: #050505 !important; +} + .surface-card { border: 1px solid var(--border); background: linear-gradient(180deg, color-mix(in srgb, var(--card) 92%, #ffffff 8%), var(--card)); @@ -174,6 +180,11 @@ body { animation: lndTicker 40s linear infinite; } +.lnd-root, +.lnd-root * { + cursor: none !important; +} + .lnd-root ::selection { background: rgba(129, 140, 248, 0.2); color: #fff; @@ -227,13 +238,25 @@ body { .lnd-cta-secondary:hover { color: #e0e0e0; border-color: #444; } .lnd-cell { - background: #0e0e0e; - border: 1px solid #1a1a1a; - border-radius: 8px; - padding: 14px 16px; - transition: background 0.2s, border-color 0.2s; + background: rgba(8, 8, 10, 0.55); + border: 1px solid rgba(255, 255, 255, 0.035); + border-radius: 12px; + padding: 18px 20px; + backdrop-filter: blur(16px); + -webkit-backdrop-filter: blur(16px); + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.05), + 0 4px 20px rgba(0, 0, 0, 0.35); + transition: all 0.4s cubic-bezier(0.16, 1, 0.3, 1); +} +.lnd-cell:hover { + background: rgba(12, 12, 16, 0.75); + border-color: rgba(129, 140, 248, 0.35); + transform: translateY(-2px); + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.08), + 0 8px 30px rgba(129, 140, 248, 0.12); } -.lnd-cell:hover { background: #141414; border-color: #2a2a2a; } .lnd-heatmap-cell { cursor: crosshair; } .lnd-heatmap-cell:hover { diff --git a/src/components/CustomCursor.tsx b/src/components/CustomCursor.tsx new file mode 100644 index 000000000..142fdcd19 --- /dev/null +++ b/src/components/CustomCursor.tsx @@ -0,0 +1,159 @@ +"use client"; + +import { useEffect, useState, useRef } from "react"; + +export default function CustomCursor() { + const [mounted, setMounted] = useState(false); + const [isHovered, setIsHovered] = useState(false); + const [isHidden, setIsHidden] = useState(true); + + // Target mouse coordinates + const mouseRef = useRef({ x: -100, y: -100 }); + + // Interpolated ring coordinates + const ringRef = useRef({ x: -100, y: -100 }); + + const dotDomRef = useRef(null); + const ringDomRef = useRef(null); + + useEffect(() => { + setMounted(true); + + const onMouseMove = (e: MouseEvent) => { + mouseRef.current.x = e.clientX; + mouseRef.current.y = e.clientY; + setIsHidden(false); + }; + + const onMouseLeave = () => { + setIsHidden(true); + }; + + const onMouseEnter = () => { + setIsHidden(false); + }; + + // Check if user is hovering over interactive components + const onMouseOver = (e: MouseEvent) => { + const target = e.target as HTMLElement; + if ( + target && + (target.closest("a") || + target.closest("button") || + target.closest(".lnd-cell") || + target.closest(".lnd-cta-primary") || + target.closest(".lnd-cta-secondary") || + target.closest(".lnd-nav-link") || + target.style.cursor === "pointer") + ) { + setIsHovered(true); + } else { + setIsHovered(false); + } + }; + + window.addEventListener("mousemove", onMouseMove, { passive: true }); + window.addEventListener("mouseleave", onMouseLeave, { passive: true }); + window.addEventListener("mouseenter", onMouseEnter, { passive: true }); + window.addEventListener("mouseover", onMouseOver, { passive: true }); + + let rafId: number; + + const render = () => { + const targetX = mouseRef.current.x; + const targetY = mouseRef.current.y; + + // Solid central core dot follows exactly + if (dotDomRef.current) { + dotDomRef.current.style.transform = `translate3d(${targetX - 4}px, ${targetY - 4}px, 0)`; + } + + // Smooth lag-interpolation (lerp) for the outer concentric ring + const rx = ringRef.current.x; + const ry = ringRef.current.y; + + const nextRx = rx + (targetX - rx) * 0.15; + const nextRy = ry + (targetY - ry) * 0.15; + + ringRef.current.x = nextRx; + ringRef.current.y = nextRy; + + if (ringDomRef.current) { + // Offset by -16px to align center (32px wide) + ringDomRef.current.style.transform = `translate3d(${nextRx - 16}px, ${nextRy - 16}px, 0)`; + } + + rafId = requestAnimationFrame(render); + }; + + rafId = requestAnimationFrame(render); + + return () => { + window.removeEventListener("mousemove", onMouseMove); + window.removeEventListener("mouseleave", onMouseLeave); + window.removeEventListener("mouseenter", onMouseEnter); + window.removeEventListener("mouseover", onMouseOver); + cancelAnimationFrame(rafId); + }; + }, []); + + if (!mounted) return null; + + return ( + <> + {/* 1. Glowing Cyan Dot Core */} +
+ + {/* 2. Concentric spring outer ring */} +
16px diff -> 8px margin shift) + marginTop: -8, + }), + }} + /> + + ); +} diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx index 72aa497b3..856bc7dda 100644 --- a/src/components/Footer.tsx +++ b/src/components/Footer.tsx @@ -1,10 +1,16 @@ +"use client"; + import Link from "next/link"; +import { usePathname } from "next/navigation"; const year = new Date().getFullYear(); export default function Footer() { + const pathname = usePathname(); + const isLanding = pathname === "/"; + return ( -