Skip to content

Owner review 5, lane H: one match grammar with avatars, doubles lines, the MediaBand, the block door (2026-10-12) ​

Status: Draft · Date: 2026-10-12 · Lane H of owner review 5, points 1, 2, 3, 4 (the grammar half) and 5. Inputs: the kit as built today (flat-v2-kit-content.jsx, flat-v2-kit-controls.jsx, flat-v2-kit-chrome.jsx), the library (§3 MediaBand, §4 Block, ResultRow, ScoreEntry, Match row states), the flat grammar doc, the decision log, lane E of review 4 (ATP, faces, the chevron), the shipped MatchScoreboard and its call sites, and the built board flat-v2-match-row.html measured in headless Chromium on 2026-10-12. Audience: Korean club members aged 40 to 60. Only this file was written. Points 4 (honor system, appeals, racing) and 6 (marks) belong to other lanes.

Owner, review 5. Point 1: "Is the mediaband going to stay in this size? If we were to apply the relative sizing rule to the mediabane, in relation to other components in the layout it sits, how should it be sized?" Point 2: "In the block, the text + chevron should always render on top even if the top row is too long. So the top row will either be truncated or renderd across multiple lines, and the chevron + text will always be on the top right, in the top row." Point 3: "In the match block, I want there to be avatars. In fact, across all match rows, I want there to be avatars." Point 4: "Score entry on the number pad is great, but we also need to add avatars. I don't think we are yet to have a unified grammar for match rows (for singles or dobles)". Point 5: "I like the ATP pattern. Let's keep things clean. This means that for doubles, we have two lines for a team, not a single one. But I do wonder if it can be compacted to single lines for a team. As long as each player is individually clickable (to a profile), we should be fine? And the whole row should be clickable as well for a team stat?"

0. Evidence tags and limits ​

TagMeaning
CRead in our code or kit today, with path:line.
MMeasured in the DOM of the built board docs/public/wireframes/flat-v2-match-row.html (390 wide, headless Chromium) on 2026-10-12.
AArithmetic from kit constants (not DOM-measured). Said so each time.
WFetched as text from the live page on 2026-10-12.
LTaken from an earlier lane or the decision log (their tables measured it); not re-measured here.
XNot reached. No claim is made.

Not reached today: Apple's HIG and Material 3 pages (the fetch returned titles only, and Android's accessibility page did not carry the 48dp text, so the 44 and 48 figures below come from our own grammar doc and secondary reports, marked), the live ATP doubles page (the main thread's Roland-Garros check in lane E is what the board records), Sofascore and Flashscore doubles layout (a search returned only CSS fragments), a measured screenshot of any peer's club or place detail page (the iTunes lookup frames for Airbnb, 네이버지도 and 카카오맵 are marketing carousels of list screens, not detail pages, so none was measured), 야놀자 and Playtomic club pages (iTunes lookup returned no app for the ids tried). How X, Instagram and Strava nest avatars in tappable cards is described from product behaviour and is marked X; no pixel claim rests on it. English at 200% was not drawn on the board and is not measured.

1. Points 3 to 5, one grammar for every match surface ​

1.1 Inventory (C), what draws a match or a team today ​

Redesign (kit):

SurfaceFile and lineWhat it draws today
FvkTeamLines (the one team-line primitive)flat-v2-kit-content.jsx:2276; contexts at :2050 (FVK_TEAM_CTX)Four contexts: row (names 15/22, numerals 17/24, line 22 or 24), block (24/32 on ink, line 32), sheet (24/32, line 48), cells (a 48 Input per set, line 48). One team per line, viewer first, "(나)".
Faces inside it:2387 to :2420Opt-in faces. Row: 32 faces in a 56 slot (singles one face, doubles two whole faces overlapping 8), dropped from 1.5x text. Sheet: a 40 face, or two faces inside one 40 circle. Block and cells: none, no code path.
FvkMatchShell (header, then team lines):2523Header line of facts plus one trailing Tag, Button or text, then FvkTeamLines context="row". Passes faces through. Heights 104 to 116, 156 with the answer pair.
FvkMatchRow (round, scorecard, request rows; every state)flat-v2-kit-controls.jsx:4495A thin wrapper on the shell; draws no faces (no faces prop is passed).
FvkResultRow (profile history, session view, h2h list)flat-v2-kit-content.jsx:2677The shell with a date header. Accepts photos and ignores it ("the shell draws no faces", compat mapping). Profile board passes photos={[1,2]} (flat-v2-hub.jsx:1384) and gets none.
FvkCBlockMatch (the Match state block):4209, frame FvkCBlockFrame :3768Zone A eyebrow plus door or Tag, zone B FvkTeamLines context="block". 144 tall in every state. No faces.
FvkResultRequestRow (확인할 점수 list)flat-v2-kit-controls.jsx:4615A Match row with the answer pair. Header comment: "no avatar" (photo: ignored).
FvkScoreTable and FvkScoreEntry (number pad):4057, :4115 (stop step FvkScoreStop :4317)Cells context only: names and 48 x 48 Inputs. No faces.
Confirm, Dispute, Decide sheetsFvkConfirmSheet :4704, FvkDisputeSheet :4807, FvkDecideSheet :4909; the claim is fvkCtlClaim :4673Confirm passes context:'sheet', faces:true (:4717), so it has 40 faces. Dispute (:4864) and Decide (:5043) draw the same claim in the row context with no faces.
Scorecard and 대진표 framesflat-v2-detail.jsx:1911 onward (FvkMatchRow in rounds), block at :1960FvkMatchRow, no faces; the block has none.
Profile recent matches and h2h listflat-v2-hub.jsx:1384, flat-v2-detail.jsx:5990 (profile composes the shell), :6418FvkResultRow, no faces. h2h also has a form strip (flat-v2-lib-surfaces.jsx:1312), which is not a match row.
Comparison boardflat-v2-match-row.jsx (sections 1 to 7)The one place faces are drawn on a row, by hand-passed faces; ATP reference and shipped scoreboard reconstructions.

Shipped app:

SurfaceFileWhat it draws today
MatchScoreboard (scorecard live, completed, draws; session matchups card; tournament board; add-round preview; king-of-court card)packages/app/src/presentation/components/sessions/match-scoreboard.tsx (TeamRow around :395 to :470, ScoreCell :481)One line per player, each with a 32 (SM) avatar through PlayerIdentityChip, name 700 for the winner, 500 otherwise; tier badge, guest chip and pickup club chip; VS divider; set cells 26 wide. About 96 plus header singles, 176 plus header doubles (A).
MatchHistoryRow (profile recent matches, records history)packages/app/src/presentation/components/match-history-row.tsx:100 (AvatarStack size="XS" = 28), whole row Pressable to the session :94One AvatarStack of the opponents, result and score badges, a chevron. Row press opens the session, not a team.
h2h opponent listpackages/features/records/src/head-to-head-screen.tsx:171 (PersonRow, avatarUrl={r.opponentAvatarUrl})A person row (avatar, name), one opponent per row. Not a match row.
Top partnerspackages/features/records/src/cards/top-partners-card.tsxRows keyed by partnerName, a medal Badge, a win rate; not pressable, no avatar, no id (PartnerStats has partnerName only, player-stats.ts:39).

Where they differ (C): the shipped scoreboard already has a face on every player line (32), the history row has a 28 stack, the redesign kit has faces only on the Confirm sheet (40) and as an unused option on the row (32 overlap). Names per line: shipped one per player, kit one per team (partners joined by the break). Numerals: 26-wide cells in the app, 32-wide columns in the row, 48 Inputs in the cells. So three face sizes (28, 32, 40), two lines-per-team conventions, and three set-column widths are in play. Review 2 dropped the shipped scoreboard's chips and VS bar but kept "the idea of a face" (flat-v2-match-row.jsx section 2), which is why the owner now asks for faces back everywhere.

1.2 Best practice for one match grammar, singles and doubles ​

What peers do.

  • ATP (W, main-thread check from lane E and the board's section 1): a header line (round and court), then one line per player with a small headshot, the name as a link, the seed after it, and the set numbers on that player's own line; in doubles every partner has their own line and photo, with the seed repeated and no slash (ATP US Open results). The card is not one big link: the names and the "H2H" and "Stats" footer are links. The board draws it at 270 tall for doubles including its footer (M).
  • Flashscore (X for the live layout): its stylesheets carry separate doubles grid areas with a second participant slot (homeLogo1/homeParticipant1 over homeLogo2/homeParticipant2 in the snooker template), which suggests partners stack on their own lines, not on one line with a slash (Flashscore snooker template CSS; the tennis template excerpt was cut off before the participant layout, so this is inference).
  • Eurosport (W): its ATP Moscow doubles draw lists each team as two names on separate lines with the set scores beside them (Eurosport draw).
  • Printed draws: "SURNAME, Firstname / SURNAME, Firstname" on one line with a slash, set scores in columns (Winston-Salem 2012 doubles). That is the one-line form, on paper.
  • Review-3 lane C table (L): Naver and Flashscore use no photo on a dense result row (Naver a crest); Playtomic uses avatar groups for who is playing. Sofascore, WTA, ESPN, Tennis TV and the US Open and Australian Open apps were not reachable for a live layout in this or the earlier lane, so no claim is made about them.
  • HIG and Material row anatomy: a leading avatar or thumbnail, one or two text lines, a trailing value; Material's list item leading avatar is 40dp and Apple's grouped row carries an avatar at about 40 to 44. These are from our own grammar doc (Seed 40 to 48, Toss) and secondary reports, not from the Apple or Google pages (X, §0).

Reading. The tennis pattern (ATP, Eurosport, Flashscore by inference) is one line per competitor with a face, and scores per line. A pair is two lines because a pro scoreboard has room and the players are celebrities. Our matches are club members in a 350-wide column, where the unit that carries a score is the team.

1.3 Recommendation: one anatomy ​

One anatomy, drawn by FvkTeamLines, in every match surface (row, block, sheet, number pad), one face size, no density variants of the anatomy itself; only the line height changes with the context.

PartRule
Header lineRow and sheet: facts 13/18·500 secondary (court first) with the one trailing element (Tag 24, sm Button 36 or text), as FvkMatchShell today. Block: zone A, the eyebrow, with the door or Tag (§2). The header line is the only place a state Tag sits.
Team lineOne team per line, the viewer's team first with "(나)" on the viewer's own name. Face slot, then 12, then the names, then the set columns.
Face32, everywhere. Singles: one face. Doubles: two whole faces overlapping 8 in a 56 slot, a 2 px ring in the surface colour on the front one (canvas on white, the block's ink on a block). The slot is always 56 so names start at one x in a list that mixes singles and doubles.
InitialsThe same circle, gray100 with ink initials (on ink, white α0.2 with white initials), per face. A refused or missing face is drawn the same way, so the app never reveals that a photo exists (§1.5).
NamesFvkNames: partners as separate names with the 1 px break (never a slash, comma, "&" or "vs"), spaces kept inside a name so a wrap falls between names, up to NAME_LINES (2) lines, never cut. Winner 600 ink, loser 400 secondary, only on an accepted result.
Set columns32 wide per set (48 in the number pad's Input cells), the numeral on the team's own line, the loser's tiebreak as the small superscript in the reserved slot; the header line of set labels only in the number pad.
Winner markerWeight and tone only (names 600 ink against 400 secondary; numerals ink against secondary; on a block the winner's numeral is ball and the loser's white α0.7), plus the 승, 패 Tag in the header. No check, no VS, no arrow (earlier rulings stand).
Partner order, doublesThe pair is one unit: both faces in one slot, both names in one text flow. The order is the data order, the viewer's own name first in their team.

How it scales to every context (values). The face is 32 everywhere, so the line height is max(text line, 32):

ContextLine height todayLine height with the 32 faceRow or block height change
Row (round, scorecard, profile, h2h, requests)22 or 2432+16 to +20 per match (M: 108 to 124 with numerals, 104 to 124 names only)
Block (Match state)32320: the block stays 144 (24 zone A + 8 + 72 + 40 padding = 144, A)
Sheet (Confirm, Dispute, Decide)48480 (a 32 face is centred in a 48 line); the 40 face and the 40 footprint pair retire
Number pad cells48480

Why one size, not two: a smaller face for dense lists would be 24 or 28 (the shipped AvatarStack XS is 28, box-sizes.ts:26); at 24 the 20 inner face of an overlap has one-character initials about 10 px tall, which the earlier lane E judged illegible. A second size would also mean every list that mixes contexts re-aligns. The cost of 32 is height in rows only, and the owner has asked for faces in every row.

Heights, doubles, at 390 (M from the board).

StackText only (today)One line per team with faces (recommended)Two lines per team, ATP form with faces (A)
One match108124about 200 (4 lines of 32, 4 within a team, 12 between teams, plus 24 header, 4, 24 padding)
4 matches432496about 800
8 matches864992about 1600

The visible list area on a 390 x 844 phone with header and tab bar is about 646 (844 − 44 − 56 − 98, from the owner's numbers). Four recommended rows (496) fit in one screen; four ATP-form rows (about 800) do not.

1.4 Point 5: doubles on one line or two ​

Options.

  • A. Two lines per team (ATP), one face per partner. Faithful to ATP; every player gets a full 32 face and a line that could be a tap target. Costs about 76 more per doubles match (200 against 124, A) and 4 lines of 32 where the set numerals belong to a team: the score has to be drawn on one partner's line, repeated on both (ATP repeats it per line) or merged across two lines, so the score column no longer reads as a team's. In the number pad, the 48 Inputs belong to teams, not players, so a four-line table is wrong there; it would need a different table for singles and doubles, against "one anatomy".
  • B. One line per team, two faces and two names (recommended, and what the kit already draws). A team is one line: overlapped faces, both names with the thin break, the team's games on that line. 124 per doubles match (M), identical to singles (a singles match is also 124), so the list rhythm does not change between singles and doubles. Names get 206 wide (M, 278 without faces); Korean pairs fit one line at 100% ("김민준 (나) | 이서연" about 125, A), a long English pair wraps to two (M: "Minjun Kim (You) | Seoyeon Lee" is 44 tall in 206 wide, row 136 against 124).

Measured (M). 100% Korean, 2 sets with a tiebreak, faces: names column 206 x 22 per line, row 124; 4 matches 496, 8 matches 992. 150%: names 278 x 33, row 141 (faces dropped from 1.5x). 200% Korean: names 278 x 44 (two lines), row 166. English at 100% with faces: team 0 wraps to 44, row 136. English at 200%: not measured (X).

Recommendation: B, with the ATP header line kept. Evidence: the score belongs to a team; the number pad, the block and the sheet all need one line per team; the height rhythm (124 for any match) and the 496 / 992 stacks. Trade-off: the back face of an overlapped pair shows 24 of 32 (the 8 overlap) and the pair is not literally the ATP picture. The owner's phrase "keep things clean" is better served by two clean lines in every match than by four.

1.5 Nested targets: players tappable inside a tappable row ​

What the standards say.

  • WCAG 2.5.8 (AA), W: a pointer target is "at least 24 by 24 CSS pixels" unless a spacing exception (a 24 circle centred on an undersized target must not overlap another target), an inline exception or the like applies (Understanding 2.5.8). 2.5.5 (AAA) asks 44 x 44 (Understanding 2.5.5); on overlapping targets, the overlapped area does not count towards size unless both do the same thing.
  • ARIA and axe, W: "Interactive control elements must not have focusable descendants" (rule nested-interactive, WCAG 4.1.2, user impact serious); the inner control "is not announced by screen readers" and leaves an empty tab stop (Deque, nested-interactive).
  • Our grammar: touch target 44 minimum, 48 for primary actions (flat-mobile-grammar-v2-2026-10-09.md §1, Apple and Material as the sources there; Apple and Google pages not reached, X). The DOM verifier fails any data-hit under 44 x 44 (scripts/verify-flat-v2-boards.mjs, header). The library already has a pattern for a control in a row: the sm Button is "its own stop outside the press area", a separate stop after the row (library Match row, Accessibility), never inside the row's press area.
  • X, Instagram, Strava (X): from product behaviour, not fetched: X and Strava feed cards put a tappable avatar and name inside a card whose body opens the post or activity; Instagram's feed post keeps the avatar and name tappable and the media a separate target. Those are small header strips with one avatar and one name, and the card body is not one button around them.

Applied to a doubles team line. Two overlapped faces are 56 wide with the back face visible 24 and the front 32: neither can be a 44 target, and the two cannot be separated by the 24 spacing exception with a pitch of 24. At 1.5x and above the faces are dropped, so the per-player tap would vanish at the very text sizes where an older member most needs it. A name inside the wrapped text flow is an inline link, which 2.5.8 exempts, but two links nested in a button row break the ARIA rule and read as an empty stop on a screen reader. The row itself is already the one stop with the whole match as its label ("확인 대기, 1번 코트, 내 팀 김민준, 이서연 6. ...", library Match row, Accessibility).

Recommendation.

  1. The row press opens the match (the record, the confirm sheet, the session's match), one stop, as today. Not a team stat.
  2. Players are tapped one level down, on the match page, where each player is a PersonRow (avatar 40, pitch 64, one stop each) that opens the profile. The row itself carries no nested control. This keeps one stop per row, the 44 target, and works at 200%.
  3. A team stat page does not exist and the shipped app has no pair model: TopPartnersCard is keyed by a name string, not pressable (top-partners-card.tsx, player-stats.ts:39), and h2h is player against opponent (PlayerMatchup is "UNIQUE player_id + opponent_id + club_id", player-matchup.repository.port.ts:6). A row press to "team stats" would need a new pair-keyed read model first. Do not promise it from the row.

Trade-off. Reaching a profile from a row costs one extra tap (row, then person) against ATP's direct name links. The gain is a row that is one 124-tall target, correct for screen readers and for the 40 to 60 audience at 200%. If the owner later wants a direct name link, the safe form is a separate stop after the row (like the library's sm Button), not a link inside the press area.

1.6 Point 4: avatars everywhere (sizes and behaviour) ​

ContextFaceSinglesDoubles
Row (round card, scorecard, request, profile, h2h)32, line 32one face in the 56 slottwo faces, overlap 8, ring 2 canvas
Match state block (on ink)32, line 32, ring in the block's ink; initials circle white α0.2, white initialsone facetwo faces; block stays 144
Sheets (Confirm, Dispute, Decide)32 in a 48 line (replaces the 40 and the 40 footprint pair)one facetwo faces
Number pad (ScoreEntry, ScoreStop's score step)32 in a 48 line, left of the names, same 12 gapone facetwo faces
PersonRow on the match page (§1.5)40, the person-row canonn/aone row per player

Initials where can_view_face refuses. Each face is decided separately by public.can_view_face; a pair can be one photo and one initials circle. A refused face and a missing photo draw the same initials, so the row never discloses that a photo exists (canon data-and-hooks.md § Faces: "a NULL face means show the initials"). A pickup list is a column of initials and the owner accepts it (review 4, point 11). Initials come from the name, so the circle is decorative and never the only carrier of who is who.

200% text. The existing rule stands and is the right one: faces are decorative and are dropped from 1.5x (M: FvkTeamLines row context, flat-v2-kit-content.jsx:2394; the 200% frame returns the text row, 166 tall, names 278 wide on two lines). Dropping the face frees the 68 the slot used (56 + 12), which is what lets a Korean pair stay on two lines at 200%. In the block and the number pad the same rule applies: faces go from 1.5x; the keypad cells stay 48 and the sheet scrolls.

Faces in the number-pad ScoreEntry. The cells context gets the same face slot in front of each team line: slot 56, gap 12, names, then the set cells (48 wide, 8 apart). The line is already 48 tall, so a 32 face costs no height. Names width is 350 − 68 − cells: one set 234, two sets 178, three sets 122 (A, from FVK_TEAM_CTX cells col: 48, gap 8). A Korean pair with "(나)" is about 125 (A), so it fits with two sets and wraps to two lines (44, inside the 48 line) with three or in English. The sheet is docked on a 250 keypad (FVK_SCORE_KEYPAD, flat-v2-kit-controls.jsx:4109); because faces add no height, the sheet's total does not change. The viewer's team stays on the top line, with the face.

2. Point 1, the MediaBand ​

2.1 What it is today (C) ​

FvkMediaBand draws a 240-tall band in every kind (photo, map, error) (flat-v2-kit-chrome.jsx:1011, height: 240; the SVG art is 390 x 240). The status bar is 44 (FVK_STATUS_BAR, flat-v2-kit.jsx:168) and the header sits on the band at top: 44 (:1042). So the image area below the status bar is 196 (240 − 44). The library says "full-bleed 240 including the status bar" (§3 MediaBand, Anatomy) and "first content starts 24 below the band".

The shipped app is on the same number by a different path: HERO_BAND_ASPECT = 2, resolveHeroBandHeight(width) = min(280, round(width / 2)) = 195 at 390 (packages/ui/src/media-hero-visual.ts:38 to :45), recorded as PHOTO-BAND-2:1 in the decision log (2026-09-27, the owner's "might be too small" on 132): "group pages cluster at 2:1. 당근 모임 covers are 2:1, Strava clubs about 2.04:1, Facebook Groups 1.91:1, Meetup 16:9, Band 4:3 ... Rejected: 16:9 or taller, Airbnb's 3:2 gallery job. It would push the club's name and tabs toward a false floor (NN/g)." The same ruling extended the rule to the session and venue map ("make the map photo ... match the size of that on the club detail page", media-hero-visual.ts:24 to :37). So the kit's 240 is the shipped 2:1 (195, 196 on the 4 pt grid) plus the 44 the kit draws for the status bar. The owner's 28% (240 / 844) counts the status bar; the photo area is 196 / 844 = 23%.

2.2 The relative sizing rule, where it is in our docs (C) ​

The flat grammar doc (§4 Component proportions) states proportions as ratios: Block 144 / 844 = 17% of the viewport (peers 16 to 18%), width : height = 2.4 : 1; header 56; tab bar 56 + home inset 34; thumb : pitch 0.7; label : row 0.3; seam ladder 8 : 16 : 32. It does not give the MediaBand a ratio; the library fixes 240 and the code fixes 2:1. The only band rule is the decision-log aspect one. There is no document that derives the band from the Block.

2.3 Peers (L, no new measurement reached) ​

PeerBandShare of width or frameSource
당근 모임 (DOM at 390)cover 390 x 170 full-bleed, starting at y 60 (below its header)2.29 : 1; photo bottom at y 230 = 27% of 844review 2 lane B, owner-review-2-lane-b-2026-10-11.md:92, :306
Strava clubsabout 2.04 : 1about 191 at 390decision log PHOTO-BAND-2:1
Facebook Groups1.91 : 1about 204decision log
Meetup groups16 : 9, a rounded photo card on the page (review 2 lane B :93)219decision log, lane B
Band4 : 3293decision log
Airbnba 3:2 gallery on the listing, a different job ("a bigger photo job"); no official spec reached (X)260decision log; a 2026 blog says 3:2, unverified
네이버 지도 and 카카오맵 places, 야놀자, Playtomic club pageNot measured (§0). 카카오맵's place page puts a photo then labelled pills under the title (review 1 lane C :225), no band size takenX

On a like-for-like measure (screen top to the bottom of the photo) 당근 is at 230 and ours at 240: within 5%. So the kit's 240 is not out of line with the closest Korean peer; it only looks large because the status bar is drawn into the band.

2.4 Recommendation, with the rule that derives it ​

Keep the band at 2:1 of the screen width for the image, the status bar on top of it: 196 + 44 = 240 at 390. State it as one rule in the library: "A MediaBand's image area is half the screen width (cap 280), measured below the status bar; the status bar is drawn over it." The kit constant should be round(width / 2 / 4) * 4 (196 at 390), not a literal 240, so the mock and resolveHeroBandHeight cannot drift.

Relations this rule gives (A, from the owner's numbers and the grammar doc):

ComponentHeightShare of 844Against the band image (196)
Status bar445%0.22
Header566.6%0.29 (overlays the band)
Block14417%0.73
MediaBand image19623%1.0
Band with status24028%1.22
Tab bar9811.6%0.5

The band is 1.36 times the Block and the one big image on a place page; the Block is the biggest ink surface and one per hub, and a band is never on the same page as a hub Block, so they do not compete. First content starts 24 below the band (library) and, with the header gone under it, about 72% of the viewport (604 of 844) is left for the hero, the facts and the first section. That keeps the hero and the first row above the fold, which is what the 2026-09-27 ruling rejected 16:9 to protect.

Variants. Club photo, venue photo, session map: one geometry (the library already says so; the shipped resolveMediaHeroBand returns the same fixed height for "a map, a photo and an album"). Loading and error states keep the same height. Wide windows cap at 280 (shipped).

The alternative, if the owner wants smaller (taste). Tie the band to the Block's own aspect: 2.4 : 1 of the width, 164 below the status bar, 208 with it, 21% and 25% of 844. It is within 5% of 당근 (2.29), is the grammar's own ratio, and still 24% bigger than the 132 the owner called too small on 2026-09-27. Evidence favours 2:1 (five peers cluster at 1.9 to 2.3 and the owner ruled once already); 2.4 : 1 is the "one ratio for the block and the band" version. This is the one real question for the owner on point 1.

3. Point 2, the block's door always at the top right of the top row ​

3.1 Check against the kit (C) ​

FvkCBlockFrame zone A (flat-v2-kit-content.jsx:3830 to :3850) is a flex row with minHeight: aH (24 with a door or Tag, else 18); the eyebrow node is flex: 1 1 auto; min-width: 0; the door (aEnd) is flex-shrink: 0; margin-left: auto; and, only when a door exists, flex-wrap: wrap (:3835). The eyebrow is one line with tail ellipsis (FvkCEyebrow lines={1}). Consequence: because the eyebrow's flex-basis is its full text width, a long eyebrow plus a door that does not fit makes the door wrap to a second row, right-aligned. The library says this on purpose for 1.5x and above ("from 1.5x text the door wraps under the eyebrow, right-aligned", §4 Block, Door), and the kit does it at 100% too whenever the sum exceeds 310. The 310 is the block's inner width (350 − 2 x 20). An English held-state eyebrow ("I need my earlier match's score", about 210 at 13 px, A) plus "Check score ›" (about 90, A) plus the 8 gap is 308 or more, so it already sits at the edge at 100% (A, not DOM-measured).

So the owner's rule is a change from the current behaviour, not a restatement of it.

3.2 Peers ​

Where a title may be long and a trailing action must stay in place, peers keep the action on the first row: Apple Health's summary card has the chevron on the top row beside the category name and time (L, lane E §7.2, review 1 lane A :384), Toss's 내 투자 puts the chevron after its label (L), and section heads ("전체 보기 ›") sit at the end of the title row with the title yielding (library Section head). None puts the action on a second row when the title is long; the title truncates or wraps and the action stays.

3.3 Recommendation and the rule ​

Yes, it is a good idea, and it is the same law as the chevron law (a chevron is the suffix of a text that names where it goes) made stable. The rule:

Zone A is two columns on one row: the left text, and the door or Tag. The right column never wraps, never shrinks and never moves to another row; it is aligned to the first line of the left text. The left column takes the rest and, when it is too long, truncates (places) or wraps (dates, sentences, names), never pushing the door down.

Specifics:

  • Door column: flex: 0 0 auto, one line, nowrap, 13/18·600 onBlockEyebrow, a 16 chevron after the text, sitting in a line slot the height of line 1 (18 at 100%, growing with the text), so at any wrap it rides line 1, as the library's line slot rule does for facts. The door text is short by the labelling law (name of the destination, at most about 6 syllables: "점수 확인 ›", "내 일정 ›"), which is what makes a never-wrap door safe.
  • Left column, by content: a place or a court name crops with a tail ellipsis on one line at 100% ("places may crop"); a date or a sentence (the held-state line, "내일, 9월 19일 (토)") wraps, up to 2 lines at 100% and 3 from 1.5x (the same cap zone C already has), because a date or an instruction cut mid-word is wrong; a person name never crops (NAME_LINES 2). In zone A of the Match block the left text is the court (place, crops) or the held sentence (wraps).
  • 200%: the door at 26 px with a 32 chevron is about 140 wide (A), under half of the 310 inner width, so the left column keeps about 160 and wraps to 3 lines; the door stays in the top right of the first row and the block grows. This replaces the library's "from 1.5x the door wraps under the eyebrow". Zone A's height becomes max(24, left lines x line height).
  • A Tag in aEnd follows the same rule (it is already flex-shrink: 0, 8 after the eyebrow); a block has a door or a Tag, never both (FvkCBlockMatch sets tagKey only when there is no door).
  • The press: the whole block stays the one press target and the door the visible cue, not a second stop (library Block, BG3 and BG4), so no 44 target is needed for the door itself.

4. Decided by evidence, and the owner's taste ​

Decided by evidence. One anatomy and one 32 face (the block and sheets cost nothing, the row costs 16 to 20: M); one line per team in every context (the score, the number pad and the block need it; the heights 124 and 496 / 992 against about 200 and 800 / 1600); the row is one 44-plus target and one stop (WCAG 2.5.8, the nested-interactive rule, our 44 rule); the row press opens the match because no team stat exists in the data or the shipped app; faces drop from 1.5x; initials identical for refused and missing faces; the band at 2:1 (shipped, decision log, peers at 1.9 to 2.3, the 240 is 196 plus the status bar); the door stays on row 1, never wrapping.

Owner's taste. Whether the back face of a pair showing 24 of 32 is clean enough against an ATP-style four lines; whether the band should instead follow the Block's 2.4 : 1 (164 + 44); whether a direct name link from the row (as a separate stop) is ever wanted.

5. Questions for the owner (few) ​

  1. Doubles on one line with an overlapped pair of faces (recommended). Players are reached by tapping the match, then a person row; the row press opens the match. Is that acceptable in place of a tap on each partner in the row and a team stat page (which needs a new pair-keyed model first)?
  2. MediaBand. Keep 2:1 (196 + the status bar 44 = 240, the shipped rule) or go to the Block's 2.4 : 1 (164 + 44 = 208)?
  3. Block door. Confirm that the left text should wrap for dates and sentences and crop for places, with the door fixed to row 1 at every text size (this changes the library's "wraps under the eyebrow from 1.5x").

6. Implementation ​

Kit (wireframes, one commit per surface; REGROUND per board only):

  • [ ] flat-v2-kit-content.jsx FvkTeamLines (:2276): make faces the default for every context; one face size 32; the 56 slot for every context (row, block, sheet, cells); remove the 40 face and the 40 footprint pair path (:2387); a ring colour prop (canvas or block ink); on-ink initials tone; drop faces at scale >= 1.5 in every context; the cells context draws the face slot before the names; faces becomes a per-slot array of photos only.
  • [ ] FvkMatchShell (:2523), FvkResultRow (:2677): pass photos through; FvkResultRow stops ignoring photos.
  • [ ] flat-v2-kit-controls.jsx FvkMatchRow (:4495), FvkResultRequestRow (:4615): pass faces; update their "no avatar" header comments; fvkCtlClaim (:4673), FvkConfirmSheet (:4704), FvkDisputeSheet (:4807), FvkDecideSheet (:4909): faces on every claim; FvkScoreTable (:4057) and FvkScoreEntry (:4115): faces in the cells context.
  • [ ] FvkCBlockMatch (:4209): faces on ink (zero height change); FvkCBlockFrame zone A (:3830 to :3850): remove flexWrap for a door, door column flex 0 0 auto, aligned to line 1 through a line slot, left column wraps (dates, sentences) or crops (places) per §3.3; the Tag follows the same rule.
  • [ ] flat-v2-kit-chrome.jsx FvkMediaBand (:1011, art SVGs at :930 to :960): replace the literal 240 with FVK_STATUS_BAR + round(width / 2 / 4) * 4; decide with the owner on 2:1 or 2.4 : 1.
  • [ ] Boards: flat-v2-match-row.jsx (rename to a spec board of the one anatomy: singles, doubles, block, sheet, number pad, 100% / 150% / 200%, Korean and English, and the English 200% frame this lane could not measure); flat-v2-lib-social.jsx (ScoreEntry, confirm, dispute, decide frames get faces); flat-v2-detail.jsx (scorecard, 대진표, profile frames); flat-v2-hub.jsx:1384 (profile recents now draw faces).
  • [ ] Library docs/design/flat-v2-component-library-2026-10-09.md: §3 MediaBand (the 2:1 rule, 196 + status bar), §4 Block (Door: the top-right rule, replacing "wraps under the eyebrow from 1.5x"), ResultRow and Match row states (the anatomy and the face rule), ScoreEntry (faces in the table), a new "Match line" paragraph that all match surfaces cite; decision log entries for 2 to 5.
  • [ ] Verifier scripts/verify-flat-v2-boards.mjs and scripts/lib/flat-v2-verify-*.mjs: a faces check (every [data-team] line in a match surface has a face slot of 56 at under 1.5x, none at or above), a door check (the door's top equals the eyebrow's first line top in a block, at 100% and 200%), each with a canary.

App (after the boards are marked, never before):

  • [ ] packages/app/src/presentation/components/sessions/match-scoreboard.tsx: replace TeamRow/ScoreCell with the one shared team-line primitive (one team per line, the 56 slot, the break between partners); its seven call sites (scorecard completed, live and draws rows, matchups card, tournament board, add-round preview, king-of-court card) follow.
  • [ ] packages/app/src/presentation/components/match-history-row.tsx: retire its 28 AvatarStack; use the same face slot; row press unchanged (opens the session).
  • [ ] The match page: a PersonRow per player (40, opens the profile), the one place a player is tapped from a match.
  • [ ] packages/ui/src/media-hero-visual.ts: unchanged for 2:1; if the owner picks 2.4 : 1 change HERO_BAND_ASPECT and media-hero-visual.test.ts.
  • [ ] Faces stay behind public.can_view_face (no new read path); FvkAvatar initials and the shipped avatar fallback cover a refused face.
  • [ ] Update docs/architecture/screen-blueprint.md for every changed screen; run yarn check.

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