plugins/acss-kit/skills/styles/SKILL.md
Generate and update CSS themes for fpkit/acss projects — OKLCH light/dark palettes, brand presets, theme role edits, and token extraction from images or Figma.
npx skillsauth add shawn-sandy/acss-plugins stylesInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Theme generation and management for fpkit/acss projects. Routes between four flows depending on which slash command was invoked.
Token and role conventions: see references/role-catalogue.md and the CSS Token Convention below.
OKLCH palette algorithm: see references/palette-algorithm.md.
JSON Schema and round-tripping: see references/theme-schema.md. The JSON schema is internal to the round-trip scripts (tokens_to_css.py / css_to_tokens.py); the CSS Token Convention below is the user-facing authoring format.
Applies to every flow below (/theme-create, /theme-brand, /theme-update, /theme-extract). If the session is in plan mode, call ExitPlanMode before proceeding — the flows write theme CSS (light.css / dark.css / brand-*.css) and shell out to a per-flow subset of generate_palette.py, tokens_to_css.py, validate_theme.py, css_to_tokens.py, and verify_integration.py. Specifically: /theme-create and /theme-extract exercise the full generate → write → validate pipeline; /theme-brand with --from runs generate_palette.py then writes the brand file and validates, and without --from copies assets/brand-template.css and validates; /theme-update edits CSS in place and validates. Every flow then runs verify_integration.py per the "Integration verification (all flows)" section below. Plan mode blocks every one of those Write/Edit/Bash calls.
Stay in plan mode only when it is absolutely necessary — i.e. the user explicitly asked for a preview ("show me the palette first", "don't write the theme yet"). In that case, narrate which roles would be generated, which files would be written, and which validator would run — without invoking Write/Edit/Bash — and wait for approval before re-entering this skill.
The authoring format for theme tokens is CSS custom properties — not JSON. Users edit light.css / dark.css / brand-*.css files directly; the JSON schema at ${CLAUDE_PLUGIN_ROOT}/assets/theme.schema.json is internal to the round-trip scripts and is not a user-facing contract. Existing CSS theme files remain byte-compatible with this convention.
assets/theme.schema.json $defs/palette declares 18 defined --color-* properties total: 15 required roles plus 3 optional roles (--color-surface-subtle, --color-text-subtle, --color-brand-accent). Names stay byte-compatible with the bundled CSS theme files — no renames, no removals, ever. Group them by purpose, matching ROLE_GROUPS in ${CLAUDE_PLUGIN_ROOT}/scripts/_tokens.py:
Backgrounds
--color-background (required) — page background--color-surface (required) — card / panel surface--color-surface-raised (required) — elevated surface (modals, popovers)--color-surface-subtle (optional) — table-stripe / hover surfaceText
--color-text (required) — body text--color-text-muted (required) — secondary text--color-text-inverse (required) — text on primary background--color-text-subtle (optional) — tertiary text (timestamps, footnotes)Borders
--color-border (required) — default border--color-border-strong (required) — emphasized border (form-field focus)Brand & semantic
--color-primary (required) — brand primary--color-primary-hover (required) — primary hover state--color-success (required) — success / valid state--color-warning (required) — caution state--color-danger (required) — destructive / error state--color-info (required) — informational state--color-brand-accent (optional) — secondary brand colorFocus
--color-focus-ring (required) — focus indicator color (inputs, buttons)Full role catalog with contrast pairings is in references/role-catalogue.md.
Every theme must pass these pairings — the validator at ${CLAUDE_PLUGIN_ROOT}/scripts/validate_theme.py checks them automatically on every /theme-create, /theme-brand, /theme-update, and /theme-extract:
| Foreground | Background | Min ratio | Why |
|---|---|---|---|
| --color-text | --color-background | 4.5:1 | Body text on page (WCAG 1.4.3) |
| --color-text-muted | --color-background | 4.5:1 | Secondary text on page (WCAG 1.4.3) |
| --color-text | --color-surface | 4.5:1 | Body text on cards/panels |
| --color-text-inverse | --color-primary | 4.5:1 | Label text on primary buttons |
| --color-text-inverse | --color-success | 4.5:1 | Success state buttons / badges |
| --color-text-inverse | --color-danger | 4.5:1 | Destructive buttons / error chips |
| --color-text-inverse | --color-warning | 4.5:1 | Warning chips / banners |
| --color-text-inverse | --color-info | 4.5:1 | Info chips / banners |
| --color-focus-ring | --color-background | 3:1 | Focus indicator on page (WCAG 1.4.11) |
| --color-border-strong | --color-surface | 3:1 | Form-field focus border (WCAG 1.4.11) |
The validator's full pair list (10 pairs at default thresholds) is in scripts/validate_theme.py:PAIRS. Any theme that fails one of these pairings should be revised — usually by adjusting the seed color or manually tuning the OKLCH lightness on the failing role.
/theme-create and /theme-brand write light.css / dark.css / brand-*.css using the convention above. /theme-update edits role values in place.theme.tokens.json to write or maintain.tokens_to_css.py, css_to_tokens.py) remain internal. They use the JSON schema to translate between the OKLCH palette generator's output and CSS, but the JSON shape is not a user-facing contract.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 / 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 (P1) so theme
files hold the only source of truth for every --color-* variable.
/theme-create <hex-color> [--mode=light|dark|both]Purpose: Generate light.css and/or dark.css under src/styles/theme/ from a seed color.
${CLAUDE_PLUGIN_ROOT}/scripts/generate_palette.py <hex-color> --mode=<mode> (default both). Capture JSON stdout.
"reasons" non-empty), print the reasons and halt.src/styles/theme/ exists in the project, use it. Otherwise ask the developer where to write theme files.${CLAUDE_PLUGIN_ROOT}/scripts/tokens_to_css.py --stdin --out-dir=<dir> piping the palette JSON. This writes light.css and/or dark.css with mandatory var(--x, <fallback>) syntax.${CLAUDE_PLUGIN_ROOT}/scripts/validate_theme.py <dir>. If contrast failures are found, print them as warnings and continue — generation is complete but the developer should adjust the seed or manually tune values.${CLAUDE_PLUGIN_ROOT}/assets/utilities/token-bridge.css exists, run python3 ${CLAUDE_PLUGIN_ROOT}/scripts/generate_bridge.py --theme-dir=<dir> --out=<projectRoot>/<utilitiesDir>/token-bridge.css so utility aliases reflect the new palette. Skip silently when no utilitiesDir is configured (i.e. detect_target.py --what=utilities returns source: none).references/palette-algorithm.md — OKLCH lightness targets and state-color hue offsets.references/role-catalogue.md — full role list and contrast targets./theme-brand <name> [--from=<hex-color>]Purpose: Scaffold brand-<name>.css with light and dark primary/accent overrides.
<name> is a lowercase slug (alphanumeric + hyphens). If not, suggest a corrected slug./theme-create).--from is provided:
${CLAUDE_PLUGIN_ROOT}/scripts/generate_palette.py <hex> --mode=brand. Capture brand overrides JSON.brand-<name>.css using the :root (light) and [data-theme="dark"] overrides.--from is not provided:
${CLAUDE_PLUGIN_ROOT}/assets/brand-template.css to brand-<name>.css. Instruct the developer to replace the placeholder values.${CLAUDE_PLUGIN_ROOT}/scripts/validate_theme.py <brand-file>. Report contrast results. Failures are warnings only (the brand file's primary is validated in context of the project's existing light.css background, which may not be present here).light.css and dark.css in their entry file.references/palette-algorithm.md — brand mode generation.references/role-catalogue.md — which roles are allowed in brand files./theme-update <file> <--color-role=#hex> [...]Purpose: Edit specific role values in an existing theme file and re-validate.
<file> exists and its name matches light.css, dark.css, or brand-*.css. Halt if not.--color-<role>=#<hex>.${CLAUDE_PLUGIN_ROOT}/scripts/validate_theme.py <file>.
c. If any contrast pair fails: print the failure, revert the edit for that role (restore original value), and continue with remaining roles.python3 ${CLAUDE_PLUGIN_ROOT}/scripts/generate_bridge.py --theme-dir=<dir> --out=<projectRoot>/<utilitiesDir>/token-bridge.css so utility aliases stay in sync. Skip silently when no utilitiesDir is configured.references/role-catalogue.md — contrast thresholds per pair./theme-extract <image-path|figma-url>Purpose: Extract brand colors from an image or Figma design and generate theme CSS.
figma.com URL → delegate to user-level figma-design-tokens skill.design-token-extractor skill.primary hex color (and optionally secondary, accent, background). Map the primary value to the seed color.${CLAUDE_PLUGIN_ROOT}/scripts/generate_palette.py <primary-hex> --mode=both. Capture JSON.secondary or accent colors, AskUserQuestion: "Should I also scaffold a brand preset using the secondary color?" If yes, run the brand flow inline.${CLAUDE_PLUGIN_ROOT}/scripts/tokens_to_css.py. Validate with ${CLAUDE_PLUGIN_ROOT}/scripts/validate_theme.py.theme.tokens.json alongside the CSS files:
${CLAUDE_PLUGIN_ROOT}/scripts/css_to_tokens.py on the written files and save the result.scripts/css_to_tokens.py.references/palette-algorithm.mdreferences/theme-schema.md — JSON output format and round-trip contract.references/role-catalogue.md/color-scale <color> [--name=<name>] [--format=css|json|both]Purpose: Generate a 10-step OKLCH color scale (steps 50–900) from any seed color — a hex value, a CSS named color, or a theme role from the project's existing theme.
Resolve <color> to a 6-digit hex string before calling the script:
#rrggbb or #rgb) — use directly, no lookup needed.background, primary, surface, text, border, etc.) — find the project's light.css (or dark.css if specified), grep for --color-<role>:, and extract the hex fallback from the var(--color-<role>, <hex>) value. If both files exist, default to light.css. If neither exists, halt with: "No theme file found. Run /theme-create first or pass a hex value directly."red, cornflowerblue, tomato, etc.) — resolve to hex using the W3C CSS Color 4 named-color table (Claude knows these). Example: red → #ff0000, cornflowerblue → #6495ed.Default --name:
primary → --color-primary-50)red → --color-red-50)scale unless --name is providedResolve <color> to hex per the rules above.
Run ${CLAUDE_PLUGIN_ROOT}/scripts/generate_color_scale.py <hex> --name=<name> --format=both. Capture stdout.
Parse the JSON section (everything before the first blank line) to extract the 10 steps entries.
Display results in this order:
a. CSS block — the :root { … } output from the script (ready to paste or write to a file).
b. Scale table — a compact Markdown table:
| Step | Hex | OKLCH |
|------|-----|-------|
| 50 | #f3f5fc | oklch(0.971 0.010 273.4) |
| … | … | … |
| 900 | #050128 | oklch(0.137 0.080 276.5) |
If the user asked to write the scale to a file, write to the path they specified. Otherwise display only — no files written unless explicitly requested.
references/palette-algorithm.md — OKLCH color space and gamut clamping behaviour.| Situation | Action |
|---|---|
| Resolved hex is invalid | Halt: "<value>" is not a valid hex color. Use #rrggbb or #rgb. |
| Theme role not found in CSS file | Halt: "--color-<role> not found in <file>. Check the role name or pass a hex directly." |
| generate_color_scale.py exits 2 | Print stderr message and halt. |
Invoked by /style-tune when the subject resolves to a theme role. See style-tune/SKILL.md Step A for intent parsing and dispatch.
Locate src/styles/theme/light.css and src/styles/theme/dark.css.
brand-*.css files are NOT edited unless the user names the brand explicitly. When brand files are present and unnamed, surface a hint in Step F: "Brand <name> is present and unchanged. To tune it, say 'tune the <name> brand'."src/styles/theme/. Run /theme-create #seedhex first."For each (role, delta) resolved in style-tune/SKILL.md Step A:
python3 ${CLAUDE_PLUGIN_ROOT}/scripts/css_to_tokens.py <theme-file>.python3 ${CLAUDE_PLUGIN_ROOT}/scripts/oklch_shift.py <currentHex> --hue=±deg --chroma=×float --lightness=±float.
hex. If clamped: true, surface reasons in Step F as informational warnings and still apply.--color-primary, always shift --color-primary-hover by the same delta. Treat both as a single atomic batch entry.(file, role, oldHex, newHex) tuples. Do not invoke /theme-update yet — pre-validate first.mktemp -d.python3 ${CLAUDE_PLUGIN_ROOT}/scripts/validate_theme.py <staged-file> on each staged copy./theme-update."With the batch pre-validated, invoke /theme-update <file> --color-<role>=#<hex> [...] once per theme file. /theme-update runs validate_theme.py internally; given D0, this should always pass.
If /theme-update's post-write validation fires unexpectedly (rounding mismatch between oklch_shift.py and validate_theme.py), surface the discrepancy as a bug and suggest git revert.
After applying any colour shift, inspect the post-edit OKLCH of the tuned role. If chroma < 0.05 OR |hue − palette-derived hue| > 30°, append a drift hint to Step F: "Note: --color-<role> has drifted from its palette-derived value. Consecutive tunes accumulate hex round-trip noise — run /theme-create #seedhex to reset."
After any flow writes theme CSS to disk and validate_theme.py succeeds, run ${CLAUDE_PLUGIN_ROOT}/scripts/verify_integration.py <project_root> to confirm the entrypoint actually imports the generated theme. The verifier reads stack.entrypointFile from .acss-target.json (populated by detect_stack.py during /kit-add first-run, or after /setup).
reasons array as a numbered fix-up list. Do not auto-edit the entrypoint — the developer adds the import themselves..acss-target.json lacks a stack block, the verifier exits 1 with a hint to run detect_stack.py first. Surface that hint and continue (theme generation itself succeeded).| Situation | Action |
|---|---|
| Seed hex invalid | Halt with: "<value>" is not a valid hex color. Use #rrggbb or #rgb. |
| generate_palette.py exits 1 | Print each reason from "reasons" array. Halt. |
| Output file already exists | Halt with list of conflicts. Proceed only with --force. |
| Contrast failure during update | Revert that specific role. Continue with remaining roles. |
| Extractor skill unavailable | Halt with: "/theme-extract requires the design-token-extractor or figma-design-tokens skill. Run: /find-skills design-token" |
development
Internal orchestrator for /kit-create, /kit-list, /kit-sync, /kit-update and Form/HTML/Style-Tune modes. Per-component generation lives in component-<name> skills; do not auto-trigger for component requests.
data-ai
Use when the user asks to generate, create, or scaffold a Table — accessible data table with caption, scope headers, responsive scroll wrapper, and sortable column support.
tools
Use when the user asks to generate, create, or scaffold a Popover — accessible tooltip/popover using the Popover API with focus trap, aria-expanded, and light-dismiss.
tools
Use when the user asks to generate, create, or scaffold a Nav — accessible navigation landmark with aria-label, current-page link marking, and horizontal/vertical layout.