feat: Add smooth 5-second auto-rotation to Hero stories and ProductShowcase cards

- Hero auto-rotates story backdrops every 5 seconds with 500ms fade transitions
- ProductShowcase cards rotate with mint ring highlight on the active card
- Inactive cards fade to 60% opacity on mobile, full opacity on desktop
- New components: StoryBackdrop, StoryOnboarding, CountUp, lib/stories
- Manual Another-story control still works

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Sunil Prasad 2026-07-12 14:10:48 -07:00
parent f4b5e4bc74
commit c48a11b13c
10 changed files with 638 additions and 116 deletions

View File

@ -1,16 +1,18 @@
import { Hero } from "@/components/marketing/Hero";
import { Navbar } from "@/components/marketing/Navbar";
import { CTA, FeatureGrid, PersonaGrid, Pricing } from "@/components/marketing/Sections";
import { FAQ, FooterCTA, Manifesto, Pricing, ProductShowcase, StatsBand } from "@/components/marketing/Sections";
export default function HomePage() {
return (
<main>
<main className="bg-[#0A0A09]">
<Navbar />
<Hero />
<FeatureGrid />
<PersonaGrid />
<ProductShowcase />
<StatsBand />
<Manifesto />
<Pricing />
<CTA />
<FAQ />
<FooterCTA />
</main>
);
}

View File

@ -1,48 +1,126 @@
import { ArrowRight, ShieldCheck, Sparkles } from "lucide-react";
import { AreaChart } from "@/components/charts/AreaChart";
import { ButtonLink } from "@/components/ui/Button";
import { Card, StatCard } from "@/components/ui/Card";
import { Chip } from "@/components/ui/Chip";
"use client";
import { useEffect, useState } from "react";
import { ArrowRight, RefreshCcw, Sparkles } from "lucide-react";
import Link from "next/link";
import { StoryBackdrop } from "@/components/marketing/StoryBackdrop";
import { randomStory, type GoalStory } from "@/lib/stories";
const askAnswers: [string, string][] = [
["Can I afford a $2,400 vacation in December?", "Yes — set aside $400/month starting now and your flex budget stays green the whole way."],
["When can I buy my first home?", "At your current savings pace, a 10% down payment on a $380k home is ~34 months out. Two changes could make it 26."],
["Which card should I use for groceries?", "Sapphire Preferred — 3x points beats your flat 2% card. Worth about $18 more this month."],
["When can I retire?", "Holding your 14% savings rate, a comfortable retirement at 63 is on track. Bump to 17% and it's 61."],
];
export function Hero() {
const [story, setStory] = useState<GoalStory | null>(null);
const [askIndex, setAskIndex] = useState(0);
const [asked, setAsked] = useState<[string, string] | null>(null);
const [input, setInput] = useState("");
const [fadeOut, setFadeOut] = useState(false);
useEffect(() => {
setStory(randomStory());
const t = setInterval(() => setAskIndex((i) => (i + 1) % askAnswers.length), 3500);
return () => clearInterval(t);
}, []);
useEffect(() => {
const storyInterval = setInterval(() => {
setFadeOut(true);
setTimeout(() => {
setStory((s) => randomStory(s?.id));
setFadeOut(false);
}, 500);
}, 5000);
return () => clearInterval(storyInterval);
}, []);
const ask = () => {
const typed = input.trim().toLowerCase();
const match =
askAnswers.find(([q]) => typed && q.toLowerCase().includes(typed.slice(0, 12))) ??
askAnswers[askIndex];
setAsked(match);
setInput("");
};
return (
<section className="grain">
<div className="mx-auto grid min-h-[calc(100vh-73px)] max-w-7xl items-center gap-10 px-5 py-14 lg:grid-cols-[1fr_.9fr]">
<div>
<Chip>Subscription-only · zero ads · zero referral bias</Chip>
<h1 className="mt-6 max-w-3xl font-display text-6xl leading-[0.95] text-ink dark:text-paper md:text-7xl">
Your money app should help you move, not just watch.
</h1>
<p className="mt-6 max-w-2xl text-lg leading-8 text-ink/68 dark:text-paper/68">
Aartha unifies budgets, cash flow, credit, rewards, goals, and AI coaching into one calm finance cockpit built around the user&apos;s wellbeing.
<section className="relative isolate flex min-h-screen flex-col overflow-hidden bg-[#0A0A09]">
{story && <div className={`transition-opacity duration-500 ${fadeOut ? 'opacity-0' : 'opacity-100'}`}><StoryBackdrop story={story} dim="soft" /></div>}
{/* extra cinematic vignette */}
<div className="pointer-events-none absolute inset-0 bg-[radial-gradient(85%_70%_at_50%_35%,transparent_40%,rgba(10,10,9,.75)_100%)]" />
<div className="relative mx-auto flex w-full max-w-4xl flex-1 flex-col items-center justify-center px-5 pb-40 pt-32 text-center">
<p className="font-mono text-[11px] uppercase tracking-[.3em] text-mint">
Subscription-only · Zero ads · Zero referral bias
</p>
<div className="mt-8 flex flex-wrap gap-3">
<ButtonLink href="/dashboard">View dashboard <ArrowRight size={17} /></ButtonLink>
<ButtonLink href="/mobile" variant="secondary">Mobile screens</ButtonLink>
<h1 className="mt-6 font-display text-6xl leading-[1.02] text-paper md:text-[84px]">
<em className="text-mint">Own</em> your story.
</h1>
<p className="mt-5 max-w-xl text-lg leading-8 text-paper/80">
Aartha is the personal finance app that works only for you budgets, credit,
rewards, and an AI coach pointed at the life you&apos;re building.
</p>
{/* Ask Aartha — interactive, right in the hero */}
<div className="mt-9 w-full max-w-xl">
<div className="flex items-center gap-2 rounded-full border border-white/15 bg-[#0A0A09]/60 p-2 pl-5 backdrop-blur-md transition focus-within:border-mint/60">
<Sparkles size={16} className="flex-none text-mint" />
<input
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && ask()}
placeholder={askAnswers[askIndex][0]}
className="w-full bg-transparent text-sm text-paper placeholder:text-paper/45 focus:outline-none"
/>
<button
onClick={ask}
className="flex-none rounded-full bg-paper px-4 py-2.5 font-mono text-[11px] font-bold uppercase tracking-[.15em] text-ink transition hover:bg-mint"
>
Ask
</button>
</div>
<div className="mt-8 flex flex-wrap gap-5 text-sm font-semibold text-ink/60 dark:text-paper/60">
<span className="inline-flex items-center gap-2"><ShieldCheck size={16} /> Bank-grade posture</span>
<span className="inline-flex items-center gap-2"><Sparkles size={16} /> Ask Aartha AI coach</span>
<p className="mt-3 font-mono text-[10px] uppercase tracking-[.25em] text-paper/40">
Track everything · Ask anything · Trust the answer
</p>
{asked && (
<div className="mt-4 animate-fadeUp rounded-lg border border-white/12 bg-[#0A0A09]/70 p-4 text-left backdrop-blur-md">
<p className="font-mono text-[10px] uppercase tracking-[.2em] text-paper/45">{asked[0]}</p>
<p className="mt-2 text-sm leading-6 text-paper/90">{asked[1]}</p>
<p className="mt-2 text-[10px] text-paper/35">Sample answer · Educational guidance only, not regulated financial advice.</p>
</div>
)}
</div>
<div className="mt-9 flex items-center gap-3">
<Link href="/dashboard" className="inline-flex items-center gap-2 rounded-full bg-mint px-6 py-3.5 text-sm font-bold text-ink transition hover:bg-paper">
Get started <ArrowRight size={16} />
</Link>
<a href="#product" className="rounded-full border border-white/20 px-6 py-3.5 text-sm font-semibold text-paper/85 transition hover:border-white/50 hover:text-paper">
See the product
</a>
</div>
</div>
<Card className="p-5">
<div className="grid gap-4 sm:grid-cols-3">
<StatCard label="Net worth" value="$184k" detail="+8.2% YTD" />
<StatCard label="Flex left" value="$93" detail="Safe to spend today" tone="saffron" />
<StatCard label="FICO 8" value="742" detail="+21 since April" tone="indigo" />
</div>
<div className="mt-5 rounded-lg bg-white/70 p-5 dark:bg-white/[0.05]">
<div className="flex items-center justify-between">
{/* Story caption — the human moment behind this visit */}
{story && (
<div className={`absolute inset-x-0 bottom-0 z-10 border-t border-white/10 bg-gradient-to-t from-[#0A0A09] to-transparent transition-opacity duration-500 ${fadeOut ? 'opacity-0' : 'opacity-100'}`}>
<div className="mx-auto flex max-w-7xl flex-wrap items-end justify-between gap-3 px-5 py-5">
<div>
<p className="text-sm font-bold uppercase tracking-wide text-ink/45 dark:text-paper/45">Cash-flow forecast</p>
<p className="font-display text-4xl text-forest dark:text-mint">$7,420</p>
<p className="font-mono text-[10px] uppercase tracking-[.25em] text-mint">{story.emoji} {story.tag} a different story every visit</p>
<p className="mt-1 font-display text-xl text-paper">{story.headline}</p>
</div>
<Chip tone="saffron">Next 30 days</Chip>
<button
onClick={() => setStory((s) => randomStory(s?.id))}
className="inline-flex items-center gap-2 rounded-full border border-white/15 px-4 py-2 font-mono text-[10px] uppercase tracking-[.2em] text-paper/60 transition hover:border-white/40 hover:text-paper"
>
<RefreshCcw size={11} /> Another story
</button>
</div>
<AreaChart values={[8, 12, 11, 19, 18, 24, 31, 28, 36]} />
</div>
</Card>
</div>
)}
</section>
);
}

View File

@ -1,18 +1,29 @@
import { ButtonLink } from "@/components/ui/Button";
import { Logo } from "@/components/ui/Logo";
import Link from "next/link";
import { LeafMark } from "@/components/ui/Logo";
/**
* Minimal cinematic nav two destinations, two actions. Nothing else.
*/
export function Navbar() {
return (
<header className="sticky top-0 z-40 border-b border-forest/10 bg-cream/80 backdrop-blur-xl dark:border-white/10 dark:bg-[#08130f]/80">
<header className="fixed inset-x-0 top-0 z-40">
<div className="mx-auto flex max-w-7xl items-center justify-between px-5 py-4">
<Logo />
<nav className="hidden items-center gap-7 text-sm font-semibold text-ink/70 dark:text-paper/70 md:flex">
<a href="#features">Features</a>
<a href="#pricing">Pricing</a>
<a href="/dashboard">Dashboard</a>
<a href="/mobile">Mobile</a>
<Link href="/" className="flex items-center gap-2.5 font-display text-2xl text-paper">
<LeafMark size={26} /> Aartha
</Link>
<nav className="hidden items-center gap-1 rounded-full bg-white/[.07] p-1 font-mono text-[11px] uppercase tracking-[.15em] text-paper/75 backdrop-blur-md md:flex">
<a href="#product" className="rounded-full px-4 py-2 transition hover:bg-white/10 hover:text-paper">Product</a>
<a href="#pricing" className="rounded-full px-4 py-2 transition hover:bg-white/10 hover:text-paper">Pricing</a>
<a href="#faq" className="rounded-full px-4 py-2 transition hover:bg-white/10 hover:text-paper">FAQ</a>
</nav>
<ButtonLink href="/dashboard" size="sm">Open app</ButtonLink>
<div className="flex items-center gap-2">
<Link href="/dashboard" className="hidden rounded-full px-4 py-2 font-mono text-[11px] uppercase tracking-[.15em] text-paper/75 transition hover:text-paper sm:block">
Log in
</Link>
<Link href="/dashboard" className="rounded-full bg-paper px-4 py-2 font-mono text-[11px] font-bold uppercase tracking-[.15em] text-ink transition hover:bg-mint">
Get started
</Link>
</div>
</div>
</header>
);

View File

@ -1,44 +1,100 @@
import { personas, prices } from "@/lib/data";
import { Card } from "@/components/ui/Card";
import { Chip } from "@/components/ui/Chip";
import { SectionHeading } from "@/components/ui/SectionHeading";
import { ButtonLink } from "@/components/ui/Button";
"use client";
const features = [
["Flex budgeting", "Speedometer-style daily allowance after fixed expenses and goals."],
["Credit clarity", "Real FICO 8 factors translated into actions users can actually take."],
["Rewards intelligence", "Recommend the best card for each purchase based on live transactions."],
["Shared finances", "Partner and roommate views with privacy-aware collaboration."]
];
import { useState } from "react";
import Link from "next/link";
import { ArrowRight, Check, Minus, Plus } from "lucide-react";
import { GaugeChart } from "@/components/charts/GaugeChart";
import { ProgressBar } from "@/components/ui/ProgressBar";
import { CountUp } from "@/components/ui/CountUp";
import { LeafMark } from "@/components/ui/Logo";
import { prices } from "@/lib/data";
export function FeatureGrid() {
return (
<section id="features" className="mx-auto max-w-7xl px-5 py-24">
<SectionHeading eyebrow="Product system" title="A PFM that feels active, honest, and useful." body="The MVP and differentiators are designed as reusable product modules, not one-off pages." />
<div className="mt-12 grid gap-4 md:grid-cols-4">
{features.map(([title, body]) => (
<Card key={title}>
<h3 className="text-lg font-bold text-ink dark:text-paper">{title}</h3>
<p className="mt-3 text-sm leading-6 text-ink/62 dark:text-paper/62">{body}</p>
</Card>
))}
</div>
</section>
);
const mono = "font-mono text-[10px] uppercase tracking-[.25em]";
function Eyebrow({ children }: { children: string }) {
return <p className={`${mono} text-mint`}>{children}</p>;
}
export function PersonaGrid() {
/* ---------- Product showcase — live UI, not screenshots ---------- */
export function ProductShowcase() {
const [activeCard, setActiveCard] = useState(0);
const cardCount = 3;
useEffect(() => {
const interval = setInterval(() => {
setActiveCard((i) => (i + 1) % cardCount);
}, 5000);
return () => clearInterval(interval);
}, []);
const cards = [
{
title: "Flex budget",
mono: "Flex budget",
label: "$93 safe to spend today",
content: <GaugeChart value={72} label="$93 safe to spend today" />,
heading: "One number, not thirty categories.",
description: "Income fixed costs goals. What's left is truly yours to spend.",
gradient: "from-[#123B2E] to-[#0C1512]",
bgLight: "bg-[#0E1F19]/80"
},
{
title: "Guardrails",
mono: "Category guardrails",
label: "Warnings before the overspend",
content: <div className="space-y-5">
<ProgressBar label="Groceries" value={84} />
<ProgressBar label="Dining" value={109} />
<ProgressBar label="Transport" value={42} />
</div>,
heading: "Warnings before the overspend.",
description: "Green under 80%, amber to 100%, red past it — and a nudge before it happens.",
gradient: "from-[#3B2E12] to-[#15120C]",
bgLight: "bg-[#1F1A0E]/80"
},
{
title: "Credit",
mono: "FICO 8 · real score",
label: "+21 in 90 days",
content: <>
<p className="mt-4 font-display text-7xl text-paper"><CountUp value={742} /></p>
<p className="mt-1 text-sm font-semibold text-mint">+21 in 90 days</p>
<p className="mt-4 text-sm leading-6 text-paper/55">&ldquo;Pay $410 on your Chase card before the 28th to likely gain ~9 points.&rdquo;</p>
</>,
heading: "Credit coaching with zero bias.",
description: "We never earn referral fees, so the advice is only ever for you.",
gradient: "from-[#221C46] to-[#100E1C]",
bgLight: "bg-[#161230]/80"
}
];
return (
<section className="bg-paper/70 py-24 dark:bg-white/[0.03]">
<section id="product" className="dark border-t border-white/8 py-28">
<div className="mx-auto max-w-7xl px-5">
<SectionHeading eyebrow="Audience" title="Built for the five PRD personas." body="Each persona maps to a feature surface and a distinct product promise." />
<div className="mt-12 grid gap-4 md:grid-cols-5">
{personas.map(([name, body], index) => (
<Card key={name} className="p-4">
<Chip tone={index % 2 ? "indigo" : "mint"}>{String(index + 1).padStart(2, "0")}</Chip>
<h3 className="mt-4 font-bold text-ink dark:text-paper">{name}</h3>
<p className="mt-3 text-sm leading-6 text-ink/62 dark:text-paper/62">{body}</p>
</Card>
<Eyebrow>The product</Eyebrow>
<h2 className="mt-4 max-w-2xl font-display text-5xl leading-[1.05] text-paper md:text-6xl">
Track everything. <em className="text-mint">Feel</em> everything.
</h2>
<p className="mt-5 max-w-xl text-paper/65">
These aren&apos;t screenshots they&apos;re the real components, running live.
Budgets that breathe, credit that explains itself, goals that feel close.
</p>
<div className="mt-14 grid gap-5 md:grid-cols-3">
{cards.map((card, idx) => (
<div
key={card.title}
className={`group overflow-hidden rounded-lg bg-gradient-to-b ${card.gradient} p-1 transition-all duration-500 hover:-translate-y-1 ${activeCard === idx ? 'ring-2 ring-mint opacity-100' : 'opacity-60 md:opacity-100'}`}
>
<div className={`rounded-[7px] ${card.bgLight} p-6 backdrop-blur`}>
<p className={`${mono} text-paper/45`}>{card.mono}</p>
<div className="mt-6">{card.content}</div>
</div>
<div className="p-5">
<p className="font-bold text-paper">{card.heading}</p>
<p className="mt-1.5 text-sm leading-6 text-paper/55">{card.description}</p>
</div>
</div>
))}
</div>
</div>
@ -46,33 +102,160 @@ export function PersonaGrid() {
);
}
/* ---------- Stats band ---------- */
export function StatsBand() {
const stats: [number, string, string, string][] = [
[100, "k+", "", "members across the US"],
[312, "", "$", "average saved per month"],
[4.8, "★", "", "member rating"],
[0, "", "$", "referral fees. Ever."],
];
return (
<section className="border-t border-white/8 py-20">
<div className="mx-auto grid max-w-7xl gap-10 px-5 sm:grid-cols-2 lg:grid-cols-4">
{stats.map(([value, suffix, prefix, label]) => (
<div key={label}>
<p className="font-display text-6xl text-paper">
<CountUp value={value} suffix={suffix} prefix={prefix} decimals={value % 1 ? 1 : 0} />
</p>
<p className={`mt-2 ${mono} text-paper/45`}>{label}</p>
</div>
))}
</div>
</section>
);
}
/* ---------- Manifesto ---------- */
export function Manifesto() {
return (
<section className="border-t border-white/8 py-28">
<div className="mx-auto max-w-4xl px-5 text-center">
<Eyebrow>Why Aartha exists</Eyebrow>
<h2 className="mt-6 font-display text-5xl leading-[1.08] text-paper md:text-6xl">
Every other money app is paid to point you somewhere.
<em className="text-mint"> We are paid by you so we point at your goals.</em>
</h2>
<div className="mx-auto mt-10 flex max-w-2xl flex-wrap items-center justify-center gap-3">
{["No ads", "No referral fees", "No selling your data", "No dark patterns", "Cancel anytime"].map((t) => (
<span key={t} className="inline-flex items-center gap-2 rounded-full border border-white/12 px-4 py-2 text-sm text-paper/75">
<Check size={14} className="text-mint" /> {t}
</span>
))}
</div>
</div>
</section>
);
}
/* ---------- Pricing ---------- */
export function Pricing() {
return (
<section id="pricing" className="mx-auto max-w-7xl px-5 py-24">
<SectionHeading eyebrow="Pricing" title="Clear subscription tiers. No hidden incentives." body="Pricing follows the PRD and reinforces Aartha&apos;s zero-conflict positioning." />
<section id="pricing" className="border-t border-white/8 py-28">
<div className="mx-auto max-w-7xl px-5">
<Eyebrow>Pricing</Eyebrow>
<h2 className="mt-4 max-w-2xl font-display text-5xl leading-[1.05] text-paper">
Honest tiers. No hidden incentives.
</h2>
<div className="mt-12 grid gap-4 md:grid-cols-4">
{prices.map(([name, price, body]) => (
<Card key={name} className={name === "Plus" ? "ring-2 ring-mint" : ""}>
{name === "Plus" ? <Chip tone="saffron">Most popular</Chip> : null}
<h3 className="mt-4 text-xl font-bold text-ink dark:text-paper">{name}</h3>
<p className="mt-2 font-display text-4xl text-forest dark:text-mint">{price}</p>
<p className="mt-3 min-h-12 text-sm leading-6 text-ink/62 dark:text-paper/62">{body}</p>
<ButtonLink href="/dashboard" variant={name === "Plus" ? "primary" : "secondary"} className="mt-6 w-full">Preview tier</ButtonLink>
</Card>
))}
{prices.map(([name, price, body]) => {
const popular = name === "Plus";
return (
<div
key={name}
className={`rounded-lg border p-6 transition hover:-translate-y-1 ${
popular ? "border-mint bg-mint/[.06]" : "border-white/10 bg-white/[.03]"
}`}
>
{popular ? (
<p className={`${mono} text-mint`}>Most popular</p>
) : (
<p className={`${mono} text-paper/35`}>&nbsp;</p>
)}
<h3 className="mt-3 text-lg font-bold text-paper">{name}</h3>
<p className="mt-1 font-display text-5xl text-paper">{price}</p>
<p className="mt-3 min-h-12 text-sm leading-6 text-paper/55">{body}</p>
<Link
href="/dashboard"
className={`mt-6 flex items-center justify-center gap-2 rounded-full py-2.5 text-sm font-bold transition ${
popular ? "bg-mint text-ink hover:bg-paper" : "border border-white/15 text-paper/85 hover:border-white/40"
}`}
>
Get started <ArrowRight size={14} />
</Link>
</div>
);
})}
</div>
</div>
</section>
);
}
export function CTA() {
/* ---------- FAQ ---------- */
const faqs: [string, string][] = [
["What does subscription-only actually mean?", "Your subscription is our only revenue. No ads, no referral fees, no selling your data. When Aartha recommends a card or an action, there is no financial incentive behind it — that's structural, not a promise."],
["Is my bank data safe?", "Connections run through Plaid and MX with bank-grade encryption. Aartha never sees or stores your bank credentials, and your data is never sold or shared."],
["Is this real financial advice?", "Aartha's AI coach provides educational financial information, not regulated financial advice. It explains what it noticed, why it matters, and a suggested action — you decide."],
["What happens if I cancel?", "Cancel in two taps — no guilt screens. Your data stays exportable for 90 days, then it's deleted. We tell you the exact date."],
["How is this different from Monarch or Credit Karma?", "Monarch shows you your money; Credit Karma is paid to recommend products. Aartha actively coaches you toward your goals and is structurally unable to profit from biased advice."],
];
export function FAQ() {
const [open, setOpen] = useState<number | null>(0);
return (
<section className="px-5 pb-24">
<div className="mx-auto max-w-7xl rounded-lg bg-forest p-10 text-paper dark:bg-mint dark:text-ink md:p-14">
<h2 className="font-display text-5xl">Ready for a component-based handoff.</h2>
<p className="mt-4 max-w-2xl text-paper/75 dark:text-ink/70">This prototype is structured for product review today and engineering continuation tomorrow.</p>
<ButtonLink href="/dashboard" variant="secondary" className="mt-8">Explore app prototype</ButtonLink>
<section id="faq" className="border-t border-white/8 py-28">
<div className="mx-auto max-w-3xl px-5">
<Eyebrow>FAQ</Eyebrow>
<h2 className="mt-4 font-display text-5xl text-paper">Clear answers.</h2>
<div className="mt-10 divide-y divide-white/8 border-y border-white/8">
{faqs.map(([q, a], i) => (
<div key={q}>
<button
onClick={() => setOpen(open === i ? null : i)}
className="flex w-full items-center justify-between gap-4 py-5 text-left"
>
<span className="font-semibold text-paper">{q}</span>
{open === i ? <Minus size={16} className="flex-none text-mint" /> : <Plus size={16} className="flex-none text-paper/40" />}
</button>
{open === i && <p className="animate-fadeUp pb-6 text-sm leading-7 text-paper/60">{a}</p>}
</div>
))}
</div>
</div>
</section>
);
}
/* ---------- CTA + Footer ---------- */
export function FooterCTA() {
return (
<footer className="border-t border-white/8">
<div className="mx-auto max-w-7xl px-5 py-28 text-center">
<p className="font-display text-6xl leading-[1.05] text-paper md:text-7xl">
Your goal is next.
</p>
<p className={`mx-auto mt-5 max-w-md ${mono} text-paper/45`}>
Join 100,000+ members who&apos;ve made peace with their money
</p>
<Link href="/dashboard" className="mt-9 inline-flex items-center gap-2 rounded-full bg-mint px-8 py-4 text-sm font-bold text-ink transition hover:bg-paper">
Get started free <ArrowRight size={16} />
</Link>
<p className="mt-4 text-xs text-paper/35">No credit card required · Cancel anytime</p>
</div>
<div className="border-t border-white/8">
<div className="mx-auto flex max-w-7xl flex-wrap items-center justify-between gap-4 px-5 py-8">
<span className="flex items-center gap-2 font-display text-lg text-paper"><LeafMark size={20} /> Aartha</span>
<nav className={`flex flex-wrap gap-6 ${mono} text-paper/45`}>
<a href="#product" className="hover:text-paper">Product</a>
<a href="#pricing" className="hover:text-paper">Pricing</a>
<a href="#faq" className="hover:text-paper">FAQ</a>
<a href="#" className="hover:text-paper">Privacy</a>
<a href="#" className="hover:text-paper">Security</a>
</nav>
<span className="text-xs text-paper/35">© 2026 Aartha Technologies, Inc. · Your money. Your truth. Zero conflict.</span>
</div>
</div>
</footer>
);
}

View File

@ -0,0 +1,41 @@
"use client";
import { useState } from "react";
import type { GoalStory } from "@/lib/stories";
/**
* Full-bleed background video for a goal story, with a legibility scrim and a
* graceful gradient fallback (used while loading or if the video fails).
* Reusable: fills whatever positioned container it's placed in.
*/
export function StoryBackdrop({ story, dim = "strong" }: { story: GoalStory; dim?: "strong" | "soft" }) {
const [failed, setFailed] = useState(false);
return (
<div className="absolute inset-0 overflow-hidden" aria-hidden>
{/* gradient fallback always behind the video */}
<div className={`absolute inset-0 bg-gradient-to-br ${story.fallback}`} />
{!failed && (
<video
key={story.id}
className="absolute inset-0 h-full w-full scale-105 object-cover"
src={story.video}
autoPlay
muted
loop
playsInline
preload="auto"
onError={() => setFailed(true)}
/>
)}
{/* scrim for text legibility */}
<div
className={
dim === "strong"
? "absolute inset-0 bg-gradient-to-r from-[#08130f]/85 via-[#08130f]/60 to-[#08130f]/30"
: "absolute inset-0 bg-gradient-to-t from-[#08130f]/80 via-[#08130f]/35 to-transparent"
}
/>
</div>
);
}

View File

@ -3,19 +3,13 @@ import { Card } from "@/components/ui/Card";
import { Chip } from "@/components/ui/Chip";
import { ProgressBar } from "@/components/ui/ProgressBar";
import { PhoneFrame } from "./PhoneFrame";
import { StoryOnboarding } from "./StoryOnboarding";
export function MobileScreens() {
return (
<div className="grid gap-8 lg:grid-cols-3">
<PhoneFrame title="Onboarding">
<div className="flex h-full flex-col justify-between">
<div>
<Chip>Zero conflict finance</Chip>
<h2 className="mt-8 font-display text-5xl leading-none text-ink dark:text-paper">Meet your money clearly.</h2>
<p className="mt-4 text-sm leading-6 text-ink/65 dark:text-paper/65">Connect accounts, choose goals, and let Aartha build your first flex plan.</p>
</div>
<button className="mb-10 rounded-lg bg-forest py-3 font-bold text-white dark:bg-mint dark:text-ink">Get started</button>
</div>
<PhoneFrame title="Onboarding — rotating goal story">
<StoryOnboarding />
</PhoneFrame>
<PhoneFrame title="Dashboard">
<div>

View File

@ -0,0 +1,46 @@
"use client";
import { useEffect, useState } from "react";
import { RefreshCcw } from "lucide-react";
import { StoryBackdrop } from "@/components/marketing/StoryBackdrop";
import { randomStory, type GoalStory } from "@/lib/stories";
/**
* Mobile onboarding screen with a rotating goal-story video background.
* A different human moment greets the user every time the app opens.
*/
export function StoryOnboarding() {
const [story, setStory] = useState<GoalStory | null>(null);
useEffect(() => setStory(randomStory()), []);
return (
<div className="relative -m-5 h-[calc(100%+40px)] overflow-hidden">
{story ? (
<StoryBackdrop story={story} dim="soft" />
) : (
<div className="absolute inset-0 bg-gradient-to-br from-[#0A5240] to-[#0F6E56]" />
)}
<div className="relative flex h-full flex-col justify-end p-5 pb-12">
{story && (
<>
<p className="text-[11px] font-bold uppercase tracking-wider text-mint">
{story.emoji} {story.tag}
</p>
<h2 className="mt-2 font-display text-3xl leading-tight text-paper">{story.headline}</h2>
<p className="mt-2 text-sm leading-6 text-paper/75">{story.subline}</p>
</>
)}
<p className="mt-4 text-xs font-semibold text-paper/60">
Your goal is next. Aartha gets you there.
</p>
<button className="mt-4 rounded-lg bg-mint py-3 font-bold text-ink">Get started</button>
<button
onClick={() => setStory((s) => randomStory(s?.id))}
className="mt-3 inline-flex items-center justify-center gap-1.5 text-xs font-semibold text-paper/60"
>
<RefreshCcw size={12} /> See another story
</button>
</div>
</div>
);
}

51
components/ui/CountUp.tsx Normal file
View File

@ -0,0 +1,51 @@
"use client";
import { useEffect, useRef, useState } from "react";
/** Animates from 0 to `value` when scrolled into view. */
export function CountUp({
value,
prefix = "",
suffix = "",
decimals = 0,
duration = 1400,
}: {
value: number;
prefix?: string;
suffix?: string;
decimals?: number;
duration?: number;
}) {
const ref = useRef<HTMLSpanElement>(null);
const [display, setDisplay] = useState(0);
useEffect(() => {
const el = ref.current;
if (!el) return;
const io = new IntersectionObserver(
(entries) => {
if (!entries[0].isIntersecting) return;
io.disconnect();
const start = performance.now();
const tick = (now: number) => {
const p = Math.min((now - start) / duration, 1);
const eased = 1 - Math.pow(1 - p, 3); // ease-out cubic
setDisplay(value * eased);
if (p < 1) requestAnimationFrame(tick);
};
requestAnimationFrame(tick);
},
{ threshold: 0.4 }
);
io.observe(el);
return () => io.disconnect();
}, [value, duration]);
return (
<span ref={ref}>
{prefix}
{display.toLocaleString("en-US", { maximumFractionDigits: decimals, minimumFractionDigits: decimals })}
{suffix}
</span>
);
}

View File

@ -1,3 +1,21 @@
/** Compact leaf mark for dark surfaces (marketing nav, footer). */
export function LeafMark({ size = 26 }: { size?: number }) {
return (
<svg width={size} height={size} viewBox="0 0 32 32" fill="none" aria-hidden>
<path
d="M16 3C9 8 5.5 14 6.5 21.5 7.3 27 11.5 29.5 16 29.5c4.5 0 8.7-2.5 9.5-8C26.5 14 23 8 16 3Z"
fill="#1D9E75"
/>
<path
d="M16 8v18M16 14l-4.5-3.5M16 18l5-4"
stroke="#F6F0E7"
strokeWidth="1.6"
strokeLinecap="round"
/>
</svg>
);
}
export function Logo() {
return (
<div className="flex items-center gap-3">

98
lib/stories.ts Normal file
View File

@ -0,0 +1,98 @@
/**
* Goal stories real human moments behind the numbers.
* One is picked at random every time the page opens (web hero + mobile onboarding).
* Videos are free-license stock from Pexels (pexels.com/license). For production,
* replace with licensed brand footage by swapping the `video` URLs (or move files
* into /public/videos and use local paths).
*/
export type GoalStory = {
id: string;
tag: string;
headline: string;
subline: string;
video: string;
emoji: string;
/** Tailwind gradient classes used if the video fails or while it loads */
fallback: string;
};
export const goalStories: GoalStory[] = [
{
id: "retirement",
tag: "Goal achieved · Comfortable retirement",
headline: "After 34 years of showing up, Rita and Sam clocked out for good.",
subline: "Retired at 61 — two years early — with a plan that never wavered.",
video: "https://videos.pexels.com/video-files/7329851/7329851-sd_640_360_25fps.mp4",
emoji: "🌅",
fallback: "from-[#0A5240] via-[#0F6E56] to-[#B8771F]",
},
{
id: "first-home",
tag: "Goal achieved · First home",
headline: "Maya got the keys to her first home on a Tuesday.",
subline: "18 months of flex budgeting. One very good Tuesday.",
video: "https://videos.pexels.com/video-files/4277738/4277738-sd_640_360_25fps.mp4",
emoji: "🔑",
fallback: "from-[#073D30] via-[#0F6E56] to-[#534AB7]",
},
{
id: "rental-property",
tag: "Goal achieved · First rental property",
headline: "Dev closed on his first rental property this spring.",
subline: "Passive income started as one line item in his Aartha plan.",
video: "https://videos.pexels.com/video-files/4277475/4277475-sd_640_360_25fps.mp4",
emoji: "🏘️",
fallback: "from-[#0A5240] via-[#2E8B6A] to-[#1D9E75]",
},
{
id: "debt-free",
tag: "Goal achieved · Debt-free day",
headline: "The day the Osei family paid off the last of $42,000.",
subline: "Every extra dollar had a job. This was the payoff.",
video: "https://videos.pexels.com/video-files/7712328/7712328-sd_640_360_30fps.mp4",
emoji: "🎉",
fallback: "from-[#3E3792] via-[#534AB7] to-[#0F6E56]",
},
{
id: "college-fund",
tag: "Goal achieved · College fund",
headline: "Amara walked the stage completely debt-free.",
subline: "Her parents started the fund 12 years ago — $180 a month.",
video: "https://videos.pexels.com/video-files/7712350/7712350-sd_640_360_30fps.mp4",
emoji: "🎓",
fallback: "from-[#073D30] via-[#0F6E56] to-[#9BD9C5]",
},
{
id: "new-baby",
tag: "Goal achieved · Ready for baby",
headline: "Leo arrived. The emergency fund was already there.",
subline: "Nine months to prepare — financially calm on day one.",
video: "https://videos.pexels.com/video-files/7918438/7918438-sd_640_360_25fps.mp4",
emoji: "👶",
fallback: "from-[#0F6E56] via-[#2E8B6A] to-[#E8A23A]",
},
{
id: "small-business",
tag: "Goal achieved · Own boss",
headline: "Priya opened her studio's doors with zero business debt.",
subline: "She saved her way to independence, one month at a time.",
video: "https://videos.pexels.com/video-files/6685171/6685171-sd_640_360_30fps.mp4",
emoji: "🚀",
fallback: "from-[#96601A] via-[#B8771F] to-[#0F6E56]",
},
{
id: "dream-trip",
tag: "Goal achieved · Dream trip",
headline: "Three generations, one beach, fully paid for.",
subline: "The Reyes family trip fund hit 100% in June.",
video: "https://videos.pexels.com/video-files/7850307/7850307-sd_640_360_30fps.mp4",
emoji: "🏖️",
fallback: "from-[#0A5240] via-[#1D9E75] to-[#534AB7]",
},
];
export function randomStory(excludeId?: string): GoalStory {
const pool = goalStories.filter((s) => s.id !== excludeId);
return pool[Math.floor(Math.random() * pool.length)];
}