// Admin/editor view for driver intel.
//
// Service-role data sits behind /api/admin/intel; this page sends the
// admin token in the Authorization header. The token is stored in
// localStorage after first entry so refresh-and-keep-working just works.
//
// Workflow per row:
//   pending → verified  → mark visibility public + published_at = now() to publish
//   pending → rejected  (drops out of aggregates)
//   pending → duplicate (same)
//
// Bulk actions left for v2; one-row-at-a-time keeps editorial discipline tight.

const { useState: useStateA, useEffect: useEffectA, useMemo: useMemoA, useRef: useRefA } = React;

// Save-to-Photos button — uses the iOS Safari Web Share API to push an MP4
// through the native share sheet, where "Save Video" writes it directly to
// the Photos app. Falls back to hidden on browsers without canShare(files).
//
// Flow on iPhone:
//   1. Tap "📱 Save to Photos"
//   2. Browser fetches the MP4 (CORS allowed by Supabase Storage public buckets)
//   3. We construct a File and call navigator.share({ files: [file] })
//   4. iOS share sheet opens → tap "Save Video" → lands in Photos
//
// Bandwidth cost: the full MP4 is downloaded to the browser before sharing.
// On wifi this is fine; on cellular it's the cost of getting the file.
function SaveToPhotosButton({ url, filename, title }) {
  const [busy, setBusy] = React.useState(false);
  const [err, setErr] = React.useState(null);

  // Hide entirely on browsers that don't support the API. Desktop Chrome on
  // most platforms doesn't, but iOS Safari + Chrome Android do (which is the
  // primary use case). Probe with a tiny dummy file because canShare without
  // args is true even when files aren't supported.
  const supported = React.useMemo(() => {
    if (typeof navigator === "undefined" || !navigator.canShare) return false;
    try {
      const probe = new File([""], "probe.mp4", { type: "video/mp4" });
      return navigator.canShare({ files: [probe] });
    } catch (_) {
      return false;
    }
  }, []);

  if (!supported) return null;

  async function handleClick() {
    if (busy) return;
    setBusy(true);
    setErr(null);
    try {
      const r = await fetch(url);
      if (!r.ok) throw new Error(`Fetch failed: ${r.status}`);
      const blob = await r.blob();
      const file = new File([blob], filename, { type: "video/mp4" });
      if (!navigator.canShare({ files: [file] })) {
        throw new Error("MP4 not shareable");
      }
      await navigator.share({
        files: [file],
        title: title || "Shipping Clarity video",
      });
      // Success — no toast; the share sheet itself is the feedback.
    } catch (e) {
      // AbortError = user dismissed the share sheet; not really an error.
      if (e && e.name !== "AbortError") {
        setErr(e.message || String(e));
      }
    }
    setBusy(false);
  }

  return (
    <span style={{ display: "inline-flex", alignItems: "center", gap: 8 }}>
      <button
        onClick={handleClick}
        disabled={busy}
        style={{
          display: "inline-block", padding: "6px 14px",
          background: "#5fa9ff", color: "#0a1224",
          borderRadius: 4, border: "none", fontWeight: 600,
          cursor: busy ? "wait" : "pointer", fontSize: 12,
        }}
      >
        {busy ? "Preparing…" : "📱 Save to Photos"}
      </button>
      {err && (
        <span style={{ fontSize: 11, color: "var(--red, #c0392b)" }}>{err}</span>
      )}
    </span>
  );
}

function AdminPage({ onNav }) {
  const [token, setToken] = useStateA(() => {
    if (typeof window === "undefined") return "";
    // Allow ?token=... in the URL hash for first-visit setup, then store.
    const url = new URL(window.location.href.replace("#/", ""));
    const fromUrl = url.searchParams.get("token");
    if (fromUrl) localStorage.setItem("sc_admin_token", fromUrl);
    return localStorage.getItem("sc_admin_token") || "";
  });
  const [tokenInput, setTokenInput] = useStateA("");
  const [signInError, setSignInError] = useStateA(null);
  const [signInChecking, setSignInChecking] = useStateA(false);
  const [rows, setRows] = useStateA([]);
  const [loading, setLoading] = useStateA(false);
  const [statusFilter, setStatusFilter] = useStateA("pending");
  const [carrierFilter, setCarrierFilter] = useStateA("");
  const [error, setError] = useStateA(null);
  const [authError, setAuthError] = useStateA(null);
  const [actionMessage, setActionMessage] = useStateA(null);
  // Mode is initialized from the URL hash so deep-links can land directly
  // on a specific admin sub-page. e.g. #/admin?mode=video deep-links into
  // the Video Studio (used by the home video-strip placeholder cards
  // when the editor has no videos pinned yet).
  const [mode, setMode] = useStateA(() => {
    if (typeof window === "undefined") return "overview";
    const m = (window.location.hash || "").match(/[?&]mode=([a-z]+)/i);
    return (m && ["overview", "list", "tray", "video"].includes(m[1])) ? m[1] : "overview";
  });

  // Dashboard state
  const [dash, setDash] = useStateA(null);
  const [dashLoading, setDashLoading] = useStateA(false);
  const [recentTypeFilter, setRecentTypeFilter] = useStateA("all");

  // Voices state (research_voices)
  const [voices, setVoices] = useStateA([]);
  const [voiceForm, setVoiceForm] = useStateA({
    handle: "", platform: "reddit", real_name: "", affiliation: "",
    why: "", source_url: "", topic_tags: "",
  });
  const [voiceMessage, setVoiceMessage] = useStateA(null);
  const [voiceSaving, setVoiceSaving] = useStateA(false);

  async function loadVoices() {
    if (!token) return;
    try {
      const r = await fetch(`/api/admin/research-voices?limit=200`, {
        headers: { Authorization: `Bearer ${token}` },
      });
      if (!r.ok) return;
      const d = await r.json();
      setVoices(Array.isArray(d) ? d : []);
    } catch (_) {}
  }

  async function saveVoice() {
    if (voiceSaving) return;
    const handle = voiceForm.handle.trim();
    if (!handle) {
      setVoiceMessage("Handle required");
      setTimeout(() => setVoiceMessage(null), 1800);
      return;
    }
    setVoiceSaving(true);
    setVoiceMessage("Saving…");
    try {
      const tags = voiceForm.topic_tags.split(",").map(t => t.trim()).filter(Boolean);
      const body = {
        handle,
        platform: voiceForm.platform,
        real_name: voiceForm.real_name.trim() || null,
        affiliation: voiceForm.affiliation.trim() || null,
        why: voiceForm.why.trim() || null,
        source_url: voiceForm.source_url.trim() || null,
        topic_tags: tags,
        last_observed_at: new Date().toISOString(),
      };
      const r = await fetch(`/api/admin/research-voices`, {
        method: "POST",
        headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
        body: JSON.stringify(body),
      });
      const data = await r.json().catch(() => ({}));
      if (!r.ok) throw new Error(data.error || `${r.status}`);
      setVoiceMessage(`✓ Added ${handle}`);
      setVoiceForm({
        handle: "", platform: voiceForm.platform, real_name: "", affiliation: "",
        why: "", source_url: "", topic_tags: "",
      });
      setTimeout(() => setVoiceMessage(null), 2200);
      loadVoices();
    } catch (e) {
      setVoiceMessage(`✗ ${e.message || e}`);
    }
    setVoiceSaving(false);
  }

  async function deleteVoice(id) {
    if (!confirm("Delete this voice?")) return;
    try {
      const r = await fetch(`/api/admin/research-voices?id=${encodeURIComponent(id)}`, {
        method: "DELETE",
        headers: { Authorization: `Bearer ${token}` },
      });
      if (!r.ok) throw new Error(`${r.status}`);
      setVoices(prev => prev.filter(v => v.id !== id));
    } catch (e) {
      setVoiceMessage(`✗ ${e.message || e}`);
    }
  }

  useEffectA(() => { if (mode === "voices" && token) loadVoices(); }, [mode, token]);

  // Editorial drafts (committed by daily-editorial.yml + weekly-editorial.yml)
  const [drafts, setDrafts]               = useStateA([]);
  const [draftsLoading, setDraftsLoading] = useStateA(false);
  const [openDraft, setOpenDraft]         = useStateA(null);
  const [draftKindFilter, setDraftKindFilter] = useStateA("all");

  async function loadDrafts() {
    if (!token || draftsLoading) return;
    setDraftsLoading(true);
    try {
      const r = await fetch(`/api/admin/drafts`, {
        headers: { Authorization: `Bearer ${token}` },
      });
      if (!r.ok) throw new Error(`${r.status}`);
      const d = await r.json();
      setDrafts(Array.isArray(d.drafts) ? d.drafts : []);
    } catch (_) {}
    setDraftsLoading(false);
  }

  useEffectA(() => { if (mode === "drafts" && token) loadDrafts(); }, [mode, token]);

  // Synthesized intel_alerts (output of synthesize-alerts.py). Separate
  // table from intel_submissions, so it gets its own admin tab.
  const [alerts, setAlerts]               = useStateA([]);
  const [alertsLoading, setAlertsLoading] = useStateA(false);
  const [alertStatusFilter, setAlertStatusFilter] = useStateA("pending");
  const [alertMessage, setAlertMessage]   = useStateA(null);

  async function loadAlerts() {
    if (!token || alertsLoading) return;
    setAlertsLoading(true);
    try {
      const r = await fetch(`/api/admin/alerts?status=${alertStatusFilter}&limit=200`, {
        headers: { Authorization: `Bearer ${token}` },
      });
      if (!r.ok) throw new Error(`${r.status}`);
      const d = await r.json();
      setAlerts(Array.isArray(d) ? d : []);
    } catch (_) {}
    setAlertsLoading(false);
  }

  async function alertAction(id, status) {
    setAlertMessage("Saving…");
    try {
      const r = await fetch(`/api/admin/alerts?id=${encodeURIComponent(id)}`, {
        method: "PATCH",
        headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
        body: JSON.stringify({ status }),
      });
      if (!r.ok) throw new Error(`${r.status}`);
      setAlertMessage(`✓ ${status}`);
      setAlerts(prev => prev.filter(a => a.id !== id));
      setTimeout(() => setAlertMessage(null), 1800);
    } catch (e) {
      setAlertMessage(`✗ ${e.message || e}`);
    }
  }

  useEffectA(() => { if (mode === "alerts" && token) loadAlerts(); }, [mode, token, alertStatusFilter]);

  // Subscribers (newsletter_signups + pulse_subscriptions, unified)
  const [subs, setSubs]             = useStateA([]);
  const [subsTotals, setSubsTotals] = useStateA({});
  const [subsLoading, setSubsLoading] = useStateA(false);
  const [subsKind, setSubsKind]     = useStateA("all");
  const [subsQuery, setSubsQuery]   = useStateA("");

  async function loadSubscribers() {
    if (!token || subsLoading) return;
    setSubsLoading(true);
    try {
      const params = new URLSearchParams();
      params.set("source", subsKind);
      params.set("limit", "500");
      if (subsQuery.trim()) params.set("q", subsQuery.trim());
      const r = await fetch(`/api/admin/subscribers?${params.toString()}`, {
        headers: { Authorization: `Bearer ${token}` },
      });
      if (!r.ok) throw new Error(`${r.status}`);
      const d = await r.json();
      setSubs(Array.isArray(d.subscribers) ? d.subscribers : []);
      setSubsTotals({
        total: d.total || 0,
        newsletter_total: d.newsletter_total || 0,
        pulse_total: d.pulse_total || 0,
        by_source: d.by_source || {},
      });
    } catch (_) {}
    setSubsLoading(false);
  }

  useEffectA(() => { if (mode === "subs" && token) loadSubscribers(); }, [mode, token, subsKind]);

  // Shipper contacts (scraped from websites — generic emails for the most
  // part, no decision-maker layer yet). Lets the editor browse + export.
  const [contacts, setContacts]       = useStateA([]);
  const [contactsSummary, setContactsSummary] = useStateA({});
  const [contactsLoading, setContactsLoading] = useStateA(false);
  const [contactsHasEmail, setContactsHasEmail] = useStateA(true);
  const [contactsQ, setContactsQ]     = useStateA("");

  async function loadContacts() {
    if (!token || contactsLoading) return;
    setContactsLoading(true);
    try {
      const params = new URLSearchParams();
      if (contactsHasEmail) params.set("has_email", "1");
      if (contactsQ.trim()) params.set("q", contactsQ.trim());
      params.set("limit", "500");
      const r = await fetch(`/api/admin/contacts?${params.toString()}`, {
        headers: { Authorization: `Bearer ${token}` },
      });
      if (!r.ok) throw new Error(`${r.status}`);
      const d = await r.json();
      setContacts(Array.isArray(d.contacts) ? d.contacts : []);
      setContactsSummary(d.summary || {});
    } catch (_) {}
    setContactsLoading(false);
  }

  useEffectA(() => { if (mode === "contacts" && token) loadContacts(); }, [mode, token, contactsHasEmail]);

  // If a token is restored from localStorage (or set via ?token=), validate
  // it once on mount. A bad/stale token used to silently render the admin UI
  // with empty data because every load function swallowed 401s — that masked
  // the real issue. Now: probe → if 401, drop token + show sign-in error.
  useEffectA(() => {
    if (!token) return;
    let cancelled = false;
    (async () => {
      try {
        const r = await fetch(`/api/admin/dashboard`, {
          headers: { Authorization: `Bearer ${token}` },
        });
        if (cancelled) return;
        if (r.status === 401) {
          localStorage.removeItem("sc_admin_token");
          setToken("");
          setSignInError("Stored token rejected. Re-enter the value from Vercel env (ADMIN_TOKEN).");
        }
      } catch (_) { /* network errors handled per-call */ }
    })();
    return () => { cancelled = true; };
  }, [token]);

  async function loadDashboard() {
    if (!token) return;
    setDashLoading(true);
    try {
      const res = await fetch(`/api/admin/dashboard`, {
        headers: { Authorization: `Bearer ${token}` },
      });
      if (res.status === 401) {
        setAuthError("Token rejected. Sign out and re-enter.");
      } else if (res.ok) {
        setAuthError(null);
        setDash(await res.json());
      }
    } catch (_) { /* swallow */ }
    setDashLoading(false);
  }

  useEffectA(() => {
    if (mode === "overview" && token) loadDashboard();
  }, [mode, token]);
  const [trayIdx, setTrayIdx] = useStateA(0);
  const [bulkRunning, setBulkRunning] = useStateA(false);

  // Carrier list (best-effort: from current page of rows). Declared up here
  // so the hook order stays stable across the !token early return below;
  // putting useMemo after a conditional return is a Rules of Hooks
  // violation that crashes when the user signs in mid-session.
  const carriers = useMemoA(() => {
    const set = new Set();
    rows.forEach(r => r.parsed_carrier && set.add(r.parsed_carrier));
    return [...set].sort();
  }, [rows]);

  // Vetting state
  const [vettingRows, setVettingRows] = useStateA([]);
  const [vettingFilter, setVettingFilter] = useStateA("pending");
  const [vettingLoading, setVettingLoading] = useStateA(false);
  const [vettingMessage, setVettingMessage] = useStateA(null);

  async function loadVetting() {
    if (!token) return;
    setVettingLoading(true);
    try {
      const r = await fetch(`/api/admin/vetting?status=${encodeURIComponent(vettingFilter)}&limit=200`, {
        headers: { Authorization: `Bearer ${token}` },
      });
      if (r.status === 401) {
        setAuthError("Token rejected. Sign out and re-enter.");
        setVettingLoading(false);
        return;
      }
      if (!r.ok) { setVettingLoading(false); return; }
      const data = await r.json();
      setVettingRows(Array.isArray(data) ? data : []);
    } catch (_) {}
    setVettingLoading(false);
  }

  async function vettingAction(userId, action, notes) {
    if (!token) return;
    try {
      const r = await fetch(`/api/admin/vetting`, {
        method: "PATCH",
        headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
        body: JSON.stringify({ user_id: userId, action, notes: notes || null }),
      });
      if (!r.ok) {
        const j = await r.json().catch(() => ({}));
        setVettingMessage(`✗ ${j.error || r.status}`);
        return;
      }
      setVettingMessage(`✓ ${action} applied`);
      setTimeout(() => setVettingMessage(null), 2000);
      loadVetting();
    } catch (e) {
      setVettingMessage(`✗ ${e.message || e}`);
    }
  }

  useEffectA(() => { if (mode === "vetting" && token) loadVetting(); }, [mode, token, vettingFilter]);

  // ---- Verdicts queue: editorial gate on Distress flags ----
  const [verdictRows, setVerdictRows] = useStateA([]);
  const [verdictFilter, setVerdictFilter] = useStateA("heuristic_distress");
  const [verdictLoading, setVerdictLoading] = useStateA(false);
  const [verdictMessage, setVerdictMessage] = useStateA(null);

  async function loadVerdicts() {
    if (!token) return;
    setVerdictLoading(true);
    try {
      const r = await fetch(`/api/admin/verdict-override?status=${encodeURIComponent(verdictFilter)}`, {
        headers: { Authorization: `Bearer ${token}` },
      });
      if (r.status === 401) {
        setAuthError("Token rejected. Sign out and re-enter.");
        setVerdictLoading(false);
        return;
      }
      if (!r.ok) { setVerdictLoading(false); return; }
      const data = await r.json();
      setVerdictRows(Array.isArray(data) ? data : []);
    } catch (_) {}
    setVerdictLoading(false);
  }

  async function setVerdictOverride(table, slug, override) {
    if (!token) return;
    try {
      const r = await fetch(`/api/admin/verdict-override`, {
        method: "PATCH",
        headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
        body: JSON.stringify({ table, slug, override }),
      });
      if (!r.ok) {
        const j = await r.json().catch(() => ({}));
        setVerdictMessage(`✗ ${j.error || r.status}`);
        return;
      }
      setVerdictMessage(`✓ ${slug}: ${override === null ? "reset to heuristic" : override}`);
      setTimeout(() => setVerdictMessage(null), 2200);
      loadVerdicts();
    } catch (e) {
      setVerdictMessage(`✗ ${e.message || e}`);
    }
  }

  useEffectA(() => { if (mode === "verdicts" && token) loadVerdicts(); }, [mode, token, verdictFilter]);

  // Video studio state
  const [videoTopic, setVideoTopic] = useStateA("");
  const [videoFormat, setVideoFormat] = useStateA("16x9");
  const [videoTargetSeconds, setVideoTargetSeconds] = useStateA(300);
  const [videoPlannerModel, setVideoPlannerModel] = useStateA("claude-sonnet-4-6");
  const [videoVoiceId, setVideoVoiceId] = useStateA("");
  const [videoObjectsHint, setVideoObjectsHint] = useStateA("");
  const [videoCharactersHint, setVideoCharactersHint] = useStateA("");
  const [videoIncludeIcons, setVideoIncludeIcons] = useStateA(false);
  const [videoDispatching, setVideoDispatching] = useStateA(false);
  const [videoDrafts, setVideoDrafts] = useStateA([]);
  const [videoMessage, setVideoMessage] = useStateA(null);

  // "Upload your own" state
  const [uplMode, setUplMode]            = useStateA("file");        // "file" | "embed"
  const [uplFile, setUplFile]            = useStateA(null);
  const [uplEmbedUrl, setUplEmbedUrl]    = useStateA("");
  const [uplTitle, setUplTitle]          = useStateA("");
  const [uplDescription, setUplDescription] = useStateA("");
  const [uplFormat, setUplFormat]        = useStateA("16x9");
  const [uplSlug, setUplSlug]            = useStateA("");
  const [uplFeatured, setUplFeatured]    = useStateA(true);
  const [uplBusy, setUplBusy]            = useStateA(false);
  const [uplProgress, setUplProgress]    = useStateA(0);
  const [uplMessage, setUplMessage]      = useStateA(null);

  async function uploadOwnVideo() {
    if (uplBusy) return;
    if (!uplTitle.trim()) {
      setUplMessage("Title required");
      setTimeout(() => setUplMessage(null), 1800);
      return;
    }
    if (uplMode === "file" && !uplFile) {
      setUplMessage("Pick an MP4 file first");
      setTimeout(() => setUplMessage(null), 1800);
      return;
    }
    if (uplMode === "embed" && !uplEmbedUrl.trim()) {
      setUplMessage("Paste a YouTube/Vimeo URL");
      setTimeout(() => setUplMessage(null), 1800);
      return;
    }
    setUplBusy(true);
    setUplProgress(0);
    setUplMessage("Working…");
    try {
      let video_path = null;
      let duration_seconds = null;

      if (uplMode === "file") {
        // Try to read duration from the file before upload (best-effort).
        try {
          duration_seconds = await new Promise((resolve) => {
            const v = document.createElement("video");
            v.preload = "metadata";
            v.onloadedmetadata = () => {
              const d = isFinite(v.duration) ? Math.round(v.duration) : null;
              URL.revokeObjectURL(v.src);
              resolve(d);
            };
            v.onerror = () => resolve(null);
            v.src = URL.createObjectURL(uplFile);
          });
        } catch (_) { /* ignore */ }

        // 1) Get a signed upload URL.
        setUplMessage("Requesting signed upload URL…");
        const signRes = await fetch(`/api/admin/video-upload`, {
          method: "POST",
          headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
          body: JSON.stringify({ action: "sign", filename: uplFile.name }),
        });
        if (!signRes.ok) {
          const j = await signRes.json().catch(() => ({}));
          throw new Error(j.error || `sign failed (${signRes.status})`);
        }
        const { upload_url, path } = await signRes.json();
        video_path = path;

        // 2) PUT the file directly to Supabase Storage with progress.
        setUplMessage(`Uploading ${(uplFile.size / 1024 / 1024).toFixed(1)} MB…`);
        await new Promise((resolve, reject) => {
          const xhr = new XMLHttpRequest();
          xhr.open("PUT", upload_url, true);
          xhr.setRequestHeader("Content-Type", uplFile.type || "video/mp4");
          xhr.upload.onprogress = (e) => {
            if (e.lengthComputable) setUplProgress(Math.round((e.loaded / e.total) * 100));
          };
          xhr.onload = () => {
            if (xhr.status >= 200 && xhr.status < 300) resolve();
            else reject(new Error(`upload failed (${xhr.status}): ${xhr.responseText.slice(0, 200)}`));
          };
          xhr.onerror = () => reject(new Error("network error during upload"));
          xhr.send(uplFile);
        });
      }

      // 3) Create the video_drafts row (auto-publishes).
      setUplMessage("Publishing…");
      const createRes = await fetch(`/api/admin/video-upload`, {
        method: "POST",
        headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
        body: JSON.stringify({
          action: "create",
          title: uplTitle.trim(),
          description: uplDescription.trim() || null,
          format: uplFormat,
          slug: uplSlug.trim() || undefined,
          video_path: video_path || undefined,
          embed_url: uplMode === "embed" ? uplEmbedUrl.trim() : undefined,
          featured: !!uplFeatured,
          duration_seconds,
        }),
      });
      if (!createRes.ok) {
        const j = await createRes.json().catch(() => ({}));
        throw new Error(j.error || `create failed (${createRes.status})`);
      }
      const out = await createRes.json();
      setUplMessage(`✓ Published — ${out.public_url}`);
      setUplFile(null);
      setUplEmbedUrl("");
      setUplTitle("");
      setUplDescription("");
      setUplSlug("");
      setUplProgress(0);
      loadVideoDrafts();
    } catch (e) {
      setUplMessage(`✗ ${e.message || e}`);
    }
    setUplBusy(false);
  }

  async function loadVideoDrafts() {
    if (!token) return;
    try {
      const res = await fetch(`/api/admin/video-trigger?limit=20`, {
        headers: { Authorization: `Bearer ${token}` },
      });
      if (res.status === 401) {
        setAuthError("Token rejected. Sign out and re-enter.");
        return;
      }
      if (!res.ok) return;
      setAuthError(null);
      const data = await res.json();
      setVideoDrafts(Array.isArray(data) ? data : []);
    } catch (_) { /* ignore polling errors */ }
  }

  async function dispatchVideo() {
    if (videoDispatching) return;
    const topic = videoTopic.trim();
    if (!topic) {
      setVideoMessage("Topic required");
      setTimeout(() => setVideoMessage(null), 1800);
      return;
    }
    setVideoDispatching(true);
    setVideoMessage("Dispatching workflow…");
    try {
      const res = await fetch(`/api/admin/video-trigger`, {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          Authorization: `Bearer ${token}`,
        },
        body: JSON.stringify({
          topic,
          format: videoFormat,
          target_seconds: videoTargetSeconds,
          planner_model: videoPlannerModel,
          voice_id: videoVoiceId,
          objects_hint: videoObjectsHint,
          characters_hint: videoCharactersHint,
          include_icons: videoIncludeIcons,
        }),
      });
      const body = await res.json().catch(() => ({}));
      if (res.status === 401) {
        setAuthError("Token rejected. Sign out and re-enter from Vercel env (ADMIN_TOKEN).");
        throw new Error("unauthorized — see banner");
      }
      if (!res.ok) {
        throw new Error(body.error || `${res.status}`);
      }
      setVideoMessage("✓ Dispatched. Render takes ~5 minutes — refreshing list every 10s.");
      setVideoTopic("");
      setTimeout(() => setVideoMessage(null), 4000);
      loadVideoDrafts();
    } catch (e) {
      setVideoMessage(`✗ ${e.message || e}`);
    }
    setVideoDispatching(false);
  }

  // Load drafts when entering video mode + poll ONLY while there's an
  // in-progress row. The previous logic polled every 10s as long as the
  // list was empty OR something was active — which meant after a clean
  // session with no drafts we'd hammer the API endpoint forever. Plus
  // the 10s cadence was tight; 30s is plenty for human-scale waiting.
  // Cuts API call volume significantly when admin tab is left open.
  useEffectA(() => {
    if (mode !== "video" || !token) return;
    loadVideoDrafts();
    const hasActive = videoDrafts.some(
      (d) => d.status !== "rendered" && d.status !== "failed"
    );
    if (!hasActive) return; // no work in flight — don't poll
    const id = setInterval(() => {
      loadVideoDrafts();
    }, 30000);
    return () => clearInterval(id);
  }, [mode, token, videoDrafts]);

  async function load() {
    if (!token) return;
    setLoading(true);
    setError(null);
    try {
      const params = new URLSearchParams();
      if (statusFilter && statusFilter !== "all") params.set("status", statusFilter);
      if (carrierFilter) params.set("carrier", carrierFilter);
      params.set("limit", "200");
      const res = await fetch(`/api/admin/intel?${params.toString()}`, {
        headers: { Authorization: `Bearer ${token}` },
      });
      if (!res.ok) {
        const body = await res.text();
        throw new Error(`${res.status}: ${body.slice(0, 120)}`);
      }
      const data = await res.json();
      setRows(Array.isArray(data) ? data : []);
    } catch (e) {
      setError(String(e.message || e));
      setRows([]);
    }
    setLoading(false);
  }

  useEffectA(() => { load(); }, [token, statusFilter, carrierFilter]);

  // Reset tray cursor when row set shifts (filter change, refresh, after action).
  useEffectA(() => {
    if (trayIdx >= rows.length) setTrayIdx(Math.max(0, rows.length - 1));
  }, [rows.length]);

  // Keyboard shortcuts — only active in tray mode + when token is set.
  useEffectA(() => {
    if (!token || mode !== "tray") return;
    const onKey = (e) => {
      // Skip if user is typing in an input/textarea/select
      const tag = (e.target && e.target.tagName) || "";
      if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return;
      if (e.metaKey || e.ctrlKey || e.altKey) return;
      const r = rows[trayIdx];
      if (!r && e.key !== "?") return;
      const k = e.key.toLowerCase();
      if (k === "j" || e.key === "ArrowDown") {
        e.preventDefault();
        setTrayIdx(i => Math.min(rows.length - 1, i + 1));
      } else if (k === "k" || e.key === "ArrowUp") {
        e.preventDefault();
        setTrayIdx(i => Math.max(0, i - 1));
      } else if (k === "p" && r) {
        e.preventDefault();
        patchRow(r.id, {
          status: "verified", visibility: "public",
          published_at: new Date().toISOString(),
        }, "published");
      } else if (k === "v" && r) {
        e.preventDefault();
        patchRow(r.id, { status: "verified" }, "verified (sealed)");
      } else if (k === "r" && r) {
        e.preventDefault();
        patchRow(r.id, { status: "rejected" }, "rejected");
      } else if (k === "d" && r) {
        e.preventDefault();
        patchRow(r.id, { status: "duplicate" }, "duplicate");
      } else if (k === "s") {
        e.preventDefault();
        setTrayIdx(i => Math.min(rows.length - 1, i + 1));   // skip
      }
    };
    document.addEventListener("keydown", onKey);
    return () => document.removeEventListener("keydown", onKey);
  }, [token, mode, rows, trayIdx]);

  // Bulk: accept all currently-loaded SEC officer-change (Item 5.02) rows as
  // sealed verified. They're routine board appointments — drown the queue
  // otherwise. Only acts on the current page (limit 200).
  async function bulkAcceptSEC502() {
    if (bulkRunning) return;
    const targets = rows.filter(r =>
      r.source === "sec-edgar"
      && (r.raw_text || "").includes("Item 5.02")
      && r.status === "verified"  // already verified, just here to filter the noise
      && r.visibility === "sealed"
    );
    if (targets.length === 0) {
      setActionMessage("No SEC Item 5.02 sealed rows in current view.");
      setTimeout(() => setActionMessage(null), 1800);
      return;
    }
    if (!confirm(`Mark ${targets.length} SEC Item 5.02 rows as REJECTED (drop from feed)?`)) return;
    setBulkRunning(true);
    let done = 0;
    for (const r of targets) {
      await patchRow(r.id, { status: "rejected" }, `bulk ${++done}/${targets.length}`);
    }
    setBulkRunning(false);
    setActionMessage(`✓ bulk-rejected ${done} SEC 5.02 rows`);
    setTimeout(() => setActionMessage(null), 2400);
  }

  async function patchRow(id, patch, label) {
    setActionMessage(`Saving ${label}…`);
    try {
      const res = await fetch(`/api/admin/intel?id=${encodeURIComponent(id)}`, {
        method: "PATCH",
        headers: {
          "Content-Type": "application/json",
          Authorization: `Bearer ${token}`,
        },
        body: JSON.stringify(patch),
      });
      if (!res.ok) {
        const body = await res.text();
        throw new Error(`${res.status}: ${body.slice(0, 120)}`);
      }
      setActionMessage(`✓ ${label}`);
      // optimistic local update — patch the row in place
      setRows((prev) => prev.filter((r) => r.id !== id));
      setTimeout(() => setActionMessage(null), 1800);
    } catch (e) {
      setActionMessage(`✗ ${e.message || e}`);
    }
  }

  function actionsFor(r) {
    return (
      <div className="admin-actions">
        <button className="btn-admin btn-verify"
                onClick={() => patchRow(r.id, { status: "verified" }, "verified")}>
          ✓ Verify
        </button>
        <button className="btn-admin btn-publish"
                onClick={() => patchRow(r.id, {
                  status: "verified",
                  visibility: "public",
                  published_at: new Date().toISOString(),
                }, "published")}>
          ✓ Verify + Publish
        </button>
        <button className="btn-admin btn-reject"
                onClick={() => patchRow(r.id, { status: "rejected" }, "rejected")}>
          ✗ Reject
        </button>
        <button className="btn-admin btn-dupe"
                onClick={() => patchRow(r.id, { status: "duplicate" }, "duplicate")}>
          ⊙ Duplicate
        </button>
      </div>
    );
  }

  // Token gate. Probe /api/admin/dashboard with the supplied token before
  // persisting — otherwise any string lets the UI render with silent 401s
  // on every fetch, which looks like "logged in but data is empty."
  if (!token) {
    return (
      <div className="admin-page">
        <section className="admin-hero">
          <div className="admin-hero-inner">
            <h1>Editor sign-in</h1>
            <p>Enter the admin token to review intel submissions.</p>
            <form
              onSubmit={async (e) => {
                e.preventDefault();
                const candidate = tokenInput.trim();
                if (!candidate) return;
                setSignInChecking(true);
                setSignInError(null);
                try {
                  const r = await fetch(`/api/admin/dashboard`, {
                    headers: { Authorization: `Bearer ${candidate}` },
                  });
                  if (r.status === 401) {
                    setSignInError("Token rejected — check the value in Vercel env (ADMIN_TOKEN).");
                    setSignInChecking(false);
                    return;
                  }
                  if (!r.ok) {
                    setSignInError(`Sign-in probe returned ${r.status}. Try again or check server logs.`);
                    setSignInChecking(false);
                    return;
                  }
                  localStorage.setItem("sc_admin_token", candidate);
                  setToken(candidate);
                } catch (err) {
                  setSignInError(`Network error: ${err.message || err}`);
                  setSignInChecking(false);
                }
              }}
            >
              <input
                type="password"
                value={tokenInput}
                onChange={(e) => { setTokenInput(e.target.value); setSignInError(null); }}
                placeholder="Admin token"
                autoFocus
                disabled={signInChecking}
              />
              <button type="submit" className="btn-primary" disabled={signInChecking}>
                {signInChecking ? "Checking…" : "Sign in"}
              </button>
            </form>
            {signInError && (
              <p style={{ marginTop: 16, color: "#d33", fontSize: 14 }}>
                {signInError}
              </p>
            )}
            <p style={{ marginTop: 24, color: "var(--ink-soft)", fontSize: 13 }}>
              Or visit <code>#/admin?token=YOUR_TOKEN</code> to set it from the URL.
            </p>
          </div>
        </section>
      </div>
    );
  }

  return (
    <div className="admin-page">
      {authError && (
        <div style={{
          background: "#fce8e8", borderBottom: "2px solid #d33",
          color: "#7a1e1e", padding: "10px 16px", fontSize: 14,
          display: "flex", justifyContent: "space-between", alignItems: "center",
        }}>
          <span><strong>Auth error:</strong> {authError}</span>
          <button
            className="btn-ghost"
            style={{ fontSize: 13, padding: "4px 10px" }}
            onClick={() => {
              localStorage.removeItem("sc_admin_token");
              setToken("");
              setAuthError(null);
            }}
          >Sign out</button>
        </div>
      )}
      <section className="admin-hero">
        <div className="admin-hero-inner">
          <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", flexWrap: "wrap", gap: 16 }}>
            <div>
              <h1>Editor console</h1>
              <p>{rows.length} rows · {statusFilter}{carrierFilter ? ` · ${carrierFilter}` : ""}</p>
            </div>
            <div className="admin-hero-actions">
              <button className="btn-ghost" onClick={() => load()}>Refresh</button>
              <button className="btn-ghost" onClick={bulkAcceptSEC502} disabled={bulkRunning}>
                {bulkRunning ? "Bulk…" : "Bulk-reject SEC 5.02"}
              </button>
              <button className="btn-ghost" onClick={() => {
                localStorage.removeItem("sc_admin_token");
                setToken("");
              }}>Sign out</button>
              <button className="btn-ghost" onClick={() => onNav("home")}>Home</button>
            </div>
          </div>

          {/* Admin tab nav — grouped by function for fast scanning. Each group
              gets a small uppercase mono label; tabs are pill buttons with a
              clear active state. Adding a new tab = add a row to ADMIN_NAV. */}
          {(() => {
            const ADMIN_NAV = [
              { group: "Pulse", tabs: [
                { id: "overview", label: "Overview", icon: "▦" },
              ]},
              { group: "Editorial", tabs: [
                { id: "list",     label: "Intel",        icon: "✎" },
                { id: "tray",     label: "Quick review", icon: "⚡" },
                { id: "drafts",   label: "Drafts",       icon: "📝" },
                { id: "alerts",   label: "Alerts",       icon: "◉" },
                { id: "voices",   label: "Voices",       icon: "🗣" },
              ]},
              { group: "Audience", tabs: [
                { id: "subs",     label: "Subscribers",  icon: "✉" },
                { id: "contacts", label: "Contacts",     icon: "☎" },
                { id: "vetting",  label: "Vetting",      icon: "⚿" },
                { id: "verdicts", label: "Verdicts",     icon: "⚖" },
              ]},
              { group: "Production", tabs: [
                { id: "video",    label: "Video studio", icon: "▶" },
                { id: "share",    label: "Share cards",  icon: "□" },
              ]},
            ];
            return (
              <nav className="admin-nav">
                {ADMIN_NAV.map((g) => (
                  <div key={g.group} className="admin-nav-group">
                    <div className="admin-nav-label">{g.group}</div>
                    <div className="admin-nav-tabs">
                      {g.tabs.map((t) => (
                        <button
                          key={t.id}
                          className={"admin-nav-tab" + (mode === t.id ? " is-on" : "")}
                          onClick={() => setMode(t.id)}
                          aria-current={mode === t.id ? "page" : undefined}
                        >
                          <span className="admin-nav-tab-ic" aria-hidden="true">{t.icon}</span>
                          <span>{t.label}</span>
                        </button>
                      ))}
                    </div>
                  </div>
                ))}
              </nav>
            );
          })()}

          <div className="admin-filters">
            <label>
              Status:
              <select value={statusFilter} onChange={(e) => setStatusFilter(e.target.value)}>
                <option value="pending">Pending</option>
                <option value="verified">Verified</option>
                <option value="rejected">Rejected</option>
                <option value="duplicate">Duplicate</option>
                <option value="all">All</option>
              </select>
            </label>
            <label>
              Carrier:
              <select value={carrierFilter} onChange={(e) => setCarrierFilter(e.target.value)}>
                <option value="">All carriers</option>
                {carriers.map(c => <option key={c} value={c}>{c}</option>)}
              </select>
            </label>
          </div>
        </div>
      </section>

      {actionMessage && <div className="admin-toast">{actionMessage}</div>}

      {mode === "tray" && (
        <section className="admin-list">
          <div className="admin-list-inner" style={{ maxWidth: 820 }}>
            {error && <div className="admin-error">{error}</div>}
            {loading && <div className="admin-loading">Loading…</div>}
            {!loading && rows.length === 0 && !error && (
              <div className="admin-empty">Inbox empty for this filter. ✓</div>
            )}
            {rows.length > 0 && (() => {
              const r = rows[trayIdx] || rows[0];
              return (
                <article className="admin-row" style={{ position: "relative", padding: "24px 28px" }}>
                  <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 12 }}>
                    <span style={{ fontFamily: "var(--font-mono)", fontSize: 11, letterSpacing: "0.14em", color: "var(--ink-soft)", textTransform: "uppercase" }}>
                      {trayIdx + 1} of {rows.length}
                    </span>
                    <span style={{ fontFamily: "var(--font-mono)", fontSize: 10, letterSpacing: "0.12em", color: "var(--ink-soft)" }}>
                      <kbd>J</kbd>/<kbd>K</kbd> nav · <kbd>P</kbd> publish · <kbd>V</kbd> verify-sealed · <kbd>R</kbd> reject · <kbd>D</kbd> dupe · <kbd>S</kbd> skip
                    </span>
                  </div>
                  <div className="admin-row-head">
                    <span className="admin-tag" data-sev={r.severity || "low"}>
                      {(r.severity || "—").toUpperCase()}
                    </span>
                    <span className="admin-meta">
                      <strong>{r.parsed_carrier || "—"}</strong> · {r.parsed_intel_type || "—"} · conf {r.llm_confidence?.toFixed?.(2) || "—"}
                    </span>
                    <span className="admin-meta admin-meta-right">
                      {r.parsed_city || "—"} · {new Date(r.created_at).toLocaleString()}
                    </span>
                  </div>
                  <p className="admin-row-text" style={{ fontSize: 16, lineHeight: 1.55, padding: "16px 0" }}>
                    {r.raw_text}
                  </p>
                  {r.notes && (
                    <p style={{ fontFamily: "var(--font-mono)", fontSize: 11, color: "var(--ink-soft)", margin: "0 0 12px" }}>
                      notes: {r.notes}
                    </p>
                  )}
                  {r.source_url && (
                    <p style={{ fontSize: 12, marginBottom: 12 }}>
                      <a href={r.source_url} target="_blank" rel="noopener noreferrer" style={{ color: "var(--ink-soft)" }}>
                        Source ↗
                      </a>
                    </p>
                  )}
                  <div className="admin-row-foot">
                    <span className="admin-status">
                      source=<strong>{r.source}</strong> · status=<strong>{r.status}</strong> · vis=<strong>{r.visibility}</strong>
                    </span>
                    {actionsFor(r)}
                  </div>
                </article>
              );
            })()}
          </div>
        </section>
      )}

      {mode === "overview" && (
        <section className="admin-list">
          <div className="admin-list-inner" style={{ maxWidth: 1240 }}>
            {dashLoading && !dash && <div className="admin-loading">Loading dashboard…</div>}
            {dash && (() => {
              const kpis    = dash.kpis     || {};
              const people  = dash.people   || {};
              const money   = dash.money    || {};
              const ed      = dash.editorial|| {};
              const sigs    = dash.signals  || {};
              const distress = dash.distress|| {};
              const graph   = dash.graph    || {};
              const ingest  = Array.isArray(dash.ingest_health) ? dash.ingest_health : [];

              const fmtMoney = (cents) => `$${((cents || 0) / 100).toLocaleString(undefined, { minimumFractionDigits: 0, maximumFractionDigits: 0 })}`;
              const fmt = (n) => (n == null ? "—" : Number(n).toLocaleString());
              const recent = (dash.recent || []).filter(ev =>
                recentTypeFilter === "all" ? true : ev.type === recentTypeFilter
              );
              const typeLabel = {
                risk_report: "Risk Report",
                carrier_sub: "Carrier Sub",
                member: "Member",
                subscriber: "Subscriber",
                load_post: "Load Post",
                load_interest: "Load Interest",
                intel: "Intel",
                video: "Video",
              };

              const sevColors = {
                critical: "#b91c1c", high: "#dc2626", moderate: "#f59e0b", low: "#6b7280", "(null)": "#9ca3af",
              };

              // ── Time-series helpers ────────────────────────────
              const trends = dash.trends || {};
              const Sparkline = ({ data, color = "#1d4ed8", height = 28, fill = true }) => {
                if (!Array.isArray(data) || data.length === 0) return <div style={{ height }} />;
                const max = Math.max(1, ...data);
                const w = 100, h = height;
                const stepX = w / Math.max(data.length - 1, 1);
                const pts = data.map((v, i) => `${(i * stepX).toFixed(2)},${(h - (v / max) * (h - 2) - 1).toFixed(2)}`);
                return (
                  <svg viewBox={`0 0 ${w} ${h}`} preserveAspectRatio="none" style={{ width: "100%", height, display: "block" }}>
                    {fill && <polygon points={`0,${h} ${pts.join(" ")} ${w},${h}`} fill={color} fillOpacity="0.12" />}
                    <polyline points={pts.join(" ")} fill="none" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
                  </svg>
                );
              };

              const HourBars = ({ data, severityCounts }) => {
                if (!Array.isArray(data) || data.length === 0) {
                  return <div style={{ height: 80, padding: 16, textAlign: "center", color: "var(--ink-soft)", fontSize: 12 }}>No intel volume in the last 24h.</div>;
                }
                const max = Math.max(1, ...data);
                const W = 100, H = 80;
                const colW = W / data.length;
                const gap = 0.4;
                const critPct = (severityCounts?.critical || 0) / Math.max(1, data.reduce((a, b) => a + b, 0));
                const highPct = (severityCounts?.high     || 0) / Math.max(1, data.reduce((a, b) => a + b, 0));
                return (
                  <svg viewBox={`0 0 ${W} ${H}`} preserveAspectRatio="none" style={{ width: "100%", height: 80, display: "block" }}>
                    {data.map((v, i) => {
                      const barH = (v / max) * (H - 4);
                      const x = i * colW + gap;
                      const y = H - barH;
                      // Color the most-recent 6 hours (right edge) more intensely
                      const fill = i >= data.length - 6 ? "#dc2626" : i >= data.length - 12 ? "#f59e0b" : "#94a3b8";
                      return <rect key={i} x={x} y={y} width={colW - gap * 2} height={barH} fill={fill} rx="0.6" />;
                    })}
                  </svg>
                );
              };

              const HBar = ({ value, max, color }) => {
                const pct = Math.max(2, Math.min(100, (value / Math.max(max, 1)) * 100));
                return (
                  <div style={{ height: 6, background: "var(--rule-soft, #f0efe9)", borderRadius: 3, overflow: "hidden" }}>
                    <div style={{ width: `${pct}%`, height: "100%", background: color, borderRadius: 3 }} />
                  </div>
                );
              };

              // Section header — matches the existing admin tone
              const SectionH = ({ children, action }) => (
                <div style={{ display: "flex", alignItems: "baseline", justifyContent: "space-between", marginTop: 28, marginBottom: 10, flexWrap: "wrap", gap: 8 }}>
                  <h3 style={{ fontSize: 12, textTransform: "uppercase", letterSpacing: "0.16em", color: "var(--ink-soft)", margin: 0, fontWeight: 700 }}>
                    {children}
                  </h3>
                  {action}
                </div>
              );

              const Tile = ({ label, value, sub, color, onClick }) => (
                <article className="admin-row" style={{ padding: "14px 16px", cursor: onClick ? "pointer" : "default" }} onClick={onClick}>
                  <div style={{ fontSize: 10, textTransform: "uppercase", letterSpacing: "0.14em", color: "var(--ink-soft)", marginBottom: 6, fontWeight: 600 }}>{label}</div>
                  <div style={{ fontSize: 24, fontWeight: 700, lineHeight: 1, marginBottom: 4, color: color || "var(--ink)" }}>{value}</div>
                  {sub && <div style={{ fontSize: 11, color: "var(--ink-soft)" }}>{sub}</div>}
                </article>
              );

              // "Needs your attention" hero — clickable cards that route to the
              // correct admin tab, glow red when non-zero.
              const alertsPending = (dash.alerts_pending != null) ? dash.alerts_pending : 0;
              const critical24    = (trends.intel_24h_by_severity?.critical || 0) + (trends.intel_24h_by_severity?.high || 0);
              const distress24    = (distress.nlrb_24h || 0) + (distress.warn_24h || 0) + (distress.bankruptcy_24h || 0);
              const NeedsCard = ({ label, value, ctaLabel, color, onClick }) => {
                const hot = (value || 0) > 0;
                const accent = hot ? color : "#16a34a";
                return (
                  <button onClick={onClick} style={{
                    textAlign: "left", padding: "16px 18px", border: `1.5px solid ${hot ? accent + "55" : "var(--rule)"}`,
                    background: hot ? `linear-gradient(135deg, ${accent}10 0%, #fff 100%)` : "#fff",
                    borderRadius: 8, cursor: "pointer", display: "block", width: "100%",
                    boxShadow: hot ? `0 0 0 4px ${accent}10` : "none", transition: "all 0.15s",
                  }}>
                    <div style={{ display: "flex", alignItems: "center", gap: 6, fontSize: 10, fontWeight: 700, letterSpacing: "0.14em", textTransform: "uppercase", color: accent, marginBottom: 6 }}>
                      <span style={{ display: "inline-block", width: 6, height: 6, borderRadius: "50%", background: accent, animation: hot ? "blinkDot 1.6s ease-in-out infinite" : "none" }} />
                      {label}
                    </div>
                    <div style={{ fontSize: 30, fontWeight: 800, lineHeight: 1, color: hot ? accent : "var(--ink-soft)" }}>{value || (hot ? value : "0")}</div>
                    <div style={{ fontSize: 12, color: "var(--ink-soft)", marginTop: 6 }}>{hot ? ctaLabel : "✓ all clear"}</div>
                  </button>
                );
              };

              return (
                <>
                  {/* ── NEEDS YOUR ATTENTION ───────────────────── */}
                  <style>{`@keyframes blinkDot{0%,100%{opacity:1}50%{opacity:0.35}}`}</style>
                  <div style={{ marginBottom: 18, padding: "16px 18px", background: "linear-gradient(135deg, #fff8ea 0%, #fffaf3 60%, #fff 100%)", border: "1px solid #fde4cf", borderLeft: "4px solid #b45309", borderRadius: 8 }}>
                    <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 12, flexWrap: "wrap", gap: 8 }}>
                      <div style={{ fontSize: 11, fontWeight: 700, letterSpacing: "0.16em", textTransform: "uppercase", color: "#92400e" }}>
                        ⚡ Needs your attention today
                      </div>
                      <div style={{ fontSize: 11, color: "var(--ink-soft)" }}>
                        Generated {dash.generatedAt ? new Date(dash.generatedAt).toLocaleTimeString() : "—"}
                        <button className="btn-ghost" style={{ marginLeft: 10, fontSize: 11, padding: "3px 10px" }} onClick={loadDashboard}>↻ Refresh</button>
                      </div>
                    </div>
                    <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(190px, 1fr))", gap: 10 }}>
                      <NeedsCard label="Pending alerts"   value={alertsPending}            color="#b45309" ctaLabel="→ Review now" onClick={() => setMode("alerts")} />
                      <NeedsCard label="Members to vet"   value={people.pending_vetting}   color="#b45309" ctaLabel="→ Open Vetting" onClick={() => setMode("vetting")} />
                      <NeedsCard label="Critical · 24h"   value={critical24}               color="#b91c1c" ctaLabel="→ Inspect Intel" onClick={() => setMode("list")} />
                      <NeedsCard label="Distress · 24h"   value={distress24}               color="#b91c1c" ctaLabel="→ See breakdown below" onClick={() => {}} />
                    </div>
                  </div>

                  {/* ── HERO KPI STRIP — now with sparklines ───── */}
                  <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(220px, 1fr))", gap: 12, marginBottom: 12 }}>
                    <article className="admin-row" style={{ padding: "16px 18px 12px", background: "linear-gradient(135deg, #fafaf7 0%, #fff 100%)" }}>
                      <div style={{ fontSize: 10, textTransform: "uppercase", letterSpacing: "0.16em", color: "#1d4ed8", marginBottom: 6, fontWeight: 700 }}>👥 Members</div>
                      <div style={{ display: "flex", alignItems: "baseline", justifyContent: "space-between", gap: 8, marginBottom: 4 }}>
                        <div style={{ fontSize: 32, fontWeight: 800, lineHeight: 1 }}>{fmt(kpis.members)}</div>
                        <div style={{ fontSize: 11, color: "#1d4ed8", fontWeight: 600 }}>+{fmt(people.members_week)} <span style={{ color: "var(--ink-soft)", fontWeight: 400 }}>wk</span></div>
                      </div>
                      <Sparkline data={trends.members_7d_daily || []} color="#1d4ed8" />
                      <div style={{ fontSize: 11, color: "var(--ink-soft)", marginTop: 4 }}>{fmt(people.pending_vetting)} pending vetting</div>
                    </article>
                    <article className="admin-row" style={{ padding: "16px 18px 12px", background: "linear-gradient(135deg, #fafaf7 0%, #fff 100%)" }}>
                      <div style={{ fontSize: 10, textTransform: "uppercase", letterSpacing: "0.16em", color: "#16a34a", marginBottom: 6, fontWeight: 700 }}>📬 Subscribers</div>
                      <div style={{ display: "flex", alignItems: "baseline", justifyContent: "space-between", gap: 8, marginBottom: 4 }}>
                        <div style={{ fontSize: 32, fontWeight: 800, lineHeight: 1 }}>{fmt(kpis.subscribers)}</div>
                        <div style={{ fontSize: 11, color: "#16a34a", fontWeight: 600 }}>+{fmt(people.newsletter_week)} <span style={{ color: "var(--ink-soft)", fontWeight: 400 }}>wk</span></div>
                      </div>
                      <Sparkline data={trends.subs_7d_daily || []} color="#16a34a" />
                      <div style={{ fontSize: 11, color: "var(--ink-soft)", marginTop: 4 }}>{fmt(people.newsletter_total)} newsletter · {fmt(people.pulse_total)} pulse</div>
                    </article>
                    <article className="admin-row" style={{ padding: "16px 18px 12px", background: "linear-gradient(135deg, #fafaf7 0%, #fff 100%)" }}>
                      <div style={{ fontSize: 10, textTransform: "uppercase", letterSpacing: "0.16em", color: "#b45309", marginBottom: 6, fontWeight: 700 }}>💰 MRR</div>
                      <div style={{ display: "flex", alignItems: "baseline", justifyContent: "space-between", gap: 8, marginBottom: 4 }}>
                        <div style={{ fontSize: 32, fontWeight: 800, lineHeight: 1 }}>{fmtMoney(kpis.mrr_cents)}</div>
                        <div style={{ fontSize: 11, color: "#b45309", fontWeight: 600 }}>{fmt(money.active_subs)} <span style={{ color: "var(--ink-soft)", fontWeight: 400 }}>subs</span></div>
                      </div>
                      <div style={{ height: 28, display: "flex", alignItems: "center", fontSize: 11, color: "var(--ink-soft)" }}>{fmtMoney(money.revenue_30d_cents)} collected last 30d</div>
                      <div style={{ fontSize: 11, color: "var(--ink-soft)", marginTop: 4 }}>{fmt(money.paid_risk_reports)} paid reports · {fmtMoney(money.paid_risk_revenue_cents)} all-time</div>
                    </article>
                    <article className="admin-row" style={{ padding: "16px 18px 12px", background: "linear-gradient(135deg, #fafaf7 0%, #fff 100%)" }}>
                      <div style={{ fontSize: 10, textTransform: "uppercase", letterSpacing: "0.16em", color: "#b91c1c", marginBottom: 6, fontWeight: 700 }}>📡 Signals · 24h</div>
                      <div style={{ display: "flex", alignItems: "baseline", justifyContent: "space-between", gap: 8, marginBottom: 4 }}>
                        <div style={{ fontSize: 32, fontWeight: 800, lineHeight: 1 }}>{fmt(kpis.signals_24h)}</div>
                        <div style={{ fontSize: 11, color: "#b91c1c", fontWeight: 600 }}>{critical24} <span style={{ color: "var(--ink-soft)", fontWeight: 400 }}>crit/high</span></div>
                      </div>
                      <Sparkline data={trends.intel_24h_hourly || []} color="#b91c1c" />
                      <div style={{ fontSize: 11, color: "var(--ink-soft)", marginTop: 4 }}>{fmt(sigs.intel_24h)} intel · {fmt(distress24)} distress events</div>
                    </article>
                  </div>

                  {/* ── 24-HOUR PULSE ──────────────────────────── */}
                  <article className="admin-row" style={{ padding: "16px 20px 12px", marginBottom: 14 }}>
                    <div style={{ display: "flex", alignItems: "baseline", justifyContent: "space-between", marginBottom: 8, flexWrap: "wrap", gap: 8 }}>
                      <div style={{ fontSize: 11, fontWeight: 700, letterSpacing: "0.16em", textTransform: "uppercase", color: "var(--ink-soft)" }}>
                        Operator-network pulse · last 24 hours
                      </div>
                      <div style={{ fontSize: 11, color: "var(--ink-soft)" }}>
                        {Object.entries(trends.intel_24h_by_severity || {}).filter(([_, v]) => v > 0).map(([s, n]) => (
                          <span key={s} style={{ marginLeft: 10, color: sevColors[s], fontWeight: 600, textTransform: "capitalize" }}>{s} {n}</span>
                        ))}
                      </div>
                    </div>
                    <HourBars data={trends.intel_24h_hourly || []} severityCounts={trends.intel_24h_by_severity} />
                    <div style={{ display: "flex", justifyContent: "space-between", fontSize: 10, color: "var(--ink-soft)", marginTop: 4 }}>
                      <span>24h ago</span>
                      <span>12h</span>
                      <span>6h</span>
                      <span>now ●</span>
                    </div>
                  </article>

                  {/* ── PEOPLE ─────────────────────────────────── */}
                  <SectionH action={<button className="btn-ghost" onClick={loadDashboard} style={{ fontSize: 11, padding: "3px 10px" }}>Refresh all</button>}>People</SectionH>
                  <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 12, marginBottom: 12 }}>
                    <article className="admin-row" style={{ padding: "16px 18px" }}>
                      <div style={{ fontSize: 12, fontWeight: 600, marginBottom: 10, color: "var(--ink-soft)" }}>Members by role</div>
                      {Object.entries(people.members_by_role || {}).sort((a, b) => b[1] - a[1]).map(([role, n]) => (
                        <div key={role} style={{ display: "flex", justifyContent: "space-between", padding: "4px 0", fontSize: 13, borderBottom: "1px solid var(--rule-soft, #f0efe9)" }}>
                          <span style={{ textTransform: "capitalize" }}>{role.replace(/_/g, " ")}</span>
                          <span style={{ fontWeight: 600 }}>{fmt(n)}</span>
                        </div>
                      ))}
                      {Object.keys(people.members_by_role || {}).length === 0 && (
                        <div style={{ fontSize: 12, color: "var(--ink-soft)" }}>No members yet.</div>
                      )}
                    </article>
                    <article className="admin-row" style={{ padding: "16px 18px" }}>
                      <div style={{ fontSize: 12, fontWeight: 600, marginBottom: 10, color: "var(--ink-soft)" }}>Members by status</div>
                      {Object.entries(people.members_by_status || {}).sort((a, b) => b[1] - a[1]).map(([status, n]) => {
                        const color = status === "auto_verified" || status === "verified" ? "#15803d"
                                    : status === "rejected" ? "#dc2626"
                                    : status === "pending"  ? "#b45309" : "var(--ink)";
                        return (
                          <div key={status} style={{ display: "flex", justifyContent: "space-between", padding: "4px 0", fontSize: 13, borderBottom: "1px solid var(--rule-soft, #f0efe9)" }}>
                            <span style={{ textTransform: "capitalize" }}>{status.replace(/_/g, " ")}</span>
                            <span style={{ fontWeight: 600, color }}>{fmt(n)}</span>
                          </div>
                        );
                      })}
                      {(people.pending_vetting || 0) > 0 && (
                        <button className="btn-ghost" onClick={() => setMode("vetting")} style={{ marginTop: 10, fontSize: 11, padding: "4px 10px", color: "#b45309", border: "1px solid #fde4cf" }}>
                          → Open Vetting tab ({fmt(people.pending_vetting)} pending)
                        </button>
                      )}
                    </article>
                  </div>
                  <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(180px, 1fr))", gap: 10, marginBottom: 4 }}>
                    <Tile label="Newsletter signups" value={fmt(people.newsletter_total)} sub={`+${fmt(people.newsletter_week)} this week`} />
                    <Tile label="Pulse subscribers"  value={fmt(people.pulse_total)}      sub={`+${fmt(people.pulse_week)} this week`} />
                    <Tile label="Members today"      value={fmt(people.members_today)}    sub={`+${fmt(people.members_week)} this week`} />
                    <Tile label="Driver pins"        value={fmt(people.drivers_pinned)}   sub="carrier_pin_submissions" />
                  </div>
                  {Object.keys(people.newsletter_by_source || {}).length > 0 && (
                    <article className="admin-row" style={{ padding: "12px 18px", marginTop: 8 }}>
                      <div style={{ fontSize: 11, textTransform: "uppercase", letterSpacing: "0.14em", color: "var(--ink-soft)", marginBottom: 8, fontWeight: 600 }}>Newsletter signups by source</div>
                      <div style={{ display: "flex", flexWrap: "wrap", gap: 14, fontSize: 12 }}>
                        {Object.entries(people.newsletter_by_source).sort((a, b) => b[1] - a[1]).slice(0, 12).map(([src, n]) => (
                          <div key={src}><b>{fmt(n)}</b> <span style={{ color: "var(--ink-soft)" }}>{src}</span></div>
                        ))}
                      </div>
                    </article>
                  )}

                  {/* ── MONEY ──────────────────────────────────── */}
                  <SectionH>Money</SectionH>
                  <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(180px, 1fr))", gap: 10 }}>
                    <Tile label="Risk reports paid"  value={fmt(money.paid_risk_reports)}     sub={fmtMoney(money.paid_risk_revenue_cents) + " collected"} />
                    <Tile label="Risk reports pending" value={fmt(money.pending_risk_reports)} sub="awaiting payment" />
                    <Tile label="Active carrier subs"  value={fmt(money.active_subs)}          sub={fmtMoney(money.mrr_cents) + "/mo MRR"} />
                    <Tile label="Revenue · last 30d"   value={fmtMoney(money.revenue_30d_cents)} sub="risk reports only" />
                  </div>

                  {/* ── EDITORIAL ──────────────────────────────── */}
                  <SectionH action={<button className="btn-ghost" onClick={() => setMode("drafts")} style={{ fontSize: 11, padding: "3px 10px" }}>Open Drafts →</button>}>Editorial pipeline</SectionH>
                  <div style={{ display: "grid", gridTemplateColumns: "2fr 1fr 1fr", gap: 10 }}>
                    <article className="admin-row" style={{ padding: "16px 18px" }}>
                      <div style={{ fontSize: 11, textTransform: "uppercase", letterSpacing: "0.14em", color: "var(--ink-soft)", marginBottom: 8, fontWeight: 600 }}>Latest editorial draft</div>
                      {ed.latest_draft ? (
                        <>
                          <div style={{ fontWeight: 600, fontSize: 14, marginBottom: 4 }}>{ed.latest_draft.title}</div>
                          <div style={{ fontSize: 12, color: "var(--ink-soft)", marginBottom: 8 }}>{ed.latest_draft.preview}…</div>
                          <button className="btn-ghost" onClick={() => setMode("drafts")} style={{ fontSize: 11, padding: "3px 10px" }}>Read draft →</button>
                        </>
                      ) : (
                        <div style={{ fontSize: 12, color: "var(--ink-soft)" }}>No drafts yet — first scan fires after the next ingest at ~10:00 UTC.</div>
                      )}
                    </article>
                    <Tile label="Drafts in queue"     value={fmt(ed.drafts_count)} />
                    <Tile label="Videos · published" value={`${fmt(ed.videos_published)} / ${fmt(ed.videos_total)}`} sub="published / total" />
                  </div>

                  {/* ── OPERATOR NETWORK ───────────────────────── */}
                  <SectionH action={<button className="btn-ghost" onClick={() => setMode("list")} style={{ fontSize: 11, padding: "3px 10px" }}>Open Intel →</button>}>Operator network · intel</SectionH>
                  <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(160px, 1fr))", gap: 10, marginBottom: 10 }}>
                    <Tile label="Intel · 24h"   value={fmt(sigs.intel_24h)} />
                    <Tile label="Intel · 7d"    value={fmt(sigs.intel_7d)} />
                    <Tile label="Intel · 30d"   value={fmt(sigs.intel_30d)} />
                    <Tile label="Intel · total" value={fmt(sigs.intel_total)} />
                  </div>
                  <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr", gap: 10 }}>
                    <article className="admin-row" style={{ padding: "14px 18px" }}>
                      <div style={{ fontSize: 11, textTransform: "uppercase", letterSpacing: "0.14em", color: "var(--ink-soft)", marginBottom: 8, fontWeight: 600 }}>Severity · 7d</div>
                      {Object.entries(sigs.intel_by_severity || {}).sort((a, b) => b[1] - a[1]).map(([sev, n]) => (
                        <div key={sev} style={{ display: "flex", justifyContent: "space-between", padding: "3px 0", fontSize: 13 }}>
                          <span style={{ color: sevColors[sev] || "var(--ink)", textTransform: "capitalize", fontWeight: 600 }}>{sev}</span>
                          <span>{fmt(n)}</span>
                        </div>
                      ))}
                      {Object.keys(sigs.intel_by_severity || {}).length === 0 && (
                        <div style={{ fontSize: 12, color: "var(--ink-soft)" }}>No intel last 7d.</div>
                      )}
                    </article>
                    <article className="admin-row" style={{ padding: "14px 18px" }}>
                      <div style={{ fontSize: 11, textTransform: "uppercase", letterSpacing: "0.14em", color: "var(--ink-soft)", marginBottom: 8, fontWeight: 600 }}>Visibility · 7d</div>
                      {Object.entries(sigs.intel_by_visibility || {}).sort((a, b) => b[1] - a[1]).map(([v, n]) => (
                        <div key={v} style={{ display: "flex", justifyContent: "space-between", padding: "3px 0", fontSize: 13 }}>
                          <span style={{ textTransform: "capitalize" }}>{v.replace(/_/g, " ")}</span>
                          <span>{fmt(n)}</span>
                        </div>
                      ))}
                      {Object.keys(sigs.intel_by_visibility || {}).length === 0 && (
                        <div style={{ fontSize: 12, color: "var(--ink-soft)" }}>—</div>
                      )}
                    </article>
                    <article className="admin-row" style={{ padding: "14px 18px" }}>
                      <div style={{ fontSize: 11, textTransform: "uppercase", letterSpacing: "0.14em", color: "var(--ink-soft)", marginBottom: 8, fontWeight: 600 }}>Top carriers · 7d</div>
                      {(sigs.top_carriers_7d || []).map(({ carrier, n }) => (
                        <div key={carrier} style={{ display: "flex", justifyContent: "space-between", padding: "3px 0", fontSize: 13, gap: 12 }}>
                          <span style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{carrier}</span>
                          <span style={{ fontWeight: 600 }}>{fmt(n)}</span>
                        </div>
                      ))}
                      {(sigs.top_carriers_7d || []).length === 0 && (
                        <div style={{ fontSize: 12, color: "var(--ink-soft)" }}>No tagged carriers yet.</div>
                      )}
                    </article>
                  </div>

                  {/* ── DISTRESS · 24h ─────────────────────────── */}
                  <SectionH>Distress signals · last 24h</SectionH>
                  <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(140px, 1fr))", gap: 10 }}>
                    <Tile label="NLRB filings"        value={fmt(distress.nlrb_24h)}   color={distress.nlrb_24h ? "#b91c1c" : undefined} />
                    <Tile label="WARN notices"        value={fmt(distress.warn_24h)}   color={distress.warn_24h ? "#b91c1c" : undefined} />
                    <Tile label="Bankruptcies"        value={fmt(distress.bankruptcy_24h)} color={distress.bankruptcy_24h ? "#b91c1c" : undefined} />
                    <Tile label="Civil litigation"    value={fmt(distress.litigation_24h)} />
                    <Tile label="OSHA injuries"       value={fmt(distress.osha_24h)} />
                    <Tile label="Bond cancellations"  value={fmt(distress.bond_pending_cancel)} sub="pending" />
                    <Tile label="Locks · closed now"  value={fmt(distress.locks_closed)} />
                  </div>

                  {/* ── DATA GRAPH ─────────────────────────────── */}
                  <SectionH>Data graph · totals</SectionH>
                  <article className="admin-row" style={{ padding: "16px 18px" }}>
                    <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(120px, 1fr))", gap: 14 }}>
                      {[
                        ["Shippers",       graph.shippers],
                        ["Carriers",       graph.carriers],
                        ["Facilities",     graph.facilities],
                        ["Lanes (DAT)",    graph.lanes],
                        ["Receivers",      graph.receivers],
                        ["Refineries",     graph.refineries],
                        ["Weigh stations", graph.weigh_stations],
                        ["Repair shops",   graph.repair_shops],
                        ["Carrier terminals", graph.terminals],
                      ].map(([label, n]) => (
                        <div key={label}>
                          <div style={{ fontSize: 11, color: "var(--ink-soft)", marginBottom: 4 }}>{label}</div>
                          <div style={{ fontSize: 18, fontWeight: 600 }}>{fmt(n)}</div>
                        </div>
                      ))}
                    </div>
                  </article>

                  {/* ── INGEST HEALTH ──────────────────────────── */}
                  {ingest.length > 0 && (
                    <>
                      <SectionH>Ingest health · last runs</SectionH>
                      <article className="admin-row" style={{ padding: "10px 0", overflow: "hidden" }}>
                        {ingest.slice(0, 12).map((row, i) => {
                          const ageMs = row.last_run_at ? (Date.now() - new Date(row.last_run_at).getTime()) : null;
                          const ageHr = ageMs != null ? (ageMs / 3600 / 1000) : null;
                          const stale = ageHr != null && ageHr > 36;
                          const ok    = (row.last_status || "").toLowerCase().includes("ok") || (row.last_status || "").toLowerCase().includes("success");
                          return (
                            <div key={i} style={{ display: "grid", gridTemplateColumns: "1fr auto auto auto", gap: 12, padding: "8px 18px", fontSize: 12, borderBottom: i === Math.min(ingest.length, 12) - 1 ? "none" : "1px solid var(--rule-soft, #f0efe9)", alignItems: "center" }}>
                              <span style={{ fontFamily: "var(--font-mono)" }}>{row.source || row.pipeline || row.id}</span>
                              <span style={{ fontSize: 11, color: "var(--ink-soft)" }}>rows: {fmt(row.rows_in || row.rows || 0)}</span>
                              <span style={{ fontSize: 11, color: ok ? "#15803d" : stale ? "#b91c1c" : "var(--ink-soft)" }}>{row.last_status || "—"}</span>
                              <span style={{ fontSize: 11, color: stale ? "#b91c1c" : "var(--ink-soft)", whiteSpace: "nowrap" }}>{row.last_run_at ? new Date(row.last_run_at).toLocaleString() : "—"}</span>
                            </div>
                          );
                        })}
                      </article>
                    </>
                  )}

                  {/* ── VISITOR STATS PANEL ────────────────────── */}
                  <div style={{ marginTop: 28 }}>
                    <VisitorStatsPanel token={token} />
                  </div>

                  {/* ── RECENT ACTIVITY ────────────────────────── */}
                  <SectionH action={
                    <select value={recentTypeFilter} onChange={(e) => setRecentTypeFilter(e.target.value)} style={{ fontSize: 11 }}>
                      <option value="all">All types</option>
                      <option value="member">Members</option>
                      <option value="subscriber">Subscribers</option>
                      <option value="risk_report">Risk reports</option>
                      <option value="carrier_sub">Carrier subs</option>
                      <option value="load_post">Load posts</option>
                      <option value="intel">Intel</option>
                      <option value="video">Videos</option>
                    </select>
                  }>Recent activity · last 60 events</SectionH>

                  <div style={{ background: "var(--paper, #fff)", border: "1px solid var(--rule)", borderRadius: 4, overflow: "hidden" }}>
                    {recent.length === 0 && (
                      <div style={{ padding: 20, textAlign: "center", color: "var(--ink-soft)", fontSize: 13 }}>No activity for this filter.</div>
                    )}
                    {recent.map((ev, i) => {
                      const isIntel = ev.type === "intel";
                      const isVisible = isIntel && ev.visibility === "public" && (ev.status === "verified" || ev.status === "auto_verified");
                      const sevColor = ev.severity ? sevColors[ev.severity] || "var(--ink-soft)" : "var(--ink-soft)";
                      return (
                        <div key={ev.id || i} style={{ display: "grid", gridTemplateColumns: "110px 1fr auto auto", gap: 12, padding: "10px 16px", borderBottom: i === recent.length - 1 ? "none" : "1px solid var(--rule)", alignItems: "center" }}>
                          <span style={{ fontFamily: "var(--font-mono)", fontSize: 10, textTransform: "uppercase", letterSpacing: "0.10em", color: isIntel ? sevColor : "var(--ink-soft)", fontWeight: isIntel ? 700 : 400 }}>
                            {typeLabel[ev.type] || ev.type}
                            {ev.severity && <span style={{ marginLeft: 4, fontSize: 9 }}>· {ev.severity}</span>}
                          </span>
                          <div style={{ minWidth: 0 }}>
                            <div style={{ fontSize: 13, fontWeight: 500, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{ev.label}</div>
                            {(ev.email || ev.name || ev.company) && (
                              <div style={{ fontSize: 11, color: "var(--ink-soft)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
                                {[ev.name, ev.email, ev.company].filter(Boolean).join(" · ")}
                                {ev.status ? ` · ${ev.status}` : ""}
                              </div>
                            )}
                          </div>
                          {/* Inline pull-back — only shown for already-public intel */}
                          {isVisible && ev.id && (
                            <button title="Pull this signal back — sets visibility=sealed"
                              onClick={async (e) => {
                                e.stopPropagation();
                                try {
                                  const r = await fetch(`/api/admin/intel?id=${encodeURIComponent(ev.id)}`, {
                                    method: "PATCH",
                                    headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
                                    body: JSON.stringify({ visibility: "sealed" }),
                                  });
                                  if (r.ok) loadDashboard();
                                } catch (_) {}
                              }}
                              className="btn-ghost"
                              style={{ fontSize: 10, padding: "2px 8px", color: "#b45309", border: "1px solid #fde4cf", whiteSpace: "nowrap" }}>
                              ↩ pull back
                            </button>
                          )}
                          <span style={{ fontSize: 11, color: "var(--ink-soft)", whiteSpace: "nowrap" }}>{new Date(ev.created_at).toLocaleString()}</span>
                        </div>
                      );
                    })}
                  </div>

                  <div style={{ marginTop: 24, fontSize: 11, color: "var(--ink-soft)", textAlign: "right" }}>
                    Generated {dash.generatedAt ? new Date(dash.generatedAt).toLocaleString() : "—"}
                  </div>
                </>
              );
            })()}
          </div>
        </section>
      )}

      {mode === "vetting" && (
        <section className="admin-list">
          <div className="admin-list-inner" style={{ maxWidth: 1100 }}>
            <h2 style={{ marginTop: 0 }}>Profile vetting</h2>
            <p style={{ color: "var(--ink-soft)", fontSize: 14, marginTop: -8 }}>
              Carriers + brokers with active FMCSA authority auto-verify on signup.
              Everyone else lands here for a one-look review.
            </p>
            <div style={{ display: "flex", gap: 8, alignItems: "center", marginBottom: 12, flexWrap: "wrap" }}>
              {["pending", "needs_info", "auto_verified", "verified", "rejected", "all"].map((s) => (
                <button
                  key={s}
                  className="btn-ghost"
                  style={{
                    fontSize: 12, padding: "5px 10px",
                    background: vettingFilter === s ? "var(--ink)" : "transparent",
                    color: vettingFilter === s ? "#fff" : "var(--ink)",
                    border: "1px solid var(--rule)", borderRadius: 4,
                  }}
                  onClick={() => setVettingFilter(s)}
                >{s}</button>
              ))}
              <button className="btn-ghost" style={{ marginLeft: "auto", fontSize: 12 }} onClick={loadVetting}>
                Refresh
              </button>
            </div>
            {vettingMessage && (
              <div style={{ padding: "8px 12px", background: "var(--paper-soft, #f6f6f4)", borderLeft: "3px solid var(--ink)", fontSize: 13, marginBottom: 12 }}>
                {vettingMessage}
              </div>
            )}
            {vettingLoading && <div className="admin-loading">Loading…</div>}
            {!vettingLoading && vettingRows.length === 0 && (
              <div className="admin-empty">No profiles in this status.</div>
            )}
            {vettingRows.map((p) => {
              const fmcsa = p.fmcsa_check_json && p.fmcsa_check_json.raw;
              return (
                <article key={p.user_id} className="admin-row" style={{ marginBottom: 12 }}>
                  <div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start", flexWrap: "wrap", gap: 12 }}>
                    <div style={{ flex: 1, minWidth: 280 }}>
                      <div style={{ fontWeight: 700, fontSize: 16 }}>
                        {p.company_name || "(no company)"} <span style={{ fontSize: 11, opacity: 0.6, fontWeight: 400, textTransform: "uppercase", letterSpacing: "0.08em", marginLeft: 6 }}>{p.role}</span>
                      </div>
                      <div style={{ fontSize: 13, color: "var(--ink-soft)", marginTop: 2 }}>
                        {p.email}{p.hq_city ? ` · ${p.hq_city}, ${p.hq_state}` : ""}
                      </div>
                      <div style={{ fontSize: 12, color: "var(--ink-soft)", marginTop: 6, fontFamily: "var(--font-mono)" }}>
                        {p.usdot ? `USDOT ${p.usdot}` : ""}{p.mc_number ? ` · MC ${p.mc_number}` : ""}{p.fleet_size_estimate ? ` · ${p.fleet_size_estimate} units` : ""}
                      </div>
                      {p.use_case && (
                        <div style={{ fontSize: 13, marginTop: 8, padding: 8, background: "var(--paper-soft, #f6f6f4)", borderRadius: 4 }}>
                          <strong>Use case:</strong> {p.use_case}
                        </div>
                      )}
                      {p.primary_lanes && (
                        <div style={{ fontSize: 12, marginTop: 6, color: "var(--ink-soft)" }}>
                          <strong>Lanes:</strong> {p.primary_lanes}
                        </div>
                      )}
                      {p.how_heard && (
                        <div style={{ fontSize: 12, marginTop: 4, color: "var(--ink-soft)" }}>
                          <strong>How heard:</strong> {p.how_heard}
                        </div>
                      )}
                    </div>
                    <div style={{ minWidth: 240 }}>
                      <div style={{ fontSize: 11, textTransform: "uppercase", letterSpacing: "0.08em", color: "var(--ink-soft)", marginBottom: 4 }}>
                        Status: <strong style={{ color: p.status === "auto_verified" || p.status === "verified" ? "#1e9b6a" : p.status === "rejected" ? "#c0392b" : "var(--ink)" }}>{p.status}</strong>
                      </div>
                      {fmcsa && (
                        <div style={{ fontSize: 11, fontFamily: "var(--font-mono)", padding: 8, background: "var(--paper-soft, #f6f6f4)", borderRadius: 4, marginBottom: 8 }}>
                          <div><strong>FMCSA:</strong> {fmcsa.legalName || fmcsa.dbaName || "—"}</div>
                          <div>Authority: {fmcsa.allowedToOperate || "—"} · Status: {fmcsa.statusCode || "—"}</div>
                          <div>Power units: {fmcsa.totalPowerUnits ?? "—"} · Drivers: {fmcsa.totalDrivers ?? "—"}</div>
                          {fmcsa.oosDate && <div style={{ color: "#c0392b" }}>OOS: {fmcsa.oosDate}</div>}
                        </div>
                      )}
                      <div style={{ display: "flex", gap: 6, flexWrap: "wrap" }}>
                        <button className="btn-primary" style={{ fontSize: 12, padding: "5px 10px" }}
                                onClick={() => vettingAction(p.user_id, "approve")}>✓ Approve</button>
                        <button className="btn-ghost" style={{ fontSize: 12, padding: "5px 10px" }}
                                onClick={() => {
                                  const note = prompt("Reject reason:");
                                  if (note !== null) vettingAction(p.user_id, "reject", note);
                                }}>✗ Reject</button>
                        <button className="btn-ghost" style={{ fontSize: 12, padding: "5px 10px" }}
                                onClick={() => {
                                  const note = prompt("What additional info do you need?");
                                  if (note !== null) vettingAction(p.user_id, "needs_info", note);
                                }}>? Needs info</button>
                      </div>
                      <div style={{ fontSize: 10, color: "var(--ink-soft)", marginTop: 8 }}>
                        Created {p.created_at ? new Date(p.created_at).toLocaleString() : "—"}
                      </div>
                    </div>
                  </div>
                </article>
              );
            })}
          </div>
        </section>
      )}

      {mode === "verdicts" && (
        <section className="admin-list">
          <div className="admin-list-inner" style={{ maxWidth: 1100 }}>
            <h2 style={{ marginTop: 0 }}>Verdict approval queue</h2>
            <p style={{ color: "var(--ink-soft)", fontSize: 14, marginTop: -8 }}>
              Distress flags don't display publicly until you approve them here.
              Heuristic-flagged rows below are downgraded to ⚠ Watch on the
              public site by default.
            </p>
            <div style={{ display: "flex", gap: 8, alignItems: "center", marginBottom: 12, flexWrap: "wrap" }}>
              {[
                ["heuristic_distress", "Pending review"],
                ["approved", "Approved Distress"],
                ["downgraded", "Editor → Watch"],
                ["cleared", "Editor → No flags"],
              ].map(([k, label]) => (
                <button
                  key={k}
                  className="btn-ghost"
                  style={{
                    fontSize: 12, padding: "5px 10px",
                    background: verdictFilter === k ? "var(--ink)" : "transparent",
                    color: verdictFilter === k ? "#fff" : "var(--ink)",
                    border: "1px solid var(--rule)", borderRadius: 4,
                  }}
                  onClick={() => setVerdictFilter(k)}
                >{label}</button>
              ))}
              <button className="btn-ghost" style={{ marginLeft: "auto", fontSize: 12 }} onClick={loadVerdicts}>
                Refresh
              </button>
            </div>
            {verdictMessage && (
              <div style={{ padding: "8px 12px", background: "var(--paper-soft, #f6f6f4)", borderLeft: "3px solid var(--ink)", fontSize: 13, marginBottom: 12 }}>
                {verdictMessage}
              </div>
            )}
            {verdictLoading && <div className="admin-loading">Loading…</div>}
            {!verdictLoading && verdictRows.length === 0 && (
              <div className="admin-empty">No rows in this status.</div>
            )}
            {verdictRows.map((r) => (
              <article key={`${r.table}-${r.slug}`} className="admin-row" style={{ marginBottom: 12 }}>
                <div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start", flexWrap: "wrap", gap: 12 }}>
                  <div style={{ flex: 1, minWidth: 280 }}>
                    <div style={{ fontWeight: 700, fontSize: 16 }}>
                      {r.name}
                      <span style={{ fontSize: 11, opacity: 0.6, fontWeight: 400, textTransform: "uppercase", letterSpacing: "0.08em", marginLeft: 8 }}>
                        {r.table.replace("_top100", "")} · #{r.rank}
                      </span>
                    </div>
                    {r.parent && r.parent !== r.name && (
                      <div style={{ fontSize: 13, color: "var(--ink-soft)", marginTop: 2 }}>{r.parent}</div>
                    )}
                    {r.matched_keywords && r.matched_keywords.length > 0 && (
                      <div style={{ fontSize: 11, color: "var(--ink-soft)", marginTop: 6, fontFamily: "var(--font-mono)" }}>
                        Matched: <strong style={{ color: "#c0392b" }}>{r.matched_keywords.join(", ")}</strong>
                      </div>
                    )}
                    {r.notes && (
                      <div style={{ fontSize: 13, marginTop: 8, padding: 8, background: "var(--paper-soft, #f6f6f4)", borderRadius: 4, lineHeight: 1.5 }}>
                        {r.notes}
                      </div>
                    )}
                  </div>
                  <div style={{ minWidth: 220 }}>
                    <div style={{ fontSize: 11, textTransform: "uppercase", letterSpacing: "0.08em", color: "var(--ink-soft)", marginBottom: 6 }}>
                      Current: <strong>{r.verdict_override || "(heuristic only)"}</strong>
                    </div>
                    <div style={{ display: "flex", gap: 6, flexWrap: "wrap" }}>
                      <button className="btn-ghost" style={{ fontSize: 12, padding: "5px 10px", color: "#c0392b", border: "1px solid #c0392b" }}
                              onClick={() => {
                                if (confirm(`Approve DISTRESS for ${r.name}? This is the strongest claim and will display publicly. Confirm the underlying note is factually accurate.`)) {
                                  setVerdictOverride(r.table, r.slug, "distress");
                                }
                              }}>▲ Approve Distress</button>
                      <button className="btn-ghost" style={{ fontSize: 12, padding: "5px 10px" }}
                              onClick={() => setVerdictOverride(r.table, r.slug, "watch")}>⚠ Set Watch</button>
                      <button className="btn-ghost" style={{ fontSize: 12, padding: "5px 10px" }}
                              onClick={() => setVerdictOverride(r.table, r.slug, "no_flags")}>○ Clear flags</button>
                      <button className="btn-ghost" style={{ fontSize: 12, padding: "5px 10px", opacity: 0.6 }}
                              onClick={() => setVerdictOverride(r.table, r.slug, null)}>↩ Reset</button>
                    </div>
                    <div style={{ marginTop: 8 }}>
                      <a href={`#/${r.table === "carriers_top100" ? "c" : r.table === "brokers_top100" ? "b" : "sh"}/${r.slug}`}
                         target="_blank" rel="noopener noreferrer"
                         style={{ fontSize: 12, color: "oklch(0.50 0.18 250)" }}>
                        Open dossier ↗
                      </a>
                    </div>
                  </div>
                </div>
              </article>
            ))}
          </div>
        </section>
      )}

      {mode === "voices" && (
        <section className="admin-list">
          <div className="admin-list-inner" style={{ maxWidth: 820 }}>
            <article className="admin-row" style={{ padding: "24px 28px", marginBottom: 24 }}>
              <h2 style={{ marginTop: 0, fontSize: 18 }}>Voices we follow</h2>
              <p style={{ fontSize: 13, color: "var(--ink-soft)", marginBottom: 16 }}>
                Private list of external voices on Reddit, Substack, LinkedIn, etc.
                whose freight-industry takes are worth tracking. Never republished —
                research only.
              </p>
              <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 10 }}>
                <label>
                  <span style={{ display: "block", fontSize: 12, fontWeight: 600, marginBottom: 4 }}>Handle *</span>
                  <input value={voiceForm.handle} onChange={e => setVoiceForm({ ...voiceForm, handle: e.target.value })}
                    placeholder="charlesholmes1" style={{ width: "100%", padding: 8 }} />
                </label>
                <label>
                  <span style={{ display: "block", fontSize: 12, fontWeight: 600, marginBottom: 4 }}>Platform *</span>
                  <select value={voiceForm.platform} onChange={e => setVoiceForm({ ...voiceForm, platform: e.target.value })} style={{ width: "100%", padding: 8 }}>
                    <option value="reddit">Reddit</option>
                    <option value="twitter">X / Twitter</option>
                    <option value="linkedin">LinkedIn</option>
                    <option value="substack">Substack</option>
                    <option value="newsletter">Newsletter</option>
                    <option value="forum">Forum (TruckersReport, etc.)</option>
                    <option value="other">Other</option>
                  </select>
                </label>
                <label>
                  <span style={{ display: "block", fontSize: 12, fontWeight: 600, marginBottom: 4 }}>Real name (optional)</span>
                  <input value={voiceForm.real_name} onChange={e => setVoiceForm({ ...voiceForm, real_name: e.target.value })}
                    placeholder="Charles Holmes" style={{ width: "100%", padding: 8 }} />
                </label>
                <label>
                  <span style={{ display: "block", fontSize: 12, fontWeight: 600, marginBottom: 4 }}>Affiliation (optional)</span>
                  <input value={voiceForm.affiliation} onChange={e => setVoiceForm({ ...voiceForm, affiliation: e.target.value })}
                    placeholder="IMC Logistics, CSO" style={{ width: "100%", padding: 8 }} />
                </label>
                <label style={{ gridColumn: "1 / -1" }}>
                  <span style={{ display: "block", fontSize: 12, fontWeight: 600, marginBottom: 4 }}>Why we follow</span>
                  <textarea value={voiceForm.why} onChange={e => setVoiceForm({ ...voiceForm, why: e.target.value })}
                    rows={2} style={{ width: "100%", padding: 8, fontFamily: "inherit", fontSize: 13 }}
                    placeholder="Weekly logistics roundup, dialed in on cargo theft + USPS." />
                </label>
                <label>
                  <span style={{ display: "block", fontSize: 12, fontWeight: 600, marginBottom: 4 }}>Source URL (optional)</span>
                  <input value={voiceForm.source_url} onChange={e => setVoiceForm({ ...voiceForm, source_url: e.target.value })}
                    placeholder="https://reddit.com/user/charlesholmes1" style={{ width: "100%", padding: 8 }} />
                </label>
                <label>
                  <span style={{ display: "block", fontSize: 12, fontWeight: 600, marginBottom: 4 }}>Topic tags (comma-separated)</span>
                  <input value={voiceForm.topic_tags} onChange={e => setVoiceForm({ ...voiceForm, topic_tags: e.target.value })}
                    placeholder="cargo-theft, broker-fraud, macro" style={{ width: "100%", padding: 8 }} />
                </label>
              </div>
              <div style={{ display: "flex", justifyContent: "flex-end", marginTop: 14, gap: 12, alignItems: "center" }}>
                {voiceMessage && <span style={{ fontSize: 12, color: "var(--ink-soft)" }}>{voiceMessage}</span>}
                <button className="btn-primary" onClick={saveVoice} disabled={voiceSaving || !voiceForm.handle.trim()}>
                  {voiceSaving ? "Saving…" : "+ Add voice"}
                </button>
              </div>
            </article>

            <h3 style={{ fontSize: 14, textTransform: "uppercase", letterSpacing: "0.12em", color: "var(--ink-soft)", marginBottom: 12 }}>
              {voices.length} tracked
              <button className="btn-ghost" style={{ marginLeft: 12, fontSize: 11, padding: "2px 8px" }} onClick={loadVoices}>Refresh</button>
            </h3>
            {voices.length === 0 && <div className="admin-empty">No voices yet. Add the first above.</div>}
            {voices.map(v => (
              <article key={v.id} className="admin-row" style={{ padding: "14px 18px", marginBottom: 8 }}>
                <div style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline", gap: 12, flexWrap: "wrap" }}>
                  <div>
                    <strong style={{ fontSize: 14 }}>{v.handle}</strong>
                    <span style={{ fontSize: 11, color: "var(--ink-soft)", marginLeft: 8, textTransform: "uppercase", letterSpacing: "0.10em" }}>
                      {v.platform}
                    </span>
                    {v.real_name && <span style={{ fontSize: 12, color: "var(--ink-soft)", marginLeft: 8 }}>· {v.real_name}</span>}
                    {v.affiliation && <span style={{ fontSize: 12, color: "var(--ink-soft)", marginLeft: 4 }}>· {v.affiliation}</span>}
                  </div>
                  <button className="btn-ghost" style={{ fontSize: 11, padding: "2px 10px" }} onClick={() => deleteVoice(v.id)}>Delete</button>
                </div>
                {v.why && <p style={{ fontSize: 13, margin: "6px 0 4px" }}>{v.why}</p>}
                {(v.topic_tags || []).length > 0 && (
                  <div style={{ display: "flex", gap: 6, flexWrap: "wrap", marginTop: 4 }}>
                    {v.topic_tags.map(t => (
                      <span key={t} style={{ fontSize: 10, padding: "2px 8px", background: "var(--paper-2, #f0ece4)", border: "1px solid var(--rule)", borderRadius: 10 }}>{t}</span>
                    ))}
                  </div>
                )}
                {v.source_url && (
                  <div style={{ marginTop: 6, fontSize: 12 }}>
                    <a href={v.source_url} target="_blank" rel="noopener noreferrer" style={{ color: "var(--ink-soft)" }}>Open ↗</a>
                  </div>
                )}
              </article>
            ))}
          </div>
        </section>
      )}

      {mode === "video" && (
        <section className="admin-list">
          <div className="admin-list-inner" style={{ maxWidth: 820 }}>

            {/* ─── Upload your own ─────────────────────────────── */}
            <article className="admin-row" style={{ padding: "24px 28px", marginBottom: 24, borderLeft: "3px solid var(--ink)" }}>
              <h2 style={{ marginTop: 0, fontSize: 18 }}>Upload your own video</h2>
              <p style={{ fontSize: 13, color: "var(--ink-soft)", marginBottom: 16 }}>
                Post a video you already created. Pick an MP4 file from your computer
                — or paste a YouTube / Vimeo / Loom URL. Goes live at <code>/v/&lt;slug&gt;</code>
                and shows up in the home-page video strip immediately.
              </p>

              {/* Mode toggle */}
              <div style={{ display: "inline-flex", border: "1px solid var(--rule)", borderRadius: 4, marginBottom: 14 }}>
                <button
                  onClick={() => setUplMode("file")}
                  style={{
                    padding: "6px 14px", fontSize: 12, fontWeight: 600,
                    background: uplMode === "file" ? "var(--ink)" : "transparent",
                    color: uplMode === "file" ? "#fff" : "var(--ink)",
                    border: "none", cursor: "pointer",
                  }}>MP4 file</button>
                <button
                  onClick={() => setUplMode("embed")}
                  style={{
                    padding: "6px 14px", fontSize: 12, fontWeight: 600,
                    background: uplMode === "embed" ? "var(--ink)" : "transparent",
                    color: uplMode === "embed" ? "#fff" : "var(--ink)",
                    border: "none", cursor: "pointer",
                  }}>YouTube / Vimeo URL</button>
              </div>

              {uplMode === "file" ? (
                <label style={{ display: "block", marginBottom: 12 }}>
                  <span style={{ display: "block", fontSize: 12, fontWeight: 600, marginBottom: 6 }}>Video file (MP4 / MOV / WebM)</span>
                  <input
                    type="file"
                    accept="video/mp4,video/quicktime,video/webm,.mp4,.mov,.m4v,.webm"
                    onChange={(e) => setUplFile(e.target.files && e.target.files[0] || null)}
                    style={{ width: "100%", fontSize: 13 }}
                  />
                  {uplFile && (
                    <div style={{ marginTop: 6, fontSize: 11, color: "var(--ink-soft)" }}>
                      {uplFile.name} · {(uplFile.size / 1024 / 1024).toFixed(1)} MB
                    </div>
                  )}
                </label>
              ) : (
                <label style={{ display: "block", marginBottom: 12 }}>
                  <span style={{ display: "block", fontSize: 12, fontWeight: 600, marginBottom: 6 }}>Embed URL</span>
                  <input
                    type="url"
                    value={uplEmbedUrl}
                    onChange={(e) => setUplEmbedUrl(e.target.value)}
                    placeholder="https://www.youtube.com/watch?v=… or https://vimeo.com/…"
                    style={{ width: "100%", padding: "8px 10px", fontSize: 13, border: "1px solid var(--rule)", borderRadius: 4 }}
                  />
                </label>
              )}

              <label style={{ display: "block", marginBottom: 12 }}>
                <span style={{ display: "block", fontSize: 12, fontWeight: 600, marginBottom: 6 }}>Title</span>
                <input
                  type="text"
                  value={uplTitle}
                  onChange={(e) => setUplTitle(e.target.value)}
                  placeholder="e.g. The 23% double-brokering problem, in 90 seconds"
                  style={{ width: "100%", padding: "8px 10px", fontSize: 14, border: "1px solid var(--rule)", borderRadius: 4 }}
                />
              </label>

              <label style={{ display: "block", marginBottom: 12 }}>
                <span style={{ display: "block", fontSize: 12, fontWeight: 600, marginBottom: 6 }}>Description (optional)</span>
                <textarea
                  value={uplDescription}
                  onChange={(e) => setUplDescription(e.target.value)}
                  rows={2}
                  placeholder="One-sentence subline that shows under the title."
                  style={{ width: "100%", padding: 10, fontFamily: "inherit", fontSize: 13, border: "1px solid var(--rule)", borderRadius: 4, resize: "vertical" }}
                />
              </label>

              <div style={{ display: "flex", gap: 12, alignItems: "center", flexWrap: "wrap", marginBottom: 12 }}>
                <label>
                  <span style={{ fontSize: 12, fontWeight: 600, marginRight: 8 }}>Format</span>
                  <select value={uplFormat} onChange={(e) => setUplFormat(e.target.value)}>
                    <option value="16x9">16:9 (landscape)</option>
                    <option value="9x16">9:16 (vertical / shorts)</option>
                    <option value="1x1">1:1 (square)</option>
                  </select>
                </label>
                <label>
                  <span style={{ fontSize: 12, fontWeight: 600, marginRight: 8 }}>Slug (optional)</span>
                  <input
                    type="text"
                    value={uplSlug}
                    onChange={(e) => setUplSlug(e.target.value.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, ""))}
                    placeholder="auto-from-title"
                    style={{ padding: "4px 8px", fontSize: 13, border: "1px solid var(--rule)", borderRadius: 4, width: 220 }}
                  />
                </label>
                <label style={{ display: "inline-flex", alignItems: "center", gap: 6, fontSize: 13 }}>
                  <input type="checkbox" checked={uplFeatured} onChange={(e) => setUplFeatured(e.target.checked)} />
                  Pin to home strip
                </label>
                <button
                  className="btn-primary"
                  onClick={uploadOwnVideo}
                  disabled={uplBusy || !uplTitle.trim() || (uplMode === "file" ? !uplFile : !uplEmbedUrl.trim())}
                  style={{ marginLeft: "auto" }}
                >
                  {uplBusy ? (uplProgress > 0 && uplProgress < 100 ? `Uploading ${uplProgress}%…` : "Working…") : "▲ Upload & publish"}
                </button>
              </div>

              {uplBusy && uplProgress > 0 && uplProgress < 100 && (
                <div style={{ height: 6, background: "var(--rule)", borderRadius: 3, overflow: "hidden", marginBottom: 10 }}>
                  <div style={{ height: "100%", width: `${uplProgress}%`, background: "var(--ink)", transition: "width 200ms" }} />
                </div>
              )}

              {uplMessage && (
                <div style={{ marginTop: 8, padding: "10px 12px", background: "var(--paper-soft, #f6f6f4)", borderLeft: "3px solid var(--ink)", fontSize: 13, wordBreak: "break-all" }}>
                  {uplMessage}
                </div>
              )}

              <div style={{ marginTop: 12, fontSize: 11, color: "var(--ink-soft)", lineHeight: 1.5 }}>
                💡 <strong>Tip:</strong> for big files, embed-mode (YouTube/Vimeo) is gentler on Supabase egress
                and gives you platform reach. MP4-mode keeps the player clean of YouTube's chrome but eats storage egress.
                Pinned videos appear in the home-page strip (max 3 — newest wins).
              </div>
            </article>

            {/* ─── Make a video (existing AI pipeline) ────────── */}
            <article className="admin-row" style={{ padding: "24px 28px", marginBottom: 24 }}>
              <h2 style={{ marginTop: 0, fontSize: 18 }}>Make a video</h2>
              <p style={{ fontSize: 13, color: "var(--ink-soft)", marginBottom: 16 }}>
                Type a topic and hit the button. Renders in ~5 minutes.
                The MP4 lands in Supabase Storage and shows up below when ready.
              </p>
              <label style={{ display: "block", marginBottom: 12 }}>
                <span style={{ display: "block", fontSize: 12, fontWeight: 600, marginBottom: 6 }}>Topic</span>
                <textarea
                  value={videoTopic}
                  onChange={(e) => setVideoTopic(e.target.value)}
                  placeholder="e.g. How to spot a broker about to lose their authority — FMCSA L&I bond cancellation pending flag, 30 days before revocation…"
                  rows={5}
                  style={{ width: "100%", padding: 10, fontFamily: "inherit", fontSize: 14, border: "1px solid var(--rule)", borderRadius: 4, resize: "vertical" }}
                />
              </label>
              <div style={{ display: "flex", gap: 12, alignItems: "center", flexWrap: "wrap" }}>
                <label>
                  <span style={{ fontSize: 12, fontWeight: 600, marginRight: 8 }}>Format</span>
                  <select value={videoFormat} onChange={(e) => setVideoFormat(e.target.value)}>
                    <option value="16x9">16:9 (landscape)</option>
                    <option value="9x16">9:16 (vertical / shorts)</option>
                    <option value="1x1">1:1 (square)</option>
                  </select>
                </label>
                <label>
                  <span style={{ fontSize: 12, fontWeight: 600, marginRight: 8 }}>Length</span>
                  <select
                    value={videoTargetSeconds}
                    onChange={(e) => setVideoTargetSeconds(parseInt(e.target.value, 10))}
                  >
                    <option value="90">90 sec (short)</option>
                    <option value="180">3 min</option>
                    <option value="300">5 min (default)</option>
                    <option value="360">6 min</option>
                    <option value="480">8 min</option>
                    <option value="600">10 min</option>
                  </select>
                </label>
                <label>
                  <span style={{ fontSize: 12, fontWeight: 600, marginRight: 8 }}>Planner</span>
                  <select
                    value={videoPlannerModel}
                    onChange={(e) => setVideoPlannerModel(e.target.value)}
                  >
                    <option value="claude-sonnet-4-6">Sonnet (default · balanced)</option>
                    <option value="claude-haiku-4-5-20251001">Haiku (cheap · fast)</option>
                  </select>
                </label>
                <label>
                  <span style={{ fontSize: 12, fontWeight: 600, marginRight: 8 }}>Voice</span>
                  <select
                    value={videoVoiceId}
                    onChange={(e) => setVideoVoiceId(e.target.value)}
                  >
                    <option value="">Default (Brian)</option>
                    <option value="nPczCjzI2devNBz1zQrb">Brian — male · documentary</option>
                    <option value="pNInz6obpgDQGcFmaJgB">Adam — male · deep · news</option>
                    <option value="ErXwobaYiN019PkySvjV">Antoni — male · warm · narrator</option>
                    <option value="21m00Tcm4TlvDq8ikWAM">Rachel — female · calm · news</option>
                    <option value="EXAVITQu4vr4xnSDxMaL">Bella — female · warm</option>
                  </select>
                </label>
                <button
                  className="btn-primary"
                  onClick={dispatchVideo}
                  disabled={videoDispatching || !videoTopic.trim()}
                  style={{ marginLeft: "auto" }}
                >
                  {videoDispatching ? "Dispatching…" : "▶ Make video"}
                </button>
              </div>

              {/* Optional editor hints — flow into the planner prompt so
                  Sonnet biases toward the listed objects/characters when
                  composing icon-pop shots. Free-text fields; tooltip-y
                  helper below the row. */}
              <div style={{ marginTop: 14, paddingTop: 14, borderTop: "1px dashed var(--rule)",
                            display: "grid", gridTemplateColumns: "1fr 1fr", gap: 12 }}>
                <label style={{ display: "block" }}>
                  <span style={{ display: "block", fontSize: 11, fontWeight: 600, marginBottom: 4,
                                 textTransform: "uppercase", letterSpacing: "0.06em",
                                 color: "var(--ink-soft)" }}>
                    Objects to feature (optional)
                  </span>
                  <input
                    type="text"
                    value={videoObjectsHint}
                    onChange={(e) => setVideoObjectsHint(e.target.value)}
                    placeholder="apples, dollar bills, trucks"
                    style={{ width: "100%", padding: "6px 10px", fontSize: 13,
                             border: "1px solid var(--rule)", borderRadius: 4 }}
                  />
                </label>
                <label style={{ display: "block" }}>
                  <span style={{ display: "block", fontSize: 11, fontWeight: 600, marginBottom: 4,
                                 textTransform: "uppercase", letterSpacing: "0.06em",
                                 color: "var(--ink-soft)" }}>
                    Characters to feature (optional)
                  </span>
                  <input
                    type="text"
                    value={videoCharactersHint}
                    onChange={(e) => setVideoCharactersHint(e.target.value)}
                    placeholder="driver, CFO, broker"
                    style={{ width: "100%", padding: "6px 10px", fontSize: 13,
                             border: "1px solid var(--rule)", borderRadius: 4 }}
                  />
                </label>
                <label style={{ gridColumn: "1 / -1", display: "flex", alignItems: "center",
                                gap: 8, fontSize: 13 }}>
                  <input
                    type="checkbox"
                    checked={videoIncludeIcons}
                    onChange={(e) => setVideoIncludeIcons(e.target.checked)}
                  />
                  <span>
                    Include 1–2 cartoon <code>icon-pop</code> shots
                    <span style={{ color: "var(--ink-soft)", marginLeft: 6 }}>
                      (off = planner only uses icon-pop when the topic naturally calls for one)
                    </span>
                  </span>
                </label>
              </div>
              {videoMessage && (
                <div style={{ marginTop: 14, padding: "10px 12px", background: "var(--paper-soft, #f6f6f4)", borderLeft: "3px solid var(--ink)", fontSize: 13 }}>
                  {videoMessage}
                </div>
              )}
            </article>

            <h3 style={{ fontSize: 14, textTransform: "uppercase", letterSpacing: "0.12em", color: "var(--ink-soft)", marginBottom: 12 }}>
              Recent drafts
              <button className="btn-ghost" style={{ marginLeft: 12, fontSize: 11, padding: "2px 8px" }} onClick={loadVideoDrafts}>Refresh</button>
            </h3>
            {videoDrafts.length === 0 && (
              <div className="admin-empty">No video drafts yet.</div>
            )}
            {videoDrafts.map((d) => {
              const terminal = d.status === "rendered" || d.status === "failed";
              const color = d.status === "rendered" ? "var(--green, #2a8a3e)"
                          : d.status === "failed" ? "var(--red, #c0392b)"
                          : "var(--amber, #c08a1c)";
              return (
                <article key={d.id} className="admin-row" style={{ padding: "16px 20px", marginBottom: 10 }}>
                  <div style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline", gap: 12, flexWrap: "wrap" }}>
                    <strong style={{ fontSize: 14 }}>{d.title || "(untitled)"}</strong>
                    <span style={{ fontFamily: "var(--font-mono)", fontSize: 11, color, textTransform: "uppercase", letterSpacing: "0.08em" }}>
                      {terminal ? d.status : `${d.status}…`}
                    </span>
                  </div>
                  <div style={{ fontSize: 12, color: "var(--ink-soft)", marginTop: 4 }}>
                    {d.format} · {d.duration_seconds ? `${d.duration_seconds}s · ` : ""}
                    {new Date(d.created_at).toLocaleString()}
                    {d.article_slug ? ` · article: ${d.article_slug}` : ""}
                  </div>
                  {d.status === "rendered" && d.video_public_url && (() => {
                    const filename = ((d.title || d.article_slug || "video")
                      .toLowerCase()
                      .replace(/[^a-z0-9]+/g, "-")
                      .replace(/^-|-$/g, "")
                      .slice(0, 60) || "video") + ".mp4";
                    return (
                      <div style={{ marginTop: 10 }}>
                        {/* preload="none" so the admin Recent drafts list
                            doesn't pull MP4 metadata for every rendered row
                            on every load — adds up to real egress over time. */}
                        <video src={d.video_public_url} controls playsInline preload="none" style={{ width: "100%", maxWidth: 640, borderRadius: 4 }} />
                        <div style={{ marginTop: 8, fontSize: 12, display: "flex", gap: 10, alignItems: "center", flexWrap: "wrap" }}>
                          {/* iPhone / Android Chrome — opens native share sheet → Save Video → Photos */}
                          <SaveToPhotosButton
                            url={d.video_public_url}
                            filename={filename}
                            title={d.title || "Shipping Clarity video"}
                          />
                          {/* Desktop / fallback */}
                          <a
                            href={d.video_public_url}
                            download={filename}
                            style={{
                              display: "inline-block", padding: "6px 14px",
                              background: "var(--ink, #0a1224)", color: "#fff",
                              borderRadius: 4, textDecoration: "none", fontWeight: 600,
                            }}
                          >
                            ⬇ Download MP4
                          </a>
                          <a href={d.video_public_url} target="_blank" rel="noopener noreferrer">
                            Open in new tab ↗
                          </a>
                        </div>
                        <PublishVideoControls draft={d} token={token} onPublished={loadVideoDrafts} />
                        <ShotEditorTrigger draft={d} token={token} onRerendered={loadVideoDrafts} />
                        <div style={{ marginTop: 6, fontSize: 11, opacity: 0.55, lineHeight: 1.45 }}>
                          📱 <strong>iPhone:</strong> tap "Save to Photos" → share sheet → "Save Video" lands in Photos.
                          Or long-press the video player → "Save to Photos".<br />
                          🖥 <strong>Desktop:</strong> "Download MP4" works on Firefox; on Chrome may open in tab — right-click → Save video as.
                        </div>
                      </div>
                    );
                  })()}
                  {d.status === "failed" && d.error && (
                    <div style={{ marginTop: 8, fontFamily: "var(--font-mono)", fontSize: 11, color: "var(--red, #c0392b)", whiteSpace: "pre-wrap" }}>
                      {d.error}
                    </div>
                  )}
                </article>
              );
            })}
          </div>
        </section>
      )}

      {mode === "drafts" && (
        <section style={{ padding: "24px 28px", maxWidth: 1100, margin: "0 auto" }}>
          <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 6, flexWrap: "wrap", gap: 12 }}>
            <div>
              <h2 style={{ fontSize: 22, fontWeight: 700, margin: 0 }}>Editorial drafts</h2>
              <p style={{ fontSize: 13, opacity: 0.6, margin: "4px 0 0", maxWidth: 720 }}>
                Auto-generated by the daily editorial scan (post-ingest, ~10:00 UTC) and the
                weekly cadence (Tuesdays 13:00 UTC). Newest first. Click a card to read.
              </p>
            </div>
            <div style={{ display: "flex", gap: 6 }}>
              {[
                { id: "all",      label: "All" },
                { id: "daily",    label: "Daily" },
                { id: "weekly",   label: "Weekly" },
                { id: "skeleton", label: "Skeletons" },
              ].map(opt => (
                <button key={opt.id} onClick={() => setDraftKindFilter(opt.id)}
                  className="btn-ghost"
                  style={{
                    padding: "5px 12px", fontSize: 12, borderRadius: 6,
                    border: "1px solid var(--rule)",
                    background: draftKindFilter === opt.id ? "var(--ink)" : "transparent",
                    color: draftKindFilter === opt.id ? "#fff" : "var(--ink)",
                  }}>{opt.label}</button>
              ))}
              <button className="btn-ghost" onClick={loadDrafts} style={{ fontSize: 12, padding: "5px 12px" }}>
                {draftsLoading ? "Loading…" : "Refresh"}
              </button>
            </div>
          </div>

          {drafts.length === 0 && !draftsLoading && (
            <div style={{ marginTop: 24, padding: 28, border: "1px dashed var(--rule)", borderRadius: 8, textAlign: "center", color: "var(--ink-soft)" }}>
              No drafts yet. The first daily scan fires after the next morning ingest at ~10:00 UTC.
            </div>
          )}

          {openDraft ? (
            <div style={{ marginTop: 18 }}>
              <button onClick={() => setOpenDraft(null)} className="btn-ghost"
                style={{ fontSize: 12, marginBottom: 12 }}>← Back to all drafts</button>
              <div style={{
                background: "#fff", border: "1px solid var(--rule)",
                borderRadius: 8, padding: "20px 26px",
              }}>
                <div style={{ fontSize: 11, letterSpacing: "0.14em", textTransform: "uppercase", opacity: 0.5, marginBottom: 6 }}>
                  {openDraft.kind} · {openDraft.date || "(no date)"} · {openDraft.name}
                </div>
                <pre style={{
                  whiteSpace: "pre-wrap", wordBreak: "break-word", fontFamily: "Menlo, monospace",
                  fontSize: 13, lineHeight: 1.55, margin: 0, color: "#1f2937",
                }}>{openDraft.body}</pre>
              </div>
            </div>
          ) : (
            <div style={{ marginTop: 18, display: "grid", gap: 10 }}>
              {drafts
                .filter(d => draftKindFilter === "all" || d.kind === draftKindFilter)
                .map(d => {
                  const firstLine = (d.body || "").split("\n").find(l => l.trim() && !l.startsWith("#")) || "";
                  const titleLine = (d.body || "").split("\n").find(l => l.startsWith("# ")) || "";
                  const kindColor = d.kind === "daily"   ? "#1d4ed8"
                                  : d.kind === "weekly"  ? "#b45309"
                                  : d.kind === "skeleton"? "#6b7280" : "#374151";
                  return (
                    <button key={d.name} onClick={() => setOpenDraft(d)}
                      style={{
                        textAlign: "left", padding: "14px 18px", borderRadius: 8,
                        border: "1px solid var(--rule)", background: "#fff", cursor: "pointer",
                        display: "block", width: "100%",
                      }}>
                      <div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 4 }}>
                        <span style={{
                          fontSize: 10, letterSpacing: "0.12em", textTransform: "uppercase",
                          fontWeight: 700, color: kindColor,
                          background: kindColor + "14", padding: "2px 7px", borderRadius: 4,
                        }}>{d.kind}</span>
                        <span style={{ fontSize: 12, opacity: 0.55 }}>{d.date || ""}</span>
                        <span style={{ fontSize: 11, opacity: 0.4, marginLeft: "auto" }}>{d.name}</span>
                      </div>
                      <div style={{ fontSize: 14, fontWeight: 600, color: "var(--ink)", marginBottom: 2 }}>
                        {titleLine.replace(/^#\s*/, "") || d.name}
                      </div>
                      <div style={{ fontSize: 12, opacity: 0.65, lineHeight: 1.45 }}>
                        {firstLine.slice(0, 160)}{firstLine.length > 160 ? "…" : ""}
                      </div>
                    </button>
                  );
                })}
            </div>
          )}
        </section>
      )}

      {mode === "alerts" && (
        <section style={{ padding: "24px 28px", maxWidth: 1100, margin: "0 auto" }}>
          <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 6, flexWrap: "wrap", gap: 12 }}>
            <div>
              <h2 style={{ fontSize: 22, fontWeight: 700, margin: 0 }}>Synthesized alerts</h2>
              <p style={{ fontSize: 13, opacity: 0.6, margin: "4px 0 0", maxWidth: 720 }}>
                Output of <code>synthesize-alerts.py</code> — second-order patterns synthesized
                across raw signals. These land here as <b>pending</b>; publish to surface on the
                public dashboard, reject to dismiss.
              </p>
            </div>
            <div style={{ display: "flex", gap: 6, alignItems: "center" }}>
              {[
                { id: "pending",   label: "Pending" },
                { id: "published", label: "Published" },
                { id: "rejected",  label: "Rejected" },
                { id: "all",       label: "All" },
              ].map(opt => (
                <button key={opt.id} onClick={() => setAlertStatusFilter(opt.id)}
                  className="btn-ghost"
                  style={{
                    padding: "5px 12px", fontSize: 12, borderRadius: 6,
                    border: "1px solid var(--rule)",
                    background: alertStatusFilter === opt.id ? "var(--ink)" : "transparent",
                    color: alertStatusFilter === opt.id ? "#fff" : "var(--ink)",
                  }}>{opt.label}</button>
              ))}
              <button className="btn-ghost" onClick={loadAlerts} style={{ fontSize: 12, padding: "5px 12px" }}>
                {alertsLoading ? "Loading…" : "Refresh"}
              </button>
            </div>
          </div>

          {alertMessage && (
            <div style={{ padding: "8px 12px", background: "var(--paper-soft, #f6f6f4)", borderLeft: "3px solid var(--ink)", fontSize: 13, marginBottom: 12, marginTop: 12 }}>
              {alertMessage}
            </div>
          )}

          {!alertsLoading && alerts.length === 0 && (
            <div style={{ marginTop: 24, padding: 28, border: "1px dashed var(--rule)", borderRadius: 8, textAlign: "center", color: "var(--ink-soft)" }}>
              No <b>{alertStatusFilter}</b> alerts. {alertStatusFilter === "pending" ? "Run synthesize-alerts.py to generate new ones." : ""}
            </div>
          )}

          <div style={{ marginTop: 18, display: "grid", gap: 10 }}>
            {alerts.map(a => {
              const sevColor = a.severity === "critical" ? "#b91c1c"
                            : a.severity === "high"     ? "#dc2626"
                            : a.severity === "moderate" ? "#f59e0b"
                            : "#6b7280";
              return (
                <article key={a.id} className="admin-row" style={{ padding: "16px 18px" }}>
                  <div style={{ display: "flex", alignItems: "flex-start", justifyContent: "space-between", gap: 14, flexWrap: "wrap" }}>
                    <div style={{ flex: 1, minWidth: 280 }}>
                      <div style={{ display: "flex", alignItems: "center", gap: 10, marginBottom: 6 }}>
                        <span style={{ background: sevColor + "18", color: sevColor, padding: "2px 8px", borderRadius: 4, fontSize: 10, textTransform: "uppercase", letterSpacing: "0.12em", fontWeight: 700 }}>{a.severity || "—"}</span>
                        {a.city_id    && <span style={{ fontSize: 11, opacity: 0.5 }}>· {a.city_id}</span>}
                        {a.carrier_slug && <span style={{ fontSize: 11, opacity: 0.5 }}>· {a.carrier_slug}</span>}
                        <span style={{ fontSize: 11, opacity: 0.45, marginLeft: "auto" }}>{a.generated_at ? new Date(a.generated_at).toLocaleString() : ""}</span>
                      </div>
                      <div style={{ fontWeight: 700, fontSize: 15, marginBottom: 4 }}>{a.topic || "(untitled)"}</div>
                      <div style={{ fontSize: 13, opacity: 0.85, lineHeight: 1.5, marginBottom: 8 }}>{a.narrative}</div>
                      {a.support_summary && (
                        <details style={{ fontSize: 12, opacity: 0.7 }}>
                          <summary style={{ cursor: "pointer" }}>Evidence ({a.evidence_count || 0} rows)</summary>
                          <pre style={{ whiteSpace: "pre-wrap", margin: "6px 0 0", fontFamily: "Menlo, monospace", fontSize: 11 }}>{a.support_summary}</pre>
                        </details>
                      )}
                    </div>
                    {a.status === "pending" && (
                      <div style={{ display: "flex", flexDirection: "column", gap: 6, minWidth: 130 }}>
                        <button className="btn-primary" style={{ fontSize: 12, padding: "5px 10px" }}
                                onClick={() => alertAction(a.id, "published")}>✓ Publish</button>
                        <button className="btn-ghost" style={{ fontSize: 12, padding: "5px 10px" }}
                                onClick={() => alertAction(a.id, "rejected")}>✗ Reject</button>
                      </div>
                    )}
                  </div>
                </article>
              );
            })}
          </div>
        </section>
      )}

      {mode === "subs" && (
        <section style={{ padding: "24px 28px", maxWidth: 1100, margin: "0 auto" }}>
          <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 6, flexWrap: "wrap", gap: 12 }}>
            <div>
              <h2 style={{ fontSize: 22, fontWeight: 700, margin: 0 }}>Subscribers</h2>
              <p style={{ fontSize: 13, opacity: 0.6, margin: "4px 0 0", maxWidth: 720 }}>
                Everyone who signed up for the Loading Dock newsletter or a city pulse.
                Unified view across <code>newsletter_signups</code> + <code>pulse_subscriptions</code>.
              </p>
            </div>
            <div style={{ display: "flex", gap: 6, alignItems: "center", flexWrap: "wrap" }}>
              {[
                { id: "all",        label: "All" },
                { id: "newsletter", label: "Newsletter" },
                { id: "pulse",      label: "Pulse" },
              ].map(opt => (
                <button key={opt.id} onClick={() => setSubsKind(opt.id)}
                  className="btn-ghost"
                  style={{
                    padding: "5px 12px", fontSize: 12, borderRadius: 6,
                    border: "1px solid var(--rule)",
                    background: subsKind === opt.id ? "var(--ink)" : "transparent",
                    color: subsKind === opt.id ? "#fff" : "var(--ink)",
                  }}>{opt.label}</button>
              ))}
              <input type="search" value={subsQuery}
                onChange={(e) => setSubsQuery(e.target.value)}
                onKeyDown={(e) => { if (e.key === "Enter") loadSubscribers(); }}
                placeholder="Search email…"
                style={{ fontSize: 12, padding: "6px 10px", border: "1px solid var(--rule)", borderRadius: 6, minWidth: 180 }} />
              <button className="btn-ghost" onClick={loadSubscribers} style={{ fontSize: 12, padding: "5px 12px" }}>
                {subsLoading ? "Loading…" : "Search"}
              </button>
              <a className="btn-primary"
                 href={`/api/admin/subscribers?format=csv&source=${subsKind}${subsQuery.trim() ? `&q=${encodeURIComponent(subsQuery.trim())}` : ""}&token=${encodeURIComponent(token)}&limit=10000`}
                 style={{ fontSize: 12, padding: "5px 12px", textDecoration: "none" }}>
                ↓ Export CSV
              </a>
            </div>
          </div>

          {/* Totals strip */}
          <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(170px, 1fr))", gap: 10, marginTop: 14, marginBottom: 18 }}>
            <article className="admin-row" style={{ padding: "14px 16px" }}>
              <div style={{ fontSize: 10, textTransform: "uppercase", letterSpacing: "0.14em", color: "var(--ink-soft)", marginBottom: 6, fontWeight: 600 }}>All subscribers</div>
              <div style={{ fontSize: 24, fontWeight: 700, lineHeight: 1 }}>{(subsTotals.total || 0).toLocaleString()}</div>
            </article>
            <article className="admin-row" style={{ padding: "14px 16px" }}>
              <div style={{ fontSize: 10, textTransform: "uppercase", letterSpacing: "0.14em", color: "#16a34a", marginBottom: 6, fontWeight: 600 }}>📬 Newsletter</div>
              <div style={{ fontSize: 24, fontWeight: 700, lineHeight: 1 }}>{(subsTotals.newsletter_total || 0).toLocaleString()}</div>
            </article>
            <article className="admin-row" style={{ padding: "14px 16px" }}>
              <div style={{ fontSize: 10, textTransform: "uppercase", letterSpacing: "0.14em", color: "#1d4ed8", marginBottom: 6, fontWeight: 600 }}>📡 Pulse</div>
              <div style={{ fontSize: 24, fontWeight: 700, lineHeight: 1 }}>{(subsTotals.pulse_total || 0).toLocaleString()}</div>
            </article>
          </div>

          {/* By-source breakdown */}
          {subsTotals.by_source && Object.keys(subsTotals.by_source).length > 0 && (
            <article className="admin-row" style={{ padding: "12px 16px", marginBottom: 14 }}>
              <div style={{ fontSize: 11, textTransform: "uppercase", letterSpacing: "0.14em", color: "var(--ink-soft)", marginBottom: 8, fontWeight: 600 }}>Breakdown by source</div>
              <div style={{ display: "flex", flexWrap: "wrap", gap: 12, fontSize: 12 }}>
                {Object.entries(subsTotals.by_source).sort((a, b) => b[1] - a[1]).map(([k, n]) => (
                  <div key={k}><b>{n}</b> <span style={{ color: "var(--ink-soft)" }}>{k}</span></div>
                ))}
              </div>
            </article>
          )}

          {/* Email list */}
          <div style={{ background: "var(--paper, #fff)", border: "1px solid var(--rule)", borderRadius: 4, overflow: "hidden" }}>
            {subs.length === 0 && !subsLoading && (
              <div style={{ padding: 20, textAlign: "center", color: "var(--ink-soft)", fontSize: 13 }}>No subscribers match this filter.</div>
            )}
            {subs.map((s, i) => (
              <div key={s.id} style={{ display: "grid", gridTemplateColumns: "70px 1fr auto auto", gap: 12, padding: "10px 16px", borderBottom: i === subs.length - 1 ? "none" : "1px solid var(--rule)", alignItems: "center" }}>
                <span style={{ fontFamily: "var(--font-mono)", fontSize: 10, textTransform: "uppercase", letterSpacing: "0.10em", color: s.kind === "newsletter" ? "#16a34a" : "#1d4ed8", fontWeight: 700 }}>{s.kind}</span>
                <div style={{ minWidth: 0, fontSize: 13, fontWeight: 500, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{s.email}</div>
                <span style={{ fontSize: 11, color: "var(--ink-soft)" }}>
                  {s.kind === "pulse" ? (s.city_id || "—") : (s.source || "—")}
                </span>
                <span style={{ fontSize: 11, color: "var(--ink-soft)", whiteSpace: "nowrap" }}>{s.created_at ? new Date(s.created_at).toLocaleDateString() : "—"}</span>
              </div>
            ))}
          </div>

          {subs.length > 0 && (
            <div style={{ marginTop: 14, fontSize: 11, color: "var(--ink-soft)", textAlign: "right" }}>
              Showing {subs.length} of {(subsTotals.total || 0).toLocaleString()}. Use search to narrow.
            </div>
          )}
        </section>
      )}

      {mode === "contacts" && (
        <section style={{ padding: "24px 28px", maxWidth: 1240, margin: "0 auto" }}>
          <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 6, flexWrap: "wrap", gap: 12 }}>
            <div>
              <h2 style={{ fontSize: 22, fontWeight: 700, margin: 0 }}>Shipper contacts</h2>
              <p style={{ fontSize: 13, opacity: 0.6, margin: "4px 0 0", maxWidth: 720 }}>
                Scraped from shipper websites. Mostly generic addresses (admin@, info@,
                contact@). The named decision-maker layer (procurement / logistics directors)
                is the paid-tier deliverable still being built.
              </p>
            </div>
            <div style={{ display: "flex", gap: 6, alignItems: "center", flexWrap: "wrap" }}>
              <label style={{ fontSize: 12, display: "flex", alignItems: "center", gap: 6 }}>
                <input type="checkbox" checked={contactsHasEmail} onChange={(e) => setContactsHasEmail(e.target.checked)} />
                Email only
              </label>
              <input type="search" value={contactsQ}
                onChange={(e) => setContactsQ(e.target.value)}
                onKeyDown={(e) => { if (e.key === "Enter") loadContacts(); }}
                placeholder="Search email / name / phone…"
                style={{ fontSize: 12, padding: "6px 10px", border: "1px solid var(--rule)", borderRadius: 6, minWidth: 200 }} />
              <button className="btn-ghost" onClick={loadContacts} style={{ fontSize: 12, padding: "5px 12px" }}>
                {contactsLoading ? "Loading…" : "Search"}
              </button>
              <a className="btn-primary"
                 href={`/api/admin/contacts?format=csv${contactsHasEmail ? "&has_email=1" : ""}${contactsQ.trim() ? `&q=${encodeURIComponent(contactsQ.trim())}` : ""}&token=${encodeURIComponent(token)}&limit=5000`}
                 style={{ fontSize: 12, padding: "5px 12px", textDecoration: "none" }}>
                ↓ Export CSV
              </a>
            </div>
          </div>

          {/* Summary bar */}
          <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(170px, 1fr))", gap: 10, marginTop: 14, marginBottom: 18 }}>
            <article className="admin-row" style={{ padding: "14px 16px" }}>
              <div style={{ fontSize: 10, textTransform: "uppercase", letterSpacing: "0.14em", color: "var(--ink-soft)", marginBottom: 6, fontWeight: 600 }}>Returned</div>
              <div style={{ fontSize: 24, fontWeight: 700, lineHeight: 1 }}>{(contactsSummary.total_returned || 0).toLocaleString()}</div>
            </article>
            <article className="admin-row" style={{ padding: "14px 16px" }}>
              <div style={{ fontSize: 10, textTransform: "uppercase", letterSpacing: "0.14em", color: "#16a34a", marginBottom: 6, fontWeight: 600 }}>📧 With email</div>
              <div style={{ fontSize: 24, fontWeight: 700, lineHeight: 1 }}>{(contactsSummary.with_email || 0).toLocaleString()}</div>
            </article>
            <article className="admin-row" style={{ padding: "14px 16px" }}>
              <div style={{ fontSize: 10, textTransform: "uppercase", letterSpacing: "0.14em", color: "#b45309", marginBottom: 6, fontWeight: 600 }}>👤 Named person</div>
              <div style={{ fontSize: 24, fontWeight: 700, lineHeight: 1 }}>{(contactsSummary.with_named_person || 0).toLocaleString()}</div>
            </article>
          </div>

          {/* Contact table */}
          <div style={{ background: "var(--paper, #fff)", border: "1px solid var(--rule)", borderRadius: 4, overflow: "hidden" }}>
            <div style={{ display: "grid", gridTemplateColumns: "1.5fr 1.2fr 1fr 0.8fr 0.6fr 90px", gap: 10, padding: "8px 14px", background: "var(--paper-soft, #f6f6f4)", borderBottom: "1px solid var(--rule)", fontSize: 10, textTransform: "uppercase", letterSpacing: "0.10em", color: "var(--ink-soft)", fontWeight: 700 }}>
              <span>Shipper</span>
              <span>Email</span>
              <span>Name / Title</span>
              <span>Source</span>
              <span>State</span>
              <span style={{ textAlign: "right" }}>Score</span>
            </div>
            {contacts.length === 0 && !contactsLoading && (
              <div style={{ padding: 20, textAlign: "center", color: "var(--ink-soft)", fontSize: 13 }}>No contacts match. Try unchecking "Email only" or clearing search.</div>
            )}
            {contacts.map((c, i) => (
              <div key={i} style={{ display: "grid", gridTemplateColumns: "1.5fr 1.2fr 1fr 0.8fr 0.6fr 90px", gap: 10, padding: "8px 14px", borderBottom: i === contacts.length - 1 ? "none" : "1px solid var(--rule-soft, #f0efe9)", fontSize: 12, alignItems: "center" }}>
                <div style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
                  <a href={`#/shipper/${c.shipper_slug}`} style={{ color: "var(--ink)", borderBottom: "1px solid var(--rule)" }} title={c.shipper_name}>{c.shipper_name}</a>
                  {c.is_public && c.ticker && <span style={{ marginLeft: 6, fontSize: 10, color: "var(--ink-soft)", fontFamily: "var(--font-mono)" }}>{c.ticker}</span>}
                </div>
                <div style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", fontFamily: "var(--font-mono)" }}>
                  {c.email ? (
                    <a href={`mailto:${c.email}`} style={{ color: "var(--ink)" }}>{c.email}</a>
                  ) : (
                    <span style={{ color: "var(--ink-soft)" }}>—</span>
                  )}
                  {c.email_status && c.email_status !== "verified" && (
                    <span style={{ marginLeft: 6, fontSize: 10, color: "#b45309", fontFamily: "var(--font-sans)" }}>· {c.email_status}</span>
                  )}
                </div>
                <div style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", color: c.full_name ? "var(--ink)" : "var(--ink-soft)" }}>
                  {c.full_name || "(generic)"}
                  {c.title && <span style={{ marginLeft: 4, fontSize: 11, color: "var(--ink-soft)" }}>· {c.title}</span>}
                </div>
                <div style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", color: "var(--ink-soft)", fontSize: 11 }}>{c.source || "—"}</div>
                <div style={{ color: "var(--ink-soft)", fontFamily: "var(--font-mono)", fontSize: 11 }}>{c.shipper_state || "—"}</div>
                <div style={{ textAlign: "right", fontFamily: "var(--font-mono)", fontSize: 11, color: "var(--ink-soft)" }}>{c.shipper_score || 0}</div>
              </div>
            ))}
          </div>

          {/* Footer note */}
          <div style={{ marginTop: 14, padding: "10px 14px", background: "#fef9f3", border: "1px solid #fde4cf", borderRadius: 6, fontSize: 12, color: "var(--ink)", lineHeight: 1.55 }}>
            <strong>⚠ Outreach hygiene:</strong> these are scraped, unverified, mostly-generic addresses.
            Don't send cold outreach from <code>shippingclarity.com</code> — it'll burn your transactional
            sender reputation. Use a secondary domain in Instantly / Smartlead / Apollo. Recipient
            count: {(contactsSummary.with_email || 0).toLocaleString()} email-eligible rows.
          </div>
        </section>
      )}

      {mode === "share" && <ShareCardStudio />}

      {mode === "list" && (
      <section className="admin-list">
        <div className="admin-list-inner">
          {error && <div className="admin-error">{error}</div>}
          {loading && <div className="admin-loading">Loading…</div>}
          {!loading && rows.length === 0 && !error && (
            <div className="admin-empty">No rows match the current filter.</div>
          )}
          {rows.map((r) => (
            <article key={r.id} className="admin-row">
              <div className="admin-row-head">
                <span className="admin-tag" data-sev={r.severity || "low"}>
                  {(r.severity || "—").toUpperCase()}
                </span>
                <span className="admin-meta">
                  <strong>{r.parsed_carrier || "—"}</strong> · {r.parsed_intel_type || "—"} · conf {r.llm_confidence?.toFixed?.(2) || "—"}
                </span>
                <span className="admin-meta admin-meta-right">
                  {r.parsed_city || "—"} · {new Date(r.created_at).toLocaleString()}
                </span>
              </div>
              <p className="admin-row-text">{r.raw_text}</p>
              <div className="admin-row-foot">
                <span className="admin-status">
                  status=<strong>{r.status}</strong> · visibility=<strong>{r.visibility}</strong>
                </span>
                {actionsFor(r)}
              </div>
            </article>
          ))}
        </div>
      </section>
      )}
    </div>
  );
}

// ---------------------------------------------------------------------------
// Share Card Studio — generate a 1200×630 LinkedIn-ready PNG with the
// article title (or any custom headline) baked in. Pure client-side canvas:
// no API calls, no costs, instant preview. The download button writes a
// fresh PNG to the user's machine; they then attach it to the LinkedIn
// post manually alongside the link to the page.
//
// Templates:
//   "article" — editorial headline card. Big serif title, kicker overline,
//               accent-blue rule, footer URL. The default shape.
//   "verdict" — for carrier/broker/shipper dossier shares. Same chassis but
//               with a verdict pill on the right (Trusted / Watch / Distress).
//   "plain"   — minimal: just the wordmark and the title. Lightest weight.
//
// 1200×630 is the LinkedIn / Twitter / OG image canonical size; renders
// crisply when LinkedIn re-encodes for feed.
// ---------------------------------------------------------------------------

const SHARE_CARD_W = 1200;
const SHARE_CARD_H = 630;

// PublishVideoControls — admin button cluster that flips a rendered
// video's is_published flag and surfaces the public /#/v/<slug> URL so
// the user can paste it into a LinkedIn post. Pure UI; the backend is
// /api/admin/publish-video.
// VisitorStatsPanel — surfaces /api/admin/visitor-stats in the Overview
// tab. Five glanceable cells (today / week / month / year / all-time)
// for both pageviews and unique visitors, plus the top-10 most-visited
// paths over the last 30 days. Best-effort — if the API is down or the
// page_views table doesn't exist yet, the panel just hides itself.
function VisitorStatsPanel({ token }) {
  const [stats, setStats] = useStateA(null);
  const [loading, setLoading] = useStateA(true);
  const [err, setErr] = useStateA(null);

  useEffectA(() => {
    if (!token) return;
    let alive = true;
    (async () => {
      setLoading(true); setErr(null);
      try {
        const r = await fetch("/api/admin/visitor-stats", {
          headers: { Authorization: `Bearer ${token}` },
        });
        if (!r.ok) throw new Error(`${r.status}`);
        const d = await r.json();
        if (alive) setStats(d);
      } catch (e) {
        if (alive) setErr(e.message || String(e));
      }
      if (alive) setLoading(false);
    })();
    return () => { alive = false; };
  }, [token]);

  if (loading && !stats) {
    return (
      <article className="admin-row" style={{ padding: "20px 24px", marginBottom: 24 }}>
        <h3 style={{ fontSize: 14, textTransform: "uppercase", letterSpacing: "0.12em", color: "var(--ink-soft)", margin: "0 0 14px" }}>Visitors</h3>
        <div style={{ fontSize: 13, color: "var(--ink-soft)" }}>Loading…</div>
      </article>
    );
  }
  if (err || !stats) {
    return (
      <article className="admin-row" style={{ padding: "20px 24px", marginBottom: 24 }}>
        <h3 style={{ fontSize: 14, textTransform: "uppercase", letterSpacing: "0.12em", color: "var(--ink-soft)", margin: "0 0 14px" }}>Visitors</h3>
        <div style={{ fontSize: 13, color: "var(--ink-soft)" }}>
          Stats unavailable. Apply <code>sql/page_views.sql</code> in Supabase to enable.
        </div>
      </article>
    );
  }

  const { totals = {}, uniques = {}, top_paths = [] } = stats;
  const cells = [
    { label: "Today",       pv: totals.day,   uv: uniques.day },
    { label: "Last 7 days", pv: totals.week,  uv: uniques.week },
    { label: "Last 30 days",pv: totals.month, uv: uniques.month },
    { label: "Last 365 days", pv: totals.year, uv: uniques.year },
    { label: "All time",    pv: totals.all,   uv: uniques.all },
  ];

  return (
    <article className="admin-row" style={{ padding: "20px 24px", marginBottom: 24 }}>
      <div style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline", marginBottom: 14, flexWrap: "wrap", gap: 8 }}>
        <h3 style={{ fontSize: 14, textTransform: "uppercase", letterSpacing: "0.12em", color: "var(--ink-soft)", margin: 0 }}>
          Visitors
        </h3>
        <span style={{ fontSize: 11, color: "var(--ink-soft)" }}>
          Pageviews · Unique visitors (IP+UA hashed daily)
        </span>
      </div>

      <div style={{ display: "grid", gridTemplateColumns: "repeat(5, 1fr)", gap: 12, marginBottom: 18 }}>
        {cells.map((c) => (
          <div key={c.label} style={{ padding: "10px 12px", background: "var(--paper-soft, #f6f6f4)",
                                      border: "1px solid var(--rule)", borderLeft: "3px solid #5fa9ff",
                                      borderRadius: 4 }}>
            <div style={{ fontSize: 10, fontWeight: 700, letterSpacing: "0.10em",
                          textTransform: "uppercase", color: "var(--ink-soft)", marginBottom: 4 }}>
              {c.label}
            </div>
            <div style={{ display: "flex", alignItems: "baseline", gap: 8 }}>
              <span style={{ fontSize: 22, fontWeight: 700 }}>{(c.pv || 0).toLocaleString()}</span>
              <span style={{ fontSize: 12, color: "var(--ink-soft)" }}>
                · {(c.uv || 0).toLocaleString()} unique
              </span>
            </div>
          </div>
        ))}
      </div>

      {top_paths.length > 0 && (
        <div>
          <div style={{ fontSize: 10, fontWeight: 700, letterSpacing: "0.10em",
                        textTransform: "uppercase", color: "var(--ink-soft)", marginBottom: 8 }}>
            Top paths · last 30 days
          </div>
          <div style={{ display: "grid", gridTemplateColumns: "1fr auto", rowGap: 4, columnGap: 16, fontSize: 13 }}>
            {top_paths.slice(0, 10).map((row) => (
              <React.Fragment key={row.path}>
                <code style={{ fontSize: 12, color: "var(--ink)", whiteSpace: "nowrap",
                               overflow: "hidden", textOverflow: "ellipsis" }}>
                  {row.path}
                </code>
                <span style={{ fontFamily: "var(--font-mono)", color: "var(--ink-soft)" }}>
                  {row.count.toLocaleString()}
                </span>
              </React.Fragment>
            ))}
          </div>
        </div>
      )}

      {/* GEO BREAKDOWN — countries, cities, ISPs/orgs */}
      {(stats.top_countries?.length || stats.top_cities?.length || stats.top_orgs?.length) && (
        <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(180px, 1fr))", gap: 14, marginTop: 18, paddingTop: 14, borderTop: "1px solid var(--rule)" }}>
          {stats.top_countries?.length > 0 && (
            <div>
              <div style={{ fontSize: 10, fontWeight: 700, letterSpacing: "0.10em", textTransform: "uppercase", color: "var(--ink-soft)", marginBottom: 8 }}>🌍 Top countries · 30d</div>
              {stats.top_countries.slice(0, 6).map(row => (
                <div key={row.country} style={{ display: "flex", justifyContent: "space-between", padding: "3px 0", fontSize: 12 }}>
                  <span>{row.country}</span>
                  <span style={{ fontFamily: "var(--font-mono)", color: "var(--ink-soft)" }}>{row.count.toLocaleString()}</span>
                </div>
              ))}
            </div>
          )}
          {stats.top_cities?.length > 0 && (
            <div>
              <div style={{ fontSize: 10, fontWeight: 700, letterSpacing: "0.10em", textTransform: "uppercase", color: "var(--ink-soft)", marginBottom: 8 }}>🏙 Top cities · 30d</div>
              {stats.top_cities.slice(0, 6).map(row => (
                <div key={row.city} style={{ display: "flex", justifyContent: "space-between", padding: "3px 0", fontSize: 12 }}>
                  <span style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{row.city}</span>
                  <span style={{ fontFamily: "var(--font-mono)", color: "var(--ink-soft)" }}>{row.count.toLocaleString()}</span>
                </div>
              ))}
            </div>
          )}
          {stats.top_orgs?.length > 0 && (
            <div>
              <div style={{ fontSize: 10, fontWeight: 700, letterSpacing: "0.10em", textTransform: "uppercase", color: "var(--ink-soft)", marginBottom: 8 }}>🛰 Top ISPs / orgs · 30d</div>
              {stats.top_orgs.slice(0, 6).map(row => (
                <div key={row.org} style={{ display: "flex", justifyContent: "space-between", padding: "3px 0", fontSize: 12 }}>
                  <span style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", marginRight: 6 }}>{row.org}</span>
                  <span style={{ fontFamily: "var(--font-mono)", color: "var(--ink-soft)", flexShrink: 0 }}>{row.count.toLocaleString()}</span>
                </div>
              ))}
            </div>
          )}
        </div>
      )}

      {/* RECENT VISITS — privacy-preserving log */}
      {Array.isArray(stats.recent_visits) && stats.recent_visits.length > 0 && (
        <div style={{ marginTop: 18, paddingTop: 14, borderTop: "1px solid var(--rule)" }}>
          <div style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline", marginBottom: 10 }}>
            <div style={{ fontSize: 10, fontWeight: 700, letterSpacing: "0.10em", textTransform: "uppercase", color: "var(--ink-soft)" }}>
              👁 Recent visits · last 7 days, owner-excluded
            </div>
            <span style={{ fontSize: 10, color: "var(--ink-soft)" }}>{stats.recent_visits.length} visits</span>
          </div>
          <div style={{ background: "var(--paper-soft, #f8f7f1)", border: "1px solid var(--rule-soft, #f0efe9)", borderRadius: 4, maxHeight: 360, overflowY: "auto" }}>
            {stats.recent_visits.map((v, i) => {
              const loc = [v.city, v.region, v.country].filter(Boolean).join(", ") || "—";
              return (
                <div key={i} style={{ display: "grid", gridTemplateColumns: "100px 1fr 1fr 90px 70px", gap: 10, padding: "7px 12px", borderBottom: i === stats.recent_visits.length - 1 ? "none" : "1px solid var(--rule-soft, #f0efe9)", fontSize: 11, alignItems: "center" }}>
                  <span style={{ fontFamily: "var(--font-mono)", color: "var(--ink-soft)", whiteSpace: "nowrap" }}>{new Date(v.created_at).toLocaleString(undefined, { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" })}</span>
                  <span style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }} title={loc}><b>{loc}</b></span>
                  <span style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", color: "var(--ink-soft)" }} title={v.org || ""}>{v.org || "—"}</span>
                  <span style={{ fontFamily: "var(--font-mono)", fontSize: 10, color: "var(--ink-soft)" }} title="Anonymized visitor ID (last 6 of ip_hash). Same person = same code.">#{v.visitor_id || "anon"}</span>
                  <code style={{ fontSize: 10, color: "var(--ink-soft)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }} title={v.path}>{(v.path || "").replace(/^#?\//, "/")}</code>
                </div>
              );
            })}
          </div>
          <div style={{ marginTop: 8, fontSize: 11, color: "var(--ink-soft)", lineHeight: 1.5 }}>
            We don't store raw IPs (privacy-by-design — see <code>sql/page_views.sql</code>). City / region / country / ISP-org are captured at the time of visit via geo lookup. Visitor IDs are the last 6 chars of <code>sha256(ip + salt)</code> — repeats identify the same visitor without revealing the IP.
          </div>
        </div>
      )}
    </article>
  );
}

function PublishVideoControls({ draft, token, onPublished }) {
  const [busy, setBusy] = useStateA(false);
  const [msg,  setMsg]  = useStateA(null);
  const [copied, setCopied] = useStateA(false);

  const isPublished = !!draft.is_published;
  const slug = draft.slug || "";
  const publicUrl = slug ? `${window.location.origin}/#/v/${slug}` : "";

  async function publish() {
    if (busy) return;
    setBusy(true); setMsg("Publishing…");
    try {
      const r = await fetch("/api/admin/publish-video", {
        method: "POST",
        headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
        body: JSON.stringify({ id: draft.id }),
      });
      const j = await r.json().catch(() => ({}));
      if (!r.ok) throw new Error(j.error || `${r.status}`);
      setMsg(`✓ Published — ${j.public_url}`);
      if (onPublished) onPublished();
      setTimeout(() => setMsg(null), 4500);
    } catch (e) {
      setMsg(`✗ ${e.message || e}`);
      setTimeout(() => setMsg(null), 4500);
    }
    setBusy(false);
  }

  function copyLink() {
    if (!publicUrl) return;
    if (navigator.clipboard) {
      navigator.clipboard.writeText(publicUrl).then(() => {
        setCopied(true);
        setTimeout(() => setCopied(false), 1800);
      }, () => {});
    }
  }

  return (
    <div style={{ marginTop: 10, padding: "10px 12px",
                  background: "var(--paper-2, #f6f6f4)",
                  border: "1px solid var(--rule)",
                  borderLeft: "3px solid #5fa9ff",
                  borderRadius: 4 }}>
      {isPublished ? (
        <div style={{ fontSize: 12, lineHeight: 1.5 }}>
          <div style={{ display: "flex", alignItems: "center", gap: 6, marginBottom: 4 }}>
            <span style={{ fontSize: 10, fontWeight: 700, letterSpacing: "0.12em",
                           color: "#2a8a3e", textTransform: "uppercase" }}>
              ● Published
            </span>
            {draft.published_at && (
              <span style={{ color: "var(--ink-soft)", fontSize: 11 }}>
                {new Date(draft.published_at).toLocaleString()}
              </span>
            )}
          </div>
          <div style={{ display: "flex", gap: 8, alignItems: "center", flexWrap: "wrap" }}>
            <a href={publicUrl} target="_blank" rel="noopener noreferrer"
               style={{ fontFamily: "var(--font-mono)", fontSize: 12, color: "#0a1224", wordBreak: "break-all" }}>
              {publicUrl}
            </a>
            <button onClick={copyLink}
                    style={{ padding: "4px 10px", fontSize: 11, fontWeight: 600,
                             background: "#5fa9ff", color: "#0a1224", border: "none",
                             borderRadius: 3, cursor: "pointer" }}>
              {copied ? "✓ Copied" : "Copy URL"}
            </button>
          </div>
          <div style={{ fontSize: 11, color: "var(--ink-soft)", marginTop: 6, lineHeight: 1.4 }}>
            Paste this URL into your LinkedIn post — LinkedIn unfurls it as a card linking back to the video page.
          </div>
        </div>
      ) : (
        <div style={{ display: "flex", gap: 10, alignItems: "center", flexWrap: "wrap" }}>
          <button onClick={publish} disabled={busy}
                  style={{ padding: "6px 14px", fontSize: 13, fontWeight: 600,
                           background: "#5fa9ff", color: "#0a1224", border: "none",
                           borderRadius: 4, cursor: busy ? "wait" : "pointer" }}>
            {busy ? "Publishing…" : "▶ Publish to site"}
          </button>
          <span style={{ fontSize: 11, color: "var(--ink-soft)", lineHeight: 1.4, flex: 1, minWidth: 200 }}>
            Posts this video at <code style={{ fontSize: 11 }}>/#/v/&lt;slug&gt;</code> so you can link to it from LinkedIn.
          </span>
        </div>
      )}
      {msg && (
        <div style={{ marginTop: 8, padding: "6px 8px", fontSize: 11,
                      background: "var(--paper, #fff)", border: "1px solid var(--rule)",
                      borderRadius: 3, fontFamily: "var(--font-mono)" }}>
          {msg}
        </div>
      )}
    </div>
  );
}

// =============================================================================
// SHOT EDITOR — opens a modal where the editor can fix a rendered video's
// shot list (delete bad frames, reorder, edit captions/durations, swap the
// imageUrl on ken-burns-still / satellite-zoom shots), then re-render.
// Re-render reuses the existing voiceover MP3 and the shots_json the editor
// just saved, skipping Sonnet planning + ElevenLabs entirely.
// =============================================================================

function ShotEditorTrigger({ draft, token, onRerendered }) {
  const [open, setOpen] = React.useState(false);
  return (
    <>
      <button
        className="btn-ghost"
        style={{ marginTop: 8, fontSize: 12, padding: "4px 10px" }}
        onClick={() => setOpen(true)}
      >
        ✎ Edit shots
      </button>
      {open && (
        <ShotEditorModal
          draftId={draft.id}
          token={token}
          onClose={() => setOpen(false)}
          onRerendered={() => { setOpen(false); onRerendered && onRerendered(); }}
        />
      )}
    </>
  );
}

function ShotEditorModal({ draftId, token, onClose, onRerendered }) {
  const [data, setData]   = React.useState(null);
  const [shots, setShots] = React.useState([]);
  const [busy, setBusy]   = React.useState(false);
  const [msg, setMsg]     = React.useState(null);
  const [err, setErr]     = React.useState(null);
  // Thumbnails generated by scrubbing the rendered MP4 client-side. Map of
  // shot-index → data URL. Generated lazily once the MP4 is loaded.
  const [thumbs, setThumbs] = React.useState({});
  // Preview state: which shot the user wants to watch full-size at the
  // moment. Null = closed.
  const [previewIdx, setPreviewIdx] = React.useState(null);

  React.useEffect(() => {
    let alive = true;
    fetch(`/api/admin/video-shots?id=${draftId}`, {
      headers: { Authorization: `Bearer ${token}` },
    })
      .then((r) => r.ok ? r.json() : r.json().then((j) => Promise.reject(j)))
      .then((d) => { if (alive) { setData(d); setShots(d.shots || []); } })
      .catch((e) => { if (alive) setErr(e?.error || "Failed to load shots"); });
    return () => { alive = false; };
  }, [draftId, token]);

  // Cumulative start time (seconds) of each shot in the rendered MP4.
  // Drives both the thumbnail-scrub timestamps AND the preview-jump
  // currentTime. Recomputed whenever shots change so reorder/edit stays
  // in sync.
  const startTimes = React.useMemo(() => {
    let acc = 0;
    return shots.map((s) => {
      const start = acc;
      acc += Math.max(1, parseInt(s.duration_seconds, 10) || 6);
      return start;
    });
  }, [shots]);

  // Generate a thumbnail for each shot by scrubbing the rendered MP4. We
  // do this client-side so there's zero server cost. Sequenced (not
  // parallel) because seeking the same <video> element from concurrent
  // promises is racy. ~200-400ms per shot on a typical connection.
  React.useEffect(() => {
    if (!data || !data.video_public_url || shots.length === 0) return;
    let cancelled = false;
    const video = document.createElement("video");
    video.crossOrigin = "anonymous";
    video.muted = true;
    video.preload = "auto";
    video.src = data.video_public_url;

    function seekAndCapture(t) {
      return new Promise((resolve) => {
        const onSeeked = () => {
          video.removeEventListener("seeked", onSeeked);
          try {
            const w = 240;
            const h = Math.round((video.videoHeight / video.videoWidth) * w) || 135;
            const canvas = document.createElement("canvas");
            canvas.width = w; canvas.height = h;
            const ctx = canvas.getContext("2d");
            ctx.drawImage(video, 0, 0, w, h);
            resolve(canvas.toDataURL("image/jpeg", 0.7));
          } catch (e) {
            // CORS-tainted canvas → resolve null and the UI shows a
            // fallback. Should not happen since the bucket allows
            // anonymous reads, but handle it anyway.
            resolve(null);
          }
        };
        video.addEventListener("seeked", onSeeked, { once: true });
        try { video.currentTime = t; }
        catch (_) { resolve(null); }
      });
    }

    video.addEventListener("loadedmetadata", async () => {
      if (cancelled) return;
      const out = {};
      for (let i = 0; i < shots.length; i++) {
        if (cancelled) return;
        const dur = Math.max(1, parseInt(shots[i].duration_seconds, 10) || 6);
        // Aim for the visual middle of the shot — most stable frame.
        const t = Math.min(video.duration - 0.05, startTimes[i] + dur / 2);
        const dataUrl = await seekAndCapture(t);
        if (cancelled) return;
        if (dataUrl) {
          out[i] = dataUrl;
          // Update progressively so the user sees thumbs appear instead of
          // waiting for all of them.
          setThumbs((prev) => ({ ...prev, [i]: dataUrl }));
        }
      }
    });

    return () => {
      cancelled = true;
      try { video.removeAttribute("src"); video.load(); } catch (_) {}
    };
    // Intentionally only re-run when video URL or shot count changes —
    // re-scrubbing the whole video on every caption edit would be wasteful.
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [data && data.video_public_url, shots.length]);

  function patchShot(idx, patch) {
    setShots((cur) => cur.map((s, i) => i === idx ? { ...s, ...patch } : s));
  }
  function patchParam(idx, key, value) {
    setShots((cur) => cur.map((s, i) => i === idx
      ? { ...s, params: { ...(s.params || {}), [key]: value } }
      : s));
  }
  function deleteShot(idx) {
    setShots((cur) => cur.filter((_, i) => i !== idx));
  }
  function moveShot(idx, dir) {
    setShots((cur) => {
      const next = [...cur];
      const j = idx + dir;
      if (j < 0 || j >= next.length) return cur;
      [next[idx], next[j]] = [next[j], next[idx]];
      return next;
    });
  }

  async function save() {
    setBusy(true); setMsg("Saving…");
    try {
      const r = await fetch(`/api/admin/video-shots`, {
        method: "PATCH",
        headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
        body: JSON.stringify({ id: draftId, shots }),
      });
      if (!r.ok) {
        const j = await r.json().catch(() => ({}));
        throw new Error(j.error || `save failed (${r.status})`);
      }
      const j = await r.json();
      setMsg(`✓ Saved ${j.shots_count} shots`);
    } catch (e) { setMsg(`✗ ${e.message || e}`); }
    setBusy(false);
  }

  async function saveAndRerender() {
    setBusy(true); setMsg("Saving + dispatching re-render…");
    try {
      const sr = await fetch(`/api/admin/video-shots`, {
        method: "PATCH",
        headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
        body: JSON.stringify({ id: draftId, shots }),
      });
      if (!sr.ok) {
        const j = await sr.json().catch(() => ({}));
        throw new Error(j.error || `save failed (${sr.status})`);
      }
      const dr = await fetch(`/api/admin/video-trigger`, {
        method: "POST",
        headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
        body: JSON.stringify({ rerender_id: draftId }),
      });
      if (!dr.ok) {
        const j = await dr.json().catch(() => ({}));
        throw new Error(j.error || `dispatch failed (${dr.status})`);
      }
      setMsg("✓ Re-render dispatched. Should land in ~3-5 min (no TTS, no planning).");
      setTimeout(() => onRerendered && onRerendered(), 1500);
    } catch (e) { setMsg(`✗ ${e.message || e}`); }
    setBusy(false);
  }

  if (err) {
    return (
      <ShotEditorScrim onClose={onClose}>
        <div style={{ padding: 24 }}>
          <h3 style={{ marginTop: 0 }}>Couldn't load shots</h3>
          <p style={{ color: "var(--ink-soft)" }}>{err}</p>
          <button onClick={onClose}>Close</button>
        </div>
      </ShotEditorScrim>
    );
  }
  if (!data) {
    return (
      <ShotEditorScrim onClose={onClose}>
        <div style={{ padding: 40, textAlign: "center", color: "var(--ink-soft)" }}>Loading…</div>
      </ShotEditorScrim>
    );
  }
  if (!data.has_voiceover) {
    return (
      <ShotEditorScrim onClose={onClose}>
        <div style={{ padding: 24, maxWidth: 540 }}>
          <h3 style={{ marginTop: 0 }}>Re-render not available</h3>
          <p style={{ color: "var(--ink-soft)", lineHeight: 1.5 }}>
            This draft doesn't have a saved voiceover yet, so it can't be re-rendered
            without re-running ElevenLabs (which the Shot Editor avoids by design).
            This usually means the original render failed mid-pipeline.
          </p>
          <p style={{ color: "var(--ink-soft)", lineHeight: 1.5 }}>
            Re-make the video from scratch via the "Make a video" form, then come back here.
          </p>
          <button onClick={onClose}>Close</button>
        </div>
      </ShotEditorScrim>
    );
  }

  return (
    <ShotEditorScrim onClose={onClose}>
      <div style={{ padding: "20px 24px", borderBottom: "1px solid var(--rule)", display: "flex", alignItems: "baseline", gap: 12, flexWrap: "wrap" }}>
        <div style={{ flex: 1, minWidth: 280 }}>
          <h3 style={{ margin: 0, fontSize: 18 }}>Shot editor — {data.title || "(untitled)"}</h3>
          <div style={{ fontSize: 12, color: "var(--ink-soft)", marginTop: 2 }}>
            {shots.length} shots · {data.format} · status: {data.status}
            {data.duration_seconds ? ` · ${data.duration_seconds}s` : ""}
          </div>
        </div>
        <button onClick={onClose} className="btn-ghost" style={{ fontSize: 12 }}>✕ Close</button>
      </div>

      <div style={{ padding: "10px 24px", background: "#f6f6f4", fontSize: 12, color: "var(--ink-soft)", lineHeight: 1.5 }}>
        Edit captions, durations, or swap a stock-photo URL on <code>ken-burns-still</code> / <code>satellite-zoom</code> shots.
        Delete shots that look bad. Reorder with the ↑/↓ buttons. "Save + Re-render" reuses
        the existing voiceover (no ElevenLabs cost) and reruns ffmpeg only.
      </div>

      <div style={{ padding: 16, overflowY: "auto", flex: 1, minHeight: 0 }}>
        {shots.length === 0 && (
          <div style={{ padding: 30, textAlign: "center", color: "var(--ink-soft)" }}>
            All shots deleted. Add at least one back, or close to keep the original list.
          </div>
        )}
        {shots.map((s, i) => {
          const kind = s.shot ? "library" : "template";
          const tplName = s.template || s.shot;
          const showImageUrl = s.template === "ken-burns-still" || s.template === "satellite-zoom";
          const thumb = thumbs[i];
          return (
            <div key={i} style={{
              border: "1px solid var(--rule)", borderRadius: 6, padding: 12, marginBottom: 10,
              background: "#fff",
              display: "grid", gridTemplateColumns: "auto 1fr", gap: 12,
            }}>
              {/* Thumbnail column — scrubbed from the rendered MP4 at the
                  middle of this shot. Click to open a full-size preview
                  jumped to the right timestamp. */}
              <div
                onClick={() => setPreviewIdx(i)}
                title={`Preview shot at ${Math.round(startTimes[i])}s`}
                style={{
                  width: 140, height: 80, borderRadius: 4, overflow: "hidden",
                  background: thumb ? "#000" : "#1a2238",
                  border: "1px solid rgba(0,0,0,0.10)",
                  cursor: data && data.video_public_url ? "pointer" : "default",
                  display: "flex", alignItems: "center", justifyContent: "center",
                  flexShrink: 0, position: "relative",
                }}
              >
                {thumb
                  ? <img src={thumb} alt={`Shot ${i + 1}`} style={{ width: "100%", height: "100%", objectFit: "cover" }} />
                  : <span style={{ fontSize: 10, color: "rgba(255,255,255,0.5)", textAlign: "center", padding: 6 }}>
                      {data && data.video_public_url ? "loading…" : "no video"}
                    </span>}
                {thumb && (
                  <div style={{
                    position: "absolute", inset: 0,
                    background: "linear-gradient(180deg,rgba(0,0,0,0) 60%,rgba(0,0,0,0.45) 100%)",
                    display: "flex", alignItems: "flex-end", justifyContent: "space-between",
                    padding: "4px 6px", color: "#fff", fontSize: 10,
                    fontFamily: "var(--font-mono)", letterSpacing: "0.05em",
                    pointerEvents: "none",
                  }}>
                    <span>▶</span>
                    <span>{fmtMmSs(startTimes[i])}</span>
                  </div>
                )}
              </div>
              <div>
              <div style={{ display: "flex", gap: 8, alignItems: "baseline", marginBottom: 8, flexWrap: "wrap" }}>
                <span style={{
                  fontFamily: "var(--font-mono)", fontSize: 10, letterSpacing: "0.08em",
                  textTransform: "uppercase", padding: "2px 6px", borderRadius: 3,
                  background: kind === "library" ? "#e6f0ff" : "#f0e6ff",
                  color: "#0a1224",
                }}>
                  #{i + 1} · {kind} · {tplName}
                </span>
                <div style={{ marginLeft: "auto", display: "flex", gap: 4 }}>
                  <button onClick={() => moveShot(i, -1)} disabled={i === 0} className="btn-ghost" style={{ padding: "2px 8px", fontSize: 12 }}>↑</button>
                  <button onClick={() => moveShot(i, 1)}  disabled={i === shots.length - 1} className="btn-ghost" style={{ padding: "2px 8px", fontSize: 12 }}>↓</button>
                  <button onClick={() => deleteShot(i)} className="btn-ghost" style={{ padding: "2px 8px", fontSize: 12, color: "#c0392b" }}>🗑</button>
                </div>
              </div>

              <div style={{ display: "grid", gridTemplateColumns: "1fr 100px", gap: 10 }}>
                <label>
                  <span style={{ display: "block", fontSize: 10, fontWeight: 600, color: "var(--ink-soft)", marginBottom: 3, textTransform: "uppercase", letterSpacing: "0.06em" }}>Caption</span>
                  <input
                    type="text"
                    value={s.caption || ""}
                    onChange={(e) => patchShot(i, { caption: e.target.value })}
                    style={{ width: "100%", padding: "6px 8px", fontSize: 13, border: "1px solid var(--rule)", borderRadius: 3 }}
                  />
                </label>
                <label>
                  <span style={{ display: "block", fontSize: 10, fontWeight: 600, color: "var(--ink-soft)", marginBottom: 3, textTransform: "uppercase", letterSpacing: "0.06em" }}>Duration (s)</span>
                  <input
                    type="number"
                    min={1}
                    max={60}
                    value={s.duration_seconds || ""}
                    onChange={(e) => patchShot(i, { duration_seconds: parseInt(e.target.value, 10) || 0 })}
                    style={{ width: "100%", padding: "6px 8px", fontSize: 13, border: "1px solid var(--rule)", borderRadius: 3 }}
                  />
                </label>
              </div>

              {showImageUrl && (
                <label style={{ display: "block", marginTop: 8 }}>
                  <span style={{ display: "block", fontSize: 10, fontWeight: 600, color: "var(--ink-soft)", marginBottom: 3, textTransform: "uppercase", letterSpacing: "0.06em" }}>
                    Image URL (override) — leave blank to let Pexels pick
                  </span>
                  <input
                    type="url"
                    value={(s.params && s.params.imageUrl) || ""}
                    onChange={(e) => patchParam(i, "imageUrl", e.target.value)}
                    placeholder="https://..."
                    style={{ width: "100%", padding: "6px 8px", fontSize: 12, border: "1px solid var(--rule)", borderRadius: 3, fontFamily: "var(--font-mono)" }}
                  />
                </label>
              )}

              {/* Show the planner's params as read-only JSON for context */}
              {s.params && Object.keys(s.params).length > 0 && (
                <details style={{ marginTop: 8 }}>
                  <summary style={{ fontSize: 11, color: "var(--ink-soft)", cursor: "pointer" }}>
                    params ({Object.keys(s.params).length} keys)
                  </summary>
                  <pre style={{ fontSize: 10, marginTop: 6, padding: 8, background: "#f6f6f4", borderRadius: 3, overflow: "auto", maxHeight: 160 }}>
{JSON.stringify(s.params, null, 2)}
                  </pre>
                </details>
              )}
              </div>
            </div>
          );
        })}
      </div>

      {/* Preview overlay — full-width <video> jumped to the start of the
          selected shot. Closes on click outside or × button. */}
      {previewIdx !== null && data && data.video_public_url && (
        <ShotPreviewOverlay
          videoUrl={data.video_public_url}
          startAt={startTimes[previewIdx] || 0}
          shotNumber={previewIdx + 1}
          onClose={() => setPreviewIdx(null)}
        />
      )}

      <div style={{ padding: "14px 24px", borderTop: "1px solid var(--rule)", display: "flex", gap: 10, alignItems: "center", flexWrap: "wrap" }}>
        {msg && (
          <span style={{ fontSize: 12, color: "var(--ink-soft)", flex: 1, minWidth: 200 }}>{msg}</span>
        )}
        <button onClick={save} disabled={busy || shots.length === 0} className="btn-ghost">
          Save only
        </button>
        <button onClick={saveAndRerender} disabled={busy || shots.length === 0} className="btn-primary">
          {busy ? "Working…" : "▶ Save + Re-render"}
        </button>
      </div>
    </ShotEditorScrim>
  );
}

function fmtMmSs(secs) {
  const s = Math.max(0, Math.round(secs || 0));
  const m = Math.floor(s / 60);
  const r = s % 60;
  return `${m}:${String(r).padStart(2, "0")}`;
}

// Plays the rendered MP4 jumped to the start of a specific shot, so the
// editor can see the actual frame as it appears in the final video. Auto-
// pauses ~1s past the next shot boundary if known (caller passes startAt
// only; the overlay just plays from there until the user closes).
function ShotPreviewOverlay({ videoUrl, startAt, shotNumber, onClose }) {
  const ref = React.useRef(null);
  React.useEffect(() => {
    const v = ref.current;
    if (!v) return;
    const onMeta = () => {
      try { v.currentTime = Math.max(0, startAt); v.play(); } catch (_) {}
    };
    v.addEventListener("loadedmetadata", onMeta, { once: true });
    return () => v.removeEventListener("loadedmetadata", onMeta);
  }, [videoUrl, startAt]);
  return (
    <div
      onClick={onClose}
      style={{
        position: "fixed", inset: 0, background: "rgba(0,0,0,0.85)",
        display: "flex", alignItems: "center", justifyContent: "center",
        zIndex: 1100, padding: 16,
      }}
    >
      <div onClick={(e) => e.stopPropagation()} style={{
        position: "relative", width: "100%", maxWidth: 960,
      }}>
        <button onClick={onClose} style={{
          position: "absolute", top: -36, right: 0,
          background: "transparent", border: "none", color: "#fff",
          fontSize: 14, cursor: "pointer", padding: "4px 10px",
        }}>
          ✕ Close preview
        </button>
        <div style={{
          fontFamily: "var(--font-mono, monospace)", fontSize: 11, color: "rgba(255,255,255,0.65)",
          letterSpacing: "0.08em", textTransform: "uppercase", marginBottom: 8,
        }}>
          Shot #{shotNumber} · jumped to {fmtMmSs(startAt)}
        </div>
        <video
          ref={ref}
          src={videoUrl}
          controls
          autoPlay
          style={{ width: "100%", borderRadius: 6, background: "#000", display: "block" }}
        />
      </div>
    </div>
  );
}

function ShotEditorScrim({ children, onClose }) {
  return (
    <div
      onClick={onClose}
      style={{
        position: "fixed", inset: 0, background: "rgba(10,18,36,0.65)",
        display: "flex", alignItems: "center", justifyContent: "center",
        zIndex: 1000, padding: 16,
      }}
    >
      <div
        onClick={(e) => e.stopPropagation()}
        style={{
          background: "#fff", borderRadius: 8, width: "100%", maxWidth: 760,
          maxHeight: "92vh", display: "flex", flexDirection: "column",
          boxShadow: "0 30px 80px rgba(0,0,0,0.4)",
        }}
      >
        {children}
      </div>
    </div>
  );
}

window.ShotEditorTrigger = ShotEditorTrigger;
window.ShotEditorModal   = ShotEditorModal;

// Theme presets — each defines the background gradient stops, ink color,
// subtle/faint text colors, default accent, and grid-line color. Keeps
// drawShareCard() polymorphic without a giant switch.
const SHARE_CARD_THEMES = {
  navy: {
    label: "Dark navy (default)",
    bg: ["#1d2d52", "#0a1224", "#050913"],
    ink: "#ffffff", subtle: "rgba(255,255,255,0.62)", faint: "rgba(255,255,255,0.42)",
    rule: "rgba(255,255,255,0.16)", accent: "#5fa9ff", grid: "#5fa9ff",
  },
  parchment: {
    label: "Light parchment",
    bg: ["#f5f1e8", "#ece7d8", "#e0dac7"],
    ink: "#1a1a1a", subtle: "rgba(26,26,26,0.65)", faint: "rgba(26,26,26,0.42)",
    rule: "rgba(26,26,26,0.18)", accent: "#0a3a8a", grid: "#0a3a8a",
  },
  alert: {
    label: "Red alert (distress)",
    bg: ["#3a0d0d", "#1f0606", "#0a0202"],
    ink: "#ffffff", subtle: "rgba(255,255,255,0.62)", faint: "rgba(255,255,255,0.42)",
    rule: "rgba(255,255,255,0.18)", accent: "#ff7a7a", grid: "#ff7a7a",
  },
  amber: {
    label: "Amber watch",
    bg: ["#3a2d0a", "#1f1804", "#0a0701"],
    ink: "#ffffff", subtle: "rgba(255,255,255,0.62)", faint: "rgba(255,255,255,0.42)",
    rule: "rgba(255,255,255,0.18)", accent: "#ffc864", grid: "#ffc864",
  },
  clear: {
    label: "Green clear",
    bg: ["#0e3a23", "#062014", "#020a06"],
    ink: "#ffffff", subtle: "rgba(255,255,255,0.62)", faint: "rgba(255,255,255,0.42)",
    rule: "rgba(255,255,255,0.18)", accent: "#9eff9e", grid: "#9eff9e",
  },
  ink: {
    label: "Pure ink (high contrast)",
    bg: ["#000000", "#000000", "#000000"],
    ink: "#ffffff", subtle: "rgba(255,255,255,0.62)", faint: "rgba(255,255,255,0.42)",
    rule: "rgba(255,255,255,0.16)", accent: "#ffffff", grid: "#ffffff",
  },
};

function ShareCardStudio() {
  const [template, setTemplate] = useStateA("article");
  const [title, setTitle] = useStateA("");
  const [kicker, setKicker] = useStateA("REPORT");
  const [subline, setSubline] = useStateA("");
  const [verdict, setVerdict] = useStateA("Watch");
  const [verdictScore, setVerdictScore] = useStateA("62");
  const [statValue, setStatValue] = useStateA("");
  const [statUnit, setStatUnit] = useStateA("");
  const [footer, setFooter] = useStateA("shippingclarity.com");
  // Customization state — theme preset, accent override, optional bg image.
  const [themeKey, setThemeKey] = useStateA("navy");
  const [accentOverride, setAccentOverride] = useStateA("");   // empty = use theme default
  const [bgImageDataUrl, setBgImageDataUrl] = useStateA("");
  const [bgImageEl, setBgImageEl] = useStateA(null);
  const canvasRef = useRefA(null);

  // Decode the uploaded image to an HTMLImageElement once, reuse on every
  // redraw. Avoids re-decoding the data URL on every keystroke.
  useEffectA(() => {
    if (!bgImageDataUrl) { setBgImageEl(null); return; }
    const img = new Image();
    img.onload = () => setBgImageEl(img);
    img.onerror = () => setBgImageEl(null);
    img.src = bgImageDataUrl;
  }, [bgImageDataUrl]);

  function onPickBgImage(e) {
    const f = e.target.files && e.target.files[0];
    if (!f) return;
    const reader = new FileReader();
    reader.onload = (ev) => setBgImageDataUrl(String(ev.target.result || ""));
    reader.readAsDataURL(f);
  }

  // Draw whenever any input changes. The canvas is the source of truth;
  // download just exports its current pixels.
  useEffectA(() => {
    const canvas = canvasRef.current;
    if (!canvas) return;
    const ctx = canvas.getContext("2d");
    const theme = SHARE_CARD_THEMES[themeKey] || SHARE_CARD_THEMES.navy;
    drawShareCard(ctx, {
      template,
      title: title || (template === "stat" ? "What the public record shows" : "Type a headline to preview…"),
      kicker, subline,
      verdict, verdictScore,
      statValue: statValue || "$77M",
      statUnit:  statUnit  || "Restatement disclosed",
      footer,
      theme,
      accentOverride: accentOverride.trim() || null,
      bgImage: bgImageEl,
    });
  }, [template, title, kicker, subline, verdict, verdictScore, statValue, statUnit, footer,
      themeKey, accentOverride, bgImageEl]);

  function download() {
    const canvas = canvasRef.current;
    if (!canvas) return;
    const safeName = (title || "share-card")
      .toLowerCase()
      .replace(/[^a-z0-9]+/g, "-")
      .replace(/^-|-$/g, "")
      .slice(0, 60) || "share-card";
    canvas.toBlob((blob) => {
      if (!blob) return;
      const url = URL.createObjectURL(blob);
      const a = document.createElement("a");
      a.href = url;
      a.download = `${safeName}-1200x630.png`;
      document.body.appendChild(a);
      a.click();
      a.remove();
      setTimeout(() => URL.revokeObjectURL(url), 1000);
    }, "image/png");
  }

  return (
    <section className="admin-list">
      <div className="admin-list-inner" style={{ maxWidth: 980 }}>
        <article className="admin-row" style={{ padding: "24px 28px", marginBottom: 24 }}>
          <h2 style={{ marginTop: 0, fontSize: 18 }}>Share card studio</h2>
          <p style={{ fontSize: 13, color: "var(--ink-soft)", marginBottom: 16, lineHeight: 1.55 }}>
            Generate a 1200×630 PNG to attach to a LinkedIn post.
            Type your headline, pick a template, click <strong>Download PNG</strong>.
            Then write your post on LinkedIn, paste the article URL, and attach this image —
            LinkedIn will display it as a custom card above your text.
          </p>

          <div style={{ display: "grid", gridTemplateColumns: "minmax(260px, 1fr) 2fr", gap: 24 }}>
            {/* Form */}
            <div>
              {/* Theme + accent + background image — the new customization
                  layer. Sits at the top of the form so it's the first thing
                  the editor sees. */}
              <label style={{ display: "block", marginBottom: 12 }}>
                <span style={{ display: "block", fontSize: 12, fontWeight: 600, marginBottom: 6 }}>Theme</span>
                <select value={themeKey} onChange={(e) => setThemeKey(e.target.value)} style={{ width: "100%", padding: 6 }}>
                  {Object.entries(SHARE_CARD_THEMES).map(([k, t]) => (
                    <option key={k} value={k}>{t.label}</option>
                  ))}
                </select>
              </label>
              <label style={{ display: "block", marginBottom: 12 }}>
                <span style={{ display: "block", fontSize: 12, fontWeight: 600, marginBottom: 6 }}>
                  Accent color (optional override)
                </span>
                <div style={{ display: "flex", gap: 8, alignItems: "center" }}>
                  <input
                    type="color"
                    value={accentOverride || (SHARE_CARD_THEMES[themeKey] || {}).accent || "#5fa9ff"}
                    onChange={(e) => setAccentOverride(e.target.value)}
                    style={{ width: 44, height: 32, padding: 0, border: "1px solid var(--rule)", borderRadius: 4, cursor: "pointer" }}
                  />
                  <input
                    type="text"
                    value={accentOverride}
                    onChange={(e) => setAccentOverride(e.target.value)}
                    placeholder="leave blank = theme default"
                    style={{ flex: 1, padding: "6px 8px", fontSize: 12, fontFamily: "var(--font-mono)" }}
                  />
                  {accentOverride && (
                    <button onClick={() => setAccentOverride("")}
                            style={{ fontSize: 11, padding: "4px 8px", cursor: "pointer" }}>
                      Reset
                    </button>
                  )}
                </div>
              </label>
              <label style={{ display: "block", marginBottom: 12 }}>
                <span style={{ display: "block", fontSize: 12, fontWeight: 600, marginBottom: 6 }}>
                  Background image (optional)
                </span>
                <input type="file" accept="image/*" onChange={onPickBgImage}
                       style={{ width: "100%", fontSize: 12 }} />
                {bgImageDataUrl && (
                  <div style={{ marginTop: 6, display: "flex", alignItems: "center", gap: 8 }}>
                    <img src={bgImageDataUrl} alt="bg preview"
                         style={{ width: 48, height: 27, objectFit: "cover", borderRadius: 3, border: "1px solid var(--rule)" }} />
                    <button onClick={() => { setBgImageDataUrl(""); setBgImageEl(null); }}
                            style={{ fontSize: 11, padding: "4px 8px", cursor: "pointer" }}>
                      Remove image
                    </button>
                  </div>
                )}
                <div style={{ fontSize: 10, color: "var(--ink-soft)", marginTop: 4 }}>
                  Auto-applies a dark scrim so the headline stays readable over any photo.
                </div>
              </label>
              <hr style={{ border: "none", borderTop: "1px solid var(--rule)", margin: "16px 0" }} />
              <label style={{ display: "block", marginBottom: 12 }}>
                <span style={{ display: "block", fontSize: 12, fontWeight: 600, marginBottom: 6 }}>Template</span>
                <select value={template} onChange={(e) => setTemplate(e.target.value)} style={{ width: "100%", padding: 6 }}>
                  <option value="article">Article — kicker + serif headline</option>
                  <option value="verdict">Verdict — carrier dossier with pill + score</option>
                  <option value="stat">Stat — big number + label (best for findings)</option>
                  <option value="plain">Plain — wordmark + headline only</option>
                </select>
              </label>

              {template === "stat" ? (
                <>
                  <label style={{ display: "block", marginBottom: 12 }}>
                    <span style={{ display: "block", fontSize: 12, fontWeight: 600, marginBottom: 6 }}>The number</span>
                    <input
                      value={statValue}
                      onChange={(e) => setStatValue(e.target.value)}
                      placeholder="$77M  ·  -11%  ·  50+  ·  #1"
                      style={{ width: "100%", padding: 6, fontSize: 13 }}
                    />
                  </label>
                  <label style={{ display: "block", marginBottom: 12 }}>
                    <span style={{ display: "block", fontSize: 12, fontWeight: 600, marginBottom: 6 }}>What it represents</span>
                    <input
                      value={statUnit}
                      onChange={(e) => setStatUnit(e.target.value)}
                      placeholder="Restatement disclosed in 10-Q"
                      style={{ width: "100%", padding: 6, fontSize: 13 }}
                    />
                  </label>
                </>
              ) : template !== "plain" ? (
                <label style={{ display: "block", marginBottom: 12 }}>
                  <span style={{ display: "block", fontSize: 12, fontWeight: 600, marginBottom: 6 }}>
                    {template === "verdict" ? "Carrier name (kicker)" : "Kicker / category"}
                  </span>
                  <input
                    value={kicker}
                    onChange={(e) => setKicker(e.target.value)}
                    placeholder={template === "verdict" ? "WERNER ENTERPRISES" : "REPORT"}
                    style={{ width: "100%", padding: 6, fontSize: 13 }}
                  />
                </label>
              ) : null}

              <label style={{ display: "block", marginBottom: 12 }}>
                <span style={{ display: "block", fontSize: 12, fontWeight: 600, marginBottom: 6 }}>
                  {template === "stat" ? "Tagline / context" : "Headline / article title"}
                </span>
                <textarea
                  value={title}
                  onChange={(e) => setTitle(e.target.value)}
                  rows={3}
                  placeholder={template === "stat"
                    ? "Four numbers that define the restructuring's scale"
                    : "The number a shipper would actually call — and why FMCSA's record never matches."}
                  style={{ width: "100%", padding: 8, fontSize: 13, fontFamily: "inherit", resize: "vertical" }}
                />
              </label>

              <label style={{ display: "block", marginBottom: 12 }}>
                <span style={{ display: "block", fontSize: 12, fontWeight: 600, marginBottom: 6 }}>Subline (optional)</span>
                <input
                  value={subline}
                  onChange={(e) => setSubline(e.target.value)}
                  placeholder="By Kris Emery · 4 min read"
                  style={{ width: "100%", padding: 6, fontSize: 13 }}
                />
              </label>

              {template === "verdict" && (
                <div style={{ display: "grid", gridTemplateColumns: "1.6fr 1fr", gap: 8, marginBottom: 12 }}>
                  <label style={{ display: "block" }}>
                    <span style={{ display: "block", fontSize: 12, fontWeight: 600, marginBottom: 6 }}>Verdict</span>
                    <select value={verdict} onChange={(e) => setVerdict(e.target.value)} style={{ width: "100%", padding: 6 }}>
                      <option value="Trusted">Trusted (green)</option>
                      <option value="Watch">Watch (amber)</option>
                      <option value="Distress">Distress (red)</option>
                    </select>
                  </label>
                  <label style={{ display: "block" }}>
                    <span style={{ display: "block", fontSize: 12, fontWeight: 600, marginBottom: 6 }}>Score</span>
                    <input
                      value={verdictScore}
                      onChange={(e) => setVerdictScore(e.target.value)}
                      placeholder="62"
                      maxLength={3}
                      style={{ width: "100%", padding: 6, fontSize: 13 }}
                    />
                  </label>
                </div>
              )}

              <label style={{ display: "block", marginBottom: 12 }}>
                <span style={{ display: "block", fontSize: 12, fontWeight: 600, marginBottom: 6 }}>Footer URL</span>
                <input
                  value={footer}
                  onChange={(e) => setFooter(e.target.value)}
                  placeholder="shippingclarity.com"
                  style={{ width: "100%", padding: 6, fontSize: 13 }}
                />
              </label>

              <button className="btn-primary" onClick={download} disabled={!title.trim() && template !== "stat"} style={{ width: "100%", marginTop: 8 }}>
                ⬇ Download PNG (1200×630)
              </button>
              <p style={{ fontSize: 11, color: "var(--ink-soft)", marginTop: 8, lineHeight: 1.5 }}>
                Renders client-side. No API calls. The PNG saves to your downloads folder.
              </p>
            </div>

            {/* Preview */}
            <div style={{ background: "var(--paper-soft, #f6f6f4)", padding: 16, borderRadius: 6, border: "1px solid var(--rule)" }}>
              <div style={{ fontSize: 11, textTransform: "uppercase", letterSpacing: "0.12em", color: "var(--ink-soft)", marginBottom: 8 }}>
                Live preview · 1200×630
              </div>
              <canvas
                ref={canvasRef}
                width={SHARE_CARD_W}
                height={SHARE_CARD_H}
                style={{ width: "100%", height: "auto", display: "block", borderRadius: 4, boxShadow: "0 8px 32px rgba(0,0,0,0.12)" }}
              />
            </div>
          </div>
        </article>
      </div>
    </section>
  );
}

// drawShareCard — renders the entire card to the given 2D context. Pure
// function of the inputs (no DOM dependencies); same inputs always
// produce identical pixels. Uses system fonts for portability — no web-
// font flash or load failure modes.
//
// v2 design language (matches /#/c/<slug> dossier):
//   - radial gradient bg with subtle blue grid texture (depth + brand)
//   - radar mark + "Shipping Clarity" wordmark anchor top-left
//   - top-edge accent sliver (4px) for visual snap on the LinkedIn feed
//   - kicker → headline → subline composition for editorial template
//   - massive numeric for stat template (best for big-finding shares)
//   - footer with date stamp + radar dot accent
function drawShareCard(ctx, params) {
  const { template, title, kicker, subline, verdict, verdictScore,
          statValue, statUnit, footer, theme, accentOverride, bgImage } = params;
  const W = SHARE_CARD_W, H = SHARE_CARD_H;
  const PAD_X = 64;

  // Theme + optional accent override. accentOverride wins when provided
  // (any valid CSS color); otherwise fall back to the theme's accent.
  const T = theme || {
    bg: ["#1d2d52", "#0a1224", "#050913"],
    ink: "#ffffff", subtle: "rgba(255,255,255,0.62)", faint: "rgba(255,255,255,0.42)",
    rule: "rgba(255,255,255,0.16)", accent: "#5fa9ff", grid: "#5fa9ff",
  };
  const ACCENT  = accentOverride || T.accent;
  const INK     = T.ink;
  const SUBTLE  = T.subtle;
  const FAINT   = T.faint;
  const RULE    = T.rule;
  const VERDICT_COLORS = {
    Trusted:  "#9eff9e",
    Watch:    "#ffc864",
    Distress: "#ff7a7a",
  };

  // ----- Background — uploaded image (cover-fit + scrim) OR radial gradient ----
  if (bgImage && bgImage.naturalWidth > 0) {
    // Cover-fit the image to the card while preserving aspect.
    const ir = bgImage.naturalWidth / bgImage.naturalHeight;
    const cr = W / H;
    let dw, dh, dx, dy;
    if (ir > cr) {
      dh = H; dw = H * ir; dx = (W - dw) / 2; dy = 0;
    } else {
      dw = W; dh = W / ir; dx = 0; dy = (H - dh) / 2;
    }
    ctx.drawImage(bgImage, dx, dy, dw, dh);
    // Auto-scrim — dark gradient bottom-up so text is legible regardless
    // of the photo. Heavier on the side where the headline lives.
    const scrim = ctx.createLinearGradient(0, 0, 0, H);
    scrim.addColorStop(0, "rgba(0,0,0,0.35)");
    scrim.addColorStop(1, "rgba(0,0,0,0.78)");
    ctx.fillStyle = scrim;
    ctx.fillRect(0, 0, W, H);
  } else {
    const grad = ctx.createRadialGradient(W * 0.30, H * 0.20, 0, W * 0.30, H * 0.20, W * 0.95);
    grad.addColorStop(0,    T.bg[0]);
    grad.addColorStop(0.55, T.bg[1]);
    grad.addColorStop(1,    T.bg[2]);
    ctx.fillStyle = grad;
    ctx.fillRect(0, 0, W, H);
  }

  // ----- Subtle grid texture overlay (60px cells, very low opacity) -----
  // Skip the grid when a background image is in use — the photo is busy
  // enough on its own and the grid muddies it.
  if (!bgImage) {
    ctx.save();
    ctx.globalAlpha = 0.06;
    ctx.strokeStyle = T.grid;
    ctx.lineWidth = 1;
    const cell = 60;
    for (let x = 0; x <= W; x += cell) {
      ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, H); ctx.stroke();
    }
    for (let y = 0; y <= H; y += cell) {
      ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(W, y); ctx.stroke();
    }
    ctx.restore();
  }

  // ----- Top-edge accent sliver — 4px brand blue (snaps the eye on feed) -----
  ctx.fillStyle = ACCENT;
  ctx.fillRect(0, 0, W, 4);

  // ----- Brand stamp: radar mark + wordmark, top-left -----
  drawRadarMark(ctx, PAD_X, 56, 36);
  ctx.fillStyle = INK;
  ctx.font = "700 22px 'Helvetica Neue', Helvetica, Arial, sans-serif";
  ctx.textBaseline = "alphabetic";
  ctx.fillText("Shipping", PAD_X + 48, 80);
  const shippingWidth = ctx.measureText("Shipping").width;
  ctx.fillStyle = ACCENT;
  ctx.font = "700 italic 22px 'Helvetica Neue', Helvetica, Arial, sans-serif";
  ctx.fillText(" Clarity", PAD_X + 48 + shippingWidth, 80);
  ctx.textBaseline = "top";

  // ----- Verdict pill (top-right), only on the verdict template -----
  if (template === "verdict") {
    const v = VERDICT_COLORS[verdict] || ACCENT;
    const pillW = 240, pillH = 80;
    const pillX = W - PAD_X - pillW;
    const pillY = 56;
    // Pill background.
    ctx.fillStyle = "rgba(255,255,255,0.05)";
    roundRect(ctx, pillX, pillY, pillW, pillH, 10);
    ctx.fill();
    // Left accent bar.
    ctx.fillStyle = v;
    roundRect(ctx, pillX, pillY, 6, pillH, 3);
    ctx.fill();
    // Verdict label.
    ctx.fillStyle = v;
    ctx.font = "700 22px 'Helvetica Neue', Helvetica, Arial, sans-serif";
    ctx.textBaseline = "top";
    ctx.fillText(verdict.toUpperCase(), pillX + 22, pillY + 14);
    // Score (right-side big numeral).
    if (verdictScore && String(verdictScore).trim()) {
      ctx.fillStyle = INK;
      ctx.font = "800 38px 'Helvetica Neue', Helvetica, Arial, sans-serif";
      ctx.textAlign = "right";
      ctx.fillText(String(verdictScore).trim(), pillX + pillW - 18, pillY + 32);
      ctx.textAlign = "left";
      // "/100" annotation
      ctx.fillStyle = SUBTLE;
      ctx.font = "500 12px 'Helvetica Neue', Helvetica, Arial, sans-serif";
      ctx.textAlign = "right";
      ctx.fillText("TRUST SCORE", pillX + pillW - 18, pillY + 14);
      ctx.textAlign = "left";
    }
  }

  // ----- Subtle horizontal rule under the brand zone -----
  ctx.strokeStyle = RULE;
  ctx.lineWidth = 1;
  ctx.beginPath();
  ctx.moveTo(PAD_X, 130);
  ctx.lineTo(W - PAD_X, 130);
  ctx.stroke();

  // ----- Body content per template -----
  if (template === "stat") {
    drawStatBody(ctx, { W, H, PAD_X, ACCENT, INK, SUBTLE, statValue, statUnit, title });
  } else {
    drawArticleBody(ctx, {
      W, H, PAD_X, ACCENT, INK, SUBTLE,
      template, kicker, title, subline, hasVerdict: template === "verdict",
    });
  }

  // ----- Footer: rule + URL + date + radar dot -----
  const footerY = H - 60;
  ctx.strokeStyle = RULE;
  ctx.beginPath();
  ctx.moveTo(PAD_X, footerY - 22);
  ctx.lineTo(W - PAD_X, footerY - 22);
  ctx.stroke();

  // Footer left: URL with leading dot.
  ctx.fillStyle = ACCENT;
  ctx.beginPath(); ctx.arc(PAD_X + 4, footerY + 9, 4, 0, Math.PI * 2); ctx.fill();
  ctx.fillStyle = INK;
  ctx.font = "600 17px 'Helvetica Neue', Helvetica, Arial, sans-serif";
  ctx.textBaseline = "top";
  ctx.fillText(footer || "shippingclarity.com", PAD_X + 18, footerY);

  // Footer right: date stamp.
  ctx.textAlign = "right";
  ctx.fillStyle = FAINT;
  ctx.font = "500 13px 'Helvetica Neue', Helvetica, Arial, sans-serif";
  const today = new Date();
  const stamp = today.toLocaleDateString("en-US", { year: "numeric", month: "short", day: "numeric" });
  drawTrackedText(ctx, stamp.toUpperCase(), W - PAD_X, footerY + 4, 1.5, /*rightAlign*/true);
  ctx.textAlign = "left";
}

// drawArticleBody — kicker + headline + subline composition. Used by
// article, verdict, and plain templates. Stat template uses a different
// body so it gets its own renderer.
function drawArticleBody(ctx, p) {
  const { W, H, PAD_X, ACCENT, INK, SUBTLE, template, kicker, title, subline, hasVerdict } = p;

  // Kicker (skipped on plain template)
  let bodyY = 178;
  if (template !== "plain" && kicker) {
    ctx.fillStyle = ACCENT;
    ctx.font = "700 17px 'Helvetica Neue', Helvetica, Arial, sans-serif";
    drawTrackedText(ctx, (kicker || "").toUpperCase(), PAD_X, bodyY, 3.5);
    bodyY += 38;
  }

  // Headline — Georgia serif, big, white. Auto-shrinks if it needs more lines.
  // Verdict template makes headline column narrower so the pill doesn't crowd.
  const titleMaxW = hasVerdict ? W - PAD_X * 2 - 260 : W - PAD_X * 2;
  const titleMaxLines = 4;
  let titleSize = template === "plain" ? 76 : 64;
  let lines = wrapText(ctx, title, titleMaxW, `700 ${titleSize}px Georgia, "Times New Roman", serif`);
  while (lines.length > titleMaxLines && titleSize > 36) {
    titleSize -= 4;
    lines = wrapText(ctx, title, titleMaxW, `700 ${titleSize}px Georgia, "Times New Roman", serif`);
  }
  ctx.fillStyle = INK;
  ctx.font = `700 ${titleSize}px Georgia, "Times New Roman", serif`;
  const lineH = Math.round(titleSize * 1.14);
  for (let i = 0; i < lines.length; i++) {
    ctx.fillText(lines[i], PAD_X, bodyY + i * lineH);
  }
  const titleEndY = bodyY + lines.length * lineH;

  // Subline (optional) — sans, soft white, sits below the headline.
  if (subline && subline.trim()) {
    ctx.fillStyle = SUBTLE;
    ctx.font = "500 22px 'Helvetica Neue', Helvetica, Arial, sans-serif";
    const subLines = wrapText(ctx, subline, titleMaxW,
      `500 22px 'Helvetica Neue', Helvetica, Arial, sans-serif`);
    for (let i = 0; i < Math.min(subLines.length, 2); i++) {
      ctx.fillText(subLines[i], PAD_X, titleEndY + 18 + i * 30);
    }
  }
}

// drawStatBody — big-number layout. The number occupies the visual center,
// with label above it (small caps blue) and a tagline below (sans soft).
function drawStatBody(ctx, p) {
  const { W, H, PAD_X, ACCENT, INK, SUBTLE, statValue, statUnit, title } = p;

  // Vertical center of body region (between top brand zone and footer).
  const centerY = H * 0.50 + 12;

  // Tiny label above the number — small caps blue.
  ctx.fillStyle = ACCENT;
  ctx.font = "700 18px 'Helvetica Neue', Helvetica, Arial, sans-serif";
  drawTrackedText(ctx, "THE NUMBER", PAD_X, centerY - 130, 4);

  // The big number — auto-shrinks to fit available width.
  let valSize = 220;
  ctx.font = `800 ${valSize}px 'Helvetica Neue', Helvetica, Arial, sans-serif`;
  let valW = ctx.measureText(statValue || "").width;
  while (valW > W - PAD_X * 2 && valSize > 100) {
    valSize -= 10;
    ctx.font = `800 ${valSize}px 'Helvetica Neue', Helvetica, Arial, sans-serif`;
    valW = ctx.measureText(statValue || "").width;
  }
  ctx.fillStyle = INK;
  ctx.font = `800 ${valSize}px 'Helvetica Neue', Helvetica, Arial, sans-serif`;
  ctx.textBaseline = "alphabetic";
  ctx.fillText(statValue || "", PAD_X, centerY + Math.round(valSize * 0.18));
  ctx.textBaseline = "top";

  // Unit / "what it represents" — sans, soft white, sits below the number.
  if (statUnit) {
    ctx.fillStyle = INK;
    ctx.font = "600 26px 'Helvetica Neue', Helvetica, Arial, sans-serif";
    ctx.fillText(statUnit, PAD_X, centerY + Math.round(valSize * 0.20) + 14);
  }

  // Optional tagline (the title field on this template) — softer, italic.
  if (title) {
    ctx.fillStyle = SUBTLE;
    ctx.font = "italic 500 19px Georgia, 'Times New Roman', serif";
    const taglineLines = wrapText(ctx, title, W - PAD_X * 2,
      "italic 500 19px Georgia, 'Times New Roman', serif");
    for (let i = 0; i < Math.min(taglineLines.length, 2); i++) {
      ctx.fillText(taglineLines[i], PAD_X, centerY + Math.round(valSize * 0.20) + 56 + i * 26);
    }
  }
}

// drawRadarMark — recreates the inline radar SVG from video-render/brand.tsx
// directly on canvas. Same dial circles + sweep + accent dot. Sized to fit
// the brand stamp area.
function drawRadarMark(ctx, x, y, size) {
  const cx = x + size / 2;
  const cy = y + size / 2;
  const r  = size / 2 - 2;
  ctx.save();
  // Outer dial.
  ctx.strokeStyle = "rgba(255,255,255,0.85)";
  ctx.lineWidth = 1.6;
  ctx.beginPath(); ctx.arc(cx, cy, r, 0, Math.PI * 2); ctx.stroke();
  // Inner dial.
  ctx.strokeStyle = "rgba(255,255,255,0.40)";
  ctx.lineWidth = 1;
  ctx.beginPath(); ctx.arc(cx, cy, r * 0.62, 0, Math.PI * 2); ctx.stroke();
  // Crosshairs.
  ctx.strokeStyle = "rgba(255,255,255,0.30)";
  ctx.beginPath(); ctx.moveTo(cx - r, cy); ctx.lineTo(cx + r, cy); ctx.stroke();
  ctx.beginPath(); ctx.moveTo(cx, cy - r); ctx.lineTo(cx, cy + r); ctx.stroke();
  // Sweep wedge (gradient blue).
  const wedge = ctx.createLinearGradient(cx, cy, cx + r * 0.86, cy - r * 0.50);
  wedge.addColorStop(0, "rgba(95,169,255,0.65)");
  wedge.addColorStop(1, "rgba(95,169,255,0.00)");
  ctx.fillStyle = wedge;
  ctx.beginPath();
  ctx.moveTo(cx, cy);
  ctx.lineTo(cx, cy - r);
  ctx.arc(cx, cy, r, -Math.PI / 2, -Math.PI / 4);
  ctx.closePath();
  ctx.fill();
  // Sweep edge line.
  ctx.strokeStyle = "#5fa9ff";
  ctx.lineWidth = 1.6;
  ctx.beginPath();
  ctx.moveTo(cx, cy);
  ctx.lineTo(cx + Math.cos(-Math.PI / 4) * r, cy + Math.sin(-Math.PI / 4) * r);
  ctx.stroke();
  // Sweep tip dot.
  ctx.fillStyle = "#5fa9ff";
  ctx.beginPath();
  ctx.arc(cx + Math.cos(-Math.PI / 4) * r, cy + Math.sin(-Math.PI / 4) * r, 2, 0, Math.PI * 2);
  ctx.fill();
  // Center dot.
  ctx.fillStyle = "#fff";
  ctx.beginPath(); ctx.arc(cx, cy, 1.6, 0, Math.PI * 2); ctx.fill();
  ctx.restore();
}

// wrapText — given a long string, return an array of lines that each
// fit within maxWidth at the given font. Uses ctx measurement; greedy
// word-wrap (simple but good enough for headlines).
function wrapText(ctx, text, maxWidth, font) {
  ctx.font = font;
  const words = String(text || "").split(/\s+/).filter(Boolean);
  if (!words.length) return [""];
  const lines = [];
  let line = words[0];
  for (let i = 1; i < words.length; i++) {
    const test = line + " " + words[i];
    if (ctx.measureText(test).width <= maxWidth) {
      line = test;
    } else {
      lines.push(line);
      line = words[i];
    }
  }
  lines.push(line);
  return lines;
}

// drawTrackedText — emulate CSS letter-spacing by drawing each character
// individually with a fixed offset. Canvas's native text doesn't support
// tracking; this is the standard workaround. Pass rightAlign=true to
// anchor the rightmost character at x (used for right-aligned date stamps).
function drawTrackedText(ctx, str, x, y, tracking, rightAlign) {
  if (rightAlign) {
    let total = 0;
    for (const ch of str) total += ctx.measureText(ch).width + tracking;
    total -= tracking;
    let cx = x - total;
    for (const ch of str) {
      ctx.fillText(ch, cx, y);
      cx += ctx.measureText(ch).width + tracking;
    }
    return;
  }
  let cx = x;
  for (const ch of str) {
    ctx.fillText(ch, cx, y);
    cx += ctx.measureText(ch).width + tracking;
  }
}

// roundRect — older Safari versions lack ctx.roundRect; polyfill for
// the verdict pill background.
function roundRect(ctx, x, y, w, h, r) {
  ctx.beginPath();
  ctx.moveTo(x + r, y);
  ctx.lineTo(x + w - r, y);
  ctx.quadraticCurveTo(x + w, y, x + w, y + r);
  ctx.lineTo(x + w, y + h - r);
  ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
  ctx.lineTo(x + r, y + h);
  ctx.quadraticCurveTo(x, y + h, x, y + h - r);
  ctx.lineTo(x, y + r);
  ctx.quadraticCurveTo(x, y, x + r, y);
  ctx.closePath();
}

window.AdminPage = AdminPage;
