Brand Palette — 16-stop hue rotation
Verdigris
0.753 0.128 191.6
mix-1
0.64 0.13 216.0
mix-2
0.52 0.14 240.5
mix-3
0.41 0.15 264.9
Midnight Purple
0.29 0.15 289.3
mp-step-1
0.39 0.16 313.0
mp-step-2
0.50 0.17 336.7
mp-step-3
0.60 0.18 0.4
Pastel Red
0.70 0.19 24.1
pr-step-1
0.74 0.19 41.1
pr-step-2
0.79 0.18 58.2
pr-step-3
0.83 0.18 75.2
Cyber Yellow
0.87 0.18 92.2
cy-step-1
0.81 0.17 113.3
cy-step-2
0.77 0.16 137.1
cy-step-3
0.75 0.14 163.9
Neutral Scale — zinc-tinted (hue ~286)
50
0.985 0 0
100
0.967 0.001 286
200
0.920 0.004 286
400
0.705 0.015 286
500
0.552 0.016 286
800
0.274 0.006 286
900
0.210 0.006 286
950
0.141 0.005 286
Brand Gradient
Contrast Check — brand teal on backgrounds
Teal on dark  PASS 7.2 : 1
Teal on white  FAIL 2.5 : 1
Semantic Tokens — Light vs Dark
Light Mode
background: white
foreground: neutral.950
card: white
muted: neutral.100
border: neutral.200
Dark Mode
background: neutral.950
foreground: neutral.50
card: neutral.900
muted: neutral.800
border: white/10%
Documentation

Color System

Why OKLch

Verdigris uses OKLch as the canonical color space. All color tokens are defined in OKLch; other formats (HSL, hex, RGB) are generated by the build pipeline.

Why not HSL?

HSL is perceptually non-uniform — hsl(60, 100%, 50%) (yellow) appears far brighter than hsl(240, 100%, 50%) (blue) despite identical L values. This makes palette interpolation unpredictable and accessibility auditing unreliable.

OKLch fixes this:

  • L (lightness) is perceptually linear — equal numeric changes produce equal visual changes
  • C (chroma) controls saturation without shifting perceived brightness
  • h (hue) rotates through the color wheel

This means Patina’s gradient palette (teal → purple → red → yellow) was generated by rotating the hue while keeping lightness and chroma in controlled ranges. The result is a harmonious palette that “feels” balanced across all hues.

Browser support: OKLch is supported in all evergreen browsers since 2023. The build pipeline generates HSL fallbacks for email clients and legacy contexts.

Brand Palette

The palette is a hue-rotation gradient anchored at four points:

Name OKLch Approx Hex Role
Verdigris (teal) oklch(0.753 0.1279 191.57) #0fc8c3 Primary brand, CTAs, links
Midnight Purple oklch(0.29 0.1506 289.33) ~#1a0a4a Deep accent, dark sections
Pastel Red oklch(0.7 0.1909 24.11) ~#e85d3a Warm accent, sidebar active
Cyber Yellow oklch(0.87 0.1786 92.23) ~#d4c520 Highlight, attention

Between each anchor, three interpolation steps create a smooth 16-color chart palette. See tokens/color/base.json for all values.

Gradient Logic

Teal (191°) → step1 (216°) → step2 (240°) → step3 (265°)
  → Purple (289°) → step1 (313°) → step2 (337°) → step3 (0°)
  → Red (24°) → step1 (41°) → step2 (58°) → step3 (75°)
  → Yellow (92°) → step1 (113°) → step2 (137°) → step3 (164°)
  → (back to Teal)

This creates a full-spectrum palette from a single generative rule — add or remove steps by interpolating between the anchors.

Palette Semantics — What the Colors Mean

The brand palette has 16 tokens but the design system must define what they mean, not just what they are. This is critical for AI agents and evaluator pipelines that select colors without human visual judgment.

The palette divides into 6 semantic regions based on content category:

Trust (Teal → Blue)

The anchor of the Verdigris brand. Bright, authoritative, technically precise. Use for technology, platform capabilities, data quality, and primary brand expression. Safe as text on dark backgrounds. NOT safe as text on white (fails WCAG AA).

See rules/visual-rules.yml -> color.palette_semantics.trust for tokens, hue ranges, and tint values.

Depth (Deep Blue → Purple)

The darkest palette region. Creates gravitas and sophistication. Works as dark section backgrounds, hero overlays, and gradient endpoints paired with teal. Too dark for text use.

See rules/visual-rules.yml -> color.palette_semantics.depth for tokens, hue ranges, and minimum opacity requirements.

Energy (Purple → Magenta)

High chroma, vibrant mid-range. Demands attention without being aggressive. Use for fault detection, real-time monitoring, anomaly indicators, and alerting.

See rules/visual-rules.yml -> color.palette_semantics.energy for tokens, hue ranges, and tint values.

Warmth (Red → Coral)

Warm tones invite human connection. The sidebar-primary color in Patina. Use for contact, CTA hover states, team sections, and customer stories.

See rules/visual-rules.yml -> color.palette_semantics.warmth for tokens, hue ranges, and tint values.

Results (Orange → Yellow)

Bright, optimistic tones (high lightness). Natural for metrics, ROI, stranded capacity recovery, and financial outcomes. Use on dark backgrounds only — high lightness means poor contrast on white.

See rules/visual-rules.yml -> color.palette_semantics.results for tokens, hue ranges, and tint values.

Growth (Yellow → Green)

Green-teal tones that close the chromatic loop back to brand teal. Natural for M&V results, capacity recovery, sustainability, and completion narratives.

See rules/visual-rules.yml -> color.palette_semantics.growth for tokens, hue ranges, and tint values.

Neutral (Breathing Room)

Body text, structural sections, form backgrounds, and breathing room between colored sections. At least 70% of page sections should use neutral backgrounds to prevent chromatic fatigue.

Taste Boundaries — Restraint Rules

The palette semantics without ceilings will produce a rainbow. The brand pillars define the boundaries:

  • “Controlled color palette, no visual clutter.” (Masterful)
  • “Subtle use of the brand gradient.” (Refined)
  • “Every element earns its place.” (Precision)
  • “Credibility through restraint. Sophisticated buyers, not impulse shoppers.”

Color is earned, not assigned by lookup table. An accent region appears because it makes the page better, not because a mapping table says it should.

Teal dominance: Teal + neutrals must comprise at least 70% of any surface’s color expression. Other palette regions are rare accents, not co-primaries. The brand IS teal.

See rules/visual-rules.yml -> color.palette_semantics for maximum regions per surface type.

Section limits (web pages):

  • No more than 30% of sections should have non-neutral palette accents — the rest should breathe
  • Never place two colored sections adjacent — always separate with neutral
  • Accents are subtle (background tints and label colors) — NOT full-saturation backgrounds

Section Flow — Lightness Rhythm

Pages commit to a lightness direction. Alternating dark and light sections creates a strobe effect that forces the eye to re-adapt at every boundary. Content sections flow through subtle tonal shifts (white, neutral.50, neutral.100), not contrast flips.

See rules/visual-rules.yml -> color.section-flow for the machine-enforceable rules: max lightness jump, dark accent limits, hero/footer boundary exceptions.

Neutral Scale

The neutral scale is zinc-tinted (hue ~286°) rather than pure gray. This gives surfaces a subtle warmth that complements the teal brand color. Extracted from Patina’s production CSS.

Token OKLch Role
neutral.50 oklch(0.985 0 0) Near-white
neutral.100 oklch(0.967 0.001 286.375) Secondary/muted bg
neutral.200 oklch(0.92 0.004 286.32) Borders, inputs
neutral.400 oklch(0.705 0.015 286.067) Ring, muted-fg (dark)
neutral.500 oklch(0.552 0.016 285.938) Muted-fg (light), ring (dark)
neutral.800 oklch(0.274 0.006 286.033) Dark mode secondary
neutral.900 oklch(0.21 0.006 285.885) Light primary, dark card
neutral.950 oklch(0.141 0.005 285.823) Light foreground, dark bg

Dark Mode Strategy

Both codebases use the same mechanism: CSS custom properties toggled via a .dark class on the HTML element. The semantic tokens (background, foreground, primary, etc.) swap values between light and dark.

Key dark mode choices:

  • Background swaps from white to neutral.950 (near-black)
  • Primary inverts from near-black to light gray — ensuring contrast in both modes
  • Borders become semi-transparent white (oklch(1 0 0 / 10%)) for a subtle glass effect
  • Sidebar primary stays pastel-red in both modes — brand consistency

See tokens/color/semantic-dark.json for all overrides.

WCAG Contrast

The www site darkened the brand teal from hsl(178, 86%, 42%) to hsl(178, 86%, 28%) for WCAG AA text contrast on white backgrounds (~4.9:1 ratio). This is documented as legacy.www-primary-light in the token set.

Recommendation: Use the original bright teal (brand.verdigris) for decorative/non-text use (backgrounds, illustrations, data viz). Use neutral.900 (Patina’s approach) or the darkened teal (www’s approach) for text/interactive elements where contrast matters.

Multi-Format Output

The build pipeline generates:

Format File Consumer
OKLch CSS vars css/oklch.css Patina, modern browsers
HSL CSS vars css/hsl.css www (until OKLch migration)
Hex JSON hex/colors.json Email templates, Figma, print
Tailwind preset tailwind/preset.js Both codebases via config

Known Discrepancies (www vs Patina)

Token www Patina Resolution
Color space HSL OKLch OKLch is canonical; HSL output available for compatibility
Primary (light) Darkened teal hsl(178,86%,28%) Near-black oklch(0.21...) Different strategies — both valid
Ring/focus Green-shifted hsl(153,67%,38%) Neutral oklch(0.705...) Neutral ring is canonical
Brand teal hsl(178,86%,42%) oklch(0.75,0.1286,191.57) Equivalent values — confirmed