/* Main App. Loads after shared.jsx and templates.jsx. */

const { useState: useStateA, useEffect: useEffectA, useMemo: useMemoA, useRef: useRefA, useCallback: useCallbackA } = React;
/** Alias avoids Babel duplicate-declaration with shared.jsx `VisualExportFrame`. */
const ExportFrame = (props) => {
  const Frame = window.VisualExportFrame;
  return Frame ? <Frame {...props} /> : <>{props.children}</>;
};

function addBylineToPlatforms(platforms, byline) {
  const sig = (byline || "").trim();
  if (!sig || !platforms || typeof platforms !== "object") return platforms;
  const out = { ...platforms };
  for (const p of window.PLATFORMS || []) {
    const key = p.id;
    const text = typeof out[key] === "string" ? out[key] : "";
    if (!text) continue;
    if (text.includes(sig)) continue;
    out[key] = `${text.trim()}\n\n${sig}`;
  }
  return out;
}

function buildPlatformComposeUrl(platformId, text) {
  const encoded = encodeURIComponent((text || "").trim());
  if (platformId === "linkedin") return "https://www.linkedin.com/feed/";
  if (platformId === "x") return encoded
    ? `https://x.com/intent/post?text=${encoded}`
    : "https://x.com/compose/post";
  if (platformId === "facebook") return "https://www.facebook.com/";
  if (platformId === "pinterest") return encoded
    ? `https://www.pinterest.com/pin-builder/?description=${encoded}`
    : "https://www.pinterest.com/pin-creation-tool/";
  if (platformId === "reddit") {
    const lines = (text || "").trim().split(/\n+/);
    const title = encodeURIComponent((lines[0] || "Discussion").slice(0, 150));
    const body = encodeURIComponent((text || "").trim());
    return `https://www.reddit.com/submit?selftext=true&title=${title}&text=${body}`;
  }
  return "";
}

function openPlatformComposer(platformId, text) {
  const url = buildPlatformComposeUrl(platformId, text);
  if (!url) return;
  try {
    window.open(url, "_blank", "noopener,noreferrer");
    window.trackAppEvent?.("open_social_platform", { platformId });
  } catch (err) {
    console.warn("open platform failed", platformId, err);
  }
}

function PublicStatsBar() {
  const fallback = window.DEFAULT_PUBLIC_STATS || { visitors: 97, posts: 33 };
  const [stats, setStats] = useStateA(fallback);

  useEffectA(() => {
    let cancelled = false;
    async function load() {
      const data = window.recordPublicVisit
        ? await window.recordPublicVisit()
        : (window.fetchPublicStats ? await window.fetchPublicStats() : fallback);
      if (!cancelled && data) setStats(data);
    }
    load();
    function onUpdate(e) {
      if (e.detail) setStats(e.detail);
      else if (window.fetchPublicStats) window.fetchPublicStats().then(s => s && setStats(s));
    }
    window.addEventListener("public-stats-updated", onUpdate);
    return () => {
      cancelled = true;
      window.removeEventListener("public-stats-updated", onUpdate);
    };
  }, []);

  return (
    <div className="public-stats" aria-label="Community usage stats">
      <span className="public-stat" title="People who opened the app">
        <i className="ti ti-users" aria-hidden="true"></i>
        <strong>{stats.visitors.toLocaleString()}</strong>
        <span>visitors</span>
      </span>
      <span className="public-stat-sep" aria-hidden="true">·</span>
      <span className="public-stat" title="Successful text or visual post generations">
        <i className="ti ti-circle-check" aria-hidden="true"></i>
        <strong>{stats.posts.toLocaleString()}</strong>
        <span>posts created</span>
      </span>
    </div>
  );
}

function DemoVideoBanner({ onDismiss }) {
  const url = window.APP_DEMO_VIDEO_URL;
  if (!url) return null;
  return (
    <div className="demo-banner" role="note">
      <i className="ti ti-player-play" aria-hidden="true"></i>
      <span>
        <strong>New here?</strong>{" "}
        <a href={url} target="_blank" rel="noopener noreferrer">Watch the YouTube demo</a>
        {" "}— setup, four platforms, visual export, and BYOK API keys.
      </span>
      <button type="button" className="demo-banner-dismiss" onClick={onDismiss} aria-label="Dismiss demo banner">
        <i className="ti ti-x"></i>
      </button>
    </div>
  );
}

function CreatorLinks({ highlightIds, showTitle }) {
  const links = window.CREATOR_LINKS || [];
  const highlightSet = new Set(highlightIds || ["demo-video", "buymeacoffee"]);
  return (
    <div className={showTitle === false ? "" : "creator-footer"}>
      {showTitle !== false && (
        <>
          <h2 className="creator-footer-title">Connect with Mayank</h2>
          <p className="creator-footer-sub">Watch the demo above, then explore more AI builds, playbooks, and ways to connect.</p>
        </>
      )}
      <div className="creator-links-grid">
        {links.map(link => (
          <a
            key={link.id}
            className={"creator-link" + (highlightSet.has(link.id) ? " is-highlight" : "")}
            href={link.url}
            target="_blank"
            rel="noopener noreferrer"
            title={link.tagline}
          >
            <span className="creator-link-icon" aria-hidden="true">
              <i className={"ti " + link.icon}></i>
            </span>
            <span className="creator-link-body">
              <span className="creator-link-label">
                <span className="creator-link-emoji" aria-hidden="true">{link.emoji}</span>
                {link.label}
                {link.badge && <span className="creator-link-badge">{link.badge}</span>}
              </span>
              <span className="creator-link-tagline">{link.tagline}</span>
            </span>
          </a>
        ))}
      </div>
    </div>
  );
}

function App() {
  const [tab, setTab] = useStateA("generate");

  const [textPosts, setTextPosts]     = useStateA([]);
  const [visualPosts, setVisualPosts] = useStateA([]);
  const [pickedLayoutSignature, setPickedLayoutSignature] = useStateA(null);
  const [settings, setSettings]       = useStateA(() => window.getDefaultSettings());
  const [loaded, setLoaded] = useStateA(false);
  const [demoBannerDismissed, setDemoBannerDismissed] = useStateA(() => {
    try { return localStorage.getItem("postgen_demo_banner_dismissed") === "1"; } catch { return false; }
  });

  const [showToast, toastNode] = window.useToast();

  function dismissDemoBanner() {
    try { localStorage.setItem("postgen_demo_banner_dismissed", "1"); } catch {}
    setDemoBannerDismissed(true);
  }

  // load
  useEffectA(() => {
    (async () => {
      const [t, v, s] = await Promise.all([
        window.loadFromStorage(window.STORAGE_KEY_TEXT),
        window.loadFromStorage(window.STORAGE_KEY_VISUAL),
        window.loadSettings(),
      ]);
      setTextPosts(t);
      setVisualPosts(v);
      setSettings(window.sanitizeSettings(s));
      setLoaded(true);
    })();
  }, []);

  // persist
  useEffectA(() => { if (loaded) window.saveToStorage(window.STORAGE_KEY_TEXT,   textPosts);   }, [textPosts, loaded]);
  useEffectA(() => { if (loaded) window.saveToStorage(window.STORAGE_KEY_VISUAL, visualPosts); }, [visualPosts, loaded]);
  useEffectA(() => { if (loaded) window.saveSettings(settings); }, [settings, loaded]);

  function deleteTextPost(id)   { setTextPosts  (prev => prev.filter(p => p.id !== id)); }
  function deleteVisualPost(id) { setVisualPosts(prev => prev.filter(p => p.id !== id)); }

  function clearAll() {
    if (!textPosts.length && !visualPosts.length) return;
    if (!window.confirm("Delete all saved posts (text + visual)? This can't be undone.")) return;
    setTextPosts([]); setVisualPosts([]);
  }

  async function copyText(text, source) {
    const ok = await window.copyToClipboard(text);
    if (ok && window.trackAppEvent) {
      window.trackAppEvent("post_copied", { source: source || "clipboard" });
    }
    showToast(ok ? "Copied to clipboard" : "Copy failed");
  }

  function pickLayoutSignature(sig) {
    setPickedLayoutSignature(sig);
    setTab("visual");
    showToast("Layout + template applied — scroll to Picked layout");
  }

  const totalCount = textPosts.length + visualPosts.length;
  const localDevHost = window.isLocalDevHost();
  const layoutExplorerOn = localDevHost;

  useEffectA(() => {
    if (!layoutExplorerOn) return;
    if (window.LINKEDIN_LAYOUT_CATALOG) return;
    const script = document.createElement("script");
    script.src = "layout-catalog.js";
    script.async = true;
    document.body.appendChild(script);
  }, [layoutExplorerOn]);

  useEffectA(() => {
    if (!layoutExplorerOn && tab === "layouts") setTab("generate");
  }, [layoutExplorerOn, tab]);

  return (
    <div className="app-shell">
      <header className="app-header">
        <div className="brand">
          <div className="brand-mark"><i className="ti ti-pencil"></i></div>
          <div>
            <h1 className="brand-title">Post Generator</h1>
            <p className="brand-sub">LinkedIn, X, Facebook &amp; Pinterest — one topic, four posts.</p>
            <PublicStatsBar />
          </div>
        </div>
        <div className="header-actions">
          {window.APP_DEMO_VIDEO_URL && (
            <a
              className="demo-video-link"
              href={window.APP_DEMO_VIDEO_URL}
              target="_blank"
              rel="noopener noreferrer"
              title="Watch the YouTube demo — how to use this app"
            >
              <i className="ti ti-player-play"></i>
              <span>Watch demo</span>
            </a>
          )}
          <button className="provider-badge" onClick={() => setTab("settings")} title="Change AI provider">
            <i className="ti ti-cpu"></i>
            <span>{window.describeSettings(settings)}</span>
            <i className="ti ti-chevron-right" style={{fontSize: 14, opacity: 0.6}}></i>
          </button>
        </div>
      </header>

      {!demoBannerDismissed && window.APP_DEMO_VIDEO_URL && (
        <DemoVideoBanner onDismiss={dismissDemoBanner} />
      )}

      <div className="tab-bar" role="tablist">
        <button
          role="tab"
          className={"tab-btn" + (tab === "generate" ? " is-active" : "")}
          onClick={() => setTab("generate")}
        >
          <i className="ti ti-sparkles"></i> Social posts
        </button>
        <button
          role="tab"
          className={"tab-btn" + (tab === "visual" ? " is-active" : "")}
          onClick={() => setTab("visual")}
        >
          <i className="ti ti-layout-collage"></i> Visual post
        </button>
        {layoutExplorerOn && (
          <button
            role="tab"
            className={"tab-btn" + (tab === "layouts" ? " is-active" : "")}
            onClick={() => setTab("layouts")}
          >
            <i className="ti ti-layout-grid"></i> Layout explorer
          </button>
        )}
        <button
          role="tab"
          className={"tab-btn" + (tab === "history" ? " is-active" : "")}
          onClick={() => setTab("history")}
        >
          <i className="ti ti-history"></i> History
          <span className="tab-count">{totalCount}</span>
        </button>
        <button
          role="tab"
          className={"tab-btn" + (tab === "settings" ? " is-active" : "")}
          onClick={() => setTab("settings")}
        >
          <i className="ti ti-settings"></i> Settings
        </button>
      </div>

      {tab === "generate" && (
        <TextTab
          posts={textPosts}
          setPosts={setTextPosts}
          copyText={copyText}
          showToast={showToast}
          settings={settings}
        />
      )}
      {tab === "visual" && (
        <VisualTab
          posts={visualPosts}
          setPosts={setVisualPosts}
          textPosts={textPosts}
          pickedLayoutSignature={pickedLayoutSignature}
          onClearPickedLayout={() => setPickedLayoutSignature(null)}
          copyText={copyText}
          showToast={showToast}
          settings={settings}
        />
      )}
      {tab === "settings" && (
        <SettingsTab
          settings={settings}
          setSettings={setSettings}
          showToast={showToast}
          textPostCount={textPosts.length}
          visualPostCount={visualPosts.length}
        />
      )}
      {layoutExplorerOn && tab === "layouts" && (
        <LayoutExplorer onPickLayout={pickLayoutSignature} />
      )}
      {tab === "history" && (
        <HistoryTab
          textPosts={textPosts}
          visualPosts={visualPosts}
          deleteTextPost={deleteTextPost}
          deleteVisualPost={deleteVisualPost}
          clearAll={clearAll}
          copyText={copyText}
          showToast={showToast}
        />
      )}

      <CreatorLinks />

      {toastNode}
    </div>
  );
}

/* Maps a layout signature to the best matching template id */
function templateIdForSignature(sig) {
  if (!sig) return null;
  const { ext, width, height, frames_or_pages: fp } = sig;
  const ratio = (width || 1) / (height || 1);
  const isAnimated = ext === ".gif" && fp > 1;

  if (ext === ".pdf" && fp > 1) {
    if (ratio >= 0.75 && ratio <= 0.85 && width >= 400 && width <= 900) return "pdf-deck-slides";
    if (ratio > 1.4) return "wide-banner";
    if (ratio >= 0.95 && ratio <= 1.05) return "square-spotlight";
    return "pdf-deck-slides";
  }

  if (isAnimated) {
    if (width >= 805 && width <= 815 && height >= 1005 && height <= 1020 && fp >= 6 && fp <= 20) return "pdf-deck-slides";
    if (fp >= 100 || height >= 1400 || (width >= 1080 && height >= 1350 && fp >= 60)) return "scroll-reel";
    if (ratio >= 0.95 && ratio <= 1.05) return "spotlight-cycle";
    if (ratio > 1.05) return "frame-sequence";
    if (height >= 1050) return "scroll-reel";
    if (height >= 900 && height <= 1010 && ratio >= 0.78 && ratio <= 0.82) return "comparison-reveal";
    if (fp >= 80) return "typewriter-list";
    if (fp >= 50) return "layer-stack-reveal";
    if (height >= 1300) return "story-slides";
    return "frame-sequence";
  }

  if (ratio > 1.15) {
    if (ratio > 1.45) return "wide-banner";
    return "horizontal-timeline";
  }

  if (ratio >= 0.95 && ratio <= 1.05) return "square-spotlight";

  if (ratio < 0.68) {
    if (width >= 1000) return "stat-dashboard";
    return "cheat-sheet";
  }

  if (width >= 1200 && height >= 1400 && ratio >= 0.78) return "big-list";
  if (width >= 1000 && height >= 1400) return "stat-dashboard";
  if (width >= 1000 && height >= 1200) return "process-flow";

  if (height >= 1100) return "concept-grid";
  if (height >= 1060) return "tip-cards";

  if (ratio >= 0.85 && ratio < 0.95) return "myth-fact";

  if (height >= 900 && height <= 1010) {
    if (width >= 1080) return "anatomy-cards";
    if (width >= 805 && width <= 815 && height >= 1005 && height <= 1020) return "pdf-deck-slides";
    return "split-comparison";
  }

  if (width >= 805 && width <= 815 && height >= 1005 && height <= 1020) return "pdf-deck-slides";

  return "tip-cards";
}


function LayoutExplorer({ onPickLayout }) {
  const catalog = window.LINKEDIN_LAYOUT_CATALOG || { summary: {}, unique_signatures: [] };
  const [query, setQuery] = useStateA("");
  const [extFilter, setExtFilter] = useStateA("all");
  const [sortBy, setSortBy] = useStateA("count-desc");
  const extCounts = useMemoA(() => {
    const counts = {};
    (catalog.unique_signatures || []).forEach(r => {
      if (!r.ext || r.ext === ".pdf") return;
      counts[r.ext] = (counts[r.ext] || 0) + 1;
    });
    return counts;
  }, [catalog.unique_signatures]);

  const rows = useMemoA(() => {
    const q = query.trim().toLowerCase();
    let out = (catalog.unique_signatures || []).filter(r => {
      if (r.ext === ".pdf") return false;
      if (extFilter !== "all" && r.ext !== extFilter) return false;
      if (!q) return true;
      const blob = `${r.ext} ${r.width} ${r.height} ${r.frames_or_pages} ${(r.sample_files || []).join(" ")}`.toLowerCase();
      return blob.includes(q);
    });
    out = [...out];
    if (sortBy === "count-desc") out.sort((a, b) => b.count - a.count);
    if (sortBy === "count-asc") out.sort((a, b) => a.count - b.count);
    if (sortBy === "size-desc") out.sort((a, b) => (b.width * b.height) - (a.width * a.height));
    if (sortBy === "size-asc") out.sort((a, b) => (a.width * a.height) - (b.width * b.height));
    return out;
  }, [catalog.unique_signatures, extFilter, query, sortBy]);

  return (
    <>
      <div className="card">
        <p className="eyebrow">Filters</p>
        <h2 className="card-title">Find a specific layout shape</h2>
        <div className="form-grid">
          <div className="field">
            <label><i className="ti ti-search"></i>Search</label>
            <input
              className="input"
              value={query}
              onChange={e => setQuery(e.target.value)}
              placeholder="Try: 1080 1350, .gif, Claude/"
            />
          </div>
          <div className="field">
            <label><i className="ti ti-file-type-pdf"></i>File type</label>
            <select className="select" value={extFilter} onChange={e => setExtFilter(e.target.value)} title="Filter file type">
              <option value="all">All types</option>
              {Object.keys(extCounts).sort().map(ext => (
                <option key={ext} value={ext}>{ext} ({extCounts[ext]})</option>
              ))}
            </select>
          </div>
          <div className="field form-row">
            <label><i className="ti ti-arrows-sort"></i>Sort</label>
            <select className="select" value={sortBy} onChange={e => setSortBy(e.target.value)} title="Sort layout rows">
              <option value="count-desc">Most frequent first</option>
              <option value="count-asc">Least frequent first</option>
              <option value="size-desc">Largest area first</option>
              <option value="size-asc">Smallest area first</option>
            </select>
          </div>
        </div>
      </div>

      <div className="card">
        <p className="eyebrow">Signatures</p>
        <h2 className="card-title">{rows.length} unique layout signatures</h2>
        <div
          style={{
            display: "grid",
            gridTemplateColumns: "repeat(2, minmax(0, 1fr))",
            gap: 12,
          }}
        >
          {rows.map((row, i) => (
            <div className="template-card" key={`${row.ext}-${row.width}-${row.height}-${row.frames_or_pages}-${i}`}>
              <div
                className="template-thumb"
                style={{
                  padding: 10,
                  background: "var(--color-background-secondary)",
                  aspectRatio: "auto",
                  height: 360,
                }}
              >
                <LayoutSignatureThumb row={row} />
              </div>
              <div>
                <div className="history-meta">
                  <span className="tag tag-accent">{row.ext}</span>
                  <span className="tag">{row.width} x {row.height}</span>
                  <span className="tag">{row.ext === ".gif" ? `${row.frames_or_pages} frames` : "static"}</span>
                  <span className="history-date">{row.count} file{row.count === 1 ? "" : "s"}</span>
                </div>
                <div className="history-topic">{row.width} x {row.height} {row.ext} layout</div>
                {(() => {
                  const tplId = templateIdForSignature(row);
                  const tpl = tplId && (window.TEMPLATES || []).find(t => t.id === tplId);
                  return tpl ? (
                    <div style={{ marginTop: 6, display: "flex", alignItems: "center", gap: 6 }}>
                      <span className="tag tag-accent" style={{ fontSize: 10 }}>
                        <i className="ti ti-layout-collage" style={{ marginRight: 4 }}></i>
                        Template: {tpl.name}
                      </span>
                    </div>
                  ) : null;
                })()}
              </div>
              <div className="result-actions" style={{ marginTop: 8 }}>
                <button
                  className="btn btn-primary btn-small"
                  onClick={() => onPickLayout(row)}
                  title="Use this layout in Visual tab"
                >
                  <i className="ti ti-arrow-right"></i> Use in Visual tab
                </button>
              </div>
            </div>
          ))}
        </div>
      </div>
    </>
  );
}

function LayoutSignatureThumb({ row }) {
  const w = Number(row.width) || 1;
  const h = Number(row.height) || 1;
  const ratio = w / h;
  const frameW = ratio >= 1 ? "96%" : `${Math.max(56, Math.round(ratio * 96))}%`;
  const frameH = ratio >= 1 ? `${Math.max(56, Math.round((1 / ratio) * 96))}%` : "96%";
  const sampleFile = (row.sample_files && row.sample_files[0]) || "";
  const isPreviewableImage = row.ext === ".jpg" || row.ext === ".jpeg" || row.ext === ".png" || row.ext === ".webp" || row.ext === ".gif";
  const previewSrc = sampleFile ? `linkedInresources/${sampleFile}` : "";
  return (
    <div style={{ width: "100%", height: "100%", display: "grid", placeItems: "center", background: "var(--color-background-tertiary)", borderRadius: 6 }}>
      <div
        style={{
          width: frameW,
          height: frameH,
          border: "1px solid var(--color-border-secondary)",
          borderRadius: 4,
          background: "var(--color-background-primary)",
          position: "relative",
          overflow: "hidden",
        }}
      >
        {isPreviewableImage && previewSrc ? (
          <img
            src={previewSrc}
            alt={`${row.width}x${row.height} ${row.ext} sample`}
            style={{
              width: "100%",
              height: "100%",
              objectFit: "contain",
              display: "block",
              background: "var(--color-background-primary)",
            }}
            loading="lazy"
          />
        ) : (
          <>
            <div style={{ position: "absolute", top: 4, left: 4, right: 4, height: 6, background: "var(--color-background-tertiary)", borderRadius: 2 }}></div>
            <div style={{ position: "absolute", top: 16, left: 4, right: 4, bottom: 4, border: "1px dashed var(--color-border-tertiary)", borderRadius: 2 }}></div>
            <div
              style={{
                position: "absolute",
                inset: 0,
                display: "grid",
                placeItems: "center",
                fontSize: 10,
                color: "var(--color-text-tertiary)",
                fontFamily: "var(--font-mono)",
                letterSpacing: 0.6,
              }}
            >
              PDF
            </div>
          </>
        )}
      </div>
    </div>
  );
}

/* ================================================================
   TEXT TAB
   ================================================================ */
function TextTab({ posts, setPosts, copyText, settings }) {
  const [niche,    setNiche]    = useStateA("Product design");
  const [topic,    setTopic]    = useStateA("");
  const [sourceContent, setSourceContent] = useStateA("");
  const [style,    setStyle]    = useStateA("personal-story");
  const [tone,     setTone]     = useStateA("casual-relatable");
  const [audience, setAudience] = useStateA("Early-career designers");

  const [generating, setGenerating] = useStateA(false);
  const [current,    setCurrent]    = useStateA(null);
  const [error,      setError]      = useStateA(null);

  const memoryCount = Math.min(posts.length, 10);
  const recent = useMemoA(() => posts.slice(0, 10), [posts]);
  const isBlankStyle = style === "blank";
  const canGenerate = (isBlankStyle
    ? sourceContent.trim().length > 0
    : (topic.trim().length > 0 || sourceContent.trim().length > 0)) && !generating;

  async function generate() {
    if (!canGenerate) return;
    setError(null); setGenerating(true);
    try {
      const effectiveTopic = topic.trim() || sourceContent.trim().split("\n")[0].slice(0, 120);
      const generated = await window.generatePlatformPosts({
        settings, niche, topic: effectiveTopic, style, tone, audience,
        recentPosts: recent, sourceContent: sourceContent.trim() || undefined,
      });
      const platforms = addBylineToPlatforms(generated.platforms, settings.authorByline);
      const entry = {
        id: "p_" + Date.now().toString(36) + Math.random().toString(36).slice(2, 6),
        kind: "text",
        platforms,
        tags: generated.tags || null,
        style, tone, niche, topic: effectiveTopic, audience,
        sourceContent: sourceContent.trim() || undefined,
        createdAt: Date.now(),
      };
      setCurrent(entry);
      setPosts(prev => [entry, ...prev]);
      window.trackAppEvent?.("text_post_generated", { style, tone });
    } catch (err) {
      console.error(err);
      window.trackAppEvent?.("text_generation_failed");
      setError(err.message || "Generation failed. Try again.");
    } finally {
      setGenerating(false);
    }
  }

  return (
    <>
      <MemoryBar count={memoryCount} label="social bundle" />

      <div className="card">
        <p className="eyebrow">Inputs</p>
        <h2 className="card-title">Tell the model what to write</h2>
        <p className="card-sub">One generate creates tailored posts for LinkedIn, X, Facebook, Pinterest, and Reddit — plus <strong>hashtags per platform</strong> (shown below each post; use <em>Copy + tags</em>).</p>

        <div className="form-grid">
          <div className="field">
            <label><i className="ti ti-briefcase"></i>Niche / expertise</label>
            <input className="input" value={niche} onChange={e => setNiche(e.target.value)} placeholder="e.g. Product design, SaaS marketing" />
          </div>
          <div className="field">
            <label><i className="ti ti-users"></i>Target audience</label>
            <input className="input" value={audience} onChange={e => setAudience(e.target.value)} placeholder="e.g. Early-career designers" />
          </div>

          <div className="field form-row">
            <label><i className="ti ti-template"></i>Post style</label>
            <div className="chip-group">
              {window.STYLES.map(s => (
                <button key={s.id} type="button"
                  className={"chip" + (style === s.id ? " is-active" : "")}
                  onClick={() => setStyle(s.id)}>
                  <i className={"ti " + s.icon} style={{marginRight: 4, fontSize: 13}}></i>
                  {s.label}
                </button>
              ))}
            </div>
          </div>

          {isBlankStyle ? (
            <div className="field form-row">
              <label><i className="ti ti-file-text"></i>Your draft <span style={{color:"var(--color-danger)"}}>*</span></label>
              <textarea
                className="textarea textarea-large"
                value={sourceContent}
                onChange={e => setSourceContent(e.target.value)}
                placeholder="Paste your full LinkedIn post, article excerpt, or rough notes here. The AI will polish it for impact — keeping your structure, facts, and voice."
              />
              <p className="field-hint" style={{ marginTop: 6, fontSize: 12, color: "var(--color-text-secondary)" }}>
                Long-form posts welcome — numbered lists and sections preserved; hashtags generated per platform below each post.
              </p>
            </div>
          ) : (
            <>
              <div className="field form-row">
                <label><i className="ti ti-message-2"></i>Post topic {!sourceContent.trim() && <span style={{color:"var(--color-danger)"}}>*</span>}</label>
                <textarea className="textarea" value={topic} onChange={e => setTopic(e.target.value)}
                  placeholder="What is this post about? One sentence or a few keywords is fine." />
              </div>
              <div className="field form-row">
                <label><i className="ti ti-pencil"></i>Your draft (optional)</label>
                <textarea
                  className="textarea"
                  style={{ minHeight: 140 }}
                  value={sourceContent}
                  onChange={e => setSourceContent(e.target.value)}
                  placeholder="Paste your own text and the AI will improve it and fit it into the selected style."
                />
              </div>
            </>
          )}

          <div className="field form-row">
            <label><i className="ti ti-microphone-2"></i>Tone</label>
            <div className="chip-group">
              {window.TONES.map(t => (
                <button key={t.id} type="button"
                  className={"chip" + (tone === t.id ? " is-active" : "")}
                  onClick={() => setTone(t.id)}>
                  {t.label}
                </button>
              ))}
            </div>
          </div>
        </div>

        <div className="generate-row">
          <span className="generate-hint">
            <i className="ti ti-info-circle"></i>
            {isBlankStyle || sourceContent.trim()
              ? " AI improves your draft and adapts it for each platform."
              : " Posts are saved locally so the AI doesn't repeat itself."}
          </span>
          <button className="btn btn-primary" onClick={generate} disabled={!canGenerate}>
            {generating
              ? <><span className="spinner"></span> {sourceContent.trim() ? "Improving…" : "Generating…"}</>
              : <><i className="ti ti-sparkles"></i> {sourceContent.trim() ? "Improve & generate" : "Generate all platforms"}</>}
          </button>
        </div>

        {error && <div className="error-banner"><i className="ti ti-alert-triangle"></i> {error}</div>}
      </div>

      {generating && !current && (
        <div className="loading-placeholder">
          <span className="spinner"></span>
          <div className="loading-text">Drafting LinkedIn, X, Facebook, Pinterest &amp; Reddit…</div>
          <div className="loading-sub">checking {memoryCount} past post{memoryCount === 1 ? "" : "s"} for repeats</div>
        </div>
      )}

      {current && (
        <TextResult
          post={current}
          generating={generating}
          copyText={copyText}
          onRegenerate={generate}
        />
      )}
    </>
  );
}

function MemoryBar({ count, label }) {
  const dots = Array.from({ length: 10 }, (_, i) => i < count);
  return (
    <div className="memory-bar">
      <i className="ti ti-brain"></i>
      <span>
        Anti-repeat memory active: <strong>{count}</strong> of last 10 {label}{count === 1 ? "" : "s"} loaded into the prompt.
      </span>
      <span className="memory-dots" aria-hidden="true">
        {dots.map((on, i) => (
          <span key={i} className={"memory-dot" + (on ? " is-filled" : "")}></span>
        ))}
      </span>
    </div>
  );
}

function SocialPostsPanel({ post, copyText, headerTags, headerActions }) {
  const [activePlatform, setActivePlatform] = useStateA("linkedin");
  if (!post.platforms) return null;
  const body = window.getPlatformText(post, activePlatform);
  const platformTags = window.getPlatformTags(post, activePlatform);
  const tagsFormatted = window.formatHashtags(platformTags);
  const bodyWithTags = window.formatPlatformTextWithTags(post, activePlatform);
  const m = window.postMetrics(body);
  const mWithTags = window.postMetrics(bodyWithTags);
  const limit = window.PLATFORM_LIMITS[activePlatform];
  const overLimit = limit && m.chars > limit;
  const overLimitWithTags = limit && mWithTags.chars > limit;
  const activePlatformLabel = (window.PLATFORMS.find(p => p.id === activePlatform) || {}).label || "Platform";

  return (
    <>
      <div className="result-card" style={{ marginTop: headerTags || headerActions ? 20 : 0 }}>
        <div className="result-head">
          <div className="result-head-left">
            {headerTags || (
              <div className="tag-row">
                <span className="tag tag-accent"><i className="ti ti-share"></i> Social posts</span>
              </div>
            )}
          </div>
          <div className="result-actions">
            {headerActions}
            <button
              className="btn btn-ghost btn-small"
              onClick={() => openPlatformComposer(activePlatform, !overLimitWithTags && bodyWithTags ? bodyWithTags : body)}
              title={`Open ${activePlatformLabel}`}
            >
              <i className="ti ti-external-link"></i> Open {activePlatformLabel}
            </button>
            <button className="btn btn-ghost btn-small" onClick={() => copyText(window.formatAllPlatforms(post), "social_all")}>
              <i className="ti ti-copy"></i> Copy all
            </button>
            <button className="btn btn-ghost btn-small" onClick={() => copyText(bodyWithTags, "social_platform")} disabled={!bodyWithTags} title="Post text plus hashtags">
              <i className="ti ti-copy"></i> Copy + tags
            </button>
            <button className="btn btn-ghost btn-small" onClick={() => copyText(body, "social_platform")} disabled={!body}>
              <i className="ti ti-copy"></i> Copy text
            </button>
          </div>
        </div>

        <div className="platform-tabs">
          {window.PLATFORMS.map(p => (
            <button
              key={p.id}
              type="button"
              className={"platform-tab" + (activePlatform === p.id ? " is-active" : "")}
              onClick={() => setActivePlatform(p.id)}
            >
              <i className={"ti " + p.icon}></i>
              {p.label}
            </button>
          ))}
        </div>

        <div className="result-body">
          {body || "(No content for this platform)"}
          {platformTags.length > 0 && (
            <>
              {"\n\n"}
              <span className="result-body-tags">{tagsFormatted}</span>
            </>
          )}
        </div>

        <div className="hashtag-block">
          <div className="hashtag-head">
            <span className="hashtag-label"><i className="ti ti-hash"></i> Tags for {activePlatformLabel}</span>
            <button
              type="button"
              className="btn btn-ghost btn-small"
              onClick={() => copyText(tagsFormatted, "social_tags")}
              disabled={!platformTags.length}
              title="Copy hashtags only"
            >
              <i className="ti ti-copy"></i> Copy tags
            </button>
          </div>
          {platformTags.length > 0 ? (
            <div className="hashtag-row">
              {platformTags.map((tag, i) => (
                <span key={i} className="hashtag-chip">#{tag.replace(/^#+/, "")}</span>
              ))}
            </div>
          ) : (
            <p className="hashtag-empty">No tags on this saved post. Click <strong>Regenerate</strong> to create hashtags for each platform.</p>
          )}
        </div>
      </div>

      <div className="metrics">
        <div className="metric">
          <div className="metric-label"><i className="ti ti-letter-case"></i> Characters</div>
          <div className={"metric-value" + (overLimit ? " is-warn" : "")}>{m.chars.toLocaleString()}{limit ? <span className="unit">/ {limit}</span> : null}</div>
        </div>
        {platformTags.length > 0 && (
          <div className="metric">
            <div className="metric-label"><i className="ti ti-hash"></i> With tags</div>
            <div className={"metric-value" + (overLimitWithTags ? " is-warn" : "")}>{mWithTags.chars.toLocaleString()}{limit ? <span className="unit">/ {limit}</span> : null}</div>
          </div>
        )}
        <div className="metric">
          <div className="metric-label"><i className="ti ti-text-size"></i> Words</div>
          <div className="metric-value">{m.words.toLocaleString()}</div>
        </div>
        <div className="metric">
          <div className="metric-label"><i className="ti ti-clock"></i> Read time</div>
          <div className="metric-value">{m.minutes}<span className="unit">min</span></div>
        </div>
      </div>
    </>
  );
}

function TextResult({ post, generating, copyText, onRegenerate }) {
  const headerTags = (
    <div className="tag-row">
      <span className="tag tag-accent"><i className="ti ti-template"></i>{window.styleLabel(post.style)}</span>
      <span className="tag"><i className="ti ti-microphone-2"></i>{window.toneLabel(post.tone)}</span>
      {post.niche && <span className="tag"><i className="ti ti-briefcase"></i>{post.niche}</span>}
    </div>
  );
  const headerActions = (
    <button className="btn btn-ghost btn-small" onClick={onRegenerate} disabled={generating}>
      {generating ? <span className="spinner" style={{width: 12, height: 12}}></span> : <i className="ti ti-refresh"></i>}
      Regenerate
    </button>
  );
  return <SocialPostsPanel post={post} copyText={copyText} headerTags={headerTags} headerActions={headerActions} />;
}

function buildDemoPlatforms(topic, byline) {
  const suffix = byline ? `\n\n${byline}` : "";
  const slug = String(topic || "demo").replace(/\s+/g, "").slice(0, 24);
  return {
    platforms: {
      linkedin: `No API key? No problem.\n\nHere is a demo LinkedIn post generated from local hardcoded content for topic: "${topic}".\n\nUse this play mode to test templates, PNG export, and GIF export before connecting any model.${suffix}`,
      x: `Demo mode: testing "${topic}" with hardcoded content. Try PNG/GIF export, then connect a model later.${suffix}`,
      facebook: `This is demo mode with hardcoded content for "${topic}". Perfect for trying visual templates and export flow without any API key.${suffix}`,
      pinterest: {
        title: `Demo visual for ${topic}`,
        description: `Hardcoded sample content so you can test templates and export PNG or GIF without AI setup.${byline ? ` ${byline}` : ""}`,
      },
      reddit: {
        title: `Demo mode for ${topic} (no API key)`,
        body: `I am using hardcoded sample content to test visual templates and exports before enabling any AI provider. This is useful for UI and workflow testing.\n\nDo you want this demo mode to include more starter templates?${byline ? `\n\n${byline}` : ""}`,
      },
    },
    tags: {
      linkedin: ["LinkedInTips", "ContentCreation", "AITools", slug || "DemoPost", "SocialMediaMarketing", "CreatorEconomy", "Productivity", "BuildInPublic"],
      x: ["DemoMode", slug || "AITools", "LinkedIn"],
      facebook: ["DemoMode", "ContentCreation", slug || "AITools"],
      pinterest: ["LinkedIn", "Infographic", "AITools", "ContentMarketing", slug || "Demo"],
      reddit: [],
    },
  };
}

function buildStaticDemoData(topic) {
  return {
    title: "Demo Timeline",
    entries: [
      { year: "Step 1", headline: "Pick a visual template", subtext: `Use "${topic}" as your test topic` },
      { year: "Step 2", headline: "Load hardcoded demo data", subtext: "No provider key needed for this mode" },
      { year: "Step 3", headline: "Preview rendered output", subtext: "Check spacing, readability, and structure" },
      { year: "Step 4", headline: "Download PNG instantly", subtext: "Validate local export quality quickly" },
      { year: "Step 5", headline: "Switch to GIF template", subtext: "Try animation-capable layout cards" },
      { year: "Step 6", headline: "Export animated GIF", subtext: "Use frame caps for reliable web export" },
      { year: "Step 7", headline: "Adjust animation speed", subtext: "Tune pacing before final download" },
      { year: "Step 8", headline: "Save in local history", subtext: "Reuse or clone this demo post later" },
      { year: "Step 9", headline: "Connect AI when ready", subtext: "Upgrade from demo to real generation" },
    ],
  };
}

function buildAnimatedDemoData(topic) {
  return {
    seriesTitle: `Demo Flow: ${topic}`,
    accent: "blue",
    slides: [
      { slideNumber: "01", headline: "Pick an animated template", body: "Select a GIF-ready card from the template grid.", icon: "layout-grid", color: "blue" },
      { slideNumber: "02", headline: "Use hardcoded sample data", body: "No API calls are made in this demo path.", icon: "shield-check", color: "green" },
      { slideNumber: "03", headline: "Preview each frame", body: "The preview cycles through frame-by-frame content automatically.", icon: "player-play", color: "purple" },
      { slideNumber: "04", headline: "Tune animation speed", body: "Use the slider to slow down or speed up the playback.", icon: "clock", color: "orange" },
      { slideNumber: "05", headline: "Export GIF safely", body: "The app applies frame caps on hosted mode for reliability.", icon: "download", color: "red" },
      { slideNumber: "06", headline: "Also export PNG", body: "You can always save a static PNG from the same visual.", icon: "photo", color: "teal" },
      { slideNumber: "07", headline: "Store in history", body: "Demo outputs are saved locally like normal generations.", icon: "history", color: "indigo" },
      { slideNumber: "08", headline: "Connect AI later", body: "When ready, switch provider and generate fresh content.", icon: "plug", color: "pink" },
    ],
    cta: "Try demo mode, then switch to AI",
  };
}

/* ================================================================
   VISUAL TAB
   ================================================================ */
function VisualTab({ posts, setPosts, textPosts, pickedLayoutSignature, onClearPickedLayout, copyText, showToast, settings }) {
  const templates = window.TEMPLATES || [];
  const defaultTemplateId = templates[0]?.id || "blank-canvas";
  const [templateId, setTemplateId] = useStateA(defaultTemplateId);
  const [topic,    setTopic]    = useStateA("");
  const [sourceContent, setSourceContent] = useStateA("");
  const [niche,    setNiche]    = useStateA("AI engineering");
  const [audience, setAudience] = useStateA("Software engineers");
  const [byline,   setByline]   = useStateA(settings.authorByline || "");
  const [avatarDataUrl, setAvatarDataUrl] = useStateA(settings.authorAvatarDataUrl || "");
  const [layoutFamilyId, setLayoutFamilyId] = useStateA("portrait-4x5-static");
  const [templateFilter, setTemplateFilter] = useStateA("all");
  const [socialWarn, setSocialWarn] = useStateA(null);

  const [generating, setGenerating] = useStateA(false);
  const [current,    setCurrent]    = useStateA(null);
  const [error,      setError]      = useStateA(null);

  const memoryCount = Math.min(posts.length, 10);
  const recentVisual = useMemoA(() => posts.slice(0, 10), [posts]);
  const recentText = useMemoA(() => textPosts.slice(0, 10), [textPosts]);
  const isBlankTemplate = templateId === "blank-canvas";
  const canGenerate = (isBlankTemplate
    ? sourceContent.trim().length > 0
    : (topic.trim().length > 0 || sourceContent.trim().length > 0)) && !generating;

  const renderRef = useRefA(null);

  const [exportingGif, setExportingGif] = useStateA(false);
  const [gifProgress, setGifProgress] = useStateA(null);
  const [animationSpeed, setAnimationSpeed] = useStateA(1);

  const template = templates.find(t => t.id === templateId) || templates[0];
  const presets = window.VISUAL_RESOURCE_PRESETS || [];
  const layoutFamilies = window.VISUAL_LAYOUT_FAMILIES || [];
  const selectedLayout = layoutFamilies.find(l => l.id === layoutFamilyId) || null;

  const filteredTemplates = useMemoA(() => {
    if (templateFilter === "animated") return templates.filter(t => t.animated);
    if (templateFilter === "comparison") return templates.filter(t => t.category === "comparison");
    return templates;
  }, [templates, templateFilter]);

  if (!templates.length) {
    return (
      <>
        <MemoryBar count={memoryCount} label="visual post" />
        <div className="error-banner">
          <i className="ti ti-alert-triangle"></i>
          Visual templates failed to load. Hard-refresh the page (Ctrl+Shift+R).
        </div>
      </>
    );
  }

  useEffectA(() => {
    if (!pickedLayoutSignature) return;
    const mapped = templateIdForSignature(pickedLayoutSignature);
    if (mapped && window.TEMPLATES.find(t => t.id === mapped)) {
      setTemplateId(mapped);
    }
  }, [pickedLayoutSignature]);

  useEffectA(() => {
    setByline(prev => prev || settings.authorByline || "");
    setAvatarDataUrl(prev => prev || settings.authorAvatarDataUrl || "");
  }, [settings.authorByline, settings.authorAvatarDataUrl]);
  const selectedSignatureGuidance = pickedLayoutSignature
    ? `Target this discovered layout signature: ${pickedLayoutSignature.width}x${pickedLayoutSignature.height} (${pickedLayoutSignature.ext}). ${
        pickedLayoutSignature.ext === ".gif"
          ? `Structure content as frame-friendly beats across ${pickedLayoutSignature.frames_or_pages} frames.`
          : pickedLayoutSignature.ext === ".pdf"
            ? `Structure content as page-wise sections across ${pickedLayoutSignature.frames_or_pages} pages.`
            : "Design as a clear single static composition."
      }`
    : "";

  function applyPreset(preset) {
    setTopic(preset.topic || "");
    setNiche(preset.niche || "");
    setAudience(preset.audience || "");
    if (preset.byline) setByline(preset.byline);
    if (preset.id && preset.id.includes("vs")) {
      setTemplateId(pickedLayoutSignature?.ext === ".gif" ? "comparison-reveal" : "split-comparison");
    }
  }

  async function onAvatarChange(e) {
    const file = e.target.files?.[0];
    if (!file) return;
    if (!file.type || !file.type.startsWith("image/")) {
      showToast("Please select an image file");
      return;
    }
    if (file.size > 4 * 1024 * 1024) {
      showToast("Image too large (max 4MB)");
      return;
    }
    try {
      const dataUrl = await new Promise((resolve, reject) => {
        const reader = new FileReader();
        reader.onload = () => resolve(reader.result);
        reader.onerror = () => reject(new Error("Failed to read image"));
        reader.readAsDataURL(file);
      });
      setAvatarDataUrl(String(dataUrl || ""));
      showToast("Avatar added");
    } catch {
      showToast("Avatar upload failed");
    } finally {
      e.target.value = "";
    }
  }

  async function generate() {
    if (!canGenerate) return;
    setError(null); setSocialWarn(null); setGenerating(true);
    try {
      const bylineForRun = (byline || settings.authorByline || "").trim();
      const avatarForRun = avatarDataUrl || settings.authorAvatarDataUrl || "";
      const effectiveTopic = topic.trim() || sourceContent.trim().split("\n")[0].slice(0, 120);
      const draftText = sourceContent.trim() || undefined;
      const userPrompt = window.buildVisualUserPrompt({
        template,
        topic: effectiveTopic,
        niche,
        audience,
        byline: bylineForRun,
        layoutGuidance: selectedSignatureGuidance || (selectedLayout ? selectedLayout.guidance : ""),
        recentVisualPosts: recentVisual,
        sourceContent: draftText,
      });
      const socialExtra = draftText
        ? `This copy accompanies a "${template.name}" visual on the same topic. The user's original draft was provided — preserve voice and key points.`
        : `This copy accompanies a "${template.name}" infographic on the same topic. Tease or reference the visual naturally where it fits each platform (e.g. "see the graphic", "full breakdown in the image") without being repetitive.`;

      const draftLen = draftText ? draftText.length : 0;
      const visualMaxTokens = draftLen > 2000 ? 4500 : draftLen > 800 ? 3500 : 3000;

      const [visualSettled, socialSettled] = await Promise.allSettled([
        window.callAI({
          settings,
          system: window.buildVisualSystemPrompt(),
          user: userPrompt,
          maxTokens: visualMaxTokens,
        }),
        window.generatePlatformPosts({
          settings,
          niche,
          topic: effectiveTopic,
          style: isBlankTemplate ? "blank" : "tips-lessons",
          tone: "professional",
          audience,
          recentPosts: recentText,
          extraNotes: socialExtra,
          sourceContent: draftText,
        }),
      ]);

      if (visualSettled.status === "rejected") {
        throw visualSettled.reason;
      }
      const data = window.extractJson(visualSettled.value);
      if (!data) throw new Error("Couldn't parse JSON from the model. Try regenerating.");
      const visualData = {
        ...data,
        byline: data.byline || bylineForRun || "",
        avatarDataUrl: avatarForRun || "",
      };

      let platforms = null;
      let tags = null;
      if (socialSettled.status === "fulfilled") {
        platforms = addBylineToPlatforms(socialSettled.value.platforms, bylineForRun);
        tags = socialSettled.value.tags || null;
      } else {
        setSocialWarn(socialSettled.reason?.message || "Social posts could not be generated.");
      }

      const entry = {
        id: "v_" + Date.now().toString(36) + Math.random().toString(36).slice(2, 6),
        kind: "visual",
        templateId: template.id,
        templateName: template.name,
        topic: effectiveTopic, niche, audience, byline: bylineForRun,
        sourceContent: draftText,
        avatarDataUrl: avatarForRun,
        data: visualData,
        platforms,
        tags,
        layoutFrameCount: pickedLayoutSignature?.ext === ".gif" ? pickedLayoutSignature.frames_or_pages : null,
        createdAt: Date.now(),
      };
      setCurrent(entry);
      setPosts(prev => [entry, ...prev]);
      window.trackAppEvent?.("visual_post_generated", { templateId: template.id });
    } catch (err) {
      console.error(err);
      window.trackAppEvent?.("visual_generation_failed", { templateId: template.id });
      setError(err.message || "Generation failed. Try again.");
    } finally {
      setGenerating(false);
    }
  }

  function generateDemo(templateKind) {
    const bylineForRun = (byline || settings.authorByline || "").trim();
    const avatarForRun = avatarDataUrl || settings.authorAvatarDataUrl || "";
    const useAnimated = templateKind === "animated";
    const demoTemplateId = useAnimated ? "story-slides" : "whiteboard-timeline";
    const demoTemplate = templates.find(t => t.id === demoTemplateId);
    if (!demoTemplate) {
      showToast("Demo template not found");
      return;
    }
    const safeTopic = (topic || "").trim() || "LinkedIn content workflow";
    const visualDataBase = useAnimated ? buildAnimatedDemoData(safeTopic) : buildStaticDemoData(safeTopic);
    const visualData = {
      ...visualDataBase,
      byline: visualDataBase.byline || bylineForRun || "",
      avatarDataUrl: avatarForRun || "",
    };
    const demoSocial = buildDemoPlatforms(safeTopic, bylineForRun);
    const entry = {
      id: "v_" + Date.now().toString(36) + Math.random().toString(36).slice(2, 6),
      kind: "visual",
      templateId: demoTemplate.id,
      templateName: demoTemplate.name,
      topic: safeTopic,
      niche,
      audience,
      byline: bylineForRun,
      avatarDataUrl: avatarForRun,
      data: visualData,
      platforms: addBylineToPlatforms(demoSocial.platforms, bylineForRun),
      tags: demoSocial.tags || null,
      layoutFrameCount: useAnimated ? (visualData.slides?.length || 8) : null,
      createdAt: Date.now(),
    };
    setTemplateId(demoTemplate.id);
    setCurrent(entry);
    setPosts(prev => [entry, ...prev]);
    setError(null);
    setSocialWarn(null);
    showToast(useAnimated ? "Demo GIF content loaded" : "Demo PNG content loaded");
    window.trackAppEvent?.("visual_demo_loaded", { templateId: demoTemplate.id });
  }

  async function downloadPng() {
    if (!renderRef.current) return;
    try {
      const ts = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
      await window.downloadNodeAsPng(renderRef.current, `post-${ts}.png`);
      window.trackAppEvent?.("png_exported", { templateId: template?.id });
      showToast("PNG saved");
    } catch (err) {
      console.error(err);
      showToast("Download failed");
    }
  }

  async function downloadGif() {
    if (!current || !template?.animated) return;
    setExportingGif(true);
    setGifProgress({ label: "Starting…" });
    try {
      const ts = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
      const layoutFrames = pickedLayoutSignature?.ext === ".gif" ? pickedLayoutSignature.frames_or_pages : null;
      const cap = window.getGifExportFrameCap();
      const maxFrames = layoutFrames ? Math.min(layoutFrames, cap) : null;
      const meta = await window.downloadTemplateAsGif({
        template,
        data: current.data,
        filename: `post-${ts}.gif`,
        maxFrames: maxFrames || current.layoutFrameCount,
        speed: animationSpeed,
        onProgress: (p) => {
          if (p.phase === "capture") {
            setGifProgress({ label: `Capturing ${p.frame}/${p.total}` });
          } else if (p.phase === "encode") {
            setGifProgress({ label: "Encoding GIF…" });
          }
          if (p.capped && p.frame === 1) {
            showToast(`Web export uses ${p.total} frames (not ${p.requested}) so it finishes reliably`);
          }
        },
      });
      window.trackAppEvent?.("gif_exported", { templateId: template?.id, frames: meta?.count });
      showToast(
        meta?.capped
          ? `GIF saved (${meta.count} frames; capped from ${meta.requested} on web)`
          : "GIF saved"
      );
    } catch (err) {
      console.error(err);
      showToast(err.message || "GIF export failed");
    } finally {
      setExportingGif(false);
      setGifProgress(null);
    }
  }

  const gifFrameCap = window.getGifExportFrameCap();
  const hostedGifHint = !window.isLocalDevHost()
    ? `GIF export on the web app is limited to ${gifFrameCap} frames (use PNG for full quality). Scroll Reel and other long animations work best on localhost.`
    : null;

  return (
    <>
      <MemoryBar count={memoryCount} label="visual post" />

      <div className="card">
        <p className="eyebrow">Template</p>
        <h2 className="card-title">Pick a layout</h2>
        <p className="card-sub">Each template is a fixed structure — the AI fills it with content for your topic. Filter <span className="tag" style={{fontSize:10}}>GIF</span> templates for animated posts like those in linkedInresources.</p>

        <div className="chip-group" style={{ marginBottom: 14 }}>
          {[
            { id: "all", label: "All templates" },
            { id: "animated", label: "Animated GIF" },
            { id: "comparison", label: "Comparison" },
          ].map(f => (
            <button
              key={f.id}
              type="button"
              className={"chip" + (templateFilter === f.id ? " is-active" : "")}
              onClick={() => setTemplateFilter(f.id)}
            >
              {f.label}
            </button>
          ))}
        </div>

        <div className="template-grid">
          {filteredTemplates.map(t => {
            const Thumb = t.Thumbnail;
            return (
            <button key={t.id} type="button"
              className={"template-card" + (templateId === t.id ? " is-active" : "")}
              onClick={() => setTemplateId(t.id)}>
              <div className="template-thumb">
                <Thumb/>
              </div>
              <div className="template-meta">
                <div className="template-name">
                  {t.name}
                  {t.category === "comparison" && <span className="tag tag-accent" style={{ marginLeft: 6, fontSize: 9, verticalAlign: "middle" }}>Compare</span>}
                  {t.animated && <span className="tag" style={{ marginLeft: 6, fontSize: 9, verticalAlign: "middle" }}>GIF</span>}
                </div>
                <div className="template-desc">{t.description}</div>
              </div>
            </button>
            );
          })}
        </div>
      </div>

      {template?.animated && (
        <div className="card">
          <p className="eyebrow">Animation</p>
          <h2 className="card-title">Speed control</h2>
          <p className="card-sub">Adjust how fast the preview plays and how long each frame lasts in the exported GIF. Move left to slow down.</p>
          <AnimationSpeedControl
            template={template}
            data={current?.data}
            maxFrames={pickedLayoutSignature?.ext === ".gif" ? pickedLayoutSignature.frames_or_pages : (current?.layoutFrameCount || 0)}
            speed={animationSpeed}
            onSpeedChange={setAnimationSpeed}
          />
        </div>
      )}

      <div className="card">
        <p className="eyebrow">Resource pack</p>
        <h2 className="card-title">Apply analyzed visual topics</h2>
        <p className="card-sub">Quick-fill inputs from your `linkedInresources` visual research so you can generate faster.</p>

        <div className="chip-group">
          {presets.map(preset => (
            <button
              key={preset.id}
              type="button"
              className="chip"
              onClick={() => applyPreset(preset)}
              title={preset.topic}
            >
              <i className="ti ti-bulb" style={{marginRight: 4, fontSize: 13}}></i>
              {preset.label}
            </button>
          ))}
        </div>
      </div>

      {window.isLayoutExplorerEnabled() && (
        <div className="card">
          <p className="eyebrow">Picked layout</p>
          <h2 className="card-title">Layout selected from explorer</h2>
          {!pickedLayoutSignature ? (
            <p className="card-sub">No specific signature picked yet. Use the Layout explorer tab and click "Use in Visual tab".</p>
          ) : (
            <>
              <div className="template-thumb" style={{ maxWidth: 220, marginBottom: 10, padding: 10 }}>
                <LayoutSignatureThumb row={pickedLayoutSignature} />
              </div>
              <div className="tag-row" style={{display: "flex", gap: 8, flexWrap: "wrap", marginBottom: 10}}>
                <span className="tag tag-accent">{pickedLayoutSignature.ext}</span>
                <span className="tag">{pickedLayoutSignature.width} x {pickedLayoutSignature.height}</span>
                <span className="tag">
                  {pickedLayoutSignature.ext === ".gif"
                    ? `${pickedLayoutSignature.frames_or_pages} frames`
                    : pickedLayoutSignature.ext === ".pdf"
                      ? `${pickedLayoutSignature.frames_or_pages} pages`
                      : "static"}
                </span>
                <span className="tag">{pickedLayoutSignature.count} file{pickedLayoutSignature.count === 1 ? "" : "s"}</span>
              </div>
              <p className="card-sub" style={{marginBottom: 12}}>
                Example source: {(pickedLayoutSignature.sample_files || [])[0] || "n/a"}
              </p>
              <button className="btn btn-ghost btn-small" onClick={onClearPickedLayout}>
                <i className="ti ti-x"></i> Clear picked layout
              </button>
            </>
          )}
        </div>
      )}

      <div className="card">
        <p className="eyebrow">Inputs</p>
        <h2 className="card-title">Tell the model what to build</h2>
        <p className="card-sub">Generates the infographic plus LinkedIn, X, Facebook, Pinterest, and Reddit captions in one run. Paste your draft to improve it, or enter a topic to generate from scratch.</p>

        <div className="form-grid">
          <div className="field">
            <label><i className="ti ti-briefcase"></i>Niche / expertise</label>
            <input className="input" value={niche} onChange={e => setNiche(e.target.value)} placeholder="e.g. AI engineering" />
          </div>
          <div className="field">
            <label><i className="ti ti-users"></i>Target audience</label>
            <input className="input" value={audience} onChange={e => setAudience(e.target.value)} placeholder="e.g. Software engineers" />
          </div>

          {isBlankTemplate ? (
            <div className="field form-row">
              <label><i className="ti ti-file-text"></i>Your draft <span style={{color:"var(--color-danger)"}}>*</span></label>
              <textarea
                className="textarea textarea-large"
                value={sourceContent}
                onChange={e => setSourceContent(e.target.value)}
                placeholder="Paste your full LinkedIn post or rough notes. The AI polishes it for impact and exports it as a clean text visual."
              />
              <p className="field-hint" style={{ marginTop: 6, fontSize: 12, color: "var(--color-text-secondary)" }}>
                Long-form posts welcome — numbered lists and sections preserved; hashtags generated per platform below each post.
              </p>
            </div>
          ) : (
            <>
              <div className="field form-row">
                <label><i className="ti ti-message-2"></i>Topic {!sourceContent.trim() && <span style={{color:"var(--color-danger)"}}>*</span>}</label>
                <textarea className="textarea" value={topic} onChange={e => setTopic(e.target.value)}
                  placeholder="What is this infographic about? e.g. 'My software engineering learning journey from 2010 to today'" />
              </div>
              <div className="field form-row">
                <label><i className="ti ti-pencil"></i>Your draft (optional)</label>
                <textarea
                  className="textarea"
                  style={{ minHeight: 140 }}
                  value={sourceContent}
                  onChange={e => setSourceContent(e.target.value)}
                  placeholder="Paste your own text and the AI will improve it and fit it into the selected template."
                />
              </div>
            </>
          )}
          <div className="field form-row">
            <label><i className="ti ti-signature"></i>Byline (optional)</label>
            <input className="input" value={byline} onChange={e => setByline(e.target.value)} placeholder="e.g. By Your Name · @handle" />
            <div style={{ display: "flex", alignItems: "center", gap: 10, marginTop: 8, flexWrap: "wrap" }}>
              <label className="btn btn-ghost btn-small" style={{ cursor: "pointer" }}>
                <i className="ti ti-photo"></i> Upload avatar
                <input type="file" accept="image/*" onChange={onAvatarChange} style={{ display: "none" }} />
              </label>
              {avatarDataUrl && (
                <>
                  <img
                    src={avatarDataUrl}
                    alt="Avatar preview"
                    style={{ width: 28, height: 28, borderRadius: "50%", border: "1px solid var(--color-border-tertiary)", objectFit: "cover" }}
                  />
                  <button type="button" className="btn btn-ghost btn-small" onClick={() => setAvatarDataUrl("")}>
                    <i className="ti ti-x"></i> Remove
                  </button>
                </>
              )}
            </div>
          </div>
          <div className="field form-row">
            <label><i className="ti ti-layout-grid"></i>Layout family</label>
            <select
              className="select"
              value={layoutFamilyId}
              onChange={e => setLayoutFamilyId(e.target.value)}
              title="Select analyzed layout family"
            >
              {layoutFamilies.map(layout => (
                <option key={layout.id} value={layout.id}>
                  {layout.label}
                </option>
              ))}
            </select>
            {selectedLayout && (
              <span className="generate-hint" style={{marginTop: 2}}>
                <i className="ti ti-info-circle"></i>
                {selectedLayout.guidance}
              </span>
            )}
          </div>
        </div>

        <div className="generate-row">
          <span className="generate-hint">
            <i className="ti ti-info-circle"></i> Infographic PNG + all four social post captions.
          </span>
          <div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
            <button className="btn btn-primary" onClick={generate} disabled={!canGenerate}>
              {generating
                ? <><span className="spinner"></span> {sourceContent.trim() ? "Improving…" : "Generating…"}</>
                : <><i className="ti ti-wand"></i> {sourceContent.trim() ? "Improve & generate" : "Generate visual + social"}</>}
            </button>
            <button className="btn btn-ghost" type="button" onClick={() => generateDemo("static")} disabled={generating}>
              <i className="ti ti-photo"></i> Demo PNG
            </button>
            <button className="btn btn-ghost" type="button" onClick={() => generateDemo("animated")} disabled={generating}>
              <i className="ti ti-player-play"></i> Demo GIF
            </button>
          </div>
        </div>
        <p className="generate-hint" style={{ marginTop: 8 }}>
          <i className="ti ti-bolt"></i> Demo buttons use hardcoded content (no API key required).
        </p>

        {error && <div className="error-banner"><i className="ti ti-alert-triangle"></i> {error}</div>}
        {socialWarn && !error && <div className="error-banner" style={{background: "var(--color-accent-soft)", color: "var(--color-text-secondary)"}}><i className="ti ti-info-circle"></i> Visual saved — {socialWarn}</div>}
      </div>

      {generating && !current && (
        <div className="loading-placeholder">
          <span className="spinner"></span>
          <div className="loading-text">Designing infographic &amp; social posts…</div>
          <div className="loading-sub">visual layout · LinkedIn · X · Facebook · Pinterest · Reddit</div>
        </div>
      )}

      {current && (
        <VisualResult
          post={current}
          generating={generating}
          exportingGif={exportingGif}
          gifProgress={gifProgress}
          hostedGifHint={hostedGifHint}
          layoutFrameCount={pickedLayoutSignature?.ext === ".gif" ? pickedLayoutSignature.frames_or_pages : current.layoutFrameCount}
          animationSpeed={animationSpeed}
          onAnimationSpeedChange={setAnimationSpeed}
          renderRef={renderRef}
          copyText={copyText}
          onRegenerate={generate}
          onDownload={downloadPng}
          onDownloadGif={downloadGif}
        />
      )}
    </>
  );
}

function AnimationSpeedControl({ template, data, maxFrames, speed, onSpeedChange }) {
  const frameCount = useMemoA(() => {
    if (!template?.getFrameCount) return 0;
    if (data) return template.getFrameCount(data, { maxFrames: maxFrames || 0 });
    return template.getFrameCount({}, { maxFrames: maxFrames || 0 });
  }, [template, data, maxFrames]);

  const delayMs = window.computeFrameDelay({ template, frameCount: Math.max(frameCount, 1), speed });
  const totalSec = frameCount > 0 ? ((delayMs * frameCount) / 1000).toFixed(1) : "—";
  const speedLabel = speed <= 0.5 ? "Very slow" : speed < 1 ? "Slow" : speed === 1 ? "Normal" : speed <= 1.5 ? "Fast" : "Very fast";

  return (
    <div className="field">
      <label>
        <i className="ti ti-clock"></i>
        Speed — {speedLabel}
        <span style={{ marginLeft: 8, fontFamily: "var(--font-mono)", fontSize: 11, color: "var(--color-text-tertiary)" }}>
          {delayMs}ms/frame · ~{totalSec}s total{frameCount > 0 ? ` (${frameCount} frames)` : ""}
        </span>
      </label>
      <input
        type="range"
        className="input"
        min={0.25}
        max={3}
        step={0.25}
        value={speed}
        onChange={e => onSpeedChange(Number(e.target.value))}
        title="Animation speed"
        aria-label="Animation speed"
        style={{ padding: "4px 0", cursor: "pointer" }}
      />
      <div style={{ display: "flex", justifyContent: "space-between", fontSize: 11, color: "var(--color-text-tertiary)", marginTop: 4 }}>
        <span>Slower</span>
        <span>Normal</span>
        <span>Faster</span>
      </div>
    </div>
  );
}

function AnimatedPreviewPlayer({ template, data, maxFrames, speed }) {
  const frameCount = useMemoA(
    () => template.getFrameCount(data, { maxFrames: maxFrames || 0 }),
    [template, data, maxFrames]
  );
  const [frame, setFrame] = useStateA(0);
  const RenderFrame = template.RenderFrame;
  const delayMs = window.computeFrameDelay({ template, frameCount: Math.max(frameCount, 1), speed: speed || 1 });

  useEffectA(() => {
    if (!template.animated || frameCount < 2) return;
    const id = setInterval(() => setFrame(f => (f + 1) % frameCount), delayMs);
    return () => clearInterval(id);
  }, [template, frameCount, delayMs]);

  if (!RenderFrame) return null;
  return (
    <ExportFrame width={template.width} height={template.height} byline={data?.byline} avatarDataUrl={data?.avatarDataUrl}>
      <RenderFrame data={data} frameIndex={frame} frameCount={frameCount} />
    </ExportFrame>
  );
}

function VisualResult({ post, generating, exportingGif, gifProgress, hostedGifHint, layoutFrameCount, animationSpeed, onAnimationSpeedChange, renderRef, copyText, onRegenerate, onDownload, onDownloadGif }) {
  const template = (window.TEMPLATES || []).find(t => t.id === post.templateId);
  if (!template) return null;
  const TemplateRender = template.Render;
  const isAnimated = !!template.animated;
  const maxFrames = layoutFrameCount || post.layoutFrameCount || 0;
  const headerTags = (
    <div className="tag-row">
      <span className="tag tag-accent"><i className="ti ti-layout-collage"></i>{template.name}</span>
      {isAnimated && <span className="tag"><i className="ti ti-player-play"></i>Animated preview</span>}
      {maxFrames > 0 && <span className="tag">{maxFrames} frames</span>}
      {post.niche && <span className="tag"><i className="ti ti-briefcase"></i>{post.niche}</span>}
      {post.platforms && <span className="tag tag-accent"><i className="ti ti-share"></i>4 platforms</span>}
    </div>
  );
  const headerActions = (
    <>
      <button className="btn btn-ghost btn-small" onClick={onDownload}>
        <i className="ti ti-download"></i> PNG
      </button>
      {isAnimated && onDownloadGif && (
        <button className="btn btn-primary btn-small" onClick={onDownloadGif} disabled={exportingGif || generating}>
          {exportingGif ? (
            <><span className="spinner" style={{width: 12, height: 12}}></span> {gifProgress?.label || "Building GIF…"}</>
          ) : (
            <><i className="ti ti-player-play"></i> Download GIF</>
          )}
        </button>
      )}
      <button className="btn btn-ghost btn-small" onClick={onRegenerate} disabled={generating || exportingGif}>
        {generating ? <span className="spinner" style={{width: 12, height: 12}}></span> : <i className="ti ti-refresh"></i>}
        Regenerate all
      </button>
    </>
  );
  return (
    <>
      <div className="result-card">
        <div className="result-head">
          <div className="result-head-left">{headerTags}</div>
          <div className="result-actions">{headerActions}</div>
        </div>
        <div className="visual-stage">
          <ScaledStage width={template.width} height={template.height} maxWidth={820}>
            {isAnimated ? (
              <AnimatedPreviewPlayer template={template} data={post.data} maxFrames={maxFrames} speed={animationSpeed} />
            ) : (
              <div ref={renderRef}>
                <ExportFrame
                  width={template.width}
                  height={template.height}
                  byline={post.byline || post.data?.byline}
                  avatarDataUrl={post.avatarDataUrl || post.data?.avatarDataUrl}
                >
                  <TemplateRender data={post.data}/>
                </ExportFrame>
              </div>
            )}
          </ScaledStage>
          {isAnimated && onAnimationSpeedChange && (
            <div style={{ padding: "16px 24px 0", maxWidth: 420, margin: "0 auto" }}>
              <AnimationSpeedControl
                template={template}
                data={post.data}
                maxFrames={maxFrames}
                speed={animationSpeed}
                onSpeedChange={onAnimationSpeedChange}
              />
              {hostedGifHint && (
                <p className="generate-hint" style={{ marginTop: 10, lineHeight: 1.5 }}>
                  <i className="ti ti-info-circle"></i> {hostedGifHint}
                </p>
              )}
            </div>
          )}
          {isAnimated && (
            <div ref={renderRef} style={{ position: "fixed", left: -99999, top: 0, pointerEvents: "none" }}>
              <ExportFrame
                width={template.width}
                height={template.height}
                byline={post.byline || post.data?.byline}
                avatarDataUrl={post.avatarDataUrl || post.data?.avatarDataUrl}
              >
                <TemplateRender data={post.data}/>
              </ExportFrame>
            </div>
          )}
        </div>
      </div>
      {post.platforms && (
        <SocialPostsPanel
          post={post}
          copyText={copyText}
          headerTags={
            <div className="tag-row">
              <span className="tag tag-accent"><i className="ti ti-share"></i>Captions for your visual</span>
            </div>
          }
        />
      )}
    </>
  );
}

/* Scale a fixed-size element to fit a container width. */
function ScaledStage({ width, height, maxWidth, children }) {
  const wrapRef = useRefA(null);
  const [scale, setScale] = useStateA(1);

  useEffectA(() => {
    function measure() {
      if (!wrapRef.current) return;
      const w = Math.min(wrapRef.current.offsetWidth, maxWidth || width);
      setScale(w / width);
    }
    measure();
    window.addEventListener("resize", measure);
    return () => window.removeEventListener("resize", measure);
  }, [width, maxWidth]);

  return (
    <div ref={wrapRef} style={{ width: "100%", display:"flex", justifyContent:"center" }}>
      <div style={{ width: width * scale, height: height * scale, position:"relative" }}>
        <div style={{
          position:"absolute", top:0, left:0,
          transform: `scale(${scale})`, transformOrigin: "top left",
          width, height,
        }}>
          {children}
        </div>
      </div>
    </div>
  );
}

/* ================================================================
   HISTORY TAB
   ================================================================ */
function HistoryTab({ textPosts, visualPosts, deleteTextPost, deleteVisualPost, clearAll, copyText, showToast }) {
  const all = useMemoA(() => {
    return [...textPosts, ...visualPosts].sort((a, b) => b.createdAt - a.createdAt);
  }, [textPosts, visualPosts]);

  return (
    <>
      <div className="history-head">
        <div>
          <p className="eyebrow">Saved</p>
          <h2 className="card-title" style={{fontSize: 18}}>{all.length} post{all.length === 1 ? "" : "s"} in history</h2>
        </div>
        <button className="btn btn-danger btn-small" onClick={clearAll} disabled={!all.length}>
          <i className="ti ti-trash"></i> Clear all
        </button>
      </div>

      {all.length === 0 ? (
        <div className="history-empty">
          <i className="ti ti-archive"></i>
          <div style={{fontSize: 14, fontWeight: 500, color: "var(--color-text-primary)"}}>No posts yet</div>
          <div style={{fontSize: 13, marginTop: 4}}>Generated posts will appear here automatically.</div>
        </div>
      ) : (
        <div className="history-list">
          {all.map(p => p.kind === "visual" ? (
            <VisualHistoryItem
              key={p.id}
              post={p}
              onCopy={copyText}
              onDelete={() => deleteVisualPost(p.id)}
              showToast={showToast}
            />
          ) : (
            <TextHistoryItem
              key={p.id}
              post={p}
              onCopy={copyText}
              onDelete={() => deleteTextPost(p.id)}
            />
          ))}
        </div>
      )}
    </>
  );
}

function TextHistoryItem({ post, onCopy, onDelete }) {
  const date = new Date(post.createdAt);
  const dateStr = date.toLocaleDateString(undefined, { month: "short", day: "numeric" }) +
    " · " + date.toLocaleTimeString(undefined, { hour: "numeric", minute: "2-digit" });
  const preview = window.getPostPreviewText(post);
  const truncated = preview.length >= 180;
  return (
    <div className="history-item">
      <div>
        <div className="history-meta">
          <span className="tag tag-accent">4 platforms</span>
          <span className="tag tag-accent">{window.styleLabel(post.style)}</span>
          <span className="tag">{window.toneLabel(post.tone)}</span>
          {post.niche && <span className="tag">{post.niche}</span>}
          <span className="history-date">{dateStr}</span>
        </div>
        <div className="history-topic">{post.topic}</div>
        <p className="history-preview">{preview}{truncated ? "…" : ""}</p>
      </div>
      <div className="history-actions">
        <button className="btn btn-ghost btn-small" onClick={() => onCopy(window.formatAllPlatforms(post))}><i className="ti ti-copy"></i> Copy all</button>
        <button className="btn btn-danger btn-small" onClick={onDelete}><i className="ti ti-trash"></i> Delete</button>
      </div>
    </div>
  );
}

function VisualHistoryItem({ post, onCopy, onDelete, showToast }) {
  const ref = useRefA(null);
  const template = (window.TEMPLATES || []).find(t => t.id === post.templateId);
  const date = new Date(post.createdAt);
  const dateStr = date.toLocaleDateString(undefined, { month: "short", day: "numeric" }) +
    " · " + date.toLocaleTimeString(undefined, { hour: "numeric", minute: "2-digit" });

  async function download() {
    if (!ref.current) return;
    try {
      const ts = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
      await window.downloadNodeAsPng(ref.current, `post-${ts}.png`);
      window.trackAppEvent?.("png_exported", { templateId: post.templateId, from: "history" });
      showToast("PNG saved");
    } catch (err) {
      console.error(err);
      showToast("Download failed");
    }
  }

  if (!template) {
    return (
      <div className="history-item">
        <div>
          <div className="history-meta">
            <span className="tag">Visual</span>
            <span className="history-date">{dateStr}</span>
          </div>
          <div className="history-topic">{post.topic}</div>
          <p className="history-preview">Template no longer available.</p>
        </div>
        <div className="history-actions">
          <button className="btn btn-danger btn-small" onClick={onDelete}><i className="ti ti-trash"></i> Delete</button>
        </div>
      </div>
    );
  }

  const TemplateRender = template.Render;

  return (
    <div className="history-item history-visual">
      <div className="history-visual-thumb">
        <div style={{ width: 130, height: Math.round(130 * template.height / template.width), position:"relative", overflow:"hidden", borderRadius: 6, border: "1px solid var(--color-border-tertiary)" }}>
          <div style={{
            position:"absolute", top:0, left:0,
            width: template.width, height: template.height,
            transform: `scale(${130 / template.width})`,
            transformOrigin: "top left",
          }}>
            <div ref={ref}>
              <ExportFrame
                width={template.width}
                height={template.height}
                byline={post.byline || post.data?.byline}
                avatarDataUrl={post.avatarDataUrl || post.data?.avatarDataUrl}
              >
                <TemplateRender data={post.data}/>
              </ExportFrame>
            </div>
          </div>
        </div>
      </div>
      <div className="history-visual-meta">
        <div className="history-meta">
          <span className="tag tag-accent">{template.name}</span>
          {post.platforms && <span className="tag tag-accent">4 platforms</span>}
          {post.niche && <span className="tag">{post.niche}</span>}
          <span className="history-date">{dateStr}</span>
        </div>
        <div className="history-topic">{post.data?.title || post.data?.eyebrow || post.topic}</div>
        <p className="history-preview">{post.platforms ? window.getPostPreviewText(post) : post.topic}</p>
      </div>
      <div className="history-actions">
        {post.platforms && (
          <button className="btn btn-ghost btn-small" onClick={() => onCopy(window.formatAllPlatforms(post))}><i className="ti ti-copy"></i> Copy all</button>
        )}
        <button className="btn btn-ghost btn-small" onClick={download}><i className="ti ti-download"></i> PNG</button>
        <button className="btn btn-danger btn-small" onClick={onDelete}><i className="ti ti-trash"></i> Delete</button>
      </div>
    </div>
  );
}

/* ================================================================
   SETTINGS TAB
   ================================================================ */
function UsageActivityCard({ textPostCount, visualPostCount }) {
  const labels = window.APP_ACTIVITY_EVENT_LABELS || {};
  const [summary, setSummary] = useStateA(() => window.getAppActivitySummary?.() || { counts: {}, recent: [] });

  useEffectA(() => {
    function refresh() {
      setSummary(window.getAppActivitySummary?.() || { counts: {}, recent: [] });
    }
    refresh();
    window.addEventListener("app-activity-updated", refresh);
    return () => window.removeEventListener("app-activity-updated", refresh);
  }, []);

  const textGens = window.countAppActivityEvent?.(summary, "text_post_generated") || 0;
  const visualGens = window.countAppActivityEvent?.(summary, "visual_post_generated") || 0;
  const copies = window.countAppActivityEvent?.(summary, "post_copied") || 0;
  const pngs = window.countAppActivityEvent?.(summary, "png_exported") || 0;
  const gifs = window.countAppActivityEvent?.(summary, "gif_exported") || 0;
  const everGenerated =
    summary.hasGeneratedPost || textPostCount > 0 || visualPostCount > 0 || textGens > 0 || visualGens > 0;

  return (
    <div className="card">
      <p className="eyebrow">Usage</p>
      <h2 className="card-title">Activity on this device</h2>
      <p className="card-sub">
        See whether posts were generated, copied, or exported. Counts are stored only in this browser — not on a server.
      </p>

      <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(140px, 1fr))", gap: 10, marginBottom: 16 }}>
        <div className="template-card" style={{ padding: "12px 14px" }}>
          <div style={{ fontSize: 11, color: "var(--color-text-tertiary)", marginBottom: 4 }}>Generated a post?</div>
          <div style={{ fontSize: 15, fontWeight: 600, color: everGenerated ? "var(--color-accent)" : "var(--color-text-secondary)" }}>
            {everGenerated ? "Yes" : "Not yet"}
          </div>
        </div>
        <div className="template-card" style={{ padding: "12px 14px" }}>
          <div style={{ fontSize: 11, color: "var(--color-text-tertiary)", marginBottom: 4 }}>Text generations</div>
          <div style={{ fontSize: 15, fontWeight: 600 }}>{textGens}</div>
          <div style={{ fontSize: 10, color: "var(--color-text-tertiary)" }}>{textPostCount} saved in history</div>
        </div>
        <div className="template-card" style={{ padding: "12px 14px" }}>
          <div style={{ fontSize: 11, color: "var(--color-text-tertiary)", marginBottom: 4 }}>Visual generations</div>
          <div style={{ fontSize: 15, fontWeight: 600 }}>{visualGens}</div>
          <div style={{ fontSize: 10, color: "var(--color-text-tertiary)" }}>{visualPostCount} saved in history</div>
        </div>
        <div className="template-card" style={{ padding: "12px 14px" }}>
          <div style={{ fontSize: 11, color: "var(--color-text-tertiary)", marginBottom: 4 }}>Copies / exports</div>
          <div style={{ fontSize: 15, fontWeight: 600 }}>{copies + pngs + gifs}</div>
          <div style={{ fontSize: 10, color: "var(--color-text-tertiary)" }}>{copies} copy · {pngs} PNG · {gifs} GIF</div>
        </div>
      </div>

      {summary.lastGeneratedAt && (
        <p className="generate-hint" style={{ marginBottom: 12 }}>
          <i className="ti ti-clock"></i>
          Last generation: {window.formatAppActivityTime(summary.lastGeneratedAt)}
          {summary.firstGeneratedAt && summary.firstGeneratedAt !== summary.lastGeneratedAt && (
            <> · First: {window.formatAppActivityTime(summary.firstGeneratedAt)}</>
          )}
        </p>
      )}

      {summary.recent && summary.recent.length > 0 && (
        <>
          <p className="eyebrow" style={{ marginTop: 8 }}>Recent actions</p>
          <ul style={{ margin: "8px 0 0", paddingLeft: 0, listStyle: "none", fontSize: 12.5, color: "var(--color-text-secondary)" }}>
            {summary.recent.slice(0, 8).map((row, i) => (
              <li key={row.at + "-" + i} style={{ padding: "6px 0", borderBottom: "1px solid var(--color-border-tertiary)", display: "flex", justifyContent: "space-between", gap: 12 }}>
                <span>{labels[row.event] || row.event}</span>
                <span style={{ color: "var(--color-text-tertiary)", whiteSpace: "nowrap" }}>{window.formatAppActivityTime(row.at)}</span>
              </li>
            ))}
          </ul>
        </>
      )}

      <p className="generate-hint" style={{ marginTop: 14, lineHeight: 1.55, display: "block" }}>
        <i className="ti ti-chart-bar" style={{ marginRight: 6 }}></i>
        <strong>All visitors:</strong> Cloudflare Analytics shows page views only. To see how many people actually generate posts,
        add a <strong>GA4 Measurement ID</strong> in <code>analytics.js</code>, then in Google Analytics open
        <strong> Reports → Engagement → Events</strong> and look for <code>text_post_generated</code> and <code>visual_post_generated</code>.
      </p>
    </div>
  );
}

function SettingsTab({ settings, setSettings, showToast, textPostCount, visualPostCount }) {
  const localDevHost = window.isLocalDevHost();
  const availableProviders = window.getAvailableProviders();
  const defaultSettings = window.getDefaultSettings();
  const provider = window.getProvider(settings.providerId);
  const [testing, setTesting] = useStateA(false);
  const [testResult, setTestResult] = useStateA(null);
  const [showKey, setShowKey] = useStateA(false);

  function patch(updates) { setSettings(s => ({ ...s, ...updates })); }

  async function onDefaultAvatarChange(e) {
    const file = e.target.files?.[0];
    if (!file) return;
    if (!file.type || !file.type.startsWith("image/")) {
      showToast("Please select an image file");
      return;
    }
    if (file.size > 4 * 1024 * 1024) {
      showToast("Image too large (max 4MB)");
      return;
    }
    try {
      const dataUrl = await new Promise((resolve, reject) => {
        const reader = new FileReader();
        reader.onload = () => resolve(reader.result);
        reader.onerror = () => reject(new Error("Failed to read image"));
        reader.readAsDataURL(file);
      });
      patch({ authorAvatarDataUrl: String(dataUrl || "") });
      showToast("Default avatar saved");
    } catch {
      showToast("Avatar upload failed");
    } finally {
      e.target.value = "";
    }
  }

  function pickProvider(id) {
    const p = window.getProvider(id);
    const firstModel = p.defaultModelId || p.models[0]?.id || "";
    patch({
      providerId: id,
      model: firstModel,
      baseUrl: p.defaultBaseUrl || "",
      customModel: "",
    });
    setTestResult(null);
  }

  async function testConnection() {
    setTesting(true); setTestResult(null);
    try {
      const out = await window.callAI({
        settings,
        system: "You are a connection test. Reply only with the word: OK",
        user: "ping",
        maxTokens: 20,
      });
      setTestResult({ ok: true, msg: `Response: "${(out || "").slice(0, 60)}"` });
      window.trackAppEvent?.("ai_connection_tested", { providerId: settings.providerId, ok: true });
    } catch (err) {
      setTestResult({ ok: false, msg: err.message || String(err) });
      window.trackAppEvent?.("ai_connection_tested", { providerId: settings.providerId, ok: false });
    } finally {
      setTesting(false);
    }
  }

  function resetToDefault() {
    const def = window.getDefaultSettings();
    const label = def.providerId === "builtin" ? "Built-in Claude" : "Google Gemini 2.5 Flash";
    if (!window.confirm(`Reset AI settings to default (${label})?`)) return;
    setSettings({ ...def });
    setTestResult(null);
    showToast("Settings reset");
  }

  return (
    <>
      <div className="card">
        <p className="eyebrow">Author defaults</p>
        <h2 className="card-title">Auto-add your signature</h2>
        <p className="card-sub">Applied by default to generated social text and visual byline/avatar, so you don't have to add it every time.</p>

        <div className="form-grid">
          <div className="field form-row">
            <label><i className="ti ti-signature"></i>Default byline / signature</label>
            <input
              className="input"
              value={settings.authorByline || ""}
              onChange={e => patch({ authorByline: e.target.value })}
              placeholder="e.g. By Mayank Chugh · @mayankchugh"
            />
            <span className="generate-hint" style={{ marginTop: 2 }}>
              <i className="ti ti-info-circle"></i>
              Added to generated social posts (if not already present).
            </span>
          </div>
          <div className="field form-row">
            <label><i className="ti ti-user-circle"></i>Default avatar (optional)</label>
            <div style={{ display: "flex", alignItems: "center", gap: 10, flexWrap: "wrap" }}>
              <label className="btn btn-ghost btn-small" style={{ cursor: "pointer" }}>
                <i className="ti ti-photo"></i> Upload avatar
                <input type="file" accept="image/*" onChange={onDefaultAvatarChange} style={{ display: "none" }} />
              </label>
              {settings.authorAvatarDataUrl && (
                <>
                  <img
                    src={settings.authorAvatarDataUrl}
                    alt="Default avatar preview"
                    style={{ width: 30, height: 30, borderRadius: "50%", border: "1px solid var(--color-border-tertiary)", objectFit: "cover" }}
                  />
                  <button type="button" className="btn btn-ghost btn-small" onClick={() => patch({ authorAvatarDataUrl: "" })}>
                    <i className="ti ti-x"></i> Remove
                  </button>
                </>
              )}
            </div>
            <span className="generate-hint" style={{ marginTop: 2 }}>
              <i className="ti ti-info-circle"></i>
              Used automatically in visual posts and exports unless you replace it in Visual tab.
            </span>
          </div>
        </div>
      </div>

      <div className="card">
        <p className="eyebrow">Provider</p>
        <h2 className="card-title">Which AI should generate your posts?</h2>
        <p className="card-sub">
          {localDevHost
            ? "Built-in Claude works on localhost. Ollama runs on your machine. Other providers use your API key."
            : "Bring your own API key — Gemini Flash has a free tier. Built-in Claude and Ollama are only available when running locally."}
        </p>

        <div className="provider-grid">
          {availableProviders.map(p => (
            <button
              key={p.id}
              type="button"
              className={"provider-card" + (settings.providerId === p.id ? " is-active" : "")}
              onClick={() => pickProvider(p.id)}
            >
              <div className="provider-card-head">
                <span className="provider-card-name">{p.name}</span>
                {p.id === defaultSettings.providerId && <span className="tag tag-accent">Default</span>}
                {!p.needsKey && p.id !== defaultSettings.providerId && <span className="tag">No key</span>}
              </div>
              <div className="provider-card-desc">{p.description}</div>
            </button>
          ))}
        </div>
      </div>

      <div className="card">
        <p className="eyebrow">Configure</p>
        <h2 className="card-title">{provider.name}</h2>
        <p className="card-sub">{provider.description}</p>

        <div className="form-grid">
          {/* model select */}
          {provider.models.length > 0 && (
            <div className="field form-row">
              <label><i className="ti ti-box"></i>Model</label>
              <select
                className="select"
                value={settings.model}
                onChange={e => patch({ model: e.target.value, customModel: "" })}
                title="Select model"
                aria-label="Model"
              >
                {provider.models.map(m => (
                  <option key={m.id} value={m.id}>{m.name} — {m.id}</option>
                ))}
                {provider.allowCustomModel && (
                  <option value="__custom__">Custom model name…</option>
                )}
              </select>
            </div>
          )}

          {/* custom model name */}
          {(provider.allowCustomModel && (settings.model === "__custom__" || settings.customModel || provider.models.length === 0)) && (
            <div className="field form-row">
              <label><i className="ti ti-tag"></i>Custom model name</label>
              <input
                className="input"
                value={settings.customModel}
                onChange={e => patch({ customModel: e.target.value })}
                placeholder={provider.id === "ollama" ? "e.g. llama3.2:latest" : "e.g. mixtral-8x7b-32768"}
              />
            </div>
          )}

          {/* base url */}
          {provider.needsBaseUrl && (
            <div className="field form-row">
              <label><i className="ti ti-link"></i>Base URL</label>
              <input
                className="input"
                value={settings.baseUrl}
                onChange={e => patch({ baseUrl: e.target.value })}
                placeholder={provider.defaultBaseUrl || "https://..."}
              />
            </div>
          )}

          {/* api key */}
          {provider.needsKey && (
            <div className="field form-row">
              <label>
                <i className="ti ti-key"></i>API key
                {provider.keyHelp && <span style={{marginLeft: 6, color: "var(--color-text-tertiary)", fontWeight: 400}}>· {provider.keyHelp}</span>}
              </label>
              <div className="key-row">
                <input
                  className="input"
                  type={showKey ? "text" : "password"}
                  value={settings.apiKey}
                  onChange={e => patch({ apiKey: e.target.value })}
                  placeholder="sk-..."
                  autoComplete="off"
                  spellCheck="false"
                />
                <button
                  type="button"
                  className="btn btn-ghost btn-small"
                  onClick={() => setShowKey(s => !s)}
                  title={showKey ? "Hide" : "Show"}
                >
                  <i className={"ti " + (showKey ? "ti-eye-off" : "ti-eye")}></i>
                </button>
              </div>
              <span className="generate-hint" style={{marginTop: 2}}>
                <i className="ti ti-shield-lock"></i>
                Stored locally in browser storage only — never sent anywhere except the provider you select.
              </span>
            </div>
          )}
        </div>

        <div className="generate-row">
          <button className="btn btn-ghost btn-small" onClick={resetToDefault}>
            <i className="ti ti-refresh"></i> Reset to default
          </button>
          <button className="btn btn-primary" onClick={testConnection} disabled={testing}>
            {testing ? <><span className="spinner"></span> Testing…</> : <><i className="ti ti-plug"></i> Test connection</>}
          </button>
        </div>

        {testResult && (
          <div className={"error-banner " + (testResult.ok ? "is-success" : "")} style={testResult.ok ? {background: "var(--color-accent-soft)", color: "var(--color-accent)"} : {}}>
            <i className={"ti " + (testResult.ok ? "ti-check" : "ti-alert-triangle")}></i>
            {testResult.ok ? "Connected. " : "Failed: "} {testResult.msg}
          </div>
        )}
      </div>

      <div className="card">
        <p className="eyebrow">Reference</p>
        <h2 className="card-title">Provider docs &amp; keys</h2>
        <p className="card-sub">Official documentation and where to create an API key for each hosted provider.</p>
        <div className="provider-docs-wrap">
          <table className="provider-docs-table">
            <thead>
              <tr>
                <th scope="col">Provider</th>
                <th scope="col">Docs</th>
                <th scope="col">Get API key</th>
              </tr>
            </thead>
            <tbody>
              {(window.getProviderDocLinks ? window.getProviderDocLinks() : []).map(row => (
                <tr key={row.providerId}>
                  <td>{row.name}</td>
                  <td>
                    <a href={row.docsUrl} target="_blank" rel="noopener noreferrer">
                      Documentation
                    </a>
                  </td>
                  <td>
                    <a href={row.keysUrl} target="_blank" rel="noopener noreferrer">
                      {row.providerId === "ollama" ? "Download Ollama" : "Get API key"}
                    </a>
                  </td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>
        <p className="generate-hint" style={{ marginTop: 10 }}>
          <i className="ti ti-info-circle"></i>
          Hugging Face tokens need <strong>Inference Providers</strong> permission. Gemini free tier: use <strong>2.5 Flash</strong> in Settings.
        </p>
      </div>

      <div className="card">
        <p className="eyebrow">Creator</p>
        <h2 className="card-title">More from Mayank</h2>
        <p className="card-sub">Demos, playbooks, support, and ways to connect. <strong>Gumroad: 50% off</strong> all digital products.</p>
        <CreatorLinks showTitle={false} highlightIds={["demo-video", "buymeacoffee", "gumroad"]} />
      </div>

      <div className="card">
        <p className="eyebrow">Notes</p>
        <h2 className="card-title">CORS &amp; browser calls</h2>
        <ul style={{margin: 0, paddingLeft: 20, fontSize: 13, color: "var(--color-text-secondary)", lineHeight: 1.7}}>
          {localDevHost && (
            <li>The <strong>Built-in</strong> option is the safest on localhost — no key handling, no CORS issues.</li>
          )}
          <li><strong>Anthropic</strong> requires the dangerous-direct-browser-access header. Your key is sent from this page.</li>
          <li><strong>OpenAI, Groq, OpenRouter, Gemini, Mistral, DeepSeek &amp; Hugging Face</strong> allow browser calls but the key is still exposed in network traffic.</li>
          <li><strong>Gemini free tier</strong> — use <strong>2.5 Flash</strong> or <strong>Flash-Lite</strong>. HTTP 429 with <code>limit: 0</code> means no free quota for that model (Pro often needs billing). <a href="https://aistudio.google.com/app/apikey" target="_blank" rel="noopener noreferrer">AI Studio</a> · <a href="https://ai.dev/rate-limit" target="_blank" rel="noopener noreferrer">usage</a></li>
          {localDevHost && (
            <li><strong>Ollama</strong> needs CORS allowed — start with <code>OLLAMA_ORIGINS='*' ollama serve</code> if calls fail.</li>
          )}
          <li><strong>Groq</strong> — fast Llama/Mixtral models; key at <a href="https://console.groq.com" target="_blank" rel="noopener noreferrer">console.groq.com</a>.</li>
          <li><strong>OpenRouter</strong> — route to many models; pick a preset or enter any ID from <a href="https://openrouter.ai/models" target="_blank" rel="noopener noreferrer">openrouter.ai/models</a>.</li>
          <li><strong>Mistral</strong> — free tier with rate limits; key at <a href="https://console.mistral.ai" target="_blank" rel="noopener noreferrer">console.mistral.ai</a>. Use <strong>Mistral Small</strong> for best free-tier value.</li>
          <li><strong>DeepSeek</strong> — free credits for new accounts; key at <a href="https://platform.deepseek.com" target="_blank" rel="noopener noreferrer">platform.deepseek.com</a>.</li>
          <li><strong>Hugging Face Inference</strong> — free tier, rate-limited. Token needs <strong>Inference Providers</strong> permission at <a href="https://huggingface.co/settings/tokens" target="_blank" rel="noopener noreferrer">huggingface.co/settings/tokens</a>.</li>
          <li><strong>Cloudflare Workers AI</strong> — free tier; get account ID + API token from <a href="https://dash.cloudflare.com" target="_blank" rel="noopener noreferrer">Cloudflare dashboard</a> → Workers AI → Use REST API. Paste base URL with your account ID.</li>
          <li><strong>Custom (OpenAI-compatible)</strong> — Together, Fireworks, LM Studio, vLLM, etc. via <code>/v1/chat/completions</code>.</li>
        </ul>
      </div>

      <UsageActivityCard textPostCount={textPostCount || 0} visualPostCount={visualPostCount || 0} />
    </>
  );
}

/* ----------- mount ----------- */
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);
