commit 58707e5b62fc6a30b43b5b728ad3bedbb1dc1ada Author: jules Date: Tue Jul 7 22:35:56 2026 +1000 lib-theme-hearth: initial import (warm-consumer set, 2026-07) Includes the 2026-07-07 UX sweep: neutral inline-code chips (accent reserved for actions/links/focus), dark-mode card/popover lightness lift for visible surface separation, 44px touch-target floor on coarse pointers, card-title pinned to a named ramp role. Co-Authored-By: Claude Fable 5 diff --git a/README.md b/README.md new file mode 100644 index 0000000..41c0af0 --- /dev/null +++ b/README.md @@ -0,0 +1,41 @@ +# lib-theme-hearth + +**Evening warmth, dark-first.** Hearth is built on the contrarian warm-consumer +thesis that consumer warmth is not daylight — it's evening: reading by a fire, +lamplight, the phone at 9pm. Dark mode is the theme's native identity — deep +umber/espresso surfaces (never blue-gray, never black) that step lighter as they +come forward like objects catching firelight, with a candle-glow amber primary +carrying dark ink. Light mode is the morning after: a chroma-free off-white with +warm espresso ink and the same ember accent, deliberately outside the +saturated-AI cream band. A single slate-teal info counterpoint keeps the palette +from suffocating; motion is slow-blink calm (350ms base, decelerate-heavy). + +## Dark-first + +Consuming apps should **default this theme to dark mode** (and still respect an +explicit user toggle). Light mode is fully defined and contract-passing, but it +is the secondary register — the theme's identity lives in `.dark`. + +## Fonts + +Load from the consuming app's entry CSS (very top, before anything else): + +```css +@import url("https://fonts.googleapis.com/css2?family=Bitter:wght@400;500;600;700&family=Nunito+Sans:wght@400;600;700&family=JetBrains+Mono:wght@400;500&display=swap"); +``` + +- **Bitter** (400/500/600/700) — `--font-heading`, warm slab-serif headings +- **Nunito Sans** (400/600/700) — `--font-sans`, highly legible body +- **JetBrains Mono** (400/500) — `--font-mono`, code + +Per the token contract, `theme.css` never `@import`s fonts itself. + +## Validation + +```bash +node ../create-crema-app/scripts/validate-theme.mjs theme.css +``` + +Passes the Crema token contract v1: full Tier-1 token set in both modes, all +contrast floors met (including dark-mode `--muted-foreground` on `--card` at +7.68:1), inert glass stubs, springs aliased to standard eases. diff --git a/theme.css b/theme.css new file mode 100644 index 0000000..3fd1f77 --- /dev/null +++ b/theme.css @@ -0,0 +1,392 @@ +/* + * Hearth — evening warmth, dark-first. The room after sunset: deep umber and + * espresso surfaces that step lighter as they come toward you, the way + * furniture catches firelight; a candle-glow amber primary; one slate-teal + * counterpoint so the warmth never suffocates. Dark mode is the theme's + * native identity — light mode is the morning after, a chroma-free off-white + * with warm espresso ink and the same ember accent, deliberately outside the + * saturated-AI cream band. Built for consumer evenings: reading, journaling, + * the phone at 9pm. + * + * Palette rationale: every neutral lives on the umber axis (hue 40–70), never + * blue-gray and never pure black — the darks are browns with the lights + * turned down, so elevation can read as firelight (a faint warm rim plus a + * soft umber ambient) instead of hard black drop shadows. The amber primary + * carries dark ink, not white — dark-ink-on-bright is the contrast pattern + * throughout. + * + * Token contract matches lib-theme-pristine/theme.css. + */ + +/* + * Fonts: consuming apps load Bitter + Nunito Sans + JetBrains Mono at the top + * of their entry CSS via the Google Fonts URL documented in this theme's + * README. No @import here — PostCSS ordering forbids it once the theme is + * imported after other statements. + */ + +:root { + --font-sans: "Nunito Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + --font-heading: "Bitter", Georgia, "Times New Roman", serif; + --font-mono: "JetBrains Mono", ui-monospace, Menlo, Consolas, monospace; + + /* Tailwind utility families — distinct names so a consuming app's + * `@theme inline` can indirect to these. Mirror --font-heading/--font-sans. */ + --heading-family: "Bitter", Georgia, "Times New Roman", serif; + --body-family: "Nunito Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + + /* ── Colors (light — the morning after) ── + * True chroma-0 off-white body, warm espresso ink, ember primary. + * The background deliberately avoids the warm-cream band; warmth is + * carried by the ink and the ember, not the paper. */ + --background: oklch(0.975 0 0); + --foreground: oklch(0.22 0.02 50); + + --card: oklch(1 0 0); + --card-foreground: oklch(0.22 0.02 50); + + --popover: oklch(0.99 0 0); + --popover-foreground: oklch(0.22 0.02 50); + + /* Ember — burnt orange deep enough for near-white ink by day. */ + --primary: oklch(0.48 0.15 40); + --primary-foreground: oklch(0.98 0.008 80); + + --secondary: oklch(0.93 0.008 60); + --secondary-foreground: oklch(0.3 0.02 50); + + --muted: oklch(0.94 0.006 60); + --muted-foreground: oklch(0.45 0.02 55); + + --accent: oklch(0.93 0.03 55); + --accent-foreground: oklch(0.42 0.12 45); + + --destructive: oklch(0.52 0.2 28); + --destructive-foreground: oklch(0.98 0.008 80); + --success: oklch(0.62 0.15 150); + --success-foreground: oklch(0.15 0.03 150); + --warning: oklch(0.75 0.13 75); + --warning-foreground: oklch(0.22 0.05 75); + /* Info — the one cool counterpoint: slate-teal. */ + --info: oklch(0.48 0.09 210); + --info-foreground: oklch(0.97 0.008 210); + + --border: oklch(0.22 0.02 50 / 0.12); + --input: oklch(0.22 0.02 50 / 0.12); + --ring: oklch(0.48 0.15 40 / 0.5); + + /* Chart palette — ember / slate-teal / sage / heather / brick. + * Hues ≥60° apart, lightness staggered between neighbours. */ + --chart-1: oklch(0.65 0.13 60); + --chart-2: oklch(0.55 0.09 200); + --chart-3: oklch(0.6 0.11 135); + --chart-4: oklch(0.58 0.1 295); + --chart-5: oklch(0.55 0.12 20); + + --sidebar: oklch(0.95 0.003 60); + --sidebar-foreground: oklch(0.22 0.02 50); + --sidebar-primary: oklch(0.48 0.15 40); + --sidebar-primary-foreground: oklch(0.98 0.008 80); + --sidebar-accent: oklch(0.92 0.025 55); + --sidebar-accent-foreground: oklch(0.42 0.12 45); + --sidebar-border: oklch(0.22 0.02 50 / 0.08); + --sidebar-ring: oklch(0.48 0.15 40 / 0.5); + + --syntax-keyword: oklch(0.48 0.15 40); + --syntax-string: oklch(0.5 0.11 150); + --syntax-number: oklch(0.52 0.13 60); + --syntax-comment: oklch(0.55 0.015 60); + --syntax-function: oklch(0.48 0.09 230); + --syntax-type: oklch(0.5 0.08 195); + + /* ── Chat surfaces ── + * User bubble is a filled deep-ember panel with near-white ink; + * assistant bubble is a card surface with a hairline border. */ + --chat-user-bg: oklch(0.46 0.14 40); + --chat-user-fg: oklch(0.98 0.008 80); + --chat-assistant-bg: oklch(1 0 0); + --chat-assistant-fg: oklch(0.22 0.02 50); + --chat-assistant-border: oklch(0.22 0.02 50 / 0.12); + + /* Generous-cozy radius — armchair, not app-store icon. */ + --radius: 1.0625rem; + + /* ── Motion ── slow-blink calm: unhurried base, decelerate-heavy. + * Springs alias to the gentle standard eases — personality lives in + * the values, never in absence. */ + --duration-fast: 150ms; + --duration-base: 350ms; + --duration-slow: 500ms; + --duration-slower: 700ms; + --duration-spring: 550ms; + + --ease-standard: cubic-bezier(0.25, 0.05, 0.2, 1); + --ease-decelerate: cubic-bezier(0.05, 0.7, 0.1, 1); + --ease-accelerate: cubic-bezier(0.3, 0, 1, 1); + --ease-emphasized: cubic-bezier(0.2, 0, 0, 1); + + --ease-spring-gentle: var(--ease-standard); + --ease-spring-snappy: var(--ease-standard); + --ease-spring-bouncy: var(--ease-decelerate); + + /* ── Typography ── product register: fixed rem, full ramp. + * Bitter for headings (warm slab-serif), Nunito Sans for body. + * Body leans slightly large with generous leading — evening reading. */ + --text-hero-size: 3.25rem; + --text-hero-lh: 1.08; + --text-display-size: 2.75rem; + --text-display-lh: 1.1; + --text-headline-size: 2rem; + --text-headline-lh: 1.15; + --text-title-size: 1.5rem; + --text-title-lh: 1.25; + --text-subtitle-size: 1.1875rem; + --text-subtitle-lh: 1.5; + --text-body-size: 1.0625rem; + --text-body-lh: 1.65; + --text-label-size: 0.875rem; + --text-label-lh: 1.25rem; + --text-caption-size: 0.75rem; + --text-caption-lh: 1rem; + + /* ── Elevation (light) ── soft warm-umber paper shadows. */ + --elevation-0: none; + --elevation-1: 0 1px 2px oklch(0.22 0.02 50 / 0.06); + --elevation-2: 0 1px 3px oklch(0.22 0.02 50 / 0.08), 0 1px 2px oklch(0.22 0.02 50 / 0.05); + --elevation-3: 0 4px 10px -2px oklch(0.22 0.02 50 / 0.09), 0 2px 4px -2px oklch(0.22 0.02 50 / 0.05); + --elevation-4: 0 12px 26px -4px oklch(0.22 0.02 50 / 0.11), 0 4px 8px -4px oklch(0.22 0.02 50 / 0.05); + --elevation-5: 0 22px 44px -8px oklch(0.22 0.02 50 / 0.18); + + --spacing: 0.25rem; + + --border-width-thin: 0.5px; + --border-width-base: 1px; + --border-width-thick: 2px; + --border-width-heavy: 4px; + + /* No glass — inert values so consuming apps referencing these get no-ops. */ + --glass-blur: 0px; + --glass-saturate: 100%; + --glass-brightness: 1; + --glass-contrast: 1; +} + +/* + * Dark mode — the hero. Deep espresso umber (never blue-gray, never black); + * surfaces step LIGHTER as they come forward, like objects catching + * firelight. Primary flips to candle-glow amber carrying dark umber ink. + */ +.dark { + --background: oklch(0.17 0.018 55); + --foreground: oklch(0.93 0.02 75); + + --card: oklch(0.23 0.02 55); + --card-foreground: oklch(0.93 0.02 75); + + --popover: oklch(0.25 0.022 55); + --popover-foreground: oklch(0.93 0.02 75); + + /* Candle-glow amber; dark-ink-on-bright beats white-on-mid-tone. */ + --primary: oklch(0.78 0.145 65); + --primary-foreground: oklch(0.22 0.05 55); + + --secondary: oklch(0.28 0.025 55); + --secondary-foreground: oklch(0.9 0.02 75); + + --muted: oklch(0.26 0.02 55); + --muted-foreground: oklch(0.74 0.03 70); + + --accent: oklch(0.3 0.045 60); + --accent-foreground: oklch(0.9 0.07 72); + + --destructive: oklch(0.68 0.19 30); + --destructive-foreground: oklch(0.15 0.04 30); + --success: oklch(0.72 0.15 145); + --success-foreground: oklch(0.16 0.03 145); + --warning: oklch(0.8 0.14 80); + --warning-foreground: oklch(0.2 0.05 80); + /* Slate-teal counterpoint, dark ink on top. */ + --info: oklch(0.72 0.1 200); + --info-foreground: oklch(0.15 0.03 210); + + --border: oklch(0.95 0.03 75 / 0.12); + --input: oklch(0.95 0.03 75 / 0.12); + --ring: oklch(0.78 0.145 65 / 0.5); + + --chart-1: oklch(0.78 0.13 65); + --chart-2: oklch(0.7 0.09 200); + --chart-3: oklch(0.74 0.11 135); + --chart-4: oklch(0.72 0.09 295); + --chart-5: oklch(0.68 0.1 20); + + /* Sidebar sits a step further from the fire — darker than the page. */ + --sidebar: oklch(0.15 0.016 55); + --sidebar-foreground: oklch(0.9 0.02 75); + --sidebar-primary: oklch(0.78 0.145 65); + --sidebar-primary-foreground: oklch(0.22 0.05 55); + --sidebar-accent: oklch(0.26 0.04 60); + --sidebar-accent-foreground: oklch(0.9 0.07 72); + --sidebar-border: oklch(0.95 0.03 75 / 0.08); + --sidebar-ring: oklch(0.78 0.145 65 / 0.5); + + --syntax-keyword: oklch(0.8 0.13 65); + --syntax-string: oklch(0.76 0.12 145); + --syntax-number: oklch(0.82 0.12 80); + --syntax-comment: oklch(0.6 0.02 65); + --syntax-function: oklch(0.74 0.09 210); + --syntax-type: oklch(0.76 0.07 180); + + /* ── Chat surfaces (dark) ── */ + --chat-user-bg: oklch(0.44 0.11 50); + --chat-user-fg: oklch(0.97 0.015 80); + --chat-assistant-bg: oklch(0.26 0.022 55); + --chat-assistant-fg: oklch(0.93 0.02 75); + --chat-assistant-border: oklch(0.95 0.03 75 / 0.1); + + /* ── Elevation (dark) — firelight, not drop shadow ── + * Each level pairs a faint warm rim on the top edge (the surface + * catching the glow) with a soft deep-umber ambient — never pure black. + * Lift is mostly conveyed by the lighter surface itself. */ + --elevation-1: inset 0 1px 0 oklch(0.9 0.06 70 / 0.05), 0 1px 2px oklch(0.08 0.015 50 / 0.45); + --elevation-2: inset 0 1px 0 oklch(0.9 0.06 70 / 0.07), 0 2px 6px oklch(0.08 0.015 50 / 0.5); + --elevation-3: inset 0 1px 0 oklch(0.9 0.06 70 / 0.09), 0 6px 14px -2px oklch(0.08 0.015 50 / 0.55); + --elevation-4: inset 0 1px 0 oklch(0.9 0.06 70 / 0.1), 0 12px 28px -4px oklch(0.08 0.015 50 / 0.6); + --elevation-5: inset 0 1px 0 oklch(0.9 0.06 70 / 0.12), 0 22px 48px -8px oklch(0.08 0.015 50 / 0.7); +} + +/* + * @layer base — Hearth's evening philosophy. + * + * No glass, no hard chrome. Slab-serif headings, generous body leading, + * surfaces separated by warmth (lighter = closer) rather than heavy shadow. + * Interactions settle slowly — nothing snaps in a room lit by a fire. + */ +@layer base { + body { + min-height: 100vh; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + font-family: var(--font-sans); + font-size: var(--text-body-size); + line-height: var(--text-body-lh); + letter-spacing: 0.002em; + } + + h1, h2, h3, h4, + [data-slot="card-title"], + [data-slot="dialog-title"], + [data-slot="sheet-title"], + [data-slot="alert-dialog-title"] { + font-family: var(--font-heading); + font-weight: 600; + letter-spacing: -0.01em; + color: var(--foreground); + } + + code, kbd, samp, pre, + [data-slot="code"], + [data-slot="code-block"] { + font-family: var(--font-mono); + } + + /* Inline code — neutral ink on a warm tinted chip; the ember stays + * reserved for actions, links and focus. */ + :not(pre) > code { + padding: 0.1em 0.35em; + border-radius: calc(var(--radius) * 0.35); + background: color-mix(in oklch, var(--foreground) 7%, transparent); + color: var(--foreground); + font-size: 0.9em; + } + + /* Selection — an ember wash, like lamplight across the line. */ + ::selection { + background: color-mix(in oklch, var(--primary) 26%, transparent); + color: var(--foreground); + } + + a { + color: var(--accent-foreground); + text-decoration-color: color-mix(in oklch, var(--primary) 40%, transparent); + text-underline-offset: 0.2em; + transition: text-decoration-color var(--duration-fast) var(--ease-standard); + } + a:hover { + text-decoration-color: var(--primary); + } + + /* Cards & overlay surfaces — a hairline plus the firelight elevation. + * In dark mode the lighter surface does most of the lifting; the shadow + * is an accent, not the mechanism. */ + [data-slot="card"], + [data-slot="popover-content"], + [data-slot="dropdown-menu-content"], + [data-slot="context-menu-content"], + [data-slot="hover-card-content"], + [data-slot="select-content"], + [data-slot="command"], + [data-slot="tooltip-content"], + [data-slot="sheet-content"], + [data-slot="dialog-content"], + [data-slot="alert-dialog-content"], + [data-slot="drawer-content"] { + border: var(--border-width-base) solid var(--border); + box-shadow: var(--elevation-2); + } + + [data-slot="dialog-content"], + [data-slot="alert-dialog-content"], + [data-slot="sheet-content"], + [data-slot="drawer-content"] { + box-shadow: var(--elevation-4); + } + + [data-slot="sidebar"] { + background: var(--sidebar); + border-right: var(--border-width-base) solid var(--sidebar-border); + } + + /* Buttons — settle, don't snap. */ + [data-slot="button"] { + font-weight: 600; + transition: + opacity var(--duration-fast) var(--ease-standard), + transform var(--duration-fast) var(--ease-standard); + } + [data-slot="button"]:hover:not(:disabled) { + opacity: 0.9; + } + [data-slot="button"]:active:not(:disabled) { + opacity: 0.82; + transform: scale(0.99); + } + [data-slot="button"]:focus-visible { + outline: 2px solid var(--ring); + outline-offset: 2px; + } +} + +/* + * Unlayered control tuning — outside @layer base so it beats Tailwind's + * utility defaults shipped with shadcn primitives. + */ + +/* Card titles — pinned to the subtitle role so the scale is the theme's + * decision, not the card primitive's default. */ +[data-slot="card-title"] { + font-size: var(--text-subtitle-size); + line-height: var(--text-subtitle-lh); +} + +/* Touch targets — desktop keeps its density; coarse pointers get the + * 44px floor on every control. */ +@media (pointer: coarse) { + [data-slot="button"] { + min-height: 2.75rem; + min-width: 2.75rem; + } + [data-slot="input"], + [data-slot="select-trigger"] { + min-height: 2.75rem; + } +}