Skip to content

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

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)',
];

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>

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 array

5. 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).

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:

TokenLightnessUse
--{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}-hoverbase+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.

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.

TokenPurpose
--primary-ultra-light through --primary-ultra-dark, --primary-hoverPrimary brand colour scale
--accent-ultra-light through --accent-ultra-dark, --accent-hoverAccent colour scale
--base-ultra-light through --base-ultra-darkBase/surface colour scale
--neutral-ultra-light through --neutral-ultra-darkNeutral palette
--heading-colorHeading text — light-dark() adaptive
--text-colorBody text — light-dark() adaptive
--text-color-mutedMuted/secondary text — light-dark() adaptive
--body-background-colorPage background — light-dark() adaptive
--link-colorLink colour — light-dark() adaptive
--link-color-hoverLink hover colour — light-dark() adaptive
--border-colorBorder colour — light-dark() adaptive
--card-bgCard background — light-dark() adaptive
--font-size-xs through --font-size-xxlText font size scale
--font-size-h1 through --font-size-h5Heading font size scale
--heading-font-familyHeading typeface
--text-font-familyBody typeface
--space-xs through --space-xxlSpacing scale
--section-padding-blockSection vertical padding (fluid: 55px → 96px)
--section-padding-block-smCompact section padding (fluid: 55px → 72px, same mobile)
--section-space-m, --section-space-lSection internal spacing
--content-widthContainer max-width (default: 1350px)
--content-width-narrowNarrow container max-width (default: 800px)
--gutterPage horizontal padding (default: 1.5rem)
--grid-gapDefault grid gap
--container-gapGap between major blocks within a section
--radius-xs through --radius-fullBorder radius scale
--shadow-sm through --shadow-xlShadow scale
--transition-fast, --transition-base, --transition-slowTransition durations
--heading-font-weightHeading font weight (default: 600)
--heading-line-heightHeading line height (default: 1.2) — shared by all headings
--line-height-h1 through --line-height-h4Per-heading line-height overrides (opt-in, see below)
--text-line-heightBody text line height (default: 1.65)
--btn-padding-top, --btn-padding-bottom, --btn-padding-inlineButton padding
--btn-lg-padding-top, --btn-lg-padding-bottom, --btn-lg-padding-inlineLarge button padding
--btn-borderButton border (default: 1px solid transparent)
--input-padding-top, --input-padding-bottom, --input-padding-inlineInput padding
--input-lg-padding-top, --input-lg-padding-bottom, --input-lg-padding-inlineLarge input padding
--input-borderInput border (default: 1px solid var(--neutral-light))
--placeholder-colorInput placeholder text colour
--modal-bgModal background — light-dark() adaptive

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.

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.

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.

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)

Font sizes and spacing use clamp() values generated by Wage\FluidGenerator. This produces fluid values that scale smoothly between a mobile and desktop viewport.

// Single clamp value — e.g. for a one-off override
Wage\FluidGenerator::generate_clamp( 44, 76 );
// → 'clamp(2.75rem, 2.0227rem + 3.2323vw, 4.75rem)'
// Full scale — returns array of named tokens
Wage\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' ],
);

The class uses static properties for viewport range. All calls use these unless overridden per-call:

Wage\FluidGenerator::$min_vw = 360; // default
Wage\FluidGenerator::$max_vw = 1350; // default
// Override in child theme functions.php:
Wage\FluidGenerator::$max_vw = 1450;
FilePurposeOverride
inc/core/fluid-type.phpBody + heading type scalesinc/fluid-type.php in child theme
inc/core/fluid-spacing.phpSpacing scale + pairsinc/fluid-spacing.php in child theme

Both return arrays that are merged into the token output by tokens.php.

Body scale — step-0 = --font-size-m (always):

StepToken
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:

StepTokenUse
step -2--font-size-h5Smallest headings
step -1--font-size-h4Small headings
step 0--font-size-h3Card titles, subsections
breakout--font-size-h2Section titles (SectionIntro)
breakout--font-size-h1Hero 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.

  • Body scale (--font-size-xs through --font-size-xxl) — for body text, labels, buttons, ledes, nav links
  • Heading scale (--font-size-h5 through --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.

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-xs through --space-xxl). Never use arbitrary values like 1.5rem.
  • Font sizes must use tokens (--font-size-xs through --font-size-h1). Never hardcode font sizes.
  • Heading colour comes from --heading-color, set in core.css. Override via the token system, not CSS rules.
  • Margin-top only. Only add margin-top to elements. Never use margin-bottom.
  • Use gap over margins in flex/grid containers.
  • Don’t set inherited values. Body sets font-family, font-size, line-height, and color. Don’t redeclare them on child elements unless overriding.
  • Desktop-first breakpoints. CSS defaults are the desktop layout. Use max-width breakpoints to step down to smaller screens. Don’t use min-width.

Standard breakpoints:

QueryTarget
@media (max-width: 1023px)Tablet and below
@media (max-width: 767px)Mobile
@media (max-width: 639px)Small mobile

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.

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.