loupe

Control: a well-built web app

Split the check

A bill-splitting calculator built to be good, checked by a second model that found no Blocker and no Major. The question here is whether loupe invents problems.

Input
Running page in Chromium (HTML, CSS and JavaScript source available)
Fixture
evals/heldout/fixtures/web-good

Claude Opus: loupe’s report

Blind judges: 1 of 3 preferred this report, 2 preferred the one written without loupe.

<!-- model: claude-opus; skill commit: 7559367 -->

Split the check: design review (loupe)

1. Summary

What was reviewed: the whole single-screen web app splitcheck/index.html ("Split the check"). It was driven in headless Chromium in its empty, populated and stressed states at 390, 820, 1440 and 2560 px, in light, dark, forced colours and increased contrast, with the source read alongside. No .loupe.md exists, so the review ran on the default profile below.

Verdict: a careful, accessible build. Contrast, target size, labels, error wiring and reduced motion all pass when measured, and the voice and the "= Each person pays" equation layout are its own. Its weak points are the money rule for unassigned items and the recovery paths: Undo, focus after a list rebuilds, and editing what was entered.

Do first:

  • #1: with any item unassigned, the whole receipt's tax is charged to the people who are assigned, and the sum is labelled "Check total". In the test, Ana paid $60.00, including the full $10.00 tax, on a $110.00 receipt.
  • #2: Undo is 30 Tab presses from where focus lands, and any other edit removes it.

Assumptions (the owner was unavailable, so nothing here was confirmed):

  • platforms: web, phone-first. The app has a 390 px layout, inputmode="decimal" and 44 px targets.
  • release_stage: development, as stated in the request.
  • audience: US diners at the table, splitting one restaurant check on one phone, occasionally. Inferred from the hard-coded $ and the en-US toLocaleString.
  • register: utilitarian, inferred because this is a money task people check and act on.
  • voice: sentence case (as the existing strings use), US spelling, humor: none, we_policy: avoid. Contractions are not used now, so contractions: per-voice.
  • density: comfortable, measured against the 16 px web body size.
  • category: bill splitting. peers: none given. direction: none. The distinctiveness pass therefore proposes directions (section 5).
  • .loupe.md was not written.

Questions I would have asked (unanswered, so the defaults above were used):

  1. In one sentence, what is the product for, and what should it leave people feeling?
  2. Who uses it, on what device, and is anyone outside the US or using another currency?
  3. Should an unassigned item block the result, be shared by everyone, or be held aside as "unassigned"?
  4. Which products do you measure yourselves against?
  5. Is it deliberate that last session's dinner is restored from localStorage on the next visit?
  6. Is there a brand direction or brand asset to work from?

Run notes

  • Tools: the Playwright MCP was not available. I drove Chromium with Node Playwright scripts in work:
    • run.js ran measure-runner.js, passed unchanged with page, and type-eval.js.
    • stress.js, toast.js and fc.js drove the stress states, keyboard paths and forced colours.
    • spec.js ran the candidate-face specimen.
  • Raw output: …/work/measure.json and …/work/type-candidates.json.
  • Screenshots are in …/work/shots/:
    • Populated state: full-{390,820,1440}-{light,dark}.png, fold-*.png.
    • Empty state: empty-*.png.
    • Long strings: long-390.png. Pseudo-localised: pseudo-390.png. Right-to-left: rtl-390.png.
    • 200% text: text200-390.png, text200-1440.png, text200-item.png.
    • Forced colours: forced-1440.png, forced-presets.png. Increased contrast: contrastmore-1440.png. Wide window: wide-2560.png.
    • Many diners: sticky-12people-1440*.png. Undo toast: toast-{390,1440}.png. Candidate faces: type-candidates.png.
  • Measurement summary (12 combinations of width, colour scheme and reduced motion, populated state; none sampled):
    • Contrast: 92 text rows, 0 fail, 0 unmeasurable, in every combination. The empty state gave 27 rows, also with no failures.
    • Targets: 16 rows under 24 px, all the custom checkboxes at 22×22, with spacingOk: true. Each sits inside a label.pick that is at least 44 px tall and is the real hit area, so this is not a finding.
    • Motion: no transition: all, nothing animates on load, and a reduced-motion rule is present. No unreadable stylesheets.
    • Template tells (defaults): none.
    • Text sizes: 8 distinct sizes, smallest 13 px (the "Group dinner" kicker).
    • CSS direction: 3 physical inline-axis declarations, 0 logical.
    • No iframes and no shadow roots.
  • Side effects:
    • localStorage was cleared and reseeded, only inside a throwaway headless browser profile.
    • DOM edits made for the stress states (pseudo-localisation, dir=rtl, 200% root font size) were undone by reloading.
    • The review made web requests: web searches and fetches for the landscape, and Google Fonts for the candidate-face specimen.
    • No .playwright-mcp/ folder was created. No project files were edited.

2. Findings

# Finding Where Impact Apple Fix
1 Unassigned items distort the money.<br>• compute() drops an unassigned item from the subtotal but still spreads the full tax (allocate(st.taxCents, base)) over the assigned items.<br>• The total of the assigned items is labelled "Check total".<br>• Test with two diners: Steak $50 assigned to Ana, Salad $50 unassigned, tax $10. Ana is shown "$60.00" (items $50 + tax $10) and "Check total $60.00", on a $110.00 receipt.<br>• In the seeded state (fold-1440-light.png), Tiramisu ($9.00) is unassigned and "Check total $88.71" is shown beside "Copy summary".<br>• The red "Not included" note is easy to read past, and Copy works anyway.<br>• This breaks the product's own lede promise ("Every share adds up to the exact total"). Result panel .bill; compute() lines 279–309; renderResult() Major Research • Split tax in proportion to the assigned share of the full item subtotal, and hold the rest as an "Unassigned" row with its amount.<br>• While anything is unassigned, title the sum "Assigned so far, $X of $Y". Use "Check total" only once every item has someone.<br>• Have Copy add a first line: "Not final: Tiramisu ($9.00) has nobody yet."<br>• Which policy to adopt (block, share among everyone, or hold aside) is a design decision.
2 Undo is hard to reach, fragile and silent.<br>• After Delete, Remove or Start over, focus goes to the Item or Name field. The toast's Undo is the last element in the DOM: 30 Tab presses away with 5 people and 4 items, measured at both 390 and 1440.<br>• save() clears undoSnap and the toast on any other edit (typing tax, ticking a box), so Undo disappears when you start correcting.<br>• After Undo, focus falls to <body> and the restored row is not shown. The only feedback is a screen-reader-only "Restored." #toast, destructive(), save(), undoBtn handler, lines 333, 354–357, 426–428 Major SHOULD • Keep focus on the control nearest the removed row (see #3), and make Undo reachable from there. One way: move focus to the toast's Undo, labelled "Undo remove Wine", with Escape to dismiss.<br>• Also support Ctrl/Cmd-Z.<br>• Don't clear the undo snapshot on unrelated edits. Keep a short stack.<br>• After Undo, focus the restored row and scroll to it.<br>• Apple's Undo pattern: "Show the result of an undo or redo."
3 Focus is lost when the item list is rebuilt.<br>• Pressing Enter on "Everyone" re-renders #items with innerHTML. Only checkboxes carry data-key for restoring focus, so focus drops to <body>, measured (afterEveryoneKey: BODY).<br>• A screen-reader user loses their place in the item list. Item card "Everyone" button, renderItems() / refresh() lines 384, 424 Major SHOULD • Give the Everyone, Delete and Remove buttons a stable data-key so refresh() restores focus to them.<br>• When the focused element is removed, focus its neighbour.<br>• Apple's Focus and selection rule: "if the focused item disappears, move focus to an item one step away."
4 Rows collapse at 200% text on a phone.<br>• At 390 px with a 200% root font size, .item-head keeps name, price and Delete on one row. The item name is squeezed to 65 px wide and 192 px tall, one word per line (text200-item.png).<br>• Share rows break names mid-word ("Max / imili / an-…", text200-390.png).<br>• Nothing is lost, so WCAG 1.4.4 passes, but it is barely readable. .item-head, .share, at 390 px with 200% text Major SHOULD • Let .item-head wrap: flex-wrap: wrap, with the name taking the full width on its own line once it would fall under about 12em.<br>• Stack .share (name above amount) at large rem sizes.<br>• Apple: make sure the layout adapts to all sizes, including accessibility sizes.
5 Nothing can be edited after it is added. (Reviewer judgement.)<br>• Item name, item price and person name cannot be changed.<br>• Fixing a mistyped price means Delete, re-type name and price, then re-tick every diner for that item.<br>• Price typos are the commonest error when copying a receipt. Item cards, people chips Major Taste • Make the item name and price editable in place: tap the price to edit it, keeping the same validation and keeping who is assigned.<br>• Rename a person from their chip.
6 One action, two words. "Delete" for items and "Remove" for people, with toasts "Deleted Wine." and "Removed Ana." Item cards, people chips, toast Minor SHOULD Use one verb. Rewrite: buttons "Remove" on both; toasts "Removed Wine." and "Removed Ana."
7 Placeholders use the abbreviation "e.g.": "e.g. Ana", "e.g. Margherita pizza". Screen readers may read it as "egg". #pName, #iName placeholders Minor SHOULD Rewrite the placeholders as "Ana" and "Margherita pizza". The visible labels already carry the meaning.
8 The spare-cents explanation is one 45-word sentence and uses the jargon "largest leftover fraction". Result, .rounding Minor SHOULD Rewrite: "3 cents didn't divide evenly. Ana gets +1¢ (tip) and Chloé +2¢ (tax, tip)." Move the rule ("the cent goes to whoever's share was closest to the next cent; ties go to whoever is listed first") into a "How spare cents work" disclosure.
9 Tax accepts any amount up to 99,999.99, whatever the items cost. Tax of 99,999.99 on $1,234.56 of items was accepted without comment, and each diner was shown $33,744.85 (long-390.png). #tax, parseMoney Minor SHOULD Show a non-blocking inline note when tax exceeds about 25% of the items: "Tax is $99,999.99, more than the items ($1,234.56). Check the receipt." Apple's error guidance: prevent the error first.
10 Comma decimals are rejected with an unhelpful message. A comma-decimal price ("12,50") gets the generic "Use numbers only, with up to 2 decimals, like 12.50." #iPrice, #tax, parseMoney Minor Research Detect the \d+,\d{1,2}$ pattern and say "Use a period for cents, like 12.50." Or accept it as a decimal when it is unambiguous.
11 The selected tip preset is invisible in forced colours. The pressed preset (18%) looks the same as the others: border rgba(5,0,73,.8) against black, same background (forced-presets.png). Tip presets, button[aria-pressed=true] Minor Web Add @media (forced-colors: active) { button[aria-pressed=true] { background: Highlight; color: HighlightText; } }
12 Capital I and lowercase l render identically in system-ui at 14–16 px (type-eval px: 0). The affected text is the names people type in chips, share rows and the copied summary. People chips, .share .who Minor Taste Low cost. Duplicate-name checking is already case-insensitive. Showing the copied summary in the preview's monospace face covers the moment where names get compared.
13 The undo toast partly covers a focused button. While it shows, Tab reaches "Delete Salad" partly under it, at 390 and 1440. WCAG 2.4.11 passes (only partly covered); 2.4.12 does not. #toast Minor Web Set scroll-padding-bottom on html to the toast's height while it is shown.
14 +5 polish items:<br>• Rewrite the item note "Split 1 way: $11.25" as "Chloé: $11.25".<br>• Rewrite "Restored." as "Wine restored."<br>• "Start over" does nothing, silently, when there is nothing to clear. Hide it or disable it with a reason.<br>• .person { padding-left } and .pick padding are physical, so RTL chips are lopsided (rtl-390.png). Use padding-inline-start.<br>• Pressing the already-pressed tip preset does nothing. Either unpress it to clear, or make the group a radio group. Various Polish Taste As listed.

waived: 0

3. Trust

No deceptive patterns found in what was seen. #1 misstates a total, but it is a calculation and labelling defect with no persuasive intent, so it sits in Findings.

4. Landscape

Category: splitting a restaurant check by item. It sits between the consumer-utility and finance playbooks. Researched 2026-10-26.

Product Why chosen Source
Splitwise Itemize Category leader thepaypers.com, 2026-10-16: https://thepaypers.com/payments/news/splitwise-launches-itemize-receipt-splitting-feature-in-the-us-and-uk
Apple Wallet bill splitting (iOS 27) Platform owner entering the category Tom's Guide (excerpt only) https://www.tomsguide.com/phones/iphones/ios-27-will-reportedly-offer-bill-splitting-in-apple-wallet-heres-how-it-will-work ; 9to5Mac, Engadget and Gadget Hacks coverage via search [S]
Tab, "the simple bill splitter" Long-running item-claim splitter App Store https://apps.apple.com/app/id595068606 ; Laughing Squid review https://laughingsquid.com/tab-an-app-for-splitting-restaurant-bills-with-friends/ [S]
Soulver 4 Adjacent exemplar: the same arithmetic as a calculator notepad Six Colors, 2026-08: https://sixcolors.com/post/2026/08/soulver-4-casts-a-wider-calculating-net/ ; https://documentation.soulver.app/documentation/variables

Conventions to keep

  • People claim items; several people can share one item; tax and tip are spread in proportion to what each person ordered. Splitwise, Tab and Apple Wallet all describe this, and SplitCheck already does it.
  • Tip presets.

Convergence to avoid

  • "AI receipt splitter" App Store clones (Tabify AI, SplitBill AI, SplitScan, TabScan), branded on the AI rather than on the task. Seen in search results 2026-10-26 [S].

Frontier moves

  • A receipt photo becomes the item list, with quantities read off the receipt. Splitwise Itemize does this (thepaypers, 2026-10-16), and Apple Wallet on iOS 27 reportedly reads items and quantities [S].
  • Each diner claims their own items on their own phone: a QR join with no account in Splitwise, a live-synced shared bill in Tab.
  • A payment request at the end: Splitwise Pay, Apple Cash.

Openings

  • None of the sourced peer material explains what happens to unclaimed items or odd cents; the Splitwise article is silent on both. SplitCheck's cent-exact, explained spare-cent ledger is an opening it already half-owns. #1 currently undercuts it.
  • No account, no network, nothing leaves the device. Every sourced peer needs an app, an account or a payment network.

Step counts: the peers' core-task step counts are not published in what was fetched. SplitCheck's core task for 4 diners and 4 items is about 29 inputs: 4 names, 4 items with prices, about 7 ticks, tax and tip. The peer-gap condition of the rework bar therefore cannot be tested.

5. Distinctiveness verdict

Verdict: authored (utilitarian register).

Channels: 3 of the 9 are authored.

Channel Classification and evidence
Layout idiom Authored. Numbered steps 1, 2, 3 lead to an "=" result column: the split read as an equation (index.html:175, 189, 209, 235). The result "sheet" has dotted rules and a heavy rule above the total, like a check stub.
Voice Authored. Plain and exact: "to the cent", "Nobody assigned yet, so this is not counted.", and errors that say how to fix the problem ("Use a positive amount, without a minus sign.").
Type Partly authored. A serif (Iowan Old Style stack) for headings and money amounts with tabular figures, and system-ui for the controls. The serif is not traced to anything in the subject.
Color Trend default. A cream base (#f5f0e6) with a terracotta or oxblood accent (#8c2f1c) and a serif display face. This matches the slop catalog's "second-layer defaults" row.
Shape Library default: 6 px radius, pill chips.
Motion Platform default: one 180 ms toast rise, removed under reduced motion.
Iconography, imagery, sound and haptics None.
  • Signature element: behavioural. Every split is exact to the cent, and the spare-cent ledger says who got which cent and why.
  • Signature moment: behavioural. "Shares add up to $88.71" under the total, a reliability cue shown the same way every time.
  • Catalog tells: 4 in 3 channels, below the cluster threshold (6 or more tells across 3 or more channels):
    • AI-template: second-layer default palette (color).
    • AI-template: a single tracked uppercase kicker, "GROUP DINNER" (type). Weak: it appears once and carries context.
    • AI-microcopy: "e.g." in placeholders (voice).
    • AI-microcopy: Delete/Remove synonyms (voice).
  • Logo-off test: with the name hidden, the "= Each person pays" column and the spare-cents line still identify the product.
  • Competitor-swap test: putting "Splitwise" on these screens, the cent ledger and the equation layout look wrong for it. The colors do not.
  • Gate: the core flow has open Majors (#1 to #5), so expression changes are held until those fixes land. The directions below are for when that happens.

No profile direction, so two directions are proposed. Both keep the authored layout and voice and re-derive only the colour and type, the channels that are currently defaults.

Default check: a generic "bill splitter app" prompt would produce white cards, a mint or teal "money" accent, Inter, and a receipt icon. Both directions were checked against that and against the slop catalog.

Direction A: Guest check pad. Trace: the pale-green, ruled waiter's guest-check pad, the actual object a restaurant check starts on.

  • Color, light (contrast measured with contrast.py):

    Role Value Contrast
    base #e9f0e4, oklch(0.947 0.018 132) ink on base 12.81:1
    surface #f7faf4, oklch(0.981 0.008 129) ink on surface 14.15:1
    ink #1f2a1f, oklch(0.271 0.025 145) n/a
    muted #4a5a49 6.99:1
    ruling and boundary #6f8a6d, oklch(0.604 0.053 143) 3.6:1, passes the 3:1 non-text minimum
    accent (pad ink) #2f6b3a, oklch(0.475 0.100 148) white on accent 6.39:1; accent text 6.07:1
    error #a3201a 7.17:1
  • Color, dark:

    Role Value Contrast
    base #121813 ink 14.89:1
    surface #1b231c ink 13.32:1
    ink #e4ecdf n/a
    muted #a9b8a6 7.74:1
    boundary #6f8a6d 4.24:1
    accent #8cc79a on-accent #10180f 9.27:1; accent text 8.25:1
    error #ff9a8d 7.87:1
  • Color inputs: derived from one hue (about 145) plus lightness steps, which also gives the increased-contrast variant.

  • Risk: green is near the generic "money" default. That is acceptable only because it is the paper colour, not a button accent on white.

  • Type: display role Barlow Semi Condensed 600/700 (OFL), from the condensed sans printed on check-pad headers. Text and controls stay system-ui.

  • Shape: 4 px radius. Ruled hairlines under item rows, like the pad's lines.

  • Motion: productive only, 120 ms ease-out. With reduced motion, no movement.

  • Signature: the cent ledger, printed as pad-style line items.

  • Voice samples:

    • Button: "Add item".
    • Empty state: "No items yet. Copy each line from the receipt."
    • Error: "Use numbers only, like 12.50."
  • How it differs from each peer: Splitwise and Tab use a single-colour accent on white with no paper material. Apple Wallet uses system chrome. Soulver is a monochrome notepad.

Direction B: Thermal receipt. Trace: the thermal-paper itemised receipt the numbers are copied from. Its print is a purplish black, and its figures are monospaced.

  • Color, light:

    Role Value Contrast
    base #f4f3ef, oklch(0.964 0.005 95) print on base 13.13:1
    surface #ffffff print on surface 14.58:1
    print #2b2733, oklch(0.282 0.022 300) n/a
    faded print #5d5866 6.88:1
    boundary #8a8494 3.62:1
    primary button print #2b2733 fill with white text 14.58:1
    error #b3261e, oklch(0.501 0.178 29) 6.54:1
  • Color, dark:

    Role Value Contrast
    base #141317 text 15.59:1
    surface #1d1c21 text 14.27:1
    text #ecebef n/a
    faded print #aeaab6 7.44:1
    boundary #77727f 3.63:1
    error #ff9a8d 8.27:1
  • Color: no decorative hue. The only colour is the error state.

  • Type: data and numerals role IBM Plex Mono 400/600 (OFL), used only for amounts and the copied summary. Headings and text stay system-ui. This deliberately avoids a geometric sans with its mono sibling, a slop-catalog combination.

  • Shape: 2 px radius on fields. No grid lines, which would also be a catalogued tell.

  • Motion: none beyond the toast. With reduced motion, the same.

  • Signature: amounts set exactly as the receipt prints them, so a diner can match each line to the paper.

  • Voice samples:

    • Button: "Add item".
    • Empty state: "No items yet. Type each line as it's printed."
    • Error: "That price has 3 decimals. Use 2, like 12.50."
  • How it differs from each peer: none of Splitwise, Tab or Apple Wallet sets amounts in monospace. Soulver does, but as a free-form notepad, not as a per-person ledger.

A and B differ on 3 channels: color (tinted paper versus neutral print), type (condensed display face versus mono numerals), and shape (ruled rows versus none). Pick one, or keep the current palette knowing it reads as a 2026 default.

6. Type

Face: system-ui (SF Pro on this macOS run; Segoe UI or Roboto elsewhere) 400/600/700. Roles: UI, reading
Legibility: CONCERN. x-height ratio 0.721; confusables failing I/l @14, 15, 16 px (Chromium, px 0); weights 400/600/700; Bold Text n/a
Figures and features: tabular available, applied via .num/.money; distinct zero n/a
Coverage: OS font (not measurable); names like "Ïñgríð Þórsdóttir" rendered without fallback jumps (long-390.png)
Scaling: rem ok; 1.4.12 not tested; layout collapse at 200% on 390 px (#4)
Delivery (web): local system font, 0 KB
Distinctiveness: default tier yes; identity carried by nothing (correct for controls)
Verdict: Keep (UI and text roles)
Evidence: work/measure.json (type), shots/fold-1440-light.png
Face: Iowan Old Style stack ("Palatino Linotype", Palatino, Georgia fallbacks) 400/700. Roles: display, data (share amounts)
Legibility: PASS. x-height ratio 0.687; confusables not failing at 18–40 px display sizes; weights 400/700
Figures and features: tabular by default (good for amounts)
Coverage: Latin only (local); fallback faces on Windows and Android differ in width, unverified
Scaling: rem ok
Delivery (web): local only, no web font; Iowan exists on Apple platforms only
Distinctiveness: default tier no; identity carried by treatment, but untraced (part of the cream + terracotta + serif second-layer combination)
Verdict: Tune (keep it for amounts and headings only if a direction keeps a serif; otherwise Replace per direction A or B)
Evidence: work/measure.json (type)
Face (candidate, A): Barlow Semi Condensed 600/700. Roles: display
Legibility: PASS for display. x-height 0.779; l/1 d 0.19 @16 px (small mark, display sizes only)
Figures: tabular by default. Coverage: Latin, Latin Ext, Vietnamese
Verdict: candidate for display only. Evidence: work/type-candidates.json, shots/type-candidates.png
Face (candidate, B): IBM Plex Mono 400/600. Roles: data (amounts, summary)
Legibility: PASS. x-height 0.739; I/l d 0.16, 0/O d 0.10 @16 px (dotted zero, small marks; check by eye at 1×)
Figures: tabular. Coverage: Latin, Latin Ext, Vietnamese, Cyrillic
Verdict: candidate for amounts only. Evidence: work/type-candidates.json, shots/type-candidates.png

7. Reworks

No rework warranted.

  • Premise holds: the first screen starts the core task.
  • No cluster: no root cause shared by three or more findings survives per-row fixes. #2 and #3 share "list rebuilt with innerHTML", but stable keys fix each one.
  • No identity case: the distinctiveness verdict is authored.
  • Peer gap not testable: peer step counts are unsourced.
  • No system case: the app is a single file.

8. Enhancements

Six enhancements, kept few because the evidence is one screen and no user data.

Experience and flows

  1. Claim by person (Simplicity).
    • Today, 4 diners and 4 items mean a 16-checkbox grid ticked item by item, but at the table people say "I had the salad and the wine."
    • Add a person-first mode: pick Ana, see every item as a list, and tick hers.
    • It writes to the same who[] model, so the result is identical.
    • Unlocks: fewer steps for the most common way people report what they ate.
  2. Paste or photograph the receipt (Purpose).
    • Turn pasted receipt text ("Margherita pizza 16.50" per line) into items, and optionally a photo through <input type="file" accept="image/*" capture>.
    • This is the category frontier (Splitwise Itemize, Apple Wallet 27).
    • Name it for the task, "Add items from receipt", not "AI", to avoid the clone convergence.
    • Unlocks: removes most of the roughly 12 typed inputs in step 2.
  3. Quantities on items (Simplicity).
    • "2 × Beer at $7.00" splits by unit, so one diner can take one beer.
    • Receipts print quantities, and Apple Wallet 27 reportedly reads them.

Information architecture

  1. Live totals on a phone (Flexibility).
    • At 390 px, "Each person pays" starts about 1,850 px below the first item card (full-390-light.png).
    • Add a slim sticky strip during step 2 ("Ana $23.85 · Ben $23.84 · …" in tabular figures) that opens the full sheet.
    • The per-item "Split N ways" notes already give partial feedback; this extends it to totals.

Platform capabilities

  1. Share the summary (Agency).
    • Next to "Copy summary", offer navigator.share({ text }) where it is supported, so the summary goes straight into the group chat.
    • The web mapping prefers navigator.share on Apple devices.

Content and voice

  1. Put the spare cent where it lands (Craft).
    • Show "+1¢" in muted text beside the amount it went to, in that person's row.
    • The cent ledger is the product's signature and the opening no sourced peer documents (Landscape). Today it lives in a paragraph under the total.

9. Unverified

  • Safari and Firefox: not run. type-eval and all measurements are Chromium-only. A WebKit run would verify Iowan, :has() focus rings, inputmode keyboards and the clipboard.
  • Real touch devices: not run (iOS Safari keyboard, viewport and toolbar behaviour). A physical iPhone or Simulator Safari would verify it.
  • Screen readers: VoiceOver and NVDA output for the toast's live region (role=status holding buttons) and the "Restored." announcement was not heard. A VoiceOver pass would verify it.
  • Clipboard failure: the "Could not copy automatically" branch was not driven. Verify by denying clipboard permission.
  • Storage blocked: localStorage unavailable (private mode or blocked storage) was not driven. save() swallows the error silently, so a refresh would lose the dinner with no message. Verify with storage disabled.
  • Restoring last session: returning after a week restores the previous dinner. Whether that is intended is unknown (question 5).
  • Error, offline and loading states: the app makes no network requests (file:// and local only), so these do not apply. Verified by reading the source; no fetch calls exist.
  • Increased contrast: prefers-contrast: more has no rule in the CSS. The screenshot looked the same as the default and every pair already passes, so there is no finding. A Windows high-contrast pass beyond the emulated forced colours would verify it.
  • Windows and Android fonts: with the Palatino, Georgia, Segoe UI or Roboto fallbacks, metrics and wrapping were not seen. Verify on those systems.
  • Text spacing: the WCAG 1.4.12 overrides (line height, letter and word spacing) were not applied.
  • 30-person and 200-item limits: the limits themselves were not reached; only 12 diners were tested.
  • Landscape claims:
    • Apple Wallet bill splitting is known from press summaries [S]; the Apple primary page was not fetched.
    • Tab's current state is from its App Store listing and a 2013 review [S].
    • Peer step counts are unknown.

10. Offer

Fixable in code now:

  • Copy rewrites: 6, 7, 8, and the polish copy in 14.
  • Focus and Undo mechanics: 2, 3, 13.
  • Layout at large text: 4.
  • Input guidance: 9, 10.
  • Forced colours: 11.
  • Polish: 14 (RTL padding, Start over).

Need a design decision first:

  • #1: the unassigned-item policy (block, share among everyone, or hold aside), plus the label wording.
  • #5: the editing model (edit in place, or an edit sheet).
  • #12: worth fixing at all.

Enhancements 1 to 6 and the two theme directions need your call. I can build any one enhancement, or a throwaway prototype of Direction A or B applied to the current page.

Optional, to verify more: a WebKit or iPhone run, a VoiceOver pass, and confirmation of the profile answers above.

Which findings should I fix?

Claude Opus without loupe: the review it wrote with no skill
<!-- model: claude-opus; skill commit: 7559367 -->

Design review: "Split the check" (splitcheck/index.html)

Scope, method, defaults

  • Product: a single-page web app for splitting a restaurant bill. You add people, add items, tick who had each item, enter tax and tip, and get per-person totals with cent-exact rounding. It saves to localStorage, has undo for destructive actions and has a copyable text summary.
  • Method: I read the whole source and drove it in Chromium with Playwright at 1280×800 (light), and at 390×844 in light and dark. I scripted probes for focus, validation, undo, sticky layout and contrast. Scripts and screenshots are in work/.
  • Defaults I assumed, since the owner was unavailable:
    1. The main use is on a phone at the table, right after the check arrives. Desktop is secondary.
    2. The app is US-centric: dollars, tax entered as an amount, tip as a percentage before tax.
    3. "Exact to the cent" is the product's core promise. So anything that makes the total disagree with the paper receipt counts as a serious problem.
    4. It is in development, so missing features are listed as recommendations, not defects.

Overall: the build is well made. Every control is at least 44 px. Labels, hints, aria-describedby, aria-invalid, focus rings and forced-colors support are all there. Reduced motion is respected and dark mode is solid. The calculation is a correct largest-remainder method on integer cents. The warm "receipt paper" look with serif headings is distinctive, not template-generic. The problems are mostly about trusting the numbers, phone ergonomics, and a few keyboard focus bugs.

Severity scale: Critical (breaks the core promise) · High (common task is hurt) · Medium (friction or an a11y defect with a workaround) · Low (polish).


Critical

C1. "Check total" doesn't match the receipt while any item is unassigned, and the full tax is pushed onto everyone else
  • What happens: Unassigned items are left out of the subtotal, but the tax amount you typed (the receipt's whole tax) is still spread across only the assigned items.
  • Reproduced:
    • Pizza costs $20 and is shared by Ana and Ben. Wine costs $30 and is unassigned. Tax is $5 and tip is 20%.
    • The result says "Check total $29.00" with each person at $14.50, including $2.50 tax.
    • The real bill is $50 + $5 tax. The pizza's fair share of tax is $2.00, not $5.00.
  • Why it matters:
    • The label "Check total" invites people to compare it with the receipt, and it won't match.
    • Anyone who copies the summary early sends overcharged amounts. The red "Not included…" line is small and sits below the fold on a phone.
  • Fix:
    • Don't show any shares (or the Copy button) as final while items are unassigned. Change the panel to "2 items still need someone. Shares so far:" with the totals visibly marked provisional. Or disable Copy summary until everything is assigned.
    • Prorate tax over the receipt subtotal (all items), so assigned people only carry their own tax share. Report the unassigned remainder ("$3.00 tax belongs to unassigned items") instead of reallocating it.
    • Rename the total "Assigned so far" until everything is assigned, and use "Check total" only after that.
C2. An invalid tax or tip value silently counts as $0 / 0% in the results
  • What happens: model() uses t.cents || 0. While the Tax field shows an error, the shares are recalculated with tax $0.00 and still look final. I confirmed this: typing "12." gives the error and the result "items $40.00 + tax $0.00 + tip $0.00".
  • Why it matters: A user can glance at the results panel (on desktop it's beside the form, and the error may be off-screen) and copy wrong totals.
  • Fix: If tax or tip is invalid, replace the results with "Fix the tax amount to see each share", or dim them and disable Copy. Never compute with an assumed value the user didn't enter.

High

H1. Live validation shows errors in the middle of normal typing
  • What happens: Tax and Tip are validated on every keystroke, and the patterns reject a trailing decimal point. Typing "12.50" shows "Use numbers only, with up to 2 decimals" at "12.". Typing "17.5" in Tip shows "Use a number like 18 or 17.5" at "17.". Typing "1,000" shows an error at "1,0".
  • Why it matters: Users are told they're wrong while doing exactly what the hint asks. Combined with C2, the totals also jump to $0 tax for a moment.
  • Fix:
    • Accept partial input while typing (a trailing . or an incomplete thousands group) and only show errors on blur or Enter.
    • Better still, reject impossible characters as they're typed and validate fully on blur.
H2. On a phone, the answer is ~3,000 px down the page
  • What happens: Below 900 px, "Each person pays" comes after every item card and the tax section. With 4 people and 4 items the page is about 3,040 px tall at 390 px wide. Every checkbox tap changes the totals out of sight.
  • Why it matters: This is the main context (assumption 1). The user can't see the effect of a tap and has to scroll down to check and back up to edit.
  • Fix:
    • Add a compact sticky bottom bar on small screens, such as "Ana $26.27 · Ben $26.26 · … ▸ See split". Tapping it jumps to (or opens a sheet with) the full result.
    • At minimum, show a running "Assigned $71.50 of $78.75 · 1 item unassigned" status near the top.
    • Also consider a step-by-step flow on phones (People → Items → Tax/Tip → Result), since the sections are already numbered 1–3 and "=".
H3. You can't edit people or items, only delete them
  • What happens: Fixing a typo in a price ($18.00 → $16.00) or a name means deleting and re-adding, which throws away all of that item's assignments.
  • Why it matters: Receipt entry is error-prone, so this will happen on almost every bill.
  • Fix: Make the item name and price editable in place (tap to edit, Enter to save, Escape to cancel), and the same for person chips. Keep the assignments.
H4. On desktop, the sticky result panel hides its own bottom half
  • What happens: .out is position: sticky; top: 16px with no height limit. With 8 people and 12 items, the panel is taller than the viewport. Copy summary stayed off-screen at scroll positions 0, 1000 and 2000 px, and only appeared near the end of a 4,000 px page (verified by script).
  • Fix: Add max-height: calc(100vh - 32px); overflow: auto to .out, or keep only the totals list sticky and let the details scroll. Also put Copy summary above the long spare-cents explanation.

Medium

M1. Keyboard focus is lost after "Everyone" and after "Undo"
  • Everyone: It re-renders the items list, but this button has no data-key, so focus falls to <body> (verified). A keyboard or screen-reader user is sent back to the top of the page after each use.
    • Fix: Give it data-key="all-<itemId>".
  • Undo: It removes the toast that contained the focused button, so focus lands on <body> (verified).
    • Fix: After undo, focus the restored element (the restored item's Delete button or person chip), or at least the related input.
M2. Undo is hard to reach by keyboard and disappears without warning
  • Hard to reach: After a delete, focus moves to the Item field. The Undo button sits in a fixed toast at the end of the DOM, and it took 15 Tabs from the Item field to reach it.
  • Disappears: Any later edit, such as typing one character in Tax or ticking a box, calls save() and silently removes the undo option (verified).
  • Fix:
    • Support Ctrl/⌘+Z for the last destructive action.
    • Keep the undo available until the next destructive action, not until any edit.
    • Consider moving focus to the Undo button for "Start over", which wipes everything.
M3. Placeholders look like real values
  • What happens: The placeholder colour is --muted (#5b544a, 7.3:1). That's good for hints but too strong for placeholders. In the screenshots, "0.00" in Price and "e.g. Ana" look like entered text, and an empty Price field reads as "0.00 already filled".
  • Fix:
    • Drop the "0.00" and "0" placeholders. The labels and hints already say what to enter.
    • Or use a lighter placeholder tone that still passes 4.5:1. The current one is far above that, so there's room.
M4. Only equal splits, percentage-only tip, pre-tax-only tip
  • Missing split options: Real bills include "I had 2 of the 3 beers", a discount or comp, an automatic service charge, or a tip in dollars. The engine already supports weighted shares (allocate(total, weights)), but the UI only offers equal ticks.
  • Recommendations (in priority order):
    1. A quantity or weight per person on an item, e.g. long-press a chip to show "×2".
    2. A "Tip $ / %" switch.
    3. A "tip on total after tax" option.
    4. A discount line. These are feature gaps for a build in development, not defects.
M5. The spare-cents explanation is long and technical
  • What happens: "…handed out, one at a time, to whoever had the largest leftover fraction (ties go to the person listed first): Ana +2¢ on tax, tip; Ben +1¢ on tax…". It's four lines of text for differences of a few cents, and it appears on almost every bill.
  • Why it matters: It is honest, but it pushes the Copy button down and reads as alarming.
  • Fix: Shorten it to "Shares differ by up to 1¢ so they add up exactly. Details" and put the full explanation behind a disclosure. Do the same in the copied summary.
M6. Tip presets are announced as independent toggle buttons, not one choice
  • What happens: They're aria-pressed buttons, but they behave as single-select, and pressing the active one doesn't un-press it.
  • Fix: Use a radio group (role="radiogroup" with role="radio" and aria-checked, plus arrow-key movement), or real radio inputs styled as segments. Also consider showing the tip amount in dollars next to the selection ("18% = $12.87").

Low

  • L1. Inconsistent verbs: People use "Remove" and items use "Delete" for the same kind of action. Pick one. "Remove" fits both, and undo exists.
  • L2. Person chips are wordy: Every chip reads "Ana Remove". With 8 or more people, that's a wall of the word "Remove". Use an × icon with the existing aria-label="Remove Ana" and a visible hover/focus state.
  • L3. "Everyone" doesn't show its state: It stays the same when everyone is already ticked, and there is no "Clear". Make it a toggle ("Everyone" ↔ "Clear"), or disable it when everyone is already ticked.
  • L4. Hints are always visible: "Up to 24 characters. Each name must be different." and "Total tax on the receipt. Blank means none." add clutter on every visit.
    • Shorten them, or keep only the validation messages. maxlength already enforces the length.
    • maxlength silently cuts off pasted text, so a counter near the limit would help.
  • L5. Duplicate item names are allowed: "Beer", "Beer" makes the per-item notes and the spare-cents line ambiguous ("on Beer, Beer"). Either allow it but add a number automatically ("Beer (2)"), or warn.
  • L6. "Start over" sits right next to the title: On mobile it's a full-size button directly under the intro. It is undoable, which softens this. Consider moving it to the end of the results, or making it a quieter text button.
  • L7. The restored bill isn't flagged: Coming back the next day silently shows the previous bill. A one-line "Continuing your last split · Start over" makes it clear the data is saved.
  • L8. The copied summary only has totals: Friends often ask "what's in my $26.27?". Add each person's breakdown (items + tax + tip) to the summary, and offer navigator.share on mobile next to Copy.
  • L9. Currency and locale are fixed: "$" and en-US formatting are hard-coded, and "12,50" is rejected (the message is clear). This is fine under assumption 2, but note it before any non-US release.
  • L10. Contrast is borderline in light mode: Input and checkbox borders are --line #8a8173, which is 3.38:1 on the page background and 3.78:1 on the card surface. That passes the 3:1 minimum for UI components but is close to it. Darkening slightly would give more margin in glare at a restaurant table. Everything else measured well: muted text 7.35:1, accent 7.28:1, error text 7.43:1, focus ring 5.87:1, and dark mode is comfortable.

What's working (keep it)

  • Exact cents, explained: It's cent-exact and says so. Each person's line shows items + tax + tip, and "Shares add up to $X" acts as a self-check.
  • Undo instead of confirm dialogs: Destructive actions use undo. Error messages are specific and suggest a fix.
  • Input basics: inputmode="decimal", Enter submits each add form, and focus returns to the input for fast repeated entry.
  • Accessibility: Strong overall, with real labels, fieldset/legend for "Who had it?", status regions, forced colors, reduced motion and 44 px targets.
  • Visual identity: It's coherent and suits the subject. The receipt-like result sheet with a serif total is a nice touch.

Suggested fix order

  1. C1 and C2: Never show or copy totals that don't reconcile with the receipt or the inputs.
  2. H1: Validate as the user types without false errors.
  3. H2: A sticky totals bar on mobile.
  4. H3: Inline editing of items and people.
  5. H4 and M1: Fix the sticky panel overflow and the focus bugs (both small CSS/JS changes).
  6. M2–M6: Then the remaining Medium items.
  7. Low items: Polish as time allows.

Claude Sonnet: loupe’s report

Blind judges: 3 of 3 preferred this report, 0 preferred the one written without loupe.

<!-- model: claude-sonnet; skill commit: 7559367 -->

Review: "Split the check" (splitcheck/index.html)

1. Summary

Reviewed: one screen (single-page web app, all states on one route), running file in Chromium, no source repo or .loupe.md. Profile inferred, not written.

Verdict: a careful, accessible, well-copywritten build with correct money maths. Its weak points are layout at scale (result panel and mobile distance to the result) and a few places where the screen does not tell the user the number is incomplete or wrong.

Do first: findings 1, 2 and 3.

Questions I would have asked (no answers; defaults used): purpose and audience; target platforms; release stage; voice; peers; whether the "split everything evenly" case matters; currency/locale scope.

Assumptions (defaults):

  • platforms: web only (desktop and phone browsers). No Apple-native rules applied; Apple principles are used as reasoning only, so no finding is an Apple MUST Blocker.
  • release_stage: development (dead-control rule: none found anyway).
  • register: utilitarian (frequent check-and-act task, money).
  • density: comfortable. voice: plain, sentence case, US spelling, no humor (inferred from existing strings).
  • direction: none set, so distinctiveness gives a verdict and options only.
  • Category: bill-splitting / group-expense calculators. Peers (from memory, unverified): Splitwise, Tab-style receipt claim apps, Settle Up, the iOS Calculator-plus-tip pattern.

Run notes:

  • Tools: Node + Playwright Chromium scripts in work/; screenshots there (pop-*, empty-*, errors.png, zoom200-*, rtl-390.png, long-390.png, many-*.png, forced.png, morecontrast.png); full measurement JSON work/measure-out.json.
  • measure-runner.js ran on a populated state (3 people, 4 items) across 390/820/1440 x light/dark x reduced-motion (12 combinations, none sampled). Contrast: 74 text pairs per combination, 0 failures, 0 unmeasurable. Targets: only the 9 custom 22 px checkboxes are under 44 px; each sits inside a 44 px-tall <label> pill that is the real click target, so they pass (spacingOk true, parent target >= 44). Motion: no transition: all, no animation on load, reduced-motion rule present, 0 unreadable sheets. Defaults (template tells): none. logicalProps: 3 physical, 0 logical. No iframes or shadow roots. Hidden elements skipped: 5.
  • type-eval.js: not run (only system fonts, Iowan Old Style falls back per OS; verdict by eye, see Type).
  • Side effects: localStorage seeded and cleared per fresh browser context; text-size and dir=rtl edits on the live DOM in throwaway pages. No user files touched.
  • Landscape: no web research done (time-boxed); everything in section 4 is from memory and marked (unverified). Not used for any Blocker.

2. Findings

# Finding Where Impact Apple Fix
1 The result panel is position: sticky; top: 16px but is taller than the viewport once there are many people. Measured with 12 guests at 1440x800: aside is 1462 px tall; after scrolling 600 px "Copy summary" is at y=1361, off screen. The summary cannot be copied until the left column has been scrolled almost to its end. Each person pays, >= 900 px wide Major Web Give .out max-height: calc(100vh - 32px); overflow: auto, or keep only the total sticky and put Copy summary beside the heading
2 On phones the result sits after all three input sections. With 12 people x 10 items the page is 8,792 px tall (390 px wide). Ticking boxes gives no visible total or share anywhere nearby; the only feedback is the per-item "Split N ways" line. Step 3 (tax/tip) is also below the whole item list. Whole page at 390 px Major Taste (reviewer judgement; Apple's Simplicity/Agency reasoning) A compact sticky bar at the bottom: "Check total $X" plus a link to "See shares"; or put tax and tip before the item list
3 A tax or tip that fails validation is silently counted as 0 in the result. Typing abc in Tax leaves "Check total $14.50" with no mention of tax; the only cue is the red message at the field, which on desktop is far from the result and on phones off screen. After a reload the invalid text is restored with no error message (errors are only set on input). Tax and tip; Each person pays Major Web Show "Tax not counted: fix the amount above" in the result when an error exists; run validation on load; or hide the result until valid
4 After Undo or Dismiss the focused button is removed and focus drops to <body> (measured: BODY). Keyboard and screen-reader users lose their place. Separately, the undo toast is cleared by the next unrelated edit (ticking a box after deleting an item returned an empty toast), so the Undo window closes silently. Undo toast, bottom of screen Major Web After Undo, focus the restored item's control or the section heading; do not clear the toast on save() unless the user acts on the same data, or give it a visible timeout
5 People and items cannot be edited. A mistyped price means Delete and re-add, losing the item's assignments (Undo only restores the delete). A misspelled name means Remove, which also strips that person from every item. Item rows, person chips Major Taste (reviewer judgement) Tap-to-edit name and price inline, keeping who
6 "Check total" excludes unassigned items, so it can differ from the receipt, and the lede promises "the exact total". Example: Tiramisu $9.00 unassigned, "Check total $47.35" while the receipt is $56.35+. The warning line is separate and far smaller. Each person pays, "Check total" row Major Taste (reviewer judgement; anti-ambiguity on money) Rename to "Total assigned" while anything is unassigned and add a row "Not assigned $9.00"; reword lede: "Every share adds up to what you assign, to the cent."
7 "Everyone" only adds everybody; once pressed there is no way to clear an item except un-ticking each person. With 12 people that is 12 taps. Item row, "Everyone" button Minor Taste Make it a toggle ("Everyone" / "No one") using aria-pressed, or add a "Clear"
8 Price rejects a decimal comma: 12,50 shows "Use numbers only, with up to 2 decimals, like 12.50." and $ is hard-coded in label, placeholder and output. Users with comma-decimal keyboards get a message that does not say what is wrong. Price and Tax fields Minor Research Message: "Use a period for cents, like 12.50." If non-US is in scope, read navigator.language
9 The "Spare cents" paragraph explains the algorithm ("largest leftover fraction (ties go to the person listed first)") in a 4-line block that is always open under the totals. Most users need one sentence. Result sheet Minor Taste "5 spare cents went to Ana (+3¢) and Chidi (+2¢) so the shares add up exactly." Put the where-detail in a <details>
10 "Split 3 ways: $3.34 / $3.33 / $3.33" shows amounts without names; with 12 people it is a long unlabelled run of numbers. Item rows, note line Minor Taste "Ana $3.34, Ben $3.33, Chidi $3.33" (or show the amount inside each person pill)
11 Tax and tip errors are not announced: no live region, and the message is only referenced by aria-describedby, which is read on focus, not while typing. Per-item "Nobody assigned yet" warning is also not announced. Tax, Tip, item warnings Minor Web (WCAG 3.3.1, 4.1.3) Put the error <p>s in role="status" (or aria-live="polite")
12 Name and item inputs lack autocapitalize="words"/"sentences" and enterkeyhint; phone keyboards will show lowercase "ana". Name, Item fields Polish Web Add the attributes
13 The ">= 900 px" two-column layout leaves the "Each person pays" panel empty-looking until at least one item is assigned: on first run the right column shows only "Add at least one person and one item" (seen in empty-1440.png). Acceptable, but the panel could state the next step. Each person pays, empty Polish Taste "Start with step 1: add who is at the table." (sequential cue instead of the generic line)

waived: 0 (no profile).

Positive checks (nothing to fix): contrast passes in all 12 combinations; text sizes in rem, no overflow at 200% root font size (390 and 1440), long names wrap without horizontal scroll; focus ring is a 3 px outline on every control, Tab order follows reading order (reset, name, add, removes, item fields, assign checkboxes, tax, tip, presets, copy); forced-colors rules exist; reduced motion honored; destructive actions are undoable and Start over is guarded by Undo; Remove/Delete have names that include the object; hit areas are 44 px.

3. Trust

No deceptive patterns found in what was seen. (No accounts, payments, consent, urgency or invented counts. Data stays in localStorage and is not disclosed on screen; see enhancement 6.)

4. Landscape (from memory, unverified; no web research run)

  • Category: group bill-splitting. Products named for study: Splitwise (ledger over time), receipt-claim apps where each diner taps their items, Settle Up, a plain calculator + tip. None fetched; no URLs or dates.
  • Conventions to keep: items listed with a per-person claim; tip presets; "who owes what" in one list (unverified).
  • Convergence to avoid: sign-up before first value and invites/accounts; this product has none, which is its advantage.
  • Frontier moves: scan or paste a receipt; tap your own name to claim items; send a per-person payment link (unverified).
  • Openings: zero-account, works offline, exact-cent allocation with an explanation of the rounding, which most peers hide.
  • Step count for the core task: in this app, an even split of one amount among 4 = 4 names (8 actions incl. submit) + 1 item (3) + Everyone (1) + tip preset (1) = about 13 actions. The best peer's count was not measured.

5. Distinctiveness verdict

Partly authored.

  • Channels: type = platform default (system sans body, Iowan/Palatino stack for headings and amounts); color = trend default (cream background + oxblood/clay accent is the catalog's "second-layer default" combination, one tell, --accent: #8c2f1c on #f5f0e6); shape = library/platform default (6 px radii, pills); layout = default two-column form + side panel; motion = none (appropriate); copy = authored (plain, specific, no AI-microcopy tells); iconography = none. Authored: the receipt sheet (dotted leaders, double rule above "Check total", tabular figures, amounts set in the serif) and the copy.
  • Slop tell count: 1 of the catalog's tells, far below the 6-tell, 3-channel cluster threshold; no cluster.
  • Logo-off test: the sheet and wording identify it; the header and form do not. Competitor swap: putting another calculator's header on it would barely change the form.
  • Direction: none in a profile. Options (traced to subject, not applied): (a) Receipt paper: the sheet extends to the whole page, thermal-paper off-white and ink black, perforated section dividers, monospaced figures only where money appears (subject: a paper check). (b) Table-talk: names are the visual anchor, each person gets a colour from a small role set so the claim chips and the result row match (subject: who had what). Both are proposals; no values committed.

6. Type

  • Heading/amount face: "Iowan Old Style" then Palatino, Georgia (--serif). Role: h1, h2, share amounts, bill total. Fit: suits the paper-receipt tone; amounts carry tabular-nums (.money). Whether the fallback faces (Georgia has old-style figures) keep $13.38 aligned on Windows/Android was not verified (type-eval not run); Unverified.
  • UI/body face: system-ui. Role: labels, body, controls. Fit: right for a utilitarian tool; sizes 13 to 16 px with 8 distinct sizes in use; min size 13 px (kicker, uppercase with 1.04 px tracking) which passes for desktop and is slightly small for phone (> 12 px threshold, below 16 px body). No finding.

7. Reworks

No rework warranted: no cluster shares one root cause (findings 1 to 3 are layout and feedback fixes), the premise is right for the job, and the missing receipt/claim mode is a feature, so it is an Enhancement.

8. Enhancements

Experience and flows

  1. (Simplicity, Flexibility) Even-split shortcut. The core object is a bill with N diners; the most common case is "everyone shared everything". A first-screen option "Split evenly" that takes only the total, tax, tip and head count removes about 9 of the 13 actions measured above. Unlocks: first-value in under 30 seconds.
  2. (Agency) Bulk assignment: "Select a person, then tap items" as a second view (rows by person), so a diner's whole order is claimed in one pass; the N x M matrix stays as the detail view.
  3. (Craft) Live "Your share" strip on mobile: sticky bar with Check total and number of unassigned items, tapping scrolls to the sheet. Ties to findings 2 and 6.

Visual design and theming 4. (Delight, Craft) Lean into the receipt: extend the sheet metaphor (it is the one authored element) to totals and the Copy summary preview, so the copied text matches what is on screen.

Content and voice 5. (Responsibility) Show the rule once, near step 3: "Tax and tip are shared in proportion to what each person ordered." Currently only inferable from per-person lines.

Platform capabilities 6. (Responsibility, Familiarity) Say the data is kept: a small "Saved on this device" note, and a clear-data affordance (Start over exists). Optionally use navigator.share for the summary on phones where available.

Evidence is thin on peers (no research), so these are limited to six.

9. Unverified

  • Landscape and peer step counts: web research not run; verify with a short study of 3 peers.
  • type-eval.js not run; serif figure style (old-style vs lining) of the fallback stack on Windows/Android: verify by running it there.
  • Screen reader behaviour (VoiceOver/NVDA) for the toast, live regions and checkbox pills: not driven.
  • Offline/slow-network and error states: not applicable (no network calls) except localStorage blocked: try/catch present, not exercised.
  • Pseudo-localized text (+40%): only long-string test done; RTL screenshot taken (rtl-390.png) but 3 physical-only declarations (e.g. margin/padding, padding-left on .person) mean mirroring is likely partial; not scored.
  • Increased contrast (contrast: more) and forced colors: screenshots captured, rendered without errors; not inspected in detail.
  • Firefox/WebKit rendering (notably :has() checked-state styling in older Safari): not run.
  • Pointer hover/pressed feedback timing: not timed.

10. Offer

Fixable in code now: layout (1, 2 sticky bar part), validation/result messaging (3, 6, 11), focus and toast (4), small copy rewrites (6 lede, 8, 9, 10), attribute polish (12), Everyone toggle (7). Need a design decision first: 2 (placement of tax/tip and the mobile bar), 5 (inline edit pattern), and enhancements 1 to 4. Which would you like? Optional input that would let me verify more: the source repo (to write .loupe.md and check tokens), a peer list, and a Windows or iOS Safari capture.

Claude Sonnet without loupe: the review it wrote with no skill
<!-- model: claude-sonnet; skill commit: 7559367 -->

Design review: "Split the check" (index.html, in development)

Assumptions (owner unavailable): audience is a group at a restaurant table, mostly on phones, US dollars, pre-release so no analytics or user research. I read the source and drove it in Chromium at 1280x800, 375x740 and dark mode, with real data (3 people, 4 items, tax, 18% tip, then 17 people). Screenshots are in the work/ folder.

Verdict

This is a solid, restrained build. It has real labels, 44px targets, visible focus, error text tied to inputs, undo for destructive actions, dark mode, reduced-motion and forced-colors support, and no horizontal scroll at 375px. The arithmetic is exact: shares sum to the total, and spare cents are tracked. Problems are mostly in workflow gaps, focus handling and the results panel on small or crowded screens. I found no crash and no console errors.

Findings

High
  1. Invalid tax or tip is silently treated as 0 in the results. Typing 4.1x in Tax shows an error under the field, but the "Each person pays" panel keeps showing totals computed with $0 tax (model() uses t.cents || 0). The panel is far from the error on both desktop and mobile, so someone can read and share wrong amounts. Fix: when tax or tip is invalid, replace the totals with "Fix the tax amount to see totals" (or mark them "tax not included"), and make Copy summary disabled.
  2. Cannot edit an item or person. A mistyped price or name means Delete, re-add, and re-tick every checkbox. This is the most likely mistake when copying a receipt. Fix: make name and price editable inline (tap to edit) or add an Edit button; renaming a person especially should keep assignments.
  3. Sticky results panel breaks with larger groups (desktop). At 17 people the aside is 1902px tall in an 800px window and is position: sticky; top: 16px. Its lower part, including the check total, Copy button and spare-cent notes, can never scroll into view while the left column is shorter or the panel is pinned. Fix: add max-height: calc(100vh - 32px); overflow: auto to .out, or sticky only when it fits (put the total and Copy at the top of the panel).
Medium
  1. Results are at the bottom of the page on mobile. The headline answer ("what do I owe") is below four sections and every item card. Users must scroll past all items after each tick to see the effect. Fix: a compact sticky bottom bar on narrow screens showing the check total and a "See shares" link/anchor, or place a collapsed results summary right under the header.
  2. Focus is lost after "Everyone". Pressing it re-renders the list and focus moves to <body> (confirmed: BODY is the active element). Keyboard and screen-reader users lose their place in a long list. Checkboxes restore focus via data-key; buttons (Everyone, tip presets after refresh) do not. Fix: give those buttons a data-key too. Also make Everyone a toggle (second press clears), since there is no quick "clear" and it is not clear which state it is in.
  3. Per-item "Split 3 ways: $10.34 / $10.33 / $10.33" does not say who pays which. The amounts are in checkbox order but unlabeled. Fix: name them ("Ana $10.34, Ben $10.33...") or drop the per-person list and keep "3 ways, about $10.33 each".
  4. Spare-cent explanation is jargon and shown prominently. "largest leftover fraction (ties go to the person listed first)... Ana +3¢ on Wine, tax, tip" is a long, dense paragraph in a small muted style, and it is displayed for almost every bill. Fix: one line ("5 cents didn't divide evenly and went to Ana, Ben and Cleo") and put the detail behind a disclosure, like the existing Preview.
  5. Red warning on every new item. Each item added shows a bold red "Nobody assigned yet, so this is not counted" immediately, though that is the normal next step and the user has not made an error. Red also competes with real errors. Fix: neutral note until the user has moved on, or show it only in the results panel (where it already appears).
  6. Add button alignment relies on a magic number (margin-top: 28px to sit beside the input). It aligns to the input only when no error/hint wraps; in the empty-submit screenshot the button floats at the input's height while the error text grows below, and on desktop the layout shifts when errors appear and disappear. Fix: use align-items: end on a wrapper that excludes the hint/error, or put hints below the row.
  7. Persistence is invisible. State is saved to localStorage and reloaded next visit with no cue. A returning user, or the next person on a shared device, sees the old dinner. Start over exists but is in the header, far from the items and with an undo toast that disappears on the next change. Fix: a quiet "Saved on this device" note, and consider confirming or at least keeping undo longer; place Start over near the results.
  8. Undo toast vanishes on any state change (save() clears it) and is not announced reliably: it is replaced via innerHTML in a role=status region, and undo then writes a separate sr-only "Restored." message. Screen-reader users may not hear "Removed Ana". Fix: keep a persistent live region and update its text; do not clear the toast on unrelated edits for a few seconds.
Low
  1. Always-visible hints add noise. "Up to 24 characters. Each name must be different." is shown permanently on a field everyone fills in without trouble; hints under the Tax/Tip fields wrap to three lines. Show length/uniqueness rules only when violated, keep the tax/tip hints short ("From the receipt. Blank = none").
  2. Tip semantics hidden in small text. Tip is calculated on items before tax, which differs from many receipts' suggested tips; the only mention is a muted hint. Offer a "tip on pre-tax / post-tax" toggle or state it next to the presets.
  3. Tip presets and the Tip field don't read as one control. The preset row sits below both inputs, and "No tip" sets "0" while a blank field also means no tip; the pressed state of "No tip" is only shown after typing. Place presets directly under the Tip field.
  4. Currency hard-coded to $ and en-US formatting; the label "Price ($)" and $ stripping will confuse non-US users. Acceptable if US-only; otherwise detect locale or add a currency setting.
  5. Copy status disappears whenever any input changes, because the results panel re-renders (and the status is rendered next to the button, easy to miss). Keep the "copied" note for a few seconds or on the button label itself ("Copied").
  6. Preview summary is collapsed inside the sheet; summary text does not include the per-person item breakdown, which people usually want when sending to the group.
  7. Step markers "1/2/3/=" are decoration (aria-hidden) and the "=" reads oddly; the order is also not enforced, so people may add items before people, and the "Add people above to assign" note covers that. Consider dropping the numerals or showing progress.
  8. Generic structure. Visually the product is distinctive (warm paper palette, serif headings), so no template concerns; the small "GROUP DINNER" kicker adds little and could go.

What works, keep it

  • Contrast and focus rings are strong in light and dark.
  • Touch targets are at least 44px; chips and checkboxes are generous.
  • Clear empty states for every panel and plain-language validation messages.
  • Money parsing is forgiving ($, commas) and results explain "Shares add up to".
  • Keyboard flow: Enter submits and returns focus to the first field of the form.

Priority order

1 (invalid tax/tip silently ignored), 3 (sticky panel overflow), 2 (no editing), 4 (results below fold on mobile), 5 (focus loss), then 6-11.

Claude Haiku: loupe’s report

Blind judges: 0 of 3 preferred this report, 3 preferred the one written without loupe.

<!-- model: claude-haiku; skill commit: 7559367 -->

Loupe review: Split the check (splitcheck/index.html)

1. Summary

  • Reviewed: single-page web app "Split the check" (splitcheck/index.html), whole app (one screen, inline pass, no split). Input: running app via file://, driven in Chromium through Node Playwright. Seeded state: 4 people, 5 items, tax 4.87, tip 18%. Plus empty state.
  • Verdict: a careful, well-built utility. Contrast, targets, reduced-motion and 320 px reflow all pass measurement, and the split arithmetic is sound. The defects sit in the money path: an invalid tax or tip is silently treated as zero in the total and in "Copy summary", and on a phone the total sits about 1,600 px below the fold.
  • Most important to do: make invalid tax and tip block or visibly flag the total and the copied summary (F1). Then bring the total into view on phones (F2).
  • Rework: No rework warranted. The rework bar is not met: no cluster of 3+ findings shares one root cause, the premise serves the core job, and no peer step count was sourced (landscape not run). Three enhancements are listed below.
  • waived: 0 (no profile waivers).

Assumptions (no owner answers; proceeded on defaults, no .loupe.md written):

  • Platform: web only. Release stage: development, so dead controls would be Major with an Unverified note (none found).
  • Density: comfortable. Register: utilitarian (a frequent check-and-act tool). Voice: en-US, sentence case, no humour.
  • Direction: none set, so distinctiveness gives a verdict only and offers directions (see section 5).
  • Audience: a small group at a restaurant, on a phone at the table, and on a laptop when splitting later.
  • Category and peers: not confirmed. Landscape research was not run (see Unverified). Peer claims are absent, so no peer-gap rework is possible.
  • Questions that would have been asked (recorded, not answered): what is the primary device; which bill-splitting apps are the peers; is this a release build or a prototype; whether tip should apply before or after tax (the app uses before tax, as its hint says).

Run notes

  • Tools: Node Playwright 1.64.1 from [local path], Chromium. The Playwright MCP was not used. measure-runner.js logic was reproduced in Node (the runner's file-input trick is MCP-specific), calling scripts/measure.js directly on the page for 3 widths x light/dark x reduced motion (12 combinations). type-eval.js was run with sizes 12, 13, 16.
  • Scripts and evidence (all in work/): measure.js, states.js, probe.js, probe2.js, reflow.js; results in measure-seeded-full.json, measure-summary.json, type-eval.json, states.json; screenshots in shots/.
  • Side effects: localStorage on the file:// origin was written for the test states only. The reviewed file was not edited. No simulator or native change. The .playwright-mcp/ folder was not used.
  • Not done because the user said "proceed on defaults" but the step needs input: no AskUserQuestion; questions recorded above.

2. Findings

Measured results: contrast, 102 rows in the seeded state, 0 fail and 0 unmeasurable at 390, 820 and 1440, light and dark, with and without reduced motion. Targets: all controls are 44 px high. The 20 checkboxes are 22 px inside 44 px label pills; spacing passes (spacingOk true), so they are not findings. transition: all count 0. Reduced-motion rule present. No horizontal overflow at 320, 360, 390, 820 or 1440 with normal text. At 200% root font size at 390 px there is no overflow.

# Finding Where Impact Apple Fix
1 Invalid tax is silently counted as $0.00. Typing abc in Tax shows an inline error, but the results still show "Check total $1,234.50" and "tax $0.00", and the Copy summary text reads "Items $1,234.50 + tax $0.00 + tip $0.00 (0%) = $1,234.50". An invalid tip (150) is likewise silently 0%. model() falls back to 0 on parse error. Tax and Tip fields; Each person pays (#result); Copy summary Major Taste (reviewer judgement: a wrong total is shown and copyable) Keep the last valid total and mark it "Not final: fix tax" (or hide the total); disable Copy summary while tax or tip is invalid, and say why. Example text: "Tax is not counted until it is a valid amount."
2 On a phone (390 x 844, seeded), the Each person pays result starts at y=2420 px, after all items (items start at y=813). The total is about 1,600 px below the first screen of work, and the result is the payoff of the whole task. aside.out at under 900 px; .grid single column Major Taste (reviewer judgement; no Apple rule covers this layout) Make the total a compact bar pinned to the bottom (above the safe area) on narrow widths, showing "Check total $62.40" and a tap-to-expand share list. Keep the full result in place on wide screens.
3 Clicking "Everyone" (and so any button that re-renders the list) leaves focus on <body>. Measured: document.activeElement = BODY after the click. The keyboard user loses position in a list of 5 items. Checkbox toggles do restore focus (they have data-key); the Everyone button does not. Items list, Everyone button (data-all); refresh() re-renders the whole list Major Web (WCAG 2.4.3 focus order: judged, not a measured failure; caps at Major) After re-render, restore focus to the same [data-all] button, or give buttons a data-key as checkboxes already have.
4 Pseudo-localised text (+40%, unbroken runs) at 390 px makes the page scroll horizontally: scrollWidth 683 against 390. The grid track .grid / main grows to about 667 px because of some min-content child; header also overflows to 452 px. Normal text does not overflow. main.grid, header.top Minor Web (reflow is judged here; 1.4.10 holds at 320 with normal text) Add min-width: 0 on the grid children and overflow-wrap: anywhere on labels and hints; re-run the +40% test at 390.
5 Tip presets: when the tip field is blank, the effective tip is 0%, but no preset is pressed ("No tip" has aria-pressed=false). Typing 0 presses "No tip". renderTip() requires st.tipText.trim() !== "" Minor Taste Treat blank as 0: press "No tip" when the field is blank, or show "0%" in the field value.
6 The spare-cents paragraph uses jargon: "handed out, one at a time, to whoever had the largest leftover fraction (ties go to the person listed first)". For a group at dinner this is hard to parse. Each person pays, .rounding Minor Taste (reviewer judgement; voice rule) "The bill did not divide evenly, so 4 cents went to: Ana +3¢ on Carbonara, tax, tip; Dev +1¢ on tax."
7 "12." (a trailing point) is rejected with "Use numbers only, with up to 2 decimals, like 12.50." That message does not describe the problem: the user typed a valid prefix of 12.50. Item Price and Tax fields, parseMoney Minor Taste Accept a trailing point as 12.00, or say "Add the cents, like 12.50 or 12."
8 Undo disappears silently. The undo toast is cleared by save() on any later change, including typing in Tax or Tip (each keystroke calls refresh() then save()). A user who removes Ben, then edits tax, loses the undo with no trace. save() clears toast(""); refresh() on tax and tip input Minor Taste Keep the undo until a new destructive action, or for a fixed time (about 8 s), and don't clear it on field edits.
9 Visual check (screenshots, 390 and 1440, light and dark): hierarchy is clear, the serif headings and terracotta primary action read well, and the dark theme keeps the same roles. Nothing else found on the screens reviewed. All screens Polish Taste None required.

Trust: No deceptive patterns found in what was seen. No fake urgency, no hidden costs, no pre-selected options, no invented counts. The copy does not claim anything it does not compute.

waived: 0

3. Trust

No deceptive patterns found in what was seen. Finding 1 is an accuracy failure (a wrong total presented as final), not a dark pattern, so it stays in Findings at Major.

4. Landscape

Not run. The landscape step (web research of 3 to 6 peers, such as Splitwise, Tab and Settle Up) was not done in this pass. Without it there is no sourced peer step count, so the rework peer-gap condition cannot be tested, and the enhancements below rest on the product's own evidence only. Run this before any rework decision.

5. Distinctiveness verdict

Partly authored. Evidence:

  • Authored: the editorial type pairing (serif Iowan/Palatino/Georgia headings, system sans body), the numbered sections with a "=" marker on the result, the cream surface with a single terracotta accent, and the copy voice (plain, specific: "Nobody assigned yet, so this is not counted").
  • Platform default: system-ui body text, native controls for tip presets and checkboxes, 44 px controls.
  • Library/trend defaults: none observed. No gradients, no shadow cards, no glass.
  • Slop-cluster threshold not met (no more than 2 tells in the scan).
  • Logo-off and competitor-swap tests: not applicable (no logo; no rendered peer to swap against).
  • Direction: none in profile. Options (not required for the verdict): (a) keep the current editorial direction and tighten the result as the centre of the screen; (b) a utilitarian receipt direction (monospace figures, ruled table) that fits the "check" metaphor. Both differ on at least two channels. No theming values proposed beyond what exists.

6. Type

Type evaluation (type-eval.json, sizes 12, 13, 16):

  • System-ui: loaded, 4 sizes in use (13, 14, 15, 16), tabular figures available. Confusables at 12 px: I/l not separated (d 0), an acceptable risk for the money column since figures use tabular numerals.
  • Iowan Old Style / Palatino fallback to Georgia: serif headings, Latin only, rendered via the local face (coverage Latin only). Serif only at headings, so the readable confusable risk is low.
  • Smallest text: 13 px (a single usage). Helper text at 14 px. Acceptable under the 12 px desktop floor; the 16 px mobile body is met for inputs and body.

Defects found here: none beyond the sizes above.

7. Reworks

No rework warranted: none of the five bar conditions holds. Cluster: findings 1 and 2 share a cause only loosely (a result shown in the wrong place versus a validity rule), and no third finding shares it. Premise: the core job is served. Peer gap: not measured (no landscape). Identity: not default-assembled. System: no token problem seen.

8. Enhancements

Up to 3, fewer because the evidence is thin and no landscape was run.

  1. Experience and flows: a bottom total bar on phones. Traces to finding 2 and to the core job of seeing "what do I pay" at the table. Mechanism: a pinned bar with the check total and a tap target that opens the share list; on wide screens the existing sticky column stays. HIG principle: Purpose (the result is the point of the screen). Unlocks: the answer is visible while items are being ticked.
  2. Experience and flows: "Split evenly among" for large groups. Traces to this app's core task (groups of 4 or more, with checkboxes repeated per item). Each item shows one checkbox per person, so 10 items and 6 people means 60 checkboxes. Mechanism: a "Everyone" plus "Choose" pattern already exists; extend it so a user can select the group once and reuse it for other items. HIG principle: Flexibility. Unlocks: fewer taps for a common case. Needs the landscape check before committing.
  3. Content and voice: say what counts. Traces to findings 1, 6 and 8. Mechanism: a single status line under the total that reads "All 5 items and tax counted" or "Tax is missing, so this total is not final", updated on every change, in the same place as the total. HIG principle: Responsibility (the app says what it counted). Unlocks: a trustworthy total without reading the fine print.

9. Unverified

  • Dark mode with the full seeded state at 390 and 1440: measured (contrast 0 fail), screenshot reviewed for the empty state only.
  • Forced colours and prefers-contrast: more: screenshots captured (shots/forced-1440.png, shots/contrastmore-1440.png) but not reviewed by eye. The CSS has a forced-colors block; there is no prefers-contrast rule. Verify by looking at the two screenshots.
  • RTL (shots/rtl-390.png) captured, not reviewed by eye; logicalProps was not run for this page (the runner's output was not summarised for RTL).
  • Offline and error states: the app uses localStorage only, so no network error state exists. Storage-unavailable state (private window with storage blocked) not tested.
  • Transient states: undo toast (seen in text, not by eye), focus rings (checked by computed outline only), the 200 ms rise animation (reduced-motion removes it; measured rule present).
  • Other browsers: Firefox and WebKit not run. Safari/iOS behaviour of viewport-fit, dvh and the sticky column not checked (the page has no viewport-fit=cover, so safe areas do not apply; the bottom toast uses bottom: 16px without env(safe-area-inset-bottom), which matters on iPhone once finding 2's bar is added).
  • Landscape claims: none made; not researched.
  • Keyboard walk: 45 tab stops recorded in states.json; all visible stops had a 3 px focus outline. Full screen-reader pass not run (no VoiceOver).
  • Money edge cases: the arithmetic was spot-checked (shares add to the total, $62.40 on the seeded state), not exhaustively tested.

10. Offer

Fixable in code now, on your word:

  • Copy and status text: findings 6, 7 and enhancement 3 (the total status line).
  • Logic and feedback: findings 1 (block or flag an invalid tax or tip in the total and in Copy summary), 5, 8.
  • Focus and layout: findings 3 and 4.
  • Layout decision first: finding 2 and enhancement 1 (a bottom total bar on phones).
  • Design decision first: enhancement 2 (group reuse) and the direction choice in section 5.

Optional input that would let the review verify more: a running URL on another device or browser for the bottom-bar check, the real device width, and a landscape decision (run the peer research or not). I will not edit any file until you pick findings to fix.

Claude Haiku without loupe: the review it wrote with no skill
<!-- model: claude-haiku; skill commit: 7559367 -->

Design review: Split the check (splitcheck/index.html)

Scope and defaults

  • Reviewed: the single file splitcheck/index.html (HTML, CSS and JS, about 500 lines), driven in Chromium through Playwright.
  • No product owner was available, so these defaults were used:
    • Audience: a group at a restaurant table, usually on a phone, splitting one bill.
    • Target: WCAG 2.2 AA, phone widths (320 to 430 px) and desktop, light and dark system themes.
    • Build status: development. Findings are ranked for pre-release fixing.
  • Evidence: scripts and screenshots are in work (run.js, invalid.js, contrast.js, and PNGs of desktop, mobile, empty and 320 px states).
  • Math check: I worked a 4-item, 3-person, tax and tip example by hand. The displayed shares ($19.63, $16.79, $8.87) and the spare-cent allocation are correct, and shares sum exactly to the $45.29 total. The core calculation is sound.

Summary

The core product works and looks deliberate: clear three-step flow, exact-cent allocation, a receipt-like result sheet, correct math, and good baseline accessibility. The most serious problem is that invalid tax or tip input silently removes tax and tip from every share and from the copied summary. The second group is mobile layout (the answer is far from the inputs) and screen-reader and focus behaviour.

Findings

Severity: High = wrong money, blocked task, or a11y failure for a common path. Medium = friction or real confusion. Low = polish.

High

H1. Invalid tax or tip silently zeroes them in every share. (Verified.)

  • model() uses t.cents || 0 and p.pct || 0. When the field shows an error, the result still computes, with tax or tip set to 0.
  • Repro: items $20.00, tax 2.00, tip 20 gives a bill of $26.00. Typing "20." in tip shows "Use a number like 18 or 17.5." and the bill drops to $22.00. Typing "2." in tax drops the bill to $20.00.
  • "Copy summary" stays enabled and copies the wrong totals.
  • Change: when tax or tip is invalid, show the last valid values with a visible warning in the result ("Tax is not counted until fixed"), or disable Copy and mark the result as incomplete. Never show a total that silently omits a field.

H2. Live validation fires on half-typed numbers.

  • Tax and tip validate on every input event. "3." and "17." produce errors while the user is still typing, and aria-invalid flips on and off.
  • Change: validate on blur and on submit, or accept a trailing decimal point as a valid partial state. Keep the H1 fix so the total still reflects the last valid value.

H3. Focus is lost after "Everyone" and after "Undo". (Verified.)

  • After clicking "Everyone", document.activeElement is BODY. refresh() re-renders the item list, and only checkboxes carry data-key, so focus restoration skips these buttons.
  • After "Undo", focus is also BODY, because the toast (and its button) is removed.
  • Keyboard and switch users lose their place in the list.
  • Change: restore focus to the same control after re-render (give "Everyone" a data-key too), and after Undo move focus to the restored item or to the heading of its section.

H4. Screen-reader users get no feedback on the result, and the item legend is ambiguous. (Verified: the legend is the same text on every item.)

  • The result panel and per-item splits update through innerHTML with no live region. Ticking a checkbox changes the totals silently.
  • Every item's fieldset has the legend "Who had it?", so the checkboxes for "Ana", "Ben" and "Cy" are announced without the item name.
  • Change: use <legend>Who had the Margherita pizza?</legend> (or aria-label on the fieldset). Add a polite live region that announces a short message after a change ("Each share updated. Total $45.29."), not the whole panel.

H5. On mobile, the answer is two screens below the inputs. (Verified: on a 390 px viewport the result starts at y=2171 px, document height is 2895 px, with only 4 items.)

  • With more items the result recedes further. While editing, nobody sees the running total.
  • Change: on narrow screens, add a sticky bottom bar ("Check total $45.29 · See shares") that opens or jumps to the result, or show a compact total under the step 2 heading. The bar must not cover the Undo toast (the toast is already at the bottom).
Medium

M1. The undo toast vanishes on the next tick and never times out. (Confirmed in code.)

  • save() calls toast(""), so ticking any checkbox or typing in tax or tip removes Undo. Undo therefore survives only until the next action of any kind.
  • The bar stays visible indefinitely and sits over the bottom of the page.
  • Change: keep Undo for about 6 to 8 seconds, or until another destructive action, and do not clear it on checkbox changes. Provide a visible dismiss (already present).

M2. The assignment UI does not scale.

  • Each item repeats one pill per person plus an "Everyone" button. With 8 people and 10 items that is 80 pills. The people limit is 30, so one item could show 30 pills.
  • On a phone, "Everyone" wraps to its own line in every card (seen in the 390 px screenshot).
  • Change: on desktop, a matrix (items as rows, people as columns, one checkbox per cell, sticky headers). On phone, either keep the cards but use a "Who had it?" disclosure, or a per-person view ("What did Ana have?"). At minimum, put "Everyone" in the same row as the pills at a fixed width.

M3. Per-item split text does not say who pays the extra cent. (Verified: the note reads "Split 2 ways: $6.25 / $6.25", without names.)

  • With an odd split, for example $6.26 and $6.25, the reader cannot tell who got the extra cent.
  • Change: name the amounts ("Ana $6.26 · Ben $6.25"), or show the note only when the split is uneven.

M4. The spare-cent explanation is jargon. (Seen in the result panel.)

  • "Splitting does not always divide evenly, so 3 spare cents were handed out, one at a time, to whoever had the largest leftover fraction (ties go to the person listed first)" is accurate but hard to scan.
  • Change: "3¢ went to Ben and Cy so the shares add up exactly." Move the detail into the preview or a disclosure.

M5. Persistence is silent. (Verified: the state survives a reload.)

  • Names, items and prices are saved to localStorage under splitcheck.v1, and reloading restores them. Nothing on the page says this, and a second person on a shared device sees the previous bill.
  • Change: a small "Saved on this device" note near "Start over", and keep Start over as the clear action.

M6. Focus after deleting an item jumps to the top form. (Verified: focus moves to #iName.)

  • Deleting item 6 moves focus to the "Item" field at the top of the page.
  • Change: move focus to the next item's delete button, or to the list heading when the list becomes empty.

M7. Tax is a dollar amount but tip is a percentage. This is a defensible choice, but the two fields use different units and a user with a receipt that shows "8.25%" must convert by hand.

  • Change: accept a percent for tax as well, or label the tax hint "Dollar amount from the receipt, not a percentage".
Low

L1. Placeholders look like values. The empty tax field shows "0.00" and the empty tip field shows "0" as placeholders (seen in the empty-state screenshot). Make the placeholder visibly different (for example "none") or use an empty field with the hint carrying the default.

L2. Blank tip and "No tip" disagree. A blank tip counts as 0%, but no preset is highlighted (aria-pressed is false for "No tip" when the field is empty). Either highlight "No tip" when blank or state "Blank means no tip" in the hint.

L3. The sticky result panel is not capped. On a short laptop screen (height under about 700 px) a tall result sheet with many people can be taller than the viewport and cannot be fully read. Add max-height: calc(100vh - 32px); overflow: auto to .out.

L4. Control shapes are mixed. Person and item checkboxes are pills, while "Everyone", "Delete" and "Remove" are rectangles. "Everyone" reads like a fourth person. Give it a distinct treatment (outline text button or a different row position).

L5. The step marker "=" for the result is unclear. It reads as a glyph, not a step. Use "4", or drop the number and keep the heading.

L6. Tax hint wraps to three lines in the narrow field. At desktop width the tax hint ("Total tax on the receipt. Blank means none.") wraps awkwardly under a 140 px field. Shorten it or let the two fields share a row with the hints below.

L7. Undo after Start over is the only recovery path. This is fine, but the "Everything cleared" toast should say what was cleared ("Cleared 3 people and 4 items.") so the user knows what Undo will restore.

What works (keep)

  • Math: largest-remainder allocation, exact to the cent, and the sum of shares equals the bill. Tested by hand.
  • Contrast (computed, WCAG): muted text 6.6:1 on the light background and 7.9:1 on dark; input and control borders 3.4:1 light and 4.9:1 dark (passes the 3:1 non-text rule); error text 7.4:1 and 8.2:1; focus ring 5.9:1 and 9.1:1; accent button text 8.1:1 and 7.6:1. The divider colour (--rule, 1.5:1) is decorative only and is labelled as such in the CSS.
  • Touch targets: every button, checkbox pill and summary is at least 44 px (no violations found by script).
  • No horizontal overflow at 320, 390 or desktop widths. No console errors or page errors in the flow.
  • Escapes user text in every innerHTML path, and guards localStorage in try/catch.
  • Honours prefers-reduced-motion, forced-colors and prefers-color-scheme. The dark palette reads well in the rendered check.
  • Visual hierarchy: serif step headings, a receipt-like result sheet, and restrained colour make the result the strongest element on desktop.

Priority order for the fix

  1. H1 (invalid tax or tip zeroes totals and copy) and H2 (validate on blur), as one change.
  2. H3 and H4 (focus restoration, legends, live region).
  3. H5 (mobile total bar).
  4. M1, M3, M5 (undo lifetime, uneven-split names, saved-on-device note).
  5. M2 (matrix or per-person view), then the remaining medium and low items.

Caveats

  • Playwright was run against Chromium only. Safari and Firefox were not tested, so the :has() pill styling and the forced-colors behaviour are unverified outside Chromium.
  • Screen-reader output was inferred from the DOM and ARIA attributes, not tested with VoiceOver or NVDA.
  • The contrast figures were computed from the hex values in the stylesheet.