const { useState, useEffect, useMemo } = React;

const CFG = window.FEED_CONFIG;
const SUPA_URL = CFG.supabaseUrl;
const SUPA_HEADERS = {
  apikey: CFG.supabaseAnonKey,
  Authorization: 'Bearer ' + CFG.supabaseAnonKey,
  'Content-Type': 'application/json',
  Prefer: 'return=representation'
};

// ─────────────────────────────────────────────
// Supabase REST helpers
// ─────────────────────────────────────────────
async function api(path, opts) {
  const res = await fetch(SUPA_URL + '/rest/v1' + path, Object.assign({ headers: SUPA_HEADERS }, opts || {}));
  const text = await res.text();
  let body = null;
  try { body = text ? JSON.parse(text) : null; } catch (e) { body = text; }
  if (!res.ok) {
    const msg = (body && body.message) || text || ('HTTP ' + res.status);
    throw new Error(msg);
  }
  return body;
}

const Events = {
  list: () => api('/webinar_events?order=event_date.asc'),
  create: (row) => api('/webinar_events', { method: 'POST', body: JSON.stringify(row) }),
  update: (id, patch) => api('/webinar_events?id=eq.' + id, { method: 'PATCH', body: JSON.stringify(patch) }),
  remove: (id) => api('/webinar_events?id=eq.' + id, { method: 'DELETE' })
};

const Questions = {
  list: () => api('/form_questions?order=position.asc'),
  listForForm: (formId) => api('/form_questions?form_id=eq.' + formId + '&order=position.asc'),
  create: (row) => api('/form_questions', { method: 'POST', body: JSON.stringify(row) }),
  update: (id, patch) => api('/form_questions?id=eq.' + id, { method: 'PATCH', body: JSON.stringify(patch) }),
  remove: (id) => api('/form_questions?id=eq.' + id, { method: 'DELETE' })
};

const Forms = {
  list: () => api('/forms?order=created_at.asc'),
  create: (row) => api('/forms', { method: 'POST', body: JSON.stringify(row) }),
  update: (id, patch) => api('/forms?id=eq.' + id, { method: 'PATCH', body: JSON.stringify(patch) }),
  remove: (id) => api('/forms?id=eq.' + id, { method: 'DELETE' })
};

const Funnels = {
  list: () => api('/funnels?order=landing_variant.asc'),
  update: (id, patch) => api('/funnels?id=eq.' + id, { method: 'PATCH', body: JSON.stringify(patch) })
};

const Registrants = {
  list: () => api('/registrants?order=registered_at.desc&select=*,webinar_events(title,event_date)')
};

const Videos = {
  list: (placement) => api('/videos?placement=eq.' + encodeURIComponent(placement) + '&order=created_at.desc'),
  create: (row) => api('/videos', { method: 'POST', body: JSON.stringify(row) }),
  update: (id, patch) => api('/videos?id=eq.' + id, { method: 'PATCH', body: JSON.stringify(patch) }),
  remove: (id) => api('/videos?id=eq.' + id, { method: 'DELETE' })
};

const MetaAds = {
  list: (sinceIso) => api('/meta_ad_spend?date=gte.' + sinceIso + '&order=date.desc&limit=20000'),
  status: () => api('/app_settings?key=eq.meta_sync_status').then(rs => rs[0] && rs[0].value),
};

// ─────────────────────────────────────────────
// Date helpers (form ↔ DB)
// ─────────────────────────────────────────────
// Convert UTC timestamptz to a string the <input type="datetime-local"> can show in the user's local tz
function utcToLocalInput(utcIso) {
  if (!utcIso) return '';
  const d = new Date(utcIso);
  if (isNaN(d.getTime())) return '';
  const pad = (n) => String(n).padStart(2, '0');
  return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate())
    + 'T' + pad(d.getHours()) + ':' + pad(d.getMinutes());
}

// Convert datetime-local input value (treated as local time) → UTC ISO
function localInputToUtc(local) {
  if (!local) return null;
  return new Date(local).toISOString();
}

// Display formatter: "Mon, Jun 22, 2026 · 12:00 PM EDT"
function formatEventDate(utcIso) {
  if (!utcIso) return '';
  const d = new Date(utcIso);
  const datePart = new Intl.DateTimeFormat('en-US', {
    weekday: 'short', month: 'short', day: 'numeric', year: 'numeric',
    timeZone: CFG.displayTimeZone
  }).format(d);
  const timePart = new Intl.DateTimeFormat('en-US', {
    hour: 'numeric', minute: '2-digit', hour12: true,
    timeZone: CFG.displayTimeZone
  }).format(d);
  const zone = new Intl.DateTimeFormat('en-US', {
    timeZoneName: 'short', timeZone: CFG.displayTimeZone
  }).format(d).split(' ').pop();
  return datePart + ' · ' + timePart + ' ' + zone;
}

function relativeTo(utcIso) {
  if (!utcIso) return '';
  const diff = new Date(utcIso).getTime() - Date.now();
  const absMs = Math.abs(diff);
  const days = Math.floor(absMs / 86400000);
  const hours = Math.floor((absMs / 3600000) % 24);
  if (days > 0) return (diff > 0 ? 'in ' : '') + days + 'd ' + hours + 'h' + (diff < 0 ? ' ago' : '');
  if (hours > 0) return (diff > 0 ? 'in ' : '') + hours + 'h' + (diff < 0 ? ' ago' : '');
  return diff > 0 ? 'soon' : 'just past';
}

// ─────────────────────────────────────────────
// Navigation (left sidebar)
// ─────────────────────────────────────────────
const NAV_ITEMS = [
  {
    id: 'metrics',
    label: 'Funnel Metrics',
    icon: (
      <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
        <path d="M3 3v18h18" />
        <path d="M7 14l4-4 4 4 6-6" />
      </svg>
    ),
    render: () => <MetricsSection />
  },
  {
    id: 'variant_compare',
    label: 'Variant Compare',
    icon: (
      <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
        <rect x="3" y="3" width="7" height="18" rx="1" />
        <rect x="14" y="3" width="7" height="18" rx="1" />
      </svg>
    ),
    render: () => <VariantCompareSection />
  },
  {
    id: 'meta_ads',
    label: 'Meta Ad Data',
    icon: (
      <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
        <path d="M22 12c0-5.52-4.48-10-10-10S2 6.48 2 12c0 4.84 3.44 8.87 8 9.8V15H8v-3h2V9.5C10 7.57 11.57 6 13.5 6H16v3h-2c-.55 0-1 .45-1 1v2h3v3h-3v6.95c5.05-.5 9-4.76 9-9.95z" />
      </svg>
    ),
    render: () => <MetaAdDataSection />
  },
  {
    id: 'blended',
    label: 'Blended Data',
    icon: (
      <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
        <line x1="12" y1="1" x2="12" y2="23" />
        <path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6" />
      </svg>
    ),
    render: () => <BlendedDataSection />
  },
  {
    id: 'events',
    label: 'Webinar Schedule',
    icon: (
      <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
        <rect x="3" y="4" width="18" height="18" rx="2" />
        <path d="M16 2v4M8 2v4M3 10h18" />
      </svg>
    ),
    render: () => <EventsSection />
  },
  {
    id: 'forms',
    label: 'Forms',
    icon: (
      <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
        <path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3" />
        <path d="M12 17h.01" />
        <circle cx="12" cy="12" r="10" />
      </svg>
    ),
    render: () => <FormsSection />
  },
  {
    id: 'post_reg_videos',
    label: 'Post-Reg Videos',
    icon: (
      <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
        <polygon points="23 7 16 12 23 17 23 7" />
        <rect x="1" y="5" width="15" height="14" rx="2" ry="2" />
      </svg>
    ),
    render: () => <VideosSection placement="post_reg" title="Post-Registration Videos" subtitle="The video shown to registrants on the thank-you page. Mark one as active and the post-reg page swaps to it live." />
  },
  {
    id: 'review',
    label: 'Design Review',
    icon: (
      <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
        <path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z" />
      </svg>
    ),
    fullBleed: true,  // No main padding — iframe fills the whole content area
    render: () => <ReviewEmbed />
  },
  {
    id: 'registrants',
    label: 'Registrants',
    icon: (
      <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
        <path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" />
        <circle cx="9" cy="7" r="4" />
        <path d="M23 21v-2a4 4 0 0 0-3-3.87M16 3.13a4 4 0 0 1 0 7.75" />
      </svg>
    ),
    render: () => <RegistrantsSection />
  },
  {
    id: 'organic',
    label: 'Organic (Nathan)',
    icon: (
      <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
        <rect x="3" y="4" width="18" height="18" rx="2" />
        <path d="M16 2v4M8 2v4M3 10h18" />
        <path d="M9 16l2 2 4-4" />
      </svg>
    ),
    render: () => <OrganicSection />
  }
  // Future: Attribution · Campaigns · Live Feed · Settings
];

// ─────────────────────────────────────────────
// UI
// ─────────────────────────────────────────────
function App() {
  const [routeId, setRouteId] = useState('metrics');
  const active = NAV_ITEMS.find(n => n.id === routeId) || NAV_ITEMS[0];
  const isFullBleed = active.fullBleed === true;
  return (
    <div style={{ display: 'flex', minHeight: '100vh', alignItems: 'stretch' }}>
      <Sidebar activeId={routeId} onSelect={setRouteId} />
      <main style={{
        flex: 1,
        padding: isFullBleed ? 0 : '40px 48px 80px',
        minWidth: 0, overflowX: 'hidden',
        // Full-bleed sections (like Design Review embed) need to be exactly viewport-height
        height: isFullBleed ? '100vh' : 'auto'
      }}>
        {active.render()}
      </main>
    </div>
  );
}

// Embeds the standalone /review.html design-review tool inside the admin shell.
// Same localStorage origin, so the reviewer's name + comments shared with the
// standalone URL persist seamlessly between the embed and the public review URL.
function ReviewEmbed() {
  return (
    <iframe
      src="/review.html"
      style={{
        width: '100%', height: '100%',
        border: 'none', display: 'block',
        background: '#0a0a0a',
      }}
    />
  );
}

function Sidebar({ activeId, onSelect }) {
  return (
    <aside style={{
      width: 240, flexShrink: 0,
      background: '#000',
      borderRight: '1px solid var(--line)',
      padding: '22px 16px',
      display: 'flex', flexDirection: 'column', gap: 20,
      position: 'sticky', top: 0, alignSelf: 'flex-start',
      height: '100vh'
    }}>
      {/* Brand */}
      <div style={{ padding: '4px 8px 14px', borderBottom: '1px solid var(--line)' }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 6 }}>
          <span className="mono" style={{
            fontSize: 9, letterSpacing: '0.22em', textTransform: 'uppercase',
            color: 'var(--accent)', fontWeight: 700,
            padding: '3px 7px', border: '1px solid var(--accent)', borderRadius: 3
          }}>FUNNEL HQ</span>
        </div>
        <div style={{
          fontFamily: "'Geist', system-ui, sans-serif",
          fontWeight: 800, fontSize: 18, letterSpacing: '-0.035em', lineHeight: 1.1
        }}>The Feed Media</div>
      </div>

      {/* Nav */}
      <nav style={{ display: 'flex', flexDirection: 'column', gap: 2, flex: 1 }}>
        {NAV_ITEMS.map(item => (
          <NavLink
            key={item.id}
            active={item.id === activeId}
            onClick={() => onSelect(item.id)}
            icon={item.icon}
          >
            {item.label}
          </NavLink>
        ))}
      </nav>

      {/* Footer */}
      <SidebarUser />

      <div className="mono" style={{
        fontSize: 10, letterSpacing: '0.2em', color: 'var(--muted-2)',
        padding: '12px 8px 4px', borderTop: '1px solid var(--line)'
      }}>
        v0.1 · LOCAL
      </div>
    </aside>
  );
}

// Shows the signed-in user's email + a sign-out button at the bottom of the sidebar
function SidebarUser() {
  const [email, setEmail] = useState(null);
  const [hover, setHover] = useState(false);
  useEffect(() => {
    let cancelled = false;
    if (window.FeedAuth) {
      window.FeedAuth.getUser().then(u => { if (!cancelled) setEmail(u && u.email); });
    }
    return () => { cancelled = true; };
  }, []);
  if (!email) return null;
  return (
    <div style={{
      padding: '12px 8px',
      borderTop: '1px solid var(--line)',
      display: 'flex', flexDirection: 'column', gap: 6
    }}>
      <div className="mono" style={{ fontSize: 9, letterSpacing: '0.2em', color: 'var(--muted)', fontWeight: 600 }}>
        SIGNED IN AS
      </div>
      <div style={{ fontSize: 12, color: 'var(--ink-2)', wordBreak: 'break-all' }}>
        {email}
      </div>
      <button
        onClick={() => window.FeedAuth.signOut()}
        onMouseEnter={() => setHover(true)}
        onMouseLeave={() => setHover(false)}
        style={{
          marginTop: 4,
          padding: '6px 10px',
          background: hover ? 'rgba(239,68,68,0.10)' : 'transparent',
          border: '1px solid ' + (hover ? 'rgba(239,68,68,0.35)' : 'var(--line-2)'),
          borderRadius: 5,
          color: hover ? '#fca5a5' : 'var(--muted)',
          fontSize: 11, fontFamily: 'inherit', cursor: 'pointer',
          textAlign: 'left',
          transition: 'all 140ms'
        }}
      >Sign out →</button>
    </div>
  );
}

function NavLink({ active, onClick, icon, children }) {
  const [hover, setHover] = useState(false);
  return (
    <button
      onClick={onClick}
      onMouseEnter={() => setHover(true)}
      onMouseLeave={() => setHover(false)}
      style={{
        display: 'flex', alignItems: 'center', gap: 11,
        padding: '9px 12px',
        background: active
          ? 'rgba(168,85,247,0.12)'
          : (hover ? 'rgba(255,255,255,0.03)' : 'transparent'),
        color: active ? 'var(--accent)' : (hover ? 'var(--ink)' : 'var(--ink-2)'),
        border: 'none',
        borderRadius: 6,
        fontSize: 13, fontWeight: active ? 600 : 500,
        letterSpacing: '-0.005em',
        cursor: 'pointer',
        textAlign: 'left',
        position: 'relative',
        transition: 'background 140ms, color 140ms'
      }}
    >
      {active && (
        <span style={{
          position: 'absolute', left: 0, top: 8, bottom: 8, width: 2,
          background: 'var(--accent)', borderRadius: 2
        }} />
      )}
      <span style={{ display: 'inline-flex', opacity: active ? 1 : 0.7 }}>{icon}</span>
      <span>{children}</span>
    </button>
  );
}

function EventsSection() {
  const [events, setEvents] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  const [editing, setEditing] = useState(null); // null = no form open, {} = creating, {id, ...} = editing

  async function refresh() {
    setLoading(true); setError(null);
    try {
      const rows = await Events.list();
      setEvents(rows);
      // Push a refresh into FeedEvent's cache so the landing/post-reg pages see new data on reload
      window.FeedEvent && window.FeedEvent.refresh && window.FeedEvent.refresh();
    } catch (e) {
      setError(e.message);
    } finally {
      setLoading(false);
    }
  }

  useEffect(() => { refresh(); }, []);

  const now = Date.now();
  const upcoming = useMemo(() => events.filter(e => new Date(e.event_date).getTime() > now), [events, now]);
  const past = useMemo(() => events.filter(e => new Date(e.event_date).getTime() <= now), [events, now]);

  return (
    <section>
      <SectionHeader
        eyebrow="Webinar events"
        title="Schedule"
        subtitle="The next upcoming event populates the landing + post-registration pages automatically."
        action={
          <button onClick={() => setEditing({})} style={btnPrimary}>
            + Add Event
          </button>
        }
      />

      {error && <ErrorBanner message={error} onDismiss={() => setError(null)} />}

      {editing !== null && (
        <EventForm
          initial={editing}
          onCancel={() => setEditing(null)}
          onSaved={async () => { setEditing(null); await refresh(); }}
        />
      )}

      <Subhead>Upcoming ({upcoming.length})</Subhead>
      {loading ? (
        <Empty label="Loading…" />
      ) : upcoming.length === 0 ? (
        <Empty label="No upcoming events. Click + Add Event to create one." />
      ) : (
        <EventList events={upcoming} onEdit={setEditing} onDeleted={refresh} highlight />
      )}

      <Subhead>Past ({past.length})</Subhead>
      {past.length === 0 ? (
        <Empty label="No past events yet." />
      ) : (
        <EventList events={past} onEdit={setEditing} onDeleted={refresh} dimmed />
      )}
    </section>
  );
}

function SectionHeader({ eyebrow, title, subtitle, action }) {
  return (
    <div style={{
      display: 'flex', alignItems: 'flex-end', justifyContent: 'space-between',
      gap: 24, marginBottom: 28, flexWrap: 'wrap'
    }}>
      <div>
        <div className="mono" style={{
          fontSize: 11, letterSpacing: '0.22em', textTransform: 'uppercase',
          color: 'var(--accent)', fontWeight: 600, marginBottom: 8
        }}>{eyebrow}</div>
        <h1 style={{
          fontSize: 38, fontWeight: 700, letterSpacing: '-0.025em',
          margin: 0, lineHeight: 1.1
        }}>{title}</h1>
        {subtitle && <p style={{ color: 'var(--muted)', margin: '10px 0 0', maxWidth: 640 }}>{subtitle}</p>}
      </div>
      {action}
    </div>
  );
}

function Subhead({ children }) {
  return (
    <div className="mono" style={{
      fontSize: 11, letterSpacing: '0.2em', textTransform: 'uppercase',
      color: 'var(--muted)', fontWeight: 600,
      margin: '32px 0 14px', paddingBottom: 8, borderBottom: '1px solid var(--line)'
    }}>{children}</div>
  );
}

function Empty({ label }) {
  return (
    <div style={{
      color: 'var(--muted-2)', fontSize: 13, padding: '18px 0',
      fontStyle: 'italic'
    }}>{label}</div>
  );
}

function EventList({ events, onEdit, onDeleted, highlight, dimmed }) {
  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
      {events.map((ev, i) => (
        <EventCard
          key={ev.id}
          event={ev}
          isNext={highlight && i === 0}
          dimmed={dimmed}
          onEdit={() => onEdit(ev)}
          onDeleted={onDeleted}
        />
      ))}
    </div>
  );
}

function EventCard({ event, isNext, dimmed, onEdit, onDeleted }) {
  const [busy, setBusy] = useState(false);

  async function handleDelete() {
    if (!confirm('Delete this event? This cannot be undone.\n\n' + event.title)) return;
    setBusy(true);
    try {
      await Events.remove(event.id);
      onDeleted();
    } catch (e) {
      alert('Delete failed: ' + e.message);
      setBusy(false);
    }
  }

  return (
    <div style={{
      background: 'var(--card)',
      border: '1px solid ' + (isNext ? 'var(--accent)' : 'var(--line)'),
      borderRadius: 10, padding: '18px 20px',
      display: 'grid', gridTemplateColumns: '1fr auto', gap: 20,
      opacity: dimmed ? 0.5 : 1,
      transition: 'border-color 200ms'
    }}>
      <div>
        <div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 8 }}>
          {isNext && (
            <span className="mono" style={{
              fontSize: 9, letterSpacing: '0.22em', fontWeight: 700,
              padding: '3px 8px', borderRadius: 999,
              background: 'var(--accent)', color: '#fff'
            }}>NEXT UP</span>
          )}
          <StatusPill status={event.status} />
          <span className="mono" style={{ fontSize: 11, color: 'var(--muted-2)' }}>
            {relativeTo(event.event_date)}
          </span>
        </div>
        <div style={{ fontSize: 17, fontWeight: 600, marginBottom: 4 }}>
          {event.title}
        </div>
        <div className="mono" style={{ fontSize: 12, color: 'var(--ink-2)' }}>
          {formatEventDate(event.event_date)}
        </div>
        {event.notes && (
          <div style={{ fontSize: 13, color: 'var(--muted)', marginTop: 8 }}>
            {event.notes}
          </div>
        )}
      </div>
      <div style={{ display: 'flex', gap: 8, alignItems: 'flex-start' }}>
        <button onClick={onEdit} disabled={busy} style={btnGhost}>Edit</button>
        <button onClick={handleDelete} disabled={busy} style={btnDanger}>Delete</button>
      </div>
    </div>
  );
}

function StatusPill({ status }) {
  const colors = {
    upcoming: { bg: 'rgba(168,85,247,0.15)', fg: '#c084fc' },
    live: { bg: 'rgba(0,255,133,0.15)', fg: '#00ff85' },
    completed: { bg: 'rgba(255,255,255,0.05)', fg: '#8a8a8a' }
  };
  const c = colors[status] || colors.upcoming;
  return (
    <span className="mono" style={{
      fontSize: 9, letterSpacing: '0.2em', textTransform: 'uppercase',
      fontWeight: 700, padding: '3px 8px', borderRadius: 4,
      background: c.bg, color: c.fg
    }}>{status}</span>
  );
}

// Title is auto-generated from the event date — single source of truth so admin list
// and Google Calendar invite always match. Format: "Newsletter Accelerator Webinar - June 22nd, 2026"
function autoTitleFromUtc(utcIso) {
  if (!utcIso) return '';
  const d = new Date(utcIso);
  if (isNaN(d.getTime())) return '';
  const month = new Intl.DateTimeFormat('en-US', { month: 'long', timeZone: CFG.displayTimeZone }).format(d);
  const day = parseInt(new Intl.DateTimeFormat('en-US', { day: 'numeric', timeZone: CFG.displayTimeZone }).format(d), 10);
  const year = new Intl.DateTimeFormat('en-US', { year: 'numeric', timeZone: CFG.displayTimeZone }).format(d);
  function suffix(n) {
    const v = n % 100;
    if (v >= 11 && v <= 13) return 'th';
    switch (n % 10) { case 1: return 'st'; case 2: return 'nd'; case 3: return 'rd'; default: return 'th'; }
  }
  return 'Newsletter Accelerator Webinar - ' + month + ' ' + day + suffix(day) + ', ' + year;
}

function EventForm({ initial, onCancel, onSaved }) {
  const isEdit = !!initial.id;
  const [eventLocal, setEventLocal] = useState(utcToLocalInput(initial.event_date));
  const [status, setStatus] = useState(initial.status || 'upcoming');
  const [campaignId, setCampaignId] = useState(initial.campaign_id || '');
  const [notes, setNotes] = useState(initial.notes || '');
  const [saving, setSaving] = useState(false);
  const [err, setErr] = useState(null);

  const previewTitle = eventLocal ? autoTitleFromUtc(localInputToUtc(eventLocal)) : '';

  async function handleSubmit(e) {
    e.preventDefault();
    setErr(null);
    if (!eventLocal) {
      setErr('Date/time is required.');
      return;
    }
    setSaving(true);
    try {
      const utcIso = localInputToUtc(eventLocal);
      const payload = {
        title: autoTitleFromUtc(utcIso),
        event_date: utcIso,
        status,
        campaign_id: campaignId.trim() || null,
        notes: notes.trim() || null
      };
      if (isEdit) await Events.update(initial.id, payload);
      else await Events.create(payload);
      onSaved();
    } catch (e) {
      setErr(e.message);
      setSaving(false);
    }
  }

  return (
    <form
      onSubmit={handleSubmit}
      style={{
        background: 'var(--bg-2)', border: '1px solid var(--accent)',
        borderRadius: 12, padding: 24, marginBottom: 24,
        boxShadow: '0 30px 80px -30px rgba(168,85,247,0.3)'
      }}
    >
      <div className="mono" style={{
        fontSize: 11, letterSpacing: '0.2em', textTransform: 'uppercase',
        color: 'var(--accent)', fontWeight: 700, marginBottom: 16
      }}>
        {isEdit ? 'Edit event' : 'New event'}
      </div>

      <div style={{ display: 'grid', gap: 14 }}>
        <Field
          label="Date & time (your local timezone)"
          hint={'Times are stored in UTC and displayed to registrants in ' + CFG.displayTimeZone}
          required
        >
          <input type="datetime-local" value={eventLocal} onChange={e => setEventLocal(e.target.value)} style={inputStyle} autoFocus />
        </Field>

        {previewTitle && (
          <div style={{
            background: 'rgba(168,85,247,0.06)', border: '1px solid rgba(168,85,247,0.25)',
            borderRadius: 6, padding: '11px 14px', display: 'flex', alignItems: 'center', gap: 10
          }}>
            <span className="mono" style={{ fontSize: 10, letterSpacing: '0.18em', color: 'var(--accent)', fontWeight: 600 }}>
              TITLE →
            </span>
            <span style={{ fontSize: 13, color: 'var(--ink)' }}>{previewTitle}</span>
          </div>
        )}

        <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 14 }}>
          <Field label="Status">
            <select value={status} onChange={e => setStatus(e.target.value)} style={inputStyle}>
              <option value="upcoming">Upcoming</option>
              <option value="live">Live</option>
              <option value="completed">Completed</option>
            </select>
          </Field>
          <Field label="Campaign ID" hint="Optional — tie to an ad campaign for attribution">
            <input value={campaignId} onChange={e => setCampaignId(e.target.value)} style={inputStyle} placeholder="e.g. june-cohort-1" />
          </Field>
        </div>

        <Field label="Notes" hint="Internal only — not shown on landing/post-reg pages">
          <textarea value={notes} onChange={e => setNotes(e.target.value)} style={{ ...inputStyle, resize: 'vertical', minHeight: 80 }} />
        </Field>
      </div>

      {err && <div style={{ color: 'var(--danger)', fontSize: 13, marginTop: 14 }}>{err}</div>}

      <div style={{ display: 'flex', gap: 10, marginTop: 22, justifyContent: 'flex-end' }}>
        <button type="button" onClick={onCancel} disabled={saving} style={btnGhost}>Cancel</button>
        <button type="submit" disabled={saving} style={btnPrimary}>
          {saving ? 'Saving…' : (isEdit ? 'Save changes' : 'Create event')}
        </button>
      </div>
    </form>
  );
}

function Field({ label, hint, required, children }) {
  return (
    <label style={{ display: 'block' }}>
      <div className="mono" style={{
        fontSize: 10, letterSpacing: '0.18em', textTransform: 'uppercase',
        color: 'var(--muted)', fontWeight: 600, marginBottom: 6
      }}>
        {label}{required && <span style={{ color: 'var(--accent)' }}> *</span>}
      </div>
      {children}
      {hint && <div style={{ fontSize: 12, color: 'var(--muted-2)', marginTop: 5 }}>{hint}</div>}
    </label>
  );
}

function ErrorBanner({ message, onDismiss }) {
  return (
    <div style={{
      background: 'rgba(239,68,68,0.08)', border: '1px solid rgba(239,68,68,0.4)',
      borderRadius: 8, padding: '12px 16px', marginBottom: 18,
      display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12,
      color: '#fecaca', fontSize: 13
    }}>
      <span>{message}</span>
      <button onClick={onDismiss} style={{
        background: 'transparent', border: 'none', color: '#fecaca',
        cursor: 'pointer', fontSize: 18, padding: 0
      }}>×</button>
    </div>
  );
}

// ─────────────────────────────────────────────
// Shared styles
// ─────────────────────────────────────────────
const inputStyle = {
  width: '100%',
  padding: '11px 13px',
  background: 'var(--bg-3)',
  border: '1px solid var(--line-2)',
  borderRadius: 6,
  color: 'var(--ink)',
  fontSize: 14,
  outline: 'none',
  fontFamily: 'inherit'
};

const btnPrimary = {
  padding: '10px 18px',
  background: 'linear-gradient(135deg, #7c3aed 0%, #a855f7 48%, #d946ef 100%)',
  color: '#fff',
  border: 'none',
  borderRadius: 6,
  fontSize: 13,
  fontWeight: 600,
  letterSpacing: '0.04em',
  cursor: 'pointer'
};

const btnGhost = {
  padding: '8px 14px',
  background: 'transparent',
  color: 'var(--ink-2)',
  border: '1px solid var(--line-2)',
  borderRadius: 6,
  fontSize: 13,
  fontWeight: 500,
  cursor: 'pointer'
};

const btnDanger = {
  padding: '8px 14px',
  background: 'transparent',
  color: '#fca5a5',
  border: '1px solid rgba(239,68,68,0.35)',
  borderRadius: 6,
  fontSize: 13,
  fontWeight: 500,
  cursor: 'pointer'
};

// ─────────────────────────────────────────────
// FORM QUESTIONS SECTION
// ─────────────────────────────────────────────
// ─────────────────────────────────────────────
// FORMS SECTION — forms list + funnels manager; open a form for Build + Analytics
// ─────────────────────────────────────────────
function FormsSection() {
  const [forms, setForms] = useState([]);
  const [funnels, setFunnels] = useState([]);
  const [counts, setCounts] = useState({});
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  const [selectedForm, setSelectedForm] = useState(null);
  const [creating, setCreating] = useState(false);

  async function refresh() {
    setLoading(true); setError(null);
    try {
      const [fs, fn, qs, regs] = await Promise.all([
        Forms.list(),
        Funnels.list(),
        api('/form_questions?select=id,form_id,active'),
        api('/registrants?status=eq.complete&select=form_id'),
      ]);
      setForms(fs);
      setFunnels(fn);
      const c = {};
      fs.forEach(f => { c[f.id] = { questions: 0, submissions: 0 }; });
      qs.forEach(q => { if (q.active !== false && c[q.form_id]) c[q.form_id].questions++; });
      regs.forEach(r => { if (c[r.form_id]) c[r.form_id].submissions++; });
      setCounts(c);
    } catch (e) { setError(e.message); }
    finally { setLoading(false); }
  }
  useEffect(() => { refresh(); }, []);

  // Duplicate a form + all its questions into a new form (does not copy funnel assignments or submissions).
  async function duplicateForm(form) {
    const base = (form.name || 'Form').replace(/\s*\(copy\)\s*$/i, '').trim();
    const slugBase = base.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 36);
    const slug = (slugBase || 'form') + '-copy-' + Math.random().toString(36).slice(2, 6);
    const res = await Forms.create({ name: base + ' (copy)', slug, description: form.description || '' });
    const created = Array.isArray(res) ? res[0] : res;
    if (!created || !created.id) throw new Error('Could not create the copy.');
    const qs = await Questions.listForForm(form.id);
    for (const q of qs) {
      await Questions.create({
        prompt: q.prompt,
        subheadline: q.subheadline || '',
        options: q.options || [],
        active: q.active,
        position: q.position,
        form_id: created.id,
      });
    }
    await refresh();
  }

  if (selectedForm) {
    const fresh = forms.find(f => f.id === selectedForm.id) || selectedForm;
    return <FormDetail form={fresh} onBack={() => { setSelectedForm(null); refresh(); }} />;
  }

  return (
    <section>
      <SectionHeader
        eyebrow="Forms"
        title="Forms & funnels"
        subtitle="Build a form (a set of qualifying questions), then assign it to a funnel (a landing page → post-registration). Open a form to edit it or view its analytics."
        action={<button onClick={() => setCreating(true)} style={btnPrimary}>+ New form</button>}
      />
      {error && <ErrorBanner message={error} onDismiss={() => setError(null)} />}

      <FunnelsManager funnels={funnels} forms={forms} onChange={refresh} />

      {creating && (
        <FormCreateForm onCancel={() => setCreating(false)} onSaved={async () => { setCreating(false); await refresh(); }} />
      )}

      {loading ? (
        <Empty label="Loading…" />
      ) : forms.length === 0 ? (
        <Empty label="No forms yet. Click + New form to create one." />
      ) : (
        <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
          {forms.map(f => (
            <FormCard key={f.id} form={f} counts={counts[f.id]}
              funnels={funnels.filter(fn => fn.form_id === f.id)} onOpen={() => setSelectedForm(f)}
              onDuplicate={() => duplicateForm(f).catch(e => alert('Duplicate failed: ' + e.message))} />
          ))}
        </div>
      )}
    </section>
  );
}

// Assign a form to each funnel (landing -> post-reg pairing)
function FunnelsManager({ funnels, forms, onChange }) {
  const [busy, setBusy] = useState(false);
  async function assign(funnelId, formId) {
    setBusy(true);
    try { await Funnels.update(funnelId, { form_id: formId || null }); await onChange(); }
    catch (e) { alert('Update failed: ' + e.message); }
    finally { setBusy(false); }
  }
  return (
    <div style={{ background: 'var(--card)', border: '1px solid var(--line)', borderRadius: 10, padding: '16px 20px', marginBottom: 24 }}>
      <div className="mono" style={{ fontSize: 10, letterSpacing: '0.2em', textTransform: 'uppercase', color: 'var(--muted)', fontWeight: 600, marginBottom: 12 }}>Funnels</div>
      {funnels.length === 0 ? (
        <div style={{ fontSize: 13, color: 'var(--muted)' }}>No funnels configured.</div>
      ) : (
        <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
          {funnels.map(fn => (
            <div key={fn.id} style={{ display: 'flex', alignItems: 'center', gap: 12, flexWrap: 'wrap' }}>
              <span style={{ fontSize: 14, fontWeight: 600, minWidth: 150 }}>{fn.name}</span>
              <span className="mono" style={{ fontSize: 11, color: 'var(--muted)' }}>{fn.landing_variant} landing → {fn.post_reg_variant} post-reg</span>
              <select value={fn.form_id || ''} disabled={busy} onChange={e => assign(fn.id, e.target.value)}
                style={{ ...inputStyle, width: 'auto', minWidth: 200, marginLeft: 'auto', padding: '8px 10px', fontSize: 13 }}>
                <option value="">— no form —</option>
                {forms.map(f => <option key={f.id} value={f.id}>{f.name}</option>)}
              </select>
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

function FormCard({ form, counts, funnels, onOpen, onDuplicate }) {
  const c = counts || { questions: 0, submissions: 0 };
  const [busy, setBusy] = useState(false);
  async function handleDuplicate(e) {
    e.stopPropagation();
    setBusy(true);
    try { await onDuplicate(); } finally { setBusy(false); }
  }
  return (
    <div onClick={onOpen} style={{
      background: 'var(--card)', border: '1px solid var(--line)', borderRadius: 10,
      padding: '18px 20px', cursor: 'pointer', display: 'grid', gridTemplateColumns: '1fr auto', gap: 16, alignItems: 'center'
    }}>
      <div>
        <div style={{ fontSize: 17, fontWeight: 600 }}>{form.name}</div>
        {form.description ? <div style={{ fontSize: 13, color: 'var(--muted)', marginTop: 2 }}>{form.description}</div> : null}
        <div className="mono" style={{ fontSize: 11, color: 'var(--muted-2)', marginTop: 8 }}>
          {c.questions} questions · {c.submissions} submissions · {funnels.length ? funnels.map(f => f.name).join(', ') : 'unassigned'}
        </div>
      </div>
      <div style={{ display: 'flex', gap: 10, alignItems: 'center' }} onClick={e => e.stopPropagation()}>
        <button onClick={handleDuplicate} disabled={busy} style={btnGhost}>{busy ? 'Duplicating…' : 'Duplicate'}</button>
        <span onClick={onOpen} className="mono" style={{ fontSize: 12, color: 'var(--accent)', cursor: 'pointer' }}>Open →</span>
      </div>
    </div>
  );
}

function FormCreateForm({ onCancel, onSaved }) {
  const [name, setName] = useState('');
  const [description, setDescription] = useState('');
  const [saving, setSaving] = useState(false);
  const [err, setErr] = useState(null);
  async function submit(e) {
    e.preventDefault();
    setErr(null);
    if (!name.trim()) { setErr('Name is required.'); return; }
    setSaving(true);
    try {
      const base = name.trim().toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 36);
      const slug = (base || 'form') + '-' + Math.random().toString(36).slice(2, 6);
      await Forms.create({ name: name.trim(), slug, description: description.trim() });
      onSaved();
    } catch (e) { setErr(e.message); setSaving(false); }
  }
  return (
    <form onSubmit={submit} style={{ background: 'var(--bg-2)', border: '1px solid var(--accent)', borderRadius: 12, padding: 24, marginBottom: 24 }}>
      <div className="mono" style={{ fontSize: 11, letterSpacing: '0.2em', textTransform: 'uppercase', color: 'var(--accent)', fontWeight: 700, marginBottom: 16 }}>New form</div>
      <Field label="Form name" required>
        <input value={name} onChange={e => setName(e.target.value)} style={inputStyle} placeholder="e.g. Agency funnel form" autoFocus />
      </Field>
      <div style={{ marginTop: 16 }}>
        <Field label="Description" hint="Optional. Internal note about this form.">
          <input value={description} onChange={e => setDescription(e.target.value)} style={inputStyle} placeholder="Optional" />
        </Field>
      </div>
      {err && <div style={{ color: 'var(--danger)', fontSize: 13, marginTop: 14 }}>{err}</div>}
      <div style={{ display: 'flex', gap: 10, marginTop: 22, justifyContent: 'flex-end' }}>
        <button type="button" onClick={onCancel} disabled={saving} style={btnGhost}>Cancel</button>
        <button type="submit" disabled={saving} style={btnPrimary}>{saving ? 'Creating…' : 'Create form'}</button>
      </div>
    </form>
  );
}

// A single form: Build (its questions) + Analytics (its own Responses/Summary/Insights)
function FormDetail({ form, onBack }) {
  const [tab, setTab] = useState('build');
  return (
    <section>
      <button onClick={onBack} style={{ ...btnGhost, marginBottom: 16 }}>← All forms</button>
      <SectionHeader eyebrow="Form" title={form.name}
        subtitle={form.description || 'Edit this form\'s questions and view its analytics.'} />
      <div style={{ display: 'flex', gap: 4, borderBottom: '1px solid var(--line)', marginBottom: 20 }}>
        {[['build', 'Build'], ['analytics', 'Analytics']].map(([id, label]) => (
          <button key={id} onClick={() => setTab(id)} style={{
            background: 'transparent', border: 'none', cursor: 'pointer',
            padding: '10px 16px', fontSize: 14, fontWeight: 600,
            color: tab === id ? 'var(--ink)' : 'var(--muted)',
            borderBottom: '2px solid ' + (tab === id ? 'var(--accent)' : 'transparent'), marginBottom: -1
          }}>{label}</button>
        ))}
      </div>
      {tab === 'build' ? <QuestionsBuilder formId={form.id} /> : <FormAnalyticsView form={form} />}
    </section>
  );
}

// The question editor scoped to one form
function QuestionsBuilder({ formId }) {
  const [questions, setQuestions] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  const [editing, setEditing] = useState(null);

  async function refresh() {
    setLoading(true); setError(null);
    try {
      const rows = await Questions.listForForm(formId);
      setQuestions(rows);
      window.FeedQuestions && window.FeedQuestions.refresh && window.FeedQuestions.refresh();
    } catch (e) { setError(e.message); }
    finally { setLoading(false); }
  }
  useEffect(() => { refresh(); }, [formId]);

  return (
    <div>
      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
        <div className="mono" style={{ fontSize: 11, color: 'var(--muted)' }}>
          These questions show in the qualifying flow after the email opt-in, on every funnel using this form.
        </div>
        <button onClick={() => setEditing({ position: questions.length, form_id: formId })} style={btnPrimary}>+ Add question</button>
      </div>
      {error && <ErrorBanner message={error} onDismiss={() => setError(null)} />}

      {editing !== null && (
        <QuestionForm
          initial={editing}
          formId={formId}
          onCancel={() => setEditing(null)}
          onSaved={async () => { setEditing(null); await refresh(); }}
        />
      )}

      {loading ? (
        <Empty label="Loading…" />
      ) : questions.length === 0 ? (
        <Empty label="No questions yet. Click + Add question to create one." />
      ) : (
        <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
          {questions.map((q, i) => (
            <QuestionCard key={q.id} question={q} order={i + 1} onEdit={() => setEditing(q)} onDeleted={refresh} />
          ))}
        </div>
      )}
    </div>
  );
}

function QuestionCard({ question, order, onEdit, onDeleted }) {
  const [busy, setBusy] = useState(false);
  async function handleDelete() {
    if (!confirm('Delete this question? It will be removed from the live form.\n\n' + question.prompt)) return;
    setBusy(true);
    try { await Questions.remove(question.id); onDeleted(); }
    catch (e) { alert('Delete failed: ' + e.message); setBusy(false); }
  }
  return (
    <div style={{
      background: 'var(--card)', border: '1px solid ' + (question.active ? 'var(--line)' : 'var(--line-2)'),
      borderRadius: 10, padding: '18px 20px',
      display: 'grid', gridTemplateColumns: '1fr auto', gap: 20,
      opacity: question.active ? 1 : 0.55
    }}>
      <div>
        <div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 8 }}>
          <span className="mono" style={{
            fontSize: 9, letterSpacing: '0.22em', fontWeight: 700,
            padding: '3px 8px', borderRadius: 4,
            background: 'rgba(168,85,247,0.15)', color: '#c084fc'
          }}>Q{order}</span>
          {!question.active && (
            <span className="mono" style={{
              fontSize: 9, letterSpacing: '0.2em', fontWeight: 700,
              padding: '3px 8px', borderRadius: 4,
              background: 'rgba(255,255,255,0.05)', color: '#8a8a8a'
            }}>INACTIVE</span>
          )}
          <span className="mono" style={{ fontSize: 11, color: 'var(--muted-2)' }}>
            {(question.options || []).length} options
          </span>
        </div>
        <div style={{ fontSize: 17, fontWeight: 600, marginBottom: question.subheadline ? 4 : 8 }}>{question.prompt}</div>
        {question.subheadline ? (
          <div style={{ fontSize: 14, fontWeight: 400, color: 'var(--muted)', marginBottom: 10 }}>{question.subheadline}</div>
        ) : null}
        <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
          {(question.options || []).map((opt, i) => (
            <span key={i} style={{
              fontSize: 12, padding: '4px 9px',
              background: '#0d0d0d', border: '1px solid var(--line-2)',
              borderRadius: 999, color: 'var(--ink-2)'
            }}>{opt.label}</span>
          ))}
        </div>
      </div>
      <div style={{ display: 'flex', gap: 8, alignItems: 'flex-start' }}>
        <button onClick={onEdit} disabled={busy} style={btnGhost}>Edit</button>
        <button onClick={handleDelete} disabled={busy} style={btnDanger}>Delete</button>
      </div>
    </div>
  );
}

function QuestionForm({ initial, onCancel, onSaved, formId }) {
  const isEdit = !!initial.id;
  const [prompt, setPrompt] = useState(initial.prompt || '');
  const [subheadline, setSubheadline] = useState(initial.subheadline || '');
  const [active, setActive] = useState(initial.active !== false);
  const [options, setOptions] = useState(
    (initial.options && initial.options.length)
      ? initial.options
      : [{ value: '', label: '', sublabel: '' }, { value: '', label: '', sublabel: '' }]
  );
  const [saving, setSaving] = useState(false);
  const [err, setErr] = useState(null);

  function updateOpt(i, key, val) {
    setOptions(opts => opts.map((o, idx) => idx === i ? Object.assign({}, o, { [key]: val }) : o));
  }
  function addOpt() {
    setOptions(opts => [...opts, { value: '', label: '', sublabel: '' }]);
  }
  function removeOpt(i) {
    setOptions(opts => opts.filter((_, idx) => idx !== i));
  }
  function moveOpt(i, dir) {
    setOptions(opts => {
      const next = [...opts];
      const target = i + dir;
      if (target < 0 || target >= next.length) return opts;
      [next[i], next[target]] = [next[target], next[i]];
      return next;
    });
  }

  // Auto-derive `value` from label if value left blank, so admin only needs to type the label
  function normalize(opts) {
    return opts
      .filter(o => (o.label || '').trim())
      .map(o => ({
        value: (o.value || '').trim() || (o.label || '').trim().toLowerCase().replace(/[^a-z0-9]+/g, '-').slice(0, 40),
        label: (o.label || '').trim(),
        sublabel: (o.sublabel || '').trim()
      }));
  }

  async function handleSubmit(e) {
    e.preventDefault();
    setErr(null);
    if (!prompt.trim()) { setErr('Prompt is required.'); return; }
    const cleanedOptions = normalize(options);
    if (cleanedOptions.length < 2) { setErr('Add at least two options.'); return; }
    setSaving(true);
    try {
      const payload = {
        prompt: prompt.trim(),
        subheadline: subheadline.trim(),
        options: cleanedOptions,
        active,
        position: initial.position || 0,
        form_id: initial.form_id || formId || null
      };
      if (isEdit) await Questions.update(initial.id, payload);
      else await Questions.create(payload);
      onSaved();
    } catch (e) {
      setErr(e.message);
      setSaving(false);
    }
  }

  return (
    <form onSubmit={handleSubmit} style={{
      background: 'var(--bg-2)', border: '1px solid var(--accent)',
      borderRadius: 12, padding: 24, marginBottom: 24,
      boxShadow: '0 30px 80px -30px rgba(168,85,247,0.3)'
    }}>
      <div className="mono" style={{
        fontSize: 11, letterSpacing: '0.2em', textTransform: 'uppercase',
        color: 'var(--accent)', fontWeight: 700, marginBottom: 16
      }}>
        {isEdit ? 'Edit question' : 'New question'}
      </div>

      <Field label="Prompt" required>
        <input value={prompt} onChange={e => setPrompt(e.target.value)} style={inputStyle}
          placeholder="What stage is your newsletter at?" autoFocus />
      </Field>

      <div style={{ marginTop: 16 }}>
        <Field label="Subheadline" hint="Optional. Shown as non-bold supporting text beneath the prompt on the live form.">
          <input value={subheadline} onChange={e => setSubheadline(e.target.value)} style={inputStyle}
            placeholder="e.g. This helps us tailor the training to where you are." />
        </Field>
      </div>

      <div className="mono" style={{
        fontSize: 10, letterSpacing: '0.18em', textTransform: 'uppercase',
        color: 'var(--muted)', fontWeight: 600, margin: '20px 0 10px',
        display: 'flex', justifyContent: 'space-between', alignItems: 'center'
      }}>
        <span>Answer options</span>
        <button type="button" onClick={addOpt} style={{
          ...btnGhost, padding: '5px 11px', fontSize: 11, letterSpacing: '0.04em'
        }}>+ Add option</button>
      </div>

      <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
        {options.map((opt, i) => (
          <div key={i} style={{
            display: 'grid',
            gridTemplateColumns: 'auto 1fr 1fr auto',
            gap: 8,
            alignItems: 'center',
            background: '#0d0d0d', border: '1px solid var(--line-2)',
            borderRadius: 8, padding: 10
          }}>
            <div style={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
              <button type="button" onClick={() => moveOpt(i, -1)} disabled={i === 0} style={miniBtn}>▲</button>
              <button type="button" onClick={() => moveOpt(i, 1)} disabled={i === options.length - 1} style={miniBtn}>▼</button>
            </div>
            <input value={opt.label} onChange={e => updateOpt(i, 'label', e.target.value)}
              placeholder="Label (shown to user)" style={smallInputStyle} />
            <input value={opt.sublabel} onChange={e => updateOpt(i, 'sublabel', e.target.value)}
              placeholder="Sublabel (optional)" style={smallInputStyle} />
            <button type="button" onClick={() => removeOpt(i)}
              disabled={options.length <= 2}
              style={{ ...miniBtn, color: '#fca5a5', width: 28, height: 28 }}>×</button>
          </div>
        ))}
      </div>

      <label style={{ display: 'inline-flex', alignItems: 'center', gap: 8, marginTop: 18, fontSize: 13, cursor: 'pointer' }}>
        <input type="checkbox" checked={active} onChange={e => setActive(e.target.checked)} />
        <span>Active (show on the live form)</span>
      </label>

      {err && <div style={{ color: 'var(--danger)', fontSize: 13, marginTop: 14 }}>{err}</div>}

      <div style={{ display: 'flex', gap: 10, marginTop: 22, justifyContent: 'flex-end' }}>
        <button type="button" onClick={onCancel} disabled={saving} style={btnGhost}>Cancel</button>
        <button type="submit" disabled={saving} style={btnPrimary}>
          {saving ? 'Saving…' : (isEdit ? 'Save changes' : 'Create question')}
        </button>
      </div>
    </form>
  );
}

// ─────────────────────────────────────────────
// FORM ANALYTICS SECTION — Typeform-style Responses / Summary / Insights
// ─────────────────────────────────────────────
// Per-form analytics (Responses / Summary / Insights), scoped to one form's data
function FormAnalyticsView({ form }) {
  const [tab, setTab] = useState('responses');
  const [rangeId, setRangeId] = useState('30');
  const [questions, setQuestions] = useState([]);
  const [registrants, setRegistrants] = useState([]);
  const [events, setEvents] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  const { start, end, prevStart, prevEnd } = useMemo(() => {
    const cfg = DATE_RANGES.find(r => r.id === rangeId) || DATE_RANGES[2];
    const s = dateRangeStart(cfg.days);
    const e = new Date();
    const pe = new Date(s.getTime() - 1);
    const ps = new Date(pe.getTime() - cfg.days * 86400000);
    return { start: s, end: e, prevStart: ps, prevEnd: pe };
  }, [rangeId]);

  async function refresh() {
    setLoading(true); setError(null);
    try {
      const sinceIso = start.toISOString();
      const [qs, regs, evs] = await Promise.all([
        Questions.listForForm(form.id),
        api('/registrants?form_id=eq.' + form.id + '&registered_at=gte.' + sinceIso + '&order=registered_at.desc&select=*'),
        api('/tracking_events?created_at=gte.' + sinceIso + '&event_type=like.form*&order=created_at.desc'),
      ]);
      setQuestions(qs);
      setRegistrants(regs);
      // events carry form_id in event_data — keep only this form's
      setEvents(evs.filter(e => e.event_data && e.event_data.form_id === form.id));
    } catch (e) { setError(e.message); }
    finally { setLoading(false); }
  }
  useEffect(() => { refresh(); }, [start.getTime(), form.id]);
  useEffect(() => {
    const unsub = window.FeedRegistrants && window.FeedRegistrants.onChange(() => refresh());
    return () => { unsub && unsub(); };
  }, [start.getTime(), form.id]);

  const activeQuestions = useMemo(
    () => questions.filter(q => q.active !== false).sort((a, b) => (a.position || 0) - (b.position || 0)),
    [questions]
  );

  const TABS = [
    { id: 'responses', label: 'Responses' },
    { id: 'summary', label: 'Summary' },
    { id: 'insights', label: 'Insights' },
  ];

  return (
    <div>
      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: 10, marginBottom: 14 }}>
        <div className="mono" style={{ fontSize: 11, color: 'var(--muted)' }}>
          Submissions & trend are historical; views / starts / drop-off / time-to-complete accrue from when form tracking went live.
        </div>
        <div style={{ display: 'flex', gap: 6 }}>
          {DATE_RANGES.map(r => (
            <button key={r.id} onClick={() => setRangeId(r.id)} style={{
              ...btnGhost, padding: '7px 11px', fontSize: 12,
              ...(rangeId === r.id ? { borderColor: 'var(--accent)', color: 'var(--accent)' } : {})
            }}>{r.label}</button>
          ))}
        </div>
      </div>
      {error && <ErrorBanner message={error} onDismiss={() => setError(null)} />}

      <div style={{ display: 'flex', gap: 4, borderBottom: '1px solid var(--line)', marginBottom: 20 }}>
        {TABS.map(t => (
          <button key={t.id} onClick={() => setTab(t.id)} style={{
            background: 'transparent', border: 'none', cursor: 'pointer',
            padding: '10px 16px', fontSize: 14, fontWeight: 600,
            color: tab === t.id ? 'var(--ink)' : 'var(--muted)',
            borderBottom: '2px solid ' + (tab === t.id ? 'var(--accent)' : 'transparent'),
            marginBottom: -1
          }}>{t.label}</button>
        ))}
      </div>

      {loading ? <Empty label="Loading…" /> : (
        tab === 'responses' ? <FormResponsesTab questions={activeQuestions} registrants={registrants} />
        : tab === 'summary' ? <FormSummaryTab questions={activeQuestions} registrants={registrants} />
        : <FormInsightsTab questions={activeQuestions} registrants={registrants} events={events}
            start={start} end={end} prevStart={prevStart} prevEnd={prevEnd} />
      )}
    </div>
  );
}

// Resolve a registrant's answer to one question (snapshot first, legacy fallback for Q1).
function answerForQuestion(r, q, questions) {
  const list = Array.isArray(r.answers) ? r.answers : [];
  const hit = list.find(a => a && a.question_id === q.id);
  if (hit) return { value: hit.value, label: hit.label != null ? hit.label : hit.value };
  if (questions[0] && q.id === questions[0].id && r.qualifying_answer != null) {
    return { value: r.qualifying_answer, label: r.qualifying_answer };
  }
  return { value: null, label: null };
}

// ── Responses: queryable table, one row per submission, a column per question
function FormResponsesTab({ questions, registrants }) {
  const [search, setSearch] = useState('');
  const [statusFilter, setStatusFilter] = useState('complete');
  const [qFilter, setQFilter] = useState({});

  const filtered = useMemo(() => {
    let rows = registrants;
    if (statusFilter !== 'all') rows = rows.filter(r => r.status === statusFilter);
    Object.entries(qFilter).forEach(([qid, val]) => {
      if (!val) return;
      const q = questions.find(x => x.id === qid);
      if (q) rows = rows.filter(r => answerForQuestion(r, q, questions).value === val);
    });
    if (search.trim()) {
      const s = search.toLowerCase();
      rows = rows.filter(r => JSON.stringify(r).toLowerCase().includes(s));
    }
    return rows;
  }, [registrants, statusFilter, qFilter, search, questions]);

  function exportCsv() {
    if (!filtered.length) return;
    const esc = v => {
      if (v == null) return '';
      const s = String(v).replace(/"/g, '""');
      return /[",\n]/.test(s) ? '"' + s + '"' : s;
    };
    const header = ['When', 'Status', 'Email', 'Phone', ...questions.map(q => q.prompt), 'Source'].map(esc).join(',');
    const lines = filtered.map(r => {
      const cells = [r.registered_at, r.status, r.email, r.phone];
      questions.forEach(q => cells.push(answerForQuestion(r, q, questions).label));
      cells.push(r.utm_source || 'direct');
      return cells.map(esc).join(',');
    });
    const csv = header + '\n' + lines.join('\n');
    const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = 'form_responses_' + new Date().toISOString().slice(0, 10) + '.csv';
    a.click();
    URL.revokeObjectURL(url);
  }

  return (
    <div>
      <div style={{ display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap', marginBottom: 14 }}>
        <input value={search} onChange={e => setSearch(e.target.value)} placeholder="Search responses…" style={{ ...inputStyle, width: 240 }} />
        {['complete', 'partial', 'all'].map(s => (
          <button key={s} onClick={() => setStatusFilter(s)} style={{
            ...btnGhost, padding: '7px 12px', fontSize: 12, textTransform: 'capitalize',
            ...(statusFilter === s ? { borderColor: 'var(--accent)', color: 'var(--accent)' } : {})
          }}>{s}</button>
        ))}
        {questions.map(q => (
          <select key={q.id} value={qFilter[q.id] || ''} onChange={e => setQFilter(f => Object.assign({}, f, { [q.id]: e.target.value }))}
            style={{ ...inputStyle, width: 'auto', maxWidth: 220, padding: '8px 10px', fontSize: 12 }}>
            <option value="">{(q.prompt || '').slice(0, 26)}: any</option>
            {(q.options || []).map(o => <option key={o.value} value={o.value}>{o.label}</option>)}
          </select>
        ))}
        <div style={{ marginLeft: 'auto', display: 'flex', gap: 8, alignItems: 'center' }}>
          <span className="mono" style={{ fontSize: 11, color: 'var(--muted)' }}>{filtered.length} of {registrants.length}</span>
          <button onClick={exportCsv} style={btnGhost}>Export CSV</button>
        </div>
      </div>

      <div style={{ background: 'var(--card)', border: '1px solid var(--line)', borderRadius: 10, overflow: 'hidden' }}>
        <div style={{ overflowX: 'auto', maxHeight: 600, overflowY: 'auto' }}>
          <table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 12 }}>
            <thead style={{ position: 'sticky', top: 0, zIndex: 1 }}>
              <tr style={{ background: '#0d0d0d', borderBottom: '1px solid var(--line)' }}>
                <Th>When</Th><Th>Status</Th><Th>Email</Th><Th>Phone</Th>
                {questions.map(q => <Th key={q.id}>{q.prompt}</Th>)}
                <Th>Source</Th>
              </tr>
            </thead>
            <tbody>
              {filtered.length === 0 ? (
                <tr><td colSpan={5 + questions.length} style={{ padding: 32, textAlign: 'center', color: 'var(--muted)' }}>No responses match.</td></tr>
              ) : filtered.map(r => (
                <tr key={r.id} style={{ borderBottom: '1px solid #1a1a1a' }}>
                  <Td><span className="mono" style={{ fontSize: 11, color: 'var(--muted)' }}>{formatRegisteredAt(r.registered_at)}</span></Td>
                  <Td><StatusBadge status={r.status} dropoffStep={r.dropoff_step} /></Td>
                  <Td><span style={{ color: 'var(--ink)' }}>{r.email}</span></Td>
                  <Td><span className="mono" style={{ fontSize: 11 }}>{r.phone || '—'}</span></Td>
                  {questions.map(q => <Td key={q.id}>{answerForQuestion(r, q, questions).label || '—'}</Td>)}
                  <Td><span style={{ color: 'var(--muted)' }}>{r.utm_source || 'direct'}</span></Td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>
      </div>
    </div>
  );
}

// ── Summary: per-question answer breakdown over completed submissions
function FormSummaryTab({ questions, registrants }) {
  const [mode, setMode] = useState('count');
  const subs = useMemo(() => registrants.filter(r => r.status === 'complete'), [registrants]);

  function tallies(q) {
    const counts = {};
    let answered = 0;
    subs.forEach(r => {
      const val = answerForQuestion(r, q, questions).value;
      if (val != null && val !== '') { counts[val] = (counts[val] || 0) + 1; answered++; }
    });
    const rows = (q.options || []).map(o => ({ label: o.label, value: o.value, count: counts[o.value] || 0 }));
    Object.keys(counts).forEach(v => {
      if (!(q.options || []).some(o => o.value === v)) rows.push({ label: v, value: v, count: counts[v] });
    });
    return { rows, answered };
  }

  if (!subs.length) return <Empty label="No completed submissions in this window yet." />;

  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 20 }}>
      <div style={{ display: 'flex', justifyContent: 'flex-end', gap: 6 }}>
        {[['count', '#'], ['percent', '%']].map(([m, lbl]) => (
          <button key={m} onClick={() => setMode(m)} style={{
            ...btnGhost, padding: '6px 12px', fontSize: 13,
            ...(mode === m ? { borderColor: 'var(--accent)', color: 'var(--accent)' } : {})
          }}>{lbl}</button>
        ))}
      </div>
      {questions.map(q => {
        const { rows, answered } = tallies(q);
        const max = Math.max(1, ...rows.map(r => r.count));
        return (
          <div key={q.id} style={{ background: 'var(--card)', border: '1px solid var(--line)', borderRadius: 10, padding: '18px 20px' }}>
            <div style={{ fontSize: 16, fontWeight: 600, marginBottom: 4 }}>{q.prompt}</div>
            <div className="mono" style={{ fontSize: 11, color: 'var(--muted)', marginBottom: 16 }}>{answered} of {subs.length} answered</div>
            <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
              {rows.map(row => {
                const pct = answered > 0 ? (row.count / answered) * 100 : 0;
                return (
                  <div key={row.value}>
                    <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 13, marginBottom: 4 }}>
                      <span>{row.label}</span>
                      <span className="mono" style={{ color: 'var(--muted)' }}>{mode === 'count' ? row.count : pct.toFixed(0) + '%'}</span>
                    </div>
                    <div style={{ height: 8, background: '#0d0d0d', borderRadius: 4, overflow: 'hidden' }}>
                      <div style={{ width: (row.count / max * 100) + '%', height: '100%', background: 'linear-gradient(90deg, #7c3aed, #a855f7)' }} />
                    </div>
                  </div>
                );
              })}
            </div>
          </div>
        );
      })}
    </div>
  );
}

// ── Insights: funnel + trend + per-question drop-off
function FormInsightsTab({ questions, registrants, events, start, end, prevStart, prevEnd }) {
  const sessionsOf = type => new Set(events.filter(e => e.event_type === type).map(e => e.session_id));
  const views = useMemo(() => sessionsOf('form_view').size, [events]);
  const starts = useMemo(() => sessionsOf('form_start').size, [events]);
  const submittedSessions = useMemo(() => sessionsOf('form_submit').size, [events]);
  const subs = useMemo(() => registrants.filter(r => r.status === 'complete'), [registrants]);
  const submissions = subs.length;
  const completionRate = starts > 0 ? (submittedSessions / starts) * 100 : null;

  const avgMs = useMemo(() => {
    const arr = events.filter(e => e.event_type === 'form_submit')
      .map(e => e.event_data && e.event_data.ms_to_complete)
      .filter(n => typeof n === 'number' && n > 0);
    if (!arr.length) return null;
    return arr.reduce((a, b) => a + b, 0) / arr.length;
  }, [events]);
  function fmtDur(ms) {
    if (ms == null) return '—';
    const s = Math.round(ms / 1000);
    const m = Math.floor(s / 60);
    return m > 0 ? m + 'm ' + (s % 60) + 's' : s + 's';
  }

  const perQ = useMemo(() => {
    const base = questions.map(q => ({
      q,
      views: new Set(events.filter(e => e.event_type === 'form_question_view' && e.event_data && e.event_data.question_id === q.id).map(e => e.session_id)).size,
    }));
    return base.map((row, i) => {
      const next = base[i + 1];
      const floor = next ? next.views : submittedSessions;
      const drop = row.views > 0 ? Math.max(0, row.views - floor) : 0;
      const dropPct = row.views > 0 ? (drop / row.views) * 100 : 0;
      return Object.assign({}, row, { drop, dropPct });
    });
  }, [questions, events, submittedSessions]);

  const card = { background: 'var(--card)', border: '1px solid var(--line)', borderRadius: 10, padding: '16px 20px' };

  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 24 }}>
      <div style={{ display: 'flex', gap: 32, flexWrap: 'wrap', ...card }}>
        <Stat label="Views" value={views} />
        <Stat label="Starts" value={starts} />
        <Stat label="Submissions" value={submittedSessions} />
        <div style={{ minWidth: 140 }}>
          <div className="mono" style={{ fontSize: 9, letterSpacing: '0.18em', textTransform: 'uppercase', color: 'var(--muted)', fontWeight: 600, marginBottom: 4 }}>Completion rate</div>
          <div style={{ fontSize: 20, fontWeight: 600, fontVariantNumeric: 'tabular-nums' }}>{completionRate == null ? '—' : completionRate.toFixed(1) + '%'}</div>
        </div>
        <div style={{ minWidth: 140 }}>
          <div className="mono" style={{ fontSize: 9, letterSpacing: '0.18em', textTransform: 'uppercase', color: 'var(--muted)', fontWeight: 600, marginBottom: 4 }}>Time to complete</div>
          <div style={{ fontSize: 20, fontWeight: 600, fontVariantNumeric: 'tabular-nums' }}>{fmtDur(avgMs)}</div>
        </div>
      </div>

      <div>
        <div className="mono" style={{ fontSize: 10, letterSpacing: '0.2em', textTransform: 'uppercase', color: 'var(--muted)', fontWeight: 600, marginBottom: 10 }}>Submissions over time</div>
        <TimeSeriesChart current={{ registrants: subs }} prior={{ registrants: [] }} start={start} end={end} prevStart={prevStart} prevEnd={prevEnd} />
      </div>

      <div>
        <div className="mono" style={{ fontSize: 10, letterSpacing: '0.2em', textTransform: 'uppercase', color: 'var(--muted)', fontWeight: 600, marginBottom: 10 }}>Question by question</div>
        {views === 0 ? (
          <Empty label="No form tracking events in this window yet. Views, starts and drop-off populate as new visitors go through the form." />
        ) : (
          <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
            {perQ.map((row, i) => (
              <div key={row.q.id} style={{ ...card, display: 'flex', alignItems: 'center', gap: 16 }}>
                <span className="mono" style={{ fontSize: 11, color: 'var(--accent)', fontWeight: 700, minWidth: 28 }}>Q{i + 1}</span>
                <span style={{ flex: 1, fontSize: 14 }}>{row.q.prompt}</span>
                <span className="mono" style={{ fontSize: 12, color: 'var(--muted)', minWidth: 80, textAlign: 'right' }}>{row.views} views</span>
                <span className="mono" style={{ fontSize: 12, minWidth: 110, textAlign: 'right', color: row.drop > 0 ? '#fca5a5' : 'var(--muted-2)' }}>
                  {row.drop > 0 ? '-' + row.drop + ' (' + row.dropPct.toFixed(0) + '%)' : '—'}
                </span>
              </div>
            ))}
          </div>
        )}
      </div>
    </div>
  );
}

// ─────────────────────────────────────────────
// REGISTRANTS SECTION
// ─────────────────────────────────────────────
function RegistrantsSection() {
  const [rows, setRows] = useState([]);
  const [eventsByRegistrant, setEventsByRegistrant] = useState({});
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  const [filter, setFilter] = useState('');
  const [statusFilter, setStatusFilter] = useState('all'); // 'all' | 'complete' | 'partial'
  const [dateRange, setDateRange] = useState('all'); // 'all' | '7' | '14' | '30' | '90' | 'custom'
  const [customStart, setCustomStart] = useState('');
  const [customEnd, setCustomEnd] = useState('');
  const [sortBy, setSortBy] = useState('registered_at'); // default: newest first
  const [sortDir, setSortDir] = useState('desc');

  function handleSort(key) {
    if (sortBy === key) {
      setSortDir(d => d === 'asc' ? 'desc' : 'asc');
    } else {
      setSortBy(key);
      // Sensible defaults: time-like fields default desc; everything else asc
      setSortDir(key === 'registered_at' ? 'desc' : 'asc');
    }
  }

  async function refresh() {
    setLoading(true); setError(null);
    try {
      const [regs, events] = await Promise.all([
        Registrants.list(),
        api('/tracking_events?registrant_id=not.is.null&order=created_at.asc&limit=20000'),
      ]);
      setRows(regs);
      // Group events by registrant_id (already sorted oldest → newest for timeline display)
      const grouped = {};
      events.forEach(e => {
        if (!grouped[e.registrant_id]) grouped[e.registrant_id] = [];
        grouped[e.registrant_id].push(e);
      });
      setEventsByRegistrant(grouped);
    } catch (e) { setError(e.message); }
    finally { setLoading(false); }
  }
  useEffect(() => {
    refresh();
    // Subscribe to realtime changes — new submissions appear instantly
    const unsub = window.FeedRegistrants && window.FeedRegistrants.onChange(() => refresh());
    return () => { unsub && unsub(); };
  }, []);

  const counts = useMemo(() => ({
    all: rows.length,
    complete: rows.filter(r => r.status === 'complete').length,
    partial: rows.filter(r => r.status === 'partial').length,
  }), [rows]);

  // Precompute engagement per registrant once so we can sort by it
  const engagementByRegistrant = useMemo(() => {
    const map = {};
    rows.forEach(r => {
      const events = eventsByRegistrant[r.id] || [];
      const pageViews = events.filter(e => e.event_type === 'page_view').length;
      const hasPlayed = events.some(e => e.event_type === 'video_play');
      let maxVideoPct = 0;
      events.forEach(e => {
        if (e.event_type === 'video_progress') {
          const p = e.event_data && e.event_data.percent;
          if (typeof p === 'number') maxVideoPct = Math.max(maxVideoPct, p);
        }
      });
      const calendarClicks = events.filter(e => e.event_type === 'calendar_click').length;
      // Composite score for sorting: SMS reply >> calendar click >> video % >> page views
      const score =
        (r.sms_responded ? 1000 : 0) +
        calendarClicks * 200 +
        maxVideoPct * 3 +
        pageViews * 5 +
        (hasPlayed ? 10 : 0);
      map[r.id] = { pageViews, hasPlayed, maxVideoPct, calendarClicks, totalEvents: events.length, score };
    });
    return map;
  }, [rows, eventsByRegistrant]);

  const filtered = useMemo(() => {
    let out = rows;
    if (statusFilter !== 'all') out = out.filter(r => r.status === statusFilter);
    // Date-added filter
    if (dateRange !== 'all') {
      let startMs = null, endMs = null;
      if (dateRange === 'custom') {
        if (customStart) startMs = new Date(customStart + 'T00:00:00').getTime();
        if (customEnd)   endMs   = new Date(customEnd   + 'T23:59:59').getTime();
      } else {
        const days = parseInt(dateRange, 10);
        startMs = Date.now() - days * 24 * 60 * 60 * 1000;
      }
      out = out.filter(r => {
        if (!r.registered_at) return false;
        const t = new Date(r.registered_at).getTime();
        if (startMs !== null && t < startMs) return false;
        if (endMs   !== null && t > endMs)   return false;
        return true;
      });
    }
    if (filter.trim()) {
      const q = filter.toLowerCase();
      out = out.filter(r =>
        (r.email || '').toLowerCase().includes(q) ||
        (r.phone || '').toLowerCase().includes(q) ||
        (r.qualifying_answer || '').toLowerCase().includes(q) ||
        (r.utm_source || '').toLowerCase().includes(q) ||
        (r.utm_campaign || '').toLowerCase().includes(q) ||
        (r.utm_medium || '').toLowerCase().includes(q)
      );
    }
    // Sort
    const dir = sortDir === 'asc' ? 1 : -1;
    const safeStr = v => (v == null ? '' : String(v)).toLowerCase();
    const sorted = [...out].sort((a, b) => {
      let av, bv;
      switch (sortBy) {
        case 'source':
          av = safeStr(a.utm_source || 'direct');
          bv = safeStr(b.utm_source || 'direct');
          return av.localeCompare(bv) * dir;
        case 'entry':
          av = safeStr(a.entry_page_type || 'landing') + '|' + safeStr(a.entry_page_variant || 'default');
          bv = safeStr(b.entry_page_type || 'landing') + '|' + safeStr(b.entry_page_variant || 'default');
          return av.localeCompare(bv) * dir;
        case 'answer':
          av = safeStr(a.qualifying_answer);
          bv = safeStr(b.qualifying_answer);
          return av.localeCompare(bv) * dir;
        case 'status':
          // complete < partial alphabetically; secondary key = dropoff_step
          av = safeStr(a.status) + '|' + safeStr(a.dropoff_step);
          bv = safeStr(b.status) + '|' + safeStr(b.dropoff_step);
          return av.localeCompare(bv) * dir;
        case 'engagement':
          av = (engagementByRegistrant[a.id] && engagementByRegistrant[a.id].score) || 0;
          bv = (engagementByRegistrant[b.id] && engagementByRegistrant[b.id].score) || 0;
          return (av - bv) * dir;
        case 'webinar':
          av = safeStr(a.webinar_events && a.webinar_events.title);
          bv = safeStr(b.webinar_events && b.webinar_events.title);
          return av.localeCompare(bv) * dir;
        case 'registered_at':
        default:
          av = a.registered_at ? new Date(a.registered_at).getTime() : 0;
          bv = b.registered_at ? new Date(b.registered_at).getTime() : 0;
          return (av - bv) * dir;
      }
    });
    return sorted;
  }, [rows, filter, statusFilter, dateRange, customStart, customEnd, sortBy, sortDir, engagementByRegistrant]);

  return (
    <section>
      <SectionHeader
        eyebrow="Form submissions"
        title="Registrants"
        subtitle="Everyone who completed the qualifying flow. Updates live as new ones come in."
        action={
          <button onClick={refresh} style={btnGhost}>Refresh</button>
        }
      />
      {error && <ErrorBanner message={error} onDismiss={() => setError(null)} />}

      {/* Status filter pills */}
      <div style={{ display: 'flex', gap: 6, margin: '0 0 10px', flexWrap: 'wrap' }}>
        <StatusFilterPill label="All"      count={counts.all}      active={statusFilter === 'all'}      onClick={() => setStatusFilter('all')} />
        <StatusFilterPill label="Complete" count={counts.complete} active={statusFilter === 'complete'} onClick={() => setStatusFilter('complete')} color="#00ff85" />
        <StatusFilterPill label="Partial"  count={counts.partial}  active={statusFilter === 'partial'}  onClick={() => setStatusFilter('partial')}  color="#fbbf24" />
      </div>

      {/* Date-added filter */}
      <div style={{ display: 'flex', gap: 6, margin: '0 0 14px', flexWrap: 'wrap', alignItems: 'center' }}>
        <span className="mono" style={{ fontSize: 10, letterSpacing: '0.2em', textTransform: 'uppercase', color: 'var(--muted)', marginRight: 4 }}>Date added</span>
        {[
          { id: 'all',    label: 'All time' },
          { id: '7',      label: '7d' },
          { id: '14',     label: '14d' },
          { id: '30',     label: '30d' },
          { id: '90',     label: '90d' },
          { id: 'custom', label: 'Custom' },
        ].map(opt => (
          <button
            key={opt.id}
            onClick={() => setDateRange(opt.id)}
            className="mono"
            style={{
              fontSize: 10, letterSpacing: '0.16em', textTransform: 'uppercase',
              padding: '6px 10px', borderRadius: 6, cursor: 'pointer',
              background: dateRange === opt.id ? 'var(--accent)' : 'var(--bg-3)',
              color: dateRange === opt.id ? '#fff' : 'var(--ink-2)',
              border: '1px solid ' + (dateRange === opt.id ? 'var(--accent)' : 'var(--line-2)'),
              fontWeight: 600,
            }}
          >{opt.label}</button>
        ))}
        {dateRange === 'custom' && (
          <span style={{ display: 'inline-flex', gap: 6, alignItems: 'center', marginLeft: 8 }}>
            <input type="date" value={customStart} onChange={e => setCustomStart(e.target.value)}
              style={{ ...inputStyle, padding: '6px 10px', fontSize: 12, width: 150 }} />
            <span className="mono" style={{ fontSize: 10, color: 'var(--muted)' }}>to</span>
            <input type="date" value={customEnd} onChange={e => setCustomEnd(e.target.value)}
              style={{ ...inputStyle, padding: '6px 10px', fontSize: 12, width: 150 }} />
          </span>
        )}
      </div>

      <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12, margin: '0 0 16px', flexWrap: 'wrap' }}>
        <div style={{ position: 'relative', flex: 1, maxWidth: 360 }}>
          <input
            value={filter}
            onChange={e => setFilter(e.target.value)}
            placeholder="Search email, phone, or answer…"
            style={{ ...inputStyle, paddingLeft: 36 }}
          />
          <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#525252" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"
            style={{ position: 'absolute', left: 12, top: '50%', transform: 'translateY(-50%)', pointerEvents: 'none' }}>
            <circle cx="11" cy="11" r="8" /><path d="M21 21l-4.35-4.35" />
          </svg>
        </div>
        <div className="mono" style={{ fontSize: 11, letterSpacing: '0.18em', textTransform: 'uppercase', color: 'var(--muted)' }}>
          {filtered.length} of {statusFilter === 'all' ? rows.length : counts[statusFilter]}
        </div>
      </div>

      {loading ? (
        <Empty label="Loading…" />
      ) : filtered.length === 0 ? (
        <Empty label={filter ? 'No matches.' : 'No registrants yet. Submissions will appear here in real-time.'} />
      ) : (
        <RegistrantTable
          rows={filtered}
          eventsByRegistrant={eventsByRegistrant}
          engagementByRegistrant={engagementByRegistrant}
          sortBy={sortBy}
          sortDir={sortDir}
          onSort={handleSort}
        />
      )}
    </section>
  );
}

function RegistrantTable({ rows, eventsByRegistrant, engagementByRegistrant, sortBy, sortDir, onSort }) {
  return (
    <div style={{
      background: 'var(--card)', border: '1px solid var(--line)',
      borderRadius: 10, overflow: 'hidden'
    }}>
      <div style={{ overflowX: 'auto' }}>
        <table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 13 }}>
          <thead>
            <tr style={{ background: '#0d0d0d', borderBottom: '1px solid var(--line)' }}>
              <Th>{/* expand carat */}</Th>
              <SortableTh sortKey="status"     currentSort={sortBy} currentDir={sortDir} onSort={onSort}>Status</SortableTh>
              <Th>Email</Th>
              <Th>Phone</Th>
              <SortableTh sortKey="answer"     currentSort={sortBy} currentDir={sortDir} onSort={onSort}>Answer</SortableTh>
              <SortableTh sortKey="source"     currentSort={sortBy} currentDir={sortDir} onSort={onSort}>Source</SortableTh>
              <SortableTh sortKey="entry"      currentSort={sortBy} currentDir={sortDir} onSort={onSort}>Entry Page</SortableTh>
              <SortableTh sortKey="engagement" currentSort={sortBy} currentDir={sortDir} onSort={onSort}>Engagement</SortableTh>
              <Th>IP</Th>
              <SortableTh sortKey="webinar"    currentSort={sortBy} currentDir={sortDir} onSort={onSort}>Webinar</SortableTh>
              <SortableTh sortKey="registered_at" currentSort={sortBy} currentDir={sortDir} onSort={onSort}>Date Added</SortableTh>
              <Th>SMS</Th>
            </tr>
          </thead>
          <tbody>
            {rows.map(r => (
              <RegistrantRow
                key={r.id}
                row={r}
                events={eventsByRegistrant[r.id] || []}
                precomputedEngagement={engagementByRegistrant && engagementByRegistrant[r.id]}
              />
            ))}
          </tbody>
        </table>
      </div>
    </div>
  );
}

function RegistrantRow({ row: r, events, precomputedEngagement }) {
  const [expanded, setExpanded] = useState(false);

  // Use precomputed engagement from the section (so sorting + display stay in sync); fall back if not provided
  const engagement = useMemo(() => {
    if (precomputedEngagement) return precomputedEngagement;
    const pageViews = events.filter(e => e.event_type === 'page_view').length;
    const hasPlayed = events.some(e => e.event_type === 'video_play');
    let maxVideoPct = 0;
    events.forEach(e => {
      if (e.event_type === 'video_progress') {
        const p = e.event_data && e.event_data.percent;
        if (typeof p === 'number') maxVideoPct = Math.max(maxVideoPct, p);
      }
    });
    const calendarClicks = events.filter(e => e.event_type === 'calendar_click').length;
    return { pageViews, hasPlayed, maxVideoPct, calendarClicks, totalEvents: events.length };
  }, [events, precomputedEngagement]);

  const hasAnyActivity = engagement.totalEvents > 0;

  return (
    <React.Fragment>
      <tr
        onClick={() => hasAnyActivity && setExpanded(e => !e)}
        style={{
          borderBottom: '1px solid #1a1a1a',
          opacity: r.status === 'partial' ? 0.85 : 1,
          cursor: hasAnyActivity ? 'pointer' : 'default',
          background: expanded ? 'rgba(168,85,247,0.04)' : 'transparent',
        }}
      >
        <Td>
          {hasAnyActivity ? (
            <span style={{
              display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
              width: 18, height: 18,
              color: 'var(--muted)', fontSize: 10,
              transform: expanded ? 'rotate(90deg)' : 'none',
              transition: 'transform 140ms',
            }}>▶</span>
          ) : <span style={{ display: 'inline-block', width: 18 }} />}
        </Td>
        <Td><StatusBadge status={r.status} dropoffStep={r.dropoff_step} /></Td>
        <Td><span style={{ color: 'var(--ink)' }}>{r.email}</span></Td>
        <Td><span className="mono" style={{ fontSize: 12 }}>{r.phone || '—'}</span></Td>
        <Td>{r.qualifying_answer || '—'}</Td>
        <Td><SourceCell row={r} /></Td>
        <Td><EntryPageCell row={r} /></Td>
        <Td><EngagementSummary engagement={engagement} smsResponded={r.sms_responded} /></Td>
        <Td>
          <span className="mono" style={{ fontSize: 11, color: r.ip_address ? 'var(--ink-2)' : 'var(--muted-2)' }}>
            {r.ip_address || '—'}
          </span>
        </Td>
        <Td>
          <span style={{ color: 'var(--muted)' }}>
            {r.webinar_events ? r.webinar_events.title.replace('Newsletter Accelerator Webinar - ', '') : '—'}
          </span>
        </Td>
        <Td>
          <div>
            <div className="mono" style={{ fontSize: 11, color: 'var(--ink-2)' }}>
              {formatRegisteredAt(r.registered_at)}
            </div>
            {r.last_seen_at && r.last_seen_at !== r.registered_at && (
              <div className="mono" style={{ fontSize: 10, color: 'var(--muted-2)', marginTop: 2 }}>
                last seen: {formatRegisteredAt(r.last_seen_at)}
              </div>
            )}
          </div>
        </Td>
        <Td>
          {r.sms_responded
            ? <span className="mono" style={{ fontSize: 9, letterSpacing: '0.18em', padding: '3px 7px', borderRadius: 4, background: 'rgba(0,255,133,0.12)', color: '#00ff85', fontWeight: 700 }}>REPLIED</span>
            : <span style={{ color: 'var(--muted-2)' }}>—</span>
          }
        </Td>
      </tr>
      {expanded && (
        <tr style={{ background: 'rgba(168,85,247,0.04)' }}>
          <td colSpan={12} style={{ padding: '6px 18px 18px' }}>
            <RegistrantAnswers answers={r.answers} fallback={r.qualifying_answer} />
            <EventTimeline events={events} />
          </td>
        </tr>
      )}
    </React.Fragment>
  );
}

// Full qualifying answers for one registrant (prompt -> chosen label).
// Falls back to the legacy single qualifying_answer for older rows.
function RegistrantAnswers({ answers, fallback }) {
  let list = Array.isArray(answers)
    ? answers.filter(a => a && a.value != null && a.value !== '')
    : [];
  if (list.length === 0) {
    if (!fallback) return null;
    list = [{ prompt: 'Qualifying answer', label: fallback }];
  }
  return (
    <div style={{ marginBottom: 16 }}>
      <div className="mono" style={{ fontSize: 10, letterSpacing: '0.2em', textTransform: 'uppercase', color: 'var(--muted)', fontWeight: 600, marginBottom: 8 }}>
        Form answers
      </div>
      <div style={{ display: 'grid', gap: 6 }}>
        {list.map((a, i) => (
          <div key={i} style={{ display: 'flex', gap: 12, fontSize: 13, alignItems: 'baseline' }}>
            <span style={{ color: 'var(--muted)', flex: '1 1 48%' }}>{a.prompt || a.question_id}</span>
            <span style={{ color: 'var(--ink)', fontWeight: 600, flex: '1 1 52%' }}>{a.label != null ? a.label : a.value}</span>
          </div>
        ))}
      </div>
    </div>
  );
}

// Compact engagement badges shown inline on each row
function EngagementSummary({ engagement, smsResponded }) {
  const items = [];
  if (engagement.pageViews > 0) items.push({ key: 'views', label: engagement.pageViews + 'v', tip: engagement.pageViews + ' page view(s)', color: 'var(--muted)' });
  if (engagement.hasPlayed) {
    if (engagement.maxVideoPct >= 90) items.push({ key: 'video', label: engagement.maxVideoPct + '%', tip: 'Watched ' + engagement.maxVideoPct + '% of post-reg video', color: '#00ff85' });
    else if (engagement.maxVideoPct >= 50) items.push({ key: 'video', label: engagement.maxVideoPct + '%', tip: 'Watched ' + engagement.maxVideoPct + '% of post-reg video', color: '#fbbf24' });
    else items.push({ key: 'video', label: engagement.maxVideoPct > 0 ? engagement.maxVideoPct + '%' : '▶', tip: engagement.maxVideoPct > 0 ? 'Watched ' + engagement.maxVideoPct + '%' : 'Started but no progress recorded', color: '#fca5a5' });
  }
  if (engagement.calendarClicks > 0) items.push({ key: 'cal', label: '📅', tip: 'Clicked Add to Calendar', color: 'var(--accent)' });
  if (items.length === 0) {
    return <span style={{ color: 'var(--muted-2)', fontSize: 11 }}>—</span>;
  }
  return (
    <div style={{ display: 'flex', flexWrap: 'wrap', gap: 4 }}>
      {items.map(it => (
        <span
          key={it.key}
          title={it.tip}
          className="mono"
          style={{
            fontSize: 10, letterSpacing: '0.06em',
            padding: '3px 7px', borderRadius: 4,
            background: 'rgba(255,255,255,0.04)',
            border: '1px solid var(--line-2)',
            color: it.color, fontWeight: 600,
          }}
        >{it.label}</span>
      ))}
    </div>
  );
}

// Full chronological timeline of every event for one registrant
function EventTimeline({ events }) {
  if (events.length === 0) {
    return <div style={{ color: 'var(--muted-2)', fontSize: 12, fontStyle: 'italic' }}>No events yet for this registrant.</div>;
  }
  return (
    <div style={{
      borderLeft: '2px solid var(--line-2)',
      marginLeft: 12, paddingLeft: 18,
      display: 'flex', flexDirection: 'column', gap: 8,
    }}>
      <div className="mono" style={{
        fontSize: 9, letterSpacing: '0.22em', textTransform: 'uppercase',
        color: 'var(--accent)', fontWeight: 600, marginBottom: 4,
        marginLeft: -8,
      }}>
        Activity timeline ({events.length} {events.length === 1 ? 'event' : 'events'})
      </div>
      {events.map((e, i) => <EventRow key={e.id} event={e} index={i + 1} />)}
    </div>
  );
}

function EventRow({ event: e, index }) {
  const cfg = EVENT_DISPLAY[e.event_type] || { label: e.event_type, color: 'var(--muted)' };
  const detail = describeEvent(e);
  return (
    <div style={{ display: 'grid', gridTemplateColumns: '22px 130px 1fr 140px', gap: 12, alignItems: 'center', fontSize: 12, paddingLeft: 4 }}>
      <span style={{
        width: 10, height: 10, borderRadius: '50%',
        background: cfg.color, marginLeft: -23,
        boxShadow: '0 0 0 3px var(--card)',
      }} />
      <span className="mono" style={{ fontSize: 10, letterSpacing: '0.08em', color: cfg.color, fontWeight: 600, textTransform: 'uppercase' }}>
        {cfg.label}
      </span>
      <span style={{ color: 'var(--ink-2)' }}>{detail}</span>
      <span className="mono" style={{ fontSize: 10, color: 'var(--muted-2)', textAlign: 'right' }}>
        {formatRegisteredAt(e.created_at)}
        <span style={{ color: 'var(--muted-2)' }}> · {e.page || '—'}</span>
      </span>
    </div>
  );
}

const EVENT_DISPLAY = {
  page_view:         { label: 'Page view',       color: '#8a8a8a' },
  partial_submission:{ label: 'Partial',         color: '#fbbf24' },
  video_play:        { label: 'Video play',      color: '#00ff85' },
  video_progress:    { label: 'Video progress',  color: '#00ff85' },
  video_complete:    { label: 'Video complete',  color: '#00ff85' },
  calendar_click:    { label: 'Calendar click',  color: '#a855f7' },
  registration_complete: { label: 'Registered', color: '#a855f7' },
  sms_response:      { label: 'SMS reply',       color: '#00ff85' },
};

function describeEvent(e) {
  const d = e.event_data || {};
  // Build a video attribution string from whatever we have. Title preferred,
  // falls back to "video <short-id>" if title is missing.
  function videoRef() {
    if (d.video_title) return '"' + d.video_title + '"';
    if (d.video_id) return 'video ' + String(d.video_id).slice(0, 8);
    return 'post-reg video';
  }
  switch (e.event_type) {
    case 'page_view':
      return (d.device_type ? d.device_type + ' · ' : '') + (d.utm_source ? 'source: ' + d.utm_source : (d.referrer ? 'referrer: ' + d.referrer.slice(0, 40) : 'direct'));
    case 'video_play':
      return 'Started playing ' + videoRef();
    case 'video_progress':
      return 'Reached ' + d.percent + '% of ' + videoRef();
    case 'video_complete':
      return 'Watched to the end of ' + videoRef();
    case 'calendar_click':
      return 'Clicked ' + (d.source ? d.source.replace('_', ' ') + ' calendar button' : 'Add to Calendar');
    default:
      return e.event_type;
  }
}

function StatusFilterPill({ label, count, active, onClick, color }) {
  return (
    <button
      onClick={onClick}
      style={{
        display: 'inline-flex', alignItems: 'center', gap: 8,
        padding: '7px 13px',
        background: active ? 'rgba(168,85,247,0.12)' : 'transparent',
        border: '1px solid ' + (active ? 'var(--accent)' : 'var(--line-2)'),
        borderRadius: 6,
        color: active ? 'var(--accent)' : 'var(--ink-2)',
        fontSize: 13, fontWeight: 600, letterSpacing: '-0.005em',
        cursor: 'pointer', fontFamily: 'inherit',
        transition: 'all 140ms'
      }}
    >
      {color && (
        <span style={{
          width: 7, height: 7, borderRadius: '50%',
          background: color, flexShrink: 0
        }} />
      )}
      <span>{label}</span>
      <span className="mono" style={{
        fontSize: 11, fontWeight: 700,
        padding: '1px 6px', borderRadius: 4,
        background: active ? 'rgba(168,85,247,0.2)' : 'var(--bg-3)',
        color: active ? 'var(--accent)' : 'var(--muted)'
      }}>{count}</span>
    </button>
  );
}

function StatusBadge({ status, dropoffStep }) {
  if (status === 'complete') {
    return (
      <span className="mono" style={{
        fontSize: 9, letterSpacing: '0.18em', padding: '3px 7px', borderRadius: 4,
        background: 'rgba(0,255,133,0.12)', color: '#00ff85', fontWeight: 700
      }}>COMPLETE</span>
    );
  }
  // Partial — show which step they bailed on
  const label = (dropoffStep || 'email_entered').replace(/_/g, ' ');
  return (
    <span title={'Partial · ' + label} className="mono" style={{
      fontSize: 9, letterSpacing: '0.18em', padding: '3px 7px', borderRadius: 4,
      background: 'rgba(251,191,36,0.12)', color: '#fbbf24', fontWeight: 700
    }}>PARTIAL</span>
  );
}

// Renders the registrant's traffic source — UTM if captured, else click ID, else referrer, else "Direct"
function SourceCell({ row }) {
  if (row.utm_source) {
    return (
      <div>
        <div style={{ color: 'var(--ink)', textTransform: 'capitalize' }}>{row.utm_source}</div>
        {(row.utm_medium || row.utm_campaign) && (
          <div className="mono" style={{ fontSize: 10, color: 'var(--muted)', marginTop: 2, letterSpacing: '0.04em' }}>
            {[row.utm_medium, row.utm_campaign].filter(Boolean).join(' · ')}
          </div>
        )}
      </div>
    );
  }
  if (row.fbclid) {
    return (
      <span className="mono" style={{
        fontSize: 10, letterSpacing: '0.16em', textTransform: 'uppercase', fontWeight: 700,
        padding: '3px 7px', borderRadius: 4, background: 'rgba(24,119,242,0.15)', color: '#60a5fa'
      }}>Meta · click</span>
    );
  }
  if (row.gclid) {
    return (
      <span className="mono" style={{
        fontSize: 10, letterSpacing: '0.16em', textTransform: 'uppercase', fontWeight: 700,
        padding: '3px 7px', borderRadius: 4, background: 'rgba(234,67,53,0.15)', color: '#f87171'
      }}>Google · click</span>
    );
  }
  if (row.referrer) {
    try {
      const host = new URL(row.referrer).hostname.replace(/^www\./, '');
      return <span style={{ color: 'var(--ink-2)' }}>{host}</span>;
    } catch (e) { /* fallthrough */ }
  }
  return <span className="mono" style={{ fontSize: 10, letterSpacing: '0.16em', color: 'var(--muted-2)' }}>DIRECT</span>;
}

// Renders the entry page where this registrant first landed, e.g. "Landing · Agency"
function EntryPageCell({ row }) {
  const pageType = row.entry_page_type || 'landing';
  const variant  = row.entry_page_variant || 'default';
  const typeLabel = (PAGE_TYPE_LABELS && PAGE_TYPE_LABELS[pageType]) || (pageType.charAt(0).toUpperCase() + pageType.slice(1));
  const variantLabel = variant.charAt(0).toUpperCase() + variant.slice(1);
  // Visual emphasis: non-default variants get the accent color so they stand out
  const isDefault = variant === 'default';
  return (
    <div style={{ display: 'flex', alignItems: 'center', whiteSpace: 'nowrap' }}>
      <span style={{ color: 'var(--ink-2)', fontSize: 12 }}>{typeLabel}</span>
      <span style={{ color: 'var(--muted-2)', margin: '0 6px', fontSize: 11 }}>·</span>
      <span className="mono" style={{
        fontSize: 10, letterSpacing: '0.14em', textTransform: 'uppercase', fontWeight: 700,
        padding: '3px 7px', borderRadius: 4,
        background: isDefault ? 'rgba(255,255,255,0.04)' : 'rgba(168,85,247,0.15)',
        color:      isDefault ? 'var(--muted)'          : 'var(--accent)',
      }}>{variantLabel}</span>
    </div>
  );
}

function Th({ children }) {
  return (
    <th className="mono" style={{
      padding: '12px 14px', textAlign: 'left',
      fontSize: 10, letterSpacing: '0.2em', textTransform: 'uppercase',
      color: 'var(--muted)', fontWeight: 600,
      whiteSpace: 'nowrap'
    }}>{children}</th>
  );
}

function SortableTh({ children, sortKey, currentSort, currentDir, onSort }) {
  const active = currentSort === sortKey;
  return (
    <th
      className="mono"
      onClick={() => onSort(sortKey)}
      style={{
        padding: '12px 14px', textAlign: 'left',
        fontSize: 10, letterSpacing: '0.2em', textTransform: 'uppercase',
        color: active ? 'var(--accent)' : 'var(--muted)', fontWeight: 600,
        whiteSpace: 'nowrap', cursor: 'pointer', userSelect: 'none',
      }}
    >
      <span style={{ display: 'inline-flex', alignItems: 'center', gap: 6 }}>
        {children}
        <span style={{ opacity: active ? 1 : 0.3, fontSize: 9 }}>
          {active ? (currentDir === 'asc' ? '▲' : '▼') : '↕'}
        </span>
      </span>
    </th>
  );
}

function Td({ children }) {
  return (
    <td style={{ padding: '14px', verticalAlign: 'middle', whiteSpace: 'nowrap' }}>{children}</td>
  );
}

function formatRegisteredAt(iso) {
  if (!iso) return '';
  const d = new Date(iso);
  const diff = Date.now() - d.getTime();
  const mins = Math.floor(diff / 60000);
  if (mins < 1) return 'just now';
  if (mins < 60) return mins + 'm ago';
  const hrs = Math.floor(mins / 60);
  if (hrs < 24) return hrs + 'h ago';
  return new Intl.DateTimeFormat('en-US', { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit', timeZone: CFG.displayTimeZone }).format(d);
}

const smallInputStyle = {
  width: '100%',
  padding: '8px 10px',
  background: '#161616',
  border: '1px solid var(--line-2)',
  borderRadius: 5,
  color: 'var(--ink)',
  fontSize: 13,
  outline: 'none',
  fontFamily: 'inherit'
};

const miniBtn = {
  width: 22, height: 20, padding: 0,
  background: 'transparent',
  border: '1px solid var(--line-2)',
  borderRadius: 4,
  color: 'var(--muted)',
  cursor: 'pointer',
  fontSize: 9,
  fontFamily: 'inherit',
  display: 'flex', alignItems: 'center', justifyContent: 'center'
};

// ═════════════════════════════════════════════════════════════
// VIDEOS SECTION — typed by placement (post_reg, vsl, etc.)
// Each placement is its own admin surface so they never get confused.
// ═════════════════════════════════════════════════════════════
function VideosSection({ placement, title, subtitle }) {
  const [videos, setVideos] = useState([]);
  const [stats, setStats] = useState({}); // { [videoId]: { plays, reached: {5..100}, completes, calendarClicks } }
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  const [editing, setEditing] = useState(null);

  async function refresh() {
    setLoading(true); setError(null);
    try {
      const [rows, events] = await Promise.all([
        Videos.list(placement),
        // Pull all video-related events tagged with this placement
        api('/tracking_events?event_type=in.(video_play,video_progress,video_complete,calendar_click)&order=created_at.desc&limit=20000'),
      ]);
      setVideos(rows);

      // Aggregate stats per video_id
      const agg = {};
      const sessionsByVideo = {}; // videoId -> Set of session_ids that interacted
      events.forEach(e => {
        const vid = e.event_data && e.event_data.video_id;
        if (e.event_type === 'video_play' && vid) {
          if (!agg[vid]) agg[vid] = { plays: 0, reached: {}, completes: 0, calendarClicks: 0 };
          agg[vid].plays++;
          if (!sessionsByVideo[vid]) sessionsByVideo[vid] = new Set();
          sessionsByVideo[vid].add(e.session_id);
        }
        if (e.event_type === 'video_progress' && vid) {
          if (!agg[vid]) agg[vid] = { plays: 0, reached: {}, completes: 0, calendarClicks: 0 };
          const p = e.event_data && e.event_data.percent;
          if (typeof p === 'number') {
            // Track UNIQUE sessions that reached this milestone
            if (!agg[vid].reached[p]) agg[vid].reached[p] = new Set();
            agg[vid].reached[p].add(e.session_id);
          }
        }
        if (e.event_type === 'video_complete' && vid) {
          if (!agg[vid]) agg[vid] = { plays: 0, reached: {}, completes: 0, calendarClicks: 0 };
          agg[vid].completes++;
        }
      });
      // Calendar clicks attributed to a video are clicks from sessions that played that video
      events.filter(e => e.event_type === 'calendar_click').forEach(e => {
        Object.entries(sessionsByVideo).forEach(([vid, sset]) => {
          if (sset.has(e.session_id)) {
            if (!agg[vid]) agg[vid] = { plays: 0, reached: {}, completes: 0, calendarClicks: 0 };
            agg[vid].calendarClicks++;
          }
        });
      });
      // Convert reached Sets to counts
      Object.values(agg).forEach(s => {
        const counts = {};
        Object.entries(s.reached).forEach(([pct, set]) => { counts[pct] = set.size; });
        s.reached = counts;
      });
      setStats(agg);

      window.FeedVideo && window.FeedVideo.refresh && window.FeedVideo.refresh(placement);
    } catch (e) { setError(e.message); }
    finally { setLoading(false); }
  }
  useEffect(() => { refresh(); }, [placement]);

  const active = useMemo(() => videos.find(v => v.status === 'active'), [videos]);
  const archived = useMemo(() => videos.filter(v => v.status === 'archived'), [videos]);

  async function setAsActive(id) {
    // Archive whatever's currently active for this placement, then mark this one active
    setLoading(true);
    try {
      // Archive current active (if any) other than the one we're activating
      const current = videos.find(v => v.status === 'active');
      if (current && current.id !== id) {
        await Videos.update(current.id, { status: 'archived' });
      }
      await Videos.update(id, { status: 'active' });
      await refresh();
    } catch (e) {
      alert('Failed to switch active video: ' + e.message);
      setLoading(false);
    }
  }

  return (
    <section>
      <SectionHeader
        eyebrow="Videos"
        title={title}
        subtitle={subtitle}
        action={
          <button onClick={() => setEditing({})} style={btnPrimary}>
            + Add Video
          </button>
        }
      />
      {error && <ErrorBanner message={error} onDismiss={() => setError(null)} />}

      {editing !== null && (
        <VideoForm
          initial={editing}
          placement={placement}
          onCancel={() => setEditing(null)}
          onSaved={async () => { setEditing(null); await refresh(); }}
        />
      )}

      <Subhead>Active</Subhead>
      {loading ? (
        <Empty label="Loading…" />
      ) : !active ? (
        <Empty label="No active video. Add one or activate an archived video below." />
      ) : (
        <VideoCard
          video={active}
          stats={stats[active.id]}
          isActive
          onEdit={() => setEditing(active)}
          onArchive={async () => {
            if (!confirm('Archive this video? Post-reg page will have no active video until you set another.')) return;
            await Videos.update(active.id, { status: 'archived' });
            await refresh();
          }}
        />
      )}

      <Subhead>Archived ({archived.length})</Subhead>
      {archived.length === 0 ? (
        <Empty label="No archived videos yet." />
      ) : (
        <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
          {archived.map(v => (
            <VideoCard
              key={v.id}
              video={v}
              stats={stats[v.id]}
              isActive={false}
              onEdit={() => setEditing(v)}
              onMakeActive={() => setAsActive(v.id)}
              onDelete={async () => {
                if (!confirm('Permanently delete this video?')) return;
                await Videos.remove(v.id);
                await refresh();
              }}
            />
          ))}
        </div>
      )}
    </section>
  );
}

function VideoCard({ video, stats, isActive, onEdit, onArchive, onMakeActive, onDelete }) {
  const [showStats, setShowStats] = useState(false);
  const s = stats || { plays: 0, reached: {}, completes: 0, calendarClicks: 0 };
  const plays = s.plays || 0;
  const reach25 = s.reached[25] || 0;
  const reach50 = s.reached[50] || 0;
  const reach75 = s.reached[75] || 0;
  const reach90 = s.reached[90] || 0;
  const reach100 = s.reached[100] || 0;
  const completeRate = plays > 0 ? (reach90 / plays) * 100 : 0;

  return (
    <div style={{
      background: 'var(--card)',
      border: '1px solid ' + (isActive ? 'var(--accent)' : 'var(--line)'),
      borderRadius: 10, padding: '18px 20px',
      opacity: isActive ? 1 : 0.85
    }}>
      <div style={{ display: 'grid', gridTemplateColumns: '1fr auto', gap: 20 }}>
        <div>
          <div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 8 }}>
            {isActive && (
              <span className="mono" style={{
                fontSize: 9, letterSpacing: '0.22em', fontWeight: 700,
                padding: '3px 8px', borderRadius: 999,
                background: 'var(--accent)', color: '#fff'
              }}>ACTIVE</span>
            )}
            <span className="mono" style={{ fontSize: 11, color: 'var(--muted-2)' }}>
              Created {new Date(video.created_at).toLocaleDateString()}
            </span>
            {video.duration_seconds && (
              <span className="mono" style={{ fontSize: 11, color: 'var(--muted-2)' }}>
                · {Math.floor(video.duration_seconds / 60)}:{String(video.duration_seconds % 60).padStart(2, '0')}
              </span>
            )}
          </div>
          <div style={{ fontSize: 17, fontWeight: 600, marginBottom: 6 }}>{video.title}</div>
          <div className="mono" style={{
            fontSize: 11, color: 'var(--ink-2)',
            padding: '6px 9px',
            background: '#0d0d0d', border: '1px solid var(--line-2)',
            borderRadius: 5,
            wordBreak: 'break-all',
            marginBottom: video.notes ? 8 : 0
          }}>{video.src_url}</div>
          {video.notes && (
            <div style={{ fontSize: 13, color: 'var(--muted)' }}>{video.notes}</div>
          )}
        </div>
        <div style={{ display: 'flex', gap: 8, alignItems: 'flex-start' }}>
          <button onClick={onEdit} style={btnGhost}>Edit</button>
          {isActive && <button onClick={onArchive} style={btnGhost}>Archive</button>}
          {!isActive && onMakeActive && (
            <button onClick={onMakeActive} style={btnPrimary}>Make active</button>
          )}
          {!isActive && onDelete && (
            <button onClick={onDelete} style={btnDanger}>Delete</button>
          )}
        </div>
      </div>

      {/* Inline summary stats — visible at-a-glance */}
      <div style={{
        marginTop: 16, padding: '12px 14px',
        background: '#0d0d0d',
        border: '1px solid var(--line-2)',
        borderRadius: 8,
        display: 'grid',
        gridTemplateColumns: 'repeat(auto-fit, minmax(110px, 1fr))',
        gap: 8
      }}>
        <StatChip label="Plays" value={plays} />
        <StatChip label="Reached 25%" value={reach25} pct={plays && (reach25 / plays) * 100} />
        <StatChip label="Reached 50%" value={reach50} pct={plays && (reach50 / plays) * 100} />
        <StatChip label="Reached 75%" value={reach75} pct={plays && (reach75 / plays) * 100} />
        <StatChip label="Reached 90%" value={reach90} pct={plays && (reach90 / plays) * 100} accent />
        <StatChip label="Calendar clicks" value={s.calendarClicks || 0} />
      </div>

      {/* Expandable detail */}
      <div style={{ marginTop: 10, textAlign: 'right' }}>
        <button onClick={() => setShowStats(s => !s)} style={{
          ...btnGhost, fontSize: 11, padding: '5px 11px',
        }}>{showStats ? '↑ Hide detailed metrics' : '↓ Show all 5% milestones'}</button>
      </div>

      {showStats && (
        <div style={{
          marginTop: 10, padding: 14,
          background: '#0a0a0a',
          border: '1px solid var(--line-2)',
          borderRadius: 8,
        }}>
          <div className="mono" style={{
            fontSize: 9, letterSpacing: '0.22em', textTransform: 'uppercase',
            color: 'var(--accent)', fontWeight: 600, marginBottom: 10
          }}>
            Every 5% milestone — sessions reaching ≥ X%
          </div>
          <div style={{
            display: 'grid',
            gridTemplateColumns: 'repeat(auto-fit, minmax(90px, 1fr))',
            gap: 6
          }}>
            {Array.from({ length: 20 }, (_, i) => (i + 1) * 5).map(m => {
              const v = s.reached[m] || 0;
              const rate = plays > 0 ? (v / plays) * 100 : 0;
              return (
                <div key={m} style={{
                  background: '#0d0d0d', border: '1px solid var(--line-2)',
                  borderRadius: 5, padding: '7px 9px',
                }}>
                  <div className="mono" style={{ fontSize: 10, color: 'var(--accent)', fontWeight: 600 }}>≥{m}%</div>
                  <div style={{ fontSize: 16, fontWeight: 600, fontVariantNumeric: 'tabular-nums', marginTop: 1 }}>{v}</div>
                  <div className="mono" style={{ fontSize: 10, color: 'var(--muted-2)', marginTop: 1 }}>
                    {rate.toFixed(0)}% of plays
                  </div>
                </div>
              );
            })}
          </div>
        </div>
      )}
    </div>
  );
}

function StatChip({ label, value, pct, accent }) {
  return (
    <div>
      <div className="mono" style={{
        fontSize: 9, letterSpacing: '0.18em', textTransform: 'uppercase',
        color: 'var(--muted)', fontWeight: 600
      }}>{label}</div>
      <div style={{
        fontSize: 18, fontWeight: 600, marginTop: 2,
        color: accent ? 'var(--accent)' : 'var(--ink)',
        fontVariantNumeric: 'tabular-nums',
      }}>
        {value.toLocaleString()}
        {pct !== undefined && pct !== null && typeof pct === 'number' && pct > 0 && (
          <span className="mono" style={{ fontSize: 11, color: 'var(--muted)', marginLeft: 6, fontWeight: 500 }}>
            {pct.toFixed(0)}%
          </span>
        )}
      </div>
    </div>
  );
}

function VideoForm({ initial, placement, onCancel, onSaved }) {
  const isEdit = !!initial.id;
  const [title, setTitle] = useState(initial.title || '');
  const [srcUrl, setSrcUrl] = useState(initial.src_url || '');
  const [duration, setDuration] = useState(initial.duration_seconds || '');
  const [notes, setNotes] = useState(initial.notes || '');
  const [makeActive, setMakeActive] = useState(!isEdit); // new videos default to active
  const [saving, setSaving] = useState(false);
  const [err, setErr] = useState(null);

  async function handleSubmit(e) {
    e.preventDefault();
    setErr(null);
    if (!title.trim() || !srcUrl.trim()) {
      setErr('Title and source URL are required.');
      return;
    }
    setSaving(true);
    try {
      const payload = {
        placement,
        title: title.trim(),
        src_url: srcUrl.trim(),
        duration_seconds: duration ? parseInt(duration, 10) : null,
        notes: notes.trim() || null,
        status: makeActive ? 'active' : (initial.status || 'archived'),
      };
      if (isEdit) {
        await Videos.update(initial.id, payload);
      } else {
        // If creating a new active video, archive any other active ones for this placement
        if (makeActive) {
          const existingActive = await api('/videos?placement=eq.' + encodeURIComponent(placement) + '&status=eq.active');
          for (const v of existingActive) {
            await Videos.update(v.id, { status: 'archived' });
          }
        }
        await Videos.create(payload);
      }
      onSaved();
    } catch (e) {
      setErr(e.message);
      setSaving(false);
    }
  }

  return (
    <form
      onSubmit={handleSubmit}
      style={{
        background: 'var(--bg-2)', border: '1px solid var(--accent)',
        borderRadius: 12, padding: 24, marginBottom: 24,
        boxShadow: '0 30px 80px -30px rgba(168,85,247,0.3)'
      }}
    >
      <div className="mono" style={{
        fontSize: 11, letterSpacing: '0.2em', textTransform: 'uppercase',
        color: 'var(--accent)', fontWeight: 700, marginBottom: 16
      }}>
        {isEdit ? 'Edit video' : 'New video'}
      </div>

      <div style={{ display: 'grid', gap: 14 }}>
        <Field label="Title" required hint="Internal label so you can tell variants apart">
          <input value={title} onChange={e => setTitle(e.target.value)} style={inputStyle}
            placeholder="e.g. June 2026 — Hook V1" autoFocus />
        </Field>

        <Field label="Source URL" required hint="Cloudflare Stream MP4 URL, or any direct video URL">
          <input value={srcUrl} onChange={e => setSrcUrl(e.target.value)} style={inputStyle}
            placeholder="https://customer-xxx.cloudflarestream.com/.../downloads/default.mp4" />
        </Field>

        <Field label="Duration (seconds)" hint="Optional — used in admin display only">
          <input type="number" value={duration} onChange={e => setDuration(e.target.value)} style={inputStyle}
            placeholder="103" />
        </Field>

        <Field label="Notes" hint="Internal: what changed in this variant, who recorded it, etc.">
          <textarea value={notes} onChange={e => setNotes(e.target.value)} style={{ ...inputStyle, resize: 'vertical', minHeight: 60 }} />
        </Field>

        <label style={{ display: 'inline-flex', alignItems: 'center', gap: 8, fontSize: 13, cursor: 'pointer' }}>
          <input type="checkbox" checked={makeActive} onChange={e => setMakeActive(e.target.checked)} />
          <span>Make this the active video (will archive any current active video for this placement)</span>
        </label>
      </div>

      {err && <div style={{ color: 'var(--danger)', fontSize: 13, marginTop: 14 }}>{err}</div>}

      <div style={{ display: 'flex', gap: 10, marginTop: 22, justifyContent: 'flex-end' }}>
        <button type="button" onClick={onCancel} disabled={saving} style={btnGhost}>Cancel</button>
        <button type="submit" disabled={saving} style={btnPrimary}>
          {saving ? 'Saving…' : (isEdit ? 'Save changes' : 'Create video')}
        </button>
      </div>
    </form>
  );
}

// ═════════════════════════════════════════════════════════════
// META AD DATA SECTION — pure Meta Marketing API data, no funnel blend
// ═════════════════════════════════════════════════════════════

function MetaAdDataSection() {
  const [rangeId, setRangeId] = useState('7');
  const [customStart, setCustomStart] = useState('');
  const [customEnd, setCustomEnd] = useState('');
  const [spend, setSpend] = useState([]);
  const [status, setStatus] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  const [syncing, setSyncing] = useState(false);

  const { start, end, label } = useMemo(() => {
    if (rangeId === 'custom' && customStart && customEnd) {
      const s = new Date(customStart); const e = new Date(customEnd);
      e.setHours(23, 59, 59, 999);
      return { start: s, end: e, label: customStart + ' → ' + customEnd };
    }
    const cfg = DATE_RANGES.find(r => r.id === rangeId) || DATE_RANGES[0];
    return { start: dateRangeStart(cfg.days), end: new Date(), label: 'Last ' + cfg.days + ' days' };
  }, [rangeId, customStart, customEnd]);

  async function refresh() {
    setLoading(true); setError(null);
    try {
      const sinceIso = start.toISOString().slice(0, 10);
      const [rows, st] = await Promise.all([MetaAds.list(sinceIso), MetaAds.status()]);
      setSpend(rows);
      setStatus(st);
    } catch (e) { setError(e.message); }
    finally { setLoading(false); }
  }
  useEffect(() => { refresh(); }, [start.getTime(), end.getTime()]);

  async function handleSync() {
    setSyncing(true);
    try {
      // POST to the meta-sync Edge Function (will be created once Meta API access is provided)
      const url = CFG.supabaseUrl + '/functions/v1/meta-sync';
      const res = await fetch(url, {
        method: 'POST',
        headers: {
          apikey: CFG.supabaseAnonKey,
          Authorization: 'Bearer ' + CFG.supabaseAnonKey,
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({ days_back: 30 })
      });
      if (!res.ok) {
        const text = await res.text();
        throw new Error(text || ('HTTP ' + res.status));
      }
      await refresh();
    } catch (e) {
      alert('Sync failed: ' + e.message + '\n\n(This is expected until the meta-sync Edge Function is deployed.)');
    } finally {
      setSyncing(false);
    }
  }

  // Aggregate rows into account-wide and per-campaign totals
  const totals = useMemo(() => {
    const acc = { spend: 0, impressions: 0, clicks: 0, link_clicks: 0, reach: 0 };
    spend.forEach(r => {
      acc.spend       += parseFloat(r.spend) || 0;
      acc.impressions += parseInt(r.impressions, 10) || 0;
      acc.clicks      += parseInt(r.clicks, 10) || 0;
      acc.link_clicks += parseInt(r.link_clicks, 10) || 0;
      acc.reach       += parseInt(r.reach, 10) || 0;
    });
    acc.cpm     = acc.impressions > 0 ? (acc.spend / acc.impressions) * 1000 : 0;
    acc.cpc     = acc.link_clicks > 0 ? (acc.spend / acc.link_clicks) : 0;
    acc.ctr     = acc.impressions > 0 ? (acc.clicks / acc.impressions) * 100 : 0;
    acc.ctr_link = acc.impressions > 0 ? (acc.link_clicks / acc.impressions) * 100 : 0;
    return acc;
  }, [spend]);

  // Build campaign → adset → ad tree
  const tree = useMemo(() => buildMetaTree(spend), [spend]);

  return (
    <section>
      <SectionHeader
        eyebrow="Meta ad data"
        title="Meta Marketing API"
        subtitle="Pure ad-side metrics from Meta. Sync runs every 30 minutes plus manual refresh. Pair this with Blended Data to overlay funnel performance."
        action={
          <button onClick={handleSync} disabled={syncing} style={btnPrimary}>
            {syncing ? 'Syncing…' : 'Sync now'}
          </button>
        }
      />
      {error && <ErrorBanner message={error} onDismiss={() => setError(null)} />}

      <MetaSyncBanner status={status} />

      <DateRangeBar
        rangeId={rangeId} onSelect={setRangeId}
        customStart={customStart} setCustomStart={setCustomStart}
        customEnd={customEnd} setCustomEnd={setCustomEnd}
        label={label}
      />

      {loading ? (
        <Empty label="Loading…" />
      ) : (
        <>
          <MetaKpiPanel totals={totals} />
          {spend.length === 0 ? (
            <div style={{
              background: 'var(--card)', border: '1px dashed var(--line-2)',
              borderRadius: 10, padding: 32, textAlign: 'center', marginBottom: 24
            }}>
              <div style={{ fontSize: 14, color: 'var(--muted)', marginBottom: 8 }}>
                No Meta ad data yet.
              </div>
              <div style={{ fontSize: 12, color: 'var(--muted-2)' }}>
                Connect Meta API + run a sync to populate this view.
              </div>
            </div>
          ) : (
            <MetaCampaignTable tree={tree} />
          )}
        </>
      )}
    </section>
  );
}

function MetaSyncBanner({ status }) {
  const connected = status && status.connected;
  const lastSync = status && status.last_sync_at;
  const lastError = status && status.last_error;
  const adAccount = status && status.ad_account_id;

  return (
    <div style={{
      background: connected ? 'rgba(0,255,133,0.04)' : 'rgba(168,85,247,0.06)',
      border: '1px solid ' + (connected ? 'rgba(0,255,133,0.2)' : 'rgba(168,85,247,0.25)'),
      borderRadius: 8, padding: '12px 16px',
      marginBottom: 18, display: 'flex', alignItems: 'center', gap: 14, flexWrap: 'wrap'
    }}>
      <span className="mono" style={{
        fontSize: 10, letterSpacing: '0.22em', textTransform: 'uppercase',
        padding: '3px 8px', borderRadius: 4,
        background: connected ? 'rgba(0,255,133,0.15)' : 'rgba(251,191,36,0.15)',
        color: connected ? '#00ff85' : '#fbbf24', fontWeight: 700
      }}>
        {connected ? 'Connected' : 'Not connected'}
      </span>
      {connected ? (
        <>
          {adAccount && (
            <span style={{ fontSize: 13, color: 'var(--ink-2)' }}>
              <span className="mono" style={{ fontSize: 10, color: 'var(--muted)', letterSpacing: '0.16em', marginRight: 6 }}>ACCOUNT</span>
              {adAccount}
            </span>
          )}
          {lastSync && (
            <span style={{ fontSize: 13, color: 'var(--ink-2)' }}>
              <span className="mono" style={{ fontSize: 10, color: 'var(--muted)', letterSpacing: '0.16em', marginRight: 6 }}>LAST SYNC</span>
              {new Date(lastSync).toLocaleString()}
            </span>
          )}
          {lastError && (
            <span style={{ fontSize: 12, color: '#fca5a5' }}>
              ⚠ Last error: {lastError}
            </span>
          )}
        </>
      ) : (
        <span style={{ fontSize: 13, color: 'var(--ink-2)' }}>
          Meta sync isn't set up yet. To connect: provide a Meta Business Manager System User access token + Ad Account ID. The <code style={{ background: 'var(--bg-3)', padding: '1px 5px', borderRadius: 3, fontSize: 12 }}>meta-sync</code> Edge Function will then pull insights every 30 min plus on-demand.
        </span>
      )}
    </div>
  );
}

function MetaKpiPanel({ totals }) {
  const cards = [
    { label: 'Spend',          value: '$' + totals.spend.toFixed(2) },
    { label: 'Impressions',    value: totals.impressions.toLocaleString() },
    { label: 'CPM',            value: '$' + totals.cpm.toFixed(2) },
    { label: 'Link clicks',    value: totals.link_clicks.toLocaleString() },
    { label: 'CPC',            value: '$' + totals.cpc.toFixed(2) },
    { label: 'CTR (all)',      value: totals.ctr.toFixed(2) + '%' },
    { label: 'CTR (link)',     value: totals.ctr_link.toFixed(2) + '%' },
    { label: 'Reach',          value: totals.reach.toLocaleString() },
  ];
  return (
    <div style={{
      display: 'grid',
      gridTemplateColumns: 'repeat(auto-fit, minmax(150px, 1fr))',
      gap: 10, marginBottom: 24
    }}>
      {cards.map(c => (
        <div key={c.label} style={{
          background: 'var(--card)', border: '1px solid var(--line)',
          borderRadius: 10, padding: '14px 16px'
        }}>
          <div className="mono" style={{
            fontSize: 9, letterSpacing: '0.22em', textTransform: 'uppercase',
            color: 'var(--muted)', fontWeight: 600, marginBottom: 8
          }}>{c.label}</div>
          <div style={{ fontSize: 22, fontWeight: 700, letterSpacing: '-0.02em', fontVariantNumeric: 'tabular-nums' }}>
            {c.value}
          </div>
        </div>
      ))}
    </div>
  );
}

function buildMetaTree(rows) {
  // Aggregate by campaign → adset → ad over the window
  const tree = {};
  rows.forEach(r => {
    const cId = r.campaign_id;
    if (!tree[cId]) tree[cId] = { id: cId, name: r.campaign_name || cId, metrics: emptyMetaMetrics(), children: {} };
    const c = tree[cId];

    const sId = r.adset_id;
    if (!c.children[sId]) c.children[sId] = { id: sId, name: r.adset_name || sId, metrics: emptyMetaMetrics(), children: {} };
    const s = c.children[sId];

    const aId = r.ad_id;
    if (!s.children[aId]) s.children[aId] = { id: aId, name: r.ad_name || aId, metrics: emptyMetaMetrics() };
    const a = s.children[aId];

    [c.metrics, s.metrics, a.metrics].forEach(m => {
      m.spend       += parseFloat(r.spend) || 0;
      m.impressions += parseInt(r.impressions, 10) || 0;
      m.clicks      += parseInt(r.clicks, 10) || 0;
      m.link_clicks += parseInt(r.link_clicks, 10) || 0;
      m.reach       = Math.max(m.reach, parseInt(r.reach, 10) || 0); // reach doesn't sum cleanly across days
    });
  });
  // Compute derived metrics
  function finalize(node) {
    const m = node.metrics;
    m.cpm     = m.impressions > 0 ? (m.spend / m.impressions) * 1000 : 0;
    m.cpc     = m.link_clicks > 0 ? (m.spend / m.link_clicks) : 0;
    m.ctr     = m.impressions > 0 ? (m.clicks / m.impressions) * 100 : 0;
    m.ctr_link = m.impressions > 0 ? (m.link_clicks / m.impressions) * 100 : 0;
    if (node.children) node.children = Object.values(node.children).map(finalize);
    return node;
  }
  return Object.values(tree).map(finalize).sort((a,b) => b.metrics.spend - a.metrics.spend);
}

function emptyMetaMetrics() {
  return { spend: 0, impressions: 0, clicks: 0, link_clicks: 0, reach: 0, cpm: 0, cpc: 0, ctr: 0, ctr_link: 0 };
}

function MetaCampaignTable({ tree }) {
  const [expanded, setExpanded] = useState(new Set());
  function toggle(key) {
    setExpanded(p => { const n = new Set(p); n.has(key) ? n.delete(key) : n.add(key); return n; });
  }
  function walk(nodes, depth, prefix) {
    const out = [];
    nodes.forEach(n => {
      const key = (prefix ? prefix + '|' : '') + n.id;
      out.push({ ...n, depth, key });
      if (expanded.has(key) && n.children && n.children.length) {
        out.push(...walk(n.children, depth + 1, key));
      }
    });
    return out;
  }
  const rows = walk(tree, 0, '');

  return (
    <div style={{
      background: 'var(--card)', border: '1px solid var(--line)',
      borderRadius: 10, overflow: 'hidden', marginBottom: 24
    }}>
      <div style={{ overflowX: 'auto' }}>
        <table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 13 }}>
          <thead>
            <tr style={{ background: '#0d0d0d', borderBottom: '1px solid var(--line)' }}>
              <Th>Campaign / Ad Set / Ad</Th>
              <Th>Spend</Th>
              <Th>Impressions</Th>
              <Th>CPM</Th>
              <Th>Link clicks</Th>
              <Th>CPC</Th>
              <Th>CTR</Th>
              <Th>CTR (link)</Th>
              <Th>Reach</Th>
            </tr>
          </thead>
          <tbody>
            {rows.map(row => {
              const m = row.metrics;
              const expandable = row.children && row.children.length > 0;
              return (
                <tr key={row.key} style={{
                  borderBottom: '1px solid #1a1a1a',
                  background: row.depth === 0 ? 'transparent' : (row.depth === 1 ? 'rgba(255,255,255,0.015)' : 'rgba(255,255,255,0.03)')
                }}>
                  <td style={{ padding: '12px 14px', paddingLeft: 14 + row.depth * 22 }}>
                    <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
                      {expandable ? (
                        <button onClick={() => toggle(row.key)} style={{
                          width: 18, height: 18, padding: 0,
                          background: 'transparent', border: '1px solid var(--line-2)', borderRadius: 4,
                          color: 'var(--muted)', cursor: 'pointer', fontSize: 9,
                          display: 'inline-flex', alignItems: 'center', justifyContent: 'center'
                        }}>{expanded.has(row.key) ? '▾' : '▸'}</button>
                      ) : (
                        <span style={{ width: 18, height: 18, display: 'inline-block' }} />
                      )}
                      <span style={{
                        color: row.depth === 0 ? 'var(--ink)' : 'var(--ink-2)',
                        fontWeight: row.depth === 0 ? 600 : 400,
                      }}>{row.name}</span>
                    </div>
                  </td>
                  <Td><span className="mono" style={{ fontVariantNumeric: 'tabular-nums' }}>${m.spend.toFixed(2)}</span></Td>
                  <Td><span className="mono" style={{ fontVariantNumeric: 'tabular-nums' }}>{m.impressions.toLocaleString()}</span></Td>
                  <Td><span className="mono" style={{ fontVariantNumeric: 'tabular-nums' }}>${m.cpm.toFixed(2)}</span></Td>
                  <Td><span className="mono" style={{ fontVariantNumeric: 'tabular-nums' }}>{m.link_clicks.toLocaleString()}</span></Td>
                  <Td><span className="mono" style={{ fontVariantNumeric: 'tabular-nums' }}>${m.cpc.toFixed(2)}</span></Td>
                  <Td><span className="mono" style={{ fontVariantNumeric: 'tabular-nums' }}>{m.ctr.toFixed(2)}%</span></Td>
                  <Td><span className="mono" style={{ fontVariantNumeric: 'tabular-nums' }}>{m.ctr_link.toFixed(2)}%</span></Td>
                  <Td><span className="mono" style={{ fontVariantNumeric: 'tabular-nums' }}>{m.reach.toLocaleString()}</span></Td>
                </tr>
              );
            })}
          </tbody>
        </table>
      </div>
    </div>
  );
}

// ═════════════════════════════════════════════════════════════
// BLENDED DATA SECTION — Full ad + funnel spectrum
// Campaign → Ad Set → Ad hierarchy with 28-column wide view
// ═════════════════════════════════════════════════════════════

// Column definition: full spectrum from spend → ROAS.
// `source` = 'meta' (requires Meta sync), 'funnel' (from our data), 'sales' (TBD), 'computed'.
// `formula` shows on header tooltip so anyone using the dashboard knows the math.
const BLENDED_COLUMNS = [
  { id: 'spend',          label: 'Spend',                            group: 'spend',      source: 'meta',     fmt: 'money',   formula: 'Meta API: sum of ad spend in window' },
  { id: 'impressions',    label: 'Impressions',                      group: 'spend',      source: 'meta',     fmt: 'int',     formula: 'Meta API: impressions' },
  { id: 'cpm',            label: 'CPM',                              group: 'spend',      source: 'meta',     fmt: 'money',   formula: '= Spend / (Impressions / 1000)' },
  { id: 'link_clicks',    label: 'Link Clicks',                      group: 'spend',      source: 'meta',     fmt: 'int',     formula: 'Meta API: inline_link_clicks' },
  { id: 'cpc',            label: 'CPC',                              group: 'spend',      source: 'meta',     fmt: 'money',   formula: '= Spend / Link Clicks' },
  { id: 'ctr',            label: 'CTR',                              group: 'spend',      source: 'meta',     fmt: 'pct',     formula: '= All clicks / Impressions (Meta)' },
  { id: 'ctr_link',       label: 'CTR (Link CT)',                    group: 'spend',      source: 'meta',     fmt: 'pct',     formula: '= Link Clicks / Impressions' },

  { id: 'lpv',            label: 'Landing Page Views',               group: 'funnel',     source: 'funnel',   fmt: 'int',     formula: 'page_view events on landing page (matched to ad)' },
  { id: 'cost_per_lpv',   label: 'Cost / LPV',                       group: 'funnel',     source: 'computed', fmt: 'money',   formula: '= Spend / Landing Page Views' },
  { id: 'registrants',    label: 'Registrants (Leads)',              group: 'funnel',     source: 'funnel',   fmt: 'int',     formula: 'All registrant rows (partial + complete)' },
  { id: 'cpl',            label: 'CPL',                              group: 'funnel',     source: 'computed', fmt: 'money',   formula: '= Spend / Registrants' },

  { id: 'partials',       label: 'Partial Submissions',              group: 'form',       source: 'funnel',   fmt: 'int',     formula: 'Registrants with status=partial' },
  { id: 'completes',      label: 'Completed Submissions',            group: 'form',       source: 'funnel',   fmt: 'int',     formula: 'Registrants with status=complete' },
  { id: 'lead_complete_rate', label: 'Lead → Completed Rate',        group: 'form',       source: 'computed', fmt: 'pct',     formula: '= Completes / All Registrants. Includes everyone who entered an email.' },
  { id: 'app_complete_rate',  label: 'Application Completed Rate',   group: 'form',       source: 'computed', fmt: 'pct',     formula: '= Completes / (Completes + partials past email step). Excludes email-only bounces.' },
  { id: 'cost_per_complete',  label: 'Cost / Completed',             group: 'form',       source: 'computed', fmt: 'money',   formula: '= Spend / Completed Submissions' },

  { id: 'pr_video_start', label: 'PR Video Start',                   group: 'video',      source: 'funnel',   fmt: 'int',     formula: 'video_play events from this ad\'s traffic' },
  { id: 'pr_video_25',    label: 'PR Video 25%',                     group: 'video',      source: 'funnel',   fmt: 'int',     formula: 'Sessions that reached ≥25%' },
  { id: 'pr_video_50',    label: 'PR Video 50%',                     group: 'video',      source: 'funnel',   fmt: 'int',     formula: 'Sessions that reached ≥50%' },
  { id: 'pr_video_75',    label: 'PR Video 75%',                     group: 'video',      source: 'funnel',   fmt: 'int',     formula: 'Sessions that reached ≥75%' },
  { id: 'pr_video_100',   label: 'PR Video 100%',                    group: 'video',      source: 'funnel',   fmt: 'int',     formula: 'Sessions that reached 100%' },
  { id: 'calendar',       label: 'Add to Calendar',                  group: 'video',      source: 'funnel',   fmt: 'int',     formula: 'calendar_click events from this ad\'s traffic' },

  { id: 'sms_replied',    label: 'Responded w/ SMS',                 group: 'conv',       source: 'funnel',   fmt: 'int',     formula: 'Registrants with sms_responded=true' },
  { id: 'showed_webinar', label: 'Showed To Webinar',                group: 'conv',       source: 'wjam',     fmt: 'int',     formula: 'Pending: WebinarJam integration' },
  { id: 'sales',          label: 'Sales',                            group: 'conv',       source: 'sales',    fmt: 'int',     formula: 'Pending: sales source (Stripe / GHL / manual)' },
  { id: 'revenue',        label: 'Revenue',                          group: 'conv',       source: 'sales',    fmt: 'money',   formula: 'Pending: sum of sales.amount linked to this ad\'s registrants' },
  { id: 'roas',           label: 'ROAS',                             group: 'conv',       source: 'computed', fmt: 'multi',   formula: '= Revenue / Spend' },
];

const COL_GROUPS = {
  spend:  { label: 'Meta spend',  tint: 'transparent' },
  funnel: { label: 'Funnel entry', tint: 'rgba(168,85,247,0.03)' },
  form:   { label: 'Form metrics', tint: 'transparent' },
  video:  { label: 'Engagement',  tint: 'rgba(168,85,247,0.03)' },
  conv:   { label: 'Conversion',  tint: 'transparent' },
};

function fmtCell(v, kind) {
  if (v === null || v === undefined) return <span style={{ color: 'var(--muted-2)' }}>—</span>;
  if (typeof v === 'number' && isNaN(v)) return <span style={{ color: 'var(--muted-2)' }}>—</span>;
  if (kind === 'money') return '$' + v.toFixed(2);
  if (kind === 'pct')   return v.toFixed(2) + '%';
  if (kind === 'multi') return v.toFixed(2) + '×';
  if (kind === 'int')   return Number(v).toLocaleString();
  return v;
}

function BlendedDataSection() {
  const [rangeId, setRangeId] = useState('7');
  const [customStart, setCustomStart] = useState('');
  const [customEnd, setCustomEnd] = useState('');
  const [videoFilter, setVideoFilter] = useState(null);
  const [registrants, setRegistrants] = useState([]);
  const [events, setEvents] = useState([]);
  const [adSpend, setAdSpend] = useState([]); // empty until Meta sync is configured
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  const [sortBy, setSortBy] = useState({ col: 'spend', dir: 'desc' });
  const [expanded, setExpanded] = useState(new Set());

  const { start, end, label } = useMemo(() => {
    if (rangeId === 'custom' && customStart && customEnd) {
      const s = new Date(customStart);
      const e = new Date(customEnd);
      e.setHours(23, 59, 59, 999);
      return { start: s, end: e, label: customStart + ' → ' + customEnd };
    }
    const cfg = DATE_RANGES.find(r => r.id === rangeId) || DATE_RANGES[0];
    return { start: dateRangeStart(cfg.days), end: new Date(), label: 'Last ' + cfg.days + ' days' };
  }, [rangeId, customStart, customEnd]);

  async function refresh() {
    setLoading(true); setError(null);
    try {
      const sinceIso = start.toISOString();
      const [regs, evs] = await Promise.all([
        api('/registrants?registered_at=gte.' + sinceIso),
        api('/tracking_events?created_at=gte.' + sinceIso),
        // TODO: load meta_ad_spend once table exists
      ]);
      setRegistrants(regs);
      setEvents(evs);
      setAdSpend([]); // placeholder
    } catch (e) {
      setError(e.message);
    } finally {
      setLoading(false);
    }
  }
  useEffect(() => { refresh(); }, [start.getTime(), end.getTime()]);

  // Build hierarchy: campaign → ad set → ad
  // Maps from UTM fields: utm_campaign / utm_term / utm_content
  // videoFilter narrows the pr_video_* metrics to a single video when set
  const tree = useMemo(() => buildBlendedTree(registrants, events, adSpend, videoFilter), [registrants, events, adSpend, videoFilter]);

  function toggle(key) {
    setExpanded(prev => {
      const next = new Set(prev);
      if (next.has(key)) next.delete(key);
      else next.add(key);
      return next;
    });
  }

  function handleSort(colId) {
    setSortBy(prev =>
      prev.col === colId
        ? { col: colId, dir: prev.dir === 'desc' ? 'asc' : 'desc' }
        : { col: colId, dir: 'desc' }
    );
  }

  const sortedTree = useMemo(() => {
    if (!tree) return [];
    const sorter = (a, b) => {
      const av = a.metrics[sortBy.col];
      const bv = b.metrics[sortBy.col];
      const aN = (av === null || av === undefined || isNaN(av)) ? -Infinity : av;
      const bN = (bv === null || bv === undefined || isNaN(bv)) ? -Infinity : bv;
      return sortBy.dir === 'desc' ? bN - aN : aN - bN;
    };
    const cloneSort = (nodes) => {
      const out = [...nodes].sort(sorter);
      out.forEach(n => {
        if (n.children) n.children = cloneSort(n.children);
      });
      return out;
    };
    return cloneSort(tree);
  }, [tree, sortBy]);

  return (
    <section>
      <SectionHeader
        eyebrow="Blended data"
        title="Ad spend × funnel"
        subtitle="Campaign → Ad Set → Ad. Click any row to expand. Sortable by any column. Meta spend columns activate once the Meta sync is wired."
      />
      {error && <ErrorBanner message={error} onDismiss={() => setError(null)} />}

      <div style={{ display: 'flex', alignItems: 'center', gap: 14, flexWrap: 'wrap' }}>
        <DateRangeBar
          rangeId={rangeId}
          onSelect={setRangeId}
          customStart={customStart} setCustomStart={setCustomStart}
          customEnd={customEnd} setCustomEnd={setCustomEnd}
          label={label}
        />
        <VideoFilter value={videoFilter} onChange={setVideoFilter} placement="post_reg" />
      </div>

      <MetaSyncStatus />

      {loading ? (
        <Empty label="Loading…" />
      ) : sortedTree.length === 0 ? (
        <Empty label="No registrant data in this window yet." />
      ) : (
        <BlendedTable
          tree={sortedTree}
          expanded={expanded}
          onToggle={toggle}
          sortBy={sortBy}
          onSort={handleSort}
        />
      )}
    </section>
  );
}

// Builds the campaign → ad set → ad tree from registrants + events,
// joining (eventually) with meta_ad_spend rows.
// If `videoFilter` is provided, the pr_video_* metrics only count
// sessions that interacted with that specific video.
function buildBlendedTree(registrants, events, adSpend, videoFilter) {
  // Sessions → utm_content mapping from page_view events (for ad-level visitor metrics)
  const sessionAd = {};
  events.forEach(e => {
    if (e.event_type === 'page_view') {
      const utm = e.event_data || {};
      const ad = utm.utm_content || null;
      if (!sessionAd[e.session_id] && ad) sessionAd[e.session_id] = ad;
    }
  });

  // Per-session signals: video play, video max %, calendar
  // When videoFilter is set, only count video events that match that video_id
  const sessionMaxVidPct = {};
  const sessionsPlayed = new Set();
  const sessionsCalClick = new Set();
  events.forEach(e => {
    if (e.event_type === 'video_progress' && matchesVideo(e, videoFilter)) {
      const p = e.event_data && e.event_data.percent;
      if (typeof p === 'number') sessionMaxVidPct[e.session_id] = Math.max(sessionMaxVidPct[e.session_id] || 0, p);
    }
    if (e.event_type === 'video_play' && matchesVideo(e, videoFilter)) sessionsPlayed.add(e.session_id);
    if (e.event_type === 'calendar_click') sessionsCalClick.add(e.session_id);
  });

  // Per-session source attribution (campaign / adset / ad), from page_view events first, then fallback to landing event
  const sessionAttribution = {};
  events.forEach(e => {
    if (e.event_type === 'page_view' && e.page === 'landing') {
      const utm = e.event_data || {};
      if (!sessionAttribution[e.session_id]) {
        sessionAttribution[e.session_id] = {
          campaign: utm.utm_campaign || null,
          adset: utm.utm_term || null,
          ad: utm.utm_content || null
        };
      }
    }
  });

  // Group registrants into tree
  const tree = {};
  function ensureNode(map, key, info) {
    if (!map[key]) map[key] = { key, name: key, info, metrics: emptyMetrics(), children: {} };
    return map[key];
  }

  registrants.forEach(r => {
    const campaign = r.utm_campaign || '(no campaign)';
    const adset    = r.utm_term     || '(no ad set)';
    const ad       = r.utm_content  || '(no ad)';

    const cNode = ensureNode(tree, campaign, { level: 'campaign' });
    const aNode = ensureNode(cNode.children, adset, { level: 'adset', campaign });
    const adNode = ensureNode(aNode.children, ad, { level: 'ad', campaign, adset });

    const isComplete = r.status === 'complete';
    const advancedPastEmail = (r.dropoff_step && r.dropoff_step !== 'email_entered') || isComplete;
    [cNode, aNode, adNode].forEach(n => {
      n.metrics.registrants += 1;
      if (isComplete) n.metrics.completes += 1;
      else n.metrics.partials += 1;
      if (advancedPastEmail) n.metrics._advancedPastEmail += 1;
      if (r.sms_responded) n.metrics.sms_replied += 1;
    });
  });

  // Attribute session-level signals to ads via sessionAttribution
  Object.entries(sessionAttribution).forEach(([sid, attr]) => {
    const campaign = attr.campaign || '(no campaign)';
    const adset    = attr.adset    || '(no ad set)';
    const ad       = attr.ad       || '(no ad)';

    const cNode = tree[campaign];
    if (!cNode) return;
    const aNode = cNode.children[adset];
    if (!aNode) return;
    const adNode = aNode.children[ad];
    if (!adNode) return;

    const nodes = [cNode, aNode, adNode];
    nodes.forEach(n => {
      n.metrics.lpv += 1;
      if (sessionsPlayed.has(sid)) n.metrics.pr_video_start += 1;
      const maxPct = sessionMaxVidPct[sid] || 0;
      if (maxPct >= 25) n.metrics.pr_video_25 += 1;
      if (maxPct >= 50) n.metrics.pr_video_50 += 1;
      if (maxPct >= 75) n.metrics.pr_video_75 += 1;
      if (maxPct >= 100) n.metrics.pr_video_100 += 1;
      if (sessionsCalClick.has(sid)) n.metrics.calendar += 1;
    });
  });

  // TODO: Join meta_ad_spend rows here. For each ad with spend data, fill in:
  //   metrics.spend, .impressions, .cpm, .link_clicks, .cpc, .ctr, .ctr_link

  // Compute derived columns (cpl, cost_per_lpv, lead_complete_rate, etc.) for each node
  function finalize(node) {
    const m = node.metrics;
    m.cpl              = m.spend && m.registrants ? m.spend / m.registrants : null;
    m.cost_per_lpv     = m.spend && m.lpv         ? m.spend / m.lpv : null;
    m.cost_per_complete= m.spend && m.completes   ? m.spend / m.completes : null;
    m.lead_complete_rate = m.registrants > 0 ? (m.completes / m.registrants) * 100 : 0;
    m.app_complete_rate  = (m.completes + (m._advancedPastEmail - m.completes)) > 0
      ? (m.completes / (m.completes + (m._advancedPastEmail - m.completes))) * 100
      : 0;
    // Placeholders until wired
    m.showed_webinar = null;
    m.sales = null;
    m.revenue = null;
    m.roas = (m.revenue && m.spend) ? m.revenue / m.spend : null;

    node.children = Object.values(node.children).map(finalize);
    return node;
  }
  return Object.values(tree).map(finalize);
}

function emptyMetrics() {
  return {
    // Meta side (filled in by sync)
    spend: null, impressions: null, cpm: null,
    link_clicks: null, cpc: null, ctr: null, ctr_link: null,
    // Our side
    lpv: 0,
    registrants: 0,
    partials: 0,
    completes: 0,
    pr_video_start: 0,
    pr_video_25: 0,
    pr_video_50: 0,
    pr_video_75: 0,
    pr_video_100: 0,
    calendar: 0,
    sms_replied: 0,
    _advancedPastEmail: 0,
    // Placeholder (TBD source)
    showed_webinar: null,
    sales: null,
    revenue: null,
    roas: null,
    // Computed (set in finalize)
    cpl: null, cost_per_lpv: null, cost_per_complete: null,
    lead_complete_rate: 0, app_complete_rate: 0,
  };
}

function MetaSyncStatus() {
  // For now a static "not connected" banner. Will become a real status indicator once meta-sync exists.
  return (
    <div style={{
      background: 'rgba(168,85,247,0.06)',
      border: '1px solid rgba(168,85,247,0.25)',
      borderRadius: 8,
      padding: '11px 14px',
      marginBottom: 18,
      display: 'flex', alignItems: 'center', gap: 12, flexWrap: 'wrap'
    }}>
      <span className="mono" style={{
        fontSize: 10, letterSpacing: '0.22em', textTransform: 'uppercase',
        padding: '3px 8px', borderRadius: 4,
        background: 'rgba(251,191,36,0.15)', color: '#fbbf24', fontWeight: 700
      }}>Not connected</span>
      <span style={{ fontSize: 13, color: 'var(--ink-2)' }}>
        Meta API sync isn't wired yet. Spend, impressions, CPM, CPC and CTR columns will populate after the Meta integration ships.
      </span>
    </div>
  );
}

function BlendedTable({ tree, expanded, onToggle, sortBy, onSort }) {
  // Walk the tree → flat list of visible rows respecting expanded state
  function walk(nodes, depth, prefix) {
    const out = [];
    nodes.forEach(n => {
      const key = (prefix ? prefix + '|' : '') + n.key;
      out.push({ ...n, depth, key });
      if (expanded.has(key) && n.children && n.children.length) {
        out.push(...walk(n.children, depth + 1, key));
      }
    });
    return out;
  }
  const rows = walk(tree, 0, '');

  return (
    <div style={{
      background: 'var(--card)',
      border: '1px solid var(--line)',
      borderRadius: 10,
      overflow: 'hidden',
      marginBottom: 28
    }}>
      <div style={{ overflowX: 'auto', maxHeight: 640 }}>
        <table style={{ borderCollapse: 'collapse', fontSize: 12, width: 'max-content' }}>
          <thead>
            <tr style={{ background: '#0d0d0d', borderBottom: '1px solid var(--line)' }}>
              <BlendedTh
                label="Campaign / Ad Set / Ad"
                sticky
                width={300}
                formula="Hierarchy from utm_campaign → utm_term → utm_content"
              />
              {BLENDED_COLUMNS.map(col => (
                <BlendedTh
                  key={col.id}
                  label={col.label}
                  width={col.id === 'pr_video_start' || col.id.startsWith('pr_video_') ? 110 : 130}
                  active={sortBy.col === col.id}
                  dir={sortBy.col === col.id ? sortBy.dir : null}
                  onClick={() => onSort(col.id)}
                  tint={COL_GROUPS[col.group] && COL_GROUPS[col.group].tint}
                  formula={col.formula}
                  pending={col.source === 'meta' || col.source === 'wjam' || col.source === 'sales'}
                />
              ))}
            </tr>
          </thead>
          <tbody>
            {rows.map(row => (
              <BlendedRow
                key={row.key}
                row={row}
                expanded={expanded.has(row.key)}
                onToggle={() => onToggle(row.key)}
                expandable={row.children && row.children.length > 0}
              />
            ))}
          </tbody>
        </table>
      </div>
      <div className="mono" style={{
        padding: '10px 14px', borderTop: '1px solid var(--line)',
        fontSize: 10, letterSpacing: '0.16em', textTransform: 'uppercase',
        color: 'var(--muted)'
      }}>
        Scroll horizontally to see all {1 + BLENDED_COLUMNS.length} columns · click any header to sort · click ▸ to drill into ad sets and ads
      </div>
    </div>
  );
}

function BlendedTh({ label, formula, sticky, width, active, dir, onClick, tint, pending }) {
  return (
    <th
      title={formula || ''}
      onClick={onClick}
      style={{
        position: sticky ? 'sticky' : 'static',
        left: sticky ? 0 : undefined,
        zIndex: sticky ? 3 : 2,
        background: sticky ? '#0d0d0d' : (tint || '#0d0d0d'),
        padding: '10px 12px',
        textAlign: sticky ? 'left' : 'right',
        fontSize: 10, letterSpacing: '0.16em', textTransform: 'uppercase',
        color: active ? 'var(--accent)' : 'var(--muted)',
        fontFamily: 'Geist Mono, ui-monospace, monospace',
        fontWeight: 600,
        cursor: onClick ? 'pointer' : 'default',
        whiteSpace: 'nowrap',
        borderRight: sticky ? '1px solid var(--line)' : 'none',
        width: width ? width + 'px' : undefined,
        minWidth: width ? width + 'px' : undefined,
        userSelect: 'none'
      }}
    >
      <span style={{ opacity: pending ? 0.6 : 1 }}>{label}</span>
      {active && <span style={{ marginLeft: 6 }}>{dir === 'desc' ? '↓' : '↑'}</span>}
    </th>
  );
}

function BlendedRow({ row, expanded, onToggle, expandable }) {
  const isTopLevel = row.depth === 0;
  return (
    <tr style={{
      borderBottom: '1px solid #1a1a1a',
      background: row.depth === 0 ? 'transparent' : (row.depth === 1 ? 'rgba(255,255,255,0.015)' : 'rgba(255,255,255,0.03)')
    }}>
      <td style={{
        position: 'sticky',
        left: 0,
        zIndex: 1,
        background: row.depth === 0 ? 'var(--card)' : (row.depth === 1 ? '#131316' : '#16161a'),
        padding: '12px 14px',
        paddingLeft: 14 + row.depth * 22,
        borderRight: '1px solid var(--line)',
        whiteSpace: 'nowrap'
      }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
          {expandable ? (
            <button onClick={onToggle} style={{
              width: 18, height: 18, padding: 0,
              background: 'transparent', border: '1px solid var(--line-2)', borderRadius: 4,
              color: 'var(--muted)', cursor: 'pointer', fontSize: 9,
              display: 'inline-flex', alignItems: 'center', justifyContent: 'center'
            }}>{expanded ? '▾' : '▸'}</button>
          ) : (
            <span style={{ width: 18, height: 18, display: 'inline-block' }} />
          )}
          <span style={{
            color: isTopLevel ? 'var(--ink)' : 'var(--ink-2)',
            fontWeight: isTopLevel ? 600 : 400,
            fontSize: 13
          }}>{row.name}</span>
        </div>
      </td>
      {BLENDED_COLUMNS.map(col => {
        const v = row.metrics[col.id];
        return (
          <td key={col.id} style={{
            padding: '12px 12px',
            textAlign: 'right',
            color: 'var(--ink-2)',
            fontVariantNumeric: 'tabular-nums',
            background: COL_GROUPS[col.group] && COL_GROUPS[col.group].tint,
            whiteSpace: 'nowrap',
            fontFamily: 'Geist Mono, ui-monospace, monospace'
          }}>
            {fmtCell(v, col.fmt)}
          </td>
        );
      })}
    </tr>
  );
}

// ═════════════════════════════════════════════════════════════
// FUNNEL METRICS SECTION — Phase 1: KPI panel + funnel + spreadsheet
// ═════════════════════════════════════════════════════════════

// All available date ranges in the KPI selector
const DATE_RANGES = [
  { id: '7',   label: '7 days',  days: 7 },
  { id: '14',  label: '14 days', days: 14 },
  { id: '30',  label: '30 days', days: 30 },
  { id: '90',  label: '90 days', days: 90 },
];

// ─────────────────────────────────────────────
// ORGANIC (NATHAN) — non-attributed stats for the standalone organic pages
// ─────────────────────────────────────────────
const ORGANIC_LABELS = {
  'nathan-webinar-1': "Nathan · Tue Jun 23, 12pm ET",
  'nathan-webinar-2': "Nathan · Tue Jun 23, 7pm ET",
};
const ORGANIC_PATHS = {
  'nathan-webinar-1': '/nathan-webinar-1',
  'nathan-webinar-2': '/nathan-webinar-2',
};
const ORGANIC_SERVICE_LABEL = { apple: 'Apple', google: 'Google', office365: 'Office 365', outlook: 'Outlook', outlookcom: 'Outlook.com', yahoo: 'Yahoo' };
const ORGANIC_SOURCE_LABEL = { hero: 'Hero button', final_cta: 'Final CTA', sticky: 'Sticky bar' };

function OrganicSection() {
  const [events, setEvents] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  async function refresh() {
    setLoading(true); setError(null);
    try {
      const evs = await api('/tracking_events?page_type=eq.organic&order=created_at.desc&limit=5000');
      setEvents(evs);
    } catch (e) { setError(e.message); }
    finally { setLoading(false); }
  }
  useEffect(() => { refresh(); }, []);
  useEffect(() => {
    const unsub = window.FeedRegistrants && window.FeedRegistrants.onChange(() => refresh());
    return () => { unsub && unsub(); };
  }, []);

  const byVariant = useMemo(() => {
    const map = {};
    function get(v) {
      if (!map[v]) map[v] = { viewSessions: new Set(), pageViews: 0, clicks: [], clickSessions: new Set(), videoPlays: 0 };
      return map[v];
    }
    events.forEach(e => {
      const m = get(e.page_variant || 'unknown');
      if (e.event_type === 'page_view') { m.pageViews++; if (e.session_id) m.viewSessions.add(e.session_id); }
      else if (e.event_type === 'calendar_click') { m.clicks.push(e); if (e.session_id) m.clickSessions.add(e.session_id); }
      else if (e.event_type === 'video_play') { m.videoPlays++; }
    });
    return map;
  }, [events]);

  const variants = useMemo(() => {
    const known = ['nathan-webinar-1', 'nathan-webinar-2'];
    const extra = Object.keys(byVariant).filter(v => known.indexOf(v) < 0);
    return known.concat(extra);
  }, [byVariant]);

  function tally(arr, key) {
    const out = {};
    arr.forEach(e => { const k = (e.event_data && e.event_data[key]) || '—'; out[k] = (out[k] || 0) + 1; });
    return Object.entries(out).sort((a, b) => b[1] - a[1]);
  }

  return (
    <section>
      <SectionHeader eyebrow="Organic" title="Nathan's organic webinars"
        subtitle="Non-attributed traffic on the standalone organic pages (visitors don't register through us). Add-to-calendar clicks, page views, and video plays per page." />
      {error && <ErrorBanner message={error} onDismiss={() => setError(null)} />}
      {loading ? <Empty label="Loading…" /> : (
        <div style={{ display: 'flex', flexDirection: 'column', gap: 20 }}>
          {variants.map(v => {
            const m = byVariant[v] || { viewSessions: new Set(), pageViews: 0, clicks: [], clickSessions: new Set(), videoPlays: 0 };
            const services = tally(m.clicks, 'service');
            const sources = tally(m.clicks, 'source');
            return (
              <div key={v} style={{ background: 'var(--card)', border: '1px solid var(--line)', borderRadius: 10, padding: '18px 20px' }}>
                <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', flexWrap: 'wrap', gap: 8, marginBottom: 16 }}>
                  <div style={{ fontSize: 16, fontWeight: 600 }}>{ORGANIC_LABELS[v] || v}</div>
                  {ORGANIC_PATHS[v] && <a href={ORGANIC_PATHS[v]} target="_blank" rel="noopener" className="mono" style={{ fontSize: 11, color: 'var(--accent)', textDecoration: 'none' }}>{ORGANIC_PATHS[v]} ↗</a>}
                </div>
                <div style={{ display: 'flex', gap: 32, flexWrap: 'wrap', marginBottom: 18 }}>
                  <Stat label="Visitors" value={m.viewSessions.size} />
                  <Stat label="Page views" value={m.pageViews} />
                  <Stat label="Calendar clicks" value={m.clicks.length} />
                  <Stat label="Unique clickers" value={m.clickSessions.size} />
                  <Stat label="Video plays" value={m.videoPlays} />
                </div>
                <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(220px, 1fr))', gap: 20 }}>
                  <OrganicBreakdown title="By calendar" rows={services} labels={ORGANIC_SERVICE_LABEL} />
                  <OrganicBreakdown title="By button" rows={sources} labels={ORGANIC_SOURCE_LABEL} />
                </div>
              </div>
            );
          })}
        </div>
      )}
    </section>
  );
}

function OrganicBreakdown({ title, rows, labels }) {
  return (
    <div>
      <div className="mono" style={{ fontSize: 10, letterSpacing: '0.18em', textTransform: 'uppercase', color: 'var(--muted)', fontWeight: 600, marginBottom: 10 }}>{title}</div>
      {rows.length === 0 ? <div style={{ fontSize: 13, color: 'var(--muted-2)' }}>No clicks yet</div> : (
        <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
          {rows.map(([k, n]) => (
            <div key={k} style={{ display: 'flex', justifyContent: 'space-between', fontSize: 13 }}>
              <span style={{ color: 'var(--ink-2)' }}>{(labels && labels[k]) || k}</span>
              <span className="mono" style={{ color: 'var(--muted)' }}>{n}</span>
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

function dateRangeStart(days) {
  const d = new Date();
  d.setDate(d.getDate() - days);
  d.setHours(0, 0, 0, 0);
  return d;
}

function MetricsSection() {
  const [rangeId, setRangeId] = useState('7');
  const [customStart, setCustomStart] = useState('');
  const [customEnd, setCustomEnd] = useState('');
  const [pageScope, setPageScope] = useState('all'); // 'all' | 'landing' | 'post_reg'
  const [videoFilter, setVideoFilter] = useState(null); // video_id or null = all
  const [deepDive, setDeepDive] = useState(false);
  const [registrants, setRegistrants] = useState([]);
  const [events, setEvents] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  // Determine date window
  const { start, end, prevStart, prevEnd, label } = useMemo(() => {
    if (rangeId === 'custom' && customStart && customEnd) {
      const s = new Date(customStart);
      const e = new Date(customEnd);
      e.setHours(23, 59, 59, 999);
      const days = Math.max(1, Math.round((e.getTime() - s.getTime()) / 86400000));
      const prevE = new Date(s.getTime() - 1);
      const prevS = new Date(prevE.getTime() - days * 86400000);
      return { start: s, end: e, prevStart: prevS, prevEnd: prevE, label: customStart + ' → ' + customEnd };
    }
    const cfg = DATE_RANGES.find(r => r.id === rangeId) || DATE_RANGES[0];
    const s = dateRangeStart(cfg.days);
    const e = new Date();
    const prevE = new Date(s.getTime() - 1);
    const prevS = new Date(prevE.getTime() - cfg.days * 86400000);
    return { start: s, end: e, prevStart: prevS, prevEnd: prevE, label: 'Last ' + cfg.days + ' days' };
  }, [rangeId, customStart, customEnd]);

  async function refresh() {
    setLoading(true); setError(null);
    try {
      // Pull everything in the window + the prior window (for deltas)
      const sinceIso = prevStart.toISOString();
      const [regs, evs] = await Promise.all([
        api('/registrants?registered_at=gte.' + sinceIso + '&order=registered_at.desc&select=*,webinar_events(title)'),
        api('/tracking_events?created_at=gte.' + sinceIso + '&order=created_at.desc'),
      ]);
      setRegistrants(regs);
      setEvents(evs);
    } catch (e) {
      setError(e.message);
    } finally {
      setLoading(false);
    }
  }
  useEffect(() => { refresh(); }, [start.getTime(), end.getTime()]);

  // Auto-refresh on Realtime registrant changes
  useEffect(() => {
    const unsub = window.FeedRegistrants && window.FeedRegistrants.onChange(() => refresh());
    return () => { unsub && unsub(); };
  }, [start.getTime(), end.getTime()]);

  // Split into current vs previous window — flat arrays for downstream components
  const { current, prior } = useMemo(() => {
    function inRange(t, s, e) { return t >= s.getTime() && t <= e.getTime(); }
    const cur = { registrants: [], events: [] };
    const prv = { registrants: [], events: [] };
    registrants.forEach(r => {
      const t = new Date(r.registered_at).getTime();
      if (inRange(t, start, end)) cur.registrants.push(r);
      else if (inRange(t, prevStart, prevEnd)) prv.registrants.push(r);
    });
    events.forEach(e => {
      const t = new Date(e.created_at).getTime();
      if (inRange(t, start, end)) cur.events.push(e);
      else if (inRange(t, prevStart, prevEnd)) prv.events.push(e);
    });
    return { current: cur, prior: prv };
  }, [registrants, events, start, end, prevStart, prevEnd]);

  // Apply the page-variant filter (if any) before passing data to children.
  // The page_type half of the scope is still passed via `scope` prop so existing
  // `scope === 'landing'` checks inside children keep working unchanged.
  const scopeParts = parseScope(pageScope);
  const scopePageType = scopeParts.pageType || 'all';
  const variantFilter = scopeParts.pageVariant; // null = no variant filter
  const filteredCurrent = useMemo(() => {
    if (!variantFilter) return current;
    return {
      events: current.events.filter(e => !e.page_variant || e.page_variant === variantFilter),
      registrants: current.registrants.filter(r => !r.entry_page_variant || r.entry_page_variant === variantFilter),
    };
  }, [current, variantFilter]);
  const filteredPrior = useMemo(() => {
    if (!variantFilter) return prior;
    return {
      events: prior.events.filter(e => !e.page_variant || e.page_variant === variantFilter),
      registrants: prior.registrants.filter(r => !r.entry_page_variant || r.entry_page_variant === variantFilter),
    };
  }, [prior, variantFilter]);
  const scopeOptions = useScopeOptions(events);

  return (
    <section>
      <SectionHeader
        eyebrow="Funnel metrics"
        title="Pipeline performance"
        subtitle="The dashboard. Every metric pulls live from registrants + tracking_events."
      />
      {error && <ErrorBanner message={error} onDismiss={() => setError(null)} />}

      <div style={{ display: 'flex', alignItems: 'center', gap: 14, marginBottom: 16, flexWrap: 'wrap' }}>
        <PageScopeDropdown scope={pageScope} onSelect={setPageScope} options={scopeOptions} />
        {scopePageType !== 'landing' && (
          <VideoFilter value={videoFilter} onChange={setVideoFilter} placement="post_reg" />
        )}
      </div>

      <DateRangeBar
        rangeId={rangeId}
        onSelect={setRangeId}
        customStart={customStart} setCustomStart={setCustomStart}
        customEnd={customEnd} setCustomEnd={setCustomEnd}
        label={label}
      />

      {loading ? (
        <Empty label="Loading…" />
      ) : (
        <>
          <KpiPanel current={filteredCurrent} prior={filteredPrior} scope={scopePageType} videoFilter={videoFilter} />
          <FunnelViz current={filteredCurrent} scope={scopePageType} videoFilter={videoFilter} />
          <TimeSeriesChart current={filteredCurrent} prior={filteredPrior} start={start} end={end} prevStart={prevStart} prevEnd={prevEnd} />
          <SourceBreakdown current={filteredCurrent} />

          <DeepDiveToggle open={deepDive} onToggle={() => setDeepDive(d => !d)} scope={scopePageType} />
          {deepDive && <DeepDive current={filteredCurrent} scope={scopePageType} videoFilter={videoFilter} />}

          <DataTable registrants={filteredCurrent.registrants} events={filteredCurrent.events} />
        </>
      )}
    </section>
  );
}

// ─────────────────────────────────────────────
// Page scope tabs — restrict metrics to a single page
// ─────────────────────────────────────────────
// Page-type baseline labels. Variants discovered dynamically from data.
const PAGE_TYPE_LABELS = {
  landing: 'Landing',
  post_reg: 'Post-Registration',
  sales: 'Sales',
  upsell: 'Upsell',
  vsl: 'VSL',
  webinar: 'Webinar',
  email: 'Email',
  thank_you: 'Thank You',
};

// Parse a flat scope id (e.g. 'landing:agency') into its components.
// 'all' = no filter. 'landing' = all variants of landing. 'landing:agency' = specific variant.
function parseScope(id) {
  if (!id || id === 'all') return { pageType: null, pageVariant: null };
  const [pageType, pageVariant] = id.split(':');
  return { pageType, pageVariant: pageVariant || null };
}

// Format a scope id for display.
function formatScope(id) {
  if (!id || id === 'all') return 'All pages';
  const { pageType, pageVariant } = parseScope(id);
  const typeLabel = PAGE_TYPE_LABELS[pageType] || pageType;
  if (!pageVariant) return typeLabel + ' · All variants';
  return typeLabel + ' · ' + pageVariant.charAt(0).toUpperCase() + pageVariant.slice(1);
}

// Builds the full list of scope options from data: 'all' + each page_type + each (page_type, page_variant) pair.
function useScopeOptions(events) {
  return useMemo(() => {
    const variantsByType = {};
    events.forEach(e => {
      const t = e.page_type;
      const v = e.page_variant;
      if (!t) return;
      if (!variantsByType[t]) variantsByType[t] = new Set();
      if (v) variantsByType[t].add(v);
    });
    const opts = [{ id: 'all', label: 'All pages' }];
    Object.keys(variantsByType).sort().forEach(type => {
      // "Landing · All variants" entry only when there's more than one variant for that type
      const variants = Array.from(variantsByType[type]).sort();
      const typeLabel = PAGE_TYPE_LABELS[type] || type;
      if (variants.length > 1) {
        opts.push({ id: type, label: typeLabel + ' · All variants' });
      }
      variants.forEach(v => {
        opts.push({
          id: type + ':' + v,
          label: typeLabel + ' · ' + v.charAt(0).toUpperCase() + v.slice(1),
        });
      });
    });
    return opts;
  }, [events]);
}

function PageScopeDropdown({ scope, onSelect, options }) {
  return (
    <select
      value={scope}
      onChange={e => onSelect(e.target.value)}
      className="mono"
      style={{
        padding: '8px 14px',
        background: 'var(--bg-3)',
        color: 'var(--ink)',
        border: '1px solid var(--line)',
        borderRadius: 8,
        fontSize: 12, letterSpacing: '0.06em',
        fontFamily: 'inherit',
        fontWeight: 600,
        cursor: 'pointer',
        marginBottom: 16,
        minWidth: 220,
      }}
    >
      {options.map(o => (
        <option key={o.id} value={o.id}>{o.label}</option>
      ))}
    </select>
  );
}

// ─────────────────────────────────────────────
// VARIANT COMPARE SECTION
// Pick any two variants and see side-by-side metrics
// across collapsible funnel-stage groups.
// ─────────────────────────────────────────────
function VariantCompareSection() {
  const [rangeId, setRangeId] = useState('30');
  const [customStart, setCustomStart] = useState('');
  const [customEnd, setCustomEnd] = useState('');
  const [variantA, setVariantA] = useState(null);
  const [variantB, setVariantB] = useState(null);
  const [registrants, setRegistrants] = useState([]);
  const [events, setEvents] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  const { start, end, label } = useMemo(() => {
    if (rangeId === 'custom' && customStart && customEnd) {
      const s = new Date(customStart);
      const e = new Date(customEnd);
      e.setHours(23, 59, 59, 999);
      return { start: s, end: e, label: customStart + ' → ' + customEnd };
    }
    const cfg = DATE_RANGES.find(r => r.id === rangeId) || DATE_RANGES[0];
    return { start: dateRangeStart(cfg.days), end: new Date(), label: 'Last ' + cfg.days + ' days' };
  }, [rangeId, customStart, customEnd]);

  async function refresh() {
    setLoading(true); setError(null);
    try {
      const sinceIso = start.toISOString();
      const [regs, evs] = await Promise.all([
        api('/registrants?registered_at=gte.' + sinceIso + '&order=registered_at.desc'),
        api('/tracking_events?created_at=gte.' + sinceIso + '&order=created_at.desc'),
      ]);
      setRegistrants(regs);
      setEvents(evs);
    } catch (e) { setError(e.message); }
    finally { setLoading(false); }
  }
  useEffect(() => { refresh(); }, [start.getTime(), end.getTime()]);

  // Distinct variants discovered from data
  const variants = useMemo(() => {
    const set = new Set();
    events.forEach(e => {
      if (e.page_type && e.page_variant) set.add(e.page_type + ':' + e.page_variant);
    });
    return Array.from(set).sort();
  }, [events]);

  // Auto-pick first two if not yet chosen
  useEffect(() => {
    if (!variantA && variants[0]) setVariantA(variants[0]);
    if (!variantB && variants[1]) setVariantB(variants[1]);
  }, [variants, variantA, variantB]);

  const metricsA = useMemo(() => computeVariantMetrics(events, registrants, variantA), [events, registrants, variantA]);
  const metricsB = useMemo(() => computeVariantMetrics(events, registrants, variantB), [events, registrants, variantB]);

  function labelFor(v) { return v ? formatScope(v) : '— pick a variant —'; }

  return (
    <section>
      <SectionHeader
        eyebrow="A/B comparison"
        title="Variant compare"
        subtitle="Pick any two page variants and see how they perform side by side."
        action={<button onClick={refresh} style={btnGhost}>Refresh</button>}
      />
      {error && <ErrorBanner message={error} onDismiss={() => setError(null)} />}

      <DateRangeBar
        rangeId={rangeId}
        onSelect={setRangeId}
        customStart={customStart} setCustomStart={setCustomStart}
        customEnd={customEnd} setCustomEnd={setCustomEnd}
        label={label}
      />

      <div style={{ display: 'flex', gap: 16, marginBottom: 24, flexWrap: 'wrap' }}>
        <div style={{ flex: 1, minWidth: 240 }}>
          <div className="mono" style={{ fontSize: 10, letterSpacing: '0.2em', textTransform: 'uppercase', color: 'var(--muted)', marginBottom: 6 }}>Variant A</div>
          <select value={variantA || ''} onChange={e => setVariantA(e.target.value)} style={compareSelectStyle}>
            <option value="">— pick a variant —</option>
            {variants.map(v => <option key={v} value={v}>{formatScope(v)}</option>)}
          </select>
        </div>
        <div style={{ flex: 1, minWidth: 240 }}>
          <div className="mono" style={{ fontSize: 10, letterSpacing: '0.2em', textTransform: 'uppercase', color: 'var(--muted)', marginBottom: 6 }}>Variant B</div>
          <select value={variantB || ''} onChange={e => setVariantB(e.target.value)} style={compareSelectStyle}>
            <option value="">— pick a variant —</option>
            {variants.map(v => <option key={v} value={v}>{formatScope(v)}</option>)}
          </select>
        </div>
      </div>

      {loading ? (
        <Empty label="Loading…" />
      ) : !variantA || !variantB ? (
        <Empty label={variants.length < 2 ? 'Only one variant exists in this date range. Need at least two to compare.' : 'Pick two variants above to begin.'} />
      ) : (
        <>
          <CompareHeader labelA={labelFor(variantA)} labelB={labelFor(variantB)} />

          <CompareGroup title="Top of funnel" defaultOpen={true}>
            <CompareRow label="Visitors"               a={metricsA.visitors}   b={metricsB.visitors}   />
            <CompareRow label="Email captures"         a={metricsA.emails}     b={metricsB.emails}     />
            <CompareRow label="Completed registrations" a={metricsA.completed} b={metricsB.completed} />
            <CompareRow label="Conversion rate" a={metricsA.conversionRate} b={metricsB.conversionRate} format="percent" />
            <CompareRow label="Email-to-complete rate" a={metricsA.emailToCompleteRate} b={metricsB.emailToCompleteRate} format="percent" />
          </CompareGroup>

          <CompareGroup title="Engagement (downstream sessions)">
            <CompareRow label="Post-reg page views" a={metricsA.postRegViews} b={metricsB.postRegViews} />
            <CompareRow label="Video plays" a={metricsA.videoPlays} b={metricsB.videoPlays} />
            <CompareRow label="Reached 25% of video" a={metricsA.video25} b={metricsB.video25} />
            <CompareRow label="Reached 50% of video" a={metricsA.video50} b={metricsB.video50} />
            <CompareRow label="Reached 75% of video" a={metricsA.video75} b={metricsB.video75} />
            <CompareRow label="Reached 90% of video" a={metricsA.video90} b={metricsB.video90} />
          </CompareGroup>

          <CompareGroup title="Bottom of funnel">
            <CompareRow label="Calendar clicks" a={metricsA.calClicks} b={metricsB.calClicks} />
            <CompareRow label="SMS replies" a={metricsA.smsReplies} b={metricsB.smsReplies} />
            <CompareRow label="Show rate (pending WebinarJam wiring)" a={null} b={null} pending />
            <CompareRow label="Buy rate (pending sales wiring)" a={null} b={null} pending />
          </CompareGroup>
        </>
      )}
    </section>
  );
}

// Computes per-variant funnel metrics from raw events + registrants arrays.
// Sessions = distinct session_ids that had a page_view on this variant's page.
// Registrants = those whose entry_page_type + entry_page_variant match.
function computeVariantMetrics(events, registrants, variantId) {
  const empty = {
    visitors: 0, emails: 0, completed: 0, conversionRate: 0, emailToCompleteRate: 0,
    postRegViews: 0, videoPlays: 0, video25: 0, video50: 0, video75: 0, video90: 0,
    calClicks: 0, smsReplies: 0,
  };
  if (!variantId) return empty;
  const { pageType, pageVariant } = parseScope(variantId);
  if (!pageType || !pageVariant) return empty;

  // Sessions that hit this variant's page (any page_view event)
  const sessions = new Set();
  events.forEach(e => {
    if (e.event_type === 'page_view' && e.page_type === pageType && e.page_variant === pageVariant) {
      sessions.add(e.session_id);
    }
  });

  // Registrants who first entered through this variant
  const regs = registrants.filter(r =>
    (r.entry_page_type || 'landing') === pageType &&
    (r.entry_page_variant || 'default') === pageVariant
  );
  const completed = regs.filter(r => r.status === 'complete').length;
  const emails    = regs.length; // all registrants = email-captured
  const visitors  = sessions.size;
  const smsReplies = regs.filter(r => r.sms_responded).length;

  // Downstream events: any event from a session that hit this variant
  const downstream = events.filter(e => sessions.has(e.session_id));

  const postRegSessions = new Set();
  const videoPlaySessions = new Set();
  const maxVidPct = {};
  const calClickSessions = new Set();
  downstream.forEach(e => {
    if (e.event_type === 'page_view' && e.page_type === 'post_reg') postRegSessions.add(e.session_id);
    if (e.event_type === 'video_play') videoPlaySessions.add(e.session_id);
    if (e.event_type === 'video_progress') {
      const p = e.event_data && e.event_data.percent;
      if (typeof p === 'number') maxVidPct[e.session_id] = Math.max(maxVidPct[e.session_id] || 0, p);
    }
    if (e.event_type === 'calendar_click') calClickSessions.add(e.session_id);
  });
  const countMilestone = pct => Object.values(maxVidPct).filter(v => v >= pct).length;

  return {
    visitors,
    emails,
    completed,
    conversionRate:        visitors > 0 ? completed / visitors : 0,
    emailToCompleteRate:   emails > 0   ? completed / emails   : 0,
    postRegViews:          postRegSessions.size,
    videoPlays:            videoPlaySessions.size,
    video25:               countMilestone(25),
    video50:               countMilestone(50),
    video75:               countMilestone(75),
    video90:               countMilestone(90),
    calClicks:             calClickSessions.size,
    smsReplies,
  };
}

const compareSelectStyle = {
  width: '100%',
  padding: '10px 14px',
  background: 'var(--bg-3)',
  color: 'var(--ink)',
  border: '1px solid var(--line)',
  borderRadius: 8,
  fontSize: 13, fontWeight: 600,
  fontFamily: 'inherit',
  cursor: 'pointer',
};

// Header row: column titles for variant A / B
function CompareHeader({ labelA, labelB }) {
  return (
    <div style={{
      display: 'grid', gridTemplateColumns: '1fr 1fr 1fr',
      gap: 0, alignItems: 'end',
      padding: '12px 18px',
      borderTop: '1px solid var(--line)',
      borderBottom: '1px solid var(--line)',
      background: '#0d0d0d',
      marginBottom: 0,
    }}>
      <div className="mono" style={{ fontSize: 10, letterSpacing: '0.22em', textTransform: 'uppercase', color: 'var(--muted)' }}>
        Metric
      </div>
      <div style={{ fontSize: 14, fontWeight: 700, color: 'var(--ink)', textAlign: 'center' }}>{labelA}</div>
      <div style={{ fontSize: 14, fontWeight: 700, color: 'var(--ink)', textAlign: 'center' }}>{labelB}</div>
    </div>
  );
}

// Collapsible group of comparison rows
function CompareGroup({ title, children, defaultOpen }) {
  const [open, setOpen] = useState(!!defaultOpen);
  return (
    <div style={{ borderBottom: '1px solid var(--line)' }}>
      <button onClick={() => setOpen(o => !o)} style={{
        width: '100%', padding: '14px 18px', textAlign: 'left',
        background: open ? 'rgba(168,85,247,0.04)' : '#0a0a0a',
        border: 'none', cursor: 'pointer', fontFamily: 'inherit',
        display: 'flex', alignItems: 'center', justifyContent: 'space-between',
      }}>
        <span style={{ fontSize: 13, fontWeight: 700, color: 'var(--ink)', letterSpacing: '-0.005em' }}>{title}</span>
        <span style={{ color: 'var(--muted)', fontSize: 10, transform: open ? 'rotate(90deg)' : 'none', transition: 'transform 140ms' }}>▶</span>
      </button>
      {open && <div>{children}</div>}
    </div>
  );
}

// One side-by-side comparison row. Computes the delta % and color-codes the winner.
function CompareRow({ label, a, b, format, pending }) {
  function fmt(v) {
    if (v == null) return '—';
    if (format === 'percent') return (v * 100).toFixed(1) + '%';
    return Number(v).toLocaleString();
  }
  let deltaPct = null;
  let aWins = false, bWins = false;
  if (!pending && a != null && b != null && (a > 0 || b > 0)) {
    if (a === 0 && b > 0) { bWins = true; deltaPct = Infinity; }
    else if (b === 0 && a > 0) { aWins = true; deltaPct = Infinity; }
    else if (a !== b) {
      const base = Math.min(a, b);
      deltaPct = Math.abs(a - b) / base;
      if (a > b) aWins = true;
      if (b > a) bWins = true;
    }
  }
  function cellStyle(wins) {
    return {
      padding: '12px 18px', textAlign: 'center',
      fontSize: 16, fontWeight: 700,
      color: pending ? 'var(--muted-2)' : (wins ? '#00ff85' : 'var(--ink-2)'),
      fontVariantNumeric: 'tabular-nums',
    };
  }
  function deltaLabel(wins) {
    if (!wins || deltaPct == null) return null;
    if (deltaPct === Infinity) return <span className="mono" style={{ fontSize: 10, color: '#00ff85', marginLeft: 6 }}>WIN</span>;
    return <span className="mono" style={{ fontSize: 10, color: '#00ff85', marginLeft: 6 }}>+{(deltaPct * 100).toFixed(0)}%</span>;
  }
  return (
    <div style={{
      display: 'grid', gridTemplateColumns: '1fr 1fr 1fr',
      borderBottom: '1px solid #1a1a1a',
      alignItems: 'center',
    }}>
      <div style={{ padding: '12px 18px', fontSize: 13, color: 'var(--ink-2)' }}>{label}</div>
      <div style={cellStyle(aWins)}>{fmt(a)}{deltaLabel(aWins)}</div>
      <div style={cellStyle(bWins)}>{fmt(b)}{deltaLabel(bWins)}</div>
    </div>
  );
}

function DateRangeBar({ rangeId, onSelect, customStart, setCustomStart, customEnd, setCustomEnd, label }) {
  return (
    <div style={{
      display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap',
      margin: '0 0 24px'
    }}>
      {DATE_RANGES.map(r => (
        <button
          key={r.id}
          onClick={() => onSelect(r.id)}
          style={rangeId === r.id ? rangePillActive : rangePill}
        >{r.label}</button>
      ))}
      <button
        onClick={() => onSelect('custom')}
        style={rangeId === 'custom' ? rangePillActive : rangePill}
      >Custom</button>
      {rangeId === 'custom' && (
        <div style={{ display: 'inline-flex', alignItems: 'center', gap: 6, marginLeft: 6 }}>
          <input type="date" value={customStart} onChange={e => setCustomStart(e.target.value)} style={dateInput} />
          <span style={{ color: 'var(--muted)' }}>→</span>
          <input type="date" value={customEnd} onChange={e => setCustomEnd(e.target.value)} style={dateInput} />
        </div>
      )}
      <span className="mono" style={{ marginLeft: 'auto', fontSize: 11, letterSpacing: '0.16em', color: 'var(--muted)' }}>
        {label}
      </span>
    </div>
  );
}

// ─────────────────────────────────────────────
// KPI Panel — top-level metrics with period-over-period deltas
// ─────────────────────────────────────────────
// ─────────────────────────────────────────────
// Shared metric computation helpers
// All video-related helpers accept an optional `videoFilter` (a video_id).
// When provided, only events tagged with that video_id are counted.
// ─────────────────────────────────────────────
function uniqueSessionsForPage(events, page) {
  return new Set(
    events.filter(e => e.event_type === 'page_view' && (page ? e.page === page : true))
      .map(e => e.session_id)
  ).size;
}
function uniqueSessionsAny(events) {
  return new Set(events.filter(e => e.event_type === 'page_view').map(e => e.session_id)).size;
}
function matchesVideo(e, videoFilter) {
  if (!videoFilter) return true;
  return e.event_data && e.event_data.video_id === videoFilter;
}
// Sessions that reached at least `threshold` % on the post-reg video.
// If `videoFilter` is provided, only counts sessions that reached that threshold on THAT specific video.
function videoReachedSessions(events, threshold, videoFilter) {
  const sessionMaxPct = {};
  events.filter(e => e.event_type === 'video_progress' && matchesVideo(e, videoFilter)).forEach(e => {
    const p = e.event_data && e.event_data.percent;
    if (typeof p === 'number') {
      sessionMaxPct[e.session_id] = Math.max(sessionMaxPct[e.session_id] || 0, p);
    }
  });
  return Object.values(sessionMaxPct).filter(p => p >= threshold).length;
}
function calendarClicksTotal(events) {
  return events.filter(e => e.event_type === 'calendar_click').length;
}
function calendarClicksBySource(events, source) {
  return events.filter(e =>
    e.event_type === 'calendar_click'
    && e.event_data
    && e.event_data.source === source
  ).length;
}
function videoPlays(events, videoFilter) {
  return events.filter(e => e.event_type === 'video_play' && matchesVideo(e, videoFilter)).length;
}

// Hook: loads all videos (active + archived) for a given placement, for the filter dropdown
function useAllVideosForPlacement(placement) {
  const [videos, setVideos] = useState([]);
  useEffect(() => {
    let cancelled = false;
    api('/videos?placement=eq.' + encodeURIComponent(placement) + '&order=created_at.desc')
      .then(rows => { if (!cancelled) setVideos(rows); })
      .catch(() => {});
    return () => { cancelled = true; };
  }, [placement]);
  return videos;
}

// Compact dropdown for filtering by a specific video.
// `value = null` means "all videos" (no filter applied).
function VideoFilter({ value, onChange, placement }) {
  const videos = useAllVideosForPlacement(placement);
  if (videos.length <= 1) return null; // No filter needed if only one video exists
  return (
    <div style={{ display: 'inline-flex', alignItems: 'center', gap: 8 }}>
      <span className="mono" style={{ fontSize: 10, letterSpacing: '0.18em', color: 'var(--muted)', textTransform: 'uppercase', fontWeight: 600 }}>
        Video
      </span>
      <select
        value={value || ''}
        onChange={e => onChange(e.target.value || null)}
        style={{
          padding: '7px 28px 7px 11px',
          background: 'var(--bg-3)',
          border: '1px solid var(--line-2)',
          borderRadius: 7,
          color: 'var(--ink)',
          fontSize: 12, fontWeight: 500,
          fontFamily: 'inherit',
          cursor: 'pointer',
          appearance: 'none',
          backgroundImage: "url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath fill='%238a8a8a' d='M0 0l5 6 5-6z'/%3E%3C/svg%3E\")",
          backgroundRepeat: 'no-repeat',
          backgroundPosition: 'right 10px center',
        }}
      >
        <option value="">All post-reg videos</option>
        {videos.map(v => (
          <option key={v.id} value={v.id}>
            {v.title}{v.status === 'archived' ? ' (archived)' : ''}
          </option>
        ))}
      </select>
    </div>
  );
}

function KpiPanel({ current, prior, scope, videoFilter }) {
  function metricSet(group) {
    const allRegs = group.registrants;
    const completes = allRegs.filter(r => r.status === 'complete').length;
    const partials = allRegs.filter(r => r.status === 'partial').length;
    const landingVisitors = uniqueSessionsForPage(group.events, 'landing');
    const postregVisitors = uniqueSessionsForPage(group.events, 'post_reg');
    const allVisitors = uniqueSessionsAny(group.events);
    return {
      visitors_all: allVisitors,
      visitors_landing: landingVisitors,
      visitors_postreg: postregVisitors,
      registrations: completes,
      partials,
      conv_rate: landingVisitors > 0 ? (completes / landingVisitors) * 100 : 0,
      sms_replies: allRegs.filter(r => r.sms_responded).length,
      calendar_clicks: calendarClicksTotal(group.events),
      video_plays: videoPlays(group.events, videoFilter),
      video_25: videoReachedSessions(group.events, 25, videoFilter),
      video_50: videoReachedSessions(group.events, 50, videoFilter),
      video_90: videoReachedSessions(group.events, 90, videoFilter),
    };
  }
  const cur = metricSet(current);
  const prv = metricSet(prior);

  // Scope determines which cards to show
  let cards;
  if (scope === 'landing') {
    cards = [
      { label: 'Landing visitors', value: cur.visitors_landing, prior: prv.visitors_landing, kind: 'count' },
      { label: 'Registrations',    value: cur.registrations,    prior: prv.registrations,    kind: 'count', accent: true },
      { label: 'Partial leads',    value: cur.partials,         prior: prv.partials,         kind: 'count' },
      { label: 'Conversion rate',  value: cur.conv_rate,        prior: prv.conv_rate,        kind: 'percent' },
    ];
  } else if (scope === 'post_reg') {
    cards = [
      { label: 'Post-reg visitors',           value: cur.visitors_postreg, prior: prv.visitors_postreg, kind: 'count' },
      { label: 'Post-reg video plays',        value: cur.video_plays,      prior: prv.video_plays,      kind: 'count' },
      { label: 'Post-reg video 25%+ reached', value: cur.video_25,         prior: prv.video_25,         kind: 'count' },
      { label: 'Post-reg video 50%+ reached', value: cur.video_50,         prior: prv.video_50,         kind: 'count' },
      { label: 'Post-reg video 90%+ reached', value: cur.video_90,         prior: prv.video_90,         kind: 'count', accent: true },
      { label: 'Calendar clicks',             value: cur.calendar_clicks,  prior: prv.calendar_clicks,  kind: 'count' },
      { label: 'SMS replies',                 value: cur.sms_replies,      prior: prv.sms_replies,      kind: 'count' },
    ];
  } else {
    // All pages — the full picture
    cards = [
      { label: 'Visitors',                    value: cur.visitors_all,     prior: prv.visitors_all,     kind: 'count' },
      { label: 'Registrations',               value: cur.registrations,    prior: prv.registrations,    kind: 'count', accent: true },
      { label: 'Partial leads',               value: cur.partials,         prior: prv.partials,         kind: 'count' },
      { label: 'Conversion rate',             value: cur.conv_rate,        prior: prv.conv_rate,        kind: 'percent' },
      { label: 'Post-reg video 25%+ reached', value: cur.video_25,         prior: prv.video_25,         kind: 'count' },
      { label: 'Post-reg video 50%+ reached', value: cur.video_50,         prior: prv.video_50,         kind: 'count' },
      { label: 'Post-reg video 90%+ reached', value: cur.video_90,         prior: prv.video_90,         kind: 'count' },
      { label: 'Calendar clicks',             value: cur.calendar_clicks,  prior: prv.calendar_clicks,  kind: 'count' },
      { label: 'SMS replies',                 value: cur.sms_replies,      prior: prv.sms_replies,      kind: 'count' },
    ];
  }

  return (
    <div style={{
      display: 'grid',
      gridTemplateColumns: 'repeat(auto-fit, minmax(170px, 1fr))',
      gap: 10,
      marginBottom: 28
    }}>
      {cards.map(c => <KpiCard key={c.label} {...c} />)}
    </div>
  );
}

function KpiCard({ label, value, prior, kind, accent }) {
  const delta = prior > 0 ? ((value - prior) / prior) * 100 : (value > 0 ? 100 : 0);
  const arrow = delta > 0.5 ? '▲' : delta < -0.5 ? '▼' : '·';
  const color = delta > 0.5 ? '#00ff85' : delta < -0.5 ? '#fca5a5' : 'var(--muted)';
  const display = kind === 'percent'
    ? (value.toFixed(1) + '%')
    : value.toLocaleString();
  return (
    <div style={{
      background: accent ? 'linear-gradient(135deg, rgba(168,85,247,0.06), rgba(168,85,247,0.02))' : 'var(--card)',
      border: '1px solid ' + (accent ? 'rgba(168,85,247,0.35)' : 'var(--line)'),
      borderRadius: 10,
      padding: '14px 16px'
    }}>
      <div className="mono" style={{
        fontSize: 9, letterSpacing: '0.22em', textTransform: 'uppercase',
        color: 'var(--muted)', fontWeight: 600, marginBottom: 8
      }}>{label}</div>
      <div style={{ fontSize: 26, fontWeight: 700, letterSpacing: '-0.02em', color: accent ? 'var(--accent)' : 'var(--ink)', lineHeight: 1 }}>
        {display}
      </div>
      <div className="mono" style={{ fontSize: 11, color, marginTop: 6, letterSpacing: '0.04em' }}>
        {arrow} {Math.abs(delta).toFixed(0)}%
        <span style={{ color: 'var(--muted-2)' }}> vs prior</span>
      </div>
    </div>
  );
}

// ─────────────────────────────────────────────
// Funnel Visualization — stepped bar with drop-off %
// ─────────────────────────────────────────────
function FunnelViz({ current, scope, videoFilter }) {
  const allRegs = current.registrants;
  const completed = allRegs.filter(r => r.status === 'complete').length;
  const calClicks = new Set(current.events.filter(e => e.event_type === 'calendar_click').map(e => e.session_id)).size;
  const sms = allRegs.filter(r => r.sms_responded).length;
  const postregVisitors = uniqueSessionsForPage(current.events, 'post_reg');
  const videoPlays_ = videoPlays(current.events, videoFilter);
  const video25 = videoReachedSessions(current.events, 25, videoFilter);
  const video50 = videoReachedSessions(current.events, 50, videoFilter);
  const video90 = videoReachedSessions(current.events, 90, videoFilter);

  let stages;
  if (scope === 'landing') {
    const visitorSessions = uniqueSessionsForPage(current.events, 'landing');
    const emailCaptured = allRegs.length;
    const qualifying = allRegs.filter(r => ['qualifying_in_progress', 'phone_in_progress'].includes(r.dropoff_step) || r.status === 'complete').length;
    const phone = allRegs.filter(r => r.dropoff_step === 'phone_in_progress' || r.status === 'complete').length;
    stages = [
      { label: 'Landing visitors',  value: visitorSessions },
      { label: 'Email captured',    value: emailCaptured },
      { label: 'Qualifying step',   value: qualifying },
      { label: 'Phone step',        value: phone },
      { label: 'Completed',         value: completed, accent: true },
    ];
  } else if (scope === 'post_reg') {
    stages = [
      { label: 'Post-reg visitors',  value: postregVisitors },
      { label: 'Video played',       value: videoPlays_ },
      { label: 'Video 25%+ reached', value: video25 },
      { label: 'Video 50%+ reached', value: video50 },
      { label: 'Video 90%+ reached', value: video90, accent: true },
      { label: 'Calendar added',     value: calClicks },
      { label: 'SMS replied',        value: sms },
    ];
  } else {
    // All pages — the full end-to-end journey from landing → SMS reply
    const visitorSessions = uniqueSessionsForPage(current.events, 'landing');
    const emailCaptured = allRegs.length;
    const qualifying = allRegs.filter(r => ['qualifying_in_progress', 'phone_in_progress'].includes(r.dropoff_step) || r.status === 'complete').length;
    const phone = allRegs.filter(r => r.dropoff_step === 'phone_in_progress' || r.status === 'complete').length;
    stages = [
      { label: 'Landing visitors',         value: visitorSessions },
      { label: 'Email captured',           value: emailCaptured },
      { label: 'Qualifying step',          value: qualifying },
      { label: 'Phone step',               value: phone },
      { label: 'Completed registration',   value: completed, accent: true },
      { label: 'Post-reg visitors',        value: postregVisitors, divider: true },
      { label: 'Post-reg video played',    value: videoPlays_ },
      { label: 'Post-reg video 25%+',      value: video25 },
      { label: 'Post-reg video 50%+',      value: video50 },
      { label: 'Post-reg video 90%+',      value: video90 },
      { label: 'Calendar added',           value: calClicks },
      { label: 'SMS replied',              value: sms },
    ];
  }
  const top = Math.max(stages[0].value, 1);

  // Find biggest drop-off step to highlight
  let biggestDropIdx = -1;
  let biggestDropPct = 0;
  for (let i = 1; i < stages.length; i++) {
    const prev = stages[i-1].value;
    const cur = stages[i].value;
    if (prev > 0) {
      const dropPct = ((prev - cur) / prev) * 100;
      if (dropPct > biggestDropPct) {
        biggestDropPct = dropPct;
        biggestDropIdx = i;
      }
    }
  }

  return (
    <div style={{
      background: 'var(--card)', border: '1px solid var(--line)',
      borderRadius: 10, padding: 22, marginBottom: 28
    }}>
      <div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', marginBottom: 18, gap: 16, flexWrap: 'wrap' }}>
        <div>
          <div className="mono" style={{ fontSize: 10, letterSpacing: '0.2em', textTransform: 'uppercase', color: 'var(--muted)', fontWeight: 600, marginBottom: 4 }}>
            Funnel
          </div>
          <div style={{ fontSize: 18, fontWeight: 600 }}>Where people drop off</div>
        </div>
        {biggestDropIdx > 0 && stages[biggestDropIdx].value > 0 && (
          <div className="mono" style={{
            fontSize: 11, letterSpacing: '0.12em',
            padding: '6px 10px', borderRadius: 6,
            background: 'rgba(251,191,36,0.10)', border: '1px solid rgba(251,191,36,0.35)',
            color: '#fbbf24'
          }}>
            Biggest leak: {stages[biggestDropIdx - 1].label} → {stages[biggestDropIdx].label} ({biggestDropPct.toFixed(0)}%)
          </div>
        )}
      </div>

      <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
        {stages.map((s, i) => {
          const prevValue = i > 0 ? stages[i-1].value : null;
          const stepConversion = (i > 0 && prevValue > 0) ? (s.value / prevValue) * 100 : null;
          // Bar width = step-to-step retention %. First stage = full width (no previous to compare).
          // This makes the bar visually match the percentage shown to its right.
          // If a step has 0 value (no one made it), bar is at minimum width (2%) so the row stays visible.
          const widthPct = i === 0
            ? 100
            : (stepConversion !== null ? Math.max(2, stepConversion) : 2);
          const isLeak = i === biggestDropIdx;
          return (
            <React.Fragment key={s.label}>
              {s.divider && (
                <div style={{
                  display: 'flex', alignItems: 'center', gap: 10,
                  margin: '6px 0 2px',
                  fontSize: 10, letterSpacing: '0.22em', textTransform: 'uppercase',
                  color: 'var(--muted-2)', fontFamily: 'Geist Mono, ui-monospace, monospace',
                  fontWeight: 600
                }}>
                  <span style={{ flex: 1, height: 1, background: 'var(--line)' }} />
                  <span>↓ Post-registration page ↓</span>
                  <span style={{ flex: 1, height: 1, background: 'var(--line)' }} />
                </div>
              )}
              <div style={{ display: 'grid', gridTemplateColumns: '200px 1fr 100px 90px', alignItems: 'center', gap: 12 }}>
              <div style={{ fontSize: 13, color: s.accent ? 'var(--accent)' : 'var(--ink-2)', fontWeight: s.accent ? 600 : 500 }}>
                {s.label}
              </div>
              <div style={{ background: '#0d0d0d', height: 22, borderRadius: 4, position: 'relative', overflow: 'hidden' }}>
                <div style={{
                  width: widthPct + '%', height: '100%',
                  background: s.accent
                    ? 'linear-gradient(90deg, #7c3aed, #a855f7, #d946ef)'   // headline stage — full brand gradient
                    : (isLeak
                        ? 'linear-gradient(90deg, #fbbf24cc, #fbbf2466)'    // biggest-leak warning — yellow
                        : 'linear-gradient(90deg, #7c3aedaa, #a855f766)'),  // every other stage — muted purple
                  borderRadius: 4, transition: 'width 400ms cubic-bezier(.2,.8,.2,1)'
                }} />
              </div>
              <div className="mono" style={{ fontSize: 13, color: 'var(--ink)', fontWeight: 600, textAlign: 'right', fontVariantNumeric: 'tabular-nums' }}>
                {s.value.toLocaleString()}
              </div>
              <div className="mono" style={{ fontSize: 11, letterSpacing: '0.04em', color: stepConversion === null ? 'var(--muted-2)' : (stepConversion >= 70 ? '#00ff85' : (stepConversion >= 40 ? 'var(--muted)' : '#fca5a5')), textAlign: 'right' }}>
                {stepConversion === null ? '—' : stepConversion.toFixed(1) + '%'}
              </div>
            </div>
            </React.Fragment>
          );
        })}
      </div>
    </div>
  );
}

// ─────────────────────────────────────────────
// Spreadsheet view — all registrant rows in the current window
// (this is the daily/historical scrollable table)
// ─────────────────────────────────────────────
function DataTable({ registrants, events }) {
  const [search, setSearch] = useState('');
  const filtered = useMemo(() => {
    if (!search.trim()) return registrants;
    const q = search.toLowerCase();
    return registrants.filter(r =>
      JSON.stringify(r).toLowerCase().includes(q)
    );
  }, [registrants, search]);

  function exportCsv() {
    if (filtered.length === 0) return;
    const cols = ['registered_at','status','email','phone','qualifying_answer','answers','utm_source','utm_medium','utm_campaign','utm_content','fbclid','gclid','ip_address','dropoff_step','sms_responded','last_seen_at','completed_at','event_id'];
    const header = cols.join(',');
    const lines = filtered.map(r => cols.map(c => {
      let v = r[c];
      if (c === 'answers') {
        const list = Array.isArray(r.answers) ? r.answers : [];
        v = list.map(a => (a.prompt || a.question_id) + ': ' + (a.label != null ? a.label : a.value)).join(' | ');
      }
      if (v === null || v === undefined) return '';
      const s = String(v).replace(/"/g, '""');
      return /[",\n]/.test(s) ? '"' + s + '"' : s;
    }).join(','));
    const csv = header + '\n' + lines.join('\n');
    const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = 'registrants_' + new Date().toISOString().slice(0, 10) + '.csv';
    a.click();
    URL.revokeObjectURL(url);
  }

  return (
    <div style={{ marginBottom: 24 }}>
      <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12, marginBottom: 12, flexWrap: 'wrap' }}>
        <div>
          <div className="mono" style={{ fontSize: 10, letterSpacing: '0.2em', textTransform: 'uppercase', color: 'var(--muted)', fontWeight: 600, marginBottom: 4 }}>
            Data table
          </div>
          <div style={{ fontSize: 18, fontWeight: 600 }}>All registrants in window</div>
        </div>
        <div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
          <input
            value={search}
            onChange={e => setSearch(e.target.value)}
            placeholder="Search any field…"
            style={{ ...inputStyle, width: 280 }}
          />
          <button onClick={exportCsv} style={btnGhost}>Export CSV</button>
        </div>
      </div>

      <div style={{ background: 'var(--card)', border: '1px solid var(--line)', borderRadius: 10, overflow: 'hidden' }}>
        <div style={{ overflowX: 'auto', maxHeight: 540, overflowY: 'auto' }}>
          <table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 12 }}>
            <thead style={{ position: 'sticky', top: 0, zIndex: 1 }}>
              <tr style={{ background: '#0d0d0d', borderBottom: '1px solid var(--line)' }}>
                <Th>When</Th>
                <Th>Status</Th>
                <Th>Email</Th>
                <Th>Phone</Th>
                <Th>Answer</Th>
                <Th>Source</Th>
                <Th>Campaign</Th>
                <Th>IP</Th>
                <Th>Webinar</Th>
                <Th>SMS</Th>
              </tr>
            </thead>
            <tbody>
              {filtered.length === 0 ? (
                <tr><td colSpan={10} style={{ padding: 32, textAlign: 'center', color: 'var(--muted)' }}>No registrants in this window.</td></tr>
              ) : filtered.map(r => (
                <tr key={r.id} style={{ borderBottom: '1px solid #1a1a1a' }}>
                  <Td><span className="mono" style={{ fontSize: 11, color: 'var(--muted)' }}>{formatRegisteredAt(r.registered_at)}</span></Td>
                  <Td><StatusBadge status={r.status} dropoffStep={r.dropoff_step} /></Td>
                  <Td><span style={{ color: 'var(--ink)' }}>{r.email}</span></Td>
                  <Td><span className="mono" style={{ fontSize: 11 }}>{r.phone || '—'}</span></Td>
                  <Td>{r.qualifying_answer || '—'}</Td>
                  <Td>
                    {r.utm_source
                      ? <span style={{ textTransform: 'capitalize' }}>{r.utm_source}</span>
                      : (r.fbclid ? <span className="mono" style={{ fontSize: 10, color: '#60a5fa' }}>meta·click</span>
                        : (r.gclid ? <span className="mono" style={{ fontSize: 10, color: '#f87171' }}>google·click</span>
                          : <span className="mono" style={{ fontSize: 10, color: 'var(--muted-2)' }}>DIRECT</span>))}
                  </Td>
                  <Td><span className="mono" style={{ fontSize: 11, color: 'var(--muted-2)' }}>{r.utm_campaign || '—'}</span></Td>
                  <Td><span className="mono" style={{ fontSize: 11, color: 'var(--muted-2)' }}>{r.ip_address || '—'}</span></Td>
                  <Td>
                    <span style={{ color: 'var(--muted)', fontSize: 11 }}>
                      {r.webinar_events ? r.webinar_events.title.replace('Newsletter Accelerator Webinar - ', '') : '—'}
                    </span>
                  </Td>
                  <Td>
                    {r.sms_responded
                      ? <span className="mono" style={{ fontSize: 9, letterSpacing: '0.18em', padding: '2px 6px', borderRadius: 3, background: 'rgba(0,255,133,0.12)', color: '#00ff85', fontWeight: 700 }}>YES</span>
                      : <span style={{ color: 'var(--muted-2)' }}>—</span>}
                  </Td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>
        <div className="mono" style={{
          padding: '10px 14px', borderTop: '1px solid var(--line)',
          fontSize: 10, letterSpacing: '0.16em', textTransform: 'uppercase',
          color: 'var(--muted)', display: 'flex', justifyContent: 'space-between'
        }}>
          <span>{filtered.length} {filtered.length === 1 ? 'row' : 'rows'}</span>
          <span>Showing all matching</span>
        </div>
      </div>
    </div>
  );
}

// ─────────────────────────────────────────────
// Deep Dive — every measurable signal per page
// ─────────────────────────────────────────────
function DeepDiveToggle({ open, onToggle, scope }) {
  const label = scope === 'landing' ? 'landing page'
              : scope === 'post_reg' ? 'post-registration page'
              : 'full funnel';
  return (
    <div style={{ display: 'flex', justifyContent: 'flex-end', marginBottom: 16 }}>
      <button onClick={onToggle} style={{
        display: 'inline-flex', alignItems: 'center', gap: 8,
        padding: '8px 14px',
        background: open ? 'rgba(168,85,247,0.10)' : 'transparent',
        border: '1px solid ' + (open ? 'var(--accent)' : 'var(--line-2)'),
        borderRadius: 6,
        color: open ? 'var(--accent)' : 'var(--ink-2)',
        fontSize: 13, fontWeight: 500, fontFamily: 'inherit',
        cursor: 'pointer'
      }}>
        <span>{open ? '▾' : '▸'}</span>
        <span>{open ? 'Hide' : 'Show'} deep dive · {label}</span>
      </button>
    </div>
  );
}

function DeepDive({ current, scope, videoFilter }) {
  if (scope === 'landing') return <DeepDiveLanding current={current} />;
  if (scope === 'post_reg') return <DeepDivePostReg current={current} videoFilter={videoFilter} />;
  return (
    <>
      <DeepDiveLanding current={current} />
      <DeepDivePostReg current={current} videoFilter={videoFilter} />
    </>
  );
}

function DeepDiveLanding({ current }) {
  const visitors = uniqueSessionsForPage(current.events, 'landing');
  const allRegs = current.registrants;
  const partials = allRegs.filter(r => r.status === 'partial');

  // Dropoff step distribution
  const dropoffCounts = {
    email_entered: 0,
    qualifying_in_progress: 0,
    phone_in_progress: 0,
  };
  partials.forEach(p => {
    if (p.dropoff_step && dropoffCounts[p.dropoff_step] !== undefined) {
      dropoffCounts[p.dropoff_step]++;
    }
  });

  // Device split from page_view events
  const deviceCounts = {};
  current.events.filter(e => e.event_type === 'page_view' && e.page === 'landing').forEach(e => {
    const dt = e.event_data && e.event_data.device_type;
    if (dt) deviceCounts[dt] = (deviceCounts[dt] || 0) + 1;
  });

  // Top referrers
  const referrerCounts = {};
  current.events.filter(e => e.event_type === 'page_view' && e.page === 'landing').forEach(e => {
    const r = e.event_data && e.event_data.referrer;
    if (r) {
      try { const h = new URL(r).hostname.replace(/^www\./, ''); referrerCounts[h] = (referrerCounts[h] || 0) + 1; }
      catch {}
    }
  });

  return (
    <DeepDiveCard title="Landing page · full breakdown">
      <Subdive title="Top of funnel">
        <Stat label="Total page views" value={current.events.filter(e => e.event_type === 'page_view' && e.page === 'landing').length} />
        <Stat label="Unique visitors" value={visitors} />
        <Stat label="Returning sessions" value={Math.max(0, current.events.filter(e => e.event_type === 'page_view' && e.page === 'landing').length - visitors)} />
      </Subdive>

      <Subdive title="Dropoff distribution (partials)">
        <Stat label="Bailed at email" value={dropoffCounts.email_entered} pct={pctOf(dropoffCounts.email_entered, partials.length)} />
        <Stat label="Bailed at qualifying" value={dropoffCounts.qualifying_in_progress} pct={pctOf(dropoffCounts.qualifying_in_progress, partials.length)} />
        <Stat label="Bailed at phone" value={dropoffCounts.phone_in_progress} pct={pctOf(dropoffCounts.phone_in_progress, partials.length)} />
      </Subdive>

      <Subdive title="Device split">
        {Object.keys(deviceCounts).length === 0
          ? <Empty label="No device data yet." />
          : Object.entries(deviceCounts).sort((a,b) => b[1]-a[1]).map(([k,v]) =>
              <Stat key={k} label={k} value={v} pct={pctOf(v, visitors)} />)}
      </Subdive>

      <Subdive title="Top referrers">
        {Object.keys(referrerCounts).length === 0
          ? <Empty label="No referrer data yet." />
          : Object.entries(referrerCounts).sort((a,b) => b[1]-a[1]).slice(0, 8).map(([k,v]) =>
              <Stat key={k} label={k} value={v} />)}
      </Subdive>
    </DeepDiveCard>
  );
}

function DeepDivePostReg({ current, videoFilter }) {
  const visitors = uniqueSessionsForPage(current.events, 'post_reg');
  const events = current.events;

  // Every 5% milestone individually — filtered by video_id when set
  const milestones = [];
  for (let m = 5; m <= 100; m += 5) {
    milestones.push({ pct: m, sessions: videoReachedSessions(events, m, videoFilter) });
  }
  const videoPlays_ = videoPlays(events, videoFilter);

  // Calendar clicks by button source
  const calBySource = {
    hero: calendarClicksBySource(events, 'hero'),
    final_cta: calendarClicksBySource(events, 'final_cta'),
    sticky: calendarClicksBySource(events, 'sticky'),
  };
  const totalCal = calBySource.hero + calBySource.final_cta + calBySource.sticky;

  // Device split for post-reg
  const deviceCounts = {};
  events.filter(e => e.event_type === 'page_view' && e.page === 'post_reg').forEach(e => {
    const dt = e.event_data && e.event_data.device_type;
    if (dt) deviceCounts[dt] = (deviceCounts[dt] || 0) + 1;
  });

  return (
    <DeepDiveCard title="Post-registration page · full breakdown">
      <Subdive title="Page traffic">
        <Stat label="Total page views" value={events.filter(e => e.event_type === 'page_view' && e.page === 'post_reg').length} />
        <Stat label="Unique visitors" value={visitors} />
        <Stat label="Video plays" value={videoPlays_} pct={pctOf(videoPlays_, visitors)} suffix=" of visitors" />
      </Subdive>

      <Subdive title="Post-reg video — every 5% milestone (sessions reaching ≥ X%)">
        <div style={{
          display: 'grid',
          gridTemplateColumns: 'repeat(auto-fit, minmax(120px, 1fr))',
          gap: 8
        }}>
          {milestones.map(m => (
            <MilestoneTile key={m.pct} pct={m.pct} value={m.sessions} totalVisitors={visitors} />
          ))}
        </div>
      </Subdive>

      <Subdive title="Calendar clicks · by button">
        <Stat label="Hero button"        value={calBySource.hero}      pct={pctOf(calBySource.hero, totalCal)} suffix=" of clicks" />
        <Stat label="Final CTA button"   value={calBySource.final_cta} pct={pctOf(calBySource.final_cta, totalCal)} suffix=" of clicks" />
        <Stat label="Mobile sticky bar"  value={calBySource.sticky}    pct={pctOf(calBySource.sticky, totalCal)} suffix=" of clicks" />
        <Stat label="Total clicks"       value={totalCal} />
      </Subdive>

      <Subdive title="Device split">
        {Object.keys(deviceCounts).length === 0
          ? <Empty label="No device data yet." />
          : Object.entries(deviceCounts).sort((a,b) => b[1]-a[1]).map(([k,v]) =>
              <Stat key={k} label={k} value={v} pct={pctOf(v, visitors)} />)}
      </Subdive>
    </DeepDiveCard>
  );
}

function DeepDiveCard({ title, children }) {
  return (
    <div style={{
      background: 'var(--card)', border: '1px solid var(--line)',
      borderRadius: 10, padding: 22, marginBottom: 28
    }}>
      <div className="mono" style={{ fontSize: 10, letterSpacing: '0.2em', textTransform: 'uppercase', color: 'var(--muted)', fontWeight: 600, marginBottom: 4 }}>
        Deep dive
      </div>
      <div style={{ fontSize: 18, fontWeight: 600, marginBottom: 18 }}>{title}</div>
      {children}
    </div>
  );
}
function Subdive({ title, children }) {
  return (
    <div style={{ marginBottom: 22 }}>
      <div className="mono" style={{
        fontSize: 10, letterSpacing: '0.2em', textTransform: 'uppercase',
        color: 'var(--accent)', fontWeight: 600, marginBottom: 10
      }}>{title}</div>
      <div style={{ display: 'flex', flexWrap: 'wrap', gap: 22 }}>
        {children}
      </div>
    </div>
  );
}
function Stat({ label, value, pct, suffix }) {
  return (
    <div style={{ minWidth: 140 }}>
      <div className="mono" style={{ fontSize: 9, letterSpacing: '0.18em', textTransform: 'uppercase', color: 'var(--muted)', fontWeight: 600, marginBottom: 4 }}>
        {label}
      </div>
      <div style={{ fontSize: 20, fontWeight: 600, lineHeight: 1.1, fontVariantNumeric: 'tabular-nums', textTransform: 'capitalize' }}>
        {value.toLocaleString()}
        {pct !== undefined && pct !== null && (
          <span className="mono" style={{ fontSize: 12, color: 'var(--muted)', marginLeft: 8, fontWeight: 500 }}>
            {pct.toFixed(0)}%{suffix || ''}
          </span>
        )}
      </div>
    </div>
  );
}
function MilestoneTile({ pct, value, totalVisitors }) {
  const rate = totalVisitors > 0 ? (value / totalVisitors) * 100 : 0;
  return (
    <div style={{
      background: '#0d0d0d', border: '1px solid var(--line-2)',
      borderRadius: 6, padding: '10px 12px'
    }}>
      <div className="mono" style={{ fontSize: 10, letterSpacing: '0.14em', color: 'var(--accent)', fontWeight: 600 }}>
        ≥{pct}%
      </div>
      <div style={{ fontSize: 18, fontWeight: 600, fontVariantNumeric: 'tabular-nums', marginTop: 2 }}>
        {value}
      </div>
      <div className="mono" style={{ fontSize: 10, color: 'var(--muted-2)', marginTop: 2 }}>
        {rate.toFixed(0)}% of visitors
      </div>
    </div>
  );
}

function pctOf(part, whole) {
  if (!whole || whole === 0) return 0;
  return (part / whole) * 100;
}

// ─────────────────────────────────────────────
// Time-series chart — daily registrations, current vs prior period overlay
// ─────────────────────────────────────────────
function TimeSeriesChart({ current, prior, start, end, prevStart, prevEnd }) {
  const [hoverDayIdx, setHoverDayIdx] = useState(null);

  const { days, curSeries, prvSeries, maxY, peakDay } = useMemo(() => {
    const dayMs = 86400000;
    const numDays = Math.max(1, Math.ceil((end.getTime() - start.getTime()) / dayMs));
    const days = [];
    const curSeries = [];
    const prvSeries = [];
    let maxY = 0;
    let peakDay = null;
    for (let i = 0; i < numDays; i++) {
      const dStart = new Date(start.getTime() + i * dayMs);
      const dEnd = new Date(dStart.getTime() + dayMs);
      const prvDStart = new Date(prevStart.getTime() + i * dayMs);
      const prvDEnd = new Date(prvDStart.getTime() + dayMs);
      const curCount = current.registrants.filter(r => {
        const t = new Date(r.registered_at).getTime();
        return t >= dStart.getTime() && t < dEnd.getTime();
      }).length;
      const prvCount = prior.registrants.filter(r => {
        const t = new Date(r.registered_at).getTime();
        return t >= prvDStart.getTime() && t < prvDEnd.getTime();
      }).length;
      days.push(dStart);
      curSeries.push(curCount);
      prvSeries.push(prvCount);
      maxY = Math.max(maxY, curCount, prvCount);
      if (curCount > (peakDay ? peakDay.count : 0)) peakDay = { date: dStart, count: curCount };
    }
    return { days, curSeries, prvSeries, maxY: Math.max(maxY, 4), peakDay };
  }, [current, prior, start, end, prevStart, prevEnd]);

  // SVG layout
  const W = 1000;
  const H = 220;
  const PAD = { left: 36, right: 18, top: 18, bottom: 30 };
  const innerW = W - PAD.left - PAD.right;
  const innerH = H - PAD.top - PAD.bottom;
  const stepX = days.length > 1 ? innerW / (days.length - 1) : 0;

  function x(i) { return PAD.left + i * stepX; }
  function y(v) { return PAD.top + innerH - (v / maxY) * innerH; }

  function buildPath(series) {
    if (series.length === 0) return '';
    return series.map((v, i) => (i === 0 ? 'M' : 'L') + x(i).toFixed(1) + ',' + y(v).toFixed(1)).join(' ');
  }

  const curPath = buildPath(curSeries);
  const prvPath = buildPath(prvSeries);

  // Build smooth area for current period
  const areaPath = curSeries.length > 0
    ? curPath + ' L ' + x(curSeries.length - 1).toFixed(1) + ',' + (PAD.top + innerH) + ' L ' + x(0).toFixed(1) + ',' + (PAD.top + innerH) + ' Z'
    : '';

  // Y-axis ticks (4 evenly spaced)
  const yTicks = [];
  for (let i = 0; i <= 4; i++) {
    const v = Math.round((maxY / 4) * i);
    yTicks.push({ v, y: y(v) });
  }

  // X-axis labels — pick ~6 evenly distributed days
  const xLabels = [];
  const maxLabels = Math.min(7, days.length);
  for (let i = 0; i < maxLabels; i++) {
    const idx = Math.round((i / Math.max(1, maxLabels - 1)) * (days.length - 1));
    xLabels.push({ idx, date: days[idx] });
  }
  const fmtTick = d => new Intl.DateTimeFormat('en-US', { month: 'short', day: 'numeric' }).format(d);

  return (
    <div style={{
      background: 'var(--card)', border: '1px solid var(--line)',
      borderRadius: 10, padding: 22, marginBottom: 28
    }}>
      <div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', marginBottom: 18, gap: 16, flexWrap: 'wrap' }}>
        <div>
          <div className="mono" style={{ fontSize: 10, letterSpacing: '0.2em', textTransform: 'uppercase', color: 'var(--muted)', fontWeight: 600, marginBottom: 4 }}>
            Trend
          </div>
          <div style={{ fontSize: 18, fontWeight: 600 }}>Daily registrations</div>
        </div>
        <div style={{ display: 'flex', alignItems: 'center', gap: 18 }}>
          <LegendDot color="var(--accent)" label="Current" />
          <LegendDot color="#525252" label="Prior period" dashed />
        </div>
      </div>

      <svg viewBox={`0 0 ${W} ${H}`} style={{ width: '100%', height: 'auto', display: 'block' }}
        onMouseLeave={() => setHoverDayIdx(null)}>
        {/* horizontal grid */}
        {yTicks.map((t, i) => (
          <g key={i}>
            <line x1={PAD.left} x2={W - PAD.right} y1={t.y} y2={t.y} stroke="#1a1a1a" strokeWidth="1" />
            <text x={PAD.left - 6} y={t.y + 3} textAnchor="end" fontSize="10" fill="#525252" fontFamily="Geist Mono, ui-monospace, monospace">
              {t.v}
            </text>
          </g>
        ))}
        {/* x-axis labels */}
        {xLabels.map((l, i) => (
          <text key={i} x={x(l.idx)} y={H - 10} textAnchor="middle" fontSize="10" fill="#525252" fontFamily="Geist Mono, ui-monospace, monospace" letterSpacing="0.04em">
            {fmtTick(l.date)}
          </text>
        ))}

        {/* prior period area (subtle) */}
        <path d={prvPath} fill="none" stroke="#525252" strokeWidth="1.5" strokeDasharray="4 3" />

        {/* current period area fill */}
        <defs>
          <linearGradient id="curFill" x1="0" y1="0" x2="0" y2="1">
            <stop offset="0%"   stopColor="#a855f7" stopOpacity="0.25" />
            <stop offset="100%" stopColor="#a855f7" stopOpacity="0" />
          </linearGradient>
        </defs>
        <path d={areaPath} fill="url(#curFill)" />
        <path d={curPath} fill="none" stroke="#a855f7" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" />

        {/* hover hit-areas */}
        {days.map((d, i) => (
          <rect
            key={i}
            x={x(i) - stepX / 2}
            y={PAD.top}
            width={stepX || 12}
            height={innerH}
            fill="transparent"
            onMouseEnter={() => setHoverDayIdx(i)}
          />
        ))}

        {/* hover indicator */}
        {hoverDayIdx !== null && (
          <g>
            <line x1={x(hoverDayIdx)} x2={x(hoverDayIdx)} y1={PAD.top} y2={PAD.top + innerH} stroke="#3a3a42" strokeWidth="1" />
            <circle cx={x(hoverDayIdx)} cy={y(curSeries[hoverDayIdx])} r="5" fill="#a855f7" stroke="#0a0a0a" strokeWidth="2" />
            <circle cx={x(hoverDayIdx)} cy={y(prvSeries[hoverDayIdx])} r="3.5" fill="#525252" stroke="#0a0a0a" strokeWidth="2" />
          </g>
        )}
      </svg>

      {/* hover readout */}
      {hoverDayIdx !== null && (
        <div className="mono" style={{
          marginTop: 8, padding: '10px 12px',
          background: '#0d0d0d', borderRadius: 6,
          fontSize: 12, display: 'flex', gap: 24, flexWrap: 'wrap', alignItems: 'center'
        }}>
          <span style={{ letterSpacing: '0.16em', color: 'var(--muted)' }}>
            {new Intl.DateTimeFormat('en-US', { weekday: 'short', month: 'short', day: 'numeric' }).format(days[hoverDayIdx])}
          </span>
          <span><span style={{ color: 'var(--accent)' }}>●</span> {curSeries[hoverDayIdx]} registrations</span>
          <span style={{ color: 'var(--muted)' }}>prior: {prvSeries[hoverDayIdx]}</span>
        </div>
      )}
    </div>
  );
}

function LegendDot({ color, label, dashed }) {
  return (
    <div style={{ display: 'inline-flex', alignItems: 'center', gap: 7 }}>
      {dashed ? (
        <span style={{ width: 16, height: 2, borderTop: '2px dashed ' + color, display: 'inline-block' }} />
      ) : (
        <span style={{ width: 9, height: 9, borderRadius: '50%', background: color, display: 'inline-block' }} />
      )}
      <span className="mono" style={{ fontSize: 11, letterSpacing: '0.08em', color: 'var(--muted)' }}>{label}</span>
    </div>
  );
}

// ─────────────────────────────────────────────
// Source / Campaign breakdown — drilldown table
// Aggregates registrants by utm_source → utm_campaign → utm_content
// ─────────────────────────────────────────────
function SourceBreakdown({ current }) {
  const [expanded, setExpanded] = useState(new Set()); // keys like 'src:facebook', 'cmp:facebook|june-cohort-1'
  const [sortBy, setSortBy] = useState('count');

  const tree = useMemo(() => {
    // Build session→source map from page_view events (for visitor-level metrics)
    const sessionSource = {};
    current.events.forEach(e => {
      if (e.event_type === 'page_view' && e.event_data) {
        const src = e.event_data.utm_source || null;
        if (!sessionSource[e.session_id] || src) sessionSource[e.session_id] = src || sessionSource[e.session_id] || 'direct';
      }
    });

    // Track which sessions hit a 50%+ video and which sessions had a calendar click
    const sessionsWithVideo50 = new Set();
    const sessionsWithCalClick = new Set();
    current.events.forEach(e => {
      if (e.event_type === 'video_progress' && e.event_data && typeof e.event_data.percent === 'number' && e.event_data.percent >= 50) {
        sessionsWithVideo50.add(e.session_id);
      }
      if (e.event_type === 'calendar_click') {
        sessionsWithCalClick.add(e.session_id);
      }
    });

    // Group registrants
    const root = {}; // source -> { count, complete, sms, campaigns: { campaign -> { count, ..., ads: { content -> {} } } } }
    function ensureSrc(src) {
      if (!root[src]) root[src] = { source: src, count: 0, complete: 0, sms: 0, partial: 0, campaigns: {} };
      return root[src];
    }
    function ensureCmp(srcNode, cmp) {
      if (!srcNode.campaigns[cmp]) srcNode.campaigns[cmp] = { campaign: cmp, count: 0, complete: 0, sms: 0, partial: 0, ads: {} };
      return srcNode.campaigns[cmp];
    }
    function ensureAd(cmpNode, ad) {
      if (!cmpNode.ads[ad]) cmpNode.ads[ad] = { content: ad, count: 0, complete: 0, sms: 0, partial: 0 };
      return cmpNode.ads[ad];
    }

    current.registrants.forEach(r => {
      const src = r.utm_source || (r.fbclid ? 'meta-click' : (r.gclid ? 'google-click' : 'direct'));
      const cmp = r.utm_campaign || '(no campaign)';
      const ad  = r.utm_content || '(no ad)';
      const srcNode = ensureSrc(src);
      const cmpNode = ensureCmp(srcNode, cmp);
      const adNode  = ensureAd(cmpNode, ad);
      const isComplete = r.status === 'complete';
      [srcNode, cmpNode, adNode].forEach(n => {
        n.count++;
        if (isComplete) n.complete++;
        else n.partial++;
        if (r.sms_responded) n.sms++;
      });
    });

    // Visitor sessions per source (for visitor counts and engagement rates)
    const visitorsBySrc = {};
    const cal50BySrc = {};
    const video50BySrc = {};
    Object.entries(sessionSource).forEach(([sid, src]) => {
      visitorsBySrc[src] = (visitorsBySrc[src] || 0) + 1;
      if (sessionsWithCalClick.has(sid)) cal50BySrc[src] = (cal50BySrc[src] || 0) + 1;
      if (sessionsWithVideo50.has(sid)) video50BySrc[src] = (video50BySrc[src] || 0) + 1;
    });
    Object.values(root).forEach(srcNode => {
      srcNode.visitors = visitorsBySrc[srcNode.source] || 0;
      srcNode.calendar_visitors = cal50BySrc[srcNode.source] || 0;
      srcNode.video50_visitors = video50BySrc[srcNode.source] || 0;
    });

    return Object.values(root);
  }, [current]);

  // Sorting
  const sorters = {
    count: (a, b) => b.count - a.count,
    complete: (a, b) => b.complete - a.complete,
    convRate: (a, b) => (b.complete / (b.count || 1)) - (a.complete / (a.count || 1)),
    smsRate: (a, b) => (b.sms / (b.complete || 1)) - (a.sms / (a.complete || 1)),
  };
  const sortedTree = useMemo(() => {
    const sort = sorters[sortBy] || sorters.count;
    return [...tree].sort(sort);
  }, [tree, sortBy]);

  function toggle(key) {
    setExpanded(prev => {
      const next = new Set(prev);
      if (next.has(key)) next.delete(key);
      else next.add(key);
      return next;
    });
  }

  if (tree.length === 0) {
    return (
      <div style={{
        background: 'var(--card)', border: '1px solid var(--line)',
        borderRadius: 10, padding: 22, marginBottom: 28
      }}>
        <div className="mono" style={{ fontSize: 10, letterSpacing: '0.2em', textTransform: 'uppercase', color: 'var(--muted)', fontWeight: 600, marginBottom: 4 }}>
          Source / Campaign
        </div>
        <div style={{ fontSize: 18, fontWeight: 600, marginBottom: 8 }}>Attribution breakdown</div>
        <div style={{ color: 'var(--muted-2)', fontSize: 13, fontStyle: 'italic', padding: '14px 0' }}>
          No registrants in this window yet.
        </div>
      </div>
    );
  }

  return (
    <div style={{
      background: 'var(--card)', border: '1px solid var(--line)',
      borderRadius: 10, padding: 22, marginBottom: 28
    }}>
      <div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', marginBottom: 18, gap: 16, flexWrap: 'wrap' }}>
        <div>
          <div className="mono" style={{ fontSize: 10, letterSpacing: '0.2em', textTransform: 'uppercase', color: 'var(--muted)', fontWeight: 600, marginBottom: 4 }}>
            Source / Campaign
          </div>
          <div style={{ fontSize: 18, fontWeight: 600 }}>Attribution breakdown</div>
        </div>
        <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
          <span className="mono" style={{ fontSize: 10, letterSpacing: '0.18em', color: 'var(--muted)', textTransform: 'uppercase', marginRight: 4 }}>Sort by</span>
          {[
            { id: 'count', label: 'Volume' },
            { id: 'complete', label: 'Completes' },
            { id: 'convRate', label: 'Conv rate' },
            { id: 'smsRate', label: 'SMS rate' },
          ].map(opt => (
            <button key={opt.id} onClick={() => setSortBy(opt.id)}
              style={sortBy === opt.id ? sortPillActive : sortPill}>
              {opt.label}
            </button>
          ))}
        </div>
      </div>

      <div style={{ overflowX: 'auto' }}>
        <table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 13 }}>
          <thead>
            <tr style={{ borderBottom: '1px solid var(--line)' }}>
              <Th>Source / Campaign / Ad</Th>
              <Th>Registrants</Th>
              <Th>Completes</Th>
              <Th>Conv rate</Th>
              <Th>Partials</Th>
              <Th>SMS replies</Th>
              <Th>Calendar adds</Th>
              <Th>Video 50%+</Th>
            </tr>
          </thead>
          <tbody>
            {sortedTree.map(srcNode => {
              const srcKey = 'src:' + srcNode.source;
              const isOpen = expanded.has(srcKey);
              const campaigns = Object.values(srcNode.campaigns).sort((a, b) => b.count - a.count);
              return (
                <React.Fragment key={srcKey}>
                  <BreakdownRow
                    name={srcNode.source}
                    node={srcNode}
                    indent={0}
                    expandable={campaigns.length > 0}
                    expanded={isOpen}
                    onToggle={() => toggle(srcKey)}
                    extra={{
                      calendar_visitors: srcNode.calendar_visitors,
                      video50_visitors: srcNode.video50_visitors,
                      visitors: srcNode.visitors
                    }}
                  />
                  {isOpen && campaigns.map(cmpNode => {
                    const cmpKey = 'cmp:' + srcNode.source + '|' + cmpNode.campaign;
                    const cmpOpen = expanded.has(cmpKey);
                    const ads = Object.values(cmpNode.ads).sort((a, b) => b.count - a.count);
                    return (
                      <React.Fragment key={cmpKey}>
                        <BreakdownRow
                          name={cmpNode.campaign}
                          node={cmpNode}
                          indent={1}
                          expandable={ads.length > 1 || (ads.length === 1 && ads[0].content !== '(no ad)')}
                          expanded={cmpOpen}
                          onToggle={() => toggle(cmpKey)}
                        />
                        {cmpOpen && ads.map(adNode => (
                          <BreakdownRow
                            key={cmpKey + '|' + adNode.content}
                            name={adNode.content}
                            node={adNode}
                            indent={2}
                            expandable={false}
                          />
                        ))}
                      </React.Fragment>
                    );
                  })}
                </React.Fragment>
              );
            })}
          </tbody>
        </table>
      </div>
    </div>
  );
}

function BreakdownRow({ name, node, indent, expandable, expanded, onToggle, extra }) {
  const convRate = node.count > 0 ? (node.complete / node.count) * 100 : 0;
  const smsRate  = node.complete > 0 ? (node.sms / node.complete) * 100 : 0;
  const calRate  = extra && extra.visitors ? (extra.calendar_visitors / extra.visitors) * 100 : null;
  const videoRate = extra && extra.visitors ? (extra.video50_visitors / extra.visitors) * 100 : null;

  return (
    <tr style={{ borderBottom: '1px solid #1a1a1a', background: indent === 0 ? 'transparent' : '#0a0a0a' }}>
      <td style={{ padding: '12px 14px', paddingLeft: 14 + indent * 22 }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
          {expandable ? (
            <button onClick={onToggle} style={{
              width: 18, height: 18, padding: 0,
              background: 'transparent', border: '1px solid var(--line-2)', borderRadius: 4,
              color: 'var(--muted)', cursor: 'pointer', fontSize: 9,
              display: 'inline-flex', alignItems: 'center', justifyContent: 'center'
            }}>{expanded ? '▾' : '▸'}</button>
          ) : (
            <span style={{ width: 18, height: 18, display: 'inline-block' }} />
          )}
          <span style={{
            color: indent === 0 ? 'var(--ink)' : 'var(--ink-2)',
            fontWeight: indent === 0 ? 600 : 400,
            textTransform: indent === 0 ? 'capitalize' : 'none'
          }}>{name}</span>
        </div>
      </td>
      <Td><span className="mono" style={{ fontVariantNumeric: 'tabular-nums' }}>{node.count}</span></Td>
      <Td><span className="mono" style={{ fontVariantNumeric: 'tabular-nums' }}>{node.complete}</span></Td>
      <Td><RateBadge value={convRate} threshold={70} /></Td>
      <Td><span className="mono" style={{ fontVariantNumeric: 'tabular-nums', color: 'var(--muted)' }}>{node.partial}</span></Td>
      <Td><span className="mono">{node.sms}</span> <span className="mono" style={{ color: 'var(--muted-2)', fontSize: 11 }}>({smsRate.toFixed(0)}%)</span></Td>
      <Td>{calRate !== null ? <span className="mono" style={{ color: 'var(--muted-2)' }}>{extra.calendar_visitors} ({calRate.toFixed(0)}%)</span> : <span style={{ color: 'var(--muted-2)' }}>—</span>}</Td>
      <Td>{videoRate !== null ? <span className="mono" style={{ color: 'var(--muted-2)' }}>{extra.video50_visitors} ({videoRate.toFixed(0)}%)</span> : <span style={{ color: 'var(--muted-2)' }}>—</span>}</Td>
    </tr>
  );
}

function RateBadge({ value, threshold }) {
  const c = value >= threshold ? '#00ff85' : value >= threshold * 0.6 ? '#fbbf24' : '#fca5a5';
  return (
    <span className="mono" style={{ color: c, fontVariantNumeric: 'tabular-nums', fontSize: 12 }}>
      {value.toFixed(1)}%
    </span>
  );
}

const sortPill = {
  padding: '5px 10px',
  background: 'transparent',
  border: '1px solid var(--line-2)',
  borderRadius: 5,
  color: 'var(--ink-2)',
  fontSize: 11, fontWeight: 500,
  cursor: 'pointer', fontFamily: 'inherit',
  letterSpacing: '-0.005em'
};
const sortPillActive = {
  ...sortPill,
  background: 'rgba(168,85,247,0.12)',
  border: '1px solid var(--accent)',
  color: 'var(--accent)',
  fontWeight: 600
};

const rangePill = {
  padding: '7px 14px',
  background: 'transparent',
  border: '1px solid var(--line-2)',
  borderRadius: 6,
  color: 'var(--ink-2)',
  fontSize: 13, fontWeight: 500,
  cursor: 'pointer', fontFamily: 'inherit',
  transition: 'all 140ms'
};
const rangePillActive = {
  ...rangePill,
  background: 'rgba(168,85,247,0.12)',
  border: '1px solid var(--accent)',
  color: 'var(--accent)',
  fontWeight: 600
};
const dateInput = {
  padding: '7px 9px',
  background: 'var(--bg-3)',
  border: '1px solid var(--line-2)',
  borderRadius: 5,
  color: 'var(--ink)',
  fontSize: 12,
  fontFamily: 'inherit',
  colorScheme: 'dark'
};

ReactDOM.createRoot(document.getElementById('root')).render(<App />);
