/* ═══════════════════════════════════════════════════════════════════
   WP Site Beam — Dashboard 2.0
   Tabbed: Overview / Performance / Financial
   Customizable widget grid; SA can add any widget at-or-below their tier.
   ═══════════════════════════════════════════════════════════════════ */

const { useState, useMemo, useEffect } = React;

/* ── ROLE HIERARCHY ─────────────────────────────────────────────
   Higher index = more privileged. SA can pull widgets from anywhere.
   Customer can only see widgets at their own tier. */
const ROLE_TIER = { customer: 0, partner: 1, admin: 2, sa: 3 };
const TIER_LABEL = { 0: 'Customer', 1: 'Partner', 2: 'Admin', 3: 'SA only' };
const TIER_COLOR = { 0: 'var(--beam)', 1: 'var(--orange)', 2: 'var(--green)', 3: 'var(--rose)' };

/* ── HELPER: TINY CHART RENDERERS (no dep, pure SVG) ──────────── */
function Sparkline({ data, color = 'var(--beam)', height = 36, fillOpacity = 0.2 }) {
  const w = 200, h = height, max = Math.max(...data), min = Math.min(...data);
  const range = max - min || 1;
  const pts = data.map((v, i) => [i * (w / (data.length - 1)), h - ((v - min) / range) * (h - 4) - 2]);
  const path = pts.map((p, i) => `${i ? 'L' : 'M'}${p[0].toFixed(1)},${p[1].toFixed(1)}`).join(' ');
  const areaPath = `${path} L${w},${h} L0,${h} Z`;
  return (
    <svg viewBox={`0 0 ${w} ${h}`} preserveAspectRatio="none" width="100%" height={h} aria-hidden="true">
      <path d={areaPath} fill={color} opacity={fillOpacity}/>
      <path d={path} fill="none" stroke={color} strokeWidth="1.5"/>
    </svg>
  );
}

function BarChart({ data, color = 'var(--beam)', height = 90, labels }) {
  const max = Math.max(...data) || 1;
  return (
    <div style={{ display:'flex', alignItems:'flex-end', gap:6, height, paddingBottom:18, position:'relative' }} aria-hidden="true">
      {data.map((v, i) => (
        <div key={i} style={{ flex:1, display:'flex', flexDirection:'column', alignItems:'center', gap:4, height:'100%' }}>
          <div style={{ flex:1, display:'flex', alignItems:'flex-end', width:'100%' }}>
            <div style={{
              width:'100%', height: `${(v / max) * 100}%`, background: color, borderRadius:'3px 3px 0 0',
              minHeight: v > 0 ? 4 : 0, opacity: 0.85
            }}/>
          </div>
          {labels && <div className="mono" style={{ fontSize:'.58rem', color:'var(--dim)' }}>{labels[i]}</div>}
        </div>
      ))}
    </div>
  );
}

function AreaChart({ datasets, height = 110, labels }) {
  /* datasets: [{ data:[], color:'', label:'' }, ...] — overlay multiple series */
  const w = 360;
  const max = Math.max(...datasets.flatMap(d => d.data)) || 1;
  return (
    <div>
      <svg viewBox={`0 0 ${w} ${height}`} preserveAspectRatio="none" width="100%" height={height} role="img" aria-label="Trend chart">
        {/* Grid lines */}
        {[0.25, 0.5, 0.75].map(g => (
          <line key={g} x1="0" x2={w} y1={height * g} y2={height * g} stroke="var(--border)" strokeDasharray="2,3" opacity="0.5"/>
        ))}
        {datasets.map((ds, di) => {
          const pts = ds.data.map((v, i) => [i * (w / (ds.data.length - 1)), height - (v / max) * (height - 8) - 4]);
          const path = pts.map((p, i) => `${i ? 'L' : 'M'}${p[0].toFixed(1)},${p[1].toFixed(1)}`).join(' ');
          const areaPath = `${path} L${w},${height} L0,${height} Z`;
          return (
            <g key={di}>
              <path d={areaPath} fill={ds.color} opacity="0.15"/>
              <path d={path} fill="none" stroke={ds.color} strokeWidth="1.8"/>
              {pts.map((p, i) => <circle key={i} cx={p[0]} cy={p[1]} r="2" fill={ds.color}/>)}
            </g>
          );
        })}
      </svg>
      {labels && (
        <div style={{ display:'flex', justifyContent:'space-between', fontSize:'.6rem', fontFamily:'var(--font-mono)', color:'var(--dim)', marginTop:4 }} aria-hidden="true">
          {labels.map(l => <span key={l}>{l}</span>)}
        </div>
      )}
    </div>
  );
}

window.Sparkline = Sparkline;
window.BarChart = BarChart;
window.AreaChart = AreaChart;

/* ── WIDGET DEFINITIONS ──────────────────────────────────────────
   Each widget declares: id, label, tier, panel ('overview'|'performance'|'financial'),
   default size ('1x1' | '1x2' | '2x1'), supportsViz (bool), render fn. */

const WIDGETS = [
  {
    id: 'recent-activity',
    label: 'Recent Activity',
    icon: '🕒',
    tier: 0,
    panel: 'overview',
    size: '1x1',
    desc: 'Latest scans, fixes, alerts across your sites',
  },
  {
    id: 'site-health',
    label: 'Site Health',
    icon: '💚',
    tier: 0,
    panel: 'overview',
    size: '1x1',
    desc: 'Health bars + last-scan timestamps for each site',
  },
  {
    id: 'recent-notices',
    label: 'Recent Notices',
    icon: '🔔',
    tier: 0,
    panel: 'overview',
    size: '1x1',
    desc: 'Last 3 unread alerts inline',
  },
  {
    id: 'upcoming-work',
    label: 'Upcoming Work',
    icon: '📅',
    tier: 0,
    panel: 'overview',
    size: '1x1',
    desc: 'Tasks, scans, renewals due this week',
  },
  {
    id: 'top-recs',
    label: 'Top Recommendations',
    icon: '🚦',
    tier: 0,
    panel: 'overview',
    size: '1x1',
    desc: 'AI-prioritized actions across your portfolio',
  },
  {
    id: 'goals',
    label: 'Goals & KPIs',
    icon: '🎯',
    tier: 0,
    panel: 'overview',
    size: '1x1',
    desc: 'User-defined targets with progress',
  },

  /* ── PERFORMANCE ── */
  {
    id: 'stats-chart',
    label: 'Stats Trend',
    icon: '📊',
    tier: 0,
    panel: 'performance',
    size: '2x1',
    supportsViz: true,
    desc: 'Sites scanned + issues over time',
  },
  {
    id: 'seo-snapshot',
    label: 'SEO Snapshot',
    icon: '🔍',
    tier: 0,
    panel: 'performance',
    size: '1x1',
    desc: 'Top SEO issues across portfolio + score trend',
  },
  {
    id: 'scan-volume',
    label: 'Scan Volume',
    icon: '📈',
    tier: 1,
    panel: 'performance',
    size: '1x1',
    supportsViz: true,
    desc: 'Sites scanned this period (Partner+ shows aggregate)',
  },

  /* ── FINANCIAL ── */
  {
    id: 'savings',
    label: 'Time + Cost Savings',
    icon: '💰',
    tier: 0,
    panel: 'financial',
    size: '2x1',
    desc: 'Hours + dollars WPSB has saved you',
  },
  {
    id: 'open-estimates',
    label: 'Open Estimates',
    icon: '📋',
    tier: 1,
    panel: 'financial',
    size: '1x1',
    desc: 'Recent proposals + pipeline value',
  },
  {
    id: 'mrr-mini',
    label: 'MRR Snapshot',
    icon: '💸',
    tier: 1,
    panel: 'financial',
    size: '1x1',
    desc: 'Monthly recurring revenue + delta',
  },
  {
    id: 'churn-risk',
    label: 'Churn Risk',
    icon: '⚠️',
    tier: 2,
    panel: 'financial',
    size: '1x1',
    desc: 'Clients with declining engagement (Admin+)',
  },
];

window.WIDGETS = WIDGETS;

/* Default layout per role — which widgets to show out of the box */
const DEFAULT_LAYOUT = {
  customer: ['recent-activity','site-health','recent-notices','upcoming-work','stats-chart','seo-snapshot','savings','goals'],
  partner:  ['recent-activity','site-health','top-recs','upcoming-work','stats-chart','seo-snapshot','scan-volume','savings','open-estimates','mrr-mini'],
  admin:    ['recent-activity','top-recs','recent-notices','upcoming-work','stats-chart','seo-snapshot','scan-volume','savings','open-estimates','mrr-mini','churn-risk'],
  sa:       ['recent-activity','top-recs','recent-notices','upcoming-work','stats-chart','seo-snapshot','scan-volume','savings','open-estimates','mrr-mini','churn-risk','site-health','goals'],
};

/* ── WIDGET RENDERERS ──────────────────────────────────────────── */

function W_RecentActivity({ overview }) {
  /* Relative time formatter. */
  const fmt = (iso) => {
    if (!iso) return '—';
    const sec = Math.floor((Date.now() - new Date(iso).getTime()) / 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`;
    if (sec < 604800) return `${Math.floor(sec/86400)}d ago`;
    return new Date(iso).toLocaleDateString();
  };
  const tagFor = (type, event) => {
    if (event === 'completed' || event === 'scan')  return { cls: 'ok',   txt: type === 'scan' ? 'SCAN' : 'DONE' };
    if (event === 'failed')                          return { cls: 'crit', txt: 'FAIL' };
    if (event === 'queued' || event === 'claimed') return { cls: 'warn', txt: 'WORK' };
    if (event === 'cancelled' || event === 'expired') return { cls: 'beam', txt: 'CANC' };
    return { cls: 'beam', txt: 'EVENT' };
  };
  const siteShort = (url) => {
    if (!url) return '—';
    try { return new URL(url).hostname; } catch (e) { return url.replace(/^https?:\/\//, ''); }
  };

  const rows = overview?.recent_activity || [];
  if (!overview) {
    return <div style={{ padding:'12px 0', fontSize:'.85rem', color:'var(--dim)' }}>Loading…</div>;
  }
  if (rows.length === 0) {
    return (
      <div style={{ padding:'24px 12px', textAlign:'center', color:'var(--dim)', fontSize:'.85rem' }}>
        No activity yet. Run a scan or queue a plugin operation to populate this.
      </div>
    );
  }
  return (
    <table className="table">
      <thead><tr><th scope="col">Event</th><th scope="col">Site</th><th scope="col">When</th></tr></thead>
      <tbody>
        {rows.map((r, i) => {
          const t = tagFor(r.type, r.event);
          return (
            <tr key={i}>
              <td><span className={`tag ${t.cls}`}>{t.txt}</span> {r.title}</td>
              <td>{siteShort(r.site_url)}</td>
              <td>{fmt(r.at)}</td>
            </tr>
          );
        })}
      </tbody>
    </table>
  );
}

function W_SiteHealth({ overview }) {
  if (!overview) {
    return <div style={{ padding:'12px 0', fontSize:'.85rem', color:'var(--dim)' }}>Loading…</div>;
  }
  const sites = overview.sites || [];
  if (sites.length === 0) {
    return (
      <div style={{ padding:'24px 12px', textAlign:'center', color:'var(--dim)', fontSize:'.85rem' }}>
        No sites connected yet. Install the WPSiteBeam plugin on a WordPress site and log in to see it here.
      </div>
    );
  }
  const fmtAgo = (iso) => {
    if (!iso) return 'never';
    const sec = Math.floor((Date.now() - new Date(iso).getTime()) / 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 siteShort = (url) => {
    try { return new URL(url).hostname; } catch (e) { return url; }
  };
  return (
    <div style={{ display:'flex', flexDirection:'column' }}>
      {sites.slice(0, 6).map((s, i) => {
        const dotColor = s.is_online ? 'var(--green)' : 'var(--dim)';
        const lastConnected = s.last_connected_at ? `Last seen ${fmtAgo(s.last_connected_at)}` : 'Never connected';
        const pluginInfo = s.plugin_version ? `WPSB v${s.plugin_version}` : 'No plugin version';
        return (
          <div key={s.id} style={{ display:'flex', alignItems:'center', gap:12, padding:'10px 0', borderBottom: i < Math.min(sites.length, 6) - 1 ? '1px solid var(--border)' : 'none' }}>
            <div style={{ width:8, height:8, borderRadius:'50%', background: dotColor, boxShadow: s.is_online ? '0 0 6px var(--green-dim)' : 'none' }} aria-hidden="true"/>
            <div style={{ flex:1, minWidth: 0 }}>
              <div style={{ fontWeight:600, fontSize:'.85rem', overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap' }}>{siteShort(s.site_url)}</div>
              <div style={{ fontSize:'.72rem', color:'var(--dim)' }}>{lastConnected} · {pluginInfo}</div>
            </div>
            <div className="mono" style={{ fontSize:'.72rem', color: s.is_online ? 'var(--green)' : 'var(--dim)' }}>
              {s.is_online ? 'online' : 'offline'}
            </div>
          </div>
        );
      })}
      {sites.length > 6 && (
        <div style={{ paddingTop: 10, fontSize: '.72rem', color: 'var(--dim)', textAlign: 'center' }}>
          + {sites.length - 6} more
        </div>
      )}
    </div>
  );
}

function W_RecentNotices({ api, overview }) {
  /* 2026-05-19 — computes real notices from /dashboard/overview:
       - Outdated plugin versions (plugin_version !== latest_plugin_version)
       - Failed plugin operations (event_type === 'failed' in recent_activity)
       - Offline sites (last_connected_at > 7 days ago)
     No backend changes needed — derives from data already in context.
     When the notices/alerts system ships, this becomes a real /notices fetch. */
  if (!overview) {
    return <div style={{ padding:'12px 0', fontSize:'.85rem', color:'var(--dim)' }}>Loading…</div>;
  }
  const notices = [];
  const fmtSiteShort = (url) => {
    if (!url) return '—';
    try { return new URL(url).hostname; } catch (e) { return url.replace(/^https?:\/\//, ''); }
  };
  /* Outdated plugin versions */
  const latest = overview.stats?.latest_plugin_version;
  (overview.sites || []).forEach(s => {
    if (latest && s.plugin_version && s.plugin_version !== latest) {
      notices.push({
        sev: 'warn',
        t:   `Plugin update available — v${latest} (currently v${s.plugin_version})`,
        site: fmtSiteShort(s.site_url),
        ago: 'now',
      });
    }
  });
  /* Offline sites — last connected > 7 days ago (also flagged by is_online: false) */
  (overview.sites || []).forEach(s => {
    if (s.is_online === false && s.last_connected_at) {
      const days = Math.floor((Date.now() - new Date(s.last_connected_at).getTime()) / (1000*60*60*24));
      notices.push({
        sev: 'critical',
        t:   `Site offline — no plugin check-in for ${days} day${days===1?'':'s'}`,
        site: fmtSiteShort(s.site_url),
        ago: `${days}d`,
      });
    }
  });
  /* Failed operations from recent_activity */
  (overview.recent_activity || []).forEach(a => {
    if (a.event === 'failed') {
      notices.push({
        sev: 'critical',
        t:   `Operation failed — ${a.title}`,
        site: fmtSiteShort(a.site_url),
        ago:  a.at ? Math.floor((Date.now() - new Date(a.at).getTime()) / 60000) + 'm' : '—',
      });
    }
  });
  /* Cap to 5 most-relevant notices */
  const shown = notices.slice(0, 5);

  if (shown.length === 0) {
    return (
      <div style={{ padding:'24px 12px', textAlign:'center', color:'var(--dim)', fontSize:'.85rem' }}>
        <div style={{ marginBottom:6 }}>🎉 All clear — no notices right now.</div>
        <div style={{ fontSize:'.72rem' }}>Plugin updates, failed operations, and offline sites will appear here.</div>
      </div>
    );
  }
  return (
    <div style={{ display:'flex', flexDirection:'column', gap:0 }}>
      {shown.map((n, i) => (
        <div key={i} style={{ display:'flex', gap:10, padding:'10px 0', borderBottom: i < shown.length - 1 ?'1px solid var(--border)':'none', alignItems:'flex-start' }}>
          <span className={`tag ${n.sev==='critical'?'rose':n.sev==='warn'?'warn':'beam'}`} style={{ flexShrink:0, marginTop:1 }}>
            {n.sev.toUpperCase().slice(0,4)}
          </span>
          <div style={{ flex:1, minWidth:0 }}>
            <div style={{ fontSize:'.82rem', fontWeight:500 }}>{n.t}</div>
            <div className="mono" style={{ fontSize:'.66rem', color:'var(--dim)', marginTop:2 }}>{n.site} · {n.ago} ago</div>
          </div>
        </div>
      ))}
    </div>
  );
}

function W_UpcomingWork({ overview }) {
  /* 2026-05-19 — Uses /dashboard/overview.upcoming_work (server v1.10.8+)
     which returns pending_operations.{pending,claimed} for this account.
     Falls back to legacy filter on recent_activity for older server versions. */
  if (!overview) {
    return <div style={{ padding:'12px 0', fontSize:'.85rem', color:'var(--dim)' }}>Loading…</div>;
  }
  const fmtSiteShort = (url) => {
    if (!url) return '—';
    try { return new URL(url).hostname; } catch (e) { return url; }
  };
  /* Prefer the dedicated upcoming_work field; fall back to filtering activity */
  let upcoming;
  if (Array.isArray(overview.upcoming_work)) {
    upcoming = overview.upcoming_work.map(w => ({
      t:    w.title,
      site: fmtSiteShort(w.site_url),
      when: w.status === 'claimed' ? 'in progress' : 'queued',
      tone: w.status === 'claimed' ? 'orange' : 'beam',
    }));
  } else {
    /* Legacy fallback */
    upcoming = (overview.recent_activity || [])
      .filter(a => a.event === 'queued' || a.event === 'claimed')
      .slice(0, 6)
      .map(a => ({
        t:    a.title,
        site: fmtSiteShort(a.site_url),
        when: a.event === 'claimed' ? 'in progress' : 'queued',
        tone: a.event === 'claimed' ? 'orange' : 'beam',
      }));
  }

  return (
    <div style={{ display:'flex', flexDirection:'column' }}>
      {upcoming.length === 0 ? (
        <div style={{ padding:'18px 12px', textAlign:'center', color:'var(--dim)', fontSize:'.82rem' }}>
          No pending operations. Queue one from Redirects, Alt Text, or Kit Builder.
        </div>
      ) : upcoming.map((it, i) => (
        <div key={i} style={{ display:'flex', gap:10, padding:'9px 0', borderBottom: i<upcoming.length-1?'1px solid var(--border)':'none', alignItems:'center' }}>
          <div style={{ width:6, height:6, borderRadius:'50%', background: `var(--${it.tone === 'orange' ? 'orange' : 'beam'})`, flexShrink:0 }} aria-hidden="true"/>
          <div style={{ flex:1, minWidth:0 }}>
            <div style={{ fontSize:'.82rem', overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap' }}>{it.t}</div>
            <div style={{ fontSize:'.66rem', color:'var(--dim)' }}>{it.site}</div>
          </div>
          <div className="mono" style={{ fontSize:'.66rem', color:'var(--dim)' }}>{it.when}</div>
        </div>
      ))}
      <div style={{ padding:'10px 0 4px', fontSize:'.7rem', color:'var(--muted)', borderTop: upcoming.length ? '1px solid var(--border)' : 'none', marginTop: upcoming.length ? 4 : 0, fontStyle:'italic' }}>
        Scheduled scans coming soon
      </div>
    </div>
  );
}

function W_TopRecs({ api }) {
  const recs = [
    { sev:'critical', t:'Add MFA to admin accounts',           site:'shop.acme.co', impact:'Security · Critical' },
    { sev:'warn',     t:'Compress 12 oversized images',         site:'blog.acme.co', impact:'Performance · 1.2s gain' },
    { sev:'warn',     t:'Add missing meta descriptions (8)',    site:'acme.co',      impact:'SEO · +14% CTR est.' },
    { sev:'info',     t:'Migrate from PHP 7.4 → 8.2',           site:'shop.acme.co', impact:'Performance · 30% faster' },
  ];
  return (
    <div style={{ display:'flex', flexDirection:'column' }}>
      {recs.map((r, i) => (
        <div key={i} style={{ display:'flex', gap:10, padding:'10px 0', borderBottom: i<recs.length-1?'1px solid var(--border)':'none', alignItems:'flex-start' }}>
          <span className={`tag ${r.sev==='critical'?'rose':r.sev==='warn'?'warn':'beam'}`} style={{ flexShrink:0, marginTop:1 }}>
            {r.sev.toUpperCase().slice(0,4)}
          </span>
          <div style={{ flex:1, minWidth:0 }}>
            <div style={{ fontSize:'.82rem', fontWeight:500 }}>{r.t}</div>
            <div className="mono" style={{ fontSize:'.66rem', color:'var(--dim)', marginTop:2 }}>{r.site} · {r.impact}</div>
          </div>
        </div>
      ))}
    </div>
  );
}

function W_Goals({ overview }) {
  /* 2026-05-19 — Sites Monitored uses real count from /dashboard/overview.
     Other goals (issues resolved, avg health) need backing data sources
     that don't exist yet — shown with current value 0 + clear that they're
     pending. When notices/findings systems ship, this becomes fully real. */
  const sitesCount   = overview?.stats?.sites_count ?? null;
  const issueClosed  = null;                                /* TBD when notices system exists */
  const avgHealth    = null;                                /* TBD when health score exists */
  const goals = [
    { lbl:'Sites monitored',  cur: sitesCount,  tgt: 10, unit:'',   real: true },
    { lbl:'Issues resolved',  cur: issueClosed, tgt: 25, unit:'/mo', real: false, note: 'needs notices system' },
    { lbl:'Avg health score', cur: avgHealth,   tgt: 98, unit:'%',   real: false, note: 'needs health score' },
  ];
  return (
    <div style={{ display:'flex', flexDirection:'column', gap:14 }}>
      {goals.map((g, i) => {
        const hasValue = g.cur != null;
        const pct = hasValue ? Math.min(100, (g.cur / g.tgt) * 100) : 0;
        return (
          <div key={i}>
            <div style={{ display:'flex', justifyContent:'space-between', marginBottom:5 }}>
              <span style={{ fontSize:'.78rem', fontWeight:500, color: g.real ? 'var(--text)' : 'var(--dim)' }}>
                {g.lbl}
                {!g.real && <span style={{ fontSize:'.6rem', marginLeft:6, color:'var(--dim)', fontStyle:'italic' }}>(pending)</span>}
              </span>
              <span className="mono" style={{ fontSize:'.74rem', color:'var(--dim)' }}>
                {hasValue ? `${g.cur}${g.unit}` : '—'} / {g.tgt}{g.unit}
              </span>
            </div>
            <div style={{ height:6, background:'var(--surface-3)', borderRadius:3, overflow:'hidden' }} role="progressbar" aria-valuenow={g.cur || 0} aria-valuemin="0" aria-valuemax={g.tgt} aria-label={g.lbl}>
              <div style={{ height:'100%', width: `${pct}%`, background: !hasValue ? 'var(--surface-3)' : pct >= 90 ? 'var(--green)' : pct >= 60 ? 'var(--beam)' : 'var(--warn)' }}/>
            </div>
          </div>
        );
      })}
      <button className="btn btn-ghost btn-sm" style={{ justifyContent:'center', marginTop:4 }} disabled title="Custom goals coming in a future release">+ Add goal</button>
    </div>
  );
}

function W_StatsChart({ viz, setViz }) {
  const labels = ['Wk 14','15','16','17','18','19','20'];
  const scans   = [12, 18, 15, 22, 28, 24, 32];
  const issues  = [ 8, 11,  9, 14, 12,  7,  5];
  return (
    <div>
      <div style={{ display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom:10 }}>
        <div style={{ display:'flex', gap:14, fontSize:'.74rem' }}>
          <div style={{ display:'flex', alignItems:'center', gap:6 }}>
            <span style={{ width:10, height:10, background:'var(--beam)', borderRadius:2 }} aria-hidden="true"/>
            <span style={{ color:'var(--dim)' }}>Sites scanned</span>
          </div>
          <div style={{ display:'flex', alignItems:'center', gap:6 }}>
            <span style={{ width:10, height:10, background:'var(--orange)', borderRadius:2 }} aria-hidden="true"/>
            <span style={{ color:'var(--dim)' }}>Issues found</span>
          </div>
        </div>
        <div className="tw-seg" role="group" aria-label="Chart style">
          <button className={viz==='area'?'active':''}     onClick={() => setViz('area')}     aria-pressed={viz==='area'}>Area</button>
          <button className={viz==='bar'?'active':''}      onClick={() => setViz('bar')}      aria-pressed={viz==='bar'}>Bar</button>
          <button className={viz==='spark'?'active':''}    onClick={() => setViz('spark')}    aria-pressed={viz==='spark'}>Spark</button>
        </div>
      </div>
      {viz === 'area' && <AreaChart datasets={[{ data:scans, color:'var(--beam)', label:'Scans' }, { data:issues, color:'var(--orange)', label:'Issues' }]} labels={labels}/>}
      {viz === 'bar' && (
        <div style={{ display:'grid', gridTemplateColumns:'1fr 1fr', gap:18 }}>
          <div>
            <div className="mono" style={{ fontSize:'.66rem', color:'var(--dim)', marginBottom:6 }}>SITES SCANNED</div>
            <BarChart data={scans} color="var(--beam)" labels={labels}/>
          </div>
          <div>
            <div className="mono" style={{ fontSize:'.66rem', color:'var(--dim)', marginBottom:6 }}>ISSUES FOUND</div>
            <BarChart data={issues} color="var(--orange)" labels={labels}/>
          </div>
        </div>
      )}
      {viz === 'spark' && (
        <div style={{ display:'grid', gridTemplateColumns:'1fr 1fr', gap:18 }}>
          <div>
            <div className="mono" style={{ fontSize:'.66rem', color:'var(--dim)' }}>SCANS</div>
            <div style={{ fontSize:'1.8rem', fontWeight:700, fontFamily:'var(--font-brand)' }}>{scans[scans.length-1]}</div>
            <Sparkline data={scans} color="var(--beam)"/>
          </div>
          <div>
            <div className="mono" style={{ fontSize:'.66rem', color:'var(--dim)' }}>ISSUES</div>
            <div style={{ fontSize:'1.8rem', fontWeight:700, fontFamily:'var(--font-brand)', color:'var(--orange)' }}>{issues[issues.length-1]}</div>
            <Sparkline data={issues} color="var(--orange)"/>
          </div>
        </div>
      )}
    </div>
  );
}

function W_SeoSnapshot({ viz, setViz }) {
  const issues = [
    { lbl:'Missing meta desc',  count:24, sev:'warn' },
    { lbl:'Slow LCP (>4s)',     count:12, sev:'critical' },
    { lbl:'Broken links',       count: 8, sev:'warn' },
    { lbl:'Missing alt text',   count:42, sev:'info' },
  ];
  const trend = [62, 65, 68, 71, 70, 74, 78];
  return (
    <div>
      <div style={{ display:'flex', justifyContent:'space-between', alignItems:'baseline', marginBottom:10 }}>
        <div>
          <div style={{ fontSize:'1.6rem', fontWeight:700, fontFamily:'var(--font-brand)' }}>78<span style={{ fontSize:'.7rem', color:'var(--dim)', marginLeft:4 }}>/ 100</span></div>
          <div className="mono" style={{ fontSize:'.66rem', color:'var(--green)' }}>↑ +4 this month</div>
        </div>
        <div className="tw-seg" role="group" aria-label="View">
          <button className={viz==='list'?'active':''}  onClick={() => setViz('list')}  aria-pressed={viz==='list'}>Issues</button>
          <button className={viz==='spark'?'active':''} onClick={() => setViz('spark')} aria-pressed={viz==='spark'}>Trend</button>
        </div>
      </div>
      {viz === 'spark' ? (
        <Sparkline data={trend} color="var(--green)" height={50}/>
      ) : (
        <div style={{ display:'flex', flexDirection:'column' }}>
          {issues.map((iss, i) => (
            <div key={i} style={{ display:'flex', gap:10, padding:'7px 0', borderBottom: i<issues.length-1?'1px solid var(--border)':'none', alignItems:'center' }}>
              <span className={`tag ${iss.sev==='critical'?'rose':iss.sev==='warn'?'warn':'beam'}`} style={{ flexShrink:0 }}>
                {iss.sev.toUpperCase().slice(0,4)}
              </span>
              <span style={{ flex:1, fontSize:'.78rem' }}>{iss.lbl}</span>
              <span className="mono" style={{ fontSize:'.78rem', fontWeight:700 }}>{iss.count}</span>
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

function W_ScanVolume({ viz, setViz }) {
  const data = [42, 58, 51, 63, 72, 68, 85];
  const labels = ['Wk14','15','16','17','18','19','20'];
  return (
    <div>
      <div style={{ display:'flex', justifyContent:'space-between', alignItems:'baseline', marginBottom:8 }}>
        <div>
          <div style={{ fontSize:'1.8rem', fontWeight:700, fontFamily:'var(--font-brand)' }}>85</div>
          <div className="mono" style={{ fontSize:'.66rem', color:'var(--dim)' }}>SCANS THIS WEEK</div>
        </div>
        <div className="tw-seg" role="group" aria-label="View">
          <button className={viz==='bar'?'active':''}   onClick={() => setViz('bar')}   aria-pressed={viz==='bar'}>Bar</button>
          <button className={viz==='spark'?'active':''} onClick={() => setViz('spark')} aria-pressed={viz==='spark'}>Line</button>
        </div>
      </div>
      {viz === 'bar' ? <BarChart data={data} color="var(--beam)" labels={labels} height={70}/> : <Sparkline data={data} color="var(--beam)" height={60}/>}
    </div>
  );
}

/* SAVINGS — the big SaaS-pitch widget. Editable rates via Tweaks-style inline panel. */
function W_Savings() {
  /* Default rates from spec — user can change in collapsible Edit panel */
  const [rates, setRates] = useState({
    hourly:  125,             // their billable rate
    estReview: 1.5,           // hrs saved per estimate scope/review
    estWrite:  1.0,           // hrs saved per estimate written
    emailWrite: 0.25,         // hrs saved per email
    migrationSm: 4,           // hrs saved per small migration
    migrationLg: 10,          // hrs saved per large migration
  });
  /* Activity counts this month (would come from real data) */
  const counts = {
    estReviewed: 8,
    estWritten:  6,
    emails:      24,
    migSm:       3,
    migLg:       1,
  };
  const [editing, setEditing] = useState(false);

  const lines = [
    { lbl:'Site reviews & scoping',     count: counts.estReviewed, hrs: rates.estReview * counts.estReviewed },
    { lbl:'Estimates written',          count: counts.estWritten,  hrs: rates.estWrite  * counts.estWritten  },
    { lbl:'Client emails (auto-drafted)', count: counts.emails,    hrs: rates.emailWrite * counts.emails     },
    { lbl:'Small site migrations',      count: counts.migSm,       hrs: rates.migrationSm * counts.migSm },
    { lbl:'Large site migrations',      count: counts.migLg,       hrs: rates.migrationLg * counts.migLg },
  ];
  const totalHrs = lines.reduce((a,l) => a + l.hrs, 0);
  const totalDollars = totalHrs * rates.hourly;

  return (
    <div>
      <div style={{ display:'flex', alignItems:'baseline', justifyContent:'space-between', flexWrap:'wrap', gap:10, marginBottom:14 }}>
        <div>
          <div style={{ fontSize:'2.2rem', fontWeight:700, fontFamily:'var(--font-brand)', color:'var(--green)', lineHeight:1 }}>
            ${totalDollars.toLocaleString(undefined, { maximumFractionDigits:0 })}
          </div>
          <div className="mono" style={{ fontSize:'.7rem', color:'var(--dim)', marginTop:4 }}>SAVED THIS MONTH · {totalHrs.toFixed(1)}HRS</div>
        </div>
        <button className="btn btn-ghost btn-sm" onClick={() => setEditing(v => !v)} aria-expanded={editing}>
          <Icon name="settings" size={12}/>{editing ? 'Done' : 'Edit rates'}
        </button>
      </div>

      <div style={{ display:'flex', flexDirection:'column', gap:0 }}>
        {lines.map((l, i) => (
          <div key={i} style={{ display:'flex', gap:10, padding:'8px 0', borderBottom: i<lines.length-1?'1px solid var(--border)':'none', alignItems:'center' }}>
            <div style={{ flex:1, fontSize:'.8rem' }}>{l.lbl} <span className="mono" style={{ fontSize:'.7rem', color:'var(--dim)' }}>×{l.count}</span></div>
            <div className="mono" style={{ fontSize:'.78rem', color:'var(--dim)' }}>{l.hrs.toFixed(1)}h</div>
            <div className="mono" style={{ fontSize:'.78rem', fontWeight:700, color:'var(--green)', minWidth:64, textAlign:'right' }}>
              ${(l.hrs * rates.hourly).toLocaleString(undefined, { maximumFractionDigits:0 })}
            </div>
          </div>
        ))}
      </div>

      {editing && (
        <div style={{ marginTop:12, padding:12, background:'var(--surface-2)', border:'1px solid var(--border)', borderRadius:8 }}>
          <div className="mono" style={{ fontSize:'.66rem', color:'var(--dim)', marginBottom:8 }}>YOUR RATES — used to compute savings</div>
          <div style={{ display:'grid', gridTemplateColumns:'1fr 1fr', gap:10 }}>
            <Field label="Hourly rate ($)"><input type="number" value={rates.hourly} onChange={e => setRates(r => ({...r, hourly:+e.target.value}))}/></Field>
            <Field label="Hrs per scope review"><input type="number" step="0.25" value={rates.estReview} onChange={e => setRates(r => ({...r, estReview:+e.target.value}))}/></Field>
            <Field label="Hrs per estimate written"><input type="number" step="0.25" value={rates.estWrite} onChange={e => setRates(r => ({...r, estWrite:+e.target.value}))}/></Field>
            <Field label="Hrs per email drafted"><input type="number" step="0.05" value={rates.emailWrite} onChange={e => setRates(r => ({...r, emailWrite:+e.target.value}))}/></Field>
            <Field label="Hrs per small migration"><input type="number" step="0.5" value={rates.migrationSm} onChange={e => setRates(r => ({...r, migrationSm:+e.target.value}))}/></Field>
            <Field label="Hrs per large migration"><input type="number" step="0.5" value={rates.migrationLg} onChange={e => setRates(r => ({...r, migrationLg:+e.target.value}))}/></Field>
          </div>
        </div>
      )}
    </div>
  );
}

function W_OpenEstimates({ api }) {
  const ests = [
    { client:'Northwind Traders', val:'$8,400',  status:'sent',     ago:'2d'  },
    { client:'Acme Holdings',     val:'$12,200', status:'viewed',   ago:'4d'  },
    { client:'Globex Corp',       val:'$3,800',  status:'draft',    ago:'1d'  },
  ];
  const total = '$24,400';
  return (
    <div>
      <div style={{ display:'flex', alignItems:'baseline', justifyContent:'space-between', marginBottom:10 }}>
        <div>
          <div style={{ fontSize:'1.6rem', fontWeight:700, fontFamily:'var(--font-brand)' }}>{total}</div>
          <div className="mono" style={{ fontSize:'.66rem', color:'var(--dim)' }}>OPEN PIPELINE · {ests.length} PROPOSALS</div>
        </div>
      </div>
      {ests.map((e, i) => (
        <div key={i} style={{ display:'flex', gap:10, padding:'7px 0', borderBottom: i<ests.length-1?'1px solid var(--border)':'none', alignItems:'center' }}>
          <div style={{ flex:1, minWidth:0 }}>
            <div style={{ fontSize:'.8rem', fontWeight:500, overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap' }}>{e.client}</div>
            <div className="mono" style={{ fontSize:'.66rem', color:'var(--dim)' }}>{e.status} · {e.ago} ago</div>
          </div>
          <div className="mono" style={{ fontSize:'.82rem', fontWeight:700 }}>{e.val}</div>
        </div>
      ))}
      <button className="btn btn-ghost btn-sm" style={{ marginTop:10, justifyContent:'center', width:'100%' }} onClick={() => api?.switchTab('estimator')}>Open Estimator →</button>
    </div>
  );
}

function W_MrrMini() {
  const data = [3120, 3280, 3340, 3580, 3710, 3850, 3970];
  return (
    <div>
      <div style={{ display:'flex', alignItems:'baseline', justifyContent:'space-between', marginBottom:6 }}>
        <div>
          <div style={{ fontSize:'1.8rem', fontWeight:700, fontFamily:'var(--font-brand)', color:'var(--green)' }}>$3,970</div>
          <div className="mono" style={{ fontSize:'.66rem', color:'var(--green)' }}>↑ +3.2% MoM</div>
        </div>
        <div style={{ flex:1, marginLeft:14 }}>
          <Sparkline data={data} color="var(--green)" height={48}/>
        </div>
      </div>
      <div className="mono" style={{ fontSize:'.62rem', color:'var(--dim)', marginTop:6 }}>MRR · LAST 7 MONTHS</div>
    </div>
  );
}

function W_ChurnRisk() {
  const at = [
    { client:'Initech LLC',   risk:78, reason:'Last login 32d ago' },
    { client:'Stark Industries', risk:62, reason:'Support escalations ↑' },
    { client:'Wayne Co.',     risk:45, reason:'Slow renewal response' },
  ];
  return (
    <div style={{ display:'flex', flexDirection:'column' }}>
      {at.map((c, i) => (
        <div key={i} style={{ display:'flex', alignItems:'center', gap:10, padding:'8px 0', borderBottom: i<at.length-1?'1px solid var(--border)':'none' }}>
          <div style={{ flex:1, minWidth:0 }}>
            <div style={{ fontSize:'.82rem', fontWeight:600 }}>{c.client}</div>
            <div className="mono" style={{ fontSize:'.66rem', color:'var(--dim)' }}>{c.reason}</div>
          </div>
          <div style={{ textAlign:'right', minWidth:50 }}>
            <div className="mono" style={{ fontSize:'.78rem', fontWeight:700, color: c.risk >= 70 ? 'var(--rose)' : c.risk >= 50 ? 'var(--warn)' : 'var(--dim)' }}>{c.risk}%</div>
            <div className="mono" style={{ fontSize:'.58rem', color:'var(--dim)' }}>RISK</div>
          </div>
        </div>
      ))}
    </div>
  );
}

/* ── WIDGET DISPATCHER ──────────────────────────────────────────── */
const RENDERERS = {
  'recent-activity': W_RecentActivity,
  'site-health':     W_SiteHealth,
  'recent-notices':  W_RecentNotices,
  'upcoming-work':   W_UpcomingWork,
  'top-recs':        W_TopRecs,
  'goals':           W_Goals,
  'stats-chart':     W_StatsChart,
  'seo-snapshot':    W_SeoSnapshot,
  'scan-volume':     W_ScanVolume,
  'savings':         W_Savings,
  'open-estimates':  W_OpenEstimates,
  'mrr-mini':        W_MrrMini,
  'churn-risk':      W_ChurnRisk,
};

function WidgetCard({ widget, api, editing, onRemove, vizState, setViz, overview }) {
  const Renderer = RENDERERS[widget.id];
  if (!Renderer) return null;
  return (
    <div className="card" style={{ position:'relative', gridColumn: widget.size === '2x1' ? 'span 2' : 'auto' }}>
      <div className="card-head">
        <h2 className="card-title" style={{ display:'flex', alignItems:'center', gap:8 }}>
          <span aria-hidden="true">{widget.icon}</span>
          <span>{widget.label}</span>
        </h2>
        <div style={{ display:'flex', gap:6, alignItems:'center' }}>
          <span className="tag" style={{ background: 'transparent', borderColor: TIER_COLOR[widget.tier], color: TIER_COLOR[widget.tier] }}>{TIER_LABEL[widget.tier].toUpperCase()}</span>
          {editing && (
            <button className="icon-btn" onClick={onRemove} aria-label={`Remove ${widget.label} widget`} title="Remove">
              <Icon name="x" size={12}/>
            </button>
          )}
        </div>
      </div>
      <div className="card-body">
        <Renderer api={api} viz={vizState} setViz={setViz} overview={overview}/>
      </div>
    </div>
  );
}

/* ── DASHBOARD SHELL ────────────────────────────────────────────── */
function Dashboard({ s, api }) {
  const r = window.WPSBD.ROLES[s.role];
  const userTier = ROLE_TIER[s.role];
  const isSA = s.role === 'sa';

  const [tab, setTab] = useState(() => localStorage.getItem('wpsbd-dash-tab') || 'overview');
  const [editing, setEditing] = useState(false);
  const [layout, setLayout] = useState(() => {
    const saved = localStorage.getItem(`wpsbd-dash-layout-${s.role}`);
    if (saved) try { return JSON.parse(saved); } catch {}
    return DEFAULT_LAYOUT[s.role] || DEFAULT_LAYOUT.customer;
  });
  /* per-widget viz preference */
  const [vizPrefs, setVizPrefs] = useState(() => {
    const saved = localStorage.getItem('wpsbd-dash-viz');
    if (saved) try { return JSON.parse(saved); } catch {}
    return { 'stats-chart':'area', 'seo-snapshot':'list', 'scan-volume':'bar' };
  });

  useEffect(() => { localStorage.setItem('wpsbd-dash-tab', tab); }, [tab]);
  useEffect(() => { localStorage.setItem(`wpsbd-dash-layout-${s.role}`, JSON.stringify(layout)); }, [layout, s.role]);
  useEffect(() => { localStorage.setItem('wpsbd-dash-viz', JSON.stringify(vizPrefs)); }, [vizPrefs]);

  /* 2026-05-19 — fetch real account dashboard data (replaces hardcoded acme.co
     placeholders in widget renderers). Refreshes every 60s. */
  const [overview, setOverview] = useState(null);
  const [overviewErr, setOverviewErr] = useState(null);
  useEffect(() => {
    let cancelled = false;
    function fetchOverview() {
      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) { setOverviewErr('No token'); return; }
      fetch(`${RAILWAY}/dashboard/overview`, {
        headers: { 'Authorization': 'Bearer ' + token },
      })
        .then(r => r.json().then(b => ({ ok: r.ok, status: r.status, body: b })))
        .then(({ ok, status, body }) => {
          if (cancelled) return;
          if (!ok) { setOverviewErr(`HTTP ${status}: ${body.error || ''}`); return; }
          setOverview(body);
          setOverviewErr(null);
        })
        .catch(e => { if (!cancelled) setOverviewErr(e.message); });
    }
    fetchOverview();
    const id = setInterval(fetchOverview, 60_000);
    return () => { cancelled = true; clearInterval(id); };
  }, []);

  /* Derived stat values — fall back to "—" when data hasn't loaded */
  const sitesCount    = overview?.stats?.sites_count ?? null;
  const sitesOnline   = overview?.stats?.sites_online ?? null;
  const latestVersion = overview?.stats?.latest_plugin_version ?? null;
  const outdatedCount = overview?.stats?.outdated_count ?? null;
  const openIssues    = overview?.stats?.open_issues_count ?? null;

  /* Available widgets to add: at-or-below user's tier (SA gets all),
     filtered to current panel, excluding ones already in layout. */
  const addable = useMemo(() => WIDGETS.filter(w =>
    w.panel === tab &&
    !layout.includes(w.id) &&
    (isSA || w.tier <= userTier)
  ), [tab, layout, userTier, isSA]);

  /* Active widgets for this tab, in user's order */
  const activeWidgets = layout
    .map(id => WIDGETS.find(w => w.id === id))
    .filter(w => w && w.panel === tab && (isSA || w.tier <= userTier));

  const removeWidget = id => setLayout(L => L.filter(x => x !== id));
  const addWidget    = id => setLayout(L => [...L, id]);
  const resetLayout  = () => setLayout(DEFAULT_LAYOUT[s.role] || DEFAULT_LAYOUT.customer);

  /* Stat cards stay above the tabs — they're cross-panel */
  return (
    <div>
      <PageHead
        crumb="Overview"
        title="Dashboard"
        sub={`Welcome back, ${(window.WPSBD.getUserName ? window.WPSBD.getUserName() : 'there').split(' ')[0]}. Here's what's happening across your sites.`}
        actions={<>
          <button className="btn btn-ghost btn-sm" onClick={() => setEditing(v => !v)} aria-pressed={editing}>
            <Icon name={editing ? 'check' : 'sliders'} size={13}/>{editing ? 'Done editing' : 'Edit dashboard'}
          </button>
          <button className="btn btn-ghost btn-sm" onClick={() => window.wpsbOpenQuickSearch?.() || window.wpsbToast?.('Quick Search · press ⌘K anywhere', 'info')}><Icon name="search" size={14}/>Search</button>
          <button className="btn btn-primary btn-sm" onClick={() => api.switchTab('sites')}><Icon name="plus" size={14}/>Add site</button>
        </>}
      />

      {/* Top stat strip — same on every tab. 2026-05-19: wired to real
          per-account data from /dashboard/overview. Falls back to "—"
          while loading or if the fetch fails. */}
      <div className="grid grid-4 grid-stretch" style={{ marginBottom: 16 }}>
        <Stat
          label="Sites monitored"
          value={sitesCount != null ? String(sitesCount) : '—'}
          sub={
            sitesCount == null ? 'Loading…' :
            sitesCount === 0   ? 'None yet — add one' :
            sitesOnline === sitesCount ? 'All online' :
                                  `${sitesOnline ?? 0} of ${sitesCount} online`
          }
          tone={sitesCount === 0 ? 'warn' : 'ok'}
        />
        <Stat
          label="Avg health"
          value="—"
          sub={sitesCount === 0 ? 'No sites' : 'Computed from scans'}
        />
        <Stat
          label="Open issues"
          value={openIssues != null ? String(openIssues) : '—'}
          sub={openIssues == null ? 'Not yet computed' : (openIssues === 0 ? 'All clear' : 'See alerts')}
          tone={openIssues && openIssues > 0 ? 'warn' : undefined}
        />
        <Stat
          label="Plugin version"
          value={latestVersion || '—'}
          sub={
            !latestVersion       ? 'No plugin installed' :
            outdatedCount === 0  ? 'All up to date' :
                                   `${outdatedCount} site${outdatedCount === 1 ? '' : 's'} outdated`
          }
          tone={outdatedCount && outdatedCount > 0 ? 'warn' : undefined}
        />
      </div>

      {overviewErr && (
        <div className="box warn" style={{ marginBottom: 12, fontSize: '.78rem' }}>
          Dashboard data unavailable: {overviewErr}. Showing limited info.
        </div>
      )}

      {/* Sub-tabs */}
      <div className="sub-tabs" role="tablist" style={{ marginBottom: 14 }}>
        {[
          ['overview',    'Overview'],
          ['performance', 'Performance'],
          ['financial',   (s.role === 'customer' ? 'Savings' : 'Financial')],
        ].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>

      {/* Edit mode banner */}
      {editing && (
        <div className="box info" style={{ marginBottom: 14, display:'flex', justifyContent:'space-between', alignItems:'center', flexWrap:'wrap', gap:10 }}>
          <div style={{ display:'flex', alignItems:'center', gap:10 }}>
            <Icon name="sliders" size={14}/>
            <span><strong>Edit mode.</strong> Remove widgets with the × icon, or add from the tray below. {isSA && <span style={{ color:'var(--rose)' }}>SA: you can add any-tier widgets.</span>}</span>
          </div>
          <button className="btn btn-ghost btn-sm" onClick={resetLayout}>Reset to default</button>
        </div>
      )}

      {/* Widget grid */}
      <div className="grid grid-2" style={{ gridAutoRows: '1fr' }}>
        {activeWidgets.length === 0 && (
          <div className="card" style={{ gridColumn:'span 2', padding:'40px 20px', textAlign:'center', color:'var(--dim)' }}>
            <div style={{ fontSize:'2rem', marginBottom:8 }} aria-hidden="true">📭</div>
            <div style={{ fontWeight:600, marginBottom:4 }}>No widgets on this tab</div>
            <div style={{ fontSize:'.82rem' }}>Click <strong>Edit dashboard</strong> above to add some.</div>
          </div>
        )}
        {activeWidgets.map(w => (
          <WidgetCard
            key={w.id}
            widget={w}
            api={api}
            editing={editing}
            onRemove={() => removeWidget(w.id)}
            vizState={vizPrefs[w.id]}
            setViz={v => setVizPrefs(p => ({...p, [w.id]: v}))}
            overview={overview}
          />
        ))}
      </div>

      {/* Add-widget tray */}
      {editing && (
        <div className="card" style={{ marginTop:16, borderStyle:'dashed', borderColor:'var(--border-2)' }}>
          <div className="card-head">
            <h2 className="card-title">+ Add widget to {tab}</h2>
            <span className="tag">{addable.length} AVAILABLE</span>
          </div>
          <div className="card-body">
            {addable.length === 0 ? (
              <div style={{ color:'var(--dim)', fontSize:'.85rem', textAlign:'center', padding:'20px 0' }}>
                All available widgets for your tier are already on this tab.
              </div>
            ) : (
              <div className="grid" style={{ gridTemplateColumns: 'repeat(auto-fill, minmax(220px, 1fr))', gap:10 }}>
                {addable.map(w => {
                  const aboveUserTier = w.tier > userTier;
                  return (
                    <button key={w.id}
                      onClick={() => addWidget(w.id)}
                      className="card"
                      style={{
                        textAlign:'left', cursor:'pointer', padding:0,
                        background: aboveUserTier ? 'rgba(192,53,123,0.05)' : 'var(--surface)',
                        borderColor: aboveUserTier ? 'var(--rose)' : 'var(--border)',
                      }}
                      aria-label={`Add ${w.label} widget`}>
                      <div className="card-body" style={{ padding:12 }}>
                        <div style={{ display:'flex', alignItems:'center', gap:8, marginBottom:6 }}>
                          <span style={{ fontSize:'1.1rem' }} aria-hidden="true">{w.icon}</span>
                          <span style={{ fontWeight:600, fontSize:'.85rem', flex:1 }}>{w.label}</span>
                          <span className="tag" style={{ background:'transparent', borderColor: TIER_COLOR[w.tier], color: TIER_COLOR[w.tier], fontSize:'.58rem' }}>
                            {TIER_LABEL[w.tier].toUpperCase()}
                          </span>
                        </div>
                        <div style={{ fontSize:'.74rem', color:'var(--dim)', lineHeight:1.4 }}>{w.desc}</div>
                        {aboveUserTier && isSA && (
                          <div className="mono" style={{ fontSize:'.62rem', color:'var(--rose)', marginTop:6 }}>SA OVERRIDE · normally restricted</div>
                        )}
                      </div>
                    </button>
                  );
                })}
              </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 can add any-tier widget — even Customer/Partner-only ones — for testing or impersonation.</div>
        </div>
      )}
    </div>
  );
}

window.Dashboard = Dashboard;
