/* ============================================================================
   Operation Outbreak — Wrapped  ·  SHARE / EXPORT
   Renders on-brand share graphics to <canvas> and downloads real PNGs.
   ============================================================================ */

const SHARE_SPECS = {
  group:       { w: 1920, h: 1080, label: "16:9 · LinkedIn / Desktop", platform: "LinkedIn", file: "operation-outbreak-summary" },
  individual:  { w: 1080, h: 1920, label: "9:16 · Instagram Story",   platform: "Instagram Story", file: "outbreak-report-card" },
  personality: { w: 1080, h: 1920, label: "9:16 · Story / Trading Card", platform: "Story", file: "outbreak-trading-card" },
};

async function ensureFonts() {
  try {
    await Promise.all([
      document.fonts.load('300 100px Manrope'), document.fonts.load('400 100px Manrope'),
      document.fonts.load('700 100px Manrope'), document.fonts.load('600 100px "Geist Mono"'),
      document.fonts.load('700 100px "Geist Mono"'),
    ]);
    await document.fonts.ready;
  } catch (e) {}
}
function loadImg(src) {
  return new Promise((res) => { const im = new Image(); im.crossOrigin = "anonymous"; im.onload = () => res(im); im.onerror = () => res(null); im.src = src; });
}
async function lucideImg(name, color, stroke = 1.5) {
  const holder = document.createElement("div");
  holder.style.cssText = "position:absolute;left:-9999px;top:-9999px";
  const i = document.createElement("i"); i.setAttribute("data-lucide", name);
  holder.appendChild(i); document.body.appendChild(holder);
  try {
    window.lucide.createIcons({ attrs: { width: 240, height: 240, stroke: color, fill: "none", "stroke-width": stroke, "stroke-linecap": "round", "stroke-linejoin": "round" }, nameAttr: "data-lucide" });
  } catch (e) {}
  const svg = holder.querySelector("svg");
  let img = null;
  if (svg) { const xml = new XMLSerializer().serializeToString(svg); img = await loadImg("data:image/svg+xml;charset=utf-8," + encodeURIComponent(xml)); }
  document.body.removeChild(holder);
  return img;
}

/* ── canvas helpers ──────────────────────────────────────────────────────── */
function rr(ctx, x, y, w, h, r) {
  ctx.beginPath(); ctx.moveTo(x + r, y);
  ctx.arcTo(x + w, y, x + w, y + h, r); ctx.arcTo(x + w, y + h, x, y + h, r);
  ctx.arcTo(x, y + h, x, y, r); ctx.arcTo(x, y, x + w, y, r); ctx.closePath();
}
function starPath(ctx, cx, cy, R) {
  const s = R / 47, m = (x, y) => [cx + (x - 50) * s, cy + (y - 50) * s];
  ctx.beginPath(); ctx.moveTo(...m(50, 3));
  ctx.bezierCurveTo(...m(56, 31), ...m(69, 44), ...m(97, 50));
  ctx.bezierCurveTo(...m(69, 56), ...m(56, 69), ...m(50, 97));
  ctx.bezierCurveTo(...m(44, 69), ...m(31, 56), ...m(3, 50));
  ctx.bezierCurveTo(...m(31, 44), ...m(44, 31), ...m(50, 3));
  ctx.closePath();
}
function wrap(ctx, text, x, y, maxW, lineH) {
  const words = text.split(" "); let line = "", yy = y;
  for (const w of words) {
    const test = line ? line + " " + w : w;
    if (ctx.measureText(test).width > maxW && line) { ctx.fillText(line, x, yy); line = w; yy += lineH; }
    else line = test;
  }
  ctx.fillText(line, x, yy); return yy;
}
function ring(ctx, cx, cy, r, width, pct, from, to) {
  ctx.save();
  ctx.lineWidth = width; ctx.lineCap = "round";
  ctx.strokeStyle = "rgba(255,255,255,0.08)";
  ctx.beginPath(); ctx.arc(cx, cy, r, 0, Math.PI * 2); ctx.stroke();
  const g = ctx.createLinearGradient(cx - r, cy - r, cx + r, cy + r);
  g.addColorStop(0, from); g.addColorStop(1, to);
  ctx.strokeStyle = g;
  ctx.beginPath(); ctx.arc(cx, cy, r, -Math.PI / 2, -Math.PI / 2 + Math.PI * 2 * pct); ctx.stroke();
  ctx.restore();
}

/* ── GROUP · 16:9 LinkedIn ───────────────────────────────────────────────── */
async function renderGroup(ctx, spec) {
  const D = window.OO_DATA.group, M = window.OO_DATA.meta, W = spec.w, H = spec.h;
  ctx.fillStyle = "#08090A"; ctx.fillRect(0, 0, W, H);
  ctx.fillStyle = "#0E1116"; rr(ctx, 56, 56, W - 112, H - 112, 36); ctx.fill();
  ctx.strokeStyle = "rgba(255,255,255,0.08)"; ctx.lineWidth = 2; rr(ctx, 56, 56, W - 112, H - 112, 36); ctx.stroke();

  const logo = await loadImg((window.__resources && window.__resources.appIcon) || "assets/app-icon.png");
  if (logo) { ctx.save(); rr(ctx, 110, 104, 56, 56, 13); ctx.clip(); ctx.drawImage(logo, 110, 104, 56, 56); ctx.restore(); }
  ctx.textAlign = "left";
  ctx.fillStyle = "#F4F6F6"; ctx.font = '700 22px "Geist Mono"';
  ctx.fillText("OPERATION OUTBREAK", 182, 132);
  ctx.fillStyle = "#58DBA6"; ctx.font = '700 18px "Geist Mono"';
  ctx.fillText(`SIMULATION SUMMARY // ${M.runId}`, 182, 158);

  // title
  ctx.fillStyle = "#F4F6F6"; ctx.font = '300 96px Manrope';
  ctx.fillText("Our outbreak timeline", 110, 300);
  ctx.fillStyle = "#9BA3A6"; ctx.font = '600 20px "Geist Mono"';
  ctx.fillText(`${M.cohort.toUpperCase()}   ·   RUNTIME ${M.duration.toUpperCase()}   ·   ${M.date}`, 112, 344);

  // curve — both infection (red) and recovered (blue dashed), matching org report
  const cx0 = 110, cy0 = 400, cw = W - 220, ch = 360;
  const rec = D.recovered || [];
  const max = Math.max(...D.curve, ...rec, 1), n = D.curve.length;
  const PX = (i) => cx0 + (i / (n - 1)) * cw;
  const PY = (v) => cy0 + ch - (v / max) * ch;
  const bezPath = (pts_) => {
    ctx.moveTo(pts_[0][0], pts_[0][1]);
    for (let i = 0; i < pts_.length - 1; i++) {
      const p0=pts_[i-1]||pts_[i],p1=pts_[i],p2=pts_[i+1],p3=pts_[i+2]||p2;
      ctx.bezierCurveTo(p1[0]+(p2[0]-p0[0])/6,p1[1]+(p2[1]-p0[1])/6,p2[0]-(p3[0]-p1[0])/6,p2[1]-(p3[1]-p1[1])/6,p2[0],p2[1]);
    }
  };
  // gridlines
  ctx.strokeStyle = "rgba(255,255,255,0.06)"; ctx.lineWidth = 1;
  [0, .33, .66, 1].forEach((g) => { ctx.beginPath(); ctx.moveTo(cx0, cy0 + ch * g); ctx.lineTo(cx0 + cw, cy0 + ch * g); ctx.stroke(); });
  // recovered — blue area + dashed line (drawn first, underneath)
  if (rec.length) {
    const rpts = rec.map((v, i) => [PX(i), PY(v)]);
    ctx.beginPath(); bezPath(rpts);
    ctx.save(); ctx.lineTo(PX(n-1), cy0+ch); ctx.lineTo(PX(0), cy0+ch); ctx.closePath();
    const ag2 = ctx.createLinearGradient(0, cy0, 0, cy0+ch);
    ag2.addColorStop(0, "rgba(84,101,229,0.20)"); ag2.addColorStop(1, "rgba(84,101,229,0)");
    ctx.fillStyle = ag2; ctx.fill(); ctx.restore();
    ctx.beginPath(); bezPath(rpts);
    const lg2 = ctx.createLinearGradient(cx0, 0, cx0+cw, 0);
    lg2.addColorStop(0, "#7387F9"); lg2.addColorStop(1, "#5465E5");
    ctx.strokeStyle = lg2; ctx.lineWidth = 4; ctx.setLineDash([10, 7]);
    ctx.lineJoin = "round"; ctx.lineCap = "round"; ctx.stroke();
    ctx.setLineDash([]);
  }
  // infection curve — red solid (on top)
  const pts = D.curve.map((v, i) => [PX(i), PY(v)]);
  ctx.beginPath(); bezPath(pts);
  ctx.save(); ctx.lineTo(PX(n-1), cy0+ch); ctx.lineTo(PX(0), cy0+ch); ctx.closePath();
  const ag = ctx.createLinearGradient(0, cy0, 0, cy0+ch);
  ag.addColorStop(0, "rgba(255,113,113,0.32)"); ag.addColorStop(1, "rgba(219,58,58,0)");
  ctx.fillStyle = ag; ctx.fill(); ctx.restore();
  ctx.beginPath(); bezPath(pts);
  const lg = ctx.createLinearGradient(cx0, 0, cx0+cw, 0);
  lg.addColorStop(0, "#FF7171"); lg.addColorStop(1, "#DB3A3A");
  ctx.strokeStyle = lg; ctx.lineWidth = 5; ctx.lineJoin = "round"; ctx.lineCap = "round"; ctx.stroke();
  // peak dot + label
  const pk = pts[D.curve.indexOf(Math.max(...D.curve))];
  ctx.fillStyle = "#FF7171"; ctx.beginPath(); ctx.arc(pk[0], pk[1], 8, 0, Math.PI * 2); ctx.fill();
  ctx.fillStyle = "#FF7171"; ctx.font = '700 18px "Geist Mono"'; ctx.textAlign = "center";
  ctx.fillText(`PEAK · ${D.peakConcurrent} ACTIVE`, pk[0], pk[1] - 26);

  // stat strip
  const stats = [
    ["TOTAL INFECTIONS", D.totalInfections, "#FF7171"],
    ["DECEASED", D.deceased, "#ED5050"],
    ["R₀ OBSERVED", D.r0, "#866AFF"],
    ["PARTICIPANTS", D.participants, "#7387F9"],
  ];
  const sw = (W - 220) / 4;
  ctx.textAlign = "left";
  stats.forEach((s, i) => {
    const x = 110 + i * sw;
    ctx.fillStyle = "#5F676A"; ctx.font = '700 16px "Geist Mono"'; ctx.fillText(s[0], x, 880);
    ctx.fillStyle = "#F4F6F6"; ctx.font = '700 72px "Geist Mono"'; ctx.fillText(String(s[1]), x, 952);
    if (i > 0) { ctx.strokeStyle = "rgba(255,255,255,0.08)"; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(x - 24, 854); ctx.lineTo(x - 24, 958); ctx.stroke(); }
  });

  // footer
  ctx.fillStyle = "#5F676A"; ctx.font = '600 18px "Geist Mono"'; ctx.textAlign = "right";
  ctx.fillText("OPERATIONOUTBREAK.ORG", W - 110, 990);
  ctx.fillStyle = "#58DBA6"; starPath(ctx, W - 132, 124, 18); ctx.fill();
}

/* ── INDIVIDUAL · 9:16 story — mirrors slide 2 (sonar + count) ───────────── */
async function renderIndividual(ctx, spec) {
  const D = window.OO_DATA.individual, M = window.OO_DATA.meta, W = spec.w, H = spec.h;
  const PAD = 80;

  // ── Resolve theme (matches MIndividual logic) ─────────────────────────────
  const infected = !!D.infected, indexCase = !!D.indexCase;
  let accent, headerColor, header, g1c, g2c, count, countLabel, message, stats;
  if (indexCase) {
    accent = "#DB3A3A"; headerColor = "#DB3A3A"; header = "// INDEX CASE";
    g1c = "rgba(255,113,113,0.18)"; g2c = "rgba(219,58,58,0.13)";
    count = D.contactsBeforeInfection || 0; countLabel = "INFECTED CONTACTS";
    message = "Patient zero. You caught it from no one — the outbreak started with you, and spread from here.";
    stats = [["DIRECT INF.", String(D.directInfections || 0), "#DB3A3A"], ["DOWNSTREAM INF.", String(D.downstreamInfections || 0), "#FF7171"]];
  } else if (infected) {
    accent = "#FF7171"; headerColor = "#DB3A3A"; header = "// SUCCUMBED";
    g1c = "rgba(237,80,80,0.18)"; g2c = "rgba(219,58,58,0.13)";
    count = D.contactsBeforeInfection != null ? D.contactsBeforeInfection : D.infectedEncounters;
    countLabel = "CONTACTS BEFORE INFECTION";
    message = "Contacts you shared with the infected before the virus finally caught you — one of them was the one.";
    stats = [["TIME HEALTHY", String(D.timeSurvived || "—"), "#DB3A3A"], ["TIME/CONTACT", String(D.avgEncounterDuration || "—"), "#866AFF"], ["DOWNSTREAM", String(D.downstreamInfections || 0), "#FF7171"]];
  } else if (!D.infectedEncounters) {
    accent = "#58DBA6"; headerColor = "#58DBA6"; header = "// UNTOUCHED";
    g1c = "rgba(88,219,166,0.14)"; g2c = "rgba(42,173,154,0.10)";
    count = 0; countLabel = "INFECTED CONTACTS";
    message = "The outbreak never reached you. Zero exposure events — you moved through the room untouched.";
    stats = [["TIME HEALTHY", String(D.timeHealthy || "—"), "#58DBA6"], ["TIME MASKED", String(D.timeMasked || "—"), "#58DBA6"], ["TIME VAXED", String(D.timeVaxed || "—"), "#7387F9"]];
  } else {
    accent = "#58DBA6"; headerColor = "#866AFF"; header = "// CLOSE CALLS";
    g1c = "rgba(134,106,255,0.20)"; g2c = "rgba(88,219,166,0.12)";
    count = D.infectedEncounters; countLabel = "INFECTED CONTACTS";
    message = "Every infectious contact the outbreak threw at you — and how you held them off.";
    stats = [["TIME/CONTACT", String(D.avgEncounterDuration || "—"), "#866AFF"], ["WHILE MASKED", String(D.contactsMasked || 0), "#58DBA6"], ["WHILE VAXED", String(D.contactsVaccinated || 0), "#7387F9"]];
  }

  // ── Background ────────────────────────────────────────────────────────────
  ctx.fillStyle = "#08090A"; ctx.fillRect(0, 0, W, H);
  const rg1 = ctx.createRadialGradient(W * 0.06, 0, 60, W * 0.06, 0, H * 0.70);
  rg1.addColorStop(0, g1c); rg1.addColorStop(1, "rgba(8,9,10,0)");
  ctx.fillStyle = rg1; ctx.fillRect(0, 0, W, H);
  const rg2 = ctx.createRadialGradient(W, H, 80, W, H, H * 0.65);
  rg2.addColorStop(0, g2c); rg2.addColorStop(1, "rgba(8,9,10,0)");
  ctx.fillStyle = rg2; ctx.fillRect(0, 0, W, H);

  // ── Logo header ───────────────────────────────────────────────────────────
  const logo = await loadImg((window.__resources && window.__resources.appIcon) || "assets/app-icon.png");
  if (logo) { ctx.save(); rr(ctx, PAD, 100, 52, 52, 12); ctx.clip(); ctx.drawImage(logo, PAD, 100, 52, 52); ctx.restore(); }
  ctx.textAlign = "left"; ctx.fillStyle = "#5F676A"; ctx.font = '700 18px "Geist Mono"';
  ctx.fillText("OPERATION OUTBREAK", PAD + 68, 124);
  ctx.fillStyle = "#9BA3A6"; ctx.font = '600 15px "Geist Mono"';
  ctx.fillText(M.runId + "  ·  " + M.date, PAD + 68, 148);

  // ── Status eyebrow ────────────────────────────────────────────────────────
  ctx.textAlign = "left"; ctx.fillStyle = headerColor; ctx.font = '700 30px "Geist Mono"';
  ctx.fillText(header, PAD, 234);
  ctx.textAlign = "right"; ctx.fillStyle = "rgba(255,255,255,0.55)"; ctx.font = '600 26px "Geist Mono"';
  ctx.fillText(D.handle, W - PAD, 234);

  // ── Sonar ─────────────────────────────────────────────────────────────────
  const CX = W / 2, CY = 790, RMAX = 360;
  const hexToRgb = (h) => [parseInt(h.slice(1,3),16),parseInt(h.slice(3,5),16),parseInt(h.slice(5,7),16)];
  const [ar,ag_,ab] = hexToRgb(accent);
  const A = (a) => `rgba(${ar},${ag_},${ab},${a})`;
  const blipHex = infected ? "#DB3A3A" : accent;
  const [br,bg_,bb] = hexToRgb(blipHex);
  const B = (a) => `rgba(${br},${bg_},${bb},${a})`;

  // concentric rings
  [52,104,156,190].forEach((r0,i) => {
    const r = r0 / 190 * RMAX;
    ctx.beginPath(); ctx.arc(CX, CY, r, 0, Math.PI*2);
    ctx.strokeStyle = i === 3 ? A(0.16) : A(0.10); ctx.lineWidth = 2;
    ctx.setLineDash(i === 3 ? [5,18] : []); ctx.stroke();
  });
  ctx.setLineDash([]);

  // crosshairs
  [0,45,90,135,180,225,270,315].forEach(a => {
    const rad = a * Math.PI / 180;
    ctx.beginPath(); ctx.moveTo(CX, CY);
    ctx.lineTo(CX + Math.cos(rad)*RMAX, CY + Math.sin(rad)*RMAX);
    ctx.strokeStyle = "rgba(255,255,255,0.04)"; ctx.lineWidth = 1.4; ctx.stroke();
  });

  // blips (max 12 shown, matches jA/jR in app.jsx)
  const jA = [12,137,263,41,196,312,78,229,349,168,301,95];
  const jR = [72,152,112,140,94,170,134,106,152,122,160,128];
  for (let i = 0; i < Math.min(count, 12); i++) {
    const rad = jA[i % jA.length] * Math.PI / 180;
    const r = jR[i % jR.length] / 190 * RMAX;
    const bx = CX + Math.cos(rad)*r, by = CY + Math.sin(rad)*r;
    ctx.beginPath(); ctx.arc(bx, by, 16, 0, Math.PI*2);
    ctx.strokeStyle = B(0.30); ctx.lineWidth = 1.8; ctx.stroke();
    ctx.beginPath(); ctx.arc(bx, by, 6, 0, Math.PI*2);
    ctx.fillStyle = blipHex; ctx.globalAlpha = 0.7; ctx.fill(); ctx.globalAlpha = 1;
  }

  // ── Hero count ────────────────────────────────────────────────────────────
  const numStr = String(count);
  const numSize = numStr.length > 2 ? 210 : 270;
  ctx.textAlign = "center"; ctx.textBaseline = "middle";
  ctx.shadowColor = "rgba(8,9,10,0.95)"; ctx.shadowBlur = 44;
  ctx.fillStyle = "#F4F6F6"; ctx.font = `700 ${numSize}px "Geist Mono"`;
  ctx.fillText(numStr, CX, CY - 16);
  ctx.shadowBlur = 0;
  ctx.fillStyle = accent; ctx.font = '700 28px "Geist Mono"';
  ctx.fillText(countLabel, CX, CY + numSize * 0.5 + 32);
  ctx.textBaseline = "alphabetic";

  // ── Message ───────────────────────────────────────────────────────────────
  const msgTop = CY + RMAX + 40;
  ctx.textAlign = "center"; ctx.fillStyle = "rgba(255,255,255,0.50)"; ctx.font = '400 33px Manrope';
  const msgWords = message.split(" "); let line = "", msgLines = [];
  for (const w of msgWords) {
    const t = line ? line + " " + w : w;
    if (ctx.measureText(t).width > W - PAD*2 - 20 && line) { msgLines.push(line); line = w; } else line = t;
  }
  msgLines.push(line);
  msgLines.forEach((l, i) => ctx.fillText(l, CX, msgTop + i * 50));

  // ── Stat strip ────────────────────────────────────────────────────────────
  const statsTop = H - 300, statsH = 156;
  ctx.fillStyle = "rgba(255,255,255,0.04)"; rr(ctx, PAD, statsTop, W-PAD*2, statsH, 22); ctx.fill();
  ctx.strokeStyle = "rgba(255,255,255,0.09)"; ctx.lineWidth = 1.5; rr(ctx, PAD, statsTop, W-PAD*2, statsH, 22); ctx.stroke();
  const cw = (W - PAD*2) / stats.length;
  stats.forEach(([lbl, val, col], i) => {
    const cx_ = PAD + cw*i + cw/2;
    if (i > 0) { ctx.strokeStyle="rgba(255,255,255,0.09)"; ctx.lineWidth=1; ctx.beginPath(); ctx.moveTo(PAD+cw*i, statsTop+24); ctx.lineTo(PAD+cw*i, statsTop+statsH-24); ctx.stroke(); }
    ctx.textAlign = "center"; ctx.textBaseline = "middle";
    ctx.fillStyle = "#F4F6F6"; ctx.font = '700 44px "Geist Mono"';
    ctx.fillText(val, cx_, statsTop + 68);
    ctx.fillStyle = col; ctx.font = '700 18px "Geist Mono"';
    ctx.fillText(lbl, cx_, statsTop + 120);
    ctx.textBaseline = "alphabetic";
  });

  // ── Footer ────────────────────────────────────────────────────────────────
  ctx.textAlign = "left"; ctx.fillStyle = "#5F676A"; ctx.font = '600 20px "Geist Mono"';
  ctx.fillText("OPERATIONOUTBREAK.ORG", PAD, H - 76);
  ctx.fillStyle = accent; starPath(ctx, W - PAD - 8, H - 88, 18); ctx.fill();
}

/* ── PERSONALITY · 9:16 trading card ─────────────────────────────────────── */
async function renderPersonality(ctx, spec, bucket) {
  const p = window.OO_DATA.personalities[bucket], W = spec.w, H = spec.h, PAD = 80;
  const onGrad = p.mode === "gradient";
  const ink = onGrad ? "#FFFFFF" : p.color;
  ctx.fillStyle = p.mode === "gradient" ? "#160c0d" : "#0B0D11"; ctx.fillRect(0, 0, W, H);
  // themed wash
  if (p.mode === "gradient") {
    const g = ctx.createLinearGradient(0, 0, W, H); g.addColorStop(0, "rgba(255,113,113,0.30)"); g.addColorStop(1, "rgba(219,58,58,0.05)");
    ctx.fillStyle = g; ctx.fillRect(0, 0, W, H);
  } else {
    const g = ctx.createRadialGradient(W / 2, 720, 100, W / 2, 720, 900);
    g.addColorStop(0, accentAlpha(p.color, 0.20)); g.addColorStop(1, accentAlpha(p.color, 0));
    ctx.fillStyle = g; ctx.fillRect(0, 0, W, H);
  }
  // border frame
  ctx.strokeStyle = onGrad ? "rgba(255,255,255,0.35)" : accentAlpha(p.color, 0.5); ctx.lineWidth = 3; rr(ctx, 40, 40, W - 80, H - 80, 40); ctx.stroke();

  // telemetry header
  ctx.textAlign = "center"; ctx.font = '600 24px "Geist Mono"';
  const seg = [["CONTACT", p.contact], ["INFECTION", p.infection], ["COMPLIANCE", p.compliance]];
  let line = seg.map((s) => `${s[0]}: ${s[1]}`).join("   //   ");
  ctx.fillStyle = onGrad ? "rgba(255,255,255,0.92)" : "#9BA3A6"; ctx.fillText(line, W / 2, 180);

  // medallion
  const mcx = W / 2, mcy = 600;
  ctx.save(); ctx.shadowColor = onGrad ? "rgba(255,255,255,0.5)" : accentAlpha(p.color, 0.6); ctx.shadowBlur = 60;
  starPath(ctx, mcx, mcy, 210); ctx.strokeStyle = ink; ctx.lineWidth = 4; ctx.stroke();
  ctx.restore();
  starPath(ctx, mcx, mcy, 150); ctx.fillStyle = onGrad ? "rgba(255,255,255,0.14)" : accentAlpha(p.color, 0.12); ctx.fill();
  ctx.strokeStyle = onGrad ? "rgba(255,255,255,0.4)" : accentAlpha(p.color, 0.4); ctx.lineWidth = 2; ctx.stroke();
  const ic = await lucideImg(OO_ICON_MAP[p.icon] || "shield", ink, 1.5);
  if (ic) ctx.drawImage(ic, mcx - 95, mcy - 95, 190, 190);

  // tagline + name
  ctx.textAlign = "center"; ctx.fillStyle = ink; ctx.font = '700 30px "Geist Mono"';
  ctx.fillText(p.tagline.toUpperCase(), W / 2, 940);
  ctx.fillStyle = "#F4F6F6"; ctx.font = '300 110px Manrope';
  ctx.fillText(p.name, W / 2, 1060);

  // description
  ctx.fillStyle = onGrad ? "rgba(255,255,255,0.94)" : "#C7CDCE"; ctx.font = '400 38px Manrope';
  let y = 1200; const lines = [];
  // simple wrap centered
  const words = p.desc.split(" "); let ln = "";
  for (const w of words) { const t = ln ? ln + " " + w : w; if (ctx.measureText(t).width > W - PAD * 2 - 80 && ln) { lines.push(ln); ln = w; } else ln = t; }
  lines.push(ln);
  lines.forEach((l, i) => ctx.fillText(l, W / 2, y + i * 56));

  // footer
  const logo = await loadImg((window.__resources && window.__resources.appIcon) || "assets/app-icon.png");
  if (logo) { ctx.save(); rr(ctx, PAD, H - 170, 64, 64, 15); ctx.clip(); ctx.drawImage(logo, PAD, H - 170, 64, 64); ctx.restore(); }
  ctx.textAlign = "left"; ctx.fillStyle = onGrad ? "rgba(255,255,255,0.85)" : "#9BA3A6"; ctx.font = '700 22px "Geist Mono"';
  ctx.fillText("OPERATION OUTBREAK", PAD + 84, H - 142);
  ctx.fillStyle = onGrad ? "rgba(255,255,255,0.6)" : "#5F676A"; ctx.font = '600 18px "Geist Mono"';
  ctx.fillText("OPERATIONOUTBREAK.ORG", PAD + 84, H - 114);
  ctx.textAlign = "right"; ctx.fillStyle = ink; ctx.font = '600 24px "Geist Mono"';
  ctx.fillText(window.OO_DATA.individual.handle, W - PAD, H - 128);
}

/* ── Share modal UI ──────────────────────────────────────────────────────── */
function ShareModal({ share, onClose }) {
  const canvasRef = useRef(null);
  const [ready, setReady] = useState(false);
  const spec = share ? SHARE_SPECS[share.type] : null;

  useEffect(() => {
    if (!share) return;
    setReady(false);
    let cancelled = false;
    (async () => {
      await ensureFonts();
      await new Promise((r) => requestAnimationFrame(r));
      const c = canvasRef.current; if (!c || cancelled) return;
      c.width = spec.w; c.height = spec.h;
      const ctx = c.getContext("2d");
      ctx.textBaseline = "alphabetic";
      if (share.type === "group") await renderGroup(ctx, spec);
      else if (share.type === "individual") await renderIndividual(ctx, spec);
      else await renderPersonality(ctx, spec, share.bucket);
      if (!cancelled) setReady(true);
    })();
    return () => { cancelled = true; };
  }, [share]);

  const download = () => {
    const c = canvasRef.current; if (!c) return;
    c.toBlob((blob) => {
      const url = URL.createObjectURL(blob);
      const a = document.createElement("a");
      a.href = url; a.download = `${spec.file}.png`; a.click();
      setTimeout(() => URL.revokeObjectURL(url), 2000);
    }, "image/png");
  };

  const accent = share && share.type === "individual" ? T.purple : share && share.type === "personality" ? (window.OO_DATA.personalities[share.bucket].color) : T.mint;
  const vertical = spec && spec.h > spec.w;

  return (
    <Modal open={!!share} onClose={onClose} maxWidth={vertical ? 940 : 1180}>
      <div style={{ background: T.panel, border: `1px solid ${T.line}`, borderRadius: 24, overflow: "hidden", display: "flex", flexDirection: vertical ? "row" : "column" }}>
        <div style={{ flex: vertical ? "0 0 auto" : "none", display: "flex", justifyContent: "center", alignItems: "center", padding: 28, background: "#0A0C10", minHeight: vertical ? "auto" : 380 }}>
          <div style={{ position: "relative" }}>
            <canvas ref={canvasRef} style={{ display: "block", maxHeight: vertical ? "76vh" : "52vh", maxWidth: "100%", width: "auto", height: vertical ? "76vh" : "auto", borderRadius: 14, border: `1px solid ${T.line}`, boxShadow: "0 30px 80px -30px rgba(0,0,0,0.8)", opacity: ready ? 1 : 0 }} />
            {!ready && <div style={{ position: "absolute", inset: 0, display: "flex", alignItems: "center", justifyContent: "center", fontFamily: T.mono, fontSize: 13, letterSpacing: "0.1em", color: T.fg3 }}>RENDERING…</div>}
          </div>
        </div>
        <div style={{ flex: 1, padding: "28px 30px", display: "flex", flexDirection: "column", justifyContent: "center", minWidth: vertical ? 320 : "auto" }}>
          <Eyebrow color={accent}>SHARE ASSET · {spec && spec.label}</Eyebrow>
          <h3 style={{ fontFamily: T.sans, fontWeight: 300, fontSize: 32, color: T.fg, margin: "12px 0 0", letterSpacing: "-0.02em" }}>
            {share && share.type === "group" ? "Outbreak summary" : share && share.type === "individual" ? "Your report card" : "Your trading card"}
          </h3>
          <p style={{ fontFamily: T.sans, fontSize: 15, color: T.fg2, lineHeight: 1.55, marginTop: 12, maxWidth: 360 }}>
            A pixel-perfect, on-brand graphic ready for {spec && spec.platform}. Download the PNG and post it — or drop it straight into your story.
          </p>
          <div style={{ marginTop: 24, display: "flex", gap: 12, flexWrap: "wrap" }}>
            <button onClick={download} disabled={!ready} style={{
              display: "inline-flex", alignItems: "center", gap: 10, fontFamily: T.mono, fontSize: 13, fontWeight: 700, letterSpacing: "0.06em", textTransform: "uppercase",
              color: T.ink, background: ready ? accent : T.fg3, border: "none", padding: "14px 22px", borderRadius: 999, cursor: ready ? "pointer" : "default",
              boxShadow: ready ? `0 10px 30px -8px ${accentAlpha(accent, 0.6)}` : "none",
            }}>
              <Icon name="download" size={16} color={T.ink} stroke={2.25} /> Download PNG
            </button>
            <button onClick={onClose} style={{
              fontFamily: T.mono, fontSize: 13, fontWeight: 700, letterSpacing: "0.06em", textTransform: "uppercase",
              color: T.fg2, background: "transparent", border: `1px solid ${T.line}`, padding: "14px 22px", borderRadius: 999, cursor: "pointer",
            }}>Close</button>
          </div>
          <div style={{ marginTop: 22, fontFamily: T.mono, fontSize: 11, letterSpacing: "0.06em", color: T.fg3 }}>
            {spec && `${spec.w} × ${spec.h}px · PNG`}
          </div>
        </div>
      </div>
    </Modal>
  );
}
window.ShareModal = ShareModal;
