// Layouts2.jsx — second-generation layouts: Marquee, Outline Echo, Spine, Ransom,
// Ticket, Tracklist, Swiss, Contour, Wave Text.
// Loaded BEFORE Poster.jsx; Poster merges window.EXTRA_LAYOUTS into its layout map.

// Local copy of the lyric block primitive (Babel scripts don't share scope)
function LyricBlock2({ text, color, font, size, lh = 1.2, align = "left", style = {} }) {
  const lines = text.split("\n");
  const selfMap = { left: "flex-start", center: "center", right: "flex-end" };
  return (
    <div style={{ color, fontFamily: font, fontSize: `${size}mm`, lineHeight: lh, textAlign: align, alignSelf: selfMap[align] || "flex-start", whiteSpace: "pre-wrap", ...style }}>
      {lines.map((l, i) => <div key={i}>{l || "\u00a0"}</div>)}
    </div>
  );
}

// --- Marquee: huge hero cropped off both edges, lyrics above + below ---------
function LayoutMarquee({ hero, blocks, palette, heroFont, bodyFont, w, h, heroSize, bodySize, padding, lyricAlign, flexAlign }) {
  const k = heroSize / 100;
  const len = Math.max(1, (hero || "").length);
  // Intentionally wider than the poster so it crops at both sides
  const fs = ((w * 1.35) / (len * 0.52)) * k;
  const half = Math.ceil(blocks.length / 2);
  return (
    <>
      <div style={{ position: "absolute", inset: 0, display: "flex", alignItems: "center", justifyContent: "center", zIndex: 1, pointerEvents: "none" }}>
        <div style={{ fontFamily: heroFont, fontWeight: 900, fontSize: `${fs}mm`, lineHeight: 0.8, color: palette.accent, whiteSpace: "nowrap", letterSpacing: "-0.03em" }}>{hero}</div>
      </div>
      <div style={{ position: "absolute", top: `${padding}mm`, left: `${padding}mm`, right: `${padding}mm`, zIndex: 3, display: "flex", flexDirection: "column", alignItems: flexAlign, gap: `${bodySize * 1.4}mm` }}>
        {blocks.slice(0, half).map((b, i) => (
          <LyricBlock2 key={b.id || i} text={b.text} color={palette.ink} font={bodyFont} size={bodySize} align={b.align || lyricAlign} />
        ))}
      </div>
      <div style={{ position: "absolute", bottom: `${padding}mm`, left: `${padding}mm`, right: `${padding}mm`, zIndex: 3, display: "flex", flexDirection: "column", justifyContent: "flex-end", alignItems: flexAlign, gap: `${bodySize * 1.4}mm` }}>
        {blocks.slice(half).map((b, i) => (
          <LyricBlock2 key={b.id || i} text={b.text} color={palette.ink} font={bodyFont} size={bodySize} align={b.align || lyricAlign} />
        ))}
      </div>
    </>
  );
}

// --- Outline Echo: stacked hero words, one solid, rest outlined --------------
function LayoutEcho({ hero, blocks, palette, heroFont, bodyFont, w, h, heroSize, bodySize, padding, lyricAlign, flexAlign }) {
  const k = heroSize / 100;
  const len = Math.max(1, (hero || "").length);
  const count = 5;
  const solidIdx = 2;
  const fs = Math.min((h * 0.6) / (count * 0.92), (w * 0.86) / (len * 0.52)) * k;
  const strokeW = Math.max(0.3, fs * 0.014);
  return (
    <>
      <div style={{ position: "absolute", top: `${padding}mm`, left: 0, right: 0, height: `${h * 0.66}mm`, display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", zIndex: 1, pointerEvents: "none" }}>
        {Array.from({ length: count }).map((_, i) => (
          <div
            key={i}
            style={{
              fontFamily: heroFont,
              fontWeight: 900,
              fontSize: `${fs}mm`,
              lineHeight: 0.92,
              letterSpacing: "-0.02em",
              whiteSpace: "nowrap",
              color: i === solidIdx ? palette.accent : "transparent",
              WebkitTextStroke: i === solidIdx ? "0" : `${strokeW}mm ${palette.accent}`,
            }}
          >
            {hero}
          </div>
        ))}
      </div>
      <div style={{ position: "absolute", bottom: `${padding}mm`, left: `${padding}mm`, right: `${padding}mm`, zIndex: 3, display: "flex", flexDirection: "column", justifyContent: "flex-end", alignItems: flexAlign, gap: `${bodySize * 1.4}mm` }}>
        {blocks.map((b, i) => (
          <LyricBlock2 key={b.id || i} text={b.text} color={palette.ink} font={bodyFont} size={bodySize} align={b.align || lyricAlign} />
        ))}
      </div>
    </>
  );
}

// --- Spine: thin full-height vertical hero on the left edge ------------------
function LayoutSpine({ hero, blocks, palette, heroFont, bodyFont, w, h, heroSize, bodySize, padding, lyricAlign, flexAlign }) {
  const k = heroSize / 100;
  const len = Math.max(1, (hero || "").length);
  const fs = Math.min(w * 0.14, (h * 0.88) / (len * 0.55)) * k;
  const spineW = padding * 0.6 + fs * 1.15;
  return (
    <>
      <div style={{ position: "absolute", left: `${padding * 0.6}mm`, top: 0, bottom: 0, display: "flex", alignItems: "center", zIndex: 1, pointerEvents: "none" }}>
        <div style={{ writingMode: "vertical-rl", transform: "rotate(180deg)", fontFamily: heroFont, fontWeight: 900, fontSize: `${fs}mm`, lineHeight: 1, color: palette.accent, whiteSpace: "nowrap", letterSpacing: "-0.02em" }}>{hero}</div>
      </div>
      <div style={{ position: "absolute", left: `${spineW + padding * 0.5}mm`, top: `${padding}mm`, bottom: `${padding}mm`, width: "0.5mm", background: palette.ink, opacity: 0.5, zIndex: 1 }}></div>
      <div style={{ position: "absolute", left: `${spineW + padding * 1.2}mm`, right: `${padding}mm`, top: `${padding}mm`, bottom: `${padding}mm`, zIndex: 3, display: "flex", flexDirection: "column", justifyContent: "center", alignItems: flexAlign, gap: `${bodySize * 1.8}mm` }}>
        {blocks.map((b, i) => (
          <LyricBlock2 key={b.id || i} text={b.text} color={palette.ink} font={bodyFont} size={bodySize} align={b.align || lyricAlign} />
        ))}
      </div>
    </>
  );
}

// --- Ransom: each hero letter on its own tinted chip in a different font -----
function LayoutRansom({ hero, blocks, palette, heroFont, bodyFont, w, h, heroSize, bodySize, padding, lyricAlign, flexAlign }) {
  const k = heroSize / 100;
  const chars = (hero || "").split("");
  const len = Math.max(1, chars.length);
  const fonts = ((window.FONTS || []).map((f) => f.css));
  const rots = [-5, 3, -2, 6, -4, 2, 5, -3];
  // chip background → text color pairs, picked for contrast within the palette
  const chipPairs = [
    { bg: palette.accent, fg: palette.bg },
    { bg: palette.ink, fg: palette.bg },
    { bg: palette.highlight || palette.muted, fg: palette.ink },
    { bg: palette.bg, fg: palette.ink },
  ];
  const fs = Math.min(w * 0.15, (w * 0.82) / (len * 0.75)) * k;
  return (
    <>
      <div style={{ position: "absolute", top: 0, left: `${padding}mm`, right: `${padding}mm`, height: `${h * 0.62}mm`, display: "flex", flexWrap: "wrap", alignItems: "center", alignContent: "center", justifyContent: "center", gap: `${fs * 0.1}mm`, zIndex: 1, pointerEvents: "none" }}>
        {chars.map((ch, i) => {
          if (ch === " ") return <div key={i} style={{ width: `${fs * 0.5}mm` }}></div>;
          const pair = chipPairs[i % chipPairs.length];
          return (
            <div
              key={i}
              style={{
                fontFamily: fonts[i % Math.max(1, fonts.length)] || heroFont,
                fontWeight: 900,
                fontSize: `${fs}mm`,
                lineHeight: 1,
                color: pair.fg,
                background: pair.bg,
                padding: `${fs * 0.1}mm ${fs * 0.14}mm`,
                transform: `rotate(${rots[i % rots.length]}deg)`,
                border: pair.bg === palette.bg ? `0.4mm solid ${palette.ink}` : "none",
                boxShadow: "0 0.6mm 1.2mm rgba(0,0,0,0.12)",
              }}
            >
              {ch}
            </div>
          );
        })}
      </div>
      <div style={{ position: "absolute", bottom: `${padding}mm`, left: `${padding}mm`, right: `${padding}mm`, zIndex: 3, display: "flex", flexDirection: "column", justifyContent: "flex-end", alignItems: flexAlign, gap: `${bodySize * 1.4}mm` }}>
        {blocks.map((b, i) => (
          <LyricBlock2 key={b.id || i} text={b.text} color={palette.ink} font={bodyFont} size={bodySize} align={b.align || lyricAlign} />
        ))}
      </div>
    </>
  );
}

// --- Ticket: ruled double frame, hero as the event, perforated divider -------
function LayoutTicket({ hero, blocks, palette, heroFont, bodyFont, w, h, heroSize, bodySize, padding, lyricAlign, flexAlign }) {
  const k = heroSize / 100;
  const len = Math.max(1, (hero || "").length);
  const inset = padding * 0.7;
  const fs = Math.min((w * 0.72) / (len * 0.68), h * 0.16) * k;
  const notchR = Math.min(w, h) * 0.025;
  const dividerY = 0.36; // fraction of frame height
  return (
    <div style={{ position: "absolute", inset: `${inset}mm`, border: `0.8mm solid ${palette.ink}`, zIndex: 1 }}>
      <div style={{ position: "absolute", inset: "1.5mm", border: `0.3mm solid ${palette.ink}` }}></div>
      {/* Hero area */}
      <div style={{ position: "absolute", top: 0, left: 0, right: 0, height: `${dividerY * 100}%`, display: "flex", alignItems: "center", justifyContent: "center", padding: `${padding * 0.5}mm` }}>
        <div style={{ fontFamily: heroFont, fontWeight: 900, fontSize: `${fs}mm`, lineHeight: 0.85, color: palette.accent, textAlign: "center", letterSpacing: "-0.02em", whiteSpace: "nowrap" }}>{hero}</div>
      </div>
      {/* Perforated divider with edge notches */}
      <div style={{ position: "absolute", top: `${dividerY * 100}%`, left: `${notchR * 1.2}mm`, right: `${notchR * 1.2}mm`, borderTop: `0.6mm dashed ${palette.ink}`, opacity: 0.8 }}></div>
      <div style={{ position: "absolute", top: `${dividerY * 100}%`, left: 0, transform: "translate(-50%, -50%)", width: `${notchR * 2}mm`, height: `${notchR * 2}mm`, borderRadius: "50%", background: palette.bg, border: `0.8mm solid ${palette.ink}` }}></div>
      <div style={{ position: "absolute", top: `${dividerY * 100}%`, right: 0, transform: "translate(50%, -50%)", width: `${notchR * 2}mm`, height: `${notchR * 2}mm`, borderRadius: "50%", background: palette.bg, border: `0.8mm solid ${palette.ink}` }}></div>
      {/* Fine print lyrics */}
      <div style={{ position: "absolute", top: `${dividerY * 100}%`, bottom: 0, left: 0, right: 0, padding: `${padding * 0.8}mm`, display: "flex", flexDirection: "column", justifyContent: "center", alignItems: flexAlign, gap: `${bodySize * 1.3}mm`, overflow: "hidden" }}>
        {blocks.map((b, i) => (
          <LyricBlock2 key={b.id || i} text={b.text} color={palette.ink} font={bodyFont} size={bodySize} align={b.align || lyricAlign} />
        ))}
      </div>
    </div>
  );
}

// --- Tracklist: album-sleeve back — numbered blocks with rules ---------------
function LayoutTracklist({ hero, blocks, palette, heroFont, bodyFont, w, h, heroSize, bodySize, padding, lyricAlign, flexAlign }) {
  const k = heroSize / 100;
  const len = Math.max(1, (hero || "").length);
  const fs = Math.min((w * 0.88) / (len * 0.55), h * 0.14) * k;
  return (
    <div style={{ position: "absolute", inset: `${padding}mm`, zIndex: 1, display: "flex", flexDirection: "column" }}>
      <div style={{ fontFamily: heroFont, fontWeight: 900, fontSize: `${fs}mm`, lineHeight: 0.9, color: palette.accent, letterSpacing: "-0.02em" }}>{hero}</div>
      <div style={{ height: "1.2mm", background: palette.ink, margin: `${padding * 0.5}mm 0 ${padding * 0.4}mm` }}></div>
      <div style={{ flex: 1, display: "flex", flexDirection: "column", justifyContent: "flex-start", overflow: "hidden" }}>
        {blocks.map((b, i) => (
          <div key={b.id || i} style={{ display: "flex", gap: `${bodySize * 1.2}mm`, padding: `${bodySize * 0.9}mm 0`, borderBottom: i < blocks.length - 1 ? `0.3mm solid ${palette.muted}` : "none", alignItems: "flex-start" }}>
            <div style={{ fontFamily: bodyFont, fontWeight: 700, fontSize: `${bodySize * 1.15}mm`, color: palette.accent, flex: "0 0 auto", minWidth: `${bodySize * 2.4}mm` }}>
              {String(i + 1).padStart(2, "0")}
            </div>
            <div style={{ flex: 1 }}>
              <LyricBlock2 text={b.text} color={palette.ink} font={bodyFont} size={bodySize} align={b.align || lyricAlign} />
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}

// --- Swiss: modernist grid — hero bottom-left, lyric columns top-right -------
function LayoutSwiss({ hero, blocks, palette, heroFont, bodyFont, w, h, heroSize, bodySize, padding, lyricAlign, flexAlign }) {
  const k = heroSize / 100;
  const len = Math.max(1, (hero || "").length);
  const fs = Math.min((w * 0.9) / (len * 0.5), h * 0.28) * k;
  return (
    <>
      <div style={{ position: "absolute", top: `${padding}mm`, left: `${padding}mm`, right: `${padding}mm`, height: "1.2mm", background: palette.accent, zIndex: 1 }}></div>
      <div style={{ position: "absolute", top: `${padding * 1.8}mm`, left: `${w * 0.42}mm`, right: `${padding}mm`, bottom: `${h * 0.4}mm`, zIndex: 3, display: "grid", gridTemplateColumns: "1fr 1fr", columnGap: `${padding * 0.8}mm`, rowGap: `${bodySize * 1.4}mm`, alignContent: "start" }}>
        {blocks.map((b, i) => (
          <LyricBlock2 key={b.id || i} text={b.text} color={palette.ink} font={bodyFont} size={bodySize} align={b.align || lyricAlign} />
        ))}
      </div>
      <div style={{ position: "absolute", left: `${padding}mm`, right: `${padding}mm`, bottom: `${padding}mm`, zIndex: 1, pointerEvents: "none" }}>
        <div style={{ fontFamily: heroFont, fontWeight: 900, fontSize: `${fs}mm`, lineHeight: 0.8, color: palette.accent, letterSpacing: "-0.03em", wordBreak: "break-word" }}>{hero}</div>
      </div>
    </>
  );
}

// --- Contour: lyrics inside a big circle, hero above --------------------------
function LayoutContour({ hero, blocks, palette, heroFont, bodyFont, w, h, heroSize, bodySize, padding, lyricAlign, flexAlign }) {
  const k = heroSize / 100;
  const len = Math.max(1, (hero || "").length);
  const fs = Math.min((w * 0.8) / (len * 0.55), h * 0.1) * k;
  const d = Math.min(w, h) * 0.8;
  return (
    <>
      <div style={{ position: "absolute", top: `${padding}mm`, left: `${padding}mm`, right: `${padding}mm`, zIndex: 1, display: "flex", justifyContent: "center", pointerEvents: "none" }}>
        <div style={{ fontFamily: heroFont, fontWeight: 900, fontSize: `${fs}mm`, lineHeight: 0.9, color: palette.ink, letterSpacing: "-0.02em", textAlign: "center" }}>{hero}</div>
      </div>
      <div style={{ position: "absolute", left: "50%", top: "55%", transform: "translate(-50%, -50%)", width: `${d}mm`, height: `${d}mm`, borderRadius: "50%", background: palette.accent, zIndex: 2, display: "flex", alignItems: "center", justifyContent: "center", overflow: "hidden" }}>
        <div style={{ width: `${d * 0.68}mm`, maxHeight: `${d * 0.78}mm`, overflow: "hidden", display: "flex", flexDirection: "column", justifyContent: "center", gap: `${bodySize * 1.3}mm` }}>
          {blocks.map((b, i) => (
            <LyricBlock2 key={b.id || i} text={b.text} color={palette.bg} font={bodyFont} size={bodySize} align="center" lh={1.25} />
          ))}
        </div>
      </div>
    </>
  );
}

// --- Wave Text: every lyric line rides its own sine-curve baseline -----------
function LayoutWavy({ hero, blocks, palette, heroFont, bodyFont, w, h, heroSize, bodySize, padding, lyricAlign, flexAlign }) {
  const k = heroSize / 100;
  const len = Math.max(1, (hero || "").length);
  const heroFs = ((w * 1.05) / (len * 0.52)) * k;
  const allLines = blocks.flatMap((b) => b.text.split("\n")).filter((l) => l.trim().length > 0);
  const usable = h - padding * 2;
  const lineGap = usable / Math.max(1, allLines.length);
  const fs = Math.min(bodySize * 1.3, lineGap * 0.62);
  const amp = Math.min(lineGap * 0.28, h * 0.02);
  const period = w / 2.2;
  const segs = Math.ceil(w / period) + 1;
  return (
    <>
      {/* Subdued hero behind the waves — accent at low opacity so it reads over patterns */}
      <div style={{ position: "absolute", inset: 0, display: "flex", alignItems: "center", justifyContent: "center", zIndex: 1, pointerEvents: "none" }}>
        <div style={{ fontFamily: heroFont, fontWeight: 900, fontSize: `${heroFs}mm`, lineHeight: 0.8, color: palette.accent, whiteSpace: "nowrap", letterSpacing: "-0.03em", opacity: 0.32 }}>{hero}</div>
      </div>
      <svg style={{ position: "absolute", inset: 0, zIndex: 3, pointerEvents: "none" }} width="100%" height="100%" viewBox={`0 0 ${w} ${h}`} xmlns="http://www.w3.org/2000/svg">
        <defs>
          {allLines.map((_, i) => {
            const y = padding + (i + 0.5) * lineGap;
            const dir = i % 2 === 0 ? -1 : 1;
            let dStr = `M ${-period * 0.25} ${y}`;
            for (let s = 0; s < segs + 1; s++) dStr += ` q ${period / 2} ${dir * -amp * 2} ${period} 0`;
            return <path key={i} id={`wavyline-${i}`} d={dStr} fill="none" />;
          })}
        </defs>
        {allLines.map((line, i) => (
          <text key={i} fontFamily={bodyFont} fontSize={fs} fill={palette.ink}>
            <textPath href={`#wavyline-${i}`} startOffset="8%">{line}</textPath>
          </text>
        ))}
      </svg>
    </>
  );
}

// Deterministic pseudo-random from a string — keeps soundwave bars / barcodes
// stable across re-renders and exports.
function seedFrom(str) {
  let h = 2166136261;
  for (let i = 0; i < str.length; i++) { h ^= str.charCodeAt(i); h = Math.imul(h, 16777619); }
  return (i) => {
    const x = Math.sin((h % 100000) + i * 12.9898) * 43758.5453;
    return x - Math.floor(x);
  };
}

// --- Player: album-art block, title, progress bar, transport controls --------
// Generic music-player look — deliberately NOT any one brand's trade dress.
function LayoutPlayer({ hero, blocks, palette, heroFont, bodyFont, w, h, heroSize, bodySize, padding, lyricAlign, photo }) {
  const k = heroSize / 100;
  const len = Math.max(1, (hero || "").length);
  const titleFs = Math.min((w * 0.82) / (len * 0.55), h * 0.055) * k;
  const iconH = w * 0.052;
  const progress = 0.38;
  const knobR = w * 0.011;
  const barY = w * 0.004;
  return (
    <div style={{ position: "absolute", inset: `${padding}mm`, zIndex: 2, display: "flex", flexDirection: "column" }}>
      {/* Album-art block: customer photo when provided, hero word cover otherwise */}
      <div style={{ width: "100%", aspectRatio: "1 / 1", background: palette.accent, display: "flex", alignItems: "center", justifyContent: "center", overflow: "hidden", flex: "0 0 auto" }}>
        {photo ? (
          <img src={photo} alt="" style={{ width: "100%", height: "100%", objectFit: "cover", display: "block" }} />
        ) : (
          <div style={{ fontFamily: heroFont, fontWeight: 900, fontSize: `${Math.min((w * 0.78) / (len * 0.55), w * 0.3) * k}mm`, lineHeight: 0.85, color: palette.bg, textAlign: "center", letterSpacing: "-0.02em", padding: `${padding * 0.4}mm`, wordBreak: "break-word" }}>{hero}</div>
        )}
      </div>
      {/* Title + caption */}
      <div style={{ marginTop: `${padding * 0.7}mm` }}>
        <div style={{ fontFamily: heroFont, fontWeight: 900, fontSize: `${titleFs}mm`, lineHeight: 0.95, color: palette.ink, letterSpacing: "-0.02em" }}>{hero}</div>
        <div style={{ fontFamily: bodyFont, fontSize: `${bodySize * 0.85}mm`, color: palette.ink, opacity: 0.55, marginTop: `${bodySize * 0.5}mm`, letterSpacing: "0.14em", textTransform: "uppercase" }}>Now Playing</div>
      </div>
      {/* Progress bar */}
      <div style={{ marginTop: `${padding * 0.6}mm`, position: "relative", height: `${knobR * 2}mm`, display: "flex", alignItems: "center" }}>
        <div style={{ position: "absolute", left: 0, right: 0, height: `${barY * 2}mm`, borderRadius: `${barY}mm`, background: palette.muted }}></div>
        <div style={{ position: "absolute", left: 0, width: `${progress * 100}%`, height: `${barY * 2}mm`, borderRadius: `${barY}mm`, background: palette.accent }}></div>
        <div style={{ position: "absolute", left: `${progress * 100}%`, transform: "translateX(-50%)", width: `${knobR * 2}mm`, height: `${knobR * 2}mm`, borderRadius: "50%", background: palette.ink }}></div>
      </div>
      <div style={{ display: "flex", justifyContent: "space-between", fontFamily: '"JetBrains Mono", monospace', fontSize: `${bodySize * 0.75}mm`, color: palette.ink, opacity: 0.65, marginTop: `${bodySize * 0.4}mm` }}>
        <span>1:17</span><span>-2:26</span>
      </div>
      {/* Transport controls */}
      <div style={{ display: "flex", alignItems: "center", justifyContent: "center", gap: `${w * 0.06}mm`, marginTop: `${padding * 0.5}mm` }}>
        <svg width={`${iconH}mm`} height={`${iconH}mm`} viewBox="0 0 24 24"><path d="M6 5 v14 M18 6 l-10 6 10 6 Z" fill={palette.ink} stroke={palette.ink} strokeWidth="2" strokeLinecap="round" /></svg>
        <svg width={`${iconH * 1.7}mm`} height={`${iconH * 1.7}mm`} viewBox="0 0 24 24"><circle cx="12" cy="12" r="11" fill={palette.ink} /><path d="M10 8 l6 4 -6 4 Z" fill={palette.bg} /></svg>
        <svg width={`${iconH}mm`} height={`${iconH}mm`} viewBox="0 0 24 24"><path d="M18 5 v14 M6 6 l10 6 -10 6 Z" fill={palette.ink} stroke={palette.ink} strokeWidth="2" strokeLinecap="round" /></svg>
      </div>
      {/* Lyrics fill the remainder — side by side so they fit under the player chrome */}
      <div style={{ flex: 1, minHeight: 0, overflow: "hidden", marginTop: `${padding * 0.7}mm`, display: "flex", flexWrap: "wrap", alignItems: "flex-start", columnGap: `${bodySize * 2.2}mm`, rowGap: `${bodySize * 1.2}mm` }}>
        {blocks.map((b, i) => (
          <div key={b.id || i} style={{ flex: "1 1 40%", minWidth: 0 }}>
            <LyricBlock2 text={b.text} color={palette.ink} font={bodyFont} size={bodySize * 0.92} lh={1.25} align={b.align || lyricAlign} />
          </div>
        ))}
      </div>
    </div>
  );
}

// --- Soundwave: waveform bars generated from the lyric text ------------------
function LayoutSoundwave({ hero, blocks, palette, heroFont, bodyFont, w, h, heroSize, bodySize, padding, lyricAlign, flexAlign }) {
  const k = heroSize / 100;
  const len = Math.max(1, (hero || "").length);
  const rnd = seedFrom(hero + blocks.map((b) => b.text).join(""));
  const n = 64;
  const span = w - padding * 2;
  const bw = (span / n) * 0.55;
  const midY = h * 0.4;
  const maxH = h * 0.16 * k;
  const played = Math.floor(n * 0.42);
  const bars = [];
  for (let i = 0; i < n; i++) {
    const t = i / (n - 1);
    const env = 0.35 + 0.65 * Math.sin(Math.PI * Math.min(1, t * 1.15)); // swell envelope
    const bh = Math.max(maxH * 0.06, maxH * env * (0.3 + 0.7 * rnd(i)));
    bars.push({ x: padding + (span / n) * i, y: midY - bh, bh: bh * 2, color: i < played ? palette.accent : palette.muted });
  }
  const titleFs = Math.min((w * 0.85) / (len * 0.55), h * 0.075) * k;
  return (
    <>
      <svg style={{ position: "absolute", inset: 0, zIndex: 1, pointerEvents: "none" }} width="100%" height="100%" viewBox={`0 0 ${w} ${h}`} xmlns="http://www.w3.org/2000/svg">
        {bars.map((b, i) => <rect key={i} x={b.x} y={b.y} width={bw} height={b.bh} rx={bw / 2} fill={b.color} />)}
      </svg>
      <div style={{ position: "absolute", left: `${padding}mm`, right: `${padding}mm`, top: `${h * 0.56}mm`, zIndex: 2, textAlign: "center" }}>
        <div style={{ fontFamily: heroFont, fontWeight: 900, fontSize: `${titleFs}mm`, lineHeight: 0.9, color: palette.ink, letterSpacing: "-0.02em" }}>{hero}</div>
        <div style={{ fontFamily: '"JetBrains Mono", monospace', fontSize: `${bodySize * 0.8}mm`, color: palette.ink, opacity: 0.55, marginTop: `${bodySize}mm`, letterSpacing: "0.18em", textTransform: "uppercase" }}>1:17 ─── 3:43</div>
      </div>
      <div style={{ position: "absolute", left: `${padding}mm`, right: `${padding}mm`, bottom: `${padding}mm`, zIndex: 3, display: "flex", flexDirection: "column", alignItems: "center", gap: `${bodySize * 1.4}mm` }}>
        {blocks.slice(0, 2).map((b, i) => (
          <LyricBlock2 key={b.id || i} text={b.text} color={palette.ink} font={bodyFont} size={bodySize * 0.9} align="center" />
        ))}
      </div>
    </>
  );
}

// --- Receipt: till-receipt itemisation of the lyric lines --------------------
function LayoutReceipt({ hero, blocks, palette, heroFont, bodyFont, w, h, heroSize, bodySize, padding, lyricAlign }) {
  const k = heroSize / 100;
  const len = Math.max(1, (hero || "").length);
  const mono = '"JetBrains Mono", monospace';
  const lines = blocks.flatMap((b) => b.text.split("\n")).filter((l) => l.trim().length > 0);
  const dur = (s) => { const t = 63 + (s.length * 17) % 187; return `${Math.floor(t / 60)}:${String(t % 60).padStart(2, "0")}`; };
  const total = lines.reduce((a, l) => a + 63 + (l.length * 17) % 187, 0);
  const totalStr = `${Math.floor(total / 60)}:${String(total % 60).padStart(2, "0")}`;
  const rnd = seedFrom(hero + lines.join(""));
  const rowFs = Math.min(bodySize, ((h - padding * 2) * 0.5) / Math.max(1, lines.length) / 1.9);
  const barcode = [];
  { let x = 0; let i = 0;
    while (x < w * 0.5) { const bw = (0.4 + rnd(i) * 1.6); if (i % 2 === 0) barcode.push({ x, bw }); x += bw + 0.5; i++; } }
  const rule = (style) => <div style={{ borderTop: `0.4mm ${style} ${palette.ink}`, opacity: 0.75, margin: `${padding * 0.35}mm 0` }}></div>;
  return (
    <div style={{ position: "absolute", inset: `${padding}mm`, zIndex: 2, display: "flex", flexDirection: "column", fontFamily: mono, color: palette.ink }}>
      <div style={{ textAlign: "center" }}>
        <div style={{ fontFamily: heroFont, fontWeight: 900, fontSize: `${Math.min((w * 0.8) / (len * 0.6), h * 0.09) * k}mm`, lineHeight: 0.9, letterSpacing: "-0.01em" }}>{hero}</div>
        <div style={{ fontSize: `${rowFs * 0.85}mm`, letterSpacing: "0.22em", marginTop: `${padding * 0.4}mm` }}>* * * OFFICIAL RECEIPT * * *</div>
        <div style={{ fontSize: `${rowFs * 0.8}mm`, opacity: 0.65, marginTop: `${rowFs * 0.5}mm` }}>ORDER #0001 · 33⅓ RPM · SIDE A</div>
      </div>
      {rule("dashed")}
      <div style={{ flex: 1, minHeight: 0, overflow: "hidden", display: "flex", flexDirection: "column", justifyContent: "space-evenly" }}>
        {lines.map((l, i) => (
          <div key={i} style={{ display: "flex", justifyContent: "space-between", gap: `${rowFs * 2}mm`, fontSize: `${rowFs}mm`, lineHeight: 1.9 }}>
            <span style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{l.toUpperCase()}</span>
            <span style={{ flex: "0 0 auto", opacity: 0.8 }}>{dur(l)}</span>
          </div>
        ))}
      </div>
      {rule("dashed")}
      <div style={{ display: "flex", justifyContent: "space-between", fontSize: `${rowFs * 1.35}mm`, fontWeight: 700, color: palette.accent }}>
        <span>TOTAL</span><span>{totalStr}</span>
      </div>
      <div style={{ display: "flex", justifyContent: "space-between", fontSize: `${rowFs * 0.9}mm`, opacity: 0.7, marginTop: `${rowFs * 0.6}mm` }}>
        <span>PAID IN FULL</span><span>ON REPEAT</span>
      </div>
      {rule("solid")}
      <svg width={`${w * 0.5}mm`} height={`${h * 0.045}mm`} viewBox={`0 0 ${w * 0.5} 20`} preserveAspectRatio="none" style={{ alignSelf: "center", marginTop: `${padding * 0.3}mm` }} xmlns="http://www.w3.org/2000/svg">
        {barcode.map((b, i) => <rect key={i} x={b.x} y="0" width={b.bw} height="20" fill={palette.ink} />)}
      </svg>
      <div style={{ textAlign: "center", fontSize: `${rowFs * 0.85}mm`, letterSpacing: "0.18em", marginTop: `${rowFs * 0.8}mm` }}>THANK YOU FOR LISTENING</div>
    </div>
  );
}

// --- Vinyl: lyric lines ride circular grooves inside a record ----------------
function LayoutVinyl({ hero, blocks, palette, heroFont, bodyFont, w, h, heroSize, bodySize, padding, lyricAlign, photo }) {
  const k = heroSize / 100;
  const len = Math.max(1, (hero || "").length);
  const cx = w / 2;
  const R = Math.min(w * 0.44, h * 0.3);
  const cy = padding + R + h * 0.02;
  const lines = blocks.flatMap((b) => b.text.split("\n")).filter((l) => l.trim().length > 0);
  const ringCount = Math.min(7, lines.length);
  const labelR = R * 0.3;
  const ringSpan = R * 0.9 - labelR - R * 0.12;
  const ringFs = Math.min(bodySize * 0.95, (ringSpan / Math.max(1, ringCount)) * 0.6);
  const circlePath = (r) => `M ${cx - r} ${cy} a ${r} ${r} 0 1 1 ${r * 2} 0 a ${r} ${r} 0 1 1 ${-r * 2} 0`;
  const titleFs = Math.min((w * 0.86) / (len * 0.55), h * 0.11) * k;
  return (
    <>
      <svg style={{ position: "absolute", inset: 0, zIndex: 1, pointerEvents: "none" }} width="100%" height="100%" viewBox={`0 0 ${w} ${h}`} xmlns="http://www.w3.org/2000/svg">
        <circle cx={cx} cy={cy} r={R} fill={palette.ink} />
        {[0.97, 0.92, 0.55, 0.4].map((f, i) => (
          <circle key={i} cx={cx} cy={cy} r={R * f} fill="none" stroke={palette.bg} strokeWidth="0.5" opacity="0.18" />
        ))}
        <defs>
          {Array.from({ length: ringCount }).map((_, i) => (
            <path key={i} id={`vinylring-${i}`} d={circlePath(R * 0.9 - R * 0.06 - (ringSpan / Math.max(1, ringCount)) * i)} fill="none" />
          ))}
        </defs>
        {lines.slice(0, ringCount).map((line, i) => (
          <text key={i} fontFamily={bodyFont} fontSize={ringFs} fill={palette.bg} opacity="0.88">
            <textPath href={`#vinylring-${i}`} startOffset={`${(i * 13) % 40}%`}>{line}</textPath>
          </text>
        ))}
        {photo ? (
          <>
            {/* Photo pressed into the centre label */}
            <defs>
              <clipPath id="vinyl-label-clip"><circle cx={cx} cy={cy} r={labelR} /></clipPath>
            </defs>
            <image href={photo} x={cx - labelR} y={cy - labelR} width={labelR * 2} height={labelR * 2} clipPath="url(#vinyl-label-clip)" preserveAspectRatio="xMidYMid slice" />
            <circle cx={cx} cy={cy} r={labelR} fill="none" stroke={palette.accent} strokeWidth={labelR * 0.05} />
          </>
        ) : (
          <>
            <circle cx={cx} cy={cy} r={labelR} fill={palette.accent} />
            <text x={cx} y={cy - labelR * 0.12} textAnchor="middle" fontFamily={heroFont} fontWeight="900" fontSize={Math.min(labelR * 0.42, (labelR * 1.6) / (len * 0.6))} fill={palette.bg}>{hero}</text>
            <text x={cx} y={cy + labelR * 0.38} textAnchor="middle" fontFamily={bodyFont} fontSize={labelR * 0.14} fill={palette.bg} opacity="0.85" letterSpacing="0.6">33⅓ RPM · LONG PLAY</text>
          </>
        )}
        <circle cx={cx} cy={cy} r={R * 0.032} fill={palette.bg} />
      </svg>
      <div style={{ position: "absolute", left: `${padding}mm`, right: `${padding}mm`, top: `${cy + R + padding * 0.7}mm`, bottom: `${padding}mm`, zIndex: 2, display: "flex", flexDirection: "column", justifyContent: "flex-end" }}>
        <div style={{ fontFamily: heroFont, fontWeight: 900, fontSize: `${titleFs}mm`, lineHeight: 0.85, color: palette.ink, letterSpacing: "-0.03em", textAlign: "center", wordBreak: "break-word" }}>{hero}</div>
        <div style={{ fontFamily: '"JetBrains Mono", monospace', fontSize: `${bodySize * 0.8}mm`, color: palette.ink, opacity: 0.6, textAlign: "center", letterSpacing: "0.22em", marginTop: `${bodySize}mm`, textTransform: "uppercase" }}>Original Pressing</div>
      </div>
    </>
  );
}

// --- Lineup: festival-bill tiers — the hero headlines, lyric lines are acts --
function LayoutLineup({ hero, blocks, palette, heroFont, bodyFont, w, h, heroSize, bodySize, padding }) {
  const k = heroSize / 100;
  const len = Math.max(1, (hero || "").length);
  const fs = Math.min((w * 0.9) / (len * 0.55), h * 0.13) * k;
  const mono = '"JetBrains Mono", monospace';
  const lines = blocks.flatMap((b) => b.text.split("\n")).map((l) => l.trim()).filter(Boolean);
  const tiers = [lines.slice(0, 2), lines.slice(2, 6), lines.slice(6)];
  const tierFs = [bodySize * 2.0, bodySize * 1.4, bodySize * 1.0];
  const tierOp = [1, 0.85, 0.62];
  const rule = (key) => <div key={key} style={{ height: "0.5mm", background: palette.accent, margin: `${padding * 0.4}mm auto`, width: "34%" }} />;
  return (
    <div style={{ position: "absolute", inset: `${padding}mm`, zIndex: 2, display: "flex", flexDirection: "column", alignItems: "center", textAlign: "center", overflow: "hidden" }}>
      <div style={{ fontFamily: mono, fontSize: `${bodySize * 0.85}mm`, letterSpacing: "0.35em", color: palette.ink, opacity: 0.7 }}>LIVE · ONE NIGHT ONLY</div>
      <div style={{ fontFamily: heroFont, fontWeight: 900, fontSize: `${fs}mm`, lineHeight: 0.9, color: palette.accent, letterSpacing: "-0.02em", margin: `${padding * 0.35}mm 0 0` }}>{hero}</div>
      {rule("r1")}
      <div style={{ flex: 1, minHeight: 0, display: "flex", flexDirection: "column", justifyContent: "space-evenly", width: "100%" }}>
        {tiers.map((tier, i) => tier.length > 0 && (
          <div key={i} style={{ fontFamily: heroFont, fontWeight: 700, fontSize: `${tierFs[i]}mm`, lineHeight: 1.55, color: palette.ink, opacity: tierOp[i], textTransform: "uppercase", letterSpacing: "0.03em" }}>
            {tier.join("  ·  ")}
          </div>
        ))}
      </div>
      {rule("r2")}
      <div style={{ fontFamily: mono, fontSize: `${bodySize * 0.85}mm`, letterSpacing: "0.28em", color: palette.ink, opacity: 0.7 }}>FROM THE FIRST LISTEN TO THE LAST ENCORE</div>
    </div>
  );
}

// --- Typewriter: hand-typed keepsake — mono text, generous whitespace --------
function LayoutTypewriter({ hero, blocks, palette, w, h, heroSize, bodySize, padding }) {
  const mono = '"JetBrains Mono", monospace';
  const k = heroSize / 100;
  const len = Math.max(1, (hero || "").length);
  const titleFs = Math.min(bodySize * 1.7 * k, (w * 0.58) / (len * 0.62));
  return (
    <div style={{ position: "absolute", inset: `${padding}mm`, zIndex: 2, display: "flex", alignItems: "center", justifyContent: "center" }}>
      <div style={{ maxWidth: `${w * 0.64}mm`, maxHeight: "100%", overflow: "hidden", display: "flex", flexDirection: "column", gap: `${bodySize * 2}mm` }}>
        <div style={{ fontFamily: mono, fontSize: `${titleFs}mm`, color: palette.accent, letterSpacing: "0.08em", textTransform: "lowercase", borderBottom: `0.35mm solid ${palette.ink}`, paddingBottom: `${bodySize}mm` }}>{hero}</div>
        {blocks.map((b, i) => (
          <LyricBlock2 key={b.id || i} text={b.text} color={palette.ink} font={mono} size={bodySize} lh={1.85} align="left" />
        ))}
        <div style={{ fontFamily: mono, fontSize: `${bodySize * 0.85}mm`, color: palette.ink, opacity: 0.45, letterSpacing: "0.4em" }}>· · ·</div>
      </div>
    </div>
  );
}

// --- Neon Sign: glowing hero in a framed sign, lyrics beneath ----------------
// Best on dark palettes (Glow, Neon, Midnight); glow colour = accent.
function LayoutNeon({ hero, blocks, palette, heroFont, bodyFont, w, h, heroSize, bodySize, padding }) {
  const k = heroSize / 100;
  const len = Math.max(1, (hero || "").length);
  const fs = Math.min((w * 0.78) / (len * 0.55), h * 0.12) * k;
  const glow = palette.accent;
  const glow2 = palette.highlight || palette.accent;
  return (
    <div style={{ position: "absolute", inset: `${padding}mm`, zIndex: 2, border: `0.8mm solid ${glow}`, borderRadius: "6mm", boxShadow: `0 0 4mm ${glow}66, inset 0 0 5mm ${glow}44`, display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", padding: `${padding}mm`, textAlign: "center", overflow: "hidden" }}>
      <div style={{ fontFamily: heroFont, fontWeight: 900, fontSize: `${fs}mm`, lineHeight: 0.95, color: palette.ink, letterSpacing: "0.02em", textShadow: `0 0 1.5mm ${glow}, 0 0 4mm ${glow}, 0 0 9mm ${glow}, 0 0 16mm ${glow}99` }}>{hero}</div>
      <div style={{ height: "0.5mm", width: "30%", background: glow2, boxShadow: `0 0 3mm ${glow2}`, margin: `${padding * 0.8}mm 0` }}></div>
      <div style={{ display: "flex", flexDirection: "column", gap: `${bodySize * 1.6}mm`, maxHeight: "52%", overflow: "hidden" }}>
        {blocks.slice(0, 2).map((b, i) => (
          <LyricBlock2 key={b.id || i} text={b.text} color={palette.ink} font={bodyFont} size={bodySize} lh={1.5} align="center" />
        ))}
      </div>
    </div>
  );
}

window.EXTRA_LAYOUTS = {
  marquee: LayoutMarquee,
  echo: LayoutEcho,
  spine: LayoutSpine,
  ransom: LayoutRansom,
  ticket: LayoutTicket,
  tracklist: LayoutTracklist,
  swiss: LayoutSwiss,
  contour: LayoutContour,
  wavy: LayoutWavy,
  player: LayoutPlayer,
  soundwave: LayoutSoundwave,
  receipt: LayoutReceipt,
  vinyl: LayoutVinyl,
  lineup: LayoutLineup,
  typewriter: LayoutTypewriter,
  neon: LayoutNeon,
};
