DetailShellground: 'tinted' | 'canvas' · footer?: a pinned action band (BottomCtaBand) rendered outside the scroll body
FlatColumntop=16(hero→first section) · gap=24 around a full-bleed 1px section divider · lead?: a hero-zone block (StatStrip, a bare fact) with one divider below it, none above · divider={false} opts out
SectionBlockvariant='flat' → navTitle role, 17px/700, on the 16 rail, no min-height, 8px title→content gap · meta · right · link={{label,onPress}}
FactRow / FactListlabel · value · tone · trailing? · secondaryValue? (a second, muted line under value) · edge?: 'first'|'last'|'only' drops that row's own top/bottom padding at a section boundary — never a hairline · rhythm: 'compact' (8px/side) while every row is one line, 'roomy' (12px/side) for the WHOLE list as soon as any row carries a secondaryValue (FACT-ROW-RHYTHM, owner 2026-09-27, `resolveFactListRhythm`) — never mixed within one list · disclosure?: {expanded, onToggle, accessibilityLabel} draws a shortened list's lone chevron as the list's OWN last row, on the SAME rhythm — 24px from the row above and 24px to the divider (CHEVRON-ROW-RHYTHM, owner 2026-09-28, `resolveFactListDisclosureInset`), never a free-floating icon underneath
RowListdraws no separators between rows by default; `dividers` opts a hairline back in only when each row is itself a multi-component block (e.g. post-detail's comment rows)
ListRowleading · title · sub · trailing · onPress · chevron (shown by default when onPress && !trailing) · dense · edge
CellRow / StatStripequal flex:1 columns, content centered, 8px gap + 8px padding inside each cell, optional centered divider; StatStrip is a 2–4 cell item renderer over CellRow
BottomCtaBand16px top/sides/bottom padding (one value on every edge), 1px top hairline, $card background
MediaHeromedia{kind, uri} — the ONE hero band for a map, a photo or an album alike, 2:1 of the screen width by default (195 at a 390pt phone, capped 280 — `resolveHeroBandHeight`, MAP-BAND-2:1, owner 2026-09-27; a map's own fixed 132px band is retired) · title · badges: ONE row directly under the title, before meta, fixed order state → verification → category · meta: string | string[], one fact per line · avatar flush left/top
TypeHeroeyebrow (spans the rail above the identity row) · title · badges (same one-row grammar as MediaHero) · meta: string | string[] · leading: a top-aligned 56px avatar/logo · right: trailing count/ring only
Meta text grammarprimary meta = cardBody/$textSecondary · secondary/sub/eyebrow = cardMeta/$textTertiary (eyebrow at weight 700) · caption = cardBody/$textSecondary, smaller — one tier per slot
ActionIconRowitems: {icon,label,onPress}[3–5] — equal-ratio flex:1 cells, no resting fill; press settles + dims the cell (no tint band, owner 2026-09-28)
Welltone: neutral | primary | warning · onPress + required accessibilityLabel renders as Pressable variant='card' affordance='chevron' — the one surviving grey
Pressableaffordance?: 'chevron'|'button'|'link'|'surface'|'none' (lint-enforced) · press feedback stays inside the thing pressed — settle + dim only, no state-layer tint band, ever (retired 2026-09-28)
RemoteImageuri · contentFit · cachePolicy · transition · onError · fallback — the one expo-image wrapper
SEAMpackages/ui/src/seams.ts — element 4 · item 8 · block/chrome 12 · rail/hero/band 16 · section 24 · tail 48. The one source every primitive above reads its seam from.
lint@twomore/no-card-on-canvas · @twomore/one-flat-column · @twomore/pressable-needs-affordance