Brand Style Tokens¶
A layer today names its colours and fonts literally: "#fe2121", "GT Eesti Display". Across the
Betta catalogue that is the same red typed into dozens of layers across dozens of templates, and
changing it is a find-and-replace over a corpus. This feature replaces the literal with a
reference to a named role on the brand, so the value has one home and everything that uses it
follows.
What exists today, checked
Brand fonts exist as assets: brandFontDefinitionSchema carries a face plus an
assetRefSchema src, loaded at render from signed URLs. That is file delivery, not a style
system — a template still names the family as a string and nothing connects the two.
Brand colours do not exist in the schema at all: there is no palette, no role, no token, no
style profile. rs.colour() is a string with a colour picker on it. So this is a genuinely new
layer, not an extension of something half-built.
The shape¶
A brand declares roles. A document references them. Neither knows the other's literal values.
// on the brand
"style": {
"colours": { "primary": "#fe2121", "secondary": "#054f8e", "highlight": "#ffd100",
"onPrimary": "#ffffff", "ink": "#1a1a1a" },
"fonts": { "display": { "family": "GT Eesti Display", "weight": 700 },
"body": { "family": "GT Eesti Display", "weight": 400 } }
}
// in a layer
"typography": { "16:9": { "font": { "brand": "display" }, "size": 151,
"color": { "brand": "onPrimary" } } },
"props": { "background": { "fill": { "brand": "primary" } } }
{ "brand": "<role>" } is the whole reference vocabulary. It is a structured alternative to a
literal wherever a colour or a font is authored, the same scalar-or-structured pattern w/h,
rotation and typography size already use (Coordinate System & Sizing),
so a field that took "#fe2121" now takes that or a reference and every already-authored literal
stays valid.
Why this isn't an expression
Expressions are numeric, deliberately: arithmetic over numbers is a closed, checkable grammar. A colour is not a number and a font is not a number, and widening the expression grammar to carry them would mean strings, string functions, and a much larger surface to validate, for a lookup that has no arithmetic in it. A token reference is a lookup, so it gets a lookup's shape.
Resolution: brand, then template, then layer¶
Three levels, resolved in resolve alongside typography and bindings, most specific winning:
| Level | Sets | Use |
|---|---|---|
| Brand | the role's value | the default every template inherits |
| Template | an override for that role, scoped to this document | a seasonal campaign that runs the same structure in different colours |
| Layer | a literal, ignoring the role entirely | a one-off that genuinely isn't brand-driven |
// document level: this template's own reading of two roles
"styleOverrides": { "colours": { "primary": "#0b7d3e", "highlight": "#ffffff" } }
A template override is what makes one lockup serve Black Friday and Boxing Day without forking it:
the structure, the copy, and the animation are identical, and the two documents differ by a
styleOverrides block. It also scopes the blast radius — overriding primary here cannot affect
another template, which is the property a plain "change the brand colour" edit doesn't have.
Failure is loud, at the boundary¶
A reference names a role that must exist. validateTemplate checks the reference is well-formed
and that any styleOverrides names roles the document actually uses. Whether a particular brand
carries the role can only be checked once a document is bound to one, so that check runs there, and
a document referencing a role its brand lacks fails with a message naming both. It never falls back
to a default colour, because a silently wrong brand colour is worse than a render that stops: the
whole point of the feature is that brand values are correct by construction.
What this is not, and what it connects to¶
This page covers the engine's half: the reference shape, the resolution chain, and where each is validated. That is what a renderer and a schema need, and it is small.
The wider brand system is its own effort and deliberately out of scope here: who may edit a brand's roles, how a change is versioned and reviewed, whether a change republishes affected campaigns or only applies to new ones, what the brand-management UI looks like, and how roles extend past colour and font into spacing, radius, and motion defaults. Those are real and the current state is thin, but none of them change the two things above. The engine needs to resolve a reference; everything else is about who gets to set it and when.
One connection worth naming now: a role set is a vocabulary, and a vocabulary is only useful if it's small and stable. A brand with forty colour roles is a palette with extra steps, and nobody will know which to use. The set above is five, which is roughly what the Betta catalogue actually uses once the literals are de-duplicated, and that ratio is the thing to watch when the wider system gets designed.
Risks¶
- A role rename is a breaking change across every document that references it. There is no indirection under the indirection, and adding one would be the same mistake twice. Renames go through the same migration discipline a schema change does.
- Template overrides are easy to overuse. A document that overrides every role has bought the literals back with more steps. Worth watching once it ships, not worth constraining now.
Done when¶
A layer renders its fill and family from brand roles; changing the brand's primary changes every
referencing template's next render with no document edited; a template's styleOverrides changes
that document only; and a reference to a missing role fails validation naming the role and the
brand.