Standards meet this stack.
Brand and UI standards are implementation-neutral. This page is where they bind to the real token names, packages, and conventions in this codebase.
The rule: consume tokens
Use var(--token-name) everywhere. Hardcoded hex values, raw font strings, and arbitrary numeric sizes bypass the token system, break dark mode, and fragment the design — they’re the most common source of visual debt in this codebase. If a value you need isn’t in a token, ask before inventing one.
Colour in code
All colour is set via CSS custom properties injected from src/tokens/colour.ts. Consume via var() — never hardcode hex.
/* ✓ Always consume tokens via var() */
.my-heading { color: var(--heading); }
.my-link { color: var(--action-blue); }
.my-surface {
background: var(--surface-muted);
border: 1px solid var(--border);
}
/* ✗ Never hardcode hex — bypasses token system and breaks dark mode */
.bad { color: #0D9ADD; }Brand
--cbc-blue
Display only — large headings ≥24px, decorative fills, accent lines. Fails AA at small/interactive sizes (~3.1:1 on white). Never interactive.
--action-blue
All interactive use — links, buttons, active states, focus ring, small marks. Passes AA at any size (~5.6:1 on white).
--rhino
Headings, chrome, dark fills.
Surfaces & borders
--surface
Page background.
--surface-muted
Recessed panels, code blocks, input fields.
--surface-inverse
Dark band. Matches --rhino in light mode; overridden in dark.
--border
Dividers, card edges, input borders.
Text
--text-primary
Body copy.
--text-secondary
Supporting text, labels, captions.
--heading
All headings. Adapts in dark mode so --rhino swatch stays fixed.
--text-on-dark
Text on --surface-inverse.
Semantic states
--success
Success fill (decorative).
--warning
Warning fill (decorative).
--error
Error fill and small error text.
--success-text
AA-safe green for small text on light surfaces (~5.1:1).
--warning-text
AA-safe amber for small text on light surfaces.
Radius tokens: --radius-sm (4px) · --radius-md (8px) · --radius-lg (14px)
Display vs interactive: CBC Blue (#0D9ADD) fails WCAG AA at small sizes (~3.1:1 on white) — it is display-only. Action Blue (#0A6E9E, ~5.6:1) carries all interactive use. Using the wrong one at the wrong size is a contrast violation.
/* --cbc-blue (#0D9ADD, ~3.1:1 on white) — DISPLAY ONLY */
.accent-line { background: var(--cbc-blue); } /* ✓ decorative fill */
h1.hero-title { color: var(--cbc-blue); } /* ✓ heading ≥24px */
/* --action-blue (#0A6E9E, ~5.6:1 on white) — ALL INTERACTIVE USE */
a { color: var(--action-blue); } /* ✓ link */
.btn-primary { background: var(--action-blue); } /* ✓ button */
:focus-visible { outline-color: var(--action-blue); } /* ✓ focus ring */
/* ✗ Never use --cbc-blue for interactive or small UI elements */
a.bad { color: var(--cbc-blue); } /* fails AA at 14px */
.badge { color: var(--cbc-blue); } /* fails AA at 12px */Dark mode
Dark overrides apply via [data-theme="dark"] on <html>, set before paint. Dark mode is a demonstration that the token system can theme — not a default visitor feature. Components that consume var() tokens throughout need zero extra dark-mode rules. prefers-color-scheme is honoured as the default.
/* Dark colour overrides apply automatically via [data-theme="dark"] on <html>.
Components that consume var() tokens exclusively inherit both themes
with zero extra CSS — no per-component dark mode rules needed.
Only write [data-theme="dark"] rules for values that don't have a token
equivalent, typically decorative opacities or box-shadows. */
/* Example: a decorative fill whose opacity differs by theme */
.blade::before {
background: var(--cbc-blue);
opacity: 0.07; /* light mode */
}
[data-theme="dark"] .blade::before {
opacity: 0.18; /* dark surface; stronger fill needed */
}
/* prefers-color-scheme is the DEFAULT — honoured before any user toggle */Subsidiary accent
The subsidiary accent is expressed as two paired custom properties: --subsidiary-accent (display — large and decorative use) and --subsidiary-accent-action (interactive — AA-safe at any size). Both default to the CBC pair at :root, so an undifferentiated subsidiary reads as CBC emea.
The pair is mandatory because a display accent may fail AA at text size — the interactive accent carries every small or interactive use to guarantee contrast. The accent is scoped to the subsidiary’s own wrapper; base chrome (nav, footer, system states) stays immutable.
/* :root defaults — undifferentiated subsidiaries read as CBC emea.
This is intentional: a subsidiary with no accent yet still works. */
:root {
--subsidiary-accent: var(--cbc-blue); /* display: large/decorative */
--subsidiary-accent-action: var(--action-blue); /* interactive: AA at any size */
}
/* Subsidiary override — scoped to the subsidiary's OWN page wrapper.
NEVER set on :root or on any element that wraps CBC chrome (nav, footer). */
[data-subsidiary="energy"] {
--subsidiary-accent: #______; /* display accent — verify: large use only */
--subsidiary-accent-action: #______; /* interactive — verify: AA ≥4.5:1 on white */
}
/* Usage within the subsidiary surface */
.subsidiary-hero-rule { background: var(--subsidiary-accent); }
.subsidiary-link { color: var(--subsidiary-accent-action); }Co-brand lockups
Co-branded lockups use the multiplication sign × (U+00D7) with a space on each side. Canonical form: “CBC Surveillance × Holowits”— the subsidiary that owns the partnership flanks the partner. Both names in the display face; × de-emphasised with var(--text-secondary). Partner colours are scoped to partner surfaces and never tokenised.
/* Co-brand title — multiplication sign × (U+00D7), one space each side.
Canonical form: "CBC Surveillance × Holowits"
✗ "CBC × Holowits" — wrong: elevates a subsidiary partnership to group level
✗ "CBC Surveillance x Holowits" — wrong: letter x, not multiplication sign
✗ "CBC Surveillance * Holowits" — wrong: asterisk */
/* Partner colour — scoped to the partner surface only; never tokenised */
[data-partner="holowits"] {
--partner-colour: #D72638; /* Holowits red — valid on this surface only */
}
/* Non-text lockup: accessible name in "[Subsidiary] in partnership with [Partner]" form */
<section
aria-label="CBC Surveillance in partnership with Holowits"
style={{ borderTop: '4px solid var(--partner-colour)' }}
>
<h1>
CBC Surveillance{' '}
<span style={{ color: 'var(--text-secondary)' }}>×</span>
{' '}Holowits
</h1>
</section>Typography
Three font variables. Five type-scale classes. One sizing rule. Use the classes instead of setting font-family / font-size manually.
/* Font family variables — defined in globals.css, available everywhere */
var(--font-display) /* Poppins — headings, labels, buttons, UI chrome */
var(--font-body) /* Montserrat — all body copy, prose, captions */
var(--mono) /* system monospace — code, token names, data labels */
/* ✗ Never use Poppins for body copy or Montserrat for primary headings */
/* Type-scale utility classes (generated from src/tokens/type.ts) */
className="t-d1" /* Poppins 700 · 40px/48px — Display / H1 */
className="t-h2" /* Poppins 600 · 30px/38px — Heading / H2 */
className="t-h3" /* Poppins 600 · 22px/30px — Subheading / H3 */
className="t-body" /* Montserrat 400 · 16px/26px */
className="t-small" /* Montserrat 400 · 14px/22px */
/* Rem rule: ALL text is sized in rem or em. NEVER set font-size on <html>
in px — it overrides the user's browser preference and breaks zoom.
WCAG 1.4.4 requires text to resize to 200% without loss of function. */Icons
Lucide is the single icon set. Use it exclusively; bespoke icons are reserved for genuine brand gaps that Lucide cannot cover — not for stylistic preference. See Icons for the curated 28 and the rationale.
# Install
npm i lucide-react
# Import by name — tree-shakeable; only what you import is bundled
import { Search, ChevronRight, X } from 'lucide-react'
# Default: 24px grid, 2px stroke, stroke="currentColor"
# Colour is inherited from the parent — set via CSS token.
# Decorative icon — screen reader skips; parent provides the label
<Search size={24} aria-hidden="true" />
# Meaningful icon inside a labelled button — icon itself stays aria-hidden
<button aria-label="Search">
<Search size={24} aria-hidden="true" />
</button>
# Interactive icon colour — always --action-blue, never --cbc-blue
<ChevronRight
size={20}
style={{ color: 'var(--action-blue)' }}
aria-hidden="true"
/>
# Size scale: 16 / 20 / 24 / 32 pxStroke colour
stroke="currentColor" — colour is inherited from the parent element via CSS. Set colour on the parent, not the SVG directly.
Interactive icons
Always --action-blue, never --cbc-blue (fails AA at 24px without bold weight).
Decorative icons
aria-hidden="true" — screen reader skips the element entirely.
Meaningful icons
Wrap in a labelled interactive element. The icon itself gets aria-hidden; the label is on the button or link.
Icon-only controls
Never ship without a visible or screen-reader label (aria-label on the control). No exceptions for space constraints.
Share images — the /og/ convention
Every shareable page on a public property has a 1200 × 630 share image derived from the page’s own title and metadata — so it cannot drift or ship as a placeholder. Images live under a shared /og/ namespace with a brand default fallback. The share image is a brand surface, not a screenshot.
# Share image paths — /og/ namespace (all public properties)
/og/[subsidiary].png # subsidiary microsite
/og/[subsidiary]-[partner].png # partner landing page
/og/default.png # brand default fallback
# Dimensions
1200 × 630 px total canvas
1080 × 567 px safe area, centred
# Rules
- Content derived from the page's own title + metadata (cannot drift)
- In-image text: ≥ 24 px, WCAG AA contrast
- Visual treatment: follows Imagery page rules (brand surface, not a screenshot)
- Path corresponds to the page's canonical URL
- Defers to the Digital Architecture Standards for cross-property conventions
# Note: this reference site is intentionally noindex.
# Apply the /og/ convention on public-facing properties.This reference site is intentionally noindex. The /og/ convention applies to public-facing CBC properties. For visual treatment rules, see Imagery. For the canonical and OG meta block, see SEO & meta.
UI strings
Nav labels and recurring CTA strings are centralised in src/lib/ui-strings.ts. Body prose, section headings, and one-off copy stay inline in the component where they are authored. The boundary is: strings used in more than one place go in the file; strings used in only one place stay inline.
// src/lib/ui-strings.ts — nav labels + CTA strings ONLY.
// Prose, section headings, and one-off copy stay inline in the page component.
import { NAV_LABELS, CTA, UI } from '@/lib/ui-strings'
// ✓ Nav labels and CTA strings — always from the token file
<Link href="/colour">{NAV_LABELS.colour}</Link>
<button>{CTA.copyForAI}</button>
<nav aria-label={UI.referenceNavigation}>...</nav>
// ✗ Never inline nav labels or recurring CTA strings
<Link href="/colour">Colour</Link>
// renaming "Colour" → "Colours" requires finding every callsite
// ✗ Never centralise body prose or section-specific headings
// "CBC emea's colour system is built on..." stays in ColourSection.tsx
// Moving prose here over-engineers the site and hurts readability at source.Semantic HTML & a11y baseline
WCAG 2.1 AA is the floor. These rules are the implementation baseline — the minimum required on every page and component before it ships.
One h1 per page
Every page has exactly one <h1>. Use <h2> and <h3> for sub-sections. Never use a styled <div> as a heading — it is invisible to screen readers.
Real heading levels
Levels do not skip: h2 follows h1, h3 follows h2. The visual size and the semantic level must match.
Global focus ring
2px Action Blue outline, 3px offset, applied via :focus-visible in globals.css. Never write outline: none without a visible replacement. Never suppress it.
prefers-reduced-motion
Wrap transitions and animations in @media (prefers-reduced-motion: no-preference). Drop to instant or sub-100ms under reduce. Never auto-play animation without the pref check.
aria-live for copy feedback
Clipboard "Copied" confirmations need aria-live="polite" so screen readers announce the state change without interrupting the user. Use a visually-hidden role="status" region adjacent to the trigger.
No click on non-interactive elements
Never attach onClick to a <div>, <span>, or <p>. If it responds to clicks, use <button> or <a>. Every interaction has a keyboard equivalent.
Design vs standard
When a design decision conflicts with WCAG 2.1 AA, the design moves — not the standard. Escalate to the Marketing Team; never ship an exception.
For the full accessibility standard, see Accessibility. For contrast verification, use the contrast checker on the Colour page.
Starters
Building a subsidiary microsite or partner landing page? Start from the subsidiary microsite kit or the partner landing page kit on the Starters page — copyable scaffolds with the right tokens and meta wired in, and a “change these N things” checklist to guard against placeholder canonicals, missing OG images, and colour-scope leaks.