Skip to content

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.

Status
Approved
Status note
Canonical. The colour language of the Fernando Kylas Brand System; three items are acknowledged and intentionally excluded (§14).
Version
4
Updated
2026-08-26
Approved by
Fernando Kylas
Approved
2026-08-26
Supersedes
the cooler #FAFAFA / #09090B pairing recorded in the previous project's design.md and tokens.css.
Open items
3 open, 6 resolved, of 9 recorded
Canonical source
foundations/colour.md

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.

CompanionPurpose
docs/colour/colour-language.htmlThe chapter rendered as a reading experience, with live components
assets/colours/Machine-readable token files for design tools
platform/design-system/css/tokens.cssThe reference implementation
typography.md · accessibility.mdThe 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:

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 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 thisBecause
A red / amber / green status paletteFails greyscale, fails deuteranopia, and reads as a dashboard rather than a document
A 10-step tint ramp per hueNine of the ten steps have no documented role, so they become decoration
Pure black #000000See §13.1
Pure white #FFFFFFSee §13.2
Colour-coded chapters or sectionsPosition 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.

RoleValue
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.

ValueLab tokenEngine tokenL\*Description
#F6F4F1--paper--fk-c-paper96.26Warm paper
#FCFCFB--paper-card--fk-c-paper-card98.94Raised card surface on light
#FAFAF7--rev-1--fk-c-rev-198.20Warm off-white, reversed text
#090909--ink--fk-c-ink2.47Ink ground
#0A0A0A--ink-1--fk-c-ink-12.74Primary ink, text on light
#4A4A4A--ink-2--fk-c-ink-231.46Mid grey, secondary text on light
#616161--ink-3--fk-c-ink-341.14Metadata grey on light
#ADADAD--rev-2--fk-c-rev-270.72Secondary text on dark
#8E8E8E--rev-3--fk-c-rev-359.02Metadata grey on dark
#869F75--green--fk-c-green62.63The one accent hue
#EBF0E9--green-wash--fk-c-green-wash94.25Green wash surface, light only
#E4E3E8--line--fk-c-line90.44Rule and divider, light ground
#2A2A2A--line-rev--fk-c-line-rev17.06Rule 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
TokenLightRatioDarkRatioJob
surface#F6F4F1#090909Page and frame ground
surface-raised#FCFCFB1.07#2A2A2A1.39Card, panel, evidence block
surface-accent#EBF0E91.05rgba(134,159,117,.14)#1A1E181.18Highlighted 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
TokenLightRatioDarkRatioJob
text-primary#0A0A0A18.03#FAFAF719.04Display, headings, body
text-secondary#4A4A4A8.07#ADADAD8.87Supporting copy, long-form body
text-tertiary#6161615.64#8E8E8E6.08Captions, 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
TokenLightRatioDarkRatioJob
border#E4E3E81.16#2A2A2A1.39Decorative hairline. Never meaning-bearing
border-strong#4A4A4A8.07#ADADAD8.87A rule that carries meaning or bounds a control
rule-accent#869F752.65#869F756.86The 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
TokenLightRatioDarkRatioJob
accent#869F752.65 ❌#869F756.86Marks only on light. Text-capable on dark
accent-ink#0A0A0A18.03#869F756.86The 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 tokenLight resolves toDark resolves toCarried alongside by
criticaltext-primarytext-primaryFilled square + label
warningtext-primarytext-primaryHalf-filled square + label
successaccent-ink (ink)accent-ink (green)Filled circle + label
informationtext-secondarytext-secondaryOutlined circle + label
observationtext-secondarytext-secondaryOutlined circle + label
evidencesurface-raisedsurface-raisedPanel + source line
confidencetext-primarytext-primary1–3 filled dots of 3
disabledtext-tertiarytext-tertiaryaria-disabled + no affordance
unmeasuredtext-tertiarytext-tertiaryDashed 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
TokenLightRatioDarkRatioJob
interactive#0A0A0A18.03#FAFAF719.04Link and control text
interactive-hover#0A0A0A18.03#FAFAF719.04Text colour does not change, see below
focus-ring#0A0A0A18.03#FAFAF719.042 px ring, 2 px offset
surface-selected= surface-accent1.05= surface-accent1.18Selected 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 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
TokenLightRatioDarkRatio
data-1#0A0A0A18.03#FAFAF719.04
data-2#6161615.64#8E8E8E6.08
data-accent#869F752.65#869F756.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 classWhat it doesTokensGoverning constraint
BrandMakes the system recognisablesurface, text-primary, accentNever changes without a DECISIONS.md entry
InterfaceMakes controls operableinteractive, focus-ring, border-strong, disabled, surface-selected3:1 for the component, 4.5:1 for its label
EditorialMakes long-form reading comfortabletext-primary/secondary/tertiary, border, surface-raised, rule-accentReading hierarchy must survive greyscale
DataDistinguishes quantitiesdata-1, data-2, data-accent3:1 against the ground and 3:1 against each other
StatusClassifies findingscritical, warning, success, information, confidence, unmeasuredNever 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 systemFigma variableBrief's proposed form
--fk-surfacesurface--colour-background
--fk-surface-raisedsurface-raised--colour-panel
--fk-text-primarytext-primary--colour-reading-primary
--fk-text-secondarytext-secondary--colour-reading-secondary
--fk-text-tertiarytext-tertiary--colour-reading-tertiary
--fk-accentaccent--colour-accent
--fk-borderborder--colour-divider
--fk-successsuccess--colour-success
--fk-warningwarning--colour-warning
--fk-criticalcritical--colour-critical

5. The green asymmetry

Green is not one accent. It behaves differently on each ground, and this is deliberate.

GroundRatioPermitted for
Light paper #F6F4F12.65:1Marks only: rules, underlines, dividers, non-text accents
Raised card #FCFCFB2.83:1Marks only
Green wash #EBF0E92.51:1Nothing. The wash is a ground for ink, never for green
Dark ink #0909096.86:1Text and marks
Dark raised #2A2A2A4.94:1Text and marks
Dark accent wash #1A1E185.82:1Text 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

LayerTokenBehaviour
Groundsurface #F6F4F1Warm, low-glare, print-led
Raisesurface-raised #FCFCFB1.07:1, a suggestion of a card, not a box
Highlightsurface-accent #EBF0E91.05:1, the quietest possible emphasis
Reading18.03 → 8.07 → 5.64A wide, legible span
AccentaccentMarks 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

LayerTokenBehaviour
Groundsurface #090909Near-black, never pure
Raisesurface-raised #2A2A2A1.39:1, more separation than light, still decorative
Highlightsurface-accent #1A1E18A 14% green wash, never a solid pale fill
Reading19.04 → 8.87 → 6.08Slightly compressed at the top, wider at the bottom
AccentaccentText-capable. Green gains a role it does not have on light

What changes:

  1. Green becomes text. 2.65:1 → 6.86:1. accent-ink follows it, so a component written once renders correct ink on light and correct green on dark.
  2. The tertiary tier gains headroom. #8E8E8E at 6.08:1 on dark is a more comfortable metadata grey than #616161 at 5.64:1 on light.
  3. Surfaces separate more. 1.07:1 → 1.39:1, because a raise reads less on a dark ground.
  4. A pale fill is never used for a wash. surface-accent is 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:

NeverMeasuredWhy
#4A4A4A as text on ink2.25:1The exact regression recorded in §6.1
#616161 as body text on ink3.22:1Light-ground metadata grey on the wrong ground. Use #8E8E8E
#ADADAD as text on paper2.04:1Dark-ground grey on the wrong ground
#8E8E8E as anything on paper2.98:1Fails even the 3:1 graphical-object floor
A solid pale green panel on inkSee point 4
Inverting by overriding a fillSee §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

ElementMinimumNotes
Body text4.5:1Anything below 24 px, or below 18.66 px bold
Large text3:124 px and above, or 18.66 px bold and above
UI components, graphical objects3:1Includes a meaning-bearing rule or icon
Focus indicator3:1Against adjacent colours, both the ring and what it sits on
Data marks3:1Against the ground and against each other
Decorative surface, decorative rulenoneBut it may not be the sole carrier of anything
Disabled controlnoneExempt, 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
ForegroundRoleRatioBodyLargeUI
#0A0A0Atext-primary, interactive, focus-ring, data-118.03
#090909ink ground used as a mark18.14
#4A4A4Atext-secondary, border-strong8.07
#616161text-tertiary, disabled, data-25.64
#8E8E8E⚠︎ misuse, dark-ground grey2.98
#869F75accent, marks only2.65
#ADADAD⚠︎ misuse, dark-ground grey2.04
#E4E3E8border, decorative1.16
#FCFCFBsurface-raised, decorative1.07
#EBF0E9surface-accent, decorative1.05
On dark ink #090909
ForegroundRoleRatioBodyLargeUI
#FAFAF7text-primary, interactive, focus-ring, data-119.04
#F6F4F1paper used as a mark18.14
#ADADADtext-secondary, border-strong8.87
#869F75accent, accent-ink, success6.86
#8E8E8Etext-tertiary, disabled, data-26.08
#616161⚠︎ misuse, light-ground grey3.22
#4A4A4A⚠︎ the §6.1 regression2.25
#2A2A2Aborder, surface-raised, decorative1.39
On raised card #FCFCFB (light)
ForegroundRatioBodyLargeUI
#0A0A0A19.29
#4A4A4A8.63
#6161616.03
#869F752.83
On green wash #EBF0E9 (light)
ForegroundRatioBodyLargeUI
#0A0A0A17.13
#4A4A4A7.67
#6161615.36
#869F752.51
On dark raised surface #2A2A2A
ForegroundRatioBodyLargeUI
#FAFAF713.73
#F6F4F113.07
#ADADAD6.40
#869F754.94
#8E8E8E4.38

⚠︎ Defect: dark tertiary text on a dark raised surface. text-tertiary #8E8E8E on surface-raised #2A2A2A measures 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:

ForegroundRatioBodyLargeUI
#FAFAF716.16
#ADADAD7.53
#869F755.82
#8E8E8E5.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


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.

ChannelShapeEncodes
SeveritySquarecritical · high · medium · low
StateCirclepass · warning · fail · untested · blocked
ConfidenceDots1, 2 or 3 filled of 3
CoverageBara 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:

LightDark
data-1#0A0A0A, 18.03 on ground#FAFAF7, 19.04 on ground
data-2#616161, 5.64 on ground#8E8E8E, 6.08 on ground
mutual3.203.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:

ShareElement
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

ProhibitedMeasuredInstead
#616161 as body text on ink3.22:1#8E8E8E (text-tertiary dark)
#4A4A4A as text on ink2.25:1#ADADAD (text-secondary dark)
#ADADAD as text on paper2.04:1#4A4A4A (text-secondary light)
#8E8E8E anywhere on paper2.98:1#616161 (text-tertiary light)
Green as text on any light ground2.65 / 2.83 / 2.51:1accent-ink
Green on the green wash2.51:1Ink on the wash (17.13:1)
Green as a focus ring on light2.65:1focus-ring (18.03:1)
Orange #E0522E3.53:1 on paperOutside the palette entirely, see below
Any numeric-scale token nameA semantic token (§4.1)
Referencing a primitive directlyThe semantic token, so a ground change propagates
Inverting a ground by overriding a fillThe theme attribute (§6.1)
A third tone-only data seriesPattern or direct label (§8.3)
Colour as the sole carrier of meaningShape + 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

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:

  1. Superseded values. black-100 was #09090B and white-90 was #FAFAFA, precisely the cool pairing this document supersedes (see the header). white-80/70/60 and black-90/80/70/60 were a cool grey ramp belonging to no Fernando Kylas palette.
  2. Drifted values. accent-100 was #879F75, one hex digit from the canonical #869F75. accent-70 was #EBF0E8 against the canonical #EBF0E9. Small enough to survive review, large enough to prove the styles had no authority.
  3. Numeric names. accent-80 states 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:

  1. It is not a printing ink. A rich black on press is a four-colour build; a single-channel #000 prints as a flat, slightly dusty grey. Ink at L\* 2.47 is honest about what paper does.
  2. 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. #0A0A0A on #F6F4F1 at 18.03:1 is fully compliant and visibly calmer than 21:1.
  3. It has nowhere to go. With #000 as the darkest value, no darker tone exists for a rule, a shadow, or an emphasis. #090909 and #0A0A0A leave that room.
  4. It reads as default. #000 is 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

  1. COLOUR-07 · RESOLVED 2026-07-23 · Dark secondary token: #A6A6A6 or #ADADAD? typography-lab.html is canonical and declares --rev-2:#ADADAD (8.87:1). #A6A6A6 is Superseded.
  2. COLOUR-08 · RESOLVED 2026-07-25 · No legal focus indicator exists. focus-ring is defined in §4.2 and measured against every surface in the system. The previous --shadow-focus token, which composited to roughly 1.1:1, is Superseded and must not be carried into v3.1.
  3. 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