Dev notes
Why the zero-dependency constraint gave way to Framer Motion, how the B&W + pastel system works, and what's measurably better versus what's a design bet.
The zero-dependency constraint forced understanding what the browser can do on its own — that was worth doing. But it had a real limitation: exit animations don't exist in CSS. When a modal closes it just vanishes. CSS transitions only animate between states that are both in the DOM. Framer Motion solves that with AnimatePresence — it holds the element in the tree long enough for the exit animation to finish, then removes it.
The old approach also had spring configs scattered everywhere — inline transition objects with different stiffness values in every file. The new system has a single src/lib/animations.ts with named spring presets: snappy, smooth, bounce, gentle. Change one value there and every component that references it updates.
The old design treated all eight features the same — same card color, same border, same typography. Assigning each feature its own pastel fixes that: calendar is mint, TCG is rose, vitals is violet, NBA is amber. The colors are Tailwind 200-level — light enough not to dominate on black, saturated enough to be distinct from each other. Putting them on a black base instead of neutral gray means the pastels actually read as color. On a white background they look washed out.
Frosted glass cards let the dark background and the pastel tints bleed through, which ties everything together visually. A solid bg-surface card on a black background would just look like a white rectangle — disconnected. Technically: backdrop-filter: blur() is GPU composited. It's not a layout property, so it doesn't affect CLS. That mattered — no vitals regression for a visual effect.
The Three.js particle network was moved from the hero to /lab/particles — it was ~40kb of canvas code loading on every landing page visit. ShaderGradient takes its place with a black CSS fallback, so LCP fires on the H1 text immediately while the gradient loads behind it. The interactive mouse parallax maps cursor position to camera angles (cAzimuthAngle and cPolarAngle), RAF-throttled so there's one setState per frame maximum.
The provable improvements: bundle size (Three.js disappears from the landing chunk), exit animations (they literally didn't exist before), reduced-motion support (the app had none, now there's a provider), animation config centralization (all spring objects in one file). The web vitals dashboard gives a before/after on LCP, CLS, and INP since every version gets its own trend line — real user data, not synthetic Lighthouse scores.
What's not provable: whether glassmorphism looks better than flat cards, whether per-section animations are more engaging than a uniform stagger, whether the pastel palette is more memorable than a primary color scheme. Those are bets. The constraint changed from "zero dependencies" to "earn every dependency" — Framer earned it, ShaderGradient earned it, everything else is still the platform.
The v1 redesign added Framer Motion, glassmorphism, and the ShaderGradient hero. V2 goes further: it removes all 3D dependencies from the default path entirely. The landing page and authenticated hub were rebuilt from scratch, inspired by Adam Hartwig's editorial portfolio style — generous whitespace, full-width project cards, scroll-triggered reveals, and typographic hierarchy over visual noise.
What was removed from the default landing: WeatherCanvas, ShaderGradient (WebGL), the Three.js globe, and every R3F dependency. What replaced them: a CSS-only ambient gradient (two overlapping radial gradients on a 20-second keyframe loop), Framer Motion scroll animations, and a project showcase with grouped categories and zig-zag card layouts. The hero headline uses a staggered word reveal instead of a WebGL background effect.
The versioning scheme uses a URL parameter: ?version=v1 serves the original experience with all its 3D dependencies, loaded via next/dynamic so those chunks only download when explicitly requested. The default path ships zero Three.js bytes. A version registry in page.tsx maps each version to its Landing and Hub components, making future versions a single entry addition.
Bundle impact: the 19 chunks containing Three.js, R3F, and shader-gradient are all lazy-loaded and only fetched on ?version=v1. Zero 3D imports exist in src/app/v2/. The default landing page is now pure CSS, Framer Motion, and static React — no canvas, no WebGL, no heavy peer dependencies.
Moving the particle network to its own page at /lab/particles turns it from silent background decoration into a feature you can interact with — controls, color themes, mouse attraction toggle. A second lab page at /lab/motion documents the Framer Motion API with six interactive sections: spring physics, stagger grids, drag-to-reorder, scroll-driven parallax, gesture variants, and shared layout transitions. The spring playground was the first place to feel the difference between stiffness and damping with sliders rather than just reading about it.