/* Pulse Desk (M11 + backgrounds + text controls) — a series-scoped workspace:
   the source stack links + the 4-step formula scaffold. Composes a Pulse card
   whose background is chosen from generated brand variations or an uploaded
   image/GIF (swipe/zoom/turn/move/delete). EVERY text part on the card —
   eyebrow, headline and footer — is an editable layer with its own text,
   position, size, weight/style and colour. The finished card is baked on a
   CANVAS so all text is always present in the exported post image. */
(function () {
  const D = window.CROPR_DATA;
  const R = window.CroprRules;

  // ---- background presets ------------------------------------------------
  const PALETTE = ["#38BDF8", "#0EA5B7", "#FCA5A5", "#BFE8FA", "#38BDF8", "#0B3B42", "#0B3B42", "#7FD0F5", "#141414"];
  function mkPreset(id, name, angle, stops, extra) {
    return { id, type: "preset", name, angle, stops, extra: !!extra,
      css: `linear-gradient(${angle}deg, ${stops.map((s) => `${s.c} ${s.p}%`).join(", ")})` };
  }
  const DEFAULT_BGS = [
    mkPreset("core", "Core", 135, [{ c: "#38BDF8", p: 0 }, { c: "#FCA5A5", p: 100 }], true),
    mkPreset("deep", "Deep", 140, [{ c: "#38BDF8", p: 0 }, { c: "#0B3B42", p: 100 }]),
    mkPreset("purple", "Purple", 130, [{ c: "#0EA5B7", p: 0 }, { c: "#38BDF8", p: 100 }]),
    mkPreset("mesh", "Mesh", 125, [{ c: "#38BDF8", p: 0 }, { c: "#0EA5B7", p: 48 }, { c: "#FCA5A5", p: 100 }], true),
    mkPreset("frost", "Frost", 120, [{ c: "#FCA5A5", p: 0 }, { c: "#38BDF8", p: 100 }]),
    mkPreset("night", "Night", 145, [{ c: "#141414", p: 0 }, { c: "#0B3B42", p: 100 }], true),
    mkPreset("soft", "Soft", 135, [{ c: "#7FD0F5", p: 0 }, { c: "#BFE8FA", p: 100 }]),
  ];
  let genCount = 0;
  function generateBgs(n) {
    const out = [];
    for (let i = 0; i < n; i++) {
      genCount++;
      const a = 100 + ((genCount * 37) % 80);
      const c1 = PALETTE[(genCount * 3) % PALETTE.length];
      const c2 = PALETTE[(genCount * 5 + 2) % PALETTE.length];
      const c3 = PALETTE[(genCount * 7 + 1) % PALETTE.length];
      const stops = genCount % 2 ? [{ c: c1, p: 0 }, { c: c2, p: 100 }] : [{ c: c1, p: 0 }, { c: c3, p: 50 }, { c: c2, p: 100 }];
      out.push(mkPreset("gen" + genCount, "Style " + genCount, a, stops, genCount % 3 !== 0));
    }
    return out;
  }

  const WEIGHT = { normal: 500, bold: 800, italic: 700 };
  const FSTYLE = { normal: "normal", bold: "normal", italic: "italic" };

  // ---- canvas baker (all text is ALWAYS baked into the exported PNG) -----
  function loadImg(src) { return new Promise((res, rej) => { const im = new Image(); im.onload = () => res(im); im.onerror = rej; im.src = src; }); }
  function wrapCanvas(c, text, maxW) {
    const words = (text || "").split(/\s+/); const lines = []; let cur = "";
    words.forEach((w) => { const t = cur ? cur + " " + w : w; if (c.measureText(t).width > maxW && cur) { lines.push(cur); cur = w; } else cur = t; });
    if (cur) lines.push(cur);
    return lines.slice(0, 4);
  }
  const canvasFont = (s, size) => `${FSTYLE[s.style] === "italic" ? "italic " : ""}${WEIGHT[s.style] || 500} ${size}px 'Public Sans', system-ui, sans-serif`;
  async function bakePulseCanvas(o) {
    const W = 1200, H = 675;
    const cv = document.createElement("canvas"); cv.width = W; cv.height = H; const c = cv.getContext("2d");
    // ---- background
    if (o.bg.type === "image") {
      c.fillStyle = "#141414"; c.fillRect(0, 0, W, H);
      try {
        const img = await loadImg(o.bg.url);
        const z = o.bg.zoom || 1, r = (o.bg.rotate || 0) * Math.PI / 180, ox = (o.bg.ox || 0) / 100 * W, oy = (o.bg.oy || 0) / 100 * H;
        // contain: the whole image fits inside the frame (letterboxed) so nothing
        // is cropped by default — the user zooms/drags to fill and position it.
        const ar = img.width / img.height, car = W / H; let dw, dh;
        if (ar > car) { dw = W; dh = W / ar; } else { dh = H; dw = H * ar; }
        c.save(); c.translate(W / 2 + ox, H / 2 + oy); c.scale(z, z); c.rotate(r); c.drawImage(img, -dw / 2, -dh / 2, dw, dh); c.restore();
      } catch (e) {}
      const g = c.createLinearGradient(0, 0, W, H); g.addColorStop(0, "rgba(18,18,38,.62)"); g.addColorStop(1, "rgba(18,18,38,.12)"); c.fillStyle = g; c.fillRect(0, 0, W, H);
    } else {
      const a = (o.bg.angle || 135) * Math.PI / 180, dx = Math.sin(a), dy = -Math.cos(a);
      const len = (Math.abs(dx) * W + Math.abs(dy) * H) / 2;
      const g = c.createLinearGradient(W / 2 - dx * len, H / 2 - dy * len, W / 2 + dx * len, H / 2 + dy * len);
      (o.bg.stops || []).forEach((s) => g.addColorStop(Math.max(0, Math.min(1, s.p / 100)), s.c));
      c.fillStyle = g; c.fillRect(0, 0, W, H);
      if (o.bg.extra) { c.fillStyle = "rgba(255,255,255,0.07)"; c.beginPath(); c.arc(1040, 150, 240, 0, Math.PI * 2); c.fill(); }
    }
    const T = o.text || {}, eb = T.eyebrow || {}, hd = T.headline || {}, ft = T.footer || {};
    c.textBaseline = "alphabetic";
    // ---- eyebrow (uppercase, letter-spaced)
    try { c.letterSpacing = "6px"; } catch (e) {}
    c.fillStyle = eb.color || "#ffffff"; c.font = canvasFont(eb, eb.size || 30);
    c.fillText((eb.value || "Pulse").toUpperCase(), 72, 120);
    try { c.letterSpacing = "0px"; } catch (e) {}
    // ---- footer (+ accent bar)
    c.fillStyle = ft.color || "#ffffff"; c.font = canvasFont(ft, ft.size || 30);
    c.fillText(ft.value || "leap.app", 72, 584);
    c.fillStyle = "rgba(255,255,255,0.85)"; c.beginPath(); (c.roundRect ? c.roundRect(72, 606, 120, 7, 3.5) : c.rect(72, 606, 120, 7)); c.fill();
    // ---- headline (positioned + styled)
    const hsize = hd.size || 58;
    c.fillStyle = hd.color || "#ffffff"; c.font = canvasFont(hd, hsize);
    const lines = wrapCanvas(c, hd.value || "Your headline / number", W - 144);
    const lh = hsize * 1.06, blockH = lines.length * lh;
    const bandTop = 150, bandBottom = 560;
    let y = hd.pos === "top" ? bandTop + hsize : hd.pos === "middle" ? (bandTop + bandBottom) / 2 - blockH / 2 + hsize : bandBottom - blockH + hsize;
    lines.forEach((ln, i) => c.fillText(ln, 72, y + i * lh));
    return cv.toDataURL("image/png");
  }

  // ---- live card (HTML preview; matches the baked layout) ---------------
  // `text` = { eyebrow, headline, footer } layers, each { value, size, style, color }
  // and headline additionally { pos }. Sizes are in 1200-space px (1cqw = 12px).
  function PulseCard({ bg, text, big, onPrev, onNext, onPan }) {
    const cardRef = React.useRef(null);
    const drag = React.useRef(null);
    const isImg = bg.type === "image";
    const handlers = big ? {
      onPointerDown: (e) => { drag.current = { x: e.clientX, y: e.clientY, ox: bg.ox || 0, oy: bg.oy || 0 }; try { e.currentTarget.setPointerCapture(e.pointerId); } catch (x) {} },
      onPointerMove: (e) => { if (!drag.current || !isImg || !onPan) return; const rect = cardRef.current.getBoundingClientRect(); onPan(drag.current.ox + (e.clientX - drag.current.x) / rect.width * 100, drag.current.oy + (e.clientY - drag.current.y) / rect.height * 100); },
      onPointerUp: (e) => { if (!drag.current) return; const dx = e.clientX - drag.current.x; drag.current = null; if (!isImg) { if (dx < -40) onNext && onNext(); else if (dx > 40) onPrev && onPrev(); } },
    } : {};
    const eb = text.eyebrow, hd = text.headline, ft = text.footer;
    const cqw = (px) => `${px / 12}cqw`;
    return (
      <div style={{ position: "relative" }}>
        <div ref={cardRef} {...handlers} style={{ containerType: "inline-size", position: "relative", width: "100%", aspectRatio: "16 / 9", borderRadius: 14, overflow: "hidden", touchAction: "none", cursor: big ? (isImg ? "move" : "grab") : "pointer", background: isImg ? "#141414" : bg.css }}>
          {isImg && <img src={bg.url} alt="" draggable={false} style={{ position: "absolute", inset: 0, width: "100%", height: "100%", objectFit: "contain", transform: `translate(${bg.ox || 0}%, ${bg.oy || 0}%) scale(${bg.zoom || 1}) rotate(${bg.rotate || 0}deg)`, transformOrigin: "center", userSelect: "none", pointerEvents: "none" }} />}
          {isImg && <div style={{ position: "absolute", inset: 0, background: "linear-gradient(115deg, rgba(18,18,38,.62), rgba(18,18,38,.12))", pointerEvents: "none" }} />}
          <div style={{ position: "absolute", inset: 0, padding: "6% 6.5%", display: "flex", flexDirection: "column", pointerEvents: "none" }}>
            <div style={{ fontSize: cqw(eb.size), fontWeight: WEIGHT[eb.style] || 800, fontStyle: FSTYLE[eb.style] || "normal", letterSpacing: "0.35cqw", opacity: .92, textTransform: "uppercase", color: eb.color || "#fff" }}>{eb.value || "Pulse"}</div>
            <div style={{ flex: 1, minHeight: 0, display: "flex", flexDirection: "column", justifyContent: hd.pos === "top" ? "flex-start" : hd.pos === "bottom" ? "flex-end" : "center", paddingTop: "2%", paddingBottom: "2%" }}>
              <div style={{ fontSize: cqw(hd.size), fontWeight: WEIGHT[hd.style] || 500, fontStyle: FSTYLE[hd.style] || "normal", color: hd.color || "#fff", lineHeight: 1.06, letterSpacing: "-0.1cqw", textShadow: "0 0.3cqw 1cqw rgba(0,0,0,.28)" }}>{hd.value || "Your headline / number"}</div>
            </div>
            <div style={{ fontSize: cqw(ft.size), fontWeight: WEIGHT[ft.style] || 700, fontStyle: FSTYLE[ft.style] || "normal", opacity: .92, color: ft.color || "#fff" }}>{ft.value || "leap.app"}</div>
          </div>
        </div>
        {big && (
          <React.Fragment>
            <button onClick={onPrev} style={arrowStyle("left")}><Icon name="chevron-left" size={20} color="#fff" /></button>
            <button onClick={onNext} style={arrowStyle("right")}><Icon name="chevron-right" size={20} color="#fff" /></button>
          </React.Fragment>
        )}
      </div>
    );
  }
  const arrowStyle = (side) => ({ position: "absolute", top: "50%", [side]: 10, transform: "translateY(-50%)", width: 36, height: 36, borderRadius: 999, border: 0, background: "rgba(20,20,30,.5)", cursor: "pointer", display: "grid", placeItems: "center", backdropFilter: "blur(2px)" });

  const SWATCHES = ["#FFFFFF", "#141414", "#38BDF8", "#FCA5A5", "#12B76A", "#F79009", "#F04438"];
  const LAYERS = [{ id: "eyebrow", label: "Eyebrow" }, { id: "headline", label: "Headline" }, { id: "footer", label: "Footer" }];
  const SIZE_RANGE = { eyebrow: [16, 60], headline: [30, 92], footer: [16, 60] };

  // ---- per-layer text-styling controls (shared for every text part) -----
  function TextLayerControls({ active, onActive, style, onStyle, value, onValue, valuePlaceholder }) {
    const [mn, mx] = SIZE_RANGE[active];
    const cur = style;
    return (
      <div style={{ border: "1px solid var(--border)", borderRadius: 12, padding: 12, marginTop: 12, display: "flex", flexDirection: "column", gap: 10 }}>
        <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 10, flexWrap: "wrap" }}>
          <span style={{ fontSize: 11, fontWeight: 700, color: "var(--fg3)", textTransform: "uppercase", letterSpacing: ".05em" }}>Text layers</span>
          <Segmented value={active} onChange={onActive} options={LAYERS.map((l) => ({ value: l.id, label: l.label }))} />
        </div>
        <label style={{ display: "block" }}><span style={{ fontSize: 11.5, color: "var(--fg2)", display: "block", marginBottom: 4 }}>Text</span>
          <TextInput value={value} onChange={onValue} placeholder={valuePlaceholder} /></label>
        <div style={{ display: "flex", gap: 16, flexWrap: "wrap" }}>
          {active === "headline" && (
            <label style={{ display: "block" }}><span style={{ fontSize: 11.5, color: "var(--fg2)", display: "block", marginBottom: 4 }}>Position</span>
              <Segmented value={cur.pos} onChange={(v) => onStyle({ pos: v })} options={[{ value: "top", label: "Top" }, { value: "middle", label: "Middle" }, { value: "bottom", label: "Bottom" }]} /></label>
          )}
          <label style={{ display: "block" }}><span style={{ fontSize: 11.5, color: "var(--fg2)", display: "block", marginBottom: 4 }}>Style</span>
            <Segmented value={cur.style} onChange={(v) => onStyle({ style: v })} options={[{ value: "normal", label: "Normal" }, { value: "bold", label: "Bold" }, { value: "italic", label: "Italic" }]} /></label>
        </div>
        <div style={{ display: "flex", gap: 16, flexWrap: "wrap", alignItems: "center" }}>
          <label style={{ flex: 1, minWidth: 180 }}><span style={{ fontSize: 11.5, color: "var(--fg2)", display: "block", marginBottom: 4 }}>Font size · {cur.size}px</span>
            <input type="range" min={mn} max={mx} value={cur.size} onChange={(e) => onStyle({ size: +e.target.value })} style={{ width: "100%", accentColor: "var(--accent)" }} /></label>
          <label style={{ display: "block" }}><span style={{ fontSize: 11.5, color: "var(--fg2)", display: "block", marginBottom: 4 }}>Colour</span>
            <span style={{ display: "flex", alignItems: "center", gap: 6 }}>
              <input type="color" value={/^#[0-9a-fA-F]{6}$/.test(cur.color) ? cur.color : "#ffffff"} onChange={(e) => onStyle({ color: e.target.value.toUpperCase() })} style={{ width: 34, height: 32, border: "1px solid var(--border-strong)", borderRadius: 8, background: "none", cursor: "pointer", padding: 2 }} />
              <input value={cur.color} onChange={(e) => onStyle({ color: e.target.value })} placeholder="#FFFFFF" style={{ width: 92, fontFamily: "var(--font-mono)", fontSize: 12.5, padding: "7px 9px", borderRadius: 8, border: "1px solid var(--border-strong)", background: "var(--surface)", color: "var(--fg1)" }} />
              {SWATCHES.map((sw) => <span key={sw} onClick={() => onStyle({ color: sw })} title={sw} style={{ width: 18, height: 18, borderRadius: 5, background: sw, cursor: "pointer", border: "1px solid var(--border)" }} />)}
            </span></label>
        </div>
      </div>
    );
  }

  function PulseDesk({ onToast, onOpen }) {
    const [steps, setSteps] = React.useState(["", "", "", ""]);
    const [label, setLabel] = React.useState("Pulse");
    const [footerText, setFooterText] = React.useState("leap.app");
    const [useVisual, setUseVisual] = React.useState(true);
    const [bgs, setBgs] = React.useState(DEFAULT_BGS);
    const [bgIndex, setBgIndex] = React.useState(0);
    const [activeLayer, setActiveLayer] = React.useState("headline");
    const [tstyle, setTstyle] = React.useState({
      eyebrow: { size: 30, style: "bold", color: "#FFFFFF" },
      headline: { pos: "bottom", size: 58, style: "bold", color: "#FFFFFF" },
      footer: { size: 30, style: "bold", color: "#FFFFFF" },
    });
    const [bakedUrl, setBakedUrl] = React.useState(null);
    const [pickBg, setPickBg] = React.useState(false);
    const fileRef = React.useRef(null);

    const set = (i, v) => setSteps((s) => s.map((x, j) => (j === i ? v : x)));
    const setLayer = (patch) => setTstyle((s) => ({ ...s, [activeLayer]: { ...s[activeLayer], ...patch } }));
    const body = steps.filter((s) => s.trim()).join("\n\n");
    const n = R.countChars(body);
    const filled = steps.filter((s) => s.trim()).length;
    const headline = (steps[0] || "").trim();
    const bg = bgs[Math.min(bgIndex, bgs.length - 1)] || DEFAULT_BGS[0];

    // assemble the text layers that get drawn on the visual
    const text = {
      eyebrow: { value: label || "Pulse", ...tstyle.eyebrow },
      headline: { value: headline || "Your headline / number", ...tstyle.headline },
      footer: { value: footerText || "leap.app", ...tstyle.footer },
    };
    // text content + placeholder for whichever layer is being edited
    const layerValue = activeLayer === "eyebrow" ? label : activeLayer === "footer" ? footerText : steps[0];
    const setLayerValue = activeLayer === "eyebrow" ? setLabel : activeLayer === "footer" ? setFooterText : (v) => set(0, v);
    const layerPlaceholder = activeLayer === "eyebrow" ? "Pulse" : activeLayer === "footer" ? "leap.app" : "Your headline / number";

    const prev = () => setBgIndex((i) => (i - 1 + bgs.length) % bgs.length);
    const next = () => setBgIndex((i) => (i + 1) % bgs.length);
    const genMore = () => { const more = generateBgs(3); setBgs((b) => [...b, ...more]); setBgIndex(bgs.length); onToast("Generated 3 more backgrounds"); };
    const onUpload = async (e) => {
      const f = (e.target.files || [])[0]; e.target.value = ""; if (!f) return;
      const isGif = (f.type || "") === "image/gif";
      const m = await window.LeapUpload.readAndHost(f); if (!m) return;
      const opt = { id: "up" + Date.now(), type: "image", name: m.name, url: m.url, isGif: isGif, zoom: 1, rotate: 0, ox: 0, oy: 0 };
      setBgs((b) => { const nb = [...b, opt]; setBgIndex(nb.length - 1); return nb; });
      onToast((isGif ? "GIF" : "Image") + " added — zoom, turn or drag to position");
    };
    // pick a background from the account's asset library (primary path)
    const addAssetBg = (a) => {
      const opt = { id: "as" + a.id, type: "image", name: a.name, url: a.url, isGif: false, zoom: 1, rotate: 0, ox: 0, oy: 0 };
      setBgs((b) => { const nb = [...b, opt]; setBgIndex(nb.length - 1); return nb; });
      onToast(`Background “${a.name}” added — zoom, turn or drag to position`);
    };
    const updateBgFn = (i, fn) => setBgs((b) => b.map((x, j) => (j === i ? { ...x, ...fn(x) } : x)));
    const zoomBy = (d) => updateBgFn(bgIndex, (x) => ({ zoom: Math.min(3, Math.max(0.4, (x.zoom || 1) + d)) }));
    const turnBy = (deg) => updateBgFn(bgIndex, (x) => ({ rotate: (x.rotate || 0) + deg }));
    const panTo = (ox, oy) => updateBgFn(bgIndex, () => ({ ox: Math.max(-60, Math.min(60, ox)), oy: Math.max(-60, Math.min(60, oy)) }));
    const resetXf = () => updateBgFn(bgIndex, () => ({ zoom: 1, rotate: 0, ox: 0, oy: 0 }));
    const delBg = (i) => {
      if (bgs.length <= 1) { onToast("Keep at least one background — Generate or Upload another first", "error"); return; }
      setBgs((b) => b.filter((_, j) => j !== i));
      setBgIndex((x) => Math.max(0, Math.min(x > i ? x - 1 : x, bgs.length - 2)));
    };

    // bake the visual (async) whenever anything relevant changes → drives the
    // assembled preview AND the attached post image (so text is always baked)
    React.useEffect(() => {
      let cancelled = false;
      if (!useVisual) { setBakedUrl(null); return; }
      if (bg.type === "image" && bg.isGif) { setBakedUrl(bg.url); return; }
      bakePulseCanvas({ bg, text }).then((u) => { if (!cancelled) setBakedUrl(u); });
      return () => { cancelled = true; };
    }, [useVisual, bg, label, headline, footerText, tstyle]);

    const create = async () => {
      let media = [];
      if (useVisual) {
        const url = (bg.type === "image" && bg.isGif) ? bg.url : await bakePulseCanvas({ bg, text });
        media = [{ kind: "image", url, altText: label + " visual" + (bg.isGif ? " (animated)" : ""), status: "ready" }];
      }
      const post = window.CroprStore.create({
        title: label + " — " + (headline ? headline.slice(0, 40) : "new"),
        series: "onchain_pulse", pillar: "pulse", channels: ["x_cropr"], status: "in_draft",
        hookType: "number", campaign: label, proofMarked: true,
        content: [{ channel: "x_cropr", format: "single", body, threadParts: null, media, article: null, derivedPost: null }],
      });
      onToast("Pulse draft created → opening in Composer");
      onOpen(post.id);
    };

    return (
      <div style={{ maxWidth: "var(--page-w)", margin: "0 auto", display: "grid", gridTemplateColumns: "minmax(0,1.5fr) minmax(0,1fr)", gap: 22, alignItems: "start" }} className="pulse-grid">
        <div>
          <Card pad={22}>
            <Field label="Pulse label" hint="The series term — shown on the visual eyebrow, and used for the post title & campaign.">
              <TextInput value={label} onChange={setLabel} placeholder="Pulse" />
            </Field>

            <SectionTitle icon="activity">The 4-step Pulse formula</SectionTitle>
            {D.PULSE_FORMULA.map((f, i) => (
              <Field key={f.step} label={`${f.step}. ${f.title}`} hint={f.hint}>
                <TextArea value={steps[i]} onChange={(v) => set(i, v)} rows={2}
                  placeholder={["e.g. 62% of marketers post without a plan (2024 survey).", "e.g. That's a week of guesswork you could give back to your team.", "e.g. Plan the whole week in one sitting, then approve and go.", "e.g. Plan your week in minutes → leap.app"][i]} />
              </Field>
            ))}

            <SectionTitle icon="image" right={<div style={{ display: "flex", gap: 6 }}>
              <Button size="sm" variant="primary" icon="images" onClick={() => setPickBg(true)}>Choose from Assets</Button>
              <Button size="sm" variant="ghost" icon="sparkles" onClick={genMore}>Generate</Button>
              <Button size="sm" variant="ghost" icon="upload" onClick={() => fileRef.current && fileRef.current.click()}>Upload</Button>
            </div>}>Background · swipe to select</SectionTitle>
            <input ref={fileRef} type="file" accept="image/*,image/gif" style={{ display: "none" }} onChange={onUpload} />
            <AssetPicker open={pickBg} onClose={() => setPickBg(false)} imagesOnly onPick={addAssetBg} title="Choose a background" subtitle="Pick an image from your asset library." />

            <label style={{ display: "flex", alignItems: "center", gap: 10, cursor: "pointer", marginBottom: 10 }}>
              <input type="checkbox" checked={useVisual} onChange={(e) => setUseVisual(e.target.checked)} style={{ width: 16, height: 16, accentColor: "var(--accent)" }} />
              <span style={{ fontSize: 13, fontWeight: 600 }}>Attach a Pulse visual</span>
              <span style={{ fontSize: 11, color: "var(--fg3)" }}>{bg.type === "image" ? (bg.isGif ? "GIF · animates (text in caption)" : "uploaded image · drag to position") : bg.name + " background"}</span>
            </label>

            {useVisual && (
              <div>
                <PulseCard bg={bg} text={text} big onPrev={prev} onNext={next} onPan={panTo} />

                <div style={{ display: "flex", justifyContent: "center", alignItems: "center", gap: 6, marginTop: 8, flexWrap: "wrap" }}>
                  {bg.type === "image" && (
                    <React.Fragment>
                      <Button size="sm" variant="secondary" icon="zoom-out" title="Zoom out" onClick={() => zoomBy(-0.15)} />
                      <span style={{ fontSize: 11.5, fontWeight: 600, color: "var(--fg3)", minWidth: 38, textAlign: "center", fontVariantNumeric: "tabular-nums" }}>{Math.round((bg.zoom || 1) * 100)}%</span>
                      <Button size="sm" variant="secondary" icon="zoom-in" title="Zoom in" onClick={() => zoomBy(0.15)} />
                      <Button size="sm" variant="secondary" icon="rotate-ccw" title="Turn left" onClick={() => turnBy(-90)} />
                      <Button size="sm" variant="secondary" icon="rotate-cw" title="Turn right" onClick={() => turnBy(90)} />
                      <Button size="sm" variant="ghost" icon="maximize-2" title="Reset zoom / rotation / position" onClick={resetXf} />
                    </React.Fragment>
                  )}
                  <Button size="sm" variant="danger" icon="trash-2" title="Delete this background" onClick={() => delBg(bgIndex)} />
                </div>

                <div style={{ display: "flex", justifyContent: "center", gap: 5, margin: "10px 0" }}>
                  {bgs.map((_, i) => <span key={i} onClick={() => setBgIndex(i)} style={{ width: i === bgIndex ? 18 : 7, height: 7, borderRadius: 999, background: i === bgIndex ? "var(--accent)" : "var(--border-strong)", cursor: "pointer", transition: "width 140ms" }} />)}
                </div>
                <div style={{ display: "flex", gap: 8, overflowX: "auto", paddingBottom: 6 }}>
                  {bgs.map((b, i) => (
                    <div key={b.id} style={{ position: "relative", width: 96, flex: "none" }}>
                      <div onClick={() => setBgIndex(i)} style={{ borderRadius: 10, overflow: "hidden", cursor: "pointer", border: `2px solid ${i === bgIndex ? "var(--accent)" : "transparent"}`, boxShadow: "var(--shadow-xs)" }}>
                        <PulseCard bg={b} text={text} />
                      </div>
                      <button onClick={(e) => { e.stopPropagation(); delBg(i); }} title="Delete background" style={{ position: "absolute", top: 3, right: 3, width: 18, height: 18, borderRadius: 999, border: 0, background: "rgba(20,20,30,.6)", cursor: "pointer", display: "grid", placeItems: "center" }}><Icon name="x" size={11} color="#fff" /></button>
                    </div>
                  ))}
                </div>

                {/* per-layer text controls — every text part edits the same way */}
                <TextLayerControls active={activeLayer} onActive={setActiveLayer} style={tstyle[activeLayer]} onStyle={setLayer}
                  value={layerValue} onValue={setLayerValue} valuePlaceholder={layerPlaceholder} />
              </div>
            )}

            <div style={{ display: "flex", alignItems: "center", gap: 12, marginTop: 16 }}>
              <Button variant="primary" icon="plus" disabled={filled < 2} onClick={create}>Create post from this</Button>
              <span style={{ fontSize: 12, color: n > 220 ? "#B25E09" : "var(--fg3)", fontVariantNumeric: "tabular-nums" }}>{n} chars assembled {n >= 180 && n <= 220 ? "· on target" : ""}</span>
            </div>
          </Card>

          {body && (
            <div style={{ marginTop: 18 }}>
              <SectionTitle icon="eye">Assembled preview</SectionTitle>
              <PreviewX post={{ content: [{ channel: "x_cropr", format: "single", body, media: useVisual && bakedUrl ? [{ kind: "image", url: bakedUrl, status: "ready" }] : [] }], media: { kind: null } }} channel="x_cropr" />
            </div>
          )}
        </div>

        <div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
          <Card pad={20}>
            <SectionTitle icon="database">Source stack</SectionTitle>
            <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
              {D.PULSE_SOURCES.length === 0 && (
                <div style={{ padding: "16px 12px", borderRadius: 12, border: "1px dashed var(--border-strong)", textAlign: "center", fontSize: 12.5, color: "var(--fg3)", lineHeight: 1.5 }}>
                  No sources yet. Add the links you pull data and inspiration from — they'll live here for quick reference.
                </div>
              )}
              {D.PULSE_SOURCES.map((s) => (
                <a key={s.name} href={s.url} target="_blank" rel="noreferrer" style={{ display: "flex", alignItems: "center", gap: 12, padding: "11px 12px", borderRadius: 12, border: "1px solid var(--border)", textDecoration: "none", transition: "border-color 140ms, background 140ms" }}
                  onMouseEnter={(e) => { e.currentTarget.style.background = "var(--bg-subtle)"; e.currentTarget.style.borderColor = "var(--border-strong)"; }}
                  onMouseLeave={(e) => { e.currentTarget.style.background = "transparent"; e.currentTarget.style.borderColor = "var(--border)"; }}>
                  <span style={{ width: 34, height: 34, borderRadius: 9, background: "var(--accent-weak)", display: "grid", placeItems: "center", flex: "none" }}><Icon name="bar-chart-3" size={17} color="var(--accent)" /></span>
                  <span style={{ minWidth: 0 }}>
                    <span style={{ display: "block", fontSize: 13.5, fontWeight: 700, color: "var(--fg1)" }}>{s.name}</span>
                    <span style={{ display: "block", fontSize: 11.5, color: "var(--fg3)" }}>{s.note}</span>
                  </span>
                  <Icon name="external-link" size={15} color="var(--fg4)" style={{ marginLeft: "auto" }} />
                </a>
              ))}
            </div>
          </Card>
          <Card pad={20} style={{ background: "var(--gradient-hero)", border: 0, color: "#fff" }}>
            <div style={{ fontSize: 13, fontWeight: 700, marginBottom: 6, display: "flex", alignItems: "center", gap: 8 }}><Icon name="compass" size={18} color="#fff" />How the Pulse should read</div>
            <div style={{ fontSize: 12.5, lineHeight: 1.55, opacity: .95 }}>Start with a hard, sourced number. Say what it means for your audience. Give the takeaway. Close with exactly one clear call to action. No hashtags, 180–220 characters.</div>
          </Card>
        </div>
      </div>
    );
  }
  window.PulseDesk = PulseDesk;
})();
