// Vanday DAM — publish composer + crops grid + published view

// Tiny outlined aspect-ratio glyph used on the publish crop chips. Draws a
// max-15px rectangle in the chip's ratio so the shape reads at a glance.
function CropGlyph({ w, h }) {
  const max = 15;
  let bw, bh;
  if (w >= h) { bw = max; bh = Math.round((max * h) / w); }
  else { bh = max; bw = Math.round((max * w) / h); }
  return <span className="crop-glyph" style={{ width: bw, height: Math.max(bh, 5) }} />;
}

function CropsGrid({ asset, onOpenVariant, highlightId }) {
  const parent = getParent(asset);
  const siblings = parent ? VARIANTS.filter((v) => v.parentId === parent.id) : [];

  return (
    <div className="crops-grid">
      {siblings.map((v) => {
        const [rw, rh] = v.ratio.split(":").map(parseFloat);
        const aspect = `${rw} / ${rh}`;
        const isHere = highlightId === v.id;
        return (
          <div
            className="crop-card"
            key={v.id}
            style={{
              cursor: onOpenVariant ? "pointer" : "default",
              borderColor: isHere ? "var(--accent)" : undefined,
              boxShadow: isHere ? "0 0 0 2px var(--accent-soft)" : undefined,
            }}
            onClick={() => onOpenVariant && onOpenVariant(v)}
          >
            <div className="crop-thumb" style={{ aspectRatio: aspect }}>
              <span className="ratio-tag">{v.ratio}</span>
              <img src={v.url} alt={`${v.name} ${v.ratio}`} />
            </div>
            <div className="crop-meta">
              <div style={{ minWidth: 0 }}>
                <div className="crop-name">{v.ratioName}</div>
                <div className="crop-size">{v.w} × {v.h}</div>
              </div>
              <button
                className="btn ghost sm"
                title="Download crop"
                onClick={(e) => e.stopPropagation()}
              >
                <IcDownload size={13} />
              </button>
            </div>
          </div>
        );
      })}
    </div>
  );
}

// Mark a publication as "deleted on the platform" — keeps the record in
// Vanday so the user's history is preserved, but shows the post as no
// longer live (grayed-out, no "View" link). Use case: the user manually
// took the post down on the social platform.
function MarkDeletedButton({ asset, pub }) {
  const [busy, setBusy] = React.useState(false);
  if (pub.status !== "live") return null;

  const click = async () => {
    const platform = (window.SITES || []).find((s) => s.id === pub.site)?.name || pub.site;
    const ok = window.confirm(
      `Mark this ${platform} post as deleted in Vanday?\n\n` +
      `Use this if you've already taken the post down on ${platform}. ` +
      `The Vanday record stays as a history entry.`,
    );
    if (!ok) return;
    setBusy(true);
    try {
      const r = await fetch(
        `/api/assets/${encodeURIComponent(asset.id)}/publications`,
        {
          method: "PATCH",
          headers: {
            "Content-Type": "application/json",
            ...(window.Clerk?.session ? { Authorization: `Bearer ${await window.Clerk.session.getToken()}` } : {}),
          },
          body: JSON.stringify({
            site: pub.site,
            platformPostId: pub.platformPostId || null,
            postedAt: pub.postedAt || null,
            status: "deleted",
          }),
        },
      );
      if (!r.ok) {
        const body = await r.json().catch(() => ({}));
        window.alert("Couldn't update: " + (body.error || `HTTP ${r.status}`));
        return;
      }
      const { publications } = await r.json();
      asset.publications = publications || [];
      try { VandayServer.bumpRev && VandayServer.bumpRev(); } catch {}
    } catch {
      window.alert("Couldn't reach the server. Try again in a moment.");
    } finally { setBusy(false); }
  };

  return (
    <button
      type="button"
      onClick={click}
      disabled={busy}
      title="Mark this post as deleted on the platform (keeps the Vanday record as history)"
      style={{
        border: "1px solid var(--border)", background: "var(--surface)",
        color: "var(--text-muted)", borderRadius: 6,
        padding: "4px 8px", fontSize: 11.5,
        cursor: busy ? "wait" : "pointer", flexShrink: 0,
      }}
    >
      I deleted it
    </button>
  );
}

// Removes a single publication entry from Vanday's record. Does NOT take the
// post down on the actual social platform — Upload-Post has no API for that.
// We make that distinction explicit in the confirmation prompt.
function RemovePubButton({ asset, pub }) {
  const [busy, setBusy] = React.useState(false);

  const remove = async () => {
    const platform = (window.SITES || []).find((s) => s.id === pub.site)?.name || pub.site;
    const ok = window.confirm(
      `Remove this ${platform} post from Vanday?\n\n` +
      `This only removes Vanday's record. The actual post will stay live on ${platform} ` +
      `until you delete it there.`,
    );
    if (!ok) return;
    setBusy(true);
    try {
      const r = await fetch(
        `/api/assets/${encodeURIComponent(asset.id)}/publications`,
        {
          method: "DELETE",
          headers: {
            "Content-Type": "application/json",
            ...(window.Clerk?.session ? { Authorization: `Bearer ${await window.Clerk.session.getToken()}` } : {}),
          },
          body: JSON.stringify({
            site: pub.site,
            platformPostId: pub.platformPostId || null,
            postedAt: pub.postedAt || null,
          }),
        },
      );
      if (!r.ok) {
        const body = await r.json().catch(() => ({}));
        window.alert("Couldn't remove: " + (body.error || `HTTP ${r.status}`));
        return;
      }
      const { remaining } = await r.json();
      asset.publications = remaining || [];
      try { VandayServer.bumpRev && VandayServer.bumpRev(); } catch {}
    } catch (err) {
      window.alert("Couldn't reach the server. Try again in a moment.");
    } finally {
      setBusy(false);
    }
  };

  return (
    <button
      type="button"
      onClick={remove}
      disabled={busy}
      title="Remove from Vanday (post stays live on the platform)"
      style={{
        border: "1px solid var(--border)", background: "var(--surface)",
        color: "var(--text-muted)", borderRadius: 6,
        padding: "4px 8px", fontSize: 11.5,
        cursor: busy ? "wait" : "pointer", flexShrink: 0,
      }}
    >
      <IcTrash size={11} /> Remove
    </button>
  );
}

function PublicationList({ asset }) {
  VandayServer.useServerRev(); // re-render when publications change
  const pubs = asset.publications || [];
  if (pubs.length === 0) {
    return (
      <div style={{ fontSize: 12.5, color: "var(--text-faint)", padding: "6px 0" }}>
        Not yet published. Use Publish to schedule a post.
      </div>
    );
  }
  return (
    <div>
      {pubs.map((p, i) => {
        const site = SITES.find((s) => s.id === p.site) || { id: p.site, name: p.site || "Unknown", color: "var(--text-faint)" };
        const isFailed = p.status === "failed";
        const isScheduled = p.status === "scheduled";
        const isDeleted = p.status === "deleted";
        const rowStyle = isFailed
          ? { borderColor: "oklch(0.85 0.07 24)", background: "oklch(0.97 0.02 24)" }
          : isDeleted
            ? { opacity: 0.6 }
            : undefined;
        return (
          <div className="pub-item" key={i} style={rowStyle}>
            <SiteMark site={site} size={20} />
            <div style={{ minWidth: 0 }}>
              <div className="pub-name" style={isDeleted ? { textDecoration: "line-through" } : undefined}>
                {site.name}
              </div>
              <div className="pub-when" title={isFailed ? p.error : undefined}>
                {isFailed ? (p.error || "Failed to post")
                  : isDeleted ? `Deleted on platform · was posted ${p.postedAt}`
                  : p.postedAt}
              </div>
            </div>
            <span
              className={`pub-status ${isScheduled ? "scheduled" : ""}`}
              style={
                isFailed ? { background: "oklch(0.55 0.18 24)", color: "white" }
                : isDeleted ? { background: "var(--text-faint)", color: "white" }
                : undefined
              }
            >
              {isFailed ? <IcInfo size={10} />
                : isScheduled ? <IcClock size={10} />
                : isDeleted ? <IcTrash size={10} />
                : <IcCheck size={10} />}
              {isFailed ? "Failed" : isScheduled ? "Scheduled" : isDeleted ? "Deleted" : "Live"}
            </span>
            {p.url && !isDeleted && (
              <a
                className="pub-link"
                href={p.url}
                target="_blank"
                rel="noreferrer"
                title={`Open this post on ${site.name} in a new tab`}
              >
                View <IcExternal size={11} />
              </a>
            )}
            <MarkDeletedButton asset={asset} pub={p} />
            <RemovePubButton asset={asset} pub={p} />
          </div>
        );
      })}
    </div>
  );
}

// Build a believable "live post" URL per site. These don't reach a real
// server — they just match each platform's URL shape so the "Open post"
// link inside the DAM looks like a real outbound link the user can verify.
function fakePublishUrl(siteId) {
  const slug = () => Math.random().toString(36).slice(2, 12);
  switch (siteId) {
    case "instagram": return `https://www.instagram.com/p/${slug().toUpperCase()}/`;
    case "facebook":  return `https://www.facebook.com/share/p/${slug()}/`;
    case "linkedin":  return `https://www.linkedin.com/feed/update/urn:li:share:${Date.now()}${Math.floor(Math.random() * 1000)}/`;
    case "x":         return `https://x.com/vanday/status/${Date.now()}`;
    case "tiktok":    return `https://www.tiktok.com/@vanday/video/${Date.now()}`;
    case "pinterest": return `https://www.pinterest.com/pin/${slug()}/`;
    default:          return `https://vanday.com/p/${slug()}`;
  }
}

// Platforms we'd pre-select for the user once we know they're connected,
// in priority order. We never auto-select an account that isn't connected.
const PREFERRED_DEFAULTS = ["instagram", "facebook", "linkedin"];

function PublishModal({ asset, onClose }) {
  // Start with nothing selected. Defaulting to a fixed list here caused a flash
  // where unconnected accounts briefly showed as selected before the
  // connection status loaded; instead we pick sensible defaults once we know
  // what's actually connected (see the init effect below).
  const [selected, setSelected] = React.useState([]);
  const [activeTab, setActiveTab] = React.useState(null);
  const [captions, setCaptions] = React.useState({
    instagram: "",
    x: "",
    linkedin: "",
    facebook: "",
    tiktok: "",
    pinterest: "",
  });
  const today = new Date();
  const defaultDate = today.toISOString().slice(0, 10);
  const [date, setDate] = React.useState(defaultDate);
  const [time, setTime] = React.useState("09:00");
  const [scheduleMode, setScheduleMode] = React.useState("now"); // 'now' | 'schedule'
  // After Publish is hit we flip to a success state with live links.
  // `null` until publish; then an array of { site, status, url, postedAt }.
  const [published, setPublished] = React.useState(null);
  const [publishing, setPublishing] = React.useState(false);

  // Per-platform connection state from Upload-Post. null until loaded.
  // Shape: { instagram: "connected"|"not_connected", facebook: ..., ... }
  const [connStatus, setConnStatus] = React.useState(null);
  const [openingConnect, setOpeningConnect] = React.useState(false);
  // Which crop gets published. "auto" = send the full image and let each
  // platform crop as it likes; otherwise a ratio slug (e.g. "1x1", "16x9")
  // which we generate server-side and send to every selected destination.
  const [cropChoice, setCropChoice] = React.useState("auto");
  // Video can't be cropped server-side (Sharp is image-only), so we always
  // send the full video and let each platform auto-crop to its best aspect.
  // NOTE: this hook must live above the early `if (published)` return so the
  // hook order stays stable across renders (moving it below blanks the app).
  const isVideo = !!(window.assetIsVideo && window.assetIsVideo(asset));
  React.useEffect(() => {
    if (isVideo && cropChoice !== "auto") setCropChoice("auto");
  }, [isVideo, cropChoice]);
  const authHeaders = React.useCallback(async () => {
    try {
      const t = window.Clerk?.session ? await window.Clerk.session.getToken() : null;
      return t ? { Authorization: `Bearer ${t}` } : {};
    } catch { return {}; }
  }, []);
  const loadConnections = React.useCallback(async () => {
    try {
      const r = await fetch("/api/social/connections", { headers: await authHeaders() });
      const data = await r.json();
      const map = {};
      for (const [pid, info] of Object.entries(data?.platforms || {})) {
        map[pid] = info?.status || "unknown";
      }
      setConnStatus(map);
    } catch { setConnStatus({}); }
  }, [authHeaders]);
  React.useEffect(() => { loadConnections(); }, [loadConnections]);
  // If user comes back from the Upload-Post connect tab, refresh status.
  React.useEffect(() => {
    const onFocus = () => loadConnections();
    window.addEventListener("focus", onFocus);
    return () => window.removeEventListener("focus", onFocus);
  }, [loadConnections]);
  // Once we know which platforms are connected, drop any pre-selected
  // destination that isn't connected — users can only publish to connected
  // accounts, so they shouldn't stay checked.
  React.useEffect(() => {
    if (!connStatus) return;
    setSelected((sel) => {
      const next = sel.filter((id) => connStatus[id] === "connected");
      return next.length === sel.length ? sel : next;
    });
  }, [connStatus]);
  // Once we know what's connected, pre-select sensible defaults — the preferred
  // platforms that are connected, or (if none of those are) every connected
  // account. Runs once; after that the user's checkbox choices are respected.
  const didInitSelection = React.useRef(false);
  React.useEffect(() => {
    if (!connStatus || didInitSelection.current) return;
    didInitSelection.current = true;
    const connected = Object.keys(connStatus).filter((id) => connStatus[id] === "connected");
    const preferred = PREFERRED_DEFAULTS.filter((id) => connected.includes(id));
    const initial = preferred.length ? preferred : connected;
    if (initial.length) {
      setSelected(initial);
      setActiveTab(initial[0]);
    }
  }, [connStatus]);
  // Keep the active caption tab pointing at a platform that's still selected.
  // Without this, pruning unconnected platforms (above) can leave activeTab on
  // a dropped platform — e.g. the caption header reads "Caption for Instagram"
  // when Facebook is the only destination left.
  React.useEffect(() => {
    if (selected.length && !selected.includes(activeTab)) setActiveTab(selected[0]);
  }, [selected, activeTab]);

  const openConnectFlow = React.useCallback(async (platformArg) => {
    // Accepts a single platform id or an array of them. Passing several lets
    // the Upload-Post hosted page show all of them at once so the user can
    // authorize everything in one trip.
    const platforms = Array.isArray(platformArg) ? platformArg : [platformArg];
    if (platforms.length === 0) return;
    setOpeningConnect(true);
    try {
      const redirectUrl = `${window.location.origin}/?back_from=connect`;
      const r = await fetch("/api/social/connect-link", {
        method: "POST",
        headers: { "Content-Type": "application/json", ...(await authHeaders()) },
        body: JSON.stringify({ platforms, redirectUrl }),
      });
      const body = await r.json();
      if (!r.ok || !body.accessUrl) {
        window.alert("Couldn't generate the connect link: " + (body.error || `HTTP ${r.status}`));
        setOpeningConnect(false);
        return;
      }
      // Navigate the whole tab (not a new tab) so the Upload-Post redirect
      // lands us right back on Vanday. We remember which asset's Publish
      // modal to reopen on return, and the remounted modal re-fetches the
      // connection status — so the just-connected platform shows as live.
      try { sessionStorage.setItem("vanday_publish_return", asset.id); } catch {}
      window.location.href = body.accessUrl;
    } catch {
      window.alert("Couldn't reach the server. Try again in a moment.");
      setOpeningConnect(false);
    }
  }, [authHeaders, asset.id]);

  // Pinterest needs a board to pin to. Fetched lazily when Pinterest is in
  // the selected platforms; cached for the rest of the modal lifetime.
  const [pinterestBoards, setPinterestBoards] = React.useState(null); // null = not loaded
  const [pinterestBoardId, setPinterestBoardId] = React.useState("");
  const [pinterestBoardsError, setPinterestBoardsError] = React.useState(null);
  React.useEffect(() => {
    if (!selected.includes("pinterest")) return;
    if (pinterestBoards !== null) return;
    (async () => {
      try {
        const r = await fetch("/api/social/pinterest-boards", {
          headers: window.Clerk?.session
            ? { Authorization: `Bearer ${await window.Clerk.session.getToken()}` }
            : {},
        });
        const data = await r.json();
        if (!r.ok) {
          setPinterestBoardsError(data?.detail?.message || data?.error || "Couldn't load Pinterest boards");
          setPinterestBoards([]);
          return;
        }
        // Upload-Post returns { boards: [...] } or just an array — handle both.
        const boards = Array.isArray(data) ? data : (data.boards || data.results || []);
        setPinterestBoards(boards);
        if (boards.length > 0 && !pinterestBoardId) setPinterestBoardId(boards[0].id);
      } catch (err) {
        setPinterestBoardsError("Couldn't reach the server.");
        setPinterestBoards([]);
      }
    })();
  }, [selected, pinterestBoards, pinterestBoardId]);

  const toggle = (id) => {
    setSelected((sel) => {
      const next = sel.includes(id) ? sel.filter((x) => x !== id) : [...sel, id];
      // keep activeTab valid
      if (!next.includes(activeTab) && next.length) setActiveTab(next[0]);
      return next;
    });
  };

  const activeSite = SITES.find((s) => s.id === activeTab) || SITES[0];
  const caption = captions[activeTab] || "";
  const captionMax = activeSite.caption.max;
  const remaining = captionMax - caption.length;
  const counterClass =
    remaining < 0 ? "over" : remaining < captionMax * 0.1 ? "warn" : "";

  // Push the asset to the selected social platforms via our /api/publish
  // endpoint (which proxies to Upload-Post). The server records the result
  // in publications_json and returns the per-platform outcome, which we use
  // to drive the success view — including any per-platform failures.
  const doPublish = async () => {
    if (selected.length === 0) return;
    setPublishing(true);
    const isNow = scheduleMode === "now";
    // Combine per-platform captions into a single "title" the server sends.
    // Upload-Post supports a shared caption; per-platform tailoring would
    // need its own X-specific / FB-specific fields. For v1 we send the
    // first non-empty caption (or the active tab's) as the shared title.
    const sharedCaption =
      Object.values(captions).find((c) => c && c.trim()) ||
      captions[activeTab] || "";

    // Per-platform captions: build a map of just the selected platforms so
    // each can have its own copy (X stays under 280, LinkedIn can be longer,
    // etc.). The server uses these to set Upload-Post's per-platform title
    // fields like `x_title` for X. Anything not customized falls back to
    // the shared caption.
    const perPlatformCaptions = {};
    for (const id of selected) {
      const c = captions[id];
      if (c && c.trim()) perPlatformCaptions[id] = c;
    }

    // Resolve the crop to publish. "auto" sends the full original (each
    // platform crops as it likes); a specific ratio sends that crop variant
    // id ("<parentId>--<ratio>") which the server generates on the fly.
    const parentId = asset.parentId || String(asset.id).replace(/--.+$/, "");
    const publishAssetId = cropChoice === "auto" ? parentId : `${parentId}--${cropChoice}`;

    let newPubs = [];
    try {
      const r = await fetch("/api/publish", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          ...(window.Clerk?.session ? { Authorization: `Bearer ${await window.Clerk.session.getToken()}` } : {}),
        },
        body: JSON.stringify({
          assetId: publishAssetId,
          caption: sharedCaption,
          captions: perPlatformCaptions,
          platforms: selected,
          pinterestBoardId: selected.includes("pinterest") ? pinterestBoardId : null,
          // scheduled_date passed through when the user picked Schedule:
          scheduledDate: isNow ? null : new Date(`${date}T${time}:00`).toISOString(),
        }),
      });
      const data = await r.json();
      if (!r.ok) {
        // Surface a single "failed" pub per selected platform so the user
        // sees what went wrong instead of a silent close. For 429 quota
        // errors we also include the usage stats Upload-Post returned.
        let errMsg = data.message || data.error || `Server returned ${r.status}`;
        if (data.usage) {
          errMsg += ` (Upload-Post usage: ${data.usage.count}/${data.usage.limit})`;
        }
        newPubs = selected.map((siteId) => ({
          site: siteId, status: "failed", postedAt: new Date().toLocaleString(),
          url: null, caption: captions[siteId] || "", error: errMsg,
        }));
      } else {
        newPubs = data.publications || [];
        // Keep the in-memory asset in sync so the preview's Publications
        // tab re-renders without a full reload.
        asset.publications = [...(asset.publications || []), ...newPubs];
        try { VandayServer.bumpRev && VandayServer.bumpRev(); } catch {}
      }
    } catch (err) {
      newPubs = selected.map((siteId) => ({
        site: siteId, status: "failed", postedAt: new Date().toLocaleString(),
        url: null, caption: captions[siteId] || "",
        error: "Couldn't reach the server. Try again in a moment.",
      }));
    }
    setPublished(newPubs);
    setPublishing(false);
  };

  // ---------- Success view ----------
  if (published) {
    const liveCount = published.filter((p) => p.status === "live").length;
    const failedCount = published.filter((p) => p.status === "failed").length;
    const scheduledCount = published.filter((p) => p.status === "scheduled").length;

    // Name the actual platforms ("Facebook", "Facebook and Instagram") rather
    // than a bare count ("1 site"), which reads awkwardly and tells the user
    // less than the names do.
    const nameOf = (id) => (SITES.find((s) => s.id === id) || {}).name || id || "the site";
    const joinNames = (arr) => {
      if (arr.length <= 1) return arr[0] || "";
      if (arr.length === 2) return `${arr[0]} and ${arr[1]}`;
      return `${arr.slice(0, -1).join(", ")} and ${arr[arr.length - 1]}`;
    };
    const liveNames = published.filter((p) => p.status === "live").map((p) => nameOf(p.site));
    const failedNames = published.filter((p) => p.status === "failed").map((p) => nameOf(p.site));
    const scheduledNames = published.filter((p) => p.status === "scheduled").map((p) => nameOf(p.site));

    // Title reflects what actually happened, not what was requested:
    //   - all failed   → "Couldn't post to Facebook"
    //   - all live     → "Posted to Facebook and Instagram"
    //   - all scheduled→ "Scheduled for Facebook"
    //   - mixed        → "Posted to Facebook · 1 failed"
    let title;
    if (liveCount === 0 && failedCount > 0 && scheduledCount === 0) {
      title = `Couldn't post to ${joinNames(failedNames)}`;
    } else if (scheduledCount === published.length) {
      title = `Scheduled for ${joinNames(scheduledNames)}`;
    } else if (failedCount === 0) {
      title = `Posted to ${joinNames(liveNames)}`;
    } else {
      title = `Posted to ${joinNames(liveNames)} · ${failedCount} failed`;
    }

    return (
      <div className="modal-backdrop" onClick={onClose}>
        <div className="modal" onClick={(e) => e.stopPropagation()} style={{ maxWidth: 520 }}>
          <header className="modal-head">
            <div>
              <h2 className="modal-title">
                {title}
              </h2>
              <div style={{ fontSize: 12.5, color: "var(--text-muted)", marginTop: 2 }}>
                <span style={{ fontFamily: "var(--font-mono)" }}>{asset.name}</span>
              </div>
            </div>
            <button className="btn ghost sm" onClick={onClose} title="Close"><IcClose size={16} /></button>
          </header>
          <div className="modal-body" style={{ display: "block", padding: 20 }}>
            <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
              {published.map((p, i) => {
                const site = SITES.find((s) => s.id === p.site) || { id: p.site, name: p.site || "Unknown", color: "var(--text-faint)" };
                const isFailed = p.status === "failed";
                const isScheduled = p.status === "scheduled";
                const isLive = !isFailed && !isScheduled;
                return (
                  <div
                    key={i}
                    className="pub-item"
                    style={{
                      padding: "10px 12px",
                      border: `1px solid ${isFailed ? "oklch(0.85 0.07 24)" : "var(--border)"}`,
                      background: isFailed ? "oklch(0.97 0.02 24)" : undefined,
                      borderRadius: 10,
                      display: "flex",
                      alignItems: "center",
                      gap: 10,
                      flexWrap: "wrap",
                    }}
                  >
                    <SiteMark site={site} size={22} />
                    <div style={{ minWidth: 0, flex: 1 }}>
                      <div className="pub-name">{site.name}</div>
                      <div className="pub-when" style={{ color: isFailed ? "oklch(0.45 0.16 24)" : undefined }}>
                        {isFailed ? (p.error || "Upload-Post couldn't post this") : p.postedAt}
                      </div>
                      {/* Secondary note lives under the timestamp so it never
                          collides with the status badge / action buttons. */}
                      {isLive && !p.url ? (
                        <div style={{ fontSize: 11.5, color: "var(--text-faint)", marginTop: 2 }}>
                          Posted — check your account for the link
                        </div>
                      ) : isScheduled && !p.url ? (
                        <div style={{ fontSize: 11.5, color: "var(--text-faint)", marginTop: 2 }}>
                          Publishes on schedule
                        </div>
                      ) : null}
                    </div>
                    <span
                      className={`pub-status ${isScheduled ? "scheduled" : ""}`}
                      style={isFailed ? { background: "oklch(0.55 0.18 24)", color: "white", flexShrink: 0 } : { flexShrink: 0 }}
                    >
                      {isFailed ? <IcInfo size={10} /> : isScheduled ? <IcClock size={10} /> : <IcCheck size={10} />}
                      {isFailed ? "Failed" : isScheduled ? "Scheduled" : "Live"}
                    </span>
                    {p.url ? (
                      <a
                        className="btn sm"
                        href={p.url}
                        target="_blank"
                        rel="noreferrer"
                        style={{ flexShrink: 0 }}
                        title={`Open this post on ${site.name} in a new tab`}
                      >
                        <IcExternal size={12} /> Open post
                      </a>
                    ) : null}
                    <MarkDeletedButton asset={asset} pub={p} />
                    <RemovePubButton asset={asset} pub={p} />
                  </div>
                );
              })}
            </div>
          </div>
          <footer className="modal-foot">
            <div style={{ fontSize: 12, color: "var(--text-muted)" }}>
              Also visible in <strong>Published</strong> in the sidebar.
            </div>
            <button className="btn accent" onClick={onClose}>Done</button>
          </footer>
        </div>
      </div>
    );
  }

  // ---------- Destination rows + grouped sections ----------
  // A single selectable destination row (checkbox, mark, name, connection
  // indicator). Shared by the loading flat-list and the two grouped sections.
  const renderSiteRow = (s) => {
    const isConnected = connStatus?.[s.id] === "connected";
    // Selectable while we're still loading status, or once it's confirmed
    // connected. Unconnected platforms can't be checked — the user must
    // connect them first (via the Connect button on the row).
    const selectable = connStatus == null || isConnected;
    const sel = selected.includes(s.id);
    return (
      <div
        key={s.id}
        className={`pub2-site ${sel ? "is-sel" : ""}`}
        onClick={selectable ? () => toggle(s.id) : undefined}
        style={selectable ? undefined : { cursor: "default" }}
        title={selectable ? undefined : `${s.name} isn't connected — connect it to publish here`}
      >
        <span
          className={`pub-check ${sel ? "checked" : ""}`}
          style={selectable ? undefined : { opacity: 0.3 }}
        >
          {sel && <IcCheck size={11} />}
        </span>
        <SiteMark site={s} size={24} />
        <span className="pub2-site-name">{s.name}</span>
        <span className="pub2-site-tail">
          {connStatus == null ? (
            <span className="meta" style={{ fontFamily: "var(--font-mono)", fontSize: 10.5, color: "var(--text-faint)" }}>
              {s.caption.max} chr
            </span>
          ) : isConnected ? (
            <span className="pub2-ok" title={`${s.name} is connected`}><IcCheck size={15} /></span>
          ) : (
            <>
              <button
                type="button"
                className="pub2-connect-link"
                onClick={(e) => { e.stopPropagation(); openConnectFlow(s.id); }}
                disabled={openingConnect}
              >
                Connect
              </button>
              <span className="pub2-warn" title={`${s.name} isn't connected`} style={{ marginLeft: 8 }}>
                <IcInfo size={15} />
              </span>
            </>
          )}
        </span>
      </div>
    );
  };

  // Group header: label + a monospaced count badge.
  const renderSiteGroup = (title, count) => (
    <div className="pub2-group">{title}<span className="count">{count}</span></div>
  );

  const connectedSites = SITES.filter((s) => connStatus?.[s.id] === "connected");
  const notConnectedSites = SITES.filter((s) => connStatus && (connStatus[s.id] || "not_connected") !== "connected");

  // Available crop ratios, deduped across platforms. Each maps to a server
  // crop the user can publish everywhere.
  const cropOptions = (window.UNIQUE_RATIOS || []).map((r) => ({
    slug: window.shortRatioSlug ? window.shortRatioSlug(r.r) : String(r.r).replace(":", "x"),
    label: r.name,
    ratio: r.r,
    w: r.w,
    h: r.h,
  }));
  const activeCrop = cropOptions.find((c) => c.slug === cropChoice) || null;

  // True once we know at least one platform is connected. While connStatus is
  // still loading (null) this stays false, so we never let a publish fire
  // before we know there's somewhere to send it.
  const anyConnected = !!connStatus && Object.values(connStatus).some((v) => v === "connected");
  // Connection status has loaded and nothing is wired up — show the centered
  // connect prompt instead of the compose form, and disable Publish.
  const showConnectPrompt = !!connStatus && !anyConnected;

  return (
    <div className="modal-backdrop" onClick={onClose}>
      <div className="modal pub2-modal" onClick={(e) => e.stopPropagation()}>
        {(() => {
          // Unconnected platforms among the user's current selection. Drives
          // both the inline header-pill and which platform the click-to-fix
          // action opens.
          const unconnected = connStatus
            ? selected.filter((id) => (connStatus[id] || "not_connected") !== "connected")
            : [];
          return (
            <header className="pub2-head">
              <div className="pub2-head-main">
                <h2 className="pub2-title">Publish to social</h2>
                <div className="pub2-sub">
                  <span className="pub2-file" title={asset.name}>{asset.name}</span>
                  <span className="dot" />
                  <span className="nowrap">{selected.length} selected</span>
                  {unconnected.length > 0 && (
                    <>
                      <span className="dot" />
                      <button
                        type="button"
                        className="pub2-connect"
                        onClick={() => openConnectFlow(unconnected)}
                        disabled={openingConnect}
                        title={
                          unconnected.length === 1
                            ? `${SITES.find((s) => s.id === unconnected[0])?.name || "1 destination"} isn't connected yet — click to authorize Vanday`
                            : `${unconnected.length} destinations aren't connected yet — click to start authorizing`
                        }
                      >
                        <IcInfo size={13} /> {unconnected.length} to connect
                      </button>
                    </>
                  )}
                </div>
              </div>
              <button className="btn ghost sm" onClick={onClose} title="Close">
                <IcClose size={16} />
              </button>
            </header>
          );
        })()}

        <div className="pub2-body">
          <aside className="pub2-dests">
            {connStatus === null ? (
              // Still loading connection status — show a flat list so the
              // user can start selecting destinations immediately.
              <>
                {renderSiteGroup("Destinations", SITES.length)}
                {SITES.map((s) => renderSiteRow(s))}
              </>
            ) : (
              <>
                {renderSiteGroup("Connected", connectedSites.length)}
                {connectedSites.length === 0
                  ? <div className="pub2-empty">Nothing connected yet — connect a platform below.</div>
                  : connectedSites.map((s) => renderSiteRow(s))}
                {renderSiteGroup("Not connected", notConnectedSites.length)}
                {notConnectedSites.length === 0
                  ? <div className="pub2-empty">Everything's connected. Nice.</div>
                  : notConnectedSites.map((s) => renderSiteRow(s))}
              </>
            )}
          </aside>

          <section className="pub2-compose">
            {showConnectPrompt ? (
              <div
                style={{
                  display: "flex", flexDirection: "column",
                  alignItems: "center", justifyContent: "center",
                  textAlign: "center", gap: 14,
                  padding: "48px 24px", minHeight: 320, height: "100%",
                }}
              >
                <div
                  style={{
                    width: 56, height: 56, borderRadius: 14,
                    display: "flex", alignItems: "center", justifyContent: "center",
                    background: "var(--accent-soft, oklch(0.94 0.04 38))",
                    color: "var(--accent, #c8553d)",
                  }}
                >
                  <IcLink size={26} />
                </div>
                <div style={{ fontSize: 15, fontWeight: 700, color: "var(--text)" }}>
                  Connect an account to publish
                </div>
                <div style={{ fontSize: 13, lineHeight: 1.5, color: "var(--text-muted)", maxWidth: 360 }}>
                  No social accounts are connected yet. Connect at least one to start publishing from Vanday.
                </div>
                <button
                  className="btn accent"
                  disabled={openingConnect}
                  onClick={() => openConnectFlow(selected.length ? selected : SITES.map((s) => s.id))}
                >
                  <IcLink size={14} /> {openingConnect ? "Opening…" : "Connect an account"}
                </button>
              </div>
            ) : (
            <>
            <div className="pub2-banner">
              {isVideo ? (
                <video
                  className="pub2-banner-thumb"
                  src={asset.url}
                  muted
                  playsInline
                  preload="metadata"
                  style={{ objectFit: "cover", background: "#000" }}
                />
              ) : (
                <div
                  className="pub2-banner-thumb"
                  style={{
                    backgroundImage: `url(${asset.url})`,
                    ...(activeCrop ? { aspectRatio: `${activeCrop.w} / ${activeCrop.h}` } : {}),
                  }}
                />
              )}
              <div style={{ minWidth: 0 }}>
                <div className="pub2-banner-h">
                  <IcSparkles className="ic" size={15} />
                  {isVideo
                    ? "Best crop for each platform"
                    : activeCrop ? `${activeCrop.label} crop · ${activeCrop.ratio}` : "Best crop for each platform"}
                </div>
                <p className="pub2-banner-p">
                  {isVideo
                    ? "Vanday sends the full video and lets each platform crop it to their best aspect ratio automatically."
                    : activeCrop
                    ? "Every selected destination gets this crop, framed around the subject."
                    : "Vanday sends the full image and lets each platform crop to its best aspect — or pick a single crop below."}
                </p>
              </div>
            </div>

            {/* Crop chooser — pick one framing for all destinations, or let
                Vanday decide. Maps to the server's per-crop publish path.
                Hidden for video: Sharp can't crop video, so we always send the
                full file and let platforms auto-crop. */}
            {!isVideo && (
            <div>
              <div className="pub2-section-label">Crop to publish</div>
              <div className="pub2-crops">
                <button
                  type="button"
                  onClick={() => setCropChoice("auto")}
                  className={`pub2-crop smart ${cropChoice === "auto" ? "is-on" : ""}`}
                >
                  <IcSparkles size={14} /> Select best crop for me
                </button>
                {cropOptions.map((c) => (
                  <button
                    key={c.slug}
                    type="button"
                    onClick={() => setCropChoice(c.slug)}
                    className={`pub2-crop ${cropChoice === c.slug ? "is-on" : ""}`}
                    title={`${c.label} · ${c.ratio}`}
                  >
                    <CropGlyph w={c.w} h={c.h} />
                    {c.label}
                    <span className="ratio">{c.ratio}</span>
                  </button>
                ))}
              </div>
            </div>
            )}

            {selected.length === 0 ? (
              <div style={{
                padding: 30, textAlign: "center",
                color: "var(--text-faint)", fontSize: 13,
                border: "1px dashed var(--border)", borderRadius: 10,
              }}>
                {connStatus === null
                  ? "Loading your connected accounts…"
                  : "Select at least one destination on the left."}
              </div>
            ) : (
              <div>
                <div className="pub2-tabs">
                  {selected.map((id) => {
                    const s = SITES.find((x) => x.id === id);
                    return (
                      <button
                        key={id}
                        className={`pub2-tab ${activeTab === id ? "is-on" : ""}`}
                        onClick={() => setActiveTab(id)}
                      >
                        <SiteMark site={s} size={16} /> {s.name}
                      </button>
                    );
                  })}
                </div>

                <div className="pub2-caption-head">
                  <span className="pub2-section-label" style={{ marginBottom: 0 }}>
                    Caption for {activeSite.name}
                  </span>
                  <span className={`pub2-counter ${counterClass}`}>
                    {caption.length} / {captionMax}
                  </span>
                </div>
                <textarea
                  className="pub-textarea"
                  placeholder={
                    activeSite.id === "x"
                      ? "What's the post? (280 max)"
                      : activeSite.id === "instagram"
                      ? "Write a caption… #hashtags work here"
                      : `Write your ${activeSite.name} post…`
                  }
                  value={caption}
                  onChange={(e) =>
                    setCaptions((c) => ({ ...c, [activeTab]: e.target.value }))
                  }
                />
                <div className="pub-meta-row" style={{ marginTop: 8 }}>
                  <span>Crop: {activeSite.ratios.map((r) => r.r).join(" · ")}</span>
                </div>

                {activeTab === "pinterest" && (
                  <div style={{ marginTop: 14, paddingTop: 14, borderTop: "1px solid var(--border)" }}>
                    <div className="pub2-section-label">Pin to board</div>
                    {pinterestBoards === null ? (
                      <div style={{ fontSize: 13, color: "var(--text-faint)" }}>Loading boards…</div>
                    ) : pinterestBoards.length === 0 ? (
                      <div style={{ fontSize: 13, color: "oklch(0.55 0.18 24)" }}>
                        {pinterestBoardsError || "No Pinterest boards found. Create one on pinterest.com first, then reload."}
                      </div>
                    ) : (
                      <select
                        className="nf-select"
                        value={pinterestBoardId}
                        onChange={(e) => setPinterestBoardId(e.target.value)}
                      >
                        {pinterestBoards.map((b) => (
                          <option key={b.id} value={b.id}>{b.name || b.id}</option>
                        ))}
                      </select>
                    )}
                  </div>
                )}
              </div>
            )}

            {/* Scheduling isn't live yet — the server publishes immediately, so
                showing a date/time picker would mislead users into thinking a
                post was queued for later when it actually went out now. Until
                real scheduling ships, the modal only offers "Post now". */}
            <div className="pub-schedule">
              <IcCalendar size={16} style={{ color: "var(--text-muted)" }} />
              <span style={{ fontWeight: 600 }}>Posts now</span>
              <span style={{ fontSize: 12, color: "var(--text-faint)" }}>
                Scheduling is coming soon.
              </span>
            </div>
            </>
            )}
          </section>
        </div>

        <footer className="modal-foot">
          <div style={{ fontSize: 12, color: "var(--text-muted)" }}>
            Posts publish through Vanday's connected social accounts.
          </div>
          <div style={{ display: "flex", gap: 8 }}>
            <button className="btn" onClick={onClose}>Cancel</button>
            <button
              className="btn accent"
              disabled={selected.length === 0 || publishing || !anyConnected}
              title={!anyConnected ? "Connect a social account first" : undefined}
              onClick={doPublish}
            >
              {publishing ? (
                <>Publishing…</>
              ) : scheduleMode === "now" ? (
                <><IcSend size={14} /> Publish to {selected.length}</>
              ) : (
                <><IcClock size={14} /> Schedule {selected.length} {selected.length === 1 ? "post" : "posts"}</>
              )}
            </button>
          </div>
        </footer>
      </div>
    </div>
  );
}

// ----- Published view (a sidebar destination in the library) -----
function PublishedView({ onOpenAsset, onNav, onBack }) {
  VandayServer.useServerRev(); // re-render when publications change
  const [activeSites, setActiveSites] = React.useState([]); // empty = all
  const [query, setQuery] = React.useState("");
  const [status, setStatus] = React.useState("all"); // all | live | scheduled

  // Whether this plan can post to socials at all. Beta has publishing ON, so
  // the upgrade path below never shows in beta — it's here for when paid
  // tiers gate publishing.
  const limits = (typeof window.useVandayLimits === "function") ? window.useVandayLimits() : null;
  const features = (typeof window.useVandayFeatures === "function") ? window.useVandayFeatures() : null;
  // Plan must allow publishing AND the member's role must permit it (admins and
  // editors only — viewers and contributors can't push to socials).
  const canPublish = (limits ? limits.publishingEnabled !== false : true)
    && (features ? features.canPublish : true);

  // Per-platform connection state from Upload-Post (null until loaded), so the
  // empty state can prompt the user to connect an account when none are wired.
  const [connStatus, setConnStatus] = React.useState(null);
  const [openingConnect, setOpeningConnect] = React.useState(false);
  const authHeaders = React.useCallback(async () => {
    try {
      const t = window.Clerk?.session ? await window.Clerk.session.getToken() : null;
      return t ? { Authorization: `Bearer ${t}` } : {};
    } catch { return {}; }
  }, []);
  const loadConnections = React.useCallback(async () => {
    try {
      const r = await fetch("/api/social/connections", { headers: await authHeaders() });
      const data = await r.json();
      const map = {};
      for (const [pid, info] of Object.entries(data?.platforms || {})) {
        map[pid] = info?.status || "unknown";
      }
      setConnStatus(map);
    } catch { setConnStatus({}); }
  }, [authHeaders]);
  React.useEffect(() => { loadConnections(); }, [loadConnections]);
  React.useEffect(() => {
    const onFocus = () => loadConnections();
    window.addEventListener("focus", onFocus);
    return () => window.removeEventListener("focus", onFocus);
  }, [loadConnections]);
  const anyConnected = !!connStatus && Object.values(connStatus).some((v) => v === "connected");

  // Open the Upload-Post hosted connect page for every platform at once, then
  // come back to Vanday (the focus listener refreshes status on return).
  const openConnectFlow = React.useCallback(async () => {
    setOpeningConnect(true);
    try {
      const redirectUrl = `${window.location.origin}/?back_from=connect`;
      const r = await fetch("/api/social/connect-link", {
        method: "POST",
        headers: { "Content-Type": "application/json", ...(await authHeaders()) },
        body: JSON.stringify({ platforms: SITES.map((s) => s.id), redirectUrl }),
      });
      const body = await r.json();
      if (!r.ok || !body.accessUrl) {
        window.alert("Couldn't generate the connect link: " + (body.error || `HTTP ${r.status}`));
        setOpeningConnect(false);
        return;
      }
      window.location.href = body.accessUrl;
    } catch {
      window.alert("Couldn't reach the server. Try again in a moment.");
      setOpeningConnect(false);
    }
  }, [authHeaders]);

  const published = ASSETS
    .filter((a) => a.publications && a.publications.length)
    .flatMap((a) =>
      a.publications.map((p) => ({ asset: a, pub: p, site: SITES.find((s) => s.id === p.site) }))
    )
    .filter((row) => (activeSites.length ? activeSites.includes(row.pub.site) : true))
    .filter((row) => (status === "all" ? true : row.pub.status === status))
    .filter((row) =>
      query
        ? (row.pub.caption || "").toLowerCase().includes(query.toLowerCase()) ||
          row.asset.name.toLowerCase().includes(query.toLowerCase()) ||
          row.asset.tags.some((t) => t.toLowerCase().includes(query.toLowerCase()))
        : true
    );

  const toggleSite = (id) =>
    setActiveSites((s) => (s.includes(id) ? s.filter((x) => x !== id) : [...s, id]));

  // Distinguish "nothing posted ever" from "nothing matches the filters."
  const hasAnyPosts = ASSETS.some((a) => a.publications && a.publications.length);
  const filtersActive = activeSites.length > 0 || status !== "all" || (!!query && query.trim().length > 0);
  const clearFilters = () => { setActiveSites([]); setStatus("all"); setQuery(""); };

  return (
    <main className="main">
      <header className="topbar">
        <div className="crumbs">
          <span>{window.WorkspaceName ? <WorkspaceName /> : "Library"}</span>
          <span className="sep">/</span>
          <span
            style={{ cursor: onBack ? "pointer" : "default" }}
            onClick={() => onBack && onBack()}
          >
            Library
          </span>
          <span className="sep">/</span>
          <span className="here">Published</span>
        </div>
        <div className="topbar-right">
          <div className="search">
            <IcSearch size={14} />
            <input
              placeholder="Search published content…"
              value={query}
              onChange={(e) => setQuery(e.target.value)}
            />
          </div>
          <span className="trial-pill" title="Free beta plan"><span className="dot" /> Free Beta</span>
        </div>
      </header>

      <section className="lib-header">
        <div>
          <h1 className="lib-title">Published</h1>
          <div className="lib-sub">
            <span>{published.length} posts</span>
            <span className="dot" />
            <span>{ASSETS.filter((a) => a.publications && a.publications.length).length} assets</span>
            <span className="dot" />
            <span>across {SITES.length} channels</span>
          </div>
        </div>
      </section>

      <section className="lib-tools" style={{ flexWrap: "wrap", gap: 8 }}>
        <span style={{ fontSize: 12, color: "var(--text-faint)", marginRight: 6 }}>Site</span>
        <span
          className={`site-chip ${activeSites.length === 0 ? "active" : ""}`}
          onClick={() => setActiveSites([])}
        >
          All
        </span>
        {SITES.map((s) => (
          <span
            key={s.id}
            className={`site-chip ${activeSites.includes(s.id) ? "active" : ""}`}
            onClick={() => toggleSite(s.id)}
          >
            <SiteMark site={s} size={16} />
            {s.name}
          </span>
        ))}
        <span style={{ width: 1, height: 18, background: "var(--border)", margin: "0 4px" }} />
        <span
          className={`chip ${status === "all" ? "active" : ""}`}
          onClick={() => setStatus("all")}
        >
          All status
        </span>
        <span
          className={`chip ${status === "live" ? "active" : ""}`}
          onClick={() => setStatus("live")}
        >
          Live
        </span>
        <span
          className={`chip ${status === "scheduled" ? "active" : ""}`}
          onClick={() => setStatus("scheduled")}
        >
          Scheduled
        </span>
      </section>

      <section style={{ padding: "8px 32px 60px" }} className="published-table">
        {published.length === 0 ? (
          <PublishedEmptyState
            canPublish={canPublish}
            connStatus={connStatus}
            anyConnected={anyConnected}
            hasAnyPosts={hasAnyPosts}
            filtersActive={filtersActive}
            onClearFilters={clearFilters}
            onConnect={openConnectFlow}
            openingConnect={openingConnect}
            onNav={onNav}
            onBack={onBack}
          />
        ) : (
        <table className="table" style={{ background: "var(--surface)", border: "1px solid var(--border)", borderRadius: 14, overflow: "hidden" }}>
          <thead>
            <tr>
              <th style={{ paddingLeft: 18 }}>Asset</th>
              <th>Site</th>
              <th>Caption</th>
              <th>Posted</th>
              <th>Status</th>
              <th></th>
            </tr>
          </thead>
          <tbody>
            {published.map((row, i) => (
              <tr key={i}>
                <td style={{ paddingLeft: 18 }}>
                  <button
                    onClick={() => onOpenAsset(row.asset)}
                    style={{
                      display: "flex", alignItems: "center", gap: 12,
                      background: "transparent", border: 0, padding: 0, cursor: "pointer", textAlign: "left",
                    }}
                  >
                    <span
                      className="thumb-mini"
                      style={{ backgroundImage: `url(${row.asset.url})` }}
                    />
                    <span>
                      <div style={{ fontFamily: "var(--font-mono)", fontSize: 12 }}>{row.asset.name}</div>
                      <div style={{ fontSize: 11.5, color: "var(--text-faint)", marginTop: 2 }}>{row.asset.uploader}</div>
                    </span>
                  </button>
                </td>
                <td>
                  <span style={{ display: "inline-flex", alignItems: "center", gap: 8 }}>
                    <SiteMark site={row.site} size={18} />
                    {row.site.name}
                  </span>
                </td>
                <td style={{ maxWidth: 360, color: "var(--text-muted)" }}>
                  <div style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
                    {row.pub.caption}
                  </div>
                </td>
                <td style={{ color: "var(--text-muted)", fontFamily: "var(--font-mono)", fontSize: 12 }}>
                  {row.pub.postedAt}
                </td>
                <td>
                  <span className={`pub-status ${row.pub.status === "scheduled" ? "scheduled" : ""}`}>
                    {row.pub.status === "scheduled" ? <IcClock size={10} /> : <IcCheck size={10} />}
                    {row.pub.status === "scheduled" ? "Scheduled" : "Live"}
                  </span>
                </td>
                <td style={{ textAlign: "right", paddingRight: 18 }}>
                  {row.pub.url ? (
                    <a className="btn sm" href={row.pub.url} target="_blank" rel="noreferrer">
                      <IcExternal size={13} /> Open post
                    </a>
                  ) : (
                    <span style={{ fontSize: 11.5, color: "var(--text-faint)" }}>Will publish on schedule</span>
                  )}
                </td>
              </tr>
            ))}
          </tbody>
        </table>
        )}
      </section>
    </main>
  );
}

// Empty state for the Published view. Mirrors the centered icon-tile style
// used by the library empty states. Branches:
//   • filters active but nothing matches → clear-filters prompt
//   • plan cannot publish → upgrade prompt (never shown in beta)
//   • no social accounts connected → connect prompt
//   • connected but nothing posted yet → publish-from-library prompt
function PublishedEmptyState({
  canPublish, connStatus, anyConnected, hasAnyPosts, filtersActive,
  onClearFilters, onConnect, openingConnect, onNav, onBack,
}) {
  const wrap = {
    display: "flex", flexDirection: "column", alignItems: "center",
    gap: 14, maxWidth: 380, margin: "0 auto", textAlign: "center",
    padding: "80px 24px",
  };
  const tile = {
    width: 56, height: 56, borderRadius: 14,
    background: "var(--accent-soft, oklch(0.94 0.04 38))",
    color: "var(--accent, #c8553d)", display: "grid", placeItems: "center",
  };
  const title = { color: "var(--text)", display: "block", fontSize: 15, marginBottom: 4, fontWeight: 600 };
  const body = { color: "var(--text-muted)", fontSize: 13, lineHeight: 1.5 };

  // 1) Filters hiding everything (there ARE posts, just none match).
  if (filtersActive && hasAnyPosts) {
    return (
      <div style={wrap}>
        <div style={tile}><IcFilter size={24} /></div>
        <div>
          <strong style={title}>No posts match these filters</strong>
          <div style={body}>Try a different site or status, or clear the filters to see everything you've published.</div>
        </div>
        <button className="btn" onClick={onClearFilters}><IcClose size={12} /> Clear filters</button>
      </div>
    );
  }

  // 2) Plan does not allow publishing — offer an upgrade. (Beta has publishing
  //    enabled, so this branch is dormant until paid tiers gate it.)
  if (!canPublish) {
    return (
      <div style={wrap}>
        <div style={tile}><IcSparkles size={24} /></div>
        <div>
          <strong style={title}>Publishing is not on your plan</strong>
          <div style={body}>Upgrade to post photos and video straight to Instagram, Facebook, TikTok and more — and track every post here.</div>
        </div>
        <button className="btn primary" onClick={() => onNav && onNav("settings")}><IcSparkles size={14} /> Upgrade plan</button>
      </div>
    );
  }

  // 3) Connections still loading — keep it quiet to avoid flashing the wrong CTA.
  if (connStatus === null) {
    return (
      <div style={wrap}>
        <div style={tile}><IcSend size={24} /></div>
        <div><strong style={title}>Nothing published yet</strong>
          <div style={body}>Checking your connected social accounts…</div>
        </div>
      </div>
    );
  }

  // 4) No social accounts connected — prompt to connect.
  if (!anyConnected) {
    return (
      <div style={wrap}>
        <div style={tile}><IcLink size={24} /></div>
        <div>
          <strong style={title}>Connect a social account</strong>
          <div style={body}>Link Instagram, Facebook, TikTok and more to start posting from Vanday. Once connected, every post shows up here.</div>
        </div>
        <button className="btn primary" onClick={onConnect} disabled={openingConnect}>
          <IcLink size={14} /> {openingConnect ? "Opening…" : "Connect an account"}
        </button>
      </div>
    );
  }

  // 5) Connected, just nothing posted yet — point them at the library.
  return (
    <div style={wrap}>
      <div style={tile}><IcSend size={24} /></div>
      <div>
        <strong style={title}>Nothing published yet</strong>
        <div style={body}>Open an asset from your library and hit Publish to post it to your connected accounts. Your posts will appear here.</div>
      </div>
      <button className="btn primary" onClick={() => (onBack ? onBack() : onNav && onNav("library"))}>
        <IcGrid size={14} /> Go to library
      </button>
    </div>
  );
}

Object.assign(window, { CropsGrid, PublicationList, PublishModal, PublishedView });
