Skip to content

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.

Triggered by /theme-create, /theme-brand, /theme-extract, and /theme-update. Also invoked by the style-tune pilot skill when adjusting theme-layer tokens.

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)

Given seed hex #4f46e5:

  1. Parse → convert to OKLCH: (55%, 0.22, 270°)

  2. Primary → use seed as --color-primary base. Run lightness search: find highest L where contrast ratio ≥ 4.5:1 against background (L 99%). Binary search between L 30% and L 70%.

  3. 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
  4. 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)
  5. 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
  6. 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)
  7. State colors:

    --color-primary-hover: L primary - 5, same C and H
    --color-focus-ring: L primary + 8, C primary - 0.02, same H
  8. 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
RoleLight LDark LUsage
--color-background99%12%Page canvas
--color-surface97%16%Cards, panels
--color-surface-raised95%20%Dropdowns, modals
RoleLight LDark LUsage
--color-border88%30%Default borders
--color-border-strong72%45%Emphasized/active borders
RoleLight LDark LUsage
--color-text16%94%Body text
--color-text-inverse99%12%Text on colored backgrounds
--color-text-muted48%68%Secondary / captions
--color-text-disabled68%45%Disabled state
RoleUsage
--color-primaryseedPrimary actions, links
--color-primary-hoverseedHover state on primary
--color-focus-ringseedKeyboard focus outline
RoleUsage
--color-danger27Errors, destructive actions
--color-warning80Warnings, caution
--color-info240Informational messages
--color-success155Positive outcomes
--color-danger
--color-warning
--color-info
--color-success
TokenValue
--color-danger
--color-warning
--color-info
--color-success

Optional roles (generated when hue range allows)

Section titled “Optional roles (generated when hue range allows)”
RoleUsage
--color-accentseed +30°Secondary brand accent
--color-accent-hoverseed +30°Hover on accent
--color-brand-accentseed -30°Decorative brand element

The skill validates 10 contrast pairs against 4.5:1 (text) or 3.0:1 (non-text):

PairMinimumCategory
text / background4.5:1Text
primary / background4.5:1Text / interactive
text-inverse / primary4.5:1Text on brand
danger / background4.5:1Text
success / background4.5:1Text
warning / background3.0:1Non-text indicator
info / background4.5:1Text
text-muted / background4.5:1Text
border / background3.0:1Non-text indicator
text / surface4.5:1Text 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.

: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);
}

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.

The skill follows a strict atomic write contract for all theme operations:

  1. Exit plan mode if the session is in plan mode — validate_theme.py runs via Bash and the temp / destination writes are also blocked under plan mode
  2. Compute the full new theme in memory
  3. Write to a temp file in the system temp directory
  4. Run validate_theme.py on the temp file
  5. Only if all checks pass → copy temp file to the real destination
  6. On any failure → discard temp file, report errors, leave real file unchanged

/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 typeResolution
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 elseHalt 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
  1. Resolve <color> to a 6-digit hex value per the table above.
  2. Run generate_color_scale.py <hex> --name=<name> --format=both. If non-zero exit, print stderr and halt.
  3. Parse the JSON section (before the first blank line) to extract the 10 steps entries.
  4. Display the CSS block, then a Markdown scale table.
  5. Write to file only if the user explicitly requests it — display-only by default.
SituationAction
Resolved hex is invalidHalt: "<value>" is not a valid hex color. Use #rrggbb or #rgb.
Theme role not found in CSSHalt: "--color-<role> not found in <file>. Check the role name or pass a hex directly."
No theme file in the projectHalt: "No theme file found. Run /theme-create first or pass a hex value directly."
generate_color_scale.py exits non-zeroPrint stderr and halt