← back to Small Business Builder

src/render/website-analysis.js

481 lines

// website-analysis app — entry page + result page (brutalist theme).
//
// Brutalist design system:
//   bg #fff, ink #000, accent #facc15, secondary-accent #dc2626 (sparingly)
//   typefaces: 'Archivo Black' (display) + 'Space Mono' (UI/body)
//   hard borders, no rounded corners, tape-marquee, no-nonsense.

import { esc } from '../lib/escape.js';
import { computeGrade } from '../lib/audit-grade.js';

const HEAD = (title) => `<!doctype html><html lang="en"><head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>${esc(title)}</title>
<link href="https://fonts.googleapis.com/css2?family=Archivo+Black&family=Space+Mono:wght@400;700&display=swap" rel="stylesheet">
<style>
:root{--bg:#fff;--ink:#000;--paper:#fafaf6;--accent:#facc15;--muted:#666;--soft:#fef3c7;--soft-hi:#fef2f2;--green:#f0fdf4}
[data-theme="dark"]{--bg:#0a0a08;--ink:#fafaf6;--paper:#1a1a18;--accent:#facc15;--muted:#999;--soft:#1f1f1d;--soft-hi:#1f0a0a;--green:#0a2014}
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:'Space Mono',monospace;background:var(--bg);color:var(--ink);line-height:1.4;min-height:100vh;display:flex;flex-direction:column;transition:background .15s,color .15s}
a{color:inherit}
.theme-toggle{background:transparent;color:#fff;border:1px solid #fff;padding:8px 12px;font-family:'Archivo Black',sans-serif;font-size:12px;cursor:pointer;text-transform:uppercase;letter-spacing:.04em;margin-left:8px}
.theme-toggle:hover{background:var(--accent);color:#000;border-color:var(--accent)}
.tape{background:#facc15;border-bottom:4px solid #000;padding:8px 0;overflow:hidden;white-space:nowrap;font-size:13px;font-weight:700;letter-spacing:.04em}
.tape span{display:inline-block;animation:scroll 40s linear infinite}
.tape span::after{content:'  ★  WEBSITE ANALYSIS  ★  POWERED BY 7 SKILLS  ★  AGENT ABRAMS  ★  '}
@keyframes scroll{0%{transform:translateX(0)}100%{transform:translateX(-50%)}}
.hd{background:#000;color:#fff;border-bottom:4px solid #000}
.hd-inner{max-width:1320px;margin:0 auto;padding:14px 24px;display:flex;align-items:center;justify-content:space-between;gap:24px}
.lg{font-family:'Archivo Black',sans-serif;font-size:28px;letter-spacing:-.04em;text-transform:uppercase}
.lg::after{content:'.';color:#facc15}
.nv{display:flex;font-size:12px;font-weight:700;text-transform:uppercase}
.nv a{padding:8px 14px;border:1px solid #fff;margin-right:-1px;text-decoration:none}
.nv a.active{background:#facc15;color:#000}
main{flex:1}
.ft{background:#000;color:#aaa;padding:24px;text-align:center;font-size:11px;letter-spacing:.04em;text-transform:uppercase;border-top:4px solid #000}
.ft a{color:#facc15;text-decoration:none}
</style>
<script>(function(){try{var t=localStorage.getItem('lsa-theme');if(t==='dark')document.documentElement.setAttribute('data-theme','dark')}catch(e){}})()</script>
</head><body>
<div class="tape"><span></span></div>
<header class="hd"><div class="hd-inner">
  <div class="lg">WEBSITE / ANALYSIS</div>
  <nav class="nv"><a class="active" href="/website-analysis">START</a><a href="/wall">THE WALL</a><a href="/grades">GRADES</a><button class="theme-toggle" id="theme-toggle" title="dark/light">★ <span id="theme-icon">☾</span></button></nav>
</div></header>
<script>(function(){const t=document.getElementById('theme-toggle'),i=document.getElementById('theme-icon');function r(){const d=document.documentElement.getAttribute('data-theme')==='dark';if(i)i.textContent=d?'☀':'☾'}r();t?.addEventListener('click',()=>{const d=document.documentElement.getAttribute('data-theme')==='dark';if(d){document.documentElement.removeAttribute('data-theme');localStorage.setItem('lsa-theme','light')}else{document.documentElement.setAttribute('data-theme','dark');localStorage.setItem('lsa-theme','dark')}r()})})()</script>
<main>`;

const FOOT = `</main>
<footer class="ft">© ${new Date().getFullYear()} AGENT ABRAMS ★ ALL ANALYSIS RUNS LOCALLY ★ <a href="https://sdccanalysis.agentabrams.com">REFERENCE PATTERN</a></footer>
</body></html>`;

// ---------------------------------------------------------------------------
// /website-analysis — entry page
// ---------------------------------------------------------------------------
export function renderEntry({ error = '' } = {}) {
  return HEAD('Website Analysis — Get Started') + `
<section style="padding:96px 24px;border-bottom:4px solid #000">
  <div style="max-width:880px;margin:0 auto">
    <div style="display:inline-block;border:3px solid #000;padding:8px 14px;font-size:12px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;margin-bottom:24px;transform:rotate(-1.5deg);background:#dc2626;color:#fff">★ ANALYZE ANY SITE ★</div>
    <h1 style="font-family:'Archivo Black',sans-serif;font-size:120px;line-height:.85;letter-spacing:-.05em;text-transform:uppercase;margin-bottom:24px">SEE YOUR<br>SITE — <mark style="background:#facc15;padding:0 8px">REBUILT.</mark></h1>
    <p style="font-size:18px;line-height:1.5;font-weight:500;max-width:60ch;margin-bottom:48px">Drop in your website. Add your Instagram. We'll show you your site as it is — and three new directions you could take it. No signup. No spam.</p>

    ${error ? `<div style="border:4px solid #dc2626;background:#fef2f2;color:#dc2626;padding:16px;font-weight:700;margin-bottom:24px;text-transform:uppercase;font-size:13px">★ ${esc(error)}</div>` : ''}

    <form method="post" action="/api/website-analysis" style="display:grid;gap:0;border:4px solid #000;background:#fff">
      <div style="display:grid;grid-template-columns:200px 1fr;border-bottom:4px solid #000">
        <label for="url" style="background:#000;color:#facc15;padding:24px;font-family:'Archivo Black',sans-serif;font-size:18px;text-transform:uppercase;letter-spacing:.04em;border-right:4px solid #000;display:flex;align-items:center">★ WEBSITE</label>
        <input id="url" name="url" type="text" required placeholder="yourbusiness.com" autocomplete="url"
          style="padding:24px;border:0;font-family:'Space Mono',monospace;font-size:18px;font-weight:700;background:#fff;outline:none">
      </div>
      <div style="display:grid;grid-template-columns:200px 1fr;border-bottom:4px solid #000">
        <label for="ig" style="background:#000;color:#facc15;padding:24px;font-family:'Archivo Black',sans-serif;font-size:18px;text-transform:uppercase;letter-spacing:.04em;border-right:4px solid #000;display:flex;align-items:center">★ INSTAGRAM</label>
        <input id="ig" name="ig_handle" type="text" placeholder="@yourbusiness  (optional)"
          style="padding:24px;border:0;font-family:'Space Mono',monospace;font-size:18px;font-weight:700;background:#fff;outline:none">
      </div>
      <button type="submit"
        style="background:#facc15;color:#000;padding:28px;border:0;font-family:'Archivo Black',sans-serif;font-size:24px;text-transform:uppercase;letter-spacing:.04em;cursor:pointer;border-top:0">
        GET STARTED →
      </button>
    </form>

    <p style="margin-top:32px;font-size:12px;color:#666;font-weight:700;text-transform:uppercase;letter-spacing:.04em">★ NO ACCOUNT ★ NO STORED PII ★ ANALYSIS RUNS IN ~30 SECONDS · <a href="/wall" style="color:#000;text-decoration:none;border-bottom:2px solid #facc15">SEE THE WALL →</a></p>
  </div>
</section>

<section style="padding:48px 24px;background:#000;color:#facc15;border-bottom:4px solid #000">
  <div style="max-width:880px;margin:0 auto">
    <div style="display:flex;justify-content:space-between;align-items:flex-end;flex-wrap:wrap;gap:18px;margin-bottom:18px">
      <div>
        <div style="font-size:11px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:#facc15;margin-bottom:6px">★ NO COMMITMENT</div>
        <h2 style="font-family:'Archivo Black',sans-serif;font-size:40px;text-transform:uppercase;letter-spacing:-.02em;color:#fafaf6;margin:0">JUST WANT A ROAST?</h2>
      </div>
      <div style="font-size:13px;color:#aaa;font-weight:500;max-width:32ch">qwen3:14b will read your URL and write a stand-up paragraph dragging your design choices. Loving but savage. Free. Local. ~10 sec.</div>
    </div>
    <form id="roast-form" style="display:flex;gap:0;border:4px solid #facc15;background:#000">
      <input id="roast-url" type="text" required placeholder="https://yoursite.com" autocomplete="url"
        style="flex:1;padding:18px;border:0;font-family:'Space Mono',monospace;font-size:16px;font-weight:700;background:#000;color:#facc15;outline:none">
      <button type="submit"
        style="background:#facc15;color:#000;padding:18px 28px;border:0;font-family:'Archivo Black',sans-serif;font-size:18px;text-transform:uppercase;letter-spacing:.04em;cursor:pointer;border-left:4px solid #facc15">★ ROAST ME →</button>
    </form>
    <div id="roast-out" style="margin-top:18px;font-family:'Space Mono',monospace;font-size:16px;line-height:1.5;color:#fafaf6;min-height:24px"></div>
  </div>
</section>

<script>
(function(){
  const form = document.getElementById('roast-form');
  const out = document.getElementById('roast-out');
  if (!form || !out) return;
  form.addEventListener('submit', async (e) => {
    e.preventDefault();
    const url = document.getElementById('roast-url').value.trim();
    if (!url) return;
    const u = /^https?:\\/\\//i.test(url) ? url : 'https://' + url;
    out.style.color = '#aaa';
    out.textContent = '★ qwen3 is judging you…';
    try {
      const r = await fetch('/api/roast-me', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ url: u }) });
      const data = await r.json();
      if (!r.ok) { out.style.color = '#f87171'; out.textContent = '★ ' + (data.error || 'something broke'); return; }
      out.style.color = '#fafaf6';
      const safe = (s) => s.replace(/[<>&]/g, c => ({'<':'&lt;','>':'&gt;','&':'&amp;'}[c]));
      const tweet = encodeURIComponent('★ qwen3 just roasted ' + data.host + ':\\n\\n"' + data.roast.slice(0, 200) + (data.roast.length > 200 ? '…' : '') + '"\\n\\n— roasted free by livesiteaudit.agentabrams.com');
      const tweetUrl = 'https://twitter.com/intent/tweet?text=' + tweet;
      out.innerHTML = '<div style="border-left:4px solid #facc15;padding:8px 16px;font-style:italic">' + safe(data.roast) + '</div>' +
        '<div style="display:flex;justify-content:space-between;align-items:center;margin-top:10px;flex-wrap:wrap;gap:10px">' +
          '<div style="font-size:11px;color:#aaa;letter-spacing:.06em;text-transform:uppercase">★ ROAST OF ' + safe(data.host) + '</div>' +
          '<a href="' + tweetUrl + '" target="_blank" rel="noopener" style="background:#facc15;color:#000;padding:8px 14px;text-decoration:none;font-family:Archivo Black,sans-serif;font-size:11px;text-transform:uppercase;letter-spacing:.04em;border:3px solid #facc15">★ TWEET THIS ROAST →</a>' +
        '</div>';
    } catch (err) {
      out.style.color = '#f87171';
      out.textContent = '★ network error — try again';
    }
  });
})();
</script>

<section style="padding:64px 24px;background:var(--soft)">
  <div style="max-width:1180px;margin:0 auto">
    <h2 style="font-family:'Archivo Black',sans-serif;font-size:40px;text-transform:uppercase;letter-spacing:-.02em;margin-bottom:32px">★ HOW IT WORKS</h2>
    <div style="display:grid;grid-template-columns:repeat(3,1fr);gap:0;border:4px solid var(--ink);background:var(--paper);color:var(--ink)">
      <div style="padding:32px;border-right:4px solid #000">
        <div style="font-family:'Archivo Black',sans-serif;font-size:64px;color:#facc15;background:#000;display:inline-block;padding:0 12px;line-height:1;margin-bottom:16px">01</div>
        <h3 style="font-family:'Archivo Black',sans-serif;font-size:22px;text-transform:uppercase;margin-bottom:12px">YOUR SITE</h3>
        <p style="font-size:14px;line-height:1.5;font-weight:500">We pull your homepage, your colors, your images, your Instagram. Nothing private. All public stuff.</p>
      </div>
      <div style="padding:32px;border-right:4px solid #000;background:var(--soft)">
        <div style="font-family:'Archivo Black',sans-serif;font-size:64px;color:#facc15;background:#000;display:inline-block;padding:0 12px;line-height:1;margin-bottom:16px">02</div>
        <h3 style="font-family:'Archivo Black',sans-serif;font-size:22px;text-transform:uppercase;margin-bottom:12px">3 REBUILDS</h3>
        <p style="font-size:14px;line-height:1.5;font-weight:500">Three new directions — your same content, three completely different aesthetic systems. Pick one.</p>
      </div>
      <div style="padding:32px">
        <div style="font-family:'Archivo Black',sans-serif;font-size:64px;color:#facc15;background:#000;display:inline-block;padding:0 12px;line-height:1;margin-bottom:16px">03</div>
        <h3 style="font-family:'Archivo Black',sans-serif;font-size:22px;text-transform:uppercase;margin-bottom:12px">SHIP IT</h3>
        <p style="font-size:14px;line-height:1.5;font-weight:500">Like one? Push the button. We'll launch it on a free subdomain — or your own domain if you have one.</p>
      </div>
    </div>
  </div>
</section>
` + FOOT;
}

// ---------------------------------------------------------------------------
// /wa/:slug — result page (your site iframe + 3 mockup cards)
// ---------------------------------------------------------------------------
export function renderResult({ analysis, mockups, isPublic = false }) {
  const url = analysis.url;
  const ig = analysis.ig_handle ? `@${String(analysis.ig_handle).replace(/^@/, '')}` : null;
  const iframeBlocked = !!(analysis.meta_json && analysis.meta_json.iframe_blocked);
  const grade = computeGrade({ analysis });
  // Synthesize a palette deterministically from the slug + vertical when none
  // was scraped, so the result page is never empty. Vertical-tinted base hue.
  function synthPalette(slug, vertical) {
    const VERT_HUE = { salon:330, restaurant:18, lawyer:215, vet:160, advocacy:0, 'saas-landing':210, ecom:285, generic:265 };
    const base = VERT_HUE[vertical] ?? 220;
    let h = 0; for (let i = 0; i < slug.length; i += 1) h = (h * 31 + slug.charCodeAt(i)) >>> 0;
    const start = (base + (h % 60) - 30 + 360) % 360;
    const out = [];
    for (let i = 0; i < 5; i += 1) {
      const hue = (start + i * 24) % 360;
      const sat = 55 + ((h >> i) & 0xf) * 2;
      const lig = 32 + i * 12;
      out.push(`hsl(${hue} ${sat}% ${lig}%)`);
    }
    return out;
  }
  const rawPalette = (analysis.meta_json?.palette || []).slice(0, 5);
  const palette = rawPalette.length ? rawPalette : synthPalette(analysis.slug, analysis.vertical);
  const paletteSource = rawPalette.length ? 'scraped' : 'synthesized';
  const swatches = palette.map(c => `<div title="${esc(c)}" style="width:28px;height:28px;background:${esc(c)};border:3px solid #000"></div>`).join('');
  const verticalPill = analysis.vertical
    ? `<div style="display:inline-block;background:#000;color:#facc15;padding:4px 10px;font-family:'Archivo Black',sans-serif;font-size:12px;text-transform:uppercase;letter-spacing:.04em">★ ${esc(analysis.vertical)}</div>`
    : '';

  const cards = mockups.map(m => `
    <div class="cd" style="display:block;border:4px solid var(--ink);background:var(--paper)">
      <a href="/wa/${esc(analysis.slug)}/m/${esc(m.mockup_id)}" target="_blank" rel="noopener" style="display:block;text-decoration:none;color:#000">
        <div style="background:#000;color:#facc15;padding:14px 20px;font-family:'Archivo Black',sans-serif;font-size:14px;text-transform:uppercase;letter-spacing:.04em;border-bottom:4px solid #000;display:flex;justify-content:space-between;align-items:center">
          <span>★ OPTION ${esc(m.mockup_id.split('-')[0])}</span>
          <span>${esc(m.theme.toUpperCase())}</span>
        </div>
        <div style="aspect-ratio:4/3;position:relative;overflow:hidden;background:#fafaf6">
          <iframe src="/wa/${esc(analysis.slug)}/m/${esc(m.mockup_id)}"
                  style="position:absolute;inset:0;width:200%;height:200%;border:0;transform:scale(.5);transform-origin:0 0;pointer-events:none"
                  loading="lazy" sandbox="allow-same-origin"></iframe>
        </div>
        <div style="padding:18px 20px;border-top:4px solid #000;background:var(--soft)">
          <div style="font-family:'Archivo Black',sans-serif;font-size:20px;text-transform:uppercase;letter-spacing:-.01em;margin-bottom:6px">${esc(m.title)}</div>
          <div style="font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.04em;color:#666">★ CLICK TO PREVIEW FULL SIZE</div>
        </div>
      </a>
      ${isPublic ? '' : `<form method="post" action="/api/wa/${esc(analysis.slug)}/ship/${esc(m.mockup_id)}" style="margin:0">
        <button type="submit"
          style="width:100%;background:#facc15;color:#000;border:0;border-top:4px solid #000;padding:18px;font-family:'Archivo Black',sans-serif;font-size:18px;text-transform:uppercase;letter-spacing:.04em;cursor:pointer">
          ★ SHIP THIS REBUILD →
        </button>
      </form>`}
    </div>
  `).join('');

  // We must ALWAYS see the screen. When iframe is blocked, fall back to a Playwright
  // screenshot served from /wa/:slug/screenshot.png (disk-cached, 7-day TTL).
  const shotUrl = `/wa/${esc(analysis.slug)}/screenshot.png`;
  const yourSite = iframeBlocked
    ? `<div style="border:4px solid #000;background:#fff;position:relative;overflow:hidden">
        <a href="${esc(url)}" target="_blank" rel="noopener" style="display:block;line-height:0">
          <img src="${shotUrl}" alt="screenshot of ${esc(new URL(url).hostname)}"
               style="width:100%;height:auto;display:block;background:#f4f4f4 url('data:image/svg+xml;utf8,<svg xmlns=%22http://www.w3.org/2000/svg%22 width=%22120%22 height=%2240%22><text x=%2260%22 y=%2225%22 font-family=%22monospace%22 font-size=%2212%22 text-anchor=%22middle%22 fill=%22%23999%22>capturing screenshot…</text></svg>') no-repeat center;min-height:240px"
               onerror="this.parentElement.parentElement.innerHTML='<div style=&quot;padding:48px 32px;background:#000;color:#facc15;text-align:left&quot;><div style=&quot;font-size:11px;letter-spacing:.06em;text-transform:uppercase&quot;>★ COULD NOT CAPTURE PREVIEW</div><div style=&quot;font-family:Archivo Black,sans-serif;font-size:24px;text-transform:uppercase;color:#fff;margin-top:12px&quot;>OPEN ${esc(new URL(url).hostname).replace(/'/g,"&apos;")} TO SEE THE SITE</div><a href=&quot;${esc(url)}&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; style=&quot;display:inline-block;margin-top:18px;background:#facc15;color:#000;padding:12px 20px;text-decoration:none;font-family:Archivo Black,sans-serif;font-size:13px;text-transform:uppercase;letter-spacing:.04em&quot;>★ OPEN ↗</a></div>'">
        </a>
        <div style="position:absolute;top:14px;left:14px;background:#000;color:#facc15;padding:6px 12px;font-family:'Archivo Black',sans-serif;font-size:10px;letter-spacing:.06em;text-transform:uppercase;border:2px solid #facc15">★ STATIC SCREENSHOT — IFRAME BLOCKED</div>
        <a href="${esc(url)}" target="_blank" rel="noopener" style="position:absolute;top:14px;right:14px;background:#facc15;color:#000;padding:8px 14px;font-family:'Archivo Black',sans-serif;font-size:11px;text-transform:uppercase;letter-spacing:.04em;text-decoration:none;border:2px solid #000">OPEN LIVE ↗</a>
      </div>`
    : `<div style="border:4px solid #000;background:#fff;aspect-ratio:16/9;position:relative;overflow:hidden">
        <img src="${shotUrl}" alt="" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;object-position:top">
        <iframe src="${esc(url)}" style="position:absolute;inset:0;width:100%;height:100%;border:0;background:transparent" loading="lazy" referrerpolicy="no-referrer"></iframe>
      </div>`;

  // Live audit panel — only present when audit-public-sites.mjs has run on this row.
  const live = analysis.summary_json?.live_audit;
  const liveColor = live?.grade?.score >= 80 ? '#22c55e' : live?.grade?.score >= 65 ? '#84cc16' : live?.grade?.score >= 45 ? '#facc15' : live?.grade?.score >= 25 ? '#f97316' : '#dc2626';
  const fetchedAt = live?.fetched_at ? new Date(live.fetched_at) : null;
  const fetchedAtStr = fetchedAt && !Number.isNaN(fetchedAt.getTime()) ? fetchedAt.toLocaleString() : 'unknown';
  const liveScoreStr = String(live?.grade?.score ?? '');
  const liveBytesKB  = Number.isFinite(live?.bytes) ? Math.round(live.bytes / 1024) : 0;
  const liveFetchMs  = Number.isFinite(live?.fetch_ms) ? live.fetch_ms : 0;
  const livePanel = live ? `
<section style="padding:48px 24px;border-bottom:4px solid var(--ink);background:var(--paper)">
  <div style="max-width:1320px;margin:0 auto">
    <div style="display:flex;justify-content:space-between;align-items:flex-end;gap:16px;flex-wrap:wrap;margin-bottom:14px">
      <div>
        <div style="font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:#666;margin-bottom:6px">★ LIVE HTML AUDIT · FROM A FRESH FETCH</div>
        <h2 style="font-family:'Archivo Black',sans-serif;font-size:32px;text-transform:uppercase;letter-spacing:-.02em">WHAT YOUR SITE ACTUALLY SAYS</h2>
      </div>
      <div title="Real HTML signals — title length, meta description, og:image, viewport, alt-text coverage, JSON-LD, etc."
           style="display:flex;align-items:center;gap:14px;background:${liveColor};color:#000;border:4px solid #000;padding:8px 18px">
        <div style="font-family:'Archivo Black',sans-serif;font-size:42px;line-height:1">${esc(live.grade.letter)}</div>
        <div>
          <div style="font-family:'Archivo Black',sans-serif;font-size:11px;text-transform:uppercase;letter-spacing:.06em">★ LIVE HTML</div>
          <div style="font-family:'Space Mono',monospace;font-size:11px;font-weight:700">${esc(liveScoreStr)}/100</div>
        </div>
      </div>
    </div>
    <p style="font-size:13px;color:#444;margin-bottom:22px;font-weight:500;max-width:80ch">Signals from a live fetch: title, meta description, og:image, viewport, alt-text coverage, JSON-LD, HTTPS, lang attribute, canonical, h1 count.</p>
    <div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:18px">
      <div style="border:4px solid #000;background:#fef2f2;padding:18px 22px">
        <div style="font-family:'Archivo Black',sans-serif;font-size:14px;text-transform:uppercase;letter-spacing:.04em;color:var(--ink);margin-bottom:12px;border-bottom:2px solid var(--ink);padding-bottom:8px">TOP ISSUES</div>
        ${(live.top_3_issues?.length ? live.top_3_issues : ['(none — site is well-tagged)']).map(i => `<div style="font-size:14px;font-weight:600;line-height:1.5;margin-bottom:6px">→ ${esc(i)}</div>`).join('')}
      </div>
      <div style="border:4px solid #000;background:#f0fdf4;padding:18px 22px">
        <div style="font-family:'Archivo Black',sans-serif;font-size:14px;text-transform:uppercase;letter-spacing:.04em;color:var(--ink);margin-bottom:12px;border-bottom:2px solid var(--ink);padding-bottom:8px">WHAT'S WORKING</div>
        ${(live.top_3_wins?.length ? live.top_3_wins : ['(none yet)']).map(w => `<div style="font-size:14px;font-weight:600;line-height:1.5;margin-bottom:6px">→ ${esc(w)}</div>`).join('')}
      </div>
    </div>
    <div style="margin-top:14px;font-size:11px;font-weight:700;color:#666;font-family:'Space Mono',monospace">★ FETCHED ${esc(fetchedAtStr)} · ${esc(String(liveBytesKB))}KB · ${esc(String(liveFetchMs))}MS</div>
  </div>
</section>` : '';

  // JSON-LD for /wa/:slug — review-style structured data with grade as rating.
  const jsonLd = {
    '@context': 'https://schema.org',
    '@type': 'Review',
    itemReviewed: { '@type': 'WebSite', url: analysis.url, name: analysis.url ? new URL(analysis.url).hostname : analysis.slug },
    author: { '@type': 'Organization', name: 'Live Site Audit', url: 'https://livesiteaudit.agentabrams.com' },
    ...(live ? { reviewRating: { '@type': 'Rating', ratingValue: live.grade?.score, bestRating: 100, worstRating: 0 } } : {}),
    reviewBody: analysis.summary_json?.copy_pack?.tagline || analysis.summary_json?.tagline || analysis.meta_json?.tagline || '',
    url: `https://livesiteaudit.agentabrams.com/wa/${analysis.slug}`,
  };
  return HEAD(`${analysis.slug} — Website Analysis`).replace('</head>', `<script type="application/ld+json">${JSON.stringify(jsonLd)}</script></head>`) + `
<section style="padding:48px 24px 24px;border-bottom:4px solid #000">
  <div style="max-width:1320px;margin:0 auto">
    <div style="display:flex;justify-content:space-between;align-items:flex-end;gap:24px;margin-bottom:24px;flex-wrap:wrap">
      <div>
        <div style="font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:#666;margin-bottom:8px">★ ANALYZING</div>
        <h1 style="font-family:'Archivo Black',sans-serif;font-size:56px;line-height:.9;letter-spacing:-.03em;text-transform:uppercase">${esc(url.replace(/^https?:\/\//,'').replace(/\/$/,''))}</h1>
        ${ig ? `<div style="margin-top:8px;font-size:14px;font-weight:700">★ <a href="https://instagram.com/${esc(ig.replace(/^@/, ''))}" style="text-decoration:none;border-bottom:2px solid #000">${esc(ig)}</a></div>` : ''}
      </div>
      <div style="display:flex;align-items:center;gap:16px;flex-wrap:wrap">
        <div title="Audit grade based on metadata completeness — title, tagline, palette, hero, IG, vertical detection, embedability"
             style="display:flex;align-items:center;gap:14px;background:${grade.color};color:#000;border:4px solid #000;padding:8px 18px">
          <div style="font-family:'Archivo Black',sans-serif;font-size:48px;line-height:1">${grade.letter}</div>
          <div>
            <div style="font-family:'Archivo Black',sans-serif;font-size:11px;text-transform:uppercase;letter-spacing:.06em">★ AUDIT</div>
            <div style="font-family:'Space Mono',monospace;font-size:11px;font-weight:700">${grade.score}/100</div>
          </div>
        </div>
        <a href="/website-analysis" style="background:#000;color:#fff;padding:14px 24px;text-decoration:none;font-family:'Archivo Black',sans-serif;font-size:13px;text-transform:uppercase;letter-spacing:.04em;border:4px solid #000">← ANALYZE ANOTHER</a>
        <form method="post" action="/api/wa/${esc(analysis.slug)}/deep-dive" style="display:inline;margin:0">
          <button type="submit" title="Synthesize a mini site-audit report (SEO + content + composition checks, vertical-specific tips, suggested headlines)" style="background:#facc15;color:#000;padding:14px 24px;font-family:'Archivo Black',sans-serif;font-size:13px;text-transform:uppercase;letter-spacing:.04em;border:4px solid #000;cursor:pointer">★ DEEP DIVE →</button>
        </form>
      </div>
    </div>

    <div style="display:flex;align-items:center;gap:14px;flex-wrap:wrap;padding:14px 0;border-top:2px solid #000;border-bottom:2px solid #000">
      ${verticalPill}
      ${swatches ? `<div style="display:flex;align-items:center;gap:8px"><div style="font-size:11px;font-weight:700;color:#666;text-transform:uppercase;letter-spacing:.06em">PALETTE${paletteSource === 'synthesized' ? ' (SYNTH)' : ''}</div><div style="display:flex;gap:0">${swatches}</div></div>` : ''}
      ${iframeBlocked ? '<div style="display:inline-block;background:#dc2626;color:#fff;padding:4px 10px;font-family:\'Archivo Black\',sans-serif;font-size:11px;text-transform:uppercase;letter-spacing:.04em">★ NO IFRAME</div>' : '<div style="display:inline-block;background:#22c55e;color:#000;padding:4px 10px;font-family:\'Archivo Black\',sans-serif;font-size:11px;text-transform:uppercase;letter-spacing:.04em">★ EMBEDABLE</div>'}
      ${analysis.business_id ? '<div style="display:inline-block;background:#facc15;color:#000;padding:4px 10px;font-family:\'Archivo Black\',sans-serif;font-size:11px;text-transform:uppercase;letter-spacing:.04em">★ SHIPPED</div>' : ''}
    </div>
  </div>
</section>
${livePanel}
<section style="padding:32px 24px;background:var(--soft);border-bottom:4px solid #000">
  <div style="max-width:1320px;margin:0 auto">
    <h2 style="font-family:'Archivo Black',sans-serif;font-size:32px;text-transform:uppercase;letter-spacing:-.02em;margin-bottom:24px">★ YOUR SITE — RIGHT NOW</h2>
    ${yourSite}
  </div>
</section>

<section style="padding:64px 24px">
  <div style="max-width:1320px;margin:0 auto">
    <h2 style="font-family:'Archivo Black',sans-serif;font-size:48px;text-transform:uppercase;letter-spacing:-.03em;margin-bottom:8px">★ 3 NEW DIRECTIONS</h2>
    <p style="font-size:15px;font-weight:700;text-transform:uppercase;letter-spacing:.04em;color:#666;margin-bottom:32px">★ SAME CONTENT — THREE COMPLETELY DIFFERENT AESTHETIC SYSTEMS</p>
    <div style="display:grid;grid-template-columns:repeat(3,1fr);gap:24px">${cards}</div>
  </div>
</section>

${mockups.length >= 2 ? `
<section style="padding:64px 24px;background:#000;color:#fafaf6;border-top:4px solid #facc15;border-bottom:4px solid #000">
  <div style="max-width:1320px;margin:0 auto">
    <div style="display:flex;justify-content:space-between;align-items:flex-end;flex-wrap:wrap;gap:18px;margin-bottom:24px">
      <div>
        <div style="font-size:11px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:#facc15;margin-bottom:6px">★ A/B SLIDER</div>
        <h2 style="font-family:'Archivo Black',sans-serif;font-size:40px;text-transform:uppercase;letter-spacing:-.02em;color:#fff">DRAG TO COMPARE</h2>
      </div>
      <div style="display:flex;gap:8px;flex-wrap:wrap">
        ${mockups.slice(0, 6).map((m, i) => `<button class="ab-pick" data-side="left" data-id="${esc(m.mockup_id)}" data-label="${esc(m.theme.toUpperCase())}" style="background:${i === 0 ? '#facc15' : '#222'};color:${i === 0 ? '#000' : '#facc15'};padding:8px 12px;font-family:'Space Mono',monospace;font-size:11px;font-weight:700;border:2px solid #facc15;cursor:pointer;text-transform:uppercase;letter-spacing:.04em">${esc(m.theme.toUpperCase())}</button>`).join('')}
      </div>
    </div>
    <div id="ab-slider" style="position:relative;border:4px solid #facc15;background:#fff;aspect-ratio:16/9;overflow:hidden;user-select:none">
      <iframe id="ab-left"  src="/wa/${esc(analysis.slug)}/m/${esc(mockups[0].mockup_id)}" style="position:absolute;inset:0;width:100%;height:100%;border:0"  loading="lazy" sandbox="allow-same-origin"></iframe>
      <div id="ab-right-wrap" style="position:absolute;inset:0;width:50%;overflow:hidden;border-right:3px solid #facc15">
        <iframe id="ab-right" src="/wa/${esc(analysis.slug)}/m/${esc(mockups[1].mockup_id)}" style="position:absolute;inset:0;width:200%;height:100%;border:0" loading="lazy" sandbox="allow-same-origin"></iframe>
      </div>
      <div id="ab-handle" style="position:absolute;top:0;bottom:0;left:50%;width:6px;background:#facc15;cursor:ew-resize;z-index:10;transform:translateX(-50%)">
        <div style="position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);background:#000;color:#facc15;width:48px;height:48px;border:3px solid #facc15;display:flex;align-items:center;justify-content:center;font-family:'Archivo Black',sans-serif;font-size:24px">↔</div>
      </div>
      <div style="position:absolute;top:14px;left:14px;background:#000;color:#facc15;padding:6px 10px;font-family:'Archivo Black',sans-serif;font-size:11px;letter-spacing:.04em;border:2px solid #facc15;z-index:5">★ <span id="ab-label-left">${esc(mockups[0].theme.toUpperCase())}</span></div>
      <div style="position:absolute;top:14px;right:14px;background:#000;color:#facc15;padding:6px 10px;font-family:'Archivo Black',sans-serif;font-size:11px;letter-spacing:.04em;border:2px solid #facc15;z-index:5"><span id="ab-label-right">${esc(mockups[1].theme.toUpperCase())}</span> ★</div>
    </div>
    <p style="margin-top:14px;font-size:12px;font-weight:700;letter-spacing:.04em;color:#aaa;text-transform:uppercase">★ DRAG THE GOLD HANDLE LEFT/RIGHT — TAP THE THEME CHIPS TO SWAP EITHER SIDE</p>
  </div>
</section>

<script>
(function(){
  const wrap = document.getElementById('ab-right-wrap');
  const handle = document.getElementById('ab-handle');
  const slider = document.getElementById('ab-slider');
  const leftIfr = document.getElementById('ab-left');
  const rightIfr = document.getElementById('ab-right');
  const labL = document.getElementById('ab-label-left');
  const labR = document.getElementById('ab-label-right');
  if (!wrap || !handle || !slider) return;

  let dragging = false;
  function setPos(pct) {
    pct = Math.max(2, Math.min(98, pct));
    wrap.style.width = pct + '%';
    handle.style.left = pct + '%';
    rightIfr.style.width = (10000 / pct) + '%';
  }
  function onMove(e) {
    if (!dragging) return;
    const rect = slider.getBoundingClientRect();
    const x = (e.touches ? e.touches[0].clientX : e.clientX) - rect.left;
    setPos((x / rect.width) * 100);
  }
  handle.addEventListener('mousedown', () => { dragging = true; });
  handle.addEventListener('touchstart', () => { dragging = true; }, {passive:true});
  document.addEventListener('mousemove', onMove);
  document.addEventListener('touchmove', onMove, {passive:true});
  document.addEventListener('mouseup',   () => { dragging = false; });
  document.addEventListener('touchend',  () => { dragging = false; });
  slider.addEventListener('click', (e) => {
    if (e.target === handle || handle.contains(e.target)) return;
    const rect = slider.getBoundingClientRect();
    setPos(((e.clientX - rect.left) / rect.width) * 100);
  });

  // Theme chip clicks: alternate left/right slot.
  let nextSide = 'right';
  document.querySelectorAll('.ab-pick').forEach(btn => {
    btn.addEventListener('click', () => {
      const id = btn.dataset.id;
      const label = btn.dataset.label;
      const slug = '${esc(analysis.slug)}';
      if (nextSide === 'right') {
        rightIfr.src = '/wa/' + slug + '/m/' + id;
        labR.textContent = label;
        nextSide = 'left';
      } else {
        leftIfr.src = '/wa/' + slug + '/m/' + id;
        labL.textContent = label;
        nextSide = 'right';
      }
      document.querySelectorAll('.ab-pick').forEach(b => {
        b.style.background = '#222'; b.style.color = '#facc15';
      });
      btn.style.background = '#facc15'; btn.style.color = '#000';
    });
  });
})();
</script>
` : ''}

${analysis.summary_json?.audit_report ? renderAuditReport(analysis.summary_json.audit_report) : ''}
` + FOOT;
}

function renderAuditReport(r) {
  if (!r || !r.sections) return '';
  const lvl = r.score >= 70 ? '#22c55e' : r.score >= 40 ? '#facc15' : '#dc2626';
  const findings = (label, sec) => {
    if (!sec.findings || !sec.findings.length) return '';
    return `<div style="border-top:2px solid #000;padding:16px 0">
      <div style="font-family:'Archivo Black',sans-serif;font-size:13px;text-transform:uppercase;letter-spacing:.04em;margin-bottom:8px;display:flex;justify-content:space-between"><span>★ ${esc(label)}</span><span style="color:${sec.score_delta >= 0 ? '#22c55e' : '#dc2626'}">${sec.score_delta >= 0 ? '+' : ''}${sec.score_delta}</span></div>
      <ul style="list-style:none;font-size:13px;line-height:1.5">
        ${sec.findings.map(f => `<li style="padding:4px 0;display:flex;gap:10px"><span style="color:${f.pts >= 0 ? '#22c55e' : '#dc2626'};font-weight:700;min-width:32px">${f.pts >= 0 ? '+' : ''}${f.pts}</span><span>${esc(f.msg)}</span></li>`).join('')}
      </ul>
    </div>`;
  };
  return `<section id="audit-report" style="padding:64px 24px;background:#000;color:#fff;border-top:4px solid #000">
    <div style="max-width:980px;margin:0 auto">
      <div style="display:flex;justify-content:space-between;align-items:flex-end;gap:24px;margin-bottom:32px;flex-wrap:wrap">
        <div>
          <div style="font-size:12px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:#facc15;margin-bottom:8px">★ DEEP DIVE</div>
          <h2 style="font-family:'Archivo Black',sans-serif;font-size:56px;text-transform:uppercase;letter-spacing:-.03em">AUDIT REPORT.</h2>
          <div style="font-size:11px;font-family:'Space Mono',monospace;color:#888;margin-top:4px">★ GENERATED ${esc(new Date(r.generated_at).toISOString().slice(0,16).replace('T',' '))}</div>
        </div>
        <div style="display:flex;align-items:center;gap:14px;background:${lvl};color:#000;border:4px solid #fff;padding:14px 24px">
          <div style="font-family:'Archivo Black',sans-serif;font-size:64px;line-height:1">${r.score}</div>
          <div>
            <div style="font-family:'Archivo Black',sans-serif;font-size:11px;text-transform:uppercase;letter-spacing:.06em">★ AUDIT SCORE</div>
            <div style="font-family:'Space Mono',monospace;font-size:11px;font-weight:700">/100</div>
          </div>
        </div>
      </div>
      <div style="background:#fff;color:#000;padding:24px;border:4px solid #fff">
        ${findings('SEO',         r.sections.seo)}
        ${findings('CONTENT',     r.sections.content)}
        ${findings('COMPOSITION', r.sections.composition)}
      </div>
      <div style="display:grid;grid-template-columns:1fr 1fr;gap:24px;margin-top:32px">
        <div style="background:#facc15;color:#000;padding:24px;border:4px solid #fff">
          <div style="font-family:'Archivo Black',sans-serif;font-size:18px;text-transform:uppercase;margin-bottom:14px">★ HEADLINES TO TRY</div>
          ${(r.suggested_headlines||[]).map((h, i) => `<div style="padding:10px 0;border-bottom:2px solid #000;font-size:14px;font-weight:700;line-height:1.35"><span style="font-family:'Archivo Black',sans-serif;margin-right:8px">${i+1}.</span>${esc(h)}</div>`).join('')}
        </div>
        <div style="background:#fff;color:#000;padding:24px;border:4px solid #fff">
          <div style="font-family:'Archivo Black',sans-serif;font-size:18px;text-transform:uppercase;margin-bottom:14px">★ VERTICAL TIPS</div>
          ${(r.vertical_tips||[]).map((t, i) => `<div style="padding:10px 0;border-bottom:2px solid #000;font-size:13px;line-height:1.5"><span style="font-family:'Archivo Black',sans-serif;margin-right:8px">${String.fromCharCode(65+i)}.</span>${esc(t)}</div>`).join('')}
        </div>
      </div>
      ${r.quick_wins?.length ? `<div style="margin-top:32px;background:#dc2626;color:#fff;padding:24px;border:4px solid #fff">
        <div style="font-family:'Archivo Black',sans-serif;font-size:18px;text-transform:uppercase;margin-bottom:14px">★ ${r.quick_wins.length} QUICK WINS</div>
        <ul style="list-style:none;font-size:13px;line-height:1.5">${r.quick_wins.map(q => `<li style="padding:6px 0;border-bottom:1px solid #fff8">→ ${esc(q)}</li>`).join('')}</ul>
      </div>` : ''}
    </div>
  </section>`;
}