// admin-blog-image-generator.jsx — Blog image generator page
// CDN + in-browser-Babel / window-global style (no ES imports).
// React is the CDN global; Icon comes from admin-shell.jsx;
// Supabase client is window.supabase, configured in admin-service.js.

const { useState: useStateBI, useEffect: useEffectBI, useMemo: useMemoBI, useRef: useRefBI, useCallback: useCallbackBI } = React;
const IconBI = window.Icon;

const WF_LOGO = 'https://iwvlmpgeodctctmaacja.supabase.co/storage/v1/object/public/Logos/logo_wisefunnel_shape.png';
const SB_URL = 'https://iwvlmpgeodctctmaacja.supabase.co';
const SB_ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Iml3dmxtcGdlb2RjdGN0bWFhY2phIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjYwMzY3MTUsImV4cCI6MjA4MTYxMjcxNX0.hMs9OGPTdhPAs0lSKzDWE1Cc-K27NoW11--niolkoWY';
const SB_BUCKET = 'blog-posts';
const SB_PREFIX = 'images';
const SB_PUBLIC_BASE = `${SB_URL}/storage/v1/object/public/${SB_BUCKET}/${SB_PREFIX}/`;

let sbClientBI = null;
function getSbClient() {
  if (sbClientBI) return sbClientBI;
  if (!window.supabase || !window.supabase.createClient) return null;
  sbClientBI = window.supabase.createClient(SB_URL, SB_ANON_KEY);
  return sbClientBI;
}
const GEMINI_MODEL = 'gemini-2.5-flash-image';
const GEMINI_PROXY_URL = `${SB_URL}/functions/v1/get-gemini-api-key`;

const NICHES_BI = [
  { id:'solar',     name:'Solar',     sub:'installer' },
  { id:'roofing',   name:'Roofing',   sub:'roofer' },
  { id:'mortgage',  name:'Mortgage',  sub:'broker' },
  { id:'legal',     name:'Legal',     sub:'lawyer' },
  { id:'insurance', name:'Insurance', sub:'agent' },
  { id:'medicare',  name:'Medicare',  sub:'advisor' },
  { id:'auto',      name:'Auto',      sub:'dealer' },
  { id:'home',      name:'Home',      sub:'contractor' },
];

// WiseFunnel product screens — uploaded to Supabase storage at blog-posts/screens/
// Each is a real cropped screenshot of an app surface, used both as visual reference
// for Gemini (inline image URLs) and as thumbnail picker chips in the admin UI.
const SCREENS_BASE = `${SB_URL}/storage/v1/object/public/${SB_BUCKET}/screens/`;
const SCREENS_BI = [
  { name:'AB Testing',                     surface:'A/B Testing engine' },
  { name:'Ad Builder',                     surface:'AI Ad Blueprint generator' },
  { name:'Agency Dashboard',               surface:'Agency workspace dashboard' },
  { name:'CRM',                            surface:'Lead CRM' },
  { name:'Client Finder',                  surface:'Scout AI / Client Finder' },
  { name:'Email Outbound',                 surface:'Email & Pitch Machine' },
  { name:'Funnel Templates',               surface:'Funnel template gallery' },
  { name:'Integrations',                   surface:'Integrations settings' },
  { name:'Lead Buyers Profit Room',        surface:'Profit Room routing' },
  { name:'Lead Routing',                   surface:'Lead routing rules' },
  { name:'Workspace Settings - Funnels',   surface:'Workspace Funnels settings' },
  { name:'Workspace Settings - Seats',     surface:'Workspace Seats settings' },
].map(s => ({ ...s, url: SCREENS_BASE + encodeURIComponent(s.name) + '.webp' }));

// Templates pin elements to fixed positions on the 1200×630 (or selected) canvas.
// Each slot has a label + spatial instruction Gemini will translate into pixels.
const TEMPLATES_BI = [
  {
    id: 'freeform',
    title: 'Freeform',
    desc: 'No fixed positions — Gemini composes from the elements you toggled.',
    slots: [],
  },
  {
    id: 'integration',
    title: 'Integration',
    desc: 'Left source app → orange data flow → right WiseFunnel surface. Use for "How to send leads from X into WiseFunnel" posts.',
    slots: [
      { id:'left',  label:'Source app (left card)', position:'Far left of the canvas, ~30% width, tilted −2°, white card with rounded corners + drop shadow.', kinds:['screen','upload','logo'] },
      { id:'flow',  label:'Center data flow',       position:'Centered between the two cards: dashed orange arrow with a small "lead.json" packet riding it.', kinds:['arrow-fixed'] },
      { id:'right', label:'WiseFunnel surface (right card)', position:'Right of the canvas, ~38% width, tilted +1°, white card showing a WiseFunnel routing/CRM surface.', kinds:['screen','upload'] },
    ],
  },
  {
    id: 'workflow',
    title: 'Step-by-step workflow',
    desc: 'Three or four numbered cards laid out left→right, each showing one step. Use for setup guides.',
    slots: [
      { id:'step1', label:'Step 1 card', position:'Leftmost card (~22% width), numbered "1" badge top-left, screenshot or icon inside.', kinds:['screen','upload','icon'] },
      { id:'step2', label:'Step 2 card', position:'Second card (~22% width), connecting arrow from step 1.', kinds:['screen','upload','icon'] },
      { id:'step3', label:'Step 3 card', position:'Third card (~22% width), connecting arrow from step 2.', kinds:['screen','upload','icon'] },
      { id:'step4', label:'Step 4 card (optional)', position:'Rightmost card (~22% width), only render if a value is provided.', kinds:['screen','upload','icon'] },
    ],
  },
  {
    id: 'funnel-builder',
    title: 'Funnel builder showcase',
    desc: 'One large hero screenshot in the center with small UI callouts floating around it.',
    slots: [
      { id:'hero',  label:'Hero screenshot',         position:'Centered, ~60% width of the canvas, slight perspective tilt, large soft shadow, orange glow underneath.', kinds:['screen','upload'] },
      { id:'tl',    label:'Top-left callout (optional)', position:'Top-left of the hero, ~140px chip with an arrow pointing into the screenshot.', kinds:['callout'] },
      { id:'br',    label:'Bottom-right callout (optional)', position:'Bottom-right of the hero, ~140px chip with an arrow pointing into the screenshot.', kinds:['callout'] },
    ],
  },
  {
    id: 'comparison',
    title: 'Comparison (versus)',
    desc: 'Two side-by-side cards labeled A vs B. Use for competitor posts.',
    slots: [
      { id:'left',  label:'Left side (competitor)', position:'Left half (~45% width), gray/muted treatment, labeled with brand name. Slightly tilted −1°.', kinds:['screen','upload','logo'] },
      { id:'vs',    label:'VS divider',             position:'Vertical orange "vs" badge centered between the two cards.', kinds:['vs-fixed'] },
      { id:'right', label:'Right side (WiseFunnel)', position:'Right half (~45% width), orange accents and highlights. Slightly tilted +1°.', kinds:['screen','upload'] },
    ],
  },
  {
    id: 'results',
    title: 'Results / hero number',
    desc: 'Big hero number on the left, supporting screenshot on the right. Use for case-study and results posts.',
    slots: [
      { id:'number', label:'Hero number (left)',     position:'Left half: large display-weight number (160px+), serif, orange accent. Optional small caption below in mono caps.', kinds:['number'] },
      { id:'right',  label:'Right screenshot',       position:'Right half (~45% width), screenshot in a tilted card showing the surface that produced the number.', kinds:['screen','upload'] },
    ],
  },
];

const SLOT_KIND_LABELS = {
  screen:       'Preset screen',
  upload:       'Uploaded image',
  logo:         'Brand logo',
  icon:         '3D icon',
  callout:      'Text callout',
  number:       'KPI / hero number',
  'arrow-fixed':'Auto (orange arrow)',
  'vs-fixed':   'Auto (VS badge)',
};

const PLACEMENTS_BI = [
  { id:'cover',    label:'Cover',      w:1200, h:630,  col:'cover_image_url' },
  { id:'inline-1', label:'Inline 1',   w:1200, h:900,  col:'none' },
  { id:'inline-2', label:'Inline 2',   w:1200, h:900,  col:'none' },
  { id:'social',   label:'Social',     w:1080, h:1080, col:'og_image_url' },
  { id:'og',       label:'Open Graph', w:1200, h:675,  col:'og_image_url' },
  { id:'custom',   label:'Custom',     w:null, h:null, col:'none' },
];

// 3D icon library — loaded from window.ThreeDIconsList (admin-app/project/3dicons-data.js).
// Source: realvjy/3dicons (https://github.com/realvjy/3dicons) — 120 icons, 3 styles each.
const ICONS_BI = (window.ThreeDIconsList || []);

const ICON_STYLES_BI = [
  { id:'color',    label:'Color',    desc:'Vibrant, fully shaded' },
  { id:'gradient', label:'Gradient', desc:'Stylized gradient finish' },
  { id:'clay',     label:'Clay',     desc:'Soft matte clay look' },
];

const ICON_CATEGORY_LABELS = {
  'a':'Misc', 'art':'Art', 'at':'Symbols', 'content':'Content', 'document':'Document',
  'finance':'Finance', 'icons':'General', 'media':'Media', 'misc':'Misc',
  'money':'Money', 'money-coin':'Money', 'phone':'Phone', 'player':'Media',
  'playre':'Media', 'social':'Social', 'user':'User',
};

// Brand/competitor/integration logos that often appear in WiseFunnel posts.
// Selected logos get explicitly named in the prompt with brand-color guidance.
const LOGOS_BI = [
  // Competitors
  { id:'gohighlevel',  name:'GoHighLevel',   group:'Competitor' },
  { id:'clickfunnels', name:'ClickFunnels',  group:'Competitor' },
  { id:'typeform',     name:'Typeform',      group:'Competitor' },
  { id:'heyflow',      name:'Heyflow',       group:'Competitor' },
  { id:'leadpages',    name:'Leadpages',     group:'Competitor' },
  { id:'unbounce',     name:'Unbounce',      group:'Competitor' },
  { id:'convertflow',  name:'ConvertFlow',   group:'Competitor' },
  { id:'kajabi',       name:'Kajabi',        group:'Competitor' },
  // Ad / lead-gen platforms
  { id:'google-ads',   name:'Google Ads',    group:'Ad platform' },
  { id:'meta',         name:'Meta',          group:'Ad platform' },
  { id:'facebook',     name:'Facebook',      group:'Ad platform' },
  { id:'linkedin',     name:'LinkedIn',      group:'Ad platform' },
  { id:'tiktok',       name:'TikTok',        group:'Ad platform' },
  { id:'youtube',      name:'YouTube',       group:'Ad platform' },
  // Integrations
  { id:'stripe',       name:'Stripe',        group:'Integration' },
  { id:'slack',        name:'Slack',         group:'Integration' },
  { id:'twilio',       name:'Twilio',        group:'Integration' },
  { id:'resend',       name:'Resend',        group:'Integration' },
  { id:'findymail',    name:'Findymail',     group:'Integration' },
  { id:'firecrawl',    name:'Firecrawl',     group:'Integration' },
  { id:'calcom',       name:'Cal.com',       group:'Integration' },
  { id:'zapier',       name:'Zapier',        group:'Integration' },
  { id:'make',         name:'Make.com',      group:'Integration' },
  { id:'hubspot',      name:'HubSpot',       group:'Integration' },
  { id:'salesforce',   name:'Salesforce',    group:'Integration' },
  { id:'webhooks',     name:'Webhooks',      group:'Integration' },
  // AI providers
  { id:'gemini',       name:'Google Gemini', group:'AI' },
  { id:'anthropic',    name:'Anthropic Claude', group:'AI' },
  { id:'openai',       name:'OpenAI',        group:'AI' },
];

const STORAGE_BI = 'wf-blog-image-gen-admin-v1';

function loadStateBI() {
  const defaults = {
    md:'', slug:'', postTitle:'', mdFilename:'',
    placement:'cover', width:1200, height:630, theme:'dark',
    look:'brand', refImage:null, refImageMime:null,
    toggles:{
      logo:true, title:false, screens:false, people:false,
      threed:true, logos:false,
      numbers:false, agency:false, channels:false, supabase:true,
    },
    titleSrc:'auto', titleText:'',
    selectedScreens:[],
    selectedNiches:[],
    portrait:'full',
    selectedIcons:[],        // slugs from the 3dicons library
    iconStyle:'color',       // 'color' | 'gradient' | 'clay'
    iconQuery:'',            // search filter for the icon picker
    template:'freeform',     // layout template id
    slotFills:{},            // { [slotId]: { kind:'screen'|'upload'|'text', value:any } }
    selectedLogos:[],        // ids from LOGOS_BI
    customLogos:'',          // comma-separated extras typed by the user
    uploadedLogos:[],        // [{ name, dataUrl, mime }] — passed inline to Gemini as visual reference
    numbersText:'', numStyle:'hero',
    agencyName:'', agencyLoc:'', funnelName:'', dateRange:'',
    channels:{ google:'', meta:'', linkedin:'', tiktok:'' },
    engine:'gemini', apiKey:'',
    sbColumn:'cover_image_url',
  };
  try {
    const saved = JSON.parse(localStorage.getItem(STORAGE_BI) || 'null');
    if (saved) return { ...defaults, ...saved, toggles: { ...defaults.toggles, ...(saved.toggles||{}) }, channels: { ...defaults.channels, ...(saved.channels||{}) } };
  } catch (e) {}
  return defaults;
}

function persistStateBI(s) {
  try {
    const { lastImage, ...rest } = s;
    localStorage.setItem(STORAGE_BI, JSON.stringify(rest));
  } catch (e) {}
}

function parseFrontmatterBI(md) {
  if (!md) return {};
  const fmMatch = md.match(/^---\s*\n([\s\S]*?)\n---/);
  const fm = {};
  if (fmMatch) {
    fmMatch[1].split('\n').forEach(line => {
      const m = line.match(/^(\w[\w_-]*):\s*(.*?)\s*$/);
      if (m) fm[m[1]] = m[2].replace(/^["']|["']$/g, '');
    });
  }
  return fm;
}

function deriveTitleSlugBI(md) {
  const fm = parseFrontmatterBI(md);
  let title = fm.title || fm.seo_title;
  if (!title) {
    const h1 = md.match(/^#\s+(.+?)$/m);
    if (h1) title = h1[1].trim();
  }
  return { title: title || '', slug: fm.slug || '' };
}

function parseImageGuideBI(md) {
  const guideMatch = md && md.match(/##\s*🖼️?\s*Image Guide([\s\S]*?)(?=\n##\s|\n---\s*$|$)/i);
  if (!guideMatch) return null;
  const block = guideMatch[1];
  const guide = { cover: null, inlines: [] };

  // Walk every "### …" heading inside the guide block. Each heading + body is one image entry.
  const headingRegex = /^###\s+(.+?)\s*$/gm;
  const headings = [];
  let h;
  while ((h = headingRegex.exec(block)) !== null) {
    headings.push({ start: h.index, end: h.index + h[0].length, title: h[1].trim() });
  }

  headings.forEach((hd, i) => {
    const bodyStart = hd.end;
    const bodyEnd = i + 1 < headings.length ? headings[i + 1].start : block.length;
    const body = block.slice(bodyStart, bodyEnd);
    const entry = parseImgEntryBI(body);
    entry.heading = hd.title;

    // Cover
    if (/^cover/i.test(hd.title)) {
      // Pull dims from the heading too (e.g. "Cover Image (1200×630px)")
      const dimH = hd.title.match(/(\d+)\s*[×x]\s*(\d+)/);
      if (dimH && !entry.width)  entry.width  = parseInt(dimH[1], 10);
      if (dimH && !entry.height) entry.height = parseInt(dimH[2], 10);
      entry.kind = 'cover';
      entry.placementId = 'cover';
      entry.label = `Cover image${entry.width && entry.height ? ` (${entry.width}×${entry.height})` : ''}`;
      guide.cover = entry;
      return;
    }
    // Inline N
    const inlineM = hd.title.match(/inline\s*(?:image\s*)?(\d+)/i);
    if (inlineM) {
      const idx = parseInt(inlineM[1], 10);
      // Anchor — "After H2: …" or "After: …" or "Before H2: …"
      const anchorM = hd.title.match(/(?:after|before)\s*(?:h2:?)?\s*[""']?([^""']+?)[""']?\s*$/i);
      entry.kind = 'inline';
      entry.index = idx;
      entry.placementId = `inline-${idx}`;
      entry.anchor = anchorM ? anchorM[1].trim() : '';
      entry.label = `Inline ${idx}${entry.anchor ? ` — after "${entry.anchor.length > 48 ? entry.anchor.slice(0, 45) + '…' : entry.anchor}"` : ''}`;
      guide.inlines.push(entry);
    }
  });

  guide.inlines.sort((a, b) => a.index - b.index);
  return guide;
}

function parseImgEntryBI(text) {
  const out = { raw: text };
  const altM = text.match(/\*\*ALT(?:\s*text)?:\*\*\s*([^\n]+)/i);
  const promptM = text.match(/\*\*AI Prompt:\*\*\s*"?([^]*?)"?(?=\n\s*(?:-|\*\*|$))/i);
  const dimM = text.match(/\*\*Dimensions?:\*\*\s*(\d+)\s*[×x]\s*(\d+)/i);
  const captionM = text.match(/\*\*Caption:\*\*\s*([^\n]+)/i);
  const typeM = text.match(/\*\*Type:\*\*\s*([^\n]+)/i);
  if (altM)     out.alt = altM[1].trim();
  if (promptM)  out.prompt = promptM[1].replace(/^["']|["']$/g, '').trim();
  if (dimM)     { out.width = parseInt(dimM[1],10); out.height = parseInt(dimM[2],10); }
  if (captionM) out.caption = captionM[1].trim();
  if (typeM)    out.type = typeM[1].trim();
  return out;
}

// Flatten the parsed guide into a single ordered list for a dropdown.
function imageGuideOptionsBI(guide) {
  if (!guide) return [];
  const opts = [];
  if (guide.cover) opts.push({ id: 'cover', ...guide.cover });
  guide.inlines.forEach(e => opts.push({ id: e.placementId, ...e }));
  return opts;
}

function assemblePromptBI(s) {
  const guide = parseImageGuideBI(s.md);
  let guidedPrompt = null;
  if (guide) {
    if (s.placement === 'cover' && guide.cover?.prompt) guidedPrompt = guide.cover.prompt;
    else if (s.placement === 'inline-1') {
      const e = guide.inlines.find(i => i.index === 1);
      if (e?.prompt) guidedPrompt = e.prompt;
    } else if (s.placement === 'inline-2') {
      const e = guide.inlines.find(i => i.index === 2);
      if (e?.prompt) guidedPrompt = e.prompt;
    }
  }
  const lines = [];
  const themeBg = s.theme === 'light' ? 'warm paper beige (#FCFAF2)'
    : s.theme === 'duotone' ? 'navy-to-orange duotone (#1A2B3B → #F97316)'
    : 'dark midnight navy (#1A2B3B)';
  lines.push(`# WiseFunnel blog image · ${s.placement} · ${s.width}×${s.height}px`);
  if (s.postTitle) lines.push(`Post: ${s.postTitle}`);
  if (s.slug) lines.push(`Slug: ${s.slug}`);
  if (guidedPrompt) {
    lines.push('', '## Seed prompt from post Image Guide', guidedPrompt);
  }
  lines.push('', '## Look');
  if (s.look === 'brand') {
    lines.push('- Strict WiseFunnel brand: vivid orange (#F97316) accent, dark midnight navy (#1A2B3B) ink, warm paper beige (#FCFAF2) lights.');
    lines.push('- Typography: Inter for UI, Source Serif 4 for display, JetBrains Mono for captions.');
    lines.push('- Clean flat SaaS illustration style. No clip-art, no fake logos, no neon.');
  } else {
    lines.push('- Match the mood, palette and composition of the user-provided reference image.');
  }
  lines.push(`- Background: ${themeBg}.`);
  lines.push('- Color cues: emerald green (#10B981) for verified/positive, soft coral red (#EF4444) for rejected/negative, crisp white (#FFFFFF) for UI elements.');

  if (s.toggles.threed && s.selectedIcons.length) {
    const style = s.iconStyle || 'color';
    lines.push('', `## 3D icons (style: ${style})`);
    lines.push(`Render in the 3dicons.co "${style}" style — soft volumetric 3D objects, studio lighting, drop shadow, isolated on the background.`);
    s.selectedIcons.forEach(slug => {
      const ic = ICONS_BI.find(i => i.slug === slug);
      if (!ic) return;
      const url = ic[style] || ic.color || ic.gradient || ic.clay;
      lines.push(`- "${ic.title}" 3D icon${url ? ` (reference: ${url})` : ''}.`);
    });
    lines.push('- Compose icons at varied depths and rotations; foreground crisp, mid-ground softened, background slightly blurred (parallax).');
  }
  if (s.toggles.logos) {
    const named = s.selectedLogos.map(id => (LOGOS_BI.find(l => l.id === id) || {}).name).filter(Boolean);
    const customs = (s.customLogos || '').split(',').map(x => x.trim()).filter(Boolean);
    const uploads = (s.uploadedLogos || []).map(l => l.name);
    const all = [...named, ...customs, ...uploads];
    if (all.length) {
      lines.push('', '## Brand logos to render');
      lines.push('Render each logo accurately, using the brand’s real wordmark or symbol and its real brand colors — not generic placeholders.');
      named.forEach(name   => lines.push(`- ${name}`));
      customs.forEach(name => lines.push(`- ${name}`));
      uploads.forEach(name => lines.push(`- ${name} (see attached reference image — match exactly)`));
      lines.push('- Lay them out as small framed cards or floating chips, sized consistently.');
      lines.push('- Do NOT invent fake logos for these brands; if uncertain, use the brand name as a clean wordmark.');
    }
  }
  if (s.toggles.people && s.selectedNiches.length) {
    const niches = s.selectedNiches.map(id => NICHES_BI.find(n => n.id===id)).filter(Boolean);
    lines.push('', '## Real professionals (no emojis, no illustrations of people)');
    niches.forEach(n => lines.push(`- Real photograph of a ${n.sub} (${n.name} niche), circle-cropped portrait, friendly natural light. NOT an emoji or cartoon.`));
    const treat = { full:'Full color, untouched.', navy:'Navy duotone treatment to fit dark theme.', ring:'Wrapped in a 4px orange ring (#F97316).' }[s.portrait];
    lines.push(`- Treatment: ${treat}`);
  }
  if (s.toggles.screens && s.selectedScreens.length) {
    lines.push('', '## Product screenshots');
    s.selectedScreens.forEach(scr => {
      const meta = SCREENS_BI.find(x => x.name === scr);
      const url = meta?.url ? ` (reference: ${meta.url})` : '';
      const surface = meta?.surface ? ` — ${meta.surface}` : '';
      lines.push(`- ${scr}${surface}, framed in a tilted card with subtle shadow${url}.`);
    });
  }

  // Layout template — fixed spatial slots
  const tpl = TEMPLATES_BI.find(t => t.id === s.template) || TEMPLATES_BI[0];
  if (tpl.slots.length) {
    lines.push('', `## Layout template: ${tpl.title}`);
    lines.push(tpl.desc);
    lines.push('Fixed slot positions (do not deviate):');
    tpl.slots.forEach(slot => {
      const fill = (s.slotFills || {})[slot.id];
      const value = fill?.value;
      if (!fill || value == null || value === '') {
        // Some slots are auto-rendered fixtures (arrow, vs); render even when empty.
        if (slot.kinds.includes('arrow-fixed') || slot.kinds.includes('vs-fixed')) {
          lines.push(`- ${slot.label}: ${slot.position}`);
        }
        return;
      }
      let what = '';
      if (fill.kind === 'screen') {
        const meta = SCREENS_BI.find(x => x.name === value);
        const url = meta?.url ? ` (reference: ${meta.url})` : '';
        what = `WiseFunnel "${value}" screenshot${url}`;
      } else if (fill.kind === 'upload') {
        what = `the uploaded image labeled "${fill.label || 'upload'}" (attached as inline reference — match its content closely)`;
      } else if (fill.kind === 'text') {
        what = `text: "${value}"`;
      } else {
        what = String(value);
      }
      lines.push(`- ${slot.label}: ${slot.position} → render ${what}.`);
    });
  }
  if (s.toggles.numbers && s.numbersText) {
    lines.push('', '## Numbers / KPIs');
    lines.push(`- ${s.numbersText}`);
    const note = { hero:'one huge hero number (Source Serif or display weight, 160px+)', 'before-after':'before → after pairing with arrow between', grid:'2×2 stat grid with labels in mono caps', ribbon:'banner ribbon with result' }[s.numStyle] || 'hero number';
    lines.push(`- Style: ${note}.`);
  }
  if (s.toggles.title) {
    lines.push('', '## Headline');
    if (s.titleSrc === 'custom' && s.titleText) lines.push(`- Bake headline: "${s.titleText}"`);
    else if (s.postTitle) lines.push(`- Bake post title verbatim: "${s.postTitle}"`);
    else lines.push('- Use the post title verbatim.');
  }
  if (s.toggles.agency && (s.agencyName || s.funnelName)) {
    lines.push('', '## Agency callout (top-right tag)');
    if (s.funnelName) lines.push(`- Funnel: ${s.funnelName}`);
    if (s.agencyName) lines.push(`- Agency: ${s.agencyName}${s.agencyLoc?` · ${s.agencyLoc}`:''}`);
    if (s.dateRange)  lines.push(`- Window: ${s.dateRange}`);
  }
  if (s.toggles.channels && Object.values(s.channels).some(Boolean)) {
    lines.push('', '## Ad-channel strip');
    Object.entries(s.channels).forEach(([k,v]) => { if (v) lines.push(`- ${k}: ${v}`); });
  }
  if (s.toggles.logo) {
    lines.push('', '## Logo');
    lines.push('- WiseFunnel orange flag mark + "wisefunnel" wordmark, top-left, 28–40px tall.');
  }
  lines.push('', '## Hard constraints');
  lines.push('- No emoji characters. Use real photography or rendered 3D objects only.');
  lines.push('- No clip-art. No stock illustrations. No fake or invented logos.');
  lines.push('- Hold consistent depth: foreground elements crisp, mid-ground soft, background blurred.');
  lines.push(`- Output exactly ${s.width}×${s.height}px.`);
  lines.push('- No text in the image unless explicitly named above (headline / KPI numbers).');

  // Exclusion rules — only allow brand marks that were explicitly toggled on.
  if (!s.toggles.logo) {
    lines.push('- Do NOT include the WiseFunnel logo, the WiseFunnel orange flag mark, the "wisefunnel" wordmark, or any text containing the word "wisefunnel" anywhere in the image. The WiseFunnel logo toggle is OFF.');
    lines.push('- Do NOT invent a WiseFunnel-looking logo even if the post topic mentions WiseFunnel.');
  }
  const anyLogos =
    s.toggles.logos && (
      s.selectedLogos.length ||
      (s.customLogos || '').split(',').map(x=>x.trim()).filter(Boolean).length ||
      (s.uploadedLogos || []).length
    );
  if (!anyLogos) {
    lines.push('- Do NOT include any third-party brand logos, wordmarks, or company symbols (Google, Meta, Stripe, GoHighLevel, ClickFunnels, etc.). No third-party logos were selected.');
  } else {
    lines.push('- Do NOT add brand logos beyond the ones explicitly listed in the "Brand logos to render" section above. No fillers, no extras.');
  }

  return lines.join('\n');
}

// ── Reusable UI primitives ───────────────────────────────────────────────
function PillBI({ active, onClick, children, meta }) {
  return (
    <button type="button" onClick={onClick} style={{
      border: `1.5px solid ${active ? '#1A2B3B' : '#E5E7EB'}`,
      background: active ? '#1A2B3B' : '#fff',
      color: active ? '#fff' : '#1A2B3B',
      borderRadius: 9999,
      padding: '8px 14px',
      fontSize: 12, fontWeight: 600,
      cursor: 'pointer', fontFamily: 'inherit',
      display: 'inline-flex', alignItems: 'center', gap: 6,
      transition: 'all 120ms',
    }}>
      {children}
      {meta != null && <span style={{ fontFamily: 'JetBrains Mono, monospace', fontSize: 10.5, opacity: active ? 0.65 : 0.55 }}>{meta}</span>}
    </button>
  );
}

function FieldLabelBI({ children, hint }) {
  return (
    <label style={{ display:'block', fontSize: 11, fontWeight: 700, letterSpacing: '0.06em', textTransform: 'uppercase', color: '#1A2B3B', margin: '14px 0 6px' }}>
      {children}
      {hint && <span style={{ fontWeight:500, color:'#9CA3AF', textTransform:'none', letterSpacing:0, marginLeft: 6 }}>{hint}</span>}
    </label>
  );
}

function SwitchBI({ on }) {
  // Display-only: the parent ToggleRowBI owns the click. This avoids the
  // double-fire bug where both the switch and the row would each toggle state.
  return (
    <div role="switch" aria-checked={on} style={{
      width: 38, height: 22, borderRadius: 9999,
      background: on ? '#F97316' : '#D1D5DB',
      position: 'relative', cursor: 'pointer',
      transition: 'background 140ms', flexShrink: 0,
      pointerEvents: 'none',
    }}>
      <div style={{
        position: 'absolute', top: 3, left: on ? 19 : 3,
        width: 16, height: 16, borderRadius: '50%',
        background: '#fff', transition: 'left 140ms',
        boxShadow: '0 1px 3px rgba(0,0,0,0.18)',
      }} />
    </div>
  );
}

function ToggleRowBI({ title, desc, on, onToggle }) {
  return (
    <div onClick={onToggle} style={{
      display:'flex', alignItems:'center', justifyContent:'space-between',
      padding:'12px 14px', background:'#fff',
      border:'1px solid #E5E7EB', borderRadius: 10,
      marginBottom: 8, cursor: 'pointer',
      userSelect: 'none',
    }}>
      <div style={{ display:'flex', flexDirection:'column', gap:2 }}>
        <div style={{ fontWeight: 700, fontSize: 13, color: '#1A2B3B' }}>{title}</div>
        <div style={{ fontSize: 11.5, color: '#6B7280' }}>{desc}</div>
      </div>
      <SwitchBI on={on} />
    </div>
  );
}

function TextInputBI({ value, onChange, placeholder, mono, multiline, rows }) {
  const base = {
    width: '100%',
    border: '1.5px solid #E5E7EB',
    borderRadius: 10,
    padding: '11px 14px',
    fontFamily: mono ? 'JetBrains Mono, monospace' : 'inherit',
    fontSize: 13,
    background: '#fff',
    color: '#1A2B3B',
    outline: 'none',
    resize: 'vertical',
  };
  if (multiline) {
    return <textarea value={value || ''} onChange={onChange} placeholder={placeholder} rows={rows || 5} style={{ ...base, lineHeight: 1.5, minHeight: 100 }} />;
  }
  return <input value={value || ''} onChange={onChange} placeholder={placeholder} style={base} />;
}

function StatusBI({ kind, children }) {
  const colors = {
    ok:   { bg:'#DCFCE7', fg:'#166534' },
    warn: { bg:'#FEF3C7', fg:'#92400E' },
    err:  { bg:'#FEE2E2', fg:'#991B1B' },
  }[kind || 'ok'];
  return (
    <div style={{ display:'inline-flex', alignItems:'center', gap:8, padding:'6px 12px', borderRadius:9999, fontSize:12, fontWeight:600, background:colors.bg, color:colors.fg, marginTop:6 }}>
      <div style={{ width:6, height:6, borderRadius:'50%', background:'currentColor' }} />
      {children}
    </div>
  );
}

function SectionBI({ stepNum, title, subtitle, openByDefault, children }) {
  const [open, setOpen] = useStateBI(openByDefault !== false);
  return (
    <div style={{ marginBottom: 26, paddingBottom: 26, borderBottom: '1px solid #E5E7EB' }}>
      <button type="button" onClick={() => setOpen(o => !o)} style={{
        width:'100%', background:'transparent', border:'none',
        textAlign:'left', cursor:'pointer', padding:'10px 12px',
        margin:'0 -12px', borderRadius:12, fontFamily:'inherit', color:'inherit',
        transition:'background 140ms', display:'block',
      }}
      onMouseEnter={e => e.currentTarget.style.background = 'rgba(249,115,22,0.06)'}
      onMouseLeave={e => e.currentTarget.style.background = 'transparent'}
      >
        <div style={{ display:'flex', alignItems:'flex-start', justifyContent:'space-between', gap:16 }}>
          <span style={{ fontFamily:'JetBrains Mono, monospace', fontSize:10, color:'#F97316', fontWeight:700, letterSpacing:'0.14em', textTransform:'uppercase', flex:1, paddingTop:9 }}>
            Step {stepNum}
          </span>
          <span style={{
            width:32, height:32, borderRadius:'50%', background:'#F97316',
            display:'flex', alignItems:'center', justifyContent:'center', flexShrink:0,
            transform: open ? 'rotate(0deg)' : 'rotate(-90deg)',
            transition:'transform 240ms cubic-bezier(.4,.0,.2,1)',
            boxShadow:'0 4px 10px -3px rgba(249,115,22,0.45)',
          }}>
            <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#1A2B3B" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round">
              <path d="M6 9l6 6 6-6"/>
            </svg>
          </span>
        </div>
        <h2 style={{ fontSize: 22, fontWeight: 700, color: '#1A2B3B', margin: '4px 0', lineHeight: 1.2 }}>{title}</h2>
        <p style={{ fontSize: 13, color: '#6B7280', margin: 0, lineHeight: 1.55, maxWidth: 620 }}>{subtitle}</p>
      </button>
      {open && <div style={{ paddingTop: 6 }}>{children}</div>}
    </div>
  );
}

// ── Main page ────────────────────────────────────────────────────────────
function BlogImageGeneratorPage() {
  const [s, setS] = useStateBI(loadStateBI);
  const [lastImage, setLastImage] = useStateBI(null);
  const [genBusy, setGenBusy] = useStateBI(false);
  const [saveBusy, setSaveBusy] = useStateBI(false);
  const [saveResult, setSaveResult] = useStateBI(null);
  const [toast, setToast] = useStateBI(null);
  const [keyShown, setKeyShown] = useStateBI(false);
  const mdFileRef = useRefBI(null);
  const refFileRef = useRefBI(null);
  const importInputRef = useRefBI(null);

  useEffectBI(() => { persistStateBI(s); }, [s]);

  const showToast = useCallbackBI((msg, kind) => {
    setToast({ msg, kind: kind || 'ok', id: Date.now() });
    setTimeout(() => setToast(null), 3500);
  }, []);

  const placement = useMemoBI(() => PLACEMENTS_BI.find(p => p.id === s.placement) || PLACEMENTS_BI[0], [s.placement]);

  // When placement changes via picker, reset width/height + default column
  const setPlacement = (id) => {
    const p = PLACEMENTS_BI.find(x => x.id === id) || PLACEMENTS_BI[0];
    setS(prev => ({
      ...prev,
      placement: id,
      width:  p.w  != null ? p.w  : prev.width,
      height: p.h != null ? p.h : prev.height,
      sbColumn: p.col,
    }));
  };

  const setToggle = (key) => setS(prev => ({ ...prev, toggles: { ...prev.toggles, [key]: !prev.toggles[key] } }));
  const toggleInArr = (key, val) => setS(prev => {
    const arr = prev[key];
    const next = arr.includes(val) ? arr.filter(x => x !== val) : [...arr, val];
    return { ...prev, [key]: next };
  });

  const onPasteMd = (md) => {
    const { title, slug } = deriveTitleSlugBI(md);
    setS(prev => ({
      ...prev,
      md,
      postTitle: title || prev.postTitle,
      slug: slug || prev.slug,
    }));
  };

  const onLoadMdFile = async (file) => {
    if (!file) return;
    const text = await file.text();
    const { title, slug } = deriveTitleSlugBI(text);
    setS(prev => ({
      ...prev,
      md: text,
      mdFilename: file.name,
      postTitle: title || prev.postTitle,
      slug: slug || prev.slug,
    }));
    showToast(`Loaded ${file.name}`, 'ok');
  };

  const onLoadRefImage = async (file) => {
    if (!file) return;
    const reader = new FileReader();
    reader.onload = e => {
      setS(prev => ({ ...prev, refImage: e.target.result, refImageMime: file.type }));
      showToast('Reference image attached', 'ok');
    };
    reader.readAsDataURL(file);
  };

  const fullPrompt = useMemoBI(() => assemblePromptBI(s), [s]);
  const fileName = useMemoBI(() => `${s.slug || '[slug]'}-${s.placement}.webp`, [s.slug, s.placement]);
  const publicUrl = useMemoBI(() => `${SB_PUBLIC_BASE}${fileName}`, [fileName]);

  const copyPrompt = async () => {
    try {
      await navigator.clipboard.writeText(fullPrompt);
      showToast('Prompt copied', 'ok');
    } catch (e) {
      showToast('Copy failed', 'err');
    }
  };

  const generate = async () => {
    if (s.engine === 'prompt-only') return copyPrompt();
    if (!s.slug)   return showToast('Need a slug — pick a post or fill it in', 'err');
    setGenBusy(true);
    setLastImage(null);
    try {
      const parts = [{ text: fullPrompt + `\n\nReturn a single image at ${s.width}×${s.height}px.` }];
      if (s.look === 'reference' && s.refImage) {
        const m = /^data:([^;]+);base64,(.*)$/.exec(s.refImage);
        if (m) parts.push({ inlineData: { mimeType: m[1], data: m[2] } });
      }
      // Attach uploaded logos so Gemini can match the actual marks visually
      if (s.toggles.logos && (s.uploadedLogos || []).length) {
        s.uploadedLogos.forEach(logo => {
          const m = /^data:([^;]+);base64,(.*)$/.exec(logo.dataUrl);
          if (m) parts.push({ inlineData: { mimeType: m[1], data: m[2] } });
        });
      }
      // Attach slot uploads (per-slot custom screenshots) inline
      const fills = s.slotFills || {};
      for (const slotId of Object.keys(fills)) {
        const f = fills[slotId];
        if (f?.kind !== 'upload' || !f?.value) continue;
        const m = /^data:([^;]+);base64,(.*)$/.exec(f.value);
        if (m) parts.push({ inlineData: { mimeType: m[1], data: m[2] } });
      }
      const r = await fetch(GEMINI_PROXY_URL, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          // Supabase edge functions require the anon key on the Authorization header
          'Authorization': `Bearer ${SB_ANON_KEY}`,
          'apikey': SB_ANON_KEY,
        },
        body: JSON.stringify({
          model: GEMINI_MODEL,
          contents: [{ role: 'user', parts }],
          generationConfig: { responseModalities: ['IMAGE'] },
        }),
      });
      const text = await r.text();
      let data;
      try { data = JSON.parse(text); }
      catch { throw new Error(`Proxy returned non-JSON: ${text.slice(0, 200)}`); }
      if (!r.ok) throw new Error(data.error?.message || data.error || `Gemini proxy ${r.status}`);
      const cand = data?.candidates?.[0];
      const inline = cand?.content?.parts?.find(p => p.inlineData || p.inline_data);
      const blob = inline?.inlineData || inline?.inline_data;
      if (!blob?.data) {
        const reason = cand?.finishReason ? ` (${cand.finishReason})` : '';
        throw new Error(`Gemini returned no image data${reason}`);
      }
      // Immediately compress PNG → WebP so the preview, localStorage, and Save
      // pipeline all work with a small payload. Gemini PNGs can be 3–6 MB; WebP
      // at q=0.88 typically lands at 400–900 KB for 1200×630.
      const srcMime = blob.mimeType || blob.mime_type || 'image/png';
      const result = await compressBase64ToWebP(blob.data, srcMime, { quality: 0.88, maxWidth: Math.max(s.width, 1200), maxHeight: Math.max(s.height, 1200) });
      setLastImage({
        base64: result.base64,
        mime: result.mime,
        originalBytes: result.originalBytes,
        newBytes: result.newBytes,
        width: result.width,
        height: result.height,
        source: 'gemini',
      });
      showToast(`Rendered · ${fmtBytes(result.originalBytes)} ${srcMime.split('/')[1].toUpperCase()} → ${fmtBytes(result.newBytes)} WebP`, 'ok', 5500);
    } catch (e) {
      showToast(`Generate failed: ${e.message}`, 'err');
    } finally {
      setGenBusy(false);
    }
  };

  // Import an externally-generated image — compresses to WebP in-browser, then
  // drops the compressed result into lastImage so Save uploads a small file.
  const importExternalImage = (file) => {
    if (!file) return;
    if (!/^image\//.test(file.type)) {
      showToast('Pick an image file (PNG / JPG / WebP)', 'err');
      return;
    }
    const reader = new FileReader();
    reader.onload = async e => {
      const dataUrl = e.target.result;
      const m = /^data:([^;]+);base64,(.*)$/.exec(dataUrl);
      if (!m) { showToast('Could not read file', 'err'); return; }
      const result = await compressBase64ToWebP(m[2], m[1], { quality: 0.88, maxWidth: Math.max(s.width, 1600), maxHeight: Math.max(s.height, 1600) });
      setLastImage({
        base64: result.base64,
        mime: result.mime,
        originalBytes: result.originalBytes,
        newBytes: result.newBytes,
        width: result.width,
        height: result.height,
        source: 'import',
      });
      setSaveResult(null);
      showToast(`Imported ${file.name} · ${fmtBytes(result.originalBytes)} → ${fmtBytes(result.newBytes)} WebP`, 'ok', 5500);
    };
    reader.readAsDataURL(file);
  };

  const downloadImage = () => {
    if (!lastImage) return;
    const a = document.createElement('a');
    a.href = `data:${lastImage.mime};base64,${lastImage.base64}`;
    a.download = fileName.replace('.webp', '.png');
    a.click();
  };

  const saveImage = async () => {
    if (!lastImage) return showToast('Generate an image first', 'warn');
    if (!s.slug) return showToast('Slug required', 'err');
    const db = getSbClient();
    if (!db) return showToast('Supabase client not loaded', 'err');
    setSaveBusy(true);
    setSaveResult(null);
    const warnings = [];
    let storagePath = null, uploadedUrl = null, dbUpdated = null;
    try {
      // lastImage was already WebP-compressed at Generate / Import time, so just
      // turn the base64 back into a Blob. If for any reason it's still PNG, fall
      // back to a final pngToWebp pass.
      const byteString = atob(lastImage.base64);
      const buf = new ArrayBuffer(byteString.length);
      const arr = new Uint8Array(buf);
      for (let i = 0; i < byteString.length; i++) arr[i] = byteString.charCodeAt(i);
      const blob = lastImage.mime === 'image/webp'
        ? new Blob([buf], { type: 'image/webp' })
        : await pngToWebp(buf, lastImage.mime);
      const ext = blob.type === 'image/webp' ? 'webp' : 'png';
      const objectName = `${s.slug}-${s.placement}.${ext}`;
      const objectKey = `${SB_PREFIX}/${objectName}`;
      const { error: upErr } = await db.storage
        .from(SB_BUCKET)
        .upload(objectKey, blob, { contentType: blob.type, upsert: true });
      if (upErr) {
        warnings.push(`Upload error: ${upErr.message}`);
      } else {
        storagePath = `${SB_BUCKET}/${objectKey}`;
        const { data: pub } = db.storage.from(SB_BUCKET).getPublicUrl(objectKey);
        // refresh ref so subsequent saves get a fresh client

        uploadedUrl = pub?.publicUrl || `${SB_PUBLIC_BASE}${objectName}`;
        // Append cache-buster
        uploadedUrl = `${uploadedUrl}?v=${Date.now()}`;
      }
      // Patch DB row
      if (s.toggles.supabase && s.sbColumn && s.sbColumn !== 'none' && uploadedUrl) {
        const { data, error: dbErr } = await db
          .from('blog_posts')
          .update({ [s.sbColumn]: uploadedUrl, updated_at: new Date().toISOString() })
          .eq('slug', s.slug)
          .select('id, slug');
        if (dbErr) warnings.push(`DB update error: ${dbErr.message}`);
        else if (!data?.length) warnings.push(`No blog_posts row matched slug "${s.slug}".`);
        else dbUpdated = `blog_posts.${s.sbColumn} for slug=${s.slug}`;
      }
      setSaveResult({ storagePath, uploadedUrl, dbUpdated, warnings });
      if (uploadedUrl) showToast('Saved to Supabase storage', 'ok');
      else showToast('Save finished with warnings', 'warn');
    } catch (e) {
      showToast(`Save failed: ${e.message}`, 'err');
      setSaveResult({ warnings: [`Save threw: ${e.message}`] });
    } finally {
      setSaveBusy(false);
    }
  };

  // ── Render ──
  const themeBgPreview = s.theme === 'light' ? '#FCFAF2'
    : s.theme === 'duotone' ? 'linear-gradient(135deg,#1A2B3B 0%,#1A2B3B 60%,#F97316 100%)'
    : '#0F1A24';

  return (
    <div style={{ maxWidth: 1240, margin: '0 auto', padding: '8px 12px 80px' }}>
      <div style={{ display: 'grid', gridTemplateColumns: 'minmax(0, 1fr) 380px', gap: 32 }}>

        {/* ===== LEFT (form) ===== */}
        <div>
          <div style={{ display:'flex', alignItems:'center', gap:10, marginBottom: 16 }}>
            <img src={WF_LOGO} alt="" style={{ width: 32, height: 32 }} />
            <div>
              <div style={{ fontWeight: 800, fontSize: 16, color: '#1A2B3B' }}>Blog image generator</div>
              <div style={{ fontFamily:'JetBrains Mono, monospace', fontSize: 10.5, color: '#9CA3AF', letterSpacing:'0.04em' }}>
                Blog tools / Image generator
              </div>
            </div>
          </div>

          <h1 style={{ fontFamily:'Georgia, serif', fontWeight: 500, fontSize: 36, lineHeight: 1.1, letterSpacing:'-0.02em', margin: '4px 0 8px', color:'#1A2B3B' }}>
            Blog image generator <em style={{ color:'#F97316', fontWeight: 500 }}>that matches our brand.</em>
          </h1>
          <p style={{ fontSize: 14, lineHeight: 1.6, color:'#6B7280', margin: '0 0 28px', maxWidth: 640 }}>
            Paste or upload the post markdown, pick what to include, and ship pixel-perfect cover and inline images. Output uploads to the <code style={{ background:'#F3F4F6', padding:'1px 6px', borderRadius:4 }}>blog-posts</code> bucket and patches the matching row.
          </p>

          {/* STEP 01 — Source */}
          <SectionBI stepNum="01 · Source" title="Drop the blog post." subtitle="Auto-extracts title and slug from frontmatter. Image Guide block (if present) seeds the prompt for cover / inline-1 / inline-2.">
            <FieldLabelBI>Upload .md file</FieldLabelBI>
            <div onClick={() => mdFileRef.current?.click()}
              onDragOver={e => { e.preventDefault(); e.currentTarget.style.background = '#FFF8EE'; }}
              onDragLeave={e => { e.currentTarget.style.background = '#fff'; }}
              onDrop={e => { e.preventDefault(); e.currentTarget.style.background = '#fff'; onLoadMdFile(e.dataTransfer.files[0]); }}
              style={{ border:'1.5px dashed #E5E7EB', borderRadius:12, padding:18, textAlign:'center', background:'#fff', color:'#6B7280', fontSize:12.5, cursor:'pointer' }}>
              <div style={{ color:'#1A2B3B', fontSize: 13, fontWeight: 700, marginBottom: 3 }}>Drop the post's .md file here</div>
              or click to browse.
              <input ref={mdFileRef} type="file" accept=".md,.markdown,text/markdown,text/plain" style={{ display:'none' }}
                onChange={e => onLoadMdFile(e.target.files[0])} />
            </div>
            {s.mdFilename && <StatusBI kind="ok">Loaded: {s.mdFilename}</StatusBI>}

            <FieldLabelBI>Or paste the markdown directly</FieldLabelBI>
            <TextInputBI multiline rows={6} value={s.md} mono
              onChange={e => onPasteMd(e.target.value)}
              placeholder={`---\ntitle: WiseFunnel vs ClickFunnels...\nslug: wisefunnel-vs-clickfunnels-agencies\n---\n# H1...\n\nPaste the post body so the generator picks up KPIs, niche, and the Image Guide section.`} />

            <FieldLabelBI hint="— used for the filename. Auto-filled from frontmatter when detected.">Post slug</FieldLabelBI>
            <TextInputBI value={s.slug} mono
              onChange={e => setS(prev => ({ ...prev, slug: e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/-+/g, '-') }))}
              placeholder="wisefunnel-vs-clickfunnels-agencies" />
            <div style={{ fontFamily:'JetBrains Mono, monospace', fontSize: 11, color:'#9CA3AF', marginTop: 6 }}>
              Saves as <b style={{ color:'#1A2B3B' }}>{fileName}</b> in <code>blog-posts/{SB_PREFIX}/</code>
            </div>

            <ImageGuidePickerBI s={s} setS={setS} />
          </SectionBI>

          {/* STEP 02 — Image spec */}
          <SectionBI stepNum="02 · Image spec" title="Which image are we making?" subtitle="Pick the placement. Custom lets you set arbitrary dimensions.">
            <FieldLabelBI>Placement & aspect ratio</FieldLabelBI>
            <div style={{ display:'flex', flexWrap:'wrap', gap:6 }}>
              {PLACEMENTS_BI.map(p => (
                <PillBI key={p.id} active={s.placement === p.id} onClick={() => setPlacement(p.id)} meta={p.w ? `${p.w} × ${p.h}` : null}>
                  {p.label}
                </PillBI>
              ))}
            </div>
            {s.placement === 'custom' && (
              <div style={{ display:'grid', gridTemplateColumns:'1fr 1fr', gap:10, marginTop:12 }}>
                <TextInputBI value={s.width} onChange={e => setS(prev => ({ ...prev, width: +e.target.value || 1200 }))} placeholder="Width (px)" />
                <TextInputBI value={s.height} onChange={e => setS(prev => ({ ...prev, height: +e.target.value || 800 }))} placeholder="Height (px)" />
              </div>
            )}

            <FieldLabelBI>Theme</FieldLabelBI>
            <div style={{ display:'flex', flexWrap:'wrap', gap:6 }}>
              <PillBI active={s.theme==='dark'}    onClick={() => setS(prev=>({...prev,theme:'dark'}))}>Dark navy</PillBI>
              <PillBI active={s.theme==='light'}   onClick={() => setS(prev=>({...prev,theme:'light'}))}>Light beige</PillBI>
              <PillBI active={s.theme==='duotone'} onClick={() => setS(prev=>({...prev,theme:'duotone'}))}>Duotone</PillBI>
            </div>
          </SectionBI>

          {/* STEP 03 — Look */}
          <SectionBI stepNum="03 · Look & feel" title="What should it look like?" subtitle="Default is the full WiseFunnel brand. Upload a reference to match its palette/composition instead.">
            <div style={{ display:'grid', gridTemplateColumns:'1fr 1fr', gap:10 }}>
              <LookCardBI active={s.look==='brand'} onClick={() => setS(prev=>({...prev,look:'brand'}))}
                title="Use WiseFunnel branding" desc="Orange #F97316, navy #1A2B3B, paper #FCFAF2." />
              <LookCardBI active={s.look==='reference'} onClick={() => setS(prev=>({...prev,look:'reference'}))}
                title="Upload reference image" desc="Match palette, mood, composition." />
            </div>
            {s.look === 'reference' && (
              <div onClick={() => refFileRef.current?.click()}
                onDragOver={e => { e.preventDefault(); }}
                onDrop={e => { e.preventDefault(); onLoadRefImage(e.dataTransfer.files[0]); }}
                style={{ border:'1.5px dashed #E5E7EB', borderRadius:12, padding:16, textAlign:'center', background:'#fff', color:'#6B7280', fontSize:12.5, cursor:'pointer', marginTop:10 }}>
                <div style={{ color:'#1A2B3B', fontSize: 13, fontWeight: 700, marginBottom: 3 }}>Drop a JPG, PNG or WebP</div>
                <input ref={refFileRef} type="file" accept="image/*" style={{ display:'none' }} onChange={e => onLoadRefImage(e.target.files[0])} />
                {s.refImage && <div style={{ marginTop:8 }}><img src={s.refImage} style={{ maxHeight:120, borderRadius:8, border:'1px solid #E5E7EB' }} /></div>}
              </div>
            )}
          </SectionBI>

          {/* STEP 04 — Layout & screenshots */}
          <SectionBI stepNum="04 · Layout & screenshots" title="Pick a template, fill the slots." subtitle="Each template pins elements to fixed positions on the canvas. Per slot, pick a WiseFunnel screen or drop your own image — Gemini will compose them in those exact positions.">
            <FieldLabelBI>Template</FieldLabelBI>
            <div style={{ display:'grid', gridTemplateColumns:'repeat(auto-fill, minmax(180px, 1fr))', gap: 10 }}>
              {TEMPLATES_BI.map(t => (
                <TemplateCardBI key={t.id} active={s.template === t.id} template={t}
                  onClick={() => setS(prev => ({ ...prev, template: t.id, slotFills: {} }))} />
              ))}
            </div>

            {(() => {
              const tpl = TEMPLATES_BI.find(t => t.id === s.template) || TEMPLATES_BI[0];
              if (!tpl.slots.length) return (
                <div style={{ marginTop: 14, padding:'12px 14px', background:'#F3F4F6', border:'1px solid #E5E7EB', borderRadius:10, fontSize:12.5, color:'#374151' }}>
                  Freeform — no fixed positions. Use the Elements step to compose the image.
                </div>
              );
              return (
                <div style={{ marginTop: 18 }}>
                  <FieldLabelBI>Fill the slots</FieldLabelBI>
                  <div style={{ display:'flex', flexDirection:'column', gap:14 }}>
                    {tpl.slots.map(slot => (
                      <SlotFillerBI key={slot.id} slot={slot} fill={(s.slotFills||{})[slot.id]}
                        onChange={(fill) => setS(prev => ({ ...prev, slotFills: { ...(prev.slotFills||{}), [slot.id]: fill }}))}
                        onClear={() => setS(prev => {
                          const next = { ...(prev.slotFills||{}) };
                          delete next[slot.id];
                          return { ...prev, slotFills: next };
                        })}
                      />
                    ))}
                  </div>
                </div>
              );
            })()}
          </SectionBI>

          {/* STEP 05 — Elements */}
          <SectionBI stepNum="05 · Elements" title="What goes in the image?" subtitle="Toggle on each element. Opening a toggle reveals its specifics.">
            <ToggleRowBI title="WiseFunnel logo" desc='Orange flag mark + "wisefunnel" wordmark' on={s.toggles.logo} onToggle={() => setToggle('logo')} />

            <ToggleRowBI title="Blog title text" desc="Bake the headline into the image" on={s.toggles.title} onToggle={() => setToggle('title')} />
            {s.toggles.title && (
              <SubOptsBI>
                <FieldLabelBI>Title source</FieldLabelBI>
                <div style={{ display:'flex', gap:6 }}>
                  <PillBI active={s.titleSrc==='auto'}   onClick={() => setS(prev=>({...prev,titleSrc:'auto'}))}>Use post title</PillBI>
                  <PillBI active={s.titleSrc==='custom'} onClick={() => setS(prev=>({...prev,titleSrc:'custom'}))}>Custom headline</PillBI>
                </div>
                {s.titleSrc === 'custom' && <TextInputBI value={s.titleText} onChange={e=>setS(prev=>({...prev,titleText:e.target.value}))} placeholder="Custom headline" />}
              </SubOptsBI>
            )}

            <ToggleRowBI title="App screenshots" desc="Drop product screenshots in framed cards" on={s.toggles.screens} onToggle={() => setToggle('screens')} />
            {s.toggles.screens && (
              <SubOptsBI>
                <FieldLabelBI>Pick screens</FieldLabelBI>
                <div style={{ display:'flex', flexWrap:'wrap', gap:6 }}>
                  {SCREENS_BI.map(scr => (
                    <PillBI key={scr.name} active={s.selectedScreens.includes(scr.name)} onClick={() => toggleInArr('selectedScreens', scr.name)}>{scr.name}</PillBI>
                  ))}
                </div>
              </SubOptsBI>
            )}

            <ToggleRowBI title="Real photos of professionals" desc="Niche-specific portraits, not emojis" on={s.toggles.people} onToggle={() => setToggle('people')} />
            {s.toggles.people && (
              <SubOptsBI>
                <FieldLabelBI>Niches</FieldLabelBI>
                <div style={{ display:'flex', flexWrap:'wrap', gap:6 }}>
                  {NICHES_BI.map(n => (
                    <PillBI key={n.id} active={s.selectedNiches.includes(n.id)} onClick={() => toggleInArr('selectedNiches', n.id)}>
                      {n.name} <span style={{ opacity: 0.55 }}>· {n.sub}</span>
                    </PillBI>
                  ))}
                </div>
                <FieldLabelBI>Portrait treatment</FieldLabelBI>
                <div style={{ display:'flex', gap:6 }}>
                  <PillBI active={s.portrait==='full'} onClick={() => setS(p=>({...p,portrait:'full'}))}>Full color</PillBI>
                  <PillBI active={s.portrait==='navy'} onClick={() => setS(p=>({...p,portrait:'navy'}))}>Navy duotone</PillBI>
                  <PillBI active={s.portrait==='ring'} onClick={() => setS(p=>({...p,portrait:'ring'}))}>Orange ring</PillBI>
                </div>
              </SubOptsBI>
            )}

            <ToggleRowBI title="Numbers & KPIs" desc="CPL changes, conversion lifts, dollar figures" on={s.toggles.numbers} onToggle={() => setToggle('numbers')} />
            {s.toggles.numbers && (
              <SubOptsBI>
                <FieldLabelBI>Numbers</FieldLabelBI>
                <TextInputBI multiline rows={2} value={s.numbersText} onChange={e=>setS(p=>({...p,numbersText:e.target.value}))}
                  placeholder="e.g. CPL $47 → $19, conversion 8.1% → 32.4%, −59% lift" />
                <FieldLabelBI>Highlight style</FieldLabelBI>
                <div style={{ display:'flex', flexWrap:'wrap', gap:6 }}>
                  <PillBI active={s.numStyle==='hero'}         onClick={()=>setS(p=>({...p,numStyle:'hero'}))}>Hero number</PillBI>
                  <PillBI active={s.numStyle==='before-after'} onClick={()=>setS(p=>({...p,numStyle:'before-after'}))}>Before → After</PillBI>
                  <PillBI active={s.numStyle==='grid'}         onClick={()=>setS(p=>({...p,numStyle:'grid'}))}>Stat grid</PillBI>
                  <PillBI active={s.numStyle==='ribbon'}       onClick={()=>setS(p=>({...p,numStyle:'ribbon'}))}>Result ribbon</PillBI>
                </div>
              </SubOptsBI>
            )}

            <ToggleRowBI title="Agency & funnel callout" desc="Top-right tag with agency name, location, date range" on={s.toggles.agency} onToggle={() => setToggle('agency')} />
            {s.toggles.agency && (
              <SubOptsBI>
                <div style={{ display:'grid', gap:8 }}>
                  <TextInputBI value={s.agencyName} onChange={e=>setS(p=>({...p,agencyName:e.target.value}))} placeholder="Agency name" />
                  <TextInputBI value={s.agencyLoc}  onChange={e=>setS(p=>({...p,agencyLoc:e.target.value}))}  placeholder="Location" />
                  <TextInputBI value={s.funnelName} onChange={e=>setS(p=>({...p,funnelName:e.target.value}))} placeholder="Funnel name" />
                  <TextInputBI value={s.dateRange}  onChange={e=>setS(p=>({...p,dateRange:e.target.value}))}  placeholder="Date range" />
                </div>
              </SubOptsBI>
            )}

            <ToggleRowBI title="Ad channel breakdown" desc="Per-channel CPL strip" on={s.toggles.channels} onToggle={() => setToggle('channels')} />
            {s.toggles.channels && (
              <SubOptsBI>
                <div style={{ display:'grid', gridTemplateColumns:'1fr 1fr', gap:8 }}>
                  <TextInputBI value={s.channels.google}   onChange={e=>setS(p=>({...p,channels:{...p.channels,google:e.target.value}}))}   placeholder="Google: $22" />
                  <TextInputBI value={s.channels.meta}     onChange={e=>setS(p=>({...p,channels:{...p.channels,meta:e.target.value}}))}     placeholder="Meta: $17" />
                  <TextInputBI value={s.channels.linkedin} onChange={e=>setS(p=>({...p,channels:{...p.channels,linkedin:e.target.value}}))} placeholder="LinkedIn: $31" />
                  <TextInputBI value={s.channels.tiktok}   onChange={e=>setS(p=>({...p,channels:{...p.channels,tiktok:e.target.value}}))}   placeholder="TikTok: $12" />
                </div>
              </SubOptsBI>
            )}
          </SectionBI>

          {/* STEP 06 — 3D Icons (library picker) */}
          <SectionBI stepNum="06 · 3D Icons" title="Pick 3D icons to include." subtitle="120 icons from the 3dicons.co library by realvjy. Search, pick a style, and check the ones you want — each selected icon gets named in the prompt with its CDN reference URL.">
            <ToggleRowBI title="Include 3D icons" desc={`${s.selectedIcons.length} selected · ${ICONS_BI.length} available`} on={s.toggles.threed} onToggle={() => setToggle('threed')} />
            {s.toggles.threed && (
              <SubOptsBI>
                <FieldLabelBI>Style</FieldLabelBI>
                <div style={{ display:'flex', flexWrap:'wrap', gap:6 }}>
                  {ICON_STYLES_BI.map(opt => (
                    <PillBI key={opt.id} active={s.iconStyle === opt.id} onClick={() => setS(p => ({ ...p, iconStyle: opt.id }))}>
                      {opt.label} <span style={{ opacity: 0.55 }}>· {opt.desc}</span>
                    </PillBI>
                  ))}
                </div>

                <FieldLabelBI hint={`${s.selectedIcons.length} selected`}>Search icons</FieldLabelBI>
                <TextInputBI value={s.iconQuery} onChange={e => setS(p => ({ ...p, iconQuery: e.target.value }))} placeholder="search by name or category (coin, chart, money…)" />

                <div style={{ marginTop: 12 }}>
                  <IconPickerBI s={s} setS={setS} />
                </div>

                {s.selectedIcons.length > 0 && (
                  <div style={{ marginTop: 12, display:'flex', alignItems:'center', justifyContent:'space-between', flexWrap:'wrap', gap:8 }}>
                    <div style={{ fontSize:12, color:'#6B7280' }}>{s.selectedIcons.length} icon{s.selectedIcons.length>1?'s':''} chosen</div>
                    <PillBI onClick={() => setS(p => ({ ...p, selectedIcons: [] }))}>Clear all</PillBI>
                  </div>
                )}
              </SubOptsBI>
            )}
          </SectionBI>

          {/* STEP 07 — Logos & brands */}
          <SectionBI stepNum="07 · Logos & brands" title="Which logos appear in the image?" subtitle="Pick from the presets, type custom brand names, or upload actual logo files. Uploaded logos are passed inline to Gemini so it can match them visually.">
            <ToggleRowBI title="Include third-party logos" desc={`${s.selectedLogos.length} preset · ${(s.customLogos||'').split(',').map(x=>x.trim()).filter(Boolean).length} custom · ${(s.uploadedLogos||[]).length} uploaded`} on={s.toggles.logos} onToggle={() => setToggle('logos')} />
            {s.toggles.logos && (
              <SubOptsBI>
                {['Competitor','Ad platform','Integration','AI'].map(group => (
                  <div key={group} style={{ marginBottom: 10 }}>
                    <FieldLabelBI>{group}</FieldLabelBI>
                    <div style={{ display:'flex', flexWrap:'wrap', gap:6 }}>
                      {LOGOS_BI.filter(l => l.group === group).map(l => (
                        <PillBI key={l.id} active={s.selectedLogos.includes(l.id)} onClick={() => toggleInArr('selectedLogos', l.id)}>{l.name}</PillBI>
                      ))}
                    </div>
                  </div>
                ))}
                <FieldLabelBI hint="comma-separated — e.g. Notion, Figma, Shopify">Custom logos (text only)</FieldLabelBI>
                <TextInputBI value={s.customLogos} onChange={e => setS(p => ({ ...p, customLogos: e.target.value }))} placeholder="Notion, Figma, Shopify" />

                <FieldLabelBI hint="passed inline to Gemini so it can match the actual mark">Upload logo files</FieldLabelBI>
                <LogoUploaderBI s={s} setS={setS} showToast={showToast} />
              </SubOptsBI>
            )}
          </SectionBI>

          {/* STEP 08 — Engine */}
          <SectionBI stepNum="08 · Engine" title="Which model should render it?" subtitle="Uses the server-side Gemini key via the get-gemini-api-key edge function — no key entry needed.">
            <div style={{ display:'grid', gridTemplateColumns:'1fr 1fr', gap:10 }}>
              <LookCardBI active={s.engine==='prompt-only'} onClick={()=>setS(p=>({...p,engine:'prompt-only'}))}
                title="Prompt only · copy" desc="Skip the API call, copy the brief to any model." />
              <LookCardBI active={s.engine==='gemini'} onClick={()=>setS(p=>({...p,engine:'gemini'}))}
                title="Gemini · Nano Banana" desc="gemini-2.5-flash-image, end-to-end image render." />
            </div>
            <div style={{ marginTop: 14, padding: '12px 14px', background:'#F3F4F6', border:'1px solid #E5E7EB', borderRadius: 10, fontSize: 12, color:'#374151', lineHeight: 1.55 }}>
              <div style={{ fontWeight: 700, marginBottom: 2, color:'#1A2B3B' }}>Server-side key in use</div>
              The Gemini API key lives in Supabase as <code style={{ background:'#fff', padding:'1px 5px', borderRadius:4 }}>GEMINI_API_KEY</code>. Every Generate hits the <code style={{ background:'#fff', padding:'1px 5px', borderRadius:4 }}>get-gemini-api-key</code> edge-function proxy — the key never reaches the browser.
            </div>
          </SectionBI>

          {/* STEP 09 — Output */}
          <SectionBI stepNum="09 · Output" title="Where does it land?" subtitle={`Uploads to Supabase storage bucket "${SB_BUCKET}/${SB_PREFIX}/" and patches the matching blog_posts row.`}>
            <FieldLabelBI>Destination</FieldLabelBI>
            <div style={{ background:'#fff', border:'1.5px solid #E5E7EB', borderRadius:10, padding:'12px 14px', fontFamily:'JetBrains Mono, monospace' }}>
              <div style={{ fontSize:11, color:'#9CA3AF', wordBreak:'break-all' }}>{SB_PUBLIC_BASE}</div>
              <div style={{ fontSize:13, color:'#F97316', fontWeight:700 }}>{fileName}</div>
            </div>

            <ToggleRowBI title="Patch blog_posts row" desc="Update the matching slug with this image URL" on={s.toggles.supabase} onToggle={() => setToggle('supabase')} />
            {s.toggles.supabase && (
              <SubOptsBI>
                <FieldLabelBI>Column to update</FieldLabelBI>
                <div style={{ display:'flex', flexWrap:'wrap', gap:6 }}>
                  <PillBI active={s.sbColumn==='cover_image_url'} onClick={()=>setS(p=>({...p,sbColumn:'cover_image_url'}))}>cover_image_url</PillBI>
                  <PillBI active={s.sbColumn==='og_image_url'}    onClick={()=>setS(p=>({...p,sbColumn:'og_image_url'}))}>og_image_url</PillBI>
                  <PillBI active={s.sbColumn==='none'}            onClick={()=>setS(p=>({...p,sbColumn:'none'}))}>none (storage only)</PillBI>
                </div>
                <pre style={{ background:'#0F1A24', color:'#FFE4C2', padding:'12px 14px', borderRadius:10, fontSize:11.5, lineHeight:1.55, marginTop:12, overflow:'auto' }}>
{s.sbColumn === 'none'
  ? '-- No row update (uploads file only)'
  : `UPDATE blog_posts\nSET ${s.sbColumn} = '${publicUrl}',\n    updated_at = now()\nWHERE slug = '${s.slug || '[slug]'}';`}
                </pre>
              </SubOptsBI>
            )}
          </SectionBI>

          {/* CTA */}
          <div style={{ marginTop: 32, padding: '22px 26px', background:'#1A2B3B', color:'#fff', borderRadius: 16, display:'flex', alignItems:'center', justifyContent:'space-between', flexWrap:'wrap', gap:16 }}>
            <div>
              <div style={{ fontWeight: 800, fontSize: 15 }}>Generate image</div>
              <div style={{ fontSize:12, color:'rgba(255,255,255,0.6)' }}>Copy prompt to skip the API call · Import to upload one made elsewhere.</div>
            </div>
            <div style={{ display:'flex', gap:10, flexWrap:'wrap' }}>
              <button type="button" onClick={copyPrompt} style={{
                background:'rgba(255,255,255,0.05)', border:'1px solid rgba(255,255,255,0.18)',
                color:'#fff', borderRadius: 10, padding:'12px 18px', fontSize:13, fontWeight:700, cursor:'pointer', fontFamily:'inherit',
              }}>Copy prompt</button>
              <button type="button" onClick={() => importInputRef.current?.click()} style={{
                background:'rgba(255,255,255,0.05)', border:'1px solid rgba(255,255,255,0.18)',
                color:'#fff', borderRadius: 10, padding:'12px 18px', fontSize:13, fontWeight:700, cursor:'pointer', fontFamily:'inherit',
              }}>Import image…</button>
              <input ref={importInputRef} type="file" accept="image/*" style={{ display:'none' }}
                onChange={e => { importExternalImage(e.target.files[0]); e.target.value = ''; }} />
              <button type="button" onClick={generate} disabled={genBusy} style={{
                background: genBusy ? '#9CA3AF' : '#F97316', border:`1px solid ${genBusy?'#9CA3AF':'#F97316'}`,
                color:'#fff', borderRadius:10, padding:'12px 22px', fontSize:14, fontWeight:700, cursor: genBusy?'not-allowed':'pointer',
                display:'inline-flex', alignItems:'center', gap:8, fontFamily:'inherit',
              }}>
                {genBusy ? 'Rendering…' : 'Generate'}
              </button>
            </div>
          </div>
        </div>

        {/* ===== RIGHT (live brief preview) ===== */}
        <aside style={{ position:'sticky', top: 12, alignSelf:'start', background:'#1A2B3B', color:'#fff', borderRadius: 14, padding: 24, maxHeight: 'calc(100vh - 40px)', overflowY:'auto' }}>
          <h3 style={{ fontFamily:'Georgia, serif', fontWeight: 500, fontSize: 20, margin: '0 0 2px' }}>Live brief</h3>
          <p style={{ fontSize: 11, color:'rgba(255,255,255,0.5)', margin:'0 0 18px' }}>Rebuilds as you toggle.</p>

          <RailSection label="Output">
            <div style={{ fontSize: 12 }}>{s.width} × {s.height} px · {s.placement} · {s.theme}</div>
            {lastImage && lastImage.newBytes != null && (
              <div style={{ marginTop: 6, fontFamily:'JetBrains Mono, monospace', fontSize: 11, color:'rgba(255,255,255,0.7)', lineHeight: 1.6 }}>
                {lastImage.width}×{lastImage.height} · {fmtBytes(lastImage.newBytes)} WebP
                {lastImage.originalBytes > lastImage.newBytes && (
                  <span style={{ color: '#5FC18B', marginLeft: 6 }}>
                    (↓ from {fmtBytes(lastImage.originalBytes)} · −{Math.max(0, Math.round((1 - lastImage.newBytes / lastImage.originalBytes) * 100))}%)
                  </span>
                )}
              </div>
            )}
            <div
              onClick={() => { if (!lastImage) importInputRef.current?.click(); }}
              onDragOver={e => { e.preventDefault(); e.currentTarget.style.borderColor = '#F97316'; }}
              onDragLeave={e => { e.preventDefault(); e.currentTarget.style.borderColor = 'rgba(255,255,255,0.10)'; }}
              onDrop={e => { e.preventDefault(); e.currentTarget.style.borderColor = 'rgba(255,255,255,0.10)'; importExternalImage(e.dataTransfer.files[0]); }}
              style={{ width:'100%', aspectRatio:`${s.width} / ${s.height}`, marginTop: 8, borderRadius: 8,
                border:'1px solid rgba(255,255,255,0.10)', background: lastImage ? '#000' : themeBgPreview,
                display:'flex', alignItems:'center', justifyContent:'center', overflow:'hidden',
                cursor: lastImage ? 'default' : 'pointer', transition: 'border-color 140ms', textAlign:'center', padding: 8 }}>
              {lastImage
                ? <img src={`data:${lastImage.mime};base64,${lastImage.base64}`} style={{ width:'100%', height:'100%', objectFit:'cover' }} />
                : (
                  <div style={{ fontFamily:'JetBrains Mono, monospace', fontSize:10.5, color:'rgba(255,255,255,0.55)', lineHeight: 1.55 }}>
                    {s.placement} · {s.width} × {s.height}<br/>
                    <span style={{ color:'rgba(255,255,255,0.35)' }}>drop an image here<br/>or hit Generate / Import</span>
                  </div>
                )}
            </div>
            {lastImage && (
              <div style={{ display:'flex', gap:8, marginTop:10, flexWrap:'wrap' }}>
                <button type="button" onClick={() => importInputRef.current?.click()} title="Replace with another file" style={{ flex:1, background:'rgba(255,255,255,0.05)', border:'1px solid rgba(255,255,255,0.18)', color:'#fff', borderRadius:8, padding:'8px 12px', fontSize:12, fontWeight:700, cursor:'pointer', fontFamily:'inherit' }}>Replace</button>
                <button type="button" onClick={downloadImage} style={{ flex:1, background:'rgba(255,255,255,0.05)', border:'1px solid rgba(255,255,255,0.18)', color:'#fff', borderRadius:8, padding:'8px 12px', fontSize:12, fontWeight:700, cursor:'pointer', fontFamily:'inherit' }}>Download</button>
                <button type="button" onClick={saveImage} disabled={saveBusy} style={{ flex:1, background: saveBusy?'#6B7280':'#F97316', border:'1px solid #F97316', color:'#fff', borderRadius:8, padding:'8px 12px', fontSize:12, fontWeight:700, cursor: saveBusy?'not-allowed':'pointer', fontFamily:'inherit' }}>
                  {saveBusy ? 'Saving…' : 'Save'}
                </button>
              </div>
            )}
            {saveResult && (
              <div style={{ marginTop:10, display:'flex', flexDirection:'column', gap:6 }}>
                {saveResult.storagePath && <StatusBI kind="ok">Storage: {saveResult.storagePath}</StatusBI>}
                {saveResult.dbUpdated && <StatusBI kind="ok">Patched {saveResult.dbUpdated}</StatusBI>}
                {saveResult.uploadedUrl && (
                  <div style={{ fontSize: 10.5, color:'rgba(255,255,255,0.65)', fontFamily:'JetBrains Mono, monospace', wordBreak:'break-all', padding:'4px 0' }}>
                    {saveResult.uploadedUrl}
                  </div>
                )}
                {(saveResult.warnings || []).map((w,i) => <StatusBI key={i} kind="warn">{w}</StatusBI>)}
              </div>
            )}
          </RailSection>

          <RailSection label="Source post">
            {s.postTitle ? (
              <>
                <div style={{ fontWeight: 700 }}>{s.postTitle}</div>
                {s.slug && <div style={{ fontFamily:'JetBrains Mono, monospace', fontSize: 11, color:'rgba(255,255,255,0.5)', marginTop: 4 }}>{s.slug}</div>}
              </>
            ) : (
              <span style={{ color:'rgba(255,255,255,0.4)', fontStyle:'italic', fontSize: 12 }}>No markdown loaded yet</span>
            )}
          </RailSection>

          <RailSection label="Elements in prompt">
            <ActiveElementsBI s={s} />
          </RailSection>

          <RailSection label="Final brief" actionLabel="Copy" onAction={copyPrompt}>
            <pre style={{ background:'rgba(0,0,0,0.25)', border:'1px solid rgba(255,255,255,0.10)', borderRadius:8, padding:'10px 12px', fontFamily:'JetBrains Mono, monospace', fontSize:10.5, lineHeight:1.55, color:'rgba(255,255,255,0.85)', maxHeight:260, overflowY:'auto', whiteSpace:'pre-wrap', margin:0 }}>
              {fullPrompt}
            </pre>
          </RailSection>

          <RailSection label="Engine">
            <div style={{ fontSize: 12 }}>{s.engine === 'gemini' ? 'Gemini 2.5 Flash · Nano Banana' : 'Prompt only · no API call'}</div>
          </RailSection>

          <RailSection label="After save">
            <pre style={{ background:'rgba(0,0,0,0.25)', border:'1px solid rgba(255,255,255,0.10)', borderRadius:8, padding:'10px 12px', fontFamily:'JetBrains Mono, monospace', fontSize:10.5, lineHeight:1.55, color:'#FFE4C2', maxHeight:120, overflowY:'auto', whiteSpace:'pre-wrap', margin:0 }}>
{s.toggles.supabase && s.sbColumn !== 'none'
  ? `UPDATE blog_posts\nSET ${s.sbColumn} = '${publicUrl}'\nWHERE slug = '${s.slug || '[slug]'}';`
  : 'Storage upload only — no row patch.'}
            </pre>
          </RailSection>
        </aside>
      </div>

      {/* Toast */}
      {toast && (
        <div style={{
          position:'fixed', right:24, bottom:24, padding:'12px 16px',
          background: toast.kind === 'err' ? '#7F1D1D' : toast.kind === 'warn' ? '#92400E' : '#065F46',
          color:'#fff', borderRadius: 10, fontSize: 13, fontWeight: 600,
          boxShadow:'0 8px 24px rgba(0,0,0,0.2)', zIndex: 9999,
        }}>{toast.msg}</div>
      )}
    </div>
  );
}

function TemplateCardBI({ active, template, onClick }) {
  return (
    <button type="button" onClick={onClick} style={{
      border: `1.5px solid ${active ? '#F97316' : '#E5E7EB'}`,
      background: active ? '#FFF8EE' : '#fff',
      borderRadius: 12, padding: 12, cursor: 'pointer', textAlign: 'left',
      boxShadow: active ? '0 0 0 3px rgba(249,115,22,0.10)' : 'none',
      fontFamily: 'inherit', color: '#1A2B3B', display:'flex', flexDirection:'column', gap:6,
    }}>
      <TemplateSchematicBI id={template.id} />
      <div style={{ fontWeight: 800, fontSize: 13 }}>{template.title}</div>
      <div style={{ fontSize: 11, color: '#6B7280', lineHeight: 1.4 }}>{template.desc}</div>
    </button>
  );
}

// A small CSS schematic preview per template, so the user sees where slots land.
function TemplateSchematicBI({ id }) {
  const frame = { width:'100%', aspectRatio:'1200 / 630', borderRadius:8, background:'#F9FAFB', border:'1px solid #E5E7EB', position:'relative', overflow:'hidden' };
  const tile = (style) => ({ position:'absolute', background:'#fff', border:'1px solid #D6CFB8', borderRadius:4, boxShadow:'0 2px 6px -2px rgba(15,23,42,0.08)', ...style });
  const arrow = (style) => ({ position:'absolute', background:'#F97316', height:2, ...style });
  if (id === 'integration') return (
    <div style={frame}>
      <div style={tile({ left:'8%',  top:'25%', width:'24%', height:'50%', transform:'rotate(-2deg)' })} />
      <div style={arrow({ left:'34%', top:'50%', width:'30%' })} />
      <div style={tile({ right:'8%', top:'18%', width:'32%', height:'64%', transform:'rotate(1deg)' })} />
    </div>
  );
  if (id === 'workflow') return (
    <div style={frame}>
      {[0,1,2,3].map(i => (
        <div key={i} style={tile({ left:`${5 + i*23.5}%`, top:'28%', width:'18%', height:'44%' })} />
      ))}
      {[0,1,2].map(i => (
        <div key={i} style={arrow({ left:`${23.5 + i*23.5}%`, top:'50%', width:'5%' })} />
      ))}
    </div>
  );
  if (id === 'funnel-builder') return (
    <div style={frame}>
      <div style={tile({ left:'20%', top:'18%', width:'60%', height:'64%' })} />
      <div style={tile({ left:'6%',  top:'14%', width:'14%', height:'18%' })} />
      <div style={tile({ right:'6%', bottom:'14%', width:'16%', height:'20%' })} />
    </div>
  );
  if (id === 'comparison') return (
    <div style={frame}>
      <div style={tile({ left:'5%',  top:'18%', width:'40%', height:'64%', transform:'rotate(-1deg)', background:'#F3F4F6' })} />
      <div style={{ position:'absolute', left:'48%', top:'42%', width:'4%', height:'16%', background:'#F97316', borderRadius:4 }} />
      <div style={tile({ right:'5%', top:'18%', width:'40%', height:'64%', transform:'rotate(1deg)' })} />
    </div>
  );
  if (id === 'results') return (
    <div style={frame}>
      <div style={{ position:'absolute', left:'8%', top:'30%', width:'30%', height:'40%', background:'#F97316', borderRadius:6, opacity:0.85 }} />
      <div style={tile({ right:'6%', top:'18%', width:'46%', height:'64%' })} />
    </div>
  );
  // freeform
  return (
    <div style={frame}>
      <div style={{ position:'absolute', inset:'30%', border:'1.5px dashed #D6CFB8', borderRadius:6, display:'flex', alignItems:'center', justifyContent:'center', fontSize:9, color:'#9CA3AF' }}>no fixed slots</div>
    </div>
  );
}

function SlotFillerBI({ slot, fill, onChange, onClear }) {
  const fileRef = useRefBI(null);
  const isFixed = slot.kinds.includes('arrow-fixed') || slot.kinds.includes('vs-fixed');
  const kind = fill?.kind || (isFixed ? 'fixed' : 'screen');

  const setKind = (k) => onChange({ kind: k, value: null, label: '' });

  const onPickScreen = (name) => onChange({ kind:'screen', value: name });
  const onUpload = (file) => {
    if (!file) return;
    const reader = new FileReader();
    reader.onload = e => onChange({ kind:'upload', value: e.target.result, label: file.name.replace(/\.[^.]+$/, '') });
    reader.readAsDataURL(file);
  };

  return (
    <div style={{ background:'#fff', border:'1.5px solid #E5E7EB', borderRadius:12, padding:'12px 14px' }}>
      <div style={{ display:'flex', justifyContent:'space-between', alignItems:'flex-start', gap:10, marginBottom:8 }}>
        <div>
          <div style={{ fontWeight: 700, fontSize: 13, color:'#1A2B3B' }}>{slot.label}</div>
          <div style={{ fontSize: 11.5, color:'#6B7280', lineHeight: 1.4, marginTop: 2 }}>{slot.position}</div>
        </div>
        {fill && !isFixed && (
          <button type="button" onClick={onClear} style={{ background:'transparent', border:'none', color:'#9CA3AF', fontSize:11, fontWeight:700, cursor:'pointer', padding:'2px 6px' }}>Clear</button>
        )}
      </div>

      {isFixed ? (
        <div style={{ fontSize:11.5, color:'#6B7280', padding:'8px 10px', background:'#F9FAFB', borderRadius:8 }}>
          Auto-rendered: {slot.kinds.includes('arrow-fixed') ? 'orange dashed arrow + small packet card' : 'vertical orange VS badge'} — no input needed.
        </div>
      ) : (
        <>
          <div style={{ display:'flex', flexWrap:'wrap', gap:6, marginBottom: 10 }}>
            {slot.kinds.filter(k => ['screen','upload','text','number','callout','logo','icon'].includes(k)).map(k => (
              <PillBI key={k} active={kind === k} onClick={() => setKind(k)}>{SLOT_KIND_LABELS[k] || k}</PillBI>
            ))}
          </div>

          {kind === 'screen' && (
            <div style={{
              display:'grid', gridTemplateColumns:'repeat(auto-fill, minmax(110px, 1fr))', gap:8,
              maxHeight: 280, overflowY:'auto',
            }}>
              {SCREENS_BI.map(scr => {
                const active = fill?.kind === 'screen' && fill?.value === scr.name;
                return (
                  <button key={scr.name} type="button" onClick={() => onPickScreen(scr.name)} title={scr.surface} style={{
                    border:`1.5px solid ${active ? '#F97316' : '#E5E7EB'}`,
                    background: active ? '#FFF8EE' : '#fff',
                    boxShadow: active ? '0 0 0 3px rgba(249,115,22,0.18)' : 'none',
                    borderRadius:10, padding:6, cursor:'pointer', fontFamily:'inherit', color:'#1A2B3B',
                    display:'flex', flexDirection:'column', alignItems:'center', gap:4,
                  }}>
                    <div style={{ width:'100%', aspectRatio:'16 / 10', background:'#F9FAFB', borderRadius:6, overflow:'hidden' }}>
                      <img src={scr.url} alt={scr.name} loading="lazy" style={{ width:'100%', height:'100%', objectFit:'cover' }} />
                    </div>
                    <div style={{ fontSize: 10.5, fontWeight: 700, lineHeight: 1.2, textAlign:'center', color: active ? '#C7501A' : '#1A2B3B' }}>{scr.name}</div>
                  </button>
                );
              })}
            </div>
          )}

          {kind === 'upload' && (
            <div>
              <div
                onClick={() => fileRef.current?.click()}
                onDragOver={e => { e.preventDefault(); e.currentTarget.style.background='#FFF8EE'; e.currentTarget.style.borderColor='#F97316'; }}
                onDragLeave={e => { e.currentTarget.style.background='#fff'; e.currentTarget.style.borderColor='#E5E7EB'; }}
                onDrop={e => { e.preventDefault(); e.currentTarget.style.background='#fff'; e.currentTarget.style.borderColor='#E5E7EB'; onUpload(e.dataTransfer.files[0]); }}
                style={{ border:'1.5px dashed #E5E7EB', borderRadius:10, padding:16, textAlign:'center', background:'#fff', color:'#6B7280', fontSize:12.5, cursor:'pointer' }}>
                <div style={{ color:'#1A2B3B', fontSize:13, fontWeight:700, marginBottom:3 }}>Drop a screenshot here</div>
                Or click to browse. PNG / JPG / WebP.
                <input ref={fileRef} type="file" accept="image/*" style={{ display:'none' }} onChange={e => onUpload(e.target.files[0])} />
              </div>
              {fill?.kind === 'upload' && fill?.value && (
                <div style={{ marginTop: 10, display:'flex', alignItems:'center', gap:10 }}>
                  <img src={fill.value} alt="" style={{ width:80, height:60, objectFit:'cover', borderRadius:8, border:'1px solid #E5E7EB' }} />
                  <div style={{ fontSize:12, color:'#374151', fontWeight:700 }}>{fill.label || 'upload'}</div>
                </div>
              )}
            </div>
          )}

          {kind === 'text' && (
            <TextInputBI value={fill?.value || ''} onChange={e => onChange({ kind:'text', value: e.target.value })} placeholder="Caption / label text" />
          )}

          {kind === 'number' && (
            <TextInputBI value={fill?.value || ''} onChange={e => onChange({ kind:'text', value: e.target.value })} placeholder='e.g. "−59%" or "$18k"' />
          )}

          {kind === 'callout' && (
            <TextInputBI value={fill?.value || ''} onChange={e => onChange({ kind:'text', value: e.target.value })} placeholder='Callout text — short phrase with arrow into the screenshot' />
          )}

          {kind === 'logo' && (
            <TextInputBI value={fill?.value || ''} onChange={e => onChange({ kind:'text', value: e.target.value })} placeholder="Brand name (e.g. GoHighLevel, Stripe)" />
          )}

          {kind === 'icon' && (
            <TextInputBI value={fill?.value || ''} onChange={e => onChange({ kind:'text', value: e.target.value })} placeholder="3D icon name (e.g. 3d Coin, Funnel)" />
          )}
        </>
      )}
    </div>
  );
}

function LookCardBI({ active, onClick, title, desc }) {
  return (
    <button type="button" onClick={onClick} style={{
      border: `1.5px solid ${active ? '#F97316' : '#E5E7EB'}`,
      background: active ? '#FFF8EE' : '#fff',
      borderRadius: 12, padding: 14, cursor: 'pointer', textAlign: 'left',
      boxShadow: active ? '0 0 0 3px rgba(249,115,22,0.10)' : 'none',
      fontFamily: 'inherit', color: '#1A2B3B', display:'flex', flexDirection:'column', gap:4,
    }}>
      <div style={{ fontWeight: 800, fontSize: 13 }}>{title}</div>
      <div style={{ fontSize: 11.5, color: '#6B7280', lineHeight: 1.45 }}>{desc}</div>
    </button>
  );
}

function SubOptsBI({ children }) {
  return (
    <div style={{
      padding:'14px 16px', borderRadius: 10,
      background: '#FFFBEB', border:'1px dashed #E5E7EB', marginTop:-6, marginBottom:10,
    }}>
      {children}
    </div>
  );
}

// Dropdown of every image entry parsed from the post's Image Guide section.
// Selecting an entry pins the placement + dimensions and lets the prompt
// assembler pull THAT entry's specific AI Prompt — so each image (cover /
// inline-1 / inline-2) gets its own brief instead of all sharing one.
function ImageGuidePickerBI({ s, setS }) {
  const guide = useMemoBI(() => parseImageGuideBI(s.md), [s.md]);
  const options = useMemoBI(() => imageGuideOptionsBI(guide), [guide]);
  if (!options.length) return null;

  const currentValue =
    s.placement === 'cover'    ? 'cover' :
    s.placement === 'inline-1' ? 'inline-1' :
    s.placement === 'inline-2' ? 'inline-2' :
    '';
  const current = options.find(o => o.id === currentValue);

  const pick = (id) => {
    const opt = options.find(o => o.id === id);
    if (!opt) return;
    setS(prev => {
      const next = { ...prev };
      // Pin placement
      if (id === 'cover')         next.placement = 'cover';
      else if (id === 'inline-1') next.placement = 'inline-1';
      else if (id === 'inline-2') next.placement = 'inline-2';
      // Pin dims from the guide (falls back to the placement default)
      if (opt.width)  next.width  = opt.width;
      if (opt.height) next.height = opt.height;
      // Reset sb column dirty flag so the column-pill defaults re-apply
      next._sbColumnDirty = false;
      // Default column per placement
      const placementDef = PLACEMENTS_BI.find(p => p.id === next.placement);
      if (placementDef) next.sbColumn = placementDef.col;
      return next;
    });
  };

  return (
    <div style={{ marginTop: 18 }}>
      <FieldLabelBI hint={`${options.length} image${options.length>1?'s':''} found in this post's Image Guide`}>
        Image from this post
      </FieldLabelBI>
      <select
        value={currentValue}
        onChange={e => pick(e.target.value)}
        style={{
          width: '100%', border: '1.5px solid #E5E7EB', borderRadius: 10,
          padding: '11px 14px', fontFamily: 'inherit', fontSize: 13,
          background: '#fff', color: '#1A2B3B', outline: 'none', cursor: 'pointer',
          appearance: 'none', paddingRight: 36,
          backgroundImage: 'url("data:image/svg+xml;utf8,<svg xmlns=\'http://www.w3.org/2000/svg\' width=\'12\' height=\'8\' viewBox=\'0 0 12 8\'><path d=\'M1 1l5 5 5-5\' stroke=\'%231A2B3B\' stroke-width=\'2\' fill=\'none\' stroke-linecap=\'round\'/></svg>")',
          backgroundRepeat: 'no-repeat',
          backgroundPosition: 'right 14px center',
        }}>
        <option value="">— Pick which image to generate —</option>
        {options.map(o => (
          <option key={o.id} value={o.id}>
            {o.label}{o.width && o.height ? ` · ${o.width}×${o.height}` : ''}
          </option>
        ))}
      </select>
      {current && (
        <div style={{ marginTop: 10, padding: '10px 12px', background: '#FFF8EE', border: '1px dashed #F97316', borderRadius: 10, fontSize: 12, color: '#1A2B3B', lineHeight: 1.55 }}>
          {current.alt && (
            <div style={{ marginBottom: 4 }}>
              <span style={{ fontFamily: 'JetBrains Mono, monospace', fontSize: 10, color: '#C7501A', fontWeight: 700, letterSpacing: '0.1em', textTransform: 'uppercase' }}>ALT</span>
              {' '}{current.alt}
            </div>
          )}
          {current.prompt && (
            <div style={{ marginBottom: current.caption ? 4 : 0 }}>
              <span style={{ fontFamily: 'JetBrains Mono, monospace', fontSize: 10, color: '#C7501A', fontWeight: 700, letterSpacing: '0.1em', textTransform: 'uppercase' }}>Prompt seed</span>
              <span style={{ color: '#374151' }}> {current.prompt.length > 220 ? current.prompt.slice(0, 217) + '…' : current.prompt}</span>
            </div>
          )}
          {current.caption && (
            <div>
              <span style={{ fontFamily: 'JetBrains Mono, monospace', fontSize: 10, color: '#C7501A', fontWeight: 700, letterSpacing: '0.1em', textTransform: 'uppercase' }}>Caption</span>
              {' '}{current.caption}
            </div>
          )}
        </div>
      )}
    </div>
  );
}

function LogoUploaderBI({ s, setS, showToast }) {
  const inputRef = useRefBI(null);
  const uploaded = s.uploadedLogos || [];

  const addFiles = (files) => {
    const arr = Array.from(files || []).filter(f => /^image\//.test(f.type));
    if (!arr.length) return;
    const MAX = 800 * 1024; // 800 KB per logo — keep payload to Gemini sane
    arr.forEach(file => {
      const reader = new FileReader();
      reader.onload = e => {
        const dataUrl = e.target.result;
        if (dataUrl.length > MAX * 1.4) {
          showToast && showToast(`Skipped ${file.name}: too large (>${Math.round(MAX/1024)} KB)`, 'warn');
          return;
        }
        setS(prev => ({
          ...prev,
          uploadedLogos: [...(prev.uploadedLogos || []), { name: file.name.replace(/\.[^.]+$/, ''), dataUrl, mime: file.type }],
        }));
      };
      reader.readAsDataURL(file);
    });
  };

  const removeAt = (idx) => setS(prev => ({
    ...prev,
    uploadedLogos: (prev.uploadedLogos || []).filter((_, i) => i !== idx),
  }));

  return (
    <div>
      <div
        onClick={() => inputRef.current?.click()}
        onDragOver={e => { e.preventDefault(); e.currentTarget.style.background = '#FFF8EE'; e.currentTarget.style.borderColor = '#F97316'; }}
        onDragLeave={e => { e.currentTarget.style.background = '#fff'; e.currentTarget.style.borderColor = '#E5E7EB'; }}
        onDrop={e => {
          e.preventDefault();
          e.currentTarget.style.background = '#fff';
          e.currentTarget.style.borderColor = '#E5E7EB';
          addFiles(e.dataTransfer.files);
        }}
        style={{
          border: '1.5px dashed #E5E7EB', borderRadius: 12, padding: 18,
          textAlign: 'center', background: '#fff', color: '#6B7280',
          fontSize: 12.5, cursor: 'pointer', transition: 'all 140ms',
        }}>
        <div style={{ color: '#1A2B3B', fontSize: 13, fontWeight: 700, marginBottom: 3 }}>
          Drop logo files here
        </div>
        Or click to browse. PNG / JPG / WebP / SVG · multiple files OK.
        <input ref={inputRef} type="file" accept="image/*" multiple style={{ display: 'none' }}
          onChange={e => { addFiles(e.target.files); e.target.value = ''; }} />
      </div>

      {uploaded.length > 0 && (
        <div style={{
          marginTop: 10,
          display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(96px, 1fr))', gap: 8,
        }}>
          {uploaded.map((logo, i) => (
            <div key={i} style={{
              border: '1.5px solid #E5E7EB', borderRadius: 10,
              background: '#fff', padding: 6,
              display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 4,
              position: 'relative',
            }}>
              <div style={{
                width: '100%', aspectRatio: '1 / 1', background: '#F9FAFB',
                borderRadius: 6, display: 'flex', alignItems: 'center', justifyContent: 'center', overflow: 'hidden',
              }}>
                <img src={logo.dataUrl} alt={logo.name} style={{ width: '85%', height: '85%', objectFit: 'contain' }} />
              </div>
              <div style={{ fontSize: 10.5, fontWeight: 700, color: '#1A2B3B', textAlign: 'center', lineHeight: 1.2, wordBreak: 'break-word', maxHeight: 28, overflow: 'hidden' }}>
                {logo.name}
              </div>
              <button type="button" onClick={() => removeAt(i)} aria-label={`Remove ${logo.name}`}
                style={{
                  position: 'absolute', top: 3, right: 3,
                  width: 16, height: 16, borderRadius: '50%',
                  background: 'rgba(0,0,0,0.55)', color: '#fff',
                  border: 'none', cursor: 'pointer', fontSize: 10, fontWeight: 900,
                  display: 'flex', alignItems: 'center', justifyContent: 'center',
                  padding: 0, fontFamily: 'inherit',
                }}>×</button>
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

function IconPickerBI({ s, setS }) {
  // Filtered list based on the search query
  const q = (s.iconQuery || '').trim().toLowerCase();
  const list = ICONS_BI.filter(ic => {
    if (!q) return true;
    return (
      (ic.title || '').toLowerCase().includes(q) ||
      (ic.slug  || '').toLowerCase().includes(q) ||
      (ic.category || '').toLowerCase().includes(q) ||
      (ICON_CATEGORY_LABELS[ic.category] || '').toLowerCase().includes(q)
    );
  });
  const style = s.iconStyle || 'color';
  const toggleIcon = (slug) => {
    setS(prev => {
      const has = prev.selectedIcons.includes(slug);
      return { ...prev, selectedIcons: has ? prev.selectedIcons.filter(x => x !== slug) : [...prev.selectedIcons, slug] };
    });
  };
  if (!ICONS_BI.length) {
    return <div style={{ fontSize:12, color:'#9CA3AF', fontStyle:'italic' }}>Icon library failed to load — make sure /project/3dicons-data.js is included.</div>;
  }
  return (
    <div style={{
      maxHeight: 360, overflowY: 'auto',
      border: '1.5px solid #E5E7EB', borderRadius: 10,
      background: '#fff', padding: 10,
      display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(80px, 1fr))', gap: 8,
    }}>
      {list.length === 0 && <div style={{ gridColumn:'1 / -1', fontSize:12, color:'#9CA3AF', padding:'12px', textAlign:'center' }}>No icons match "{q}".</div>}
      {list.map(ic => {
        const active = s.selectedIcons.includes(ic.slug);
        const url = ic[style] || ic.color || ic.gradient || ic.clay;
        return (
          <button key={ic.slug} type="button" onClick={() => toggleIcon(ic.slug)}
            title={`${ic.title} · ${ic.category}`}
            style={{
              border: `1.5px solid ${active ? '#F97316' : '#E5E7EB'}`,
              background: active ? '#FFF8EE' : '#fff',
              boxShadow: active ? '0 0 0 3px rgba(249,115,22,0.18)' : 'none',
              borderRadius: 10, padding: 6, cursor: 'pointer', fontFamily: 'inherit', color: '#1A2B3B',
              display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 4, position: 'relative',
            }}>
            <div style={{ width:'100%', aspectRatio:'1 / 1', background:'#F9FAFB', borderRadius:6, display:'flex', alignItems:'center', justifyContent:'center', overflow:'hidden' }}>
              {url
                ? <img src={url} alt={ic.title} loading="lazy" style={{ width:'90%', height:'90%', objectFit:'contain' }} />
                : <span style={{ fontSize:10, color:'#9CA3AF' }}>n/a</span>}
            </div>
            <div style={{ fontSize: 10.5, fontWeight: 700, textAlign:'center', lineHeight: 1.2, color: active ? '#C7501A' : '#1A2B3B', wordBreak:'break-word' }}>{ic.title}</div>
            {active && <div style={{ position:'absolute', top:4, right:4, width:14, height:14, borderRadius:'50%', background:'#F97316', color:'#fff', fontSize:9, fontWeight:900, display:'flex', alignItems:'center', justifyContent:'center' }}>✓</div>}
          </button>
        );
      })}
    </div>
  );
}

function ActiveElementsBI({ s }) {
  const tags = [];
  const tpl = TEMPLATES_BI.find(t => t.id === s.template);
  if (tpl && tpl.id !== 'freeform') {
    const filled = Object.values(s.slotFills || {}).filter(f => f && f.value).length;
    tags.push({ k:`Template: ${tpl.title}${filled ? ` · ${filled}/${tpl.slots.filter(sl => !sl.kinds.includes('arrow-fixed') && !sl.kinds.includes('vs-fixed')).length}` : ''}`, accent:true });
  }
  if (s.toggles.logo)     tags.push({ k:'WiseFunnel logo', accent:false });
  if (s.toggles.title)    tags.push({ k: s.titleSrc==='custom' && s.titleText ? `Title: "${s.titleText.slice(0,28)}"` : 'Post title', accent:true });
  if (s.toggles.screens && s.selectedScreens.length) tags.push({ k:`${s.selectedScreens.length} screenshot${s.selectedScreens.length>1?'s':''}`, accent:false });
  if (s.toggles.people && s.selectedNiches.length)   tags.push({ k:`${s.selectedNiches.length} portrait${s.selectedNiches.length>1?'s':''}`, accent:true });
  if (s.toggles.threed && s.selectedIcons.length)    tags.push({ k:`${s.selectedIcons.length} 3D icon${s.selectedIcons.length>1?'s':''} · ${s.iconStyle || 'color'}`, accent:false });
  if (s.toggles.logos) {
    const presets = s.selectedLogos.length;
    const customs = (s.customLogos||'').split(',').map(x=>x.trim()).filter(Boolean).length;
    const uploads = (s.uploadedLogos || []).length;
    const total = presets + customs + uploads;
    if (total) tags.push({ k:`${total} logo${total>1?'s':''}${uploads ? ` · ${uploads} uploaded` : ''}`, accent:true });
  }
  if (s.toggles.numbers && s.numbersText)            tags.push({ k:'KPI numbers', accent:true });
  if (s.toggles.agency && (s.agencyName || s.funnelName)) tags.push({ k:'Agency callout', accent:false });
  if (s.toggles.channels && Object.values(s.channels).some(Boolean)) tags.push({ k:'Channel breakdown', accent:false });
  if (!tags.length) return <span style={{ color:'rgba(255,255,255,0.4)', fontStyle:'italic', fontSize:12 }}>No elements selected</span>;
  return (
    <div style={{ display:'flex', flexWrap:'wrap', gap:6 }}>
      {tags.map((t,i) => (
        <span key={i} style={{
          background: t.accent ? 'rgba(249,115,22,0.18)' : 'rgba(255,255,255,0.08)',
          border: `1px solid ${t.accent ? 'rgba(249,115,22,0.4)' : 'rgba(255,255,255,0.14)'}`,
          color: t.accent ? '#FFD1A8' : '#fff',
          borderRadius: 6, padding: '3px 9px', fontSize: 11, fontWeight: 600,
        }}>{t.k}</span>
      ))}
    </div>
  );
}

function RailSection({ label, children, actionLabel, onAction }) {
  return (
    <div style={{ marginBottom:18, paddingBottom:18, borderBottom:'1px solid rgba(255,255,255,0.08)' }}>
      <div style={{ display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom:6 }}>
        <span style={{ fontFamily:'JetBrains Mono, monospace', fontSize:9.5, letterSpacing:'0.14em', textTransform:'uppercase', color:'rgba(255,255,255,0.6)' }}>{label}</span>
        {actionLabel && (
          <button type="button" onClick={onAction} style={{ background:'rgba(255,255,255,0.08)', border:'1px solid rgba(255,255,255,0.14)', color:'#fff', padding:'3px 9px', borderRadius:6, fontSize:10, fontWeight:700, cursor:'pointer', fontFamily:'inherit', letterSpacing:'0.06em' }}>{actionLabel}</button>
        )}
      </div>
      {children}
    </div>
  );
}

// ── PNG → WebP via canvas (browser-side) ──
async function pngToWebp(buffer, mimeIn) {
  return new Promise((resolve) => {
    try {
      const blob = new Blob([buffer], { type: mimeIn || 'image/png' });
      const url = URL.createObjectURL(blob);
      const img = new Image();
      img.onload = () => {
        const c = document.createElement('canvas');
        c.width = img.naturalWidth; c.height = img.naturalHeight;
        const ctx = c.getContext('2d');
        ctx.drawImage(img, 0, 0);
        c.toBlob(b => {
          URL.revokeObjectURL(url);
          if (b) resolve(b);
          else resolve(blob); // fallback to original PNG
        }, 'image/webp', 0.88);
      };
      img.onerror = () => { URL.revokeObjectURL(url); resolve(blob); };
      img.src = url;
    } catch (e) {
      resolve(new Blob([buffer], { type: mimeIn || 'image/png' }));
    }
  });
}

// ── Base64 PNG/JPG → Base64 WebP (used at Generate + Import time) ──
async function compressBase64ToWebP(base64In, mimeIn, opts = {}) {
  const quality = opts.quality != null ? opts.quality : 0.88;
  const maxW    = opts.maxWidth  || 1600;  // never upscale; cap large outputs
  const maxH    = opts.maxHeight || 1600;
  return new Promise((resolve) => {
    try {
      const byteString = atob(base64In);
      const buf = new ArrayBuffer(byteString.length);
      const arr = new Uint8Array(buf);
      for (let i = 0; i < byteString.length; i++) arr[i] = byteString.charCodeAt(i);
      const originalBytes = arr.length;
      const blob = new Blob([buf], { type: mimeIn || 'image/png' });
      const url = URL.createObjectURL(blob);
      const img = new Image();
      img.onload = () => {
        let w = img.naturalWidth, h = img.naturalHeight;
        const scale = Math.min(1, maxW / w, maxH / h);
        w = Math.round(w * scale); h = Math.round(h * scale);
        const c = document.createElement('canvas');
        c.width = w; c.height = h;
        const ctx = c.getContext('2d');
        ctx.imageSmoothingQuality = 'high';
        ctx.drawImage(img, 0, 0, w, h);
        c.toBlob(out => {
          URL.revokeObjectURL(url);
          if (!out) return resolve({ base64: base64In, mime: mimeIn || 'image/png', originalBytes, newBytes: originalBytes, width: img.naturalWidth, height: img.naturalHeight });
          const reader = new FileReader();
          reader.onload = e => {
            const m = /^data:([^;]+);base64,(.*)$/.exec(e.target.result);
            if (m) resolve({ base64: m[2], mime: m[1], originalBytes, newBytes: out.size, width: w, height: h });
            else resolve({ base64: base64In, mime: mimeIn || 'image/png', originalBytes, newBytes: originalBytes, width: img.naturalWidth, height: img.naturalHeight });
          };
          reader.readAsDataURL(out);
        }, 'image/webp', quality);
      };
      img.onerror = () => { URL.revokeObjectURL(url); resolve({ base64: base64In, mime: mimeIn || 'image/png', originalBytes, newBytes: originalBytes, width: 0, height: 0 }); };
      img.src = url;
    } catch (e) {
      resolve({ base64: base64In, mime: mimeIn || 'image/png', originalBytes: 0, newBytes: 0, width: 0, height: 0 });
    }
  });
}

function fmtBytes(n) {
  if (n == null) return '—';
  if (n < 1024) return `${n} B`;
  if (n < 1024 * 1024) return `${(n/1024).toFixed(0)} KB`;
  return `${(n/(1024*1024)).toFixed(2)} MB`;
}

window.BlogImageGeneratorPage = BlogImageGeneratorPage;
