// Create Post — coral redesign composer, wired to live ClearCast data. (page-create-live.jsx)

const CP_LIMITS = { instagram: 2200, facebook: 63206, x: 280, linkedin: 3000, tiktok: 2200, youtube: 5000, pinterest: 500, threads: 500, mastodon: 500 };
const CP_FIT_OPTIONS = [
  { id: "original", label: "Original", ratio: "Auto", w: 26, h: 26 },
  { id: "square", label: "Square", ratio: "1:1", w: 22, h: 22 },
  { id: "portrait", label: "Portrait", ratio: "4:5", w: 18, h: 22 },
  { id: "vertical", label: "Vertical", ratio: "9:16", w: 14, h: 24 },
];
const CP_RECOMMENDED_FIT = {
  instagram: "portrait", facebook: "square", x: "original", linkedin: "square",
  tiktok: "vertical", youtube: "original", pinterest: "portrait", threads: "square", mastodon: "square",
};
const CP_FIT_ASPECT = { original: "1:1", square: "1:1", portrait: "4:5", vertical: "9:16" };
const CP_TONES = ["Shorter", "Longer", "Friendly", "Professional", "Witty", "Punchy", "Luxury"];
const AI_IMAGE_MODELS = [
  { id: "fal-ai/nano-banana-2",              provider: "fal", label: "Nano Banana 2",    badge: "Google · Fast",  desc: "Google's latest — fast, high quality",          cost: "~$0.02" },
  { id: "fal-ai/nano-banana-pro",            provider: "fal", label: "Nano Banana Pro",  badge: "Google · Pro",   desc: "Stronger realism & typography",                 cost: "~$0.04" },
  { id: "fal-ai/flux-pro/v1.1",              provider: "fal", label: "Flux 1.1 Pro",     badge: "Photorealistic", desc: "Sharp, detailed, general purpose",              cost: "~$0.05" },
  { id: "fal-ai/flux-pro/v1.1-ultra",        provider: "fal", label: "Flux Pro Ultra",   badge: "2K Detail",      desc: "4× more detail, ultra-high resolution output",  cost: "~$0.06" },
  { id: "fal-ai/flux/dev",                   provider: "fal", label: "Flux Dev",          badge: "Fast",           desc: "Quick drafts and social graphics",              cost: "~$0.01" },
  { id: "fal-ai/stable-diffusion-v35-large", provider: "fal", label: "SD 3.5 Large",     badge: "Artistic",       desc: "Rich detail, painterly style",                 cost: "~$0.04" },
  { id: "fal-ai/recraft-v3",                 provider: "fal", label: "Recraft V3",        badge: "Design + Text",  desc: "Top-ranked for text in images, vector, design", cost: "~$0.04" },
];
const AI_VIDEO_MODELS = [
  { id: "bytedance/seedance-2.0/text-to-video",        provider: "fal", label: "Seedance 2.0",       badge: "ByteDance · Pro",  desc: "Cinematic, native audio, physics-aware",   cost: "~$0.50" },
  { id: "bytedance/seedance-2.0/fast/text-to-video",   provider: "fal", label: "Seedance 2.0 Fast",  badge: "ByteDance · Fast", desc: "Same model, lower latency & cost",         cost: "~$0.20" },
  { id: "fal-ai/kling-video/v3/pro/text-to-video",     provider: "fal", label: "Kling 3 Pro",        badge: "Latest",           desc: "Newest Kling — best motion quality",       cost: "~$0.45" },
  { id: "fal-ai/kling-video/v2.6/pro/text-to-video",   provider: "fal", label: "Kling 2.6 Pro",      badge: "Best quality",     desc: "Native 9:16, smooth motion, 5s clip",     cost: "~$0.35" },
  { id: "fal-ai/kling-video/v1.6/pro/text-to-video",   provider: "fal", label: "Kling 1.6 Pro",      badge: "Balanced",         desc: "Solid quality, slightly faster",           cost: "~$0.28" },
  { id: "fal-ai/minimax/hailuo-2.3/pro/text-to-video", provider: "fal", label: "MiniMax Hailuo 2.3", badge: "Fast",             desc: "Quick generation, good motion",            cost: "~$0.20" },
  { id: "fal-ai/luma-dream-machine/ray-2",             provider: "fal", label: "Luma Ray 2",         badge: "Cinematic",        desc: "Smooth camera motion, film-like",          cost: "~$0.50" },
];
const CP_FALLBACK_HASHTAGS = ["#smallbusiness", "#community", "#shoplocal", "#newpost", "#behindthescenes"];
const CP_BEST_TIMES = [{ time: "09:15", score: 94 }, { time: "12:30", score: 88 }, { time: "18:45", score: 91 }];
// HONEST best-time: derived from THIS workspace's own published-post history (the
// hours posts actually went out), shown only with enough real data. No black box —
// we say exactly what it's based on. Falls back to clearly-labeled general windows.
function cpBestTimes(data) {
  const posts = (data && data.posts) || [];
  const hours = [];
  for (const p of posts) {
    const published = p.status === "published" || (Array.isArray(p.publishedUrls) && p.publishedUrls.length);
    if (!published) continue;
    const iso = (p.publishedUrls && p.publishedUrls[0] && p.publishedUrls[0].at) || p.lastPublishAttemptAt || (p.date && p.time ? `${p.date}T${p.time}` : p.createdAt);
    const d = new Date(iso);
    if (!isNaN(d.getTime())) hours.push(d.getHours());
  }
  if (hours.length < 4) return { personalized: false, count: hours.length, times: CP_BEST_TIMES.map((b) => b.time) };
  const byHour = {};
  hours.forEach((h) => { byHour[h] = (byHour[h] || 0) + 1; });
  const top = Object.entries(byHour).sort((a, b) => b[1] - a[1]).slice(0, 3)
    .map(([h]) => `${String(h).padStart(2, "0")}:00`)
    .sort();
  return { personalized: true, count: hours.length, times: top };
}

// Channels whose publishing API charges per post (X/Twitter) — gated to paid plans.
const CP_PREMIUM_CHANNELS = ["x"];

// A "hard" block = the channel itself is unavailable (not connected / needs
// reconnect / plan-gated / outage). These get the dimmed "unavailable" chip.
function cpChannelHardIssue(integration, canPremium) {
  const i = integration;
  if (!i || !i.connected) return "not connected";
  if (CP_PREMIUM_CHANNELS.includes(i.id) && !canPremium) return "needs Starter plan";
  if (i.status === "reauth") return "needs reconnect";
  if (i.status === "limited") return "limited access";
  if (i.status === "outage") return "platform outage";
  if ((i.issues || []).length) return i.issues[0];
  return null;
}
// Why a connected channel can't publish this post right now (mirrors the server's
// postReadinessIssues so the composer never silently submits an invalid channel).
// A "soft" issue (wrong format for this post) is NOT a hard block — the channel is
// connected and available, the post just needs the right format/media. We surface
// it as a fixable flag instead of dimming the channel like it's disconnected.
function cpChannelIssue(integration, contentType, canPremium) {
  const hard = cpChannelHardIssue(integration, canPremium);
  if (hard) return hard;
  const i = integration;
  if ((i.supportedTypes || []).length && !i.supportedTypes.includes(contentType)) {
    return "needs " + i.supportedTypes.join("/") + " format";
  }
  return null;
}
// Deterministic inclusivity + disclosure checks. All TRUE/computable from the
// draft — no AI guesswork. Returns [{ id, level: "warn"|"tip", title, detail, fixable }].
// This is a genuine differentiator: the big tools don't surface a11y/disclosure inline.
const CP_SPONSOR_WORDS = /\b(sponsored|sponsor|gifted|gift(ed)? by|partner(ed|ship)? with|in partnership|paid partnership|brand ?(ambassador|deal)|affiliate|#?ad\b|promo code|use code|advertisement)\b/i;
const CP_DISCLOSURE_TAGS = /#(ad|sponsored|paidpartnership|gifted|affiliate)\b/i;
function cpInclusivityChecks({ caption, attached, hasAiMedia }) {
  const out = [];
  const text = String(caption || "");
  const images = (attached || []).filter((m) => m.type === "image");
  // 1. Alt text on every image (screen-reader access). Genuinely sent to Mastodon/X/Facebook.
  const missingAlt = images.filter((m) => !String(m.altText || "").trim());
  if (images.length && missingAlt.length) {
    out.push({ id: "alt", level: "warn", title: `${missingAlt.length} image${missingAlt.length > 1 ? "s" : ""} missing alt text`, detail: "Add a short description so screen-reader users know what's in the image. We send it to Mastodon, X and Facebook.", fixable: true });
  }
  // 2. ALL-CAPS shouting (screen readers spell it out letter-by-letter).
  const letters = text.replace(/[^A-Za-z]/g, "");
  if (letters.length >= 12 && letters === letters.toUpperCase()) {
    out.push({ id: "caps", level: "tip", title: "Caption is in ALL CAPS", detail: "Screen readers may read this letter-by-letter, and it reads as shouting. Consider sentence case." });
  }
  // 3. Emoji overload (each emoji is announced by its full name).
  const emoji = (text.match(/\p{Extended_Pictographic}/gu) || []).length;
  if (emoji >= 6) {
    out.push({ id: "emoji", level: "tip", title: `${emoji} emoji in the caption`, detail: "Screen readers announce every emoji by name. A handful is friendlier than a wall of them." });
  }
  // 4. Non-camelCase multiword hashtags (#blacklivesmatter vs #BlackLivesMatter).
  const badTags = (text.match(/#[a-z0-9]+/g) || []).filter((t) => t.length > 9 && t === t.toLowerCase() && /[a-z]{4,}/.test(t.slice(1)));
  if (badTags.length) {
    out.push({ id: "camel", level: "tip", title: "Hashtags aren't CamelCase", detail: `Capitalize each word (e.g. ${badTags[0].replace(/^#(.)(.*)/, (_, a, b) => "#" + a.toUpperCase() + b)}) so screen readers read them correctly.` });
  }
  // 5. Sponsored content without a disclosure tag (FTC).
  if (CP_SPONSOR_WORDS.test(text) && !CP_DISCLOSURE_TAGS.test(text)) {
    out.push({ id: "disclosure", level: "warn", title: "Looks sponsored — add a disclosure", detail: "Paid/gifted content should be clearly labeled (e.g. #ad or #sponsored) per FTC and platform rules.", fixable: true, addTag: "#ad" });
  }
  // 6. AI-generated media without an AI label (Meta/TikTok increasingly require this).
  if (hasAiMedia && !/\b(ai[- ]?generated|made with ai|ai art|#ai\b)/i.test(text)) {
    out.push({ id: "ailabel", level: "tip", title: "AI-generated media in this post", detail: "Meta and TikTok ask creators to label AI-generated imagery. Consider noting it (e.g. “AI-generated”).", fixable: true, addTag: " (AI-generated)" });
  }
  return out;
}
// Native per-platform size ceilings (mirror of server PLATFORM_*_MAX_MB).
const CP_PLATFORM_VIDEO_MAX_MB = { x: 512, instagram: 300, facebook: 1024, tiktok: 4096, linkedin: 5120, youtube: 256000 };
const CP_PLATFORM_IMAGE_MAX_MB = { x: 5, instagram: 8, facebook: 8, linkedin: 5, pinterest: 10, threads: 8, bluesky: 1 };

function cpTodayISO() {
  const d = new Date();
  return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
}
function cpMediaObj(asset) {
  if (!asset) return null;
  return { id: asset.id, type: asset.type || "image", name: asset.name || asset.fileName || "media", src: asset.url, altText: asset.altText || "", aiGenerated: Boolean(asset.simulated || asset.aiGenerated || asset.prompt), duration: asset.type === "video" ? "video" : null };
}

function StepLabel({ num, done, children, hint }) {
  return (
    <div className="cp-step-label">
      <span className={`cp-step-num ${done ? "done" : ""}`}>{done ? <Icon.Check width="13" height="13" /> : num}</span>
      <h2>{children}</h2>
      {hint && <span className="cp-step-hint">{hint}</span>}
    </div>
  );
}

function CreatePostPage() {
  const hub = window.useClearCast();
  const data = hub.data || {};
  const integrations = data.integrations || [];
  const connected = integrations.filter((i) => i.connected);
  const intById = Object.fromEntries(integrations.map((i) => [i.id, i]));
  const workspace = data.workspace || {};
  const rules = workspace.publishingRules || {};
  const voice = workspace.aiSettings?.voice || workspace.brandVoice || "";
  const businessName = (data.workspaces || []).find((w) => w.id === data.activeWorkspaceId)?.name || workspace.name || "ClearCast";
  const handle = (businessName.toLowerCase().replace(/[^a-z0-9]+/g, "") || "yourbrand").slice(0, 22);
  const avatarLetter = (businessName.trim()[0] || "C").toUpperCase();

  // Edit mode (entered from Calendar/Published via sessionStorage flag)
  const editId = (() => { try { return sessionStorage.getItem("clearcast-edit-post-id") || ""; } catch (e) { return ""; } })();
  const editPost = editId ? (data.posts || []).find((p) => p.id === editId) : null;
  const isEditing = !!editPost;

  // Only ever offer REAL connected channels — never fake placeholders that can't publish.
  const channelList = connected.map((i) => i.id);
  const hasChannels = channelList.length > 0;
  const planId = (data.billing && data.billing.plan) || "free";
  // Premium channels (X) need a paid plan — but admins get full access.
  const canPremium = planId !== "free" || Boolean(data.isAdmin);

  const [platforms, setPlatforms] = React.useState(() => {
    if (editPost) return Object.fromEntries((editPost.platforms || []).map((id) => [id, true]));
    const base = {};
    // Default-select only channels that can actually publish a feed post right now,
    // so the post is valid out of the box (no silent "not ready" rejection).
    const ready = connected.filter((i) => !cpChannelIssue(i, "feed", canPremium));
    (ready.length ? ready : connected).forEach((i) => { base[i.id] = true; });
    return base;
  });
  const [caption, setCaption] = React.useState(editPost?.caption || "");
  const [attached, setAttached] = React.useState(() =>
    (editPost?.media || []).map((id) => {
      const found = (data.media || []).find((m) => m.id === id || m.url === id || m.fileName === id);
      // Don't silently drop a saved attachment that's no longer in the library —
      // keep a minimal object so the id survives a re-save.
      return found ? cpMediaObj(found) : { id, type: /\.(mp4|mov|webm)$/i.test(String(id)) ? "video" : "image", name: String(id).split("/").pop() || "media", src: /^https?:|^data:/.test(String(id)) ? id : "" };
    }).filter(Boolean)
  );
  const [activeMedia, setActiveMedia] = React.useState(0);
  const [date, setDate] = React.useState(editPost?.date || cpTodayISO());
  const [time, setTime] = React.useState(editPost?.time || "09:15");
  // Default to "feed" — unless none of the connected channels support feed (e.g.
  // a YouTube-only workspace, which is video/short only). In that case start on a
  // format the connected channels actually support, so no channel is dead on arrival.
  const cpDefaultType = (() => {
    if (editPost?.contentType) return editPost.contentType;
    const supports = (t) => connected.some((i) => !((i.supportedTypes || []).length) || i.supportedTypes.includes(t));
    if (supports("feed")) return "feed";
    return ["short", "reel", "story"].find(supports) || "feed";
  })();
  const [contentType, setContentType] = React.useState(cpDefaultType);
  const [firstComment, setFirstComment] = React.useState(editPost?.firstComment || "");

  const [showTones, setShowTones] = React.useState(false);
  const [showFirstComment, setShowFirstComment] = React.useState(!!editPost?.firstComment);
  const [showHashtags, setShowHashtags] = React.useState(false);
  // Creator tools: short-form video script, quote graphic, repurpose-content pack.
  const [showScript, setShowScript] = React.useState(false);
  const [scriptBusy, setScriptBusy] = React.useState(false);
  const [scriptResult, setScriptResult] = React.useState("");
  const [showVerse, setShowVerse] = React.useState(false);
  const [quoteMode, setQuoteMode] = React.useState("custom"); // custom quote | Bible verse lookup
  const [quoteText, setQuoteText] = React.useState("");
  const [quoteAttribution, setQuoteAttribution] = React.useState("");
  const [verseRef, setVerseRef] = React.useState("");
  const [verseBusy, setVerseBusy] = React.useState(false);
  const [showRepurpose, setShowRepurpose] = React.useState(false);
  const [packTitle, setPackTitle] = React.useState("");
  const [packText, setPackText] = React.useState("");
  const [packBusy, setPackBusy] = React.useState(false);
  const [packPosts, setPackPosts] = React.useState([]);
  const [altBusy, setAltBusy] = React.useState(false);
  const [showLibrary, setShowLibrary] = React.useState(false);
  const [discVariants, setDiscVariants] = React.useState(false);
  const [discMedia, setDiscMedia] = React.useState(false);
  const [discRules, setDiscRules] = React.useState(false);
  const [discA11y, setDiscA11y] = React.useState(false);

  const [variants, setVariants] = React.useState({});
  const [fits, setFits] = React.useState({});
  const [activeVariantPf, setActiveVariantPf] = React.useState(channelList[0] || "instagram");
  const [approvalRequired, setApprovalRequired] = React.useState(rules.approvalRequired !== false);
  const [dragging, setDragging] = React.useState(false);
  const [uploading, setUploading] = React.useState(false);
  const [aiPicker, setAiPicker] = React.useState(null); // null | "image" | "video"
  const [aiPickerModel, setAiPickerModel] = React.useState(null);
  const [aiPickerPrompt, setAiPickerPrompt] = React.useState("");
  const [busyTone, setBusyTone] = React.useState("");
  const [submitting, setSubmitting] = React.useState("");
  const [hashtagSugs, setHashtagSugs] = React.useState(CP_FALLBACK_HASHTAGS);

  const [previewFormat, setPreviewFormat] = React.useState("feed");
  const [previewDevice, setPreviewDevice] = React.useState("ios");
  const [activePreviewPlatform, setActivePreviewPlatform] = React.useState(channelList[0] || "instagram");

  const fileRef = React.useRef(null);
  const mediaLibrary = (data.media || []).map(cpMediaObj);

  const selected = Object.keys(platforms).filter((id) => platforms[id]);
  React.useEffect(() => {
    if (selected.length && !selected.includes(activePreviewPlatform)) setActivePreviewPlatform(selected[0]);
    if (selected.length && !selected.includes(activeVariantPf)) setActiveVariantPf(selected[0]);
  }, [selected.join("|")]);

  // Live hashtag suggestions when the picker opens
  React.useEffect(() => {
    if (!showHashtags || !hub.actions.hashtagSuggestions) return;
    let alive = true;
    hub.actions.hashtagSuggestions({ caption, platforms: selected, contentType })
      .then((list) => { if (alive && Array.isArray(list) && list.length) setHashtagSugs(list.slice(0, 8)); })
      .catch(() => {});
    return () => { alive = false; };
  }, [showHashtags]);

  // Pick up media handed off from Creator Studio ("Post this ad now"). Attaches
  // the generated asset and, for video ads, seeds the caption from the overlay copy.
  React.useEffect(() => {
    let raw;
    try { raw = sessionStorage.getItem("studio-inject-media"); } catch (e) { return; }
    if (!raw) return;
    try { sessionStorage.removeItem("studio-inject-media"); } catch (e) {}
    let payload;
    try { payload = JSON.parse(raw); } catch (e) { return; }
    if (!payload || !payload.url) return;
    const fromLib = (data.media || []).find((m) => m.id === payload.mediaId || m.url === payload.url);
    const obj = fromLib ? cpMediaObj(fromLib) : {
      id: payload.mediaId || payload.url,
      type: payload.type || "image",
      name: "studio-ad",
      src: payload.url,
      aiGenerated: true,
    };
    addAttached(obj);
    if (payload.caption) setCaption((c) => (c && c.trim() ? c : payload.caption));
    if (payload.firstComment) { setFirstComment((c) => (c && c.trim() ? c : payload.firstComment)); setShowFirstComment(true); }
    hub.toast("Ad media added — review and schedule when ready");
  }, []);

  const toggle = (id) => setPlatforms((p) => ({ ...p, [id]: !p[id] }));
  const minLimit = selected.length ? Math.min(...selected.map((id) => CP_LIMITS[id] || 2200)) : 2200;
  const tightestPf = selected.slice().sort((a, b) => (CP_LIMITS[a] || 2200) - (CP_LIMITS[b] || 2200))[0];
  const overLimit = caption.length > minLimit;
  const nearLimit = !overLimit && caption.length > minLimit * 0.9;
  const hashtagCount = (caption.match(/#[\p{L}\p{N}_]+/gu) || []).length;

  const variantCount = selected.filter((id) => variants[id]?.useGlobalCaption === false && variants[id]?.caption?.trim()).length;
  const fitCount = selected.filter((id) => fits[id] && fits[id] !== CP_RECOMMENDED_FIT[id]).length;

  const activeFit = fits[activeVariantPf] || CP_RECOMMENDED_FIT[activeVariantPf] || "original";
  const previewFit = fits[activePreviewPlatform] || CP_RECOMMENDED_FIT[activePreviewPlatform] || "original";
  const activeVariant = variants[activePreviewPlatform] || {};
  const usesGlobal = activeVariant.useGlobalCaption !== false;
  const previewCaption = usesGlobal ? caption : (activeVariant.caption || caption);
  const previewMediaObj = attached[activeMedia] || null;

  // A channel's own publish blocker (connection/premium/content-type + its caption
  // limit). Flagged channels are skipped at publish; they must NOT block the others.
  const channelIssueFull = (id) => {
    const base = cpChannelIssue(intById[id], contentType, canPremium);
    if (base) return base;
    const lim = CP_LIMITS[id];
    if (lim) {
      const v = variants[id];
      const txt = (v && v.useGlobalCaption === false && (v.caption || "").trim()) ? v.caption : caption;
      if (String(txt || "").length > lim) return `over ${lim.toLocaleString()}-char limit`;
    }
    const vmax = CP_PLATFORM_VIDEO_MAX_MB[id];
    const imax = CP_PLATFORM_IMAGE_MAX_MB[id];
    if (vmax || imax) {
      for (const m of attached) {
        const asset = (data.media || []).find((a) => a.id === m.id);
        if (!asset || !asset.size) continue;
        const isVid = m.type === "video" || asset.type === "video";
        const cap = isVid ? vmax : imax;
        if (cap && asset.size > cap * 1024 * 1024) {
          return `${isVid ? "video" : "image"} too large (max ${cap >= 1024 ? cap / 1024 + "GB" : cap + "MB"})`;
        }
      }
    }
    return null;
  };
  const readyChannels = selected.filter((id) => !channelIssueFull(id));
  const issues = [];
  if (!selected.length) issues.push("Select at least one channel");
  if (!caption.trim()) issues.push("Write a caption");
  if (selected.length && caption.trim() && !readyChannels.length) issues.push("No selected channel can publish this yet — fix one below");
  // Per-channel flags (surfaced as "N to fix"; the post still publishes to the ready ones).
  selected.forEach((id) => {
    const issue = channelIssueFull(id);
    if (issue) issues.push(`${Platforms[id]?.name || id}: ${issue}`);
  });
  // Ready = a caption plus at least one selected channel that can actually publish.
  const ready = Boolean(caption.trim()) && selected.length > 0 && readyChannels.length > 0;

  const appendCaption = (txt) => setCaption((c) => `${c.trim()} ${txt}`.trim());
  const removeMedia = (i) => {
    setAttached((list) => list.filter((_, idx) => idx !== i));
    setActiveMedia((a) => (i <= a && a > 0 ? a - 1 : a));
  };
  const addAttached = (obj) => {
    if (!obj) return;
    setAttached((list) => {
      if (list.some((m) => m.id === obj.id)) { setActiveMedia(list.findIndex((m) => m.id === obj.id)); return list; }
      const next = [...list, obj];
      setActiveMedia(next.length - 1); // select the newly added item by its real index
      return next;
    });
  };
  // Alt text edits update local state instantly; persisted to the media asset on
  // blur so it's sent to platforms at publish (Mastodon/X/Facebook).
  const setAltLocal = (i, text) => setAttached((list) => list.map((m, idx) => idx === i ? { ...m, altText: text } : m));
  const saveAlt = (i) => {
    const m = attached[i];
    if (m && m.id) { try { hub.actions.updateMedia(m.id, { altText: String(m.altText || "").slice(0, 1000) }); } catch (e) { /* non-fatal */ } }
  };

  const onFile = async (e) => {
    const files = Array.from(e.target.files || []);
    if (!files.length) return;
    try {
      setUploading(true);
      for (const file of files) {
        try {
          const asset = await hub.actions.uploadMedia(file, "all");
          addAttached(cpMediaObj(asset));
        } catch (err) { console.error(err); hub.toast(`${file.name || "File"}: ${err.message || "upload failed"}`); }
      }
    } finally { setUploading(false); e.target.value = ""; }
  };

  const openAiPicker = (type) => {
    setAiPicker(type);
    setAiPickerPrompt(caption.trim());
    setAiPickerModel(type === "image" ? AI_IMAGE_MODELS[0] : AI_VIDEO_MODELS[0]);
  };

  const runAiGenerate = async () => {
    if (!aiPickerModel || !aiPickerPrompt.trim()) return;
    const type = aiPicker;
    const model = aiPickerModel;
    const prompt = aiPickerPrompt.trim();
    setAiPicker(null);
    setUploading(true);
    if (type === "video") hub.toast(`Generating video with ${model.label} — this takes 3–6 minutes. You can keep working; we'll attach it when it's ready.`);
    try {
      let asset;
      if (type === "image") {
        const aspect = CP_FIT_ASPECT[activeFit] || "1:1";
        asset = await hub.actions.generateImage({ prompt, aspect, provider: model.provider, model: model.id });
      } else {
        asset = await hub.actions.generateVideo({ prompt, aspect: "9:16", provider: model.provider, model: model.id });
      }
      addAttached(cpMediaObj(asset));
    } catch (err) { console.error(err); hub.toast(err.message || `AI ${type} generation failed`); }
    finally { setUploading(false); }
  };

  const runScript = async () => {
    if (scriptBusy) return;
    const topic = caption.trim() || window.prompt("What is the video about?") || "";
    if (!topic.trim()) return;
    setScriptBusy(true);
    try {
      const r = await hub.actions.aiVideoScript({ topic, platform: activePreviewPlatform, seconds: 30 });
      setScriptResult(r.script || "");
    } catch (e) { hub.toast(e.message || "Script generation failed"); }
    finally { setScriptBusy(false); }
  };

  // Quote graphic: render any quote (or a looked-up Bible verse — public-domain
  // WEB via bible-api.com, no key) as a brand-styled 1080×1080 card, upload it
  // into the media library, attach it, and set honest alt text automatically.
  const makeQuoteGraphic = async () => {
    setVerseBusy(true);
    try {
      let text = "";
      let reference = "";
      if (quoteMode === "verse") {
        const ref = verseRef.trim();
        if (!ref) { hub.toast("Enter a verse reference, e.g. John 3:16"); setVerseBusy(false); return; }
        const vres = await fetch(`https://bible-api.com/${encodeURIComponent(ref)}?translation=web`);
        const vj = await vres.json().catch(() => ({}));
        text = String(vj.text || "").replace(/\s+/g, " ").trim();
        reference = vj.reference || ref;
        if (!vres.ok || !text) { hub.toast(`Couldn't find "${ref}" — try a reference like John 3:16`); setVerseBusy(false); return; }
      } else {
        text = quoteText.replace(/\s+/g, " ").trim();
        reference = quoteAttribution.trim();
        if (!text) { hub.toast("Enter the quote text first"); setVerseBusy(false); return; }
      }
      const c = document.createElement("canvas"); c.width = 1080; c.height = 1080;
      const x = c.getContext("2d");
      const g = x.createLinearGradient(0, 0, 1080, 1080);
      g.addColorStop(0, "#14100e"); g.addColorStop(1, "#2a1d16");
      x.fillStyle = g; x.fillRect(0, 0, 1080, 1080);
      const ring = x.createLinearGradient(0, 0, 1080, 0);
      ring.addColorStop(0, "#f0653f"); ring.addColorStop(1, "#ff7d57");
      x.strokeStyle = ring; x.lineWidth = 6; x.strokeRect(50, 50, 980, 980);
      x.fillStyle = "#f5ece4"; x.textAlign = "center";
      const verse = text.length > 360 ? text.slice(0, 357).replace(/\s+\S*$/, "") + "…" : text;
      const size = verse.length > 240 ? 40 : verse.length > 140 ? 48 : 56;
      x.font = `600 ${size}px Georgia, 'Times New Roman', serif`;
      const words = verse.split(" "); const lines = []; let line = "";
      for (const w of words) {
        if (x.measureText(line + " " + w).width > 860 && line) { lines.push(line); line = w; }
        else line = line ? line + " " + w : w;
      }
      if (line) lines.push(line);
      const lh = size * 1.35; let y = 540 - ((lines.length - 1) * lh) / 2 - 40;
      x.fillText("“", 540, y - lh * 0.9);
      for (const l of lines) { x.fillText(l, 540, y); y += lh; }
      if (reference) {
        x.fillStyle = "#ff7d57"; x.font = "700 34px Inter, sans-serif";
        x.fillText(reference.toUpperCase(), 540, y + 30);
      }
      const brandName = (hub.data.workspace && hub.data.workspace.name) || "";
      if (brandName) { x.fillStyle = "rgba(245,236,228,0.55)"; x.font = "500 24px Inter, sans-serif"; x.fillText(brandName, 540, 985); }
      const blob = await new Promise((r) => c.toBlob(r, "image/jpeg", 0.92));
      const slug = (reference || verse.slice(0, 24)).replace(/[^a-z0-9]+/gi, "-").toLowerCase();
      const file = new File([blob], `quote-${slug}.jpg`, { type: "image/jpeg" });
      const asset = await hub.actions.uploadMedia(file, "all");
      if (asset && asset.id) {
        const alt = `Quote graphic: “${verse}”${reference ? ` — ${reference}` : ""}`;
        try { await hub.actions.updateMedia(asset.id, { altText: alt }); } catch (e) { /* non-fatal */ }
        addAttached({ ...cpMediaObj(asset), altText: alt });
        hub.toast("Quote graphic added");
        setShowVerse(false);
      }
    } catch (e) { hub.toast(e.message || "Quote graphic failed"); }
    finally { setVerseBusy(false); }
  };

  const runRepurpose = async () => {
    if (packBusy) return;
    setPackBusy(true);
    try {
      const r = await hub.actions.aiRepurpose({ transcript: packText, title: packTitle });
      setPackPosts((r.posts || []).map((p) => ({ ...p, selected: true })));
    } catch (e) { hub.toast(e.message || "Repurpose failed"); }
    finally { setPackBusy(false); }
  };

  const addPackDrafts = async () => {
    const chosen = packPosts.filter((p) => p.selected);
    if (!chosen.length) return;
    const platforms = selected.length ? selected : ["facebook", "instagram"];
    let added = 0;
    for (let i = 0; i < chosen.length; i++) {
      const d = new Date(); d.setDate(d.getDate() + i + 1);
      const pad = (n) => String(n).padStart(2, "0");
      try {
        await hub.actions.createPost({
          caption: chosen[i].caption,
          platforms,
          contentType: "feed",
          date: `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`,
          time: "09:00",
          timezone: hub.data.workspace?.timezone || "America/New_York",
          status: "draft",
          saveAsDraft: true,
          media: []
        });
        added++;
      } catch (e) { hub.toast(`Draft ${i + 1}: ${e.message || "failed"}`); }
    }
    if (added) { hub.toast(`${added} drafts added to your calendar — review them in Plan`); setShowRepurpose(false); setPackPosts([]); }
  };

  const suggestAlt = async () => {
    const m = attached[activeMedia];
    if (!m || !m.id || altBusy) return;
    setAltBusy(true);
    try {
      const r = await hub.actions.aiAltText({ mediaId: m.id });
      if (r.altText) setAltLocal(activeMedia, r.altText);
    } catch (e) { hub.toast(e.message || "Alt text suggestion failed"); }
    finally { setAltBusy(false); }
  };

  const applyTone = async (tone) => {
    if (busyTone) return;
    if (!caption.trim()) { hub.toast("Write a few words first — AI will rewrite them"); return; }
    setBusyTone(tone);
    try {
      const next = await hub.actions.aiCaption(caption, tone, voice);
      if (next) setCaption(next);
    } catch (err) { console.error(err); hub.toast("AI caption failed"); }
    finally { setBusyTone(""); }
  };

  const setVariantCaption = (pf, patch) => setVariants((v) => ({ ...v, [pf]: { ...(v[pf] || {}), platform: pf, ...patch } }));
  const setFit = (pf, fit) => setFits((f) => ({ ...f, [pf]: fit }));

  const buildVariants = () => selected.reduce((acc, id) => {
    const cur = variants[id] || {};
    const useGlobalCaption = cur.useGlobalCaption !== false;
    acc[id] = {
      platform: id,
      useGlobalCaption,
      useGlobalMedia: true,
      caption: useGlobalCaption ? caption : (cur.caption || caption),
      contentType,
      media: attached.map((m) => m.id),
      mediaFit: fits[id] || CP_RECOMMENDED_FIT[id] || "original",
      mediaEdits: {},
      notes: ""
    };
    return acc;
  }, {});

  const submitPost = async (asDraft) => {
    if (submitting) return;
    if (!asDraft && !ready) { hub.toast(issues[0]); return; }
    setSubmitting(asDraft ? "draft" : "post");
    const payload = {
      caption: caption || "(draft)",
      platforms: selected.length ? selected : ["instagram"],
      date, time,
      timezone: workspace.timezone,
      contentType,
      media: attached.map((m) => m.id),
      variants: buildVariants(),
      firstComment,
      status: asDraft ? "draft" : "scheduled",
      requireApproval: approvalRequired,
      ...(asDraft ? { saveAsDraft: true } : {})
    };
    try {
      if (isEditing) {
        await hub.actions.updatePost(editPost.id, payload);
        try { sessionStorage.removeItem("clearcast-edit-post-id"); } catch (e) {}
        hub.setPage("calendar");
      } else {
        await hub.actions.createPost(payload);
        if (!asDraft) { setCaption(""); setAttached([]); setFirstComment(""); }
      }
    } catch (err) { console.error(err); hub.toast(err.message || (asDraft ? "Could not save draft" : "Could not schedule post")); }
    finally { setSubmitting(""); }
  };

  // Direct publish — create the post and push it live immediately (no schedule wait).
  const publishNow = async () => {
    if (submitting) return;
    if (!ready) { hub.toast(issues[0]); return; }
    setSubmitting("post");
    try {
      const post = await hub.actions.createPost({
        caption: caption || "(draft)",
        platforms: selected.length ? selected : ["instagram"],
        date, time,
        timezone: workspace.timezone,
        contentType,
        media: attached.map((m) => m.id),
        variants: buildVariants(),
        firstComment,
        status: "scheduled",
        requireApproval: false
      });
      const id = post && post.id;
      if (!id) throw new Error("Could not create the post");
      await hub.actions.publishPost(id);
      setCaption(""); setAttached([]); setFirstComment("");
      hub.setPage("published");
    } catch (err) { console.error(err); hub.toast(err.message || "Could not publish"); }
    finally { setSubmitting(""); }
  };

  const quietStart = rules.quietStart || "22:00";
  const quietEnd = rules.quietEnd || "06:00";
  const hashtagLimit = rules.hashtagLimit || 30;

  return (
    <div className="cp">
      <div className="cp-head">
        <div className="cp-head-l">
          <div className="cp-eyebrow"><Icon.Sparkles width="13" height="13" /> {isEditing ? "Edit post" : "New post"}</div>
          <h1>{isEditing ? <>Edit <em>post</em></> : <>Create a <em>post</em></>}</h1>
          <div className="cp-sub">Write it once, tailor it everywhere. We’ll preview each channel live and flag anything before it goes out.</div>
        </div>
        <div className="cp-head-r">
          <button className="btn btn-sm" onClick={() => hub.setPage("calendar")}><Icon.Calendar width="14" height="14" /> Calendar</button>
          {isEditing && <button className="btn btn-sm" onClick={() => { try { sessionStorage.removeItem("clearcast-edit-post-id"); } catch (e) {} hub.setPage("create"); window.location.reload(); }}><Icon.Refresh width="14" height="14" /> New</button>}
        </div>
      </div>

      <div className="cp-grid">
        <div className="cp-flow">
          {/* STEP 1 — Channels */}
          <section>
            <StepLabel num={1} done={selected.length > 0} hint={selected.length ? <span className="cp-channel-meta"><span className="cp-readydot" /> {selected.length} selected · {ready ? "all ready" : `${issues.length} to fix`}</span> : "Pick where this goes"}>Choose channels</StepLabel>
            {hasChannels ? (
              <div className="cp-channels">
                {channelList.map((id) => {
                  const on = platforms[id];
                  const issue = channelIssueFull(id);
                  // hard = channel unavailable (disconnected / plan / reauth / outage) → dim it.
                  // soft = connected & available, just needs the right format → keep it bright,
                  // flag with a warn dot + tooltip (YouTube needing a video lands here, NOT dimmed).
                  const hard = cpChannelHardIssue(intById[id], canPremium);
                  const upsell = CP_PREMIUM_CHANNELS.includes(id) && !canPremium;
                  const onClick = upsell
                    ? () => { hub.toast && hub.toast(`Posting to ${Platforms[id]?.name || id} is a Starter plan feature`); hub.setPage && hub.setPage("billing"); }
                    : () => toggle(id);
                  return (
                    <button key={id} className={`cp-channel ${on ? "on" : "off"}`} onClick={onClick}
                      title={issue ? `${Platforms[id]?.name || id} — ${issue}` : Platforms[id]?.name || id}
                      style={hard ? { opacity: 0.45 } : null}>
                      <PlatformIcon id={id} size={24} />
                      <span>{Platforms[id]?.name || id}{upsell ? " · Starter" : ""}</span>
                      {issue && <span className="cp-dot" style={{ background: "var(--warn)" }} title={issue} />}
                      {on && !issue && <span className="cp-dot" style={{ background: "var(--accent)" }} />}
                    </button>
                  );
                })}
                <button className="cp-channel-add" title="Connect more channels" onClick={() => hub.setPage("integrations")}><Icon.Plus width="16" height="16" /></button>
              </div>
            ) : (
              <div className="card" style={{ display: "flex", alignItems: "center", gap: 14, padding: 18 }}>
                <div style={{ width: 40, height: 40, borderRadius: 11, background: "var(--accent-soft)", color: "var(--accent-2)", display: "grid", placeItems: "center", flexShrink: 0 }}><Icon.Plug width="20" height="20" /></div>
                <div style={{ flex: 1, minWidth: 0 }}>
                  <div style={{ fontWeight: 600, fontSize: 14 }}>No channels connected yet</div>
                  <div className="muted" style={{ fontSize: 12.5 }}>Connect at least one social account to publish from ClearCast.</div>
                </div>
                <button className="btn btn-primary" onClick={() => hub.setPage("integrations")}><Icon.Plus width="14" height="14" /> Connect a channel</button>
              </div>
            )}
          </section>

          {/* STEP 2 — Compose */}
          <section>
            <StepLabel num={2} done={!!caption.trim() && attached.length > 0} hint="The heart of your post">Compose</StepLabel>
            <div className="card cp-compose">
              <div className="cp-compose-inner">
                <textarea className="cp-caption" value={caption} onChange={(e) => setCaption(e.target.value)} placeholder="Write your post… or tap AI Caption to draft one for you." />

                <div className="cp-media">
                  {attached.length === 0 ? (
                    <div className={`cp-dropzone ${dragging ? "drag" : ""}`} onClick={() => fileRef.current?.click()}
                      onDragOver={(e) => { e.preventDefault(); setDragging(true); }} onDragLeave={() => setDragging(false)}
                      onDrop={(e) => { e.preventDefault(); setDragging(false); const fs = Array.from(e.dataTransfer.files || []); if (fs.length) onFile({ target: { files: fs, value: "" } }); }}>
                      <div className="cp-dropzone-ico">{uploading ? <Icon.Refresh width="22" height="22" /> : <Icon.Image width="22" height="22" />}</div>
                      <div className="cp-dropzone-t">
                        <b>{uploading ? "Uploading…" : "Add photos or video"}</b>
                        <div className="cp-dropzone-sub">Drag & drop, upload, generate with AI, or pick from your library</div>
                      </div>
                      <div className="cp-dropzone-actions">
                        <button className="btn btn-sm" onClick={(e) => { e.stopPropagation(); fileRef.current?.click(); }}><Icon.Upload width="13" height="13" /> Upload</button>
                        <button className="btn btn-sm" onClick={(e) => { e.stopPropagation(); openAiPicker("image"); }}><Icon.Sparkles width="13" height="13" /> AI image</button>
                        <button className="btn btn-sm" onClick={(e) => { e.stopPropagation(); openAiPicker("video"); }}><Icon.Play width="13" height="13" /> AI video</button>
                        {mediaLibrary.length > 0 && <button className="btn btn-sm" onClick={(e) => { e.stopPropagation(); setShowLibrary((v) => !v); }}><Icon.Image width="13" height="13" /> Library</button>}
                      </div>
                    </div>
                  ) : (
                    <div className="cp-tray">
                      {attached.map((m, i) => (
                        <div key={m.id} className={`cp-thumb ${activeMedia === i ? "active" : ""}`} onClick={() => setActiveMedia(i)}>
                          {m.src && m.type === "image" ? <img className="cp-thumb-img" src={m.src} alt={m.name} />
                            : m.src && m.type === "video" ? <video className="cp-thumb-img" src={m.src} muted playsInline preload="metadata" style={{ objectFit: "cover", background: "#000" }} />
                            : <div className="cp-thumb-img" style={{ background: m.bg || "linear-gradient(135deg,var(--accent),var(--accent-2))" }} />}
                          {m.type === "video" && <span className="cp-thumb-badge"><Icon.Play width="9" height="9" /> {m.duration || "video"}</span>}
                          <button className="cp-thumb-x" onClick={(e) => { e.stopPropagation(); removeMedia(i); }}><Icon.X width="11" height="11" /></button>
                        </div>
                      ))}
                      <button className="cp-add-tile" onClick={() => fileRef.current?.click()}>
                        <Icon.Plus width="18" height="18" />
                        <span style={{ fontSize: 10.5, fontWeight: 600 }}>Add</span>
                      </button>
                      {mediaLibrary.length > 0 && (
                        <button className="cp-add-tile" onClick={() => setShowLibrary((v) => !v)}>
                          <Icon.Image width="18" height="18" />
                          <span style={{ fontSize: 10.5, fontWeight: 600 }}>Library</span>
                        </button>
                      )}
                    </div>
                  )}
                  <input ref={fileRef} type="file" multiple accept="image/*,image/heic,image/heif,.heic,.heif,video/*" onChange={onFile} style={{ display: "none" }} />

                  {attached.length > 0 && attached[activeMedia] && attached[activeMedia].type === "image" && (
                    <div className="cp-alt" style={{ marginTop: 10 }}>
                      <label className="field-label" style={{ display: "flex", alignItems: "center", gap: 6 }}>
                        <Icon.Eye width="13" height="13" /> Alt text for “{(attached[activeMedia].name || "image").slice(0, 28)}”
                        <span className="faint" style={{ fontWeight: 400 }}>— describe the image for screen readers</span>
                        <button className="btn btn-sm" disabled={altBusy} onClick={(e) => { e.preventDefault(); suggestAlt(); }} style={{ marginLeft: "auto" }}><Icon.Sparkles width="12" height="12" /> {altBusy ? "Looking…" : "Suggest"}</button>
                      </label>
                      <textarea className="textarea" rows={2} maxLength={1000}
                        value={attached[activeMedia].altText || ""}
                        onChange={(e) => setAltLocal(activeMedia, e.target.value)}
                        onBlur={() => saveAlt(activeMedia)}
                        placeholder="e.g. A linen tote bag on a wooden table beside dried flowers"
                        style={{ minHeight: 52 }} />
                      <div className="faint" style={{ fontSize: 11, marginTop: 2 }}>Sent to Mastodon, X and Facebook. {(attached[activeMedia].altText || "").length}/1000</div>
                    </div>
                  )}

                  {showLibrary && mediaLibrary.length > 0 && (
                    <div className="cp-tones" style={{ flexWrap: "wrap" }}>
                      <span className="cp-tone-label">Your library</span>
                      {mediaLibrary.slice(0, 12).map((m) => (
                        <button key={m.id} className="cp-tone" onClick={() => { addAttached(m); setShowLibrary(false); }} title={m.name}>
                          {m.name.length > 16 ? m.name.slice(0, 15) + "…" : m.name}
                        </button>
                      ))}
                    </div>
                  )}
                </div>
              </div>

              {showHashtags && (
                <div className="cp-tones">
                  <span className="cp-tone-label">Suggested</span>
                  {hashtagSugs.map((t) => <button key={t} className="cp-tone" onClick={() => appendCaption(t)}>{t}</button>)}
                </div>
              )}

              {showTones && (
                <div className="cp-tones">
                  <span className="cp-tone-label"><Icon.Sparkles width="12" height="12" style={{ verticalAlign: "-2px" }} /> Rewrite</span>
                  {CP_TONES.map((t) => <button key={t} className="cp-tone" disabled={!!busyTone} onClick={() => applyTone(t)}>{busyTone === t ? "…" : t}</button>)}
                </div>
              )}

              {showFirstComment && (
                <div className="cp-firstcomment">
                  <label className="field-label">First comment <span className="faint" style={{ fontWeight: 400 }}>— great for hashtags or links. Posted as a real reply on X &amp; Mastodon; added to the caption on Facebook, Instagram &amp; YouTube.</span></label>
                  <input className="input" value={firstComment} onChange={(e) => setFirstComment(e.target.value)} placeholder="e.g. #summerstyle #linenlove · link in bio" />
                </div>
              )}

              {showScript && (
                <div className="card" style={{ marginTop: 10, padding: 14 }}>
                  <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8 }}>
                    <label className="field-label" style={{ margin: 0 }}><Icon.Play width="13" height="13" style={{ verticalAlign: "-2px" }} /> 30-second video script <span className="faint" style={{ fontWeight: 400 }}>— from your caption</span></label>
                    <button className="btn btn-sm btn-primary" disabled={scriptBusy} onClick={runScript}>{scriptBusy ? "Writing…" : "Generate"}</button>
                  </div>
                  {scriptResult && (<>
                    <pre style={{ whiteSpace: "pre-wrap", fontSize: 12.5, background: "var(--bg-2)", border: "1px solid var(--border)", borderRadius: 10, padding: 12, marginTop: 10, maxHeight: 220, overflow: "auto", fontFamily: "inherit" }}>{scriptResult}</pre>
                    <div style={{ display: "flex", gap: 8, marginTop: 8 }}>
                      <button className="btn btn-sm" onClick={() => { navigator.clipboard && navigator.clipboard.writeText(scriptResult); hub.toast("Script copied"); }}>Copy</button>
                      <button className="btn btn-sm" onClick={() => setCaption(scriptResult)}>Use as caption</button>
                    </div>
                  </>)}
                </div>
              )}

              {showVerse && (
                <div className="card" style={{ marginTop: 10, padding: 14 }}>
                  <label className="field-label"><Icon.Bookmark width="13" height="13" style={{ verticalAlign: "-2px" }} /> Quote graphic <span className="faint" style={{ fontWeight: 400 }}>— brand-styled card, attached to this post</span></label>
                  <div className="cp-tones" style={{ marginBottom: 8 }}>
                    <button className={`cp-tone ${quoteMode === "custom" ? "on" : ""}`} onClick={() => setQuoteMode("custom")}>Custom quote</button>
                    <button className={`cp-tone ${quoteMode === "verse" ? "on" : ""}`} onClick={() => setQuoteMode("verse")}>Bible verse lookup</button>
                  </div>
                  {quoteMode === "custom" ? (
                    <>
                      <textarea className="textarea" value={quoteText} onChange={(e) => setQuoteText(e.target.value)} placeholder="The quote or line you want on the card…" style={{ minHeight: 64, marginBottom: 8 }} />
                      <div style={{ display: "flex", gap: 8 }}>
                        <input className="input" value={quoteAttribution} onChange={(e) => setQuoteAttribution(e.target.value)} placeholder="Attribution (optional) — e.g. Jane Smith, CEO" />
                        <button className="btn btn-primary" disabled={verseBusy} onClick={makeQuoteGraphic} style={{ flex: "none" }}>{verseBusy ? "Creating…" : "Create"}</button>
                      </div>
                    </>
                  ) : (
                    <>
                      <div style={{ display: "flex", gap: 8 }}>
                        <input className="input" value={verseRef} onChange={(e) => setVerseRef(e.target.value)} placeholder="e.g. John 3:16 or Psalm 23:1-3" onKeyDown={(e) => e.key === "Enter" && makeQuoteGraphic()} />
                        <button className="btn btn-primary" disabled={verseBusy} onClick={makeQuoteGraphic} style={{ flex: "none" }}>{verseBusy ? "Creating…" : "Create"}</button>
                      </div>
                      <div className="faint" style={{ fontSize: 11, marginTop: 6 }}>Verse text from the public-domain World English Bible.</div>
                    </>
                  )}
                  <div className="faint" style={{ fontSize: 11, marginTop: 6 }}>Alt text is added automatically.</div>
                </div>
              )}

              {showRepurpose && (
                <div className="card" style={{ marginTop: 10, padding: 14 }}>
                  <label className="field-label"><Icon.Star width="13" height="13" style={{ verticalAlign: "-2px" }} /> Repurpose into a week of posts <span className="faint" style={{ fontWeight: 400 }}>— blog post, podcast or video transcript, talk, newsletter…</span></label>
                  <input className="input" value={packTitle} onChange={(e) => setPackTitle(e.target.value)} placeholder="Title (optional)" style={{ marginBottom: 8 }} />
                  <textarea className="textarea" value={packText} onChange={(e) => setPackText(e.target.value)} placeholder="Paste the long-form content here — we'll turn it into a week of posts…" style={{ minHeight: 110 }} />
                  <div style={{ display: "flex", justifyContent: "flex-end", marginTop: 8 }}>
                    <button className="btn btn-primary btn-sm" disabled={packBusy || packText.trim().length < 80} onClick={runRepurpose}>{packBusy ? "Generating…" : "Generate posts"}</button>
                  </div>
                  {packPosts.length > 0 && (<>
                    <div style={{ display: "grid", gap: 8, marginTop: 10 }}>
                      {packPosts.map((p, i) => (
                        <label key={i} style={{ display: "flex", gap: 10, alignItems: "flex-start", background: "var(--bg-2)", border: "1px solid var(--border)", borderRadius: 10, padding: 10, cursor: "pointer" }}>
                          <input type="checkbox" checked={p.selected} onChange={() => setPackPosts((list) => list.map((q, j) => j === i ? { ...q, selected: !q.selected } : q))} style={{ marginTop: 3 }} />
                          <span style={{ flex: 1 }}>
                            <span className="cp-pill" style={{ fontSize: 10, textTransform: "uppercase", letterSpacing: "0.04em", color: "var(--accent-2)" }}>{p.kind}</span>
                            <span style={{ display: "block", fontSize: 12.5, whiteSpace: "pre-wrap", marginTop: 4 }}>{p.caption}</span>
                          </span>
                        </label>
                      ))}
                    </div>
                    <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginTop: 10 }}>
                      <span className="faint" style={{ fontSize: 11.5 }}>Added as drafts, one per day starting tomorrow at 9:00 — review in Plan before scheduling.</span>
                      <button className="btn btn-primary btn-sm" onClick={addPackDrafts}>Add {packPosts.filter((p) => p.selected).length} drafts</button>
                    </div>
                  </>)}
                </div>
              )}

              <div className="cp-toolbar">
                <button className="cp-tool accent" onClick={() => setShowTones((v) => !v)}><Icon.Sparkles className="ico" /> AI Caption</button>
                <span className="cp-toolbar-sep" />
                <button className={`cp-tool ${showHashtags ? "on" : ""}`} onClick={() => setShowHashtags((v) => !v)} title="Hashtags"><Icon.Hash className="ico" /></button>
                <button className={`cp-tool ${showFirstComment ? "on" : ""}`} onClick={() => setShowFirstComment((v) => !v)} title="First comment"><Icon.Message className="ico" /></button>
                <button className={`cp-tool ${showScript ? "on" : ""}`} onClick={() => { setShowScript((v) => !v); setShowVerse(false); setShowRepurpose(false); }} title="Video script"><Icon.Play className="ico" /></button>
                <button className={`cp-tool ${showVerse ? "on" : ""}`} onClick={() => { setShowVerse((v) => !v); setShowScript(false); setShowRepurpose(false); }} title="Quote graphic"><Icon.Bookmark className="ico" /></button>
                <button className={`cp-tool ${showRepurpose ? "on" : ""}`} onClick={() => { setShowRepurpose((v) => !v); setShowScript(false); setShowVerse(false); }} title="Repurpose into a week of posts"><Icon.Star className="ico" /></button>
                <span className={`cp-count ${overLimit ? "over" : nearLimit ? "warn" : ""}`}>{caption.length.toLocaleString()} / {minLimit.toLocaleString()}{tightestPf ? ` · ${Platforms[tightestPf]?.name}` : ""}</span>
              </div>
            </div>

            {/* Advanced: per-platform captions */}
            <div className={`cp-disc ${discVariants ? "open" : ""}`} style={{ marginTop: 12 }}>
              <div className="cp-disc-head" role="button" tabIndex={0} onClick={() => setDiscVariants((v) => !v)}>
                <span className="cp-disc-ico"><Icon.Layers width="16" height="16" /></span>
                <span className="cp-disc-t">
                  <b>Customize caption per channel</b>
                  <small>{variantCount ? `${variantCount} channel${variantCount > 1 ? "s" : ""} customized` : "All channels use the caption above"}</small>
                </span>
                <span className="cp-disc-r"><Icon.ChevronDown className="cp-disc-caret" width="18" height="18" /></span>
              </div>
              {discVariants && (
                <div className="cp-disc-body">
                  <div className="cp-pfbar">
                    {selected.map((id) => {
                      const custom = variants[id]?.useGlobalCaption === false && variants[id]?.caption?.trim();
                      return (
                        <button key={id} className={`cp-pftab ${activeVariantPf === id ? "active" : ""}`} onClick={() => setActiveVariantPf(id)}>
                          <PlatformIcon id={id} size={16} /> {Platforms[id]?.name}{custom && <span className="cp-custom-dot" />}
                        </button>
                      );
                    })}
                  </div>
                  <label className="cp-toggle-row">
                    <input type="checkbox" checked={variants[activeVariantPf]?.useGlobalCaption !== false} onChange={(e) => setVariantCaption(activeVariantPf, { useGlobalCaption: e.target.checked })} />
                    Use the shared caption for {Platforms[activeVariantPf]?.name}
                  </label>
                  <textarea className="textarea" disabled={variants[activeVariantPf]?.useGlobalCaption !== false}
                    value={variants[activeVariantPf]?.useGlobalCaption === false ? (variants[activeVariantPf]?.caption || "") : caption}
                    onChange={(e) => setVariantCaption(activeVariantPf, { useGlobalCaption: false, caption: e.target.value })}
                    placeholder={`Custom ${Platforms[activeVariantPf]?.name} caption`}
                    style={{ minHeight: 88, opacity: variants[activeVariantPf]?.useGlobalCaption !== false ? 0.55 : 1 }} />
                  <div style={{ display: "flex", justifyContent: "space-between", marginTop: 8, fontSize: 12 }}>
                    <span className="muted">{variants[activeVariantPf]?.useGlobalCaption === false ? "This channel uses its own caption" : "Following the shared caption"}</span>
                    <span className="faint mono">{(variants[activeVariantPf]?.useGlobalCaption === false ? (variants[activeVariantPf]?.caption || "") : caption).length.toLocaleString()} / {(CP_LIMITS[activeVariantPf] || 2200).toLocaleString()}</span>
                  </div>
                </div>
              )}
            </div>

            {/* Advanced: per-platform crop & fit */}
            <div className={`cp-disc ${discMedia ? "open" : ""}`} style={{ marginTop: 12 }}>
              <div className="cp-disc-head" role="button" tabIndex={0} onClick={() => setDiscMedia((v) => !v)}>
                <span className="cp-disc-ico"><Icon.Crop width="16" height="16" /></span>
                <span className="cp-disc-t">
                  <b>Crop & fit per channel</b>
                  <small>{fitCount ? `${fitCount} channel${fitCount > 1 ? "s" : ""} adjusted` : "Using each channel’s recommended ratio"}</small>
                </span>
                <span className="cp-disc-r"><Icon.ChevronDown className="cp-disc-caret" width="18" height="18" /></span>
              </div>
              {discMedia && (
                <div className="cp-disc-body">
                  <div className="cp-pfbar">
                    {selected.map((id) => (
                      <button key={id} className={`cp-pftab ${activeVariantPf === id ? "active" : ""}`} onClick={() => setActiveVariantPf(id)}>
                        <PlatformIcon id={id} size={16} /> {Platforms[id]?.name}{fits[id] && fits[id] !== CP_RECOMMENDED_FIT[id] && <span className="cp-custom-dot" />}
                      </button>
                    ))}
                  </div>
                  <div className="cp-fits">
                    {CP_FIT_OPTIONS.map((f) => (
                      <button key={f.id} className={`cp-fit ${activeFit === f.id ? "active" : ""}`} onClick={() => setFit(activeVariantPf, f.id)}>
                        <span className="cp-fit-shape" style={{ width: f.w, height: f.h }} />{f.label}<small>{f.ratio}</small>
                      </button>
                    ))}
                  </div>
                  <div style={{ marginTop: 12, fontSize: 12, display: "flex", justifyContent: "space-between" }}>
                    <span className="muted">Recommended for {Platforms[activeVariantPf]?.name}</span>
                    <b>{CP_FIT_OPTIONS.find((f) => f.id === CP_RECOMMENDED_FIT[activeVariantPf])?.label}</b>
                  </div>
                </div>
              )}
            </div>

            {/* Inclusivity & disclosure — live a11y + FTC/AI-label checks */}
            {(() => {
              const checks = cpInclusivityChecks({ caption, attached, hasAiMedia: attached.some((m) => m.aiGenerated) });
              const warns = checks.filter((c) => c.level === "warn").length;
              return (
                <div className={`cp-disc ${discA11y ? "open" : ""}`} style={{ marginTop: 12 }}>
                  <div className="cp-disc-head" role="button" tabIndex={0} onClick={() => setDiscA11y((v) => !v)}>
                    <span className="cp-disc-ico"><Icon.Eye width="16" height="16" /></span>
                    <span className="cp-disc-t">
                      <b>Inclusivity &amp; disclosure</b>
                      <small>{checks.length ? `${checks.length} suggestion${checks.length > 1 ? "s" : ""}${warns ? ` · ${warns} to review` : ""}` : "Looks good — accessible & properly disclosed"}</small>
                    </span>
                    <span className="cp-disc-r">
                      {checks.length > 0 && <span className="cp-pill" style={{ background: warns ? "var(--danger)" : "var(--accent)", color: "#fff", marginRight: 8 }}>{checks.length}</span>}
                      <Icon.ChevronDown className="cp-disc-caret" width="18" height="18" />
                    </span>
                  </div>
                  {discA11y && (
                    <div className="cp-disc-body">
                      {checks.length === 0 ? (
                        <div className="muted" style={{ fontSize: 13, display: "flex", alignItems: "center", gap: 8 }}>
                          <Icon.Check width="15" height="15" /> Nothing to flag — alt text present, no shouting, and disclosures look right.
                        </div>
                      ) : checks.map((c) => (
                        <div key={c.id} className="cp-a11y-row" style={{ display: "flex", gap: 10, alignItems: "flex-start", padding: "9px 0", borderBottom: "1px solid var(--border)" }}>
                          <span style={{ color: c.level === "warn" ? "var(--danger)" : "var(--accent)", marginTop: 1 }}>
                            {c.level === "warn" ? <Icon.Shield width="15" height="15" /> : <Icon.Sparkles width="15" height="15" />}
                          </span>
                          <div style={{ flex: 1 }}>
                            <div style={{ fontWeight: 600, fontSize: 13 }}>{c.title}</div>
                            <div className="muted" style={{ fontSize: 12.5 }}>{c.detail}</div>
                          </div>
                          {c.id === "alt" && <button className="btn btn-sm" onClick={() => { setDiscA11y(false); const idx = attached.findIndex((m) => m.type === "image" && !String(m.altText || "").trim()); if (idx >= 0) setActiveMedia(idx); }}>Add alt text</button>}
                          {c.addTag && <button className="btn btn-sm" onClick={() => setCaption((cur) => (cur.trim() + " " + c.addTag).trim())}>Add {c.addTag.trim()}</button>}
                        </div>
                      ))}
                      <div className="faint" style={{ fontSize: 11, marginTop: 8 }}>These checks run locally as you type. Alt text is delivered to platforms that support it.</div>
                    </div>
                  )}
                </div>
              );
            })()}
          </section>

          {/* STEP 3 — Schedule */}
          <section>
            <StepLabel num={3} done hint="When & how it publishes">Schedule</StepLabel>
            <div className="card" style={{ padding: 18 }}>
              <div className="cp-sched-grid">
                <div className="cp-field">
                  <label className="field-label">Date</label>
                  <input className="input" type="date" value={date} onChange={(e) => setDate(e.target.value)} />
                </div>
                <div className="cp-field">
                  <label className="field-label">Time</label>
                  <input className="input" type="time" value={time} onChange={(e) => setTime(e.target.value)} />
                </div>
                <div className="cp-field">
                  <label className="field-label">Time zone</label>
                  <input className="input" value={workspace.timezone || "America/New_York"} readOnly />
                </div>
              </div>

              {(() => {
                const bt = cpBestTimes(data);
                return (
                  <div className="cp-besttimes">
                    <span className="faint" style={{ fontSize: 12, display: "inline-flex", alignItems: "center", gap: 6 }}
                      title={bt.personalized ? `Based on the ${bt.count} posts you've actually published — the hours your posts have gone out most. Builds smarter as you publish more.` : "General high-engagement windows. Your personalized best-times appear once you've published a few posts."}>
                      <Icon.Star width="13" height="13" /> {bt.personalized ? "Your best times" : "Suggested times"}
                    </span>
                    {bt.times.map((t) => (
                      <button key={t} className={`cp-besttime ${time === t ? "active" : ""}`} onClick={() => setTime(t)}>
                        {t}
                      </button>
                    ))}
                    {bt.personalized && <span className="faint" style={{ fontSize: 11 }}>from your history</span>}
                  </div>
                );
              })()}

              <div className="divider" />

              <label className="field-label">Format</label>
              <div className="cp-types">
                {[["feed", "Feed", Icon.Calendar], ["reel", "Reel", Icon.Play], ["story", "Story", Icon.Eye], ["short", "Short", Icon.Play]].map(([id, label, Ico]) => (
                  <button key={id} className={`cp-type ${contentType === id ? "active" : ""}`} onClick={() => { setContentType(id); setPreviewFormat(id === "feed" ? "feed" : id === "story" ? "story" : "reel"); }}>
                    <Ico className="ico" /> {label}
                  </button>
                ))}
              </div>
            </div>
          </section>

          {/* Workspace rules */}
          <div className={`cp-disc ${discRules ? "open" : ""}`}>
            <div className="cp-disc-head" role="button" tabIndex={0} onClick={() => setDiscRules((v) => !v)}>
              <span className="cp-disc-ico"><Icon.Shield width="16" height="16" /></span>
              <span className="cp-disc-t">
                <b>Workspace rules</b>
                <small>Approval {approvalRequired ? "required" : "off"} · {hashtagCount}/{hashtagLimit} hashtags · quiet hours {quietStart}–{quietEnd}</small>
              </span>
              <span className="cp-disc-r">
                <span className={`pill ${issues.length ? "warn" : "green"}`}>{issues.length ? `${issues.length} to review` : "All clear"}</span>
                <Icon.ChevronDown className="cp-disc-caret" width="18" height="18" />
              </span>
            </div>
            {discRules && (
              <div className="cp-disc-body">
                <div className="cp-rules" style={{ border: "none", padding: "12px 0 0", background: "transparent" }}>
                  <span className="cp-rule"><Icon.Check className="ico" /> Approval route <b>{approvalRequired ? "Required" : "Direct"}</b></span>
                  <span className="cp-rule-sep" />
                  <span className="cp-rule"><Icon.Hash className="ico" /> Hashtags <b>{hashtagCount} / {hashtagLimit}</b></span>
                  <span className="cp-rule-sep" />
                  <span className="cp-rule"><Icon.Clock className="ico" /> Quiet hours <b>{quietStart}–{quietEnd}</b></span>
                  <span className="cp-rule-sep" />
                  <span className="cp-rule"><Icon.Shield className="ico" /> Profanity filter <b>{rules.profanityFilter ? "On" : "Off"}</b></span>
                  <span className="cp-rule-sep" />
                  <span className="cp-rule"><Icon.Layers className="ico" /> Duplicate check <b>{rules.duplicateDetection ? "On" : "Off"}</b></span>
                </div>
              </div>
            )}
          </div>

          {/* Sticky action bar */}
          <div className="cp-actionbar">
            <div className="cp-ab-ready">
              <span className={`cp-ab-ready-ico ${ready ? "" : "warn"}`}>{ready ? <Icon.Check width="17" height="17" /> : <Icon.Bell width="17" height="17" />}</span>
              <div className="cp-ab-ready-t">
                <b>{ready ? `${selected.length} channel${selected.length > 1 ? "s" : ""} ready` : issues[0]}</b>
                <small>{ready ? `Publishing ${contentType} · ${date} at ${time}` : `${issues.length} item${issues.length > 1 ? "s" : ""} to resolve`}</small>
              </div>
            </div>
            <div className="cp-ab-actions">
              <button className="btn" disabled={!!submitting} onClick={() => submitPost(true)}>{submitting === "draft" ? "Saving…" : "Save draft"}</button>
              <span className={`cp-approval ${approvalRequired ? "req" : "direct"}`} title="Set by your workspace's approval policy — change it in Settings → Workspace.">
                <Icon.Shield width="14" height="14" /> {approvalRequired ? "Approval required" : "Direct publish"}
              </span>
              {isEditing ? (
                <button className="btn btn-primary cp-btn-publish" disabled={!ready || !!submitting} onClick={() => submitPost(false)}>
                  {submitting === "post" ? "Working…" : "Save changes"}
                </button>
              ) : approvalRequired ? (
                <button className="btn btn-primary cp-btn-publish" disabled={!ready || !!submitting} onClick={() => submitPost(false)}>
                  {submitting === "post" ? "Working…" : "Submit for approval"}
                </button>
              ) : (
                <React.Fragment>
                  <button className="btn" disabled={!ready || !!submitting} onClick={() => submitPost(false)} title="Add to the schedule for the date & time above">
                    {submitting === "post" ? "…" : "Schedule"}
                  </button>
                  <button className="btn btn-primary cp-btn-publish" disabled={!ready || !!submitting} onClick={publishNow} title="Publish to the selected channels right now">
                    {submitting === "post" ? "Publishing…" : "Publish now"}
                  </button>
                </React.Fragment>
              )}
            </div>
          </div>
        </div>

        {/* RIGHT — preview */}
        <PreviewColumn
          selectedPlatforms={selected}
          activePreviewPlatform={activePreviewPlatform}
          setActivePreviewPlatform={setActivePreviewPlatform}
          previewFormat={previewFormat}
          setPreviewFormat={setPreviewFormat}
          previewDevice={previewDevice}
          setPreviewDevice={setPreviewDevice}
          caption={previewCaption}
          media={previewMediaObj}
          fit={previewFit}
          captionSource={usesGlobal ? "Shared" : "Custom"}
          handle={handle}
          avatarLetter={avatarLetter}
        />
      </div>

      {aiPicker && (
        <div className="ai-picker-scrim" onClick={() => setAiPicker(null)}>
          <div className="ai-picker" onClick={(e) => e.stopPropagation()}>
            <div className="ai-picker-head">
              <span className="ai-picker-title">Generate {aiPicker === "image" ? "image" : "video"}</span>
              <button className="ai-picker-close" onClick={() => setAiPicker(null)}><Icon.X width="16" height="16" /></button>
            </div>
            <div className="ai-picker-models">
              {(aiPicker === "image" ? AI_IMAGE_MODELS : AI_VIDEO_MODELS).map((m) => (
                <div key={m.id} className={`ai-picker-model${aiPickerModel?.id === m.id ? " selected" : ""}`} onClick={() => setAiPickerModel(m)}>
                  <div className="ai-picker-model-top">
                    <span className="ai-picker-model-name">{m.label}</span>
                    <span className="ai-picker-model-badge">{m.badge}</span>
                  </div>
                  <div className="ai-picker-model-desc">{m.desc}</div>
                  <div className="ai-picker-model-cost">{m.cost} / generation</div>
                </div>
              ))}
            </div>
            <textarea
              className="ai-picker-prompt"
              value={aiPickerPrompt}
              onChange={(e) => setAiPickerPrompt(e.target.value)}
              placeholder={aiPicker === "image" ? "Describe the image…" : "Describe the video (e.g. 'Sunrise over a church steeple, cinematic, 9:16')…"}
              rows={3}
            />
            {aiPickerModel && (
              <div style={{ fontSize: 12, color: "var(--text-3)", marginBottom: 10, display: "flex", alignItems: "center", gap: 5 }}>
                <Icon.Sparkles width="11" height="11" />
                Estimated cost: <strong style={{ color: "var(--text-2)" }}>{aiPickerModel.cost}</strong> · charged to your Fal.ai account
              </div>
            )}
            <div className="ai-picker-actions">
              <button className="btn btn-ghost" onClick={() => setAiPicker(null)}>Cancel</button>
              <button className="btn btn-primary" disabled={!aiPickerModel || !aiPickerPrompt.trim() || uploading} onClick={runAiGenerate}>
                <Icon.Sparkles width="13" height="13" /> Generate {aiPickerModel ? `· ${aiPickerModel.cost}` : ""}
              </button>
            </div>
          </div>
        </div>
      )}
    </div>
  );
}

window.CreatePostPage = CreatePostPage;
