Skip to content

Design drift ledger ​

The record of every place the implementation knowingly differs from the Claude Design MCP project. Governed by ADR-0015.

How to use this — 30 seconds, in the PR that diverges

Add one row when you make the change, not later. That is the whole point: reconstructing why a visual decision was made, months later, from a diff, is archaeology. Writing it down while the reason is in your head is free.

npm run check:design fails any diff touching packages/tokens/** or apps/mobile/src/ui/** without a row added in the same range. If your change genuinely has no visual effect (a refactor, a rename), say exactly that in the row — an explicit "no visual change" is a valid entry and takes less time than arguing with the gate.

Kinds of entry ​

KindMeaningNeeds sync back to the design project?
divergenceImplementation now differs from the design, deliberatelyYes — push at the next develop → uat
correctionImplementation was changed to match the design (it had drifted)No — the design was already right
aheadImplemented something the design has no opinion on yet (new component/variant)Yes — the design needs to gain it
noneTouched the systemic surface with no visual effectNo

Reconciliation ​

Open rows are worked at the develop → uat promotion (see the promotion runbook). Each is either pushed to the design project via DesignSync and marked synced, or explicitly deferred with a reason written on the row. A row is never silently dropped.

Ledger ​

Newest first. Status: 🔴 open · 🟢 synced · ⚪ n/a (correction / no visual change) · 🟡 deferred (with reason).

DateKindStatusSurfaceWhat & why
2026-09-20correction🟢 closedSYSTEMIC · apps/mobile/src/ui/brand-glyphs.ts (BRAND_GLYPH_LAYERS, BRAND_KNOCKOUT, NEW) · apps/mobile/src/ui/BrandIcon.tsxThe WhatsApp mark's handset was not white — it was whatever was behind the icon, and the owner reported it on the biodata reader's growth CTA. Simple Icons draws WhatsApp as ONE path whose handset is a hole: the fill rule cuts it out. On a plain card that is indistinguishable from white and the mark looks right; on GrowthCta's tinted LinearGradient wash the gradient shows straight through the handset. ⚠ DESIGN PULLED FRESH BEFORE ANY CODE, per ADR-0015 — prototype/consumer/icons.js, fetched 2026-09-20, not read from the local mirror. It draws the mark as TWO paths (a bubble in currentColor plus a handset painted #fff) and states the rule in its own header: "any knockout inside a filled tile is #fff in both themes." Its header also names our file by path, so the design already expects brand-glyphs.ts to carry this shape. ⚠ IMPLEMENTED ADDITIVELY. BRAND_GLYPHS is untouched and every other brand still renders the single path it rendered before; BrandIcon prefers a layered mark only where one exists. That is what let a systemic primitive change without moving eight glyphs nobody asked it to move — and it is why this row is a correction rather than a divergence. ⚠ THE PATH DATA IS THE DESIGN'S OWN, not a redraw: its bubble is a simplified circle-and-tail rather than Simple Icons' outline, so the silhouette shifts very slightly. That is the systemic surface being design-first, which is the whole of ADR-0015. ⚠ BRAND_KNOCKOUT IS WHITE IN BOTH THEMES AND IS NOT A TOKEN, deliberately: the handset is part of the trademark, so theming it would make the logo wrong exactly as a theme-shifted green would. Same sanctioned lint exception BRAND_COLORS already carries, and it holds even when a caller overrides color — a caller asking for a different colour is asking for the BODY to change. Sync target: none, the implementation now matches the design. ⚠ Native 🔴 NOT VERIFIED — this is @/ui, so Android, iOS and the Web PWA all need the device pass. 9 jest cases, including the other direction (every non-layered brand asserted to stay a single path).
2026-09-20divergence🔴 openSYSTEMIC · facebook, linkedin, youtube, telegram in brand-glyphs.tsThe same knockout defect as WhatsApp above, in four more marks, deliberately NOT fixed in that change. All four carry an explicit #fff knockout in the design and a cut-out hole in ours, so on a dark surface their knockout shows dark instead of white — SocialTab renders all eight at tone="brand". ⚠ The reason for holding is not effort, it is BLAST RADIUS: the design draws three of them with a different SILHOUETTE than Simple Icons (a circled f rather than a rounded square, for one), so adopting its paths visibly changes merchant screens that were already signed off. That is a wider design change than the one that was asked for and it needs its own device pass. 🔎 Recorded rather than fixed quietly, because the opposite failure is the one this repo keeps having: a fix to a primitive that silently moves everything the primitive touches. Sync target: none — the design is right; we are behind on four marks.
2026-09-09correction🟢 closedapps/mobile/src/ui/Icon.tsx (quote, NEW) · the biodata reader's top blockThe in-app marriage-profile reader opened with NO NAME on it, and the owner found it. It rendered the owner band and jumped straight to the work section: no headline, no age · height · city line, no life chip, no lead chips, and none of the paragraph the family wrote. ⚠⚠ The screen's own code justified the omission with a comment that was false: it excluded about from its section list because "about is consumed by the identity lines", and the identity lines were never built. The design's base.sections also starts at work, so the LIST was right and the claim about where the rest went was not. ⚠ And no contract row named the block, which is why every gate stayed green — check:design-parity checks that enumerated rows carry verdicts and is blind to an un-enumerated one. That is the fourth rule's own failure mode on the screen it was written about. Values extracted with design:spec from a fresh pull that differed from the five-day-old local copy, so reading the cache would have implemented a superseded round. The quote glyph is the one systemic change (Lucide already carries it, so no asset); QRS-1226's missing refresh is a different screen and stays open. Sync target: none, the implementation now matches the design. Native ⚪ n/a for the glyph; the block needs a device pass with the rest of the reader. QRS-1235
2026-09-09divergence🔴 openThe intro label omits the family's LANGUAGEThe design reads "Written by the family in Marathi"; what ships is "Written by the family". ⚠ It is a projection gap, not a copy choice: content_language lives on biodata.subjects and get_my_biodata_overview projects only id, first_name and owner_is_subject, so the language is not readable on this screen. Widening it is a backend change. ⚠⚠ Asserting a language I cannot read would be worse than omitting it, because the label's whole purpose is telling a reader in another language what they are looking at — so a wrong one actively misleads rather than merely lacking. Sync target: none; this closes when the projection gains the column. QRS-1235
2026-09-09correction🟢 closedapps/mobile/src/ui/Avatar.tsx (tone="brand", NEW) · ScreenHeader (titleAlign, NEW) · TextField (mono, NEW)Three additive @/ui capabilities the approved consumer designs specify and the primitives could not express. Raised by the OWNER off a screenshot pair, which is the second time this screen's fidelity was caught by a person rather than a gate. Values extracted with npm run design:spec from Account.dc.html rather than eyeballed. (1) Avatar tone="brand" — the design fills both the hub card's 64px avatar and the details view's 84px one with background:var(--brand-gradient) and color:var(--accent-contrast); both rendered a flat accent-soft tint with content-secondary initials, recorded as a deviation (QRS-1226) rather than closed because nothing here could paint a gradient. expo-linear-gradient was already a dependency, so this needed no package and no size callout, and it reuses the SAME two stops and angle as GradientText and the Wordmark's own "QR" so the monogram treatment is identical everywhere. ⚠ The gradient sits BEHIND the photo rather than instead of it, so a transparent PNG lands on brand and the box never flashes empty while a remote image loads. (2) ScreenHeader titleAlign — the two approved design systems genuinely disagree and neither is wrong: the merchant console CENTRES a pushed-route title (the decision recorded as QRS-231, which replaced a left-aligned 22px one), while the consumer artboards left-align it (flex:1; 17px; 800; letter-spacing:-.02em; ellipsis). ⚠ start also applies the tracking, and the two are ONE option on purpose: the design that asks for the alignment is the design that asks for -.02em, and separate props invite a call site that sets one and forgets the other — a half-applied design nobody would notice. (3) TextField mono — the designs specify the face PER FIELD, not per screen: the details view declares var(--font-mono) on Mobile number and Email and var(--font-ui) on Full name, in the same fields array. There was no way to say that, so a screen wanting it had to abandon the primitive and re-implement the 52dp pill, which is exactly the cross-screen-consistency failure check:parity exists to catch. ⚠⚠ ALL THREE DEFAULT TO THE EXISTING BEHAVIOUR, so not one current caller moves — that is what makes a change to the SYSTEMIC surface safe here without both native builds, which remain the gate for anything that alters an existing value. What was deliberately NOT changed: the field's radius (18 vs rounded-2xl 16), horizontal padding (16 vs 14) and value size (14.5 vs 15). Those are not additive — they would move every field in both tiers — and the merchant console's own artboards specify the current numbers, so unifying them is a real decision with a blast radius, not a fidelity fix. Left open deliberately rather than silently split. Sync target: the design-system project should learn tone="brand", the two header alignments and the per-field face, none of which its 43 components record. Native ⚪ n/a for the defaults; the three new values need a device pass with the rest of this screen (QRS-1231).
2026-09-09correction🟢 closedAccount's header title, on all eight viewsIt said "Account" on every sub-view, and the owner caught it on a screenshot. The design carries an explicit map (TITLES = { hub: 'Account', details: 'Personal details', ... }) and the Shell passed the hub's title unconditionally, so a pushed view announced itself as the screen it had been reached FROM. ⚠ On a route with a back chevron that is worse than a generic title: it tells the person the navigation did not happen. Every one of the eight keys already existed — they are the hub's own row labels and they match the design word for word, details included — so nothing was added and the screen was simply not asking. Fixed on all THREE Shell call sites, loading and error included: a wrong title on a failure state is the same defect.
2026-09-09correction🟢 closedAccount > details: the Mobile number's VALUEShown as +919273373367, an unbroken thirteen-digit run; the design prints +91 98200 41122. This is the one field on the screen whose entire job is letting somebody confirm at a glance that it is their own number, and counting digits is not a glance. formatE164ForDisplay lands in @qrsetu/domain/auth/phone.ts, beside toE164, with 6 tests. ⚠⚠ IT IS A THIRD SPELLING OF A NUMBER THAT ALREADY HAD TWO, and that is only admissible because it never leaves the screen. That module exists because GoTrue stores 919820041122 while every QRSETU table stores +919820041122, and a naive join between them matches nothing (QRS-936). A spaced string is a different value again to GoTrue, to communication_messages and to Meta's Cloud API, so it is DISPLAY ONLY — pinned by a test asserting toE164 recovers the canonical form, and by another asserting this view sends only the name. ⚠ An unrecognised country is returned UNGROUPED rather than guessed at: grouping conventions differ, and inventing one prints a number in a shape its owner does not recognise, which is worse than the run this fixes.
2026-09-09divergence🟡 partly closedAccount > details: the field NOTES (open) and the Delete row's colour (CLOSED same day)Two visible differences from the design that are DELIBERATE, flagged here because the owner's screenshot shows both and neither is a mistake. (1) The design's two field notes are marketplace copy — "Shops call this number about an order." and "Receipts and order copies go here." The marketplace is off at launch and off means ABSENT (D-a), so a launch consumer has no shops, no orders and no receipts; printing either sentence describes a product they cannot reach. What ships instead states why each field is read-only, which is the fact a person on this screen actually needs. A test asserts the design's phrases stay absent. (2) The design paints "Delete my account" in var(--danger); it ships in content-tertiary with a "Not ready yet" suffix. A red full-width row is unmistakably a live destructive control, and this one is deliberately unpressable until its disclosure is settled (QRS-1228, launch-blocking) — so looking live is the one thing it must not do. ⚠ The GEOMETRY is the design's (marginTop 10, height 48, 13.5px/700); only the colour and the suffix diverge, and both change together the moment QRS-1228 is answered. ✅ THE DELETE HALF CLOSED THE SAME DAY, by owner decision: the control is now live in var(--danger) with the artboard's geometry. Its confirm body is still NOT the design's — that one names only marketplace objects and never mentions the number — so what ships states the two facts that matter and a test asserts no marketplace noun survives. ⚠ The field NOTES half stays OPEN by owner decision ("skip field notes"): the launch-correct copy stands. ✅ And the field GEOMETRY row is resolved rather than deferred: radius 18 / padding 16 / value 14.5 are now passed per call site. ⚠⚠ The reason given for deferring it was WRONG and is corrected here — I claimed the merchant artboards specify TextField's current numbers. Measured across four artboards, the designs do not agree on one field at all (Account 52/16/18 at 14.5, Meetings 52/16 at 15, WifiQR 46/13 at 14, Biodata radii of 14/15/16), and the current default matches NONE of them. Sync target: the design needs launch-correct copy for both field notes and for the delete confirm body.
2026-09-05none⚪ n/aapps/mobile/src/ui/BrandQr.tsx (NEW) · src/ui/Icon (12 glyphs added)The consumer prototype's brand QR tile, transcribed from prototype/consumer/brand-qr.js (fetched live 2026-09-05), and the first @/ui primitive of P3. Level-H encoding through the existing createQrMatrix (the design's own encoder exists only because the prototype has no package to import; its header says so), the typographic QR mark in Akaya on the brand gradient via GradientText, a light tile in BOTH themes from the navy primitive ramp, bare keeping the tile and dropping the caption, the scheme encoded and not displayed, and a too-long payload drawing NO code. ⚠ NOT a restyle of QrCode: that is the design SYSTEM's QRDisplay (13px padding, the app icon as mark, radius.md); this is the consumer prototype's own module (10px, type mark, radius.2xl). Two designs, two components, one encoder. The twelve glyphs (message · video · scan · ticket · history · contact · wifi · creditCard · mapPin · image · layout · download) are the design's own icons.js ids mapped onto Lucide, as every earlier glyph row was; the kebab→camel mapping lives in tiers/consumer/designIcons.ts, never as duplicate keys.
2026-08-19correction🟢 closedLanding hero h1 revealThe typewriter is the design's, restored by owner decision the same day: "let's keep the typewriter which is explicitly kept to make landing feel alive." This row previously read divergence/open, because I had replaced .qs-type's clip-path reveal with an opacity pass to protect LCP - the h1 is the largest contentful element and the design's third line does not finish until 1.38s + 0.66s = 2.04s. The owner declined that trade and the effect is now literal: steps(10)/(16)/(15) at delays 0.15s/0.62s/1.38s, plus the blinking caret on the last line, verified in a browser mid-animation (clip-path: inset(0 70% 0 0) on line 1 while lines 2-3 are still fully clipped). ⚠ What makes it acceptable on an SEO surface, and it is a property rather than an opinion: clip-path is a PAINT-time operation. The words are in the DOM in reading order with zero aria-hidden, so a crawler and a screen reader get the whole h1 immediately - an effect built by withholding TEXT (JS typing, per-character nodes, a zero-width container) would not have that property and would not have been acceptable. ⚠ And the measured LCP element is the descriptive PARAGRAPH, not the h1, so the animation does not gate LCP at all; the escape hatch if that ever changes is to COMPRESS the timeline, never to delete the effect. Sync target: none - the implementation now matches the design. Native ⚪ n/a - DOM-only surface. QRS-763
2026-08-19divergence🔴 openLanding hero CTA destinations + the reserved right-hand columnThe CTAs point at #create and #discover (the design's own section ids) instead of ../onboarding/Onboarding.dc.html and ../marketplace/Discover.dc.html, because those become /app/... and /marketplace here and neither is mounted yet - and a 404 on the primary conversion path is the one outcome worse than an imperfect destination. Resolved by ADR-0028: the product app mounts once at /app and the CTA moves there in one line. The hero's right-hand column (a live Setu Card mock) is reserved and empty: its content must come from the same block vocabulary the real card renders, so hand-drawing a second card here is exactly the drift ADR-0019's one-renderer rule exists to prevent. Native ⚪ n/a. QRS-763
2026-08-19correction🟢 closedapps/web type faces (the public Setu Card shipped with NO webfont at all)fontFamily.ui was 'Baloo2_400Regular', an expo-google-fonts identifier no browser has, and apps/web loaded no @font-face: measured document.fonts.size === 0 on the live route. So the card - our primary SEO surface - rendered in the browser default while every gate stayed green, because a missing webfont reads as a styling opinion rather than a broken build. ⚠ The font NAME is per-platform even though the FACE is shared, which is the mistake underneath this: RN has no weight synthesis so each weight must be its own registered family, and collapsing that onto the DOM is what produced the bug. webFonts is now the DOM half, apps/web's own Tailwind config maps font-ui to var(--font-ui), and the 19 font-ui-<weight> call sites became font-ui font-<weight> so the weight comes from a real font-weight. Verified in a browser: Baloo 2 loads on the variable wght 400..800 axis and the h1 computes to it at 700. Sync target: none - the implementation now matches what the design always specified. Native ⚪ n/a - apps/mobile is untouched by construction (the override is in apps/web's config, deliberately, because both native builds are the gate for a shared-preset change and neither is reachable from this machine). QRS-755
2026-08-19divergence🔴 openapps/web/src/ui (Button, TextField, cn) - the DOM primitive layer the two READMEs already cited and that did not existFirst DOM primitives on Stack 1, so the ADR-0011 component-parity checklist finally has something to compare. Copied from apps/mobile/src/ui by SHAPE rather than by code: primary/secondary/ghost plus danger as an independent MODIFIER (two axes, not four variants), and `FieldState = idle
2026-08-15divergence⚪ n/aHomeHeader (QR setu wordmark replaces the business avatar)The design draws a business-initials block at the head of the row; we draw the WORDMARK. Owner decision, and the reasoning is the record worth keeping: profile and account are already reachable from the right-hand end of the same row, so the left avatar was a second door to one destination, and the highest-value position on the screen was spent on a duplicate rather than on the brand. The workspace name and category stay, and the whole block stays tappable to /profile, so nothing became harder to reach - which is what makes this a substitution rather than a removal. ⚠ Note this cuts the opposite way from QRS-228, which removed a redundant profile icon and kept the chip; same principle (one control per destination), applied to the other half of the pair now that the right side carries the owner avatar. Sync target: none - raise at the next design round so the design can decide whether branding belongs in-screen or only on the splash. Native ⏳. QRS-676
2026-08-15divergence⚪ n/aTab bar (one step larger than console-kit)Glyph 24 to 26, label 11 to 12, item minWidth 50 to 44, bar inset 20 to 12. console-kit's TabBar is the source of truth and we matched it exactly; the owner then tested on a handset and reported the icons as too small and the gaps between them as too large - at the same time. That combination is what makes it a real finding rather than a preference: it says the marks are under-scaled while the container is over-padded, so the fix moves width OUT of each item (minWidth and the bar inset) and INTO the glyph. ⚠ A 390px design preview cannot reproduce it - a browser at 100% zoom is not a 5.5-inch screen at arm's length, and this repo's own gates are blind here by construction (jest renders no pixels; Playwright drives the web export). Sync target: the design, because if the bar is under-scaled for us it is under-scaled for every screen that uses the kit - this is shared chrome, not a Home decision. Native ⏳ pending the owner's next device pass. QRS-677
2026-08-14divergence⚪ n/aHomeHeader (the reminders clock)FOUR controls where the design has three, by owner decision, and it is a divergence rather than a miss. Home.dc.html's header is workspace chip · bell · owner avatar. I removed our reminders clock earlier the same day to match it, and the owner put it back. Their reasoning beats mine and is worth recording rather than just obeying: reminders is a SHIPPED, backed feature for which the design has never drawn a screen (VENDOR-PARITY.md §5.4), so the design's header cannot be evidence about where its entry point belongs — it was never asked to account for the feature at all. Deleting the one control that reaches it made a working feature harder to find in order to satisfy a comparison the design could not adjudicate. ⚠ The generalisable trap: 'the design does not have X' is only an argument when the design KNOWS about X. For anything the design has not been shown, absence is silence, not a decision. Sync target: none — this resolves when reminders gets a designed screen, at which point the header question can be asked properly. Native ⏳, and the panel below remains the larger entry point, which is why the divergence is small. QRS-663
2026-08-14correction⚪ n/aDashboardHome (names, KPI strip, Quick tools)Four drifts the product owner found by putting the design and the build side by side, after two passes of mine had missed them. (1) The KPI strip was a different component entirely — the design renders days-to-festival · listings available · due on collection · unread chats from the real order book, and Home rendered revenue · orders · QR scans · an average RATING from the stub, with icons, +18% deltas, a heading and a Sample-data chip. dashboardKpis did not exist in @qrsetu/domain at all. The rating is a figure the product can never know (no ratings table, QRS-576). It survived because both are four numbers near the top of Home, which is what a reading that stops at LAYOUT sees. (2) The header lacked the owner-initials avatar. (3) The greeting read "Good evening, Chai & Charcha" — a merchant addressed by their shop's name — with an emoji the design lacks and four time-of-day sublines where the design has one. (4) Quick tools showed 3 tiles against the design's 7, because two entries were withheld while their screens were unbuilt and nothing re-checked once the screens landed. ⚠ AND THE NAMES WERE STILL STUB VALUES AFTER MY FIRST FIX: I corrected greets the business instead of the person by adding a second hardcoded name to the stub, which is the same defect with better copy. Home now reads get_my_context — the workspace's own display name, the signed-in user's, and the card slug — all of which were already wired. Sync target: none. ⚠ The KPI strip's LAYOUT is unproven on every surface: it needs a workspace the web preview does not have (QRS-611). Native ⏳. QRS-667
2026-08-14correction⚪ n/aDashboardHome · ConsoleTabBar · QuickActions · WidgetBody (round 22, RE-PULLED)A correction pass driven by the product owner, against a fresh pull rather than against memory — and the largest row here is a divergence of OURS being withdrawn. ⚠ THE HEADLINE: widgets were being HIDDEN that the design renders. vendor-core.widgetIsEmpty hides only an empty rows rail, an all-zero bars chart and a season-less ring; we had added seven more refusals, so a newly onboarded merchant — the only kind that exists on launch day — opened a nearly blank dashboard. Withdrawn; stats, chips and spark now print their zeroes, and FIRST RUN is the design's actual answer to an empty workspace (QRS-662). Also corrected, each of them ours rather than the design's: the header and both banners now sit OUTSIDE the scroll container as the design has them (it matters most for the offline strip); the KPI strip moved back ABOVE the attention rail; the season strip moved from the scroll body into chrome; per-section insets replaced one container inset, so rails bleed to the edge and read as swipeable; the spark renders an SVG polyline rather than fourteen small bars (a different chart type saying a different thing); the season ring is a real SVG arc rather than a full untouched track, on the grounds that RN has no conic-gradient — true, and the wrong conclusion, since react-native-svg was already bundled; the slot panel is dashed rather than a solid info row, because dashes read as reserved space and a solid card reads as one that failed to load; quick-action tiles lost their per-tile card and gained the design's 44px pill circle; the stats partition picks its lead deterministically (flagged, else first) instead of reading s.accent alone, which left several cards with no lead number at all; and the first-run number badges are uniformly accent-soft, because greying steps 2 and 3 reads as disabled rather than as later. Tab bar: the design's own paddings, a 56px centre span, and the Chats unread badge, which was missing entirely and is the only thing on that bar carrying information. ⚠ AHEAD OF THE DESIGN in one place, and it is deliberate: reserved is structurally 0 in the stock bar, because nothing in the v2 schema records a reservation (QRS-661) — the previous behaviour was to hide the widget entirely, which is worse. Sync target: none. ⚠ Native is the gate and it is OUTSTANDING on every item above — this touches src/ui/** (a new OfflineBanner primitive, the tab bar) and the web driver cannot reach this screen's data (QRS-611). Nothing in this row is visually verified on any surface. QRS-662 · QRS-663 · QRS-664
2026-08-14correction⚪ n/aDashboardHome (the widget system, 18 widgets across 5 groups)The implementation catching up to round 22's Home, which resolves a widget list from the domain and lays out whatever comes back. Faithful: collapsible groups that keep their summary line when shut, the 2-up grid with span, row-layout widgets as horizontal rails with the count in the header, per-kind skeletons ("a placeholder is never rounder than the surface it stands in for"), the failed-and-retry state that says the rest is up to date, the severity-sorted attention rail with no auto-advance timer (the design removed one and recorded why: "rotation moved the card the merchant was reaching for"), the dismissible season strip as chrome rather than a widget, and first-run steps in place of zeroes. Three groups from the design are deliberately ABSENT — bookings, people, growth — because every widget in them is backed by a table that does not exist and the design's own entries carry href: null; shipping them would put three permanently empty groups on the first screen a merchant opens. ⚠ AHEAD OF THE DESIGN in one place, and it is a refusal rather than an addition: stock_split and season_progress do not render at all while the reserved/sold split is unprojected (QRS-661), where the design assumes the data. Zero-filling would draw a bar showing every listing available and a ring reading 0% committed on a stall that has taken thirty advances. Sync target: none. ⚠ Native is the gate and it is outstanding, and unusually the WEB surface is too: the driver cannot reach this screen's data (QRS-611), so nothing here is visually verified anywhere. QRS-660
2026-08-14correction⚪ n/asrc/ui/Icon (3 glyphs added: gift, megaphone, trendingUp)Three more glyphs the design's own Lucide map already specified. All three sit in ds-revision/icon-lucide-map.md's "exact matches, no change" list, so there is no rename decision here, only the absence QRS-648 records. Needed by More's Grow group, where the design assigns a distinct glyph per entry. Substituting by meaning would have given Offers and Invite & Earn the same icon, which is exactly the mistake the scanner work refused to make one day earlier. Sync target: none, the design already has them. QRS-659
2026-08-14correction⚪ n/aDashboardHome (quick actions restored, rating tile removed, KPI cells linked)Three corrections on the first screen a merchant sees, every one of them the implementation catching up to the design rather than anything going upstream. (1) QUICK ACTIONS ARE BACK. QRS-582 deleted the row on 2026-08-12 citing a grep of the round-19 design that returned zero hits, which was true then; round 22 re-added it with REAL destinations (Orders, Collections, Payments) instead of the retired /create placeholder that made the old row worthless. The deletion was right and its premise expired: a verified claim about a document that keeps moving has a shelf life, and nothing in this repo measures it. (2) THE RATING TILE IS GONE. Design round 19 removed it because nothing writes a rating and no ratings table exists (QRS-576); the packages/data stub still carried one, so the merchant's own home screen was showing an invented 4.8 with a fabricated -0.1 trend, which is the "never fabricate insight" rule broken on the most authoritative surface in the product. Its absence is now asserted by name, not implied by a count. (3) KPI CELLS ARE LINKS, per the design's own strip: a figure a merchant cannot open is the display-only surface the proactive-value gate rejects. Optional per metric, so a number with no screen behind it stays plain text rather than becoming a pressable that does nothing. ⚠ Scan & collect is the design's fourth quick-action tile and is deliberately ABSENT until ScanCollect exists (QRS-645), because the design's own rule for More is that a tile whose tap dead-ends teaches the merchant the whole surface is unreliable. Sync target: none. QRS-659
2026-08-14divergence🔴 openMoreScreen (desktop entry rendered as an informational card, not a link)The design's desktop entry is a LINK to desktop-console/Home.dc.html; ours is the same card in the same slot with the same intent, and it is not tappable. The destination does not exist in the product: ADR-0011 makes the merchant web surface the RNW export and the DOM merchant tier is 26.3.0, so there is no separate desktop console to open. What IS true today is that the same app runs in a computer browser at the same address, so the copy says that. Linking to a console that does not exist would be the dead end the design's own More rule forbids, and the honest card is closer to the design's intent than an accurate link to nothing. ⚠ Also recorded here, same screen and same round-22 misreading: the block was REPLACED by round 22, not removed, and the implementation had deleted it and documented the deletion as the design's own decision. The test asserting its absence stayed green throughout, because it matched on /desktop/i against copy that says "computer": a negative assertion over a regex is only as strong as the wording it happens to match. Reconciles when the DOM merchant tier ships. QRS-659
2026-08-14divergence🔴 openMoreScreen (two Manage tiles the design does not have: Banking, Plan and billing)Kept on purpose, because dropping them now would strand a screen. The design reaches banking through Settings ("Language, banking, security", and that row now exists) and plan through the tier pill on Profile, which is not built yet, so removing the Plan tile before the pill lands would leave Plan and billing unreachable from anywhere. Recorded rather than fixed because the fix belongs with the Profile work, in one change, not as a deletion that quietly orphans a route. Also divergent and deliberately unchanged: the tile SUBS. The design's are terser ("Who is collecting today"); ours are equivalent, already translated into three languages, and the design's are English only, so copy churn across 8 rows and 3 locales buys very little fidelity. Reconciles with the Profile tier pill. QRS-659
2026-08-12ahead🔴 openapps/mobile/src/ui/QrCode.tsx · qrMatrix.tsThe design's QR is decorative and says so; ours is scannable. QRDisplay.prompt.md: "The matrix rendered is decorative/deterministic, not a real scannable code — swap in a generated code in production." The PRESENTATION is taken from QRDisplay.jsx exactly (white tile, r-md, e1, 13px padding, centre mark at 26% with a 24%-radius knockout, label + mono route under a r-2xl raised card). Three things differ because a real code requires them, and none is a style choice: error correction H not M (the centre mark occludes modules, and error correction is what reconstructs them — at M a logo QR fails on cheap scanners in poor light, which is a stall at dusk); the module count is derived from the data, not fixed at 21 (the design's n = 21 is version 1, ~17 characters; a card URL needs version 5 = 37, so a hard-coded 21 renders a picture of a QR code); and a ≥4-module quiet zone drawn inside the viewBox (the design supplies only 13px of CSS padding, ~2.9 modules at 168px). Open so the design gains a note that its own n = 21 and tile padding are not implementable as a real code — the next person to build a QR from that file will otherwise repeat all three.
2026-08-12divergence🔴 open.../qr-tools/screens/QrToolsScreen (the Print button)The design's Print button is removed and RE-STATED in its own "Not built yet" list, which is the design's own rule applied to the design's own control. It is implemented as window.print() — browser-only, with no native equivalent — and the design's comment already concedes "a designed print sheet for the stall board does not exist yet". Shipping it on Android and iOS would mean adding expo-print (a new native module, per-ABI weight, a prebuild change) to produce an undesigned sheet, and shipping it web-only would be a per-platform divergence with no approved fallback — which the retired exception path no longer permits. So Print becomes a named unbuilt item with its real reason and Share takes the full width. Open pending a decision on whether a print sheet is wanted at all; if it is, it needs a DESIGN (sheet size, what else is on the page) before it needs a dependency.
2026-08-12correction⚪ n/aapps/mobile/src/ui/ScreenHeader.tsx (subtitle)Added the subtitle the design's header already has — QRTools.dc.html renders a 15.5px title over an 11px content-tertiary sub, and our ScreenHeader had title-only, so QR Tools could not say whose code it was showing. ⚪ because this implements an existing design rather than inventing one. ⚠ flex: 1 moved from the title <Text> to a new wrapper (the title is no longer the flex child), which broke ScreenHeader.test.tsx first — correctly, since that assertion pinned an implementation detail. It now asserts the guarantee (a long title truncates rather than pushing the trailing actions off-screen) plus two new subtitle cases.
2026-08-12ahead🔴 openpackages/tokens (brand.onGradient → --brand-on-gradient, ThemeColors.brandOnGradient, brandGradientTopHex())Defined a token the DESIGN REFERENCES AND NEVER DECLARES. console-kit.js's Wordmark and BrandSplash.dc.html both bind var(--brand-on-gradient, #ffffff), and _ds/.../tokens/colors.css has no such variable — so the design has been running on the literal fallback everywhere it puts ink on the brand gradient. Defining it is genuinely ahead: the repo now has one name for the concept and the fallback stops being load-bearing. ⚠ It is SCHEME-INDEPENDENT and must never gain a dark variant — a brand background is the same saffron-to-coral in both schemes, so its ink is too; the emitter deliberately writes it into the light :root only and omits it from darkVars. Note the contrast with its neighbour brand.setu, which sits on a NEUTRAL surface and therefore does flip: the two look alike and must not be reasoned about together. Also added brandGradientTopHex(), so app.json's native-splash colour is ASSERTED against the token rather than trusting the design's stated #FFC229 (they agree — verified). Open so the design project gains the declaration; until it does, the design and the repo agree only by coincidence of the fallback.
2026-08-12divergence🔴 openapps/mobile/src/ui/Wordmark.tsx (stacked lockup: derived size, and the reveal's span)Two deliberate mechanism differences in the same component, both recorded because either would look like a mistake to the next reader. (1) The design MEASURES and RE-FITS; the repo DERIVES. The design renders the mark at a probe size, reads offsetWidth, computes the size that lands on 52% of the frame width, and measures again on document.fonts.loadingdone because the first pass uses fallback metrics. In RN the glyphs are drawn into a fixed SVG viewBox and both lines are centred by textAnchor, so the lockup's box is known from size alone: BrandSplash computes frameWidth * 0.52 / WORDMARK_STACK_RATIO and nothing depends on the rendered text width or on when Akaya Kanadaka arrives. Same outcome, one fewer moving part, and no re-fit flash. (2) The reveal wipes the lockup's full box, not the glyphs' own narrower span. The design clips the text's own width; deriving rather than measuring means the exact glyph span is unknown, so the first fraction of the 580ms sweep crosses empty space before the first letterform. Invisible at that duration, and the alternative is reintroducing the measurement this row's first half removed.
2026-08-12correction⚪ n/aapps/mobile/src/tiers/user/features/onboarding/screens/BrandSplash.tsx · apps/mobile/app.jsonThe splash was rebuilt TO the approved design, so five of QRS-583's six deltas are the repo catching up rather than diverging — ⚪, not 🔴. Background now the brand gradient at 160deg and scheme-independent (was c.surface, i.e. it followed the theme, which is wrong for a brand moment); the clip-safe SVG ring field added (was absent); ONE mark instead of two (was icon.png at a fixed 104×104 plus a size={30} wordmark, on the one screen whose job is to show a single brand); the attribution solid white instead of brand-gradient text — the design rules that out in as many words ("not gradient text, which would disappear here"), and it was gradient ink on a gradient ground, a contradiction rather than a variance; and a 240ms exit cross-fade instead of a hard cut. The sixth was the native hand-off: app.json moves to the gradient's top stop #FFC229 in both schemes. ⚠ Its test failed first, which is the gate working: native-splash.test.ts asserted surfaceHex(scheme) on the then-correct premise that the splash painted c.surface, and now asserts brandGradientTopHex() plus that light and dark are the SAME value — a neutral dark would have produced a seamless hand-off in light and a saffron flash against navy in dark, i.e. a one-scheme-only defect of exactly the QRS-206 shape.
2026-08-12correction⚪ n/aapps/mobile/src/app/(user)/(tabs)/_layout.tsx (the centre button's fill)The quick-create button drew the WRONG GRADIENT and the design had already fixed the same drift on its own side. It rendered LinearGradient colors={[c.accent, c.secondary]} — saffron-500 → coral-500 — where console-kit.js's CentreButton is unconditionally var(--brand-gradient), i.e. brand-qr-from → brand-qr-to (43 100% 58% → 11 100% 60%), a genuinely different pair of stops rather than a rounding difference. console-kit.js's own header records why it exists: "Home drew the centre button on var(--brand-gradient), More drew the same 60x60 button on var(--accent) … a copy drifted because it was a copy." Corrected to c.brandQrFrom/c.brandQrTo. ⚪ rather than 🔴 because the design is already right and the repo was wrong — nothing to push back. Note the design's structural guard, which the app now inherits: CentreButton takes no colour, radius or size argument at all, because it is global navigation chrome and industry customisation changes CONTENT, never CHROME — so there is nothing here to parameterise either. Geometry corrected in the same pass (60×60 not 58, r-xl/24 not 20, top:-20 not -18, glyph 28 not 26).
2026-08-12divergence🔴 openapps/mobile/src/ui/ActionLauncher.tsx (tile fill) vs design Sheet backgroundThe design's quick-create sheet sits on --surface with --surface-raised tiles; the app's shared Sheet shell is itself surface-raised, so the tile and the sheet resolve to the same fill in DARK. In LIGHT this is invisible and faithful — surface and surface-raised are both pure white, so border-subtle has always been what separates a tile from the sheet behind it, and it still does on both schemes. Recorded rather than fixed because the cheap fix is wrong in the other direction: surface is DARKER than surface-raised in dark mode, so filling tiles with it would invert the design's raised relationship, and there is no token above surface-raised. The real fix is systemic — move the shared Sheet shell to surface — and that restyles every sheet in the app (PillSelect, ConfirmSheet, the Security form sheet, the reminder composer), so it needs its own design pull and its own three-surface pass rather than riding a route retirement.
2026-08-12ahead🔴 openapps/mobile/src/ui/Icon.tsx (box)Added a box glyph, which the design references and ds-revision/icon-lucide-map.md does not list in either direction — neither among its twelve renames nor among its "exact matches (no change)" set. console-kit.js's TABS names glyph: 'box' for the Catalogue tab, so the app needs it; mapped 1:1 onto lucide Box per that document's own stated direction ("adopt lucide as the canonical set, reference icons by Lucide name only") rather than borrowing store or tag, both of which already mean something else here. Open so the map document gains the row — it is a one-line addition to a table that is otherwise complete, and a glyph the design uses but never declares is how the next person picks a different one. No app-size callout owed: lucide-react-native is already bundled and imports per-icon.
2026-08-04ahead🟢 syncedpackages/tokens/src/{palettes,contrast}.ts (new); tokens.ts (rgbFromHslTriplet extracted, exported)New card-palette layer (ADR-0019 Decision 2, M4/M2) — SemanticSet/Palette/PALETTES/buildCardColors()/contrastRatio(), plus two new seasonal palettes, ganapati-festival and diwali-2026. The design project had no card-palette concept before this (its only colour spec was the single brand tokens/colors.css), so this is genuinely ahead — but the sync happened in the SAME session rather than being a pending follow-up: the manifest + both palettes (packages/schemas/src/setu-card-template-manifests/default.v1.ts, packages/tokens/src/palettes.ts's two new entries) were authored into templates/setu-card/ in the "QR setu Design System" project first (default.v1.manifest.json, default.v1.prompt.md, palette-ganapati-festival.json, palette-diwali-2026.json, setu-card-templates.card.html), THEN mirrored into the repo — design-first per ADR-0015, not code-first-then-backfilled. Marked synced rather than open because both sides were written together, not because reconciliation is deferred. Hue choices: Ganapati Festival (vermillion/marigold over warm maroon-brown ink) and Diwali 2026 (gold over deep indigo dark scheme) are deliberately distinct from each other and from the default setu brand palette (saffron/navy) — every content-vs-surface pair clears WCAG AA (4.5:1) and every new palette's accent clears the non-text 3:1 UI floor against its own surface, verified with contrastRatio() before committing, not eyeballed. setu's own existing accent (~1.45:1 against its surface) is unchanged and was never held to that floor — a pre-existing decision, not something this row re-litigates. QRS-345
2026-07-28correction⚪ n/asrc/ui/Chip (toggleable filter pill)Chip gained onPress + selected, which is the component the design has specified all along. components/badges/Chip.prompt.md describes a toggleable filter/selection pill (<Chip selected onToggle>Food</Chip>); the RN implementation was static-only, so the reminders priority filter had nothing to build on (QRS-230). A correction, not an ahead: the contract existed and we had implemented half of it. Two implementation notes that are ours rather than the design's — a pressable chip wraps in PressableScale with a touchMin box around the smaller visible pill (the two-layer pattern from IconButton; hitSlop is invisible to RNW layout and to the Playwright gate), and selection uses the accent pair regardless of tone, because selection is a state of the control, not a restatement of what the chip is about — a selected danger pill that stayed red would read as "this filter is dangerous". Nothing to push upstream. The Chip-vs-Badge role split below is now half closed: Chip does the filter job the design gives it, and still also does Badge's status job. QRS-230
2026-07-28correction⚪ n/asrc/ui/Chip (danger tone)Chip gained the danger tone, closing a severity ladder that ran backwards. Reminder priority mapped urgent → info, so the most severe priority rendered calmer (blue) than high (amber). Recorded as a correction because the design already specifies this tone on its status pill — components/badges/Badge.prompt.md uses <Badge tone="danger"> in its own example — so nothing new was invented and nothing goes upstream. Tokens only (danger-soft / danger). The finding worth carrying, though, is bigger than the tone: the design splits Chip (a toggleable filter or removable pill — "Food", "Veg only ✕") from Badge (a small uppercase status pill with a semantic tone — Live/Draft/Paused). The RN side has only Chip, and every current use of it is a status badge — reminder priority, repeat labels, the dashboard's demo marker. So we have one component doing two specified jobs under the wrong name, which is why it lacked a danger tone in the first place. Splitting it means a new Badge primitive plus a call-site migration; deliberately not bundled into a screen fix. Sync target: none for the tone; the Chip-vs-Badge split is an implementation gap to schedule, not a design change to push. QRS-226
2026-07-28divergence🔴 opensrc/ui/ScreenHeader (new)Screen chrome is now one primitive — title, actions slot, controls slot, and the hairline rule the design specifies and we never had. Pulled from components/app-shell/AppShell.jsx (narrow-container branch) before implementing, per ADR-0015; its spec is borderBottom: 1px solid var(--border-subtle), sticky, title flex 1 + ellipsis, trailing actions. Two halves to this row. Corrective: the rule was absent on every screen, so a title and the first list group ran together with nothing but padding between them — and the headers were written className="px-5" (18px on our scale) above list content padded to 20, rendering each title 2px inboard of the cards it labels. Both now read space[6] through the exported SCREEN_GUTTER. Divergent, deliberately, three ways: (1) no vibrancy blur — the design layers its header over scrolling content at 82% opacity with backdrop-filter; here it is a sibling above the ScrollView so nothing passes beneath it, and a blur would cost expo-blur (a native module, against the app-size budget) to composite against an opaque background; (2) the title is the app's own pushed-route nav bar — centred, 17px, weight 800 — rather than AppShell's left-aligned 15px, because Settings and Profile have rendered exactly that since they were written and internal consistency beats matching a spec calibrated for a ≤560px container in a desktop preview card. (This replaced a left-aligned 22px title the component carried for one iteration, QRS-231: a large title beside a back chevron is a defensible idiom in isolation and was still a third header style in a four-screen app.) The back control uses IconButton's default outline variant and the row reuses Settings' own padding: 20 on both axes, so the chevron and any trailing action land pixel-identically to Settings and Profile; (3) 1dp rule, not StyleSheet.hairlineWidth, which is 0.33dp at 3× and vanishes at some Android densities. Sync target: either AppShell states that its narrow branch is container-scoped chrome, or the design gains a native mobile screen-header spec with the type size, the rule, and no blur. Amended same day (QRS-226): a leading back slot was added, matching AppShell's own structure (a leading element, then the title, then trailing actions) and the chevronLeft idiom Settings/Profile already use. Superseded by QRS-231 the same day: the two-idiom split this note recorded is closed — the primitive now renders Settings/Profile's own nav bar, so those screens adopting it (QRS-225) is structural (sticky + the rule) rather than a visual redesign. QRS-224
2026-07-28correction⚪ n/asrc/ui/SheetThe bottom sheet's 40px top corners now survive on native. rounded-t-3xl was on the file since the component was written and renders correctly on web, but it sits on an Animated.View — createAnimatedComponent(View), a component NativeWind's registry does not contain — which is the same interop mechanism as QRS-190/QRS-203. The radius is now ALSO set inline, derived from radius['3xl'] rather than typed as 40, so the class and the inline value cannot disagree. Recorded as a correction: the design already specifies the soft-corner shell and CLAUDE.md already states it as an invariant — nothing goes upstream, and nobody should later "tidy away" either half. QRS-222
2026-07-27correction⚪ n/apackages/tokens (theme.css dark-mode compilation)Dark variables now declare on .dark:root, preceded by an @cssInterop set darkMode class dark; flag as the file's first rule. No design value changed — every token keeps its palette value. Purely a parser correction, and the one that made the theme render correctly on Android and iOS for the first time: React Native has no CSS cascade, so react-native-css-interop only lifts a block into its dark variable table when the selector is .dark:root/:root[class~="dark"]; a bare .dark silently fell back to the LIGHT values on native (dark cards on a white page) while web was fine. And the selector alone was inert: css-interop defaults darkMode to media, in which mode no class-based dark block registers at all, so the flag had to come first. A @media (prefers-color-scheme) block had been carrying native's dark theme by accident for months. Recorded because packages/tokens/** is design-first under ADR-0015 and every change to it belongs on the record, corrective ones included. Nothing to push upstream — the design project has no equivalent construct — but apps/web will consume this same file and inherits the fix. QRS-206
2026-07-27divergence🔴 opensrc/ui/PressableScale (Android press visual)The Android material ripple is removed; press feedback is now scale + opacity on all three surfaces, with the haptic retained on both natives. This supersedes the ripple half of the 2026-07-26 row below (and the ADR-0011 carve-out it referenced): the ripple shipped as a grey block with sharp corners over rounded controls, because overlay is the 42–62% modal-scrim token and RN masks a bounded android_ripple to the view rect, never its borderRadius. RN offers no corner-radius option, so there was no "fix the ripple" available at this layer. Nine call sites had already been passing rippleColor={null} to switch it off individually. Sync target — this one genuinely needs a design decision, not just a doc update: the design project should specify the intended Android pressed state now that a bounded ripple is off the table (options: no ripple / borderless ripple / a token-driven pressed-fill), and TabBar/Button prompt specs updated accordingly. Until then the implementation is deliberately ahead of the spec and consistent across platforms, which is what product asked for. QRS-207
2026-07-27correction⚪ n/asrc/ui/PressableScale (cssInterop, native)The 2026-07-26 cssInterop registration is now web-only — on native it was removing every style from every PressableScale. No design value changed and nothing needs pushing upstream; this restores the already-approved design on Android and iOS, where controls were rendering with no background, padding, radius or row direction. Cause: Reanimated forwards className down to the wrapped Pressable (which NativeWind registers) on native, so the extra registration on the wrapper hijacked className and discarded the [style, aStyle] array. On web the wrapper does not forward, so the registration is still required there. Recorded because apps/*/src/ui/** is design-first under ADR-0015 and every change to it belongs on the record — including a corrective one. QRS-203
2026-07-27correction⚪ n/apackages/tokens (theme.css generator)Removed the @media (prefers-color-scheme: dark) block; the .dark class is now the only dark source, and color-scheme moved onto :root/.dark. No design value changed — every token keeps its palette value, and in a correctly-configured client nothing renders differently. This is a structural correction: the media query bound the CSS variables to the operating system while useThemeColors() followed the in-app preference, so an explicit "Light" on an OS-dark device rendered white cards and navy text on a dark page. Recorded here because packages/tokens/** is design-first under ADR-0015 and any change to it must be on the record, even a corrective one. Nothing to push upstream — the design project has no equivalent construct — but the DOM stack (apps/web) will consume this same file, so it inherits the fix rather than having to rediscover it. QRS-201
2026-07-27divergence🔴 openpackages/tokens (accent-active, content-tertiary — light)Two token colours fail WCAG AA as text in the light theme, and the implementation is knowingly shipping them. Measured by the new contrast gate: accent-active 2.90:1 on surface (2.58:1 in an accent-soft pill), content-tertiary 3.82:1 on surface (3.48:1 on surface-muted); AA needs 4.5:1. Dark passes. Not corrected in code because these are design values and this surface is design-first — darkening the brand amber changes brand identity, and darkening the tertiary ink changes the whole type hierarchy's feel. Sync target: the design project should re-derive the light-theme accent-active and content-tertiary ramps against a 4.5:1 floor on surface and on their soft/muted companions, then we pull them. Held meanwhile as two exact-colour exemptions in the gate so no other low-contrast pairing can slip in. QRS-202
2026-07-27divergence🔴 openDashboard home (screen composition)The console home leads with the Setu Card, not a revenue stat card. The design's merchant-dashboard/Overview opens with four equal StatCards (Revenue / Orders / QR scans / Avg rating) — correct for a desktop console with real analytics behind it. On mobile in R1 those numbers are sample data, so that layout would put a fabricated revenue figure in the largest type on the merchant's own home screen. Implemented instead: one hero showing real Setu Card status, the four numbers consolidated into a single MetricPanel, and a DemoChip wherever values are illustrative. Also removed the gradient share prompt (PromoCard) — the hero carries that CTA, and two gradient cards competed for the same tap. Screen composition is code-first under ADR-0015, so this needed no prior approval; recorded because the divergence is real. Sync target: the design should gain a mobile Overview variant, and a sample-vs-measured state for any metric it draws.
2026-07-27divergence🔴 openpackages/tokens (sizing.touchMin)Touch-target minimum raised 40 → 44. The token was ported verbatim from the design handoff at 40, which is below Apple HIG (44pt) and Material (48dp) — and it is the root cause of all 12 undersized controls in QRS-191. Chose 44, not 48, so the token matches the Playwright gate exactly rather than maintaining two numbers. Also documented that sizing.control.* are visual sizes that may legitimately be smaller than the tap box. Sync target: the design project's tokens/sizing.css should carry 44 and the visual-vs-tappable distinction, otherwise the DOM stack keeps shipping 40. QRS-193
2026-07-27ahead🔴 opensrc/ui/IconButton (new)Implemented the IconButton the design already specifies, plus a two-layer hit box the design has no concept of. The design defines components/button/IconButton (with aria-label in both its examples) but the RN side never had one, so screens hand-rolled raw <Pressable className="h-10 w-10"> — which is 32×32 on our scale. The new primitive makes accessibilityLabel a required prop. The part that is genuinely ahead: a control may keep a small visible circle while presenting a touchMin tap box, because RN/RNW ignore hitSlop for layout and a slop-only fix would look fixed without being fixed. Same two-layer pattern applied to LanguageSelect chips so the dense header switcher keeps its proportions. Sync target: the design's IconButton/spacing guidance should state the visible-vs-tappable split. QRS-191
2026-07-27correction⚪ n/asrc/ui/PressableScale, src/ui/ButtonDisabled opacity now actually renders, at the design's 0.42. Button had drifted to 0.45 and the value was inert — PressableScale's animated style overrode any caller-set opacity, so disabled buttons never dimmed at all. Dimming moved into the primitive's animated style with a new opacity.disabled token. Recorded as a correction because the design already specifies 0.42; nothing here needs pushing upstream. QRS-194
2026-07-27none⚪ n/asrc/ui/PressableScale (cssInterop)No visual change intended, but a real one occurred — worth an explicit row so it is not mistaken for design drift later. Registering cssInterop made className work on this component for the first time, so padding/radius/border declared in classes at 6 call sites began applying. That is the previously-authored design finally rendering, not a new design decision. Measured example: LanguageSelect chips went 10×17 → 30×25, exactly px-2.5 + py-1. QRS-190
2026-07-26divergence🔴 opensrc/ui/PressableScaleHaptics now fire on Android as well as iOS. The design system has no opinion on haptics (it is a DOM/JSX reference implementation and cannot express them), but this reverses a documented carve-out in ADR-0011 that gave Android the ripple instead of a haptic. Product requires the tactile response itself to be consistent across platforms, with only the visual idiom differing. Sync target: the ADR-0011 carve-out wording and the TabBar/Button prompt specs should state that press feedback is ripple + haptic on Android. QRS-183
2026-07-26ahead🔴 opensrc/ui/BrandIconNew tone="brand" variant rendering official platform colours (BRAND_COLORS, from simple-icons) instead of a monochrome token, used on Profile › Social so the list is scannable by recognition. The design project's Icon has no brand-colour variant. Includes one deliberate sub-rule worth carrying across: X/Twitter resolves against the theme because its brand colour is pure black and vanishes on a dark surface (X itself publishes a white mark for dark backgrounds). Sync target: components/icon/Icon gains a brand-tone variant + the X exception. QRS-187
2026-07-26correction⚪ n/aapp shell tab barRemoved the accent background pill behind the active tab; active state is now icon + label colour only (accent-active vs content-tertiary). This was drift, not a new design — components/app-shell/TabBar.jsx already specified background: 'none' with colour-only active state. Recorded so nobody later "syncs" this to the design, which already contains it. QRS-186
2026-07-26divergence🔴 openpackages/tokens (fontMetrics) + src/ui/AppTextLine height is clamped to a per-font floor (Math.max(variantRatio, fontMetrics[family])). The design's typography ratios (1.05–1.55) are valid CSS, where a short line-height merely lets glyphs overflow; in React Native it is a hard clip box, and Baloo 2 needs 1.61× — so every display-face variant clipped. The token ratios are deliberately unchanged because they are correct for the DOM app that shares them. Sync target: the design project's tokens/typography.css should carry a comment that these ratios are DOM-only and RN consumers must clamp — otherwise the next person re-derives this bug. QRS-181
2026-07-26ahead🔴 openProfile › Hours"Add holiday" is disabled while any holiday row is incomplete, with an inline reason line. The design has no empty/partial state for the planned-holidays list. Sync target: the Profile Hours screen needs the disabled + explanatory state drawn. QRS-185
2026-07-28correction⚪ n/asrc/ui/SheetThe height cap now measures the space ACTUALLY available, not the window — (H - keyboardOverlap) * 0.9, plus flexShrink: 1 + minHeight: 0 on the content wrapper so a consumer's own ScrollView receives a bounded height. This was a bug, not a design change: on iOS the keyboard overlays the window and reports no inset, so useWindowDimensions().height stayed 812 on an iPhone X with 336dp of keyboard up and the cap over-promised by exactly that much. Any form sheet taller than the remaining space rendered its lower half — including its submit button — outside the visible area, unreachable, because Sheet deliberately does not force-scroll. Android was unaffected (adjustResize shrinks the window, so the reported height already excludes the keyboard) and the iOS Simulator could not reproduce it (it defaults to a hardware keyboard, so nothing ever overlays) — it appeared only on a physical iPhone. No design consultation needed: the design specifies a sheet whose content is reachable, and this restores that. QRS-235.
2026-07-28ahead🔴 opensrc/ui/Banner (new)New RN primitive, built from components/overlays/Banner.jsx + .prompt.md — tone-soft fill, tinted icon, 13.5/700 title, 12.5 description, --r-3xl radius, dismiss-free. One deliberate addition: an optional action. The design has no action affordance and is right not to for its own examples ("Menu published" is a notice, and a notice with a button is a dialog). But the first real consumer is "your alerts are blocked", where the entire point is that the merchant can fix it — a banner that reports a broken state and offers no route out is the silent failure it exists to replace. Optional and absent by default, so design-faithful usages stay faithful. Sync target: add an optional action slot to the design's Banner, or record that our blocked-state banner is a distinct component. QRS-236.
2026-07-28ahead🔴 opensrc/ui/CountBadge (new)The design has NO count badge. components/badges/Badge.jsx is a different component doing a different job: an uppercase, letter-spaced STATUS pill (Live/Draft/Paused/New) that sits inline in content flow. A counter that overlays a 32px icon and stays legible at two digits is not that component with different text — reusing it would have hung a wide pill off the side of the bell. So this leads the design, while borrowing only vocabulary the design already established (11px/700 and --r-pill from Badge; the sibling-overlay + pointerEvents="none" pattern from IconButton). Two decisions worth reconciling: it renders NOTHING at zero (a "0" badge draws the eye to reassure, which is the definition of noise), and it uses the SOFT-fill/STRONG-ink tone pair rather than the filled-red-with-white-text platform default — because there is no danger-contrast token and danger is lighter in dark mode, so white ink would land near 2.6:1. A filled variant needs a real danger-contrast token first, which belongs with QRS-202. Sync target: add a count/notification badge to the design project. QRS-238.
2026-07-28correction⚪ n/apackages/tokens (danger-strong) + Chip / Banner / CountBadgeNew danger-strong token, because the design's own danger + danger-soft pairing fails WCAG AA as text. Measured on the real web export by the theme-consistency gate: danger ink on danger-soft is 3.64:1 in light mode, against AA's 4.5:1 floor. Dark already passed (4.63:1), so this was light-theme-only. danger-strong is 4 74% 44% light (4.94:1) and 4 82% 68% dark (5.23:1), and it is used as the on-soft ink in the three components that render danger text on a danger field. A correction rather than a divergence: the design specifies tone-soft/tone pairs and intends them to be legible, so this restores the intent rather than departing from it — but the design project's own Badge/Chip/Banner still specify var(--danger) as the ink, so the upstream tokens need the same addition or its red pills carry the same failure in the DOM idiom. theme.css updated alongside tokens.ts for exactly that reason. Additive: danger is unchanged, so fills keep their weight. Sync target: add --danger-strong to the design's tokens/colors.css and repoint the three components' danger ink. QRS-240, related QRS-202.
2026-07-29none⚪ n/apackages/tokens + 40 files in src/ui (static-analysis pass)No visual change intended and none expected — recorded because the systemic surface was touched broadly and a silent 40-file diff there is exactly what a later reader would mistake for design drift. All of it is QRS-247 static-analysis remediation, and every edit is type-level or value-selection with the same inputs and the same outputs: props types wrapped in Readonly<> (92 sites, types only, zero runtime); nested ternaries rewritten as guard clauses in Button (the danger-on-variant palette), TextField (border colour precedence + the error/helper caption slot), IconButton (variant → record lookup, matching the idiom Button already used), SettingRow (leading glyph), ScreenHeader (trailing actions/spacer slot), ClockPicker (the 12-o'clock label); and packages/tokens' six-deep HSL sextant chain extracted to a named hslSextant helper with byte-identical conditions and order. One behavioural change, deliberately kept off the visual surface: Sheet moved from Reanimated's deprecated runOnJS to scheduleOnRN — the same UI-runtime-to-RN-thread hop with arguments passed directly, so the drag-to-dismiss and exit-animation unmount paths are the ones to look at on device. Why a none row and not silence: ADR-0015 says record at the moment of divergence, and "we changed 40 systemic files and nothing moved" is a claim worth being on the record so it can be falsified. It is falsifiable by exactly one thing, and it is the thing CLAUDE.md names: npm run e2e:quick (99 passed) covers the WEB bundle only, and jest mocks Reanimated's createAnimatedComponent to identity, so neither gate can see native press feedback, sheet dismissal, or ripple — the seam that produced QRS-203/QRS-206/QRS-207. The Android and iOS builds are the gate for this row. RESULT (2026-07-30): confirmed none. The owner ran the iOS build and reported no issues, then installed and exercised the arm64 release APK and reported it working. That covers the one behavioural change this row flagged — Sheet's runOnJS → scheduleOnRN drag-to-dismiss and exit-animation unmount — on both natives, plus press feedback on the primitives the 40-file diff touched. The prediction is now a result, which is the whole reason the row was written as a falsifiable claim rather than left silent.
2026-07-30none⚪ n/anone — screen composition only (7 files)Recorded to state a negative: the systemic surface was NOT touched in this pass, so ADR-0015 required no row and npm run check:design correctly passed without one. This is the second half of QRS-247 — the 17 remaining static-analysis findings — and every edit landed in src/tiers/user/features/** (DashboardHome, NotificationsScreen + NotificationRow, RemindersScreen, SettingsScreen, AuthScreen, ProfileScreen), packages/domain/src/reminders, one Playwright spec, one CI script and the ESLint config. Zero changes to packages/tokens/** or apps/*/src/ui/**, which is what makes this code-first-and-sanctioned rather than a governance question. No visual change intended, and the mechanism is what makes that checkable: each screen's loading/error/empty/content ladder was replaced by a SHELL component holding the identical wrapper elements plus top-level guard returns, so the rendered element tree is unchanged — same View, same SafeAreaView edges, same ScrollView contentContainerStyle, same testIDs. Threading the content's dozen values out as props was the alternative and was rejected precisely because it does not have that property. The duplication this creates is acknowledged, not hidden: five near-identical shells now exist, and the shared @/ui primitive that would collapse them is QRS-259, deferred to the auth round because it IS a systemic-surface change and needs the design pulled first. Still native-gated despite the above. Broad JSX restructuring is the exact shape that produced QRS-203/QRS-206/QRS-207, and e2e:quick (99 passed) is the web bundle only. A fresh arm64 APK is built for the owner's device pass; until that and the iOS build are looked at, this row's none is a prediction.
2026-07-30none⚪ n/asrc/ui/{Calendar,Card,GradientHeadline} (3 files)No visual change intended. Part of QRS-260 (SonarQube CE burn-down, the layer beyond eslint-plugin-sonarjs) — every edit is a React key expression or a string-method swap, checkable by inspection rather than by trusting the description: Calendar — the weekday-letter row's key went from the bare index to `${w}-${i}` (letters repeat: 'S' and 'T' twice each, so the letter alone still could not be the key), and the day-grid View from the bare index to `${view.y}-${view.m0}-${i}` (day numbers repeat across months and the padding cells are all null, so only a month-scoped key is genuinely unique). Neither View/AppText gained, lost, or reordered a style prop. Card — the list-row divider's key now prefers child.key (already assigned by Children.toArray, which is precisely what that API exists for) over the index, falling back to it only for a non-element child. Same rendered output for every existing call site. GradientHeadline — three .replace(/\*/g, …) / .replace(/:/g, …) calls became .replaceAll(…); both do a literal-character global replace and produce byte-identical strings. Not touched, despite being in scope for the same finding: OtpInput and StepProgress also had an index-key finding, and both are suppressed in sonar-project.properties rather than edited — a fixed-length row of decorative, stateless slots has no other identity to key by, and restating the same index under a different name would not be a real fix. Their source is therefore unchanged, and this row does not cover them because there is nothing to cover. Still native-gated regardless of the size of the diff: apps/*/src/ui/** is the merchant app's shared component set, and Card/Calendar render on multiple screens; e2e:quick (part of this pass's gate sweep) is the web bundle only. A fresh arm64 APK covers this commit along with the rest of QRS-260's changes.
2026-08-01none⚪ n/asrc/ui/Icon (new isIconName guard)No visual change, and none possible: this adds an exported type-guard function and touches no rendering path. Recorded because apps/*/src/ui/** is design-first under ADR-0015 and every change to it belongs on the record, including an additive non-visual one. Why it was needed: QRS-249 moved the industry picker's glyphs out of a compile-time array and into business_domains.icon in the DATABASE, so IconName can no longer be assumed at that boundary — and an unknown name is not a benign no-op, because MAP[name] would be undefined and rendering it throws. The alternative was an unchecked cast at the call site, which converts a data-entry typo in the future command center into a crash on the merchant's industry screen. IndustryStep falls back to the generic spark glyph rather than dropping the option, on the reasoning that a merchant losing their own industry from the list is far worse than a slightly wrong picture beside it; a test covers exactly that row. Nothing to push upstream — the design project's Icon has a fixed authored set and no concept of a runtime-supplied name, which is correct for it. QRS-249
2026-08-13divergence🔴 openCard editor (screen composition; card-editor feature)Two departures from prototype/mobile-console/CardEditor.dc.html, both forced by capabilities the product does not have yet rather than by taste. (1) "Preview & publish" is ONE button in the design and TWO STATES here. The design's button links to the public card; a DRAFT card cannot be previewed, because get_public_setu_card serves published cards only and signed preview tokens for unpublished cards are explicitly deferred (they would be a third public-access path and need an ADR-0014 amendment). So a draft's button PUBLISHES and a published card's button OPENS THE LIVE CARD. A Preview button that 404s on every new merchant's first visit is worse than the divergence, and publishing is the highest-value unblocked action a new merchant has, so the primary button being the action is arguably the better screen. Sync target: the design should draw the draft state, or record that publish is a separate primary action. (2) The identity strip shows STATUS + SLUG, not open/closed + plan. SetuCardEditorService carries neither opening hours nor a resolved plan, so the design's two chips would have to be invented from nothing — and a fabricated "Open now" on a merchant's own editor is exactly the "never fabricate insight" failure. Status (Draft/Live) and the public URL are two facts we actually hold and that a merchant needs on this screen. Sync target: either add hours/plan to this contract or change the design's chips. NOT a divergence, recorded so it is not mistaken for one: the design gates its rows on the industry record (if (ind.context)), and this implementation gates on a server-resolved sections list instead. That is ADR-0021 D4 (no branching on industry/archetype/plan in app code) and the outcome is identical. Native-gated: src/ui/** was NOT touched (Sheet, Switch, SettingRow, TextField, DateTimeField, ConfirmSheet, Chip, Banner all consumed as-is), so ADR-0015 required no row for a systemic change — this row is about screen composition and is filed because the two divergences are decisions someone will otherwise re-litigate. Web PWA verified; Android and iOS outstanding, owner-track. QRS-610
2026-08-13correction⚪ n/asrc/ui/AppText (script prop) + src/ui/Chip (ChipTone)Two additive corrections on the systemic surface, both exposing something the design's OWN tokens already defined and the primitive simply would not hand out. (1) AppText gains script="mono". script was typed `'ui'
2026-08-13divergence🔴 openOrder detail + Collections (screen composition; orders feature)Four departures from prototype/mobile-console/OrderDetail.dc.html and Collections.dc.html, three of them corrections and one an absence. (1) The primary CTA on a ready order OPENS A SHEET instead of writing. The design's button reads "Mark collected, balance received" and its prototype writes both facts to local state in one assignment. Ours cannot: the balance has to be collected with a method before either fact is true, so the tap opens the pay sheet and its submit calls a single completeOrder. Two sequential calls were the alternative and are worse than the design's optimism — the failure lands between them, with real cash on one side. This is QRS-616, and it was a live defect on Collections too, where the same CTA wording sat over a status-only write. Sync target: the design should draw the method step, or record that its CTA is a two-step action. (2) The 7-day picker always shows the AGREED day, even outside its window (QRS-619). The design builds exactly seven days from model.today, so a day agreed three weeks out — or last week, now overdue — renders a strip with nothing selected beneath a card that names that day. A control that cannot display its own value invites the wrong repair: the merchant taps a day and silently rewrites an agreement. Corrected in collectionDayOptions, asserted in both directions. Sync target: the prototype still teaches the narrower behaviour. (3) Cancel reasons are archetype-neutral CODES. The design's second reason reads "Idol damaged before collection"; one orders table serves Goods, Time and Expertise, so the stored value is item_damaged and the festival wording lives in the goods label (ADR-0021 D4). Storing the merchant's English sentence would also make orders.cancelled_reason untranslatable and unanalysable, which is the defect industries.name already has. (4) Two designed blocks are ABSENT, not faked (QRS-618): the "Handed over" audit and the "N other accounts can present this code" notice. Measured — no handover audit and no order-share registry exists anywhere in supabase/migrations. A handover audit synthesised from status = 'completed' plus updated_at would be invented evidence in the one place a merchant would lean on it during a dispute, which is worse than an absent block and is the "never fabricate insight" rule at its sharpest. NOT a divergence, recorded so it is not mistaken for one: the design's RadioRow shape is reproduced by hand rather than by PillSelect/ChoiceCard, because both primitives are built for short mutually-exclusive chips and every one of these five labels is a sentence — longer still in Hindi and Marathi. Native-gated: src/ui/** was NOT touched in this pass (Sheet, TextField, Button, Chip, Avatar, Icon, EmptyState, Skeleton, ScreenHeader, PressableScale all consumed as-is), so ADR-0015 required no row for a systemic change — this row is about screen composition and is filed because all four decisions are ones someone will otherwise re-litigate. ⚠ Web PWA unverified as well as native: both screens are workspace-scoped and undrivable here (QRS-611), so what is verified is behavioural (58 jest cases against the real components) and not visual. All three surfaces outstanding, owner-track. QRS-616
2026-08-13correction⚪ n/asrc/ui/ActionLauncher (reshaped) + src/ui/Icon (edit) + the console tab barThe design MOVED and this catches up to it, so every item here is a correction rather than a divergence. (1) ActionLauncher is no longer a four-row "Quick create" list. console-kit.js's CentreSheet replaced it with a centre-navigation grid and states the defect it was fixing in its own words: the centre button "used to open a four line 'Quick create' list on three screens and the Quick add sheet on Catalogue, which is exactly the drift this module exists to stop". It is now one grid opened by the same button from every merchant surface, and it is NAVIGATION rather than creation — half its entries (View my Setu, Collections, Payments, Card activity) create nothing, which is why the module, the i18n namespace and the hook were all renamed quickTools rather than left under a word that no longer describes them. Shape: a full-width lead row for the one entry carrying primary (prominence is a contract, and the design's reason is that the Setu Card editor "used to sit two navigations deep"), a 4-up tile grid for the rest, and an "All tools and settings" footer — because five listed entries is not the whole product and a grid with no way out reads as one. Only the lead renders a sublabel: a tile is ~78px wide at 360 and a second line there wraps to noise. (2) Icon gains edit (Lucide Pencil). The set had no pencil at all, so the one destination a merchant opens most had no glyph to be drawn with. (3) The Store tab. Label Catalogue → Store and glyph box → store, straight from console-kit.js's TABS, which records the defect as two names for one destination — the bar said Catalogue while the screen header, the Settings row and the centre-nav tile said Store. ⚠ Our own i18n comment asserted the opposite ("the approved design spells it this way throughout"), which was TRUE when written and is the cleanest example in this repo of why a stale comment is worse than no comment: it argued against the change it should have prompted. (4) One real defect fixed on the way, not a design question: the gated tab painted and then VANISHED on hydration, because useFeature is optimistic during its read — right for content, wrong for chrome (QRS-623). Sync target: none for 1-3 — this repo was behind, not ahead. ⚠ Native-gated, and the tab bar is the single worst surface in this app for it: apps/*/src/ui/** plus global navigation chrome is exactly where parity keeps breaking (QRS-186 the pill, QRS-203 web-fix-killed-native, QRS-206 both natives, QRS-207 Android-only grey block), npm run e2e is the web bundle only and jest mocks Reanimated to identity. Android and iOS are the gate and both are outstanding. QRS-623, QRS-625
2026-08-14correction⚪ n/asrc/ui/Icon (9 glyphs added)Nine glyphs the design's own Lucide map already specified and this repo simply did not carry: shieldCheck, shieldOff, alertCircle, info, close, cameraOff, flashlight, flashlightOff, keyboard. A correction rather than a divergence because nothing is invented — ds-revision/icon-lucide-map.md's stated direction is "reference icons by Lucide name only" and all nine are standard Lucide names present in the installed lucide-react-native@1.25.0. Why this matters more than a missing picture: on ScanVerify the four verdicts are distinguished to the eye by glyph plus tone, so shieldCheck (Verified QR setu business) and shieldOff (Do not pay this) carry the semantics. QRS-648 had substituted absent glyphs by meaning as a stopgap; doing that here would have made "verified" and "do not pay this" share an icon on a screen whose entire purpose is telling those two apart. shield is unchanged and still maps to ShieldCheck — existing call sites depend on it; shieldCheck is the canonical name going forward and the two are deliberately the same component, not a duplicated concept. Sync target: none — the design project already has all nine; this repo was behind. ⚠ Native-gated: Icon renders on effectively every screen, and e2e:quick is the web bundle only. QRS-645
2026-08-14ahead🔴 opensrc/ui/scanner (new: ScannerScreen, CodeScanner, ScannerFrame, ManualCodeEntry, useCameraAccess, scannerCapability.{native,web})New systemic primitive, built design-first from prototype/mobile-console/ScanCollect.dc.html + prototype/consumer/ScanVerify.dc.html, which are byte-identical above the result layer — hence ONE shell serving both tiers rather than a copy in each. Faithful: the 222/36-radius reticle with 56px arms, the 2.6s scan-line sweep travelling 196 (implemented as Loop type="float" distance={-196}, so it inherits the existing reduced-motion behaviour instead of reimplementing it), the header's 44-1fr-44 grid, and the dark-always ground — navy[900] + brandOnGradient, both taken from the token package because both are scheme-independent, which is the design's own recorded invariant ("A CAMERA VIEW HAS NO LIGHT MODE"; painting it with surface/ink tokens inverted the whole scanner to a white panel in dark theme). Three things AHEAD of the design, each because a web mock cannot express it: (1) A TORCH TOGGLE in the 44px header cell the design reserved and left empty. A stall after dark is the R1 launch vertical and a scanner with no light is unusable there. Native-only — TORCH_SUPPORTED is false on web because the MediaStreamTrack torch constraint is not dependably implemented, and a control that silently does nothing is worse than an absent one. (2) THE STATUS BAR IS FORCED LIGHT (StatusBar style="light"). The ground is always dark, so a light-theme user would otherwise get black-on-navy time and battery. (3) A FIVE-STATE PERMISSION MACHINE where the design has two (allowed / denied). The design's denied state offers "Allow camera", which provably cannot work once the OS has stopped asking — iOS prompts once, Android sets "don't ask again" after two refusals, and requestPermission() then resolves as denied with no UI at all. So blocked is separated from ask and its action is openSettings(); checking renders a skeleton rather than flashing "QR setu cannot see the camera" on every open; and unsupported exists because on the Web PWA over plain http getUserMedia is undefined and the browser never offers the choice — the LAN preview this repo uses daily hits exactly that. Sync target: the design should draw blocked, checking and unsupported, and add the torch to the reserved cell. Manual entry is a PERMANENT part of the scanner, not R1 scaffolding: it is the only path with a cracked lens, with permission refused at OS level, on http, and — never designed for — with a screen reader, where aiming a viewfinder is not a usable interaction. It feeds the same onCode the camera feeds, so every refusal and verdict is identical whichever way a payload arrived. ⚠ NATIVE IS THE GATE AND IT IS OUTSTANDING. This is a new native module (expo-camera@57.0.3) plus expo-prebuild regeneration, on the systemic surface, touching press handling — the exact profile of QRS-203/QRS-206/QRS-207. Nothing automated here can see a camera: jest mocks it, e2e is the web bundle, and the web surface cannot even open a stream over http. Android APK is being built; iOS is owner-track on the Mac. QRS-645
2026-08-15correction⚪ n/aHome header + ShareCardStrip (screen composition; dashboard feature)The design MOVED and this catches up to it, so both halves are corrections rather than divergences. The re-pulled Home.dc.html header renders {{ brandMark }} (K.Wordmark({ size: 21 }), up from 19) and two controls, and nothing else: business name, business type and location are gone from the template entirely — bizName / bizTagline / bizInitials are still computed in renderVals() and rendered nowhere in it. They did not vanish from the product, they descended into the Hero Card: ShareCardStrip({ initials, name }) now draws a 62px business avatar at --share-strip-cta-bg with accent-active initials, beside a status line, the handle at 24/800, the body at 13.5/500 and a 38px CTA pill. Implemented to the design's own measurements. The one copy change that came with it: home.card.notLive was "Finish setup to go live" (an action) and is now "Your Setu Card is not published" (the state), matching the design's three parallel status labels, in all three locales. QRS-683
2026-08-15divergence🔴 openHome header — the wordmark is NOT a link (screen composition)The design wraps the brand mark in <a href="./Profile.dc.html"> and ours does not. That anchor is the wrapper the removed business chip used to sit inside; with the chip gone it makes the brand mark a second door to Profile, one control away from the avatar that already is one. That is the two-doors-one-destination redundancy QRS-228 removed from this exact row for cause, and it reads badly to a screen reader (aria-label="QR setu" on a link announces as "QR setu, link" — who, not where). Profile stays reachable from the avatar beside it and from the More tab, so nothing is stranded. Sync target: the design should either drop the anchor or give the wordmark a destination that is not Profile. QRS-683
2026-08-15divergence🔴 openHero Card CTA label + destination (screen composition)The design's CTA is "View my Setu Card" for all three card statuses; ours keeps an action label for two of them. The prototype has no publish gate, so cardHref always resolves. Ours does: is_published defaults false and the public route 404s until a merchant opts in, so on not_published the design's label would send them to a page that does not exist. Kept instead: the LABEL and the DESTINATION agree, which is the property the design is actually asserting. live uses the design's exact CTA and share glyph. Sync target: the design needs a not-published CTA once the publish toggle exists on the prototype. QRS-683
2026-08-15ahead🔴 opensrc/ui/Avatar (tone, initialsSize)Two additive props on a systemic primitive, both exposing values the design already states and the primitive would not hand out. The design's header profile control is a 44px circle at background:var(--surface-muted); border:1px solid var(--border) with 13px initials; Avatar had one appearance (accent-soft, no ring) and one type ratio (0.36 × size, i.e. 15.8px at 44). tone is a ROLE, never a colour — the same rule console-kit states for its own controls, so the caller picks what the avatar is FOR and the primitive picks the tokens. initialsSize is the same escape hatch radius already is, taken so Profile's 78px avatar is not silently moved by a global ratio change. Sync target: the design system's Avatar component should carry the muted/control variant explicitly, since the prototype uses it in chrome and the identity treatment in content. QRS-683
2026-08-15divergence🔴 openHero Card status dot — blink amplitude (screen composition)The design's qrDotBlink keyframe dips to 0.35 opacity; @/ui Loop type="pulse" dips to 0.6. Reusing the existing primitive was preferred over a bespoke animation on a 7px dot, and the primitive is already reduced-motion-aware (it renders static, which matters here because the dot's motion is meaningful: the design blinks live and issue and rests inactive). Colour and label still separate all three states, so no information is ever carried by motion alone and the shallower dip costs nothing but exactness. Sync target: add a pulse depth to the motion tokens, or accept 0.6 upstream. QRS-683
2026-08-15correction⚪ n/apackages/tokens (fonts.ui, fonts.display, fontMetrics.ui) + apps/mobile/src/app/_layout.tsxThe UI face is Baloo 2, and this is a correction because the DESIGN had already moved — we had not. The design system’s own tokens/fonts.css declares --font-ui:'Baloo 2','Plus Jakarta Sans',…: Baloo first, Jakarta only a CSS fallback, which RN has no concept of, so on this side it is a replacement rather than a stack. All five weights (400/500/600/700/800) by owner decision after the mandatory app-size callout; Plus Jakarta removed as a dependency. tooling/tailwind-config needed no edit at all — it derives from fonts.ui[*], which is the token package earning its keep. Measured: +1.18 MB (873 KB → 2,051 KB), ~3% of the arm64 baseline; Baloo costs ~410 KB/weight against ~93 because it carries Devanagari AND Latin in one family, which is the reason it is the right face for this product. ⚠ fontMetrics.ui moved 1.26 → 1.61 in the same change and that is the load-bearing half. RN treats lineHeight as a hard box, so the old floor under the new face would have clipped every line of body text on Android while looking correct on web and iOS (QRS-181/182). Consequence to expect rather than report: lines are ~28% taller everywhere. Nothing here needs pushing upstream — the design is right and was right first. QRS-689
2026-08-15correction⚪ n/asrc/ui/LockedFeature (new)The locked-capability presentation, and a correction rather than an ahead because the DESIGN ALREADY HAD IT and we did not. Built from prototype/mobile-console/Loyalty.dc.html’s demoState: 'locked', which is the design’s canonical treatment: a 72px lock badge, a title naming the VALUE not the restriction ("Become a QR setu Partner", never "Locked"), one sentence of value proposition, a card of perks, the action, and a plan-inclusion footnote. SCREENS.md states the rule outright for the admin template library — "a locked block renders with an upgrade prompt rather than disappearing" — so our hide-on-enabled behaviour was the divergence. ⚠ ONE DELIBERATE DEPARTURE, AND IT IS A COMPLIANCE ONE, NOT A TASTE ONE: the design draws an in-app "Upgrade to Pro" button. ADR-0002 (Accepted) makes the native app a free companion with no in-app purchase UI or CTA (Apple 3.1.3(d) / 3.1.1), so the button renders on WEB only; iOS and Android show "Plans are managed on qrsetu.com" instead. Discovery is identical on all three surfaces — badge, title, value proposition, perks and plan note — so the owner’s requirement is whole; only the purchase path differs. Sync target: the design’s locked state needs a native variant whose action is informational, or the prototype should note that its CTA is web-only. QRS-690
2026-08-22divergence🔴 openLocationPanel on the merchant console (screen composition; catalogue feature)A location control the approved design puts somewhere that is not built, shipped because without it every published vendor was invisible to every buyer. prototype/desktop-console/CardEditor.dc.html is the approved home for card configuration and has zero rows implemented; the desktop console is 6 of 20 designed screens. What forced it rather than a wait, measured on Dev: get_consumer_item_feed returned 4 items with no area filter and 0 for every one of eight cities tried, because location is an OPTIONAL onboarding step, provision_merchant_workspace was its ONLY writer, and the one field that looked like a remedy (manage-setu-card's free-text city) is not what the marketplace reads. So the choice was a minimal location control or a merchant journey that dead-ends at an empty Browse page. It sits in the same section as SetuCardLivePanel because publishing and being FINDABLE are the pair a merchant has to get right together, and both move to CardEditor when it ships — this is not a second location surface. Faithful to the design vocabulary where the design has one: the custom Select primitive (never a native dropdown — owner instruction), the design's own TONES pairs rather than opacity math on a token, and the same state-tagged city fetch the approved onboarding LocationStep uses, so a stale response is unrenderable rather than merely unlikely. AHEAD of the design in one respect, stated so it is not mistaken for faithfulness: the design has no undiscoverable state at all, so the conditional nudge is invented — a merchant WITH a city sees one quiet line and a Change affordance, and only a merchant who is actually invisible sees a warning. That conditionality is CLAUDE.md's anti-noise rule applied deliberately: a prompt that cannot be acted on, or has already been acted on, trains the merchant to ignore every prompt. The copy names the consequence ("buyers searching your city will not see your listing") rather than volunteering a reassurance, per the privacy-copy rule. Sync target: the design needs (a) a location control on CardEditor, and (b) an undiscoverable state for a published card with no city — today the prototype cannot express the single most costly state a live vendor can be in. ⚠ Web only, and native is NOT applicable rather than outstanding: this is apps/web DOM screen composition, not apps/*/src/ui/**, so ADR-0015's systemic gate does not apply — but it is also therefore unverified visually, because the merchant console is workspace-scoped and undrivable here (QRS-611). What is verified is behavioural and structural: type-check, 104 web tests, lint clean, build clean, and the RPC beneath it mutation-tested 16 ways on real Postgres. QRS-834

| 2026-09-02 | correction | ⚪ n/a | WelcomeStory consumer variant — scenes/OpeningArtConsumer, ProblemArtConsumer, HubArtConsumer, BuildCardConsumer, PhoneRunArtConsumer | The design had a whole second story and we had one. WelcomeStory.dc.html variant: Consumer — five scenes, own copy in three languages, own art. Ported from the design's own openingArt(PEOPLE), problemArtConsumer, hubArtConsumer (kinds from consumer-data.js PERSONAL_KINDS), buildCardConsumer, phoneRunArtConsumer. Aadhaar scene absent for the consumer per the design's comment. QRS-950. | | 2026-09-02 | correction | ⚪ n/a | PhoneAuthScreen (screen composition; auth feature) | Copy and states brought to the design's PHONE_COPY / PHONE_COPY_BIZ / OTP_COPY / RECOG_COPY tables. Title, purpose, WhatsApp note, code title, expired/rate/offline panels, and the RECOGNISED state (which account, complete or unfinished, corrected kind) — the design's answer to sign-up vs sign-in, which we had rendered as a silent redirect. Business/consumer copy by prop. | | 2026-09-02 | divergence | 🔴 open | phoneAuth.noWaBody | The design's noWaBody second sentence — "We will send the code by SMS instead." — and its sendSms button are NOT implemented. No SMS channel exists and a third-party provider is ruled out, so the promise would be false. Kept the first sentence only. QRS-952 holds the owner decision. | | 2026-09-02 | divergence | 🔴 open | phoneAuth.stepOf · PhoneNameScreen · business auth steps | Step labels differ from the design's "Step N of M". Consumer: shown on phone (1/3) and code (2/3), absent on the name step. Business: not shown, because the setup wizard restarts its own progress and two counters disagreeing is worse than one. stepOf Hindi/Marathi are additions where the design is English-only. QRS-957. | | 2026-09-02 | correction | ⚪ n/a | src/ui/Icon.tsx (fingerprint, backspace, heart) | Three icons the design already uses and the RN set lacked. PinGate.dc.html draws fingerprint and backspace glyphs; the consumer story's PEOPLE ring and phoneRunArtConsumer use a heart. Mapped to lucide Fingerprint, Delete, Heart per the DS icon-lucide map — the same library the existing set uses. No token change. | | 2026-09-02 | none | ⚪ n/a | src/ui/Wordmark.tsx + src/ui/useWebFontsReady.ts | No visual change when it works; a repaint fix for when it did not. The gradient id was wm-grad-${size}, identical for every wordmark at that size, and url(#id) resolves to the first match in the DOCUMENT — with inactive screens kept mounted, a second wordmark could win and leave the QR half unpainted (owner screenshot: "setu" visible, "QR" gone). Now useId. And on web the brand face loads lazily after useFonts resolves, so the inline lockup re-keys once document.fonts.ready settles — the same re-measure the design does on fonts.loadingdone. Scoped to the inline lockup; the stacked BrandSplash mark is untouched by owner instruction. | | 2026-09-02 | divergence | 🔴 open | PhoneAuthScreen — ModeTabs on the phone screen | The design removed the Sign up / Log in tabs (round 14: "the same gesture … a branch here and not a tab"); the owner put them back as FRAMING after testing. Heading and one short line follow the tab; the outcome is still decided by the account's state. Push back to the design project: a framing control on the PHONE and OTP steps, plus intent carried from the story (sign-up) and the "Already registered" link (sign-in). QRS-962. | | 2026-09-02 | divergence | 🔴 open | PhoneAuthScreen — step indicator and purpose copy | No "Step N of M" on the mobile-number screen, and the design's biodata-specific purpose paragraph is not rendered. Owner decision: the consumer persona is broader than biodata; supporting copy is one short generic line per framing (signupSub/signinSub). Resolves QRS-956/957 on the implementation side; the design still carries both. | | 2026-09-02 | divergence | 🔴 open | PhoneAuthScreen — resend policy and lock copy | 60 s resend cooldown (design: 24 s), three resends then a five-minute wait (design: unlimited), three wrong codes lock for five minutes (design copy: "15 minutes"). rateBody's fixed "15 minutes" is replaced by lockBody with the minutes parametrised. QRS-961, QRS-963. | | 2026-09-02 | divergence | 🔴 open | onboarding.welcome.scenesConsumer (all 3 locales) | The consumer story's headlines now carry *…* brand-gradient emphasis; the design renders a FLAT headline for both personas. A fresh pull of WelcomeStory.dc.html shows one <div> at color: ink with no per-word treatment. The business copy has always been marked in-repo, per CLAUDE.md's rule that the one QR-monogram gradient is the treatment for headline highlights; the consumer copy was transcribed verbatim and so shipped plain, which the owner reported as an inconsistency between the two personas. Marked on the equivalent words per language, 1-2 per headline. The divergence is from the design's flat rendering, and it is deliberate and now gated (i18n-catalogs.test.ts -> "brand-gradient highlight parity"). | | 2026-09-02 | correction | ⚪ n/a | WelcomeStory final scene (FINAL_HEADLINE_SIZE) | The consumer finale's headline now renders one step larger, which is the design's OWN rule (fontSize: sc.final ? '25px' : '23px') and the only scene weighting our story had not implemented. The business finale reads as a finale because it swaps in the brand-tagline hero; the consumer finale has no tagline — the design defines none for it, and inventing a consumer slogan is a brand decision rather than a developer's — so it had been closing on exactly the weight of every scene before it. 22 x (25/23) = 24. | | 2026-09-02 | correction | ⚪ n/a | WelcomeStory/scenes/_parts.tsx RadialHub + the three hub arts | Hub height is now DERIVED, not declared, so a ring label can no longer paint over the element beneath it. The design's hub is a flow-layout box that sizes to its content; the RN port hard-coded height: size, which is shorter than the ring plus its labels, so the bottom label escaped the box and landed on the tricolour chip (measured: 5px, on BOTH stories). Restoring auto-height behaviour is design fidelity, not a new decision. | | 2026-09-02 | correction | ⚪ n/a | BuildCard.tsx · BuildCardConsumer.tsx (offer/preferences chip) | The chip's layout moved from className to style. NativeWind drops className on a LinearGradient, so the pill was rendering flexDirection: column with zero padding on both stories — the icon above its label instead of beside it. The design's chip is a padded row; this restores it. Gated by new parity rule R10. | | 2026-09-02 | correction | ⚪ n/a | AccountTypeChoice — the "Sign in" link | The link gained a real padded box so it meets the 44x44 minimum (measured 37x19). No visual change of substance; the row's pt-1 was removed to absorb the added height. hitSlop alone could not do this — it never reaches the DOM box on RNW. | | 2026-09-07 | correction | ⚪ n/a | src/ui/Sheet.tsx · src/ui/keyboard.ts (new) · src/ui/ScreenFooter.tsx (new) | The keyboard and the system navigation, neither of which any artboard contains (QRS-1152, QRS-1153). ⚠⚠ A correction, not a divergence, and the distinction is the whole point of this row: the approved designs are HTML in a desktop preview, so there is no soft keyboard in one, no system navigation bar, and no safe-area inset. A screen can therefore be at 100% design fidelity and be unusable on a phone, and no amount of check:design-parity work would catch it — that gate enumerates the DESIGN's states, and these are states the design does not have. The owner found both on an Android build after the screens had passed review. 1 · the keyboard. Sheet computed its height cap from an iOS-only overlap, on the written premise that Android's window resizes for the keyboard; measured, android/gradle.properties:47 carries edgeToEdgeEnabled=true and under edge-to-edge it does not. So every form sheet kept its full height with the keyboard over it. The cap now reads a MEASURED overlap (keyboardOverlapOf compares the window against its height at rest) and the platform is never named — Android has changed this twice, and a measurement cannot go stale. KeyboardAvoidingView is removed in favour of one mechanism, and Sheet scrolls its body by DEFAULT because 7 of 15 keyboard sheets could not scroll at all. 2 · the bottom edge. ScreenFooter + useScrollBottomPadding replace hardcoded bottom padding on 22 screens; the inset is ~24dp gesture / ~48dp 3-button / 0 in some landscape, so a constant is wrong on most devices and wrong SILENTLY. 🔎 AND ONE HALF OF MY OWN FIX WAS WRONG AND IS RECORDED RATHER THAN QUIETLY DROPPED: I also wrote useTabScrollBottomPadding on the premise that tab content scrolls under the tab bar. Verified in BottomTabView.js — the bar is a SIBLING below a flex: 1 screen container, so it does not, and the hook would have added ~88dp of dead space to eight screens. Reverted, the hook deleted, and R14 carries a printed allowance for the eight with the navigator citation. I replaced one unverified platform premise with another unverified layout premise; only reading the navigator's source caught it. Sync target: the design owes a mobile spec for keyboard behaviour and safe-area treatment, or it stays permanently outside design parity and lives in check:parity R11-R14 plus QA sheet 19. QRS-1152 · QRS-1153 | | 2026-09-07 | correction | ⚪ n/a | src/ui/Icon.tsx — six glyphs the design names | The set is now 92 glyphs, and two earlier rows recorded it as 74 (QRS-1148). Added activity · arrowUp · arrowDown · ban · eyeOff · list, each named verbatim by an artboard fetched live 2026-09-07: FAMILY_LAYOUTS in biodata-core.js names activity (vine) and list (relationship list); Biodata.dc.html's photograph sheet names ban / eye for hide-and-show and arrow-up / arrow-down for reorder. ⚠⚠ AND IT REVERSES A DIVERGENCE THAT SHOULD NEVER HAVE BEEN LOGGED. QRS-1142 recorded substituting info for an absent alert-circle; alertCircle was in the map the whole time, under a camelCase key that the measuring regex (^\s+'?[a-z][a-z0-9-]*'?:) could not match. The substitution in PeopleSheet.tsx is reverted to the real glyph and that ledger row is corrected in place. 🔎 The generalisable failure is a COUNT OF THE WRONG THING, and it reads exactly like a measurement — this repo's own most-repeated defect, committed inside a tracker row whose only job was to record a measurement. It cost in both directions: a false divergence, and the whole family-layout picker DEFERRED as "gated on a design pull" for two glyphs already sitting in an installed package. ✅ Zero dependency cost: lucide-react-native ships 3,498 icon modules and is already a dependency, so this is six small SVG components and needs no owner size callout. ⚠ The arrow-rotation for up/down in PeopleSheet is kept rather than churned to the new glyphs: a rotated right-arrow IS an up-arrow, it was never a substitution, and it is tested. ⚠ alert-circle is genuinely no longer a lucide FILENAME (renamed upstream to circle-alert; AlertCircle survives as a barrel alias) — which is why a filename probe says absent while the import works, and is the one place the original claim was defensible. QRS-1148 |

2026-09-03 · Consumer home identity card — divergence (deliberate, two rows) ​

Screen: prototype/consumer/ConsumerHome.dc.html, section identity (pull 2026-09-03).

KindWhat differsWhy
divergenceThe unclaimed CTA reads "Claim my address", not the design's "Confirm my number"The design forks on registered, and assumes a registered person always HAS an address because it is minted at onboarding (D1's "claim at onboarding as a reservation"). Our INDIVIDUAL_STEPS has no slug step yet, so a signed-in person with no address is a state the design does not draw. Showing them a sign-in prompt would be a dead end. Reverts to the design's wording once the onboarding step lands.
divergenceNo copy button beside the addressClipboard needs expo-clipboard, a dependency this app does not carry. CLAUDE.md requires a size callout and an owner decision before installing one, so it is not installed unasked. Share opens a system sheet that already offers Copy, so the intent is served. Tracked as QRS-989.

⚠ Not recorded as drift, because they are not implementation choices: the pulse chip, the My QR setu secondary action and the showStart block are all absent for want of a schema (the biodata module, plan 1.7). They are blocked rows in parity-contracts/consumer-home.json, which is where the gap count lives: 21 of 39 pass, 10 gap, 8 blocked.

Marriage BioData · the people sheet (2026-09-07) ​

Built from prototype/consumer/Biodata.dc.html :: THE PEOPLE SHEET, extracted with npm run design:spec and with copy read verbatim out of the design's own biodata-core.js.

KindWhat differsWhy
noneUp and down controls use the existing arrow glyph rotated ∓90°, where the design names arrow-up / arrow-downNot a divergence: @/ui's arrow is lucide ArrowRight, and a right arrow rotated -90° is an up arrow. Recorded so the next reader does not "fix" it by adding two glyphs. Rotation is applied on a wrapping View because IconProps is { name, size, color, strokeWidth, fill } and has no style prop; widening that would be a systemic change for a feature-local effect.
divergenceThe per-row validation message uses the info glyph tinted danger, where the design names alert-circle@/ui's 74-glyph set has no alert-circle, and apps/*/src/ui/** is the systemic surface (design-first, ADR-0015) — so adding one needs its own design pull rather than riding along in a feature commit. info is the same circular form with a different mark inside, and the danger tint plus the message carry the meaning. Precedent: Icon.tsx already records substituting absent glyphs (QRS-648). Reverts if alert-circle is added in a systemic round.
divergenceError text is danger-strong, not the design's dangerThe token's own comment records plain danger at 3.64:1 on a soft field, under AA for an ink (QRS-240), and danger-strong is the 4.94:1 variant that exists for exactly this. The icon and the row border keep danger — a 13px glyph and a hairline are not text. Choosing the design's literal value here would ship failing contrast.

⚠ Not recorded as drift, because they are not implementation choices: the tier chip on the sheet head renders from surface-muted rather than the design's per-tier tint, because the sheet is opened from a field row that already shows the tier and the sheet has no tier-override control yet. It is a gap row in the parity contract, not a divergence.

2026-09-07 · The field sheet's controls, and TextField gaining a paragraph box ​

Built from prototype/consumer/Biodata.dc.html :: THE FIELD SHEET, re-extracted 2026-09-07 during the biodata editor audit.

KindWhat differsWhy
correctionsuggest now draws chips, then the heading "Or write it your way", then the inputThis is the design's own order and its own literal label; ours had the input first and no heading at all, so the write-your-own affordance was present but unannounced. Recorded as a correction rather than a divergence because the previous state was a defect, not a choice (QRS-1160).
correctionlong fields render a real paragraph box (minHeight: 112, top-aligned) rather than the 52dp pillTextField had a fixed height: 52 and no multiline handling, so six fields — intro at 420 characters, expectations at 320 — asked for a paragraph through a one-line window. The design draws rows="5" with padding: 12px 14px.
divergenceThe multiline box is minHeight: 112 rather than a literal five rowsReact Native has no rows. Five rows at the design's 14px/1.55 is ≈108px of text plus 24px of padding; 112 is the nearest value that leaves the box growable rather than clipped, and TextInput expands past it. A hardcoded height would clip the fifth line on a larger text scale.
noneThe character counter stays outside the box, where the design puts it inside for longNot yet implemented either way — the counter is absent from the sheet entirely. Tracked as a gap row in the parity contract rather than recorded here as a divergence.
aheadcity, mapPlace and placeOfBirth now resolve their suggestions from CONSUMER_AREASThe design has always specified this (optionsFrom: 'areas', PLACES, placeSuggestion()); the call site simply never passed the list, so three fields degraded to a bare text box. Marked ahead of nothing — it is the design being implemented for the first time.

⚠ The gate did not require this row, and that is worth knowing. check:design compares against a base ref, so a ledger row committed earlier in the same unpushed range satisfies it for every later commit — the blanket-amnesty shape check:docs-impact already had and fixed (QRS-570). This row was written because ADR-0015 asks for it, not because anything failed. Logged as QRS-1161.

2026-09-08 · The birth-date control ​

Built from prototype/consumer/Biodata.dc.html :: the date branch of THE FIELD SHEET.

KindWhat differsWhy
correctionA year pager (16 per page, 4×4) → a 3-column month grid → a real 7-column calendar, with three chips as the way back to any part and a derived-age chip in the sheet headerThis is the design, implemented for the first time. The previous control was three horizontal pill strips — 58 years, 12 months, 31 days — with no chips and no age (QRS-1162).
divergenceThe pager arrows use chevronLeft/chevronRight from @/ui where the design draws its own inline SVG chevronsSame glyph, same direction; apps/*/src/ui/** is the systemic surface, so adding a bespoke chevron for one control would need its own design pull. Precedent: the arrow-rotation row above.
noneThe day grid is a flexWrap row of 100/7-width cells rather than CSS grid-template-columns: repeat(7,1fr)React Native has no CSS grid. The rendered geometry is identical, including the leading blanks that put day 1 on its real weekday.
divergenceA month label (March 1994) sits under the day gridThe design puts the month in the chip only. Added because the calendar is otherwise seven columns of bare numbers with no statement of which month is being read — and the chip is above the grid, off the thumb's line of sight on a phone. ⚠ Flagged for the design round rather than assumed correct.

⚠ One measured observation for the design, not a divergence: the design anchors the opening year page on maxYear - 12, so page one is ages 28 to 43 and a 25-to-27-year-old must page forward once. Implemented faithfully and pinned by a test; raised as an observation in the drawer prompt rather than "improved" here.

2026-09-08 · The family's own fields (custom fields) ​

Built from prototype/consumer/Biodata.dc.html :: THE more SECTION (:419-457) and ADD A FIELD (:720-856), spec extracted element by element before any code was written.

KindWhat differsWhy
correctionThe whole surface exists: the more section's empty state, its dashed add control and cap line, the sheet, and the saved rowThis is the design, implemented for the first time. The section rendered an empty card because BiodataRecordModel had no custom member (QRS-1167), while the domain module, the Edge Function's validator, the cap CHECK and the public projection were all already in place.
correctionThe refusal text is danger-strong, not danger⚠ An accessibility correction, measured. The artboard sets it to var(--danger), which is 3.64:1 on danger-soft and fails WCAG AA. packages/tokens carries danger-strong at 4.94:1 for exactly this pairing, with that measurement in its own comment (QRS-240). The fill is unchanged.
aheadThe sheet scrolls the validation error into view when it appearsThe error block is last in the scroller, below the live preview, and the two typing targets are at the top and middle. With a soft keyboard up a refusal can be entirely off-screen, so the only visible reaction to pressing Save would be that nothing happens. An HTML artboard has no keyboard, so the design cannot specify this and check:design-parity can never catch it (QRS-1152).
noneThe footer sits outside the single scroller; Sheet supplies insets.bottom beneath itThe design's own three-band flex column (flex:none / flex:1; min-height:0 / flex:none). Its padding-bottom: 28px is an artboard constant and is deliberately NOT used as the inset (R12).
nonerows="4" on the long-text box became minHeight of four lines; resize:none has no RN equivalentDOM properties. The rendered intent, four lines and no user resize, is identical.
divergenceTier chips use @/ui Icon glyphs at 12 and the badges at 10, where the design inlines its own SVGSame glyphs, same sizes. apps/*/src/ui/** is the systemic surface, so a bespoke icon for one control would need its own design pull. Same precedent as the pager chevrons above.

⚠ Three things the design does NOT have, so they are NOT built — each is a tracker row rather than an invention: no presentation picker (QRS-1172), so four of the five presentations are unreachable by a family and the row's note line is the concept's only surface; reorder is global rather than per section and the arrows never disable (QRS-1173); and the sheet's Remove does not confirm while the row's trash icon does (QRS-1174).

⚠ Two observations for the design round, faithfully implemented rather than "improved": the artboard uses the string "Add a field" for both the add control and the sheet title, so the two are indistinguishable by name; and suggestion 8 is gendered and hardcoded ("Something about her day"), which Marathi can carry and Hindi structurally cannot (QRS-1175).

2026-09-08 · Two corrections to the custom-field row, both owner-decided ​

Built from prototype/consumer/Biodata.dc.html :: the saved custom-field row (:429-448).

KindWhat differsWhy
correctionThe reorder arrows move a field within its section and are disabled at the section's endsThe artboard splices the WHOLE custom list and sets canMove: true unconditionally. Two visible consequences: pressing Down on the last field of one section swapped it with a field in ANOTHER section, and since each section renders only its own members, neither row appeared to move; and an out-of-range press returned early with no toast, which is indistinguishable from a dead control. Owner decision 2026-09-08 (QRS-1173). The record's global order is still what is stored and read — the fix SWAPS two same-group members in place.
correctionThe sheet's Remove now opens the same confirmation the row's trash icon doesBoth controls are drawn and only one confirmed, so a single destructive action had two safety levels depending on which control a family reached it from — and the unconfirmed one sits beside Cancel at the bottom of a sheet, where a mis-tap is likeliest and there is no undo. Owner decision 2026-09-08 (QRS-1174). The dialog is REUSED rather than duplicated: one destructive path, one sentence.

⚠ A third question was NOT fixed in code, deliberately (QRS-1172). The artboard has no presentation picker — presentationsFor has zero callers and the presentation is derived from each type's default — so four of the five presentations are unreachable by a family. Building a picker would be inventing a control the design does not have, which is the one thing the design-first rule exists to prevent. It goes to the design round instead.

2026-09-08 · The in-app reader, the preview card and the theme strip ​

Built from prototype/consumer/BiodataView.dc.html (round 39, fetched fresh — it was absent from the local mirror) and prototype/consumer/Biodata.dc.html :: SEE IT AS THEY SEE IT and HOW IT LOOKS. Both artboards were enumerated element by element by a read-only agent that never opened the implementation, so implementation assumptions could not leak into the spec.

KindWhat differsWhy
correctionThe whole reader exists, plus the hub's preview card and theme stripThis is the design, implemented for the first time. The owner reported the preview as missing and it was: biodata-view had zero hits in the mobile tree, and ScreenHeader's actions slot had been declared and unused since it was written (QRS-1179).
correctionEvery section row is labelled through the catalogThe artboard hardcodes nine English labels for the expect* rows, bypassing fieldLabel and therefore translation, so a Marathi profile would show English in that one section while every other row was translated (QRS-1185).
correctionCorrect pluralisation, and no counter at zeroThe artboard renders 1 lines for a one-row expectations section and 0 lines beside a fully withheld one. A count of zero next to a notice explaining the release is noise (QRS-1185).
correctionNo pronoun fallback on the expectations headingThe artboard falls back to the literal she when no first name resolves: a guess about the subject's gender, wrong for a groom and wrong again when the owner IS the subject (QRS-1185).
correctionError and refusal text uses danger-strong, not danger3.64:1 against 4.94:1 on danger-soft. The token exists for exactly this pairing, with the measurement in its own comment (QRS-240).
divergenceHub rows 2 and 3 open the in-app reader at a tier, where the artboard opens the public DOM page with a scenario parameterThree measured reasons, none of them preference: the reader's own ?viewer=basic renders the RECIPIENT experience and offers the owner "Ask to see more" about their own daughter, so the design has no mechanism for what the rows promise; the public page needs the web deploy and devv.qrsetu.com/<slug>/biodata 404s (QRS-1131); and the released page needs a minted share token, so previewing it would mean minting a release for an audience of one. The row copy already promises the in-app answer: "with the tier switchable from inside" (QRS-1177).
divergenceThe owner band always renders for the owner, and the link only chooses the tier it opens onSame finding. In the artboard "open at a tier" and "see the owner band" are mutually exclusive.
aheadThe reader shows its skeleton while the overview is still resolvingNot a design state that was missing — a defect the design cannot express: a disabled TanStack query reports isLoading === false, so the first implementation rendered READY against empty data (QRS-1180).
noneThe theme strip is a wrapping 3-column gridIt is a grid in the artboard too (repeat(3, 1fr)), NOT a horizontal scroller, so it introduces no second scroll owner and R11 holds by construction.
noneSix of the twelve looks are computed rather than declaredThe design emits them as color-mix(in oklab, …), which React Native has no equivalent for. biodataTokenColor mixes from the SAME two token references, so the six derived hues still flip with the theme — which is the one property the design's own comment says a derived hue must keep.
divergenceThe owner band's People action is a line of text, not a button, while People and access is unbuiltThe artboard draws two 44dp buttons side by side. consumerKindHref returns null for view: 'people' precisely so no caller pushes to the CONTENT hub and hands the owner a screen that cannot do what the label just promised. The COUNT stays (real information the owner wants while previewing); the AFFORDANCE goes. A disabled grey button was rejected for the reason CLAUDE.md gives: it is visible and it teaches nothing. It becomes a button again in the same change that lands the screen (QRS-1189).
divergenceThe removal card carries no "Remove it now" button, and its first step reads "has already stopped"Measured, not preferred: get_public_biodata returns closed: 'removed' on every read the instant removal_requested_at is set, and publish_biodata_profile refuses while it stands. Service has already stopped and the owner cannot publish around it, so the artboard's button offers to do something already done — which implies the profile is still being served. The 72 hours is the erasure runbook, not the service stop (QRS-1194).
divergenceThe request card renders a short local date where the design writes relative prose ("two days ago")A client-computed relative label goes stale while the screen is open, needs its own clock, and needs pluralisation in three languages. A date needs none of it. Recorded as a gap too, because the design may prefer the warmer form and should decide (QRS-1198).
noneFive icon substitutions on the people view: verified→shieldCheck, check-circle→check, sparkle→spark, x→close, user-plus→usersNone exists in @/ui's registry and each substitute carries the same meaning. shieldCheck is the closest honest analogue for "confirmed by one code" — it reads as checked, which is exactly the claim the label makes and no more.
divergenceThe opening preview's panel is a flat accent-soft wash where the artboard sets radial-gradient(130% 105% at 50% 0%, accent-soft, surface-raised 76%)React Native has no radial-gradient, and expo-linear-gradient cannot express a radial one either. Adding a dependency for one panel needs an owner callout under the app-size policy, so the flat wash ships and the gradient is a note rather than a silent omission.
divergenceThe opening composer offers three routes, not four: collection is absentMeasured on both sides (QRS-1200): BIODATA_EMBLEMS' src is a citation not an asset path, there are zero emblem images in the repo, BIODATA_DRAWN_ART_IDS is ['om'] alone and that vector is not drawn, and the artboard renders collection art as a design-time <image-slot> — so the design ships no art either. One honest line stands where the route would be, naming what does work.
divergenceThe kuldaivat offer has two choices, not three: "Use our <name>" is absentThe only one of the three that needed collection art. "Add my own picture of it" and "Just the name" are both real, so the composer still opens by reading the family's own answer, which is the behaviour worth protecting.
noneFive ladders of design sizes (art 84/70/58, line 20/15/13, name 16/13/12, max-width 250/124/92, gap 0/18/12) are tables rather than ternariesSame numbers, clamped to 1..3 because BIODATA_MAX_OPENING is 3. sonarjs/no-nested-conditional was right that three-arm ladders read as noise, and a table shows the design's numbers as the data they are.
aheadThe forwardable row's note reads the real open_cities column and falls back to a no-city sentenceNot a design state that was missing, but a truth the design could not know: the column exists, nothing writes it, and its own COMMENT says the client renders without cities. Reading it means the richer note appears with no client change once a writer lands (QRS-1197).
divergenceThe biodata editor's label language is persisted per record; the artboard keeps it in component state, so it resets on every openDecision D-z, taken deliberately. onPick: () => this.setState({ uiLang: l.id }) is right for a prototype and wrong for this product: a biodata is filled "over days, by a family, in whatever order the answers arrive" — the design's own words — so a family who switched the labels to Marathi on Tuesday should not find them in English on Wednesday. Keyed by BIODATA ID rather than one shared value, because PLAN.subjectCap is an entitlement cap and not an invariant, and a shared value would relabel a second profile the moment that cap moved. ⚠ Device-local, and honest about it: persisting server-side would be a migration plus an Edge Function change while this whole slice is client work, so the choice does not follow the family to a second handset. When the record gains a column this store becomes its cache and the keying does not change.
aheadThe label language is delivered by a scoped translator (LabelLanguageProvider in apps/mobile/src/i18n.ts) rather than a uiLang parameter threaded into each resolverNot a visual divergence: the rendering is the design's. The design carries its own per-language tables (LABELS.group[id][langId]) and passes uiLang into fieldLabel, groupTitle, sectionStatusLabel, stakeLabel and metrics; in this repo the i18n CATALOGS already are that table in all three languages, so the lookup was already right and only its BINDING was wrong. Overriding the binding for one subtree changed zero call sites; threading a parameter would have touched twenty components and left every new one free to forget it. ⚠ The design needs no change, but it is recorded as ahead because the mechanism is now a reusable app capability the design has no opinion on — a Setu Card theme editor or the invitation builder could want the same thing.
noneWrittenInRow renders a plain View rather than a PressableScale when the content language is EnglishSame pixels either way. biodataScriptHelp returns null for English, so there is no sheet to open, and a pressable with no effect (or a disabled one) would be the fifth rule's exact failure: visible, and teaching nothing. The chevron is present exactly when there is something behind the chip.
divergenceThe Account profile card's identity line prefers the EMAIL and falls back to the PHONE; the artboard sets emailLine = acct.email unconditionallyMeasured, not aesthetic. The consumer spine is WhatsApp OTP first with Google as the fallback and email never, so auth.users.phone is set and auth.users.email is NULL on a phone-only account (probed 2026-09-01 — it is the same measurement that made sessionStore.userId rather than email the signed-in predicate, because if (email) was false for every WhatsApp user and the entry gate sent them to sign-in on a loop). The artboard's demo account has an email, so the design never had to answer this. Rendering an empty line under the name for MOST consumers would be worse than either field, and the design itself already prints the phone as the details row's value, so the phone is its own answer rather than an invention. A test pins the phone-only case.
divergenceThe Account avatar is Avatar's accent-soft fill where the artboard sets background: var(--brand-gradient)The 64px rounded-square identity avatar is the @/ui Avatar primitive, consumed rather than re-drawn, which is the shared-chrome rule — re-implementing initials here would make it the second implementation of an existing component. The primitive fills with accent-soft, and changing it is a SYSTEMIC-surface change under ADR-0015 (a design pull first, and it would move the merchant Profile and More avatars at the same time). So the divergence belongs to the primitive, is recorded once here, and is not silently re-decided per screen. expo-linear-gradient is already a dependency, so this is a design-governance step rather than a technical one.
noneAccount's five unbuilt views render a "not ready yet" state naming the section, where the artboard draws each view in fullNo visual divergence in a view the design has: these are views this build does not have at all. It replaces a silent redirect to the hub (params.view === 'notifs' ? 'notifs' : 'hub'), which sent a deep link, a notification tap or a back-navigation to the wrong screen with no signal — the fifth rule says render the state on the route and never bounce. It carries no plan language, because this is the AVAILABILITY axis and nothing here is purchasable.
noneFour of the design's Account hub rows and the merchant pitch card are ABSENT rather than drawnThe design draws them and they had no destination: five SettingRows and the primary Button were rendered with no onPress at all, which SettingRow shows as a flat row with no chevron. A control that goes nowhere is the fifth rule's exact failure and worse than an absence, because an absence teaches nothing while a dead row teaches something false. Each is a blocked row in consumer-account.json with the unit that unblocks it, and each lands WITH its destination. ⚠ Your areas is absent for a different reason and must not be lumped in: it is marketplace, so D-a makes it permanently absent rather than pending.
divergenceAppearance's footer note reads "Labels change here; what you write stays in your own words" where the artboard says "Shops write their own listings, so some may still read in English"The first sentence ("Prices always show in rupees") is kept verbatim. The second names a surface that is ABSENT at launch: the marketplace is off (D-a), so there are no shop listings to explain. The RULE behind the sentence is live and matters more on this product than on the one the design was describing — what a family writes is never translated — so the sentence points at what exists. It also now echoes the biodata editor's own app-language note, which is the same promise on the surface the launch actually ships. Reconcile by asking the design for a marketplace-off variant.
divergenceHelp's accordion chevron rotates STATICALLY; the artboard animates it over 200ms with cubic-bezier(.2,.8,.2,1)There is no chevronUp in @/ui, and adding an icon is a SYSTEMIC-surface change (ADR-0015 makes src/ui/** design-first), which is far too much to spend on an accordion. A plain RN transform: [{ rotate }] needs no Reanimated, renders identically on Android, iOS and RNW, and is visible at rest — so the open/closed state reads correctly on every surface. The missing transition is the whole of the divergence, and it is invisible in a screenshot.
noneHelp ships TWO of the design's five FAQ questionsNot a visual divergence: three of HELP_TOPICS answer marketplace questions ("collect an order", "is my advance refundable", "something is wrong with a listing") and the marketplace is off, so printing them would send a consumer looking for an Orders screen that is not there. An FAQ that describes absent features is worse than a shorter one. The two that ship carry the design's copy verbatim, and a test asserts the other three ids stay absent. The launch product's own questions are a content request (QRS-1217), deliberately not invented here.
noneHelp's "Message QR setu support" CTA is absentTwo independent reasons, each sufficient, and both already settled elsewhere in the repo. The artboard links ./Chats.dc.html, but a consumer messaging support is a NON-WORKSPACE counterparty and conversations.workspace_id is NOT NULL (QRS-1195). And the number is configuration: BiodataViewScreen already declined to hardcode a support line, calling it "the QRS-992 shape applied to a phone number", so inventing a second answer here would contradict a recorded decision. ⚠ QRS-1215 records that a PLACEHOLDER helpline did reach the biodata sheet, which is the same mistake made in the other direction.
noneAbout's three legal rows (Terms of use · Privacy policy · Open source licences) are absentMeasured, not assumed: apps/web/src/app/routes.ts has no /terms, no /privacy and no /licences, so every row would open a 404. A link to a 404 is worse than an absent row — the same rule as a SettingRow with no onPress, applied to a web URL. The pages are launch-blocking in their own right (QRS-1216), and the rows land with them.
divergenceAbout prints the app's REAL version from app.json; the artboard prints 1.4.0 · build 2026.08The artboard's APP_META is demo data and app.json is the version SSOT (QRS-289). A false version in front of somebody reporting a bug is the one thing this line exists to prevent, so a test asserts the design's numbers are ABSENT. ⚠ currentVersionCode() returns 0 as its "unknown" sentinel on Expo Go and some web paths, so the build half is omitted rather than rendered as "build 0".

⚠ Absent by decision, each with a tracker row rather than a silent omission: the in-app RECIPIENT half and its eight blocks (QRS-1181) · the family drawing, whose module is not in the mirror (QRS-1182) · the growth CTA, which needs a brand glyph on the systemic surface and a configured support number (QRS-1183) · the tab bar the design draws on a pushed screen (QRS-1184) · the ornament, which nothing in the mirror draws (QRS-1186) · and the hub card's fourth row, "The WhatsApp preview", which previews a LINK UNFURL and would need the OG pipeline to be truthful.

⚠⚠ ONE DISCLOSURE DEFECT IN THE APPROVED DESIGN, ESCALATED RATHER THAN IMPLEMENTED (QRS-1176, P1). The reader's headline gates the name by tier correctly, and the standing chip beside it reads fullName ungated — on the forwardable basic link the design itself calls "survivable when it lands with a stranger". It cannot reach a family through this implementation, because get_public_biodata projects server-side by tier and a basic payload does not CONTAIN the field. That is the architecture working rather than luck, and it is precisely why the projection lives in the database: "a renderer mistake is a disclosure instead of a layout bug."

2026-09-10 · The round-41 reader: the viewer's zoom, the swipe track and the opening block ​

Round 41 answered the round-4 prompt (QRS-1249) and three behaviours were implemented from it. Two divergences, both because RN has no equivalent of what the artboard uses, and one deliberate omission the design itself declares.

KindWhatWhy, and what it costs
divergenceThe zoom control's glyph is search (a magnifier), not the artboard's icoExpand@/ui's icon set has no expand or maximise glyph. Adding one is a change to the SYSTEMIC surface and needs its own design pull under ADR-0015, which a single control does not justify on its own. A magnifier is the conventional zoom affordance and reads correctly beside a 1.8x label. Needs sync back: yes, if the design wants the exact glyph, it should either name an existing one or add it to the component library.
divergenceThe opening block's wash is a LINEAR gradient, not the artboard's radial-gradient(130% 105% at 50% 0%, ...)expo-linear-gradient has no radial mode, and pulling in a radial-capable dependency for one background is not a trade worth making under the app-size rule. The approximation uses the same two colours on the same top-to-bottom axis the photograph hero already uses, so the two blocks agree with each other. Needs sync back: no, unless the radial falloff is load-bearing, in which case the design should say so.
aheadPinch AND a stepped control, where the artboard implements steps aloneThe artboard's own comment asks for this: "The stepped control is this prototype's stand in for pinch: it is also the keyboard path, which pinch never is. Production keeps both." Both land on the same three stops, so a photograph can never rest at an unlabelled scale. Needs sync back: no, it is what the design asked for.
noneThe drawn collection emblems render their NAME and no artNot a divergence: the design's own editor says "The picture collection is not ready yet. The artwork is being sourced and credited properly. Your own picture and a line of words both work today." emblem-art.js is unported and porting it is its own piece of work. Recorded here so nobody reads the absence as a defect.

⚠ The zoom is a disclosure control and not a preference. photoZoomable={tier === 'released'}, because "the tiers are served different files": a basic reader holds the 720px derivative, which exists so a forwarded photograph is low value. It is mutation-tested — forcing it true fails exactly the basic-reader test.

Four device findings from the owner, all on the marriage biodata. Three were defects measured against the artboards and are not drift; one is a deliberate reversal of a written design refusal and is recorded here because it is exactly what this ledger exists for.

KindWhatWhy, and what it costs
divergenceThe reader's photograph carousel advances on its own every 3 seconds. The design was ASKED for autoplay in the round-4 prompt and REFUSED it in writing: "A marriage profile is a document a family reads, and the identity lines sit directly under this frame, so nothing here advances under the eye: the reader swipes, or taps a dot."Owner decision, 2026-09-10, taken after the refusal was stated. The interval is AUTOPLAY_MS in AboutBlocks.tsx and the constant carries the design's words verbatim so the next session cannot read it as a mistake and delete it. Cost, stated plainly: the design's objection is a real one about reading a document while its illustration moves, and it is now our position rather than theirs. Mitigations kept: it PAUSES while the full-screen viewer is open (otherwise closing the viewer would drop the reader on a different photograph), it is OFF entirely under reduced motion, and the timer re-arms on a manual swipe or dot tap so a reader who takes control is not fought. Needs sync back: yes. The design should be told the refusal was overridden so round 42 does not re-argue it, and so the artboard and the build stop disagreeing.
divergenceManual swipe does NOT wrap at either end; autoplay DOES wrap from the last photograph to the firstA drag past the last photograph should refuse like every other carousel, but an interval that refused would stop after one lap and read as broken. Needs sync back: no, it follows from the decision above.
noneThe link card's third action, "Save as a picture", is still absentNot new drift: rasterising the card needs react-native-view-shot, which is not installed and carries an app-size decision the owner has not taken (QRS-1065). The other two actions work, so the block is useful without it; a button that cannot save would not be.
noneThe reader's "The WhatsApp preview" row is still absent from "See it as they see it"Not drift either, and not a client gap: the artboard's row selects a view on the PUBLIC page, and apps/web's biodata route serves no og: tags at all (measured 2026-09-10 — og:image appears only in setu-card.tsx and the landing SEO module). A mock preview in the app would promise a card WhatsApp will not render, which is the "handled elsewhere is a claim" shape. It needs the OG tags first.

⚠ Two things corrected in the same pass were NOT drift, they were defects against the artboard, so they are logged in the tracker rather than here: the link card's action row had lost Share on WhatsApp and had gained an accent-filled Share with someone the design does not draw (QRS-1253), and the reader's growth CTA was being withheld on two blocked reasons that had both already expired (QRS-1254).

Two owner reports: the WhatsApp preview was a bare link, and the reader showed no family at all. Both are now built. Three divergences, each measured rather than assumed.

KindWhatWhy, and what it costs
divergenceThe og:image card drops the artboard's dark veil and sets its label in DARK ink, where the design lays a linear-gradient(to top, var(--overlay), transparent) under --surface (white) textThe artboard's label sits over a photograph, and the veil is what lifts white text off it. This card has no photograph — the design's own rule forbids one — so its ground is surface-raised: the veil darkened near-white to light grey and white-on-that was all but unreadable. Found by rendering the PNG and looking at it, which is the only way that defect shows. Same decision the design made, on a different ground. Needs sync back: no.
divergenceThe card is one of twelve pre-rendered PNGs, not a live render, and it is themed but otherwise identical for every profileWhatsApp does not render SVG, so something must rasterise; a wasm rasteriser inside the Worker is a real dependency and a per-request cost for an image whose only variable is one of twelve fixed washes. Committed PNGs are one cache entry each on Meta's side and cannot fail at request time. Regenerate with node apps/web/scripts/generate-og-cards.mjs. Needs sync back: no.
noneThe family portraits show initials where the design draws a person glyphNot new drift: the DOM renderer already carries this placeholder and the RN one was written to match it deliberately, so the two surfaces cannot diverge while a shared in-SVG glyph set does not exist. The design's reason for a glyph is sound ("two Devanagari letters read as noise at this size") and is why biodataInitials walks to the first differing character for two people who share them. Recorded so the absence is not read as a defect.
noneThe drawn family frame uses a linear gradient where the design uses radial-gradient(120% 80% at 50% 0%, ...)Already-recorded shape: expo-linear-gradient has no radial mode and OpeningBlock carries the same row for the same reason. Same two colours on the same axis as the photograph hero, so the blocks agree with each other.

⚠ The family MODEL is no longer drift-prone by construction. It was built inside apps/web, which is why the reader could not draw a family at all (QRS-1256); it now lives in @qrsetu/domain/biodata/family and both surfaces call it. The design's own module states the rule this satisfies: "a drawing that exists twice drifts."

2026-09-11 (second pass) · The reader's last blocks, and one honest label change ​

Four designed blocks were missing from the in-app reader and the owner found them one at a time. All are now built. Two divergences, both because the artboard names a capability this build does not have, and one assessment that came back clean.

KindWhatWhy, and what it costs
divergenceThe talk card's second action is "WhatsApp", where the artboard says "Message" and points it at an in-app conversationA consumer-to-consumer conversation has no model: conversations requires a workspace (ADR-0032), and decision D-v's R1 answer is "route the in-app talk action to WhatsApp on the released number, as the public page does". The artboard's own chatHref is the placeholder https://qrsetu.com/app/chat, so it is not wired there either. A button labelled Message would name a capability that does not exist. Needs sync back: yes — the design should either wire the chat or rename the control.
divergenceThe talk card's footnote reuses biodataPage.repNote instead of the artboard's own sentenceThe artboard promises "Messaging keeps the conversation inside QR setu, where they can see who they are talking to." This build cannot keep that promise, and the PUBLIC page already ships an honest replacement: the number was given for this profile and stops working if the family withdraws. One sentence, both surfaces. Needs sync back: yes, with the row above.
noneThe Keep control shows for the OWNER and refusesNot drift: the artboard draws the row unguarded and its own onKeep refuses for an owner with "This is your own profile". Hiding it would stop this screen being a preview of what a recipient sees, which is its whole purpose.
nonecheck where the artboard draws check-circle on the kept state@/ui's icon set has no circled variant; adding one is a change to the SYSTEMIC surface needing its own design pull (ADR-0015), which one control does not justify.

⚠⚠ THIS ROW SAID "SECTION ORDER IS NOT DRIFT". IT IS CORRECTED, AND THE CORRECTION IS THE POINT (QRS-1262, 2026-09-20).

It read: "SECTION ORDER WAS ASSESSED AGAINST THE ARTBOARD AND IS NOT DRIFT. The owner felt 'What X is looking for' sat too high in the document. The artboard's own base.sections is work → expect → community → kin → kundli → more, and the implementation matches it exactly … Moving the section is a design decision and needs a Claude Design round, not a code change."

Every measurement in that paragraph was true of design round 41, and the conclusion drawn from it was too strong. Round 55's biodata-view.js exports READING_ORDER = [opening, identity, photos, intro, facts, story, family, community, kundli, custom, **looking**, talk] and an orderOk() asserting identity before looking, family before looking, and looking before talk — with the reason stated in the design's own words:

"A biodata is read as an introduction, so the page must answer WHO THIS IS before it answers WHAT THEY WANT. Putting looking high reverses that: the reader is asked to judge a match against requirements before they have met the person, which reads as a demand rather than an introduction."

The owner was right, and the design moved to agree with them. Our order failed the design's own assertion from round 55 until 2026-09-20.

🔎 THE GENERALISABLE DEFECT IS THE ROW, NOT THE ORDER. A "no divergence found" record is a claim with a shelf life, exactly like a blocked verdict. Its whole purpose — stated in its own last sentence — was to stop the next person re-measuring, which is valuable when the underlying thing is settled and actively harmful when it is not: it converts a one-time measurement into standing guidance that outlives its evidence. This one was pinned against a design still in motion, so it would have kept a future reader from checking exactly the thing that had changed.

⚠ Two rules follow, and they apply to every none row in this ledger. (1) A no-divergence row carries the design round it was measured against and is re-checked on the next pull like any stale contract. (2) When an owner's product instinct disagrees with the current design, report "the design currently says X, measured at round N" — never "your concern is a design question". The first invites them to push on the design; the second closes it. This is the second time their instinct led the design (the photo-autoplay reversal, QRS-1251, was the first).

Fixed, not merely re-assessed. BIODATA_READING_ORDER now lives in @qrsetu/domain/biodata/view and BOTH surfaces consume it: the in-app reader sorts its blocks by slot rather than listing them, and apps/web's public page maps over the same array instead of laying blocks out in JSX. Two tests read the rendered order back — BiodataPage.order.test.tsx from the HTML's data-slot stamps, and the reader's own suite from READER_SLOT_ORDER — both asserting biodataOrderOk and both mutation-proven against the order that shipped before. Sync target: none; the implementation now matches round 55.

⚠ One ordering divergence remains and is stated rather than quietly fixed: the public page's place block has no slot of its own in READING_ORDER — the design carries the map inside family.mapPlace — so it now renders with the family block rather than between custom and talk. That is nearer the artboard than before, not further, but it is a judgement and not a transcription.