diff --git a/docs/TOOLS.md b/docs/TOOLS.md index 75860e8..235cd9e 100644 --- a/docs/TOOLS.md +++ b/docs/TOOLS.md @@ -227,7 +227,7 @@ still report a scrollWidth equal to the viewport. Eight kinds of finding, of which `clipped`, `past_viewport` and `document_scrolls` are blocking. Exit 0 clean, 1 findings, 2 nothing swept. -It exists because #214 — the header CTA clipped at iPad portrait — was fixed, +It exists because #214, the header CTA clipped at iPad portrait, was fixed, checked in a desktop window sized to 768, and released still broken. A desktop window at 768 has a scrollbar, so the layout viewport was ~753px and the `md` breakpoint the fix was about never engaged. A device profile has no scrollbar diff --git a/docs/design/OVERHAUL_PLAN.md b/docs/design/OVERHAUL_PLAN.md index ac1b44b..0089b5f 100644 --- a/docs/design/OVERHAUL_PLAN.md +++ b/docs/design/OVERHAUL_PLAN.md @@ -826,7 +826,7 @@ The build refuses copy that breaks any of this: see `scripts/lib/content.js`. - Mobile section padding: `py-16` (64px) - Desktop section padding: `py-24` (96px) - Card padding: `p-4 md:p-6` -- **Standalone link lists: `space-y-4`, not `space-y-2`** — see the tap-target +- **Standalone link lists: `space-y-4`, not `space-y-2`.** See the tap-target rule below. This is a change from what the rest of this file describes, made 2026-09-10; it affects the footer columns, the privacy contents and the related-links lists. @@ -839,7 +839,7 @@ takes those 8px back out of the layout, so the line the link sits on does not move. **It only works if the list leaves 16px between rows**: the negative margin does not shrink the box, only its effect on layout, so at `space-y-2` each link's box reached 8px into a gap its neighbour was already reaching 8px -into. They overlapped, and `getBoundingClientRect` still read 33px — a target +into. They overlapped, and `getBoundingClientRect` still read 33px: a target that measured right and was not there. Three rules follow from that: @@ -860,7 +860,7 @@ Three rules follow from that: 768 to 1023 gets the `Sheet` menu. The desktop row cannot fit 768: brand, six nav links and the CTA want 787px of natural width against 736px of container, so flex shrank the CTA and wrapped its label and it *still* overflowed. The -menu is the better tablet experience regardless — 44px rows instead of 17px +menu is the better tablet experience regardless: 44px rows instead of 17px ones, and the Services and Industries submenus are reachable, where the desktop dropdowns open on hover and a touch device has no hover. diff --git a/docs/history/DEVELOPMENT_LOG.md b/docs/history/DEVELOPMENT_LOG.md index 6ee2bd6..c7be14f 100644 --- a/docs/history/DEVELOPMENT_LOG.md +++ b/docs/history/DEVELOPMENT_LOG.md @@ -46,10 +46,10 @@ sequence would have implied more. ## Entries -### 2026-09-10 — A device sweep, and a fix that was released without ever being exercised +### 2026-09-10: a device sweep, and a fix that was released without ever being exercised -Every page rendered on ten emulated phones and tablets — real Playwright device -profiles rather than a resized window — and measured. **190 page loads, 21 +Every page rendered on ten emulated phones and tablets, on real Playwright +device profiles rather than a resized window, and measured. **190 page loads, 21 blocking and 530 high findings** on pages that had already passed Batch 11 and Batch 16. Three defects, all live in production. Filed as #238 to #241 under Batch 19. @@ -76,7 +76,7 @@ of being quietly squeezed past the edge. **#239, the Cisco mark.** It ships in a 700x700 canvas it fills 66% of, so `object-contain` rendered it small beside 8x8's and both pages compensated with -`scale-[1.5]` and `scale-[2]` inside `overflow-hidden` — cutting 13px off the +`scale-[1.5]` and `scale-[2]` inside `overflow-hidden`, cutting 13px off the trademark on `/` and 24px on `/about`. Cropping the asset's own `viewBox` to the artwork and dropping the scale renders it at 62x46 on `/`, the same size as the 8x8 logo beside it, with nothing clipped. @@ -90,8 +90,8 @@ whichever painted last. Both versions measure 33px and neither is 33px. **A target that measures right and is not there is worse than one that measures wrong.** The rule that survived is in `design/OVERHAUL_PLAN.md`: `.tap-target` is `inline-block`, and every list that uses it moves to `space-y-4`. Then a -second surprise — a 39x17 link grown to 39x36 stops being row-shaped and starts -being judged as a compact target wanting 44px — so footer links also take +second surprise: a 39x17 link grown to 39x36 stops being row-shaped and starts +being judged as a compact target wanting 44px, so footer links also take `block` and the whole row becomes the target. **#241 is the instrument.** `scripts/device-sweep.mjs` plus @@ -103,7 +103,7 @@ the viewport while content is sliced off the right edge. Not wired into a guard that cannot run on a clean clone is one that gets skipped. Worth recording that the original in the other repository declares device profiles and then only calls `setViewportSize`, so its `isMobile` and `deviceScaleFactor` -never take effect — it is a width sweep wearing a phone's clothes, which is the +never take effect. It is a width sweep wearing a phone's clothes, which is the same blind spot in a different form. **Proven, not assumed.** After the fixes the sweep reports zero blocking and @@ -112,7 +112,7 @@ sweep reported it again: `clipped x1` and `media_overflow x1` for the logo, `past_viewport x19` for the header, `touch_target x342` for the footer. The first attempt at the header proof reverted only the nav's breakpoint and left the CTA at `lg`, so the element that overflows was not on screen and the sweep -correctly reported nothing — a wrong mutation, not a blind checker, and worth +correctly reported nothing. A wrong mutation, not a blind checker, and worth writing down because it looks identical to a checker that has stopped working. Exit codes were proven separately: 2 for an unreachable origin and for an unknown `--devices` name, because "nothing was swept" must never read as diff --git a/docs/qa/ClaudeQAPlan.md b/docs/qa/ClaudeQAPlan.md index 08237f3..210c33c 100644 --- a/docs/qa/ClaudeQAPlan.md +++ b/docs/qa/ClaudeQAPlan.md @@ -99,8 +99,8 @@ and both were invisible to the instrument being used. **A window sized to 768 is not 768.** It has a scrollbar, so the layout viewport is about 753, so the `md` breakpoint never engages and the desktop layout the -check is about is never on screen. That is how #214 — the header CTA clipped at -iPad portrait — was fixed, checked at "768", released, and was still 25px past +check is about is never on screen. That is how #214, the header CTA clipped at +iPad portrait, was fixed, checked at "768", released, and was still 25px past the right edge on every page. A device profile has no scrollbar inset. Check a breakpoint on a device, or on an emulated one; never on a window you dragged. diff --git a/scripts/device-sweep.mjs b/scripts/device-sweep.mjs index 5310309..d32917e 100755 --- a/scripts/device-sweep.mjs +++ b/scripts/device-sweep.mjs @@ -7,7 +7,7 @@ // node scripts/device-sweep.mjs --devices "iPhone SE,iPad Mini" // node scripts/device-sweep.mjs --report /tmp/sweep.md --shots /tmp/sweep // -// Exit codes: 0 nothing found. 1 findings. 2 NOTHING WAS SWEPT — playwright +// Exit codes: 0 nothing found. 1 findings. 2 NOTHING WAS SWEPT: playwright // missing, chromium unlaunchable, or no sitemap. Two is not a pass. // // ## Which incident motivated it @@ -171,8 +171,8 @@ if (!loads) { process.exit(2) } -// The same defect on six devices is one defect. Group on what identifies it — -// route, element, text — and keep the device list, because "only iPad Mini" +// The same defect on six devices is one defect. Group on what identifies it +// (route, element, text) and keep the device list, because "only iPad Mini" // versus "all ten" is the difference between a breakpoint bug and a layout bug. const groups = new Map() for (const finding of findings) { @@ -203,7 +203,7 @@ if (REPORT) { for (const row of group) { lines.push( `- **${row.route}** ${row.kind}: ${row.says || ''}`, - ` - \`${row.path}\`${row.text ? ` — text: ${JSON.stringify(String(row.text).slice(0, 60))}` : ''}`, + ` - \`${row.path}\`${row.text ? `, text: ${JSON.stringify(String(row.text).slice(0, 60))}` : ''}`, ` - on ${[...row.devices].join(', ')}`, row.detail ? ` - \`${JSON.stringify(row.detail)}\`` : '', ) diff --git a/scripts/lib/css-audit.js b/scripts/lib/css-audit.js index d2086a1..0584cb7 100644 --- a/scripts/lib/css-audit.js +++ b/scripts/lib/css-audit.js @@ -6,7 +6,7 @@ // // It measures rather than guesses. Every box is compared against its nearest // CLIPPING ancestor instead of document.scrollWidth, which lies the moment any -// container carries overflow-x: hidden or clip — and this site's body does, so +// container carries overflow-x: hidden or clip, and this site's body does, so // a page can slice content off its right edge and still report a scrollWidth // equal to the viewport. That is exactly how the header CTA at iPad portrait // survived a fix and a release. @@ -21,7 +21,9 @@ // where the thresholds were argued out and where the comments explaining each // one were written. Copied rather than shared because the two repositories have // no common package; if a threshold changes in one, it does not change in the -// other. The driver here differs from that one in a way that matters: css-qc +// other. The logic is that file's, unchanged. The prose is not byte-identical: +// this repository does not use em dashes, so the comments and the two report +// strings were repunctuated. Diff it on words, not bytes. The driver here differs from that one in a way that matters: css-qc // declares Playwright device profiles but only ever calls setViewportSize, so // its deviceScaleFactor, isMobile and hasTouch fields never take effect and it // is a width sweep wearing a phone's clothes. @@ -74,7 +76,7 @@ export const audit = function audit() { /** * Inside a closed disclosure, and therefore not on screen at all. * - * Chrome does not `display: none` a closed `
` — it skips the + * Chrome does not `display: none` a closed `
`. It skips the * subtree with `content-visibility`, and the descendants keep reporting * layout boxes at their unconstrained size. The signature form in the * documents table measured 149px wide at x=255 on a 320px screen while the @@ -149,7 +151,7 @@ export const audit = function audit() { if (scrollerOf(el)) continue; // Contained by something that clips: the reader does not see this past the - // edge, they see it cut off — which check 2 reports, with the clipper named. + // edge, they see it cut off, which check 2 reports, with the clipper named. // Reporting it here as well was the single largest source of noise in the // first run: every `truncate` in the admin has a child span whose rect runs // off the viewport by design, ellipsis and all. @@ -223,7 +225,7 @@ export const audit = function audit() { const side = worst.rect.right - box.rect.right >= box.rect.left - worst.rect.left ? "right" : "left"; // `text-overflow: ellipsis` is a container saying "I will cut text off and - // show that I did". That is an affordance, not silent loss — the reader can + // show that I did". That is an affordance, not silent loss: the reader can // see there is more. It stops being one the moment something interactive or // replaced is inside, because a button behind an ellipsis is still a button // nobody can press. @@ -238,14 +240,14 @@ export const audit = function audit() { * The first version of this asked whether a control was present at all, and * that is too coarse for the commonest shape in the admin: a truncated cell * whose text *is* a link. `span.block.truncate > a` reports the anchor's - * full 189px box against a 144px cell, so the anchor counted as swallowed — + * full 189px box against a 144px cell, so the anchor counted as swallowed, * while on screen 161px of it is visible, ellipsised, and perfectly * clickable. Three blocking findings on the projects board, all of them the * repository link reading `null/Privacy-Period-Tr...`, none of them a fault. * * What the rule is really protecting against is a control the clip puts out * of reach, so measure that: how much of it is left inside the box. Below - * the 24px WCAG floor — or its own width, for a control smaller than that — + * the 24px WCAG floor (or its own width, for a control smaller than that) * there is nothing to press and the ellipsis is not an affordance any more. */ const MIN_AIMABLE = 24; @@ -288,7 +290,7 @@ export const audit = function audit() { }, says: `${leaves.length} element(s) cut off by ${Math.round(escape(worst))}px past the ${side} edge ` + - `of an overflow-x:${box.cs.overflowX} box — no scrollbar, no hint`, + `of an overflow-x:${box.cs.overflowX} box, with no scrollbar and no hint`, }); } @@ -318,7 +320,7 @@ export const audit = function audit() { if (!rec) continue; // A control inside a