import type { CSSProperties } from 'react'; /** * sammillan.com design system. * * One source of truth for the values that were previously redeclared inline in * roughly 25 page files. The scale is lifted from the homepage, which the July * easlo-calm pass established as the reference, not invented here. * * The site styles with inline `style={{}}` objects rather than utility classes. * That is the existing idiom and it is kept. What changes is that the values now * come from one place. * * Usage: * import { color, text, rule, link } from '@/lib/design'; *

Building

*

...

* * Compose with spread when a one-off tweak is needed: *

* * Mirrored in app/globals.css :root. If you change a colour here, change it there. * See DESIGN.md. */ // ── Type stacks ────────────────────────────────────────────────────────────── // Geist Mono everywhere since 24 Sep 2026, loaded in app/layout.tsx as // --font-geist-mono. `sans` and `mono` are the same stack; both keys stay so // existing imports keep working. export const font = { sans: 'var(--font-personal-inter), Arial, sans-serif', mono: 'var(--font-personal-inter), Arial, sans-serif', } as const; // ── Colour ─────────────────────────────────────────────────────────────────── export const color = { /** Page background. Giants background. */ bg: '#ffffff', /** Cards and raised surfaces. Giants dark_background. */ surface: '#f0f0f0', /** Secondary fill, for wells and inset blocks. Giants darker_background, recessed below the page. */ surfaceSunk: '#f0f0f0', /** Headings and primary text. */ ink: '#000000', /** Body copy. Softer than ink, passes AA on bg. */ body: '#5f5f5f', /** Secondary text and section labels. */ muted: '#5f5f5f', /** * Meta, captions, timestamps. * * Giants foreground. Minimum 7.82:1 across the four shared surfaces. * Keep the source dark_foreground out of small text; it fails on hover fills. */ faint: '#5f5f5f', /** Giants magenta, the text-safe terracotta (minimum 4.91:1 on shared surfaces). */ accent: '#000000', accentBorder: 'rgba(0,0,0,0.35)', /** Hover and glow. Giants bright_magenta. */ accentBright: '#303030', accentDim: 'rgba(0,0,0,0.12)', /** Semantic only. Marks something live or free. Not decorative. */ live: '#000000', /** Hairlines. The main structural device on this site. Foreground at low alpha. */ hair: 'rgba(0,0,0,0.08)', hairStrong: 'rgba(0,0,0,0.14)', hairHover: 'rgba(0,0,0,0.22)', } as const; /** Topic colours for indexes and wayfinding. Never use these as CTA colours. */ export const domainColor = { performance: '#000000', ownership: '#000000', experiments: '#000000', craft: '#000000', race: '#000000', reading: '#000000', } as const; export type Domain = keyof typeof domainColor; /** Low-alpha companions for semantic wayfinding surfaces. */ export const domainTint = { performance: 'rgba(0,0,0,0.12)', ownership: 'rgba(0,0,0,0.12)', experiments: 'rgba(0,0,0,0.12)', craft: 'rgba(0,0,0,0.12)', race: 'rgba(224,175,104,0.12)', reading: 'rgba(158,206,106,0.12)', } as const; // ── Spacing ────────────────────────────────────────────────────────────────── // Sparse on purpose. Whitespace is doing structural work, so the big steps get used most. export const space = { 1: '4px', 2: '8px', 3: '12px', 4: '16px', 5: '24px', 6: '32px', 7: '48px', 8: '64px', 9: '72px', 10: '96px', } as const; export const radius = { sm: '8px', md: '12px', lg: '12px', xl: '12px', } as const; export const ease = { smooth: 'cubic-bezier(0.32, 0.72, 0, 1)', quick: 'cubic-bezier(0.22, 1, 0.36, 1)', } as const; /** Readable measure. Long-form prose should never exceed this. */ export const measure = '68ch'; /** Tighter measure for lead paragraphs and intros. */ export const measureTight = '58ch'; // ── Type scale ─────────────────────────────────────────────────────────────── // Typography does most of the visual work on this site, so the scale is small // and the jumps are decisive. Prefer picking a named style over hand-setting px. export const text = { /** Homepage identity masthead. One use only, above the hero statement. */ masthead: { fontFamily: font.sans, fontSize: 'clamp(64px, 10vw, 96px)', fontWeight: 700, letterSpacing: '-0.035em', lineHeight: 1, color: color.ink, margin: 0, }, /** Page title. One per page. */ h1: { fontFamily: font.sans, fontSize: 'var(--site-h1-size)', fontWeight: 500, letterSpacing: '-.045em', lineHeight: 'var(--site-h1-line)', color: color.ink, margin: 0, }, /** Section heading inside a page. */ h2: { fontFamily: font.sans, fontSize: 'var(--site-h2-size)', fontWeight: 500, letterSpacing: '-.035em', lineHeight: 'var(--site-h2-line)', color: color.ink, margin: 0, }, h3: { fontFamily: font.sans, fontSize: 'var(--site-h3-size)', fontWeight: 500, letterSpacing: '-.025em', lineHeight: 'var(--site-h3-line)', color: color.ink, margin: 0, }, /** * The workhorse section marker. Small uppercase tracked label over a hairline. * This is the editorial signature of the site. Use it instead of a big bold * heading for homepage and index sections. */ label: { fontFamily: font.sans, fontSize: '13px', fontWeight: 600, letterSpacing: '0', textTransform: 'none', lineHeight: 1.2, color: color.muted, margin: 0, }, /** Opening statement. One per page, directly under the title. */ lead: { fontFamily: font.sans, fontSize: 'var(--site-lead-size)', fontWeight: 400, lineHeight: 'var(--site-lead-line)', color: color.body, maxWidth: measureTight, margin: 0, }, /** Long-form prose. */ body: { fontFamily: font.sans, fontSize: 'var(--site-body-size)', fontWeight: 400, lineHeight: 'var(--site-body-line)', color: color.body, maxWidth: measure, margin: 0, }, /** Supporting copy inside cards and rows. */ small: { fontFamily: font.sans, fontSize: '13px', fontWeight: 400, lineHeight: 1.55, color: color.muted, margin: 0, }, /** Dates, statuses, captions. */ meta: { fontFamily: font.sans, fontSize: '12px', fontWeight: 400, lineHeight: 1.5, color: color.faint, margin: 0, }, /** A single number carrying weight. Revenue, distance, a count. */ display: { fontFamily: font.sans, fontSize: '36px', fontWeight: 700, letterSpacing: '-0.035em', lineHeight: 1, color: color.ink, margin: 0, }, /** Status pills and live markers. Mono earns its place here. */ mono: { fontFamily: font.mono, fontSize: '11px', fontWeight: 500, letterSpacing: '0.04em', lineHeight: 1.4, color: color.faint, margin: 0, }, } satisfies Record; // ── Primitives ─────────────────────────────────────────────────────────────── /** Inline link inside prose. Ink text, ochre underline. */ export const link: CSSProperties = { color: color.ink, fontWeight: 500, textDecoration: 'underline', textDecorationColor: 'var(--accent-border)', textUnderlineOffset: '3px', }; /** * Quiet link for footers and meta rows. * * Underlined rather than bare. A link sitting inside faint meta text was only * 1.83:1 against the surrounding copy, so colour alone did not mark it as a * link. WCAG wants 3:1 or a non-colour cue. The underline is the cue. */ export const linkQuiet: CSSProperties = { color: color.muted, textDecoration: 'underline', textDecorationColor: 'rgba(0,0,0,0.18)', textUnderlineOffset: '2px', }; /** Hairline divider. */ export const rule: CSSProperties = { border: 0, borderTop: `1px solid ${color.hair}`, margin: 0, }; /** * Section heading treatment: label over a hairline, with the page's standard * gap beneath. Spread onto the heading element. */ export const sectionHead: CSSProperties = { ...text.label, paddingBottom: space[3], borderBottom: `1px solid ${color.hair}`, marginBottom: space[4], }; /** Restrained card. Hairline border, white fill, no shadow at rest. */ export const card: CSSProperties = { background: color.surface, border: `1px solid ${color.hair}`, borderRadius: radius.md, padding: space[5], }; /** Row in an index list. Hairline separated, no box. */ export const row: CSSProperties = { display: 'block', padding: `${space[4]} 0`, borderBottom: `1px solid ${color.hair}`, textDecoration: 'none', color: 'inherit', }; // ── Status ─────────────────────────────────────────────────────────────────── // Project and pillar statuses. Used on /building and /now. Keep the vocabulary // closed so the same thing is never called two names. export type Status = 'live' | 'building' | 'experiment' | 'archived' | 'planned'; export const statusLabel: Record = { live: 'Live', building: 'Building', experiment: 'Experiment', archived: 'Archived', planned: 'Planned', }; export const statusColor: Record = { live: color.live, building: color.accent, experiment: color.muted, archived: color.faint, planned: color.faint, };