| name | styles |
| description | 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. |
| allowed-tools | AskUserQuestion, Bash, Edit, Glob, Grep, Read, Write |
| metadata | {"version":"0.3.0"} |
styles
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.
Step 0 — Exit plan mode
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.
CSS Token Convention
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.
Semantic role names
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 surface
Text
--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 color
Focus
--color-focus-ring (required) — focus indicator color (inputs, buttons)
Full role catalog with contrast pairings is in references/role-catalogue.md.
Required Contrast Pairings (WCAG 2.2 AA)
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.
Authoring flow
- Generate or edit a CSS theme file directly.
/theme-create and /theme-brand write light.css / dark.css / brand-*.css using the convention above. /theme-update edits role values in place.
- The user never authors JSON. No
theme.tokens.json to write or maintain.
- Round-trip scripts (
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.
- Validation runs automatically at the end of every theme command (no separate step). Failures are surfaced inline.
Dimension & typography tokens (DESIGN.md parity)
Beyond the 18 --color-* roles, the token layer carries three optional,
mode-independent kinds — spacing, rounded, and typography — that
bring it to parity with DESIGN.md:
- Emitted to
space-radius.css (--space-*, --radius-*) and
typography.css (--font-<role>-{family,size,weight,line,tracking}),
each a single :root block (no light/dark split).
- The prefixes are not drop-in Tailwind v4
@theme names — acss-kit uses
--space-* and composite --font-<role>-*, vs Tailwind's --spacing-* and
split --text-* / --font-weight-* / --leading-* / --tracking-* (only
--radius-* matches) — so a DESIGN.md css-tailwind export needs a name
adapter, not a mechanical prefix remap.
tokens_to_css.py emits these files only when the input JSON carries
spacing / rounded / typography (additive — colors-only input is
unchanged). css_to_tokens.py round-trips them. Structural validation
(units, scale completeness, typography sub-props) is in
${CLAUDE_PLUGIN_ROOT}/scripts/validate_tokens.py.
- Default scales ship at
${CLAUDE_PLUGIN_ROOT}/assets/tokens/space-radius.css
and typography.css for projects with no DESIGN.md. Full schema:
references/theme-schema.md.
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 / 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.
Workflow
- Validate the seed is a 3- or 6-digit hex color. If invalid, halt with usage hint.
- Run
${CLAUDE_PLUGIN_ROOT}/scripts/generate_palette.py <hex-color> --mode=<mode> (default both). Capture JSON stdout.
- If exit code 1 (
"reasons" non-empty), print the reasons and halt.
- Determine output directory. If
src/styles/theme/ exists in the project, use it. Otherwise ask the developer where to write theme files.
- Run
${CLAUDE_PLUGIN_ROOT}/scripts/tokens_to_css.py --stdin --out-dir=<dir> piping the palette JSON. This writes light.css and/or dark.css as raw role definitions (--color-primary: #hex;). Definitions must be raw — a self-referential var(--x, …) definition is a CSS dependency cycle that computes to guaranteed-invalid and never applies; the var(--token, <fallback>) convention is for consumers (components/utilities), not theme definitions.
- Run
${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.
- Regenerate bridge — if
${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).
- Print a summary: files written, primary color, top contrast ratios.
References to load
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.
Workflow
- Validate
<name> is a lowercase slug (alphanumeric + hyphens). If not, suggest a corrected slug.
- Determine output directory (same as
/theme-create).
- If
--from is provided:
- Run
${CLAUDE_PLUGIN_ROOT}/scripts/generate_palette.py <hex> --mode=brand. Capture brand overrides JSON.
- Write
brand-<name>.css using the :root (light) and [data-theme="dark"] overrides.
- If
--from is not provided:
- Copy
${CLAUDE_PLUGIN_ROOT}/assets/brand-template.css to brand-<name>.css. Instruct the developer to replace the placeholder values.
- Run
${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).
- Print the file path and instruct the developer to add an import after
light.css and dark.css in their entry file.
References to load
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.
Workflow
- Confirm
<file> exists and its name matches light.css, dark.css, or brand-*.css. Halt if not.
- Parse the developer's role=value arguments. Each must be of the form
--color-<role>=#<hex>.
- For each update:
a. Use Edit to replace the existing hex value for that role in-place, preserving comments and file structure.
b. After each edit, run
${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.
- Regenerate bridge — after all accepted edits, run
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.
- Print a final table: role / old value / new value / accepted|reverted.
References to load
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.
Workflow
- Detect input type:
figma.com URL → delegate to user-level figma-design-tokens skill.
- File path (png/jpg/webp/gif) → delegate to user-level
design-token-extractor skill.
- Neither → halt with usage hint.
- Both extractors return at minimum a
primary hex color (and optionally secondary, accent, background). Map the primary value to the seed color.
- Run
${CLAUDE_PLUGIN_ROOT}/scripts/generate_palette.py <primary-hex> --mode=both. Capture JSON.
- If the extractor also returned
secondary or accent colors, AskUserQuestion: "Should I also scaffold a brand preset using the secondary color?" If yes, run the brand flow inline.
- Write theme CSS via
${CLAUDE_PLUGIN_ROOT}/scripts/tokens_to_css.py. Validate with ${CLAUDE_PLUGIN_ROOT}/scripts/validate_theme.py.
- Optionally write
theme.tokens.json alongside the CSS files:
- Run
${CLAUDE_PLUGIN_ROOT}/scripts/css_to_tokens.py on the written files and save the result.
- Print files written and a note about round-tripping via
scripts/css_to_tokens.py.
References to load
references/palette-algorithm.md
references/theme-schema.md — JSON output format and round-trip contract.
references/role-catalogue.md
/theme-from-design <DESIGN.md> [--out-dir=<dir>]
Purpose: Generate a full theme — colors (light.css/dark.css) plus
space-radius.css and typography.css — from a
DESIGN.md. This is the inbound
half of the DESIGN.md ↔ acss-kit bridge (Workstream A; see
docs/plans/design-md-token-parity.md and the proposal's Appendix A mapping).
Requires Node/npx (Route 1, decided): the scripts shell
npx @google/design.md export --format css-tailwind and … lint. If npx is
absent, halt with: "/theme-from-design needs Node/npx — install Node, or author the theme with /theme-create."
Workflow
- Validate the DESIGN.md. Run
python3 ${CLAUDE_PLUGIN_ROOT}/scripts/validate_design_md.py <DESIGN.md>. Parse the JSON ({ok, reasons, warnings}).
ok: false (exit 1) → print reasons and halt. A missing primary is a hard error (it seeds the OKLCH palette) — by decision, stricter than the upstream linter's warning.
- Surface any
warnings (contrast, orphaned tokens, …) but continue.
- Determine the output directory (same rule as
/theme-create: prefer src/styles/theme/, else ask).
- Generate tokens. Run
python3 ${CLAUDE_PLUGIN_ROOT}/scripts/design_md_to_tokens.py <DESIGN.md> (it shells the css-tailwind export, maps Material-3 names → our 18 roles per Appendix A, synthesizes the roles M3 omits — success/warning/info/focus-ring — via the OKLCH generator, and lifts spacing/rounded/typography). Capture JSON stdout; the note: lines on stderr report which roles were synthesized. Exit 1 → print the reason and halt.
- Write the CSS. Pipe the tokens JSON to
python3 ${CLAUDE_PLUGIN_ROOT}/scripts/tokens_to_css.py --stdin --out-dir=<dir> → writes light.css, dark.css, space-radius.css, typography.css.
- Validate the output. Run
validate_theme.py <dir> (WCAG contrast) and validate_tokens.py on the tokens JSON (dimension units / scale completeness / typography sub-props). Report failures as warnings — generation is complete; the developer adjusts the source DESIGN.md or tunes via /theme-update.
- Regenerate the bridge — if
assets/utilities/token-bridge.css exists, run generate_bridge.py exactly as /theme-create step 6.
- Verify integration — run
verify_integration.py <project_root> (the "Integration verification" section below).
- Summary — files written, which roles were mapped vs synthesized, top contrast ratios, and a reminder that dark mode is OKLCH-mirrored (DESIGN.md is mode-thin).
References to load
- The proposal's Appendix A (Material-3 → our-roles table) — the mapping
design_md_to_tokens.py implements.
references/role-catalogue.md, references/palette-algorithm.md (synthesis), references/theme-schema.md (the token JSON shape).
/design-export [--format=design-md|dtcg|tailwind] [--dir=<dir>] [--name=<Brand>]
Purpose: The outbound half of the bridge — publish the project's theme as
a DESIGN.md (or, via the upstream
CLI, DTCG / Tailwind). This is the import-into-DESIGN.md path the upstream CLI
lacks, turning our generator into a DESIGN.md producer. See
docs/plans/design-md-spec-alignment.md (§Round-trip & export, Appendix A).
Semantic round-trip, not lossless: we emit our 18 --color-* roles under
DESIGN.md token names (inverse of Appendix A). M3 ladder tokens we do not model
(surface-tint, *-container pairs, *-fixed*) are not reproduced; roles with
no M3 slot (success, warning, focus-ring, text-subtle) keep our names —
the adapter carries matching candidates so a DESIGN.md we exported round-trips
them (an external M3 DESIGN.md omits them and they re-synthesize). State this in
the summary.
--format=design-md (default) is pure Python — no Node needed.
--format=dtcg|tailwind shells npx @google/design.md export and requires
Node/npx; halt with "--format=dtcg|tailwind needs Node/npx — use --format=design-md, or install Node." if absent.
Workflow
- Locate the theme dir (
--dir, else the /theme-create rule: prefer src/styles/theme/, else ask). It must contain light.css (and ideally space-radius.css, typography.css).
design-md path: run python3 ${CLAUDE_PLUGIN_ROOT}/scripts/tokens_to_design_md.py --dir=<dir> --name=<Brand> — it shells css_to_tokens.py to assemble the tokens JSON, applies the inverse mapping, and writes a DESIGN.md (YAML front-matter + a prose skeleton with the canonical ## sections and TODO rationale placeholders). Write stdout to <dir>/DESIGN.md (or the user's path). Exit 1 → print the note: reason (e.g. no light-mode colors) and halt.
dtcg/tailwind path: first ensure a DESIGN.md exists (run step 2 if not), then shell npx @google/design.md export --format dtcg|json-tailwind <DESIGN.md> and write its output. These formats are produced by the upstream CLI for free.
- Validate (optional — needs
npx). validate_design_md.py shells npx @google/design.md lint, so only run it if npx is available, and treat it as a courtesy lint (warnings only — the emitted file is valid by construction: a primary role is always present, since our theme requires --color-primary). Skip this step when npx is absent — never halt the pure-Python design-md export on it.
- Summary — file(s) written, the mapped-vs-kept-name color roles, and the round-trip caveat (dark mode is not represented in standard DESIGN.md front-matter; only
light is exported).
References to load
- Appendix A (our roles → DESIGN.md names is its inverse) and the §Round-trip & export section of
docs/plans/design-md-spec-alignment.md.
references/theme-schema.md (the token JSON shape css_to_tokens.py emits).
/theme-from-figma <figma-url|fileKey> [--node=<nodeId>] [--out-dir=<dir>]
Purpose: Generate a theme directly from a Figma file's variables (design
tokens), via the Figma MCP server — the standards-based evolution of the
/theme-extract Figma path. This is the inbound half of the Figma ⇄ DESIGN.md ⇄
theme bridge (Workstream C; see docs/plans/design-md-spec-alignment.md
Appendix B). After the MCP call, the pipeline is pure Python — no Node/npx.
Workflow
- Resolve the file. Parse the
fileKey (and optional nodeId) from the Figma URL (figma.com/design/:fileKey/...?node-id=:nodeId, converting -→: in the node id). If no URL/key is given, ask.
- Pull variables. Call the Figma MCP
get_variable_defs (fileKey, nodeId) → a flat map like { "color/primary": "#855300", "spacing/md": "24px", … }. If the server returns nothing, fall back to /theme-extract (image color extraction) and say so.
- Map to tokens. Pipe that JSON to
python3 ${CLAUDE_PLUGIN_ROOT}/scripts/figma_to_tokens.py --stdin — it normalizes Figma's category/name variables into the css-tailwind form and reuses the same Appendix A adapter as /theme-from-design (Material-3/Figma color names → our 18 roles, OKLCH gap synthesis, spacing/rounded/typography lift). Capture JSON stdout; note: lines on stderr report synthesized roles. Exit 1 (no resolvable color/primary) → print the reason and halt.
- Write + gate the CSS. Pipe the tokens JSON to
tokens_to_css.py --stdin --out-dir=<dir>, then run validate_theme.py <dir> (WCAG) and validate_tokens.py — same as /theme-from-design steps 4–5.
- Regenerate the bridge / verify integration — as
/theme-create steps 6–7.
- (Optional) DESIGN.md artifact. To also publish a DESIGN.md from the Figma tokens, pipe the tokens JSON through
tokens_to_design_md.py --stdin (Figma → DESIGN.md, per Appendix B).
- (Optional) Code Connect out. To push our component ↔ Figma-node bindings back so designers see which code implements each design node, use the Figma MCP
get_code_connect_map to inspect existing links and add_code_connect_map to add component-<name> ↔ node mappings (code→design direction). Confirm with the user before writing to their Figma file.
- Summary — files written, mapped vs synthesized roles, top contrast ratios, and whether a DESIGN.md / Code Connect push was performed.
References to load
- Appendix A (the shared color mapping) and Appendix B (the Figma MCP I/O contract) in
docs/plans/design-md-spec-alignment.md.
references/role-catalogue.md, references/palette-algorithm.md (synthesis).
/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.
Input resolution
Resolve <color> to a 6-digit hex string before calling the script:
- Hex value (
#rrggbb or #rgb) — use directly, no lookup needed.
- Theme role name (
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."
- CSS named color (
red, cornflowerblue, tomato, etc.) — resolve to hex using the W3C CSS Color 4 named-color table (Claude knows these). Example: red → #ff0000, cornflowerblue → #6495ed.
- Anything else — halt with the usage hint.
Default --name:
- Theme role input → use the role name (e.g.
primary → --color-primary-50)
- Named color input → use the color name (e.g.
red → --color-red-50)
- Hex input → use
scale unless --name is provided
Workflow
-
Resolve <color> to hex per the rules above.
-
Run ${CLAUDE_PLUGIN_ROOT}/scripts/generate_color_scale.py <hex> --name=<name> --format=both. Capture stdout.
- If exit code non-zero, print the stderr message and halt.
- The script emits two sections separated by a blank line: JSON first, then the CSS block. Split on the first blank line to isolate each section.
-
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 to load
references/palette-algorithm.md — OKLCH color space and gamut clamping behaviour.
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 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. |
Style-Tune Mode — Theme-Layer Adjustment
Invoked by /style-tune when the subject resolves to a theme role. See style-tune/SKILL.md Step A for intent parsing and dispatch.
ST-B. Locate theme files
Locate src/styles/theme/light.css and src/styles/theme/dark.css.
- Both exist → edit both with the same OKLCH delta (auto-mirror — same perceptual shift, mode-appropriate hex per file).
- Only one exists → edit that one.
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 brand'."
- No theme files found → halt: "No theme files at
src/styles/theme/. Run /theme-create #seedhex first."
ST-C. Compute theme deltas
For each (role, delta) resolved in style-tune/SKILL.md Step A:
- Read the current hex from each in-scope file via
python3 ${CLAUDE_PLUGIN_ROOT}/scripts/css_to_tokens.py <theme-file>.
- Compute the new hex via
python3 ${CLAUDE_PLUGIN_ROOT}/scripts/oklch_shift.py <currentHex> --hue=±deg --chroma=×float --lightness=±float.
- Exit 0 (always when a hex is produced): use the returned
hex. If clamped: true, surface reasons in Step F as informational warnings and still apply.
- Exit 2: usage / IO error — surface stderr and skip that role.
- Paired-role rule: when shifting
--color-primary, always shift --color-primary-hover by the same delta. Treat both as a single atomic batch entry.
- Dark-mirror rule: when both files are in scope, run the same delta against each file's starting hex — the two files yield mode-appropriate hex values.
- Build the full plan as
(file, role, oldHex, newHex) tuples. Do not invoke /theme-update yet — pre-validate first.
ST-D0. Pre-validate the theme batch
- Create a tmp directory via
mktemp -d.
- Copy each in-scope theme file into the tmp dir and apply the proposed edits (preserve comments and whitespace).
- Run
python3 ${CLAUDE_PLUGIN_ROOT}/scripts/validate_theme.py <staged-file> on each staged copy.
- If any contrast pair fails on any file, halt the entire batch — write nothing. Print the failures and hint: "Re-run with a smaller delta, or pick an explicit hex via
/theme-update."
ST-D1. Apply theme edits
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.
ST-E. Post-apply checks
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."
Integration verification (all flows)
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).
- Exit 0 → theme is wired up. Done.
- Exit 1 → print the
reasons array as a numbered fix-up list. Do not auto-edit the entrypoint — the developer adds the import themselves.
- If
.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).
Error handling (all flows)
| 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" |