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,$borderSubtlehairlines,$cardground — it overridesDetailShell'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-redesignboards 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$cardcanvas. 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.
| Screen | Shell / ground | Section title convention | Cards | Notes |
|---|---|---|---|---|
| session-detail | DetailShell · tinted $background | SectionBlock (title outside) | 6 (+2 nested) | 13 body sections at equal weight; 3 ad-hoc tinted XStack notice panels bypass Banner/Callout |
| directory-venue-detail | DetailShell, padding 0 · tinted | SectionBlock | 10 | full-bleed map hero with white-on-scrim title; local duplicate StatTile (card-in-card); heaviest screen |
| venue-detail (legacy) | DetailShell · tinted | SectionBlock | 4 | local DetailRow re-implements the fact row SpecList already provides |
| match-board | DetailShell · tinted | none | 3 | the least carded screen; reads as a control panel |
| club-detail | PlaceShell · $card | flat rows (home) | 0 on home, 2 on 기록 | the shipped flat precedent; ranking tab still carded |
| public-club-profile | DetailShell · tinted | in-card cardTitle | 4 + ONE-card hero | no SectionBlock at all; hero re-carded 2026-08-21s |
| post-detail | DetailShell overridden to $card | flat zones | 0 | the target language, shipped |
| join-review-detail | DetailShell · tinted | mixed: titleless cards + one bare SectionHeader | 3 | three title conventions in one screen |
| record-detail | DetailShell · tinted | mixed | 6 | $surface panel nested inside a Card |
| profile (tab root) | MainTabShell · tinted | in-card titles | ~7 + StatTile wrap="card" ×5–7 | card-in-card stat grid |
| public-profile | DetailShell · tinted | titleless "flowing card" (a named rule) | 2 | cleanest screen |
Ten cross-screen inconsistencies, each "same concept, different code":
- Three page grounds: tinted (
DetailShell),$card(PlaceShell), and a screen-local override back to$card(post-detail). - Three section-title conventions: outside the card (
SectionBlock), inside the card (cardTitleas first child), or none. - Three
StatTileimplementations (@twomore/ui,directory-venue-detail/parts/stat-tile.tsx, ranking row cells). - A zero-padding "list-card" idiom (
Card paddingHorizontal="$0"+ internalDivider) hand-rolled in record-detail, join-review-detail, and profile-menu-card. - Notice strips built three ways:
Banner,Callout, and rawXStack backgroundColor="$badgeWarningBg". - Label/value rows built three ways:
SpecList, a localDetailRow, inlineXStacks. - Card-per-tile (profile) vs one-block-many-tiles (venue basic info).
- The pinned bottom bar is
BottomCtaBandon six screens and a custom composer on post-detail. - Three hero constructions: absolute-overlay scrim (venue), ONE-card (club), plain centered Card (profile).
- Three vocabularies for "action with consequence":
SectionBlock tone="danger", an untitled CTA-band button, aCard 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(#FFFFFFlight).DetailShellandMainTabShellgainground="canvas";PlaceShellalready paints it. [Toss canvas#ffffff; the tint is reserved for section bands, never the page.] - Dark mode is not an inversion: ground near-black (
$carddark value stays), ink off-white, accents with contrast raised rather than luminance flattened. [Toss dark-mode statement; Linear ink#f7f8f8on#010102; OLED.] - The tinted
$background/$surfaceSecondarysurvives 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(SectionHeaderdisplaySm, optionalcardMetameta, 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 bySectionBlock); section→section$6on 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 (
SpecListtoday →FactRow): labelcardMeta $textTertiaryleft, valuecardBody700 right,$3vertical padding, hairline$borderSubtlebetween rows only. Never boxed. [Strava stat rows; HIG plain list.] - List row (
HairlineList— the club-home row grammar promoted to@twomore/ui):$4rail, 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(#EAF0EAon 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 —
displayMdname,cardMetaline, 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 tintedXStacks 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 staystinteduntil wave 5; decision 4).FactRow(orSpecListgains the row grammar),HairlineList,StatStrip(composesStatTile wrap="flat").SectionBlock: no change; aseam="flat"variant only if decision 1 lands as B.- Retire:
directory-venue-detail/parts/stat-tile.tsx,venue-detail-screen.tsx'sDetailRow, the three list-card sites (inconsistencies 3, 4, 6). - Lint: extend
@twomore/*rules soCardinside aground="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-variantat 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
#EAF0EAon#FFFFFFis 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
- Section→section seam on flat detail pages: keep
$4or move to$6. Recommended$6; without borders, whitespace is the only grouping device. - 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.
- Where tint survives. Recommended: recess semantics only (Well / Callout / Banner / empty state / shelf tile / inputs); the stat strip stays hairline-separated.
- Rollout: flip
DetailShellnow 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;PlaceShellremains for full-bleed-top surfaces only.screen-blueprint.md: per-wave pointers.