Skip to content

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 ​

  1. 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.
  2. The text is not the problem. Ink is #0D100E (L* 4.4, 19:1); textSecondary #4F5851 is 7.4:1 (APCA 86); textTertiary #626C64 5.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.
  3. 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 primarySubtle fill are the same mint, so the page reads as one mint sheet.
  4. The grammar spreads the ink thin. 49% of 1,155 Text nodes use a muted ink (textSecondary 40%, textTertiary 8.6%); cardMeta (14/500) is the most common role at 44% and is heavier than cardBody (14/400) — meta outweighs body and differs only by colour. 426 cardMeta nodes 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.
  5. Chips are content. GlyphFactRow in chip density puts a mint surface badge 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.
  6. 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).
  7. Web-only amplifiers, one of them large. (a) Tamagui text never rendered Pretendard on the web: fonts.ts names the family Pretendard-Regular, next/font registered the file only under its generated name bound to body, so every .is_Text fell through to the system font (Segoe UI / Malgun Gothic on Windows). Verified in the live page (document.fonts listed only pretendard); fixed 2026-10-06 with an @font-face under the emitted name plus a contract test. (b) The frame painted #F5F8F5 while 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: antialiased lightens strokes (macOS only); the hover dim (opacity .85) on a white card is nearly invisible and slightly paler.
  8. 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)HexWCAGAPCA LcNote
card on canvas#FFFFFF / #F8FBF91.04—ΔL* 1.7; Toss 3.9, Seed 3.9–5, Apple 4.4, M3 4–10
hairline on card#DFE7E1 / #FFFFFF1.26—L* 90.9; Toss 1.23, Seed 1.35 (not the outlier)
primarySubtle / surfaceTertiary on card#E5FAEA1.09—same mint for selection, chips, badges, progress track
badge fills on cardsuccess/warning/error1.09–1.14—7 fills, all one tier
text on card#0D100E19.1106fine
textSecondary on card#4F58517.3886fine, darker than references
textTertiary on card#626C645.4677fine
dark: error on card——40.5below 60
Metric (홈)nativeweb phoneweb 1440Toss당근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 levels3334–54–54–5
saturated accent areasmall (segmented pill, FAB)samesamelarge (C* 39)largegreen 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 ​

  1. No tonal step between card and ground (card.tsx tones; tamagui.ts background: colors.surface.secondary). Shadows are banned on primitives for a good reason (Android flattening, 2026-09), so the step has to be tonal.
  2. Flat in-card hierarchy: the meta/body weight inversion (text.tsx roles) and no promoted fact on the session card (session-card-v2.tsx); the fact block as a symmetric ledger (fact-row.tsx).
  3. Chips as content in pastel fills (glyph-fact-row.tsx chip density; club-card.tsx; status-badge-palette.ts).
  4. Brand green absent from content; one small accent per screen.
  5. Emoji fact glyphs beside muted strokes.
  6. Hairline-only separation of sections.
  7. 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.mjs pins the family name against fonts.ts and the canvas against classic-grass.theme.ts.
  • Proposals (not built — owner iterates on the board): a surface ladder (canvas → L* 95, a $surfaceStrong tier 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 and cardBody ≥ cardMeta in weight; inline glyph facts instead of chip-per-fact, badges reserved for status with one saturated tier; one solid $primary anchor per screen; stroke glyphs for facts; a check:color-contrast gate 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.

Markdown remains the source of truth. Run yarn docs:check before handoff.