Colour Language
Governs the colour language every Fernando Kylas product inherits: the palette, the three reading tiers, the two grounds, the state, interaction and data roles, and the contrast every one of them must meet.
Read the illustrated chapter, which demonstrates this foundation with live specimens rather than describing it.
This is the canonical colour chapter of the Fernando Kylas Brand System. Every product that inherits the brand (the website, the Publishing Platform, the Report Engine, the Documentation Engine, client deliverables, future products) inherits this file.
All contrast ratios here were computed, not transcribed. WCAG 2.x relative luminance over sRGB. The v3.1 values were measured 2026-07-23; the state, interaction, surface-composite and data-series values were measured 2026-07-25. Where a source document's figure differed, the measured value is used and the discrepancy recorded in §14.
| Companion | Purpose |
|---|---|
docs/colour/colour-language.html | The chapter rendered as a reading experience, with live components |
assets/colours/ | Machine-readable token files for design tools |
platform/design-system/css/tokens.css | The reference implementation |
typography.md · accessibility.md | The other two gates |
What is approved
Fernando Kylas approved the Colour Language on 2026-08-26. The approval covers the palette and every rule that governs it: the warm paper tone (§2), the three reading tiers, the two grounds, the semantic token set and its naming grammar (§4), the one-accent rule (§5), the theme-inversion contract (§6.1), the state, interaction and data roles (§8), the two-tone-only limit on data series, and every measured contrast ratio in this chapter.
Three items are acknowledged and intentionally excluded:
COLOUR-03, the token prefix and naming form. §4.1 recommends keeping--fk-<role>and it is in use everywhere; a naming decision, not a colour one.COLOUR-06, source ratios differing from measured. Already settled in practice: the measured values are used throughout and the discrepancy is recorded.COLOUR-05, the cool divider. This exclusion is deliberate and is the one worth naming twice.#E4E3E8ishsl(252, 10%, 90%), the only cool value in a warm palette where paper ishsl(36), the cardhsl(60)and the greenhsl(96). It has no accessibility consequence: at 1.16:1 it is decorative. It is a coherence defect, and approving this chapter locks#E4E3E8as canon with that defect known and recorded. Changing it later supersedes this decision, which is the correct cost: the fix would alter every rendered report and every Figma frame.
No undocumented change may modify the approved palette. A change supersedes a decision in DECISIONS.md, with a reason; it does not quietly replace one.
1. Colour philosophy
Typography carries the identity. Colour supports it.
This is brand-principles.md §2.1, and this section is where it was first argued.
Open any page of this system and cover the colour. The identity survives: Strawford's low x-height and open counters, Geomanist's geometric evenness, the measure, the rhythm, the rules. Now do the reverse: keep the colour and replace the type with Arial. Nothing of the brand remains.
That asymmetry is the whole argument. Colour is the least load-bearing part of this identity, so it is given the least work to do. A consultancy that publishes evidence needs its evidence to be legible in a photocopy, a greyscale print, a screenshot pasted into a client's deck, and a dark-mode inbox. Every one of those destroys colour. None of them destroys typography.
Restraint is a functional decision, not an aesthetic one
The principle is brand-principles.md §3.1. What follows is what it costs in colour.
Three hues (warm paper, near-black ink, one muted green) is not minimalism for its own sake. Each additional hue costs something specific:
- A contrast obligation. Every new colour needs measured ratios against every ground it can appear on, in both themes. Two hues is 2 pairs. Six hues is 30.
- A meaning obligation. Readers assume a new colour means something. If it does not, it is noise; if it does, that meaning must be documented, taught, and never contradicted.
- A reproduction obligation. It must hold in CMYK, in greyscale, on a projector, under
prefers-contrast: more, and for the roughly 1 in 12 men with a colour-vision deficiency.
A palette earns a hue by paying all three. Ours has one accent because one is what the work needed.
Colour communicates meaning, or it does not appear
brand-principles.md §3.2.
There is no decorative colour in this system. No tinted card because the section felt plain, no gradient to fill space, no hue rotation to distinguish chapters. If a colour is present, a reader should be able to ask "what does that mean?" and get an answer.
The corollary is the system's hardest rule, and the one most often broken by well-meaning implementations:
No colour carries meaning alone. Anything communicated by colour must also be carried by text, shape, or position.
This is why the Report Engine signals severity with a square, state with a circle, confidence with dots, and coverage with a bar: four channels distinguished by geometry, not hue. A finding marked critical stays critical in a greyscale fax. See §8.
What we deliberately do not do
| Not this | Because |
|---|---|
| A red / amber / green status palette | Fails greyscale, fails deuteranopia, and reads as a dashboard rather than a document |
| A 10-step tint ramp per hue | Nine of the ten steps have no documented role, so they become decoration |
Pure black #000000 | See §13.1 |
Pure white #FFFFFF | See §13.2 |
| Colour-coded chapters or sections | Position and typography already do this, for free, in print |
| A second accent "for variety" | Variety is not a requirement. Clarity is |
2. Paper tone: decided
Decided 2026-07-23. v3.1 uses the warm paper.
| Role | Value |
|---|---|
| Paper | #F6F4F1 |
| Ink ground | #090909 |
| Reversed text | #FAFAF7 |
The warm tone was chosen because it reads as print-led rather than screen-led, and because it sits better under Strawford's low x-height and open counters.
This is the one element of the colour system already decided. Everything else in this file is Ready for review.
3. Primitives
Raw values. Never reference a primitive directly in a design or a component. Always go through a semantic token from §4. A primitive names a colour; a token names a job. Grounds change; jobs do not.
Values confirmed against docs/typography/typography-lab.html, the canonical implementation. Lab token names are given so the two stay traceable.
| Value | Lab token | Engine token | L\* | Description |
|---|---|---|---|---|
#F6F4F1 | --paper | --fk-c-paper | 96.26 | Warm paper |
#FCFCFB | --paper-card | --fk-c-paper-card | 98.94 | Raised card surface on light |
#FAFAF7 | --rev-1 | --fk-c-rev-1 | 98.20 | Warm off-white, reversed text |
#090909 | --ink | --fk-c-ink | 2.47 | Ink ground |
#0A0A0A | --ink-1 | --fk-c-ink-1 | 2.74 | Primary ink, text on light |
#4A4A4A | --ink-2 | --fk-c-ink-2 | 31.46 | Mid grey, secondary text on light |
#616161 | --ink-3 | --fk-c-ink-3 | 41.14 | Metadata grey on light |
#ADADAD | --rev-2 | --fk-c-rev-2 | 70.72 | Secondary text on dark |
#8E8E8E | --rev-3 | --fk-c-rev-3 | 59.02 | Metadata grey on dark |
#869F75 | --green | --fk-c-green | 62.63 | The one accent hue |
#EBF0E9 | --green-wash | --fk-c-green-wash | 94.25 | Green wash surface, light only |
#E4E3E8 | --line | --fk-c-line | 90.44 | Rule and divider, light ground |
#2A2A2A | --line-rev | --fk-c-line-rev | 17.06 | Rule and divider, dark ground |
L\ is CIE lightness, given because it, not the hex, predicts how a value survives greyscale. Two primitives with close L\ are indistinguishable in print however different their hue. See §8.3.
Why there are thirteen primitives and not five
Six of the thirteen exist only because the system has two grounds. ink-2 / rev-2, ink-3 / rev-3, and line / line-rev are the same job at two different ground luminances. They are not a tint ramp and must not be used as one.
4. Semantic tokens
4.1 Naming grammar
--fk-<role> semantic token: bind to this
--fk-<role>-<variant> a qualified variant of the same role
--fk-c-<primitive> raw primitive: reference only, never bind
A token name states the job, never the colour. --fk-text-secondary is correct because it stays correct when the ground inverts and the value changes from #4A4A4A to #ADADAD. --fk-grey-700 would be a lie in one of the two themes.
There are no numeric-scale names in this system: no green-500, no grey-700, no accent-80. A number implies a position on a ramp; a ramp implies nine unused siblings; unused siblings become decoration. Where numeric-scale styles were found in Figma, they were retired (§12.2).
Open item, token prefix. The brief proposed the form
--colour-reading-primary. The recommendation is to keep--fk-<role>: it is already fully semantic, it is the form implemented across the Report Engine's seven stylesheets, five templates and validated PDF output, and it carries a product prefix that prevents collision inside a consuming application. A rename would touch every rendered artefact and every Figma variable for no gain in clarity or accessibility. The mapping is given in §4.4 so both vocabularies read the same. Fernando's call Recorded, not resolved.
4.2 The token table
Light values are the default. Dark values apply under [data-theme="dark"] and nowhere else (§6.1). Ratios are against that theme's own surface unless stated.
Surfaces
| Token | Light | Ratio | Dark | Ratio | Job |
|---|---|---|---|---|---|
surface | #F6F4F1 | — | #090909 | — | Page and frame ground |
surface-raised | #FCFCFB | 1.07 | #2A2A2A | 1.39 | Card, panel, evidence block |
surface-accent | #EBF0E9 | 1.05 | rgba(134,159,117,.14) → #1A1E18 | 1.18 | Highlighted or selected ground |
Surface separations are decorative by design and all fall far below the 3:1 threshold for a graphical object. That is permitted; WCAG does not require a decorative boundary to be perceivable. It does mean a card boundary must never be the only signal that something is a distinct control: give it border-strong, or a label, or both. See §7.3.
Reading hierarchy
| Token | Light | Ratio | Dark | Ratio | Job |
|---|---|---|---|---|---|
text-primary | #0A0A0A | 18.03 | #FAFAF7 | 19.04 | Display, headings, body |
text-secondary | #4A4A4A | 8.07 | #ADADAD | 8.87 | Supporting copy, long-form body |
text-tertiary | #616161 | 5.64 | #8E8E8E | 6.08 | Captions, labels, metadata only |
Three tiers, not five. A fourth tier would have to sit between 8.07 and 5.64, where the difference is invisible to a reader but adds a decision for every author. Hierarchy below three tiers is carried by size and weight, which typography already provides.
Lines
| Token | Light | Ratio | Dark | Ratio | Job |
|---|---|---|---|---|---|
border | #E4E3E8 | 1.16 | #2A2A2A | 1.39 | Decorative hairline. Never meaning-bearing |
border-strong | #4A4A4A | 8.07 | #ADADAD | 8.87 | A rule that carries meaning or bounds a control |
rule-accent | #869F75 | 2.65 | #869F75 | 6.86 | The signature rule. A mark, never a boundary |
This distinction is the one most often collapsed in implementation. border is a whisper that groups things a reader has already understood. border-strong is a statement, and it can be the only thing telling a reader where a control begins, so it is measured and it passes. If you find yourself wanting border to do a border-strong job, use border-strong.
Accent
| Token | Light | Ratio | Dark | Ratio | Job |
|---|---|---|---|---|---|
accent | #869F75 | 2.65 ❌ | #869F75 | 6.86 ✅ | Marks only on light. Text-capable on dark |
accent-ink | #0A0A0A | 18.03 | #869F75 | 6.86 | The accent's text stand-in, see §5 |
Semantic roles
These are role colours, not brand colours. Every one of them resolves to an existing reading token: the system deliberately has no success-green, warning-amber or critical-red. Meaning is carried by the accompanying shape and label (§8); the token exists so code has a stable name to bind, and so the meaning of a role can be changed in one place.
| Role token | Light resolves to | Dark resolves to | Carried alongside by |
|---|---|---|---|
critical | text-primary | text-primary | Filled square + label |
warning | text-primary | text-primary | Half-filled square + label |
success | accent-ink (ink) | accent-ink (green) | Filled circle + label |
information | text-secondary | text-secondary | Outlined circle + label |
observation | text-secondary | text-secondary | Outlined circle + label |
evidence | surface-raised | surface-raised | Panel + source line |
confidence | text-primary | text-primary | 1–3 filled dots of 3 |
disabled | text-tertiary | text-tertiary | aria-disabled + no affordance |
unmeasured | text-tertiary | text-tertiary | Dashed outline + "Not collected" |
observation and information resolve identically and are kept as separate names because they belong to different vocabularies: information is an interface notice, observation is an audit finding class. That is a documented relationship, not a duplicate: a future decision could diverge them without touching either call site.
success is the only role that touches the accent, and only because a positive finding is the one place green's meaning is unambiguous. On light paper it resolves to ink, because green is not a text colour there (§5).
Interaction
| Token | Light | Ratio | Dark | Ratio | Job |
|---|---|---|---|---|---|
interactive | #0A0A0A | 18.03 | #FAFAF7 | 19.04 | Link and control text |
interactive-hover | #0A0A0A | 18.03 | #FAFAF7 | 19.04 | Text colour does not change, see below |
focus-ring | #0A0A0A | 18.03 | #FAFAF7 | 19.04 | 2 px ring, 2 px offset |
surface-selected | = surface-accent | 1.05 | = surface-accent | 1.18 | Selected row or card ground |
Hover never reduces contrast. The obvious implementation, dimming the link on hover, makes the element less legible at the exact moment the reader has committed attention to it. Instead:
- the text colour holds at
interactive - the underline thickens from 1 px to 2 px
- the underline may take
rule-accent
The underline is present before hover, so the accent is never the sole carrier of the state, and the 2.65:1 green underline on light paper is decoration on top of an already-sufficient signal.
Focus is ink, not green. Green measures 2.65:1 against paper, below the 3:1 minimum for a focus indicator. Ink measures 18.03:1 against paper, 19.29:1 against a raised card and 17.13:1 against the green wash, so a single ink ring is legal on every light surface in the system without a per-surface exception. On dark, #FAFAF7 measures 19.04:1 on ink and 13.73:1 on the raised surface.
Data
| Token | Light | Ratio | Dark | Ratio |
|---|---|---|---|---|
data-1 | #0A0A0A | 18.03 | #FAFAF7 | 19.04 |
data-2 | #616161 | 5.64 | #8E8E8E | 6.08 |
data-accent | #869F75 | 2.65 | #869F75 | 6.86 |
Two tone-only series is the mathematical maximum on either ground. This is not a stylistic preference; see the proof in §8.3. data-accent is available as a third series only when accompanied by a pattern fill or a direct label.
4.3 Colour roles: five distinct jobs
The same thirteen primitives serve five different kinds of work. Confusing the categories is how palettes rot.
| Role class | What it does | Tokens | Governing constraint |
|---|---|---|---|
| Brand | Makes the system recognisable | surface, text-primary, accent | Never changes without a DECISIONS.md entry |
| Interface | Makes controls operable | interactive, focus-ring, border-strong, disabled, surface-selected | 3:1 for the component, 4.5:1 for its label |
| Editorial | Makes long-form reading comfortable | text-primary/secondary/tertiary, border, surface-raised, rule-accent | Reading hierarchy must survive greyscale |
| Data | Distinguishes quantities | data-1, data-2, data-accent | 3:1 against the ground and 3:1 against each other |
| Status | Classifies findings | critical, warning, success, information, confidence, unmeasured | Never hue alone; shape + label mandatory |
A token belongs to exactly one class. accent is brand; rule-accent is editorial; data-accent is data. They resolve to the same hex and obey different rules, which is precisely why they have different names.
4.4 Vocabulary mapping
For anyone arriving from the naming form proposed in the brief, or from the Figma variable names:
| This system | Figma variable | Brief's proposed form |
|---|---|---|
--fk-surface | surface | --colour-background |
--fk-surface-raised | surface-raised | --colour-panel |
--fk-text-primary | text-primary | --colour-reading-primary |
--fk-text-secondary | text-secondary | --colour-reading-secondary |
--fk-text-tertiary | text-tertiary | --colour-reading-tertiary |
--fk-accent | accent | --colour-accent |
--fk-border | border | --colour-divider |
--fk-success | success | --colour-success |
--fk-warning | warning | --colour-warning |
--fk-critical | critical | --colour-critical |
5. The green asymmetry
Green is not one accent. It behaves differently on each ground, and this is deliberate.
| Ground | Ratio | Permitted for |
|---|---|---|
Light paper #F6F4F1 | 2.65:1 | Marks only: rules, underlines, dividers, non-text accents |
Raised card #FCFCFB | 2.83:1 | Marks only |
Green wash #EBF0E9 | 2.51:1 | Nothing. The wash is a ground for ink, never for green |
Dark ink #090909 | 6.86:1 | Text and marks |
Dark raised #2A2A2A | 4.94:1 | Text and marks |
Dark accent wash #1A1E18 | 5.82:1 | Text and marks |
On light paper green meets neither the 4.5 threshold for body text nor the 3.0 threshold for large text, and not the 3.0 threshold for a UI component either. A green rule that carries meaning on its own fails at 2.65:1.
This asymmetry must be documented wherever the palette is published. A designer who learns "green is our accent" on a dark template will use it as text on a light one. That is the single most likely accessibility regression in this system, and accent-ink exists specifically to give that designer a correct token to reach for.
Why the green was not simply darkened
A green at 4.5:1 on paper needs roughly L\ 47 or below, around #5F7350. Measured against the ink ground it would then sit at about 2.9:1 and fail as dark*-ground text, requiring two greens. Two greens means two accents, which fails §1. Keeping one green and constraining it to marks on light costs one documented rule; darkening it costs a second hue and a second set of ratios in perpetuity.
6. Themes
6.1 Theme inversion is a single-class contract
A surface goes dark by one route: the theme attribute. Never by an inline background, and never by a second mechanism.
/* the only sanctioned route */
[data-theme="dark"] { --fk-surface: var(--fk-c-ink); /* … */ }
The moment a surface can go dark by a second route, every token hung off that class silently detaches. In the previous specimen, two carousel slides set their ground with an inline style="background:var(--ink)" instead of the theme class. The descendant rules never fired, light-paper text values leaked onto a dark ground, and body text rendered at 2.25:1: the measured ratio of #4A4A4A on #090909.
Treat an inline background on a themed container as a lint error.
An inverted panel inside a light page is legitimate, and is achieved by nesting the attribute, <aside data-theme="dark">, not by overriding a fill.
6.2 Light theme behaviour
| Layer | Token | Behaviour |
|---|---|---|
| Ground | surface #F6F4F1 | Warm, low-glare, print-led |
| Raise | surface-raised #FCFCFB | 1.07:1, a suggestion of a card, not a box |
| Highlight | surface-accent #EBF0E9 | 1.05:1, the quietest possible emphasis |
| Reading | 18.03 → 8.07 → 5.64 | A wide, legible span |
| Accent | accent | Marks only. Figures and text take ink |
The light theme's reading span is deliberately wide. Because the paper is warm and slightly dark (L\* 96.26 rather than 100), the primary ink can sit at 18.03:1 without the harsh edge that #000 on #FFF produces at 21:1.
6.3 Dark theme behaviour
| Layer | Token | Behaviour |
|---|---|---|
| Ground | surface #090909 | Near-black, never pure |
| Raise | surface-raised #2A2A2A | 1.39:1, more separation than light, still decorative |
| Highlight | surface-accent #1A1E18 | A 14% green wash, never a solid pale fill |
| Reading | 19.04 → 8.87 → 6.08 | Slightly compressed at the top, wider at the bottom |
| Accent | accent | Text-capable. Green gains a role it does not have on light |
What changes:
- Green becomes text. 2.65:1 → 6.86:1.
accent-inkfollows it, so a component written once renders correct ink on light and correct green on dark. - The tertiary tier gains headroom.
#8E8E8Eat 6.08:1 on dark is a more comfortable metadata grey than#616161at 5.64:1 on light. - Surfaces separate more. 1.07:1 → 1.39:1, because a raise reads less on a dark ground.
- A pale fill is never used for a wash.
surface-accentis a translucent green over ink, not a light green. A solid pale panel on a dark page is a hole punched in the document.
What must never happen:
| Never | Measured | Why |
|---|---|---|
#4A4A4A as text on ink | 2.25:1 | The exact regression recorded in §6.1 |
#616161 as body text on ink | 3.22:1 | Light-ground metadata grey on the wrong ground. Use #8E8E8E |
#ADADAD as text on paper | 2.04:1 | Dark-ground grey on the wrong ground |
#8E8E8E as anything on paper | 2.98:1 | Fails even the 3:1 graphical-object floor |
| A solid pale green panel on ink | — | See point 4 |
| Inverting by overriding a fill | — | See §6.1 |
6.4 Print forces light
Print output renders the light theme regardless of the reader's screen theme, because ink on paper has no dark mode and a dark-ground PDF wastes toner and reads poorly when photocopied. The Report Engine's print.css enforces this. Colour in print is otherwise identical: no separate CMYK palette, no print-only accent.
7. Accessibility
Accessibility is a gate, not a note. Targets are owned by accessibility.md, whose colour and focus sections are measured; the colour values that satisfy them live here.
7.1 Thresholds
| Element | Minimum | Notes |
|---|---|---|
| Body text | 4.5:1 | Anything below 24 px, or below 18.66 px bold |
| Large text | 3:1 | 24 px and above, or 18.66 px bold and above |
| UI components, graphical objects | 3:1 | Includes a meaning-bearing rule or icon |
| Focus indicator | 3:1 | Against adjacent colours, both the ring and what it sits on |
| Data marks | 3:1 | Against the ground and against each other |
| Decorative surface, decorative rule | none | But it may not be the sole carrier of anything |
| Disabled control | none | Exempt, but text-tertiary clears 4.5:1 anyway, so no reader is punished |
7.2 Full contrast matrix
Measured 2026-07-23 and 2026-07-25.
On light paper #F6F4F1
| Foreground | Role | Ratio | Body | Large | UI |
|---|---|---|---|---|---|
#0A0A0A | text-primary, interactive, focus-ring, data-1 | 18.03 | ✅ | ✅ | ✅ |
#090909 | ink ground used as a mark | 18.14 | ✅ | ✅ | ✅ |
#4A4A4A | text-secondary, border-strong | 8.07 | ✅ | ✅ | ✅ |
#616161 | text-tertiary, disabled, data-2 | 5.64 | ✅ | ✅ | ✅ |
#8E8E8E | ⚠︎ misuse, dark-ground grey | 2.98 | ❌ | ❌ | ❌ |
#869F75 | accent, marks only | 2.65 | ❌ | ❌ | ❌ |
#ADADAD | ⚠︎ misuse, dark-ground grey | 2.04 | ❌ | ❌ | ❌ |
#E4E3E8 | border, decorative | 1.16 | ❌ | ❌ | ❌ |
#FCFCFB | surface-raised, decorative | 1.07 | ❌ | ❌ | ❌ |
#EBF0E9 | surface-accent, decorative | 1.05 | ❌ | ❌ | ❌ |
On dark ink #090909
| Foreground | Role | Ratio | Body | Large | UI |
|---|---|---|---|---|---|
#FAFAF7 | text-primary, interactive, focus-ring, data-1 | 19.04 | ✅ | ✅ | ✅ |
#F6F4F1 | paper used as a mark | 18.14 | ✅ | ✅ | ✅ |
#ADADAD | text-secondary, border-strong | 8.87 | ✅ | ✅ | ✅ |
#869F75 | accent, accent-ink, success | 6.86 | ✅ | ✅ | ✅ |
#8E8E8E | text-tertiary, disabled, data-2 | 6.08 | ✅ | ✅ | ✅ |
#616161 | ⚠︎ misuse, light-ground grey | 3.22 | ❌ | ✅ | ✅ |
#4A4A4A | ⚠︎ the §6.1 regression | 2.25 | ❌ | ❌ | ❌ |
#2A2A2A | border, surface-raised, decorative | 1.39 | ❌ | ❌ | ❌ |
On raised card #FCFCFB (light)
| Foreground | Ratio | Body | Large | UI |
|---|---|---|---|---|
#0A0A0A | 19.29 | ✅ | ✅ | ✅ |
#4A4A4A | 8.63 | ✅ | ✅ | ✅ |
#616161 | 6.03 | ✅ | ✅ | ✅ |
#869F75 | 2.83 | ❌ | ❌ | ❌ |
On green wash #EBF0E9 (light)
| Foreground | Ratio | Body | Large | UI |
|---|---|---|---|---|
#0A0A0A | 17.13 | ✅ | ✅ | ✅ |
#4A4A4A | 7.67 | ✅ | ✅ | ✅ |
#616161 | 5.36 | ✅ | ✅ | ✅ |
#869F75 | 2.51 | ❌ | ❌ | ❌ |
On dark raised surface #2A2A2A
| Foreground | Ratio | Body | Large | UI |
|---|---|---|---|---|
#FAFAF7 | 13.73 | ✅ | ✅ | ✅ |
#F6F4F1 | 13.07 | ✅ | ✅ | ✅ |
#ADADAD | 6.40 | ✅ | ✅ | ✅ |
#869F75 | 4.94 | ✅ | ✅ | ✅ |
#8E8E8E | 4.38 | ❌ | ✅ | ✅ |
⚠︎ Defect: dark tertiary text on a dark raised surface.
text-tertiary#8E8E8Eonsurface-raised#2A2A2Ameasures 4.38:1 and fails the 4.5:1 body threshold. Captions and metadata are small text, so a caption inside a dark card is currently non-conforming. It passes large-text and UI thresholds, so headings and rules are unaffected. RESOLVED 2026-08-26, see §14.1: tertiary on a raised surface resolves to#ADADAD(6.40:1) in the dark theme. The paragraph above describes the defect as it stood, and is kept because issued output still carries it. Not silently changed at the time, because it altered the rendering of already-validated report output.
On dark accent wash #1A1E18
The dark surface-accent is declared as rgba(134, 159, 117, 0.14) and composites over ink to #1A1E18. Measured against the composite:
| Foreground | Ratio | Body | Large | UI |
|---|---|---|---|---|
#FAFAF7 | 16.16 | ✅ | ✅ | ✅ |
#ADADAD | 7.53 | ✅ | ✅ | ✅ |
#869F75 | 5.82 | ✅ | ✅ | ✅ |
#8E8E8E | 5.16 | ✅ | ✅ | ✅ |
A translucent token must be measured composited, never as its own hex. The wash separates from the ink ground by only 1.18:1: decorative, as intended.
7.3 Beyond contrast
- Colour is never the sole carrier. §8.
- Focus is always visible. 2 px
focus-ring, 2 px offset. Neveroutline: nonewithout a replacement that measures 3:1 against adjacent colours. prefers-contrast: moreraisestext-secondaryandtext-tertiarytotext-primaryand promotesbordertoborder-strong. No new colours are introduced.- A meaning-bearing boundary uses
border-strong. A card whose only boundary isborder(1.16:1) is not perceivable as a control. Give itborder-strong(8.07:1), a label, or both. - Selection is not colour alone.
surface-selectedis paired with aborder-strongleft rule andaria-selected.
8. No colour carries meaning alone
8.1 The four channels
The Report Engine distinguishes status by geometry, so the signal survives greyscale, colour- vision deficiency, and a photocopier.
| Channel | Shape | Encodes |
|---|---|---|
| Severity | Square | critical · high · medium · low |
| State | Circle | pass · warning · fail · untested · blocked |
| Confidence | Dots | 1, 2 or 3 filled of 3 |
| Coverage | Bar | a proportion |
Fill, outline and half-fill distinguish values within a channel; the shape distinguishes the channel. Nothing depends on hue. Every marker also carries a text label.
8.2 The greyscale test
Any layout must be readable when every colour is replaced by its L\* value. In practice: print it in black and white. If two things that mean different things become the same thing, the layout is wrong and not the printer.
8.3 Why only two data series
A data mark must clear 3:1 against the ground and 3:1 against every other mark it can be confused with. On light paper those two requirements collide.
The ground is #F6F4F1, luminance Y = 0.90645. A mark clears 3:1 against it when
(0.90645 + 0.05) / (Y + 0.05) ≥ 3 → Y ≤ 0.26882
So every usable mark on light paper sits in the band Y ∈ [0, 0.26882], which is why #8E8E8E (Y = 0.27050) misses at 2.98:1 and green (Y = 0.31149) misses at 2.65:1.
Now fit n marks inside that band, each 3:1 from the next. The darkest possible is Y = 0, and each step multiplies (Y + 0.05) by 3:
mark 1: Y = 0 → Y + 0.05 = 0.05
mark 2: Y + 0.05 ≥ 0.15 → Y ≥ 0.10
mark 3: Y + 0.05 ≥ 0.45 → Y ≥ 0.40 ✗ exceeds the 0.26882 ceiling
Three tone-only series at 3:1 mutual separation are mathematically impossible on light paper. The same calculation on the ink ground gives a floor of Y ≥ 0.10819 and the identical result.
The chosen pair, verified:
| Light | Dark | |
|---|---|---|
data-1 | #0A0A0A, 18.03 on ground | #FAFAF7, 19.04 on ground |
data-2 | #616161, 5.64 on ground | #8E8E8E, 6.08 on ground |
| mutual | 3.20 ✅ | 3.13 ✅ |
Beyond two series, distinguish by pattern fill, direct labelling, or small multiples, never by adding a hue.
9. Proportion
Across any single layout, aim for roughly:
| Share | Element |
|---|---|
| 78% | Neutral ground |
| 17% | Ink |
| 5% | Green |
Green is a punctuation mark, not a colour field. If a layout reads as "the green one", the proportion is wrong.
Statistics and figures
Both families use cap-height lining figures. A statistic set in green on a light card is the exact failure the source found and corrected: three card numerals were changed to metadata grey. Figures on light paper take ink or tertiary grey, never green.
10. Prohibitions
| Prohibited | Measured | Instead |
|---|---|---|
#616161 as body text on ink | 3.22:1 | #8E8E8E (text-tertiary dark) |
#4A4A4A as text on ink | 2.25:1 | #ADADAD (text-secondary dark) |
#ADADAD as text on paper | 2.04:1 | #4A4A4A (text-secondary light) |
#8E8E8E anywhere on paper | 2.98:1 | #616161 (text-tertiary light) |
| Green as text on any light ground | 2.65 / 2.83 / 2.51:1 | accent-ink |
| Green on the green wash | 2.51:1 | Ink on the wash (17.13:1) |
| Green as a focus ring on light | 2.65:1 | focus-ring (18.03:1) |
Orange #E0522E | 3.53:1 on paper | Outside the palette entirely, see below |
| Any numeric-scale token name | — | A semantic token (§4.1) |
| Referencing a primitive directly | — | The semantic token, so a ground change propagates |
| Inverting a ground by overriding a fill | — | The theme attribute (§6.1) |
| A third tone-only data series | — | Pattern or direct label (§8.3) |
| Colour as the sole carrier of meaning | — | Shape + label (§8) |
Orange #E0522E is outside the palette. It appears as a brand rule across the previous social templates and contradicts the brand rule that red and orange are never accent colours. It measures 3.53:1 on paper, failing body text regardless. Replace with #869F75, or with ink where the rule sits on a green-adjacent ground.
11. Examples
Rendered live, in both themes, at docs/colour/colour-language.html §11.
Good
A finding block: ink heading, secondary body, a filled square for severity, a text label reading "Critical", a border-strong left rule, metadata in tertiary grey. Survives greyscale, survives deuteranopia, survives a photocopy. Zero hue used.
Acceptable
A summary card on surface-raised with a green rule-accent above the title. The rule is decoration; the title, the value and the label carry everything. The 2.65:1 green is legal because it means nothing on its own. Acceptable rather than good because the raise (1.07:1) adds a boundary a reader cannot quite see; border-strong would be firmer.
Poor
A three-series bar chart in ink, mid-grey and green, with a colour-only legend. Fails three ways: the mid-grey is 2.71:1 from the ink series, the green is 2.65:1 from the ground, and removing colour removes the legend's meaning entirely. Fix: two series with direct labels, or small multiples.
12. Figma parity
The repository is canonical for rules. Figma is canonical for geometry. Where they diverge, the markdown wins and Figma is corrected.
12.1 The contract
- Every semantic token in §4.2 exists as a Figma Colour Style whose paint is bound to the matching variable, so one style resolves correctly in both Light and Dark modes.
- Every Colour Style corresponds to a documented semantic token. No orphans in either direction.
- Primitives exist as styles too, in a separate
Primitive/group, marked reference-only. - A dark ground is achieved only by setting the Dark mode on a frame. Overriding a fill is the §6.1 failure and is not permitted.
12.2 Retired: the numeric-scale styles
The file previously carried 15 numeric-scale Colour Styles: white-100…60, black-100…60, accent-100…60. All 15 were retired 2026-07-25. They were wrong on three counts:
- Superseded values.
black-100was#09090Bandwhite-90was#FAFAFA, precisely the cool pairing this document supersedes (see the header).white-80/70/60andblack-90/80/70/60were a cool grey ramp belonging to no Fernando Kylas palette. - Drifted values.
accent-100was#879F75, one hex digit from the canonical#869F75.accent-70was#EBF0E8against the canonical#EBF0E9. Small enough to survive review, large enough to prove the styles had no authority. - Numeric names.
accent-80states a ramp position, not a job: exactly the form §4.1 forbids.
169 nodes had to be rebound to semantic styles before a single style could be deleted: 168 logo vectors and the cover ground. The drift had travelled further than the styles that caused it, which is the argument for a standing parity contract rather than a periodic audit. Recorded in DECISIONS.md; recoverable from Figma version history if Fernando disagrees.
13. Design decisions
13.1 Why we avoid pure black
#000000 is not available in this system. Reasons, in order of weight:
- It is not a printing ink. A rich black on press is a four-colour build; a single-channel
#000prints as a flat, slightly dusty grey. Ink at L\* 2.47 is honest about what paper does. - It creates halation on screen. Maximum-contrast black on near-white causes visible edge shimmer at small sizes, especially with Geomanist's even stroke weights.
#0A0A0Aon#F6F4F1at 18.03:1 is fully compliant and visibly calmer than 21:1. - It has nowhere to go. With
#000as the darkest value, no darker tone exists for a rule, a shadow, or an emphasis.#090909and#0A0A0Aleave that room. - It reads as default.
#000is what a page looks like before anyone made a decision.
13.2 Why we avoid pure white
Symmetrically: #FFFFFF is a screen artefact, not a paper. Real paper is warm and absorbs light. #F6F4F1 (L\* 96.26) reduces glare in long-form reading, flatters both typefaces, and makes the raised card #FCFCFB possible, because a card cannot be lighter than white. The reversed text #FAFAF7 carries the same warmth so a dark page does not read as a different brand.
13.3 Why Sage is restrained
The green is muted (L\ 62.63, low chroma) rather than saturated because a saturated accent competes with the type for attention, and this identity is carried by type. Its 5% proportion is what makes it legible as* a signal: an accent that appears constantly stops being an accent.
Its 2.65:1 on paper is not a flaw to be fixed. It is the reason green cannot creep into text roles, which is the main way restrained palettes decay. The constraint enforces the philosophy.
13.4 Why semantic colours exist at all, given they resolve to ink
A reasonable objection: if critical is just text-primary, why name it?
Three reasons. Intent is recorded. A reader of the code learns that this ink is a severity signal, not a heading. Change has one site. If a future decision gives critical findings a distinct treatment, one token changes rather than every call site. The vocabulary is a checklist. unmeasured and confidence exist because a real-data validation proved reports need to say "not collected" honestly, and a named token makes that state impossible to forget.
13.5 Why colour is never decoration
A consultancy sells judgement. A document that decorates itself invites the question of what else was decided for appearance rather than reason. Every colour in this system can be interrogated: this ink because it is the primary reading tier at 18.03:1; this green because the finding was positive; this grey because it is metadata. Nothing is here because it looked better.
13.6 Why three reading tiers and not five
Measured, the span 18.03 → 8.07 → 5.64 is already fine-grained; a fourth tier would sit where readers cannot perceive a difference but authors must still choose. Hierarchy beyond three tiers is carried by size and weight, which cost nothing in contrast and survive greyscale perfectly.
14. Open items and conflicts
Recorded rather than resolved. Each needs Fernando's decision.
14.1 ⚠︎ Dark tertiary text on a dark raised surface fails body contrast · COLOUR-01 · RESOLVED 2026-08-26
#8E8E8E on #2A2A2A measured 4.38:1 against a 4.5:1 requirement, so a caption, label or item of metadata inside any dark card was non-conforming.
Resolved by moving the text, not the card. On surface-raised in the dark theme, text-tertiary resolves to #ADADAD: 6.40:1. One scoped rule, no new primitive (#ADADAD is already the dark theme's own text-secondary), and no change to the light theme, where #616161 measures 6.03:1 on the raised card and was never at risk.
Why the card was not darkened, measured rather than assumed. The darkest ground on which #8E8E8E reaches 4.5:1 is #282828, which clears the threshold by 0.00 and drops the card's separation from the page from 1.39:1 to 1.35:1. Every darker ground buys margin nobody needs by making the card harder to find. And no lighter ground exists at any value: #8E8E8E would need a ground at relative luminance 1.392, and white is 1.000. So every route that keeps the text colour makes the card less visible, and the text was the thing to move.
One layer was missed, and the live site found it. The fix paired the raised token with the engine's own raised surfaces. The publication's .si-card and .si-footer take surface-raised too, and a si- class is the publication's rather than the engine's, so an engine fk-label inside a site card kept the page tertiary and went on measuring 4.38:1. Measured on the deployed site on 2026-08-26 and corrected there; the engine was already right. No static assertion could have caught it: it needs a rendered page, which is what the six host-dependent assertions are for.
Applied to new output only, by Fernando's decision of 2026-08-26. Report output already validated and issued as PDF, including an issued client review set, is not reissued: the defect is recorded against those documents rather than retrofitted into them. Recorded in DECISIONS.md.
14.2 Two values for primary ink · COLOUR-02 · RESOLVED 2026-08-26
The paper-tone decision names #090909 as "ink", while the light-paper text table and the signature contract specify #0A0A0A. The difference is imperceptible: a luminance delta of 0.000304, and a contrast between the two of 1.0058:1.
Both are kept, because they are two roles rather than one value written twice. #090909 is the dark ground; #0A0A0A is text on light paper. The chapter's own guess at the intent was correct, and it is now the rule. Confirmed by Fernando, 2026-08-26.
Collapsing them would have merged a surface token into a text token, which is the confusion this chapter exists to prevent everywhere else: §4.2 separates surface, text, interface and data roles precisely so that a value cannot be reused across them by accident. Two roles that happen to sit 0.0003 apart are still two roles, and the near-identical numbers are a coincidence of the palette rather than evidence of duplication.
14.3 Token prefix and naming form · COLOUR-03 · Open
See the open item in §4.1. Recommendation: keep --fk-<role>.
14.4 Dark-ground raised surface is borrowed, not designed · COLOUR-04 · RESOLVED 2026-08-26
surface-raised on dark reuses line-rev #2A2A2A, a rule colour pressed into service as a surface. It was never chosen as a surface, and this item asked whether a purpose-designed value should replace it.
It should not, and the reason is the measurement in §14.1. This item claimed that "a purpose-designed dark card value would resolve §14.1 at the same time". That was wrong, and it is corrected here rather than quietly dropped: a purpose-designed value can only go darker, the darkest workable ground passes by 0.00, and every candidate reduces the card's separation from the page. There is no better neutral value, because the alternatives were enumerated rather than imagined.
So #2A2A2A is adopted as the designed value. Nothing renders differently. What changes is that it stops being described as borrowed: it is the value that was measured against every alternative and found better than all of them. 1.39:1 separation stands, and with §14.1 resolved every reading tier on the card now passes.
What this does not close. A card ground with a hue rather than a neutral grey was not measured, because the palette is warm and a tinted dark card is a palette question rather than a contrast one. If that is ever wanted it is a new item, not a reopening of this one.
14.5 The divider is the only cool value in a warm palette · COLOUR-05 · Open
border / line #E4E3E8 measures hsl(252, 10%, 90%), a violet-leaning cool grey. Every other value in the palette is warm or neutral: paper is hsl(36), the raised card hsl(60), the reversed text hsl(60), the greys hsl(0), the green hsl(96).
It is very likely a survivor of the superseded cool palette: Figma's retired white-70 was #E4E4E7, one digit away (§12.2).
No accessibility consequence: at 1.16:1 it is decorative (§4.2). It is a coherence problem: a cool hairline across a warm page is the kind of half-degree mismatch that reads as cheapness without being locatable.
Recommended fix: a warm equivalent at matched lightness, around #E6E3DE. Not applied, because it changes every rendered report and every Figma frame, so it wants a DECISIONS.md entry.
14.6 Source ratios differ slightly from measured · COLOUR-06 · Open
The source gives primary ink on paper as 18.9 against a measured 18.03, and #616161 on ink as 3.4 against a measured 3.22. Neither changes a pass/fail outcome. Measured values are used throughout.
14.7 Resolved since version 1
COLOUR-07· RESOLVED 2026-07-23 ·Dark secondary token:#A6A6A6or#ADADAD?typography-lab.htmlis canonical and declares--rev-2:#ADADAD(8.87:1).#A6A6A6is Superseded.COLOUR-08· RESOLVED 2026-07-25 ·No legal focus indicator exists.focus-ringis defined in §4.2 and measured against every surface in the system. The previous--shadow-focustoken, which composited to roughly 1.1:1, is Superseded and must not be carried into v3.1.COLOUR-09· RESOLVED 2026-07-25 ·Semantic roles beyond text undefined.States, interaction, data and status roles are defined in §4.2 and §8. The one remaining gap is §14.4.
15. In closing
One accent, three reading tiers, two grounds, and a formal limit of two tone-only data series. That is a deliberately small palette, and the chapter's argument is that its size is what makes it usable: every value in it has a measured ratio against every ground it can appear on, which is only affordable because there are few of them.
The practical consequence is that colour is never the answer on its own. Severity is a shape and a word before it is a tone; a status is a label before it is a colour; an accent is a mark rather than text on light paper. Anything that would need a new hue to work needs a different solution.
One thing to carry forward: a token names a job, never a colour. If a value is ever referred to by its hex rather than its role, the reference will be wrong the first time a ground changes.
Next: Grid, Layout & Rhythm, which applies this chapter's first principle to space.
16. Provenance
- Source:
typography-v3.1.md§9 and §13, read read-only and reconciled. Preserved atdocs/typography/; the original also remains in the previous website project. - Palette version: v3.1, the warm paper palette. Declared as a
Palette versionheader field until 2026-08-25, when the metadata schema retired it as an unknown field. The value is unchanged; only the place it is recorded moved. - Ratios: computed 2026-07-23 (v3.1 palette) and 2026-07-25 (states, interaction, composites, data series, surface matrices) using WCAG 2.x relative luminance over sRGB. Source figures agreed within ±0.1 except the two noted in §14.6.
- Figma audit: file
8THxEHQJfyzTAdJbd83YcO, inspected 2026-07-25: 20 pages, 3 variable collections, 34 text styles, 15 numeric-scale paint styles retired (§12.2). - Not carried forward: the source's July 2026 node-count audit ("all 257 text nodes meet their required threshold"). It is build-specific, the source warns against quoting it forward, and it must be re-measured after any change.
- Related:
typography.md§13 duplicates the green asymmetry and theme-inversion rules as an interim measure. This file is authoritative for both. Remove the duplicates fromtypography.mdonce this is approved.