Skip to content
This repository was archived by the owner on Sep 28, 2026. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions docs/evidence/lead-form-tablet-squeeze-2026-08-10.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# Homepage lead form — tablet squeeze fix, measured evidence

Date: 2026-08-10
Scope: `public/index.css` — the homepage intake form (`form.lead.two`) squeezed the domain and email inputs to ~100px at tablet width. The two-field form shares the `.lead` class with the checks-section header, whose `gap:70px / justify-content:space-between / align-items:flex-end` treatment leaked onto the form (via the `.lead` selector) and, together with the nowrap submit button, starved the inputs of width below desktop sizes.
This receipt records real browser-layout measurements. It is behavior evidence, not a source check, and it does not claim anything about the hosted/live deployment.

## What was measured

A headless Chromium rendered the homepage from a local static copy of `public/` (Google Fonts loaded over the network as in production; `document.fonts.ready` waited on, Karla confirmed loaded). For each viewport we recorded `getBoundingClientRect()` width for the form, both inputs, and the submit button, plus the computed `flex-direction`/`gap`, whether the "yourwebsite.com" placeholder's measured text width fits inside the first input's visible text area (input width minus its 42px horizontal padding), and whether `document.documentElement.scrollWidth == clientWidth` (no horizontal overflow).

The "unfixed" variant is the exact pre-change tree: same `public/`, but `index.css` (and its HTML/JS dependencies) replaced by the `origin/main` (HEAD) versions, so both states are measured with one method in one session.

## Environment

- Node v22.23.1, Playwright 1.62.1, bundled Chromium headless, Linux.
- Viewports: 390x900 and 761/768/800/834/900/901/1024/1280 wide x900 (deviceScaleFactor 1).
- Wait: `networkidle`, then `document.fonts.ready`; Karla `document.fonts.check("16px Karla")` confirmed true in every run.

## Results — input widths (px), unfixed → fixed

| Viewport | layout (**unfixed**) | i1/i2 (**unfixed**) | layout (**fixed**) | i1/i2 (**fixed**) |
|---|---|---|---|---|
| 390 | column, gap 18px | 332 / 332 | column, gap 18px | 332 / 332 (identical) |
| 761 | row, gap 70px | **99 / 98** | column, gap 18px | 623 / 623 |
| 768 | row, gap 70px | **103 / 102** | column, gap 18px | 630 / 630 |
| 800 | row, gap 70px | **119 / 118** | column, gap 18px | 662 / 662 |
| 834 | row, gap 70px | **136 / 135** | column, gap 18px | 696 / 696 |
| 900 | row, gap 70px | **139 / 138** | column, gap 18px | 762 / 762 |
| 901 | row, gap 70px | **139 / 138** | row, no gap | 209 / 208 |
| 1024 | row, gap 70px | **139 / 138** | row, no gap | 209 / 208 |
| 1280 | row, gap 70px | **139 / 138** | row, no gap | 209 / 208 |

Submit button: 294px (nowrap) in every row layout, unfixed and fixed. The unfixed "yourwebsite.com" placeholder text (~140px at 16px Karla) does **not** fit the first input's visible area at any viewport ≥761px — including desktop, where the leaked 70px gaps cap the inputs at 139px (97px visible). After the fix the placeholder fits at every width: 167px visible in the 901px+ one-line layout, full-width in the stacked layout below.

## What changed

1. The checks-header rules `.lead{...}` (desktop and mobile) are scoped to `.checks .lead`, so the shared class can no longer leak its `gap:70px / space-between / flex-end` treatment onto the lead form.
2. The two-field form's stacked breakpoint moves from `max-width:760px` to its own `max-width:900px`, with an explicit `gap:18px` (previously inherited from the leaked `.lead` mobile rule), so tablet widths (761–900px) stack full-width instead of squeezing.

## Deliberate desktop change

At ≥901px the one-line form loses the accidental 70px gaps: inputs grow 139 → 208px and the placeholders render fully. This matches the identical two-field form on /audit (shared.css), which never had the `.lead` leak and rendered 264px inputs at 1024px. The submit button (294px) and the pill styling are unchanged. At ≤390px the stacked mobile layout is pixel-identical before and after (332/332 inputs, gap 18px).

## Checks header neutrality

The scoped `.checks .lead` selector leaves the "What we actually look at" header exactly as before: 1024px `space-between / gap 70px / flex-end` row; 390px column with `gap 18px`. The audit page is untouched (its form was never squeezed: shared.css has no `.lead` class rule and its shorter button leaves 224–264px inputs; measured pre-fix).

## No horizontal overflow introduced

At every viewport in both runs, `document.documentElement.scrollWidth == clientWidth` (761px: 761; 768px: 768; 901px: 901; …). The stacked form is `max-width:100%` inside the existing 60px `.wrap` padding.

## Not CI proof

This is local static-server proof, not CI proof and not a hosted/live claim. The repo's CI (`npm test`) has no browser dependency, so it runs only the source-string guards in `scripts/check-site.mjs` (which now pin the lead-form rules and this receipt's anchors) plus the worker/UI contract tests; a browser-layout regression can therefore still ship if the checked-in CSS drifts and CI stays green. The live tinystudio.io deployment was not measured here; a deployed page could differ (CDN cache, different asset versions). To be CI-proof this measurement would need a browser step added to CI (not done — out of scope).

## Exact verification method (reproduce)

1. Copy `public/` to `fixed-site/` and serve it on 127.0.0.1:8131 (`python3 -m http.server 8131 --directory fixed-site`). Copy it again to `unfixed-site/` and overwrite `index.css` (and `index.html`, `index.js`, `fonts.js` for a clean tree) with their `origin/main` versions (`git show origin/main:public/index.css > unfixed-site/index.css`), then serve it on 127.0.0.1:8132.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Pin the unfixed fixture to the parent commit

Once this change lands and origin/main advances, this command copies the fixed stylesheet into unfixed-site, so both servers render the same version and the documented before/after measurements can no longer be reproduced. Reference the reviewed commit's first parent (preferably its immutable hash) rather than the moving origin/main ref.

Useful? React with 👍 / 👎.

2. Run this per state and viewport (requires `npm i -D playwright && npx playwright install chromium` in any project):

```js
import { chromium } from "playwright";
const browser = await chromium.launch();
const page = await browser.newPage();
await page.setViewportSize({ width: 768, height: 900 });
await page.goto("http://127.0.0.1:8131/"); // or :8132 for unfixed
await page.waitForLoadState("networkidle");
await page.evaluate(() => document.fonts.ready);
const m = await page.evaluate(() => {
const form = document.querySelector("form.two");
const inputs = [...form.querySelectorAll("input")];
const r = (el) => Math.round(el.getBoundingClientRect().width);
return {
i1: r(inputs[0]), i2: r(inputs[1]), btn: r(form.querySelector("button")),
direction: getComputedStyle(form).flexDirection, gap: getComputedStyle(form).gap,
scrollOK: document.documentElement.scrollWidth === document.documentElement.clientWidth
};
});
console.log(m);
```

Unfixed at 768px: `{ i1: 103, i2: 102, btn: 294, direction: "row", gap: "70px", scrollOK: true }`. Fixed at 768px: `{ i1: 630, i2: 630, btn: 630, direction: "column", gap: "18px", scrollOK: true }`. Fixed at 1024px: `{ i1: 209, i2: 208, btn: 294, direction: "row", gap: "normal", scrollOK: true }`.
18 changes: 12 additions & 6 deletions public/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -103,9 +103,12 @@

/* ── checks ────────────────────────────────────── */
.checks{margin-top:124px;padding-top:70px;border-top:1px solid var(--line)}
.lead{display:flex;justify-content:space-between;align-items:flex-end;gap:70px}
/* scoped to .checks: the homepage lead form shares the .lead class, and an
unscoped gap/space-between/flex-end treatment leaked onto the form,
squeezing the domain+email inputs to ~100px at tablet widths */
.checks .lead{display:flex;justify-content:space-between;align-items:flex-end;gap:70px}
.checks h2{font-family:'Fraunces',serif;font-weight:200;font-size:47px;letter-spacing:-.028em;max-width:12ch;line-height:1.08}
.lead p{max-width:38ch;font-size:15.5px;line-height:1.75;color:var(--muted)}
.checks .lead p{max-width:38ch;font-size:15.5px;line-height:1.75;color:var(--muted)}
.checkgrid{margin-top:56px;display:grid;grid-template-columns:repeat(4,1fr)}
.check{padding-right:34px;border-right:1px solid var(--hair)}
.check:last-child{border-right:0;padding-right:0}
Expand Down Expand Up @@ -169,13 +172,16 @@
.navlinks a:hover{color:var(--ink)}
.navcta{border:1px solid var(--line);border-radius:999px;padding:9px 17px;color:var(--ink)!important}
.navcta:hover{border-color:var(--brass);background:rgba(255,253,248,.7)}
/* two-field lead form — website + email */
/* two-field lead form — website + email, still one line on desktop */
form.two{max-width:720px;padding:7px 7px 7px 0;overflow:hidden}
form.two input{flex:1 1 0;min-width:0;padding:12px 18px 12px 24px}
form.two input:first-of-type{border-right:1px solid var(--line)}
form.two button{flex:0 0 auto}
@media (max-width:760px){
form.two{flex-direction:column;align-items:stretch;border-radius:20px;padding:8px;max-width:100%}
/* Stack below 900px: with the nowrap submit button, a one-line row below
tablet width squeezes the inputs to ~100px. The gap is explicit here —
it used to arrive via the .lead class leak (now scoped to .checks). */
@media (max-width:900px){
form.two{flex-direction:column;align-items:stretch;border-radius:20px;padding:8px;max-width:100%;gap:18px}
form.two input{padding:14px 18px}
form.two input:first-of-type{border-right:0;border-bottom:1px solid var(--line)}
form.two button{width:100%}
Expand Down Expand Up @@ -235,7 +241,7 @@
.finding .stat{font-size:56px}
.spec{grid-template-columns:1fr;gap:34px}
.specbody h2{font-size:34px}
.lead{flex-direction:column;align-items:flex-start;gap:18px}
.checks .lead{flex-direction:column;align-items:flex-start;gap:18px}
.checks h2{font-size:36px}
.checkgrid{grid-template-columns:1fr;gap:26px}
.check{padding-right:0;padding-bottom:26px;border-right:0;border-bottom:1px solid var(--hair)}
Expand Down
37 changes: 37 additions & 0 deletions scripts/check-site.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,43 @@ for (const anchor of ["unfixed", "390x844", "44px", "not CI proof", "Exact verif
}
}

// Lead-form tablet squeeze guard (review finding on the homepage intake form):
// the homepage form shares the .lead class with the checks-section header,
// whose gap:70px / space-between / flex-end treatment leaked onto it and,
// together with the nowrap submit button, squeezed the domain and email
// inputs to ~100px at tablet width (measured 99/98px at 761px, 103/102px at
// 768px, and never above 139px — the "yourwebsite.com" placeholder clipped —
// at any width under the fix). The fix scopes the checks-header rules to
// .checks .lead and gives the two-field form its own 900px stacking
// breakpoint, so the one-line layout keeps >=208px inputs and anything
// narrower goes full-width stacked. These are STATIC SOURCE GUARDS (regex
// over the served CSS), not behavioral tests: CI has no browser. The
// behavioral, measured layout proof lives in
// docs/evidence/lead-form-tablet-squeeze-2026-08-10.md.
const indexCss = read("public/index.css");
if (!indexCss.includes(".checks .lead{display:flex;justify-content:space-between;align-items:flex-end;gap:70px}")) {
failures.push("Homepage checks header must scope .lead to .checks .lead (the shared class leaks its gap onto the lead form).");
}
if (!indexCss.includes("@media (max-width:900px){")) {
failures.push("Two-field lead form must carry its own 900px stacking breakpoint (tablet squeeze guard).");
}
if (!indexCss.includes("form.two{flex-direction:column;align-items:stretch;border-radius:20px;padding:8px;max-width:100%;gap:18px}")) {
failures.push("Two-field lead form must stack below 900px with an explicit gap (tablet squeeze guard).");
}
if (indexCss.includes("@media (max-width:760px){\n form.two{flex-direction:column")) {
failures.push("Two-field lead form must not stack at 760px (squeezes the inputs to ~100px at tablet widths).");
}

// The behavioral lead-form proof (real Chromium measurement, local static
// copy) is checked in so the static guards above stay distinguishable from it.
// Existence and section anchors only — this does not re-verify measurements.
const leadFormReceipt = read("docs/evidence/lead-form-tablet-squeeze-2026-08-10.md");
for (const anchor of ["unfixed", "761px", "**fixed**", "208px", "not CI proof", "Exact verification method"]) {
if (!leadFormReceipt.includes(anchor)) {
failures.push(`Lead-form evidence receipt must record the ${JSON.stringify(anchor)} section.`);
}
}

// Render-blocking font stylesheet guards (dogfood finding b8f6046e942a).
// The Google Fonts css2 stylesheet was fetched render-blocking on every public
// page — a <link rel="stylesheet"> in the homepage head, and an @import chain
Expand Down
Loading