styles skill
The styles skill drives all four /theme-* commands. It defines the OKLCH palette math, the 18 semantic color role catalogue, the WCAG contrast pair validation contract, and the file format for theme output.
Trigger
Section titled “Trigger”Triggered by /theme-create, /theme-brand, /theme-extract, and /theme-update. Also invoked by the style-tune pilot skill when adjusting theme-layer tokens.
OKLCH color space
Section titled “OKLCH color space”All palette math uses OKLCH (Oklab Lightness Chroma Hue) rather than HSL or RGB. OKLCH advantages for palette generation:
- Perceptual uniformity — equal L step = equal perceived brightness change, regardless of hue
- Predictable contrast — WCAG relative luminance correlates closely with OKLCH L value
- Hue independence — you can independently adjust lightness without shifting the perceived color identity
The three channels:
- L — lightness (0% = black, 100% = white)
- C — chroma / saturation (0 = gray, higher = more saturated; typical brand colors: 0.12–0.25)
- H — hue angle in degrees (0° = red, 120° = green, 240° = blue, 270° = indigo)
Palette algorithm (step by step)
Section titled “Palette algorithm (step by step)”Given seed hex #4f46e5:
-
Parse → convert to OKLCH:
(55%, 0.22, 270°) -
Primary → use seed as
--color-primarybase. Run lightness search: find highest L where contrast ratio ≥ 4.5:1 against background (L 99%). Binary search between L 30% and L 70%. -
Backgrounds (light mode):
--color-background: L 99%, C seed*0.02, H seed (near-white, hue-tinted)--color-surface: L 97%, C seed*0.03, H seed--color-surface-raised: L 95%, C seed*0.04, H seed -
Text:
--color-text: L 16% (near-black, finds highest L with 4.5:1 vs background)--color-text-inverse: L 99% (same as background — white on colored surfaces)--color-text-muted: L 48% (AA against background: 4.5:1)--color-text-disabled: L 68% (slightly below AA threshold — intentional for disabled) -
Borders:
--color-border: L 88%, C seed*0.05, H seed (3.0:1 vs background — non-text threshold)--color-border-strong: L 72%, C seed*0.07, H seed -
Semantic hues (derived from seed chroma):
--color-danger: H 27° (red)--color-warning: H 80° (amber)--color-info: H 240° (blue)--color-success: H 155° (green)Chroma: seed chroma × 0.90–1.0 (clamped to gamut) -
State colors:
--color-primary-hover: L primary - 5, same C and H--color-focus-ring: L primary + 8, C primary - 0.02, same H -
Dark mode — symmetric inversion:
Backgrounds: L 12–20% (low lightness)Primary: find highest L with 4.5:1 vs dark background (typically L 65–75%)Text: L 92–96% (near-white)Semantic: same hues, adjusted L for dark background contrast
The 18 semantic color roles
Section titled “The 18 semantic color roles”Background roles
Section titled “Background roles”| Role | Light L | Dark L | Usage |
|---|---|---|---|
--color-background | 99% | 12% | Page canvas |
--color-surface | 97% | 16% | Cards, panels |
--color-surface-raised | 95% | 20% | Dropdowns, modals |
Border roles
Section titled “Border roles”| Role | Light L | Dark L | Usage |
|---|---|---|---|
--color-border | 88% | 30% | Default borders |
--color-border-strong | 72% | 45% | Emphasized/active borders |
Text roles
Section titled “Text roles”| Role | Light L | Dark L | Usage |
|---|---|---|---|
--color-text | 16% | 94% | Body text |
--color-text-inverse | 99% | 12% | Text on colored backgrounds |
--color-text-muted | 48% | 68% | Secondary / captions |
--color-text-disabled | 68% | 45% | Disabled state |
Brand roles
Section titled “Brand roles”| Role | H° | Usage |
|---|---|---|
--color-primary | seed | Primary actions, links |
--color-primary-hover | seed | Hover state on primary |
--color-focus-ring | seed | Keyboard focus outline |
Semantic roles
Section titled “Semantic roles”| Role | H° | Usage |
|---|---|---|
--color-danger | 27 | Errors, destructive actions |
--color-warning | 80 | Warnings, caution |
--color-info | 240 | Informational messages |
--color-success | 155 | Positive outcomes |
--color-danger
—
--color-warning
—
--color-info
—
--color-success
—
| Token | Value |
|---|---|
--color-danger | — |
--color-warning | — |
--color-info | — |
--color-success | — |
Optional roles (generated when hue range allows)
Section titled “Optional roles (generated when hue range allows)”| Role | H° | Usage |
|---|---|---|
--color-accent | seed +30° | Secondary brand accent |
--color-accent-hover | seed +30° | Hover on accent |
--color-brand-accent | seed -30° | Decorative brand element |
WCAG 2.2 AA contrast validation
Section titled “WCAG 2.2 AA contrast validation”The skill validates 10 contrast pairs against 4.5:1 (text) or 3.0:1 (non-text):
| Pair | Minimum | Category |
|---|---|---|
text / background | 4.5:1 | Text |
primary / background | 4.5:1 | Text / interactive |
text-inverse / primary | 4.5:1 | Text on brand |
danger / background | 4.5:1 | Text |
success / background | 4.5:1 | Text |
warning / background | 3.0:1 | Non-text indicator |
info / background | 4.5:1 | Text |
text-muted / background | 4.5:1 | Text |
border / background | 3.0:1 | Non-text indicator |
text / surface | 4.5:1 | Text on card |
If a pair fails, the skill adjusts the foreground lightness by ±2% and retries, up to 8 iterations. If it still can’t pass, it reports the failure and the closest achievable ratio.
Theme file format
Section titled “Theme file format”:root { /* ── Backgrounds ──────────────────────── */ --color-background: oklch(99% 0.004 270); --color-surface: oklch(97% 0.006 270); --color-surface-raised: oklch(95% 0.008 270);
/* ── Borders ──────────────────────────── */ --color-border: oklch(88% 0.014 270); --color-border-strong: oklch(72% 0.018 270);
/* ── Text ─────────────────────────────── */ --color-text: oklch(16% 0.012 270); --color-text-inverse: oklch(99% 0.004 270); --color-text-muted: oklch(48% 0.014 270); --color-text-disabled: oklch(68% 0.01 270);
/* ── Brand ────────────────────────────── */ --color-primary: oklch(55% 0.22 270); --color-primary-hover: oklch(48% 0.22 270); --color-focus-ring: oklch(63% 0.2 270);
/* ── Semantic ─────────────────────────── */ --color-danger: oklch(50% 0.22 27); --color-warning: oklch(56% 0.18 80); --color-info: oklch(55% 0.18 240); --color-success: oklch(48% 0.18 155);}CSS layer ordering
Section titled “CSS layer ordering”Theme files participate in the canonical @layer cascade. foundation.css declares the layer order at the top of every consumer project:
@layer foundation, components, utilities, theme;Generated light.css, dark.css, and brand-*.css must be imported after foundation.css. The cascade outcome — theme > utilities > components > foundation — means --color-* values from theme files always win over any primitive tokens declared in @layer foundation. This is by design: the foundation layer intentionally omits --color-* semantic roles (patch P1) so theme files hold the only source of truth for every --color-* variable.
Atomic write contract
Section titled “Atomic write contract”The skill follows a strict atomic write contract for all theme operations:
- Exit plan mode if the session is in plan mode —
validate_theme.pyruns via Bash and the temp / destination writes are also blocked under plan mode - Compute the full new theme in memory
- Write to a temp file in the system temp directory
- Run
validate_theme.pyon the temp file - Only if all checks pass → copy temp file to the real destination
- On any failure → discard temp file, report errors, leave real file unchanged
/color-scale workflow
Section titled “/color-scale workflow”/color-scale is documented in the styles skill and backed by generate_color_scale.py. The skill routes /color-scale inputs through three resolution stages before calling the script.
Input resolution
Section titled “Input resolution”| Input type | Resolution |
|---|---|
Hex value (#rrggbb or #rgb) | Used directly |
Theme role name (primary, background, surface, etc.) | Look up project’s light.css, grep for --color-<role>:, parse the oklch(...) value and convert to hex before generation. Halt if no theme file found. |
CSS named color (red, cornflowerblue, etc.) | Resolve to hex via the W3C CSS Color 4 named-color table |
| Anything else | Halt with usage hint |
Default --name when not supplied:
- Theme role input → role name (e.g.
primary→--color-primary-50) - Named color input → color name (e.g.
red→--color-red-50) - Hex input →
scale
Workflow
Section titled “Workflow”- Resolve
<color>to a 6-digit hex value per the table above. - Run
generate_color_scale.py <hex> --name=<name> --format=both. If non-zero exit, print stderr and halt. - Parse the JSON section (before the first blank line) to extract the 10
stepsentries. - Display the CSS block, then a Markdown scale table.
- Write to file only if the user explicitly requests it — display-only by default.
Error handling
Section titled “Error handling”| Situation | Action |
|---|---|
| Resolved hex is invalid | Halt: "<value>" is not a valid hex color. Use #rrggbb or #rgb. |
| Theme role not found in CSS | Halt: "--color-<role> not found in <file>. Check the role name or pass a hex directly." |
| No theme file in the project | Halt: "No theme file found. Run /theme-create first or pass a hex value directly." |
generate_color_scale.py exits non-zero | Print stderr and halt |