# 07 — UAT report (v2)

**Date:** 2026-08-24 · **Build reviewed:** `05-build.html` → `docs/index.html`
(226 KB, self-contained, 0 subresource requests)
**Gate 2 state:** `pending` — closes at the design review's visual review, then Đức's.
**Reviewed against:** the v1.1 schema, including the two new blocks for public
deliverables — the three-second test and the Craft ≥8 floor.

## Mechanical pre-pass

`static-lint` slot unfilled and the linter is network-dependent, so it was **skipped** —
recorded rather than silently omitted. Everything below was measured in a real browser at
five viewports.

A **dead-code sweep** was run in its place: every declared custom property and utility
class was checked for at least one reference. Two orphans found and deleted (`--mark-wash`,
`.prose`). Two kept and documented (`--s1`, `--s7` — the ends of a complete 8px scale).

---

## Three-second test — **PASS** *(new hard-floor row, public deliverables)*

**Frame judged:** `screens/07-first-viewport-1905.png` — the first viewport only, at
1905×937. Not the full-page capture.

**Cold-eye verdict: invested product page.** In three seconds the frame resolves as: a
dark editorial site with an 80px headline, one word in red, three photographed paper
specimens fanned on the ground with a red `GATE 1` stamp overhanging them, and a ruled
eight-cell index across the bottom with two cells picked out in red. There is colour,
there is an object, there is scale contrast, and there is something in it that could not
have come out of a default stylesheet.

**The comparison that matters.** v1's frame — same crop, reproduced on the page itself —
resolves as a warm sheet ruled into a hairline grid with small text in the cells, which is
what its owner read as a wireframe. Put side by side, the two frames are not close. That
comparison is now embedded in the page as evidence rather than kept in a report.

**Mobile.** Applied at 390 as well (`screens/07-mobile-390-first-viewport.png`): passes.
The headline holds three lines at 34px, the plate stack keeps its overlap and its stamp,
and the CTAs land above the fold.

**Honest caveat.** The weakest region of the frame is the upper-right quadrant — roughly
700×300px of empty ground between the nav and the plate stack that the headline reaches
into but does not fill. It reads as composed asymmetry rather than as a hole, but it is
the part a second reviewer is most likely to argue with.

---

## Hard floor — PASS

Measured on computed sRGB read through a canvas, at every viewport. Not asserted.

| Check | Result | Evidence |
| --- | --- | --- |
| Body ≥14px | PASS | body 17px · lead 18px · step body 15.5px · rig rule 14.5px |
| Labels/captions ≥12px | PASS | a full sweep of every text-bearing element reports **smallest = 12px**, 0 violations |
| Text contrast ≥4.5:1 (3:1 large) | PASS | swept **every** text-bearing element on the page, not a hand-picked list: **0 failures**. Lowest ratio 4.66 |
| Focus visible, full keyboard walk | PASS | 21 stops, `outline: solid 2px` accent at 3px offset on every one, DOM order matches the components contract |
| Dialogs | n/a | none on the page — and the one candidate (a lightbox) was refused in the motion spec rather than half-built |
| `prefers-reduced-motion` | PASS | 0 elements hidden; every page entrance dropped; explanatory note shown |
| Declared UI states exist | PASS | all 8 reachable states implemented; the 6 product states are declared unreachable in `components.md` with a reason |
| No horizontal overflow | PASS | 390=390 · 768=768 · 1024=1024 · 1440=1440 · 1905=1905 |
| Console errors | PASS | 0 at all five viewports; 0 page errors during rig interaction |
| No dead controls | PASS | install commands carry an explicit `PLACEHOLDER` state rather than a fake command; all 21 stops do something |
| Real logos / no CSS silhouettes | PASS | no brand depicted; four upstream projects credited as plain repo paths |
| Meaning never by colour alone | PASS | gates carry a `GATE` stamp **and** a ground inversion; toggles carry `aria-pressed` / `aria-checked` **and** a fill; the rejected plate carries a `REJECTED` stamp **and** a caption |
| Images have alt text | PASS | 5/5, each describing what the frame shows |
| Landmarks / single h1 | PASS | 1 header · 1 nav · 1 main · 1 footer · 1 h1 |
| Works with JavaScript disabled | PASS | 0 elements hidden; rig A still fully functional (pure CSS); rigs B–D inert with their explanations intact |
| Print | PASS | 0 elements hidden under `@media print` |

### Full contrast measurements

| Element | Size | Ratio | Needs |
| --- | --- | --- | --- |
| body copy | 17px | 9.19 | 4.5 |
| lead | 18px | 9.19 | 4.5 |
| hero sub | 19px | 9.19 | 4.5 |
| mark word, hero | 80px | 5.52 | 3.0 |
| mono kicker | 12px | 5.52 | 4.5 |
| nav link | 12px | 6.12 | 4.5 |
| spine name | 12px | 6.12 | 4.5 |
| spine gate name | 12px | 5.52 | 4.5 |
| step numeral | 46px | 4.73 | 3.0 |
| step body | 15.5px | 9.19 | 4.5 |
| artifact label | 12px | 6.12 | 4.5 |
| gate numeral (paper) | 46px | 6.03 | 3.0 |
| gate body (paper) | 15.5px | 7.80 | 4.5 |
| gate artifact (paper) | 12px | **4.66** | 4.5 |
| gate stamp | 12px | 8.43 | 4.5 |
| rig letter | 12px | 4.96 | 4.5 |
| rig rule | 14.5px | 8.24 | 4.5 |
| rig label | 12px | 6.52 | 4.5 |
| spec count, accent | 26px | 4.96 | 3.0 |
| file list (paper) | 13px | 5.00 | 4.5 |
| file gate mark (paper) | 13px | 6.47 | 4.5 |
| title block (paper) | 12px | 5.00 | 4.5 |
| note body (paper) | 15.5px | 8.36 | 4.5 |
| plate caption (paper) | 12px | 5.00 | 4.5 |
| plate stamp | 12px | 8.43 | 4.5 |
| code | 13px | 17.86 | 4.5 |
| prompt mark | 13px | 5.88 | 4.5 |
| placeholder pill | 12px | 5.52 | 4.5 |
| footer acknowledgement | 14.5px | 9.78 | 4.5 |
| fine print | 12px | 6.52 | 4.5 |

Two values were changed during the loop to reach this table, not after it: the on-stage
accent was lifted from `L 0.635` to `L 0.660` (the rig letter measured 4.47 against a
raised panel), and the step numeral got its own token at `L 0.600` rather than reusing the
`stage-600` hairline value, which measured 2.3:1 — a genuine failure that a hand-picked
contrast list would have missed, and the reason the sweep now walks every element.

### Viewport evidence

`screens/07-first-viewport-1905.png` · `07-full-1905.png` · `07-tablet-768.png` ·
`07-mobile-390.png` · `07-mobile-390-first-viewport.png` · `07-nojs-1905.png` ·
`07-reduced-motion-1905.png` · `07-vi-first-viewport-1905.png` · `07-vi-full-1905.png`

Mobile is composed rather than squeezed: section headers stack, step rows drop the leader
rule and move artifact names under the body, the plate stack keeps its overlap at a
smaller scale, the spine becomes 2×4, and the rigs go single-column at full width.

### Bilingual evidence

Toggling to Vietnamese sets `documentElement.lang="vi"`, moves `aria-pressed` to
`[false, true]`, and renders
`Agent của bạn đã biết viết UI. Đây là thứ dạy nó thiết kế.` Every new v2 string — the
four rig captions, the spec card, the whole v1→v2 story including the owner's quote — is
translated, not degraded. Diacritics render in the system stack with no font download.

---

## Scores

| Dimension | Score | Evidence |
| --- | --- | --- |
| **Concept** | **8** | *The stage is slate; wherever a human decides, the page turns to paper.* The motif does structural work — two full-bleed ground inversions at the gate rows, a full paper band for the deliverables, five paper specimens on the slate — and it composes the chosen and rejected directions into one narrative instead of replacing one with the other. Not a 9: the human-decision-point half would survive a neighbouring human-in-the-loop product. The half that could not be swapped is the artifacts-as-specimens half, and it carries less of the page |
| Direction consistency | **9** | Delivers direction B's stated language — deep slate ground, oversized confident type, full-bleed stacked bands — while refusing, in writing and in code, each of the two flagged defaults B originally carried. The cyan was deleted rather than tuned; there is no gradient and no glow token anywhere in the system |
| Visual hierarchy | **9** | Heading-to-body 4.7× at desktop against a 2.5× floor. Squint test gives five clear levels: display → section heading → step title → body → mono label. The two paper inversions are the loudest thing on the page and they mark the page's one structural claim |
| **Craft** | **8** | One 8px scale, three hue families, two type families, near-zero radius throughout, every contrast pairing measured rather than eyeballed, zero orphan tokens after the sweep, 0 overflow and 0 console errors at five widths. **At the floor, not above it** — see the three unfixed items below |
| Function | **8** | Every element earns its place; no invented social proof; the placeholder state is modelled honestly. The live motion section is the page's biggest bet — it is four working examples where a paragraph would have done, which is the point, but it is also the largest block a visitor can skip |
| Originality | **8** | Deep slate with a warm drafting red and photographed paper specimens is genuinely uncommon for a developer-tool page, and it is the inverse of the dark-neon genre the reference site sits in. Rarer still: publishing your own rejected build, at the same crop, as the argument for the process |
| **Overall** | **8.3** | **Excellent**, lower end. Concept 8, so no veto. Craft 8, so the public-deliverable block clears — narrowly |

```
            Concept 8
                |
 Original 8 ----+---- Direction 9
         \      |      /
          \     |     /
 Function 8 ----+---- Hierarchy 9
                |
             Craft 8
```

## Keep

- **The paper inversion at the gates.** It is the concept, it is cheap, and it happens
  exactly twice — which is why it still lands the second time.
- **The hero plate stack.** It is the thing that makes the first three seconds work, and
  it is made entirely of real artifacts. No stock, no illustration, no filler.
- **The live motion section.** A page arguing that motion must be earned, demonstrating
  four of its own rules, with the values printed on screen as labels.
- **The v1→v2 exhibit.** Publishing the rejected build beside the wireframe at the same
  crop is the strongest evidence on the page and the most honest thing on it.
- **The leader rule** connecting each step to the artifact it leaves on disk — v1's one
  device worth transplanting, and it now has a job rather than a style.
- **Vietnamese as a real second language**, including the owner's quote.

## Fix

Ordered by severity. All **blocking** items were fixed and verified inside this loop.

**1. Step numerals at 2.3:1** — *blocking · FIXED*
- Now: `--stage-600` (a hairline token) reused as the colour of the large step numerals.
  Measured 2.3:1 against the stage, failing even the 3:1 large-text floor.
- Why it matters: the numerals carry the step order, which is content, not decoration.
- Fix: a dedicated `--stepn` token at `L 0.600`. Re-measured 4.73:1.
- Found by sweeping every element rather than a curated list — the curated list did not
  include it. The sweep is now the method.

**2. Accent below 4.5:1 on raised panels** — *blocking · FIXED*
- Now: the rig letters (accent on `stage-800`) measured 4.47:1.
- Fix: on-stage accent lifted `L 0.635 → 0.660`, chroma unchanged so it stays a drafting
  red rather than becoming neon. Re-measured 4.96:1, and every other accent pairing rose
  with it.

**3. The reduced-motion note could never appear** — *blocking · FIXED*
- Now: `@media (prefers-reduced-motion) { .rm-note { display:block } }` sat *above* the
  base `.rm-note { display:none }` rule. Equal specificity, so the later base rule always
  won and the note was invisible under the exact preference it exists to serve.
- Fix: the override now sits adjacent to the rule it overrides. Verified: `display: block`
  with the preference set. This is the CSS-collision trap the taste reference warns about,
  and it produced a silent accessibility failure rather than a visible bug.

**4. A keyframe animation fired unasked at page load** — *important · FIXED*
- Now: rig D's "return" keyframe was bound to `[aria-checked=false]`, which is the initial
  state, so it played once on every load — unsolicited motion on a rig whose whole premise
  is that it is press-only.
- Fix: gated behind a `.touched` class set on first interaction.

**5. Images distorted by their own aspect-ratio attributes** — *important · FIXED*
- Now: `width`/`height` attributes present for layout reservation, but no `height: auto`
  in CSS, so the attribute height won and the plates squashed.
- Fix: `img { height: auto }`, and the attributes corrected to the real delivered
  dimensions.

**6. Every section header left the right half of its band empty** — *important · FIXED*
- Now: kicker, heading and lead stacked in one column, leaving roughly 600px of nothing to
  the right of every band. This is the same even, unfilled rhythm v1 was rejected for,
  reproduced at a smaller scale.
- Fix: section headers run two columns — heading left, lead right. The motion header
  additionally carries a spec card quoting this page's own accepted/refused counts.

**7. Dead span between a step's body and its artifact name** — *important · FIXED*
- Now: a three-column step row put the artifact filename at the far right margin with
  ~280px of nothing between it and the body text.
- Fix: a fourth column holding a leader rule that draws itself on scroll, connecting the
  two. The void became the device.

**8. The hero overflowed the review frame** — *important · FIXED*
- Now: at 92px the headline broke to four lines and pushed the CTAs and the step spine
  below 937px — so the composed first viewport was not the first viewport anyone saw.
- Fix: display capped at 80px, hero paddings and the plate stack retuned. The full hero
  now resolves inside 937px with ~70px to spare.

**9. Upper-right quadrant of the hero is empty** — *polish · NOT FIXED*
- ~700×300px of ground between the nav and the plate stack. Defensible as composed
  asymmetry; flagged because it is the first thing a second reviewer will point at, and
  because the whole rework exists to answer a complaint about emptiness.

**10. The story section's evidence column bottoms out above its prose** — *polish · NOT FIXED*
- The two evidence plates end roughly 90px above the end of the text beside them. Cosmetic,
  contained, and left deliberately so this report reflects what shipped.

**11. The motion heading rags to a one-word last line** — *polish · NOT FIXED*
- "running." sits alone. It matches the hero's rag and reads as emphasis rather than an
  orphan, but it is a judgement call and a reviewer may disagree.

## Quick wins

If there are only five minutes before publishing:
- [ ] Pull the plate stack up and left by one step so it closes the hero's upper-right gap.
- [ ] Give the story's evidence column a third plate — direction C, or the v2 first
      viewport — so the two columns bottom out together.
- [ ] Widen `.bandhead h2` to 24ch so the motion heading resolves in two lines.

## Motion review

Run against the motion playbook's own bar, including the v1.1 marketing branch. **Approve.**

No automatic blocks present: no `transition: all`, no `scale(0)` entrance, no `ease-in` on
UI, no layout-property animation, no keyframes on rapidly-fired elements, no ungated hover,
no missing reduced-motion path, no parent custom property driving child transforms.
Everything animates `transform`/`opacity` only.

Every response to an interaction is ≤220ms. The four values above 300ms (620/720/560/460ms)
are load and scroll entrances on a once-per-visitor surface, which the v1.1 branch puts on
the delight budget explicitly, each with a written justification.

**The one deliberate exception, argued rather than assumed:** the four rigs keep animating
under `prefers-reduced-motion: reduce`. They are user-initiated and a visible note in that
section says so in both languages. The alternative is four dead boxes with no explanation.
Recorded in `06-motion-spec.md` with the one-rule reversal if the reviewer disagrees.

One finding, non-blocking: `ease-in` appears once on the page, on purpose, as rig A's
labelled counter-example. It is the only `ease-in` in the file and it is captioned as the
wrong answer. A linter would flag it; a reader learns from it.

## Unverified

- **Feel.** Every timing judgement here comes from values and still frames. The load
  choreography (headline 90ms apart, plates 110ms apart) and whether rig B's curve
  difference is actually legible at 520ms both need a human watching, on a cold cache.
- **Real devices.** No physical device testing. Touch targets are sized by rule, not tried;
  the plate stack's overlap at 390px is verified visually but not by thumb.
- **Cross-browser.** Chromium only. `oklch()`, `color-mix()`, `backdrop-filter` and the
  `translate` longhand are the four features worth checking elsewhere. None was observed
  failing over, and none was observed succeeding elsewhere either.
- **Static lint.** Skipped, offline. A dead-code sweep was run instead.
- **The three-second test with an actual cold eye.** Simulated per the schema's fallback
  and written down verbatim, including the caveat. It is still a simulation, and this
  specific check is the one where a simulation is worth least — which is exactly why the
  gate is a human's.

## Gate 2

`pending` — awaiting the design review's visual review, then Đức's.

The page must not publish before a person has looked at the first viewport. That is the
whole lesson of v1.

---

## Process feedback — running the patched pack on the failure that produced the patches

1. **The three-second test changes what you build, not just what you check.** Knowing the
   first viewport would be judged alone made the hero a composition problem with a fixed
   height budget rather than "the top of a long page." Three of the eleven findings above
   are hero-geometry findings that v1's process would never have surfaced.
2. **A curated contrast list is not a contrast check.** The two blocking colour defects
   were both in elements a hand-written list omitted. The schema should say to sweep every
   text-bearing element programmatically, because the ones you forget to list are exactly
   the ones you forgot to think about. *Candidate for v1.2.*
3. **"Quiet is not bare" caught a live defect in a loud direction.** The empty right half
   of every section header is the same failure as v1, at one-sixth the scale, on a page
   that looks nothing like v1. The rule generalises better than the example it came from.
4. **The marketing branch needs a companion warning.** Spending the delight budget invites
   a motion section, and a motion section invites motion that is *about* motion rather than
   motion that teaches. The discipline that saved it was requiring each rig to name the
   playbook rule it proves — one rig was cut for failing that. Worth writing down.
   *Candidate for v1.2.*
5. **A rejected direction kept on disk is worth more than a rewritten one.** Promoting B
   cost hours, not days, because the render, the score, and the reason for rejection were
   all still there — including the sentence naming what would have to change if it were
   ever promoted. The gate file's job is not only to record a decision; it is to keep the
   losing options *executable*. `direction-gate.md` should say so.
