Frontend redesign: real type, hero, depth #7

Merged
justin merged 1 commits from design-overhaul into main 2026-06-06 21:34:48 -04:00
6 changed files with 131 additions and 52 deletions
+26 -14
View File
@@ -1,44 +1,56 @@
@import "tailwindcss"; @import "tailwindcss";
/* Brand palette (docs/brand): warm ink + bronze + paper. */ /* Brand palette + type (docs/brand): warm ink + bronze + paper, serif display. */
@theme { @theme {
--color-bronze: #a06a42; --color-bronze: #a06a42;
--color-bronze-deep: #8a5836; --color-bronze-deep: #8a5836;
--color-paper: #f7f3ec; --color-paper: #f7f3ec;
--color-ink: #1a1a17; --color-ink: #1a1a17;
--font-serif: Georgia, "Times New Roman", "Liberation Serif", ui-serif, serif; --font-sans: var(--font-inter), ui-sans-serif, system-ui, -apple-system, sans-serif;
--font-serif: var(--font-fraunces), Georgia, "Times New Roman", ui-serif, serif;
} }
/* Adaptive tokens (ink/paper flip for light/dark; bronze + paper are constant). */ /* Adaptive tokens ink/paper flip for light/dark; bronze + paper are constant. */
:root { :root {
--background: #f7f3ec; /* paper */ --background: #f7f3ec;
--foreground: #1a1a17; /* ink */ --foreground: #1a1a17;
--muted: #6b6862; --muted: #6b6862;
--surface: #fbf8f2; --surface: #fffdf9;
--border: #e4dccb; --border: #e6ddcc;
} }
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
:root { :root {
--background: #1a1a17; /* warm near-black */ --background: #161410;
--foreground: #f2eee6; /* warm off-white */ --foreground: #f2eee6;
--muted: #9a968e; --muted: #9a968e;
--surface: #232019; --surface: #211d17;
--border: #3a352c; --border: #353029;
} }
} }
body { body {
background: var(--background); /* A faint bronze warmth pooled at the top gives the flat paper some depth. */
background:
radial-gradient(
1100px 520px at 50% -8%,
color-mix(in srgb, var(--color-bronze) 9%, var(--background)),
var(--background) 60%
);
background-attachment: fixed;
color: var(--foreground); color: var(--foreground);
font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; font-family: var(--font-sans);
} }
/* Headings use the heritage serif register. */
h1, h1,
h2, h2,
h3, h3,
.font-serif { .font-serif {
font-family: var(--font-serif); font-family: var(--font-serif);
letter-spacing: -0.015em;
}
::selection {
background: color-mix(in srgb, var(--color-bronze) 22%, transparent);
} }
+29 -12
View File
@@ -1,38 +1,55 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
import { Fraunces, Inter } from "next/font/google";
import Link from "next/link"; import Link from "next/link";
import "./globals.css"; import "./globals.css";
// Heritage display serif + clean humanist sans (per docs/brand typography).
const serif = Fraunces({
subsets: ["latin"],
variable: "--font-fraunces",
display: "swap",
axes: ["opsz"],
});
const sans = Inter({ subsets: ["latin"], variable: "--font-inter", display: "swap" });
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Provenance", title: "Provenance — where it came from matters",
description: "Where it came from matters — family and land, every fact sourced.", description:
"Trace your family and your land in one place — every fact linked to the record it came from. Self-hosted, sourced, and yours to keep.",
icons: { icon: "/favicon.svg" }, icons: { icon: "/favicon.svg" },
}; };
export default function RootLayout({ children }: { children: React.ReactNode }) { export default function RootLayout({ children }: { children: React.ReactNode }) {
return ( return (
<html lang="en"> <html lang="en" className={`${serif.variable} ${sans.variable}`}>
<body className="flex min-h-screen flex-col"> <body className="flex min-h-screen flex-col antialiased">
<header className="border-b border-[var(--border)]"> <header className="sticky top-0 z-20 border-b border-[var(--border)] bg-[var(--background)]">
<div className="mx-auto flex max-w-3xl items-center justify-between px-4 py-3"> <div className="mx-auto flex max-w-5xl items-center justify-between px-5 py-3.5">
<Link href="/" className="flex items-center" aria-label="Provenance — home"> <Link href="/" className="flex items-center" aria-label="Provenance — home">
{/* eslint-disable-next-line @next/next/no-img-element */} {/* eslint-disable-next-line @next/next/no-img-element */}
<img src="/provenance-logo-plain.svg" alt="Provenance" className="h-7 w-auto" /> <img src="/provenance-logo-plain.svg" alt="Provenance" className="h-7 w-auto" />
</Link> </Link>
<nav className="flex gap-5 text-sm"> <nav className="flex items-center gap-6 text-sm">
<Link href="/trees" className="text-[var(--muted)] transition-colors hover:text-bronze"> <Link href="/trees" className="text-[var(--muted)] transition-colors hover:text-[var(--foreground)]">
Trees Trees
</Link> </Link>
<Link href="/login" className="text-[var(--muted)] transition-colors hover:text-bronze"> <Link
href="/login"
className="rounded-full border border-[var(--border)] px-4 py-1.5 font-medium transition-colors hover:border-bronze hover:text-bronze"
>
Sign in Sign in
</Link> </Link>
</nav> </nav>
</div> </div>
</header> </header>
<main className="mx-auto w-full max-w-3xl flex-1 px-4 py-10">{children}</main>
<main className="mx-auto w-full max-w-5xl flex-1 px-5 py-10">{children}</main>
<footer className="border-t border-[var(--border)]"> <footer className="border-t border-[var(--border)]">
<div className="mx-auto max-w-3xl px-4 py-6 text-sm italic text-[var(--muted)]"> <div className="mx-auto flex max-w-5xl flex-wrap items-center justify-between gap-2 px-5 py-6 text-sm text-[var(--muted)]">
where it came from matters <span className="font-serif text-base italic">where it came from matters</span>
<span>Self-hosted · source-available · your data, your infrastructure</span>
</div> </div>
</footer> </footer>
</body> </body>
+68 -18
View File
@@ -1,27 +1,77 @@
import { BadgeCheck, MapPin, ShieldCheck, Users } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
const features = [
{
icon: Users,
title: "Family and land, together",
body: "People, relationships, and life events alongside property and chain-of-title — one documented story of where you come from.",
},
{
icon: BadgeCheck,
title: "Sourced or it didn't happen",
body: "Every fact can carry a citation back to the record it came from. Sources are first-class, reusable, and visible.",
},
{
icon: ShieldCheck,
title: "Yours to keep",
body: "Self-hosted and source-available. Living people protected by default. Open formats — export anytime, run it anywhere.",
},
];
export default function Home() { export default function Home() {
return ( return (
<div className="space-y-8 py-4"> <div className="space-y-20 py-6 sm:py-12">
<div className="space-y-4"> <section className="grid items-center gap-10 sm:grid-cols-[1.3fr_1fr]">
<h1 className="text-4xl font-semibold tracking-tight sm:text-5xl"> <div>
Where it came from matters <p className="text-xs font-semibold uppercase tracking-[0.2em] text-bronze">
</h1> Family · Land · Provenance
<p className="max-w-prose text-lg text-[var(--muted)]"> </p>
Trace where you come from your family <span className="text-bronze">and</span> your <h1 className="mt-4 text-5xl font-semibold leading-[1.04] tracking-tight sm:text-6xl">
land with every fact linked to a source, on infrastructure you control. Where it came from{" "}
</p> <span className="italic text-bronze">matters</span>.
</div> </h1>
<div className="flex flex-wrap gap-3"> <p className="mt-6 max-w-xl text-lg leading-relaxed text-[var(--muted)]">
<Link href="/register"> Trace your family and your land in one place every name, every parcel, every claim
<Button>Create an account</Button> linked to the record it came from. Self-hosted, sourced, and yours to keep.
</Link> </p>
<Link href="/login"> <div className="mt-8 flex flex-wrap gap-3">
<Button variant="outline">Sign in</Button> <Link href="/register">
</Link> <Button size="lg">Create your account</Button>
</div> </Link>
<Link href="/login">
<Button size="lg" variant="outline">
Sign in
</Button>
</Link>
</div>
</div>
<div className="hidden justify-self-end sm:block">
<div className="relative grid h-64 w-64 place-items-center rounded-full border border-[var(--border)] bg-[var(--surface)] shadow-[0_24px_60px_-24px_rgba(160,106,66,0.35)]">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img src="/provenance-mark.svg" alt="" className="h-36 w-36" />
<MapPin className="absolute -right-2 top-10 h-7 w-7 text-bronze" />
</div>
</div>
</section>
<section className="grid gap-5 sm:grid-cols-3">
{features.map((f) => (
<div
key={f.title}
className="rounded-xl border border-[var(--border)] bg-[var(--surface)] p-6 shadow-[0_1px_2px_rgba(26,26,23,0.04)]"
>
<div className="grid h-10 w-10 place-items-center rounded-lg bg-bronze/12 text-bronze">
<f.icon className="h-5 w-5" />
</div>
<h2 className="mt-4 text-lg font-semibold">{f.title}</h2>
<p className="mt-2 text-sm leading-relaxed text-[var(--muted)]">{f.body}</p>
</div>
))}
</section>
</div> </div>
); );
} }
+6 -6
View File
@@ -4,19 +4,19 @@ import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
const buttonVariants = cva( const buttonVariants = cva(
"inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-bronze focus-visible:ring-offset-1 disabled:pointer-events-none disabled:opacity-50", "inline-flex items-center justify-center gap-2 rounded-lg font-medium transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-bronze focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--background)] disabled:pointer-events-none disabled:opacity-50",
{ {
variants: { variants: {
variant: { variant: {
// Bronze is the brand accent; paper reads cleanly on it. default: "bg-bronze text-paper shadow-sm hover:bg-bronze-deep hover:shadow",
default: "bg-bronze text-paper hover:bg-bronze-deep",
outline: outline:
"border border-bronze text-bronze bg-transparent hover:bg-bronze hover:text-paper", "border border-[var(--border)] bg-[var(--surface)] hover:border-bronze hover:text-bronze",
ghost: "text-[var(--foreground)] hover:bg-bronze/10", ghost: "text-[var(--foreground)] hover:bg-bronze/10",
}, },
size: { size: {
default: "h-10 px-4 py-2", default: "h-10 px-4 text-sm",
sm: "h-9 px-3", sm: "h-9 px-3 text-sm",
lg: "h-12 px-6 text-base",
}, },
}, },
defaultVariants: { variant: "default", size: "default" }, defaultVariants: { variant: "default", size: "default" },
+1 -1
View File
@@ -6,7 +6,7 @@ export function Card({ className, ...props }: React.HTMLAttributes<HTMLDivElemen
return ( return (
<div <div
className={cn( className={cn(
"rounded-lg border border-[var(--border)] bg-[var(--surface)] shadow-sm", "rounded-xl border border-[var(--border)] bg-[var(--surface)] shadow-[0_1px_2px_rgba(26,26,23,0.04),0_8px_24px_-12px_rgba(26,26,23,0.10)]",
className, className,
)} )}
{...props} {...props}
+1 -1
View File
@@ -7,7 +7,7 @@ export const Input = React.forwardRef<HTMLInputElement, React.InputHTMLAttribute
<input <input
ref={ref} ref={ref}
className={cn( className={cn(
"flex h-10 w-full rounded-md border border-[var(--border)] bg-[var(--surface)] px-3 py-2 text-sm placeholder:text-[var(--muted)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-bronze disabled:opacity-50", "flex h-10 w-full rounded-lg border border-[var(--border)] bg-[var(--surface)] px-3 py-2 text-sm placeholder:text-[var(--muted)] transition-colors focus-visible:border-bronze focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-bronze/40 disabled:opacity-50",
className, className,
)} )}
{...props} {...props}