Skip to content

Detail pages — flat language (audit · spec · migration plan) — 2026-09-05

Status: Draft · Note (2026-09-06): the row grammar below (HairlineList, bottom-hairline rows) was superseded by the Divider law in docs/canon/components.md — rows carry no divider, RowList replaced HairlineList, FlatColumn draws the section divider · owner brief: "we are migrating to a new design language for detail pages, which do not rely on cards and render flat components against a non-grey background." Nothing here is built; the decision board is /wireframes/detail-flat-language (docs/twomore_design_system/wireframes/detail-flat-language.jsx).

This document does three things: (1) records what the eleven detail screens actually render today, read from code; (2) fixes the flat grammar in concrete tokens, grounded in the canon laws that already exist and in external references; (3) lays out a screen-by-screen migration that never leaves a screen half-flattened. The four decisions it cannot make alone are in §6.


1. Where the migration already stands

  • The law exists. Canvas law (owner-directed 2026-09-02, components.md §116): one flat non-grey ground ($card), and components flatten WITH it. The 2026-08-22 pilot flattened only the ground and was reverted the next day because card-boxed components "read as floating objects". A half-flattened screen is the one state the law forbids.
  • Two precedents ship the target language. post-detail-screen.tsx (flat zones, $borderSubtle hairlines, $card ground — it overrides DetailShell's tinted ground locally) and the club home's rows (club-schedule-rows.tsx, post-row.tsx: bottom-hairline-only blocks on the bare canvas).
  • The six *-detail-redesign boards predate the law. They were drawn 2026-08-30/31 into the club-detail place-surface format: ONE-card heroes and card-boxed sections on a $card canvas. Their role-density findings (참가자 / 호스트 / 게스트 slots, density-follows-role) still stand; their chrome is superseded by this board.
  • A hero de-carding was tried and reverted (club-detail-hero.tsx:239-243, 2026-08-21s: a chrome hero "just blends against the background"). That experiment ran on the tinted ground with the hero's chrome kept; it is not evidence against a full-bleed photo with the title beneath it on a white canvas (§3.4).

2. Inventory — what the eleven screens render today

Read from the live components on 2026-09-05 (agent inventory, file:line evidence in the session record). Card counts are Card tone="default" instances on the primary path.

ScreenShell / groundSection title conventionCardsNotes
session-detailDetailShell · tinted $backgroundSectionBlock (title outside)6 (+2 nested)13 body sections at equal weight; 3 ad-hoc tinted XStack notice panels bypass Banner/Callout
directory-venue-detailDetailShell, padding 0 · tintedSectionBlock10full-bleed map hero with white-on-scrim title; local duplicate StatTile (card-in-card); heaviest screen
venue-detail (legacy)DetailShell · tintedSectionBlock4local DetailRow re-implements the fact row SpecList already provides
match-boardDetailShell · tintednone3the least carded screen; reads as a control panel
club-detailPlaceShell · $cardflat rows (home)0 on home, 2 on 기록the shipped flat precedent; ranking tab still carded
public-club-profileDetailShell · tintedin-card cardTitle4 + ONE-card herono SectionBlock at all; hero re-carded 2026-08-21s
post-detailDetailShell overridden to $cardflat zones0the target language, shipped
join-review-detailDetailShell · tintedmixed: titleless cards + one bare SectionHeader3three title conventions in one screen
record-detailDetailShell · tintedmixed6$surface panel nested inside a Card
profile (tab root)MainTabShell · tintedin-card titles~7 + StatTile wrap="card" ×5–7card-in-card stat grid
public-profileDetailShell · tintedtitleless "flowing card" (a named rule)2cleanest screen

Ten cross-screen inconsistencies, each "same concept, different code":

  1. Three page grounds: tinted (DetailShell), $card (PlaceShell), and a screen-local override back to $card (post-detail).
  2. Three section-title conventions: outside the card (SectionBlock), inside the card (cardTitle as first child), or none.
  3. Three StatTile implementations (@twomore/ui, directory-venue-detail/parts/stat-tile.tsx, ranking row cells).
  4. A zero-padding "list-card" idiom (Card paddingHorizontal="$0" + internal Divider) hand-rolled in record-detail, join-review-detail, and profile-menu-card.
  5. Notice strips built three ways: Banner, Callout, and raw XStack backgroundColor="$badgeWarningBg".
  6. Label/value rows built three ways: SpecList, a local DetailRow, inline XStacks.
  7. Card-per-tile (profile) vs one-block-many-tiles (venue basic info).
  8. The pinned bottom bar is BottomCtaBand on six screens and a custom composer on post-detail.
  9. Three hero constructions: absolute-overlay scrim (venue), ONE-card (club), plain centered Card (profile).
  10. Three vocabularies for "action with consequence": SectionBlock tone="danger", an untitled CTA-band button, a Card size="lg" with a check icon.

3. The grammar

Tokens are the existing ones (packages/ui/src/tokens.ts, theme-hex.ts); no new colors, no new type roles. Reference values in brackets are the external grounding (§5).

3.1 Ground

  • Page ground = $card (#FFFFFF light). DetailShell and MainTabShell gain ground="canvas"; PlaceShell already paints it. [Toss canvas #ffffff; the tint is reserved for section bands, never the page.]
  • Dark mode is not an inversion: ground near-black ($card dark value stays), ink off-white, accents with contrast raised rather than luminance flattened. [Toss dark-mode statement; Linear ink #f7f8f8 on #010102; OLED.]
  • The tinted $background / $surfaceSecondary survives only as a recess: Well, Callout, Banner, empty states, shelf tiles, input fields. It never spans the page and never carries a border.

3.2 Section

  • One convention: title OUTSIDE content via SectionBlock (SectionHeader displaySm, optional cardMeta meta, optional right action). In-card titles and titleless cards are retired.
  • Content sits bare on the canvas. No box, no radius, no border around a section.
  • Seams (section-rhythm law, one owner per seam): title→content $2 (unchanged, owned by SectionBlock); section→section $6 on flat detail pages (decision 1; today $4, calibrated when card borders also separated sections) [M3 "24px between sections"; proximity scale 8 inside / 24 between / 48 major]; chrome→content $4; row→row = hairline, no gap.
  • A section that has nothing renders null (existing corollary).

3.3 Rows

  • Fact row (SpecList today → FactRow): label cardMeta $textTertiary left, value cardBody 700 right, $3 vertical padding, hairline $borderSubtle between rows only. Never boxed. [Strava stat rows; HIG plain list.]
  • List row (HairlineList — the club-home row grammar promoted to @twomore/ui): $4 rail, bottom-hairline-only, last row unbordered; replaces the zero-padding list-card idiom (inconsistency 4).
  • Stat strip (StatStrip = StatTile wrap="flat" ×2–4 with vertical hairlines): replaces every carded tile grid (inconsistency 3, 7).
  • Hairline = 1px $borderSubtle (#EAF0EA on white ≈ 4% luminance step). [M3 divider 1dp at 12% opacity is the ceiling; ours is quieter.] Full-bleed hairlines are avoided between sections (a "false floor"); inside a list they are inset to the rail.

3.4 Hero

  • Media hero (session with a venue map, venue, club): the image bleeds edge to edge with no frame or radius; title (displayMd), meta, and badges sit BENEATH it on the canvas. No scrim text. [Apple Music artist, Strava activity, 당근 모임.] Status badge may overlay the image's top-left corner only.
  • Typographic hero (profile, record, applicant, tournament): the title is the hero — displayMd name, cardMeta line, badge row, optional avatar right. No card.
  • The seam-pair from the club hero (flag bottom-left, faces bottom-right) survives as an overlap on the media hero's bottom edge where it exists today.

3.5 Primary action and signals

  • Exactly one contextual BottomCtaBand (already flat: $card, top hairline). Post-detail's composer adopts the band's padding and hairline.
  • Signals keep their tint because tint now means "recessed / attention": Callout (info), Banner (action), Well (host notice, empty state). Raw tinted XStacks are replaced by these (inconsistency 5).
  • Destructive actions: SectionBlock tone="danger" everywhere a danger zone exists (inconsistency 10).

3.6 What may still be a box

Only a genuine lifted figure: a shelf tile inside a place-surface preview rail (recessed Well, not a bordered Card), a computed-result SummaryCard ($primarySubtle fill, no border), and the map thumbnail inside a session card in a LIST. On a detail page's primary vertical flow: nothing. [NN/g cards vs lists; M3 "prefer a list for homogeneous scannable content".]

4. Migration plan

Primitives first, then one screen per wave, each wave flattening ground AND components together (the law's "both halves ship together").

Wave P — primitives (@twomore/ui)

  • DetailShell / MainTabShell: ground?: 'tinted' | 'canvas' (default stays tinted until wave 5; decision 4).
  • FactRow (or SpecList gains the row grammar), HairlineList, StatStrip (composes StatTile wrap="flat").
  • SectionBlock: no change; a seam="flat" variant only if decision 1 lands as B.
  • Retire: directory-venue-detail/parts/stat-tile.tsx, venue-detail-screen.tsx's DetailRow, the three list-card sites (inconsistencies 3, 4, 6).
  • Lint: extend @twomore/* rules so Card inside a ground="canvas" DetailShell is flagged unless it is one of §3.6's figures (prevention, per the fix-with-prevention rule).

Wave 1 — session detail (flagship). Media hero absorbs 장소 + 날씨 + status; stat strip carries the viewer's relation (확정 인원 · 내 참가비 · 참석 마감); 공지 → Well; 참여 / 경기 / 정보 as flat sections; danger zone unchanged. Cards 6 → 0. Role branches from the existing redesign board apply unchanged.

Wave 2 — venue detail (directory-venue-detail first, then the legacy venue-detail-screen). Trust → hero badge; 코트 · 종류 · 요금 → stat strip; the three ActionTiles → a 4-up circular icon row; hours / fees / amenities / links as flat sections; the correction entry points stay where they are. Cards 10 → 0.

Wave 3 — profile + public profile. Typographic hero; stat strip; 업적 / 출석 / 친구 flat sections with recessed tiles; menu → HairlineList. Cards ~7 → 0. Public profile's "flowing card" rule is retired in favor of §3.2.

Wave 4 — record detail, join-review detail, match board, public club profile. Record: outcome → typographic hero + strip, match list → HairlineList. Join review: identity typographic hero, evidence → fact rows, message → flat section. Match board: hero card → typographic hero, all-done banner → Well. Public club profile: hero stays the club media hero (§3.4) with the title beneath; four cards → flat sections.

Wave 5 — flip the default. DetailShell/MainTabShell default becomes canvas; the ground prop is removed; place-shell.tsx's figure-ground comment and detail-shell.tsx's 2026-08-22c comment are rewritten; the six *-detail-redesign boards are regrounded or retired; the canon laws are edited (§7).

Each wave: wireframe iteration with the owner on the board first, then characterization test → build → yarn check → screen-blueprint pointer → preview OTA → device QA (the flat hero and hairline contrast are visual, so rung 6 is owed per wave).

5. References used (values, not principles)

  • Toss TDS: typography 30/26/22/20/17/15/13 with 1.5× line height; canvas #ffffff, section band #f2f4f6, border #e5e8eb; "one message per screen"; horizontal scroll avoided. (tossmini-docs.toss.im; oh-my-design.kr reverse-engineering, treated as approximate.)
  • Material 3: divider 1dp outline-variant at 12%; "use dividers only when whitespace cannot separate"; prefer lists over cards for homogeneous content; 24px between sections.
  • Apple HIG: plain vs grouped vs inset-grouped lists; inset-grouped recommended only in regular width.
  • NN/g: cards are less scannable than lists; whitespace before borders; full-width borders can read as false floors.
  • WCAG 2.2: text ≥ 4.5:1, non-text UI ≥ 3:1 — the hairline #EAF0EA on #FFFFFF is decorative and below 3:1, so it never carries meaning alone (the section title and spacing do).
  • Linear DESIGN.md: eyebrow 13/500/+0.4px, hairline as a 5–8% step, 96px major section token — evidence that whitespace can mark a major break with no divider.
  • 당근 모임 / Strava activity / Apple Music artist: full-bleed media, identity beneath, no card frame.

6. Decisions for the owner

  1. Section→section seam on flat detail pages: keep $4 or move to $6. Recommended $6; without borders, whitespace is the only grouping device.
  2. Media hero title: on a scrim over the image, or beneath the image on the canvas. Recommended beneath; it removes the contrast gamble and matches every reference.
  3. Where tint survives. Recommended: recess semantics only (Well / Callout / Banner / empty state / shelf tile / inputs); the stat strip stays hairline-separated.
  4. Rollout: flip DetailShell now or opt in per screen. Recommended opt-in, so no screen is ever half-flattened; flip after wave 4.

7. Canon edits once decided

  • components.md §113: add "flat detail pages: section→section $6".
  • components.md §116: add the hero rule (title beneath media, never on a scrim), the "what may still be a box" list, and retire the "flowing card" rule cited by public-profile.
  • components.md §120: DetailShell ground="canvas" becomes the detail-screen default; PlaceShell remains for full-bleed-top surfaces only.
  • screen-blueprint.md: per-wave pointers.

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