Skip to content

Flat v2 component library — every shared component, with its behaviours (2026-10-09) ​

Status: Archived · Superseded on 2026-10-11 by the consolidated v2.1 edition at docs/design/flat-v2-component-library-2026-10-09.md; kept as the record of how each value was reached.

0. Rulings on the fifteen conflicts the inventory found ​

#ConflictRuling
C1Tag geometry (grammar 24/r6/13 vs list spec 22–26/r8/12 vs code 18–28)Tag sm 24 tall, r6, 13/18·600, pad 0 8 (default); md 28, r8, 15/22·600, pad 0 10 (hero and identity only); level = 24 circle, 13/18·700 numeral; count = 20 tall, min-width 20, r full, 11/14·700.
C2Choice geometry (36 vs 32/40 vs code 40/32)Choice 36 tall, r full, 15/22·500, pad 0 16 on list and settings surfaces; 44 (pad 0 20) inside wizard forms. Hit slop to 44 everywhere.
C3Brand block (grammar vs list spec §3)Grammar: 350 wide, pad 20, radius 16; home 144, ELO 160, identity 144; eyebrow 13/18·600. List spec §3 is superseded.
C4Row pitch (80/64 vs 72/96)Grammar: media 80 (thumb 56), person 64 (avatar 40), cell 48. The list spec's 72/96 rows are superseded.
C5Underline tabs vs the group-spread lawTabs are content-width labels, left-aligned from the rail, gap 24 (Toss, 카카오뱅크, 배민). The group-spread law is amended: it governs equal-column strips (Strip, action-icon rows, CTA pairs), not scope tabs.
C6Section dividerDivider law amended per grammar §4b: whitespace + title; the band for a change of kind; cell hairline inside single-line cell groups; edge hairline only under scrolled content.
C7Middle dotsThe no-middle-dot-join rule stands: in-app meta uses MetaSegments with the 1px MetaBreak; boards draw the break, never ·.
C8FABCanon "no FABs" is retired: one extended FAB per list screen is the screen's single coloured action (홈 + 일정, 경기 + 번개, 클럽 none).
C9Tab-bar tintInk. The tab bar never carries brand colour; the only colour on it is the 7px danger dot / count tag.
C10Rail20 (SEAM.rail → 20); content 350 at 390.
C11ShadowsNone anywhere. Toast = ink surface on white (no shadow needed); header menu = white panel with a 1px #D4D4D4 edge; coachmark = ink surface.
C12$primarySubtle as a surfaceLeaves. Selected row = trailing check glyph + 600 label (no fill); selected Choice = ink fill; selection in pickers = ink check; notices = weak tone fills from the Tag table; empty-state disc = gray100; progress tracks = #E5E5E5.
C13Avatar scale40 (rows, default) · 48 (roster) · 56 (identity); 32 only inside Tag-sized contexts (DM thread avatars).
C14"Cell row" namingCode CellRow (equal columns) is renamed Strip; the new 48 label/value row is Cell.
C15Brand ink on controlsBall #D6F04A with ink text only on the primary Button, the FAB and the CTA; every control state is ink or grey; the focus ring is 2px ink (#0D100E), offset 2. The brand green #006D37 survives as the mark and the court-grass tile.

1. Foundations the components read (hand-off to Phase A) ​

Colour (S2): canvas #FFFFFF · ink #0D100E · secondary #4F5851 · tertiary #626C64 (≥ 16px only) · gray100 #F2F2F2 (neutral fill, band, tonal buttons, empty-state disc) · cell hairline #E5E5E5 · edge hairline #D4D4D4 · ball #D6F04A (primary action, level, block numeral) · block #15181A · brand mark #006D37 · brand-weak #EFF9B7 (owner to confirm) · success #00796A / text #014B40 / weak #CBEBE5 · warning fill #F5B301 / text #6B4A00 · danger #B8332E · info #4B0082 / weak #E8DCFB · court hard #089AC3 clay #8D3D00 grass #83971F · tiers and series per the ledger. Dark: canvas #0F1211, surface #1A201B, block #232824 (ball numeral), ink #DFE4DC, secondary #A9B3AB, cell hairline white α0.09, edge hairline white α0.14, band #000000, ball unchanged with ink text, success #75D1C6, warning #E1B75C/#E2B13C, danger #FA9B94.

Type: display 40/48·700 · hero 24/32·700 · title 20/28·700 · headline 17/24·600 · body 15/22·400/500 · footnote 13/18·500/600 · caption 12/16·500 (tab-bar labels 11/14·500/600). Spacing 4/8/12/16/20/24/32/40 with the §3 roles. Radii 6 tag · 8 thumb · 12 button, logo, notice, menu, toast · 16 block, sheet · full pill, avatar, choice. Icons 24 actions · 20 hero facts and inline · 16 chevrons; lucide stroke 2 in the label's colour. Touch target 44 (hit slop = (44 − visual) / 2).

2. Behaviours (carried from code unless marked new) ​

BehaviourValueSource
Press settle + dimscale/opacity per variant: button .97/.92 · pill .96/.85 · row .98/.70 → row .98/.80 (v2: the 0.70 dim on an 80pt row reads as a flash; peers dim rows 0.8–0.9) · icon .92/.78 · fab .96/.90; spring in {20, 400, .7}, out {15, 300, .7}; dim 80 ms in / 150 ms out; UI threadui/pressable-visual.ts:43-50, ui/motion.ts:29-44
Hover (web, pointer)opacity .85 + cursor: pointerui/pressable.tsx:311-324
Focus (web)2px ink ring, offset 2 (was $primary)ui/pressable-visual.ts:84-89
Disabled / loadingopacity .5, not-allowed, inert; aria-disabled / aria-busyui/pressable.tsx:99,164
Reduced motiondrop the settle, keep the dim; shimmer pins .75ui/pressable.tsx:286-299
Selection semanticsradio/checkbox/switch → aria-checked; tab/option/row → aria-selected; button → aria-pressed; arrow keys move radio groups on webui/selection-accessibility.ts
Hit slop(44 − visual) / 2ui/box-sizes.ts:74
Toastone at a time; 2800 ms; above tab bar and keyboard; swipe-down > 40px or 500px/s dismisses; timer pauses on touchpres/toast-host.tsx
ModalPanelRN Modal, fade 150 ms; backdrop ink α0.4; tap-outside dismisses (confirm dialogs do not); bottom sheets slide 200 ms quick, no grabber, no dragui/modal-panel.tsx
Skeletonopacity .6 ↔ .9, 800 ms half-cycle; shapes at the v2 pitches (§5)ui/shimmer.ts
Pull-to-refreshnative control, spinner only for a user pull, tint inkusePullToRefresh
Hapticsmutation outcomes only, never primitivesunchanged
Edge hairline on scroll (new)header, tab row, tab bar and CTA band draw #D4D4D4 when content passes under them: useScrollEdge(offsetY > 8) with 8px hysteresis, 100 ms fadegrammar §4b
FAB on scroll (new)extended pill at rest; after 120px of downward scroll collapses to a 48 circle (icon only) in 150 ms; expands on upward scroll or at top; hides behind the keyboardMaterial extended FAB
Keyboardinputs scroll into view above the keyboard (react-native-keyboard-controller, per-screen, never global pan→resize)memory: keyboard-controller adoption

3. Chrome ​

ComponentGeometryStatesReplaces
Header56; title 20/28·700 at the rail (left); back = 24 arrow in a 44 target at the rail; pushed screens: back at the rail + title 17/24·600 left-aligned at x 56; hero pages: no title at rest, the hero title fades in (150 ms) when it scrolls under the header (§13 L1); ≤ 2 action icons 24 in 44 targets, gap 8, right; no divider at rest; edge hairline when scrolled; optional subtitle 13/18 secondary below the title (height 64)icon pressed .92/.78; title truncates with ellipsis at 1 lineAppHeader (centered 17 title + permanent hairline), MainTabShell title block (24/32)
Tabs (scope)row 44; labels 15/22, active 600 ink + 2pt underline the label width, inactive 500 secondary; left-aligned from the rail, gap 24; horizontally scrollable past 4; role="tablist"; 24 below to a title or day header, 16 to a rowpressed dim .85; focus ring; underline slides with navSlide springPillNav (both variants on list surfaces), SegmentedTabs
Tab bar56 + home inset 34; 5 items; icon 24 stroke 2, label 11/14; active = filled glyph + ink + 600, inactive = outline + secondary + 500; no top line at rest, edge hairline while content lies below; badge = 7px danger dot (ring 1.5 canvas) or count tag 16 tallpressed .92/.78 on the item; aria-selected; reduced motion no settleinline tab bar in (tabs)/_layout.tsx (brand tint, permanent border)
FABextended pill 48; pad 0 20; icon 20 + 8 + label 15/22·600; ball fill, ink; 20 from the right rail, 16 above the tab bar; collapses to a 48 circle on scroll (§2)pressed .96/.90; focus ring ink; disabled .5; one per screennone (new); variant="fab" physics exists
CTA bandbutton 52 inside 16 top/bottom + 20 sides + home inset 34; one primary; a secondary tonal appears only in wizards and confirms (pair: tonal flex 1 + primary flex 2, gap 12); edge hairline while content lies below—BottomCtaBand (16 sides, permanent hairline)
Day header32; date 15/22·600 ink left, relative day 13/18·500 secondary right; 16 above; today dot = 6px ball with a 1px ink edge hung in the gutter (x 8–14) so the date stays on the rail; "오늘" in ink 600 (§13 L2)—SessionDayHeader
Section head24; title 17/24·600; action 13/18·500 secondary + chevron 16 right; 32 above, 8 below (edge rule trims the first row's top padding)action pressed .85; action target 44SectionHeader (navTitle 17/22·700 + divider)
Band12 tall, full-bleed #F2F2F2, 24 above and below; at most one per screen—Divider seam="section" (retired), tinted bands

4. Surfaces ​

ComponentGeometryStatesReplaces
Block350 × 144 (home, identity) / 160 (ELO); radius 16; pad 20; ink #15181A; eyebrow 13/18·600 white α0.8 + chevron 16 α0.8 at the row end; numeral 40/48·700 ball with unit 15/22·600 white on the baseline; meta 15/22·500 white α0.85; ELO adds progress 8 (ball on white α0.2) + caption; identity: avatar 56, name 24/32·700 white, level tag, sub 15/22 α0.85, edit icon 24 ghost-inverse top-right; whole block pressablepressed .98/.90 (the code's card variant; no new physics entry); focus ring: the standard 2px ink ring, offset 2, outside the block (ink on white 19:1; one focus recipe app-wide); loading = skeleton block; empty = eyebrow + "일정 없음" 24/32HeroCard, SummaryCard, StatGrid hero, profile-hero-card
Strip (was CellRow)80; equal columns; numeral 24/32·700 ink; label 13/18·500 secondary; 1 × 24 #E5E5E5 between columns; pad 12 insidepressed per column (.98/.80) when linkedStatStrip, CellRow, ActionIconRow (icons 24 + 12/16 labels)
Hero (detail)top 24; eyebrow 13/18·500 secondary; 4; title 24/32·700; 16; three facts 17/24·500 with a 20 glyph (calendar, map-pin, card) + 8, gap 8; 32 below—TypeHero, SessionFactsSection lead, VenueHero (retire)
Chart160; inset 16; one ink line 2; no fill; scrub: 1px #D4D4D4 guide + 8px ink marker + readout count tag (ink) above; empty = 48 glyph + 15/22 secondary centredpan-scrub; reduced motion = no marker animationSparkLine (120 × 48)
Thumb / map tile56 × 56 r8; venue photo → static map → court tile cascade; map and tile carry a 1px inset #E5E5E5; club logo 56 r12loading = skeleton squareVenueTile (aspect-bound r12), ClubAvatar
Noticeinline; r12; pad 12 16; weak tone fill (success #CBEBE5, warning #F5B301, danger #B8332E with white, info #E8DCFB, neutral gray100); icon 20 + 12; title 15/22·600 + body 15/22·400; optional text action 15/22·600 right; optional close 24 in 44; never a surface of its own on a hubaction pressed .85; dismiss animates height 150 msBanner, Callout, Well, NetworkBanner (all $primarySubtle)
Avatarcircle; 40 / 48 / 56; initials 15/22·600 (40), 17/24·600 (48), 20/28·700 (56) ink on gray100; photo when present; club logo r12—AvatarBubble (default 32, mint), ClubLogoBubble
Avatar row40 × n, gap 8, from the rail; "+N" as a 40 gray100 circle 13/18·600row pressable .98/.80AvatarStack (overlap −6)

5. Rows ​

ComponentGeometryStatesReplaces
MediaRow80; pad 12; thumb 56 r8 + 12; L1 17/24·600 ink (time, tabular); L2 15/22·400 secondary (venue, MetaSegments); trailing: count 15/22·600 ink or one Tag, centred on L1; no dividerpressed .98/.80; selected = trailing check 16 + L1 600 (no fill); loading skeleton: 56 square + 17/24 line at 60% + 15/22 line at 40%SessionCardV2, SessionCompactRow, ClubCard, DirectoryVenueCard on list surfaces
PersonRow68 (§15); avatar 40 + 12; L1 15/22·600; L2 13/18·500 secondary; trailing: amount/score 15/22·700 tabular, ELO + level tag cluster, or one Tag; rank variant: 13/18·600 secondary rank in a 24 column before the avatarpressed .98/.80; selected check; disabled .5PersonRow, MemberRow, DuesRow, LeaderboardRow, RankRow
Cell48; label 15/22·400 secondary left; value 15/22·500 ink right; variants: nav (chevron 16), toggle (Switch), value + chevron, destructive (label danger), input (value editable); cell hairline #E5E5E5 rail-to-rail between cells of one group, none after the lastpressed .98/.80 (nav/value); toggle presses the switchFactRow, ListRow (34 files), SettingsRow, AdminNavRow, PickerRow
Roster cell100 pitch, 4-up; avatar 48; name 13/18·500 (20 row); level tag 24; empty slot = 48 dashed #D4D4D4 circlecell pressable (opens profile)ParticipationSection rows
ResultRowPersonRow with opponents in L1, date L2, score 17/24·700 tabular + 승/패 Tag (success weak / neutral weak)—completed-row, match rows

6. Labels ​

ComponentGeometryTones / tiersReplaces
Tagsm 24 r6 13/18·600 pad 0 8; md 28 r8 15/22·600 pad 0 10; passive (no press, no aria-pressed); ≤ 1 per row, ≤ 2 per object; label 1–2 wordsneutral (gray100 / secondary · solid #4F5851 / white), brand-weak (#EFF9B7 / ink), success (#CBEBE5 / #014B40 · #00796A / white), warning (#F5B301 / ink), danger (#B8332E / white), info (#E8DCFB / #4B0082 · #4B0082 / white)Badge (73 files), hand-drawn pills (9), ContextChip, MarkerPill, ReliabilityPill, TierRangeBadge in rows
Level tag24 circle; ball fill; 13/18·700 ink numeral; 1px #D4D4D4 edge on white—TierRangeBadge, EloTierBadge chrome
Count tag20 tall, min 20, r full, 11/14·700; danger solid (unread) or neutral; 99+ cap—CornerCountBadge
Live tagdanger solid Tag with a 6px white pulse dot (1600 ms) before "LIVE"reduced motion: static dotSessionLiveBand on rows, PulseDot

7. Controls ​

ComponentGeometryStatesReplaces
Buttontones primary (ball fill, ink label), tonal (gray100, ink), ghost (text only, pad 0 12), danger (#B8332E, white); sizes for every tone: sm 36 r10 15/22·600 · md 44 r12 15/22·600 · lg 52 r12 17/24·600 — lg for the CTA band and full-width submits, md for inline actions (empty states, notices, sheet content), sm inside rows and notices (§13 K3); full-width on the rail or content-width (pad 0 20)pressed .97/.92; hover .85; focus ring ink (ball button: ink ring); disabled .5; loading = spinner replaces the label at full opacity, width held (ink; white on danger — ink on #B8332E is 3.24:1); icon 20 + 8 leading optionalActionButton/Button (green, 56), ActionPill, hand-drawn pressable pills
IconButton44 target, 24 glyph, ghost; tones ink / secondary / danger / inverse (on block)pressed .92/.78; disabled .5; aria-label requiredHeaderIconButton, ActionIconButton, RemoveIconButton, SaveButton
Choice36 (44 in forms), r full, 15/22·500, pad 0 16/20; resting 1px #D4D4D4 on white; selected ink fill + white 700 label + leading check 16; modes: menu (chevron 16), input (× 16, 44 target), rangepressed .96/.85; disabled .5; aria-checked / arrow keys in radio groupsSelectionChip (56 files), DropdownChip, RangeChip, RemovableChip
Segmented (form)40; track gray100 r full pad 4; selected = white pill + 1px #D4D4D4 + 600 ink; others 500 secondary; equal columns (group-spread); settings and wizards onlyindicator slides navSlide; role="radiogroup"PillNav pill variant in forms
Switch48 × 28 track r full; off #D4D4D4, on ink; thumb 24 white with 1px #D4D4D4 edge; 150 msrole="switch" + aria-checked (fixes the a11y hole); disabled .5; pressed none (toggle is the feedback)TogglePill, Switch
Input48; r12; 1px #D4D4D4; pad 0 16; text 15/22 ink; placeholder secondary; label 13/18·600 above (4); helper 13/18 secondary below (4); error: border + helper #B8332E; leading/trailing 20 iconsfocus = 2px ink border (no ring); disabled .5; filled state unchangedStyledInput, LabeledField
TextAreamin 120, auto-grows to 200 then scrolls; counter 13/18 secondary bottom-right inside the padas InputStyledInput multiline, composer
Search44; r full; gray100; icon 20 + 8; 15/22 placeholder secondary; clear × 20 (44 target) when filled; cancel ghost button right when focused; header takeover on list screens (icon in header → field replaces the title)focus: field stays gray100, caret inkSearchBar
Stepper36 pill group: − / value 15/22·600 / +, each 44 targetdisabled at bounds .5Stepper
Slidertrack 4 #E5E5E5, fill ink, thumb 24 white 1px edge; live value tag above the thumb; band sliders keep the three grab surfacesreduced motion: no thumb springTimeBandSlider, RangeBandSlider, ValueSlider (retire)
Checkbox / radio rowsCell 48 with a trailing check (single) or a 24 checkbox square r6 ink when on (multi)aria-checkedSelectionCard lists
Date / time pickerbottom ModalPanel with the native wheel; title 17/24·600; CTA 완료 primary—raw react-native-modal-datetime-picker (6 files)

8. Overlays and feedback ​

ComponentGeometryBehaviourReplaces
ModalPanel (sheet)bottom; top radius 16; pad 20; title 17/24·600 + close 24 right; content; CTA band insideslide 200 ms; backdrop ink α0.4; tap-outside dismisses; no drag, no grabberModalPanel bottom
Dialogcentred; width 350; r16; pad 20; title 17/24·600; body 15/22 secondary; CTA pair (tonal + primary, danger for destructive)fade 150 ms; backdrop does not dismissModalPanel confirm
Menuanchored under the header action; width 240; r12; 1px #D4D4D4 edge; rows 48 (icon 20 + 15/22); destructive last in #B8332E after one cell hairline; ≤ 5 rowsfade 100 ms; no shadowHeaderActionsMenu (shadowed)
Toastink surface; r12; min 48; pad 12 16; text 15/22 white; optional action 15/22·600 ball text; 16 above the tab bar / FAB clears it2800 ms; swipe-down dismiss; no shadowToastHost (shadowed, r24)
Coachmarkink surface r12 with an 8px arrow; body 15/22 white; 다음 ghost ball textspotlight unchangedCoachmarkTooltip

9. States ​

ComponentGeometryReplaces
EmptyStatecentred; top 64; disc 80 gray100 with a 32 glyph secondary; title 17/24·600; body 15/22 secondary (≤ 2 lines); one md (44) tonal button (md primary when the empty state is the screen's action); variants empty / error (AlertCircle + 다시 시도) / offline (WifiOff) / not-foundEmptyState (mint disc), QueryErrorState, OfflineEmptyState, NotFoundState
Skeletonshapes at the v2 pitches: media row (56 sq r8 + 17/24 line 60% + 15/22 line 40%), person row (40 circle + two lines), cell (label 40% + value 20%), block 144 r16, strip 80, chart 160, hero (24/32 line 70% + three 17/24 lines) — all gray100, pulse .6 ↔ .9Skeleton, SkeletonCard, SkeletonHero
SpinnerActivityIndicator ink, small; inside buttons onlyraw indicators (8 files)
Offline stripNotice neutral, 350 × 48 (min; grows with text per B6), pinned under the header, "오프라인이에요" + 다시 시도 (the size every board draws, corrected 2026-10-10)NetworkBanner

10. Forms (wizard field grammar in v2) ​

WizardShell: header 56 with back and skip as real buttons per the owner's wizard-navigation LAW (a 40 gray100 back square and an sm tonal 건너뛰기, §21.2; corrected 2026-10-10 from "ghost text"), a 2pt progress line on the header's last 2pt — ink for done + current, #E5E5E5 for remaining (§13 K6; ball on white is 1.28:1), title 24/32·700 at 24, fields at 32 seams, footer CTA band with one primary (pair only on the confirm step). Field = label 13/18·600 (a required field adds the one $error asterisk, size-matched, per the owner's required-marker LAW, and starts unset) + control + helper; Choice sets wrap left-aligned gap 8; AddRow = 56 dashed #D4D4D4 r12 with + 20 and 15/22·600 (replaces AddCard); AttachedPanel → a Notice (neutral) under its field; PickerRow → Cell value + chevron opening a sheet; RemoveIconButton → IconButton danger; RevealStack keeps its 250 ms reveal; live-draft and sequential reveal unchanged. Validation: inline helper in danger under the field, never a toast.

11. Retire (no v2 counterpart by design) ​

Card on list surfaces (figure-only use remains for the detail hero image), SessionCardV2/ClubCard boxes, Badge, SelectionChip family, PillNav, SegmentedTabs, Banner/Callout/Well, CornerCountBadge, AvatarStack, SessionLiveBand, ProgressBar (becomes the block's progress and the Slider track), GlyphFactRow chip density, the 17 dead exports listed in the inventory. $primarySubtle, $surfaceTertiary as surfaces.

12. Open for the owner ​

Brand-weak tint #EFF9B7 for 모집 중 · the figure + level-tag cluster in rank/result rows · the FAB collapse-on-scroll behaviour · the toast action colour (ball text on ink) · whether 클럽's row logo keeps the identity hues at 56 r12.

13. Resolved 2026-10-09 (owner: "go with best practices and your recommendation") ​

#Question the boards raisedResolutionWhy
L1Header with back and a titleTab roots: title 20/28·700 at the rail. Pushed screens: back glyph at the rail, title 17/24·600 left-aligned at x 56, one line, ellipsis. Detail pages with a hero: no title at rest; the hero title fades into the header (150 ms) once it scrolls under it, together with the edge hairlineleft-aligned keeps the one text axis of the grammar (Material small top app bar); the scroll-linked title is Apple's large-title pattern and useCollapsingTopBar already provides the hysteresis
L2Today dot inline or hungHung in the gutter (6px at x 8–14), the date stays on the rail; "오늘" in ink 600alignment of the text edge beats the dot (hanging punctuation); every day header starts at x 20
L3Block focus ringThe standard ink ring, offset 2, outsideone focus recipe; WCAG 2.4.13's change-of-contrast holds (white → ink)
L4Notice / offline strip min height48on the grid; equals the Cell
L5Dark weak tintsTone at 16% over the dark canvas; dark info text #C9B2F2 — accepted, verified by the dark gate at S1derived values ship only behind the contrast gate
K1Toast offset16 above the tab bar, and 16 above the FAB when one is present (code's 8 changes)matches the FAB's own 16 offset; one seam
K2Unchecked checkbox2px #4F5851 edge (7.4:1)WCAG 1.4.11 needs 3:1 for a control boundary; the 1px hairline is 1.48:1; M3 uses a 2dp outline
K3Button sizessm 36 · md 44 · lg 52 for every tone; lg only for the CTA band and full-width form submits; md for inline actions (empty states, notices, sheet content); sm inside rows and noticesone size scale instead of size-by-tone
K4Derived dark values (tonal fill, switch track/thumb, backdrop, toast surface, danger label)accepted, gated at S1as L5
K5Toast status glyphkept (20, before the text) in the dark-theme tone values on the ink surface: success #75D1C6, warning #E1B75C, danger #FA9B94, info #C9B2F2a status glyph is the state, not decoration (glyph tiers); light tones fail on ink
K6Ghost padding; wizard progress lineghost pad 0 12 (text edge lands on the rail); progress line on the header's last 2pt in ink for done + current, #E5E5E5 for remaining (not ball)the ball segment is 1.2:1 on white and the line carries information (non-text 3:1)
—Block press physicsthe code's card variant .98 / .90no new physics entry

14. Components the pages need (added 2026-10-09 from the IA audit §4.0) ​

The page audit found eleven things the screens use that the library did not specify. Same rules as §3–§10: one recipe per pattern, ink controls, ball only on the screen's one action, no shadows, 4pt grid, states per §2.

ComponentGeometryStates / behaviourUsed by / replaces
HeaderTextActionghost text 15/22·600 ink, pad 0 12 in a 44 target, text edge on the right rail; at most one per side (§16 P3), with at most one icon beside it; label ≤ 4 syllables (관리, 초대 코드, 편집, 완료); never destructive; never in a wizard header, where the owner's wizard-navigation LAW makes back and 건너뛰기 real buttons (§21.2 WizardShell header)pressed .97/.92; disabled .5; role="button"club 관리 (IA-4), 초대 코드, edit mode; re-specs HeaderTextAction ($primary → ink)
ActionSheetbottom sheet (ModalPanel): title 17/24·600 = the object's name + close; body = groups of Cells 48 (leading icon 20 optional, label 15/22·400 ink; a chevron only on cells that open another screen); group title 13/18·600 secondary, 8 below; cell hairline inside a group; 24 between groups; destructive group last in danger, each with its own Dialog; no CTA band; max 80% height, scrollsslide 200 ms; tap-outside dismisses; focus returns to the triggerhost tools (up to 9), club 관리 (IA-4); any set of more than five actions (the Menu keeps ≤ 5)
CTA band helper lineinside the band, above the button: 13/18·500 secondary, ≤ 2 lines, left-aligned at the rail, MetaSegments; 8 above the button; band 118 → 144 (one line) / 162 (two lines)static; never a second actionsession detail fee + cancellation terms (IA-3), eligibility
MediaBandphoto: full-bleed 240 including the status bar; cover-fit; a top scrim ink α0.35 → 0 over the first 100 so the overlaid header reads; the header over it is transparent with white 24 glyphs on 36 ink α0.4 circles; COVER-ALWAYS (the club's default cover when it has none). map: 160 under the white header, static map with a 24 ink pin, a neutral solid Tag "지도" bottom-right at 12 insetphoto: past (240 − 100) of scroll the header turns white (150 ms), glyphs ink, the title fades in, edge hairline on; status bar light over the photo, dark after; map: tap opens the map app; loading gray100; error court tileclub hub, club preview, venues (photo); session detail when a venue map exists (map; IA-10, owner rulings COVER-ALWAYS and MAP-BAR-INK)
PostRowauthor line (avatar 40 + 12 · name 15/22·600 · time 13/18·500 secondary · ⋯ IconButton 44) → 12 → body 15/22·400, 4-line clamp + "더보기" 15/22·600 secondary → 12 → photos (1: 350 × 262 r12, 4:3; 2: 171² r8, gap 8; 3+: two squares, "+N" 20/28·700 white on ink α0.5 over the second) → 12 → action row 44: heart 20 + count 13/18·600, comment 20 + count, gap 16; posts separated by 32 of air, no line (the author line opens each post)body pressed .98/.80 opens the post; author opens the profile; skeleton (avatar + 2 lines + 350 × 262); pinned = neutral Tag "공지" trailing the author line; deleted = "삭제된 게시물" 15/22 secondaryclub 홈 feed; replaces post-row.tsx + FeedPhotoStrip
MessageBubble + Composerbubble max 264, pad 8 12, r18 with the sender-side corner 6 on the last bubble of a run, 15/22; mine: ink #15181A, white text, right; theirs: gray100, ink, with avatar 32 + name 13/18·600 secondary on the first of a run; runs within a minute stack at 4, new runs at 12; time 11/14·500 secondary beside the last bubble; date and system lines centred 13/18·500 secondary, 16 around. Composer: min 56 + home inset; attach IconButton 24/44; field gray100 r20, min 40, pad 8 16, 15/22, grows to 5 lines then scrolls; send = 36 ball circle with an ink arrow 20 in a 44 targetsend disabled .5 when empty; the bar rides the keyboard (KeyboardStickyView), the thread stays bottom-anchored; new bubble fades in with an 8 rise over 150 ms (reduced motion: none); edge hairline over the bar while content is behindDM thread; replaces the feature-local compose bar. Mine is ink, not ball: ball stays the one action (send)
Calendarmonth: header 44 (month 17/24·600 at the rail + prev/next IconButtons); weekday row 32 (13/18·500 secondary; 일 in danger, 토 in $series1); day cells 50 × 44 (7 × 50 = 350); numeral 15/22·500 in a 36 circle; today = 1.5px ink ring; selected = ink fill, white 600; range = ink ends + gray100 band; up to three 4px activity dots under the numeral; out-of-month days not drawn. Week strip: 7 × 50, day label 12/16·500 secondary over a 36 numeral circle 15/22·600; height 64; swipes by weekdisabled dates .5; role="grid", cells aria-selected기간 기록 activity calendar, 홈/date pickers (the wheel stays for times)
Podiumthree columns over 350 (2위 · 1위 · 3위), centred; 1위 avatar 56 raised 16; 2위/3위 avatar 48; a 2px medal ring (gold #D9A00F, silver #8E8E93, bronze #B2580B); under each: rank 13/18·600 secondary, name 15/22·600 (2 lines, never cut), ELO 17/24·700 tabular, level Tag; height 168column pressed .98/.80 opens the profile; the list below starts at rank 4leaderboards
ScoreEntryteam rows (names 15/22·600, 2 lines) × set columns of number Inputs 48 × 48 r12 with 24/32·700 centred numerals, numeric keypad, auto-advance; a quick-pick Choice row (6-0 … 6-4, 7-5, 7-6) fills the active set; a tiebreak field 36 × 36 appears at 6-6 / 7-6; validation helper in danger under the table; one CTA 저장focus = 2px ink border; error per field; never a toastone entry for host and participants (ruling 14); retires the ±1 stepper sheet
LiveScoreink Block 350 × 144 r16 pad 20: two team rows (names 15/22·600 white α0.85), set scores 24/32·700 white tabular in 32 columns, the current set and the game points (0 / 15 / 30 / 40 / AD) in ball; serving = 6 ball dot before the server; host controls below: two lg tonal Buttons (팀 A 포인트 / 팀 B 포인트) + an undo IconButtonpoint = haptic outcome + 150 ms numeral cross-fade (reduced motion: none); read-only for participantslive match board
Select modeentered from a HeaderTextAction "선택" (long-press is an accelerator, never the only path); header: 취소 (text, left) · "n명 선택됨" title · 전체 선택 (text, right); each row gains a leading 24 checkbox (2px #4F5851; checked ink fill + white check) at the rail and its content shifts 36; a row tap toggles; the trailing element stays; the CTA band shows the bulk action as lg primary ("3명 입금 확인"), disabled at zeroa money action asks a Dialog before committing (canon: confirm consequential mutations); defaults per ruling 12 (only 확인 대기 ticked); role="checkbox" + aria-checked per row; the count is announced politelydues reconcile, session-fee confirm (flows 3, 8)
CountdownNoticeNotice, warning tone (#F5B301, ink): title "입금 대기 중", the countdown 15/22·600 tabular right ("09:41 남음"), body = the account line; at zero it becomes danger ("시간이 지나 자리가 풀렸어요") with the action "다시 신청"ticks every second, digits do not animate; screen readers hear 5:00 and 1:00 onlythe payment hold (flow 3)

15. Amendments from the behaviour addendum (2026-10-09) ​

flat-v2-behaviour-addendum-2026-10-09.md (B6 text scaling, B7 motion, B8 accessibility semantics, B9 platform) changes these rows; its questions Q1–Q10 are ruled by its recommendations (owner: "go with best practices").

WhereWasNow
every fixed height that holds text (§3–§9)heightthe same number as minHeight (a floor); content may grow (B6.4)
text scalingunspecifiedfollow the OS 85–200% for reading text (cap 2.0, Q1); hero capped 1.7; numerals and labels 1.5; chrome (header, scope tabs, tab bar, FAB) 1.3; iOS per-style curves via dynamicTypeRamp; from 1.5× layouts stack — trailing element under line 2, Cells label over value, roster 2-up, dialog buttons stacked (Q2); header stays 56 and tab bar 56 + inset at every scale
PersonRow (§5)pitch 64pitch 68 — the two-line text block (22 + 4 + 18 = 44) is taller than the 40 avatar; 68 = 12 + 44 + 12 (Q9)
§2 Toast2800 ms, swipe-down4000 / 6000 / 8000 ms by kind (plain / with action / error), the Android accessibility timeout honoured, × on error and action toasts, tap to dismiss (Q6); undo label 되돌리기 (Q10)
§2 Reduced motiondrop the settle, keep the dimone table: translate, scale and layout removed; cross-fades become instant; loops stop after 5 s
motion tokens (§2)springs + mixed durationseffects on timing: 100 ms (hairline, hover, exits) · 150 ms (fades, skeleton → content) · 200 ms (toggles, FAB width, sheet exit) · 250 ms (reveals) · 300 ms (sheet rise); curves standard (0.2, 0, 0, 1), enter (0, 0, 0, 1), exit (0.3, 0, 1, 1); press springs unchanged; Tamagui quick/medium/slow no longer drive opacity
lists (§5)unspecifiedno layout or entering/exiting animation inside virtualised lists: a row that moves (예비 → 참석) fades out 100 / in 150 and siblings snap; short stacks (≤ 24 children) may use a 200 ms layout transition
§2 FAB on scrollcollapses in 150 mswidth 200 ms, label out 100 / in 150, with a direction dead-band
§8 Sheetslide 200 ms, no dragrise 300 ms / exit 200 ms; iOS: swipe-down on the title row dismisses (HIG; Q3), Android unchanged
§6 Live tag pulse1600 ms loop, indefiniteloops 5 s per mount, then rests on the static frame (WCAG 2.2.2)
§3 Tab barlabel 11/14 · pressed .92/.78iOS keeps the non-scaling label + Large Content Viewer; Android caps the label at 1.3; the press physics come from a custom tabBarButton on Pressable variant="icon" (the JS tab bar sets pressOpacity: 1 today)
accessibility names (B8)per component rolesone focus stop per row with a comma-joined spoken name built from the row's own view model (월/일/요일, 오전/오후 N시부터 M시까지, N명 중 M명 참석, NTRP 3.0부터 4.0); state in aria-*, never in the name; exactly one visible h1 per screen; toasts announced on iOS explicitly
platform (B9)—predictiveBackGestureEnabled stays false (RN 0.86 + screens 4.26 have no back-progress API); fullScreenGestureEnabled: false on slider and wizard screens (iOS 26 full-screen swipe-back; Q7, OTA); ModalPanel adds navigationBarTranslucent; native tabs not adopted (they cannot report bar height, which the FAB/toast/CTA stack needs); Bold Text weight mapping later (Q8); the 3-button-navigation seam fix waits for the emulator check and the native batch (Q5)

16. Resolved from the patterns board (2026-10-09) ​

#QuestionResolution
P1ScoreEntry quick pick and tiebreaka quick pick credits the top team first (tap again to swap); the tiebreak is a 44 row under the table with a 36 field at the right; set columns are labelled 1세트 …
P2ActionSheet heightcap 88% (the code's ModalPanel value); the body scrolls and the destructive group stays last, even below the first screen
P3HeaderTextAction countat most one per side (select mode needs 취소 left and 전체 선택 right)
P4LiveScore numeralsinstant (B7 governs: scores and counts never animate)
P5Text action over a MediaBand photowhite 15/22·600 on an ink α0.4 pill, 36 tall, matching the 36 icon circles
—MediaBand map press / focusdim .85 without a settle; focus ring inset 2
—PostRow clamp4 lines with 더보기 inline on the fourth (code clamps at 5 today)
—MessageBubble sending / failedsending: a 12 clock glyph before the time; failed: "전송 실패" in danger + a retry text action under the bubble
—Week stripno activity dots (the 64 cell has no room)
—Podium height168 is a floor; a wrapped name grows its column
—CTA bandthe spec stands (16 + 52 + 16 + inset; hairline only while content scrolls under it); the code's max(16, inset) and permanent border change at build
—MetaBreak at large texta break must never end a line: MetaSegments keeps the break with the following segment (a build fix, found at 130%)

17. Rulings from the page boards (2026-10-10) ​

#QuestionRuling
H1Header trailing ordera HeaderTextAction sits left of any icon; the overflow ⋯ is always the rightmost item (Material: overflow last) — e.g. [관리] [⋯], [북마크] [⋯]
H2홈 Block targettapping the day Block opens 경기 › 내 경기 (a tab switch, scrolled to that day)
H3오늘 복기 rows and the recap card on 홈leave 홈 with the scope row; results live in 기록 and arrive by push — 홈 is the schedule
H4Club pane label순위 (shipped; 기록 is reserved for the tab)
H5회비 month controla Choice ("10월 ▾") opening a bottom Sheet of every month since dues began, newest first, current month checked — never a Menu (the Menu caps at five, older months must stay reachable)

18. Page-board rulings and the components they added (2026-10-10) ​

18.1 Rulings (list-a D1–D8) ​

#QuestionRuling
D1Dismissing notificationsSelect mode is the path (accessible, discoverable); SwipeAction is an accelerator only (§18.2); unread = a 6px danger dot hung in the gutter (x 8–14) + "읽지 않음" prefixed to the spoken label
D2Request rows (메시지 요청, applicants)an inline answer pair under line 2: two sm (36) tonal Buttons (수락 · 거절) — neutral, so the screen's one coloured action is unaffected; request lists only
D3Header subtitleyes: the 64 Header with a 13/18 secondary subtitle names the context (club, session) on pages reached from it
D4Row ⋯always an ActionSheet (bottom, thumb-reachable, destructive last with its Dialog); the anchored Menu is for the header ⋯ only
D5앨범 grid3-up full-bleed with 2pt gaps (the photo-grid standard); tile size is derived from the width and exempt from the 4pt grid, like any computed media size
D6참가비 status wordsthe terminology guide governs (입금 필요 · 입금 확인 중 · 입금 완료); flow 3's draft words are superseded
D7신고 관리 entrya persistent Cell in the 관리 ActionSheet for officials, with a count Tag when there are reports
D8Unresolved 출석 확인 rowsan inline sm tonal pair 참석 · 불참 (an explicit choice; a bare row tap never records attendance); closeout enables when none is unresolved

18.2 Components added (gaps from the hub and list-a boards) ​

ComponentSpec
Unread marker6px danger dot hung at x 8–14, centred on line 1; spoken prefix 읽지 않음
SwipeActionrow swipe left reveals one 72-wide action (danger for delete, ink for archive); full swipe commits with an undo toast; accelerator only — every swipe action also exists in Select mode or the row's ActionSheet; disabled in Select mode; reduced motion: no elastic
AnswerPairtwo sm tonal Buttons (gap 8) under line 2 of a request row, left-aligned with the text; the row grows by 44; each button 44 target
PhotoGrid3-up full-bleed, 2pt gaps, square tiles; long-press enters select; the +N overflow tile pattern from PostRow
SectionHead variantscount suffix (17/24·600 title + 15/22·500 secondary count, gap 4) · command action (13/18·600 ink, no chevron, e.g. 모두 읽음) · ExpandToggle slot (the one chevron collapse)
Tabs item with counta count Tag (20, neutral, or danger when it needs action) 4 after the label
FilterSheetbottom Sheet: groups of Choices (group title 13/18·600 secondary, 8 below, 24 between groups); footer pair 초기화 (tonal) + "결과 n개 보기" (primary, the count live); trigger: a Choice with a chevron and the active count, followed by removable Choices for each active filter
Edit Sheet footeran edit Sheet has no ×; its footer is the pair 취소 (tonal) + 저장 (primary); unsaved changes on dismiss ask a Dialog
CTA band, danger tonethe band's one button in danger for a destructive bulk action (알림 n개 지우기), always preceded by a Dialog
PersonRow trailing metatrailing 13/18·500 secondary text (time, rate) in place of a figure or Tag
Caption row13/18·500 secondary text at the rail, 32 tall, for a legend or a subtitle under a section head; no action
Activity heatmap7 rows × n weeks of 12 cells, gap 4, r3; five steps from gray100 to ink; month labels 12/16 secondary above; a scrub readout like the Chart's
MediaBand album overlayphoto band count Tag (neutral solid "1 / 12") bottom-right and pager dots (6px white α0.6, current white) bottom-centre
PostRow system variantfor 경기 결과 and 사진 업로드: the author line is the club (logo 40) with a 13/18 secondary line naming the event; body is the result (ResultRow) or the PhotoGrid preview; no ⋯
Club hero, place variantthe club hub's identity under the MediaBand: logo 56 r12 overlapping the band bottom by 16, name 24/32·700, region and members as a MetaSegments line 15/22 secondary — no glyph facts
Header icon badgethe tab-bar dot or count Tag on a header icon (top-right of the 24 glyph, ring 1.5 canvas)
Task CellCell with an ink 15/22·500 label, a count Tag before the chevron; used by the officials' 할 일 section
Two-line CellCell with a 13/18 secondary second line under the label (dates, captions); min 64
Reaction pickera Sheet with a wrapped row of reaction Choices (emoji + count; the emoji are content here, not fact glyphs); the viewer's choice ink-filled

19. Rulings and components from the detail board (2026-10-10) ​

19.1 Rulings (detail D1–D6) ​

#QuestionRuling
D1Session header for a host or official[관리] [⋯]. 일정 수정 is the first row of the 관리 sheet's 운영 group; it enters the page's edit mode as the header pencil did. This keeps the bar at two trailing items, which was the owner's own reason for the pencil (EDIT-PENCIL: "back to two glyphs with ⋯, where it had three"). The in-row pencils of edit mode are unchanged. Everyone else keeps [북마크] [⋯].
D2참석 취소 for a joined memberthe CTA band's lg tonal Button. A ghost reads as floating text and hides the one action a joined member has. The danger tone belongs to the confirming Dialog, not to the band. The screen has no ball action in this state, because the RSVP is done.
D3Map band on 일정 상세the map band shows whenever the venue has a map (one page structure for every session; owner 2026-09-20 hero ruling). The fold test is that the roster's head and first row sit above the CTA band, not the whole roster: the head's count gives the total, and a second row cut by the band is the scroll cue (NN/g, "illusion of completeness"). At the corrected band geometry (§19.2) an eight-slot roster starts at 512 and its first row ends at 644, under the 700 top of the tallest band (144). The board's alternative, a map only for one-row rosters, would give two page structures that depend on the roster size. A venue without a map keeps the text hero.
D4Score entry on the 경기 보드ScoreEntry opens in a Sheet over the board, with the keypad up, one 저장 and an action Toast with 되돌리기 (6 s, §15) that restores the previous score. Errors stay inline (§14).
D5공개 프로필 headertwo icons, [메시지] [⋯]; 공유 moves into the ⋯ Menu with 신고 and 차단 (three rows, under the Menu's five).
D6Order of workthe components in §19.3 are added to the library before any page is built from the detail board (contract §1).

19.2 Corrections to earlier sections ​

WhereWasNow (why)
§14 MediaBand map: geometry160 under the white headerthe photo variant's geometry: full-bleed 240 including the status bar; the top scrim (ink α0.35 → 0 over the first 100); a transparent header with white 24 glyphs on 36 ink α0.4 circles that turns white past 140 of scroll. Owner ruling MAP-BAR-INK (2026-09-28) gave the map the club photo's treatment. One recipe now covers every media band.
§14 MediaBand map: labela neutral solid Tag "지도" bottom-rightno Tag or chip on the map. Owner ruling MAP-NO-CHIP: the map is its own cue.
§14 MediaBand map: pressopens the map appopens 장소 상세 (the venue page). Owner ruling MAP-NO-CHIP: the map "leads back to the location page". Directions live on the venue page.
§14 MediaBand map: pin—the 24 ink pin is centred in the visible area under the header (y 170 of 240), not in the band.
§18.2 Two-line Cellmin 64min 68: 12 + 22 + 4 + 18 + 12, the same text block and pitch as PersonRow (§15)
§18.2 Reaction pickera Sheet with a wrapped row of reaction Choicesreplaced by Reactions in §19.3: one quick row, one chip summary

19.3 Components added (the detail board's library gaps, part K) ​

ComponentSpec
ExpandToggle, lone row48 full-width row; the label is centred, 15/22·600 secondary, and states its count ("4개 더 보기" / "접기"); a 16 chevron after the label rotates 180° (200 ms; instant under reduced motion); aria-expanded. The label is never a bare chevron, because an icon-only toggle hides what it does.
ExpandToggle, disclosure Cella Cell whose trailing chevron points down (closed) or up (open); a right chevron always means navigation. An optional value (for example a date) sits before the chevron. Opening reveals the body under the label: 15/22·400 ink, 12 below the label, 16 at the bottom. Cells open independently, and opening one never collapses another, because auto-collapse makes the content jump under the finger (NN/g, accordions on mobile). Used by FAQ, 공지사항 and long fact lists.
Cell, reason linethe two-line Cell (68) with a 13/18·500 secondary reason under the label, for a locked field ("시작 후에는 바꿀 수 없어요"). The label stays ink and readable. There is no chevron and no pencil, and the row is not pressable.
Cell, Tag valuethe value slot may hold one Tag sm (24) in place of text; the Cell stays 48; a navigating Cell keeps its chevron after the Tag.
Roster doorwhen people outnumber the visible cells (eight, two rows of four), the last cell is a door: a 48 gray100 circle with "+N" 15/22·600 ink and the name line "외 N명". The door opens 참석자, as does the section head's 전체 보기.
Hero facts: count and statethe facts block holds one to three facts, each 24 tall with 8 between. The hero is as tall as its facts, and no placeholder fact is drawn; with no facts the block and the 16 above it go. One viewer-state Tag (sm) may trail the fact it qualifies, with 8 between (참가비 15,000원 → 입금 완료). At most one per hero, and it counts toward the object's two Tags.
PersonRow trailing alignmenttext, figures and Tags (≤ 24 tall) centre on line 1; controls taller than 24 (a 36 Choice, sm Buttons) centre on the row. One trailing element still.
Composer reply targeta 44 row inside the composer, above the field: "OOO님에게 답글" 13/18·600 secondary, then a one-line excerpt 13/18·400 secondary that ellipsizes. The excerpt is quoted text, not a name, so it may crop. A × IconButton (20 in 44) clears the target. The row appears with the field's focus, and Escape or × removes it.
Avatar, grouptwo member avatars at 0.6× inside the avatar box, offset diagonally: the back one top-left, the front one bottom-right with a 2px canvas ring. They use photos when present, and initials otherwise. Spoken as the member names. Identities stay photos, never a generic glyph.
Avatar, anonymousa gray100 circle with a user glyph (20 in 40, 16 in 32) in secondary, spoken "참석자". It is used where privacy hides names: a non-member's preview shows counts, never names (club avatar-privacy canon).
Commentavatar 32 at the rail; text column at x 64. Line 1 is the name (15/22·600) + MetaBreak + time (13/18·500 secondary), with a ⋯ IconButton (20 in 44) at the right. The body is 15/22·400 ink in the text column: full on the post page, 4 lines + 더보기 in previews. Under the body is a 답글 달기 13/18·600 secondary text action (44 target via hit slop). Replies: one level, avatar 24 at x 64, text column at x 96; deeper replies flatten with an @name; more than two replies collapse behind a lone ExpandToggle aligned to the text column ("답글 3개 더 보기"). Spacing: 24 between comments and 12 between a comment and its replies. A deleted comment reads "삭제된 댓글입니다" (secondary) and keeps its replies. The body indents to the name because short, threaded text belongs to its author. PostRow keeps its full-width body for long content and photos.
Live rowmin 24, directly under the header: the Live tag (§6), 8, then the date and venue as MetaSegments 13/18·500 secondary at the rail; 8 above and below. Replaces the LIVE band on 스코어카드 and 경기 보드.
Strip with deltasan optional delta line 12/16·600 secondary under each label ("+3", "−2"; a plain sign, never red/green or arrows — deltas are numbers, not status). The Strip grows 80 → 100 for all its columns. The comparison ("지난달 대비") is named once, in the caption row under the section head.
Form striphead-to-head and player records only, never in rows: rows keep one trailing figure, and colour-only dots fail WCAG 1.4.1. The strip is five 24 × 24 r6 cells, gap 4, in chronological order left to right. 승 = ink fill with white 12/16·700; 패 = gray100 with secondary. Spoken "최근 5경기: 승, 승, 패, 승, 패". In a row, recent form is line-2 text ("최근 5경기 3승 2패").
ScoreEntry, Sheet form(amended §23) the Sheet rises (300 ms) without the keyboard, showing the title row (the match name, §19.5), the set table and the quick-pick row (6-0 … 6-4, 7-5, 7-6): a common result is one tap. Tapping a set field raises the keypad. The quick row folds away while typing (200 ms; instant under reduced motion) and the sheet docks to the keyboard top (KeyboardStickyView), with 저장 right above the keyboard and no home inset. The tiebreak row appears only at 6-6 / 7-6, and inline validation sits above 저장. Measured worst case while typing (tiebreak + validation): 372 + the 291 keypad = 663, 79% of the screen, under the 88% cap (§16 P2); the open state is 454 over the home inset. Dismissing with unsaved digits asks the Edit-Sheet Dialog (§18.2).
ActionSheet (scope)the ActionSheet (§14) is the generic sheet for any object's actions, not a 관리-only sheet: row ⋯ (§18.1 D4), a message's long-press, a comment's ⋯. A leading 20 icon is optional per instance, consistent within one sheet.
Reactionsone component in two parts. Quick row: at the top of the object's ActionSheet (a message's long-press, a post's reaction trigger), the content set in 44 gray100 circles with the emoji at 24. The viewer's current reaction has a 2px ink ring, and a tap toggles it and closes the sheet. Summary: under the bubble or post, chips 28 tall, r full, gray100, pad 0 8, emoji 16 + count 13/18·600 tabular. The viewer's own reaction has a 1.5px ink ring. Tapping a chip toggles that reaction; long-press lists who reacted, in a Sheet of PersonRows. There is no centred panel variant. Emoji are content here, not fact glyphs.

19.4 Build notes from the detail board ​

  • Three reaction vocabularies exist today: match reactions (thumbs_up, fire, muscle, clap, heart, reaction.entity.ts), post reactions (like, clap, fire, heart, laugh, a local map in post-row.tsx) and DM quick reactions (👍 ❤️ 😂 😮 😢 🎾, messaging/.../helpers.ts). The Reactions component reads one domain registry. Ruled 2026-10-10: one set of six for every surface, in this order: 👍 ❤️ 👏 🔥 😂 🎾 (approval, affection, applause, intensity, humour, and the sport). The build unifies the match and post DB enums and the DM constant into that registry; the migration is cheap while the database is not live. The boards draw the six.
  • The host's header pencil (EDIT-PENCIL) becomes the 일정 수정 row of the 관리 sheet (D1). The edit mode it opens, and its row pencils, are unchanged.
  • The map band's code (SessionHeroSection) already uses the photo scrim and white glyphs (MAP-BAR-INK shipped 2026-09-28), so §19.2 aligns the library with the shipped page rather than changing it.

19.5 Settled while drawing (detail board patch, 2026-10-10) ​

ItemRuling
ScoreEntry title rowthe match name ("2라운드 1번 코트"), not the teams: the teams are the set table's row labels, and two doubles pairs do not fit one 17/24 line without repeating the table
Group avatar sizesthe inner avatars are 0.6× rounded to the grid: 20 in a 32 box, 24 in a 40 box, the front one inside a 2px canvas ring
Disclosure Cell bodythe bottom pad stays 16 (spacing stays on the 4pt grid). A box whose height comes from its text lines (n × 22) is a listed exemption from the 4pt height check, never a reason to bend a pad to 17 (contract §5)
Offline strip over a MediaBandthe strip attaches to the header's bottom edge in the header's own layer; over a photo or map it is solid (gray100 fill, ink text), never translucent, and it pushes no content
ActionSheet group titlesa group title shows only when the sheet has two or more non-destructive groups; a single group and the destructive group are untitled (the danger colour marks the latter); 24 still separates groups
PostRow counts vs Reactionsfeed: the action row's heart carries the total reaction count, with no chips in the feed. Post page: the per-emoji chips sit under the body, and the heart has no number. Each count is shown once.
Content under a MediaBandthe first element starts 24 below the band. A Notice in that slot is followed by the hero at the hero's own 24 top
Strip delta: zero or no datazero shows "0"; with no comparison data the delta line stays empty in that column (its height is kept so the columns align) and never shows a dash. If no column has data, the Strip returns to 80 and the caption is omitted
Form strip datathe last five completed head-to-head matches, in chronological order, from the existing head-to-head query (no new backend). With fewer than five matches, only those cells are drawn; with none, the strip is omitted

20. Rulings and components from the list-B board (2026-10-10) ​

20.1 Rulings (list-b L1–L6, G) ​

#QuestionRuling
L1친구 찾기 result rowthe row opens 공개 프로필; 친구 추가 keeps its own target and focus stop. This is the PERSON-ROW canon ("the default tap opens the profile"); today's static row is out of canon.
L2New copy on the boardapproved as drawn. It goes through the terminology guide and the house-style gates at build. A confirming Dialog names the person and the consequence ("정우진님을 친구에서 삭제할까요?"), per the confirm-consequential-mutation canon.
L3테니스장 kind chipsinto the FilterSheet's first group (single choice: 전체 · 테니스장 · 연습장 · 샵, with counts). The active kind shows as a removable Choice after the 필터 trigger (§18.2). 가까운 순 stays on the page as the sort Choice. 저장한 코트 leads the browse as its own section (a cap and 전체 보기), not as a filter. The pane already sits under the 경기 tabs, and a second chip row would be navigation by chips, which the flat grammar retired.
L4Level Tag on 리더보드 rowskeep BADGE-EXCEPTION (owner ruling 2026-09-23): the level Tag shows only where a level run starts, and the Podium always shows it. Library §5's "every rank row" is amended to this. The Tag sits in a reserved slot so the figures align (§20.2).
L5경기 기록 facetssplit 정렬 from the filters. Sort changes order and filters narrow; a sort buried in a seven-facet sheet hides the current order. 정렬 is a Choice ("최신순 ▾") that opens a Sheet of radio Cells (every Choice ▾ opens a bottom Sheet, H5). The seven filters stay in one FilterSheet with 초기화 and the count CTA pinned.
L6Unreachable 업적 stateskeep them drawn and marked "not reachable today". The shipped screen renders progress and hidden states, so the build keeps their spec. Build note: until a non-챌린지 achievement can be earned, the unreachable states are dead UI, so the unlock engine is either scheduled or the states are deleted (no dead code).
GTen gapslibrary first; mapped below. Six were already specified (§18.2, §19.3); four are new (§20.2).
Gap (list-b)Library
Request answer pair§18.2 AnswerPair (both buttons tonal, §18.1 D2)
Text row without an avatar§20.2 Text row
Achievement tile§20.2 Thumb, glyph tile
Filter Sheet§18.2 FilterSheet
Section count suffix§18.2 SectionHead variants
Edit Sheet§18.2 Edit Sheet footer
Row trailing control§20.2 Row trailing control (and §19.3 alignment)
Rank figure column§20.2 Figure column
Four-digit rank column§20.2 Figure column (rank width)
Permission-wall EmptyState§20.2 EmptyState, permission

20.2 Components added ​

ComponentSpec
Text rowthe PersonRow text block without leading media: L1 15/22·600 ink, L2 13/18·500 secondary, min 68, one trailing element (Tag, figure or chevron). Used for lists of documents, receipts and history entries (동의 이력, 제안 내역). A Cell is a label/value pair inside a settings group; a Text row is an object in a list.
Thumb, glyph tile56 × 56 r12 gray100, with the object's 24 glyph centred. This is the object's identity image, the way a photo or logo is, so the no-glyphs-in-rows rule (which covers inline fact glyphs) does not apply. Achievements: when earned, the glyph takes the achievement's tier colour (ledger tiers); when locked, the glyph is secondary and the row's trailing figure is the progress ("7/10" 15/22·600 tabular). A hidden achievement shows "?" 20/28·700 secondary with L1 "숨겨진 업적". No weak tint fills (S2 neutrals are pure grey).
Row trailing controla row's one trailing element may be a control: an IconButton (20 glyph in a 44 target) or a sm tonal Button. It centres on the row (§19.3) and is its own focus stop. The row's press area excludes the control's target, and pressing the control never presses the row. Spoken as two stops: the row's composed label, then the control ("정우진, 친구 추가").
Figure columnranked and scored lists right-align their trailing figure (15/22·700 tabular) in a column as wide as the widest figure on the page, so the digits line up. The level Tag (run starts only, L4) sits in a reserved 24 slot, 8 before the column, so a row without a Tag keeps the same figure position. The rank column fits the widest rank on the page: 24 (1–99), 32 (100–999), 40 (1,000+), 13/18·600 tabular, right-aligned, 12 before the avatar.
EmptyState, permissionthe content area's centred block: 48 lock glyph secondary → 16 → title 17/24·600 ink ("로그인이 필요해요") → 8 → body 15/22 secondary (one line naming what sign-in unlocks) → 24 → one md Button (primary when it is the screen's only action, else tonal), drawn only when an action resolves the wall (로그인). A role wall, which the viewer cannot resolve, has no button; the header's back is the way out. It joins first-run and filtered-empty as the third kind of empty (contract §3) and never appears on a hub.

20.3 Medal colours and the glyph tile (settled 2026-10-10 from the list-B patch) ​

The glyph tile's "glyph in the tier colour on gray100" fails WCAG 1.4.11 for two tiers: silver #8E8E93 reaches 2.91:1 and gold #D9A00F 2.09:1, because the ledger made the tier colours as fills, not as glyph colours. The code also has two medal sets: shared-semantics.ts (#D9A621, #A8B0B8, #B8793A, #D9E1E8) and the ledger and Podium set. Resolved:

TierFillGlyph on the fillContrast
bronze#B2580Bwhite4.89:1
silver#8E8E93ink #15181A5.47:1
gold#D9A00Fink7.63:1
platinum#4A6378 (new; L* 41, ΔE76 ≥ 22 from every other tier, 57 from the locked tile)white6.27:1
  • Earned glyph tile = the solid tier fill with the glyph in its on-colour. Locked = gray100 with the glyph in secondary. Earned and locked never look alike (ΔE76 ≥ 36 between any fill and gray100), and the colour marks the achievement the viewer has.
  • One medal set: these four values feed the glyph tile, the Podium rings and every medal use. The shared-semantics.ts medal set retires at build. Dark mode keeps the same fills, since the glyph carries the contrast.

21. Rulings and components from the forms board (2026-10-10) ​

21.1 Rulings (forms A1–F1, plus the departures the board reported) ​

#QuestionRuling
A1Is the step-by-step 일정 만들기 a visible entry?No separate entry. A host's first creation, and any creation with nothing to copy (no 정기 일정 and no past session), opens the step-by-step path automatically. Every other 새로 만들기 opens the blank review page, whose required Cells carry the asterisk and whose CTA helper names what is missing. A third door in the entry Sheet would add a choice without adding a task. 장소 opens the full-screen Search takeover, per the VenueFinder canon (IA §4.7).
B1클럽 만들기 quick pathdrop the 지역 screen when the profile has a region (6 taps). The region shows on the final review as an editable Cell, so it is still answerable. IA ruling 10 already pre-answers it.
C1Leave guard and copyadopted: create forms ask 나갈까요? (처음부터 다시 작성해야 해요), edit forms ask 수정을 그만둘까요? (바꾼 내용은 저장되지 않아요), and every form with unsaved input gains useUnsavedChangesGuard (never usePreventRemove)
D1프라이버시 rowseach row's trailing Choice ▾ opens a bottom Sheet of radio Cells, never a Menu (one pattern for every Choice ▾, §17 H5), and the level's meaning is the second line (Two-line Cell). The title stays 공개 범위 설정: it says what the page does, which matters to the low-digital-literacy audience, and the settings entry keeps the same words.
E1Routed 관리 page versus the 관리 ActionSheetretire the routed page as a menu. The 관리 ActionSheet is the one list of management doors, built from one registry (today's admin-tab rows). The attention counts (club-admin-attention.ts) move onto its Cells as count Tags (Task Cell, §18.2), and the header 관리 action carries a Header icon badge when anything is pending. /clubs/[clubId]/admin stays as an alias that opens the club hub with the sheet presented, for old links. Two lists of the same doors would drift.
F1Twelve gapslibrary first; specified in §21.2
—건너뛰기 drawn as ghost textagainst the owner's wizard-navigation LAW (2026-08-15: "top controls must look like buttons, not bare glyphs"). The wizard header is the §21.2 WizardShell header. The library's ghost HeaderTextAction is for non-wizard pages only.
—Required fields drawn without the asteriskagainst the owner's required-marker LAW (2026-08-15, canon wizards.md): every required control's label carries the one LabeledField required asterisk ($error, size-matched), and the field starts genuinely unset. This applies to the step path and to unset required Cells on the review page.
—경기 유형 shown only when it differs from 정기always shown: every editable field has its Cell on the review page, and a default shows its value. A field hidden at its default cannot be changed.
—Save confirmation as a Toastadopted (a success needs no dialog; dialogs confirm before a consequential action)

21.2 Components added ​

ComponentSpec
Cell, edittrailing pencil (EDIT-PENCIL: the shared EditCue, 16 glyph tertiary, the same size and tone as the chevron), never beside a chevron, spoken "장소, 수정"; opens the field's bottom edit Sheet (EDIT-SHEET)
Cell, lockedthe §19.3 reason line plus a trailing 16 lock glyph in tertiary, in the chevron's slot. Not pressable.
Cell trailing slotone of: value text, a Tag (§19.3), a Choice ▾ (sm 36, opens a Sheet), an sm tonal Button or IconButton (§20.2), a Switch, or a count Tag before the chevron (danger when it needs action, Task Cell). Only one per Cell.
Cell, label Tagone Tag sm 8 after the label (e.g. 신규). The value slot then holds text only. It counts toward the object's two Tags.
WizardShell header (owner LAW)left: a 40 × 40 r12 gray100 square with a 20 ink chevron (× on step 1, with the exit Dialog) in a 44 target. Right: 건너뛰기 as an sm tonal Button, never ghost or plain text. Centre: the progress, in ink. The footer holds the single CTA band and its helper line only. Hardware back mirrors the header back (useWizardBackHandler).
Dialog, one actionfor an acknowledgement that blocks (a version gate, a rule the user must read): title + body + one full-width primary 확인. The backdrop does not dismiss it. Never used where a Toast would do.
Search takeoverfull screen, rising 300 ms with the keyboard up. The bar is a Search field (40 r12 gray100, 15/22) filling the header, with 취소 as the text action on the right. Below: recent searches as Text rows with a × trailing control, then results as MediaRows. Empty = the filtered-empty EmptyState plus the contribution row ("찾는 곳이 없나요?"). It is the one venue-pick surface (VenueFinder canon).
Map face in a formafter a venue is picked: a 350 × 140 r12 static map with the 24 ink pin, a 1px inset #E5E5E5, 8 under the 장소 Cell; a tap reopens the Search takeover; loading gray100; error = the court tile
Photo tiles (composer)4-up wrapping rows, tile = (350 − 3 × 8) / 4 (computed, a listed exemption), r8. Remove = a 24 ink α0.6 circle with a white 16 × at top-right inset 4, in a 44 target. The add tile is gray100 with a 24 + and the count against the content's real limit (a club post holds four: "2/4"). Long-press drag reorders, with an accessible move action.
Field skeletona 13/18 label bar at 30% width, then 8, then a 48 gray100 r12 box; one shimmer per screen per §9
Input counter"12/20" 12/16·500 secondary, right-aligned on the helper line under the field. At the limit it turns danger and further typing is refused. Spoken only at 80% and at the limit.
Caption row, two linesthe §18.2 Caption row may wrap to two lines (min 32, grows); past two lines the copy is too long and is rewritten, never clamped
Time range wheela Sheet titled with the field name. Tabs 시작 · 종료 show their current values, and one native time wheel (오전/오후 · 시 · 분) edits the selected tab. 완료 commits both. For a range that cannot cross midnight, an end before the start shows the danger helper "종료가 시작보다 빨라요" and disables 완료. A range that may wrap (방해 금지 시간) accepts it, and the end value reads "다음 날 오전 7:00" (settled on the forms board).

22. Rulings and components from the auth board (2026-10-10) ​

22.1 Rulings ​

ItemRuling
Owner question: sign-up routeopen (EXT-11, the owner's). The board draws Kakao OAuth as the target, plus the interim email sign-up as a labelled variant.
Consent before birth yearreversed: the birth year comes first. The shipped wizard collects it at step 1 so the under-14 gate runs before any consent is collected (onboarding-wizard-screen.tsx, PIPA §15/§22/§22-2), and the legal order is not traded for a tap. Collecting the name before consent would collect personal data unconsented, so the name follows consent. New-user path: A1 시작 → 출생 연도 (the under-14 Notice inline) → 약관 동의 → 이름 (prefilled from the provider when given) → 첫 시작. That is 6 taps (from 12 today), one more than IA Flow 1's 5. The flows board counts 6.
Value props, welcome and done stepsretired. A1's single brand line carries the promise, and the first-action chooser ends the flow. Carousel tutorials are mostly skipped and teach little; the chooser does real work.
Provider cancelleda 13/18 danger helper line above the provider button, not a danger Notice. A user's own cancel is not an alarm.
나중에 할게요a ghost Button under the chooser's Cells (six syllables exceed the header text action's four). It is a content action, not navigation.
Consent prefix[필수] / [선택], the shipped strings (onboarding.ts). The board's parentheses are corrected; one wording per concept.
Draft resume and sign-outsign-out from onboarding clears the persisted draft, so the close Dialog's "저장되지 않고" stays true (build note)
Wrong credentialsplain-language error, no invented link. Gap recorded: there is no password reset for email accounts. The build adds "비밀번호를 잊으셨나요?" with Supabase's reset mail, which needs the deep-link return (backlog, owner to schedule).
만 N세 readoutdropped: year-only arithmetic overstates the age until the birthday
Kakao label in English"Login with Kakao", Kakao's mandated English label, which overrides our "Sign in" wording on that one button

22.2 Components added ​

ComponentSpec
ProviderButton (Kakao)the lg Button geometry (52, full width) with Kakao's mandated values: container #FEE500, the speech-bubble symbol #000000 (left-aligned or centred with the label), label #000000 α0.85 in the system font, radius 6, label "카카오 로그인" / "Login with Kakao". At large text the label stops at the 1.5 cap and the container grows so the label stays within a third of its height. It is the same in dark mode. It is the screen's one coloured action (a brand-mandated exception to S2). Pressed dims .85; there is no settle, because Kakao's proportions are fixed. Source: developers.kakao.com Kakao Login design guide.
CTA band, entrythe sign-in foot: the ProviderButton, 12, a ghost md Button (이메일로 로그인), 12, then a 13/18 secondary links row (이용약관 · 개인정보 처리방침 as ghost links that open the document Sheet), plus the home inset. No hairline. Used only on A1 and the web /login.
CTA band helper, dangerthe §14 helper line in danger, for a failure that belongs to the action (provider failed, cancelled). One line, never a second action.
Brand lockupthe MARK slot from the brand touchpoints doc, stacked (mark over the wordmark, 180 wide) for A1 and the version gate, and horizontal (28 high) for the web pages. No mark is drawn until the owner's new mark exists.
Cell, consenta leading 24 checkbox (the Select-mode box), the label with its [필수]/[선택] prefix 15/22, and a trailing sm ghost 보기 (its own focus stop) that opens the document Sheet. Master row 전체 동의: 600 weight, ticks or clears every item, and shows the mixed state when partly ticked. A 16 gap and a cell hairline separate it from the items. Marketing sits last as its own group, with the (광고) wording. Nothing is pre-ticked.
Cell, busya trailing 20 spinner in place of the chevron while the Cell's action runs; sibling Cells are inert (.5) until it settles; spoken "처리 중"
Sheet, documenttitle = the document name + a 13/18 secondary version line ("시행일 2026. 9. 1."). Body: 17/24·600 article headings, 15/22 paragraphs, tables as label/value Cell pairs. It scrolls within the 88% cap, the close is sticky, and there is no CTA.
EmptyState, full screenthe §9 EmptyState centred in the whole screen with no header, for a blocking gate (the version gate): mark slot, title, body, one primary Button when an action exists (업데이트)
Offline strip, no headeron a screen with no header, the strip (350 × 48, §9) pins directly under the status bar, solid, and pushes the content down by its own height
Web install rowa 13/18 caption plus a tonal 앱 설치하기 button. Blocked on EXT-8 (store listings); drawn as a marked gap until the apps are listed.

23. Rulings from the additions board (2026-10-10) ​

/wireframes/flat-v2-additions draws §18.2–§22.2 (55 specimens, 566 frames, 0 failures) and lists 31 spec conflicts, each with a fix. These three needed a ruling:

#ConflictRuling
1ScoreEntry: with the keypad up, the quick row plus tiebreak plus validation reaches 785, over the 88% cap (742)The sheet opens without the keyboard, showing the quick picks; tapping a field raises the keypad and folds the quick row (§19.3 amended). All seven quick results stay one tap, and the typing worst case is 372 + keypad = 663. The board's alternative (five Choices, no 7-5 / 7-6) would push two common results to typing.
2Dark mode has no tertiary text tokenAdd one at the light theme's tertiary contrast ratio against the dark card; it lands in the colour gate with the other dark values at D/S1
3Search field: 40 r12 in the spec, a 44 pill shippedThe 40 r12 field is drawn inside a 44 target (the hit area stays 44, the visual follows the grammar's radii)

The board's other 28 conflict rows are recorded fixes that follow the existing rules (text-driven heights exempt, hit slop for sub-44 controls, and so on). They need no ruling.

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