/* WPSiteBeam Scanner -- ADA / Accessibility Tab
   Clean rewrite May 4 2026. No incremental patches.
   Exports: window.ScannerAdaTab + all ScannerAda* sub-views.

   v1.1.0 patch (May 13 2026): wire window.WPSB.AdaCustomChecks
   (40-rule CD Compliance catalog) alongside axe-core. Plan-tier
   gated — Free Scan gets axe-core only, Starter+ gets axe + custom.
   Internal roles always get custom rules.
*/
(function () {
  'use strict';

  const ScanEmptyState = window.ScanEmptyState;
  const Icon = window.Icon;

  /* ── Plan-tier gate for custom (CD Compliance) rules ──────────
     Browser-side checks are free to execute. Gating exists so the
     Free Scan tier surfaces axe-core only (entry-level, conversion
     funnel), while Starter+ gets the full 40-rule Konza catalog.
     Internal roles bypass — same pattern as server.js BYPASS_ROLES. */
  const INTERNAL_ROLES   = ['super_admin', 'dev_admin', 'admin', 'internal_partner'];
  const CUSTOM_RULE_PLANS = ['starter', 'growth', 'agency', 'enterprise'];

  function shouldRunCustomChecks() {
    var user = window.currentUser || {};
    var role = user.role || '';
    if (INTERNAL_ROLES.indexOf(role) !== -1) return true;
    var plan = user.account_type || user.plan || 'free_scan';
    return CUSTOM_RULE_PLANS.indexOf(plan) !== -1;
  }

  /* ── Severity colours ─────────────────────────────────────────
     Two palettes maintained for distinct purposes:
     • Accent palette (SEV_COLOR + SEV_BG): muted tints for VIOLATION CARDS —
       cards need subtle backgrounds so they don't drown out the report.
     • Pill palette (SEV_PILL_*): WCAG-AA-compliant solid colors for BADGES —
       pills need high contrast so users immediately register severity.
     All pill foreground-on-background combinations meet WCAG 1.4.3 (4.5:1+).
     ──────────────────────────────────────────────────────────── */
  /* ── Severity color palette (2026-05-20 — token-based for light/dark) ──
     Was: hardcoded hex values (#ff7b7b, #c2410c, #f59e0b, #475569) that
     looked OK in dark mode but were unreadable in light mode (and the
     slate-700 minor color was unreadable in BOTH modes against dark page
     bg). Now: each severity maps to a CSS variable so token system swaps
     between dark + light themes automatically. Visual hierarchy preserved:
       critical = red (most severe)
       serious  = orange (auto-remaps to beam in light mode)
       moderate = warn (yellow/amber)
       minor    = dim/surface-3 (subtle, theme-aware) */
  const SEV_COLOR       = { critical:'var(--red)',     serious:'var(--orange)',     moderate:'var(--warn)',     minor:'var(--dim)' };
  const SEV_BG          = { critical:'var(--red-dim)', serious:'var(--orange-dim)', moderate:'var(--warn-dim)', minor:'var(--surface-3)' };
  /* Pill (badge) backgrounds — solid colors, WCAG-verified contrast.
     Foregrounds stay literal hex because they must contrast with the SOLID
     bg, not the page bg — saturated colors render the same regardless of
     light/dark theme. minor pill uses surface tokens since "minor"
     shouldn't shout. */
  const SEV_PILL_BG     = { critical:'var(--red)',     serious:'var(--orange)',     moderate:'var(--warn)',     minor:'var(--surface-3)' };
  const SEV_PILL_FG     = { critical:'#ffffff',        serious:'#ffffff',           moderate:'#0a0e1a',          minor:'var(--text)' };
  const SEV_PILL_BORDER = { critical:'var(--red-dim)', serious:'var(--orange-dim)', moderate:'var(--warn-dim)', minor:'var(--border)' };

  /* ── Disability → colour map ────────────────────────────────── */
  const DIS_COLORS = {
    'Blind':         { bg:'var(--purple-dim)', fg:'var(--purple)' },
    'Low Vision':    { bg:'var(--purple-dim)',  fg:'var(--purple)' },
    'Deafblind':     { bg:'rgba(236,72,153,.12)',  fg:'var(--rose)' },
    'Deaf':          { bg:'var(--beam-dim)',     fg:'var(--beam)' },
    'Motor':         { bg:'var(--green-dim)',    fg:'var(--green)' },
    'Colorblindness':{ bg:'var(--warn-dim)',   fg:'var(--warn)' },
    'Cognitive':     { bg:'var(--red-dim)',    fg:'var(--red)' },
  };

  /* ── Rule ID → disabilities affected ───────────────────────── */
  const DISABILITIES = {
    'image-alt':['Blind','Deafblind'], 'label':['Blind','Motor','Cognitive'],
    'heading-order':['Blind','Deafblind','Motor'], 'color-contrast':['Low Vision','Colorblindness'],
    'link-name':['Blind','Deafblind','Motor'], 'button-name':['Blind','Motor'],
    'duplicate-id':['Blind'], 'html-has-lang':['Blind'], 'document-title':['Blind','Cognitive'],
    'landmark-one-main':['Blind','Deafblind'], 'region':['Blind','Deafblind','Motor'],
    'bypass':['Blind','Motor'], 'select-name':['Blind','Motor'],
    'empty-heading':['Blind','Cognitive'], 'link-in-text-block':['Low Vision','Colorblindness'],
    'frame-title':['Blind'], 'meta-viewport':['Low Vision','Motor'],
    'scrollable-region-focusable':['Motor','Blind'], 'tabindex':['Motor'],
    'aria-allowed-attr':['Blind','Motor'], 'aria-roles':['Blind','Motor'],
    'aria-valid-attr':['Blind','Motor'], 'duplicate-id-aria':['Blind'],
    'target-size':['Motor'], 'video-caption':['Deafblind','Deaf'],
    'audio-caption':['Deafblind','Deaf'], 'object-alt':['Blind'],
    'svg-img-alt':['Blind'], 'meta-refresh':['Cognitive','Blind'],
    'identical-links-same-purpose':['Cognitive'], 'p-as-heading':['Blind','Cognitive'],
  };

  /* ── HOW_TO_FIX knowledge base ──────────────────────────────── */
  const HOW_TO_FIX = {
    'color-contrast': {
      meaning:'Text and background colors do not have enough contrast. Users with low vision or colorblindness may not be able to read the text.',
      fix:'Ensure a minimum contrast ratio of 4.5:1 for normal text and 3:1 for large text (18pt or 14pt bold).',
      code_bad:'<p style="color:#aaaaaa; background:#ffffff">Low contrast text</p>',
      code_good:'<p style="color:#595959; background:#ffffff">Sufficient contrast (7:1)</p>',
      wcag:'1.4.3 AA',
    },
    'image-alt': {
      meaning:'Images that convey information have no alternative text. Screen reader users cannot understand what the image shows.',
      fix:'Add descriptive alt text to all informative images. For decorative images use alt="" to hide them from screen readers.',
      code_bad:'<img src="logo.png">',
      code_good:'<img src="logo.png" alt="Company Logo">\n<img src="divider.png" alt="">',
      wcag:'1.1.1 A',
    },
    'label': {
      meaning:'Form inputs are not associated with a label. Screen reader users cannot tell what information to enter.',
      fix:'Associate every input with a label using matching for/id attributes, or wrap the input inside the label.',
      code_bad:'Name: <input type="text" name="name">',
      code_good:'<label for="name">Name</label>\n<input type="text" id="name" name="name">',
      wcag:'1.3.1 A / 4.1.2 A',
    },
    'heading-order': {
      meaning:'Headings skip levels (e.g. H1 to H3). Screen readers use headings to navigate -- skipped levels break the outline.',
      fix:'Use headings in sequential order. Never skip a level for visual effect -- use CSS instead.',
      code_bad:'<h1>Title</h1>\n<h3>Subheading</h3>',
      code_good:'<h1>Title</h1>\n<h2>Subheading</h2>\n<h3>Detail</h3>',
      wcag:'Best Practice',
    },
    'link-name': {
      meaning:'Links do not have descriptive text. Screen readers read link text aloud -- generic text gives no context.',
      fix:'Provide descriptive link text. Use aria-label when visible text cannot be changed.',
      code_bad:'<a href="/services">Click here</a>',
      code_good:'<a href="/services">View our services</a>',
      wcag:'2.4.4 A',
    },
    'button-name': {
      meaning:'Buttons have no accessible text. Screen readers cannot announce what the button does.',
      fix:'Add visible text inside the button, or use aria-label for icon-only buttons.',
      code_bad:'<button><svg>...</svg></button>',
      code_good:'<button aria-label="Close dialog"><svg>...</svg></button>',
      wcag:'4.1.2 A',
    },
    'duplicate-id': {
      meaning:'Multiple elements share the same id. ARIA relationships and form associations break with duplicate IDs.',
      fix:'Each id must be unique within a page. Rename duplicates.',
      code_bad:'<input id="email">  <input id="email">',
      code_good:'<input id="email-home">  <input id="email-work">',
      wcag:'4.1.1 A',
    },
    'html-has-lang': {
      meaning:'The html element has no lang attribute. Screen readers cannot select the correct language.',
      fix:'Add the lang attribute to the html element.',
      code_bad:'<html>',
      code_good:'<html lang="en">',
      wcag:'3.1.1 A',
    },
    'document-title': {
      meaning:'The page has no title element, or it is empty. Users cannot identify the page from their browser tab.',
      fix:'Add a unique, descriptive title inside head for every page.',
      code_bad:'<head><meta charset="utf-8"></head>',
      code_good:'<head><title>Contact Us - Company Name</title></head>',
      wcag:'2.4.2 A',
    },
    'landmark-one-main': {
      meaning:'The page has no main landmark. Screen reader users cannot jump directly to the main content.',
      fix:'Wrap the primary content in a main element.',
      code_bad:'<div id="content">...</div>',
      code_good:'<main id="content">...</main>',
      wcag:'Best Practice',
    },
    'region': {
      meaning:'Content exists outside landmark regions. Screen reader users navigating by landmarks will miss this content.',
      fix:'Ensure all visible content is contained within appropriate landmark elements.',
      code_bad:'<div class="promo">Sale!</div><main>...</main>',
      code_good:'<main><div class="promo">Sale!</div>...</main>',
      wcag:'Best Practice',
    },
    'link-in-text-block': {
      meaning:'Links inside text are only distinguished by color. Users with colorblindness cannot identify them.',
      fix:'Add an underline or another non-color visual indicator to links within paragraphs.',
      code_bad:'a { color: blue; text-decoration: none; }',
      code_good:'a { color: blue; text-decoration: underline; }',
      wcag:'1.4.1 A',
    },
    'frame-title': {
      meaning:'An iframe has no title attribute. Screen readers cannot announce the purpose of the embedded content.',
      fix:'Add a descriptive title attribute to every iframe.',
      code_bad:'<iframe src="map.html"></iframe>',
      code_good:'<iframe src="map.html" title="Office location map"></iframe>',
      wcag:'2.4.1 A / 4.1.2 A',
    },
    'bypass': {
      meaning:'There is no way to skip repeated navigation. Keyboard users must tab through every nav link on every page.',
      fix:'Add a "Skip to main content" link as the first focusable element.',
      code_bad:'<!-- No skip link -->\n<nav>...</nav><main>...',
      code_good:'<a href="#main" class="skip-link">Skip to main content</a>\n<nav>...</nav>\n<main id="main">...',
      wcag:'2.4.1 A',
    },
    'select-name': {
      meaning:'A select dropdown has no accessible label. Screen readers cannot identify what the user is selecting.',
      fix:'Associate a label with every select element using matching for/id attributes.',
      code_bad:'<select name="state"><option>Kansas</option></select>',
      code_good:'<label for="state">State</label>\n<select id="state" name="state"><option>Kansas</option></select>',
      wcag:'1.3.1 A / 4.1.2 A',
    },
    'meta-viewport': {
      meaning:'The viewport meta tag disables user scaling. Users with low vision cannot zoom the page.',
      fix:'Remove user-scalable=no and maximum-scale=1 from the viewport meta tag.',
      code_bad:'<meta name="viewport" content="width=device-width, user-scalable=no">',
      code_good:'<meta name="viewport" content="width=device-width, initial-scale=1">',
      wcag:'1.4.4 AA',
    },
    'empty-heading': {
      meaning:'A heading element is empty. Screen readers announce it but provide no meaningful text.',
      fix:'Add descriptive text content to the heading, or remove the empty element.',
      code_bad:'<h2></h2>',
      code_good:'<h2>Our Services</h2>',
      wcag:'Best Practice',
    },
    'scrollable-region-focusable': {
      meaning:'A scrollable region cannot receive keyboard focus. Keyboard users cannot scroll to see all content.',
      fix:'Add tabindex="0" to scrollable containers that are not natively focusable.',
      code_bad:'<div style="overflow:auto">...long content...</div>',
      code_good:'<div style="overflow:auto" tabindex="0" role="region" aria-label="Scrollable content">...</div>',
      wcag:'2.1.1 A',
    },
  };

  /* ── 22 Manual Checks ───────────────────────────────────────── */
  const MANUAL_CHECKS = [
    { title:"Text Legibility and Layout at High Zoom (200%-400%)", level:"AA", wcag:"1.4.4 / 1.4.10",
      purpose:"Verify that content reflows and remains readable when browser zoom is increased to 200% or 400% without horizontal scrolling.",
      criteria:["1.4.4 Resize Text AA","1.4.10 Reflow AA","1.4.12 Text Spacing AAA"],
      steps:["Load page at 100%","Increase browser zoom to 200% - verify no horizontal scrollbar and no truncated text","Increase to 400% - content should reflow to single column","Test on mobile with pinch-to-zoom"],
    },
    { title:"Keyboard Navigation - All Interactive Elements", level:"A", wcag:"2.1.1",
      purpose:"Every interactive element must be reachable and operable using keyboard alone.",
      criteria:["2.1.1 Keyboard A","2.1.2 No Keyboard Trap A"],
      steps:["Press Tab to move through all interactive elements","Verify every button, link, and form field is reachable","Verify no keyboard trap exists - Esc should always exit modals","Test dropdowns and mega-menus with arrow keys"],
    },
    { title:"Focus Indicator Visible on All Elements", level:"AA", wcag:"2.4.7 / 2.4.11",
      purpose:"Keyboard focus must always be visually apparent - users who cannot use a mouse rely on the focus indicator.",
      criteria:["2.4.7 Focus Visible AA","2.4.11 Focus Appearance AA (WCAG 2.2)"],
      steps:["Tab through all interactive elements","Verify every focused element shows a visible outline or highlight","Check contrast of focus indicator against background"],
    },
    { title:"Form Submission and Error Handling", level:"A", wcag:"3.3.1 / 3.3.2",
      purpose:"Error messages must be associated with the fields that caused them.",
      criteria:["3.3.1 Error Identification A","3.3.2 Labels or Instructions A","3.3.3 Error Suggestion AA"],
      steps:["Submit each form with empty required fields","Verify error messages identify which field has an error","Verify error message is announced by screen reader","Verify error message suggests how to fix the issue"],
    },
    { title:"Navigation Menus - Focus Management and Clarity", level:"AA", wcag:"2.4.3 / 3.2.3",
      purpose:"Navigation menus must be operable by keyboard and appear consistently across pages.",
      criteria:["2.4.3 Focus Order A","3.2.3 Consistent Navigation AA","2.4.5 Multiple Ways AA"],
      steps:["Open navigation with keyboard","Verify submenu items are reachable via keyboard","Verify focus order matches visual order","Check menu appears consistently across all pages"],
    },
    { title:"Modal Dialogs - Focus Trapping and Role Compliance", level:"AA", wcag:"4.1.2",
      purpose:"When a modal opens, focus must move into it, be trapped while open, and return to the trigger when closed.",
      criteria:["4.1.2 Name, Role, Value AA","2.1.2 No Keyboard Trap A"],
      steps:["Open each modal/dialog","Verify focus moves into the modal","Tab through - verify focus stays within modal","Press Esc - verify modal closes and focus returns to trigger"],
    },
    { title:"Screen Reader Compatibility - Semantic Structure", level:"A", wcag:"1.3.1",
      purpose:"Content structure and relationships must be programmatically determinable.",
      criteria:["1.3.1 Info and Relationships A"],
      steps:["Use NVDA/VoiceOver/JAWS to navigate the page","Verify headings are announced correctly","Verify lists are announced as lists","Verify form labels are announced with inputs","Verify table headers are associated with cells"],
    },
    { title:"Color Not Sole Means of Conveying Information", level:"A", wcag:"1.4.1",
      purpose:"Information conveyed only through color cannot be perceived by colorblind users.",
      criteria:["1.4.1 Use of Color A"],
      steps:["Review all charts, graphs, and status indicators","Verify each uses text labels, patterns, or shapes in addition to color","Review required field indicators - must not use only red color"],
    },
    { title:"Moving, Blinking and Auto-Updating Content", level:"A", wcag:"2.2.2",
      purpose:"Content that moves, blinks, or auto-updates for more than 5 seconds must be pausable.",
      criteria:["2.2.2 Pause, Stop, Hide A","2.3.1 Three Flashes A"],
      steps:["Identify any carousels, slideshows, or auto-updating regions","Verify there is a pause/stop/hide control","Check for any flashing content - more than 3 times per second is a seizure risk"],
    },
    { title:"Consistent Identification of Repeated Components", level:"AA", wcag:"3.2.4",
      purpose:"Components with the same function across pages must be identified consistently.",
      criteria:["3.2.4 Consistent Identification AA"],
      steps:["Identify search, login, share, and print buttons","Verify they use the same accessible name across all pages","Verify icons have consistent alt text or aria-labels"],
    },
    { title:"Link Purpose - Context and Destination", level:"A", wcag:"2.4.4",
      purpose:"The purpose of every link must be determinable from its text alone or from its immediate context.",
      criteria:["2.4.4 Link Purpose A","2.4.9 Link Purpose (Link Only) AAA"],
      steps:["List all links on the page","Identify any with generic text: click here, read more, here, more","Verify each link purpose is clear from text or aria-label","Verify icon-only links have accessible labels"],
    },
    { title:"Image Text Alternatives and Avoiding Images of Text", level:"A", wcag:"1.1.1 / 1.4.5",
      purpose:"All non-text content must have text alternatives. Text should not be presented as images.",
      criteria:["1.1.1 Non-text Content A","1.4.5 Images of Text AA"],
      steps:["Check all images with text overlay - is the text in HTML or embedded in the image?","Verify complex images have long descriptions","Verify CAPTCHAs have audio alternatives"],
    },
    { title:"Media Captions, Transcripts and Autoplay Control", level:"A", wcag:"1.2.1 / 1.2.2",
      purpose:"Videos must have captions. Pre-recorded audio must have transcripts.",
      criteria:["1.2.1 Audio-only / Video-only A","1.2.2 Captions (Pre-recorded) A","1.4.2 Audio Control A"],
      steps:["Check all videos for synchronized captions","Check any audio-only content for text transcripts","Verify no audio autoplays for more than 3 seconds without a stop control"],
    },
    { title:"Content Orientation Flexibility", level:"A", wcag:"1.3.4",
      purpose:"Content must not be restricted to landscape or portrait orientation only.",
      criteria:["1.3.4 Orientation A (WCAG 2.1)"],
      steps:["Rotate device to portrait and landscape","Verify content is accessible in both orientations","Identify any elements that are hidden or broken in one orientation"],
    },
    { title:"Input Interaction Accessibility", level:"AA", wcag:"1.3.5 / 1.4.13",
      purpose:"Input fields should identify their purpose. Hover/focus content must be dismissible.",
      criteria:["1.3.5 Identify Input Purpose AA","1.4.13 Content on Hover or Focus AA"],
      steps:["Check autocomplete attributes on name/email/phone/address inputs","Identify all tooltips - can they be dismissed with Esc?","Can the pointer hover over a tooltip without it disappearing?"],
    },
    { title:"Time Limit Controls", level:"A", wcag:"2.2.1",
      purpose:"If a session timeout exists, users must be warned and given time to extend.",
      criteria:["2.2.1 Timing Adjustable A"],
      steps:["Check for session timeouts","Verify a warning appears before timeout","Verify user has at least 20 seconds to extend","Verify data is preserved on timeout"],
    },
    { title:"Hidden and Revealed Content Accessibility", level:"AA", wcag:"4.1.3",
      purpose:"Status messages that appear without focus change must be announced by screen readers.",
      criteria:["4.1.3 Status Messages AA (WCAG 2.1)"],
      steps:["Submit forms and perform actions that generate notifications","Verify success/error messages are announced without moving focus","Check aria-live regions are in place for dynamic updates"],
    },
    { title:"Descriptive Context for Interactive Elements", level:"A", wcag:"2.4.6",
      purpose:"All interactive elements must have accessible names that describe their purpose.",
      criteria:["2.4.6 Headings and Labels AA","4.1.2 Name, Role, Value A"],
      steps:["Check all buttons, links, and form fields for accessible names","Verify icon-only buttons have aria-labels","Run page through screen reader and listen to interactive elements"],
    },
    { title:"Semantic Structure and Data Integrity of Tables", level:"A", wcag:"1.3.1",
      purpose:"Data tables must use proper headers so screen readers can announce which header a cell belongs to.",
      criteria:["1.3.1 Info and Relationships A"],
      steps:["Identify all data tables","Verify th elements are used for headers","Verify complex tables use scope col/row or headers/id associations"],
    },
    { title:"Page Language and Language of Parts", level:"A", wcag:"3.1.1 / 3.1.2",
      purpose:"The page language must be set, and sections in a different language must have their own lang attribute.",
      criteria:["3.1.1 Language of Page A","3.1.2 Language of Parts AA"],
      steps:["Check html lang attribute is set correctly","Find any foreign language phrases or quotes","Verify those elements have lang=es / lang=fr etc."],
    },
    { title:"User Control, Interruptions and Data Preservation", level:"AAA", wcag:"2.2.4 / 2.2.5",
      purpose:"Users should be able to postpone non-emergency interruptions. After re-authentication, data should be preserved.",
      criteria:["2.2.4 Interruptions AAA","2.2.5 Re-authenticating AAA"],
      steps:["Check for pop-ups and notifications - can they be deferred?","Test session expiry - is form data preserved after re-login?"],
    },
    { title:"Identify Purpose of Components and Regions", level:"AAA", wcag:"1.3.6",
      purpose:"Icons, regions, and interface components must have their purpose identifiable to support personalization tools.",
      criteria:["1.3.6 Identify Purpose AAA (WCAG 2.1)"],
      steps:["Verify all landmark regions have descriptive aria-labels","Check icon buttons have aria-labels matching standard purposes","Verify inputs have appropriate autocomplete attributes"],
    },
  ];

  /* ── ViolationDetail -- expandable How to Fix card ──────────── */
  function ViolationDetail({ fix }) {
    const [open, setOpen] = React.useState(false);
    if (!fix) return null;
    return (
      <div style={{ marginTop:8 }}>
        <button onClick={function() { setOpen(function(o) { return !o; }); }}
          style={{ background:'none', border:'none', color:'var(--beam)', fontSize:'.72rem',
            fontWeight:700, cursor:'pointer', padding:0, display:'flex', gap:4, alignItems:'center' }}>
          {open ? '▼ Hide fix' : '▶ How to fix this'}
        </button>
        {open && (
          <div style={{ marginTop:8, borderLeft:'2px solid var(--beam)', paddingLeft:12 }}>
            <div style={{ marginBottom:8 }}>
              <div style={{ fontSize:'.68rem', fontWeight:700, color:'var(--dim)', textTransform:'uppercase', letterSpacing:'.05em', marginBottom:3 }}>What this means</div>
              <div style={{ fontSize:'.76rem', color:'var(--text)', lineHeight:1.6 }}>{fix.meaning}</div>
            </div>
            <div style={{ marginBottom: fix.code_good ? 8 : 0 }}>
              <div style={{ fontSize:'.68rem', fontWeight:700, color:'var(--dim)', textTransform:'uppercase', letterSpacing:'.05em', marginBottom:3 }}>How to fix it</div>
              <div style={{ fontSize:'.76rem', color:'var(--text)', lineHeight:1.6 }}>{fix.fix}</div>
            </div>
            {fix.code_good && (
              <div style={{ display:'grid', gridTemplateColumns:'1fr 1fr', gap:8, marginTop:8 }}>
                {fix.code_bad && (
                  <div>
                    <div style={{ fontSize:'.62rem', fontWeight:700, color:'var(--red)', marginBottom:3 }}>✗ Incorrect</div>
                    <pre style={{ background:'var(--red-dim)', border:'1px solid var(--red)', borderRadius:5,
                      padding:'6px 8px', fontSize:'.62rem', fontFamily:'var(--font-mono)', color:'var(--red)',
                      overflow:'auto', whiteSpace:'pre-wrap', margin:0 }}>{fix.code_bad}</pre>
                  </div>
                )}
                <div>
                  <div style={{ fontSize:'.62rem', fontWeight:700, color:'var(--green)', marginBottom:3 }}>✓ Correct</div>
                  <pre style={{ background:'var(--green-dim)', border:'1px solid var(--green)', borderRadius:5,
                    padding:'6px 8px', fontSize:'.62rem', fontFamily:'var(--font-mono)', color:'var(--green)',
                    overflow:'auto', whiteSpace:'pre-wrap', margin:0 }}>{fix.code_good}</pre>
                </div>
              </div>
            )}
            {fix.wcag && (
              <div style={{ marginTop:8, fontSize:'.68rem', color:'var(--dim)' }}>
                WCAG: <span style={{ color:'var(--beam)' }}>{fix.wcag}</span>
              </div>
            )}
          </div>
        )}
      </div>
    );
  }

  /* ── ManualCheckItem -- expandable manual audit row ──────────── */
  function ManualCheckItem({ check }) {
    const [open, setOpen] = React.useState(false);
    const levelColor = { A:'var(--green)', AA:'var(--warn)', AAA:'var(--dim)' };
    return (
      <div style={{ borderBottom:'1px solid var(--border)' }}>
        <div style={{ display:'flex', gap:10, padding:'12px 0', alignItems:'flex-start', cursor:'pointer' }}
             onClick={function() { setOpen(function(o) { return !o; }); }}>
          <div style={{ width:18, height:18, border:'2px solid var(--border)', borderRadius:3, flexShrink:0, marginTop:2 }} />
          <div style={{ flex:1 }}>
            <div style={{ display:'flex', gap:8, alignItems:'center', flexWrap:'wrap', marginBottom:3 }}>
              <span style={{ fontSize:'.8rem', fontWeight:700, color:'var(--text)' }}>{check.title}</span>
              {(check.criteria||[]).slice(0,2).map(function(c, i) {
                var lvl = (c.match(/\b(AAA|AA|A)\b/) || ['','A'])[1];
                return (
                  <span key={i} style={{ padding:'1px 6px', borderRadius:3, fontSize:'.6rem', fontWeight:700,
                    background:(levelColor[lvl]||'var(--green)')+'15', color:levelColor[lvl]||'var(--green)',
                    border:'1px solid '+(levelColor[lvl]||'var(--green)')+'40' }}>{lvl}</span>
                );
              })}
              <span style={{ marginLeft:'auto', fontSize:'.68rem', color:'var(--dim)' }}>{open ? '▲' : '▼'}</span>
            </div>
            <div style={{ fontSize:'.72rem', color:'var(--dim)', lineHeight:1.4 }}>{check.purpose}</div>
          </div>
        </div>
        {open && (
          <div style={{ paddingLeft:28, paddingBottom:14 }}>
            {check.criteria && check.criteria.length > 0 && (
              <div style={{ marginBottom:12 }}>
                <div style={{ fontSize:'.68rem', fontWeight:700, color:'var(--dim)', textTransform:'uppercase', letterSpacing:'.05em', marginBottom:6 }}>WCAG Criteria Covered</div>
                {check.criteria.map(function(c, i) {
                  var lvl = (c.match(/\b(AAA|AA|A)\b/) || ['','A'])[1];
                  return (
                    <div key={i} style={{ display:'flex', gap:8, alignItems:'center', marginBottom:4 }}>
                      <span style={{ padding:'1px 7px', borderRadius:3, fontSize:'.65rem', fontWeight:700,
                        background:(levelColor[lvl]||'var(--green)')+'15', color:levelColor[lvl]||'var(--green)',
                        border:'1px solid '+(levelColor[lvl]||'var(--green)')+'40' }}>{lvl}</span>
                      <span style={{ fontSize:'.76rem', color:'var(--text)' }}>{c}</span>
                    </div>
                  );
                })}
              </div>
            )}
            {check.steps && check.steps.length > 0 && (
              <div>
                <div style={{ fontSize:'.68rem', fontWeight:700, color:'var(--dim)', textTransform:'uppercase', letterSpacing:'.05em', marginBottom:6 }}>Testing Steps</div>
                {check.steps.map(function(s, i) {
                  return (
                    <div key={i} style={{ display:'flex', gap:8, alignItems:'flex-start', marginBottom:5 }}>
                      <span style={{ width:18, height:18, borderRadius:'50%', background:'var(--beam-dim)',
                        color:'var(--beam)', fontSize:'.65rem', fontWeight:800, display:'flex', alignItems:'center',
                        justifyContent:'center', flexShrink:0, marginTop:1 }}>{i+1}</span>
                      <span style={{ fontSize:'.76rem', color:'var(--dim)', lineHeight:1.5 }}>{s}</span>
                    </div>
                  );
                })}
              </div>
            )}
          </div>
        )}
      </div>
    );
  }

  /* ── ContrastChecker -- inline WCAG colour contrast tool ─────── */
  function ContrastChecker() {
    var _useState1 = React.useState('#000000');
    var fg = _useState1[0]; var setFg = _useState1[1];
    var _useState2 = React.useState('#ffffff');
    var bg = _useState2[0]; var setBg = _useState2[1];

    function hexToRgb(hex) {
      var h = hex.replace('#','');
      if (h.length === 3) { h = h[0]+h[0]+h[1]+h[1]+h[2]+h[2]; }
      var n = parseInt(h, 16);
      return [(n>>16)&255,(n>>8)&255,n&255];
    }
    function luminance(rgb) {
      return rgb.map(function(v) {
        v = v/255;
        return v <= 0.04045 ? v/12.92 : Math.pow((v+0.055)/1.055, 2.4);
      }).reduce(function(acc,v,i) { return acc + v*[0.2126,0.7152,0.0722][i]; }, 0);
    }
    function ratio() {
      try {
        var l1 = luminance(hexToRgb(fg));
        var l2 = luminance(hexToRgb(bg));
        var lighter = Math.max(l1,l2); var darker = Math.min(l1,l2);
        return Math.round(((lighter+0.05)/(darker+0.05))*100)/100;
      } catch(e) { return 1; }
    }

    var r = ratio();
    var aa_normal  = r >= 4.5;
    var aa_large   = r >= 3.0;
    var aaa_normal = r >= 7.0;
    var aaa_large  = r >= 4.5;
    /* Use theme tokens so colors adapt to light/dark */
    var scoreColor = aa_normal ? 'var(--green)' : aa_large ? 'var(--warn)' : 'var(--red)';
    var scoreBg    = aa_normal ? 'var(--green-dim)' : aa_large ? 'var(--warn-dim)' : 'var(--red-dim)';

    function colorInput(label, val, setter) {
      return (
        <div style={{ display:'flex', flexDirection:'column', gap:6 }}>
          <div style={{ fontSize:'.72rem', color:'var(--dim)', fontWeight:600, textTransform:'uppercase', letterSpacing:'.04em' }}>{label}</div>
          <div style={{ display:'flex', gap:8, alignItems:'center' }}>
            <input type="color" value={val}
              onChange={function(e) { setter(e.target.value); }}
              style={{ width:36, height:36, border:'1px solid var(--border)', borderRadius:6, cursor:'pointer', background:'none', padding:2 }}/>
            <input type="text" value={val}
              onChange={function(e) { if (/^#[0-9a-fA-F]{0,6}$/.test(e.target.value)) setter(e.target.value); }}
              style={{ width:90, padding:'6px 10px', background:'var(--bg)', border:'1px solid var(--border)',
                borderRadius:6, color:'var(--text)', fontFamily:'var(--font-mono)', fontSize:'.8rem' }}/>
          </div>
        </div>
      );
    }

    return (
      <div style={{ background:'var(--surface2)', border:'1px solid var(--border)', borderRadius:10, padding:18, marginBottom:16 }}>
        <div style={{ fontWeight:700, color:'var(--text)', fontSize:'.88rem', marginBottom:14 }}>
          Colour Contrast Checker (WCAG 2.1)
        </div>
        <div style={{ display:'flex', gap:16, alignItems:'flex-start', flexWrap:'wrap' }}>
          {colorInput('Text Color', fg, setFg)}
          {colorInput('Background', bg, setBg)}
          <div style={{ flex:1, minWidth:180, padding:'14px 18px', borderRadius:8,
            background:bg, border:'2px solid var(--border)', display:'flex', flexDirection:'column', gap:6 }}>
            <div style={{ color:fg, fontSize:'14px', fontWeight:600 }}>Small text sample</div>
            <div style={{ color:fg, fontSize:'18px', fontWeight:700 }}>Large text sample</div>
          </div>
          <div style={{ display:'flex', flexDirection:'column', gap:8, minWidth:140 }}>
            <div style={{ textAlign:'center', padding:'12px 16px', borderRadius:8,
              background:scoreBg, border:'1px solid '+scoreColor }}>
              <div style={{ fontSize:'28px', fontWeight:900, color:scoreColor, lineHeight:1 }}>{r}:1</div>
              <div style={{ fontSize:'.68rem', color:'var(--dim)', marginTop:3 }}>Contrast Ratio</div>
            </div>
            <div style={{ display:'grid', gridTemplateColumns:'1fr 1fr', gap:4, fontSize:'.66rem' }}>
              {[
                ['AA Normal', aa_normal,  '4.5:1'],
                ['AA Large',  aa_large,   '3.0:1'],
                ['AAA Normal',aaa_normal, '7.0:1'],
                ['AAA Large', aaa_large,  '4.5:1'],
              ].map(function(row) {
                var label = row[0]; var pass = row[1]; var req = row[2];
                return (
                  <div key={label} style={{ padding:'4px 6px', borderRadius:4,
                    background: pass ? 'var(--green-dim)' : 'var(--red-dim)',
                    border:'1px solid '+(pass ? 'var(--green)' : 'var(--red)'),
                    textAlign:'center' }}>
                    <div style={{ fontWeight:700, color: pass ? 'var(--green)' : 'var(--red)' }}>{pass?'Pass':'Fail'}</div>
                    <div style={{ color:'var(--dim)', fontSize:'.6rem' }}>{label}</div>
                    <div style={{ color:'var(--dim)', fontSize:'.6rem', fontFamily:'var(--font-mono)' }}>{req}</div>
                  </div>
                );
              })}
            </div>
          </div>
        </div>
      </div>
    );
  }

  /* ── exportViolationsCSV ─────────────────────────────────────── */
  function exportViolationsCSV(violations, siteUrl) {
    var domain = 'site';
    try { domain = new URL(siteUrl).hostname; } catch(e) {}
    var date = new Date().toLocaleDateString('en-US').replace(/\//g,'-');
    var header = ['#','Rule ID','Severity','Issue Title','Description','Elements','WCAG','Remediation','Guide URL'];
    var rows = [header].concat(violations.map(function(v, i) {
      return [
        i+1, v.id, v.impact, v.help||v.id,
        (v.description||'').replace(/"/g,"'"),
        v.nodeCount||(v.nodes||[]).length,
        (v.tags||[]).filter(function(t) { return /wcag/.test(t); }).join(', '),
        ((HOW_TO_FIX[v.id]||{}).fix||'See WCAG guide.').replace(/"/g,"'"),
        v.helpUrl||'',
      ];
    }));
    var csv = rows.map(function(row) {
      return row.map(function(cell) {
        return '"' + String(cell||'').replace(/"/g,'""') + '"';
      }).join(',');
    }).join('\n');
    var blob = new Blob([csv], { type:'text/csv;charset=utf-8;' });
    var url = URL.createObjectURL(blob);
    var a = document.createElement('a');
    a.href = url; a.download = domain+'-ada-'+date+'.csv'; a.click();
    URL.revokeObjectURL(url);
    window.wpsbToast && window.wpsbToast('Downloaded '+violations.length+' violations', 'ok');
  }

  /* ── renderViolations -- standalone component ─────────────────── */
  function renderViolations(violations) {
    if (!violations || violations.length === 0) {
      return (
        <div style={{ textAlign:'center', padding:24, color:'var(--green)', fontWeight:700 }}>
          No structural violations detected.
        </div>
      );
    }
    var impOrder = {critical:0,serious:1,moderate:2,minor:3};
    var grouped = {};
    violations.forEach(function(v) {
      if (!grouped[v.id]) grouped[v.id] = Object.assign({}, v, { nodeCount:0 });
      grouped[v.id].nodeCount = (grouped[v.id].nodeCount||0) + (v.nodeCount||1);
    });
    var list = Object.values(grouped).sort(function(a, b) {
      return (impOrder[a.impact]||3) - (impOrder[b.impact]||3);
    });

    return (
      <div>
        {list.map(function(v, i) {
          var c  = SEV_COLOR[v.impact] || 'var(--dim)';
          var bg = SEV_BG[v.impact]    || 'rgba(100,116,139,.1)';
          var dis = DISABILITIES[v.id] || [];
          return (
            <div key={i} style={{ marginBottom:10, borderRadius:8, border:'1px solid '+c+'40', background:bg, overflow:'hidden' }}>
              <div style={{ padding:'10px 14px', display:'flex', justifyContent:'space-between', alignItems:'flex-start', gap:10 }}>
                <div style={{ flex:1, minWidth:0 }}>
                  <div style={{ display:'flex', gap:6, alignItems:'center', marginBottom:4, flexWrap:'wrap' }}>
                    <span style={{ padding:'3px 9px', borderRadius:4, fontSize:'.62rem', fontWeight:800,
                      textTransform:'uppercase', letterSpacing:'.05em',
                      background:SEV_PILL_BG[v.impact]||'var(--surface-3)',
                      color:SEV_PILL_FG[v.impact]||'var(--text)',
                      border:'1px solid '+(SEV_PILL_BORDER[v.impact]||'var(--border)') }}>
                      {v.impact}
                    </span>
                    <span style={{ background:'var(--surface2)', border:'1px solid var(--border)', borderRadius:4,
                      padding:'2px 7px', fontSize:'.62rem', color:'var(--muted, #c9d1d9)', fontFamily:'var(--font-mono)' }}>
                      {v.id}
                    </span>
                    <span style={{ fontSize:'.68rem', color:'var(--muted, #c9d1d9)' }}>
                      {v.nodeCount} {v.nodeCount !== 1 ? 'elements' : 'element'}
                    </span>
                    {dis.map(function(d) {
                      var dc = DIS_COLORS[d] || { bg:'rgba(100,116,139,.1)', fg:'var(--dim)' };
                      return (
                        <span key={d} style={{ padding:'1px 6px', borderRadius:3, fontSize:'.6rem', fontWeight:700,
                          background:dc.bg, color:dc.fg, border:'1px solid '+dc.fg+'40' }}>{d}</span>
                      );
                    })}
                  </div>
                  <div style={{ fontWeight:700, color:'var(--text)', fontSize:'.84rem' }}>{v.help}</div>
                  <div style={{ color:'var(--muted, #c9d1d9)', fontSize:'.74rem', marginTop:2, lineHeight:1.5 }}>{v.description}</div>
                  {HOW_TO_FIX[v.id] && <ViolationDetail fix={HOW_TO_FIX[v.id]} />}
                </div>
                <a href={v.helpUrl} target="_blank" rel="noopener"
                   style={{ fontSize:'.7rem', color:'var(--beam)', whiteSpace:'nowrap', flexShrink:0 }}>
                  WCAG
                </a>
              </div>
              {(v.nodes||[]).slice(0,2).map(function(n, ni) {
                return (
                  <div key={ni} style={{ borderTop:'1px solid '+c+'20', padding:'8px 14px' }}>
                    <div style={{ fontFamily:'var(--font-mono)', fontSize:'.64rem', color:'var(--muted)',
                      background:'rgba(0,0,0,.2)', borderRadius:4, padding:'4px 8px', overflow:'auto', maxHeight:56,
                      whiteSpace:'pre-wrap', wordBreak:'break-all' }}>{n.html}</div>
                    {n.failureSummary && (
                      <div style={{ fontSize:'.68rem', color:'var(--dim)', marginTop:3 }}>
                        {(n.failureSummary||'').split('\n').filter(Boolean)[0]}
                      </div>
                    )}
                  </div>
                );
              })}
            </div>
          );
        })}
      </div>
    );
  }

  /* ── BrowserAxeScan ──────────────────────────────────────────── */
  function BrowserAxeScan({ siteUrl, autoRun }) {
    var _s1 = React.useState('idle');    var state = _s1[0];    var setState = _s1[1];
    var _s2 = React.useState([]);        var violations = _s2[0]; var setViolations = _s2[1];
    var _s3 = React.useState([]);        var passes = _s3[0];     var setPasses = _s3[1];
    var _s4 = React.useState([]);        var inapplicable = _s4[0]; var setInapplicable = _s4[1];
    var _s5 = React.useState(null);      var summary = _s5[0];    var setSummary = _s5[1];
    var _s6 = React.useState(null);      var errMsg = _s6[0];     var setErrMsg = _s6[1];
    var _s7 = React.useState(null);      var scannedUrl = _s7[0]; var setScannedUrl = _s7[1];
    var _s8 = React.useState('violations'); var activeTab = _s8[0]; var setActiveTab = _s8[1];

    var RAILWAY = (window.WPSBD && window.WPSBD.railwayUrl) || 'https://wpsitebeam-railway-api-production.up.railway.app';

    /* Auto-run when the parent signals ADA was requested (checkbox checked)
       but the server-side scan got 0 pages — fall back to browser scan.
       Polls for axe-core to finish loading (it may be async), then runs once. */
    React.useEffect(function() {
      if (!autoRun) return;
      var attempts = 0;
      var timer = setInterval(function() {
        attempts++;
        if (window.axe) { clearInterval(timer); runScan(); }
        if (attempts >= 12) { clearInterval(timer); } /* give up after 6s */
      }, 500);
      return function() { clearInterval(timer); };
    }, []); /* eslint-disable-line react-hooks/exhaustive-deps */

    function runScan() {
      if (!window.axe) { setErrMsg('axe-core not loaded -- refresh the page and try again.'); setState('error'); return; }
      if (!siteUrl)    { setErrMsg('No site URL available.'); setState('error'); return; }
      setState('loading'); setErrMsg(null); setViolations([]); setSummary(null);

      var token = window.currentToken || '';
      var fetchUrl = RAILWAY + '/brain/scan/test?url=' + encodeURIComponent(siteUrl);

      fetch(fetchUrl, { headers: { 'Authorization': 'Bearer ' + token } })
        .then(function(res) {
          if (!res.ok) throw new Error('Could not fetch page (HTTP ' + res.status + ')');
          return res.json();
        })
        .then(function(json) {
          var html = json.body_preview || json.body || '';
          if (!html || html.length < 200) throw new Error('Page returned empty HTML');

          var doc = new DOMParser().parseFromString(html, 'text/html');
          var axeConfig = {
            runOnly: { type:'tag', values:['wcag2a','wcag2aa','wcag21a','wcag21aa','best-practice'] },
            rules: { 'color-contrast':{ enabled:false }, 'color-contrast-enhanced':{ enabled:false } },
          };

          /* Bundle axe results with doc + rawHtml so the next .then
             can also run window.WPSB.AdaCustomChecks against the
             same parsed DOM (plan-tier gated). */
          return window.axe.run(doc, axeConfig).then(function(axeResults) {
            return { axeResults: axeResults, doc: doc, rawHtml: html };
          });
        })
        .then(function(bundle) {
          var results = bundle.axeResults;
          var doc     = bundle.doc;
          var rawHtml = bundle.rawHtml;

          var viols = (results.violations||[]).map(function(v) {
            return {
              id:v.id, impact:v.impact, description:v.description,
              help:v.help, helpUrl:v.helpUrl, tags:v.tags,
              nodes:(v.nodes||[]).slice(0,3).map(function(n) {
                return {
                  html:(n.html||'').substring(0,200),
                  failureSummary:(n.failureSummary||'').replace('Fix any of the following:\n','').replace('Fix all of the following:\n',''),
                };
              }),
              nodeCount:(v.nodes||[]).length,
            };
          });

          /* ── Merge custom-rule findings (CD Compliance catalog) ──
             Runs the 40-rule Konza catalog browser-side alongside
             axe-core. Plan-gated: Free Scan tier gets axe only;
             Starter+ and internal roles get the full catalog.
             Custom violations are tagged 'wpsb-custom' so the UI
             can differentiate them from axe-core findings. */
          var customCount = 0;
          if (shouldRunCustomChecks() && window.WPSB && window.WPSB.AdaCustomChecks) {
            try {
              var customResults = window.WPSB.AdaCustomChecks.runChecks(doc, { rawHtml: rawHtml });
              var mapped = (customResults || []).map(function(v) {
                return {
                  id: v.id,
                  impact: v.impact || 'moderate',
                  description: v.description || '',
                  help: v.help || '',
                  helpUrl: v.helpUrl || '',
                  tags: (v.tags || []).concat(['wpsb-custom']),
                  nodes: (v.nodes || []).slice(0, 3).map(function(n) {
                    return {
                      html: (n.html || '').substring(0, 200),
                      failureSummary: n.failureSummary || '',
                    };
                  }),
                  nodeCount: (v.nodes || []).length,
                  /* Extra metadata for report/score derivation */
                  wpsb_custom:        true,
                  wpsb_severity:      v.wpsb_severity || null,
                  wpsb_category:      v.wpsb_category || null,
                  wpsb_fqhc_priority: v.wpsb_fqhc_priority || false,
                };
              });
              viols = viols.concat(mapped);
              customCount = mapped.length;
              console.log('[ADA] axe-core:' + (results.violations||[]).length + ' + custom:' + customCount + ' = ' + viols.length + ' total violations');
            } catch (customErr) {
              console.warn('[ADA] Custom checks failed (axe-core results preserved):', customErr.message);
            }
          } else {
            console.log('[ADA] axe-core:' + (results.violations||[]).length + ' violations (custom checks not eligible for this plan tier)');
          }

          var passedAudits = (results.passes||[]).map(function(v) {
            return { id:v.id, help:v.help, description:v.description, helpUrl:v.helpUrl, nodeCount:(v.nodes||[]).length };
          });
          var notApplicable = (results.inapplicable||[]).map(function(v) {
            return { id:v.id, help:v.help, description:v.description };
          });

          var sum = { critical:0, serious:0, moderate:0, minor:0, total:viols.length };
          viols.forEach(function(v) { if (sum[v.impact] !== undefined) sum[v.impact]++; });
          sum.passed       = passedAudits.length;
          sum.inapplicable = notApplicable.length;
          sum.custom_count = customCount;

          /* Compliance score derivation — uses CD Compliance Framework
             bucketing (critical/warning/suggestion) which differs from
             axe's impact taxonomy. WPSB_Scanner is the SSOT helper.
             Score lands on summary so tab-ada UI + report + estimator
             all read the same number. */
          if (window.WPSB_Scanner && typeof window.WPSB_Scanner.deriveComplianceBreakdown === 'function') {
            var breakdown = window.WPSB_Scanner.deriveComplianceBreakdown(viols);
            sum.compliance_score      = breakdown.score;
            sum.compliance_critical   = breakdown.critical;
            sum.compliance_warning    = breakdown.warning;
            sum.compliance_suggestion = breakdown.suggestion;
          }

          /* Push to ProposalCart so Estimator + Proposal modules
             see the latest scan tokens without a re-fetch. Quiet
             on failure — ProposalCart isn't guaranteed loaded. */
          if (window.ProposalCart && typeof window.ProposalCart.setTokens === 'function') {
            try {
              window.ProposalCart.setTokens({
                site:        siteUrl,
                violations:  viols.length,
                score:       sum.compliance_score,
                pagesScanned: 1,
                pdfsFound:   0,
              });
            } catch (cartErr) {
              console.warn('[ADA] ProposalCart sync failed:', cartErr.message);
            }
          }

          setViolations(viols);
          setPasses(passedAudits);
          setInapplicable(notApplicable);
          setSummary(sum);
          setScannedUrl(siteUrl);
          setActiveTab('violations');
          setState('done');
        })
        .catch(function(e) {
          setErrMsg(e.message || 'Scan failed');
          setState('error');
        });
    }

    /* Score calculation */
    var score = null;
    if (summary) {
      var total = (summary.passed||0) + (summary.total||0);
      score = total > 0 ? Math.round((summary.passed||0) / total * 100) : 0;
    }

    var tabs = summary ? [
      { id:'violations',  label:'Critical Issues', count:summary.total||0,         color:'var(--red)' },
      { id:'passes',      label:'Passed Audits',   count:summary.passed||0,        color:'var(--green)' },
      { id:'manual',      label:'Manual Checks',   count:MANUAL_CHECKS.length,     color:'var(--warn)' },
      { id:'inapplicable',label:'Not Applicable',  count:summary.inapplicable||0,  color:'var(--dim)' },
    ] : [];

    return (
      <div style={{ background:'var(--surface2)', border:'1px solid var(--border)', borderRadius:10, padding:16, marginBottom:16 }}>
        <div style={{ display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom:state==='idle'?0:14 }}>
          <div>
            <div style={{ fontWeight:700, color:'var(--text)', fontSize:'.9rem' }}>Browser Accessibility Scan</div>
            <div style={{ fontSize:'.74rem', color:'var(--dim)', marginTop:3 }}>
              Runs axe-core WCAG 2.1 AA structural checks in your browser -- no server required.
            </div>
          </div>
          <button onClick={runScan} disabled={state==='loading'}
            style={{ padding:'8px 18px', borderRadius:7, fontWeight:700, fontSize:'.82rem',
              cursor: state==='loading' ? 'default' : 'pointer',
              background: state==='loading' ? 'var(--surface)' : 'var(--beam)',
              color: state==='loading' ? 'var(--dim)' : 'var(--bg)',
              border:'1px solid '+(state==='loading' ? 'var(--border)' : 'var(--beam)'),
              transition:'all .2s', flexShrink:0, marginLeft:16 }}>
            {state==='loading' ? 'Scanning...' : state==='done' ? 'Re-scan' : 'Run Scan'}
          </button>
        </div>

        {state === 'error' && (
          <div style={{ padding:'10px 14px', borderRadius:6, background:'var(--red-dim)',
            border:'1px solid var(--red)', color:'var(--red)', fontSize:'.8rem' }}>
            {errMsg}
          </div>
        )}

        {state === 'done' && summary && (
          <div>
            {/* Score + summary row */}
            <div style={{ display:'flex', gap:16, alignItems:'flex-start', marginBottom:16, flexWrap:'wrap' }}>
              {/* ── 4-ring score header (design review 2026-05-26) ──────────
                   Uses ScoreRing.jsx. Accessibility = this scan's compliance_score.
                   SEO/Tech/AI Readiness pulled from last full scanner payload
                   (window._lastScanPayload set by scanner.html on scan complete).
                   Falls back gracefully when full scan hasn't run. */}
              <div style={{ display:'flex', gap:16, flexWrap:'wrap', alignItems:'flex-end',
                padding:'12px 16px', background:'var(--surface)', border:'1px solid var(--border)',
                borderRadius:10 }}>
                {window.ScoreRing ? (
                  <>
                    <window.ScoreRing value={score !== null ? score : 0} label="Accessibility" size={72}/>
                    {(function() {
                      var payload = window._lastScanPayload || {};
                      var seoScore = payload.seo_score != null ? Math.round(payload.seo_score) : null;
                      var techScore = payload.tech_score != null ? Math.round(payload.tech_score) : null;
                      var aiScore = payload.ai_score != null ? Math.round(payload.ai_score) : null;
                      return (
                        <>
                          {seoScore !== null
                            ? <window.ScoreRing value={seoScore} label="SEO" size={56}/>
                            : <div style={{ display:'flex', flexDirection:'column', alignItems:'center', gap:4, opacity:.4 }}>
                                <div style={{ width:56, height:56, borderRadius:'50%', border:'3px solid var(--border)', display:'grid', placeItems:'center', fontSize:'.72rem', color:'var(--dim)' }}>—</div>
                                <span style={{ fontSize:'.62rem', color:'var(--dim)', textTransform:'uppercase', letterSpacing:'.03em', fontWeight:600 }}>SEO</span>
                              </div>
                          }
                          {techScore !== null
                            ? <window.ScoreRing value={techScore} label="Tech Stack" size={56}/>
                            : <div style={{ display:'flex', flexDirection:'column', alignItems:'center', gap:4, opacity:.4 }}>
                                <div style={{ width:56, height:56, borderRadius:'50%', border:'3px solid var(--border)', display:'grid', placeItems:'center', fontSize:'.72rem', color:'var(--dim)' }}>—</div>
                                <span style={{ fontSize:'.62rem', color:'var(--dim)', textTransform:'uppercase', letterSpacing:'.03em', fontWeight:600 }}>Tech Stack</span>
                              </div>
                          }
                          {aiScore !== null
                            ? <window.ScoreRing value={aiScore} label="AI Readiness" size={56}/>
                            : <div style={{ display:'flex', flexDirection:'column', alignItems:'center', gap:4, opacity:.4 }}>
                                <div style={{ width:56, height:56, borderRadius:'50%', border:'3px solid var(--border)', display:'grid', placeItems:'center', fontSize:'.72rem', color:'var(--dim)' }}>—</div>
                                <span style={{ fontSize:'.62rem', color:'var(--dim)', textTransform:'uppercase', letterSpacing:'.03em', fontWeight:600 }}>AI Readiness</span>
                              </div>
                          }
                        </>
                      );
                    })()}
                    <div style={{ fontSize:'.62rem', color:'var(--dim)', alignSelf:'flex-end', paddingBottom:4, lineHeight:1.5 }}>
                      {score < 90 ? 'ADA lawsuit risk elevated' : 'Low compliance risk'}
                      <br/>Run full scan for SEO + Tech scores
                    </div>
                  </>
                ) : (
                  /* Fallback: original single ring when ScoreRing not loaded */
                  score !== null && (
                    <div style={{ fontSize:'1.2rem', fontWeight:700, color: score >= 90 ? 'var(--green)' : score >= 70 ? 'var(--warn)' : 'var(--red)' }}>
                      {score}% Audit Score
                    </div>
                  )
                )}
              </div>
              <div style={{ flex:1, display:'flex', flexDirection:'column', gap:6, justifyContent:'center' }}>
                <div style={{ fontWeight:700, color:'var(--text)', fontSize:'.82rem' }}>{scannedUrl}</div>
                <div style={{ display:'flex', gap:8, flexWrap:'wrap' }}>
                  {['critical','serious','moderate','minor'].map(function(sev) {
                    var hasIssues = (summary[sev]||0) > 0;
                    return (
                      <div key={sev} style={{ display:'flex', gap:6, alignItems:'center', padding:'4px 11px',
                        borderRadius:20,
                        background: hasIssues ? SEV_PILL_BG[sev] : 'var(--surface)',
                        border: '1px solid '+(hasIssues ? SEV_PILL_BORDER[sev] : 'var(--border)') }}>
                        <span style={{ fontWeight:800, color: hasIssues ? SEV_PILL_FG[sev] : 'var(--dim)', fontSize:'.84rem' }}>{summary[sev]}</span>
                        <span style={{ color: hasIssues ? SEV_PILL_FG[sev] : 'var(--dim)', fontSize:'.66rem', textTransform:'uppercase', letterSpacing:'.06em', fontWeight:700 }}>{sev}</span>
                      </div>
                    );
                  })}
                </div>
                <div style={{ display:'flex', gap:8, flexWrap:'wrap' }}>
                  {violations.length > 0 && (
                    <button onClick={function() { exportViolationsCSV(violations, siteUrl); }}
                      style={{ padding:'6px 12px', background:'var(--surface)', border:'1px solid var(--border)',
                        borderRadius:6, color:'var(--dim)', fontSize:'.74rem', fontWeight:600, cursor:'pointer' }}>
                      Export CSV
                    </button>
                  )}
                  {window.ADAReportExport && violations.length > 0 && (
                    <window.ADAReportExport violations={violations} summary={summary}
                      siteUrl={siteUrl} scannedAt={new Date().toISOString()} />
                  )}
                  {/* Push findings to Estimator (Item 5 — quote auto-gen).
                      Uses window.WPSB_Scanner.generateAdaCartItems() to convert
                      violations into ProposalCart line items grouped by rule.
                      Each item carries hours estimate, low/high price, severity,
                      and source='ada-scan' so the existing Estimator quote flow
                      picks them up automatically. */}
                  {window.WPSB_Scanner && window.WPSB_Scanner.generateAdaCartItems && window.ProposalCart && violations.length > 0 && (
                    <button onClick={function() {
                      try {
                        var items = window.WPSB_Scanner.generateAdaCartItems(violations, null, 125);
                        if (!items.length) {
                          window.wpsbToast && window.wpsbToast('No findings eligible for Estimator import', 'warn');
                          return;
                        }
                        items.forEach(function(item) {
                          if (typeof window.ProposalCart.addItem === 'function') {
                            window.ProposalCart.addItem(item);
                          }
                        });
                        var hours = items.reduce(function(s, i) { return s + (i.hours||0); }, 0);
                        window.wpsbToast && window.wpsbToast(
                          'Pushed ' + items.length + ' finding' + (items.length>1?'s':'') + ' to Estimator (' + (Math.round(hours*10)/10) + ' hrs total)',
                          'ok'
                        );
                      } catch (err) {
                        console.error('[ADA] Push to Estimator failed:', err);
                        window.wpsbToast && window.wpsbToast('Push to Estimator failed: ' + err.message, 'warn');
                      }
                    }}
                      style={{ padding:'6px 12px', background:'var(--beam-dim)', border:'1px solid var(--beam)',
                        borderRadius:6, color:'var(--beam)', fontSize:'.74rem', fontWeight:600, cursor:'pointer' }}>
                      ⚡ Push to Estimator
                    </button>
                  )}
                </div>
              </div>
            </div>

            {/* Contrast checker always visible after scan */}
            <ContrastChecker />

            {/* Tab bar */}
            <div style={{ display:'flex', gap:4, marginBottom:14, borderBottom:'1px solid var(--border)', flexWrap:'wrap' }}>
              {tabs.map(function(t) {
                var isActive = activeTab === t.id;
                /* Solid pill style for count badges — WCAG-AA contrast.
                   Inactive tabs use neutral colors; active tab uses tab.color. */
                var countBg, countFg, countBorder;
                if (isActive) {
                  /* Map tab.color to corresponding SOLID pill palette.
                     2026-05-20: borders moved to theme-aware *-dim vars so
                     pills don't have invisible borders in light mode. Manual
                     tab solid bg + minor "else" fallback now use CSS vars
                     so badge count is readable in both themes. */
                  if (t.id === 'violations')       { countBg = 'var(--red)';     countFg = '#ffffff';   countBorder = 'var(--red-dim)';   }
                  else if (t.id === 'passes')      { countBg = 'var(--green)';   countFg = '#ffffff';   countBorder = 'var(--green-dim)'; }
                  else if (t.id === 'manual')      { countBg = 'var(--warn)';    countFg = '#0a0e1a';   countBorder = 'var(--warn-dim)';  }
                  else                              { countBg = 'var(--surface-3)'; countFg = 'var(--text)'; countBorder = 'var(--border)'; }
                } else {
                  countBg = 'var(--surface)';
                  countFg = 'var(--text)';
                  countBorder = 'var(--border)';
                }
                return (
                  <button key={t.id} onClick={function() { setActiveTab(t.id); }}
                    aria-label={t.label + ' (' + t.count + ')'}
                    style={{ padding:'8px 14px', background:'none', border:'none',
                      borderBottom: isActive ? '2px solid '+t.color : '2px solid transparent',
                      color: isActive ? t.color : 'var(--text)',
                      fontWeight: isActive ? 700 : 600,
                      fontSize:'.78rem', cursor:'pointer', display:'flex', gap:7, alignItems:'center',
                      transition:'all .15s', marginBottom:-1 }}>
                    {t.label}
                    <span style={{ padding:'2px 8px', borderRadius:10, fontSize:'.66rem', fontWeight:800,
                      background: countBg, color: countFg, border:'1px solid '+countBorder,
                      minWidth:18, textAlign:'center', lineHeight:1.2 }}>
                      {t.count}
                    </span>
                  </button>
                );
              })}
            </div>

            {/* Violations tab */}
            {activeTab === 'violations' && renderViolations(violations)}

            {/* Passed audits tab */}
            {activeTab === 'passes' && (
              <div>
                <div style={{ marginBottom:12, fontSize:'.78rem', color:'var(--dim)', lineHeight:1.5 }}>
                  {passes.length} WCAG rules passed on this page.
                </div>
                {passes.map(function(v, i) {
                  return (
                    <div key={i} style={{ display:'flex', gap:10, alignItems:'center', padding:'9px 12px',
                      background:'var(--green-dim)', border:'1px solid var(--green-dim)', borderRadius:6, marginBottom:6 }}>
                      <span style={{ color:'var(--green)', fontSize:'1rem', flexShrink:0 }}>✓</span>
                      <div style={{ flex:1 }}>
                        <div style={{ fontSize:'.8rem', fontWeight:600, color:'var(--text)' }}>{v.help}</div>
                        <div style={{ fontSize:'.68rem', fontFamily:'var(--font-mono)', color:'var(--dim)', marginTop:1 }}>{v.id}</div>
                      </div>
                      <a href={v.helpUrl} target="_blank" rel="noopener" style={{ fontSize:'.68rem', color:'var(--beam)' }}>WCAG</a>
                    </div>
                  );
                })}
              </div>
            )}

            {/* Manual checks tab */}
            {activeTab === 'manual' && (
              <div>
                <div style={{ marginBottom:14, padding:'10px 14px', background:'var(--warn-dim)',
                  border:'1px solid var(--warn-dim)', borderRadius:8, fontSize:'.78rem', color:'var(--dim)', lineHeight:1.6 }}>
                  <strong style={{ color:'var(--warn)' }}>Automated testing catches ~30-40% of issues.</strong> These
                  {' '}{MANUAL_CHECKS.length} manual checks require human review. Mark each resolved after verifying.
                </div>
                {MANUAL_CHECKS.map(function(check, i) {
                  return <ManualCheckItem key={i} check={check} />;
                })}
              </div>
            )}

            {/* Not applicable tab */}
            {activeTab === 'inapplicable' && (
              <div>
                <div style={{ marginBottom:12, fontSize:'.78rem', color:'var(--dim)', lineHeight:1.5 }}>
                  {inapplicable.length} WCAG rules were not applicable to this page.
                </div>
                {inapplicable.map(function(v, i) {
                  return (
                    <div key={i} style={{ display:'flex', gap:10, alignItems:'center', padding:'8px 12px',
                      background:'var(--surface2)', border:'1px solid var(--border)', borderRadius:5, marginBottom:4 }}>
                      <span style={{ color:'var(--dim)', flexShrink:0 }}>-</span>
                      <div style={{ flex:1 }}>
                        <div style={{ fontSize:'.78rem', color:'var(--muted, #c9d1d9)' }}>{v.help}</div>
                        <div style={{ fontSize:'.65rem', fontFamily:'var(--font-mono)', color:'var(--dim)', marginTop:1 }}>{v.id}</div>
                      </div>
                    </div>
                  );
                })}
              </div>
            )}

            <div style={{ fontSize:'.68rem', color:'var(--dim)', marginTop:10, lineHeight:1.5 }}>
              Color contrast excluded (requires rendered CSS). Powered by axe-core WCAG 2.1 AA.
            </div>
          </div>
        )}
      </div>
    );
  }

  /* ── Free tools grid ─────────────────────────────────────────── */
  function FreeToolsGrid({ siteUrl }) {
    var tools = [
      { name:'WebAIM WAVE', desc:'Visual page-by-page accessibility evaluation. Free, no account needed.',
        link:'https://wave.webaim.org/report#/'+encodeURIComponent(siteUrl||''), cta:'Open WAVE' },
      { name:'Google Lighthouse', desc:'Built into Chrome DevTools. Scores 0-100 with actionable fixes.',
        link:'https://developer.chrome.com/docs/lighthouse/accessibility/', cta:'How to run' },
      { name:'Axe DevTools', desc:'Browser extension by Deque -- most accurate WCAG 2.1/2.2 automated detection.',
        link:'https://www.deque.com/axe/devtools/', cta:'Get extension' },
      { name:'AChecker', desc:'WCAG 2.0/2.1 checker with detailed ARIA and HTML report. Free online tool.',
        link:'https://achecker.achecks.ca/', cta:'Run check' },
    ];
    return (
      <div style={{ maxWidth:700, margin:'16px auto 0' }}>
        <div style={{ fontWeight:700, color:'var(--dim)', fontSize:'.75rem', textTransform:'uppercase',
          letterSpacing:'.06em', marginBottom:10 }}>
          Free Accessibility Audit Tools
        </div>
        <div style={{ display:'grid', gridTemplateColumns:'repeat(auto-fill, minmax(260px, 1fr))', gap:10 }}>
          {tools.map(function(t) {
            return (
              <div key={t.name} style={{ padding:16, background:'var(--surface2)', border:'1px solid var(--border)', borderRadius:8 }}>
                <div style={{ fontWeight:700, color:'var(--text)', fontSize:'.86rem', marginBottom:6 }}>{t.name}</div>
                <div style={{ fontSize:'.76rem', color:'var(--dim)', lineHeight:1.5, marginBottom:10 }}>{t.desc}</div>
                <a href={t.link} target="_blank" rel="noopener"
                   style={{ fontSize:'.78rem', fontWeight:700, color:'var(--beam)', textDecoration:'none' }}>
                  {t.cta} →
                </a>
              </div>
            );
          })}
        </div>
      </div>
    );
  }

  /* ── statusPill helper ──────────────────────────────────────── */
  function statusPill(label, color) {
    return (
      <span style={{ display:'inline-block', padding:'2px 9px', borderRadius:4, fontSize:'.65rem',
        fontWeight:700, textTransform:'uppercase', letterSpacing:'.05em',
        background:color+'20', color:color, border:'1px solid '+color+'40' }}>
        {label}
      </span>
    );
  }

  /* ── sevBadge helper — solid pill style matching ada-pill design ── */
  function sevBadge(sev) {
    return (
      <span style={{ display:'inline-block', padding:'3px 9px', borderRadius:4, fontSize:'.62rem',
        fontWeight:800, textTransform:'uppercase', letterSpacing:'.05em',
        background: SEV_PILL_BG[sev] || 'var(--surface-3)',
        color:      SEV_PILL_FG[sev] || 'var(--text)',
        border:     '1px solid '+(SEV_PILL_BORDER[sev] || 'var(--border)') }}>
        {sev}
      </span>
    );
  }

  /* ══════════════════════════════════════════════════════════════
     Main ScannerAdaTab component
     ══════════════════════════════════════════════════════════════ */
  function ScannerAdaTab({ data }) {
    var _v1 = React.useState('violations');
    var view = _v1[0]; var setView = _v1[1];

    var cart = (window.useProposalCart && window.useProposalCart()) || { items:[], tokens:{} };
    var ada  = data && data.ada;

    function addToCart(item) {
      if (!window.ProposalCart) return;
      window.ProposalCart.addItem(Object.assign({}, item, { source:'ada-scan' }));
      window.wpsbToast && window.wpsbToast('Added to Proposal Cart: ' + item.label, 'ok');
    }

    /* ── No scan yet ── */
    if (!data || !data.site) {
      return <ScanEmptyState tab="accessibility" reason="Run a scan first to see accessibility audit results." />;
    }

    /* ── ADA mode was off ── */
    if (!ada) {
      return (
        <div style={{ padding:'0 0 24px' }}>
          <div style={{ maxWidth:540, margin:'32px auto', padding:24, background:'var(--surface2)',
            border:'1px solid var(--border)', borderRadius:12, textAlign:'center' }}>
            <div style={{ fontSize:'2rem', marginBottom:12 }}>🔒</div>
            <div style={{ fontWeight:700, color:'var(--text)', fontSize:'.96rem', marginBottom:8 }}>
              ADA mode was off for this scan
            </div>
            <div style={{ fontSize:'.8rem', color:'var(--dim)', lineHeight:1.6, marginBottom:16 }}>
              Re-run the scan with ADA mode enabled in Step 2 to get full accessibility audit results.
            </div>
          </div>
          <BrowserAxeScan siteUrl={data.siteUrl || ('https://' + data.site)} />
          <FreeToolsGrid siteUrl={data.siteUrl || ('https://' + data.site)} />
        </div>
      );
    }

    /* ── ADA ran but 0 pages scanned ── */
    var pagesScanned = (ada.executiveSummary && ada.executiveSummary.pagesScanned) || 0;
    if (pagesScanned === 0) {
      return (
        <div style={{ padding:'0 0 24px' }}>
          {/* Full-width notice using theme tokens — adapts to light/dark.
              var(--warn) = #f5b800 dark / #946300 light (AA compliant).
              var(--warn-dim) = 10% tint of warn color in current theme.
              Polished card feel with generous padding + larger radius. */}
          {/* 2026-05-18 v2 per Jordan: compact one-line notice + expandable
              "Why?" details. Was a full-screen yellow card before — moved the
              4 causes into a <details> so the BrowserAxeScan below gets prime
              real estate. */}
          <details style={{ width:'100%', margin:'8px 0 14px', padding:'10px 14px',
            background:'rgba(251, 191, 36, 0.06)', border:'1px solid rgba(251, 191, 36, 0.3)',
            borderLeft:'4px solid var(--warn)', borderRadius:8 }}>
            <summary style={{ cursor:'pointer', listStyle:'none', display:'flex', alignItems:'center', gap:10, fontSize:'.84rem' }}>
              <span style={{ color:'var(--warn)', fontSize:'1.1rem', lineHeight:1 }}>⚠</span>
              <strong style={{ color:'var(--warn)' }}>Server-side ADA scan returned 0 pages</strong>
              <span style={{ color:'var(--muted)', fontSize:'.76rem', marginLeft:'auto' }}>
                Browser scan below works regardless · click for causes
              </span>
            </summary>
            <div style={{ marginTop:10, paddingTop:10, borderTop:'1px solid rgba(251, 191, 36, 0.2)', fontSize:'.78rem', color:'var(--text-2, var(--text))', lineHeight:1.6 }}>
              <ul style={{ marginTop:0, marginBottom:0, paddingLeft:22 }}>
                <li style={{ marginBottom:5 }}><strong style={{ color:'var(--text)' }}>Security plugin or WAF blocking the scanner</strong> — Wordfence, Sucuri, iThemes Security, Cloudflare bot protection. Temporarily whitelist WPSiteBeam IPs.</li>
                <li style={{ marginBottom:5 }}><strong style={{ color:'var(--text)' }}>JavaScript-only rendering</strong> — sites built with React, Vue, or heavy AJAX. Browser scan below handles these.</li>
                <li style={{ marginBottom:5 }}><strong style={{ color:'var(--text)' }}>Site offline or DNS misconfigured</strong> — check the URL resolves and returns 200.</li>
                <li><strong style={{ color:'var(--text)' }}>Cloudflare challenge / rate limiting</strong> — add WPSiteBeam scanner IPs to allowlist.</li>
              </ul>
            </div>
          </details>
          <BrowserAxeScan siteUrl={data.siteUrl || ('https://' + data.site)} autoRun={true} />
          <FreeToolsGrid siteUrl={data.siteUrl || ('https://' + data.site)} />
        </div>
      );
    }

    /* ── Full Railway ada results ── */
    var totals   = ada.executiveSummary || ada.totals || {};
    var byPriority = ada.byPriority || [];

    /* Normalise Railway byPriority to same shape as browser violations */
    var railwayViols = Object.values(
      byPriority.reduce(function(acc, v) {
        if (!acc[v.rule]) {
          acc[v.rule] = { id:v.rule, impact:v.impact||'minor', help:v.help||v.rule,
            helpUrl:v.helpUrl||'', description:'', nodeCount:0, nodes:[], tags:v.wcag_criteria||[] };
        }
        acc[v.rule].nodeCount += (v.nodeCount||1);
        if (acc[v.rule].nodes.length < 3) {
          acc[v.rule].nodes = acc[v.rule].nodes.concat((v.nodes||[]).slice(0,1));
        }
        return acc;
      }, {})
    ).sort(function(a,b) {
      var o = {critical:0,serious:1,moderate:2,minor:3};
      return (o[a.impact]||3)-(o[b.impact]||3);
    });

    var railwaySummary = {
      critical: totals.critical||0, serious: totals.serious||0,
      moderate: totals.moderate||0, minor: totals.minor||0,
      total: totals.totalViolations||railwayViols.length, passed:0, inapplicable:0,
    };

    var views = [
      { id:'violations', label:'Violations',    icon:'shield'  },
      { id:'perpage',    label:'Per Page',       icon:'pages'   },
      { id:'details',    label:'Full Details',   icon:'list'    },
      { id:'manual',     label:'Manual Checks',  icon:'check'   },
      { id:'docs',       label:'Compliance',     icon:'file'    },
      { id:'thirdparty', label:'Overlay Tools',  icon:'support' },
    ];

    return (
      <div>
        {/* View switcher */}
        <div style={{ display:'flex', gap:6, marginBottom:16, flexWrap:'wrap' }}>
          {views.map(function(v) {
            return (
              <button key={v.id}
                className={'btn btn-sm ' + (view===v.id ? 'btn-primary' : 'btn-ghost')}
                onClick={function() { setView(v.id); }}>
                <Icon name={v.icon} size={12}/>{v.label}
              </button>
            );
          })}
          <div style={{ flex:1 }}/>
          <button className="btn btn-ghost btn-sm"
            onClick={function() { window.wpsbDownload && window.wpsbDownload(data.site+'-ada.json', JSON.stringify(ada, null, 2), 'application/json'); }}>
            <Icon name="download" size={12}/>JSON
          </button>
          {railwayViols.length > 0 && (
            <button className="btn btn-ghost btn-sm"
              onClick={function() { exportViolationsCSV(railwayViols, data.site); }}>
              <Icon name="download" size={12}/>CSV
            </button>
          )}
          {window.ADAReportExport && railwayViols.length > 0 && (
            <window.ADAReportExport violations={railwayViols} summary={railwaySummary}
              siteUrl={data.site} scannedAt={new Date().toISOString()} />
          )}
        </div>

        {/* Violations view */}
        {view === 'violations' && (
          <div>
            <ContrastChecker />
            {renderViolations(railwayViols)}
          </div>
        )}

        {view === 'manual' && (
          <div>
            <div style={{ marginBottom:14, padding:'10px 14px', background:'var(--warn-dim)',
              border:'1px solid var(--warn-dim)', borderRadius:8, fontSize:'.78rem', color:'var(--dim)', lineHeight:1.6 }}>
              <strong style={{ color:'var(--warn)' }}>22 manual checks</strong> -- require keyboard testing,
              screen reader usage, or visual inspection. Mark each as resolved once verified.
            </div>
            {MANUAL_CHECKS.map(function(check, i) {
              return <ManualCheckItem key={i} check={check} />;
            })}
          </div>
        )}

        {view === 'perpage'    && <ScannerAdaPerPageView ada={ada} sevBadge={sevBadge} />}
        {view === 'details'    && <ScannerAdaDetailsView ada={ada} statusPill={statusPill} addToCart={addToCart} />}
        {view === 'docs'       && <ScannerAdaComplianceDocsView data={data} ada={ada} />}
        {view === 'thirdparty' && <ScannerAdaThirdPartyView ada={ada} />}
      </div>
    );
  }

  window.ScannerAdaTab = ScannerAdaTab;
  window.HOW_TO_FIX = HOW_TO_FIX;

  /* ══════════════════════════════════════════════════════════════
     Sub-view stubs -- full implementations preserved below
     ══════════════════════════════════════════════════════════════ */

  function ScannerAdaProposalCart({ data, ada, cart, addToCart, setView }) {
    return (
      <div style={{ padding:16 }}>
        <div style={{ fontWeight:700, color:'var(--text)', marginBottom:8 }}>Proposal Cart</div>
        <div style={{ fontSize:'.8rem', color:'var(--dim)' }}>
          {(cart.items||[]).length === 0 ? 'No items in cart yet. Add violations from the Details view.' : cart.items.length + ' items in cart.'}
        </div>
      </div>
    );
  }
  window.ScannerAdaProposalCart = ScannerAdaProposalCart;

  function ScannerAdaDetailsView({ ada, statusPill, addToCart }) {
    var byPriority = (ada && ada.byPriority) || [];
    return (
      <div>
        <div style={{ fontWeight:700, color:'var(--text)', marginBottom:12 }}>
          Full Audit Details -- {byPriority.length} violation types
        </div>
        {renderViolations(byPriority.map(function(v) {
          return { id:v.rule||v.id, impact:v.impact, help:v.help, description:'', helpUrl:v.helpUrl,
                   nodeCount:v.nodeCount||1, nodes:v.nodes||[], tags:[] };
        }))}
      </div>
    );
  }
  window.ScannerAdaDetailsView = ScannerAdaDetailsView;

  function ScannerAdaPerPageView({ ada, sevBadge }) {
    var byPage = (ada && ada.byPage) || [];
    return (
      <div>
        <div style={{ fontWeight:700, color:'var(--text)', marginBottom:12 }}>
          Per-Page Results -- {byPage.length} pages scanned
        </div>
        {byPage.map(function(page, i) {
          return (
            <div key={i} style={{ padding:'12px 14px', background:'var(--surface2)',
              border:'1px solid var(--border)', borderRadius:8, marginBottom:8 }}>
              <div style={{ display:'flex', justifyContent:'space-between', alignItems:'center' }}>
                <div style={{ fontFamily:'var(--font-mono)', fontSize:'.76rem', color:'var(--text)' }}>
                  {(page.url||'').replace(/^https?:\/\/[^/]+/,'')||'/'}
                </div>
                <div style={{ display:'flex', gap:6 }}>
                  {sevBadge && sevBadge(page.worstImpact||'minor')}
                  <span style={{ fontSize:'.74rem', color:'var(--dim)' }}>{page.violationCount||0} issues</span>
                </div>
              </div>
            </div>
          );
        })}
        {byPage.length === 0 && (
          <div style={{ color:'var(--dim)', fontSize:'.8rem', padding:16, textAlign:'center' }}>
            No per-page data available.
          </div>
        )}
      </div>
    );
  }
  window.ScannerAdaPerPageView = ScannerAdaPerPageView;

  function ScannerAdaMonitoringView({ ada }) {
    return (
      <div style={{ padding:24, textAlign:'center', color:'var(--dim)' }}>
        <div style={{ fontSize:'1.5rem', marginBottom:8 }}>📅</div>
        <div style={{ fontWeight:700, color:'var(--text)', marginBottom:6 }}>Continuous Monitoring</div>
        <div style={{ fontSize:'.8rem', lineHeight:1.6 }}>Scheduled rescans with email alerts when new violations appear. Coming in Phase 2.</div>
      </div>
    );
  }
  window.ScannerAdaMonitoringView = ScannerAdaMonitoringView;

  function ScannerAdaComplianceDocsView({ data, ada }) {
    var domain = (data && data.site) || 'yoursite.com';
    var date   = new Date().toLocaleDateString('en-US', { year:'numeric', month:'long', day:'numeric' });
    var wcagLevel = 'WCAG 2.1 Level AA';
    return (
      <div>
        <div style={{ fontWeight:700, color:'var(--text)', fontSize:'1rem', marginBottom:16 }}>
          Compliance Documents
        </div>
        <div style={{ background:'var(--surface2)', border:'1px solid var(--border)', borderRadius:10, padding:20, marginBottom:16 }}>
          <div style={{ fontWeight:700, color:'var(--text)', marginBottom:8 }}>Accessibility Statement</div>
          <div style={{ fontSize:'.8rem', color:'var(--dim)', lineHeight:1.8, fontFamily:'Georgia, serif',
            background:'var(--bg)', border:'1px solid var(--border)', borderRadius:6, padding:16 }}>
            <strong>Accessibility Commitment</strong>
            <p style={{ margin:'8px 0' }}>{domain} is committed to ensuring digital accessibility for people with disabilities. We continually improve the user experience for everyone and apply relevant accessibility standards.</p>
            <strong>Conformance Status</strong>
            <p style={{ margin:'8px 0' }}>This website partially conforms to {wcagLevel} — the technical standard codified under HHS Section 504 (45 CFR 84.84, compliance dates May 11, 2027 / May 10, 2028 per HHS Interim Final Rule) and DOJ ADA Title II (28 CFR 35.200). Partial conformance means that some parts do not fully conform to the accessibility standard.</p>
            <strong>Technical Specifications</strong>
            <p style={{ margin:'8px 0' }}>Accessibility relies on the following technologies: HTML, CSS, JavaScript. The following technologies are relied upon for conformance: WAI-ARIA.</p>
            <strong>Assessment Approach</strong>
            <p style={{ margin:'8px 0' }}>This statement was prepared on {date}. It was last reviewed on {date}. Assessment was performed using automated tools (axe-core) and manual review.</p>
            <strong>Limitations</strong>
            <p style={{ margin:'8px 0' }}>Automated accessibility testing detects approximately 30–57% of WCAG violations. This statement reflects substantial conformance based on automated scanning combined with manual review. It is not a legal compliance certification.</p>
            <strong>Feedback &amp; Contact</strong>
            <p style={{ margin:'8px 0' }}>We welcome feedback on the accessibility of {domain}. Please contact us if you experience accessibility barriers.</p>
          </div>
          <button style={{ marginTop:12, padding:'8px 16px', background:'var(--surface)', border:'1px solid var(--border)',
            borderRadius:6, color:'var(--text)', fontSize:'.78rem', fontWeight:600, cursor:'pointer' }}
            onClick={function() {
              var el = document.createElement('a');
              el.href = 'data:text/plain;charset=utf-8,' + encodeURIComponent(document.querySelector('.compliance-statement')||'Copy statement text manually.');
              el.download = domain+'-accessibility-statement.txt'; el.click();
              window.wpsbToast && window.wpsbToast('Statement text copied -- paste to your website', 'ok');
            }}>
            Copy Statement Text
          </button>
        </div>
      </div>
    );
  }
  window.ScannerAdaComplianceDocsView = ScannerAdaComplianceDocsView;

  function ScannerAdaThirdPartyView({ ada }) {
    var overlays = [
      { name:'accessiBe', risk:'HIGH', desc:'AI overlay tool. Does not fix underlying code. FTC investigation pending. Often increases violations.' },
      { name:'UserWay',   risk:'HIGH', desc:'Widget-based overlay. Does not remediate source HTML. Frequently conflicts with existing ARIA attributes.' },
      { name:'AudioEye',  risk:'MEDIUM', desc:'Hybrid approach -- combines automated fixes with human auditing. Better than pure overlays but still not a substitute for real remediation.' },
      { name:'EqualWeb',  risk:'MEDIUM', desc:'AI-powered widget. Similar limitations to other overlays -- cosmetic fixes only.' },
    ];
    var riskColor = { HIGH:'var(--red)', MEDIUM:'var(--warn)', LOW:'var(--green)' };
    return (
      <div>
        <div style={{ marginBottom:16, padding:'12px 16px', background:'var(--red-dim)',
          border:'1px solid var(--red-dim)', borderRadius:8, fontSize:'.8rem', color:'var(--dim)', lineHeight:1.6 }}>
          <strong style={{ color:'var(--red)' }}>Warning:</strong> Accessibility overlay tools do not fix underlying code
          and are frequently cited in ADA lawsuits alongside the websites that use them. The only path to genuine
          compliance is fixing the source HTML, ARIA, and CSS.
        </div>
        {overlays.map(function(o) {
          return (
            <div key={o.name} style={{ padding:'12px 14px', background:'var(--surface2)',
              border:'1px solid var(--border)', borderRadius:8, marginBottom:8,
              display:'flex', gap:12, alignItems:'flex-start' }}>
              <span style={{ padding:'2px 8px', borderRadius:4, fontSize:'.65rem', fontWeight:800,
                textTransform:'uppercase', background:(riskColor[o.risk]||'var(--dim)')+'20',
                color:riskColor[o.risk]||'var(--dim)', flexShrink:0 }}>{o.risk}</span>
              <div>
                <div style={{ fontWeight:700, color:'var(--text)', fontSize:'.84rem', marginBottom:3 }}>{o.name}</div>
                <div style={{ fontSize:'.76rem', color:'var(--dim)', lineHeight:1.5 }}>{o.desc}</div>
              </div>
            </div>
          );
        })}
      </div>
    );
  }
  window.ScannerAdaThirdPartyView = ScannerAdaThirdPartyView;

})();
