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:

TierRoleExampleWho uses it
1 · PrimitiveRaw values. The only place literal colours / sizes live.--caat-blue-900: #003750Semantic tier (rarely components directly)
2 · SemanticIntent-based aliases. Components should consume these.--caat-color-action-primary: var(--caat-blue-900)Components & templates
3 · BootstrapMaps 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.

CategoryPrefix
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

Blue 900 --caat-blue-900 #003750
Blue 700 --caat-blue-700 #0b5a80
Blue --caat-blue #0f6791
Blue 500 --caat-blue-500 #2f95d2
Blue 300 --caat-blue-300 #9dd2ed
Blue 100 --caat-blue-100 #e7f4fb

Greens

Green 900 --caat-green-900 #204b1f
Green 700 --caat-green-700 #367f32
Green --caat-green #55a546
Green 100 --caat-green-100 #e8f5e6
Lime --caat-lime #95e35c
Teal --caat-teal #12a7b8

Neutrals

Ink --caat-ink #102637
Muted --caat-muted #506273
Grey 300 --caat-grey-300 #cbd7e3
Grey 200 --caat-grey-200 #dfe6ef
Grey --caat-grey #eff2f7
Beige --caat-beige #f8f5ee

Semantic / Feedback

Success --caat-success #2f7d32
Warning --caat-warning #efaa39
Danger --caat-danger #b83232

Colour Usage Reference

ContextTokenExample
Primary text / headings--caat-blue-900Page titles, hero headings
Body text--caat-inkParagraphs, list items
Secondary text--caat-mutedSubtitles, captions, meta
Primary buttons / links--caat-blue-900CTAs, navigation links
Secondary actions--caat-greenSecondary buttons, accents
Eyebrow labels--caat-color-eyebrow--caat-blue-700Category labels above headings
Eyebrow on dark--caat-color-eyebrow-on-dark--caat-blue-300Labels on dark backgrounds (default)
Eyebrow on dark — hero--caat-color-eyebrow-on-dark-hero--caat-limeHero eyebrows only — deliberate brand accent
Section backgrounds--caat-greyAlternating section bands
Dark section backgrounds--caat-blue-900Stat strips, dark heroes, footers
Card / input borders--caat-grey-200Card outlines, dividers
Success / positive--caat-successAlert success, form validation
Warning--caat-warningAlert warning
Error / danger--caat-dangerAlert 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
Status: all 34 required pairings pass AA. Three issues were found and fixed at the token layer (below).

Fixes applied

IssueWasNow
Focus ring invisiblergba(47,149,210,.35) — 1.47:1 on whiteSolid --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 faintgrey-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:

ColourRatio on whiteRule
--caat-warning (amber)2.0:1Dark text only, never white. Don't signal by colour alone — pair with an icon/label (1.4.1).
--caat-teal2.9:1 (white on teal)Decorative accent — don't place white (or any 4.5:1 text) on it.
--caat-lime1.56:1Use on dark or as a fill behind dark text (e.g. selection, hero eyebrow) — never as text/UI on white.
--caat-blue-3001.63:1Tint / on-dark text only — not a UI boundary on white.
--caat-grey-200 / -3001.26 / 1.46:1Decorative dividers only (1.4.11 exempt). For control borders use --caat-color-border-interactive.
--caat-disabled2.92:1Disabled 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

TokenAliasesUsage
--caat-color-text--caat-inkDefault body text
--caat-color-text-muted--caat-mutedSecondary / supporting text
--caat-color-text-inverse#fffText on dark / brand backgrounds
--caat-color-text-inverse-muted--caat-white-88Body copy on dark / brand surfaces
--caat-color-text-inverse-dim--caat-white-70Meta / secondary text on dark surfaces
--caat-color-text-link--caat-blue-900Links (resting)
--caat-color-text-link-hover--caat-blue-700Links (hover)
--caat-color-text-disabled--caat-disabledDisabled text

Surfaces & borders

TokenAliasesUsage
--caat-color-bg-page#fffPage background
--caat-color-bg-subtle--caat-greyTinted section band
--caat-color-bg-brand--caat-blue-900Dark brand band
--caat-color-bg-brand-subtle--caat-blue-100Light brand tint
--caat-color-bg-overlayrgba(0,0,0,.25)Modal / drawer scrim
--caat-color-bg-hero-panel--caat-blue-glassHero overlay panel (glass over imagery)
--caat-color-bg-hero-panel-hover--caat-blue-glass-hoverHero overlay panel — hover
--caat-overlay-side--caat-scrim-soft → -faintHero media gradient scrim (text-side legibility)
--caat-overlay-bottom--caat-scrim-strong → -midOverlay-card gradient scrim (bottom legibility)
--caat-color-border--caat-grey-200Default borders / dividers
--caat-color-border-strong--caat-grey-300Emphasised borders
--caat-color-border-inverse--caat-white-30Dividers on dark / brand surfaces

Actions & focus

TokenAliasesUsage
--caat-color-action-primary--caat-blue-900Primary buttons / CTAs
--caat-color-action-primary-hover--caat-blue-700Primary hover
--caat-color-action-secondary--caat-greenSecondary actions
--caat-color-focus-ring--caat-focusFocus ring (box-shadow)
--caat-color-focus-ring-inset--caat-focus-insetInset focus ring (clipped containers)
--caat-color-focus-edge--caat-blue-500Focused 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

TokenValueUsage
--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

TokenValueUsage
--caat-letter-spacing-normal0Body text
--caat-letter-spacing-tight-.015emLarge headings for visual tightening
--caat-letter-spacing-label.06emUppercase labels, eyebrows, badges

Font Weights

Light 300
Regular 400
Medium 500
Semi-bold 600
Bold 700
Extra-bold 800
Black 900

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.

Display Display heading 3.5rem / 900 / -.04em
H1 Heading one 2.5rem / 800 / -.02em
H2 Heading two 2rem / 800 / -.02em
H3 Heading three 1.75rem / 800 / -.02em
H4 Heading four 1.5rem / 800 / -.02em
H5 Heading five 1.25rem / 800 / -.02em
H6 Heading six 1rem / 800 / -.02em
Lead Lead paragraph text for introductions and callouts. 1.25rem / 400 / 1.6
Body Body text for paragraphs, lists, and general content. 1rem / 400 / 1.6
Small Small text for captions, footnotes, and meta. .875rem / 400 / 1.5
Eyebrow SECTION LABEL .75rem / 700 / .08em / uppercase

Title & Text CSS Classes

The Text / Title component provides ready-made utility classes for typography.

ClassUsageSizeWeight
.caat-titleBase title — apply with heading level modifier800
.caat-title--displayHero-level display heading3.5rem900
.caat-title--h1Page title2.5rem800
.caat-title--h2Section heading2rem800
.caat-title--h3Sub-section heading1.75rem800
.caat-title--h4Card / panel heading1.5rem800
.caat-title--h5Minor heading1.25rem800
.caat-title--h6Smallest heading1rem800
.caat-title--sectionSection heading with bottom border1.75rem800
.caat-title__subtitleSupporting text below a heading1.25rem400
ClassUsageSize
.caat-textBase body text1rem
.caat-text--leadLead / intro paragraph1.25rem
.caat-text--smallSmall / caption text.875rem
.caat-text--mutedDe-emphasized text (60% opacity)inherit
.caat-rich-textCanonical 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

TokenValueRole
--caat-prose-leading1.7Body line-height
--caat-prose-flow1.25remBetween paragraphs / lists
--caat-prose-h2-space-before2.5remBefore an h2 (section)
--caat-prose-h3-space-before2remBefore an h3
--caat-prose-h4-space-before1.5remBefore an h4
--caat-prose-heading-space-after0.5remAfter any heading
--caat-prose-measure68chMax line length

Heading before ≫ paragraph flow ≫ heading after. The block's first child has no top gap and its last child no bottom gap.

Prose rhythm vs. Stack rhythm. Use prose rhythm (.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.

Pension Solutions

A better pension matters

The attraction and retention strategy every employer should be thinking about.

Pension Solutions

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-dark over 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.

TokenValueTypical usage
--caat-space-1.25rem · 4pxIcon gaps, hairline insets
--caat-space-2.5rem · 8pxTight gaps, chip padding
--caat-space-3.75rem · 12pxInput padding, small gaps
--caat-space-41rem · 16pxDefault element spacing
--caat-space-51.5rem · 24pxCard padding, stack gaps
--caat-space-62rem · 32pxGroup separation
--caat-space-73rem · 48pxSub-section spacing
--caat-space-84rem · 64pxSection padding (mobile)
--caat-space-96rem · 96pxSection padding (desktop)

Radius

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

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

TokenValueUsage
--caat-duration-fast120msHover, small state changes
--caat-duration-base200msDefault — most transitions
--caat-duration-slow320msDrawers, modals, expand/collapse
--caat-ease-standardcubic-bezier(.2, 0, 0, 1)Most transitions
--caat-ease-emphasizedcubic-bezier(.3, 0, 0, 1)Enter / expand
--caat-ease-exitcubic-bezier(.4, 0, 1, 1)Leave / collapse
--caat-lift-subtle-1pxHover lift: buttons, small controls
--caat-lift-row-2pxHover lift: list rows, logo tiles
--caat-lift-card-3pxHover lift: cards in grids
--caat-lift-panel-4pxHover lift: hero panels, large previews
Reduced motion is handled globally. 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.

TokenValueLayer
--caat-z-base0Default flow
--caat-z-sticky1020Sticky header (= Bootstrap)
--caat-z-drawer1045Off-canvas / drawer
--caat-z-backdrop1050Overlay backdrop
--caat-z-modal1055Dialog
--caat-z-popover1070Popover / tooltip
--caat-z-toast1090Toast / snackbar
--caat-z-skiplink1100Skip link — always on top

Focus Ring

TokenValueUsage
--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

Default
Resting
Focused
:focus-visible

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 tokenCAAT mappingValue
--bs-body-font-familyvar(--caat-font-primary)Libre Franklin stack
--bs-body-colorvar(--caat-ink)#102637
--bs-primaryvar(--caat-blue-900)#003750
--bs-primary-rgb0, 55, 80
--bs-secondaryvar(--caat-green)#55a546
--bs-secondary-rgb85, 165, 70
--bs-link-colorvar(--caat-blue-900)#003750
--bs-link-hover-colorvar(--caat-blue-700)#0b5a80
--bs-border-colorvar(--caat-color-border)#dfe6ef — .border utilities
--bs-border-radius / -smvar(--caat-radius-sm).375rem — .rounded/.rounded-1/2
--bs-border-radius-lgvar(--caat-radius).75rem — .rounded-3
--bs-border-radius-xlvar(--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-danger for errors
  • Use --caat-radius and --caat-radius-lg for all rounded corners
  • Apply --caat-focus on :focus-visible for all interactive elements
  • Use --caat-shadow for card and panel elevation
  • Use --caat-font-primary for 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-shadow or --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:

Open tokens.css