The rebrand to avanserv, on a monochrome token system
How the rebrand to avanserv turned every colour into a role on one ink ramp, and why the contrast gate had to be fixed before it could be trusted.
Most of the rebrand was not about colour. It was about making every colour a role, so that the whole application could be re-themed from one file, and then proving that each role stayed legible in both themes. The new identity only became visible at the end, once there was one place to apply it.
The old names were a vocabulary#
The interface had two brand colours, carbon and ruby, and they were not only colours. They were
the names of component variants: a badge, a toast or an alert took variant="ruby", keyed style
maps read them, formatters defaulted to them and tests asserted them. Retiring them meant renaming
a variant vocabulary across 54 files. I kept every colour value exactly as it was during the
rename, so the light application rendered identically and only the names moved. A rebrand that
changes names and values in the same step leaves no way to tell a typo from a decision.
Roles on top of one ramp#
Components painted raw slate greys and whites, so nothing could re-theme by context. I put a
semantic layer between the palette and the components: canvas, card, line, content in three tiers,
and ring. Each role has a light default and is remapped inside a .brand-dark scope. Because
custom properties cascade, bg-card resolves to white on a light ground and to the dark surface
inside any dark subtree, so a component themes by the scope it sits in rather than by a prop.
Roughly ninety components moved onto those roles, each light value equal to the slate it replaced.
One detail of Tailwind v4 mattered here. It removes theme variables that no utility uses from
:root, and the dark surfaces referenced their tokens through var() before anything consumed
them, so the token block had to be declared static to be emitted at all.
With the seam in place, the theme switch was small: a System, Light and Dark control, a store that tracks the operating system live, and an inline snippet that applies the resolved theme before first paint, so reloading in dark never flashes white. The harder part was the neutrals. They were spelled three incompatible ways, and they collapsed onto one eleven-stop ink ramp designed in OKLCH, pinned to a single hue and spaced on a smooth curve rather than an even one. The stops at each end are surface pairs that must read as adjacent elevations, while the midtones carry text and borders that must separate.
The contrast gate that was lying#
The contrast ratios lived in three hand-written tables that disagreed with each other and with the
stylesheet. I replaced them with a unit test that reads the tokens out of the stylesheet, resolves
the var() chains and the relative colours, and asserts AA in both themes. Reverting any tuned
value fails it, which no table ever did.
That test had a bug of its own. It blended translucent fills by averaging linear-light channels, whereas a browser composites in the gamma-encoded space, so every tinted fill measured louder than it renders. Four light status chips were reported as passing AA while they painted at 4.44:1. With the instrument corrected, the light theme's borders turned out to sit a full tier below their dark counterparts: a divider measured 1.06:1 against the card, the subtle line held the exact value of the surfaces it was meant to separate, and a resting field fill was byte-identical to the page behind it. The gate gained a cross-theme parity bound, which is the only way to say that a tier should weigh the same in both themes.
The ramp also has a physical limit. At its hue, the sRGB gamut collapses toward white, so the near-white surfaces cannot carry much of the blue tint the midtones have, whatever the design would like. Only the stops that paint borders could be made less grey.
A typeface chosen for its digits#
Numbers in a column jittered as they updated: file sizes, timestamps, integer cells. The cause was
the typeface. Lexend, as it was being served, carries no tabular-figure feature, so
font-variant-numeric had nothing to switch to. I checked the font binary before acting, then
moved to Spline Sans, which has real tabular figures, with Spline Sans Mono taking the monospace
slot so code and identifiers share its metrics.
The mark and the name#
The finished avanserv identity arrived two days after the token system landed, and the name and the visuals moved onto it in one commit, because the token gates pin the accent, the status colours and the mark to each other. The accent became a green, with a darker stop for type on light grounds and the brand stop itself on dark. The supplied status colours measured 3.67:1 and 2.90:1 as text on the near-white grounds, so I kept their hue and chroma and bent only the lightness, to the value closest to the supplied one that clears 4.5:1 everywhere it is painted.
The mark is one closed loop on a 45 degree axis, opened by an ellipsis drawn as a dash pattern on its stroke. The guidelines asked for a solid fill below 44 pixels, which turned every placement in the application solid and hid the ellipsis that carries the identity, so the outline became the default with a heavier stroke at small sizes. The name is an x-height word with no ascenders, so it sat visibly low beside the mark; a lift of 0.075em centres it. The rename carried no compatibility shims, so saved preferences such as the theme reset once, an accepted cost of not keeping dead aliases.
One defect fell out of the asset pipeline. The SVG masters documented their tokens by name in a leading comment, and a double hyphen is illegal inside an XML comment, so a browser parsing the favicon strictly drew a broken tile. The rasters were fine because the generator inlined each SVG into HTML before screenshotting, so the one path that matters to browsers was the one never exercised. The generator now refuses a master that is not well-formed XML.
What followed#
In September the application and the public site stopped keeping their own copies of the identity and started reading it from the brand's package. The tokens are imported from it rather than restated, and the mark is drawn from the geometry it publishes. That exposed what the local copy had cost: the favicon was drawn with a stroke of 88 against gaps of 107, 97 and 107 units, which left notches of 19, 9 and 19 that round caps then closed, so the tab icon had shipped the silhouette without the ellipsis. The published cut, a stroke of 64 against gaps of 116, reopens them to 52.
The same week, typefaces and icons moved to the site's own origin. Every page had fetched fonts from Google and an icon font from a third-party host, handing each visitor's address to two parties the privacy policy had to name. The icons are now Hugeicons Free under MIT, shipped as one SVG sprite, and the site build fails on any reference to a host outside the site's own domains, so the claim the privacy policy rests on is checked by the build rather than remembered.