Skip to content
This repository was archived by the owner on Sep 28, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
c0607b8
fix(a11y): give appraisal intake fields persistent, programmatic labels
nish3451 Aug 12, 2026
0528571
Merge branch 'main' into chore/intake-persistent-labels
nish3451 Aug 12, 2026
f5fcb9b
Merge branch 'main' into chore/intake-persistent-labels
nish3451 Aug 12, 2026
e2000ef
Merge branch 'main' into chore/intake-persistent-labels
nish3451 Aug 12, 2026
b3fc542
Merge branch 'main' into chore/intake-persistent-labels
nish3451 Aug 12, 2026
bc87051
Merge branch 'main' into chore/intake-persistent-labels
nish3451 Aug 12, 2026
f0acbfa
Merge branch 'main' into chore/intake-persistent-labels
nish3451 Aug 12, 2026
dc3aa86
Merge branch 'main' into chore/intake-persistent-labels
nish3451 Aug 12, 2026
e1a9baf
Merge branch 'main' into chore/intake-persistent-labels
nish3451 Aug 12, 2026
d1e3902
Merge branch 'main' into chore/intake-persistent-labels
nish3451 Aug 12, 2026
7e9b858
Merge branch 'main' into chore/intake-persistent-labels
nish3451 Aug 12, 2026
7119d95
Merge branch 'main' into chore/intake-persistent-labels
nish3451 Aug 12, 2026
62ca3e2
Merge branch 'main' into chore/intake-persistent-labels
nish3451 Aug 12, 2026
e9213bb
Merge branch 'main' into chore/intake-persistent-labels
nish3451 Aug 12, 2026
ae961e2
Merge branch 'main' into chore/intake-persistent-labels
nish3451 Aug 12, 2026
14f0e80
Merge branch 'main' into chore/intake-persistent-labels
nish3451 Aug 12, 2026
e2259d8
Merge branch 'main' into chore/intake-persistent-labels
nish3451 Aug 12, 2026
09ac0b5
Merge branch 'main' into chore/intake-persistent-labels
nish3451 Aug 13, 2026
3ccbde1
Merge branch 'main' into chore/intake-persistent-labels
nish3451 Aug 13, 2026
9a011cf
Merge branch 'main' into chore/intake-persistent-labels
nish3451 Aug 13, 2026
8b619a9
Merge branch 'main' into chore/intake-persistent-labels
nish3451 Aug 13, 2026
cc2f48f
Merge branch 'main' into chore/intake-persistent-labels
nish3451 Aug 13, 2026
94b3539
Merge branch 'main' into chore/intake-persistent-labels
nish3451 Aug 13, 2026
1add021
Merge branch 'main' into chore/intake-persistent-labels
nish3451 Aug 13, 2026
fcb425c
Merge branch 'main' into chore/intake-persistent-labels
nish3451 Aug 13, 2026
1724bb3
Merge branch 'main' into chore/intake-persistent-labels
nish3451 Aug 13, 2026
96be299
Merge branch 'main' into chore/intake-persistent-labels
nish3451 Aug 13, 2026
f0aa5d3
Merge branch 'main' into chore/intake-persistent-labels
nish3451 Aug 13, 2026
26ae9e3
Merge branch 'main' into chore/intake-persistent-labels
nish3451 Aug 13, 2026
e03c97c
Merge branch 'main' into chore/intake-persistent-labels
nish3451 Aug 13, 2026
2d2c9a6
Merge branch 'main' into chore/intake-persistent-labels
nish3451 Aug 13, 2026
4177a8a
Merge branch 'main' into chore/intake-persistent-labels
nish3451 Aug 13, 2026
1476541
Merge branch 'main' into chore/intake-persistent-labels
nish3451 Aug 13, 2026
bb5b67b
Merge branch 'main' into chore/intake-persistent-labels
nish3451 Aug 13, 2026
94d7f43
Merge branch 'main' into chore/intake-persistent-labels
nish3451 Aug 13, 2026
1007c10
Merge branch 'main' into chore/intake-persistent-labels
nish3451 Aug 13, 2026
763b078
Merge branch 'main' into chore/intake-persistent-labels
nish3451 Aug 13, 2026
ab96019
Merge branch 'main' into chore/intake-persistent-labels
nish3451 Aug 13, 2026
fd8e290
Merge branch 'main' into chore/intake-persistent-labels
nish3451 Aug 13, 2026
fc7c6b4
Merge branch 'main' into chore/intake-persistent-labels
nish3451 Aug 13, 2026
07d5716
Merge branch 'main' into chore/intake-persistent-labels
nish3451 Aug 13, 2026
ea49ab8
Merge branch 'main' into chore/intake-persistent-labels
nish3451 Aug 13, 2026
d557be6
Merge branch 'main' into chore/intake-persistent-labels
nish3451 Aug 13, 2026
b77a837
merge: resolve PR #154 conflict with main (keep persistent labels, re…
Aug 18, 2026
e8975ca
Merge branch 'main' into chore/intake-persistent-labels
nish3451 Aug 19, 2026
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
4 changes: 2 additions & 2 deletions public/audit.html
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,8 @@
<h1>We read your page <em>as a customer with intent reads it.</em></h1>
<p class="sub">Four passes, by hand, on the surfaces that decide whether someone who arrived ready to buy stays that way. You get a short, plain document — each fault named, in order of what it costs you, with the fix beside each one. No deck. No dashboard.</p>
<form class="lead two" id="start" action="/api/signups" method="post">
<input type="text" name="website" required inputmode="url" autocomplete="url" placeholder="yourwebsite.com" aria-label="Your website domain" pattern="(https?://)?([a-zA-Z0-9\-]+\.)+[a-zA-Z]{2,}(:[0-9]+)?(/[^\s]*)?" title="Enter your domain, like example.com">
<input type="email" name="email" required placeholder="Your work email" aria-label="Your work email">
<label for="intake-website"><span>Your website domain</span><input type="text" name="website" id="intake-website" required inputmode="url" autocomplete="url" placeholder="yourwebsite.com" pattern="(https?://)?([a-zA-Z0-9\-]+\.)+[a-zA-Z]{2,}(:[0-9]+)?(/[^\s]*)?" title="Enter your domain, like example.com"></label>
<label for="intake-email"><span>Your work email</span><input type="email" name="email" id="intake-email" required placeholder="you@company.com"></label>
<button>Request the appraisal</button>
</form>
<p class="micro">Thirty seconds to ask. Findings inside five working days. Yours to keep either way.</p>
Expand Down
4 changes: 2 additions & 2 deletions public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,8 @@
<h1>Most of them leave <em>before they ever get in touch.</em></h1>
<p class="sub">We read the one page your revenue depends on the way a customer with intent reads it, and show you the exact points at which they go.</p>
<form class="lead two" id="start" action="/api/signups" method="post">
<input type="text" name="website" required inputmode="url" autocomplete="url" placeholder="yourwebsite.com" aria-label="Your website domain" pattern="(https?://)?([a-zA-Z0-9\-]+\.)+[a-zA-Z]{2,}(:[0-9]+)?(/[^\s]*)?" title="Enter your domain, like example.com">
<input type="email" name="email" required placeholder="Your work email" aria-label="Your work email">
<label for="intake-website"><span>Your website domain</span><input type="text" name="website" id="intake-website" required inputmode="url" autocomplete="url" placeholder="yourwebsite.com" pattern="(https?://)?([a-zA-Z0-9\-]+\.)+[a-zA-Z]{2,}(:[0-9]+)?(/[^\s]*)?" title="Enter your domain, like example.com"></label>
<label for="intake-email"><span>Your work email</span><input type="email" name="email" id="intake-email" required placeholder="you@company.com"></label>

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 Apply the label layout to the homepage stylesheet

The homepage loads only index.css, while the new label-column rules were added exclusively to shared.css, which is used by /audit. Consequently, on / the existing form.two input rules still assume the inputs are direct flex children: the wrapper labels receive no flex sizing or grid layout, the captions remain unstyled inline content, and both nested inputs match input:first-of-type, producing incorrect dividers and sizing in the primary intake form. Mirror the label rules in index.css or load a stylesheet that supplies them.

Useful? React with 👍 / 👎.

<button>Show me where our site undersells us</button>
</form>
<p class="micro">Thirty seconds to ask. Findings inside five working days. No call at any point.</p>
Expand Down
14 changes: 9 additions & 5 deletions public/shared.css
Original file line number Diff line number Diff line change
Expand Up @@ -97,15 +97,19 @@ footer a:hover{color:var(--ink)}
[data-r].in{opacity:1;transform:none}
}

/* two-field lead form — website + email, still one line on desktop */
/* two-field lead form — website + email, still one line on desktop.
Each field carries a persistent <label> caption: placeholder text alone
disappears as buyers type, so the caption never doubles as the label. */
form.lead.two{max-width:720px;padding:7px 7px 7px 0;overflow:hidden}
form.lead.two input{flex:1 1 0;min-width:0;padding:12px 18px 12px 24px}
form.lead.two input:first-child{border-right:1px solid var(--line)}
form.lead.two label{display:grid;gap:2px;flex:1 1 0;min-width:0;padding:10px 18px 11px 24px}
form.lead.two label + label{border-left:1px solid var(--line);padding-left:18px}
form.lead.two label span{font-size:10px;letter-spacing:.13em;text-transform:uppercase;font-weight:600;color:var(--muted)}

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 Increase the caption text contrast

On /audit, the new persistent captions are only 10px and use --muted (#8A7C6B), which has at most a 3.99:1 contrast ratio even against pure white—and roughly 3.9:1 against the form background—below the 4.5:1 threshold for normal-sized text. This makes the labels difficult to read for low-vision users; use a darker color token for these captions.

Useful? React with 👍 / 👎.

form.lead.two input{padding:0;width:100%}
form.lead.two button{flex:0 0 auto}
@media (max-width:760px){
form.lead.two{flex-direction:column;align-items:stretch;border-radius:20px;padding:8px;max-width:100%}
form.lead.two input{padding:14px 18px}
form.lead.two input:first-child{border-right:0;border-bottom:1px solid var(--line)}
form.lead.two label{padding:12px 14px}
form.lead.two label + label{border-left:0;border-top:1px solid var(--line)}
form.lead.two button{width:100%}
}

Expand Down
25 changes: 16 additions & 9 deletions scripts/check-site.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1633,12 +1633,12 @@ for (const [pageName, pageHtml] of titlePages) {
}

// ---- Intake field labels (activation) -------------------------------------
// Both appraisal intake forms (homepage and /audit) labelled their fields
// only with placeholder text, which disappears the moment a buyer starts
// typing and is not a persistent programmatic label. Each intake input must
// carry a non-empty aria-label so the field keeps its name for assistive
// tech and for the browser's own validation announcements, no matter what
// the field contains.
// Both appraisal intake forms (homepage and /audit) must label each field
// persistently AND programmatically: a visible <label> bound to the input via
// label[for]/input[id], so the name survives typing and is exposed to
// assistive tech and the browser's validation announcements. aria-label alone
// is programmatic but invisible; placeholder-only labels disappear the moment
// a buyer starts typing — neither is enough.
const intakePages = [
["homepage", siteHome],
["audit page", siteAudit]
Expand All @@ -1648,9 +1648,16 @@ for (const [pageName, pageHtml] of intakePages) {
for (const input of pageHtml.matchAll(/<input\b[^>]*>/gi)) {
const tag = input[0];
if (!/\bname="(?:website|email)"/.test(tag)) continue;
Comment on lines 1960 to 1962

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 Require both expected intake fields before validating labels

If either page's email input is deleted or its name is mistyped, this loop simply never visits it and the check still passes; the earlier field-presence guard covers only website. That leaves CI green even though submitting the form without email reaches handleSignup, where the missing value fails validation and the appraisal request is rejected. Assert that exactly one website and one email input were found on each intake page before checking their labels.

Useful? React with 👍 / 👎.

const aria = tag.match(/\baria-label="([^"]*)"/)?.[1] ?? "";
if (!aria.trim()) {
failures.push(`Intake input on ${pageName} must carry a persistent programmatic aria-label (placeholder-only labels disappear as buyers type): ${tag}`);
const id = tag.match(/\bid="([^"]*)"/)?.[1] ?? "";

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 Match only the actual id attribute

If an intake input's id is accidentally renamed to data-id, this regex still extracts it because the hyphen creates a word boundary. The subsequent lookup then finds the unchanged <label for> and the checker passes, even though the browser has no element with that ID and the programmatic label association is broken; require an HTML attribute boundary such as whitespace before id= rather than \b.

Useful? React with 👍 / 👎.

if (!id.trim()) {
failures.push(`Intake input on ${pageName} must carry an id so a persistent <label> can bind to it: ${tag}`);
continue;
}
const labelBody = pageHtml.match(
new RegExp(`<label\\b[^>]*\\bfor="${id}"[^>]*>([\\s\\S]*?)<\\/label>`, "i")

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 Escape IDs before interpolating them into the label regex

When an otherwise valid HTML ID contains a regular-expression metacharacter, this check can accept a mismatched label association. For example, an input with id="intake.email" passes against <label for="intake-email"> because the unescaped . matches -, although the browser does not associate that label with the input. Escape id before building the regex, or parse the attributes and compare their values exactly.

Useful? React with 👍 / 👎.

)?.[1] ?? "";
Comment on lines +1968 to +1970

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 Reject duplicate intake IDs before accepting labels

If a future edit accidentally assigns the same id to both intake inputs, each iteration finds the same first matching <label for> and this guard still passes. In the browser, both explicit labels then resolve to the first element with that ID, leaving the second field without the intended programmatic label; verify IDs are unique (and that each matched label resolves to the current input) before accepting the association.

Useful? React with 👍 / 👎.

Comment on lines +1968 to +1970

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 Ignore labels inside HTML comments

If an intake form is commented out while its old markup remains in the page, both the input scan and this label lookup still inspect the commented markup, so the check reports success even though the browser serves no intake controls. Strip HTML comments before scanning, as the canonical-link check above already does, so only live inputs and labels can satisfy this regression guard.

Useful? React with 👍 / 👎.

if (!labelBody.trim()) {

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 Validate visible label text instead of raw markup

When a caption is emptied or removed while the input remains inside its <label>, labelBody still contains the <input> markup and therefore passes this non-empty check. The guard would consequently accept an intake field with no persistent visible label, defeating the regression protection introduced here; strip tags and verify non-empty visible text, or explicitly validate the caption element's text.

Useful? React with 👍 / 👎.

failures.push(`Intake input on ${pageName} must be bound to a persistent programmatic <label for="${id}"> (placeholder-only labels disappear as buyers type): ${tag}`);
Comment on lines +1968 to +1972

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Validate label text, not only label markup.

labelBody.trim() succeeds when the label contains only the nested <input>. A future input-only label can therefore pass without a visible caption. Extract the label's text content and require non-whitespace text. Add a regression case for an input-only label.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/check-site.mjs` around lines 1656 - 1660, Update the label validation
near labelBody to strip nested markup and evaluate the label’s text content,
requiring non-whitespace visible text rather than merely any captured HTML.
Preserve the existing failure message and add a regression case covering a label
containing only a nested input.

}
}
}
Expand Down
Loading