Engineering

The site was setting its headings in two different typefaces

The home page rendered headings in Clash Display, every other template in Oswald. The fix was one variable — except for the instance a variable cannot reach.

The site was setting its headings in two different typefaces

Click “Shop” and the headings changed typeface. Not subtly — Clash Display to Oswald, a geometric display face to a condensed one, on every h1, h2 and card title on the page. The site had been shipping like that since the marketplace home page was built, and nobody caught it, because you only see it if you look at two templates in a row and are looking at the type rather than the content.

The cause was two variables that meant the same thing.

Two names for one decision

Both of these are declared in the same :root block, in the same file — assets/css/sections/global.root.css:

/* — type families (inner pages) — */
--display:'Oswald','Vazirmatn',sans-serif;
--sans:'Inter','Vazirmatn',sans-serif;

/* … later in the same block … */
--lav-font-display:"Clash Display","Satoshi",-apple-system,BlinkMacSystemFont,
                   "SF Pro Display","Segoe UI",system-ui,sans-serif;
--lav-font-text:"Satoshi",-apple-system,BlinkMacSystemFont,
                "SF Pro Text","Segoe UI",system-ui,sans-serif;

The --lav-* pair is the current brand stack. The unprefixed pair is inherited from the theme this one grew out of, and the comment above it is the whole story: type families (inner pages). At some point the marketplace home page was built on a new token vocabulary, and everything that was not the home page kept reading the old one.

The home page’s heading rules read --lav-font-display. Every other template’s heading rules read --display, either from that file or from the minified assets/css/main.css, which declares the same two names with the same two values. Both were correct in their own file. Together they were a site with two display faces.

What kept it alive was that the two halves also loaded different fonts. The home page requests Clash Display and Satoshi from Fontshare; the inner pages were requesting Satoshi only. So the inner pages had no Clash Display to fall through to even if a rule had asked for it, and the fault reinforced itself: the CSS said Oswald, the network agreed, and everything looked deliberate.

The repair is one variable, not a hunt through selectors

The temptation, once you see it, is to grep for font-family and start fixing rules. That is the wrong shape of fix — it is one edit per rule, it misses whatever is rendered from markup held in the database, and every component added next month reintroduces the bug.

Everything on the inner pages already routes through --display. Pointing that one name at the brand stack moves all of them at once:

:root,
.app,
.main,
.topbar,
.sidebar {
  --display:"Clash Display","Satoshi","Vazirmatn",-apple-system,BlinkMacSystemFont,
            "SF Pro Display","Segoe UI",system-ui,sans-serif;
  --sans:"Satoshi","Vazirmatn",-apple-system,BlinkMacSystemFont,
         "SF Pro Text","Segoe UI",system-ui,sans-serif;
}

One declaration. Every heading on every template that is not the home page, plus anything anyone adds later, which inherits the fix instead of needing it.

Two details in there are not decoration.

Vazirmatn stays in both stacks. It is the Persian fallback. The site’s owner writes Persian, and dropping it would silently degrade every non-Latin string on the site to a default face — the kind of regression that does not show up in any screenshot the person making the change is looking at.

The selector list is five selectors, not :root. That is the part worth generalising.

Token layers only reach what reads tokens

This theme re-declares its token set locally in four subtrees: .app, .topbar, .sidebar and the .lav-* page contexts. An override on :root does not reach inside any of them, because a custom property declared on a nearer ancestor wins for that whole subtree regardless of where the :root declaration came from or how specific it looks.

Some of those re-declarations are not even in a file. Part of this site’s CSS lives in the database, printed on wp_head at priority 100 — after everything wp_enqueue_style emitted — and the database copy redefines the whole token set on .topbar with literal values. An enqueued stylesheet loses to it at equal specificity, every time. The stylesheets that need to win are hand-printed with a <link> at later priorities:

100  database CSS (Code Studio)
101  css/bineret-brand.css      ← inc/brand.php
102  assets/css/packages.css    ← inc/packages.php, services views only
103  css/bineret-liquid.css     ← inc/liquid.php

If you take one thing from this: a design token is only a single source of truth for the parts of the tree that read it, in the layer order that actually executes. “I changed it on :root” is a hypothesis, not a fix. Two questions settle it — who else declares this name lower down, and who prints last.

The other half of the fix was on the network

Repointing the variable is not enough if the face never arrives. The inner pages had to start requesting Clash Display too:

wp_enqueue_style( 'bineret-fonts', lavtheme_fontshare_url(), array(), null );

Two things came out of doing that properly, both of which are the sort of thing that only shows up when you read the two requests side by side.

First, the URLs were nearly identical and not quite. The inner-page request asked for satoshi@400,500,700,900; the home page asked for satoshi@400,500,700. Overlapping, not identical — so a visitor who landed on the home page and clicked through to anything else paid for a second font-CSS round trip that returned almost the same bytes. Both now build the URL from one helper and cannot drift again.

Second, once --display and --sans pointed elsewhere, the Google Fonts request was carrying two families nothing on the site could select any more. It had been asking for five; it asks for three now — Vazirmatn, Newsreader and JetBrains Mono, each tied to a token something actually reads. Oswald and Inter are gone. The declarations naming them still sit in assets/css/main.css and global.root.css, dead by the time a browser resolves anything, and they are left there rather than edited: those two files are the legacy base, and a change in them is a change to the cascade in every template at once. The override wins; the dead text is harmless; the risk of touching it is not.

And a fourth instance survived, in the wordmark

This is the part that makes the story worth writing down rather than a tidy before-and-after.

The one-variable repair reaches everything that reads a CSS variable. The site’s logo is an inline SVG, and its text half sets the family as an SVG presentation attribute:

<text font-family="Clash Display, Oswald, sans-serif" font-size="30"
      font-weight="700" letter-spacing="0.6" x="44" y="36"
      fill="currentColor">BINERET</text>

That attribute is a literal. No var(), no token, no cascade layer — it names families by string, and it appears three times: the topbar, the marketplace footer, and the footer used by every inner page. The heading fix could not reach it, and it was not found by the round that fixed the headings. It surfaced later, this week, while auditing which font families the site still names anywhere, and the only reason it surfaced then is that somebody grepped for the family name across the whole theme rather than across the stylesheets.

It is a small bug — the wordmark is one word — but it is exactly the class of thing a token layer is supposed to have made impossible, which is what makes it worth naming out loud. Things that do not read tokens are the permanent blind spot of a token system, and there is always a list of them: inline SVG, canvas drawing, anything rendered into an image, e-mail templates, OG cards, PDFs. On this site the OG card and the favicon are generated from the same SVG path data by a script, which is why the mark was safe and the type was not.

The wordmarks now name Clash Display first. Oswald stays behind it in the list, and it is now inert — nothing requests that family from anywhere, so it will only ever be selected on a machine that happens to have it installed locally. That is a fallback with no plan behind it, and it should probably become sans-serif. It is on the list, and it has not been done.

Two smaller things I would do differently

The bug was found by clicking, not by anything automatic. No test on this site would have caught it, and the obvious candidates would not have either: a visual regression suite compares each page to its own previous screenshot, so a site where every inner page has consistently rendered in the wrong face for months is a suite that is consistently green. What catches this class of fault is a check across pages rather than across time — assert that the computed font-family of an h2 is the same string on the home page and on the shop page. That is a few lines in whatever browser automation is already running, and it is not written yet.

The deeper fix is not to have two names in the first place. --display and --lav-font-display both resolve to the brand stack now, which means the next person to add a component picks one arbitrarily and both work — until one of them is repointed and the other is not, and this article gets written again. The right end state is one name, with the other aliased to it and marked deprecated, so there is exactly one place a change has to happen. That is a larger edit across two legacy stylesheets and it is not done either.

What the round did produce is measurable and small: display typefaces in use across the site, 2 → 1.

Comments

No comments yet — be the first to share what you think.

Leave a comment

Your email address stays private. Required fields are marked