+
```
@@ -199,8 +203,8 @@ Start from a preset and tweak specific values:
@import '@e412/rnui-themes';
@import '@e412/rnui-themes/presets';
-/* Start from Ocean, customize primary */
-[data-theme="ocean"] {
+/* Start from Signal, customize primary */
+[data-theme="signal"] {
--primary: oklch(0.48 0.15 220);
}
```
@@ -226,3 +230,53 @@ Or access CSS variables directly:
font-family: var(--font-heading, var(--font-sans));
}
```
+
+## Reading Tokens Programmatically
+
+For tooling — a token browser, a Figma sync, a theme editor — read
+`tokens.json` rather than scanning the compiled CSS. The generated stylesheet
+mixes rnui's tokens with Tailwind's internal custom properties, so a CSS scan
+reports plumbing as if it were part of the palette.
+
+```ts
+import tokens from '@e412/rnui-themes/tokens.json'
+
+const signalColors = tokens.tokens.filter(
+ (t) => t.theme === 'signal' && t.kind === 'color',
+)
+// → [{ name: '--primary', theme: 'signal', kind: 'color',
+// value: 'oklch(…)', light: 'oklch(…)', dark: 'oklch(…)',
+// darkInherited: false }, …]
+```
+
+`kind` is one of `color`, `dimension`, `font`, `radius`. `darkInherited: true`
+means the theme's dark ramp doesn't declare that token and it falls through to
+the light value — useful for auditing a theme for completeness.
+
+## Using rnui Without a Tailwind Build
+
+rnui normally expects Tailwind in your build, which compiles whatever classes
+you write. If you're embedding the design system somewhere that can't run
+Tailwind — a static stylesheet, a preview iframe, a design tool — any class that
+wasn't compiled ahead of time silently does nothing.
+
+Import the precompiled utility layer instead:
+
+```css
+@import '@e412/rnui-themes/utilities';
+```
+
+It ships complete scales (spacing `0`–`24`, `text-xs`–`text-9xl`,
+`grid-cols-1..12`, `max-w-xs`–`max-w-7xl`, radius/border/opacity, plus
+`sm: md: lg: xl:` and `hover: focus: active: disabled: dark:` variants) at
+roughly 353 KB, 40 KB gzipped. `utilities.json` lists exactly what shipped:
+
+```ts
+import utilities from '@e412/rnui-themes/utilities.json'
+
+utilities.classes.includes('gap-10') // true
+```
+
+If you *do* run Tailwind, don't import this — keep importing
+`@e412/rnui-themes` and let your build emit only what you use. To add the same
+scales to your own build, import `@e412/rnui-themes/safelist`.
diff --git a/apps/storybook/src/stories/progress.stories.tsx b/apps/storybook/src/stories/progress.stories.tsx
index 2969d55..1dcadcb 100644
--- a/apps/storybook/src/stories/progress.stories.tsx
+++ b/apps/storybook/src/stories/progress.stories.tsx
@@ -5,6 +5,10 @@ const meta = {
title: 'Components/Progress',
component: Progress,
tags: ['autodocs'],
+ // The stories size the track with a percentage width. The global
+ // `layout: 'centered'` makes the story root shrink-to-fit, so a percentage
+ // resolves against a 0px parent and the bar renders invisible.
+ parameters: { layout: 'padded' },
argTypes: {
value: {
control: { type: 'range', min: 0, max: 100, step: 1 },
diff --git a/apps/storybook/src/stories/slider.stories.tsx b/apps/storybook/src/stories/slider.stories.tsx
index e8230f1..0286850 100644
--- a/apps/storybook/src/stories/slider.stories.tsx
+++ b/apps/storybook/src/stories/slider.stories.tsx
@@ -5,6 +5,10 @@ const meta = {
title: 'Components/Slider',
component: Slider,
tags: ['autodocs'],
+ // The stories size the track with a percentage width. The global
+ // `layout: 'centered'` makes the story root shrink-to-fit, so a percentage
+ // resolves against a 0px parent and the slider renders invisible.
+ parameters: { layout: 'padded' },
argTypes: {
defaultValue: {
control: 'object',
diff --git a/apps/storybook/src/styles.css b/apps/storybook/src/styles.css
index c88d9e0..f86f7f9 100644
--- a/apps/storybook/src/styles.css
+++ b/apps/storybook/src/styles.css
@@ -2,3 +2,7 @@
@import "@e412/rnui-themes";
@import "tw-animate-css";
@source "../node_modules/@e412/rnui-react/dist";
+/* Emit the complete utility scales, not just the values these stories happen
+ * to use. The design-sync bundle is scraped from this build, and it is consumed
+ * precompiled — a class missing there fails silently for every consumer. */
+@import "@e412/rnui-themes/safelist";
diff --git a/packages/react/src/components/calendar.tsx b/packages/react/src/components/calendar.tsx
index 6963fd1..42afaee 100644
--- a/packages/react/src/components/calendar.tsx
+++ b/packages/react/src/components/calendar.tsx
@@ -33,7 +33,7 @@ function Calendar({
svg]:rotate-180`,
String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
className,
@@ -58,24 +58,24 @@ function Calendar({
),
button_previous: cn(
buttonVariants({ variant: buttonVariant }),
- 'size-(--cell-size) p-0 select-none aria-disabled:opacity-50',
+ 'size-(--rnui-_cell-size) p-0 select-none aria-disabled:opacity-50',
defaultClassNames.button_previous,
),
button_next: cn(
buttonVariants({ variant: buttonVariant }),
- 'size-(--cell-size) p-0 select-none aria-disabled:opacity-50',
+ 'size-(--rnui-_cell-size) p-0 select-none aria-disabled:opacity-50',
defaultClassNames.button_next,
),
month_caption: cn(
- 'flex h-(--cell-size) w-full items-center justify-center px-(--cell-size)',
+ 'flex h-(--rnui-_cell-size) w-full items-center justify-center px-(--rnui-_cell-size)',
defaultClassNames.month_caption,
),
dropdowns: cn(
- 'flex h-(--cell-size) w-full items-center justify-center gap-1.5 text-sm font-medium',
+ 'flex h-(--rnui-_cell-size) w-full items-center justify-center gap-1.5 text-sm font-medium',
defaultClassNames.dropdowns,
),
dropdown_root: cn(
- 'relative rounded-(--cell-radius)',
+ 'relative rounded-(--rnui-_cell-radius)',
defaultClassNames.dropdown_root,
),
dropdown: cn(
@@ -86,18 +86,18 @@ function Calendar({
'font-medium select-none',
captionLayout === 'label'
? 'text-sm'
- : 'flex items-center gap-1 rounded-(--cell-radius) text-sm [&>svg]:size-3.5 [&>svg]:text-muted-foreground',
+ : 'flex items-center gap-1 rounded-(--rnui-_cell-radius) text-sm [&>svg]:size-3.5 [&>svg]:text-muted-foreground',
defaultClassNames.caption_label,
),
table: 'w-full border-collapse',
weekdays: cn('flex', defaultClassNames.weekdays),
weekday: cn(
- 'flex-1 rounded-(--cell-radius) text-[0.8rem] font-normal text-muted-foreground select-none',
+ 'flex-1 rounded-(--rnui-_cell-radius) text-[0.8rem] font-normal text-muted-foreground select-none',
defaultClassNames.weekday,
),
week: cn('mt-2 flex w-full', defaultClassNames.week),
week_number_header: cn(
- 'w-(--cell-size) select-none',
+ 'w-(--rnui-_cell-size) select-none',
defaultClassNames.week_number_header,
),
week_number: cn(
@@ -105,23 +105,23 @@ function Calendar({
defaultClassNames.week_number,
),
day: cn(
- 'group/day relative aspect-square h-full w-full rounded-(--cell-radius) p-0 text-center select-none [&:last-child[data-selected=true]_button]:rounded-r-(--cell-radius)',
+ 'group/day relative aspect-square h-full w-full rounded-(--rnui-_cell-radius) p-0 text-center select-none [&:last-child[data-selected=true]_button]:rounded-r-(--rnui-_cell-radius)',
props.showWeekNumber
- ? '[&:nth-child(2)[data-selected=true]_button]:rounded-l-(--cell-radius)'
- : '[&:first-child[data-selected=true]_button]:rounded-l-(--cell-radius)',
+ ? '[&:nth-child(2)[data-selected=true]_button]:rounded-l-(--rnui-_cell-radius)'
+ : '[&:first-child[data-selected=true]_button]:rounded-l-(--rnui-_cell-radius)',
defaultClassNames.day,
),
range_start: cn(
- 'relative isolate z-0 rounded-l-(--cell-radius) bg-muted after:absolute after:inset-y-0 after:right-0 after:w-4 after:bg-muted',
+ 'relative isolate z-0 rounded-l-(--rnui-_cell-radius) bg-muted after:absolute after:inset-y-0 after:right-0 after:w-4 after:bg-muted',
defaultClassNames.range_start,
),
range_middle: cn('rounded-none', defaultClassNames.range_middle),
range_end: cn(
- 'relative isolate z-0 rounded-r-(--cell-radius) bg-muted after:absolute after:inset-y-0 after:left-0 after:w-4 after:bg-muted',
+ 'relative isolate z-0 rounded-r-(--rnui-_cell-radius) bg-muted after:absolute after:inset-y-0 after:left-0 after:w-4 after:bg-muted',
defaultClassNames.range_end,
),
today: cn(
- 'rounded-(--cell-radius) bg-muted text-foreground data-[selected=true]:rounded-none',
+ 'rounded-(--rnui-_cell-radius) bg-muted text-foreground data-[selected=true]:rounded-none',
defaultClassNames.today,
),
outside: cn(
@@ -172,7 +172,7 @@ function Calendar({
WeekNumber: ({ children, ...props }) => {
return (
- |
@@ -214,7 +214,7 @@ function CalendarDayButton({
data-range-end={modifiers.range_end}
data-range-middle={modifiers.range_middle}
className={cn(
- 'relative isolate z-10 flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 border-0 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-[3px] group-data-[focused=true]/day:ring-ring/50 data-[range-end=true]:rounded-(--cell-radius) data-[range-end=true]:rounded-r-(--cell-radius) data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground data-[range-middle=true]:rounded-none data-[range-middle=true]:bg-muted data-[range-middle=true]:text-foreground data-[range-start=true]:rounded-(--cell-radius) data-[range-start=true]:rounded-l-(--cell-radius) data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground dark:hover:text-foreground [&>span]:text-xs [&>span]:opacity-70',
+ 'relative isolate z-10 flex aspect-square size-auto w-full min-w-(--rnui-_cell-size) flex-col gap-1 border-0 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-[3px] group-data-[focused=true]/day:ring-ring/50 data-[range-end=true]:rounded-(--rnui-_cell-radius) data-[range-end=true]:rounded-r-(--rnui-_cell-radius) data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground data-[range-middle=true]:rounded-none data-[range-middle=true]:bg-muted data-[range-middle=true]:text-foreground data-[range-start=true]:rounded-(--rnui-_cell-radius) data-[range-start=true]:rounded-l-(--rnui-_cell-radius) data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground dark:hover:text-foreground [&>span]:text-xs [&>span]:opacity-70',
defaultClassNames.day,
className,
)}
diff --git a/packages/react/src/components/data-grid/data-grid-table.tsx b/packages/react/src/components/data-grid/data-grid-table.tsx
index 3daecaf..d094021 100644
--- a/packages/react/src/components/data-grid/data-grid-table.tsx
+++ b/packages/react/src/components/data-grid/data-grid-table.tsx
@@ -337,7 +337,7 @@ function DataGridTableFillCol() {
return (
)
}
@@ -351,7 +351,7 @@ function DataGridTableFillHeadCell() {
|
)
@@ -366,7 +366,7 @@ function DataGridTableFillBodyCell() {
|
)
@@ -381,7 +381,7 @@ function DataGridTableFillFootCell() {
|
)
@@ -430,7 +430,7 @@ function DataGridTableBase({ children }: { children: ReactNode }) {
props.tableLayout?.columnsResizable
? {
...columnSizeVars,
- width: `calc(${table.getTotalSize()}px + var(--data-grid-fill-size, 0px))`,
+ width: `calc(${table.getTotalSize()}px + var(--rnui-_data-grid-fill-size, 0px))`,
}
: undefined
}
@@ -519,8 +519,8 @@ function DataGridTableViewport({
style={{
...(props.tableLayout?.columnsResizable
? {
- width: `calc(${table.getTotalSize()}px + var(--data-grid-fill-size, 0px))`,
- ['--data-grid-fill-size' as string]: `${fillWidth}px`,
+ width: `calc(${table.getTotalSize()}px + var(--rnui-_data-grid-fill-size, 0px))`,
+ ['--rnui-_data-grid-fill-size' as string]: `${fillWidth}px`,
}
: undefined),
...style,
diff --git a/packages/themes/README.md b/packages/themes/README.md
index 31a247c..764509e 100644
--- a/packages/themes/README.md
+++ b/packages/themes/README.md
@@ -1,6 +1,6 @@
# @e412/rnui-themes
-CSS themes for [`@e412/rnui-react`](../react) — 8 built-in presets, a `data-theme` attribute system, full Tailwind CSS v4 integration, and the OKLch color space for perceptually uniform palettes.
+CSS themes for [`@e412/rnui-react`](../react) — 3 built-in presets, a `data-theme` attribute system, full Tailwind CSS v4 integration, and the OKLch color space for perceptually uniform palettes.
```bash
pnpm add @e412/rnui-themes
@@ -21,36 +21,33 @@ That gives you the warm-neutral default theme. Add presets to unlock the built-i
```
```html
-
+
```
## Built-in presets
-| Theme | Personality | Font | Radius |
-| --------- | ----------------------------- | ------------------------ | ---------- |
-| `oxide` | Editorial craft, warm copper | Bitter + DM Sans | `0.375rem` |
-| `ocean` | Smooth aquatic, deep teal | Plus Jakarta Sans | `0.875rem` |
-| `violet` | Luxury creative, rich purple | Outfit | `1rem` |
-| `forest` | Organic natural, deep emerald | Fraunces + Source Sans 3 | `0.5rem` |
-| `rose` | Soft & bubbly, warm pink | Nunito | `1.25rem` |
-| `amber` | Sharp & energetic, golden | Sora | `0.25rem` |
-| `slate` | Professional tech, blue-gray | Geist | `0.5rem` |
-| `crimson` | Brutalist bold, scarlet | Instrument Sans | `0` |
+| Theme | Character | Body / Headings | Radius |
+| --------- | ------------------------------------------ | ------------------------------- | ---------- |
+| `press` | Editorial — warm paper, high contrast | Source Sans 3 / Source Serif 4 | `0.5rem` |
+| `console` | Technical — dense, cool, terminal-adjacent | Geist / Geist Mono | `0.125rem` |
+| `signal` | Brand-forward — one confident accent | Instrument Sans / Space Grotesk | `0.75rem` |
-Themes declare `--font-sans` and sometimes `--font-heading`, but **don't load the fonts** — that's your choice (Google Fonts, `next/font`, self-hosted). Each preset's CSS file documents the Google Fonts URL in its header comment.
+Every theme sets `--font-sans`, `--font-heading` and `--font-mono`, ships a complete light _and_ dark ramp, and meets WCAG AA in both modes (4.5:1 text, 3:1 borders and rings) — enforced by `scripts/check-contrast.mjs`, which fails the build on a regression.
+
+Themes declare their font families but **don't load the fonts** — that's your choice (Google Fonts, `next/font`, self-hosted). Each preset's CSS file documents the Google Fonts URL in its header comment.
## Dark mode
The docs and most apps expect a `.dark` class on ``:
```html
-
+
```
You can also scope themes to containers:
```html
-...
+...
```
## Custom themes
@@ -83,8 +80,8 @@ Since themes are plain CSS, your overrides cascade naturally:
```css
@import '@e412/rnui-themes/presets';
-/* Start from Ocean, customize the primary */
-[data-theme='ocean'] {
+/* Start from Signal, customize the primary */
+[data-theme='signal'] {
--primary: oklch(0.48 0.15 220);
}
```
@@ -92,7 +89,7 @@ Since themes are plain CSS, your overrides cascade naturally:
## Runtime switching
```ts
-document.documentElement.dataset.theme = 'violet'
+document.documentElement.dataset.theme = 'console'
```
## Available tokens
@@ -107,6 +104,65 @@ document.documentElement.dataset.theme = 'violet'
**Typography**: `--font-sans`, `--font-heading` (falls back to `--font-sans`).
+### Machine-readable token export
+
+`dist/tokens.json` is the declared token surface — the source of truth for
+tooling. Don't scan the generated CSS: it mixes these tokens with Tailwind's own
+internal custom properties, which are not part of the rnui palette.
+
+```ts
+import tokens from '@e412/rnui-themes/tokens.json'
+
+tokens.tokens
+ .filter((t) => t.theme === 'signal' && t.kind === 'color')
+ .map((t) => [t.name, t.light, t.dark])
+```
+
+Each entry carries `name`, `theme`, `kind` (`color` | `dimension` | `font` |
+`radius`), `value`, and its `light` / `dark` pair. A token a theme's dark ramp
+doesn't declare is flagged `darkInherited: true` rather than presented as if the
+theme set it deliberately.
+
+## Precompiled consumers
+
+If you run Tailwind yourself, ignore this section — importing
+`@e412/rnui-themes` and letting your build compile what you use is smaller and
+better.
+
+If you **can't** run Tailwind (a static stylesheet, an embedded preview, a design
+tool), a class that was never emitted at build time silently does nothing: no
+error, no warning, the element just renders unstyled. Two exports address that:
+
+```css
+/* Precompiled utilities: complete scales, ~353 KB (~40 KB gzip) */
+@import '@e412/rnui-themes/utilities';
+```
+
+```css
+/* Or add the same scales to your own Tailwind build */
+@import 'tailwindcss';
+@import '@e412/rnui-themes';
+@import '@e412/rnui-themes/safelist';
+```
+
+Covered: spacing `0`–`24` on every box-model utility, `text-xs`–`text-9xl`,
+`grid-cols-1..12`, `col-span-*`, `max-w-xs`–`max-w-7xl`, radius, border, opacity,
+`sm: md: lg: xl:` responsive, and `hover: focus: active: disabled: dark:` on the
+interactive subset.
+
+`dist/utilities.json` lists what actually shipped, so tooling can tell a user
+"`gap-9` isn't in this bundle" instead of letting it fail silently:
+
+```ts
+import utilities from '@e412/rnui-themes/utilities.json'
+
+utilities.classes.includes('gap-10') // true
+utilities.families['gap'] // every gap step that shipped
+```
+
+It's generated from the compiled output rather than the safelist source — the
+safelist is what was requested, `utilities.json` is what Tailwind emitted.
+
## Imports
```css
@@ -114,17 +170,14 @@ document.documentElement.dataset.theme = 'violet'
@import '@e412/rnui-themes/base'; /* reset + body font + heading */
@import '@e412/rnui-themes/light'; /* light variables on :root */
@import '@e412/rnui-themes/dark'; /* dark variables on .dark */
-@import '@e412/rnui-themes/presets'; /* all 8 presets as [data-theme] */
+@import '@e412/rnui-themes/presets'; /* all 3 presets as [data-theme] */
+@import '@e412/rnui-themes/utilities'; /* precompiled utility layer (see above) */
+@import '@e412/rnui-themes/safelist'; /* safelist source for your own build */
/* Individual presets (sets :root directly — use for single-theme apps) */
-@import '@e412/rnui-themes/oxide';
-@import '@e412/rnui-themes/ocean';
-@import '@e412/rnui-themes/violet';
-@import '@e412/rnui-themes/forest';
-@import '@e412/rnui-themes/rose';
-@import '@e412/rnui-themes/amber';
-@import '@e412/rnui-themes/slate';
-@import '@e412/rnui-themes/crimson';
+@import '@e412/rnui-themes/press';
+@import '@e412/rnui-themes/console';
+@import '@e412/rnui-themes/signal';
```
## License
diff --git a/packages/themes/package.json b/packages/themes/package.json
index a6b3342..7cbc880 100644
--- a/packages/themes/package.json
+++ b/packages/themes/package.json
@@ -16,24 +16,26 @@
"./base": "./dist/base.css",
"./light": "./dist/light.css",
"./dark": "./dist/dark.css",
- "./oxide": "./dist/oxide.css",
- "./ocean": "./dist/ocean.css",
- "./violet": "./dist/violet.css",
- "./forest": "./dist/forest.css",
- "./rose": "./dist/rose.css",
- "./amber": "./dist/amber.css",
- "./slate": "./dist/slate.css",
- "./crimson": "./dist/crimson.css",
- "./presets": "./dist/presets.css"
+ "./presets": "./dist/presets.css",
+ "./utilities": "./dist/utilities.css",
+ "./safelist": "./dist/safelist.css",
+ "./tokens.json": "./dist/tokens.json",
+ "./utilities.json": "./dist/utilities.json",
+ "./press": "./dist/press.css",
+ "./console": "./dist/console.css",
+ "./signal": "./dist/signal.css"
},
"publishConfig": {
"access": "public"
},
"scripts": {
- "generate": "node scripts/generate-presets.mjs",
- "build": "node scripts/generate-presets.mjs && vite build"
+ "generate": "node scripts/generate-themes.mjs && node scripts/generate-presets.mjs",
+ "build": "node scripts/generate-themes.mjs && node scripts/generate-presets.mjs && vite build && node scripts/emit-utilities.mjs && node scripts/emit-tokens.mjs && node scripts/check-contrast.mjs"
},
"devDependencies": {
+ "@tailwindcss/vite": "^4.2.2",
+ "culori": "^4.0.2",
+ "tailwindcss": "^4.2.2",
"vite": "^8.0.0"
}
}
diff --git a/packages/themes/scripts/check-contrast.mjs b/packages/themes/scripts/check-contrast.mjs
new file mode 100644
index 0000000..86d782d
--- /dev/null
+++ b/packages/themes/scripts/check-contrast.mjs
@@ -0,0 +1,293 @@
+#!/usr/bin/env node
+/**
+ * Accessibility and ramp-structure gate for every theme.
+ *
+ * Accessibility is a floor on all themes, not a separate "high contrast"
+ * preset — so this runs in the build and fails it on a regression rather than
+ * leaving contrast to be eyeballed after the fact.
+ *
+ * Checks, per theme and per mode:
+ *
+ * AA text 4.5:1 for text-on-surface pairs
+ * AA UI 3:1 for borders, rings and other non-text boundaries
+ * surfaces dark ramps must step monotonically by >= 4% lightness, so
+ * elevation reads as elevation instead of noise
+ * chroma a dark token may not be more saturated than its light
+ * counterpart (raising chroma to compensate for darkness is
+ * what makes a dark mode look muddy)
+ * neutrality no literally-gray neutrals; this system's greys are warm
+ *
+ * Pair semantics are taken from how the components actually use the tokens,
+ * not from a generic convention:
+ * - `primary`/`primary-foreground` etc. are true text-on-solid pairs.
+ * - status colors (`destructive`, `success`, `info`, `warning`) are used as
+ * TEXT on ordinary surfaces (`text-destructive`) and their `-foreground`
+ * variants as text on a 10% tint of themselves (`bg-destructive/10`), so
+ * that is what gets measured.
+ *
+ * Run: node scripts/check-contrast.mjs [--quiet] (wired into `pnpm build`)
+ */
+
+import { readFileSync } from 'node:fs'
+import { dirname, join } from 'node:path'
+import { fileURLToPath } from 'node:url'
+import { converter, parse } from 'culori'
+
+const root = join(dirname(fileURLToPath(import.meta.url)), '..')
+const toRgb = converter('rgb')
+const QUIET = process.argv.includes('--quiet')
+
+const AA_TEXT = 4.5
+const AA_UI = 3
+
+/**
+ * Themes produced by generate-themes.mjs. They must additionally satisfy the
+ * ramp-structure rules (surface separation, dark-chroma ceiling); every theme,
+ * generated or not, must satisfy the accessibility floor.
+ */
+const GENERATED = new Set(['press', 'console', 'signal'])
+const MIN_SURFACE_STEP = 0.04 // 4% lightness between dark elevation steps
+
+/** WCAG relative luminance from an sRGB triple in 0..1. */
+function luminance({ r, g, b }) {
+ const lin = (c) => (c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4)
+ return 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b)
+}
+
+function contrast(a, b) {
+ const [x, y] = [luminance(a), luminance(b)].sort((m, n) => n - m)
+ return (x + 0.05) / (y + 0.05)
+}
+
+/** Composite `fg` at `alpha` over `bg` (sRGB, as a browser would). */
+function over(fg, bg, alpha) {
+ return {
+ r: fg.r * alpha + bg.r * (1 - alpha),
+ g: fg.g * alpha + bg.g * (1 - alpha),
+ b: fg.b * alpha + bg.b * (1 - alpha),
+ }
+}
+
+const failures = []
+const notes = []
+
+function need(theme, mode, ratio, min, label) {
+ if (ratio == null) return
+ if (ratio + 1e-9 < min) {
+ failures.push(
+ `${theme}/${mode}: ${label} is ${ratio.toFixed(2)}:1, needs ${min}:1`,
+ )
+ }
+}
+
+// Text-on-solid pairs: `` is a surface, `-foreground` is text on it.
+const SOLID_PAIRS = [
+ ['background', 'foreground'],
+ ['card', 'card-foreground'],
+ ['popover', 'popover-foreground'],
+ ['primary', 'primary-foreground'],
+ ['secondary', 'secondary-foreground'],
+ ['muted', 'muted-foreground'],
+ ['accent', 'accent-foreground'],
+ ['invert', 'invert-foreground'],
+ ['focus', 'focus-foreground'],
+ ['sidebar', 'sidebar-foreground'],
+ ['sidebar-primary', 'sidebar-primary-foreground'],
+ ['sidebar-accent', 'sidebar-accent-foreground'],
+]
+
+// Used as text directly on ordinary surfaces, and as `-foreground` on a tint.
+const STATUS = ['destructive', 'success', 'info', 'warning']
+
+// Non-text boundaries.
+const UI_ON_SURFACE = [
+ 'border',
+ 'input',
+ 'ring',
+ 'sidebar-border',
+ 'sidebar-ring',
+]
+
+// Dark elevation order. Equal steps are allowed where the spec allows them
+// (card <= popover); everything else must strictly increase.
+const SURFACE_ORDER = [
+ 'background',
+ 'card',
+ 'popover',
+ 'muted',
+ 'accent',
+ 'border',
+]
+
+function checkTheme(theme, mode, vars, lightVars) {
+ /**
+ * Resolve a token to an opaque sRGB colour. Several tokens are authored with
+ * alpha (`oklch(100% .004 75/.1)` for borders); measuring those as opaque
+ * reports a near-white boundary at ~20:1 when the rendered line is faint, so
+ * composite over the surface it sits on before measuring.
+ */
+ const rgb = (name, backdropName = 'background') => {
+ const raw = vars[`--${name}`]
+ if (!raw) return null
+ const p = parse(raw)
+ if (!p) return null
+ const c = toRgb(p)
+ const alpha = p.alpha ?? 1
+ if (alpha >= 1) return c
+ const backRaw = vars[`--${backdropName}`]
+ const back = backRaw ? parse(backRaw) : null
+ if (!back) return c
+ return over(c, toRgb(back), alpha)
+ }
+ const oklchOf = (name) => {
+ const raw = vars[`--${name}`]
+ if (!raw) return null
+ return parse(raw)
+ }
+
+ for (const [base, fg] of SOLID_PAIRS) {
+ const a = rgb(base)
+ const b = rgb(fg)
+ if (!a || !b) continue
+ need(theme, mode, contrast(a, b), AA_TEXT, `${fg} on ${base}`)
+ }
+
+ const bg = rgb('background')
+ const card = rgb('card')
+
+ // muted-foreground must be readable on BOTH background and card — passing
+ // against one and failing the other is the usual defect.
+ const mutedFg = rgb('muted-foreground')
+ if (mutedFg && bg) {
+ need(
+ theme,
+ mode,
+ contrast(mutedFg, bg),
+ AA_TEXT,
+ 'muted-foreground on background',
+ )
+ }
+ if (mutedFg && card) {
+ need(
+ theme,
+ mode,
+ contrast(mutedFg, card),
+ AA_TEXT,
+ 'muted-foreground on card',
+ )
+ }
+
+ for (const s of STATUS) {
+ const solid = rgb(s)
+ const fg = rgb(`${s}-foreground`)
+ if (solid && bg) {
+ need(theme, mode, contrast(solid, bg), AA_TEXT, `text-${s} on background`)
+ }
+ if (solid && card) {
+ need(theme, mode, contrast(solid, card), AA_TEXT, `text-${s} on card`)
+ }
+ // `-foreground` sits on a 10% tint of its own colour over the background.
+ if (fg && solid && bg) {
+ need(
+ theme,
+ mode,
+ contrast(fg, over(solid, bg, 0.1)),
+ AA_TEXT,
+ `${s}-foreground on ${s}/10 tint`,
+ )
+ }
+ }
+
+ for (const ui of UI_ON_SURFACE) {
+ const backdrop = ui.startsWith('sidebar') ? 'sidebar' : 'background'
+ const c = rgb(ui, backdrop)
+ const surface = ui.startsWith('sidebar') ? (rgb('sidebar') ?? bg) : bg
+ if (!c || !surface) continue
+ const ratio = contrast(c, surface)
+ need(theme, mode, ratio, AA_UI, `${ui} against its surface`)
+ // "Visible without glowing" — flag borders that read as a bright line.
+ if (ui === 'border' && ratio > 7) {
+ notes.push(
+ `${theme}/${mode}: border is ${ratio.toFixed(2)}:1 against background — very high for a boundary`,
+ )
+ }
+ }
+
+ // Neutrals must not be literally gray.
+ for (const name of ['background', 'foreground', 'card', 'muted']) {
+ const c = oklchOf(name)
+ if (c && typeof c.c === 'number' && c.c === 0) {
+ failures.push(
+ `${theme}/${mode}: --${name} is a pure gray (chroma 0); neutrals must be warm`,
+ )
+ }
+ }
+
+ // Structural rules below are the ramp discipline the generated presets are
+ // authored to (see generate-themes.mjs). The default theme predates them and
+ // is deliberately held only to the accessibility floor above — restyling it
+ // would change the look every consumer gets out of the box.
+ if (mode === 'dark' && GENERATED.has(theme)) {
+ // Elevation must be monotonic and separated.
+ let prev = null
+ for (const name of SURFACE_ORDER) {
+ const c = oklchOf(name)
+ if (!c || typeof c.l !== 'number') continue
+ if (prev) {
+ const step = c.l - prev.l
+ const equalAllowed = prev.name === 'card' && name === 'popover'
+ if (step < 0) {
+ failures.push(
+ `${theme}/dark: --${name} (L ${c.l.toFixed(3)}) is darker than --${prev.name} (L ${prev.l.toFixed(3)}); elevation must increase`,
+ )
+ } else if (step < MIN_SURFACE_STEP && !(equalAllowed && step >= 0)) {
+ failures.push(
+ `${theme}/dark: --${prev.name} → --${name} steps only ${(step * 100).toFixed(1)}% lightness, needs ${MIN_SURFACE_STEP * 100}%`,
+ )
+ }
+ }
+ prev = { ...c, name }
+ }
+
+ // Dark chroma may not exceed the light value for the same token.
+ for (const key of Object.keys(vars)) {
+ const d = parse(vars[key])
+ const l = lightVars[key] ? parse(lightVars[key]) : null
+ if (!d || !l || typeof d.c !== 'number' || typeof l.c !== 'number')
+ continue
+ if (d.c > l.c + 1e-6) {
+ failures.push(
+ `${theme}/dark: ${key} chroma ${d.c.toFixed(3)} exceeds light ${l.c.toFixed(3)}`,
+ )
+ }
+ }
+ }
+}
+
+const { tokens } = JSON.parse(
+ readFileSync(join(root, 'dist', 'tokens.json'), 'utf8'),
+)
+const themes = {}
+for (const t of tokens) {
+ const entry = (themes[t.theme] ??= { light: {}, dark: {} })
+ entry.light[t.name] = t.light ?? t.dark
+ entry.dark[t.name] = t.dark
+}
+
+for (const [theme, { light, dark }] of Object.entries(themes)) {
+ checkTheme(theme, 'light', light, light)
+ checkTheme(theme, 'dark', dark, light)
+}
+
+if (!QUIET) for (const n of notes) console.error(` ~ ${n}`)
+
+if (failures.length) {
+ console.error(`\n✗ contrast gate: ${failures.length} failure(s)\n`)
+ for (const f of failures) console.error(` ${f}`)
+ console.error('')
+ process.exit(1)
+}
+
+console.log(
+ ` contrast: ${Object.keys(themes).length} themes pass AA (${AA_TEXT}:1 text, ${AA_UI}:1 UI) in both modes`,
+)
diff --git a/packages/themes/scripts/emit-tokens.mjs b/packages/themes/scripts/emit-tokens.mjs
new file mode 100644
index 0000000..2620c22
--- /dev/null
+++ b/packages/themes/scripts/emit-tokens.mjs
@@ -0,0 +1,151 @@
+#!/usr/bin/env node
+/**
+ * Emits dist/tokens.json — the declared public token surface.
+ *
+ * Why this exists: the token surface used to be discoverable only by scanning
+ * generated CSS, which mixes rnui's tokens with Tailwind's `--tw-*` plumbing
+ * (`--tw-translate-x`, `--tw-ease`, the `--tw-space-*-reverse` pair, …).
+ * Downstream extraction then presents Tailwind internals as if they were part
+ * of the rnui palette, and the only way to correct it is to hand-patch the
+ * generated stylesheet — patches that every recompile wipes.
+ *
+ * So the export is built from the hand-authored theme sources instead, never
+ * from compiled output. `--tw-*` cannot appear here by construction: those
+ * variables exist only in Tailwind's output, which this script never reads.
+ *
+ * Run: node scripts/emit-tokens.mjs (wired into `pnpm build`)
+ */
+
+import { readFileSync, readdirSync, writeFileSync } from 'node:fs'
+import { dirname, join } from 'node:path'
+import { fileURLToPath } from 'node:url'
+
+const root = join(dirname(fileURLToPath(import.meta.url)), '..')
+const srcDir = join(root, 'src')
+const distDir = join(root, 'dist')
+
+// Files that are not themes: infrastructure, or the generated aggregate.
+const NOT_A_THEME = new Set([
+ 'base.css',
+ 'index.css',
+ 'presets.css',
+ 'safelist.css',
+ 'utilities.css',
+ 'light.css',
+ 'dark.css',
+])
+
+/** Pull `--name: value` pairs out of the first block matching `selector`. */
+function readBlock(css, selector) {
+ const re = new RegExp(
+ `${selector.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*\\{([\\s\\S]*?)\\n\\}`,
+ 'm',
+ )
+ const m = re.exec(css)
+ if (!m) return {}
+ const out = {}
+ for (const decl of m[1].matchAll(/--([\w-]+)\s*:\s*([^;]+);/g)) {
+ out[`--${decl[1]}`] = decl[2].trim()
+ }
+ return out
+}
+
+/**
+ * `kind` is derived from the token's name first (names are the stable contract)
+ * and only falls back to sniffing the value.
+ */
+function kindOf(name, value) {
+ if (name.startsWith('--font-')) return 'font'
+ if (name === '--radius' || name.startsWith('--radius-')) return 'radius'
+ if (/^(oklch|rgb|hsl|color|#)/i.test(value)) return 'color'
+ if (/^-?[\d.]+(rem|px|em|%|vh|vw)$/.test(value)) return 'dimension'
+ if (/^(calc|var)\(/.test(value)) return 'dimension'
+ return 'color'
+}
+
+const themes = []
+
+// The default theme is split across light.css (:root) and dark.css (.dark).
+themes.push({
+ slug: 'default',
+ light: readBlock(readFileSync(join(srcDir, 'light.css'), 'utf8'), ':root'),
+ dark: readBlock(readFileSync(join(srcDir, 'dark.css'), 'utf8'), '.dark'),
+})
+
+// Each preset carries both ramps in one file.
+for (const file of readdirSync(srcDir).sort()) {
+ if (!file.endsWith('.css') || NOT_A_THEME.has(file)) continue
+ const css = readFileSync(join(srcDir, file), 'utf8')
+ themes.push({
+ slug: file.replace(/\.css$/, ''),
+ light: readBlock(css, ':root'),
+ dark: readBlock(css, '.dark'),
+ })
+}
+
+const tokens = []
+for (const { slug, light, dark } of themes) {
+ // A preset's dark block may omit tokens it inherits from its own light ramp.
+ // Record what the theme actually declares, and flag the inherited ones so an
+ // incomplete dark ramp is visible rather than silently papered over.
+ for (const name of Object.keys(light)) {
+ const darkValue = dark[name] ?? null
+ tokens.push({
+ name,
+ theme: slug,
+ kind: kindOf(name, light[name]),
+ value: light[name],
+ light: light[name],
+ dark: darkValue ?? light[name],
+ darkInherited: darkValue === null,
+ })
+ }
+ // Tokens that appear only in the dark ramp (rare, but don't drop them).
+ for (const name of Object.keys(dark)) {
+ if (name in light) continue
+ tokens.push({
+ name,
+ theme: slug,
+ kind: kindOf(name, dark[name]),
+ value: dark[name],
+ light: null,
+ dark: dark[name],
+ darkInherited: false,
+ })
+ }
+}
+
+const offenders = tokens.filter((t) => t.name.startsWith('--tw-'))
+if (offenders.length) {
+ console.error(
+ `✗ tokens.json would contain ${offenders.length} --tw-* entries: ${offenders
+ .map((t) => t.name)
+ .join(', ')}`,
+ )
+ process.exit(1)
+}
+
+const byTheme = {}
+for (const t of tokens) (byTheme[t.theme] ??= []).push(t)
+
+writeFileSync(
+ join(distDir, 'tokens.json'),
+ JSON.stringify(
+ {
+ $comment:
+ 'Declared public design tokens for @e412/rnui-themes. Built from the hand-authored theme sources, never from compiled CSS, so Tailwind-internal custom properties are structurally excluded. This file is the source of truth for token extraction; do not scan the generated stylesheets.',
+ generator: '@e412/rnui-themes',
+ themes: Object.keys(byTheme),
+ count: tokens.length,
+ tokens,
+ },
+ null,
+ 2,
+ ) + '\n',
+)
+
+const incomplete = tokens.filter((t) => t.darkInherited).length
+console.log(
+ ` tokens.json: ${tokens.length} tokens across ${Object.keys(byTheme).length} themes` +
+ (incomplete ? ` (${incomplete} inherit their dark value)` : ''),
+)
diff --git a/packages/themes/scripts/emit-utilities.mjs b/packages/themes/scripts/emit-utilities.mjs
new file mode 100644
index 0000000..fa9c547
--- /dev/null
+++ b/packages/themes/scripts/emit-utilities.mjs
@@ -0,0 +1,77 @@
+#!/usr/bin/env node
+/**
+ * Post-build step for the precompiled utility layer.
+ *
+ * 1. Copies src/safelist.css to dist/ so `@e412/rnui-themes/safelist` is
+ * importable from the published package (only `dist` is published).
+ * 2. Writes dist/utilities.json — the list of utility classes that actually
+ * shipped in dist/utilities.css.
+ *
+ * (2) is deliberately derived from the COMPILED output, not from the safelist
+ * source. The safelist says what we asked for; utilities.json says what Tailwind
+ * emitted. Downstream tooling can then tell a consumer "`gap-9` is not in this
+ * bundle" instead of letting the class fail silently.
+ *
+ * Run: node scripts/emit-utilities.mjs (wired into `pnpm build`)
+ */
+
+import { copyFileSync, readFileSync, writeFileSync } from 'node:fs'
+import { dirname, join } from 'node:path'
+import { fileURLToPath } from 'node:url'
+
+const root = join(dirname(fileURLToPath(import.meta.url)), '..')
+const srcDir = join(root, 'src')
+const distDir = join(root, 'dist')
+
+copyFileSync(join(srcDir, 'safelist.css'), join(distDir, 'safelist.css'))
+
+const css = readFileSync(join(distDir, 'utilities.css'), 'utf8')
+
+// Tailwind escapes the characters that aren't bare-identifier safe: `sm\:p-4`,
+// `w-1\/2`, `p-0\.5`, `bg-primary\/50`. Unescape to get the class a consumer
+// would actually type. Selectors are collected from rule preludes only, so
+// at-rule bodies (@media, @supports) contribute their inner selectors and
+// nothing else.
+const classes = new Set()
+for (const m of css.matchAll(/\.((?:[\w-]|\\.)+)/g)) {
+ const cls = m[1].replace(/\\(.)/g, '$1')
+ // Skip Tailwind's internal marker classes and anything that isn't a utility
+ // a consumer would write.
+ if (cls.startsWith('tw-') || cls.startsWith('_')) continue
+ classes.add(cls)
+}
+
+const sorted = [...classes].sort((a, b) => a.localeCompare(b))
+
+// Group by utility family (the part before the first numeric/fractional value)
+// so consumers can answer "which gap steps exist?" without scanning the flat list.
+const families = {}
+for (const cls of sorted) {
+ const bare = cls.includes(':') ? cls.slice(cls.lastIndexOf(':') + 1) : cls
+ const family =
+ bare.replace(/-?\[.*$/, '').replace(/-(\d.*|full|auto|none)$/, '') || bare
+ ;(families[family] ??= []).push(cls)
+}
+
+writeFileSync(
+ join(distDir, 'utilities.json'),
+ JSON.stringify(
+ {
+ $comment:
+ 'Utility classes present in dist/utilities.css. Generated from the compiled output by scripts/emit-utilities.mjs — a class absent here will not apply in a precompiled consumer.',
+ generator: '@e412/rnui-themes',
+ count: sorted.length,
+ familyCount: Object.keys(families).length,
+ classes: sorted,
+ families: Object.fromEntries(
+ Object.entries(families).sort(([a], [b]) => a.localeCompare(b)),
+ ),
+ },
+ null,
+ 2,
+ ) + '\n',
+)
+
+console.log(
+ ` utilities.json: ${sorted.length} classes across ${Object.keys(families).length} families`,
+)
diff --git a/packages/themes/scripts/generate-presets.mjs b/packages/themes/scripts/generate-presets.mjs
index e9cb8a5..7815870 100644
--- a/packages/themes/scripts/generate-presets.mjs
+++ b/packages/themes/scripts/generate-presets.mjs
@@ -14,16 +14,7 @@ import { fileURLToPath } from 'node:url'
const __dirname = dirname(fileURLToPath(import.meta.url))
const srcDir = join(__dirname, '..', 'src')
-const themeNames = [
- 'oxide',
- 'ocean',
- 'violet',
- 'forest',
- 'rose',
- 'amber',
- 'slate',
- 'crimson',
-]
+const themeNames = ['press', 'console', 'signal']
let output = `/* Auto-generated from theme source files — do not edit directly.
* Run: node scripts/generate-presets.mjs
diff --git a/packages/themes/scripts/generate-themes.mjs b/packages/themes/scripts/generate-themes.mjs
new file mode 100644
index 0000000..846c5a2
--- /dev/null
+++ b/packages/themes/scripts/generate-themes.mjs
@@ -0,0 +1,405 @@
+#!/usr/bin/env node
+/**
+ * Generates the theme source files from a small anchor spec.
+ *
+ * Each theme is defined by a handful of anchors (background, foreground,
+ * primary, in both modes) plus its hues, radius and fonts. Everything else —
+ * surfaces, borders, rings, muted text, status foregrounds — is DERIVED here so
+ * the numeric constraints hold by construction rather than by hand-tuning ~270
+ * values and hoping:
+ *
+ * - dark surfaces step monotonically by >= 4% lightness (elevation reads as
+ * elevation, not noise)
+ * - dark chroma never exceeds the light value for the same token
+ * - neutrals are never literally gray
+ * - one accent hue per theme drives primary / focus / ring / sidebar-primary,
+ * and `accent` is a tint of it rather than a second colour
+ * - status hues are shared across all themes so they stay recognizable
+ * - contrast floors are solved for, not guessed
+ *
+ * scripts/check-contrast.mjs is the gate that proves the output; this script is
+ * what makes passing it reproducible.
+ *
+ * Run: node scripts/generate-themes.mjs (wired into `pnpm build`)
+ */
+
+import { writeFileSync } from 'node:fs'
+import { dirname, join } from 'node:path'
+import { fileURLToPath } from 'node:url'
+import { converter, parse } from 'culori'
+
+const srcDir = join(dirname(fileURLToPath(import.meta.url)), '..', 'src')
+const toRgb = converter('rgb')
+
+const ok = (l, c, h, a) =>
+ a == null ? { mode: 'oklch', l, c, h } : { mode: 'oklch', l, c, h, alpha: a }
+const fmt = ({ l, c, h, alpha }) => {
+ const base = `oklch(${+(l * 100).toFixed(1)}% ${+c.toFixed(4)} ${+h.toFixed(1)}`
+ return alpha == null || alpha >= 1
+ ? `${base})`
+ : `${base} / ${+alpha.toFixed(2)})`
+}
+
+const lum = (rgb) => {
+ const lin = (x) => (x <= 0.04045 ? x / 12.92 : ((x + 0.055) / 1.055) ** 2.4)
+ return 0.2126 * lin(rgb.r) + 0.7152 * lin(rgb.g) + 0.0722 * lin(rgb.b)
+}
+const ratio = (a, b) => {
+ const [x, y] = [lum(toRgb(a)), lum(toRgb(b))].sort((m, n) => n - m)
+ return (x + 0.05) / (y + 0.05)
+}
+const over = (fg, bg, alpha) => {
+ const f = toRgb(fg)
+ const b = toRgb(bg)
+ return {
+ mode: 'rgb',
+ r: f.r * alpha + b.r * (1 - alpha),
+ g: f.g * alpha + b.g * (1 - alpha),
+ b: f.b * alpha + b.b * (1 - alpha),
+ }
+}
+
+/**
+ * Find the lightness at (chroma, hue) that just clears `target` contrast
+ * against `backdrop`, searching in `dir` (+1 lighter, -1 darker). Binary search
+ * on L; returns the least-extreme value that passes so colours stay as close to
+ * the surface as accessibility allows.
+ */
+function solveL(backdrop, target, { c, h, dir, from }) {
+ let lo = from
+ let hi = dir > 0 ? 1 : 0
+ if (ratio(ok(hi, c, h), backdrop) < target) return hi // unreachable; clamp
+ for (let i = 0; i < 40; i++) {
+ const mid = (lo + hi) / 2
+ if (ratio(ok(mid, c, h), backdrop) >= target) hi = mid
+ else lo = mid
+ }
+ return +hi.toFixed(4)
+}
+
+// Status hues are shared across every theme so success/info/warning/destructive
+// stay recognizable; only their lightness adapts to the mode.
+const STATUS = {
+ destructive: { h: 27, c: 0.16 },
+ success: { h: 152, c: 0.11 },
+ info: { h: 250, c: 0.12 },
+ warning: { h: 75, c: 0.13 },
+}
+
+const THEMES = [
+ {
+ slug: 'press',
+ title: 'Press — Editorial',
+ blurb: 'Warm paper, near-black ink, high contrast. Serif headings.',
+ fonts: {
+ sans: "'Source Sans 3', ui-sans-serif, system-ui, sans-serif",
+ heading: "'Source Serif 4', ui-serif, Georgia, serif",
+ mono: "'Geist Mono', ui-monospace, SFMono-Regular, monospace",
+ },
+ fontUrl:
+ 'https://fonts.googleapis.com/css2?family=Source+Sans+3:wght@400..700&family=Source+Serif+4:wght@400..700&display=swap',
+ radius: '0.5rem',
+ neutralHue: 70,
+ accentHue: 25,
+ light: {
+ bg: ok(0.975, 0.012, 85),
+ fg: ok(0.16, 0.015, 60),
+ primary: ok(0.45, 0.12, 25),
+ },
+ dark: {
+ bg: ok(0.15, 0.012, 60),
+ fg: ok(0.93, 0.012, 80),
+ primary: ok(0.72, 0.11, 30),
+ },
+ },
+ {
+ slug: 'console',
+ title: 'Console — Technical',
+ blurb: 'Dense, low-chroma, cool-leaning but never cold. Mono headings.',
+ fonts: {
+ sans: "'Geist', ui-sans-serif, system-ui, sans-serif",
+ heading: "'Geist Mono', ui-monospace, SFMono-Regular, monospace",
+ mono: "'Geist Mono', ui-monospace, SFMono-Regular, monospace",
+ },
+ fontUrl:
+ 'https://fonts.googleapis.com/css2?family=Geist:wght@400..700&family=Geist+Mono:wght@400..700&display=swap',
+ radius: '0.125rem',
+ neutralHue: 243,
+ accentHue: 235,
+ dense: true,
+ light: {
+ bg: ok(0.965, 0.005, 240),
+ fg: ok(0.2, 0.01, 245),
+ primary: ok(0.52, 0.11, 235),
+ },
+ dark: {
+ bg: ok(0.16, 0.008, 245),
+ fg: ok(0.91, 0.008, 240),
+ primary: ok(0.7, 0.1, 230),
+ },
+ },
+ {
+ slug: 'signal',
+ title: 'Signal — Brand-forward',
+ blurb: 'Warm neutral shell, one confident accent doing all the work.',
+ fonts: {
+ sans: "'Instrument Sans', ui-sans-serif, system-ui, sans-serif",
+ heading: "'Space Grotesk', ui-sans-serif, system-ui, sans-serif",
+ mono: "'Geist Mono', ui-monospace, SFMono-Regular, monospace",
+ },
+ fontUrl:
+ 'https://fonts.googleapis.com/css2?family=Instrument+Sans:wght@400..700&family=Space+Grotesk:wght@400..700&display=swap',
+ radius: '0.75rem',
+ neutralHue: 72,
+ accentHue: 265,
+ light: {
+ bg: ok(0.98, 0.008, 75),
+ fg: ok(0.18, 0.014, 60),
+ primary: ok(0.55, 0.14, 265),
+ },
+ dark: {
+ bg: ok(0.17, 0.012, 65),
+ fg: ok(0.94, 0.01, 75),
+ primary: ok(0.72, 0.13, 268),
+ },
+ },
+]
+
+/** Build one mode's complete ramp. */
+function ramp(theme, mode) {
+ const dark = mode === 'dark'
+ const { bg, fg, primary } = theme[mode]
+ const nh = theme.neutralHue
+ const ah = theme.accentHue
+ // Neutral chroma: small but never zero. Console runs lower by design.
+ // Identical in both modes so the dark-chroma clamp is a no-op for neutrals;
+ // clamping AFTER solving would shift a colour off the contrast floor it was
+ // solved for.
+ const nc = theme.slug === 'console' ? 0.006 : 0.01
+ const dir = dark ? 1 : -1 // in dark, surfaces get lighter as they elevate
+
+ // Surface ladder. Dark steps 4.5% so the >=4% gate has headroom; light stays
+ // tight because elevation there reads via border and shadow, not lightness.
+ // Dark elevates by getting lighter; light elevates by getting closer to
+ // white, with recessed surfaces stepping down instead.
+ const step = dark ? 0.045 : 0.02
+ const card = ok(
+ dark ? bg.l + step : Math.min(0.995, bg.l + step * 0.8),
+ nc * 0.8,
+ nh,
+ )
+ const popover = ok(card.l, nc * 0.8, nh)
+ const secondary = ok(dark ? card.l + step : bg.l - step, nc, nh)
+ const muted = ok(secondary.l, nc, nh)
+ // `accent` is a tint of the theme's one accent hue, not a second colour.
+ const accent = ok(dark ? muted.l + step : bg.l - step * 1.4, 0.018, ah)
+
+ // Boundaries: solve for exactly the floor the gate requires.
+ const borderL = solveL(bg, 3.05, {
+ c: nc,
+ h: nh,
+ dir,
+ from: dark ? accent.l : bg.l,
+ })
+ const border = ok(borderL, nc, nh)
+ const input = ok(borderL, nc, nh)
+ const ring = ok(
+ solveL(bg, 3.05, { c: 0.06, h: ah, dir, from: bg.l }),
+ 0.06,
+ ah,
+ )
+
+ /**
+ * Body text has to clear AA on EVERY surface it can land on, so solve against
+ * the worst one: in light mode that's the darkest surface (least contrast for
+ * dark text), in dark mode the lightest. Solving against `card` alone is what
+ * produces text that passes on cards and fails on the page.
+ */
+ const textSurfaces = [bg, card, muted]
+ const hardest = dark
+ ? textSurfaces.reduce((a, b) => (b.l > a.l ? b : a))
+ : textSurfaces.reduce((a, b) => (b.l < a.l ? b : a))
+ // Small margin above the 4.5 floor absorbs the rounding applied on output.
+ const AA = 4.65
+
+ const mutedFgL = solveL(hardest, AA, {
+ c: nc,
+ h: nh,
+ dir: dark ? 1 : -1,
+ from: dark ? accent.l : muted.l,
+ })
+ const mutedFg = ok(mutedFgL, nc, nh)
+
+ /**
+ * Text on a solid fill wants a clean, decisive colour — near-white on a dark
+ * fill, near-black on a light one — not the least-extreme value that merely
+ * clears the floor (that reads as a washed-out tint). Take the decisive end
+ * and only fall back to solving if the fill is so mid-toned that neither end
+ * passes.
+ */
+ const onSolid = (fill) => {
+ for (const cand of fill.l > 0.55
+ ? [ok(0.16, 0.02, ah)]
+ : [ok(0.98, 0.015, ah)]) {
+ if (ratio(cand, fill) >= AA) return cand
+ }
+ return ok(
+ solveL(fill, AA, {
+ c: 0.02,
+ h: ah,
+ dir: fill.l > 0.55 ? -1 : 1,
+ from: fill.l,
+ }),
+ 0.02,
+ ah,
+ )
+ }
+ const onPrimary = onSolid(primary)
+
+ const status = {}
+ for (const [name, { h, c }] of Object.entries(STATUS)) {
+ // The solid colour is used as TEXT on ordinary surfaces (`text-destructive`).
+ const solidL = solveL(hardest, AA, { c, h, dir: dark ? 1 : -1, from: 0.6 })
+ const solid = ok(solidL, c, h)
+ // The `-foreground` variant sits on a 10% tint of itself.
+ const tint = over(solid, bg, 0.1)
+ const fgL = solveL(tint, AA, {
+ c: c * 0.9,
+ h,
+ dir: dark ? 1 : -1,
+ from: solidL,
+ })
+ status[name] = solid
+ status[`${name}-foreground`] = ok(fgL, c * 0.9, h)
+ }
+
+ const invert = ok(dark ? 0.37 : 0.21, nc, nh)
+ const invertFg = ok(dark ? 0.98 : 0.98, nc * 0.4, nh)
+ const focus = ok(dark ? 0.62 : 0.55, 0.14, ah)
+ const focusFg = onSolid(focus)
+
+ // Charts: spread around the accent hue, moderate chroma, dark never hotter.
+ const chartC = dark ? 0.11 : 0.12
+ const chart = [0, 62, 128, 196, 284].map((offset, i) =>
+ ok(dark ? 0.7 - i * 0.015 : 0.55 + i * 0.01, chartC, (ah + offset) % 360),
+ )
+
+ // Sidebar is a slightly recessed surface sharing the theme's accent.
+ const sidebar = ok(dark ? bg.l + step * 0.6 : bg.l - step * 0.8, nc, nh)
+ const sidebarBorderL = solveL(sidebar, 3.05, {
+ c: nc,
+ h: nh,
+ dir,
+ from: sidebar.l,
+ })
+ const sidebarAccent = ok(
+ dark ? sidebar.l + step : sidebar.l - step * 1.4,
+ 0.018,
+ ah,
+ )
+ // Measured against the sidebar, so solve against it too.
+ const sidebarRing = ok(
+ solveL(sidebar, 3.05, { c: 0.06, h: ah, dir, from: sidebar.l }),
+ 0.06,
+ ah,
+ )
+
+ return {
+ '--background': bg,
+ '--foreground': fg,
+ '--card': card,
+ '--card-foreground': fg,
+ '--popover': popover,
+ '--popover-foreground': fg,
+ '--primary': primary,
+ '--primary-foreground': onPrimary,
+ '--secondary': secondary,
+ '--secondary-foreground': fg,
+ '--muted': muted,
+ '--muted-foreground': mutedFg,
+ '--accent': accent,
+ '--accent-foreground': fg,
+ '--destructive': status.destructive,
+ '--destructive-foreground': status['destructive-foreground'],
+ '--success': status.success,
+ '--success-foreground': status['success-foreground'],
+ '--info': status.info,
+ '--info-foreground': status['info-foreground'],
+ '--warning': status.warning,
+ '--warning-foreground': status['warning-foreground'],
+ '--invert': invert,
+ '--invert-foreground': invertFg,
+ '--focus': focus,
+ '--focus-foreground': focusFg,
+ '--border': border,
+ '--input': input,
+ '--ring': ring,
+ '--chart-1': chart[0],
+ '--chart-2': chart[1],
+ '--chart-3': chart[2],
+ '--chart-4': chart[3],
+ '--chart-5': chart[4],
+ '--sidebar': sidebar,
+ '--sidebar-foreground': fg,
+ '--sidebar-primary': primary,
+ '--sidebar-primary-foreground': onPrimary,
+ '--sidebar-accent': sidebarAccent,
+ '--sidebar-accent-foreground': fg,
+ '--sidebar-border': ok(sidebarBorderL, nc, nh),
+ '--sidebar-ring': sidebarRing,
+ }
+}
+
+/** Clamp every dark chroma to its light counterpart. */
+function clampChroma(light, dark) {
+ for (const key of Object.keys(dark)) {
+ const l = light[key]
+ if (l && dark[key].c > l.c) dark[key] = { ...dark[key], c: l.c }
+ }
+ return dark
+}
+
+for (const theme of THEMES) {
+ const light = ramp(theme, 'light')
+ const dark = clampChroma(light, ramp(theme, 'dark'))
+
+ const head = [
+ '/*',
+ ` * ${theme.title}`,
+ ` * ${theme.blurb}`,
+ ' *',
+ ` * Fonts: ${theme.fonts.heading.split(',')[0]} (headings) + ${theme.fonts.sans.split(',')[0]} (body)`,
+ ' * Load via Google Fonts, next/font, or self-host:',
+ ` * ${theme.fontUrl}`,
+ ' *',
+ ` * Radius: ${theme.radius}`,
+ ` * Accent hue: ~${theme.accentHue} · neutral hue: ~${theme.neutralHue}`,
+ ' *',
+ ' * GENERATED by scripts/generate-themes.mjs — edit the anchors there, not',
+ ' * this file. Values are solved against scripts/check-contrast.mjs.',
+ ' */',
+ ].join('\n')
+
+ const block = (sel, vars, extra = '') =>
+ `${sel} {\n` +
+ ` --font-sans: ${theme.fonts.sans};\n` +
+ ` --font-heading: ${theme.fonts.heading};\n` +
+ ` --font-mono: ${theme.fonts.mono};\n` +
+ ` --radius: ${theme.radius};\n` +
+ extra +
+ Object.entries(vars)
+ .map(([k, v]) => ` ${k}: ${fmt(v)};`)
+ .join('\n') +
+ '\n}\n'
+
+ // Console runs denser type than the other two.
+ const dense = theme.dense
+ ? ' --leading-base: 1.4;\n --text-base-size: 0.9375rem;\n'
+ : ''
+
+ writeFileSync(
+ join(srcDir, `${theme.slug}.css`),
+ `${head}\n\n${block(':root', light, dense)}\n${block('.dark', dark, dense)}`,
+ )
+ console.log(` ${theme.slug}.css`)
+}
diff --git a/packages/themes/src/amber.css b/packages/themes/src/amber.css
deleted file mode 100644
index 51a88cb..0000000
--- a/packages/themes/src/amber.css
+++ /dev/null
@@ -1,105 +0,0 @@
-/*
- * Amber — Sharp & Energetic
- * Angular, tight radius, golden tones with high contrast.
- * Feels like a fintech dashboard or developer tool.
- *
- * Font: Sora (geometric, energetic, precise)
- * Load via Google Fonts, next/font, or self-host:
- * https://fonts.googleapis.com/css2?family=Sora:wght@300;400;500;600;700;800&display=swap
- *
- * Radius: 0.25rem (sharp, angular, decisive)
- * Hue: ~75 (golden/amber)
- */
-
-:root {
- --font-sans: 'Sora', ui-sans-serif, system-ui, sans-serif;
- --radius: 0.25rem;
- --background: oklch(0.98 0.015 82);
- --foreground: oklch(0.18 0.025 70);
- --card: oklch(0.974 0.018 80);
- --card-foreground: oklch(0.18 0.025 70);
- --popover: oklch(0.98 0.015 82);
- --popover-foreground: oklch(0.18 0.025 70);
- --primary: oklch(0.62 0.17 75);
- --primary-foreground: oklch(0.2 0.035 70);
- --secondary: oklch(0.94 0.022 80);
- --secondary-foreground: oklch(0.28 0.04 70);
- --muted: oklch(0.935 0.02 80);
- --muted-foreground: oklch(0.5 0.028 75);
- --accent: oklch(0.92 0.026 78);
- --accent-foreground: oklch(0.28 0.04 70);
- --destructive: oklch(0.52 0.2 10);
- --destructive-foreground: oklch(0.4 0.15 10);
- --success: oklch(0.62 0.14 160);
- --success-foreground: oklch(0.38 0.09 160);
- --info: oklch(0.52 0.13 260);
- --info-foreground: oklch(0.38 0.1 260);
- --warning: oklch(0.72 0.17 70);
- --warning-foreground: oklch(0.4 0.12 60);
- --invert: oklch(0.21 0.025 70);
- --invert-foreground: oklch(0.97 0.008 80);
- --focus: oklch(0.6 0.17 75);
- --focus-foreground: oklch(0.2 0.035 70);
- --border: oklch(0.89 0.02 80);
- --input: oklch(0.89 0.02 80);
- --ring: oklch(0.62 0.08 75);
- --chart-1: oklch(0.65 0.18 75);
- --chart-2: oklch(0.55 0.17 45);
- --chart-3: oklch(0.72 0.15 100);
- --chart-4: oklch(0.5 0.14 20);
- --chart-5: oklch(0.58 0.11 130);
- --sidebar: oklch(0.968 0.018 80);
- --sidebar-foreground: oklch(0.18 0.025 70);
- --sidebar-primary: oklch(0.62 0.17 75);
- --sidebar-primary-foreground: oklch(0.2 0.035 70);
- --sidebar-accent: oklch(0.92 0.026 78);
- --sidebar-accent-foreground: oklch(0.28 0.04 70);
- --sidebar-border: oklch(0.89 0.02 80);
- --sidebar-ring: oklch(0.62 0.08 75);
-}
-
-.dark {
- /* Dark gold — deep, warm sepia, golden highlights pop with high contrast */
- --background: oklch(0.13 0.02 72);
- --foreground: oklch(0.93 0.012 80);
- --card: oklch(0.18 0.025 72);
- --card-foreground: oklch(0.93 0.012 80);
- --popover: oklch(0.18 0.025 72);
- --popover-foreground: oklch(0.93 0.012 80);
- --primary: oklch(0.78 0.16 75);
- --primary-foreground: oklch(0.13 0.035 70);
- --secondary: oklch(0.24 0.022 72);
- --secondary-foreground: oklch(0.93 0.012 80);
- --muted: oklch(0.24 0.022 72);
- --muted-foreground: oklch(0.62 0.028 75);
- --accent: oklch(0.25 0.026 72);
- --accent-foreground: oklch(0.93 0.012 80);
- --destructive: oklch(0.65 0.17 10);
- --destructive-foreground: oklch(0.52 0.2 10);
- --success: oklch(0.68 0.14 160);
- --success-foreground: oklch(0.56 0.12 160);
- --info: oklch(0.6 0.13 260);
- --info-foreground: oklch(0.5 0.15 260);
- --warning: oklch(0.78 0.17 70);
- --warning-foreground: oklch(0.65 0.15 65);
- --invert: oklch(0.34 0.025 70);
- --invert-foreground: oklch(0.97 0.008 80);
- --focus: oklch(0.72 0.17 75);
- --focus-foreground: oklch(0.2 0.035 70);
- --border: oklch(1 0.01 80 / 11%);
- --input: oklch(1 0.01 80 / 16%);
- --ring: oklch(0.55 0.08 75);
- --chart-1: oklch(0.75 0.18 75);
- --chart-2: oklch(0.66 0.17 45);
- --chart-3: oklch(0.8 0.15 100);
- --chart-4: oklch(0.62 0.14 20);
- --chart-5: oklch(0.7 0.11 130);
- --sidebar: oklch(0.16 0.023 72);
- --sidebar-foreground: oklch(0.93 0.012 80);
- --sidebar-primary: oklch(0.78 0.16 75);
- --sidebar-primary-foreground: oklch(0.2 0.035 70);
- --sidebar-accent: oklch(0.25 0.026 72);
- --sidebar-accent-foreground: oklch(0.93 0.012 80);
- --sidebar-border: oklch(1 0.01 80 / 11%);
- --sidebar-ring: oklch(0.55 0.08 75);
-}
diff --git a/packages/themes/src/console.css b/packages/themes/src/console.css
new file mode 100644
index 0000000..b516eb2
--- /dev/null
+++ b/packages/themes/src/console.css
@@ -0,0 +1,116 @@
+/*
+ * Console — Technical
+ * Dense, low-chroma, cool-leaning but never cold. Mono headings.
+ *
+ * Fonts: 'Geist Mono' (headings) + 'Geist' (body)
+ * Load via Google Fonts, next/font, or self-host:
+ * https://fonts.googleapis.com/css2?family=Geist:wght@400..700&family=Geist+Mono:wght@400..700&display=swap
+ *
+ * Radius: 0.125rem
+ * Accent hue: ~235 · neutral hue: ~243
+ *
+ * GENERATED by scripts/generate-themes.mjs — edit the anchors there, not
+ * this file. Values are solved against scripts/check-contrast.mjs.
+ */
+
+:root {
+ --font-sans: 'Geist', ui-sans-serif, system-ui, sans-serif;
+ --font-heading: 'Geist Mono', ui-monospace, SFMono-Regular, monospace;
+ --font-mono: 'Geist Mono', ui-monospace, SFMono-Regular, monospace;
+ --radius: 0.125rem;
+ --leading-base: 1.4;
+ --text-base-size: 0.9375rem;
+ --background: oklch(96.5% 0.005 240);
+ --foreground: oklch(20% 0.01 245);
+ --card: oklch(98.1% 0.0048 243);
+ --card-foreground: oklch(20% 0.01 245);
+ --popover: oklch(98.1% 0.0048 243);
+ --popover-foreground: oklch(20% 0.01 245);
+ --primary: oklch(52% 0.11 235);
+ --primary-foreground: oklch(98% 0.015 235);
+ --secondary: oklch(94.5% 0.006 243);
+ --secondary-foreground: oklch(20% 0.01 245);
+ --muted: oklch(94.5% 0.006 243);
+ --muted-foreground: oklch(52.2% 0.006 243);
+ --accent: oklch(93.7% 0.018 235);
+ --accent-foreground: oklch(20% 0.01 245);
+ --destructive: oklch(54.1% 0.16 27);
+ --destructive-foreground: oklch(52% 0.144 27);
+ --success: oklch(51% 0.11 152);
+ --success-foreground: oklch(49.5% 0.099 152);
+ --info: oklch(52.2% 0.12 250);
+ --info-foreground: oklch(50.5% 0.108 250);
+ --warning: oklch(52.8% 0.13 75);
+ --warning-foreground: oklch(51% 0.117 75);
+ --invert: oklch(21% 0.006 243);
+ --invert-foreground: oklch(98% 0.0024 243);
+ --focus: oklch(55% 0.14 235);
+ --focus-foreground: oklch(99.7% 0.02 235);
+ --border: oklch(63.9% 0.006 243);
+ --input: oklch(63.9% 0.006 243);
+ --ring: oklch(63.6% 0.06 235);
+ --chart-1: oklch(55% 0.12 235);
+ --chart-2: oklch(56% 0.12 297);
+ --chart-3: oklch(57% 0.12 3);
+ --chart-4: oklch(58% 0.12 71);
+ --chart-5: oklch(59% 0.12 159);
+ --sidebar: oklch(94.9% 0.006 243);
+ --sidebar-foreground: oklch(20% 0.01 245);
+ --sidebar-primary: oklch(52% 0.11 235);
+ --sidebar-primary-foreground: oklch(98% 0.015 235);
+ --sidebar-accent: oklch(92.1% 0.018 235);
+ --sidebar-accent-foreground: oklch(20% 0.01 245);
+ --sidebar-border: oklch(62.7% 0.006 243);
+ --sidebar-ring: oklch(62.4% 0.06 235);
+}
+
+.dark {
+ --font-sans: 'Geist', ui-sans-serif, system-ui, sans-serif;
+ --font-heading: 'Geist Mono', ui-monospace, SFMono-Regular, monospace;
+ --font-mono: 'Geist Mono', ui-monospace, SFMono-Regular, monospace;
+ --radius: 0.125rem;
+ --leading-base: 1.4;
+ --text-base-size: 0.9375rem;
+ --background: oklch(16% 0.005 245);
+ --foreground: oklch(91% 0.008 240);
+ --card: oklch(20.5% 0.0048 243);
+ --card-foreground: oklch(91% 0.008 240);
+ --popover: oklch(20.5% 0.0048 243);
+ --popover-foreground: oklch(91% 0.008 240);
+ --primary: oklch(70% 0.1 230);
+ --primary-foreground: oklch(16% 0.015 235);
+ --secondary: oklch(25% 0.006 243);
+ --secondary-foreground: oklch(91% 0.008 240);
+ --muted: oklch(25% 0.006 243);
+ --muted-foreground: oklch(63.4% 0.006 243);
+ --accent: oklch(29.5% 0.018 235);
+ --accent-foreground: oklch(91% 0.008 240);
+ --destructive: oklch(65.3% 0.16 27);
+ --destructive-foreground: oklch(65.3% 0.144 27);
+ --success: oklch(62.2% 0.11 152);
+ --success-foreground: oklch(62.2% 0.099 152);
+ --info: oklch(63.3% 0.12 250);
+ --info-foreground: oklch(63.3% 0.108 250);
+ --warning: oklch(64% 0.13 75);
+ --warning-foreground: oklch(64% 0.117 75);
+ --invert: oklch(37% 0.006 243);
+ --invert-foreground: oklch(98% 0.0024 243);
+ --focus: oklch(62% 0.14 235);
+ --focus-foreground: oklch(16% 0.02 235);
+ --border: oklch(48.6% 0.006 243);
+ --input: oklch(48.6% 0.006 243);
+ --ring: oklch(48.3% 0.06 235);
+ --chart-1: oklch(70% 0.11 235);
+ --chart-2: oklch(68.5% 0.11 297);
+ --chart-3: oklch(67% 0.11 3);
+ --chart-4: oklch(65.5% 0.11 71);
+ --chart-5: oklch(64% 0.11 159);
+ --sidebar: oklch(18.7% 0.006 243);
+ --sidebar-foreground: oklch(91% 0.008 240);
+ --sidebar-primary: oklch(70% 0.1 230);
+ --sidebar-primary-foreground: oklch(16% 0.015 235);
+ --sidebar-accent: oklch(23.2% 0.018 235);
+ --sidebar-accent-foreground: oklch(91% 0.008 240);
+ --sidebar-border: oklch(49.6% 0.006 243);
+ --sidebar-ring: oklch(49.4% 0.06 235);
+}
diff --git a/packages/themes/src/crimson.css b/packages/themes/src/crimson.css
deleted file mode 100644
index 2fe4992..0000000
--- a/packages/themes/src/crimson.css
+++ /dev/null
@@ -1,105 +0,0 @@
-/*
- * Crimson — Brutalist Bold
- * Zero radius, stark, high contrast, bold red.
- * Feels like a striking editorial site or brutalist web design.
- *
- * Font: Instrument Sans (sharp, contemporary, confident)
- * Load via Google Fonts, next/font, or self-host:
- * https://fonts.googleapis.com/css2?family=Instrument+Sans:wght@400;500;600;700&display=swap
- *
- * Radius: 0 (completely square, brutalist)
- * Hue: ~18 (scarlet/crimson)
- */
-
-:root {
- --font-sans: 'Instrument Sans', ui-sans-serif, system-ui, sans-serif;
- --radius: 0;
- --background: oklch(0.982 0.012 15);
- --foreground: oklch(0.14 0.025 15);
- --card: oklch(0.976 0.015 14);
- --card-foreground: oklch(0.14 0.025 15);
- --popover: oklch(0.982 0.012 15);
- --popover-foreground: oklch(0.14 0.025 15);
- --primary: oklch(0.5 0.2 18);
- --primary-foreground: oklch(0.97 0.008 15);
- --secondary: oklch(0.94 0.018 15);
- --secondary-foreground: oklch(0.22 0.04 15);
- --muted: oklch(0.935 0.016 15);
- --muted-foreground: oklch(0.48 0.028 15);
- --accent: oklch(0.92 0.022 12);
- --accent-foreground: oklch(0.22 0.04 15);
- --destructive: oklch(0.48 0.22 5);
- --destructive-foreground: oklch(0.36 0.16 5);
- --success: oklch(0.6 0.14 155);
- --success-foreground: oklch(0.38 0.09 155);
- --info: oklch(0.52 0.13 245);
- --info-foreground: oklch(0.38 0.1 245);
- --warning: oklch(0.74 0.16 55);
- --warning-foreground: oklch(0.4 0.1 45);
- --invert: oklch(0.18 0.025 15);
- --invert-foreground: oklch(0.97 0.008 15);
- --focus: oklch(0.5 0.2 18);
- --focus-foreground: oklch(0.97 0.008 15);
- --border: oklch(0.88 0.018 15);
- --input: oklch(0.88 0.018 15);
- --ring: oklch(0.58 0.1 18);
- --chart-1: oklch(0.52 0.2 18);
- --chart-2: oklch(0.45 0.16 350);
- --chart-3: oklch(0.65 0.17 45);
- --chart-4: oklch(0.4 0.1 30);
- --chart-5: oklch(0.58 0.14 75);
- --sidebar: oklch(0.968 0.015 15);
- --sidebar-foreground: oklch(0.14 0.025 15);
- --sidebar-primary: oklch(0.5 0.2 18);
- --sidebar-primary-foreground: oklch(0.97 0.008 15);
- --sidebar-accent: oklch(0.92 0.022 12);
- --sidebar-accent-foreground: oklch(0.22 0.04 15);
- --sidebar-border: oklch(0.88 0.018 15);
- --sidebar-ring: oklch(0.58 0.1 18);
-}
-
-.dark {
- /* True black — deepest dark mode, stark contrast, bold red cuts through */
- --background: oklch(0.08 0.015 15);
- --foreground: oklch(0.94 0.01 15);
- --card: oklch(0.13 0.02 15);
- --card-foreground: oklch(0.94 0.01 15);
- --popover: oklch(0.13 0.02 15);
- --popover-foreground: oklch(0.94 0.01 15);
- --primary: oklch(0.68 0.2 18);
- --primary-foreground: oklch(0.97 0.008 15);
- --secondary: oklch(0.18 0.02 15);
- --secondary-foreground: oklch(0.94 0.01 15);
- --muted: oklch(0.18 0.02 15);
- --muted-foreground: oklch(0.58 0.025 15);
- --accent: oklch(0.19 0.025 12);
- --accent-foreground: oklch(0.94 0.01 15);
- --destructive: oklch(0.6 0.2 5);
- --destructive-foreground: oklch(0.48 0.22 5);
- --success: oklch(0.66 0.14 155);
- --success-foreground: oklch(0.54 0.12 155);
- --info: oklch(0.6 0.13 245);
- --info-foreground: oklch(0.5 0.15 245);
- --warning: oklch(0.78 0.16 55);
- --warning-foreground: oklch(0.66 0.14 50);
- --invert: oklch(0.28 0.025 15);
- --invert-foreground: oklch(0.97 0.008 15);
- --focus: oklch(0.6 0.2 18);
- --focus-foreground: oklch(0.97 0.008 15);
- --border: oklch(1 0.008 15 / 14%);
- --input: oklch(1 0.008 15 / 18%);
- --ring: oklch(0.52 0.1 18);
- --chart-1: oklch(0.65 0.2 18);
- --chart-2: oklch(0.6 0.16 350);
- --chart-3: oklch(0.75 0.17 45);
- --chart-4: oklch(0.55 0.1 30);
- --chart-5: oklch(0.7 0.14 75);
- --sidebar: oklch(0.11 0.018 15);
- --sidebar-foreground: oklch(0.94 0.01 15);
- --sidebar-primary: oklch(0.68 0.2 18);
- --sidebar-primary-foreground: oklch(0.97 0.008 15);
- --sidebar-accent: oklch(0.19 0.025 12);
- --sidebar-accent-foreground: oklch(0.94 0.01 15);
- --sidebar-border: oklch(1 0.008 15 / 14%);
- --sidebar-ring: oklch(0.52 0.1 18);
-}
diff --git a/packages/themes/src/dark.css b/packages/themes/src/dark.css
index e02ba62..c7c885a 100644
--- a/packages/themes/src/dark.css
+++ b/packages/themes/src/dark.css
@@ -14,19 +14,19 @@
--accent: oklch(0.27 0.008 55);
--accent-foreground: oklch(0.96 0.005 75);
--destructive: oklch(0.704 0.191 22.216);
- --destructive-foreground: oklch(57.7% 0.245 27.325);
+ --destructive-foreground: oklch(63.9% 0.245 27.3);
--success: oklch(69.6% 0.17 162.48);
--success-foreground: oklch(59.6% 0.145 163.225);
- --info: oklch(60.6% 0.25 292.717);
- --info-foreground: oklch(54.1% 0.281 293.009);
+ --info: oklch(67.4% 0.25 292.7);
+ --info-foreground: oklch(64% 0.281 293);
--warning: oklch(79.5% 0.184 86.047);
--warning-foreground: oklch(68.1% 0.162 75.834);
--invert: oklch(0.37 0.008 55);
--invert-foreground: oklch(0.98 0.004 75);
--focus: oklch(54.6% 0.245 262.881);
--focus-foreground: oklch(98.5% 0 0);
- --border: oklch(1 0.004 75 / 10%);
- --input: oklch(1 0.004 75 / 15%);
+ --border: oklch(48.5% 0.004 75);
+ --input: oklch(48.5% 0.004 75);
--ring: oklch(0.55 0.008 55);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
@@ -39,6 +39,6 @@
--sidebar-primary-foreground: oklch(0.98 0.004 75);
--sidebar-accent: oklch(0.27 0.008 55);
--sidebar-accent-foreground: oklch(0.96 0.005 75);
- --sidebar-border: oklch(1 0.004 75 / 10%);
+ --sidebar-border: oklch(50.7% 0.004 75);
--sidebar-ring: oklch(0.55 0.008 55);
}
diff --git a/packages/themes/src/forest.css b/packages/themes/src/forest.css
deleted file mode 100644
index 791aef8..0000000
--- a/packages/themes/src/forest.css
+++ /dev/null
@@ -1,106 +0,0 @@
-/*
- * Forest — Organic Natural
- * Serif display, balanced radius, deep emerald tones.
- * Feels like a sustainable brand or editorial nature publication.
- *
- * Fonts: Fraunces (headings, optical-size serif) + Source Sans 3 (body, clean humanist)
- * Load via Google Fonts, next/font, or self-host:
- * https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,400;9..144,500;9..144,600;9..144,700&family=Source+Sans+3:wght@400;500;600;700&display=swap
- *
- * Radius: 0.5rem (balanced, grounded)
- * Hue: ~160 (emerald/jade)
- */
-
-:root {
- --font-sans: 'Source Sans 3', ui-sans-serif, system-ui, sans-serif;
- --font-heading: 'Fraunces', ui-serif, Georgia, serif;
- --radius: 0.5rem;
- --background: oklch(0.98 0.015 152);
- --foreground: oklch(0.17 0.025 160);
- --card: oklch(0.974 0.018 150);
- --card-foreground: oklch(0.17 0.025 160);
- --popover: oklch(0.98 0.015 152);
- --popover-foreground: oklch(0.17 0.025 160);
- --primary: oklch(0.48 0.14 160);
- --primary-foreground: oklch(0.97 0.008 155);
- --secondary: oklch(0.94 0.02 155);
- --secondary-foreground: oklch(0.25 0.04 160);
- --muted: oklch(0.935 0.018 155);
- --muted-foreground: oklch(0.5 0.028 158);
- --accent: oklch(0.92 0.025 150);
- --accent-foreground: oklch(0.25 0.04 160);
- --destructive: oklch(0.52 0.18 20);
- --destructive-foreground: oklch(0.4 0.14 20);
- --success: oklch(0.55 0.16 145);
- --success-foreground: oklch(0.37 0.1 145);
- --info: oklch(0.55 0.11 240);
- --info-foreground: oklch(0.38 0.08 240);
- --warning: oklch(0.74 0.15 90);
- --warning-foreground: oklch(0.42 0.1 80);
- --invert: oklch(0.21 0.025 160);
- --invert-foreground: oklch(0.97 0.008 155);
- --focus: oklch(0.52 0.14 165);
- --focus-foreground: oklch(0.97 0.008 155);
- --border: oklch(0.89 0.018 155);
- --input: oklch(0.89 0.018 155);
- --ring: oklch(0.58 0.06 158);
- --chart-1: oklch(0.52 0.15 160);
- --chart-2: oklch(0.45 0.12 130);
- --chart-3: oklch(0.68 0.14 120);
- --chart-4: oklch(0.55 0.1 190);
- --chart-5: oklch(0.62 0.13 90);
- --sidebar: oklch(0.968 0.017 152);
- --sidebar-foreground: oklch(0.17 0.025 160);
- --sidebar-primary: oklch(0.48 0.14 160);
- --sidebar-primary-foreground: oklch(0.97 0.008 155);
- --sidebar-accent: oklch(0.92 0.025 150);
- --sidebar-accent-foreground: oklch(0.25 0.04 160);
- --sidebar-border: oklch(0.89 0.018 155);
- --sidebar-ring: oklch(0.58 0.06 158);
-}
-
-.dark {
- /* Dense canopy — medium-dark, visible green tint, moonlit feel */
- --background: oklch(0.14 0.025 162);
- --foreground: oklch(0.93 0.012 152);
- --card: oklch(0.19 0.03 162);
- --card-foreground: oklch(0.93 0.012 152);
- --popover: oklch(0.19 0.03 162);
- --popover-foreground: oklch(0.93 0.012 152);
- --primary: oklch(0.7 0.13 158);
- --primary-foreground: oklch(0.14 0.03 160);
- --secondary: oklch(0.25 0.025 162);
- --secondary-foreground: oklch(0.93 0.012 152);
- --muted: oklch(0.25 0.025 162);
- --muted-foreground: oklch(0.62 0.028 157);
- --accent: oklch(0.26 0.03 158);
- --accent-foreground: oklch(0.93 0.012 152);
- --destructive: oklch(0.65 0.16 20);
- --destructive-foreground: oklch(0.52 0.18 20);
- --success: oklch(0.62 0.15 145);
- --success-foreground: oklch(0.52 0.13 145);
- --info: oklch(0.62 0.11 240);
- --info-foreground: oklch(0.5 0.13 240);
- --warning: oklch(0.78 0.15 90);
- --warning-foreground: oklch(0.66 0.13 85);
- --invert: oklch(0.34 0.028 160);
- --invert-foreground: oklch(0.97 0.008 155);
- --focus: oklch(0.58 0.14 165);
- --focus-foreground: oklch(0.97 0.008 155);
- --border: oklch(1 0.01 155 / 10%);
- --input: oklch(1 0.01 155 / 15%);
- --ring: oklch(0.52 0.06 158);
- --chart-1: oklch(0.65 0.15 160);
- --chart-2: oklch(0.58 0.12 130);
- --chart-3: oklch(0.76 0.14 120);
- --chart-4: oklch(0.68 0.1 190);
- --chart-5: oklch(0.72 0.13 90);
- --sidebar: oklch(0.17 0.028 162);
- --sidebar-foreground: oklch(0.93 0.012 152);
- --sidebar-primary: oklch(0.7 0.13 158);
- --sidebar-primary-foreground: oklch(0.97 0.008 155);
- --sidebar-accent: oklch(0.26 0.03 158);
- --sidebar-accent-foreground: oklch(0.93 0.012 152);
- --sidebar-border: oklch(1 0.01 155 / 10%);
- --sidebar-ring: oklch(0.52 0.06 158);
-}
diff --git a/packages/themes/src/light.css b/packages/themes/src/light.css
index 19739aa..1772b5a 100644
--- a/packages/themes/src/light.css
+++ b/packages/themes/src/light.css
@@ -13,24 +13,24 @@
--secondary: oklch(0.96 0.007 75);
--secondary-foreground: oklch(0.22 0.008 55);
--muted: oklch(0.955 0.006 75);
- --muted-foreground: oklch(0.55 0.01 55);
+ --muted-foreground: oklch(53.1% 0.01 55);
--accent: oklch(0.96 0.007 75);
--accent-foreground: oklch(0.22 0.008 55);
- --destructive: oklch(0.577 0.245 27.325);
+ --destructive: oklch(55.5% 0.245 27.3);
--destructive-foreground: oklch(44.4% 0.177 26.899);
- --success: oklch(69.6% 0.17 162.48);
+ --success: oklch(49.9% 0.17 162.5);
--success-foreground: oklch(37.8% 0.077 168.94);
- --info: oklch(60.6% 0.25 292.717);
+ --info: oklch(56% 0.25 292.7);
--info-foreground: oklch(38% 0.189 293.745);
- --warning: oklch(79.5% 0.184 86.047);
+ --warning: oklch(53.3% 0.184 86);
--warning-foreground: oklch(42.1% 0.095 57.708);
--invert: oklch(0.21 0.006 55);
--invert-foreground: oklch(0.98 0.004 75);
--focus: oklch(62.3% 0.214 259.815);
- --focus-foreground: oklch(98.5% 0 0);
- --border: oklch(0.912 0.006 75);
- --input: oklch(0.912 0.006 75);
- --ring: oklch(0.7 0.008 55);
+ --focus-foreground: oklch(22.4% 0 0);
+ --border: oklch(66.1% 0.006 75);
+ --input: oklch(66.1% 0.006 75);
+ --ring: oklch(66.2% 0.008 55);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
@@ -42,6 +42,6 @@
--sidebar-primary-foreground: oklch(0.98 0.004 75);
--sidebar-accent: oklch(0.96 0.007 75);
--sidebar-accent-foreground: oklch(0.22 0.008 55);
- --sidebar-border: oklch(0.912 0.006 75);
- --sidebar-ring: oklch(0.7 0.008 55);
+ --sidebar-border: oklch(65% 0.006 75);
+ --sidebar-ring: oklch(65.1% 0.008 55);
}
diff --git a/packages/themes/src/ocean.css b/packages/themes/src/ocean.css
deleted file mode 100644
index dc0c671..0000000
--- a/packages/themes/src/ocean.css
+++ /dev/null
@@ -1,105 +0,0 @@
-/*
- * Ocean — Smooth Aquatic
- * Rounded, generous spacing, deep teal tones.
- * Feels like a modern wellness or travel app.
- *
- * Font: Plus Jakarta Sans (geometric, friendly)
- * Load via Google Fonts, next/font, or self-host:
- * https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap
- *
- * Radius: 0.875rem (generous, smooth)
- * Hue: ~200 (deep teal/cyan)
- */
-
-:root {
- --font-sans: 'Plus Jakarta Sans', ui-sans-serif, system-ui, sans-serif;
- --radius: 0.875rem;
- --background: oklch(0.982 0.015 210);
- --foreground: oklch(0.17 0.025 220);
- --card: oklch(0.976 0.018 208);
- --card-foreground: oklch(0.17 0.025 220);
- --popover: oklch(0.982 0.015 210);
- --popover-foreground: oklch(0.17 0.025 220);
- --primary: oklch(0.52 0.115 198);
- --primary-foreground: oklch(0.97 0.008 200);
- --secondary: oklch(0.94 0.02 210);
- --secondary-foreground: oklch(0.25 0.04 210);
- --muted: oklch(0.935 0.018 210);
- --muted-foreground: oklch(0.5 0.03 215);
- --accent: oklch(0.92 0.025 205);
- --accent-foreground: oklch(0.25 0.04 210);
- --destructive: oklch(0.55 0.2 15);
- --destructive-foreground: oklch(0.42 0.15 15);
- --success: oklch(0.62 0.14 170);
- --success-foreground: oklch(0.38 0.09 170);
- --info: oklch(0.52 0.13 230);
- --info-foreground: oklch(0.38 0.1 230);
- --warning: oklch(0.76 0.15 80);
- --warning-foreground: oklch(0.42 0.1 65);
- --invert: oklch(0.21 0.025 220);
- --invert-foreground: oklch(0.97 0.008 200);
- --focus: oklch(0.55 0.12 220);
- --focus-foreground: oklch(0.97 0.008 200);
- --border: oklch(0.89 0.02 210);
- --input: oklch(0.89 0.02 210);
- --ring: oklch(0.6 0.06 205);
- --chart-1: oklch(0.55 0.13 200);
- --chart-2: oklch(0.45 0.14 230);
- --chart-3: oklch(0.68 0.13 175);
- --chart-4: oklch(0.55 0.14 260);
- --chart-5: oklch(0.72 0.1 170);
- --sidebar: oklch(0.97 0.018 210);
- --sidebar-foreground: oklch(0.17 0.025 220);
- --sidebar-primary: oklch(0.52 0.115 198);
- --sidebar-primary-foreground: oklch(0.97 0.008 200);
- --sidebar-accent: oklch(0.92 0.025 205);
- --sidebar-accent-foreground: oklch(0.25 0.04 210);
- --sidebar-border: oklch(0.89 0.02 210);
- --sidebar-ring: oklch(0.6 0.06 205);
-}
-
-.dark {
- /* Deep abyss — very dark, high chroma surfaces, bioluminescent teal accents */
- --background: oklch(0.13 0.03 218);
- --foreground: oklch(0.93 0.012 205);
- --card: oklch(0.18 0.035 218);
- --card-foreground: oklch(0.93 0.012 205);
- --popover: oklch(0.18 0.035 218);
- --popover-foreground: oklch(0.93 0.012 205);
- --primary: oklch(0.72 0.12 195);
- --primary-foreground: oklch(0.13 0.03 210);
- --secondary: oklch(0.24 0.03 218);
- --secondary-foreground: oklch(0.93 0.012 205);
- --muted: oklch(0.24 0.03 218);
- --muted-foreground: oklch(0.62 0.03 210);
- --accent: oklch(0.25 0.035 212);
- --accent-foreground: oklch(0.93 0.012 205);
- --destructive: oklch(0.68 0.17 15);
- --destructive-foreground: oklch(0.55 0.2 15);
- --success: oklch(0.68 0.14 170);
- --success-foreground: oklch(0.56 0.12 170);
- --info: oklch(0.6 0.13 230);
- --info-foreground: oklch(0.5 0.15 230);
- --warning: oklch(0.78 0.15 80);
- --warning-foreground: oklch(0.66 0.13 72);
- --invert: oklch(0.34 0.03 218);
- --invert-foreground: oklch(0.97 0.008 200);
- --focus: oklch(0.58 0.14 220);
- --focus-foreground: oklch(0.97 0.008 200);
- --border: oklch(1 0.01 210 / 10%);
- --input: oklch(1 0.01 210 / 15%);
- --ring: oklch(0.55 0.06 205);
- --chart-1: oklch(0.68 0.13 200);
- --chart-2: oklch(0.58 0.14 230);
- --chart-3: oklch(0.75 0.13 175);
- --chart-4: oklch(0.68 0.14 260);
- --chart-5: oklch(0.8 0.1 170);
- --sidebar: oklch(0.16 0.032 218);
- --sidebar-foreground: oklch(0.93 0.012 205);
- --sidebar-primary: oklch(0.72 0.12 195);
- --sidebar-primary-foreground: oklch(0.97 0.008 200);
- --sidebar-accent: oklch(0.25 0.035 212);
- --sidebar-accent-foreground: oklch(0.93 0.012 205);
- --sidebar-border: oklch(1 0.01 210 / 10%);
- --sidebar-ring: oklch(0.55 0.06 205);
-}
diff --git a/packages/themes/src/oxide.css b/packages/themes/src/oxide.css
deleted file mode 100644
index bc967a6..0000000
--- a/packages/themes/src/oxide.css
+++ /dev/null
@@ -1,106 +0,0 @@
-/*
- * Oxide — Editorial Craft
- * Serif-forward, squared-off, warm copper tones.
- * Feels like a well-made magazine or pottery studio.
- *
- * Fonts: Bitter (headings) + DM Sans (body)
- * Load via Google Fonts, next/font, or self-host:
- * https://fonts.googleapis.com/css2?family=Bitter:wght@400;500;600;700&family=DM+Sans:wght@400;500;600;700&display=swap
- *
- * Radius: 0.375rem (modest, angular)
- * Hue: ~35 (copper/terracotta)
- */
-
-:root {
- --font-sans: 'DM Sans', ui-sans-serif, system-ui, sans-serif;
- --font-heading: 'Bitter', ui-serif, Georgia, serif;
- --radius: 0.375rem;
- --background: oklch(0.978 0.016 58);
- --foreground: oklch(0.18 0.03 35);
- --card: oklch(0.972 0.018 56);
- --card-foreground: oklch(0.18 0.03 35);
- --popover: oklch(0.978 0.016 58);
- --popover-foreground: oklch(0.18 0.03 35);
- --primary: oklch(0.48 0.145 35);
- --primary-foreground: oklch(0.97 0.01 40);
- --secondary: oklch(0.94 0.022 55);
- --secondary-foreground: oklch(0.28 0.04 35);
- --muted: oklch(0.935 0.02 55);
- --muted-foreground: oklch(0.5 0.035 40);
- --accent: oklch(0.92 0.028 50);
- --accent-foreground: oklch(0.28 0.04 35);
- --destructive: oklch(0.52 0.2 22);
- --destructive-foreground: oklch(0.4 0.15 22);
- --success: oklch(0.6 0.14 155);
- --success-foreground: oklch(0.38 0.09 155);
- --info: oklch(0.55 0.11 250);
- --info-foreground: oklch(0.38 0.08 250);
- --warning: oklch(0.74 0.15 65);
- --warning-foreground: oklch(0.42 0.1 55);
- --invert: oklch(0.21 0.025 35);
- --invert-foreground: oklch(0.97 0.01 40);
- --focus: oklch(0.55 0.14 35);
- --focus-foreground: oklch(0.97 0.01 40);
- --border: oklch(0.88 0.022 55);
- --input: oklch(0.88 0.022 55);
- --ring: oklch(0.58 0.08 38);
- --chart-1: oklch(0.55 0.16 35);
- --chart-2: oklch(0.48 0.14 15);
- --chart-3: oklch(0.72 0.16 75);
- --chart-4: oklch(0.45 0.08 55);
- --chart-5: oklch(0.6 0.14 100);
- --sidebar: oklch(0.965 0.02 55);
- --sidebar-foreground: oklch(0.18 0.03 35);
- --sidebar-primary: oklch(0.48 0.145 35);
- --sidebar-primary-foreground: oklch(0.97 0.01 40);
- --sidebar-accent: oklch(0.92 0.028 50);
- --sidebar-accent-foreground: oklch(0.28 0.04 35);
- --sidebar-border: oklch(0.88 0.022 55);
- --sidebar-ring: oklch(0.58 0.08 38);
-}
-
-.dark {
- /* Warm cave — highest surface lightness, amber glow, visible terracotta tint */
- --background: oklch(0.17 0.025 35);
- --foreground: oklch(0.92 0.016 50);
- --card: oklch(0.22 0.03 35);
- --card-foreground: oklch(0.92 0.016 50);
- --popover: oklch(0.22 0.03 35);
- --popover-foreground: oklch(0.92 0.016 50);
- --primary: oklch(0.74 0.13 35);
- --primary-foreground: oklch(0.18 0.03 35);
- --secondary: oklch(0.28 0.03 35);
- --secondary-foreground: oklch(0.92 0.016 50);
- --muted: oklch(0.28 0.03 35);
- --muted-foreground: oklch(0.62 0.04 42);
- --accent: oklch(0.29 0.035 35);
- --accent-foreground: oklch(0.92 0.016 50);
- --destructive: oklch(0.65 0.17 22);
- --destructive-foreground: oklch(0.52 0.2 22);
- --success: oklch(0.66 0.14 155);
- --success-foreground: oklch(0.55 0.12 155);
- --info: oklch(0.6 0.11 250);
- --info-foreground: oklch(0.5 0.13 250);
- --warning: oklch(0.76 0.15 65);
- --warning-foreground: oklch(0.64 0.13 60);
- --invert: oklch(0.36 0.03 35);
- --invert-foreground: oklch(0.97 0.01 40);
- --focus: oklch(0.65 0.13 35);
- --focus-foreground: oklch(0.97 0.01 40);
- --border: oklch(1 0.012 50 / 11%);
- --input: oklch(1 0.012 50 / 16%);
- --ring: oklch(0.55 0.08 38);
- --chart-1: oklch(0.68 0.16 35);
- --chart-2: oklch(0.6 0.14 15);
- --chart-3: oklch(0.8 0.16 75);
- --chart-4: oklch(0.58 0.08 55);
- --chart-5: oklch(0.7 0.14 100);
- --sidebar: oklch(0.2 0.028 35);
- --sidebar-foreground: oklch(0.92 0.016 50);
- --sidebar-primary: oklch(0.74 0.13 35);
- --sidebar-primary-foreground: oklch(0.97 0.01 40);
- --sidebar-accent: oklch(0.29 0.035 35);
- --sidebar-accent-foreground: oklch(0.92 0.016 50);
- --sidebar-border: oklch(1 0.012 50 / 11%);
- --sidebar-ring: oklch(0.55 0.08 38);
-}
diff --git a/packages/themes/src/presets.css b/packages/themes/src/presets.css
index 0a72dec..f99ecce 100644
--- a/packages/themes/src/presets.css
+++ b/packages/themes/src/presets.css
@@ -13,765 +13,307 @@
* [data-theme="brand"].dark, .dark [data-theme="brand"] { ... }
*/
-/* Oxide */
-[data-theme="oxide"] {
- --font-sans: 'DM Sans', ui-sans-serif, system-ui, sans-serif;
- --font-heading: 'Bitter', ui-serif, Georgia, serif;
- --radius: 0.375rem;
- --background: oklch(0.978 0.016 58);
- --foreground: oklch(0.18 0.03 35);
- --card: oklch(0.972 0.018 56);
- --card-foreground: oklch(0.18 0.03 35);
- --popover: oklch(0.978 0.016 58);
- --popover-foreground: oklch(0.18 0.03 35);
- --primary: oklch(0.48 0.145 35);
- --primary-foreground: oklch(0.97 0.01 40);
- --secondary: oklch(0.94 0.022 55);
- --secondary-foreground: oklch(0.28 0.04 35);
- --muted: oklch(0.935 0.02 55);
- --muted-foreground: oklch(0.5 0.035 40);
- --accent: oklch(0.92 0.028 50);
- --accent-foreground: oklch(0.28 0.04 35);
- --destructive: oklch(0.52 0.2 22);
- --destructive-foreground: oklch(0.4 0.15 22);
- --success: oklch(0.6 0.14 155);
- --success-foreground: oklch(0.38 0.09 155);
- --info: oklch(0.55 0.11 250);
- --info-foreground: oklch(0.38 0.08 250);
- --warning: oklch(0.74 0.15 65);
- --warning-foreground: oklch(0.42 0.1 55);
- --invert: oklch(0.21 0.025 35);
- --invert-foreground: oklch(0.97 0.01 40);
- --focus: oklch(0.55 0.14 35);
- --focus-foreground: oklch(0.97 0.01 40);
- --border: oklch(0.88 0.022 55);
- --input: oklch(0.88 0.022 55);
- --ring: oklch(0.58 0.08 38);
- --chart-1: oklch(0.55 0.16 35);
- --chart-2: oklch(0.48 0.14 15);
- --chart-3: oklch(0.72 0.16 75);
- --chart-4: oklch(0.45 0.08 55);
- --chart-5: oklch(0.6 0.14 100);
- --sidebar: oklch(0.965 0.02 55);
- --sidebar-foreground: oklch(0.18 0.03 35);
- --sidebar-primary: oklch(0.48 0.145 35);
- --sidebar-primary-foreground: oklch(0.97 0.01 40);
- --sidebar-accent: oklch(0.92 0.028 50);
- --sidebar-accent-foreground: oklch(0.28 0.04 35);
- --sidebar-border: oklch(0.88 0.022 55);
- --sidebar-ring: oklch(0.58 0.08 38);
-}
-
-[data-theme="oxide"].dark,
-.dark [data-theme="oxide"] {
- /* Warm cave — highest surface lightness, amber glow, visible terracotta tint */
- --background: oklch(0.17 0.025 35);
- --foreground: oklch(0.92 0.016 50);
- --card: oklch(0.22 0.03 35);
- --card-foreground: oklch(0.92 0.016 50);
- --popover: oklch(0.22 0.03 35);
- --popover-foreground: oklch(0.92 0.016 50);
- --primary: oklch(0.74 0.13 35);
- --primary-foreground: oklch(0.18 0.03 35);
- --secondary: oklch(0.28 0.03 35);
- --secondary-foreground: oklch(0.92 0.016 50);
- --muted: oklch(0.28 0.03 35);
- --muted-foreground: oklch(0.62 0.04 42);
- --accent: oklch(0.29 0.035 35);
- --accent-foreground: oklch(0.92 0.016 50);
- --destructive: oklch(0.65 0.17 22);
- --destructive-foreground: oklch(0.52 0.2 22);
- --success: oklch(0.66 0.14 155);
- --success-foreground: oklch(0.55 0.12 155);
- --info: oklch(0.6 0.11 250);
- --info-foreground: oklch(0.5 0.13 250);
- --warning: oklch(0.76 0.15 65);
- --warning-foreground: oklch(0.64 0.13 60);
- --invert: oklch(0.36 0.03 35);
- --invert-foreground: oklch(0.97 0.01 40);
- --focus: oklch(0.65 0.13 35);
- --focus-foreground: oklch(0.97 0.01 40);
- --border: oklch(1 0.012 50 / 11%);
- --input: oklch(1 0.012 50 / 16%);
- --ring: oklch(0.55 0.08 38);
- --chart-1: oklch(0.68 0.16 35);
- --chart-2: oklch(0.6 0.14 15);
- --chart-3: oklch(0.8 0.16 75);
- --chart-4: oklch(0.58 0.08 55);
- --chart-5: oklch(0.7 0.14 100);
- --sidebar: oklch(0.2 0.028 35);
- --sidebar-foreground: oklch(0.92 0.016 50);
- --sidebar-primary: oklch(0.74 0.13 35);
- --sidebar-primary-foreground: oklch(0.97 0.01 40);
- --sidebar-accent: oklch(0.29 0.035 35);
- --sidebar-accent-foreground: oklch(0.92 0.016 50);
- --sidebar-border: oklch(1 0.012 50 / 11%);
- --sidebar-ring: oklch(0.55 0.08 38);
-}
-
-/* Ocean */
-[data-theme="ocean"] {
- --font-sans: 'Plus Jakarta Sans', ui-sans-serif, system-ui, sans-serif;
- --radius: 0.875rem;
- --background: oklch(0.982 0.015 210);
- --foreground: oklch(0.17 0.025 220);
- --card: oklch(0.976 0.018 208);
- --card-foreground: oklch(0.17 0.025 220);
- --popover: oklch(0.982 0.015 210);
- --popover-foreground: oklch(0.17 0.025 220);
- --primary: oklch(0.52 0.115 198);
- --primary-foreground: oklch(0.97 0.008 200);
- --secondary: oklch(0.94 0.02 210);
- --secondary-foreground: oklch(0.25 0.04 210);
- --muted: oklch(0.935 0.018 210);
- --muted-foreground: oklch(0.5 0.03 215);
- --accent: oklch(0.92 0.025 205);
- --accent-foreground: oklch(0.25 0.04 210);
- --destructive: oklch(0.55 0.2 15);
- --destructive-foreground: oklch(0.42 0.15 15);
- --success: oklch(0.62 0.14 170);
- --success-foreground: oklch(0.38 0.09 170);
- --info: oklch(0.52 0.13 230);
- --info-foreground: oklch(0.38 0.1 230);
- --warning: oklch(0.76 0.15 80);
- --warning-foreground: oklch(0.42 0.1 65);
- --invert: oklch(0.21 0.025 220);
- --invert-foreground: oklch(0.97 0.008 200);
- --focus: oklch(0.55 0.12 220);
- --focus-foreground: oklch(0.97 0.008 200);
- --border: oklch(0.89 0.02 210);
- --input: oklch(0.89 0.02 210);
- --ring: oklch(0.6 0.06 205);
- --chart-1: oklch(0.55 0.13 200);
- --chart-2: oklch(0.45 0.14 230);
- --chart-3: oklch(0.68 0.13 175);
- --chart-4: oklch(0.55 0.14 260);
- --chart-5: oklch(0.72 0.1 170);
- --sidebar: oklch(0.97 0.018 210);
- --sidebar-foreground: oklch(0.17 0.025 220);
- --sidebar-primary: oklch(0.52 0.115 198);
- --sidebar-primary-foreground: oklch(0.97 0.008 200);
- --sidebar-accent: oklch(0.92 0.025 205);
- --sidebar-accent-foreground: oklch(0.25 0.04 210);
- --sidebar-border: oklch(0.89 0.02 210);
- --sidebar-ring: oklch(0.6 0.06 205);
-}
-
-[data-theme="ocean"].dark,
-.dark [data-theme="ocean"] {
- /* Deep abyss — very dark, high chroma surfaces, bioluminescent teal accents */
- --background: oklch(0.13 0.03 218);
- --foreground: oklch(0.93 0.012 205);
- --card: oklch(0.18 0.035 218);
- --card-foreground: oklch(0.93 0.012 205);
- --popover: oklch(0.18 0.035 218);
- --popover-foreground: oklch(0.93 0.012 205);
- --primary: oklch(0.72 0.12 195);
- --primary-foreground: oklch(0.13 0.03 210);
- --secondary: oklch(0.24 0.03 218);
- --secondary-foreground: oklch(0.93 0.012 205);
- --muted: oklch(0.24 0.03 218);
- --muted-foreground: oklch(0.62 0.03 210);
- --accent: oklch(0.25 0.035 212);
- --accent-foreground: oklch(0.93 0.012 205);
- --destructive: oklch(0.68 0.17 15);
- --destructive-foreground: oklch(0.55 0.2 15);
- --success: oklch(0.68 0.14 170);
- --success-foreground: oklch(0.56 0.12 170);
- --info: oklch(0.6 0.13 230);
- --info-foreground: oklch(0.5 0.15 230);
- --warning: oklch(0.78 0.15 80);
- --warning-foreground: oklch(0.66 0.13 72);
- --invert: oklch(0.34 0.03 218);
- --invert-foreground: oklch(0.97 0.008 200);
- --focus: oklch(0.58 0.14 220);
- --focus-foreground: oklch(0.97 0.008 200);
- --border: oklch(1 0.01 210 / 10%);
- --input: oklch(1 0.01 210 / 15%);
- --ring: oklch(0.55 0.06 205);
- --chart-1: oklch(0.68 0.13 200);
- --chart-2: oklch(0.58 0.14 230);
- --chart-3: oklch(0.75 0.13 175);
- --chart-4: oklch(0.68 0.14 260);
- --chart-5: oklch(0.8 0.1 170);
- --sidebar: oklch(0.16 0.032 218);
- --sidebar-foreground: oklch(0.93 0.012 205);
- --sidebar-primary: oklch(0.72 0.12 195);
- --sidebar-primary-foreground: oklch(0.97 0.008 200);
- --sidebar-accent: oklch(0.25 0.035 212);
- --sidebar-accent-foreground: oklch(0.93 0.012 205);
- --sidebar-border: oklch(1 0.01 210 / 10%);
- --sidebar-ring: oklch(0.55 0.06 205);
-}
-
-/* Violet */
-[data-theme="violet"] {
- --font-sans: 'Outfit', ui-sans-serif, system-ui, sans-serif;
- --radius: 1rem;
- --background: oklch(0.98 0.016 292);
- --foreground: oklch(0.18 0.03 285);
- --card: oklch(0.974 0.02 290);
- --card-foreground: oklch(0.18 0.03 285);
- --popover: oklch(0.98 0.016 292);
- --popover-foreground: oklch(0.18 0.03 285);
- --primary: oklch(0.52 0.2 285);
- --primary-foreground: oklch(0.97 0.01 285);
- --secondary: oklch(0.94 0.025 290);
- --secondary-foreground: oklch(0.28 0.05 285);
- --muted: oklch(0.935 0.022 290);
- --muted-foreground: oklch(0.5 0.035 288);
- --accent: oklch(0.92 0.03 288);
- --accent-foreground: oklch(0.28 0.05 285);
- --destructive: oklch(0.55 0.2 355);
- --destructive-foreground: oklch(0.4 0.15 355);
- --success: oklch(0.62 0.14 155);
- --success-foreground: oklch(0.38 0.09 155);
- --info: oklch(0.52 0.18 275);
- --info-foreground: oklch(0.38 0.14 275);
- --warning: oklch(0.76 0.15 70);
- --warning-foreground: oklch(0.42 0.1 60);
- --invert: oklch(0.22 0.03 285);
- --invert-foreground: oklch(0.97 0.01 285);
- --focus: oklch(0.55 0.2 285);
- --focus-foreground: oklch(0.97 0.01 285);
- --border: oklch(0.89 0.022 290);
- --input: oklch(0.89 0.022 290);
- --ring: oklch(0.6 0.1 285);
- --chart-1: oklch(0.55 0.2 285);
- --chart-2: oklch(0.58 0.18 320);
- --chart-3: oklch(0.48 0.16 260);
- --chart-4: oklch(0.68 0.16 350);
- --chart-5: oklch(0.65 0.12 240);
- --sidebar: oklch(0.968 0.02 290);
- --sidebar-foreground: oklch(0.18 0.03 285);
- --sidebar-primary: oklch(0.52 0.2 285);
- --sidebar-primary-foreground: oklch(0.97 0.01 285);
- --sidebar-accent: oklch(0.92 0.03 288);
- --sidebar-accent-foreground: oklch(0.28 0.05 285);
- --sidebar-border: oklch(0.89 0.022 290);
- --sidebar-ring: oklch(0.6 0.1 285);
-}
-
-[data-theme="violet"].dark,
-.dark [data-theme="violet"] {
- /* Midnight purple — deep, high chroma, vivid neon accents */
- --background: oklch(0.12 0.035 285);
- --foreground: oklch(0.93 0.014 290);
- --card: oklch(0.17 0.04 285);
- --card-foreground: oklch(0.93 0.014 290);
- --popover: oklch(0.17 0.04 285);
- --popover-foreground: oklch(0.93 0.014 290);
- --primary: oklch(0.72 0.18 285);
- --primary-foreground: oklch(0.12 0.04 285);
- --secondary: oklch(0.24 0.035 285);
- --secondary-foreground: oklch(0.93 0.014 290);
- --muted: oklch(0.24 0.035 285);
- --muted-foreground: oklch(0.62 0.04 288);
- --accent: oklch(0.25 0.04 285);
- --accent-foreground: oklch(0.93 0.014 290);
- --destructive: oklch(0.68 0.17 355);
- --destructive-foreground: oklch(0.55 0.2 355);
- --success: oklch(0.68 0.14 155);
- --success-foreground: oklch(0.56 0.12 155);
- --info: oklch(0.6 0.18 275);
- --info-foreground: oklch(0.5 0.2 275);
- --warning: oklch(0.78 0.15 70);
- --warning-foreground: oklch(0.66 0.13 65);
- --invert: oklch(0.34 0.04 285);
- --invert-foreground: oklch(0.97 0.01 285);
- --focus: oklch(0.6 0.2 285);
- --focus-foreground: oklch(0.97 0.01 285);
- --border: oklch(1 0.012 290 / 10%);
- --input: oklch(1 0.012 290 / 15%);
- --ring: oklch(0.55 0.1 285);
- --chart-1: oklch(0.68 0.2 285);
- --chart-2: oklch(0.7 0.18 320);
- --chart-3: oklch(0.6 0.18 260);
- --chart-4: oklch(0.76 0.16 350);
- --chart-5: oklch(0.75 0.12 240);
- --sidebar: oklch(0.15 0.038 285);
- --sidebar-foreground: oklch(0.93 0.014 290);
- --sidebar-primary: oklch(0.72 0.18 285);
- --sidebar-primary-foreground: oklch(0.97 0.01 285);
- --sidebar-accent: oklch(0.25 0.04 285);
- --sidebar-accent-foreground: oklch(0.93 0.014 290);
- --sidebar-border: oklch(1 0.012 290 / 10%);
- --sidebar-ring: oklch(0.55 0.1 285);
-}
-
-/* Forest */
-[data-theme="forest"] {
+/* Press */
+[data-theme="press"] {
--font-sans: 'Source Sans 3', ui-sans-serif, system-ui, sans-serif;
- --font-heading: 'Fraunces', ui-serif, Georgia, serif;
+ --font-heading: 'Source Serif 4', ui-serif, Georgia, serif;
+ --font-mono: 'Geist Mono', ui-monospace, SFMono-Regular, monospace;
--radius: 0.5rem;
- --background: oklch(0.98 0.015 152);
- --foreground: oklch(0.17 0.025 160);
- --card: oklch(0.974 0.018 150);
- --card-foreground: oklch(0.17 0.025 160);
- --popover: oklch(0.98 0.015 152);
- --popover-foreground: oklch(0.17 0.025 160);
- --primary: oklch(0.48 0.14 160);
- --primary-foreground: oklch(0.97 0.008 155);
- --secondary: oklch(0.94 0.02 155);
- --secondary-foreground: oklch(0.25 0.04 160);
- --muted: oklch(0.935 0.018 155);
- --muted-foreground: oklch(0.5 0.028 158);
- --accent: oklch(0.92 0.025 150);
- --accent-foreground: oklch(0.25 0.04 160);
- --destructive: oklch(0.52 0.18 20);
- --destructive-foreground: oklch(0.4 0.14 20);
- --success: oklch(0.55 0.16 145);
- --success-foreground: oklch(0.37 0.1 145);
- --info: oklch(0.55 0.11 240);
- --info-foreground: oklch(0.38 0.08 240);
- --warning: oklch(0.74 0.15 90);
- --warning-foreground: oklch(0.42 0.1 80);
- --invert: oklch(0.21 0.025 160);
- --invert-foreground: oklch(0.97 0.008 155);
- --focus: oklch(0.52 0.14 165);
- --focus-foreground: oklch(0.97 0.008 155);
- --border: oklch(0.89 0.018 155);
- --input: oklch(0.89 0.018 155);
- --ring: oklch(0.58 0.06 158);
- --chart-1: oklch(0.52 0.15 160);
- --chart-2: oklch(0.45 0.12 130);
- --chart-3: oklch(0.68 0.14 120);
- --chart-4: oklch(0.55 0.1 190);
- --chart-5: oklch(0.62 0.13 90);
- --sidebar: oklch(0.968 0.017 152);
- --sidebar-foreground: oklch(0.17 0.025 160);
- --sidebar-primary: oklch(0.48 0.14 160);
- --sidebar-primary-foreground: oklch(0.97 0.008 155);
- --sidebar-accent: oklch(0.92 0.025 150);
- --sidebar-accent-foreground: oklch(0.25 0.04 160);
- --sidebar-border: oklch(0.89 0.018 155);
- --sidebar-ring: oklch(0.58 0.06 158);
+ --background: oklch(97.5% 0.012 85);
+ --foreground: oklch(16% 0.015 60);
+ --card: oklch(99.1% 0.008 70);
+ --card-foreground: oklch(16% 0.015 60);
+ --popover: oklch(99.1% 0.008 70);
+ --popover-foreground: oklch(16% 0.015 60);
+ --primary: oklch(45% 0.12 25);
+ --primary-foreground: oklch(98% 0.015 25);
+ --secondary: oklch(95.5% 0.01 70);
+ --secondary-foreground: oklch(16% 0.015 60);
+ --muted: oklch(95.5% 0.01 70);
+ --muted-foreground: oklch(52.9% 0.01 70);
+ --accent: oklch(94.7% 0.018 25);
+ --accent-foreground: oklch(16% 0.015 60);
+ --destructive: oklch(54.8% 0.16 27);
+ --destructive-foreground: oklch(52.7% 0.144 27);
+ --success: oklch(51.6% 0.11 152);
+ --success-foreground: oklch(50.1% 0.099 152);
+ --info: oklch(52.8% 0.12 250);
+ --info-foreground: oklch(51.1% 0.108 250);
+ --warning: oklch(53.4% 0.13 75);
+ --warning-foreground: oklch(51.7% 0.117 75);
+ --invert: oklch(21% 0.01 70);
+ --invert-foreground: oklch(98% 0.004 70);
+ --focus: oklch(55% 0.14 25);
+ --focus-foreground: oklch(98% 0.015 25);
+ --border: oklch(64.7% 0.01 70);
+ --input: oklch(64.7% 0.01 70);
+ --ring: oklch(65.3% 0.06 25);
+ --chart-1: oklch(55% 0.12 25);
+ --chart-2: oklch(56% 0.12 87);
+ --chart-3: oklch(57% 0.12 153);
+ --chart-4: oklch(58% 0.12 221);
+ --chart-5: oklch(59% 0.12 309);
+ --sidebar: oklch(95.9% 0.01 70);
+ --sidebar-foreground: oklch(16% 0.015 60);
+ --sidebar-primary: oklch(45% 0.12 25);
+ --sidebar-primary-foreground: oklch(98% 0.015 25);
+ --sidebar-accent: oklch(93.1% 0.018 25);
+ --sidebar-accent-foreground: oklch(16% 0.015 60);
+ --sidebar-border: oklch(63.5% 0.01 70);
+ --sidebar-ring: oklch(64.1% 0.06 25);
}
-[data-theme="forest"].dark,
-.dark [data-theme="forest"] {
- /* Dense canopy — medium-dark, visible green tint, moonlit feel */
- --background: oklch(0.14 0.025 162);
- --foreground: oklch(0.93 0.012 152);
- --card: oklch(0.19 0.03 162);
- --card-foreground: oklch(0.93 0.012 152);
- --popover: oklch(0.19 0.03 162);
- --popover-foreground: oklch(0.93 0.012 152);
- --primary: oklch(0.7 0.13 158);
- --primary-foreground: oklch(0.14 0.03 160);
- --secondary: oklch(0.25 0.025 162);
- --secondary-foreground: oklch(0.93 0.012 152);
- --muted: oklch(0.25 0.025 162);
- --muted-foreground: oklch(0.62 0.028 157);
- --accent: oklch(0.26 0.03 158);
- --accent-foreground: oklch(0.93 0.012 152);
- --destructive: oklch(0.65 0.16 20);
- --destructive-foreground: oklch(0.52 0.18 20);
- --success: oklch(0.62 0.15 145);
- --success-foreground: oklch(0.52 0.13 145);
- --info: oklch(0.62 0.11 240);
- --info-foreground: oklch(0.5 0.13 240);
- --warning: oklch(0.78 0.15 90);
- --warning-foreground: oklch(0.66 0.13 85);
- --invert: oklch(0.34 0.028 160);
- --invert-foreground: oklch(0.97 0.008 155);
- --focus: oklch(0.58 0.14 165);
- --focus-foreground: oklch(0.97 0.008 155);
- --border: oklch(1 0.01 155 / 10%);
- --input: oklch(1 0.01 155 / 15%);
- --ring: oklch(0.52 0.06 158);
- --chart-1: oklch(0.65 0.15 160);
- --chart-2: oklch(0.58 0.12 130);
- --chart-3: oklch(0.76 0.14 120);
- --chart-4: oklch(0.68 0.1 190);
- --chart-5: oklch(0.72 0.13 90);
- --sidebar: oklch(0.17 0.028 162);
- --sidebar-foreground: oklch(0.93 0.012 152);
- --sidebar-primary: oklch(0.7 0.13 158);
- --sidebar-primary-foreground: oklch(0.97 0.008 155);
- --sidebar-accent: oklch(0.26 0.03 158);
- --sidebar-accent-foreground: oklch(0.93 0.012 152);
- --sidebar-border: oklch(1 0.01 155 / 10%);
- --sidebar-ring: oklch(0.52 0.06 158);
-}
-
-/* Rose */
-[data-theme="rose"] {
- --font-sans: 'Nunito', ui-sans-serif, system-ui, sans-serif;
- --radius: 1.25rem;
- --background: oklch(0.98 0.016 352);
- --foreground: oklch(0.18 0.025 355);
- --card: oklch(0.974 0.02 350);
- --card-foreground: oklch(0.18 0.025 355);
- --popover: oklch(0.98 0.016 352);
- --popover-foreground: oklch(0.18 0.025 355);
- --primary: oklch(0.6 0.18 350);
- --primary-foreground: oklch(0.97 0.008 350);
- --secondary: oklch(0.94 0.022 355);
- --secondary-foreground: oklch(0.28 0.04 350);
- --muted: oklch(0.935 0.02 355);
- --muted-foreground: oklch(0.5 0.028 352);
- --accent: oklch(0.92 0.026 348);
- --accent-foreground: oklch(0.28 0.04 350);
- --destructive: oklch(0.52 0.2 15);
- --destructive-foreground: oklch(0.4 0.15 15);
- --success: oklch(0.62 0.14 155);
- --success-foreground: oklch(0.38 0.09 155);
- --info: oklch(0.55 0.15 280);
- --info-foreground: oklch(0.38 0.12 280);
- --warning: oklch(0.76 0.15 75);
- --warning-foreground: oklch(0.42 0.1 65);
- --invert: oklch(0.21 0.025 355);
- --invert-foreground: oklch(0.97 0.008 350);
- --focus: oklch(0.58 0.18 350);
- --focus-foreground: oklch(0.97 0.008 350);
- --border: oklch(0.89 0.02 355);
- --input: oklch(0.89 0.02 355);
- --ring: oklch(0.62 0.08 350);
- --chart-1: oklch(0.6 0.18 350);
- --chart-2: oklch(0.55 0.16 20);
- --chart-3: oklch(0.65 0.16 330);
- --chart-4: oklch(0.72 0.14 50);
- --chart-5: oklch(0.58 0.14 300);
- --sidebar: oklch(0.968 0.018 355);
- --sidebar-foreground: oklch(0.18 0.025 355);
- --sidebar-primary: oklch(0.6 0.18 350);
- --sidebar-primary-foreground: oklch(0.97 0.008 350);
- --sidebar-accent: oklch(0.92 0.026 348);
- --sidebar-accent-foreground: oklch(0.28 0.04 350);
- --sidebar-border: oklch(0.89 0.02 355);
- --sidebar-ring: oklch(0.62 0.08 350);
-}
-
-[data-theme="rose"].dark,
-.dark [data-theme="rose"] {
- /* Warm dusk — warmest dark mode, pink-warm, cozy and inviting */
- --background: oklch(0.16 0.022 358);
- --foreground: oklch(0.93 0.012 350);
- --card: oklch(0.21 0.026 358);
- --card-foreground: oklch(0.93 0.012 350);
- --popover: oklch(0.21 0.026 358);
- --popover-foreground: oklch(0.93 0.012 350);
- --primary: oklch(0.74 0.16 350);
- --primary-foreground: oklch(0.16 0.03 355);
- --secondary: oklch(0.27 0.025 358);
- --secondary-foreground: oklch(0.93 0.012 350);
- --muted: oklch(0.27 0.025 358);
- --muted-foreground: oklch(0.62 0.03 352);
- --accent: oklch(0.28 0.03 355);
- --accent-foreground: oklch(0.93 0.012 350);
- --destructive: oklch(0.65 0.17 15);
- --destructive-foreground: oklch(0.52 0.2 15);
- --success: oklch(0.68 0.14 155);
- --success-foreground: oklch(0.56 0.12 155);
- --info: oklch(0.62 0.15 280);
- --info-foreground: oklch(0.5 0.17 280);
- --warning: oklch(0.78 0.15 75);
- --warning-foreground: oklch(0.66 0.13 68);
- --invert: oklch(0.36 0.028 355);
- --invert-foreground: oklch(0.97 0.008 350);
- --focus: oklch(0.65 0.18 350);
- --focus-foreground: oklch(0.97 0.008 350);
- --border: oklch(1 0.01 350 / 10%);
- --input: oklch(1 0.01 350 / 15%);
- --ring: oklch(0.55 0.08 350);
- --chart-1: oklch(0.7 0.18 350);
- --chart-2: oklch(0.65 0.16 20);
- --chart-3: oklch(0.75 0.16 330);
- --chart-4: oklch(0.8 0.14 50);
- --chart-5: oklch(0.68 0.14 300);
- --sidebar: oklch(0.19 0.024 358);
- --sidebar-foreground: oklch(0.93 0.012 350);
- --sidebar-primary: oklch(0.74 0.16 350);
- --sidebar-primary-foreground: oklch(0.97 0.008 350);
- --sidebar-accent: oklch(0.28 0.03 355);
- --sidebar-accent-foreground: oklch(0.93 0.012 350);
- --sidebar-border: oklch(1 0.01 350 / 10%);
- --sidebar-ring: oklch(0.55 0.08 350);
-}
-
-/* Amber */
-[data-theme="amber"] {
- --font-sans: 'Sora', ui-sans-serif, system-ui, sans-serif;
- --radius: 0.25rem;
- --background: oklch(0.98 0.015 82);
- --foreground: oklch(0.18 0.025 70);
- --card: oklch(0.974 0.018 80);
- --card-foreground: oklch(0.18 0.025 70);
- --popover: oklch(0.98 0.015 82);
- --popover-foreground: oklch(0.18 0.025 70);
- --primary: oklch(0.62 0.17 75);
- --primary-foreground: oklch(0.2 0.035 70);
- --secondary: oklch(0.94 0.022 80);
- --secondary-foreground: oklch(0.28 0.04 70);
- --muted: oklch(0.935 0.02 80);
- --muted-foreground: oklch(0.5 0.028 75);
- --accent: oklch(0.92 0.026 78);
- --accent-foreground: oklch(0.28 0.04 70);
- --destructive: oklch(0.52 0.2 10);
- --destructive-foreground: oklch(0.4 0.15 10);
- --success: oklch(0.62 0.14 160);
- --success-foreground: oklch(0.38 0.09 160);
- --info: oklch(0.52 0.13 260);
- --info-foreground: oklch(0.38 0.1 260);
- --warning: oklch(0.72 0.17 70);
- --warning-foreground: oklch(0.4 0.12 60);
- --invert: oklch(0.21 0.025 70);
- --invert-foreground: oklch(0.97 0.008 80);
- --focus: oklch(0.6 0.17 75);
- --focus-foreground: oklch(0.2 0.035 70);
- --border: oklch(0.89 0.02 80);
- --input: oklch(0.89 0.02 80);
- --ring: oklch(0.62 0.08 75);
- --chart-1: oklch(0.65 0.18 75);
- --chart-2: oklch(0.55 0.17 45);
- --chart-3: oklch(0.72 0.15 100);
- --chart-4: oklch(0.5 0.14 20);
- --chart-5: oklch(0.58 0.11 130);
- --sidebar: oklch(0.968 0.018 80);
- --sidebar-foreground: oklch(0.18 0.025 70);
- --sidebar-primary: oklch(0.62 0.17 75);
- --sidebar-primary-foreground: oklch(0.2 0.035 70);
- --sidebar-accent: oklch(0.92 0.026 78);
- --sidebar-accent-foreground: oklch(0.28 0.04 70);
- --sidebar-border: oklch(0.89 0.02 80);
- --sidebar-ring: oklch(0.62 0.08 75);
-}
-
-[data-theme="amber"].dark,
-.dark [data-theme="amber"] {
- /* Dark gold — deep, warm sepia, golden highlights pop with high contrast */
- --background: oklch(0.13 0.02 72);
- --foreground: oklch(0.93 0.012 80);
- --card: oklch(0.18 0.025 72);
- --card-foreground: oklch(0.93 0.012 80);
- --popover: oklch(0.18 0.025 72);
- --popover-foreground: oklch(0.93 0.012 80);
- --primary: oklch(0.78 0.16 75);
- --primary-foreground: oklch(0.13 0.035 70);
- --secondary: oklch(0.24 0.022 72);
- --secondary-foreground: oklch(0.93 0.012 80);
- --muted: oklch(0.24 0.022 72);
- --muted-foreground: oklch(0.62 0.028 75);
- --accent: oklch(0.25 0.026 72);
- --accent-foreground: oklch(0.93 0.012 80);
- --destructive: oklch(0.65 0.17 10);
- --destructive-foreground: oklch(0.52 0.2 10);
- --success: oklch(0.68 0.14 160);
- --success-foreground: oklch(0.56 0.12 160);
- --info: oklch(0.6 0.13 260);
- --info-foreground: oklch(0.5 0.15 260);
- --warning: oklch(0.78 0.17 70);
- --warning-foreground: oklch(0.65 0.15 65);
- --invert: oklch(0.34 0.025 70);
- --invert-foreground: oklch(0.97 0.008 80);
- --focus: oklch(0.72 0.17 75);
- --focus-foreground: oklch(0.2 0.035 70);
- --border: oklch(1 0.01 80 / 11%);
- --input: oklch(1 0.01 80 / 16%);
- --ring: oklch(0.55 0.08 75);
- --chart-1: oklch(0.75 0.18 75);
- --chart-2: oklch(0.66 0.17 45);
- --chart-3: oklch(0.8 0.15 100);
- --chart-4: oklch(0.62 0.14 20);
- --chart-5: oklch(0.7 0.11 130);
- --sidebar: oklch(0.16 0.023 72);
- --sidebar-foreground: oklch(0.93 0.012 80);
- --sidebar-primary: oklch(0.78 0.16 75);
- --sidebar-primary-foreground: oklch(0.2 0.035 70);
- --sidebar-accent: oklch(0.25 0.026 72);
- --sidebar-accent-foreground: oklch(0.93 0.012 80);
- --sidebar-border: oklch(1 0.01 80 / 11%);
- --sidebar-ring: oklch(0.55 0.08 75);
+[data-theme="press"].dark,
+.dark [data-theme="press"] {
+ --font-sans: 'Source Sans 3', ui-sans-serif, system-ui, sans-serif;
+ --font-heading: 'Source Serif 4', ui-serif, Georgia, serif;
+ --font-mono: 'Geist Mono', ui-monospace, SFMono-Regular, monospace;
+ --radius: 0.5rem;
+ --background: oklch(15% 0.012 60);
+ --foreground: oklch(93% 0.012 80);
+ --card: oklch(19.5% 0.008 70);
+ --card-foreground: oklch(93% 0.012 80);
+ --popover: oklch(19.5% 0.008 70);
+ --popover-foreground: oklch(93% 0.012 80);
+ --primary: oklch(72% 0.11 30);
+ --primary-foreground: oklch(16% 0.015 25);
+ --secondary: oklch(24% 0.01 70);
+ --secondary-foreground: oklch(93% 0.012 80);
+ --muted: oklch(24% 0.01 70);
+ --muted-foreground: oklch(62.7% 0.01 70);
+ --accent: oklch(28.5% 0.018 25);
+ --accent-foreground: oklch(93% 0.012 80);
+ --destructive: oklch(64.6% 0.16 27);
+ --destructive-foreground: oklch(64.6% 0.144 27);
+ --success: oklch(61.5% 0.11 152);
+ --success-foreground: oklch(61.5% 0.099 152);
+ --info: oklch(62.6% 0.12 250);
+ --info-foreground: oklch(62.6% 0.108 250);
+ --warning: oklch(63.2% 0.13 75);
+ --warning-foreground: oklch(63.2% 0.117 75);
+ --invert: oklch(37% 0.01 70);
+ --invert-foreground: oklch(98% 0.004 70);
+ --focus: oklch(62% 0.14 25);
+ --focus-foreground: oklch(16% 0.015 25);
+ --border: oklch(48.3% 0.01 70);
+ --input: oklch(48.3% 0.01 70);
+ --ring: oklch(49% 0.06 25);
+ --chart-1: oklch(70% 0.11 25);
+ --chart-2: oklch(68.5% 0.11 87);
+ --chart-3: oklch(67% 0.11 153);
+ --chart-4: oklch(65.5% 0.11 221);
+ --chart-5: oklch(64% 0.11 309);
+ --sidebar: oklch(17.7% 0.01 70);
+ --sidebar-foreground: oklch(93% 0.012 80);
+ --sidebar-primary: oklch(72% 0.11 30);
+ --sidebar-primary-foreground: oklch(16% 0.015 25);
+ --sidebar-accent: oklch(22.2% 0.018 25);
+ --sidebar-accent-foreground: oklch(93% 0.012 80);
+ --sidebar-border: oklch(49.3% 0.01 70);
+ --sidebar-ring: oklch(49.9% 0.06 25);
}
-/* Slate */
-[data-theme="slate"] {
+/* Console */
+[data-theme="console"] {
--font-sans: 'Geist', ui-sans-serif, system-ui, sans-serif;
- --radius: 0.5rem;
- --background: oklch(0.982 0.012 250);
- --foreground: oklch(0.17 0.018 250);
- --card: oklch(0.976 0.015 248);
- --card-foreground: oklch(0.17 0.018 250);
- --popover: oklch(0.982 0.012 250);
- --popover-foreground: oklch(0.17 0.018 250);
- --primary: oklch(0.48 0.1 250);
- --primary-foreground: oklch(0.97 0.006 250);
- --secondary: oklch(0.945 0.016 250);
- --secondary-foreground: oklch(0.25 0.03 250);
- --muted: oklch(0.94 0.014 250);
- --muted-foreground: oklch(0.5 0.02 250);
- --accent: oklch(0.93 0.018 248);
- --accent-foreground: oklch(0.25 0.03 250);
- --destructive: oklch(0.52 0.18 20);
- --destructive-foreground: oklch(0.4 0.14 20);
- --success: oklch(0.6 0.14 165);
- --success-foreground: oklch(0.38 0.09 165);
- --info: oklch(0.52 0.11 250);
- --info-foreground: oklch(0.38 0.08 250);
- --warning: oklch(0.76 0.14 75);
- --warning-foreground: oklch(0.42 0.1 60);
- --invert: oklch(0.21 0.018 250);
- --invert-foreground: oklch(0.97 0.006 250);
- --focus: oklch(0.52 0.12 255);
- --focus-foreground: oklch(0.97 0.006 250);
- --border: oklch(0.9 0.014 250);
- --input: oklch(0.9 0.014 250);
- --ring: oklch(0.58 0.05 250);
- --chart-1: oklch(0.52 0.12 250);
- --chart-2: oklch(0.58 0.13 220);
- --chart-3: oklch(0.5 0.14 290);
- --chart-4: oklch(0.62 0.1 200);
- --chart-5: oklch(0.6 0.11 330);
- --sidebar: oklch(0.97 0.014 250);
- --sidebar-foreground: oklch(0.17 0.018 250);
- --sidebar-primary: oklch(0.48 0.1 250);
- --sidebar-primary-foreground: oklch(0.97 0.006 250);
- --sidebar-accent: oklch(0.93 0.018 248);
- --sidebar-accent-foreground: oklch(0.25 0.03 250);
- --sidebar-border: oklch(0.9 0.014 250);
- --sidebar-ring: oklch(0.58 0.05 250);
+ --font-heading: 'Geist Mono', ui-monospace, SFMono-Regular, monospace;
+ --font-mono: 'Geist Mono', ui-monospace, SFMono-Regular, monospace;
+ --radius: 0.125rem;
+ --leading-base: 1.4;
+ --text-base-size: 0.9375rem;
+ --background: oklch(96.5% 0.005 240);
+ --foreground: oklch(20% 0.01 245);
+ --card: oklch(98.1% 0.0048 243);
+ --card-foreground: oklch(20% 0.01 245);
+ --popover: oklch(98.1% 0.0048 243);
+ --popover-foreground: oklch(20% 0.01 245);
+ --primary: oklch(52% 0.11 235);
+ --primary-foreground: oklch(98% 0.015 235);
+ --secondary: oklch(94.5% 0.006 243);
+ --secondary-foreground: oklch(20% 0.01 245);
+ --muted: oklch(94.5% 0.006 243);
+ --muted-foreground: oklch(52.2% 0.006 243);
+ --accent: oklch(93.7% 0.018 235);
+ --accent-foreground: oklch(20% 0.01 245);
+ --destructive: oklch(54.1% 0.16 27);
+ --destructive-foreground: oklch(52% 0.144 27);
+ --success: oklch(51% 0.11 152);
+ --success-foreground: oklch(49.5% 0.099 152);
+ --info: oklch(52.2% 0.12 250);
+ --info-foreground: oklch(50.5% 0.108 250);
+ --warning: oklch(52.8% 0.13 75);
+ --warning-foreground: oklch(51% 0.117 75);
+ --invert: oklch(21% 0.006 243);
+ --invert-foreground: oklch(98% 0.0024 243);
+ --focus: oklch(55% 0.14 235);
+ --focus-foreground: oklch(99.7% 0.02 235);
+ --border: oklch(63.9% 0.006 243);
+ --input: oklch(63.9% 0.006 243);
+ --ring: oklch(63.6% 0.06 235);
+ --chart-1: oklch(55% 0.12 235);
+ --chart-2: oklch(56% 0.12 297);
+ --chart-3: oklch(57% 0.12 3);
+ --chart-4: oklch(58% 0.12 71);
+ --chart-5: oklch(59% 0.12 159);
+ --sidebar: oklch(94.9% 0.006 243);
+ --sidebar-foreground: oklch(20% 0.01 245);
+ --sidebar-primary: oklch(52% 0.11 235);
+ --sidebar-primary-foreground: oklch(98% 0.015 235);
+ --sidebar-accent: oklch(92.1% 0.018 235);
+ --sidebar-accent-foreground: oklch(20% 0.01 245);
+ --sidebar-border: oklch(62.7% 0.006 243);
+ --sidebar-ring: oklch(62.4% 0.06 235);
}
-[data-theme="slate"].dark,
-.dark [data-theme="slate"] {
- /* Ice steel — very dark, desaturated, clinical precision, icy blue accents */
- --background: oklch(0.11 0.018 252);
- --foreground: oklch(0.93 0.008 250);
- --card: oklch(0.16 0.02 252);
- --card-foreground: oklch(0.93 0.008 250);
- --popover: oklch(0.16 0.02 252);
- --popover-foreground: oklch(0.93 0.008 250);
- --primary: oklch(0.72 0.1 248);
- --primary-foreground: oklch(0.11 0.025 250);
- --secondary: oklch(0.22 0.02 252);
- --secondary-foreground: oklch(0.93 0.008 250);
- --muted: oklch(0.22 0.02 252);
- --muted-foreground: oklch(0.62 0.022 250);
- --accent: oklch(0.23 0.022 250);
- --accent-foreground: oklch(0.93 0.008 250);
- --destructive: oklch(0.65 0.16 20);
- --destructive-foreground: oklch(0.52 0.18 20);
- --success: oklch(0.66 0.14 165);
- --success-foreground: oklch(0.54 0.12 165);
- --info: oklch(0.6 0.11 250);
- --info-foreground: oklch(0.5 0.13 250);
- --warning: oklch(0.78 0.14 75);
- --warning-foreground: oklch(0.66 0.12 68);
- --invert: oklch(0.32 0.02 250);
- --invert-foreground: oklch(0.97 0.006 250);
- --focus: oklch(0.58 0.14 255);
- --focus-foreground: oklch(0.97 0.006 250);
- --border: oklch(1 0.008 250 / 9%);
- --input: oklch(1 0.008 250 / 14%);
- --ring: oklch(0.52 0.05 250);
- --chart-1: oklch(0.68 0.12 250);
- --chart-2: oklch(0.7 0.13 220);
- --chart-3: oklch(0.65 0.14 290);
- --chart-4: oklch(0.72 0.1 200);
- --chart-5: oklch(0.7 0.11 330);
- --sidebar: oklch(0.14 0.019 252);
- --sidebar-foreground: oklch(0.93 0.008 250);
- --sidebar-primary: oklch(0.72 0.1 248);
- --sidebar-primary-foreground: oklch(0.97 0.006 250);
- --sidebar-accent: oklch(0.23 0.022 250);
- --sidebar-accent-foreground: oklch(0.93 0.008 250);
- --sidebar-border: oklch(1 0.008 250 / 9%);
- --sidebar-ring: oklch(0.52 0.05 250);
+[data-theme="console"].dark,
+.dark [data-theme="console"] {
+ --font-sans: 'Geist', ui-sans-serif, system-ui, sans-serif;
+ --font-heading: 'Geist Mono', ui-monospace, SFMono-Regular, monospace;
+ --font-mono: 'Geist Mono', ui-monospace, SFMono-Regular, monospace;
+ --radius: 0.125rem;
+ --leading-base: 1.4;
+ --text-base-size: 0.9375rem;
+ --background: oklch(16% 0.005 245);
+ --foreground: oklch(91% 0.008 240);
+ --card: oklch(20.5% 0.0048 243);
+ --card-foreground: oklch(91% 0.008 240);
+ --popover: oklch(20.5% 0.0048 243);
+ --popover-foreground: oklch(91% 0.008 240);
+ --primary: oklch(70% 0.1 230);
+ --primary-foreground: oklch(16% 0.015 235);
+ --secondary: oklch(25% 0.006 243);
+ --secondary-foreground: oklch(91% 0.008 240);
+ --muted: oklch(25% 0.006 243);
+ --muted-foreground: oklch(63.4% 0.006 243);
+ --accent: oklch(29.5% 0.018 235);
+ --accent-foreground: oklch(91% 0.008 240);
+ --destructive: oklch(65.3% 0.16 27);
+ --destructive-foreground: oklch(65.3% 0.144 27);
+ --success: oklch(62.2% 0.11 152);
+ --success-foreground: oklch(62.2% 0.099 152);
+ --info: oklch(63.3% 0.12 250);
+ --info-foreground: oklch(63.3% 0.108 250);
+ --warning: oklch(64% 0.13 75);
+ --warning-foreground: oklch(64% 0.117 75);
+ --invert: oklch(37% 0.006 243);
+ --invert-foreground: oklch(98% 0.0024 243);
+ --focus: oklch(62% 0.14 235);
+ --focus-foreground: oklch(16% 0.02 235);
+ --border: oklch(48.6% 0.006 243);
+ --input: oklch(48.6% 0.006 243);
+ --ring: oklch(48.3% 0.06 235);
+ --chart-1: oklch(70% 0.11 235);
+ --chart-2: oklch(68.5% 0.11 297);
+ --chart-3: oklch(67% 0.11 3);
+ --chart-4: oklch(65.5% 0.11 71);
+ --chart-5: oklch(64% 0.11 159);
+ --sidebar: oklch(18.7% 0.006 243);
+ --sidebar-foreground: oklch(91% 0.008 240);
+ --sidebar-primary: oklch(70% 0.1 230);
+ --sidebar-primary-foreground: oklch(16% 0.015 235);
+ --sidebar-accent: oklch(23.2% 0.018 235);
+ --sidebar-accent-foreground: oklch(91% 0.008 240);
+ --sidebar-border: oklch(49.6% 0.006 243);
+ --sidebar-ring: oklch(49.4% 0.06 235);
}
-/* Crimson */
-[data-theme="crimson"] {
+/* Signal */
+[data-theme="signal"] {
--font-sans: 'Instrument Sans', ui-sans-serif, system-ui, sans-serif;
- --radius: 0;
- --background: oklch(0.982 0.012 15);
- --foreground: oklch(0.14 0.025 15);
- --card: oklch(0.976 0.015 14);
- --card-foreground: oklch(0.14 0.025 15);
- --popover: oklch(0.982 0.012 15);
- --popover-foreground: oklch(0.14 0.025 15);
- --primary: oklch(0.5 0.2 18);
- --primary-foreground: oklch(0.97 0.008 15);
- --secondary: oklch(0.94 0.018 15);
- --secondary-foreground: oklch(0.22 0.04 15);
- --muted: oklch(0.935 0.016 15);
- --muted-foreground: oklch(0.48 0.028 15);
- --accent: oklch(0.92 0.022 12);
- --accent-foreground: oklch(0.22 0.04 15);
- --destructive: oklch(0.48 0.22 5);
- --destructive-foreground: oklch(0.36 0.16 5);
- --success: oklch(0.6 0.14 155);
- --success-foreground: oklch(0.38 0.09 155);
- --info: oklch(0.52 0.13 245);
- --info-foreground: oklch(0.38 0.1 245);
- --warning: oklch(0.74 0.16 55);
- --warning-foreground: oklch(0.4 0.1 45);
- --invert: oklch(0.18 0.025 15);
- --invert-foreground: oklch(0.97 0.008 15);
- --focus: oklch(0.5 0.2 18);
- --focus-foreground: oklch(0.97 0.008 15);
- --border: oklch(0.88 0.018 15);
- --input: oklch(0.88 0.018 15);
- --ring: oklch(0.58 0.1 18);
- --chart-1: oklch(0.52 0.2 18);
- --chart-2: oklch(0.45 0.16 350);
- --chart-3: oklch(0.65 0.17 45);
- --chart-4: oklch(0.4 0.1 30);
- --chart-5: oklch(0.58 0.14 75);
- --sidebar: oklch(0.968 0.015 15);
- --sidebar-foreground: oklch(0.14 0.025 15);
- --sidebar-primary: oklch(0.5 0.2 18);
- --sidebar-primary-foreground: oklch(0.97 0.008 15);
- --sidebar-accent: oklch(0.92 0.022 12);
- --sidebar-accent-foreground: oklch(0.22 0.04 15);
- --sidebar-border: oklch(0.88 0.018 15);
- --sidebar-ring: oklch(0.58 0.1 18);
+ --font-heading: 'Space Grotesk', ui-sans-serif, system-ui, sans-serif;
+ --font-mono: 'Geist Mono', ui-monospace, SFMono-Regular, monospace;
+ --radius: 0.75rem;
+ --background: oklch(98% 0.008 75);
+ --foreground: oklch(18% 0.014 60);
+ --card: oklch(99.5% 0.008 72);
+ --card-foreground: oklch(18% 0.014 60);
+ --popover: oklch(99.5% 0.008 72);
+ --popover-foreground: oklch(18% 0.014 60);
+ --primary: oklch(55% 0.14 265);
+ --primary-foreground: oklch(98% 0.015 265);
+ --secondary: oklch(96% 0.01 72);
+ --secondary-foreground: oklch(18% 0.014 60);
+ --muted: oklch(96% 0.01 72);
+ --muted-foreground: oklch(53.3% 0.01 72);
+ --accent: oklch(95.2% 0.018 265);
+ --accent-foreground: oklch(18% 0.014 60);
+ --destructive: oklch(55.1% 0.16 27);
+ --destructive-foreground: oklch(53% 0.144 27);
+ --success: oklch(52% 0.11 152);
+ --success-foreground: oklch(50.4% 0.099 152);
+ --info: oklch(53.2% 0.12 250);
+ --info-foreground: oklch(51.5% 0.108 250);
+ --warning: oklch(53.8% 0.13 75);
+ --warning-foreground: oklch(52% 0.117 75);
+ --invert: oklch(21% 0.01 72);
+ --invert-foreground: oklch(98% 0.004 72);
+ --focus: oklch(55% 0.14 265);
+ --focus-foreground: oklch(98% 0.015 265);
+ --border: oklch(65% 0.01 72);
+ --input: oklch(65% 0.01 72);
+ --ring: oklch(65.1% 0.06 265);
+ --chart-1: oklch(55% 0.12 265);
+ --chart-2: oklch(56% 0.12 327);
+ --chart-3: oklch(57% 0.12 33);
+ --chart-4: oklch(58% 0.12 101);
+ --chart-5: oklch(59% 0.12 189);
+ --sidebar: oklch(96.4% 0.01 72);
+ --sidebar-foreground: oklch(18% 0.014 60);
+ --sidebar-primary: oklch(55% 0.14 265);
+ --sidebar-primary-foreground: oklch(98% 0.015 265);
+ --sidebar-accent: oklch(93.6% 0.018 265);
+ --sidebar-accent-foreground: oklch(18% 0.014 60);
+ --sidebar-border: oklch(63.8% 0.01 72);
+ --sidebar-ring: oklch(63.9% 0.06 265);
}
-[data-theme="crimson"].dark,
-.dark [data-theme="crimson"] {
- /* True black — deepest dark mode, stark contrast, bold red cuts through */
- --background: oklch(0.08 0.015 15);
- --foreground: oklch(0.94 0.01 15);
- --card: oklch(0.13 0.02 15);
- --card-foreground: oklch(0.94 0.01 15);
- --popover: oklch(0.13 0.02 15);
- --popover-foreground: oklch(0.94 0.01 15);
- --primary: oklch(0.68 0.2 18);
- --primary-foreground: oklch(0.97 0.008 15);
- --secondary: oklch(0.18 0.02 15);
- --secondary-foreground: oklch(0.94 0.01 15);
- --muted: oklch(0.18 0.02 15);
- --muted-foreground: oklch(0.58 0.025 15);
- --accent: oklch(0.19 0.025 12);
- --accent-foreground: oklch(0.94 0.01 15);
- --destructive: oklch(0.6 0.2 5);
- --destructive-foreground: oklch(0.48 0.22 5);
- --success: oklch(0.66 0.14 155);
- --success-foreground: oklch(0.54 0.12 155);
- --info: oklch(0.6 0.13 245);
- --info-foreground: oklch(0.5 0.15 245);
- --warning: oklch(0.78 0.16 55);
- --warning-foreground: oklch(0.66 0.14 50);
- --invert: oklch(0.28 0.025 15);
- --invert-foreground: oklch(0.97 0.008 15);
- --focus: oklch(0.6 0.2 18);
- --focus-foreground: oklch(0.97 0.008 15);
- --border: oklch(1 0.008 15 / 14%);
- --input: oklch(1 0.008 15 / 18%);
- --ring: oklch(0.52 0.1 18);
- --chart-1: oklch(0.65 0.2 18);
- --chart-2: oklch(0.6 0.16 350);
- --chart-3: oklch(0.75 0.17 45);
- --chart-4: oklch(0.55 0.1 30);
- --chart-5: oklch(0.7 0.14 75);
- --sidebar: oklch(0.11 0.018 15);
- --sidebar-foreground: oklch(0.94 0.01 15);
- --sidebar-primary: oklch(0.68 0.2 18);
- --sidebar-primary-foreground: oklch(0.97 0.008 15);
- --sidebar-accent: oklch(0.19 0.025 12);
- --sidebar-accent-foreground: oklch(0.94 0.01 15);
- --sidebar-border: oklch(1 0.008 15 / 14%);
- --sidebar-ring: oklch(0.52 0.1 18);
+[data-theme="signal"].dark,
+.dark [data-theme="signal"] {
+ --font-sans: 'Instrument Sans', ui-sans-serif, system-ui, sans-serif;
+ --font-heading: 'Space Grotesk', ui-sans-serif, system-ui, sans-serif;
+ --font-mono: 'Geist Mono', ui-monospace, SFMono-Regular, monospace;
+ --radius: 0.75rem;
+ --background: oklch(17% 0.008 65);
+ --foreground: oklch(94% 0.01 75);
+ --card: oklch(21.5% 0.008 72);
+ --card-foreground: oklch(94% 0.01 75);
+ --popover: oklch(21.5% 0.008 72);
+ --popover-foreground: oklch(94% 0.01 75);
+ --primary: oklch(72% 0.13 268);
+ --primary-foreground: oklch(16% 0.015 265);
+ --secondary: oklch(26% 0.01 72);
+ --secondary-foreground: oklch(94% 0.01 75);
+ --muted: oklch(26% 0.01 72);
+ --muted-foreground: oklch(64.2% 0.01 72);
+ --accent: oklch(30.5% 0.018 265);
+ --accent-foreground: oklch(94% 0.01 75);
+ --destructive: oklch(66% 0.16 27);
+ --destructive-foreground: oklch(66% 0.144 27);
+ --success: oklch(62.9% 0.11 152);
+ --success-foreground: oklch(62.9% 0.099 152);
+ --info: oklch(64% 0.12 250);
+ --info-foreground: oklch(64% 0.108 250);
+ --warning: oklch(64.7% 0.13 75);
+ --warning-foreground: oklch(64.7% 0.117 75);
+ --invert: oklch(37% 0.01 72);
+ --invert-foreground: oklch(98% 0.004 72);
+ --focus: oklch(62% 0.14 265);
+ --focus-foreground: oklch(16% 0.015 265);
+ --border: oklch(49% 0.01 72);
+ --input: oklch(49% 0.01 72);
+ --ring: oklch(49.1% 0.06 265);
+ --chart-1: oklch(70% 0.11 265);
+ --chart-2: oklch(68.5% 0.11 327);
+ --chart-3: oklch(67% 0.11 33);
+ --chart-4: oklch(65.5% 0.11 101);
+ --chart-5: oklch(64% 0.11 189);
+ --sidebar: oklch(19.7% 0.01 72);
+ --sidebar-foreground: oklch(94% 0.01 75);
+ --sidebar-primary: oklch(72% 0.13 268);
+ --sidebar-primary-foreground: oklch(16% 0.015 265);
+ --sidebar-accent: oklch(24.2% 0.018 265);
+ --sidebar-accent-foreground: oklch(94% 0.01 75);
+ --sidebar-border: oklch(50.1% 0.01 72);
+ --sidebar-ring: oklch(50.2% 0.06 265);
}
diff --git a/packages/themes/src/press.css b/packages/themes/src/press.css
new file mode 100644
index 0000000..962623e
--- /dev/null
+++ b/packages/themes/src/press.css
@@ -0,0 +1,112 @@
+/*
+ * Press — Editorial
+ * Warm paper, near-black ink, high contrast. Serif headings.
+ *
+ * Fonts: 'Source Serif 4' (headings) + 'Source Sans 3' (body)
+ * Load via Google Fonts, next/font, or self-host:
+ * https://fonts.googleapis.com/css2?family=Source+Sans+3:wght@400..700&family=Source+Serif+4:wght@400..700&display=swap
+ *
+ * Radius: 0.5rem
+ * Accent hue: ~25 · neutral hue: ~70
+ *
+ * GENERATED by scripts/generate-themes.mjs — edit the anchors there, not
+ * this file. Values are solved against scripts/check-contrast.mjs.
+ */
+
+:root {
+ --font-sans: 'Source Sans 3', ui-sans-serif, system-ui, sans-serif;
+ --font-heading: 'Source Serif 4', ui-serif, Georgia, serif;
+ --font-mono: 'Geist Mono', ui-monospace, SFMono-Regular, monospace;
+ --radius: 0.5rem;
+ --background: oklch(97.5% 0.012 85);
+ --foreground: oklch(16% 0.015 60);
+ --card: oklch(99.1% 0.008 70);
+ --card-foreground: oklch(16% 0.015 60);
+ --popover: oklch(99.1% 0.008 70);
+ --popover-foreground: oklch(16% 0.015 60);
+ --primary: oklch(45% 0.12 25);
+ --primary-foreground: oklch(98% 0.015 25);
+ --secondary: oklch(95.5% 0.01 70);
+ --secondary-foreground: oklch(16% 0.015 60);
+ --muted: oklch(95.5% 0.01 70);
+ --muted-foreground: oklch(52.9% 0.01 70);
+ --accent: oklch(94.7% 0.018 25);
+ --accent-foreground: oklch(16% 0.015 60);
+ --destructive: oklch(54.8% 0.16 27);
+ --destructive-foreground: oklch(52.7% 0.144 27);
+ --success: oklch(51.6% 0.11 152);
+ --success-foreground: oklch(50.1% 0.099 152);
+ --info: oklch(52.8% 0.12 250);
+ --info-foreground: oklch(51.1% 0.108 250);
+ --warning: oklch(53.4% 0.13 75);
+ --warning-foreground: oklch(51.7% 0.117 75);
+ --invert: oklch(21% 0.01 70);
+ --invert-foreground: oklch(98% 0.004 70);
+ --focus: oklch(55% 0.14 25);
+ --focus-foreground: oklch(98% 0.015 25);
+ --border: oklch(64.7% 0.01 70);
+ --input: oklch(64.7% 0.01 70);
+ --ring: oklch(65.3% 0.06 25);
+ --chart-1: oklch(55% 0.12 25);
+ --chart-2: oklch(56% 0.12 87);
+ --chart-3: oklch(57% 0.12 153);
+ --chart-4: oklch(58% 0.12 221);
+ --chart-5: oklch(59% 0.12 309);
+ --sidebar: oklch(95.9% 0.01 70);
+ --sidebar-foreground: oklch(16% 0.015 60);
+ --sidebar-primary: oklch(45% 0.12 25);
+ --sidebar-primary-foreground: oklch(98% 0.015 25);
+ --sidebar-accent: oklch(93.1% 0.018 25);
+ --sidebar-accent-foreground: oklch(16% 0.015 60);
+ --sidebar-border: oklch(63.5% 0.01 70);
+ --sidebar-ring: oklch(64.1% 0.06 25);
+}
+
+.dark {
+ --font-sans: 'Source Sans 3', ui-sans-serif, system-ui, sans-serif;
+ --font-heading: 'Source Serif 4', ui-serif, Georgia, serif;
+ --font-mono: 'Geist Mono', ui-monospace, SFMono-Regular, monospace;
+ --radius: 0.5rem;
+ --background: oklch(15% 0.012 60);
+ --foreground: oklch(93% 0.012 80);
+ --card: oklch(19.5% 0.008 70);
+ --card-foreground: oklch(93% 0.012 80);
+ --popover: oklch(19.5% 0.008 70);
+ --popover-foreground: oklch(93% 0.012 80);
+ --primary: oklch(72% 0.11 30);
+ --primary-foreground: oklch(16% 0.015 25);
+ --secondary: oklch(24% 0.01 70);
+ --secondary-foreground: oklch(93% 0.012 80);
+ --muted: oklch(24% 0.01 70);
+ --muted-foreground: oklch(62.7% 0.01 70);
+ --accent: oklch(28.5% 0.018 25);
+ --accent-foreground: oklch(93% 0.012 80);
+ --destructive: oklch(64.6% 0.16 27);
+ --destructive-foreground: oklch(64.6% 0.144 27);
+ --success: oklch(61.5% 0.11 152);
+ --success-foreground: oklch(61.5% 0.099 152);
+ --info: oklch(62.6% 0.12 250);
+ --info-foreground: oklch(62.6% 0.108 250);
+ --warning: oklch(63.2% 0.13 75);
+ --warning-foreground: oklch(63.2% 0.117 75);
+ --invert: oklch(37% 0.01 70);
+ --invert-foreground: oklch(98% 0.004 70);
+ --focus: oklch(62% 0.14 25);
+ --focus-foreground: oklch(16% 0.015 25);
+ --border: oklch(48.3% 0.01 70);
+ --input: oklch(48.3% 0.01 70);
+ --ring: oklch(49% 0.06 25);
+ --chart-1: oklch(70% 0.11 25);
+ --chart-2: oklch(68.5% 0.11 87);
+ --chart-3: oklch(67% 0.11 153);
+ --chart-4: oklch(65.5% 0.11 221);
+ --chart-5: oklch(64% 0.11 309);
+ --sidebar: oklch(17.7% 0.01 70);
+ --sidebar-foreground: oklch(93% 0.012 80);
+ --sidebar-primary: oklch(72% 0.11 30);
+ --sidebar-primary-foreground: oklch(16% 0.015 25);
+ --sidebar-accent: oklch(22.2% 0.018 25);
+ --sidebar-accent-foreground: oklch(93% 0.012 80);
+ --sidebar-border: oklch(49.3% 0.01 70);
+ --sidebar-ring: oklch(49.9% 0.06 25);
+}
diff --git a/packages/themes/src/rose.css b/packages/themes/src/rose.css
deleted file mode 100644
index c223d01..0000000
--- a/packages/themes/src/rose.css
+++ /dev/null
@@ -1,105 +0,0 @@
-/*
- * Rose — Soft & Bubbly
- * Very rounded, bouncy, warm pink tones.
- * Feels like a friendly consumer app or social platform.
- *
- * Font: Nunito (rounded, approachable, bubbly)
- * Load via Google Fonts, next/font, or self-host:
- * https://fonts.googleapis.com/css2?family=Nunito:wght@400;500;600;700;800&display=swap
- *
- * Radius: 1.25rem (pill-like, very soft)
- * Hue: ~350 (blush/pink)
- */
-
-:root {
- --font-sans: 'Nunito', ui-sans-serif, system-ui, sans-serif;
- --radius: 1.25rem;
- --background: oklch(0.98 0.016 352);
- --foreground: oklch(0.18 0.025 355);
- --card: oklch(0.974 0.02 350);
- --card-foreground: oklch(0.18 0.025 355);
- --popover: oklch(0.98 0.016 352);
- --popover-foreground: oklch(0.18 0.025 355);
- --primary: oklch(0.6 0.18 350);
- --primary-foreground: oklch(0.97 0.008 350);
- --secondary: oklch(0.94 0.022 355);
- --secondary-foreground: oklch(0.28 0.04 350);
- --muted: oklch(0.935 0.02 355);
- --muted-foreground: oklch(0.5 0.028 352);
- --accent: oklch(0.92 0.026 348);
- --accent-foreground: oklch(0.28 0.04 350);
- --destructive: oklch(0.52 0.2 15);
- --destructive-foreground: oklch(0.4 0.15 15);
- --success: oklch(0.62 0.14 155);
- --success-foreground: oklch(0.38 0.09 155);
- --info: oklch(0.55 0.15 280);
- --info-foreground: oklch(0.38 0.12 280);
- --warning: oklch(0.76 0.15 75);
- --warning-foreground: oklch(0.42 0.1 65);
- --invert: oklch(0.21 0.025 355);
- --invert-foreground: oklch(0.97 0.008 350);
- --focus: oklch(0.58 0.18 350);
- --focus-foreground: oklch(0.97 0.008 350);
- --border: oklch(0.89 0.02 355);
- --input: oklch(0.89 0.02 355);
- --ring: oklch(0.62 0.08 350);
- --chart-1: oklch(0.6 0.18 350);
- --chart-2: oklch(0.55 0.16 20);
- --chart-3: oklch(0.65 0.16 330);
- --chart-4: oklch(0.72 0.14 50);
- --chart-5: oklch(0.58 0.14 300);
- --sidebar: oklch(0.968 0.018 355);
- --sidebar-foreground: oklch(0.18 0.025 355);
- --sidebar-primary: oklch(0.6 0.18 350);
- --sidebar-primary-foreground: oklch(0.97 0.008 350);
- --sidebar-accent: oklch(0.92 0.026 348);
- --sidebar-accent-foreground: oklch(0.28 0.04 350);
- --sidebar-border: oklch(0.89 0.02 355);
- --sidebar-ring: oklch(0.62 0.08 350);
-}
-
-.dark {
- /* Warm dusk — warmest dark mode, pink-warm, cozy and inviting */
- --background: oklch(0.16 0.022 358);
- --foreground: oklch(0.93 0.012 350);
- --card: oklch(0.21 0.026 358);
- --card-foreground: oklch(0.93 0.012 350);
- --popover: oklch(0.21 0.026 358);
- --popover-foreground: oklch(0.93 0.012 350);
- --primary: oklch(0.74 0.16 350);
- --primary-foreground: oklch(0.16 0.03 355);
- --secondary: oklch(0.27 0.025 358);
- --secondary-foreground: oklch(0.93 0.012 350);
- --muted: oklch(0.27 0.025 358);
- --muted-foreground: oklch(0.62 0.03 352);
- --accent: oklch(0.28 0.03 355);
- --accent-foreground: oklch(0.93 0.012 350);
- --destructive: oklch(0.65 0.17 15);
- --destructive-foreground: oklch(0.52 0.2 15);
- --success: oklch(0.68 0.14 155);
- --success-foreground: oklch(0.56 0.12 155);
- --info: oklch(0.62 0.15 280);
- --info-foreground: oklch(0.5 0.17 280);
- --warning: oklch(0.78 0.15 75);
- --warning-foreground: oklch(0.66 0.13 68);
- --invert: oklch(0.36 0.028 355);
- --invert-foreground: oklch(0.97 0.008 350);
- --focus: oklch(0.65 0.18 350);
- --focus-foreground: oklch(0.97 0.008 350);
- --border: oklch(1 0.01 350 / 10%);
- --input: oklch(1 0.01 350 / 15%);
- --ring: oklch(0.55 0.08 350);
- --chart-1: oklch(0.7 0.18 350);
- --chart-2: oklch(0.65 0.16 20);
- --chart-3: oklch(0.75 0.16 330);
- --chart-4: oklch(0.8 0.14 50);
- --chart-5: oklch(0.68 0.14 300);
- --sidebar: oklch(0.19 0.024 358);
- --sidebar-foreground: oklch(0.93 0.012 350);
- --sidebar-primary: oklch(0.74 0.16 350);
- --sidebar-primary-foreground: oklch(0.97 0.008 350);
- --sidebar-accent: oklch(0.28 0.03 355);
- --sidebar-accent-foreground: oklch(0.93 0.012 350);
- --sidebar-border: oklch(1 0.01 350 / 10%);
- --sidebar-ring: oklch(0.55 0.08 350);
-}
diff --git a/packages/themes/src/safelist.css b/packages/themes/src/safelist.css
new file mode 100644
index 0000000..29bb229
--- /dev/null
+++ b/packages/themes/src/safelist.css
@@ -0,0 +1,96 @@
+/* Utility safelist for precompiled consumers.
+ *
+ * rnui is normally consumed with Tailwind running in the consumer's build, so
+ * any class they write is compiled on demand. But the design system is ALSO
+ * shipped as a precompiled stylesheet (see utilities.css, and the bundle
+ * published to Claude Design). In that mode a class that wasn't emitted at
+ * build time silently does nothing — no error, no warning, the element just
+ * renders unstyled.
+ *
+ * This file safelists complete scales rather than only the values rnui's own
+ * components happen to use. Import it into any Tailwind build that must
+ * produce a self-contained stylesheet. Bundle size is the deliberate trade:
+ * silent failure costs far more than kilobytes.
+ *
+ * Keep in sync with scripts/emit-utilities.mjs, which reports what actually
+ * shipped by reading the compiled output.
+ */
+
+/* ── Box model: the full spacing scale on every side/axis variant ───────── */
+@source inline(
+ "{,sm:,md:,lg:,xl:}{p,px,py,pt,pr,pb,pl,m,mx,my,mt,mr,mb,ml,gap,gap-x,gap-y,space-x,space-y}-{0,0.5,1,1.5,2,2.5,3,3.5,4,5,6,7,8,9,10,11,12,14,16,20,24}"
+);
+
+/* ── Sizing ─────────────────────────────────────────────────────────────── */
+@source inline(
+ "{,sm:,md:,lg:,xl:}{w,h,min-w,min-h,max-h}-{0,0.5,1,1.5,2,2.5,3,3.5,4,5,6,7,8,9,10,11,12,14,16,20,24}"
+);
+@source inline("{,sm:,md:,lg:,xl:}{w,h}-{auto,full,screen,fit,min,max}");
+@source inline("{,sm:,md:,lg:,xl:}{min-w,min-h,max-h}-{0,full,fit,min,max,screen}");
+@source inline(
+ "{,sm:,md:,lg:,xl:}w-{1/2,1/3,2/3,1/4,3/4,1/5,2/5,3/5,4/5,1/6,5/6,1/12,5/12,7/12,11/12}"
+);
+@source inline(
+ "{,sm:,md:,lg:,xl:}max-w-{xs,sm,md,lg,xl,2xl,3xl,4xl,5xl,6xl,7xl,full,none,prose,screen}"
+);
+
+/* ── Typography ─────────────────────────────────────────────────────────── */
+@source inline("{,sm:,md:,lg:,xl:}text-{xs,sm,base,lg,xl,2xl,3xl,4xl,5xl,6xl,7xl,8xl,9xl}");
+@source inline("font-{thin,extralight,light,normal,medium,semibold,bold,extrabold,black}");
+@source inline("tracking-{tighter,tight,normal,wide,wider,widest}");
+@source inline("leading-{none,tight,snug,normal,relaxed,loose}");
+@source inline("{,sm:,md:,lg:,xl:}text-{left,center,right,justify}");
+@source inline("{truncate,text-ellipsis,text-clip,break-words,break-all,whitespace-nowrap,whitespace-pre-wrap}");
+@source inline("font-{sans,serif,mono}");
+
+/* ── Layout ─────────────────────────────────────────────────────────────── */
+@source inline("{,sm:,md:,lg:,xl:}grid-cols-{1,2,3,4,5,6,7,8,9,10,11,12}");
+@source inline("{,sm:,md:,lg:,xl:}grid-rows-{1,2,3,4,5,6}");
+@source inline("{,sm:,md:,lg:,xl:}col-span-{1,2,3,4,5,6,7,8,9,10,11,12,full}");
+@source inline("{,sm:,md:,lg:,xl:}row-span-{1,2,3,4,5,6,full}");
+@source inline("{,sm:,md:,lg:,xl:}{flex,inline-flex,grid,inline-grid,block,inline-block,inline,hidden,contents}");
+@source inline("{,sm:,md:,lg:,xl:}flex-{row,row-reverse,col,col-reverse,wrap,nowrap,1,auto,initial,none}");
+@source inline("{,sm:,md:,lg:,xl:}items-{start,end,center,baseline,stretch}");
+@source inline("{,sm:,md:,lg:,xl:}justify-{start,end,center,between,around,evenly}");
+@source inline("{,sm:,md:,lg:,xl:}self-{auto,start,end,center,stretch}");
+@source inline("{,sm:,md:,lg:,xl:}{shrink,grow}-{0,1}");
+@source inline("{relative,absolute,fixed,sticky,static}");
+@source inline("{inset,top,right,bottom,left}-{0,0.5,1,2,3,4,auto,full}");
+@source inline("z-{0,10,20,30,40,50,auto}");
+@source inline("{overflow,overflow-x,overflow-y}-{auto,hidden,visible,scroll,clip}");
+@source inline("{,sm:,md:,lg:,xl:}mx-auto");
+
+/* ── Borders, radius, shadow, opacity ───────────────────────────────────── */
+@source inline("rounded{,-t,-r,-b,-l,-tl,-tr,-br,-bl}-{none,xs,sm,md,lg,xl,2xl,3xl,4xl,full}");
+@source inline("border{,-t,-r,-b,-l,-x,-y}-{0,2,4,8}");
+@source inline("{shadow,drop-shadow}-{xs,sm,md,lg,xl,2xl,none}");
+@source inline(
+ "{,hover:,focus:,active:,disabled:,group-hover:,dark:}opacity-{0,5,10,15,20,25,30,35,40,45,50,55,60,65,70,75,80,85,90,95,100}"
+);
+
+/* ── Design-token colors, plain and stateful ────────────────────────────────
+ * Every color the theme layer defines, on bg/text/border, plus the states an
+ * interactive element actually needs. Token names mirror @theme inline in
+ * index.css — if a token is added there, add it here too.
+ */
+@source inline(
+ "{,hover:,focus:,focus-visible:,active:,disabled:,dark:}{bg,text,border}-{background,foreground,card,card-foreground,popover,popover-foreground,primary,primary-foreground,secondary,secondary-foreground,muted,muted-foreground,accent,accent-foreground,destructive,destructive-foreground,info,info-foreground,success,success-foreground,warning,warning-foreground,invert,invert-foreground,focus,focus-foreground,border,input,ring,sidebar,sidebar-foreground,sidebar-primary,sidebar-primary-foreground,sidebar-accent,sidebar-accent-foreground,sidebar-border,sidebar-ring}"
+);
+@source inline("{bg,text,border}-{chart-1,chart-2,chart-3,chart-4,chart-5}");
+/* Common alpha steps on the surfaces that get overlaid. */
+@source inline(
+ "{,hover:,dark:}bg-{primary,secondary,muted,accent,destructive,success,info,warning,foreground,background}/{5,10,20,30,40,50,60,70,80,90}"
+);
+@source inline("{,hover:}border-{border,input,ring,primary,destructive}/{20,30,40,50,60}");
+
+/* ── Rings and outlines ─────────────────────────────────────────────────── */
+@source inline("{,focus:,focus-visible:}ring-{0,1,2,4,8}");
+@source inline("{,focus:,focus-visible:}ring-{ring,primary,destructive,focus,border}");
+@source inline("{,focus:,focus-visible:}outline-{none,0,1,2}");
+
+/* ── Interaction, transitions ───────────────────────────────────────────── */
+@source inline("cursor-{pointer,default,not-allowed,wait,text,move}");
+@source inline("{pointer-events-none,pointer-events-auto,select-none,select-text}");
+@source inline("transition{,-all,-colors,-opacity,-transform,-shadow}");
+@source inline("duration-{75,100,150,200,300,500,700,1000}");
+@source inline("ease-{linear,in,out,in-out}");
diff --git a/packages/themes/src/signal.css b/packages/themes/src/signal.css
new file mode 100644
index 0000000..30a21bb
--- /dev/null
+++ b/packages/themes/src/signal.css
@@ -0,0 +1,112 @@
+/*
+ * Signal — Brand-forward
+ * Warm neutral shell, one confident accent doing all the work.
+ *
+ * Fonts: 'Space Grotesk' (headings) + 'Instrument Sans' (body)
+ * Load via Google Fonts, next/font, or self-host:
+ * https://fonts.googleapis.com/css2?family=Instrument+Sans:wght@400..700&family=Space+Grotesk:wght@400..700&display=swap
+ *
+ * Radius: 0.75rem
+ * Accent hue: ~265 · neutral hue: ~72
+ *
+ * GENERATED by scripts/generate-themes.mjs — edit the anchors there, not
+ * this file. Values are solved against scripts/check-contrast.mjs.
+ */
+
+:root {
+ --font-sans: 'Instrument Sans', ui-sans-serif, system-ui, sans-serif;
+ --font-heading: 'Space Grotesk', ui-sans-serif, system-ui, sans-serif;
+ --font-mono: 'Geist Mono', ui-monospace, SFMono-Regular, monospace;
+ --radius: 0.75rem;
+ --background: oklch(98% 0.008 75);
+ --foreground: oklch(18% 0.014 60);
+ --card: oklch(99.5% 0.008 72);
+ --card-foreground: oklch(18% 0.014 60);
+ --popover: oklch(99.5% 0.008 72);
+ --popover-foreground: oklch(18% 0.014 60);
+ --primary: oklch(55% 0.14 265);
+ --primary-foreground: oklch(98% 0.015 265);
+ --secondary: oklch(96% 0.01 72);
+ --secondary-foreground: oklch(18% 0.014 60);
+ --muted: oklch(96% 0.01 72);
+ --muted-foreground: oklch(53.3% 0.01 72);
+ --accent: oklch(95.2% 0.018 265);
+ --accent-foreground: oklch(18% 0.014 60);
+ --destructive: oklch(55.1% 0.16 27);
+ --destructive-foreground: oklch(53% 0.144 27);
+ --success: oklch(52% 0.11 152);
+ --success-foreground: oklch(50.4% 0.099 152);
+ --info: oklch(53.2% 0.12 250);
+ --info-foreground: oklch(51.5% 0.108 250);
+ --warning: oklch(53.8% 0.13 75);
+ --warning-foreground: oklch(52% 0.117 75);
+ --invert: oklch(21% 0.01 72);
+ --invert-foreground: oklch(98% 0.004 72);
+ --focus: oklch(55% 0.14 265);
+ --focus-foreground: oklch(98% 0.015 265);
+ --border: oklch(65% 0.01 72);
+ --input: oklch(65% 0.01 72);
+ --ring: oklch(65.1% 0.06 265);
+ --chart-1: oklch(55% 0.12 265);
+ --chart-2: oklch(56% 0.12 327);
+ --chart-3: oklch(57% 0.12 33);
+ --chart-4: oklch(58% 0.12 101);
+ --chart-5: oklch(59% 0.12 189);
+ --sidebar: oklch(96.4% 0.01 72);
+ --sidebar-foreground: oklch(18% 0.014 60);
+ --sidebar-primary: oklch(55% 0.14 265);
+ --sidebar-primary-foreground: oklch(98% 0.015 265);
+ --sidebar-accent: oklch(93.6% 0.018 265);
+ --sidebar-accent-foreground: oklch(18% 0.014 60);
+ --sidebar-border: oklch(63.8% 0.01 72);
+ --sidebar-ring: oklch(63.9% 0.06 265);
+}
+
+.dark {
+ --font-sans: 'Instrument Sans', ui-sans-serif, system-ui, sans-serif;
+ --font-heading: 'Space Grotesk', ui-sans-serif, system-ui, sans-serif;
+ --font-mono: 'Geist Mono', ui-monospace, SFMono-Regular, monospace;
+ --radius: 0.75rem;
+ --background: oklch(17% 0.008 65);
+ --foreground: oklch(94% 0.01 75);
+ --card: oklch(21.5% 0.008 72);
+ --card-foreground: oklch(94% 0.01 75);
+ --popover: oklch(21.5% 0.008 72);
+ --popover-foreground: oklch(94% 0.01 75);
+ --primary: oklch(72% 0.13 268);
+ --primary-foreground: oklch(16% 0.015 265);
+ --secondary: oklch(26% 0.01 72);
+ --secondary-foreground: oklch(94% 0.01 75);
+ --muted: oklch(26% 0.01 72);
+ --muted-foreground: oklch(64.2% 0.01 72);
+ --accent: oklch(30.5% 0.018 265);
+ --accent-foreground: oklch(94% 0.01 75);
+ --destructive: oklch(66% 0.16 27);
+ --destructive-foreground: oklch(66% 0.144 27);
+ --success: oklch(62.9% 0.11 152);
+ --success-foreground: oklch(62.9% 0.099 152);
+ --info: oklch(64% 0.12 250);
+ --info-foreground: oklch(64% 0.108 250);
+ --warning: oklch(64.7% 0.13 75);
+ --warning-foreground: oklch(64.7% 0.117 75);
+ --invert: oklch(37% 0.01 72);
+ --invert-foreground: oklch(98% 0.004 72);
+ --focus: oklch(62% 0.14 265);
+ --focus-foreground: oklch(16% 0.015 265);
+ --border: oklch(49% 0.01 72);
+ --input: oklch(49% 0.01 72);
+ --ring: oklch(49.1% 0.06 265);
+ --chart-1: oklch(70% 0.11 265);
+ --chart-2: oklch(68.5% 0.11 327);
+ --chart-3: oklch(67% 0.11 33);
+ --chart-4: oklch(65.5% 0.11 101);
+ --chart-5: oklch(64% 0.11 189);
+ --sidebar: oklch(19.7% 0.01 72);
+ --sidebar-foreground: oklch(94% 0.01 75);
+ --sidebar-primary: oklch(72% 0.13 268);
+ --sidebar-primary-foreground: oklch(16% 0.015 265);
+ --sidebar-accent: oklch(24.2% 0.018 265);
+ --sidebar-accent-foreground: oklch(94% 0.01 75);
+ --sidebar-border: oklch(50.1% 0.01 72);
+ --sidebar-ring: oklch(50.2% 0.06 265);
+}
diff --git a/packages/themes/src/slate.css b/packages/themes/src/slate.css
deleted file mode 100644
index ab9ad7f..0000000
--- a/packages/themes/src/slate.css
+++ /dev/null
@@ -1,105 +0,0 @@
-/*
- * Slate — Professional Tech
- * Clean, functional, moderate radius, cool blue-gray.
- * Feels like a developer tool, IDE, or SaaS dashboard.
- *
- * Font: Geist (clean, monospaced-inspired, techy)
- * Load via Google Fonts, next/font, or self-host:
- * https://fonts.googleapis.com/css2?family=Geist:wght@300;400;500;600;700;800&display=swap
- *
- * Radius: 0.5rem (moderate, precise)
- * Hue: ~250 (cool blue-steel)
- */
-
-:root {
- --font-sans: 'Geist', ui-sans-serif, system-ui, sans-serif;
- --radius: 0.5rem;
- --background: oklch(0.982 0.012 250);
- --foreground: oklch(0.17 0.018 250);
- --card: oklch(0.976 0.015 248);
- --card-foreground: oklch(0.17 0.018 250);
- --popover: oklch(0.982 0.012 250);
- --popover-foreground: oklch(0.17 0.018 250);
- --primary: oklch(0.48 0.1 250);
- --primary-foreground: oklch(0.97 0.006 250);
- --secondary: oklch(0.945 0.016 250);
- --secondary-foreground: oklch(0.25 0.03 250);
- --muted: oklch(0.94 0.014 250);
- --muted-foreground: oklch(0.5 0.02 250);
- --accent: oklch(0.93 0.018 248);
- --accent-foreground: oklch(0.25 0.03 250);
- --destructive: oklch(0.52 0.18 20);
- --destructive-foreground: oklch(0.4 0.14 20);
- --success: oklch(0.6 0.14 165);
- --success-foreground: oklch(0.38 0.09 165);
- --info: oklch(0.52 0.11 250);
- --info-foreground: oklch(0.38 0.08 250);
- --warning: oklch(0.76 0.14 75);
- --warning-foreground: oklch(0.42 0.1 60);
- --invert: oklch(0.21 0.018 250);
- --invert-foreground: oklch(0.97 0.006 250);
- --focus: oklch(0.52 0.12 255);
- --focus-foreground: oklch(0.97 0.006 250);
- --border: oklch(0.9 0.014 250);
- --input: oklch(0.9 0.014 250);
- --ring: oklch(0.58 0.05 250);
- --chart-1: oklch(0.52 0.12 250);
- --chart-2: oklch(0.58 0.13 220);
- --chart-3: oklch(0.5 0.14 290);
- --chart-4: oklch(0.62 0.1 200);
- --chart-5: oklch(0.6 0.11 330);
- --sidebar: oklch(0.97 0.014 250);
- --sidebar-foreground: oklch(0.17 0.018 250);
- --sidebar-primary: oklch(0.48 0.1 250);
- --sidebar-primary-foreground: oklch(0.97 0.006 250);
- --sidebar-accent: oklch(0.93 0.018 248);
- --sidebar-accent-foreground: oklch(0.25 0.03 250);
- --sidebar-border: oklch(0.9 0.014 250);
- --sidebar-ring: oklch(0.58 0.05 250);
-}
-
-.dark {
- /* Ice steel — very dark, desaturated, clinical precision, icy blue accents */
- --background: oklch(0.11 0.018 252);
- --foreground: oklch(0.93 0.008 250);
- --card: oklch(0.16 0.02 252);
- --card-foreground: oklch(0.93 0.008 250);
- --popover: oklch(0.16 0.02 252);
- --popover-foreground: oklch(0.93 0.008 250);
- --primary: oklch(0.72 0.1 248);
- --primary-foreground: oklch(0.11 0.025 250);
- --secondary: oklch(0.22 0.02 252);
- --secondary-foreground: oklch(0.93 0.008 250);
- --muted: oklch(0.22 0.02 252);
- --muted-foreground: oklch(0.62 0.022 250);
- --accent: oklch(0.23 0.022 250);
- --accent-foreground: oklch(0.93 0.008 250);
- --destructive: oklch(0.65 0.16 20);
- --destructive-foreground: oklch(0.52 0.18 20);
- --success: oklch(0.66 0.14 165);
- --success-foreground: oklch(0.54 0.12 165);
- --info: oklch(0.6 0.11 250);
- --info-foreground: oklch(0.5 0.13 250);
- --warning: oklch(0.78 0.14 75);
- --warning-foreground: oklch(0.66 0.12 68);
- --invert: oklch(0.32 0.02 250);
- --invert-foreground: oklch(0.97 0.006 250);
- --focus: oklch(0.58 0.14 255);
- --focus-foreground: oklch(0.97 0.006 250);
- --border: oklch(1 0.008 250 / 9%);
- --input: oklch(1 0.008 250 / 14%);
- --ring: oklch(0.52 0.05 250);
- --chart-1: oklch(0.68 0.12 250);
- --chart-2: oklch(0.7 0.13 220);
- --chart-3: oklch(0.65 0.14 290);
- --chart-4: oklch(0.72 0.1 200);
- --chart-5: oklch(0.7 0.11 330);
- --sidebar: oklch(0.14 0.019 252);
- --sidebar-foreground: oklch(0.93 0.008 250);
- --sidebar-primary: oklch(0.72 0.1 248);
- --sidebar-primary-foreground: oklch(0.97 0.006 250);
- --sidebar-accent: oklch(0.23 0.022 250);
- --sidebar-accent-foreground: oklch(0.93 0.008 250);
- --sidebar-border: oklch(1 0.008 250 / 9%);
- --sidebar-ring: oklch(0.52 0.05 250);
-}
diff --git a/packages/themes/src/utilities.css b/packages/themes/src/utilities.css
new file mode 100644
index 0000000..8df782f
--- /dev/null
+++ b/packages/themes/src/utilities.css
@@ -0,0 +1,12 @@
+/* Precompiled utility layer + design tokens.
+ *
+ * For consumers who cannot run Tailwind themselves (a precompiled stylesheet,
+ * an embedded preview, a design tool). Everything in ./safelist.css is emitted
+ * eagerly, so ordinary layout markup works without a build step.
+ *
+ * If you DO run Tailwind, import '@e412/rnui-themes' instead and let your own
+ * build compile only what you use — this file is deliberately much larger.
+ */
+@import 'tailwindcss';
+@import './index.css';
+@import './safelist.css';
diff --git a/packages/themes/src/violet.css b/packages/themes/src/violet.css
deleted file mode 100644
index 82397b6..0000000
--- a/packages/themes/src/violet.css
+++ /dev/null
@@ -1,105 +0,0 @@
-/*
- * Violet — Luxury Creative
- * Geometric, very rounded, rich purple tones.
- * Feels like a premium design tool or creative platform.
- *
- * Font: Outfit (geometric, modern, clean)
- * Load via Google Fonts, next/font, or self-host:
- * https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;800&display=swap
- *
- * Radius: 1rem (very round, soft)
- * Hue: ~285 (amethyst/purple)
- */
-
-:root {
- --font-sans: 'Outfit', ui-sans-serif, system-ui, sans-serif;
- --radius: 1rem;
- --background: oklch(0.98 0.016 292);
- --foreground: oklch(0.18 0.03 285);
- --card: oklch(0.974 0.02 290);
- --card-foreground: oklch(0.18 0.03 285);
- --popover: oklch(0.98 0.016 292);
- --popover-foreground: oklch(0.18 0.03 285);
- --primary: oklch(0.52 0.2 285);
- --primary-foreground: oklch(0.97 0.01 285);
- --secondary: oklch(0.94 0.025 290);
- --secondary-foreground: oklch(0.28 0.05 285);
- --muted: oklch(0.935 0.022 290);
- --muted-foreground: oklch(0.5 0.035 288);
- --accent: oklch(0.92 0.03 288);
- --accent-foreground: oklch(0.28 0.05 285);
- --destructive: oklch(0.55 0.2 355);
- --destructive-foreground: oklch(0.4 0.15 355);
- --success: oklch(0.62 0.14 155);
- --success-foreground: oklch(0.38 0.09 155);
- --info: oklch(0.52 0.18 275);
- --info-foreground: oklch(0.38 0.14 275);
- --warning: oklch(0.76 0.15 70);
- --warning-foreground: oklch(0.42 0.1 60);
- --invert: oklch(0.22 0.03 285);
- --invert-foreground: oklch(0.97 0.01 285);
- --focus: oklch(0.55 0.2 285);
- --focus-foreground: oklch(0.97 0.01 285);
- --border: oklch(0.89 0.022 290);
- --input: oklch(0.89 0.022 290);
- --ring: oklch(0.6 0.1 285);
- --chart-1: oklch(0.55 0.2 285);
- --chart-2: oklch(0.58 0.18 320);
- --chart-3: oklch(0.48 0.16 260);
- --chart-4: oklch(0.68 0.16 350);
- --chart-5: oklch(0.65 0.12 240);
- --sidebar: oklch(0.968 0.02 290);
- --sidebar-foreground: oklch(0.18 0.03 285);
- --sidebar-primary: oklch(0.52 0.2 285);
- --sidebar-primary-foreground: oklch(0.97 0.01 285);
- --sidebar-accent: oklch(0.92 0.03 288);
- --sidebar-accent-foreground: oklch(0.28 0.05 285);
- --sidebar-border: oklch(0.89 0.022 290);
- --sidebar-ring: oklch(0.6 0.1 285);
-}
-
-.dark {
- /* Midnight purple — deep, high chroma, vivid neon accents */
- --background: oklch(0.12 0.035 285);
- --foreground: oklch(0.93 0.014 290);
- --card: oklch(0.17 0.04 285);
- --card-foreground: oklch(0.93 0.014 290);
- --popover: oklch(0.17 0.04 285);
- --popover-foreground: oklch(0.93 0.014 290);
- --primary: oklch(0.72 0.18 285);
- --primary-foreground: oklch(0.12 0.04 285);
- --secondary: oklch(0.24 0.035 285);
- --secondary-foreground: oklch(0.93 0.014 290);
- --muted: oklch(0.24 0.035 285);
- --muted-foreground: oklch(0.62 0.04 288);
- --accent: oklch(0.25 0.04 285);
- --accent-foreground: oklch(0.93 0.014 290);
- --destructive: oklch(0.68 0.17 355);
- --destructive-foreground: oklch(0.55 0.2 355);
- --success: oklch(0.68 0.14 155);
- --success-foreground: oklch(0.56 0.12 155);
- --info: oklch(0.6 0.18 275);
- --info-foreground: oklch(0.5 0.2 275);
- --warning: oklch(0.78 0.15 70);
- --warning-foreground: oklch(0.66 0.13 65);
- --invert: oklch(0.34 0.04 285);
- --invert-foreground: oklch(0.97 0.01 285);
- --focus: oklch(0.6 0.2 285);
- --focus-foreground: oklch(0.97 0.01 285);
- --border: oklch(1 0.012 290 / 10%);
- --input: oklch(1 0.012 290 / 15%);
- --ring: oklch(0.55 0.1 285);
- --chart-1: oklch(0.68 0.2 285);
- --chart-2: oklch(0.7 0.18 320);
- --chart-3: oklch(0.6 0.18 260);
- --chart-4: oklch(0.76 0.16 350);
- --chart-5: oklch(0.75 0.12 240);
- --sidebar: oklch(0.15 0.038 285);
- --sidebar-foreground: oklch(0.93 0.014 290);
- --sidebar-primary: oklch(0.72 0.18 285);
- --sidebar-primary-foreground: oklch(0.97 0.01 285);
- --sidebar-accent: oklch(0.25 0.04 285);
- --sidebar-accent-foreground: oklch(0.93 0.014 290);
- --sidebar-border: oklch(1 0.012 290 / 10%);
- --sidebar-ring: oklch(0.55 0.1 285);
-}
diff --git a/packages/themes/vite.config.ts b/packages/themes/vite.config.ts
index 340fdcd..7aef865 100644
--- a/packages/themes/vite.config.ts
+++ b/packages/themes/vite.config.ts
@@ -1,7 +1,11 @@
import { defineConfig } from 'vite'
+import tailwindcss from '@tailwindcss/vite'
import { resolve } from 'node:path'
export default defineConfig({
+ // utilities.css is the only entry that runs Tailwind; the theme files are
+ // plain token declarations and pass through untouched.
+ plugins: [tailwindcss()],
build: {
rollupOptions: {
input: {
@@ -9,15 +13,11 @@ export default defineConfig({
base: resolve(import.meta.dirname, 'src/base.css'),
light: resolve(import.meta.dirname, 'src/light.css'),
dark: resolve(import.meta.dirname, 'src/dark.css'),
- oxide: resolve(import.meta.dirname, 'src/oxide.css'),
- ocean: resolve(import.meta.dirname, 'src/ocean.css'),
- violet: resolve(import.meta.dirname, 'src/violet.css'),
- forest: resolve(import.meta.dirname, 'src/forest.css'),
- rose: resolve(import.meta.dirname, 'src/rose.css'),
- amber: resolve(import.meta.dirname, 'src/amber.css'),
- slate: resolve(import.meta.dirname, 'src/slate.css'),
- crimson: resolve(import.meta.dirname, 'src/crimson.css'),
+ press: resolve(import.meta.dirname, 'src/press.css'),
+ console: resolve(import.meta.dirname, 'src/console.css'),
+ signal: resolve(import.meta.dirname, 'src/signal.css'),
presets: resolve(import.meta.dirname, 'src/presets.css'),
+ utilities: resolve(import.meta.dirname, 'src/utilities.css'),
},
output: {
assetFileNames: '[name][extname]',
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 89ff9c9..4028832 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -396,6 +396,15 @@ importers:
packages/themes:
devDependencies:
+ '@tailwindcss/vite':
+ specifier: ^4.2.2
+ version: 4.2.2(vite@8.0.5(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))
+ culori:
+ specifier: ^4.0.2
+ version: 4.0.2
+ tailwindcss:
+ specifier: ^4.2.2
+ version: 4.2.2
vite:
specifier: ^8.0.0
version: 8.0.5(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)