Skip to content

Owner review 3, lane A: inline adornments, the tab bar, the wizard shell, the sign-up route, and the foundations, behaviour, chrome and overlays audit (2026-10-11) ​

Status: Draft · Date: 2026-10-11 · Lane A of owner review 3. Inputs: the library v2.1 (§1, §2, §3, §6 count Tag, §8), flat-v2-kit.jsx, flat-v2-kit-chrome.jsx and the overlay and label parts of flat-v2-kit-controls.jsx and flat-v2-kit-content.jsx as built at 18:51 (so with the main thread's one-chevron Block change), the boards flat-v2, flat-v2-lib-chrome, flat-v2-lib-behaviour and flat-v2-lib-overlays, the review-1 and review-2 rulings and lanes, RN 0.86.3 in node_modules, and sources read on 2026-10-11 (§10). Audience: Korean club members aged 40 to 60, on phones, low digital literacy. Nothing outside this file was changed; every prototype below lives in the scratchpad (a copy of the kit with a patch applied before it is evaluated), never in the repo.

Owner, review 3 (points for this lane). "1. On chrome, in the variants where we have count tags after the label, the count tags should be properly aligned centrally to the texts. In fact, we should make sure all of these tags that render beside another component are centrally aligned. This seems to be a recurring issue and needs a foundational fix; it usually happens because the tags and the components they attach to have different sizes. What's the best practice solution? 2. In the bottom navigation, when we have dots and or counts, the space above gets too tight. What's the best practice solution here? 3. In the wizard shell, there seem to be more padding below the controls than above? (controls meaning the navigation buttons). Can you take care of spacing in the shell properly? ... And can you make sure all of the components in the library and foundations are in line with industry best practices? What are the sign-up route questions? We are definitely doing kakao, naver, google and or e-mail, I think? What do you suggest?"

Coordinator addition. "Let's minimize the use of custom implementations across all building blocks": §7 lists every place in my families that redoes layout, positioning, typography or spacing that a shared primitive already owns.

0. Evidence tags, method and limits ​

TagMeaning
CCode in this repo or in node_modules (path:line), including RN 0.86.3 sources.
DA documentation page, token file or package source read in full or by section (URL in §10).
FAn App Store listing frame (iTunes lookup, country kr). Marketing frames, so pt values are ratios to the screen width, assuming a 390 pt screen.
WA public web page rendered logged out in headless Chrome at 390 × 844, @2x, mobile user agent.
PA DOM or pixel measurement: headless Chrome over CDP on the built boards (docs/public/wireframes/*.html, 18:51 build), or on scratch pages built from the same kit with the repo's Pretendard OTFs (flat-v2-kit*.jsx loaded the way build-wireframes.mjs loads them). Text offsets are box centres, or ink centres where stated.
SA search snippet only. Flagged; no recommendation rests on one.

Limits. The boards render in Chrome, which floors half-leading and rounds ascent and descent to whole pixels (§2.4), so ink centres on a board differ from a device by up to 1 px; box centres, which the kit controls, do not. App Store frames rarely show a sign-in screen or a bottom CTA, so §5 measures the peers' mobile web sign-in pages (W) and says so. A claim I could not verify is in §9.

1. At a glance ​

#PointRecommendation (exact values, type roles, tokens)EvidenceKit functions and library entries to change
1Count tags and every adornment beside textOne primitive, FvkLineSlot: a box exactly as tall as the label's first line (fvkType(role, scale, cls).lineHeight: 22 for 15/22, 29 at 1.3×, 44 at 2×) that flex-centres the adornment and anchors to the label's top (alignSelf: flex-start). Never align-items: center on the row, never a per-site margin. The count Tag grows with its numeral (box = ceil(20 × min(scale, 1.3) / 4) × 4). Text beside text of another size aligns on the baseline. Corner badges keep their anchor. New verifier check adorn: |Δ| ≤ 0.5 px at every scale, with a canary. Today all 52 tab counts on the boards are off (51 by +2.0 px low, one by −3.5); the hand-tuned offsets elsewhere are right at 100% and break from 130% (up to −11 px).P (84 failures in 2,136 pairs on 16 boards), C, D (M3, HIG, RN 0.86.3, MDN)Core: fvkLineH (promote fvkCLineH), FvkLineSlot. Chrome: FvkTabs, FvkCtaBand, FvkLiveRow. Content: FvkCountTag, FvkCRow (marker), FvkCell, FvkNotice, FvkHero, FvkCaptionRow. Controls: FvkField, FvkPickerCell, FvkSheetFooter, FvkCountdownNotice. Library: §1.13 (LM6), new IA rules, §3 Tabs, §6 Count tag. Verifier: adorn.
2Tab-bar dots and countsThe badge geometry is right (it is Material 3's plus our 1.5 ring); the bar has no headroom for it. Bar 64 + inset (was 56 + inset); icon top 12 (was 6); icon 12–36, label 38–54, 10 below. The badge ring's top clears the bar's top edge by 8.5 (was 2.5): dot 6 and count 16 unchanged, ring 1.5, ring corner at the icon's top-right +3.5 / −3.5. FAB and Toast stay 16 above the bar, so they rise 8. If the owner wants Material's airiness: 72 + inset, icon top 16 (badge 12.5 clear). New check: ring top ≥ 8 below the bar's top.P (before and after), D (M3: icon 16 and badge 14 or 16 dp below a 80 dp bar), F (KakaoTalk 15 pt, 당근 8.4 pt)Core: FVK_TABBAR = 64. Chrome: FvkTabBar (height, item height, paddingTop), FvkFab (placed offset). Boards: flat-v2-lib-overlays (two literals 56 + FVK_HOME_INSET), flat-v2.jsx (frame ruler). Library: §1.1 chrome stack, §3 Tab bar, §3 Header icon badge, §1.1 FAB row.
3Wizard shell spacingBoth ends are unbalanced. Header: buttons sit 8 below the status area and 16 above the bar; make it 12 and 12 by adding 8 of top padding to the 56 row (square 12 to 52 from the row's top; the bar stays 8 below the row and the title at frame y 136, so chrome above the title is still 92). Footer: the button sits 16 below its top edge and 50 above the screen edge (37 above the home indicator); make the band's bottom padding max(inset, 16): 34 on an iPhone (21 above the indicator), about 24 on gesture-navigation Android, about 48 on 3-button, 16 with the keyboard up or no inset. Top stays 16. This is Toss's rule (padding-bottom: max(safe-area-inset-bottom, 20px)) and halves the lean.P (before and after, three footer options side by side), D (TDS BottomCTA source, Seed safe-area pattern)Chrome: FvkWizardHeader (header row padding), FvkCtaBand (bottom padding; helper line), FvkSheetFooter (same geometry). Library: §3 CTA band (118 → 102), §3 WizardShell (square 12–52, "12 clear"), §1.1 chrome stack, §8 Sheet footer. Disputes review-1 ruling 10 ("16 clear"), see §4.5.
4Sign-up routeKakao, Naver, and Sign in with Apple on iOS (App Review 4.8 makes Apple mandatory the moment any social login ships); Google only if you want a fourth button. Same size, shape and prominence (52, r12). Email is sign-in only (accounts you provision, the App Review demo account), not a self-serve sign-up. 본인인증 happens just-in-time, once, before the first action that depends on trust (join a club, host, pay, enter a result); Naver's own guide warns against phone verification during sign-up. Link accounts by the verified identity, with Supabase linkIdentity for the same person. Eight decisions for you are in §5.1.D (Apple 4.8 and 5.1.1, Google Play, Kakao, Naver, Google, Apple HIG, Supabase), W (five Korean services, measured), C (repo)Library §7 ProviderButton (one entry becomes a provider set), §3 CTA band Entry variant (N providers), flat-v2-auth.jsx sign-in frames, flat-v2-kit-controls.jsx FvkProviderButton (Naver, Apple, Google variants). Owner/external: EXT-11 (Kakao), EXT-4 (Apple), new: Naver 검수, Google OAuth client.
5Best-practice audit of 47 entries4 major (the owner's three points, which land on Tabs, Tab bar, CTA band and the WizardShell header), 12 minor, 31 none. The minors: the library's own 6.5:1 floor is missed by five components (success solid Tag 5.33; the red fills 5.91); the dark inverse surface is #2A302B in code, #232824 in the library, and 1.1 to 1.4:1 from its ground, where Material flips it; the frame is labelled iPhone 15 but is 390 × 844 with a 44 status area (the iPhone 15 is 393 × 852, 59 / 34); aria-pressed does not exist in RN 0.86.3 (the app already folds it into aria-selected); Dialog and Sheet widths are unspecified from 600 pt (Material 560 and 640; the app 640); the loading start is 300 ms where Seed says 1 s; the predictive-back opt-out is Android's "temporary" one; navSlide is 311 ms against a 300 ms law; the unread dot is 1 off at 15/22; the Live row. Full table in §6.P, C, D§6 (47 rows), §8 fix list.
6Custom implementations20 findings (§7): the press table three times with three appliers, the focus ring eight times, the inverse surface twice (both with the wrong dark value), five skeleton bars, six copies of the error line, two band helpers, eight text actions, ten rail-alignment margins, ten hand-set first-line offsets; a Notice re-implemented as FvkCountdownNotice (88 lines) and a Cell as the MenuSheet row; the chrome stack typed as literals in 17 places; eight board stage and phone wrappers (16 repo-wide). Consolidate onto FvkLineSlot, FvkHelperLine, FvkTextAction, FvkBadged, fvkFx / fvkRing / fvkInverse, FvkSkBar, fvkChrome and FvkFrame props.C (greps), P§7 table, §8 fix list.

2. Point 1: tags and glyphs beside text ​

2.1 The question ​

Count tags after a tab label sit visibly low. The owner asks for a foundational fix for every adornment that renders beside another component, and for the best-practice solution.

2.2 What I measured ​

Method. For every Tag, count Tag, Level tag, dot, badge and inline glyph on the 16 kit boards (flat-v2, the seven flat-v2-lib-* boards, brand, auth, hub, list A, list B, detail, forms, flows), find the text it sits beside (the nearest [data-t] whose first line is within 24 px horizontally and overlaps it vertically by a third, in the same overlay layer), and compute the centre of the adornment's box minus the centre of the label's first line box (line box = the role's line height, e.g. 22 for 15/22). P.

The defect the owner saw. The tab count pill is 2.0 px below the label's line-box centre (3 px below its top, where 1 was intended), and 2.1 px below the label's ink centre (pixel measurement at 4×; the "12" is 1.0 px above the pill's centre inside, a Chrome rounding bias, §2.4). It happens on all 52 tab counts across the boards. The existing verifier passes all four of my boards because no check measures vertical alignment (it passes flat-v2, 21 frames; lib-chrome, 111; lib-behaviour, 132; lib-overlays, 96). P.

The inventory, per adornment, at three text sizes. Δ is the box centre of the adornment minus the first-line centre of its label, in px; 0.0 is centred. One controlled page per specimen, built from the kit at scale 1, 1.3 and 2. P.

#Adornment beside textKit function and how it is aligned today (C)100%130%200%Verdict
1Count Tag after a tab labelFvkTabs (kit-chrome:401): a block <span style={{marginTop: 1}}> around an inline-flex pill+2.0−3.5−3.5Wrong at every size; the pill also stays 20 tall while its digits grow to 14.3/21
2Label Tag in a CellFvkCell label cluster: inline-flex, alignItems: center0.00.00.0Right (single-line labels)
3Level tag next to the figure of a person rowfvkCPersonTrailing: flex, centre; the cluster sits in FvkCRow's trailing slot (height: l1H)0.00.00.0Right
4Level tag after the identity nameFvkCBlockIdentity: flex, centre, wrap0.00.00.0Right
5Glyph and Tag in a hero factFvkHero: a wrapper height: fvkCLineH('headline'), flex centre0.00.00.0Right (this is the slot rule, hand-made)
6Live tag before the date and venueFvkLiveRow: paddingTop: 2 on the meta0.0−0.5wrapsRight at 100% only
7Alert glyph in a NoticeFvkNotice: wrapper height: titleLine, flex centre0.00.00.0Right when the title is the first line (slot rule, hand-made); 1 off when the first line is the 16/24 body (row 14)
8Alert glyph in the CTA danger helperFvkCtaBand: paddingTop: 3 around a 16 glyph0.0−3.5−11.0Right at 100% only
9Alert glyph in a field errorFvkField: marginTop: 1 (and five more copies of the error line, §7 row 5)0.0−2.5−9.0Right at 100% only
10Unread dot on a rowFvkCRow: absolute top: 8 (library LM6: "its top sits 8 below the text-box top")+1.0−2.5−10.0Off at 15/22 even at 100%; violates LM8 ("follow line 1's centre")
11Level tag and Tag in a roster cellFvkRoster: flex, centre, minHeight: 240.00.00.0Right
12Clock glyph in the CountdownNoticeFvkCountdownNotice: marginTop: 1, alignItems: flex-start0.0n/a−6.0Right at 100% only
13Message time glyph in a bubble rowsocial board+3.0n/an/aOff at 100% (lane C's board; same cure)
14Lock glyph in a Notice with a body and no titleFvkNotice through FvkFieldNote box: a 20 glyph in a 22 title-line slot, but the first line is the 16/24 body−1.0n/an/aOff by 1 (the slot takes the title role, not the first line's role); a one-line Notice alert is +1.0

Totals on the boards. 2,136 adornment and label pairs, 84 failures at the 0.5 px tolerance: 52 tab count Tags (51 at +2.0, 1 at −3.5), 24 trailing chevrons (23 pointing right, centred on a Block or row by design, which must carry data-adorn="box"; one expander chevron at 200% text, +22, centred on a three-line label where Material top-anchors, §2.6), 2 unread dots (+1.0), 2 clock glyphs (−6.0 and +3.0), 2 pencil glyphs on wrapped Cells (+11.0, centred on the row: a Cell rule, §2.6), 1 lock (−1.0), 1 alert (+1.0). A trailing Tag in a row is centred on line 1 by FvkCRow and passes. P.

What the experiment shows about the cure. Five recipes for the tab pill, measured on a page with the same fonts (Pretendard Medium 15/22 label, 20-tall pill), by box and by ink (pixel measurement at 16×). P.

RecipeBox Δ vs first lineInk Δ (label bbox)Two-line label
A. today: block span with marginTop: 1, inline-flex child+2.00+2.31not tested
B. slot: a flex box as tall as the line, align-items: center, top-anchored0.00+0.310.00 (first line)
C. align-items: center on the row+1.00+1.31+12.00 (centred on block)
D. CSS text-box: trim-both cap alphabetic on the label, row centredn/a (box moved)+0.50n/a
B at 1.3× (29 line, 24.8 pill that grows with its numeral)0.00+0.44n/a

Recipe B is exact at 100%, 130% and 200% (every specimen above, rebuilt with the slot, reads 0.00; §2.5). Recipe C fails twice: it inherits the phantom strut that makes A wrong, and it centres on the whole block, so a label that wraps drags its tag to the middle. Recipe D is the closest to ink but exists only on the web.

2.3 Root causes ​

  1. A phantom strut. In FvkTabs the pill is an inline-flex inside a block <span> whose own line box (the page's 16px, line-height: normal) is taller than the pill's; the pill sits on the strut's baseline, 2 px lower than the marginTop: 1 that was meant to centre it. Reproduced in isolation: recipe A +2.00. P.
  2. Magic numbers that are right at one size. marginTop: 1, paddingTop: 3, top: 8, paddingTop: 2: each is (line − adornment) / 2 computed by hand for one text size (22 − 20 = 2, so 1; 22 − 16 = 6, so 3). From 130% the line grows and the number does not (rows 6, 8, 9, 10, 12 above). The library states the right rule (LM8: "under text scaling the dot and gap stay 8 and follow line 1's centre") and the kit breaks it. C, P.
  3. Row centring where first-line centring is meant. align-items: center on a row centres on the whole block. For one line the two agree; for a label that wraps (names wrap to two lines, three from 1.5×) the tag lands between the lines (recipe C, +12). C, P.
  4. The tag does not grow with its numeral. The count Tag is a fixed 16 + 2 × 2 box (flat-v2-kit-content.jsx:548) while its digits scale to the chrome cap 1.3: at 130% the 14.3/21 digits overflow a 20 pill and the pill is 3.5 px above the label. The Level tag already grows (ceil(24 × min(scale, 1.5) / 4) × 4). C, P.
  5. Sizes differ by design. A 24 Tag beside a 22 line, a 20 pill beside a 22 line, a 16 glyph beside 15/22 and a 6 dot all need a different offset; with a hand-written offset each site re-derives it, with a slot none does.
  6. Font metrics are not the cause, and that is useful. Pretendard's hhea metrics are ascent 1950, descent 494 per 2048 em (identical in all four weights; fontTools on the bundled OTFs), so the line box's centre sits (1950 − 494) / 2 / 2048 = 0.3555 em above the baseline. Cap height 1448 gives a cap-centre of 0.3535 em and Hangul ink (−162 to 1616 on 가) a centre of 0.355 em: the centre of the line box is the optical centre of Hangul and digits to within 0.003 em (0.04 px at 15 px). Lowercase Latin is the exception: its x-height centre is 0.265 em, 0.09 em (1.4 px at 15 px) below the line centre. So centring on the line box is right for this font, and text-box-trim is not needed (§2.4). C, P.
  7. The platforms agree with the web model. RN 0.86.3 centres glyphs in the line box on both platforms: iOS sets NSBaselineOffsetAttributeName = (lineHeight − font.lineHeight) / 2 whenever a lineHeight is set and at least the font's own (RCTAttributedTextUtils.mm:337,339); Android's CustomLineHeightSpan "follows web-like behavior for line height" and splits the leading ceil above and floor below (CustomLineHeightSpan.kt:16,42-43). Android's includeFontPadding defaults to true (TextAttributeProps.kt:95), which a line height neutralises for the first and last line, so every Text role must carry an explicit lineHeight (the library's roles do). C.
  8. The app already hit this and patched it per site. GlyphSlot ("THE canonical centered slot for any small graphic that pairs with text", packages/ui/src/glyph-fact-row.tsx:19) is a fixed 16 or 20 box centred by alignItems="center" on the row, Badge defaults to alignSelf: 'flex-start' (badge.tsx), and three call sites override it with alignSelf="center" (status-badges.tsx:179, match-scoreboard.tsx:433, club-session-card.tsx:142). The emoji slot applies includeFontPadding: false, textAlignVertical: 'center' for the same reason. C.

2.4 Best practice ​

  • Material 3 treats the two cases differently. A notification badge is anchored to its host's corner by fixed offsets (small 6 dp, large 16 dp with a 12 / 14 dp offset: §3.3), never inline. An item's trailing and leading content is centred vertically for one- and two-line rows and top-anchored for three-line rows (ListItem.kt:1193: y = if (isThreeLine) topPadding else CenterVertically.align(...)). So first-line anchoring is Material's rule for rows that wrap. D (androidx-main, read 2026-10-11).
  • Apple solves it in the glyph. SF Symbols scales are "defined relative to the cap height of the San Francisco system font", and a custom symbol follows the template's alignment guides, so a symbol beside text shares the text's cap band; a tab-bar badge is "a red oval containing white text and either a number or an exclamation point". D (HIG sf-symbols, tab-bars).
  • Toss and 당근 anchor notification marks to the host. TDS Tab draws its dot at position: absolute; right: 0; top: 4px; 6 × 6; #f44336 inside the label's box; Seed's Notification Badge "is generally at the top right, included in another component, and its position and size are fixed, never changed arbitrarily". A count that is a quantity (12 sessions, 3 saved), as in our tabs, is not a notification; it is an inline attached label, which is the case for the slot. D (TDS package source @toss/tds-mobile 2.5.1; seed-design.io notification-badge, tabs).
  • CSS has a feature for this, with a narrow reach. text-box-trim and text-box-edge (cap alphabetic trims the line box to cap height and baseline) are standard: Chrome and Edge 133, Safari 18.2, Firefox 154, Baseline "newly available" in 2026. They remove the leading, so centring on the trimmed box centres on cap height, which for Pretendard is the line centre anyway (recipe D above: ink Δ +0.50 against +0.31). React Native has no equivalent, and the wireframe is the spec for RN, so the rule has to be expressible in flex terms. align-items: baseline is right for text beside text of another size (the Section head's title and count) and wrong for a pill or a glyph, whose "baseline" is its own text or its bottom edge. D (MDN BCD text-box-trim).
  • What the rule must therefore say. Text beside text: share the baseline. A non-text adornment (Tag, count, dot, glyph, spinner) beside text: centre it on the first line's box. A notification mark on an icon or a tab: anchor to the host's corner. Everything else is one of these three.

2.5 Recommendation: one primitive and one rule ​

The rule (library §1.13 gains "Inline adornments", IA1 to IA6).

  • IA1. An adornment beside a label is attached through one primitive, the line slot: a box whose height is the label's first-line height, with the adornment flex-centred in it, top-anchored to the label's box. The row aligns its children to the start (flex-start), never to the centre.
  • IA2. The formula. slotHeight = fvkType(role, scale, cls).lineHeight, with the label's own role and scale class (15/22 reading class: 22 at 100%, 29 at 130%, 44 at 200%; 13/18 label class: 18, 24, 36). The adornment's top is (slotHeight − adornmentHeight) / 2 from the label's top, which is negative for a taller adornment: overflow is allowed and never grows the row (a 24 Tag beside a 22 line pokes out 1 above and below). A row that must hold a taller adornment sets its own minHeight.
  • IA3. First line. For a label that wraps, the slot anchors at the top: the adornment stays on line 1 (a tag after a name that wraps to two lines sits beside the first line).
  • IA4. Sizes by scale. A glyph beside text grows by min(scale, 1.5) (existing rule); the count Tag and the Level tag grow with their numeral (ceil(20 × min(scale, 1.3) / 4) × 4 and ceil(24 × min(scale, 1.5) / 4) × 4); a dot never scales; the slot height follows the label, so none of them needs an offset.
  • IA5. Corner adornments (a badge on an icon or a tab) are not inline: they anchor to the host's corner with fixed offsets (§3.4) and carry data-adorn="corner".
  • IA6. Exemptions assert the alternative. An adornment that centres on its container by design (the Block chevron, a trailing glyph on a row that centres its trailing control) carries data-adorn="box", and the check then asserts that it is centred on its parent's box. A NAME is never a pass (the god-file lesson in the project memory).

Applied to the unread dot: top = (line − 8) / 2, which is 7 for a 22 line and 8 for a 24 line; LM6 should say that, not "8 for 17/24 and 15/22" (+1.0 at 15/22).

Prototype, all rebuilt with the slot and measured at 100%, 130% and 200%. Tabs with neutral and danger counts (a 120 renders "99+"), a Tag beside a label that wraps to two lines, the CTA danger helper, the field error, the unread dot, and the CountdownNotice clock all read 0.00 at every size. P. The count Tag at 130% grows to 28 (26 snapped to the grid) with its 14.3 digits inside.

2.6 What changes ​

Kit (names).

  • flat-v2-kit.jsx: add fvkLineH(role, scale, cls) (promote fvkCLineH, kit-content:273, which seven call sites already use privately) and FvkLineSlot({ role, cls, h, children }) (display: flex; alignItems: center; alignSelf: flex-start; flexShrink: 0; height: fvkLineH(...), data-g="line-slot", data-line). Document data-adorn="box" | "corner" in the header comment.
  • kit-chrome: FvkTabs (the count goes in <FvkLineSlot role="body" cls="chrome">; delete marginTop: 1), FvkCtaBand (the danger helper's glyph; delete paddingTop: 3), FvkLiveRow (the Live tag; delete paddingTop: 2).
  • kit-content: FvkCountTag (grow with the numeral; keep the 16 form), FvkCRow (marker through the slot; the absolute top: 8 and the markerLeft arithmetic go), FvkCell (the consent box's computed marginTop, the error line; mark the trailing chevron or pencil of a two-line Cell data-adorn="box"), FvkNotice, FvkHero and FvkCaptionRow (their private height: lineH wrappers become the slot), FvkCBlockFrame (the new chevron gets data-adorn="box").
  • kit-controls: the six error-line copies (§7 row 5) become one FvkHelperLine, which uses the slot; FvkCountdownNotice becomes a FvkNotice (§7 row 11).

Library. §1.13 gets IA1 to IA6 and LM6 is rewritten (top = (line − 8) / 2); §3 Tabs ("a count Tag 4 after the label, in a line slot"); §6 Count tag ("grows with its numeral, chrome cap 1.3: 20, 28 at 130% and above"); §2.4 table row "Tag, count Tag, level numeral" keeps one line and adds "attached through the line slot".

Verifier (scripts/verify-flat-v2-boards.mjs, a new check adorn, counted with the others).

  1. Pair. For each Tag, count Tag, Level tag, dot, badge and svg[data-icon] not part of another control (button, iconbutton, fab, tabbar-item, avatar, input, choice, checkbox, switch, thumb, a tag, a count) and not marked data-adorn="box" or "corner", find the nearest [data-t] in the same layer (the page, or one sheet, dialog, toast or coachmark) whose first line is within 24 px horizontally and overlaps the adornment vertically by at least a third; skip transformed frames (compare getBoundingClientRect().width to offsetWidth).
  2. Measure. Δ = centre of the adornment's box − centre of that first line box (top + line height / 2, from the computed line-height).
  3. Tolerance. 0.5 px, at every text scale (the kit's boxes are integers or halves; the board's Chrome rounds ink, not boxes). Ink is reported, never gated: Hangul shapes move an ink centre by up to ±1 px.
  4. Marked exemptions. data-adorn="box": |parent centre − adornment centre| ≤ 0.5. data-adorn="corner": the ring's outer corner is within 0.5 of the host's top-right +3.5 / −3.5 (§3.4).
  5. Canary. The check injects, off screen in each page, two rows built from plain markup: a label and a count pill in a margin-top: 3px wrapper (the tab bug, +2) and the same pill in a data-g="line-slot" of the line height (0). It must flag exactly the first, and the page fails with "canary adorn lost" if it does not, as the existing text rules do (TEXT_RULES, lostRules). I ran this: 16 boards, the canary flagged one row in each page. P.

On today's boards the check reports the 84 failures of §2.2. After the kit changes above, the 52 tab counts pass, and the 24 chevrons pass once the Block and the Cell mark them data-adorn="box" (the expander chevron at 200% moves to the first line). The other 8 (two dots, two clocks, one lock, one alert, two pencils) are fixes in the list, except the pencils, which are a ruling: a two-line Cell centres its trailing glyph on the row, as Material's two-line ListItem does, so it is marked data-adorn="box".

In Tamagui (packages/ui). Add LineSlot beside GlyphSlot (glyph-fact-row.tsx) and make GlyphSlot a thin use of it: <XStack height={lineHeight(role) * min(fontScale, cap)} alignItems="center" alignSelf="flex-start" flexShrink={0}>, with the line height from the planned text-scale.ts role table. The slot, not the child, owns the alignment, so Badge's alignSelf: 'flex-start' default and the three per-site overrides stop mattering. Keep $platform-native={{ includeFontPadding: false, textAlignVertical: 'center' }} on emoji and on numerals inside a fixed pill; with an explicit lineHeight RN already centres the first and last line. Move CornerCountBadge (corner-count-badge.tsx, an 18 circle at top: -2, right: -2) onto a Badged host with the §3.4 offsets, and let UnderlineNav (pill-nav-underline.tsx, which renders only a 6 BadgeDot beside the label today) take a count. The device check is an on-device probe: onLayout of the label and the slot in the dev panel, Δ within 1 px (Android adds up to 0.7 px of ceil leading).

3. Point 2: dots and counts in the tab bar ​

3.1 The question ​

With a dot or a count on a tab icon, the space above the badge gets too tight. What is the best practice?

3.2 What I measured ​

The library's tab bar on the chrome board, with badges { 0: dot, 1: 2 } (and 99+ on tab 3 in my scratch page). P.

PartBefore (56 + 34 = 90)After (64 + 34 = 98)
Icon (24)y 6–30, x 27–51 in its 78 wide itemy 12–36
Label (12/16)y 32–48, 8 below it in the bary 38–54, 10 below it
Dot (6, ring 1.5, outer 9)y 2.5–11.5, x 45.5–54.5: 3.5 above and 3.5 right of the icon boxy 8.5–17.5
Count (16, ring 1.5, outer 18 to 19)y 2.5–20.5, right edge 3.5 beyond the icon; overlaps the icon's upper right and 11.5 above the labely 8.5–26.5
Clearance, bar top edge to the badge2.58.5

The bar's top edge is where the sticky hairline appears while content scrolls under it, so the 2.5 is the badge's gap to a drawn line. The "tight" the owner sees is exactly that: the icon has 6 above it, the ring hangs 3.5 above the icon, and 6 − 3.5 = 2.5. P.

3.3 What the references do ​

ReferenceBarIcon top below the bar's topBadgeClearance above the badge
Material 3, NavigationBar (D, androidx-main)80 dp (TallContainerHeight; 64 dp older)16 dp (indicator 56 × 32 at 12, 4 dp padding)Small 6 dp, large 16 dp (label small 11/16, 4 dp side padding past one digit). Small: x [W − 6, W], y [0, 6], inside the icon box's corner (BadgeOffset = 6). Large: x [W − 12, W + 4], y [−2, 14] (BadgeWithContentHorizontalOffset = 12, …VerticalOffset = 14; Badge.kt:107-110,208-218). NavigationBarItemLayout applies Modifier.badgeBounds() to the whole item, which clamps the badge's top to the item's top and its end to the item's end (NavigationBar.kt:536, Badge.kt:112-123,223-238): in Material the headroom is structural14 dp large, 16 dp small
KakaoTalk, 4th tab, "194" (F, kakaotalk:3)at least 64.5 pt visible20.7 pt27 × 17 pt pill; top 5.4 pt above the icon's top and 7 pt beyond its right edge15 pt
당근 알바 bar, floating capsule (F, daangn:4)capsule 63 pt12.9 pt17 pt circle, centre about 5 pt right of the icon and 4 pt below its top8.4 pt
Toss TDS Tab (D, package source)top tabsn/adot 6, right: 0; top: 4px, #f44336; a dot only, no countsn/a
Seed Bottom Navigation (D)not statednot statedSmall (dot) for "something changed", Large for a count (1 to 99, then 99+, at most 4 characters); position and size fixed; "never on three or more tabs"; fill glyphs, no font scalingn/a
Apple HIG, Tab bars (D)systemsystem"a red oval containing white text and either a number or an exclamation point"; "reserve badges for critical information"system
Ours566dot 6 and count 16, both with a 1.5 canvas ring, ring corner at the icon's top-right +3.5 / −3.52.5

Two readings. Our badge is Material's: the large badge's fill top is 2 above the icon (ours: ring 3.5, fill 2) and its right edge 4 beyond it (ours: fill 2, ring 3.5). Every reference leaves 8 to 16 above the badge, and ours leaves 2.5, because Material puts the icon 16 into an 80 dp bar and we put it 6 into a 56. KakaoTalk's frame suggests a bar of about 64 pt or more. F is a marketing frame, so these are ratios (assumed 390 pt).

3.4 Recommendation ​

Keep the badge geometry and give the badge its headroom.

  • Bar FVK_TABBAR = 64 + inset (was 56). Item: paddingTop: 12, icon 24 (y 12–36), 2, label 12/16 (y 38–54), 10 below. The content area above the bar shrinks by 8.
  • Badge anchors (relative to the 24 icon box; right and down positive): ring outer corner at +3.5 right, −3.5 above. Dot: fill 6 spans x [W − 4, W + 2], y [−2, 4], ring 9. Count: fill 16 spans y [−2, 14], right edge at W + 2, ring 18 to 19; it grows to the left for 99+. Both are unchanged from the board; the numbers are Material's plus our ring.
  • Clearance rule: the badge ring's top is at least 8 below the bar's top edge (8.5 with the new bar), the spacing ladder's base unit and the 당근 measurement; Material and KakaoTalk give 14 to 15.
  • Option for a Material-like bar: 72 + inset, icon top 16, label 42–58, 14 below; ring clearance 12.5, fill clearance 14, the same as Material's large badge. I recommend 64 because it costs 8 of content height, not 16, and clears the 당근 figure.
  • Why not the alternatives. Moving the badge inside the corner like Material's dot lifts the clearance only to 4.5 (6 − 1.5 ring). Dots only (no counts) still leave 2.5. Shrinking the icon changes the ratio of the bar's items for every tab.
  • FAB and Toast stay 16 above the bar, so both rise 8 (FAB bottom = 64 + inset + 16; Toast with a FAB 64 + inset + 16 + 56 + 16).
  • Seed's rule joins the library: a badge on at most two tabs at once; a count only where the number is actionable (the 클럽 tab's "읽지 않은 게시글 2개" is; 홈's "진행 중" is a dot).
  • Icon style stays. HIG ("prefer filled symbols") and Seed (fill only, state by colour tone) draw all tab icons filled; the library draws the active tab filled and the others outlined, Material's pairing, because it requires a second cue that is not colour (WCAG 1.4.1). Not a drift.

3.5 What changes ​

  • Kit. flat-v2-kit.jsx: FVK_TABBAR = 64. kit-chrome: FvkTabBar (height: FVK_TABBAR + inset, the item's height: 64, paddingTop: 12), FvkFab (placed === 'tab' offset FVK_TABBAR + FVK_HOME_INSET + 16). Boards: flat-v2-lib-overlays.jsx lines 234 and 1173 (56 + FVK_HOME_INSET), flat-v2.jsx (the frame ruler's tab bar 56 and content 654 become 64 and 646), flat-v2-lib-chrome.jsx (add a 200% frame, §6 row C3).
  • Library. §1.1 chrome stack and Frame table ("Tab bar: 56 + inset" becomes 64), §3 Tab bar anatomy ("Item: 12 above a 24 icon"), §3 Header icon badge ("the tab bar uses the same marks": add the clearance rule), §1.1 FAB row, §2.4 ("the tab bar holds 64 + inset").
  • Verifier. A check tabbar in verify-flat-v2-boards.mjs: for each [data-g="tabbar"] with a [data-g^="badge"], badge.top − bar.top ≥ 8 (the ring's outer box is the badge element). Canary: a bar fixture whose icon sits 6 down, which must fail. On today's boards the check fails on all 3 badged bars (two on the chrome board, one on the hub board), each at 2.5; 3 of the 146 tab bars drawn on the flat v2 boards carry a badge.

4. Point 3: the wizard shell ​

4.1 The question ​

"More padding below the controls than above? (controls meaning the navigation buttons)". Take care of the spacing in the shell properly.

4.2 What I measured ​

The ideal frame wz-ideal on the chrome board (step 3 of 6, skippable, title and one field), 390 × 844. P.

Gapframe ypx
Status area0–4444
Status bottom to the back square's top44 → 528
Back square (40 in a 48 target, 48–96)52–9240
Skip button (sm, 36)54–9010 below the status area
Back square's bottom to the progress bar92 → 10816 (skip bottom to the bar 18)
Progress bar (4) and the count "3/6" (18, centred on the bar)108–1124
Bar's bottom to the title112 → 13624
Title (hero 24/32)136–16832
CTA band726–844118
Band top to the button's top726 → 74216
Button (lg)742–79452
Button's bottom to the screen's bottom794 → 84450 (16 + the 34 inset)
Button's bottom to the home indicator's top794 → 83137

4.3 Which controls look unbalanced ​

Both ends, by the same factor.

  • The header buttons: 8 above, 16 below (1 : 2). The square is tucked under the status area and floats clear of the bar.
  • The footer button: 16 above, 50 below (1 : 3.1), or 16 above and 37 above the indicator (1 : 2.3). The page's content ends 40 above the band, so the only visible "above" is the band's own top (the hairline, when content scrolls under).

I drew three footers side by side on the same frame (A the library's 16 + inset, B bottom = the inset, C bottom = inset + 8) and looked at them at 2×: in A the button floats high above the indicator; in B it rests on the safe-area boundary and the 16 above and 21 below read as one margin; C is in between. Measured gaps, button bottom to indicator: A 37, B 21, C 29. P.

4.4 What the references do ​

  • Toss TDS BottomCTA (D, @toss/tds-mobile 2.5.1 source). The container's padding is 0 20px max(env(safe-area-inset-bottom), 20px) 20px: the button's bottom sits at the safe-area boundary (34 above the screen's bottom on an iPhone, 20 where there is no inset), not 20 above it. Over the button, a 36 px gradient fades content into the band instead of a hairline. The button is CTAButton size="xlarge" (56). While the keyboard is up (the viewport shrinks by more than 30%) the band becomes isFull: side and bottom padding 0 and border-radius: 0, so the button attaches to the keys; fixedAboveKeyboard defaults to true. Accessories: a top line (15, grey, 26 above the button) and a bottom line (24 below).
  • 당근 Seed, Safe Area pattern (D). Spacing is measured from the safe-area boundary, not the screen edge. For the bottom: "if a fixed bottom bar holds a button, set the button away from the bottom safe-area boundary by the bar's inner padding" (translated from the Korean page), which is inset + padding, our rule today. Backgrounds fill to the edge; fixed bars stay inside the safe area.
  • Material 3 and Android (D). Edge-to-edge is enforced for apps targeting API 36 and cannot be opted out, so a bottom bar paints behind the navigation bar and adds the inset to its own content. Material's own NavigationBar is windowInsetsPadding(...) around a row of minimum 80 dp (NavigationBar.kt:124-129): inset outside, content inside. I found no numeric rule in Material 3 for a bottom action bar, so it neither supports nor contradicts a bottom padding of max(inset, 16).
  • Apple HIG (D). Safe areas must be respected; the system draws the bars; there is no numeric rule for a custom bottom button. iOS 26's scroll-edge effects replace a hairline with a soft or hard edge.

The references split: Seed and Material measure from the boundary (inset + padding); the project's benchmark, Toss, lets the inset be the padding. The project law is "Toss is the UX benchmark" and the owner reports the footer as bottom-heavy, so the evidence and the owner agree.

4.5 Recommendation ​

Header. The 40 square and the sm skip are centred in the header row; the row stays 56 (border-box) and gains padding-top: 8, so its content area is 48 and the controls move down 4. Measured from the row's top (the library's coordinates): the square runs 12 to 52 (was 8 to 48), the skip 14 to 50, the bar stays at y 64, and the title stays at frame y 136, so "chrome above the title is 92" is unchanged and the controls sit 12 below the status area and 12 above the bar. Prototype: gap above 12, below 12, title top 136. P. Library §3 WizardShell: "The bar is 12 clear of the header buttons (the 40 square ends at y 52; the bar starts at y 64)".

This disputes review-1 ruling 10 ("The progress bar moves to its own row, 16 clear of the buttons"). The 16 answered a crowded v1 where the segments sat between the buttons; the owner now reports 16 as too much against 8. If the owner keeps 16 below, the balanced alternative is 16 above: a 64 header row (square 16–56), the bar at y 72 and the title at y 144, chrome 100 (8 more).

Footer. Band padding: 16 rail max(inset, 16): top 16 (hairline to button), bottom = the inset when there is one, 16 when there is none or the keyboard is up. iPhone 34 (button 21 above the indicator), gesture-navigation Android about 24, 3-button Android about 48 (typical insets; the real value is read at runtime), desktop web 16. Heights: 118 becomes 102; helper line 148 becomes 132; two lines 170 becomes 154; sign-in entry 204 becomes 188; keyboard up 84 stays 84. A Sheet footer follows (library §8: "footer = the CTA band geometry").

Keyboard up. The band rides the keyboard (KeyboardStickyView), inset 0, so top 16 and bottom 16, a 1 : 1 margin. Toss goes full-bleed (padding 0, radius 0) there; I keep the floating button because a full-bleed square button breaks the one-button grammar of the other screens. Not a ruling for the owner; a note.

The helper line stays 8 above the button. Content keeps 40 clear above the band.

Why this and not Seed's rule. The home indicator is a 5 pt bar 8 pt above the screen's bottom, so the zone under the button already holds visible furniture; 16 of padding on top of a 34 zone puts 37 between the button and the only thing below it. Toss, the benchmark, draws it flush to the boundary.

4.6 What the owner can choose ​

OptionHeader above / below the buttonsFooter: top / button to the screen's bottom / to the indicatorChrome above the titleBand
Now8 / 1616 / 50 / 3792118
Recommended12 / 1216 / 34 / 21 (bottom = max(inset, 16))92102
Middle footer (inset + 8)12 / 1216 / 42 / 2992110
Header 16 / 1616 / 16 (header row 64)as recommended100102

4.7 What changes ​

  • Kit. kit-chrome: FvkWizardHeader (the header row's padding: '8px ' + rail + 'px 0'), FvkCtaBand (padding: '16px ' + rail + 'px ' + Math.max(16, inset) + 'px'). kit-controls: FvkSheetFooter and FvkSheetBox (the footer's 16 / 16 + inset becomes 16 / max(16, inset)). Boards: flat-v2-lib-chrome.jsx (the h of every CTA frame: 118, 148, 170, 204; LibChrAbove h values), flat-v2-lib-overlays.jsx (FvkToast pin={118 + 16}), flat-v2-auth.jsx (the 204 entry band, in its spec text), and any page board that hard-codes a band height (search for 118, 84 + , + 34).
  • Library. §1.1 chrome stack (CTA band: 16 + helper + 52 + max(16, inset)), §1.3 (the ladder line "CTA band top and bottom padding" stays 16), §3 CTA band anatomy and heights, §3 WizardShell (the numbers above), §8 Sheet (footer), the paint-once law (the base padding now overlaps the inset instead of adding to it).
  • Verifier. A check cta: for each [data-g="cta-band"], band bottom − button bottom = max(16, 34) on a phone frame with the home indicator and 16 in a keyboard frame; canary: a band built with 16 + inset must fail.
  • App. bottom-cta-band.tsx and bottom-cta-band-visual.ts (16 sides, permanent hairline today) take paddingBottom = Math.max(insets.bottom, 16), the safe-area hook the app already uses; WizardShell (wizard-shell.tsx) moves its buttons per the header numbers.

5. The sign-up route ​

5.0 Where it stands ​

Today the app has email and password sign-in only; the copy says "계정이 없다면 운영팀에 문의해 주세요" (ask the operations team if you have no account), use-auth.ts has no signUp, and Kakao OAuth waits on EXT-11 (usability-audit-2026-09-16.md U-01; external-blockers.md). The auth board draws Kakao as the target and an interim email sign-up beside it. The owner's standing directive (2026-06-02; the repo records it as the "verified-identity directive" in docs/design/open-issues-plan-2026-09-24.md, line 67, with EXT-11) is that real sign-up goes through officially recognised identity providers, with nationality derived, not typed; the repo already carries a precedent for friction before the first tier-gated RSVP "until 본인인증 ships" (docs/architecture/rating-hardening-audit.md:15). The architecture decided earlier is one signInWithOAuth(provider) over Supabase with PKCE and expo-web-browser, no native SDKs. C.

5.1 The questions, with my answer to each ​

#Question for youRecommendationWhy
1Which providers ship?Kakao and Naver on every platform; Sign in with Apple on iOS; Google on Android and web. Three provider buttons per platform, plus the email link. Add Google on iOS only if you want four.Kakao and Naver each appear on four of the five Korean services I rendered, and both together on three; Google appears once (BAND) and Apple once (무신사). Apple is not a taste question (next row). Google adds a credential, a button and a linking case for an audience that has Kakao and Naver; on Android it is the platform account and costs one button in the third slot.
2Is Sign in with Apple required?Yes on iOS, the moment Kakao, Naver or Google is how a person creates or signs in to the primary account. Your own email sign-in does not satisfy it. Apple's route: native sign-in with the system button.App Review 4.8: an app that uses a third-party or social login to set up or authenticate the primary account "must also offer as an equivalent option another login service" that limits data to name and email, lets the person keep the email private, and does not collect interactions for advertising without consent. Own email cannot offer a private relay. The exemptions (your own account system exclusively; a government or industry-backed citizen ID; an education or enterprise account; a client for one third-party service) do not describe this app.
3Order and prominenceKakao, Naver, then Apple (iOS) or Google (Android, web), all 52 tall, r12, full width, 12 apart; then the quiet email link, then the terms links. Same size and shape for all; only the brand fills differ.Google: "displayed at least as prominently as other third party sign-in options". Apple: "no smaller than other sign-in buttons", no scrolling to see it. Kakao: do not leave its colour unspecified so another provider's button stands out. Equal size is the compliant layout in all four guides.
4Where does email fit?Email is sign-in only, for accounts you provision, for the App Review demo account, and for a member who has neither Kakao nor Naver. No self-serve email sign-up in production unless it ends in the same 본인인증 gate. The drawn "interim email sign-up" stays a contingency if OAuth slips.Self-serve email creates unverified accounts that the directive forbids and that the abuse audit already worries about; Naver's guide forbids asking for a separate password right after a Naver sign-in; and App Review needs "an active demo account or fully-featured demo mode" for an app with account features.
5When does 본인인증 happen?Just-in-time, once, before the first action that depends on trust: 클럽 가입 신청, hosting or creating a club or session, a fee payment, a score entry. Sign-up itself is: provider tap, consents, 출생 연도. Alternatives and the trade in §5.5.Naver's guide (translated): asking for phone verification, or for too much extra information, during sign-up makes many people leave; collect extra information at the moment it is needed. Apple HIG: "Delay sign-in as long as possible"; "minimize your requests for data as people set up an account". The repo's own precedent is friction at the first tier-gated RSVP.
6One person, several providers: how are accounts linked?Link by the verified identity. After 본인인증 stores the person's CI hash, a second provider that lands on a new account and verifies to the same CI is offered "이미 가입한 계정이 있어요" and linked with Supabase linkIdentity. Same verified email also links automatically. The 계정 section of Settings lists the linked logins and offers 연결 and 해제 (one login must always remain). Never merge on name and birth date.Supabase links automatically only on the same, verified email; Kakao may return no email (Biz App needed); manual linking is "beta" and must be enabled. CI is the one key a Korean 본인인증 gives for the same person across services.
7What do we collect beyond the provider?Name and nickname from the provider, 출생 연도 (the board's existing step), the consents, nothing else at sign-up. Phone, real name and CI come at the 본인인증 step. Never a password after a Kakao or Naver sign-in.App Review 5.1.1(iii) (data minimisation) and the Naver rule above; Apple's name is provided only on the first sign-in, so save it then.
8What must you start now?Kakao: the Biz App conversion (needs a business registration number; EXT-11) if you want email or CI; Naver: register the app and submit the review request (검수; until approved only registered test IDs can log in); Apple: the developer account (EXT-4) and the Sign in with Apple key; Google: an OAuth client and consent screen. Counsel: CI storage, under-14 consent. Details in §5.2 and §5.4.The long poles are outside the repo: Naver's review and Kakao's Biz App have no date I could verify.

5.2 Store rules that bind the route ​

  • Apple 4.8 Login Services (quoted in row 2; read from developer.apple.com/app-store/review/guidelines, 2026-10-11). D.
  • Apple 5.1.1(v): an app with account creation "must also offer account deletion within the app" (we have /settings/delete-account: a typed confirmation phrase, a double confirm, a soft delete and a 30-day anonymisation, with no password step, so it already works for provider sign-ins; it must also revoke the Apple token, next bullet); an app that logs in with a social network "must include a mechanism to revoke social network credentials ... may not store credentials or tokens to social networks off of the device" (so do not persist Kakao or Naver provider tokens server-side); an app with account features must give App Review "an active demo account or fully-featured demo mode". D.
  • Apple, account deletion page: apps with Sign in with Apple "should use the Sign in with Apple REST API to revoke user tokens", and "developers in the Republic of Korea must also provide a server-to-server notification endpoint when registering a new Services ID". D.
  • Google Play: an app that lets people create an account must offer an in-app path to delete it and a web link for the request, declared in the Data safety form (enforced after 2024-05-31). Play has no "Sign in with Google" equivalent rule. D.

5.3 Each provider's button rules ​

ProviderFill, symbol, labelLabel textSize, shapeProminence ruleSource
Kakaocontainer #FEE500, symbol #000000, label #000000 at 85%; no other colour; symbol shape, ratio and colour fixed; the Kakao corporate logo (CI) or a functional icon may not stand in for the symbol, and a symbol is mandatory"카카오 로그인" / "Login with Kakao"; short "로그인" / "Login" for a smaller buttonradius 12; label height at most one third of the container; the symbol may be left-aligned or centred; system fontdo not leave the Kakao colour unspecified so another provider's button stands out (translated)D (Kakao Developers design guide)
Navergreen background #03A94D recommended, colours fixed; white N logo (the guide: at least 18 px in the icon form and 16 px in the complete form, translated)free text in Korean or English that fits the purpose (the guide says the wording may be changed, translated)complete form recommended; centred logo and label 8 apart, or the logo left-aligned when widened; assets in Figma, Illustrator, PNGplace the button where it is seen at once; use it on both the sign-in and the sign-up screensD (Naver Login button guide, devguide 3.2)
Googlelight #FFFFFF with 1 inside stroke #747775, text #1F1F1F; dark #131314, stroke #8E918F, text #E3E3E3; neutral #F2F2F2; the full-colour G stays"Sign in with Google", "Sign up with Google" or "Continue with Google"; localising is permittedGoogle Sans Medium 14/20; padding iOS 16, 12, 16 and Android and web 12, 10, 12 (left of the logo, right of it, right of the text); keep the aspect ratio"displayed at least as prominently as other third party sign-in options", roughly the same sizeD (Google branding guidelines)
Applesystem black, white, or white with an outline; custom buttons only black or white, Apple's logo artwork onlyonly "Sign in with Apple", "Sign up with Apple" or "Continue with Apple" (the system button localises)minimum 140 × 30 pt, 44 default; title 43% of the button height; right margin at least 8% of the width; the corner radius may match the other buttons"Prominently display ... no smaller than other sign-in buttons, and avoid making people scroll"D (HIG sign-in-with-apple)

The four guides allow one layout: the same height and width, each in its own fill, the Kakao radius at 12 (the review-1 ruling and Kakao's own "radius 12 px"), Apple through the system button, which brings the approved look and the translation. The law "one ball-filled action per screen" is not touched (no ball), but two or three brand-filled provider buttons extend the review-1 exception, which named Kakao only: flagged for the owner.

5.4 What Supabase gives us ​

ProviderNative in Supabase Auth?What to know
KakaoYesScopes profile_nickname, profile_image, account_email (optional). "The account_email consent item is only available for apps that are registered as Biz App"; otherwise omit it and switch on Allow users without an email. Client ID is the REST API key. Kakao can return a CI (kakao_account.ci) when the app has the CI consent item and the person agrees; the conditions for getting it are not on the page.
GoogleYesStandard.
AppleYesNative iOS sign-in needs only the App ID and the client IDs; no OAuth settings. The OAuth web flow (Android, web) needs a Services ID and a key, and a new client secret every 6 months. The full name arrives only on the first sign-in.
NaverNoA custom OAuth2 provider: identifier custom:naver, endpoints entered by hand, PKCE on, signInWithOAuth({ provider: 'custom:naver' }); Free plans hold 3 custom providers. Naver nests its profile under response, and whether Supabase's custom provider maps that shape is not on the page: a one-day spike before promising Naver.

Naver's production use needs the app review: login without the limit to registered test IDs works only once the review is completed and approved (translated from the devguide); its profile items (name, nickname, email, birthday, age range, gender, birth year, mobile number) are consent items, each with a stable per-app identifier. D.

Apple on iOS: use the native flow (expo-apple-authentication, an Expo SDK module for the first iOS build, which EXT-4 already gates): no Services ID, no 6-month secret rotation, the system button. The OAuth browser flow is the Android and web path if you add Apple there.

5.5 When 본인인증 happens ​

OptionWhat it isForAgainst
A. At sign-up, for everyoneProvider tap, consents, PASS or carrier verification, then profileEvery account is a verified person; the directive read literallyNaver says this loses users; adds a context switch to PASS or the carrier app for a 40 to 60 audience before anyone sees the app; Apple HIG: delay sign-in
B. Just-in-time at the first trust action (recommended)One screen "본인 확인" the first time someone joins a club, hosts, pays, or enters a result; the result is stored on the account and never asked twiceSign-up is two taps; the abuse the audit worries about is a first-RSVP problem; Naver and Apple both ask for delayA member can browse and be invited before they are verified; some actions wait on the step
C. Provider-verified onlyTake name, birth year, phone and CI from Kakao (CI consent item) and treat a Kakao sign-in as verifiedNo second step for Kakao usersOnly if Kakao approves the CI item (conditions unknown); Naver users and email users still need B; not an officially recognised 본인확인 service in the sense of 정보통신망법 unless counsel says so (unverified)

The vendor path for the step is PASS or carrier 본인확인 through PortOne, which the payments work already uses (the server SDK exposes an IdentityVerification client with SMS and app flows; I could not verify its returned fields or price). The step stores real name, birth date, mobile number and a CI hash, and sets nationality_code. Counsel decides what may be stored (CI in particular) and the under-14 rule.

5.6 What the Korean services do (measured) ​

No App Store frame of these services shows a sign-in screen, so I rendered each one's mobile web sign-in at 390 × 844 (W, 2026-10-11). The web page is not the app, but the provider set and order are the product's choice.

ServiceProviders, in orderLayout (measured)
무신사Kakao, Apple, email sign-up (the email and password form is above them)358 × 40, r4, 12 apart; Kakao yellow, the others outlined; a coupon line above them
컬리Naver, Kakao, own ID sign-up; own ID sign-in collapsed under a chevron350 × 52, r6; Naver green, Kakao yellow, own purple, stacked
BAND (Naver)email, phone, "또는" (or), Naver, Facebook, Google, "로그인 수단 모두 보기 ⌄" (show all sign-in methods)310 × 48, r8, 12 apart; email ink and phone green filled, social outlined white with the logo at the left
번개장터Kakao, Naver, Facebook, 본인인증으로 로그인text list read from the page (an app-install sheet covered the layout)
오늘의집Facebook, Kakao, Naver as three 48 round icons under "SNS계정으로 간편 로그인/회원가입" (quick sign-in with a social account)a "1초만에 앱으로 로그인하기" (sign in on the app in one second) button first; the email form second
쿠팡, 야놀자, 당근, 배민blocked, 404 or no sign-in on the web pagenot measured

Kakao and Naver each appear on four of the five and both together on three; Google on one, Apple on one. The widths are 40 to 52; ours is 52. A search snippet reports Kakao Talk used by 92.5% of Koreans aged 19 to 69 and Naver search by 67.5% (MSIT 2025 survey, S, platform use and not login share; I do not rely on it): ask ten of your members which of the two they open daily before you rank them.

5.7 The screen, in the library's terms ​

  • Entry band (CTA band variant "Entry"): 16, provider buttons 52 each with 12 between, 12, the ghost md "이메일로 로그인" (44), 12, the links row (이용약관 and 개인정보 처리방침, 18 tall, 44 targets), then max(16, inset). Three providers: 16 + 180 + 12 + 44 + 12 + 18 + 34 = 316 (the Kakao-only band is 204 today, 188 under §4.5). A fourth provider adds 64.
  • Labels: Kakao "카카오 로그인" (mandated, English "Login with Kakao"); Naver "네이버 로그인"; Apple the system button's own ("Apple로 계속하기" or "Sign in with Apple"); Google "Google로 계속하기". The verbs differ (로그인 and 계속하기); Kakao's label is fixed, so I would write 로그인 on Naver and Google as well and let Apple's system text stand.
  • After a cancelled provider flow the existing 15/22·600 danger helper line above the first button.
  • iOS: Kakao, Naver, Apple. Android and web: Kakao, Naver, Google. Order within is fixed so a person's second visit finds the same thing.

5.8 Law check ​

Touches: the Kakao exception to S2 (widened to Naver green and Apple black or white), "one bottom CTA per screen" (the Entry band already holds several buttons; N providers is the same variant), "legal text stays verbatim until counsel reviews it" (nothing changes), terminology (카카오 / 네이버 / 애플 as the services name themselves). No law is overridden; the exceptions are for you to confirm.

6. Best-practice audit of 47 entries ​

Scope: the entries of library §1 (without 1.8, the Medal set, which lane B audits with the labels), §2, §3 and §8. Major is something a reviewer sees on the boards today or a break of a platform or accessibility rule; minor departs from a reference or from the library's own rule with no visible harm today; none means I found no contradiction (a note may follow). The reference column names what I compared with; it does not claim the entry was matched against everything the reference says. Result: 4 major, 12 minor, 31 none. The four majors are the owner's three points (they land on Tabs, Tab bar, CTA band and the WizardShell header).

6.1 Foundations (12) ​

#EntryCurrent specBest-practice referenceDriftFix
A11.1 Frame and safe areas390 × 844 "iPhone 15 class", status area 44, bottom inset 34; paint-once law; stack: tab bar 56 + inset, CTA band 16 + helper + 52 + 16 + inset, Sheet bottom 20 + insetThe iPhone 15 is 393 × 852 with top inset 59 and bottom 34; the 390 × 844 class (iPhone 12 to 14) has top 47 (D, useyourloaf). Android 16 enforces edge-to-edge at API 36 (D).minorRelabel the frame "390 × 844 (iPhone 12 to 14 class)" and say the top inset is read at runtime; "chrome above the title is 92" is measured below the status area. The two stack numbers follow rows C3 and C5.
A21.2 Rail and content width16 under 390, 20 to 599, 24 from 600; grids from the content widthApple's own margin measured 16 up to 375 and 20 from 390 (P, review-1 lane A §4).noneNone.
A31.3 Grid and spacing ladderMultiples of 4; ladder 4 to 40; line heights multiples of 2Seed spacing tokens x1 4, x2 8, x4 16, x6 24 (D), with half-steps 14 and 18 that this ladder does not use.noneNone.
A41.4 TypePretendard; display 40/48 to count 11/16; reading 16/24, body 15/22, footnote 13/18; minimums 16 reading, 15 single-line, 13 labels, 12 non-essential, 11 badge digitsHIG: iOS default text 17 pt, minimum 11 pt (D). Material 3: bodyLarge 16/24, bodyMedium 14/20, labelMedium 12/16, labelSmall 11/16 (D, TypeScaleTokens.kt).noneBody 15 is 2 under the iOS default and 1 under bodyLarge: a density choice the owner approved, carried by the 16 reading floor and 200% scaling. Revisit only if the audience test flags small text.
A51.5 Heading spacingSection head 32 above, 12 below; day header 24 and 12; step title 24 under the bar11 headings in 6 products: 33.8 above, 16.8 below, ink to ink (P, review-1 lane A §2); the ladder gives 37 and 17.noneNone.
A61.6 SeamsTwo hairline tiers (cell #E5E5E5, edge #D4D4D4); edge only while content scrolls under; at most one band per screenHIG layout: use a scroll edge effect, not a solid backing, to lift controls above content (D). WCAG 1.4.11 does not apply to separators.noneNone.
A71.7 Colour (S2)Rule 6: 6.5:1 at 14 and below. $error #B8332E for fills; success fill #00796A; inverse surface ink, dark #232824WCAG 1.4.3 needs 4.5:1; the house floor is 6.5:1. P: white on #00796A is 5.33 and on #B8332E 5.91, and both carry 11 to 13 px text (success solid Tag; danger Tag, whose weak and solid are one pair; Live tag; count Tag; header badge count). The dark inverse renders #2A302B (both helpers return pal.block), not the #232824 of the library, the board captions and the helper comments, and sits 1.26:1 (1.40:1) from the dark canvas and 1.11:1 (1.23:1) from the dark card. Material 3's snackbar container is InverseSurface, Neutral20 in the light scheme and Neutral90 in the dark (D, ColorLightTokens.kt:37, ColorDarkTokens.kt:37, SnackbarTokens.kt:39).minor(1) White text at 14 px and below on a light red fill uses $errorText #A3261F (7.36:1), or $error becomes #A3261F (in dark they are already one value, #FA9B94); success solid #00695C (6.61:1). (2) Add a palette key inverse; both helpers read it. Value is the owner's call: keep dark #232824 and give Toast and Coachmark the edge-tier outline (white α0.14), or flip as Material does (#DFE4DC fill, ink text, 14.59:1 to the canvas). I would flip the Toast.
A81.9 Radii6 Tag, 8 thumb, 12 button and notice, 16 block and sheet, fullKakao's button guide fixes radius 12 (D).noneNone.
A91.10 Icons24 actions, 20 hero facts and inline, 16 chevrons; Lucide stroke 2; inline glyphs grow to 1.5×HIG: SF Symbols scales are defined against the cap height of the system font (D). Material icons are 24 dp (D).noneA glyph beside text now attaches through the line slot (Point 1).
A101.11 Targets48 icon button, 52 primary, 56 FAB, 44 floor by hit slopWCAG 2.5.8 (24 CSS px floor). HIG: a hit region of at least 44 × 44 pt, and iOS 26 button heights Mini 28, Small 32, Regular 44, Large 52 (D); ours sm 36, md 44, lg 52. Material minimumInteractiveComponentSize 48 dp (D).noneNone.
A111.12 Motion tokensDurations 80 to 300 ms; standard (0.2, 0, 0, 1), enter (0, 0, 0, 1), exit (0.3, 0, 1, 1), emphasizedEnter (0.05, 0.7, 0.1, 1); "nothing animates longer than 300 ms except loops and the platform's own push"All four curves equal Material 3's Standard, StandardDecelerate, StandardAccelerate and EmphasizedDecelerate; its durations are Short 50 to 200 and Medium 250 to 400 (D, MotionTokens.kt).minornavSlide settles at about 311 ms (§2.5), 11 over the law. Say "about 300 ms" or retune the spring to settle by 300.
A121.13 Leading-marker ruleLM6: the marker's top sits 8 below the text-box top (17/24 and 15/22); LM8: it follows line 1's centre under scalingMaterial 3 ListItem top-anchors leading and trailing content for a three-line item (D, ListItem.kt:1193). P: the unread dot is +1.0 at 15/22 and −2.5 / −10.0 at 130% / 200%.minorLM6 becomes "centred on line 1 through the line slot: top = (line − 8) / 2" (7 at 15/22, 8 at 17/24); §1.13 gains IA1 to IA6 (§2.5).

6.2 Behaviours (9) ​

#EntryCurrent specBest-practice referenceDriftFix
B12.1 Press physicsSix kinds (scale .92 to .98, dim .78 to .92), springs pressIn and pressOut; tabs dim onlyMaterial 3 draws a pressed state layer at 0.10 opacity (D, StateTokens.kt); ours is scale and dim by the "no press band" law, a deliberate departure with a lint behind it.noneNone (the kit holds the table three times: §7 row 1).
B22.2 Hover and focusHover .85 over 100 ms; focus a 2 ink ring, offset 2, outside, instant; Input shows a 2 ink border; sticky chrome never covers the ringWCAG 2.4.7, 2.4.11 (focus not obscured, AA) and 2.4.13 (2 px, 3:1 change of contrast, AAA) (D).noneNone (eight ring copies in the kit: §7 row 2).
B32.3 Disabled, soft, loadingDisabled .5, inert, aria-disabled; soft-disabled stays focusable and explains; loading keeps width and sets aria-busyWCAG 1.4.3 exempts inactive components (D); the reason-line and soft-disabled forms follow review-1 lane B (Baymard, GOV.UK).noneNone.
B42.4 Text scaling85 to 200%; classes reading and label 2.0, hero 1.7, numeral 1.5, chrome 1.3; stacked layouts from 1.5×; inline glyphs grow to 1.5×WCAG 1.4.4 (200%); iOS Dynamic Type and Android non-linear scaling; RN maxFontSizeMultiplier (C). The chrome cap 1.3 departs from 1.4.4 for header and tab labels: recorded, and the iOS Large Content Viewer covers it on iOS only.minor(1) The count Tag and Level tag grow with their numeral (IA4): the count Tag is a fixed 20 box around digits that reach 14.3/21 at the cap. (2) Add "an adornment attaches through the line slot". (3) Keep the 1.3 cap on the accepted-departures list.
B52.5 Motion22 interactions; Reduce Motion replaces, never cuts; lists never animate; "never animate" listWCAG 2.2.2 (D); the platform's own reduced-motion rules.noneNone.
B62.6 Accessibility semanticsOne stop per row; state in aria-selected, aria-checked, aria-pressed, aria-expanded, aria-busy; composed names; status and alert; focus managementRN 0.86.3 View declares aria-busy, aria-checked, aria-disabled, aria-expanded, aria-selected, not aria-pressed or aria-invalid (C, ViewAccessibility.d.ts:55-59); the app already folds pressed into selected on native (C, packages/ui/src/pressable.tsx:142). WCAG 2.5.3 and 4.1.3 (D).minorWording: "aria-pressed on web; on native it is folded into aria-selected (React Native has no aria-pressed)". Tabs and FilterSheet entries say the same. The "입력 오류" suffix for invalid is already stated.
B72.7 PlatformEdge-to-edge handled by the chrome; predictive back stays off; swipe-back; sheets; keyboard; haptics; native tabs not adopted; 600 dp verifiedAndroid 16: edge-to-edge opt-out removed at API 36; predictive back on by default, with a "temporary" opt-out (android:enableOnBackInvokedCallback="false"); orientation, resizability and aspect-ratio limits ignored from 600 dp, and that opt-out ends at API 37 (D, developer.android.com 16 and 17 behaviour changes).minorAdd a dated line: the back opt-out is temporary; plan the predictive-back migration before the API 37 target. Keep the 600 dp, 200% text check on the Play checklist (already there).
B82.8 Skeleton and loading timingNothing under 300 ms; a skeleton from 300 ms, held at least 500 ms; determinate indicator past 10 s; shimmer rests after 5 sSeed's loading pattern: show no indicator under 1 s (flicker), spinner or skeleton from 1 to 4 s, determinate past 10 s (D). WCAG 2.2.2 (D).minorThe 300 ms start is 700 ms earlier than the Korean benchmark, and the 500 ms hold means a load that lands at 350 ms shows content at 800 ms. Keep 300 + 500 only if loads over 300 ms are rare; otherwise start at 1 s, or drop the hold for loads that finish within 1 s.
B92.9 Toast timing4 s result, 8 s with 되돌리기; Android takes the larger of that and the recommended timeout; pauses on touch and on screen-reader focusMaterial 3 Snackbar: Short 4000 ms, Long 10000 ms, and Indefinite by default when there is an action, all passed through calculateRecommendedTimeoutMillis (D, SnackbarHost.kt:104-105,298-312). Seed: 4 s default, longer for long text (D).noneNone (Material holds an action snackbar until acted on; 8 s is defensible because a toast never holds the only copy).

6.3 Chrome and navigation (12) ​

#EntryCurrent specBest-practice referenceDriftFix
C1Header56 below the status inset (64 with a subtitle); back and title at x 56; at most two trailing icons in 48 targets; closed registry of ten glyphs; mode bar of sm ButtonsSeed Top Navigation allows two or three icon buttons on a root page (D); HIG toolbars; targets per A10.noneThe hover tooltip is hand-drawn in FvkHeader (§7 row 15).
C2TabsRow at least 48; content-width labels, 24 between; active 600 ink with a 2 underline; count Tag 20 tall, 4 after the label; filter tabs aria-pressedMaterial 3 centres trailing content for one- and two-line items and top-anchors it for three (D, ListItem.kt:1193). P: the count pill is 2.0 px low on all 52 tab counts (−3.5 at 130% and 200%): an inline-flex pill in a block span plus marginTop: 1. The board verifier has no vertical check.majorPoint 1 (§2.5): the count goes in FvkLineSlot; FvkCountTag grows; aria-pressed per B6.
C3Tab bar56 + inset; icon 24 at 6; label 12/16; badge dot 6 or count 16 with a 1.5 ring at the icon's corner; badge on any tabMaterial 3 NavigationBar: 80 dp, icon 16 below the top, and badgeBounds() clamps the badge inside the item, so the badge top is 14 to 16 dp below the bar's top (D, NavigationBar.kt:536, Badge.kt:107-123,208-218). KakaoTalk 15 pt, 당근 8.4 pt (F). Seed: badges on at most two tabs (D). P: ours is 2.5 on all 3 badged bars (two on the chrome board, one on the hub).majorPoint 2 (§3.4): bar 64 + inset, icon top 12, ring clearance at least 8 (8.5), tabbar check, a 200% frame, "at most two badged tabs".
C4FABExtended pill 56, ball, bottom = tab-bar top + 16, collapses after 120 pt of downward scroll, label 15/22·600Seed: a Snackbar always sits above a FAB (D); the library's Toast does the same. Targets per A10.noneThe kit hard-codes 56 + FVK_HOME_INSET (kit-chrome:505): read the shared constant after C3 (§7 row 16).
C5CTA bandPadding 16 top, 16 + inset bottom; lg button 52; helper 8 above; danger helper = 16 alert glyph + 15/22·600 $errorText; Entry variantTDS BottomCTA: padding 0 20px max(env(safe-area-inset-bottom), 20px), a 36 px gradient over the content, xlarge 56, full-bleed over the keyboard (D, @toss/tds-mobile 2.5.1). Seed measures from the safe-area boundary plus the bar's inner padding (D). HIG Large button 52 (D). P: the button is 50 above the screen's bottom and 37 above the home indicator; the danger glyph is −3.5 at 130%.majorPoint 3 (§4.5): bottom max(inset, 16) (118 becomes 102); the danger helper through FvkHelperLine.
C6Sticky edgesA 1 px edge-tier line only while content scrolls under; on above 8, off at 0; none at restHIG layout: a scroll edge effect lifts controls above content (D).noneNone.
C7MediaBand240 including the status bar; scrim ink α0.35 to 0 over 100; 40 circles at ink α0.5 in 48 targets; white glyphsP: a white glyph on the circle over a white photo is 6.54:1 inside the scrim and 3.57:1 below it, above the 3:1 of WCAG 1.4.11.noneNone.
C8Live rowLive tag, 8, date and venue 13/20·500; min 24; padding 8P: the meta's paddingTop: 2 is exact at 100% only (−0.5 at 130%; at 200% the meta wraps).minorThe Live tag and the meta's first line through FvkLineSlot; delete paddingTop: 2.
C9WizardShell header, progressHeader 56: a 40 square in a 48 target, 건너뛰기 sm tonal; progress row 8 below; the bar "16 clear of the header buttons"; title 24 under the barP: the buttons sit 8 below the status area and 16 above the bar; no reference gives numbers for a wizard header, so the standard is its own balance, the same factor as the footer (C5).majorPoint 3 (§4.5): 12 and 12 by adding 8 of top padding to the 56 row; the library says "12 clear".
C10Search takeoverHeader row 56: Search field 40 and 취소 sm tonal; recents; results; staged CTA bandInput text is 16/24, the §1.4 reading floor; targets per A10; the keyboard rules of §2.7.noneNone.
C11Header icon badgeDot 6 or count 16 with a 1.5 ring, overhang 2; dot means pending, count means a number; never bothMaterial 3 Badge: small 6, large 16, offsets 6 and 12 / 14 (D, Badge.kt:107-110,201-218). Seed Notification Badge: top right, fixed position and size, 0 not shown, at most 4 characters (D). HIG: a red oval with white text, reserved for critical information (D).noneAdd the clearance rule of C3 (ring top at least 8 below the host's top edge).
C12CoachmarkInverse surface r12, pad 16, arrow 8, width at most 280, body 16/24 white, action 15/22·600 in ball (14.96:1 on ink)P: ball on the dark inverse is 11.73:1 on #232824 and 10.55:1 on #2A302B; both pass.noneDark inverse per A7; the action's hit box is 44 here and 48 in the Toast (§7 row 7).

6.4 Overlays and feedback (14) ​

#EntryCurrent specBest-practice referenceDriftFix
O1The overlay ruleSeven cases S-1 to S-7; one overlay at a time; backdrop ink α0.4; system back closes the topmostHIG alerts: avoid alerts for common undoable actions; up to three buttons (D). HIG sheets: one sheet at a time (D). Seed's Snackbar has a Critical variant that S-7 deliberately forbids (D).noneNone.
O2DialogCentred, width 350 (the content width), r16, pad 20, title 17/24·600, body 16/24, tonal flex 1 + primary flex 2; the backdrop does not dismissMaterial 3 AlertDialog width 280 to 560 dp (D, AlertDialog.kt:409-412). Seed: fixed width on large screens, 90% of the width below 768 px (D). The app's ModalPanel caps both placements at maxWidth={640} (C, modal-panel.tsx:189). HIG: the default button on the trailing side (D). The kit's FvkDialogBox is width − 2 × rail, so it grows with the stage.minorState the cap: Dialog width = min(content width, 560). Code: 640 becomes 560 for placement="center". Kit: FvkDialogBox width through fvkOverlayWidth('dialog').
O3MenuSheetA bottom Sheet of Cells 48; groups 24 apart; a group title only with two or more non-destructive groups; destructive group last; from 600 pt an anchored Menu 240 wideSeed Menu Sheet: groups only with three or more items, at most three groups, Neutral and Critical tones (D).noneNone (the row is a Cell: §7 row 12).
O4SheetTop radius 16, pad 20, title row 48 with ×, footer = CTA band geometry, 88% cap, no grabberMaterial 3 ModalBottomSheet: SheetMaxWidth 640 dp and a drag handle by default (D, SheetDefaults.kt:556); Seed Bottom Sheet: max width 480, and for important tasks remove the handle and add a top close button (D); HIG sheets: Cancel or Close, one sheet at a time (D).minor(1) Footer follows C5: max(16, inset). (2) State the width cap the code already has (640, Material's; Seed's 480 is the narrower Korean reference): one number for Sheet, MenuSheet and Value picker.
O5Sheet, value pickerRadio Cells 48; chosen 600 with a 20 check; counts in a figure column; no footer, a tap selects and closesRadio-group semantics (§2.6); Seed Bottom Sheet (D).noneNone.
O6Edit SheetTitle and ×; the control; one 저장, soft-disabled until the value changes; guard Dialog on dirty dismissHIG sheets: pair a Done with a Cancel or Back (D); the × is the Cancel.noneNone.
O7FilterSheetGroups of Choices 36 tall; pair 초기화 + "결과 n개 보기"; trigger Choice with the active countChoice 36 with a 44 hit slop meets A10.noneNone.
O8Document pageA pushed page; version caption; article headings 17/24·600; paragraphs 16/24HIG sheets: lengthy or prolonged flows belong in a full-screen page (D).noneNone.
O9ToastInk surface r12, min 48, pad 12 16, text 16/24 white, action 15/22·600 in ball plus ×; no glyph; 4 s or 8 sMaterial 3 Snackbar: InverseSurface (light on dark), single line 48 and two lines 68, elevation level 3 (D, SnackbarTokens.kt). Seed Snackbar: 4 s default, Positive and Critical variants (D). P: the dark surface is 1.1 to 1.4:1 from the ground it floats over.minorDark: A7. The action's hit box and the × share one row; text actions unify (§7 row 7).
O10EmptyState80 disc, 32 glyph, title 17/24·600, body 16/24, one md Button; seven variantsHIG alerts: a title says what happened, never "Error" (D).noneNone.
O11SkeletonShapes at the row pitches, pulse .6 to .9 over 800 ms, static .75 under Reduce Motion, aria-busyWCAG 2.2.2 (D); Seed loading pattern (D); timing per B8.noneNone (five bar copies in the kit: §7 row 4).
O12Offline strip350 × 48 neutral Notice, glyph, title, 다시 시도 text action; pinned under the header; solid over a MediaBandHIG alerts: for a connection problem prefer a quiet indicator the person can open (Mail's example) (D).noneNone.
O13CountdownNoticeWarning Notice with a clock glyph, 입금 대기 중 and a tabular countdown; at zero a danger Notice with 다시 신청; announces at 5:00 and 1:00WCAG 2.2.1: an essential, server-side limit (D); 4.1.3 (D).noneNone (the kit re-implements the Notice: §7 row 11).
O14SpinnerThe platform indicator, 20, only inside a control; never alone on a list or screenSeed loading pattern: a Progress Circle at the start of a user-triggered action, none under 1 s (D).noneNone.

7. Custom implementations ​

The owner asked to minimise custom implementations across all building blocks. Below is every place in flat-v2-kit-chrome.jsx, the overlay and sheet frames of flat-v2-kit-controls.jsx, and the chrome, overlays and behaviour boards that redoes layout, positioning, typography or spacing which a shared primitive already owns or should own. Counts are from greps of the working tree as of 2026-10-11 20:00 (with the main thread's one-chevron Block change). The main thread's change to FvkCBlockFrame (one chevron, centred on the Block's height) is the model: one owner, and a verifier mark (data-adorn="box") that asserts it.

7.1 A helper the kit holds more than once ​

#Where (file:line, function)What it redoesConsolidate ontoDelete or change
1FVK_CH_PRESS and fvkChFx (kit-chrome:36,44, 4 callers); FVK_C_PRESS and fvkCFx (kit-content:180,181, 10); FVK_CTL_PRESS and fvkCtlState (kit-controls:85,96, 9)The press table of library §2.1 three times, with three appliers (the chrome one has 4 kinds including a dim-only dim for tabs, the content one 4, the controls one all 6)One FVK_PRESS (the controls table plus a tab kind: scale 1, dim .85) and fvkFx(state, kind) in flat-v2-kit.jsxThe other two tables and two appliers; 23 call sites change a name
2Focus ring: fvkChRing (kit-chrome:56, 4 callers), the focus branch of fvkCFx (kit-content:189), of fvkCtlState (kit-controls:102), and five inline copies: FvkSwitch (:542), FvkSlider (:617), FvkStepper (:771), FvkCheckBox (:929), FvkPhotoTiles (:1701, offset 0)The §2.2 ring (2 ink, offset 2) eight timesfvkRing(pal, state, radius, inset) in the core kit (the chrome version, which already handles the inset case)Seven copies; FvkPhotoTiles passes inset
3fvkChInverse (kit-chrome:67; tooltip :293, Coachmark :1120), fvkCtlInverse (kit-controls:153; Toast :2838)The inverse surface twice; both return pal.block (#2A302B) while their comments, the library and two board captions say #232824fvkInverse(pal) reading a palette key inverse (value per audit row A7)Both helpers
4Skeleton bars: FvkCSk (kit-content:203, about 40 call sites, opacity .6), FvkFieldSkeleton (kit-controls:1487), FvkDocPage (:2757), FvkSkeleton (:3002), FvkPostRow loading (:3378) (all .75)One gray100 bar, five times, with two opacities; the library says static .75 under Reduce MotionOne FvkSkBar({ w, h, r }) (data-g="skel-bar", aria-hidden, static .75)Four private bar closures; FvkCSk becomes an alias
5The error line (alert glyph + message in $errorText): FvkCtaBand (kit-chrome:562-570), FvkField (kit-controls:1137-1144), FvkPickerCell (:1399-1406), FvkProviderButton (:1938-1953), FvkSheetFooter (:2176-2183), FvkCell (kit-content:2225-2227)The same line six times with three alignments (paddingTop: 3, marginTop: 1/3, alignItems: center) and two gaps (4 and 8); the Sheet footer's centres on the whole block, so it fails the first-line rule at 130%One FvkHelperLine({ tone, role }) built on FvkLineSlot (role body for bands, foot for fields)Six hand-set offsets; fixes the −3.5, −2.5 and −9 offsets of §2.2 rows 8 and 9
6The band helper and band geometry: FvkCtaBand (kit-chrome:559-586, :679) and FvkSheetFooter (kit-controls:2174-2191) with the footer wrapper in FvkSheetBox (:2158-2163)The neutral helper (string, FvkMeta array, node), the danger helper, and the 16 / 16 + inset padding, twice. When FvkCtaBand takes max(16, inset) (§4.5) the Sheet footer would silently stay at 16 + insetFvkBandHelper and fvkBandPad(inset, rail); FvkSheetFooter renders the same body as the bandThe second helper and the second padding expression
7Text actions (15/22·600, 44 floor): FvkCoachmark (kit-chrome:1165-1176, 44, ball on inverse), FvkToast (kit-controls:2872-2881, 48, ball), FvkNotice (kit-content:3372-3386, 44, margin −11), FvkCountdownNotice (kit-controls:3248-3257, 48, margin −12), the Entry links row in FvkCtaBand (kit-chrome:592-643, 44, margin −13), the consent tail in FvkCell (kit-content:2159-2165), FvkComment Reply and more-replies (kit-controls:3616-3664, 13/18), FvkBubble Resend (:3752-3758)Eight components, each with its own hit box and negative margin; the library says text actions keep a 44 floor and 48 is for icon buttons (Offline strip), yet Toast and Countdown use 48One FvkTextAction({ role, tone, onInverse }) with the 44 hit slop from fvkCtlHitEight hand-built boxes and their margins
8A glyph's edge put on the rail by a negative margin on an FvkIconButton: wizard back (kit-chrome:759), Sheet × (kit-controls:2143), Toast wrapper and × (:2867,2891), :3466, Comment menu (:3587), :3939, pager (kit-content:2450), Notice × (:3391), :3837Ten sites compute (48 − glyph) / 2 by hand (−4, −12, −14)FvkIconButton prop bleed="end" or "start" that applies the offsetTen margins
9Group titles (13/18·600 secondary, 8 below): FvkMenuSheetBox (kit-controls:2416), FvkValueSheet (:2454), FvkFilterSheet (:2578)One element three timesFvkGroupTitleThree inline FvkT blocks
10The badged icon host (a position-relative 24 box with FvkHeaderBadge anchor="glyph"): FvkIconButton (kit-controls:460-468), FvkTabBar (kit-chrome:474-477), the board specimen LibChrBadgeCell (flat-v2-lib-chrome:1687-1705)The host box and its anchor three times; the specimen draws its own item (padding 16 / 0 / 8, gap 12) instead of the real tab barFvkBadged({ size, badge, children }) owning the host and the corner anchor (data-adorn="corner"); the specimen renders FvkTabBarThree host spans; the specimen's own layout

7.2 A component that redoes another component ​

#WhereWhat it redoesConsolidate ontoDelete or change
11FvkCountdownNotice (kit-controls:3175-3262, 88 lines)A Notice: the rounded box, tone fill and foreground (its own light and dark colour logic, equal to fvkCNoticeTone), glyph with marginTop: 1, title, action box. Only the tabular countdown at the right of the title is newFvkNotice with a trailing prop and tone warning or dangerThe private box, colours, glyph offset and action box
12fvkCtlMenuRow (kit-controls:2292-2340)A Cell: min 48, rail padding, gap 12, 20 icon, label, count Tag, 16 chevron, cell hairline; the library says "groups of Cells 48"FvkCell (leading icon, count, chevron, hairline, inset for the anchored Menu's 16)The row function, its hairline and the padX special case
13FvkWizardHeader (kit-chrome:738-779,803-823)An icon button: the 40 square (squareStyle) in a 48 target with its own press and ring, and the labelled-back pill; the done segments' 44 hit zone is a box height (height: done ? 44 : 20)FvkIconButton variant="square" (40 in a 48 target); FvkButton tone="tonal" icon="back"; fvkCtlHitsquareStyle, the wrapper, the height trick. The 40 square and 40 pill are one-off sizes: the owner may prefer the md (44) geometry
14Hand-set first-line offsets: FvkTabs (kit-chrome:401), FvkCtaBand (:564), FvkLiveRow (:713), FvkField (kit-controls:1138), FvkPickerCell (:1400), FvkFieldNote (:1464), FvkProviderButton (:1947), FvkCountdownNotice (:3222), FvkCell (kit-content:2226), the unread dot in FvkCRow (:1079, top: 8)Ten (line − adornment) / 2 constants, each right at one text sizeFvkLineSlot (§2.5)Ten constants (rows 5 and 11 above overlap)
15The hover tooltip inside FvkHeader (kit-chrome:279-304)A tooltip primitive that nothing else shares: padding 4 8, r6, centred-or-right position maths, inverse fillFvkTooltip({ at, children })The inline block; the position maths moves with it
16The chrome stack as literals: FvkFab (kit-chrome:505), FvkTabBar (:440,463,465), flat-v2-lib-overlays:234,1173, LibChrAbove h values 118, 148, 170, 204 in flat-v2-lib-chrome (11 sites), the spec text in flat-v2-authThe library §1.1 stack (tab bar, band, FAB, Toast pin) re-typed as numbers. Point 2 and 3 would have to edit 17 placesfvkChrome(inset) in the core kit: { tabBar, band(helperLines), fabBottom(placed), toastPin(...) }The literals; LibChrAbove takes the bar's own height
17FvkDialogBox width (kit-controls:2229), FvkSheetBox width (:2118), FvkToast pin (:2893-2900)Overlay widths and pins computed per componentfvkOverlayWidth(kind) (Dialog 560, Sheet 640, per O2 and O4) and fvkChromeThe per-component expressions
18FvkWizardHeader title block (kit-chrome:880-884) and FvkSheetBox title row (kit-controls:2128-2147)A title row each, with its own padding and 48 minimum. The Sheet's FvkIconButton takes marginRight: -12 (row 8)Fold into bleed="end" (row 8)The margin

7.3 Board-local wrappers ​

#WhereWhat it redoesConsolidate ontoDelete or change
19FndS (flat-v2:17), LibChrS and LibChrP (lib-chrome:60,77), LibBehS and LibBehP (lib-behaviour:19,74), LibOvlF, LibOvlSlice and LibOvlP (lib-overlays:9,26,46)A FvkFrame around a FvkStage, FvkPhone or FvkSlice, passing the same 8 to 12 props through. Eight wrappers in my four boards and eight more in the brand, detail, controls and social boards (16 in all; a seventeenth match, LibBehDialogPair, is a specific frame)FvkFrame taking stage, phone or slice props (one place to add a prop)The eight wrappers (16 repo-wide); their prop pass-through lists
20LibBehSpoken (lib-behaviour:41-72) and LIBBEH_PINK (:16)A spoken-line box with its own font strings (700 11px/16px, 500 13px/20px), padding 8px 12px and a literal #EAF1EC, beside the kit's FvkSpec and FVK_BOARDFvkSpec / FVK_BOARD tokensThe literals

Total: 20 findings: ten duplicated helpers (rows 1 to 10), five components that redo another (11 to 14 and 18), three one-offs around the chrome stack (15 to 17) and two board-wrapper groups (19, 20). Rows 5, 6, 14 and 16 are the ones that already caused the defects of §2 to §4.

Order of work. The line slot (row 14) and the helper line (row 5) first, because the owner's Point 1 and the CTA danger helper need them; fvkChrome (row 16) before the tab bar and band changes of Points 2 and 3; press, ring and inverse (rows 1 to 3) next, as pure renames; the text action and icon-button bleed (rows 7 and 8) after; the two component merges (rows 11 and 12) last because they change markup the verifier already counts.

8. Fix list, in implementation order ​

Decisions first (all marked in the sections above): the header gap, 12 and 12 or 16 and 16 (§4.6); the tab bar, 64 or 72 (§3.4); the dark inverse surface and the one red (row A7); the sign-up decisions (§5.1).

8.1 flat-v2-kit.jsx (core; nothing below lands before it)

  1. Add fvkLineH(role, scale, cls) (promote fvkCLineH) and FvkLineSlot({ role, cls, children }); document data-adorn="box" | "corner" and data-line in the header comment.
  2. Add FVK_TABBAR = 64 and fvkChrome(inset) returning { tabBar, band(helperLines), fabBottom(placed), toastPin(layers) }.
  3. Add FVK_PRESS and fvkFx(state, kind) (six kinds plus tab), fvkRing(pal, state, radius, inset), fvkInverse(pal), and a palette key inverse.
  4. Colours per A7 once decided: light danger fill for 14 px and below, success solid, dark inverse.
  5. FvkFrame takes stage, phone and slice props.

8.2 flat-v2-kit-chrome.jsx

  1. FvkTabs: the count Tag in FvkLineSlot; delete marginTop: 1; the 12 spacer derives from the line.
  2. FvkTabBar: height: FVK_TABBAR + inset, item height: 64, paddingTop: 12; host through FvkBadged.
  3. FvkFab: bottom from fvkChrome.
  4. FvkCtaBand: padding 16 rail max(16, inset) through fvkBandPad; helper through FvkBandHelper; the Entry links row through FvkTextAction; delete paddingTop: 3.
  5. FvkLiveRow: Live tag and the meta's first line in the slot; delete paddingTop: 2.
  6. FvkWizardHeader: the header row padding: '8px rail 0' (border-box, stays 56); back square and pill onto FvkIconButton and FvkButton; segments' hit zone through fvkCtlHit.
  7. FvkHeader: tooltip onto FvkTooltip; badge host onto FvkBadged. FvkCoachmark: action onto FvkTextAction.
  8. Delete FVK_CH_PRESS, fvkChFx, fvkChRing, fvkChInverse.

8.3 flat-v2-kit-content.jsx

  1. FvkCountTag: grow with the numeral (box = ceil(20 × min(scale, 1.3) / 4) × 4; the 16 badge form is fixed).
  2. FvkCRow: the unread dot through the slot (delete top: 8 and the markerLeft arithmetic); FvkCell: error line onto FvkHelperLine, consent tail onto FvkTextAction, trailing chevron or pencil of a two-line Cell marked data-adorn="box".
  3. FvkNotice: trailing prop, action onto FvkTextAction, glyph through the slot; FvkHero and FvkCaptionRow use the slot instead of private height: lineH wrappers.
  4. FvkCBlockFrame: the new chevron carries data-adorn="box".
  5. FvkCSk becomes an alias of FvkSkBar; delete FVK_C_PRESS and fvkCFx.

8.4 flat-v2-kit-controls.jsx

  1. FvkField, FvkPickerCell, FvkProviderButton, FvkSheetFooter: error line onto FvkHelperLine.
  2. FvkSheetBox: footer through fvkBandPad; width through fvkOverlayWidth; the × through bleed="end". FvkSheetFooter: shares FvkBandHelper. FvkDialogBox: width cap 560.
  3. FvkToast: action onto FvkTextAction; inverse per A7; pin from fvkChrome.
  4. FvkCountdownNotice becomes a FvkNotice; fvkCtlMenuRow becomes FvkCell.
  5. Skeleton bars in FvkFieldSkeleton, FvkDocPage, FvkSkeleton, FvkPostRow onto FvkSkBar; group titles onto FvkGroupTitle.
  6. FvkIconButton: bleed and variant="square" props, FvkBadged. FvkSwitch, FvkSlider, FvkStepper, FvkCheckBox, FvkPhotoTiles: rings onto fvkRing.
  7. Delete FVK_CTL_PRESS, fvkCtlState, fvkCtlInverse.

8.5 Boards

  1. flat-v2.jsx: FndS onto FvkFrame stage; the frame ruler (tab bar 64, content 646) and the "iPhone 12 to 14 class" label.
  2. flat-v2-lib-chrome.jsx: drop LibChrS and LibChrP; LibChrAbove measures its bar (118, 148, 170, 204 go); LibChrBadgeCell renders the real FvkTabBar; add a 200% tab-bar frame and the two-badged-tabs rule; captions for tab bar, CTA band, wizard header and count Tag carry the new numbers.
  3. flat-v2-lib-overlays.jsx: drop LibOvlF, LibOvlSlice, LibOvlP; Toast pins from fvkChrome (lines 234 and 1173); the dark inverse caption per A7.
  4. flat-v2-lib-behaviour.jsx: drop LibBehS and LibBehP; LibBehSpoken onto FVK_BOARD tokens.
  5. flat-v2-auth.jsx: the sign-in frames take the provider set (iOS and Android variants) and the 316-tall entry band (§5.7).

8.6 Library (docs/design/flat-v2-component-library-2026-10-09.md)

  1. §1.1 Frame label and stack (tab bar 64 + inset; CTA band 16 + helper + 52 + max(16, inset)); §1.3 unchanged; §1.7 colour fixes per A7; §1.12 the 300 ms wording; §1.13 LM6 rewrite plus the new IA1 to IA6 block.
  2. §2.4 add "an adornment attaches through the line slot" and the count Tag growth; §2.6 aria-pressed wording; §2.7 the dated predictive-back line; §2.8 the start threshold (B8).
  3. §3 Tabs, Tab bar (anatomy, clearance rule, "at most two badged tabs"), CTA band (118 becomes 102; 148 to 132; 170 to 154; Entry 204 to 188 with one provider, 316 with three), WizardShell ("12 clear"; square 12 to 52), Header icon badge (clearance rule); §6 Count tag (grows with its numeral); §7 ProviderButton (a provider set); §8 Dialog and Sheet widths, Sheet footer, Toast dark.

8.7 Verifier (scripts/verify-flat-v2-boards.mjs)

  1. adorn (§2.6), tabbar (§3.5) and cta (§4.7), each with a known-positive canary that must fail when its fixture is broken.

8.8 App (packages/ui, packages/app)

  1. LineSlot beside GlyphSlot; Badged (the §3.4 offsets) replacing CornerCountBadge; UnderlineNav takes a count; BottomCtaBand and bottom-cta-band-visual.ts take paddingBottom = Math.max(insets.bottom, 16); WizardShell header numbers; tab-bar constants; the colour tokens of A7; ModalPanel centre placement capped at 560.
  2. Sign-up: the signInWithOAuth provider list, Sign in with Apple (native), the 본인인증 step and linkIdentity flow, in the order of §5.

9. What I could not verify ​

  • Devices. Every measurement is a Chrome board or a scratch page built from the kit with the repo's Pretendard OTFs. Chrome floors half-leading and rounds ascent and descent, so an ink centre differs from a device by up to 1 px; box centres do not. No React Native prototype was run: the iOS baseline offset and the Android ceil / floor split (up to 0.7 px) are read from source (§2.3), not measured. The on-device probe in §2.6 closes this.
  • VoiceOver and TalkBack. The wording of §2.6 (aria state, togglebutton, status and alert) was not run on a screen reader.
  • Peers. KakaoTalk and 당근 App Store frames are marketing images, so pt values are ratios to a 390 pt screen (F). Five Korean sign-in pages were measured as mobile web (W), not as apps; 쿠팡, 야놀자, 당근 and 배민 showed no usable page. The search snippet of Kakao Talk and Naver use (MSIT 2025) is S and carries no weight.
  • Providers. Whether Supabase's custom OAuth provider maps Naver's nested response profile (one-day spike); the conditions on which Kakao returns a CI; PortOne's identity-verification fields and price; Naver's review time and Kakao's Biz App conversion time; whether a Kakao CI counts as a 본인확인 service under the information and communications network act (counsel).
  • Standards that are the library's own. The 6.5:1 floor for 14 px and below is a house rule, not WCAG (which asks 4.5:1).
  • Platform behaviour I read but did not run. The Android 16 and 17 behaviour pages; text-box-trim support dates (MDN browser-compat-data, read 2026-10-11); the TDS BottomCTA source is @toss/tds-mobile 2.5.1 and may differ in a later version.
  • Material 3 values are dp, read from androidx-main on 2026-10-11 (Badge.kt, NavigationBar.kt, ListItem.kt, AlertDialog.kt, SheetDefaults.kt, SnackbarHost.kt, SnackbarTokens.kt, MotionTokens.kt, TypeScaleTokens.kt, StateTokens.kt, ColorLightTokens.kt, ColorDarkTokens.kt); I treated 1 dp as 1 pt.

10. Sources ​

Repo (C). docs/twomore_design_system/wireframes/flat-v2-kit.jsx, flat-v2-kit-chrome.jsx, flat-v2-kit-content.jsx, flat-v2-kit-controls.jsx, flat-v2.jsx, flat-v2-lib-chrome.jsx, flat-v2-lib-overlays.jsx, flat-v2-lib-behaviour.jsx, flat-v2-auth.jsx; docs/design/flat-v2-component-library-2026-10-09.md; docs/design/owner-review-1-rulings-2026-10-11.md; docs/design/owner-review-2-rulings-2026-10-11.md; docs/design/research/owner-review-2-lane-a-2026-10-11.md; docs/external-blockers.md (EXT-4, EXT-11); docs/guides/owner-credential-runsheet.md; docs/design/usability-audit-2026-09-16.md (U-01); docs/architecture/rating-hardening-audit.md; packages/ui/src/{glyph-fact-row,badge,corner-count-badge,pill-nav-underline,pressable,modal-panel}.tsx; node_modules/react-native 0.86.3 (RCTAttributedTextUtils.mm, CustomLineHeightSpan.kt, TextAttributeProps.kt, ViewAccessibility.d.ts).

Platform guidance (D).

  • Apple App Review Guidelines (4.8, 5.1.1): developer.apple.com/app-store/review/guidelines
  • Apple, offering account deletion in your app: developer.apple.com/support/offering-account-deletion-in-your-app
  • Apple HIG (JSON endpoints, read 2026-10-11): tab-bars, sheets, alerts, buttons, layout, typography, sf-symbols, sign-in-with-apple under developer.apple.com/tutorials/data/design/human-interface-guidelines
  • Android 16 and 17 behaviour changes: developer.android.com/about/versions/16/behavior-changes-16 and /17/behavior-changes-17
  • Google Play account deletion requirement: support.google.com/googleplay/android-developer/answer/13327111
  • Material 3 Compose sources: github.com/androidx/androidx, compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3 (files named in §9)
  • MDN browser-compat-data css/properties/text-box-trim.json and text-box-edge.json; webstatus.dev/features/text-box-trim
  • WCAG 2.2: w3.org/TR/WCAG22 (1.4.3, 1.4.4, 1.4.11, 2.2.1, 2.2.2, 2.4.7, 2.4.11, 2.4.13, 2.5.3, 2.5.8, 4.1.3)
  • Device sizes: useyourloaf.com/blog/iphone-14-screen-sizes and /iphone-15-screen-sizes

Korean design systems (D). Seed (daangn): seed-design.io, pages bottom-navigation, notification-badge, snackbar, dialog, menu-sheet, bottom-sheet, tabs, top-navigation, patterns/safe-area, patterns/loading, foundations/spacing. Toss TDS: @toss/tds-mobile 2.5.1 package source (BottomCTA, Tab).

Providers (D). Kakao design guide (developers.kakao.com/docs/latest/ko/kakaologin/design-guide) and REST API page; Naver login button guide (developers.naver.com/docs/login/bi/bi.md) and devguide; Google branding guidelines (developers.google.com/identity/branding-guidelines); Supabase Auth: social login, Kakao, Apple, custom OAuth providers, identity linking (supabase.com/docs/guides/auth); PortOne v2 identity verification (developers.portone.io) and @portone/server-sdk.

Peers (F, W). iTunes lookup, country kr, for KakaoTalk (kakaotalk:3) and 당근 (daangn:4); mobile web sign-in pages of 무신사, 컬리, BAND, 번개장터, 오늘의집 rendered at 390 × 844, @2x, mobile user agent.

Method files (scratchpad, not in the repo). Mini-builder with a kit patch hook, CDP helper, the adorn check and canary, tab-bar, wizard and footer prototypes, contrast scripts, and the screenshots, kept in the session scratchpad for lane review.

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