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
| Tag | Meaning |
|---|---|
| C | Code in this repo or in node_modules (path:line), including RN 0.86.3 sources. |
| D | A documentation page, token file or package source read in full or by section (URL in §10). |
| F | An App Store listing frame (iTunes lookup, country kr). Marketing frames, so pt values are ratios to the screen width, assuming a 390 pt screen. |
| W | A public web page rendered logged out in headless Chrome at 390 × 844, @2x, mobile user agent. |
| P | A 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. |
| S | A 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
| # | Point | Recommendation (exact values, type roles, tokens) | Evidence | Kit functions and library entries to change |
|---|---|---|---|---|
| 1 | Count tags and every adornment beside text | One 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. |
| 2 | Tab-bar dots and counts | The 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. |
| 3 | Wizard shell spacing | Both 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. |
| 4 | Sign-up route | Kakao, 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. |
| 5 | Best-practice audit of 47 entries | 4 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. |
| 6 | Custom implementations | 20 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 text | Kit function and how it is aligned today (C) | 100% | 130% | 200% | Verdict |
|---|---|---|---|---|---|---|
| 1 | Count Tag after a tab label | FvkTabs (kit-chrome:401): a block <span style={{marginTop: 1}}> around an inline-flex pill | +2.0 | −3.5 | −3.5 | Wrong at every size; the pill also stays 20 tall while its digits grow to 14.3/21 |
| 2 | Label Tag in a Cell | FvkCell label cluster: inline-flex, alignItems: center | 0.0 | 0.0 | 0.0 | Right (single-line labels) |
| 3 | Level tag next to the figure of a person row | fvkCPersonTrailing: flex, centre; the cluster sits in FvkCRow's trailing slot (height: l1H) | 0.0 | 0.0 | 0.0 | Right |
| 4 | Level tag after the identity name | FvkCBlockIdentity: flex, centre, wrap | 0.0 | 0.0 | 0.0 | Right |
| 5 | Glyph and Tag in a hero fact | FvkHero: a wrapper height: fvkCLineH('headline'), flex centre | 0.0 | 0.0 | 0.0 | Right (this is the slot rule, hand-made) |
| 6 | Live tag before the date and venue | FvkLiveRow: paddingTop: 2 on the meta | 0.0 | −0.5 | wraps | Right at 100% only |
| 7 | Alert glyph in a Notice | FvkNotice: wrapper height: titleLine, flex centre | 0.0 | 0.0 | 0.0 | Right when the title is the first line (slot rule, hand-made); 1 off when the first line is the 16/24 body (row 14) |
| 8 | Alert glyph in the CTA danger helper | FvkCtaBand: paddingTop: 3 around a 16 glyph | 0.0 | −3.5 | −11.0 | Right at 100% only |
| 9 | Alert glyph in a field error | FvkField: marginTop: 1 (and five more copies of the error line, §7 row 5) | 0.0 | −2.5 | −9.0 | Right at 100% only |
| 10 | Unread dot on a row | FvkCRow: absolute top: 8 (library LM6: "its top sits 8 below the text-box top") | +1.0 | −2.5 | −10.0 | Off at 15/22 even at 100%; violates LM8 ("follow line 1's centre") |
| 11 | Level tag and Tag in a roster cell | FvkRoster: flex, centre, minHeight: 24 | 0.0 | 0.0 | 0.0 | Right |
| 12 | Clock glyph in the CountdownNotice | FvkCountdownNotice: marginTop: 1, alignItems: flex-start | 0.0 | n/a | −6.0 | Right at 100% only |
| 13 | Message time glyph in a bubble row | social board | +3.0 | n/a | n/a | Off at 100% (lane C's board; same cure) |
| 14 | Lock glyph in a Notice with a body and no title | FvkNotice through FvkFieldNote box: a 20 glyph in a 22 title-line slot, but the first line is the 16/24 body | −1.0 | n/a | n/a | Off 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.
| Recipe | Box Δ vs first line | Ink Δ (label bbox) | Two-line label |
|---|---|---|---|
A. today: block span with marginTop: 1, inline-flex child | +2.00 | +2.31 | not tested |
B. slot: a flex box as tall as the line, align-items: center, top-anchored | 0.00 | +0.31 | 0.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 centred | n/a (box moved) | +0.50 | n/a |
| B at 1.3× (29 line, 24.8 pill that grows with its numeral) | 0.00 | +0.44 | n/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
- A phantom strut. In
FvkTabsthe pill is aninline-flexinside 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 themarginTop: 1that was meant to centre it. Reproduced in isolation: recipe A +2.00. P. - Magic numbers that are right at one size.
marginTop: 1,paddingTop: 3,top: 8,paddingTop: 2: each is(line − adornment) / 2computed 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. - Row centring where first-line centring is meant.
align-items: centeron 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. - 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. - 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.
- 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-trimis not needed (§2.4). C, P. - 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) / 2whenever a lineHeight is set and at least the font's own (RCTAttributedTextUtils.mm:337,339); Android'sCustomLineHeightSpan"follows web-like behavior for line height" and splits the leadingceilabove andfloorbelow (CustomLineHeightSpan.kt:16,42-43). Android'sincludeFontPaddingdefaults totrue(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. - 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 byalignItems="center"on the row,Badgedefaults toalignSelf: 'flex-start'(badge.tsx), and three call sites override it withalignSelf="center"(status-badges.tsx:179,match-scoreboard.tsx:433,club-session-card.tsx:142). The emoji slot appliesincludeFontPadding: 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
Tabdraws its dot atposition: absolute; right: 0; top: 4px; 6 × 6; #f44336inside 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-mobile2.5.1; seed-design.io notification-badge, tabs). - CSS has a feature for this, with a narrow reach.
text-box-trimandtext-box-edge(cap alphabetictrims 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: baselineis 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 BCDtext-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) / 2from 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 ownminHeight. - 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) × 4andceil(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: addfvkLineH(role, scale, cls)(promotefvkCLineH,kit-content:273, which seven call sites already use privately) andFvkLineSlot({ role, cls, h, children })(display: flex; alignItems: center; alignSelf: flex-start; flexShrink: 0; height: fvkLineH(...),data-g="line-slot",data-line). Documentdata-adorn="box" | "corner"in the header comment.kit-chrome:FvkTabs(the count goes in<FvkLineSlot role="body" cls="chrome">; deletemarginTop: 1),FvkCtaBand(the danger helper's glyph; deletepaddingTop: 3),FvkLiveRow(the Live tag; deletepaddingTop: 2).kit-content:FvkCountTag(grow with the numeral; keep the 16 form),FvkCRow(marker through the slot; the absolutetop: 8and themarkerLeftarithmetic go),FvkCell(the consent box's computedmarginTop, the error line; mark the trailing chevron or pencil of a two-line Celldata-adorn="box"),FvkNotice,FvkHeroandFvkCaptionRow(their privateheight: lineHwrappers become the slot),FvkCBlockFrame(the new chevron getsdata-adorn="box").kit-controls: the six error-line copies (§7 row 5) become oneFvkHelperLine, which uses the slot;FvkCountdownNoticebecomes aFvkNotice(§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).
- 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 markeddata-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 (comparegetBoundingClientRect().widthtooffsetWidth). - Measure. Δ = centre of the adornment's box − centre of that first line box (top + line height / 2, from the computed
line-height). - 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.
- 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). - 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: 3pxwrapper (the tab bug, +2) and the same pill in adata-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.
| Part | Before (56 + 34 = 90) | After (64 + 34 = 98) |
|---|---|---|
| Icon (24) | y 6–30, x 27–51 in its 78 wide item | y 12–36 |
| Label (12/16) | y 32–48, 8 below it in the bar | y 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 box | y 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 label | y 8.5–26.5 |
| Clearance, bar top edge to the badge | 2.5 | 8.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
| Reference | Bar | Icon top below the bar's top | Badge | Clearance 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 structural | 14 dp large, 16 dp small |
KakaoTalk, 4th tab, "194" (F, kakaotalk:3) | at least 64.5 pt visible | 20.7 pt | 27 × 17 pt pill; top 5.4 pt above the icon's top and 7 pt beyond its right edge | 15 pt |
당근 알바 bar, floating capsule (F, daangn:4) | capsule 63 pt | 12.9 pt | 17 pt circle, centre about 5 pt right of the icon and 4 pt below its top | 8.4 pt |
Toss TDS Tab (D, package source) | top tabs | n/a | dot 6, right: 0; top: 4px, #f44336; a dot only, no counts | n/a |
| Seed Bottom Navigation (D) | not stated | not stated | Small (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 scaling | n/a |
| Apple HIG, Tab bars (D) | system | system | "a red oval containing white text and either a number or an exclamation point"; "reserve badges for critical information" | system |
| Ours | 56 | 6 | dot 6 and count 16, both with a 1.5 canvas ring, ring corner at the icon's top-right +3.5 / −3.5 | 2.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 atW + 2, ring 18 to 19; it grows to the left for99+. 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'sheight: 64,paddingTop: 12),FvkFab(placed === 'tab'offsetFVK_TABBAR + FVK_HOME_INSET + 16). Boards:flat-v2-lib-overlays.jsxlines 234 and 1173 (56 + FVK_HOME_INSET),flat-v2.jsx(the frame ruler'stab bar 56andcontent 654become 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
tabbarinverify-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.
| Gap | frame y | px |
|---|---|---|
| Status area | 0–44 | 44 |
| Status bottom to the back square's top | 44 → 52 | 8 |
| Back square (40 in a 48 target, 48–96) | 52–92 | 40 |
| Skip button (sm, 36) | 54–90 | 10 below the status area |
| Back square's bottom to the progress bar | 92 → 108 | 16 (skip bottom to the bar 18) |
| Progress bar (4) and the count "3/6" (18, centred on the bar) | 108–112 | 4 |
| Bar's bottom to the title | 112 → 136 | 24 |
| Title (hero 24/32) | 136–168 | 32 |
| CTA band | 726–844 | 118 |
| Band top to the button's top | 726 → 742 | 16 |
| Button (lg) | 742–794 | 52 |
| Button's bottom to the screen's bottom | 794 → 844 | 50 (16 + the 34 inset) |
| Button's bottom to the home indicator's top | 794 → 831 | 37 |
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-mobile2.5.1 source). The container's padding is0 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 isCTAButtonsize="xlarge"(56). While the keyboard is up (the viewport shrinks by more than 30%) the band becomesisFull: side and bottom padding 0 andborder-radius: 0, so the button attaches to the keys;fixedAboveKeyboarddefaults 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
NavigationBariswindowInsetsPadding(...)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 ofmax(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
| Option | Header above / below the buttons | Footer: top / button to the screen's bottom / to the indicator | Chrome above the title | Band |
|---|---|---|---|---|
| Now | 8 / 16 | 16 / 50 / 37 | 92 | 118 |
| Recommended | 12 / 12 | 16 / 34 / 21 (bottom = max(inset, 16)) | 92 | 102 |
| Middle footer (inset + 8) | 12 / 12 | 16 / 42 / 29 | 92 | 110 |
| Header 16 / 16 | 16 / 16 (header row 64) | as recommended | 100 | 102 |
4.7 What changes
- Kit.
kit-chrome:FvkWizardHeader(the header row'spadding: '8px ' + rail + 'px 0'),FvkCtaBand(padding: '16px ' + rail + 'px ' + Math.max(16, inset) + 'px').kit-controls:FvkSheetFooterandFvkSheetBox(the footer's16 / 16 + insetbecomes16 / max(16, inset)). Boards:flat-v2-lib-chrome.jsx(thehof every CTA frame: 118, 148, 170, 204;LibChrAbove hvalues),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 for118,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 with16 + insetmust fail. - App.
bottom-cta-band.tsxandbottom-cta-band-visual.ts(16 sides, permanent hairline today) takepaddingBottom = 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 you | Recommendation | Why |
|---|---|---|---|
| 1 | Which 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. |
| 2 | Is 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. |
| 3 | Order and prominence | Kakao, 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. |
| 4 | Where 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. |
| 5 | When 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. |
| 6 | One 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. |
| 7 | What 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. |
| 8 | What 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
| Provider | Fill, symbol, label | Label text | Size, shape | Prominence rule | Source |
|---|---|---|---|---|---|
| Kakao | container #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 button | radius 12; label height at most one third of the container; the symbol may be left-aligned or centred; system font | do not leave the Kakao colour unspecified so another provider's button stands out (translated) | D (Kakao Developers design guide) |
| Naver | green 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, PNG | place the button where it is seen at once; use it on both the sign-in and the sign-up screens | D (Naver Login button guide, devguide 3.2) |
light #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 permitted | Google 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 size | D (Google branding guidelines) | |
| Apple | system black, white, or white with an outline; custom buttons only black or white, Apple's logo artwork only | only "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
| Provider | Native in Supabase Auth? | What to know |
|---|---|---|
| Kakao | Yes | Scopes 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. |
| Yes | Standard. | |
| Apple | Yes | Native 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. |
| Naver | No | A 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
| Option | What it is | For | Against |
|---|---|---|---|
| A. At sign-up, for everyone | Provider tap, consents, PASS or carrier verification, then profile | Every account is a verified person; the directive read literally | Naver 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 twice | Sign-up is two taps; the abuse the audit worries about is a first-RSVP problem; Naver and Apple both ask for delay | A member can browse and be invited before they are verified; some actions wait on the step |
| C. Provider-verified only | Take name, birth year, phone and CI from Kakao (CI consent item) and treat a Kakao sign-in as verified | No second step for Kakao users | Only 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.
| Service | Providers, in order | Layout (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 chevron | 350 × 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 page | not 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)
| # | Entry | Current spec | Best-practice reference | Drift | Fix |
|---|---|---|---|---|---|
| A1 | 1.1 Frame and safe areas | 390 × 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 + inset | The 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). | minor | Relabel 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. |
| A2 | 1.2 Rail and content width | 16 under 390, 20 to 599, 24 from 600; grids from the content width | Apple's own margin measured 16 up to 375 and 20 from 390 (P, review-1 lane A §4). | none | None. |
| A3 | 1.3 Grid and spacing ladder | Multiples of 4; ladder 4 to 40; line heights multiples of 2 | Seed spacing tokens x1 4, x2 8, x4 16, x6 24 (D), with half-steps 14 and 18 that this ladder does not use. | none | None. |
| A4 | 1.4 Type | Pretendard; 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 digits | HIG: 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). | none | Body 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. |
| A5 | 1.5 Heading spacing | Section head 32 above, 12 below; day header 24 and 12; step title 24 under the bar | 11 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. | none | None. |
| A6 | 1.6 Seams | Two hairline tiers (cell #E5E5E5, edge #D4D4D4); edge only while content scrolls under; at most one band per screen | HIG 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. | none | None. |
| A7 | 1.7 Colour (S2) | Rule 6: 6.5:1 at 14 and below. $error #B8332E for fills; success fill #00796A; inverse surface ink, dark #232824 | WCAG 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. |
| A8 | 1.9 Radii | 6 Tag, 8 thumb, 12 button and notice, 16 block and sheet, full | Kakao's button guide fixes radius 12 (D). | none | None. |
| A9 | 1.10 Icons | 24 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). | none | A glyph beside text now attaches through the line slot (Point 1). |
| A10 | 1.11 Targets | 48 icon button, 52 primary, 56 FAB, 44 floor by hit slop | WCAG 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). | none | None. |
| A11 | 1.12 Motion tokens | Durations 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). | minor | navSlide settles at about 311 ms (§2.5), 11 over the law. Say "about 300 ms" or retune the spring to settle by 300. |
| A12 | 1.13 Leading-marker rule | LM6: the marker's top sits 8 below the text-box top (17/24 and 15/22); LM8: it follows line 1's centre under scaling | Material 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%. | minor | LM6 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)
| # | Entry | Current spec | Best-practice reference | Drift | Fix |
|---|---|---|---|---|---|
| B1 | 2.1 Press physics | Six kinds (scale .92 to .98, dim .78 to .92), springs pressIn and pressOut; tabs dim only | Material 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. | none | None (the kit holds the table three times: §7 row 1). |
| B2 | 2.2 Hover and focus | Hover .85 over 100 ms; focus a 2 ink ring, offset 2, outside, instant; Input shows a 2 ink border; sticky chrome never covers the ring | WCAG 2.4.7, 2.4.11 (focus not obscured, AA) and 2.4.13 (2 px, 3:1 change of contrast, AAA) (D). | none | None (eight ring copies in the kit: §7 row 2). |
| B3 | 2.3 Disabled, soft, loading | Disabled .5, inert, aria-disabled; soft-disabled stays focusable and explains; loading keeps width and sets aria-busy | WCAG 1.4.3 exempts inactive components (D); the reason-line and soft-disabled forms follow review-1 lane B (Baymard, GOV.UK). | none | None. |
| B4 | 2.4 Text scaling | 85 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. |
| B5 | 2.5 Motion | 22 interactions; Reduce Motion replaces, never cuts; lists never animate; "never animate" list | WCAG 2.2.2 (D); the platform's own reduced-motion rules. | none | None. |
| B6 | 2.6 Accessibility semantics | One stop per row; state in aria-selected, aria-checked, aria-pressed, aria-expanded, aria-busy; composed names; status and alert; focus management | RN 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). | minor | Wording: "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. |
| B7 | 2.7 Platform | Edge-to-edge handled by the chrome; predictive back stays off; swipe-back; sheets; keyboard; haptics; native tabs not adopted; 600 dp verified | Android 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). | minor | Add 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). |
| B8 | 2.8 Skeleton and loading timing | Nothing under 300 ms; a skeleton from 300 ms, held at least 500 ms; determinate indicator past 10 s; shimmer rests after 5 s | Seed'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). | minor | The 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. |
| B9 | 2.9 Toast timing | 4 s result, 8 s with 되돌리기; Android takes the larger of that and the recommended timeout; pauses on touch and on screen-reader focus | Material 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). | none | None (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)
| # | Entry | Current spec | Best-practice reference | Drift | Fix |
|---|---|---|---|---|---|
| C1 | Header | 56 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 Buttons | Seed Top Navigation allows two or three icon buttons on a root page (D); HIG toolbars; targets per A10. | none | The hover tooltip is hand-drawn in FvkHeader (§7 row 15). |
| C2 | Tabs | Row 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-pressed | Material 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. | major | Point 1 (§2.5): the count goes in FvkLineSlot; FvkCountTag grows; aria-pressed per B6. |
| C3 | Tab bar | 56 + 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 tab | Material 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). | major | Point 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". |
| C4 | FAB | Extended pill 56, ball, bottom = tab-bar top + 16, collapses after 120 pt of downward scroll, label 15/22·600 | Seed: a Snackbar always sits above a FAB (D); the library's Toast does the same. Targets per A10. | none | The kit hard-codes 56 + FVK_HOME_INSET (kit-chrome:505): read the shared constant after C3 (§7 row 16). |
| C5 | CTA band | Padding 16 top, 16 + inset bottom; lg button 52; helper 8 above; danger helper = 16 alert glyph + 15/22·600 $errorText; Entry variant | TDS 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%. | major | Point 3 (§4.5): bottom max(inset, 16) (118 becomes 102); the danger helper through FvkHelperLine. |
| C6 | Sticky edges | A 1 px edge-tier line only while content scrolls under; on above 8, off at 0; none at rest | HIG layout: a scroll edge effect lifts controls above content (D). | none | None. |
| C7 | MediaBand | 240 including the status bar; scrim ink α0.35 to 0 over 100; 40 circles at ink α0.5 in 48 targets; white glyphs | P: 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. | none | None. |
| C8 | Live row | Live tag, 8, date and venue 13/20·500; min 24; padding 8 | P: the meta's paddingTop: 2 is exact at 100% only (−0.5 at 130%; at 200% the meta wraps). | minor | The Live tag and the meta's first line through FvkLineSlot; delete paddingTop: 2. |
| C9 | WizardShell header, progress | Header 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 bar | P: 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). | major | Point 3 (§4.5): 12 and 12 by adding 8 of top padding to the 56 row; the library says "12 clear". |
| C10 | Search takeover | Header row 56: Search field 40 and 취소 sm tonal; recents; results; staged CTA band | Input text is 16/24, the §1.4 reading floor; targets per A10; the keyboard rules of §2.7. | none | None. |
| C11 | Header icon badge | Dot 6 or count 16 with a 1.5 ring, overhang 2; dot means pending, count means a number; never both | Material 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). | none | Add the clearance rule of C3 (ring top at least 8 below the host's top edge). |
| C12 | Coachmark | Inverse 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. | none | Dark 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)
| # | Entry | Current spec | Best-practice reference | Drift | Fix |
|---|---|---|---|---|---|
| O1 | The overlay rule | Seven cases S-1 to S-7; one overlay at a time; backdrop ink α0.4; system back closes the topmost | HIG 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). | none | None. |
| O2 | Dialog | Centred, 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 dismiss | Material 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. | minor | State the cap: Dialog width = min(content width, 560). Code: 640 becomes 560 for placement="center". Kit: FvkDialogBox width through fvkOverlayWidth('dialog'). |
| O3 | MenuSheet | A 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 wide | Seed Menu Sheet: groups only with three or more items, at most three groups, Neutral and Critical tones (D). | none | None (the row is a Cell: §7 row 12). |
| O4 | Sheet | Top radius 16, pad 20, title row 48 with ×, footer = CTA band geometry, 88% cap, no grabber | Material 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. |
| O5 | Sheet, value picker | Radio Cells 48; chosen 600 with a 20 check; counts in a figure column; no footer, a tap selects and closes | Radio-group semantics (§2.6); Seed Bottom Sheet (D). | none | None. |
| O6 | Edit Sheet | Title and ×; the control; one 저장, soft-disabled until the value changes; guard Dialog on dirty dismiss | HIG sheets: pair a Done with a Cancel or Back (D); the × is the Cancel. | none | None. |
| O7 | FilterSheet | Groups of Choices 36 tall; pair 초기화 + "결과 n개 보기"; trigger Choice with the active count | Choice 36 with a 44 hit slop meets A10. | none | None. |
| O8 | Document page | A pushed page; version caption; article headings 17/24·600; paragraphs 16/24 | HIG sheets: lengthy or prolonged flows belong in a full-screen page (D). | none | None. |
| O9 | Toast | Ink surface r12, min 48, pad 12 16, text 16/24 white, action 15/22·600 in ball plus ×; no glyph; 4 s or 8 s | Material 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. | minor | Dark: A7. The action's hit box and the × share one row; text actions unify (§7 row 7). |
| O10 | EmptyState | 80 disc, 32 glyph, title 17/24·600, body 16/24, one md Button; seven variants | HIG alerts: a title says what happened, never "Error" (D). | none | None. |
| O11 | Skeleton | Shapes at the row pitches, pulse .6 to .9 over 800 ms, static .75 under Reduce Motion, aria-busy | WCAG 2.2.2 (D); Seed loading pattern (D); timing per B8. | none | None (five bar copies in the kit: §7 row 4). |
| O12 | Offline strip | 350 × 48 neutral Notice, glyph, title, 다시 시도 text action; pinned under the header; solid over a MediaBand | HIG alerts: for a connection problem prefer a quiet indicator the person can open (Mail's example) (D). | none | None. |
| O13 | CountdownNotice | Warning Notice with a clock glyph, 입금 대기 중 and a tabular countdown; at zero a danger Notice with 다시 신청; announces at 5:00 and 1:00 | WCAG 2.2.1: an essential, server-side limit (D); 4.1.3 (D). | none | None (the kit re-implements the Notice: §7 row 11). |
| O14 | Spinner | The platform indicator, 20, only inside a control; never alone on a list or screen | Seed loading pattern: a Progress Circle at the start of a user-triggered action, none under 1 s (D). | none | None. |
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 redoes | Consolidate onto | Delete or change |
|---|---|---|---|---|
| 1 | FVK_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.jsx | The other two tables and two appliers; 23 call sites change a name |
| 2 | Focus 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 times | fvkRing(pal, state, radius, inset) in the core kit (the chrome version, which already handles the inset case) | Seven copies; FvkPhotoTiles passes inset |
| 3 | fvkChInverse (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 #232824 | fvkInverse(pal) reading a palette key inverse (value per audit row A7) | Both helpers |
| 4 | Skeleton 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 Motion | One FvkSkBar({ w, h, r }) (data-g="skel-bar", aria-hidden, static .75) | Four private bar closures; FvkCSk becomes an alias |
| 5 | The 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 |
| 6 | The 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 + inset | FvkBandHelper and fvkBandPad(inset, rail); FvkSheetFooter renders the same body as the band | The second helper and the second padding expression |
| 7 | Text 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 48 | One FvkTextAction({ role, tone, onInverse }) with the 44 hit slop from fvkCtlHit | Eight hand-built boxes and their margins |
| 8 | A 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), :3837 | Ten sites compute (48 − glyph) / 2 by hand (−4, −12, −14) | FvkIconButton prop bleed="end" or "start" that applies the offset | Ten margins |
| 9 | Group titles (13/18·600 secondary, 8 below): FvkMenuSheetBox (kit-controls:2416), FvkValueSheet (:2454), FvkFilterSheet (:2578) | One element three times | FvkGroupTitle | Three inline FvkT blocks |
| 10 | The 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 bar | FvkBadged({ size, badge, children }) owning the host and the corner anchor (data-adorn="corner"); the specimen renders FvkTabBar | Three host spans; the specimen's own layout |
7.2 A component that redoes another component
| # | Where | What it redoes | Consolidate onto | Delete or change |
|---|---|---|---|---|
| 11 | FvkCountdownNotice (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 new | FvkNotice with a trailing prop and tone warning or danger | The private box, colours, glyph offset and action box |
| 12 | fvkCtlMenuRow (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 |
| 13 | FvkWizardHeader (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"; fvkCtlHit | squareStyle, the wrapper, the height trick. The 40 square and 40 pill are one-off sizes: the owner may prefer the md (44) geometry |
| 14 | Hand-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 size | FvkLineSlot (§2.5) | Ten constants (rows 5 and 11 above overlap) |
| 15 | The 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 fill | FvkTooltip({ at, children }) | The inline block; the position maths moves with it |
| 16 | The 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-auth | The library §1.1 stack (tab bar, band, FAB, Toast pin) re-typed as numbers. Point 2 and 3 would have to edit 17 places | fvkChrome(inset) in the core kit: { tabBar, band(helperLines), fabBottom(placed), toastPin(...) } | The literals; LibChrAbove takes the bar's own height |
| 17 | FvkDialogBox width (kit-controls:2229), FvkSheetBox width (:2118), FvkToast pin (:2893-2900) | Overlay widths and pins computed per component | fvkOverlayWidth(kind) (Dialog 560, Sheet 640, per O2 and O4) and fvkChrome | The per-component expressions |
| 18 | FvkWizardHeader 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
| # | Where | What it redoes | Consolidate onto | Delete or change |
|---|---|---|---|---|
| 19 | FndS (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 |
| 20 | LibBehSpoken (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_BOARD | FvkSpec / FVK_BOARD tokens | The 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)
- Add
fvkLineH(role, scale, cls)(promotefvkCLineH) andFvkLineSlot({ role, cls, children }); documentdata-adorn="box" | "corner"anddata-linein the header comment. - Add
FVK_TABBAR = 64andfvkChrome(inset)returning{ tabBar, band(helperLines), fabBottom(placed), toastPin(layers) }. - Add
FVK_PRESSandfvkFx(state, kind)(six kinds plustab),fvkRing(pal, state, radius, inset),fvkInverse(pal), and a palette keyinverse. - Colours per A7 once decided: light
dangerfill for 14 px and below, success solid, darkinverse. FvkFrametakesstage,phoneandsliceprops.
8.2 flat-v2-kit-chrome.jsx
FvkTabs: the count Tag inFvkLineSlot; deletemarginTop: 1; the 12 spacer derives from the line.FvkTabBar:height: FVK_TABBAR + inset, itemheight: 64,paddingTop: 12; host throughFvkBadged.FvkFab:bottomfromfvkChrome.FvkCtaBand: padding16 rail max(16, inset)throughfvkBandPad; helper throughFvkBandHelper; the Entry links row throughFvkTextAction; deletepaddingTop: 3.FvkLiveRow: Live tag and the meta's first line in the slot; deletepaddingTop: 2.FvkWizardHeader: the header rowpadding: '8px rail 0'(border-box, stays 56); back square and pill ontoFvkIconButtonandFvkButton; segments' hit zone throughfvkCtlHit.FvkHeader: tooltip ontoFvkTooltip; badge host ontoFvkBadged.FvkCoachmark: action ontoFvkTextAction.- Delete
FVK_CH_PRESS,fvkChFx,fvkChRing,fvkChInverse.
8.3 flat-v2-kit-content.jsx
FvkCountTag: grow with the numeral (box = ceil(20 × min(scale, 1.3) / 4) × 4; the 16 badge form is fixed).FvkCRow: the unread dot through the slot (deletetop: 8and themarkerLeftarithmetic);FvkCell: error line ontoFvkHelperLine, consent tail ontoFvkTextAction, trailing chevron or pencil of a two-line Cell markeddata-adorn="box".FvkNotice:trailingprop, action ontoFvkTextAction, glyph through the slot;FvkHeroandFvkCaptionRowuse the slot instead of privateheight: lineHwrappers.FvkCBlockFrame: the new chevron carriesdata-adorn="box".FvkCSkbecomes an alias ofFvkSkBar; deleteFVK_C_PRESSandfvkCFx.
8.4 flat-v2-kit-controls.jsx
FvkField,FvkPickerCell,FvkProviderButton,FvkSheetFooter: error line ontoFvkHelperLine.FvkSheetBox: footer throughfvkBandPad; width throughfvkOverlayWidth; the × throughbleed="end".FvkSheetFooter: sharesFvkBandHelper.FvkDialogBox: width cap 560.FvkToast: action ontoFvkTextAction; inverse per A7; pin fromfvkChrome.FvkCountdownNoticebecomes aFvkNotice;fvkCtlMenuRowbecomesFvkCell.- Skeleton bars in
FvkFieldSkeleton,FvkDocPage,FvkSkeleton,FvkPostRowontoFvkSkBar; group titles ontoFvkGroupTitle. FvkIconButton:bleedandvariant="square"props,FvkBadged.FvkSwitch,FvkSlider,FvkStepper,FvkCheckBox,FvkPhotoTiles: rings ontofvkRing.- Delete
FVK_CTL_PRESS,fvkCtlState,fvkCtlInverse.
8.5 Boards
flat-v2.jsx:FndSontoFvkFrame stage; the frame ruler (tab bar 64,content 646) and the "iPhone 12 to 14 class" label.flat-v2-lib-chrome.jsx: dropLibChrSandLibChrP;LibChrAbovemeasures its bar (118, 148, 170, 204 go);LibChrBadgeCellrenders the realFvkTabBar; 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.flat-v2-lib-overlays.jsx: dropLibOvlF,LibOvlSlice,LibOvlP; Toast pins fromfvkChrome(lines 234 and 1173); the dark inverse caption per A7.flat-v2-lib-behaviour.jsx: dropLibBehSandLibBehP;LibBehSpokenontoFVK_BOARDtokens.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 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.4 add "an adornment attaches through the line slot" and the count Tag growth; §2.6
aria-pressedwording; §2.7 the dated predictive-back line; §2.8 the start threshold (B8). - §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)
adorn(§2.6),tabbar(§3.5) andcta(§4.7), each with a known-positive canary that must fail when its fixture is broken.
8.8 App (packages/ui, packages/app)
LineSlotbesideGlyphSlot;Badged(the §3.4 offsets) replacingCornerCountBadge;UnderlineNavtakes acount;BottomCtaBandandbottom-cta-band-visual.tstakepaddingBottom = Math.max(insets.bottom, 16);WizardShellheader numbers; tab-bar constants; the colour tokens of A7;ModalPanelcentre placement capped at 560.- Sign-up: the
signInWithOAuthprovider list, Sign in with Apple (native), the 본인인증 step andlinkIdentityflow, 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/floorsplit (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
responseprofile (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-trimsupport dates (MDN browser-compat-data, read 2026-10-11); the TDSBottomCTAsource is@toss/tds-mobile2.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-appleunder 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.jsonandtext-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.