Skip to main content
A brand family exists so that shared decisions live in exactly one file. The family’s config.global.yaml is the foundation; each sub-brand’s config.yaml holds only what differs. The two layers combine by a recursive deep merge, which is what makes deltas-only authoring possible: a sub-brand can override one field inside a nested object and inherit the rest of it.

The two layers

Later layers win, per leaf. The merged result is stamped with the family slug {parent}-{sub}aurora-rewards — which is what appears in generated CSS as [data-brand="aurora-rewards"].

Partial objects merge

This is the central rule, and the one most likely to surprise you if you have used systems where an override replaces the whole object. An object in the child merges recursively into the object in the parent. It does not replace it. The fictional Aurora Airways family demonstrates it. The parent declares the brand intent in full:
And the child overrides one field of it:
The merged intents.brand is { hue: 15, chroma: 0.220 }. The Rewards sub-brand never restates the hue. Likewise, its typography block overrides only heading-familybody-family, base-font-size, scale-ratio, fluid, and density all come from the parent untouched, and tier-gold is simply added to the inherited intent roster. A sibling sub-brand like Aurora Cargo can show the same merge at three different depths at once: overriding intents.brand entirely (both fields), intents.neutral.chroma alone, typography.scale-ratio alone, motion.duration-base alone (keeping the family’s linear easing), and inside presets adjusting individual mode levels’ scheme values while leaving each level’s contrast-factor inherited.

The full merge table

Applied per key of the child layer: Two consequences are worth stating plainly. null is the only delete signal — there is no separate unset or remove keyword, so a sub-brand that needs to drop an inherited intent or gradient sets that key to null. And arrays replace, which matters most for scheme-track stop lists: a child supplying a track supplies the whole track. Key normalization runs before the merge, so a parent authoring heading-family and a child authoring headingFamily merge as the same key. (Authoring both spellings side by side inside one object is an error, not a silent win for one of them.)

Deltas-only is enforced, not just encouraged

The cascade validator walks every family and flags each leaf in a sub-brand config whose value structurally equals what the parent already provides. Comparison is deep and by value, so an object or array that merely matches the parent’s content is caught even though it is a different reference. Genuine overrides are never flagged, and null deletes are not leaves so they are never flagged either. Each finding names the brand, the config path, and the redundant key path, so a config that has drifted into duplication tells you exactly which lines to delete.
Redundant leaves are worse than noise. A value copied into a sub-brand no longer tracks the family — the next time the parent’s value changes, that one sub-brand silently keeps the old one. That is the drift the family structure exists to prevent, and it is why the validator treats duplication as a finding rather than a style preference.

What inheritance does not include

There is no workspace layer above the family, no defaults block, and no system-wide brand defaults file. The cascade is exactly two layers deep: family and sub-brand. A flat brand has no cascade at all — its single config.yaml is the whole configuration, and it must therefore be complete, including the required intents.brand and intents.neutral.
When starting a new sub-brand, write nothing and generate. Then add fields one at a time until it looks right. Starting from a copy of the parent guarantees a config full of redundant leaves you will have to delete.

Adding a Brand

Walk through creating a sub-brand and a flat brand end to end.

Brand Config Overview

The schema every layer is authored against, and the key normalization rule.