0.753 0.128 191.6
0.64 0.13 216.0
0.52 0.14 240.5
0.41 0.15 264.9
0.29 0.15 289.3
0.39 0.16 313.0
0.50 0.17 336.7
0.60 0.18 0.4
0.70 0.19 24.1
0.74 0.19 41.1
0.79 0.18 58.2
0.83 0.18 75.2
0.87 0.18 92.2
0.81 0.17 113.3
0.77 0.16 137.1
0.75 0.14 163.9
0.985 0 0
0.967 0.001 286
0.920 0.004 286
0.705 0.015 286
0.552 0.016 286
0.274 0.006 286
0.210 0.006 286
0.141 0.005 286
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 |