lib-theme-grape: 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 <noreply@anthropic.com>
This commit is contained in:
jules
2026-07-07 23:20:55 +10:00
commit d7565f448a
2 changed files with 493 additions and 0 deletions

62
README.md Normal file
View File

@@ -0,0 +1,62 @@
# lib-theme-grape
**Grape — violet night-lounge.** A bold, identity-forward Crema theme where the
committed color IS the surface: the entire world is a deep luminous
grape-violet field (hue 300) — Skyrise's violet ancestry taken to a full
drench, without glass or iridescence. There are no white cards; cards are
clearly-lighter violet panels lifted off the field, popovers a step above
cards, the sidebar a deeper violet stripe, and everything separates with
white-alpha hairlines — light, not darkness. Ink is near-white with a violet
tint.
One counterpoint does all the pointing: a hot coral-pink primary
(`oklch(0.7 0.19 20)`) carrying dark wine ink, spent on the action, the
selection, the focus ring, and the user's chat bubble. Destructive stays a
*deep crimson fill with a light label* — a different lightness class and
construction from the bright coral primary, so the two hue-20 neighbours never
read as the same swatch. Success is mint, warning amber, info cool cyan — all
bright swatches with dark ink, tuned to read on violet panels.
The theme is violet in **both** modes. `.dark` is simply *later that night*:
the field deepens (`0.38 → 0.26 L`), the panels keep the same visible step
above it, and the coral burns slightly brighter. The dark violet ladder keeps
muted above popover: bg `0.26` → card `0.32` → popover `0.34` → muted `0.36`.
Radius is a soft-lounge `1.125rem`. Motion is languid-confident
(160/320/480/640ms) with a real gentle spring on the emphasized ease. Shadows
are deep violet-black pools. Consumer type register: 1rem body at 1.6
line-height.
All contract contrast floors verified in both modes on both surfaces (field
and card) — including the risky one in an all-violet world,
`muted-foreground`, which lands ≥5:1 on field, card, and the muted wash in
both modes. `✓ PASS` from `validate-theme.mjs`.
## Fonts
Load in the **consuming app's** entry CSS (very top, before anything else —
themes never `@import` fonts themselves):
```css
@import url("https://fonts.googleapis.com/css2?family=Instrument+Serif&family=Instrument+Sans:wght@400;500;600;700&family=Geist+Mono:wght@400;500;600&display=swap");
```
| Role | Family | Notes |
|---|---|---|
| Headings | **Instrument Serif** | Single weight (400) — the serif itself is the emphasis; the theme pins headings to `font-weight: 400; letter-spacing: -0.01em` and never fakes bold. |
| Body / UI | **Instrument Sans** | 400 / 500 / 600 / 700. Warm modern grotesque; buttons sit at 500. |
| Code | **Geist Mono** | 400 / 500 / 600. Inline code renders as a white-alpha tint chip with foreground ink. |
## Usage
```css
/* app entry CSS */
@import url("https://fonts.googleapis.com/css2?family=Instrument+Serif&family=Instrument+Sans:wght@400;500;600;700&family=Geist+Mono:wght@400;500;600&display=swap");
@import "../../lib-theme-grape/theme.css";
```
Validate:
```bash
node create-crema-app/scripts/validate-theme.mjs lib-theme-grape/theme.css
```

431
theme.css Normal file
View File

@@ -0,0 +1,431 @@
/*
* Grape — violet night-lounge.
*
* A committed, identity-forward theme: the surface itself is the brand. The
* whole world is a deep luminous grape-violet FIELD (hue 300) — Skyrise's
* violet ancestry taken to a full drench, with no glass and no iridescence.
* There are no white cards anywhere: cards are clearly-lighter violet panels
* lifted off the field, popovers a step above cards, the sidebar a deeper
* violet stripe. Ink is near-white with a violet tint; hairlines are
* white-alpha. The one action glows: a hot coral-pink primary (hue 20) with
* dark ink on top, the single warm counterpoint in an otherwise violet room.
*
* Dark mode is later that night — the same lounge with the lights turned
* down: the field deepens, the panels follow, and the coral burns slightly
* brighter against it. The theme is violet in BOTH modes; .dark only changes
* the hour.
*
* Status colors are tuned to read on violet panels: destructive stays a deep
* crimson fill with a light label (a different lightness class from the
* bright coral primary, so the two hue-20 swatches never read as siblings);
* success is mint, warning amber, info cool cyan — all bright swatches with
* dark ink.
*
* Type is a lounge voice: Instrument Serif (single weight 400) for headings,
* Instrument Sans for body, Geist Mono for code. Consumer register — 1rem
* body with a generous 1.6 line-height. Radius is a soft 1.125rem; motion is
* languid-confident with a real gentle spring on the emphasized ease; shadows
* are deep violet-black pools.
*
* Token contract matches lib-theme-caffe-florian/theme.css (Crema v1).
*/
/*
* Fonts: consuming apps load Instrument Serif + Instrument Sans + Geist 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: "Instrument Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
--font-heading: "Instrument Serif", Georgia, "Times New Roman", serif;
--font-mono: "Geist 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: "Instrument Serif", Georgia, "Times New Roman", serif;
--body-family: "Instrument Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
/* ── Colors ── the grape field. Background is a deep luminous violet;
* cards are clearly-lighter violet panels (+0.06 L — the step must be
* visible); ink is near-white with a violet tint. NOT white cards — the
* whole world is violet. */
--background: oklch(0.38 0.13 300);
--foreground: oklch(0.97 0.012 300);
--card: oklch(0.44 0.12 300);
--card-foreground: oklch(0.97 0.012 300);
--popover: oklch(0.46 0.115 300);
--popover-foreground: oklch(0.97 0.012 300);
/* Primary — the hot coral-pink counterpoint. One warm glow against the
* violet, carrying a deep wine-dark ink (bright-swatch/dark-ink pairing,
* ~6.3:1) so the action reads at label sizes. */
--primary: oklch(0.7 0.19 20);
--primary-foreground: oklch(0.2 0.06 20);
--secondary: oklch(0.5 0.11 300);
--secondary-foreground: oklch(0.97 0.012 300);
/* Muted — a recessed violet wash sitting between field and card, so muted
* chrome sinks rather than lifts. Muted ink is a pale lavender, verified
* ≥4.5:1 on field, card, AND the muted wash itself. */
--muted: oklch(0.42 0.115 300);
--muted-foreground: oklch(0.85 0.045 300);
/* Accent — a lighter violet wash with near-white ink: hover/selection
* lift inside the violet family, never a second warm fill. */
--accent: oklch(0.52 0.12 300);
--accent-foreground: oklch(0.98 0.01 300);
/* ── Status ── tuned to read on violet panels. All four are BRIGHT
* swatches with dark ink: on a mid-lightness violet world, status-colored
* text (badge chips render `text-destructive` on a 10% tint) can only
* clear 4.5:1 if the status color itself is light. Destructive is a
* luminous rose (hue 5) — separated from the coral primary (hue 20) by
* hue and by a visibly higher lightness. */
--destructive: oklch(0.84 0.11 5);
--destructive-foreground: oklch(0.2 0.04 5);
--success: oklch(0.75 0.13 160);
--success-foreground: oklch(0.2 0.05 160);
--warning: oklch(0.8 0.13 85);
--warning-foreground: oklch(0.22 0.05 85);
--info: oklch(0.75 0.1 220);
--info-foreground: oklch(0.2 0.04 220);
/* White-alpha hairlines — the violet world separates with light, not
* darkness. */
--border: oklch(1 0 0 / 0.16);
--input: oklch(1 0 0 / 0.2);
--ring: oklch(0.7 0.19 20 / 0.6);
/* Chart palette — coral lead, then mint / amber / cyan / pale orchid:
* spaced hues that all read on violet panels, adjacent-index lightness
* varied. */
--chart-1: oklch(0.72 0.18 20);
--chart-2: oklch(0.78 0.13 160);
--chart-3: oklch(0.85 0.12 85);
--chart-4: oklch(0.72 0.11 220);
--chart-5: oklch(0.86 0.08 320);
/* Sidebar — a deeper violet stripe against the field. */
--sidebar: oklch(0.33 0.12 300);
--sidebar-foreground: oklch(0.97 0.012 300);
--sidebar-primary: oklch(0.7 0.19 20);
--sidebar-primary-foreground: oklch(0.2 0.06 20);
--sidebar-accent: oklch(0.44 0.12 300);
--sidebar-accent-foreground: oklch(0.98 0.01 300);
--sidebar-border: oklch(1 0 0 / 0.12);
--sidebar-ring: oklch(0.7 0.19 20 / 0.6);
/* Syntax — bright, tuned to read on violet card panels. */
--syntax-keyword: oklch(0.82 0.12 350);
--syntax-string: oklch(0.85 0.1 160);
--syntax-number: oklch(0.87 0.1 85);
--syntax-comment: oklch(0.76 0.04 300);
--syntax-function: oklch(0.83 0.09 220);
--syntax-type: oklch(0.88 0.06 300);
/* ── Chat surfaces ──
* Contract for lib-agent-chat-ui / lib-agent-dock-ui. User bubble is the
* coral glow with dark ink (the user's turn IS the action); assistant
* bubble is a violet card panel with a white-alpha hairline. */
--chat-user-bg: oklch(0.7 0.19 20);
--chat-user-fg: oklch(0.2 0.06 20);
--chat-assistant-bg: oklch(0.44 0.12 300);
--chat-assistant-fg: oklch(0.97 0.012 300);
--chat-assistant-border: oklch(1 0 0 / 0.16);
/* Soft-lounge radius. */
--radius: 1.125rem;
/* ── Motion ── languid-confident. Slower than a product theme on
* purpose — the lounge doesn't rush — with a real gentle spring on the
* emphasized ease and the spring family. */
--duration-fast: 160ms;
--duration-base: 320ms;
--duration-slow: 480ms;
--duration-slower: 640ms;
--duration-spring: 480ms;
--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.34, 1.24, 0.54, 1);
--ease-spring-gentle: cubic-bezier(0.38, 1.18, 0.58, 1);
--ease-spring-snappy: cubic-bezier(0.3, 1.35, 0.5, 1);
--ease-spring-bouncy: cubic-bezier(0.3, 1.55, 0.42, 1);
/* ── Typography ── consumer register, fixed rem, full role ramp. */
--text-hero-size: 3.5rem;
--text-hero-lh: 1.05;
--text-display-size: 2.75rem;
--text-display-lh: 1.1;
--text-headline-size: 2rem;
--text-headline-lh: 1.2;
--text-title-size: 1.5rem;
--text-title-lh: 1.3;
--text-subtitle-size: 1.1875rem;
--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.0625rem;
/* ── Elevation ── deep violet-black pools; on a dark field the shadows
* carry more alpha than a daylight theme's. */
--elevation-0: none;
--elevation-1: 0 1px 2px oklch(0.12 0.06 300 / 0.3);
--elevation-2: 0 2px 4px oklch(0.12 0.06 300 / 0.34), 0 1px 2px oklch(0.12 0.06 300 / 0.24);
--elevation-3: 0 6px 12px -2px oklch(0.12 0.06 300 / 0.38), 0 2px 4px -2px oklch(0.12 0.06 300 / 0.26);
--elevation-4: 0 14px 28px -6px oklch(0.1 0.05 300 / 0.44), 0 4px 10px -4px oklch(0.1 0.05 300 / 0.28);
--elevation-5: 0 24px 48px -8px oklch(0.1 0.05 300 / 0.55);
--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 — later that night. The field deepens toward violet-black, the
* panels keep the same visible step above it, and the coral counterpoint
* burns slightly brighter. The ladder keeps muted ABOVE popover
* (bg 0.26 → card 0.32 → popover 0.34 → muted 0.36) so muted chrome still
* reads as a lift on the darker field.
*/
.dark {
--background: oklch(0.26 0.1 300);
--foreground: oklch(0.97 0.012 300);
--card: oklch(0.32 0.1 300);
--card-foreground: oklch(0.97 0.012 300);
--popover: oklch(0.34 0.1 300);
--popover-foreground: oklch(0.97 0.012 300);
--primary: oklch(0.73 0.19 20);
--primary-foreground: oklch(0.2 0.06 20);
--secondary: oklch(0.36 0.1 300);
--secondary-foreground: oklch(0.97 0.012 300);
--muted: oklch(0.36 0.1 300);
--muted-foreground: oklch(0.83 0.05 300);
--accent: oklch(0.4 0.11 300);
--accent-foreground: oklch(0.98 0.01 300);
--destructive: oklch(0.84 0.11 5);
--destructive-foreground: oklch(0.2 0.04 5);
--success: oklch(0.77 0.13 160);
--success-foreground: oklch(0.2 0.05 160);
--warning: oklch(0.82 0.13 85);
--warning-foreground: oklch(0.22 0.05 85);
--info: oklch(0.77 0.1 220);
--info-foreground: oklch(0.2 0.04 220);
--border: oklch(1 0 0 / 0.14);
--input: oklch(1 0 0 / 0.18);
--ring: oklch(0.73 0.19 20 / 0.6);
--chart-1: oklch(0.74 0.18 20);
--chart-2: oklch(0.8 0.13 160);
--chart-3: oklch(0.87 0.12 85);
--chart-4: oklch(0.74 0.11 220);
--chart-5: oklch(0.88 0.08 320);
--sidebar: oklch(0.22 0.09 300);
--sidebar-foreground: oklch(0.97 0.012 300);
--sidebar-primary: oklch(0.73 0.19 20);
--sidebar-primary-foreground: oklch(0.2 0.06 20);
--sidebar-accent: oklch(0.32 0.1 300);
--sidebar-accent-foreground: oklch(0.98 0.01 300);
--sidebar-border: oklch(1 0 0 / 0.1);
--sidebar-ring: oklch(0.73 0.19 20 / 0.6);
--syntax-keyword: oklch(0.84 0.12 350);
--syntax-string: oklch(0.87 0.1 160);
--syntax-number: oklch(0.89 0.1 85);
--syntax-comment: oklch(0.74 0.04 300);
--syntax-function: oklch(0.85 0.09 220);
--syntax-type: oklch(0.9 0.06 300);
/* ── Chat surfaces (dark) ── */
--chat-user-bg: oklch(0.73 0.19 20);
--chat-user-fg: oklch(0.2 0.06 20);
--chat-assistant-bg: oklch(0.32 0.1 300);
--chat-assistant-fg: oklch(0.97 0.012 300);
--chat-assistant-border: oklch(1 0 0 / 0.14);
--elevation-1: 0 1px 2px oklch(0 0 0 / 0.35);
--elevation-2: 0 2px 4px oklch(0 0 0 / 0.4), 0 1px 2px oklch(0 0 0 / 0.28);
--elevation-3: 0 6px 12px -2px oklch(0 0 0 / 0.45), 0 2px 4px -2px oklch(0 0 0 / 0.3);
--elevation-4: 0 14px 28px -6px oklch(0 0 0 / 0.5), 0 4px 10px -4px oklch(0 0 0 / 0.3);
--elevation-5: 0 24px 48px -8px oklch(0 0 0 / 0.6);
}
/*
* @layer base — Grape's night-lounge philosophy.
*
* One drenched violet field, panels lifted with light (white-alpha hairlines
* and lighter violet), a serif voice for headings, and the coral glow spent
* only on the action, the selection, and the focus ring. Nothing rushes.
*/
@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 — Instrument Serif, a single weight (400): the serif itself
* is the emphasis, so no faux-bold. */
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.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 — a white-alpha tint chip with foreground ink, neutral
* inside the violet world. */
:not(pre) > code {
padding: 0.1em 0.35em;
border-radius: calc(var(--radius) * 0.5);
background: oklch(1 0 0 / 0.12);
color: var(--foreground);
font-size: 0.9em;
}
/* Selection — the coral glow. */
::selection {
background: color-mix(in oklch, var(--primary) 40%, transparent);
color: var(--foreground);
}
/* Links — coral-warmed, 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 coral 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; opacity absorb on press with a languid ease. */
[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 lounge 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.99);
}
/* Inputs — card-violet panels, white-alpha hairline, coral 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: oklch(1 0 0 / 0.32);
}
[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;
}