The 2016 CodePen had five CSS custom properties:
:root {
--blue: #0b1d51;
--purple: #797596;
--pink: #a1869e;
--beige: #bbada0;
--tan: #d1c6ad;
} In 2025, those five values became the foundation of a design system with 600+ CSS variables, 11-step palette ramps, a two-layer chroma architecture, shifted semantic hues, and 8 creative color application patterns. This is how that happened.
From Hex to OKLCH
The first step was understanding what I actually had. Hex values tell you nothing about perceptual relationships. #0b1d51 looks dark and blue, but how dark? How blue? And does it relate to #797596 in any systematic way?
Converting to OKLCH revealed the structure:
| Hex | OKLCH | L | C | H |
|---|---|---|---|---|
#0b1d51 | oklch(0.22 0.07 265) | 0.22 | 0.07 | 265 |
#797596 | oklch(0.55 0.04 290) | 0.55 | 0.04 | 290 |
#a1869e | oklch(0.62 0.04 330) | 0.62 | 0.04 | 330 |
#bbada0 | oklch(0.74 0.03 65) | 0.74 | 0.03 | 65 |
#d1c6ad | oklch(0.82 0.03 87) | 0.82 | 0.03 | 87 |
Three things jumped out:
Lightness ramps naturally. 0.22 → 0.55 → 0.62 → 0.74 → 0.82. The palette was already a lightness scale, dark to light, without my having designed it that way.
Chroma is uniformly low. Maximum C:0.07 on the darkest color, C:0.03-0.04 on the rest. This is a muted palette — no vivid accents, no saturated primaries. The 2016 me picked colors that are all in the same chromatic register.
Hues span the wheel. 265 → 290 → 330 → 65 → 87. Navy, lavender, rose, warm neutral, warm neutral. Not evenly distributed, but covering a wide range with a deliberate gap in the green/teal region.
The AI Slop Problem
When I started expanding these into full palette ramps, I immediately ran into a problem: AI-generated OKLCH values converge on the same stops as every other design system.
I asked Claude to expand #0b1d51 (H:265) into an 11-step ramp. The result centered on H:277 — dead center of what I now call the AI slop purple range (H:255-285). This is the exact hue used by Claude.ai (~270), ChatGPT (~275), Vercel (~265), Linear (~268), GitHub Copilot (~272), and every Tailwind Indigo/Violet configuration. If you let an AI pick your purple, you get the same purple as everyone else.
The fix: shift the hue. H:265 → H:252 (Navy) moves it out of the slop range while staying in the blue family. H:290 (Lavender) was already outside. For the other palette that had been generated at H:295 (Dusk), I shifted to H:310 to create clear separation.
The same problem applied to semantic colors. Default OKLCH for “error red” is around H:25 — the same as Ark UI, Radix, and Shadcn destructive. I shifted it to H:4, a warmer red derived from the original portfolio’s coral accent (#e55d87). Warning moved from H:85 to H:55. Success from H:145 to H:168. Info from H:240 to H:195.
Every shift was 15-25 degrees away from the AI default, enough to be perceptibly different while staying within the semantic family (red is still red, green is still green).
Two-Layer Chroma Architecture
The original palette was uniformly low-chroma. For a surface-and-text system, that’s fine. But for interactive elements — buttons, links, badges, focus rings — you need more saturation. The question was: add new hues, or increase chroma on existing hues?
I chose chroma separation:
Layer 1 — Base (max C:0.095): The original 5 hues at their natural low chroma. Surfaces, text, borders, backgrounds. The visual infrastructure.
Layer 2 — Interactive (C:0.12-0.14): The same hues, louder. Rose vivid at C:0.12 for primary actions. Navy vivid at C:0.14 for secondary actions. Teal at C:0.095 to fill the gap between Sand (H:87) and Navy (H:252) on the hue wheel.
The chroma gap between layers is the visual hierarchy. A primary button (Rose vivid, C:0.12) stands out from a surface (Rose base, C:0.04) not because it’s a different color — it’s the same hue — but because it’s louder. The eye reads chroma difference as importance.
Chroma Hierarchy for Semantics
The semantic colors needed their own internal hierarchy. Not all status messages carry the same urgency:
| Semantic | Chroma | Why |
|---|---|---|
| Error | C:0.165 | Highest urgency. Must command attention. |
| Warning | C:0.12 | Elevated attention. Not an emergency. |
| Success | C:0.10 | Settled, resolved. Should recede. |
| Info | C:0.095 | Passive, contextual. Lowest urgency. |
Error is almost 2x the chroma of Info. This means an error alert is visibly more saturated than an info alert — the urgency is encoded in the color’s intensity, not just its hue. Most design systems use uniform chroma across semantics (all at C:0.12 or similar), which flattens the urgency hierarchy.
The 8 Personality Patterns
Having a palette isn’t the same as having a visual identity. The patterns describe how to apply the palette:
Chroma-as-interaction —
@propertyenables smooth animation of--surface-chroma. Hover saturates, doesn’t darken. The component becomes more vivid, not just different.Temperature mapping — Content sections carry hue: cool (H:252) for technical writing, warm (H:65) for personal content, rose (H:330) for featured items. The hue tells you the kind of content before you read it.
Depth through chroma — Elevation isn’t shadow. It’s saturation. A modal (highest elevation) is more chromatic than the page body (lowest elevation). This replaces
box-shadowstacking in dark mode, where shadows are invisible.Typographic color weight — Instead of using font-weight to create hierarchy, use chroma. A heading at C:0.06 stands out from body text at C:0.01 the same way bold stands out from regular — but through color volume, not stroke width.
Asymmetric color fields — Full-bleed hue bands with hard cuts. No gradients (gradients are an AI slop pattern). A rose section meets a navy section at a sharp boundary. The contrast is the design.
Inverted content/chrome — The frame (navigation, backgrounds, section chrome) is chromatic. The content (cards, text blocks) is neutral. The personality is in the structure, not the content.
Contextual chroma by density — Dense reading content gets low chroma (P7 in the article body — you’re reading in it right now). Sparse content (hero, status bar) gets higher chroma. The eye needs less distraction when processing more text.
Temperature-shift states — Hover warms the hue. Focus cools it. Active goes warmest. The state change moves along the color wheel, not just up and down the lightness scale.
:has()enables parent-level response to child interaction.
What the Color Journey Taught Me
The jump from 5 hex values to 600+ CSS variables isn’t about the numbers. It’s about the difference between “colors I like” and “a system I can reason about.”
The 2016 palette worked because the human eye is good at picking harmonious colors intuitively. The 2025 system works because OKLCH gives names to why those colors harmonize — perceptual uniformity, hue spacing, chroma register — and lets me extend the intuition systematically.
I couldn’t have built Stratos in 2016. Not because the CSS features didn’t exist (OKLCH wouldn’t ship until 2023), but because I didn’t have the vocabulary. “These colors feel good together” is a starting point. “These colors have low chroma, distributed hues, and a natural lightness ramp that I can formalize” is a design system.
This is Part 6 of 7 in the Resume Evolution series.