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
| Tag | Meaning |
|---|---|
| C | Read in our code or kit today, with path:line. |
| M | Measured in the DOM of the built board docs/public/wireframes/flat-v2-match-row.html (390 wide, headless Chromium) on 2026-10-12. |
| A | Arithmetic from kit constants (not DOM-measured). Said so each time. |
| W | Fetched as text from the live page on 2026-10-12. |
| L | Taken from an earlier lane or the decision log (their tables measured it); not re-measured here. |
| X | Not 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):
| Surface | File and line | What 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 :2420 | Opt-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) | :2523 | Header 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:4495 | A 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:2677 | The 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 :3768 | Zone 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:4615 | A 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 sheets | FvkConfirmSheet :4704, FvkDisputeSheet :4807, FvkDecideSheet :4909; the claim is fvkCtlClaim :4673 | Confirm 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 대진표 frames | flat-v2-detail.jsx:1911 onward (FvkMatchRow in rounds), block at :1960 | FvkMatchRow, no faces; the block has none. |
| Profile recent matches and h2h list | flat-v2-hub.jsx:1384, flat-v2-detail.jsx:5990 (profile composes the shell), :6418 | FvkResultRow, no faces. h2h also has a form strip (flat-v2-lib-surfaces.jsx:1312), which is not a match row. |
| Comparison board | flat-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:
| Surface | File | What 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 :94 | One AvatarStack of the opponents, result and score badges, a chevron. Row press opens the session, not a team. |
| h2h opponent list | packages/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 partners | packages/features/records/src/cards/top-partners-card.tsx | Rows 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/homeParticipant1overhomeLogo2/homeParticipant2in 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.
| Part | Rule |
|---|---|
| Header line | Row 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 line | One 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. |
| Face | 32, 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. |
| Initials | The 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). |
| Names | FvkNames: 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 columns | 32 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 marker | Weight 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, doubles | The 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):
| Context | Line height today | Line height with the 32 face | Row or block height change |
|---|---|---|---|
| Row (round, scorecard, profile, h2h, requests) | 22 or 24 | 32 | +16 to +20 per match (M: 108 to 124 with numerals, 104 to 124 names only) |
| Block (Match state) | 32 | 32 | 0: the block stays 144 (24 zone A + 8 + 72 + 40 padding = 144, A) |
| Sheet (Confirm, Dispute, Decide) | 48 | 48 | 0 (a 32 face is centred in a 48 line); the 40 face and the 40 footprint pair retire |
| Number pad cells | 48 | 48 | 0 |
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).
| Stack | Text only (today) | One line per team with faces (recommended) | Two lines per team, ATP form with faces (A) |
|---|---|---|---|
| One match | 108 | 124 | about 200 (4 lines of 32, 4 within a team, 12 between teams, plus 24 header, 4, 24 padding) |
| 4 matches | 432 | 496 | about 800 |
| 8 matches | 864 | 992 | about 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 anydata-hitunder 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.
- The row press opens the match (the record, the confirm sheet, the session's match), one stop, as today. Not a team stat.
- 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%. - A team stat page does not exist and the shipped app has no pair model:
TopPartnersCardis keyed by a name string, not pressable (top-partners-card.tsx,player-stats.ts:39), and h2h is player against opponent (PlayerMatchupis "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)
| Context | Face | Singles | Doubles |
|---|---|---|---|
| Row (round card, scorecard, request, profile, h2h) | 32, line 32 | one face in the 56 slot | two 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 initials | one face | two faces; block stays 144 |
| Sheets (Confirm, Dispute, Decide) | 32 in a 48 line (replaces the 40 and the 40 footprint pair) | one face | two faces |
| Number pad (ScoreEntry, ScoreStop's score step) | 32 in a 48 line, left of the names, same 12 gap | one face | two faces |
| PersonRow on the match page (§1.5) | 40, the person-row canon | n/a | one 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)
| Peer | Band | Share of width or frame | Source |
|---|---|---|---|
| 당근 모임 (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 844 | review 2 lane B, owner-review-2-lane-b-2026-10-11.md:92, :306 |
| Strava clubs | about 2.04 : 1 | about 191 at 390 | decision log PHOTO-BAND-2:1 |
| Facebook Groups | 1.91 : 1 | about 204 | decision log |
| Meetup groups | 16 : 9, a rounded photo card on the page (review 2 lane B :93) | 219 | decision log, lane B |
| Band | 4 : 3 | 293 | decision log |
| Airbnb | a 3:2 gallery on the listing, a different job ("a bigger photo job"); no official spec reached (X) | 260 | decision log; a 2026 blog says 3:2, unverified |
| 네이버 지도 and 카카오맵 places, 야놀자, Playtomic club page | Not measured (§0). 카카오맵's place page puts a photo then labelled pills under the title (review 1 lane C :225), no band size taken | X |
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):
| Component | Height | Share of 844 | Against the band image (196) |
|---|---|---|---|
| Status bar | 44 | 5% | 0.22 |
| Header | 56 | 6.6% | 0.29 (overlays the band) |
| Block | 144 | 17% | 0.73 |
| MediaBand image | 196 | 23% | 1.0 |
| Band with status | 240 | 28% | 1.22 |
| Tab bar | 98 | 11.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·600onBlockEyebrow, 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
aEndfollows the same rule (it is alreadyflex-shrink: 0, 8 after the eyebrow); a block has a door or a Tag, never both (FvkCBlockMatchsetstagKeyonly 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)
- 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)?
- 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)?
- 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.jsxFvkTeamLines(: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); aringcolour prop (canvas or block ink); on-ink initials tone; drop faces atscale >= 1.5in every context; the cells context draws the face slot before the names;facesbecomes a per-slot array of photos only. - [ ]
FvkMatchShell(:2523),FvkResultRow(:2677): pass photos through;FvkResultRowstops ignoringphotos. - [ ]
flat-v2-kit-controls.jsxFvkMatchRow(:4495),FvkResultRequestRow(:4615): passfaces; update their "no avatar" header comments;fvkCtlClaim(:4673),FvkConfirmSheet(:4704),FvkDisputeSheet(:4807),FvkDecideSheet(:4909): faces on every claim;FvkScoreTable(:4057) andFvkScoreEntry(:4115): faces in the cells context. - [ ]
FvkCBlockMatch(:4209): faces on ink (zero height change);FvkCBlockFramezone A (:3830to:3850): removeflexWrapfor a door, door columnflex 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.jsxFvkMediaBand(:1011, art SVGs at:930to:960): replace the literal 240 withFVK_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.mjsandscripts/lib/flat-v2-verify-*.mjs: afacescheck (every[data-team]line in a match surface has a face slot of 56 at under 1.5x, none at or above), adoorcheck (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: replaceTeamRow/ScoreCellwith 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 28AvatarStack; use the same face slot; row press unchanged (opens the session). - [ ] The match page: a
PersonRowper 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 changeHERO_BAND_ASPECTandmedia-hero-visual.test.ts. - [ ] Faces stay behind
public.can_view_face(no new read path);FvkAvatarinitials and the shipped avatar fallback cover a refused face. - [ ] Update
docs/architecture/screen-blueprint.mdfor every changed screen; runyarn check.