commit 8f6afab89ed2d2dff99810f43b733ae3eb692f9c Author: jules Date: Tue Jul 7 22:35:56 2026 +1000 lib-theme-siena: 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..5b954c5 --- /dev/null +++ b/README.md @@ -0,0 +1,47 @@ +# lib-theme-siena + +**Siena** — drenched Mediterranean courtyard. Warmth carried by the surface itself: +the background is a real terracotta wall (not a tinted near-white), cards are +sun-bleached plaster panels resting on it, and the ink is deep warm olive-brown. +Primary is burnt sienna; one cool Mediterranean blue is spent on accent and info +so the heat has a counterpoint. Dark mode is the same courtyard at dusk — umber-brown +surfaces, terracotta turned to glow. Fraunces display voice over Albert Sans, +modest 0.75rem corners, low warm shadows: things rest on the wall, they don't float. + +## Fonts + +Consuming apps must load the theme's fonts at the very top of their entry CSS +(`app/app.css`), before any other statement: + +```css +@import url("https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,500;9..144,600;9..144,700&family=Albert+Sans:ital,wght@0,400;0,500;0,600;0,700;1,400&family=JetBrains+Mono:wght@400;500&display=swap"); +``` + +- **Fraunces** (500/600/700, variable optical size) — `--font-heading` / `--heading-family` +- **Albert Sans** (400/500/600/700 + 400 italic) — `--font-sans` / `--body-family` +- **JetBrains Mono** (400/500) — `--font-mono` + +Do not `@import` fonts inside `theme.css` — per the token contract, fonts load +from the consuming app's entry CSS only. + +## Usage + +Clone as a sibling lib and point the app's theme import at it: + +```css +/* app/app.css */ +@import "../../lib-theme-siena/theme.css"; +``` + +## Contract + +Passes the Crema token contract v1: + +```bash +node create-crema-app/scripts/validate-theme.mjs lib-theme-siena/theme.css +``` + +All Tier-1 tokens defined in both `:root` and `.dark`; contrast floors verified +(foreground/background 10.0:1 light / 14.1:1 dark; every paired `-foreground` +≥ 5.3:1). Glass tokens are inert stubs; springs alias to standard easings +(calm-theme pattern); no `font-size` on root. diff --git a/theme.css b/theme.css new file mode 100644 index 0000000..4aab566 --- /dev/null +++ b/theme.css @@ -0,0 +1,392 @@ +/* + * Siena — drenched Mediterranean courtyard. A Sienese piazza at five in the + * afternoon: the background is a real terracotta wall, not a tinted white; + * cards are sun-bleached plaster panels resting ON that wall; the ink is a + * deep warm olive-brown that never goes black. Warmth lives in the surface + * itself — hierarchy is done by material (plaster on clay), not by shadow + * theatrics or balloon corners. + * + * Palette rationale: burnt sienna carries the primary action (deep enough for + * plaster-white labels at 6.9:1); worn-stone secondaries and an ochre warning + * stay inside the courtyard's heat; one cool note — a Mediterranean blue — is + * spent on accent and info so the warmth has something to push against. Dark + * mode is the same courtyard at dusk: umber-brown surfaces (never blue-gray) + * with the terracotta turned into glow. + * + * Token contract matches lib-theme-pristine/theme.css. + */ + +/* + * Fonts: consuming apps load Fraunces + Albert 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 — and no URL smuggled + * in comments for build scripts to lift. + */ + +:root { + --font-sans: "Albert Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + --font-heading: "Fraunces", Georgia, 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: "Fraunces", Georgia, serif; + --body-family: "Albert Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + + /* ── Colors ── terracotta field, plaster cards, olive-brown ink. + * The background commits: L 0.84, chroma 0.055 at hue 55 — a painted + * wall, not the pale cream band. */ + --background: oklch(0.84 0.055 55); + --foreground: oklch(0.24 0.03 60); + + --card: oklch(0.96 0.018 75); + --card-foreground: oklch(0.24 0.03 60); + + --popover: oklch(0.97 0.015 75); + --popover-foreground: oklch(0.24 0.03 60); + + /* Burnt sienna — deep enough to carry plaster-white labels (6.9:1). */ + --primary: oklch(0.46 0.13 38); + --primary-foreground: oklch(0.97 0.02 75); + + /* Worn stone — a step off the wall, still warm. */ + --secondary: oklch(0.9 0.03 68); + --secondary-foreground: oklch(0.28 0.035 55); + + --muted: oklch(0.89 0.035 65); + --muted-foreground: oklch(0.38 0.045 55); + + /* The one cool note — washed Mediterranean blue. */ + --accent: oklch(0.88 0.04 230); + --accent-foreground: oklch(0.3 0.08 240); + + --destructive: oklch(0.48 0.18 28); + --destructive-foreground: oklch(0.97 0.02 75); + /* Olive success — courtyard green, dark olive ink on top. */ + --success: oklch(0.62 0.11 130); + --success-foreground: oklch(0.18 0.05 135); + /* Ochre warning — sun pigment, umber ink label. */ + --warning: oklch(0.76 0.13 80); + --warning-foreground: oklch(0.24 0.06 80); + /* Info shares the accent's Mediterranean blue, deepened for plaster text. */ + --info: oklch(0.46 0.09 245); + --info-foreground: oklch(0.97 0.02 75); + + /* Hairlines are ink washes — on a colored wall they need a little more + * presence than they would on white. */ + --border: oklch(0.24 0.03 60 / 0.2); + --input: oklch(0.24 0.03 60 / 0.2); + --ring: oklch(0.46 0.13 38 / 0.5); + + /* Chart palette — terracotta / olive / sea blue / ochre / plum. + * Hues ≥60° apart, adjacent lightness varied. */ + --chart-1: oklch(0.58 0.13 40); + --chart-2: oklch(0.68 0.1 125); + --chart-3: oklch(0.55 0.1 240); + --chart-4: oklch(0.75 0.12 85); + --chart-5: oklch(0.62 0.09 320); + + /* Sidebar — a deeper stripe of the same wall, in shadow. */ + --sidebar: oklch(0.8 0.065 50); + --sidebar-foreground: oklch(0.24 0.03 60); + --sidebar-primary: oklch(0.46 0.13 38); + --sidebar-primary-foreground: oklch(0.97 0.02 75); + --sidebar-accent: oklch(0.86 0.05 55); + --sidebar-accent-foreground: oklch(0.38 0.12 38); + --sidebar-border: oklch(0.24 0.03 60 / 0.16); + --sidebar-ring: oklch(0.46 0.13 38 / 0.5); + + --syntax-keyword: oklch(0.44 0.13 38); + --syntax-string: oklch(0.46 0.09 135); + --syntax-number: oklch(0.5 0.12 60); + --syntax-comment: oklch(0.5 0.03 60); + --syntax-function: oklch(0.44 0.1 245); + --syntax-type: oklch(0.46 0.07 195); + + /* ── Chat surfaces ── + * Contract for lib-chat-ui / lib-agent-ui / lib-agent-dock-ui. User + * bubble is filled burnt sienna with plaster text; assistant bubble is + * a plaster card with an ink hairline. */ + --chat-user-bg: oklch(0.44 0.13 38); + --chat-user-fg: oklch(0.97 0.02 75); + --chat-assistant-bg: oklch(0.96 0.018 75); + --chat-assistant-fg: oklch(0.24 0.03 60); + --chat-assistant-border: oklch(0.24 0.03 60 / 0.16); + + /* Modest, honest corners — softness comes from color, not radius. */ + --radius: 0.75rem; + + /* ── Motion ── settled and unhurried, still product-fast. */ + --duration-fast: 130ms; + --duration-base: 300ms; + --duration-slow: 420ms; + --duration-slower: 560ms; + --duration-spring: 340ms; + + --ease-standard: cubic-bezier(0.25, 0.1, 0.25, 1); + --ease-decelerate: cubic-bezier(0, 0, 0.2, 1); + --ease-accelerate: cubic-bezier(0.4, 0, 1, 1); + --ease-emphasized: cubic-bezier(0.3, 0, 0.15, 1); + + /* Calm theme — springs alias to standard easings rather than bounce. + * Personality lives in the values, never in absence. */ + --ease-spring-gentle: var(--ease-standard); + --ease-spring-snappy: var(--ease-decelerate); + --ease-spring-bouncy: var(--ease-emphasized); + + /* ── Typography ── product register: fixed rem sizes, full ramp. + * Fraunces carries the display voice; Albert Sans does the work. */ + --text-hero-size: 3.5rem; + --text-hero-lh: 3.75rem; + --text-display-size: 3rem; + --text-display-lh: 3.25rem; + --text-headline-size: 2rem; + --text-headline-lh: 2.375rem; + --text-title-size: 1.5rem; + --text-title-lh: 1.875rem; + --text-subtitle-size: 1.1875rem; + --text-subtitle-lh: 1.75rem; + --text-body-size: 1.0625rem; + --text-body-lh: 1.625rem; + --text-label-size: 0.9375rem; + --text-label-lh: 1.25rem; + --text-caption-size: 0.8125rem; + --text-caption-lh: 1.125rem; + + /* ── Elevation ── warm umber shadows, low-key: things rest on the wall, + * they don't float. Never pure black. */ + --elevation-0: none; + --elevation-1: 0 1px 2px oklch(0.24 0.03 60 / 0.1); + --elevation-2: 0 1px 3px oklch(0.24 0.03 60 / 0.12), 0 1px 2px oklch(0.24 0.03 60 / 0.08); + --elevation-3: 0 4px 10px -2px oklch(0.24 0.03 60 / 0.14), 0 2px 4px -2px oklch(0.24 0.03 60 / 0.08); + --elevation-4: 0 12px 28px -6px oklch(0.24 0.03 60 / 0.18), 0 4px 10px -4px oklch(0.24 0.03 60 / 0.1); + --elevation-5: 0 24px 48px -12px oklch(0.24 0.03 60 / 0.26); + + --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 — dusk in the same courtyard. Deep umber-brown surfaces (never + * blue-gray); the terracotta becomes glow and takes dark umber labels, since + * dark-ink-on-bright beats white-on-mid-tone. + */ +.dark { + --background: oklch(0.22 0.02 55); + --foreground: oklch(0.93 0.02 75); + + --card: oklch(0.28 0.022 55); + --card-foreground: oklch(0.93 0.02 75); + + --popover: oklch(0.295 0.024 55); + --popover-foreground: oklch(0.93 0.02 75); + + --primary: oklch(0.7 0.14 45); + --primary-foreground: oklch(0.16 0.03 45); + + --secondary: oklch(0.31 0.025 55); + --secondary-foreground: oklch(0.9 0.02 75); + + --muted: oklch(0.3 0.025 55); + --muted-foreground: oklch(0.72 0.03 70); + + --accent: oklch(0.3 0.05 240); + --accent-foreground: oklch(0.85 0.06 235); + + --destructive: oklch(0.64 0.19 30); + --destructive-foreground: oklch(0.14 0.03 28); + --success: oklch(0.7 0.12 130); + --success-foreground: oklch(0.14 0.04 135); + --warning: oklch(0.78 0.13 80); + --warning-foreground: oklch(0.2 0.05 80); + --info: oklch(0.7 0.1 240); + --info-foreground: oklch(0.14 0.03 245); + + --border: oklch(0.93 0.02 75 / 0.12); + --input: oklch(0.93 0.02 75 / 0.12); + --ring: oklch(0.7 0.14 45 / 0.5); + + --chart-1: oklch(0.68 0.13 40); + --chart-2: oklch(0.75 0.1 125); + --chart-3: oklch(0.68 0.1 240); + --chart-4: oklch(0.8 0.12 85); + --chart-5: oklch(0.72 0.09 320); + + --sidebar: oklch(0.2 0.02 55); + --sidebar-foreground: oklch(0.93 0.02 75); + --sidebar-primary: oklch(0.7 0.14 45); + --sidebar-primary-foreground: oklch(0.16 0.03 45); + --sidebar-accent: oklch(0.32 0.04 45); + --sidebar-accent-foreground: oklch(0.88 0.07 50); + --sidebar-border: oklch(0.93 0.02 75 / 0.1); + --sidebar-ring: oklch(0.7 0.14 45 / 0.5); + + --syntax-keyword: oklch(0.75 0.13 45); + --syntax-string: oklch(0.76 0.11 135); + --syntax-number: oklch(0.8 0.12 65); + --syntax-comment: oklch(0.6 0.03 65); + --syntax-function: oklch(0.74 0.1 240); + --syntax-type: oklch(0.75 0.07 195); + + /* ── Chat surfaces (dark) ── */ + --chat-user-bg: oklch(0.48 0.13 40); + --chat-user-fg: oklch(0.97 0.02 75); + --chat-assistant-bg: oklch(0.29 0.023 55); + --chat-assistant-fg: oklch(0.93 0.02 75); + --chat-assistant-border: oklch(0.93 0.02 75 / 0.12); + + /* Dusk shadows — deeper umber, never blue-black. */ + --elevation-1: 0 1px 2px oklch(0.1 0.012 55 / 0.4); + --elevation-2: 0 1px 3px oklch(0.1 0.012 55 / 0.45), 0 1px 2px oklch(0.1 0.012 55 / 0.3); + --elevation-3: 0 4px 10px -2px oklch(0.1 0.012 55 / 0.45), 0 2px 4px -2px oklch(0.1 0.012 55 / 0.3); + --elevation-4: 0 12px 28px -6px oklch(0.1 0.012 55 / 0.5), 0 4px 10px -4px oklch(0.1 0.012 55 / 0.3); + --elevation-5: 0 24px 48px -12px oklch(0.1 0.012 55 / 0.6); +} + +/* + * @layer base — Siena's material philosophy. + * + * Plaster panels on a painted wall: cards get a hairline ink border and a + * low warm shadow (they rest, they don't float). Fraunces carries headings; + * everything else is quiet Albert Sans on olive-brown ink. + */ +@layer base { + body { + min-height: 100vh; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + letter-spacing: -0.011em; + font-family: var(--font-sans); + font-size: var(--text-body-size); + line-height: var(--text-body-lh); + } + + 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 chip; sienna stays reserved for + * actions, links and focus so the accent keeps pointing at one thing. */ + :not(pre) > code { + padding: 0.1em 0.35em; + border-radius: calc(var(--radius) * 0.5); + background: color-mix(in oklch, var(--foreground) 7%, transparent); + color: var(--foreground); + font-size: 0.9em; + } + + /* Selection — sun-warmed sienna wash. */ + ::selection { + background: color-mix(in oklch, var(--primary) 24%, transparent); + color: var(--foreground); + } + + a { + color: var(--primary); + 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); + } + + /* Plaster panels — hairline ink border + resting warm shadow. Border + * AND shadow both stay light so surfaces read as material, not chrome. */ + [data-slot="card"], + [data-slot="popover-content"], + [data-slot="dropdown-menu-content"], + [data-slot="context-menu-content"], + [data-slot="menubar-content"], + [data-slot="hover-card-content"], + [data-slot="select-content"], + [data-slot="combobox-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="popover-content"], + [data-slot="dropdown-menu-content"], + [data-slot="context-menu-content"], + [data-slot="menubar-content"], + [data-slot="hover-card-content"], + [data-slot="select-content"], + [data-slot="combobox-content"], + [data-slot="tooltip-content"] { + box-shadow: var(--elevation-3); + } + + [data-slot="sheet-content"], + [data-slot="dialog-content"], + [data-slot="alert-dialog-content"], + [data-slot="drawer-content"] { + box-shadow: var(--elevation-4); + } + + [data-slot="button"] { + font-weight: 500; + } + [data-slot="button"]:focus-visible { + outline: 2px solid var(--primary); + outline-offset: 2px; + } + + [data-slot="sidebar"] { + background: var(--sidebar); + border-right: var(--border-width-base) solid var(--sidebar-border); + } +} + +/* + * 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; + } +}