// running/src/campaign.jsx — the Campaign Command Station.
//
// Build a sponsorship campaign (goal / raised / deadline / socials /
// matching / milestones stored at users/{uid}/campaign/current), watch
// pacing vs the hard deadline, keep every channel inside its posting
// budget (window.CampaignCadence — mirrored lib), and run the server-side
// agent roster (runCampaignAgent: strategist / copywriter / outreach) to
// draft platform-native posts and 1:1 asks from live training data.
// Posted content is logged to users/{uid}/campaignPosts so the cadence
// board and the agents both know what the audience has already seen.
//
// Public visitors get the donor view: progress, the story line, the
// donate button, and the athlete's social links.

const { C, Card, Btn, EmptyState } = window.SharedUI;
const CampaignCadence = window.CampaignCadence;
const { useState, useEffect, useMemo, useCallback } = React;

    const CAMPAIGN_DEFAULTS = {
      name: "NYC Marathon 2026 — Sponsorship Campaign",
      charity: "New York Road Runners",
      cause: "free youth running programs and community running across NYC's five boroughs",
      publicLink: "https://fundraisers.nyrr.org/chriscookeJPM",
      raceName: "TCS New York City Marathon",
      raceDate: "2026-11-01",
      goalTime: "sub-3:00",
      startDate: "2026-07-28",
      deadline: "2026-10-21",
      goal: 3000,
      raised: 0,
      perMileAsk: 27,
      milesTotal: 26.2,
      matching: { rate: 1.0, note: "JPMorgan employee gift matching — 100% standard", boostRate: 2.0, boostLabel: "Giving Tuesday", boostDate: "2026-12-01" },
      milestones: [
        { amount: 1000, unlock: "long run in NYRR colours" },
        { amount: 2000, unlock: "20-miler dedicated to donors, name on singlet ≥$50" },
        { amount: 3000, unlock: "race-day mile dedications for every sponsor" },
      ],
      socials: { instagram: "", linkedin: "", strava: "", facebook: "" },
    };

    const AGENT_META = {
      strategist: { icon: "🧭", label: "Strategist", skill: "Pacing verdict + this week's plan", color: C.cyan },
      copywriter: { icon: "✍️", label: "Copywriter", skill: "Platform-native post drafts", color: C.green },
      outreach: { icon: "🤝", label: "Outreach", skill: "Personal 1:1 asks & thank-yous", color: C.purple },
      studio: { icon: "🎨", label: "Studio", skill: "Campaign graphics from your photos (Gemini)", color: C.amber },
    };

    const STUDIO_PRESETS = [
      { key: "progress", label: "Progress graphic" },
      { key: "story", label: "Story (9:16)" },
      { key: "countdown", label: "Race countdown" },
      { key: "milestone", label: "Milestone celebration" },
      { key: "freeform", label: "Freeform edit" },
    ];

    // Downscale any image blob/file to ≤1536px JPEG so callable payloads
    // stay small and Gemini gets a sane input.
    function downscaleImage(blob, maxDim = 1536) {
      return new Promise((resolve, reject) => {
        const url = URL.createObjectURL(blob);
        const img = new Image();
        img.onload = () => {
          const scale = Math.min(1, maxDim / Math.max(img.width, img.height));
          const canvas = document.createElement("canvas");
          canvas.width = Math.round(img.width * scale);
          canvas.height = Math.round(img.height * scale);
          canvas.getContext("2d").drawImage(img, 0, 0, canvas.width, canvas.height);
          URL.revokeObjectURL(url);
          const dataUrl = canvas.toDataURL("image/jpeg", 0.9);
          resolve({ base64: dataUrl.split(",")[1], mimeType: "image/jpeg", previewUrl: dataUrl });
        };
        img.onerror = () => { URL.revokeObjectURL(url); reject(new Error("Could not read image")); };
        img.src = url;
      });
    }

    // Google Photos Picker flow. Full-library access was retired by Google
    // (Mar 2025) — the Picker is the supported path: we open picker.google.com,
    // the owner selects photos there, and we fetch just those. Requires the
    // "Google Photos Picker API" enabled on the Firebase GCP project.
    const PICKER_SCOPE = "https://www.googleapis.com/auth/photospicker.mediaitems.readonly";
    async function pickFromGooglePhotos(onStatus) {
      const provider = new firebase.auth.GoogleAuthProvider();
      provider.addScope(PICKER_SCOPE);
      onStatus("Authorizing Google Photos…");
      const cred = await auth.currentUser.reauthenticateWithPopup(provider);
      const token = cred.credential && cred.credential.accessToken;
      if (!token) throw new Error("No Google access token returned");
      const H = { Authorization: `Bearer ${token}`, "Content-Type": "application/json" };

      const sessResp = await fetch("https://photospicker.googleapis.com/v1/sessions", { method: "POST", headers: H, body: "{}" });
      if (!sessResp.ok) throw new Error(`Picker session failed (${sessResp.status}) — is the Google Photos Picker API enabled on the project?`);
      const session = await sessResp.json();

      const w = window.open(session.pickerUri, "_blank");
      if (!w) throw new Error("Popup blocked — allow popups and retry");
      onStatus("Pick your photo in the Google Photos tab…");

      const pollMs = Math.max(2000, Number((session.pollingConfig?.pollInterval || "3s").replace(/s$/, "")) * 1000 || 3000);
      const deadline = Date.now() + 4 * 60 * 1000;
      let done = false;
      while (Date.now() < deadline) {
        await new Promise(r => setTimeout(r, pollMs));
        const s = await (await fetch(`https://photospicker.googleapis.com/v1/sessions/${session.id}`, { headers: H })).json();
        if (s.mediaItemsSet) { done = true; break; }
      }
      if (!done) throw new Error("Timed out waiting for a selection");

      const items = await (await fetch(`https://photospicker.googleapis.com/v1/mediaItems?sessionId=${session.id}&pageSize=5`, { headers: H })).json();
      const first = items.mediaItems && items.mediaItems[0];
      const baseUrl = first && (first.mediaFile?.baseUrl || first.baseUrl);
      if (!baseUrl) throw new Error("No photo selected");
      onStatus("Downloading photo…");
      // The bytes must come down server-side: fetching baseUrl here with an
      // Authorization header trips a CORS preflight googleusercontent.com
      // doesn't answer, which surfaces as a bare "Failed to fetch".
      const dl = functions.httpsCallable("fetchPickedPhoto", { timeout: 60000 });
      const picked = (await dl({ baseUrl, accessToken: token, width: 2048 })).data || {};
      if (!picked.imageBase64) throw new Error("Photo download returned no image");
      const bin = atob(picked.imageBase64);
      const bytes = new Uint8Array(bin.length);
      for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
      const blob = new Blob([bytes], { type: picked.mimeType || "image/jpeg" });
      try { fetch(`https://photospicker.googleapis.com/v1/sessions/${session.id}`, { method: "DELETE", headers: H }); } catch (e) {}
      return downscaleImage(blob);
    }

    const inputStyle = {
      width: "100%", boxSizing: "border-box", background: C.bg2, color: C.text,
      border: `1px solid ${C.border}`, borderRadius: 8, padding: "8px 10px",
      fontSize: 13, fontFamily: "'IBM Plex Mono', monospace", outline: "none",
    };

    const Field = ({ label, children, span }) => (
      <label style={{ display: "block", gridColumn: span ? "1 / -1" : undefined }}>
        <div style={{ fontSize: 10, color: C.textMuted, textTransform: "uppercase", letterSpacing: 1, marginBottom: 4 }}>{label}</div>
        {children}
      </label>
    );

    function ProgressBar({ pct, height = 14 }) {
      return (
        <div style={{ background: C.bg3, borderRadius: height, height, overflow: "hidden", border: `1px solid ${C.border}` }}>
          <div style={{
            width: `${Math.max(2, pct)}%`, height: "100%", borderRadius: height,
            background: `linear-gradient(90deg, ${C.cyanMuted}, ${C.green})`, transition: "width 0.4s",
          }} />
        </div>
      );
    }

    function copyText(text) {
      try { navigator.clipboard.writeText(text); } catch (e) { /* http fallback */
        const ta = document.createElement("textarea");
        ta.value = text; document.body.appendChild(ta); ta.select();
        try { document.execCommand("copy"); } catch (e2) {}
        document.body.removeChild(ta);
      }
    }

    function Campaign({ userId, isOwner }) {
      const [campaign, setCampaign] = useState(null);
      const [campaignLoaded, setCampaignLoaded] = useState(false);
      const [posts, setPosts] = useState([]);
      const [agentState, setAgentState] = useState({});
      const [editOpen, setEditOpen] = useState(false);
      const [form, setForm] = useState(null);
      const [saving, setSaving] = useState(false);
      const [running, setRunning] = useState(null); // agent key currently running
      const [outputs, setOutputs] = useState({});   // agent → text
      const [gate, setGate] = useState(null);       // copywriter cadence block
      const [copied, setCopied] = useState(null);
      // Copywriter controls
      const [cwPlatform, setCwPlatform] = useState("instagram");
      const [cwType, setCwType] = useState("milestone");
      const [cwNotes, setCwNotes] = useState("");
      // Outreach controls
      const [orMode, setOrMode] = useState("ask");
      const [orNames, setOrNames] = useState("");
      // Strategist controls
      const [stNotes, setStNotes] = useState("");
      // Studio controls
      const [photo, setPhoto] = useState(null);          // {base64, mimeType, previewUrl}
      const [studioPreset, setStudioPreset] = useState("progress");
      const [studioNotes, setStudioNotes] = useState("");
      const [studioStatus, setStudioStatus] = useState(null);
      const [studioResult, setStudioResult] = useState(null); // {dataUrl, mimeType}
      // Photo library + asset pack
      const [library, setLibrary] = useState([]);
      const [libStatus, setLibStatus] = useState(null);
      const [packPlatform, setPackPlatform] = useState("instagram");
      const [packType, setPackType] = useState("milestone");
      const [packNotes, setPackNotes] = useState("");
      const [packStatus, setPackStatus] = useState(null);
      const [pack, setPack] = useState(null); // {caption, direction, dataUrl, mimeType, why}

      // Live campaign doc + post log + last agent runs
      useEffect(() => {
        if (!userId) return;
        const ref = db.collection("users").doc(userId).collection("campaign");
        const unsub1 = ref.doc("current").onSnapshot(
          d => { setCampaign(d.exists ? d.data() : null); setCampaignLoaded(true); },
          () => setCampaignLoaded(true));
        const unsub2 = db.collection("users").doc(userId).collection("campaignPosts")
          .orderBy("postedAt", "desc").limit(25).onSnapshot(
            s => setPosts(s.docs.map(d => {
              const p = d.data();
              return { id: d.id, ...p, postedAt: p.postedAt?.toMillis ? p.postedAt.toMillis() : p.postedAt };
            })),
            () => {});
        const unsub3 = isOwner ? ref.doc("agentState").onSnapshot(
          d => {
            if (!d.exists) return;
            const s = d.data();
            setAgentState(s);
            setOutputs(prev => {
              const next = { ...prev };
              for (const k of Object.keys(AGENT_META)) if (!next[k] && s[k]?.text) next[k] = s[k].text;
              return next;
            });
          }, () => {}) : () => {};
        const unsub4 = isOwner ? db.collection("users").doc(userId).collection("campaignPhotos")
          .orderBy("createdAt", "desc").limit(60).onSnapshot(
            s => setLibrary(s.docs.map(d => ({ id: d.id, ...d.data() }))), () => {}) : () => {};
        return () => { unsub1(); unsub2(); unsub3(); unsub4(); };
      }, [userId, isOwner]);

      const math = useMemo(() => CampaignCadence.campaignMath(campaign, Date.now()), [campaign]);
      const board = useMemo(() => CampaignCadence.cadenceBoard(posts, Date.now()), [posts]);

      const openEditor = useCallback(() => {
        const base = campaign || CAMPAIGN_DEFAULTS;
        setForm({
          ...CAMPAIGN_DEFAULTS, ...base,
          matching: { ...CAMPAIGN_DEFAULTS.matching, ...(base.matching || {}) },
          socials: { ...CAMPAIGN_DEFAULTS.socials, ...(base.socials || {}) },
          milestonesText: (base.milestones || CAMPAIGN_DEFAULTS.milestones)
            .map(m => `${m.amount}: ${m.unlock}`).join("\n"),
        });
        setEditOpen(true);
      }, [campaign]);

      const saveCampaign = useCallback(async () => {
        if (!form) return;
        setSaving(true);
        const milestones = (form.milestonesText || "").split("\n").map(l => l.trim()).filter(Boolean)
          .map(l => { const m = l.match(/^\$?([\d,]+)\s*[:→-]\s*(.+)$/); return m ? { amount: Number(m[1].replace(/,/g, "")), unlock: m[2].trim() } : null; })
          .filter(Boolean);
        const doc = {
          name: form.name || "", charity: form.charity || "", cause: form.cause || "",
          publicLink: form.publicLink || "", raceName: form.raceName || "", raceDate: form.raceDate || "",
          goalTime: form.goalTime || "", startDate: form.startDate || "", deadline: form.deadline || "",
          goal: Number(form.goal) || 0, raised: Number(form.raised) || 0,
          perMileAsk: Number(form.perMileAsk) || 27, milesTotal: Number(form.milesTotal) || 26.2,
          matching: {
            rate: Number(form.matching.rate) || 0, note: form.matching.note || "",
            boostRate: Number(form.matching.boostRate) || 0, boostLabel: form.matching.boostLabel || "",
            boostDate: form.matching.boostDate || "",
          },
          milestones, socials: form.socials,
          updatedAt: firebase.firestore.FieldValue.serverTimestamp(),
        };
        try {
          await db.collection("users").doc(userId).collection("campaign").doc("current").set(doc, { merge: true });
          setEditOpen(false);
        } catch (e) { alert("Save failed: " + e.message); }
        setSaving(false);
      }, [form, userId]);

      const runAgent = useCallback(async (agent, input, force = false) => {
        setRunning(agent); setGate(null);
        try {
          const fn = functions.httpsCallable("runCampaignAgent", { timeout: 120000 });
          const res = await fn({ agent, input, force });
          const d = res.data || {};
          if (d.gated) { setGate(d.cadence); }
          else if (d.text) { setOutputs(prev => ({ ...prev, [agent]: d.text })); }
        } catch (e) { alert("Agent failed: " + e.message); }
        setRunning(null);
      }, []);

      const markPosted = useCallback(async (platform, postType, text) => {
        try {
          await db.collection("users").doc(userId).collection("campaignPosts").add({
            platform, postType, text,
            postedAt: firebase.firestore.FieldValue.serverTimestamp(),
          });
        } catch (e) { alert("Log failed: " + e.message); }
      }, [userId]);

      const doCopy = useCallback((key, text) => {
        copyText(text); setCopied(key); setTimeout(() => setCopied(k => (k === key ? null : k)), 1500);
      }, []);

      const onUploadPhoto = useCallback(async (e) => {
        const file = e.target.files && e.target.files[0];
        e.target.value = "";
        if (!file) return;
        try { setPhoto(await downscaleImage(file)); setStudioResult(null); setStudioStatus(null); }
        catch (err) { setStudioStatus("✗ " + err.message); }
      }, []);

      const onGooglePhotos = useCallback(async () => {
        try {
          const p = await pickFromGooglePhotos(setStudioStatus);
          setPhoto(p); setStudioResult(null); setStudioStatus(null);
        } catch (err) { setStudioStatus("✗ " + err.message); }
      }, []);

      const runStudio = useCallback(async () => {
        if (!photo) return;
        setRunning("studio"); setStudioStatus("Editing with Gemini…"); setStudioResult(null);
        try {
          const fn = functions.httpsCallable("editCampaignImage", { timeout: 180000 });
          const res = await fn({ imageBase64: photo.base64, mimeType: photo.mimeType, preset: studioPreset, instruction: studioNotes });
          const d = res.data || {};
          setStudioResult({ dataUrl: `data:${d.mimeType || "image/png"};base64,${d.imageBase64}`, mimeType: d.mimeType || "image/png" });
          setStudioStatus(null);
        } catch (e) { setStudioStatus("✗ " + e.message); }
        setRunning(null);
      }, [photo, studioPreset, studioNotes]);

      // ── Photo library ────────────────────────────────────────────────
      // One-time deposit, many autonomous uses: each photo is downscaled,
      // stored under users/{uid}/campaignPhotos, and captioned by the vision
      // model so the art director can later choose between them unaided.
      const onAddToLibrary = useCallback(async (e) => {
        const files = Array.from(e.target.files || []);
        e.target.value = "";
        if (!files.length) return;
        const tag = functions.httpsCallable("tagCampaignPhoto", { timeout: 60000 });
        let done = 0;
        for (const file of files) {
          done += 1;
          setLibStatus(`Adding ${done}/${files.length}: ${file.name}…`);
          try {
            const img = await downscaleImage(file, 1536);
            const id = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
            const storagePath = `users/${userId}/campaignPhotos/${id}.jpg`;
            await storage.ref(storagePath).putString(img.base64, "base64", { contentType: "image/jpeg" });

            let tags = null;
            try {
              const res = await tag({ imageBase64: img.base64, mimeType: img.mimeType });
              tags = (res.data && res.data.tags) || null;
            } catch (err) { console.warn("tagging failed", err); }

            await db.collection("users").doc(userId).collection("campaignPhotos").doc(id).set({
              storagePath, usedCount: 0,
              createdAt: firebase.firestore.FieldValue.serverTimestamp(),
              ...(tags || {}),
            });
          } catch (err) { setLibStatus(`✗ ${file.name}: ${err.message}`); return; }
        }
        setLibStatus(`✓ Added ${files.length} photo${files.length > 1 ? "s" : ""}`);
        setTimeout(() => setLibStatus(null), 4000);
      }, [userId]);

      const removeFromLibrary = useCallback(async (p) => {
        try { if (p.storagePath) await storage.ref(p.storagePath).delete(); } catch (e) { /* already gone */ }
        try { await db.collection("users").doc(userId).collection("campaignPhotos").doc(p.id).delete(); } catch (e) {}
      }, [userId]);

      // ── Asset pack: caption + graphic in one run ─────────────────────
      // Server writes the caption, then picks the photo and treatment from the
      // library; the browser fetches those bytes and renders the graphic, so
      // what lands is a finished post rather than two half-finished tools.
      const runAssetPack = useCallback(async (force = false) => {
        setRunning("pack"); setPack(null); setGate(null);
        setPackStatus("Writing the caption and picking a photo…");
        try {
          const fn = functions.httpsCallable("runCampaignAssetPack", { timeout: 180000 });
          const res = await fn({ platform: packPlatform, postType: packType, notes: packNotes, force });
          const d = res.data || {};
          if (d.gated) {
            setGate({ ...d.cadence, scope: "pack" });
            setPackStatus(null); setRunning(null); return;
          }
          const chosen = library.find(p => p.id === d.direction.photoId);
          if (!chosen) throw new Error("Chosen photo is no longer in the library");

          setPackStatus(`Rendering "${d.direction.preset}" from ${chosen.caption || chosen.id}…`);
          const url = await storage.ref(chosen.storagePath).getDownloadURL();
          const blob = await (await fetch(url)).blob();
          const img = await downscaleImage(blob, 1536);

          const edit = functions.httpsCallable("editCampaignImage", { timeout: 180000 });
          const out = await edit({
            imageBase64: img.base64, mimeType: img.mimeType,
            preset: d.direction.preset, instruction: d.direction.instruction,
          });
          const o = out.data || {};
          setPack({
            caption: d.caption, direction: d.direction, platform: d.platform, postType: d.postType,
            dataUrl: `data:${o.mimeType || "image/png"};base64,${o.imageBase64}`,
            mimeType: o.mimeType || "image/png",
          });
          setPackStatus(null);
          // Rotation signal: the art director prefers photos it hasn't used.
          try {
            await db.collection("users").doc(userId).collection("campaignPhotos").doc(chosen.id)
              .set({ usedCount: (chosen.usedCount || 0) + 1 }, { merge: true });
          } catch (e) {}
        } catch (e) { setPackStatus("✗ " + e.message); }
        setRunning(null);
      }, [packPlatform, packType, packNotes, library, userId]);

      const downloadPack = useCallback(() => {
        if (!pack) return;
        const a = document.createElement("a");
        a.href = pack.dataUrl;
        a.download = `campaign-${pack.platform}-${new Date().toISOString().slice(0, 10)}.${pack.mimeType.includes("png") ? "png" : "jpg"}`;
        a.click();
      }, [pack]);

      const downloadStudio = useCallback(() => {
        if (!studioResult) return;
        const a = document.createElement("a");
        a.href = studioResult.dataUrl;
        a.download = `campaign-${studioPreset}-${new Date().toISOString().slice(0, 10)}.${studioResult.mimeType.includes("png") ? "png" : "jpg"}`;
        a.click();
      }, [studioResult, studioPreset]);

      const socials = campaign?.socials || {};

      // ── Public donor view ────────────────────────────────────────────
      if (!isOwner) {
        if (!campaignLoaded) return null;
        if (!campaign) return <EmptyState title="No active campaign" subtitle="Nothing to sponsor right now — check back soon." />;
        return (
          <div style={{ maxWidth: 640, margin: "0 auto", display: "flex", flexDirection: "column", gap: 16 }}>
            <Card style={{ textAlign: "center", padding: 28 }}>
              <div style={{ fontFamily: "'Space Grotesk',sans-serif", fontSize: 24, fontWeight: 900, color: C.text }}>{campaign.name}</div>
              {campaign.charity && <div style={{ fontSize: 13, color: C.textDim, marginTop: 8, lineHeight: 1.6 }}>
                Raising for <span style={{ color: C.cyan, fontWeight: 700 }}>{campaign.charity}</span>{campaign.cause ? ` — ${campaign.cause}` : ""}
              </div>}
              {math && <div style={{ marginTop: 20 }}>
                <ProgressBar pct={math.pct} height={18} />
                <div style={{ display: "flex", justifyContent: "space-between", marginTop: 8, fontSize: 13, color: C.textDim, fontFamily: "'IBM Plex Mono',monospace" }}>
                  <span style={{ color: C.green, fontWeight: 700 }}>${math.raised.toLocaleString()} raised</span>
                  <span>{math.pct}% of ${math.goal.toLocaleString()}</span>
                </div>
                {math.milesLeft > 0 && <div style={{ fontSize: 12, color: C.textMuted, marginTop: 6 }}>
                  {math.milesLeft} of {math.milesTotal} miles still unsponsored at ${math.perMileAsk} each
                </div>}
              </div>}
              {campaign.publicLink && <a href={campaign.publicLink} target="_blank" rel="noopener noreferrer" style={{
                display: "inline-block", marginTop: 20, background: C.green, color: C.bg0,
                padding: "12px 32px", borderRadius: 10, fontWeight: 800, fontSize: 15, textDecoration: "none",
                fontFamily: "'Space Grotesk',sans-serif",
              }}>Sponsor a mile →</a>}
              <div style={{ display: "flex", gap: 14, justifyContent: "center", marginTop: 18 }}>
                {Object.entries(socials).filter(([, v]) => v).map(([k, v]) => (
                  <a key={k} href={v} target="_blank" rel="noopener noreferrer" style={{ color: C.cyan, fontSize: 12, textDecoration: "none", textTransform: "capitalize" }}>{k}</a>
                ))}
              </div>
            </Card>
          </div>
        );
      }

      // ── Owner: the Command Station ───────────────────────────────────
      if (!campaignLoaded) return null;

      return (
        <div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
          {/* Mission header */}
          <Card>
            <div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start", flexWrap: "wrap", gap: 10 }}>
              <div>
                <div style={{ fontSize: 10, color: C.textMuted, textTransform: "uppercase", letterSpacing: 2 }}>Campaign Command Station</div>
                <div style={{ fontFamily: "'Space Grotesk',sans-serif", fontSize: 20, fontWeight: 900, color: C.text, marginTop: 2 }}>
                  {campaign ? campaign.name : "No campaign yet"}
                </div>
              </div>
              <Btn small variant="secondary" onClick={openEditor}>{campaign ? "⚙ Configure" : "＋ Build campaign"}</Btn>
            </div>
            {campaign && math && (
              <div style={{ marginTop: 16 }}>
                <ProgressBar pct={math.pct} />
                <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(120px, 1fr))", gap: 12, marginTop: 14 }}>
                  <div><div style={{ fontSize: 22, fontWeight: 800, color: C.green, fontFamily: "'Space Grotesk',sans-serif" }}>${math.raised.toLocaleString()}</div><div style={{ fontSize: 10, color: C.textMuted }}>RAISED OF ${math.goal.toLocaleString()} ({math.pct}%)</div></div>
                  <div><div style={{ fontSize: 22, fontWeight: 800, color: math.daysToDeadline != null && math.daysToDeadline < 21 && math.remaining > 0 ? C.amber : C.cyan, fontFamily: "'Space Grotesk',sans-serif" }}>{math.daysToDeadline ?? "—"}d</div><div style={{ fontSize: 10, color: C.textMuted }}>TO MINIMUM DEADLINE{campaign.deadline ? ` (${campaign.deadline})` : ""}</div></div>
                  <div><div style={{ fontSize: 22, fontWeight: 800, color: C.purple, fontFamily: "'Space Grotesk',sans-serif" }}>${math.requiredPerWeek.toLocaleString()}</div><div style={{ fontSize: 10, color: C.textMuted }}>REQUIRED / WEEK</div></div>
                  <div><div style={{ fontSize: 22, fontWeight: 800, color: C.pink, fontFamily: "'Space Grotesk',sans-serif" }}>{math.milesLeft}</div><div style={{ fontSize: 10, color: C.textMuted }}>MILES UNSPONSORED @ ${math.perMileAsk}</div></div>
                  <div><div style={{ fontSize: 22, fontWeight: 800, color: C.cyanDim, fontFamily: "'Space Grotesk',sans-serif" }}>{math.daysToRace ?? "—"}d</div><div style={{ fontSize: 10, color: C.textMuted }}>TO RACE{math.weekOfCampaign ? ` · WK ${math.weekOfCampaign}` : ""}</div></div>
                </div>
                {campaign.publicLink && <div style={{ marginTop: 10, fontSize: 11, fontFamily: "'IBM Plex Mono',monospace" }}>
                  <span style={{ color: C.textMuted }}>donor link </span>
                  <a href={campaign.publicLink} target="_blank" rel="noopener noreferrer" style={{ color: C.cyan }}>{campaign.publicLink}</a>
                  <span onClick={() => doCopy("link", campaign.publicLink)} style={{ color: C.textDim, cursor: "pointer", marginLeft: 8 }}>{copied === "link" ? "✓ copied" : "⧉ copy"}</span>
                </div>}
              </div>
            )}
          </Card>

          {/* Config editor */}
          {editOpen && form && (
            <Card>
              <div style={{ fontWeight: 800, color: C.text, marginBottom: 14, fontFamily: "'Space Grotesk',sans-serif" }}>Campaign configuration</div>
              <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(220px, 1fr))", gap: 12 }}>
                <Field label="Campaign name" span><input style={inputStyle} value={form.name} onChange={e => setForm({ ...form, name: e.target.value })} /></Field>
                <Field label="Charity"><input style={inputStyle} value={form.charity} onChange={e => setForm({ ...form, charity: e.target.value })} /></Field>
                <Field label="Cause (one line, used in copy)"><input style={inputStyle} value={form.cause} onChange={e => setForm({ ...form, cause: e.target.value })} /></Field>
                <Field label="Public donor link" span><input style={inputStyle} value={form.publicLink} onChange={e => setForm({ ...form, publicLink: e.target.value })} /></Field>
                <Field label="Goal ($)"><input style={inputStyle} type="number" value={form.goal} onChange={e => setForm({ ...form, goal: e.target.value })} /></Field>
                <Field label="Raised so far ($)"><input style={inputStyle} type="number" value={form.raised} onChange={e => setForm({ ...form, raised: e.target.value })} /></Field>
                <Field label="$ per mile ask"><input style={inputStyle} type="number" value={form.perMileAsk} onChange={e => setForm({ ...form, perMileAsk: e.target.value })} /></Field>
                <Field label="Race name"><input style={inputStyle} value={form.raceName} onChange={e => setForm({ ...form, raceName: e.target.value })} /></Field>
                <Field label="Race date"><input style={inputStyle} type="date" value={form.raceDate} onChange={e => setForm({ ...form, raceDate: e.target.value })} /></Field>
                <Field label="Campaign start"><input style={inputStyle} type="date" value={form.startDate} onChange={e => setForm({ ...form, startDate: e.target.value })} /></Field>
                <Field label="Minimum deadline"><input style={inputStyle} type="date" value={form.deadline} onChange={e => setForm({ ...form, deadline: e.target.value })} /></Field>
                <Field label="Goal time"><input style={inputStyle} value={form.goalTime} onChange={e => setForm({ ...form, goalTime: e.target.value })} /></Field>
                <Field label="Match rate (1 = 100%)"><input style={inputStyle} type="number" step="0.5" value={form.matching.rate} onChange={e => setForm({ ...form, matching: { ...form.matching, rate: e.target.value } })} /></Field>
                <Field label="Match note"><input style={inputStyle} value={form.matching.note} onChange={e => setForm({ ...form, matching: { ...form.matching, note: e.target.value } })} /></Field>
                <Field label="Boost day / rate / date">
                  <div style={{ display: "flex", gap: 6 }}>
                    <input style={{ ...inputStyle, flex: 2 }} placeholder="Giving Tuesday" value={form.matching.boostLabel} onChange={e => setForm({ ...form, matching: { ...form.matching, boostLabel: e.target.value } })} />
                    <input style={{ ...inputStyle, flex: 1 }} type="number" step="0.5" value={form.matching.boostRate} onChange={e => setForm({ ...form, matching: { ...form.matching, boostRate: e.target.value } })} />
                    <input style={{ ...inputStyle, flex: 2 }} type="date" value={form.matching.boostDate} onChange={e => setForm({ ...form, matching: { ...form.matching, boostDate: e.target.value } })} />
                  </div>
                </Field>
                {Object.keys(CAMPAIGN_DEFAULTS.socials).map(k => (
                  <Field key={k} label={`${k} profile URL`}>
                    <input style={inputStyle} placeholder={`https://…`} value={form.socials[k] || ""} onChange={e => setForm({ ...form, socials: { ...form.socials, [k]: e.target.value } })} />
                  </Field>
                ))}
                <Field label={'Milestone unlocks (one per line: "1500: run in charity colours")'} span>
                  <textarea style={{ ...inputStyle, minHeight: 64, resize: "vertical" }} value={form.milestonesText} onChange={e => setForm({ ...form, milestonesText: e.target.value })} />
                </Field>
              </div>
              <div style={{ display: "flex", gap: 8, marginTop: 14 }}>
                <Btn onClick={saveCampaign} disabled={saving}>{saving ? "Saving…" : "Save campaign"}</Btn>
                <Btn variant="ghost" onClick={() => setEditOpen(false)}>Cancel</Btn>
              </div>
            </Card>
          )}

          {campaign && (
            <React.Fragment>
              {/* Cadence board */}
              <Card>
                <div style={{ fontWeight: 800, color: C.text, fontFamily: "'Space Grotesk',sans-serif" }}>Cadence board</div>
                <div style={{ fontSize: 11, color: C.textMuted, marginTop: 2, marginBottom: 12 }}>Posting budgets per channel — the guard against overloading your audience. Agents respect this automatically.</div>
                <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(230px, 1fr))", gap: 10 }}>
                  {board.map(c => c && (
                    <div key={c.platform} style={{ background: C.bg2, border: `1px solid ${c.ok ? C.border : C.amber + "55"}`, borderRadius: 10, padding: 12 }}>
                      <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
                        <span style={{ fontWeight: 700, color: C.text, fontSize: 13 }}>{c.label}</span>
                        <span style={{
                          fontSize: 10, fontWeight: 800, padding: "2px 8px", borderRadius: 20, letterSpacing: 1,
                          background: c.ok ? C.green + "22" : C.amber + "22", color: c.ok ? C.green : C.amber,
                        }}>{c.ok ? "CLEAR" : "WAIT"}</span>
                      </div>
                      <div style={{ fontSize: 11, color: C.textDim, marginTop: 6, fontFamily: "'IBM Plex Mono',monospace" }}>
                        {c.inWindow}/{c.maxInWindow} in {c.windowDays}d{c.totalCap ? ` · ${c.total}/${c.totalCap} total` : ""}
                      </div>
                      <div style={{ fontSize: 10, color: C.textMuted, marginTop: 4 }}>
                        {c.ok ? c.note : c.reason + (c.nextOkAt ? ` — clear ${new Date(c.nextOkAt).toLocaleDateString()}` : "")}
                      </div>
                      {socials[c.platform] && <a href={socials[c.platform]} target="_blank" rel="noopener noreferrer" style={{ fontSize: 10, color: C.cyan, textDecoration: "none", display: "inline-block", marginTop: 6 }}>open {c.label} ↗</a>}
                    </div>
                  ))}
                </div>
              </Card>

              {/* Agents */}
              <Card>
                <div style={{ fontWeight: 800, color: C.text, fontFamily: "'Space Grotesk',sans-serif" }}>Agents</div>
                <div style={{ fontSize: 11, color: C.textMuted, marginTop: 2, marginBottom: 12 }}>Each runs server-side on your live campaign + training data. Outputs are drafts — you always post by hand.</div>
                <div style={{ display: "flex", flexDirection: "column", gap: 12 }}>

                  {/* Strategist */}
                  <div style={{ background: C.bg2, border: `1px solid ${C.border}`, borderRadius: 10, padding: 14 }}>
                    <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
                      <span style={{ fontSize: 18 }}>{AGENT_META.strategist.icon}</span>
                      <span style={{ fontWeight: 800, color: AGENT_META.strategist.color }}>{AGENT_META.strategist.label}</span>
                      <span style={{ fontSize: 11, color: C.textMuted }}>· {AGENT_META.strategist.skill}</span>
                    </div>
                    <div style={{ display: "flex", gap: 8, marginTop: 10, flexWrap: "wrap" }}>
                      <input style={{ ...inputStyle, flex: 1, minWidth: 180 }} placeholder="optional context (travel, a big donation landed, feeling flat…)" value={stNotes} onChange={e => setStNotes(e.target.value)} />
                      <Btn small onClick={() => runAgent("strategist", { notes: stNotes })} disabled={running === "strategist"}>{running === "strategist" ? "Thinking…" : "▶ Run"}</Btn>
                    </div>
                    {outputs.strategist && <div style={{ marginTop: 10, background: C.bg1, border: `1px solid ${C.border}`, borderRadius: 8, padding: 12, fontSize: 13, color: C.text, whiteSpace: "pre-wrap", lineHeight: 1.6 }}>{outputs.strategist}</div>}
                  </div>

                  {/* Copywriter */}
                  <div style={{ background: C.bg2, border: `1px solid ${C.border}`, borderRadius: 10, padding: 14 }}>
                    <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
                      <span style={{ fontSize: 18 }}>{AGENT_META.copywriter.icon}</span>
                      <span style={{ fontWeight: 800, color: AGENT_META.copywriter.color }}>{AGENT_META.copywriter.label}</span>
                      <span style={{ fontSize: 11, color: C.textMuted }}>· {AGENT_META.copywriter.skill}</span>
                    </div>
                    <div style={{ display: "flex", gap: 8, marginTop: 10, flexWrap: "wrap" }}>
                      <select style={{ ...inputStyle, width: "auto" }} value={cwPlatform} onChange={e => { setCwPlatform(e.target.value); setGate(null); }}>
                        {Object.entries(CampaignCadence.PLATFORMS).map(([k, p]) => <option key={k} value={k}>{p.label}</option>)}
                      </select>
                      <select style={{ ...inputStyle, width: "auto" }} value={cwType} onChange={e => setCwType(e.target.value)}>
                        <option value="launch">Launch</option>
                        <option value="milestone">Weekly milestone</option>
                        <option value="match_push">Matching push</option>
                        <option value="race_week">Race week</option>
                        <option value="thank_you">Thank-you</option>
                      </select>
                      <input style={{ ...inputStyle, flex: 1, minWidth: 160 }} placeholder="optional angle / notes" value={cwNotes} onChange={e => setCwNotes(e.target.value)} />
                      <Btn small onClick={() => runAgent("copywriter", { platform: cwPlatform, postType: cwType, notes: cwNotes })} disabled={running === "copywriter"}>{running === "copywriter" ? "Writing…" : "▶ Draft"}</Btn>
                    </div>
                    {gate && gate.scope !== "pack" && <div style={{ marginTop: 10, background: C.amber + "15", border: `1px solid ${C.amber}44`, borderRadius: 8, padding: 12, fontSize: 12, color: C.amber }}>
                      Cadence hold on {gate.label}: {gate.reason}{gate.nextOkAt ? ` — clear ${new Date(gate.nextOkAt).toLocaleDateString()}` : ""}. Audience fatigue costs more than a missed post.
                      <span style={{ marginLeft: 10 }}><Btn small variant="ghost" onClick={() => runAgent("copywriter", { platform: cwPlatform, postType: cwType, notes: cwNotes }, true)}>Draft anyway</Btn></span>
                    </div>}
                    {outputs.copywriter && outputs.copywriter.split(/\n---\n/).map((variant, i) => (
                      <div key={i} style={{ marginTop: 10, background: C.bg1, border: `1px solid ${C.border}`, borderRadius: 8, padding: 12 }}>
                        <div style={{ fontSize: 10, color: C.textMuted, textTransform: "uppercase", letterSpacing: 1, marginBottom: 6 }}>Variant {i + 1}</div>
                        <div style={{ fontSize: 13, color: C.text, whiteSpace: "pre-wrap", lineHeight: 1.6 }}>{variant.trim()}</div>
                        <div style={{ display: "flex", gap: 8, marginTop: 10, flexWrap: "wrap" }}>
                          <Btn small variant="secondary" onClick={() => doCopy(`cw${i}`, variant.trim())}>{copied === `cw${i}` ? "✓ Copied" : "⧉ Copy"}</Btn>
                          {socials[cwPlatform] && <Btn small variant="ghost" onClick={() => { copyText(variant.trim()); window.open(socials[cwPlatform], "_blank"); }}>Copy & open {CampaignCadence.PLATFORMS[cwPlatform].label} ↗</Btn>}
                          <Btn small variant="ghost" onClick={() => markPosted(cwPlatform, cwType, variant.trim())}>✓ Mark posted</Btn>
                        </div>
                      </div>
                    ))}
                  </div>

                  {/* Outreach */}
                  <div style={{ background: C.bg2, border: `1px solid ${C.border}`, borderRadius: 10, padding: 14 }}>
                    <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
                      <span style={{ fontSize: 18 }}>{AGENT_META.outreach.icon}</span>
                      <span style={{ fontWeight: 800, color: AGENT_META.outreach.color }}>{AGENT_META.outreach.label}</span>
                      <span style={{ fontSize: 11, color: C.textMuted }}>· {AGENT_META.outreach.skill}</span>
                    </div>
                    <div style={{ display: "flex", gap: 8, marginTop: 10, flexWrap: "wrap", alignItems: "flex-start" }}>
                      <select style={{ ...inputStyle, width: "auto" }} value={orMode} onChange={e => setOrMode(e.target.value)}>
                        <option value="ask">Sponsorship asks</option>
                        <option value="thanks">Thank-yous</option>
                      </select>
                      <textarea style={{ ...inputStyle, flex: 1, minWidth: 200, minHeight: 60, resize: "vertical" }}
                        placeholder={'One person per line: "name — context"\nDave — college roommate, ran Boston 2019\nPriya — JPM desk mate, big Knicks fan'}
                        value={orNames} onChange={e => setOrNames(e.target.value)} />
                      <Btn small onClick={() => runAgent("outreach", { mode: orMode, names: orNames })} disabled={running === "outreach" || !orNames.trim()}>{running === "outreach" ? "Writing…" : "▶ Write"}</Btn>
                    </div>
                    {outputs.outreach && <div style={{ marginTop: 10, background: C.bg1, border: `1px solid ${C.border}`, borderRadius: 8, padding: 12, fontSize: 13, color: C.text, whiteSpace: "pre-wrap", lineHeight: 1.6 }}>
                      {outputs.outreach}
                      <div style={{ marginTop: 10 }}><Btn small variant="secondary" onClick={() => doCopy("or", outputs.outreach)}>{copied === "or" ? "✓ Copied" : "⧉ Copy all"}</Btn></div>
                    </div>}
                  </div>

                  {/* Asset pack — caption + graphic in one run */}
                  <div style={{ background: C.bg2, border: `1px solid ${C.green}44`, borderRadius: 10, padding: 14 }}>
                    <div style={{ display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap" }}>
                      <span style={{ fontSize: 18 }}>📦</span>
                      <span style={{ fontWeight: 800, color: C.green }}>Asset pack</span>
                      <span style={{ fontSize: 11, color: C.textMuted }}>· one click → caption + matching graphic</span>
                    </div>
                    <div style={{ display: "flex", gap: 8, marginTop: 10, flexWrap: "wrap" }}>
                      <select style={{ ...inputStyle, width: "auto" }} value={packPlatform} onChange={e => setPackPlatform(e.target.value)}>
                        {["instagram", "linkedin", "facebook", "strava", "whatsapp"].map(p => <option key={p} value={p}>{p}</option>)}
                      </select>
                      <select style={{ ...inputStyle, width: "auto" }} value={packType} onChange={e => setPackType(e.target.value)}>
                        <option value="launch">Launch</option>
                        <option value="milestone">Weekly milestone</option>
                        <option value="match_push">Matching push</option>
                        <option value="race_week">Race week</option>
                        <option value="thank_you">Thank you</option>
                      </select>
                      <input style={{ ...inputStyle, flex: 1, minWidth: 160 }} placeholder="optional steer, e.g. 'mention the 20-miler'"
                        value={packNotes} onChange={e => setPackNotes(e.target.value)} />
                      <Btn small onClick={() => runAssetPack(false)} disabled={running === "pack" || !library.length}>
                        {running === "pack" ? "Building…" : "▶ Build asset pack"}
                      </Btn>
                    </div>
                    {!library.length && <div style={{ marginTop: 8, fontSize: 12, color: C.textMuted }}>
                      Add photos to the library below first — the art director picks from it.
                    </div>}
                    {gate && gate.scope === "pack" && <div style={{ marginTop: 10, background: C.amber + "15", border: `1px solid ${C.amber}44`, borderRadius: 8, padding: 12, fontSize: 12, color: C.amber }}>
                      Cadence hold on {gate.label}: {gate.reason}{gate.nextOkAt ? ` — clear ${new Date(gate.nextOkAt).toLocaleDateString()}` : ""}.
                      <div style={{ marginTop: 8 }}><Btn small variant="ghost" onClick={() => runAssetPack(true)}>Build anyway</Btn></div>
                    </div>}
                    {packStatus && <div style={{ marginTop: 8, fontSize: 12, color: packStatus.startsWith("✗") ? C.red : C.textDim }}>{packStatus}</div>}
                    {pack && <div style={{ marginTop: 10, background: C.bg1, border: `1px solid ${C.border}`, borderRadius: 8, padding: 12 }}>
                      <img src={pack.dataUrl} alt="campaign asset" style={{ maxWidth: "100%", borderRadius: 8, display: "block" }} />
                      {pack.direction?.why && <div style={{ fontSize: 11, color: C.textMuted, marginTop: 8, fontStyle: "italic" }}>
                        Art direction: {pack.direction.why} ({pack.direction.preset})
                      </div>}
                      <div style={{ whiteSpace: "pre-wrap", fontSize: 13, color: C.textDim, lineHeight: 1.6, marginTop: 10, paddingTop: 10, borderTop: `1px solid ${C.border}` }}>{pack.caption}</div>
                      <div style={{ display: "flex", gap: 8, marginTop: 10, flexWrap: "wrap" }}>
                        <Btn small variant="secondary" onClick={() => doCopy("pack", pack.caption)}>{copied === "pack" ? "✓ Copied" : "⧉ Copy caption"}</Btn>
                        <Btn small variant="secondary" onClick={downloadPack}>⬇ Download image</Btn>
                        <Btn small variant="ghost" onClick={() => markPosted(pack.platform, pack.postType, pack.caption)}>✓ Mark posted</Btn>
                      </div>
                      <div style={{ fontSize: 10, color: C.textMuted, marginTop: 8 }}>Overlay numbers come from the live campaign — check the text before posting (models can mangle characters).</div>
                    </div>}
                  </div>

                  {/* Photo library */}
                  <div style={{ background: C.bg2, border: `1px solid ${C.border}`, borderRadius: 10, padding: 14 }}>
                    <div style={{ display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap" }}>
                      <span style={{ fontSize: 18 }}>🗂</span>
                      <span style={{ fontWeight: 800, color: C.cyan }}>Photo library</span>
                      <span style={{ fontSize: 11, color: C.textMuted }}>· {library.length} photo{library.length === 1 ? "" : "s"} — captioned on upload so agents can choose</span>
                    </div>
                    <div style={{ display: "flex", gap: 8, marginTop: 10, flexWrap: "wrap", alignItems: "center" }}>
                      <label style={{ display: "inline-block" }}>
                        <input type="file" accept="image/*" multiple onChange={onAddToLibrary} style={{ display: "none" }} />
                        <span style={{ display: "inline-block", padding: "4px 12px", borderRadius: 8, cursor: "pointer", fontSize: 12, fontWeight: 600, fontFamily: "'IBM Plex Mono', monospace", background: "transparent", color: C.cyan, border: `1px solid ${C.cyan}40` }}>⬆ Add photos (multi-select)</span>
                      </label>
                      {libStatus && <span style={{ fontSize: 12, color: libStatus.startsWith("✗") ? C.red : C.textDim }}>{libStatus}</span>}
                    </div>
                    {library.length > 0 && <div style={{ display: "flex", flexWrap: "wrap", gap: 8, marginTop: 10 }}>
                      {library.map(p => (
                        <div key={p.id} title={`${p.caption || p.id}\n${p.orientation || ""} · quality ${p.quality || "?"} · used ${p.usedCount || 0}×`}
                          style={{ width: 130, background: C.bg1, border: `1px solid ${C.border}`, borderRadius: 8, padding: 8, fontSize: 10, color: C.textMuted, position: "relative" }}>
                          <div style={{ color: C.textDim, lineHeight: 1.4, minHeight: 28 }}>{(p.caption || "(untagged)").slice(0, 60)}</div>
                          <div style={{ marginTop: 4 }}>{p.orientation || "?"} · {p.quality || "?"} · {p.usedCount || 0}×</div>
                          <span onClick={() => removeFromLibrary(p)} style={{ position: "absolute", top: 4, right: 6, color: C.red, cursor: "pointer", fontSize: 11 }}>✕</span>
                        </div>
                      ))}
                    </div>}
                  </div>

                  {/* Studio */}
                  <div style={{ background: C.bg2, border: `1px solid ${C.border}`, borderRadius: 10, padding: 14 }}>
                    <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
                      <span style={{ fontSize: 18 }}>{AGENT_META.studio.icon}</span>
                      <span style={{ fontWeight: 800, color: AGENT_META.studio.color }}>{AGENT_META.studio.label}</span>
                      <span style={{ fontSize: 11, color: C.textMuted }}>· {AGENT_META.studio.skill}</span>
                    </div>
                    <div style={{ display: "flex", gap: 8, marginTop: 10, flexWrap: "wrap", alignItems: "center" }}>
                      <label style={{ display: "inline-block" }}>
                        <input type="file" accept="image/*" onChange={onUploadPhoto} style={{ display: "none" }} />
                        <span style={{ display: "inline-block", padding: "4px 12px", borderRadius: 8, cursor: "pointer", fontSize: 12, fontWeight: 600, fontFamily: "'IBM Plex Mono', monospace", background: "transparent", color: C.cyan, border: `1px solid ${C.cyan}40` }}>⬆ Upload photo</span>
                      </label>
                      <Btn small variant="secondary" onClick={onGooglePhotos}>🖼 Google Photos</Btn>
                      {photo && <img src={photo.previewUrl} alt="source" style={{ height: 44, borderRadius: 6, border: `1px solid ${C.border}` }} />}
                    </div>
                    <div style={{ display: "flex", gap: 8, marginTop: 8, flexWrap: "wrap" }}>
                      <select style={{ ...inputStyle, width: "auto" }} value={studioPreset} onChange={e => setStudioPreset(e.target.value)}>
                        {STUDIO_PRESETS.map(p => <option key={p.key} value={p.key}>{p.label}</option>)}
                      </select>
                      <input style={{ ...inputStyle, flex: 1, minWidth: 160 }}
                        placeholder={studioPreset === "milestone" ? 'milestone text, e.g. "$1,500 UNLOCKED"' : studioPreset === "freeform" ? "describe the edit (required)" : "optional extra instruction"}
                        value={studioNotes} onChange={e => setStudioNotes(e.target.value)} />
                      <Btn small onClick={runStudio} disabled={running === "studio" || !photo || (studioPreset === "freeform" && !studioNotes.trim())}>{running === "studio" ? "Rendering…" : "▶ Generate"}</Btn>
                    </div>
                    {studioStatus && <div style={{ marginTop: 8, fontSize: 12, color: studioStatus.startsWith("✗") ? C.red : C.textDim }}>{studioStatus}</div>}
                    {studioResult && <div style={{ marginTop: 10, background: C.bg1, border: `1px solid ${C.border}`, borderRadius: 8, padding: 12 }}>
                      <img src={studioResult.dataUrl} alt="edited" style={{ maxWidth: "100%", borderRadius: 8, display: "block" }} />
                      <div style={{ display: "flex", gap: 8, marginTop: 10, flexWrap: "wrap" }}>
                        <Btn small variant="secondary" onClick={downloadStudio}>⬇ Download</Btn>
                        <Btn small variant="ghost" onClick={() => { setPhoto({ base64: studioResult.dataUrl.split(",")[1], mimeType: studioResult.mimeType, previewUrl: studioResult.dataUrl }); setStudioResult(null); }}>↺ Use as source</Btn>
                      </div>
                      <div style={{ fontSize: 10, color: C.textMuted, marginTop: 8 }}>Overlay numbers come from the live campaign — double-check text before posting (models can mangle characters).</div>
                    </div>}
                  </div>
                </div>
              </Card>

              {/* Post log */}
              <Card>
                <div style={{ fontWeight: 800, color: C.text, fontFamily: "'Space Grotesk',sans-serif", marginBottom: 10 }}>Post log</div>
                {posts.length === 0
                  ? <div style={{ fontSize: 12, color: C.textMuted }}>Nothing posted yet. When you publish a draft, hit "Mark posted" so the cadence board and the agents know.</div>
                  : posts.map(p => (
                    <div key={p.id} style={{ display: "flex", gap: 10, alignItems: "flex-start", padding: "8px 0", borderBottom: `1px solid ${C.border}` }}>
                      <span style={{ fontSize: 10, fontWeight: 800, color: C.cyan, textTransform: "uppercase", letterSpacing: 1, minWidth: 74, paddingTop: 2 }}>{p.platform}</span>
                      <span style={{ flex: 1, fontSize: 12, color: C.textDim, lineHeight: 1.5 }}>{String(p.text || "").slice(0, 140)}{(p.text || "").length > 140 ? "…" : ""}</span>
                      <span style={{ fontSize: 10, color: C.textMuted, whiteSpace: "nowrap", paddingTop: 2 }}>{p.postedAt ? new Date(p.postedAt).toLocaleDateString() : ""}</span>
                      <span onClick={() => db.collection("users").doc(userId).collection("campaignPosts").doc(p.id).delete()} style={{ color: C.red, cursor: "pointer", fontSize: 11, paddingTop: 2 }}>✕</span>
                    </div>
                  ))}
              </Card>
            </React.Fragment>
          )}

          {!campaign && !editOpen && (
            <EmptyState title="No campaign configured" subtitle='Hit "Build campaign" — the form comes pre-filled with the NYC Marathon 2026 sponsorship campaign.' />
          )}
        </div>
      );
    }

window.AppViews = Object.assign(window.AppViews || {}, { Campaign });
