Tokens & Styling
WAGE uses CSS custom properties (design tokens) defined as PHP arrays. The Wage\Assets class merges core + child arrays and outputs them as an inline <style id="wage-tokens"> block in the <head>.
Token layers
Section titled “Token layers”1. Core defaults (inc/core/tokens.php)
Section titled “1. Core defaults (inc/core/tokens.php)”The core token file is split into two sections:
Raw tokens — the palette, scales, and primitive values:
$raw = [ // Color scales — 5 steps per scale (OKLCH) '--base-ultra-light' => 'oklch(0.99 0.005 250)', '--base-light' => 'oklch(0.97 0.008 250)', '--base' => 'oklch(0.50 0.01 250)', '--base-dark' => 'oklch(0.30 0.01 250)', '--base-ultra-dark' => 'oklch(0.15 0.01 250)',
'--primary-ultra-light'=> 'oklch(0.93 0.02 250)', '--primary-light' => 'oklch(0.80 0.04 250)', '--primary' => 'oklch(0.35 0.04 250)', '--primary-dark' => 'oklch(0.25 0.04 250)', '--primary-ultra-dark' => 'oklch(0.15 0.04 250)', '--primary-hover' => 'oklch(0.40 0.04 250)',
// Buttons & inputs '--btn-padding-top' => '0.625rem', '--btn-padding-bottom' => '0.625rem', '--btn-padding-inline' => '1.5rem', '--btn-border' => '1px solid transparent', '--input-padding-top' => '0.625rem', '--input-padding-bottom'=> '0.625rem', '--input-padding-inline'=> '0.875rem', '--input-border' => '1px solid var(--neutral-light)',
// Typography, spacing, radii, shadows, transitions... '--heading-font-family' => "system-ui, -apple-system, sans-serif", '--space-m' => '2rem', '--radius-m' => '1rem',];Semantic defaults — map purpose to raw tokens using light-dark() for automatic dark/light resolution:
$semantic = [ // Page (light-dark adaptive) '--body-background-color' => 'light-dark(var(--base-light), var(--base-dark))', '--text-color' => 'light-dark(var(--base-dark), var(--base-light))', '--text-color-muted' => 'light-dark(var(--neutral), color-mix(in oklch, var(--base-light) 55%, transparent))', '--heading-color' => 'light-dark(var(--base-dark), var(--white))', '--link-color' => 'light-dark(var(--primary), var(--accent-light))', '--link-color-hover' => 'light-dark(var(--primary-dark), var(--white))', '--border-color' => 'light-dark(var(--neutral-light), color-mix(in oklch, var(--white) 10%, transparent))', '--card-bg' => 'light-dark(var(--white), color-mix(in oklch, var(--white) 5%, transparent))',
// Components '--modal-bg' => 'light-dark(var(--base-light), var(--primary-ultra-dark))',];Fluid scales — font sizes and spacing are generated by Wage\FluidGenerator and merged into the token array:
$fluid_type = require __DIR__ . '/fluid-type.php'; // or child override$fluid_spacing = require __DIR__ . '/fluid-spacing.php'; // or child override
return array_merge( $raw, $fluid_type, $fluid_spacing, $semantic );For example, --heading-color defaults to light-dark(var(--base-dark), var(--white)). On a light surface it resolves to --base-dark. On a dark surface (with color-scheme: dark) it resolves to --white. No manual overrides needed.
2. Child overrides (inc/tokens.php in child theme)
Section titled “2. Child overrides (inc/tokens.php in child theme)”The child theme only overrides what differs from core. Wage\Assets::get_tokens() merges with array_merge() — child values win:
return [ // Primary (forest green, H:163) '--primary' => 'oklch(0.35 0.055 163)', '--primary-light' => 'oklch(0.48 0.078 163)', '--primary-dark' => 'oklch(0.25 0.045 163)',
// Accent (gold, H:86) '--accent' => 'oklch(0.75 0.098 86)', '--accent-light' => 'oklch(0.90 0.035 86)',
// Typography '--heading-font-family' => "'DM Serif Display', Georgia, serif", '--text-font-family' => "'Inter', system-ui, sans-serif",
// Semantic overrides '--heading-color' => 'var(--primary)',];3. How tokens are output
Section titled “3. How tokens are output”Wage\Assets::init() registers a wp_head hook at priority 0. It calls Wage\Assets::output_tokens(), which iterates over the merged token array and outputs them as :root custom properties:
<style id="wage-tokens">:root{--base-dark:oklch(0.30 0.01 250);--primary:oklch(0.35 0.055 163);...}</style>4. Accessing tokens in PHP
Section titled “4. Accessing tokens in PHP”Use Wage\Assets to read token values at runtime:
// Get a single token value$primary = Wage\Assets::token( 'primary' ); // returns 'oklch(0.35 0.055 163)'
// Get all merged tokens$all = Wage\Assets::get_tokens(); // returns full array5. Visual styling (assets/css/site.css in child theme)
Section titled “5. Visual styling (assets/css/site.css in child theme)”Project-specific CSS that is not related to framework components — page layouts, custom sections, brand-specific visual treatments. This file should not contain framework component styling (that lives with each component as a paired .css file).
Color system
Section titled “Color system”All token color values must be OKLCH. Never use hex, rgb, or hsl in tokens.php. OKLCH provides perceptually uniform shading — adjusting lightness gives predictable, visually consistent results across the entire palette. Format: oklch(L C H) where L = lightness (0-1), C = chroma (saturation intensity), H = hue (degrees, 0-360).
Each color scale has 5 steps ordered lightest to darkest, plus a hover variant. All share a fixed hue (H) and chroma (C), with lightness (L) stepped:
| Token | Lightness | Use |
|---|---|---|
--{scale}-ultra-light | ~93-99% | Tinted backgrounds |
--{scale}-light | ~80% | Subtle accents, borders |
--{scale} | ~50% | Base/default usage (mid-tone) |
--{scale}-dark | ~25-30% | Darker variants |
--{scale}-ultra-dark | ~15% | Dark section backgrounds |
--{scale}-hover | base+5% | Interactive hover states |
There is no “semi-light” step. Every scale uses exactly these 5 steps plus hover.
Scales: base, primary, accent, neutral. Status scales: success, danger, warning, info.
Brand color placement
Section titled “Brand color placement”Place the brand’s primary color at whichever step matches its natural lightness, then build the rest of the scale with even spacing around it:
- Dark brand color (L:0.30) → set as
--primary-dark, build lighter steps above - Mid brand color (L:0.55) → set as
--primary, build steps in both directions - Light brand color (L:0.80) → set as
--primary-light, build darker steps below
Use semantic tokens (--heading-color: var(--primary-dark)) to map purpose to the appropriate step.
Key tokens
Section titled “Key tokens”| Token | Purpose |
|---|---|
--primary-ultra-light through --primary-ultra-dark, --primary-hover | Primary brand colour scale |
--accent-ultra-light through --accent-ultra-dark, --accent-hover | Accent colour scale |
--base-ultra-light through --base-ultra-dark | Base/surface colour scale |
--neutral-ultra-light through --neutral-ultra-dark | Neutral palette |
--heading-color | Heading text — light-dark() adaptive |
--text-color | Body text — light-dark() adaptive |
--text-color-muted | Muted/secondary text — light-dark() adaptive |
--body-background-color | Page background — light-dark() adaptive |
--link-color | Link colour — light-dark() adaptive |
--link-color-hover | Link hover colour — light-dark() adaptive |
--border-color | Border colour — light-dark() adaptive |
--card-bg | Card background — light-dark() adaptive |
--font-size-xs through --font-size-xxl | Text font size scale |
--font-size-h1 through --font-size-h5 | Heading font size scale |
--heading-font-family | Heading typeface |
--text-font-family | Body typeface |
--space-xs through --space-xxl | Spacing scale |
--section-padding-block | Section vertical padding (fluid: 55px → 96px) |
--section-padding-block-sm | Compact section padding (fluid: 55px → 72px, same mobile) |
--section-space-m, --section-space-l | Section internal spacing |
--content-width | Container max-width (default: 1350px) |
--content-width-narrow | Narrow container max-width (default: 800px) |
--gutter | Page horizontal padding (default: 1.5rem) |
--grid-gap | Default grid gap |
--container-gap | Gap between major blocks within a section |
--radius-xs through --radius-full | Border radius scale |
--shadow-sm through --shadow-xl | Shadow scale |
--transition-fast, --transition-base, --transition-slow | Transition durations |
--heading-font-weight | Heading font weight (default: 600) |
--heading-line-height | Heading line height (default: 1.2) — shared by all headings |
--line-height-h1 through --line-height-h4 | Per-heading line-height overrides (opt-in, see below) |
--text-line-height | Body text line height (default: 1.65) |
--btn-padding-top, --btn-padding-bottom, --btn-padding-inline | Button padding |
--btn-lg-padding-top, --btn-lg-padding-bottom, --btn-lg-padding-inline | Large button padding |
--btn-border | Button border (default: 1px solid transparent) |
--input-padding-top, --input-padding-bottom, --input-padding-inline | Input padding |
--input-lg-padding-top, --input-lg-padding-bottom, --input-lg-padding-inline | Large input padding |
--input-border | Input border (default: 1px solid var(--neutral-light)) |
--placeholder-color | Input placeholder text colour |
--modal-bg | Modal background — light-dark() adaptive |
Button & input height alignment
Section titled “Button & input height alignment”Buttons and inputs share matching vertical padding and border tokens so they are the same height by default:
'--btn-padding-top' => '0.625rem','--btn-padding-bottom' => '0.625rem','--btn-padding-inline' => '1.5rem','--btn-border' => '1px solid transparent',
'--input-padding-top' => '0.625rem','--input-padding-bottom' => '0.625rem','--input-padding-inline' => '0.875rem','--input-border' => '1px solid var(--neutral-light)',Both use the same padding-top + padding-bottom + border values, so a button next to an input in a row will align perfectly. If the project’s heading or button font has uneven ascenders/descenders, override --btn-padding-top and --btn-padding-bottom independently to compensate.
Heading line-height
Section titled “Heading line-height”All headings share --heading-line-height (default: 1.2). Each heading level can optionally override this with a per-level token using CSS fallbacks:
h1 { line-height: var(--line-height-h1, calc(var(--heading-line-height) * 0.9)); }h2 { line-height: var(--line-height-h2, var(--heading-line-height)); }h3 { line-height: var(--line-height-h3, var(--heading-line-height)); }h4 { line-height: var(--line-height-h4, var(--heading-line-height)); }H1 is proportionally tighter by default — it uses calc(var(--heading-line-height) * 0.9) because larger text needs tighter line-height to look balanced. At the default 1.2, h1 gets 1.08. If you bump --heading-line-height to 1.3, h1 moves to 1.17 — everything stays proportional.
Per-level overrides are opt-in. If a child theme sets --line-height-h1: 1.15 in its tokens.php, it takes precedence over the calc. If it doesn’t set one, the fallback handles it. No wasted tokens for headings that don’t need overrides.
Token naming convention
Section titled “Token naming convention”The token system uses two naming patterns for different purposes:
Group defaults use --{category}-{property} — these are shared by all members of a category:
--heading-font-family,--heading-font-weight,--heading-line-height,--heading-color--text-font-family,--text-line-height
Scale tokens use --{property}-{variant} — these are a single property with size/level variants:
--font-size-h1,--font-size-m,--font-size-xl--line-height-h1,--line-height-h2--space-xl,--shadow-md,--radius-l
The distinction: --heading-line-height is “the line-height for headings as a group.” --line-height-h1 is “the h1 step on the line-height scale.” Both patterns coexist because they represent different concepts.
Token audit
Section titled “Token audit”The Wage Dashboard (accessible via the admin bar) includes a Token Audit tool under the Tools section. It compares core tokens against the child theme and shows:
- New in core — tokens added to core that the child hasn’t overridden yet (using core defaults)
- Overridden — tokens the child has customised, with side-by-side preview
- Orphaned — tokens in the child that no longer exist in core (safe to remove)
Fluid Type & Spacing
Section titled “Fluid Type & Spacing”Font sizes and spacing use clamp() values generated by Wage\FluidGenerator. This produces fluid values that scale smoothly between a mobile and desktop viewport.
FluidGenerator class
Section titled “FluidGenerator class”// Single clamp value — e.g. for a one-off overrideWage\FluidGenerator::generate_clamp( 44, 76 );// → 'clamp(2.75rem, 2.0227rem + 3.2323vw, 4.75rem)'
// Full scale — returns array of named tokensWage\FluidGenerator::generate_scale( min_base: 15, // base size at mobile (px) max_base: 16, // base size at desktop (px) ratio: 1.125, // scale ratio names_down: [ '--font-size-s', '--font-size-xs' ], base_name: '--font-size-m', names_up: [ '--font-size-l', '--font-size-xl', '--font-size-xxl' ],);Viewport defaults
Section titled “Viewport defaults”The class uses static properties for viewport range. All calls use these unless overridden per-call:
Wage\FluidGenerator::$min_vw = 360; // defaultWage\FluidGenerator::$max_vw = 1350; // default
// Override in child theme functions.php:Wage\FluidGenerator::$max_vw = 1450;Config files
Section titled “Config files”| File | Purpose | Override |
|---|---|---|
inc/core/fluid-type.php | Body + heading type scales | inc/fluid-type.php in child theme |
inc/core/fluid-spacing.php | Spacing scale + pairs | inc/fluid-spacing.php in child theme |
Both return arrays that are merged into the token output by tokens.php.
Step mapping
Section titled “Step mapping”Body scale — step-0 = --font-size-m (always):
| Step | Token |
|---|---|
| step -2 | --font-size-xs |
| step -1 | --font-size-s |
| step 0 | --font-size-m |
| step 1 | --font-size-l |
| step 2 | --font-size-xl |
| step 3 | --font-size-xxl |
Heading scale — separate, larger base calibrated for the heading typeface:
| Step | Token | Use |
|---|---|---|
| step -2 | --font-size-h5 | Smallest headings |
| step -1 | --font-size-h4 | Small headings |
| step 0 | --font-size-h3 | Card titles, subsections |
| breakout | --font-size-h2 | Section titles (SectionIntro) |
| breakout | --font-size-h1 | Hero display titles |
H3 is the scale base. H4 and H5 are generated steps below it. H2 and H1 break out of the scale with specific clamp() values because the gap between card headings and section titles is too large for a single ratio to handle.
Two scales, two purposes
Section titled “Two scales, two purposes”- Body scale (
--font-size-xsthrough--font-size-xxl) — for body text, labels, buttons, ledes, nav links - Heading scale (
--font-size-h5through--font-size-h1) — for headings and elements styled as headings (card titles, step titles, etc.)
Always use the heading scale for heading elements, even if the body scale has a similar pixel value. The heading scale is calibrated for the heading typeface, which may have a different x-height than the body font.
Breaking out of the scale
Section titled “Breaking out of the scale”Individual tokens can override the generated scale:
return array_merge( $body, $headings, [ // H1 breaks out — 44px mobile, 76px desktop '--font-size-h1' => \Wage\FluidGenerator::generate_clamp( 44, 76 ),] );- Never use raw colour values in CSS. Always use tokens. If a variable doesn’t exist, add a new token. For transparent variants, use
color-mix(see below). - Always use spacing tokens (
--space-xsthrough--space-xxl). Never use arbitrary values like1.5rem. - Font sizes must use tokens (
--font-size-xsthrough--font-size-h1). Never hardcode font sizes. - Heading colour comes from
--heading-color, set incore.css. Override via the token system, not CSS rules. - Margin-top only. Only add
margin-topto elements. Never usemargin-bottom. - Use
gapover margins in flex/grid containers. - Don’t set inherited values. Body sets
font-family,font-size,line-height, andcolor. Don’t redeclare them on child elements unless overriding. - Desktop-first breakpoints. CSS defaults are the desktop layout. Use
max-widthbreakpoints to step down to smaller screens. Don’t usemin-width.
Standard breakpoints:
| Query | Target |
|---|---|
@media (max-width: 1023px) | Tablet and below |
@media (max-width: 767px) | Mobile |
@media (max-width: 639px) | Small mobile |
SectionIntro lede_max removed
Section titled “SectionIntro lede_max removed”The lede_max prop has been removed from SectionIntro. Lede max-width is now handled in CSS (max-width: 50ch on .section-intro__lede). Do not pass lede_max to SectionIntro — it will throw an error.
Transparency
Section titled “Transparency”Never create separate transparency tokens or use hardcoded rgba() values. Use color-mix to apply opacity to any token inline:
/* Primary at 20% opacity — e.g. subtle borders */border: 1px solid color-mix(in oklch, var(--primary) 20%, transparent);
/* Accent at 50% opacity — e.g. hover backgrounds */background: color-mix(in oklch, var(--accent) 50%, transparent);This keeps colours tied to their token. If --primary changes, every transparent variant updates automatically. No hardcoded rgba values to find and replace.