website/docs/plans/2026-02-25-taste-redesign.md
For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
Goal: Upgrade the Mos homepage to "Precision Instrument" aesthetic — upgraded material quality, asymmetric bento grid, Framer Motion spring entrance/3D-hover, split Hero layout.
Architecture: Pure monochrome stays; quality comes from layered glass materials, diffusion shadows, and spring physics. Framer Motion replaces CSS reveal animations; CSS canvas/orb animations stay. A new BentoCard wraps all feature cards with 3D hover. A new HeroCurvePanel adds a right-column visual to the Hero.
Tech Stack: Next.js 16, React 19, Tailwind CSS v3, Framer Motion v11, TypeScript
Files:
package.jsonStep 1: Install the package
cd /Users/caldis/Code/Mos/website
pnpm add framer-motion@^11
Step 2: Verify install
Run: grep framer package.json
Expected output: "framer-motion": "^11.x.x"
Step 3: Commit
git add package.json pnpm-lock.yaml
git commit -m "feat: add framer-motion dependency"
Files:
app/globals.cssThis task upgrades the visual foundation. No JS changes.
Step 1: Update :root tokens
In app/globals.css, replace the :root block:
:root {
--bg0: #000000;
--bg1: #090909;
--fg0: rgba(255, 255, 255, 0.92);
--fg1: rgba(255, 255, 255, 0.72);
--fg2: rgba(255, 255, 255, 0.52);
--border: rgba(255, 255, 255, 0.12);
--accent: #ffffff;
--accent2: #a1a1aa;
--accent3: #52525b;
--radius-xl: 26px;
--radius-card: 32px;
--ease-out: cubic-bezier(0.16, 1, 0.3, 1);
--noise-opacity: 0.065;
}
Step 2: Update body background to asymmetric positions
Replace the body background gradient block:
body {
color: var(--fg0);
background:
radial-gradient(1100px 680px at 5% 8%, rgba(255, 255, 255, 0.10), transparent 55%),
radial-gradient(900px 620px at 88% 14%, rgba(255, 255, 255, 0.06), transparent 55%),
radial-gradient(1100px 820px at 45% 92%, rgba(255, 255, 255, 0.045), transparent 60%),
linear-gradient(180deg, var(--bg0), var(--bg1));
font-family:
var(--font-body),
ui-sans-serif,
system-ui,
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
Helvetica,
Arial,
"Apple Color Emoji",
"Segoe UI Emoji";
text-rendering: geometricPrecision;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
Step 3: Upgrade .glass utility
Replace the .glass block inside @layer utilities:
.glass {
background: rgba(8, 9, 14, 0.60);
border: 1px solid var(--border);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
box-shadow:
0 0 0 1px rgba(255,255,255,0.06) inset,
0 1px 0 rgba(255,255,255,0.12) inset,
0 32px 120px rgba(0,0,0,0.8);
}
Step 4: Upgrade .shadow-elevated utility
.shadow-elevated {
box-shadow:
0 32px 120px rgba(0, 0, 0, 0.80),
0 1px 0 rgba(255, 255, 255, 0.08) inset;
}
Step 5: Update .orb opacity
Change opacity: 0.18 → opacity: 0.10 in the .orb block.
Step 6: Remove the .reveal and .reveal.in-view CSS
Delete these blocks entirely (they will be replaced by Framer Motion in Task 3):
/* DELETE THESE: */
.reveal { ... }
.reveal.in-view { ... }
Also delete the @media (scripting: none) block that mentions .reveal:
/* DELETE: */
@media (scripting: none) {
.reveal { ... }
}
Keep @keyframes hero-in for now (removed in Task 6).
Keep .ring-accent, .hairline, .orb, .homebrew-highlight, etc.
Step 7: Remove the prefers-reduced-motion .reveal rule
The @media (prefers-reduced-motion: reduce) block at the bottom has no .reveal rules, so nothing to delete there. Keep homebrew-highlight reduced-motion rules.
Step 8: Start dev server and visually verify
pnpm dev
Open http://localhost:3000. The page should look the same in structure but with:
Step 9: Commit
git add app/globals.css
git commit -m "style: upgrade material tokens, glass highlight, deeper shadows"
Files:
app/components/Reveal/Reveal.tsxThe old implementation uses IntersectionObserver + CSS class toggling. Replace with Framer Motion useInView + motion.div.
Step 1: Replace the entire file content
"use client";
import { motion, useInView, useReducedMotion } from "framer-motion";
import { ReactNode, useRef } from "react";
const SPRING = { type: "spring" as const, stiffness: 100, damping: 20 };
const variants = {
hidden: { opacity: 0, y: 24, filter: "blur(12px)" },
visible: { opacity: 1, y: 0, filter: "blur(0px)" },
};
export function Reveal({
children,
className = "",
delayMs = 0,
}: {
children: ReactNode;
className?: string;
delayMs?: number;
}) {
const ref = useRef<HTMLDivElement | null>(null);
const shouldReduceMotion = useReducedMotion();
const inView = useInView(ref, {
once: true,
margin: "40px 0px -10% 0px",
});
return (
<motion.div
ref={ref}
className={className}
variants={variants}
initial={shouldReduceMotion ? "visible" : "hidden"}
animate={inView ? "visible" : "hidden"}
transition={{ ...SPRING, delay: delayMs / 1000 }}
>
{children}
</motion.div>
);
}
Step 2: Verify dev server
pnpm dev
Open http://localhost:3000. Scroll through the page — all sections should fade+slide in with spring motion. No broken layout.
Step 3: Commit
git add app/components/Reveal/Reveal.tsx
git commit -m "feat: replace CSS reveal with Framer Motion spring useInView"
Files:
app/components/BentoCard/BentoCard.tsxThis wraps bento feature cards with a 3D hover tilt effect. The card rotates slightly toward the cursor, and a spotlight follows the cursor inside the card.
Step 1: Create the file
"use client";
import {
motion,
useMotionTemplate,
useMotionValue,
useReducedMotion,
useSpring,
useTransform,
} from "framer-motion";
import { MouseEvent, ReactNode } from "react";
const SPRING = { stiffness: 150, damping: 30 };
export function BentoCard({
children,
className = "",
}: {
children: ReactNode;
className?: string;
}) {
const shouldReduceMotion = useReducedMotion();
const mouseX = useMotionValue(0.5);
const mouseY = useMotionValue(0.5);
const rawRotateX = useTransform(mouseY, [0, 1], [4, -4]);
const rawRotateY = useTransform(mouseX, [0, 1], [-6, 6]);
const rotateX = useSpring(rawRotateX, SPRING);
const rotateY = useSpring(rawRotateY, SPRING);
const spotX = useTransform(mouseX, [0, 1], [0, 100]);
const spotY = useTransform(mouseY, [0, 1], [0, 100]);
const spotlight = useMotionTemplate`radial-gradient(480px circle at ${spotX}% ${spotY}%, rgba(255,255,255,0.08), transparent 60%)`;
const handleMouseMove = (e: MouseEvent<HTMLDivElement>) => {
if (shouldReduceMotion) return;
const rect = e.currentTarget.getBoundingClientRect();
mouseX.set((e.clientX - rect.left) / rect.width);
mouseY.set((e.clientY - rect.top) / rect.height);
};
const handleMouseLeave = () => {
mouseX.set(0.5);
mouseY.set(0.5);
};
return (
<motion.div
className={`group relative h-full rounded-[var(--radius-card)] glass shadow-elevated overflow-hidden border border-white/10 ${className}`}
style={
shouldReduceMotion
? {}
: { rotateX, rotateY, transformPerspective: 1200 }
}
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
>
<motion.div
className="pointer-events-none absolute inset-0 opacity-0 group-hover:opacity-100 transition-opacity duration-500"
style={{ background: spotlight }}
/>
{children}
</motion.div>
);
}
Step 2: Verify (no visual change yet, just ensure no TypeScript errors)
pnpm build 2>&1 | head -40
Expected: no TypeScript errors related to BentoCard.
Step 3: Commit
git add app/components/BentoCard/BentoCard.tsx
git commit -m "feat: add BentoCard with 3D cursor-tilt and spotlight hover"
Files:
app/components/HeroCurvePanel/HeroCurvePanel.tsxA purely presentational right-column panel for the Hero section. Shows an animated SVG scroll curve (no sliders) with three floating parameter badges.
Step 1: Create the file
"use client";
import { motion, useReducedMotion } from "framer-motion";
const SPRING = { type: "spring" as const, stiffness: 80, damping: 18 };
// Pre-computed path representing a typical Mos smooth-scroll curve:
// fast rise to peak, then smooth exponential decay.
// ViewBox: 0 0 400 200 (y=200 is baseline, y=0 is max speed)
const CURVE_D =
"M 0 192 C 8 192 18 28 48 16 S 96 14 128 22 S 192 52 248 102 S 320 162 400 188";
const PARAMS = [
{ label: "STEP", value: "33.6" },
{ label: "GAIN", value: "×2.7" },
{ label: "DURATION", value: "4.35s" },
];
export function HeroCurvePanel() {
const shouldReduceMotion = useReducedMotion();
return (
<motion.div
className="hidden md:block w-[280px] lg:w-[320px] xl:w-[340px] shrink-0"
initial={shouldReduceMotion ? false : { opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ ...SPRING, delay: 0.34 }}
>
<motion.div
className="relative rounded-[var(--radius-card)] glass shadow-elevated border border-white/10 overflow-hidden p-5"
style={{ transformPerspective: 1200 }}
whileHover={shouldReduceMotion ? {} : { rotateY: -3 }}
transition={SPRING}
>
<div className="font-display text-[10px] tracking-[0.22em] uppercase text-white/45 mb-4">
Smooth Scroll Curve
</div>
<div className="rounded-2xl border border-white/8 bg-black/30 overflow-hidden">
<svg
viewBox="0 0 400 200"
className="block w-full h-auto"
aria-hidden="true"
>
<defs>
<linearGradient id="heroCurveFill" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="rgba(255,255,255,0.14)" />
<stop offset="100%" stopColor="rgba(255,255,255,0)" />
</linearGradient>
</defs>
<g stroke="rgba(255,255,255,0.06)" strokeWidth="1">
{[0, 1, 2, 3].map((i) => (
<line
key={`vl-${i}`}
x1={i * 133}
y1="8"
x2={i * 133}
y2="196"
/>
))}
{[0, 1, 2, 3].map((i) => (
<line
key={`hl-${i}`}
x1="0"
y1={8 + i * 62}
x2="400"
y2={8 + i * 62}
/>
))}
</g>
<path
d={`${CURVE_D} L 400 192 L 0 192 Z`}
fill="url(#heroCurveFill)"
opacity="0.9"
/>
<path
d={CURVE_D}
fill="none"
stroke="rgba(255,255,255,0.90)"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
pathLength={1}
strokeDasharray={1}
strokeDashoffset={1}
style={{
animation: "stroke-in 1200ms var(--ease-out) 500ms both",
}}
/>
</svg>
</div>
<div className="mt-4 grid grid-cols-3 gap-2">
{PARAMS.map((p) => (
<div
key={p.label}
className="rounded-xl border border-white/8 bg-white/4 px-2.5 py-2"
>
<div className="font-display text-[9px] tracking-[0.18em] uppercase text-white/40">
{p.label}
</div>
<div className="mt-1 font-mono text-sm tabular-nums text-white/80">
{p.value}
</div>
</div>
))}
</div>
</motion.div>
</motion.div>
);
}
Step 2: Verify no TypeScript errors
pnpm build 2>&1 | head -40
Step 3: Commit
git add app/components/HeroCurvePanel/HeroCurvePanel.tsx
git commit -m "feat: add HeroCurvePanel static SVG curve for Hero right column"
Files:
app/home-client.tsxThis is the largest change. We:
motion-safe:animate-[hero-in_...] on every hero element with motion.div FM staggermin-h-screen (keep only min-h-[100dvh])Step 1: Add imports at the top of home-client.tsx
After the existing imports, add:
import { motion, useReducedMotion } from "framer-motion";
import { HeroCurvePanel } from "./components/HeroCurvePanel/HeroCurvePanel";
Also add useReducedMotion to the top-level component — add this inside HomeClient():
const shouldReduceMotion = useReducedMotion();
Step 2: Define hero spring at module level (above HomeClient)
const HERO_SPRING = { type: "spring" as const, stiffness: 100, damping: 20 };
function heroMotion(delayS: number, shouldReduceMotion: boolean | null) {
return {
initial: shouldReduceMotion ? false : ({ opacity: 0, y: 24 } as const),
animate: { opacity: 1, y: 0 } as const,
transition: { ...HERO_SPRING, delay: delayS },
};
}
Step 3: Replace the hero <section> in the JSX
Find the hero section (starts at <section className="relative min-h-screen min-h-[100svh] ...).
Replace the entire section with the code below. Key changes:
min-h-screen min-h-[100svh] → min-h-[100dvh]<div className="w-full"> → <div className="w-full grid grid-cols-1 md:grid-cols-[1fr_auto] gap-12 lg:gap-20 items-center"><motion.div {...heroMotion(delay, shouldReduceMotion)}><h1> upgraded<HeroCurvePanel /> added as second grid column<section className="relative min-h-[100dvh] pt-28 sm:pt-36 pb-10 sm:pb-12 flex flex-col">
<div className="flex-1 flex items-center">
<div className="w-full grid grid-cols-1 md:grid-cols-[1fr_auto] gap-12 lg:gap-20 items-center">
<div>
<motion.div
className="inline-flex items-center gap-3 rounded-full border border-white/10 bg-black/40 px-4 py-2 text-xs text-white/70 shadow-elevated"
{...heroMotion(0, shouldReduceMotion)}
>
<span className="inline-flex items-center gap-2">
<span className="h-2 w-2 rounded-full bg-[color:var(--accent)] shadow-[0_0_22px_rgba(255,255,255,0.35)]" />
{t.hero.badgeLine1}
</span>
<span className="hidden sm:inline text-white/35">•</span>
<span className="hidden sm:inline font-mono text-white/45">
{t.hero.badgeLine2}
</span>
</motion.div>
<motion.h1
className="mt-7 font-display text-balance text-[52px] leading-[0.95] tracking-[-0.02em] sm:text-[88px] md:text-[108px] lg:text-[124px] text-white"
{...heroMotion(0.08, shouldReduceMotion)}
>
{t.hero.titleLine1}
<span className="block">
{t.hero.titleLine2Before}
<span
className="inline-block text-flow"
style={{ textShadow: "0 0 42px rgba(255,255,255,0.08)" }}
>
{t.hero.titleLine2Highlight}
</span>
{t.hero.titleLine2After}
</span>
</motion.h1>
<motion.p
className="mt-5 max-w-2xl text-balance text-[15px] sm:text-lg text-white/72 leading-[1.7]"
{...heroMotion(0.18, shouldReduceMotion)}
>
{t.hero.lead}
</motion.p>
<motion.div
className="mt-8 flex flex-col sm:flex-row sm:items-start gap-3 sm:gap-4"
{...heroMotion(0.26, shouldReduceMotion)}
>
<div className="flex flex-col items-start w-fit">
<Magnetic strength={22}>
<a
href={downloadUrl}
target="_blank"
rel="noopener noreferrer"
className="group relative overflow-hidden rounded-[18px] px-6 py-3.5 text-sm sm:text-base font-semibold tracking-wide text-black shadow-elevated border border-black/10 inline-flex items-center justify-center"
style={{
background:
"linear-gradient(180deg, rgba(255,255,255,0.96) 0%, rgba(255,255,255,0.84) 100%)",
}}
>
<span className="relative z-10">{t.hero.ctaDownload}</span>
<span className="absolute inset-0 opacity-0 group-hover:opacity-100 transition-opacity duration-500 [background:radial-gradient(800px_240px_at_30%_0%,rgba(0,0,0,0.18),transparent_55%)]" />
</a>
</Magnetic>
<a
href="#homebrew"
onClick={(e) => {
e.preventDefault();
scrollToHomebrew();
}}
className="mt-2 self-center text-xs font-mono text-white/50 hover:text-white/75 transition-colors underline decoration-white/15 hover:decoration-white/35 underline-offset-4"
>
{t.hero.ctaInstallHomebrew}
</a>
</div>
<Magnetic strength={14}>
<a
href="https://github.com/Caldis/Mos"
target="_blank"
rel="noopener noreferrer"
className="group inline-flex items-center justify-center rounded-[18px] px-6 py-3.5 text-sm sm:text-base font-semibold tracking-wide text-white/85 border border-white/12 bg-white/5 hover:bg-white/8 transition-colors"
>
<span className="mr-2 opacity-70 group-hover:opacity-100 transition-opacity">↗</span>
<span>{t.hero.ctaViewGitHub}</span>
</a>
</Magnetic>
<div className="sm:ml-auto sm:self-center text-xs text-white/45">
<div className="font-mono tabular-nums">{t.hero.requirementsLine1}</div>
<div className="font-mono">{t.hero.requirementsLine2}</div>
</div>
</motion.div>
</div>
<HeroCurvePanel />
</div>
</div>
<div className="mt-8 sm:mt-10 flex items-center gap-3 text-white/40">
<div className="h-[1px] flex-1 hairline" />
<div className="font-mono text-[11px] tracking-[0.18em] uppercase">
{t.hero.scrollHint}
</div>
<div className="h-[1px] flex-1 hairline" />
</div>
</section>
Step 4: Remove @keyframes hero-in from globals.css
Now that hero elements use Framer Motion, the hero-in keyframe is unused. Open app/globals.css and delete the @keyframes hero-in block:
/* DELETE THIS: */
@keyframes hero-in {
from { ... }
to { ... }
}
Step 5: Visual verify on dev server
pnpm dev
Step 6: Commit
git add app/home-client.tsx app/globals.css
git commit -m "feat: hero split layout with FM spring stagger, upgraded typography"
Files:
app/home-client.tsxChange the features section from [12] / [6,6] / [12] to [7,5] / [5,7].
Wrap each card's inner <div> with <BentoCard>.
Step 1: Add BentoCard import at top of home-client.tsx
import { BentoCard } from "./components/BentoCard/BentoCard";
Step 2: Replace the bento grid section
Find the <div className="mt-10 grid grid-cols-1 md:grid-cols-12 gap-4"> block and replace it with:
<div className="mt-10 grid grid-cols-1 md:grid-cols-12 gap-4">
<Reveal className="md:col-span-7" delayMs={140}>
<BentoCard>
<div className="relative p-6 sm:p-8">
<div className="font-display text-[11px] tracking-[0.22em] uppercase text-white/50">
{t.sectionFeel.cards.curves.kicker}
</div>
<div className="mt-4 text-2xl sm:text-3xl text-white font-semibold">
{t.sectionFeel.cards.curves.title}
</div>
<p className="mt-3 text-white/62 leading-[1.7]">
{t.sectionFeel.cards.curves.body}
</p>
<EasingPlayground className="mt-6" />
</div>
</BentoCard>
</Reveal>
<Reveal className="md:col-span-5" delayMs={200}>
<BentoCard>
<div className="relative p-6 sm:p-8">
<div className="font-display text-[11px] tracking-[0.22em] uppercase text-white/50">
{t.sectionFeel.cards.axes.kicker}
</div>
<div className="mt-4 text-2xl sm:text-3xl text-white font-semibold">
{t.sectionFeel.cards.axes.title}
</div>
<p className="mt-3 text-white/62 leading-[1.7]">
{t.sectionFeel.cards.axes.body}
</p>
<div className="mt-6 rounded-2xl border border-white/10 bg-black/30 p-5">
<div className="space-y-3">
{(["Y", "X"] as const).map((axis) => {
const row = axesDemo[axis];
return (
<div key={axis} className="flex items-center gap-3">
<div className="h-10 w-10 rounded-2xl border border-white/10 bg-white/5 grid place-items-center">
<span className="font-mono text-xs text-white/60">{axis}</span>
</div>
<div className="flex flex-1 flex-wrap gap-2">
<div className="flex min-w-[150px] flex-1 items-center justify-between rounded-xl border border-white/10 bg-white/5 px-3 py-2">
<span className="font-mono text-[11px] text-white/60">
{t.sectionFeel.cards.axes.smooth}
</span>
<MiniToggle
checked={row.smooth}
onToggle={() => toggleAxis(axis, "smooth")}
ariaLabel={`${axis} ${t.sectionFeel.cards.axes.smooth}`}
/>
</div>
<div className="flex min-w-[150px] flex-1 items-center justify-between rounded-xl border border-white/10 bg-white/5 px-3 py-2">
<span className="font-mono text-[11px] text-white/60">
{t.sectionFeel.cards.axes.reverse}
</span>
<MiniToggle
checked={row.reverse}
onToggle={() => toggleAxis(axis, "reverse")}
ariaLabel={`${axis} ${t.sectionFeel.cards.axes.reverse}`}
/>
</div>
</div>
</div>
);
})}
</div>
</div>
</div>
</BentoCard>
</Reveal>
<Reveal className="md:col-span-5" delayMs={260}>
<BentoCard>
<div className="relative p-6 sm:p-8 min-h-[360px]">
<div className="font-display text-[11px] tracking-[0.22em] uppercase text-white/50">
{t.sectionFeel.cards.perApp.kicker}
</div>
<div className="mt-4 text-2xl sm:text-3xl text-white font-semibold">
{t.sectionFeel.cards.perApp.title}
</div>
<p className="mt-3 text-white/62 leading-[1.7]">
{t.sectionFeel.cards.perApp.body}
</p>
<div className="mt-6 grid grid-cols-2 gap-3">
{APP_PROFILES.map((a) => (
<div
key={a.id}
className="rounded-2xl border border-white/10 bg-white/5 p-3 flex items-center gap-3"
>
<div className="h-12 w-12 shrink-0 rounded-xl border border-white/10 bg-black/20 overflow-hidden">
<Image
src={a.icon}
alt=""
width={48}
height={48}
className="h-full w-full object-cover"
/>
</div>
<div>
<div className="font-mono text-[11px] text-white/65">{a.name}</div>
{a.axes.Y.smooth && (
<div className="mt-0.5 inline-flex items-center gap-1 rounded-full bg-white/8 border border-white/10 px-1.5 py-0.5">
<span className="h-1.5 w-1.5 rounded-full bg-white/60" />
<span className="font-mono text-[9px] text-white/50">smooth</span>
</div>
)}
</div>
</div>
))}
</div>
</div>
</BentoCard>
</Reveal>
<Reveal className="md:col-span-7" delayMs={320}>
<BentoCard>
<div className="relative p-6 sm:p-8 min-h-[360px]">
<div className="font-display text-[11px] tracking-[0.22em] uppercase text-white/50">
{t.sectionFeel.cards.buttons.kicker}
</div>
<div className="mt-4 text-2xl sm:text-3xl text-white font-semibold">
{t.sectionFeel.cards.buttons.title}
</div>
<p className="mt-3 text-white/62 leading-[1.7]">
{t.sectionFeel.cards.buttons.body}
</p>
<div className="mt-6 rounded-2xl border border-white/10 bg-black/30 p-5">
<div className="font-mono text-xs text-white/45">
{t.sectionFeel.cards.buttons.quickBind}
</div>
<div className="mt-3 grid gap-2">
{[
{
k: t.sectionFeel.cards.buttons.rows.button4,
v: t.sectionFeel.cards.buttons.rows.missionControl,
},
{
k: t.sectionFeel.cards.buttons.rows.button5,
v: t.sectionFeel.cards.buttons.rows.nextSpace,
},
{
k: t.sectionFeel.cards.buttons.rows.wheelClick,
v: t.sectionFeel.cards.buttons.rows.appSwitcher,
},
].map((row) => (
<div
key={row.k}
className="flex items-center justify-between rounded-xl border border-white/10 bg-white/5 px-3 py-2"
>
<div className="font-mono text-xs text-white/75">{row.k}</div>
<div className="font-mono text-xs text-white/45">{row.v}</div>
</div>
))}
<div className="flex items-center justify-between rounded-xl border border-white/8 bg-white/3 px-3 py-2 opacity-60">
<div className="flex items-center gap-2">
<span className="relative flex h-1.5 w-1.5">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-white/60 opacity-75" />
<span className="relative inline-flex rounded-full h-1.5 w-1.5 bg-white/40" />
</span>
<div className="font-mono text-xs text-white/45">—</div>
</div>
<div className="font-mono text-[10px] text-white/30 italic">recording…</div>
</div>
</div>
</div>
</div>
</BentoCard>
</Reveal>
</div>
Step 3: Visual verify
pnpm dev
Step 4: Commit
git add app/home-client.tsx
git commit -m "feat: asymmetric bento grid 7+5/5+7, BentoCard 3D hover, per-app 2col, buttons pulse"
Files:
app/home-client.tsxUpgrade the "Feel" section header and download section header with new kicker tracking.
Step 1: Update section kicker tracking in Feel section
Find the <h2> block in the Feel section:
<h2 className="font-display text-balance text-3xl sm:text-5xl text-white leading-tight">
Change to:
<h2 className="font-display text-balance text-3xl sm:text-5xl text-white leading-[0.95] tracking-[-0.01em]">
Find the lead <p> in the Feel section:
<p className="mt-4 max-w-3xl text-white/68 leading-relaxed">
Change to:
<p className="mt-4 max-w-3xl text-white/68 leading-[1.7]">
Step 2: Update download section header
Find the <h3> in the download section:
<h3 className="font-display text-balance text-3xl sm:text-6xl text-white leading-tight">
Change to:
<h3 className="font-display text-balance text-3xl sm:text-6xl text-white leading-[0.95] tracking-[-0.015em]">
Find the download lead <p>:
<p className="mt-4 max-w-3xl text-white/68 leading-relaxed">
Change to:
<p className="mt-4 max-w-3xl text-white/68 leading-[1.7]">
Step 3: Commit
git add app/home-client.tsx
git commit -m "style: tighten section header leading and tracking"
Files:
app/components/FlowField/FlowField.tsxStep 1: Reduce fill alpha for longer trails
In FlowField.tsx, find the line inside the tick function:
ctx.fillStyle = `rgba(0, 0, 0, ${0.12 + scroll * 0.06})`;
Change to:
ctx.fillStyle = `rgba(0, 0, 0, ${0.08 + scroll * 0.05})`;
This reduces the background erase alpha, making particle trails persist longer and creating a more fluid, flowing look.
Step 2: Commit
git add app/components/FlowField/FlowField.tsx
git commit -m "style: longer FlowField particle trails (fill alpha 0.12→0.08)"
Files:
app/home-client.tsx (dvh check)pnpm buildStep 1: Verify min-h-[100dvh] is used
Search the file for any remaining min-h-screen or min-h-[100svh]:
grep -n "min-h-screen\|100svh" /Users/caldis/Code/Mos/website/app/home-client.tsx
Expected: no matches (the hero section should already use 100dvh from Task 6). If any found, replace with min-h-[100dvh].
Step 2: Verify useReducedMotion is used in FM components
All three FM components (Reveal, BentoCard, HeroCurvePanel) use useReducedMotion. Confirm:
grep -n "useReducedMotion" \
/Users/caldis/Code/Mos/website/app/components/Reveal/Reveal.tsx \
/Users/caldis/Code/Mos/website/app/components/BentoCard/BentoCard.tsx \
/Users/caldis/Code/Mos/website/app/components/HeroCurvePanel/HeroCurvePanel.tsx
Expected: one match per file.
Step 3: Production build — verify no TypeScript/build errors
cd /Users/caldis/Code/Mos/website && pnpm build
Expected: ✓ Compiled successfully. Fix any TypeScript errors before committing.
Step 4: Verify no purple/blue colors crept in
grep -rn "purple\|violet\|indigo\|blue" /Users/caldis/Code/Mos/website/app/globals.css
Expected: no matches (monochrome constraint satisfied).
Step 5: Final commit
git add app/home-client.tsx
git commit -m "feat: taste redesign pre-flight — dvh, reduced-motion, build verified"
| File | Change |
|---|---|
package.json | Add framer-motion@^11 |
app/globals.css | Material tokens, glass highlight, deeper shadows, remove .reveal CSS |
app/components/Reveal/Reveal.tsx | FM useInView + spring animation |
app/components/BentoCard/BentoCard.tsx | New — 3D hover tilt + cursor spotlight |
app/components/HeroCurvePanel/HeroCurvePanel.tsx | New — static SVG curve panel for hero |
app/home-client.tsx | Hero split grid, FM stagger entrance, asymmetric bento, typography |
app/components/FlowField/FlowField.tsx | Longer trails (fill alpha) |
Tasks must run in order (each task builds on the previous): 1 → 2 → 3 → 4 → 5 → 6 → 7 → 8 → 9 → 10
Tasks 4 and 5 (BentoCard and HeroCurvePanel) are independent of each other and can be parallelized if using subagent-driven development.