/* WP Site Beam — Pages (Customer + SA views) */

/* ── Shared pieces ───────────────────────────────────────────── */

/* Accessible form field: wires <label htmlFor> + <input id> automatically,
   supports hint + error via aria-describedby/aria-invalid. Use everywhere
   to guarantee screen-reader-safe forms. */
function Field({ label, children, hint, error, id, required }) {
  const autoId = React.useId();
  const fid = id || autoId;
  const hintId = hint ? `${fid}-hint` : undefined;
  const errId = error ? `${fid}-err` : undefined;
  const describedBy = [hintId, errId].filter(Boolean).join(' ') || undefined;
  const child = React.Children.only(children);
  const enhanced = React.cloneElement(child, {
    id: fid,
    'aria-describedby': describedBy,
    'aria-invalid': error ? 'true' : undefined,
    'aria-required': required ? 'true' : undefined,
    required: required || child.props.required,
  });
  return (
    <div className={`field ${error ? 'has-error' : ''}`}>
      <label htmlFor={fid}>
        {label}
        {required && <span aria-hidden="true" className="field-req">*</span>}
      </label>
      {enhanced}
      {hint && !error && <div className="field-hint" id={hintId}>{hint}</div>}
      {error && <div className="field-err" id={errId} role="alert">{error}</div>}
    </div>
  );
}
window.Field = Field;

/* Compact field — tiny label, tight spacing. Used in gallery-style layouts
   where vertical real estate matters. Supports `span={2}` to span two cols. */
function CompactField({ label, children, span }) {
  return (
    <div style={{ display:'flex', flexDirection:'column', gap:3, minWidth:0, ...(span ? { gridColumn:`span ${span}` } : {}) }}>
      <div style={{ fontSize:'.64rem', color:'var(--muted)', fontFamily:'var(--font-mono)', textTransform:'uppercase', letterSpacing:'.3px' }}>{label}</div>
      <div className="compact-field-body" style={{ minWidth:0 }}>{children}</div>
    </div>
  );
}
window.CompactField = CompactField;

/* ── FONT PICKER ─────────────────────────────────────────────────
   Searchable dropdown of popular Google Fonts for brand pickers.
   - Lazy-loads Google Fonts CSS for rendered options so the dropdown
     previews in the actual typeface.
   - Accepts value + onChange; renders the current selection with its
     own live font-family for instant visual feedback.
   - Typing a custom name is still allowed — search matches, but if
     none match the user's value is preserved. */
const WPSB_GOOGLE_FONTS = [
  // Sans — most used
  { name:'Inter',           cat:'Sans' },
  { name:'Roboto',          cat:'Sans' },
  { name:'Open Sans',       cat:'Sans' },
  { name:'Montserrat',      cat:'Sans' },
  { name:'Lato',            cat:'Sans' },
  { name:'Poppins',         cat:'Sans' },
  { name:'Nunito',          cat:'Sans' },
  { name:'Nunito Sans',     cat:'Sans' },
  { name:'Work Sans',       cat:'Sans' },
  { name:'DM Sans',         cat:'Sans' },
  { name:'Manrope',         cat:'Sans' },
  { name:'Plus Jakarta Sans', cat:'Sans' },
  { name:'Rubik',           cat:'Sans' },
  { name:'Raleway',         cat:'Sans' },
  { name:'Oswald',          cat:'Sans' },
  { name:'Source Sans 3',   cat:'Sans' },
  { name:'IBM Plex Sans',   cat:'Sans' },
  { name:'Archivo',         cat:'Sans' },
  { name:'Urbanist',        cat:'Sans' },
  { name:'Barlow',          cat:'Sans' },
  { name:'Figtree',         cat:'Sans' },
  { name:'Outfit',          cat:'Sans' },
  { name:'Geist',           cat:'Sans' },
  // Serif
  { name:'Merriweather',    cat:'Serif' },
  { name:'Playfair Display',cat:'Serif' },
  { name:'Lora',            cat:'Serif' },
  { name:'PT Serif',        cat:'Serif' },
  { name:'Fraunces',        cat:'Serif' },
  { name:'DM Serif Display',cat:'Serif' },
  { name:'Cormorant Garamond', cat:'Serif' },
  { name:'Libre Baskerville', cat:'Serif' },
  { name:'EB Garamond',     cat:'Serif' },
  { name:'Source Serif 4',  cat:'Serif' },
  // Display / Brand
  { name:'Orbitron',        cat:'Display' },
  { name:'Bebas Neue',      cat:'Display' },
  { name:'Archivo Black',   cat:'Display' },
  { name:'Abril Fatface',   cat:'Display' },
  { name:'Unbounded',       cat:'Display' },
  { name:'Space Grotesk',   cat:'Display' },
  // Mono
  { name:'JetBrains Mono',  cat:'Mono' },
  { name:'IBM Plex Mono',   cat:'Mono' },
  { name:'Fira Code',       cat:'Mono' },
  { name:'Space Mono',      cat:'Mono' },
];
window.WPSB_GOOGLE_FONTS = WPSB_GOOGLE_FONTS;

/* Lazy-load a single Google Font family (400/700) if not already loaded. */
const _wpsbLoadedFonts = new Set();
function wpsbEnsureFont(name) {
  if (!name || _wpsbLoadedFonts.has(name)) return;
  _wpsbLoadedFonts.add(name);
  const link = document.createElement('link');
  link.rel = 'stylesheet';
  link.href = `https://fonts.googleapis.com/css2?family=${encodeURIComponent(name).replace(/%20/g,'+')}:wght@400;700&display=swap`;
  document.head.appendChild(link);
}
window.wpsbEnsureFont = wpsbEnsureFont;

function FontPicker({ value, onChange, placeholder = 'Choose font…', ariaLabel }) {
  const { useState, useRef, useEffect } = React;
  const [open, setOpen] = useState(false);
  const [q, setQ]       = useState('');
  const wrapRef = useRef(null);
  const inputRef = useRef(null);

  useEffect(() => { wpsbEnsureFont(value); }, [value]);

  // Close on outside click / Escape
  useEffect(() => {
    if (!open) return;
    const onDown = (e) => { if (wrapRef.current && !wrapRef.current.contains(e.target)) setOpen(false); };
    const onEsc  = (e) => { if (e.key === 'Escape') { setOpen(false); inputRef.current?.blur(); } };
    document.addEventListener('mousedown', onDown);
    document.addEventListener('keydown', onEsc);
    return () => { document.removeEventListener('mousedown', onDown); document.removeEventListener('keydown', onEsc); };
  }, [open]);

  const filtered = (q.trim() ? WPSB_GOOGLE_FONTS.filter(f => f.name.toLowerCase().includes(q.trim().toLowerCase())) : WPSB_GOOGLE_FONTS);
  const grouped = filtered.reduce((acc, f) => { (acc[f.cat] ||= []).push(f); return acc; }, {});

  const pick = (name) => {
    wpsbEnsureFont(name);
    onChange?.(name);
    setQ('');
    setOpen(false);
  };

  return (
    <div ref={wrapRef} style={{ position:'relative', minWidth:0 }}>
      <div style={{ position:'relative' }}>
        <input
          ref={inputRef}
          value={open ? q : (value || '')}
          placeholder={placeholder}
          aria-label={ariaLabel || 'Font picker'}
          onFocus={() => setOpen(true)}
          onChange={(e) => { setQ(e.target.value); setOpen(true); }}
          onKeyDown={(e) => {
            if (e.key === 'Enter' && filtered[0]) { e.preventDefault(); pick(filtered[0].name); }
          }}
          style={{
            fontFamily: value ? `'${value}', system-ui, sans-serif` : 'inherit',
            paddingRight: 26,
          }}
        />
        <span aria-hidden="true" style={{
          position:'absolute', right:8, top:'50%', transform:`translateY(-50%) rotate(${open?180:0}deg)`,
          transition:'transform .15s', fontSize:10, color:'var(--muted)', pointerEvents:'none',
        }}>▾</span>
      </div>
      {open && (
        <div role="listbox" aria-label="Google Fonts" style={{
          position:'absolute', zIndex:50, top:'calc(100% + 4px)', left:0, right:0,
          maxHeight:260, overflow:'auto',
          background:'var(--surface)', border:'1px solid var(--border2)', borderRadius:8,
          boxShadow:'0 12px 32px rgba(0,0,0,.35)',
          fontSize:'.82rem',
        }}>
          {Object.keys(grouped).length === 0 ? (
            <div style={{ padding:'10px 12px', color:'var(--dim)', fontSize:'.75rem' }}>
              No matches — "{q}" will be used as a custom font name.
            </div>
          ) : Object.entries(grouped).map(([cat, fonts]) => (
            <div key={cat}>
              <div style={{ padding:'6px 10px', fontSize:'.6rem', fontFamily:'var(--font-mono)',
                letterSpacing:'.08em', color:'var(--dim)', background:'var(--surface2)',
                position:'sticky', top:0, textTransform:'uppercase' }}>{cat}</div>
              {fonts.map(f => {
                const selected = f.name === value;
                return (
                  <button
                    key={f.name}
                    type="button"
                    role="option"
                    aria-selected={selected}
                    onMouseEnter={() => wpsbEnsureFont(f.name)}
                    onClick={() => pick(f.name)}
                    style={{
                      display:'flex', alignItems:'center', justifyContent:'space-between',
                      width:'100%', padding:'8px 12px', textAlign:'left', cursor:'pointer',
                      background: selected ? 'var(--orange-dim, var(--orange-dim))' : 'transparent',
                      color: selected ? 'var(--orange)' : 'var(--text)',
                      border:'none', borderBottom:'1px solid var(--border)',
                      fontFamily:`'${f.name}', system-ui, sans-serif`,
                      fontSize:'.88rem',
                    }}
                    onMouseOver={e => { if (!selected) e.currentTarget.style.background = 'var(--surface2)'; }}
                    onMouseOut ={e => { if (!selected) e.currentTarget.style.background = 'transparent'; }}
                  >
                    <span>{f.name}</span>
                    <span style={{ fontFamily:'var(--font-mono)', fontSize:'.64rem', color:'var(--dim)' }}>Aa 123</span>
                  </button>
                );
              })}
            </div>
          ))}
        </div>
      )}
    </div>
  );
}
window.FontPicker = FontPicker;

/* ── TOOLTIP ─────────────────────────────────────────────────────
   Hover/focus-activated description popup. Use on icon-only buttons
   and anywhere a function needs a richer explanation than a title attr.
   Usage:  <Tooltip text="Mark notice as read"><button>…</button></Tooltip>
*/
function Tooltip({ text, children, placement='top', delay=350 }) {
  const { useState, useRef, useEffect } = React;
  const [open, setOpen] = useState(false);
  const [coords, setCoords] = useState({ top:0, left:0 });
  const ref = useRef(null);
  const timer = useRef(null);

  const show = () => {
    clearTimeout(timer.current);
    timer.current = setTimeout(() => {
      const el = ref.current;
      if (!el) return;
      const r = el.getBoundingClientRect();
      const top = placement === 'top' ? r.top - 8 : r.bottom + 8;
      setCoords({ top, left: r.left + r.width/2 });
      setOpen(true);
    }, delay);
  };
  const hide = () => { clearTimeout(timer.current); setOpen(false); };

  useEffect(() => () => clearTimeout(timer.current), []);

  // Clone the single child so we can attach event handlers + ref without a wrapper span
  const child = React.Children.only(children);
  const attached = React.cloneElement(child, {
    ref,
    onMouseEnter: (e) => { show(); child.props.onMouseEnter?.(e); },
    onMouseLeave: (e) => { hide(); child.props.onMouseLeave?.(e); },
    onFocus:      (e) => { show(); child.props.onFocus?.(e); },
    onBlur:       (e) => { hide(); child.props.onBlur?.(e); },
    // Keep aria-describedby so SR reads the tooltip content
    'aria-describedby': open ? 'wpsbd-tooltip' : undefined,
  });

  return (
    <>
      {attached}
      {open && ReactDOM.createPortal(
        <div id="wpsbd-tooltip" role="tooltip" className={`wpsbd-tooltip wpsbd-tooltip-${placement}`}
             style={{ top: coords.top, left: coords.left }}>
          {text}
        </div>,
        document.body
      )}
    </>
  );
}
window.Tooltip = Tooltip;

/* Accessible progress bar. Pair with visible % label if desired. */
function Progress({ value, max = 100, tone = 'ok', label, width }) {
  const pct = Math.max(0, Math.min(100, (value / max) * 100));
  return (
    <div
      role="progressbar"
      aria-valuenow={Math.round(value)}
      aria-valuemin={0}
      aria-valuemax={max}
      aria-label={label || 'Progress'}
      className="progress"
      style={{ width }}
    >
      <div className="progress-fill" data-tone={tone} style={{ width: pct + '%' }} />
    </div>
  );
}
function PageHead({ crumb, title, sub, actions, sa }) {
  return (
    <div className="page-head">
      <div>
        {crumb && <div className={`crumb ${sa ? 'sa' : ''}`}>{crumb}</div>}
        <h1 className="page-title" id="page-title">{title}</h1>
        {sub && <p className="page-sub">{sub}</p>}
      </div>
      {actions && <div className="page-actions">{actions}</div>}
    </div>
  );
}

function Stat({ label, value, sub, trend, tone }) {
  return (
    <div className="stat">
      <div className="stat-lbl">{label}</div>
      <div className={`stat-val ${tone || ''}`}>{value}</div>
      {sub && <div className="stat-sub">{sub}</div>}
      {trend && <div className={`stat-trend ${trend.dir}`}>{trend.label}</div>}
    </div>
  );
}

/* ── DASHBOARD ───────────────────────────────────────────────── */
function Dashboard({ s }) {
  const r = window.WPSBD.ROLES[s.role];
  /* SECURITY (Phase A 2026-05-18): real user name comes from window.currentUser
     via WPSBD helper. Previously used `r.name` which was hardcoded "Jordan Davis". */
  const firstName = (window.WPSBD.getUserName ? window.WPSBD.getUserName() : 'there').split(' ')[0];
  return (
    <div>
      <PageHead
        crumb="Overview"
        title="Dashboard"
        sub={`Welcome back, ${firstName}. Here's what's happening across your sites.`}
        actions={<>
          <button className="btn btn-ghost btn-sm"><Icon name="search" size={14}/>Search</button>
          <button className="btn btn-primary btn-sm"><Icon name="plus" size={14}/>Add site</button>
        </>}
      />

      <div className="grid grid-4" style={{ marginBottom: 16 }}>
        <Stat label="Sites monitored" value="3"   sub="All online" tone="ok"   trend={{ dir: 'up',   label: '+1 this month' }}/>
        <Stat label="Avg health"      value="97%" sub="Across all sites"      trend={{ dir: 'up',   label: '+2.3%' }}/>
        <Stat label="Open issues"     value="4"   sub="2 warnings, 2 info"    trend={{ dir: 'down', label: '-3 from last week' }} tone="warn"/>
        <Stat label="Plugin version"  value="1.4.2" sub="1 site outdated"      trend={{ dir: 'flat', label: 'up to date' }}/>
      </div>

      <div className="grid grid-2">
        <div className="card">
          <div className="card-head"><h2 className="card-title">Recent activity</h2><button className="card-action">View all</button></div>
          <div className="card-body" style={{ padding: 0 }}>
            <div className="table-wrap" style={{ border: 'none', borderRadius: 0 }}>
              <table className="table">
                <thead><tr><th scope="col">Event</th><th scope="col">Site</th><th scope="col">When</th></tr></thead>
                <tbody>
                  <tr><td><span className="tag ok">SCAN</span> Weekly scan complete</td><td>acme.co</td><td>12m ago</td></tr>
                  <tr><td><span className="tag warn">WARN</span> Plugin update available</td><td>blog.acme.co</td><td>1h ago</td></tr>
                  <tr><td><span className="tag beam">AI</span> Brand audit ready</td><td>shop.acme.co</td><td>3h ago</td></tr>
                  <tr><td><span className="tag ok">OK</span> Backup complete</td><td>acme.co</td><td>yesterday</td></tr>
                </tbody>
              </table>
            </div>
          </div>
        </div>

        <div className="card">
          <div className="card-head"><h2 className="card-title">Site health</h2></div>
          <div className="card-body">
            {['acme.co (main)', 'blog.acme.co', 'shop.acme.co'].map((n, i) => (
              <div key={n} style={{ display:'flex', alignItems:'center', gap:12, padding:'10px 0', borderBottom: i<2 ? '1px solid var(--border)' : 'none' }}>
                <div style={{ width:8, height:8, borderRadius:'50%', background:'var(--green)', boxShadow:'0 0 6px var(--green-dim)' }}/>
                <div style={{ flex:1 }}>
                  <div style={{ fontWeight:600, fontSize:'.85rem' }}>{n}</div>
                  <div style={{ fontSize:'.72rem', color:'var(--dim)' }}>Last scan 12m ago · WP 6.5.2</div>
                </div>
                <div style={{ fontFamily: 'var(--font-mono)', fontSize:'.72rem', color: i === 1 ? 'var(--warn)' : 'var(--green)' }}>
                  {i === 1 ? '92%' : '98%'}
                </div>
              </div>
            ))}
          </div>
        </div>
      </div>

      {s.role === 'sa' && (
        <div className="box sa" style={{ marginTop: 16 }}>
          <Icon name="warn" size={16}/>
          <div><strong>SA mode on.</strong> You're viewing the customer dashboard with Super Admin privileges — any override is logged.</div>
        </div>
      )}
    </div>
  );
}

/* ── SITES ───────────────────────────────────────────────────── */
function Sites() {
  const { useState, useEffect, useRef, useMemo } = React;
  const [s] = window.useWPSBD();

  /* ── Real-data fetch from /dashboard/overview (2026-05-19) ──
     Was: hardcoded SITES array with acme.co / blog.acme.co / etc.
     Now: fetches actual connected sites. If user has zero connected sites,
     shows an explicit empty state with CTAs (install plugin / connect / scan)
     instead of misleading demo data. Demo data toggle available so user can
     preview the populated UI before connecting their own sites. */
  const [realSites, setRealSites] = useState(null);   /* null = loading; array = loaded */
  const [fetchErr, setFetchErr]   = useState(null);
  const [showDemo, setShowDemo]   = useState(false);

  useEffect(() => {
    const RAILWAY = (window.WPSBD && window.WPSBD.railwayUrl)
      || 'https://wpsitebeam-railway-api-production.up.railway.app';
    const token = (window.WPSB && window.WPSB.getToken && window.WPSB.getToken()) || window.currentToken;
    if (!token) { setFetchErr('Not authenticated'); setRealSites([]); return; }
    let cancelled = false;
    fetch(`${RAILWAY}/dashboard/overview`, { headers: { 'Authorization': 'Bearer ' + token } })
      .then(r => r.json().then(b => ({ ok: r.ok, body: b })))
      .then(({ ok, body }) => {
        if (cancelled) return;
        if (!ok) { setFetchErr(body.error || 'Fetch failed'); setRealSites([]); return; }
        /* Adapt /dashboard/overview shape → Sites-component shape.
           Real-side fields (plugin_version, last_connected_at, is_online)
           map cleanly. Fields we don't yet track (health, plugins count,
           updates, warnings, etc.) are nulls — UI shows "—" gracefully. */
        const adapted = (body.sites || []).map((site, i) => ({
          id:        site.id || `real-${i}`,
          name:      (function(){ try { return new URL(site.site_url).hostname; } catch(e) { return site.site_url; } })(),
          url:       site.site_url,
          plan:      null,                         /* TBD: cross-ref with account plan */
          health:    null,                          /* TBD: scan score */
          version:   site.plugin_version || null,
          php:       null,
          plugins:   null,
          updates:   null,
          warnings:  null,
          lastSync:  site.last_connected_at,
          status:    site.is_online ? 'online' : 'offline',
          platform:  'wp',                         /* sites table is always WP (plugin-connected) */
          ssl:       null,
          backupAge: null,
          _real:     true,
        }));
        setRealSites(adapted);
        setFetchErr(null);
      })
      .catch(e => { if (!cancelled) { setFetchErr(e.message); setRealSites([]); } });
    return () => { cancelled = true; };
  }, []);

  /* ── Sample/demo data shown when toggled OR when realSites is empty ── */
  const DEMO_SITES = [
    { id:'d1',  name:'acme.co',                url:'acme.co',                plan:'Agency',  health:98, version:'6.5.2', php:'8.2', plugins:24, updates:0,  warnings:0, lastSync:'2m ago',  status:'online',  platform:'wp',  ssl:true,  backupAge:1,  _demo:true },
    { id:'d2',  name:'blog.acme.co',           url:'blog.acme.co',           plan:'Growth',  health:82, version:'6.5.1', php:'8.1', plugins:18, updates:3,  warnings:2, lastSync:'4h ago',  status:'online',  platform:'wp',  ssl:true,  backupAge:3,  _demo:true },
    { id:'d3',  name:'shop.acme.co',           url:'shop.acme.co',           plan:'Agency',  health:96, version:'6.5.2', php:'8.2', plugins:31, updates:1,  warnings:1, lastSync:'12m ago', status:'online',  platform:'wp',  ssl:true,  backupAge:1,  _demo:true },
    { id:'d4',  name:'riverside-dental.com',   url:'riverside-dental.com',   plan:'Starter', health:91, version:'6.4.3', php:'8.0', plugins:14, updates:2,  warnings:1, lastSync:'1h ago',  status:'online',  platform:'wp',  ssl:true,  backupAge:2,  _demo:true },
    { id:'d5',  name:'mountainview-law.com',   url:'mountainview-law.com',   plan:'Growth',  health:74, version:'6.3.2', php:'7.4', plugins:22, updates:5,  warnings:3, lastSync:'6h ago',  status:'warn',    platform:'wp',  ssl:true,  backupAge:7,  _demo:true },
    { id:'d6',  name:'sunsetgrill.co',         url:'sunsetgrill.co',         plan:'Starter', health:95, version:'6.5.2', php:'8.2', plugins:9,  updates:0,  warnings:0, lastSync:'30m ago', status:'online',  platform:'wp',  ssl:true,  backupAge:1,  _demo:true },
    { id:'d7',  name:'lakeside-spa.net',       url:'lakeside-spa.net',       plan:'Growth',  health:88, version:'6.5.0', php:'8.1', plugins:16, updates:2,  warnings:1, lastSync:'2h ago',  status:'online',  platform:'wp',  ssl:true,  backupAge:4,  _demo:true },
    { id:'d8',  name:'techco-dev.io',          url:'techco-dev.io',          plan:'Agency',  health:61, version:'6.2.0', php:'7.4', plugins:40, updates:8,  warnings:4, lastSync:'1d ago',  status:'warn',    platform:'wp',  ssl:false, backupAge:14, _demo:true },
    { id:'d9',  name:'honorsp.com',            url:'honorsp.com',            plan:'Starter', health:99, version:null,    php:null,  plugins:0,  updates:0,  warnings:0, lastSync:'5m ago',  status:'online',  platform:'sq',  ssl:true,  backupAge:0,  _demo:true },
    { id:'d10', name:'wrendigitalmedia.com',   url:'wrendigitalmedia.com',   plan:'Starter', health:97, version:null,    php:null,  plugins:0,  updates:0,  warnings:0, lastSync:'10m ago', status:'online',  platform:'wix', ssl:true,  backupAge:0,  _demo:true },
  ];

  /* Decide what to show: real sites if any, demo only if toggled or no real sites yet. */
  const SITES = useMemo(() => {
    if (realSites === null) return [];                        /* still loading */
    if (realSites.length > 0 && !showDemo) return realSites;
    if (realSites.length === 0) return DEMO_SITES;            /* empty account → demo placeholders */
    return [...realSites, ...DEMO_SITES];                     /* toggle: show real + demo */
  }, [realSites, showDemo]);

  const isLoading       = realSites === null;
  const hasRealSites    = realSites !== null && realSites.length > 0;
  const isShowingDemoOnly = realSites !== null && realSites.length === 0;

  const [view, setView]           = useState('list');
  const [search, setSearch]       = useState('');
  const [filterStatus, setFilter] = useState('all');
  const [selectedId, setSelected] = useState(null);
  const [panelTab, setPanelTab]   = useState('overview');
  const [menuFor, setMenuFor]     = useState(null);
  const panelRef = useRef(null);

  const selected = SITES.find(s => s.id === selectedId) || null;

  /* Close menu on outside click */
  useEffect(() => {
    if (!menuFor) return;
    const close = () => setMenuFor(null);
    document.addEventListener('click', close);
    return () => document.removeEventListener('click', close);
  }, [menuFor]);

  /* ESC closes panel */
  useEffect(() => {
    const onKey = (e) => { if (e.key === 'Escape') setSelected(null); };
    document.addEventListener('keydown', onKey);
    return () => document.removeEventListener('keydown', onKey);
  }, []);

  /* Filtered sites */
  const filtered = useMemo(() => SITES.filter(st => {
    if (filterStatus === 'warn' && st.status === 'online' && st.warnings === 0) return false;
    if (filterStatus === 'error' && st.health >= 80) return false;
    if (search && !st.name.toLowerCase().includes(search.toLowerCase())) return false;
    return true;
  }), [search, filterStatus]);

  /* Aggregate stats — null-safe so missing health/warnings on real sites don't break the math. */
  const totalOk    = SITES.filter(s => s.status === 'online' && (s.warnings || 0) === 0).length;
  const totalWarn  = SITES.filter(s => (s.warnings || 0) > 0 || s.status === 'warn').length;
  const totalError = SITES.filter(s => s.health != null && s.health < 70).length;
  const sitesWithHealth = SITES.filter(s => s.health != null);
  const avgHealth  = sitesWithHealth.length > 0
    ? Math.round(sitesWithHealth.reduce((a, s) => a + s.health, 0) / sitesWithHealth.length)
    : null;

  /* Format last-sync. Real data comes as ISO timestamp; demo data is preformatted ("2m ago"). */
  function fmtLastSync(v) {
    if (!v) return '—';
    /* If it's already a relative string ("2m ago"), pass through */
    if (typeof v === 'string' && !/^\d{4}-\d{2}-\d{2}/.test(v)) return v;
    const ts = new Date(v).getTime();
    if (isNaN(ts)) return v;
    const sec = Math.floor((Date.now() - ts) / 1000);
    if (sec < 60)    return `${sec}s ago`;
    if (sec < 3600)  return `${Math.floor(sec/60)}m ago`;
    if (sec < 86400) return `${Math.floor(sec/3600)}h ago`;
    return `${Math.floor(sec/86400)}d ago`;
  }

  const SCAN_OPTIONS = [
    { id:'full',      label:'Full Health Check',      hint:'Uptime, SSL, WP core, plugins, DB, security', tab:'scanner' },
    { id:'brand',     label:'Brand & Content Scan',   hint:'Logo, colors, fonts, contact, social',         tab:'scanner' },
    { id:'seo',       label:'SEO + Content Audit',    hint:'Titles, meta, headings, Core Web Vitals',      tab:'seo' },
    { id:'migration', label:'Migration Pre-flight',   hint:'URLs, files, redirects, 404s',                 tab:'scanner' },
    { id:'security',  label:'Security & Firewall',    hint:'Known vulns, permissions, exposed files',      tab:'scanner' },
  ];

  function openSite(st) {
    setSelected(st.id);
    setPanelTab('overview');
  }

  function runScan(siteName, opt) {
    window.wpsbToast(`${opt.label} started for ${siteName}`, 'beam');
    if (opt.tab && window.WPSBD?.switchTab) setTimeout(() => window.WPSBD.switchTab(opt.tab), 400);
    setMenuFor(null);
  }

  /* ── Health colour helper ── */
  function hColor(n) {
    if (n >= 90) return 'var(--green)';
    if (n >= 75) return 'var(--warn)';
    return 'var(--danger, #ef4444)';
  }

  /* ── Platform badge ── */
  function PlatformBadge({ p }) {
    const map = { wp:'WordPress', sq:'Squarespace', wix:'Wix', webflow:'Webflow', other:'Other' };
    const color = p === 'wp' ? 'var(--beam)' : 'var(--dim)';
    return <span style={{ fontSize:'.6rem', fontWeight:700, padding:'2px 6px', borderRadius:3,
      background:`${color}18`, color, border:`1px solid ${color}33` }}>{map[p]||p}</span>;
  }

  /* ── WP Health Panel ── */
  function SitePanel({ site, panelTab, setPanelTab, onClose, onPrev, onNext }) {
    const isWp = site.platform === 'wp';
    /* WP-specific tabs hidden for Wix/Squarespace/etc. — those sites can
       only show Overview + SEO since plugin/security data requires plugin. */
    const TABS = isWp ? [
      { id:'overview', label:'Overview' },
      { id:'plugins',  label:'Plugins'  },
      { id:'seo',      label:'SEO'      },
      { id:'security', label:'Security' },
    ] : [
      { id:'overview', label:'Overview' },
      { id:'seo',      label:'SEO'      },
    ];
    /* If panelTab was set to a WP-only tab and the site is not WP, snap back to overview */
    React.useEffect(() => {
      if (!isWp && (panelTab === 'plugins' || panelTab === 'security')) setPanelTab('overview');
    }, [isWp, panelTab]);

    const healthColor = site.health != null ? hColor(site.health) : 'var(--dim)';

    return (
      <div ref={panelRef} style={{
        position:'sticky', top:14, width:420, flexShrink:0,
        background:'var(--surface)', border:'1px solid var(--border)',
        borderRadius:12, overflow:'hidden', maxHeight:'calc(100vh - 80px)',
        display:'flex', flexDirection:'column',
      }}>
        {/* Panel header */}
        <div style={{ padding:'14px 16px', borderBottom:'1px solid var(--border)',
          background:'var(--surface-2)', display:'flex', alignItems:'center', gap:10 }}>
          <div style={{ flex:1, minWidth:0 }}>
            <div style={{ fontWeight:700, fontSize:'.95rem', color:'var(--text)',
              overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap' }}>{site.name}</div>
            <div style={{ display:'flex', gap:6, alignItems:'center', marginTop:3 }}>
              <PlatformBadge p={site.platform} />
              <span style={{ fontSize:'.66rem', color:site.status==='online'?'var(--green)':'var(--warn)',
                fontFamily:'var(--font-mono)', fontWeight:600 }}>
                {site.status === 'online' ? '● ONLINE' : '● SYNCING'}
              </span>
              <span style={{ fontSize:'.66rem', color:'var(--dim)' }}>{fmtLastSync(site.lastSync)}</span>
            </div>
          </div>
          <div style={{ display:'flex', gap:4 }}>
            <button onClick={onPrev} style={{ width:26,height:26,borderRadius:5,border:'1px solid var(--border)',
              background:'transparent',cursor:'pointer',color:'var(--dim)',fontSize:14,lineHeight:1 }}>‹</button>
            <button onClick={onNext} style={{ width:26,height:26,borderRadius:5,border:'1px solid var(--border)',
              background:'transparent',cursor:'pointer',color:'var(--dim)',fontSize:14,lineHeight:1 }}>›</button>
            <button onClick={onClose} style={{ width:26,height:26,borderRadius:5,border:'1px solid var(--border)',
              background:'transparent',cursor:'pointer',color:'var(--dim)',fontSize:14,lineHeight:1 }}>×</button>
          </div>
        </div>

        {/* Header lastSync uses formatter so ISO timestamps render as "12m ago" */}
        {/* Health score hero — null-safe for sites without scan data */}
        <div style={{ padding:'14px 16px', borderBottom:'1px solid var(--border)',
          display:'flex', gap:16, alignItems:'center' }}>
          <div style={{ textAlign:'center', minWidth:64 }}>
            <div style={{ fontSize:'2rem', fontWeight:800, color:healthColor, lineHeight:1,
              fontFamily:'var(--font-mono)' }}>{site.health != null ? site.health : '—'}</div>
            <div style={{ fontSize:'.62rem', color:'var(--dim)', marginTop:2 }}>HEALTH SCORE</div>
          </div>
          <div style={{ flex:1, display:'grid', gridTemplateColumns:'1fr 1fr', gap:'6px 14px' }}>
            {[
              ...(isWp ? [
                ['WP Version', site.version || '—',  site.version && site.version < '6.5' ? 'var(--warn)' : 'var(--text)'],
                ['PHP',        site.php || '—',      site.php && site.php < '8.0' ? 'var(--warn)' : 'var(--text)'],
                ['Plugins',    site.plugins != null ? site.plugins : '—', 'var(--text)'],
                ['Updates',    site.updates != null ? site.updates : '—', (site.updates||0) > 0 ? 'var(--warn)' : 'var(--green)'],
              ] : [
                ['Platform',   site.platform,        'var(--text)'],
                ['Last seen',  fmtLastSync(site.lastSync), 'var(--text)'],
              ]),
              ['SSL', site.ssl == null ? '—' : (site.ssl ? 'Valid' : 'Missing'), site.ssl == null ? 'var(--dim)' : (site.ssl ? 'var(--green)' : 'var(--red)')],
              ['Backup', site.backupAge == null ? '—' : (site.backupAge === 0 ? 'N/A' : site.backupAge+'d ago'),
                site.backupAge == null ? 'var(--dim)' : (site.backupAge > 7 ? 'var(--red)' : site.backupAge > 3 ? 'var(--warn)' : 'var(--green)')],
            ].map(([label, val, color]) => (
              <div key={label}>
                <div style={{ fontSize:'.6rem', color:'var(--muted)', textTransform:'uppercase',
                  letterSpacing:'.05em' }}>{label}</div>
                <div style={{ fontSize:'.8rem', fontWeight:600, color }}>{val}</div>
              </div>
            ))}
          </div>
        </div>

        {/* Panel tabs */}
        <div style={{ display:'flex', borderBottom:'1px solid var(--border)', background:'var(--surface-2)' }}>
          {TABS.map(t => (
            <button key={t.id} onClick={() => setPanelTab(t.id)}
              style={{ flex:1, padding:'9px 4px', border:'none', cursor:'pointer', fontSize:'.72rem',
                fontWeight:panelTab===t.id?700:400, background:'transparent',
                color:panelTab===t.id?'var(--orange)':'var(--dim)',
                borderBottom:panelTab===t.id?'2px solid var(--orange)':'2px solid transparent' }}>
              {t.label}
            </button>
          ))}
        </div>

        {/* Panel content */}
        <div style={{ flex:1, overflowY:'auto', padding:'14px 16px' }}>
          {panelTab === 'overview' && (
            <div style={{ display:'flex', flexDirection:'column', gap:12 }}>
              {site.warnings > 0 && (
                <div style={{ padding:'10px 12px', background:'var(--orange-dim)',
                  border:'1px solid var(--orange-dim)', borderRadius:7, fontSize:'.78rem',
                  color:'var(--warn)', display:'flex', gap:8, alignItems:'center' }}>
                  <span>⚠</span>
                  <span>{site.warnings} warning{site.warnings>1?'s':''} require attention</span>
                </div>
              )}
              {site.updates > 0 && (
                <div style={{ padding:'10px 12px', background:'var(--beam-dim)',
                  border:'1px solid var(--beam-dim)', borderRadius:7, fontSize:'.78rem',
                  color:'var(--beam)' }}>
                  {site.updates} plugin update{site.updates>1?'s':''} available
                </div>
              )}
              <div style={{ display:'flex', flexDirection:'column', gap:6 }}>
                {[
                  { label:'Theme', val:'Elementor Pro', status:'ok' },
                  { label:'Uptime (30d)', val:'99.9%', status:'ok' },
                  { label:'Last scan', val:site.lastSync, status:'ok' },
                  { label:'Plan', val:site.plan, status:'ok' },
                ].map(row => (
                  <div key={row.label} style={{ display:'flex', justifyContent:'space-between',
                    padding:'7px 0', borderBottom:'1px solid var(--border)', fontSize:'.8rem' }}>
                    <span style={{ color:'var(--dim)' }}>{row.label}</span>
                    <span style={{ fontWeight:500 }}>{row.val}</span>
                  </div>
                ))}
              </div>
              <div style={{ display:'flex', gap:7, marginTop:4 }}>
                <button className="btn btn-primary btn-sm" style={{ flex:1, justifyContent:'center' }}
                  onClick={() => window.wpsbToast('Full scan started for '+site.name, 'beam')}>
                  <Icon name="scanner" size={12}/>Run full scan
                </button>
                <button className="btn btn-ghost btn-sm"
                  onClick={() => window.open('https://'+site.url,'_blank','noopener')}>
                  Visit <Icon name="external" size={10}/>
                </button>
              </div>
            </div>
          )}
          {panelTab === 'plugins' && (
            <div style={{ display:'flex', flexDirection:'column', gap:0 }}>
              <div style={{ fontSize:'.72rem', color:'var(--dim)', marginBottom:10 }}>
                {site.plugins} plugins installed · {site.updates} updates available
              </div>
              {[
                { name:'Elementor Pro', version:'3.21.0', update:false, status:'ok' },
                { name:'WooCommerce',   version:'8.2.1',  update:true,  status:'warn' },
                { name:'Yoast SEO',     version:'21.5',   update:false, status:'ok' },
                { name:'WPSiteBeam',    version:'1.4.1',  update:false, status:'ok' },
                { name:'Wordfence',     version:'7.10.0', update:true,  status:'warn' },
              ].map(p => (
                <div key={p.name} style={{ display:'flex', alignItems:'center', gap:8, padding:'8px 0',
                  borderBottom:'1px solid var(--border)', fontSize:'.8rem' }}>
                  <span style={{ width:7, height:7, borderRadius:'50%', flexShrink:0,
                    background:p.update?'var(--warn)':'var(--green)' }}/>
                  <span style={{ flex:1 }}>{p.name}</span>
                  <span style={{ fontSize:'.7rem', color:'var(--dim)', fontFamily:'var(--font-mono)' }}>
                    {p.version}
                  </span>
                  {p.update && <span style={{ fontSize:'.6rem', color:'var(--warn)', fontWeight:700 }}>UPDATE</span>}
                </div>
              ))}
              <div style={{ fontSize:'.7rem', color:'var(--dim)', marginTop:8, textAlign:'center' }}>
                + {Math.max(0, site.plugins - 5)} more plugins
              </div>
            </div>
          )}
          {panelTab === 'seo' && (
            <div style={{ display:'flex', flexDirection:'column', gap:8 }}>
              {[
                { label:'Title tag',         val:'Present', ok:true  },
                { label:'Meta description',  val:'Present', ok:true  },
                { label:'Canonical URL',     val:'Set',     ok:true  },
                { label:'XML Sitemap',       val:'Found',   ok:true  },
                { label:'robots.txt',        val:'OK',      ok:true  },
                { label:'Core Web Vitals',   val:'Needs review', ok:false },
                { label:'Schema markup',     val:'Partial', ok:false },
              ].map(row => (
                <div key={row.label} style={{ display:'flex', justifyContent:'space-between',
                  padding:'7px 0', borderBottom:'1px solid var(--border)', fontSize:'.8rem', alignItems:'center' }}>
                  <span style={{ color:'var(--dim)' }}>{row.label}</span>
                  <span style={{ color:row.ok?'var(--green)':'var(--warn)', fontWeight:500, fontSize:'.75rem' }}>
                    {row.ok ? '✓ ' : '⚠ '}{row.val}
                  </span>
                </div>
              ))}
            </div>
          )}
          {panelTab === 'security' && (
            <div style={{ display:'flex', flexDirection:'column', gap:8 }}>
              {[
                { label:'SSL Certificate',   val:site.ssl?'Valid':'Missing',          ok:site.ssl },
                { label:'WP Core',           val:site.version>='6.5'?'Up to date':'Outdated', ok:site.version>='6.5' },
                { label:'Admin user',        val:'Non-default username', ok:true  },
                { label:'File permissions',  val:'Correct',              ok:true  },
                { label:'Login protection',  val:'Active',               ok:true  },
                { label:'WP debug mode',     val:'Off',                  ok:true  },
                { label:'Exposed config',    val:'No exposure',          ok:true  },
              ].map(row => (
                <div key={row.label} style={{ display:'flex', justifyContent:'space-between',
                  padding:'7px 0', borderBottom:'1px solid var(--border)', fontSize:'.8rem', alignItems:'center' }}>
                  <span style={{ color:'var(--dim)' }}>{row.label}</span>
                  <span style={{ color:row.ok?'var(--green)':'var(--red)', fontWeight:500, fontSize:'.75rem' }}>
                    {row.ok ? '✓ ' : '✗ '}{row.val}
                  </span>
                </div>
              ))}
            </div>
          )}
        </div>

        {/* Panel footer */}
        <div style={{ padding:'10px 16px', borderTop:'1px solid var(--border)',
          background:'var(--surface-2)', fontSize:'.72rem', color:'var(--dim)',
          display:'flex', justifyContent:'space-between', alignItems:'center' }}>
          <span>Last full scan: {site.lastSync}</span>
          <button className="btn btn-ghost btn-sm" style={{ fontSize:'.68rem' }}
            onClick={() => window.wpsbToast('Full WP Health report opened','beam')}>
            Full report →
          </button>
        </div>
      </div>
    );
  }

  /* ── Grid card ── */
  function SiteCard({ st }) {
    const active = selectedId === st.id;
    const isWp   = st.platform === 'wp';
    const isDemo = !!st._demo;
    return (
      <div className="card" style={{ cursor:'pointer',
        border: active ? '1px solid var(--orange-dim)' : undefined,
        boxShadow: active ? '0 0 0 2px var(--orange-dim)' : undefined,
        opacity: isDemo ? 0.7 : 1 }}
        onClick={() => openSite(st)}>
        <div className="card-body" style={{ display:'flex', flexDirection:'column', gap:12 }}>
          <div style={{ display:'flex', alignItems:'center', gap:10, minHeight:44 }}>
            <div style={{ width:36, height:36, borderRadius:8, background:'var(--surface-3)',
              border:'1px solid var(--border)', display:'flex', alignItems:'center', justifyContent:'center',
              color:'var(--beam)', flexShrink:0 }}>
              <Icon name="globe" size={18}/>
            </div>
            <div style={{ flex:1, minWidth:0 }}>
              <div style={{ fontWeight:700, fontSize:'.9rem', overflow:'hidden',
                textOverflow:'ellipsis', whiteSpace:'nowrap', color:'var(--text)',
                display:'flex', alignItems:'center', gap:4 }}>
                {st.name}
                {isDemo && <span style={{ fontSize:'.55rem', fontWeight:700, padding:'1px 5px', borderRadius:2,
                  background:'var(--dim)', color:'var(--surface)', letterSpacing:'.05em' }}>DEMO</span>}
                {(st.warnings||0) > 0 && <span style={{ fontSize:'.6rem', color:'var(--warn)', fontWeight:700 }}>⚠{st.warnings}</span>}
              </div>
              <div style={{ display:'flex', gap:5, marginTop:2, alignItems:'center' }}>
                <PlatformBadge p={st.platform} />
                {isWp && st.version && <span style={{ fontSize:'.66rem', color:'var(--dim)' }}>WP {st.version}</span>}
                {!isWp && <span style={{ fontSize:'.66rem', color:'var(--dim)' }}>External</span>}
              </div>
            </div>
            {st.health != null ? (
              <span style={{ fontSize:'.62rem', fontWeight:700, color:hColor(st.health),
                fontFamily:'var(--font-mono)' }}>{st.health}%</span>
            ) : (
              <span style={{ fontSize:'.62rem', color:'var(--dim)', fontFamily:'var(--font-mono)' }}>—</span>
            )}
          </div>
          <div style={{ display:'flex', alignItems:'center', gap:8, minHeight:22 }}>
            {st.health != null ? (
              <Progress value={st.health} tone={(st.warnings||0)>0?'warn':'ok'} label={`${st.name} health`}/>
            ) : (
              <div style={{ flex:1, fontSize:'.7rem', color:'var(--dim)', textAlign:'center', padding:'4px 0' }}>
                {isDemo ? 'sample data' : 'No scan yet — run one below'}
              </div>
            )}
          </div>
          <div style={{ display:'flex', gap:6, position:'relative' }}>
            {isWp ? (
              <button className="btn btn-ghost btn-sm" style={{ flex:1 }}
                disabled={isDemo}
                title={isDemo ? 'Sample data — connect a real site to enable' : 'Open WP Health details'}
                onClick={e => { e.stopPropagation(); if (!isDemo) openSite(st); }}>
                <Icon name="shield" size={12}/>WP Health
              </button>
            ) : (
              <button className="btn btn-ghost btn-sm" style={{ flex:1 }}
                disabled={isDemo}
                title={isDemo ? 'Sample data' : `${st.platform} sites use Overview only — install WP plugin for Health checks`}
                onClick={e => { e.stopPropagation(); if (!isDemo) openSite(st); }}>
                <Icon name="globe" size={12}/>Overview
              </button>
            )}
            <button className="btn btn-ghost btn-sm"
              aria-label={`Scan ${st.name}`} aria-haspopup="menu" aria-expanded={menuFor===st.name}
              disabled={isDemo}
              onClick={e => { e.stopPropagation(); if (!isDemo) setMenuFor(menuFor===st.name?null:st.name); }}>
              <Icon name="scanner" size={12}/>Scan<Icon name="chevron-down" size={10}/>
            </button>
            <button className="btn btn-ghost btn-sm"
              onClick={e => { e.stopPropagation(); window.open('https://'+st.url,'_blank','noopener'); }}>
              <Icon name="external" size={10}/>
            </button>
            {menuFor === st.name && !isDemo && (
              <div role="menu" onClick={e=>e.stopPropagation()}
                style={{ position:'absolute', top:'calc(100% + 4px)', left:0, right:0,
                  background:'var(--surface-2)', border:'1px solid var(--border)',
                  borderRadius:8, boxShadow:'0 8px 24px rgba(0,0,0,.3)',
                  zIndex:40, padding:6, display:'flex', flexDirection:'column', gap:2 }}>
                <div className="mono" style={{ fontSize:'.58rem', color:'var(--dim)', padding:'4px 8px' }}>
                  RUN SCAN ON {st.name.toUpperCase()}
                </div>
                {SCAN_OPTIONS.map(opt => (
                  <button key={opt.id} role="menuitem" onClick={() => runScan(st.name, opt)}
                    className="btn btn-ghost btn-sm"
                    style={{ justifyContent:'flex-start', padding:'6px 8px', borderRadius:4,
                      borderColor:'transparent', flexDirection:'column', alignItems:'flex-start', gap:1 }}>
                    <div style={{ display:'flex', alignItems:'center', gap:6, fontSize:'.78rem', color:'var(--text)' }}>
                      <span className="tag" style={{ fontSize:'.54rem', padding:'1px 5px' }}>{opt.id.toUpperCase()}</span>
                      {opt.label}
                    </div>
                    <div style={{ fontSize:'.66rem', color:'var(--dim)', marginLeft:44 }}>{opt.hint}</div>
                  </button>
                ))}
              </div>
            )}
          </div>
        </div>
      </div>
    );
  }

  /* ── Compact list row ── */
  function SiteRow({ st }) {
    const active = selectedId === st.id;
    const isWp   = st.platform === 'wp';
    const isDemo = !!st._demo;
    return (
      <div onClick={() => openSite(st)}
        style={{ display:'flex', alignItems:'center', gap:10, padding:'9px 14px',
          cursor:'pointer', borderBottom:'1px solid var(--border)',
          background: active ? 'var(--orange-dim)' : 'transparent',
          borderLeft: active ? '3px solid var(--orange)' : '3px solid transparent',
          opacity: isDemo ? 0.7 : 1,
          transition:'background .12s' }}>
        {window.SiteFavicon ? <window.SiteFavicon domain={st.url||st.name} size={24} radius={5}/> : null}
        {/* Status dot (fallback) */}
        <span style={{ width:7, height:7, borderRadius:'50%', flexShrink:0,
          background:st.status==='online'&&(st.warnings||0)===0?'var(--green)':st.status==='warn'||(st.warnings||0)>0?'var(--warn)':'var(--red)' }}/>
        {/* Site name */}
        <div style={{ flex:'0 0 200px', minWidth:0 }}>
          <div style={{ fontWeight:600, fontSize:'.82rem', color:'var(--text)',
            overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap',
            display:'flex', alignItems:'center', gap:6 }}>
            {st.name}
            {isDemo && <span style={{ fontSize:'.55rem', fontWeight:700, padding:'1px 5px', borderRadius:2,
              background:'var(--dim)', color:'var(--surface)', letterSpacing:'.05em' }}>DEMO</span>}
          </div>
        </div>
        {/* Platform */}
        <div style={{ flex:'0 0 90px' }}>
          <PlatformBadge p={st.platform} />
        </div>
        {/* WP version */}
        <div style={{ flex:'0 0 70px', fontSize:'.75rem', color:'var(--dim)', fontFamily:'var(--font-mono)' }}>
          {isWp ? (st.version || '—') : '—'}
        </div>
        {/* Health */}
        <div style={{ flex:'0 0 60px', display:'flex', alignItems:'center', gap:6 }}>
          {st.health != null ? (
            <span style={{ fontSize:'.82rem', fontWeight:700, color:hColor(st.health),
              fontFamily:'var(--font-mono)' }}>{st.health}%</span>
          ) : (
            <span style={{ fontSize:'.75rem', color:'var(--dim)' }}>—</span>
          )}
        </div>
        {/* Plugins */}
        <div style={{ flex:'0 0 70px', fontSize:'.75rem', color:'var(--dim)' }}>
          {isWp && st.plugins != null ? `${st.plugins} plugins` : '—'}
        </div>
        {/* Warnings */}
        <div style={{ flex:'0 0 80px', fontSize:'.75rem',
          color:(st.warnings||0)>0?'var(--warn)':'var(--muted)', fontWeight:(st.warnings||0)>0?600:400 }}>
          {(st.warnings||0) > 0 ? `⚠ ${st.warnings} warn` : ((st.updates||0) > 0 ? `${st.updates} updates` : '—')}
        </div>
        {/* Last sync */}
        <div style={{ flex:'0 0 80px', fontSize:'.72rem', color:'var(--dim)' }}>{fmtLastSync(st.lastSync)}</div>
        {/* Actions — WP Health hidden for non-WP platforms */}
        <div style={{ marginLeft:'auto', display:'flex', gap:5 }} onClick={e=>e.stopPropagation()}>
          {isWp && (
            <button className="btn btn-ghost btn-sm" style={{ fontSize:'.68rem' }}
              onClick={() => openSite(st)} disabled={isDemo} title={isDemo ? 'Sample data — connect a real site to enable' : 'Open WP Health details'}>
              WP Health
            </button>
          )}
          {!isWp && (
            <button className="btn btn-ghost btn-sm" style={{ fontSize:'.68rem' }}
              onClick={() => openSite(st)} disabled={isDemo} title={isDemo ? 'Sample data' : 'Open site overview'}>
              Overview
            </button>
          )}
          <button className="btn btn-ghost btn-sm" style={{ fontSize:'.68rem' }}
            onClick={() => window.open('https://'+st.url,'_blank','noopener')}>
            <Icon name="external" size={10}/>
          </button>
        </div>
      </div>
    );
  }

  /* Navigate panel between sites */
  const filteredIds = filtered.map(s => s.id);
  const curIdx = filteredIds.indexOf(selectedId);
  function prevSite() {
    const i = curIdx > 0 ? curIdx - 1 : filteredIds.length - 1;
    setSelected(filteredIds[i]);
  }
  function nextSite() {
    const i = curIdx < filteredIds.length - 1 ? curIdx + 1 : 0;
    setSelected(filteredIds[i]);
  }

  return (
    <div>
      <PageHead crumb="Overview" title="Sites"
        sub={
          isLoading ? 'Loading your connected sites…' :
          hasRealSites ? 'Manage connected sites and run scans. Click any site to open WP Health details.' :
          'No sites connected yet. The list below shows sample data so you can preview the populated view.'
        }
        actions={<button className="btn btn-primary btn-sm" onClick={() => window.wpsbToast('Connect site dialog — install the WPSB plugin on a site, then log in there to connect it.', 'beam')}><Icon name="plus" size={14}/>Add site</button>}
      />

      {/* Fetch error banner */}
      {fetchErr && (
        <div className="box warn" style={{ marginBottom: 12, fontSize: '.82rem' }}>
          Could not load your sites: {fetchErr}. Showing sample data.
        </div>
      )}

      {/* Empty-state CTA banner — shown when account has zero connected sites.
          Lists the three actions that populate this page: install plugin,
          connect a site, run a free scan. */}
      {isShowingDemoOnly && !fetchErr && (
        <div className="card" style={{
          padding: '16px 18px', marginBottom: 14,
          background: 'linear-gradient(135deg, var(--surface) 0%, var(--surface-2, var(--surface)) 100%)',
          border: '1px solid var(--orange-dim)', borderLeftWidth: 4, borderLeftColor: 'var(--orange)',
        }}>
          <div style={{ display:'flex', alignItems:'flex-start', gap:14, flexWrap:'wrap' }}>
            <div style={{ flex:'1 1 280px', minWidth:0 }}>
              <div style={{ fontWeight:700, fontSize:'.95rem', marginBottom:4, display:'flex', alignItems:'center', gap:8 }}>
                <Icon name="rocket" size={16}/> Welcome — your Sites page is empty
              </div>
              <div style={{ fontSize:'.82rem', color:'var(--dim)', lineHeight:1.5 }}>
                The rows below are <strong style={{ color:'var(--text)' }}>sample data</strong> showing what this page looks like populated. To get real data flowing, do any of the following:
              </div>
            </div>
            <div style={{ display:'flex', gap:8, flexWrap:'wrap' }}>
              <button className="btn btn-primary btn-sm"
                onClick={() => window.wpsbToast('Plugin download — visit /downloads or your account billing page', 'beam')}
                style={{ fontSize:'.75rem' }}>
                <Icon name="download" size={12}/> Install plugin
              </button>
              <button className="btn btn-ghost btn-sm"
                onClick={() => window.WPSBD?.switchTab?.('scanner')}
                style={{ fontSize:'.75rem' }}>
                <Icon name="search" size={12}/> Run a free scan
              </button>
              <button className="btn btn-ghost btn-sm"
                onClick={() => window.wpsbToast('Connect site — install the WPSB plugin on a WordPress site and log in there', 'info')}
                style={{ fontSize:'.75rem' }}>
                <Icon name="link" size={12}/> Connect a site
              </button>
            </div>
          </div>
        </div>
      )}

      {/* Demo toggle — only shown when user has real sites + wants to peek at the populated UI */}
      {hasRealSites && (
        <div style={{ marginBottom: 12, fontSize: '.75rem', color: 'var(--dim)', display:'flex', alignItems:'center', gap:8 }}>
          <label style={{ display:'flex', alignItems:'center', gap:6, cursor:'pointer' }}>
            <input type="checkbox" checked={showDemo} onChange={e => setShowDemo(e.target.checked)}/>
            Append sample sites for preview
          </label>
          {showDemo && <span style={{ color:'var(--warn)' }}>· {DEMO_SITES.length} demo rows added</span>}
        </div>
      )}

      {/* Aggregate stats bar */}
      <div style={{ display:'flex', gap:8, marginBottom:16, flexWrap:'wrap' }}>
        {[
          { label:'Total sites',    val:SITES.length,  color:'var(--text)' },
          { label:'All clear',      val:totalOk,       color:'var(--green)' },
          { label:'Warnings',       val:totalWarn,     color:'var(--warn)' },
          { label:'Critical',       val:totalError,    color:'var(--red)' },
          { label:'Avg health',     val:avgHealth != null ? avgHealth+'%' : '—', color:avgHealth != null ? hColor(avgHealth) : 'var(--dim)' },
        ].map(stat => (
          <div key={stat.label} style={{ padding:'8px 14px', background:'var(--surface)',
            border:'1px solid var(--border)', borderRadius:8, display:'flex', alignItems:'center', gap:8 }}>
            <span style={{ fontSize:'1.1rem', fontWeight:800, color:stat.color,
              fontFamily:'var(--font-mono)' }}>{stat.val}</span>
            <span style={{ fontSize:'.72rem', color:'var(--dim)' }}>{stat.label}</span>
          </div>
        ))}
      </div>

      {/* Search + filter + view toggle */}
      <div style={{ display:'flex', gap:8, marginBottom:14, flexWrap:'wrap', alignItems:'center' }}>
        <input value={search} onChange={e=>setSearch(e.target.value)}
          placeholder="Search sites..."
          style={{ flex:'1', minWidth:180, padding:'7px 11px', borderRadius:7, fontSize:'.82rem',
            background:'var(--surface)', border:'1px solid var(--border)',
            color:'var(--text)', outline:'none', fontFamily:'inherit' }}
        />
        {[['all','All'],['warn','Warnings'],['error','Critical']].map(([val,lbl]) => (
          <button key={val} onClick={() => setFilter(val)}
            style={{ padding:'6px 12px', borderRadius:6, fontSize:'.75rem', fontWeight:filterStatus===val?700:400,
              cursor:'pointer', border:'1px solid '+(filterStatus===val?'var(--orange-dim)':'var(--border)'),
              background:filterStatus===val?'var(--orange-dim)':'var(--surface)',
              color:filterStatus===val?'var(--orange)':'var(--dim)' }}>
            {lbl}
          </button>
        ))}
        <div style={{ display:'flex', gap:2, marginLeft:'auto',
          background:'var(--surface)', border:'1px solid var(--border)', borderRadius:6, padding:3 }}>
          {[['grid','Grid'],['list','List']].map(([v,l]) => (
            <button key={v} onClick={()=>setView(v)}
              style={{ padding:'4px 12px', borderRadius:4, fontSize:'.72rem', fontWeight:view===v?700:400,
                cursor:'pointer', border:'none', background:view===v?'var(--orange)':'transparent',
                color:view===v?'#000':'var(--dim)' }}>
              {l}
            </button>
          ))}
        </div>
        <span style={{ fontSize:'.72rem', color:'var(--dim)' }}>
          {filtered.length} of {SITES.length} sites
        </span>
      </div>

      {/* Main content + optional panel */}
      <div style={{ display:'flex', gap:14, alignItems:'flex-start' }}>
        <div style={{ flex:1, minWidth:0 }}>
          {view === 'grid' ? (
            <div className="grid" style={{ gridTemplateColumns:'repeat(auto-fill, minmax(270px, 1fr))' }}>
              {filtered.map(st => <SiteCard key={st.id} st={st} />)}
              {/* Add site tile */}
              <div className="card"
                style={{ borderStyle:'dashed', borderWidth:'1.5px', borderColor:'var(--border-2)',
                  background:'transparent', cursor:'pointer' }}
                onClick={() => window.wpsbToast('Connect site dialog opened', 'beam')}
                role="button" tabIndex={0}>
                <div className="card-body" style={{ display:'flex', flexDirection:'column',
                  alignItems:'center', justifyContent:'center', textAlign:'center', gap:8 }}>
                  <div style={{ width:36, height:36, borderRadius:8, background:'var(--surface-3)',
                    border:'1px solid var(--border)', display:'flex', alignItems:'center', justifyContent:'center' }}>
                    <Icon name="plus" size={18}/>
                  </div>
                  <div style={{ fontWeight:600, fontSize:'.88rem', color:'var(--text)' }}>Connect new site</div>
                  <div style={{ fontSize:'.72rem', color:'var(--dim)', maxWidth:200, lineHeight:1.45 }}>
                    Install the WPSiteBeam plugin and log in to connect
                  </div>
                  <button type="button" className="btn btn-primary btn-sm"
                    onClick={e=>{e.stopPropagation();window.wpsbDownload?.('wpsitebeam.zip','plugin');}}>
                    <Icon name="download" size={12}/>Download plugin
                  </button>
                </div>
              </div>
            </div>
          ) : (
            /* List view */
            <div style={{ background:'var(--surface)', border:'1px solid var(--border)', borderRadius:10, overflow:'hidden' }}>
              {/* List header */}
              <div style={{ display:'flex', alignItems:'center', gap:10, padding:'7px 14px 7px 24px',
                borderBottom:'1px solid var(--border)', background:'var(--surface-2)' }}>
                <div style={{ flex:'0 0 200px', fontSize:'.65rem', fontWeight:600, color:'var(--muted)', textTransform:'uppercase', letterSpacing:'.05em' }}>Site</div>
                <div style={{ flex:'0 0 90px', fontSize:'.65rem', fontWeight:600, color:'var(--muted)', textTransform:'uppercase', letterSpacing:'.05em' }}>Platform</div>
                <div style={{ flex:'0 0 70px', fontSize:'.65rem', fontWeight:600, color:'var(--muted)', textTransform:'uppercase', letterSpacing:'.05em' }}>Version</div>
                <div style={{ flex:'0 0 60px', fontSize:'.65rem', fontWeight:600, color:'var(--muted)', textTransform:'uppercase', letterSpacing:'.05em' }}>Health</div>
                <div style={{ flex:'0 0 70px', fontSize:'.65rem', fontWeight:600, color:'var(--muted)', textTransform:'uppercase', letterSpacing:'.05em' }}>Plugins</div>
                <div style={{ flex:'0 0 80px', fontSize:'.65rem', fontWeight:600, color:'var(--muted)', textTransform:'uppercase', letterSpacing:'.05em' }}>Issues</div>
                <div style={{ flex:'0 0 80px', fontSize:'.65rem', fontWeight:600, color:'var(--muted)', textTransform:'uppercase', letterSpacing:'.05em' }}>Last sync</div>
              </div>
              {filtered.map(st => <SiteRow key={st.id} st={st} />)}
              {filtered.length === 0 && (
                <div style={{ padding:'32px 0', textAlign:'center', color:'var(--dim)', fontSize:'.85rem' }}>
                  No sites match current filters
                </div>
              )}
            </div>
          )}
        </div>

        {/* WP Health slide-out panel */}
        {selected && (
          <SitePanel site={selected} panelTab={panelTab} setPanelTab={setPanelTab}
            onClose={() => setSelected(null)} onPrev={prevSite} onNext={nextSite} />
        )}
      </div>
    </div>
  );
}

/* ── ACCOUNT ─────────────────────────────────────────────────── */
/* ── SecurityCard — M2 fix 2026-05-18 (Sprint 2) ──────────────────
   Password change form on the Account page. Uses WPSBD.changePassword
   which verifies current password via signInWithPassword before calling
   auth.updateUser({password}). Inline validation prevents the obvious
   mistakes (mismatched confirm, too short, same as current) before the
   network round-trip. Reset on success. 2FA + active sessions go here
   in M3/M4 future sessions. */
function SecurityCard() {
  const { useState, useEffect } = React;
  const apiBase = window.WPSBD?.apiBase || 'https://api.wpsitebeam.io';
  const [currentPwd, setCurrentPwd] = useState('');
  const [newPwd,     setNewPwd]     = useState('');
  const [confirmPwd, setConfirmPwd] = useState('');
  const [pwSaving,   setPwSaving]   = useState(false);
  const [pwErr,      setPwErr]      = useState('');
  const [mfa,        setMfa]        = useState(false);
  const [sessionTO,  setSessionTO]  = useState(true);
  const [auditRet,   setAuditRet]   = useState(true);
  const [auditLog,   setAuditLog]   = useState([]);

  useEffect(() => {
    const token = window.WPSBD?.getToken?.();
    if (!token) return;
    fetch(apiBase + '/account/audit-log?limit=8', {
      headers: { Authorization: 'Bearer ' + token },
    }).then(r => r.ok ? r.json() : null)
      .then(d => { if (d?.entries) setAuditLog(d.entries); })
      .catch(() => {});
  }, []);

  const tooShort   = newPwd.length > 0 && newPwd.length < 8;
  const mismatch   = confirmPwd.length > 0 && newPwd !== confirmPwd;
  const sameAsCurr = newPwd.length > 0 && newPwd === currentPwd;
  const canSubmit  = !pwSaving && currentPwd && newPwd && confirmPwd && !tooShort && !mismatch && !sameAsCurr;

  const handlePwSubmit = async () => {
    if (!canSubmit) return;
    setPwSaving(true); setPwErr('');
    try {
      const result = await window.WPSBD.changePassword({ current_password: currentPwd, new_password: newPwd });
      if (result.success) {
        window.wpsbToast?.('Password updated', 'ok');
        setCurrentPwd(''); setNewPwd(''); setConfirmPwd('');
      } else {
        setPwErr(result.error || 'Could not update password');
        window.wpsbToast?.(result.error || 'Could not update password', 'err');
      }
    } finally { setPwSaving(false); }
  };

  const SECURITY_FEATURES = [
    { key:'mfa',     label:'Two-factor authentication', val:mfa,       set:setMfa,      desc:'Require 2FA for all team members', badge:null },
    { key:'session', label:'Session timeout (30 min)',  val:sessionTO, set:setSessionTO,desc:'Auto sign-out on inactivity', badge:null },
    { key:'audit',   label:'Audit log retention (90d)', val:auditRet,  set:setAuditRet, desc:'Stores all account security events', badge:null },
    { key:'byok',    label:'BYOK — Bring Your Own Key', val:false,     set:null,        desc:'Use your own Anthropic key — 20% off', badge:'Save 20%' },
  ];

  const DEMO_LOG = [
    { t:'Just now',  who:'You',    what:'viewed security settings', ip:'local' },
    { t:'2 min ago', who:'You',    what:'signed in',                ip:'73.x.x.x' },
    { t:'Yesterday', who:'System', what:'rotated JWT signing key',  ip:'system' },
    { t:'May 24',    who:'System', what:'password hash upgrade',    ip:'system' },
  ];
  const logEntries = auditLog.length > 0 ? auditLog : DEMO_LOG;

  return (
    <div className="card" style={{ marginTop:16 }}>
      <div className="card-head">
        <h2 className="card-title">Security</h2>
        <span className="tag" style={{ background:'var(--surface-2)', color:'var(--dim)' }}>2FA via email coming soon</span>
      </div>
      <div className="card-body" style={{ padding:0 }}>
        <div className="grid grid-2" style={{ gap:0 }}>
          {/* Left: toggles + password */}
          <div style={{ borderRight:'1px solid var(--border)' }}>
            {SECURITY_FEATURES.map((f, i) => (
              <div key={f.key} style={{ display:'flex', alignItems:'center', gap:12, padding:'13px 18px',
                borderBottom:'1px solid var(--border)' }}>
                <div style={{ flex:1 }}>
                  <div style={{ display:'flex', alignItems:'center', gap:8 }}>
                    <span style={{ fontWeight:600, fontSize:'.86rem' }}>{f.label}</span>
                    {f.badge && <span className="tag beam" style={{ fontSize:'.58rem' }}>{f.badge}</span>}
                  </div>
                  <div style={{ fontSize:'.72rem', color:'var(--dim)', marginTop:2 }}>{f.desc}</div>
                </div>
                {window.Toggle
                  ? <window.Toggle checked={f.val} onChange={f.set || (() => window.wpsbToast?.('Coming soon', 'info'))} size="sm" disabled={!f.set}/>
                  : <input type="checkbox" checked={f.val} readOnly/>}
              </div>
            ))}
            <div style={{ padding:'14px 18px' }}>
              <div style={{ fontWeight:600, fontSize:'.86rem', marginBottom:10 }}>Change password</div>
              <Field label="Current password">
                <input value={currentPwd} onChange={e => setCurrentPwd(e.target.value)}
                  type="password" autoComplete="current-password" placeholder="Current password"/>
              </Field>
              <Field label="New password"
                error={tooShort ? 'Min 8 characters' : sameAsCurr ? 'Must differ from current' : null}>
                <input value={newPwd} onChange={e => setNewPwd(e.target.value)}
                  type="password" autoComplete="new-password" placeholder="At least 8 characters"/>
              </Field>
              <Field label="Confirm password" error={mismatch ? "Passwords don't match" : null}>
                <input value={confirmPwd} onChange={e => setConfirmPwd(e.target.value)}
                  type="password" autoComplete="new-password" placeholder="Re-enter new password"/>
              </Field>
              {pwErr && <div style={{ fontSize:'.75rem', color:'var(--red)', marginTop:6 }}>{pwErr}</div>}
              <button className="btn btn-primary btn-sm" style={{ marginTop:10 }}
                disabled={!canSubmit} onClick={handlePwSubmit}>
                {pwSaving ? 'Updating…' : 'Update password'}
              </button>
            </div>
          </div>
          {/* Right: audit log */}
          <div>
            <div style={{ display:'flex', alignItems:'center', justifyContent:'space-between',
              padding:'12px 18px 8px', borderBottom:'1px solid var(--border)' }}>
              <span style={{ fontWeight:600, fontSize:'.84rem' }}>Audit log</span>
              <button className="btn btn-ghost btn-sm"
                onClick={() => window.wpsbDownload?.('audit-log.csv',
                  'time,user,action,ip\n' + logEntries.map(e => `"${e.t}","${e.who}","${e.what}","${e.ip}"`).join('\n'),
                  'text/csv')}>
                <Icon name="download" size={12}/>Export
              </button>
            </div>
            {logEntries.map((entry, i) => (
              <div key={i} style={{ display:'flex', alignItems:'flex-start', gap:10, padding:'10px 18px',
                borderBottom: i < logEntries.length - 1 ? '1px solid var(--border)' : 'none' }}>
                <span style={{ fontFamily:'var(--font-mono)', fontSize:'.7rem', color:'var(--dim)',
                  flexShrink:0, width:58, paddingTop:1 }}>{entry.t}</span>
                <div style={{ flex:1, fontSize:'.82rem', lineHeight:1.4 }}>
                  <strong style={{ color:'var(--text)' }}>{entry.who}</strong>
                  {' '}<span style={{ color:'var(--dim)' }}>{entry.what}</span>
                </div>
                <span style={{ fontFamily:'var(--font-mono)', fontSize:'.68rem', color:'var(--dim)',
                  flexShrink:0 }}>{entry.ip}</span>
              </div>
            ))}
          </div>
        </div>
      </div>
    </div>
  );
}


function Account({ s }) {
  const r = window.WPSBD.ROLES[s.role];
  const { useState } = React;
  /* SECURITY (Phase A 2026-05-18): user identity comes from window.currentUser
     via WPSBD helpers — NOT from the ROLES table which only has persona metadata.
     This component was previously using r.name/r.email/r.initials which were
     hardcoded demo data ("Jordan Davis" / "jordan@acme.co"). After the Phase A
     ROLES table cleanup those fields are undefined; without these helpers the
     form fields would resolve to undefined and rely on Chrome autofill. */
  const userName     = window.WPSBD.getUserName ? window.WPSBD.getUserName() : '';
  const userEmail    = window.WPSBD.getUserEmail ? window.WPSBD.getUserEmail() : '';
  const userInitials = window.WPSBD.getUserInitials ? window.WPSBD.getUserInitials() : '?';

  /* Q2 fix 2026-05-18 — split Full Name into First Name + Last Name + Phone.
     Reads from Supabase user_metadata via WPSBD helpers (which fall back to
     parsing full_name if first/last not set). Saves via WPSBD.saveProfile()
     which calls supabase.auth.updateUser({ data: {...} }) — no Railway endpoint
     needed since user_metadata is user-controllable directly. */
  const [firstName, setFirstName] = useState(() => window.WPSBD.getUserFirstName ? window.WPSBD.getUserFirstName() : '');
  const [lastName,  setLastName]  = useState(() => window.WPSBD.getUserLastName  ? window.WPSBD.getUserLastName()  : '');
  const [phone,     setPhone]     = useState(() => window.WPSBD.getUserPhone     ? window.WPSBD.getUserPhone()     : '');
  /* Latent bug fix 2026-05-18 (Sprint 2 follow-up): timezone was uncontrolled
     (defaultValue only) and not included in the save payload — the dropdown
     looked wired but silently didn't persist. Now controlled + saved. */
  const [timezone,  setTimezone]  = useState(() => window.WPSBD.getUserTimezone  ? window.WPSBD.getUserTimezone()  : 'America/New_York');
  const [saving,    setSaving]    = useState(false);

  const handleSave = async () => {
    if (saving) return;
    setSaving(true);
    try {
      const result = await window.WPSBD.saveProfile({ first_name: firstName, last_name: lastName, phone, timezone });
      if (result.success) {
        window.wpsbToast?.('Profile saved', 'ok');
      } else {
        window.wpsbToast?.('Save failed: ' + (result.error || 'unknown error'), 'err');
      }
    } finally {
      setSaving(false);
    }
  };

  return (
    <div>
      <PageHead crumb="Account" title="Account" sub="Manage your profile and plugin installation."
        actions={<>
          <div style={{ display:'flex', alignItems:'center', gap:10, padding:'4px 10px 4px 4px', background:'var(--surface)', border:'1px solid var(--border)', borderRadius:999 }}>
            <div className={`user-avatar ${s.role === 'sa' ? 'sa' : ''}`} style={{ width:30, height:30, fontSize:'.72rem' }}>{userInitials}</div>
            <div style={{ lineHeight:1.15 }}>
              <div style={{ fontSize:'.78rem', fontWeight:600 }}>{userName}</div>
              <div style={{ fontSize:'.66rem', color:'var(--dim)' }}>{userEmail}</div>
            </div>
            <span className={r.planClass} style={{ marginLeft:2 }}>{r.plan}</span>
          </div>
          <button className="btn btn-ghost btn-sm" onClick={() => {
            const form = document.getElementById('account-profile-form');
            const input = form && form.querySelector('input');
            if (form) form.scrollIntoView({ behavior:'smooth', block:'start' });
            if (input) setTimeout(() => input.focus(), 300);
          }}>Edit profile</button>
        </>}/>
      <div className="grid grid-2">
        <div className="card" id="account-profile-form">
          <div className="card-head"><h2 className="card-title">Profile</h2></div>
          <div className="card-body">
            <div style={{ display:'grid', gridTemplateColumns:'1fr 1fr', gap:12, marginBottom:12 }}>
              <Field label="First name">
                <input value={firstName} onChange={e => setFirstName(e.target.value)} autoComplete="given-name" placeholder="e.g. Jordan"/>
              </Field>
              <Field label="Last name">
                <input value={lastName} onChange={e => setLastName(e.target.value)} autoComplete="family-name" placeholder="e.g. Erickson"/>
              </Field>
            </div>
            <Field label="Email">
              <input value={userEmail} type="email" autoComplete="email" readOnly
                style={{ background:'var(--surface-2)', cursor:'not-allowed' }}
                title="Email is your login. Changing email requires re-verification (coming soon)."/>
            </Field>
            <Field label="Phone">
              <input value={phone} onChange={e => setPhone(e.target.value)} type="tel" autoComplete="tel" placeholder="e.g. +1 (555) 123-4567"/>
            </Field>
            <Field label="Time zone">
              {/* A3 fix 2026-05-18 (Sprint 1) — was hardcoded to 3 options. Now uses
                  Intl.supportedValuesOf('timeZone') for full IANA list (~430 zones,
                  supported in all modern browsers since 2022). Defaults to the user's
                  detected zone. Falls back to a short list on legacy browsers.
                  Latent bug fix 2026-05-18 (Sprint 2 follow-up): converted from
                  uncontrolled `defaultValue` to controlled `value`+`onChange` so the
                  selection actually persists when Save is clicked. The "detected"
                  default now lives in WPSBD.getUserTimezone(). */}
              {(() => {
                let zones;
                try {
                  zones = (typeof Intl.supportedValuesOf === 'function')
                    ? Intl.supportedValuesOf('timeZone')
                    : ['America/New_York','America/Chicago','America/Denver','America/Los_Angeles','America/Anchorage','Pacific/Honolulu','Europe/London','Europe/Paris','Europe/Berlin','Asia/Tokyo','Asia/Singapore','Australia/Sydney','UTC'];
                } catch(e) {
                  zones = ['America/New_York','America/Chicago','America/Denver','America/Los_Angeles','Europe/London','UTC'];
                }
                /* Guard: if the current `timezone` state isn't in the zone list
                   (e.g. legacy-browser fallback list), prepend it so the controlled
                   <select> still has a matching option and doesn't blank out. */
                if (timezone && !zones.includes(timezone)) zones = [timezone, ...zones];
                return (
                  <select value={timezone} onChange={e => setTimezone(e.target.value)}>
                    {zones.map(tz => <option key={tz} value={tz}>{tz}</option>)}
                  </select>
                );
              })()}
            </Field>
            {/* Q2 fix 2026-05-18 (Sprint 1) — Save button fully wired via
                window.WPSBD.saveProfile() → supabase.auth.updateUser().
                Writes to user_metadata. No Railway endpoint needed for these fields. */}
            <button className="btn btn-primary btn-sm" style={{ marginTop: 6 }}
                    disabled={saving}
                    onClick={handleSave}>
              {saving ? 'Saving…' : 'Save changes'}
            </button>
          </div>
        </div>
        <div className="card">
          <div className="card-head"><h2 className="card-title">Quick connect a site</h2><span className="tag beam">SSO</span></div>
          <div className="card-body">
            <div className="box info" style={{ marginBottom: 12 }}>
              <Icon name="info" size={16}/>
              <div>WPSiteBeam uses <strong>single sign-on</strong> — no API keys to copy or rotate. Connect any WordPress site in three steps:</div>
            </div>
            <ol style={{ margin:0, paddingLeft:18, fontSize:'.82rem', lineHeight:1.7, color:'var(--text)' }}>
              <li>Install the WPSiteBeam plugin on the WordPress site
                <div style={{ fontSize:'.7rem', color:'var(--dim)' }}>Download from wpsitebeam.io/downloads</div>
              </li>
              <li>Open <strong>Settings → WPSiteBeam</strong> in WP admin
                <div style={{ fontSize:'.7rem', color:'var(--dim)' }}>Enter your WPSiteBeam email</div>
              </li>
              <li>Authenticate with the same email + password you use here
                <div style={{ fontSize:'.7rem', color:'var(--dim)' }}>Site appears under <a href="#" onClick={(e)=>{e.preventDefault();window.WPSBD?.switchTab?.('sites');}} style={{ color:'var(--beam)' }}>Sites</a> within 30 seconds</div>
              </li>
            </ol>
            <div style={{ display:'flex', gap:8, marginTop: 14 }}>
              <button className="btn btn-primary btn-sm" onClick={() => window.wpsbToast('Plugin download link — coming soon at wpsitebeam.io/downloads', 'beam')}>
                <Icon name="download" size={13}/>Download plugin
              </button>
              <button className="btn btn-ghost btn-sm" onClick={() => window.WPSBD?.switchTab?.('sites')}>
                <Icon name="external" size={13}/>View connected sites
              </button>
            </div>
            <div style={{ fontSize:'.7rem', color:'var(--dim)', marginTop:12, lineHeight:1.5, paddingTop:10, borderTop:'1px solid var(--border)' }}>
              <strong style={{ color:'var(--text)' }}>Why no API key?</strong> SSO is more secure (no long-lived shared secret) and simpler (no key copying/rotation). The plugin uses short-lived JWTs minted per session.
            </div>
          </div>
        </div>
      </div>

      {/* ── SECURITY CARD ──────────────────────────────────────────
          M2 fix 2026-05-18 (Sprint 2) — Password change form.
          Uses Supabase auth.updateUser({password}) via WPSBD.changePassword helper.
          Verifies current password first (signInWithPassword) before updating so
          we surface "current password is incorrect" cleanly. 2FA / sessions list
          (M3/M4) go in this same card in a future session. */}
      <SecurityCard />

      {/* ── WORDPRESS PLUGIN SECTION ──────────────────────────── */}
      <div className="card" style={{ marginTop:16 }}>
        <div className="card-head" style={{ alignItems:'center' }}>
          <div style={{ display:'flex', alignItems:'center', gap:10 }}>
            <div style={{
              width:36, height:36, borderRadius:8,
              background:'linear-gradient(135deg, #21759b 0%, #464646 100%)',
              color:'#fff', display:'flex', alignItems:'center', justifyContent:'center',
              fontSize:'1.1rem', fontWeight:700,
            }}>W</div>
            <div>
              <h2 className="card-title" style={{ fontSize:'1rem', margin:0 }}>WordPress Plugin</h2>
              <div style={{ fontSize:'.72rem', color:'var(--dim)', marginTop:2 }}>Connect your WP sites to WPSiteBeam · Scanner · Image Tools · Redirects · Site Builder</div>
            </div>
          </div>
          <div style={{ display:'flex', gap:6, alignItems:'center' }}>
            {/* A8 fix 2026-05-18 (Sprint 1) — version tag was hardcoded "v0.6.3.0 LATEST".
                Hidden until /sa/plugin/latest endpoint exists. PHP/WP requirements
                are stable across versions so left visible. */}
            <span className="tag beam">PHP 7.4+ · WP 5.8+</span>
          </div>
        </div>
        <div className="card-body" style={{ padding:0 }}>
          {/* Download strip */}
          <div style={{ padding:'18px 20px', background:'linear-gradient(135deg, var(--beam-dim), rgba(52,211,153,.04))', borderBottom:'1px solid var(--border)', display:'flex', alignItems:'center', justifyContent:'space-between', gap:14, flexWrap:'wrap' }}>
            <div>
              <div style={{ fontSize:'.92rem', fontWeight:600, marginBottom:4 }}>📦 WPSiteBeam Plugin</div>
              {/* A8/A11/A12 fix 2026-05-18 (Sprint 1 partial) — was hardcoded
                  "wpsitebeam-plugin-v0.6.3.0.zip · 1.2 MB · Released Nov 14, 2025 · MD5: 4a2fe891c3..."
                  Now shows neutral copy until /sa/plugin/latest endpoint exists. */}
              <div style={{ fontSize:'.74rem', color:'var(--muted)' }}>Connect your WordPress sites to WPSiteBeam · Plugin download coming with the public launch.</div>
            </div>
            <div style={{ display:'flex', gap:8 }}>
              {/* A9 fix 2026-05-18 (Sprint 1) — was a fake toast.
                  Plugin distribution endpoint not yet ready; button disabled with
                  explanatory tooltip. Re-enable when /sa/plugin/download endpoint exists. */}
              <button className="btn btn-primary btn-sm" disabled
                title="Plugin download will be available at public launch"
                style={{ opacity: 0.6, cursor: 'not-allowed' }}>
                <Icon name="download" size={13}/>Download ZIP (coming soon)
              </button>
              {/* A10 fix 2026-05-18 (Sprint 1) — was a fake toast.
                  Plugin not yet on WordPress.org; button hidden until submission.
                  Restore as: onClick={() => window.open('https://wordpress.org/plugins/wpsitebeam', '_blank')} */}
            </div>
          </div>

          {/* 4-step install guide */}
          <div style={{ padding:'18px 20px' }}>
            <div style={{ fontSize:'.74rem', color:'var(--muted)', letterSpacing:1, fontFamily:'var(--font-mono)', marginBottom:12 }}>INSTALLATION · 4 STEPS</div>
            <div className="grid grid-4" style={{ gap:10 }}>
              {[
                { n:'1', t:'Download the ZIP', d:'Click "Download ZIP" above. Don\'t unzip.' },
                { n:'2', t:'Upload to WordPress', d:'WP Admin → Plugins → Add New → Upload Plugin → Choose file → Install Now.' },
                { n:'3', t:'Activate', d:'Click "Activate" after install completes. A new "WPSiteBeam" menu appears in the sidebar.' },
                { n:'4', t:'Paste your key', d:'Settings → WPSiteBeam → paste the API key above → Save. Site connects within 30 seconds.' },
              ].map(step => (
                <div key={step.n} style={{ padding:'12px 14px', background:'var(--surface-2)', border:'1px solid var(--border)', borderRadius:8 }}>
                  <div style={{ display:'flex', alignItems:'center', gap:8, marginBottom:6 }}>
                    <div style={{ width:24, height:24, borderRadius:'50%', background:'var(--beam-dim)', color:'var(--beam)', display:'flex', alignItems:'center', justifyContent:'center', fontSize:'.75rem', fontWeight:700 }}>{step.n}</div>
                    <strong style={{ fontSize:'.84rem' }}>{step.t}</strong>
                  </div>
                  <div style={{ fontSize:'.74rem', color:'var(--muted)', lineHeight:1.5 }}>{step.d}</div>
                </div>
              ))}
            </div>

            <div className="box beam" style={{ marginTop:14, fontSize:'.8rem' }}>
              <Icon name="info" size={14}/>
              <div>
                <strong>Prefer auto-install?</strong> Hosting partners (Kinsta, WP Engine, Flywheel) can push the plugin automatically via their MU-plugins system. <button className="btn-link" style={{ fontSize:'.8rem' }}>View partner install guides →</button>
              </div>
            </div>
          </div>

          {/* Connected sites + changelog split */}
          <div style={{ display:'grid', gridTemplateColumns:'1fr 1fr', gap:0, borderTop:'1px solid var(--border)' }}>
            {/* Connected sites — A11 fix 2026-05-18 (Sprint 1)
                Was hardcoded "acme.co · riverway.health · studio-amberleaf.com · 42 SITES".
                Replaced with empty-state pending /sa/accounts/me/connected-sites endpoint. */}
            <div style={{ padding:'16px 20px', borderRight:'1px solid var(--border)' }}>
              <div style={{ display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom:10 }}>
                <div style={{ fontSize:'.74rem', color:'var(--muted)', letterSpacing:1, fontFamily:'var(--font-mono)' }}>CONNECTED SITES</div>
              </div>
              <div style={{ padding:'20px 12px', textAlign:'center', background:'var(--surface-2)', border:'1px dashed var(--border)', borderRadius:6 }}>
                <div style={{ fontSize:'.82rem', color:'var(--text-2)', marginBottom:4 }}>No sites connected yet</div>
                <div style={{ fontSize:'.7rem', color:'var(--dim)' }}>Install the plugin on any WordPress site and paste your API key to connect it. Connected sites will appear here.</div>
              </div>
            </div>

            {/* Changelog — A12 fix 2026-05-18 (Sprint 2)
                Live fetch from /changelog.json in app repo (Vercel serves it).
                Editing the JSON file + redeploying updates the changelog without
                touching code. Falls back to empty state if file is missing or fetch fails. */}
            <div style={{ padding:'16px 20px' }}>
              <div style={{ display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom:10 }}>
                <div style={{ fontSize:'.74rem', color:'var(--muted)', letterSpacing:1, fontFamily:'var(--font-mono)' }}>CHANGELOG · LAST 4 RELEASES</div>
              </div>
              {(() => {
                const { useState, useEffect } = React;
                const [releases, setReleases] = useState(null); // null = loading, [] = empty, [...] = data
                const [error, setError] = useState(null);
                useEffect(() => {
                  let cancelled = false;
                  fetch('/changelog.json', { cache: 'no-cache' })
                    .then(r => r.ok ? r.json() : Promise.reject(new Error('changelog.json ' + r.status)))
                    .then(data => {
                      if (cancelled) return;
                      const all = Array.isArray(data?.releases) ? data.releases : [];
                      setReleases(all.slice(0, 4));
                    })
                    .catch(e => {
                      if (cancelled) return;
                      console.warn('[Changelog]', e.message);
                      setError(e.message);
                      setReleases([]);
                    });
                  return () => { cancelled = true; };
                }, []);
                if (releases === null) {
                  return (
                    <div style={{ padding:'20px 12px', textAlign:'center', background:'var(--surface-2)', border:'1px dashed var(--border)', borderRadius:6 }}>
                      <div style={{ fontSize:'.78rem', color:'var(--dim)' }}>Loading changelog…</div>
                    </div>
                  );
                }
                if (releases.length === 0) {
                  return (
                    <div style={{ padding:'20px 12px', textAlign:'center', background:'var(--surface-2)', border:'1px dashed var(--border)', borderRadius:6 }}>
                      <div style={{ fontSize:'.82rem', color:'var(--text-2)', marginBottom:4 }}>Changelog coming soon</div>
                      <div style={{ fontSize:'.7rem', color:'var(--dim)' }}>Release notes will be published with the public launch of the plugin.</div>
                    </div>
                  );
                }
                /* Render real changelog entries */
                const TYPE_COLORS = {
                  added:      { bg:'rgba(46,204,113,.14)', fg:'#2ecc71', label:'NEW' },
                  fixed:      { bg:'rgba(52,152,219,.14)', fg:'#3498db', label:'FIX' },
                  changed:    { bg:'rgba(241,196,15,.14)', fg:'#f1c40f', label:'CHG' },
                  security:   { bg:'rgba(231,76,60,.14)',  fg:'#e74c3c', label:'SEC' },
                  deprecated: { bg:'rgba(149,165,166,.14)',fg:'#95a5a6', label:'DEP' },
                  removed:    { bg:'rgba(149,165,166,.14)',fg:'#95a5a6', label:'RM' },
                };
                const fmtDate = d => {
                  if (!d) return null;
                  try {
                    const dt = new Date(d);
                    return dt.toLocaleDateString('en-US', { month:'short', day:'numeric' });
                  } catch(e) { return d; }
                };
                return (
                  <div style={{ display:'flex', flexDirection:'column', gap:8, fontSize:'.78rem' }}>
                    {releases.map((rel, idx) => (
                      <div key={rel.version} style={{ padding:'10px 12px', background:'var(--surface-2)', border:'1px solid var(--border)', borderRadius:6 }}>
                        <div style={{ display:'flex', alignItems:'center', gap:8, marginBottom: rel.changes?.length ? 6 : 0, flexWrap:'wrap' }}>
                          <strong style={{ fontFamily:'var(--font-mono)', fontSize:'.8rem' }}>v{rel.version}</strong>
                          {rel.date && <span style={{ fontSize:'.7rem', color:'var(--dim)' }}>· {fmtDate(rel.date)}</span>}
                          {rel.status === 'unreleased' && <span className="tag warn" style={{ fontSize:'.6rem' }}>UNRELEASED</span>}
                          {rel.status === 'beta'       && <span className="tag warn" style={{ fontSize:'.6rem' }}>BETA</span>}
                          {rel.status === 'rc'         && <span className="tag beam" style={{ fontSize:'.6rem' }}>RC</span>}
                          {idx === 0 && rel.status === 'released' && <span className="tag ok" style={{ fontSize:'.6rem' }}>LATEST</span>}
                        </div>
                        {rel.highlights && (
                          <div style={{ fontSize:'.74rem', color:'var(--text-2)', marginBottom:6, lineHeight:1.45 }}>{rel.highlights}</div>
                        )}
                        {Array.isArray(rel.changes) && rel.changes.length > 0 && (
                          <ul style={{ margin:0, paddingLeft:0, listStyle:'none', fontSize:'.74rem', color:'var(--muted)', lineHeight:1.6 }}>
                            {rel.changes.map((c, j) => {
                              const meta = TYPE_COLORS[c.type] || { bg:'var(--surface-3)', fg:'var(--dim)', label:'•' };
                              return (
                                <li key={j} style={{ display:'flex', gap:8, alignItems:'flex-start', marginBottom:2 }}>
                                  <span style={{ display:'inline-block', padding:'1px 6px', fontSize:'.58rem', fontWeight:700, letterSpacing:'.04em', background:meta.bg, color:meta.fg, borderRadius:3, flexShrink:0, marginTop:2, minWidth:30, textAlign:'center' }}>{meta.label}</span>
                                  <span>{c.text}</span>
                                </li>
                              );
                            })}
                          </ul>
                        )}
                      </div>
                    ))}
                  </div>
                );
              })()}
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}


/* ── BillingAddressCard — wired to PUT /account/billing-address ── */
function BillingAddressCard() {
  const { useState } = React;
  const [saving, setSaving] = useState(false);
  const [addr, setAddr] = useState({ name:'', line1:'', city:'', state:'', zip:'', country:'US' });
  const save = async () => {
    setSaving(true);
    try {
      const token = window.WPSBD?.getToken?.();
      const base = window.WPSBD?.apiBase || 'https://api.wpsitebeam.io';
      const r = await fetch(base + '/account/billing-address', {
        method:'PUT', headers:{ Authorization:'Bearer '+token, 'Content-Type':'application/json' },
        body: JSON.stringify({ ...addr }),
      });
      const d = await r.json();
      r.ok ? window.wpsbToast?.('Billing address saved','ok') : window.wpsbToast?.(d.error||'Save failed','err');
    } catch(e) { window.wpsbToast?.(e.message,'err'); }
    setSaving(false);
  };
  const upd = k => e => setAddr(a => ({...a,[k]:e.target.value}));
  return (
    <div className="grid grid-2" style={{ marginTop:16 }}>
      <div className="card">
        <div className="card-head"><h2 className="card-title">Billing address</h2></div>
        <div className="card-body">
          <Field label="Company / Name"><input value={addr.name} onChange={upd('name')}/></Field>
          <Field label="Country"><select value={addr.country} onChange={upd('country')}>
            <option value="US">United States</option><option value="GB">United Kingdom</option>
            <option value="CA">Canada</option><option value="AU">Australia</option>
          </select></Field>
          <Field label="Address line 1"><input value={addr.line1} onChange={upd('line1')}/></Field>
          <Field label="City / State / ZIP">
            <div style={{ display:'grid', gridTemplateColumns:'2fr 1fr 1fr', gap:8 }}>
              <input value={addr.city} onChange={upd('city')} placeholder="City"/>
              <input value={addr.state} onChange={upd('state')} placeholder="State"/>
              <input value={addr.zip} onChange={upd('zip')} placeholder="ZIP"/>
            </div>
          </Field>
          <button className="btn btn-primary btn-sm" onClick={save} disabled={saving}>{saving?'Saving…':'Save'}</button>
        </div>
      </div>
      <div className="card">
        <div className="card-head"><h2 className="card-title">Tax information</h2></div>
        <div className="card-body">
          <Field label="Tax ID type"><select><option>EIN (US)</option><option>VAT (EU)</option><option>GST (AU/NZ)</option></select></Field>
          <Field label="Tax ID"><input placeholder="00-0000000"/></Field>
          <Field label="Invoice email"><input type="email" placeholder="billing@yourco.com"/></Field>
          <button className="btn btn-primary btn-sm" style={{ marginTop:10 }} onClick={() => window.wpsbToast('Tax info saved','ok')}>Save</button>
        </div>
      </div>
    </div>
  );
}

/* ── NotificationSettings ── */
function NotificationSettings() {
  const { useState, useEffect } = React;
  const apiBase = window.WPSBD?.apiBase || 'https://api.wpsitebeam.io';
  const DEFAULT_PREFS = [
    { key:'scan_completed',     label:'Scan completed',       email:true,  slack:true  },
    { key:'critical_issue',     label:'Critical issue found', email:true,  slack:true  },
    { key:'migration_progress', label:'Migration progress',   email:true,  slack:false },
    { key:'weekly_digest',      label:'Weekly digest',        email:true,  slack:false },
    { key:'plugin_auto_update', label:'Plugin auto-update',   email:false, slack:false },
    { key:'billing_invoices',   label:'Billing & invoices',   email:true,  slack:false },
    { key:'ssl_expiry',         label:'SSL expiry warning',   email:true,  slack:true  },
    { key:'scan_failed',        label:'Scan failed',          email:true,  slack:true  },
  ];
  const [prefs,    setPrefs]    = useState(DEFAULT_PREFS);
  const [slackUrl, setSlackUrl] = useState('');
  const [saving,   setSaving]   = useState(false);
  const [testing,  setTesting]  = useState(false);
  const [loaded,   setLoaded]   = useState(false);
  useEffect(() => {
    const token = window.WPSBD?.getToken?.();
    if (!token) { setLoaded(true); return; }
    fetch(apiBase + '/account/notification-prefs', { headers:{ Authorization:'Bearer '+token } })
      .then(r => r.ok ? r.json() : null)
      .then(d => {
        if (d?.prefs) setPrefs(prev => prev.map(p => { const s=d.prefs.find(x=>x.key===p.key); return s?{...p,email:!!s.email_on,slack:!!s.slack_on}:p; }));
        if (d?.slack_webhook_url) setSlackUrl(d.slack_webhook_url);
        setLoaded(true);
      }).catch(()=>setLoaded(true));
  }, []);
  const toggle = (key,ch) => setPrefs(prev => prev.map(p => p.key===key?{...p,[ch]:!p[ch]}:p));
  const save = async () => {
    setSaving(true);
    try {
      const token = window.WPSBD?.getToken?.();
      const r = await fetch(apiBase+'/account/notification-prefs',{method:'PUT',headers:{Authorization:'Bearer '+token,'Content-Type':'application/json'},body:JSON.stringify({prefs:prefs.map(p=>({key:p.key,email_on:p.email,slack_on:p.slack})),slack_webhook_url:slackUrl||null})});
      const d = await r.json();
      r.ok ? window.wpsbToast?.('Preferences saved','ok') : window.wpsbToast?.(d.error||'Save failed','err');
    } catch(e){window.wpsbToast?.(e.message,'err');}
    setSaving(false);
  };
  const testSlack = async () => {
    if (!slackUrl) { window.wpsbToast?.('Enter a webhook URL first','warn'); return; }
    setTesting(true);
    try {
      const token = window.WPSBD?.getToken?.();
      const r = await fetch(apiBase+'/account/notification-prefs/slack-test',{method:'POST',headers:{Authorization:'Bearer '+token,'Content-Type':'application/json'},body:JSON.stringify({slack_webhook_url:slackUrl})});
      const d = await r.json();
      r.ok ? window.wpsbToast?.('Test sent to Slack ✓','ok') : window.wpsbToast?.(d.error||'Slack test failed','err');
    } catch(e){window.wpsbToast?.(e.message,'err');}
    setTesting(false);
  };
  if (!loaded) return null;
  return (
    <div className="card" style={{ marginTop:16 }}>
      <div className="card-head">
        <h2 className="card-title">Notifications</h2>
        <button className="btn btn-primary btn-sm" onClick={save} disabled={saving}>{saving?'Saving…':'Save'}</button>
      </div>
      <div className="card-body" style={{ padding:0 }}>
        <div style={{ display:'flex', alignItems:'center', padding:'8px 18px 6px', borderBottom:'1px solid var(--border)', background:'var(--surface)' }}>
          <div style={{ flex:1, fontSize:'.68rem', color:'var(--dim)', fontWeight:600, textTransform:'uppercase', letterSpacing:'.06em' }}>Event</div>
          <div style={{ display:'flex', gap:32, paddingRight:2 }}>
            <span style={{ fontSize:'.68rem', color:'var(--dim)', fontWeight:600, textTransform:'uppercase', letterSpacing:'.06em', minWidth:44, textAlign:'center' }}>Email</span>
            <span style={{ fontSize:'.68rem', color:'var(--dim)', fontWeight:600, textTransform:'uppercase', letterSpacing:'.06em', minWidth:44, textAlign:'center' }}>Slack</span>
          </div>
        </div>
        {prefs.map((p,i) => (
          <div key={p.key} style={{ display:'flex', alignItems:'center', padding:'12px 18px', borderBottom:i<prefs.length-1?'1px solid var(--border)':'none' }}>
            <div style={{ flex:1, fontSize:'.88rem', fontWeight:500 }}>{p.label}</div>
            <div style={{ display:'flex', gap:32 }}>
              <div style={{ minWidth:44, display:'flex', justifyContent:'center' }}>
                {window.Toggle ? <window.Toggle checked={p.email} onChange={()=>toggle(p.key,'email')} size="sm"/> : <input type="checkbox" checked={p.email} onChange={()=>toggle(p.key,'email')}/>}
              </div>
              <div style={{ minWidth:44, display:'flex', justifyContent:'center' }}>
                {window.Toggle ? <window.Toggle checked={p.slack} onChange={()=>toggle(p.key,'slack')} size="sm"/> : <input type="checkbox" checked={p.slack} onChange={()=>toggle(p.key,'slack')}/>}
              </div>
            </div>
          </div>
        ))}
      </div>
      <div className="card-body" style={{ borderTop:'1px solid var(--border)', paddingTop:14 }}>
        <div style={{ fontSize:'.78rem', fontWeight:600, marginBottom:6 }}>Slack webhook URL</div>
        <div style={{ display:'flex', gap:8 }}>
          <input value={slackUrl} onChange={e=>setSlackUrl(e.target.value)} placeholder="https://hooks.slack.com/services/…" style={{ flex:1, fontFamily:'var(--font-mono)', fontSize:'.78rem' }}/>
          <button className="btn btn-ghost btn-sm" onClick={testSlack} disabled={testing||!slackUrl}>{testing?'Sending…':'Test'}</button>
        </div>
      </div>
    </div>
  );
}

/* ── AICreditsTopUp ── */
function AICreditsTopUp() {
  const { useState, useEffect } = React;
  const apiBase = window.WPSBD?.apiBase || 'https://api.wpsitebeam.io';
  const [credits,setCredits]=useState(null); const [total,setTotal]=useState(null); const [resetAt,setResetAt]=useState(null); const [buying,setBuying]=useState(null);
  useEffect(() => {
    const token = window.WPSBD?.getToken?.();
    if (!token) return;
    Promise.all([
      fetch(apiBase+'/account/usage',{headers:{Authorization:'Bearer '+token}}).then(r=>r.ok?r.json():null),
      fetch(apiBase+'/billing/subscription',{headers:{Authorization:'Bearer '+token}}).then(r=>r.ok?r.json():null),
    ]).then(([usage,sub]) => {
      if (usage) { const u=usage.ai_calls_used??0,l=usage.ai_calls_limit??null; setCredits(l!==null?Math.max(0,l-u):null); setTotal(l); }
      if (sub?.current_period_end) { const d=new Date(sub.current_period_end*1000); setResetAt(d.toLocaleDateString('en-US',{month:'short',day:'numeric'})); }
    }).catch(()=>{});
  },[]);
  const buyPack = async (priceId) => {
    setBuying(priceId);
    try {
      const token=window.WPSBD?.getToken?.();
      const r=await fetch(apiBase+'/billing/checkout',{method:'POST',headers:{Authorization:'Bearer '+token,'Content-Type':'application/json'},body:JSON.stringify({price_id:priceId,mode:'payment',quantity:1})});
      const d=await r.json();
      if (d.url) window.location.href=d.url; else window.wpsbToast?.(d.error||'Checkout unavailable','err');
    } catch(e){window.wpsbToast?.(e.message,'err');}
    setBuying(null);
  };
  const pct=total&&total>0?Math.round(((total-(credits??total))/total)*100):0;
  const tone=pct>=90?'var(--rose)':pct>=70?'var(--warn)':'var(--cyan,#06b6d4)';
  const PACKS=[{id:'ai_credits_50',label:'+50 credits',price:'$0.50',desc:'~50 AI scans'},{id:'ai_credits_200',label:'+200 credits',price:'$1.99',desc:'~200 AI scans'},{id:'ai_credits_500',label:'+500 credits',price:'$4.99',desc:'Best value'}];
  return (
    <div className="card" style={{ marginBottom:16 }}>
      <div className="card-head">
        <h2 className="card-title">⚡ AI Credits</h2>
        {resetAt && <span style={{ fontSize:'.74rem', color:'var(--dim)' }}>Resets {resetAt}</span>}
      </div>
      <div className="card-body">
        {credits!==null&&total!==null ? (
          <>
            <div style={{ display:'flex', justifyContent:'space-between', alignItems:'baseline', marginBottom:8 }}>
              <span style={{ fontFamily:'var(--font-mono)', fontSize:'1.5rem', fontWeight:600, color:tone }}>{credits.toLocaleString()}</span>
              <span style={{ fontSize:'.78rem', color:'var(--dim)' }}>of {total.toLocaleString()} total</span>
            </div>
            <div style={{ height:6, borderRadius:3, background:'var(--border)', marginBottom:14, overflow:'hidden' }}>
              <div style={{ height:'100%', width:pct+'%', background:tone, borderRadius:3, transition:'width .4s' }}/>
            </div>
          </>
        ) : <div style={{ color:'var(--dim)', fontSize:'.82rem', marginBottom:14 }}>Loading usage…</div>}
        <div style={{ display:'grid', gridTemplateColumns:'repeat(auto-fit, minmax(140px,1fr))', gap:10 }}>
          {PACKS.map(pack => (
            <button key={pack.id} className="btn btn-ghost" style={{ padding:'10px 12px', display:'flex', flexDirection:'column', alignItems:'flex-start', gap:2, textAlign:'left', height:'auto' }} disabled={buying===pack.id} onClick={()=>buyPack(pack.id)}>
              <span style={{ fontWeight:700, fontSize:'.88rem', color:'var(--text)' }}>{pack.label}</span>
              <span style={{ fontSize:'.72rem', color:'var(--dim)' }}>{pack.desc}</span>
              <span style={{ fontFamily:'var(--font-mono)', fontSize:'.82rem', color:'var(--cyan,#06b6d4)', marginTop:2 }}>{buying===pack.id?'Opening…':pack.price}</span>
            </button>
          ))}
        </div>
        <div style={{ fontSize:'.7rem', color:'var(--dim)', marginTop:10 }}>$0.01 per AI invocation · Overages auto-billed monthly.</div>
      </div>
      <NotificationSettings />
    </div>
  );
}

/* ── BILLING ─────────────────────────────────────────────────── */
function Billing({ s }) {
  const { useState } = React;
  if (s.role === 'partner') {
    return (
      <div>
        <PageHead crumb="Account" title="Billing" sub="Internal Partner account — managed centrally."/>
        <div className="box info" style={{ marginBottom: 16 }}>
          <Icon name="info" size={16}/>
          <div><strong>Internal Partner account.</strong> No Stripe billing — all usage costs are covered by WPSiteBeam LLC during development.</div>
        </div>
      </div>
    );
  }

  const [tab, setTab] = useState('methods');
  const [addOpen, setAddOpen] = useState(false);
  const [cancelOpen, setCancelOpen] = useState(false);
  const [defaultId, setDefaultId] = useState('card-1');
  const [methods, setMethods] = useState([
    { id:'card-1', kind:'card',     brand:'Visa',       last4:'4242', exp:'09/27', name:'Casey Dorman', link:true  },
    { id:'card-2', kind:'card',     brand:'Mastercard', last4:'8210', exp:'11/28', name:'Casey Dorman', link:false },
    { id:'ach-1',  kind:'ach',      bank:'Chase',       last4:'6789', name:'Acme Corp · Checking' },
    { id:'link-1', kind:'link',     email:'casey@acme.co' },
  ]);

  const brandLogo = (b) => {
    const map = {
      Visa:       { bg:'linear-gradient(135deg,#1a1f71,#3b4298)', label:'VISA',   color:'#fff' },
      Mastercard: { bg:'linear-gradient(135deg,#eb001b,#f79e1b)', label:'M',      color:'#fff' },
      Amex:       { bg:'linear-gradient(135deg,#2e77bc,#5aa9e0)', label:'AMEX',   color:'#fff' },
      Discover:   { bg:'linear-gradient(135deg,#ff6000,#ffb500)', label:'DISC',   color:'#fff' },
    };
    const m = map[b] || { bg:'var(--surface-3)', label:b.slice(0,4).toUpperCase(), color:'var(--text)' };
    return (
      <div style={{ width:44, height:28, borderRadius:4, background:m.bg, color:m.color, display:'flex', alignItems:'center', justifyContent:'center', fontSize:'.6rem', fontWeight:800, letterSpacing:'.5px', flexShrink:0 }}>{m.label}</div>
    );
  };

  return (
    <div>
      <PageHead crumb="Account" title="Billing" sub="Subscription and payment history."
        actions={<>
          {/* B1 fix 2026-05-18 (Sprint 1) — wired to /billing/portal endpoint.
              Returns the Stripe Customer Portal URL; redirects user.
              Endpoint requires JWT; returns 503 if Stripe not configured (test mode),
              400 if account has no Stripe customer linked. Frontend surfaces both. */}
          <button className="btn btn-ghost btn-sm" onClick={async () => {
            try {
              window.wpsbToast?.('Opening Stripe Customer Portal…', 'info');
              const token = window.WPSB?.getToken?.();
              if (!token) { window.wpsbToast?.('Not authenticated', 'err'); return; }
              const apiBase = window.WPSB_API_BASE || 'https://wpsitebeam-railway-api-production.up.railway.app';
              const r = await fetch(apiBase + '/billing/portal', {
                method: 'POST',
                headers: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' },
              });
              const data = await r.json();
              if (!r.ok) {
                window.wpsbToast?.(data.error || 'Could not open billing portal', 'err');
                return;
              }
              if (data.url) { window.location.href = data.url; return; }
              window.wpsbToast?.('Unexpected response from billing portal', 'err');
            } catch(e) {
              console.error('[billing portal]', e);
              window.wpsbToast?.('Could not open billing portal: ' + e.message, 'err');
            }
          }}>
            <Icon name="lock" size={13}/>Manage in Stripe
          </button>
          <button className="btn btn-ghost btn-sm" style={{ color:'var(--rose)' }} onClick={() => setCancelOpen(true)}>Cancel subscription</button>
          <button className="btn btn-primary btn-sm" onClick={() => setAddOpen(true)}>
            <Icon name="plus" size={13}/>Add payment method
          </button>
        </>}/>

      {/* Stripe connection banner */}
      <div className="card" style={{ marginBottom:16, background:'linear-gradient(135deg, rgba(99,91,255,.08), var(--beam-dim))', border:'1px solid rgba(99,91,255,.3)' }}>
        <div className="card-body" style={{ display:'flex', gap:14, alignItems:'center', flexWrap:'wrap' }}>
          <div style={{ width:38, height:38, borderRadius:8, background:'#635bff', color:'#fff', display:'flex', alignItems:'center', justifyContent:'center', fontWeight:800, letterSpacing:'-1px', fontSize:'.95rem' }}>S</div>
          <div style={{ flex:1, minWidth:220 }}>
            <div style={{ fontWeight:700, fontSize:'.92rem' }}>Stripe · Secure payments powered by Stripe & Link</div>
            <div style={{ fontSize:'.74rem', color:'var(--muted)', marginTop:2 }}>PCI DSS Level 1 · card, ACH, Apple Pay, Google Pay, and 1-click Link all supported.</div>
          </div>
          <span className="tag ok">CONNECTED</span>
          <span style={{ display:'inline-flex', alignItems:'center', gap:6, padding:'3px 10px', background:'var(--beam)', color:'#000', fontSize:'.7rem', fontWeight:700, borderRadius:4, letterSpacing:'.5px' }}>
            ⚡ LINK READY
          </span>
        </div>
      </div>

      {window.BillingInfo ? <window.BillingInfo /> : <div style={{ padding:20, color:'var(--dim)', fontSize:'.85rem' }}>Loading billing info…</div>}
      <AICreditsTopUp />
      <BillingAddressCard />
      {/* Tabs */}
      <div className="sub-tabs" role="tablist" style={{ marginTop:18 }}>
        {[['methods','Payment Methods'],['history','Invoice History'],['tax','Tax & Billing Info']].map(([id,lbl]) => (
          <button key={id} role="tab" aria-selected={tab===id} tabIndex={tab===id?0:-1} className={'sub-tab' + (tab===id?' active':'')} onClick={() => setTab(id)}>{lbl}</button>
        ))}
      </div>

      {tab === 'methods' && (
        <div className="card" style={{ marginTop:16 }}>
          <div className="card-head">
            <h2 className="card-title">Saved payment methods · {methods.length}</h2>
            <button className="btn btn-primary btn-sm" onClick={() => setAddOpen(true)}><Icon name="plus" size={13}/>Add method</button>
          </div>
          <div className="card-body" style={{ padding:0 }}>
            {methods.map((m, i) => {
              const isDefault = m.id === defaultId;
              return (
                <div key={m.id} style={{ display:'flex', alignItems:'center', gap:14, padding:'14px 18px', borderBottom: i<methods.length-1 ? '1px solid var(--border)' : 'none' }}>
                  {m.kind === 'card' && brandLogo(m.brand)}
                  {m.kind === 'ach' && (
                    <div style={{ width:44, height:28, borderRadius:4, background:'linear-gradient(135deg,#0f766e,#14b8a6)', color:'#fff', display:'flex', alignItems:'center', justifyContent:'center', fontSize:'.58rem', fontWeight:800, flexShrink:0 }}>ACH</div>
                  )}
                  {m.kind === 'link' && (
                    <div style={{ width:44, height:28, borderRadius:4, background:'var(--beam)', color:'#000', display:'flex', alignItems:'center', justifyContent:'center', fontSize:'.8rem', fontWeight:800, flexShrink:0 }}>⚡</div>
                  )}
                  <div style={{ flex:1, minWidth:0 }}>
                    <div style={{ fontWeight:600, fontSize:'.88rem', display:'flex', alignItems:'center', gap:8, flexWrap:'wrap' }}>
                      {m.kind === 'card' && <>{m.brand} •••• {m.last4}</>}
                      {m.kind === 'ach'  && <>{m.bank} •••• {m.last4} <span className="tag">ACH</span></>}
                      {m.kind === 'link' && <>Link · {m.email}</>}
                      {m.link && m.kind==='card' && <span style={{ display:'inline-flex', alignItems:'center', gap:3, padding:'1px 6px', background:'var(--beam)', color:'#000', fontSize:'.58rem', fontWeight:800, borderRadius:3, letterSpacing:'.4px' }}>⚡ LINK</span>}
                      {isDefault && <span className="tag ok">DEFAULT</span>}
                    </div>
                    <div style={{ fontSize:'.72rem', color:'var(--dim)', marginTop:2 }}>
                      {m.kind === 'card' && <>Expires {m.exp} · {m.name}</>}
                      {m.kind === 'ach'  && <>{m.name} · Verified via Plaid</>}
                      {m.kind === 'link' && <>1-click checkout · synced across all Link merchants</>}
                    </div>
                  </div>
                  {!isDefault && (
                    <button className="btn btn-ghost btn-sm" onClick={() => { setDefaultId(m.id); window.wpsbToast('Default payment method updated', 'ok'); }}>
                      Set default
                    </button>
                  )}
                  <button className="btn btn-ghost btn-sm" onClick={() => window.wpsbToast('Edit dialog opened', 'info')}>Edit</button>
                  <button className="btn btn-ghost btn-sm" onClick={() => {
                    if (isDefault) { window.wpsbToast('Cannot remove the default method', 'warn'); return; }
                    setMethods(ms => ms.filter(x => x.id !== m.id));
                    window.wpsbToast('Payment method removed', 'ok');
                  }} style={{ color:'var(--rose)' }}>Remove</button>
                </div>
              );
            })}
          </div>
          <div className="card-body" style={{ borderTop:'1px solid var(--border)', background:'var(--surface)', padding:'10px 18px', display:'flex', gap:14, alignItems:'center', flexWrap:'wrap' }}>
            <Icon name="lock" size={14}/>
            <span style={{ fontSize:'.72rem', color:'var(--dim)', flex:1 }}>
              Card numbers are tokenized by Stripe. WP Site Beam never stores raw PAN or bank credentials. PCI DSS compliant.
            </span>
            <span style={{ fontSize:'.64rem', color:'var(--dim)', fontFamily:'var(--font-mono)' }}>cus_Nabc…4k2f</span>
          </div>
        </div>
      )}

      {tab === 'history' && (
        <div className="card" style={{ marginTop:16 }}>
          <div className="card-head">
            <h2 className="card-title">Invoice history</h2>
            <button className="btn btn-ghost btn-sm" onClick={() => window.wpsbDownload('invoices.csv', 'date,amount,status,invoice\n2026-04-01,39.00,paid,in_3KABC\n2026-03-01,39.00,paid,in_2KDEF\n2026-02-01,39.00,paid,in_1KGHI\n', 'text/csv')}>
              <Icon name="download" size={12}/>Export CSV
            </button>
          </div>
          <div className="card-body" style={{ padding: 0 }}>
            <table className="table">
              <thead><tr><th scope="col">Date</th><th scope="col">Description</th><th scope="col">Amount</th><th scope="col">Method</th><th scope="col">Status</th><th scope="col">Invoice</th></tr></thead>
              <tbody>
                {[
                  { d:'Apr 1, 2026', desc:'Pro plan · monthly', amt:'$39.00', m:'Visa •••• 4242', status:'paid',  inv:'in_3KxABC' },
                  { d:'Mar 1, 2026', desc:'Pro plan · monthly', amt:'$39.00', m:'Visa •••• 4242', status:'paid',  inv:'in_2KxDEF' },
                  { d:'Feb 1, 2026', desc:'Pro plan · monthly', amt:'$39.00', m:'Link · casey@acme.co', status:'paid', inv:'in_1KxGHI' },
                  { d:'Jan 1, 2026', desc:'Pro plan · monthly', amt:'$39.00', m:'Visa •••• 4242', status:'paid',  inv:'in_0KxJKL' },
                  { d:'Dec 1, 2025', desc:'Pro plan · monthly', amt:'$39.00', m:'Visa •••• 4242', status:'paid',  inv:'in_9KxMNO' },
                ].map(r => (
                  <tr key={r.inv}>
                    <td>{r.d}</td>
                    <td>{r.desc}</td>
                    <td className="mono">{r.amt}</td>
                    <td style={{ fontSize:'.78rem', color:'var(--dim)' }}>{r.m}</td>
                    <td><span className="tag ok">{r.status.toUpperCase()}</span></td>
                    <td><a href="#" onClick={e=>{e.preventDefault(); window.wpsbToast(`Downloading ${r.inv}.pdf`, 'ok');}}>Download PDF</a></td>
                  </tr>
                ))}
              </tbody>
            </table>
          </div>
        </div>
      )}

      {tab === 'tax' && (
        <div className="grid grid-2" style={{ marginTop:16 }}>
          <div className="card">
            <div className="card-head"><h2 className="card-title">Billing address</h2></div>
            <div className="card-body">
              <Field label="Company / Name"><input defaultValue="Acme Corp"/></Field>
              <Field label="Country"><select defaultValue="US"><option value="US">United States</option><option value="GB">United Kingdom</option><option value="CA">Canada</option></select></Field>
              <Field label="Address line 1"><input defaultValue="500 Market Street"/></Field>
              <Field label="City / State / ZIP">
                <div style={{ display:'grid', gridTemplateColumns:'2fr 1fr 1fr', gap:8 }}>
                  <input defaultValue="San Francisco"/>
                  <input defaultValue="CA"/>
                  <input defaultValue="94105"/>
                </div>
              </Field>
              <button className="btn btn-primary btn-sm" onClick={() => window.wpsbToast('Billing address saved', 'ok')}>Save</button>
            </div>
          </div>
          <div className="card">
            <div className="card-head"><h2 className="card-title">Tax information</h2></div>
            <div className="card-body">
              <Field label="Tax ID type"><select><option>EIN (US)</option><option>VAT (EU)</option><option>GST (AU/NZ)</option></select></Field>
              <Field label="Tax ID"><input placeholder="00-0000000"/></Field>
              <Field label="Invoice email"><input defaultValue="ap@acme.co" type="email"/></Field>
              <div className="box info" style={{ fontSize:'.78rem' }}>
                <Icon name="info" size={14}/>
                <div>Tax calculated automatically by <strong>Stripe Tax</strong>. W-9 on file for US accounts.</div>
              </div>
              <button className="btn btn-primary btn-sm" style={{ marginTop:10 }} onClick={() => window.wpsbToast('Tax info saved', 'ok')}>Save</button>
            </div>
          </div>
        </div>
      )}

      {/* Add payment method modal — Stripe PaymentElement mock */}
      {addOpen && <AddPaymentModal onClose={() => setAddOpen(false)} onAdd={(m) => {
        const id = `m-${Date.now()}`;
        setMethods(ms => [...ms, { id, ...m }]);
        setAddOpen(false);
        window.wpsbToast(`${m.kind === 'link' ? 'Link' : m.kind === 'ach' ? 'Bank account' : 'Card'} added`, 'ok');
      }}/>}
    </div>
  );
}

/* ── Stripe PaymentElement-style modal ─────────────────────────
   Mocks the real Stripe Elements UX: accordion of payment methods
   with Link at the top, then card form, then ACH / wallets. */
function AddPaymentModal({ onClose, onAdd }) {
  const { useState } = React;
  const [method, setMethod] = useState('link');   // link | card | ach | applepay | googlepay
  const [linkEmail, setLinkEmail] = useState('');
  const [card, setCard] = useState({ num:'', exp:'', cvc:'', zip:'', name:'' });
  const [ach, setAch]   = useState({ routing:'', acct:'', name:'' });
  const [saveDefault, setSaveDefault] = useState(true);

  // Format card number into groups of 4
  const fmtCard = v => v.replace(/\D/g,'').slice(0,16).replace(/(\d{4})(?=\d)/g,'$1 ').trim();
  const fmtExp  = v => { const x = v.replace(/\D/g,'').slice(0,4); return x.length>=3 ? x.slice(0,2)+'/'+x.slice(2) : x; };
  const cardBrand = (() => {
    const n = card.num.replace(/\s/g,'');
    if (/^4/.test(n))      return 'Visa';
    if (/^5[1-5]/.test(n)) return 'Mastercard';
    if (/^3[47]/.test(n))  return 'Amex';
    if (/^6/.test(n))      return 'Discover';
    return '';
  })();

  function submit() {
    if (method === 'link') {
      if (!linkEmail.includes('@')) { window.wpsbToast('Enter a valid Link email', 'warn'); return; }
      onAdd({ kind:'link', email: linkEmail });
    } else if (method === 'card') {
      const n = card.num.replace(/\s/g,'');
      if (n.length < 15) { window.wpsbToast('Enter a valid card number', 'warn'); return; }
      if (!/^\d{2}\/\d{2}$/.test(card.exp)) { window.wpsbToast('Enter expiry as MM/YY', 'warn'); return; }
      onAdd({ kind:'card', brand: cardBrand || 'Visa', last4: n.slice(-4), exp: card.exp, name: card.name || 'Card holder', link:false });
    } else if (method === 'ach') {
      if (ach.acct.length < 4) { window.wpsbToast('Enter a valid account number', 'warn'); return; }
      onAdd({ kind:'ach', bank:'Verified Bank', last4: ach.acct.slice(-4), name: ach.name || 'Checking' });
    } else if (method === 'applepay') {
      onAdd({ kind:'card', brand:'Visa', last4:'0001', exp:'12/29', name:'Apple Pay · Device', link:false });
    } else if (method === 'googlepay') {
      onAdd({ kind:'card', brand:'Mastercard', last4:'7777', exp:'08/30', name:'Google Pay · Device', link:false });
    }
  }

  return (
    <div role="dialog" aria-label="Add payment method" aria-modal="true"
         style={{ position:'fixed', inset:0, background:'rgba(0,0,0,.55)', backdropFilter:'blur(4px)', zIndex:2000, display:'flex', alignItems:'center', justifyContent:'center', padding:20 }}>
      <div style={{ background:'var(--bg-2)', border:'1px solid var(--border)', borderRadius:12, width:'100%', maxWidth:480, maxHeight:'90vh', overflow:'auto', boxShadow:'0 30px 80px rgba(0,0,0,.5)' }}>
        {/* Header */}
        <div style={{ padding:'16px 20px', borderBottom:'1px solid var(--border)', display:'flex', alignItems:'center', gap:10 }}>
          <div style={{ width:28, height:28, borderRadius:6, background:'#635bff', color:'#fff', display:'flex', alignItems:'center', justifyContent:'center', fontWeight:800, fontSize:'.8rem' }}>S</div>
          <div style={{ flex:1 }}>
            <div style={{ fontWeight:700, fontSize:'.95rem' }}>Add payment method</div>
            <div style={{ fontSize:'.7rem', color:'var(--dim)' }}>Secured by Stripe · PCI DSS Level 1</div>
          </div>
          <button className="icon-btn" aria-label="Close" onClick={onClose}><Icon name="x" size={14}/></button>
        </div>

        {/* Method accordion — mimics Stripe PaymentElement */}
        <div style={{ padding:20, display:'flex', flexDirection:'column', gap:10 }}>
          {/* Link — always top */}
          <MethodOption
            active={method==='link'}
            onClick={() => setMethod('link')}
            icon={<div style={{ width:28, height:18, borderRadius:3, background:'var(--beam)', color:'#000', display:'flex', alignItems:'center', justifyContent:'center', fontSize:'.65rem', fontWeight:800 }}>⚡</div>}
            title="Link"
            sub="1-click pay — saved across all Link merchants"
            badge={<span style={{ background:'var(--beam)', color:'#000', fontSize:'.56rem', fontWeight:800, padding:'1px 5px', borderRadius:3, letterSpacing:'.4px' }}>FASTEST</span>}
          >
            <div style={{ padding:'12px 0 0' }}>
              <Field label="Email">
                <input type="email" autoFocus placeholder="you@company.com" value={linkEmail} onChange={e => setLinkEmail(e.target.value)}/>
              </Field>
              <div style={{ fontSize:'.7rem', color:'var(--dim)', marginTop:-4, marginBottom:8 }}>
                We'll text a 6-digit code to verify. Link remembers your cards so you never re-enter them.
              </div>
            </div>
          </MethodOption>

          {/* Card */}
          <MethodOption
            active={method==='card'}
            onClick={() => setMethod('card')}
            icon={<Icon name="billing" size={16}/>}
            title="Card"
            sub="Visa, Mastercard, Amex, Discover"
          >
            <div style={{ padding:'12px 0 0' }}>
              <Field label="Card number">
                <div style={{ position:'relative' }}>
                  <input className="mono" placeholder="1234 1234 1234 1234" value={card.num} onChange={e => setCard(c => ({ ...c, num: fmtCard(e.target.value) }))} style={{ paddingRight: cardBrand ? 64 : 12 }}/>
                  {cardBrand && <span style={{ position:'absolute', right:8, top:'50%', transform:'translateY(-50%)', fontSize:'.6rem', fontWeight:800, padding:'2px 6px', background:'var(--surface-3)', borderRadius:3, color:'var(--text-2)' }}>{cardBrand.toUpperCase()}</span>}
                </div>
              </Field>
              <div style={{ display:'grid', gridTemplateColumns:'1fr 1fr 1fr', gap:8 }}>
                <Field label="Expiry"><input className="mono" placeholder="MM/YY" value={card.exp} onChange={e => setCard(c => ({ ...c, exp: fmtExp(e.target.value) }))}/></Field>
                <Field label="CVC"><input className="mono" placeholder="123" maxLength={4} value={card.cvc} onChange={e => setCard(c => ({ ...c, cvc: e.target.value.replace(/\D/g,'').slice(0,4) }))}/></Field>
                <Field label="ZIP"><input className="mono" placeholder="94105" value={card.zip} onChange={e => setCard(c => ({ ...c, zip: e.target.value.slice(0,10) }))}/></Field>
              </div>
              <Field label="Name on card"><input placeholder="Casey Dorman" value={card.name} onChange={e => setCard(c => ({ ...c, name: e.target.value }))}/></Field>
              <label className="opt-check" style={{ margin:'4px 0 0', fontSize:'.74rem' }}>
                <input type="checkbox" defaultChecked/>
                <span>Save to Link for 1-click checkout everywhere</span>
              </label>
            </div>
          </MethodOption>

          {/* ACH / US bank */}
          <MethodOption
            active={method==='ach'}
            onClick={() => setMethod('ach')}
            icon={<div style={{ width:28, height:18, borderRadius:3, background:'linear-gradient(135deg,#0f766e,#14b8a6)', color:'#fff', display:'flex', alignItems:'center', justifyContent:'center', fontSize:'.55rem', fontWeight:800 }}>ACH</div>}
            title="US bank account"
            sub="Direct debit — fee 0.8% (cap $5)"
          >
            <div style={{ padding:'12px 0 0' }}>
              <div className="box info" style={{ fontSize:'.74rem', marginBottom:10 }}>
                <Icon name="info" size={13}/>
                <div>We'll open <strong>Plaid</strong> to verify your bank. Instant for 11,000+ US banks.</div>
              </div>
              <Field label="Account holder"><input placeholder="Acme Corp · Checking" value={ach.name} onChange={e => setAch(a => ({ ...a, name: e.target.value }))}/></Field>
              <div style={{ display:'grid', gridTemplateColumns:'1fr 1fr', gap:8 }}>
                <Field label="Routing"><input className="mono" placeholder="021000021" value={ach.routing} onChange={e => setAch(a => ({ ...a, routing: e.target.value.replace(/\D/g,'').slice(0,9) }))}/></Field>
                <Field label="Account"><input className="mono" placeholder="•••••6789" value={ach.acct} onChange={e => setAch(a => ({ ...a, acct: e.target.value.replace(/\D/g,'').slice(0,17) }))}/></Field>
              </div>
              <button className="btn btn-ghost btn-sm" style={{ marginTop:4 }} type="button" onClick={() => window.wpsbToast('Plaid Link opened (mocked)', 'info')}>
                Or connect instantly via Plaid →
              </button>
            </div>
          </MethodOption>

          {/* Apple Pay */}
          <MethodOption
            active={method==='applepay'}
            onClick={() => setMethod('applepay')}
            icon={<span style={{ fontSize:'.85rem' }}></span>}
            title="Apple Pay"
            sub="Touch ID / Face ID — device-bound"
          />

          {/* Google Pay */}
          <MethodOption
            active={method==='googlepay'}
            onClick={() => setMethod('googlepay')}
            icon={<span style={{ fontSize:'.85rem', fontWeight:800, color:'#4285f4' }}>G</span>}
            title="Google Pay"
            sub="Tap to pay with saved cards"
          />
        </div>

        {/* Footer */}
        <div style={{ padding:'14px 20px', borderTop:'1px solid var(--border)', background:'var(--surface)' }}>
          <label className="opt-check" style={{ margin:'0 0 10px', fontSize:'.78rem' }}>
            <input type="checkbox" checked={saveDefault} onChange={e => setSaveDefault(e.target.checked)}/>
            <span>Set as default payment method</span>
          </label>
          <div style={{ display:'flex', gap:8 }}>
            <button className="btn btn-ghost" onClick={onClose} style={{ flex:1 }}>Cancel</button>
            <button className="btn btn-primary" onClick={submit} style={{ flex:2 }}>
              <Icon name="lock" size={13}/>
              {method==='link'      ? 'Continue with Link' :
               method==='applepay'  ? 'Pay with Apple Pay' :
               method==='googlepay' ? 'Pay with Google Pay' :
               method==='ach'       ? 'Verify bank account'  : 'Add card'}
            </button>
          </div>
          <div style={{ fontSize:'.64rem', color:'var(--dim)', textAlign:'center', marginTop:8, fontFamily:'var(--font-mono)' }}>
            🔒 powered by stripe · tokenized · we never see your card
          </div>
        </div>
      </div>
    </div>
  );
}

function MethodOption({ active, onClick, icon, title, sub, badge, children }) {
  return (
    <div style={{ border: active ? '1.5px solid var(--beam)' : '1px solid var(--border)', borderRadius:8, background: active ? 'var(--surface)' : 'transparent', transition:'all .15s' }}>
      <button type="button" onClick={onClick} style={{ width:'100%', display:'flex', alignItems:'center', gap:12, padding:'12px 14px', background:'transparent', border:0, textAlign:'left', cursor:'pointer', color:'var(--text)' }}>
        <span style={{ width:18, height:18, borderRadius:999, border:`2px solid ${active?'var(--beam)':'var(--dim)'}`, display:'flex', alignItems:'center', justifyContent:'center', flexShrink:0 }}>
          {active && <span style={{ width:8, height:8, borderRadius:999, background:'var(--beam)' }}/>}
        </span>
        <span style={{ width:36, display:'flex', alignItems:'center', justifyContent:'center', flexShrink:0 }}>{icon}</span>
        <span style={{ flex:1, minWidth:0 }}>
          <span style={{ display:'flex', alignItems:'center', gap:6 }}>
            <span style={{ fontWeight:600, fontSize:'.88rem' }}>{title}</span>
            {badge}
          </span>
          <span style={{ fontSize:'.7rem', color:'var(--dim)', display:'block', marginTop:1 }}>{sub}</span>
        </span>
      </button>
      {active && children && <div style={{ padding:'0 14px 14px 14px' }}>{children}</div>}
    </div>
  );
}

window.Pages1 = { PageHead, Stat, Dashboard, Sites, Account, Billing };

/* ── EDITABLE TITLE ──────────────────────────────────────────────
   Renders a heading (default <h2 class="card-title">); for SA role,
   shows a pencil that opens an inline editor. Saved renames persist
   to localStorage under wpsbd-title-<id> and apply globally to
   anyone with the same id.                                         */
function EditableTitle({ id, children, as:Tag='h2', className='card-title', role }) {
  const { useState, useEffect } = React;
  const [val, setVal] = useState(() => localStorage.getItem('wpsbd-title-' + id) || children);
  const [editing, setEditing] = useState(false);
  const [draft, setDraft] = useState(val);

  // Re-read on mount in case value was written by another instance
  useEffect(() => {
    const stored = localStorage.getItem('wpsbd-title-' + id);
    if (stored) setVal(stored);
  }, [id]);

  const save = () => {
    const next = draft.trim() || children;
    setVal(next);
    if (next === children) localStorage.removeItem('wpsbd-title-' + id);
    else localStorage.setItem('wpsbd-title-' + id, next);
    setEditing(false);
    if (window.wpsbToast) window.wpsbToast('Title updated — visible to all users', 'ok');
  };

  const reset = () => {
    localStorage.removeItem('wpsbd-title-' + id);
    setVal(children);
    setDraft(children);
    setEditing(false);
    if (window.wpsbToast) window.wpsbToast('Title reset to default', 'info');
  };

  const isSA = role === 'sa';

  if (editing && isSA) {
    return (
      <span style={{ display:'inline-flex', gap:6, alignItems:'center', flexWrap:'wrap' }}>
        <input autoFocus value={draft} onChange={e => setDraft(e.target.value)}
               onKeyDown={e => { if (e.key === 'Enter') save(); if (e.key === 'Escape') setEditing(false); }}
               style={{ fontSize:'1rem', fontWeight:700, padding:'4px 8px', minWidth:200, background:'var(--surface)', border:'1px solid var(--beam)', borderRadius:6, color:'var(--text)' }}/>
        <button className="btn btn-primary btn-sm" onClick={save}>Save</button>
        <button className="btn btn-ghost btn-sm" onClick={() => setEditing(false)}>Cancel</button>
        {val !== children && <button className="btn btn-ghost btn-sm" onClick={reset} title="Restore default">↺</button>}
      </span>
    );
  }

  return (
    <Tag className={className} style={{ display:'inline-flex', alignItems:'center', gap:8 }}>
      <span>{val}</span>
      {isSA && (
        <Tooltip text={`Rename "${val}" (Super Admin) — change applies site-wide for all users.`}>
          <button className="icon-btn" aria-label={`Rename "${val}" (Super Admin)`}
                  onClick={() => { setDraft(val); setEditing(true); }}
                  style={{ padding:2, opacity:.5 }}>
            <Icon name="edit" size={12}/>
          </button>
        </Tooltip>
      )}
      {isSA && val !== children && (
        <span className="tag" style={{ fontSize:'.56rem' }} title={`Default: "${children}"`}>RENAMED</span>
      )}
    </Tag>
  );
}
window.EditableTitle = EditableTitle;
