(function () {
  const base = (document.querySelector('meta[name="base-url"]') || {}).content || '';

  // ── State ──────────────────────────────────────────────────────────────────
  const state = {
    projectId:   null,
    crawlRunId:  null,
    limit:       25,
    offset:      0,
    sortBy:      'url',
    sortDir:     'ASC',
    tab:         'all_urls',
    tabOffset:   0,
    noise:       false,   // false = noise hidden (default)
    techOffset:  0,
    techLimit:   25,
  };

  // Cached issue summary — recomputed per crawl run, not per filter change
  let issueSummaryCache    = null;
  let issueSummaryCacheRun = null;
  let deckBuilt  = false;
  let deckRunId  = null;
  let deckChart  = null;

  // ── Issue metadata ─────────────────────────────────────────────────────────
  const ISSUE_FIX = {
    'Missing title':          'Add a unique <title> tag (50–60 chars) to every HTML page.',
    'Missing meta description':'Write a unique meta description (120–158 chars) per page.',
    'Missing H1':             'Add exactly one H1 heading per page containing the primary keyword.',
    'Client/Server errors':   'Fix or remove links to 4xx/5xx pages. Redirect broken URLs with 301s.',
    'Noindex pages':          'Audit noindex directives — ensure important pages are not accidentally blocked.',
    'Thin content':           'Expand pages with <300 words — add value, FAQs, step-by-step guidance.',
    'Duplicate content hash': 'Consolidate duplicates with 301 redirects or <link rel="canonical"> tags.',
    'Low readability (Flesch <50)': 'Rewrite content at a 7th–10th grade level. Use shorter sentences (<18 words), everyday vocabulary, active voice, and concrete examples.',
    'Images missing alt text': 'Add descriptive alt text to every image. Alt text is used by screen readers and by Google Images for indexing. Keep it under 125 chars.',
  };

  const ISSUE_NAV = {
    'Missing title':          { section: 'onpage',    tab: 'meta_tags'  },
    'Missing meta description':{ section: 'onpage',   tab: 'meta_tags'  },
    'Missing H1':             { section: 'onpage',    tab: 'meta_tags'  },
    'Thin content':           { section: 'onpage',    tab: 'content'    },
    'Duplicate content hash': { section: 'onpage',    tab: 'content'    },
    'Client/Server errors':           { section: 'technical', tab: null         },
    'Noindex pages':                  { section: 'technical', tab: null         },
    'Slow page (>3s response)':       { section: 'allpages',  tab: null         },
    'Low readability (Flesch <50)':   { section: 'allpages',  tab: null         },
    'Images missing alt text':        { section: 'onpage',    tab: 'images'     },
  };

  // ── DOM helpers ───────────────────────────────────────────────────────────
  const qs = (id) => document.getElementById(id);

  function guardElement(id) {
    const el = qs(id);
    if (!el) throw new Error(`Expected element #${id} in audit UI`);
    return el;
  }

  function selectedProjectOption() {
    const select = qs('project-id');
    return select ? select.options[select.selectedIndex] : null;
  }

  function updateProjectSummaryMetadata() {
    const option      = selectedProjectOption();
    const projectText = option ? option.textContent.trim() : '—';
    const domainText  = option ? option.dataset.domain || '' : '';

    const spEl = qs('summary-project');
    if (spEl) spEl.textContent = projectText || '—';
    const sdEl = qs('summary-domain');
    if (sdEl) sdEl.textContent = domainText  || '—';
    const ssEl = qs('summary-status');
    if (ssEl) ssEl.textContent = state.crawlRunId ? `Run #${state.crawlRunId}` : 'Idle';
    const siteProject = qs('site-project-name');
    const siteDomain  = qs('site-domain');
    if (siteProject) siteProject.textContent = projectText || '—';
    if (siteDomain)  siteDomain.textContent  = domainText  || '—';
  }

  function updateDomainFromProject() {
    const option = selectedProjectOption();
    if (!option) return;
    const projectDomain = option.dataset.domain || '';
    if (projectDomain) qs('domain').value = projectDomain;
    updateProjectSummaryMetadata();
    updateStartButtonState();
  }

  function updateStartButtonState() {
    const startButton = guardElement('start-crawl-btn');
    const projectSelected = Boolean(Number(qs('project-id').value || 0));
    const domainValue = qs('domain').value.trim();
    startButton.disabled = !projectSelected || domainValue === '';
  }

  function setStatusText(message, isError = false) {
    const status = guardElement('crawl-status-text');
    status.textContent = message;
    status.className   = `small ${isError ? 'text-danger' : 'text-muted'}`;
  }

  function setButtonLoading(isLoading) {
    const button = guardElement('start-crawl-btn');
    button.disabled = isLoading
      || !Boolean(Number(qs('project-id').value || 0))
      || qs('domain').value.trim() === '';
    button.innerHTML = isLoading
      ? '<span class="spinner-border spinner-border-sm me-1" role="status" aria-hidden="true"></span>Starting...'
      : '<i class="bi bi-play-fill me-1"></i>Start Crawl';
  }

  // ── API fetch wrapper ─────────────────────────────────────────────────────
  async function api(path, options) {
    const url      = base + path;
    const response = await fetch(url, options || {});
    const ct       = response.headers.get('content-type') || '';
    if (ct.includes('application/json')) {
      const json = await response.json();
      if (!response.ok || !json.success) {
        throw new Error(`${json.message || `Request failed (${response.status})`} [${url}]`);
      }
      return json.data;
    }
    if (!response.ok) {
      const text = await response.text();
      throw new Error(`Request failed (${response.status}) ${response.statusText} [${url}]${text ? ': ' + text.slice(0, 200) : ''}`);
    }
    return response;
  }

  // ── Noise URL detection ───────────────────────────────────────────────────
  function isNoiseUrl(url) {
    if (!url) return false;
    const patterns = [
      /\/page\/\d+\//,
      /\/tag\//,
      /\/author\//,
      /\/category\//,
      /[?&]s=/,
      /\/feed\//,
      /\/amp(?:\/|$|\?)/,
      /\/\d{4}\/\d{2}\/\d{2}\//,
    ];
    return patterns.some((p) => p.test(url));
  }

  // ── Noise toggle UI ───────────────────────────────────────────────────────
  function updateNoiseToggle() {
    const btn = qs('noise-toggle');
    if (!btn) return;
    if (state.noise) {
      btn.textContent = '✓ Noise Pages';
      btn.classList.replace('btn-outline-secondary', 'btn-outline-primary');
    } else {
      btn.textContent = '⊘ Noise Pages';
      btn.classList.replace('btn-outline-primary', 'btn-outline-secondary');
    }
  }

  // ── Section / tab navigation ──────────────────────────────────────────────
  function navigateTo(section, tab) {
    const sBtn = document.querySelector(`#section-tabs button[data-section="${section}"]`);
    if (sBtn) sBtn.click();
    if (tab) {
      setTimeout(() => {
        const tBtn = document.querySelector(`#audit-tabs button[data-tab="${tab}"]`);
        if (tBtn) tBtn.click();
      }, 60);
    }
  }

  // ── Tooltip init ──────────────────────────────────────────────────────────
  function initTooltips() {
    document.querySelectorAll('[data-bs-toggle="tooltip"]').forEach((el) => {
      if (!bootstrap.Tooltip.getInstance(el)) new bootstrap.Tooltip(el);
    });
  }

  // ── Sort (named as per spec) ──────────────────────────────────────────────
  function sortAuditBy(column) {
    if (state.sortBy === column) {
      state.sortDir = state.sortDir === 'ASC' ? 'DESC' : 'ASC';
    } else {
      state.sortBy  = column;
      state.sortDir = 'ASC';
    }
    state.offset = 0;
    wrap(loadWebsiteAudit());
  }

  // ── Unified filter apply (named as per spec) ──────────────────────────────
  function applyAuditFilters() {
    state.offset = 0;
    wrap(loadWebsiteAudit());
  }

  // ── Query builder ─────────────────────────────────────────────────────────
  function buildAuditQuery() {
    const statusVal = (qs('filter-status') || {}).value || '';
    const typeVal   = (qs('filter-type')   || {}).value || '';

    const statusRanges = {
      ok:       [200, 299],
      redirect: [300, 399],
      client:   [400, 499],
      server:   [500, null],
    };
    const range = statusRanges[statusVal] || null;

    const p = new URLSearchParams({
      crawl_run_id: String(state.crawlRunId || ''),
      limit:        String(state.limit),
      offset:       String(state.offset),
      sort_by:      state.sortBy,
      sort_dir:     state.sortDir,
      search:       (qs('filter-search')      || {}).value || '',
      indexability: (qs('filter-indexability')|| {}).value || '',
      hide_noise:   state.noise ? '1' : '',
    });

    if (range) {
      p.set('status_code_min', String(range[0]));
      if (range[1] !== null) p.set('status_code_max', String(range[1]));
    }
    if (typeVal) p.set('type_filter', typeVal);

    return p.toString();
  }

  // ── Renderers ─────────────────────────────────────────────────────────────

  function renderMainTable(rows) {
    const table = qs('main-audit-table');
    if (!table) return;
    const tbody = table.querySelector('tbody');
    tbody.innerHTML = '';

    const visible = state.noise ? rows : rows.filter((r) => !isNoiseUrl(r.url));

    if (!visible.length) {
      tbody.innerHTML = '<tr><td colspan="7" class="text-muted text-center py-3">No rows found</td></tr>';
      return;
    }

    visible.forEach((row) => {
      const statusClass = row.status_code >= 400 ? 'text-danger' : row.status_code >= 300 ? 'text-warning' : '';
      const tr = document.createElement('tr');
      tr.innerHTML = `
        <td><a href="#" class="row-url small text-truncate d-inline-block" style="max-width:260px;" data-url="${escapeHtml(row.url)}" title="${escapeHtml(row.url)}">${escapeHtml(row.url || '')}</a></td>
        <td class="small ${statusClass} fw-semibold">${row.status_code ?? ''}</td>
        <td class="small text-truncate" style="max-width:160px;">${escapeHtml(row.title || '')}</td>
        <td class="small text-truncate text-muted" style="max-width:160px;">${escapeHtml(row.meta_description || '')}</td>
        <td class="small">${row.word_count ?? 0}</td>
        <td class="small">${row.depth ?? 0}</td>
        <td class="small ${Number(row.response_time || 0) > 3 ? 'text-danger fw-semibold' : 'text-muted'}">${Number(row.response_time || 0).toFixed(3)}s</td>
        <td><button class="btn btn-xs btn-outline-secondary serp-preview-btn py-0 px-1" style="font-size:10px;" data-url="${escapeHtml(row.url)}" data-title="${escapeHtml(row.title || '')}" data-meta="${escapeHtml(row.meta_description || '')}" title="SERP Preview">▤</button></td>
      `;
      tbody.appendChild(tr);
    });
  }

  // ── Tiered Action Queue (Item 3) ─────────────────────────────────────────
  //
  // Tiers: critical → Fix This Week, high → Fix This Sprint,
  //        medium → Next Sprint, low → Backlog
  //
  function renderIssueSummary(containerId, issues) {
    const container = qs(containerId);
    if (!container) return;

    const tiers = {
      critical: { label: 'Fix This Week',   color: 'danger',    icon: 'bi-exclamation-triangle-fill', items: [] },
      high:     { label: 'Fix This Sprint', color: 'warning',   icon: 'bi-exclamation-circle-fill',   items: [] },
      medium:   { label: 'Next Sprint',     color: 'info',      icon: 'bi-info-circle-fill',           items: [] },
      low:      { label: 'Backlog',         color: 'secondary', icon: 'bi-circle-fill',               items: [] },
    };

    issues.forEach((issue) => {
      const sev = (issue.severity || 'low').toLowerCase();
      if (tiers[sev]) tiers[sev].items.push(issue);
    });

    if (!issues.length) {
      container.innerHTML = '<p class="text-success mb-0"><i class="bi bi-check-circle me-1"></i>No issues found — this crawl is clean.</p>';
      return;
    }

    const html = Object.entries(tiers)
      .filter(([, t]) => t.items.length > 0)
      .map(([, tier]) => {
        const rows = tier.items.map((issue) => {
          const fix  = ISSUE_FIX[issue.label] || 'Review and fix the flagged pages.';
          const nav  = ISSUE_NAV[issue.label]  || null;
          const samples = (issue.sample_urls || []).slice(0, 2);
          return `<div class="d-flex align-items-start gap-2 py-2 border-bottom">
            <div class="flex-grow-1 min-w-0">
              <a href="#" class="issue-nav fw-medium text-decoration-none small"
                 data-nav-section="${nav ? nav.section : ''}"
                 data-nav-tab="${nav ? (nav.tab || '') : ''}">
                ${escapeHtml(issue.label || '')}
              </a>
              <span class="ms-1" data-bs-toggle="tooltip" data-bs-title="${escapeHtml(fix)}" style="cursor:help;">
                <i class="bi bi-question-circle text-muted" style="font-size:.7rem;"></i>
              </span>
              ${samples.length ? `<div class="text-muted mt-1" style="font-size:.7rem;">${
                samples.map((u) => `<span class="me-2 text-truncate d-inline-block" style="max-width:180px;vertical-align:middle;" title="${escapeHtml(u)}">${escapeHtml(u.replace(/^https?:\/\/[^/]+/, '') || u)}</span>`).join('')
              }</div>` : ''}
            </div>
            <span class="badge bg-${tier.color}-subtle text-${tier.color} flex-shrink-0 mt-1">${issue.count}</span>
          </div>`;
        }).join('');

        return `<div class="mb-3">
          <div class="d-flex align-items-center gap-2 mb-1 pt-1">
            <i class="bi ${tier.icon} text-${tier.color}" style="font-size:.85rem;"></i>
            <span class="fw-semibold small text-${tier.color}">${tier.label}</span>
            <span class="badge bg-${tier.color}-subtle text-${tier.color} ms-auto">${tier.items.length}</span>
          </div>
          ${rows}
        </div>`;
      }).join('');

    container.innerHTML = html;
    initTooltips();
  }

  // Technical Issues URL-level table
  function renderTechnicalDetail(rows, total) {
    const tbody   = qs('technical-detail-table').querySelector('tbody');
    const infoEl  = qs('tech-page-info');
    const totalEl = qs('tech-total');
    tbody.innerHTML = '';

    if (!rows.length) {
      tbody.innerHTML = '<tr><td colspan="6" class="text-center text-muted py-4">No technical issues found for this run.</td></tr>';
      if (infoEl)  infoEl.textContent  = '';
      if (totalEl) totalEl.textContent = '';
      return;
    }

    const issueColors = {
      'Server Error': 'danger',
      'Client Error': 'danger',
      'Redirect':     'warning',
      'Slow Page':    'warning',
      'Noindex':      'info',
      'Check':        'secondary',
    };

    rows.forEach((row) => {
      const clr = issueColors[row.issue_type] || 'secondary';
      const tr  = document.createElement('tr');
      tr.innerHTML = `
        <td><a href="#" class="row-url small text-truncate d-inline-block" style="max-width:260px;" data-url="${escapeHtml(row.url)}" title="${escapeHtml(row.url)}">${escapeHtml(row.url || '')}</a></td>
        <td><span class="badge bg-${clr}-subtle text-${clr} font-monospace">${row.status_code || '—'}</span></td>
        <td><span class="badge bg-${clr}-subtle text-${clr}">${escapeHtml(row.issue_type)}</span></td>
        <td class="small text-muted text-truncate" style="max-width:160px;" title="${escapeHtml(row.canonical_url || '')}">${escapeHtml(row.canonical_url || '—')}</td>
        <td><span class="badge ${row.indexability === 'Noindex' ? 'bg-danger-subtle text-danger' : 'bg-success-subtle text-success'}">${escapeHtml(row.indexability || '')}</span></td>
        <td class="small ${parseFloat(row.response_time) > 3 ? 'text-danger fw-semibold' : 'text-muted'}">${escapeHtml(row.response_time || '')}</td>
      `;
      tbody.appendChild(tr);
    });

    const page = Math.floor(state.techOffset / state.techLimit) + 1;
    if (infoEl)  infoEl.textContent  = `Showing ${state.techOffset + 1}–${state.techOffset + rows.length} of ${total}`;
    if (totalEl) totalEl.textContent = `${total} issues`;

    const techNext = qs('tech-next');
    const techPrev = qs('tech-prev');
    if (techNext) techNext.disabled = rows.length < state.techLimit;
    if (techPrev) techPrev.disabled = state.techOffset === 0;
  }

  // ── On-Page tab-specific renderers ────────────────────────────────────────

  function tabSortHeader(col, label, tab) {
    const cur = tabState.sortCol[tab];
    const dir = tabState.sortDir[tab] || 'asc';
    const arrow = cur === col ? (dir === 'asc' ? ' ↑' : ' ↓') : '';
    return `<th data-tab-sort="${escapeHtml(col)}" style="cursor:pointer;user-select:none;white-space:nowrap;">${escapeHtml(label)}${arrow}</th>`;
  }

  function bindTabSort(tab) {
    const thead = qs('tab-thead');
    if (!thead) return;
    thead.querySelectorAll('th[data-tab-sort]').forEach(th => {
      th.addEventListener('click', () => {
        const col = th.dataset.tabSort;
        if (tabState.sortCol[tab] === col) {
          tabState.sortDir[tab] = tabState.sortDir[tab] === 'asc' ? 'desc' : 'asc';
        } else {
          tabState.sortCol[tab] = col;
          tabState.sortDir[tab] = 'asc';
        }
        reRenderCurrentTab();
      });
    });
  }

  function bindTechTabSort(tab) {
    const thead = qs('tech-detail-thead');
    if (!thead) return;
    thead.querySelectorAll('th[data-tech-sort]').forEach(th => {
      th.addEventListener('click', () => {
        const col = th.dataset.techSort;
        if (techTabState.sortCol[tab] === col) {
          techTabState.sortDir[tab] = techTabState.sortDir[tab] === 'asc' ? 'desc' : 'asc';
        } else {
          techTabState.sortCol[tab] = col;
          techTabState.sortDir[tab] = 'asc';
        }
        reRenderTechTab();
      });
    });
  }

  function renderMetaTab(rows) {
    const thead = qs('tab-thead');
    const tbody = qs('tab-tbody');
    const t = state.tab;
    thead.innerHTML = '<tr>' + [['url','Address'],['title','Title'],['meta_description','Meta Description'],['h1','H1'],['_issues','Issues']].map(([c,l]) => tabSortHeader(c,l,t)).join('') + '</tr>';
    tbody.innerHTML = '';
    bindTabSort(t);

    if (!rows.length) {
      tbody.innerHTML = '<tr><td colspan="5" class="text-muted text-center py-3">No rows for this tab.</td></tr>';
      return;
    }

    rows.forEach((row) => {
      const issues = [];
      if (!row.title)            issues.push('<span class="badge bg-danger-subtle text-danger">No Title</span>');
      if (!row.meta_description) issues.push('<span class="badge bg-warning-subtle text-warning">No Meta</span>');
      if (!row.h1)               issues.push('<span class="badge bg-warning-subtle text-warning">No H1</span>');

      const tr = document.createElement('tr');
      tr.innerHTML = `
        <td class="text-nowrap" style="max-width:240px;overflow:hidden;text-overflow:ellipsis;"><a href="#" class="row-url small" data-url="${escapeHtml(row.url)}" title="${escapeHtml(row.url)}">${escapeHtml((row.url || '').replace(/^https?:\/\/[^/]+/, '') || '/')}</a></td>
        <td class="small text-nowrap" style="max-width:200px;overflow:hidden;text-overflow:ellipsis;" title="${escapeHtml(row.title || '')}">${escapeHtml((row.title || '').slice(0, 60))}${(row.title || '').length > 60 ? '…' : ''}</td>
        <td class="small text-nowrap text-muted" style="max-width:200px;overflow:hidden;text-overflow:ellipsis;" title="${escapeHtml(row.meta_description || '')}">${escapeHtml((row.meta_description || '').slice(0, 80))}${(row.meta_description || '').length > 80 ? '…' : ''}</td>
        <td class="small text-nowrap" style="max-width:150px;overflow:hidden;text-overflow:ellipsis;">${escapeHtml((row.h1 || '').slice(0, 45))}${(row.h1 || '').length > 45 ? '…' : ''}</td>
        <td class="text-nowrap">${issues.join(' ') || '<span class="badge bg-success-subtle text-success">OK</span>'}</td>
      `;
      tbody.appendChild(tr);
    });
  }

  function renderUrlIssuesTab(rows) {
    const thead = qs('tab-thead');
    const tbody = qs('tab-tbody');
    const t = state.tab;
    thead.innerHTML = '<tr>' + [['url','Address'],['_length','URL Length'],['_params','Params'],['_upper','Uppercase'],['_under','Underscore']].map(([c,l]) => tabSortHeader(c,l,t)).join('') + '</tr>';
    tbody.innerHTML = '';
    bindTabSort(t);

    const visible = state.noise ? rows : rows.filter((r) => !isNoiseUrl(r.url));

    if (!visible.length) {
      tbody.innerHTML = '<tr><td colspan="5" class="text-muted text-center py-3">No rows for this tab.</td></tr>';
      return;
    }

    visible.forEach((row) => {
      const url          = row.url || '';
      const pathOnly     = url.replace(/^https?:\/\/[^/]+/, '');
      const hasParams    = url.includes('?');
      const hasUpper     = /[A-Z]/.test(pathOnly);
      const hasUnderscore= pathOnly.includes('_');
      const longUrl      = url.length > 115;

      const tr = document.createElement('tr');
      tr.innerHTML = `
        <td class="text-nowrap" style="max-width:280px;overflow:hidden;text-overflow:ellipsis;"><a href="#" class="row-url small" data-url="${escapeHtml(url)}" title="${escapeHtml(url)}">${escapeHtml(pathOnly || '/')}</a></td>
        <td class="small text-nowrap ${longUrl ? 'text-danger fw-semibold' : 'text-muted'}" title="${longUrl ? 'URL too long (>115 chars)' : ''}">${url.length}</td>
        <td class="text-nowrap">${hasParams    ? badge('warning','Yes') : badge('success','No')}</td>
        <td class="text-nowrap">${hasUpper     ? badge('warning','Yes') : badge('success','No')}</td>
        <td class="text-nowrap">${hasUnderscore? badge('warning','Yes') : badge('success','No')}</td>
      `;
      tbody.appendChild(tr);
    });
  }

  function renderContentTab(rows) {
    const thead = qs('tab-thead');
    const tbody = qs('tab-tbody');
    const t = state.tab;
    thead.innerHTML = '<tr>' + [['url','Address'],['word_count','Words'],['flesch_reading_ease','Flesch'],['near_duplicate_hash','Duplicate'],['_thin','Thin']].map(([c,l]) => tabSortHeader(c,l,t)).join('') + '</tr>';
    tbody.innerHTML = '';
    bindTabSort(t);

    if (!rows.length) {
      tbody.innerHTML = '<tr><td colspan="5" class="text-muted text-center py-3">No content rows found.</td></tr>';
      return;
    }

    rows.forEach((row) => {
      const wc   = parseInt(row.word_count || 0, 10);
      const thin = wc > 0 && wc < 300;
      const dup  = Boolean(row.near_duplicate_hash);

      const tr = document.createElement('tr');
      tr.innerHTML = `
        <td class="text-nowrap" style="max-width:240px;overflow:hidden;text-overflow:ellipsis;"><a href="#" class="row-url small" data-url="${escapeHtml(row.url)}" title="${escapeHtml(row.url)}">${escapeHtml((row.url || '').replace(/^https?:\/\/[^/]+/, '') || '/')}</a></td>
        <td class="small text-nowrap ${thin ? 'text-danger fw-semibold' : ''}">${wc}</td>
        <td class="small text-nowrap text-muted">—</td>
        <td class="text-nowrap">${dup  ? badge('warning','Yes') : badge('success','No')}</td>
        <td class="text-nowrap">${thin ? badge('danger','Thin') : badge('success','OK')}</td>
      `;
      tbody.appendChild(tr);
    });
  }

  function renderImagesTab(rows) {
    const thead = qs('tab-thead');
    const tbody = qs('tab-tbody');
    const t = state.tab;
    thead.innerHTML = '<tr>' + [['page_url','Page URL'],['image_url','Image URL'],['alt_text','Alt Text'],['is_missing_alt','Missing Alt']].map(([c,l]) => tabSortHeader(c,l,t)).join('') + '</tr>';
    tbody.innerHTML = '';
    bindTabSort(t);

    if (!rows.length) {
      tbody.innerHTML = '<tr><td colspan="4" class="text-muted text-center py-3">No images found for this run.</td></tr>';
      return;
    }

    rows.forEach((row) => {
      const tr = document.createElement('tr');
      tr.innerHTML = `
        <td class="small text-nowrap" style="max-width:200px;overflow:hidden;text-overflow:ellipsis;" title="${escapeHtml(row.page_url || '')}">${escapeHtml((row.page_url || '').replace(/^https?:\/\/[^/]+/, '') || '/')}</td>
        <td class="small text-nowrap text-muted" style="max-width:220px;overflow:hidden;text-overflow:ellipsis;" title="${escapeHtml(row.image_url || '')}">${escapeHtml(row.image_url || '')}</td>
        <td class="small text-nowrap" style="max-width:160px;overflow:hidden;text-overflow:ellipsis;">${escapeHtml(row.alt_text || '')}<em class="text-muted">${row.alt_text ? '' : 'none'}</em></td>
        <td class="text-nowrap">${row.is_missing_alt ? badge('danger','Missing') : badge('success','OK')}</td>
      `;
      tbody.appendChild(tr);
    });
  }

  // ── Tab helper: status badge ────────────────────────────────────────────
  function tabStatusBadge(code) {
    const c = parseInt(code || 0);
    const cl = c >= 500 ? 'danger' : c >= 400 ? 'warning' : c >= 300 ? 'info' : c === 200 ? 'success' : 'secondary';
    return `<span class="badge bg-${cl}-subtle text-${cl}">${c || '—'}</span>`;
  }

  // ── All URLs tab ─────────────────────────────────────────────────────────
  function renderAllUrlsTab(rows) {
    const thead = qs('tab-thead');
    const tbody = qs('tab-tbody');
    if (!thead || !tbody) return;
    thead.innerHTML = `<tr>
      <th style="min-width:280px;">URL</th>
      <th style="min-width:80px;">Status</th>
      <th style="min-width:60px;">Depth</th>
      <th style="min-width:180px;">Title</th>
      <th style="min-width:150px;">H1</th>
      <th style="min-width:70px;">Words</th>
      <th style="min-width:90px;">Resp. Time</th>
    </tr>`;
    tbody.innerHTML = '';
    if (!rows.length) {
      tbody.innerHTML = '<tr><td colspan="7" class="text-center text-muted py-3">No pages found.</td></tr>';
      return;
    }
    rows.forEach((row) => {
      const tr = document.createElement('tr');
      const path = (row.url || '').replace(/^https?:\/\/[^/]+/, '') || '/';
      tr.innerHTML = `
        <td class="text-nowrap" style="max-width:280px;overflow:hidden;text-overflow:ellipsis;">
          <a href="${escapeHtml(row.url)}" target="_blank" class="small" title="${escapeHtml(row.url)}">${escapeHtml(path)}</a>
        </td>
        <td class="text-nowrap">${tabStatusBadge(row.status_code)}</td>
        <td class="text-nowrap text-center small">${row.depth ?? 0}</td>
        <td class="text-nowrap small" style="max-width:200px;overflow:hidden;text-overflow:ellipsis;" title="${escapeHtml(row.title || '')}">${escapeHtml((row.title || '').slice(0, 55))}${(row.title || '').length > 55 ? '…' : ''}</td>
        <td class="text-nowrap small" style="max-width:160px;overflow:hidden;text-overflow:ellipsis;" title="${escapeHtml(row.h1 || '')}">${escapeHtml((row.h1 || '').slice(0, 45))}${(row.h1 || '').length > 45 ? '…' : ''}</td>
        <td class="text-nowrap text-end small">${row.word_count ?? 0}</td>
        <td class="text-nowrap text-end small">${Number(row.response_time || 0).toFixed(2)}s</td>
      `;
      tbody.appendChild(tr);
    });
  }

  // ── Schema tab ──────────────────────────────────────────────────────────
  function parseSchemaTypes(jsonStr) {
    try {
      const arr = typeof jsonStr === 'string' ? JSON.parse(jsonStr) : (jsonStr || []);
      if (!Array.isArray(arr) || !arr.length) return { count: 0, types: [] };
      const types = [];
      arr.forEach((s) => {
        try {
          const obj = typeof s === 'string' ? JSON.parse(s) : s;
          if (!obj) return;
          const t = obj['@type'];
          if (t) types.push(Array.isArray(t) ? t.join('+') : String(t));
        } catch (_) {}
      });
      return { count: arr.length, types: [...new Set(types)] };
    } catch (_) {
      return { count: 0, types: [] };
    }
  }

  function renderSchemaTab(rows) {
    const thead = qs('tab-thead');
    const tbody = qs('tab-tbody');
    if (!thead || !tbody) return;
    thead.innerHTML = `<tr>
      <th style="min-width:280px;">URL</th>
      <th style="min-width:80px;">Status</th>
      <th style="min-width:80px;">Has Schema</th>
      <th style="min-width:200px;">Schema Types</th>
      <th style="min-width:60px;">Count</th>
      <th style="min-width:60px;">Depth</th>
    </tr>`;
    tbody.innerHTML = '';
    if (!rows.length) {
      tbody.innerHTML = '<tr><td colspan="6" class="text-center text-muted py-3">No pages found.</td></tr>';
      return;
    }
    rows.forEach((row) => {
      const schema = parseSchemaTypes(row.json_ld_schema);
      const hasBadge = schema.count > 0
        ? '<span class="badge bg-success-subtle text-success">Yes</span>'
        : '<span class="badge bg-secondary-subtle text-secondary">No</span>';
      const path = (row.url || '').replace(/^https?:\/\/[^/]+/, '') || '/';
      const typesBadges = schema.types.map((t) => `<span class="badge bg-info-subtle text-info me-1">${escapeHtml(t)}</span>`).join('');
      const tr = document.createElement('tr');
      tr.innerHTML = `
        <td class="text-nowrap" style="max-width:280px;overflow:hidden;text-overflow:ellipsis;">
          <a href="${escapeHtml(row.url)}" target="_blank" class="small" title="${escapeHtml(row.url)}">${escapeHtml(path)}</a>
        </td>
        <td class="text-nowrap">${tabStatusBadge(row.status_code)}</td>
        <td class="text-nowrap">${hasBadge}</td>
        <td class="text-nowrap" style="max-width:220px;overflow:hidden;text-overflow:ellipsis;">${typesBadges || '<span class="text-muted small">—</span>'}</td>
        <td class="text-nowrap text-center small">${schema.count}</td>
        <td class="text-nowrap text-center small">${row.depth ?? 0}</td>
      `;
      tbody.appendChild(tr);
    });
  }

  // ── Links tab (inlinks / outlinks / external_links) ────────────────────
  function renderLinksTab(rows) {
    const thead = qs('tab-thead');
    const tbody = qs('tab-tbody');
    if (!thead || !tbody) return;
    thead.innerHTML = `<tr>
      <th style="min-width:240px;">Source URL</th>
      <th style="min-width:240px;">Destination URL</th>
      <th style="min-width:140px;">Anchor Text</th>
      <th style="min-width:100px;">Location</th>
      <th style="min-width:75px;">Nofollow</th>
    </tr>`;
    tbody.innerHTML = '';
    if (!rows.length) {
      tbody.innerHTML = '<tr><td colspan="5" class="text-center text-muted py-3">No links found.</td></tr>';
      return;
    }
    const locBadge = (loc) => {
      const map = { Header: 'primary', Footer: 'secondary', Navigation: 'info', Sidebar: 'warning', Button: 'success', Content: 'light', Body: 'light' };
      const c = map[loc] || 'light';
      const tc = ['Content','Body'].includes(loc) ? 'muted' : 'white';
      return `<span class="badge" style="background:var(--bs-${c}-bg-subtle,#e2e8f0);color:var(--bs-${c}-text-emphasis,#64748b);">${escapeHtml(loc || 'Body')}</span>`;
    };
    rows.forEach((row) => {
      const src = row.source_url || '';
      const dst = row.destination_url || '';
      const srcPath = src.replace(/^https?:\/\/[^/]+/, '') || '/';
      const dstPath = dst.replace(/^https?:\/\/[^/]+/, '') || dst;
      const tr = document.createElement('tr');
      tr.innerHTML = `
        <td class="text-nowrap" style="max-width:240px;overflow:hidden;text-overflow:ellipsis;">
          <a href="${escapeHtml(src)}" target="_blank" class="small" title="${escapeHtml(src)}">${escapeHtml(srcPath)}</a>
        </td>
        <td class="text-nowrap" style="max-width:240px;overflow:hidden;text-overflow:ellipsis;">
          <a href="${escapeHtml(dst)}" target="_blank" class="small" title="${escapeHtml(dst)}">${escapeHtml(dstPath)}</a>
        </td>
        <td class="text-nowrap small" style="max-width:150px;overflow:hidden;text-overflow:ellipsis;" title="${escapeHtml(row.anchor_text || '')}">${escapeHtml((row.anchor_text || '').slice(0, 40))}${(row.anchor_text || '').length > 40 ? '…' : ''}</td>
        <td class="text-nowrap">${locBadge(row.location)}</td>
        <td class="text-nowrap text-center small">${row.is_nofollow ? '<span class="badge bg-warning-subtle text-warning">NF</span>' : '—'}</td>
      `;
      tbody.appendChild(tr);
    });
  }

  function renderGenericTab(rows) {
    const thead = qs('tab-thead');
    const tbody = qs('tab-tbody');
    if (!thead || !tbody) return;
    thead.innerHTML = '';
    tbody.innerHTML = '';

    if (!rows.length) {
      tbody.innerHTML = '<tr><td class="text-muted text-center py-3">No rows for this tab.</td></tr>';
      return;
    }

    const t    = state.tab;
    const keys = Object.keys(rows[0]);
    thead.innerHTML = '<tr>' + keys.map(k => `<th class="text-nowrap">${escapeHtml(k)}</th>`).join('') + '</tr>';
    rows.forEach((row) => {
      const tr = document.createElement('tr');
      tr.innerHTML = keys.map((k) => `<td class="small text-nowrap" style="max-width:220px;overflow:hidden;text-overflow:ellipsis;">${escapeHtml(typeof row[k] === 'object' ? JSON.stringify(row[k]) : String(row[k] ?? ''))}</td>`).join('');
      tbody.appendChild(tr);
    });
  }

  function renderTabTable(rows, tab) {
    const t = tab || state.tab;
    const noiseFiltered = state.noise ? rows : rows.filter((r) => !isNoiseUrl(r.url || r.page_url || r.source_url || ''));

    switch (t) {
      case 'all_urls':       renderAllUrlsTab(noiseFiltered);             break;
      case 'meta_tags':      renderMetaTab(noiseFiltered);                break;
      case 'url_issues':     renderUrlIssuesTab(noiseFiltered);           break;
      case 'content':        renderContentTab(noiseFiltered);             break;
      case 'images':         renderImagesTab(rows);                       break;
      case 'schema_markup':  renderSchemaTab(noiseFiltered);              break;
      case 'inlinks':
      case 'outlinks':
      case 'external_links': renderLinksTab(rows);                        break;
      default:               renderGenericTab(noiseFiltered);             break;
    }
  }

  function renderSummary(summary) {
    renderList('status-dist',  summary.status_distribution,       (x) => `${x.status_code}: ${x.cnt}`);
    renderList('depth-dist',   summary.depth_distribution,        (x) => `Depth ${x.depth}: ${x.cnt}`);
    renderList('ctype-dist',   summary.content_type_distribution, (x) => `${x.content_type || 'unknown'}: ${x.cnt}`);
    renderList('orphan-sample',summary.orphan_sample,             (x) => x.url);
  }

  function renderList(id, items, mapper) {
    const ul = qs(id);
    if (!ul) return;
    ul.innerHTML = '';
    (items || []).forEach((item) => {
      const li = document.createElement('li');
      li.textContent = mapper(item);
      ul.appendChild(li);
    });
  }

  // ── Badge helper ──────────────────────────────────────────────────────────
  function badge(color, text) {
    return `<span class="badge bg-${color}-subtle text-${color}">${escapeHtml(text)}</span>`;
  }

  // ── Data loaders ──────────────────────────────────────────────────────────

  async function startCrawl() {
    state.projectId = Number(qs('project-id').value || 0);
    if (!state.projectId) {
      setStatusText('Please select a project before starting the crawl.', true);
      return;
    }

    const payload = {
      domain:      qs('domain').value.trim(),
      max_depth:   Number(qs('max-depth').value || 2),
      max_pages:   Number(qs('max-pages').value || 200),
      concurrency: Number(qs('concurrency').value || 5),
      user_agent:  qs('user-agent').value || '',
    };

    if (!payload.domain) {
      setStatusText('Enter a valid domain before starting the crawl.', true);
      return;
    }

    const requestPath = `/api/projects/${state.projectId}/crawl`;
    setButtonLoading(true);
    setStatusText(`Starting crawl to ${base}${requestPath}`);

    try {
      const data = await api(requestPath, {
        method:  'POST',
        headers: { 'Content-Type': 'application/json' },
        body:    JSON.stringify(payload),
      });

      state.crawlRunId       = data.crawl_run_id;
      state.offset           = 0;
      state.tabOffset        = 0;
      state.techOffset       = 0;
      issueSummaryCache      = null;
      issueSummaryCacheRun   = null;

      setStatusText(`Crawl started — Run #${state.crawlRunId}. Waiting for worker to process pages.`);
      updateProjectSummaryMetadata();
      await loadAll();
    } catch (err) {
      setStatusText(err.message || 'Failed to start crawl', true);
      throw err;
    } finally {
      setButtonLoading(false);
      updateStartButtonState();
    }
  }

  async function pollLatest() {
    if (!state.projectId) return;
    try {
      const data = await api(`/api/projects/${state.projectId}/crawl-status/latest`);
      state.crawlRunId = data.crawl_run_id;
      qs('crawl-status-text').textContent = `Run #${data.crawl_run_id} — ${data.status} — pages: ${data.total_pages} — pending: ${data.pending_queue} — failed: ${data.failed_queue ?? 0}`;
      qs('summary-pages').textContent   = String(data.total_pages ?? 0);
      qs('summary-pending').textContent = String(data.pending_queue ?? 0);
      qs('summary-last').textContent    = data.finished_at || data.started_at || 'N/A';
      qs('summary-status').textContent  = data.status || 'Idle';
    } catch (error) {
      qs('crawl-status-text').textContent = error.message;
    }
  }

  async function loadWebsiteAudit() {
    if (!state.projectId || !state.crawlRunId) return;
    const data = await api(`/api/projects/${state.projectId}/website-audit?${buildAuditQuery()}`);

    renderMainTable(data.crawl_rows || []);

    // Cache issue summary per run — avoids re-rendering stale data on filter change
    if (data.issue_summary && state.crawlRunId !== issueSummaryCacheRun) {
      issueSummaryCache    = data.issue_summary;
      issueSummaryCacheRun = state.crawlRunId;
    }
    renderIssueSummary('action-queue-container', issueSummaryCache || data.issue_summary || []);

    const mainTotal = qs('main-total');
    if (mainTotal) mainTotal.textContent = `${data.crawl_total || 0} pages — offset ${state.offset}`;
    const summaryIssues = qs('summary-issues');
    if (summaryIssues) summaryIssues.textContent = String((issueSummaryCache || data.issue_summary || []).length);
    updateProjectSummaryMetadata();
  }

  async function loadTechnicalDetail() {
    if (!state.projectId || !state.crawlRunId) return;
    const data = await api(
      `/api/projects/${state.projectId}/technical-issues-detail?crawl_run_id=${state.crawlRunId}&limit=${state.techLimit}&offset=${state.techOffset}`
    );
    renderTechnicalDetail(data.rows || [], data.total || 0);
  }

  async function loadSummary() {
    if (!state.projectId || !state.crawlRunId) return;
    const data = await api(`/api/projects/${state.projectId}/website-level-summary?crawl_run_id=${state.crawlRunId}`);
    renderSummary(data || {});
    updateProjectSummaryMetadata();
  }

  async function loadTab() {
    if (!state.crawlRunId) return;
    const data = await api(
      `/api/projects/${state.projectId}/crawl/${state.crawlRunId}/tabs?tab=${encodeURIComponent(state.tab)}&limit=25&offset=${state.tabOffset}`
    );
    // Cache raw rows for client-side re-sort / re-filter
    tabState.rawRows[state.tab] = data.rows || [];
    reRenderCurrentTab();
    renderTabQuickFilters(state.tab);

    const tabNext = qs('tab-next');
    const tabPrev = qs('tab-prev');
    if (tabNext) tabNext.disabled = (data.rows || []).length < 25;
    if (tabPrev) tabPrev.disabled = state.tabOffset === 0;
  }

  // ── Auto-diff: compare latest 2 completed runs (Item 2) ──────────────────
  async function loadAutoDiff() {
    const diffEl = qs('diff-banner');
    if (!diffEl || !state.projectId) return;
    diffEl.innerHTML = '';

    try {
      const data = await api(`/api/projects/${state.projectId}/auto-diff`);
      if (!data.available) return;

      const scoreDelta  = (data.health_b || 0) - (data.health_a || 0);
      const deltaStr    = scoreDelta >= 0 ? `+${scoreDelta}` : String(scoreDelta);
      const deltaColor  = scoreDelta >= 0 ? 'success' : 'danger';
      const arrow       = scoreDelta >= 0 ? '↑' : '↓';
      const newCount    = data.new_issues?.length   || 0;
      const fixedCount  = data.fixed_issues?.length || 0;
      const newPages    = data.new_pages?.length    || 0;
      const removedPgs  = data.removed_pages?.length|| 0;
      const statusChg   = data.status_changed?.length|| 0;

      const chips = [
        `<span class="badge bg-${deltaColor}-subtle text-${deltaColor} fs-6 me-1">${arrow} ${Math.abs(scoreDelta)} pts health ${scoreDelta >= 0 ? 'improved' : 'declined'}</span>`,
        newCount   > 0 ? `<span class="badge bg-danger-subtle text-danger me-1">${newCount} issue${newCount>1?'s':''} worsened</span>` : '',
        fixedCount > 0 ? `<span class="badge bg-success-subtle text-success me-1">${fixedCount} issue${fixedCount>1?'s':''} improved</span>` : '',
        newPages   > 0 ? `<span class="badge bg-secondary-subtle text-secondary me-1">+${newPages} new pages</span>` : '',
        removedPgs > 0 ? `<span class="badge bg-secondary-subtle text-secondary me-1">-${removedPgs} pages removed</span>` : '',
        statusChg  > 0 ? `<span class="badge bg-warning-subtle text-warning me-1">${statusChg} status changes</span>` : '',
      ].filter(Boolean).join('');

      // Build detail rows for expandable section
      let detailHtml = '';
      if (newCount > 0) {
        detailHtml += `<div class="mb-2"><div class="small fw-semibold text-danger mb-1">Worsened:</div>
          ${data.new_issues.map((i) => `<span class="badge bg-danger-subtle text-danger me-1 mb-1">${escapeHtml(i.label)} (${i.change})</span>`).join('')}</div>`;
      }
      if (fixedCount > 0) {
        detailHtml += `<div class="mb-2"><div class="small fw-semibold text-success mb-1">Improved:</div>
          ${data.fixed_issues.map((i) => `<span class="badge bg-success-subtle text-success me-1 mb-1">${escapeHtml(i.label)} (${i.change})</span>`).join('')}</div>`;
      }
      if (statusChg > 0) {
        detailHtml += `<div class="mb-2"><div class="small fw-semibold text-warning mb-1">Status Changes (sample):</div>
          ${data.status_changed.slice(0, 5).map((s) => `<div class="small text-truncate" style="max-width:420px;"><a href="#" class="row-url text-decoration-none" data-url="${escapeHtml(s.url)}">${escapeHtml(s.url.replace(/^https?:\/\/[^/]+/, ''))}</a> <span class="text-muted">${s.from} → ${s.to}</span></div>`).join('')}</div>`;
      }

      diffEl.innerHTML = `
        <div class="alert alert-light border py-2 mb-0 d-flex align-items-center gap-2 flex-wrap">
          <i class="bi bi-git text-muted flex-shrink-0"></i>
          <span class="text-muted small flex-shrink-0">vs Run #${data.run_old}:</span>
          <div class="d-flex flex-wrap gap-1">${chips}</div>
          ${detailHtml ? `<button class="btn btn-sm btn-outline-secondary py-0 ms-auto flex-shrink-0" id="diff-detail-toggle" style="font-size:.75rem;">Details ▾</button>` : ''}
        </div>
        ${detailHtml ? `<div id="diff-details" class="d-none card-body border border-top-0 rounded-bottom py-2 px-3 bg-light small">${detailHtml}</div>` : ''}
      `;

      qs('diff-detail-toggle')?.addEventListener('click', (e) => {
        const details = qs('diff-details');
        if (details) {
          details.classList.toggle('d-none');
          e.target.textContent = details.classList.contains('d-none') ? 'Details ▾' : 'Details ▴';
        }
      });

    } catch (_) {
      // Non-critical — silently ignore if endpoint fails
    }
  }

  async function loadAll() {
    deckBuilt = false; // mark deck stale on new run
    await pollLatest();
    await loadWebsiteAudit();    // action queue + health score inputs
    await loadSummary();
    await loadTab();
    wrap(loadCategories());
    wrap(loadNewSections());
    wrap(loadAutoDiff());
    wrap(loadPerformance());
    wrap(buildAuditDeck());      // auto-build summary deck
    // Full 46-col table loads lazily when user clicks "All Pages" tab
  }

  async function loadPreviousRuns() {
    if (!state.projectId) return;
    const rows   = await api(`/api/projects/${state.projectId}/crawl-runs`);
    const select = qs('previous-run-id');
    if (!select) return;
    select.innerHTML = '<option value="">Select run</option>';
    rows.forEach((row) => {
      const option       = document.createElement('option');
      option.value       = String(row.id);
      option.textContent = `#${row.id} | ${row.status} | ${row.domain} | pages:${row.total_pages}`;
      select.appendChild(option);
    });

    // Auto-load the most recent completed run
    const latestCompleted = rows.find(r => r.status === 'completed');
    if (latestCompleted && !state.crawlRunId) {
      select.value     = String(latestCompleted.id);
      state.crawlRunId = latestCompleted.id;
      state.offset     = 0;
      state.tabOffset  = 0;
      state.techOffset = 0;
      issueSummaryCache    = null;
      issueSummaryCacheRun = null;
      setStatusText(`Loaded run #${latestCompleted.id}`);
      wrap(loadAll());
    }
  }

  async function loadPageDetail(url) {
    const data = await api(
      `/api/projects/${state.projectId}/page-detail?crawl_run_id=${state.crawlRunId}&url=${encodeURIComponent(url)}`
    );
    const page = data.page || {};
    qs('page-detail-body').innerHTML = `
      <div class="mb-2"><strong>URL:</strong> ${escapeHtml(page.url || '')}</div>
      <div class="mb-2"><strong>Status:</strong> ${page.status_code || ''}</div>
      <div class="mb-2"><strong>Title:</strong> ${escapeHtml(page.title || '')}</div>
      <div class="mb-2"><strong>Meta Description:</strong> ${escapeHtml(page.meta_description || '')}</div>
      <div class="mb-2"><strong>H1:</strong> ${escapeHtml(page.h1 || '')}</div>
      <div class="mb-2"><strong>Word Count:</strong> ${page.word_count || 0}</div>
      <div class="mb-2"><strong>Canonical:</strong> ${escapeHtml(page.canonical_url || '—')}</div>
      <div class="mb-2"><strong>Robots:</strong> ${escapeHtml(page.meta_robots || '—')}</div>
      <div class="mb-2"><strong>Response Time:</strong> ${Number(page.response_time || 0).toFixed(3)}s</div>
      <hr>
      <div class="row text-center">
        <div class="col"><strong>${(data.inlinks  || []).length}</strong><div class="text-muted small">Inlinks</div></div>
        <div class="col"><strong>${(data.outlinks || []).length}</strong><div class="text-muted small">Outlinks</div></div>
        <div class="col"><strong>${(data.images   || []).length}</strong><div class="text-muted small">Images</div></div>
      </div>
    `;
    new bootstrap.Modal(qs('pageDetailModal')).show();
  }

  // ── Section tabs ──────────────────────────────────────────────────────────
  function bindSectionTabs() {
    const sectionTabs = qs('section-tabs');
    if (!sectionTabs) return;

    const sections = ['summary', 'performance', 'technical', 'onpage', 'allpages', 'content-analysis', 'links', 'schema', 'cwv', 'comparison', 'sitelevel'];
    sectionTabs.querySelectorAll('button[data-section]').forEach((btn) => {
      btn.addEventListener('click', () => {
        sectionTabs.querySelectorAll('button').forEach((b) => b.classList.remove('active'));
        btn.classList.add('active');
        const selected = btn.getAttribute('data-section');
        sections.forEach((name) => {
          const panel = qs(`section-${name}`);
          if (!panel) return;
          panel.classList.toggle('d-none', name !== selected);
        });
        // Load technical detail on first entry into that section
        if (selected === 'technical' && state.crawlRunId) {
          wrap(loadTechnicalDetail());
        }
      });
    });
  }

  // ── Event bindings ────────────────────────────────────────────────────────
  function bindEvents() {
    guardElement('start-crawl-btn').addEventListener('click', () => wrap(startCrawl()));
    guardElement('refresh-btn').addEventListener('click',    () => wrap(loadAll()));

    guardElement('domain').addEventListener('input', () => {
      updateStartButtonState();
      setStatusText('Domain edited; ready to crawl.');
    });

    // Legacy filter inputs removed from HTML — noise toggle is the only one kept here


    // Noise toggle
    const noiseBtn = qs('noise-toggle');
    if (noiseBtn) {
      noiseBtn.addEventListener('click', () => {
        state.noise   = !state.noise;
        state.offset  = 0;
        updateNoiseToggle();
        wrap(loadWebsiteAudit());
        // Re-render current tab with noise filter applied client-side
        if (state.crawlRunId) wrap(loadTab());
      });
    }

    // Main table pagination (elements removed when 46-col table replaced this section)
    const mainPrev = qs('main-prev');
    const mainNext = qs('main-next');
    if (mainPrev) mainPrev.addEventListener('click', () => { state.offset = Math.max(0, state.offset - state.limit); wrap(loadWebsiteAudit()); });
    if (mainNext) mainNext.addEventListener('click', () => { state.offset += state.limit; wrap(loadWebsiteAudit()); });

    // Technical detail pagination
    const techPrev = qs('tech-prev');
    const techNext = qs('tech-next');
    if (techPrev) techPrev.addEventListener('click', () => {
      state.techOffset = Math.max(0, state.techOffset - state.techLimit);
      wrap(loadTechnicalDetail());
    });
    if (techNext) techNext.addEventListener('click', () => {
      state.techOffset += state.techLimit;
      wrap(loadTechnicalDetail());
    });

    // Tab pagination
    const tabPrev = qs('tab-prev');
    const tabNext = qs('tab-next');
    if (tabPrev) tabPrev.addEventListener('click', () => {
      state.tabOffset = Math.max(0, state.tabOffset - 25);
      wrap(loadTab());
    });
    if (tabNext) tabNext.addEventListener('click', () => {
      state.tabOffset += 25;
      wrap(loadTab());
    });

    // Project selector
    const projectSelect = guardElement('project-id');
    state.projectId = Number(projectSelect.value || 0);
    updateDomainFromProject();
    wrap(loadPreviousRuns());
    projectSelect.addEventListener('change', () => {
      state.projectId      = Number(projectSelect.value || 0);
      issueSummaryCache    = null;
      issueSummaryCacheRun = null;
      auditState.rawRows   = [];
      perfDataCache        = {};   // clear performance cache on project switch
      auditState.categories = [];
      updateDomainFromProject();
      updateStartButtonState();
      wrap(loadPreviousRuns());
      if (state.projectId) {
        wrap(pollLatest());
        wrap(loadCategories());
        // Load performance if that's the current section
        const activeSection = document.querySelector('#section-tabs button.active')?.getAttribute('data-section');
        if (activeSection === 'performance') {
          wrap(loadPerformance());
        }
      }
    });

    // Load previous run
    const loadRunBtn = qs('load-run-btn');
    if (loadRunBtn) {
      loadRunBtn.addEventListener('click', () => {
        const previousRun = qs('previous-run-id');
        if (!previousRun || !previousRun.value) {
          setStatusText('Select a previous run first.', true);
          return;
        }
        state.crawlRunId     = Number(previousRun.value);
        state.offset         = 0;
        state.tabOffset      = 0;
        state.techOffset     = 0;
        issueSummaryCache    = null;
        issueSummaryCacheRun = null;
        setStatusText(`Loaded previous run #${state.crawlRunId}`);
        wrap(loadAll());
      });
    }

    // Column sort (main table)
    document.querySelectorAll('#main-audit-table th[data-sort]').forEach((th) => {
      th.style.cursor = 'pointer';
      th.addEventListener('click', () => sortAuditBy(th.getAttribute('data-sort')));
    });

    // On-Page sub-tabs
    const auditTabs = guardElement('audit-tabs');
    auditTabs.querySelectorAll('button[data-tab]').forEach((btn) => {
      btn.addEventListener('click', () => {
        auditTabs.querySelectorAll('button').forEach((b) => b.classList.remove('active'));
        btn.classList.add('active');
        state.tab       = btn.getAttribute('data-tab');
        state.tabOffset = 0;
        wrap(loadTab());
      });
    });

    // Issue name → navigate to relevant section/tab (delegated)
    document.addEventListener('click', (e) => {
      const issueLink = e.target.closest('.issue-nav');
      if (!issueLink) return;
      e.preventDefault();
      const section = issueLink.dataset.navSection;
      const tab     = issueLink.dataset.navTab;
      if (section) navigateTo(section, tab || null);
    });

    // URL row click → page detail
    document.addEventListener('click', (e) => {
      const anchor = e.target.closest('a.row-url');
      if (!anchor) return;
      e.preventDefault();
      if (state.crawlRunId) wrap(loadPageDetail(anchor.getAttribute('data-url')));
    });

    // Exports
    qs('export-csv').addEventListener('click',  () => {
      if (!state.crawlRunId) return;
      window.location.href = `${base}/api/projects/${state.projectId}/crawl/${state.crawlRunId}/export?tab=${encodeURIComponent(state.tab)}&fmt=csv`;
    });
    qs('export-json').addEventListener('click', () => {
      if (!state.crawlRunId) return;
      window.location.href = `${base}/api/projects/${state.projectId}/crawl/${state.crawlRunId}/export?tab=${encodeURIComponent(state.tab)}&fmt=json`;
    });

    // Error guards
    window.addEventListener('error',             (e) => { setStatusText(`JS error: ${e.message}`, true); console.error(e.error || e.message); });
    window.addEventListener('unhandledrejection',(e) => { setStatusText('Unhandled promise rejection', true); console.error(e.reason); });

    updateStartButtonState();
    updateNoiseToggle();
    bindSectionTabs();
  }

  // ── Utilities ─────────────────────────────────────────────────────────────
  function wrap(promise) {
    Promise.resolve(promise).catch((err) => {
      const el = qs('crawl-status-text');
      if (el) el.textContent = err.message;
    });
  }

  function escapeHtml(str) {
    if (typeof str !== 'string') str = String(str ?? '');
    return str
      .replaceAll('&', '&amp;')
      .replaceAll('<', '&lt;')
      .replaceAll('>', '&gt;')
      .replaceAll('"', '&quot;')
      .replaceAll("'", '&#039;');
  }

  // ═══════════════════════════════════════════════════════════════════════
  //  FULL AUDIT TABLE  (46 columns, real-time client-side filtering)
  // ═══════════════════════════════════════════════════════════════════════

  const auditState = { rawRows: [], total: 0, sortCol: 'url', sortDir: 'asc' };

  // ── Status code → text ────────────────────────────────────────────────
  const STATUS_TEXT = {
    200:'OK', 201:'Created', 204:'No Content', 206:'Partial Content',
    301:'Moved Permanently', 302:'Found', 303:'See Other', 307:'Temporary Redirect', 308:'Permanent Redirect',
    400:'Bad Request', 401:'Unauthorized', 403:'Forbidden', 404:'Not Found', 405:'Method Not Allowed',
    410:'Gone', 429:'Too Many Requests',
    500:'Internal Server Error', 502:'Bad Gateway', 503:'Service Unavailable', 504:'Gateway Timeout',
  };

  function statusText(code) { return STATUS_TEXT[parseInt(code)] || (code ? 'Unknown (' + code + ')' : ''); }

  // ── Readability label from Flesch score ───────────────────────────────
  function fleschLabel(score) {
    const s = parseFloat(score || 0);
    if (s >= 90) return 'Very Easy';
    if (s >= 80) return 'Easy';
    if (s >= 70) return 'Fairly Easy';
    if (s >= 60) return 'Standard';
    if (s >= 50) return 'Fairly Difficult';
    if (s >= 30) return 'Difficult';
    return 'Very Difficult';
  }

  // ── Indexability from row ─────────────────────────────────────────────
  function rowIndexability(row) {
    const code   = parseInt(row.status_code || 0);
    const robots = ((row.meta_robots || '') + ' ' + (row.x_robots_tag || '')).toLowerCase();
    if (robots.includes('noindex')) return { status: 'Not Indexable', reason: 'Noindex tag' };
    if (code >= 400)  return { status: 'Not Indexable', reason: `Error (${code})` };
    if (code >= 300)  return { status: 'Not Indexable', reason: `Redirect (${code})` };
    if (code !== 200) return { status: 'Not Indexable', reason: `Non-200 (${code})` };
    return { status: 'Indexable', reason: 'Indexable' };
  }

  // ── Derive all computed fields from a raw row ─────────────────────────
  function deriveRow(row, totalPages) {
    const h2s     = safeParseJson(row.h2_list);
    const idx     = rowIndexability(row);
    const flesch  = parseFloat(row.flesch_reading_ease || 0);
    const wc      = parseInt(row.word_count    || 0);
    const sc      = parseInt(row.sentence_count || 0);
    const urlPath = (row.url || '').replace(/^https?:\/\/[^/]+/, '');
    // Distinguish "not yet calculated" (migration 005 columns empty) from genuine zero
    const hasMetrics = sc > 0 || flesch > 0 || parseFloat(row.text_ratio || 0) > 0;
    return {
      ...row,
      _status_text  : statusText(row.status_code),
      _indexability : idx.status,
      _idx_status   : idx.reason,
      _title_px     : Math.round((parseInt(row.title_length || 0)) * 7.5),
      _meta_px      : Math.round((parseInt(row.meta_description_length || 0)) * 6.2),
      _h1_length    : (row.h1 || '').length,
      _h2_1         : h2s[0] || '',
      _h2_1_len     : (h2s[0] || '').length,
      _h2_2         : h2s[1] || '',
      _h2_2_len     : (h2s[1] || '').length,
      _avg_words_sent: sc > 0 ? (wc / sc).toFixed(2) : '—',
      _readability  : hasMetrics ? fleschLabel(flesch) : 'N/A',
      _folder_depth : (urlPath.match(/\//g) || []).length,
      _js_inlinks   : 0,
      _pct_total    : totalPages > 0 ? (100 / totalPages).toFixed(2) : '0',
      _js_outlinks  : 0,
      _ext_js_out   : 0,
      _url_encoded  : encodeURIComponent(row.url || ''),
      _has_metrics  : hasMetrics,
      _category     : getCategoryForUrl(row.url || ''),
    };
  }

  function safeParseJson(v) {
    if (!v) return [];
    try { const r = JSON.parse(v); return Array.isArray(r) ? r : []; } catch { return []; }
  }

  // ── Quick-filter preset logic (All Pages table) ──────────────────────
  let activePreset = 'all';

  const FA_PRESETS = {
    all:          () => true,
    broken:       r => parseInt(r.status_code || 0) >= 400 && parseInt(r.status_code || 0) < 500,
    'server-err': r => parseInt(r.status_code || 0) >= 500,
    slow:         r => parseFloat(r.response_time || 0) > 3,
    'no-title':   r => !r.title || r.title === '',
    'title-long': r => parseInt(r.title_length || 0) > 60,
    'no-meta':    r => !r.meta_description || r.meta_description === '',
    'meta-long':  r => parseInt(r.meta_description_length || 0) > 160,
    'no-h1':      r => (!r.h1 || r.h1 === '') && parseInt(r.status_code || 0) === 200,
    noindex:      r => ((r.meta_robots || '') + (r.x_robots_tag || '')).toLowerCase().includes('noindex'),
    duplicate:    r => Boolean(r.near_duplicate_hash),
    'no-canonical': r => (!r.canonical_url || r.canonical_url === '') && parseInt(r.status_code || 0) === 200,
    'low-read':   r => parseFloat(r.flesch_reading_ease || 0) > 0 && parseFloat(r.flesch_reading_ease || 0) < 50,
    pdf:          r => (r.content_type || '').toLowerCase().includes('pdf'),
  };

  function bindPresetButtons() {
    const bar = qs('fa-presets');
    if (!bar) return;
    bar.querySelectorAll('button[data-preset]').forEach(btn => {
      btn.addEventListener('click', () => {
        bar.querySelectorAll('button').forEach(b => b.classList.remove('active'));
        btn.classList.add('active');
        activePreset = btn.dataset.preset;
        applyAuditFiltersAndRender();
      });
    });
  }

  // ── Tab state (cache + sort + search + quick-filter) ──────────────────
  const tabState = {
    rawRows:     {},   // keyed by tab name
    sortCol:     {},
    sortDir:     {},
    search:      '',
    quickFilter: {},   // keyed by tab: index of active preset
  };

  // Per-tab quick filter definitions
  const TAB_PRESETS = {
    all_urls: [],
    meta_tags: [
      { label: 'All',           fn: () => true },
      { label: 'Missing Title', fn: r => !r.title },
      { label: 'Title >60',     fn: r => parseInt(r.title_length || 0) > 60 },
      { label: 'Title <30',     fn: r => parseInt(r.title_length || 0) > 0 && parseInt(r.title_length || 0) < 30 },
      { label: 'Missing Meta',  fn: r => !r.meta_description },
      { label: 'Meta >160',     fn: r => parseInt(r.meta_description_length || 0) > 160 },
      { label: 'Missing H1',    fn: r => !r.h1 },
    ],
    url_issues: [
      { label: 'All',              fn: () => true },
      { label: 'Long URL (>115)',   fn: r => (r.url || '').length > 115 },
      { label: 'Has Parameters',   fn: r => (r.url || '').includes('?') },
      { label: 'Has Underscore',   fn: r => (r.url || '').replace(/^https?:\/\/[^/]+/, '').includes('_') },
      { label: 'Uppercase in Path',fn: r => /[A-Z]/.test((r.url || '').replace(/^https?:\/\/[^/]+/, '')) },
    ],
    content: [
      { label: 'All',          fn: () => true },
      { label: 'Thin (<300)',  fn: r => parseInt(r.word_count || 0) > 0 && parseInt(r.word_count || 0) < 300 },
      { label: 'Duplicate',    fn: r => Boolean(r.near_duplicate_hash) },
      { label: 'Low Readability', fn: r => parseFloat(r.flesch_reading_ease || 0) > 0 && parseFloat(r.flesch_reading_ease || 0) < 50 },
    ],
    images: [
      { label: 'All',          fn: () => true },
      { label: 'Missing Alt',  fn: r => parseInt(r.is_missing_alt || 0) === 1 },
    ],
    inlinks: [
      { label: 'All',       fn: () => true },
      { label: 'Orphans (0 inlinks)', fn: r => parseInt(r.inlinks || 0) === 0 },
    ],
  };

  function renderTabQuickFilters(tab) {
    const bar = qs('tab-quick-filters');
    if (!bar) return;
    const presets = TAB_PRESETS[tab] || [];
    if (!presets.length) { bar.innerHTML = ''; return; }

    const activeIdx = tabState.quickFilter[tab] ?? 0;
    bar.innerHTML = presets.map((p, i) =>
      `<button class="btn btn-xs ${i === activeIdx ? 'btn-secondary' : 'btn-outline-secondary'} tab-qf-btn" data-qf-idx="${i}" style="font-size:11px;padding:1px 8px;">${escapeHtml(p.label)}</button>`
    ).join('');

    bar.querySelectorAll('.tab-qf-btn').forEach(btn => {
      btn.addEventListener('click', () => {
        tabState.quickFilter[tab] = parseInt(btn.dataset.qfIdx);
        reRenderCurrentTab();
        renderTabQuickFilters(tab);
      });
    });
  }

  function applyTabSearch(rows, tab) {
    const q = tabState.search.toLowerCase();
    if (!q) return rows;
    return rows.filter(r =>
      Object.values(r).some(v => String(v ?? '').toLowerCase().includes(q))
    );
  }

  function applyTabQuickFilter(rows, tab) {
    const presets = TAB_PRESETS[tab];
    if (!presets || !presets.length) return rows;
    const idx = tabState.quickFilter[tab] ?? 0;
    const fn  = presets[idx]?.fn;
    return fn ? rows.filter(fn) : rows;
  }

  function sortTabRows(rows, tab) {
    const col = tabState.sortCol[tab];
    const dir = tabState.sortDir[tab] || 'asc';
    if (!col) return rows;
    return [...rows].sort((a, b) => {
      const va = a[col] ?? '', vb = b[col] ?? '';
      const na = parseFloat(va), nb = parseFloat(vb);
      const cmp = (!isNaN(na) && !isNaN(nb)) ? na - nb : String(va).localeCompare(String(vb));
      return dir === 'asc' ? cmp : -cmp;
    });
  }

  function reRenderCurrentTab() {
    const tab  = state.tab;
    let rows   = tabState.rawRows[tab] || [];
    rows = applyTabSearch(rows, tab);
    rows = applyTabQuickFilter(rows, tab);
    rows = sortTabRows(rows, tab);
    renderTabTable(rows, tab);
    const infoEl = qs('tab-page-info');
    if (infoEl) infoEl.textContent = rows.length + ' rows';
  }

  // Tech tab state
  const techTabState = {
    rawRows: {},
    sortCol: {},
    sortDir: {},
  };

  function applyTechFilters(rows) {
    const q      = ((qs('tech-search') || {}).value || '').toLowerCase().trim();
    const noindex = (qs('tech-qf-noindex')  || {}).checked;
    const redir   = (qs('tech-qf-redirect') || {}).checked;
    const slow    = (qs('tech-qf-slow')     || {}).checked;
    const errors  = (qs('tech-qf-errors')   || {}).checked;

    if (q)      rows = rows.filter(r => Object.values(r).some(v => String(v ?? '').toLowerCase().includes(q)));
    if (noindex) rows = rows.filter(r => r.indexability !== 'Indexable');
    if (redir)   rows = rows.filter(r => r.redirect_type && r.redirect_type !== '');
    if (slow)    rows = rows.filter(r => parseFloat(r.response_time || 0) > 3);
    if (errors)  rows = rows.filter(r => parseInt(r.status_code || 0) >= 400);
    return rows;
  }

  function reRenderTechTab() {
    const tab  = activeTechTab;
    let rows   = techTabState.rawRows[tab] || [];
    const col  = techTabState.sortCol[tab];
    const dir  = techTabState.sortDir[tab] || 'asc';

    rows = applyTechFilters(rows);

    if (col) {
      rows = [...rows].sort((a, b) => {
        const va = a[col] ?? '', vb = b[col] ?? '';
        const na = parseFloat(va), nb = parseFloat(vb);
        const cmp = (!isNaN(na) && !isNaN(nb)) ? na - nb : String(va).localeCompare(String(vb));
        return dir === 'asc' ? cmp : -cmp;
      });
    }

    const totalEl = qs('tech-total');
    if (totalEl) totalEl.textContent = rows.length + ' rows';
    renderTechTabTable(TECH_TAB_COLS[tab] || [], rows);
  }

  // ── 46-column definitions ─────────────────────────────────────────────
  const FA_COLS = [
    { k:'url',                    h:'Address',                       w:260 },
    { k:'_category',              h:'Category',                      w:120 },
    { k:'content_type',           h:'Content Type',                  w:120 },
    { k:'status_code',            h:'Status Code',                   w:80  },
    { k:'_status_text',           h:'Status',                        w:120 },
    { k:'_indexability',          h:'Indexability',                  w:110 },
    { k:'_idx_status',            h:'Indexability Status',           w:160 },
    { k:'title',                  h:'Title 1',                       w:200 },
    { k:'title_length',           h:'Title 1 Length',                w:80  },
    { k:'_title_px',              h:'Title 1 Pixel Width',           w:110 },
    { k:'meta_description',       h:'Meta Description 1',            w:220 },
    { k:'meta_description_length',h:'Meta Description 1 Length',     w:110 },
    { k:'_meta_px',               h:'Meta Desc 1 Pixel Width',       w:120 },
    { k:'h1',                     h:'H1-1',                          w:180 },
    { k:'_h1_length',             h:'H1-1 Length',                   w:80  },
    { k:'_h2_1',                  h:'H2-1',                          w:180 },
    { k:'_h2_1_len',              h:'H2-1 Length',                   w:80  },
    { k:'_h2_2',                  h:'H2-2',                          w:180 },
    { k:'_h2_2_len',              h:'H2-2 Length',                   w:80  },
    { k:'canonical_url',          h:'Canonical Link Element 1',      w:220 },
    { k:'content_length',         h:'Size (bytes) ⁽¹⁾',              w:100 },
    { k:'transferred_bytes',      h:'Transferred (bytes)',            w:110 },
    { k:'word_count',             h:'Word Count',                    w:80  },
    { k:'sentence_count',         h:'Sentence Count',                w:100 },
    { k:'_avg_words_sent',        h:'Avg Words Per Sentence',        w:130 },
    { k:'flesch_reading_ease',    h:'Flesch Reading Ease Score',     w:130 },
    { k:'_readability',           h:'Readability',                   w:120 },
    { k:'text_ratio',             h:'Text Ratio',                    w:80  },
    { k:'depth',                  h:'Crawl Depth',                   w:80  },
    { k:'_folder_depth',          h:'Folder Depth',                  w:90  },
    { k:'inlinks',                h:'Inlinks',                       w:70  },
    { k:'unique_inlinks',         h:'Unique Inlinks',                w:95  },
    { k:'_js_inlinks',            h:'Unique JS Inlinks',             w:105 },
    { k:'_pct_total',             h:'% of Total',                    w:75  },
    { k:'outlinks',               h:'Outlinks',                      w:70  },
    { k:'unique_outlinks',        h:'Unique Outlinks',               w:100 },
    { k:'_js_outlinks',           h:'Unique JS Outlinks',            w:105 },
    { k:'external_outlinks',      h:'External Outlinks',             w:110 },
    { k:'unique_external_outlinks',h:'Unique External Outlinks',     w:135 },
    { k:'_ext_js_out',            h:'Unique External JS Outlinks',   w:145 },
    { k:'response_time',          h:'Response Time',                 w:100 },
    { k:'last_modified',          h:'Last Modified',                 w:150 },
    { k:'redirect_url',           h:'Redirect URL',                  w:220 },
    { k:'redirect_type',          h:'Redirect Type',                 w:90  },
    { k:'http_version',           h:'HTTP Version',                  w:90  },
    { k:'_url_encoded',           h:'URL Encoded Address',           w:260 },
    { k:'created_at',             h:'Crawl Timestamp',               w:140 },
  ];

  // ── Build thead ───────────────────────────────────────────────────────
  function buildFullAuditHead() {
    const thead = qs('full-audit-thead');
    if (!thead) return;
    const row = document.createElement('tr');
    FA_COLS.forEach((col) => {
      const th = document.createElement('th');
      th.textContent   = col.h;
      th.style.minWidth = col.w + 'px';
      th.style.cursor  = 'pointer';
      th.style.userSelect = 'none';
      th.dataset.faCol = col.k;
      th.title         = 'Sort by ' + col.h;
      row.appendChild(th);
    });
    thead.appendChild(row);
  }

  // ── Render filtered + sorted rows ─────────────────────────────────────
  function renderFullAuditTable(rows) {
    const tbody   = qs('full-audit-tbody');
    const countEl = qs('fa-count');
    const sortEl  = qs('fa-sort-info');
    if (!tbody) return;
    tbody.innerHTML = '';

    if (!rows.length) {
      tbody.innerHTML = '<tr><td colspan="46" class="text-muted text-center py-4">No rows match the current filters.</td></tr>';
      if (countEl) countEl.textContent = '0 rows';
      return;
    }

    const loadedTotal = auditState.rawRows.length;
    const filteredNote = rows.length < loadedTotal
      ? ` (filtered from ${loadedTotal.toLocaleString()} loaded)`
      : ` of ${auditState.total.toLocaleString()} in run`;
    if (countEl) countEl.textContent = rows.length.toLocaleString() + ' rows' + filteredNote;
    if (sortEl)  sortEl.textContent  = '↕ ' + (FA_COLS.find(c => c.k === auditState.sortCol)?.h || auditState.sortCol) + ' ' + auditState.sortDir.toUpperCase();

    rows.forEach((row) => {
      const tr = document.createElement('tr');
      FA_COLS.forEach((col) => {
        const td  = document.createElement('td');
        const val = row[col.k];

        if (col.k === '_category') {
          td.innerHTML = catBadge(val);
        } else if (col.k === 'url') {
          td.innerHTML = `<a href="${escapeHtml(val || '')}" target="_blank" rel="noopener" class="small text-truncate d-inline-block" style="max-width:${col.w - 10}px;">${escapeHtml(val || '')}</a>
            <button class="btn serp-preview-btn py-0 px-1 ms-1" style="font-size:10px;line-height:1;" data-url="${escapeHtml(row.url)}" data-title="${escapeHtml(row.title||'')}" data-meta="${escapeHtml(row.meta_description||'')}">▤</button>`;
        } else if (col.k === 'status_code') {
          const c = parseInt(val || 0);
          const cls = c >= 400 ? 'text-danger' : c >= 300 ? 'text-warning' : c === 200 ? 'text-success' : '';
          td.innerHTML = `<span class="fw-semibold ${cls}">${val ?? ''}</span>`;
        } else if (col.k === '_indexability') {
          td.innerHTML = val === 'Indexable'
            ? '<span class="badge bg-success-subtle text-success">Indexable</span>'
            : `<span class="badge bg-danger-subtle text-danger">${escapeHtml(val || '')}</span>`;
        } else if (col.k === 'title' || col.k === 'meta_description' || col.k === 'h1' || col.k === '_h2_1' || col.k === '_h2_2') {
          td.innerHTML = `<span class="small text-truncate d-inline-block" style="max-width:${col.w - 10}px;" title="${escapeHtml(val||'')}">${escapeHtml(val||'')}</span>`;
        } else if (col.k === 'canonical_url' || col.k === 'redirect_url' || col.k === '_url_encoded') {
          td.innerHTML = `<span class="small text-truncate text-muted d-inline-block" style="max-width:${col.w - 10}px;" title="${escapeHtml(val||'')}">${escapeHtml(val||'')}</span>`;
        } else if (col.k === 'response_time') {
          const rt = parseFloat(val || 0);
          td.innerHTML = `<span class="${rt > 3 ? 'text-danger fw-semibold' : 'text-muted'} small">${rt.toFixed(3)}s</span>`;
        } else if (col.k === 'text_ratio') {
          const tr2 = parseFloat(val || 0);
          td.innerHTML = tr2 > 0
            ? `<span class="small">${tr2.toFixed(1)}%</span>`
            : `<span class="small text-muted">—</span>`;
        } else if (col.k === 'sentence_count' || col.k === 'flesch_reading_ease') {
          const sv = parseFloat(val || 0);
          td.className = 'small text-muted';
          td.textContent = (!row._has_metrics || sv === 0) ? '—' : sv;
        } else if (col.k === '_readability') {
          const label = val || '';
          if (label === 'N/A') {
            td.innerHTML = `<span class="small text-muted">—</span>`;
          } else {
            const cls = label.includes('Easy') ? 'success' : label.includes('Standard') ? 'info' : label.includes('Difficult') ? 'warning' : 'danger';
            td.innerHTML = `<span class="badge bg-${cls}-subtle text-${cls}">${escapeHtml(label)}</span>`;
          }
        } else {
          td.className  = 'small text-muted';
          td.textContent = val !== null && val !== undefined ? val : '—';
        }
        tr.appendChild(td);
      });
      tbody.appendChild(tr);
    });
  }

  // ── Real-time filter + sort ───────────────────────────────────────────
  function applyAuditFiltersAndRender() {
    const search   = (qs('fa-search')       || {}).value?.trim().toLowerCase() || '';
    const status   = (qs('fa-status')       || {}).value || 'all';
    const type     = (qs('fa-type')         || {}).value || 'html';
    const indexF   = (qs('fa-indexability') || {}).value || 'all';
    const assetRx  = /\.(jpe?g|png|gif|svg|webp|ico|css|js|woff2?|ttf|eot|pdf|zip|mp4|mp3)$/i;

    let rows = auditState.rawRows.map(r => deriveRow(r, auditState.total));

    // Noise filter
    if (!state.noise) rows = rows.filter(r => !isNoiseUrl(r.url));

    // Search (Address, Title 1, Meta Desc, Status, Status Code)
    if (search) rows = rows.filter(r =>
      [r.url, r.title, r.meta_description, r._status_text, String(r.status_code || '')]
        .join(' ').toLowerCase().includes(search)
    );

    // Status filter
    if (status !== 'all') {
      rows = rows.filter(r => {
        const c  = parseInt(r.status_code || 0);
        const rt = parseFloat(r.response_time || 0);
        if (status === 'ok')       return c >= 200 && c < 300;
        if (status === 'redirect') return c >= 300 && c < 400;
        if (status === '4xx')      return c >= 400 && c < 500;
        if (status === '5xx')      return c >= 500;
        if (status === 'slow')     return rt > 3;
        return true;
      });
    }

    // Type filter
    if (type !== 'all') {
      rows = rows.filter(r => {
        const ct  = (r.content_type || '').toLowerCase();
        const url = (r.url || '').toLowerCase();
        if (type === 'html')  return ct.includes('html') || (!ct && !assetRx.test(url));
        if (type === 'image') return ct.includes('image') || /\.(jpe?g|png|gif|svg|webp|ico)$/i.test(url);
        if (type === 'other') return !ct.includes('html') && !ct.includes('image');
        return true;
      });
    }

    // Indexability filter
    if (indexF === 'indexable') rows = rows.filter(r => r._indexability === 'Indexable');
    if (indexF === 'noindex')   rows = rows.filter(r => r._indexability !== 'Indexable');

    // Quick-preset filter (All Pages)
    const presetFn = FA_PRESETS[activePreset];
    if (presetFn) rows = rows.filter(presetFn);

    // Category filter
    const catF = (qs('fa-category') || {}).value || 'all';
    if (catF !== 'all') {
      const catObj = auditState.categories.find(c => String(c.id) === catF);
      if (catObj) rows = rows.filter(r => matchesCategory(r.url, catObj));
    }

    // Exclude "exclude_from_filters" categories when noise is OFF
    if (!state.noise) {
      const excludedCats = auditState.categories.filter(c => c.exclude_from_filters);
      if (excludedCats.length) {
        rows = rows.filter(r => !excludedCats.some(cat => matchesCategory(r.url, cat)));
      }
    }

    // Sort (client-side, all rows, numeric-aware)
    const sortKey = auditState.sortCol;
    const dir     = auditState.sortDir === 'asc' ? 1 : -1;
    rows.sort((a, b) => {
      const va = a[sortKey], vb = b[sortKey];
      const na = parseFloat(va), nb = parseFloat(vb);
      if (!isNaN(na) && !isNaN(nb)) return (na - nb) * dir;
      return String(va ?? '').localeCompare(String(vb ?? '')) * dir;
    });

    renderFullAuditTable(rows);
  }

  // ── Load full audit rows from server ──────────────────────────────────
  async function loadFullAudit() {
    if (!state.projectId || !state.crawlRunId) return;
    const limit = Math.min(1000, Math.max(10, parseInt((qs('fa-limit') || {}).value || 200, 10)));
    const countEl = qs('fa-count');
    if (countEl) countEl.textContent = 'Loading…';

    const data = await api(
      `/api/projects/${state.projectId}/website-audit-full?crawl_run_id=${state.crawlRunId}&limit=${limit}`
    );
    auditState.rawRows = data.rows  || [];
    auditState.total   = data.total || 0;
    applyAuditFiltersAndRender();
  }

  // ── Technical sub-tabs ────────────────────────────────────────────────

  const TECH_TAB_COLS = {
    tech_security: [
      { k: 'url',                h: 'Address',              w: 250 },
      { k: 'content_type',       h: 'Content Type',         w: 120 },
      { k: 'status_code',        h: 'Status Code',          w: 80  },
      { k: 'indexability',       h: 'Indexability',         w: 110 },
      { k: 'indexability_status',h: 'Indexability Status',  w: 160 },
      { k: 'inlinks',            h: 'Inlinks',              w: 70  },
      { k: 'response_time',      h: 'Response Time',        w: 100 },
      { k: 'canonical_url',      h: 'Canonical Link Element 1', w: 200 },
      { k: 'meta_robots',        h: 'Meta Robots 1',        w: 110 },
      { k: 'x_robots_tag',       h: 'X-Robots-Tag 1',      w: 110 },
      { k: 'redirect_url',       h: 'Redirect URL',         w: 200 },
      { k: 'redirect_type',      h: 'Redirect Type',        w: 90  },
    ],
    tech_canonical: [
      { k: 'url',                h: 'Address',              w: 250 },
      { k: 'indexability',       h: 'Indexability',         w: 110 },
      { k: 'indexability_status',h: 'Indexability Status',  w: 160 },
      { k: 'canonical_url',      h: 'Canonical Link Element 1', w: 220 },
      { k: 'meta_robots',        h: 'Meta Robots 1',        w: 110 },
      { k: 'x_robots_tag',       h: 'X-Robots-Tag 1',      w: 110 },
    ],
    tech_schema: [
      { k: 'url',    h: 'Address', w: 250 },
      { k: 'type',   h: 'Type',    w: 120 },
      { k: 'valid',  h: 'Valid',   w: 70  },
      { k: 'missing',h: 'Missing Fields', w: 200 },
    ],
    tech_speed: null,
    tech_jscss: [
      { k: 'url',                h: 'Address',              w: 250 },
      { k: 'content_type',       h: 'Content Type',         w: 120 },
      { k: 'status_code',        h: 'Status Code',          w: 80  },
      { k: 'indexability',       h: 'Indexability',         w: 110 },
      { k: 'inlinks',            h: 'Inlinks',              w: 70  },
      { k: 'response_time',      h: 'Response Time',        w: 100 },
      { k: 'redirect_url',       h: 'Redirect URL',         w: 200 },
    ],
  };

  let activeTechTab = 'tech_security';

  async function loadTechTab(tab) {
    activeTechTab = tab;
    const thead   = qs('tech-detail-thead');
    const tbody   = qs('tech-detail-tbody');
    const totalEl = qs('tech-total');
    if (!thead || !tbody) return;
    thead.innerHTML = '';
    tbody.innerHTML = '<tr><td class="text-muted text-center py-4">Loading…</td></tr>';

    // tech_security: built from rawRows so inlinks are accurate (rawRows has link JOINs)
    if (tab === 'tech_security') {
      const inlinkMap = {};
      auditState.rawRows.forEach(r => { inlinkMap[r.url] = parseInt(r.inlinks || 0); });
      techTabState.rawRows[tab] = auditState.rawRows.map(r => {
        const idx = rowIndexability(r);
        return {
          url: r.url, content_type: r.content_type || '', status_code: r.status_code,
          indexability: idx.status, indexability_status: idx.reason,
          inlinks: inlinkMap[r.url] ?? 0, response_time: r.response_time,
          canonical_url: r.canonical_url || '', meta_robots: r.meta_robots || '',
          x_robots_tag: r.x_robots_tag || '', redirect_url: r.redirect_url || '',
          redirect_type: r.redirect_type || '',
        };
      });
      reRenderTechTab();
      return;
    }

    if (tab === 'tech_speed') {
      tbody.innerHTML = '<tr><td class="py-4 text-center text-muted">Use the <strong>Core Web Vitals</strong> section above for Site Speed data (requires PageSpeed Insights API key).</td></tr>';
      if (totalEl) totalEl.textContent = '';
      return;
    }

    // tech_jscss = filter rawRows to non-HTML types and show security columns
    if (tab === 'tech_jscss') {
      techTabState.rawRows[tab] = auditState.rawRows
        .filter(r => !(r.content_type || '').toLowerCase().includes('html'))
        .map(r => {
          const idx = rowIndexability(r);
          return {
            url: r.url, content_type: r.content_type, status_code: r.status_code,
            indexability: idx.status, inlinks: r.inlinks ?? 0,
            response_time: r.response_time, redirect_url: r.redirect_url ?? '',
          };
        });
      reRenderTechTab();
      return;
    }

    if (!state.crawlRunId) {
      tbody.innerHTML = '<tr><td class="text-muted text-center py-4">Load a crawl run first.</td></tr>';
      return;
    }

    const data = await api(
      `/api/projects/${state.projectId}/crawl/${state.crawlRunId}/tabs?tab=${tab}&limit=${state.techLimit}&offset=${state.techOffset}`
    );
    techTabState.rawRows[tab] = data.rows || [];
    reRenderTechTab();

    const tn = qs('tech-next'); const tp = qs('tech-prev');
    if (tn) tn.disabled = (data.rows || []).length < state.techLimit;
    if (tp) tp.disabled = state.techOffset === 0;
  }

  function renderTechTabTable(cols, rows) {
    const thead = qs('tech-detail-thead');
    const tbody = qs('tech-detail-tbody');
    if (!thead || !tbody) return;
    thead.innerHTML = '';
    tbody.innerHTML = '';

    if (!rows.length) {
      tbody.innerHTML = '<tr><td colspan="' + cols.length + '" class="text-muted text-center py-4">No data for this tab.</td></tr>';
      return;
    }

    const tab = activeTechTab;
    const sc  = techTabState.sortCol[tab];
    const sd  = techTabState.sortDir[tab] || 'asc';
    const hr  = document.createElement('tr');
    cols.forEach(col => {
      const th = document.createElement('th');
      th.style.minWidth  = col.w + 'px';
      th.style.cursor    = 'pointer';
      th.style.userSelect = 'none';
      th.dataset.techSort = col.k;
      th.textContent = col.h + (sc === col.k ? (sd === 'asc' ? ' ↑' : ' ↓') : '');
      hr.appendChild(th);
    });
    thead.appendChild(hr);
    bindTechTabSort(tab);

    rows.forEach(row => {
      const tr = document.createElement('tr');
      cols.forEach(col => {
        const td  = document.createElement('td');
        const val = row[col.k];
        if (col.k === 'url') {
          td.innerHTML = `<a href="${escapeHtml(val||'')}" target="_blank" rel="noopener" class="small text-truncate d-inline-block" style="max-width:${col.w-10}px;">${escapeHtml(val||'')}</a>`;
        } else if (col.k === 'status_code') {
          const c = parseInt(val || 0);
          const cls = c >= 400 ? 'text-danger' : c >= 300 ? 'text-warning' : 'text-success';
          td.innerHTML = `<span class="fw-semibold small ${cls}">${val ?? ''}</span>`;
        } else if (col.k === 'indexability') {
          td.innerHTML = val === 'Indexable'
            ? '<span class="badge bg-success-subtle text-success">Indexable</span>'
            : `<span class="badge bg-danger-subtle text-danger">${escapeHtml(val||'')}</span>`;
        } else if (col.k === 'redirect_type') {
          if (val && val !== '—' && val !== '') {
            td.innerHTML = `<span class="badge bg-warning-subtle text-warning" title="Original URL returned ${val}; crawler followed to final destination">${escapeHtml(String(val))}</span>`;
          } else {
            td.className = 'small text-muted'; td.textContent = '—';
          }
        } else if (col.k === 'valid') {
          td.innerHTML = val ? '<span class="badge bg-success-subtle text-success">✓</span>' : '<span class="badge bg-danger-subtle text-danger">✗</span>';
        } else if (col.k === 'response_time') {
          const rt = parseFloat(val || 0);
          td.innerHTML = `<span class="${rt > 3 ? 'text-danger fw-semibold' : 'text-muted'} small">${rt.toFixed(3)}s</span>`;
        } else if (col.k === 'missing') {
          td.innerHTML = `<span class="small text-danger">${escapeHtml(Array.isArray(val) ? val.join(', ') : (val||'—'))}</span>`;
        } else {
          td.className = 'small text-muted';
          td.textContent = val !== null && val !== undefined ? String(val) : '—';
        }
        tr.appendChild(td);
      });
      tbody.appendChild(tr);
    });
  }

  // ═══════════════════════════════════════════════════════════════════════
  //  PAGE CATEGORIES
  // ═══════════════════════════════════════════════════════════════════════

  auditState.categories = [];   // loaded per project

  async function loadCategories() {
    if (!state.projectId) return;
    auditState.categories = await api(`/api/projects/${state.projectId}/categories`);
    syncCategoryFilterSelect();
  }

  function syncCategoryFilterSelect() {
    const sel = qs('fa-category');
    if (!sel) return;
    const cur = sel.value;
    sel.innerHTML = '<option value="all">All Categories</option>';
    auditState.categories.forEach((c) => {
      const o = document.createElement('option');
      o.value = String(c.id);
      o.textContent = c.name;
      if (String(c.id) === cur) o.selected = true;
      sel.appendChild(o);
    });
    sel.addEventListener('change', () => applyAuditFiltersAndRender());
  }

  // Match a URL against a single category's rule
  function matchesCategory(url, cat) {
    if (!url) return false;
    if (cat.match_type === 'noise_builtin') return isNoiseUrl(url);
    if (cat.match_type === 'path_prefix') {
      const v = (cat.match_value || '').trim();
      return v !== '' && url.includes(v);
    }
    if (cat.match_type === 'regex') {
      try { return new RegExp(cat.match_value || '').test(url); } catch { return false; }
    }
    if (cat.match_type === 'url_list') {
      const list = (cat.match_value || '').split('\n').map(s => s.trim()).filter(Boolean);
      return list.includes(url);
    }
    return false;
  }

  // Return the first matching category for a URL (or null)
  function getCategoryForUrl(url) {
    for (const cat of auditState.categories) {
      if (matchesCategory(url, cat)) return cat;
    }
    return null;
  }

  // Render category badge HTML
  function catBadge(cat) {
    if (!cat) return '<span class="small text-muted">—</span>';
    return `<span class="badge bg-${escapeHtml(cat.color)}-subtle text-${escapeHtml(cat.color)}">${escapeHtml(cat.name)}</span>`;
  }

  // ── Category modal ────────────────────────────────────────────────────────

  async function renderCategoryModal() {
    const list = qs('cat-list');
    if (!list) return;
    if (!auditState.categories.length) {
      list.innerHTML = '<p class="text-muted small">No categories yet. Add one below.</p>';
      return;
    }
    list.innerHTML = auditState.categories.map(cat => `
      <div class="d-flex align-items-center gap-2 mb-2 p-2 border rounded">
        <span class="badge bg-${escapeHtml(cat.color)}-subtle text-${escapeHtml(cat.color)} flex-shrink-0">${escapeHtml(cat.name)}</span>
        <span class="small text-muted flex-grow-1">${escapeHtml(cat.match_type === 'noise_builtin' ? 'Built-in noise detection' : cat.match_value || '')} &nbsp;<em class="text-muted">(${escapeHtml(cat.match_type)})</em></span>
        ${cat.exclude_from_filters ? '<span class="badge bg-secondary-subtle text-secondary small">Excludes from filters</span>' : ''}
        <button class="btn btn-sm btn-outline-danger ms-auto cat-delete-btn" data-cat-id="${cat.id}" style="padding:0 6px;"><i class="bi bi-trash"></i></button>
      </div>
    `).join('');

    list.querySelectorAll('.cat-delete-btn').forEach(btn => {
      btn.addEventListener('click', async () => {
        const id = btn.dataset.catId;
        await api(`/api/projects/${state.projectId}/categories/${id}`, { method: 'DELETE' });
        auditState.categories = auditState.categories.filter(c => String(c.id) !== String(id));
        syncCategoryFilterSelect();
        renderCategoryModal();
        applyAuditFiltersAndRender();
      });
    });
  }

  function bindCategoryModal() {
    const openBtn = qs('open-categories-btn');
    if (openBtn) openBtn.addEventListener('click', () => {
      renderCategoryModal();
      new bootstrap.Modal(qs('categoriesModal')).show();
    });

    const saveBtn = qs('cat-save-btn');
    if (saveBtn) saveBtn.addEventListener('click', async () => {
      const name = (qs('cat-name') || {}).value?.trim();
      if (!name) { alert('Please enter a category name.'); return; }
      if (!state.projectId) { alert('Select a project first.'); return; }
      const body = {
        name,
        color:                 (qs('cat-color')       || {}).value || 'secondary',
        match_type:            (qs('cat-match-type')   || {}).value || 'path_prefix',
        match_value:           (qs('cat-match-value')  || {}).value || null,
        exclude_from_filters:  (qs('cat-exclude')      || {}).checked ? 1 : 0,
        sort_order:            auditState.categories.length,
      };
      auditState.categories = await api(`/api/projects/${state.projectId}/categories`, {
        method:  'POST',
        headers: { 'Content-Type': 'application/json' },
        body:    JSON.stringify(body),
      });
      syncCategoryFilterSelect();
      renderCategoryModal();
      applyAuditFiltersAndRender();
      if (qs('cat-name')) qs('cat-name').value = '';
      if (qs('cat-match-value')) qs('cat-match-value').value = '';
    });
  }

  // ── Site-Level scan ───────────────────────────────────────────────────────

  async function loadSiteHealth() {
    if (!state.projectId || !state.crawlRunId) return;
    const domain = (qs('domain') || {}).value?.trim();
    if (!domain) return;

    const setEl = (id, html) => { const e = qs(id); if (e) e.innerHTML = html; };
    setEl('site-robots',  '<span class="text-muted small">Scanning…</span>');
    setEl('site-sitemap', '<span class="text-muted small">Scanning…</span>');
    setEl('site-schema',  '<span class="text-muted small">Scanning…</span>');

    const data = await api(`/api/projects/${state.projectId}/site-health?crawl_run_id=${state.crawlRunId}&domain=${encodeURIComponent(domain)}`);

    // KPI cards
    const idxEl = qs('site-indexed-pages');  if (idxEl) idxEl.textContent = data.indexed_pages ?? '—';
    const smcEl = qs('site-sitemap-count');  if (smcEl) smcEl.textContent = data.sitemap?.total_urls ?? '—';
    const schEl = qs('site-schema-count');   if (schEl) schEl.textContent = data.schema_pages_count ?? '—';

    // Robots.txt
    const r = data.robots || {};
    if (r.found) {
      const rIssues = (r.issues || []).length;
      setEl('site-robots', rIssues
        ? `<span class="text-warning">Found — ${rIssues} issue${rIssues>1?'s':''}</span>`
        : '<span class="text-success">Found ✓</span>');
    } else {
      setEl('site-robots', `<span class="text-danger">Not found (${r.status || 'error'})</span>`);
    }

    // Sitemap
    const sm = data.sitemap || {};
    if (sm.found) {
      const smIssues = (sm.issues || []).length;
      setEl('site-sitemap', smIssues
        ? `<span class="text-warning">Found — ${sm.total_urls} URLs, ${smIssues} issue${smIssues>1?'s':''}</span>`
        : `<span class="text-success">Found ✓ — ${sm.total_urls} URLs</span>`);
    } else {
      setEl('site-sitemap', '<span class="text-warning">Not found — add /sitemap.xml</span>');
    }

    // Schema
    setEl('site-schema', `<span class="text-success">${data.schema_pages_count ?? 0} pages with schema</span>`);

    // Issues list
    const issueBody = qs('site-issues-body');
    if (issueBody) {
      const allIssues = [...(r.issues || []), ...(sm.issues || [])];
      if (!allIssues.length) {
        issueBody.innerHTML = '<p class="text-success small mb-0">✓ No robots.txt or sitemap issues found.</p>';
      } else {
        issueBody.innerHTML = '<ul class="list-unstyled mb-0 small">' +
          allIssues.map(i => `<li class="mb-1"><span class="text-warning">⚠</span> ${escapeHtml(i)}</li>`).join('') +
          '</ul>';
      }
    }

    // Disallow rules
    const disList = qs('site-disallows-list');
    if (disList) {
      const disallows = r.disallows || [];
      if (!disallows.length) {
        disList.innerHTML = '<li class="list-group-item small text-muted">No Disallow rules found.</li>';
      } else {
        disList.innerHTML = disallows.map(d =>
          `<li class="list-group-item small d-flex justify-content-between">
            <code>${escapeHtml(d)}</code>
            ${d === '/' ? '<span class="badge bg-danger-subtle text-danger">Blocks all</span>' : ''}
          </li>`
        ).join('');
      }
    }
  }

  // ── Health score ─────────────────────────────────────────────────────────
  async function loadHealthScore() {
    if (!state.projectId || !state.crawlRunId) return;
    const data  = await api(`/api/projects/${state.projectId}/health-score?crawl_run_id=${state.crawlRunId}`);
    const score = data.score ?? 0;
    const badge = qs('health-score-badge');
    const label = qs('health-score-label');
    if (!badge) return;
    const color = score >= 80 ? '#16a34a' : score >= 50 ? '#d97706' : '#dc2626';
    badge.textContent = score;
    badge.style.color = color;
    if (label) label.textContent = `/ 100 · ${data.issue_count} issue type(s)`;
  }

  // ── SERP Preview ──────────────────────────────────────────────────────────
  function showSerpPreview(title, metaDesc, url) {
    const maxTitle = 60;
    const maxMeta  = 160;

    const displayTitle = title.length > maxTitle ? title.slice(0, maxTitle) + '…' : title;
    const displayMeta  = metaDesc.length > maxMeta ? metaDesc.slice(0, maxMeta) + '…' : metaDesc;

    const breadcrumb = url.replace(/^https?:\/\//, '').replace(/\//g, ' › ');
    qs('serp-url').textContent   = breadcrumb.slice(0, 80);
    qs('serp-title').textContent = displayTitle || '(No title)';
    qs('serp-meta').textContent  = displayMeta  || '(No meta description)';

    const tLen = title.length;
    const mLen = metaDesc.length;
    qs('serp-title-len').textContent = tLen;
    qs('serp-meta-len').textContent  = mLen;

    const tPct = Math.min(100, Math.round(tLen / 70 * 100));
    const mPct = Math.min(100, Math.round(mLen / 200 * 100));
    const tBar = qs('serp-title-bar').querySelector('.progress-bar');
    const mBar = qs('serp-meta-bar').querySelector('.progress-bar');
    tBar.style.width = tPct + '%';
    tBar.className   = 'progress-bar ' + (tLen > 60 ? 'bg-danger' : tLen < 30 ? 'bg-warning' : 'bg-success');
    mBar.style.width = mPct + '%';
    mBar.className   = 'progress-bar ' + (mLen > 160 ? 'bg-danger' : mLen < 70 ? 'bg-warning' : 'bg-success');

    new bootstrap.Modal(qs('serpPreviewModal')).show();
  }

  // ── Duplicate groups ──────────────────────────────────────────────────────
  async function loadDuplicateGroups() {
    if (!state.projectId || !state.crawlRunId) return;
    const data  = await api(`/api/projects/${state.projectId}/duplicate-groups?crawl_run_id=${state.crawlRunId}`);
    const tbody = qs('dup-groups-table').querySelector('tbody');
    tbody.innerHTML = '';
    if (!data.length) {
      tbody.innerHTML = '<tr><td colspan="3" class="text-success text-center py-3">No duplicate content found ✓</td></tr>';
      return;
    }
    data.forEach((group, i) => {
      const tr = document.createElement('tr');
      tr.innerHTML = `
        <td class="fw-semibold text-danger">${group.count}</td>
        <td><span class="font-monospace small text-muted">${escapeHtml(group.hash.slice(0, 12))}…</span></td>
        <td class="small">${group.urls.slice(0, 3).map((u) => `<div class="text-truncate" style="max-width:280px;" title="${escapeHtml(u)}">${escapeHtml(u)}</div>`).join('')}</td>
      `;
      tbody.appendChild(tr);
    });
  }

  // ── Canonical audit ───────────────────────────────────────────────────────
  async function loadCanonicalAudit() {
    if (!state.projectId || !state.crawlRunId) return;
    const data  = await api(`/api/projects/${state.projectId}/canonical-audit?crawl_run_id=${state.crawlRunId}`);
    const tbody = qs('canonical-table').querySelector('tbody');
    tbody.innerHTML = '';
    if (!data.length) {
      tbody.innerHTML = '<tr><td colspan="3" class="text-success text-center py-3">No canonical mismatches ✓</td></tr>';
      return;
    }
    data.forEach((row) => {
      const tr = document.createElement('tr');
      tr.innerHTML = `
        <td class="small text-truncate" style="max-width:200px;">${escapeHtml(row.url)}</td>
        <td class="small text-truncate text-muted" style="max-width:200px;">${escapeHtml(row.canonical_url || '—')}</td>
        <td><span class="badge bg-warning-subtle text-warning">${row.status_code}</span></td>
      `;
      tbody.appendChild(tr);
    });
  }

  // ── Heading audit ─────────────────────────────────────────────────────────
  let headingOffset = 0;
  async function loadHeadingAudit() {
    if (!state.projectId || !state.crawlRunId) return;
    const data  = await api(`/api/projects/${state.projectId}/heading-audit?crawl_run_id=${state.crawlRunId}&limit=25&offset=${headingOffset}`);
    const tbody = qs('heading-table').querySelector('tbody');
    tbody.innerHTML = '';
    if (!data.length) {
      tbody.innerHTML = '<tr><td colspan="6" class="text-muted text-center py-3">No data.</td></tr>';
      return;
    }
    data.forEach((row) => {
      const issueColor = row.issues === 'OK' ? 'success' : 'warning';
      const tr = document.createElement('tr');
      tr.innerHTML = `
        <td><a href="#" class="row-url small text-truncate d-inline-block" style="max-width:220px;" data-url="${escapeHtml(row.url)}" title="${escapeHtml(row.url)}">${escapeHtml(row.url)}</a></td>
        <td class="small">${escapeHtml(row.h1 || '—')}</td>
        <td class="small text-center">${row.h2_count}</td>
        <td class="small text-center">${row.h3_count}</td>
        <td class="small text-muted">${row.h2_sample.map((h) => escapeHtml(h)).join('<br>')}</td>
        <td><span class="badge bg-${issueColor}-subtle text-${issueColor}">${escapeHtml(row.issues)}</span></td>
      `;
      tbody.appendChild(tr);
    });
    const hn = qs('heading-next'); const hp = qs('heading-prev');
    if (hn) hn.disabled = data.length < 25;
    if (hp) hp.disabled = headingOffset === 0;
  }

  // ── Link equity ───────────────────────────────────────────────────────────
  let linkOffset = 0;
  async function loadLinkEquity() {
    if (!state.projectId || !state.crawlRunId) return;
    const data  = await api(`/api/projects/${state.projectId}/link-equity?crawl_run_id=${state.crawlRunId}&limit=25&offset=${linkOffset}`);
    const tbody = qs('link-equity-table').querySelector('tbody');
    tbody.innerHTML = '';
    (data.pages || []).forEach((row) => {
      const tr = document.createElement('tr');
      tr.innerHTML = `
        <td><a href="#" class="row-url small text-truncate d-inline-block" style="max-width:260px;" data-url="${escapeHtml(row.url)}" title="${escapeHtml(row.url)}">${escapeHtml(row.url)}</a></td>
        <td class="fw-semibold small">${row.inlink_count}</td>
        <td class="small text-muted">${row.top_anchors.map((a) => escapeHtml(a)).join(', ')}</td>
      `;
      tbody.appendChild(tr);
    });
    const orphanEl = qs('link-orphan-count');
    if (orphanEl && data.orphan_count > 0) orphanEl.textContent = data.orphan_count + ' orphan pages';

    // Anchor distribution
    const anchorList = qs('anchor-dist-list');
    if (anchorList) {
      anchorList.innerHTML = '';
      (data.anchor_dist || []).slice(0, 15).forEach((a) => {
        const li = document.createElement('li');
        li.className   = 'list-group-item d-flex justify-content-between align-items-center py-1 px-3 small';
        li.innerHTML   = `<span class="text-truncate" style="max-width:160px;">${escapeHtml(a.anchor_text)}</span><span class="badge bg-secondary-subtle text-secondary">${a.cnt}</span>`;
        anchorList.appendChild(li);
      });
    }

    const ln = qs('link-next'); const lp = qs('link-prev');
    if (ln) ln.disabled = (data.pages || []).length < 25;
    if (lp) lp.disabled = linkOffset === 0;
  }

  // ── Crawl budget ──────────────────────────────────────────────────────────
  async function loadCrawlBudget() {
    if (!state.projectId || !state.crawlRunId) return;
    const data = await api(`/api/projects/${state.projectId}/crawl-budget?crawl_run_id=${state.crawlRunId}`);
    const body = qs('crawl-budget-body');
    if (!body) return;
    body.innerHTML = `<p class="small text-muted mb-2">Total crawled: <strong>${data.total}</strong> pages</p>`;
    (data.categories || []).forEach((cat) => {
      body.innerHTML += `
        <div class="d-flex justify-content-between align-items-center mb-1 small">
          <span>${escapeHtml(cat.label)}</span>
          <span class="text-muted">${cat.count} <span class="text-${cat.color}">(${cat.pct}%)</span></span>
        </div>
        <div class="progress mb-2" style="height:5px;">
          <div class="progress-bar bg-${cat.color}" style="width:${cat.pct}%;"></div>
        </div>`;
    });
  }

  // ── Schema validation ─────────────────────────────────────────────────────
  async function loadSchemaValidation() {
    if (!state.projectId || !state.crawlRunId) return;
    const data  = await api(`/api/projects/${state.projectId}/schema-validation?crawl_run_id=${state.crawlRunId}`);
    const tbody = qs('schema-table').querySelector('tbody');
    const sumEl = qs('schema-summary-body');
    tbody.innerHTML = '';

    if (sumEl) {
      const typeList = Object.entries(data.type_counts || {}).map(([t, c]) => `<li class="d-flex justify-content-between small py-1"><span>${escapeHtml(t)}</span><span class="badge bg-secondary-subtle text-secondary">${c}</span></li>`).join('');
      sumEl.innerHTML = `
        <p class="small mb-2">Total schemas: <strong>${data.total_schemas}</strong> &nbsp;|&nbsp; Errors: <strong class="text-danger">${data.error_count}</strong></p>
        <ul class="list-unstyled mb-0">${typeList}</ul>`;
    }

    if (!data.results || !data.results.length) {
      tbody.innerHTML = '<tr><td colspan="4" class="text-muted text-center py-3">No structured data found.</td></tr>';
      return;
    }
    data.results.forEach((row) => {
      const tr = document.createElement('tr');
      tr.innerHTML = `
        <td class="small text-truncate" style="max-width:220px;">${escapeHtml(row.url)}</td>
        <td><span class="badge bg-secondary-subtle text-secondary">${escapeHtml(row.type)}</span></td>
        <td>${row.valid ? '<span class="badge bg-success-subtle text-success">Valid</span>' : '<span class="badge bg-danger-subtle text-danger">Invalid</span>'}</td>
        <td class="small text-danger">${row.missing.map((f) => escapeHtml(f)).join(', ') || '—'}</td>
      `;
      tbody.appendChild(tr);
    });
  }

  // ── Core Web Vitals ───────────────────────────────────────────────────────
  async function loadCwv() {
    const urlEl = qs('cwv-url');
    const stratEl = qs('cwv-strategy');
    const url = urlEl ? urlEl.value.trim() : '';
    if (!url || !state.projectId) return;
    const body = qs('cwv-body');
    if (body) body.innerHTML = '<div class="spinner-border spinner-border-sm me-2"></div>Fetching PageSpeed data…';
    const data = await api(`/api/projects/${state.projectId}/page-speed?url=${encodeURIComponent(url)}&strategy=${stratEl ? stratEl.value : 'mobile'}`);
    if (!body) return;
    const renderScore = (s) => {
      const c = s >= 90 ? 'success' : s >= 50 ? 'warning' : 'danger';
      return `<div class="text-center"><div class="fs-3 fw-bold text-${c}">${s}</div><div class="small text-muted">/100</div></div>`;
    };
    const sc = data.mobile?.scores || data.scores || {};
    const lm = data.mobile?.lab_metrics || data.lab_metrics || {};
    const opps = data.mobile?.opportunities || data.opportunities || [];
    body.innerHTML = `
      <div class="row g-3 mb-3">
        ${['performance','seo','accessibility','best_practices'].map((k) => `
          <div class="col-md-3"><div class="card p-3 text-center border-0 bg-light"><div class="small text-muted mb-1">${k.replace('_',' ').replace(/\b\w/g,c=>c.toUpperCase())}</div>${renderScore(sc[k] ?? 0)}</div></div>
        `).join('')}
      </div>
      <div class="row g-3 mb-3">
        <div class="col-md-2"><div class="card p-3 bg-light border-0 text-center"><div class="small text-muted">FCP</div><div class="fw-semibold">${lm.fcp_ms != null ? (lm.fcp_ms/1000).toFixed(2)+'s' : '—'}</div></div></div>
        <div class="col-md-2"><div class="card p-3 bg-light border-0 text-center"><div class="small text-muted">LCP</div><div class="fw-semibold">${lm.lcp_ms != null ? (lm.lcp_ms/1000).toFixed(2)+'s' : '—'}</div></div></div>
        <div class="col-md-2"><div class="card p-3 bg-light border-0 text-center"><div class="small text-muted">TBT</div><div class="fw-semibold">${lm.tbt_ms != null ? lm.tbt_ms+'ms' : '—'}</div></div></div>
        <div class="col-md-2"><div class="card p-3 bg-light border-0 text-center"><div class="small text-muted">CLS</div><div class="fw-semibold">${lm.cls ?? '—'}</div></div></div>
        <div class="col-md-2"><div class="card p-3 bg-light border-0 text-center"><div class="small text-muted">Speed Index</div><div class="fw-semibold">${lm.si_ms != null ? (lm.si_ms/1000).toFixed(2)+'s' : '—'}</div></div></div>
        <div class="col-md-2"><div class="card p-3 bg-light border-0 text-center"><div class="small text-muted">TTI</div><div class="fw-semibold">${lm.tti_ms != null ? (lm.tti_ms/1000).toFixed(2)+'s' : '—'}</div></div></div>
      </div>
      ${opps.length ? `<h6 class="fw-semibold mb-2">Top Opportunities</h6><ul class="list-group list-group-flush">${opps.map((o) => `<li class="list-group-item py-2 small"><strong>${escapeHtml(o.title)}</strong>${o.savings_ms > 0 ? ` <span class="text-success ms-1">−${o.savings_ms}ms</span>` : ''}<div class="text-muted">${escapeHtml(o.description.slice(0,120))}</div></li>`).join('')}</ul>` : ''}
    `;
  }

  // ── Crawl comparison ──────────────────────────────────────────────────────
  async function runCrawlComparison() {
    const runA = (qs('compare-run-a') || {}).value;
    const runB = (qs('compare-run-b') || {}).value;
    if (!runA || !runB) {
      const body = qs('comparison-body');
      if (body) body.innerHTML = '<div class="alert alert-info mb-0"><strong>Need at least 2 crawl runs to compare.</strong> Run a second crawl for this project, then return here to see what changed between runs.</div>';
      return;
    }
    const body = qs('comparison-body');
    if (body) body.innerHTML = '<div class="spinner-border spinner-border-sm me-2"></div>Comparing…';
    const d = await api(`/api/projects/${state.projectId}/crawl-comparison?run_a=${runA}&run_b=${runB}`);
    if (!body) return;

    const healthDelta = d.health_b - d.health_a;
    const hColor = healthDelta >= 0 ? 'success' : 'danger';

    const issueRow = (label, items, color) => items.length
      ? `<h6 class="fw-semibold mt-3 text-${color}">${label}</h6><ul class="list-group list-group-flush mb-2">${items.map((i) => `<li class="list-group-item py-1 small d-flex justify-content-between"><span>${escapeHtml(i.label)}</span><span class="badge bg-${color}-subtle text-${color}">${escapeHtml(i.change)}</span></li>`).join('')}</ul>`
      : '';

    body.innerHTML = `
      <div class="row g-3 mb-3">
        <div class="col-md-3"><div class="card p-3 bg-light border-0 text-center"><div class="small text-muted">Run A pages</div><div class="fs-4 fw-bold">${d.pages_a}</div></div></div>
        <div class="col-md-3"><div class="card p-3 bg-light border-0 text-center"><div class="small text-muted">Run B pages</div><div class="fs-4 fw-bold">${d.pages_b}</div></div></div>
        <div class="col-md-3"><div class="card p-3 bg-light border-0 text-center"><div class="small text-muted">Health A→B</div><div class="fs-4 fw-bold">${d.health_a} → <span class="text-${hColor}">${d.health_b}</span></div></div></div>
        <div class="col-md-3"><div class="card p-3 bg-light border-0 text-center"><div class="small text-muted">Status Changes</div><div class="fs-4 fw-bold">${d.status_changed.length}</div></div></div>
      </div>
      ${issueRow('New / Worsened Issues', d.new_issues, 'danger')}
      ${issueRow('Fixed / Improved Issues', d.fixed_issues, 'success')}
      ${d.new_pages.length ? `<p class="small text-muted mt-2"><strong>${d.new_pages.length}</strong> new pages found in Run B.</p>` : ''}
      ${d.removed_pages.length ? `<p class="small text-muted"><strong>${d.removed_pages.length}</strong> pages from Run A not found in Run B.</p>` : ''}
    `;
  }

  // ── Print report ──────────────────────────────────────────────────────────
  function printReport() {
    const score   = (qs('health-score-badge') || {}).textContent || '—';
    const pages   = (qs('summary-pages') || {}).textContent || '0';
    const issues  = (qs('summary-issues') || {}).textContent || '—';
    const project = (qs('summary-project') || {}).textContent || '—';
    const domain  = (qs('summary-domain') || {}).textContent || '—';

    const queueEl   = qs('action-queue-container');
    const issueRows = queueEl ? queueEl.innerHTML : '';

    const html = `<!DOCTYPE html><html><head><title>SEO Audit Report — ${domain}</title>
    <style>
      body{font-family:Arial,sans-serif;max-width:900px;margin:40px auto;color:#111;font-size:13px;}
      h1{font-size:22px;border-bottom:2px solid #2563eb;padding-bottom:8px;}
      h2{font-size:16px;margin-top:24px;color:#2563eb;}
      .meta{color:#666;font-size:12px;margin-bottom:20px;}
      .cards{display:flex;gap:16px;margin-bottom:20px;}
      .card{border:1px solid #e2e8f0;border-radius:8px;padding:12px 20px;flex:1;text-align:center;}
      .score{font-size:32px;font-weight:bold;}
      table{width:100%;border-collapse:collapse;margin-top:8px;}
      th,td{text-align:left;padding:6px 8px;border-bottom:1px solid #e2e8f0;font-size:12px;}
      th{background:#f8fafc;font-weight:600;}
      .badge{padding:2px 8px;border-radius:4px;font-size:11px;}
      @media print{body{margin:20px;}}
    </style></head><body>
    <h1>SEO Audit Report</h1>
    <p class="meta">Project: <strong>${project}</strong> &nbsp;|&nbsp; Domain: <strong>${domain}</strong> &nbsp;|&nbsp; Generated: ${new Date().toLocaleString()}</p>
    <div class="cards">
      <div class="card"><div style="font-size:11px;color:#666;">Site Health</div><div class="score">${score}</div><div style="font-size:11px;color:#666;">/100</div></div>
      <div class="card"><div style="font-size:11px;color:#666;">Total Pages</div><div class="score">${pages}</div></div>
      <div class="card"><div style="font-size:11px;color:#666;">Issue Types</div><div class="score">${issues}</div></div>
    </div>
    <h2>Top Issues</h2>
    <table><thead><tr><th>Issue</th><th>Priority</th><th>Type</th><th>URLs Affected</th></tr></thead><tbody>${issueRows}</tbody></table>
    </body></html>`;

    const w = window.open('', '_blank');
    w.document.write(html);
    w.document.close();
    w.print();
  }

  // ── New section data loader ───────────────────────────────────────────────
  async function loadNewSections() {
    await loadHealthScore();
    await loadDuplicateGroups();
    await loadCanonicalAudit();
    await loadCrawlBudget();
    await loadSchemaValidation();
  }

  // ── Extended bindEvents additions ─────────────────────────────────────────
  function bindExtendedEvents() {
    // ── All Pages preset buttons ──────────────────────────────────────────
    bindPresetButtons();

    // ── On-Page tab: search + quick filters ──────────────────────────────
    const tabSearch = qs('tab-search');
    if (tabSearch) {
      tabSearch.addEventListener('input', () => {
        tabState.search = tabSearch.value;
        reRenderCurrentTab();
      });
    }

    // ── Technical tab: search + quick filter checkboxes ──────────────────
    ['tech-search','tech-qf-noindex','tech-qf-redirect','tech-qf-slow','tech-qf-errors'].forEach(id => {
      const el = qs(id);
      if (el) el.addEventListener(el.type === 'checkbox' ? 'change' : 'input', () => reRenderTechTab());
    });

    // ── Full audit table: real-time filters ──────────────────────────────
    buildFullAuditHead();

    const faInputs = ['fa-search', 'fa-status', 'fa-type', 'fa-indexability'];
    faInputs.forEach((id) => {
      const el = qs(id);
      if (el) el.addEventListener('input', () => applyAuditFiltersAndRender());
      if (el) el.addEventListener('change', () => applyAuditFiltersAndRender());
    });

    const faLimit = qs('fa-limit');
    if (faLimit) faLimit.addEventListener('change', () => wrap(loadFullAudit()));

    // Column header sort (full audit table)
    const faHead = qs('full-audit-thead');
    if (faHead) {
      faHead.addEventListener('click', (e) => {
        const th = e.target.closest('th[data-fa-col]');
        if (!th) return;
        const col = th.dataset.faCol;
        if (auditState.sortCol === col) {
          auditState.sortDir = auditState.sortDir === 'asc' ? 'desc' : 'asc';
        } else {
          auditState.sortCol = col;
          auditState.sortDir = 'asc';
        }
        // Update header indicators
        faHead.querySelectorAll('th').forEach(t => t.classList.remove('text-primary'));
        th.classList.add('text-primary');
        applyAuditFiltersAndRender();
      });
    }

    // ── Technical sub-tabs ────────────────────────────────────────────────
    const techTabBar = qs('tech-tabs');
    if (techTabBar) {
      techTabBar.querySelectorAll('button[data-tech-tab]').forEach((btn) => {
        btn.addEventListener('click', () => {
          techTabBar.querySelectorAll('button').forEach(b => b.classList.remove('active'));
          btn.classList.add('active');
          state.techOffset = 0;
          wrap(loadTechTab(btn.dataset.techTab));
        });
      });
    }

    // Override tech pagination to use activeTechTab
    const techPrev = qs('tech-prev'); const techNext = qs('tech-next');
    if (techPrev) {
      techPrev.replaceWith(techPrev.cloneNode(true)); // remove old handlers
      qs('tech-prev').addEventListener('click', () => {
        state.techOffset = Math.max(0, state.techOffset - state.techLimit);
        wrap(loadTechTab(activeTechTab));
      });
    }
    if (techNext) {
      techNext.replaceWith(techNext.cloneNode(true));
      qs('tech-next').addEventListener('click', () => {
        state.techOffset += state.techLimit;
        wrap(loadTechTab(activeTechTab));
      });
    }
    // Heading pagination
    const hp = qs('heading-prev'); const hn = qs('heading-next');
    if (hp) hp.addEventListener('click', () => { headingOffset = Math.max(0, headingOffset - 25); wrap(loadHeadingAudit()); });
    if (hn) hn.addEventListener('click', () => { headingOffset += 25; wrap(loadHeadingAudit()); });

    // Link equity pagination
    const lp = qs('link-prev'); const ln = qs('link-next');
    if (lp) lp.addEventListener('click', () => { linkOffset = Math.max(0, linkOffset - 25); wrap(loadLinkEquity()); });
    if (ln) ln.addEventListener('click', () => { linkOffset += 25; wrap(loadLinkEquity()); });

    // CWV run
    const cwvBtn = qs('cwv-run-btn');
    if (cwvBtn) cwvBtn.addEventListener('click', () => wrap(loadCwvManual()));

    // Comparison
    const cmpBtn = qs('compare-btn');
    if (cmpBtn) cmpBtn.addEventListener('click', () => wrap(runCrawlComparison()));

    // Print report
    const printBtn = qs('print-report-btn');
    if (printBtn) printBtn.addEventListener('click', printReport);

    // SERP preview — triggered from main table rows via delegated click
    document.addEventListener('click', (e) => {
      const serpBtn = e.target.closest('.serp-preview-btn');
      if (!serpBtn) return;
      e.preventDefault();
      showSerpPreview(
        serpBtn.dataset.title   || '',
        serpBtn.dataset.meta    || '',
        serpBtn.dataset.url     || '',
      );
    });

    // Populate comparison selects whenever previous runs are loaded
    const origLoadPreviousRuns = loadPreviousRuns;
    // mirror previous-run-id options into compare selects
    const mirrorRunsToCompare = () => {
      const src = qs('previous-run-id');
      if (!src) return;
      ['compare-run-a', 'compare-run-b'].forEach((id) => {
        const sel = qs(id);
        if (!sel) return;
        const cur = sel.value;
        sel.innerHTML = `<option value="">— select run —</option>`;
        Array.from(src.options).forEach((o) => {
          if (!o.value) return;
          const opt = document.createElement('option');
          opt.value = o.value; opt.textContent = o.textContent;
          if (o.value === cur) opt.selected = true;
          sel.appendChild(opt);
        });
      });
    };

    // Patch loadPreviousRuns to also sync compare selects
    const prevRunSelect = qs('previous-run-id');
    if (prevRunSelect) {
      const observer = new MutationObserver(mirrorRunsToCompare);
      observer.observe(prevRunSelect, { childList: true });
    }

    // Load new sections when entering those section tabs
    const sectionTabs2 = qs('section-tabs');
    if (sectionTabs2) {
      sectionTabs2.querySelectorAll('button[data-section]').forEach((btn) => {
        btn.addEventListener('click', () => {
          const sec = btn.getAttribute('data-section');
          if (sec === 'performance')      { wrap(loadPerformance()); return; }
          if (!state.crawlRunId) return;
          if (sec === 'allpages')         { if (!auditState.rawRows.length) wrap(loadFullAudit()); }
          if (sec === 'content-analysis') { wrap(loadDuplicateGroups()); wrap(loadCanonicalAudit()); wrap(loadHeadingAudit()); }
          if (sec === 'links')            { wrap(loadLinkEquity()); wrap(loadCrawlBudget()); }
          if (sec === 'schema')           { wrap(loadSchemaValidation()); }
          if (sec === 'sitelevel')        { wrap(loadSiteHealth()); }
        });
      });
    }

    // Categories
    bindCategoryModal();

    // Site-level re-scan button
    const reScanBtn = qs('site-rescan-btn');
    if (reScanBtn) reScanBtn.addEventListener('click', () => wrap(loadSiteHealth()));
  }

  // ── Performance Section (Items: GA4 + GSC + GBP) ──────────────────────────

  let perfDays        = 7;
  let perfTabActive   = 'overview';
  let perfDataCache   = {};   // keyed by `${projectId}:${perfDays}`

  function perfKpi(label, value, sub, color) {
    return `<div class="col-6 col-md-3">
      <div class="card border-0 bg-light p-3 text-center">
        <div class="text-muted small mb-1">${label}</div>
        <div class="fs-4 fw-bold ${color ? 'text-' + color : ''}">${value}</div>
        ${sub ? `<div class="text-muted" style="font-size:.7rem;">${sub}</div>` : ''}
      </div>
    </div>`;
  }

  function renderPerfConnBar(status) {
    const bar = qs('perf-connection-bar');
    if (!bar) return;

    const settingsUrl = base.replace('/Audit/public', '/Content') +
      `/projects/${state.projectId}/edit#tab-integrations`;

    const missing = [];
    if (!status?.ga4?.connected)  missing.push('GA4');
    if (!status?.gsc?.connected)  missing.push('Search Console');
    if (!status?.gbp?.connected)  missing.push('Business Profile');

    if (!missing.length) { bar.innerHTML = ''; return; }

    bar.innerHTML = `<div class="alert alert-info py-2 small d-flex align-items-center gap-2 mb-0">
      <i class="bi bi-plug text-primary flex-shrink-0"></i>
      <span><strong>${missing.join(', ')}</strong> not connected for this project.
      Performance data will be incomplete.</span>
      <a href="${settingsUrl}" class="btn btn-sm btn-outline-primary py-0 ms-auto flex-shrink-0">Connect →</a>
    </div>`;
  }

  function renderPerfOverview(data, status) {
    const kpiEl = qs('perf-overview-kpis');
    const empty = qs('perf-overview-empty');
    if (!kpiEl) return;

    const ga4   = data.overview  || {};
    const gsc   = data.gscSummary?.summary || {};
    const trend = data.trend?.trend || [];

    // Clear empty state
    if (empty) empty.style.display = 'none';

    const chips = [];
    if (!ga4.error && ga4.sessions !== undefined) {
      chips.push(perfKpi('Sessions', (ga4.sessions||0).toLocaleString('en-IN'), `${perfDays}d`, ''));
      chips.push(perfKpi('Users',    (ga4.users||0).toLocaleString('en-IN'), '', ''));
      chips.push(perfKpi('Page Views',(ga4.page_views||0).toLocaleString('en-IN'), '', ''));
      chips.push(perfKpi('Bounce Rate', (ga4.bounce_rate||0) + '%', '', ga4.bounce_rate > 60 ? 'warning' : 'success'));
    } else {
      chips.push(perfKpi('Sessions', '—', 'Connect GA4', 'secondary'));
      chips.push(perfKpi('Users',    '—', '', 'secondary'));
      chips.push(perfKpi('Page Views', '—', '', 'secondary'));
      chips.push(perfKpi('Bounce Rate', '—', '', 'secondary'));
    }
    if (!gsc.error && gsc.clicks !== undefined) {
      chips.push(perfKpi('Clicks',       (gsc.clicks||0).toLocaleString('en-IN'), 'GSC', ''));
      chips.push(perfKpi('Impressions',  (gsc.impressions||0).toLocaleString('en-IN'), 'GSC', ''));
      chips.push(perfKpi('Avg CTR',      (gsc.ctr||0) + '%', 'GSC', ''));
      chips.push(perfKpi('Avg Position', (gsc.position||0).toFixed(1), 'GSC', ''));
    } else {
      chips.push(perfKpi('Clicks',       '—', 'Connect GSC', 'secondary'));
      chips.push(perfKpi('Impressions',  '—', '', 'secondary'));
      chips.push(perfKpi('Avg CTR',      '—', '', 'secondary'));
      chips.push(perfKpi('Avg Position', '—', '', 'secondary'));
    }

    kpiEl.innerHTML = chips.join('');

    // Trend sparkline (simple inline bar chart without Chart.js dependency)
    const trendEl  = qs('perf-sessions-chart');
    const trendEmp = qs('perf-trend-empty');
    if (trend.length > 0 && trendEl) {
      const max = Math.max(...trend.map(d => d.sessions), 1);
      const barW = Math.floor(100 / trend.length);
      trendEl.style.display = 'none';
      if (trendEmp) {
        trendEmp.innerHTML = `<div class="d-flex align-items-end gap-px" style="height:80px;">
          ${trend.map(d => {
            const h = Math.max(2, Math.round((d.sessions / max) * 80));
            return `<div style="flex:1;height:${h}px;background:#6366f1;border-radius:2px 2px 0 0;opacity:.8;"
                        title="${d.date}: ${d.sessions.toLocaleString()} sessions"></div>`;
          }).join('')}
        </div>
        <div class="text-muted mt-1" style="font-size:.7rem;">${trend[0]?.date} → ${trend[trend.length-1]?.date}</div>`;
      }
    }

    // GSC summary mini card
    const gscEl = qs('perf-gsc-summary');
    if (gscEl && !gsc.error && gsc.clicks !== undefined) {
      gscEl.innerHTML = `<div class="row g-2">
        <div class="col-6"><div class="p-2 bg-light rounded text-center">
          <div class="fw-semibold">${(gsc.clicks||0).toLocaleString()}</div><div class="text-muted" style="font-size:.7rem;">Clicks</div>
        </div></div>
        <div class="col-6"><div class="p-2 bg-light rounded text-center">
          <div class="fw-semibold">${(gsc.impressions||0).toLocaleString()}</div><div class="text-muted" style="font-size:.7rem;">Impressions</div>
        </div></div>
        <div class="col-6"><div class="p-2 bg-light rounded text-center">
          <div class="fw-semibold">${(gsc.ctr||0)}%</div><div class="text-muted" style="font-size:.7rem;">CTR</div>
        </div></div>
        <div class="col-6"><div class="p-2 bg-light rounded text-center">
          <div class="fw-semibold">${(gsc.position||0).toFixed(1)}</div><div class="text-muted" style="font-size:.7rem;">Avg Position</div>
        </div></div>
      </div>`;
    } else {
      if (gscEl) gscEl.innerHTML = '<div class="text-muted small">Connect Search Console to see GSC data.</div>';
    }
  }

  function renderPerfTopPages(pages) {
    const tbody = qs('perf-pages-tbody');
    if (!tbody) return;
    if (!pages?.length) {
      tbody.innerHTML = '<tr><td colspan="6" class="text-muted text-center py-4">No page data available. Connect GA4 for this project.</td></tr>';
      return;
    }
    tbody.innerHTML = pages.map(p => `
      <tr>
        <td class="small text-truncate" style="max-width:280px;" title="${escapeHtml(p.path)}">${escapeHtml(p.path)}</td>
        <td class="text-end small">${(p.sessions||0).toLocaleString()}</td>
        <td class="text-end small">${p.clicks   != null ? p.clicks.toLocaleString()   : '<span class="text-muted">—</span>'}</td>
        <td class="text-end small">${p.impressions != null ? p.impressions.toLocaleString() : '<span class="text-muted">—</span>'}</td>
        <td class="text-end small">${p.ctr      != null ? p.ctr + '%'   : '<span class="text-muted">—</span>'}</td>
        <td class="text-end small">${p.position != null ? (+p.position).toFixed(1) : '<span class="text-muted">—</span>'}</td>
      </tr>`).join('');
  }

  function renderPerfSearch(searchData) {
    const tbody = qs('perf-queries-tbody');
    if (!tbody) return;
    const queries = searchData?.queries || [];
    if (!queries.length) {
      tbody.innerHTML = '<tr><td colspan="5" class="text-muted text-center py-4">No search data. Connect Search Console for this project.</td></tr>';
      return;
    }
    tbody.innerHTML = queries.map(q => `
      <tr>
        <td class="small">${escapeHtml(q.query)}</td>
        <td class="text-end small fw-semibold">${q.clicks.toLocaleString()}</td>
        <td class="text-end small">${q.impressions.toLocaleString()}</td>
        <td class="text-end small">${q.ctr}%</td>
        <td class="text-end small">${q.position.toFixed(1)}</td>
      </tr>`).join('');
  }

  function renderPerfLocal(localData) {
    const el = qs('perf-local-content');
    if (!el) return;
    if (!localData || localData.error) {
      const settingsUrl = base.replace('/Audit/public', '/Content') + `/projects/${state.projectId}/edit#tab-integrations`;
      el.innerHTML = `<div class="text-center py-5 text-muted">
        <i class="bi bi-geo-alt" style="font-size:2.5rem;opacity:.3;"></i>
        <div class="mt-2 small">Connect Google Business Profile to see local performance data.</div>
        <a href="${settingsUrl}" class="btn btn-sm btn-outline-primary mt-2">Connect GBP →</a>
      </div>`;
      return;
    }
    el.innerHTML = `<div class="row g-3">
      <div class="col-md-4">
        <div class="card border-0 bg-light p-3 text-center">
          <div class="text-muted small">Maps Impressions</div>
          <div class="fs-3 fw-bold">${(localData.impressions_maps||0).toLocaleString()}</div>
          <div class="text-muted" style="font-size:.7rem;">Last ${localData.period_days||30} days</div>
        </div>
      </div>
    </div>`;
  }

  function switchPerfPane(tab) {
    ['overview','top-pages','search','local'].forEach((t) => {
      const pane = qs('perf-pane-' + t);
      if (pane) pane.classList.toggle('d-none', t !== tab);
    });
    qs('perf-tabs')?.querySelectorAll('[data-perf-tab]').forEach((btn) => {
      btn.classList.toggle('active', btn.dataset.perfTab === tab);
    });
    perfTabActive = tab;
  }

  async function loadPerformance(force = false) {
    if (!state.projectId) return;

    const cacheKey = `${state.projectId}:${perfDays}`;
    if (!force && perfDataCache[cacheKey]) {
      const cached = perfDataCache[cacheKey];
      renderPerfConnBar(cached.oauthStatus);
      renderPerfOverview(cached, cached.oauthStatus);
      renderPerfTopPages(cached.pages?.pages);
      renderPerfSearch(cached.searchData);
      renderPerfLocal(cached.localData);
      return;
    }

    // Parallel fetch all performance data + oauth status
    const [oauthStatus, overview, trend, pages, searchData, localData] = await Promise.all([
      api(`/api/projects/${state.projectId}/oauth/status`).catch(() => ({})),
      api(`/api/projects/${state.projectId}/performance/overview?days=${perfDays}`).catch(() => ({ error: 'fetch_failed' })),
      api(`/api/projects/${state.projectId}/performance/trend?days=${perfDays}`).catch(() => ({ trend: [] })),
      api(`/api/projects/${state.projectId}/performance/top-pages?days=${perfDays}`).catch(() => ({ pages: [] })),
      api(`/api/projects/${state.projectId}/performance/search?days=${perfDays}`).catch(() => ({})),
      api(`/api/projects/${state.projectId}/performance/local?days=${perfDays}`).catch(() => null),
    ]);

    const cached = { oauthStatus, overview, trend, pages, searchData, localData, gscSummary: searchData };
    perfDataCache[cacheKey] = cached;

    renderPerfConnBar(oauthStatus);
    renderPerfOverview(cached, oauthStatus);
    renderPerfTopPages(pages?.pages);
    renderPerfSearch(searchData);
    renderPerfLocal(localData);
  }

  function bindPerfEvents() {
    // Sub-tab switching
    qs('perf-tabs')?.addEventListener('click', (e) => {
      const btn = e.target.closest('[data-perf-tab]');
      if (btn) switchPerfPane(btn.dataset.perfTab);
    });

    // Days selector
    qs('perf-days-selector')?.addEventListener('click', (e) => {
      const btn = e.target.closest('[data-days]');
      if (!btn) return;
      qs('perf-days-selector').querySelectorAll('.btn').forEach(b => b.classList.remove('active'));
      btn.classList.add('active');
      perfDays = +btn.dataset.days;
      if (state.projectId) wrap(loadPerformance(true));
    });

    // Refresh
    qs('perf-refresh-btn')?.addEventListener('click', () => {
      if (state.projectId) {
        const key = `${state.projectId}:${perfDays}`;
        delete perfDataCache[key];
        wrap(loadPerformance(true));
      }
    });
  }

  bindPerfEvents();

  // ── Boot ──────────────────────────────────────────────────────────────────
  bindEvents();
  bindExtendedEvents();
  setInterval(() => wrap(pollLatest()), 5000);

  // Auto-select project from URL ?p= param or saved project state for default landing
  (function autoSelectProjectFromUrl() {
    const params = new URLSearchParams(window.location.search);
    let urlP     = params.get('p');
    const urlSec = params.get('section');

    // Switch to the requested section first (before data loads)
    if (urlSec) {
      const secBtn = document.querySelector(`[data-section="${urlSec}"]`);
      if (secBtn) {
        secBtn.click();
      }
    }

    if (!urlP && window.savitProject) {
      const stored = window.savitProject.get();
      if (stored && stored.id) {
        urlP = String(stored.id);
      }
    }

    if (!urlP) return;
    const sel = qs('project-id');
    if (!sel) return;

    const match = Array.from(sel.options).find((o) => o.value === urlP);
    if (match) {
      sel.value = urlP;
      if (window.savitProject) {
        window.savitProject.set(urlP, match.textContent.trim(), match.dataset.domain || '');
      }
      sel.dispatchEvent(new Event('change'));
    }
  })();

  // ── AUDIT DECK (replaces old client/internal view toggle) ────────────────

  // Issue → section tab mapping for click-to-navigate
  const issueTabMap = {
    'Duplicate title tag':           'onpage',
    'Missing title tag':             'onpage',
    'Title too long (>60 chars)':    'onpage',
    'Title too short (<30 chars)':   'onpage',
    'Missing meta description':      'onpage',
    'Duplicate meta description':    'onpage',
    'Meta description too long (>160)': 'onpage',
    'Missing H1':                    'onpage',
    'Multiple H1 tags':              'onpage',
    'Non-self canonical (review)':   'onpage',
    'Duplicate content':             'content-analysis',
    'Thin content (<300 words)':     'content-analysis',
    'Low readability (Flesch <50)':  'content-analysis',
    'Images missing alt text':       'images',
    'Client/Server errors':          'technical',
    'Noindex pages':                 'technical',
    'Slow page (>3s response)':      'cwv',
  };

  // Classify each issue as on-page or technical
  const ON_PAGE_RE = /title|meta|description|heading|h1|canonical|readability|schema|content|image|alt/i;

  function isOnPage(label) { return ON_PAGE_RE.test(label); }

  // Navigate to the section that shows this issue type
  function navigateToIssueTab(label) {
    const sec = issueTabMap[label] || (isOnPage(label) ? 'onpage' : 'technical');
    const btn = document.querySelector(`[data-section="${sec}"]`);
    if (btn) { btn.click(); }
  }

  // ── Render the audit deck ─────────────────────────────────────────────────
  async function buildAuditDeck() {
    const deck = document.getElementById('audit-deck');
    if (!deck) return;
    if (!state.projectId || !state.crawlRunId) {
      deck.innerHTML = `<div class="text-center py-5 text-muted">
        <i class="bi bi-search fs-1 d-block mb-3 opacity-25"></i>
        <div class="fw-semibold">Select a project and load a crawl run</div>
        <div class="small mt-1">The audit deck will appear here automatically.</div>
      </div>`;
      return;
    }
    if (deckBuilt && deckRunId === state.crawlRunId) return;

    deck.innerHTML = `<div class="text-center py-5 text-muted">
      <div class="spinner-border mb-2"></div><div class="small">Building audit deck…</div>
    </div>`;

    try {
      const domain = document.getElementById('summary-domain')?.textContent?.trim()
                  || document.querySelector('#project-id option:checked')?.dataset.domain
                  || '';

      const [healthData, summaryData, siteData, issueData, psData] = await Promise.all([
        api(`/api/projects/${state.projectId}/health-score?crawl_run_id=${state.crawlRunId}`),
        api(`/api/projects/${state.projectId}/website-level-summary?crawl_run_id=${state.crawlRunId}`),
        api(`/api/projects/${state.projectId}/site-health?crawl_run_id=${state.crawlRunId}&domain=${encodeURIComponent(domain)}`),
        api(`/api/projects/${state.projectId}/website-audit?crawl_run_id=${state.crawlRunId}&limit=300`),
        api(`/api/projects/${state.projectId}/page-speed/stored?crawl_run_id=${state.crawlRunId}`).catch(() => ({})),
      ]);

      const issues   = issueData.issue_summary || issueSummaryCache || [];
      const score    = healthData.score  || 0;
      const total    = healthData.total_pages || 1;

      // Separate On-Page vs Technical
      const onPageIssues  = issues.filter(i => isOnPage(i.label));
      const techIssues    = issues.filter(i => !isOnPage(i.label));

      const critCount = issues.filter(i => i.severity === 'critical' || i.severity === 'high').reduce((s,i) => s+i.count, 0);
      const medCount  = issues.filter(i => i.severity === 'medium').reduce((s,i) => s+i.count, 0);

      // Compute category scores
      function calcScore(cats) {
        if (!cats.length) return 100;
        const w = {critical:10, high:7, medium:4, low:1};
        const penalty = cats.reduce((s,i) => s + (w[i.severity]||0)*Math.min(i.count, total), 0);
        return Math.max(0, Math.round(100 - (penalty / (total * 10)) * 100));
      }
      const onPageScore = calcScore(onPageIssues);
      const techScore   = calcScore(techIssues);

      const healthLabel = score >= 80 ? 'Excellent' : score >= 60 ? 'Good' : score >= 40 ? 'Needs Work' : 'Critical';
      const healthColor = score >= 80 ? '#22c55e'   : score >= 60 ? '#f59e0b': score >= 40 ? '#f97316'  : '#ef4444';

      // PageSpeed
      const psDesktop = psData.desktop || null;
      const psMobile  = psData.mobile  || null;
      const hasPsData = !!(psDesktop || psMobile);

      // Site health data
      const sitmapTotal = siteData?.sitemap?.total_urls      || 0;
      const notCrawled  = siteData?.sitemap?.not_crawled?.length || 0;
      const indexed     = siteData?.indexed_pages || total;
      const schemaCnt   = siteData?.schema_pages_count || 0;
      const robots      = siteData?.robots;
      const sitemap     = siteData?.sitemap;

      // Status breakdown
      const statDist    = summaryData.status_distribution || [];
      const errors404   = (statDist.find(s => s.status_code === 404) || {}).cnt || 0;
      const errors5xx   = statDist.filter(s => s.status_code >= 500).reduce((s,r) => s+(r.cnt||0), 0);
      const redirects   = statDist.filter(s => s.status_code >= 300 && s.status_code < 400).reduce((s,r) => s+(r.cnt||0), 0);
      const orphanCount = summaryData.orphan_sample?.length || 0;

      const slowIssue = issues.find(i => i.label.includes('Slow'));
      const slowPct   = slowIssue ? Math.round((1 - slowIssue.count/total)*100) : 90;
      const indexPct  = Math.min(100, Math.round(indexed/total*100));
      const schemaPct = schemaCnt ? Math.round(schemaCnt/total*100) : 0;
      const https     = domain.startsWith('https');

      const date  = new Date().toLocaleDateString('en-IN', {day:'numeric',month:'long',year:'numeric'});
      const projName = document.querySelector('#project-id option:checked')?.textContent?.trim() || domain;

      function escH(s) { const d=document.createElement('div');d.appendChild(document.createTextNode(String(s||'')));return d.innerHTML; }

      function scoreBar(label, pct, icon='') {
        const p   = Math.max(0, Math.min(100, Math.round(pct)));
        const col = p >= 80 ? '#22c55e' : p >= 60 ? '#f59e0b' : '#ef4444';
        return `<div class="mb-3">
          <div class="d-flex justify-content-between mb-1">
            <span class="small fw-medium">${icon} ${label}</span>
            <span class="fw-bold small" style="color:${col};">${p}%</span>
          </div>
          <div style="height:8px;background:#f1f5f9;border-radius:4px;overflow:hidden;">
            <div style="height:100%;width:${p}%;background:${col};border-radius:4px;"></div>
          </div>
        </div>`;
      }

      function issueRow(issue) {
        const biz = {
          'Duplicate title tag':           'Duplicate page titles — Google can\'t decide which page to rank',
          'Missing title tag':             'Pages with no title — invisible to search engines',
          'Title too long (>60 chars)':    'Page titles cut off in Google search results',
          'Title too short (<30 chars)':   'Page titles too short to rank for meaningful keywords',
          'Missing meta description':      'No Google preview text — fewer clicks from search results',
          'Duplicate meta description':    'Duplicate preview text — missed keyword targeting',
          'Meta description too long (>160)': 'Preview text truncated in Google search results',
          'Missing H1':                    'No main heading — Google lacks a clear page topic signal',
          'Multiple H1 tags':              'Multiple main headings — confuses search engine structure',
          'Client/Server errors':          'Broken pages (4xx/5xx) — visitors and Google hit dead ends',
          'Noindex pages':                 'Pages hidden from Google — verify these should be excluded',
          'Thin content (<300 words)':     'Too little content — pages unlikely to rank for any keyword',
          'Slow page (>3s response)':      'Slow loading — visitors leave before content appears',
          'Low readability (Flesch <50)':  'Hard-to-read content — may signal low quality to Google',
          'Non-self canonical (review)':   'Link authority redirected to other pages — review intent',
          'Images missing alt text':       'Images without descriptions — missed SEO and accessibility',
        };
        const label  = biz[issue.label] || issue.label;
        const sev    = issue.severity;
        const cols   = {critical:'#ef4444', high:'#f97316', medium:'#f59e0b', low:'#94a3b8'};
        const icons  = {critical:'🔴', high:'🟠', medium:'🟡', low:'⚪'};
        const tab    = issueTabMap[issue.label] || (isOnPage(issue.label) ? 'onpage' : 'technical');
        const c      = cols[sev]||'#94a3b8';
        return `<div class="pitch-issue-row" data-tab="${tab}" style="border-left:3px solid ${c};" title="Click to see affected pages →">
          <span class="pitch-issue-label">${icons[sev]||''} ${escH(label)}</span>
          <div class="d-flex align-items-center gap-2">
            <span class="pitch-count-badge" style="background:${c}22;color:${c};">${issue.count} page${issue.count>1?'s':''}</span>
            <i class="bi bi-arrow-right" style="color:#94a3b8;font-size:.7rem;"></i>
          </div>
        </div>`;
      }

      function psScoreBlock(label, data, icon) {
        if (!data) return `<div class="card border-0 bg-light p-3 text-center h-100">
          <div class="small text-muted mb-2">${icon} ${label}</div>
          <div class="text-muted small">No data — run test below</div></div>`;
        const sc = data.score||0;
        const c  = sc >= 90 ? '#22c55e' : sc >= 50 ? '#f59e0b' : '#ef4444';
        const lm = data.lab_metrics || {};
        const fmtMs = (ms) => ms != null ? (ms >= 1000 ? (ms/1000).toFixed(1)+'s' : ms+'ms') : '—';
        const fmtS  = (s)  => s  != null ? s.toFixed(2)+'s' : '—';
        return `<div class="card border-0 shadow-sm p-3 h-100">
          <div class="d-flex align-items-center justify-content-between mb-2">
            <span class="fw-semibold small">${icon} ${label}</span>
            <span style="font-size:1.4rem;font-weight:800;color:${c};">${sc}</span>
          </div>
          <div class="ps-metric"><span class="text-muted">FCP</span> <strong>${fmtMs(lm.fcp_ms)}</strong></div>
          <div class="ps-metric"><span class="text-muted">LCP</span> <strong>${fmtMs(lm.lcp_ms)}</strong></div>
          <div class="ps-metric"><span class="text-muted">TBT</span> <strong>${fmtMs(lm.tbt_ms)}</strong></div>
          <div class="ps-metric"><span class="text-muted">CLS</span> <strong>${lm.cls||'—'}</strong></div>
        </div>`;
      }

      // Quick wins
      const effortLow = new Set([
        'Duplicate title tag','Missing title tag','Title too long (>60 chars)','Title too short (<30 chars)',
        'Missing meta description','Duplicate meta description','Meta description too long (>160)',
        'Missing H1','Client/Server errors','Noindex pages','Images missing alt text',
      ]);
      const qw = [...issues.filter(i => i.severity === 'critical' || i.severity === 'high'), ...issues.filter(i => i.severity === 'medium')]
        .filter(i => effortLow.has(i.label))
        .slice(0, 5);

      // Build HTML
      deck.innerHTML = `
      <!-- Cover -->
      <div class="d-flex align-items-center justify-content-between flex-wrap gap-3 p-4 mb-4"
           style="background:linear-gradient(135deg,#0f172a 0%,#1e293b 100%);border-radius:14px;color:#fff;">
        <div class="d-flex align-items-center gap-3">
          <div style="width:46px;height:46px;background:#2563eb;border-radius:12px;display:flex;align-items:center;justify-content:center;font-weight:900;font-size:1.2rem;flex-shrink:0;">S</div>
          <div>
            <div style="font-size:1.2rem;font-weight:800;">${escH(domain||projName)}</div>
            <div style="color:#94a3b8;font-size:.78rem;">Website SEO Audit &nbsp;·&nbsp; ${date} &nbsp;·&nbsp; Prepared by Savit SEO</div>
          </div>
        </div>
        <button onclick="window.print()" class="btn btn-sm" style="background:rgba(255,255,255,.1);color:#e2e8f0;border:1px solid rgba(255,255,255,.25);">
          <i class="bi bi-file-earmark-pdf me-1"></i>Export PDF
        </button>
      </div>

      <!-- Health Ring + Counts -->
      <div class="row g-3 mb-4 align-items-stretch">
        <div class="col-md-3">
          <div class="card border-0 shadow-sm h-100 text-center p-3">
            <div style="position:relative;display:inline-block;margin:0 auto 8px;">
              <canvas id="deck-health-ring" width="150" height="150"></canvas>
              <div style="position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);text-align:center;line-height:1.1;">
                <div style="font-size:1.8rem;font-weight:900;color:${healthColor};">${score}</div>
                <div style="font-size:.6rem;color:#94a3b8;text-transform:uppercase;">/100</div>
              </div>
            </div>
            <div style="font-weight:700;">${healthLabel}</div>
            <div style="color:#64748b;font-size:.72rem;">Overall Health Score</div>
          </div>
        </div>
        <div class="col-md-9">
          <div class="row g-3 mb-2">
            <div class="col-4">
              <div class="card border-0 shadow-sm text-center p-3" style="background:#fef2f2;">
                <div style="font-size:1.9rem;font-weight:800;color:#ef4444;">${critCount}</div>
                <div style="font-size:.72rem;font-weight:600;color:#991b1b;">Priority Issues</div>
                <div style="font-size:.65rem;color:#94a3b8;">Hurting rankings now</div>
              </div>
            </div>
            <div class="col-4">
              <div class="card border-0 shadow-sm text-center p-3" style="background:#fffbeb;">
                <div style="font-size:1.9rem;font-weight:800;color:#d97706;">${medCount}</div>
                <div style="font-size:.72rem;font-weight:600;color:#92400e;">To Improve</div>
                <div style="font-size:.65rem;color:#94a3b8;">Limiting growth</div>
              </div>
            </div>
            <div class="col-4">
              <div class="card border-0 shadow-sm text-center p-3" style="background:#f0fdf4;">
                <div style="font-size:1.9rem;font-weight:800;color:#16a34a;">${total}</div>
                <div style="font-size:.72rem;font-weight:600;color:#166534;">Pages Crawited</div>
                <div style="font-size:.65rem;color:#94a3b8;">Fully analysed</div>
              </div>
            </div>
          </div>
          <div class="card border-0 shadow-sm p-3" style="background:#f8fafc;font-size:.86rem;line-height:1.6;color:#374151;">
            ${critCount > 0
              ? `<strong>What we found:</strong> ${critCount} high-priority issue${critCount>1?'s':''} actively hurting your search rankings, plus ${medCount} medium-priority improvements limiting growth.`
              : medCount > 0
              ? `<strong>What we found:</strong> Your site has a solid foundation. ${medCount} improvements could meaningfully lift search performance.`
              : '<strong>Your site is in great shape!</strong> Only minor optimisations remain.'}
            ${sitmapTotal > 0 && notCrawled > 0 ? ` Sitemap lists <strong>${sitmapTotal}</strong> URLs but <strong>${notCrawled}</strong> weren't crawled — possible indexability gap.` : ''}
          </div>
        </div>
      </div>

      <!-- On-Page vs Technical Scores -->
      <div class="row g-3 mb-4">
        <div class="col-md-6">
          <div class="card border-0 shadow-sm h-100">
            <div class="card-header fw-semibold d-flex align-items-center justify-content-between" style="background:transparent;">
              <span><span class="deck-section-badge" style="background:#eff6ff;color:#1d4ed8;">ON-PAGE SEO</span></span>
              <span style="font-size:1.2rem;font-weight:800;color:${onPageScore>=80?'#22c55e':onPageScore>=60?'#f59e0b':'#ef4444'};">${onPageScore}%</span>
            </div>
            <div class="card-body">
              ${scoreBar('Title Tags',       onPageIssues.filter(i=>i.label.includes('title'||'Title')).reduce((s,i)=>Math.max(s,0),100) - onPageIssues.filter(i=>i.label.toLowerCase().includes('title')).reduce((s,i)=>s+(i.severity==='critical'?20:i.severity==='high'?10:5),0), '📝')}
              ${scoreBar('Meta Descriptions',100 - onPageIssues.filter(i=>i.label.toLowerCase().includes('meta')).reduce((s,i)=>s+(i.severity==='critical'?15:i.severity==='high'?8:4),0), '📋')}
              ${scoreBar('Heading Structure',100 - onPageIssues.filter(i=>i.label.toLowerCase().includes('h1')).reduce((s,i)=>s+(i.severity==='critical'?20:i.severity==='high'?12:6),0), '📌')}
              ${scoreBar('Content Quality',  100 - onPageIssues.filter(i=>i.label.toLowerCase().includes('content')||i.label.toLowerCase().includes('read')).reduce((s,i)=>s+(i.severity==='critical'?10:i.severity==='high'?5:3),0), '📄')}
              ${scoreBar('Images & Alt Text',100 - onPageIssues.filter(i=>i.label.toLowerCase().includes('image')||i.label.toLowerCase().includes('alt')).reduce((s,i)=>s+(i.count/total*30),0), '🖼️')}
            </div>
          </div>
        </div>
        <div class="col-md-6">
          <div class="card border-0 shadow-sm h-100">
            <div class="card-header fw-semibold d-flex align-items-center justify-content-between" style="background:transparent;">
              <span><span class="deck-section-badge" style="background:#fdf4ff;color:#7e22ce;">TECHNICAL SEO</span></span>
              <span style="font-size:1.2rem;font-weight:800;color:${techScore>=80?'#22c55e':techScore>=60?'#f59e0b':'#ef4444'};">${techScore}%</span>
            </div>
            <div class="card-body">
              ${scoreBar('Indexability',     indexPct,  '🔍')}
              ${scoreBar('Page Speed',       slowPct,   '⚡')}
              ${scoreBar('Schema Markup',    schemaPct, '🧩')}
              ${scoreBar('HTTPS Security',   https ? 100 : 0, '🔒')}
              ${scoreBar('Internal Linking', Math.round((1-orphanCount/total)*100), '🔗')}
            </div>
          </div>
        </div>
      </div>

      <!-- PageSpeed Scores -->
      <div class="card border-0 shadow-sm mb-4">
        <div class="card-header fw-semibold d-flex align-items-center justify-content-between" style="background:transparent;">
          <span><i class="bi bi-speedometer2 me-2 text-primary"></i>PageSpeed Insights — Homepage</span>
          ${!hasPsData ? `<button class="btn btn-sm btn-outline-primary" id="deck-run-ps-btn"><i class="bi bi-play-fill me-1"></i>Run Test</button>` : '<span class="badge bg-success-subtle text-success small">✓ Data available</span>'}
        </div>
        <div class="card-body" id="deck-ps-body">
          ${hasPsData ? `<div class="row g-3">
            <div class="col-md-6">${psScoreBlock('Desktop', psData.desktop ? {score: psData.desktop.score, lab_metrics:{fcp_ms:psData.desktop.fcp*1000,lcp_ms:psData.desktop.lcp*1000,tbt_ms:psData.desktop.tbt*1000,cls:psData.desktop.cls}} : null, '🖥️')}</div>
            <div class="col-md-6">${psScoreBlock('Mobile',  psData.mobile  ? {score: psData.mobile.score,  lab_metrics:{fcp_ms:psData.mobile.fcp*1000,lcp_ms:psData.mobile.lcp*1000,tbt_ms:psData.mobile.tbt*1000,cls:psData.mobile.cls}}  : null, '📱')}</div>
          </div>` : `<div class="text-center py-3 text-muted small">
            <i class="bi bi-speedometer2 fs-3 d-block mb-2 opacity-25"></i>
            Click "Run Test" to fetch real PageSpeed scores for the homepage.
            ${!hasPsData&&domain ? `<div class="mt-1">URL: <strong>${escH(domain)}</strong></div>` : ''}
          </div>`}
        </div>
      </div>

      <!-- On-Page Issues -->
      ${onPageIssues.length ? `<div class="card border-0 shadow-sm mb-4">
        <div class="card-header fw-semibold" style="background:transparent;">
          <span class="deck-section-badge" style="background:#eff6ff;color:#1d4ed8;">ON-PAGE SEO</span>
          Issues Found — Click any row to see affected pages
        </div>
        <div class="card-body py-2">
          ${onPageIssues.sort((a,b)=>{const o={critical:0,high:1,medium:2,low:3};return(o[a.severity]||3)-(o[b.severity]||3);}).map(issueRow).join('')}
        </div>
      </div>` : ''}

      <!-- Technical Issues -->
      ${techIssues.length ? `<div class="card border-0 shadow-sm mb-4">
        <div class="card-header fw-semibold" style="background:transparent;">
          <span class="deck-section-badge" style="background:#fdf4ff;color:#7e22ce;">TECHNICAL SEO</span>
          Issues Found — Click any row to see affected pages
        </div>
        <div class="card-body py-2">
          ${techIssues.sort((a,b)=>{const o={critical:0,high:1,medium:2,low:3};return(o[a.severity]||3)-(o[b.severity]||3);}).map(issueRow).join('')}
        </div>
      </div>` : ''}

      <!-- Quick Wins -->
      ${qw.length ? `<div class="card border-0 shadow-sm mb-4">
        <div class="card-header fw-semibold" style="background:transparent;">
          <i class="bi bi-lightning-fill me-2 text-warning"></i>Quick Wins — Fixes That Take Under an Hour
        </div>
        <div class="card-body py-1">
          ${qw.map(i => {
            const tab = issueTabMap[i.label]||(isOnPage(i.label)?'onpage':'technical');
            const biz = {'Duplicate title tag':'Duplicate page titles','Missing title tag':'Pages missing title tag','Client/Server errors':'Broken pages (404/5xx)','Missing meta description':'Missing meta descriptions','Images missing alt text':'Images missing alt text','Missing H1':'Pages missing H1 heading'};
            const c = {critical:'#ef4444',high:'#f97316',medium:'#f59e0b'}[i.severity]||'#94a3b8';
            return `<div class="d-flex align-items-center gap-3 py-2 border-bottom deck-qw-row" data-tab="${tab}" style="cursor:pointer;" title="Click to view →">
              <i class="bi bi-check2-circle text-success" style="font-size:1.1rem;flex-shrink:0;"></i>
              <div class="flex-grow-1">
                <div class="fw-medium small">${escH(biz[i.label]||i.label)}</div>
                <div class="text-muted" style="font-size:.7rem;">${i.count} page${i.count>1?'s':''} affected</div>
              </div>
              <span class="badge" style="background:${c}22;color:${c};font-size:.65rem;">High Impact</span>
              <i class="bi bi-arrow-right text-muted" style="font-size:.75rem;"></i>
            </div>`;
          }).join('')}
        </div>
      </div>` : ''}

      <!-- Crawlability -->
      <div class="card border-0 shadow-sm mb-4">
        <div class="card-header fw-semibold" style="background:transparent;">
          <i class="bi bi-search me-2 text-primary"></i>Crawlability &amp; Indexability
        </div>
        <div class="card-body">
          <div class="row g-3 mb-3 text-center">
            <div class="col-6 col-md-3">
              <div style="font-size:1.5rem;font-weight:700;">${total}</div>
              <div style="font-size:.72rem;color:#64748b;">Pages Crawled</div>
            </div>
            <div class="col-6 col-md-3">
              <div style="font-size:1.5rem;font-weight:700;color:${errors404>0?'#ef4444':'#22c55e'};">${errors404}</div>
              <div style="font-size:.72rem;color:#64748b;">404 Errors</div>
            </div>
            <div class="col-6 col-md-3">
              <div style="font-size:1.5rem;font-weight:700;">${redirects}</div>
              <div style="font-size:.72rem;color:#64748b;">Redirects</div>
            </div>
            <div class="col-6 col-md-3">
              <div style="font-size:1.5rem;font-weight:700;">${robots?.found ? '✅' : '❌'}</div>
              <div style="font-size:.72rem;color:#64748b;">Robots.txt</div>
            </div>
          </div>
          ${notCrawled > 0
            ? `<div class="p-3" style="background:#fff7ed;border-radius:8px;border-left:3px solid #f97316;">
                <div class="fw-semibold small" style="color:#c2410c;">⚠️ ${notCrawled} sitemap URLs not reached by this crawl</div>
                <div style="font-size:.72rem;color:#64748b;margin-top:3px;">These pages may have no internal links. Add links from your main navigation to help Google discover them.</div>
              </div>`
            : `<div class="p-3" style="background:#f0fdf4;border-radius:8px;border-left:3px solid #22c55e;">
                <span class="fw-semibold small" style="color:#166534;">✅ All sitemap URLs are reachable</span>
              </div>`}
        </div>
      </div>

      <!-- CTA -->
      <div class="p-4 text-center text-white mb-2" style="background:linear-gradient(135deg,#1d4ed8 0%,#2563eb 100%);border-radius:14px;">
        <div style="font-size:1.15rem;font-weight:700;margin-bottom:6px;">Ready to fix these issues and grow organic traffic?</div>
        <div style="color:rgba(255,255,255,.8);font-size:.85rem;margin-bottom:18px;">Our team resolves all critical issues within 30 days as part of an SEO engagement.</div>
        <div class="d-flex gap-3 justify-content-center flex-wrap">
          <a href="mailto:Sigmaseopod@ethinos.com" class="btn" style="background:#fff;color:#1d4ed8;font-weight:600;padding:8px 22px;border-radius:8px;">
            <i class="bi bi-envelope me-1"></i>Get in Touch
          </a>
          <button onclick="window.print()" class="btn" style="background:rgba(255,255,255,.15);color:#fff;border:1px solid rgba(255,255,255,.3);padding:8px 22px;border-radius:8px;">
            <i class="bi bi-file-earmark-pdf me-1"></i>Export PDF
          </button>
        </div>
      </div>`;

      deckBuilt = true;
      deckRunId = state.crawlRunId;

      // Draw health ring
      if (window.Chart) {
        if (deckChart) deckChart.destroy();
        const ctx = document.getElementById('deck-health-ring')?.getContext('2d');
        if (ctx) {
          deckChart = new Chart(ctx, {
            type: 'doughnut',
            data: { datasets: [{ data: [score, 100-score], backgroundColor: [healthColor, '#e2e8f0'], borderWidth: 0 }] },
            options: { cutout: '76%', responsive: false, animation: { duration: 700 }, plugins: { legend: { display: false }, tooltip: { enabled: false } } },
          });
        }
      }

      // Wire up issue row clicks → navigate to section tab
      deck.querySelectorAll('.pitch-issue-row, .deck-qw-row').forEach(row => {
        row.addEventListener('click', () => {
          const tab = row.dataset.tab;
          if (tab) {
            const btn = document.querySelector(`[data-section="${tab}"]`);
            if (btn) btn.click();
          }
        });
      });

      // Run PageSpeed button
      document.getElementById('deck-run-ps-btn')?.addEventListener('click', async () => {
        const url = domain || document.querySelector('#project-id option:checked')?.dataset.domain || '';
        if (!url) return;
        const btn = document.getElementById('deck-run-ps-btn');
        const body = document.getElementById('deck-ps-body');
        btn.disabled = true;
        btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Running…';
        if (body) body.innerHTML = '<div class="text-center py-3 text-muted"><div class="spinner-border mb-2"></div><div class="small">Fetching PageSpeed data for ' + escH(url) + '…<br><span class="text-muted" style="font-size:.7rem;">This takes ~15 seconds</span></div></div>';
        try {
          const psResult = await api(`/api/projects/${state.projectId}/page-speed?url=${encodeURIComponent(url)}&crawl_run_id=${state.crawlRunId}`);
          // Rebuild deck with new data
          deckBuilt = false;
          await buildAuditDeck();
          // Also refresh CWV tab stored results
          await loadCwvStored();
        } catch(e) {
          if (body) body.innerHTML = `<div class="alert alert-danger small">${e.message}</div>`;
        }
      });

    } catch (err) {
      deck.innerHTML = `<div class="alert alert-danger"><i class="bi bi-exclamation-triangle me-2"></i>Failed to build audit deck: ${escH(err.message)}</div>`;
    }
  }

  // ── Auto-trigger deck when crawl run loads ───────────────────────────────
  const _origUpdateMeta = updateProjectSummaryMetadata;
  async function refreshAuditDeck() {
    if (state.projectId && state.crawlRunId) {
      await buildAuditDeck();
    }
  }

  // Rebuild deck when run changes
  document.getElementById('project-id')?.addEventListener('change', () => {
    deckBuilt = false;
    deckRunId = null;
  });

  // ── PageSpeed: load stored results into CWV tab ──────────────────────────
  async function loadCwvStored() {
    const container = document.getElementById('cwv-stored-results');
    if (!container || !state.projectId || !state.crawlRunId) {
      if (container) container.innerHTML = '<p class="text-muted small">Select a project and crawl run to see PageSpeed results.</p>';
      return;
    }
    container.innerHTML = '<div class="text-center py-3 text-muted small"><div class="spinner-border spinner-border-sm mb-1"></div><br>Loading stored results…</div>';
    const data = await api(`/api/projects/${state.projectId}/page-speed/stored?crawl_run_id=${state.crawlRunId}`).catch(() => ({}));
    if (!data.desktop && !data.mobile) {
      container.innerHTML = `<div class="alert alert-info small">
        <i class="bi bi-info-circle me-2"></i>No PageSpeed data for this crawl run yet.
        Enter the homepage URL below and click "Run Test" — results will be saved automatically.
      </div>`;
      return;
    }
    const fmtMs = ms => ms != null ? (ms >= 1000 ? (ms/1000).toFixed(2)+'s' : Math.round(ms)+'ms') : '—';
    const scoreCard = (label, d, icon) => {
      if (!d) return '';
      const sc = d.score||0;
      const c  = sc>=90?'#22c55e':sc>=50?'#f59e0b':'#ef4444';
      const ops= d.opportunities ? (typeof d.opportunities==='string'?JSON.parse(d.opportunities):d.opportunities) : [];
      return `<div class="card border-0 shadow-sm mb-3">
        <div class="card-header fw-semibold d-flex justify-content-between" style="background:transparent;">
          <span>${icon} ${label} PageSpeed</span>
          <span style="font-size:1.3rem;font-weight:800;color:${c};">${sc} <span style="font-size:.7rem;color:#94a3b8;font-weight:400;">/100</span></span>
        </div>
        <div class="card-body">
          <div class="row g-2 mb-3">
            ${[
              ['FCP', fmtMs(d.fcp*1000)], ['LCP', fmtMs(d.lcp*1000)],
              ['TBT', fmtMs(d.tbt*1000)], ['CLS', d.cls!=null?d.cls.toFixed(4):'—'],
              ['Speed Index', fmtMs(d.si*1000)], ['TTI', fmtMs(d.tti*1000)],
            ].map(([k,v]) => `<div class="col-6 col-md-2 text-center">
              <div class="card p-2 bg-light border-0">
                <div style="font-size:.65rem;color:#64748b;text-transform:uppercase;">${k}</div>
                <div class="fw-semibold small">${v}</div>
              </div>
            </div>`).join('')}
          </div>
          ${ops.length ? `<div class="fw-semibold small mb-2">Top Opportunities</div>
            ${ops.slice(0,4).map(o => `<div class="d-flex justify-content-between py-1 border-bottom small">
              <span>${o.title||o.id||''}</span>
              ${o.savings_ms>0?`<span class="text-success">−${o.savings_ms}ms</span>`:''}
            </div>`).join('')}` : ''}
        </div>
      </div>`;
    };
    container.innerHTML =
      scoreCard('Desktop', data.desktop, '🖥️') +
      scoreCard('Mobile',  data.mobile,  '📱') +
      `<div class="text-muted small">Fetched: ${data.desktop?.fetched_at||data.mobile?.fetched_at||'—'}</div>`;
  }

  // Enhanced CWV run — pre-fills URL, stores result, refreshes panel
  async function loadCwvManual() {
    // Pre-fill URL with project homepage
    const urlEl = qs('cwv-url');
    if (urlEl && !urlEl.value) {
      const domain = document.querySelector('#project-id option:checked')?.dataset.domain || '';
      if (domain) urlEl.value = domain.startsWith('http') ? domain : 'https://' + domain;
    }
    // Run manual test (original logic reused, but store=true via crawl_run_id)
    const url   = qs('cwv-url')?.value?.trim() || '';
    const runId = state.crawlRunId;
    if (!url || !state.projectId) return;
    const body = qs('cwv-body');
    if (body) body.innerHTML = '<div class="spinner-border spinner-border-sm me-2"></div>Fetching PageSpeed data…<br><small class="text-muted">This typically takes 10–20 seconds</small>';
    const endpoint = runId
      ? `/api/projects/${state.projectId}/page-speed?url=${encodeURIComponent(url)}&crawl_run_id=${runId}`
      : `/api/projects/${state.projectId}/page-speed?url=${encodeURIComponent(url)}`;
    const data = await api(endpoint);
    if (!body) return;
    const renderScore = (s) => {
      const c = s >= 90 ? 'success' : s >= 50 ? 'warning' : 'danger';
      return `<div class="text-center"><div class="fs-3 fw-bold text-${c}">${s}</div><div class="small text-muted">/100</div></div>`;
    };
    const sc = data.mobile?.scores || data.scores || {};
    const lm = data.mobile?.lab_metrics || data.lab_metrics || {};
    const opps = data.mobile?.opportunities || data.opportunities || [];
    body.innerHTML = `
      <div class="row g-3 mb-3">
        ${['performance','seo','accessibility','best_practices'].map((k) => `
          <div class="col-md-3"><div class="card p-3 text-center border-0 bg-light"><div class="small text-muted mb-1">${k.replace('_',' ').replace(/\b\w/g,c=>c.toUpperCase())}</div>${renderScore(sc[k]??0)}</div></div>
        `).join('')}
      </div>
      <div class="row g-3 mb-3">
        <div class="col-md-2"><div class="card p-3 bg-light border-0 text-center"><div class="small text-muted">FCP</div><div class="fw-semibold">${lm.fcp_ms!=null?(lm.fcp_ms/1000).toFixed(2)+'s':'—'}</div></div></div>
        <div class="col-md-2"><div class="card p-3 bg-light border-0 text-center"><div class="small text-muted">LCP</div><div class="fw-semibold">${lm.lcp_ms!=null?(lm.lcp_ms/1000).toFixed(2)+'s':'—'}</div></div></div>
        <div class="col-md-2"><div class="card p-3 bg-light border-0 text-center"><div class="small text-muted">TBT</div><div class="fw-semibold">${lm.tbt_ms!=null?lm.tbt_ms+'ms':'—'}</div></div></div>
        <div class="col-md-2"><div class="card p-3 bg-light border-0 text-center"><div class="small text-muted">CLS</div><div class="fw-semibold">${lm.cls??'—'}</div></div></div>
        <div class="col-md-2"><div class="card p-3 bg-light border-0 text-center"><div class="small text-muted">Speed Index</div><div class="fw-semibold">${lm.si_ms!=null?(lm.si_ms/1000).toFixed(2)+'s':'—'}</div></div></div>
        <div class="col-md-2"><div class="card p-3 bg-light border-0 text-center"><div class="small text-muted">TTI</div><div class="fw-semibold">${lm.tti_ms!=null?(lm.tti_ms/1000).toFixed(2)+'s':'—'}</div></div></div>
      </div>
      ${opps.length?`<h6 class="fw-semibold mb-2">Top Opportunities</h6><ul class="list-group list-group-flush">${opps.map((o)=>`<li class="list-group-item py-2 small"><strong>${escapeHtml(o.title)}</strong>${o.savings_ms>0?` <span class="text-success ms-1">−${o.savings_ms}ms</span>`:''}</li>`).join('')}</ul>`:''}
    `;
    // Refresh stored panel after saving
    if (runId) await loadCwvStored();
  }

  // Pre-fill CWV URL when switching to that tab
  document.addEventListener('click', e => {
    if (e.target.closest('[data-section="cwv"]')) {
      setTimeout(async () => {
        const urlEl = qs('cwv-url');
        if (urlEl && !urlEl.value) {
          const domain = document.querySelector('#project-id option:checked')?.dataset.domain || '';
          if (domain) urlEl.value = domain.startsWith('http') ? domain : 'https://' + domain;
        }
        await loadCwvStored();
      }, 50);
    }
  });

  // deckBuilt is reset in loadAll() — no extra hook needed

  // Hook into the section switcher to trigger deck build
  document.getElementById('section-tabs')?.addEventListener('click', async e => {
    if (e.target.dataset.section === 'summary') {
      await buildAuditDeck();
    }
  });

  // ── [OLD buildClientView removed — replaced by buildAuditDeck above] ──────
  // GUARD: prevent old dead-code from running
  if (false) { // eslint-disable-line
  document.getElementById('btn-client-view')?.addEventListener('click', async () => {
    document.getElementById('internal-view').classList.add('d-none');
    document.getElementById('client-view').classList.remove('d-none');
    document.getElementById('client-view-actions').classList.remove('d-none');
    document.getElementById('btn-client-view').classList.replace('btn-outline-secondary', 'btn-dark');
    document.getElementById('btn-client-view').classList.add('active');
    document.getElementById('btn-internal-view').classList.replace('btn-dark', 'btn-outline-secondary');
    document.getElementById('btn-internal-view').classList.remove('active');

    if (!state.projectId || !state.crawlRunId) {
      document.getElementById('client-view').innerHTML =
        '<div class="alert alert-warning"><i class="bi bi-exclamation-triangle me-2"></i>Please select a project and load a crawl run first.</div>';
      return;
    }
    if (!clientViewBuilt || document.getElementById('client-view').dataset.runId !== String(state.crawlRunId)) {
      await buildClientView();
    }
  });

  document.getElementById('btn-internal-view')?.addEventListener('click', () => {
    document.getElementById('client-view').classList.add('d-none');
    document.getElementById('client-view-actions').classList.add('d-none');
    document.getElementById('internal-view').classList.remove('d-none');
    document.getElementById('btn-internal-view').classList.replace('btn-outline-secondary', 'btn-dark');
    document.getElementById('btn-internal-view').classList.add('active');
    document.getElementById('btn-client-view').classList.replace('btn-dark', 'btn-outline-secondary');
    document.getElementById('btn-client-view').classList.remove('active');
  });

  async function buildClientView() {
    const cv = document.getElementById('client-view');
    cv.innerHTML = '<div class="text-center py-5 text-muted"><div class="spinner-border mb-2"></div><div>Building client report…</div></div>';

    try {
      const domain = document.getElementById('summary-domain')?.textContent?.trim() || '';

      const [healthData, summaryData, siteData, issueData] = await Promise.all([
        api(`/api/projects/${state.projectId}/health-score?crawl_run_id=${state.crawlRunId}`),
        api(`/api/projects/${state.projectId}/website-level-summary?crawl_run_id=${state.crawlRunId}`),
        api(`/api/projects/${state.projectId}/site-health?crawl_run_id=${state.crawlRunId}&domain=${encodeURIComponent(domain)}`),
        api(`/api/projects/${state.projectId}/website-audit?crawl_run_id=${state.crawlRunId}&limit=300`),
      ]);

      const issues = issueData.issue_summary || issueSummaryCache || [];
      const score  = healthData.score  || 0;
      const total  = healthData.total_pages || 1;

      const biz = {
        'Duplicate title tag':           'Duplicate page titles — Google can\'t decide which page to rank',
        'Missing title tag':             'Pages with no title — completely invisible to search engines',
        'Title too long (>60 chars)':    'Page titles cut off in Google — less compelling in search results',
        'Title too short (<30 chars)':   'Page titles too short — not descriptive enough to rank',
        'Missing meta description':      'No Google preview text — fewer clicks from search results',
        'Duplicate meta description':    'Duplicate preview text — missed keyword targeting opportunity',
        'Meta description too long (>160)': 'Preview text gets truncated in Google search results',
        'Missing H1':                    'No main heading — Google lacks a clear signal for page topic',
        'Multiple H1 tags':              'Multiple main headings — confuses search engine page structure',
        'Client/Server errors':          'Broken pages (4xx/5xx) — visitors and Google hit dead ends',
        'Noindex pages':                 'Pages hidden from Google — verify these should be excluded',
        'Thin content (<300 words)':     'Thin content — pages unlikely to rank for any keyword',
        'Slow page (>3s response)':      'Slow loading pages — visitors abandon before content loads',
        'Low readability (Flesch <50)':  'Hard-to-read content — may signal low quality to Google',
        'Non-self canonical (review)':   'Link authority redirected to other pages — review canonical setup',
        'Images missing alt text':       'Images without descriptions — missed SEO and accessibility signal',
      };

      const effort = {
        'Duplicate title tag':        { effort:'Low',    impact:'High' },
        'Missing title tag':          { effort:'Low',    impact:'Critical' },
        'Title too long (>60 chars)': { effort:'Low',    impact:'Medium' },
        'Title too short (<30 chars)':{ effort:'Low',    impact:'Medium' },
        'Missing meta description':   { effort:'Low',    impact:'High' },
        'Duplicate meta description': { effort:'Low',    impact:'Medium' },
        'Meta description too long (>160)':{ effort:'Low', impact:'Medium' },
        'Missing H1':                 { effort:'Low',    impact:'High' },
        'Client/Server errors':       { effort:'Low',    impact:'Critical' },
        'Noindex pages':              { effort:'Low',    impact:'Critical' },
        'Images missing alt text':    { effort:'Low',    impact:'Medium' },
        'Thin content (<300 words)':  { effort:'High',   impact:'High' },
        'Slow page (>3s response)':   { effort:'Medium', impact:'High' },
        'Low readability (Flesch <50)':{ effort:'High',  impact:'Medium' },
      };

      const critical = issues.filter(i => i.severity === 'critical');
      const high     = issues.filter(i => i.severity === 'high');
      const medium   = issues.filter(i => i.severity === 'medium');
      const critCount = [...critical,...high].reduce((s,i) => s + i.count, 0);
      const medCount  = medium.reduce((s,i) => s + i.count, 0);

      const healthLabel = score >= 80 ? 'Excellent' : score >= 60 ? 'Good' : score >= 40 ? 'Needs Work' : 'Critical Attention';
      const healthColor = score >= 80 ? '#22c55e'   : score >= 60 ? '#f59e0b'  : score >= 40 ? '#f97316'   : '#ef4444';

      const slowIssue  = issues.find(i => i.label.includes('Slow'));
      const indexed    = siteData?.indexed_pages || total;
      const schemaPct  = siteData?.schema_pages_count ? Math.round(siteData.schema_pages_count / total * 100) : 0;
      const slowPct    = slowIssue ? Math.round((1 - slowIssue.count / total) * 100) : 90;
      const indexPct   = Math.min(100, Math.round(indexed / total * 100));
      const highIss    = critical.length + high.length;
      const onPagePct  = Math.max(10, Math.round(100 - (highIss / Math.max(issues.length,1)) * 60 - (medium.length / Math.max(issues.length,1)) * 30));
      const orphanCount = summaryData.orphan_sample?.length || 0;
      const linkPct    = Math.round((1 - orphanCount / total) * 100);

      const quickWins = [...critical,...high,...medium]
        .filter(i => effort[i.label]?.effort === 'Low')
        .sort((a,b) => { const o={critical:0,high:1,medium:2,low:3}; return (o[a.severity]||3)-(o[b.severity]||3); })
        .slice(0, 5);

      const topIssues = [...critical,...high,...medium,...issues.filter(i=>i.severity==='low')].slice(0,8);

      const sitmapTotal   = siteData?.sitemap?.total_urls      || 0;
      const notCrawled    = siteData?.sitemap?.not_crawled?.length || 0;
      const robots        = siteData?.robots;
      const sitemap       = siteData?.sitemap;
      const date = new Date().toLocaleDateString('en-IN', { day:'numeric', month:'long', year:'numeric' });

      // ── Render helpers ──────────────────────────────────────────────────
      function escH(s) { const d=document.createElement('div'); d.appendChild(document.createTextNode(String(s||''))); return d.innerHTML; }

      function scoreBar(label, pct) {
        const p   = Math.max(0, Math.min(100, pct));
        const col = p >= 80 ? '#22c55e' : p >= 60 ? '#f59e0b' : '#ef4444';
        return `<div class="mb-3">
          <div class="d-flex justify-content-between mb-1">
            <span class="small fw-medium">${label}</span>
            <span class="fw-bold small" style="color:${col}">${p}%</span>
          </div>
          <div style="height:9px;background:#f1f5f9;border-radius:5px;overflow:hidden;">
            <div style="height:100%;width:${p}%;background:${col};border-radius:5px;"></div>
          </div>
        </div>`;
      }

      function issueRow(issue) {
        const label = biz[issue.label] || issue.label;
        const sev   = issue.severity;
        const cols  = {critical:'#ef4444',high:'#f97316',medium:'#f59e0b',low:'#94a3b8'};
        const icons = {critical:'🔴',high:'🟠',medium:'🟡',low:'⚪'};
        const c     = cols[sev] || '#94a3b8';
        return `<div class="pitch-issue-row" style="border-left:3px solid ${c};">
          <span class="pitch-issue-label">${icons[sev]||''} ${escH(label)}</span>
          <span class="pitch-count-badge" style="background:${c}22;color:${c};">${issue.count} page${issue.count>1?'s':''}</span>
        </div>`;
      }

      function qwRow(issue) {
        const label = biz[issue.label] || issue.label;
        const e     = effort[issue.label] || {};
        const ic    = {Critical:'#ef4444',High:'#f97316',Medium:'#f59e0b'}[e.impact] || '#94a3b8';
        return `<div class="d-flex align-items-center gap-3 py-2 border-bottom">
          <i class="bi bi-check2-circle text-success" style="font-size:1.1rem;flex-shrink:0;"></i>
          <div class="flex-grow-1">
            <div class="fw-medium small">${escH(label)}</div>
            <div class="text-muted" style="font-size:.72rem;">${issue.count} page${issue.count>1?'s':''} affected</div>
          </div>
          <div class="d-flex gap-1 flex-shrink-0">
            <span class="badge" style="background:#f1f5f9;color:#64748b;font-size:.65rem;">Effort: ${e.effort||'—'}</span>
            <span class="badge" style="background:${ic}22;color:${ic};font-size:.65rem;">Impact: ${e.impact||'—'}</span>
          </div>
        </div>`;
      }

      // ── Narrative ────────────────────────────────────────────────────────
      const narrative = critCount > 0
        ? `We found <strong>${critCount} high-priority issue${critCount>1?'s':''}</strong> actively hurting your search rankings right now, plus <strong>${medCount} medium-priority improvements</strong> that are limiting your growth potential.`
        : medCount > 0
        ? `Your site has a solid foundation. We found <strong>${medCount} improvements</strong> that could meaningfully lift your search performance.`
        : 'Your site is in great shape. Only minor optimisations remain.';

      // ── Full HTML ────────────────────────────────────────────────────────
      cv.innerHTML = `
      <!-- Cover -->
      <div class="mb-4 p-4 d-flex align-items-center justify-content-between flex-wrap gap-3"
           style="background:linear-gradient(135deg,#0f172a 0%,#1e293b 100%);border-radius:14px;color:#fff;">
        <div class="d-flex align-items-center gap-3">
          <div style="width:48px;height:48px;background:#7c3aed;border-radius:12px;display:flex;align-items:center;justify-content:center;font-weight:900;font-size:1.2rem;flex-shrink:0;">S</div>
          <div>
            <div style="font-size:1.25rem;font-weight:800;line-height:1.1;">${escH(domain || 'Website Audit')}</div>
            <div style="color:#94a3b8;font-size:.78rem;margin-top:3px;">SEO Audit Report &nbsp;·&nbsp; ${date} &nbsp;·&nbsp; Prepared by Savit SEO</div>
          </div>
        </div>
        <button onclick="window.print()" class="btn btn-sm" style="background:rgba(255,255,255,.1);color:#e2e8f0;border:1px solid rgba(255,255,255,.25);">
          <i class="bi bi-file-earmark-pdf me-1"></i>Save as PDF
        </button>
      </div>

      <!-- Health + Counts -->
      <div class="row g-3 mb-4 align-items-stretch">
        <div class="col-md-3">
          <div class="card border-0 shadow-sm h-100 text-center p-3">
            <div style="position:relative;display:inline-block;margin:0 auto 8px;">
              <canvas id="pitch-health-ring" width="160" height="160"></canvas>
              <div style="position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);text-align:center;line-height:1.1;">
                <div style="font-size:1.9rem;font-weight:900;color:${healthColor};">${score}</div>
                <div style="font-size:.6rem;color:#94a3b8;text-transform:uppercase;letter-spacing:.05em;">/100</div>
              </div>
            </div>
            <div style="font-weight:700;font-size:1rem;">${healthLabel}</div>
            <div style="color:#64748b;font-size:.73rem;">Overall Health Score</div>
          </div>
        </div>

        <div class="col-md-9">
          <div class="row g-3 mb-3">
            <div class="col-4">
              <div class="card border-0 shadow-sm text-center p-3" style="background:#fef2f2;">
                <div style="font-size:2rem;font-weight:800;color:#ef4444;">${critCount}</div>
                <div style="font-size:.72rem;font-weight:600;color:#991b1b;">Priority Issues</div>
                <div style="font-size:.66rem;color:#94a3b8;margin-top:2px;">Hurting rankings now</div>
              </div>
            </div>
            <div class="col-4">
              <div class="card border-0 shadow-sm text-center p-3" style="background:#fffbeb;">
                <div style="font-size:2rem;font-weight:800;color:#d97706;">${medCount}</div>
                <div style="font-size:.72rem;font-weight:600;color:#92400e;">To Improve</div>
                <div style="font-size:.66rem;color:#94a3b8;margin-top:2px;">Limiting growth</div>
              </div>
            </div>
            <div class="col-4">
              <div class="card border-0 shadow-sm text-center p-3" style="background:#f0fdf4;">
                <div style="font-size:2rem;font-weight:800;color:#16a34a;">${total}</div>
                <div style="font-size:.72rem;font-weight:600;color:#166534;">Pages Audited</div>
                <div style="font-size:.66rem;color:#94a3b8;margin-top:2px;">Fully crawled</div>
              </div>
            </div>
          </div>
          <div class="card border-0 shadow-sm p-3" style="background:#f8fafc;font-size:.88rem;line-height:1.65;color:#374151;">
            <strong>What we found:</strong> ${narrative}
            ${sitmapTotal > 0 && notCrawled > 0 ? ` Your sitemap lists <strong>${sitmapTotal} URLs</strong> but <strong>${notCrawled} were not crawled</strong> — possible indexability gap.` : ''}
          </div>
        </div>
      </div>

      <!-- Technical Scores + Quick Wins side by side -->
      <div class="row g-3 mb-4">
        <div class="col-md-6">
          <div class="card border-0 shadow-sm h-100">
            <div class="card-header fw-semibold" style="background:transparent;">
              <i class="bi bi-bar-chart me-2 text-primary"></i>Technical Breakdown
            </div>
            <div class="card-body">
              ${scoreBar('Indexability',     indexPct)}
              ${scoreBar('On-Page SEO',      onPagePct)}
              ${scoreBar('Page Speed',       slowPct)}
              ${scoreBar('Internal Linking', linkPct)}
              ${scoreBar('Schema Markup',    schemaPct)}
            </div>
          </div>
        </div>
        <div class="col-md-6">
          <div class="card border-0 shadow-sm h-100">
            <div class="card-header fw-semibold" style="background:transparent;">
              <i class="bi bi-lightning-fill me-2 text-warning"></i>Quick Wins — Fix in One Week
            </div>
            <div class="card-body py-1">
              ${quickWins.length
                ? quickWins.map(qwRow).join('')
                : '<div class="text-center py-4 text-muted"><i class="bi bi-check-circle-fill text-success fs-4 d-block mb-2"></i>No quick wins needed — site is well maintained!</div>'}
            </div>
          </div>
        </div>
      </div>

      <!-- All Issues -->
      <div class="card border-0 shadow-sm mb-4">
        <div class="card-header fw-semibold" style="background:transparent;">
          <i class="bi bi-exclamation-triangle me-2 text-danger"></i>Issues Affecting Your Search Rankings
        </div>
        <div class="card-body py-2">
          ${topIssues.length
            ? topIssues.map(issueRow).join('')
            : '<div class="text-center py-4 text-success"><i class="bi bi-check-circle-fill fs-4 d-block mb-1"></i>No significant issues found!</div>'}
        </div>
      </div>

      <!-- Crawlability -->
      <div class="card border-0 shadow-sm mb-4">
        <div class="card-header fw-semibold" style="background:transparent;">
          <i class="bi bi-search me-2 text-primary"></i>Crawlability &amp; Indexability
        </div>
        <div class="card-body">
          <div class="row g-3 text-center mb-3">
            <div class="col-6 col-md-3">
              <div style="font-size:1.6rem;font-weight:700;">${total}</div>
              <div style="font-size:.72rem;color:#64748b;">Pages Crawled</div>
            </div>
            <div class="col-6 col-md-3">
              <div style="font-size:1.6rem;font-weight:700;">${sitmapTotal || '—'}</div>
              <div style="font-size:.72rem;color:#64748b;">In Sitemap</div>
            </div>
            <div class="col-6 col-md-3">
              <div style="font-size:1.6rem;font-weight:700;color:${notCrawled>0?'#f97316':'#22c55e'};">${notCrawled}</div>
              <div style="font-size:.72rem;color:#64748b;">Not Yet Crawled</div>
            </div>
            <div class="col-6 col-md-3">
              <div style="font-size:1.4rem;">${robots?.found ? '✅' : '❌'}</div>
              <div style="font-size:.72rem;color:#64748b;">Robots.txt</div>
            </div>
          </div>
          ${notCrawled > 0 ? `<div class="p-3" style="background:#fff7ed;border-radius:8px;border-left:3px solid #f97316;">
            <div class="fw-semibold small" style="color:#c2410c;">⚠️ ${notCrawled} sitemap URLs not reached by this crawl</div>
            <div class="text-muted" style="font-size:.72rem;margin-top:4px;">These pages may have no internal links pointing to them. Adding links from your main pages will help Google discover and index them.</div>
          </div>` : `<div class="p-3" style="background:#f0fdf4;border-radius:8px;border-left:3px solid #22c55e;">
            <div class="fw-semibold small" style="color:#166534;">✅ All sitemap URLs are crawlable</div>
          </div>`}
        </div>
      </div>

      <!-- CTA -->
      <div class="p-4 text-center text-white mb-2" style="background:linear-gradient(135deg,#4c1d95 0%,#7c3aed 100%);border-radius:14px;">
        <div style="font-size:1.2rem;font-weight:700;margin-bottom:6px;">Ready to fix these issues and grow organic traffic?</div>
        <div style="color:rgba(255,255,255,.75);font-size:.85rem;margin-bottom:18px;">
          Our team can resolve all critical issues within 30 days as part of an SEO engagement.
        </div>
        <div class="d-flex gap-3 justify-content-center flex-wrap">
          <a href="mailto:Sigmaseopod@ethinos.com" class="btn" style="background:#fff;color:#7c3aed;font-weight:600;padding:8px 22px;border-radius:8px;">
            <i class="bi bi-envelope me-1"></i>Get in Touch
          </a>
          <button onclick="window.print()" class="btn" style="background:rgba(255,255,255,.15);color:#fff;border:1px solid rgba(255,255,255,.3);padding:8px 22px;border-radius:8px;">
            <i class="bi bi-file-earmark-pdf me-1"></i>Save as PDF
          </button>
        </div>
      </div>`;

      cv.dataset.runId = String(state.crawlRunId);
      clientViewBuilt  = true;

      // Draw health donut using Chart.js
      if (window.Chart) {
        if (pitchChart) pitchChart.destroy();
        const ctx = document.getElementById('pitch-health-ring')?.getContext('2d');
        if (ctx) {
          pitchChart = new Chart(ctx, {
            type: 'doughnut',
            data: {
              datasets: [{
                data: [score, 100 - score],
                backgroundColor: [healthColor, '#e2e8f0'],
                borderWidth: 0,
              }],
            },
            options: {
              cutout: '76%',
              responsive: false,
              animation: { duration: 800 },
              plugins: { legend: { display: false }, tooltip: { enabled: false } },
            },
          });
        }
      }

    } catch (err) {
      document.getElementById('client-view').innerHTML =
        `<div class="alert alert-danger"><i class="bi bi-exclamation-triangle me-2"></i>Failed to build client view: ${err.message}</div>`;
    }
  }

  } // end if(false) guard for old dead code

})();
