Color
Editorial Ink has one accent (clay, terracotta) and three status hues (olive success, brick danger, ochre attention) on warm paper neutrals. App code never touches a hue directly: it picks a semantic token or a component variant, and the token flips between data-color-mode="light" and data-color-mode="dark" on <html>. Every pair listed here passes WCAG AA in both themes; any pairing not listed here is unverified and an audit should flag it.
- App code uses only semantic tokens:
--fgColor-*,--bgColor-*,--borderColor-*. It never references a--base-color-*primitive (neutral,clay,olive,brick,ochre,white,black) and never writes a hex,rgb(), orhsl()literal for a colour. - Text on surfaces:
--fgColor-defaultand--fgColor-mutedare the only body-text colours. They sit on--bgColor-default,--bgColor-muted,--bgColor-inset, or a soft--bgColor-<status>-mutedtint. They are never placed on a solid-emphasisbackground. - Status text (
--fgColor-accent,--fgColor-success,--fgColor-danger,--fgColor-attention) sits on the three neutral surfaces or on its own soft tint (--bgColor-<status>-muted, matched by meaning). It is never placed on a solid-emphasisbackground. --fgColor-onEmphasis(white) is used only on--bgColor-accent-emphasis,--bgColor-success-emphasis, and--bgColor-danger-emphasis. It is never used on--bgColor-emphasisand never on--bgColor-attention-emphasis.- The ochre attention solid takes dark text: text on
--bgColor-attention-emphasisis always--fgColor-onAttention. White on ochre fails AA. - Text on
--bgColor-emphasis(the ink surface) is--bgColor-default, not--fgColor-onEmphasis. The two tokens are designed as inverses per theme, so the pair stays high-contrast in both modes.Tooltipowns this pattern; do not rebuild it. - Soft tints come in pairs: a
--bgColor-<status>-mutedfill always takes its matching--borderColor-<status>-mutedhairline and--fgColor-<status>text. Never compute a translucent fill (rgba(...),color-mix(...), oropacityon a status colour) to fake a tint. - The accent appears once per surface as an action: exactly one
ButtonorIconButtonwithvariant="primary"per page, dialog, card, or toolbar.Linktext andTag variant="accent"(a selected or active token) are the only other accent uses. - Status colours carry status meaning only:
successmeans done, passing, or published;dangermeans failed, blocked, or destructive;attentionmeans a warning or something that needs action. Categories, teams, and brands are never coloured with a status variant; usedefaultormuted. BannerandToastvariants are chosen by meaning, not by the colour they produce:info(clay),success(olive),warning(ochre),danger(brick).- Surfaces nest inward: the page is
--bgColor-inset, aCard variant="default"on it is--bgColor-default, and a recessed strip inside that isCard variant="muted"(--bgColor-muted). A default surface is not placed on another default surface without a--borderColor-defaultedge. - Borders:
--borderColor-defaultfor control outlines and card edges,--borderColor-mutedfor row and list separators.--borderColor-accent-emphasisis the focus and selected-state colour and is applied only by components. - Colour is never the only signal. A status is a coloured word (
Tag), a coloured message (FormControl.Validation,Banner), or an icon plus text, never a bare coloured dot with no name. - Theming is not branched in JavaScript. Code never reads
data-color-modeto pick a colour; it uses the token and lets the theme resolve it.
AA pairs
Section titled “AA pairs”Pairs that pass AA for normal text in both themes. The machine-readable form is $extensions.tenet.contrast on each fgColor token in tokens/tokens.json (aa and aaLarge lists per theme); a pair must be in the aa list of both themes to appear here.
| Foreground token | Allowed backgrounds |
|---|---|
--fgColor-default | --bgColor-default, --bgColor-muted, --bgColor-inset, any --bgColor-<status>-muted tint |
--fgColor-muted | --bgColor-default, --bgColor-muted, --bgColor-inset, any --bgColor-<status>-muted tint |
--fgColor-accent | the three neutral surfaces, --bgColor-accent-muted |
--fgColor-success | the three neutral surfaces, --bgColor-success-muted |
--fgColor-danger | the three neutral surfaces, --bgColor-danger-muted |
--fgColor-attention | the three neutral surfaces, --bgColor-attention-muted |
--fgColor-onEmphasis | --bgColor-accent-emphasis, --bgColor-success-emphasis, --bgColor-danger-emphasis only |
--fgColor-onAttention | --bgColor-attention-emphasis only |
--bgColor-default (as text) | --bgColor-emphasis only (the inverse pair) |
Do / Don’t
Section titled “Do / Don’t”- ✅
<Text tone="muted" size="small">Updated 3 minutes ago</Text>(secondary copy through the token, not a literal) - ✅
<Tag variant="success">Published</Tag>(status word gets the tint, hairline, and text tokens as a set) - ✅
<Stack direction="horizontal" gap="condensed"><Button variant="invisible">Cancel</Button><Button variant="primary">Save changes</Button></Stack>(one accent action) - ❌
<span style={{ color: '#6e6049' }}>Updated</span>(aneutral.650literal; breaks in dark mode) - ❌
<span style={{ background: 'var(--bgColor-attention-emphasis)', color: 'var(--fgColor-onEmphasis)' }}>Pending</span>(white on ochre fails AA; the text token is--fgColor-onAttention) - ❌
<div style={{ background: 'rgba(194, 80, 46, 0.12)' }}>(a computed tint; use--bgColor-accent-mutedorTag variant="accent") - ❌
<Button variant="primary">Save</Button><Button variant="primary">Publish</Button>(two accents on one surface) - ❌
<Tag variant="danger">Design</Tag>(a team name coloured as a failure)