[object Object]

← 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

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&rsquo;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 &ldquo;spec home listings&rdquo; 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&apos;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 &ldquo;Leander&rdquo; or &ldquo;85212&rdquo;"
+              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 →