// templates.jsx — Clipper Round the World post templates.
//
// Every template takes the same props: { w, h, slotId, data }. Sizes are in `u`
// units (u = min(w,h)/100) so a template lays out identically at 1:1, 4:5, 9:16
// and 16:9. Photos/video go through <Media> (image-slot for stills, <MediaSlot>
// for video) — and <Media> is always the FIRST child (the background), with all
// brand overlays layered ON TOP, so the export compositor punches the photo
// through cleanly. These are built to match the Clipper Instagram post system.

const RED = '#b1001e', REDBR = '#e0102a', NAVY = '#213f7c', BLUE = '#004c90',
      CYAN = '#009eeb', BLACK = '#0f0b0c', MIST = '#98a4ac', WHITE = '#ffffff';
const DISP = 'var(--font-display)';

const U = (w, h) => Math.min(w, h) / 100;
const rankSuffix = (n) => (n === 1 ? 'ST' : n === 2 ? 'ND' : n === 3 ? 'RD' : 'TH');

// ── Movable / resizable cut-out subject layer ──────────────────────────────
// useDraggable — drag-to-reposition, stored as % of the frame (ported from the
// 16°S builder) so a position survives ratio switches + renders identically at
// export resolution.
function useDraggable(storageKey, defaultPos = { x: 50, y: 60 }) {
  const [pos, setPos] = React.useState(() => {
    if (!storageKey || typeof localStorage === 'undefined') return defaultPos;
    try { const v = localStorage.getItem(storageKey); if (v) return JSON.parse(v); } catch (e) {}
    return defaultPos;
  });
  React.useEffect(() => {
    if (!storageKey) return;
    try { localStorage.setItem(storageKey, JSON.stringify(pos)); } catch (e) {}
  }, [pos, storageKey]);
  React.useEffect(() => {
    if (!storageKey) return;
    try { const v = localStorage.getItem(storageKey); setPos(v ? JSON.parse(v) : defaultPos); } catch (e) { setPos(defaultPos); }
  }, [storageKey]);
  const [dragging, setDragging] = React.useState(false);
  const onPointerDown = (e) => {
    if (e.button != null && e.button !== 0) return;
    e.preventDefault(); e.stopPropagation();
    const handle = e.currentTarget;
    const container = handle.offsetParent || handle.parentElement;
    if (!container) return;
    const cr = container.getBoundingClientRect();
    const hr = handle.getBoundingClientRect();
    const offX = e.clientX - (hr.left + hr.width / 2);
    const offY = e.clientY - (hr.top + hr.height / 2);
    setDragging(true);
    const onMove = (me) => {
      const cx = me.clientX - offX - cr.left, cy = me.clientY - offY - cr.top;
      const halfW = (hr.width / 2) / cr.width * 100, halfH = (hr.height / 2) / cr.height * 100;
      const xPct = cx / cr.width * 100, yPct = cy / cr.height * 100;
      setPos({ x: Math.max(halfW - 20, Math.min(120 - halfW, xPct)), y: Math.max(halfH - 20, Math.min(120 - halfH, yPct)) });
    };
    const onUp = () => { setDragging(false); window.removeEventListener('pointermove', onMove); window.removeEventListener('pointerup', onUp); };
    window.addEventListener('pointermove', onMove); window.addEventListener('pointerup', onUp);
  };
  return { pos, dragging, onPointerDown };
}
function ratioKey(w, h) {
  const r = w / h;
  if (Math.abs(r - 1) < 0.05) return '1x1';
  if (Math.abs(r - 4 / 5) < 0.05) return '4x5';
  if (Math.abs(r - 9 / 16) < 0.05) return '9x16';
  if (Math.abs(r - 16 / 9) < 0.05) return '16x9';
  return r > 1 ? 'wide' : 'tall';
}
window.useDraggable = useDraggable; window.ratioKey = ratioKey;

// Downscale a dropped image to a data URL, PRESERVING ALPHA (webp → png) so a
// transparent cut-out exports cleanly.
async function loadCutout(file, cap = 1200) {
  const bmp = await createImageBitmap(file);
  try {
    const s = Math.min(1, cap / Math.max(bmp.width, bmp.height));
    const cw = Math.max(1, Math.round(bmp.width * s)), ch = Math.max(1, Math.round(bmp.height * s));
    const c = document.createElement('canvas'); c.width = cw; c.height = ch;
    c.getContext('2d').drawImage(bmp, 0, 0, cw, ch);
    const webp = c.toDataURL('image/webp', 0.92);
    return webp.indexOf('data:image/webp') === 0 ? webp : c.toDataURL('image/png');
  } finally { bmp.close && bmp.close(); }
}

// ── Background removal — MediaPipe Selfie Segmentation (Apache-2.0) ─────────
// Fully in-browser (no API key, no server, images never leave the device).
// Lazy-loaded on first use. `importESM` hides the dynamic import() from the
// in-browser Babel transform. Falls back to the original image on any failure.
const importESM = (u) => (new Function('u', 'return import(u)'))(u);
let _segPromise = null;
function getSegmenter() {
  if (_segPromise) return _segPromise;
  const V = '0.10.14';
  _segPromise = (async () => {
    const vision = await importESM(`https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@${V}`);
    const fileset = await vision.FilesetResolver.forVisionTasks(`https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@${V}/wasm`);
    return vision.ImageSegmenter.createFromOptions(fileset, {
      baseOptions: { modelAssetPath: 'https://storage.googleapis.com/mediapipe-models/image_segmenter/selfie_segmenter/float16/latest/selfie_segmenter.tflite' },
      runningMode: 'IMAGE', outputCategoryMask: false, outputConfidenceMasks: true,
    });
  })().catch((e) => { _segPromise = null; throw e; });
  return _segPromise;
}
async function removeBg(file) {
  const seg = await getSegmenter();
  const bmp = await createImageBitmap(file);
  try {
    const cap = 1400;
    const s = Math.min(1, cap / Math.max(bmp.width, bmp.height));
    const w = Math.max(1, Math.round(bmp.width * s)), h = Math.max(1, Math.round(bmp.height * s));
    const cv = document.createElement('canvas'); cv.width = w; cv.height = h;
    const ctx = cv.getContext('2d', { willReadFrequently: true });
    ctx.drawImage(bmp, 0, 0, w, h);
    const res = seg.segment(cv);
    const mask = res && res.confidenceMasks && res.confidenceMasks[0];
    if (!mask) throw new Error('no segmentation mask');
    const mw = mask.width || w, mh = mask.height || h;
    const md = mask.getAsFloat32Array();
    // Person probability → alpha. destination-in keeps only the masked photo
    // pixels (drawImage scales the mask to the photo, giving soft edges).
    const mc = document.createElement('canvas'); mc.width = mw; mc.height = mh;
    const mctx = mc.getContext('2d');
    const mi = mctx.createImageData(mw, mh);
    for (let i = 0; i < mw * mh; i++) {
      const a = Math.round(Math.max(0, Math.min(1, md[i])) * 255);
      mi.data[i * 4] = mi.data[i * 4 + 1] = mi.data[i * 4 + 2] = 255; mi.data[i * 4 + 3] = a;
    }
    mctx.putImageData(mi, 0, 0);
    ctx.globalCompositeOperation = 'destination-in';
    ctx.drawImage(mc, 0, 0, w, h);
    ctx.globalCompositeOperation = 'source-over';
    try { mask.close && mask.close(); } catch (e) {}
    try { res.close && res.close(); } catch (e) {}
    return await new Promise((r) => cv.toBlob(r, 'image/png'));
  } finally { bmp.close && bmp.close(); }
}

// CutoutSlot — one draggable + resizable transparent PNG over the background.
// Empty = a dashed drop target (data-no-export, so it never bakes); filled = the
// cut-out (object-fit: contain, bottom-anchored so the figure "stands").
function CutoutSlot({ id, w, h, defaultPos, defaultScale = 1, baseWu = 26, baseHu = 44, autoCut = false }) {
  const u = U(w, h);
  const rk = ratioKey(w, h);
  const drag = useDraggable(`clip-cut-${id}-pos-${rk}`, defaultPos);
  const resize = window.useResizable(`clip-cut-${id}-scale-${rk}`, defaultScale, 0.3, 3);
  const imgKey = `clip-cut-${id}-img`;
  const [url, setUrl] = React.useState(() => { try { return localStorage.getItem(imgKey) || null; } catch (e) { return null; } });
  const [over, setOver] = React.useState(false);
  const [busy, setBusy] = React.useState('');
  const inputRef = React.useRef(null);
  const ingest = async (file) => {
    if (!file || !/^image\/(png|webp|avif|jpeg)$/.test(file.type)) return;
    setBusy(autoCut ? 'Removing background…' : 'Loading…');
    try {
      let src = file;
      if (autoCut) { try { src = await removeBg(file); } catch (e) { console.warn('bg removal failed; using original', e); src = file; } }
      const d = await loadCutout(src);
      setUrl(d); try { localStorage.setItem(imgKey, d); } catch (e) {}
    } catch (e) { console.warn(e); } finally { setBusy(''); }
  };
  const clear = (e) => { e.stopPropagation(); setUrl(null); try { localStorage.removeItem(imgKey); } catch (er) {} };
  return (
    <div data-resizable-card
      onPointerDown={url ? drag.onPointerDown : undefined}
      onClick={url ? undefined : () => inputRef.current && inputRef.current.click()}
      onDragEnter={(e) => { e.preventDefault(); setOver(true); }}
      onDragOver={(e) => e.preventDefault()}
      onDragLeave={() => setOver(false)}
      onDrop={(e) => { e.preventDefault(); setOver(false); const f = e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files[0]; if (f) ingest(f); }}
      style={{ position: 'absolute', left: `${drag.pos.x}%`, top: `${drag.pos.y}%`, width: baseWu * u, height: baseHu * u,
        transform: `translate(-50%,-50%) scale(${resize.scale})`, transformOrigin: 'center center', pointerEvents: 'auto',
        touchAction: 'none', cursor: url ? (drag.dragging ? 'grabbing' : 'grab') : 'pointer',
        outline: (drag.dragging || resize.resizing || over) ? '1px dashed rgba(255,255,255,0.85)' : 'none',
        zIndex: url ? undefined : 4 }}>
      <input ref={inputRef} type="file" accept="image/png,image/webp,image/avif,image/jpeg" style={{ display: 'none' }}
        onChange={(e) => { const f = e.target.files && e.target.files[0]; if (f) ingest(f); e.target.value = ''; }} />
      {url ? (
        <img src={url} alt="" draggable="false" style={{ width: '100%', height: '100%', objectFit: 'contain',
          objectPosition: 'center bottom', display: 'block', pointerEvents: 'none', filter: 'drop-shadow(0 6px 18px rgba(0,0,0,0.5))' }} />
      ) : (
        <div data-no-export="1" style={{ position: 'absolute', inset: 0, border: '1.5px dashed rgba(255,255,255,0.5)',
          borderRadius: 0.8 * u, background: 'rgba(15,11,12,0.24)', display: 'flex', flexDirection: 'column',
          alignItems: 'center', justifyContent: 'flex-end', gap: 0.6 * u, padding: 1.4 * u, textAlign: 'center',
          color: 'rgba(255,255,255,0.74)', fontFamily: 'var(--font-brand)', fontWeight: 700, fontSize: 1.25 * u,
          letterSpacing: '0.1em', textTransform: 'uppercase' }}>
          <svg width={4 * u} height={4 * u} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.4" style={{ opacity: 0.7 }}>
            <circle cx="12" cy="8" r="4" /><path d="M4 21c0-4.5 4-6.5 8-6.5s8 2 8 6.5" />
          </svg>
          Drop cut-out PNG
        </div>
      )}
      {url && (
        <>
          <window.ResizeHandle onPointerDown={resize.onPointerDown} color="#fff" bg="rgba(15,11,12,0.6)" />
          <button data-no-export="1" onPointerDown={(e) => e.stopPropagation()} onClick={clear} title="Remove cut-out"
            style={{ position: 'absolute', top: -0.6 * u, right: -0.6 * u, width: 3 * u, height: 3 * u, borderRadius: '50%',
              border: 'none', background: 'rgba(15,11,12,0.82)', color: '#fff', cursor: 'pointer', fontSize: 1.9 * u, lineHeight: 1, padding: 0 }}>×</button>
        </>
      )}
      {busy && (
        <div data-no-export="1" style={{ position: 'absolute', inset: 0, display: 'flex', flexDirection: 'column',
          alignItems: 'center', justifyContent: 'center', gap: 0.9 * u, background: 'rgba(15,11,12,0.68)', borderRadius: 0.8 * u,
          color: '#fff', fontFamily: 'var(--font-brand)', fontWeight: 700, fontSize: 1.2 * u, letterSpacing: '0.08em',
          textTransform: 'uppercase', textAlign: 'center', padding: 1 * u, zIndex: 6 }}>
          <div style={{ width: 3.6 * u, height: 3.6 * u, borderRadius: '50%', border: `${0.5 * u}px solid rgba(255,255,255,0.3)`,
            borderTopColor: '#fff', animation: 'clipspin 0.8s linear infinite' }} />
          {busy}
        </div>
      )}
    </div>
  );
}

// SubjectLayer — N cut-out slots spread along a baseline. The wrapper is
// click-through (pointerEvents:none); only the slots capture, and the chrome
// rendered after it is also click-through, so a filled figure sits *behind* the
// podium cards yet stays draggable.
function SubjectLayer({ slotId, w, h, count = 2, autoCut = false }) {
  const n = Math.max(0, Math.min(6, Number(count) || 0));
  if (!n) return null;
  const xs = n === 1 ? [50] : Array.from({ length: n }, (_, i) => 18 + i * (64 / (n - 1)));
  return (
    <div style={{ position: 'absolute', inset: 0, pointerEvents: 'none' }}>
      {xs.map((x, i) => <CutoutSlot key={i} id={`${slotId}-${i}`} w={w} h={h} defaultPos={{ x, y: 58 }} autoCut={autoCut} />)}
    </div>
  );
}

// ── CLIPPER lockup — the official 25-26 horizontal logo (do not substitute) ──
function ClipperLogo({ h = 34 }) {
  return (
    <img src="assets/clipper-logo.png" alt="Clipper Round the World 2025-26" draggable="false"
      style={{ height: h, width: 'auto', display: 'block', filter: 'drop-shadow(0 1px 3px rgba(0,0,0,0.35))' }} />
  );
}

// "STAGE 10: WARRANT'S WEST COAST CHALLENGE" — stage no. accented.
function StageStrap({ text, size = 20, color = WHITE, accent = REDBR, align = 'center' }) {
  if (!text) return null;
  const m = String(text).match(/^(stage\s*\d+\s*:?)(.*)$/i);
  return (
    <div style={{ fontFamily: DISP, fontWeight: 800, fontStyle: 'italic', textTransform: 'uppercase',
      fontSize: size, letterSpacing: '0.01em', lineHeight: 1.06, color, textAlign: align }}>
      {m ? <><span style={{ color: accent }}>{m[1]}</span>{m[2]}</> : text}
    </div>
  );
}

function Scrim({ dir = 'to top', from = 'rgba(15,11,12,0)', mid, to = 'rgba(15,11,12,0.9)', style }) {
  const stops = mid ? `${from}, ${mid}, ${to}` : `${from}, ${to}`;
  return <div style={{ position: 'absolute', inset: 0, background: `linear-gradient(${dir}, ${stops})`, pointerEvents: 'none', ...style }} />;
}

// Faint survey grid the Clipper posts lay over photography.
function GridOverlay({ opacity = 0.07, cells = 8 }) {
  const s = 100 / cells;
  return <div style={{ position: 'absolute', inset: 0, pointerEvents: 'none',
    backgroundImage: `linear-gradient(rgba(255,255,255,${opacity}) 1px,transparent 1px),linear-gradient(90deg,rgba(255,255,255,${opacity}) 1px,transparent 1px)`,
    backgroundSize: `${s}% ${s}%` }} />;
}

// Side-profile racing yacht silhouette in a team colour (Scoring Gate rows).
function BoatGlyph({ colour = NAVY, size = 26 }) {
  return (
    <svg width={size * 1.7} height={size} viewBox="0 0 68 40" style={{ flex: '0 0 auto', display: 'block' }} aria-hidden="true">
      <path d="M2 29 L64 29 L55 38 L11 38 Z" fill={colour} />
      <path d="M35 3 L35 27 L13 27 Z" fill={colour} />
      <path d="M37 8 L37 27 L55 27 Z" fill={colour} opacity="0.78" />
    </svg>
  );
}

function FlagCircle({ flag, size }) {
  return (
    <div style={{ width: size, height: size, borderRadius: '50%', flex: '0 0 auto', overflow: 'hidden',
      display: 'flex', alignItems: 'center', justifyContent: 'center', background: 'rgba(255,255,255,0.14)',
      boxShadow: '0 0 0 2px rgba(255,255,255,0.85)', fontSize: size * 0.7, lineHeight: 1 }}>{flag}</div>
  );
}

// Photo OR video drop — always the background layer.
function Media({ slotId, data, fit = 'cover', placeholder = 'Drop a photo or video', style }) {
  if (data && data.mediaKind === 'video') {
    return <window.MediaSlot id={slotId} fit={fit} placeholder={placeholder} style={style} />;
  }
  return (
    <image-slot id={slotId} fit={fit} placeholder={placeholder}
      style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', display: 'block', ...style }} />
  );
}

// White time pill with a coloured label tab (GAP / FINISH TIME).
function TimePill({ u, label, labelBg = RED, children, big }) {
  return (
    <div style={{ display: 'inline-flex', flexDirection: 'column', alignItems: 'center' }}>
      {label && (
        <div style={{ background: labelBg, color: '#fff', fontFamily: DISP, fontWeight: 800, fontStyle: 'italic',
          fontSize: 1.5 * u, letterSpacing: '0.06em', textTransform: 'uppercase', padding: `${0.3 * u}px ${1 * u}px`,
          transform: `translateY(${0.5 * u}px)`, zIndex: 1, borderRadius: 0.3 * u }}>{label}</div>
      )}
      <div style={{ background: '#fff', color: BLACK, fontFamily: DISP, fontWeight: 900, fontStyle: 'italic',
        fontSize: (big ? 2.9 : 2.4) * u, letterSpacing: '-0.01em', textAlign: 'center',
        padding: `${0.7 * u}px ${1.1 * u}px`, borderRadius: 0.5 * u, whiteSpace: 'nowrap',
        boxShadow: '0 4px 14px rgba(0,0,0,0.3)' }}>{children}</div>
    </div>
  );
}

// ════════════════════════════════════════════════════════════════════════════
// 1 · RACE RESULTS — crew hero + overlapping team-colour podium blocks
// ════════════════════════════════════════════════════════════════════════════
function PodiumBlocks({ u, entries }) {
  const byRank = {}; entries.forEach((e) => { byRank[e.rank] = e; });
  const order = [byRank[2], byRank[1], byRank[3]].filter(Boolean);
  const ht = { 1: 19, 2: 14.5, 3: 12.5 };
  return (
    <div style={{ display: 'flex', alignItems: 'flex-end', justifyContent: 'center', gap: 0.5 * u, width: '100%' }}>
      {order.map((e) => {
        const t = window.getTeam(e.key); const first = e.rank === 1;
        return (
          <div key={e.rank} style={{ flex: first ? 1.32 : 1, height: ht[e.rank] * u, background: t.colour,
            color: t.textOn, borderRadius: `${0.9 * u}px ${0.9 * u}px 0 0`, position: 'relative', display: 'flex',
            flexDirection: 'column', alignItems: 'center', padding: `${1.1 * u}px ${0.7 * u}px ${0.8 * u}px`,
            boxShadow: '0 -2px 22px rgba(0,0,0,0.32)' }}>
            <window.TeamBadge teamKey={e.key} size={(first ? 5 : 4.2) * u} radius={0.5 * u} />
            <div style={{ marginTop: 0.5 * u, fontFamily: DISP, fontWeight: 800, fontStyle: 'italic',
              fontSize: 1.4 * u, lineHeight: 0.98, letterSpacing: '-0.01em', textTransform: 'uppercase',
              textAlign: 'center', color: t.textOn, opacity: 0.96 }}>{t.name}</div>
            <div style={{ marginTop: 'auto', fontFamily: DISP, fontWeight: 900, fontStyle: 'italic',
              fontSize: (first ? 8 : 6) * u, lineHeight: 0.82, color: t.textOn }}>
              {e.rank}<span style={{ fontSize: (first ? 3 : 2.4) * u }}>{rankSuffix(e.rank)}</span></div>
          </div>
        );
      })}
    </div>
  );
}

function TplRaceResults({ w, h, slotId, data }) {
  const u = U(w, h);
  return (
    <div style={{ position: 'relative', width: w, height: h, overflow: 'hidden', background: BLACK }}>
      <Media slotId={slotId} data={data} placeholder="Drop the winning crew / boat photo" />
      <Scrim dir="to top" from="rgba(15,11,12,0.12)" mid="rgba(15,11,12,0.4)" to="rgba(15,11,12,0.93)" />
      <div style={{ position: 'absolute', inset: 0, padding: `${4 * u}px ${4 * u}px ${3.5 * u}px`, display: 'flex', flexDirection: 'column' }}>
        <div style={{ flex: 1 }} />
        <PodiumBlocks u={u} entries={data.entries || []} />
        <div style={{ marginTop: 2.6 * u, textAlign: 'center' }}>
          <StageStrap text={data.stage} size={2.2 * u} />
          <div style={{ fontFamily: DISP, fontWeight: 900, fontStyle: 'italic', fontSize: 9.5 * u, lineHeight: 0.88,
            color: '#fff', textTransform: 'uppercase', letterSpacing: '-0.025em', marginTop: 0.4 * u,
            textShadow: '0 3px 20px rgba(0,0,0,0.5)' }}>Race Results</div>
        </div>
        <div style={{ marginTop: 2.2 * u, display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 1.8 * u }}>
          <ClipperLogo h={4 * u} />
          {data.partner && (
            <>
              <div style={{ width: 1, height: 3.6 * u, background: 'rgba(255,255,255,0.4)' }} />
              <div style={{ color: '#fff', fontFamily: DISP, fontWeight: 800, fontStyle: 'italic', fontSize: 2.4 * u,
                textTransform: 'uppercase', letterSpacing: '0.01em' }}>{data.partner}</div>
            </>
          )}
        </div>
        {data.partner && (
          <div style={{ marginTop: 0.8 * u, textAlign: 'center', color: MIST, fontFamily: 'var(--font-brand)',
            fontWeight: 700, fontSize: 1.3 * u, letterSpacing: '0.2em', textTransform: 'uppercase' }}>Official Celebration Partner</div>
        )}
      </div>
    </div>
  );
}

// ════════════════════════════════════════════════════════════════════════════
// 2 · OCEAN SPRINT — hero + team-colour podium cards + times/points + footer
// ════════════════════════════════════════════════════════════════════════════
function TplOceanSprint({ w, h, slotId, data }) {
  const u = U(w, h);
  const entries = data.entries || [];
  const byRank = {}; entries.forEach((e) => { byRank[e.rank] = e; });
  const order = [byRank[2], byRank[1], byRank[3]].filter(Boolean);
  return (
    <div style={{ position: 'relative', width: w, height: h, overflow: 'hidden', background: BLACK }}>
      <Media slotId={slotId} data={data} placeholder="Drop a fleet / crew photo" />
      <Scrim dir="to top" from="rgba(15,11,12,0.3)" mid="rgba(15,11,12,0.45)" to="rgba(15,11,12,0.8)" />
      {/* drop-in, drag-to-move, corner-resize crew cut-outs (transparent PNGs) */}
      <SubjectLayer slotId={`${slotId}-sub`} w={w} h={h} count={data.cutouts} autoCut={data.autoCut} />
      {/* header */}
      <div style={{ position: 'absolute', top: 3.4 * u, left: 0, right: 0, textAlign: 'center', pointerEvents: 'none' }}>
        <div style={{ fontFamily: DISP, fontWeight: 900, fontStyle: 'italic', fontSize: 6.6 * u, lineHeight: 0.86,
          color: '#fff', textTransform: 'uppercase', letterSpacing: '-0.02em', textShadow: '0 2px 14px rgba(0,0,0,0.6)' }}>
          Ocean<br />Sprint</div>
        {data.presenter && (
          <div style={{ marginTop: 0.5 * u, color: '#fff', fontFamily: 'var(--font-brand)', fontWeight: 700,
            fontSize: 1.5 * u, letterSpacing: '0.18em', textTransform: 'uppercase', opacity: 0.9 }}>
            Powered by {data.presenter}</div>
        )}
      </div>
      {/* podium cards + times/points/elapsed columns */}
      <div style={{ position: 'absolute', left: 3.5 * u, right: 3.5 * u, top: 24 * u, bottom: 16 * u,
        display: 'flex', alignItems: 'flex-end', justifyContent: 'center', gap: 1.2 * u, pointerEvents: 'none' }}>
        {order.map((e) => {
          const t = window.getTeam(e.key); const first = e.rank === 1;
          return (
            <div key={e.rank} style={{ flex: first ? 1.25 : 1, display: 'flex', flexDirection: 'column',
              alignItems: 'center', gap: 0.7 * u }}>
              {/* card */}
              <div style={{ width: '100%', background: t.colour, color: t.textOn, borderRadius: 0.9 * u,
                padding: `${0.9 * u}px ${0.7 * u}px ${1 * u}px`, textAlign: 'center', boxShadow: '0 8px 22px rgba(0,0,0,0.35)' }}>
                <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 0.6 * u }}>
                  <window.TeamBadge teamKey={e.key} size={3.2 * u} radius={0.4 * u} />
                  <div style={{ fontFamily: DISP, fontWeight: 800, fontStyle: 'italic', fontSize: 1.5 * u,
                    lineHeight: 0.95, textTransform: 'uppercase', textAlign: 'left', letterSpacing: '-0.01em' }}>{t.name}</div>
                </div>
                <div style={{ fontFamily: DISP, fontWeight: 900, fontStyle: 'italic', fontSize: (first ? 8 : 6.4) * u,
                  lineHeight: 0.9, marginTop: 0.3 * u }}>
                  {e.rank}<span style={{ fontSize: (first ? 3 : 2.4) * u }}>{rankSuffix(e.rank)}</span></div>
              </div>
              {e.line1 && <TimePill u={u} big={first} label={first ? 'Finish Time' : 'Gap'} labelBg={first ? RED : '#3f4344'}>{e.line1}</TimePill>}
              {e.pts != null && e.pts !== '' && (
                <div style={{ fontFamily: DISP, fontWeight: 900, fontStyle: 'italic', color: '#fff',
                  fontSize: (first ? 3.4 : 2.8) * u, textTransform: 'uppercase', letterSpacing: '-0.01em',
                  textShadow: '0 2px 10px rgba(0,0,0,0.5)' }}>{e.pts} {String(e.pts).replace(/[^0-9]/g, '') === '1' ? 'Point' : 'Points'}</div>
              )}
              {e.line2 && (
                <div style={{ display: 'flex', alignItems: 'center', gap: 0.5 * u, background: 'rgba(15,11,12,0.7)',
                  borderRadius: 0.4 * u, padding: `${0.3 * u}px ${0.7 * u}px` }}>
                  <span style={{ color: MIST, fontFamily: 'var(--font-brand)', fontWeight: 700, fontSize: 1.1 * u,
                    letterSpacing: '0.1em' }}>ELAPSED</span>
                  <span style={{ color: '#fff', fontFamily: DISP, fontWeight: 800, fontStyle: 'italic', fontSize: 1.4 * u }}>{e.line2}</span>
                </div>
              )}
            </div>
          );
        })}
      </div>
      {/* black footer band */}
      <div style={{ position: 'absolute', left: 0, right: 0, bottom: 0, height: 15 * u, background: BLACK,
        display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 1.4 * u, padding: `0 ${4 * u}px`, pointerEvents: 'none' }}>
        <StageStrap text={data.stage} size={3 * u} />
        <ClipperLogo h={3.8 * u} />
      </div>
    </div>
  );
}

// ════════════════════════════════════════════════════════════════════════════
// 3 · SCORING GATE — full-bleed hero + grid + ranked rows with boat + pts
// ════════════════════════════════════════════════════════════════════════════
function TplScoringGate({ w, h, slotId, data }) {
  const u = U(w, h);
  const entries = (data.entries || []).slice().sort((a, b) => a.rank - b.rank);
  return (
    <div style={{ position: 'relative', width: w, height: h, overflow: 'hidden', background: BLACK }}>
      <Media slotId={slotId} data={data} placeholder="Drop a boat photo" />
      <GridOverlay opacity={0.08} cells={8} />
      <Scrim dir="to bottom" from="rgba(0,40,80,0.55)" mid="rgba(0,40,80,0.1)" to="rgba(0,30,60,0.75)" />
      <div style={{ position: 'absolute', top: 4 * u, left: 0, right: 0, display: 'flex', justifyContent: 'center' }}>
        <ClipperLogo h={4 * u} />
      </div>
      {/* title with wave */}
      <div style={{ position: 'absolute', top: 12 * u, left: 0, right: 0, textAlign: 'center' }}>
        <div style={{ position: 'relative', display: 'inline-block' }}>
          <div style={{ fontFamily: DISP, fontWeight: 900, fontStyle: 'italic', fontSize: 11 * u, lineHeight: 0.9,
            color: '#fff', textTransform: 'uppercase', letterSpacing: '-0.03em', textShadow: '0 3px 18px rgba(0,0,0,0.4)' }}>
            Scoring Gate</div>
          <svg width="100%" height={3.4 * u} viewBox="0 0 100 8" preserveAspectRatio="none"
            style={{ position: 'absolute', left: 0, bottom: -1 * u }}>
            <path d="M0 5 Q 12 1 25 5 T 50 5 T 75 5 T 100 5" stroke={CYAN} strokeWidth="2.6" fill="none" />
            <path d="M0 7 Q 12 3 25 7 T 50 7 T 75 7 T 100 7" stroke={CYAN} strokeWidth="1.6" fill="none" opacity="0.6" />
          </svg>
        </div>
      </div>
      {/* ranked rows */}
      <div style={{ position: 'absolute', left: 4.5 * u, right: 4.5 * u, bottom: 10 * u, display: 'flex',
        flexDirection: 'column', gap: 1.3 * u }}>
        {entries.map((e) => {
          const t = window.getTeam(e.key);
          return (
            <div key={e.rank} style={{ display: 'flex', alignItems: 'center', gap: 1.3 * u, background: '#fff',
              borderRadius: 1 * u, padding: `${0.9 * u}px ${1.2 * u}px`, boxShadow: '0 6px 18px rgba(0,0,0,0.3)' }}>
              <div style={{ width: 5 * u, height: 5 * u, borderRadius: 0.8 * u, background: '#eef0f2', color: NAVY,
                display: 'flex', alignItems: 'center', justifyContent: 'center', fontFamily: DISP, fontWeight: 900,
                fontStyle: 'italic', fontSize: 3.4 * u, flex: '0 0 auto' }}>{e.rank}</div>
              <window.TeamBadge teamKey={e.key} size={5 * u} radius={0.8 * u} />
              <div style={{ flex: 1, fontFamily: DISP, fontWeight: 800, fontStyle: 'italic', fontSize: 2.7 * u,
                color: BLACK, textTransform: 'uppercase', letterSpacing: '-0.01em', whiteSpace: 'nowrap',
                overflow: 'hidden', textOverflow: 'ellipsis' }}>{t.name}</div>
              <BoatGlyph colour={t.colour} size={3.2 * u} />
              <div style={{ background: '#dfe3e6', color: NAVY, fontFamily: DISP, fontWeight: 900, fontStyle: 'italic',
                fontSize: 3 * u, padding: `${0.4 * u}px ${1.1 * u}px`, borderRadius: 0.6 * u, flex: '0 0 auto' }}>{e.pts}pt</div>
            </div>
          );
        })}
      </div>
      <div style={{ position: 'absolute', left: 0, right: 0, bottom: 3.5 * u, textAlign: 'center' }}>
        <StageStrap text={data.stage} size={2.1 * u} color="#fff" accent={CYAN} />
        {data.presenter && (
          <div style={{ color: 'rgba(255,255,255,0.85)', fontFamily: 'var(--font-brand)', fontWeight: 600,
            fontSize: 1.4 * u, letterSpacing: '0.14em', textTransform: 'uppercase', marginTop: 0.5 * u }}>Presented by {data.presenter}</div>
        )}
      </div>
    </div>
  );
}

// ════════════════════════════════════════════════════════════════════════════
// 4 · OVERALL STANDINGS — full-fleet table, per-stat boxes, red total
// ════════════════════════════════════════════════════════════════════════════
function TplStandings({ w, h, slotId, data }) {
  const u = U(w, h);
  const rows = data.standings || [];
  const cols = [['race', 'Race'], ['gate', 'Gate'], ['sprint', 'Sprint'], ['pen', 'Pen']];
  const colW = 6.4 * u, totW = 8 * u;
  return (
    <div style={{ position: 'relative', width: w, height: h, overflow: 'hidden',
      background: `radial-gradient(130% 90% at 50% -10%, #2a0e16 0%, ${BLACK} 55%)` }}>
      <GridOverlay opacity={0.04} cells={9} />
      <div style={{ position: 'absolute', inset: 0, padding: `${3.5 * u}px ${3.5 * u}px ${3 * u}px`, display: 'flex', flexDirection: 'column' }}>
        <div style={{ fontFamily: DISP, fontWeight: 900, fontStyle: 'italic', fontSize: 7 * u, lineHeight: 0.92,
          color: '#fff', textTransform: 'uppercase', letterSpacing: '-0.03em' }}>Overall Standings</div>
        <div style={{ marginTop: 0.3 * u, marginBottom: 1.4 * u }}><StageStrap text={data.stage} size={2 * u} align="left" /></div>
        {/* header row: logo + column labels */}
        <div style={{ display: 'flex', alignItems: 'flex-end', gap: 0.8 * u, paddingBottom: 0.7 * u,
          borderBottom: '1px solid rgba(255,255,255,0.18)', color: MIST, fontFamily: 'var(--font-brand)',
          fontWeight: 700, fontSize: 1.15 * u, letterSpacing: '0.06em', textTransform: 'uppercase', lineHeight: 1.05 }}>
          <div style={{ width: 3 * u, flex: '0 0 auto' }} />
          <div style={{ flex: 1 }}><ClipperLogo h={2.8 * u} /></div>
          {cols.map(([k, lbl]) => <div key={k} style={{ width: colW, flex: '0 0 auto', textAlign: 'center' }}>{lbl}<br />Points</div>)}
          <div style={{ width: totW, flex: '0 0 auto', textAlign: 'center', color: '#fff' }}>Overall<br />Points</div>
        </div>
        {/* rows */}
        <div style={{ flex: 1, display: 'flex', flexDirection: 'column', justifyContent: 'space-between', padding: `${0.8 * u}px 0` }}>
          {rows.map((row, i) => {
            const t = window.getTeam(row.key);
            return (
              <div key={row.key + i} style={{ display: 'flex', alignItems: 'center', gap: 0.8 * u }}>
                <div style={{ width: 3 * u, flex: '0 0 auto', fontFamily: DISP, fontWeight: 900, fontStyle: 'italic',
                  fontSize: 2.6 * u, color: '#fff', textAlign: 'center' }}>{i + 1}</div>
                <window.TeamBadge teamKey={row.key} size={3.8 * u} radius={0.6 * u} />
                <div style={{ flex: 1, minWidth: 0, fontFamily: DISP, fontWeight: 800, fontStyle: 'italic',
                  fontSize: 1.85 * u, lineHeight: 0.95, color: '#fff', textTransform: 'uppercase', letterSpacing: '-0.01em' }}>
                  {t.full || t.name}</div>
                {cols.map(([k]) => (
                  <div key={k} style={{ width: colW, flex: '0 0 auto', display: 'flex', justifyContent: 'center' }}>
                    <span style={{ minWidth: 4.6 * u, textAlign: 'center', background: 'rgba(255,255,255,0.07)',
                      border: '1px solid rgba(255,255,255,0.12)', borderRadius: 0.4 * u, padding: `${0.35 * u}px ${0.5 * u}px`,
                      fontFamily: DISP, fontWeight: 800, fontSize: 2 * u, color: k === 'pen' ? MIST : '#fff' }}>
                      {row[k] != null ? row[k] : '—'}</span>
                  </div>
                ))}
                <div style={{ width: totW, flex: '0 0 auto', display: 'flex', justifyContent: 'center' }}>
                  <span style={{ minWidth: 6 * u, textAlign: 'center', background: RED, color: '#fff', fontFamily: DISP,
                    fontWeight: 900, fontStyle: 'italic', fontSize: 2.7 * u, padding: `${0.4 * u}px ${0.7 * u}px`, borderRadius: 0.4 * u }}>
                    {row.overall}</span>
                </div>
              </div>
            );
          })}
        </div>
        <div style={{ textAlign: 'center', color: REDBR, fontFamily: DISP, fontWeight: 800, fontStyle: 'italic',
          fontSize: 2.3 * u, textTransform: 'uppercase', letterSpacing: '0.01em' }}>Follow the fleet on the race tracker</div>
      </div>
    </div>
  );
}

// ════════════════════════════════════════════════════════════════════════════
// 5 · STAGE WINNERS — route header + crew hero + team-colour band
// ════════════════════════════════════════════════════════════════════════════
function PortNode({ u, flag, port, country, align }) {
  return (
    <div style={{ display: 'flex', alignItems: 'center', gap: 1.1 * u, flexDirection: align === 'right' ? 'row-reverse' : 'row' }}>
      <FlagCircle flag={flag || '🏁'} size={4.4 * u} />
      <div style={{ textAlign: align === 'right' ? 'right' : 'left', lineHeight: 1.02 }}>
        <div style={{ fontFamily: DISP, fontWeight: 900, fontStyle: 'italic', color: '#fff', fontSize: 2.1 * u,
          textTransform: 'uppercase', letterSpacing: '-0.01em' }}>{port}</div>
        {country && <div style={{ color: 'rgba(255,255,255,0.78)', fontFamily: 'var(--font-brand)', fontWeight: 600,
          fontSize: 1.3 * u, letterSpacing: '0.1em', textTransform: 'uppercase' }}>{country}</div>}
      </div>
    </div>
  );
}

function TplStageWinners({ w, h, slotId, data }) {
  const u = U(w, h);
  const win = window.getTeam((data.entries && data.entries[0] && data.entries[0].key) || data.team1 || 'tongyeong');
  const r = data.route || {};
  return (
    <div style={{ position: 'relative', width: w, height: h, overflow: 'hidden', background: BLACK }}>
      <Media slotId={slotId} data={data} placeholder="Drop the winning crew photo" />
      <Scrim dir="to top" from="rgba(15,11,12,0.05)" mid="rgba(15,11,12,0.15)" to="rgba(15,11,12,0.8)" />
      {/* route header */}
      <div style={{ position: 'absolute', top: 0, left: 0, right: 0, padding: `${3 * u}px ${3.5 * u}px`,
        display: 'flex', alignItems: 'center', justifyContent: 'space-between',
        background: 'linear-gradient(to bottom, rgba(15,11,12,0.82), rgba(15,11,12,0))' }}>
        <PortNode u={u} flag={r.fromFlag} port={r.fromPort} country={r.fromCountry} align="left" />
        <div style={{ color: '#fff', fontFamily: DISP, fontWeight: 900, fontSize: 2.6 * u, letterSpacing: '-0.1em', opacity: 0.92 }}>❯❯❯</div>
        <PortNode u={u} flag={r.toFlag} port={r.toPort} country={r.toCountry} align="right" />
      </div>
      {/* crew names */}
      {data.crewNames && (
        <div style={{ position: 'absolute', left: 4 * u, right: 4 * u, bottom: 30 * u, display: 'flex',
          justifyContent: 'space-between', color: '#fff', fontFamily: DISP, fontWeight: 700, fontStyle: 'italic',
          fontSize: 2.1 * u, textTransform: 'uppercase', textShadow: '0 1px 8px rgba(0,0,0,0.85)' }}>
          {String(data.crewNames).split('·').map((n, i) => <span key={i}>{n.trim()}</span>)}
        </div>
      )}
      {/* WINNERS title */}
      <div style={{ position: 'absolute', left: 4 * u, right: 4 * u, bottom: 17 * u }}>
        <div style={{ fontFamily: DISP, fontWeight: 800, fontStyle: 'italic', fontSize: 3 * u, color: '#fff',
          textTransform: 'uppercase', letterSpacing: '0.03em' }}>Stage {data.stageNo}</div>
        <div style={{ fontFamily: DISP, fontWeight: 900, fontStyle: 'italic', fontSize: 11 * u, color: '#fff',
          textTransform: 'uppercase', letterSpacing: '-0.03em', lineHeight: 0.82, marginTop: -0.4 * u,
          textShadow: '0 3px 18px rgba(0,0,0,0.5)' }}>Winners</div>
      </div>
      {/* team-colour band */}
      <div style={{ position: 'absolute', left: 0, right: 0, bottom: 0, background: win.colour, overflow: 'hidden',
        padding: `${2.4 * u}px ${4 * u}px ${2.6 * u}px`, clipPath: `polygon(0 ${2.6 * u}px, 100% 0, 100% 100%, 0 100%)` }}>
        {/* faint repeated watermark */}
        <div style={{ position: 'absolute', inset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center',
          color: win.textOn, opacity: 0.1, fontFamily: DISP, fontWeight: 900, fontStyle: 'italic', fontSize: 8 * u,
          textTransform: 'uppercase', whiteSpace: 'nowrap', pointerEvents: 'none' }}>Winners&nbsp;Winners</div>
        <div style={{ position: 'relative', textAlign: 'center', marginTop: 1 * u }}>
          <div style={{ fontFamily: DISP, fontWeight: 900, fontStyle: 'italic', color: win.textOn, fontSize: 5.4 * u,
            textTransform: 'uppercase', letterSpacing: '-0.02em', lineHeight: 0.92 }}>{win.name}</div>
          {data.stage && (
            <div style={{ color: win.textOn, opacity: 0.95, fontFamily: DISP, fontWeight: 700, fontStyle: 'italic',
              fontSize: 1.9 * u, textTransform: 'uppercase', letterSpacing: '0.03em', marginTop: 0.5 * u }}>{data.stage}</div>
          )}
          <div style={{ display: 'flex', justifyContent: 'center', marginTop: 1.2 * u }}><ClipperLogo h={3.4 * u} /></div>
        </div>
      </div>
    </div>
  );
}

// ════════════════════════════════════════════════════════════════════════════
// 6 · CREW STORY — pull quote + portrait + name box + CTA
// ════════════════════════════════════════════════════════════════════════════
function TplCrewStory({ w, h, slotId, data }) {
  const u = U(w, h);
  const c = data.crew || {};
  return (
    <div style={{ position: 'relative', width: w, height: h, overflow: 'hidden', background: `linear-gradient(140deg, #20171b, ${BLACK})` }}>
      {/* portrait right */}
      <div style={{ position: 'absolute', top: 0, right: 0, bottom: 0, width: '60%', overflow: 'hidden' }}>
        <Media slotId={slotId} data={data} placeholder="Drop the crew portrait" />
        <div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(90deg, rgba(15,11,12,0.97) 0%, rgba(15,11,12,0.4) 28%, rgba(15,11,12,0) 52%)' }} />
      </div>
      {/* big faint quote mark */}
      <div style={{ position: 'absolute', left: 3.5 * u, top: 9 * u, fontFamily: 'Georgia, serif', fontSize: 34 * u,
        lineHeight: 1, color: 'rgba(255,255,255,0.12)', pointerEvents: 'none' }}>“</div>
      <div style={{ position: 'absolute', left: 5 * u, top: 5 * u }}><ClipperLogo h={3.8 * u} /></div>
      {/* quote */}
      <div style={{ position: 'absolute', left: 5 * u, top: 16 * u, width: '58%' }}>
        <div style={{ fontFamily: DISP, fontWeight: 800, fontStyle: 'italic', color: '#fff', fontSize: 6 * u,
          lineHeight: 1.04, textTransform: 'uppercase', letterSpacing: '-0.01em' }}>
          {c.quote || 'Did I really do that? Or was it a dream?'}</div>
        <div style={{ width: 12 * u, height: 0.6 * u, background: RED, marginTop: 1.8 * u }} />
      </div>
      {/* name box with connector */}
      <div style={{ position: 'absolute', left: 5 * u, bottom: 11 * u, display: 'flex', alignItems: 'center' }}>
        <div style={{ background: 'rgba(15,11,12,0.66)', borderRadius: 1 * u, padding: `${1.2 * u}px ${1.6 * u}px`,
          backdropFilter: 'blur(2px)' }}>
          <div style={{ display: 'flex', alignItems: 'center', gap: 1 * u }}>
            {c.flag && <span style={{ fontSize: 2.6 * u }}>{c.flag}</span>}
            <div style={{ fontFamily: DISP, fontWeight: 900, fontStyle: 'italic', color: '#fff', fontSize: 2.4 * u,
              textTransform: 'uppercase', borderBottom: `${0.3 * u}px solid ${RED}`, paddingBottom: 0.2 * u }}>{c.name || ''}</div>
          </div>
          <div style={{ color: 'rgba(255,255,255,0.8)', fontFamily: 'var(--font-brand)', fontWeight: 600, fontSize: 1.5 * u,
            letterSpacing: '0.03em', marginTop: 0.7 * u, lineHeight: 1.35 }}>
            {[c.age, c.role].filter(Boolean).join('   |   ')}<br />
            {[c.boat && `Race Crew on board ${c.boat}`, c.legs].filter(Boolean).join(', ')}
          </div>
        </div>
        {/* connector toward the portrait */}
        <div style={{ width: 6 * u, height: 0.3 * u, background: 'rgba(255,255,255,0.5)' }} />
        <div style={{ width: 1.2 * u, height: 1.2 * u, borderRadius: '50%', background: '#fff', marginLeft: -0.2 * u }} />
      </div>
      {/* CTA bar */}
      {c.cta && (
        <div style={{ position: 'absolute', left: 0, right: 0, bottom: 0, background: 'rgba(15,11,12,0.8)',
          padding: `${1.8 * u}px ${5 * u}px`, color: '#fff', fontFamily: DISP, fontWeight: 900, fontStyle: 'italic',
          fontSize: 3 * u, textTransform: 'uppercase', letterSpacing: '0.01em' }}>{c.cta}</div>
      )}
    </div>
  );
}

// ════════════════════════════════════════════════════════════════════════════
// 7 · HOST PORT — "We're coming to [City]" + diagonal photo seam
// ════════════════════════════════════════════════════════════════════════════
function TplHostPort({ w, h, slotId, data }) {
  const u = U(w, h);
  return (
    <div style={{ position: 'relative', width: w, height: h, overflow: 'hidden',
      background: `linear-gradient(160deg, #6e0f23 0%, ${RED} 55%, #7a0c1f 100%)` }}>
      <Media slotId={slotId} data={data} placeholder="Drop a boat-in-the-city photo" />
      {/* red gradient seam over the lower photo (diagonal top edge), export-safe */}
      <div style={{ position: 'absolute', left: 0, right: 0, bottom: 0, height: '46%',
        clipPath: 'polygon(0 28%, 100% 0, 100% 100%, 0 100%)',
        background: 'linear-gradient(to top, rgba(122,12,31,1) 38%, rgba(177,0,30,0.7) 70%, rgba(177,0,30,0) 100%)', pointerEvents: 'none' }} />
      {data.dates && (
        <div style={{ position: 'absolute', top: 3.5 * u, left: '50%', transform: 'translateX(-50%)',
          background: 'rgba(40,40,46,0.6)', color: '#fff', borderRadius: 5 * u, padding: `${1 * u}px ${3 * u}px`,
          fontFamily: DISP, fontWeight: 800, fontSize: 3 * u, textTransform: 'uppercase', letterSpacing: '0.08em',
          whiteSpace: 'nowrap', backdropFilter: 'blur(3px)' }}>{data.dates}</div>
      )}
      <div style={{ position: 'absolute', left: 4.5 * u, right: 4.5 * u, bottom: 12.5 * u }}>
        <div style={{ fontFamily: DISP, fontWeight: 800, fontSize: 5.2 * u, color: '#fff', textTransform: 'uppercase',
          letterSpacing: '-0.01em', lineHeight: 1 }}>We're coming to</div>
        <div style={{ fontFamily: DISP, fontWeight: 900, fontStyle: 'italic', fontSize: 11.5 * u, color: '#fff',
          textTransform: 'uppercase', letterSpacing: '-0.035em', lineHeight: 0.86, marginTop: 0.3 * u,
          textShadow: '0 3px 18px rgba(0,0,0,0.4)' }}>{data.city || 'Washington, DC'}</div>
      </div>
      <div style={{ position: 'absolute', left: 4.5 * u, right: 4.5 * u, bottom: 5.5 * u, display: 'flex', alignItems: 'center', gap: 2 * u }}>
        <ClipperLogo h={4.2 * u} />
        {data.partner && (
          <>
            <div style={{ width: 1, height: 4 * u, background: 'rgba(255,255,255,0.45)' }} />
            <div style={{ color: '#fff', fontFamily: DISP, fontWeight: 800, fontStyle: 'italic', fontSize: 3 * u,
              textTransform: 'uppercase' }}>{data.partner}</div>
          </>
        )}
      </div>
      {data.partner && (
        <div style={{ position: 'absolute', left: 0, right: 0, bottom: 2.6 * u, textAlign: 'center', color: 'rgba(255,255,255,0.85)',
          fontFamily: 'var(--font-brand)', fontWeight: 700, fontSize: 1.3 * u, letterSpacing: '0.2em', textTransform: 'uppercase' }}>
          Team and Host Port Partner</div>
      )}
    </div>
  );
}

// ════════════════════════════════════════════════════════════════════════════
// 8 · LIVE / VIRTUAL EVENT — corner brackets, LIVE pill, title, date-time
// ════════════════════════════════════════════════════════════════════════════
function TplLiveEvent({ w, h, slotId, data }) {
  const u = U(w, h);
  const ev = data.event || {};
  const bracket = (pos) => {
    const s = 7 * u, t = 0.5 * u, m = 4 * u;
    const base = { position: 'absolute', width: s, height: s, borderColor: '#fff', borderStyle: 'solid' };
    const map = {
      tl: { top: m, left: m, borderWidth: `${t}px 0 0 ${t}px` }, tr: { top: m, right: m, borderWidth: `${t}px ${t}px 0 0` },
      bl: { bottom: m, left: m, borderWidth: `0 0 ${t}px ${t}px` }, br: { bottom: m, right: m, borderWidth: `0 ${t}px ${t}px 0` },
    };
    return <div style={{ ...base, ...map[pos] }} />;
  };
  // First word(s) stay small ("FROM THE"); the rest is the big italic headline.
  const title = ev.title || 'From the South Atlantic to the Roaring Forties';
  const m = String(title).match(/^((?:from|to|the|a|an|in|on|at|of)\s+){1,2}/i);
  const lead = m ? m[0].trim() : '';
  const rest = m ? title.slice(m[0].length) : title;
  return (
    <div style={{ position: 'relative', width: w, height: h, overflow: 'hidden', background: BLACK }}>
      <Media slotId={slotId} data={data} placeholder="Drop an at-sea photo / video" />
      <Scrim dir="to top" from="rgba(15,11,12,0.1)" to="rgba(15,11,12,0.9)" />
      {bracket('tl')}{bracket('tr')}{bracket('bl')}{bracket('br')}
      {ev.live && (
        <div style={{ position: 'absolute', top: 5 * u, right: 5 * u, display: 'flex', alignItems: 'center', gap: 1 * u,
          background: 'rgba(15,11,12,0.85)', color: '#fff', borderRadius: 0.8 * u, padding: `${0.7 * u}px ${1.4 * u}px`,
          fontFamily: DISP, fontWeight: 900, fontSize: 2.6 * u, letterSpacing: '0.04em', textTransform: 'uppercase' }}>
          LIVE<span style={{ width: 1.8 * u, height: 1.8 * u, borderRadius: '50%', background: REDBR }} /></div>
      )}
      <div style={{ position: 'absolute', left: 6 * u, right: 6 * u, bottom: 12 * u }}>
        <div style={{ color: '#fff', fontFamily: 'var(--font-brand)', fontWeight: 700, fontSize: 2 * u,
          letterSpacing: '0.38em', textTransform: 'uppercase', opacity: 0.85 }}>{ev.kind || 'Virtual Event'}</div>
        <div style={{ marginTop: 1 * u }}>
          {lead && <div style={{ fontFamily: DISP, fontWeight: 600, fontSize: 3.6 * u, color: '#fff',
            textTransform: 'uppercase', letterSpacing: '0.01em', lineHeight: 1 }}>{lead}</div>}
          <div style={{ fontFamily: DISP, fontWeight: 900, fontStyle: 'italic', fontSize: 6.6 * u, color: '#fff',
            textTransform: 'uppercase', letterSpacing: '-0.02em', lineHeight: 0.94, textShadow: '0 3px 18px rgba(0,0,0,0.5)' }}>{rest}</div>
        </div>
      </div>
      {ev.when && (
        <div style={{ position: 'absolute', left: 6 * u, right: 6 * u, bottom: 4.5 * u, background: 'rgba(255,255,255,0.95)',
          color: BLACK, borderRadius: 0.8 * u, padding: `${1 * u}px ${1.8 * u}px`, fontFamily: DISP, fontWeight: 800,
          fontStyle: 'italic', fontSize: 2.6 * u, textTransform: 'uppercase', textAlign: 'center' }}>{ev.when}</div>
      )}
    </div>
  );
}

// ════════════════════════════════════════════════════════════════════════════
// 9 · COUNTDOWN — "X DAYS TO GO" (scrambles in video mode)
// ════════════════════════════════════════════════════════════════════════════
function TplCountdown({ w, h, slotId, data }) {
  const u = U(w, h);
  const video = data.mediaKind === 'video';
  const leg = data.legColour || REDBR;
  return (
    <div style={{ position: 'relative', width: w, height: h, overflow: 'hidden', background: BLACK }}>
      <Media slotId={slotId} data={data} placeholder="Drop a hero photo / video" />
      <Scrim dir="to top" from="rgba(15,11,12,0.2)" to="rgba(15,11,12,0.85)" />
      <div style={{ position: 'absolute', top: 5 * u, left: 5 * u }}><ClipperLogo h={4.2 * u} /></div>
      <div style={{ position: 'absolute', left: 0, right: 0, top: '34%', textAlign: 'center' }}>
        <div style={{ fontFamily: DISP, fontWeight: 900, fontStyle: 'italic', fontSize: 30 * u, color: '#fff',
          lineHeight: 0.8, letterSpacing: '-0.04em', textShadow: '0 4px 28px rgba(0,0,0,0.5)' }}>
          {video ? <window.Scramble enabled value={String(data.count != null ? data.count : 14)} duration={1.1} /> : (data.count != null ? data.count : 14)}</div>
        <div style={{ fontFamily: DISP, fontWeight: 900, fontStyle: 'italic', fontSize: 6 * u, color: leg,
          textTransform: 'uppercase', letterSpacing: '0.02em', marginTop: -1 * u }}>Days to go</div>
      </div>
      <div style={{ position: 'absolute', left: 0, right: 0, bottom: 6 * u, textAlign: 'center' }}>
        <div style={{ fontFamily: DISP, fontWeight: 800, fontStyle: 'italic', fontSize: 3.4 * u, color: '#fff',
          textTransform: 'uppercase', letterSpacing: '0.01em' }}>{data.countdownLabel || 'Until race start'}</div>
        {data.dates && <div style={{ color: MIST, fontFamily: 'var(--font-brand)', fontWeight: 700, fontSize: 1.8 * u,
          letterSpacing: '0.18em', textTransform: 'uppercase', marginTop: 0.8 * u }}>{data.dates}</div>}
      </div>
    </div>
  );
}

// ════════════════════════════════════════════════════════════════════════════
// 10 · CONDITIONS — at-sea data slate: wind / sea / position
// ════════════════════════════════════════════════════════════════════════════
function TplConditions({ w, h, slotId, data }) {
  const u = U(w, h);
  const c = data.conditions || {};
  const leg = data.legColour || CYAN;
  const Stat = ({ label, value, unit }) => (
    <div style={{ flex: 1 }}>
      <div style={{ color: leg, fontFamily: 'var(--font-brand)', fontWeight: 700, fontSize: 1.5 * u,
        letterSpacing: '0.18em', textTransform: 'uppercase' }}>{label}</div>
      <div style={{ color: '#fff', fontFamily: DISP, fontWeight: 900, fontStyle: 'italic', fontSize: 6.5 * u,
        lineHeight: 1, letterSpacing: '-0.02em' }}>{value}<span style={{ fontSize: 2.6 * u, fontWeight: 700 }}> {unit}</span></div>
    </div>
  );
  return (
    <div style={{ position: 'relative', width: w, height: h, overflow: 'hidden', background: BLACK }}>
      <Media slotId={slotId} data={data} placeholder="Drop a storm / ocean photo" />
      <Scrim dir="to top" from="rgba(15,11,12,0.25)" to="rgba(15,11,12,0.92)" />
      <div style={{ position: 'absolute', top: 5 * u, left: 5 * u, right: 5 * u, display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
        <div>
          <div style={{ color: leg, fontFamily: 'var(--font-brand)', fontWeight: 700, fontSize: 1.7 * u,
            letterSpacing: '0.28em', textTransform: 'uppercase' }}>Conditions at sea</div>
          <div style={{ fontFamily: DISP, fontWeight: 900, fontStyle: 'italic', fontSize: 6 * u, color: '#fff',
            textTransform: 'uppercase', letterSpacing: '-0.02em', lineHeight: 0.92, marginTop: 0.6 * u }}>{c.place || 'The Roaring Forties'}</div>
        </div>
        <ClipperLogo h={3.8 * u} />
      </div>
      <div style={{ position: 'absolute', left: 5 * u, right: 5 * u, bottom: 9 * u }}>
        <div style={{ display: 'flex', gap: 2 * u }}>
          <Stat label="Wind" value={c.wind || '38'} unit="kts" />
          <Stat label="Direction" value={c.dir || 'SW'} unit="" />
        </div>
        <div style={{ display: 'flex', gap: 2 * u, marginTop: 2.5 * u }}>
          <Stat label="Sea state" value={c.sea || '6.2'} unit="m" />
          <Stat label="Position" value={c.position || '46°S'} unit={c.position2 || '142°W'} />
        </div>
      </div>
      <div style={{ position: 'absolute', left: 5 * u, right: 5 * u, bottom: 3.5 * u, display: 'flex', justifyContent: 'space-between',
        alignItems: 'center', borderTop: '1px solid rgba(255,255,255,0.18)', paddingTop: 1.4 * u }}>
        <StageStrap text={data.stage} size={1.9 * u} align="left" accent={leg} />
        {c.day && <div style={{ color: MIST, fontFamily: DISP, fontWeight: 800, fontStyle: 'italic', fontSize: 2.2 * u, textTransform: 'uppercase' }}>{c.day}</div>}
      </div>
    </div>
  );
}

// ════════════════════════════════════════════════════════════════════════════
// 11 · CREW HERO — branded portrait card (cut-out engine + stats) [prototype]
// Proves the "every crew member a hero" + "Crew Wrapped" personalisation play.
// ════════════════════════════════════════════════════════════════════════════
function TplCrewHero({ w, h, slotId, data }) {
  const u = U(w, h);
  const t = window.getTeam(data.heroTeam || 'tongyeong');
  const c = data.crew || {};
  const stats = data.stats || [];
  return (
    <div style={{ position: 'relative', width: w, height: h, overflow: 'hidden',
      background: `linear-gradient(155deg, ${t.colour} 0%, #14101a 52%, ${BLACK} 100%)` }}>
      <GridOverlay opacity={0.05} cells={8} />
      <div style={{ position: 'absolute', right: 0, top: 0, bottom: 0, width: '60%', pointerEvents: 'none',
        background: `radial-gradient(120% 90% at 78% 42%, ${t.colour}77 0%, transparent 62%)` }} />
      {/* hero cut-out (drop a transparent PNG, drag/resize) */}
      <div style={{ position: 'absolute', inset: 0, pointerEvents: 'none' }}>
        <CutoutSlot id={`${slotId}-hero`} w={w} h={h} defaultPos={{ x: 64, y: 60 }} baseWu={54} baseHu={80} autoCut={data.autoCut} />
      </div>
      <div style={{ position: 'absolute', top: 5 * u, left: 5 * u, pointerEvents: 'none' }}><ClipperLogo h={4 * u} /></div>
      {/* name block */}
      <div style={{ position: 'absolute', left: 5 * u, top: 17 * u, width: '56%', pointerEvents: 'none' }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 1.2 * u, marginBottom: 1 * u }}>
          {c.flag && <span style={{ fontSize: 3.4 * u }}>{c.flag}</span>}
          <window.TeamBadge teamKey={t.key} size={5 * u} radius={0.8 * u} />
        </div>
        <div style={{ fontFamily: DISP, fontWeight: 900, fontStyle: 'italic', color: '#fff', fontSize: 8 * u,
          lineHeight: 0.85, textTransform: 'uppercase', letterSpacing: '-0.03em', textShadow: '0 3px 16px rgba(0,0,0,0.5)' }}>{c.name || ''}</div>
        <div style={{ width: 10 * u, height: 0.6 * u, background: '#fff', margin: `${1.3 * u}px 0` }} />
        <div style={{ color: 'rgba(255,255,255,0.9)', fontFamily: DISP, fontWeight: 700, fontStyle: 'italic',
          fontSize: 2.2 * u, textTransform: 'uppercase', lineHeight: 1.28 }}>
          {[c.age, c.role].filter(Boolean).join('  ·  ')}<br />
          {[c.boat && `On board ${c.boat}`, c.legs].filter(Boolean).join(' · ')}
        </div>
      </div>
      {/* stats strip (Crew-Wrapped style) */}
      {stats.length > 0 && (
        <div style={{ position: 'absolute', left: 5 * u, right: 5 * u, bottom: 4.5 * u, display: 'flex', gap: 1.5 * u, pointerEvents: 'none' }}>
          {stats.map((s, i) => (
            <div key={i} style={{ flex: 1, background: 'rgba(15,11,12,0.55)', borderRadius: 0.8 * u,
              padding: `${1.1 * u}px ${1.2 * u}px`, borderLeft: `${0.5 * u}px solid ${t.colour}` }}>
              <div style={{ fontFamily: DISP, fontWeight: 900, fontStyle: 'italic', color: '#fff', fontSize: 3.4 * u, lineHeight: 1 }}>{s.value}</div>
              <div style={{ color: MIST, fontFamily: 'var(--font-brand)', fontWeight: 700, fontSize: 1.2 * u,
                letterSpacing: '0.12em', textTransform: 'uppercase', marginTop: 0.4 * u }}>{s.label}</div>
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

// ════════════════════════════════════════════════════════════════════════════
// 12 · RECRUIT AD — "Could you do it?" + CTA + offer [prototype, ad mode]
// Best run at 1.91:1 for a Facebook/LinkedIn link ad.
// ════════════════════════════════════════════════════════════════════════════
function TplRecruit({ w, h, slotId, data }) {
  const u = U(w, h);
  const r = data.recruit || {};
  return (
    <div style={{ position: 'relative', width: w, height: h, overflow: 'hidden', background: BLACK }}>
      <Media slotId={slotId} data={data} placeholder="Drop a crew / ocean hero photo" />
      <Scrim dir="to top" from="rgba(15,11,12,0.22)" mid="rgba(15,11,12,0.55)" to="rgba(15,11,12,0.92)" />
      <div style={{ position: 'absolute', top: 5 * u, left: 5 * u }}><ClipperLogo h={4.2 * u} /></div>
      {r.offer && (
        <div style={{ position: 'absolute', top: 5.6 * u, right: 5 * u, background: RED, color: '#fff', borderRadius: 0.6 * u,
          padding: `${0.6 * u}px ${1.2 * u}px`, fontFamily: DISP, fontWeight: 800, fontStyle: 'italic', fontSize: 1.7 * u,
          textTransform: 'uppercase', letterSpacing: '0.04em' }}>{r.offer}</div>
      )}
      <div style={{ position: 'absolute', left: 5 * u, right: 5 * u, bottom: 5 * u }}>
        <div style={{ fontFamily: DISP, fontWeight: 900, fontStyle: 'italic', color: '#fff', fontSize: 11 * u,
          lineHeight: 0.84, textTransform: 'uppercase', letterSpacing: '-0.03em', textShadow: '0 3px 18px rgba(0,0,0,0.5)' }}>
          {r.headline || 'Could you do it?'}</div>
        {r.sub && (
          <div style={{ color: 'rgba(255,255,255,0.92)', fontFamily: 'var(--font-brand)', fontWeight: 600, fontSize: 2.3 * u,
            lineHeight: 1.3, marginTop: 1.2 * u, maxWidth: '90%' }}>{r.sub}</div>
        )}
        <div style={{ display: 'flex', alignItems: 'center', gap: 1.8 * u, marginTop: 2 * u, flexWrap: 'wrap' }}>
          <div style={{ background: RED, color: '#fff', fontFamily: DISP, fontWeight: 900, fontStyle: 'italic', fontSize: 2.8 * u,
            textTransform: 'uppercase', letterSpacing: '0.02em', padding: `${1.1 * u}px ${2.2 * u}px`, borderRadius: 0.7 * u,
            boxShadow: '0 8px 22px rgba(177,0,30,0.5)' }}>{r.cta || 'Apply now'} ❯</div>
          {r.url && <div style={{ color: '#fff', fontFamily: DISP, fontWeight: 700, fontStyle: 'italic', fontSize: 2 * u,
            textTransform: 'uppercase', opacity: 0.9 }}>{r.url}</div>}
        </div>
      </div>
    </div>
  );
}

// ── register ────────────────────────────────────────────────────────────────
window.ClipperLogo = ClipperLogo;
window.ClipperStageStrap = StageStrap;
window.ClipperTemplates = {
  raceresults: TplRaceResults, oceansprint: TplOceanSprint, scoringgate: TplScoringGate,
  standings: TplStandings, stagewinners: TplStageWinners, crewstory: TplCrewStory,
  hostport: TplHostPort, liveevent: TplLiveEvent, countdown: TplCountdown, conditions: TplConditions,
  crewhero: TplCrewHero, recruit: TplRecruit,
};
window.ClipperTemplateNames = {
  raceresults: 'Race Results', oceansprint: 'Ocean Sprint', scoringgate: 'Scoring Gate',
  standings: 'Standings', stagewinners: 'Stage Winners', crewstory: 'Crew Story',
  hostport: 'Host Port', liveevent: 'Live Event', countdown: 'Countdown', conditions: 'Conditions',
  crewhero: 'Crew Hero', recruit: 'Recruit Ad',
};
