The Seafoam That Should Have Been Lime

How a pair of drifting secondary buttons forced us to harden our component reuse process.

The Seafoam That Should Have Been Lime: A Love Letter to Component Reuse

Somewhere around the fifteenth epic, a secondary button showed up wearing the wrong colour to the party. Nobody noticed for weeks because it was the Header button — sitting just far enough from the Hero button that your eyes never did the side-by-side. One was lime. One was seafoam. Both confidently claimed to be "secondary."

This is the story of how two buttons diverged in a codebase, and what we built into the process so it won't happen again.

How It Happened (Organically, Obviously)

The Header component was born early — before the design system adapter layer existed. It needed a CTA button, so it did the reasonable thing: slapped some CSS classes onto the Link atom and called it a day. Background colour? Seafoam, because that's what the Sanity schema said: "Secondary (Seafoam)."

Months later, the DS adapter Button.jsx was created. It pulled its secondary colour from the token system: --st-button-secondary-bg: var(--st-color-lime). Lime. Not seafoam. But nobody went back to the Header because it was "already working."

The result? Same style: 'secondary' value in Sanity. Different colour on screen depending on which component renders it. The CMS was lying to editors ("Seafoam") while the main Button was rendering lime. The Header Button was rendering seafoam through CSS classes that lived outside the component system entirely.

Three separate files each had their own inline const isExternal = url?.startsWith('http') check. The Button itself rendered a plain <a href> even for internal pages — causing full page reloads instead of SPA navigation. Every caller manually wired target="_blank" and rel="noopener noreferrer". Copy. Paste. Drift. Repeat.

Why Nobody Caught It

Because nothing in the process asked.

The epic template verified layout contracts, schema enums, token parity, and theme modifiers. It never asked: "Does something already exist that does this?" The "Before You Build" rule in MEMORY.md covered three component directories but didn't mention utilities or schemas. There was no registry of shared helpers, so each author wrote their own.

The ctaButton object schema and ctaButtonDoc document schema represented the same concept but were treated as unrelated files. When the doc-level schema was fixed to say "Lime," the object-level schema was missed. Half the pair was corrected. Half wasn't.

Every one of these decisions was locally reasonable. That's what makes organic drift dangerous — it's never one bad choice. It's a hundred reasonable ones that compound.

The Guardrails We Built

Four conventions now live in the project system:

1. Interaction Surface Audit (epic template gate)

Before creating any new interactive element, search all four layers: components, design system, utilities, and schemas. List what exists. Justify why it's insufficient, or confirm you're extending it.

2. Use Case Coverage (epic template gate)

When creating a new component, enumerate the target layer's use cases, not just the source layer's. A web adapter for a <button> that also needs to handle <a href> internal links, external links, and mailto: — those use cases must be listed before writing code.

3. Shared Utility Registry (MEMORY.md)

A named table of cross-cutting utilities. URL construction lives in routes.js. Link resolution will live in linkUtils.js. Before writing an inline helper, check the registry.

4. Paired Schema Convention (CLAUDE.md)

Object/document schema pairs are formally linked. A fix to one half requires review of the other in the same commit. ctaButton and ctaButtonDoc are the first registered pair.

The Uncomfortable Truth

Component reuse discipline is a process problem, not a skill problem. Smart engineers will always make locally reasonable decisions that drift globally. The fix isn't "be more careful" — it's building the question into the workflow so it gets asked before the code is written, not after the audit.

Unknown block type "richImage", specify a component for it in the `components.types` optionUnknown block type "code", specify a component for it in the `components.types` option

From Locally Reasonable to Globally Inconsistent

Two secondary buttons diverged in a codebase: one born before the design system adapter layer, one after tokens were canonical. Both were "right" in their local context, and both quietly broke the promise that style: 'secondary' meant the same thing everywhere.

The fix wasn't a refactor sprint; it was adding explicit gates:

The lesson: if your process doesn't force the "does this already exist?" question, your codebase will answer it the hard way.

Operational Checklist