diff --git a/.cursor/rules/v2-color-scale.mdc b/.cursor/rules/v2-color-scale.mdc
new file mode 100644
index 000000000..9241445c0
--- /dev/null
+++ b/.cursor/rules/v2-color-scale.mdc
@@ -0,0 +1,19 @@
+---
+description: v2 UI color system — Radix numbered scale, no hardcoded colors, no manual dark mode
+globs: "packages/ui/src/v2/**/*.{ts,tsx}"
+alwaysApply: false
+---
+
+# v2 color scale
+
+Use the Radix 12-step numbered scale for all colors in v2 components: `sand`, `gold`, `red`, `green`, `amber`, `sky` (steps 1–12).
+
+## Rules
+
+- Use scale classes (`bg-sand-3`, `text-red-11`, `border-gold-7`) — never v1 semantic names (`txt-primary`, `border-low`, `bg-primary`, etc.)
+- Never hardcode hex/rgb/oklch values for colors that exist in the scale
+- Never use the `dark:` variant prefix for v2 color overrides — dark mode is handled by the `.dark` class on `` via Radix CSS imports
+- Do not mix v1 and v2 color classes in the same component
+- Respect step ranges: 1–2 backgrounds, 3–5 component bg, 6–8 borders, 9–10 solid bg, 11–12 text
+
+Full guide: `contrib/claude/v2-colors.md`
diff --git a/AGENTS.md b/AGENTS.md
index 5a178d58c..d56223331 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -24,6 +24,7 @@ Detailed guides for specific subsystems live in `contrib/claude/`:
- [`contrib/claude/relay.md`](contrib/claude/relay.md) — Frontend Relay client (queries, fragments, mutations, pagination)
- [`contrib/claude/react-components.md`](contrib/claude/react-components.md) — React component shape (file/export, props, configure vs data via hooks)
- [`contrib/claude/ui.md`](contrib/claude/ui.md) — @probo/ui, Tailwind, tailwind-variants, folders, skeletons, compound components
+- [`contrib/claude/v2-colors.md`](contrib/claude/v2-colors.md) — v2 color system (Radix scale, step usage, do/don't)
- [`contrib/claude/config.md`](contrib/claude/config.md) — Configuration propagation (all files to update when config changes)
- [`contrib/claude/file-naming.md`](contrib/claude/file-naming.md) — File naming conventions (template files, extensions)
- [`contrib/claude/prompt-style.md`](contrib/claude/prompt-style.md) — Agent prompt template structure (role/task/instructions XML style)
diff --git a/contrib/claude/v2-colors.md b/contrib/claude/v2-colors.md
new file mode 100644
index 000000000..fcdbce28f
--- /dev/null
+++ b/contrib/claude/v2-colors.md
@@ -0,0 +1,163 @@
+# v2 color system (Radix scale)
+
+The v2 UI kit uses [Radix Colors](https://www.radix-ui.com/colors) 12-step scales as its color primitive. Each hue provides 12 numbered steps designed for specific use cases. Components consume these through Tailwind utility classes (`bg-sand-3`, `text-red-11`, `border-gold-7`, …).
+
+Theme file: [`packages/ui/src/v2/theme.css`](../../packages/ui/src/v2/theme.css)
+
+## Available scales
+
+| Scale | Role |
+|-------|------|
+| **sand** | Neutral — primary UI chrome (backgrounds, borders, text) |
+| **gold** | Warm accent neutral |
+| **red** | Destructive / error |
+| **green** | Success / positive |
+| **amber** | Warning |
+| **sky** | Informational |
+
+## Step-to-usage mapping
+
+Every scale follows the same 12-step structure:
+
+| Step | Use case | Tailwind example |
+|------|----------|------------------|
+| 1 | App background | `bg-sand-1` |
+| 2 | Subtle background | `bg-sand-2` |
+| 3 | UI element background | `bg-sand-3` |
+| 4 | Hovered UI element background | `hover:bg-sand-4` |
+| 5 | Active / selected UI element background | `bg-sand-5` |
+| 6 | Subtle borders and separators | `border-sand-6` |
+| 7 | UI element border and focus rings | `border-sand-7` |
+| 8 | Hovered UI element border | `border-sand-8` |
+| 9 | Solid backgrounds | `bg-green-9` |
+| 10 | Hovered solid backgrounds | `hover:bg-green-10` |
+| 11 | Low-contrast text | `text-sand-11` |
+| 12 | High-contrast text | `text-sand-12` |
+
+### Quick mental model
+
+Three bands: **low = light/background**, **middle = borders**, **high = text/solid**.
+
+- **1–2** → backgrounds
+- **3–5** → component backgrounds (normal → hover → active)
+- **6–8** → borders (subtle → default → strong)
+- **9–10** → solid backgrounds (normal → hover)
+- **11–12** → text (low-contrast → high-contrast)
+
+## Choosing a color step
+
+1. **What am I styling?**
+ - Background → steps 1–5 (or 9–10 for solid fills)
+ - Border → steps 6–8
+ - Text / icon → steps 11–12
+2. **What state?**
+ - Default → lower step in the range (3, 6, 9, 11)
+ - Hover → next step up (4, 7, 10)
+ - Active / pressed → one more (5, 8)
+3. **Which hue?**
+ - Neutral UI → `sand`
+ - Semantic meaning → `red` (error), `green` (success), `amber` (warning), `sky` (info)
+ - Warm accent → `gold`
+
+## Neutral vs accent
+
+Use **sand** for all general UI chrome: page backgrounds, card backgrounds, borders, primary text. Use hue scales only when conveying semantic meaning:
+
+```tsx
+// Neutral card
+
+
Title
+
Description
+
+
+// Error state
+
+
Something went wrong
+
+
+// Success badge
+Approved
+```
+
+## Contrast guarantees
+
+Per the Radix spec, steps 11 and 12 are guaranteed to meet APCA contrast requirements on top of a step 1 or 2 background from the same scale. This means `text-sand-11` on `bg-sand-2` is always readable, and `text-sand-12` on `bg-sand-1` is always readable.
+
+## Dark mode
+
+**Never apply dark-mode color overrides in components.** The v2 theme imports `@radix-ui/colors` CSS files which handle light/dark switching automatically. Dark mode activates when a `.dark` class is present on ``:
+
+```ts
+document.documentElement.classList.toggle("dark", isDark);
+```
+
+The same Tailwind classes (`bg-sand-1`, `text-red-11`, etc.) resolve to the correct dark values automatically because the `@theme inline` mappings reference the Radix variables (`var(--sand-1)`, etc.) which switch based on the `.dark` class. P3 wide-gamut colors are included for both light and dark modes on supported displays.
+
+This is independent of v1's dark mode which uses `@variant dark` / `prefers-color-scheme`.
+
+## `data-theme="v2"` boundary
+
+All v2 components must render inside a `[data-theme="v2"]` ancestor. This boundary scopes v2 token values (colors now, fonts/spacing/shadows later) so they don't collide with the v1 theme during progressive migration.
+
+```html
+
+
+
+```
+
+## Do / don't
+
+### Use the numbered scale
+
+```tsx
+// Good — numbered scale step
+
...
+
+// Bad — hardcoded hex
+
...
+
+// Bad — v1 semantic color names in a v2 component
+
...
+```
+
+### Respect step ranges
+
+```tsx
+// Good — step 3 for element background, step 11 for text
+
+
+// Bad — step 11 is a text step, not a background step
+
+```
+
+### Do not mix v1 and v2 colors
+
+```tsx
+// Bad — mixing v1 (txt-primary) and v2 (sand-3) in one component
+
...
+
+// Good — all v2
+
...
+```
+
+### Let the theme handle dark mode
+
+```tsx
+// Bad — manual dark: overrides for v2 colors
+
...
+
+// Good — just use the scale; dark values come from the theme scope
+
...
+```
+
+### Solid backgrounds (steps 9–10)
+
+Steps 9 and 10 are designed for prominent, solid-color backgrounds (primary buttons, badges, banners). Most step 9 colors are designed for white foreground text. Exceptions: **sky**, **amber** are designed for dark foreground text on steps 9–10.
+
+```tsx
+// Good — green solid button with white text
+
+
+// Good — amber badge with dark text (amber 9-10 are light/bright)
+Warning
+```
diff --git a/package-lock.json b/package-lock.json
index 8b650dc01..39e081321 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -4229,6 +4229,12 @@
"integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==",
"license": "BSD-3-Clause"
},
+ "node_modules/@radix-ui/colors": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/colors/-/colors-3.0.0.tgz",
+ "integrity": "sha512-FUOsGBkHrYJwCSEtWRCIfQbZG7q1e6DgxCIOe1SUQzDe/7rXXeA47s8yCn6fuTNQAj1Zq4oTFi9Yjp3wzElcxg==",
+ "license": "MIT"
+ },
"node_modules/@radix-ui/number": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz",
@@ -22269,6 +22275,7 @@
"@probo/helpers": "1.0.0",
"@probo/i18n": "1.0.0",
"@probo/prosemirror": "1.0.0",
+ "@radix-ui/colors": "^3.0.0",
"@radix-ui/react-alert-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-label": "^2.1.8",
diff --git a/packages/ui/package.json b/packages/ui/package.json
index 8cbafc048..edbf10697 100644
--- a/packages/ui/package.json
+++ b/packages/ui/package.json
@@ -18,6 +18,7 @@
"@probo/helpers": "1.0.0",
"@probo/i18n": "1.0.0",
"@probo/prosemirror": "1.0.0",
+ "@radix-ui/colors": "^3.0.0",
"@radix-ui/react-alert-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-label": "^2.1.8",
diff --git a/packages/ui/src/v2/theme.css b/packages/ui/src/v2/theme.css
new file mode 100644
index 000000000..7cfdd3578
--- /dev/null
+++ b/packages/ui/src/v2/theme.css
@@ -0,0 +1,254 @@
+/* Copyright (c) 2026 Probo Inc .
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
+ * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+ * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
+ * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+ * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
+ * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+ * PERFORMANCE OF THIS SOFTWARE.
+ */
+
+/*
+ * v2 color system — Radix 12-step scale
+ *
+ * Each hue has 12 steps designed for specific use cases:
+ *
+ * Step Use case
+ * ────── ──────────────────────────────────────
+ * 1 App background
+ * 2 Subtle background
+ * 3 UI element background
+ * 4 Hovered UI element background
+ * 5 Active / selected UI element background
+ * 6 Subtle borders and separators
+ * 7 UI element border and focus rings
+ * 8 Hovered UI element border
+ * 9 Solid backgrounds
+ * 10 Hovered solid backgrounds
+ * 11 Low-contrast text
+ * 12 High-contrast text
+ *
+ * Scales:
+ * sand — neutral (primary UI chrome)
+ * gold — warm accent neutral
+ * red — destructive / error
+ * green — success / positive
+ * amber — warning
+ * sky — informational
+ *
+ * Dark mode: toggle .dark on . Radix dark imports handle the rest.
+ * P3 wide gamut: included automatically via @radix-ui/colors @supports blocks.
+ *
+ * See contrib/claude/v2-colors.md for the full usage guide.
+ */
+
+/* ---------------------------------------------------------------------------
+ * Radix color imports (light → :root, dark → .dark)
+ *
+ * These define --sand-1 … --sand-12, --gold-1 … etc. as raw CSS variables.
+ * P3 wide-gamut overrides are included via @supports in each file.
+ * ------------------------------------------------------------------------- */
+@import "@radix-ui/colors/sand.css";
+@import "@radix-ui/colors/sand-dark.css";
+@import "@radix-ui/colors/gold.css";
+@import "@radix-ui/colors/gold-dark.css";
+@import "@radix-ui/colors/red.css";
+@import "@radix-ui/colors/red-dark.css";
+@import "@radix-ui/colors/green.css";
+@import "@radix-ui/colors/green-dark.css";
+@import "@radix-ui/colors/amber.css";
+@import "@radix-ui/colors/amber-dark.css";
+@import "@radix-ui/colors/sky.css";
+@import "@radix-ui/colors/sky-dark.css";
+
+/* ---------------------------------------------------------------------------
+ * Register v2 color names with Tailwind (generates utility classes)
+ *
+ * Maps --sand-1 → --color-sand-1 so Tailwind generates bg-sand-1, text-sand-1,
+ * border-sand-1, etc. The `inline` keyword makes utilities reference
+ * var(--sand-1) directly, so light/dark switching at the Radix level
+ * propagates automatically.
+ * ------------------------------------------------------------------------- */
+@theme inline {
+ /* sand */
+ --color-sand-1: var(--sand-1);
+ --color-sand-2: var(--sand-2);
+ --color-sand-3: var(--sand-3);
+ --color-sand-4: var(--sand-4);
+ --color-sand-5: var(--sand-5);
+ --color-sand-6: var(--sand-6);
+ --color-sand-7: var(--sand-7);
+ --color-sand-8: var(--sand-8);
+ --color-sand-9: var(--sand-9);
+ --color-sand-10: var(--sand-10);
+ --color-sand-11: var(--sand-11);
+ --color-sand-12: var(--sand-12);
+
+ /* gold */
+ --color-gold-1: var(--gold-1);
+ --color-gold-2: var(--gold-2);
+ --color-gold-3: var(--gold-3);
+ --color-gold-4: var(--gold-4);
+ --color-gold-5: var(--gold-5);
+ --color-gold-6: var(--gold-6);
+ --color-gold-7: var(--gold-7);
+ --color-gold-8: var(--gold-8);
+ --color-gold-9: var(--gold-9);
+ --color-gold-10: var(--gold-10);
+ --color-gold-11: var(--gold-11);
+ --color-gold-12: var(--gold-12);
+
+ /* red */
+ --color-red-1: var(--red-1);
+ --color-red-2: var(--red-2);
+ --color-red-3: var(--red-3);
+ --color-red-4: var(--red-4);
+ --color-red-5: var(--red-5);
+ --color-red-6: var(--red-6);
+ --color-red-7: var(--red-7);
+ --color-red-8: var(--red-8);
+ --color-red-9: var(--red-9);
+ --color-red-10: var(--red-10);
+ --color-red-11: var(--red-11);
+ --color-red-12: var(--red-12);
+
+ /* green */
+ --color-green-1: var(--green-1);
+ --color-green-2: var(--green-2);
+ --color-green-3: var(--green-3);
+ --color-green-4: var(--green-4);
+ --color-green-5: var(--green-5);
+ --color-green-6: var(--green-6);
+ --color-green-7: var(--green-7);
+ --color-green-8: var(--green-8);
+ --color-green-9: var(--green-9);
+ --color-green-10: var(--green-10);
+ --color-green-11: var(--green-11);
+ --color-green-12: var(--green-12);
+
+ /* amber */
+ --color-amber-1: var(--amber-1);
+ --color-amber-2: var(--amber-2);
+ --color-amber-3: var(--amber-3);
+ --color-amber-4: var(--amber-4);
+ --color-amber-5: var(--amber-5);
+ --color-amber-6: var(--amber-6);
+ --color-amber-7: var(--amber-7);
+ --color-amber-8: var(--amber-8);
+ --color-amber-9: var(--amber-9);
+ --color-amber-10: var(--amber-10);
+ --color-amber-11: var(--amber-11);
+ --color-amber-12: var(--amber-12);
+
+ /* sky */
+ --color-sky-1: var(--sky-1);
+ --color-sky-2: var(--sky-2);
+ --color-sky-3: var(--sky-3);
+ --color-sky-4: var(--sky-4);
+ --color-sky-5: var(--sky-5);
+ --color-sky-6: var(--sky-6);
+ --color-sky-7: var(--sky-7);
+ --color-sky-8: var(--sky-8);
+ --color-sky-9: var(--sky-9);
+ --color-sky-10: var(--sky-10);
+ --color-sky-11: var(--sky-11);
+ --color-sky-12: var(--sky-12);
+}
+
+/* ---------------------------------------------------------------------------
+ * v2 boundary scope
+ *
+ * All v2 tokens resolve from this scope at runtime. Colors reference the Radix
+ * variables so light/dark switching is automatic. Future non-color tokens
+ * (fonts, spacing, shadows, radii) will go here too.
+ * ------------------------------------------------------------------------- */
+[data-theme="v2"] {
+ /* sand */
+ --color-sand-1: var(--sand-1);
+ --color-sand-2: var(--sand-2);
+ --color-sand-3: var(--sand-3);
+ --color-sand-4: var(--sand-4);
+ --color-sand-5: var(--sand-5);
+ --color-sand-6: var(--sand-6);
+ --color-sand-7: var(--sand-7);
+ --color-sand-8: var(--sand-8);
+ --color-sand-9: var(--sand-9);
+ --color-sand-10: var(--sand-10);
+ --color-sand-11: var(--sand-11);
+ --color-sand-12: var(--sand-12);
+
+ /* gold */
+ --color-gold-1: var(--gold-1);
+ --color-gold-2: var(--gold-2);
+ --color-gold-3: var(--gold-3);
+ --color-gold-4: var(--gold-4);
+ --color-gold-5: var(--gold-5);
+ --color-gold-6: var(--gold-6);
+ --color-gold-7: var(--gold-7);
+ --color-gold-8: var(--gold-8);
+ --color-gold-9: var(--gold-9);
+ --color-gold-10: var(--gold-10);
+ --color-gold-11: var(--gold-11);
+ --color-gold-12: var(--gold-12);
+
+ /* red */
+ --color-red-1: var(--red-1);
+ --color-red-2: var(--red-2);
+ --color-red-3: var(--red-3);
+ --color-red-4: var(--red-4);
+ --color-red-5: var(--red-5);
+ --color-red-6: var(--red-6);
+ --color-red-7: var(--red-7);
+ --color-red-8: var(--red-8);
+ --color-red-9: var(--red-9);
+ --color-red-10: var(--red-10);
+ --color-red-11: var(--red-11);
+ --color-red-12: var(--red-12);
+
+ /* green */
+ --color-green-1: var(--green-1);
+ --color-green-2: var(--green-2);
+ --color-green-3: var(--green-3);
+ --color-green-4: var(--green-4);
+ --color-green-5: var(--green-5);
+ --color-green-6: var(--green-6);
+ --color-green-7: var(--green-7);
+ --color-green-8: var(--green-8);
+ --color-green-9: var(--green-9);
+ --color-green-10: var(--green-10);
+ --color-green-11: var(--green-11);
+ --color-green-12: var(--green-12);
+
+ /* amber */
+ --color-amber-1: var(--amber-1);
+ --color-amber-2: var(--amber-2);
+ --color-amber-3: var(--amber-3);
+ --color-amber-4: var(--amber-4);
+ --color-amber-5: var(--amber-5);
+ --color-amber-6: var(--amber-6);
+ --color-amber-7: var(--amber-7);
+ --color-amber-8: var(--amber-8);
+ --color-amber-9: var(--amber-9);
+ --color-amber-10: var(--amber-10);
+ --color-amber-11: var(--amber-11);
+ --color-amber-12: var(--amber-12);
+
+ /* sky */
+ --color-sky-1: var(--sky-1);
+ --color-sky-2: var(--sky-2);
+ --color-sky-3: var(--sky-3);
+ --color-sky-4: var(--sky-4);
+ --color-sky-5: var(--sky-5);
+ --color-sky-6: var(--sky-6);
+ --color-sky-7: var(--sky-7);
+ --color-sky-8: var(--sky-8);
+ --color-sky-9: var(--sky-9);
+ --color-sky-10: var(--sky-10);
+ --color-sky-11: var(--sky-11);
+ --color-sky-12: var(--sky-12);
+}