Foundations

Contextual accent

A surface declares its accent; everything inside follows without being told.

The five accents

Each accent has a solid form and a 13% tonal form. Selection, focus rings, active states and accent buttons all derive from whichever one the surface has declared.

violetProduct default · Design mode · style-panel local override
pinkEditor mode · style-panel variable-bound fields
greenCMS mode
blueComponents panel
redDestructive actions — and nothing else

How a surface sets one

One attribute on the surface. Nothing below it takes an accent prop — that is the whole point, and it is why the same Button markup is correct in the builder and in the CMS.

MODE_ACCENT in ModeSwitcher.tsx is the map for the builder's three modes, and BuilderShell reads the same map — so a mode's surface can never disagree with its own tab.

data-accent="violet"
data-accent="green"
data-accent="blue"
Identical markup in all three. Only the wrapper's data-accent differs.

The inheritance trap

Custom properties inherit as computed values, so overriding the source token is not enough. --color-accent-primary is declared at :root by reading its semantic source, which means the substitution happens once, at the root. A descendant that overrides only that semantic source inherits the already-computed violet and never re-derives it.

This is why the [data-accent] block in semantic.css restates every derived token rather than just the source — and it has to. If you add a token that should track the accent, add it to that block too, then verify by measuring getComputedStyle inside each scope. Do not verify by eye.

Known gaps

--color-primary-hover is bound flat to the base accent in every [data-accent] scope, so hover and default measure the same colour: its seven consumers have no hover lift on an accent surface. Only the unscoped :root violet gets the lighter tint. This is pre-existing and has live consumers, so fixing it is a visible change and belongs with a density and interaction pass rather than a sweep.