commit bd87175b522503eba70faf2d88b15f4bcdbdd398 Author: jules Date: Tue Jul 7 23:20:55 2026 +1000 lib-theme-klein: initial import (bold/identity-forward set, 2026-07) One of four divergent candidates for the bold consumer theme family (committed saturated color at 30-60% surface coverage, strong display type). Includes the post-diverge fixes: heading color:inherit so brand slabs keep their foreground pairing, plus per-theme refinements. Co-Authored-By: Claude Fable 5 diff --git a/README.md b/README.md new file mode 100644 index 0000000..4923f9f --- /dev/null +++ b/README.md @@ -0,0 +1,59 @@ +# lib-theme-klein + +**Klein — ultramarine drench.** A bold, identity-forward Crema theme where the +background field itself is committed, saturated Yves-Klein ultramarine. + +## Thesis + +Most light themes are a neutral page with a colored accent. Klein inverts that: +the **field is the identity**. `:root` (light mode) sets `--background` to a +deep saturated ultramarine — `oklch(0.44 0.20 265)`, a commitment, not a +tint — carrying near-white ink. Content lives on clean near-white card panels +that float on the blue with deep blue-black shadows (a drenched field casts +blue shadow, never gray). By construction the brand blue carries 30–60% of +every screen. + +One counterpoint does all the pointing: a warm amber-orange +(`oklch(0.72 0.17 60)`) with dark ink on top — the primary action, focus ring, +selection, link underlines, and the user chat bubble. Everything else stays +inside the blue family: secondary/muted are deeper stripes of the same hue, +the sidebar is a deeper band of ultramarine. + +Dark mode is **ultramarine at midnight**: the field deepens to +`oklch(0.30 0.13 265)`, cards become deep blue panels stepping *lighter* off +the field (bg 0.30 → card 0.36 → popover 0.385 → muted 0.40), ink stays +near-white. The theme is unmistakably blue in both modes. + +Border note: `--border` is a dark ink hairline because borders are drawn where +hairlines live — on the white cards and inputs. Hairlines on the blue field +itself are white-alpha and carried by `--sidebar-border`. + +## Fonts + +Consuming apps load the fonts at the very top of their entry CSS (per the +Crema token contract, themes never `@import` fonts): + +```css +@import url("https://fonts.googleapis.com/css2?family=Syne:wght@600;700;800&family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap"); +``` + +- **Syne** (600/700/800) — headings. The art-poster voice; the base heading + rule uses **700**, with 800 available for hero moments. Syne runs wide, so + headings carry `-0.015em` tracking. +- **Inter** (400/500/600/700) — body/UI. Legible, modern, stays out of the + poster's way. Body register: 1rem / 1.6. +- **JetBrains Mono** (400/500/600) — code, `kbd`, code blocks. + +## Register + +Consumer: body 1rem/1.6, label 0.875rem, caption 0.75rem, subtitle 1.125rem, +title 1.375rem, headline 1.75rem, display 2.5rem, hero 3.25rem. Radius is a +generous `1rem`. Motion is confident (150/300/450/600ms) with real gentle +springs (`--ease-spring-*` are true overshoot curves, not aliases). + +## Validation + +```bash +node create-crema-app/scripts/validate-theme.mjs lib-theme-klein/theme.css +# ✓ PASS — all mandatory tokens present, contrast floors met, no ban violations. +``` diff --git a/theme.css b/theme.css new file mode 100644 index 0000000..31ca980 --- /dev/null +++ b/theme.css @@ -0,0 +1,434 @@ +/* + * Klein — ultramarine drench. + * + * Yves Klein blue as the identity. The background field ITSELF is a committed, + * saturated deep ultramarine — not a tint, not an accent, the world you stand + * in — carrying near-white ink. Cards are clean near-white panels floating on + * the blue, with dark ink on top. This inverts the usual light-theme logic: + * :root (light mode) IS the drenched blue world; the blue carries 30–60% of + * every surface by construction. + * + * The counterpoint is a warm amber-orange (hue ~60) spent on the primary + * action, focus, selection, and the user chat bubble — the one hot signal that + * pops off the ultramarine. Secondary and muted stay inside the blue family + * (deeper stripes of the same hue); the sidebar is a deeper band of + * ultramarine. Borders are dark hairlines because borders live on the white + * cards and inputs; the sidebar gets its own white-alpha hairline token. + * + * Dark mode is ultramarine at midnight: the field deepens, cards become deep + * blue panels stepping LIGHTER off the field, ink stays near-white — the theme + * is unmistakably blue in both modes. + * + * Type is the art-poster voice: Syne (700+) for headings, Inter for body, + * JetBrains Mono for code. Consumer register — generous 1rem radius, confident + * motion (150/300/450/600ms) with real gentle springs, deep blue-black shadows + * (a drenched field casts blue shadow, never gray). + * + * Token contract matches lib-theme-caffe-florian/theme.css (Crema v1). + */ + +/* + * Fonts: consuming apps load Syne + Inter + JetBrains Mono at the top of their + * entry CSS (where @import statements are allowed), via the Google Fonts URL + * documented in this theme's README. Embedding the @import here — or smuggling + * the URL in a comment for a build script to lift — breaks PostCSS's ordering + * rules, so neither is done in this file. + */ + +:root { + --font-sans: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + --font-heading: "Syne", "Inter", -apple-system, BlinkMacSystemFont, sans-serif; + --font-mono: "JetBrains Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace; + + /* Tailwind utility families. Distinct names so a consuming app's + * `@theme inline` can indirect to these (--font-heading/--font-sans collide + * with Tailwind's own font-* namespace). Mirror --font-heading/--font-sans. */ + --heading-family: "Syne", "Inter", -apple-system, BlinkMacSystemFont, sans-serif; + --body-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + + /* ── Colors ── the drench: deep saturated ultramarine field, near-white + * ink on the blue, clean near-white cards carrying dark blue-black ink. */ + --background: oklch(0.44 0.2 265); + --foreground: oklch(0.985 0.008 265); + + --card: oklch(0.99 0.005 265); + --card-foreground: oklch(0.21 0.045 265); + + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.21 0.045 265); + + /* Primary — the warm counterpoint. Vivid amber-orange with near-black warm + * ink on top (bright-swatch/dark-ink pairing); the one hot signal that + * pops off the blue field. */ + --primary: oklch(0.72 0.17 60); + --primary-foreground: oklch(0.2 0.05 60); + + /* Secondary — a deeper stripe of the same ultramarine, white ink. */ + --secondary: oklch(0.38 0.18 265); + --secondary-foreground: oklch(0.975 0.008 265); + + /* Muted — the family's quiet register: deeper blue surface; the muted ink + * is a pale desaturated ultramarine that still clears 4.5:1 on the field. */ + --muted: oklch(0.39 0.18 265); + --muted-foreground: oklch(0.87 0.05 265); + + /* Accent — hover/selection tint for menus and popovers (which are white + * panels here): a pale ultramarine wash with deep blue ink. */ + --accent: oklch(0.93 0.035 265); + --accent-foreground: oklch(0.3 0.13 265); + + /* ── Status ── tuned for the CARD surface (chips sit on white panels). + * Dark ink on bright swatches for success/warning/info; destructive keeps + * a light label on a deep alarm red. Warning sits at yellow ~90 so it + * never reads as the amber-orange primary (hue 60). Info is a cyan-sky + * (~220) clearly apart from the ultramarine field (265). */ + --destructive: oklch(0.55 0.21 27); + --destructive-foreground: oklch(0.98 0.01 27); + --success: oklch(0.62 0.14 150); + --success-foreground: oklch(0.17 0.03 150); + --warning: oklch(0.8 0.15 90); + --warning-foreground: oklch(0.23 0.05 90); + --info: oklch(0.7 0.12 220); + --info-foreground: oklch(0.16 0.03 220); + + /* Borders live where hairlines are drawn: on white cards and inputs — so + * --border is a dark blue-ink hairline. The blue field's own hairlines are + * carried by --sidebar-border (white-alpha) below. */ + --border: oklch(0.21 0.045 265 / 0.14); + --input: oklch(0.21 0.045 265 / 0.18); + --ring: oklch(0.72 0.17 60 / 0.6); + + /* Chart palette — amber counterpoint leads, then the identity ultramarine, + * teal, magenta, green. Hues spaced ≥60°, adjacent lightness varied. + * Tuned for the white card surface where charts render. */ + --chart-1: oklch(0.72 0.17 60); + --chart-2: oklch(0.48 0.2 265); + --chart-3: oklch(0.65 0.12 190); + --chart-4: oklch(0.56 0.2 340); + --chart-5: oklch(0.62 0.15 150); + + /* Sidebar — a deeper stripe of ultramarine; hairlines on the blue are + * white-alpha; the amber signal marks the active item. */ + --sidebar: oklch(0.36 0.17 265); + --sidebar-foreground: oklch(0.975 0.008 265); + --sidebar-primary: oklch(0.72 0.17 60); + --sidebar-primary-foreground: oklch(0.2 0.05 60); + --sidebar-accent: oklch(0.47 0.2 265); + --sidebar-accent-foreground: oklch(0.985 0.008 265); + --sidebar-border: oklch(1 0 0 / 0.18); + --sidebar-ring: oklch(0.72 0.17 60 / 0.6); + + /* Syntax — code blocks render on light card surfaces: deep inks with the + * theme's blue and amber in the leading roles. */ + --syntax-keyword: oklch(0.45 0.2 265); + --syntax-string: oklch(0.48 0.12 150); + --syntax-number: oklch(0.55 0.14 60); + --syntax-comment: oklch(0.55 0.03 265); + --syntax-function: oklch(0.5 0.16 300); + --syntax-type: oklch(0.48 0.1 190); + + /* ── Chat surfaces ── + * Contract for lib-chat-ui / lib-agent-ui / lib-agent-dock-ui. The user + * bubble is the amber counterpoint (dark ink) glowing against the blue + * field; the assistant answers on a clean white card with a hairline. */ + --chat-user-bg: oklch(0.72 0.17 60); + --chat-user-fg: oklch(0.2 0.05 60); + --chat-assistant-bg: oklch(0.99 0.005 265); + --chat-assistant-fg: oklch(0.21 0.045 265); + --chat-assistant-border: oklch(0.21 0.045 265 / 0.12); + + /* Generous consumer radius. */ + --radius: 1rem; + + /* ── Motion ── confident consumer pacing with real gentle springs — the + * bold identity is allowed a physical settle. */ + --duration-fast: 150ms; + --duration-base: 300ms; + --duration-slow: 450ms; + --duration-slower: 600ms; + --duration-spring: 450ms; + + --ease-standard: cubic-bezier(0.2, 0, 0, 1); + --ease-decelerate: cubic-bezier(0, 0, 0, 1); + --ease-accelerate: cubic-bezier(0.3, 0, 1, 1); + --ease-emphasized: cubic-bezier(0.3, 0, 0, 1); + + --ease-spring-gentle: cubic-bezier(0.34, 1.16, 0.64, 1); + --ease-spring-snappy: cubic-bezier(0.42, 1.28, 0.56, 1); + --ease-spring-bouncy: cubic-bezier(0.36, 1.44, 0.44, 1); + + /* ── Typography ── consumer register, fixed rem, full role ramp. */ + --text-hero-size: 3.25rem; + --text-hero-lh: 1.05; + --text-display-size: 2.5rem; + --text-display-lh: 1.1; + --text-headline-size: 1.75rem; + --text-headline-lh: 1.2; + --text-title-size: 1.375rem; + --text-title-lh: 1.3; + --text-subtitle-size: 1.125rem; + --text-subtitle-lh: 1.45; + --text-body-size: 1rem; + --text-body-lh: 1.6; + --text-label-size: 0.875rem; + --text-label-lh: 1.25rem; + --text-caption-size: 0.75rem; + --text-caption-lh: 1.125rem; + + /* ── Elevation ── shadows cast on a drenched field are deep blue-black, + * never gray — white cards lift off the ultramarine on blue shadow. */ + --elevation-0: none; + --elevation-1: 0 1px 2px oklch(0.12 0.08 265 / 0.2); + --elevation-2: 0 2px 4px oklch(0.12 0.08 265 / 0.24), 0 1px 2px oklch(0.12 0.08 265 / 0.16); + --elevation-3: 0 6px 12px -2px oklch(0.12 0.08 265 / 0.28), 0 2px 4px -2px oklch(0.12 0.08 265 / 0.18); + --elevation-4: 0 14px 28px -6px oklch(0.12 0.08 265 / 0.34), 0 4px 10px -4px oklch(0.12 0.08 265 / 0.2); + --elevation-5: 0 24px 48px -8px oklch(0.12 0.08 265 / 0.44); + + --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 — ultramarine at midnight. The field deepens (still unmistakably + * blue, never charcoal); cards become deep blue panels stepping LIGHTER off + * the field (bg 0.30 → card 0.36 → popover 0.385 → muted 0.40); ink stays + * near-white; the amber signal brightens a touch against the night. + */ +.dark { + --background: oklch(0.3 0.13 265); + --foreground: oklch(0.965 0.01 265); + + --card: oklch(0.36 0.14 265); + --card-foreground: oklch(0.965 0.01 265); + + --popover: oklch(0.385 0.14 265); + --popover-foreground: oklch(0.965 0.01 265); + + --primary: oklch(0.75 0.16 60); + --primary-foreground: oklch(0.2 0.05 60); + + --secondary: oklch(0.4 0.15 265); + --secondary-foreground: oklch(0.95 0.012 265); + + --muted: oklch(0.4 0.15 265); + --muted-foreground: oklch(0.85 0.045 265); + + --accent: oklch(0.45 0.16 265); + --accent-foreground: oklch(0.97 0.01 265); + + --destructive: oklch(0.56 0.21 27); + --destructive-foreground: oklch(0.98 0.01 27); + --success: oklch(0.7 0.15 150); + --success-foreground: oklch(0.17 0.03 150); + --warning: oklch(0.83 0.15 90); + --warning-foreground: oklch(0.23 0.05 90); + --info: oklch(0.74 0.12 220); + --info-foreground: oklch(0.16 0.03 220); + + /* At midnight every surface is blue, so hairlines are white-ink alpha. */ + --border: oklch(0.97 0.01 265 / 0.16); + --input: oklch(0.97 0.01 265 / 0.2); + --ring: oklch(0.75 0.16 60 / 0.6); + + --chart-1: oklch(0.78 0.16 60); + --chart-2: oklch(0.68 0.15 265); + --chart-3: oklch(0.74 0.12 190); + --chart-4: oklch(0.7 0.17 340); + --chart-5: oklch(0.74 0.15 150); + + --sidebar: oklch(0.26 0.12 265); + --sidebar-foreground: oklch(0.965 0.01 265); + --sidebar-primary: oklch(0.75 0.16 60); + --sidebar-primary-foreground: oklch(0.2 0.05 60); + --sidebar-accent: oklch(0.37 0.15 265); + --sidebar-accent-foreground: oklch(0.97 0.01 265); + --sidebar-border: oklch(1 0 0 / 0.14); + --sidebar-ring: oklch(0.75 0.16 60 / 0.6); + + --syntax-keyword: oklch(0.82 0.1 265); + --syntax-string: oklch(0.82 0.12 150); + --syntax-number: oklch(0.84 0.13 60); + --syntax-comment: oklch(0.72 0.03 265); + --syntax-function: oklch(0.8 0.12 300); + --syntax-type: oklch(0.8 0.1 190); + + /* ── Chat surfaces (dark) ── the amber user bubble glows against the + * midnight field; the assistant answers on a deep blue card panel. */ + --chat-user-bg: oklch(0.75 0.16 60); + --chat-user-fg: oklch(0.2 0.05 60); + --chat-assistant-bg: oklch(0.36 0.14 265); + --chat-assistant-fg: oklch(0.965 0.01 265); + --chat-assistant-border: oklch(0.97 0.01 265 / 0.14); + + --elevation-1: 0 1px 2px oklch(0.08 0.05 265 / 0.4); + --elevation-2: 0 2px 4px oklch(0.08 0.05 265 / 0.45), 0 1px 2px oklch(0.08 0.05 265 / 0.3); + --elevation-3: 0 6px 12px -2px oklch(0.08 0.05 265 / 0.5), 0 2px 4px -2px oklch(0.08 0.05 265 / 0.3); + --elevation-4: 0 14px 28px -6px oklch(0.08 0.05 265 / 0.55), 0 4px 10px -4px oklch(0.08 0.05 265 / 0.3); + --elevation-5: 0 24px 48px -8px oklch(0.08 0.05 265 / 0.65); +} + +/* + * @layer base — Klein's poster philosophy. + * + * The field is the identity: content floats on committed ultramarine. Syne + * headings at 700 give the art-poster voice; Inter keeps the body legible and + * modern; the amber counterpoint does all the pointing — selection, focus, + * links' underlines, the CTA. + */ +@layer base { + body { + min-height: 100vh; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + letter-spacing: -0.005em; + font-family: var(--font-sans); + font-size: var(--text-body-size); + line-height: var(--text-body-lh); + } + + /* Headings — Syne at 700, the poster voice. Syne runs wide; a touch of + * negative tracking keeps it confident rather than sprawling. */ + 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: 700; + letter-spacing: -0.015em; + /* inherit, not var(--foreground): identity themes put headings on + * colored slabs (bg-primary heroes) — forcing the field ink there + * breaks the slab's own foreground pairing. */ + color: inherit; + } + + code, kbd, samp, pre, + [data-slot="code"], + [data-slot="code-block"] { + font-family: var(--font-mono); + } + + /* Inline code — neutral chip: foreground-tint wash with foreground ink, + * so it reads correctly on both the blue field and the white cards. */ + :not(pre) > code { + padding: 0.1em 0.35em; + border-radius: calc(var(--radius) * 0.5); + background: color-mix(in oklch, var(--foreground) 8%, transparent); + color: var(--foreground); + font-size: 0.9em; + } + + /* Selection — the amber counterpoint, everywhere. */ + ::selection { + background: color-mix(in oklch, var(--primary) 45%, transparent); + color: var(--foreground); + } + + /* Links — ink follows the surface (white on the field, dark on cards); + * the amber underline is the constant link signal on both. */ + a { + color: inherit; + font-weight: 600; + text-decoration-color: color-mix(in oklch, var(--primary) 70%, transparent); + text-decoration-thickness: 2px; + text-underline-offset: 0.2em; + transition: text-decoration-color var(--duration-fast) var(--ease-standard); + } + a:hover { + text-decoration-color: var(--primary); + } + + [data-slot="sidebar"] { + background: var(--sidebar); + border-right: var(--border-width-base) solid var(--sidebar-border); + } +} + +/* + * Focus-visible — the amber ring, offset (not inset), for keyboard users. + * Placed outside @layer base so it beats Tailwind's default ring utilities. + */ +[data-slot="button"]:focus-visible, +[data-slot="input"]:focus-visible, +[data-slot="textarea"]:focus-visible, +[data-slot="select-trigger"]:focus-visible { + outline: 2px solid var(--primary); + outline-offset: 2px; + box-shadow: none; +} + +/* Buttons — confident weight; a springy press absorb suits the identity. */ +[data-slot="button"] { + font-weight: 600; + letter-spacing: -0.005em; +} + +/* 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 the consumer density ladder; 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; + } +} +[data-slot="button"]:hover:not(:disabled) { + opacity: 0.92; +} +[data-slot="button"]:active:not(:disabled) { + opacity: 0.84; + transform: scale(0.98); + transition: + transform var(--duration-fast) var(--ease-spring-gentle), + opacity var(--duration-fast) var(--ease-standard); +} + +/* Inputs — white card panels with dark hairlines floating on the blue; + * the amber focus ring is the constant. */ +[data-slot="input"], +[data-slot="textarea"], +[data-slot="select-trigger"] { + background: var(--card); + border: var(--border-width-base) solid var(--input); + border-radius: var(--radius); + color: var(--card-foreground); + transition: + border-color var(--duration-fast) var(--ease-standard), + box-shadow var(--duration-fast) var(--ease-standard); +} +[data-slot="input"]::placeholder, +[data-slot="textarea"]::placeholder { + color: color-mix(in oklch, var(--card-foreground) 55%, transparent); +} +[data-slot="input"]:hover:not(:disabled), +[data-slot="textarea"]:hover:not(:disabled), +[data-slot="select-trigger"]:hover:not(:disabled) { + border-color: color-mix(in oklch, var(--card-foreground) 32%, transparent); +} +[data-slot="input"]:focus, +[data-slot="textarea"]:focus, +[data-slot="select-trigger"]:focus { + border-color: var(--primary); + box-shadow: 0 0 0 1px var(--primary); + outline: none; +}