For developers

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. */
t-d1

Poppins 700 · 40px/48px

Display / H1

t-h2

Poppins 600 · 30px/38px

Heading / H2

t-h3

Poppins 600 · 22px/30px

Subheading / H3

t-body

Montserrat 400 · 16px/26px

Body text

t-small

Montserrat 400 · 14px/22px

Small / caption

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 px
  • Stroke 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.