[object Object]

← back to Commercialrealestate

a11y(index): accessibility pass (DTD verdict A, 2/3 Codex+Claude; Qwen dissent C=export-compare) — aria-label + aria-pressed on ★ shortlist buttons, scope=col on list + compare table headers, :focus-visible keyboard focus rings, @media prefers-reduced-motion, and Escape-to-close on the compare + broker/property-history modals. Contrarian-gated, 0 errors.

589fdb59d72267c992cd28065ba23ce93dc7ac83 · 2026-07-02 14:17:49 -0700 · Steve Abrams

Officer: vp-engineering
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Files touched

Diff

commit 589fdb59d72267c992cd28065ba23ce93dc7ac83
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu Jul 2 14:17:49 2026 -0700

    a11y(index): accessibility pass (DTD verdict A, 2/3 Codex+Claude; Qwen dissent C=export-compare) — aria-label + aria-pressed on ★ shortlist buttons, scope=col on list + compare table headers, :focus-visible keyboard focus rings, @media prefers-reduced-motion, and Escape-to-close on the compare + broker/property-history modals. Contrarian-gated, 0 errors.
    
    Officer: vp-engineering
    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---
 public/index.html | 12 ++++++++----
 1 file changed, 8 insertions(+), 4 deletions(-)

diff --git a/public/index.html b/public/index.html
index e3d3d2e..32b9cb3 100644
--- a/public/index.html
+++ b/public/index.html
@@ -8,6 +8,8 @@
 <style>
   :root { --cols:3; --bg:#0e1116; --card:#161b22; --line:#2a313c; --ink:#e6edf3; --mut:#8b949e; --acc:#3fb950; --warn:#d29922; --bad:#f85149; --blue:#58a6ff; --drawerW:316px; }
   * { box-sizing:border-box; }
+  :focus-visible { outline:2px solid var(--blue); outline-offset:2px; border-radius:4px; }
+  @media (prefers-reduced-motion: reduce){ *,*::before,*::after { animation-duration:.001ms !important; animation-iteration-count:1 !important; transition-duration:.001ms !important; scroll-behavior:auto !important; } }
   body { margin:0; background:var(--bg); color:var(--ink); font:15px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif; }
 
   /* ---- top bar ---- */
@@ -687,9 +689,9 @@ function colCell(k,p){ const f=p.finance||{};
 function renderList(rows){
   const cols=listCols();
   const arrow=k=>colSort&&colSort.key===k?(colSort.dir>0?' ▲':' ▼'):'';
-  const head='<th class="stick sortable" data-sortk="addr">Property'+arrow('addr')+'</th>'+cols.map(c=>`<th class="sortable" data-sortk="${c.k}">${esc(c.label)}${arrow(c.k)}</th>`).join('');
+  const head='<th scope="col" class="stick sortable" data-sortk="addr" title="Sort by address">Property'+arrow('addr')+'</th>'+cols.map(c=>`<th scope="col" class="sortable" data-sortk="${c.k}" title="Sort by ${esc(c.label)}">${esc(c.label)}${arrow(c.k)}</th>`).join('');
   const body=rows.map(p=>`<tr class="lrow" data-id="${p.id}">`
-    +`<td class="stick"><button class="starbtn ${SHORT.has(p.id)?'on':''}" data-star="${p.id}" title="Shortlist">★</button> <a class="la" href="${p.source}" target="_blank" rel="noopener noreferrer">${esc(p.address)}</a><div class="lc">${esc(p.city)}, CA ${p.zip||''}</div></td>`
+    +`<td class="stick"><button class="starbtn ${SHORT.has(p.id)?'on':''}" data-star="${p.id}" aria-label="${SHORT.has(p.id)?'Remove from':'Add to'} shortlist" aria-pressed="${SHORT.has(p.id)}" title="Shortlist">★</button> <a class="la" href="${p.source}" target="_blank" rel="noopener noreferrer">${esc(p.address)}</a><div class="lc">${esc(p.city)}, CA ${p.zip||''}</div></td>`
     +cols.map(c=>`<td>${colCell(c.k,p)}</td>`).join('')+`</tr>`).join('');
   return `<div class="listwrap"><table class="listtbl"><thead><tr>${head}</tr></thead><tbody>${body}</tbody></table></div>`;
 }
@@ -836,7 +838,7 @@ function card(p){
     <div class="top">
       <div><div class="rank">#${p.rank} · score</div><div class="addr">${p.address}</div><div class="city">${p.city}, CA ${p.zip||''}</div></div>
       <div style="display:flex;align-items:flex-start;gap:8px">
-        <button class="starbtn ${SHORT.has(p.id)?'on':''}" data-star="${p.id}" title="Shortlist this listing">★</button>
+        <button class="starbtn ${SHORT.has(p.id)?'on':''}" data-star="${p.id}" aria-label="${SHORT.has(p.id)?'Remove from':'Add to'} shortlist" aria-pressed="${SHORT.has(p.id)}" title="Shortlist this listing">★</button>
         ${vis('score')?`<div class="score">${p.composite}<small>fin ${p.financeScore}${p.qwenScore!=null?' · ai '+p.qwenScore:''}</small></div>`:''}
       </div>
     </div>
@@ -1086,7 +1088,7 @@ function openCompare(){
   const ov=$('#cmpov'), body=$('#cmpbody');
   const items=DATA.ranked.filter(p=>SHORT.has(p.id));
   if(items.length<2){ body.innerHTML='<div class="cmphint">Star at least 2 listings (the ★ on any card) to compare them side by side.</div>'; ov.classList.add('on'); return; }
-  const head='<tr><th class="cmpk"></th>'+items.map(p=>`<th>${esc(p.address)}<div class="cmpsub">${esc(p.city)}</div></th>`).join('')+'</tr>';
+  const head='<tr><th class="cmpk" scope="col"></th>'+items.map(p=>`<th scope="col">${esc(p.address)}<div class="cmpsub">${esc(p.city)}</div></th>`).join('')+'</tr>';
   const rows=CMP.map(m=>`<tr><td class="cmpk">${m.l}</td>`+items.map(p=>`<td>${m.f(p)}</td>`).join('')+'</tr>').join('');
   body.innerHTML=`<table class="cmptbl"><thead>${head}</thead><tbody>${rows}</tbody></table>`;
   ov.classList.add('on');
@@ -1407,6 +1409,8 @@ fetch('data/ranked.json').then(r=>r.json()).then(async d=>{
   $('#cmpbtn').onclick=openCompare;
   $('#heat').value=heat; $('#heat').onchange=()=>{ heat=$('#heat').value; localStorage.setItem('cre_heat',heat); render(); };
   $('#cmpov').addEventListener('click',e=>{ if(e.target.id==='cmpov') e.currentTarget.classList.remove('on'); });
+  // a11y: Escape closes any open overlay (compare / broker / property-history modals)
+  document.addEventListener('keydown',e=>{ if(e.key==='Escape'){ ['cmpov','bkov'].forEach(id=>{ const el=document.getElementById(id); if(el) el.classList.remove('on'); }); } });
 
   window.DATA_READY = true;
   recompute(); syncChips(); render();

← ac724c3 feat(index): empty-state quick-start chips (DTD verdict D, 2  ·  back to Commercialrealestate  ·  feat(condos): surface parity (DTD verdict C, 2/3 Codex+Claud 6d8e182 →