commit 5927d86c03dfff6f3040b2ff10d66d8c13ff6686 Author: jules Date: Tue Jul 7 22:56:29 2026 +1000 lib-theme-graphite: initial import (calm-pro/dense-utility set, 2026-07) One of four divergent candidates for the operator/console theme family (successor to Skyrise for developer-product screens). Dense/pro type register, keyboard-first focus treatment, tabular numerals, coarse-pointer 44px touch floor, neutral code chips, dark card separation per house sweep. Co-Authored-By: Claude Fable 5 diff --git a/README.md b/README.md new file mode 100644 index 0000000..9e1a287 --- /dev/null +++ b/README.md @@ -0,0 +1,36 @@ +# lib-theme-graphite + +**Graphite — achromatic precision.** Dieter-Rams calm for operator/console/admin +surfaces. Every neutral is true achromatic (chroma 0) — near-white grey field, +pure white cards, near-black ink — with ONE electric azure accent spent only on +the primary action, links, selection, and focus. Dark mode is true dark grey +(never blue-black) with cards stepping visibly lighter. Dense/pro type register, +compact 0.5rem radius, product-fast motion, crisp low elevation, tabular +numerals wired for tables. + +## Fonts + +Consuming apps must load the fonts at the very top of their entry CSS +(`app/app.css`), before anything else — the theme file itself does not +`@import` fonts (Crema token-contract rule): + +```css +@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500;600&display=swap"); +``` + +### Font stacks + +| Token | Stack | Notes | +|---|---|---| +| `--font-sans` / `--body-family` | `"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif` | body text | +| `--font-heading` / `--heading-family` | same Inter stack | headings differentiate via weight 600 + tight tracking (−0.022em), **not** a second family | +| `--font-mono` | `"IBM Plex Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace` | code, tabular data | + +## Validation + +```bash +node create-crema-app/scripts/validate-theme.mjs lib-theme-graphite/theme.css +``` + +Passes the Crema v1 token contract (all Tier 1 tokens both modes, WCAG AA +contrast floors, no font `@import`, no root `font-size`). diff --git a/theme.css b/theme.css new file mode 100644 index 0000000..9f3cbcd --- /dev/null +++ b/theme.css @@ -0,0 +1,426 @@ +/* + * Graphite — achromatic precision. + * + * Dieter-Rams calm for operator/console/admin surfaces. The thesis is the + * ABSENCE of tint: every neutral in the theme is true achromatic (chroma 0), + * from the near-white grey field to the pure-white cards to the near-black + * ink. No blue-grey, no warm-grey, no atmosphere — just graphite on paper. + * ONE accent, an electric azure blue (hue ~258), is spent only where the eye + * must go: the primary action, links, selection, and the focus ring. Because + * every other pixel is colorless, that single blue points with unusual force. + * + * Dark mode is true dark GREY — not blue-black, not near-black drama. A + * charcoal field (L 0.19, chroma 0) with cards stepping visibly lighter + * (≥0.05 L) so altitude reads without shadows doing the work. The same azure + * carries the pointing. + * + * Type is Inter for both body and headings — headings differentiate through + * weight (600) and tight tracking, never through a second family. IBM Plex + * Mono for code. Dense/pro register: body 0.9375rem, labels 0.8125rem — + * built for tables, logs, and columns of numbers (tabular-nums is wired in). + * Radius is a compact 0.5rem; motion is product-fast (120/200/300ms) with + * springs aliased to standard eases — personality lives in speed, not bounce. + * Elevation is crisp and low; borders do most of the separating. + * + * Token contract matches lib-theme-caffe-florian/theme.css (Crema v1). + */ + +/* + * Fonts: consuming apps load Inter + IBM Plex 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: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + --font-mono: "IBM Plex 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: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + --body-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + + /* ── Colors ── true achromatic neutrals (chroma 0 everywhere), pure white + * cards, near-black ink, one electric azure accent. */ + --background: oklch(0.97 0 0); + --foreground: oklch(0.2 0 0); + + --card: oklch(1 0 0); + --card-foreground: oklch(0.2 0 0); + + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.2 0 0); + + /* Primary — the sole color. Electric azure deep enough (L 0.5) to carry + * white labels well above the floor, saturated enough to read electric + * against a field that has no color at all. */ + --primary: oklch(0.5 0.19 258); + --primary-foreground: oklch(1 0 0); + + --secondary: oklch(0.94 0 0); + --secondary-foreground: oklch(0.27 0 0); + + --muted: oklch(0.94 0 0); + --muted-foreground: oklch(0.45 0 0); + + /* Accent — hover/selection tint: the faintest azure wash with deep azure + * ink. The only place besides primary where chroma is allowed to live. */ + --accent: oklch(0.94 0.025 258); + --accent-foreground: oklch(0.4 0.16 258); + + /* ── Status ── the only other hues in the theme, kept precise and + * unambiguous: red / green / amber / cyan-blue, each hue distinct from the + * azure primary. Destructive and info carry white on deep fills for alarm + * legibility; success and warning carry dark ink on brighter swatches. */ + --destructive: oklch(0.55 0.21 27); + --destructive-foreground: oklch(0.98 0.01 27); + --success: oklch(0.6 0.14 150); + --success-foreground: oklch(0.17 0.03 150); + --warning: oklch(0.75 0.15 70); + --warning-foreground: oklch(0.22 0.05 70); + --info: oklch(0.5 0.12 230); + --info-foreground: oklch(0.98 0.005 230); + + --border: oklch(0.2 0 0 / 0.12); + --input: oklch(0.2 0 0 / 0.14); + --ring: oklch(0.5 0.19 258 / 0.6); + + /* Chart palette — azure lead, then a restrained cool spectrum (teal / + * violet / green / graphite slate). Hues spaced, adjacent-index lightness + * varied; the greys stay achromatic, the data gets the color. */ + --chart-1: oklch(0.5 0.19 258); + --chart-2: oklch(0.68 0.11 195); + --chart-3: oklch(0.55 0.14 305); + --chart-4: oklch(0.64 0.12 150); + --chart-5: oklch(0.42 0 0); + + --sidebar: oklch(0.955 0 0); + --sidebar-foreground: oklch(0.2 0 0); + --sidebar-primary: oklch(0.5 0.19 258); + --sidebar-primary-foreground: oklch(1 0 0); + --sidebar-accent: oklch(0.94 0.025 258); + --sidebar-accent-foreground: oklch(0.4 0.16 258); + --sidebar-border: oklch(0.2 0 0 / 0.1); + --sidebar-ring: oklch(0.5 0.19 258 / 0.6); + + /* Syntax — muted-precise. Low-chroma cool hues that read as annotation, + * not decoration; comments recede to achromatic grey. */ + --syntax-keyword: oklch(0.45 0.13 290); + --syntax-string: oklch(0.47 0.1 150); + --syntax-number: oklch(0.5 0.11 70); + --syntax-comment: oklch(0.55 0 0); + --syntax-function: oklch(0.47 0.15 258); + --syntax-type: oklch(0.48 0.09 220); + + /* ── Chat surfaces ── + * Contract for lib-chat-ui / lib-agent-ui / lib-agent-dock-ui. User bubble + * is a deep graphite panel with light text (neutral, so the azure stays + * scarce); assistant bubble is a clean white card with a hairline border. */ + --chat-user-bg: oklch(0.25 0 0); + --chat-user-fg: oklch(0.97 0 0); + --chat-assistant-bg: oklch(1 0 0); + --chat-assistant-fg: oklch(0.2 0 0); + --chat-assistant-border: oklch(0.2 0 0 / 0.12); + + /* Compact, precise radius. */ + --radius: 0.5rem; + + /* ── Motion ── product-fast. Springs alias to the standard eases so the + * theme stays calm — personality lives in the speed, never in bounce. */ + --duration-fast: 120ms; + --duration-base: 200ms; + --duration-slow: 300ms; + --duration-slower: 420ms; + --duration-spring: 200ms; + + --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); + --ease-spring-snappy: var(--ease-decelerate); + --ease-spring-bouncy: var(--ease-standard); + + /* ── Typography ── dense/pro register, fixed rem, full role ramp. */ + --text-hero-size: 3rem; + --text-hero-lh: 1.05; + --text-display-size: 2.25rem; + --text-display-lh: 1.1; + --text-headline-size: 1.625rem; + --text-headline-lh: 1.25; + --text-title-size: 1.25rem; + --text-title-lh: 1.3; + --text-subtitle-size: 1.0625rem; + --text-subtitle-lh: 1.45; + --text-body-size: 0.9375rem; + --text-body-lh: 1.5; + --text-label-size: 0.8125rem; + --text-label-lh: 1.125rem; + --text-caption-size: 0.75rem; + --text-caption-lh: 1rem; + + /* ── Elevation ── crisp, low, pure-neutral shadows. Borders separate; + * shadows only signal altitude. */ + --elevation-0: none; + --elevation-1: 0 1px 2px oklch(0 0 0 / 0.05); + --elevation-2: 0 1px 3px oklch(0 0 0 / 0.07), 0 1px 2px oklch(0 0 0 / 0.04); + --elevation-3: 0 4px 8px -2px oklch(0 0 0 / 0.08), 0 2px 4px -2px oklch(0 0 0 / 0.04); + --elevation-4: 0 10px 20px -5px oklch(0 0 0 / 0.1), 0 4px 8px -4px oklch(0 0 0 / 0.05); + --elevation-5: 0 18px 36px -8px oklch(0 0 0 / 0.15); + + --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 — true dark grey, not blue-black. Charcoal field (chroma 0), + * cards stepping visibly lighter (bg 0.19 → card 0.24 → popover 0.26 → + * muted 0.28) so altitude reads as lightness. The azure brightens a step to + * keep its pop; everything else stays colorless. + */ +.dark { + --background: oklch(0.19 0 0); + --foreground: oklch(0.93 0 0); + + --card: oklch(0.24 0 0); + --card-foreground: oklch(0.93 0 0); + + --popover: oklch(0.26 0 0); + --popover-foreground: oklch(0.93 0 0); + + --primary: oklch(0.55 0.19 258); + --primary-foreground: oklch(1 0 0); + + --secondary: oklch(0.28 0 0); + --secondary-foreground: oklch(0.9 0 0); + + --muted: oklch(0.28 0 0); + --muted-foreground: oklch(0.72 0 0); + + --accent: oklch(0.3 0.05 258); + --accent-foreground: oklch(0.85 0.08 258); + + --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.78 0.14 70); + --warning-foreground: oklch(0.22 0.05 70); + --info: oklch(0.68 0.12 230); + --info-foreground: oklch(0.16 0.03 230); + + --border: oklch(0.93 0 0 / 0.12); + --input: oklch(0.93 0 0 / 0.14); + --ring: oklch(0.62 0.17 258 / 0.6); + + --chart-1: oklch(0.62 0.17 258); + --chart-2: oklch(0.74 0.11 195); + --chart-3: oklch(0.66 0.14 305); + --chart-4: oklch(0.72 0.13 150); + --chart-5: oklch(0.68 0 0); + + --sidebar: oklch(0.21 0 0); + --sidebar-foreground: oklch(0.93 0 0); + --sidebar-primary: oklch(0.55 0.19 258); + --sidebar-primary-foreground: oklch(1 0 0); + --sidebar-accent: oklch(0.3 0.05 258); + --sidebar-accent-foreground: oklch(0.85 0.08 258); + --sidebar-border: oklch(0.93 0 0 / 0.09); + --sidebar-ring: oklch(0.62 0.17 258 / 0.6); + + --syntax-keyword: oklch(0.72 0.12 290); + --syntax-string: oklch(0.74 0.11 150); + --syntax-number: oklch(0.78 0.12 70); + --syntax-comment: oklch(0.6 0 0); + --syntax-function: oklch(0.72 0.13 258); + --syntax-type: oklch(0.74 0.09 220); + + /* ── Chat surfaces (dark) ── */ + --chat-user-bg: oklch(0.32 0 0); + --chat-user-fg: oklch(0.96 0 0); + --chat-assistant-bg: oklch(0.24 0 0); + --chat-assistant-fg: oklch(0.93 0 0); + --chat-assistant-border: oklch(0.93 0 0 / 0.12); + + --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 10px 20px -5px oklch(0 0 0 / 0.45), 0 4px 8px -4px oklch(0 0 0 / 0.25); + --elevation-5: 0 18px 36px -8px oklch(0 0 0 / 0.55); +} + +/* + * @layer base — Graphite's achromatic-precision philosophy. + * + * No glass, no gradients, no tint. One clean grotesque at two weights, + * colorless surfaces hairline-separated, and the azure reserved for focus, + * selection, links, and the primary action. Numbers align in columns because + * operators read columns. + */ +@layer base { + body { + min-height: 100vh; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + letter-spacing: -0.006em; + font-family: var(--font-sans); + font-size: var(--text-body-size); + line-height: var(--text-body-lh); + } + + /* Headings — same family as body; hierarchy is weight + tracking. */ + 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.022em; + color: var(--foreground); + } + + code, kbd, samp, pre, + [data-slot="code"], + [data-slot="code-block"] { + font-family: var(--font-mono); + } + + /* Inline code — achromatic chip, never accent-colored. */ + :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 azure moment. */ + ::selection { + background: color-mix(in oklch, var(--primary) 30%, transparent); + color: var(--foreground); + } + + /* Links — azure, underline-on-hover. */ + a { + color: color-mix(in oklch, var(--primary) 70%, 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); + } + + /* Dense utility — operators read columns of numbers. Tables and anything + * marked .num get lining, equal-width figures so columns align. */ + [data-slot="table"] { + font-variant-numeric: tabular-nums; + } + .num { + font-variant-numeric: tabular-nums; + letter-spacing: 0; + } +} + +/* + * Focus-visible — keyboard-first is a Graphite signature. Buttons get a + * crisp 2px azure outline with 2px offset; inputs get a flush 1px ring plus + * a border-color change (handled in the input block below). Placed outside + * @layer base so it beats Tailwind's default ring utilities. + */ +[data-slot="button"]:focus-visible { + outline: 2px solid var(--primary); + outline-offset: 2px; + box-shadow: none; +} +[data-slot="input"]:focus-visible, +[data-slot="textarea"]:focus-visible, +[data-slot="select-trigger"]:focus-visible { + outline: none; + border-color: var(--primary); + box-shadow: 0 0 0 1px var(--primary); +} + +/* Buttons — quiet weight; opacity absorb on press, product-fast. */ +[data-slot="button"] { + font-weight: 500; + letter-spacing: -0.006em; +} + +/* 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 dense 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 — white card surface, hairline border, azure 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) 24%, 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; +}