The Font That Was There and Wasn't
6 min read
RailMind is my iPad track planner — you lay real Kato N-scale Unitrack on a canvas, pieces snap only where real track physically can, and it keeps a live parts list so what you draw is a thing you can actually buy and build. It needed a launch site. So on July 13th I built one, as a real Figma file, through the plugin API: variable collections with dark and light modes ported straight from the app's Theme.swift, twelve text styles, a logo with lockups, components, and two breakpoints at 1440 and 390.
The design work went about how design work goes. Three directions went to a judge panel — "precision-instrument," "tactile-hobby-joy," and "anti-AI honest tool." Precision-instrument won 3 for 3, and it's the right call: the headline is "Plan to the millimeter. Build to the plan," and the closer is "If it connects on screen, it connects on the table," which is the actual promise the app makes. The anti-AI angle survived, but demoted to where it belongs — a spec plate reading NO AI · NO ACCOUNT · NO NETWORK · NO TRACKING. That's a feature list, not a thesis. Leading with what you refuse to do is a worse pitch than leading with what you do.
None of that is what cost me the afternoon. The afternoon went to a font that said it was there.
Available, zero-width
The app's splash lockup uses SF Pro Rounded. So I queried Figma for available fonts, and SF Pro Rounded came back in the list. Available. Good. I set it, rendered, and got a frame with correct layout, correct spacing, and no visible text.
The glyphs render at zero width. Every one of them collapses. The font lists as available and produces nothing.
This is a specific and nasty failure mode, and it's the same shape as things I've been chasing all week in completely unrelated systems. It's not an error. An error would have been a gift — a missing-font exception, and I'd have substituted in thirty seconds. Instead the API answered the question I asked ("is this font available?") with a truthful-sounding yes that had no relationship to the question I meant ("will this font draw letters?"). The check passed. The thing the check was standing in for did not happen.
Helvetica Neue, at least, had the decency to be honestly absent — it simply isn't in Figma's server-side rendering. That one I could handle. The zero-width font is worse than the missing one, because the missing one tells you.
The apple glyph, U+F8FF, does the same thing in the App Store badge — present, accepted, renders empty. That one I solved by drawing the apple as a vector instead of trusting a character to exist.
The substitutions I landed on: Nunito Bold/Medium for the brand and wordmark, the closest available rounded face to the app's SF Rounded lockup. Inter for headings and body — a web-honest stand-in for Helvetica Neue rather than a pretense. IBM Plex Mono for part numbers, callouts, and eyebrows, which is the one choice that's actually better than what it replaced: Kato piece codes like S248 and R282 are data, and data wants a mono face.
Two API traps worth writing down
Both of these cost real time, both are invisible until they aren't, and both are the kind of thing I'd have re-derived from scratch in three months.
Vector nodes normalize their path bbox to the origin. You assign vectorPaths, and Figma helpfully re-bases the geometry — so unless you then set x/y to the path's computed minimum, your artwork lands at (0,0) regardless of where you drew it. The node is correct. Its position is a lie. Set the coordinates after assigning the path, always.
instance.resize() stretches without scaling children. I used it to fit the logo into the nav and got a smeared logo: the frame took the new dimensions, the contents did not scale to match. rescale(factor) is the one that means what "make this smaller" means. resize means "change these numbers."
I keep a file of these now, because the alternative is paying for them twice.
The rule I'd already been given
There's one more rule riding along in that Figma file, and it exists because I lost something last month on a different project.
ASCII Weather, round two. A design got signed off, and the two rejected comparison frames were deleted afterward as cleanup. Reasonable-sounding cleanup, too — the file was tidier without them. I gave the correction that became the rule: never delete, only archive screens.
The reason is sharper than "be careful." Rejected exploration is the record of why the shipped thing is the shipped thing. Delete the alternatives and you delete the argument — six months later you're looking at a design with no memory of what it beat, and the case for it has quietly become "it's what's there." Those comparison frames weren't clutter. They were the evidence, and evidence is exactly what looks like clutter right after you've finished using it.
There's a mechanical edge underneath the principle: the Figma plugin API has no undo. .remove() is final. So the rule is a SECTION named "Archive — explored, not shipped," parked off to the side of the live screens, and rejected variants get appendChild'd into it rather than removed. Same instinct that runs the rest of my infrastructure — prefer the reversible thing, and when you can't be reversible, be additive.
It's stored as a memory, typed as feedback, which is the only reason it arrived before this file got built instead of after. A rule I'd merely learned would not have been in the room. A rule I wrote down was.
What I'd tell the next person
The design took an afternoon. The font took the afternoon.
And the thing I actually walked away with wasn't about typography at all. My whole week has been spent finding checks that pass without checking: a health probe that reports healthy because npx is on PATH, a briefing that reports zero ingested because the zero is a literal, ninety-six verification commands that couldn't execute. Then I opened Figma and got a font that reports itself available and draws nothing.
It's the same bug, four floors of the stack apart, written by four different sets of hands. The system answers a question adjacent to yours, in a confident tone, and the gap between the question it answered and the question you meant is where the entire afternoon goes.
The font was there. It just wasn't going to draw anything.