Detail pages — club-detail canon vs flat primitives (spacing audit)
Status: Accepted · Applied: 2026-09-05 · Owner ruling: "think of the spacing and alignment in the club detail page as the canonical configurations." Club detail wins on every conflict; the flat build spec (detail-pages-flat-build-spec-2026-09-05.md §1) was corrected from this audit.
All values read from live code on the 2026-09-05 tree. Tokens (packages/ui/src/tokens.ts): $1=4 $2=8 $3=12 $4=16 $5=20 $6=24 $7=32 $9=48. Text roles (packages/ui/src/text.tsx): pageTitle 24/32·700 · cardTitle 16/24·600 · cardBody 14/22·400 · cardMeta 13/18·500 · badge 12/18·600 · displaySm 20/28·700 · displayMd 28/36·700.
1. Club-detail canon (the numbers)
| Dimension | Value | Source |
|---|---|---|
| Page rail | $4 = 16 | club-identity-header.tsx, post-row.tsx, members-pane.tsx |
| Banner | fixed 132px visible (+ top inset), not aspect-driven | club-album-banner.tsx BANNER_VISIBLE_HEIGHT |
| Banner → identity | $3 = 12 | club-place-header.tsx |
| Identity → nav (the one big break) | $4 = 16 | club-place-header.tsx (section-rhythm law) |
| Chrome → content | 12px (CHROME_CONTENT_GAP) | use-collapsing-banner.ts |
| Pane header rhythm | uniform $3 = 12 | home/mission-header.tsx |
| Identity avatar | 56px, 3px ring, flush below the banner (overlap retired) | club-identity-header.tsx |
| Avatar ↔ title | $3 = 12 | club-identity-header.tsx |
| Club name role | pageTitle 24/32·700 | club-identity-header.tsx |
| Name → chips · chip row gap | $1 · $2 | club-identity-header.tsx |
| Section titles | cardTitle 16/24 (SectionHeader variant="list") | members-pane.tsx, ranking-tab.tsx |
| Row vertical padding | $3 = 12 (PostFeedRow, MemberRow, DuesRow, LeaderboardRow) | each row file |
| Row min height | 44 (TOUCH_TARGET.MIN); 36 dense (ClubScheduleRows) | member-row.tsx, club-schedule-rows.tsx |
| Row avatar tier | 40 MD (member/dues) · 32 SM (post author) · 28 XS (rank) | box-sizes.ts |
| Leading → text gap | $3 = 12 | member-row.tsx, dues-row.tsx |
| Title → sub inside a row | $0 (MemberRow, PostFeedRow) / $1 (DuesRow) — canon is split | row files |
| Hairline ownership | mixed: 4 of 6 rows full-bleed, MemberRow rail-inset | row files vs members-pane.tsx contentContainerStyle |
| List bottom padding | 48 ($9) | pane files |
| CTA band | $3 top · $4 sides · $4 bottom · 1px top hairline | bottom-cta-band.tsx (shared byte-for-byte with flat pages) |
| Underline nav | 48px cell + 1px hairline; label 14px 800/600 | pill-nav.tsx |
2. The diff (before this audit was applied)
| Dimension | Canon | Flat primitive / page (before) | Resolution (applied 2026-09-05) |
|---|---|---|---|
| Section → section seam | $4 (one big break) | FlatColumn gap $6 | FLAT_COLUMN_DEFAULT_GAP → $4 |
| Chrome → content | 12 | FlatColumn top $4 | FLAT_COLUMN_DEFAULT_TOP → $3 |
| Hero title role | pageTitle 24/32 | displayMd 28/36 (MediaHero, TypeHero) | both heroes → pageTitle |
| Hero avatar | 56px flush, overlap retired | MediaHero avatar overlaps media by −34px | overlap removed; avatar sits flush in the identity row (club-identity-header anatomy) |
| Media band height | fixed 132px | MediaHero 16:9 (≈219px at 390 wide) | default → fixed 132px band; aspect becomes the explicit opt-in |
| TypeHero top | banner→identity 12 | paddingTop $2 | → $3 |
| Row avatar tier | 40 MD on member rows | session roster rows SM 32 | roster rows → MD |
| Hero avatar tier | 56 | profile XL 80, join-review LG 44, club profile 56 | AVATAR_SIZE.HERO = 56 everywhere |
| Section title role | cardTitle 16/24 | cardTitle 16/24 (spec text said otherwise) | no code change; spec §1.4 corrected |
| Row padding / min height | $3 / 44 (36 dense) | same | no delta |
| Leading → text gap | $3 | same | no delta |
| Title → sub gap | $0 or $1 (canon split) | $1 | keep $1 (DuesRow precedent); spec's $0.5 non-token removed |
| Hairline ownership | inconsistent on canon | uniformly rail-inset | flat pages keep rail-inset (the more disciplined reading); canon rows are a follow-up |
| Stat cells | only precedent is bordered AdminSnapshotStrip (admin surface) | hairline-flush StatStrip | keep StatStrip (canvas law: tint/borders only as recess); admin strip is not a place surface |
| Doubled seams | none | match-board round row and record scope row: paddingBottom $3 + hairline, then column gap | kept: the padding is the hairline's own inner space, not a second seam; with $4 gap the stack is 28px |
| CTA band | shared primitive | identical | no delta |
3. Group-spread law (owner, same day)
"If we are using tabs or buttons or action buttons or any other group of components, we need them evenly spread out to fill the width (minus the canonical spacing)." Recorded in docs/canon/components.md and the build spec §1.5 item 7. Applied to ActionIconRow (was a centered cluster of 52px items with $5 gap; now every item is flex={1} across the rail). Scrollable pill rails (match-board rounds, record scope chips) and badge previews (achievement tiles) are not peer groups and stay left-anchored.
4. Follow-ups (not blocking)
- Canon's own hairline ownership is split (full-bleed vs rail-inset); a later pass should pick one for the club-detail rows.
StatStripcellminHeight56 is an off-grid literal; keep until a device pass says otherwise.