← back to Prestige Car Wash
A11y + perf pass: focus-visible, reduced-motion, aria labels, font preconnect
acd004ae3514ac1245fee93f47376dddfc6528c4 · 2026-07-26 21:57:56 -0700 · Steve Abrams
DTD unanimous 5/5 (A). pcw.css: visible :focus-visible outlines on all controls, .sr-only
utility, prefers-reduced-motion block. Hamburger buttons: aria-label + real aria-expanded
toggle (all 4 page types). FAQ decorative +/- marker aria-hidden. decorate(): gstatic font
preconnect (crossorigin, deduped) on every page. Contrarian GO; applied its fix — the JS
form-success scrollTo now honors prefers-reduced-motion (CSS scroll-behavior didn't cover it).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Files touched
M public/assets/pcw.cssM public/contact.htmlM public/index.htmlM public/services.htmlM server.js
Diff
commit acd004ae3514ac1245fee93f47376dddfc6528c4
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Sun Jul 26 21:57:56 2026 -0700
A11y + perf pass: focus-visible, reduced-motion, aria labels, font preconnect
DTD unanimous 5/5 (A). pcw.css: visible :focus-visible outlines on all controls, .sr-only
utility, prefers-reduced-motion block. Hamburger buttons: aria-label + real aria-expanded
toggle (all 4 page types). FAQ decorative +/- marker aria-hidden. decorate(): gstatic font
preconnect (crossorigin, deduped) on every page. Contrarian GO; applied its fix — the JS
form-success scrollTo now honors prefers-reduced-motion (CSS scroll-behavior didn't cover it).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
public/assets/pcw.css | 14 ++++++++++++++
public/contact.html | 4 ++--
public/index.html | 2 +-
public/services.html | 2 +-
server.js | 7 +++++--
5 files changed, 23 insertions(+), 6 deletions(-)
diff --git a/public/assets/pcw.css b/public/assets/pcw.css
index 66ca21c..e497fb9 100644
--- a/public/assets/pcw.css
+++ b/public/assets/pcw.css
@@ -96,3 +96,17 @@ img{max-width:100%;display:block}
.grid{grid-template-columns:1fr!important}
.controls{gap:8px}
}
+
+/* ── Accessibility + performance pass (DTD iteration 8, 2026-07-26) ─────────────── */
+/* Visible keyboard focus on every interactive control — the dark theme + custom-styled
+ controls had suppressed the browser default, leaving keyboard users with no focus cue. */
+a:focus-visible,button:focus-visible,input:focus-visible,select:focus-visible,
+textarea:focus-visible,summary:focus-visible,[tabindex]:focus-visible{
+ outline:2px solid var(--brand);outline-offset:2px;border-radius:6px}
+/* Screen-reader-only utility (visually hidden, still announced). */
+.sr-only{position:absolute!important;width:1px;height:1px;padding:0;margin:-1px;
+ overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
+/* Honor reduced-motion: don't force autoplay animation/transition on users who opt out. */
+@media (prefers-reduced-motion:reduce){
+ *,*::before,*::after{animation-duration:.001ms!important;animation-iteration-count:1!important;transition-duration:.001ms!important;scroll-behavior:auto!important}
+}
diff --git a/public/contact.html b/public/contact.html
index fadaac8..53e3492 100644
--- a/public/contact.html
+++ b/public/contact.html
@@ -24,7 +24,7 @@
<body>
<nav class="nav"><div class="wrap row">
<div class="brand"><span class="mk">🚗</span><div>Prestige<br><small>Car Wash · SFV</small></div></div>
- <button class="hamb" onclick="document.getElementById('lnk').classList.toggle('open')">☰</button>
+ <button class="hamb" aria-label="Menu" aria-expanded="false" onclick="var o=document.getElementById('lnk').classList.toggle('open');this.setAttribute('aria-expanded',o)">☰</button>
<div class="links" id="lnk">
<a href="/">Home</a><a href="/services">Services</a><a href="/contact" class="on">Book / Contact</a>
</div>
@@ -108,7 +108,7 @@ document.getElementById('f').addEventListener('submit',async e=>{
try{
const r=await fetch('/api/contact',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(data)});
const j=await r.json();
- if(j.ok){ok.textContent=j.message;ok.style.borderColor='var(--good)';ok.style.color='var(--good)';ok.style.background='rgba(58,210,159,.12)';ok.style.display='block';form.reset();window.scrollTo({top:0,behavior:'smooth'});}
+ if(j.ok){ok.textContent=j.message;ok.style.borderColor='var(--good)';ok.style.color='var(--good)';ok.style.background='rgba(58,210,159,.12)';ok.style.display='block';form.reset();window.scrollTo({top:0,behavior:window.matchMedia&&window.matchMedia('(prefers-reduced-motion:reduce)').matches?'auto':'smooth'});}
else{showErr(j.error||'Something went wrong — please try again.');}
}catch{ showErr('Could not reach the server — please check your connection and try again.'); }
finally{ _submitting=false; if(btn){btn.disabled=false;btn.textContent=btn.dataset.t||'Request booking';} }
diff --git a/public/index.html b/public/index.html
index 6c034bd..5333bfe 100644
--- a/public/index.html
+++ b/public/index.html
@@ -12,7 +12,7 @@
<body>
<nav class="nav"><div class="wrap row">
<div class="brand"><span class="mk">🚗</span><div>Prestige<br><small>Car Wash · SFV</small></div></div>
- <button class="hamb" onclick="document.getElementById('lnk').classList.toggle('open')">☰</button>
+ <button class="hamb" aria-label="Menu" aria-expanded="false" onclick="var o=document.getElementById('lnk').classList.toggle('open');this.setAttribute('aria-expanded',o)">☰</button>
<div class="links" id="lnk">
<a href="/" class="on">Home</a>
<a href="/services">Services</a>
diff --git a/public/services.html b/public/services.html
index 5c8ab4c..686d5c8 100644
--- a/public/services.html
+++ b/public/services.html
@@ -12,7 +12,7 @@
<body>
<nav class="nav"><div class="wrap row">
<div class="brand"><span class="mk">🚗</span><div>Prestige<br><small>Car Wash · SFV</small></div></div>
- <button class="hamb" onclick="document.getElementById('lnk').classList.toggle('open')">☰</button>
+ <button class="hamb" aria-label="Menu" aria-expanded="false" onclick="var o=document.getElementById('lnk').classList.toggle('open');this.setAttribute('aria-expanded',o)">☰</button>
<div class="links" id="lnk">
<a href="/">Home</a><a href="/services" class="on">Services</a><a href="/contact">Book / Contact</a>
<a class="btn" href="/contact">Book a Wash</a>
diff --git a/server.js b/server.js
index 2651b11..9e87a7d 100644
--- a/server.js
+++ b/server.js
@@ -598,7 +598,7 @@ function faqHtml() {
return faqs().map((f, i) => `
<details class="faq"${i === 0 ? ' open' : ''} style="border:1px solid var(--line);border-radius:12px;margin-bottom:10px;background:var(--panel)">
<summary style="cursor:pointer;padding:15px 18px;font-weight:700;font-size:16px;display:flex;justify-content:space-between;gap:12px;align-items:center">
- <span>${esc(f.q)}</span><span class="faq-mk" style="color:var(--mut);font-weight:800;font-size:20px">+</span>
+ <span>${esc(f.q)}</span><span class="faq-mk" aria-hidden="true" style="color:var(--mut);font-weight:800;font-size:20px">+</span>
</summary>
<div style="padding:0 18px 16px;color:var(--mut);font-size:15px;line-height:1.55">${esc(f.a)}</div>
</details>`).join('');
@@ -661,7 +661,7 @@ function guideLayout(title, description, bodyHtml) {
</head><body>
<nav class="nav"><div class="wrap row">
<div class="brand"><span class="mk">🚗</span><div>Prestige<br><small>Car Wash · SFV</small></div></div>
- <button class="hamb" onclick="document.getElementById('lnk').classList.toggle('open')">☰</button>
+ <button class="hamb" aria-label="Menu" aria-expanded="false" onclick="var o=document.getElementById('lnk').classList.toggle('open');this.setAttribute('aria-expanded',o)">☰</button>
<div class="links" id="lnk"><a href="/">Home</a><a href="/services">Services</a><a href="/guides" class="on">Guides</a><a class="btn" href="/contact">Book a Wash</a></div>
</div></nav>
${bodyHtml}
@@ -783,6 +783,9 @@ function decorate(html, pagePath, ld) {
// Persistent mobile Book/Call bar on every customer page (guard against double-inject).
if (!/class="mbar"/.test(html)) html = html.replace('</body>', mobileBar(pagePath) + '\n</body>');
let inject = buildSocialTags(html, pagePath);
+ // Perf: preconnect to the font-file origin (pages already preconnect googleapis; the actual
+ // WOFF2 files come from gstatic, so this shaves a round-trip on first paint). Dedup-guarded.
+ if (!/fonts\.gstatic\.com/.test(html)) inject += '\n<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>';
// ld may be a single JSON-LD object OR an array of them (e.g. AutoWash + FAQPage on the
// homepage). Emit one <script> per object; escape "<" so the JSON can't break out of the tag.
for (const obj of [].concat(ld || []).filter(Boolean)) {
← 2062a31 Admin: lead worklist (click-to-call/email + follow-up-first
·
back to Prestige Car Wash
·
Analytics-lite: first-party, cookie-free conversion tracking e694e62 →