The primary button was too loud. The hover state was the answer.
The decision. Primary is Seam at rest and Edge on hover. Secondary stays unfilled in every state so the tiers never share a fill.
The design system’s primary control was a Strong fill, #050B10, close to black, on a warm paper field. On the style guide’s specimen sheet it read as a shout on an otherwise quiet page. I kept looking at the hover specimen in section 09 and realizing that was the control I wanted at rest.
So primary is now Seam at rest and Edge on hover. Hover steps one rung deeper into the same blue family instead of changing the control’s identity under the cursor. Contrast against Strong text is 12.97:1 at rest and 6.17:1 on hover, both well clear of AA for the 12px uppercase label, and the step between the two states is visible without any motion.
I considered Pearl for the hover and rejected it. Pearl is dark-theme body (#C9C5BE); in the light theme that same role resolves to Ink (#212B36). So “Pearl on hover” either renders a warm bone that nearly vanishes on paper, or resolves to another near-black and brings back exactly the starkness I was removing. Edge is the honest neighbour.
There was a consequence I didn’t ask for. Secondary used to gain a Seam fill on hover, and that fill is now Primary’s resting state. Two tiers sharing one fill makes the hierarchy unreadable. So Secondary stays unfilled at every state and its hover thickens the outline and strengthens the label instead. The tiers now differ by fill versus no fill throughout.
/* Primary: Seam fill, deepening to Edge on hover. */
.jfb-button--primary { background: var(--seam); color: var(--strong); border: 1px solid var(--edge); }
.jfb-button--primary:hover { background: var(--edge); }
/* Secondary: outlined, and it stays unfilled so the tiers never share a fill. */
.jfb-button--secondary { background: transparent; border: 1px solid var(--edge); }
.jfb-button--secondary:hover { box-shadow: inset 0 0 0 1px var(--edge); }
The change had to land in three places at once or the parity test fails: components.css, both copies of the style guide (the section 09 specimens, their arrow strokes, and the section 16 handoff snippet), and the README’s tier list. check:parity and check:render both pass, which is the proof the guide and the package still agree, and the reason that test exists.