Files
lib-theme-modena/theme.css
jules 02f7a99374 theme: dark status tokens + charts; remove font @import; contract conformance
Adds dark destructive/success/warning overrides, --chart-1..5, --info,
subtitle, motion aliases. Font @import moved to README (PostCSS ordering).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 13:45:33 +10:00

514 lines
19 KiB
CSS

/*
* Modena — Editorial design system theme.
*
* Content-presentation tokens + base typography. Designed for blogs,
* articles, documentation, and long-form essays. Editorial minimalism:
* the UI disappears so the content takes center stage.
*
* Ports the relevant subset of the Modena reference into a Crema-style
* theme.css that:
* • Defines the standard Crema design tokens (--background, --primary,
* --foreground, etc.) so all shadcn/lib-* components keep working.
* • Adds Modena-specific tokens (--mod-prose-*, --mod-text-*, etc.)
* consumed by lib-modena-ui components and the article surface.
* • Applies base typography to h1-h4, [data-slot="article"], and
* [data-slot="prose"] so any consumer of @crema/content-ui
* automatically picks up the editorial look.
*
* Pairs with: @crema/content-ui (article primitives), lib-modena-ui
* (Modena-specific extra components).
*/
/* Fonts (Newsreader editorial serif + Inter + JetBrains Mono) are loaded by
* the consuming app's entry CSS, not @import-ed here — the token contract bans
* font @imports inside theme.css (they break PostCSS ordering). The Google
* Fonts URL is documented in this theme's README. */
:root {
/* ── Crema-shaped tokens (apps and shadcn components consume these) ── */
--background: oklch(1 0 0);
--foreground: oklch(0.16 0.02 255);
--card: oklch(1 0 0);
--card-foreground: oklch(0.16 0.02 255);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.16 0.02 255);
--primary: oklch(0.55 0.22 260);
--primary-foreground: oklch(0.99 0.005 80);
--secondary: oklch(0.96 0.005 250);
--secondary-foreground: oklch(0.20 0.025 255);
--muted: oklch(0.97 0.003 250);
--muted-foreground: oklch(0.50 0.015 250);
--accent: oklch(0.95 0.01 260);
--accent-foreground: oklch(0.18 0.02 255);
--destructive: oklch(0.55 0.22 25);
--destructive-foreground: oklch(0.99 0.005 80);
--success: oklch(0.52 0.18 145); /* deepened 0.65→0.52 so light foreground clears AA (was 2.93:1) */
--success-foreground: oklch(0.99 0.005 80);
--warning: oklch(0.72 0.16 70);
--warning-foreground: oklch(0.15 0.02 70);
/* Info — a cyan-blue held clear of the editorial primary (blue 260) so
* status reads distinct from brand; deep enough to carry a light label. */
--info: oklch(0.52 0.15 235);
--info-foreground: oklch(0.99 0.005 80);
--border: oklch(0.92 0.005 250);
--input: oklch(0.92 0.005 250);
--ring: oklch(0.55 0.22 260);
/* ── Chat surfaces ──
* Contract for lib-agent-chat-ui / lib-agent-dock-ui. User bubble is
* a filled, deep, high-contrast panel with light text; assistant
* bubble is a card-like surface with a hairline border. */
--chat-user-bg: oklch(0.48 0.2 260);
--chat-user-fg: oklch(0.99 0.005 80);
--chat-assistant-bg: oklch(0.98 0.003 250);
--chat-assistant-fg: oklch(0.16 0.02 255);
--chat-assistant-border: oklch(0.92 0.005 250);
/* ── Charts ──
* Five editorial accents drawn from Modena's own status hues (brand blue,
* venetian red, sage green, magenta, amber). Hues sit ≥60° apart and
* adjacent indices alternate lightness so neighbouring series separate. */
--chart-1: oklch(0.55 0.20 260); /* brand blue */
--chart-2: oklch(0.63 0.20 25); /* venetian red */
--chart-3: oklch(0.58 0.15 145); /* sage green */
--chart-4: oklch(0.66 0.19 320); /* magenta */
--chart-5: oklch(0.60 0.16 70); /* amber */
--radius: 0.5rem;
--spacing: 0.25rem;
--border-width-thin: 1px;
--border-width-base: 1px;
--border-width-thick: 2px;
--border-width-heavy: 4px;
--sidebar: oklch(0.985 0.003 250);
--sidebar-foreground: oklch(0.16 0.02 255);
--sidebar-accent: oklch(0.94 0.005 250);
--sidebar-accent-foreground: oklch(0.20 0.025 255);
--sidebar-border: oklch(0.92 0.005 250);
--sidebar-ring: oklch(0.55 0.22 260);
--sidebar-primary: oklch(0.55 0.22 260);
--sidebar-primary-foreground: oklch(0.99 0.005 80);
/* ── Modena-specific tokens ─────────────────────────────────────── */
/* Accent — duplicate of --primary so Modena components can read
* either. Override --mod-accent at the app/site level to brand. */
--mod-accent: var(--primary);
/* Gray scale (11 steps, matches Modena's reference). */
--mod-gray-50: oklch(0.985 0.002 250);
--mod-gray-100: oklch(0.965 0.003 250);
--mod-gray-200: oklch(0.92 0.004 250);
--mod-gray-300: oklch(0.86 0.006 250);
--mod-gray-400: oklch(0.70 0.012 250);
--mod-gray-500: oklch(0.55 0.018 250);
--mod-gray-600: oklch(0.45 0.020 250);
--mod-gray-700: oklch(0.36 0.022 250);
--mod-gray-800: oklch(0.24 0.024 250);
--mod-gray-900: oklch(0.17 0.025 250);
--mod-gray-950: oklch(0.10 0.025 250);
/* Text */
--mod-text: var(--foreground);
--mod-text-secondary: oklch(0.42 0.020 250);
--mod-text-muted: var(--muted-foreground);
--mod-text-faint: oklch(0.68 0.012 250);
/* Prose */
--mod-prose-color: oklch(0.22 0.022 250);
--mod-prose-size: clamp(1.1875rem, 1.0625rem + 0.4vw, 1.3125rem);
--mod-prose-line-height: 1.7;
--mod-prose-max-width: 720px;
--mod-prose-tracking: -0.011em;
/* Inline code */
--mod-code-color: oklch(0.55 0.22 350);
--mod-code-bg: var(--muted);
--mod-code-border: var(--border);
/* Code block (dark always) */
--mod-bg-code: oklch(0.16 0.020 250);
--mod-bg-code-fg: oklch(0.92 0.010 80);
--mod-bg-code-header: oklch(0.13 0.020 250);
/* Status colors */
--mod-info: oklch(0.55 0.22 260);
--mod-info-bg: color-mix(in oklch, oklch(0.55 0.22 260) 6%, transparent);
--mod-info-border: color-mix(in oklch, oklch(0.55 0.22 260) 20%, transparent);
--mod-success: oklch(0.65 0.18 145);
--mod-success-bg: color-mix(in oklch, oklch(0.65 0.18 145) 6%, transparent);
--mod-success-border: color-mix(in oklch, oklch(0.65 0.18 145) 20%, transparent);
--mod-warning: oklch(0.72 0.16 70);
--mod-warning-bg: color-mix(in oklch, oklch(0.72 0.16 70) 6%, transparent);
--mod-warning-border: color-mix(in oklch, oklch(0.72 0.16 70) 20%, transparent);
--mod-danger: oklch(0.55 0.22 25);
--mod-danger-bg: color-mix(in oklch, oklch(0.55 0.22 25) 6%, transparent);
--mod-danger-border: color-mix(in oklch, oklch(0.55 0.22 25) 20%, transparent);
/* Fonts */
--font-sans: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
--font-heading: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
--font-mono: "JetBrains Mono", ui-monospace, Menlo, Consolas, monospace;
--mod-font-body: "Newsreader", "Source Serif 4", Georgia, "Times New Roman", serif;
--font-ai-prose: var(--mod-font-body);
/* Tailwind utility families (mirror --font-heading/--font-sans). */
--heading-family: var(--font-heading);
--body-family: var(--font-sans);
/* Letter-spacing scale */
--mod-tracking: -0.011em;
--mod-tracking-tight: -0.02em;
--mod-tracking-heading: -0.025em;
--mod-tracking-wide: 0.04em;
/* Spacing (Modena's 4px grid) */
--mod-space-1: 4px;
--mod-space-2: 8px;
--mod-space-3: 12px;
--mod-space-4: 16px;
--mod-space-5: 20px;
--mod-space-6: 24px;
--mod-space-8: 32px;
--mod-space-10: 40px;
--mod-space-12: 48px;
--mod-space-16: 64px;
/* Radius scale */
--mod-radius-sm: 4px;
--mod-radius: 6px;
--mod-radius-md: 8px;
--mod-radius-lg: 12px;
--mod-radius-xl: 16px;
--mod-radius-pill: 9999px;
/* Type scale (Crema-shaped) — editorial, opts into fluid clamp() at the
* hero/display/headline end (permitted for editorial themes). */
--text-hero-size: clamp(3.25rem, 2.6rem + 2.6vw, 4.5rem);
--text-hero-lh: 1.05;
--text-display-size: clamp(2.5rem, 2rem + 2vw, 3.25rem);
--text-display-lh: 1.1;
--text-headline-size: clamp(2rem, 1.7rem + 1.2vw, 2.5rem);
--text-headline-lh: 1.15;
--text-title-size: 1.5rem;
--text-title-lh: 1.3;
--text-subtitle-size: 1.25rem; /* fills the label→title hole (deck/standfirst) */
--text-subtitle-lh: 1.4;
--text-body-size: 1rem;
--text-body-lh: 1.6;
--text-label-size: 0.875rem;
--text-label-lh: 1.4;
--text-caption-size: 0.75rem;
--text-caption-lh: 1.4;
/* Motion — calm editorial. No physical bounce: the springs the contract
* mandates are aliased to the standard easings so var() always resolves
* while the theme keeps its quiet, un-bouncy character. */
--duration-fast: 120ms;
--duration-base: 200ms;
--duration-slow: 300ms;
--duration-spring: var(--duration-slow);
--ease-standard: cubic-bezier(0.25, 0.1, 0.25, 1);
--ease-decelerate: cubic-bezier(0, 0, 0.25, 1);
--ease-accelerate: cubic-bezier(0.4, 0, 1, 1);
--ease-emphasized: var(--ease-standard);
--ease-spring-gentle: var(--ease-standard);
--ease-spring-snappy: var(--ease-standard);
--ease-spring-bouncy: var(--ease-standard);
/* Elevation */
--elevation-0: none;
--elevation-1: 0 1px 2px rgba(0, 0, 0, 0.04);
--elevation-2: 0 1px 3px rgba(0, 0, 0, 0.06), 0 1px 2px rgba(0, 0, 0, 0.03);
--elevation-3: 0 4px 8px -2px rgba(0, 0, 0, 0.06), 0 2px 4px -2px rgba(0, 0, 0, 0.03);
--elevation-4: 0 12px 24px -4px rgba(0, 0, 0, 0.08), 0 4px 8px -4px rgba(0, 0, 0, 0.03);
--elevation-5: 0 24px 48px -8px rgba(0, 0, 0, 0.10), 0 8px 16px -8px rgba(0, 0, 0, 0.04);
/* Glass — inert stubs. Modena is a flat editorial theme, not a glass one;
* these resolve var() to no-ops so glass-aware components stay flat. */
--glass-blur: 0px;
--glass-saturate: 100%;
--glass-brightness: 1;
--glass-contrast: 1;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* ── DARK MODE ──────────────────────────────────────────────────── */
.dark {
--background: oklch(0.13 0.018 250);
--foreground: oklch(0.95 0.010 250);
--card: oklch(0.17 0.020 250);
--card-foreground: oklch(0.95 0.010 250);
--popover: oklch(0.17 0.020 250);
--popover-foreground: oklch(0.95 0.010 250);
--primary: oklch(0.55 0.22 260); /* deepened 0.65→0.55 so the near-white foreground clears AA on the filled button (was 3.38:1) */
--primary-foreground: oklch(0.99 0.005 80);
--secondary: oklch(0.22 0.020 250);
--secondary-foreground: oklch(0.92 0.010 250);
--muted: oklch(0.20 0.020 250);
--muted-foreground: oklch(0.65 0.020 250);
--accent: oklch(0.24 0.022 250);
--accent-foreground: oklch(0.92 0.010 250);
--border: oklch(0.27 0.020 250);
--input: oklch(0.27 0.020 250);
--ring: oklch(0.65 0.22 260);
/* ── Status (dark-calibrated) ──
* Light values (red/green/amber at L≈0.55-0.72) were inheriting into
* dark mode, failing both as colored text on the dark field and as
* button fills. Brightened here; foregrounds flipped to dark ink so
* the fills carry text as well. */
--destructive: oklch(0.68 0.2 25); /* text-on-bg 6.37:1 (was 3.70:1 inherited) */
--destructive-foreground: oklch(0.16 0.04 25); /* dark-ink fill 6.19:1 */
--success: oklch(0.72 0.15 145); /* text-on-bg 8.58:1 */
--success-foreground: oklch(0.16 0.03 145); /* dark-ink fill 8.24:1 */
--warning: oklch(0.80 0.14 70); /* text-on-bg 10.51:1 */
--warning-foreground: oklch(0.16 0.02 70); /* dark-ink fill 10.16:1 */
--info: oklch(0.72 0.14 235); /* brightened for the dark field; dark-ink fill */
--info-foreground: oklch(0.16 0.03 235);
/* ── Charts (dark) ── same five hues, brightened for the dark field. */
--chart-1: oklch(0.68 0.19 260);
--chart-2: oklch(0.72 0.18 25);
--chart-3: oklch(0.70 0.15 145);
--chart-4: oklch(0.74 0.17 320);
--chart-5: oklch(0.78 0.14 70);
/* ── Chat surfaces (dark) ── */
--chat-user-bg: oklch(0.55 0.2 260);
--chat-user-fg: oklch(0.99 0.005 80);
--chat-assistant-bg: oklch(0.20 0.020 250);
--chat-assistant-fg: oklch(0.95 0.010 250);
--chat-assistant-border: oklch(0.27 0.020 250);
--sidebar: oklch(0.11 0.018 250);
--sidebar-foreground: oklch(0.92 0.010 250);
--sidebar-accent: oklch(0.22 0.022 250);
--sidebar-accent-foreground: oklch(0.92 0.010 250);
--sidebar-border: oklch(0.25 0.020 250);
--sidebar-primary: oklch(0.65 0.22 260);
--sidebar-primary-foreground: oklch(0.99 0.005 80);
--sidebar-ring: oklch(0.65 0.22 260);
--mod-text: var(--foreground);
--mod-text-secondary: oklch(0.65 0.020 250);
--mod-text-muted: oklch(0.55 0.020 250);
--mod-text-faint: oklch(0.59 0.018 250); /* raised 0.40→0.59 for AA vs dark bg/card (was 2.19:1) */
--mod-prose-color: oklch(0.86 0.012 250);
--mod-code-color: oklch(0.78 0.22 350);
--mod-code-bg: oklch(0.22 0.020 250);
--mod-code-border: oklch(0.30 0.020 250);
}
/* ── BASE TYPOGRAPHY ─────────────────────────────────────────────
* Caffe Florian's theme applies serif font-heading to h1-h4 globally;
* Modena keeps headings in Inter sans. Apply explicitly so it sticks
* regardless of cascade order with other themes' base layers. */
html {
font-family: var(--font-sans);
}
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-sans);
font-weight: 700;
letter-spacing: var(--mod-tracking-heading);
}
/* ── ARTICLE SURFACE ─────────────────────────────────────────────
* Targets @crema/content-ui's data-slot attributes. Anyone using
* <Article><ArticleHeader/><Prose>...</Prose></Article>
* automatically picks up the editorial look. Outside [data-slot="article"]
* Prose stays neutral (so comment bodies don't get drop-cap/serif). */
[data-slot="article"] {
max-width: var(--mod-prose-max-width);
margin-inline: auto;
padding-inline: var(--mod-space-6);
color: var(--mod-text);
}
[data-slot="article"] [data-slot="article-header"] {
text-align: center;
padding-top: var(--mod-space-16);
margin-bottom: 3vmin;
border: none;
}
[data-slot="article"] [data-slot="article-header"] h1 {
font-family: var(--font-sans);
font-size: var(--text-display-size);
line-height: var(--text-display-lh);
letter-spacing: var(--mod-tracking-heading);
font-weight: 800;
margin-top: 0.5rem;
color: var(--mod-text);
}
[data-slot="article"] [data-slot="article-header"] p {
font-family: var(--mod-font-body);
font-optical-sizing: auto;
font-size: clamp(1.125rem, 1rem + 0.4vw, 1.375rem);
line-height: 1.5;
color: var(--mod-text-secondary);
margin: 0.75em auto 0;
max-width: 36rem;
}
[data-slot="article"] [data-slot="article-header"] > div:first-child {
font-size: 0.875rem;
font-weight: 600;
letter-spacing: var(--mod-tracking-wide);
text-transform: uppercase;
color: var(--mod-accent);
}
[data-slot="article"] [data-slot="article-meta"] {
justify-content: center;
padding-top: var(--mod-space-6);
margin-top: var(--mod-space-6);
font-size: 0.875rem;
color: var(--mod-text-muted);
}
[data-slot="article"] [data-slot="prose"] {
font-family: var(--mod-font-body);
font-optical-sizing: auto;
font-size: var(--mod-prose-size);
line-height: var(--mod-prose-line-height);
letter-spacing: var(--mod-prose-tracking);
color: var(--mod-prose-color);
}
[data-slot="article"] [data-slot="prose"] > * + * {
margin-top: 1.5em;
}
[data-slot="article"] [data-slot="prose"] :where(p) { margin: 0; }
[data-slot="article"] [data-slot="prose"] :where(h1, h2, h3, h4, h5, h6) {
font-family: var(--font-sans);
font-weight: 700;
line-height: 1.2;
letter-spacing: var(--mod-tracking-heading);
color: var(--mod-text);
}
[data-slot="article"] [data-slot="prose"] :where(h2) {
font-size: 1.875rem;
margin-top: 2.4em;
margin-bottom: 0.6em;
}
[data-slot="article"] [data-slot="prose"] :where(h3) {
font-size: 1.5rem;
margin-top: 2em;
margin-bottom: 0.5em;
}
[data-slot="article"] [data-slot="prose"] :where(h4) {
font-size: 1.25rem;
margin-top: 1.8em;
margin-bottom: 0.4em;
}
[data-slot="article"] [data-slot="prose"] :where(strong) {
font-weight: 700;
color: var(--mod-text);
}
[data-slot="article"] [data-slot="prose"] :where(ul, ol) {
padding-left: 1.5em;
margin: 0;
}
[data-slot="article"] [data-slot="prose"] :where(li + li) {
margin-top: 0.4em;
}
[data-slot="article"] [data-slot="prose"] :where(li::marker) {
color: var(--mod-text-faint);
}
[data-slot="article"] [data-slot="prose"] :where(blockquote) {
position: relative;
font-size: 1.25em;
font-weight: 500;
line-height: 1.5;
color: var(--mod-text);
padding: 0 0 0 1.5em;
margin: 2em 0;
border: none;
font-style: normal;
}
[data-slot="article"] [data-slot="prose"] :where(blockquote)::before {
content: "";
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 3px;
background: var(--mod-accent);
border-radius: 2px;
}
[data-slot="article"] [data-slot="prose"] :where(blockquote p) { margin: 0; }
[data-slot="article"] [data-slot="prose"] :where(blockquote p + p) { margin-top: 0.6em; }
[data-slot="article"] [data-slot="prose"] :where([data-slot="pullquote"]) {
font-family: var(--mod-font-body);
font-size: 1.625em;
font-style: italic;
font-weight: 400;
line-height: 1.5;
text-align: center;
padding: 0;
color: var(--mod-text);
margin: 2.5em 0;
}
[data-slot="article"] [data-slot="prose"] :where([data-slot="pullquote"])::before {
display: none;
}
[data-slot="article"] [data-slot="prose"] :where(hr) {
border: none;
border-top: 1px solid var(--border);
opacity: 0.6;
margin: 3em 0;
width: auto;
height: auto;
background: transparent;
}
[data-slot="article"] [data-slot="prose"] :where(:not(pre) > code) {
font-family: var(--font-mono);
font-size: 0.85em;
color: var(--mod-code-color);
background: var(--mod-code-bg);
border: 1px solid var(--mod-code-border);
border-radius: var(--mod-radius-sm);
padding: 0.1em 0.35em;
}
[data-slot="article"] [data-slot="prose"] :where(pre) {
font-family: var(--font-mono);
font-size: 0.875rem;
line-height: 1.65;
background: var(--mod-bg-code);
color: var(--mod-bg-code-fg);
border-radius: var(--mod-radius-md);
padding: var(--mod-space-5);
overflow-x: auto;
margin: 2em 0;
}
[data-slot="article"] [data-slot="prose"] :where(pre code) {
background: transparent;
color: inherit;
border: none;
padding: 0;
border-radius: 0;
font-size: inherit;
}
[data-slot="article"] [data-slot="prose"] :where(a) {
color: var(--mod-accent);
text-decoration: underline;
text-decoration-thickness: 1px;
text-underline-offset: 2px;
transition: color var(--duration-fast) var(--ease-standard);
}
[data-slot="article"] [data-slot="prose"] :where(a:hover) {
color: color-mix(in oklch, var(--mod-accent) 75%, var(--foreground));
}