← Lab

Fitnit · v1.123.1 → v1.123.2

Profile
surface

Six cards stacked in one column, carrying three corner radii, two paddings and two gaps between them. No test could see any of it. It was reported the only way this class ever gets reported — someone looked at the screen and said the cards were all different.

Why this is worth a board. Every number here was defensible on its own. The settings list used Apple’s inset-grouped radius; the cards used a prominent-surface radius; the referral card used the design token. Each decision was locally right and the screen was collectively wrong, which is the only interesting kind of design debt.

What was actually there

Measured from the stylesheets, not estimated. The last column is the gap to the next card below.

SurfaceRadiusCurvePaddingGap below
Subscription16continuous1612
Refer & Earn14continuous1612
Account16continuous1624
Muscle Map16continuous1624
Achievements16continuous2024
Settings groups12circular

Three radii, two paddings, two gaps. Only one of the six radii (14) was a value from theme/tokens.ts at all — the rest were typed at the call site.

The corner nobody could name

The settings groups were the only surface written as a plain borderRadius, with no borderCurve: 'continuous'. That is not a rounding difference, it is a different curve: a circular arc meets the straight edge at a tangent discontinuity, and iOS uses a superellipse that blends curvature into the edge instead.

Both shapes below are sampled from |x/r|ⁿ + |y/r|ⁿ = 1 at the same radius — n = 2 is a circle, n = 5 is close to what iOS draws. Same corner radius, visibly different shape.

n = 2 · borderRadius
n = 5 · continuous

Once the two are side by side, the circular one reads as slightly wrong on every card in the app. That is why squircle(r) in the token file sets both properties and the raw value is never used directly.

Cards, before and after

Same device, same account, same crop box. The tell in the left frame is the rhythm: the gap under Premium is 12, under Refer & Earn is 12, under Signed In is 24.

Before · v1.123.0
Profile cards before: uneven gaps between Premium, Refer and Earn, Signed In and Muscle Map
After · v1.123.1
Profile cards after: one corner radius and one gap throughout

The Refer & Earn subtitle differs between the two frames (“$0.00 earned” vs the pitch copy) — that is live account data changing between captures, not part of this change.

Rows you can actually hit

The settings list is the app’s primary navigation: eighteen destinations reached one-handed, often mid-workout. It was on 46pt rows. Apple’s 44pt is a minimum touch target, not a size to design to.

Before · 46pt row
Subscription
Refer & Earn
Edit Profile
row 46 · tile 29 · glyph 16 · label 16 · radius 12 circular
After · 56pt row
Subscription
Refer & Earn
Edit Profile
row 56 · tile 34 · glyph 19 · label 17 · radius 14 continuous

These two are CSS reproductions at true scale (1 CSS px per iOS pt) rather than screenshots, so the two states can sit at identical scale side by side. Every number in them is the real value from SettingsList.tsx. The shipped result is below.

Shipped · v1.123.1
The shipped settings list with 56pt rows

The badge that was missing

A notifications inbox shipped in v1.123.0 and went unnoticed, for a boring reason: nothing on the nav said anything was waiting, so it only ever got opened by someone already looking for it. NativeTabs.Trigger.Badge is a real UITabBarItem badge, so this is the system pill rather than a drawn dot.

Before · nothing waiting, nothing said
Tab bar with no badge on the Social tab
After · live count
Tab bar with a red 4 badge on the Social tab

It sums unread notifications and unread messages, because both live behind that tab and UIKit cannot draw two badges on one item. It went 3 → 4 live, with the app sitting on the Home tab and nobody touching it.

What that took, which was not the badge

notifications was never added to the supabase_realtime publication. The client subscribed, Postgres reported SUBSCRIBED, and no event ever arrived. Nothing client-side can detect that state — it is only visible as a tab bar that will not change.

The inbox it badges
The notifications inbox with four rows

Making it mechanical

A rule nobody can check drifts back within a release. Two checkers now hold this:

CheckCatches
check-card-surface.tsa card with a hand-typed corner; a list row under 44pt
check-badge-count.tsan empty red pill that can never be cleared; a four-digit count widening it; a failed read rendering “NaN” on the tab bar

Both take --self-test, which breaks one expectation on purpose and must exit 1. That flag earned itself here: the card checker’s first version passed its self-test by crashing — it imported the token file, which pulls in React Native and dies under node, and a crash also exits non-zero. It now parses the token out of the source instead, and fails with an actual violation report.

Screenshots are real captures from a signed-in simulator, iPhone 17 / iOS 26.2
Row comparison is a true-scale CSS reproduction; every value in it is from SettingsList.tsx
Corner shapes sampled from the superellipse equation at n = 2 and n = 5