commit b44532665f8bca06245d5a9f5e4dfc12312921d4 Author: jules Date: Tue Jul 7 23:20:55 2026 +1000 lib-theme-vermilion: 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..200dc2a --- /dev/null +++ b/README.md @@ -0,0 +1,56 @@ +# lib-theme-vermilion + +**Poster chrome.** A bold, identity-forward Crema theme: one hot vermilion +red-orange is the brand, and it is carried by the *chrome* — the primary +action, hero panels, the entire sidebar, selection — in big committed slabs, +editorial-poster style. Roughly 30–40% of a typical screen is drenched +vermilion, while the content well stays warm paper with near-black warm ink, +so the color reads as identity, not noise. Massive condensed display type +(Anton) is the other half of the voice: the vermilion and the type do the +pointing together, and everything else stays quiet ink-on-paper. + +Key decisions: + +- `--primary` is the poster vermilion `oklch(0.55 0.21 30)` — deepened from + the nominal 0.60 so near-white ink clears the 4.5:1 floor with margin. +- `--sidebar` is fully drenched vermilion (near-white foreground, white-alpha + hairline borders); the active item is a paper chip punched out of the slab. +- `--destructive` is deliberately not a sibling of the brand: deeper crimson + (`oklch(0.48 0.19 15)`, light label) vs the brighter orange-leaning primary, + so a destructive button and a primary button never look related. +- Dark mode is the poster at night: near-black warm charcoal + (`oklch(0.18 0.01 40)`), cards stepping ≥0.05 lighter, and the vermilion + glowing slightly brighter (`oklch(0.64 0.2 33)`) with dark ink on top. The + chrome stays vermilion — the sidebar does not go grey at night. +- Radius `0.5rem` (poster edges), snappy motion (120/240/360/500ms, springs + aliased), low warm-black shadows. + +## Fonts + +Consuming apps load the fonts via a Google Fonts `@import` at the very top of +their entry CSS (never inside `theme.css` — see the token contract): + +```css +@import url("https://fonts.googleapis.com/css2?family=Anton&family=Archivo:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap"); +``` + +Notes: + +- **Anton** (headings) ships in a single weight, 400 — it is already + black-condensed, so the theme keeps heading `font-weight: 400`, opens + tracking a hair (`letter-spacing: 0.01em`; condensed faces need air), and + does **not** force uppercase — the face carries the register on its own. + Big sizes stay narrow: the ramp tops out at a 4rem hero. +- **Archivo** (body) at 400/500/600/700 — a grotesque with enough width + contrast against Anton to keep reading text calm. +- **JetBrains Mono** (code) at 400/500/600. + +## Validation + +```bash +node create-crema-app/scripts/validate-theme.mjs lib-theme-vermilion/theme.css +``` + +Passes the Crema v1 token contract: full Tier-1 token set in both modes, +status quartet with ≥4.5:1 label pairs, contrast floors in light and dark, +chart hues ≥60° apart, inert glass stubs, springs aliased (never absent). diff --git a/theme.css b/theme.css new file mode 100644 index 0000000..50be4aa --- /dev/null +++ b/theme.css @@ -0,0 +1,429 @@ +/* + * Vermilion — poster chrome. + * + * A bold, identity-forward consumer theme. One hot vermilion red-orange is THE + * brand, and it is carried by the CHROME — the primary action, hero panels, + * the entire sidebar, selection — in big committed slabs, editorial-poster + * style. Roughly 30–40% of a typical screen is drenched vermilion. The content + * well stays out of the fight: warm paper with near-black warm ink, so the + * poster color reads as identity, not noise. Massive condensed display type + * (Anton) is the second half of the voice; the vermilion and the type do the + * pointing together. + * + * Destructive is deliberately NOT a sibling of the brand: pulled to a deeper + * crimson (hue ~15, L≈0.48, light label) so a destructive button and a primary + * button never read as the same family. + * + * Dark mode is the poster at night: near-black warm charcoal with the same + * vermilion glowing slightly brighter, carrying dark ink on top + * (bright-swatch/dark-ink) — the chrome stays vermilion. + * + * Type: Anton (single weight 400 — condensed poster caps, it IS the voice) for + * headings, Archivo for body, JetBrains Mono for code. Consumer register with + * a tall display end of the ramp (hero 4rem — Anton is condensed, big sizes + * stay narrow). Radius is a modest 0.5rem — poster edges, not toys. Motion is + * snappy (120/240/360/500ms); springs alias to the standard eases. + * + * Token contract matches lib-theme-caffe-florian/theme.css (Crema v1). + */ + +/* + * Fonts: consuming apps load Anton + Archivo + 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: "Archivo", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + --font-heading: "Anton", "Archivo", -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: "Anton", "Archivo", -apple-system, BlinkMacSystemFont, sans-serif; + --body-family: "Archivo", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + + /* ── Colors ── warm paper content well, near-black warm ink, and the hot + * vermilion drenching the chrome. The paper carries a faint vermilion + * tint (chroma 0.012, hue pulled toward the brand) so the brand reaches + * the content well without gambling its legibility. */ + --background: oklch(0.97 0.012 45); + --foreground: oklch(0.21 0.012 40); + + --card: oklch(0.995 0.004 60); + --card-foreground: oklch(0.21 0.012 40); + + --popover: oklch(0.99 0.005 60); + --popover-foreground: oklch(0.21 0.012 40); + + /* Primary — the poster vermilion, deepened from the nominal 0.60 to L0.55 + * so near-white ink clears the 4.5:1 floor with room to spare. This is the + * same slab the sidebar is drenched in — one color, big commitments. */ + --primary: oklch(0.55 0.21 30); + --primary-foreground: oklch(0.985 0.006 40); + + --secondary: oklch(0.93 0.014 48); + --secondary-foreground: oklch(0.3 0.014 40); + + --muted: oklch(0.93 0.014 48); + --muted-foreground: oklch(0.45 0.014 40); + + /* Accent — a pale vermilion wash with deep vermilion ink: the hover / + * selection tint that echoes the brand without adding a second fill. */ + --accent: oklch(0.94 0.035 40); + --accent-foreground: oklch(0.42 0.16 31); + + /* ── Status ── destructive must never look like the brand's sibling: + * crimson hue ~15 AND visibly deeper (L0.48) with a light label, vs the + * brighter orange-leaning primary (hue 30, L0.55). Success/warning carry + * dark ink on bright swatches; warning sits at amber 70 — far from the + * vermilion — so alerts never blend into the chrome. */ + --destructive: oklch(0.48 0.19 15); + --destructive-foreground: oklch(0.98 0.01 15); + --success: oklch(0.62 0.14 150); + --success-foreground: oklch(0.16 0.03 150); + --warning: oklch(0.72 0.15 70); + --warning-foreground: oklch(0.22 0.05 60); + --info: oklch(0.52 0.13 250); + --info-foreground: oklch(0.98 0.01 250); + + --border: oklch(0.21 0.012 40 / 0.12); + --input: oklch(0.21 0.012 40 / 0.14); + --ring: oklch(0.55 0.21 30 / 0.55); + + /* Chart palette — vermilion leads, then a spaced spectrum (blue / teal / + * violet / gold), hues ≥60° apart, adjacent lightness varied. */ + --chart-1: oklch(0.58 0.2 32); + --chart-2: oklch(0.5 0.13 250); + --chart-3: oklch(0.68 0.11 185); + --chart-4: oklch(0.55 0.15 305); + --chart-5: oklch(0.72 0.13 95); + + /* ── Sidebar ── DRENCHED. The whole rail is the poster slab: vermilion + * field, near-white ink, white-alpha hairlines. The active item is a paper + * chip punched out of the slab (deep vermilion ink on near-white). */ + --sidebar: oklch(0.52 0.2 31); + --sidebar-foreground: oklch(0.985 0.006 40); + --sidebar-primary: oklch(0.985 0.008 60); + --sidebar-primary-foreground: oklch(0.42 0.17 31); + --sidebar-accent: oklch(0.46 0.185 31); + --sidebar-accent-foreground: oklch(0.985 0.006 40); + --sidebar-border: oklch(1 0 0 / 0.22); + --sidebar-ring: oklch(0.985 0.008 60 / 0.6); + + /* Syntax — warm-ink base; the vermilion takes the function seat. */ + --syntax-keyword: oklch(0.48 0.15 305); + --syntax-string: oklch(0.5 0.12 150); + --syntax-number: oklch(0.52 0.14 55); + --syntax-comment: oklch(0.52 0.012 40); + --syntax-function: oklch(0.5 0.18 30); + --syntax-type: oklch(0.5 0.1 200); + + /* ── Chat surfaces ── + * Contract for lib-chat-ui / lib-agent-ui / lib-agent-dock-ui. The user + * bubble is a vermilion slab with near-white ink — the poster color owns + * the human's voice; the assistant answers from clean paper. */ + --chat-user-bg: oklch(0.55 0.21 30); + --chat-user-fg: oklch(0.985 0.006 40); + --chat-assistant-bg: oklch(0.995 0.004 60); + --chat-assistant-fg: oklch(0.21 0.012 40); + --chat-assistant-border: oklch(0.21 0.012 40 / 0.1); + + /* Poster edges, not toys. */ + --radius: 0.5rem; + + /* ── Motion ── snappy poster energy. Springs alias to the standard eases — + * the personality is speed and commitment, not bounce. */ + --duration-fast: 120ms; + --duration-base: 240ms; + --duration-slow: 360ms; + --duration-slower: 500ms; + --duration-spring: 240ms; + + --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.2, 0, 0, 1); + + --ease-spring-gentle: var(--ease-standard); + /* One real spring — a bold consumer theme earns a snappy press. */ + --ease-spring-snappy: cubic-bezier(0.2, 1.4, 0.3, 1); + --ease-spring-bouncy: var(--ease-standard); + + /* ── Typography ── consumer register with a big condensed display end. + * Anton is condensed, so the tall sizes stay narrow on screen; line + * heights tighten hard at the poster end of the ramp. */ + --text-hero-size: 4rem; + --text-hero-lh: 1.02; + --text-display-size: 3rem; + --text-display-lh: 1.05; + --text-headline-size: 2rem; + --text-headline-lh: 1.12; + --text-title-size: 1.375rem; + --text-title-lh: 1.25; + --text-subtitle-size: 1.125rem; + --text-subtitle-lh: 1.4; + --text-body-size: 1rem; + --text-body-lh: 1.55; + --text-label-size: 0.875rem; + --text-label-lh: 1.25rem; + --text-caption-size: 0.75rem; + --text-caption-lh: 1.0625rem; + + /* ── Elevation ── low, warm-black shadows — print-flat until something + * genuinely lifts. */ + --elevation-0: none; + --elevation-1: 0 1px 2px oklch(0.18 0.02 40 / 0.07); + --elevation-2: 0 1px 3px oklch(0.18 0.02 40 / 0.09), 0 1px 2px oklch(0.18 0.02 40 / 0.05); + --elevation-3: 0 4px 8px -2px oklch(0.18 0.02 40 / 0.1), 0 2px 4px -2px oklch(0.18 0.02 40 / 0.05); + --elevation-4: 0 12px 24px -6px oklch(0.18 0.02 40 / 0.12), 0 4px 8px -4px oklch(0.18 0.02 40 / 0.06); + --elevation-5: 0 20px 40px -8px oklch(0.18 0.02 40 / 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 poster at night. Near-black warm charcoal (low chroma, hue + * ~40, never blue-slate), cards stepping clearly lighter, and the vermilion + * glowing slightly brighter with dark ink on top (bright-swatch/dark-ink). + * The chrome stays vermilion — the sidebar does not go grey at night. + */ +.dark { + --background: oklch(0.18 0.01 40); + --foreground: oklch(0.95 0.008 55); + + --card: oklch(0.23 0.012 40); + --card-foreground: oklch(0.95 0.008 55); + + --popover: oklch(0.25 0.012 40); + --popover-foreground: oklch(0.95 0.008 55); + + --primary: oklch(0.64 0.2 33); + --primary-foreground: oklch(0.13 0.018 35); + + --secondary: oklch(0.27 0.012 40); + --secondary-foreground: oklch(0.92 0.01 55); + + --muted: oklch(0.27 0.012 40); + --muted-foreground: oklch(0.73 0.014 50); + + --accent: oklch(0.32 0.075 33); + --accent-foreground: oklch(0.89 0.08 40); + + --destructive: oklch(0.52 0.2 15); + --destructive-foreground: oklch(0.98 0.01 15); + --success: oklch(0.7 0.15 150); + --success-foreground: oklch(0.16 0.03 150); + --warning: oklch(0.76 0.15 70); + --warning-foreground: oklch(0.22 0.05 60); + --info: oklch(0.66 0.13 250); + --info-foreground: oklch(0.15 0.03 250); + + --border: oklch(0.95 0.008 55 / 0.13); + --input: oklch(0.95 0.008 55 / 0.15); + --ring: oklch(0.64 0.2 33 / 0.55); + + --chart-1: oklch(0.66 0.19 33); + --chart-2: oklch(0.62 0.13 250); + --chart-3: oklch(0.74 0.11 185); + --chart-4: oklch(0.66 0.15 305); + --chart-5: oklch(0.8 0.13 95); + + /* Sidebar — still drenched, tuned a step deeper so near-white ink glows. */ + --sidebar: oklch(0.48 0.185 30); + --sidebar-foreground: oklch(0.985 0.006 40); + --sidebar-primary: oklch(0.97 0.01 55); + --sidebar-primary-foreground: oklch(0.4 0.165 31); + --sidebar-accent: oklch(0.4 0.16 30); + --sidebar-accent-foreground: oklch(0.985 0.006 40); + --sidebar-border: oklch(1 0 0 / 0.2); + --sidebar-ring: oklch(0.97 0.01 55 / 0.6); + + --syntax-keyword: oklch(0.72 0.13 305); + --syntax-string: oklch(0.74 0.12 150); + --syntax-number: oklch(0.78 0.13 60); + --syntax-comment: oklch(0.62 0.012 45); + --syntax-function: oklch(0.72 0.16 33); + --syntax-type: oklch(0.72 0.1 200); + + /* ── Chat surfaces (dark) ── user bubble a step deeper than the glowing + * primary so near-white ink stays comfortably legible. */ + --chat-user-bg: oklch(0.5 0.19 30); + --chat-user-fg: oklch(0.985 0.006 40); + --chat-assistant-bg: oklch(0.23 0.012 40); + --chat-assistant-fg: oklch(0.95 0.008 55); + --chat-assistant-border: oklch(0.95 0.008 55 / 0.13); + + --elevation-1: 0 1px 2px oklch(0 0 0 / 0.3); + --elevation-2: 0 1px 3px oklch(0 0 0 / 0.4), 0 1px 2px oklch(0 0 0 / 0.25); + --elevation-3: 0 4px 8px -2px oklch(0 0 0 / 0.4), 0 2px 4px -2px oklch(0 0 0 / 0.25); + --elevation-4: 0 12px 24px -6px oklch(0 0 0 / 0.45), 0 4px 8px -4px oklch(0 0 0 / 0.25); + --elevation-5: 0 20px 40px -8px oklch(0 0 0 / 0.55); +} + +/* + * @layer base — Vermilion's poster philosophy. + * + * Condensed display type in committed sizes, warm paper for reading, and the + * one hot color spent in slabs — chrome, primary, selection — never scattered. + * Everything that isn't the poster stays quiet ink-on-paper. + */ +@layer base { + body { + min-height: 100vh; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + letter-spacing: -0.004em; + font-family: var(--font-sans); + font-size: var(--text-body-size); + line-height: var(--text-body-lh); + } + + /* Headings — Anton, the single-weight poster voice. It ships at 400 only; + * it is already black-condensed, so weight stays 400 and tracking opens a + * hair (condensed faces need air, not squeeze). Case is the author's call — + * the face carries the register without forced uppercase. */ + 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: 400; + text-transform: none; + letter-spacing: 0.01em; + /* 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 ink chip, no brand color spent here. */ + :not(pre) > code { + padding: 0.1em 0.35em; + border-radius: calc(var(--radius) * 0.5); + background: color-mix(in oklch, var(--foreground) 6%, transparent); + color: var(--foreground); + font-size: 0.9em; + } + + /* Selection — the poster color claims what you grab. */ + ::selection { + background: color-mix(in oklch, var(--primary) 30%, transparent); + color: var(--foreground); + } + + /* Links — vermilion, underline-on-hover. */ + a { + color: color-mix(in oklch, var(--primary) 75%, var(--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); + } + + [data-slot="sidebar"] { + background: var(--sidebar); + border-right: var(--border-width-base) solid var(--sidebar-border); + } +} + +/* + * Focus-visible — the vermilion 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 — a touch of poster weight; opacity absorb on press, snappy. */ +[data-slot="button"] { + font-weight: 600; + letter-spacing: 0; +} + +/* Card titles — pinned to the subtitle role so the scale is the theme's + * decision, not the card primitive's default. (Anton at title sizes inside a + * card would shout; the subtitle size keeps the poster voice measured.) */ +[data-slot="card-title"] { + font-size: var(--text-subtitle-size); + line-height: var(--text-subtitle-lh); +} + +/* Touch targets — desktop keeps the crisp 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.9; +} +[data-slot="button"]:active:not(:disabled) { + opacity: 0.82; + transform: scale(0.99); +} + +/* Inputs — clean paper surface, hairline border, vermilion focus as a flush + * ring. */ +[data-slot="input"], +[data-slot="textarea"], +[data-slot="select-trigger"] { + background: var(--card); + border: var(--border-width-base) solid var(--border); + border-radius: var(--radius); + color: var(--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: var(--muted-foreground); + opacity: 0.8; +} +[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(--foreground) 22%, 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; +}