Foundations
Design Tokens
The single source of truth for colour, typography, spacing, shadow, and radius values used across every CAAT component and template.
Overview
Design tokens are named CSS custom properties that encode the brand's visual decisions. Every CAAT component references tokens instead of hard-coded values, ensuring consistency and enabling theming at a single point of change.
All tokens are defined in /assets/css/tokens.css on the :root selector and are available globally. Components should never use raw hex values or pixel measurements — always reference the token.
Token tiers
Tokens are organized in tiers so a brand change happens in one place and cascades everywhere:
| Tier | Role | Example | Who uses it |
|---|---|---|---|
| 1 · Primitive | Raw values. The only place literal colours / sizes live. | --caat-blue-900: #003750 | Semantic tier (rarely components directly) |
| 2 · Semantic | Intent-based aliases. Components should consume these. | --caat-color-action-primary: var(--caat-blue-900) | Components & templates |
| 3 · Bootstrap | Maps semantic tokens onto Bootstrap's --bs-* vars. | --bs-primary: var(--caat-color-action-primary) | Bootstrap internals |
Theme: light only (by decision). The semantic tier still earns its keep — a rebrand or future theme only re-points Tier 2. See docs/design-system-recommendations.md §A.
| Category | Prefix |
|---|---|
| Colour (primitive) | --caat-blue-*, --caat-green-*, --caat-grey-*, … |
| Colour (semantic) | --caat-color-text-*, --caat-color-bg-*, --caat-color-action-*, --caat-color-feedback-* |
| Typography | --caat-font-size-*, --caat-leading-*, --caat-weight-*, --caat-text-* |
| Spacing | --caat-space-1 … --caat-space-9 |
| Radius / Border | --caat-radius-*, --caat-border-* |
| Elevation | --caat-shadow-sm/-/-md/-lg |
| Motion | --caat-duration-*, --caat-ease-* |
| Layering | --caat-z-* |
| Focus / Opacity | --caat-focus, --caat-opacity-* |
| Bootstrap overrides | --bs-* |
Colour Tokens
The palette is divided into brand blues, greens, neutrals, and semantic colours for feedback states.
Blues
--caat-blue-900
#003750
--caat-blue-700
#0b5a80
--caat-blue
#0f6791
--caat-blue-500
#2f95d2
--caat-blue-300
#9dd2ed
--caat-blue-100
#e7f4fb
Greens
--caat-green-900
#204b1f
--caat-green-700
#367f32
--caat-green
#55a546
--caat-green-100
#e8f5e6
--caat-lime
#95e35c
--caat-teal
#12a7b8
Neutrals
--caat-ink
#102637
--caat-muted
#506273
--caat-grey-300
#cbd7e3
--caat-grey-200
#dfe6ef
--caat-grey
#eff2f7
--caat-beige
#f8f5ee
Semantic / Feedback
--caat-success
#2f7d32
--caat-warning
#efaa39
--caat-danger
#b83232
Colour Usage Reference
| Context | Token | Example |
|---|---|---|
| Primary text / headings | --caat-blue-900 | Page titles, hero headings |
| Body text | --caat-ink | Paragraphs, list items |
| Secondary text | --caat-muted | Subtitles, captions, meta |
| Primary buttons / links | --caat-blue-900 | CTAs, navigation links |
| Secondary actions | --caat-green | Secondary buttons, accents |
| Eyebrow labels | --caat-color-eyebrow → --caat-blue-700 | Category labels above headings |
| Eyebrow on dark | --caat-color-eyebrow-on-dark → --caat-blue-300 | Labels on dark backgrounds (default) |
| Eyebrow on dark — hero | --caat-color-eyebrow-on-dark-hero → --caat-lime | Hero eyebrows only — deliberate brand accent |
| Section backgrounds | --caat-grey | Alternating section bands |
| Dark section backgrounds | --caat-blue-900 | Stat strips, dark heroes, footers |
| Card / input borders | --caat-grey-200 | Card outlines, dividers |
| Success / positive | --caat-success | Alert success, form validation |
| Warning | --caat-warning | Alert warning |
| Error / danger | --caat-danger | Alert danger, form errors |
Colour Contrast (AODA / WCAG AA)
CAAT is bound by the AODA, which requires WCAG 2.0 AA today and WCAG 2.2 AA by 2027 — we build to 2.2 AA now. Colour contrast thresholds:
- 4.5:1 — normal text
- 3:1 — large text (≥24px, or ≥18.66px bold)
- 3:1 — UI components, graphical objects & focus indicators (1.4.11)
An audit script computes the ratio for every real pairing. Re-run after any colour change:
node scripts/contrast-audit.js # exits non-zero if any required pairing fails
Fixes applied
| Issue | Was | Now |
|---|---|---|
| Focus ring invisible | rgba(47,149,210,.35) — 1.47:1 on white | Solid --caat-blue-500 — 3.31:1 light / 3.82:1 dark |
| Secondary button (white on green) | --caat-green — 3.06:1 | --caat-green-700 fill — 4.95:1 (hover green-900) |
| Form-control borders too faint | grey-200 — 1.26:1 | --caat-color-border-interactive (grey-500) — 4.21:1 |
Usage restrictions (colours that fail on light surfaces)
These colours are fine in their intended role but must not be used as described:
| Colour | Ratio on white | Rule |
|---|---|---|
--caat-warning (amber) | 2.0:1 | Dark text only, never white. Don't signal by colour alone — pair with an icon/label (1.4.1). |
--caat-teal | 2.9:1 (white on teal) | Decorative accent — don't place white (or any 4.5:1 text) on it. |
--caat-lime | 1.56:1 | Use on dark or as a fill behind dark text (e.g. selection, hero eyebrow) — never as text/UI on white. |
--caat-blue-300 | 1.63:1 | Tint / on-dark text only — not a UI boundary on white. |
--caat-grey-200 / -300 | 1.26 / 1.46:1 | Decorative dividers only (1.4.11 exempt). For control borders use --caat-color-border-interactive. |
--caat-disabled | 2.92:1 | Disabled text — WCAG-exempt by design (inactive controls). |
Semantic Tokens
Semantic tokens express intent ("primary action", "muted text") rather than a raw value. Components should reference these — they alias the primitive palette, so a brand change happens once, here.
Text
| Token | Aliases | Usage |
|---|---|---|
--caat-color-text | --caat-ink | Default body text |
--caat-color-text-muted | --caat-muted | Secondary / supporting text |
--caat-color-text-inverse | #fff | Text on dark / brand backgrounds |
--caat-color-text-inverse-muted | --caat-white-88 | Body copy on dark / brand surfaces |
--caat-color-text-inverse-dim | --caat-white-70 | Meta / secondary text on dark surfaces |
--caat-color-text-link | --caat-blue-900 | Links (resting) |
--caat-color-text-link-hover | --caat-blue-700 | Links (hover) |
--caat-color-text-disabled | --caat-disabled | Disabled text |
Surfaces & borders
| Token | Aliases | Usage |
|---|---|---|
--caat-color-bg-page | #fff | Page background |
--caat-color-bg-subtle | --caat-grey | Tinted section band |
--caat-color-bg-brand | --caat-blue-900 | Dark brand band |
--caat-color-bg-brand-subtle | --caat-blue-100 | Light brand tint |
--caat-color-bg-overlay | rgba(0,0,0,.25) | Modal / drawer scrim |
--caat-color-bg-hero-panel | --caat-blue-glass | Hero overlay panel (glass over imagery) |
--caat-color-bg-hero-panel-hover | --caat-blue-glass-hover | Hero overlay panel — hover |
--caat-overlay-side | --caat-scrim-soft → -faint | Hero media gradient scrim (text-side legibility) |
--caat-overlay-bottom | --caat-scrim-strong → -mid | Overlay-card gradient scrim (bottom legibility) |
--caat-color-border | --caat-grey-200 | Default borders / dividers |
--caat-color-border-strong | --caat-grey-300 | Emphasised borders |
--caat-color-border-inverse | --caat-white-30 | Dividers on dark / brand surfaces |
Actions & focus
| Token | Aliases | Usage |
|---|---|---|
--caat-color-action-primary | --caat-blue-900 | Primary buttons / CTAs |
--caat-color-action-primary-hover | --caat-blue-700 | Primary hover |
--caat-color-action-secondary | --caat-green | Secondary actions |
--caat-color-focus-ring | --caat-focus | Focus ring (box-shadow) |
--caat-color-focus-ring-inset | --caat-focus-inset | Inset focus ring (clipped containers) |
--caat-color-focus-edge | --caat-blue-500 | Focused input border |
Feedback
Each status has matching -bg, -border, and (where text sits on the fill) -text tokens: --caat-color-feedback-info-*, -success-*, -warning-*, -danger-*.
Typography
CAAT uses Libre Franklin as its sole typeface across all digital touchpoints. The font is loaded from Google Fonts in weights 300–900.
Font Families
| Token | Value | Usage |
|---|---|---|
--caat-font-primary |
"Libre Franklin", Arial, Helvetica, sans-serif | All body text, headings, UI elements |
--caat-font-secondary |
Arial, Helvetica, sans-serif | System fallback for email / print templates |
Letter Spacing
| Token | Value | Usage |
|---|---|---|
--caat-letter-spacing-normal | 0 | Body text |
--caat-letter-spacing-tight | -.015em | Large headings for visual tightening |
--caat-letter-spacing-label | .06em | Uppercase labels, eyebrows, badges |
Font Weights
Type Scale
The heading scale uses .caat-title classes from the Text / Title component. All headings use weight 800, tight letter-spacing, and --caat-blue-900.
Title & Text CSS Classes
The Text / Title component provides ready-made utility classes for typography.
| Class | Usage | Size | Weight |
|---|---|---|---|
.caat-title | Base title — apply with heading level modifier | — | 800 |
.caat-title--display | Hero-level display heading | 3.5rem | 900 |
.caat-title--h1 | Page title | 2.5rem | 800 |
.caat-title--h2 | Section heading | 2rem | 800 |
.caat-title--h3 | Sub-section heading | 1.75rem | 800 |
.caat-title--h4 | Card / panel heading | 1.5rem | 800 |
.caat-title--h5 | Minor heading | 1.25rem | 800 |
.caat-title--h6 | Smallest heading | 1rem | 800 |
.caat-title--section | Section heading with bottom border | 1.75rem | 800 |
.caat-title__subtitle | Supporting text below a heading | 1.25rem | 400 |
| Class | Usage | Size |
|---|---|---|
.caat-text | Base body text | 1rem |
.caat-text--lead | Lead / intro paragraph | 1.25rem |
.caat-text--small | Small / caption text | .875rem |
.caat-text--muted | De-emphasized text (60% opacity) | inherit |
.caat-rich-text | Canonical long-form prose container — vertical rhythm for headings, paragraphs, lists, links (see below) | inherit |
Long-form prose & vertical rhythm
Running prose (articles, guides, policy) uses a typographic rhythm, not the 4px UI spacing scale. The principle: space belongs to what follows — a heading opens a large gap before it (it begins a new section) and a small gap after it (so it attaches to its first paragraph). The cadence is heading-before > paragraph-flow > heading-after. Use the canonical .caat-rich-text container; other components (e.g. the Article Layout body) compose it.
Rendered — .caat-rich-text
Your DBplus pension provides a secure, predictable retirement income for life. Understanding how it works is the first step to making the most of it.
How your pension is calculated
Notice the generous space above this heading — it signals a new section — and the tighter space below it, so the heading sits with the paragraph it introduces.
Following paragraphs are separated by one consistent flow gap, keeping a steady reading rhythm down the page.
Credited interest
Sub-headings open a slightly smaller gap than section headings, so hierarchy is felt through spacing as well as size.
- Lists share the paragraph flow gap…
- …with tighter spacing between items.
Prose tokens
| Token | Value | Role |
|---|---|---|
--caat-prose-leading | 1.7 | Body line-height |
--caat-prose-flow | 1.25rem | Between paragraphs / lists |
--caat-prose-h2-space-before | 2.5rem | Before an h2 (section) |
--caat-prose-h3-space-before | 2rem | Before an h3 |
--caat-prose-h4-space-before | 1.5rem | Before an h4 |
--caat-prose-heading-space-after | 0.5rem | After any heading |
--caat-prose-measure | 68ch | Max line length |
Heading before ≫ paragraph flow ≫ heading after. The block's first child has no top gap and its last child no bottom gap.
.caat-rich-text) for running copy, where a heading needs more space before than after. Use the even single-gap Stack rhythm for UI groupings — card content, form fields, hero copy.
Eyebrow (overline label)
A short kicker above a heading used for categorization or context only. There is now one canonical class, .caat-eyebrow, driven by the --caat-text-eyebrow-* / --caat-color-eyebrow tokens. The legacy component-scoped aliases (.caat-title__eyebrow, .caat-feature-hero__eyebrow) have been removed; all markup uses the canonical class.
A better pension matters
The attraction and retention strategy every employer should be thinking about.
A better pension matters
Add --on-dark over imagery or dark brand bands.
<!-- on light -->
<span class="caat-eyebrow">Pension Solutions</span>
<h2 class="caat-title caat-title--h2">A better pension matters</h2>
<!-- on dark / imagery -->
<span class="caat-eyebrow caat-eyebrow--on-dark">Pension Solutions</span>
Do
- Use above a page or section title to label topic, series, or content type.
- Keep it short — ≤ 24 characters / 2–3 words. Allow for French running ~30% longer.
- Make the headline stand on its own — the eyebrow only adds context.
- Add
--on-darkover dark/photographic backgrounds.
Don't
- Don't mark it up as a heading (
<h*>) — use<span>/<p>so it doesn't pollute the document outline for screen readers. - Don't put critical information in it — users skip it on first scan.
- Don't use it above sub-headings inside body content.
- Don't restyle per-instance — change the tokens, not the element.
Accessibility & rationale: see docs/design-system-recommendations.md §B1.
Spacing & Radius
Spacing Scale
A 4px-based scale. Use these for padding, margins, and gaps instead of arbitrary values — it's what keeps rhythm consistent across components and the page-layout grid.
| Token | Value | Typical usage | |
|---|---|---|---|
--caat-space-1 | .25rem · 4px | Icon gaps, hairline insets | |
--caat-space-2 | .5rem · 8px | Tight gaps, chip padding | |
--caat-space-3 | .75rem · 12px | Input padding, small gaps | |
--caat-space-4 | 1rem · 16px | Default element spacing | |
--caat-space-5 | 1.5rem · 24px | Card padding, stack gaps | |
--caat-space-6 | 2rem · 32px | Group separation | |
--caat-space-7 | 3rem · 48px | Sub-section spacing | |
--caat-space-8 | 4rem · 64px | Section padding (mobile) | |
--caat-space-9 | 6rem · 96px | Section padding (desktop) |
Radius
| Token | Value | Usage |
|---|---|---|
--caat-radius-sm |
.375rem (6px) | Small controls, tags, inline code |
--caat-radius |
.75rem (12px) | Cards, buttons, inputs, images, badges |
--caat-radius-lg |
1.25rem (20px) | Hero frames, modals, large panels, stat strips |
--caat-radius-hero |
4.5rem (72px) | Hero panel signature corner (bottom-right only) |
--caat-radius-pill |
999px | Pills, status badges, toggles |
Radius Preview
0
None
--caat-radius
.75rem
--caat-radius-lg
1.25rem
50%
Circle (icons)
Shadows & Elevation
Elevation maps to a role, not a pixel value — pick by what the surface is, so layering reads as a system.
| Token | Value | Elevation role |
|---|---|---|
--caat-shadow-sm |
0 .25rem .75rem rgba(0,55,80,.08) | Raised card / card-on-hover |
--caat-shadow |
0 .75rem 1.75rem rgba(0,55,80,.10) | Default elevation: cards, hero frames |
--caat-shadow-md |
0 1rem 2.5rem rgba(0,55,80,.14) | Dropdown / popover |
--caat-shadow-lg |
0 1.5rem 3.5rem rgba(0,55,80,.20) | Modal / dialog |
--caat-shadow-hover |
0 1.25rem 3rem rgba(0,55,80,.24) | Lifted hero panel (hover) |
Shadow Preview
none
--caat-shadow-sm
--caat-shadow
Motion
Consistent timing makes the UI feel like one product. Pair a duration with an easing; default to --caat-duration-base + --caat-ease-standard for most transitions.
| Token | Value | Usage |
|---|---|---|
--caat-duration-fast | 120ms | Hover, small state changes |
--caat-duration-base | 200ms | Default — most transitions |
--caat-duration-slow | 320ms | Drawers, modals, expand/collapse |
--caat-ease-standard | cubic-bezier(.2, 0, 0, 1) | Most transitions |
--caat-ease-emphasized | cubic-bezier(.3, 0, 0, 1) | Enter / expand |
--caat-ease-exit | cubic-bezier(.4, 0, 1, 1) | Leave / collapse |
--caat-lift-subtle | -1px | Hover lift: buttons, small controls |
--caat-lift-row | -2px | Hover lift: list rows, logo tiles |
--caat-lift-card | -3px | Hover lift: cards in grids |
--caat-lift-panel | -4px | Hover lift: hero panels, large previews |
caat-base.css honours prefers-reduced-motion: reduce and collapses animations/transitions for users who request it — you don't need to repeat it per component (WCAG 2.2).
Z-index / Layering
A single ordered scale prevents stacking bugs (modal behind a sticky header, toast behind a backdrop). It is deliberately aligned to coexist with Bootstrap's 1000–1090 range — use these instead of inventing z-index values.
| Token | Value | Layer |
|---|---|---|
--caat-z-base | 0 | Default flow |
--caat-z-sticky | 1020 | Sticky header (= Bootstrap) |
--caat-z-drawer | 1045 | Off-canvas / drawer |
--caat-z-backdrop | 1050 | Overlay backdrop |
--caat-z-modal | 1055 | Dialog |
--caat-z-popover | 1070 | Popover / tooltip |
--caat-z-toast | 1090 | Toast / snackbar |
--caat-z-skiplink | 1100 | Skip link — always on top |
Focus Ring
| Token | Value | Usage |
|---|---|---|
--caat-focus |
0 0 0 .1875rem var(--caat-blue-500) | Applied via box-shadow on :focus-visible for buttons, links, inputs |
--caat-focus-inset |
inset 0 0 0 .1875rem var(--caat-blue-500) | Inset variant for items inside overflow-clipped rounded containers (dropdown items, list-group rows) |
Focus Preview
The focus ring is a solid blue-500 ring — it meets WCAG 1.4.11 (3:1) on both light (3.31:1) and dark navy (3.82:1) surfaces. (An earlier semi-transparent version failed at 1.47:1 and was replaced.) Consume it through the semantic alias and apply as:
.my-component:focus-visible {
outline: 0;
box-shadow: var(--caat-color-focus-ring);
}
Bootstrap Overrides
The token file maps CAAT values onto Bootstrap's CSS custom properties so Bootstrap utilities and components inherit brand styling automatically.
| Bootstrap token | CAAT mapping | Value |
|---|---|---|
--bs-body-font-family | var(--caat-font-primary) | Libre Franklin stack |
--bs-body-color | var(--caat-ink) | #102637 |
--bs-primary | var(--caat-blue-900) | #003750 |
--bs-primary-rgb | — | 0, 55, 80 |
--bs-secondary | var(--caat-green) | #55a546 |
--bs-secondary-rgb | — | 85, 165, 70 |
--bs-link-color | var(--caat-blue-900) | #003750 |
--bs-link-hover-color | var(--caat-blue-700) | #0b5a80 |
--bs-border-color | var(--caat-color-border) | #dfe6ef — .border utilities |
--bs-border-radius / -sm | var(--caat-radius-sm) | .375rem — .rounded/.rounded-1/2 |
--bs-border-radius-lg | var(--caat-radius) | .75rem — .rounded-3 |
--bs-border-radius-xl | var(--caat-radius-lg) | 1.25rem — .rounded-4 |
Usage Guidelines
Do
- Reference tokens in all component CSS:
color: var(--caat-blue-900); - Use semantic tokens for feedback:
--caat-dangerfor errors - Use
--caat-radiusand--caat-radius-lgfor all rounded corners - Apply
--caat-focuson:focus-visiblefor all interactive elements - Use
--caat-shadowfor card and panel elevation - Use
--caat-font-primaryfor all text — never import a different typeface
Don't
- Hard-code hex values:
color: #003750;✗ - Invent new colours outside the token set
- Use pixel values for border-radius — use the token
- Override Bootstrap variables without going through tokens.css
- Mix
--caat-green(brand) with--caat-success(semantic) — they serve different purposes - Apply box-shadow values directly — use
--caat-shadowor--caat-shadow-sm
Source File
All tokens are defined in a single file that every component and template imports:
/assets/css/tokens.css
The file is organized in three tiers — Primitives (raw values), Semantic (intent aliases that components consume), and Bootstrap overrides. Rather than duplicate it here (which drifts out of date), view the canonical source directly: