Skip to content

Owner review 3, lane B: surfaces, rows and labels (2026-10-11) ​

Status: Draft · Date: 2026-10-11 · Evidence and one recommendation per point for owner review 3, points 4 to 9 and the unnumbered chevron paragraph, plus the best-practice audit of library families 4, 5 and 6 (surfaces and content, rows and cells, labels and avatars). Audience: Korean club members aged 40 to 60, on phones, low digital literacy. Every source was opened, rendered or measured on 2026-10-11 (tags in §0). Companions: library, review-1 rulings, review-2 rulings, review-2 lane B. Nothing outside this file was changed.

Four of the owner's points reopen review-2 rulings, so each section says whether the evidence confirms, refines or disputes what is in the library now:

  • Strip (point 5) reverses review-2 ruling 5 (the last column hugs its figure). The owner's direction is followed; the numbers in §4 agree with it.
  • Form strip (point 7) reverses review-2 ruling 6 (the summary sits 12 after the cells).
  • AnswerPair (point 9) refines review-2 ruling 11: below, equal columns stay; the spacing and the button height are what was wrong.
  • 200% text (point 4) refines review-1 §2.4 (trailing elements stack under line 2 from 1.5×): the stack becomes fit-based.

0. Evidence tags and limits ​

TagMeaning
CCode or spec in this repo (path, function).
PPixel or DOM measurement: headless Chromium over CDP on the built boards (docs/public/wireframes/*.html, 390 wide, 1×), or pixel analysis of an F capture.
FApp Store listing frame (app:n, n counts from 0), fetched through the iTunes lookup API on 2026-10-11. A marketing frame of a real build; pt values are ratios.
WPublic web page rendered logged out at 390 × 844.
DDocumentation or source read this session: Material 3 Compose tokens (androidx androidx-main), Apple HIG JSON, TDS docs, KRDS, GOV.UK, Carbon, Atlassian, Seed.
SSearch snippet only. Flagged; no decision rests on one.

Not observed, so no claim is made. Instagram's native profile counts row and follow-request rows (the listing frames show no profile; the logged-out web profile is a login modal over inline "104M followers 90 following"); LinkedIn Premium analytics (the logged-out profile is a modal over inline "41M followers 8 connections"); Naver Blog app stats beyond the mobile web header (centred inline "오늘 3,596 · 전체 157,198,185" and "97,508명의 이웃", W); 당근 profile stats (the public group page shows inline "멤버 1030 · 게시글 2533 · 12분 전 활동", W); Toss stat strips (the listing shows two equal gray tiles with left-aligned text, toss:2, and no strip); Slack and Discord member rows; 밴드, 소모임 and 문토 member and request rows; iOS section-footer metrics. Material 3 and Apple HIG pages are client-rendered, so M3 numbers come from the Compose token source (ListTokens, BadgeTokens, AssistChipTokens, TextFieldImpl) and HIG text from its JSON endpoint; TDS Badge pixel sizes are not published (the package is obfuscated). Marketing frames are not full apps, so peer pt values carry about ±2.

1. At a glance ​

#PointRecommendationEvidenceKit and library
3Chevron and custom codeThe chevron is centred in all four block kinds (verified: 144 → 72, 154 → 77, 160 → 80, 166 → 83, 170 → 85). Twelve custom implementations consolidate onto eight shared pieces (§2): one equal-cell grid, one Cell shell, one trailing builder, one team row, one head action, one note line, one line stack, one glyph fact. About 350 lines go.C, Pflat-v2-kit-content.jsx, flat-v2-kit-controls.jsx (delete FvkPickerCell, FvkCheckRow)
4A lone Tag on its own rowNew law: a Tag, count or level numeral never owns a row. Homes, in order: the trailing slot of line 1; inline after line 1's first fact; a fact in the header subtitle. Match state block: the Tag trails zone A (24 tall), none when the eyebrow is already an action, zone C is deleted, so the block is 144 in every state (170 now; footprint with its line and tail 226 → 168). 200%: stays beside the name unless its longest word and the Tag do not fit.F, D, PFvkCBlockMatch, FvkCBlockFrame (aEnd slot), Tag and Match state block entries, forms CsProv, verifier loneRow
5StripOne grid for both faces: n equal cells of 350 / n, label over figure centred in the cell, no dividers. Fit: each content ≤ its cell and a neighbour gap ≥ 16, else 2-up (an odd last cell spans both columns). Largest empty run in the row 71, 120, 61 (n = 3, 2, 4) against 111, 239, 78 now.P, F, DFvkStrip, fvkCStripFits, Strip entry, lib-surfaces frames
6Caption rowThree roles, not one: state or support line (text only, 8 above the text box), section note (a slot of the Section head, 4 under the title), page legend (12). Align at the rail (291 of 292 measured). Wording rules (one sentence has no final period, two have; no imperatives; one wording per concept) and 32 rewrites of 59 strings (§5.4).C, P, D, FFvkCaptionRow (attach), FvkSectionHead (note), Caption row entry
7Form stripSummary right-aligned on the right rail: x 321 to 370 in Korean, 324 to 370 in English, 15/22·600 ink tabular. It fits at 200% (x 272 to 370, row 44), so the wrap rule goes.PFvkFormStrip, Form strip entry
8PersonRow line 2One config for 16 contexts (§7.4): at most two facts in a fixed order, dropped when equal to the section, the filter or the trailing element; a fact constant down a list moves to the head. context prop on FvkPersonRow. A row with no fact is 64, not 68.C, P, F, WFvkPersonRow, new FVK_PERSON_CTX, PersonRow entry
9AnswerPair and request row12 above, 36 tall buttons (44 target), 12 below; the row is 116 (112 now: buttons are drawn 44 tall with 0 above). Stacked from 1.5×: 8 between.P, F, DFvkAnswerPair, FvkPersonRow (answer), FvkResultRequestRow, AnswerPair entry
10Audit of 43 components6 major drifts, 4 minor, 33 none (§9). The majors are the owner's points 4 to 9.C, P, D§9, §10

2. The chevron, and custom implementations ​

The chevron. The main thread centres it in FvkCBlockFrame; no research here. Verified on the built library board (P): the chevron's centre equals the block's centre in every kind and state at 100% (day 144 → 72 and 166 → 83, ELO 160 → 80, identity 144 → 72 and 154 → 77, match 144 → 72 and 170 → 85), 20 from the right edge, and it clears the Tag proposed for zone A in §3 (Tag right edge x 326, chevron x 334 to 350).

Custom implementations (C). The owner's rule is "no custom implementations, minimise them across all building blocks". The kit already owns the shared primitives (FvkCBlockFrame, FvkCRow, FvkMeta, FvkT, fvkCFx, FvkCell). The table lists each place a kind-specific component re-does layout, hit area or typography that a shared primitive owns, with what to delete. Line counts are the function's length today.

#Where (function, lines)What it re-implementsConsolidate ontoDelete
1FvkPickerCell (controls, 103) and FvkCheckRow (controls, 75), plus the consent, selected and input branches of FvkCell (335)Three row shells for one pattern: a 48 label row inside a group, one trailing slot, the cell hairline, a support line. FvkPickerCell is FvkCell variant="value" with its own padding and hint block; FvkCheckRow kind="radio" is variant="selected"; kind="check" lead is variant="consent".FvkCell only. Split its body into FvkCellShell (hit area, hairline, focus ring, support line) and a trailing-slot resolver; variants become data.FvkPickerCell, FvkCheckRow (keep one-line aliases: 27 call sites each), the duplicate support-line blocks (3 copies), task (= count)
2Equal-cell grids: FvkStrip (130), FvkRoster (125), FvkPodium (100), FvkCalendar (202, three repeat(7, 1fr) grids)Each draws display: grid; repeat(n, 1fr), centres a stack in each cell and wires its own press, hit area and focus. Strip's stat face and action face are two cell implementations in one function.One FvkCCells({ n, cell, gap }): equal columns, centred stack, fvkCFx press, data-hit on the cell. After §4 the Strip is the same cell for both faces.The four inline grid and cell blocks (about 40 lines net), the auto-last branch in FvkStrip
3FvkSectionHead (141): three trailing variants (action, command, expand) and the pagerEach re-draws a 44-high, margin: -8px 0 hit span with its own press style. FvkNotice draws a fourth copy for its action; FvkCell consent draws a fifth ("보기 ›").One FvkCHeadAction({ kind: link | command | expand | text }).4 copies of the hit span (about 45 lines)
4FvkCBlockMatch (111), fvkCtlBoardRow (controls, 25), FvkScoreTable (89)Three "team names plus games" rows: a hand-built flex row on ink, a PersonRow with a group avatar and a 24/32 trailing numeral, and the score table's team column.One FvkCTeamRow({ names, games, onInk }): FvkMeta names, a 32-wide games column. Used by the Match state block, ConfirmSheet, DisputeSheet, DecideSheet.The inline row(t) closure, fvkCtlBoardRow, the duplicate spoken builder
5fvkCPersonTrailing (59), the cluster in FvkResultRow (48), the cluster in FvkMatchRow (controls, 109), FvkCCount, fvkCTrailNodeThree builders of "Level slot, figure, Tag, meta" clusters with different gaps.One FvkCTrail({ level, figure, tag, meta, control, score }).Two of the three builders (about 35 lines)
6FvkCClubHero (93), reached through the kind: 'clubHero' branch of FvkBlockNot a Block (no ink, no zones, no chevron), yet dispatched through FvkBlock, with its own loading skeleton and a logo-plus-name layout that the identity block also draws.Export it as FvkClubHero; compose FvkThumb, FvkT, FvkMeta.The clubHero branch of FvkBlock
7FvkShelf card (170) and FvkMediaRow (134)The card's two lines (headline 600 and body 500 tabular, 4 apart) are MediaRow's L1 and L2 typed again.FvkCLines({ l1, l2, l3 }) shared by both.The card's inline typography (about 15 lines)
8FvkCaptionRow (40), FvkFieldNote (controls, 26), the hint and error lines in FvkCell, FvkPickerCell and FvkField, CsProv (forms board)Five implementations of "a 13px secondary line under an object", with 13/18 in some and 13/20 in others, and spacing chosen per call site.One FvkCNote({ size: line | wrap, attach }) (§5.3); the field's support line and the Caption row are two sizes of it.Four copies; CsProv (§3.6)
9FvkNotice (104): oneLine and body layoutsTwo code paths that each draw the title, the action and the glyph.One layout with flex-wrap: the action trails the title while it fits and drops under the body otherwise.The oneLine branch
10FvkHero facts (107)Glyph, text and Tag rows re-typed; the same glyph-plus-line is the Caption row's glyph form.FvkCFact({ glyph, text, tag }), also used by the Caption row's glyph form and the Cell's locked reason.The inline fact row
11FvkCBlockIdentity (75)Name plus Level tag wrapping logic, also drawn on the Podium column and the Roster cell.FvkCName({ name, level, tag }).Optional; low value
12Page boards: CsProv (forms), DtlRail, local Gap, Head, Fields, BleedBoard-local layout helpers. Only CsProv re-implements a library pattern (a Tag row, §3.6).The kit's FvkRail and Section head spacing.CsProv

The sum is about 350 lines, and it removes the class of drift that this review keeps finding (a spec value in one copy and not in another): the AnswerPair stretch (§8), the 68 against 64 row, and the Caption row's 6/6 are each a second copy of a rule.

3. Point 4: a lone small element on its own row ​

Question. "On the match state block, the tag at the bottom, occupying a single row, is abysmal. We should not let a single, small component take up an entire row, adding to the bulk. Can you ensure this doesn't happen here and anywhere else? Find best practices for it." Where do peers put a status chip on a card or block, and what is the rule?

3.1 Where it happens today (P) ​

Method: for every Tag, Count tag, Level tag and sm Button in the 14 flat v2 boards, a script looks for text, glyph or avatar that shares its line (vertical overlap of at least 40% inside the same frame). A match means the element is alone on its row.

WhereFramesThe elementCost
Match state block, zone C12: 8 on the library board, 4 on 일정 상세 (sc-waiting, sc-hostset, sc-live-confirm, sc-consensus)the state Tag: 24 tall, 38.5 to 80.2 wide (13% to 28% of the 286 content width), alone at y 12624 + 8 above it: block 144 → 170. Then a Caption row (32) and the 24 tail: 226 for one match. In 3 of the 7 states the line restates the Tag (확인 대기 / 상대 팀 확인을 기다려요) and in a fourth it restates the eyebrow.
일정 만들기 (create session) under the legend23 frames draw the row; the Tag is alone in 2 (cs-blank-set 클럽 기본값, cs-pickup 번개), stacks above its text in cs-200, and shares a line with its provenance text in the other 20CsProv: a provenance Tag, with a text such as "토 09:00 올림픽공원" that repeats the 일시 and 장소 Cells below it44 in every one of the 23 (12 above, 24, 8 below)
Podium column1 flagged (lb-overflow)the Level tag under the ELO figurenone: a centred stack draws every element on its own line by grammar. Exempt.
Stacked trailing element at 200%3: home-200 (참석), gm-200 (진행 중), the Hero's 입금 완료the documented stacked layout (library §2.4)4 or 8 above + 36 to 42 per row; §3.5 settles it
The same class with a control: a lone filter Choice (필터 ▾, 전체 ▾, 기간 ▾)85 rows in 82 of the 822 page frames (list A 37, hub 30, list B 12, detail 6)a 36 Choice alone on its row, often followed by a Caption row that explains it. lb-period: Choice 36 + Caption 32 + the 16 gap = 84 before the first titlethe Choice belongs in the first Section head's trailing slot; the caption becomes the Choice's label or the head's note (§5)

The scan's other flags are not instances of the problem. Of 498 lone Buttons on the page boards, 322 are full-width CTAs, 127 are the one action of an empty or error state (다시 시도, 필터 초기화) centred under its text, and 49 are the single action of a sheet, dialog or sign-in frame (기본값으로, 돌아가기, 로그인, 나중에 할게요). None is a status element. Lone Caption rows are the same class as the Tag when they repeat something (§3.3, second rule; §5); a count line under a filter ("47경기 중 4경기") belongs at the trailing end of the Choice's own row.

3.2 What peers do (F, W, D) ​

PeerThe chip or statusWhere it sitsTag
플랩풋볼 match list (flab:1)확정임박, AI 리포트inline after the time on line 1: "20:00 [확정임박] [AI 리포트]". A promo label ("5,000원 할인") has its own row, on the one item that has itF
Flashscore match list (flashscore:0)live minute, BIG CHANCE, PREVIEWthe live minute is a column between the teams and the scores; chips sit inline after a team name or in the right column; never a row of their ownF
Flashscore and Sofascore match header (flashscore:3,6, sofascore:1,5)"Final", "Finished"directly under the score, in the score column, inside the band the two team names already fill (the header does not grow)F
Spond event card (spond:5)"Respond by 12 May, 19:00" (red)one text line under the title; not a chipF
Playtomic match card (playtomic:3)NEW MATCHa label on the card's top edge, outside the boxF
당근 listing (daangn:1)바로구매its own row under the price, on the item that has the feature: the one peer with a lone chip row, and it is a feature label, not a stateF
Toss app (toss:0, toss:5)pill and colour texta state is text on line 2 ("포인트 받기" in blue) or a trailing pill (송금, 내역); no row of chipsF
Material 3 (Compose AssistChipTokens, BadgeTokens)chips and badgesa chip is a 32-tall action or filter in a row of chips, a badge is a 6 dot or a 16 count on an icon; there is no status-label component, so a status is text or a glyph in the card's own linesD
TDS ListRowBadgethe right slot: "Switch, Badge 같은 다른 컴포넌트와 조합할 수 있어요"D
KRDS Badge, Structured list접수 중, 마감됨"near the component"; in a list item the badge area is above the content; one badge per element ("한 번만"); not interactive; at least 4.5:1D
Carbon Tag"Vertically align the tag's container to the other components or text near it"; keep it inside the grid, never hung into the gutterD
GOV.UK Tagadjectives, not verbs; never colour alone; the examples sit in a Status columnD

Reading. In six of the seven apps observed no status owns a row: it is inline after line 1's first fact (플랩풋볼, Flashscore), in the score column (Flashscore, Sofascore), a text line (Spond), text on line 2 or a trailing pill (Toss), or a label on the card's edge (Playtomic). The seventh, 당근, gives a feature chip (바로구매) a row under the price on the items that have it, and 플랩풋볼 does the same for a promo label while its status chip stays inline: a lone chip row is a promotion, never a state. The design systems agree: a Tag is aligned with neighbouring text and appears once per element, and Material 3 has no status-label component at all. The Match state block is the only place in the library where a state owns a row.

3.3 Recommendation: the law ​

A Tag, a count or a level numeral never owns a row. It shares a line with text.

Where it goes, in order of preference:

  1. The trailing slot of line 1. Rows, Cells and Hero facts already do this (library: one trailing element, centred on L1).
  2. Inline after line 1's first fact. A Block's eyebrow row (zone A) takes one Tag at its trailing end; the eyebrow yields first (one line, tail ellipsis), the Tag never shrinks. This is the 플랩풋볼 and Flashscore pattern.
  3. A fact in the header subtitle, for a state of the whole page (a form's provenance: "강남 라켓클럽 | 정기 일정에서").
  4. Never a bottom row, never an eyebrow row of its own above the title.

Exempt: centred stacks (Podium column, Roster cell, avatar-over-label cells), where every element is a line by grammar; and the stacked layout at large text (§3.5).

A second rule keeps the Tag from doubling something else: a Caption row under a Tag must add what the Tag does not (a consequence or a next step). A Tag and a line that say the same thing are one fact drawn twice (KRDS: one badge per element).

Alternatives considered for the Match state block. (a) Merge the state into zone C's meta line ("확인 대기 | 동의하면 이 점수로 확정돼요"): keeps a 22 row (+30) and turns a state into plain white text on ink, less scannable than a Tag, and the Tag set is the one map of a result shared by Match rows, session views and the block (FVK_MATCH_TAGS). (b) Delete the Tag and keep only the line: loses that shared map and the tone cue. (c) Put the Tag on a team row: ambiguous, since the state belongs to the claim, not to a team. (d) A label on the block's top edge, as Playtomic does: a new component with its own overlap rules. Row A's trailing slot is already empty since the chevron moved to the block's middle, and it costs 6 (18 → 24), not 32.

3.4 The Match state block ​

Anatomy (replaces library §4, Match state block). Block grammar, min-height 144 in every state. Zone A, 13/18·600 α0.8: the viewer's next action (점수 입력, 점수 확인) or, when there is nothing to do, the court name; at its trailing end, 8 after the eyebrow, one state Tag (sm, 24) when the eyebrow is not an action. The Tag's right edge is the numerals' right edge (x = content right − 24, clear of the chevron lane). Zone B: two team rows, min 32, 8 apart, names MetaSegments and a 32 column of games once a claim exists. Zone C is deleted. Row A grows from 18 to 24 only when it carries a Tag; 20 + 24 + 8 + 72 + 20 = 144, the same height as the no-claim block, so a claim arriving or changing state never moves the layout.

When the eyebrow is an action the action is the state for this viewer, so the Tag is not drawn (the English "Confirm score" beside "To confirm" says the same thing twice, P). Spoken label unchanged except it omits an undrawn Tag.

StateA: eyebrowA: Tag at the endBLine under the block (Caption row, attach="object")
nothing to do, no claimcourt namenoneteamsnone
점수 입력점수 입력noneteams경기가 끝났어요
점수 확인 (an opponent)점수 확인noneteams and the claim동의하면 이 점수로 확정돼요
waiting (the submitter)court name확인 대기 (info)teams and the claimnone
confirmedcourt name확정 (success)teams and the claimnone
host setcourt name호스트 확정 (neutral)teams and the claimnone; the contest Notice with 이의 있어요 stays, as in the library
disputedcourt name분쟁 (warning)teams, no numerals호스트가 확인하고 있어요
not countedcourt name기록 제외 (neutral)teams, no numerals레이팅에 반영되지 않아요

Of the library's six lines, three go ("상대 팀 확인을 기다려요", "상대가 동의했어요" and "호스트가 확정했어요" repeat their Tag) and one is shortened ("경기가 끝났어요. 점수를 입력해 주세요" repeats the eyebrow, so only its first sentence stays); the not-counted state gains one. English: "The match is over", "It's final once you agree", "The host is reviewing it", "This match won't affect ratings".

Prototype (P). Moving the Tag into row A and deleting zone C in the built DOM: every claim state is 144 (from 170), 200% text is 206 (from 250), English "To confirm" fits beside "Confirm score". Footprint of block, line and tail: 226 → 168 with no line, 196 with one (8 + 20 under the block, then the 24 tail).

3.5 200% text ​

The library stacks a trailing Tag or figure under line 2 from 1.5× (§2.4). That makes a lone-Tag row of every such row at large text, and the boards draw all five of their 200% rows that way (P). The widths say it is not needed in the common case (P, 200% Korean): a 3-syllable name is 77.8 and a 6-syllable name 155.6 (15/22 becomes 30 px, 25.9 per syllable); Tags measure 61 (참석), 103.6 (진행 중) and 112 (확인 대기, 입금 완료); a ResultRow's Tag and score cluster is about 100; the PersonRow text column is 298 (350 − 40 − 12) and the MediaRow column 282. So a name of up to six syllables fits beside a four-syllable Tag (155.6 + 112 + 12 = 280 ≤ 298), and a ResultRow's two 3-syllable names fit beside their cluster (172.6 + 100 + 12 = 285 ≤ 298); a nine-syllable venue (264.5 at 17/24) beside 진행 중 does not. HIG (D) says the stacked layout is for when "inline items … crowd text and cause truncation or overlapping", not a default.

Rule. The trailing Tag or figure stays beside line 1 while the name's longest word + the element + 12 fits the column; otherwise it wraps beneath the text block at the text edge, 4 below line 2 (PersonRow already uses 4; MediaRow's 8 becomes 4: it belongs to the row). A Block's zone A follows the same rule with the eyebrow's first six syllables as the floor. The stack stays as the fallback and the documented exception; nothing else may own a row. Names still wrap and are never cut.

3.6 The create-session provenance Tag ​

Delete CsProv. The provenance is a fact about the page, and the Cells below it already show the values it quotes. Put it in the header subtitle as MetaSegments: 강남 라켓클럽 | 정기 일정에서, 강남 라켓클럽 | 클럽 기본값, and 번개 for a pickup (no club). The blank state's "일시와 장소만 정하면 돼요" is what the soft-disabled primary's press already says. It is drawn in 23 frames (cs-ideal to cs-dark); the Tag is alone in cs-blank-set and cs-pickup.

3.7 Ensuring it stays fixed ​

Add a loneRow check to scripts/verify-flat-v2-boards.mjs: every [data-g=tag|count|level], and every sm [data-g=button] inside a Block or a row, shares a line with text or a glyph; a frame at scale 1.5 or more is reported, not failed, unless the fit test of §3.5 passes; Podium columns and Roster cells are exempt. Ship it with a known-positive canary frame (the pre-fix Match block), because a zero-findings check passes blind. Today it would report 14 at 100% (12 Match state frames, 2 create-session frames) and 3 stacked.

3.8 What changes ​

  • Kit. FvkCBlockFrame: an aEnd node at the trailing end of row A (flexShrink: 0, 8 gap, row min-height 24 only when present). FvkCBlockMatch: draw the Tag through aEnd when result && !action, delete the zone C block, derive the state line from state and action (a small map) instead of a caption string the board passes. fvkMatchTagOf and FvkMatchTag stay.
  • Library. Tag entry gains the placement law; Block entry: "row A may carry one Tag at its trailing end"; Match state block entry: anatomy and the table above; §2.4: the fit-based stack.
  • Boards. lib-surfaces matchstate (8 frames: captions and the Tag position), detail sc-* (4), forms cs-* (23 frames: delete the Tag row, header subtitle).
  • Same class, not specified here. The 85 lone filter Choice rows: the Choice trails the first Section head, the caption becomes the Choice's label or the head's note (§5.3). Choice belongs to the controls family; flagged for lane C.

4. Point 5: the strip ​

Question. "All the cells should occupy the same width, including the spaces. At the moment there is simply too much spacing in the middle, unlike the action face. And would it be better to add vertical dividers or no?" This is the strip's third ruling: review 1 set equal pitch, left-aligned; review 2 let the last column hug its figure; now equal cells like the action face. Follow the owner unless the evidence clearly disputes it. It does not; the numbers below favour it.

4.1 Measurements (P, 390 wide, Korean unless marked) ​

The current strip and the action face on the built library board, then the same cells with the grid and alignment changed in the DOM. "Largest empty run" is the widest stretch of the row with no ink: the biggest gap between neighbouring contents, or the air before the first or after the last.

Grammarn = 3 (총 경기 47, 승률 68%, 출석률 92%)n = 2 (이번 달 참석 12회, 연속 참석 5주)n = 4 (총 경기, 승, 패, 연승)English n = 3
Hug-last (library now, review 2)starts 20, 167.8, 315.5; gaps 110.9, 93.3; no end airgap 239.3gaps 72.3, 77.8, 77.8gaps 101.1, 86.3
Equal cells, left-aligned (review 1)starts 20, 136.7, 253.3; gaps 79.8, 62.2; end air 62.2gap 112.5; end air 126.9gaps 50.6, 56.1, 56.1; end air 65.0gaps 77.1, 62.2; end air 48.1
Equal cells, centred (proposed)content starts 59.9, 167.8, 284.4; gaps 71.0, 62.2; insets 39.9 and 31.1gap 119.7; insets 56.2 and 63.4gaps 53.4, 56.1, 60.5; insets 25.3 and 32.5gaps 69.6, 55.2; insets 38.5 and 24.1
Largest empty run, hug / left / centred110.9 / 79.8 / 71.0239.3 / 126.9 / 119.777.8 / 65.0 / 60.5101.1 / 77.1 / 69.6

The action face (길찾기 · 공유 · 전화): three equal cells of 116.66, glyph centres at 78.3, 195.0 and 311.7 (pitch 116.67), label gaps 88.6 and 94.2, glyph insets from the rail 46.3 each side. So the owner's eye is right: the hug-last strip pins its first content to the left rail and its last to the right rail, which makes the gaps the largest in the row (111 and 93, and 239 for two figures), while the action face centres each content in an equal cell and shows balanced air. The hug rule made the middle gap largest exactly where there are fewest columns.

Equal cells with centred content have the smallest largest-empty-run in every case (71, 120, 61 against 111, 239, 78 now) and leave no air at the end that stands out: the outer insets (25 to 63) are smaller than the inner gaps, so the strip reads as a set inside the rail. The first content now starts 25 to 56 inside the rail. That is inside the margin, so the front-gap law (LM1) holds; it is the same grammar as the action face, the Roster and the Podium.

4.2 What peers do (F, W, D) ​

PeerGrammarVertical dividersTag
Peloton Home (peloton:1)three equal cells, ring, figure and label centrednoneF
Strava workout and activity detail (strava:3, strava:8)2 × 2 grids, label over figure, centred in each halfnoneF
Sofascore win probability (sofascore:7)three figures in equal cells between two logos, centred; the gap between figures is about 1.3 figure widths (ours: 71 / 54.5 = 1.3)1px hairlines between themF
Sofascore box-score header (sofascore:5)PTS, REB, AST centred over their columns1px hairlines between the labelsF
플랩풋볼 player stats (flab:5, flab:6)three equal columns, label over figure, left-aligned: pitch 212 of 640 inner px (33%), so the last figure ends 27% short of the card edgenone between columns (hairlines between rows)F
Garmin Connect (garmin:1, garmin:2)two columns, figure over label, left-aligned in eachnoneF
Toss (toss:2)two equal gray tiles, label over figure, left-aligned inside each tilenone (the tile is the cell)F
UTR (utr:1), NRC (nrc:1), Strava feed (strava:5), earlier laneUTR left-aligned at 0, 35.4 and 71.4%; NRC content-wide with equal gaps, ending 29% short; Strava content-wide with the last column pinned rightnoneF
Toss Securities web header (earlier lane)content-wide groupsa 1px hairline between groupsW
Naver Blog mobile header (m.blog.naver.com)centred inline "오늘 3,596 · 전체 157,198,185" and "97,508명의 이웃": not cellsn/aW
Instagram, LinkedIn (profile counts), 당근 (group counts)not observed as cells: web renders are inline text, native Instagram is not in any listing framen/aW

Reading. Peers split by domain, not by quality: fitness and sports stat grids are left-aligned (플랩풋볼, Garmin, UTR, Toss tiles); tiles, rings and score headers are centred (Peloton, Strava, Sofascore). Equal cells with an empty tail (플랩풋볼, 27%) are the case the owner already rejected in review 2. The evidence neither requires nor disputes centred equal cells; it supports the owner's reading that the strip should be a row of equal cells like the action face, and the table in §4.1 shows centred cells have the smallest empty run.

Dividers. Two kinds of peer draw them: content-sized groups with ragged gaps (Toss Securities), and Sofascore, whose centred figures sit at the same proportion as ours, so its hairlines are a house style rather than a need (it also rules its box-score column heads). Peloton, Strava and 플랩풋볼 draw none. The seam law decides here: delete the line and keep the gap; if the two things still read as separate groups there is no line. With equal cells the fit rule below guarantees at least 16 between neighbouring contents, more than four word spaces (a 13/18 word space is 3.6), and the real gaps are 53 to 71 at 390. So no divider, and no new line type. Contrast is not a constraint either way: the cell hairline (#E5E5E5, 1.27:1 on white) and the edge hairline (#D4D4D4, 1.48:1) are decorative when spacing already groups the cells (WCAG 1.4.11 covers graphics that carry information), and a divider drawn at the 3:1 that would make it a boundary (#8E8E93) would dominate a row of figures. A divider would only help figures squeezed under 16 apart, and the fit test reflows those to 2-up instead.

4.3 The law ​

  1. One grid for both faces. n equal cells (n = 2 to 4), each (content width) / n: 175, 116.7, 87.5 at 390; 109.3 and 82 at 360 (rail 16).
  2. Each cell is a vertical stack, centred in the cell: label 13/18·500 secondary · 4 · figure 24/32·700 ink tabular · an optional delta line (4 + 16), pad 12 above and below. The action face is the same cell with a 24 glyph, 8, and the 13/18·500 label. Both keep the 80 floor (100 with deltas).
  3. No dividers. Equal cells and the minimum gap are what group the columns.
  4. Fit test. Let w be a cell's widest line (its figure, or its label's longest word). Every cell needs w ≤ cell (nothing leaves its cell, so the outer contents stay inside the rail), and every pair of neighbours needs (cell − w₁) / 2 + (cell − w₂) / 2 ≥ 16. A label wraps at its spaces first; a figure never shrinks and never ellipsizes. If either test fails the strip reflows to 2-up: rows of two equal cells, 12 apart, an odd last cell spanning both columns, centred (HIG, D: "reduce the number of columns when the font size increases"). The 40% rule and the auto last column go.
  5. Press. A linked cell presses as a whole (.98/.80); its target is the full cell, 116.7 × 80, larger than before.
  6. Deltas stay plain signs, never red or green, centred under the figure.
  7. Number of cells is the only layout input. The layout depends on n and the text scale and never on the data.

The head-to-head strip (승률 68% | 전적 32승 15패 | 상대 12명) stays three-up under this test: measured contents are 54.5, 109.8 and 52.2 in 116.7 cells, so the neighbour gaps are 34.6 and 35.7. The old cell − 8 rule (108.7) would have reflowed it at 100% by 1.1, which is why the test is the pair gap and not a side margin. The library's reflow frames keep their outcomes: four columns at 130% (the figure is 90.4 in an 87.5 cell) and three columns with units at 130% (117.4 in 116.7) reflow, three columns at 150% (104.4) stay.

Why it stops changing. The review-2 layout moved every start whenever the last figure changed width; this one is a function of n and the text scale alone, shared by both faces. The verifier gets a check: cell widths equal within 1, no content closer than 16 to its neighbour, at 100, 130, 150 and 200% (§10).

4.4 What changes ​

  • Kit. FvkStrip: repeat(n, 1fr) for every face; each cell alignItems: 'center', textAlign: 'center'; two-up with gridColumn: '1 / -1' on an odd last cell; delete the auto branch. fvkCStripFits: replace the body with the two tests above (cell = cw / n, w ≤ cell, the pair gap), drop the 40% rule and the − 8. The Strip becomes the first user of the shared FvkCCells of §2.
  • Library. Strip entry: Anatomy, Fit test, Why (the line "No dividers: the shared left edge and the even starts group the columns" goes) and Replaces (StatStrip, CellRow, ActionIconRow all map to the one primitive, as now). Foundations §1.2: "n equal strip columns" stays true.
  • Boards. lib-surfaces strip: the Rest caption ("even starts, the last column hugs"), the Guide lines at x 20 and 370, the reflow frames' captions (the outcomes hold, the numbers change: 90.4 against an 87.5 cell, not 83 against 79.5, which was an estimate). Every page that draws a Strip follows with no edit (records, club analytics, venue facts, head-to-head).

5. Point 6: the caption row ​

Question. "As for caption row, we should make sure the wording is natural and stylized and aligned and spaced properly." Inventory every use with its Korean and English wording; rewrite each in natural 해요체; give the style, the alignment (rail) and the spacing above and below, with peer and TDS evidence.

5.1 Inventory (P, C) ​

The built boards hold 292 Caption rows in 14 boards, 64 unique strings (59 Korean, 5 English; the legend "* 필수 항목" alone is 103 of the 292). Alignment: the text starts at x = rail (20) in 291 of 292; the one exception is a scaled document frame (10.8). Spacing is one thing everywhere: the row is 32 (6 + a 20 line + 6), and the distance from the previous element to the text box is 6 in every context except the Section head, where the head's own 12 bottom padding makes it 18:

UnderRowsPrevious element to the text boxText box to the next element
a header or its subtitle (page line)9166 + the next element's own padding
a Section head (section note)4118 (12 + 6)6 + 12 to the first row
a Block (state line)1666 + 24 (the block's tail)
a Notice or the offline strip1766 + the next element
a Strip, Cell group or Choice (note)1066 + the next element; 30 above a CTA band

The other 117 sit under a wrapper div (79), a Sheet title row (14) or nothing (5) and measure the same 6.

So three different jobs share one box: a section note sits 18 from its title, farther than the first row is (12), which breaks proximity; a Block's state line sits 6 under a 144 slab; a page legend sits 6 under a header. The wording has the same drift (§5.4): two wordings for one concept, imperatives, a spacing error, a filler label ("안내"), and a caption that is really a title.

5.2 Evidence (D, F, C) ​

SourceWhat it showsTag
TDS ListHeadertitle, description and a right slot are one component; descriptionPosition top or bottom. A section's note is a slot of its head, not a separate row.D
Material 3 TextFieldsupporting text: 4dp below the field, 16dp inset (aligned with the input's text), minimum line 16dp (12sp)D
This library, field grammarsupport line 13/18, 4 under the controlC
HIG lists and tables"Keep item text succinct so row content is comfortable to read"D
Toss (toss:1)a centred 13 to 14px gray caption under the headline: "오늘부터 이자가 또 쌓여요. 내일도 만나요." Both sentences end with a period.F
KakaoTalk (kakaotalk:5)a one-sentence row text, "친구의 생일을 확인해 보세요." with a period; 해요체 throughoutF
Shipped Korean strings (packages/app/src/config/i18n/ko/*.ts), description keysone sentence: 417 without a final period, 52 with; two or more sentences: 25 with a final period, 4 withoutC
docs/design/voice-and-tone-guide.md §3.3, §4no imperative -세요; request with -해 주세요; plain words over 한자어; remove fillerC

The shipped house style is therefore: one sentence, no period; two or more, a period after each including the last. The boards break it in five captions (two sentences, no final period; rows 1, 12, 13, 16 and 17 of §5.4).

5.3 Recommendation: roles, style, alignment, spacing ​

Style. 13/20·500 $textSecondary (the footWrap role: wraps, scales to 200%), at most two lines (31 Hangul syllables per line at 350: 350 / (13 × 0.864); past two lines the copy is rewritten, never clamped). A leading 16 glyph only for a specific meaning (lock, pin), 8 before the text. The labelled form (600 ink prefix) only when the label is a real noun the reader knows (공지); "안내" is filler and goes.

Alignment. At the rail, the object's outer edge, never an inner text edge: under a Block it is x 20, not the block's text at 40. Measured 291 of 292. (Material aligns supporting text with the field's text inset because its fields hold their label inside the box; ours put the label above and every object starts at the rail.)

Spacing, by role. The 6/6 box goes; the caption is a text box with no padding of its own, and the role sets the gap:

RoleWhereAbove the text boxBelowHow drawn
Object linea state line under a Block, a footnote under a Cell group or list, a note under a Choice, a source line8 (the support line's 4, one rung up because the objects are larger)the caller's seam: 24 before a new section (a Block's tail), 12 before more rows of the same sectionFvkCaptionRow attach="object"
Section notewhat a section is or how it is counted ("조는 클럽 순위로 나눈 실력 그룹이에요")4 under the head's title12 to the first row (the head's own bottom padding, which now follows the note)FvkSectionHead note="…" (TDS ListHeader description)
Page linea legend or one-line intro under a header ("* 필수 항목", "항목마다 누가 내 정보를 볼 수 있는지 정해요")12the next element's own seam (32 before a Section head, 12 before a row, 16 before a Field)FvkCaptionRow attach="page"

Effect: a section note moves from 18 to 4 under its title, so the head and its note read as one unit, and each of the 41 notes costs 8 less than the old separate row (head 72 + row 32 = 104, now 32 + 28 + 4 + 20 + 12 = 96); the Match state line is 8 + 20 = 28 (32 now), and three of its six lines go (§3.4).

5.4 Wording: rules and rewrites ​

Rules (the voice guide and the house style, applied to captions):

  1. 해요체, contracted past (됐어요, 바뀌었어요).
  2. One sentence: no final period. Two or more: a period after each, including the last.
  3. A fact or a consequence ("~면 ~돼요", "~예요"). No imperative -세요; a request is "~해 주세요" and only where the person must act.
  4. One wording per concept, everywhere.
  5. Add what the nearby Tag, title or eyebrow does not (§3.3).
  6. Plain words: 변경 → 바뀌었어요, 처리 → drop, 프리셋 → 설정.
  7. Counts as the house style writes them: "47경기 중 4경기", not "4/47경기 표시" (the slash form is for RSVP counts such as 6/8). Two facts are two nodes with the break: "방문 1회 | 한도 3회", never "방문 1회 / 한도 3회".

Rewrites (Korean now → proposed): 32 of the 59 Korean strings; every other caption in the inventory stays. Terms are from terminology-guide.md; the wording is for Korean copy review.

#NowProposedWhy
1경기가 끝났어요. 점수를 입력해 주세요경기가 끝났어요the eyebrow already says 점수 입력; rule 5
2상대 팀 확인을 기다려요 / 상대가 동의했어요 / 호스트가 확정했어요(delete)each repeats its Tag (확인 대기, 확정, 호스트 확정); rule 5
3숫자 아래는 이전 30일과의 차이예요아래 숫자는 이전 30일과 비교한 변화예요one template with "지난달과 비교한 변화예요" (rule 4)
44/47경기 표시, 0/47경기 표시47경기 중 4경기, 47경기 중 0경기rule 7
5검색 결과 제공: 네이버네이버 제공the natural credit line; no label
6방문 1회 / 한도 3회방문 1회 | 한도 3회 (two nodes, the break)rule 7
7계좌번호를 길게 눌러서 복사하세요계좌번호를 길게 누르면 복사돼요rule 3: a consequence, not a command
8납부 문의는 총무에게 연락해 주세요 / 납부 관련 문의는 총무에게 직접 연락해주세요.납부 문의는 총무에게 연락해 주세요two wordings for one concept, a missing space (연락해주세요) and a stray final period
9확인되면 누적 30,000원으로 완납 처리돼요확인되면 누적 30,000원으로 완납돼요rule 6
10각 기록을 인정하면 해당 페널티가 즉시 사라져요기록을 인정하면 그 페널티가 바로 사라져요shorter, spoken words
11총 비용을 참석 인원으로 나눠 1,000원 단위로 올림한 금액이 참가비가 돼요총 비용을 참석 인원으로 나누고 1,000원 단위로 올려서 참가비를 정해요2 lines of 한자어 noun phrases to one active sentence
12불참한 멤버만 ✕를 눌러 표시해 주세요. 나머지는 완료되면 자동으로 기록돼요불참한 멤버만 ✕를 눌러 표시해 주세요. 나머지는 완료하면 자동으로 기록돼요.rule 2 (final period)
13접수 내역이 나타날 때까지 이 화면을 열어 두세요. 앱을 다시 시작하면 작성 중인 내용은 남지 않아요접수 내역이 나타날 때까지 이 화면을 닫지 말아 주세요. 앱을 다시 시작하면 작성 중인 내용이 사라져요.rules 2, 3 ("열어 두세요" is an order and an awkward one)
14확인한 날짜와, 설명이나 공개 링크 중 하나가 필요해요확인한 날짜와 함께 설명이나 공개 링크 중 하나가 필요해요the comma after 와 breaks the phrase
15수정을 시작할 때 불러온 장소 정보가 기준이에요수정을 시작한 시점의 장소 정보가 기준이에요plainer
16테마는 시각 프리셋이에요. 특정 대회나 단체와 관련이 없어요테마는 화면 색을 바꾸는 설정이에요. 특정 대회나 단체와는 관련이 없어요.rules 2, 6
17모든 알림은 알림함에 표시돼요. 아래에서 푸시로도 받을 항목을 켜세요모든 알림은 알림함에 표시돼요. 푸시로도 받고 싶은 항목은 아래에서 켜 주세요.rules 2, 3
18필수 동의는 회원 탈퇴 시에만 철회돼요필수 동의는 탈퇴할 때만 철회돼요spoken: 시에만 → 때만
19카카오로 가입했다면 카카오 로그인을 써 주세요카카오로 가입했다면 카카오 로그인을 이용해 주세요register
20안내 참가비는 시작 전까지 입금하면 돼요참가비는 시작 전까지 입금하면 돼요"안내" is filler; the labelled form keeps real nouns (공지)
21공지 이번 주 토요일 코트가 변경됐어요공지 이번 주 토요일 코트가 바뀌었어요rule 6
22클럽 일정에 참석한 기록이 없어요 / 추천한 멤버가 없어요아직 클럽 일정에 참석한 기록이 없어요 / 아직 추천한 멤버가 없어요the four siblings say 아직; a temporary empty reads as 아직 (voice guide §3.5)
23시행일 2026. 9. 1. / 시행일: 2026. 5. 18.버전: KR-1.0.02026년 5월 18일 시행 | 버전 KR-1.0.0 (two nodes)one format on every document page; colons and the dotted date go (the legal text stays verbatim)
241인당 20,000원, 매월 5일 납부 / 매월 5일 납부1인당 20,000원 | 매월 5일까지two wordings on one page; also "매월 25일" on list A: one sample date in every board
25올림픽공원 테니스장 (a pin caption naming the court of a list)a Section head (the same-court exception names the court once, in the list head)it is the list's title, set at 13/20 secondary

The other 27 Korean strings are natural as written and keep their form (for example "동의하면 이 점수로 확정돼요", "조는 클럽 순위로 나눈 실력 그룹이에요", "숫자를 입력하면 다음 칸으로 넘어가요", "* 필수 항목"). English: sentence case, a period only when there are two sentences, "Notice" label kept only where Korean keeps 공지.

One more: jc-unknown ("코드에 맞는 클럽을 찾지 못했어요") and jc-nocode sit directly under a code field; they are support lines and should use the Field's support line, not a Caption row (13/18 and 4 under, not 13/20 and 8).

5.5 What changes ​

  • Kit. FvkCaptionRow({ text, label, glyph, attach }): attach: 'object' | 'page' sets the top padding 8 or 12; delete minHeight: 32 and the 6/6 padding; the 24 wrapper FvkCBlockMatch draws around the line goes (the Block's own tail does it). FvkSectionHead({ note }): the note under the title at 4, the head's 12 bottom padding after it. One shared FvkCNote({ size: line | wrap }) behind the field support line, the Cell support line and the Caption row (§2 row 8).
  • Library. Caption row entry: Anatomy and the role table; Content: the seven rules and "never a title". Section head entry: the note variant ("title, optional count suffix, optional note"). Foundations §1.4: "caption row" stays footWrap.
  • Boards. lib-surfaces caption (5 frames: add one per role and a Section-head note frame); the 41 notes that follow a Section head become note (hub, list A and B, detail); the 32 Korean strings per §5.4. Verifier: a captionSpacing check (text box 8 or 12 below the previous element, never 6 or 18; x = rail).

6. Point 7: the form strip ​

Question. "In the form strip, why can't we render the summary at the end, not to the immediate right of the cells?" Review 2 put "3승 2패" 12 after the last cell. It can sit at the end; nothing blocks it. Cells start at the rail and the summary is right-aligned at the right rail, like a row's trailing figure. Check it at 200% and in English, with exact values.

6.1 Evidence ​

SourceWhat it showsTag
This library, PersonRow and ResultRowthe trailing figure (15/22·700 tabular) is right-aligned on the right rail; a ranked list right-aligns every figure in one columnC
TDS ListRow right slot; M3 list ItemTrailingSupportingTextFontthe value of a row sits at the trailing end, not beside its labelD
Spond event cards (spond:5)the counterexample: the response counts (✓6 ?4 ✗2) are the last left-aligned text line of the card, adjacent to its text, not trailingF
Strava segment leaderboard (strava:9)time and pace are right-aligned at the trailing end, two lines, before the chevronF
Strava Progress and ATP head-to-head (review-2 lane B)the right value is pinned to the right margin, the left value to the leftF
Flashscore and Naver form chips (review-2 lane B)five chips with no count at all; no peer pairs chips with a trailing count, so this composition is oursF

6.2 Measurements (P, 390, built library board plus DOM prototype) ​

CaseCellsSummary now (12 after the cells)Summary at the endGap, cells to summaryRow
Korean, five cells, 100%20 to 156168 to 216.8 (48.8 wide), 153 empty after321.3 to 370 (48.7 wide)165.324
English "3W 2L", 100%20 to 156168 to 213.5324.5 to 370 (45.5 wide)168.524
Korean, three cells (2승 1패)20 to 100112 to 160.8321.3 to 370221.324
Korean, 200% text20 to 156wraps under the cells (the rule today)272.5 to 370 (97.5 wide, 44 tall)116.544
English, 200% text20 to 156wraps279 to 370 (91 wide)12344

The five cells are 5 × 24 + 4 × 4 = 136 and do not scale. The widest case (Korean at 200%) needs 136 + 16 + 97.5 = 249.5 of a 350 row, and fits a 320-wide frame (content 288) as well. So the summary never wraps up to 200%, and the review-2 line "from 1.5× text the count wraps under the cells" goes.

6.3 Recommendation ​

  • Layout. A row, align-items: center: the cells from the rail (24, r6, gap 4), the summary at the end with margin-left: auto, its right edge on the right rail (x 370), at least 16 between the last cell and the summary.
  • Summary. t().common.recordValue(wins, losses): "3승 2패" (English "3W 2L"), 15/22·600 ink, tabular, one line, never red or green; draws shown only when not 0 (unchanged).
  • Vertical. Centred on the cells. At large text the summary line grows (44 at 200%), the row becomes 44, and the cells (which do not scale) stay centred in it.
  • Fewer than five results. The cells from the rail, the summary still at the end; none: the strip is omitted (unchanged).
  • Spoken. "최근 5경기 3승 2패: 승, 승, 패, 승, 패": the count first, as now.
  • Effect. Right-aligned summaries of stacked strips share one right edge, so the digits align down a page the way the Figure column does.

The trade-off, said plainly. At the end, the empty middle is 165 of 350 (47%), the same kind of gap the owner dislikes in the old strip. It is what a trailing figure always is; if it reads as too empty on the device, the alternative is the Section head's trailing slot ("최근 5경기" left, "3승 2패" right), with the cells alone below. Not recommended: the strip should keep its own summary.

6.4 What changes ​

  • Kit. FvkFormStrip: the container display: flex; gap: 4 becomes cells then the summary with marginLeft: 'auto' and paddingLeft: 16; remove marginLeft: 8.
  • Library. Form strip entry, Anatomy: "…then the count 3승 2패 at the end of the row, right-aligned on the right rail, at least 16 after the last cell, 15/22·600 ink tabular, never wrapping"; delete "From 1.5× text the count wraps under the cells". Accessibility unchanged.
  • Boards. lib-surfaces form (4 frames: the cell positions and the caption; add a 200% frame); detail head-to-head rv-* ("최근 5경기").

7. Point 8: what PersonRow line 2 carries ​

Question. "As for PersonRow, can you organize and configure what optional line 2 carries for its content? It better be suited to each context." Inventory every use and what its L2 carries; research what peers put in L2 of people lists (members, leaderboards, friends, applicants, waitlists, dues, DMs, search); give a context-to-L2 table with exact facts and order, when L2 is omitted, and the trailing element; propose a kit API so boards pick L2 from one config.

7.1 Inventory (P) ​

On the 13 built boards there are 1,079 PersonRows at 100% in Korean. 888 (82%) carry an L2, 191 do not. Of the 888: 838 hold one fact, 39 two, 11 three or more. The review-2 rule ("one context fact or nothing, never a repeat") is kept by the kit only for NTRP beside a Level tag; the rest is whatever a board passes. What the boards pass:

Context (frame prefix, rows)What L2 carries todayProblem
멤버 관리 (mb-, 107)"A조 | 신뢰 멤버", "B조 수동 | 신뢰 멤버", "C조"role is the section head (fine); the trust word is a second fact on 42 of the 107 rows
심사 대상 in 멤버 관리"공개 일정 5회 | 매너 4 | 함께 6명"three facts; no application time
회비 (du-, 94)"9월 회비" on 23 rows, "9월 18일 입금 완료 신청", "일부 납부 | 잔액 20,000원"the month is the page's filter: a constant on every row
참가비 (fe-, 41)"9월 17일 확인", "9월 18일 21:30 신청"fine: a state and its time
단톡방, 멤버 초대 (ng-, am-, 92)"강남 라켓클럽" (40), "친구" (24)two values repeated down one mixed section (멤버 8명): the relation is a grouping, not a row fact
친구 (fr-, af-, 57)"ELO 1,482 | 서초구", "서초구"a rating as text beside no Level tag; two different shapes in one list
경기 기록 (hx-, 60)"9월 17일 (목) | 복식 | ELO +12"three facts
상대 전적 (rv-, tn-, 45)"최근 5경기 2승 3패" on all 16 rows of 전체 상대one string on every row
메시지 요청 (ms-, 14), 알림 (nt-, 73)"10분 전 | 안녕하세요, 같이 복식 치실래요?", "강남 라켓클럽 | 이서연님"fine; the time sits in L2 here and in the trailing meta in threads
리더보드 (lb-, 74)nonefine
any row with no L2row is 68the library says 64 (12 + 40 + 12); the kit's minH is 68 for all 191 rows

7.2 What peers carry (F, W) ​

ContextPeerL2 (and L3)TrailingTag
Group members당근 모임 (public group page)L2 region (동); L3 one line of the member's own intro; role shown only as a crown on a fewnoneW
FriendsKakaoTalk 친구 (kakaotalk:5)a context line: "오늘 · 내게 생일 선물 준 친구", or the last-contact word "어제"one tonal pill (선물하기)F
RequestsFacebook (facebook:9)the relationship under the sentence: "6 mutual friends"none: Confirm and Delete belowF
Requests, searchLinkedIn (linkedin:5)L2 headline; L3 "3 mutual connections"none: Accept and Ignore belowF
ParticipantsSpond recipients (spond:2)none: avatar and name only, grouped under Attending, Waiting list, Not answered, DeclinednoneF
Match rosterPlaytomic (playtomic:3)none: name under the avatar and a level pillnoneF
LeaderboardStrava segment (strava:9)the date of the efforttime and pace, right-aligned, two lines, chevronF
ThreadsMeetup (meetup:4), KakaoTalkthe last message, one line, croppedthe time (trailing), an unread dotF

Reading. L2 answers the question the list exists for. Where the question is "who is this and why are they here" (friends, requests, search, suggestions) L2 is a relationship or a reason ("N mutual", "내게 생일 선물 준 친구"); where it is "what did they do" (threads, leaderboards) it is the activity; in a pure roster or a ranking, where rank, level and figure already answer, there is none (Spond, Playtomic). Peers never put a role the section already names, a constant, or the trailing figure's twin in L2. Not observed: Slack, Discord and 밴드 member rows, dues rows (Toss 모임통장 and Venmo have no listing frame), waitlist rows.

7.3 The rules ​

  1. L2 answers the list's question. One config per context (§7.4).
  2. At most two facts, one line (an extra quote under L2 is the one allowed third line, for a report or a request message). The first fact is the decisive one.
  3. Never a repeat. A fact is dropped when it equals the section head, the filter value, the trailing element or the Tag; NTRP is dropped beside a Level tag (already in the kit).
  4. A constant is not a row fact. A fact that is the same on every row of a list moves to the head (a Section head count or note, or the filter).
  5. The same kind of fact on every row of a list, so the eye can scan down it; a row that lacks the fact omits L2, it does not substitute a different one.
  6. No L2, no space: a row with no fact is 64 (12 + 40 + 12), the name centred on the avatar.

7.4 The configuration ​

omit says when L2 is absent. Facts are in order.

ContextWhereL2 facts (max two, in order)Omit whenTrailing
members멤버 관리, 멤버 목록조 (the group, with 수동 as part of it), 신뢰 등급no 조 in the club and the default tier⋯ control (officials); else none
participants참석자 (the full list)nonealwaysLevel tag; one Tag (호스트 > 게스트 > 친구 > 예비)
waitlist예비예비 N번never (it is the position)Level tag
applicants가입 신청, 게스트 신청, 심사 대상신청 시점 ("10분 전 신청"), then the one decisive basis (함께 N명 > 매너 N > 공개 일정 N회)no basis data: the time aloneAnswerPair below; Tag 신청 or 지명 when mixed
suggestions추천할 만한 게스트, 친구 추천the reason, one ("함께 8회", "실력이 비슷해요", "신뢰가 높아요")neverone button (추천, 친구 추가)
dues회비 (the member's own and the ledger)the state with its date ("9월 17일 입금 완료 신청"), 잔액the section already says it (완납, 미납) and nothing variesa select checkbox in Select mode; a figure only for a partial balance
fees참가비the state with its time ("9월 18일 21:30 신청")the section says itamount or Tag
ranking리더보드none; in 지역 and 전국 scopes, the club namethe 우리 클럽 scoperank column, Level slot, ELO figure
friends친구 목록지역no region setLevel tag; a row control (친구 삭제)
requests친구 요청, 메시지 요청시점, then the first message line or the 지역no messageAnswerPair below
threads메시지 목록the last message, one line, cropped (the sender's name only in a group thread)nevertime (13/18 secondary); unread dot in the marker slot
opponents상대 전적최근 5경기 N승 N패 (varies by row); a callout label first (라이벌, 천적, 최다 대결, 최고 승률)fewer than 5 matches: count what existsthe overall record (figure)
results경기 기록, a session's 경기the date (history); round, court and the winners (a session view)neverTag and score
picker단톡방, 멤버 초대none; a friend who is also a member shows the shared clubthe grouping already names the relationcheck
searchpeople results클럽, 지역 (disambiguators only)one matchnone
reports신고 (officials)신고 날짜, 분류; the quoted reason as extra (two lines, clamped)nevernone

From today to proposed, on the real boards:

FramesTodayProposed
du-* (94 rows)"9월 회비" on 23 rowsno L2 on rows the section already states; the month stays in the filter
ng-*, am-* (92 rows)"강남 라켓클럽" and "친구" on every rowsplit the section by relation (클럽 멤버, 친구), the heads say it; L2 none (64 rows)
fr-*, af-*"ELO 1,482 | 서초구""서초구" and a Level tag; the rating is in 랭킹 and the profile
심사 대상 (mb-*)"공개 일정 5회 | 매너 4 | 함께 6명""10분 전 신청 | 함께 6명"
hx-* (60 rows)"9월 17일 (목) | 복식 | ELO +12""9월 17일 (목) | ELO +12"; 복식 and 단식 are the filter's values
rv-* (전체 상대, 16 rows)"최근 5경기 2승 3패" on every rowthe per-row form, or none until the data differs
any row with no fact6864

7.5 The kit API ​

One table, one resolver; boards pass data and a context, never L2 strings.

js
// flat-v2-kit-content.jsx
var FVK_PERSON_CTX = {
  members: { l2: ['group', 'trust'], trail: 'control' },
  participants: { l2: [], trail: 'level+tag' },
  waitlist: { l2: ['position'], trail: 'level' },
  applicants: { l2: ['when', 'basis'], trail: 'answer', extra: 'message' },
  suggestions: { l2: ['reason'], trail: 'button' },
  dues: { l2: ['state', 'balance'], trail: 'check|figure' },
  fees: { l2: ['state'], trail: 'figure|tag' },
  ranking: { l2: [], trail: 'rank+level+figure', clubWhen: 'scope !== club' },
  friends: { l2: ['region'], trail: 'level|control' },
  requests: { l2: ['when', 'preview'], trail: 'answer' },
  threads: { l2: ['preview'], trail: 'meta', marker: 'unread' },
  opponents: { l2: ['callout', 'form'], trail: 'figure' },
  results: { l2: ['when'], trail: 'tag+score' },
  picker: { l2: [], trail: 'check' },
  search: { l2: ['club', 'region'], trail: null },
  reports: { l2: ['when', 'category'], trail: null, extra: 'quote' },
};
// <FvkPersonRow context="applicants" person={{ name, photo }} data={{ when, basis, message }} />
// fvkPersonL2(context, data, locale) -> [fact, fact] (max 2; drops what equals data.head, data.filter or the trailing)
  • FvkPersonRow keeps its low-level props (l1, l2, figure, level, tag, meta) for the Text row and one-offs; context resolves them from the table.
  • minH is 64 when fvkPersonL2 returns nothing and there is no extra or answer.
  • The verifier adds personL2: at most two facts per row; no L2 string identical on 3 or more rows of one frame; no L2 equal to the nearest Section head's text; rows without L2 measure 64.

7.6 What changes ​

  • Kit. FvkPersonRow (minH, context, data), new FVK_PERSON_CTX and fvkPersonL2; FvkResultRow and FvkResultRequestRow become context="results" and "requests".
  • Library. PersonRow entry: replace "L2 … one context fact" with the six rules and the table of §7.4; the Anatomy line "Without L2 the row is 64" becomes true in the kit.
  • Boards. Migrate frame by frame as boards are rebuilt: du-*, ng-*, am-*, fr-*, af-*, hx-*, rv-* first (the constants and the three-fact rows above).

8. Point 9: AnswerPair and the result request row ​

Question. "In the AnswerPair, if we are rendering the buttons below, shouldn't there be more proper spacing above and below the buttons? Can you inspect and use best practices? The same goes for result request rows because they have almost exactly the same composition." Measure the gaps in both; research Facebook friend requests, Instagram follow requests, LinkedIn invitations, 당근, Kakao and Material 3 list and card action spacing; recommend exact values for the gap from line 2 to the buttons, from the buttons to the row's bottom edge, and the row pitch.

8.1 Measurements (P) ​

Every request row on the built boards measures the same (library AnswerPair, Result request row, list A ms-requests and mb-* 심사 대상, list B fr-*, detail sc-results):

ItemLibrary saysDrawn
Rowgrows by 44112 = 12 + 22 (L1) + 4 + 18 (L2) + 44 (pair) + 12
Button heightsm 36, 44 target by slop44 (the kit stretches the sm Button to its container: alignItems: stretch, minHeight: 44)
Line 2 to the buttons80 from the line box (2 from the glyph box)
Buttons to the row's bottom edgenone stated12
Buttons to the next row's name26 (12 + the next row's 12 + 2)
Columns(text column − 8) / 2145 + 8 + 145 = 298 at 390 ✓
200% stackedstacked, 8 apart58 + 8 + 58 = 124; row 312

Seen on the board: two gray 44-tall slabs touching the line 2 text, then 12 of air, then the next row. The spacing is 0 above and 12 below, and the buttons are bulkier than the spec. The owner's eye is right on both counts.

8.2 What peers do (F, D) ​

Pixel analysis of the two listing frames; screen width 663 px = 390 pt, so ×0.588 (±2 pt).

SourceGap, last text line to the buttonsButtonGap, buttons to the row's endTag
Facebook friend request (facebook:9)29 px from the baseline, about 12 pt from the line boxtwo equal columns, 239 px each, 63.5 px = 37 pt tall, gap about 9 pt22 px = 13 ptF, P
LinkedIn invitation (linkedin:5)36 px from the last ink = 21 pt, about 16 pt from the line boxpills at auto width (Accept 135 px = 79 pt), 55 px = 32 pt tallnot in the frameF, P
Material card, MDC Android reference layoutcontent padding 16 plus the action row's margin 8 = 24text buttons, 8 apart8D
Material list item, Compose ListTokensn/a: a list item holds no button row; vertical padding 8, one-line 56, two-line 72, three-line 88D
Instagram follow requests, 당근, Kakao requestsnot observed in any listing frame; one SEO page describes Instagram's "Confirm" beside "Delete", which would be a trailing pair (S, unverified)S

Reading. The two peers whose request rows carry a pair of answers put them in the text column under the text with 12 to 16 above, about 13 below, buttons 32 to 37 tall. The card reference leaves 24 above. None draws them touching the text, and none draws buttons as tall as the 44 the kit does. 36 is the library's sm Button; its 44 target is the hit slop, which the kit already draws.

8.3 Recommendation ​

ValueNowRecommended
Line 2 line box to the buttons012 (Facebook about 12, LinkedIn about 16, card 24; 12 is the grid's row-padding step and the one the row already uses on top)
Buttons4436 tall, r10, tonal, hit slop 4 above and below so the target is 44; equal columns of 145, gap 8 (unchanged)
Buttons to the row's bottom edge1212 (the row's own bottom padding: the pair sits in 12 / 36 / 12)
Next row's name below the buttons2624 (12 + the next row's 12): twice the 12 above, so the pair groups with its own row, not the next
Row with L2112116 = 12 + 22 + 4 + 18 + 12 + 36 + 12
Row with no L2 (출석 확인)n/a94 = 12 + 22 + 12 + 36 + 12
200% or a label + 32 over its halfstacked, 8 apartstacked, 8 apart, 12 above and below as at 100%; yes on top. The 200% row: 12 + 58 + 8 + 58 + 12 under L2

The Result request row is the same component (FvkResultRequestRow is a FvkPersonRow with the pair), so it takes the same numbers: L1 "김민준 님이 6-4를 입력했어요" 15/22·600 (wraps), L2 MetaSegments("3라운드 2번 코트", "김민준, 이서연 승"), then 12, the 36 pair, 12. A bare row tap still never answers.

One decision for the owner. 36 matches the library's sm Button and the peers (32 to 37), and the 4 slop makes the target 44 (HIG, D). For this audience a taller visual button is a defensible choice: the md Button (44) with the same 12 and 12 gives a 124 row, 8 more than 116. The recommendation is 36 because the 44 slabs are what read as bulk on the board; say if the larger visual is wanted.

8.4 What changes ​

  • Kit. FvkAnswerPair: remove minHeight: 44 and alignItems: 'stretch'; the container is display: flex; gap: 8; marginTop: 12 with flex: 1 cells; the sm FvkButton keeps its own 36 and its 4 slop. FvkPersonRow: nothing else; leadTop: 2 stays. FvkResultRequestRow follows with no edit.
  • Library. AnswerPair entry, Anatomy: "…two sm (36) tonal Buttons in two equal columns of (text column − 8) / 2 (145 at 390), gap 8, 12 under line 2, with the row's own 12 below; the row grows by 48 (12 + 36); stacked from 1.5×…". Result request row entry: "the row grows by 48".
  • Boards. lib-rows answer (4 frames), lib-social request row (3 frames), list A ms-requests, mb-*, list B fr-*, detail sc-results: all follow the kit. Verifier: an answerPair check (12 ± 1 above, 36 tall, 12 ± 1 below).

9. Audit of families 4, 5 and 6 ​

Each component against current best practice and against what the kit draws. Major: visible to the owner or against a ruling, needs a design change. Minor: a spec or kit mismatch, or a missing rule, with no visible harm yet. None. References are tagged as in §0; "M3" values are the Compose token source. Code-structure findings (duplicate implementations) are in §2, not rated here.

9.1 Surfaces and content (library §4, 20 entries) ​

ComponentCurrent specBest-practice referenceDriftFix
Blockr16, pad 20, min 144, zones 18 / 48 / 22 (8 apart), one centred chevron, press .98/.90M3 filled card shape is Medium, 12 (D); Toss and Strava summary cards (toss:2, strava:6, F)nonenone (16 is the Toss-like step above M3's 12)
Block, 홈 dayA day label, B count, C two factsGarmin Connect's Today's Activity card (garmin:0, F)nonenone
Block, ELO160, bar 8 r4 on a white α0.2 trackStrava Progress "384.6 mi, +12%" over its chart (strava:6, F)nonenone
Block, identity144 to 154, avatar 56, name 24/32 wraps, Level tag inlineLinkedIn and Meetup profile headers (linkedin:3, meetup:5, F)nonenone
Match state blockTag in zone C: 170, then a 32 line and a 24 tail (226)Sofascore and Flashscore status in the score column (F); 플랩풋볼 chip inline after the time (F); KRDS one badge per element (D)major§3: Tag in row A, none on an action, 144 always, 3 state lines
Club herologo 56 on the band edge (28 over, 28 under), 2 ring, name 24/32LinkedIn 51%, X 50%, Strava 48% (review 2)nonenone
Striphug-last grid; stat face left-aligned, action face centred§4: peers split by domain; the numbers favour equal centred cellsmajor§4: one equal-cell grid, centred, no dividers
Hero (detail)top 24, eyebrow, title 24/32, up to three 24-tall glyph facts, one TagMeetup event page: title, then calendar and pin glyph rows (meetup:3, F)nonenone
Noticer12, pad 12 16, title 15/22·600, body 16/24, five tones, four tiers, no default glyphSeed Callout: title, description, icon; outline, normal, info, warning, danger (D); WCAG 4.1.3 status messagesnonenone (no default icon is the review-1 ruling)
Caption row13/20·500 secondary, min 32 with 6 above and below, one box for three rolesTDS ListHeader description slot (D); M3 supporting text 4dp (D); Toss and Kakao captions end with periods (F)major§5: roles with 8 / 4 / 12, note slot, wording rules
Bandfull-bleed 12 gray100, 24 of air above and belowTDS Border height16 section separator (D)nonenone (12 is a lighter step than TDS's 16)
Chart160, inset 16, one 2px ink line, scrub, spoken summaryStrava Progress: a summary figure above the chart (F); WCAG 1.1.1nonenone
Activity heatmap12 × 12 r3, gap 4, five steps, legend, text equivalentWCAG 1.4.1 and 1.4.11: the text equivalent carries the value, so the steps are exemptnonenone
Calendarmonth 300; day cell 50 × 44, circle 36, today ring 1.5, header 48, weekday row 32M3 date picker: date container 40 × 40, today outline 1 (D)nonenone (target 50 × 44 clears 44; 36 against 40 is a density choice)
Podiumthree equal columns, 168 floor, 2px medal rings, Level tag alwaysStrava leaderboard marks rank 1 with a crown in the rank column (strava:9, F); no three-face podium measurednonenone
Form stripfive 24 cells, then the summary 12 after them, wrapping under at 1.5×§6: trailing figures sit at the end (Strava, ATP); Spond keeps counts adjacent (F)major§6: summary at the right rail; the wrap rule goes
PhotoGridthree columns, gap 2, square 128.7, +N tileno peer measurednonenone
Shelfcard 240, tile 140, cell 76; the next item peeks; five then a doorM3 carousel and HIG collection views: the next item peeks (D, concept); review-1 lane C (eight peers)nonenone
Brand lockupstacked 180, horizontal 28, clear space 1un/anonenone
Thumb56 r8 (club r12), compact 48M3 list leading image 56 × 56 and avatar 40 (D); Seed avatar sizes (D)nonenone

9.2 Rows and cells (library §5, 16 entries) ​

ComponentCurrent specBest-practice referenceDriftFix
MediaRowmin 80, pad 12, thumb 56 + 12, L1 17/24, L2 15/22, L3 13/18; trailing stacks from 1.5×M3 two-line 72, three-line 88, image 56, leading 16, top and bottom 10 (D); 플랩풋볼 puts the status chip after the time on line 1 (F)minor200%: stack only when it does not fit (§3.5); the density steps (12 against 16) are a choice
PersonRowmin 68 (64 with no L2 in the spec, 68 in the kit); L2 optional, "one context fact"M3 two-line 72, avatar 40 (D); KakaoTalk, Facebook, LinkedIn, Spond, Strava L2 by context (F)major§7: 16 contexts, two facts at most, constants to the head, 64 with no fact
Text rowthe PersonRow text block with no media, min 68TDS ListRow 2RowTypeA: top and bottom text, right slot (D)nonenone
Cellmin 48 (68 two-line), label 15/22·400 secondary, value 15/22·500, 17 variants in one functionHIG 44 target (D); M3 one-line 56 (D); TDS ListRow (D)minorone FvkCell shell; delete FvkPickerCell and FvkCheckRow (§2 row 1)
Day headera 22 line box, 24 above and 12 below, sticky with an edge hairlinemeasured peers 33.8 above and 16.8 below, ink to ink (review-1 lane A); this ladder gives 37 and 17nonenone
Section headmin 28, title 20/28·700, 32 above and 12 below, trailing action with a 44 targetTDS ListHeader: title, description, right slot (D)minoradd note (§5.3)
ExpandTogglea 48 lone row stating its count; or a disclosure CellHIG disclosure controls; the chevron turnsnonenone
Rosterfour equal cells 87.5, avatar 48, name, Level tag; +N door; empty slots not drawnPlaytomic match row: four avatars with names and level pills, open slot drawn as "+ Available" (playtomic:3, F); Spond: names only (F)nonenone (we do not draw open slots: one coloured action per screen)
Figure columnright-aligned tabular figures; rank column 20 / 28 / 40Strava leaderboard: rank column, right-aligned time and pace (strava:9, F)nonenone
Row trailing control20 glyph in a 48 target, or an sm Button, centred on the rowM3 trailing icon 24 (D); KakaoTalk pill (kakaotalk:5, F)nonenone
Leading marker slot8 dot + 8 inside the rail, text edge 36M3 small badge 6 for icon dots (D)nonenone
SwipeActionone 72 action, an accelerator onlyn/a: the spec requires a non-gesture path, as WCAG 2.5.7nonenone
Select modeheader 취소 / n명 선택됨 / 전체 선택; 24 checkbox slotHIG lists: people enter an edit mode before selecting rows (D)nonenone
ResultRowa PersonRow with L2 the date and the Tag plus score at the endStrava leaderboard trailing cluster (F)nonenone (the cluster builder is shared in §2 row 5)
Task Cella Cell with an ink label and a count Tag before the chevronKRDS structured list: consistent classification and order (D)nonealias of the count variant
AnswerPairsm 36 tonal buttons, 8 under L2 (spec); drawn 44 tall with 0 aboveFacebook 12 above, 37 tall; LinkedIn 16 above, 32 tall; Material card 24 above (F, P, D)major§8: 12 above, 36 tall, 12 below, row 116

9.3 Labels and avatars (library §6, 7 entries) ​

ComponentCurrent specBest-practice referenceDriftFix
Tagsm 24 r6 13/18·600 pad 8; md 28; one per row, two per object; weak and solid, 6 tonesKRDS Badge: one per element, at least 4.5:1, not interactive, near the component (D); TDS Badge sizes and fill / weak (D); GOV.UK: adjectives, never colour alone (D); Carbon: align with nearby text (D)minoradd the placement law (§3.3); nothing in the spec forbids a Tag on its own row
Level tag24 ball circle, 13/18·700 ink; grows to 36n/anonenone
Count tag20 tall, numeral 11/16·700; 16 with a 1.5 ring on tabs and header icons; 99+M3 badge large 16 and small 6 (D); KRDS number badge "999+" (D)nonenone (99+ is a choice; the 20 row count is larger than M3's 16 on purpose)
Live tagdanger solid, 6 pulse dot for 5 s, then restWCAG 2.2.2 pause, stop, hidenonenone
Unread marker8 circle inside the rail; 6 on an icon or tabM3 small badge 6 (D)nonenone
Avatar32 / 40 / 48 / 56, initials never scale, group as a diagonal pair, anonymous, presenceM3 avatar 40 (D); Seed 36 / 48 / 64 and group overlap −10 / −8 (D)nonenone (the diagonal pair is our own convention, documented)
Medal setfour tiers with on-colours, earned and locked tilesWCAG 1.4.1: the tier is in the namenonenone

9.4 Counts ​

43 components: 6 major (Match state block, Strip, Caption row, Form strip, PersonRow, AnswerPair), 4 minor (MediaRow, Cell, Section head, Tag), 33 none. The six majors are the owner's points 4 to 9; the four minors are the supporting rule changes those points need.

9.5 Spec against kit, found by measuring ​

Four places where the library says one thing and the built kit draws another (P):

ComponentLibraryKit drawsFixed by
AnswerPairbuttons 36, 8 abovebuttons 44, 0 above§8
PersonRow64 with no L268 with no L2 (191 rows)§7
Caption rowone idea, 32 minthe same 6 / 6 box for three roles§5
Match block"taller with a claim"170, a Tag row of 24 + 8§3 (144 always)

10. Fix list, in implementation order ​

Grouped by file, naming functions. Do the shared primitives first, so each point fix builds on them. Nothing here is committed; the main thread applies it.

10.1 docs/twomore_design_system/wireframes/flat-v2-kit-content.jsx ​

  1. New private primitives (§2): FvkCCells({ n, cell, gap }), FvkCHeadAction({ kind }), FvkCTeamRow({ names, games, onInk }), FvkCTrail({ level, figure, tag, meta, control, score }), FvkCNote({ size, attach }), FvkCLines({ l1, l2, l3 }), FvkCFact({ glyph, text, tag }).
  2. FvkStrip, fvkCStripFits (§4): repeat(n, 1fr) for every face; centred cells; two-up with an odd last cell spanning; w ≤ cell and a pair gap of 16; delete the auto branch and the 40% rule.
  3. FvkCBlockFrame, FvkCBlockMatch (§3): aEnd slot in row A; the Tag through aEnd when result && !action; delete zone C; derive the state line from state and action; FvkBlock drops the kind: 'clubHero' branch.
  4. FvkFormStrip (§6): the summary at the end (marginLeft: 'auto', paddingLeft: 16).
  5. FvkCaptionRow, FvkSectionHead (§5): attach: 'object' | 'page' (top 8 or 12, no 6/6, no minHeight); note on the head (4 under the title, 12 after).
  6. FvkAnswerPair (§8): remove minHeight: 44 and alignItems: 'stretch'; marginTop: 12; the sm Button keeps 36 and its 4 slop.
  7. FvkPersonRow (§7): minH 64 when there is no L2, extra or answer; context and data with the new FVK_PERSON_CTX and fvkPersonL2; fvkCPersonTrailing and the FvkResultRow cluster become FvkCTrail.
  8. FvkCell (§2 row 1): split into FvkCellShell and a trailing-slot resolver; FvkTaskCell becomes an alias of count.
  9. FvkCClubHero exported as FvkClubHero; FvkNotice one layout; FvkHero facts through FvkCFact.
  10. FvkRoster, FvkPodium, FvkCalendar onto FvkCCells; FvkShelf card lines through FvkCLines.
  11. FvkTag and FvkMediaRow: no code change; the placement law and the fit-based stack (§3.3, §3.5) go in the library, and FvkCRow's stacked branch (stacked && trailing) becomes a fit test on the name's longest word.

10.2 flat-v2-kit-controls.jsx ​

  1. Delete FvkPickerCell and FvkCheckRow (keep one-line aliases to FvkCell: 27 call sites each); FvkMatchRow trailing through FvkCTrail; fvkCtlBoardRow and the team column of FvkScoreTable through FvkCTeamRow; FvkFieldNote through FvkCNote.

10.3 flat-v2-kit-chrome.jsx ​

  1. FvkHeader: sub accepts an array drawn with FvkMeta (lines={1}), for the create-session subtitle "강남 라켓클럽 | 정기 일정에서". Cross-lane note for lane A.

10.4 Library, docs/design/flat-v2-component-library-2026-10-09.md ​

  1. Entries: Strip (Anatomy, Fit test, Why), Match state block (Anatomy, Variants table, State line), Form strip (Anatomy), Caption row (Anatomy, roles, Content rules), Section head (the note variant), PersonRow (L2 rules, §7.4 table), AnswerPair and Result request row (spacing), Tag (placement law), Block ("row A may carry one Tag"), §2.4 (the fit-based stack), §1.3 spacing ladder (add "12: pair above its row's text").
  2. Decision log: reverses review-2 rulings 5 and 6; refines 11 and review-1 §2.4; adds the lone-row law and the caption roles.

10.5 Boards ​

  1. lib-surfaces: strip (captions, guides, the 130 / 150% reflow captions), matchstate (8 frames), caption (frames per role and a Section-head note), form (summary position, a 200% frame).
  2. lib-rows: answer (4), personrow (add a context frame set and a no-L2 row).
  3. lib-social: the Result request row (3 frames).
  4. detail: sc-* (4 Match frames), rv-* (form strip, strip), sc-results.
  5. forms: delete CsProv in the 23 cs-* frames and use the header subtitle; ng-*, am-* pickers (constant L2).
  6. list A and B, hub, detail: the 41 section notes become note; the 32 Korean strings of §5.4; PersonRow migrations of §7.6.

10.6 scripts/verify-flat-v2-boards.mjs: new checks, each with a known-positive canary ​

  1. loneRow (§3.7), stripEqual (cell widths within 1; no content under 16 from its neighbour, at 100, 130, 150 and 200%), captionSpacing (text box 8 or 12 below the previous element; x = rail), answerPair (12 ± 1 above, 36 tall, 12 ± 1 below), personL2 (§7.5).

Appendix A. Listing frames used (F) ​

Frames come from the screenshotUrls of https://itunes.apple.com/lookup?id=ID&country=CC; index n counts from 0. Version and release date as of the lookup on 2026-10-11.

KeyListingID (CC)Version, released
facebookFacebook284882215 (us)582.0.0, 2026-10-07
linkedinLinkedIn288429040 (us)9.1.579, 2026-10-07
sofascoreSofascore1176147574 (us)26.09.28, 2026-10-05
flashscoreFlashscore766443283 (us)26.10.1, 2026-10-06
stravaStrava426826309 (us)484.0.0, 2026-10-07
spondSpond755596884 (us)4.63.3, 2026-09-24
meetupMeetup375990038 (us)2026.10.8, 2026-10-09
playtomicPlaytomic1242321076 (us)6.93.2, 2026-10-07
pelotonPeloton792750948 (us)15.134.0, 2026-10-08
garminGarmin Connect583446403 (us)5.30.1, 2026-10-11
utrUTR Sports1519232627 (us)2.1.5, 2026-10-05
flab플랩풋볼6608972481 (kr)2.0.1, 2026-09-21
daangn당근1018769995 (kr)26.41.0, 2026-10-07
kakaotalk카카오톡362057947 (kr)26.8.4, 2026-10-07
toss토스839333328 (kr)5.279.0, 2026-10-02
naverblog네이버 블로그328813873 (kr)6.49.0, 2026-09-29

Appendix B. Pages and documents used (W, D) ​

  • W, rendered logged out at 390 × 844 on 2026-10-11: https://www.daangn.com/kr/group/열정의-테니스-jt4fgqhx1vaf/ (멤버 1030; member rows read as text), https://m.blog.naver.com/PostList.naver?blogId=naver_diary, https://www.instagram.com/nasa/ (login modal; inline counts in the page text), https://www.linkedin.com/in/williamhgates (modal; inline counts).
  • D: Material 3 Compose source and tokens in androidx/androidx (androidx-main): material3/tokens/{ListTokens, BadgeTokens, DividerTokens, FilledCardTokens, AssistChipTokens, SuggestionChipTokens, FilledTonalButtonTokens, DatePickerModalTokens}.kt, material3/ListItem.kt, material3/internal/TextFieldImpl.kt (SupportingTopPadding 4dp); Material Components Android docs/components/Card.md; Apple HIG JSON developer.apple.com/tutorials/data/design/human-interface-guidelines/{lists-and-tables, typography, accessibility, layout}.json; TDS tossmini-docs.toss.im/tds-mobile/components/{badge, grid-list, list-footer, bottom-info, list-header, border} and ListRow/list-row-components; KRDS krds.go.kr/html/site/component/{component_04_06 (Badge), component_04_01 (Structured list)}.html; GOV.UK Design System Tag; Carbon Tag usage; Atlassian Lozenge usage; Seed Design daangn/seed-design docs/content/component/{avatar, avatar-group, callout}.
  • C: docs/design/flat-v2-component-library-2026-10-09.md §1 to §10; docs/design/voice-and-tone-guide.md; docs/design/terminology-guide.md; packages/app/src/config/i18n/ko/*.ts (period counts); docs/twomore_design_system/wireframes/flat-v2-kit-content.jsx, flat-v2-kit-controls.jsx, flat-v2-kit-chrome.jsx, flat-v2-forms.jsx.

Appendix C. Measurements (P) ​

All on the boards built from the working tree at 18:47 on 2026-10-11 (chevron change in FvkCBlockFrame included), headless Chromium over CDP, 390 wide at 1×. Pixel analysis of F frames reads source pixels; screen width 663 px (Facebook, LinkedIn) = 390 pt.

ItemNumbers
Strip, hug-last (library board)n = 3: columns at 20, 167.8, 315.5; widths 147.8, 147.8, 54.5; gaps between contents 110.9 and 93.3. n = 2: gap 239.3. n = 4: gaps 72.3, 77.8, 77.8. Height 80 (100 with deltas).
Action facethree cells of 116.66 at x 20, 136.66, 253.33; glyph centres 78.33, 194.98, 311.66; label gaps 88.6 and 94.2; height 80.
Strip experimentDOM mutation of the same cells (§4.1 table): equal-left end air 62.2, 126.9, 65.0; equal-centred insets 39.9 / 31.1, 56.2 / 63.4, 25.3 / 32.5.
Head-to-head stripcontents 54.5, 109.8, 52.2 in 116.7 cells.
Match state block144 without a claim, 170 with one; Tag 24 tall at y 126 (zone C), 38.5 to 80.2 wide; Caption row 32 directly under it, then the 24 tail; chevron centre = block centre in every kind. Prototype (Tag in row A, zone C deleted): 144; 200%: 250 → 206.
Lone-row scan205 Tag, Count, Level, Button and Choice candidates on the two library boards, 2,878 on the six page boards; lone Tags at 100%: 12 Match state frames, 2 create-session frames; Choice rows alone: 85 in 82 frames.
AnswerPairrow 112 = 12 + 22 + 4 + 18 + 44 + 12; buttons 44; L2 line box to pair 0; pair to row bottom 12; next row's name 26 below the buttons; pair 298 = 145 + 8 + 145; at 200%: pair 124, row 312.
Facebook facebook:9screen 118 to 781 (663); text bands 791 to 816, 828 to 850, 863 to 878 (baseline of "6 mutual friends"); Confirm 907 to 970 (63.5 tall), x 267 to 506 (239); the highlight ends at 992.
LinkedIn linkedin:5text bands 1330 to 1353, 1365 to 1381, 1401 to 1419, 1442 to 1466 (mutual-connections line with its icon); Accept 1498 to 1552 (55 tall), x 261 to 395 (135).
Caption rows292 in 14 boards; text x = 20 in 291; box 32 (52 for two lines); previous element to the text box 6 in all contexts, 18 under a Section head.
PersonRow1,079 rows at 100% Korean on 13 boards; 888 with L2; 838 one fact, 39 two, 11 three or more; 191 rows with no L2 are 68 tall; avatar x 20, text x 72; MediaRow thumb 56, text x 88.
Form stripcells 20 to 156 (5 × 24 + 4 × 4); summary 168 to 216.8 now; 321.3 to 370 at the end (KO), 324.5 to 370 (EN); at 200%: 272.5 to 370 (KO, 44 tall), 279 to 370 (EN).
Create-session provenanceTag row in 23 cs-* frames (alone in 2); 44 in every one.
M3 tokens (D)list one, two, three line 56, 72, 88; vertical 8 (three-line 12), start and end 16, leading 16, trailing 16, avatar 40, image 56; divider inset 16 and 1 thick; badge large 16, small 6; assist and suggestion chip 32, label large; tonal button 40; supporting text top 4, inset 16, min line 16; card action row margin 8, content padding 16.

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