/* WPSiteBeam Scanner — Core (Scanner main component + payload builders)
   Auto-split from Scanner.jsx. MUST be loaded LAST among scanner/ files.
   Imports all tab components and shared utilities from window.*.
*/
(function () {
  'use strict';
  const { useState, useMemo, useEffect, useRef, useCallback } = React;

  /* ── Pull shared components and tab components from window ─────── */
  const ScanErrorBanner = window.ScanErrorBanner;
  const TabErrorBoundary = window.TabErrorBoundary;
  const ScanLowConfidenceCard = window.ScanLowConfidenceCard;
  const ScanAllowlistPanel = window.ScanAllowlistPanel;
  const ScanConfidenceWidget = window.ScanConfidenceWidget;
  const ScanConfidenceCompact = window.ScanConfidenceCompact;
  const ConfirmDialog = window.ConfirmDialog;
  const useConfirmDialog = window.useConfirmDialog;
  const ScanProgressBar = window.ScanProgressBar;
  const ScanEmptyState = window.ScanEmptyState;
  const ScannerBrandKitTab = window.ScannerBrandKitTab;
  const ScannerContactTab = window.ScannerContactTab;
  const ScannerImagesTab = window.ScannerImagesTab;
  const ScannerFilesTab = window.ScannerFilesTab;
  const ScannerSitemapTab = window.ScannerSitemapTab;
  const ScannerPagesTab = window.ScannerPagesTab;
  const ScannerTechTab = window.ScannerTechTab;
  const ScannerPluginsFormsTab = window.ScannerPluginsFormsTab;
  const ScannerAdaTab = window.ScannerAdaTab;
  const ScannerRecsTab = window.ScannerRecsTab;
  const ScanModeHelp = window.ScanModeHelp;

  /* ── Payload builders ─────────────────────────────────────────── */
function buildBrandKitTxt(d) {
  /* Session 9: handles both {hex, name} objects and plain hex strings for backward compat. */
  const lines = [];
  lines.push(`WP Site Beam — Brand Kit: ${d?.site || 'unknown'}`);
  lines.push(`Scanned: ${d?.scannedAt || ''}`);
  lines.push('');
  lines.push('COLORS');
  (d?.colors || []).forEach(c => {
    const hex = (typeof c === 'string') ? c : (c.hex || '');
    const name = (typeof c === 'string') ? c.toUpperCase() : (c.name || hex.toUpperCase());
    lines.push(`  ${hex}  ${name}`);
  });
  lines.push('');
  lines.push('TYPOGRAPHY');
  (d?.fonts || []).forEach(f => {
    if (typeof f === 'string') { lines.push(`  ${f}`); return; }
    const name = f.name || 'Unknown';
    const src  = f.src || f.source || '';
    const wts  = Array.isArray(f.weights) ? f.weights.join(' ') : (f.weights || '');
    const uses = f.uses || f.usage || '';
    lines.push(`  ${name} (${src}) — ${wts} — uses: ${uses}${f.isSystem ? ' [system]' : ''}`);
  });
  lines.push('');
  lines.push('CONTACT');
  const c = d?.contact || {};
  (c.emails || []).forEach(e => lines.push(`  email:  ${typeof e === 'object' ? (e.email || '') : e}`));
  (c.phones || []).forEach(p => lines.push(`  phone:  ${typeof p === 'object' ? (p.number || '') : p}`));
  if (c.address) lines.push(`  address: ${c.address}`);
  return lines.join('\n');
}

/* ── Brand Kit PDF export (client-side) ───────────────────────────────
   Generates a Canva-ready brand kit PDF using jsPDF loaded from CDN.
   Customers download this and manually upload to Canva → Brand Kit.
   Enterprise Canva API integration comes later.

   Why client-side PDF not server-side:
   - Zero Railway cost — no server work, no bandwidth
   - Instant download — no round-trip latency
   - Works offline once jsPDF is cached

   Why jsPDF not other libs:
   - Well-maintained, small (~100KB), works in all browsers
   - Supports images (for logo embedding) + custom fonts
   - CDN-loadable like JSZip pattern we already use

   Layout (matches typical brand kit style):
   Page 1 — Cover: Site name + logo + scan date
   Page 2 — Colors: Primary + secondary palette with hex + RGB
   Page 3 — Typography: Font families + usage
   Page 4 — Contact: Email, phone, address, social
*/
async function exportBrandKitPdf(data) {
  try {
    /* Load jsPDF from CDN (same lazy-load pattern as JSZip) */
    if (typeof window.jspdf === 'undefined') {
      await new Promise((resolve, reject) => {
        const script = document.createElement('script');
        script.src = 'https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.2/jspdf.umd.min.js';
        script.onload = resolve;
        script.onerror = () => reject(new Error('Failed to load PDF library'));
        document.head.appendChild(script);
      });
    }

    const { jsPDF } = window.jspdf;
    const doc = new jsPDF({ unit: 'pt', format: 'letter' });  /* 612 × 792 pt */
    const PAGE_W = 612, PAGE_H = 792;
    const MARGIN = 50;

    const site = data?.site || 'Unknown Site';
    const scanDate = data?.scannedAt || new Date().toLocaleDateString();

    /* Helper: hex → RGB triplet for jsPDF setFillColor */
    function hexToRgb(hex) {
      if (!hex) return [0, 0, 0];
      const h = hex.replace('#', '');
      if (h.length !== 6) return [0, 0, 0];
      return [
        parseInt(h.substring(0, 2), 16),
        parseInt(h.substring(2, 4), 16),
        parseInt(h.substring(4, 6), 16),
      ];
    }

    /* Helper: text brightness check — for overlaying hex labels on swatches */
    function brightness(hex) {
      const [r, g, b] = hexToRgb(hex);
      return (r * 299 + g * 587 + b * 114) / 1000;
    }

    /* ───── PAGE 1: COVER ───── */
    doc.setFillColor(15, 23, 42);  /* Slate-900 bg */
    doc.rect(0, 0, PAGE_W, PAGE_H, 'F');

    doc.setTextColor(255, 255, 255);
    doc.setFont('helvetica', 'bold');
    doc.setFontSize(36);
    doc.text('Brand Kit', MARGIN, 180);

    doc.setFont('helvetica', 'normal');
    doc.setFontSize(22);
    doc.setTextColor(6, 182, 212);  /* beam cyan */
    doc.text(site, MARGIN, 215);

    doc.setFontSize(11);
    doc.setTextColor(148, 163, 184);  /* slate-400 */
    doc.text(`Extracted ${scanDate}`, MARGIN, 240);

    /* Footer branding */
    doc.setFontSize(9);
    doc.setTextColor(100, 116, 139);
    doc.text('Generated by WPSiteBeam — Import to Canva → Brand Kit', MARGIN, PAGE_H - 40);

    /* ───── PAGE 2: COLORS ───── */
    doc.addPage();
    doc.setFillColor(255, 255, 255);
    doc.rect(0, 0, PAGE_W, PAGE_H, 'F');

    doc.setTextColor(15, 23, 42);
    doc.setFont('helvetica', 'bold');
    doc.setFontSize(24);
    doc.text('Color Palette', MARGIN, 80);

    doc.setFont('helvetica', 'normal');
    doc.setFontSize(10);
    doc.setTextColor(100, 116, 139);
    doc.text('Hex values and names extracted from site stylesheets.', MARGIN, 100);

    const colors = (data?.colors || []).filter(c => {
      const hex = typeof c === 'string' ? c : c.hex;
      return hex && /^#[0-9A-Fa-f]{6}$/.test(hex);
    });

    if (colors.length === 0) {
      doc.setTextColor(148, 163, 184);
      doc.setFontSize(11);
      doc.setFont('helvetica', 'italic');
      doc.text('No colors were extracted from this site.', MARGIN, 150);
    } else {
      /* Swatch grid — 3 columns, 6 rows max per page */
      const COLS = 3;
      const SWATCH_W = 160;
      const SWATCH_H = 100;
      const GAP = 16;
      const gridStartX = MARGIN;
      const gridStartY = 130;

      colors.slice(0, 18).forEach((color, i) => {
        const hex = typeof color === 'string' ? color : color.hex;
        const name = typeof color === 'string' ? hex.toUpperCase() : (color.name || hex.toUpperCase());
        const col = i % COLS;
        const row = Math.floor(i / COLS);
        const x = gridStartX + col * (SWATCH_W + GAP);
        const y = gridStartY + row * (SWATCH_H + GAP + 30);

        /* Swatch block */
        const [r, g, b] = hexToRgb(hex);
        doc.setFillColor(r, g, b);
        doc.rect(x, y, SWATCH_W, SWATCH_H, 'F');

        /* Hex label overlay — white or black depending on brightness */
        const textColor = brightness(hex) > 128 ? [15, 23, 42] : [255, 255, 255];
        doc.setTextColor(...textColor);
        doc.setFont('helvetica', 'bold');
        doc.setFontSize(12);
        doc.text(hex.toUpperCase(), x + 10, y + 25);

        /* Name below swatch */
        doc.setTextColor(15, 23, 42);
        doc.setFont('helvetica', 'normal');
        doc.setFontSize(9);
        doc.text(name, x, y + SWATCH_H + 14);

        /* RGB values */
        doc.setTextColor(100, 116, 139);
        doc.setFontSize(8);
        doc.text(`RGB ${r} · ${g} · ${b}`, x, y + SWATCH_H + 26);
      });
    }

    /* ───── PAGE 3: TYPOGRAPHY ───── */
    doc.addPage();
    doc.setFillColor(255, 255, 255);
    doc.rect(0, 0, PAGE_W, PAGE_H, 'F');

    doc.setTextColor(15, 23, 42);
    doc.setFont('helvetica', 'bold');
    doc.setFontSize(24);
    doc.text('Typography', MARGIN, 80);

    doc.setFont('helvetica', 'normal');
    doc.setFontSize(10);
    doc.setTextColor(100, 116, 139);
    doc.text('Font families detected on the site with usage context.', MARGIN, 100);

    const fonts = data?.fonts || [];
    if (fonts.length === 0) {
      doc.setTextColor(148, 163, 184);
      doc.setFontSize(11);
      doc.setFont('helvetica', 'italic');
      doc.text('No fonts were extracted from this site.', MARGIN, 150);
    } else {
      let y = 140;
      fonts.slice(0, 10).forEach(f => {
        const name = (typeof f === 'string') ? f : (f.name || 'Unknown');
        const src = (typeof f === 'object' && (f.src || f.source)) || '';
        const weights = (typeof f === 'object' && Array.isArray(f.weights))
          ? f.weights.join(', ')
          : (typeof f === 'object' ? f.weights || '' : '');
        const uses = (typeof f === 'object' && (f.uses || f.usage)) || '';
        const isSystem = typeof f === 'object' && f.isSystem;

        doc.setTextColor(15, 23, 42);
        doc.setFont('helvetica', 'bold');
        doc.setFontSize(16);
        doc.text(name, MARGIN, y);

        /* "System" / "Google Fonts" etc. tag */
        if (src) {
          doc.setTextColor(6, 182, 212);
          doc.setFont('helvetica', 'normal');
          doc.setFontSize(9);
          doc.text(`[${src}]`, MARGIN + doc.getTextWidth(name) + 10, y);
        }

        y += 18;
        if (weights) {
          doc.setTextColor(100, 116, 139);
          doc.setFont('helvetica', 'normal');
          doc.setFontSize(10);
          doc.text(`Weights: ${weights}`, MARGIN, y);
          y += 14;
        }
        if (uses) {
          doc.setTextColor(100, 116, 139);
          doc.setFont('helvetica', 'italic');
          doc.setFontSize(10);
          doc.text(`Used for: ${uses}`, MARGIN, y);
          y += 14;
        }

        y += 16;
        /* Divider */
        doc.setDrawColor(226, 232, 240);
        doc.setLineWidth(0.5);
        doc.line(MARGIN, y, PAGE_W - MARGIN, y);
        y += 18;

        if (y > PAGE_H - 80) return; /* overflow — stop, rest in TXT */
      });
    }

    /* ───── PAGE 4: CONTACT ───── */
    doc.addPage();
    doc.setFillColor(255, 255, 255);
    doc.rect(0, 0, PAGE_W, PAGE_H, 'F');

    doc.setTextColor(15, 23, 42);
    doc.setFont('helvetica', 'bold');
    doc.setFontSize(24);
    doc.text('Contact & Brand Assets', MARGIN, 80);

    doc.setFont('helvetica', 'normal');
    doc.setFontSize(10);
    doc.setTextColor(100, 116, 139);
    doc.text('Contact info extracted from the site.', MARGIN, 100);

    const contact = data?.contact || {};
    let cy = 140;

    function contactLine(label, value) {
      if (!value) return;
      doc.setTextColor(100, 116, 139);
      doc.setFont('helvetica', 'normal');
      doc.setFontSize(9);
      doc.text(label.toUpperCase(), MARGIN, cy);
      doc.setTextColor(15, 23, 42);
      doc.setFontSize(12);
      doc.text(String(value), MARGIN, cy + 16);
      cy += 44;
    }

    /* Emails */
    (contact.emails || []).slice(0, 5).forEach(e => {
      const addr = typeof e === 'object' ? (e.email || e.value) : e;
      const lbl = typeof e === 'object' ? (e.label || 'Email') : 'Email';
      contactLine(lbl, addr);
    });

    /* Phones */
    (contact.phones || []).slice(0, 5).forEach(p => {
      const num = typeof p === 'object' ? (p.number || p.value) : p;
      const lbl = typeof p === 'object' ? (p.label || p.type || 'Phone') : 'Phone';
      contactLine(lbl, num);
    });

    /* Address */
    if (contact.address) contactLine('Address', contact.address);

    /* Social profiles */
    const socials = contact.socials || contact.social || [];
    if (socials.length > 0) {
      cy += 10;
      doc.setTextColor(100, 116, 139);
      doc.setFont('helvetica', 'normal');
      doc.setFontSize(9);
      doc.text('SOCIAL PROFILES', MARGIN, cy);
      cy += 20;
      socials.slice(0, 8).forEach(s => {
        const name = s.name || s.n || 'Link';
        const url = s.url || s.u || '';
        const handle = s.handle || s.h || '';
        doc.setTextColor(15, 23, 42);
        doc.setFont('helvetica', 'bold');
        doc.setFontSize(11);
        doc.text(`${name}${handle ? ' @' + handle : ''}`, MARGIN, cy);
        if (url) {
          doc.setTextColor(6, 182, 212);
          doc.setFont('helvetica', 'normal');
          doc.setFontSize(9);
          doc.text(url, MARGIN, cy + 14);
        }
        cy += 32;
        if (cy > PAGE_H - 80) return;
      });
    }

    /* Footer on every page except cover */
    const pageCount = doc.internal.getNumberOfPages();
    for (let p = 2; p <= pageCount; p++) {
      doc.setPage(p);
      doc.setTextColor(148, 163, 184);
      doc.setFont('helvetica', 'normal');
      doc.setFontSize(8);
      doc.text(`${site} · Brand Kit · ${scanDate}`, MARGIN, PAGE_H - 30);
      doc.text(`Page ${p} of ${pageCount}`, PAGE_W - MARGIN, PAGE_H - 30, { align: 'right' });
    }

    /* Trigger download */
    const safeSite = site.replace(/[^a-zA-Z0-9.-]/g, '_');
    doc.save(`${safeSite}-brand-kit.pdf`);

    return { success: true };
  } catch (e) {
    if (typeof console !== 'undefined') console.error('[BrandKit PDF] Export failed:', e);
    return { success: false, error: e.message };
  }
}
function buildSitemapXml(d) {
  const urls = (d?.sitemap || []).map(n => `  <url><loc>https://${d?.site || 'example.com'}${n.p}</loc></url>`).join('\n');
  return `<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${urls}\n</urlset>`;
}
function buildContactCsv(d) {
  /* Session A: shape-safe — handles both string and object shapes for emails/phones.
     Also emits label as third column so Sales Dept / Web Master / Fax context
     is preserved in CRM imports. */
  const rows = [['type','value','label']];
  const c = d?.contact || {};
  (c.emails || []).forEach(e => {
    if (typeof e === 'object') rows.push(['email', e.email || e.value || '', e.label || 'Email']);
    else rows.push(['email', String(e), 'Email']);
  });
  (c.phones || []).forEach(p => {
    if (typeof p === 'object') rows.push(['phone', p.number || p.value || '', p.label || p.type || 'Phone']);
    else rows.push(['phone', String(p), 'Phone']);
  });
  if (c.address) rows.push(['address', c.address, 'Address']);
  (c.socials || c.social || []).forEach(s => rows.push([s.n || s.name || '', s.h || s.url || '', 'Social']));
  return rows.map(r => r.map(c => `"${String(c).replace(/"/g,'""')}"`).join(',')).join('\n');
}
function buildSeoCsv(pages) {
  const rows = [['url','title','description','keywords','h1','og','twitter','canonical','images','score']];
  (pages || []).forEach(p => rows.push([p.url, p.title||'', p.desc||'', p.kw||'', p.h1, p.og, p.twitter, p.canonical, p.imgs, p.score]));
  return rows.map(r => r.map(c => `"${String(c).replace(/"/g,'""')}"`).join(',')).join('\n');
}
function buildDnsCsv(dns) {
  const rows = [['type','host','value']];
  (dns || []).forEach(r => rows.push([r.type, r.host, r.val]));
  return rows.map(r => r.map(c => `"${String(c).replace(/"/g,'""')}"`).join(',')).join('\n');
}

/* ── Tabs ─────────────────────────────────────────────────────── */

/* Empty-state placeholder shown when a tab's data is missing.
   Used by BrandKit / Contact / Images / Maps / Icons / SEO / ADA tabs
   which Session 7 Railway backend doesn't fully populate. */

  /* ── Scanner main component ───────────────────────────────────── */
function Scanner() {
  const { useState, useMemo } = React;
  const [url, setUrl] = useState('');
  const [activeTab, setActiveTab] = useState('brand-kit');
  const [advOpen, setAdvOpen] = useState(false);
  const [helpOpen, setHelpOpen] = useState(false);
  const [modes, setModes] = useState({ brand: true, platform: false, ada: false });
  const [opts, setOpts] = useState({ multipage: true, contact: true, seo: true, maps: true, icons: true });
  const [adaOpts, setAdaOpts] = useState({ level: 'AA', taxonomy: 'auto', pdfs: true, lighthouse: true });

  /* Branded confirm dialog (replaces window.confirm).
     Returns { confirm: async fn, dialogProps: object }. We render
     <ConfirmDialog {...dialogProps}/> at the bottom of this component
     and call confirm() wherever we used to call window.confirm(). */
  const { confirm, dialogProps: confirmDialogProps } = useConfirmDialog();

  /* Lifted state for ScanConfidenceWidget toggles. The widget used to own
     these (showSignals, showChecklist) as internal state, but per UX
     feedback the toggle buttons need to render INLINE with the Actions
     toolbar (JSON, Switch to ongoing SEO, etc.) — not in their own row
     above it. Lifting state here lets the Actions row render the toggle
     buttons + pass the boolean down to the widget. */
  const [showConfidenceSignals, setShowConfidenceSignals] = useState(false);
  const [showPreMigrationChecklist, setShowPreMigrationChecklist] = useState(false);

  // ---------- Wizard step (Step 1 URL → Step 2 Configure → Step 3 Results) ----------
  const [step, setStep] = useState(1);
  const steps = [
    { n:1, lbl:'Target site',     sub:'Enter the URL to scan',       icon:'scanner' },
    { n:2, lbl:'Configure scans', sub:'Choose scan types + options', icon:'sliders' },
    { n:3, lbl:'Review Results',  sub:'Scan data + next actions',    icon:'check' },
  ];
  const hasUrl = (url || '').trim().length > 0;
  const hasMode = modes.brand || modes.platform || modes.ada;
  const canNextFromStep = step === 1 ? hasUrl : step === 2 ? hasMode : false;
  function next() { if (step === 2 && !done) { run(); return; } if (step < 3) setStep(step + 1); }
  function back() { if (step > 1) setStep(step - 1); }
  function resetWizard() { resetScan(); setStep(1); window.wpsbToast('Scan reset — start over', 'ok'); }

  /* ── Real scan via Railway /brain/scan (Session 7) ─────────────
     Replaces the old 1.4s setTimeout demo with a real API call.
     Hook handles loading, errors, plan limits, etc. */
  const scannerState = (window.WPSB_Scanner && window.WPSB_Scanner.useScannerData)
                     ? window.WPSB_Scanner.useScannerData()
                     : { data: {}, scanning: false, done: false, error: null,
                         scan: async () => {}, reset: () => {} };
  const { data, scanning, done, error, scan: triggerScan, reset: resetScan } = scannerState;

  function run() {
    const trimmed = (url || '').trim();
    if (!trimmed) { window.wpsbToast('Enter a URL first', 'warn'); return; }
    if (!modes.brand && !modes.platform && !modes.ada) { window.wpsbToast('Pick at least one scan type', 'warn'); return; }

    /* v1.3.8 patch h3: forward configurator state to the scan engine.
       Previously `modes` and `opts` were local UI state only — toggling
       them changed nothing about what got scanned. Now the toggles
       actually steer behaviour:
         opts.multipage → enables 100-page enrichment walk (forms +
                          per-page content). When OFF, only homepage
                          is enriched (faster, less bandwidth).
         opts.contact   → enables Cloudflare email decoder + JSON-LD
                          contact merge (Pass 5 in scrapeContact).
         opts.seo       → enables per-page SEO/meta extraction.
         opts.maps      → enables Google Maps / iframe embed detection
                          (Phase 2 stub today; preserved for future).
         opts.icons     → enables icon-library / Font Awesome detection.
       Modes also forwarded so the server can scope ADA / platform-only
       scans differently when those flows are wired. */
    const scanOptions = {
      modes: { ...modes },
      opts: { ...opts },
      adaOpts: { ...adaOpts },
    };

    /* Fire the real scan. On success → step 3. On error → stay on step 2.
       Defensive: triggerScan might return undefined if the scanner module
       failed to load (parse error in ScannerData.jsx, missing dependency,
       etc.). The fallback noop stub at the top of this component returns
       undefined from scan(). Without this guard, undefined.success would
       throw and the user would see a generic React error overlay. */
    triggerScan(trimmed, scanOptions).then(result => {
      if (result && result.success) {
        setActiveTab('brand-kit');
        setStep(3);
      } else if (!result && window.wpsbToast) {
        window.wpsbToast('Scanner failed to initialize — refresh the page.', 'warn');
      }
      /* Errors shown via error state in render (see ErrorBanner component) */
    }).catch(err => {
      console.warn('[WPSB] triggerScan threw:', err && err.message);
      if (window.wpsbToast) {
        window.wpsbToast('Scan failed unexpectedly — please retry.', 'warn');
      }
    });
  }


  /* Tab labels with safe counts from real data (0 when empty, real count when present) */
  const contactCount = (data?.contact?.emails?.length || 0) + (data?.contact?.phones?.length || 0);
  const filesCount   = data?.files?.items?.length || 0;
  const seoCount     = data?.seoPages?.length || 0;
  const pagesTabCount= data?.pagesList?.length || 0;
  const adaCount     = data?.ada?.totalViolations || data?.ada?.totals?.totalViolations || 0;

  const tabs = [
    /* v1.3.8 patch h7: tab order updated per user feedback. New flow
       follows user-mental-model: brand identity → who-they-are (contact)
       → what-they-have (pages, images) → how-it-works (tech/dns) →
       structure (sitemap, plugins/forms) → housekeeping (docs, ADA, AI recs). */
    { id:'brand-kit', label:'Brand Kit' },
    { id:'contact',   label:'Contact' },
    { id:'images',    label:'Images' },
    { id:'tech',      label:'Tech / DNS' },
    { id:'sitemap',   label:'Sitemap' },
    { id:'pages',     label:'Pages', badge:'NEW' },
    { id:'plugins',   label:'Plugins & Forms', badge:'NEW' },
    { id:'files',     label:'Files & Docs', badge:'NEW' },
    { id:'ada',       label:'ADA', badge:'NEW' },
    { id:'recs',      label:'AI Recs' },
  ];
  /* Removed tabs (consolidated elsewhere):
     - SEO   → per-page SEO meta already shown in Pages tab; no aggregate
               data in scan engine yet. "Switch to ongoing SEO" button on
               the Scan Overview card opens the separate SEO workspace app
               for ongoing tracking (not a scanner tab).
     - Maps  → rendered as a full-width card inside Contact tab (maps are
               location data, belongs with contact info).
     - Icons → rendered as a full-width card inside Brand Kit below Colors
               (icon libraries are visual brand assets). */

  // Build downloadable payloads once
  const payloads = useMemo(() => ({
    json: JSON.stringify(data, null, 2),
    brandKitTxt: buildBrandKitTxt(data),
    sitemapXml: buildSitemapXml(data),
    sitemapTxt: (data?.sitemap || []).map(n => `${'  '.repeat(n.d)}${n.p}`).join('\n'),
    contactCsv: buildContactCsv(data),
    /* seoCsv retained even though the dedicated SEO tab was removed —
       Pages tab may add a "Download all SEO as CSV" action in the future,
       and the build function is effectively free (runs only if accessed). */
    seoCsv: buildSeoCsv(data?.seoPages),
    dnsCsv: buildDnsCsv(data?.dns),
  }), [done]);

  return (
    <div>
      <PageHead crumb="Operations" title={<span style={{ display:'inline-flex', alignItems:'center', gap:10 }}>Site Scanner <span style={{ fontFamily:'var(--font-mono)', fontSize:'.58rem', padding:'3px 8px', borderRadius:4, background:'var(--gdim)', border:'1px solid var(--green-dim)', color:'var(--green)', letterSpacing:'.08em' }}>● LIVE v5.58</span></span>}
        sub="Scan any public URL — brand extraction runs in your browser; platform &amp; migration runs server-side; ADA accessibility audit detects WCAG 2.1 AA violations against the WPSiteBeam scanner knowledge base."
      />

      {/* (Authorized Use + Scan Snapshot notices moved to Legal/Policy repository —
           see _WPSITEBEAM-LEGAL-NOTICES.md. Those disclaimers belong in Terms of Service,
           Privacy Policy, and Disclaimer pages on wpsitebeam.io, not inline on the scanner.) */}

      {/* Wizard — merged STEP counter + stepper circles + Back/Next buttons in one card */}
      <div className="card" style={{ marginBottom: 14 }}>
        <div className="card-body" style={{ padding: 14 }}>
          {/* Top row: STEP counter on left, action buttons on right */}
          <div style={{
            display:'flex', alignItems:'center', gap:10, justifyContent:'space-between',
            marginBottom: 12, paddingBottom: 12, borderBottom:'1px solid var(--border)',
          }}>
            <div style={{ fontSize:'.72rem', color:'var(--dim)', fontFamily:'var(--font-mono)' }}>
              STEP {step} / 3 · {
                step === 1 ? (hasUrl ? `ready: ${url.trim()}` : 'enter a URL to continue')
                : step === 2 ? (hasMode ? `${[modes.brand && 'Brand', modes.platform && 'Platform', modes.ada && 'ADA'].filter(Boolean).join(' + ')} selected` : 'pick at least one scan type')
                : done ? `scan complete · ${data.pages} pages` : 'running…'
              }
            </div>
            <div style={{ display:'flex', gap:8 }}>
              {/* Back button hidden on step 1 (no previous step) */}
              {step > 1 && (
                <button className="btn btn-ghost btn-sm" onClick={back} disabled={scanning}>
                  <Icon name="chevron-left" size={13}/>Back
                </button>
              )}
              {/* Step 1's primary action is now inline with the URL input below.
                  Top-row shows Back only (hidden) on step 1 — no duplicate CTA. */}
              {step === 2 && !done && (
                <button className="btn btn-primary btn-sm" onClick={run} disabled={!hasMode || scanning} aria-busy={scanning}>
                  <Icon name={scanning ? 'activity' : 'scanner'} size={13}/>
                  {scanning ? 'Scanning…' : 'Start Scan'}
                </button>
              )}
              {step === 2 && done && (
                <button className="btn btn-primary btn-sm" onClick={() => setStep(3)}>
                  View results →
                </button>
              )}
              {step === 3 && (
                <>
                  {/* Save scan — moved from Actions row to header for quick access.
                      UPSERT on (account, site) so re-saving the same site
                      replaces the old scan. See ScannerData.jsx
                      saveScanPermanent() for endpoint + plan-limit handling. */}
                  <button className="btn btn-ghost btn-sm"
                          onClick={async () => {
                            const ok = await confirm({
                              title: `Save this scan of ${data.site}?`,
                              message: `If you've saved this site before, the new scan will replace the old one. The scan data will appear in My Scans on your dashboard.`,
                              confirmLabel: 'Save scan',
                              cancelLabel: 'Cancel',
                            });
                            if (!ok) return;
                            window.wpsbToast?.('Saving scan…', 'ok');
                            const result = await window.WPSB_Scanner?.saveScanPermanent?.(data) || { success: false, error: 'Scanner module not loaded' };
                            if (result.success) {
                              const msg = result.replaced
                                ? `Scan saved — replaced previous ${data.site} scan`
                                : `Scan saved — find it in My Scans on your dashboard`;
                              window.wpsbToast?.(msg, 'ok');
                            } else if (result.notDeployed) {
                              window.wpsbToast?.('Saved scans coming soon — endpoint not yet deployed.', 'warn');
                            } else if (result.code === 'PLAN_LIMIT') {
                              window.wpsbToast?.(`Plan limit reached (${result.used}/${result.limit} saved scans) — delete old scans or upgrade.`, 'warn');
                            } else {
                              window.wpsbToast?.(`Save failed: ${result.error || 'Unknown error'}`, 'warn');
                            }
                          }}
                          title="Save this scan to your dashboard">
                    <Icon name="download" size={13}/>Save scan
                  </button>
                  <button className="btn btn-ghost btn-sm" onClick={resetWizard}>
                    <Icon name="refresh" size={13}/>New scan
                  </button>
                </>
              )}
            </div>
          </div>

          {/* Stepper circles row */}
          <div style={{ display:'grid', gridTemplateColumns:'1fr 24px 1fr 24px 1fr', gap: 0, alignItems:'stretch' }}>
            {steps.map((sObj, i) => {
              const state = step === sObj.n ? 'active' : step > sObj.n ? 'done' : 'todo';
              const clickable = step > sObj.n || (sObj.n === 2 && hasUrl) || (sObj.n === 3 && done);
              const go = () => { if (clickable) setStep(sObj.n); };
              return (
                <React.Fragment key={sObj.n}>
                  <button
                    type="button"
                    onClick={go}
                    disabled={!clickable}
                    aria-current={state === 'active' ? 'step' : undefined}
                    style={{
                      display:'flex', alignItems:'center', gap:12, padding:'10px 12px', borderRadius:8,
                      background: state === 'active' ? 'var(--beam-dim)' : 'transparent',
                      border: '1px solid ' + (state === 'active' ? 'var(--beam-dim)' : state === 'done' ? 'var(--border)' : 'transparent'),
                      cursor: clickable ? 'pointer' : 'default', textAlign:'left', color:'inherit',
                      opacity: state === 'todo' ? .55 : 1,
                    }}>
                    <div style={{
                      width:32, height:32, borderRadius:'50%', display:'flex', alignItems:'center', justifyContent:'center',
                      background: state === 'done' ? 'var(--green, #22c55e)' : state === 'active' ? 'var(--beam)' : 'var(--surface-3)',
                      color: state === 'todo' ? 'var(--dim)' : '#000',
                      fontWeight:700, fontSize:'.82rem', flexShrink:0,
                    }}>
                      {state === 'done' ? <Icon name="check" size={14}/> : sObj.n}
                    </div>
                    <div style={{ minWidth:0 }}>
                      <div style={{ fontSize:'.82rem', fontWeight:600 }}>{sObj.lbl}</div>
                      <div style={{ fontSize:'.66rem', color:'var(--dim)' }}>{sObj.sub}</div>
                    </div>
                  </button>
                  {i < steps.length - 1 && (
                    <div style={{ display:'flex', alignItems:'center', justifyContent:'center' }}>
                      <div style={{ height: 2, width:'100%', background: step > sObj.n ? 'var(--green, #22c55e)' : 'var(--border)', transition:'background .2s ease' }}/>
                    </div>
                  )}
                </React.Fragment>
              );
            })}
          </div>

          {/* Scanning indicator — animated progress bar + rotating phase text.
               Since /brain/scan is a single non-streaming POST, we can't get real
               phase updates from the server. Instead: animate 0→90% over ~25s,
               hold at 90% if slower, snap to 100% on completion. Text rotates
               through phases that describe what the server ACTUALLY does in
               parallel (not fake specific steps). */}
          {scanning && <ScanProgressBar modes={modes} />}

          {/* Scan-complete summary — 25/75 split.
               Top row: scan title (35%) + confidence inline (65%) — single band.
               Below: labeled "Actions" section with downloads + handoffs.
               Pre-migration checklist + advisories + signal table render
               separately from this header — see ScanConfidenceWidget. */}
          {step === 3 && done && (
            <>
            <div style={{
              marginTop:12, paddingTop:12, borderTop:'1px solid var(--border)',
              display:'flex', gap:14, alignItems:'center', flexWrap:'wrap',
            }}>
              {/* LEFT 35%: Scan complete title + timestamp */}
              <div style={{ flex:'0 0 35%', minWidth:220 }}>
                <div style={{ fontSize:'.88rem', fontWeight:600, color:'var(--beam)', marginBottom:2 }}>
                  Scan complete — {data.site}
                </div>
                <div style={{ fontSize:'.7rem', color:'var(--dim)', fontFamily:'var(--font-mono)' }}>
                  {data.scannedAt}
                </div>
              </div>
              {/* RIGHT 65%: Confidence score + bar inline (rendered by widget
                  in compact mode — full breakdown still expandable below). */}
              <div style={{ flex:'1 1 0', minWidth:280 }}>
                {data?.scanConfidence && <ScanConfidenceCompact confidence={data.scanConfidence}/>}
              </div>
            </div>

            {/* ACTIONS SECTION — labeled, separated visually so the buttons
                feel like a deliberate toolbar, not a header overflow. */}
            <div style={{
              marginTop:14, paddingTop:14, borderTop:'1px solid var(--border)',
            }}>
              <div style={{
                fontSize:'.7rem', color:'var(--dim)', fontFamily:'var(--font-mono)',
                letterSpacing:'.08em', marginBottom:10,
              }}>
                ACTIONS
              </div>
              {/* Action toolbar — global scan-level exports + workflow handoffs only.
                  Tab-specific actions (Brand Kit PDF/TXT → Brand Kit tab,
                  ADA Report → ADA tab) have been moved into per-tab toolbars
                  to reduce button noise and surface the right action in context.
                  Save scan is in the top header row (next to Back / New scan).

                  Layout: natural-width inline buttons, left-aligned, consistent
                  10px gap. flex:wrap allows them to flow to a 2nd line on narrow
                  screens; rowGap maintains breathing room between wrapped lines.
                  No flex:1 stretching — that produced inconsistent button sizes
                  and looked awkward when only 4 buttons were on screen. */}
              <div style={{ display:'flex', gap:10, rowGap:10, flexWrap:'wrap', justifyContent:'flex-start', width:'100%', alignItems:'center' }}>
                {/* Confidence toggles (lifted from inside ScanConfidenceWidget
                    so they sit inline with the rest of the actions, not in
                    a separate row beneath). Render only when scan_confidence
                    has data — older scans / fallback states have nothing to
                    show. Same .btn .btn-ghost .btn-sm class so they match
                    the rest of the toolbar visually. */}
                {/* Show breakdown / Show pre-migration checklist toggles —
                    when band='low', these move INTO the consolidated
                    low-confidence card header (right side of "Scan results
                    may be incomplete · 35/100"), so we suppress them here
                    to avoid duplication. For high/medium-band scans, they
                    stay in the Actions row inline with JSON / SEO / etc. */}
                {data?.scanConfidence && data.scanConfidence.band !== 'low' && Array.isArray(data.scanConfidence.signals) && data.scanConfidence.signals.length > 0 && (
                  <button
                    className="btn btn-ghost btn-sm"
                    onClick={() => {
                      // 2026-05-18 v2 per Jordan: mutual exclusion — opening one auto-closes the other,
                      // so a maximum of one diagnostic panel is visible at a time. Keeps vertical
                      // footprint small and avoids the "two stacked walls of data" issue.
                      if (!showConfidenceSignals) setShowPreMigrationChecklist(false);
                      setShowConfidenceSignals(s => !s);
                    }}
                    aria-expanded={showConfidenceSignals}
                    aria-controls="confidence-signals"
                    title="Toggle the per-signal scan confidence breakdown"
                  >
                    {showConfidenceSignals ? 'Hide' : 'Show'} breakdown ({data.scanConfidence.signals.filter(s => s.met).length}/{data.scanConfidence.signals.length})
                  </button>
                )}
                {data?.scanConfidence && data.scanConfidence.band !== 'low' && Array.isArray(data.scanConfidence.pre_migration_checklist) && data.scanConfidence.pre_migration_checklist.length > 0 && (
                  <button
                    className="btn btn-ghost btn-sm"
                    onClick={() => {
                      // 2026-05-18 v2 per Jordan: mutual exclusion (see breakdown button above).
                      if (!showPreMigrationChecklist) setShowConfidenceSignals(false);
                      setShowPreMigrationChecklist(c => !c);
                    }}
                    aria-expanded={showPreMigrationChecklist}
                    aria-controls="pre-migration-checklist"
                    title="Toggle the pre-migration prep checklist"
                  >
                    {showPreMigrationChecklist ? 'Hide' : 'Show'} pre-migration checklist ({data.scanConfidence.pre_migration_checklist.length})
                  </button>
                )}
                <button className="btn btn-ghost btn-sm" onClick={() => window.wpsbDownload(`${data.site}-scan.json`, payloads.json, 'application/json')} title="Full scan data as JSON">
                  <Icon name="download" size={13}/>JSON
                </button>
                {/* 2026-05-20 — Brand Kit (PDF) moved from tab-brand.jsx in-tab
                    toolbar to here, the global Actions row. Reasons:
                    (1) Previously the in-tab onClick called bare
                        `exportBrandKitPdf(data)` which fell through tab-brand's
                        IIFE scope to window.exportBrandKitPdf — Pages2.jsx
                        defines an unrelated ACME-placeholder PDF function at
                        module scope which shadowed it. Result: tms-inc.us
                        scan exported a PDF full of "Acme Holdings, LLC".
                    (2) Putting export in the global Actions row keeps all
                        post-scan actions in one place — easier to discover. */}
                <button className="btn btn-ghost btn-sm"
                        onClick={async () => {
                          window.wpsbToast?.('Generating Brand Kit PDF…', 'ok');
                          const fn = window.WPSB?.Scanner?.exportBrandKitPdf;
                          if (typeof fn !== 'function') {
                            window.wpsbToast?.('PDF exporter not loaded — refresh and try again', 'err');
                            return;
                          }
                          const result = await fn(data);
                          if (result?.success) {
                            window.wpsbToast?.('Brand Kit PDF downloaded — upload to Canva → Brand Kit', 'ok');
                          } else {
                            window.wpsbToast?.(`PDF failed: ${result?.error || 'Unknown error'}`, 'warn');
                          }
                        }}
                        title="Download brand kit as PDF (ready for Canva)">
                  <Icon name="download" size={13}/>Brand Kit (PDF)
                </button>
                {/* Navigation buttons — warn before navigating away.
                    Scan data is auto-saved to sessionStorage, so user won't
                    lose anything, but we still confirm to set expectations. */}
                <button className="btn btn-ghost btn-sm"
                        onClick={async () => {
                          const ok = await confirm({
                            title: 'Open the ongoing SEO workspace?',
                            message: 'Your scan results are auto-saved for this browser tab — you can come back and the scan will still be here.',
                            confirmLabel: 'Open SEO workspace',
                            cancelLabel: 'Stay here',
                          });
                          if (!ok) return;
                          /* Handoff payload — SEO workspace reads this on mount
                             to pre-fill target site, known pages, and baseline
                             SEO metrics. Shape is documented in ScannerData.jsx
                             getScanHandoff('seo'). Safe to overwrite stale data. */
                          try {
                            const handoff = {
                              source: 'scanner',
                              timestamp: Date.now(),
                              site: data.site,
                              siteUrl: data.siteUrl,
                              pages: (data.pagesList || []).map(p => ({
                                url: p.url, title: p.title,
                                description: p.description,
                                h1: p.headings?.find?.(h => h.level === 'h1')?.text || null,
                              })),
                              perPageSeo: data.perPageSeo || [],
                              detectedPlatform: data.tech?.platform || null,
                            };
                            sessionStorage.setItem('wpsb-seo-target', JSON.stringify(handoff));
                          } catch (e) {
                            if (typeof console !== 'undefined') console.warn('[Handoff] sessionStorage write failed:', e.message);
                          }
                          if (window.WPSBD?.switchTab) {
                            window.WPSBD.switchTab('seo');
                            window.wpsbToast?.(`Opening SEO workspace — pre-filled from ${data.site}`, 'ok');
                          } else {
                            window.wpsbToast?.('SEO workspace not available yet — feature coming soon.', 'warn');
                          }
                        }}
                        title="Push scan data to ongoing SEO tracking (auto-saves current scan)">
                  <Icon name="spark" size={13}/>Switch to ongoing SEO
                </button>
                <button className="btn btn-ghost btn-sm"
                        onClick={async () => {
                          const ok = await confirm({
                            title: `Open Site Builder with ${data.site}?`,
                            message: 'Your scan results are auto-saved for this browser tab.',
                            confirmLabel: 'Open Site Builder',
                            cancelLabel: 'Stay here',
                          });
                          if (!ok) return;
                          /* Handoff payload — Site Builder reads this on mount
                             to pre-fill brand identity (logo, colors, fonts),
                             page structure, and contact info for new-site
                             template generation. */
                          try {
                            const handoff = {
                              source: 'scanner',
                              timestamp: Date.now(),
                              site: data.site,
                              siteUrl: data.siteUrl,
                              brand: {
                                logo: data.logoUrl || null,
                                colors: data.colors || [],
                                fonts: data.fonts || [],
                              },
                              contact: data.contact || {},
                              pages: (data.pagesList || []).map(p => ({
                                url: p.url,
                                label: p.label,
                                title: p.title,
                                description: p.description,
                                headingCount: p.heading_count || 0,
                                imageCount: p.image_count || 0,
                              })),
                              sitemap: data.sitemap || [],
                              imageCount: data.images || 0,
                            };
                            sessionStorage.setItem('wpsb-sitebuilder-prefill', JSON.stringify(handoff));
                          } catch (e) {
                            if (typeof console !== 'undefined') console.warn('[Handoff] sessionStorage write failed:', e.message);
                          }
                          if (window.WPSBD?.switchTab) {
                            window.WPSBD.switchTab('sitebuilder');
                            window.wpsbToast?.(`Opening Site Builder — pre-filled from ${data.site}`, 'ok');
                          } else {
                            window.wpsbToast?.('Site Builder not available yet — feature coming soon.', 'warn');
                          }
                        }}
                        title="Send brand/contact/pages to Site Builder template">
                  <Icon name="sitebuilder" size={13}/>Send to Site Builder
                </button>
                {/* Push to WordPress: requires the WPSiteBeam plugin installed
                    on the target site, with an auth'd migration-import endpoint.
                    Plugin receive endpoint not built yet — disable button. */}
                <button className="btn btn-primary btn-sm"
                        disabled
                        style={{ opacity:.5, cursor:'not-allowed' }}
                        title="Requires WPSiteBeam plugin installed on target site — integration coming in a future release">
                  <Icon name="spark" size={13}/>Push to WordPress
                  <span style={{ fontSize:'.58rem', marginLeft:4, padding:'1px 5px', borderRadius:3, background:'rgba(255,255,255,.15)', color:'#fff', letterSpacing:'.05em' }}>SOON</span>
                </button>
              </div>
            </div>
            </>
          )}

          {/* Phase 1A — Scan Confidence detail rendering (advisories + signal
              breakdown + pre-migration checklist). Compact score widget is
              already shown inline in the header above; this renders the
              expandable detail sections only (skipScore=true). */}
          {step === 3 && done && data?.scanConfidence && (
            <ScanConfidenceWidget
              confidence={data.scanConfidence}
              skipScore={true}
              wasBlocked={data.wasBlocked}
              showSignalsExternal={showConfidenceSignals}
              setShowSignalsExternal={setShowConfidenceSignals}
              showChecklistExternal={showPreMigrationChecklist}
              setShowChecklistExternal={setShowPreMigrationChecklist}
            />
          )}
        </div>
      </div>

      {/* Scan error banner — plan limit, payment, rate limit, network */}
      {error && <ScanErrorBanner error={error} onDismiss={() => resetScan()} />}

      {/* Scan box — only shown during Steps 1 & 2 (Step 3 has its own layout) */}
      {step !== 3 && (
      <div className="card">
        <div className="card-body">
          {/* URL row — Step 1. Configure Scans CTA inline with the input
              so the next action is visible right where attention lands.
              Alignment: label + input + button are all in a single flex row;
              button wraps a separate column that matches input height so it
              sits next to the input, not the helper text. On mobile (<640px),
              button wraps below full-width. */}
          {step === 1 && (
          <div>
            {/* Existing-scan banner — when user has an in-session scan (from
                a previous visit to this tab) but landed back on Step 1, surface
                that fact + give them two clear paths: resume viewing the saved
                scan, or start fresh. Without this, Step 1 looks like a blank
                slate even though their scan data is still in memory.
                Uses warning amber palette so it stands out against the dark
                background — it's a "heads up, you have unfinished work"
                notice, not informational chrome. */}
            {done && data?.site && (
              <div style={{
                marginBottom: 14, padding: '12px 14px', borderRadius: 8,
                background: 'rgba(251, 191, 36, 0.10)',
                border: '1px solid rgba(251, 191, 36, 0.45)',
                display: 'flex', gap: 14, alignItems: 'center', flexWrap: 'wrap',
              }}>
                <div style={{
                  flexShrink: 0, width: 36, height: 36, borderRadius: '50%',
                  background: 'rgba(251, 191, 36, 0.18)',
                  display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
                  color: 'var(--warn, #fbbf24)',
                }}>
                  <Icon name="alert" size={18}/>
                </div>
                <div style={{ flex: '1 1 240px', minWidth: 0 }}>
                  <div style={{ fontSize: '.84rem', fontWeight: 600, color: 'var(--text)', marginBottom: 2 }}>
                    You have a recent scan in this session
                  </div>
                  <div style={{ fontSize: '.74rem', color: 'var(--muted)' }}>
                    {data.site} · {data.scannedAt || 'just now'}
                    {data.pages != null ? ` · ${data.pages} pages` : ''}
                  </div>
                </div>
                <div style={{ display: 'flex', gap: 8, flexShrink: 0 }}>
                  <button
                    className="btn btn-primary btn-sm"
                    onClick={() => { setStep(3); setActiveTab('brand-kit'); }}
                    title="Open the results from your most recent scan"
                  >
                    <Icon name="chevron-right" size={13}/>View scan results
                  </button>
                  <button
                    className="btn btn-ghost btn-sm"
                    onClick={async () => {
                      const ok = await confirm({
                        title: `Discard saved scan of ${data.site}?`,
                        message: `You can save scans to your dashboard before starting fresh — close this and click "View scan results" first if you want to keep it.`,
                        confirmLabel: 'Discard and start new',
                        cancelLabel: 'Keep saved scan',
                        danger: true,
                      });
                      if (!ok) return;
                      resetScan();
                      setUrl('');
                      window.wpsbToast?.('Cleared — enter a new URL to scan', 'ok');
                    }}
                    title="Discard saved scan and start fresh"
                  >
                    <Icon name="refresh" size={13}/>Start new
                  </button>
                </div>
              </div>
            )}

            <div style={{ display:'flex', gap:10, flexWrap:'wrap', alignItems:'stretch' }}>
              <div className="field" style={{ flex:'1 1 320px', margin:0, display:'flex', flexDirection:'column' }}>
                <label htmlFor="scanner-url">Site URL</label>
                <input id="scanner-url" value={url} onChange={e => {
                         const newVal = e.target.value;
                         setUrl(newVal);
                         /* Auto-clear old scan when user types a different domain.
                            Prevents "see old results after entering new URL" confusion.
                            Only clear if we have old data AND the domain has changed. */
                         if (done && data?.site) {
                           try {
                             const oldDomain = data.site.replace(/^https?:\/\//, '').replace(/^www\./, '').split('/')[0].toLowerCase();
                             const newDomain = newVal.replace(/^https?:\/\//, '').replace(/^www\./, '').split('/')[0].toLowerCase();
                             if (newDomain && oldDomain && !oldDomain.includes(newDomain) && !newDomain.includes(oldDomain)) {
                               resetScan();
                             }
                           } catch (e) { /* swallow */ }
                         }
                       }}
                       onKeyDown={e => { if (e.key === 'Enter' && hasUrl) setStep(2); }}
                       placeholder="example.com   or   https://www.example.com"
                       aria-describedby="scanner-help" autoFocus/>
              </div>
              {/* Button column — matches input row by adding a spacer that
                  mirrors the label's height, so the button vertically aligns
                  with the input field. flexShrink:0 keeps the button from
                  squishing when the URL input is long. */}
              <div style={{ display:'flex', flexDirection:'column', flexShrink:0 }}>
                {/* Spacer that mirrors label height (label + its margin) */}
                <div aria-hidden="true" style={{ visibility:'hidden', fontSize:'.8rem', fontWeight:600, lineHeight:1.4, marginBottom:6 }}>&nbsp;</div>
                <button
                  className="btn btn-primary"
                  onClick={() => setStep(2)}
                  disabled={!hasUrl}
                  style={{
                    padding: '10px 18px',
                    fontSize: '.88rem',
                    whiteSpace: 'nowrap',
                    height: 'auto',
                    /* min-width keeps button from collapsing on mobile;
                       on very narrow screens (flex-wrap) it stretches full-width */
                    minWidth: 160,
                  }}
                  title={hasUrl ? 'Continue to scan configuration' : 'Enter a URL first'}
                >
                  Configure scans →
                </button>
              </div>
            </div>
            <div id="scanner-help" style={{ fontSize:'.7rem', color:'var(--dim)', marginTop:8 }}>
              Enter any public URL. We'll start with brand extraction by default; configure platform &amp; ADA in the next step.
            </div>
          </div>
          )}

          {/* Scan-type selector — Step 2 */}
          {step === 2 && (<React.Fragment>
          <div style={{ background:'var(--surface-3, rgba(255,255,255,.02))', border:'1px solid var(--border)', borderRadius:8, padding:'12px 14px' }}>
            <div style={{ display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom:10 }}>
              <div style={{ fontFamily:'var(--font-mono)', fontSize:'.66rem', color:'var(--dim)', letterSpacing:'.12em' }}>SCAN TYPE · <span style={{ color:'var(--beam)' }}>{((url||'').trim() || 'no url')}</span></div>
              <button type="button" onClick={() => setHelpOpen(true)}
                      style={{ background:'none', border:'none', color:'var(--beam)', fontSize:'.72rem', cursor:'pointer', display:'inline-flex', alignItems:'center', gap:4, padding:0 }}>
                <span style={{ fontSize:'.9rem' }}>ⓘ</span> What's included?
              </button>
            </div>

            {/* Brand Extraction row */}
            <label style={{ display:'flex', gap:12, alignItems:'flex-start', padding:'12px 14px', borderRadius:6,
                            background: modes.brand ? 'var(--green-dim)' : 'transparent',
                            border:'1px solid', borderColor: modes.brand ? 'var(--green-dim)' : 'var(--border)',
                            cursor:'pointer', marginBottom:8 }}>
              <input type="checkbox" checked={modes.brand}
                     onChange={e => setModes({...modes, brand: e.target.checked})}
                     style={{ marginTop:3, accentColor:'var(--green)' }}/>
              <div style={{ flex:1, minWidth:0 }}>
                <div style={{ display:'flex', gap:8, alignItems:'center', flexWrap:'wrap' }}>
                  <strong style={{ fontSize:'.88rem' }}>Brand Extraction</strong>
                  <span style={{ fontFamily:'var(--font-mono)', fontSize:'.58rem', padding:'2px 7px', borderRadius:4, background:'var(--gdim)', border:'1px solid var(--green-dim)', color:'var(--green)', letterSpacing:'.08em' }}>FREE</span>
                  <span style={{ fontFamily:'var(--font-mono)', fontSize:'.66rem', color:'var(--dim)' }}>~10 sec · browser-side</span>
                </div>
                <div style={{ fontSize:'.74rem', color:'var(--dim)', marginTop:4 }}>
                  Colors · fonts · logos · images · typography · favicon
                </div>
              </div>
            </label>

            {/* Platform & Migration row */}
            <label style={{ display:'flex', gap:12, alignItems:'flex-start', padding:'12px 14px', borderRadius:6,
                            background: modes.platform ? 'var(--beam-dim)' : 'transparent',
                            border:'1px solid', borderColor: modes.platform ? 'var(--beam-dim)' : 'var(--border)',
                            cursor:'pointer' }}>
              <input type="checkbox" checked={modes.platform}
                     onChange={e => setModes({...modes, platform: e.target.checked})}
                     style={{ marginTop:3, accentColor:'var(--beam)' }}/>
              <div style={{ flex:1, minWidth:0 }}>
                <div style={{ display:'flex', gap:8, alignItems:'center', flexWrap:'wrap' }}>
                  <strong style={{ fontSize:'.88rem' }}>Platform &amp; Migration</strong>
                  <span style={{ fontFamily:'var(--font-mono)', fontSize:'.58rem', padding:'2px 7px', borderRadius:4, background:'var(--beam-dim)', border:'1px solid var(--beam-dim)', color:'var(--beam)', letterSpacing:'.08em' }}>1 SCAN</span>
                  <span style={{ fontFamily:'var(--font-mono)', fontSize:'.66rem', color:'var(--dim)' }}>~30-90 sec · server-side</span>
                </div>
                <div style={{ fontSize:'.74rem', color:'var(--dim)', marginTop:4 }}>
                  Platform · version · URLs · sitemap · menus · documents · migration analysis
                </div>
              </div>
            </label>

            {/* ADA Accessibility row */}
            <label style={{ display:'flex', gap:12, alignItems:'flex-start', padding:'12px 14px', borderRadius:6,
                            background: modes.ada ? 'var(--rose-dim)' : 'transparent',
                            border:'1px solid', borderColor: modes.ada ? 'var(--rose-dim)' : 'var(--border)',
                            cursor:'pointer', marginTop:8 }}>
              <input type="checkbox" checked={modes.ada}
                     onChange={e => setModes({...modes, ada: e.target.checked})}
                     style={{ marginTop:3, accentColor:'var(--rose)' }}/>
              <div style={{ flex:1, minWidth:0 }}>
                <div style={{ display:'flex', gap:8, alignItems:'center', flexWrap:'wrap' }}>
                  <strong style={{ fontSize:'.88rem' }}>ADA Accessibility Audit</strong>
                  <span style={{ fontFamily:'var(--font-mono)', fontSize:'.58rem', padding:'2px 7px', borderRadius:4, background:'var(--rose-dim)', border:'1px solid var(--rose-dim)', color:'var(--rose)', letterSpacing:'.08em' }}>1 SCAN</span>
                  <span style={{ fontFamily:'var(--font-mono)', fontSize:'.58rem', padding:'2px 7px', borderRadius:4, background:'var(--beam-dim)', border:'1px solid var(--beam-dim)', color:'var(--beam)', letterSpacing:'.08em' }}>WCAG 2.1 AA</span>
                  <span style={{ fontFamily:'var(--font-mono)', fontSize:'.66rem', color:'var(--dim)' }}>~60-180 sec · axe-core + CD rules + Lighthouse</span>
                </div>
                <div style={{ fontSize:'.74rem', color:'var(--dim)', marginTop:4 }}>
                  Big 6 violations · 40 CD Compliance rules <span style={{ color:'var(--beam)' }}>(Starter+)</span> · color contrast · PDF labeling · heading hierarchy · ARIA · ADA Title II / III compliance report
                </div>
              </div>
            </label>
          </div>

          {/* Advanced ADA options (collapsible, shown only when ADA checked) */}
          {modes.ada && (
            <details style={{ marginTop:12, padding:'10px 14px', background:'var(--rose-dim)', border:'1px dashed var(--rose-dim)', borderRadius:6 }}>
              <summary style={{ cursor:'pointer', fontSize:'.74rem', color:'var(--rose)', userSelect:'none', listStyle:'none', display:'inline-flex', alignItems:'center', gap:6, fontWeight:600 }}>
                <Icon name="shield" size={12}/>
                ADA Audit options
              </summary>
              <div style={{ display:'grid', gridTemplateColumns:'repeat(auto-fit, minmax(220px, 1fr))', gap:14, marginTop:12 }}>
                <div>
                  <label style={{ fontSize:'.68rem', color:'var(--dim)', fontFamily:'var(--font-mono)', letterSpacing:'.08em', textTransform:'uppercase', display:'block', marginBottom:6 }}>Conformance level</label>
                  <div style={{ display:'flex', flexDirection:'column', gap:6 }}>
                    {[
                      { lvl:'A',   desc:'Minimum bar — must pass.' },
                      { lvl:'AA',  desc:'Legal standard — ADA Title II / III target.' },
                      { lvl:'AAA', desc:'Enhanced — rarely required for legal compliance.' },
                    ].map(o => (
                      <button key={o.lvl} type="button" onClick={() => setAdaOpts({...adaOpts, level:o.lvl})}
                              className={'btn btn-sm ' + (adaOpts.level === o.lvl ? 'btn-primary' : 'btn-ghost')}
                              style={{ display:'flex', alignItems:'center', gap:10, textAlign:'left', width:'100%', padding:'6px 10px' }}>
                        <span style={{ flexShrink:0, fontFamily:'var(--font-mono)', fontSize:'.72rem', fontWeight:700, minWidth:78, textAlign:'center', padding:'3px 6px', borderRadius:3, background: adaOpts.level === o.lvl ? 'rgba(0,0,0,.18)' : 'var(--surface-2)', border:'1px solid rgba(255,255,255,.06)' }}>WCAG {o.lvl}</span>
                        <span style={{ fontSize:'.66rem', color: adaOpts.level === o.lvl ? 'inherit' : 'var(--dim)', lineHeight:1.4, whiteSpace:'normal', fontWeight:400 }}>{o.desc}</span>
                      </button>
                    ))}
                  </div>
                </div>
                <div>
                  <label style={{ fontSize:'.68rem', color:'var(--dim)', fontFamily:'var(--font-mono)', letterSpacing:'.08em', textTransform:'uppercase', display:'block', marginBottom:6 }}>Site taxonomy</label>
                  <select value={adaOpts.taxonomy}
                          onChange={e => setAdaOpts({...adaOpts, taxonomy: e.target.value})}
                          style={{ width:'100%', padding:'7px 10px', background:'var(--surface-2)', border:'1px solid var(--border)', borderRadius:6, color:'var(--text)', fontSize:'.78rem' }}>
                    <option value="auto">Auto-detect from scan</option>
                    <optgroup label="Government (Title II)">
                      <option value="gov-city">City</option>
                      <option value="gov-county">County</option>
                      <option value="gov-municipal">Municipal / Other</option>
                    </optgroup>
                    <optgroup label="Business (Title III)">
                      <option value="biz-healthcare">Healthcare / FQHC</option>
                      <option value="biz-ecommerce">E-commerce</option>
                      <option value="biz-service">Service</option>
                      <option value="biz-other">Other</option>
                    </optgroup>
                    <optgroup label="Personal">
                      <option value="personal">Blog / Personal</option>
                    </optgroup>
                  </select>
                  <div style={{ fontSize:'.66rem', color:'var(--dim)', marginTop:6 }}>
                    Drives legal framing, deadline, and proposal urgency.
                  </div>
                </div>
                <div style={{ display:'flex', flexDirection:'column', gap:8 }}>
                  <label style={{ fontSize:'.68rem', color:'var(--dim)', fontFamily:'var(--font-mono)', letterSpacing:'.08em', textTransform:'uppercase', display:'block' }}>Additional checks</label>
                  <label className="opt-check">
                    <input type="checkbox" checked={adaOpts.pdfs}
                           onChange={e => setAdaOpts({...adaOpts, pdfs: e.target.checked})}/>
                    <span>Crawl linked PDFs <span style={{ color:'var(--dim)' }}>(auto-generates upsell)</span></span>
                  </label>
                  <label className="opt-check">
                    <input type="checkbox" checked={adaOpts.lighthouse}
                           onChange={e => setAdaOpts({...adaOpts, lighthouse: e.target.checked})}/>
                    <span>Lighthouse pass <span style={{ color:'var(--dim)' }}>(contrast + mobile zoom)</span></span>
                  </label>
                </div>
              </div>
              <div style={{ marginTop:10, padding:'8px 10px', background:'var(--beam-dim)', border:'1px solid var(--beam-dim)', borderRadius:4, fontSize:'.66rem', color:'var(--dim)', fontFamily:'var(--font-mono)' }}>
                Engines: axe-core v4.10 (MPL-2.0) · CD Compliance Rule Catalog v1.0 (40 production-tested rules, Starter+) · Lighthouse 12 (Apache-2.0) · Custom PDF crawler. ~57% of WCAG violations auto-detected by axe; CD rules add FQHC / civic / healthcare-specific coverage (multilingual tagging, phone link E.164, vague link text, PDF mislabeling, semantic structure). Manual review items flagged in report.
              </div>
            </details>
          )}

          {/* Advanced options (collapsible — eye-catching callout) */}
          <details open={advOpen} onToggle={e => setAdvOpen(e.target.open)}
                   style={{
                     marginTop:14,
                     background: advOpen ? 'linear-gradient(135deg, var(--beam-dim), var(--orange-dim))' : 'var(--surface-2)',
                     border: advOpen ? '1px solid var(--beam)' : '1px dashed var(--border)',
                     borderRadius:8, overflow:'hidden',
                     transition:'all .2s',
                   }}>
            <summary style={{
              cursor:'pointer', padding:'12px 14px', userSelect:'none', listStyle:'none',
              display:'flex', alignItems:'center', gap:10, outline:'none',
            }}>
              <div style={{
                width:28, height:28, borderRadius:8, flexShrink:0,
                background: advOpen ? 'var(--beam)' : 'var(--surface)',
                color: advOpen ? '#001018' : 'var(--beam)',
                border:'1px solid var(--beam)',
                display:'flex', alignItems:'center', justifyContent:'center',
                transform: advOpen ? 'rotate(90deg)' : 'none',
                transition:'transform .2s',
              }}>
                <span style={{ fontSize:'.72rem', fontWeight:700 }}>▶</span>
              </div>
              <div style={{ flex:1, minWidth:0 }}>
                <div style={{ fontSize:'.84rem', fontWeight:700, color:'var(--text)', display:'flex', alignItems:'center', gap:8, flexWrap:'wrap' }}>
                  Customize what we extract
                  <span style={{
                    fontSize:'.58rem', padding:'2px 7px', borderRadius:4, fontWeight:700, letterSpacing:'.08em',
                    background:'var(--beam)', color:'#001018',
                  }}>5 OPTIONS</span>
                </div>
                <div style={{ fontSize:'.7rem', color:'var(--dim)', marginTop:2 }}>
                  Multi-page crawl · contact info · SEO · maps · icon packs — {advOpen ? 'click any option to toggle' : 'click to configure'}
                </div>
              </div>
              {!advOpen && (
                <div style={{ display:'flex', gap:3, flexShrink:0 }}>
                  {Object.values(opts).filter(Boolean).length > 0 && Object.entries(opts).filter(([,v]) => v).slice(0, 5).map(([k]) => (
                    <div key={k} title={k} style={{
                      width:6, height:6, borderRadius:'50%', background:'var(--beam)',
                    }}/>
                  ))}
                  <span style={{ fontSize:'.64rem', color:'var(--beam)', marginLeft:6, fontFamily:'var(--font-mono)' }}>
                    {Object.values(opts).filter(Boolean).length}/5 on
                  </span>
                </div>
              )}
            </summary>
            <div style={{ padding:'6px 14px 14px', borderTop:'1px solid var(--border)', marginTop:0 }}>
              <div style={{ fontSize:'.66rem', color:'var(--dim)', letterSpacing:'.08em', textTransform:'uppercase', fontWeight:700, marginTop:10, marginBottom:8 }}>
                Brand Extraction toggles
              </div>
              <div style={{ display:'grid', gridTemplateColumns:'repeat(auto-fill, minmax(180px, 1fr))', gap:8 }}>
                {[
                  ['multipage','Multi-page (up to 300)', 'Crawls sitemap · all internal links'],
                  ['contact',  'Contact info',            'Phones · emails · addresses · hours'],
                  ['seo',      'SEO & image data',        'Meta tags · alt text · schema'],
                  ['maps',     'Maps & embeds',           'Google Maps · embedded iframes'],
                  ['icons',    'Icon packs',              'Font Awesome · Material · custom SVGs'],
                ].map(([k,label,desc]) => {
                  const on = opts[k];
                  return (
                    <label key={k} style={{
                      padding:'10px 12px',
                      background: on ? 'var(--beam-dim)' : 'var(--surface)',
                      border: on ? '1px solid var(--beam)' : '1px solid var(--border)',
                      borderRadius:6, cursor: modes.brand ? 'pointer' : 'not-allowed',
                      opacity: modes.brand ? 1 : .5,
                      display:'flex', alignItems:'flex-start', gap:8,
                      transition:'all .15s',
                    }}>
                      <input type="checkbox" checked={on} disabled={!modes.brand}
                             onChange={e => setOpts({...opts, [k]: e.target.checked})}
                             style={{ marginTop:2, accentColor:'var(--beam)' }}/>
                      <div style={{ flex:1, minWidth:0 }}>
                        <div style={{ fontSize:'.76rem', fontWeight:600, color: on ? 'var(--beam)' : 'var(--text)' }}>{label}</div>
                        <div style={{ fontSize:'.64rem', color:'var(--dim)', marginTop:1, lineHeight:1.3 }}>{desc}</div>
                      </div>
                    </label>
                  );
                })}
              </div>
            </div>
          </details>
          </React.Fragment>
          )}
        </div>
      </div>
      )}

      {helpOpen && <ScanModeHelp onClose={() => setHelpOpen(false)}/>}

      {step === 3 && done && (
        <>
          {/* ADA compliance banner — shown only when ADA audit actually ran AND returned data.
              Session 7 Railway backend doesn't produce ADA data yet, so this stays hidden. */}
          {modes.ada && data?.ada?.totals?.totalViolations > 0 && (
            <div style={{ marginTop:14, padding:'14px 16px', background:'linear-gradient(90deg, var(--rose-dim), var(--rose-dim))', border:'1px solid var(--rose-dim)', borderLeft:'3px solid #ff6b9d', borderRadius:8 }}>
              <div style={{ display:'flex', gap:16, alignItems:'center', flexWrap:'wrap' }}>
                <div style={{ display:'flex', alignItems:'center', gap:10 }}>
                  <Icon name="shield" size={18} style={{ color:'var(--rose)' }}/>
                  <div>
                    <div style={{ fontSize:'.68rem', fontFamily:'var(--font-mono)', letterSpacing:'.08em', color:'var(--rose)', textTransform:'uppercase' }}>ADA Compliance Audit</div>
                    <div style={{ fontSize:'.92rem', fontWeight:600, marginTop:2 }}>
                      {data.ada.legalFramework} · {data.ada.standard} · <span style={{ color:'var(--warn)' }}>Needs attention</span>
                    </div>
                  </div>
                </div>
                <div style={{ flex:1, minWidth:220, display:'flex', gap:18, flexWrap:'wrap', fontSize:'.74rem' }}>
                  <div><span style={{ color:'var(--dim)' }}>Taxonomy:</span> <strong>{data.ada.taxonomy}</strong></div>
                  <div><span style={{ color:'var(--dim)' }}>Deadline:</span> <strong style={{ color:'var(--warn)' }}>{data.ada.deadline}</strong></div>
                  <div><span style={{ color:'var(--dim)' }}>Pages scanned:</span> <strong>{data.ada.totals.pagesScanned}</strong></div>
                  <div><span style={{ color:'var(--dim)' }}>Violations:</span> <strong style={{ color:'var(--rose)' }}>{data.ada.totals.totalViolations}</strong></div>
                </div>
                <div style={{ display:'flex', gap:6 }}>
                  <button className="btn btn-ghost btn-sm" onClick={() => setActiveTab('ada')}><Icon name="shield" size={12}/>View Big 6</button>
                  <button className="btn btn-primary btn-sm" onClick={() => { window.WPSBD.switchTab('estimator'); window.wpsbToast(`ADA remediation proposal ready — $3.5K–$5K core + $1.8K PDFs + $75/mo monitoring`, 'ok'); }}><Icon name="billing" size={12}/>Build Proposal</button>
                </div>
              </div>
            </div>
          )}

          {(data?.scores?.length || 0) > 0 && (
          <div className="grid grid-4" style={{ marginTop: 16 }}>
            {data.scores.map(s => {
              const v = (s.lbl === 'Accessibility' && modes.ada) ? data.ada.score : s.v;
              const t = (s.lbl === 'Accessibility' && modes.ada) ? (v >= 80 ? 'ok' : v >= 60 ? 'warn' : 'bad') : s.t;
              return (
              <div key={s.lbl} className="stat">
                <div className="stat-lbl">{s.lbl}{s.lbl === 'Accessibility' && modes.ada && <span style={{ marginLeft:6, fontFamily:'var(--font-mono)', fontSize:'.52rem', padding:'1px 5px', borderRadius:3, background:'var(--rose-dim)', border:'1px solid var(--rose-dim)', color:'var(--rose)', letterSpacing:'.08em' }}>AUDITED</span>}</div>
                <div className={`stat-val ${t}`}>{v}</div>
                <div style={{ height:4, background:'var(--surface-3)', borderRadius:2, marginTop:4, overflow:'hidden' }}>
                  <div style={{ height:'100%', width: v + '%', background: `var(--${t === 'warn' ? 'warn' : t === 'bad' ? 'red' : 'green'})`, transition:'width .8s ease' }}/>
                </div>
              </div>
              );
            })}
          </div>
          )}

          {/* Tabs — styled as pill buttons for clearer affordance per UX feedback */}
          <div style={{
            marginTop: 20, display:'flex', gap:6, flexWrap:'wrap',
            padding:6, background:'var(--surface-2)', border:'1px solid var(--border)', borderRadius:8,
          }} role="tablist" aria-label="Scan result sections">
            {tabs.map(t => {
              const isActive = activeTab === t.id;
              return (
                <button key={t.id} role="tab" aria-selected={isActive} tabIndex={isActive ? 0 : -1}
                        onClick={() => setActiveTab(t.id)}
                        style={{
                          padding:'8px 14px', borderRadius:6,
                          fontSize:'.82rem', fontWeight: isActive ? 600 : 500,
                          background: isActive ? 'var(--beam)' : 'transparent',
                          color: isActive ? '#000' : 'var(--text-2)',
                          border: '1px solid ' + (isActive ? 'var(--beam)' : 'transparent'),
                          cursor:'pointer', transition:'all .15s ease',
                          display:'inline-flex', alignItems:'center', gap:6,
                          whiteSpace:'nowrap',
                        }}
                        onMouseEnter={e => { if (!isActive) { e.currentTarget.style.background = 'var(--surface-3)'; e.currentTarget.style.color = 'var(--text)'; } }}
                        onMouseLeave={e => { if (!isActive) { e.currentTarget.style.background = 'transparent'; e.currentTarget.style.color = 'var(--text-2)'; } }}>
                  {t.label}
                  {t.badge && <span style={{ fontFamily:'var(--font-mono)', fontSize:'.52rem', padding:'1px 5px', borderRadius:3, background: isActive ? 'rgba(0,0,0,.15)' : 'var(--rose-dim)', border:'1px solid ' + (isActive ? 'rgba(0,0,0,.25)' : 'var(--rose-dim)'), color: isActive ? '#000' : 'var(--rose)', letterSpacing:'.08em' }}>{t.badge}</span>}
                </button>
              );
            })}
          </div>

          <div style={{ marginTop:16 }}>
            {/* Empty-state messaging for unwired tabs lives INSIDE each tab component
                (e.g. ScannerBrandKitTab, ScannerImagesTab). Per UX feedback: avoid duplicate banner
                on top of the tab's own empty state. Each tab knows its own context.

                Each dispatch is wrapped in TabErrorBoundary so a crash in one tab
                shows an inline error message instead of blanking the whole Scanner
                view. Boundary state resets automatically when the tab changes
                (via React's keying: new element at same position = new boundary). */}

            {activeTab === 'brand-kit' && <TabErrorBoundary tabName="Brand Kit"><ScannerBrandKitTab data={data} payloads={payloads}/></TabErrorBoundary>}
            {activeTab === 'contact'   && <TabErrorBoundary tabName="Contact"><ScannerContactTab data={data} payloads={payloads}/></TabErrorBoundary>}
            {activeTab === 'images'    && <TabErrorBoundary tabName="Images"><ScannerImagesTab data={data}/></TabErrorBoundary>}
            {activeTab === 'files'     && <TabErrorBoundary tabName="Files & Docs"><ScannerFilesTab data={data}/></TabErrorBoundary>}
            {activeTab === 'sitemap'   && <TabErrorBoundary tabName="Sitemap"><ScannerSitemapTab data={data} payloads={payloads}/></TabErrorBoundary>}
            {activeTab === 'pages'     && <TabErrorBoundary tabName="Pages"><ScannerPagesTab data={data}/></TabErrorBoundary>}
            {activeTab === 'tech'      && <TabErrorBoundary tabName="Tech / DNS"><ScannerTechTab data={data} payloads={payloads}/></TabErrorBoundary>}
            {activeTab === 'plugins'   && <TabErrorBoundary tabName="Plugins & Forms"><ScannerPluginsFormsTab data={data}/></TabErrorBoundary>}
            {activeTab === 'ada'       && <TabErrorBoundary tabName="ADA"><ScannerAdaTab data={data}/></TabErrorBoundary>}
            {activeTab === 'recs'      && <TabErrorBoundary tabName="AI Recs"><ScannerRecsTab data={data}/></TabErrorBoundary>}
          </div>

          {/* (Scanned data flows to: banner removed — customer-facing noise.
               Tab routing happens elsewhere in the app shell.) */}

          {/* (SCAN ENGINE footer removed — engine info wasn't customer-valuable
               and the scan date already shows in the wizard card summary.) */}
        </>
      )}

      {/* Branded confirm dialog — replaces window.confirm() throughout
          this component. Renders only when open. Centered overlay with
          backdrop blur. Mounted here at top-level so it can fire from
          ANY child action (Save scan, Discard saved scan, navigate away). */}
      <ConfirmDialog {...confirmDialogProps}/>
    </div>
  );
}

window.Scanner = Scanner;
/* 2026-05-20 — Expose scanner PDF + TXT builders so tab-brand.jsx (also
   IIFE-wrapped) can pull them from window scope. Without this exposure,
   the onClick in tab-brand.jsx falls through to window.exportBrandKitPdf
   which Pages2.jsx (at module scope) sets — that one has hardcoded ACME
   placeholder branding and ignores its `data` arg. Bug surfaced when
   Jordan scanned tms-inc.us and downloaded a PDF that came back as ACME.
   Namespaced under window.WPSB.Scanner.* to avoid the same collision. */
window.WPSB = window.WPSB || {};
window.WPSB.Scanner = window.WPSB.Scanner || {};
window.WPSB.Scanner.exportBrandKitPdf = exportBrandKitPdf;
window.WPSB.Scanner.buildBrandKitTxt = buildBrandKitTxt;
console.log('[WPSB Scanner] Core loaded. All', 21, 'tab/shared components resolved from window.');
})();
