loupe

Web page with planted defects

Lendwell, a tool-lending library

A one-page app for borrowing tools from a neighbourhood library, with ten defects planted by a model that was told not to read loupe’s rules and five more found by a checker.

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

Claude Opus: loupe’s report

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

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

Lendwell: design review (loupe)

1. Summary

What was reviewed: the whole app, which is one page (lendwell/index.html) plus the Reserve dialog. I walked the core flow (browse, reserve, check My reservations, cancel). I had the running page and its source. There was no .loupe.md, so the profile below is inferred and was not written to disk.

Verdict: the reserve flow works on the happy path. But a keyboard user can't see where they are, the page has a marquee that never stops, the card notes are unreadable grey, and the dialog wipes the chosen date. The page leads with an 88vh marketing hero, and the tools don't start until y=933 px, below the fold at every width.

Do first: fix the four accessibility Blockers (#1 to #4) and the dialog that loses the date (#5). Then take Rework R1: open on the tools, and book by date.

Assumptions (no owner available; questions recorded, defaults used):

  • purpose: members of a volunteer-run neighbourhood tool library find a tool and reserve it for a pickup day. Inferred from the title, hero and footer.
  • audience: occasional-public. People reserve a tool a few times a year.
  • register: utilitarian. Browsing and reserving is a check-and-act task.
  • motion_personality: serious, relaxed.
  • platforms: web. release_stage: development, as the brief says. density: comfortable, from the 16 px body.
  • voice: inferred traits are neighbourly but not cute, practical but not curt. Spelling is British ("Neighbourhood", "neighbours"). Casing is sentence case for buttons and headings. humor: none. we_policy: avoid.
  • brand.accents: --brand #1f6b4a (index.html:8). brand.typefaces: system-ui stack.
  • category: community tool library / library of things. peers: none given. direction: empty, so I propose directions in §5.
  • waivers: none. The 26+ gate doesn't apply (web only).
  • Questions I would have asked: Is membership (£12) needed before reserving? Where and when is pickup? Do reservations need to block stock by date? Do saved stars need to persist? Is the hero meant for logged-out visitors only?

Run notes:

  • Tools: Playwright (Chromium) driven from Node scripts, in place of the MCP tools.
  • Measurement: skill/scripts/measure-runner.js ran unchanged through work/measure.js (12 combinations: 390/820/1440 × light/dark × motion on/reduce). Output: work/measure.json.
  • Type: skill/scripts/type-eval.js at sizes 13/14/16/18. Output: work/type-eval.json.
  • Flows and stress states: work/drive.js covered screenshots, a Tab walk, the dialog flow, cancel, empty states, 200% text, RTL, pseudo-localisation, forced colours and contrast:more, a 2560 px width, and reduced motion. Screenshots are in work/shots/.
  • Contrast sampling: scripts/png_contrast.py on the hero, and scripts/contrast.py on proposed tokens.
  • Paths: all evidence is under …/runs/[run]/work/. Nothing in lendwell/ was modified.
  • Contrast results: 57 text pairs per combination. Results were identical in all 12 combinations because the page has no prefers-color-scheme rule.
    • 6 fail: .meta #a9b0ab on #fff is 2.21:1 at 14 px.
    • 8 unmeasurable: hero text over the gradient. Pixel sampling shows white on #257854 to #2a835c at 4.67 to 5.39:1, which passes.
  • Targets: nav links are 33.6–115×18 px (spacingOk true). Star buttons are 31.9×32. The select is 42 px tall at 390.
  • Type: 8 distinct sizes, minimum 13 px, all in px.
  • Motion: 1 element animated on load (the ticker), no reduced-motion rule, 0 unreadable sheets.
  • Layout: logicalProps physical 5 / logical 0.
  • Coverage: no iframes or shadow roots. Nothing was sampled or skipped.
  • Landscape: the web searches went to third-party sites (cited in §4).

2. Findings

# Finding Where Impact Apple Fix
1 Focus is invisible on every control. button,input,select,a{outline:none} has no replacement style. The Tab walk shows outline: none, box-shadow: none at all 16 stops (nav links, search, category, stars, Reserve). This fails WCAG 2.4.7 (AA). Whole page, index.html:27 Blocker Web Delete line 27. Add :focus-visible{outline:2px solid var(--brand);outline-offset:2px}, and a white ring on the hero and toast.
2 The ticker scrolls forever with no pause, stop or hide control. Under prefers-reduced-motion: reduce it still reports running (measured). It also carries the "Library closed Bank Holiday Monday" notice, which members need. This fails WCAG 2.2.2 (A), and Apple's Reduce Motion SHOULD on repeating animation (content-a11y.md:45). Ticker under the header, index.html:14–16, 53 Blocker Web Replace the marquee with a static notice line. Put closures first: "Closed Monday 31 Aug (bank holiday)". At minimum, stop the animation under prefers-reduced-motion and add a pause button.
3 Card meta text (category · note) is #a9b0ab on #fff at 2.21:1, 14 px, on all 6 cards. That includes "Back 14 Nov", the only explanation of why a tool can't be reserved. This fails WCAG 1.4.3 (needs 4.5:1). Browse tools, every card .meta, index.html:33 Blocker Web Use --muted #5f6b64 (5.56:1 on #fff, measured) and drop --faint for text.
4 The save star has the accessible name "☆" ("white star") and no aria-pressed. Toggling only swaps the glyph to ★, so screen readers get neither its purpose nor its state. This fails WCAG 4.1.2. The star also saves nowhere (nothing persists, there is no saved list); see Unverified. Every card, top-right .save, index.html:98 Blocker Web <button aria-pressed="false" aria-label="Save Cordless drill">, and toggle aria-pressed. Remove the star if "saved tools" isn't a planned feature.
5 Changing Loan length rebuilds the whole dialog (renderDlg() on change). The chosen pickup date is cleared (measured: 2026-11-05 became "") and focus drops to <body>. Submit then fails with "Please fill out this field." The member loses input and keyboard position mid-task. This breaks Apple's "Avoid changing focus without user interaction" (inputs-tech.md, Focus and selection). Reserve dialog, index.html:117 Major SHOULD Don't re-render on change. Read both fields on submit, or store date from the input's change event and keep focus on the select.
6 Cancel deletes a reservation on one click, with no confirm and no undo. The toast says "Done" for 3 s and focus falls to <body> (measured). Apple: undo makes exploration safe; on the web, put "Undo" in the toast after a destructive action (patterns.md, Undo and redo). My reservations, Cancel button, index.html:106–108 Major SHOULD Remove the row, then show a toast with an Undo button for at least 6 s: "Socket set reservation cancelled. Undo". Move focus to the next row's Cancel button, or to the heading.
7 The first screen answers a visitor's question, not a member's. The 88vh hero, stats and slogan push "Browse tools" to y=933 at 390, 820 and 1440 wide (viewport 900, measured). No tool, search or reservation is in the first viewport. This breaks Apple's Purpose principle: every screen serves the one-sentence purpose (red-team.md, attack ladder 1). Hero, index.html:17, 55–59 Major SHOULD Cut the hero to one line plus search, or move it below My reservations for returning members. See R1.
8 No dark appearance and no increased-contrast variant. There is no prefers-color-scheme or prefers-contrast rule and no color-scheme. Literal hex values are used for UI and text (header #fff, ticker #e7efe9, star #444, toast #1f2a24, dot #b3472f). The page renders identically in light and dark (measured). This breaks Apple's "use semantic/adaptive colors… no hard-coded values" (visual.md, Dark Mode). :root tokens and inline literals, index.html:8–48 Major SHOULD Move every literal into :root roles and add a @media (prefers-color-scheme: dark) set: bg #151a17, text #e6ebe7 (14.6:1), muted #9aa59f (6.93:1), accent #7fd1a6 (9.71:1), all computed. Declare color-scheme: light dark.
9 Empty states are blank. With no reservations, #resv is "" and only the heading remains. A search for "chainsaw" gives an empty grid with no message (measured). Apple: every empty state gives a next step (writing.md, Empty states). My reservations; Browse tools search Major SHOULD Reservations: "No reservations yet. Pick a tool above and choose a pickup day." Search: "No tools match ‘chainsaw’. Try another word or show all categories" (link that resets the filter).
10 Confirmations say nothing. Reserving shows "Success" and cancelling shows "Done", both gone in 3 s. Neither names the tool, the date or where to collect it. Apple: for routine success, state the result (writing.md, Success and feedback). Toast, index.html:108, 118 Major SHOULD "Cordless drill reserved: collect Thu 5 Nov, back Sun 8 Nov." Keep it in My reservations as the lasting record, and highlight the new row.
11 The disabled Reserve on Wallpaper steamer looks the same as the enabled ones (same fill and colour, no :disabled style; see work/shots/full-1440.png). It gives no reason. The return date sits in the 2.21:1 meta line. Disabled buttons can't take focus or explain themselves (web-mapping.md, Forms). Wallpaper steamer card Major Web Replace the button with the text "Out on loan, back Sat 14 Nov", or an enabled "Reserve from 14 Nov". Give .btn:disabled a distinct style.
12 At 390 px the page scrolls sideways. scrollWidth is 664 because the reservations table has min-width:640px and no scroll wrapper. The header, ticker and hero stop at 390 while the table runs off-screen (work/shots/full-390.png). This breaks WCAG 1.4.10 Reflow; the data-table exemption is arguable for a 4-field list, so this is judged Major. My reservations table, index.html:38 Major Web Below about 600 px, render each reservation as a stacked row (tool, date, length, Cancel), or wrap the table in overflow-x:auto.
13 All type is in px (body 16px, h2 22px, meta 14px…). Setting the root font size to 200% left body text at 16 px (measured). Browser zoom still works, so WCAG 1.4.4 passes, but the user's font-size setting is ignored. index.html:10–48 Major Web Convert sizes to rem (16→1rem, 14→0.875rem, 13→0.8125rem…).
14 "N available" never changes. After reserving a Cordless drill it still says "3 available" (measured). You can reserve the same tool again for the same day, and the count isn't tied to the pickup date you pick. The model promises availability it can't check. Reviewer judgement. Card .avail; Reserve dialog Major Taste Show availability for the chosen date ("2 of 3 free on Thu 5 Nov"), block dates when none are free, and decrement on reserve. See R1.
15 Pickup says "Collect from the usual place" on every row. A first-time member has no "usual place", and no address or opening time appears anywhere. The cognitive walkthrough fails at step 4 (seeing what happens next). Reviewer judgement. My reservations, Pickup column; dialog Major Taste Name the place and slot, e.g. "Alder Street garage, Sat 10:00–13:00". If it's the same for every row, show it once above the table and delete the column.
16 The slop-cluster threshold is met: 7 tells across the layout, motion and colour channels. They are a hero metric row (412 / 181 / 3,960), a decorative 135° gradient hero that encodes nothing, a near-viewport-height hero (88vh), a perpetual marquee, no reduced-motion branch, disabled buttons that look enabled, and a blank search result. See §5. Hero, ticker, cards Major Taste See R2. Swapping tells one at a time would only produce the next default.
17 The dialog buttons "Close" and "Submit" don't say what happens. Apple: dialog buttons name the specific action (writing.md, Labels and buttons). Reserve dialog, index.html:115 Minor SHOULD "Cancel" and "Reserve drill" (use the tool name).
18 Targets are small. Nav links are 18 px tall (spacingOk true, so WCAG passes), and star buttons are 31.9×32, against a 44 px goal on touch. Header nav; card stars Minor Web Pad nav links to 44 px high and make the star 44×44.
19 Filtering by search or category isn't announced, and no result count is shown. Screen-reader users get no feedback that the grid changed (content-a11y.md web translation: aria-live for async changes). Browse tools Minor Web Add a visible "6 tools" / "1 tool" line with aria-live="polite".
20 The <dialog> has no accessible name (no aria-labelledby pointing to "Reserve Cordless drill"). Reserve dialog, index.html:79, 112 Minor Web Add an id to the h2 and aria-labelledby on the dialog.
21 In RTL, the star sits on top of the tool name (position:absolute; right:10px) and the status dot uses margin-right. Measured: 5 physical properties, 0 logical (work/shots/rtl-390.png). Cards Minor Web Use inset-inline-end:10px and margin-inline-end:6px, and reserve padding for the star.
22 Dates are shown three ways. The table shows ISO "2026-11-02", the date picker shows dd/mm/yyyy, and the notes say "14 Nov". None gives a weekday, and pickup depends on weekday (the help line says Saturday volunteers). Reviewer judgement. My reservations; dialog; card note Minor Taste Format with Intl.DateTimeFormat('en-GB',{weekday:'short',day:'numeric',month:'short'}), e.g. "Mon 2 Nov".
23 The pickup date has min="2026-10-27" hard-coded, so it goes stale tomorrow. It doesn't limit dates to collection days. Reviewer judgement. Reserve dialog, index.html:113 Minor Taste Compute min from today, and limit choices to real collection days once they're known.
24 The hero claims "keeping hundreds of drills out of landfill and a lot of money in your pocket". This is an unsourced figure beside real counts, and the catalogue holds 3 drills. Anti-fabrication. Hero paragraph Minor Taste Use the real figure: "Since 2019, neighbours have borrowed tools 3,960 times instead of buying them."
25 +4 polish: <br>1. The h1 "Share more. Own less." breaks with "less." alone at 1440 (max-width:14ch).<br>2. 8 type sizes on one page (13, 14, 16, 18, 20, 22, 28, 64) where 5 would do.<br>3. The last table header is empty, so the action column has no name for screen readers.<br>4. The search has no visible label (placeholder only; aria-label exists). Hero; type scale; table; search Polish Taste 1. Balance the headline with text-wrap: balance.<br>2. Merge 13/14 and 20/22.<br>3. Add a visually hidden "Actions" header.<br>4. Add a visible "Search tools" label.

waived: 0

3. Trust

I found no deceptive patterns in what I could see. Note two things:

  • The hero claim (#24): "hundreds of drills out of landfill" is unsourced. It stays Minor because I can't show it is invented. If the team has no figure behind it, it becomes fabricated social proof.
  • The membership fee: the £12 a year appears only in the footer. If membership is required before a reservation is honoured, the Reserve flow hides a cost. This is unverified; see §9.

4. Landscape

Category: community tool library / library of things. I did a short study, scaled to a single-page app, checked 2026-10-26.

Products studied:

  • myTurn, the reservation platform many tool libraries use (category leader). Members reserve a specific period instead of joining a hold queue, and can filter by what's free in a date range. Sources: JCLS, Indiana Humanities guide, KDLA training.
  • Lend Engine, a challenger built for small volunteer libraries. Its member site shows availability and online reservation, has a waiting list for unavailable items with an automatic email when an item comes back, and can mark items as in repair. Sources: lend-engine.com, for tool libraries, pricing.
  • Library of Things (UK), a peer with self-service kiosks. You reserve online and collect from a named kiosk or locker; if the item is available you can collect it right away. Source: How borrowing works.
  • Brunswick Tool Library, a volunteer-run peer. Members pick a date, add tools and submit, with a confirmation email; pickup is limited to certain weekdays. Source: how to reserve tools.
  • Adjacent exemplar: car clubs such as Zipcar (unverified, from memory). Search starts with "when", and every result shows availability for that time slot.

Conventions to keep:

  • Reserve a specific date range, not a queue (myTurn).
  • Show the pickup location next to the booking (Indianapolis PL via myTurn; Library of Things kiosks).
  • Confirm by email (Brunswick).

Convergence to avoid:

  • Generic catalogue grids with a stock "Reserve" button and no date context. Every myTurn and Lend Engine embed looks the same.

Frontier moves:

  • Date-first availability filtering (myTurn, KDLA).
  • A waitlist with an automatic "back in" email for out-on-loan items (Lend Engine pricing page).
  • Collection from a named locker (Library of Things).

Openings (things no studied product does that Lendwell's material justifies):

  • Lendwell's practical handling notes ("Heavy, bring a trolley", "Petrol, bring own fuel") are its own content. No peer brings this kind of note into the booking confirmation.
  • Volunteer-staffed Saturday collection could become the pickup slot itself, instead of an "usual place".

Step counts (none sourced as a count, so not used for the peer-gap bar):

  • Lendwell's core task, as measured: scroll past the hero → Reserve → pick a date → pick a length → Submit. That is 4 inputs plus a scroll of 933 px.
  • myTurn: pick dates → add items → submit, per the public guides; the count is unverified.

5. Distinctiveness verdict

Verdict: default-assembled. The slop-cluster threshold is met, which forces this verdict.

Channels:

  • Type: platform default (system-ui, untouched).
  • Colour: trend default. A sustainability green and a decorative 135° gradient hero, where the gradient encodes nothing. The green itself traces to the brand token and is kept as an asset.
  • Shape: library-style default (8/12 px radii, 1 px hairline cards).
  • Motion: trend default (perpetual marquee).
  • Iconography: default (a Unicode ☆ glyph and coloured dots).
  • Imagery: none (gradient only).
  • Sound and haptics: n/a.
  • Layout idiom: trend default (hero, then a 3-number metric row, then a card grid).
  • Voice: authored. "Started in a garage on Alder Street in 2019", "bring a trolley", "ask a volunteer on Saturday" trace to the subject.

Counts:

  • 1 of 9 channels authored.
  • No signature element; 0 signature moments.
  • Catalog tells, AI-template section: 1 hero metric row, 2 perpetual idle motion, 3 motion that ignores Reduce Motion, 4 decorative gradient, 5 a disabled state that looks enabled, 6 a blank search result list, 7 a vh-sized hero.

Tests:

  • Logo-off: nothing in the layout or colour identifies Lendwell. Only the copy does.
  • Competitor swap: you could drop in any myTurn or Lend Engine catalogue embed and nobody would notice.

Proposed directions (no direction set; pick one):

A. "On the calendar": time is the material. A loan is a slot of days, so the identity lives in dates.

  • Colour roles:
    • light: bg #faf8f4, text #1f2a24, muted #5f6b64 (5.25:1 on bg), free-day accent #1f6b4a (6.44:1 on #fff), booked/out #a8432a (6.0:1)
    • dark: bg #151a17, text #e6ebe7 (14.6:1), muted #9aa59f (6.93:1), accent #7fd1a6 (9.71:1), out #e8836b (6.62:1)
    • All computed with contrast.py. The green is kept from the brand token.
  • Type: system-ui for UI, with tabular figures (font-variant-numeric: tabular-nums; available per type-eval) for every date and count.
  • Shape: a square-cornered day cell, 4 px radius.
  • Motion: none on idle. A 150 ms fade when the date changes (reduced motion: instant).
  • Icons: none beyond a calendar glyph from one open set.
  • Signature element: a 7-day strip on each card showing free and booked days.
  • Signature moment (behavioural): pick a day once and the whole grid re-sorts to what's free that day.
  • Voice samples:
    • "Free Thu 5 to Sun 8 Nov."
    • "Back Sat 14 Nov. Tell me when it's back?"
    • "Cordless drill reserved for Thu 5 Nov."
  • Against peers: myTurn filters by date range but shows the same tiles. Library of Things leads with lockers. Lendwell would show time on every card.

B. "The Saturday table": volunteers and handling notes are the material. The library is people at a table in a garage.

  • Colour roles:
    • light: paper #ffffff, ink #1f2a24, green #1f6b4a for one primary action per view, note text #5f6b64 (5.56:1)
    • dark: as in A
  • Type: Atkinson Hyperlegible (OFL) for UI and notes. It needs a type-eval run before adoption; system-ui has I/l rendering identically at 14 and 16 px. System-ui is the fallback.
  • Shape: notes set as small handwritten-style labels is ruled out (kitsch). Plain inset note blocks instead.
  • Motion: none.
  • Icons: one stroke family at 1.5 px, used only for handling notes (weight, fuel, safety).
  • Signature element: a "Before you collect" block on each tool and in the confirmation, carrying its handling note.
  • Signature moment (verbal): the confirmation names the volunteer slot.
  • Voice samples:
    • "Heavy. Bring a trolley or a friend."
    • "Collect Saturday 10:00–13:00 at the Alder Street garage."
    • "Back 14 Nov. We'll email you."
  • Against peers: Lend Engine and myTurn treat notes as metadata. Library of Things removes people (lockers). Lendwell would put its people and its practical knowledge in front.

6. Type

Face: system-ui (SF Pro on this macOS run) 400/700. Roles: UI, reading, data, display
Legibility: CONCERN. x-height ratio 0.72; confusables failing I/l @14px and 16px (identical render, Chromium); weights 400, 700; Bold Text n/a
Figures and features: tabular available, not by default; distinct zero n/a (no zero-heavy data); alternates used none
Coverage: system font, Latin assumed; other scripts unchecked
Scaling: rem issue. All sizes in px, so root 200% has no effect (#13); 1.4.12 not tested
Delivery (web): no font files (0 KB), system stack with Segoe UI and sans-serif fallback
Distinctiveness: default tier yes; identity carried by nothing
Verdict: Tune (rem sizes; tabular-nums on counts and dates; 5 sizes instead of 8). Replace only if direction B is chosen (constraint: I/l distinction for tool names and model numbers; candidate Atkinson Hyperlegible, not yet measured)
Evidence: work/type-eval.json, work/shots/full-1440.png

7. Reworks

R1. Open on the tools, and book by date (bar: Premise)

  • What changes:
    • Before: an 88vh marketing hero and ticker sit above a catalogue whose counts ignore dates. Reservations are a separate table below.
    • After: the first viewport holds "When do you need it?" (a day picker defaulting to the next collection day), search, and the grid filtered to what's free that day. An upcoming reservation shows as one line above the grid. The story and stats move to a short About section below the catalogue.
  • Evidence:
    • #7: tools start at y=933 at every width.
    • #14: availability is not tied to the date.
    • #15: pickup place and time are unknown.
    • #10: the confirmation carries no date or place.
    • The core question for a member is "can I get X on Y?". The first screen answers neither.
    • Peers book by date (myTurn, per the JCLS and KDLA sources).
  • Cost: one page. The hero, the cards (date-aware availability) and the dialog (the date moves out of it, leaving only length) change. It needs a per-date stock model and some learnability work for members used to scrolling.
  • Unlocks:
    • The dialog shrinks to one decision.
    • Out-on-loan tools can offer "Reserve from 14 Nov".
    • Double-booking stops.
    • A waitlist email becomes possible.
  • Smallest experiment: a throwaway prototype of the top 900 px with a date chip and a filtered grid. Five members try to "get a hedge trimmer for next Saturday"; compare time and errors with the current page.

R2. Identity from Lendwell's own material (bar: Identity)

  • What changes:
    • Before: gradient hero, metric row and marquee, with the voice as the only authored channel.
    • After: one direction from §5 (A or B) carried through the colour roles, the signature element and the confirmation copy.
  • Evidence: #16 and the §5 counts. Removing the gradient or the ticker alone would leave a plain card grid, the next default.
  • Cost: a token layer (all literals are currently inline; #8), card and confirmation templates, no new screens.
  • Unlocks: dark mode and increased contrast from the same roles, and a page a neighbour would recognise as Lendwell's.
  • Smallest experiment: mock one card and one confirmation in each direction, then run a five-second recall test with members.

8. Enhancements

Evidence is moderate, so there are three, not eight.

Experience and flows: "Tell me when it's back" on out-on-loan tools (Agency)

  • Where: the Wallpaper steamer card shows "Back 14 Nov" behind a dead-looking button.
  • What: a one-tap request that emails the member on return. Lend Engine offers this on every plan (pricing page).
  • Unlocks: fewer volunteer emails asking "is it back?".

Content and voice: put handling notes in the confirmation (Responsibility)

  • Where: "Heavy, bring a trolley" and "Petrol, bring own fuel" sit in the 14 px meta line and vanish after booking.
  • What: repeat the note in the reservation row and the confirmation ("Lawn aerator, Sat 7 Nov. Heavy: bring a trolley").
  • Unlocks: fewer wasted trips. This is the opening in §4.

Information architecture: replace the ticker with pinned notices (Simplicity)

  • Where: closures, new tools and volunteer calls are three different kinds of message in one moving line.
  • What:
    • Closures go into the date picker as unavailable days.
    • "New this week" becomes a sort or filter in the grid.
    • Volunteer calls go in the footer.
  • Unlocks: each message appears where the member acts on it.

9. Unverified

  • Save star: has no persistence and no saved list. It may be unfinished; dead controls are Major in development. Check whether stars should persist and where saved tools appear.
  • Membership: whether members must pay or sign in before reserving (footer £12). Owner confirmation and a sign-in flow would verify.
  • Loading, error and offline states: not exercised. The data is hard-coded, with no network requests. Verify once an API exists, using page.route aborts and setOffline.
  • Validation: only the browser's native "Please fill out this field." was seen. Past dates, closure days and overlapping reservations were not tested beyond min.
  • Toast: has role="status", but whether screen readers announce it was not confirmed. Test with VoiceOver and NVDA.
  • Forced colours and contrast: more: screenshot taken (work/shots/forced-1440.png) but not judged in detail. Availability dots lose colour there; the text still carries the meaning.
  • Pseudo-localisation (+40%): captured (work/shots/pseudo-390.png, pseudo-1440.png) but not examined row by row. Check card and button wrapping by eye.
  • Very wide (2560): captured (work/shots/wide-2560.png) and not judged beyond the 980 px max-width.
  • Browsers: Firefox and WebKit were not run. Repeat the measurement and type-eval in those engines.
  • Landscape: step counts are not sourced as counts, so R1 does not claim the peer-gap bar. The Zipcar exemplar is from memory (unverified).

10. Offer

Fixable in code now:

  • Focus and keyboard: 1, 5 (focus part), 6 (focus part), 20
  • Motion: 2
  • Contrast and tokens: 3, 8, 11 (styling)
  • Accessible names and states: 4, 19, 20, 25.3
  • Dialog bug: 5
  • Undo on cancel: 6
  • Copy rewrites: 9, 10, 17, 22, 24, 25.4
  • Layout: 12, 13, 18, 21, 25.1, 25.2
  • Dates: 22, 23

Need a design decision first:

  • 7 (hero placement), 14 (per-date stock model), 15 (the real pickup place and time), 16 (direction A or B).
  • Whether the star (4) stays, and the membership question.

I can build an enhancement (the waitlist, or handling notes in the confirmation) or a throwaway prototype of R1's top viewport. Which would you like?

Optional input that would let me verify more: the real pickup rules and location, and an API or backend so I can test loading, error and offline states.

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

Lendwell: UI/UX design review

Scope: lendwell/index.html, a single 7 KB file with inline CSS and JS. I read the source and drove it in Chromium with Playwright at 1280×800 and 375×740. The script and screenshots are in work/.

Assumptions (no owner was available):

  • The primary users are existing members who come back to find a tool, reserve it and manage their bookings. Recruiting new members is a secondary goal.
  • The product is UK-based (£, "Bank Holiday", "neighbours"), so dates should be shown in UK format.
  • It is a prototype. The data is hard-coded, there is no backend or login, and "today" is 2026-10-26. I judged the interaction design as if the data were real.
  • The target is WCAG 2.2 AA on desktop and mobile web.

Severity scale: Critical means a user loses data, can't finish a task, or is blocked by an accessibility failure. High means the user is likely to be confused or make an error, or there is a clear AA failure. Medium means friction or a missing state. Low means polish.


Summary

The visual base is calm and suitable: a warm off-white page, one green brand colour, readable system type and simple cards. The problems are in behaviour and states, not in looks. The core flow, reserving a tool, has a bug that loses data. Keyboard users get no focus indicator at all. Unavailable tools look reservable. Inventory never changes after a booking. The reservations table breaks the layout on phones. Every feedback message says only "Success" or "Done". The landing hero also fills 88% of the first screen with marketing that returning members have to scroll past every visit.

Fix the four Critical items before anyone outside the team uses this.

# Issue Severity
1 Changing the loan length wipes the chosen pickup date and drops keyboard focus Critical
2 All focus outlines removed (outline:none) Critical
3 Reserving doesn't reduce stock, and you can double-book without limit Critical
4 Reservations table forces horizontal scroll on mobile and hides "Cancel" Critical
5 Disabled "Reserve" looks identical to the enabled button High
6 Cancel has no confirmation or undo, and the feedback is just "Done" High
7 Auto-scrolling ticker can't be paused and ignores reduced-motion High
8 Card metadata text is 2.2:1 contrast High
9 Save/star button has no name, no state and doesn't persist High
10 88vh marketing hero pushes the actual product below the fold High
11 Vague copy: "Submit", "Success", "Done", "Collect from the usual place" Medium
12 Nothing is shown when there are no results or no reservations Medium
13 Dialog doesn't show the return date or a summary, and the date limit is hard-coded Medium
14 ISO dates and "1 days" in the reservations table Medium
15 Out-of-stock tools are a dead end (no "notify me" or next free date) Medium
16 Hero body text on the light end of the gradient is about 4.0:1 Low
17 Heading structure, landmarks and the stats-vs-catalogue mismatch Low
18 Generic template look in the hero Low

Critical

1. Changing the loan length erases the pickup date and loses focus

Where: renderDlg(), line 117: $("#n").onchange = e => { days = +e.target.value; renderDlg() }

What happens: changing the loan length re-renders the whole dialog. The date the user typed is never saved to the date variable, so it comes back empty. I confirmed this in the browser: I set the date to 2026‑11‑05, changed the length to 3 days, and the date read back as "". Focus also jumped to <body>, so a keyboard or screen-reader user is thrown out of the modal's flow. Most people pick the date first and the length second, which is exactly the order that triggers the bug. They then see "Please fill out this field" on a field they had already filled in.

Fix: don't re-render on change. Nothing in the dialog depends on days yet. Delete the onchange handler and read both fields on submit. If you later add a live return-date line (see #13), update only that text node.

2. No focus indicator anywhere

Where: line 27: button,input,select,a{outline:none}

What happens: in the browser, tabbing to the nav link gave outline-style: none and no box-shadow. Keyboard users can't see where they are on any link, input, card button, star, dialog control or Cancel button. This fails WCAG 2.4.7 (Focus Visible), and also 2.4.11/2.4.13 in 2.2.

Fix: delete line 27 and add a visible ring that only shows for keyboard focus:

:focus-visible{outline:3px solid #1f6b4a;outline-offset:2px}
.hero :focus-visible,.btn:focus-visible{outline-color:#0b3d27}
3. Inventory never changes, so double-booking is unlimited

Where: the #f.onsubmit handler (line 118) adds to resv but never decrements qty, never re-renders the grid, and doesn't check for conflicts.

What happens: after reserving a Cordless drill, the card still said "3 available". I then reserved it again with no warning, so the same member held two overlapping drill bookings. Cancelling doesn't put stock back either. For a lending library, the availability count is the whole promise. If it's wrong, members turn up and leave empty-handed.

Fix:

  • Decrement qty when a booking is made and increment it on cancel.
  • Re-render the cards after both.
  • Availability really depends on dates (a tool can be free on the 5th and out on the 9th), so model it per day. Disable or flag dates that are already taken in the picker.
  • Warn when the member already holds that tool for overlapping days.
4. The reservations table breaks the mobile layout

Where: line 38: table{min-width:640px} with no scroll container.

What happens: at 375 px wide, document.scrollWidth is 664 px, so the whole page scrolls sideways. The Pickup column and the Cancel button are off-screen (see work/mobile-resv.png). A member on their phone, which is the likely device on pickup day, can't cancel a booking without first discovering that the page scrolls horizontally.

Fix: remove min-width. Below about 600 px, show each reservation as a stacked card ("Socket set · Mon 2 Nov · 2 days · Cancel"). If you keep the table, wrap it in <div style="overflow-x:auto"> so only the table scrolls.


High

5. Unavailable tools look reservable

The disabled "Reserve" on the Wallpaper steamer has the same computed style as the enabled buttons: green background rgb(31,107,74), opacity 1 and cursor:pointer. The only clue is a small red dot (status shown by colour alone, WCAG 1.4.1) and the faint text "Back 14 Nov". Users will click it and get no response.

Fix: add .btn:disabled{background:#e4e2dc;color:#5f6b64;cursor:not-allowed}. Better still, swap the button for an action that does something (see #15), and make the status text explicit, for example "Out on loan · back Sat 14 Nov".

6. Cancel is instant and permanent, with no meaningful feedback

cancelR deletes the row straight away and shows the toast "Done". A mis-tap on mobile, where Cancel sits next to the row, silently loses a booking. Once the stock fix in #3 is in, someone else could take that tool in the meantime.

Fix: make the toast say what happened and offer undo: "Reservation for Socket set (Mon 2 Nov) cancelled. Undo". Keep the toast up for about 6 seconds and pause the timer on hover or focus. Name the button for screen readers too: aria-label="Cancel Socket set reservation". If you prefer a confirm step instead of undo, use a small <dialog>, not confirm().

7. The ticker moves constantly and can't be stopped

.ticker span loops every 14 seconds forever. The browser test showed the animation still running under prefers-reduced-motion: reduce. This fails WCAG 2.2.2 (Pause, Stop, Hide) and is a known trigger for people with vestibular or attention conditions. The format also hurts the content. "Library closed Bank Holiday Monday" is operational information that matters, and a scrolling ticker is the hardest place to read it, because users have to wait for it to come round.

Fix: replace the ticker with a static notice bar showing the single most urgent item (the closure), plus a "News" link for the rest. If you keep the motion, wrap it in @media (prefers-reduced-motion: no-preference) and add a pause control.

8. Card metadata is too faint to read

.meta uses --faint #a9b0ab on white, which is about 2.2:1; AA needs 4.5:1. This text carries real decision information: category, "Petrol, bring own fuel", "Heavy, bring a trolley" and "Back 14 Nov". The Help section already overrides it inline with style="color:var(--muted)", which suggests the faint value was known to be a problem.

Fix: use --muted #5f6b64 (about 5.5:1) for .meta. Keep --faint only for borders and other decoration.

9. The star (save) button doesn't work as a control
  • It has no accessible name, so screen readers announce "☆ button".
  • It has no aria-pressed, so the state is never announced.
  • It doesn't persist: I starred a card, typed in search, cleared it, and the star was empty again because renderTools() rebuilds the HTML.
  • Nothing in the app uses saved items. There is no "Saved" filter or list.
  • The hit area is about 20 × 24 px, below the 24 px minimum in WCAG 2.5.8 and well below the roughly 44 px touch guidance from Apple and Google.

Fix: either remove it until saving has a purpose, or do it properly. Doing it properly means storing saved IDs in state (or localStorage), rendering aria-pressed and aria-label="Save Cordless drill", giving it a 44 × 44 px target, and adding a "Saved" chip next to the category filter.

10. The hero hides the product

The hero is min-height:88vh. On a 1280 × 800 desktop, "Browse tools" starts at y = 845, below the fold. On a 375 × 740 phone it starts at y = 792. Every visit, a returning member scrolls past a mission statement and three vanity numbers before reaching search. The page's main job is reserving tools, not persuading people to join.

Fix:

  • Shrink the hero to a slim band (about 160–220 px). Put search inside it, so the first thing on screen is "What do you need to borrow?" with a search field and category filter.
  • Move the origin story and stats to an "About" section further down, or show them only to logged-out visitors.
  • Make the header sticky so "My reservations" is always one tap away.

Medium

11. Vague copy at the important moments
Now Problem Suggested
Dialog button "Submit" Says nothing about what happens "Reserve drill" or "Confirm reservation"
Dialog "Close" Unclear whether it saves anything "Cancel" (and rename the table action to "Cancel booking" so the two don't clash)
Toast "Success" Doesn't say what succeeded "Cordless drill reserved for Thu 5 Nov–Sat 7 Nov. Pick up at the Alder St garage, Sat 10–1."
Toast "Done" See #6 "Reservation cancelled. Undo"
"Collect from the usual place" New members don't know the usual place, and every row repeats it Give a real address and opening hours once, above the table, with a map link
"Out on loan" "Back 14 Nov" is in the faint meta line "Out on loan · back 14 Nov" in the status line
"Help" section One sentence, and the email isn't a link Make it mailto:, add hours, the pickup location, and the late-return and damage policy
12. Missing empty states
  • No reservations: #resv renders an empty string, leaving just a heading with nothing under it (confirmed). Show "You have no reservations. Browse tools ↑" instead.
  • No search results: searching "zzz" leaves the grid blank with no message. Show "No tools match 'zzz'. Try another word, or ask us to add it" and a clear-filters button.
  • No results count: add "6 tools" or "2 of 6 tools" next to the filters, in an aria-live="polite" region so screen-reader users hear the results change.
13. The reservation dialog doesn't give enough information to decide
  • It never shows the return date. "3 days" from a Thursday means… what, exactly? Show "Return by Sun 8 Nov" and update it live.
  • It never shows pickup hours. If the library only hands tools out on Saturdays (the Help copy hints at volunteers on Saturdays), the date picker should only allow valid days.
  • min="2026-10-27" is hard-coded and will be wrong from tomorrow. Compute it from today.
  • The item note ("Petrol, bring own fuel") isn't repeated in the dialog, which is where it matters.
  • A "1 week" option in a library open some days only raises questions about the return day. State the rule.
14. Date and pluralisation formatting

The table shows raw ISO dates ("2026-11-02") on a UK site, and a 1-day loan shows as "1 days" (confirmed). Use toLocaleDateString('en-GB',{weekday:'short',day:'numeric',month:'short'}), which gives "Mon 2 Nov", and pluralise properly with Intl.PluralRules or a simple ternary.

15. Out-of-stock tools are a dead end

When a tool is out, the only response is a disabled button. Tool libraries live on hand-offs, so give users a next step:

  • "Reserve from 14 Nov", which opens the dialog with the date pre-set
  • "Notify me when it's back"
  • "Similar: Steam cleaner (2 available)"

Low

16. Hero text contrast at the gradient's light end

White on #2f8f65 is about 4.0:1. That's fine for the 64 px headline, but the 18 px paragraph and the 16 px stat labels fall just under AA where they cross the lighter half. Use a darker second stop (for example #246f4f), or keep the text over the left, darker side, which is what happens on desktop today.

17. Structure and semantics
  • The only <h1> is the slogan, and the product name is a <b>. Make "Lendwell" a home link. Consider using the hero <h1> for the page's purpose ("Borrow tools from your neighbours").
  • Each card's "Reserve" button has the same name. Add aria-label="Reserve Cordless drill", or use aria-describedby pointing at the h3.
  • The status dot needs a text equivalent. It has one ("3 available"), so just mark the dot aria-hidden="true".
  • The hero says "over 180 tools", the stats say 181, and the catalogue lists 6. Placeholder data is fine for a prototype, but don't let the numbers contradict each other once real data is in.
  • Rendering with innerHTML and string-built onclick attributes will become an injection risk once tool names come from a backend or volunteer input. Escape the values or build nodes with DOM APIs.
  • The toast's setTimeout timers aren't cleared, so two actions within 3 seconds make the first timer blank the second message early.
18. Generic look

A full-bleed diagonal green gradient, a two-beat slogan ("Share more. Own less.") and a row of three big stats is the default SaaS-landing template. It says nothing about a volunteer-run library in a garage on Alder Street, which is Lendwell's most distinctive asset. Lean into that story:

  • a real photo of the shelf or the volunteers
  • tool cards with photos or simple line icons, so people can scan by shape
  • a "how it works" strip (Reserve → Pick up Saturday → Return)
  • tone that sounds like a neighbour, not a startup

What's working

  • The palette is restrained and the type is legible, with a sensible 980 px content width and a responsive card grid.
  • It uses native <dialog> with showModal() (built-in focus trap, Esc to close, backdrop), native type="date" and type="search", and labelled form controls.
  • The toast has role="status", so screen readers announce it.
  • Search filters live as you type, with no submit step.
  • Useful practical notes on cards ("bring a trolley") show real knowledge of the domain.

Suggested order of work

  1. Fix the dialog re-render bug (#1), restore focus rings (#2) and make the reservations table work on mobile (#4). Each is a one-line or few-line change.
  2. Make stock real and add undo on cancel (#3, #6), and style disabled buttons (#5).
  3. Shrink the hero and bring search up (#10), replace the ticker (#7), and fix the meta contrast (#8).
  4. Fix the copy, empty states, dialog details and dates (#11–#14).
  5. Add out-of-stock paths (#15), sort out the star (#9) and do the brand pass (#18).

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 -->

Lendwell review (loupe)

Loupe is independent and not affiliated with Apple.

1. Summary

Reviewed: whole app (single page, 6 tools, 1 reservation dialog), input a running web app (file:///.../lendwell/index.html) plus its source (one file, 7455 bytes). Profile: none on disk; none written. The owner was unavailable, so the draft profile ran on defaults.

Verdict: the product is a small, working reservation tool wrapped in a marketing page. Several defects block keyboard users and mobile users, and the reserve flow loses input and gives no real confirmation.

Do first: restore a visible focus indicator (findings 1, 2), stop the reserve dialog wiping the date (3), and lead with the tool list instead of an 88vh hero (4).

Assumptions (questions recorded, not answered):

  • Questions for the owner: purpose in one sentence; audience and visit frequency; peers; any brand assets (logo, colours); target browsers; is a hold or a loan record created on submit; real release date.
  • Platform: web only (desktop and mobile browsers). No native Apple rules applied.
  • release_stage: development. Dead controls are Major, not Blocker; App Review checks not run.
  • Register: utilitarian (a frequent check-and-act task: find a tool, reserve it).
  • Voice: sentence case, British spelling (neighbours, Membership £, "Bank Holiday"), humor none.
  • Density: comfortable. No direction set. No waivers.
  • Build SDK and 26-gate: not applicable (web).

Run notes:

  • Tools: Node and Playwright Chromium (scripts and screenshots in work/). measure-runner.js was run (widths 390, 820, 1440 x light, dark x reduced motion), saved in work/measure.json. type-eval.js in work/type.json. Probe script work/probe.js. Screenshots: full-390.png, full-820.png, full-1440.png, dark.png, dlg.png, dlg-390.png, t200-390.png, rtl.png, forced.png, empty-resv.png.
  • Measurements: 57 text nodes checked per combination, 6 contrast failures, 8 unmeasurable (the hero, over a gradient), 18 targets, 3 under 24 px, 9 to 10 under 44 px. Results were identical across light and dark and across reduced motion (the page has no dark or reduced-motion CSS). The 1 animated-on-load element is the ticker; reducedMotionRule false, unreadable sheets 0 (so it is a true absence).
  • Side effects: in-page DOM edits (200% root size, dir=rtl) were done in throwaway pages and reloaded. No offline or error routes were driven (the app has no network calls).
  • Landscape: no web research was done (an unattended run; claims below come from memory and are marked unverified).

2. Findings

# Finding Where Impact Apple Fix
1 Focus indicator removed everywhere: button,input,select,a{outline:none} (style block) and no replacement. Tab walk of 22 stops shows outline-style: none and no box-shadow on every stop (links, search, select, stars, Reserve, Cancel). A keyboard user cannot see where they are. WCAG 2.4.7 fails (AA) Whole page, CSS rule button,input,select,a{outline:none} Blocker Web Delete the rule; add :focus-visible{outline:3px solid var(--brand);outline-offset:2px} (check 3:1 against both the white cards and the green hero)
2 Same cause hits the dialog: after showModal() focus lands on the date input with no ring, and Close/Submit have none. Merged under the root cause of 1 for the fix, but listed because the dialog is the money step Reserve dialog Blocker Web Fixed by 1; also return focus to the invoking Reserve button on close (not currently done)
3 Changing "Loan length" re-renders the whole dialog and clears the chosen date (renderDlg() in the #n onchange; date is never updated from the input). Measured: date set to 2026-11-05, select to 3 days, date input reads empty. Anyone who picks the date first (the natural order) loses it, and no message says so Reserve dialog, Loan length Blocker Web Do not re-render on change; just set days. Data loss in a task flow (user-entered input discarded). Retain date on input
4 Hero takes 88vh (792 px of 900) and pushes the actual tool list to y=1040 at 1440 and y=1090 at 390, below the first screen on both. A returning member's frequent question is "is the drill free?"; the first screen answers "how did Lendwell start?". Premise attack with evidence: the core object is below the fold Top of page, .hero Major Taste Cut hero to a compact strip (or move the story and stats under the list); put the search and tool grid in the first viewport. Reviewer judgement (red-team layer 1)
5 Hero body text and stat labels are white on a gradient from #1f6b4a to #2f8f65. White on #2f8f65 is 4.01:1 (computed from the source colours), under 4.5 for the 18 px paragraph and 16 px labels on the right-hand end; the runner marks the rows unmeasurable (background image). Over a gradient this is by eye, so capped at Major Hero paragraph, "members/tools/loans" Major Web Darken the light stop to about #25805a or lower so every pixel is 4.5:1; confirm with png_contrast.py on hero.png
6 Secondary text is #a9b0ab on white (2.21:1, measured, fail) for the card meta line ("Power · 18V, two batteries") on all 6 cards. It carries real information (battery count, "bring own fuel", "Back 14 Nov"). WCAG 1.4.3 fails .meta on tool cards Blocker Web Use --muted #5f6b64 (about 5.5:1 on white) or darker
7 Mobile (390 px): the reservations table has min-width:640px with no scroll wrapper (parent overflow visible). Page scrollWidth is 664 at a 390 viewport, so the whole page scrolls sideways, the header ticker is cut, and the Cancel button sits off-screen at about x=420 (see full-390.png) My reservations table Major Web Wrap the table in an overflow-x:auto container, or below 600 px render each reservation as a stacked card with its Cancel button
8 "Cancel" deletes the reservation instantly with no confirmation and no undo; the only feedback is a 3 s toast reading "Done". On a phone the Cancel sits next to the row text, easy to hit. (Verified: click removes the row, toast "Done".) My reservations Major Web Offer "Undo" in the toast for about 8 s ("Reservation for Socket set cancelled. Undo"), or confirm first
9 Submit gives toast "Success" and nothing else. The tool's availability does not change (drill stays "3 available" after reserving it), and the same tool and date can be reserved again (3 rows after a duplicate). The product cannot be trusted to hold the tool. Dead-state or unfinished backend: Unverified line below Cards .avail, reservations Major Taste Decrement or show "Reserved for you", block duplicates, and say what happened: "Cordless drill reserved for Thu 5 Nov, 3 days. Collect from the usual place."
10 Copy is generic: toasts "Success" and "Done"; button "Submit"; "Close" (it abandons the reservation, so it should say Cancel); "Collect from the usual place" repeats on every row and names no place; dialog title "Reserve Cordless drill" lacks "a/the". Writing rule: say what happened Toast, dialog, table Major Research "Reserved: Cordless drill, 5 to 8 Nov." / "Reservation cancelled." Primary "Reserve", secondary "Cancel". Show the real pickup address or a link to Help
11 Favourite star buttons ("☆"/"★") have no accessible name and no pressed state (aria-label null, text is the glyph). A screen reader says "white star", and the toggle is by glyph only. 4.1.2 name/role/value fails; the stars also do nothing else (no favourites list), so they may be unfinished Each card, .save Blocker Web aria-label="Save Cordless drill" and aria-pressed; either build a Saved view or remove the stars (dead-control note in Unverified)
12 Star buttons are 32 x 32 px and sit above Reserve with a 10 px offset; Reserve itself is about 44 px high (OK). Three nav links are 18 px tall (38 to 115 px wide), at 24 px spacingOk true so WCAG 2.5.8 passes Nav "Tools / My reservations / Help"; .save Minor Web Min 44 x 44 hit areas on stars and nav links (padding), more gap between nav links
13 Ticker marquee scrolls forever (animation:tick 14s linear infinite), cannot be paused, no reduced-motion rule (measured reducedMotionRule:false), text starts off-screen at 100% padding and is unreadable as a sentence ("Library closed Bank Holiday Monday" is the one piece of operational news). WCAG 2.2.2 (A) needs pause/stop for moving content over 5 s Ticker under the header Blocker Web Replace with a static line (or 1 to 3 visible notices); if it must move, add a pause control and @media (prefers-reduced-motion:reduce){animation:none}
14 Out-of-stock "Wallpaper steamer" shows a disabled Reserve button and "Out on loan" while the note "Back 14 Nov" sits in the pale meta line (finding 6). The disabled button offers no route (join a waitlist, reserve from 14 Nov), a dead end for a member. Disabled colour is the browser default Wallpaper steamer card Major Research Keep it enabled as "Reserve from 14 Nov" opening the dialog with min date 2026-11-14; or "Tell me when back"
15 Dialog date min="2026-10-27" is hard-coded (the review date is 2026-10-26); it will be wrong tomorrow. Browser validation blocked 2020-01-01 only because of this min, with a native tooltip and no inline message; no max or "closed on" days (library closed Bank Holiday Monday) Reserve dialog Major Web Compute min from today's date; list closed days; show an inline error with aria-describedby
16 Both empty states are blank: with zero reservations #resv is an empty string (heading "My reservations" over nothing), and a no-match search leaves #grid empty with no message My reservations; Browse tools search Major Research "No reservations yet. Pick a tool above." / "No tools match 'zzz'. Clear the search."
17 No dark-mode or increased-contrast support: dark.png is identical to light (measured, no prefers-color-scheme rule), hex literals everywhere (#fff, #444, #b3472f). The page is a light-only design; also hard-coded literals (not tokenised) in 10+ places CSS Minor Web Add dark token set under prefers-color-scheme:dark; move #fff/#444 into variables
18 Availability is carried by a small coloured dot plus text ("3 available" / "Out on loan"); the text is present so colour is not the only cue, but the red/green dots at 8 px are weak. Quantity wording "1 available" reads as ambiguous when a cordless drill's batteries are separate .avail Polish Taste Keep the text, enlarge or drop the dot
19 Main navigation: "Tools", "My reservations", "Help" are in-page anchors with no aria-current, no skip link, and the header is not sticky, so after scrolling to reservations there is no way back without scrolling (hero is 792 px); the page is also a single scroll, so "My reservations" is a section, not a destination Header nav Minor Web Add skip link, sticky header, aria-current; consider a real reservations view
20 Search runs on every keystroke with no label visible (placeholder plus aria-label only, so a name exists, judged Major max): the placeholder disappears as soon as the user types, and the category select has no visible label either Search form Minor Web Visible labels or a clear search button; add result count in an aria-live region
21 Loan length offers 1, 2, 3 days or "1 week" with no return date shown and no limit explained; Pickup date row in the table shows ISO "2026-11-02" while everything else is British prose; mixed formats Dialog, table Minor Research Show "Mon 2 Nov, back by Wed 4 Nov"; one date format everywhere
22 Dialog has no labelled title association (aria-labelledby), the Escape and backdrop paths work natively (checked), but the success toast role="status" is added only after the dialog closes; there is no confirmation inside the flow, and the toast auto-dismisses at 3 s (too short for screen reader users, WCAG 2.2.1 AA) Toast Minor Web Keep toast 6 to 8 s, pause on hover/focus
23 +polish: h1 max-width 14ch makes "Share more. Own less." wrap to two lines on desktop, leaving the right 55% of the hero empty; the hero slogan is a template line (Taste); footer "Membership £12 a year" is the only price, with no way to join; physical margin-left in nav (5 physical inline declarations, 0 logical) so RTL does not mirror (checked rtl.png); table header text 13 px Hero, nav, footer Polish Taste Use logical properties; show how to join

waived: 0

Notes on impact: Blockers 1, 2 (same cause), 6, 11 are WCAG A/AA failures measured or confirmed in source (2.4.7, 1.4.3, 4.1.2). 13 is WCAG 2.2.2 (A) confirmed in source. 3 is data loss in the core flow.

3. Trust

No deceptive patterns found in what was seen. One adjacent honesty issue: the hero says "412 neighbours", "181 tools", "over 180 tools" and "3,960 loans" while the page lists 6 tools (finding 9 and Unverified). Not fake urgency or social proof by itself, but the numbers cannot be confirmed from the build; if they are placeholders, replace them before release.

4. Landscape

Not researched live (unattended run; no web research was performed). From memory and marked (unverified): category is community tool and equipment libraries and rental/booking tools. Likely peers to check: Peerby, Library of Things (UK), Sharetribe-based marketplaces, and a library-catalogue pattern (Libby/Overdrive hold-and-waitlist) as the adjacent exemplar. (unverified)

  • Conventions to keep: availability shown on the card, one primary action per card, date picker with a clear return date. (unverified)
  • Convergence to avoid: green "sustainability" hero plus three stat counters; an anonymous "Share more. Own less." slogan. (unverified)
  • Frontier moves: waitlist or "next available" on out-of-stock items, return-by date and late reminders, a "ready for pickup" status. (unverified)
  • Openings: Lendwell is volunteer-run and local; the product could show who is on duty Saturday, the library's address and opening hours on the card, and the lender's note (battery count, fuel). (unverified) No peer step counts were sourced, so no rework can cite the peer-gap condition.

5. Distinctiveness verdict

Partly authored, leaning default-assembled. Channels: colour is a single green brand with a cream background (#faf8f4) and warm line colour (slightly authored); type is system-ui (platform default); layout is hero, stat counters, card grid (template default); shape (12 px cards, 8 px radius) library default; motion is an infinite marquee (trend default); imagery none; iconography is Unicode stars (default); voice has real specifics ("garage on Alder Street in 2019", "bring a trolley", "bring own fuel"), which is the authored part; the tool notes are the best content on the page. Fewer than two channels authored, so the verdict is not "authored". Slop tells: green gradient hero, big-number stat row, tagline "Share more. Own less.", generic "Success / Done" microcopy, uniform card grid, system-ui only. That is 5 tells over 4 channels, below the 6-tell threshold, so default-assembled is not forced. Logo-off test: the page without the word "Lendwell" could be any local-sharing site (fails). Competitor swap: another tool library could drop in its name and keep the layout (fails).

Directions (no profile direction set; sketched from the product's own subject, tool-library materials, no house style):

  • A. Workshop ledger: the library's lending card index. Traced to the subject (a physical loan system run by volunteers). Cream paper #faf8f4 (existing token), ink #1f2a24 (existing), brand green #1f6b4a (existing); stamp-red #b3472f (the existing "out" colour) as the single accent for "out on loan"/due dates. Type: a free slab or mono for dates and item numbers (candidates: Zilla Slab, JetBrains Mono, both OFL), system-ui for body. Signature moment: a "stamped" return-by date on a confirmed reservation. Contrast: #b3472f on #faf8f4 about 5.0:1 (computed from tokens, check with contrast.py).
  • B. Street-level noticeboard: the Alder Street garage story. Photo-led cards (each tool photographed on a workbench), hand-lettered "Saturday volunteers" note; colour from the photographs.
  • Both differ from peers by being about this place and its volunteers; neither borrows another product's look. These are sketches, to be verified against the landscape before adoption.

6. Type

Single face: system-ui (resolves to SF Pro on macOS in this run; loaded: true, no font files shipped, so it is the platform face).

  • Role: all (display, body, UI, table). Fit: good for a utilitarian tool at body 16 px; x-height ratio 0.72 (high, legible at 14 px). Confusables at 14/16/18: l/1 and I/1 differ by only about 0.93 share of differing ink pixels (fine), I/l differ 0 at 14 and 16 px (identical glyphs in this system face), which matters little here (no IDs or codes).
  • Scale: 8 distinct sizes (13, 14, 16, 18, 20, 22, 28, 36 to 64 clamp); min 13 px (table header), 14 px for meta. Too many near sizes (18, 20, 22) for a page this small. Weights 400 and 700 only. Numerics in tables are not tabular (numericInTables: normal) which matters for dates and counts.
  • Defects also in table: sizes in px not rem (does not follow root font size; 200% root size test did scale rem-less text? the 200% screenshot kept layout without page overflow at 390 px, scrollWidth 390). Minor, not logged separately.
  • If the identity direction A is chosen, candidate faces need their own verdict block from type-eval.js on a specimen page (Unverified).

7. Reworks

No rework warranted. Findings 1, 2, 6, 11 and 13 share a cause ("controls styled from scratch with no accessibility defaults") but per-row fixes remove it and it is a two-line CSS change, so it does not clear the cluster bar. The hero premise (finding 4) is a layout change on one screen, not a model change. No sourced peer step count exists, so the peer-gap condition cannot be met. Identity condition: the verdict is not default-assembled.

8. Enhancements

Experience and flows

  • Waitlist or "Reserve from 14 Nov" on out-of-stock tools (Agency). Mechanism: the Wallpaper steamer's own "Back 14 Nov" note becomes a bookable date. Unlocks: members stop checking back by hand.
  • Return-by date and calendar view in the dialog (Simplicity). Mechanism: a start-and-length picker that shows the end date and which days the library is closed (the ticker already mentions Bank Holiday). Unlocks: fewer wrong pickups. Information architecture
  • Make "My reservations" a persistent entry (a header badge showing the count) instead of a section at the bottom (Familiarity). Unlocks: returning members reach their loan in one tap. Visual design and brand
  • Put each tool's practical note (battery count, fuel, trolley) in the card body in the primary text colour, since that is the useful content (Purpose). Unlocks: the "is this what I need" decision on the card.
  • Direction A or B from section 5 as the theming base, one sketch to be tested, not applied wholesale (Craft). Motion and feedback
  • A reserved-state change on the card ("Reserved for you") with an Undo toast (Responsibility). Unlocks: confidence that something happened. Content and voice
  • Rewrite the generic strings in the library's volunteer voice, sentence case, specific (Delight, within humor none). See finding 10. Platform capabilities
  • Add the date's .ics download for the pickup (Flexibility). Unlocks: reminders without building notifications. Evidence for further enhancements is thin: there is no live landscape research.

9. Unverified

  • Landscape claims: not researched live; verify with web research on 3 to 6 peers.
  • Real behaviour of Submit and Cancel with a backend: the app is client-only (array resv), so availability, duplicates and holds (finding 9), and the favourite stars (finding 11), may be unfinished; confirm with the owner.
  • Hero stats (412 / 181 / 3,960) against real data: cannot verify.
  • Error and offline states: the app makes no network requests, so none exist; behaviour with a real API unseen.
  • Loading and skeleton states: none exist; unseen.
  • Pseudo-localised +40% text and very long tool names: not driven (cards use h3 with no truncation rule; a long name will wrap, unverified).
  • RTL: dir=rtl screenshot taken (rtl.png); the nav spacing does not mirror (5 physical vs 0 logical properties); fuller RTL review not done.
  • Increased contrast and forced colours: emulated once (forced.png), not inspected closely.
  • Hero contrast across the whole gradient: computed only at the stop colours; pixel sampling (png_contrast.py) not run.
  • Screen reader output (VoiceOver, NVDA): not run; names inferred from source.
  • Firefox and WebKit: only Chromium run.
  • Touch behaviour and the date picker's native UI on iOS and Android: not seen.
  • Font files: none shipped, type-eval ran on the system face only.
  • Hover states: not captured (no custom hover styles in the source).

10. Offer

Fixable in code now (no design decision first):

  • Focus ring and dialog focus return: 1, 2
  • Contrast tokens: 5, 6, 17
  • Dialog date bug and min date: 3, 15
  • Accessible names and targets: 11, 12, 20, 22
  • Ticker (static or pausable, reduced motion): 13
  • Mobile table overflow: 7
  • Empty states and copy rewrites: 10, 16, 21
  • Undo and confirm for Cancel: 8

Need a design or product decision first: 4 (hero versus tool list), 9 (what Reserve actually does), 14 (out-of-stock behaviour), 11 (build favourites or drop them), and any enhancement or theming direction in sections 5 and 8. Enhancements and reworks need your call; I can build any enhancement or a throwaway prototype of one.

Which should I do? Optional extra input that would let me verify more: the real backend or data, a profile (purpose, audience, peers, brand assets), and a screen reader run.

Not written: .loupe.md (no answers were available, and the run was told not to write it).

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

Lendwell design review (index.html, pre-release build)

Method: read the source; drove it in Chromium at 1280 and 375 px with Playwright (screenshots, a reserve flow, search with no results). No HIG skill used. Assumed defaults: audience is neighbours of mixed age and tech comfort; mostly phone use, some desktop; UK English/dates; no real backend yet (reservations are in memory); no account system yet.

Severity key

Critical = blocks a task or excludes users. High = frequent, harmful friction. Medium = noticeable. Low = polish.

Critical

  1. Focus indicators removed everywhere. button,input,select,a{outline:none} and no replacement. Keyboard and switch users cannot see where they are (nav, Reserve, star, dialog buttons). Fix: delete the rule; add :focus-visible{outline:3px solid var(--brand);outline-offset:2px}.

  2. The reserve dialog wipes the chosen date when the loan length changes (verified). onchange calls renderDlg(), which rebuilds the form from date="" and never stores the date. Pick a date, change length, date is gone; the user also loses focus on the select. Fix: do not re-render; or store date on input. Better, delete the re-render and just read values on submit.

  3. No validation or feedback that fits the task. Date min is hard-coded to 2026-10-27 (will be wrong tomorrow; compute from today). Nothing checks the tool is still available, that the pickup day is an opening day (library is Saturday-staffed, closed on bank holidays per the ticker), or that return date is shown. Submitting closes the dialog with a toast reading "Success" and nothing else. Users cannot tell what was reserved, when to return it, or where to collect it. Fix: confirmation message such as "Socket set reserved. Collect Mon 2 Nov, return by Wed 4 Nov", show the return date live in the dialog, and restrict dates to opening days.

  4. Cancel deletes immediately with a toast saying "Done". No confirmation, no undo, and the toast does not say what was done. Destructive and ambiguous. Fix: confirm, or offer "Cancelled Socket set. Undo" for ~8 s. Name the item in the button's accessible label ("Cancel reservation for Socket set").

High

  1. Hero consumes the whole first screen (88vh) and holds no task. On both desktop and phone, the tool search is below the fold, while the right half of the desktop hero is empty gradient. The core job (find and reserve a tool) is hidden behind a slogan and stats. Fix: shrink the hero to a short header, or put search and the tool grid directly under it; drop the generic "Share more. Own less." gradient banner or make it a compact strip. The hero copy is a story paragraph, not a decision aid.

  2. Auto-scrolling ticker with no pause control. Constant motion (WCAG 2.2.2 requires a pause/stop for >5 s), ignores prefers-reduced-motion, and the text is mostly off screen, so "Library closed Bank Holiday Monday" is hard to read. Important notices like closures should be a static banner. Fix: static text, or at minimum pause on hover/focus and disable under reduced motion.

  3. Low-contrast and tiny meta text. .meta uses --faint #a9b0ab on white, roughly 2.2:1 (needs 4.5:1), and it carries the tool category and notes ("Petrol, bring own fuel", "Heavy, bring a trolley") which matter for the decision. th at 13px muted is marginal. Fix: use --muted or darker for all informative text.

  4. Empty states are blank. Searching for something not stocked leaves an empty grid (verified, zero HTML). "My reservations" shows only a heading when there are none, and after cancelling the last one. Fix: "No tools match 'xyz'. Clear search" and "You have no reservations. Browse tools".

  5. Search results are not announced and the search is partial. The grid updates silently (no live region), no result count, and search matches name only, not category or notes (typing "petrol" or "garden" finds nothing). Add a polite live count and widen matching.

  6. Mobile: table overflows and navigation is cramped. The reservations table has min-width:640px with no scroll wrapper, so at 375 px the page itself scrolls sideways or the table is clipped. The "Pickup" column repeats "Collect from the usual place" on every row (noise, and vague). Fix: render reservations as cards on narrow screens, or wrap in overflow-x:auto; show pickup location once. The nav's three links fit at 375 px but have no touch spacing beyond 16 px margins and no 44 px targets.

Medium

  1. Touch targets too small. The save star is a ~20px glyph button with no size or accessible name (screen readers read "white star" or nothing); it is also coloured #444. Needs aria-label="Save Cordless drill", aria-pressed, min 44x44. Nav links are text-height only. The save state also resets on every filter/search render (innerHTML rebuilt) and is not persisted or listed anywhere, so "saved" does nothing useful. Either build a Saved view or remove it.

  2. Disabled "Reserve" for out-of-stock tools gives no path forward. The wallpaper steamer is "Out on loan", with "Back 14 Nov" buried in grey meta text. Make it the visible availability line ("Back 14 Nov") and offer "Reserve from 14 Nov" or "Notify me". Disabled buttons are also skipped by keyboard and may be low contrast (browser default).

  3. Availability relies on a colour dot plus text. Text is present, which is good, but dot colours are red/green only and the dot is decorative; mark it aria-hidden. Minor.

  4. Dialog semantics and polish. Dialog has no accessible name link (aria-labelledby to its heading), the heading is an h2 inside the dialog, the primary button says "Submit" (use "Reserve Cordless drill"), "Close" should be "Cancel", Escape works natively but there is no initial focus choice (first field is fine). Date field required gives only the browser tooltip; add inline error text. No keyboard hint that length 7 = "1 week". Dialog re-render (issue 2) breaks screen-reader context too.

  5. Raw dates and unclear units. The table shows ISO "2026-11-02" and "2 days"; show "Mon 2 Nov" and a return date. "Length" should read "Loan length" and show "Return by".

  6. No indication of who or what the user is. "My reservations" is a nav link with no sign-in, member number, or note that membership (£12/year) is needed to reserve. The £12 fee appears only in the footer. Surface membership requirement before the Reserve step, and say what happens if you are not a member.

  7. Navigation is in-page anchors only; header is not sticky. On a long page the user must scroll back to switch sections; "Help" is one line of text, not a destination (contact email is not a link, mailto: missing). No visible current-section state. Make email a link; consider a sticky header.

  8. Toast is not reliably accessible. role=status is good, but a 3 s fixed timeout is short for screen-reader and low-vision users, and the generic words "Success"/"Done" carry no information. Also a second toast within 3 s is cleared early by the first timer (no clearTimeout).

Low

  1. Generic styling. Green gradient hero, white rounded cards, stat triple (412 / 181 / 3,960) is a common template look and says little about a tool library. Real tool photography or simple illustrated icons per tool, and a warmer neighbourhood voice would give the product identity. Stats also conflict with copy ("over 180" vs 181) and the demo lists only six tools while claiming 181.
  2. Hero h1 wraps awkwardly ("Share more. Own / less.") due to max-width:14ch. Use text-wrap:balance.
  3. Copy and consistency. "Save" is a star with no label; "Reserve" vs "Submit" vs "Cancel" inconsistently name the action; "Pickup date" column header and "Pickup" column both exist. Mixed "neighbours/neighbourhood" is fine (UK) but dates are ISO.
  4. Fonts and spacing. System font stack is fine; no type scale beyond ad hoc sizes; section headings (22px) are weak against the 64px hero. Cards have no image area so the grid is text-heavy and cards of different note lengths vary in height.
  5. Safe handling of data. Tool names are injected via innerHTML (fine for the static list, a risk once data comes from users). Reservations vanish on reload (no persistence), which a user will read as lost bookings.
  6. Dark mode and zoom. No color-scheme or dark styles; the header and table use hard-coded #fff. Low priority for v1.

What works

  • Clear single-purpose layout, readable body size (16px), good contrast on primary buttons and body text.
  • Native <dialog>, native date input and select reduce custom code; the search input has an accessible label; toast uses role=status.
  • Viewport meta set, grid reflows by auto-fill, text includes status alongside the colour dot.

Suggested order of work

  1. Restore focus styles; fix the dialog date reset; real confirmation and return date; undo on cancel (items 1-4).
  2. Compact the hero so search is visible on first load; make the ticker static (5-6).
  3. Contrast, empty states, mobile reservations layout, labels for icon buttons (7-11).
  4. Out-of-stock path, membership clarity, copy and date formats (12-18).
  5. Identity and polish (19-24).

Claude Haiku: loupe’s report

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

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

Loupe review: Lendwell (single-page web prototype)

1. Summary

  • Reviewed: lendwell/index.html (one file, 124 lines: CSS, markup, inline script). Unit: whole app (6 tools, one reservation flow, one table). Input: running page in local Chromium (file://), measured at 390, 820 and 1440 px, light and dark, reduced motion on and off, plus interaction probes.
  • Verdict: Not ready for anyone beyond the team. Two WCAG A/AA failures are source-confirmed (focus rings removed; auto-moving ticker with no pause), contrast on card metadata measures 2.21:1, and the reservation flow loses typed input and never reduces availability. The page also presents invented membership and stock numbers as fact.
  • Most important to do: Fix the Blockers first (focus ring, ticker pause, metadata contrast), then the reservation flow (date wipe, availability, cancel with no undo). Reworks: none warranted (see section 7).

Assumptions (no owner answers; safe defaults, nothing written to .loupe.md):

  • platforms: web. Native rules not applied.
  • release_stage: development, so dead controls are Major, not Blocker, and App Review checks (4.2, 2.1) are not applied.
  • voice: sentence case; British spelling (the copy uses "neighbours" and £). Inferred, not confirmed.
  • register: utilitarian (inferred: a frequent check-and-reserve task).
  • density: comfortable. humor: none. direction: empty, so the distinctiveness pass gives a verdict only and proposes no theming.
  • Landscape (1b): not run. No web research was done in this pass, so no peer claims are made.

Run notes

  • Tools: Node scripts with the local Playwright install in work/. Playwright MCP was not used. Scripts: work/measure.cjs (ran the skill's measure.js over 12 combinations, which is the same loop as measure-runner.js), work/probe.cjs (keyboard walk, reservation flow, search, mobile), work/stress.cjs (320 px reflow, 200% root font, dark, RTL), work/long.cjs (long names at 390 px).
  • Evidence: work/measure-full.json, work/type-eval.json, screenshots in work/ (desk-1440-top.png, mob-390-top.png, dialog-after-length.png, long-strings-390.png, reflow-320.png). Full paths are under work/.
  • Measurement results: contrast and target results were identical in all 12 combinations. Contrast failures: card metadata only (2.21:1). Hero text is over a gradient (unmeasurable by the script); pixel-sampled text areas measured 5.2 to 5.6:1. Targets: 18 under 44 px, none under 24 px without spacing; all 18 pass 2.5.8. Motion: one infinite animation (ticker), reducedMotionRule: false. Type: system-ui; sizes 13 to 36 px, all in px. Physical CSS properties: 5, logical: 0. Coverage: no iframes or shadow roots.

2. Findings

Impact sort, then Apple column. Line numbers refer to lendwell/index.html.

# Finding Where Impact Apple Fix
1 Focus ring removed on every control: button,input,select,a{outline:none}. Computed outline is none on every tab stop (tested: links, search, select, all buttons). Keyboard users cannot see where they are. WCAG 2.4.7 (AA), source-confirmed. Line 27; all pages Blocker Web Delete outline:none. Add :focus-visible{outline:3px solid var(--brand);outline-offset:2px}. Check it against 3:1 on white and on the #faf8f4 background.
2 Ticker scrolls forever (animation: tick 14s linear infinite) with no pause control and no reduced-motion rule. WCAG 2.2.2 (A), source-confirmed; measured reducedMotionRule: false in all 12 combinations. Lines 14-16, 53 (header ticker) Blocker SHOULD (content-a11y.md:45, reduce repeating animation) Add a visible Pause/Play button. Under @media (prefers-reduced-motion: reduce) stop the animation and show the text statically.
3 Card metadata (category and note, e.g. "Power · 18V, two batteries") is #a9b0ab on white: 2.21:1 at 14 px, failing 4.5:1. Measured on all 6 cards, every width and mode. Line 33 (.meta), every card Blocker Web Use #5f6b64 (5.56:1 on white, already used for "3 available").
4 Invented or unsupported figures in copy: hero says "412 members", "181 tools", "3,960 loans", "over 180 tools", and the browse grid shows 6 tools. Ticker names "pressure washer, tile saw, 3 hedge trimmers", none of which are in the grid. The hero also says "hundreds of drills out of landfill", which is unsupported. Lines 53, 57-58 Blocker Taste (Trust gate) See Trust. Replace with figures from the real library, or label them as placeholders before any release.
5 Save (☆) button only swaps its glyph. Nothing is stored, no list uses it, and its accessible name is the symbol "☆" (no label, no state). Dead control. Line 37, 98; each card Major (dead control, dev stage; Unverified: may be unfinished) Taste (reviewer judgement) aria-label="Save Cordless drill" and aria-pressed. Persist it and show a Saved list, or remove the button.
6 Disabled Reserve on Wallpaper steamer looks identical to enabled buttons: .btn has no :disabled style, the button has the same brand background, and no reason is given beyond "Out on loan". Line 28, 101; "Wallpaper steamer" card Major Taste (inputs-tech.md:217 slop tell); Web (web-mapping: disabled buttons cannot explain themselves) Style :disabled (muted, cursor:not-allowed). Show "Back 14 Nov" next to Reserve, or offer "Notify me".
7 Reserving does not change availability. Reserving the drill (qty 3) leaves "3 available"; repeated reservations are unlimited. Measured in probe. Line 118 (submit), 100 (avail text) Major reviewer judgement (no Apple line covers inventory) Count reservations per date range against qty; show availability for the chosen date.
8 Changing "Loan length" wipes the pickup date. renderDlg() re-renders with value="${date}", but date is never updated, so a typed "2026-11-05" becomes empty. Reproduced and screenshot-confirmed. Line 117 (onchange), 113 Major SHOULD (principles-platforms.md:35, no lost form input) Save the date on input, or update only the length without re-rendering the dialog.
9 Search with no matches shows a blank grid (#grid empty for "xyzzy"); no message, no clear action. Line 96-101, 120 Major SHOULD (inputs-tech.md:95, suggest alternatives instead of empty "no results") Render "No tools match 'xyzzy'" with a Clear search button.
10 Cancel reservation is immediate: no confirmation and no undo; toast says "Done". Losing a booking is irreversible here. Line 108 Major SHOULD (components-presentation.md:67; beyond-hig.md:176, undo for everything) Show "Reservation cancelled. Undo" in the toast for about 5 s; keep the booking until it expires.
11 Browse starts at y = 933 px on a 1440x900 screen. The hero is min-height:88vh, so the primary task sits below the fold. Line 17 (.hero), 22, 62 Major Taste (structure, reviewer judgement) Cap the hero (for example min-height: 0, padding about 48 px) so the search and first row show above the fold.
12 Reservations table has min-width:640px with no scroll container. The page scrolls sideways: scrollWidth 664 px at both 390 and 320 px. Data tables may be exempt from WCAG 1.4.10, but the whole page still scrolls. Line 38; #resv Major Web Wrap the table in overflow-x:auto, or stack rows as cards below about 600 px.
13 No dark mode. No prefers-color-scheme rule and literal hex values everywhere; with dark emulated the page is identical (bg rgb(250,248,244)). Pass 3 rule: a literal color with no dark variant is Major. Lines 8, whole CSS Major Web (visual.md Dark Mode; web-mapping colour section) Move colors to :root tokens and add @media (prefers-color-scheme: dark) values. Recheck contrast in dark.
14 All type is in px (font:16px, .meta 14px, th 13px, etc.). At a root font size of 200%, body stays 16 px and .meta stays 14 px. Browser zoom still works, so WCAG 1.4.4 passes, but user font settings are ignored. Lines 10, 22-48 Major Web (web-mapping Text, Major not Blocker) Use rem for font sizes and spacing that should follow user settings.
15 Save button and header nav targets are small: star 32x32 px (under 44 px goal, over 24 px minimum); header links 18 px tall. Both pass 2.5.8 through spacing (spacingOk: true). Line 37; lines 13, 52 Minor Web (44 px goal) Use 44 px hit areas for star and header links.
16 Star button overlaps long card titles at 390 px (tested with "Àçcéntéd extra longer name"). The title runs under the star. Line 37, 99 (.card h3) Minor Taste Add right padding on h3 equal to the star's width.
17 Empty "My reservations" shows only its heading when there are no bookings. No explanation or link to browse. Line 105-106 (renderResv) Minor SHOULD (beyond-hig.md:200, empty states say how content will appear) Add "No reservations yet. Browse tools to book one." with a link to #tools.
18 Dialog cancel button says "Close" and the form has no label association for the dialog heading. Web idiom is Cancel plus a verb. Line 115 Minor Web (web-mapping, Forms) Rename to "Cancel". Add aria-labelledby from the dialog to its h2.
19 Pickup date min="2026-10-27" is hard-coded and will go stale; the table shows raw ISO dates ("2026-11-02") with no weekday or return date. Lines 105, 113 Minor Taste Compute min from today; show "Mon 2 Nov, back Wed 4 Nov".
20 Pickup info is "Collect from the usual place": no address, no hours. Line 105 Minor Taste (content) Show the real pickup address and hours (needs a fact from the owner).
21 Generic toasts: "Success" (line 118), "Done" (line 108). Say what happened: "Reserved: Cordless drill, collect 2 Nov." Lines 108, 118 Minor Taste (writing.md) Use the rewrites shown.
22 Layout template tells: full-bleed green gradient hero, centred stats row, marquee ticker, uniform card grid with a star corner. Provisional: the full slop catalog was not counted in this pass. Lines 14-17, 55-59, 62-68 Minor Taste Replace the hero with a product-specific first screen (the search and tools). Not a Blocker.
23 Five physical CSS margins/insets (margin-left, right) and zero logical ones; RTL would not mirror. Only matters if RTL is a goal. Lines 13, 37, 45 Polish Web (web-mapping, logical properties) Use margin-inline-start and inset-inline-end.

waived: 0

3. Trust

  • Element: hero stats (412 members, 181 tools, 3,960 loans), hero paragraph ("Today 412 neighbours share over 180 tools"), and the ticker ("New this week: pressure washer, tile saw, 3 hedge trimmers").
  • Screen: top of the page.
  • Why: the browse grid shows 6 tools, not 181, and none of the ticker's new items appear. These are invented counts presented as fact.
  • Honest rewrite: use the library's real counts, or mark them as placeholders until they are real. Take the ticker items from the real catalogue.
  • Caveat: this is a development build. If the numbers are placeholders, the Blocker still stands until they are replaced or flagged, because the copy reads as fact.
  • Also check the hero claim "keeping hundreds of drills out of landfill", which has no source.

4. Landscape

Not run. No web research or peer study was done in this pass, so no conventions or openings are claimed.

5. Distinctiveness verdict

Provisional: default-assembled. Evidence: the green gradient hero, centred stats row, marquee ticker and uniform card grid with a corner star are common template choices. The full slop-catalog count was not done, so treat the verdict as provisional. No direction is set, so no theming is proposed. Logo-off and competitor-swap tests were not run (no peers researched).

6. Type

  • Face: system-ui only (no custom face). Sizes measured: 13, 14, 16, 18, 20, 22, 28, 36 px. Smallest text 13 px (table headers), under the 16 px body rule on mobile web but not a defect on its own.
  • Heading scale is clear (h1 36-64 px, h2 22 px, card h3 18 px). Hierarchy is fine.
  • Defects: finding 14 (px sizing), finding 3 (contrast of metadata text).

7. Reworks

No rework warranted. The defects are local (CSS, a few handlers, copy). The reservation model (finding 7) lacks a per-date inventory, but that is a fix plus an enhancement, not a rework: no single structural change clears the rework bar for this one-page prototype.

8. Enhancements

Evidence is thin (one screen, no peers researched). Three only:

  • Experience and flows: the reservation sheet should show availability for the chosen dates and the return date before Submit (Simplicity, Agency). Ties to finding 7 and the existing "1 day to 1 week" options.
  • Experience and flows: for out-of-stock tools, offer "Notify me when back" next to the "Back 14 Nov" note in the Wallpaper steamer card (Agency, Flexibility).
  • Content and voice: write the pickup line as a real place and time, not "the usual place" (Purpose, Familiarity).

9. Unverified

  • Error, offline and loading states: the app is static and has no network code, so none exist to test. Verify by adding a data source and driving route.abort.
  • Empty catalogue and very long strings at 1440 px: long names were tested at 390 px only.
  • Hero gradient end stop #2f8f65 gives 4.01:1 with white text. Pixel sampling of the text blocks gave 5.2 to 5.6:1, so it passes as shipped. Recheck if the gradient or text changes.
  • Keyboard walk, focus and dialog behaviour were tested in Chromium only. Firefox, WebKit and screen-reader order were not tested.
  • Reduced motion beyond the ticker, RTL rendering (only the CSS property count was checked), and prefers-contrast: more.
  • Dead-control findings (5) may be unfinished development work. Confirm with the team.
  • Landscape: not run.

10. Offer

Fixable in code now (no design decision needed):

  • Accessibility tokens and CSS: 1 (focus ring), 2 (reduced motion for ticker, plus Pause), 3 (.meta colour), 14 (rem sizing), 15 (targets), 16 (title padding), 12 (table scroll wrapper), 13 (dark tokens).
  • Copy rewrites: 4 (after the owner confirms the figures), 18 ("Cancel"), 19, 20 (needs the address), 21, 17, 9.
  • Behaviour fixes: 6 (disabled style and reason), 8 (keep the date when the length changes), 9 (empty search), 10 (undo toast), 7 (availability from reservations).

Needs a design decision first: 11 (hero height and first-screen content), 5 (keep favourites or remove the button), 22 (visual direction).

Optional inputs that would let the review verify more: the real library figures and pickup details, the intended release stage and voice, and a running backend for error and offline states.

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

Lendwell design review (development build, single page)

Scope: lendwell/index.html only (one file, inline CSS and JS, six sample tools, one sample reservation). Reviewed at 1440x900 desktop and 375x812 phone in Chromium via Playwright. Screenshots and the probe script are in work/.

Defaults I assumed (the owner was unavailable)

  • Audience: UK neighbourhood members (copy uses £, "neighbours", "Bank Holiday"). Desktop and phone are both real use.
  • Primary job: find a tool and request it for a pickup date. Everything else is secondary.
  • A reservation is a request. Nothing in the page says it is confirmed, so I treated the missing confirmation as a gap, not a choice.
  • No account system exists. "My reservations" is a global list. I judged this acceptable for a prototype but flagged it.
  • Severity: Blocker = stops the core task or breaks trust in it. High = clear user harm or an accessibility failure. Medium = friction or unclear state. Low = polish.

Summary

The page has a sound core: a clean palette, a readable type scale, native date input and <dialog>, a role=status toast, and availability shown as text as well as a colour dot. But the core task is undermined by functional bugs (reservations do not reduce stock, a loan-length change wipes the chosen date), the first screen hides the tools, and there is a serious accessibility problem (focus outlines removed globally). Fix the Blocker and High items before anyone uses this.

Findings, most serious first

Blocker / High
  1. Reservations do not reduce availability (overbooking). Verified: "Orbital sander" showed "1 available" before a reservation and still "1 available" after; its Reserve button stayed enabled. qty is never decremented and no date-range check exists. Users can book stock that does not exist. Fix: decrement or check availability per tool and per date window before accepting, show "Fully booked on that date" in the dialog, and disable Submit when the pickup date is unavailable.

  2. Changing the loan length clears the pickup date. Verified: set date to 2026-11-05, change length to "3 days", and the date field is empty. The dialog re-renders via innerHTML, and the date variable is never updated from the input. Submit is then blocked with "Please fill out this field." and focus is lost. Fix: keep state in JS and update it on input, or render the dialog once and only update the summary text. Add a live summary line: "Pickup Fri 6 Nov, back Mon 9 Nov (3 days)".

  3. Out-of-stock Reserve buttons look enabled. There is no :disabled style. The disabled "Wallpaper steamer" button has the same green background, opacity 1, and cursor: pointer as the enabled ones. The state is only conveyed by the small grey "Out on loan" line. Fix: style .btn:disabled (muted fill, cursor: not-allowed), and move the reason next to the button: "Out on loan, back 14 Nov". Offer "Notify me" if the product wants demand capture.

  4. Focus indicators are removed globally. button,input,select,a{outline:none} removes the focus ring for keyboard users. Verified: the focused nav link has outline-style: none and box-shadow: none. Fix: delete that rule. If a custom ring is wanted, use :focus-visible { outline: 3px solid <brand>; outline-offset: 2px }.

  5. The first screen is a marketing hero, and the tools sit below the fold. The hero is min-height: 88vh. At 1440x900 it is 792px tall and the first tool card starts at y = 1040. The hero has no call to action, not even "Browse tools", so the search is never introduced. The story copy ("started in a garage on Alder Street") and the stats come before the task. Fix: make the first viewport the task. Put a short line and the search field in the top 300px, and keep the stats smaller or move them under the grid. If a hero stays, give it a real CTA that anchors to #tools.

  6. The page scrolls sideways on phones. The reservations table has min-width: 640px, and the page measured 664px wide on a 375px viewport. The "Pickup" column is cut off in the screenshot. Fix: wrap the table in overflow-x: auto with a scroll cue, or (better) render each reservation as a stacked card below 600px with tool, date, length, and Cancel visible.

  7. Text contrast fails WCAG AA in several places. Measured:

    • .meta (category, notes) and the search placeholder use #a9b0ab on white: 2.21:1 (needs 4.5:1). This is the notes text users need to choose a tool ("18V, two batteries", "Back 14 Nov").
    • Hero paragraph, white on #2f8f65 (gradient end): 4.01:1. At 18px regular it needs 4.5:1. Fix: use --muted (#5f6b64, 5.56:1) for meta and placeholder text, and darken the gradient end (or drop the gradient) so white body text reaches 4.5:1.
  8. Autoplaying ticker ignores reduced motion and cannot be paused. The tick animation runs infinitely at 14s. Verified: the animation is still tick with prefers-reduced-motion: reduce. This fails WCAG 2.2.2 (pause, stop, hide) and the reduced-motion expectation. Fix: wrap the animation in @media (prefers-reduced-motion: no-preference), add a pause control, or replace it with a static line that rotates on click. Ticker content (closures, new stock) is also what users most need, so a static strip is better.

  9. The product's membership and sign-in story is missing. "Membership £12 a year" appears only in the footer. There is no Join, no Sign in, and no indication that reserving requires membership. "My reservations" lists everything without identity. For a members-only service this is the first question a new visitor has. Fix: put "Join for £12 a year" in the header and on the reserve flow. Add a sign-in or "Use my membership number" step, or state clearly that this is a demo.

Medium
  1. Unlabelled repeated controls. Every card has a "Reserve" button with no tool name in its accessible name, and the star button has no aria-label or title. The star toggles a glyph only: it stores nothing and there is no favourites view. Fix: aria-label="Reserve Cordless drill", aria-pressed on a real "Save" toggle, or remove the star until favourites exist.

  2. Cancel is immediate and vague. Cancel removes the row with no confirmation or undo, and the toast says only "Done". Fix: confirm inline ("Cancel this reservation? Yes, cancel / Keep it") or offer "Undo" in the toast for about 5s, and name the tool in the toast.

  3. The success message does not say what happened. "Success" appears after a request, but the page does not say the request is pending or confirmed, what the pickup time is, or when the tool is due back. Fix: "Requested: Socket set, pickup Mon 2 Nov, back Wed 4 Nov. We will confirm by email." (the confirmation mechanism is an assumption to check with the owner).

  4. The reserve dialog is missing information and has weak labels. It has no return date, no summary, and no link between the <h2> and the <dialog> (no aria-labelledby). The buttons say "Close" and "Submit", which is generic: use "Cancel" and "Request reservation". Fix as above.

  5. Dates and plurals are not UK-formatted. Dates show as "2026-11-02" in the table and the date input uses the browser's locale. Length reads "1 days". Fix: format as "Mon 2 Nov 2026", use Intl.DateTimeFormat('en-GB'), and handle singular "1 day".

  6. Hard-coded minimum date and no closed-day logic. min="2026-10-27" is written into the markup and will go stale. The ticker mentions a Bank Holiday closure, but the date picker does not block it. Fix: compute min from today, and block closed days from the same data source as the ticker.

  7. Missing empty states. Searching for "zzz" leaves the grid blank with no message. An empty reservations list shows only the heading. Fix: "No tools match 'zzz'. Try a shorter word or clear the category." and "No reservations yet. Browse tools to request one."

  8. Pickup location is vague. "Collect from the usual place" appears on every row. Users need an address or the opening hours of the collection point. Fix: a real location line with opening times, or a link to a map.

  9. Tap targets in the header are too small on phones. The nav links measure 18px tall and 33 to 115px wide. This is below the 44pt minimum in Apple's HIG and below WCAG 2.5.8. The header also has no sticky behaviour and no active-state for the current section. Fix: 44px hit areas (padding) and an active underline with aria-current on the matching section.

Low
  1. Hero stats contradict the catalogue. The hero says "181 tools" and "over 180 tools". The catalogue shows six tool types with 13 units. This is probably placeholder data, but it will be noticed. Fix: compute the counts from the data or remove them until the numbers are real.

  2. Hero and gradient look templated. A full-height 135° green gradient with a centred-left headline is a common template pattern. There are no tool images, and every card is the same shape. Fix: use a flat brand colour with a real photo or illustration of the tool library, and let the catalogue carry the visual interest.

  3. Search has no visible labels and no feedback. The search and category controls rely on aria-label and placeholder text alone. There is no result count, and the filter is not debounced (fine at six items, not at 180). Fix: visible labels or a result count ("4 of 6 tools").

  4. Help email is plain text. hello@lendwell.example is not a mailto: link. Fix: link it.

  5. Footer and copy. The footer is one line with the membership fee. The lang="en" attribute is correct, but the page would read better with en-GB. There is no favicon or meta description.

  6. Toast timer can clear a newer message. toast() sets a 3s timeout that clears the text, even if a newer toast has replaced it. Fix: keep a timer handle and clear it on each new toast.

  7. Inline handlers and innerHTML rendering. The onclick attributes and template-string rendering are fine at this size but will make the page hard to change. Note for when the build grows, not a user-facing issue.

What already works (keep these)

  • One clear palette (green brand, warm off-white, muted text) and a readable type scale.
  • Native <input type="date"> and <dialog> with showModal(), which gives a focus trap and Escape-to-close for free.
  • Availability is shown as text ("3 available", "Out on loan") and not by colour alone.
  • The toast uses role="status", and the search uses role="search".
  • The cards are compact, with the key information (category, note, availability) in a predictable order.

Suggested order of work

  1. Fix the functional bugs: stock decrement and date-window check (1), date reset (2), disabled state (3).
  2. Restore focus rings (4) and fix contrast (7), both are one-line CSS changes.
  3. Move the tools above the fold and fix the mobile table (5, 6).
  4. Fix the ticker motion (8) and the labels and empty states (10, 16).
  5. Rework the reserve dialog and success message (11 to 15).
  6. Decide the membership and sign-in story (9) with the owner before polishing the header (18).

Verification

  • Probe script: work/probe.js (Playwright, Chromium, run with node).
  • Screenshots: work/desktop-fold.png, work/desktop-dialog.png, work/desktop-noresults.png, work/desktop-focus-nav.png, work/mobile-fold.png, work/mobile-resv.png.
  • Measured values quoted above come from work/probe.js output. Contrast ratios were computed from the computed colours with the WCAG formula.
  • Not checked: real-device touch behaviour, screen-reader output, Firefox or Safari, and the other breakpoints between 375 and 1440. The mobile table screenshot shows two rows because the probe added a reservation for the test; the overflow measurement is independent of that.