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, orsemanticblocks. - 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|trainingschool(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
schoolbias first. Each school constrains hue range, lightness range, neutrals temperature, and accent saturation:School Hue bias Lightness bias Neutrals Accent saturation Reference (vendored) editorial-monoclewarm 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-minimalcool neutral (slate/blue-gray) balanced true grays one electric accent, used <3× per viewport _library/airbnb/,_library/cal/warm-softwarm pastels (peach/blush/cream) high warm grays + cream tints low-saturation rose/coral _library/airtable/,_library/cafe/tech-utilityindigo / cyan / slate low (dark surfaces preferred) cool grays toward slate-900 cyan signal accent, used only on deltas / state _library/binance/,_library/arc/brutalist-experimentalhigh-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 + ivoryrestoration: teal + slate + limeindustrial: graphite + steel + amberconsumer: hero red/blue + warm white + slatetraining: 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
BrandConfigstorage. Keep anoklchsibling 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) andoklch(15% 0 hue)(900), tiltingchromatoward the primary's hue per the school bias (warm-soft pulls neutrals warm; tech-utility pulls them cool). -
Build semantics — pick
success/warning/dangerfrom a fixed pool that doesn't clash with primary. -
Build dark variant — invert neutrals (
50 ↔ 900lightness) 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-codifyand 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.mdcolors:block — token names follow the@google/design.mdspec (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
darkVariantset 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-monoclewarmth withtech-utilityslate produces the muddy "AI-default" palette every brand starts with.
Hands off to
remotion-brand-codify writes the result into src/brands/{slug}.ts.