← back to Lawyer Directory Builder

public/audit.html

360 lines

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Site Audit · Counsel &amp; Bar</title>
<meta name="description" content="Independent audit of California-licensed attorney websites against twelve published signals. 1,016 sites scored on methodology v1.0. Audit decoupled from any commercial offering.">
<link rel="canonical" href="https://lawyers.agentabrams.com/audit.html">
<meta name="robots" content="index, follow">
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32.png">
<link rel="apple-touch-icon" href="/favicon-32.png">
<meta name="theme-color" content="#0a0a0c">

<meta property="og:type" content="website">
<meta property="og:site_name" content="Counsel &amp; Bar">
<meta property="og:title" content="Site Audit · Counsel &amp; Bar">
<meta property="og:description" content="1,016 California-licensed attorney sites scored against twelve published signals. Methodology v1.0 — independent of any commercial offering.">
<meta property="og:url" content="https://lawyers.agentabrams.com/audit.html">
<meta property="og:image" content="https://lawyers.agentabrams.com/og.png">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta property="og:image:alt" content="Counsel &amp; Bar — site audit cohort, methodology v1.0">

<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="Site Audit · Counsel &amp; Bar">
<meta name="twitter:description" content="1,016 attorney sites scored. Methodology v1.0 published. Champagne-tier scoring; no traffic-light alerts next to real attorney names.">
<meta name="twitter:image" content="https://lawyers.agentabrams.com/og.png">

<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "Dataset",
  "@id": "https://lawyers.agentabrams.com/audit.html#dataset",
  "name": "Counsel & Bar — California attorney site audits",
  "description": "Independent audit of California-licensed attorney websites against twelve technical and presentation signals (HTTPS, mobile viewport, Open Graph image, web analytics, Schema.org markup, page weight, load time, favicon, H1 heading, phone visibility, font count, image count). Scores normalized to 0-100, refreshed on a 90-day rolling cadence.",
  "url": "https://lawyers.agentabrams.com/audit.html",
  "creator": {
    "@type": "Organization",
    "name": "Counsel & Bar",
    "url": "https://lawyers.agentabrams.com/"
  },
  "publisher": {
    "@type": "Organization",
    "name": "Counsel & Bar",
    "url": "https://lawyers.agentabrams.com/"
  },
  "license": "https://lawyers.agentabrams.com/terms.html",
  "keywords": ["California attorneys", "site audit", "web standards", "attorney directory"],
  "isAccessibleForFree": true
}
</script>

<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300;0,400;0,500;1,300;1,400&family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet">
<style>
  :root {
    --noir:#0a0a0c;--noir-rise:#131316;--noir-deep:#050507;
    --rule:#2a2724;--rule-faint:#1a1815;
    --ink:#f4f1ea;--ink-soft:#d8d2c5;--ink-mute:#8b857a;
    --metal:#b89968;--metal-glow:#d4b683;--metal-deep:#8a7044;
    --good:#34d399;--warn:#d4a04a;--bad:#d87a7a;
    --serif:"Cormorant Garamond","Cormorant","Georgia",serif;
    --sans:"Inter",-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;
  }
  * { box-sizing: border-box; }
  body { margin: 0; background: var(--noir); color: var(--ink); font-family: var(--sans); font-weight: 300; font-size: 14px; line-height: 1.5; }
  ::selection { background: var(--metal); color: var(--noir); }
  a { color: var(--metal); text-decoration: none; }
  a:hover { color: var(--metal-glow); }
  header { padding: 22px 32px; border-bottom: 1px solid var(--rule); display: flex; align-items: center; gap: 18px; flex-wrap: wrap; background: var(--noir-deep); }
  header h1 { margin: 0; font-family: var(--serif); font-size: 20px; font-weight: 400; letter-spacing: 0.005em; }
  header h1 em { font-style: italic; color: var(--metal); font-weight: 400; }
  header .stat { color: var(--ink-mute); font-size: 11px; letter-spacing: 0.14em; text-transform: uppercase; }
  header .filter { margin-left: auto; display: flex; gap: 12px; align-items: center; font-size: 11px; letter-spacing: 0.14em; text-transform: uppercase; color: var(--ink-mute); }
  header input[type=range] { width: 200px; accent-color: var(--metal); }
  main { padding: 32px; }
  .audit-masthead { max-width: 1280px; margin: 24px auto 36px; padding: 36px 0 32px; border-bottom: 1px solid var(--rule); display: grid; grid-template-columns: 1fr auto; gap: 24px; align-items: end; }
  @media (max-width: 720px) { .audit-masthead { grid-template-columns: 1fr; padding: 24px 0; } }
  .public-vol { display: flex; align-items: center; gap: 18px; margin: 0 0 18px; color: var(--metal); }
  .public-vol .v-rule { flex: 0 0 56px; height: 1px; background: linear-gradient(90deg, transparent, var(--metal) 50%, transparent); }
  .public-vol .v-text { font-family: var(--serif); font-style: italic; font-weight: 400; font-size: 14px; letter-spacing: .02em; }
  .public-vol .v-meta { font-size: 10px; letter-spacing: .28em; text-transform: uppercase; color: var(--ink-mute); font-weight: 500; }
  .audit-display { font-family: var(--serif); font-weight: 300; font-size: clamp(34px, 4.6vw, 56px); line-height: 1.04; letter-spacing: -.018em; margin: 0; max-width: 22ch; color: var(--ink); }
  .audit-display em { font-style: italic; color: var(--metal); font-weight: 400; }
  .audit-lede { font-family: var(--serif); font-style: italic; font-weight: 300; color: var(--ink-soft); font-size: clamp(15px, 1.4vw, 18px); line-height: 1.55; max-width: 64ch; margin: 18px 0 0; letter-spacing: .005em; }
  .audit-distribution { max-width: 1280px; margin: 0 auto 32px; padding: 22px 0; border-top: 1px solid var(--rule); border-bottom: 1px solid var(--rule); display: grid; grid-template-columns: 200px 1fr; gap: 28px; align-items: center; }
  .ad-eyebrow { font-size: 10px; letter-spacing: .3em; text-transform: uppercase; color: var(--metal); font-weight: 500; margin: 0 0 6px; }
  .ad-meta { font-family: var(--serif); font-style: italic; color: var(--ink-mute); font-size: 12px; line-height: 1.45; margin: 0; letter-spacing: .005em; }
  .ad-bars { display: grid; grid-template-columns: repeat(4, 1fr); gap: 22px; align-items: end; }
  .ad-bar { display: flex; flex-direction: column; gap: 8px; }
  .ad-bar-tier { font-family: var(--serif); font-style: italic; font-weight: 400; font-size: 13px; color: var(--metal); letter-spacing: .01em; display: flex; justify-content: space-between; align-items: baseline; gap: 12px; }
  .ad-bar-tier b { font-style: normal; font-family: var(--sans); color: var(--ink); font-weight: 500; font-size: 18px; font-variant-numeric: tabular-nums; letter-spacing: -.01em; }
  .ad-bar-track { height: 4px; background: var(--rule); position: relative; }
  .ad-bar-fill { position: absolute; inset: 0 auto 0 0; background: var(--metal); width: 0%; transition: width 700ms cubic-bezier(.16,.84,.3,1); }
  .ad-bars[data-painted] .ad-bar-fill { transition: width 200ms linear; }
  .ad-bar.ad-tier-a .ad-bar-fill { background: linear-gradient(90deg, var(--metal-glow), var(--metal)); }
  .ad-bar.ad-tier-b .ad-bar-fill { background: var(--metal); }
  .ad-bar.ad-tier-c .ad-bar-fill { background: #a07a40; }
  .ad-bar.ad-tier-d .ad-bar-fill { background: #5a4220; }
  .ad-bar-meta { font-size: 9px; letter-spacing: .26em; text-transform: uppercase; color: var(--ink-mute); font-weight: 500; }
  @media (max-width: 780px) { .audit-distribution { grid-template-columns: 1fr; gap: 18px; } .ad-bars { gap: 14px; } }
  .audit-key { display: flex; gap: 28px; align-items: baseline; }
  .audit-key .k-pair { display: flex; flex-direction: column; gap: 4px; min-width: 64px; }
  .audit-key .k-num { font-family: var(--serif); font-weight: 300; font-size: 22px; line-height: 1; font-variant-numeric: tabular-nums; color: var(--metal); }
  .audit-key .k-num.high { opacity: 1; }
  .audit-key .k-num.mid { opacity: 0.65; }
  .audit-key .k-num.low { opacity: 0.4; }
  .audit-key .k-lbl { font-size: 9px; letter-spacing: .26em; text-transform: uppercase; color: var(--ink-mute); font-weight: 500; }
  .audit-keystrip { max-width: 1280px; margin: 0 auto 28px; padding: 14px 0; border-top: 1px solid var(--rule); border-bottom: 1px solid var(--rule); display: flex; align-items: center; gap: 32px; }
  .audit-keystrip .ks-label { font-size: 10px; letter-spacing: .26em; text-transform: uppercase; color: var(--ink-mute); font-weight: 500; }
  .audit-keystrip .ks-pair { font-family: var(--serif); font-style: italic; font-size: 13px; color: var(--metal); letter-spacing: .02em; }
  .audit-keystrip .ks-pair span { font-style: normal; font-family: var(--sans); font-size: 9px; letter-spacing: .26em; text-transform: uppercase; color: var(--ink-mute); margin-left: 6px; }
  .grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(360px, 1fr)); gap: 24px; max-width: 1280px; margin: 0 auto; }
  .audit-foot { max-width: 1280px; margin: 56px auto 0; padding: 24px 0 0; border-top: 1px solid var(--rule); font-family: var(--serif); font-style: italic; color: var(--ink-mute); font-size: 13px; letter-spacing: .02em; text-align: center; }
  .card { background: var(--noir-rise); border: 1px solid var(--rule); overflow: hidden; display: flex; flex-direction: column; }
  .shot { width: 100%; aspect-ratio: 1280/800; background: var(--noir-deep) center/cover no-repeat; cursor: zoom-in; border-bottom: 1px solid var(--rule); }
  .row { padding: 16px 18px 12px; }
  .name { font-family: var(--serif); font-weight: 400; font-size: 17px; line-height: 1.25; color: var(--ink); }
  .url { color: var(--ink-mute); font-size: 11px; word-break: break-all; margin-top: 4px; letter-spacing: 0.02em; }
  .url a { color: var(--ink-mute); text-decoration: none; }
  .url a:hover { color: var(--metal); }
  .score-row { display: flex; align-items: baseline; gap: 14px; padding: 0 18px 14px; }
  .score { font-family: var(--serif); font-variant-numeric: tabular-nums; font-weight: 300; font-size: 42px; line-height: 1; min-width: 56px; letter-spacing: -.01em; }
  .score.tier-a { color: var(--metal-glow); }
  .score.tier-b { color: var(--metal); }
  .score.tier-c { color: #a07a40; }
  .score.tier-d { color: #5a4220; font-weight: 400; }
  .score.tier-x { color: var(--ink-mute); font-style: italic; font-size: 22px; font-family: var(--serif); }
  .score-label { color: var(--ink-mute); font-size: 10px; letter-spacing: 0.22em; text-transform: uppercase; line-height: 1.3; }
  .palette { display: flex; padding: 0 18px 12px; gap: 3px; }
  .swatch { flex: 1; aspect-ratio: 1; border: 1px solid var(--rule); }
  .signals { padding: 0 18px 12px; display: flex; flex-wrap: wrap; gap: 6px; }
  .pill { font-size: 10px; padding: 3px 9px; border: 1px solid var(--rule); letter-spacing: 0.16em; text-transform: uppercase; font-weight: 500; transition: border-color 200ms ease, color 200ms ease; }
  .pill.on { background: linear-gradient(180deg,rgba(184,153,104,.06),transparent); color: var(--metal-glow); border-color: rgba(184,153,104,.4); }
  .pill.off { background: transparent; color: var(--ink-mute); border-color: var(--rule); opacity: .55; text-decoration: line-through; text-decoration-color: rgba(139,133,122,.5); text-decoration-thickness: 1px; }
  .card:hover .pill.on { border-color: var(--metal); }
  .sugg { background: var(--noir-deep); padding: 14px 18px; border-top: 1px solid var(--rule); font-size: 12px; color: var(--ink-mute); }
  .sugg b { font-family: var(--serif); font-style: italic; font-weight: 400; color: var(--metal); font-size: 13px; display: block; margin-bottom: 6px; }
  .sugg ul { margin: 0; padding-left: 18px; list-style: none; }
  .sugg li { margin: 5px 0; line-height: 1.5; position: relative; }
  .sugg li:before { content: '→'; color: var(--metal); position: absolute; left: -18px; }
  .meta { padding: 0 18px 16px; font-size: 11px; color: var(--ink-mute); display: flex; gap: 16px; flex-wrap: wrap; letter-spacing: 0.04em; }
  .meta b { color: var(--ink); font-weight: 500; font-variant-numeric: tabular-nums; }
  .audit-stamp { padding: 0 18px 14px; font-size: 10px; color: var(--ink-mute); letter-spacing: .12em; text-transform: uppercase; font-weight: 500; opacity: .7; }
  .empty { padding: 80px 28px; text-align: center; color: var(--ink-mute); font-style: italic; }
  .lightbox { position: fixed; inset: 0; background: rgba(5,5,7,0.95); display: none; align-items: center; justify-content: center; z-index: 100; padding: 20px; cursor: zoom-out; }
  .lightbox.show { display: flex; }
  .lightbox img { max-width: 100%; max-height: 100%; border: 1px solid var(--rule); }

*:focus-visible{outline:none;box-shadow:0 0 0 3px rgba(184,153,104,.45);border-radius:1px}
*:focus:not(:focus-visible){outline:none}

</style>
</head>
<body>

<header>
  <a href="/" style="display:inline-flex;align-items:center;gap:14px;text-decoration:none"><svg width="28" height="28" viewBox="0 0 28 28" aria-hidden="true" style="flex:0 0 28px"><defs><linearGradient id="cb-aud" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stop-color="#d4b683"/><stop offset="50%" stop-color="#b89968"/><stop offset="100%" stop-color="#8a7044"/></linearGradient></defs><circle cx="14" cy="14" r="13" fill="none" stroke="url(#cb-aud)" stroke-width="1"/><path fill="url(#cb-aud)" d="M9.6 9.5c-1.6 0-2.7 1.2-2.7 3v3c0 1.8 1.1 3 2.7 3 1.4 0 2.4-.8 2.6-2.1h-1.3c-.1.6-.6 1-1.3 1-.9 0-1.4-.6-1.4-1.7v-2.4c0-1.1.5-1.7 1.4-1.7.7 0 1.2.4 1.3 1h1.3c-.2-1.3-1.2-2.1-2.6-2.1zm5.5.1v8.7h2.7c1.6 0 2.6-.9 2.6-2.4 0-1-.5-1.7-1.3-2 .7-.3 1.1-1 1.1-1.8 0-1.5-1-2.5-2.5-2.5h-2.6zm1.3 1.1h1.2c.8 0 1.4.5 1.4 1.4 0 .9-.5 1.4-1.4 1.4h-1.2v-2.8zm0 3.9h1.4c.9 0 1.4.6 1.4 1.5s-.5 1.5-1.4 1.5h-1.4V14.6z"/></svg><h1 style="color:var(--ink)">Counsel <em>&amp;</em> Bar — Site Audit</h1></a>
  <span class="stat" id="stat-count">loading…</span>
  <span class="stat" id="stat-avg"></span>
  <div class="filter">
    <label class="stat">Min score <span id="min-score-val">0</span></label>
    <input type="range" id="min-score" min="0" max="100" value="0" step="5">
  </div>
</header>

<main>
  <section class="audit-masthead">
    <div>
      <div class="public-vol">
        <span class="v-rule"></span>
        <span class="v-text">Volume I</span>
        <span class="v-meta">Edition 2026 · Independent Site Audit</span>
      </div>
      <h2 class="audit-display">A standard the registry hasn't <em>codified.</em></h2>
      <p class="audit-lede">Independent audit of every California-licensed firm with a published website. Twelve technical and presentation signals, weighted, refresh cadence published. Firms do not pay for inclusion or score adjustment.</p>
    </div>
  </section>

  <section class="audit-distribution" aria-label="Tier distribution across audited sites">
    <div>
      <p class="ad-eyebrow">Distribution at a glance</p>
      <p class="ad-meta"><span id="dist-denominator">— audited firms</span> · <a href="/methodology.html" style="color:var(--ink-mute);border-bottom:1px dotted var(--rule);text-decoration:none;font-style:normal;font-family:var(--sans);font-size:11px;letter-spacing:.04em">methodology v1.0 →</a></p>
    </div>
    <div class="ad-bars">
      <div class="ad-bar ad-tier-a">
        <div class="ad-bar-tier"><span>High · 90+</span><b id="dist-a-pct">—</b></div>
        <div class="ad-bar-track"><div class="ad-bar-fill" id="dist-a-fill"></div></div>
        <div class="ad-bar-meta"><span id="dist-a-n">— sites</span></div>
      </div>
      <div class="ad-bar ad-tier-b">
        <div class="ad-bar-tier"><span>Mid · 70–89</span><b id="dist-b-pct">—</b></div>
        <div class="ad-bar-track"><div class="ad-bar-fill" id="dist-b-fill"></div></div>
        <div class="ad-bar-meta"><span id="dist-b-n">— sites</span></div>
      </div>
      <div class="ad-bar ad-tier-c">
        <div class="ad-bar-tier"><span>Low · 50–69</span><b id="dist-c-pct">—</b></div>
        <div class="ad-bar-track"><div class="ad-bar-fill" id="dist-c-fill"></div></div>
        <div class="ad-bar-meta"><span id="dist-c-n">— sites</span></div>
      </div>
      <div class="ad-bar ad-tier-d">
        <div class="ad-bar-tier"><span>Weak · &lt;50</span><b id="dist-d-pct">—</b></div>
        <div class="ad-bar-track"><div class="ad-bar-fill" id="dist-d-fill"></div></div>
        <div class="ad-bar-meta"><span id="dist-d-n">— sites</span></div>
      </div>
    </div>
  </section>


  <div class="grid" id="grid"></div>
  <div class="empty" id="empty" style="display:none">No audited sites yet — run <code>npx tsx src/enrich/site_audit.ts</code> first.</div>

  <p class="audit-foot">Counsel &amp; Bar publishes this audit independently. No firm pays for inclusion or score adjustment.</p>
</main>

<div class="lightbox" id="lightbox" onclick="this.classList.remove('show')"><img id="lightbox-img" alt=""></div>

<script>
let DATA = [];

function fmtBytes(b) {
  if (!b) return '–';
  if (b > 1e6) return (b / 1e6).toFixed(1) + ' MB';
  if (b > 1e3) return (b / 1e3).toFixed(0) + ' KB';
  return b + ' B';
}

function scoreClass(s) {
  if (s == null) return 'tier-x';
  if (s >= 90) return 'tier-a';
  if (s >= 70) return 'tier-b';
  if (s >= 50) return 'tier-c';
  return 'tier-d';
}

function scoreGrade(s) {
  if (s == null) return '';
  if (s >= 95) return 'A+';
  if (s >= 90) return 'A';
  if (s >= 85) return 'A−';
  if (s >= 80) return 'B+';
  if (s >= 70) return 'B';
  if (s >= 60) return 'C+';
  if (s >= 50) return 'C';
  return 'C−';
}

function pill(name, on) {
  return `<span class="pill ${on ? 'on' : 'off'}">${name}</span>`;
}

function render() {
  const min = +document.getElementById('min-score').value;
  document.getElementById('min-score-val').textContent = min;
  const filtered = DATA.filter(r => (r.marketing_score || 0) >= min);
  const grid = document.getElementById('grid');
  const empty = document.getElementById('empty');
  grid.innerHTML = '';
  if (filtered.length === 0) { empty.style.display = 'block'; return; }
  empty.style.display = 'none';

  const avg = Math.round(filtered.reduce((a, r) => a + (r.marketing_score || 0), 0) / filtered.length);
  document.getElementById('stat-count').textContent = `${filtered.length} sites`;
  document.getElementById('stat-avg').textContent = `· avg score ${avg}`;

  // Tier distribution
  const tiers = { a: 0, b: 0, c: 0, d: 0 };
  for (const r of filtered) {
    const s = r.marketing_score || 0;
    if (s >= 90) tiers.a++;
    else if (s >= 70) tiers.b++;
    else if (s >= 50) tiers.c++;
    else tiers.d++;
  }
  const total = filtered.length;
  // Denominator line — exposes the implicit "filtered ≠ population" so the chart can't be misread as the population.
  const denom = document.getElementById('dist-denominator');
  if (denom) {
    if (min > 0) {
      denom.textContent = `${total.toLocaleString()} of ${DATA.length.toLocaleString()} audited firms shown`;
    } else {
      denom.textContent = `${total.toLocaleString()} audited firm${total === 1 ? '' : 's'}`;
    }
  }
  for (const k of ['a','b','c','d']) {
    const n = tiers[k];
    const pct = total ? Math.round((n / total) * 100) : 0;
    document.getElementById(`dist-${k}-pct`).textContent = `${pct}%`;
    document.getElementById(`dist-${k}-n`).textContent = `${n.toLocaleString()} site${n === 1 ? '' : 's'}`;
    document.getElementById(`dist-${k}-fill`).style.width = `${pct}%`;
  }
  // After first paint shorten the bar transition so slider drags don't replay 700ms each tick.
  const bars = document.querySelector('.ad-bars');
  if (bars && !bars.dataset.painted) {
    requestAnimationFrame(() => { bars.dataset.painted = '1'; });
  }

  for (const r of filtered) {
    const palette = (r.palette || []).slice(0, 6);
    const swatchHtml = palette.map(c => `<div class="swatch" style="background:${c.hex}" title="${c.hex} — ${(c.fraction*100).toFixed(0)}%"></div>`).join('');
    const sigs = [
      pill('HTTPS', r.has_https),
      pill('Favicon', r.has_favicon),
      pill('Social card', r.has_og_image),
      pill('Mobile-ready', r.has_meta_viewport),
      pill('Analytics', r.has_analytics),
      pill('H1 heading', r.has_h1),
      pill('Schema.org', r.has_schema_org),
      pill('Phone visible', r.has_phone_visible),
    ].join('');
    const sugList = (r.suggestions || []).slice(0, 4).map(s => `<li>${s.replace(/</g,'&lt;')}</li>`).join('');
    const card = document.createElement('div');
    card.className = 'card';
    card.innerHTML = `
      <div class="shot" style="background-image:url('${r.screenshot_path || ''}')" onclick="openLightbox('${r.screenshot_path || ''}')"></div>
      <div class="row">
        <div class="name">${r.name.replace(/</g,'&lt;')}</div>
        <div class="url"><a href="${r.final_url || r.url}" target="_blank" rel="noopener">${(r.final_url || r.url).replace(/^https?:\/\//,'')}</a></div>
      </div>
      <div class="score-row">
        <div class="score ${scoreClass(r.marketing_score)}">${r.marketing_score ?? 'unaudited'}</div>
        <div class="score-label">site<br>signals</div>
      </div>
      <div class="palette">${swatchHtml}</div>
      <div class="signals">${sigs}</div>
      <div class="meta">
        <span><b>${fmtBytes(r.page_size_bytes)}</b> page</span>
        <span><b>${r.load_time_ms ? (r.load_time_ms/1000).toFixed(1)+'s' : '–'}</b> load</span>
        <span><b>${r.font_count ?? '–'}</b> fonts</span>
        <span><b>${r.image_count ?? '–'}</b> images</span>
      </div>
      ${r.audited_at ? `<div class="audit-stamp">Audited ${new Date(r.audited_at).toLocaleDateString('en-US',{month:'short',day:'numeric',year:'numeric'})} · <a href="/methodology.html" style="color:var(--ink-mute);border-bottom:1px dotted var(--rule);padding-bottom:1px">methodology v1.0</a></div>` : ''}
      ${sugList ? `<div class="sugg"><b>Suggestions</b><ul>${sugList}</ul></div>` : ''}
    `;
    grid.appendChild(card);
  }
}

function openLightbox(src) {
  if (!src) return;
  document.getElementById('lightbox-img').src = src;
  document.getElementById('lightbox').classList.add('show');
}

async function load() {
  const r = await fetch('/api/audits?limit=2000');
  const j = await r.json();
  DATA = j.rows || [];
  render();
}

document.getElementById('min-score').addEventListener('input', render);
load();
</script>

</body>
</html>