commit b25596c28140eddaa6c35ede0071f94aea37763d Author: jules Date: Tue Jul 7 23:20:55 2026 +1000 lib-theme-acid: 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..3ef5781 --- /dev/null +++ b/README.md @@ -0,0 +1,61 @@ +# lib-theme-acid + +**Chartreuse on black.** Festival/Wrapped energy for consumer surfaces that +want an unmistakable identity — the committed color carries 30–60% of the +surface, the opposite of a calm settings app. + +## Thesis + +Dark mode is the native identity: a near-black green-cast field +(`oklch(0.16 0.01 130)`) with electric acid chartreuse +(`oklch(0.87 0.21 128)`) carrying the type accents, the panels, and the +fills. The signature pair is **black ink on acid** (~13:1) — the primary +button, the user chat bubble, and the sidebar accents all run black-on-acid, +never white-on-green. Cards step a clear ≥0.05 lighter than the field; the +sidebar drops to a pure-black stripe so the acid accents hit hardest there. + +`:root` is the brand's **paper mode** — an honest light mode on a committed +chartreuse-washed paper (`oklch(0.96 0.05 120)`, visibly acid, not cream) +with near-black green ink. The primary inverts the signature: a deep +green-black fill (`oklch(0.3 0.09 130)`) carrying acid-bright labels. + +Status never borrows the brand's lane: destructive is hot red (hue 25, +light label); warning is amber 85, held clearly apart from the chartreuse in +both hue and chroma; **success is pushed to teal (hue 175, dark ink)** so no +second green ever fights the brand; info is cool blue 240. Charts are +acid-led with a cool spectrum behind (128 / 240 / 330 / 190 / 40). + +Radius is a chunky-sharp `0.75rem`. Motion is punchy and fast +(100/200/320/450ms) with **one real snappy spring** +(`--ease-spring-snappy: cubic-bezier(0.2, 1.4, 0.3, 1)` — actual overshoot) +reserved for affordances; gentle/bouncy alias to the standard ease. +Selection is acid at 40% alpha. Elevation in dark mode is pure-black +shadows; no glass (inert stubs). + +## Fonts + +Load in the **consuming app's** entry CSS, at the very top (never `@import` +inside `theme.css` — it breaks PostCSS ordering): + +```css +@import url("https://fonts.googleapis.com/css2?family=Archivo+Black&family=Space+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap"); +``` + +- **Archivo Black** — headings. A single-weight display black: it only ships + weight 400, so the theme's heading rule pins `font-weight: 400` and lets + the blackness of the cut do the work. Tracking is tight (`-0.02em`). + Don't ask for bold — faux-bolding this face ruins it. +- **Space Grotesk** (400/500/600/700) — body, labels, buttons. Consumer + register: body 1rem/1.55, label 0.875rem, caption 0.75rem, title + 1.375rem, subtitle 1.125rem, headline 1.875rem, display 2.75rem, + hero 3.5rem. +- **JetBrains Mono** (400/500/600) — code. Dark-mode syntax is a rave + terminal with discipline: acid keyword, cool companions (teal strings, + magenta numbers, blue functions, orange types). + +## Validation + +```bash +node create-crema-app/scripts/validate-theme.mjs lib-theme-acid/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..92a4dbd --- /dev/null +++ b/theme.css @@ -0,0 +1,426 @@ +/* + * Acid — chartreuse on black. + * + * Festival/Wrapped energy. The native identity is dark mode: a near-black + * green-cast field with electric acid chartreuse (oklch 0.87 0.21 128) + * carrying the type, the panels, and the fills. Black ink on acid is the + * signature pair (~13:1) — the primary button, the user bubble, the sidebar + * accents all run black-on-acid, never white-on-green. + * + * :root is the brand's paper mode — honest light mode on a committed + * chartreuse-washed paper (oklch 0.96 0.05 120; visibly acid, not cream) + * with near-black green ink and a DEEP green-black primary (0.3 0.09 130) + * whose labels are acid-bright. The saturated color carries 30–60% of the + * surface in both modes; this is the opposite of a calm settings app. + * + * Status stays out of the brand's lane: destructive is hot red (hue 25, + * light label), warning is amber 85 pulled clearly apart from the + * chartreuse in both hue and chroma, success is pushed to TEAL (hue 175, + * dark ink) so no second green ever fights the brand, info is cool blue 240. + * + * Type: Archivo Black (single-weight display black, weight 400, tight + * -0.02em) for headings, Space Grotesk for body, JetBrains Mono for code. + * Radius is a chunky-sharp 0.75rem. Motion is punchy and fast + * (100/200/320/450ms) with one real snappy spring for affordances. + * + * Token contract matches lib-theme-caffe-florian/theme.css (Crema v1). + */ + +/* + * Fonts: consuming apps load Archivo Black + Space Grotesk + 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: "Space Grotesk", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + --font-heading: "Archivo Black", "Space Grotesk", -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 Black", "Space Grotesk", -apple-system, BlinkMacSystemFont, sans-serif; + --body-family: "Space Grotesk", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + + /* ── Colors ── bright acid-tinted paper (committed chartreuse wash, not + * cream), near-white cards a step above it, near-black green ink, and a + * deep green-black primary carrying acid-bright labels. */ + --background: oklch(0.96 0.05 120); + --foreground: oklch(0.18 0.02 130); + + --card: oklch(0.985 0.02 120); + --card-foreground: oklch(0.18 0.02 130); + + --popover: oklch(0.99 0.015 120); + --popover-foreground: oklch(0.18 0.02 130); + + /* Primary — deep green-black fill with the acid chartreuse as its label: + * the paper-mode inversion of the dark-mode black-on-acid signature. */ + --primary: oklch(0.3 0.09 130); + --primary-foreground: oklch(0.9 0.21 126); + + --secondary: oklch(0.9 0.06 120); + --secondary-foreground: oklch(0.28 0.05 130); + + --muted: oklch(0.915 0.045 120); + --muted-foreground: oklch(0.42 0.05 130); + + /* Accent — an acid wash for hover/selection tints, with deep green ink. */ + --accent: oklch(0.9 0.1 122); + --accent-foreground: oklch(0.3 0.09 130); + + /* ── Status ── kept out of the chartreuse lane. Destructive = hot red + * (hue 25) with a light label; warning = amber 85, clearly apart from the + * brand in hue AND chroma; success = TEAL 175 with dark ink so no second + * green fights the chartreuse; info = cool blue 240. */ + --destructive: oklch(0.54 0.21 25); + --destructive-foreground: oklch(0.98 0.01 25); + --success: oklch(0.72 0.13 175); + --success-foreground: oklch(0.16 0.03 175); + --warning: oklch(0.8 0.16 85); + --warning-foreground: oklch(0.22 0.05 85); + --info: oklch(0.54 0.15 240); + --info-foreground: oklch(0.98 0.01 240); + + --border: oklch(0.18 0.02 130 / 0.14); + --input: oklch(0.18 0.02 130 / 0.16); + --ring: oklch(0.55 0.16 130 / 0.6); + + /* Chart palette — acid-led: deep chartreuse first, then cool blue / + * magenta / teal / hot orange. Hues ≥60° apart, adjacent lightness varied. */ + --chart-1: oklch(0.68 0.17 128); + --chart-2: oklch(0.55 0.15 240); + --chart-3: oklch(0.6 0.18 330); + --chart-4: oklch(0.7 0.12 190); + --chart-5: oklch(0.62 0.17 40); + + /* Sidebar — a deeper acid-paper stripe: more committed chartreuse than + * the field, with the deep-green primary and acid washes on it. */ + --sidebar: oklch(0.93 0.07 120); + --sidebar-foreground: oklch(0.18 0.02 130); + --sidebar-primary: oklch(0.3 0.09 130); + --sidebar-primary-foreground: oklch(0.9 0.21 126); + --sidebar-accent: oklch(0.88 0.11 122); + --sidebar-accent-foreground: oklch(0.3 0.09 130); + --sidebar-border: oklch(0.18 0.02 130 / 0.12); + --sidebar-ring: oklch(0.55 0.16 130 / 0.6); + + /* Syntax — deep chartreuse keyword leads; the rest stays cool. */ + --syntax-keyword: oklch(0.45 0.13 130); + --syntax-string: oklch(0.48 0.1 175); + --syntax-number: oklch(0.55 0.16 330); + --syntax-comment: oklch(0.52 0.03 130); + --syntax-function: oklch(0.48 0.14 240); + --syntax-type: oklch(0.5 0.13 40); + + /* ── Chat surfaces ── + * Contract for lib-chat-ui / lib-agent-ui / lib-agent-dock-ui. In paper + * mode the user bubble is the deep green-black panel with light text; + * the assistant bubble is a clean card with a hairline border. */ + --chat-user-bg: oklch(0.3 0.09 130); + --chat-user-fg: oklch(0.97 0.03 120); + --chat-assistant-bg: oklch(0.985 0.02 120); + --chat-assistant-fg: oklch(0.18 0.02 130); + --chat-assistant-border: oklch(0.18 0.02 130 / 0.14); + + /* Chunky-sharp radius. */ + --radius: 0.75rem; + + /* ── Motion ── punchy and fast. One REAL snappy spring (actual overshoot) + * for affordances; gentle/bouncy alias to the standard ease so the + * personality stays a single sharp gesture, not a bounce-house. */ + --duration-fast: 100ms; + --duration-base: 200ms; + --duration-slow: 320ms; + --duration-slower: 450ms; + --duration-spring: 380ms; + + --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: var(--ease-standard); + --ease-spring-snappy: cubic-bezier(0.2, 1.4, 0.3, 1); + --ease-spring-bouncy: var(--ease-standard); + + /* ── Typography ── consumer register, fixed rem, full role ramp. */ + --text-hero-size: 3.5rem; + --text-hero-lh: 1.02; + --text-display-size: 2.75rem; + --text-display-lh: 1.05; + --text-headline-size: 1.875rem; + --text-headline-lh: 1.15; + --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: 1rem; + + /* ── Elevation ── crisp green-black shadows on the paper. */ + --elevation-0: none; + --elevation-1: 0 1px 2px oklch(0.18 0.03 130 / 0.08); + --elevation-2: 0 1px 3px oklch(0.18 0.03 130 / 0.1), 0 1px 2px oklch(0.18 0.03 130 / 0.06); + --elevation-3: 0 4px 8px -2px oklch(0.18 0.03 130 / 0.1), 0 2px 4px -2px oklch(0.18 0.03 130 / 0.06); + --elevation-4: 0 12px 24px -6px oklch(0.18 0.03 130 / 0.13), 0 4px 8px -4px oklch(0.18 0.03 130 / 0.06); + --elevation-5: 0 20px 40px -8px oklch(0.18 0.03 130 / 0.2); + + --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 native identity. Near-black green-cast field, cards + * stepping clearly lighter, and the full acid chartreuse as primary with + * BLACK ink on it (the signature pair, ~13:1). The acid carries the type + * accents, the panels, and the fills — a rave terminal with discipline. + */ +.dark { + --background: oklch(0.16 0.01 130); + --foreground: oklch(0.95 0.03 120); + + --card: oklch(0.21 0.012 130); + --card-foreground: oklch(0.95 0.03 120); + + --popover: oklch(0.23 0.014 130); + --popover-foreground: oklch(0.95 0.03 120); + + /* Black-on-acid — the signature. */ + --primary: oklch(0.87 0.21 128); + --primary-foreground: oklch(0.15 0.02 130); + + --secondary: oklch(0.26 0.02 130); + --secondary-foreground: oklch(0.93 0.03 120); + + --muted: oklch(0.25 0.015 130); + --muted-foreground: oklch(0.75 0.05 125); + + /* Accent — a dark acid wash with acid-bright ink. */ + --accent: oklch(0.28 0.06 128); + --accent-foreground: oklch(0.88 0.18 126); + + --destructive: oklch(0.56 0.21 25); + --destructive-foreground: oklch(0.98 0.01 25); + --success: oklch(0.76 0.13 175); + --success-foreground: oklch(0.16 0.03 175); + --warning: oklch(0.82 0.16 85); + --warning-foreground: oklch(0.22 0.05 85); + --info: oklch(0.68 0.14 240); + --info-foreground: oklch(0.16 0.03 240); + + --border: oklch(0.95 0.03 120 / 0.12); + --input: oklch(0.95 0.03 120 / 0.14); + --ring: oklch(0.87 0.21 128 / 0.6); + + /* Chart palette — acid leads at full charge; cool spectrum follows. */ + --chart-1: oklch(0.87 0.21 128); + --chart-2: oklch(0.68 0.14 240); + --chart-3: oklch(0.74 0.17 330); + --chart-4: oklch(0.8 0.12 190); + --chart-5: oklch(0.72 0.16 40); + + /* Sidebar — a pure-black stripe with acid accents. */ + --sidebar: oklch(0.13 0.008 130); + --sidebar-foreground: oklch(0.95 0.03 120); + --sidebar-primary: oklch(0.87 0.21 128); + --sidebar-primary-foreground: oklch(0.15 0.02 130); + --sidebar-accent: oklch(0.26 0.06 128); + --sidebar-accent-foreground: oklch(0.88 0.18 126); + --sidebar-border: oklch(0.95 0.03 120 / 0.09); + --sidebar-ring: oklch(0.87 0.21 128 / 0.6); + + /* Syntax — rave terminal, disciplined: acid keyword against cool + * companions, nothing else allowed near the chartreuse lane. */ + --syntax-keyword: oklch(0.85 0.2 128); + --syntax-string: oklch(0.78 0.11 175); + --syntax-number: oklch(0.76 0.16 330); + --syntax-comment: oklch(0.6 0.03 130); + --syntax-function: oklch(0.72 0.13 240); + --syntax-type: oklch(0.76 0.14 40); + + /* ── Chat surfaces (dark) ── + * User bubble IS the signature: acid chartreuse with black ink. */ + --chat-user-bg: oklch(0.87 0.21 128); + --chat-user-fg: oklch(0.15 0.02 130); + --chat-assistant-bg: oklch(0.21 0.012 130); + --chat-assistant-fg: oklch(0.95 0.03 120); + --chat-assistant-border: oklch(0.95 0.03 120 / 0.12); + + /* ── Elevation (dark) ── pure-black shadows. */ + --elevation-1: 0 1px 2px oklch(0 0 0 / 0.35); + --elevation-2: 0 1px 3px oklch(0 0 0 / 0.45), 0 1px 2px oklch(0 0 0 / 0.28); + --elevation-3: 0 4px 8px -2px oklch(0 0 0 / 0.45), 0 2px 4px -2px oklch(0 0 0 / 0.28); + --elevation-4: 0 12px 24px -6px oklch(0 0 0 / 0.5), 0 4px 8px -4px oklch(0 0 0 / 0.28); + --elevation-5: 0 20px 40px -8px oklch(0 0 0 / 0.6); +} + +/* + * @layer base — Acid's festival philosophy, disciplined. + * + * Archivo Black headings hit like posters; Space Grotesk keeps the body + * legible; the chartreuse is committed (it carries panels and fills, not + * just a ring) but status meaning never borrows it. Selection is acid at + * 40% alpha — even highlighting text is on-brand. + */ +@layer base { + body { + min-height: 100vh; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + letter-spacing: -0.008em; + font-family: var(--font-sans); + font-size: var(--text-body-size); + line-height: var(--text-body-lh); + } + + /* Headings — Archivo Black: single-weight display black. The face only + * ships 400, so the rule pins weight 400 and lets the blackness of the + * cut do the shouting; tracking pulled tight. */ + 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; + letter-spacing: -0.02em; + /* 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. */ + :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 — acid at 40% alpha. */ + ::selection { + background: color-mix(in oklch, var(--primary) 40%, transparent); + color: var(--foreground); + } + + /* Links — brand-inked, underline-on-hover. */ + a { + color: color-mix(in oklch, var(--primary) 75%, var(--foreground)); + text-decoration-color: color-mix(in oklch, var(--primary) 45%, 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 acid 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; snappy spring press, festival-fast. */ +[data-slot="button"] { + font-weight: 600; + letter-spacing: -0.008em; + transition: + opacity var(--duration-fast) var(--ease-standard), + transform var(--duration-spring) var(--ease-spring-snappy); +} + +/* 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 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.92; +} +[data-slot="button"]:active:not(:disabled) { + opacity: 0.85; + transform: scale(0.97); +} + +/* Inputs — card surface, hairline border, acid 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) 25%, 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; +}