Visual tone audit — "a pale overtone all throughout"
Status: Active Last reviewed: 2026-10-06 Trigger: owner, 2026-10-06, on the desktop web renders: "the app looks poorly designed with a pale overtone all throughout. Is our mobile app suffering from the same issue? What is our visual problem?"
Three evidence lanes (tokens and contrast; rendered pixels on web, native and references; component grammar), one first-hand verification of the web font, and one fix shipped the same day. Proposals that change the look are on the visual-tone board for the owner's iteration — this document only states what is true.
Answer in short
- Yes, the phone app has the same tone. Sampled pixels are identical on web and native: canvas
#F8FBF9, card#FFFFFF, hairline#DFE7E1, mint fills#E5FAEA. Near-white share on 홈 is 90.8% native vs 85.2% web-phone vs 93.0% web-1440. The palette is the cause; the web amplifies it. - The text is not the problem. Ink is
#0D100E(L* 4.4, 19:1);textSecondary#4F5851is 7.4:1 (APCA 86);textTertiary#626C645.5:1 (APCA 77). Those are darker than Toss (grey700 L* 37, grey500 3.0:1) and Seed (gray-700 3.4:1). No light-theme pair fails WCAG 4.5 / APCA 60. - The surfaces are the problem. Every light surface, tint and canvas sits inside L* 96.1–100 (a 3.9 L* span). Card on canvas is 1.04:1 (ΔL* 1.7) with no shadow; the only edge is a 1px hairline at 1.26:1. Toss separates card from canvas by 3.9 L*, Seed 3.9–5, Apple 4.4, Material 3 by 4–10 across five tiers. Badge and tint fills sit at 1.09–1.18:1 — 141 Badge sites and every
primarySubtlefill are the same mint, so the page reads as one mint sheet. - The grammar spreads the ink thin. 49% of 1,155
Textnodes use a muted ink (textSecondary40%,textTertiary8.6%);cardMeta(14/500) is the most common role at 44% and is heavier thancardBody(14/400) — meta outweighs body and differs only by colour. 426cardMetanodes re-set a colour redundantly, 59 force tertiary. A 홈 card has one strong element (the 16/600 title) over four equal 14/500 lines; a session's fact block is a symmetric ledger of eight rows with nothing promoted. Brand green appears 253 times as thin ink and almost never as a solid anchor. - Chips are content.
GlyphFactRowin chip density puts a mintsurfacebadge on every fact (a club discover card carries about five); status badges (success#EBF8F1, warning#FFF4D6, error#FDECEC) are all 1.09–1.14:1 on white. Chips read as tags and add a grey-mint layer without adding hierarchy. - Glyphs clash. Facts use 12px colour emoji (⏰ 🎾 🎯) beside lucide strokes at
$iconMuted; the emoji are the most saturated things on screen and depend on the platform font (Noto on Android, Segoe UI Emoji on Windows web, tofu in headless capture). - Web-only amplifiers, one of them large. (a) Tamagui text never rendered Pretendard on the web:
fonts.tsnames the familyPretendard-Regular, next/font registered the file only under its generated name bound tobody, so every.is_Textfell through to the system font (Segoe UI / Malgun Gothic on Windows). Verified in the live page (document.fontslisted onlypretendard); fixed 2026-10-06 with an@font-faceunder the emitted name plus a contract test. (b) The frame painted#F5F8F5while the theme canvas is#F8FBF9— two near-whites; unified. (c) 1px hairlines at DPR 1 vs ~3 device px on the phone. (d) At 1440 the phone column floats on 91–97% near-white with 0.1–0.6% ink — about five times emptier than phone width. (e)-webkit-font-smoothing: antialiasedlightens strokes (macOS only); the hover dim (opacity .85) on a white card is nearly invisible and slightly paler. - Dark mode is not pale (4-step ladder L* 3.6 / 12.2 / 18.1 / 26.7, 93% ink on 홈). It has its own, smaller issue: success/warning/error on card and several badge pairs sit at APCA 40–59.
Measurements
| Pair (light) | Hex | WCAG | APCA Lc | Note |
|---|---|---|---|---|
| card on canvas | #FFFFFF / #F8FBF9 | 1.04 | — | ΔL* 1.7; Toss 3.9, Seed 3.9–5, Apple 4.4, M3 4–10 |
| hairline on card | #DFE7E1 / #FFFFFF | 1.26 | — | L* 90.9; Toss 1.23, Seed 1.35 (not the outlier) |
| primarySubtle / surfaceTertiary on card | #E5FAEA | 1.09 | — | same mint for selection, chips, badges, progress track |
| badge fills on card | success/warning/error | 1.09–1.14 | — | 7 fills, all one tier |
| text on card | #0D100E | 19.1 | 106 | fine |
| textSecondary on card | #4F5851 | 7.38 | 86 | fine, darker than references |
| textTertiary on card | #626C64 | 5.46 | 77 | fine |
| dark: error on card | — | — | 40.5 | below 60 |
| Metric (홈) | native | web phone | web 1440 | Toss | 당근 | Band |
|---|---|---|---|---|---|---|
| near-white (L* > 92) | 90.8% | 85.2% | 93.0% | 94% | 70–86% | 72–86% |
| ink (L* < 35) | 1.5% | 2.9% | 0.1–0.6% | 1.0–1.5% | 3.6–6.1% | 6–8.5% |
| distinct surface levels | 3 | 3 | 3 | 4–5 | 4–5 | 4–5 |
| saturated accent area | small (segmented pill, FAB) | same | same | large (C* 39) | large | green fills |
Usage census (packages/ui, features, app presentation; tests excluded): textSecondary 518 literals, primary 253, textTertiary 156, text 138 (under-counted: it is the default), borderSubtle 105, primarySubtle 62. HAIRLINE 56 uses in 23 files. Badge surface 5, neutral 39, status variants the rest of 141.
Token drift: packages/ui/src/tokens.ts primitives (borderLight #EAF0EA, textSecondary #455D49) differ from the live theme (#DFE7E1, #4F5851); only two pairs are pinned by test; check:color-contrast gates no surface-separation pair, so the 1.04:1 card edge never tripped anything.
What is wrong, in order of visual impact
- No tonal step between card and ground (
card.tsxtones;tamagui.tsbackground: colors.surface.secondary). Shadows are banned on primitives for a good reason (Android flattening, 2026-09), so the step has to be tonal. - Flat in-card hierarchy: the meta/body weight inversion (
text.tsxroles) and no promoted fact on the session card (session-card-v2.tsx); the fact block as a symmetric ledger (fact-row.tsx). - Chips as content in pastel fills (
glyph-fact-row.tsxchip density;club-card.tsx;status-badge-palette.ts). - Brand green absent from content; one small accent per screen.
- Emoji fact glyphs beside muted strokes.
- Hairline-only separation of sections.
- Web: the font fallback (fixed), the canvas drift (fixed), 1px hairlines, whitespace at 1440, antialiasing, hover dim.
Items 1–6 are shared with the phone app; 7 is web-only.
What was done today
apps/web/app/globals.css:@font-face 'Pretendard-Regular'(variable file, weight 45–920) so Tamagui text renders Pretendard;--tm-bg= theme canvas.web-frame-contract.test.mjspins the family name againstfonts.tsand the canvas againstclassic-grass.theme.ts.- Proposals (not built — owner iterates on the board): a surface ladder (canvas → L* 95, a
$surfaceStrongtier at L* 93, mint fills at L* 92, hairline at ~1.5:1 — card/canvas 1.04 → 1.17:1, ΔL* 1.7 → 4.9); one promoted fact per card andcardBody≥cardMetain weight; inline glyph facts instead of chip-per-fact, badges reserved for status with one saturated tier; one solid$primaryanchor per screen; stroke glyphs for facts; acheck:color-contrastgate for canvas/card ΔL* ≥ 3.5 and fill/card ≥ 1.2 that fails on today's values (a canary).
Sources
Lane reports (session scratch, 2026-10-06): token/contrast lane (APCA via scripts/check-color-contrast.mjs's function; Toss TDS and Seed hexes from tossmini-docs.toss.im and seed-design.io; M3 surface-container tiers from m3.material.io; Apple label alphas from the HIG), render lane (scripts/screenshot-web.mjs captures at phone/1440/dark; native frames from the 2026-09-30 emulator session; App Store frames for Toss, 당근, Band, Strava via the iTunes lookup API), component lane (file:line per cause). Band's palette was not verifiable; Strava values are from a third-party write-up.