commit 15558df0ff75e8057bca3b2e586b1c2cd3ef65ac Author: jules Date: Tue Jul 7 22:56:30 2026 +1000 lib-theme-flint: 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..7228b98 --- /dev/null +++ b/README.md @@ -0,0 +1,35 @@ +# lib-theme-flint + +**Flint — instrument panel.** Braun/Dieter-Rams workshop calm with one signal spark: a warm-stone +gray field, white cards, graphite-umber ink, and a single signal-orange accent — the machine's +indicator lamp — spent only on the primary action, selection, focus, and the live dot. Dark mode is +the panel at night: dark warm graphite (never blue) with the orange glowing luminous under dark ink. +Dense/pro register, machined 0.625rem radius, crisp low shadows, mechanical-quick motion +(120/220/320/450ms, springs aliased — a good tool clicks, it doesn't bounce). Built for +operator/console/admin surfaces. + +## Fonts + +Themes never `@import` fonts (PostCSS ordering). The **consuming app** loads them at the very top of +its entry CSS (`app/app.css`): + +```css +@import url("https://fonts.googleapis.com/css2?family=Archivo:wght@400;500;600;700&family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap"); +``` + +| Role | Stack | +|---|---| +| Headings (`--font-heading`) | Archivo → Inter → system sans. Grotesk with DIN-instrument character; headings render at weight 600. | +| Body (`--font-sans`) | Inter → system sans. Body 0.9375rem / 1.5. | +| Code (`--font-mono`) | JetBrains Mono → ui-monospace → SF Mono → Menlo → Consolas. | + +Notes: tables (`[data-slot="table"]`) and the `.num` utility class get `font-variant-numeric: +tabular-nums` — instrument readouts demand aligned digits. Keyboard-first focus is a signature: +buttons get an always-visible 2px signal-orange outline (2px offset); inputs a flush 1px ring + +border-color change. + +## Validate + +```bash +node ../create-crema-app/scripts/validate-theme.mjs theme.css +``` diff --git a/theme.css b/theme.css new file mode 100644 index 0000000..37acda9 --- /dev/null +++ b/theme.css @@ -0,0 +1,423 @@ +/* + * Flint — instrument panel. + * + * Braun-era workshop calm with one signal spark. The whole light surface is a + * warm-stone gray (hue ~80, chroma 0.004 — clearly warmer than graphite's + * chroma-0, far quieter than any cream), with clean white cards and a deep + * graphite-umber ink (hue ~60). ONE accent: signal orange (hue ~48) — the + * machine's indicator lamp — spent only on the primary action, selection, + * focus, and the live/recording dot. It carries dark umber ink on top + * (bright-swatch/dark-ink) so it lands high above the contrast floor. + * + * Status hues are held clearly apart from the lamp: destructive red is + * DEEPENED at hue 25 so it never blurs into the orange at 48; warning sits at + * ochre hue 85, a distinct earthy yellow, never a second orange; success green + * 150, info blue 248. + * + * Dark mode is the panel at night: dark warm graphite (hue ~60, low chroma, + * never blue), cards a full lightness step above the field (≥0.05 L), and the + * same signal orange glowing luminous with dark ink. + * + * Type: Archivo for headings (grotesk with DIN-instrument character, weight + * 600), Inter for body, JetBrains Mono for code. Dense/pro register — body + * 0.9375rem, fixed rem, full role ramp. Radius is a machined, softly + * chamfered 0.625rem. Shadows are crisp, low, and tight. Motion is mechanical + * and quick (120/220/320/450ms); springs alias to the standard eases — a good + * tool clicks, it doesn't bounce. + * + * Token contract matches lib-theme-caffe-florian/theme.css (Crema v1). + */ + +/* + * Fonts: consuming apps load Archivo + 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: "Archivo", "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: "Archivo", "Inter", -apple-system, BlinkMacSystemFont, sans-serif; + --body-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + + /* ── Colors ── warm-stone gray field, white cards, graphite-umber ink, + * one signal-orange indicator lamp. */ + --background: oklch(0.945 0.004 80); + --foreground: oklch(0.22 0.008 60); + + --card: oklch(1 0 0); + --card-foreground: oklch(0.22 0.008 60); + + --popover: oklch(0.99 0.002 80); + --popover-foreground: oklch(0.22 0.008 60); + + /* Primary — the indicator lamp. Signal orange at hue 48 (pushed off the + * destructive red at 25), bright enough to glow, with near-black umber ink + * on top (bright-swatch/dark-ink) — crisp, never orange-on-orange. */ + --primary: oklch(0.66 0.18 48); + --primary-foreground: oklch(0.15 0.025 48); + + --secondary: oklch(0.915 0.005 75); + --secondary-foreground: oklch(0.3 0.008 60); + + --muted: oklch(0.915 0.005 75); + --muted-foreground: oklch(0.45 0.01 60); + + /* Accent — the lamp as a hover/selection tint: a faint warm-orange wash + * with deep burnt-orange ink, not a second competing fill. */ + --accent: oklch(0.92 0.035 55); + --accent-foreground: oklch(0.42 0.11 45); + + /* ── Status ── held clearly apart from the signal orange (48). Destructive + * red is deepened at hue 25 with a light label; warning is ochre at hue 85 + * (earthy yellow, never a second orange) with dark ink; success/info follow + * the bright-swatch/dark-ink pattern where legible. */ + --destructive: oklch(0.52 0.2 25); + --destructive-foreground: oklch(0.98 0.01 25); + --success: oklch(0.62 0.14 150); + --success-foreground: oklch(0.17 0.03 150); + --warning: oklch(0.72 0.14 85); + --warning-foreground: oklch(0.24 0.05 85); + --info: oklch(0.54 0.13 248); + --info-foreground: oklch(0.98 0.01 248); + + --border: oklch(0.22 0.008 60 / 0.12); + --input: oklch(0.22 0.008 60 / 0.14); + --ring: oklch(0.66 0.18 48 / 0.6); + + /* Chart palette — signal-orange lead, then restrained spaced hues: + * blue / teal / violet / green. Adjacent hues ≥60° apart, adjacent + * lightness varied. */ + --chart-1: oklch(0.68 0.17 48); + --chart-2: oklch(0.56 0.13 248); + --chart-3: oklch(0.66 0.1 180); + --chart-4: oklch(0.54 0.14 300); + --chart-5: oklch(0.68 0.13 130); + + --sidebar: oklch(0.925 0.005 78); + --sidebar-foreground: oklch(0.22 0.008 60); + --sidebar-primary: oklch(0.66 0.18 48); + --sidebar-primary-foreground: oklch(0.15 0.025 48); + --sidebar-accent: oklch(0.9 0.035 55); + --sidebar-accent-foreground: oklch(0.42 0.11 45); + --sidebar-border: oklch(0.22 0.008 60 / 0.1); + --sidebar-ring: oklch(0.66 0.18 48 / 0.6); + + /* Syntax — muted-precise warm-neutral base; the number readout gets the + * deep signal hue, everything else stays restrained. */ + --syntax-keyword: oklch(0.5 0.12 300); + --syntax-string: oklch(0.5 0.11 150); + --syntax-number: oklch(0.55 0.14 48); + --syntax-comment: oklch(0.55 0.008 60); + --syntax-function: oklch(0.5 0.13 248); + --syntax-type: oklch(0.52 0.09 190); + + /* ── Chat surfaces ── + * Contract for lib-chat-ui / lib-agent-ui / lib-agent-dock-ui. User bubble + * is a deep warm-graphite panel with light text (neutral, so the orange + * stays scarce); assistant bubble is a clean card with a hairline border. */ + --chat-user-bg: oklch(0.28 0.01 60); + --chat-user-fg: oklch(0.96 0.004 80); + --chat-assistant-bg: oklch(1 0 0); + --chat-assistant-fg: oklch(0.22 0.008 60); + --chat-assistant-border: oklch(0.22 0.008 60 / 0.12); + + /* Machined, softly chamfered. */ + --radius: 0.625rem; + + /* ── Motion ── mechanical and quick; a good tool clicks, it doesn't + * bounce. Springs alias to the standard eases. */ + --duration-fast: 120ms; + --duration-base: 220ms; + --duration-slow: 320ms; + --duration-slower: 450ms; + --duration-spring: 220ms; + + --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.4; + --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, machined: small offsets, tight blur. */ + --elevation-0: none; + --elevation-1: 0 1px 1px oklch(0.22 0.008 60 / 0.07); + --elevation-2: 0 1px 2px oklch(0.22 0.008 60 / 0.08), 0 1px 1px oklch(0.22 0.008 60 / 0.05); + --elevation-3: 0 2px 6px -1px oklch(0.22 0.008 60 / 0.1), 0 1px 2px -1px oklch(0.22 0.008 60 / 0.06); + --elevation-4: 0 8px 16px -4px oklch(0.22 0.008 60 / 0.12), 0 2px 4px -2px oklch(0.22 0.008 60 / 0.06); + --elevation-5: 0 14px 28px -6px oklch(0.22 0.008 60 / 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 panel at night. Dark warm graphite (hue ~60, low chroma, + * never blue), cards a full step above the field, popover a step above card + * and capped below --muted; the signal orange glows luminous with dark ink. + */ +.dark { + --background: oklch(0.2 0.006 60); + --foreground: oklch(0.93 0.005 75); + + --card: oklch(0.26 0.007 60); + --card-foreground: oklch(0.93 0.005 75); + + --popover: oklch(0.28 0.007 60); + --popover-foreground: oklch(0.93 0.005 75); + + --primary: oklch(0.72 0.185 48); + --primary-foreground: oklch(0.15 0.025 48); + + --secondary: oklch(0.29 0.007 60); + --secondary-foreground: oklch(0.9 0.005 75); + + --muted: oklch(0.3 0.007 60); + --muted-foreground: oklch(0.72 0.01 70); + + --accent: oklch(0.32 0.05 48); + --accent-foreground: oklch(0.85 0.11 55); + + --destructive: oklch(0.56 0.2 25); + --destructive-foreground: oklch(0.98 0.01 25); + --success: oklch(0.7 0.14 150); + --success-foreground: oklch(0.17 0.03 150); + --warning: oklch(0.78 0.13 85); + --warning-foreground: oklch(0.24 0.05 85); + --info: oklch(0.68 0.13 248); + --info-foreground: oklch(0.16 0.03 248); + + --border: oklch(0.93 0.005 75 / 0.12); + --input: oklch(0.93 0.005 75 / 0.14); + --ring: oklch(0.72 0.185 48 / 0.6); + + --chart-1: oklch(0.74 0.17 48); + --chart-2: oklch(0.66 0.13 248); + --chart-3: oklch(0.72 0.1 180); + --chart-4: oklch(0.64 0.14 300); + --chart-5: oklch(0.74 0.13 130); + + --sidebar: oklch(0.22 0.007 60); + --sidebar-foreground: oklch(0.93 0.005 75); + --sidebar-primary: oklch(0.72 0.185 48); + --sidebar-primary-foreground: oklch(0.15 0.025 48); + --sidebar-accent: oklch(0.32 0.05 48); + --sidebar-accent-foreground: oklch(0.85 0.11 55); + --sidebar-border: oklch(0.93 0.005 75 / 0.09); + --sidebar-ring: oklch(0.72 0.185 48 / 0.6); + + --syntax-keyword: oklch(0.72 0.12 300); + --syntax-string: oklch(0.74 0.12 150); + --syntax-number: oklch(0.76 0.15 48); + --syntax-comment: oklch(0.6 0.008 65); + --syntax-function: oklch(0.72 0.13 248); + --syntax-type: oklch(0.72 0.09 190); + + /* ── Chat surfaces (dark) ── */ + --chat-user-bg: oklch(0.34 0.008 60); + --chat-user-fg: oklch(0.96 0.004 80); + --chat-assistant-bg: oklch(0.26 0.007 60); + --chat-assistant-fg: oklch(0.93 0.005 75); + --chat-assistant-border: oklch(0.93 0.005 75 / 0.12); + + --elevation-1: 0 1px 1px oklch(0 0 0 / 0.32); + --elevation-2: 0 1px 2px oklch(0 0 0 / 0.4), 0 1px 1px oklch(0 0 0 / 0.25); + --elevation-3: 0 2px 6px -1px oklch(0 0 0 / 0.42), 0 1px 2px -1px oklch(0 0 0 / 0.25); + --elevation-4: 0 8px 16px -4px oklch(0 0 0 / 0.46), 0 2px 4px -2px oklch(0 0 0 / 0.25); + --elevation-5: 0 14px 28px -6px oklch(0 0 0 / 0.55); +} + +/* + * @layer base — Flint's instrument-panel philosophy. + * + * No glass, no flourish. DIN-flavored grotesk headings, calm warm-neutral + * surfaces hairline-separated, aligned digits everywhere numbers live, and the + * signal-orange lamp reserved for focus, selection, and the primary action. + * Everything else stays quiet and out of the way. + */ +@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 — Archivo, machined and firm. */ + 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.015em; + color: var(--foreground); + } + + code, kbd, samp, pre, + [data-slot="code"], + [data-slot="code-block"] { + font-family: var(--font-mono); + } + + /* Inline code — warm-neutral chip. */ + :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; + } + + /* Instrument readouts — aligned digits wherever numbers stack. */ + [data-slot="table"], + .num { + font-variant-numeric: tabular-nums; + } + + /* Selection — the signal lamp. */ + ::selection { + background: color-mix(in oklch, var(--primary) 30%, transparent); + color: var(--foreground); + } + + /* Links — signal-tinted ink, underline-on-hover. */ + a { + color: color-mix(in oklch, var(--primary) 65%, 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 — keyboard-first is a Flint signature. Buttons get a crisp, + * always-visible 2px signal-orange outline with 2px offset; inputs get a flush + * 1px ring + border-color change (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, mechanical-fast. */ +[data-slot="button"] { + font-weight: 500; + letter-spacing: -0.004em; +} + +/* 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 — clean card surface, hairline border, signal 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; +}