---
name: platform-make-theme
description: "Author a NET-NEW appearance for apps/mod-cms — a Palette (token VALUES, light+dark), a Theme (presentation: Shell chrome + optional per-screen override templates), or both. ALWAYS starts with a SCAN/REUSE-GATE: an existing PALETTES entry or THEME_REGISTRY_META theme that already satisfies the request → STOP and reuse it (never author a near-duplicate). The skill's whole value is four BLOCKING gates an authored appearance MUST pass — a11y contrast (engine AA + mod-cms 12:1 ink floor, HEX-only), token-purity lint (no raw color literals in theme components), override functional-contract (an override renders every data-bound sub-region the default does), registry honesty (fail-closed construction guard). Host-owned (apps/mod-cms), NOT a @platform-modules package. Use on /platform-make-theme or when asked to add a palette/theme/colour-scheme/appearance to mod-cms. Holds no code rules of its own — points to the bundled coding standard, the SP2 theme-contract, and mod-ui-enforcement."
---

# Make Theme — Net-New Appearance Authoring (scan-gated, gate-enforced)

**Audience: AI coding agents first.** Optimize for activation, not prose — keep ladders as ladders.

This skill authors ONE appearance for **`apps/mod-cms`** (host-owned — NOT a `@platform-modules/*` package). Two orthogonal axes:

- **Palette** = token VALUES (`ColorTokens` per light/dark mode + shape tokens). Added to `MOD_CMS_PALETTE_SET.palettes` in `apps/mod-cms/src/lib/theme.ts`.
- **Theme** = PRESENTATION (a `ModTheme`: a Shell rendering visible chrome + optional per-screen override templates). Registered in `apps/mod-cms/src/themes/registry.ts` + `THEME_REGISTRY_META`.

Palette and theme are independent — author one, the other, or both. The skill's value = the **scan-gate** (don't author a near-duplicate) + the **four BLOCKING gates** (§4) every authored artifact MUST pass.

> Holds **no code rules of its own.** The coding standard is bundled verbatim → `references/coding-standard.generated.md` (build-time concat of `docs/standards/coding-standard.md`; never re-inline it, never read the source copy — read the generated one). Contract shapes → `apps/mod-cms/src/lib/theme-contract.ts` (read it; do not invent the shape). Token-purity → the `.astro`-capable canary `apps/mod-cms/src/themes/token-purity.canary.test.ts` (§4.2; in `pnpm gate`). HEX-only + 12:1 ink floor → `AGENTS.md`. Program rationale (Palette/theme split, structural join) → `docs/specs/2026-06-22-theme-system-program-design.md`. This file is *process*, not doctrine.

## Step 0 — SCAN / REUSE-GATE (BLOCKING — before authoring anything)

Does an existing artifact already satisfy the request?

1. **Palette:** read `PALETTES` / `MOD_CMS_PALETTE_SET.palettes` in `apps/mod-cms/src/lib/theme.ts`.
2. **Theme:** read `THEME_REGISTRY_META` in `apps/mod-cms/src/lib/theme.ts` + the registered themes under `apps/mod-cms/src/themes/`.
3. **An existing entry satisfies it → STOP, reuse it.** YAGNI — never author a near-duplicate. A reuse outcome is the gate **succeeding**.
4. Only a genuinely new appearance need proceeds to Step 1.

## Step 1 — Choose grain

State the concrete artifact name(s) before writing one line:
- **Palette** only — a new colour-value set (e.g. `solarized`).
- **Theme** only — a new presentation over existing palettes.
- **Both** — a palette + a theme that pairs with it.

## Step 2 — Author (read the worked example FIRST — never invent the shape)

Read these before writing, as the worked example:
- `apps/mod-cms/src/lib/theme-contract.ts` — the contract: `ModTheme`, `ThemeComponent`, `ScreenId`, `SCREEN_IDS`, `WIRED_SCREENS` (= `Set(['post'])`), `ThemeMeta`, `resolveShell`, `resolveScreen`, `findUnwiredOverrides`.
- `apps/mod-cms/src/themes/default/` and `apps/mod-cms/src/themes/editorial/` + `apps/mod-cms/src/themes/registry.ts` — two real themes + the fail-closed construction guard.
- `references/coding-standard.generated.md` — the bundled standard (the ONLY copy you read; never the `docs/standards/` source).

**Author rules (HARD FLOOR):**
- A **Palette** uses **HEX values only** — the engine contrast fns are HEX-only (mod-cms HEX seam mandate, `AGENTS.md`).
- A **Theme** Shell + override components reference **contract token names** (`--mod-color-*`, shape tokens) only — **NEVER** a raw color literal (`oklch()` / `#hex` / `rgb()`). Raw color = the theme is not palette-reactive and not portable.
- A **Theme override** receives the **same props** the host-default body receives. It chooses arrangement, NEVER content.

## Step 3 — GATE (BLOCKING — §4; the skill's whole value)

Run every applicable §4 gate. Any fail → fix and re-run. **NEVER register a failing artifact.** All four §4 gates are **vitest** tests under `pnpm gate`: §4.1 contrast, §4.2 token-purity canary, §4.3 override-contract, §4.4 registry honesty. No separate command — `pnpm gate` runs them all.

## Step 4 — Register + wire

- **Palette →** add to `MOD_CMS_PALETTE_SET.palettes` in `apps/mod-cms/src/lib/theme.ts`.
- **Theme →** add to `apps/mod-cms/src/themes/registry.ts` AND `THEME_REGISTRY_META`. The §4.4 construction guard MUST pass at import.

## Step 5 — Test

- **Palette:** a vitest asserting the §4.1 contrast gate passes for **every** mode (light + dark). Model after `apps/mod-cms/src/themes/render.test.ts`.
- **Theme:** the §4.4 registry honesty guard + (for an override) a test asserting the override renders **every** data-bound sub-region the default renders (§4.3).
- **Test placement (learned in SP5 dogfood — get this right or the gate doesn't run):**
  - §4.3 override-contract + §4.4 registry-honesty tests **import the `.astro` render graph** → they go in the **render** vitest project (add the new test to `RENDER_TESTS` in BOTH `vitest.config.ts` and `vitest.render.config.ts`). A render-graph test placed in the unit project fails to resolve the `.astro` import.
  - The §4.2 token-purity canary is a **pure file-read** (reads `.astro` as text, imports none) → it stays in the **unit** project as a plain `src/**/*.test.ts`, no config edit.
- Run `pnpm gate` — covers all four §4 gates (§4.1 contrast, §4.2 token-purity canary, §4.3 override-contract, §4.4 registry honesty) + the skill-reference drift check. Green before done.

---

## 4. The gates (BLOCKING — register nothing that fails)

### 4.1 a11y contrast gate (palette)

Every authored palette MUST pass, for **each** mode (light + dark): the engine **AA** check **AND** the mod-cms **12:1 ink-on-bg editorial floor** (`AGENTS.md`).

- Mechanism: the engine contrast fns from the **`@platform-modules/ui-tokens/theme-engine` subpath** — `parseHex` / `contrastRatio` / `relativeLuminance` / `validateContrast` / `validateContrastTokens`. **DO NOT** import them from the bare `@platform-modules/ui-tokens` barrel — it exports only token symbols (`TOKEN_COLORS`, `colorVar`, …) and does NOT re-export the engine.
- HEX-only inputs (the fns reject non-hex).
- A failing pair is **NOT admitted** (Hard floor — a11y is never on the chopping block, `CLAUDE.md §1`).
- Use only **validated pairings** — `--mod-color-accent-fg` on `--mod-color-accent` is the engine's only validated text-on-accent pair. NEVER invent an unvalidated pairing.

### 4.2 token-purity gate (theme)

A theme's Shell + override components reference **contract token names only** (`var(--mod-color-*)`, shape tokens) — **NEVER** a raw color literal (`oklch()` / `#hex` / `rgb()` / `hsl()`). Raw color = the theme is not palette-reactive and not portable.

Mechanism: the **token-purity canary** `apps/mod-cms/src/themes/token-purity.canary.test.ts` — a vitest that reads every `.astro`/`.tsx` theme component under `src/themes/**` as TEXT and asserts **ZERO** raw color literals, with an **anti-vacuity guard** (asserts the scan saw the expected file count, so it can never pass by scanning nothing). It runs in the **unit** vitest project → **covered by `pnpm gate`**, no separate command. A new theme falls under it automatically (it scans the whole `src/themes/**` tree) — no per-theme wiring.

> Why a canary, NOT the ast-grep lint: theme components are `.astro`; the `mod-ui-no-layout-raw-color.yml` rule is `language: tsx` and ast-grep has no `.astro` grammar, so an ast-grep scan of a theme dir parses ZERO files and passes **VACUOUSLY** (the SP5 dogfood proved this — `ast-grep scan <theme-dir>` returned 0 hits without inspecting any `.astro`). The text-scan canary is `.astro`-capable and lands in the gate. The tsx ast-grep rule stays the enforcement for `.tsx` UI primitives elsewhere — it was never the right mechanism for `.astro` themes.

BLOCKING — any hit fails the gate. This is what makes a theme portable + palette-reactive.

### 4.3 override functional-contract gate (theme)

An override-template renders **every data-bound sub-region the host-default body renders** — for `post`: prose AND comments AND metadata, **NEVER** a subset. Dropping a sub-region is a silent feature regression the moment the theme is selected. The override receives the **same props** the default receives; it chooses arrangement, not content. BLOCKING — a prose-only override of a comments-bearing screen fails the gate.

### 4.4 registry honesty gate (theme)

The theme registers in `apps/mod-cms/src/themes/registry.ts`, which runs the fail-closed **construction guard**: registry keys exactly equal `THEME_REGISTRY_META` ids, AND `findUnwiredOverrides(theme) === []` for every theme (no override on a screen not in `WIRED_SCREENS`). A lying theme is a deterministic import-time throw — NEVER a silent no-op or a user 500. Pass this guard before declaring done.

---

## 5. The bundled coding standard — single source, drift-gated

Read `references/coding-standard.generated.md` for all code rules — it is a **build-time concat** of `docs/standards/coding-standard.md` (the single source).

- **NEVER** hand-edit the generated file; **NEVER** re-inline the standard's prose into this SKILL.md; **NEVER** read/copy the `docs/standards/` source as a second copy.
- Regenerate: `pnpm build:skill-references`. The `pnpm check:skill-references` drift mode is wired into the standing `pnpm gate` — a stale copy fails the gate (single source is *enforced*, not trusted).
- This is build-time concat, **NOT a symlink** (a symlink breaks across skill-copy / Windows / agent-sandbox portability and rots silently).

## 6. MCP scaffold/configure/test — Phase-2 (DEC-1)

The **manual authoring procedure above, over existing tooling** (the contrast vitest, the registry construction guard, the `mod-ui-enforcement` lint, the `pnpm gate` chain) is the **live path** — fully usable today with zero MCP.

An MCP-driven scaffold/configure/test surface is the **documented Phase-2 upgrade path** (CLAUDE.md — never build Phase-2 speculatively), NOT a shipped dependency. When the mod MCP ships, the scaffold/configure/test steps gain an automated variant **without changing the gates** above.
