← back to Homesonspec
Design system iter-1: @theme token layer (brand/accent/status ramps, Fraunces+Inter, radii/shadows), reskin app shell + hero from tokens
678cf48b58de1028957dacaa9db5d9f07eb67626 · 2026-07-22 17:27:34 -0700 · Steve
Files touched
M apps/web/src/app/globals.cssM apps/web/src/app/layout.tsxM apps/web/src/app/page.tsx
Diff
commit 678cf48b58de1028957dacaa9db5d9f07eb67626
Author: Steve <steve@designerwallcoverings.com>
Date: Wed Jul 22 17:27:34 2026 -0700
Design system iter-1: @theme token layer (brand/accent/status ramps, Fraunces+Inter, radii/shadows), reskin app shell + hero from tokens
---
apps/web/src/app/globals.css | 111 +++++++++++++++++++++++++++++++++++++++++++
apps/web/src/app/layout.tsx | 52 +++++++++++++-------
apps/web/src/app/page.tsx | 61 +++++++++++++++---------
3 files changed, 183 insertions(+), 41 deletions(-)
diff --git a/apps/web/src/app/globals.css b/apps/web/src/app/globals.css
index f1d8c73..5dacd86 100644
--- a/apps/web/src/app/globals.css
+++ b/apps/web/src/app/globals.css
@@ -1 +1,112 @@
@import "tailwindcss";
+
+/* ============================================================================
+ HomesOnSpec design system — token layer
+ These @theme tokens are the single source of truth. The Claude Design bundle
+ in /design-system mirrors them 1:1 so the synced component library and the
+ live app never drift. Tailwind 4 exposes each token as a real CSS custom
+ property (e.g. --color-brand-700) AND generates utilities (bg-brand-700).
+ ========================================================================== */
+@theme {
+ /* Type — Fraunces (display serif) + Inter (body). next/font injects the
+ --font-fraunces / --font-inter vars in layout.tsx. */
+ --font-sans: var(--font-inter), ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
+ --font-display: var(--font-fraunces), ui-serif, Georgia, "Times New Roman", serif;
+
+ /* Brand — deep teal / pine. Anchors the HomesOnSpec wordmark and conveys
+ trust ("Verified from the source"). */
+ --color-brand-50: #f0f9f8;
+ --color-brand-100: #d9efec;
+ --color-brand-200: #b4deda;
+ --color-brand-300: #83c5c0;
+ --color-brand-400: #4fa5a1;
+ --color-brand-500: #2f8783;
+ --color-brand-600: #226b69;
+ --color-brand-700: #1e5654;
+ --color-brand-800: #1b4645;
+ --color-brand-900: #123231;
+ --color-brand-950: #081e1e;
+
+ /* Accent — honey / amber. CTAs, "move-in ready", highlights. Warm counter
+ to the cool brand teal (new-construction optimism). */
+ --color-accent-50: #fff9ed;
+ --color-accent-100: #fef0d0;
+ --color-accent-200: #fbdd9f;
+ --color-accent-300: #f8c464;
+ --color-accent-400: #f4a934;
+ --color-accent-500: #ed8f16;
+ --color-accent-600: #d1700f;
+ --color-accent-700: #ae5310;
+ --color-accent-800: #8d4114;
+ --color-accent-900: #743714;
+
+ /* Semantic status — construction status markers, reused by the map legend. */
+ --color-status-ready: #16a34a; /* move-in ready */
+ --color-status-uc: #f59e0b; /* under construction */
+ --color-status-plan: #3b82f6; /* planned */
+
+ --radius-lg: 0.75rem;
+ --radius-xl: 1rem;
+ --radius-2xl: 1.5rem;
+
+ --shadow-card: 0 1px 2px rgba(16, 50, 49, 0.05), 0 8px 24px -14px rgba(16, 50, 49, 0.20);
+ --shadow-lift: 0 2px 8px rgba(16, 50, 49, 0.07), 0 28px 56px -24px rgba(16, 50, 49, 0.34);
+}
+
+/* ---------------------------------------------------------------------------
+ Component classes — the reusable primitives. Each maps to a card in the
+ Claude Design bundle (/design-system/components/*). Kept as thin @apply
+ compositions so the token layer stays the source of truth.
+ ------------------------------------------------------------------------- */
+@layer components {
+ /* Tailwind 4's @apply only accepts real utilities, so each variant is
+ self-contained rather than composing a shared .btn class. */
+ .btn-primary {
+ @apply inline-flex items-center justify-center gap-2 rounded-full px-5 py-2.5 text-sm font-semibold
+ transition focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2
+ bg-brand-700 text-white shadow-sm hover:bg-brand-800 focus-visible:ring-brand-300;
+ }
+ .btn-accent {
+ @apply inline-flex items-center justify-center gap-2 rounded-full px-5 py-2.5 text-sm font-semibold
+ transition focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2
+ bg-accent-500 text-brand-950 shadow-sm hover:bg-accent-400 focus-visible:ring-accent-300;
+ }
+ .btn-ghost {
+ @apply inline-flex items-center justify-center gap-2 rounded-full px-5 py-2.5 text-sm font-semibold
+ transition focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2
+ bg-white text-brand-800 ring-1 ring-inset ring-brand-200 hover:bg-brand-50 focus-visible:ring-brand-300;
+ }
+
+ .badge-verified {
+ @apply inline-flex items-center gap-1 rounded-full px-2.5 py-0.5 text-xs font-medium
+ bg-brand-50 text-brand-700 ring-1 ring-inset ring-brand-200;
+ }
+ .badge-ready {
+ @apply inline-flex items-center gap-1 rounded-full px-2.5 py-0.5 text-xs font-medium
+ bg-accent-100 text-accent-800;
+ }
+ .badge-neutral {
+ @apply inline-flex items-center gap-1 rounded-full px-2.5 py-0.5 text-xs font-medium
+ bg-neutral-100 text-neutral-600;
+ }
+
+ .card {
+ @apply rounded-2xl border border-neutral-200/70 bg-white shadow-[var(--shadow-card)];
+ }
+ .card-interactive {
+ @apply rounded-2xl border border-neutral-200/70 bg-white shadow-[var(--shadow-card)]
+ transition hover:-translate-y-0.5 hover:border-brand-300 hover:shadow-[var(--shadow-lift)];
+ }
+
+ .field {
+ @apply w-full rounded-xl border-0 bg-white px-4 py-3 text-neutral-900 shadow-sm
+ ring-1 ring-inset ring-neutral-200 focus:ring-2 focus:ring-brand-400;
+ }
+
+ .eyebrow {
+ @apply text-xs font-semibold uppercase tracking-[0.18em] text-brand-600;
+ }
+}
+
+/* Display headings pick up Fraunces with a tighter optical setting. */
+.font-display { font-optical-sizing: auto; letter-spacing: -0.01em; }
diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx
index 4851b23..8afd51a 100644
--- a/apps/web/src/app/layout.tsx
+++ b/apps/web/src/app/layout.tsx
@@ -1,7 +1,17 @@
import type { Metadata } from "next";
import Link from "next/link";
+import { Fraunces, Inter } from "next/font/google";
import "./globals.css";
+// Display serif + body sans, exposed as CSS variables the @theme layer reads.
+const display = Fraunces({
+ subsets: ["latin"],
+ variable: "--font-fraunces",
+ weight: ["400", "500", "600", "700"],
+ style: ["normal", "italic"],
+});
+const sans = Inter({ subsets: ["latin"], variable: "--font-inter" });
+
export const metadata: Metadata = {
title: "HomesOnSpec — Every new home. Every builder. One search.",
description:
@@ -10,32 +20,38 @@ export const metadata: Metadata = {
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
- <html lang="en">
- <body className="min-h-screen bg-white text-neutral-900 antialiased">
+ <html lang="en" className={`${display.variable} ${sans.variable}`}>
+ <body className="min-h-screen bg-neutral-50 font-sans text-neutral-900 antialiased">
{/* Live inventory only — every listing is real builder data with a verification label. */}
- <div className="bg-teal-800 px-4 py-1.5 text-center text-xs font-medium text-white">
- Early preview — every listing is verified from the builder’s own website and shows its verification
- label and last-verified time. New builders are being added continuously.
+ <div className="bg-brand-950 px-4 py-1.5 text-center text-xs font-medium text-brand-100">
+ Early preview — every listing is verified from the builder’s own website and shows its
+ verification label and last-verified time. New builders are added continuously.
</div>
- <header className="border-b border-neutral-200">
+
+ <header className="sticky top-0 z-[500] border-b border-neutral-200/70 bg-white/85 backdrop-blur">
<nav className="mx-auto flex max-w-7xl items-center justify-between px-4 py-3">
- <Link href="/" className="text-xl font-bold tracking-tight text-teal-800">
- HomesOnSpec<span className="text-neutral-400">.com</span>
+ <Link href="/" className="font-display text-xl font-semibold tracking-tight text-brand-800">
+ HomesOnSpec<span className="text-accent-500">.</span>
</Link>
- <div className="flex items-center gap-5 text-sm text-neutral-700">
- <Link href="/search" className="hover:text-teal-700">Search</Link>
- <Link href="/map" className="hover:text-teal-700">Map</Link>
- <Link href="/builders" className="hover:text-teal-700">Builders</Link>
- <Link href="/compare" className="hover:text-teal-700">Compare</Link>
- <Link href="/saved" className="hover:text-teal-700">Saved</Link>
- <Link href="/contact" className="hover:text-teal-700">Contact a builder</Link>
+ <div className="flex items-center gap-1 text-sm text-neutral-600">
+ <Link href="/search" className="rounded-full px-3 py-1.5 hover:bg-brand-50 hover:text-brand-700">Search</Link>
+ <Link href="/map" className="rounded-full px-3 py-1.5 hover:bg-brand-50 hover:text-brand-700">Map</Link>
+ <Link href="/builders" className="rounded-full px-3 py-1.5 hover:bg-brand-50 hover:text-brand-700">Builders</Link>
+ <Link href="/compare" className="rounded-full px-3 py-1.5 hover:bg-brand-50 hover:text-brand-700">Compare</Link>
+ <Link href="/saved" className="rounded-full px-3 py-1.5 hover:bg-brand-50 hover:text-brand-700">Saved</Link>
+ <Link href="/contact" className="btn-accent ml-2">Contact a builder</Link>
</div>
</nav>
</header>
+
<main>{children}</main>
- <footer className="mt-16 border-t border-neutral-200 py-8 text-center text-xs text-neutral-500">
- <p>Every new home. Every builder. One search. Verified from the source.</p>
- <p className="mt-1">
+
+ <footer className="mt-16 border-t border-neutral-200 bg-white py-10 text-center text-xs text-neutral-500">
+ <p className="font-display text-sm text-brand-800">
+ Every new home. Every builder. One search.
+ </p>
+ <p className="mt-1">Verified from the source.</p>
+ <p className="mt-3">
Builder inclusion never implies sponsorship. Organic results are never mixed with paid placement.
</p>
</footer>
diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx
index 50a99bc..b9129b0 100644
--- a/apps/web/src/app/page.tsx
+++ b/apps/web/src/app/page.tsx
@@ -45,16 +45,19 @@ export default async function HomePage() {
return (
<div>
- <section className="bg-gradient-to-b from-teal-900 to-teal-700 px-4 py-20 text-white">
- <div className="mx-auto max-w-3xl text-center">
- <h1 className="text-4xl font-bold tracking-tight sm:text-5xl">
- Every new home. Every builder. One search.
+ <section className="relative overflow-hidden bg-gradient-to-b from-brand-950 via-brand-900 to-brand-800 px-4 py-20 text-white">
+ {/* subtle radial glow behind the headline */}
+ <div className="pointer-events-none absolute inset-0 bg-[radial-gradient(60%_50%_at_50%_0%,rgba(244,169,52,0.14),transparent_70%)]" />
+ <div className="relative mx-auto max-w-3xl text-center">
+ <p className="eyebrow text-accent-300">Verified from the source</p>
+ <h1 className="mt-3 font-display text-4xl font-semibold leading-[1.05] tracking-tight sm:text-6xl">
+ Every new home.<br className="hidden sm:block" /> Every builder. One search.
</h1>
- <p className="mt-4 text-lg text-teal-100">
+ <p className="mt-5 text-lg text-brand-100">
Move-in-ready and under-construction homes, verified from the source.
</p>
- <p className="mx-auto mt-3 max-w-2xl text-sm text-teal-200">
- Search “spec home listings” today and you get scattered builder sites, generic portals, and
+ <p className="mx-auto mt-3 max-w-2xl text-sm text-brand-200/90">
+ Search “spec home listings” today and you get scattered builder sites, generic portals, and
blog posts — there is no one place to see them all. Builders list inventory on dozens of
separate websites that never meet. HomesOnSpec puts every builder's available inventory in
one verified search.
@@ -63,46 +66,58 @@ export default async function HomePage() {
<input
type="text"
name="q"
- placeholder="City, ZIP, or community — try “Leander” or “85212”"
- className="w-full rounded-lg border-0 px-4 py-3 text-neutral-900 shadow-lg focus:ring-2 focus:ring-teal-300"
+ placeholder="City, ZIP, or community — try “Leander” or “85212”"
+ className="field flex-1 shadow-lg ring-0"
/>
- <button
- type="submit"
- className="rounded-lg bg-white px-6 py-3 font-semibold text-teal-800 shadow-lg hover:bg-teal-50"
- >
+ <button type="submit" className="btn-accent px-6 py-3 text-base shadow-lg">
Search
</button>
</form>
- <p className="mt-4 text-sm text-teal-200">
- {homeCount.toLocaleString()} verified homes · {builderCount} builders · {markets.length} metros
+ <p className="mt-5 text-sm text-brand-200">
+ <span className="font-semibold text-white">{homeCount.toLocaleString()}</span> verified homes
+ {" · "}<span className="font-semibold text-white">{builderCount}</span> builders
+ {" · "}<span className="font-semibold text-white">{markets.length}</span> metros
</p>
</div>
{/* Live national map — loads on page load, one marker per community with
available inventory; click a marker to open that community. */}
- <div className="mx-auto mt-10 max-w-6xl">
- <div className="h-[420px] w-full overflow-hidden rounded-2xl border border-teal-600/40 shadow-2xl" data-testid="hero-map">
+ <div className="relative mx-auto mt-12 max-w-6xl">
+ <div
+ className="h-[440px] w-full overflow-hidden rounded-2xl border border-brand-600/40 shadow-[var(--shadow-lift)]"
+ data-testid="hero-map"
+ >
<HeroMap markers={heroMarkers} />
</div>
- <p className="mt-2 text-center text-xs text-teal-200/80">
+ <p className="mt-3 text-center text-xs text-brand-200/80">
Every marker is a community with live, verified inventory.{" "}
- <Link href="/map" className="font-semibold text-white underline underline-offset-2 hover:text-teal-100">
+ <Link href="/map" className="font-semibold text-accent-300 underline underline-offset-2 hover:text-accent-200">
Open the full map explorer →
</Link>
</p>
</div>
</section>
- <section className="mx-auto max-w-7xl px-4 py-12">
- <h2 className="text-2xl font-semibold">Browse by market</h2>
+ <section className="mx-auto max-w-7xl px-4 py-14">
+ <div className="flex items-end justify-between">
+ <div>
+ <p className="eyebrow">Where inventory is live</p>
+ <h2 className="mt-1 font-display text-2xl font-semibold text-brand-900">Browse by market</h2>
+ </div>
+ <Link href="/map" className="hidden text-sm font-semibold text-brand-700 hover:text-brand-800 sm:block">
+ See all on the map →
+ </Link>
+ </div>
<div className="mt-6 grid gap-4 sm:grid-cols-3">
{markets.map((market) => (
<Link
key={`${market.metro}-${market.state}`}
href={`/search?q=${encodeURIComponent(market.metro?.split("-")[0] ?? "")}`}
- className="rounded-xl border border-neutral-200 p-6 shadow-sm transition hover:border-teal-400 hover:shadow-md"
+ className="card-interactive group p-6"
>
- <div className="text-lg font-semibold">{market.metro}</div>
+ <div className="font-display text-lg font-semibold text-brand-900 group-hover:text-brand-700">
+ {market.metro}
+ </div>
<div className="mt-1 text-sm text-neutral-500">
{market.state} · {market._count._all} communities
</div>
← be45764 auto-save: 2026-07-22T17:15:39 (2 files) — deploy-heromap.sh
·
back to Homesonspec
·
Design system iter-1: Claude Design bundle (tokens + buttons 8b8126d →