Skip to content

One identity, two registers: turning a personal design language into a gate

I have a photography and writing site1 whose look I built by hand, and about eighteen self-hosted tools, such as a word game, a mouse configurator and a push-notification broker, that a coding agent mostly wrote. Side by side they looked like the work of two different people. This page covers how I gave the tools the same identity as the site, and how a program now checks that identity so an agent does not have to remember it.

Provenance. The design history comes from my own decision log for this work, kept as it happened on 2026-11-07 and 2026-11-08. Numbers about the tool (rule counts, the port fixes, the centring results) were taken from its repository and test output on 2026-11-08. My own reactions are paraphrased.

TL;DR:

  • A tool reads as mine when it copies my art site’s frame and type literally; copying only its colours did not register.
  • The technical body can differ: denser, a visible section nav, tables and action bars. Catalogue costume (part numbers, dotted leaders, bracket labels) does not belong.
  • Sub-pixel misalignment is visible and measurable. Most of it came from one font’s vertical metrics, fixed with ascent-override and descent-override.
  • Rules in a prose skill drifted within days. A small Go command now holds the only copy of the design files and fails CI when an app departs from them.
ElementCopied from the art siteDifferent in the tools
TypeOverpass Mono for titles, nav and labels; Inconsolata for prose and data; no sans-serifDenser sizes: 15-16px body, 12-14px labels
FrameMark top left, icon controls top right, same gutters, theme toggleA visible section nav under the header
ColourPage colours, metadata grey, slate accentState colours for ok, warning and error
SignatureThe ea monogramFooter signature only; the header carries the app’s own mark
Body-Spec rows, tables, action bars, notices

The art site and the tools have different jobs. A photograph page wants space and one subject; a device configurator wants every setting visible and every action findable. So the goal was never to make the tools look like the art site. The goal was for a viewer to tie a tool to the art site at a glance, the way two books from one publisher share a spine and a typeface but not a cover.

I call these the art register and the tech register. They share a core:

  • Type. The art site has no sans-serif face at all. Overpass Mono, bold and uppercase, sets titles, navigation and labels; Inconsolata sets prose and metadata. The tools now use exactly those two faces in the same roles, and nothing else.
  • Frame. A mark at the top left, icon controls at the top right, the same page gutters, and the art site’s theme toggle: a filled dot in light mode that shrinks into the core of a sun while its eight rays fade in, over 300ms.
  • Surfaces and accents. The art site’s light and dark page colours, its grey for metadata, and a slate accent that marks only the current item, selection lines and hovers.
  • Signature. The ea monogram is the art site’s header mark. On a tool it appears only as an unlinked footer signature, announced to screen readers as “by Erfi Anugrah”. A tool’s header carries its own mark.

The tech register then differs in the body: a visible section nav (a tool cannot hide its sections behind a menu button), denser spec rows and tables, action bars, and notices.

Three directions came before this one, and each failed for a reason worth keeping.

The first was a palette pass. It replaced the tools’ warm cream with the art site’s neutral grey, introduced plum and lilac for selection, and separated the colour roles for information, danger, state text and game tiles. The semantic clean-up was worth keeping. The look was not: the tools kept their old composition, and repainting a composition does not change whose work it reads as.

The second was structural. It rebuilt both pilot apps’ page layouts around their tasks, with the art site’s surfaces and accent, a sans-serif for interface copy and a monospace for data, and it removed bracketed actions and stacks of boxed sections. A later font pass swapped the sans for Overpass and the data face for Overpass Mono. Put next to production, my reaction to that version was that the difference was so subtle it barely registered.

Reading the art site’s source explained why. Its identity lives in its frame and its type, and the pilots had copied neither. They had the art site’s colours inside someone else’s page. Using a sans-serif for interface copy, which the art site never does, was enough on its own to break the link.

The third failure ran through all of them: borrowing from McMaster-Carr’s catalogue as costume. McMaster is a good reference for density, how much one screen can hold while staying findable. Part-number stamps, dotted leaders between a label and its value, labels wrapped in square brackets and boxed chips are its costume, and they had crept into the tools. All of them were removed, and the gate now rejects them.

What worked was to stop designing and copy. The header, footer, gutters, type roles, toggle and section-heading bar were taken from the art site’s source and rendered pages, then the tech body was built inside that frame.

My review of the restyled tools ran through screenshots, and most of the complaints were about a pixel or two: text that sat slightly high in a button, a tab underline that pushed its label up, a status line whose label and value did not share a baseline. Each of those had a mechanical cause.

The largest was in the font itself. Overpass Mono declares an ascent of 0.883 em and a descent of 0.383 em around a cap height of 0.700 em. An uppercase label has no descenders, so the empty space below its baseline is larger than the space above its caps, and the ink sits 0.1 em above the middle of every line box: in every button, tab and badge. CSS can correct this per font face with ascent-override and descent-override23. Moving 0.1 em from the descent to the ascent (ascent-override: 98.3%, descent-override: 28.3%, line-gap-override: 0%) keeps the line height and centres the caps. Inconsolata, used for prose and data, keeps its stock metrics.

The rest became rules:

  • Buttons centre their content, and equal-weight button pairs share their width.
  • Indicator lines hang outside the label’s box as a pseudo-element, so they never take space from the label. Nav, tab and link underlines grow from the middle over 220ms and stay full under the current item.
  • A row that mixes the caps face with the data face aligns on the text baseline. A box next to text, such as an SVG wordmark, aligns on its centre.
  • Colour usage is shared, not just colour values. Two apps with identical tokens still looked different because one filled its primary buttons with the accent and the other filled them with ink. Text on an accent fill gets its own token, because white on the light dark-mode slate failed contrast.

Eyeballing does not scale to a fleet, so centring is measured. A script loads the page in headless Chromium, screenshots every button, tab and nav link at twice the device scale, finds the bounding box of the text ink against the control’s own background, and reports its offset from the box centre. Anything more than 1px off fails. The first run flagged controls in both pilot apps; after the font metrics and the layout rules above, it reports none in light or dark mode.

The rules first lived where my coding agents look for instructions: a skill, a folder of Markdown with the token stylesheet, the shared component styles and four Python checkers beside it. That was not enough, for two reasons. The shared styles had been copied by hand into each app, about 45 rules apiece, and the two copies had already drifted apart. And the checkers were scripts only an agent working in that folder knew to run.

So the files and the checks moved into a small Go command, house. The skill keeps the rules and the reasoning; the binary holds the only copy of the design files and enforces them:

  • house check is the gate. It confirms the app’s colour tokens match the canonical set and pass contrast in both themes, that the vendored block of shared styles is byte-identical to the binary’s copy, and that a static audit of the source finds none of the rejected costume (part numbers, bracket labels, sans-serif faces, dotted leaders, tinted boxes, copyright lines or social icons on a tool, grey fills on disabled buttons) and does find the signature. Given a preview URL it also runs the centring measurement in light and dark and checks the rendered page: the signature is present and its image loads, the body is set in Inconsolata, no visible element uses a sans-serif face, and the theme toggle exists. A rendered step that cannot reach its URL reports SKIP, never PASS.
  • house sync writes the shared block into an app’s stylesheet between marker comments, inside a CSS cascade layer so the app’s own utility classes still win. house init sets up a new app, and house fleet lists every app with the version of the block it carries and the result of its last check.
  • house shot takes light, dark and phone-width screenshots of an app next to the art site.
  • Each migrated app runs house check . --static in CI with a pinned release of the binary, and my coding harness exposes the gate as a tool that returns only failures.
house inittokens and framehouse checkwith a preview URLFAILhouse shotnext to the art sitePASSmy reviewchangesCI gateand deployapproved
  1. house init vendors the shared files into the app.
  2. Align the app’s tokens and build its frame.
  3. house check against a running preview; on FAIL, back to step 2.
  4. house shot renders light, dark and phone-width screenshots next to the art site.
  5. I review them; changes go back to step 2, approval adds the CI gate and deploys.

The screenshots are the step a check cannot replace. The gate catches what can be stated as a rule. Whether a page reads as mine is something I decide by looking, and most of the corrections that shaped the rules started as a cropped screenshot with one line of complaint.

The Python checkers were ported to Go by coding-agent subagents, and the port was compared against the originals before anything relied on it. The comparison found one real difference. On the live word game the Go centring check flagged a control the Python one passed, because its screenshot clip was 4px larger than the element on every side and it compared colour channels separately instead of on luma. With both fixed, the two agreed: 11 controls, none off.

The first time the Go source audit ran over the word game, it found a dotted leader left in the match report, a line reading REASON........ followed by the value. The hand review had missed it, and so would the Python original, which only looked inside quoted strings.

An end-to-end run of every command on throwaway copies of real apps then found five bugs in the port itself: the app registry pointed at app subfolders rather than repository roots, where CI runs; the two pilot apps’ preview URLs were swapped, so rendered checks would have measured the wrong app; the token check crashed on an app that had not adopted the tokens yet, instead of failing with findings; the favicon generator drew its letter upside down, because the Go font library returns outlines with the y axis pointing down; and the screenshot command accepted a flag to skip the reference image and then ignored it.

The first app migrated after the pilots, the push-notification broker, found one more gap. Every check passed while its footer signature was invisible. The signature is a CSS mask over a small SVG, and the broker’s server only serves an allowlist of static paths, so the image returned 404 and the mask drew nothing while the element sat in place. The rendered check now fetches the mask image and fails on anything but an SVG.

The tools carry an interim favicon: the app’s initial on a paper tile with a coloured corner notch. It reads as a catalogue stamp more than as part of the identity. The intended marks are drawn in the style of the ea monogram, one per app, each specific to what the app does; that drawing work has not started.

Three apps run on the shared block today. About fifteen more are queued, smallest first, each with its own migration and its own review against the art site.

  1. E. Anugrah, “Erfi Anugrah,” photography and writing site. https://erfianugrah.com/ ↩

  2. MDN Web Docs, “ascent-override,” CSS @font-face descriptor. https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/ascent-override ↩

  3. MDN Web Docs, “descent-override,” CSS @font-face descriptor. https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/descent-override ↩