Fitnit · v1.123.1 → v1.123.2
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.
Measured from the stylesheets, not estimated. The last column is the gap to the next card below.
| Surface | Radius | Curve | Padding | Gap below |
|---|---|---|---|---|
| Subscription | 16 | continuous | 16 | 12 |
| Refer & Earn | 14 | continuous | 16 | 12 |
| Account | 16 | continuous | 16 | 24 |
| Muscle Map | 16 | continuous | 16 | 24 |
| Achievements | 16 | continuous | 20 | 24 |
| Settings groups | 12 | circular | — | — |
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 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.
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.
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.
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.
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.
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.
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.
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.
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.
A rule nobody can check drifts back within a release. Two checkers now hold this:
| Check | Catches |
|---|---|
| check-card-surface.ts | a card with a hand-typed corner; a list row under 44pt |
| check-badge-count.ts | an 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