vlmkit demo · judgment log

Pantry Club checkout

How this multi-step form / checkout was judged, in the order it happened: every gate run with its whole output, every screenshot that was looked at with what was seen in it, and where each defect came from. Each finished round keeps the pictures of one full-page walk per width; the other shots keep what was seen in them. Open the site.

The brief the builder was given
# Brief: multi-step checkout — "Pantry Club" meal-kit subscription

- **Directory**: `examples/sites/checkout/`
- **Pattern**: multi-step form / checkout
- **Language**: English

## What it is

The sign-up and checkout flow of **Pantry Club**, a fictional weekly meal-kit subscription. One page,
`index.html`, walking through four steps. A notice says plainly that it is a demo and takes no
payment. Nothing is sent anywhere.

## Layout and behaviour

- **Steps**: 1 Plan · 2 Delivery · 3 Payment · 4 Review, shown as an ordered list with the current
  step marked (`aria-current="step"`). Continue and Back buttons; moving to a step puts focus on its
  heading.
- **1 Plan**: three plans as radio cards (2 people · 3 meals a week, $59.94; 4 people · 3 meals a
  week, $107.88; 2 people · 5 meals a week, $94.90), dietary preferences as checkboxes (Vegetarian,
  No pork, Dairy-free, Nut-free) and a delivery-day select.
- **2 Delivery**: full name, email, phone (optional), address line 1, address line 2 (optional),
  city, state (select), ZIP code, and delivery instructions (optional, with a 200-character
  counter). Use the right `autocomplete` values.
- **3 Payment**: card number (grouped in fours as you type), expiry MM/YY, CVC and name on card; a
  "Billing address is the same as delivery" checkbox, checked by default, whose unchecking reveals
  the billing fields; a promo code field — `WELCOME10` takes 10% off, anything else shows an error.
- **4 Review**: every choice summarised with an Edit link back to its step, a terms checkbox, and
  "Place order", which replaces the form with a confirmation: order number PC-20417 and the first
  delivery date.
- **Validation**: Continue on an invalid step shows an error summary at the top of the step with
  links to each field, marks each field (`aria-invalid`, message tied with `aria-describedby`, an
  icon and text — not colour alone) and moves focus to the summary. After the first attempt, fields
  re-validate as they are left.
- **Order summary**: a sidebar that stays in view on desktop and is a collapsible "Show order
  summary" panel on phones; it follows the plan, the promo code, free shipping, tax at 8% and the
  total.
- Also write a `flow.json` for `vlmkit verify flow` that completes the whole checkout.

## Visual direction

Trustworthy and friendly: white and warm greys, one forest-green accent, large tap targets, text
fields whose boundaries are clearly visible, strong focus states, and errors that are impossible
to miss without being alarming.

## Required copy

```
Pantry Club
Choose your plan
Delivery details
Payment
Review your order
Order summary
Continue
Place order
This is a demo. No payment is taken.
Thanks — your first box is on its way.
```

## States to show (final round)

Each of the four steps on desktop; step 2 with validation errors; the promo code accepted and
rejected; the billing fields revealed; the confirmation; step 3 on a phone with the order summary
expanded.

## Gates this brief leans on

`check color` (text-field boundaries, WCAG 1.4.11), `check a11y focus` (focus moves between steps and
to errors), `check a11y touch`, `check interactions` (radio cards, checkboxes, reveal, errors),
`verify flow` (the whole checkout), `check layout` or `check scroll` (the sticky summary).

Defects

DefectFound byWhatFixChecked by
D1eye L1 on S1

.summary-item__name / .summary-item__meta (desktop summary)

The order-summary item breaks badly in the 360px card: the name wraps as '2 people · 3 / meals a week' (the number stranded from its noun) and the meta as '6 servings · $9.99 per / serving' (orphan), because the $59.94 price column squeezes the text to ~160px.

Dropped the duplicate price from the summary item row (Subtotal carries it one line below), so the text column goes from ~160px to ~240px; the name and meta are built from no-wrap phrases ('2 people' · '3 meals a week', '6 servings' · '$9.99 each'), so any wrap can only fall at the middle dot.

L6
D2eye L1 on S1

.plan__badge (4-person card)

The 'Best value' pill sits on its own line under the meta, so the 4-person card is 108px tall against 78px for the other two; the three plan rows lose their even rhythm and the pill reads as an afterthought rather than a label.

The 'Best value' pill is now absolutely positioned to straddle the 4-person card's top border (top:-11px, aligned with the title text at 54px / 50px on phones); the plans list gap went 12 -> 18px to leave the pill room. It no longer adds a row, so all three cards share one height.

L6
D3eye L2 on S2

.plan:nth-child(2) .plan__meals @375

On a phone the 4-person card wraps its title as '3 meals a / week' — the wide $107.88 price column leaves ~135px for the text, stranding 'week' on a line of its own.

Below 480px the plan price drops from 20px to 18px, the card's column gap from 14 to 12px, and the panel padding from 20 to 16px, which returns ~20px to the title column so '3 meals a week' fits on one line at 375 and 360.

L7
D4eye L4 on S4

#email / #phone row (step 2)

Email and Phone sit side by side with hints of different length; the Phone hint wraps to two lines, so the Phone input is ~20px lower than the Email input and the pair no longer reads as one row.

Email and Phone each take a full row (single-column is also the faster form layout); the phone input is capped at 20rem because a phone number is short. The CVC hint was shortened to '3 digits on the back' so the remaining Expiry/CVC pair stays single-line at the narrowest two-column width.

L8
D5eye L1 on S1

.brand-bar__meta

A grey bold 'Checkout' sits alone at the right of the brand bar, exactly where a Help or Sign-in link would be; it is plain text, so it looks clickable and does nothing, and it says nothing the page does not already say.

Removed the stray 'Checkout' label from the brand bar; the header is now the mark and wordmark only, which is the convention for a checkout.

L6
D6gate G12

#billing-preview (step 3)

verify flow step 17 failed: the 'same as delivery' address preview under the billing checkbox is empty at step 3 (got: ''), so the customer cannot see which address they are agreeing to reuse. The one-line address is only recomputed on plan/day/diet changes, never when a step is entered.

goTo() now calls updateSummary() on every step change, so derived text (the one-line delivery address for the billing preview, the summary lines) is recomputed whenever a step is entered.

G16
D7gate G3

select padding, main#main row-gap

check design flags three spacing values just off the page's own scale: 11px input padding next to the established 10px, the 20px main row gap next to 18px, and 44px select right padding next to 40px.

Snapped the three values to the page's scale: input padding 11 -> 10px (min-height still 48px), main row gap 20 -> 18px, select right padding 44 -> 40px.

G20
D8gate G16

blur re-validation vs. pointer clicks (step 3 billing tile; any control below a corrected field)

verify flow step 23 failed: after the last card error was fixed, one click on the 'same as delivery' tile did nothing (#billing-fields still hidden). The click works on its own (S9), so the cause is the sequence: mousedown blurs #card-name, blur re-validation clears its error and hides the now-empty error summary at the top of the step, the whole form jumps up ~130px, and mouseup lands on a different element, so no click is dispatched. A real customer hits the same dead click on Continue or on any control below a field whose error appears or clears on blur.

Blur re-validation no longer runs while a mouse button is held: a capture-phase mousedown sets a flag, focusout queues the field name, and mouseup flushes the queue in a setTimeout(0), i.e. after the click has been dispatched. The error still appears or clears as the field is left, but the page moves only after the click, never between mousedown and mouseup. Tab-away keeps the immediate re-validation.

Refinement after G17: the queued re-validation is flushed synchronously in a capture-phase click listener (the click's target is already fixed, so the layout may move) instead of in a timer after mouseup; G17 showed the timer left a 1-task window where the error was still on screen after the click. mouseup keeps the timer as the fallback for a press that ends in no click.

G19
D9gate G20

ol#steps margin-bottom

check design now flags the step tracker's 20px bottom margin as the one value next to the page's established 18px (11 uses), left over after D7 moved the main gap to 18px.

Tracker bottom margin 20 -> 18px, onto the page's established step.

G25
D10eye L12 on S12

#promo-message (rejected code)

The rejected-code message wraps and strands 'again.' on a line of its own under the promo field; the sentence is longer than it needs to be for the width it has.

Shortened the message to '“SAVE20” isn’t a valid promo code. Check the spelling.' (~50 characters), which fits the field's width on one line at desktop.

L13
D11eye L15 on S15

#billing-preview (and the confirmation's Delivering to)

At 375 the one-line delivery address wraps as '…Portland, OR / 97214', stranding the ZIP code from its city and state; the address parts are joined as one string, so the browser breaks at the last space.

The address is rendered as no-wrap parts ('1450 Alder Street', 'Apt 3', 'Portland, OR 97214') joined by ', ', so it can only wrap between parts and the ZIP stays with its city and state; the billing preview and the confirmation's 'Delivering to' share the renderer.

L16
D12eye L17 on S17

#step-3 .promo label

With the billing fields revealed, 'Promo code (optional)' is styled as an ordinary 16px field label and follows the billing ZIP row directly, so it reads as part of the billing address; the page's other sections ('Card details', 'Billing address') have 18px headings and this one does not.

The promo label now carries the section-heading style (18px/700, like the 'Card details' and 'Billing address' legends), so Promo code reads as the third section of the step rather than a billing field; '(optional)' stays grey regular as in 'Dietary preferences (optional)'.

L18
D13eye L19 on S19

.review__row dt (desktop)

The review's 150px term column wraps 'Dietary preferences' to two lines at desktop, the only wrapped term, so the Plan block's row rhythm breaks.

Term column 150 -> 168px and terms set at 15px (line-height 1.6, so each term still sits on its value's 24px line box); 'Dietary preferences' fits on one line and the longest value still has ~13px to spare.

L21
D14eye L24 on S24

.confirmation__facts @375 / .nw address parts

On a phone the confirmation's facts table keeps the 140px term column, leaving ~110px for values: dates and plan wrap into three-line stacks, and the no-wrap address parts introduced by F12 overflow the table and are clipped at the screen edge ('Portland, OR 9721…'). Regression from the D11 fix, which assumed every address part fits its container.

Two changes. (1) The keep-together phrases are now inline-blocks carrying their own trailing separator ('1450 Alder Street,' 'Apt 3,' 'Portland, OR 97214'; '2 people ·' '3 meals a week'), so a line still breaks only between phrases when they fit, but a phrase wider than its box wraps inside itself instead of overflowing. (2) Below 480px the confirmation's term/value rows stack like the review rows, giving the values the full ~275px.

L25
D15eye L30 on S30

.wrap (header/notice/footer) vs main.checkout, 560-959px

At tablet widths the header, demo note and footer start at x=24 while the content column (capped at 720px and centred) starts at x=48, so the logo and footer do not share a left edge with the summary bar, tracker and card.

Moved the 720px cap from main.checkout to every .wrap below 960px, so the demo note, header, main column and footer share one centred column and one left edge (x=48 at 768).

L31
D16eye L30 on S30

.demo-notice p @768

At 768px the demo note wraps with 'page.' alone on its second line — an orphan in the first line of copy on the page.

Running text (p, li, dd) now uses text-wrap: pretty, which avoids one-word last lines, and h1/h2 use text-wrap: balance; applies page-wide rather than rewording the one sentence.

L31

Rounds

R1 · first draft

builder
S1

step 1 at load

index.html · 1280×800 · full page, 2 screen(s)

S1 screen 1S1 screen 2
L1 saw

Desktop 1280, step 1 at load, two screens. Screen 1: sand demo bar across the top reads "(i) This is a demo. No payment is taken. Pantry Club is fictional…" in one line; white brand bar with the green bowl mark + "Pantry Club" left and a grey bold "Checkout" far right that looks like it could be a link but is plain text. Left column: 4-step tracker (1 green ring with pale halo "Plan" bold; 2-4 grey outlined circles, grey labels, grey connectors), then the white step card: "Choose your plan" h1, two-line intro, "Box size" legend, three plan rows. Selected row 1 has a green 2px border + mint tint + filled radio; prices right-aligned ($59.94 / a week). Row 2 is taller than rows 1 and 3 because the "Best value" pill sits on its own third line, so the three rows are 78/108/78px — uneven rhythm. Right column: sticky white "Order summary" card; the box illustration reads as a parcel; the item name breaks badly as "2 people · 3 / meals a week" and the meta as "6 servings · $9.99 per / serving" (orphan). Totals block is clear: Subtotal $59.94, Shipping Free (green), Tax (8%) $4.80, bold "Total today $64.74", renewal note. Screen 2 (scrolled 541): summary stays pinned at top 24px (sticky works); 2x2 checkbox tiles, "Delivery day" label + hint + 320px select "Select a day" with chevron; hairline, then "Continue" green button bottom right; footer line. Nothing clipped or overlapping.

D1 defect .summary-item__name / .summary-item__meta (desktop summary)

The order-summary item breaks badly in the 360px card: the name wraps as '2 people · 3 / meals a week' (the number stranded from its noun) and the meta as '6 servings · $9.99 per / serving' (orphan), because the $59.94 price column squeezes the text to ~160px.

D2 defect .plan__badge (4-person card)

The 'Best value' pill sits on its own line under the meta, so the 4-person card is 108px tall against 78px for the other two; the three plan rows lose their even rhythm and the pill reads as an afterthought rather than a label.

D5 defect .brand-bar__meta

A grey bold 'Checkout' sits alone at the right of the brand bar, exactly where a Help or Sign-in link would be; it is plain text, so it looks clickable and does nothing, and it says nothing the page does not already say.

S2

step 1 at load

index.html · 375×812 · full page, 3 screen(s)

S2 screen 1S2 screen 2S2 screen 3
L2 saw

Mobile 375, step 1, three screens. Screen 1: the demo note wraps to 3 lines in the sand bar; brand bar with mark, "Pantry Club" and the same grey "Checkout" at right; full-bleed summary toggle row "bag icon Show order summary v $64.74" — reads well as a disclosure with the total visible. Tracker fits in one row (Plan bold/current, Delivery, Payment, Review). Step card with 16px gutters: "Choose your plan" big h1, 3-line intro, "Box size". Plan cards stack people / meals / servings / per-serving on the left with price right. Card 2 wraps its title as "3 meals a / week" because the $107.88 column squeezes it — an orphaned word, and it makes card 2 five lines tall with the "Best value" pill. Screen 2: third card, "Dietary preferences (optional)" + hint, four full-width checkbox tiles (52px, good tap size), "Delivery day" + 2-line hint + full-width select, full-width green "Continue". Screen 3: end of card and the footer sentence. No horizontal overflow visible; spacing between groups is consistent (~32px).

D3 defect .plan:nth-child(2) .plan__meals @375

On a phone the 4-person card wraps its title as '3 meals a / week' — the wide $107.88 price column leaves ~135px for the text, stranding 'week' on a line of its own.

S3

delivery day chosen by keyboard (ArrowDown x2)

index.html · 1280×800 · element .field--day · focus #day · press ArrowDown · press ArrowDown

1 screen(s), looked at when taken; not kept once the round was done — a round keeps one full-page walk per width.

L3 saw

Close-up of the Delivery day field after focus + ArrowDown twice: the closed select now reads "Tuesday" (keyboard changes the value without opening the list in this Chromium), and the change handler ran — a green line with a small truck icon appeared under it: "Your first box arrives Tuesday, October 13." with the date in bold. The focus ring is a 3px dark-green outline offset 2px from the grey field border, clearly visible against white. Label, hint and control stack with even ~6px gaps. This proves the flow can choose select options with press ArrowDown.

S4

step 2: state chosen by typeahead 'Oregon'

index.html · 1280×800 · element #step-2 .fields · focus #day · press ArrowDown · click #step-1 button[type=submit] · focus #state · type Oregon

1 screen(s), looked at when taken; not kept once the round was done — a round keeps one full-page walk per width.

L4 saw

Close-up of step 2's field grid (desktop) after reaching it with a day chosen and typing "Oregon" into State: typeahead selected Oregon and the select shows the dark-green focus ring. Problem: the Email and Phone pair are misaligned — the Email hint "For your receipt and delivery updates." fits one line but the Phone hint "Only if there's a problem with a / delivery." wraps to two, so the Phone input sits ~20px lower than the Email input (tops ~153 vs ~173) and the pair no longer reads as one row. City / State / ZIP (no hints) line up correctly. Field borders are clearly visible grey on white; labels bold, "(optional)" in grey regular. The empty 3-row textarea and "200 characters left" counter sit under the ZIP row. The address grid otherwise has an even rhythm.

D4 defect #email / #phone row (step 2)

Email and Phone sit side by side with hints of different length; the Phone hint wraps to two lines, so the Phone input is ~20px lower than the Email input and the pair no longer reads as one row.

G1exit 0vlmkit check integrity index.html

verdict: CLEAN (0 fail, 0 warn, 45 exempted)

verdict clean · findings 0 · fails 0 · warns 0 · exempted 45 · viewports 3

output, 27 line(s), 4.3s
vlmkit check integrity
source: index.html

verdict: CLEAN (0 fail, 0 warn, 45 exempted)
  1280x800: 8 component(s), ink 96.4%, 67 text block(s)
  768x900: 8 component(s), ink 95.4%, 52 text block(s)
  375x700: 8 component(s), ink 95.0%, 55 text block(s)

No integrity defects detected.

Exempted candidates (the tool's call — audit the rule, not the page):
  - [text-collision] div.plans > label:nth-of-type(1) > span:nth-of-type(1) > span:nth-of-type(2) > span:nth-of-type(3) @1280: clipped away by div.plans > label:nth-of-type(1) > span:nth-of-type(1) > span:nth-of-type(2) > span:nth-of-type(3) (overflow is not visible) — its box still overlaps text below the clip, but no glyph is painted there
  - [text-collision] div.plans > label:nth-of-type(2) > span:nth-of-type(1) > span:nth-of-type(2) > span:nth-of-type(3) @1280: clipped away by div.plans > label:nth-of-type(2) > span:nth-of-type(1) > span:nth-of-type(2) > span:nth-of-type(3) (overflow is not visible) — its box still overlaps text below the clip, but no glyph is painted there
  - [text-collision] div.plans > label:nth-of-type(3) > span:nth-of-type(1) > span:nth-of-type(2) > span:nth-of-type(3) @1280: clipped away by div.plans > label:nth-of-type(3) > span:nth-of-type(1) > span:nth-of-type(2) > span:nth-of-type(3) (overflow is not visible) — its box still overlaps text below the clip, but no glyph is painted there
  - [text-clipped] div.plans > label:nth-of-type(1) > span:nth-of-type(1) > span:nth-of-type(2) > span:nth-of-type(3) @1280: visually-hidden (sr-only) pattern — 1px box, text for AT only
  - [text-clipped] div.plans > label:nth-of-type(1) > span:nth-of-type(1) > span:nth-of-type(3) > span:nth-of-type(1) @1280: visually-hidden (sr-only) pattern — 1px box, text for AT only
  - [text-clipped] div.plans > label:nth-of-type(1) > span:nth-of-type(1) > span:nth-of-type(3) > span:nth-of-type(3) @1280: visually-hidden (sr-only) pattern — 1px box, text for AT only
  - [text-clipped] div.plans > label:nth-of-type(1) > span:nth-of-type(1) > span:nth-of-type(4) > span:nth-of-type(1) @1280: visually-hidden (sr-only) pattern — 1px box, text for AT only
  - [text-clipped] div.plans > label:nth-of-type(2) > span:nth-of-type(1) > span:nth-of-type(2) > span:nth-of-type(3) @1280: visually-hidden (sr-only) pattern — 1px box, text for AT only
  - [text-clipped] div.plans > label:nth-of-type(2) > span:nth-of-type(1) > span:nth-of-type(3) > span:nth-of-type(1) @1280: visually-hidden (sr-only) pattern — 1px box, text for AT only
  - [text-clipped] div.plans > label:nth-of-type(2) > span:nth-of-type(1) > span:nth-of-type(3) > span:nth-of-type(3) @1280: visually-hidden (sr-only) pattern — 1px box, text for AT only
  - [text-clipped] div.plans > label:nth-of-type(2) > span:nth-of-type(1) > span:nth-of-type(4) > span:nth-of-type(1) @1280: visually-hidden (sr-only) pattern — 1px box, text for AT only
  - [text-clipped] span.plan__badge > span:nth-of-type(1) @1280: visually-hidden (sr-only) pattern — 1px box, text for AT only
  - [text-clipped] div.plans > label:nth-of-type(3) > span:nth-of-type(1) > span:nth-of-type(2) > span:nth-of-type(3) @1280: visually-hidden (sr-only) pattern — 1px box, text for AT only
  - [text-clipped] div.plans > label:nth-of-type(3) > span:nth-of-type(1) > span:nth-of-type(3) > span:nth-of-type(1) @1280: visually-hidden (sr-only) pattern — 1px box, text for AT only
  - [text-clipped] div.plans > label:nth-of-type(3) > span:nth-of-type(1) > span:nth-of-type(3) > span:nth-of-type(3) @1280: visually-hidden (sr-only) pattern — 1px box, text for AT only
  … 30 more
G2exit 1vlmkit check copy index.html --manifest copy.txt

missing 4 · placeholders 0 · manifestLines 10 · statesExplored 1 · revealedOnly 0

output, 13 line(s), 1.4s
vlmkit check copy
source: index.html

status: suspect
rendered text: 1160 chars
disclosure states: 1 explored (details / tabs / aria-expanded)
manifest: 10 line(s), missing 4

Issues:
  x copy-missing: Manifest line not found in rendered text or any of 1 revealed disclosure state(s): "Delivery details" (comparison is whitespace-normalized, case-sensitive).
  x copy-missing: Manifest line not found in rendered text or any of 1 revealed disclosure state(s): "Review your order" (comparison is whitespace-normalized, case-sensitive).
  x copy-missing: Manifest line not found in rendered text or any of 1 revealed disclosure state(s): "Place order" (comparison is whitespace-normalized, case-sensitive).
  x copy-missing: Manifest line not found in rendered text or any of 1 revealed disclosure state(s): "Thanks — your first box is on its way." (comparison is whitespace-normalized, case-sensitive).
G3exit 0vlmkit check design index.html

verdict: COHERENT (1 finding(s))

verdict coherent · drifting 0 · roles 8

output, 26 line(s), 1.6s
vlmkit check design
source: index.html

verdict: COHERENT (1 finding(s))
  roles judged: 1 of 8 seen, spacing values: 18
  coverage: 12 of 152 visible element(s) carried an inferable role; 1 skipped as non-resting
    no role: span x56, div x21, p x11, label x8, path x8, dd x6, +14 more tag(s)
    A role comes from role="..." or from button/input/select/textarea/h1-h6. Layout elements
    (div, span, p, a) have none, so a large skip count is normal — this gate judges components,
    not every box. Add role="..." where an element IS a component to widen the coverage.

Role reuse (instances / distinct styles; drift below 3x from 3 instances)
  input:checkbox   4 inst    1 styles  reuse     4x  0 one-off  ok
  input:radio      2 inst    1 styles  reuse     2x  0 one-off  not judged
  note             1 inst    1 styles  reuse     1x  1 one-off  not judged
  h2               1 inst    1 styles  reuse     1x  1 one-off  not judged
  list             1 inst    1 styles  reuse     1x  1 one-off  not judged
  h1               1 inst    1 styles  reuse     1x  1 one-off  not judged
  select           1 inst    1 styles  reuse     1x  1 one-off  not judged
  button           1 inst    1 styles  reuse     1x  1 one-off  not judged
  not judged: input:radio (2), note (1), h2 (1), list (1), h1 (1), select (1), button (1) — under --min-instances 3, so no finding can come from them.
  To keep judging them: --min-instances 2 --min-reuse 2 (both — a 2-instance role cannot reach 3x however many instances the floor allows).

Informational (true, but does not carry the verdict)
  i [scale-outlier]: 3 spacing value(s) sit just off the page's own scale: 11px (2x) next to 10px (10x) — select#day paddingTop; 20px (2x) next to 18px (8x) — main#main rowGap; 44px (1x) next to 40px (5x) — select#day paddingRight. Snap them to the established value or add them deliberately.
D7 defect select padding, main#main row-gap

check design flags three spacing values just off the page's own scale: 11px input padding next to the established 10px, the 20px main row gap next to 18px, and 44px select right padding next to 40px.

G4exit 0vlmkit check composition index.html

verdict: COMPOSED (0 finding(s))

verdict composed · proximity 0 · flatSteps 0 · railNearMisses 0

output, 16 line(s), 1.7s
vlmkit check composition
source: index.html

verdict: COMPOSED (0 finding(s))
  measured: 0 label(s), 2 heading level(s), 31 wide block(s) on 4 left / 4 right rail(s) — from 137 visible box(es)
  2 label(s) not judged: they open a box that paints its own group edge, so nothing above them could be mis-grouped with

Type hierarchy (largest instance of each declared level)
  h1     30px / 700   h1#step-1-title
  h2     18px / 700   h2#summary-title
  body 16px, largest 30px (1.88x), heaviest +400 weight

Group gaps: 20px between heading-led groups vs 8px inside them (2.5x, 1 sample(s)). Context only — intact pages measure 0.86-3.00x, so this cannot carry a verdict.

Proximity, alignment and type hierarchy all read consistently.
G5exit 0vlmkit check color index.html

verdict: CONSISTENT (0 finding(s))

verdict consistent · base #f6f3ee · bodyInk #1f1c19 · linkInk #000000 · invisibleControls 0 · colorOnlyLinks 0 · unreadableColors 0

output, 25 line(s), 1.6s
vlmkit check color
source: index.html

verdict: CONSISTENT (0 finding(s))
  measured: 1 control(s), 0 link(s) in a text flow — from 160 visible box(es)

Palette (by painted area; reported, never judged — see the rejected candidates in the module docs)
  surfaces  4 distinct
    #f6f3ee   58.9%     1 el  
    #ffffff   37.6%    20 el  header.site-header>div.brand-bar
    #f4ecdc    2.0%     2 el  header.site-header>div.demo-notice
    #edf6f0    1.6%     1 el  div.plans>label.plan>input.plan__input
  ink       4 distinct
    #1f1c19   54.7%    42 el  div.demo-notice>div.wrap>p
    #6a635b   32.4%    26 el  div.summary-item>div.summary-item__text>p.summary-item__meta
    #57514a    9.2%     5 el  div.brand-bar>div.wrap>p.brand-bar__meta
    #ffffff    3.6%     3 el  label.plan>span.plan__body>span.plan__badge
  marks     5 distinct
    #857d73   37.2%    64 el  ol#steps>li.steps__item>span.steps__marker
    #dcd5ca   35.4%    10 el  header.site-header>div.brand-bar
    #2d6a4f   14.3%    12 el  ol#steps>li.steps__item>span.steps__marker
    #e7e1d8    7.1%     4 el  div#summary-panel>div.summary-item
    #e6d9bf    6.0%     1 el  header.site-header>div.demo-notice
  base #f6f3ee, body ink #1f1c19, link ink #000000 on 7 element(s)
G6exit 0vlmkit check a11y contrast index.html

inspected 81 · failures 0

output, 5 line(s), 1.5s
  vlmkit check a11y contrast
  html: examples/sites/checkout/index.html
  inspected 81 text-bearing element(s)
  ✓ 0 contrast failure(s)
  report: examples/sites/checkout/test-results/a11y-contrast/index-ca6a16e3/report.md
G7exit 1vlmkit check a11y focus index.html

steps 7 · findings 1

output, 6 line(s), 1.6s
  vlmkit check a11y focus
  source: index.html
  captured 7 focus step(s)
  ✗ 1 finding(s)
    [reverse] Focus moved up by 81px (from `form#step-1>fieldset.group>div.plans>label.plan>input.plan__input` at y=414 to `form#step-1>fieldset.group>div.choices>label.choice>input.choice__input` at y=334). Visual order is top-to-bottom; check `tabindex` or DOM order.
  report: examples/sites/checkout/test-results/a11y-focus-order/index-ca6a16e3/report.md
G8exit 0vlmkit check a11y touch index.html

level AA · inspected 9 · failures 0 · wcagExempt 0

output, 5 line(s), 1.5s
  vlmkit check a11y touch
  source: index.html  level: WCAG AA (24×24 min)
  inspected 9 interactive element(s)
  ✓ 0 undersized target(s)
  report: examples/sites/checkout/test-results/a11y-touch/index-ca6a16e3/report.md
S5

focus-order check: 2 Tabs at the gate's 1280x720 (radio -> Vegetarian)

index.html · 1280×720 · press Tab · press Tab

1 screen(s), looked at when taken; not kept once the round was done — a round keeps one full-page walk per width.

L5 saw

Viewport shot at the focus gate's own 1280x720 after pressing Tab twice. Focus is on the "Vegetarian" tile: a 3px dark-green ring offset 3px around the tile, clearly visible. The browser has scrolled the page to centre it: the tile's top sits at viewport y≈334, and the three plan cards are ABOVE it (card 1 is scrolled almost off the top, only its bottom edge and "a week" visible; cards 2 and 3 at y≈38-238). So focus moved from the first plan radio (page y≈414) DOWN to the checkbox (page y≈801); the 334 in the gate report is a post-scroll viewport coordinate. Tab order matches reading order: plans, then the four diet tiles left-to-right, then the select, then Continue. The sticky summary stays pinned at top 24px while the column scrolls.

N1false-positiveabout G7

check a11y focus reports 'reverse: focus moved up by 81px' from the plan radio (y=414) to the Vegetarian checkbox (y=334). Both y values are VIEWPORT coordinates read after the browser scrolled the new focus into view: its own report lists Continue at y=639 although it sits at page y~1106 (S1-2), and S5 (Tab x2 at the gate's 1280x720) shows the focused Vegetarian tile at viewport y~334 BELOW the plan cards, which scrolled up past the top. In page coordinates focus moves down (414 -> ~801). The DOM order equals the visual order and no tabindex is used on the page.

N2toolabout G7

check a11y focus compares focus positions in viewport coordinates without adding the scroll offset, so any Tab stop that makes the page scroll reads as moving backward. Verbatim: '[reverse] Focus moved up by 81px (from `form#step-1>fieldset.group>div.plans>label.plan>input.plan__input` at y=414 to `form#step-1>fieldset.group>div.choices>label.choice>input.choice__input` at y=334). Visual order is top-to-bottom; check `tabindex` or DOM order.' The report's own table shows it: stops 1-6 have y 334, 334, 396, 396, 534, 639 — all inside a 720px viewport although the page is 1341px tall. Any form longer than one screen will trip this.

G9exit 0vlmkit scan scroll index.html

containers 0 · overflowX 0 · issues 13

output, 25 line(s), 1.4s
vlmkit scan scroll
source: index.html (1280x720)

status: warn
  exits 0 — 13 warn(s) did not fail this command. To gate on one: --rule clipped-content=suspect
page: 1280x1341 — horizontal overflow 0px, vertical scroll 621px
scroll containers: 0 (dead scrollports 1, clipped 13)

Dead scrollports (declared scrollable, content fits):
  - #instructions (overflow: auto auto)

Issues:
  ! clipped-content div.plans > label:nth-of-type(1) > span:nth-of-type(1) > span:nth-of-type(2) > span:nth-of-type(3): div.plans > label:nth-of-type(1) > span:nth-of-type(1) > span:nth-of-type(2) > span:nth-of-type(3) clips 21px of its content behind overflow: hidden — cut-off content unless the oversize is decorative.
  ! clipped-content div.plans > label:nth-of-type(2) > span:nth-of-type(1) > span:nth-of-type(2) > span:nth-of-type(3): div.plans > label:nth-of-type(2) > span:nth-of-type(1) > span:nth-of-type(2) > span:nth-of-type(3) clips 21px of its content behind overflow: hidden — cut-off content unless the oversize is decorative.
  ! clipped-content div.plans > label:nth-of-type(3) > span:nth-of-type(1) > span:nth-of-type(2) > span:nth-of-type(3): div.plans > label:nth-of-type(3) > span:nth-of-type(1) > span:nth-of-type(2) > span:nth-of-type(3) clips 21px of its content behind overflow: hidden — cut-off content unless the oversize is decorative.
  ! clipped-content div.plans > label:nth-of-type(1) > span:nth-of-type(1) > span:nth-of-type(3) > span:nth-of-type(1): div.plans > label:nth-of-type(1) > span:nth-of-type(1) > span:nth-of-type(3) > span:nth-of-type(1) clips 20px of its content behind overflow: hidden — cut-off content unless the oversize is decorative.
  ! clipped-content div.plans > label:nth-of-type(1) > span:nth-of-type(1) > span:nth-of-type(3) > span:nth-of-type(3): div.plans > label:nth-of-type(1) > span:nth-of-type(1) > span:nth-of-type(3) > span:nth-of-type(3) clips 20px of its content behind overflow: hidden — cut-off content unless the oversize is decorative.
  ! clipped-content div.plans > label:nth-of-type(2) > span:nth-of-type(1) > span:nth-of-type(3) > span:nth-of-type(1): div.plans > label:nth-of-type(2) > span:nth-of-type(1) > span:nth-of-type(3) > span:nth-of-type(1) clips 20px of its content behind overflow: hidden — cut-off content unless the oversize is decorative.
  ! clipped-content div.plans > label:nth-of-type(2) > span:nth-of-type(1) > span:nth-of-type(3) > span:nth-of-type(3): div.plans > label:nth-of-type(2) > span:nth-of-type(1) > span:nth-of-type(3) > span:nth-of-type(3) clips 20px of its content behind overflow: hidden — cut-off content unless the oversize is decorative.
  ! clipped-content div.plans > label:nth-of-type(3) > span:nth-of-type(1) > span:nth-of-type(3) > span:nth-of-type(1): div.plans > label:nth-of-type(3) > span:nth-of-type(1) > span:nth-of-type(3) > span:nth-of-type(1) clips 20px of its content behind overflow: hidden — cut-off content unless the oversize is decorative.
  ! clipped-content div.plans > label:nth-of-type(3) > span:nth-of-type(1) > span:nth-of-type(3) > span:nth-of-type(3): div.plans > label:nth-of-type(3) > span:nth-of-type(1) > span:nth-of-type(3) > span:nth-of-type(3) clips 20px of its content behind overflow: hidden — cut-off content unless the oversize is decorative.
  ! clipped-content div.plans > label:nth-of-type(1) > span:nth-of-type(1) > span:nth-of-type(4) > span:nth-of-type(1): div.plans > label:nth-of-type(1) > span:nth-of-type(1) > span:nth-of-type(4) > span:nth-of-type(1) clips 19px of its content behind overflow: hidden — cut-off content unless the oversize is decorative.
  ! clipped-content div.plans > label:nth-of-type(2) > span:nth-of-type(1) > span:nth-of-type(4) > span:nth-of-type(1): div.plans > label:nth-of-type(2) > span:nth-of-type(1) > span:nth-of-type(4) > span:nth-of-type(1) clips 19px of its content behind overflow: hidden — cut-off content unless the oversize is decorative.
  ! clipped-content span.plan__badge > span:nth-of-type(1): span.plan__badge > span:nth-of-type(1) clips 19px of its content behind overflow: hidden — cut-off content unless the oversize is decorative.
  ! clipped-content div.plans > label:nth-of-type(3) > span:nth-of-type(1) > span:nth-of-type(4) > span:nth-of-type(1): div.plans > label:nth-of-type(3) > span:nth-of-type(1) > span:nth-of-type(4) > span:nth-of-type(1) clips 19px of its content behind overflow: hidden — cut-off content unless the oversize is decorative.
G10exit 0vlmkit scan handlers index.html

registrations 37 · elements 33 · suspects 0

output, 43 line(s), 1.7s
vlmkit scan handlers
source: index.html

status: 1 warn(s)
  exits 0 — 1 warn(s) did not fail this command. To gate on one: --rule unprobed-handler-types=suspect
registrations: 37 across 33 element(s), on a page presenting 9 control(s)

  - main#main>div.flow>form#step-1 "Choose your plan Fresh ingredients and e": submit, focusout (delegation container)
  - main#main>div.flow>form#step-2 "Delivery details Where should we bring y": submit, focusout (no role)
  - main#main>div.flow>form#step-3 "Payment Use a test card: this demo never": submit, focusout (no role)
  - main#main>div.flow>form#step-4 "Review your order Check the details, the": submit, focusout (no role)
  - div.flow>form#step-2>div.actions>button.btn "Back": click (no role)
  - div.flow>form#step-3>div.actions>button.btn "Back": click (no role)
  - div.flow>form#step-4>div.actions>button.btn "Back": click (no role)
  - div.review>section.review__block>div.review__head>a.review__edit "Edit plan": click (no role)
  - div.review>section.review__block>div.review__head>a.review__edit "Edit delivery details": click (no role)
  - div.review>section.review__block>div.review__head>a.review__edit "Edit payment": click (no role)
  - main#main>div.flow>form#step-1>div#errors-1 "": click (no role)
  - main#main>div.flow>form#step-2>div#errors-2 "": click (no role)
  - main#main>div.flow>form#step-3>div#errors-3 "": click (no role)
  - main#main>div.flow>form#step-4>div#errors-4 "": click (no role)
  - fieldset.group>div.plans>label.plan>input.plan__input "2p3m": change
  - fieldset.group>div.plans>label.plan>input.plan__input "4p3m": change
  - fieldset.group>div.plans>label.plan>input.plan__input "2p5m": change
  - fieldset.group>div.choices>label.choice>input.choice__input "Vegetarian": change
  - fieldset.group>div.choices>label.choice>input.choice__input "No pork": change
  - fieldset.group>div.choices>label.choice>input.choice__input "Dairy-free": change
  - fieldset.group>div.choices>label.choice>input.choice__input "Nut-free": change
  - form#step-1>div.field>div.select>select#day "Select a day Monday Tuesday Wednesday Th": change
  - div.fields>div.field>div.select>select#state "Select AlabamaAlaskaArizonaArkansasCalif": change (no role)
  - div#billing-fields>div.field>div.select>select#billing-state "Select AlabamaAlaskaArizonaArkansasCalif": change (no role)
  - form#step-4>div.field>label.choice>input#terms "": change (no role)
  - form#step-2>div.fields>div.field>textarea#instructions "": input (no role)
  - fieldset.group>div.fields>div.field>input#card-number "": input (no role)
  - fieldset.group>div.fields>div.field>input#expiry "": input (no role)
  - fieldset.group>div.fields>div.field>input#cvc "": input (no role)
  - form#step-3>fieldset.group>label.choice>input#billing-same "": change (no role)
  - form#step-3>div.field>div.promo__row>button#promo-apply "Apply": click (no role)
  - form#step-3>div.field>div.promo__row>input#promo "": keydown (no role)
  - main#main>aside#summary>button#summary-toggle "Show order summary $64.74": click (no role)

Issues:
  warn [unprobed-handler-types] 6 handler type(s) registered and NONE exercised: change, click, focusout, input, keydown, submit — this gate is an inventory and presses nothing. A clean result here says the wiring exists, not that it works. Run 'check interactions --handlers' to focus and activate the controls, add '--probe-drag' for the drag surfaces, or drive the rest with a 'verify flow' script.
G11exit 0vlmkit check interactions index.html

elements 9 · suspects 0 · warns 0

output, 15 line(s), 10.0s
vlmkit check interactions
source: index.html

status: ok
interactive elements: 9

  - [radio] "2p3m" focus✓ | ArrowDown: layout change
  - [radio] "4p3m" unreachable | ArrowDown: layout change
  - [radio] "2p5m" unreachable | ArrowDown: focus moves
  - [checkbox] "Vegetarian" focus✓ | Space: layout change
  - [checkbox] "No pork" focus✓ | Space: layout change
  - [checkbox] "Dairy-free" focus✓ | Space: layout change
  - [checkbox] "Nut-free" focus✓ | Space: layout change
  - [combobox] "Select a day Monday Tuesday Wednesday Thursday Friday Saturday" focus✓ | ArrowDown: layout change | Esc closes
  - [button] "Continue" focus✓ | Enter: focus moves within
G12exit 1vlmkit verify flow index.html --flow flow.json

verdict: FAILED (16/34 steps)

done false · passed 16 · total 34

output, 66 line(s), 2.1s
vlmkit verify flow
source: index.html

verdict: FAILED (16/34 steps)

✓ step 1: Step 1: Continue with no delivery day shows the error summary and moves focus to it
    ✓ #errors-1 visible 
    ✓ #errors-1 focused 
    ✓ #errors-1 text~"Select a delivery day" 
    ✓ #day aria-invalid=true 
    ✓ #day aria-describedby=day-hint day-error 
    ✓ #day-error visible 
✓ step 2: Choosing a day re-validates the field and clears the summary
    ✓ #day-error count=0 
    ✓ #errors-1 hidden 
    ✓ #day-note text~"Monday, October 12" 
    ✓ #summary-panel text~"Mon, Oct 12" 
✓ step 3: The 4-person radio card updates the order summary
    ✓ #summary-panel text~"4 people · 3 meals a week" 
    ✓ #summary-panel text~"$107.88" 
    ✓ #summary-panel text~"$116.51" 
✓ step 4: A dietary preference shows in the summary
    ✓ #summary-panel text~"Vegetarian" 
✓ step 5: Continue moves to step 2, marks it current and focuses its heading
    ✓ #step-1 hidden 
    ✓ #step-2 visible 
    ✓ #step-2-title focused 
    ✓ #step-2-title text~"Delivery details" 
    ✓ #steps [data-step='2'] aria-current=step 
    ✓ #steps [data-step='1'] text~"completed" 
✓ step 6: Step 2: Continue with empty fields lists six errors and ties each message to its field
    ✓ #errors-2 focused 
    ✓ #errors-2 li count=6 
    ✓ #full-name aria-invalid=true 
    ✓ #full-name aria-describedby=full-name-error 
    ✓ #zip aria-invalid=true 
    ✓ #phone[aria-invalid] count=0 
    ✓ .field__error .icon count=6 
✓ step 7: An error-summary link moves focus to its field
    ✓ #email focused 
✓ step 8: Fill the name
    ✓ #full-name focused 
✓ step 9: Leaving the name field re-validates it (error gone, summary down to five)
    ✓ #full-name[aria-invalid] count=0 
    ✓ #full-name-error count=0 
    ✓ #errors-2 li count=5 
✓ step 10: Leaving a malformed email shows the format message
    ✓ #email-error text~"like name@example.com" 
✓ step 11: Correct the email
    ✓ #email focused 
✓ step 12: Address line 1
    ✓ #email-error count=0 
✓ step 13: City
    ✓ #address1-error count=0 
✓ step 14: State by typeahead
    ✓ #state-error count=0 
✓ step 15: ZIP code
    ✓ #city-error count=0 
✓ step 16: Delivery instructions count down from 200
    ✓ #instructions-count text~"179 characters left" 
    ✓ #zip-error count=0 
✗ step 17: Continue to step 3
    ✓ #step-3 visible 
    ✓ #step-3-title focused 
    ✓ #steps [data-step='3'] aria-current=step 
    ✗ #billing-preview text~"1450 Alder Street, Portland, OR 97214" (got: )
D6 defect #billing-preview (step 3)

verify flow step 17 failed: the 'same as delivery' address preview under the billing checkbox is empty at step 3 (got: ''), so the customer cannot see which address they are agreeing to reuse. The one-line address is only recomputed on plan/day/diet changes, never when a step is entered.

G13exit 0vlmkit check scroll index.html

pageScrolled 621 · stickyFixed 1 · engagedSticky 1 · snaps 0 · issues 0

output, 12 line(s), 1.4s
vlmkit check scroll
source: index.html

status: ok
page scrolled: 621px
sticky/fixed elements: 1 (sticky engaged by the scroll: 1)
snap containers driven: 0

Sticky / fixed:
  - #summary: sticky top=24px viewport y 143 -> 24

No scroll-behavior issues detected.
G14exit 0vlmkit check breakpoints index.html --sweep

breakpoints 6 · issues 0 · sweepRanges 0

output, 15 line(s), 3.7s
vlmkit check breakpoints
source: index.html

status: ok
width sweep: 320-1280px step 25 (39 widths) — clean
breakpoints checked: 479, 480, 559, 560, 959, 960px (each at B-1 / B / B+1)

  479px: clean (max-width: 479px)
  480px: clean (min-width: 480px)
  559px: clean (max-width: 559px)
  560px: clean (min-width: 560px)
  959px: clean (max-width: 959px)
  960px: clean (min-width: 960px)

All boundaries consistent.
G15exit 0vlmkit check grounding index.html

targets 3 · scale 0.5 · issues 0

output, 13 line(s), 1.3s
vlmkit check grounding
source: index.html (1280x720)

status: ok
frame: 640x360 screenshot px — medium, cap 640x480, scale 0.50 (every coordinate below is in these 640x360 pixels)
targets: 3 actionable in frame (6 disabled or out of the frame)

Action map (click point in screenshot px):
  t1 radio "2p3m" @ (220,227) 287x40 div.plans > label:nth-of-type(1) > input:nth-of-type(1)
  t2 radio "4p3m" @ (220,280) 287x54 div.plans > label:nth-of-type(2) > input:nth-of-type(1)
  t3 radio "2p5m" @ (220,333) 287x40 div.plans > label:nth-of-type(3) > input:nth-of-type(1)

Every target in the frame is resolvable and routes its own clicks.
N3decision

Gate set, run every round and in full in the final round: the protocol floor (check integrity; check copy --manifest copy.txt; check design; check composition; check color; check a11y contrast; check a11y focus; check a11y touch; scan scroll; scan handlers; check interactions) plus, for this brief: verify flow --flow flow.json (the whole checkout: validation, re-validation on leave, focus to summary and headings, promo rejected and accepted, billing reveal, Edit round-trip, confirmation; it also asserts the required copy that only exists in later states, which check copy cannot reach), check scroll (the brief's sticky desktop summary must actually stick), check breakpoints --sweep (layout changes at 480/560/960 and the summary becomes a disclosure below 960), check grounding (three Edit links on the review step). One theme only (the brief asks for white and warm greys, no dark mode), so contrast runs once per state. No check animation/motion: nothing animates except 150ms colour transitions, all cut to 0.01ms under prefers-reduced-motion.

N4acceptedabout G2

The 4 missing lines (Delivery details, Review your order, Place order, Thanks — your first box is on its way.) are state-dependent by the brief's own design: step 2/3/4 headings, the step-4 button, and the confirmation that replaces the form. check copy renders the load state and sweeps only details/tabs/aria-expanded, so it cannot reach them. They are proven where they occur by verify flow text/visible assertions (step 5 asserts #step-2-title contains 'Delivery details'; later steps assert 'Review your order', 'Place order' and the confirmation title) — the flow will carry that evidence once D6 lets it past step 17.

N5false-positiveabout G9

All 13 clipped-content warnings name the 1px visually-hidden comma spans inside the plan cards (label.plan > span > span > span), the standard sr-only pattern that gives the radios their spoken pauses. check integrity's G1 exempts exactly these nodes as 'visually-hidden (sr-only) pattern — 1px box, text for AT only'. The one dead scrollport, #instructions, is a textarea whose default overflow:auto is correct: it scrolls once the text passes 3 rows (maxlength 200 keeps that small).

N6acceptedabout G10

unprobed-handler-types is the inventory telling us it pressed nothing. No pointer-only control is reported. Every listed handler type (change, click, focusout, input, keydown, submit) is driven for real by verify flow (G12 and later), and check interactions probes the keyboard side.

N7toolabout G15

check grounding lists the three plan radios as t1 radio "2p3m", t2 radio "4p3m", t3 radio "2p5m", i.e. their value attributes, under a column documented as the 'visible label'. check interactions (G11) does the same ([radio] "2p3m") and names the delivery-day select by its option text ('Select a day Monday Tuesday…') instead of its label. Chromium's own aria snapshot of the same page (Playwright locator.ariaSnapshot, a one-off diagnostic) gives radio "2 people , 3 meals a week , 6 servings , $9.99 per serving , $59.94 a week" and combobox "Delivery day". So the page is labelled correctly, and a reader of these two reports would think it is not.

F1fix for D1

Dropped the duplicate price from the summary item row (Subtotal carries it one line below), so the text column goes from ~160px to ~240px; the name and meta are built from no-wrap phrases ('2 people' · '3 meals a week', '6 servings' · '$9.99 each'), so any wrap can only fall at the middle dot.

F2fix for D2

The 'Best value' pill is now absolutely positioned to straddle the 4-person card's top border (top:-11px, aligned with the title text at 54px / 50px on phones); the plans list gap went 12 -> 18px to leave the pill room. It no longer adds a row, so all three cards share one height.

F3fix for D3

Below 480px the plan price drops from 20px to 18px, the card's column gap from 14 to 12px, and the panel padding from 20 to 16px, which returns ~20px to the title column so '3 meals a week' fits on one line at 375 and 360.

F4fix for D4

Email and Phone each take a full row (single-column is also the faster form layout); the phone input is capped at 20rem because a phone number is short. The CVC hint was shortened to '3 digits on the back' so the remaining Expiry/CVC pair stays single-line at the narrowest two-column width.

F5fix for D5

Removed the stray 'Checkout' label from the brand bar; the header is now the mark and wordmark only, which is the convention for a checkout.

F6fix for D6

goTo() now calls updateSummary() on every step change, so derived text (the one-line delivery address for the billing preview, the summary lines) is recomputed whenever a step is entered.

F7fix for D7

Snapped the three values to the page's scale: input padding 11 -> 10px (min-height still 48px), main row gap 20 -> 18px, select right padding 44 -> 40px.

R2 · fix pass + deep links

builder
N8decision

Added ?step=2|3|4|done deep links (documented in README). Every gate renders only the load state, so in round 1 check color measured 1 control and never saw a text field, although the brief leans on it for text-field boundaries (WCAG 1.4.11); contrast, touch, focus and interactions were likewise blind to steps 2-4. The parameter opens a step with the earlier steps filled from one fixed sample customer (Jordan Rivera, Portland OR), so each gate can be pointed at each step. verify flow still starts at step 1 and fills everything itself.

S6

step 1 after the fix pass

index.html · 1280×800 · full page, 2 screen(s)

S6 screen 1S6 screen 2
L6 saw

Desktop 1280, step 1 after the fix pass. Brand bar now holds only the green bowl mark and "Pantry Club" — the stray grey "Checkout" is gone and the bar reads as a clean checkout header. Summary card: the item shows the box illustration, "2 people · 3 meals a week" on ONE line and "6 servings · $9.99 each" on one line beneath, no stranded words; Subtotal $59.94 follows directly so no price is lost. The three plan rows are now the same height (~76px each, 18px apart); the "Best value" pill straddles the top border of the 4-person card, left-aligned with the title text at x≈207, and reads as a tab label with ~7px clear of card 1. Selected card 1 keeps its green border, mint fill and filled radio. Screen 2: diet tiles, delivery-day select and Continue unchanged; the summary is still pinned at top 24px while scrolled.

S7

step 1 after the fix pass

index.html · 375×812 · full page, 3 screen(s)

S7 screen 1S7 screen 2S7 screen 3
L7 saw

Mobile 375, step 1 after the fix pass, three screens. Header has only the mark and wordmark. Plan cards: card 2 now reads "4 people" / "3 meals a week" / "12 servings" / "$8.99 per serving" with "$107.88 / a week" at the right — "3 meals a week" stays on one line, no orphan; all three cards are the same four-line height. The "Best value" pill sits on the top border of card 2, aligned with the text at x≈83, 8px clear of card 1's bottom edge. The card text column gained width from the 16px panel padding; the left edge of every card and field aligns on one rail at x=33. Screen 2-3: four full-width diet tiles, "Delivery day" with a 2-line hint and a full-width select, full-width green Continue, footer. No overflow, no clipping.

V1verified D1 by L6

Summary name and meta each fit on one line; the wrap can now only fall at the middle dot.

V2verified D2 by L6

Three plan rows at one height; the pill straddles card 2's top edge.

V3verified D3 by L7

At 375 '3 meals a week' stays on one line in the 4-person card.

V4verified D5 by L6

Brand bar shows only the mark and wordmark.

S8

step 2 via ?step=2 (empty fields)

index.html?step=2 · 1280×800 · full page, 2 screen(s)

2 screen(s), looked at when taken; not kept once the round was done — a round keeps one full-page walk per width.

L8 saw

Desktop, step 2 opened with ?step=2 (step 1 prefilled: Tuesday). Tracker: step 1 is a solid green circle with a white check and a green connector to step 2, which has the ring + pale halo and a bold "Delivery" label; 3 and 4 grey. Summary shows "First delivery Tue, Oct 13". Card: "Delivery details" h1 + "Where should we bring your box?". Full name, then Email with its hint on its own full row, then Phone (optional) with its hint and a deliberately shorter 320px input — the misaligned Email/Phone pair from L4 is gone. Address line 1, Address line 2 (optional), then City / State (Select + chevron) / ZIP code on one aligned row, then Delivery instructions (optional) with hint, a 3-row textarea and "200 characters left". Actions: outlined "Back" left, green "Continue" right, above a hairline. Labels bold 16px, hints grey 14px, 6px label-to-control and ~22px between fields — an even rhythm. All field boundaries are clearly visible grey lines on white.

V5verified D4 by L8

Email and Phone are on separate full rows; nothing left to misalign.

G16exit 1vlmkit verify flow index.html --flow flow.json

verdict: FAILED (22/34 steps)

done false · passed 22 · total 34

output, 82 line(s), 2.3s
vlmkit verify flow
source: index.html

verdict: FAILED (22/34 steps)

✓ step 1: Step 1: Continue with no delivery day shows the error summary and moves focus to it
    ✓ #errors-1 visible 
    ✓ #errors-1 focused 
    ✓ #errors-1 text~"Select a delivery day" 
    ✓ #day aria-invalid=true 
    ✓ #day aria-describedby=day-hint day-error 
    ✓ #day-error visible 
✓ step 2: Choosing a day re-validates the field and clears the summary
    ✓ #day-error count=0 
    ✓ #errors-1 hidden 
    ✓ #day-note text~"Monday, October 12" 
    ✓ #summary-panel text~"Mon, Oct 12" 
✓ step 3: The 4-person radio card updates the order summary
    ✓ #summary-panel text~"4 people · 3 meals a week" 
    ✓ #summary-panel text~"$107.88" 
    ✓ #summary-panel text~"$116.51" 
✓ step 4: A dietary preference shows in the summary
    ✓ #summary-panel text~"Vegetarian" 
✓ step 5: Continue moves to step 2, marks it current and focuses its heading
    ✓ #step-1 hidden 
    ✓ #step-2 visible 
    ✓ #step-2-title focused 
    ✓ #step-2-title text~"Delivery details" 
    ✓ #steps [data-step='2'] aria-current=step 
    ✓ #steps [data-step='1'] text~"completed" 
✓ step 6: Step 2: Continue with empty fields lists six errors and ties each message to its field
    ✓ #errors-2 focused 
    ✓ #errors-2 li count=6 
    ✓ #full-name aria-invalid=true 
    ✓ #full-name aria-describedby=full-name-error 
    ✓ #zip aria-invalid=true 
    ✓ #phone[aria-invalid] count=0 
    ✓ .field__error .icon count=6 
✓ step 7: An error-summary link moves focus to its field
    ✓ #email focused 
✓ step 8: Fill the name
    ✓ #full-name focused 
✓ step 9: Leaving the name field re-validates it (error gone, summary down to five)
    ✓ #full-name[aria-invalid] count=0 
    ✓ #full-name-error count=0 
    ✓ #errors-2 li count=5 
✓ step 10: Leaving a malformed email shows the format message
    ✓ #email-error text~"like name@example.com" 
✓ step 11: Correct the email
    ✓ #email focused 
✓ step 12: Address line 1
    ✓ #email-error count=0 
✓ step 13: City
    ✓ #address1-error count=0 
✓ step 14: State by typeahead
    ✓ #state-error count=0 
✓ step 15: ZIP code
    ✓ #city-error count=0 
✓ step 16: Delivery instructions count down from 200
    ✓ #instructions-count text~"179 characters left" 
    ✓ #zip-error count=0 
✓ step 17: Continue to step 3
    ✓ #step-3 visible 
    ✓ #step-3-title focused 
    ✓ #steps [data-step='3'] aria-current=step 
    ✓ #billing-preview text~"1450 Alder Street, Portland, OR 97214" 
✓ step 18: Step 3: Continue with no card lists four errors
    ✓ #errors-3 focused 
    ✓ #errors-3 li count=4 
    ✓ #card-number aria-invalid=true 
✓ step 19: Card number
    ✓ #card-number focused 
✓ step 20: Expiry (the slash is inserted as you type)
    ✓ #card-number-error count=0 
✓ step 21: Security code
    ✓ #expiry-error count=0 
✓ step 22: Name on card
    ✓ #cvc-error count=0 
✗ step 23: Unticking 'same as delivery' reveals the billing fields
    ✗ #billing-fields visible (got: hidden/absent)
    ✗ #billing-address1 visible (got: hidden/absent)
    ✓ #card-name-error count=0
D8 defect blur re-validation vs. pointer clicks (step 3 billing tile; any control below a corrected field)

verify flow step 23 failed: after the last card error was fixed, one click on the 'same as delivery' tile did nothing (#billing-fields still hidden). The click works on its own (S9), so the cause is the sequence: mousedown blurs #card-name, blur re-validation clears its error and hides the now-empty error summary at the top of the step, the whole form jumps up ~130px, and mouseup lands on a different element, so no click is dispatched. A real customer hits the same dead click on Continue or on any control below a field whose error appears or clears on blur.

S9

step 3: click the billing tile (flow step 23 repro)

index.html?step=3 · 1280×800 · element #step-3 fieldset:nth-of-type(2) · click .choice:has(#billing-same)

1 screen(s), looked at when taken; not kept once the round was done — a round keeps one full-page walk per width.

L9 saw

Close-up of step 3's Billing address group (opened with ?step=3) after one click on the "Billing address is the same as delivery" tile: the box is now unticked (grey outline, white fill, no check) and the billing fields appeared directly below it — Billing address line 1, Billing address line 2 (optional), and a City / State (Select) / ZIP code row, all with clear grey borders and bold labels, the same field style as step 2. So the tile toggles on a click when nothing else on the page is changing; the failure in G16 step 23 is not the tile itself.

V6verified D6 by G16

Flow step 17 now passes: #billing-preview contains '1450 Alder Street, Portland, OR 97214'.

F8fix for D8

Blur re-validation no longer runs while a mouse button is held: a capture-phase mousedown sets a flag, focusout queues the field name, and mouseup flushes the queue in a setTimeout(0), i.e. after the click has been dispatched. The error still appears or clears as the field is left, but the page moves only after the click, never between mousedown and mouseup. Tab-away keeps the immediate re-validation.

G17exit 1vlmkit verify flow index.html --flow flow.json

verdict: FAILED (22/34 steps)

done false · passed 22 · total 34

output, 82 line(s), 2.3s
vlmkit verify flow
source: index.html

verdict: FAILED (22/34 steps)

✓ step 1: Step 1: Continue with no delivery day shows the error summary and moves focus to it
    ✓ #errors-1 visible 
    ✓ #errors-1 focused 
    ✓ #errors-1 text~"Select a delivery day" 
    ✓ #day aria-invalid=true 
    ✓ #day aria-describedby=day-hint day-error 
    ✓ #day-error visible 
✓ step 2: Choosing a day re-validates the field and clears the summary
    ✓ #day-error count=0 
    ✓ #errors-1 hidden 
    ✓ #day-note text~"Monday, October 12" 
    ✓ #summary-panel text~"Mon, Oct 12" 
✓ step 3: The 4-person radio card updates the order summary
    ✓ #summary-panel text~"4 people · 3 meals a week" 
    ✓ #summary-panel text~"$107.88" 
    ✓ #summary-panel text~"$116.51" 
✓ step 4: A dietary preference shows in the summary
    ✓ #summary-panel text~"Vegetarian" 
✓ step 5: Continue moves to step 2, marks it current and focuses its heading
    ✓ #step-1 hidden 
    ✓ #step-2 visible 
    ✓ #step-2-title focused 
    ✓ #step-2-title text~"Delivery details" 
    ✓ #steps [data-step='2'] aria-current=step 
    ✓ #steps [data-step='1'] text~"completed" 
✓ step 6: Step 2: Continue with empty fields lists six errors and ties each message to its field
    ✓ #errors-2 focused 
    ✓ #errors-2 li count=6 
    ✓ #full-name aria-invalid=true 
    ✓ #full-name aria-describedby=full-name-error 
    ✓ #zip aria-invalid=true 
    ✓ #phone[aria-invalid] count=0 
    ✓ .field__error .icon count=6 
✓ step 7: An error-summary link moves focus to its field
    ✓ #email focused 
✓ step 8: Fill the name
    ✓ #full-name focused 
✓ step 9: Leaving the name field re-validates it (error gone, summary down to five)
    ✓ #full-name[aria-invalid] count=0 
    ✓ #full-name-error count=0 
    ✓ #errors-2 li count=5 
✓ step 10: Leaving a malformed email shows the format message
    ✓ #email-error text~"like name@example.com" 
✓ step 11: Correct the email
    ✓ #email focused 
✓ step 12: Address line 1
    ✓ #email-error count=0 
✓ step 13: City
    ✓ #address1-error count=0 
✓ step 14: State by typeahead
    ✓ #state-error count=0 
✓ step 15: ZIP code
    ✓ #city-error count=0 
✓ step 16: Delivery instructions count down from 200
    ✓ #instructions-count text~"179 characters left" 
    ✓ #zip-error count=0 
✓ step 17: Continue to step 3
    ✓ #step-3 visible 
    ✓ #step-3-title focused 
    ✓ #steps [data-step='3'] aria-current=step 
    ✓ #billing-preview text~"1450 Alder Street, Portland, OR 97214" 
✓ step 18: Step 3: Continue with no card lists four errors
    ✓ #errors-3 focused 
    ✓ #errors-3 li count=4 
    ✓ #card-number aria-invalid=true 
✓ step 19: Card number
    ✓ #card-number focused 
✓ step 20: Expiry (the slash is inserted as you type)
    ✓ #card-number-error count=0 
✓ step 21: Security code
    ✓ #expiry-error count=0 
✓ step 22: Name on card
    ✓ #cvc-error count=0 
✗ step 23: Unticking 'same as delivery' reveals the billing fields
    ✓ #billing-fields visible 
    ✓ #billing-address1 visible 
    ✗ #card-name-error count=0 (got: 1)
G18exit 0vlmkit verify flow index.html --flow flow.json

verdict: DONE (34/34 steps)

done true · passed 34 · total 34

output, 130 line(s), 2.6s
vlmkit verify flow
source: index.html

verdict: DONE (34/34 steps)

✓ step 1: Step 1: Continue with no delivery day shows the error summary and moves focus to it
    ✓ #errors-1 visible 
    ✓ #errors-1 focused 
    ✓ #errors-1 text~"Select a delivery day" 
    ✓ #day aria-invalid=true 
    ✓ #day aria-describedby=day-hint day-error 
    ✓ #day-error visible 
✓ step 2: Choosing a day re-validates the field and clears the summary
    ✓ #day-error count=0 
    ✓ #errors-1 hidden 
    ✓ #day-note text~"Monday, October 12" 
    ✓ #summary-panel text~"Mon, Oct 12" 
✓ step 3: The 4-person radio card updates the order summary
    ✓ #summary-panel text~"4 people · 3 meals a week" 
    ✓ #summary-panel text~"$107.88" 
    ✓ #summary-panel text~"$116.51" 
✓ step 4: A dietary preference shows in the summary
    ✓ #summary-panel text~"Vegetarian" 
✓ step 5: Continue moves to step 2, marks it current and focuses its heading
    ✓ #step-1 hidden 
    ✓ #step-2 visible 
    ✓ #step-2-title focused 
    ✓ #step-2-title text~"Delivery details" 
    ✓ #steps [data-step='2'] aria-current=step 
    ✓ #steps [data-step='1'] text~"completed" 
✓ step 6: Step 2: Continue with empty fields lists six errors and ties each message to its field
    ✓ #errors-2 focused 
    ✓ #errors-2 li count=6 
    ✓ #full-name aria-invalid=true 
    ✓ #full-name aria-describedby=full-name-error 
    ✓ #zip aria-invalid=true 
    ✓ #phone[aria-invalid] count=0 
    ✓ .field__error .icon count=6 
✓ step 7: An error-summary link moves focus to its field
    ✓ #email focused 
✓ step 8: Fill the name
    ✓ #full-name focused 
✓ step 9: Leaving the name field re-validates it (error gone, summary down to five)
    ✓ #full-name[aria-invalid] count=0 
    ✓ #full-name-error count=0 
    ✓ #errors-2 li count=5 
✓ step 10: Leaving a malformed email shows the format message
    ✓ #email-error text~"like name@example.com" 
✓ step 11: Correct the email
    ✓ #email focused 
✓ step 12: Address line 1
    ✓ #email-error count=0 
✓ step 13: City
    ✓ #address1-error count=0 
✓ step 14: State by typeahead
    ✓ #state-error count=0 
✓ step 15: ZIP code
    ✓ #city-error count=0 
✓ step 16: Delivery instructions count down from 200
    ✓ #instructions-count text~"179 characters left" 
    ✓ #zip-error count=0 
✓ step 17: Continue to step 3
    ✓ #step-3 visible 
    ✓ #step-3-title focused 
    ✓ #steps [data-step='3'] aria-current=step 
    ✓ #billing-preview text~"1450 Alder Street, Portland, OR 97214" 
✓ step 18: Step 3: Continue with no card lists four errors
    ✓ #errors-3 focused 
    ✓ #errors-3 li count=4 
    ✓ #card-number aria-invalid=true 
✓ step 19: Card number
    ✓ #card-number focused 
✓ step 20: Expiry (the slash is inserted as you type)
    ✓ #card-number-error count=0 
✓ step 21: Security code
    ✓ #expiry-error count=0 
✓ step 22: Name on card
    ✓ #cvc-error count=0 
✓ step 23: Unticking 'same as delivery' reveals the billing fields
    ✓ #billing-fields visible 
    ✓ #billing-address1 visible 
    ✓ #card-name-error count=0 
✓ step 24: Ticking it again hides them
    ✓ #billing-fields hidden 
✓ step 25: Type an unknown promo code
    ✓ #promo focused 
✓ step 26: An unknown promo code is rejected with a message
    ✓ #promo-message text~"isn’t a valid promo code" 
    ✓ #promo aria-invalid=true 
    ✓ #summary-promo hidden 
✓ step 27: Type WELCOME10 in lower case
    ✓ #promo focused 
✓ step 28: WELCOME10 takes 10% off and the summary follows
    ✓ #promo-message text~"WELCOME10 applied" 
    ✓ #promo[aria-invalid] count=0 
    ✓ #summary-promo visible 
    ✓ #summary-promo text~"$10.79" 
    ✓ #summary-panel text~"$104.86" 
    ✓ #promo-apply text~"Remove" 
✓ step 29: Continue to review
    ✓ #step-4 visible 
    ✓ #step-4-title focused 
    ✓ #step-4-title text~"Review your order" 
    ✓ #step-4 text~"Card ending 4242, expires 12/28" 
    ✓ #step-4 text~"1450 Alder Street" 
    ✓ #step-4 text~"WELCOME10" 
    ✓ #step-4 text~"Vegetarian" 
    ✓ #step-4 button[type=submit] visible 
    ✓ #step-4 button[type=submit] text~"Place order" 
✓ step 30: Edit on the Delivery block goes back to step 2
    ✓ #step-2 visible 
    ✓ #step-2-title focused 
✓ step 31: …and Continue returns straight to review
    ✓ #step-4 visible 
    ✓ #step-4-title focused 
✓ step 32: Place order without agreeing to the terms is stopped
    ✓ #errors-4 focused 
    ✓ #terms aria-invalid=true 
    ✓ #confirmation hidden 
✓ step 33: Agree to the terms
    ✓ #terms-error count=0 
✓ step 34: Place order replaces the form with the confirmation
    ✓ #confirmation visible 
    ✓ #step-4 hidden 
    ✓ #steps hidden 
    ✓ #confirmation-title focused 
    ✓ #confirmation-title text~"Thanks — your first box is on its way." 
    ✓ #confirmation text~"PC-20417" 
    ✓ #confirmation text~"Monday, October 12, 2026" 
    ✓ #summary-panel text~"$104.86"
F9fix for D8

Refinement after G17: the queued re-validation is flushed synchronously in a capture-phase click listener (the click's target is already fixed, so the layout may move) instead of in a timer after mouseup; G17 showed the timer left a 1-task window where the error was still on screen after the click. mouseup keeps the timer as the fallback for a press that ends in no click.

G19exit 0vlmkit verify flow index.html --flow flow.json

verdict: DONE (34/34 steps)

done true · passed 34 · total 34

output, 130 line(s), 2.8s
vlmkit verify flow
source: index.html

verdict: DONE (34/34 steps)

✓ step 1: Step 1: Continue with no delivery day shows the error summary and moves focus to it
    ✓ #errors-1 visible 
    ✓ #errors-1 focused 
    ✓ #errors-1 text~"Select a delivery day" 
    ✓ #day aria-invalid=true 
    ✓ #day aria-describedby=day-hint day-error 
    ✓ #day-error visible 
✓ step 2: Choosing a day re-validates the field and clears the summary
    ✓ #day-error count=0 
    ✓ #errors-1 hidden 
    ✓ #day-note text~"Monday, October 12" 
    ✓ #summary-panel text~"Mon, Oct 12" 
✓ step 3: The 4-person radio card updates the order summary
    ✓ #summary-panel text~"4 people · 3 meals a week" 
    ✓ #summary-panel text~"$107.88" 
    ✓ #summary-panel text~"$116.51" 
✓ step 4: A dietary preference shows in the summary
    ✓ #summary-panel text~"Vegetarian" 
✓ step 5: Continue moves to step 2, marks it current and focuses its heading
    ✓ #step-1 hidden 
    ✓ #step-2 visible 
    ✓ #step-2-title focused 
    ✓ #step-2-title text~"Delivery details" 
    ✓ #steps [data-step='2'] aria-current=step 
    ✓ #steps [data-step='1'] text~"completed" 
✓ step 6: Step 2: Continue with empty fields lists six errors and ties each message to its field
    ✓ #errors-2 focused 
    ✓ #errors-2 li count=6 
    ✓ #full-name aria-invalid=true 
    ✓ #full-name aria-describedby=full-name-error 
    ✓ #zip aria-invalid=true 
    ✓ #phone[aria-invalid] count=0 
    ✓ .field__error .icon count=6 
✓ step 7: An error-summary link moves focus to its field
    ✓ #email focused 
✓ step 8: Fill the name
    ✓ #full-name focused 
✓ step 9: Leaving the name field re-validates it (error gone, summary down to five)
    ✓ #full-name[aria-invalid] count=0 
    ✓ #full-name-error count=0 
    ✓ #errors-2 li count=5 
✓ step 10: Leaving a malformed email shows the format message
    ✓ #email-error text~"like name@example.com" 
✓ step 11: Correct the email
    ✓ #email focused 
✓ step 12: Address line 1
    ✓ #email-error count=0 
✓ step 13: City
    ✓ #address1-error count=0 
✓ step 14: State by typeahead
    ✓ #state-error count=0 
✓ step 15: ZIP code
    ✓ #city-error count=0 
✓ step 16: Delivery instructions count down from 200
    ✓ #instructions-count text~"179 characters left" 
    ✓ #zip-error count=0 
✓ step 17: Continue to step 3
    ✓ #step-3 visible 
    ✓ #step-3-title focused 
    ✓ #steps [data-step='3'] aria-current=step 
    ✓ #billing-preview text~"1450 Alder Street, Portland, OR 97214" 
✓ step 18: Step 3: Continue with no card lists four errors
    ✓ #errors-3 focused 
    ✓ #errors-3 li count=4 
    ✓ #card-number aria-invalid=true 
✓ step 19: Card number
    ✓ #card-number focused 
✓ step 20: Expiry (the slash is inserted as you type)
    ✓ #card-number-error count=0 
✓ step 21: Security code
    ✓ #expiry-error count=0 
✓ step 22: Name on card
    ✓ #cvc-error count=0 
✓ step 23: Unticking 'same as delivery' reveals the billing fields
    ✓ #billing-fields visible 
    ✓ #billing-address1 visible 
    ✓ #card-name-error count=0 
✓ step 24: Ticking it again hides them
    ✓ #billing-fields hidden 
✓ step 25: Type an unknown promo code
    ✓ #promo focused 
✓ step 26: An unknown promo code is rejected with a message
    ✓ #promo-message text~"isn’t a valid promo code" 
    ✓ #promo aria-invalid=true 
    ✓ #summary-promo hidden 
✓ step 27: Type WELCOME10 in lower case
    ✓ #promo focused 
✓ step 28: WELCOME10 takes 10% off and the summary follows
    ✓ #promo-message text~"WELCOME10 applied" 
    ✓ #promo[aria-invalid] count=0 
    ✓ #summary-promo visible 
    ✓ #summary-promo text~"$10.79" 
    ✓ #summary-panel text~"$104.86" 
    ✓ #promo-apply text~"Remove" 
✓ step 29: Continue to review
    ✓ #step-4 visible 
    ✓ #step-4-title focused 
    ✓ #step-4-title text~"Review your order" 
    ✓ #step-4 text~"Card ending 4242, expires 12/28" 
    ✓ #step-4 text~"1450 Alder Street" 
    ✓ #step-4 text~"WELCOME10" 
    ✓ #step-4 text~"Vegetarian" 
    ✓ #step-4 button[type=submit] visible 
    ✓ #step-4 button[type=submit] text~"Place order" 
✓ step 30: Edit on the Delivery block goes back to step 2
    ✓ #step-2 visible 
    ✓ #step-2-title focused 
✓ step 31: …and Continue returns straight to review
    ✓ #step-4 visible 
    ✓ #step-4-title focused 
✓ step 32: Place order without agreeing to the terms is stopped
    ✓ #errors-4 focused 
    ✓ #terms aria-invalid=true 
    ✓ #confirmation hidden 
✓ step 33: Agree to the terms
    ✓ #terms-error count=0 
✓ step 34: Place order replaces the form with the confirmation
    ✓ #confirmation visible 
    ✓ #step-4 hidden 
    ✓ #steps hidden 
    ✓ #confirmation-title focused 
    ✓ #confirmation-title text~"Thanks — your first box is on its way." 
    ✓ #confirmation text~"PC-20417" 
    ✓ #confirmation text~"Monday, October 12, 2026" 
    ✓ #summary-panel text~"$104.86"
V7verified D8 by G19

verify flow DONE 34/34: step 23 reveals #billing-fields on the first click and #card-name-error is already gone right after it; every later step (promo rejected/accepted, review, Edit round-trip, terms error, confirmation) passes.

N9supersededabout G17

G17 was the first D8 attempt (re-validation deferred to a timer after mouseup): the tile click landed, but the flow read #card-name-error before the timer ran. Replaced by the click-phase flush (F9); G18/G19 pass 34/34.

G20exit 0vlmkit check design index.html

verdict: COHERENT (1 finding(s))

verdict coherent · drifting 0 · roles 8

output, 26 line(s), 1.6s
vlmkit check design
source: index.html

verdict: COHERENT (1 finding(s))
  roles judged: 1 of 8 seen, spacing values: 16
  coverage: 12 of 154 visible element(s) carried an inferable role; 1 skipped as non-resting
    no role: span x60, div x21, p x9, label x8, path x8, dd x6, +14 more tag(s)
    A role comes from role="..." or from button/input/select/textarea/h1-h6. Layout elements
    (div, span, p, a) have none, so a large skip count is normal — this gate judges components,
    not every box. Add role="..." where an element IS a component to widen the coverage.

Role reuse (instances / distinct styles; drift below 3x from 3 instances)
  input:checkbox   4 inst    1 styles  reuse     4x  0 one-off  ok
  input:radio      2 inst    1 styles  reuse     2x  0 one-off  not judged
  note             1 inst    1 styles  reuse     1x  1 one-off  not judged
  h2               1 inst    1 styles  reuse     1x  1 one-off  not judged
  list             1 inst    1 styles  reuse     1x  1 one-off  not judged
  h1               1 inst    1 styles  reuse     1x  1 one-off  not judged
  select           1 inst    1 styles  reuse     1x  1 one-off  not judged
  button           1 inst    1 styles  reuse     1x  1 one-off  not judged
  not judged: input:radio (2), note (1), h2 (1), list (1), h1 (1), select (1), button (1) — under --min-instances 3, so no finding can come from them.
  To keep judging them: --min-instances 2 --min-reuse 2 (both — a 2-instance role cannot reach 3x however many instances the floor allows).

Informational (true, but does not carry the verdict)
  i [scale-outlier]: 1 spacing value(s) sit just off the page's own scale: 20px (1x) next to 18px (11x) — ol#steps marginBottom. Snap them to the established value or add them deliberately.
D9 defect ol#steps margin-bottom

check design now flags the step tracker's 20px bottom margin as the one value next to the page's established 18px (11 uses), left over after D7 moved the main gap to 18px.

G21exit 0vlmkit check integrity index.html

verdict: CLEAN (0 fail, 0 warn, 45 exempted)

verdict clean · findings 0 · fails 0 · warns 0 · exempted 45 · viewports 3

output, 27 line(s), 4.1s
vlmkit check integrity
source: index.html

verdict: CLEAN (0 fail, 0 warn, 45 exempted)
  1280x800: 8 component(s), ink 96.3%, 65 text block(s)
  768x900: 8 component(s), ink 95.3%, 51 text block(s)
  375x700: 8 component(s), ink 94.9%, 54 text block(s)

No integrity defects detected.

Exempted candidates (the tool's call — audit the rule, not the page):
  - [text-collision] div.plans > label:nth-of-type(1) > span:nth-of-type(1) > span:nth-of-type(2) > span:nth-of-type(3) @1280: clipped away by div.plans > label:nth-of-type(1) > span:nth-of-type(1) > span:nth-of-type(2) > span:nth-of-type(3) (overflow is not visible) — its box still overlaps text below the clip, but no glyph is painted there
  - [text-collision] div.plans > label:nth-of-type(2) > span:nth-of-type(1) > span:nth-of-type(2) > span:nth-of-type(3) @1280: clipped away by div.plans > label:nth-of-type(2) > span:nth-of-type(1) > span:nth-of-type(2) > span:nth-of-type(3) (overflow is not visible) — its box still overlaps text below the clip, but no glyph is painted there
  - [text-collision] div.plans > label:nth-of-type(3) > span:nth-of-type(1) > span:nth-of-type(2) > span:nth-of-type(3) @1280: clipped away by div.plans > label:nth-of-type(3) > span:nth-of-type(1) > span:nth-of-type(2) > span:nth-of-type(3) (overflow is not visible) — its box still overlaps text below the clip, but no glyph is painted there
  - [text-clipped] div.plans > label:nth-of-type(1) > span:nth-of-type(1) > span:nth-of-type(2) > span:nth-of-type(3) @1280: visually-hidden (sr-only) pattern — 1px box, text for AT only
  - [text-clipped] div.plans > label:nth-of-type(1) > span:nth-of-type(1) > span:nth-of-type(3) > span:nth-of-type(1) @1280: visually-hidden (sr-only) pattern — 1px box, text for AT only
  - [text-clipped] div.plans > label:nth-of-type(1) > span:nth-of-type(1) > span:nth-of-type(3) > span:nth-of-type(3) @1280: visually-hidden (sr-only) pattern — 1px box, text for AT only
  - [text-clipped] div.plans > label:nth-of-type(1) > span:nth-of-type(1) > span:nth-of-type(4) > span:nth-of-type(1) @1280: visually-hidden (sr-only) pattern — 1px box, text for AT only
  - [text-clipped] div.plans > label:nth-of-type(2) > span:nth-of-type(1) > span:nth-of-type(2) > span:nth-of-type(3) @1280: visually-hidden (sr-only) pattern — 1px box, text for AT only
  - [text-clipped] div.plans > label:nth-of-type(2) > span:nth-of-type(1) > span:nth-of-type(3) > span:nth-of-type(1) @1280: visually-hidden (sr-only) pattern — 1px box, text for AT only
  - [text-clipped] div.plans > label:nth-of-type(2) > span:nth-of-type(1) > span:nth-of-type(3) > span:nth-of-type(3) @1280: visually-hidden (sr-only) pattern — 1px box, text for AT only
  - [text-clipped] div.plans > label:nth-of-type(2) > span:nth-of-type(1) > span:nth-of-type(4) > span:nth-of-type(1) @1280: visually-hidden (sr-only) pattern — 1px box, text for AT only
  - [text-clipped] span.plan__badge > span:nth-of-type(1) @1280: visually-hidden (sr-only) pattern — 1px box, text for AT only
  - [text-clipped] div.plans > label:nth-of-type(3) > span:nth-of-type(1) > span:nth-of-type(2) > span:nth-of-type(3) @1280: visually-hidden (sr-only) pattern — 1px box, text for AT only
  - [text-clipped] div.plans > label:nth-of-type(3) > span:nth-of-type(1) > span:nth-of-type(3) > span:nth-of-type(1) @1280: visually-hidden (sr-only) pattern — 1px box, text for AT only
  - [text-clipped] div.plans > label:nth-of-type(3) > span:nth-of-type(1) > span:nth-of-type(3) > span:nth-of-type(3) @1280: visually-hidden (sr-only) pattern — 1px box, text for AT only
  … 30 more
V8verified D7 by G20

The three flagged values (11px padding, 20px main gap, 44px select padding) no longer appear.

F10fix for D9

Tracker bottom margin 20 -> 18px, onto the page's established step.

G22exit 1vlmkit check color 'index.html?step=2'
output, 1 line(s), 0.7s
error: file not found: examples/sites/checkout/index.html?step=2
G23exit 1vlmkit check color 'file://$PWD/examples/sites/checkout/index.html?step=2'
output, 1 line(s), 0.8s
error: file not found: examples/sites/checkout/file:examples/sites/checkout/index.html?step=2
N10toolabout G23

Gates cannot open a local page in any state but its default: a query string on a path is read as part of the filename (G22: 'error: file not found: examples/sites/checkout/index.html?step=2'), and a file:// URL is joined onto the cwd as a relative path (G23: 'Error: file not found: examples/sites/checkout/file:examples/sites/checkout/index.html?step=2'; the same happens straight from the CLI with --no-ledger: 'file not found: /home/user/vlmkit/examples/sites/checkout/file:/home/user/vlmkit/examples/sites/checkout/index.html?step=2'). judge.mjs shot accepts 'index.html?step=2' fine, so only the gates are limited. For a multi-step form that leaves every gate except verify flow blind to steps 2-4.

N11supersededabout G22

Same run as G23's tool note: the query string made the path unresolvable; the per-step gate runs go through a local static server instead (see the next decision note).

N12supersededabout G23

Superseded by the http runs: the per-step gates now open http://127.0.0.1:48217/index.html?step=N.

N13decision

Per-step gate runs use a plain static server bound to 127.0.0.1 (python3 -m http.server 48217 --bind 127.0.0.1 --directory examples/sites/checkout), started for these runs only, because the gates cannot take a query string on a file path (N10). The page itself still makes no network requests and works from file://; the server only lets a gate open index.html?step=2|3|4|done. index.html with no parameter keeps being measured straight from disk.

G24exit 0vlmkit check color 'http://127.0.0.1:48217/index.html?step=2'

verdict: CONSISTENT (0 finding(s))

verdict consistent · base #f6f3ee · bodyInk #1f1c19 · linkInk #ffffff · invisibleControls 0 · colorOnlyLinks 0 · unreadableColors 0

output, 26 line(s), 1.5s
vlmkit check color
source: http://127.0.0.1:48217/index.html?step=2

verdict: CONSISTENT (0 finding(s))
  measured: 9 control(s), 0 link(s) in a text flow — from 118 visible box(es)

Palette (by painted area; reported, never judged — see the rejected candidates in the module docs)
  surfaces  3 distinct
    #f6f3ee   60.0%     1 el  
    #ffffff   38.2%    16 el  header.site-header>div.brand-bar
    #f4ecdc    1.8%     2 el  header.site-header>div.demo-notice
  ink       4 distinct
    #1f1c19   58.1%    30 el  div.demo-notice>div.wrap>p
    #6a635b   34.0%    16 el  div.summary-item>div.summary-item__text>p.summary-item__meta
    #57514a    4.8%     3 el  li.steps__item>span.steps__marker>span.steps__num
    #ffffff    3.1%     1 el  form#step-2>div.actions>button.btn
  marks     5 distinct
    #857d73   45.6%    48 el  ol#steps>li.steps__item>span.steps__marker
    #dcd5ca   38.2%    10 el  header.site-header>div.brand-bar
    #e7e1d8    7.4%     4 el  div#summary-panel>div.summary-item
    #e6d9bf    6.3%     1 el  header.site-header>div.demo-notice
    #2d6a4f    2.5%     8 el  ol#steps>li.steps__item>span.steps__marker
  base #f6f3ee, body ink #1f1c19, link ink #ffffff on 1 element(s)

  http://127.0.0.1:48217/index.html?step=2 is live and not pinned — a re-run may measure different data. Pin it: vlmkit snapshot record-har http://127.0.0.1:48217/index.html?step=2 --out app.har, then --har app.har
G25exit 0vlmkit check design index.html

verdict: COHERENT (0 finding(s))

verdict coherent · drifting 0 · roles 8

output, 25 line(s), 1.6s
vlmkit check design
source: index.html

verdict: COHERENT (0 finding(s))
  roles judged: 1 of 8 seen, spacing values: 15
  coverage: 12 of 154 visible element(s) carried an inferable role; 1 skipped as non-resting
    no role: span x60, div x21, p x9, label x8, path x8, dd x6, +14 more tag(s)
    A role comes from role="..." or from button/input/select/textarea/h1-h6. Layout elements
    (div, span, p, a) have none, so a large skip count is normal — this gate judges components,
    not every box. Add role="..." where an element IS a component to widen the coverage.

Role reuse (instances / distinct styles; drift below 3x from 3 instances)
  input:checkbox   4 inst    1 styles  reuse     4x  0 one-off  ok
  input:radio      2 inst    1 styles  reuse     2x  0 one-off  not judged
  note             1 inst    1 styles  reuse     1x  1 one-off  not judged
  h2               1 inst    1 styles  reuse     1x  1 one-off  not judged
  list             1 inst    1 styles  reuse     1x  1 one-off  not judged
  h1               1 inst    1 styles  reuse     1x  1 one-off  not judged
  select           1 inst    1 styles  reuse     1x  1 one-off  not judged
  button           1 inst    1 styles  reuse     1x  1 one-off  not judged
  not judged: input:radio (2), note (1), h2 (1), list (1), h1 (1), select (1), button (1) — under --min-instances 3, so no finding can come from them.
  To keep judging them: --min-instances 2 --min-reuse 2 (both — a 2-instance role cannot reach 3x however many instances the floor allows).

No design drift detected.
V9verified D9 by G25

check design: COHERENT, 0 findings, no scale outliers left.

R3 · states: errors, promo, billing, review, confirmation, phone summary

builder
S10

step 2 with validation errors (Continue on empty fields)

index.html?step=2 · 1280×800 · full page, 3 screen(s) · click #step-2 button[type=submit]

3 screen(s), looked at when taken; not kept once the round was done — a round keeps one full-page walk per width.

L10 saw

Desktop, step 2 after Continue on empty fields (3 screens). The error summary sits directly under "Delivery details / Where should we bring your box?": a pale pink panel with a 5px red left bar, a red (!) icon and "6 fields need your attention" in dark red bold, then six underlined dark-red links — Enter your full name / email address / the first line of your address / city / Select your state / Enter your ZIP code. It has focus, shown by the same 3px dark-green ring used everywhere (offset 3px), so it is impossible to miss, yet the tint is soft rather than alarming. Each failing field has a 2px red border plus a bold red line with a (!) icon under it ("Enter your full name", "Enter your email address", …); Phone and Address line 2 (optional) stay grey and unmarked, so the page distinguishes required from optional. Screen 2: the City / State / ZIP row carries three errors; "Enter your / ZIP code" wraps to two lines in the 131px ZIP column, so that row's error line is ragged — legible and still under its field, acceptable. Screen 3: Back + Continue (hover-darkened after the click). The summary stays pinned at top 24px throughout.

S11

step 2 with validation errors (Continue on empty fields)

index.html?step=2 · 375×812 · full page, 3 screen(s) · click #step-2 button[type=submit]

3 screen(s), looked at when taken; not kept once the round was done — a round keeps one full-page walk per width.

L11 saw

Mobile 375, step 2 with the same six errors (3 screens). The summary box is focused (dark-green ring) directly under the heading; its title wraps as "6 fields need your / attention" and two links wrap ("Enter your email / address", "Enter the first line of / your address") — all still underlined and readable with a clear hanging indent. Fields stack full width with 2px red borders and icon + bold red messages beneath; City has its own row, State and ZIP share one (7/5 split) with "Select your state" on one line and "Enter your / ZIP code" on two. Phone keeps its grey unmarked border. Back and a wider Continue sit side by side at the bottom (both 52px tall). No text overlaps a border, nothing is clipped, and the page scrolls vertically only (2204px).

S12

step 3: card filled, promo SAVE20 rejected

index.html?step=3 · 1280×800 · full page, 2 screen(s) · fill #card-number "4242424242424242" · fill #expiry "1228" · fill #cvc "123" · fill #card-name "Jordan Rivera" · fill #promo "SAVE20" · click #promo-apply

2 screen(s), looked at when taken; not kept once the round was done — a round keeps one full-page walk per width.

L12 saw

Desktop, step 3 opened with ?step=3, card fields filled and SAVE20 applied (2 screens). Tracker: Plan and Delivery done (green checks, green connectors), Payment current. "Payment" h1 with "Use a test card: this demo never charges or stores it." Card number typed as 16 bare digits now reads "4242 4242 4242 4242" (grouped in fours by the input handler); Expiry "12/28" got its slash; Expiry/CVC hints ("MM/YY", "3 digits on the back") are both one line so the pair aligns. The "Card details" legend sits only 12px above the "Card number" label — two bold lines close together, readable but tight. Screen 2: the checked "Billing address is the same as delivery" tile (green border, mint fill, white tick) with the delivery address previewed underneath in grey: "1450 Alder Street, Apt 3, Portland, OR 97214". Promo: red-bordered "SAVE20" input + outlined "Apply", and the error line (!) "“SAVE20” isn’t a valid promo code. Check the spelling and try" wraps, leaving "again." alone on a second line — an orphan in the one message the customer reads closely. Summary total unchanged at $64.74, no promo row, which is correct.

D10 defect #promo-message (rejected code)

The rejected-code message wraps and strands 'again.' on a line of its own under the promo field; the sentence is longer than it needs to be for the width it has.

F11fix for D10

Shortened the message to '“SAVE20” isn’t a valid promo code. Check the spelling.' (~50 characters), which fits the field's width on one line at desktop.

S13

promo rejected, shorter message

index.html?step=3 · 1280×800 · element #step-3 .promo · fill #promo "SAVE20" · click #promo-apply

1 screen(s), looked at when taken; not kept once the round was done — a round keeps one full-page walk per width.

L13 saw

Close-up of the promo field at desktop after typing SAVE20 and pressing Apply: red-bordered "SAVE20" input, outlined "Apply" button of the same 48px height, and the error on ONE line with its (!) icon: "“SAVE20” isn’t a valid promo code. Check the spelling." — no stranded word. The hint above still names WELCOME10, so the customer has a way forward. Colour is not the only cue: icon, words and the thicker red border all say "rejected".

S14

promo WELCOME10 accepted (field + summary)

index.html?step=3 · 1280×800 · fill #promo "welcome10" · click #promo-apply · scroll-to #step-3 .promo

1 screen(s), looked at when taken; not kept once the round was done — a round keeps one full-page walk per width.

L14 saw

Desktop, step 3 after typing "welcome10" and pressing Apply, scrolled to the promo field. The code was normalised to "WELCOME10", the input turned read-only (warm off-white fill), the button now says "Remove", and a green line with a check-circle icon reads "WELCOME10 applied: 10% off your first box (−$5.99)." The sticky summary picked it up: a new row "Promo [WELCOME10 tag] −$5.99" in green between Subtotal $59.94 and Shipping Free, Tax (8%) dropped to $4.32, "Total today $58.27", and the renewal note still says "Then $64.74 a week" — the discount is honestly first-box only. Arithmetic checks: 59.94 − 5.99 = 53.95; 8% = 4.32; 53.95 + 4.32 = 58.27. Card fields are empty here (only the promo was driven), billing tile checked with the address preview.

S15

promo WELCOME10 accepted (field + summary)

index.html?step=3 · 375×812 · fill #promo "welcome10" · click #promo-apply · scroll-to #step-3 .promo

1 screen(s), looked at when taken; not kept once the round was done — a round keeps one full-page walk per width.

L15 saw

Mobile 375, the same promo-accepted state, one screen around the promo. Fields stack full width; the billing tile wraps its label to "Billing address is the same as / delivery" with the tick box vertically centred — fine. The address preview under it wraps as "1450 Alder Street, Apt 3, Portland, OR / 97214", leaving the ZIP code alone on the second line, split from its city and state. Promo row: "WELCOME10" read-only input and "Remove" button side by side at equal 48px height; the success line wraps into two clean lines "WELCOME10 applied: 10% off / your first box (−$5.99)." with a green check icon. No overflow.

D11 defect #billing-preview (and the confirmation's Delivering to)

At 375 the one-line delivery address wraps as '…Portland, OR / 97214', stranding the ZIP code from its city and state; the address parts are joined as one string, so the browser breaks at the last space.

V10verified D10 by L13

Rejected-code message is one line.

F12fix for D11

The address is rendered as no-wrap parts ('1450 Alder Street', 'Apt 3', 'Portland, OR 97214') joined by ', ', so it can only wrap between parts and the ZIP stays with its city and state; the billing preview and the confirmation's 'Delivering to' share the renderer.

S16

mobile billing preview after D11

index.html?step=3 · 375×812 · element #step-3 fieldset:nth-of-type(2) · scroll-to #billing-preview

1 screen(s), looked at when taken; not kept once the round was done — a round keeps one full-page walk per width.

L16 saw

Close-up at 375 of step 3's Billing address group: the checked tile "Billing address is the same as / delivery" (green border, mint fill, white tick) and under it the preview now breaks between address parts — "1450 Alder Street, Apt 3," on the first line and "Portland, OR 97214" together on the second. The ZIP stays with its city and state; grey 14px text, left edge on the same rail as the tile.

V11verified D11 by L16

The preview wraps between parts: 'Portland, OR 97214' stays together.

S17

step 3: billing fields revealed

index.html?step=3 · 1280×800 · full page, 2 screen(s) · click .choice:has(#billing-same)

2 screen(s), looked at when taken; not kept once the round was done — a round keeps one full-page walk per width.

L17 saw

Desktop, step 3 after one click on the "same as delivery" tile (2 screens). Screen 1: tracker at step 3, Payment h1, Card details group, empty card fields. Screen 2: the tile is now unticked (white fill, grey border, empty box) and the billing fields appeared under it: Billing address line 1, Billing address line 2 (optional), and a City / State (Select) / ZIP code row — same field styling as step 2, aligned on the same rails. The reveal itself works. Problem: the hierarchy breaks once the fields are open. "Card details" and "Billing address" are 18px section headings, but "Promo code (optional)" is a 16px bold field label exactly like "City" or "ZIP code", and it follows the ZIP row with only ~40px of space, so the promo input reads as one more billing-address field rather than its own section. Back / Continue below, summary pinned at right with $64.74.

D12 defect #step-3 .promo label

With the billing fields revealed, 'Promo code (optional)' is styled as an ordinary 16px field label and follows the billing ZIP row directly, so it reads as part of the billing address; the page's other sections ('Card details', 'Billing address') have 18px headings and this one does not.

F13fix for D12

The promo label now carries the section-heading style (18px/700, like the 'Card details' and 'Billing address' legends), so Promo code reads as the third section of the step rather than a billing field; '(optional)' stays grey regular as in 'Dietary preferences (optional)'.

S18

billing revealed, promo as its own section

index.html?step=3 · 1280×800 · click .choice:has(#billing-same) · scroll-to #step-3 .promo

1 screen(s), looked at when taken; not kept once the round was done — a round keeps one full-page walk per width.

L18 saw

Desktop, step 3 with billing revealed, scrolled to the lower half. "Billing address" (18px heading, top edge) → unticked tile → Billing address line 1 / line 2 (optional) / City-State-ZIP, all 16px bold labels with 22px between fields. Then, ~40px below the ZIP row, "Promo code (optional)" now renders at the 18px section-heading size with "(optional)" in grey regular, followed by its hint, the input and "Apply". It reads as the third section of the step, a peer of Card details and Billing address, not as a sixth billing field. Hairline, Back / Continue; summary unchanged at $64.74.

V12verified D12 by L18

Promo code heading now matches the section-heading size; the billing block ends at the ZIP row.

S19

step 4 review (sample customer)

index.html?step=4 · 1280×800 · full page, 2 screen(s)

2 screen(s), looked at when taken; not kept once the round was done — a round keeps one full-page walk per width.

L19 saw

Desktop, step 4 opened with ?step=4 (sample customer), 2 screens. Tracker: Plan, Delivery, Payment done with checks and an all-green connector; Review current. "Review your order" + "Check the details, then place your order." Three warm off-white blocks with 18px headings and a right-aligned underlined green "Edit": Plan (Box "2 people · 3 meals a week, $59.94 a week", Delivery day "Tuesdays, first box Tuesday, October 13"), Delivery (Name, Contact, a three-line Address "1450 Alder Street / Apt 3 / Portland, OR 97214", Instructions) and Payment ("Card ending 4242, expires 12/28 / Jordan Rivera", Billing "Same as delivery address", Promo "None"). Grey term column, ink values, even 8px row gaps. Flaw: the 150px term column is too narrow for "Dietary preferences", which wraps to "Dietary / preferences" — the only two-line label, so the Plan block's rows go uneven. Screen 2: terms tile (unticked, grey border) "I agree to the subscription terms: my box renews every week at $64.74 until I skip or cancel." with the price bold; Back + green "Place order". Summary pinned with Total today $64.74.

D13 defect .review__row dt (desktop)

The review's 150px term column wraps 'Dietary preferences' to two lines at desktop, the only wrapped term, so the Plan block's row rhythm breaks.

S20

step 4 review (sample customer)

index.html?step=4 · 375×812 · full page, 3 screen(s)

3 screen(s), looked at when taken; not kept once the round was done — a round keeps one full-page walk per width.

L20 saw

Mobile 375, step 4 review, 3 screens. Summary toggle bar on top, tracker with three green checks and Review current. The review blocks stack term above value (grey 16px term, ink value), so "Dietary preferences" fits on its line here; values wrap cleanly ("2 people · 3 meals a week, / $59.94 a week", "Tuesdays, first box Tuesday, / October 13"). Each block's "Edit" sits right of its heading with a 44px-tall hit area. The terms tile wraps to four lines with the checkbox aligned to the first. Back and a wider "Place order" side by side at the bottom, both 52px. Nothing clipped; nested padding (16px card + 20px block) leaves ~267px of text width, which is enough.

F14fix for D13

Term column 150 -> 168px and terms set at 15px (line-height 1.6, so each term still sits on its value's 24px line box); 'Dietary preferences' fits on one line and the longest value still has ~13px to spare.

S21

review Plan block after D13

index.html?step=4 · 1280×800 · element #step-4 .review__block:first-child

1 screen(s), looked at when taken; not kept once the round was done — a round keeps one full-page walk per width.

L21 saw

Close-up of the review's Plan block at desktop after D13: three rows on one line each — "Box: 2 people · 3 meals a week, $59.94 a week", "Dietary preferences: No preferences", "Delivery day: Tuesdays, first box Tuesday, October 13". The grey 15px terms fit the 168px column and share a baseline with their 16px values; the longest value ends ~10px short of the block's padding. "Edit" stays right-aligned beside the "Plan" heading. Even row rhythm restored.

V13verified D13 by L21

'Dietary preferences' is on one line; all three rows align.

S22

step 4: Place order without the terms

index.html?step=4 · 1280×800 · click #step-4 button[type=submit]

1 screen(s), looked at when taken; not kept once the round was done — a round keeps one full-page walk per width.

L22 saw

Desktop viewport after pressing "Place order" on step 4 without ticking the terms. The page scrolled up to the error summary (its top 16px below the viewport edge, heading scrolled off above): pale pink panel, red left bar, dark-green focus ring, "(!) 1 field needs your attention" and one underlined link "Tick the box to agree to the subscription terms". Below it the three review blocks, unchanged (the Plan block now keeps "Dietary preferences" on one line). The terms tile itself is below the fold here, so its own error state is not visible in this frame; the summary link is the way back down to it.

S23

confirmation after Place order

index.html?step=4 · 1280×800 · full page, 2 screen(s) · click .choice:has(#terms) · click #step-4 button[type=submit]

S23 screen 1S23 screen 2
L23 saw

Desktop confirmation after ticking the terms and pressing Place order (2 screens; the page is only 820px tall). The tracker is gone and the step card is replaced by a centred confirmation card: a 56px green check-circle, "Thanks — your first box is on its way." as a bold h1 on one line, the lead "Your weekly plan is set up. Here’s what we have on file.", then a bordered 480px facts table — Order number PC-20417, First delivery Tuesday, October 13, 2026, Plan 2 people · 3 meals a week, Delivering to "1450 Alder Street, Apt 3, / Portland, OR 97214" — grey terms, bold values, hairlines between rows. A small grey note: "A real order would send a receipt to jordan@example.com. This demo sent nothing and charged nothing." The summary stays at right (Total today $64.74), card tops aligned at y=143. Calm, clearly finished; nothing clipped.

S24

confirmation after Place order

index.html?step=4 · 375×812 · full page, 2 screen(s) · click .choice:has(#terms) · click #step-4 button[type=submit]

2 screen(s), looked at when taken; not kept once the round was done — a round keeps one full-page walk per width.

L24 saw

Mobile 375 confirmation, 2 screens — broken. The facts table keeps its desktop 140px term column, so values get ~110px: "Tuesday, / October 13, / 2026" and "2 people · 3 / meals a / week" wrap into three-line stacks. Worse, the address overflows: "1450 Alder Street," runs straight through the table's right border to the screen edge, and the last line "Portland, OR 9721…" is cut off at the viewport edge — the ZIP is not readable. That comes from D11's fix: each address part is white-space:nowrap, and a part wider than its column can only overflow. The heading wraps as "Thanks — your first / box is on its way." (fine, centred), the check icon and the note below are fine.

D14 defect .confirmation__facts @375 / .nw address parts

On a phone the confirmation's facts table keeps the 140px term column, leaving ~110px for values: dates and plan wrap into three-line stacks, and the no-wrap address parts introduced by F12 overflow the table and are clipped at the screen edge ('Portland, OR 9721…'). Regression from the D11 fix, which assumed every address part fits its container.

F15fix for D14

Two changes. (1) The keep-together phrases are now inline-blocks carrying their own trailing separator ('1450 Alder Street,' 'Apt 3,' 'Portland, OR 97214'; '2 people ·' '3 meals a week'), so a line still breaks only between phrases when they fit, but a phrase wider than its box wraps inside itself instead of overflowing. (2) Below 480px the confirmation's term/value rows stack like the review rows, giving the values the full ~275px.

S25

mobile confirmation after D14

index.html?step=done · 375×812 · full page, 2 screen(s)

2 screen(s), looked at when taken; not kept once the round was done — a round keeps one full-page walk per width.

L25 saw

Mobile 375 confirmation (?step=done) after D14, 2 screens. The facts table now stacks each term above its value: "Order number / PC-20417", "First delivery / Tuesday, October 13, 2026" on one line, "Plan / 2 people · 3 meals a week" on one line, "Delivering to / 1450 Alder Street, Apt 3, / Portland, OR 97214" — two clean lines, the ZIP with its city and state, everything well inside the table's rounded border with ~18px to spare. Hairlines between rows, grey terms, bold values. Heading, check icon and the "this demo sent nothing" note unchanged. No overflow, nothing clipped.

V14verified D14 by L25

Values are full width on phones; the address wraps between parts and stays inside the table.

G26exit 0vlmkit check integrity 'http://127.0.0.1:48217/index.html?step=done'

verdict: CLEAN (0 fail, 0 warn, 0 exempted)

verdict clean · findings 0 · fails 0 · warns 0 · exempted 0 · viewports 3

output, 10 line(s), 3.9s
vlmkit check integrity
source: http://127.0.0.1:48217/index.html?step=done

verdict: CLEAN (0 fail, 0 warn, 0 exempted)
  1280x800: 8 component(s), ink 94.2%, 34 text block(s)
  768x900: 8 component(s), ink 92.9%, 18 text block(s)
  375x700: 8 component(s), ink 92.6%, 18 text block(s)

No integrity defects detected.
  http://127.0.0.1:48217/index.html?step=done is live and not pinned — a re-run may measure different data. Pin it: vlmkit snapshot record-har http://127.0.0.1:48217/index.html?step=done --out app.har, then --har app.har
N14decision

Probe for the eye-vs-gate question on D14: a scratch copy of the site OUTSIDE the site dir with only the D14 fix reverted (address parts back to white-space:nowrap, the phone stacking rule removed), served on 127.0.0.1:48218. The next check integrity run is against that copy, not the site; it asks whether the gate would have caught the confirmation overflow had it been able to reach the state.

G27exit 1vlmkit check integrity 'http://127.0.0.1:48218/index.html?step=done'

verdict: DEFECTS (1 fail, 0 warn, 0 exempted)

verdict defects · findings 1 · fails 1 · warns 0 · exempted 0 · viewports 3

output, 11 line(s), 4.0s
vlmkit check integrity
source: http://127.0.0.1:48218/index.html?step=done

verdict: DEFECTS (1 fail, 0 warn, 0 exempted)
  1280x800: 8 component(s), ink 94.2%, 30 text block(s)
  768x900: 8 component(s), ink 92.9%, 16 text block(s)
  375x700: 8 component(s), ink 71.7%, 16 text block(s)

Findings:
  x [page-overflow-x] @375: The page scrolls horizontally by 9px at 375px viewport width — sticking out: dl.confirmation__facts > div:nth-of-type(4) > dd:nth-of-type(1) > span:nth-of-type(3) (right edge 384px).
  http://127.0.0.1:48218/index.html?step=done is live and not pinned — a re-run may measure different data. Pin it: vlmkit snapshot record-har http://127.0.0.1:48218/index.html?step=done --out app.har, then --har app.har
N15supersededabout G27

Probe run against the reverted scratch copy (N14), not the site; the site itself is clean in the same state (G26). Answer to the question: check integrity WOULD have caught half of D14 — '[page-overflow-x] @375: The page scrolls horizontally by 9px … sticking out: dl.confirmation__facts > div:nth-of-type(4) > dd:nth-of-type(1) > span:nth-of-type(3) (right edge 384px)', i.e. the clipped 'Portland, OR 97214'. It did not report '1450 Alder Street,' crossing the table's own border (no container-protrusion row), nor the three-line value stacks from the 140px term column, which only the eye saw in S24. In round 3 it could not reach the state at all until the ?step=done deep link and the local server existed.

S26

phone step 3 with the order summary expanded

index.html?step=3 · 375×812 · full page, 3 screen(s) · click #summary-toggle

S26 screen 1S26 screen 2S26 screen 3
L26 saw

Mobile 375, step 3 (?step=3) after tapping the summary bar, 3 screens. Screen 1: the bar now reads "Hide order summary" with the chevron flipped up, total $64.74 still at the right; the full-bleed panel opened directly below it on the warm off-white surface: "Order summary", the box illustration with "2 people · 3 meals a week / 6 servings · $9.99 each", Dietary preferences None, First delivery Tue, Oct 13, Subtotal $59.94, Shipping Free (green), Tax (8%) $4.80, a bold "Total today $64.74" and the renewal note. Its bottom edge meets the grey page cleanly, and the tracker (two checks, Payment current) and the Payment card follow. Screens 2-3: on the phone the card fields stack full width (Expiry and CVC each on their own row), then the checked billing tile with the two-line address preview, the 18px "Promo code (optional)" section with input + Apply, and Back / Continue. Everything within the 16px gutters.

S27

terms tile error state

index.html?step=4 · 1280×800 · element #step-4 .field--terms · click #step-4 button[type=submit]

1 screen(s), looked at when taken; not kept once the round was done — a round keeps one full-page walk per width.

L27 saw

Close-up of step 4's terms tile after "Place order" was pressed unticked: the tile's edge turned 2px red with a pale pink fill and the tick box outlined red, and directly beneath it a bold red line with the (!) icon: "Tick the box to agree to the subscription terms". The agreement text (with the bold $64.74 renewal price) stays fully legible in ink on the tint. Icon, words and edge all carry the error, not colour alone.

S28

keyboard: Tab then ArrowDown on the plan cards

index.html · 1280×800 · element #step-1 .plans · @2x · press Tab · press ArrowDown

1 screen(s), looked at when taken; not kept once the round was done — a round keeps one full-page walk per width.

L28 saw

2x close-up of the three plan cards after Tab (focus lands on the checked radio) and ArrowDown. Selection and focus moved together to the 4-person card, as native radios do: it now has the 2px green border, mint fill and filled radio dot, and a separate 3px dark-green focus ring 3px outside the card, so "selected" and "focused" read as two different signals. Card 1 went back to a grey border with an empty radio. The "Best value" pill sits on top of the ring's upper edge and stays fully legible (white on green). The ring's far left/right are cut only by this crop's edge. Price column right-aligned, tabular figures, even 18px gaps.

G28exit 0vlmkit check color 'http://127.0.0.1:48217/index.html?step=3'

verdict: CONSISTENT (0 finding(s))

verdict consistent · base #f6f3ee · bodyInk #1f1c19 · linkInk #ffffff · invisibleControls 0 · colorOnlyLinks 0 · unreadableColors 0

output, 27 line(s), 1.6s
vlmkit check color
source: http://127.0.0.1:48217/index.html?step=3

verdict: CONSISTENT (0 finding(s))
  measured: 5 control(s), 0 link(s) in a text flow — from 116 visible box(es)

Palette (by painted area; reported, never judged — see the rejected candidates in the module docs)
  surfaces  4 distinct
    #f6f3ee   60.9%     1 el  
    #ffffff   35.9%    12 el  header.site-header>div.brand-bar
    #f4ecdc    2.1%     2 el  header.site-header>div.demo-notice
    #edf6f0    1.1%     1 el  input#billing-same
  ink       4 distinct
    #1f1c19   60.9%    31 el  div.demo-notice>div.wrap>p
    #6a635b   30.8%    15 el  div.summary-item__text>p.summary-item__meta>span.nw
    #57514a    4.9%     2 el  li.steps__item>span.steps__marker>span.steps__num
    #ffffff    3.3%     1 el  form#step-3>div.actions>button.btn
  marks     5 distinct
    #dcd5ca   38.9%    10 el  header.site-header>div.brand-bar
    #857d73   28.2%    32 el  ol#steps>li.steps__item>span.steps__marker
    #2d6a4f   18.2%    20 el  ol#steps>li.steps__item>span.steps__marker
    #e7e1d8    7.9%     4 el  div#summary-panel>div.summary-item
    #e6d9bf    6.7%     1 el  header.site-header>div.demo-notice
  base #f6f3ee, body ink #1f1c19, link ink #ffffff on 1 element(s)

  http://127.0.0.1:48217/index.html?step=3 is live and not pinned — a re-run may measure different data. Pin it: vlmkit snapshot record-har http://127.0.0.1:48217/index.html?step=3 --out app.har, then --har app.har
G29exit 0vlmkit check a11y contrast 'http://127.0.0.1:48217/index.html?step=2'

inspected 50 · failures 0

output, 6 line(s), 1.5s
  vlmkit check a11y contrast
  html: http://127.0.0.1:48217/index.html?step=2
  inspected 50 text-bearing element(s)
  ✓ 0 contrast failure(s)
  report: examples/sites/checkout/test-results/a11y-contrast/index-0d8f1101/report.md
  http://127.0.0.1:48217/index.html?step=2 is live and not pinned — a re-run may measure different data. Pin it: vlmkit snapshot record-har http://127.0.0.1:48217/index.html?step=2 --out app.har, then --har app.har
G30exit 0vlmkit check a11y contrast 'http://127.0.0.1:48217/index.html?step=3'

inspected 51 · failures 0

output, 6 line(s), 1.4s
  vlmkit check a11y contrast
  html: http://127.0.0.1:48217/index.html?step=3
  inspected 51 text-bearing element(s)
  ✓ 0 contrast failure(s)
  report: examples/sites/checkout/test-results/a11y-contrast/index-19231d1c/report.md
  http://127.0.0.1:48217/index.html?step=3 is live and not pinned — a re-run may measure different data. Pin it: vlmkit snapshot record-har http://127.0.0.1:48217/index.html?step=3 --out app.har, then --har app.har
G31exit 0vlmkit check a11y contrast 'http://127.0.0.1:48217/index.html?step=4'

inspected 69 · failures 0

output, 6 line(s), 1.4s
  vlmkit check a11y contrast
  html: http://127.0.0.1:48217/index.html?step=4
  inspected 69 text-bearing element(s)
  ✓ 0 contrast failure(s)
  report: examples/sites/checkout/test-results/a11y-contrast/index-3caffc24/report.md
  http://127.0.0.1:48217/index.html?step=4 is live and not pinned — a re-run may measure different data. Pin it: vlmkit snapshot record-har http://127.0.0.1:48217/index.html?step=4 --out app.har, then --har app.har
G32exit 0vlmkit check a11y contrast 'http://127.0.0.1:48217/index.html?step=done'

inspected 37 · failures 0

output, 6 line(s), 1.4s
  vlmkit check a11y contrast
  html: http://127.0.0.1:48217/index.html?step=done
  inspected 37 text-bearing element(s)
  ✓ 0 contrast failure(s)
  report: examples/sites/checkout/test-results/a11y-contrast/index-628ce05f/report.md
  http://127.0.0.1:48217/index.html?step=done is live and not pinned — a re-run may measure different data. Pin it: vlmkit snapshot record-har http://127.0.0.1:48217/index.html?step=done --out app.har, then --har app.har
G33exit 0vlmkit check a11y touch 'http://127.0.0.1:48217/index.html?step=3'

level AA · inspected 9 · failures 0 · wcagExempt 0

output, 6 line(s), 1.4s
  vlmkit check a11y touch
  source: http://127.0.0.1:48217/index.html?step=3  level: WCAG AA (24×24 min)
  inspected 9 interactive element(s)
  ✓ 0 undersized target(s)
  report: examples/sites/checkout/test-results/a11y-touch/index-19231d1c/report.md
  http://127.0.0.1:48217/index.html?step=3 is live and not pinned — a re-run may measure different data. Pin it: vlmkit snapshot record-har http://127.0.0.1:48217/index.html?step=3 --out app.har, then --har app.har
G34exit 0vlmkit check a11y touch 'http://127.0.0.1:48217/index.html?step=4'

level AA · inspected 6 · failures 0 · wcagExempt 0

output, 6 line(s), 1.5s
  vlmkit check a11y touch
  source: http://127.0.0.1:48217/index.html?step=4  level: WCAG AA (24×24 min)
  inspected 6 interactive element(s)
  ✓ 0 undersized target(s)
  report: examples/sites/checkout/test-results/a11y-touch/index-3caffc24/report.md
  http://127.0.0.1:48217/index.html?step=4 is live and not pinned — a re-run may measure different data. Pin it: vlmkit snapshot record-har http://127.0.0.1:48217/index.html?step=4 --out app.har, then --har app.har
G35exit 0vlmkit check interactions 'http://127.0.0.1:48217/index.html?step=3'

elements 9 · suspects 0 · warns 0

output, 16 line(s), 5.4s
vlmkit check interactions
source: http://127.0.0.1:48217/index.html?step=3

status: ok
interactive elements: 9

  - [textbox] "" focus✓
  - [textbox] "" focus✓
  - [textbox] "" focus✓
  - [textbox] "" focus✓
  - [checkbox] "" focus✓ | Space: layout change
  - [textbox] "" focus✓
  - [button] "Apply" focus✓ | Enter: layout change | announces
  - [button] "Back" focus✓ | Enter: focus moves within
  - [button] "Continue" focus✓ | Enter: focus moves within
  http://127.0.0.1:48217/index.html?step=3 is live and not pinned — a re-run may measure different data. Pin it: vlmkit snapshot record-har http://127.0.0.1:48217/index.html?step=3 --out app.har, then --har app.har
N16toolabout G35

check interactions at step 3 lists four '[textbox] ""' rows, '[checkbox] ""' and a fifth '[textbox] ""', i.e. empty names, although every control has a <label>. Chromium's own aria snapshot of the same URL gives textbox "Card number", "Expiry date", "Security code (CVC)", "Name on card", checkbox "Billing address is the same as delivery" and textbox "Promo code (optional)". Same pattern as N7 (radios shown by value): the name column is not the accessible name, so the inventory cannot be matched against a brief's control list by name.

G36exit 0vlmkit check interactions 'http://127.0.0.1:48217/index.html?step=4'

elements 6 · suspects 0 · warns 1

output, 17 line(s), 4.4s
vlmkit check interactions
source: http://127.0.0.1:48217/index.html?step=4

status: ok
  exits 0 — 1 warn(s) did not fail this command. To gate on one: --rule inert-control=suspect
interactive elements: 6

  - [link] "Edit plan" focus✓
  - [link] "Edit delivery details" focus✓
  - [link] "Edit payment" focus✓
  - [checkbox] "" focus✓ | Space: no response
  - [button] "Back" focus✓ | Enter: focus moves within
  - [button] "Place order" focus✓ | Enter: focus moves within

Issues:
  warn [inert-control] checkbox "" (form#step-4>div.field>label.choice>input#terms) shows no observable response to Space — no ARIA change, no layout change. Dead control, or its response is outside this probe.
  http://127.0.0.1:48217/index.html?step=4 is live and not pinned — a re-run may measure different data. Pin it: vlmkit snapshot record-har http://127.0.0.1:48217/index.html?step=4 --out app.har, then --har app.har
G37exit 0vlmkit check integrity 'http://127.0.0.1:48217/index.html?step=2'

verdict: CLEAN (0 fail, 0 warn, 4 exempted)

verdict clean · findings 0 · fails 0 · warns 0 · exempted 4 · viewports 3

output, 16 line(s), 4.1s
vlmkit check integrity
source: http://127.0.0.1:48217/index.html?step=2

verdict: CLEAN (0 fail, 0 warn, 4 exempted)
  1280x800: 8 component(s), ink 96.7%, 46 text block(s)
  768x900: 8 component(s), ink 95.7%, 30 text block(s)
  375x700: 8 component(s), ink 94.9%, 30 text block(s)

No integrity defects detected.

Exempted candidates (the tool's call — audit the rule, not the page):
  - [text-clipped] li.steps__item.is-done > span:nth-of-type(2) > span:nth-of-type(1) @1280: visually-hidden (sr-only) pattern — 1px box, text for AT only
  - [text-clipped] li.steps__item.is-done > span:nth-of-type(2) > span:nth-of-type(1) @768: visually-hidden (sr-only) pattern — 1px box, text for AT only
  - [text-collision] li.steps__item.is-done > span:nth-of-type(2) > span:nth-of-type(1) @375: clipped away by li.steps__item.is-done > span:nth-of-type(2) > span:nth-of-type(1) (overflow is not visible) — its box still overlaps text below the clip, but no glyph is painted there
  - [text-clipped] li.steps__item.is-done > span:nth-of-type(2) > span:nth-of-type(1) @375: visually-hidden (sr-only) pattern — 1px box, text for AT only
  http://127.0.0.1:48217/index.html?step=2 is live and not pinned — a re-run may measure different data. Pin it: vlmkit snapshot record-har http://127.0.0.1:48217/index.html?step=2 --out app.har, then --har app.har
G38exit 0vlmkit check integrity 'http://127.0.0.1:48217/index.html?step=3'

verdict: CLEAN (0 fail, 0 warn, 8 exempted)

verdict clean · findings 0 · fails 0 · warns 0 · exempted 8 · viewports 3

output, 20 line(s), 4.2s
vlmkit check integrity
source: http://127.0.0.1:48217/index.html?step=3

verdict: CLEAN (0 fail, 0 warn, 8 exempted)
  1280x800: 8 component(s), ink 96.3%, 49 text block(s)
  768x900: 8 component(s), ink 95.2%, 33 text block(s)
  375x700: 8 component(s), ink 94.6%, 33 text block(s)

No integrity defects detected.

Exempted candidates (the tool's call — audit the rule, not the page):
  - [text-clipped] #steps > li:nth-of-type(1) > span:nth-of-type(2) > span:nth-of-type(1) @1280: visually-hidden (sr-only) pattern — 1px box, text for AT only
  - [text-clipped] #steps > li:nth-of-type(2) > span:nth-of-type(2) > span:nth-of-type(1) @1280: visually-hidden (sr-only) pattern — 1px box, text for AT only
  - [text-clipped] #steps > li:nth-of-type(1) > span:nth-of-type(2) > span:nth-of-type(1) @768: visually-hidden (sr-only) pattern — 1px box, text for AT only
  - [text-clipped] #steps > li:nth-of-type(2) > span:nth-of-type(2) > span:nth-of-type(1) @768: visually-hidden (sr-only) pattern — 1px box, text for AT only
  - [text-collision] #steps > li:nth-of-type(1) > span:nth-of-type(2) > span:nth-of-type(1) @375: clipped away by #steps > li:nth-of-type(1) > span:nth-of-type(2) > span:nth-of-type(1) (overflow is not visible) — its box still overlaps text below the clip, but no glyph is painted there
  - [text-collision] #steps > li:nth-of-type(2) > span:nth-of-type(2) > span:nth-of-type(1) @375: clipped away by #steps > li:nth-of-type(2) > span:nth-of-type(2) > span:nth-of-type(1) (overflow is not visible) — its box still overlaps text below the clip, but no glyph is painted there
  - [text-clipped] #steps > li:nth-of-type(1) > span:nth-of-type(2) > span:nth-of-type(1) @375: visually-hidden (sr-only) pattern — 1px box, text for AT only
  - [text-clipped] #steps > li:nth-of-type(2) > span:nth-of-type(2) > span:nth-of-type(1) @375: visually-hidden (sr-only) pattern — 1px box, text for AT only
  http://127.0.0.1:48217/index.html?step=3 is live and not pinned — a re-run may measure different data. Pin it: vlmkit snapshot record-har http://127.0.0.1:48217/index.html?step=3 --out app.har, then --har app.har
G39exit 0vlmkit check integrity 'http://127.0.0.1:48217/index.html?step=4'

verdict: CLEAN (0 fail, 0 warn, 30 exempted)

verdict clean · findings 0 · fails 0 · warns 0 · exempted 30 · viewports 3

output, 28 line(s), 4.4s
vlmkit check integrity
source: http://127.0.0.1:48217/index.html?step=4

verdict: CLEAN (0 fail, 0 warn, 30 exempted)
  1280x800: 8 component(s), ink 96.6%, 63 text block(s)
  768x900: 8 component(s), ink 95.6%, 47 text block(s)
  375x700: 8 component(s), ink 95.3%, 47 text block(s)

No integrity defects detected.

Exempted candidates (the tool's call — audit the rule, not the page):
  - [text-collision] div.review > section:nth-of-type(1) > div:nth-of-type(1) > a:nth-of-type(1) > span:nth-of-type(1) @1280: clipped away by div.review > section:nth-of-type(1) > div:nth-of-type(1) > a:nth-of-type(1) > span:nth-of-type(1) (overflow is not visible) — its box still overlaps text below the clip, but no glyph is painted there
  - [text-collision] div.review > section:nth-of-type(2) > div:nth-of-type(1) > a:nth-of-type(1) > span:nth-of-type(1) @1280: clipped away by div.review > section:nth-of-type(2) > div:nth-of-type(1) > a:nth-of-type(1) > span:nth-of-type(1) (overflow is not visible) — its box still overlaps text below the clip, but no glyph is painted there
  - [text-collision] div.review > section:nth-of-type(3) > div:nth-of-type(1) > a:nth-of-type(1) > span:nth-of-type(1) @1280: clipped away by div.review > section:nth-of-type(3) > div:nth-of-type(1) > a:nth-of-type(1) > span:nth-of-type(1) (overflow is not visible) — its box still overlaps text below the clip, but no glyph is painted there
  - [text-clipped] #steps > li:nth-of-type(1) > span:nth-of-type(2) > span:nth-of-type(1) @1280: visually-hidden (sr-only) pattern — 1px box, text for AT only
  - [text-clipped] #steps > li:nth-of-type(2) > span:nth-of-type(2) > span:nth-of-type(1) @1280: visually-hidden (sr-only) pattern — 1px box, text for AT only
  - [text-clipped] #steps > li:nth-of-type(3) > span:nth-of-type(2) > span:nth-of-type(1) @1280: visually-hidden (sr-only) pattern — 1px box, text for AT only
  - [text-clipped] div.review > section:nth-of-type(1) > div:nth-of-type(1) > a:nth-of-type(1) > span:nth-of-type(1) @1280: visually-hidden (sr-only) pattern — 1px box, text for AT only
  - [text-clipped] div.review > section:nth-of-type(2) > div:nth-of-type(1) > a:nth-of-type(1) > span:nth-of-type(1) @1280: visually-hidden (sr-only) pattern — 1px box, text for AT only
  - [text-clipped] div.review > section:nth-of-type(3) > div:nth-of-type(1) > a:nth-of-type(1) > span:nth-of-type(1) @1280: visually-hidden (sr-only) pattern — 1px box, text for AT only
  - [text-collision] div.review > section:nth-of-type(1) > div:nth-of-type(1) > a:nth-of-type(1) > span:nth-of-type(1) @768: clipped away by div.review > section:nth-of-type(1) > div:nth-of-type(1) > a:nth-of-type(1) > span:nth-of-type(1) (overflow is not visible) — its box still overlaps text below the clip, but no glyph is painted there
  - [text-collision] div.review > section:nth-of-type(2) > div:nth-of-type(1) > a:nth-of-type(1) > span:nth-of-type(1) @768: clipped away by div.review > section:nth-of-type(2) > div:nth-of-type(1) > a:nth-of-type(1) > span:nth-of-type(1) (overflow is not visible) — its box still overlaps text below the clip, but no glyph is painted there
  - [text-collision] div.review > section:nth-of-type(3) > div:nth-of-type(1) > a:nth-of-type(1) > span:nth-of-type(1) @768: clipped away by div.review > section:nth-of-type(3) > div:nth-of-type(1) > a:nth-of-type(1) > span:nth-of-type(1) (overflow is not visible) — its box still overlaps text below the clip, but no glyph is painted there
  - [text-clipped] #steps > li:nth-of-type(1) > span:nth-of-type(2) > span:nth-of-type(1) @768: visually-hidden (sr-only) pattern — 1px box, text for AT only
  - [text-clipped] #steps > li:nth-of-type(2) > span:nth-of-type(2) > span:nth-of-type(1) @768: visually-hidden (sr-only) pattern — 1px box, text for AT only
  - [text-clipped] #steps > li:nth-of-type(3) > span:nth-of-type(2) > span:nth-of-type(1) @768: visually-hidden (sr-only) pattern — 1px box, text for AT only
  … 15 more
  http://127.0.0.1:48217/index.html?step=4 is live and not pinned — a re-run may measure different data. Pin it: vlmkit snapshot record-har http://127.0.0.1:48217/index.html?step=4 --out app.har, then --har app.har
S29

terms checkbox after focus + Space (inert-control check)

index.html?step=4 · 1280×800 · element #step-4 .field--terms · focus #terms · press Space

1 screen(s), looked at when taken; not kept once the round was done — a round keeps one full-page walk per width.

L29 saw

Close-up of step 4's terms tile after focusing #terms and pressing Space once: the box is now solid green with a white tick, the tile's edge turned 2px green and its fill mint — the checked state, exactly as when it is clicked. (The focus ring sits 3px outside the tile, beyond this element crop.) So the control does respond to Space; the only thing that changes is the native checked state and its paint, with no layout shift, because nothing else on the page depends on the terms box until Place order is pressed.

N17false-positiveabout G36

warn [inert-control] says the terms checkbox 'shows no observable response to Space — no ARIA change, no layout change'. S29 (focus #terms + Space) shows it ticked: green box with a white tick, 2px green edge, mint fill. It is a native <input type=checkbox>; its checked state flips, which is the ARIA transition (checked false -> true), and verify flow G19 steps 31-33 depend on exactly that state (error stays until it is ticked, then Place order succeeds). The step-1 diet boxes and the step-3 billing box only pass this probe because ticking them also moves layout (summary line, revealed fields).

N18toolabout G36

check interactions' inert-control probe does not read the native checked property of an <input type=checkbox>: a checkbox whose only effect is its own checked state (a terms box) is reported 'no ARIA change, no layout change. Dead control, or its response is outside this probe.' Verbatim: 'warn [inert-control] checkbox "" (form#step-4>div.field>label.choice>input#terms) shows no observable response to Space — no ARIA change, no layout change.' The same run also shows the name as "" (see N16).

S30

tablet: step 2 layout (summary as bordered disclosure)

index.html?step=2 · 768×1024 · full page, 2 screen(s)

S30 screen 1S30 screen 2
L30 saw

Tablet 768x1024, step 2 via ?step=2, 2 screens. Single column: the summary is a bordered rounded bar "Show order summary v … $64.74" above the tracker and the step card; fields full width, Phone input 320px, City / State / ZIP on one row, Back / Continue at the bottom. Two faults. (1) Rails: the demo note, the logo and the footer text start at x=24, but the summary bar, tracker and card start at x=48 — the main column is capped at 720px and centred while the header and footer run the full width, so the page has two left edges 24px apart and the logo does not sit over the content. (2) The demo note wraps as "…nothing you type leaves this / page.", a one-word orphan on its second line in the very first thing on the page. Otherwise the card, hints and field rhythm match desktop.

D15 defect .wrap (header/notice/footer) vs main.checkout, 560-959px

At tablet widths the header, demo note and footer start at x=24 while the content column (capped at 720px and centred) starts at x=48, so the logo and footer do not share a left edge with the summary bar, tracker and card.

D16 defect .demo-notice p @768

At 768px the demo note wraps with 'page.' alone on its second line — an orphan in the first line of copy on the page.

F16fix for D15

Moved the 720px cap from main.checkout to every .wrap below 960px, so the demo note, header, main column and footer share one centred column and one left edge (x=48 at 768).

F17fix for D16

Running text (p, li, dd) now uses text-wrap: pretty, which avoids one-word last lines, and h1/h2 use text-wrap: balance; applies page-wide rather than rewording the one sentence.

S31

tablet after D15/D16 (rails, notice wrap)

index.html?step=2 · 768×1024

1 screen(s), looked at when taken; not kept once the round was done — a round keeps one full-page walk per width.

L31 saw

Tablet 768, step 2 after D15/D16 (first screen). One left rail now: the demo note's info icon, the Pantry Club mark, the summary bar, the tracker's column and the step card all start at x=48, and their right edges share x=720, so the header sits squarely over the content. The note wraps as "This is a demo. No payment is taken. Pantry Club is fictional, and nothing you type / leaves this page." — a balanced two-line block with no orphan. Tracker, card and fields are unchanged from S30.

V15verified D15 by L31

Note, header, summary bar and card share the x=48 rail at 768.

V16verified D16 by L31

The note's second line is 'leaves this page.', not a lone 'page.'.

R4 · final check

builder
G40exit 0vlmkit check integrity index.html

verdict: CLEAN (0 fail, 0 warn, 45 exempted)

verdict clean · findings 0 · fails 0 · warns 0 · exempted 45 · viewports 3

output, 27 line(s), 4.3s
vlmkit check integrity
source: index.html

verdict: CLEAN (0 fail, 0 warn, 45 exempted)
  1280x800: 8 component(s), ink 96.3%, 67 text block(s)
  768x900: 8 component(s), ink 95.3%, 51 text block(s)
  375x700: 8 component(s), ink 94.9%, 54 text block(s)

No integrity defects detected.

Exempted candidates (the tool's call — audit the rule, not the page):
  - [text-collision] div.plans > label:nth-of-type(1) > span:nth-of-type(1) > span:nth-of-type(2) > span:nth-of-type(3) @1280: clipped away by div.plans > label:nth-of-type(1) > span:nth-of-type(1) > span:nth-of-type(2) > span:nth-of-type(3) (overflow is not visible) — its box still overlaps text below the clip, but no glyph is painted there
  - [text-collision] div.plans > label:nth-of-type(2) > span:nth-of-type(1) > span:nth-of-type(2) > span:nth-of-type(3) @1280: clipped away by div.plans > label:nth-of-type(2) > span:nth-of-type(1) > span:nth-of-type(2) > span:nth-of-type(3) (overflow is not visible) — its box still overlaps text below the clip, but no glyph is painted there
  - [text-collision] div.plans > label:nth-of-type(3) > span:nth-of-type(1) > span:nth-of-type(2) > span:nth-of-type(3) @1280: clipped away by div.plans > label:nth-of-type(3) > span:nth-of-type(1) > span:nth-of-type(2) > span:nth-of-type(3) (overflow is not visible) — its box still overlaps text below the clip, but no glyph is painted there
  - [text-clipped] div.plans > label:nth-of-type(1) > span:nth-of-type(1) > span:nth-of-type(2) > span:nth-of-type(3) @1280: visually-hidden (sr-only) pattern — 1px box, text for AT only
  - [text-clipped] div.plans > label:nth-of-type(1) > span:nth-of-type(1) > span:nth-of-type(3) > span:nth-of-type(1) @1280: visually-hidden (sr-only) pattern — 1px box, text for AT only
  - [text-clipped] div.plans > label:nth-of-type(1) > span:nth-of-type(1) > span:nth-of-type(3) > span:nth-of-type(3) @1280: visually-hidden (sr-only) pattern — 1px box, text for AT only
  - [text-clipped] div.plans > label:nth-of-type(1) > span:nth-of-type(1) > span:nth-of-type(4) > span:nth-of-type(1) @1280: visually-hidden (sr-only) pattern — 1px box, text for AT only
  - [text-clipped] div.plans > label:nth-of-type(2) > span:nth-of-type(1) > span:nth-of-type(2) > span:nth-of-type(3) @1280: visually-hidden (sr-only) pattern — 1px box, text for AT only
  - [text-clipped] div.plans > label:nth-of-type(2) > span:nth-of-type(1) > span:nth-of-type(3) > span:nth-of-type(1) @1280: visually-hidden (sr-only) pattern — 1px box, text for AT only
  - [text-clipped] div.plans > label:nth-of-type(2) > span:nth-of-type(1) > span:nth-of-type(3) > span:nth-of-type(3) @1280: visually-hidden (sr-only) pattern — 1px box, text for AT only
  - [text-clipped] div.plans > label:nth-of-type(2) > span:nth-of-type(1) > span:nth-of-type(4) > span:nth-of-type(1) @1280: visually-hidden (sr-only) pattern — 1px box, text for AT only
  - [text-clipped] span.plan__badge > span:nth-of-type(1) @1280: visually-hidden (sr-only) pattern — 1px box, text for AT only
  - [text-clipped] div.plans > label:nth-of-type(3) > span:nth-of-type(1) > span:nth-of-type(2) > span:nth-of-type(3) @1280: visually-hidden (sr-only) pattern — 1px box, text for AT only
  - [text-clipped] div.plans > label:nth-of-type(3) > span:nth-of-type(1) > span:nth-of-type(3) > span:nth-of-type(1) @1280: visually-hidden (sr-only) pattern — 1px box, text for AT only
  - [text-clipped] div.plans > label:nth-of-type(3) > span:nth-of-type(1) > span:nth-of-type(3) > span:nth-of-type(3) @1280: visually-hidden (sr-only) pattern — 1px box, text for AT only
  … 30 more
G41exit 1vlmkit check copy index.html --manifest copy.txt

missing 4 · placeholders 0 · manifestLines 10 · statesExplored 1 · revealedOnly 0

output, 13 line(s), 1.4s
vlmkit check copy
source: index.html

status: suspect
rendered text: 1137 chars
disclosure states: 1 explored (details / tabs / aria-expanded)
manifest: 10 line(s), missing 4

Issues:
  x copy-missing: Manifest line not found in rendered text or any of 1 revealed disclosure state(s): "Delivery details" (comparison is whitespace-normalized, case-sensitive).
  x copy-missing: Manifest line not found in rendered text or any of 1 revealed disclosure state(s): "Review your order" (comparison is whitespace-normalized, case-sensitive).
  x copy-missing: Manifest line not found in rendered text or any of 1 revealed disclosure state(s): "Place order" (comparison is whitespace-normalized, case-sensitive).
  x copy-missing: Manifest line not found in rendered text or any of 1 revealed disclosure state(s): "Thanks — your first box is on its way." (comparison is whitespace-normalized, case-sensitive).
G42exit 0vlmkit check design index.html

verdict: COHERENT (0 finding(s))

verdict coherent · drifting 0 · roles 8

output, 25 line(s), 1.7s
vlmkit check design
source: index.html

verdict: COHERENT (0 finding(s))
  roles judged: 1 of 8 seen, spacing values: 15
  coverage: 12 of 154 visible element(s) carried an inferable role; 1 skipped as non-resting
    no role: span x60, div x21, p x9, label x8, path x8, dd x6, +14 more tag(s)
    A role comes from role="..." or from button/input/select/textarea/h1-h6. Layout elements
    (div, span, p, a) have none, so a large skip count is normal — this gate judges components,
    not every box. Add role="..." where an element IS a component to widen the coverage.

Role reuse (instances / distinct styles; drift below 3x from 3 instances)
  input:checkbox   4 inst    1 styles  reuse     4x  0 one-off  ok
  input:radio      2 inst    1 styles  reuse     2x  0 one-off  not judged
  note             1 inst    1 styles  reuse     1x  1 one-off  not judged
  h2               1 inst    1 styles  reuse     1x  1 one-off  not judged
  list             1 inst    1 styles  reuse     1x  1 one-off  not judged
  h1               1 inst    1 styles  reuse     1x  1 one-off  not judged
  select           1 inst    1 styles  reuse     1x  1 one-off  not judged
  button           1 inst    1 styles  reuse     1x  1 one-off  not judged
  not judged: input:radio (2), note (1), h2 (1), list (1), h1 (1), select (1), button (1) — under --min-instances 3, so no finding can come from them.
  To keep judging them: --min-instances 2 --min-reuse 2 (both — a 2-instance role cannot reach 3x however many instances the floor allows).

No design drift detected.
G43exit 0vlmkit check composition index.html

verdict: COMPOSED (0 finding(s))

verdict composed · proximity 0 · flatSteps 0 · railNearMisses 0

output, 16 line(s), 1.7s
vlmkit check composition
source: index.html

verdict: COMPOSED (0 finding(s))
  measured: 0 label(s), 2 heading level(s), 31 wide block(s) on 4 left / 4 right rail(s) — from 139 visible box(es)
  2 label(s) not judged: they open a box that paints its own group edge, so nothing above them could be mis-grouped with

Type hierarchy (largest instance of each declared level)
  h1     30px / 700   h1#step-1-title
  h2     18px / 700   h2#summary-title
  body 16px, largest 30px (1.88x), heaviest +400 weight

Group gaps: 18px between heading-led groups vs 8px inside them (2.25x, 1 sample(s)). Context only — intact pages measure 0.86-3.00x, so this cannot carry a verdict.

Proximity, alignment and type hierarchy all read consistently.
G44exit 1vlmkit check copy 'http://127.0.0.1:48217/index.html?step=2' --manifest copy.txt

missing 4 · placeholders 0 · manifestLines 10 · statesExplored 1 · revealedOnly 0

output, 14 line(s), 1.4s
vlmkit check copy
source: http://127.0.0.1:48217/index.html?step=2

status: suspect
rendered text: 1359 chars
disclosure states: 1 explored (details / tabs / aria-expanded)
manifest: 10 line(s), missing 4

Issues:
  x copy-missing: Manifest line not found in rendered text or any of 1 revealed disclosure state(s): "Choose your plan" (comparison is whitespace-normalized, case-sensitive).
  x copy-missing: Manifest line not found in rendered text or any of 1 revealed disclosure state(s): "Review your order" (comparison is whitespace-normalized, case-sensitive).
  x copy-missing: Manifest line not found in rendered text or any of 1 revealed disclosure state(s): "Place order" (comparison is whitespace-normalized, case-sensitive).
  x copy-missing: Manifest line not found in rendered text or any of 1 revealed disclosure state(s): "Thanks — your first box is on its way." (comparison is whitespace-normalized, case-sensitive).
  http://127.0.0.1:48217/index.html?step=2 is live and not pinned — a re-run may measure different data. Pin it: vlmkit snapshot record-har http://127.0.0.1:48217/index.html?step=2 --out app.har, then --har app.har
G45exit 1vlmkit check copy 'http://127.0.0.1:48217/index.html?step=4' --manifest copy.txt

missing 4 · placeholders 0 · manifestLines 10 · statesExplored 1 · revealedOnly 0

output, 14 line(s), 1.4s
vlmkit check copy
source: http://127.0.0.1:48217/index.html?step=4

status: suspect
rendered text: 1155 chars
disclosure states: 1 explored (details / tabs / aria-expanded)
manifest: 10 line(s), missing 4

Issues:
  x copy-missing: Manifest line not found in rendered text or any of 1 revealed disclosure state(s): "Choose your plan" (comparison is whitespace-normalized, case-sensitive).
  x copy-missing: Manifest line not found in rendered text or any of 1 revealed disclosure state(s): "Delivery details" (comparison is whitespace-normalized, case-sensitive).
  x copy-missing: Manifest line not found in rendered text or any of 1 revealed disclosure state(s): "Continue" (comparison is whitespace-normalized, case-sensitive).
  x copy-missing: Manifest line not found in rendered text or any of 1 revealed disclosure state(s): "Thanks — your first box is on its way." (comparison is whitespace-normalized, case-sensitive).
  http://127.0.0.1:48217/index.html?step=4 is live and not pinned — a re-run may measure different data. Pin it: vlmkit snapshot record-har http://127.0.0.1:48217/index.html?step=4 --out app.har, then --har app.har
G46exit 1vlmkit check copy 'http://127.0.0.1:48217/index.html?step=done' --manifest copy.txt

missing 6 · placeholders 0 · manifestLines 10 · statesExplored 1 · revealedOnly 0

output, 16 line(s), 1.4s
vlmkit check copy
source: http://127.0.0.1:48217/index.html?step=done

status: suspect
rendered text: 818 chars
disclosure states: 1 explored (details / tabs / aria-expanded)
manifest: 10 line(s), missing 6

Issues:
  x copy-missing: Manifest line not found in rendered text or any of 1 revealed disclosure state(s): "Choose your plan" (comparison is whitespace-normalized, case-sensitive).
  x copy-missing: Manifest line not found in rendered text or any of 1 revealed disclosure state(s): "Delivery details" (comparison is whitespace-normalized, case-sensitive).
  x copy-missing: Manifest line not found in rendered text or any of 1 revealed disclosure state(s): "Payment" (comparison is whitespace-normalized, case-sensitive).
  x copy-missing: Manifest line not found in rendered text or any of 1 revealed disclosure state(s): "Review your order" (comparison is whitespace-normalized, case-sensitive).
  x copy-missing: Manifest line not found in rendered text or any of 1 revealed disclosure state(s): "Continue" (comparison is whitespace-normalized, case-sensitive).
  x copy-missing: Manifest line not found in rendered text or any of 1 revealed disclosure state(s): "Place order" (comparison is whitespace-normalized, case-sensitive).
  http://127.0.0.1:48217/index.html?step=done is live and not pinned — a re-run may measure different data. Pin it: vlmkit snapshot record-har http://127.0.0.1:48217/index.html?step=done --out app.har, then --har app.har
N19acceptedabout G41

Every required line is present verbatim and visibly, but no single state shows all ten, and check copy renders one state. Union of four check copy runs in this round: load G41 finds Pantry Club, Choose your plan, Payment (tracker), Order summary, Continue, This is a demo. No payment is taken.; ?step=2 G44 adds Delivery details; ?step=4 G45 adds Review your order and Place order; ?step=done G46 adds Thanks — your first box is on its way. That makes 10 of 10. The same lines are also asserted visible in their states by verify flow (steps 5, 29, 34). The 'missing' rows are the brief's own multi-step design: later headings, the step-4 button and the confirmation that replaces the form.

N20acceptedabout G44

Per-state run for the copy union described on G41: this state shows its own lines. The ones reported missing belong to the other steps and are found by the other runs (G41/G44/G45/G46 together cover all 10 lines).

N21acceptedabout G45

Per-state run for the copy union described on G41: this state shows its own lines. The ones reported missing belong to the other steps and are found by the other runs (G41/G44/G45/G46 together cover all 10 lines).

N22acceptedabout G46

Per-state run for the copy union described on G41: this state shows its own lines. The ones reported missing belong to the other steps and are found by the other runs (G41/G44/G45/G46 together cover all 10 lines).

G47exit 0vlmkit check color index.html

verdict: CONSISTENT (0 finding(s))

verdict consistent · base #f6f3ee · bodyInk #1f1c19 · linkInk #ffffff · invisibleControls 0 · colorOnlyLinks 0 · unreadableColors 0

output, 25 line(s), 1.7s
vlmkit check color
source: index.html

verdict: CONSISTENT (0 finding(s))
  measured: 1 control(s), 0 link(s) in a text flow — from 162 visible box(es)

Palette (by painted area; reported, never judged — see the rejected candidates in the module docs)
  surfaces  4 distinct
    #f6f3ee   59.4%     1 el  
    #ffffff   37.1%    20 el  header.site-header>div.brand-bar
    #f4ecdc    2.0%     2 el  header.site-header>div.demo-notice
    #edf6f0    1.6%     1 el  div.plans>label.plan>input.plan__input
  ink       4 distinct
    #1f1c19   55.1%    42 el  div.demo-notice>div.wrap>p
    #6a635b   32.2%    27 el  div.summary-item__text>p.summary-item__meta>span.nw
    #57514a    9.0%     4 el  li.steps__item>span.steps__marker>span.steps__num
    #ffffff    3.7%     3 el  label.plan>span.plan__body>span.plan__badge
  marks     5 distinct
    #857d73   37.2%    64 el  ol#steps>li.steps__item>span.steps__marker
    #dcd5ca   35.2%    10 el  header.site-header>div.brand-bar
    #2d6a4f   14.4%    12 el  ol#steps>li.steps__item>span.steps__marker
    #e7e1d8    7.1%     4 el  div#summary-panel>div.summary-item
    #e6d9bf    6.1%     1 el  header.site-header>div.demo-notice
  base #f6f3ee, body ink #1f1c19, link ink #ffffff on 1 element(s)
G48exit 0vlmkit check color 'http://127.0.0.1:48217/index.html?step=2'

verdict: CONSISTENT (0 finding(s))

verdict consistent · base #f6f3ee · bodyInk #1f1c19 · linkInk #ffffff · invisibleControls 0 · colorOnlyLinks 0 · unreadableColors 0

output, 26 line(s), 1.7s
vlmkit check color
source: http://127.0.0.1:48217/index.html?step=2

verdict: CONSISTENT (0 finding(s))
  measured: 9 control(s), 0 link(s) in a text flow — from 118 visible box(es)

Palette (by painted area; reported, never judged — see the rejected candidates in the module docs)
  surfaces  3 distinct
    #f6f3ee   60.0%     1 el  
    #ffffff   38.2%    16 el  header.site-header>div.brand-bar
    #f4ecdc    1.8%     2 el  header.site-header>div.demo-notice
  ink       4 distinct
    #1f1c19   58.2%    29 el  div.demo-notice>div.wrap>p
    #6a635b   33.6%    15 el  div.summary-item__text>p.summary-item__meta>span.nw
    #57514a    4.9%     3 el  li.steps__item>span.steps__marker>span.steps__num
    #ffffff    3.2%     1 el  form#step-2>div.actions>button.btn
  marks     5 distinct
    #857d73   45.6%    48 el  ol#steps>li.steps__item>span.steps__marker
    #dcd5ca   38.2%    10 el  header.site-header>div.brand-bar
    #e7e1d8    7.4%     4 el  div#summary-panel>div.summary-item
    #e6d9bf    6.3%     1 el  header.site-header>div.demo-notice
    #2d6a4f    2.5%     8 el  ol#steps>li.steps__item>span.steps__marker
  base #f6f3ee, body ink #1f1c19, link ink #ffffff on 1 element(s)

  http://127.0.0.1:48217/index.html?step=2 is live and not pinned — a re-run may measure different data. Pin it: vlmkit snapshot record-har http://127.0.0.1:48217/index.html?step=2 --out app.har, then --har app.har
G49exit 0vlmkit check color 'http://127.0.0.1:48217/index.html?step=3'

verdict: CONSISTENT (0 finding(s))

verdict consistent · base #f6f3ee · bodyInk #1f1c19 · linkInk #ffffff · invisibleControls 0 · colorOnlyLinks 0 · unreadableColors 0

output, 27 line(s), 1.7s
vlmkit check color
source: http://127.0.0.1:48217/index.html?step=3

verdict: CONSISTENT (0 finding(s))
  measured: 5 control(s), 0 link(s) in a text flow — from 116 visible box(es)

Palette (by painted area; reported, never judged — see the rejected candidates in the module docs)
  surfaces  4 distinct
    #f6f3ee   60.9%     1 el  
    #ffffff   35.9%    12 el  header.site-header>div.brand-bar
    #f4ecdc    2.1%     2 el  header.site-header>div.demo-notice
    #edf6f0    1.1%     1 el  input#billing-same
  ink       4 distinct
    #1f1c19   60.9%    31 el  div.demo-notice>div.wrap>p
    #6a635b   30.8%    15 el  div.summary-item__text>p.summary-item__meta>span.nw
    #57514a    4.9%     2 el  li.steps__item>span.steps__marker>span.steps__num
    #ffffff    3.3%     1 el  form#step-3>div.actions>button.btn
  marks     5 distinct
    #dcd5ca   38.9%    10 el  header.site-header>div.brand-bar
    #857d73   28.2%    32 el  ol#steps>li.steps__item>span.steps__marker
    #2d6a4f   18.2%    20 el  ol#steps>li.steps__item>span.steps__marker
    #e7e1d8    7.9%     4 el  div#summary-panel>div.summary-item
    #e6d9bf    6.7%     1 el  header.site-header>div.demo-notice
  base #f6f3ee, body ink #1f1c19, link ink #ffffff on 1 element(s)

  http://127.0.0.1:48217/index.html?step=3 is live and not pinned — a re-run may measure different data. Pin it: vlmkit snapshot record-har http://127.0.0.1:48217/index.html?step=3 --out app.har, then --har app.har
G50exit 0vlmkit check a11y contrast index.html

inspected 81 · failures 0

output, 5 line(s), 1.8s
  vlmkit check a11y contrast
  html: examples/sites/checkout/index.html
  inspected 81 text-bearing element(s)
  ✓ 0 contrast failure(s)
  report: examples/sites/checkout/test-results/a11y-contrast/index-ca6a16e3/report.md
G51exit 0vlmkit check a11y contrast 'http://127.0.0.1:48217/index.html?step=2'

inspected 50 · failures 0

output, 6 line(s), 1.6s
  vlmkit check a11y contrast
  html: http://127.0.0.1:48217/index.html?step=2
  inspected 50 text-bearing element(s)
  ✓ 0 contrast failure(s)
  report: examples/sites/checkout/test-results/a11y-contrast/index-0d8f1101/report.md
  http://127.0.0.1:48217/index.html?step=2 is live and not pinned — a re-run may measure different data. Pin it: vlmkit snapshot record-har http://127.0.0.1:48217/index.html?step=2 --out app.har, then --har app.har
G52exit 0vlmkit check a11y contrast 'http://127.0.0.1:48217/index.html?step=3'

inspected 51 · failures 0

output, 6 line(s), 1.7s
  vlmkit check a11y contrast
  html: http://127.0.0.1:48217/index.html?step=3
  inspected 51 text-bearing element(s)
  ✓ 0 contrast failure(s)
  report: examples/sites/checkout/test-results/a11y-contrast/index-19231d1c/report.md
  http://127.0.0.1:48217/index.html?step=3 is live and not pinned — a re-run may measure different data. Pin it: vlmkit snapshot record-har http://127.0.0.1:48217/index.html?step=3 --out app.har, then --har app.har
G53exit 0vlmkit check a11y contrast 'http://127.0.0.1:48217/index.html?step=4'

inspected 69 · failures 0

output, 6 line(s), 1.6s
  vlmkit check a11y contrast
  html: http://127.0.0.1:48217/index.html?step=4
  inspected 69 text-bearing element(s)
  ✓ 0 contrast failure(s)
  report: examples/sites/checkout/test-results/a11y-contrast/index-3caffc24/report.md
  http://127.0.0.1:48217/index.html?step=4 is live and not pinned — a re-run may measure different data. Pin it: vlmkit snapshot record-har http://127.0.0.1:48217/index.html?step=4 --out app.har, then --har app.har
G54exit 0vlmkit check a11y contrast 'http://127.0.0.1:48217/index.html?step=done'

inspected 37 · failures 0

output, 6 line(s), 1.6s
  vlmkit check a11y contrast
  html: http://127.0.0.1:48217/index.html?step=done
  inspected 37 text-bearing element(s)
  ✓ 0 contrast failure(s)
  report: examples/sites/checkout/test-results/a11y-contrast/index-628ce05f/report.md
  http://127.0.0.1:48217/index.html?step=done is live and not pinned — a re-run may measure different data. Pin it: vlmkit snapshot record-har http://127.0.0.1:48217/index.html?step=done --out app.har, then --har app.har
G55exit 1vlmkit check a11y focus index.html

steps 7 · findings 1

output, 6 line(s), 1.6s
  vlmkit check a11y focus
  source: index.html
  captured 7 focus step(s)
  ✗ 1 finding(s)
    [reverse] Focus moved up by 78px (from `form#step-1>fieldset.group>div.plans>label.plan>input.plan__input` at y=412 to `form#step-1>fieldset.group>div.choices>label.choice>input.choice__input` at y=334). Visual order is top-to-bottom; check `tabindex` or DOM order.
  report: examples/sites/checkout/test-results/a11y-focus-order/index-ca6a16e3/report.md
G56exit 0vlmkit check a11y touch index.html

level AA · inspected 9 · failures 0 · wcagExempt 0

output, 5 line(s), 1.7s
  vlmkit check a11y touch
  source: index.html  level: WCAG AA (24×24 min)
  inspected 9 interactive element(s)
  ✓ 0 undersized target(s)
  report: examples/sites/checkout/test-results/a11y-touch/index-ca6a16e3/report.md
G57exit 0vlmkit check a11y touch 'http://127.0.0.1:48217/index.html?step=3'

level AA · inspected 9 · failures 0 · wcagExempt 0

output, 6 line(s), 1.5s
  vlmkit check a11y touch
  source: http://127.0.0.1:48217/index.html?step=3  level: WCAG AA (24×24 min)
  inspected 9 interactive element(s)
  ✓ 0 undersized target(s)
  report: examples/sites/checkout/test-results/a11y-touch/index-19231d1c/report.md
  http://127.0.0.1:48217/index.html?step=3 is live and not pinned — a re-run may measure different data. Pin it: vlmkit snapshot record-har http://127.0.0.1:48217/index.html?step=3 --out app.har, then --har app.har
G58exit 0vlmkit check a11y touch 'http://127.0.0.1:48217/index.html?step=4'

level AA · inspected 6 · failures 0 · wcagExempt 0

output, 6 line(s), 1.5s
  vlmkit check a11y touch
  source: http://127.0.0.1:48217/index.html?step=4  level: WCAG AA (24×24 min)
  inspected 6 interactive element(s)
  ✓ 0 undersized target(s)
  report: examples/sites/checkout/test-results/a11y-touch/index-3caffc24/report.md
  http://127.0.0.1:48217/index.html?step=4 is live and not pinned — a re-run may measure different data. Pin it: vlmkit snapshot record-har http://127.0.0.1:48217/index.html?step=4 --out app.har, then --har app.har
G59exit 0vlmkit scan scroll index.html

containers 0 · overflowX 0 · issues 13

output, 25 line(s), 1.4s
vlmkit scan scroll
source: index.html (1280x720)

status: warn
  exits 0 — 13 warn(s) did not fail this command. To gate on one: --rule clipped-content=suspect
page: 1280x1316 — horizontal overflow 0px, vertical scroll 596px
scroll containers: 0 (dead scrollports 1, clipped 13)

Dead scrollports (declared scrollable, content fits):
  - #instructions (overflow: auto auto)

Issues:
  ! clipped-content div.plans > label:nth-of-type(1) > span:nth-of-type(1) > span:nth-of-type(2) > span:nth-of-type(3): div.plans > label:nth-of-type(1) > span:nth-of-type(1) > span:nth-of-type(2) > span:nth-of-type(3) clips 21px of its content behind overflow: hidden — cut-off content unless the oversize is decorative.
  ! clipped-content div.plans > label:nth-of-type(2) > span:nth-of-type(1) > span:nth-of-type(2) > span:nth-of-type(3): div.plans > label:nth-of-type(2) > span:nth-of-type(1) > span:nth-of-type(2) > span:nth-of-type(3) clips 21px of its content behind overflow: hidden — cut-off content unless the oversize is decorative.
  ! clipped-content div.plans > label:nth-of-type(3) > span:nth-of-type(1) > span:nth-of-type(2) > span:nth-of-type(3): div.plans > label:nth-of-type(3) > span:nth-of-type(1) > span:nth-of-type(2) > span:nth-of-type(3) clips 21px of its content behind overflow: hidden — cut-off content unless the oversize is decorative.
  ! clipped-content div.plans > label:nth-of-type(1) > span:nth-of-type(1) > span:nth-of-type(3) > span:nth-of-type(1): div.plans > label:nth-of-type(1) > span:nth-of-type(1) > span:nth-of-type(3) > span:nth-of-type(1) clips 20px of its content behind overflow: hidden — cut-off content unless the oversize is decorative.
  ! clipped-content div.plans > label:nth-of-type(1) > span:nth-of-type(1) > span:nth-of-type(3) > span:nth-of-type(3): div.plans > label:nth-of-type(1) > span:nth-of-type(1) > span:nth-of-type(3) > span:nth-of-type(3) clips 20px of its content behind overflow: hidden — cut-off content unless the oversize is decorative.
  ! clipped-content div.plans > label:nth-of-type(2) > span:nth-of-type(1) > span:nth-of-type(3) > span:nth-of-type(1): div.plans > label:nth-of-type(2) > span:nth-of-type(1) > span:nth-of-type(3) > span:nth-of-type(1) clips 20px of its content behind overflow: hidden — cut-off content unless the oversize is decorative.
  ! clipped-content div.plans > label:nth-of-type(2) > span:nth-of-type(1) > span:nth-of-type(3) > span:nth-of-type(3): div.plans > label:nth-of-type(2) > span:nth-of-type(1) > span:nth-of-type(3) > span:nth-of-type(3) clips 20px of its content behind overflow: hidden — cut-off content unless the oversize is decorative.
  ! clipped-content div.plans > label:nth-of-type(3) > span:nth-of-type(1) > span:nth-of-type(3) > span:nth-of-type(1): div.plans > label:nth-of-type(3) > span:nth-of-type(1) > span:nth-of-type(3) > span:nth-of-type(1) clips 20px of its content behind overflow: hidden — cut-off content unless the oversize is decorative.
  ! clipped-content div.plans > label:nth-of-type(3) > span:nth-of-type(1) > span:nth-of-type(3) > span:nth-of-type(3): div.plans > label:nth-of-type(3) > span:nth-of-type(1) > span:nth-of-type(3) > span:nth-of-type(3) clips 20px of its content behind overflow: hidden — cut-off content unless the oversize is decorative.
  ! clipped-content div.plans > label:nth-of-type(1) > span:nth-of-type(1) > span:nth-of-type(4) > span:nth-of-type(1): div.plans > label:nth-of-type(1) > span:nth-of-type(1) > span:nth-of-type(4) > span:nth-of-type(1) clips 19px of its content behind overflow: hidden — cut-off content unless the oversize is decorative.
  ! clipped-content div.plans > label:nth-of-type(2) > span:nth-of-type(1) > span:nth-of-type(4) > span:nth-of-type(1): div.plans > label:nth-of-type(2) > span:nth-of-type(1) > span:nth-of-type(4) > span:nth-of-type(1) clips 19px of its content behind overflow: hidden — cut-off content unless the oversize is decorative.
  ! clipped-content div.plans > label:nth-of-type(3) > span:nth-of-type(1) > span:nth-of-type(4) > span:nth-of-type(1): div.plans > label:nth-of-type(3) > span:nth-of-type(1) > span:nth-of-type(4) > span:nth-of-type(1) clips 19px of its content behind overflow: hidden — cut-off content unless the oversize is decorative.
  ! clipped-content span.plan__badge > span:nth-of-type(1): span.plan__badge > span:nth-of-type(1) clips 17px of its content behind overflow: hidden — cut-off content unless the oversize is decorative.
G60exit 0vlmkit scan scroll index.html --viewport 375x812

containers 0 · overflowX 0 · issues 13

output, 25 line(s), 1.4s
vlmkit scan scroll
source: index.html (375x812)

status: warn
  exits 0 — 13 warn(s) did not fail this command. To gate on one: --rule clipped-content=suspect
page: 375x1731 — horizontal overflow 0px, vertical scroll 919px
scroll containers: 0 (dead scrollports 1, clipped 13)

Dead scrollports (declared scrollable, content fits):
  - #instructions (overflow: auto auto)

Issues:
  ! clipped-content div.plans > label:nth-of-type(1) > span:nth-of-type(1) > span:nth-of-type(2) > span:nth-of-type(3): div.plans > label:nth-of-type(1) > span:nth-of-type(1) > span:nth-of-type(2) > span:nth-of-type(3) clips 21px of its content behind overflow: hidden — cut-off content unless the oversize is decorative.
  ! clipped-content div.plans > label:nth-of-type(2) > span:nth-of-type(1) > span:nth-of-type(2) > span:nth-of-type(3): div.plans > label:nth-of-type(2) > span:nth-of-type(1) > span:nth-of-type(2) > span:nth-of-type(3) clips 21px of its content behind overflow: hidden — cut-off content unless the oversize is decorative.
  ! clipped-content div.plans > label:nth-of-type(3) > span:nth-of-type(1) > span:nth-of-type(2) > span:nth-of-type(3): div.plans > label:nth-of-type(3) > span:nth-of-type(1) > span:nth-of-type(2) > span:nth-of-type(3) clips 21px of its content behind overflow: hidden — cut-off content unless the oversize is decorative.
  ! clipped-content div.plans > label:nth-of-type(1) > span:nth-of-type(1) > span:nth-of-type(3) > span:nth-of-type(1): div.plans > label:nth-of-type(1) > span:nth-of-type(1) > span:nth-of-type(3) > span:nth-of-type(1) clips 20px of its content behind overflow: hidden — cut-off content unless the oversize is decorative.
  ! clipped-content div.plans > label:nth-of-type(1) > span:nth-of-type(1) > span:nth-of-type(3) > span:nth-of-type(3): div.plans > label:nth-of-type(1) > span:nth-of-type(1) > span:nth-of-type(3) > span:nth-of-type(3) clips 20px of its content behind overflow: hidden — cut-off content unless the oversize is decorative.
  ! clipped-content div.plans > label:nth-of-type(2) > span:nth-of-type(1) > span:nth-of-type(3) > span:nth-of-type(1): div.plans > label:nth-of-type(2) > span:nth-of-type(1) > span:nth-of-type(3) > span:nth-of-type(1) clips 20px of its content behind overflow: hidden — cut-off content unless the oversize is decorative.
  ! clipped-content div.plans > label:nth-of-type(2) > span:nth-of-type(1) > span:nth-of-type(3) > span:nth-of-type(3): div.plans > label:nth-of-type(2) > span:nth-of-type(1) > span:nth-of-type(3) > span:nth-of-type(3) clips 20px of its content behind overflow: hidden — cut-off content unless the oversize is decorative.
  ! clipped-content div.plans > label:nth-of-type(3) > span:nth-of-type(1) > span:nth-of-type(3) > span:nth-of-type(1): div.plans > label:nth-of-type(3) > span:nth-of-type(1) > span:nth-of-type(3) > span:nth-of-type(1) clips 20px of its content behind overflow: hidden — cut-off content unless the oversize is decorative.
  ! clipped-content div.plans > label:nth-of-type(3) > span:nth-of-type(1) > span:nth-of-type(3) > span:nth-of-type(3): div.plans > label:nth-of-type(3) > span:nth-of-type(1) > span:nth-of-type(3) > span:nth-of-type(3) clips 20px of its content behind overflow: hidden — cut-off content unless the oversize is decorative.
  ! clipped-content div.plans > label:nth-of-type(1) > span:nth-of-type(1) > span:nth-of-type(4) > span:nth-of-type(1): div.plans > label:nth-of-type(1) > span:nth-of-type(1) > span:nth-of-type(4) > span:nth-of-type(1) clips 19px of its content behind overflow: hidden — cut-off content unless the oversize is decorative.
  ! clipped-content div.plans > label:nth-of-type(2) > span:nth-of-type(1) > span:nth-of-type(4) > span:nth-of-type(1): div.plans > label:nth-of-type(2) > span:nth-of-type(1) > span:nth-of-type(4) > span:nth-of-type(1) clips 19px of its content behind overflow: hidden — cut-off content unless the oversize is decorative.
  ! clipped-content div.plans > label:nth-of-type(3) > span:nth-of-type(1) > span:nth-of-type(4) > span:nth-of-type(1): div.plans > label:nth-of-type(3) > span:nth-of-type(1) > span:nth-of-type(4) > span:nth-of-type(1) clips 19px of its content behind overflow: hidden — cut-off content unless the oversize is decorative.
  ! clipped-content span.plan__badge > span:nth-of-type(1): span.plan__badge > span:nth-of-type(1) clips 17px of its content behind overflow: hidden — cut-off content unless the oversize is decorative.
G61exit 0vlmkit scan handlers index.html

registrations 41 · elements 33 · suspects 0

output, 44 line(s), 1.6s
vlmkit scan handlers
source: index.html

status: 1 warn(s)
  exits 0 — 1 warn(s) did not fail this command. To gate on one: --rule unprobed-handler-types=suspect
registrations: 41 across 33 element(s) + globals, on a page presenting 9 control(s)

  - main#main>div.flow>form#step-1 "Choose your plan Fresh ingredients and e": submit, focusout (delegation container)
  - main#main>div.flow>form#step-2 "Delivery details Where should we bring y": submit, focusout (no role)
  - main#main>div.flow>form#step-3 "Payment Use a test card: this demo never": submit, focusout (no role)
  - main#main>div.flow>form#step-4 "Review your order Check the details, the": submit, focusout (no role)
  - div.flow>form#step-2>div.actions>button.btn "Back": click (no role)
  - div.flow>form#step-3>div.actions>button.btn "Back": click (no role)
  - div.flow>form#step-4>div.actions>button.btn "Back": click (no role)
  - div.review>section.review__block>div.review__head>a.review__edit "Edit plan": click (no role)
  - div.review>section.review__block>div.review__head>a.review__edit "Edit delivery details": click (no role)
  - div.review>section.review__block>div.review__head>a.review__edit "Edit payment": click (no role)
  - main#main>div.flow>form#step-1>div#errors-1 "": click (no role)
  - main#main>div.flow>form#step-2>div#errors-2 "": click (no role)
  - main#main>div.flow>form#step-3>div#errors-3 "": click (no role)
  - main#main>div.flow>form#step-4>div#errors-4 "": click (no role)
  - fieldset.group>div.plans>label.plan>input.plan__input "2p3m": change
  - fieldset.group>div.plans>label.plan>input.plan__input "4p3m": change
  - fieldset.group>div.plans>label.plan>input.plan__input "2p5m": change
  - fieldset.group>div.choices>label.choice>input.choice__input "Vegetarian": change
  - fieldset.group>div.choices>label.choice>input.choice__input "No pork": change
  - fieldset.group>div.choices>label.choice>input.choice__input "Dairy-free": change
  - fieldset.group>div.choices>label.choice>input.choice__input "Nut-free": change
  - form#step-1>div.field>div.select>select#day "Select a day Monday Tuesday Wednesday Th": change
  - div.fields>div.field>div.select>select#state "Select AlabamaAlaskaArizonaArkansasCalif": change (no role)
  - div#billing-fields>div.field>div.select>select#billing-state "Select AlabamaAlaskaArizonaArkansasCalif": change (no role)
  - form#step-4>div.field>label.choice>input#terms "": change (no role)
  - form#step-2>div.fields>div.field>textarea#instructions "": input (no role)
  - fieldset.group>div.fields>div.field>input#card-number "": input (no role)
  - fieldset.group>div.fields>div.field>input#expiry "": input (no role)
  - fieldset.group>div.fields>div.field>input#cvc "": input (no role)
  - form#step-3>fieldset.group>label.choice>input#billing-same "": change (no role)
  - form#step-3>div.field>div.promo__row>button#promo-apply "Apply": click (no role)
  - form#step-3>div.field>div.promo__row>input#promo "": keydown (no role)
  - main#main>aside#summary>button#summary-toggle "Show order summary $64.74": click (no role)
  - globals: document:mousedown, document:click, document:mouseup, window:blur

Issues:
  warn [unprobed-handler-types] 6 handler type(s) registered and NONE exercised: change, click, focusout, input, keydown, submit — this gate is an inventory and presses nothing. A clean result here says the wiring exists, not that it works. Run 'check interactions --handlers' to focus and activate the controls, add '--probe-drag' for the drag surfaces, or drive the rest with a 'verify flow' script.
G62exit 0vlmkit check interactions index.html

elements 9 · suspects 0 · warns 0

output, 15 line(s), 10.1s
vlmkit check interactions
source: index.html

status: ok
interactive elements: 9

  - [radio] "2p3m" focus✓ | ArrowDown: layout change
  - [radio] "4p3m" unreachable | ArrowDown: layout change
  - [radio] "2p5m" unreachable | ArrowDown: focus moves
  - [checkbox] "Vegetarian" focus✓ | Space: layout change
  - [checkbox] "No pork" focus✓ | Space: layout change
  - [checkbox] "Dairy-free" focus✓ | Space: layout change
  - [checkbox] "Nut-free" focus✓ | Space: layout change
  - [combobox] "Select a day Monday Tuesday Wednesday Thursday Friday Saturday" focus✓ | ArrowDown: layout change | Esc closes
  - [button] "Continue" focus✓ | Enter: focus moves within
G63exit 0vlmkit check interactions 'http://127.0.0.1:48217/index.html?step=3'

elements 9 · suspects 0 · warns 0

output, 16 line(s), 5.3s
vlmkit check interactions
source: http://127.0.0.1:48217/index.html?step=3

status: ok
interactive elements: 9

  - [textbox] "" focus✓
  - [textbox] "" focus✓
  - [textbox] "" focus✓
  - [textbox] "" focus✓
  - [checkbox] "" focus✓ | Space: layout change
  - [textbox] "" focus✓
  - [button] "Apply" focus✓ | Enter: layout change | announces
  - [button] "Back" focus✓ | Enter: focus moves within
  - [button] "Continue" focus✓ | Enter: focus moves within
  http://127.0.0.1:48217/index.html?step=3 is live and not pinned — a re-run may measure different data. Pin it: vlmkit snapshot record-har http://127.0.0.1:48217/index.html?step=3 --out app.har, then --har app.har
G64exit 0vlmkit check interactions 'http://127.0.0.1:48217/index.html?step=4'

elements 6 · suspects 0 · warns 1

output, 17 line(s), 4.5s
vlmkit check interactions
source: http://127.0.0.1:48217/index.html?step=4

status: ok
  exits 0 — 1 warn(s) did not fail this command. To gate on one: --rule inert-control=suspect
interactive elements: 6

  - [link] "Edit plan" focus✓
  - [link] "Edit delivery details" focus✓
  - [link] "Edit payment" focus✓
  - [checkbox] "" focus✓ | Space: no response
  - [button] "Back" focus✓ | Enter: focus moves within
  - [button] "Place order" focus✓ | Enter: focus moves within

Issues:
  warn [inert-control] checkbox "" (form#step-4>div.field>label.choice>input#terms) shows no observable response to Space — no ARIA change, no layout change. Dead control, or its response is outside this probe.
  http://127.0.0.1:48217/index.html?step=4 is live and not pinned — a re-run may measure different data. Pin it: vlmkit snapshot record-har http://127.0.0.1:48217/index.html?step=4 --out app.har, then --har app.har
G65exit 0vlmkit verify flow index.html --flow flow.json

verdict: DONE (34/34 steps)

done true · passed 34 · total 34

output, 130 line(s), 2.8s
vlmkit verify flow
source: index.html

verdict: DONE (34/34 steps)

✓ step 1: Step 1: Continue with no delivery day shows the error summary and moves focus to it
    ✓ #errors-1 visible 
    ✓ #errors-1 focused 
    ✓ #errors-1 text~"Select a delivery day" 
    ✓ #day aria-invalid=true 
    ✓ #day aria-describedby=day-hint day-error 
    ✓ #day-error visible 
✓ step 2: Choosing a day re-validates the field and clears the summary
    ✓ #day-error count=0 
    ✓ #errors-1 hidden 
    ✓ #day-note text~"Monday, October 12" 
    ✓ #summary-panel text~"Mon, Oct 12" 
✓ step 3: The 4-person radio card updates the order summary
    ✓ #summary-panel text~"4 people · 3 meals a week" 
    ✓ #summary-panel text~"$107.88" 
    ✓ #summary-panel text~"$116.51" 
✓ step 4: A dietary preference shows in the summary
    ✓ #summary-panel text~"Vegetarian" 
✓ step 5: Continue moves to step 2, marks it current and focuses its heading
    ✓ #step-1 hidden 
    ✓ #step-2 visible 
    ✓ #step-2-title focused 
    ✓ #step-2-title text~"Delivery details" 
    ✓ #steps [data-step='2'] aria-current=step 
    ✓ #steps [data-step='1'] text~"completed" 
✓ step 6: Step 2: Continue with empty fields lists six errors and ties each message to its field
    ✓ #errors-2 focused 
    ✓ #errors-2 li count=6 
    ✓ #full-name aria-invalid=true 
    ✓ #full-name aria-describedby=full-name-error 
    ✓ #zip aria-invalid=true 
    ✓ #phone[aria-invalid] count=0 
    ✓ .field__error .icon count=6 
✓ step 7: An error-summary link moves focus to its field
    ✓ #email focused 
✓ step 8: Fill the name
    ✓ #full-name focused 
✓ step 9: Leaving the name field re-validates it (error gone, summary down to five)
    ✓ #full-name[aria-invalid] count=0 
    ✓ #full-name-error count=0 
    ✓ #errors-2 li count=5 
✓ step 10: Leaving a malformed email shows the format message
    ✓ #email-error text~"like name@example.com" 
✓ step 11: Correct the email
    ✓ #email focused 
✓ step 12: Address line 1
    ✓ #email-error count=0 
✓ step 13: City
    ✓ #address1-error count=0 
✓ step 14: State by typeahead
    ✓ #state-error count=0 
✓ step 15: ZIP code
    ✓ #city-error count=0 
✓ step 16: Delivery instructions count down from 200
    ✓ #instructions-count text~"179 characters left" 
    ✓ #zip-error count=0 
✓ step 17: Continue to step 3
    ✓ #step-3 visible 
    ✓ #step-3-title focused 
    ✓ #steps [data-step='3'] aria-current=step 
    ✓ #billing-preview text~"1450 Alder Street, Portland, OR 97214" 
✓ step 18: Step 3: Continue with no card lists four errors
    ✓ #errors-3 focused 
    ✓ #errors-3 li count=4 
    ✓ #card-number aria-invalid=true 
✓ step 19: Card number
    ✓ #card-number focused 
✓ step 20: Expiry (the slash is inserted as you type)
    ✓ #card-number-error count=0 
✓ step 21: Security code
    ✓ #expiry-error count=0 
✓ step 22: Name on card
    ✓ #cvc-error count=0 
✓ step 23: Unticking 'same as delivery' reveals the billing fields
    ✓ #billing-fields visible 
    ✓ #billing-address1 visible 
    ✓ #card-name-error count=0 
✓ step 24: Ticking it again hides them
    ✓ #billing-fields hidden 
✓ step 25: Type an unknown promo code
    ✓ #promo focused 
✓ step 26: An unknown promo code is rejected with a message
    ✓ #promo-message text~"isn’t a valid promo code" 
    ✓ #promo aria-invalid=true 
    ✓ #summary-promo hidden 
✓ step 27: Type WELCOME10 in lower case
    ✓ #promo focused 
✓ step 28: WELCOME10 takes 10% off and the summary follows
    ✓ #promo-message text~"WELCOME10 applied" 
    ✓ #promo[aria-invalid] count=0 
    ✓ #summary-promo visible 
    ✓ #summary-promo text~"$10.79" 
    ✓ #summary-panel text~"$104.86" 
    ✓ #promo-apply text~"Remove" 
✓ step 29: Continue to review
    ✓ #step-4 visible 
    ✓ #step-4-title focused 
    ✓ #step-4-title text~"Review your order" 
    ✓ #step-4 text~"Card ending 4242, expires 12/28" 
    ✓ #step-4 text~"1450 Alder Street" 
    ✓ #step-4 text~"WELCOME10" 
    ✓ #step-4 text~"Vegetarian" 
    ✓ #step-4 button[type=submit] visible 
    ✓ #step-4 button[type=submit] text~"Place order" 
✓ step 30: Edit on the Delivery block goes back to step 2
    ✓ #step-2 visible 
    ✓ #step-2-title focused 
✓ step 31: …and Continue returns straight to review
    ✓ #step-4 visible 
    ✓ #step-4-title focused 
✓ step 32: Place order without agreeing to the terms is stopped
    ✓ #errors-4 focused 
    ✓ #terms aria-invalid=true 
    ✓ #confirmation hidden 
✓ step 33: Agree to the terms
    ✓ #terms-error count=0 
✓ step 34: Place order replaces the form with the confirmation
    ✓ #confirmation visible 
    ✓ #step-4 hidden 
    ✓ #steps hidden 
    ✓ #confirmation-title focused 
    ✓ #confirmation-title text~"Thanks — your first box is on its way." 
    ✓ #confirmation text~"PC-20417" 
    ✓ #confirmation text~"Monday, October 12, 2026" 
    ✓ #summary-panel text~"$104.86"
G66exit 0vlmkit check scroll index.html

pageScrolled 596 · stickyFixed 1 · engagedSticky 1 · snaps 0 · issues 0

output, 12 line(s), 1.4s
vlmkit check scroll
source: index.html

status: ok
page scrolled: 596px
sticky/fixed elements: 1 (sticky engaged by the scroll: 1)
snap containers driven: 0

Sticky / fixed:
  - #summary: sticky top=24px viewport y 143 -> 24

No scroll-behavior issues detected.
G67exit 0vlmkit check breakpoints index.html --sweep

breakpoints 6 · issues 0 · sweepRanges 0

output, 15 line(s), 3.7s
vlmkit check breakpoints
source: index.html

status: ok
width sweep: 320-1280px step 25 (39 widths) — clean
breakpoints checked: 479, 480, 559, 560, 959, 960px (each at B-1 / B / B+1)

  479px: clean (max-width: 479px)
  480px: clean (min-width: 480px)
  559px: clean (max-width: 559px)
  560px: clean (min-width: 560px)
  959px: clean (max-width: 959px)
  960px: clean (min-width: 960px)

All boundaries consistent.
G68exit 0vlmkit check grounding 'http://127.0.0.1:48217/index.html?step=4'

targets 2 · scale 0.5 · issues 0

output, 13 line(s), 1.5s
vlmkit check grounding
source: http://127.0.0.1:48217/index.html?step=4 (1280x720)

status: ok
frame: 640x360 screenshot px — medium, cap 640x480, scale 0.50 (every coordinate below is in these 640x360 pixels)
targets: 2 actionable in frame (4 disabled or out of the frame)

Action map (click point in screenshot px):
  t1 link "Edit plan" @ (344,197) 30x22 div.review > section:nth-of-type(1) > div:nth-of-type(1) > a:nth-of-type(1)
  t2 link "Edit delivery details" @ (344,295) 30x22 div.review > section:nth-of-type(2) > div:nth-of-type(1) > a:nth-of-type(1)

Every target in the frame is resolvable and routes its own clicks.
  http://127.0.0.1:48217/index.html?step=4 is live and not pinned — a re-run may measure different data. Pin it: vlmkit snapshot record-har http://127.0.0.1:48217/index.html?step=4 --out app.har, then --har app.har
S32

final: step 1 (load)

index.html · 1280×800 · full page, 2 screen(s)

S32 screen 1S32 screen 2
L32 saw

Final, desktop 1280, step 1 at load (2 screens). Sand demo note on one line; brand bar with mark + "Pantry Club" only. Tracker with step 1 current (green ring + halo, bold "Plan"). Step card: "Choose your plan", two-line intro, "Box size", three equal-height plan rows 18px apart — row 1 selected (green 2px border, mint fill, filled radio), the "Best value" pill straddling row 2's top edge aligned with its title; prices right-aligned in tabular figures. Screen 2: "Dietary preferences (optional)" + hint, 2x2 tiles, "Delivery day" + hint + 320px select, hairline, green "Continue" at right, footer. Sticky summary: box art, "2 people · 3 meals a week" / "6 servings · $9.99 each" on single lines, None / Pick a day, Subtotal $59.94, Shipping Free, Tax $4.80, Total today $64.74, renewal note; it stays pinned at top 24px on screen 2. Left rail x=153 inside the card, x=112 for the page; nothing overlaps or clips.

S33

final: step 1 (load)

index.html · 375×812 · full page, 3 screen(s)

S33 screen 1S33 screen 2S33 screen 3
L33 saw

Final, mobile 375, step 1 (3 screens). Demo note in three lines, brand bar, full-bleed "Show order summary v $64.74" bar, one-row tracker. Plan cards stack people / meals / servings / per-serving with the price column right; the "Best value" pill sits on card 2's top edge; the three cards share one height and "3 meals a week" never wraps. Screen 2: the diet hint now breaks as "We’ll steer your weekly menu / around these." (text-wrap: pretty — no lone word), four full-width 52px tiles, Delivery day with a two-line hint and full-width select, full-width green Continue. Screen 3: card end and footer. 16px gutters, one rail at x=33 inside the card, no horizontal overflow.

S34

final: step 2 on desktop

index.html?step=2 · 1280×800 · full page, 2 screen(s)

2 screen(s), looked at when taken; not kept once the round was done — a round keeps one full-page walk per width.

L34 saw

Final, desktop, step 2 (?step=2), 2 screens. Tracker: Plan done (check, green connector), Delivery current. "Delivery details / Where should we bring your box?"; Full name, Email + hint, Phone (optional) + hint with the 320px input, Address line 1, Address line 2 (optional), City / State (Select) / ZIP on one aligned row, Delivery instructions (optional) + hint + textarea + "200 characters left". Back left, Continue right under a hairline. Every field boundary is a clear grey line on white; bold labels, grey hints, a steady 6px/22px rhythm. The summary (First delivery Tue, Oct 13) is pinned beside the form on both screens.

S35

final: step 3 on desktop

index.html?step=3 · 1280×800 · full page, 2 screen(s)

2 screen(s), looked at when taken; not kept once the round was done — a round keeps one full-page walk per width.

L35 saw

Final, desktop, step 3 (?step=3), 2 screens. Tracker with Plan and Delivery done, Payment current. "Payment" + "Use a test card: this demo never charges or stores it." Card details: Card number with the 4242 hint, Expiry (MM/YY) and Security code (CVC) side by side with one-line hints, Name on card. Billing address: the ticked "Billing address is the same as delivery" tile with "1450 Alder Street, Apt 3, Portland, OR 97214" previewed in grey. Promo code (optional) at the 18px section size with its WELCOME10 hint, a 320px input and "Apply". Back / Continue. Three sections of equal weight read clearly in order.

S36

final: step 4 on desktop

index.html?step=4 · 1280×800 · full page, 2 screen(s)

2 screen(s), looked at when taken; not kept once the round was done — a round keeps one full-page walk per width.

L36 saw

Final, desktop, step 4 (?step=4), 2 screens. Tracker all green up to Review (current). "Review your order" + "Check the details, then place your order." Plan block: Box / Dietary preferences / Delivery day each on one line in the 168px term column; Delivery block: Name, Contact, three-line Address, Instructions; Payment block: Card ending 4242, expires 12/28 with the cardholder, Billing "Same as delivery address", Promo "None". Each block has an underlined "Edit" at the right of its heading. The unticked terms tile with the bold $64.74 renewal price, then Back and "Place order". Summary pinned with Total today $64.74.

S37

final: step 2 with validation errors

index.html?step=2 · 1280×800 · full page, 3 screen(s) · click #step-2 button[type=submit]

3 screen(s), looked at when taken; not kept once the round was done — a round keeps one full-page walk per width.

L37 saw

Final, desktop, step 2 after Continue on empty fields (3 screens). Directly under the heading and intro: the focused error summary (pale pink, 5px red left bar, dark-green focus ring) with "(!) 6 fields need your attention" and six underlined links in field order. Each failing field has a 2px red border and a bold red (!) message under it; Phone and Address line 2 (optional) stay plain grey. In the City / State / ZIP row the ZIP message wraps to "Enter your / ZIP code" in its 131px column — ragged but legible and under its own field (accepted in L10). Back / Continue at the end; summary pinned throughout. The errors are unmistakable without being loud: a soft tint, one strong edge, icons and plain words.

S38

final: promo code rejected

index.html?step=3 · 1280×800 · element #step-3 .promo · fill #promo "SAVE20" · click #promo-apply

1 screen(s), looked at when taken; not kept once the round was done — a round keeps one full-page walk per width.

L38 saw

Final close-up of the promo field after SAVE20 + Apply: red-bordered input "SAVE20", the 48px "Apply" button beside it, and the error on one line with its (!) icon: "“SAVE20” isn’t a valid promo code. Check the spelling." The 18px "Promo code (optional)" section heading and the WELCOME10 hint sit above. No discount is applied (the summary row stays hidden — confirmed in G65 step 26).

S39

final: promo code accepted (field + summary)

index.html?step=3 · 1280×800 · fill #promo "welcome10" · click #promo-apply · scroll-to #step-3 .promo

1 screen(s), looked at when taken; not kept once the round was done — a round keeps one full-page walk per width.

L39 saw

Final, desktop, step 3 after typing "welcome10" + Apply, viewport scrolled to the promo. The code was upper-cased to "WELCOME10", the field went read-only (warm off-white fill), the button reads "Remove", and a green check line says "WELCOME10 applied: 10% off your first box (−$5.99)." The sticky summary shows the new "Promo [WELCOME10] −$5.99" row in green, Tax (8%) $4.32, "Total today $58.27", and still "Then $64.74 a week" for the renewal. Card fields above are empty (only the promo was driven), the billing tile ticked with the address preview.

S40

final: billing fields revealed

index.html?step=3 · 1280×800 · element #step-3 fieldset:nth-of-type(2) · click .choice:has(#billing-same)

1 screen(s), looked at when taken; not kept once the round was done — a round keeps one full-page walk per width.

L40 saw

Final close-up of step 3's Billing address group after one click on the tile: box unticked (grey outline, white), and the billing fields revealed beneath — Billing address line 1, Billing address line 2 (optional), City / State (Select) / ZIP code — with the same 1px grey boundaries, bold labels and rails as the delivery fields. The first click works (the D8 dead-click is gone; G65 step 23 confirms it right after a corrected field).

S41

final: confirmation after Place order

index.html?step=4 · 1280×800 · full page, 2 screen(s) · click .choice:has(#terms) · click #step-4 button[type=submit]

2 screen(s), looked at when taken; not kept once the round was done — a round keeps one full-page walk per width.

L41 saw

Final desktop confirmation after ticking the terms and pressing Place order (2 screens of an 820px page). The tracker and form are gone; a centred card shows the green check-circle, "Thanks — your first box is on its way." on one line, "Your weekly plan is set up. Here’s what we have on file.", and the facts table: Order number PC-20417, First delivery Tuesday, October 13, 2026, Plan 2 people · 3 meals a week, Delivering to "1450 Alder Street, Apt 3, / Portland, OR 97214". The grey note says the demo sent and charged nothing. Summary still at the right (Total today $64.74), card tops aligned at y=143. Finished and calm; nothing clipped.

S42

final: phone step 3 with the order summary expanded

index.html?step=3 · 375×812 · full page, 3 screen(s) · click #summary-toggle

3 screen(s), looked at when taken; not kept once the round was done — a round keeps one full-page walk per width.

L42 saw

Final, phone 375, step 3 with the order summary expanded (3 screens). The bar reads "Hide order summary ^ $64.74" and the panel below it shows the item (box art, "2 people · 3 meals a week", "6 servings · $9.99 each"), None / Tue, Oct 13, Subtotal $59.94, Shipping Free, Tax $4.80, bold "Total today $64.74" and the renewal note, on the warm off-white surface, full bleed. Then the tracker (two checks, Payment current) and the Payment card: stacked card fields, the ticked billing tile with "1450 Alder Street, Apt 3, / Portland, OR 97214", the 18px Promo code section with input + Apply, Back / Continue, footer. All within the 16px gutters.

S43

final: focus-gate check, 2 Tabs at 1280x720

index.html · 1280×720 · press Tab · press Tab

1 screen(s), looked at when taken; not kept once the round was done — a round keeps one full-page walk per width.

L43 saw

Final repeat of the focus-gate check: 1280x720 viewport after two Tabs. Focus is on the "Vegetarian" tile (3px dark-green ring), which the browser scrolled to viewport y≈334; the plan cards sit ABOVE it (card 1 almost scrolled off the top, cards 2-3 at y≈66-237), the select and Continue below. Focus moved down the page from the plan radio to the first diet tile, in reading order — the "reverse" in G55 is the post-scroll viewport coordinate again.

N23false-positiveabout G55

Same as G7 (N1/N2): '[reverse] Focus moved up by 78px (from … input.plan__input at y=412 to … input.choice__input at y=334)'. S43, taken this round at the gate's own 1280x720 after two Tabs, shows the focused Vegetarian tile at viewport y~334 BELOW the plan cards, which have scrolled up past the top; in page coordinates focus moves from ~412 down to ~801. DOM order equals visual order and the page sets no tabindex on any control.

N24false-positiveabout G59

Same inventory as G9 (N5): the 13 clipped-content warnings are the 1px visually-hidden comma spans inside the plan cards (the sr-only pattern check integrity G40 exempts as 'visually-hidden (sr-only) pattern'), and the one dead scrollport is the 3-row delivery-instructions textarea, whose default overflow:auto is correct. No page-overflow-x at 1280.

N25false-positiveabout G60

At 375x812 the inventory is identical to G59: the same 13 sr-only comma spans and the textarea's default overflow:auto; no page-overflow-x, no scroll container expected on this page.

N26acceptedabout G61

unprobed-handler-types is the inventory reporting it pressed nothing; no pointer-only control. All six handler types it lists (change, click, focusout, input, keydown, submit) are driven for real by verify flow G65 (34/34) and the keyboard side by check interactions G62-G64.

N27false-positiveabout G64

Same as G36 (N17/N18): inert-control on #terms, whose only response to Space is its native checked state and paint. S29 shows Space ticking it, and G65 steps 31-33 depend on that state (error until ticked, then Place order succeeds).

done

Pantry Club checkout, four rounds. One static page (index.html + styles.css + app.js) with the brief's four steps, radio-card plans, checkbox tiles, delivery-day and state selects, grouped card number, MM/YY expiry, billing reveal, WELCOME10 promo, review with Edit links, terms, and the PC-20417 confirmation; a sticky desktop summary that becomes a disclosure below 960px; an error summary that takes focus and links to fields, with aria-invalid, aria-describedby, an icon and a thicker edge on every failing field; blur re-validation after the first attempt. Final gate set, all green or noted: integrity, design, composition, color (15 text controls over steps 1-3), a11y contrast (5 states), touch (3 states), check scroll (sticky 143->24px), breakpoints --sweep (320-1280 clean), grounding and interactions (3 states), and verify flow DONE 34/34 through the whole checkout. check copy covers all 10 required lines across four states (G41/G44/G45/G46) because 4 of them exist only in later steps. 16 defects, all verified: 12 found by eye (card rhythm, awkward wraps and orphans, misaligned rows, a stray header label, promo hierarchy, a confirmation overflow on phones the D11 fix had caused, tablet rails), 4 by gates (verify flow found the empty billing preview and a real dead click caused by blur re-validation moving the layout between mousedown and mouseup; check design found off-scale spacing twice). Tool findings: the focus gate compares post-scroll viewport y (false 'reverse' on any long form); interactions/grounding print values or empty strings as names; inert-control ignores a checkbox's native checked state; gates cannot open a local page with a query string or a file:// URL, so per-step runs needed a 127.0.0.1 static server.

R5 · independent review

reviewer
G69exit 0vlmkit check integrity index.html

verdict: CLEAN (0 fail, 0 warn, 45 exempted)

verdict clean · findings 0 · fails 0 · warns 0 · exempted 45 · viewports 3

output, 27 line(s), 4.4s
vlmkit check integrity
source: index.html

verdict: CLEAN (0 fail, 0 warn, 45 exempted)
  1280x800: 8 component(s), ink 96.3%, 67 text block(s)
  768x900: 8 component(s), ink 95.3%, 51 text block(s)
  375x700: 8 component(s), ink 94.9%, 54 text block(s)

No integrity defects detected.

Exempted candidates (the tool's call — audit the rule, not the page):
  - [text-collision] div.plans > label:nth-of-type(1) > span:nth-of-type(1) > span:nth-of-type(2) > span:nth-of-type(3) @1280: clipped away by div.plans > label:nth-of-type(1) > span:nth-of-type(1) > span:nth-of-type(2) > span:nth-of-type(3) (overflow is not visible) — its box still overlaps text below the clip, but no glyph is painted there
  - [text-collision] div.plans > label:nth-of-type(2) > span:nth-of-type(1) > span:nth-of-type(2) > span:nth-of-type(3) @1280: clipped away by div.plans > label:nth-of-type(2) > span:nth-of-type(1) > span:nth-of-type(2) > span:nth-of-type(3) (overflow is not visible) — its box still overlaps text below the clip, but no glyph is painted there
  - [text-collision] div.plans > label:nth-of-type(3) > span:nth-of-type(1) > span:nth-of-type(2) > span:nth-of-type(3) @1280: clipped away by div.plans > label:nth-of-type(3) > span:nth-of-type(1) > span:nth-of-type(2) > span:nth-of-type(3) (overflow is not visible) — its box still overlaps text below the clip, but no glyph is painted there
  - [text-clipped] div.plans > label:nth-of-type(1) > span:nth-of-type(1) > span:nth-of-type(2) > span:nth-of-type(3) @1280: visually-hidden (sr-only) pattern — 1px box, text for AT only
  - [text-clipped] div.plans > label:nth-of-type(1) > span:nth-of-type(1) > span:nth-of-type(3) > span:nth-of-type(1) @1280: visually-hidden (sr-only) pattern — 1px box, text for AT only
  - [text-clipped] div.plans > label:nth-of-type(1) > span:nth-of-type(1) > span:nth-of-type(3) > span:nth-of-type(3) @1280: visually-hidden (sr-only) pattern — 1px box, text for AT only
  - [text-clipped] div.plans > label:nth-of-type(1) > span:nth-of-type(1) > span:nth-of-type(4) > span:nth-of-type(1) @1280: visually-hidden (sr-only) pattern — 1px box, text for AT only
  - [text-clipped] div.plans > label:nth-of-type(2) > span:nth-of-type(1) > span:nth-of-type(2) > span:nth-of-type(3) @1280: visually-hidden (sr-only) pattern — 1px box, text for AT only
  - [text-clipped] div.plans > label:nth-of-type(2) > span:nth-of-type(1) > span:nth-of-type(3) > span:nth-of-type(1) @1280: visually-hidden (sr-only) pattern — 1px box, text for AT only
  - [text-clipped] div.plans > label:nth-of-type(2) > span:nth-of-type(1) > span:nth-of-type(3) > span:nth-of-type(3) @1280: visually-hidden (sr-only) pattern — 1px box, text for AT only
  - [text-clipped] div.plans > label:nth-of-type(2) > span:nth-of-type(1) > span:nth-of-type(4) > span:nth-of-type(1) @1280: visually-hidden (sr-only) pattern — 1px box, text for AT only
  - [text-clipped] span.plan__badge > span:nth-of-type(1) @1280: visually-hidden (sr-only) pattern — 1px box, text for AT only
  - [text-clipped] div.plans > label:nth-of-type(3) > span:nth-of-type(1) > span:nth-of-type(2) > span:nth-of-type(3) @1280: visually-hidden (sr-only) pattern — 1px box, text for AT only
  - [text-clipped] div.plans > label:nth-of-type(3) > span:nth-of-type(1) > span:nth-of-type(3) > span:nth-of-type(1) @1280: visually-hidden (sr-only) pattern — 1px box, text for AT only
  - [text-clipped] div.plans > label:nth-of-type(3) > span:nth-of-type(1) > span:nth-of-type(3) > span:nth-of-type(3) @1280: visually-hidden (sr-only) pattern — 1px box, text for AT only
  … 30 more
G70exit 0vlmkit check color index.html

verdict: CONSISTENT (0 finding(s))

verdict consistent · base #f6f3ee · bodyInk #1f1c19 · linkInk #ffffff · invisibleControls 0 · colorOnlyLinks 0 · unreadableColors 0

output, 25 line(s), 1.7s
vlmkit check color
source: index.html

verdict: CONSISTENT (0 finding(s))
  measured: 1 control(s), 0 link(s) in a text flow — from 162 visible box(es)

Palette (by painted area; reported, never judged — see the rejected candidates in the module docs)
  surfaces  4 distinct
    #f6f3ee   59.4%     1 el  
    #ffffff   37.1%    20 el  header.site-header>div.brand-bar
    #f4ecdc    2.0%     2 el  header.site-header>div.demo-notice
    #edf6f0    1.6%     1 el  div.plans>label.plan>input.plan__input
  ink       4 distinct
    #1f1c19   55.1%    42 el  div.demo-notice>div.wrap>p
    #6a635b   32.2%    27 el  div.summary-item__text>p.summary-item__meta>span.nw
    #57514a    9.0%     4 el  li.steps__item>span.steps__marker>span.steps__num
    #ffffff    3.7%     3 el  label.plan>span.plan__body>span.plan__badge
  marks     5 distinct
    #857d73   37.2%    64 el  ol#steps>li.steps__item>span.steps__marker
    #dcd5ca   35.2%    10 el  header.site-header>div.brand-bar
    #2d6a4f   14.4%    12 el  ol#steps>li.steps__item>span.steps__marker
    #e7e1d8    7.1%     4 el  div#summary-panel>div.summary-item
    #e6d9bf    6.1%     1 el  header.site-header>div.demo-notice
  base #f6f3ee, body ink #1f1c19, link ink #ffffff on 1 element(s)
G71exit 0vlmkit check a11y contrast index.html

inspected 81 · failures 0

output, 5 line(s), 1.6s
  vlmkit check a11y contrast
  html: examples/sites/checkout/index.html
  inspected 81 text-bearing element(s)
  ✓ 0 contrast failure(s)
  report: examples/sites/checkout/test-results/a11y-contrast/index-ca6a16e3/report.md
G72exit 0vlmkit verify flow index.html --flow flow.json

verdict: DONE (34/34 steps)

done true · passed 34 · total 34

output, 130 line(s), 3.5s
vlmkit verify flow
source: index.html

verdict: DONE (34/34 steps)

✓ step 1: Step 1: Continue with no delivery day shows the error summary and moves focus to it
    ✓ #errors-1 visible 
    ✓ #errors-1 focused 
    ✓ #errors-1 text~"Select a delivery day" 
    ✓ #day aria-invalid=true 
    ✓ #day aria-describedby=day-hint day-error 
    ✓ #day-error visible 
✓ step 2: Choosing a day re-validates the field and clears the summary
    ✓ #day-error count=0 
    ✓ #errors-1 hidden 
    ✓ #day-note text~"Monday, October 12" 
    ✓ #summary-panel text~"Mon, Oct 12" 
✓ step 3: The 4-person radio card updates the order summary
    ✓ #summary-panel text~"4 people · 3 meals a week" 
    ✓ #summary-panel text~"$107.88" 
    ✓ #summary-panel text~"$116.51" 
✓ step 4: A dietary preference shows in the summary
    ✓ #summary-panel text~"Vegetarian" 
✓ step 5: Continue moves to step 2, marks it current and focuses its heading
    ✓ #step-1 hidden 
    ✓ #step-2 visible 
    ✓ #step-2-title focused 
    ✓ #step-2-title text~"Delivery details" 
    ✓ #steps [data-step='2'] aria-current=step 
    ✓ #steps [data-step='1'] text~"completed" 
✓ step 6: Step 2: Continue with empty fields lists six errors and ties each message to its field
    ✓ #errors-2 focused 
    ✓ #errors-2 li count=6 
    ✓ #full-name aria-invalid=true 
    ✓ #full-name aria-describedby=full-name-error 
    ✓ #zip aria-invalid=true 
    ✓ #phone[aria-invalid] count=0 
    ✓ .field__error .icon count=6 
✓ step 7: An error-summary link moves focus to its field
    ✓ #email focused 
✓ step 8: Fill the name
    ✓ #full-name focused 
✓ step 9: Leaving the name field re-validates it (error gone, summary down to five)
    ✓ #full-name[aria-invalid] count=0 
    ✓ #full-name-error count=0 
    ✓ #errors-2 li count=5 
✓ step 10: Leaving a malformed email shows the format message
    ✓ #email-error text~"like name@example.com" 
✓ step 11: Correct the email
    ✓ #email focused 
✓ step 12: Address line 1
    ✓ #email-error count=0 
✓ step 13: City
    ✓ #address1-error count=0 
✓ step 14: State by typeahead
    ✓ #state-error count=0 
✓ step 15: ZIP code
    ✓ #city-error count=0 
✓ step 16: Delivery instructions count down from 200
    ✓ #instructions-count text~"179 characters left" 
    ✓ #zip-error count=0 
✓ step 17: Continue to step 3
    ✓ #step-3 visible 
    ✓ #step-3-title focused 
    ✓ #steps [data-step='3'] aria-current=step 
    ✓ #billing-preview text~"1450 Alder Street, Portland, OR 97214" 
✓ step 18: Step 3: Continue with no card lists four errors
    ✓ #errors-3 focused 
    ✓ #errors-3 li count=4 
    ✓ #card-number aria-invalid=true 
✓ step 19: Card number
    ✓ #card-number focused 
✓ step 20: Expiry (the slash is inserted as you type)
    ✓ #card-number-error count=0 
✓ step 21: Security code
    ✓ #expiry-error count=0 
✓ step 22: Name on card
    ✓ #cvc-error count=0 
✓ step 23: Unticking 'same as delivery' reveals the billing fields
    ✓ #billing-fields visible 
    ✓ #billing-address1 visible 
    ✓ #card-name-error count=0 
✓ step 24: Ticking it again hides them
    ✓ #billing-fields hidden 
✓ step 25: Type an unknown promo code
    ✓ #promo focused 
✓ step 26: An unknown promo code is rejected with a message
    ✓ #promo-message text~"isn’t a valid promo code" 
    ✓ #promo aria-invalid=true 
    ✓ #summary-promo hidden 
✓ step 27: Type WELCOME10 in lower case
    ✓ #promo focused 
✓ step 28: WELCOME10 takes 10% off and the summary follows
    ✓ #promo-message text~"WELCOME10 applied" 
    ✓ #promo[aria-invalid] count=0 
    ✓ #summary-promo visible 
    ✓ #summary-promo text~"$10.79" 
    ✓ #summary-panel text~"$104.86" 
    ✓ #promo-apply text~"Remove" 
✓ step 29: Continue to review
    ✓ #step-4 visible 
    ✓ #step-4-title focused 
    ✓ #step-4-title text~"Review your order" 
    ✓ #step-4 text~"Card ending 4242, expires 12/28" 
    ✓ #step-4 text~"1450 Alder Street" 
    ✓ #step-4 text~"WELCOME10" 
    ✓ #step-4 text~"Vegetarian" 
    ✓ #step-4 button[type=submit] visible 
    ✓ #step-4 button[type=submit] text~"Place order" 
✓ step 30: Edit on the Delivery block goes back to step 2
    ✓ #step-2 visible 
    ✓ #step-2-title focused 
✓ step 31: …and Continue returns straight to review
    ✓ #step-4 visible 
    ✓ #step-4-title focused 
✓ step 32: Place order without agreeing to the terms is stopped
    ✓ #errors-4 focused 
    ✓ #terms aria-invalid=true 
    ✓ #confirmation hidden 
✓ step 33: Agree to the terms
    ✓ #terms-error count=0 
✓ step 34: Place order replaces the form with the confirmation
    ✓ #confirmation visible 
    ✓ #step-4 hidden 
    ✓ #steps hidden 
    ✓ #confirmation-title focused 
    ✓ #confirmation-title text~"Thanks — your first box is on its way." 
    ✓ #confirmation text~"PC-20417" 
    ✓ #confirmation text~"Monday, October 12, 2026" 
    ✓ #summary-panel text~"$104.86"
S44

step 1 as a visitor lands

index.html · 1280×800 · full page, 2 screen(s)

S44 screen 1S44 screen 2
L44 saw

Desktop, step 1 as a visitor lands (2 screens). Screen 1: beige demo notice, white header with the green bowl logo, a four-step stepper with step 1 ringed in green, and a white form card: "Choose your plan" h1, a two-line lede, "Box size", three radio cards. The selected card has a green border and a pale green fill; the "Best value" badge sits on the second card's top border, left-aligned with its text. Prices are right-aligned and bold with "a week" under them. The order summary card on the right lines up its top with the stepper and itemises plan, dietary, first delivery ("Pick a day"), subtotal, shipping in green, tax and total. Screen 2: the summary stays pinned 24px from the top while the form scrolls; the 2x2 dietary tiles, the delivery-day select with its hint, a rule and a right-aligned Continue close the card; the footer note sits alone below. Nothing collides or truncates; the column edges line up.

S45

step 1 as a visitor lands

index.html · 375×812 · full page, 3 screen(s)

S45 screen 1S45 screen 2S45 screen 3
L45 saw

Phone, same state (3 screens). The notice wraps to three lines, then the header, then a full-width "Show order summary" bar carrying the $64.74 total, so the price is visible before anything is expanded. The stepper's four labels fit on one row. Plan cards reflow to people / meals / servings / per-serving on four lines with the price on the right; dietary tiles go single column at full width; Continue spans the card. The dietary hint wraps early ("…weekly menu / around these.") — that is the stylesheet's text-wrap: pretty avoiding a one-word last line, not a width bug. Screen 3 is the flush end: the fictional-brand footer. No horizontal overflow, no clipped text.

S46

step 1, Continue with nothing chosen (phone)

index.html · 375×812 · click #step-1 button[type=submit]

1 screen(s), looked at when taken; not kept once the round was done — a round keeps one full-page walk per width.

L46 saw

Phone, Continue pressed with no delivery day. The error summary took focus and scrolled to the top of the screen: a pale red box with a red left bar, an alert icon, "1 field needs your attention" and one underlined link, "Select a delivery day", all inside a dark green focus ring. The error is impossible to miss and names the fix. The box sits about 10px from the top edge, so the ring nearly touches it — a scroll-margin would give it room, but nothing is cut off. The plan cards below keep their selected state.

S47

confirmation

index.html?step=done · 1280×800 · full page, 2 screen(s)

2 screen(s), looked at when taken; not kept once the round was done — a round keeps one full-page walk per width.

L47 saw

Desktop confirmation (?step=done, 2 screens; the second is the same view 20px lower because the page is 820px tall). A green check, "Thanks — your first box is on its way.", a line of context, then a bordered table: order number PC-20417, first delivery Tuesday, October 13, 2026, plan, and the delivery address on two lines. The receipt sentence says plainly that nothing was sent or charged. The summary card now reads "Tue, Oct 13" where it said "Pick a day", so the two agree. Balanced, calm, nothing misaligned.

N28decision

Independent review: I re-ran integrity, color, contrast and the 34-step verify flow (all green), and looked at step 1 at both widths, the phone error state and the confirmation. I found nothing to raise as a defect; the only nit is the error summary scrolling flush to the top edge on a phone (S46), which I leave as an observation.

done

Reviewer round: the builder's final state holds up — gates green on re-run, and four fresh shots show nothing the builder's log missed.