Lane I — tags vs chips, top-down row hierarchy, and the attention inventory of the session and club cards (2026-10-09)
Status: Reference · Date: 2026-10-09 · Evidence appendix for list-surfaces-spec-2026-10-09.md. Frame, crop and script paths refer to the session scratch directory (not committed); App Store frames are re-fetchable from the store URLs in the tables.
Status: Evidence + spec for the owner rulings of 2026-10-09 (a) one passive label primitive, (b) top-down ink, (c) a compact session card without the bottom row, (d) icons are action targets. Read-only lane: nothing under /home/sungkyu08/Dev/twomore-v2 was changed. Paths are relative to that repo unless stated. Frames are under bench/{crops,b_crops,crops_c}; fetched raw sources are under bench/lane_i/.
Method notes: m3.material.io, fluent2.microsoft.design, polaris docs and Apple's HIG pages are script-rendered, so I read each system's shipped tokens or source on GitHub, Apple's developer.apple.com/tutorials/data/design/human-interface-guidelines/<page>.json mirror, and Seed's rootage JSON plus its llms text exports. Values marked [relay] came through a search summary or my own knowledge and were not read from a primary page. Peer-frame readings are mine, from marketing/store frames (±2 pt, see section 7).
0. The six findings that matter
- Our label vocabulary is split in two shapes and the same recipes are worn by labels and controls.
Badgeis a rounded rectangle ($28px /$312px,packages/ui/src/badge.tsx:88-96), but at least nine other sites hand-draw pills. Recipe A ($surfaceSecondary+borderRadius="$7"full pill +$textSecondary) is worn by three passive labels (club-photo-seam.tsx:228-235,club-identity-header.tsx:84-98,public-club-profile-screen.tsx:568-577) and by two pressable things (ActionPill,packages/ui/src/action-pill.tsx:27,51-71; the session chip inpost-row.tsx:481-500): a tappable pill and three labels are indistinguishable. Recipe B ($primarySubtle+$primary+ weight 600/700) is the selected-chip recipe (selection-chip.tsx:122-123, 58 consumer files) and is also worn by the display-onlyVenueLink(venue-link.tsx:88-115),MarkerPill(marker-pill.tsx:14, 4 files) and the tier label (tier-badge-utils.tsx:46-56, 13 files via two wrappers, at 8px radius). - The market splits passive labels from controls almost everywhere, with named warnings. Seed Badge is "정적인 정보 전달" (static information) and not to be made clickable (https://seed-design.io/llms/components/badge.txt); GOV.UK "Do not make a tag interactive" (https://design-system.service.gov.uk/components/tag/); Carbon "Do not use tags as links" (https://carbondesignsystem.com/components/tag/usage/); Polaris keeps non-interactive
chipapart fromclickable-chip(https://shopify.dev/docs/api/app-home/polaris-web-components/actions/clickable-chip). Only Carbon, Fluent Tag and Atlassian Tag fold selectable/dismissible modes into one Tag; Carbon explicitly warns against multi-function tags, Fluent splits off an "Interaction tag", and Atlassian bars status from Tag. So one passiveTag+ one controlChoiceis the position the systems support; "chips and badges are the same component" is supported only for the passive half. - Passive-label systems converge on two emphasis steps, and ours has twelve variants. Seed weak/solid (+ outline), TDS fill/weak, Polaris base/strong, Atlassian subtle/bold, Fluent tint/filled (+ outline, ghost), GOV.UK tint only.
Badgehas 12 variants but literal call sites use four (neutral39,warning11,surface5,live3);primarySubtleandoutlinehave no consumer at all. About 19 of the 39variant="neutral"call sites print a fact (round-rules strip x6, amenities, payment methods, duration, distance, D-day, format), not a status, which is the T3 finding in code. - Top-down is what the session peers do; the board's T2 phone is the exception. In the 11 session/listing rows I read, the first body line is the heaviest in 8 (Smash, TennisFriends, Munto, Band, Greenlight, Somoim, Spond, Heja); the three exceptions are commerce rows that promote a price (당근, Frip) and Flap (time lighter above a bold venue). Lane B counted whatever leads as "bold 15-20 pt, everything else 11-13 pt grey" (
lane-b-category.md:70). The T2 "after" card puts the 18/700 time on line 2 under a 16/600 venue (docs/twomore_design_system/wireframes/visual-tone.jsx:654-705 (time at :705)), which is the middle-line promotion the owner rejected. Seed (16 → 13) and M3 (16 → 14) both step down in size between headline and supporting text; ourListRowis 14/600 over 14/500, a zero size step. - Lead the row with the facet that is the list's sort key. Of the four day-grouped or date-filtered peers, three lead the text with time/date (Flap, Greenlight, Band) and one carries the date in a left date block (Spond); all four ungrouped feeds lead with the title (Smash, Munto, TennisFriends, Frip). Our lists are grouped by day (
SessionDayHeader, D10), so the first line is the time at 16/600, the venue second at 14/500, and the venue photo carries the "where" identity. NN/g: users see about two words of each list item and the left edge is scanned (https://www.nngroup.com/articles/first-2-words-a-signal-for-scanning/); venue names repeat down a day, times never do. - The session card carries 12-14 visual objects against a peer median of about 7; the compact row is 5. Current card (browse viewer): tile, venue, bookmark, 3 emoji + 3 texts, fee, status word, bar, count = 13 (+1 corner chip on the map tile). The compact row: photo, time, venue, one meta line, one state element (count text, or one Tag only when something outranks it) = 5; bookmark moves to the detail header (it already exists there). Height falls from about 130 px to 96-98 px (-25%). Zero inline glyphs (peers draw 0-3, median 2), zero bars (0 of 11 peers draw one), at most one accent.
Also found: the session-card-v2.tsx header says the fee left the card and the lead chip sits in the title seat (:45-47, :53, :61) but the code still renders both in the closing line (:473-485); and canon/components.md:29 (fact glyphs only for boolean facts) conflicts with canon/components.md:57 (emoji may lead any fact). See section 7.
1. Passive labels, controls and counts across design systems
1.1 Table
Column key: Passive = read-only label (name · shape · radius · height · font). Control = selectable/actionable (name · shape · selected state). Input = dismissible. Count = numeric/dot badge.
| System | Passive label | Interactive selection | Dismissible input | Count badge | Rule text (quoted) |
|---|---|---|---|---|---|
Material 3 (tokens: material-web v0_192; Android docs) | None named. Badge is count/dot only | Filter chip: rect, corner-small 8px, 32px, 1px outline, label-large 14/500 (0.875rem, weight medium), icon 18; selected = secondary-container (tonal) + check. Assist, Suggestion chips: same 32px/8px | Input chip (32px, avatar 24px, trailing remove) | Badge: small 6px dot, large 16px tall, corner-full, label-small 11/500, colour error | Chip docs (paraphrase): chips appear dynamically as a group of interactive elements, buttons are for consistent calls to action; badges show notifications, counts or status on navigation items and icons. |
| Apple HIG (JSON mirror) | None. No chip, no tag, no lozenge | No chip. Segmented control: one choice affecting "an object, state, or view", "no more than about five segments on iPhone", nouns; pop-up button shows the current selection | macOS Token field (input token) | Tab badge: "a red oval containing white text and either a number or an exclamation point" | "Reserve badges for critical information so you don't dilute their impact and meaning." "Prefer displaying text in a list or table." "Keep item text succinct." |
| 당근 Seed (rootage JSON + llms) | Badge: rect, radius 6px large / 4px medium, 24 / 20px tall, 12 / 11px, weak weight 500, solid/outline 700, 6 tones; Tag Group: inline icon+text items with separators, 12/13/14px, tones neutral-subtle/neutral/brand, regular or bold; Manner Temp Badge: pill, 20px min, 11/700, 10 level colours | Chip: radius.full, 32/36/40px, label 14/500; solid selected = bg.neutral-solid (ink) + fg.on-neutral-solid; outlineWeak (recommended for selection) selected = neutral-weak fill + neutral-contrast stroke. Segmented control item: 34px, full radius, 16/700, selected label fg.neutral | Chip "Input" = value + Remove button in the suffix; Filter Bar shows Clear | Notification Badge: dot (small) or number (large, ≤4 chars, never 0, top-right, disappears once viewed) | Badge: "정적인 정보 전달" (static information); paraphrase: keep to one or two badges per object, never clickable (use Chip, Toggle Button or Action Button). Tag Group: "카드형 콘텐츠에서 메타데이터 표시" (card metadata); 6-10 chars. Tabs (paraphrase): do not combine Chip-style Tabs with Filter Chips on one screen. |
| Toss TDS mobile | Badge only: fill / weak, sizes xsmall..large, colours blue/teal/green/red/yellow/elephant; no px published (docs index lists no chip, no tag) | None. Filtering uses Segmented Control and Tab | none | none listed | "Badge… 항목의 상태를 빠르게 인식할 수 있도록 강조" (emphasise status for quick recognition); fill = "higher saturation, visually striking". ListRow right slot may hold Badge. |
Shopify Polaris (web components + Badge.module.css, Tag.module.css) | Badge (static, system-generated status): radius --p-border-radius-200 (8px [relay]), padding 2/8, weight medium, tones info/success/warning/critical/neutral/caution, color base/strong, size base/large; Chip (non-interactive label for user-created tags) | Clickable chip (interactive, color subdued/base/strong); legacy Tag: radius 8, min-height 24px (large), bg fill-tertiary | Clickable chip removable; Tag removable | none | Badge: "Keep labels to 1-2 words"; paraphrase: base size in table cells, list items and badge groups, and use chip rather than badge for user-created labels. Clickable chip (paraphrase): use the non-interactive chip for labels users do not need to click or remove. |
| Atlassian | Lozenge (status/important attribute; bold by default; maxWidth 200; sentence case); Tag (descriptive metadata, 10 colours, maxWidth) | Tag can link (non-interactive otherwise) | Tag: removable by default, isRemovable | Badge: numeric only, 6 semantics + 5 bold | Lozenge: "important labels that need to be easily noticed". Tag: "subtle, compact label used to categorize and organize content"; paraphrase: not for status. Badge: "For tallies or scores, use a badge." px values not on the usage/code pages. |
IBM Carbon (_tag.scss, usage page) | Tag read-only: pill (radius 16px; v12 flag uses a token), sm 18 / md 24 / lg 32px, label-01 (12px) | Selectable: selected = $layer-selected-inverse + $text-inverse (ink); also Operational (opens popover) | Dismissible: dismiss button = tag height | none on the Tag page | "Do not use tags as links that direct you to an entirely different page."; paraphrase: avoid tags with multiple functions (accidental clicks); titles under 20 characters. |
Fluent 2 (useBadgeStyles, useTagStyles, usage page) | Badge: circular default, rounded (medium radius) or square; 6 / 10 / 16 / 20 / 24 / 32px, caption1Strong (12) / caption2Strong; appearances filled/ghost/outline/tint; Counter Badge, Presence Badge | Tag: 20/24/32px, rounded or circular; filled/outline/brand; selected = colorBrandBackground + on-brand text. Interaction Tag: primary action + dismiss | Tag with dismiss icon (12/16/20px) | Counter Badge ("numeric values such as tallies and scores") | Badge: "system-generated data that people can't change." Tag: "a representation of a value that someone has picked." |
GOV.UK (_mixin.scss) | Tag: status only; padding 2/8/3/8, radius 1px, font 19, 9 colours as tint-80 bg + shade-50 text, regular weight, sentence case | none ("Do not make a tag interactive by making it into a link or button") | none | none | "Tags are only used to indicate a status."; paraphrase: adjectives, not verbs (a verb suggests a click); dark tags were lightened because users tried to click them. |
| Ours today | Badge: 8px (xs/sm) / 12px (md), 18 / 26 / 28px tall, text 10 / 12 / 14, weight 600, 12 variants | SelectionChip: pill, 32 / 40px, label 12/500 → 12/700, resting $card + 1px $border, selected $primarySubtle + 1px $primary | RemovableChip (always selected look) | CornerCountBadge: 18px circle, 10/700 | badge.tsx:2-8 calls a badge "a passive label, not a tappable control". |
Sources (all fetched 2026-10-09): Seed https://seed-design.io/rootage/components/{chip,badge,tag-group,manner-temp-badge,segmented-control-item,list-item}.json, https://seed-design.io/llms/components/{badge,tag-group,notification-badge,tabs,segmented-control,manner-temp,list}.txt, https://seed-design.io/docs/components/chip, dimension and font-size tokens https://github.com/daangn/seed-design/blob/dev/packages/rootage/dimension.yaml and font-size.yaml (x1_5 = 6px, x5 = 20px, x6 = 24px; t1 = 11px, t2 = 12px, t4 = 14px). M3 https://github.com/material-components/material-web/blob/main/tokens/versions/v0_192/_md-comp-filter-chip.scss (container-height 32px :37, container-shape corner-small :38, flat-unselected-outline-width 1px :85, icon 18px :160), _md-comp-badge.scss (large-size 16px :57, size 6px :59), _md-sys-shape.scss (corner-small 8px), _md-sys-typescale.scss (label-large 0.875rem/medium), https://github.com/material-components/material-components-android/blob/master/docs/components/Chip.md and BadgeDrawable.md. Apple https://developer.apple.com/design/human-interface-guidelines/{tab-bars,segmented-controls,lists-and-tables,token-fields,pop-up-buttons}. TDS https://tossmini-docs.toss.im/tds-mobile/llms.txt, /components/badge/llm.txt, /components/ListRow/list-row-components/llm.txt. Polaris https://shopify.dev/docs/api/app-home/polaris-web-components/feedback-and-status-indicators/badge, /actions/clickable-chip, https://github.com/Shopify/polaris/blob/main/polaris-react/src/components/Badge/Badge.module.css and Tag/Tag.module.css. Atlassian https://atlassian.design/components/{lozenge,tag,badge}/usage, /lozenge/code, /tag/examples. Carbon https://carbondesignsystem.com/components/tag/usage/ and https://github.com/carbon-design-system/carbon/blob/main/packages/styles/scss/components/tag/_tag.scss. Fluent https://github.com/microsoft/fluentui/blob/master/packages/react-components/react-badge/library/src/components/Badge/useBadgeStyles.styles.ts, react-tags/.../useTagStyles.styles.ts, https://fluent2.microsoft.design/components/web/react/core/tag/usage. GOV.UK https://design-system.service.gov.uk/components/tag/ and https://github.com/alphagov/govuk-frontend/blob/main/packages/govuk-frontend/src/govuk/components/tag/_mixin.scss.
1.2 What the table supports
- Passive/interactive split by name: Seed (Badge vs Chip), Polaris (Badge, Chip vs Clickable chip), Atlassian (Lozenge/Tag vs link/removable Tag), Fluent (Badge vs Tag/Interaction Tag), M3 (no passive label at all), GOV.UK (passive only). One component with modes: Carbon (read-only/dismissible/selectable/operational share one 16px pill), partly Fluent and Atlassian Tag.
- Shape separates the two in Seed: Badge radius 4-6px (rect) against Chip
radius.full. Our ledger row 12 (docs/design/research/colour-system-research-2026-10-09.md:22) already records this correction. M3 inverts it (chips 8dp rect, badge full) but still gives the two different shapes. - Two emphasis steps for passive labels, with a third outline only in Seed and Fluent.
- Counts are a separate micro component in 5 of 9 (M3 Badge, Seed Notification Badge, Fluent Counter Badge, Atlassian Badge, HIG tab badge). Seed's rules map onto our
CornerCountBadge: cap 4 chars, never 0, top-right, clear when seen. - Selected state for a chip is ink in Seed solid and Carbon, brand fill in Fluent, tonal container in M3; Seed itself recommends the low-prominence
outlineWeakfor selection lists because several selected values appear at once (https://seed-design.io/docs/components/chip). - Chip label size: Seed 14px (all three heights), M3 label-large 14px, Fluent medium Tag body1 14px; ours is 12px at the 40px height (
badgerole,selection-chip.tsx:107-111viaChipLabel). - Navigation vs manipulation: Seed "Tabs는 탐색(Navigation)을 위한 것이고, Segmented Control은 조작(Manipulation)을 위한 것" (https://seed-design.io/llms/components/segmented-control.txt); Seed still ships a chip-variant Tabs (https://seed-design.io/llms/components/tabs.txt), so "chips stop being navigation" is the owner's call, not a market given. HIG: segmented control for "closely related choices that affect an object, state, or view"; use a tab bar for "completely separate sections".
2. Our inventory
2.1 Components, consumers (non-test files with a JSX use, grep -rlE "<Name\b" packages apps), specs
| Component | File | Consumers | Radius | Height | Font | Fill / state | Role today |
|---|---|---|---|---|---|---|---|
Badge | packages/ui/src/badge.tsx | 75 files (ui 1, app 17, clubs 19, sessions 27, profile 5, home 3, records 2, messaging 1) + 18 status maps | $2 8px (xs, sm), $3 12px (md) :88-96 | xs 18 (10/14 + 2x2), sm 26 (12/18 + 2x4), md 28 (14/20 + 2x4) | badge role, weight 600, size 10 / 12 / 14 badge-visual.ts:10-16, badge.tsx:108 | 12 variants :58-86; literal uses: size xs 33, sm 30, md 0 | passive label, status + facts |
status wrappers (XxxStatusBadge) | packages/app/src/presentation/components/status/status-badges.tsx | e.g. RsvpStatusBadge 6, MatchStatusBadge 5, SessionStatusBadge 4, WinResultBadge 4, ScoreStatusBadge 4, 15 more with 1-3 | via Badge | via Badge | via Badge | 18 variant maps status-variant-maps.ts: success 15 mentions, neutral 14, muted 14, warning 13, accent 7, live 5, info 4, primarySolid 2, premium 1 | domain status |
TierBadgeChrome (Elo/Trust) | app/.../tier-badge-utils.tsx:46-56 | EloTierBadge 7 files, TrustTierBadge 6 | $2 | $1 v-pad + emoji line box | badge | $primarySubtle, emoji + label | passive level label |
TierRangeBadge | app/.../status/tier-range-badge.tsx:29,33 | 2 files + inline density on the session card | $2/$3 | 22 / 26 / 30 | badge / cardMeta inline | surface pill or bare text | passive fact |
ReliabilityPill | app/.../reliability-pill.tsx:27 | 1 | Badge | Badge | Badge | <Badge neutral sm> | passive fact (named "pill") |
MarkerPill | packages/ui/src/marker-pill.tsx:14-15 | 4 | full pill $7 | $1/$2 padding | badge 600 | $primarySubtle or $card | passive marker (a second passive label shape) |
ContextChip | features/sessions/src/add-round/feasibility-tiles.tsx | 1 | $7 | $2 pad | cardMeta 600 | $card + 1px $borderSubtle | passive label drawn as a chip |
CornerCountBadge | packages/ui/src/corner-count-badge.tsx:31 | 2 (app-nav.tsx, notification-bell.tsx) | circle | 18px | micro 10/700 | $badgeErrorBg / $badgeWarningBg | count |
SelectionChip | packages/ui/src/selection-chip.tsx:25,85,111-123 | 58 files (≈45 wizard/settings/form option pickers: create-club*, create-session*, settings/, venue-correction/, venue-contributions/*; ≈10 scope/filter: match-history-scope-selector, venue-filter-bar, club-feed-chips, ranking-tab, sessions-pane, activity-metric-calendar, court-usage-panel, notification FilterChip, ...; 2 are DropdownChip/RangeChip themselves; split by path, approximate) | $7 pill | 32 / 40 (+ hitSlop to 44) | ChipLabel 12 badge role, 500 → 700 chip-label-visual.ts:20-26 | resting $card + $border; selected $primarySubtle + $primary 1px | control |
DropdownChip | packages/ui/src/dropdown-chip.tsx | 11 | via SelectionChip | via SelectionChip | via SelectionChip | trailing chevron, ModalPanel | control (pop-up) |
RangeChip | packages/ui/src/range-chip.tsx | 2 (pickup-filters, discover-filters) | via SelectionChip | control | |||
RemovableChip | packages/ui/src/removable-chip.tsx | 2 | $7 | $8 (40) | ChipLabel selected | always selected look + x | input chip (does not compose SelectionChip, own XStack) |
PillNav / SegmentedTabs | packages/ui/src/pill-nav.tsx:75-135 | 7 direct + segmented-tabs.tsx; SegmentedTabs 8 (incl. main-tab-shell) | track $5 20, pill $4 16 | $2 v-pad | 14/600, active $primaryText on $primary indicator | track $surfaceTertiary + 1px $borderSubtle | navigation / view switch |
ActionPill | packages/ui/src/action-pill.tsx:27,51-71 | 2 | $7 | 28 | badge 600 $textSecondary | $surfaceSecondary | action drawn as a label pill |
TogglePill | packages/ui/src/toggle-pill.tsx:17-18 | 9 | $3 | 44x24 track | none | $primary / $border | a switch named "pill" |
GlyphFactRow chip density | packages/ui/src/glyph-fact-row.tsx:136-168 | 4 (club card x2, type-hero, media-hero; session card uses inline) | Badge surface | Badge sm 26 | one Badge + emoji per fact | passive facts as chips | |
MetaSegments | packages/ui/src/meta-segments.tsx | 31 | n/a | n/a | META_TEXT tiers text-grammar.ts:40-45 | text + MetaBreak | inline facts (the T3 destination) |
| hand-drawn pills | club-photo-seam.tsx:228-235, club-identity-header.tsx:84-98, public-club-profile-screen.tsx:568-577, venue-link.tsx:100-112, banner-capacity-cluster.tsx:67 (labels); post-row.tsx:481-500, arrival-picker.tsx:46-61, pickup-host-card.tsx:190-205, club-album-banner.tsx:215-229 (controls) | 9 sites | $7 | 24-32 | badge / cardMeta | $surfaceSecondary, $primarySubtle, $overlayBackdrop | both |
| mis-named | TournamentContinueChip (format-chooser-sheet.tsx:403) is a full-width action banner, PlayerIdentityChip (6) is a row, FilterChip (notification-center) is a SelectionChip wrapper | 1 / 6 / 1 | name drift |
Raw count: borderRadius="$7" appears 28 times in 22 files outside tests (grep -rn --include=*.tsx 'borderRadius="\$7"' features app/src/presentation ui/src); only 2 files (selection-chip.tsx, removable-chip.tsx) are in the selection-chip family.
2.2 Places a label is drawn as a chip, or the reverse
Labels drawn as chips or pills (should be Tag):
club-identity-header.tsx:84-98emoji + label static identity "chips" (24px floor),club-photo-seam.tsx:228-235capacity pill,public-club-profile-screen.tsx:568-577capacity pill with chevron,venue-link.tsx:100-112venue "link chip" (display-only percanon/components.md:18),feasibility-tiles.tsxContextChip,MarkerPillx4.GlyphFactRowdefaultchipdensity: 5 mint chips on a discover card (club-card.tsx:322,329), the hero (type-hero.tsx:96,media-hero.tsx:261).- ~19 of 39
<Badge variant="neutral">call sites print facts:round-rules-strip.tsx:58-64(six Badges in a row: set format, target games, win-by, tiebreak, draw, deuce),amenities.tsx:39,payment.tsx:27,venue-confidence.tsx:242,practice-log-screen.tsx:177(duration),directory-venue-card.tsx:262(distance),club-schedule-rows.tsx:119andclub-session-card.tsx:142(D-day),match-row-header.tsx:96(format),challenge-accept-modal.tsx:57(type),king-of-court-ladder.tsx:258. Seed's cap (paraphrase) is one or two badges per object;canon/components.md:26says chips are a closed whitelist, one row.
Controls drawn as labels:
ActionPill(28px,$surfaceSecondary,$7) and thepost-rowsession chip are tappable and look exactly like the four passive pills above.TournamentContinueChipis an action with a "chip" name and a$primarySubtle+$primaryborder like a selected chip.RemovableChipalways paints the selected look (removable-chip.tsxheader), so "selected" and "chosen item" are one glyph.- A selected
SelectionChip(mint fill, brand border),VenueLink(mint fill, brand text),MarkerPillandTierBadgeChrome(mint fill) differ from each other only by border and radius.
Names that blur: "chip" (SelectionChip, MarkerPill, ContextChip, PlayerIdentityChip, TournamentContinueChip), "pill" (ActionPill, MarkerPill, TogglePill, ReliabilityPill, PillNav), "badge" (Badge, CornerCountBadge, CameraBadge, NaverBadge, 20 wrappers).
3. The unification
3.1 Position
Two primitives, one shared geometry module, one retired word.
Tag— the one passive label. ReplacesBadge,MarkerPill,ContextChip, the tier/reliability chrome and every hand-drawn label pill. It has no press props at the type level.Choice— the one selection control (renamedSelectionChip; absorbsDropdownChip,RangeChip,RemovableChipas modesmenu,range,input). Wizard multi-select options and scope filters both use it. Look: outline, ink selected, check.SegmentedTabs/PillNavstay as navigation / view switch (Seed: "Tabs… Navigation, Segmented Control… Manipulation");CornerCountBadgeis renamedCountBadge(Seed Notification Badge rules);ActionPillfolds intoButton(small tonal);TogglePillis renamedSwitchTrack(it is a switch, not a pill).- Retire the word "chip" from component names; keep it in prose for the control only if wanted.
- Shared implementation: one
label-chrome.ts(framework-free, likebadge-visual.ts) holds heights, label font and paddings that bothTagandChoiceread, so the two cannot drift, which is the "unify implementation" half of the owner's ask without making a pressable thing and a label the same shape.
Why not one Tag with selectable mode (Carbon-style): Carbon (avoid tags with multiple functions), GOV.UK ("Do not make a tag interactive…") and Seed (badges never clickable) warn against it, and ledger row 12 already cites NN/g on non-clickable items that look like buttons; the passive half is where our inconsistency lives (finding 1); and the board ruling 12 already keeps shape as the cue. If the owner insists on one name, Carbon's single Tag is the precedent, but it needs the same look split inside it.
3.2 Tag spec
| Property | Value | Support |
|---|---|---|
| Name / file | Tag, packages/ui/src/tag.tsx + tag-visual.ts | |
| Props | label, tone, tier (default weak), size (default sm), icon? (status glyph only: check, pulse dot; never emoji), alignSelf default center (Badge's baked flex-start pins a pill to the top of a row, badge.tsx:57), shrink?. No onPress, no aria-pressed/checked/selected. | GOV.UK, Seed |
| Tones | neutral, brand, success, warning, danger, info, level | Seed 6 tones; board VT_BADGE visual-tone.jsx:260-267 |
| Tiers | weak (tinted, repeated rows), solid (over photos), outline (neutral only, on tinted surfaces: replaces surface, 5 sites, and outline, 0 sites) | 6 of 7 systems have two; Seed/Fluent add outline |
| Sizes | sm 22px (12/18 + 2x2 pad), md 26px (12/18 + 2x4 pad); xs (10px text, 33 call sites) retired. Heights 22/26 sit between Seed 20/24, Fluent 20/24, Carbon 18/24. | owner rejected 10px for the count on 2026-07-28 (session-compact-row.tsx count comment) |
| Radius | $2 (8px) for both sizes (retire md's $3); level tone is a 22px circle (numeral) | board ruling "rounded rectangles $2 in two tiers" (colour-system-research…:22); Seed 4-6, Polaris 8 [relay] |
| Font | badge role 12/18, weight 600 weak, 700 solid; tabular numerals when the label is a number | Seed weak 500 / solid 700; ours 600 (badge.tsx:108) |
| Label | 1-2 words, ≤10 chars default, sentence case, adjectives (참석, 마감 임박, 완납), never a verb | GOV.UK, Polaris, Seed ("2-3 words", >10 chars truncates) |
| Count of tags | ≤2 per object; ≥3 literal Tags in one container is a lint finding, lists of facts use inline text (MetaSegments) | Seed (paraphrase: one or two badges per object); canon/components.md:26 |
Tone table (values from the board's VT_BADGE and the ledger palette, contrast re-computed here with WCAG 2.x):
| tone | weak (fill / text) | solid (fill / text) | used for |
|---|---|---|---|
neutral | #DFE7E1 track / $text | #4F5851 / white | role (호스트), pending, metadata |
brand | $primaryMuted #C8EED2 / $primary #006D37 (5.12:1) | same as weak (brand solid is reserved for the one action) | 모집 중, invitation |
success | #CBEBE5 / $successText #014B40 (7.95:1) | $success #00796A / white | 완납, 참석 확정, win |
warning | $warningFill #F5B301 / $text (10.32:1) | same | 마감 임박, 미납 |
danger | $error #B8332E / white | same | live (+ PulseDot), cancelled, overdue |
info | #E8DCFB / $info #4B0082 (9.91:1) | $info / white | informational only |
level | $levelChipBg #C6E84B / $levelChipText #0D100E (13.71:1) + 1px $border | same | NTRP/tier numeral |
3.3 Migration map
| Today | Consumers | Becomes |
|---|---|---|
Badge (12 variants) | 75 files | Tag. neutral→neutral; muted→neutral (cancelled/expired read through the label); warning→warning; live→danger+pulse; accent→brand (모집 중 per T8 D1); success→success (teal per T8); info→info; primarySolid→brand solid (the viewer's 참석 only); surface/outline→outline; primarySubtle→brand; premium deleted. Codemod on the variant literal, 18 maps edited in one file. |
<Badge variant="neutral"> printing a fact | ≈19 sites | not a Tag: MetaSegments text. Round-rules strip becomes one meta line. |
Badge xs | 33 | Tag sm |
TierBadgeChrome (Elo/Trust) | 13 files | Tag tone="neutral" with the tier dot (T7 "white tier chip with a tier-hue dot") on hero/profile surfaces; plain text in rows |
TierRangeBadge | 2 + card | text segment in rows; Tag only on the hero |
ReliabilityPill, MarkerPill, ContextChip | 1, 4, 1 | Tag |
| hand-drawn label pills | 5 sites | Tag |
GlyphFactRow density="chip" | 4 | delete the chip density; inline without glyphs |
SelectionChip | 58 | Choice (same props; look changes) |
DropdownChip, RangeChip, RemovableChip | 11, 2, 2 | `Choice mode="menu" |
ActionPill, hand-drawn pressable pills | 2 + 4 | Button size="sm" tone="tonal" or Choice |
CornerCountBadge | 2 | CountBadge (keep; Seed rules: ≤4 chars, never 0, clear when seen) |
TogglePill | 9 | SwitchTrack (rename only) |
3.4 Choice (control) look
| State | Look | Support |
|---|---|---|
| resting | 1px $border outline, $background fill, label 14/500 $text, full pill, 32 / 40px (+ hitSlop to 44, as today selection-chip.tsx:85) | Seed outlineWeak/outlineStrong, M3 outlined chip |
| selected | $text ink fill, $textInverse label 700, leading check glyph (second non-colour cue) | Seed solid selected = bg.neutral-solid; Carbon $layer-selected-inverse; ledger row 10 |
menu | trailing chevron, opens ModalPanel (no sheets) | Apple pop-up button |
input | trailing x (min 44 hitSlop, as removable-chip.tsx) | Seed/M3/Polaris/Fluent input chips |
| disabled | 0.5 opacity | Seed |
Shape cue that survives a squint: tinted fill + 8px corners = label, outlined white pill = control. Seed does the same (Badge 4-6px filled vs Chip full-radius outlined). Wizard multi-select option sets keep this look (they are form controls); big tiles stay SelectionCard.
3.5 Lint rules (modelled on no-hand-built-person-row.js, no-adhoc-status-badge.js, bounded-variant-enums.js)
@twomore/tag-is-passive(error, features +app/src/presentation): a<Tag>carryingonPress*,role="button|tab|radio|checkbox",aria-pressed|checked|selected|expanded, or a<Pressable>whose only JSX child is a<Tag>. Message: "A pressable label is a control: use Choice or Button." Also a TS-levelOmitonTagProps.@twomore/no-hand-drawn-label(error): aView/XStack/YStackthat hasborderRadiusin {$2,$3,$7} and (backgroundColororborderWidth) and a<Text textRole="badge"|"micro">child and noonPressorPressableancestor. Exempt onlytag.tsx,choice.tsx,count-badge.tsxand avatar/dot geometry (square size == height). Routes toTag. The six sites in 2.1 are its first findings.@twomore/no-hand-rolled-pressable-pill(error): aPressable(oronPresshost) withborderRadius="$7"and abackgroundColor/borderWidthand abadge/cardMetaTextchild is a hand-built chip or action pill: route toChoiceorButton size="sm". First findings:post-row.tsx:481,arrival-picker.tsx:46,pickup-host-card.tsx:190,club-album-banner.tsx:215,action-pill.tsx(until folded).styling.md:93already retires hand-rolled selectable pills in prose; this makes it a gate.@twomore/choice-needs-handler(error):<Choice>withoutonPress/onValueChange/onRemoveis a label;<Choice>may not appear inside<Tag>or the reverse.@twomore/tags-per-container(warn → error): three or more literal<Tag>siblings in one JSX parent (Seed (one or two)).- Keep and extend:
no-adhoc-status-badge(route status through wrappers),bounded-variant-enums(boundTagtone/tier), replaceno-badge-weight-overridewith aTagtext-weight check. - Canary (memory: "Gates need canaries"): a known-positive fixture under
packages/eslint-plugin/__tests__/fixtures/for each rule with an expected finding count, so a rule that matches nothing fails.
4. Part B: top-down hierarchy in rows
4.1 Evidence
| Source | Finding | URL |
|---|---|---|
| NN/g F-pattern | 232 users, thousands of pages: "two horizontal stripes followed by a vertical stripe"; "Front-load words"; "They'll read the third word on a line much less often than the first two words." (web content pages, desktop) | https://www.nngroup.com/articles/f-shaped-pattern-reading-web-content-discovered/ |
| NN/g first 2 words | 80 participants, 20 links: users "typically see about 2 words for most list items"; seeing only the first 11 characters, 35% of links gave no prediction; strong front-loading ("Gift Cards") 85% | https://www.nngroup.com/articles/first-2-words-a-signal-for-scanning/ |
| NN/g left bias | 120+ participants, 130,000 fixations: 80% of time on the left half (69% in 2010); 94% on SERPs | https://www.nngroup.com/articles/horizontal-attention-leans-left/ |
| NN/g visual hierarchy | "Make the most important element biggest"; "If everything is contrasted, then nothing stands out"; at most 3 sizes, at most 2 big elements | https://www.nngroup.com/articles/visual-hierarchy-ux-definition/ |
| NN/g cards | "Card layouts are less scannable than lists"; "content at the top and on the left… is perceived as more important" | https://www.nngroup.com/articles/cards-component/ |
| NN/g photos | Amazon thumbnails 0.9 fixations vs 4.4 on the description, 18% of viewing time on photos; informative images get looked at, decorative ones ignored | https://www.nngroup.com/articles/photos-as-web-content/ |
| UF eyetracking (video selection) | thumbnails got "almost twice as much attention" as title text and were looked at before it (no n published) | https://jainlab.cise.ufl.edu/research/completed-projects/eye-tracking-for-online-user-experience/ |
| NN/g icons | "always include a visible text label"; ambiguity of hearts/stars/clocks; no claim that decorative icons slow scanning | https://www.nngroup.com/articles/icon-usability/ |
| M3 list item (tokens) | headline body-large 16/400, supporting body-medium 14/400, trailing supporting label-small 11/500; one-line 56, two-line 72, three-line 88 px; leading/trailing 16 | https://github.com/material-components/material-web/blob/main/tokens/versions/v0_192/_md-comp-list.scss (:96-100, :156, :202, :220, :232, :250) |
| Seed list item | title t5 16 regular fg.neutral; detail t3 13 regular fg.neutral-subtle; suffix text 16 regular subtle; paddingY 12, body gap 2; "Detail 은 List 제목 하단에 위치" (Sub Text + Tag Group) | https://seed-design.io/rootage/components/list-item.json, https://seed-design.io/llms/components/list.txt |
| Apple HIG | "Prefer displaying text in a list or table"; "Keep item text succinct"; info button vs disclosure indicator; no sizes. subtitle style (primary body-ish / secondary subheadline) [relay] | https://developer.apple.com/design/human-interface-guidelines/lists-and-tables |
| Toss TDS | ListRow left / contents / right; text types 2RowTypeA..F (top, bottom), right slot takes Texts, Switch, Badge, arrow; no px published | https://tossmini-docs.toss.im/tds-mobile/components/ListRow/list-row-components/llm.txt |
| Toss frames | heaviest = top line in every row: toss_f0 amount ~17-18 pt bold over a ~14 pt grey label, trailing tonal button; toss_f2 stock name over "4주", trailing price bold over a coloured change; toss_f5 title with blue sub, one grey status chip in 1 of 7 rows | bench/crops/toss_f0.png, toss_f2.png, toss_f5.png (visual estimates ±2 pt) |
| Monzo frame | merchant name + grey sub left, amount right, one logo avatar, no chips | bench/crops_c/m_monzo_0.png |
| Ours today | ListRow: title cardBody/cardMeta 14/600, sub META_TEXT.secondary 14/500 $textTertiary; zero size step | packages/ui/src/list-row-visual.ts:34-39, text-grammar.ts:42 |
4.2 Which line is heaviest in peer rows (my read of the frames)
| App (frame) | Line 1 | Heaviest line | First body line heaviest? | Note |
|---|---|---|---|---|
Smash (b_zoom1.png mid) | small grey region eyebrow | title, bold ~17 | yes (after eyebrow) | date regular, count in grey rect tag top-right, 1 inline glyph |
TennisFriends (tennisfriends_3) | small grey region eyebrow | title bold | yes (after eyebrow) | 4 chips (복식, 3면, Lv, 마감), count + glyph |
Munto (b_zoom2.png mid) | category chip (+ red 마감임박) | title bold ~19 | yes (after chips) | meta one grey line, host avatar + count text, heart on photo |
Flap (flapfootball_1) | time "18:00" in grey | venue bold, line 2 | no | time first but light; chips 확정임박 + AI 리포트 |
Band (band_3) | date bold ~20 | date | yes | event title below, avatar stack + "28명 참석", green chip |
Greenlight (greenlight_2) | "1.10(수) 17:55" | first line | yes | region 2nd, category 3rd, red "1자리 비었어요!" text |
Somoim (somoim_5) | "10/2 (수) D-2" bold, D-2 red | first line | yes | title below |
Spond (spond_5) | title bold | title | yes | date block left, when 2nd, three response counts |
Heja (heja_2) | coloured type eyebrow | title bold large | yes (after eyebrow) | the only row with buttons |
당근 item (danggeun_f1) | title regular 16 | price bold, line 3 | no | meta grey line with pin glyph, one orange "바로구매" tonal tag in 1 of 3 rows, counts bottom-right, kebab top-right |
Frip (frip_3) | small region eyebrow | title bold, price bold last | partly | rating, price, heart |
| Toss, Monzo | amount / merchant | top line | yes | trailing amount top-right |
Tally: first body line heaviest in 9 of 12 (8 of 9 session rows); the three exceptions are commerce or a time-over-venue pair. Lane B's wider count (n = 19): time/date led 7, title led 6 (lane-b-category.md:70).
4.3 Time-first or venue-first
The user's question is "when and where". Peers answer it in two grammars, and the split follows the list structure:
| List structure | Peers | Line 1 |
|---|---|---|
| grouped or filtered by a date strip/header | Flap, Greenlight, Band lead the text with time/date (3 of 4); Spond puts the date in a left date block and leads the text with the title | time/date (3), date block + title (1) |
| ungrouped discovery feed | Smash, Munto, TennisFriends, Frip (4 of 4) | title/venue |
Our lists are grouped by day (SessionDayHeader; session-card-v2.tsx:31-32: "the DATE LEAVES THE CARD"). Within a day the time is the sort key and the only thing that differs row to row; venue names repeat (a club plays at one or two courts). NN/g's "first two words" says the left edge carries the scan, so the unique, ordered datum goes there. The photo already carries the venue's identity (NN/g: informative images are fixated), so the text can lead with the time. Recommendation: line 1 = time (16/600), line 2 = venue (14/500). When a list is not grouped by day, line 1 = 토 19:00-21:00 (same style), so there is one grammar, configured by the projection, not forked. Fallback (owner prefers place-first): line 1 = venue 16/600, line 2 = time 14/500; every other rule below is unchanged. Either way the T2 "after" order (16/600 venue over 18/700 time) is out.
4.4 The ladder and the row specs
One ladder for all rows. Weight and size never increase down the row; colour goes ink → ink → secondary.
| Step | Size / line | Weight | Colour | Where |
|---|---|---|---|---|
| T1 title | 16/24 | 600 | $text #0D100E | line 1 (cardTitle role) |
| T2 body | 14/20 | 500 | $text | line 2 (cardMeta role with $text) |
| T3 meta | 13/18 | 500 | $textSecondary #4F5851 (7.38:1 white) | line 3 and the trailing text (new meta role, ledger row 8) |
| Tag | 12/18 | 600 | per tone | only on exception, never mid-row |
Rules: no bold below line 1; no promoted numeral inside a row (hero numerals stay on hero blocks, 28-44/700); $textTertiary not used for row text (Lc 77/71, ledger row 8); at most one trailing element; trailing is aligned to line 1 on the right; exactly one picture or avatar at left.
| Row | Left | L1 (T1) | L2 | L3 | Trailing (one) | Moves out |
|---|---|---|---|---|---|---|
| Session (grouped by day) | venue's own photo, else map tile, 72 x 72, radius $3, 1px $borderSubtle (map tile inset $border) | 19:00-21:00 (tabular) | venue name, T2, one line | 복식 · NTRP 3.0-4.0 T3 via MetaSegments (no glyphs) | 6/8 T3 text, or one Tag when a state outranks it (live, the viewer's standing, 마감 임박) | bookmark, bar, fee, glyphs, status word, court dot |
| Club (list) | club logo / medallion 56, circle | club name (wraps, NAME_LINES) | — | 서초구 · NTRP 3.0-4.0 · 멤버 42 T3; for a member, L2 = next session 토 19:00 올림픽공원 T2 and L3 = region meta | discover: 모집 중 brand Tag only when recruiting; member: CountBadge alarm only when >0 | cover photo window, tier corner badge, activity dot text, tagline, 5 fact chips |
| Dues | avatar 40 | member name | — | 9월 · 30,000원 T3 tabular | status Tag (완납 success, 미납/일부 warning) or, for the admin when action is due, the one 납부 확인 button (replaces the Tag, as dues-row.tsx header already says "at most ONE visible pill") | amount emphasis |
| Member | avatar 40 | name (NAME_LINES) | — | 조 name + trust word as T3 text | kebab (admins) or one exception Tag (호스트, 게스트, 예비, 차단됨), never both | TrustTierBadge chip, manual-pin chip become T3 text |
Seed's and M3's numbers for scale: Seed 16 regular over 13 regular subtle; M3 16/400 over 14/400 with an 11/500 trailing. The ladder (16/600, 14/500, 13/500 secondary) is Seed's step plus a weight step, with every cell at or above the ledger floors (13/500 secondary, colour-system-research…:18).
5. Part C: attention inventory and the compact row
5.1 Session card today (packages/app/src/presentation/components/sessions/session-card-v2.tsx, browse viewer, no live band)
| # | Element | Size / weight | Colour role | Shape / fill | Source |
|---|---|---|---|---|---|
| 1 | Card frame | padding $3 12 | $card, 1px $borderSubtle | radius $4 16 | card.tsx:28-31, session-card-v2.tsx:546 |
| 2 | Venue tile | square, stretched to the 3-line column = 24 + 8 + 20 + 4 + 20 = 76 | map/photo | radius $3, 1px $borderSubtle (+ corner surface chip on the map branch) | venue-tile.tsx:92-94,124-126 |
| 3 | L1 venue name | 16/24 600 | $text | text | :376 |
| 4 | Bookmark | 22px outline / filled | $textTertiary / $primary | icon | :388-395, save-button.tsx:39 |
| 5-6 | L2 time glyph + text | emoji 12 in 20px slot; 14/20 500 | $text | emoji (saturated, platform-dependent) | :408-413, glyph-fact-row.tsx:20 |
| 7-8 | L2 format glyph + text | same | $text | :414-421 | |
| 9-10 | L3 tier glyph + band | 🎯/🔒 emoji; 14/500 (muted $textTertiary when locked) | $text | :436-447 | |
| 11 | Lead badge (conditional) | Badge xs 10/14 600, 18px tall | status tint | radius 8, filled | :473, badge.tsx:88-92 |
| 12 | Fee (browse only) | 14/20 500 | $text | text | :474-478 |
| 13 | Status word (when no lead chip) | 12/18 600 | $textSecondary | text | session-compact-row.tsx:124-128 |
| 14 | Capacity bar | 5px tall | recruitment hue on muted track | radius $2 | session-compact-row.tsx:129-136, progress-bar.tsx:70,97 |
| 15 | Count 6/8 | 12/18 600 tabular | $text | text | session-compact-row.tsx:137-141 |
Typical browse viewer: items 2-4, 5-10 (six), 12-15 = 13 objects (+ the tile's corner chip) in a 130 px card (12 + 76 + 8 + 20 + 12 padding/gaps + 2 borders; arithmetic from the numbers above). A confirmed member swaps 12-13 for 11: 12 objects. Weight steps in view: 600 title, 500 facts, 600 badge, 600 count, bar fill, emoji colour: three competing accents (emoji, badge fill, bar fill) beside the photo.
Board T2 "after" card (visual-tone.jsx:654-705 (time at :705)): tile, venue 16/600, bookmark, time 18/700, format 14/500, tier 13/500 + glyphs, RSVP badge or fee, bar, count (10 objects, 6 text sizes/weights); the promoted line is the second.
5.2 Club card today (packages/features/clubs/src/shared/club-card.tsx, club-photo-seam.tsx)
| Element | Size / weight | Colour / fill | Shape | Source |
|---|---|---|---|---|
| Photo window | 3:1, full width | photo | top corners $4 | club-photo-seam.tsx:55,128 |
| Tier band corner badge | Badge sm 26px, 12/600 | surface pill / $surfaceTertiary | radius $2 | :152, tier-range-badge.tsx:29 |
| Recruitment badge (discover) / alarm circle (member) | Badge / 22px circle | accent tint / $error | :175,189 | |
| Flag / logo | 48px circle straddling the seam | logo or medallion | circle | :55,193 |
| Faces + capacity pill | 28px cluster; pill 12/600 | $surfaceSecondary | $7 | :212-235 |
| Name | 16/600 | $text | club-card.tsx:269 | |
| Activity indicator | 6px dot + 14/500 | $success / $textTertiary / $warning | club-card-activity-indicator.tsx:46-47 | |
| Tagline (conditional) | 14/500 | $textSecondary | club-card.tsx:310-312 | |
| 3 identity chips (📍👥📅) + 2 terms chips (🔒💳) | Badge surface sm 26px, 12/600 + emoji | $surfaceTertiary | $2 | :322,329, glyph-fact-row.tsx:156-168 |
Discover card: photo, 2 corner badges, logo, faces, capacity pill, name, activity (2), tagline, 5 chips with 5 emoji = 16 objects + 5 glyphs. Member card: photo, tier, alarm, logo, faces, capacity, name, activity (2), then per upcoming session (up to 2) dot, date, time, venue, 24x3 meter, count (club-card.tsx:375-440) = up to 21 objects. Two pictures (the 3:1 cover and the 48px flag).
5.3 What peers put in a list row (11 session/listing rows + Toss/Monzo, my reading of bench/b_crops, bench/crops, bench/crops_c)
| Element | Peers that show it in the row | Ours today |
|---|---|---|
| Identity picture | 7 of 11 (Smash, TennisFriends, Munto, Flap, Greenlight, Frip, 당근); one per row | tile + (club: cover + flag) |
| Title / lead line | 11 of 11 | yes |
| Time/date in the row | 11 of 11 | yes |
| Format (복식 etc.) | 3 of 11 (Smash, TennisFriends, Flap), as meta text or one chip | glyph + text |
| Level/tier | 3 of 11 in the row (Smash, TennisFriends, Flap); lane B: 7 of 8 racket apps somewhere on the card | glyph + text |
| Fee/price | 3 of 11 (Flap, Frip, 당근), all commerce-like | browse viewers |
| Capacity as text | 7 of 11 (Smash, TennisFriends, Munto, Flap, Band, Greenlight, Spond; Somoim only in its detail sheet) | yes (plus bar) |
| Capacity bar | 0 of 11 (lane B reports none in 19) | yes |
| Status/urgency | 6 of 11 (TennisFriends, Munto, Flap, Greenlight, Somoim, Spond), mostly one chip or one red word; TennisFriends 4 chips, Flap 2 | lead badge + status word |
| Save / heart in row | 4 of 11 (Munto on the photo, Flap, Frip, 당근) | yes |
| Inline fact glyphs | 0-3 per row, median 2 (Smash 1, TennisFriends 3, Munto 2, Flap 2, Greenlight 3, 당근 1-2, Toss/Monzo 0) | 3 emoji + bookmark |
| Avatars of people | 3 of 11 (Munto host, Band stack, Greenlight ring) | none |
| Buttons in the row | 1 of 11 (Heja) | none |
| Objects per row | Smash ~6, TennisFriends ~9, Munto ~8, Flap ~7, Band ~8, Greenlight ~7, Spond ~7, 당근 ~6-7, Toss/Monzo 4 (median ≈ 7) | 12-14 |
5.4 The compact row
Merges and moves:
- Price → gone from the row. Canon already says the fee left the card (
session-card-v2.tsx:53-57); code disagrees (see 7). Pickup fees are settled upfront and club sessions are dues-covered; the detail's cost row keeps it. Where a fee ever decides the tap (public paid pickups), it goes into T3 as a text segment (8,000원), never its own element. - Capacity → the one trailing element.
6/8as T3 text. It becomes a Tag only when a state outranks it, in this order: live (danger+ pulse, "진행 중"), the viewer's standing (success"참석",info"예비",neutral"호스트"), urgency (warning"마감 임박"/"1자리"). One element, never Tag + count (the strip's lead is already one chip,session-strip-ui.tsx:130-157). - Status word and lead badge collapse into that Tag.
- Bookmark → detail header (exists, see the session detail screenshot
bench/ours-phone/sessions_*.png). Fallback: a 22px outline icon over the photo's top-right corner (Munto puts the heart on the photo); it is an action target, so (d) allows it. Needs the owner to overturn the 2026-09-08 "bookmark on every card" ruling (session-card-v2.tsx:67-75). - Capacity bar → detail (the hero's 확정 인원 stat).
- Format + tier → one T3 line of text, no glyphs.
- Court/surface dot, weather, fee, host/role stay off the row.
- Live band: replace the full-width solid ribbon by the
dangerTag in the trailing slot for compact lists; keep the band on the detail anchor (owner decision, the 2026-09-08 carve-out).
The single accent: the trailing Tag when it fires, in its weak tint; default rows have none. The photo is the only other colour. Day-header count anchor and the one brand action stay on the screen, outside the row.
Identity picture: sessions = the venue's own photo, else the map tile in the same 72px box (ledger row 13); clubs = the logo/medallion at 56px; people = avatar 40. One per row.
Line-by-line session row (flat on list screens; the same anatomy in a Card on hubs, as variant: 'embedded' already does):
| Line | Content | Size / weight | Colour | Notes |
|---|---|---|---|---|
| photo | venue photo / map tile | 72 x 72, radius $3 | 1px $borderSubtle | gap 12 to text |
| L1 | 19:00-21:00 | 16/24 · 600 · tabular | $text | one line; ungrouped lists prefix the day |
| L1 right | 6/8 or one Tag | 13/18 · 500 · tabular | $textSecondary / tone | aligned to L1 baseline, flexShrink 0 |
| L2 | venue name | 14/20 · 500 | $text | one line, places may crop |
| L3 | 복식 · NTRP 3.0-4.0 | 13/18 · 500 | $textSecondary | MetaSegments, no glyph |
| frame | row padding 12 vertical, hairline $border below (flat) | height 72 + 24 = 96 px (98 in a card) |
Resulting element count: photo, L1, L2, L3, trailing = 5 (6 with a photo-corner bookmark) against 12-14 today and a peer median of about 7. Height 130 → 96-98 px, so a 600 pt list area shows about 6.2 rows instead of 4.6 (arithmetic, same padding). Inline glyphs 0 (today 3 emoji + bookmark), bars 0, fills other than the photo 0 by default and at most 1 on exception.
Club row: logo 56 · name 16/600 · 서초구 · NTRP 3.0-4.0 · 멤버 42 13/500 · at most one trailing Tag/count = 3-4 elements against 16-21 on the cards; Band, Spond and Meetup group rows are image + name + count with no CTA (lane-b-category.md:75).
6. Falsifiable observations
- Time-first beats venue-first in day-grouped lists. Test: 6 rows per list, 12 participants, tasks "find the 19:00 session" and "find the Olympic Park session"; metric time-to-first-correct-tap. The recommendation fails if venue-first is within 10% on the time task or time-first is more than 20% slower on the venue task.
- Peers lead with the heaviest line in at least 75% of session rows. Observed 8 of 9 (n small). Falsified if a 30-row sample shows below 60%.
- No peer draws a capacity bar in a list row. 0 of 11 here, none reported in 19 (lane B). One counter-example in the remaining 8 apps weakens "bar → detail" but does not reverse it.
- A size step of at least 2 px between line 1 and line 2 survives a 5-10 px blur (NN/g squint test) where today's 14/600 over 14/500 does not. Test with the board's screenshot harness.
- Compact row height 130 → 96-98 px. Verify on the board: render the row at 390 px width and measure; any extra gap token above 4 px between lines breaks the arithmetic.
- Removing the three emoji glyphs does not slow "find the doubles session". No study measures this; the NN/g icon article says only that icons need labels. If a prototype test shows a slowdown, restore one glyph (format), not three.
- Tag count drops. After the migration
grep -c "<Tag"per container: no container above 2;<Badge variant="neutral">fact sites 19 → 0. - One recipe, two meanings today. On day one
no-hand-drawn-labelshould report the 6 label sites (club-photo-seam,club-identity-header,public-club-profile-screen,venue-link,banner-capacity-cluster,ContextChip) and nothing in the pressable ones (ActionPill,post-row,arrival-picker,pickup-host-card,club-album-banner), whichno-hand-rolled-pressable-pillcatches; a different count means the rule or this inventory is wrong. - Seed's
outlineWeakrecommendation ("several selected values may appear at once") applies to us. Wizard multi-select pages with 3+ selected options should read calmer with outline + ink + check than with 3+ mint fills; test on the day-chip section (create-club/step3/days-chips-section.tsx). - A 12px chip label is a smell. Seed (14), M3 (14) and Fluent (14) all use 14px at the 32-40px chip heights; check that
Choiceat 14/500 fits 5-character Korean labels at 360 pt in one row.
7. Caveats
- Primary sources I could not open: m3.material.io and fluent2 design pages, Polaris docs (moved to shopify.dev), Apple's own HIG pages (read through the JSON mirror), Atlassian shape/height values (not on the usage/code pages), TDS pixel values (not published). Where a number comes from tokens/source rather than the designer docs I say so; Polaris
border-radius-200= 8px and Fluent'sborderRadiusMediumare [relay]. M3 badge 6/16 is from_md-comp-badge.scss, confirmed. - Apple
subtitleCellsizes (body / subheadline) are unverified; HIG gives no numbers. - Peer frames are marketing or store images, 222-392 px wide, sizes ±2 pt (
lane-b-category.md:8-11). "Heaviest line" and object counts are my reading of 11-12 rows, not a measured census; the grouped/ungrouped split (4 vs 4) is small and could have confounds (the grouped apps are also the ones that put date blocks in their frames). - NN/g evidence is web desktop (F-pattern 232 users; first-2-words 80 participants on link lists; left bias on 1920x1080 monitors). Transfer to a 390 px Korean list row is inference; Hangul reads left to right, which supports but does not prove it. No source measures chips vs badges, or inline decorative icons vs scan time; finding 6 and observation 6 are design arguments, not results.
- UF thumbnail study gives no sample size and is for video selection; NN/g's Amazon numbers point the other way (0.9 vs 4.4 fixations); what reconciles them is information content, which is why the photo must be the venue's own.
- Doc/code drift found, not fixed: (1)
session-card-v2.tsx:53-61says the fee left the card and the closing line is the meter alone;:473-485renders the lead badge and, for browse viewers, the fee. (2)canon/components.md:29limits fact glyphs on browse cards to boolean facts "never in front of a bare count";canon/components.md:57lets an emoji lead any fact; the card's ⏰ time glyph is neither boolean nor sanctioned by :29. (3)canon/components.md:81still calls PillNav "chip-style tab controls". (4) Board T5's "stroke icons at$text" for fact rows is superseded for rows by ruling (d); the detail fact block's label-plus-icon rows duplicate the label (NN/g: labels are what carry meaning) and are worth the same review. - The bookmark move contradicts an owner ruling (2026-09-08,
session-card-v2.tsx:67-75) and thederiveSessionStrip(model, saved)input; it needs an explicit decision, and the fallback overlay is given. The live-band downgrade also touches a 2026-09-08 carve-out. - Tag sizes 22/26 are my fit to Seed/Fluent/Carbon ranges and the existing 12px text; a 24px single size is equally defensible. The
solidweight 700 is Seed's, not ours today. - Variant and size tallies (
neutral39,xs33, ...) come from a single-line<Badge ...>regex and undercount multi-line tags; file counts in 2.1 are<NameJSX files; a component used only through a wrapper or a spread is undercounted (e.g.GlyphFactRowreaches the session card throughsession-card-v2.tsxonly once). The 19-of-39 "fact" classification is a manual read of the grep listing. - Nothing was modified in the repo; fetched sources and scripts are in
bench/lane_i/.