// builder.jsx — Clipper Round the World Post Builder.
//
// Re-skins the 16°S builder engine for the Clipper fleet: same proven export
// pipeline (captureWithPhotos → JPEG, exportFinalVideo → mp4), same stage +
// drop-media model, but the sidebar is rebuilt around teams / race context /
// results rather than places + weather. Templates live on window.ClipperTemplates.

const { useState, useEffect, useRef, useCallback } = React;

// ── captureWithPhotos — identical compositing approach to the 16°S builder:
// rasterise the text/design overlay (photo regions punched transparent), then
// draw bg → each dropped photo (cover-cropped) → overlay on a canvas. ─────────
async function captureWithPhotos(node, tw, th, bg, mime, quality) {
  const baseW = node.offsetWidth || tw;
  const scale = tw / baseW;
  const specs = window.imageSlotCaptureSpecs ? window.imageSlotCaptureSpecs(node) : [];
  const restoreHoles = window.imageSlotPunchHoles ? window.imageSlotPunchHoles(node) : () => {};
  let overlayUrl;
  try {
    overlayUrl = await htmlToImage.toPng(node, {
      width: tw, height: th, pixelRatio: 1,
      style: { transform: `scale(${scale})`, transformOrigin: 'top left', width: baseW + 'px', height: node.offsetHeight + 'px' },
      cacheBust: true,
    });
  } finally { restoreHoles(); }
  const load = (src) => new Promise((res, rej) => { const im = new Image(); im.onload = () => res(im); im.onerror = rej; im.src = src; });
  const canvas = document.createElement('canvas');
  canvas.width = tw; canvas.height = th;
  const ctx = canvas.getContext('2d');
  if (bg) { ctx.fillStyle = bg; ctx.fillRect(0, 0, tw, th); }
  for (const sp of specs) {
    try {
      const im = await load(sp.url);
      ctx.save();
      ctx.beginPath();
      ctx.rect(sp.clip.x * scale, sp.clip.y * scale, sp.clip.w * scale, sp.clip.h * scale);
      ctx.clip();
      ctx.drawImage(im, sp.draw.x * scale, sp.draw.y * scale, sp.draw.w * scale, sp.draw.h * scale);
      ctx.restore();
    } catch (e) { /* skip a photo that fails to load */ }
  }
  const overlay = await load(overlayUrl);
  ctx.drawImage(overlay, 0, 0, tw, th);
  return canvas.toDataURL(mime, quality);
}

function shouldShareFiles(files) {
  try { if (!(navigator.canShare && navigator.canShare({ files }))) return false; } catch (e) { return false; }
  return window.matchMedia('(display-mode: standalone)').matches || navigator.standalone === true
    || window.matchMedia('(pointer: coarse)').matches;
}
async function shareFiles(files, title) {
  try { await navigator.share({ files, title }); return true; }
  catch (e) { return !!(e && e.name === 'AbortError'); }
}
function downloadFile(file) {
  const a = document.createElement('a');
  a.download = file.name; a.href = URL.createObjectURL(file); a.click();
  setTimeout(() => URL.revokeObjectURL(a.href), 5000);
}
async function downloadAll(files) {
  for (const f of files) { downloadFile(f); await new Promise((r) => setTimeout(r, 150)); }
}
async function dataUrlToFile(dataUrl, name) {
  const blob = await (await fetch(dataUrl)).blob();
  return new File([blob], name, { type: blob.type || 'application/octet-stream' });
}

// The export compositor punches each template's background transparent (so the
// dropped photo can be drawn straight onto the canvas) and refills with one
// colour. Every template now sits full-bleed on Heavy Black, so the surround is
// always black; per-template overrides can go here if that ever changes.
const EXPORT_BG = {};
const exportBg = (template) => EXPORT_BG[template] || '#0f0b0c';

const RATIOS = {
  '4:5':  { name: '4:5 · feed',     w: 480, h: 600, target: [1080, 1350] },
  '1:1':  { name: '1:1 · feed',     w: 540, h: 540, target: [1080, 1080] },
  '9:16': { name: '9:16 · story',   w: 380, h: 676, target: [1080, 1920] },
  '16:9': { name: '16:9 · landscape', w: 820, h: 461, target: [1920, 1080] },
  '1.91:1': { name: '1.91:1 · link ad', w: 820, h: 429, target: [1200, 628] },
};
// Formats the "Export kit" button bakes in one click.
const KIT_FORMATS = ['1:1', '4:5', '9:16', '16:9', '1.91:1'];

const FLAGS = ['🏁', '🇬🇧', '🇺🇸', '🇰🇷', '🇨🇳', '🇺🇾', '🇿🇦', '🇵🇦', '🇵🇹', '🇫🇷', '🇩🇪', '🇮🇪', '🇦🇺', '🇻🇳', '🇺🇳'];

// Default standings, seeded from the Stage 10 "Overall Standings" still.
const DEFAULT_STANDINGS = [
  'gosh 96 12 22 4 126',
  'seattle 69 10 2 0 81',
  'scotland 71 5 4 1 79',
  'lbs 66 4 8 1 77',
  'punta 59 10 7 9 67',
  'tongyeong 56 2 5 2 61',
  'qingdao 57 4 2 4 59',
  'unicef 56 0 1 7 57',
  'warrant 44 2 2 1 47',
  'washington 44 3 1 6 42',
].join('\n');

function parseStandings(text) {
  return String(text || '').split('\n').map((ln) => ln.trim()).filter(Boolean).map((ln) => {
    const p = ln.split(/\s+/);
    const key = p[0];
    const n = (i) => (p[i] != null && p[i] !== '' ? Number(p[i]) : null);
    return { key, race: n(1), gate: n(2), sprint: n(3), pen: n(4), overall: n(5) != null ? n(5) : '' };
  });
}

const DEFAULT_FIELDS = {
  year: '25·26', legNo: 7, stageNo: 10, stageName: "Warrant's West Coast Challenge",
  p1: 'gosh', p2: 'seattle', p3: 'lbs',
  finishTime: '1D 11H 31M 56S', gap2: '+7H 13M 57S', gap3: '+8H 26M 12S',
  elapsed2: '1D 18H 45M 53S', elapsed3: '1D 19H 58M 08S',
  pts1: '+3', pts2: '+2', pts3: '+1', cutouts: 2, autoCut: true,
  heroTeam: 'tongyeong', statMiles: '4,250 nm', statSpeed: '24.1 kts', statDays: '31',
  recruitOffer: 'Applications open · 2027-28', recruitHeadline: 'Could you do it?',
  recruitSub: "No experience needed — we'll train you to race the world's oceans.",
  recruitCta: 'Apply now', recruitUrl: 'clipperroundtheworld.com',
  gpts1: '3', gpts2: '2', gpts3: '1',
  presenter: 'Hyde Sails', partner: 'Hambledon Vineyard',
  fromPort: 'Seattle', fromFlag: '🇺🇸', fromCountry: 'USA', toPort: 'Panama', toFlag: '🇵🇦', toCountry: 'Panama',
  crewNames: 'Angela Brandsma · Lauren Corn',
  crewName: 'Christin Schulz', crewAge: '38', crewRole: 'Project Manager',
  crewBoat: 'Tongyeong', crewLegs: 'Legs 2 & 3', crewFlag: '🇩🇪',
  crewQuote: 'Did I really do that? Or was it a dream?', crewCta: "Watch Christin's story now",
  city: 'Washington, DC', dates: 'June 14–22',
  eventKind: 'Virtual Event', eventTitle: 'From the South Atlantic to the Roaring Forties',
  eventWhen: 'Thursday 11 June | 1600–1700', eventLive: true,
  count: '14', countdownLabel: 'Until Washington, DC',
  condPlace: 'The Roaring Forties', wind: '38', dir: 'SW', sea: '6.2', position: '46°S', position2: '142°W', condDay: 'Day 18',
  standingsText: DEFAULT_STANDINGS,
  mediaKind: 'photo',
};

// Map flat fields → each template's data shape.
function buildData(template, f) {
  const stage = `Stage ${f.stageNo}: ${f.stageName}`;
  const common = { year: f.year, stage, stageNo: f.stageNo, legColour: window.legColour(f.legNo), mediaKind: f.mediaKind };
  switch (template) {
    case 'raceresults':
      return { ...common, partner: f.partner, entries: [
        { key: f.p1, rank: 1, pts: f.pts1 }, { key: f.p2, rank: 2, pts: f.pts2 }, { key: f.p3, rank: 3, pts: f.pts3 }] };
    case 'oceansprint':
      return { ...common, presenter: f.presenter, cutouts: f.cutouts, autoCut: f.autoCut, entries: [
        { key: f.p1, rank: 1, pts: f.pts1, line1: f.finishTime },
        { key: f.p2, rank: 2, pts: f.pts2, line1: f.gap2, line2: f.elapsed2 },
        { key: f.p3, rank: 3, pts: f.pts3, line1: f.gap3, line2: f.elapsed3 }] };
    case 'scoringgate':
      return { ...common, presenter: f.presenter, entries: [
        { key: f.p1, rank: 1, pts: f.gpts1 }, { key: f.p2, rank: 2, pts: f.gpts2 }, { key: f.p3, rank: 3, pts: f.gpts3 }] };
    case 'standings':
      return { ...common, standings: parseStandings(f.standingsText) };
    case 'stagewinners':
      return { ...common, entries: [{ key: f.p1, rank: 1 }], team1: f.p1, crewNames: f.crewNames,
        route: { fromPort: f.fromPort, fromFlag: f.fromFlag, fromCountry: f.fromCountry,
          toPort: f.toPort, toFlag: f.toFlag, toCountry: f.toCountry } };
    case 'crewstory':
      return { ...common, crew: { name: f.crewName, age: f.crewAge, role: f.crewRole, boat: f.crewBoat,
        legs: f.crewLegs, flag: f.crewFlag, quote: f.crewQuote, cta: f.crewCta } };
    case 'hostport':
      return { ...common, city: f.city, dates: f.dates, partner: f.partner };
    case 'liveevent':
      return { ...common, event: { kind: f.eventKind, title: f.eventTitle, when: f.eventWhen, live: f.eventLive } };
    case 'countdown':
      return { ...common, count: f.count, countdownLabel: f.countdownLabel, dates: f.dates };
    case 'conditions':
      return { ...common, conditions: { place: f.condPlace, wind: f.wind, dir: f.dir, sea: f.sea,
        position: f.position, position2: f.position2, day: f.condDay } };
    case 'crewhero':
      return { ...common, heroTeam: f.heroTeam, autoCut: f.autoCut,
        crew: { name: f.crewName, age: f.crewAge, role: f.crewRole, boat: f.crewBoat, legs: f.crewLegs, flag: f.crewFlag },
        stats: [{ label: 'Nautical miles', value: f.statMiles }, { label: 'Top speed', value: f.statSpeed }, { label: 'Days at sea', value: f.statDays }] };
    case 'recruit':
      return { ...common, recruit: { headline: f.recruitHeadline, sub: f.recruitSub, cta: f.recruitCta, offer: f.recruitOffer, url: f.recruitUrl } };
    default: return common;
  }
}

// Which sidebar groups a template needs.
const NEEDS = {
  raceresults:  ['podium', 'partner'],
  oceansprint:  ['podium', 'times', 'presenter', 'cutouts'],
  scoringgate:  ['gate', 'presenter'],
  standings:    ['standings'],
  stagewinners: ['winner', 'route', 'crewnames'],
  crewstory:    ['crew'],
  hostport:     ['port'],
  liveevent:    ['event'],
  countdown:    ['countdown'],
  conditions:   ['conditions'],
  crewhero:     ['hero'],
  recruit:      ['recruit'],
};
const has = (template, g) => NEEDS[template] && NEEDS[template].includes(g);

// ── small controls ──────────────────────────────────────────────────────────
function Field({ label, value, onChange, placeholder, w }) {
  return (
    <label style={w ? { width: w } : null}>
      <span className="lbl">{label}</span>
      <input type="text" value={value || ''} placeholder={placeholder} onChange={(e) => onChange(e.target.value)} />
    </label>
  );
}
function TextArea({ label, value, onChange, placeholder, rows = 3, mono }) {
  return (
    <label>
      <span className="lbl">{label}</span>
      <textarea rows={rows} value={value || ''} placeholder={placeholder} onChange={(e) => onChange(e.target.value)}
        style={mono ? { fontFamily: 'ui-monospace, Menlo, monospace', fontSize: 12, letterSpacing: 0 } : null} />
    </label>
  );
}
function TeamSelect({ label, value, onChange }) {
  return (
    <label>
      <span className="lbl">{label}</span>
      <select value={value} onChange={(e) => onChange(e.target.value)}>
        {window.TEAMS.map((t) => <option key={t.key} value={t.key}>{t.name}</option>)}
      </select>
    </label>
  );
}
function FlagSelect({ label, value, onChange }) {
  return (
    <label>
      <span className="lbl">{label}</span>
      <select value={value} onChange={(e) => onChange(e.target.value)} style={{ fontSize: 18 }}>
        {FLAGS.map((fl) => <option key={fl} value={fl}>{fl}</option>)}
      </select>
    </label>
  );
}
function Row({ children }) { return <div style={{ display: 'flex', gap: 8 }}>{children}</div>; }

// ── App ───────────────────────────────────────────────────────────────────
function App() {
  const [template, setTemplate] = useState('raceresults');
  const [ratio, setRatio] = useState('4:5');
  const [fields, setFields] = useState(DEFAULT_FIELDS);
  const [exporting, setExporting] = useState(false);
  const [videoExporting, setVideoExporting] = useState(false);
  const [videoProgress, setVideoProgress] = useState({ frac: 0, label: '' });
  const [pendingShare, setPendingShare] = useState(null);
  const [resetN, setResetN] = useState(0);
  const [kitBusy, setKitBusy] = useState(false);
  const [kitLabel, setKitLabel] = useState('');
  const previewRef = useRef(null);
  const getPreviewNode = useCallback(() => previewRef.current, []);
  const set = (patch) => setFields((f) => ({ ...f, ...patch }));

  const Component = window.ClipperTemplates[template];
  const name = window.ClipperTemplateNames[template];
  const r = RATIOS[ratio];
  const data = buildData(template, fields);
  const slotId = `clip-${template}`;
  const saveVerb = (window.matchMedia && window.matchMedia('(pointer: coarse)').matches) ? 'Save' : 'Download';

  const deliver = useCallback(async (files, title, label) => {
    if (shouldShareFiles(files)) {
      if (await shareFiles(files, title)) { setPendingShare(null); return; }
      setPendingShare({ files, title, label }); return;
    }
    await downloadAll(files);
  }, []);

  const onExport = useCallback(async () => {
    if (!previewRef.current) return;
    setExporting(true);
    try {
      const node = previewRef.current;
      const [tw, th] = r.target;
      const hidden = [...node.querySelectorAll('[data-no-export]')];
      const prevVis = hidden.map((el) => el.style.visibility);
      hidden.forEach((el) => { el.style.visibility = 'hidden'; });
      let dataUrl;
      try { dataUrl = await captureWithPhotos(node, tw, th, exportBg(template), 'image/jpeg', 0.95); }
      finally { hidden.forEach((el, i) => { el.style.visibility = prevVis[i]; }); }
      const safe = (s) => (s || 'post').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
      const fname = `clipper-${template}-${safe(fields.stageName)}-${ratio.replace(':', 'x')}.jpeg`;
      await deliver([await dataUrlToFile(dataUrl, fname)], 'Clipper post', 'image');
    } catch (e) { console.error(e); alert('Export failed: ' + e.message); }
    finally { setExporting(false); }
  }, [r, template, ratio, fields.stageName, deliver]);

  // ────────── Export kit — bake the current design at every format at once ──
  // Renders the template off-screen at each KIT_FORMATS ratio (image-slots share
  // their dropped photo by id), composites each via captureWithPhotos, and
  // delivers all the JPEGs together. Proves "design once, ship everywhere".
  const onExportKit = async () => {
    setKitBusy(true);
    const host = document.createElement('div');
    host.style.cssText = 'position:fixed;left:-100000px;top:0;z-index:-1;';
    document.body.appendChild(host);
    const root = ReactDOM.createRoot(host);
    const safe = (s) => (s || 'post').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
    const files = [];
    try {
      for (let i = 0; i < KIT_FORMATS.length; i++) {
        const key = KIT_FORMATS[i]; const rr = RATIOS[key];
        setKitLabel(`${i + 1}/${KIT_FORMATS.length} · ${key}`);
        const node = await new Promise((resolve) => {
          function Frame() {
            const ref = useRef(null);
            useEffect(() => { const t = setTimeout(() => resolve(ref.current), 420); return () => clearTimeout(t); }, []);
            return (
              <div ref={ref} style={{ width: rr.w, height: rr.h, position: 'relative', overflow: 'hidden', background: '#0f0b0c' }}>
                <Component w={rr.w} h={rr.h} slotId={slotId} data={{ ...data, mediaKind: 'photo', safe: rr.safe }} />
              </div>
            );
          }
          root.render(<Frame />);
        });
        const [tw, th] = rr.target;
        const dataUrl = await captureWithPhotos(node, tw, th, exportBg(template), 'image/jpeg', 0.95);
        files.push(await dataUrlToFile(dataUrl, `clipper-${template}-${safe(fields.stageName)}-${key.replace(/[:.]/g, 'x')}.jpeg`));
      }
      await deliver(files, 'Clipper kit', `${files.length} formats`);
    } catch (e) { console.error(e); alert('Export kit failed: ' + e.message); }
    finally { root.unmount(); host.remove(); setKitBusy(false); setKitLabel(''); }
  };

  const onExportVideo = useCallback(async () => {
    if (!previewRef.current) return;
    setVideoExporting(true);
    setVideoProgress({ frac: 0, label: 'Starting…' });
    try {
      const [tw, th] = r.target;
      const blob = await window.exportFinalVideo({
        previewNode: previewRef.current, targetW: tw, targetH: th, maxDuration: 90, fps: 25, bg: exportBg(template),
        onProgress: (frac, label) => setVideoProgress({ frac, label }),
      });
      const safe = (s) => (s || 'post').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
      const ext = blob.extension || 'webm';
      const fname = `clipper-${template}-${safe(fields.stageName)}-${ratio.replace(':', 'x')}.${ext}`;
      await deliver([new File([blob], fname, { type: ext === 'mp4' ? 'video/mp4' : (blob.type || 'video/webm') })], 'Clipper video', 'video');
      setVideoProgress({ frac: 1, label: 'Done' });
    } catch (e) { console.error(e); alert('Video export failed: ' + e.message); }
    finally { setVideoExporting(false); setTimeout(() => setVideoProgress({ frac: 0, label: '' }), 1500); }
  }, [r, template, ratio, fields.stageName, deliver]);

  return (
    <div className="app">
      <aside className="panel">
        <div className="brand">
          <window.ClipperLogo h={30} year={fields.year} />
          <div className="label">Post Builder</div>
        </div>

        <div className="group">
          <div className="group-title">// Template</div>
          <div className="template-row">
            {Object.entries(window.ClipperTemplateNames).map(([k, n]) => (
              <button key={k} className={`ratio-btn ${template === k ? 'active' : ''}`} onClick={() => setTemplate(k)}>{n}</button>
            ))}
          </div>
        </div>

        <div className="group">
          <div className="group-title">// Aspect Ratio</div>
          <div className="ratio-row">
            {Object.keys(RATIOS).map((k) => (
              <button key={k} className={`ratio-btn ${ratio === k ? 'active' : ''}`} onClick={() => setRatio(k)}>{k}</button>
            ))}
          </div>
        </div>

        <div className="group">
          <div className="group-title">// Media</div>
          <div className="ratio-row" style={{ gridTemplateColumns: '1fr 1fr' }}>
            <button className={`ratio-btn ${fields.mediaKind !== 'video' ? 'active' : ''}`} onClick={() => set({ mediaKind: 'photo' })}>Photo</button>
            <button className={`ratio-btn ${fields.mediaKind === 'video' ? 'active' : ''}`} onClick={() => set({ mediaKind: 'video' })}>Video</button>
          </div>
          <div className="hint">Drop a photo/video on the preview frame. {template === 'oceansprint' ? 'Ocean Sprint has three photo slots.' : ''}</div>
        </div>

        {/* Race context — every template uses it */}
        <div className="group">
          <div className="group-title">// Race</div>
          <Row>
            <Field label="Edition" value={fields.year} onChange={(v) => set({ year: v })} placeholder="25·26" />
            <label style={{ width: 110 }}>
              <span className="lbl">Leg · colour</span>
              <select value={fields.legNo} onChange={(e) => set({ legNo: Number(e.target.value) })}>
                {[1, 2, 3, 4, 5, 6, 7, 8].map((n) => <option key={n} value={n}>Leg {n}</option>)}
              </select>
            </label>
          </Row>
          <Row>
            <Field label="Stage #" value={fields.stageNo} onChange={(v) => set({ stageNo: v })} placeholder="10" w={90} />
            <Field label="Stage name" value={fields.stageName} onChange={(v) => set({ stageName: v })} placeholder="Warrant's West Coast Challenge" />
          </Row>
        </div>

        {(has(template, 'podium') || has(template, 'gate') || has(template, 'winner')) && (
          <div className="group">
            <div className="group-title">// {has(template, 'winner') ? 'Winning team' : 'Podium'}</div>
            <TeamSelect label="1st" value={fields.p1} onChange={(v) => set({ p1: v })} />
            {!has(template, 'winner') && <TeamSelect label="2nd" value={fields.p2} onChange={(v) => set({ p2: v })} />}
            {!has(template, 'winner') && <TeamSelect label="3rd" value={fields.p3} onChange={(v) => set({ p3: v })} />}
          </div>
        )}

        {has(template, 'times') && (
          <div className="group">
            <div className="group-title">// Times &amp; points</div>
            <Field label="1st · finish time" value={fields.finishTime} onChange={(v) => set({ finishTime: v })} placeholder="1D 11H 31M 56S" />
            <Row>
              <Field label="2nd · gap" value={fields.gap2} onChange={(v) => set({ gap2: v })} placeholder="+7H 13M 57S" />
              <Field label="3rd · gap" value={fields.gap3} onChange={(v) => set({ gap3: v })} placeholder="+8H 26M 12S" />
            </Row>
            <Row>
              <Field label="2nd · elapsed" value={fields.elapsed2} onChange={(v) => set({ elapsed2: v })} placeholder="1D 18H 45M 53S" />
              <Field label="3rd · elapsed" value={fields.elapsed3} onChange={(v) => set({ elapsed3: v })} placeholder="1D 19H 58M 08S" />
            </Row>
            <Row>
              <Field label="Pts 1" value={fields.pts1} onChange={(v) => set({ pts1: v })} placeholder="+3" />
              <Field label="Pts 2" value={fields.pts2} onChange={(v) => set({ pts2: v })} placeholder="+2" />
              <Field label="Pts 3" value={fields.pts3} onChange={(v) => set({ pts3: v })} placeholder="+1" />
            </Row>
          </div>
        )}

        {has(template, 'podium') && !has(template, 'times') && (
          <div className="group">
            <div className="group-title">// Points</div>
            <Row>
              <Field label="Pts 1" value={fields.pts1} onChange={(v) => set({ pts1: v })} placeholder="+3" />
              <Field label="Pts 2" value={fields.pts2} onChange={(v) => set({ pts2: v })} placeholder="+2" />
              <Field label="Pts 3" value={fields.pts3} onChange={(v) => set({ pts3: v })} placeholder="+1" />
            </Row>
          </div>
        )}

        {has(template, 'cutouts') && (
          <div className="group">
            <div className="group-title">// Crew cut-outs</div>
            <label>
              <span className="lbl">How many ({fields.cutouts})</span>
              <input type="range" min={0} max={6} step={1} value={fields.cutouts}
                onChange={(e) => set({ cutouts: parseInt(e.target.value, 10) })} style={{ width: '100%', accentColor: '#e0102a' }} />
            </label>
            <label>
              <span className="lbl">Auto-remove background</span>
              <div className="ratio-row" style={{ gridTemplateColumns: '1fr 1fr' }}>
                <button className={`ratio-btn ${fields.autoCut ? 'active' : ''}`} onClick={() => set({ autoCut: true })}>On</button>
                <button className={`ratio-btn ${!fields.autoCut ? 'active' : ''}`} onClick={() => set({ autoCut: false })}>Off</button>
              </div>
            </label>
            <div className="hint">On → drop <b>any</b> photo and the crew is cut out in-browser (first use loads a small model; images never leave the device). Off → expects a transparent PNG. Drag to move, corner grip to resize; empty boxes never export.</div>
            <div className="actions" style={{ marginTop: 6 }}>
              <button className="ratio-btn" style={{ flex: 1 }} onClick={() => {
                try { Object.keys(localStorage).filter((k) => k.indexOf('clip-cut-') === 0).forEach((k) => localStorage.removeItem(k)); } catch (e) {}
                setResetN((n) => n + 1);
              }}>Reset cut-outs</button>
            </div>
          </div>
        )}

        {has(template, 'gate') && (
          <div className="group">
            <div className="group-title">// Gate points</div>
            <Row>
              <Field label="1st" value={fields.gpts1} onChange={(v) => set({ gpts1: v })} placeholder="3" />
              <Field label="2nd" value={fields.gpts2} onChange={(v) => set({ gpts2: v })} placeholder="2" />
              <Field label="3rd" value={fields.gpts3} onChange={(v) => set({ gpts3: v })} placeholder="1" />
            </Row>
          </div>
        )}

        {has(template, 'route') && (
          <div className="group">
            <div className="group-title">// Route</div>
            <Row>
              <FlagSelect label="From" value={fields.fromFlag} onChange={(v) => set({ fromFlag: v })} />
              <Field label="From port" value={fields.fromPort} onChange={(v) => set({ fromPort: v })} placeholder="Seattle" />
              <Field label="Country" value={fields.fromCountry} onChange={(v) => set({ fromCountry: v })} placeholder="USA" />
            </Row>
            <Row>
              <FlagSelect label="To" value={fields.toFlag} onChange={(v) => set({ toFlag: v })} />
              <Field label="To port" value={fields.toPort} onChange={(v) => set({ toPort: v })} placeholder="Panama" />
              <Field label="Country" value={fields.toCountry} onChange={(v) => set({ toCountry: v })} placeholder="Panama" />
            </Row>
          </div>
        )}

        {has(template, 'crewnames') && (
          <div className="group">
            <div className="group-title">// Crew names</div>
            <Field label="Names · separate with ·" value={fields.crewNames} onChange={(v) => set({ crewNames: v })} placeholder="Lou Boorman · Brian Uniacke" />
          </div>
        )}

        {has(template, 'crew') && (
          <div className="group">
            <div className="group-title">// Crew</div>
            <TextArea label="Pull quote" value={fields.crewQuote} onChange={(v) => set({ crewQuote: v })} placeholder="Did I really do that?" rows={2} />
            <Row>
              <FlagSelect label="Flag" value={fields.crewFlag} onChange={(v) => set({ crewFlag: v })} />
              <Field label="Name" value={fields.crewName} onChange={(v) => set({ crewName: v })} placeholder="Christin Schulz" />
            </Row>
            <Row>
              <Field label="Age" value={fields.crewAge} onChange={(v) => set({ crewAge: v })} placeholder="38" w={80} />
              <Field label="Role" value={fields.crewRole} onChange={(v) => set({ crewRole: v })} placeholder="Project Manager" />
            </Row>
            <Row>
              <Field label="On board" value={fields.crewBoat} onChange={(v) => set({ crewBoat: v })} placeholder="Tongyeong" />
              <Field label="Legs" value={fields.crewLegs} onChange={(v) => set({ crewLegs: v })} placeholder="Legs 2 & 3" />
            </Row>
            <Field label="CTA bar" value={fields.crewCta} onChange={(v) => set({ crewCta: v })} placeholder="Watch the story now" />
          </div>
        )}

        {has(template, 'port') && (
          <div className="group">
            <div className="group-title">// Host port</div>
            <Field label="City" value={fields.city} onChange={(v) => set({ city: v })} placeholder="Washington, DC" />
            <Field label="Dates" value={fields.dates} onChange={(v) => set({ dates: v })} placeholder="June 14–22" />
            <Field label="Host-port partner" value={fields.partner} onChange={(v) => set({ partner: v })} placeholder="Events DC" />
          </div>
        )}

        {has(template, 'event') && (
          <div className="group">
            <div className="group-title">// Event</div>
            <Field label="Kind / eyebrow" value={fields.eventKind} onChange={(v) => set({ eventKind: v })} placeholder="Virtual Event" />
            <TextArea label="Title" value={fields.eventTitle} onChange={(v) => set({ eventTitle: v })} placeholder="From the South Atlantic to the Roaring Forties" rows={2} />
            <Field label="When" value={fields.eventWhen} onChange={(v) => set({ eventWhen: v })} placeholder="Thursday 11 June | 1600–1700" />
            <div className="ratio-row" style={{ gridTemplateColumns: '1fr 1fr', marginTop: 4 }}>
              <button className={`ratio-btn ${fields.eventLive ? 'active' : ''}`} onClick={() => set({ eventLive: true })}>LIVE badge</button>
              <button className={`ratio-btn ${!fields.eventLive ? 'active' : ''}`} onClick={() => set({ eventLive: false })}>No badge</button>
            </div>
          </div>
        )}

        {has(template, 'countdown') && (
          <div className="group">
            <div className="group-title">// Countdown</div>
            <Row>
              <Field label="Number" value={fields.count} onChange={(v) => set({ count: v })} placeholder="14" w={90} />
              <Field label="Label" value={fields.countdownLabel} onChange={(v) => set({ countdownLabel: v })} placeholder="Until race start" />
            </Row>
            <Field label="Dates" value={fields.dates} onChange={(v) => set({ dates: v })} placeholder="June 14–22" />
            <div className="hint">In Video mode the number scrambles in.</div>
          </div>
        )}

        {has(template, 'conditions') && (
          <div className="group">
            <div className="group-title">// Conditions</div>
            <Field label="Place" value={fields.condPlace} onChange={(v) => set({ condPlace: v })} placeholder="The Roaring Forties" />
            <Row>
              <Field label="Wind · kts" value={fields.wind} onChange={(v) => set({ wind: v })} placeholder="38" />
              <Field label="Direction" value={fields.dir} onChange={(v) => set({ dir: v })} placeholder="SW" />
            </Row>
            <Row>
              <Field label="Sea · m" value={fields.sea} onChange={(v) => set({ sea: v })} placeholder="6.2" />
              <Field label="Day" value={fields.condDay} onChange={(v) => set({ condDay: v })} placeholder="Day 18" />
            </Row>
            <Row>
              <Field label="Position" value={fields.position} onChange={(v) => set({ position: v })} placeholder="46°S" />
              <Field label="Position 2" value={fields.position2} onChange={(v) => set({ position2: v })} placeholder="142°W" />
            </Row>
          </div>
        )}

        {has(template, 'hero') && (
          <div className="group">
            <div className="group-title">// Crew hero</div>
            <TeamSelect label="Team · accent" value={fields.heroTeam} onChange={(v) => set({ heroTeam: v })} />
            <Row>
              <FlagSelect label="Flag" value={fields.crewFlag} onChange={(v) => set({ crewFlag: v })} />
              <Field label="Name" value={fields.crewName} onChange={(v) => set({ crewName: v })} placeholder="Christin Schulz" />
            </Row>
            <Row>
              <Field label="Age" value={fields.crewAge} onChange={(v) => set({ crewAge: v })} placeholder="38" w={80} />
              <Field label="Role" value={fields.crewRole} onChange={(v) => set({ crewRole: v })} placeholder="Race Crew" />
            </Row>
            <Row>
              <Field label="On board" value={fields.crewBoat} onChange={(v) => set({ crewBoat: v })} placeholder="Tongyeong" />
              <Field label="Legs" value={fields.crewLegs} onChange={(v) => set({ crewLegs: v })} placeholder="Legs 2 & 3" />
            </Row>
            <Row>
              <Field label="Miles" value={fields.statMiles} onChange={(v) => set({ statMiles: v })} placeholder="4,250 nm" />
              <Field label="Top speed" value={fields.statSpeed} onChange={(v) => set({ statSpeed: v })} placeholder="24.1 kts" />
              <Field label="Days" value={fields.statDays} onChange={(v) => set({ statDays: v })} placeholder="31" />
            </Row>
            <label>
              <span className="lbl">Auto-remove background</span>
              <div className="ratio-row" style={{ gridTemplateColumns: '1fr 1fr' }}>
                <button className={`ratio-btn ${fields.autoCut ? 'active' : ''}`} onClick={() => set({ autoCut: true })}>On</button>
                <button className={`ratio-btn ${!fields.autoCut ? 'active' : ''}`} onClick={() => set({ autoCut: false })}>Off</button>
              </div>
            </label>
            <div className="hint">Drop <b>any</b> photo of the crew member on the preview — with auto-remove on, it's cut out in-browser. Drag/resize. Background is a branded gradient.</div>
            <div className="actions" style={{ marginTop: 6 }}>
              <button className="ratio-btn" style={{ flex: 1 }} onClick={() => {
                try { Object.keys(localStorage).filter((k) => k.indexOf('clip-cut-') === 0).forEach((k) => localStorage.removeItem(k)); } catch (e) {}
                setResetN((n) => n + 1);
              }}>Reset cut-out</button>
            </div>
          </div>
        )}

        {has(template, 'recruit') && (
          <div className="group">
            <div className="group-title">// Recruitment ad</div>
            <Field label="Eyebrow / offer" value={fields.recruitOffer} onChange={(v) => set({ recruitOffer: v })} placeholder="Applications open" />
            <Field label="Headline" value={fields.recruitHeadline} onChange={(v) => set({ recruitHeadline: v })} placeholder="Could you do it?" />
            <TextArea label="Subhead" value={fields.recruitSub} onChange={(v) => set({ recruitSub: v })} rows={2} placeholder="No experience needed…" />
            <Row>
              <Field label="CTA button" value={fields.recruitCta} onChange={(v) => set({ recruitCta: v })} placeholder="Apply now" />
              <Field label="URL" value={fields.recruitUrl} onChange={(v) => set({ recruitUrl: v })} placeholder="clipperroundtheworld.com" />
            </Row>
            <div className="hint">Tip: switch the ratio to <b>1.91:1</b> for a Facebook / LinkedIn link ad.</div>
          </div>
        )}

        {has(template, 'standings') && (
          <div className="group">
            <div className="group-title">// Standings</div>
            <TextArea label="key race gate sprint pen total" value={fields.standingsText}
              onChange={(v) => set({ standingsText: v })} rows={10} mono />
            <div className="hint">One team per line. Keys: {window.TEAMS.map((t) => t.key).join(', ')}.</div>
          </div>
        )}

        {(has(template, 'presenter') || has(template, 'partner')) && (
          <div className="group">
            <div className="group-title">// Partner</div>
            {has(template, 'presenter') && <Field label="Presented / powered by" value={fields.presenter} onChange={(v) => set({ presenter: v })} placeholder="Hyde Sails" />}
            {has(template, 'partner') && <Field label="Partner" value={fields.partner} onChange={(v) => set({ partner: v })} placeholder="Hambledon Vineyard" />}
          </div>
        )}

        {/* Export */}
        <div className="group">
          <div className="group-title">// Export</div>
          <div className="actions">
            <button className="btn" onClick={onExport} disabled={exporting || videoExporting}>
              {exporting ? 'Rendering…' : `${saveVerb} ${r.target.join(' × ')} JPEG`}
            </button>
          </div>
          <div className="actions" style={{ marginTop: 8 }}>
            <button className="btn" style={{ background: '#213f7c', borderColor: '#213f7c' }} onClick={onExportKit}
              disabled={exporting || videoExporting || kitBusy}>
              {kitBusy ? `Baking kit… ${kitLabel}` : `${saveVerb} kit · ${KIT_FORMATS.length} formats`}
            </button>
          </div>
          <div className="hint">Kit bakes 1:1 · 4:5 · 9:16 · 16:9 · 1.91:1 in one go.</div>
          {pendingShare && (
            <div className="actions" style={{ marginTop: 10 }}>
              <button className="btn" onClick={async () => {
                if (await shareFiles(pendingShare.files, pendingShare.title)) setPendingShare(null);
                else await downloadAll(pendingShare.files);
              }}>Save {pendingShare.label} to device</button>
            </div>
          )}
          {fields.mediaKind === 'video' && (
            <>
              <div style={{ marginTop: 14 }}>
                <div className="group-title" style={{ marginBottom: 8 }}>// Video playback</div>
                <window.VideoControls getPreviewNode={getPreviewNode} />
              </div>
              <div className="actions" style={{ marginTop: 14 }}>
                <button className="btn" onClick={onExportVideo} disabled={exporting || videoExporting}>
                  {videoExporting ? `Rendering… ${Math.round(videoProgress.frac * 100)}%` : 'Export final video (≤ 90s)'}
                </button>
              </div>
              {videoExporting && (
                <>
                  <div className="progress-track"><div className="progress-fill" style={{ width: `${Math.round(videoProgress.frac * 100)}%` }} /></div>
                  <div className="progress-label">{videoProgress.label}</div>
                </>
              )}
              <div className="hint" style={{ marginTop: 10 }}>Offline frame-accurate bake → MP4 (WebM fallback). Intro graphics animate in.</div>
            </>
          )}
        </div>
      </aside>

      <main className="stage">
        <div className="stage-inner" style={{ width: r.w, height: r.h }}>
          <div ref={previewRef} className="stage-frame" data-app-name="Clipper" style={{ width: r.w, height: r.h }}>
            <Component key={`${template}-${ratio}-${resetN}`} w={r.w} h={r.h} slotId={slotId} data={data} />
          </div>
          <div className="stage-meta">
            <span>// {name}</span>
            <span><span className="accent">{ratio}</span> · {r.target.join(' × ')} px · drop a {fields.mediaKind === 'video' ? 'video' : 'photo'}</span>
          </div>
        </div>
      </main>
    </div>
  );
}

ReactDOM.createRoot(document.getElementById('root')).render(<App />);
