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.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:
space-radius.css (--space-*, --radius-*) and
typography.css (--font-<role>-{family,size,weight,line,tracking}),
each a single :root block (no light/dark split).@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.${CLAUDE_PLUGIN_ROOT}/assets/tokens/space-radius.css
and typography.css for projects with no DESIGN.md. Full schema:
references/theme-schema.md.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 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.${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/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."
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.warnings (contrast, orphaned tokens, …) but continue./theme-create: prefer src/styles/theme/, else ask).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.python3 ${CLAUDE_PLUGIN_ROOT}/scripts/tokens_to_css.py --stdin --out-dir=<dir> → writes light.css, dark.css, space-radius.css, typography.css.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.assets/utilities/token-bridge.css exists, run generate_bridge.py exactly as /theme-create step 6.verify_integration.py <project_root> (the "Integration verification" section below).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.
--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.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.light is exported).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.
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.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.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.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./theme-create steps 6–7.tokens_to_design_md.py --stdin (Figma → DESIGN.md, per Appendix B).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.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.
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" |
tools
Use when the developer wants to author CSS from a plain-language description — emits a raw CSS/SCSS rule or an inline style attribute, reusing the project's own custom properties. For a utility-class string instead, use create-utilities.
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.