remotion-colour-family
Builds a complete palette from minimal anchors. Reuses remotion-studio/src/colour/index.ts contrast() helper.
Triggers
- BrandConfig is missing
secondary, accent, neutral, or semantic blocks.
- A render fails contrast: any text-on-background pair has ratio < 4.5.
- User says "extend the palette for {brand}" or "audit colour".
Inputs
- 1-3 hex anchors (primary required; secondary, accent optional)
family: restoration | safety | industrial | consumer | training
school (optional): editorial-monocle | modern-minimal | warm-soft | tech-utility | brutalist-experimental — visual-school preset (open-design lineage). Selects the type-character, contrast bias, and accent role before family heuristics run. Default: modern-minimal.
- Existing palette (if extending)
Method
School preset — apply the school bias first. Each school constrains hue range, lightness range, neutrals temperature, and accent saturation:
| School |
Hue bias |
Lightness bias |
Neutrals |
Accent saturation |
Reference (vendored) |
editorial-monocle |
warm earth (terracotta/clay/sand) |
high (parchment / ivory) |
warm grays only — every gray carries yellow-brown undertone |
one chromatic accent, deeply earthy |
_library/claude/, _library/monocle/ (if present) |
modern-minimal |
cool neutral (slate/blue-gray) |
balanced |
true grays |
one electric accent, used <3× per viewport |
_library/airbnb/, _library/cal/ |
warm-soft |
warm pastels (peach/blush/cream) |
high |
warm grays + cream tints |
low-saturation rose/coral |
_library/airtable/, _library/cafe/ |
tech-utility |
indigo / cyan / slate |
low (dark surfaces preferred) |
cool grays toward slate-900 |
cyan signal accent, used only on deltas / state |
_library/binance/, _library/arc/ |
brutalist-experimental |
high-contrast primary on raw white |
extreme high + extreme dark |
pure black/white only |
sharp neon, used aggressively |
_library/brutalism/, _library/bold/ |
Family heuristics — applied after school. Family supplies the canonical adjacents:
safety: navy + emergency orange + ivory
restoration: teal + slate + lime
industrial: graphite + steel + amber
consumer: hero red/blue + warm white + slate
training: terracotta + cream + ink
Generate in OKLch — work in OKLch colour space, not HSL, so hue rotations preserve perceived lightness. Convert hex anchors to OKLch first; rotate hue ±30°/±60° while clamping lightness to the school's band. Snap output back to hex for BrandConfig storage. Keep an oklch sibling field in the contrast report for downstream Tailwind-4 consumers (Phase 2).
Determinism — same (primary, family, school) triple MUST yield the same output palette across runs. No randomness; no temperature; no LLM interpolation. The generator is a pure function of its inputs.
Build neutrals — interpolate between oklch(99% 0 hue) (50) and oklch(15% 0 hue) (900), tilting chroma toward the primary's hue per the school bias (warm-soft pulls neutrals warm; tech-utility pulls them cool).
Build semantics — pick success/warning/danger from a fixed pool that doesn't clash with primary.
Build dark variant — invert neutrals (50 ↔ 900 lightness) and shift primary up 8% lightness in OKLch space.
Validate — for each pair (primary, neutral.50), (primary, neutral.900), (secondary, neutral.50), (accent, secondary), ensure contrast ≥ 4.5 (WCAG-AA). If not, regenerate the failing slot. Brutalist-experimental requires ≥ 7.0 (AAA).
Output
Two artifacts:
- JSON contrast report with OKLch coords (for
remotion-brand-codify and Phase 2 web consumption):
{
"school": "modern-minimal",
"family": "restoration",
"tokens": {
"primary": { "hex": "#0E7C7B", "oklch": "oklch(48% 0.10 195)" },
"neutral": {
"50": { "hex": "#F5F7F8", "oklch": "oklch(97% 0.005 200)" },
"900": { "hex": "#0E1518", "oklch": "oklch(15% 0.010 215)" }
}
},
"passes": [ { "fg":"#F5F7F8", "bg":"#0E7C7B", "ratio": 7.21, "level":"AAA" } ],
"fails": []
}
- YAML fragment ready to drop into
Synthex/packages/brand-config/src/brands/{slug}.design.md colors: block — token names follow the @google/design.md spec (lowercase + dashes for tier names, e.g. neutral-50, dark-primary):
colors:
primary: "#0E7C7B"
secondary: "#2A3D45"
accent: "#C5E063"
neutral-50: "#F5F7F8"
neutral-100: "#E4E9EC"
neutral-500: "#6F7B82"
neutral-900: "#0E1518"
success: "#3FA34D"
warning: "#E0A800"
danger: "#C0392B"
on-primary: "#FFFFFF"
on-secondary: "#FFFFFF"
on-accent: "#0E1518"
surface: "{colors.neutral-50}"
on-surface: "{colors.neutral-900}"
dark-primary: "#16B5B3"
dark-secondary: "#1A2428"
dark-surface: "#0E1518"
dark-on-surface: "#F5F7F8"
Run npx --prefix Pi-Dev-Ops/remotion-studio design.md lint against the resulting .design.md to confirm zero contrast errors.
Boundaries
- Never deliver a palette with any AA failure — block and request founder input on the offending slot.
- Never override an explicit
darkVariant set by the brand.
- Never use red as a primary unless
family === 'consumer' and the brand name explicitly anchors red.
- Never reach for an LLM mid-generation — the function is deterministic. If determinism fails (same input → different output across runs), that's a bug in the generator, not a feature.
- Never blend two schools — pick one. Mixing
editorial-monocle warmth with tech-utility slate produces the muddy "AI-default" palette every brand starts with.
Hands off to
remotion-brand-codify writes the result into src/brands/{slug}.ts.
1---2name: remotion-colour-family3description: Generates a complete, accessible palette (primary, secondary, accent, 4-step neutral, semantic, and dark variant) from one to three hex anchors plus a colour-family classification. Validates WCAG-AA contrast for all text-on-background pairs. Triggered when a BrandConfig has fewer than 5 defined colours or contrast fails.4---56# remotion-colour-family78Builds a complete palette from minimal anchors. Reuses [`remotion-studio/src/colour/index.ts`](../../remotion-studio/src/colour/index.ts) `contrast()` helper.910## Triggers1112- BrandConfig is missing `secondary`, `accent`, `neutral`, or `semantic` blocks.13- A render fails contrast: any text-on-background pair has ratio < 4.5.14- User says "extend the palette for {brand}" or "audit colour".1516## Inputs1718- 1-3 hex anchors (primary required; secondary, accent optional)19- `family`: `restoration` | `safety` | `industrial` | `consumer` | `training`20- `school` *(optional)*: `editorial-monocle` | `modern-minimal` | `warm-soft` | `tech-utility` | `brutalist-experimental` — visual-school preset (open-design lineage). Selects the type-character, contrast bias, and accent role *before* family heuristics run. Default: `modern-minimal`.21- Existing palette (if extending)2223## Method24251. **School preset** — apply the `school` bias *first*. Each school constrains hue range, lightness range, neutrals temperature, and accent saturation:2627 | School | Hue bias | Lightness bias | Neutrals | Accent saturation | Reference (vendored) |28 |---|---|---|---|---|---|29 | `editorial-monocle` | warm earth (terracotta/clay/sand) | high (parchment / ivory) | warm grays only — every gray carries yellow-brown undertone | one chromatic accent, deeply earthy | `_library/claude/`, `_library/monocle/` (if present) |30 | `modern-minimal` | cool neutral (slate/blue-gray) | balanced | true grays | one electric accent, used <3× per viewport | `_library/airbnb/`, `_library/cal/` |31 | `warm-soft` | warm pastels (peach/blush/cream) | high | warm grays + cream tints | low-saturation rose/coral | `_library/airtable/`, `_library/cafe/` |32 | `tech-utility` | indigo / cyan / slate | low (dark surfaces preferred) | cool grays toward slate-900 | cyan signal accent, used only on deltas / state | `_library/binance/`, `_library/arc/` |33 | `brutalist-experimental` | high-contrast primary on raw white | extreme high + extreme dark | pure black/white only | sharp neon, used aggressively | `_library/brutalism/`, `_library/bold/` |34352. **Family heuristics** — applied *after* school. Family supplies the canonical adjacents:36 - `safety`: navy + emergency orange + ivory37 - `restoration`: teal + slate + lime38 - `industrial`: graphite + steel + amber39 - `consumer`: hero red/blue + warm white + slate40 - `training`: terracotta + cream + ink413. **Generate in OKLch** — work in OKLch colour space, not HSL, so hue rotations preserve perceived lightness. Convert hex anchors to OKLch first; rotate hue ±30°/±60° while clamping lightness to the school's band. Snap output back to hex for `BrandConfig` storage. Keep an `oklch` sibling field in the contrast report for downstream Tailwind-4 consumers (Phase 2).424. **Determinism** — same `(primary, family, school)` triple MUST yield the same output palette across runs. No randomness; no temperature; no LLM interpolation. The generator is a pure function of its inputs.435. **Build neutrals** — interpolate between `oklch(99% 0 hue)` (50) and `oklch(15% 0 hue)` (900), tilting `chroma` toward the primary's hue per the school bias (warm-soft pulls neutrals warm; tech-utility pulls them cool).446. **Build semantics** — pick `success`/`warning`/`danger` from a fixed pool that doesn't clash with primary.457. **Build dark variant** — invert neutrals (`50 ↔ 900` lightness) and shift primary up 8% lightness in OKLch space.468. **Validate** — for each pair `(primary, neutral.50)`, `(primary, neutral.900)`, `(secondary, neutral.50)`, `(accent, secondary)`, ensure contrast ≥ 4.5 (WCAG-AA). If not, regenerate the failing slot. Brutalist-experimental requires ≥ 7.0 (AAA).4748## Output4950Two artifacts:51521. **JSON contrast report** with OKLch coords (for `remotion-brand-codify` and Phase 2 web consumption):5354```jsonc55{56 "school": "modern-minimal",57 "family": "restoration",58 "tokens": {59 "primary": { "hex": "#0E7C7B", "oklch": "oklch(48% 0.10 195)" },60 "neutral": {61 "50": { "hex": "#F5F7F8", "oklch": "oklch(97% 0.005 200)" },62 "900": { "hex": "#0E1518", "oklch": "oklch(15% 0.010 215)" }63 }64 },65 "passes": [ { "fg":"#F5F7F8", "bg":"#0E7C7B", "ratio": 7.21, "level":"AAA" } ],66 "fails": []67}68```69702. **YAML fragment** ready to drop into `Synthex/packages/brand-config/src/brands/{slug}.design.md` `colors:` block — token names follow the `@google/design.md` spec (lowercase + dashes for tier names, e.g. `neutral-50`, `dark-primary`):7172```yaml73colors:74 primary: "#0E7C7B"75 secondary: "#2A3D45"76 accent: "#C5E063"77 neutral-50: "#F5F7F8"78 neutral-100: "#E4E9EC"79 neutral-500: "#6F7B82"80 neutral-900: "#0E1518"81 success: "#3FA34D"82 warning: "#E0A800"83 danger: "#C0392B"84 on-primary: "#FFFFFF"85 on-secondary: "#FFFFFF"86 on-accent: "#0E1518"87 surface: "{colors.neutral-50}"88 on-surface: "{colors.neutral-900}"89 dark-primary: "#16B5B3"90 dark-secondary: "#1A2428"91 dark-surface: "#0E1518"92 dark-on-surface: "#F5F7F8"93```9495Run `npx --prefix Pi-Dev-Ops/remotion-studio design.md lint` against the resulting `.design.md` to confirm zero contrast errors.9697## Boundaries9899- Never deliver a palette with any AA failure — block and request founder input on the offending slot.100- Never override an explicit `darkVariant` set by the brand.101- Never use red as a primary unless `family === 'consumer'` and the brand name explicitly anchors red.102- Never reach for an LLM mid-generation — the function is deterministic. If determinism fails (same input → different output across runs), that's a bug in the generator, not a feature.103- Never blend two schools — pick one. Mixing `editorial-monocle` warmth with `tech-utility` slate produces the muddy "AI-default" palette every brand starts with.104105## Hands off to106107`remotion-brand-codify` writes the result into `src/brands/{slug}.ts`.