// templates-morph.jsx — Templates that host Mediaset Infinity brand-animation
// iframes (mediaset-morph, in-dot-cloud, infinity-dot-cloud). Used for the
// cover (slide 01), the IN reveal (slide 26), and the IN roadmap (slide 27).
//
// All three iframes are self-contained Three.js scenes that strip their own
// chrome via ?embed=1 and inherit the host background as transparent.
//
// Slide data shape:
//   { template: 'Morph',
//     scheme:   'purpleDeep' | 'nearBlack' | ...,
//     iframeSrc: 'mediaset-morph/index.html?embed=1&state=auto&color=brand',
//     kicker, title, subtitle, footer (optional)
//     iframeHeight: 600 (override default 540)
//     iframePosition: 'center' | 'top' (default 'center') }

function TemplateMorph({ slide, scheme }) {
  const {
    kicker, title, titleSize, subtitle, footer, footerMeta = [],
    iframeSrc, iframeHeight = 460, iframeWidth = 1300,
    iframeTop = 140,
    bottomCard,
  } = slide;

  const iframeRef = React.useRef(null);
  const [iframeLoaded, setIframeLoaded] = React.useState(false);

  // Hide the morph file's chrome (#ui / #morphbar / #brand / #hint) and only
  // fade the iframe in AFTER the embedded morph has actually drawn its first
  // frame. The morph file posts {type:'morph-ready'} on its first real
  // render; until then the iframe stays at opacity:0 so the default-white
  // about:blank/pre-render canvas can never flash through the slide.
  React.useEffect(() => {
    const el = iframeRef.current;
    if (!el) return;
    const stripChrome = () => {
      try {
        const doc = el.contentDocument;
        if (!doc) return;
        ['ui', 'morphbar', 'brand', 'hint', 'vignette'].forEach(id => {
          const node = doc.getElementById(id);
          if (node) node.style.display = 'none';
        });
      } catch (_) {}
    };
    const onMessage = (e) => {
      if (e.source === el.contentWindow && e.data && e.data.type === 'morph-ready') {
        stripChrome();
        setIframeLoaded(true);
      }
    };
    window.addEventListener('message', onMessage);
    el.addEventListener('load', stripChrome);
    // Fallback: if the message never arrives (e.g. stale embed without the
    // postMessage hook), reveal after 1500ms so the slide isn't blank forever.
    const fallback = setTimeout(() => { stripChrome(); setIframeLoaded(true); }, 1500);
    return () => {
      window.removeEventListener('message', onMessage);
      el.removeEventListener('load', stripChrome);
      clearTimeout(fallback);
    };
  }, []);

  // Title placement: anchored to a fixed gap below the iframe, NOT to bottom.
  // This guarantees no overlap with the morph, regardless of title length.
  const titleTop = iframeTop + iframeHeight + 56;

  return (
    <SlideShell scheme={scheme}>
      {/* Big glow halo behind the morph for cinematic depth */}
      <div style={{
        position: 'absolute',
        left: '50%', top: iframeTop + iframeHeight / 2,
        width: 1800, height: 900,
        transform: 'translate(-50%, -50%)',
        background: `radial-gradient(ellipse, ${scheme.accent}30 0%, ${scheme.accent}10 35%, transparent 65%)`,
        mixBlendMode: 'screen',
        pointerEvents: 'none',
      }}/>

      {/* Kicker on top */}
      {kicker && (
        <div style={{
          position: 'absolute',
          left: 0, right: 0, top: 92,
          textAlign: 'center',
          fontFamily: Tokens.fontMono, fontSize: 13,
          color: scheme.accent, letterSpacing: '0.32em',
          textTransform: 'uppercase',
          fontWeight: Tokens.weight.semibold,
        }}>{kicker}</div>
      )}

      {/* The iframe — centered, transparent. Wrapper has a solid dark bg
          so any moment of browser-default white (before the morph file's
          embed-script makes its body transparent) is masked. The iframe
          itself starts invisible (opacity:0) and fades in on load. */}
      <div style={{
        position: 'absolute',
        left: '50%',
        top: iframeTop,
        transform: 'translateX(-50%)',
        width: iframeWidth, height: iframeHeight,
        pointerEvents: 'none',
      }}>
        <iframe
          ref={iframeRef}
          src={iframeSrc}
          title={title}
          allowtransparency="true"
          loading="eager"
          style={{
            width: '100%', height: '100%',
            border: 0, background: 'transparent',
            opacity: iframeLoaded ? 1 : 0,
            transition: 'opacity 240ms ease-out',
            // Brighten + saturate the dots without modifying the morph file.
            filter: 'brightness(1.22) saturate(1.18)',
          }}
        />
      </div>

      {/* Title overlay — anchored relative to iframe bottom, never overlaps */}
      <div style={{
        position: 'absolute',
        left: 80, right: 80, top: titleTop,
        textAlign: 'center',
        zIndex: 2,
      }}>
        <div style={{
          fontFamily: Tokens.fontDisplay,
          fontSize: titleSize || (title && title.length > 30 ? 56 : 72),
          fontWeight: Tokens.weight.black,
          color: scheme.text,
          letterSpacing: '-0.025em',
          textTransform: 'uppercase',
          lineHeight: 0.95,
        }}>{renderTitle(title)}</div>
        {subtitle && (
          <div style={{
            marginTop: 18,
            fontFamily: Tokens.fontDisplay,
            fontSize: 17,
            fontWeight: Tokens.weight.regular,
            color: scheme.textDim,
            lineHeight: 1.55,
            maxWidth: 980,
            margin: '18px auto 0',
          }}>{subtitle}</div>
        )}
      </div>

      {/* Bottom card (pillars/roadmap, optional) */}
      {bottomCard && (
        <div style={{
          position: 'absolute',
          left: 80, right: 80, bottom: 180,
          zIndex: 2,
        }}>
          {bottomCard}
        </div>
      )}

      {/* Footer meta strip — small caps line at the bottom */}
      {footerMeta.length > 0 && (
        <div style={{
          position: 'absolute',
          left: 0, right: 0, bottom: 140,
          textAlign: 'center',
          fontFamily: Tokens.fontMono, fontSize: 11,
          color: scheme.textDim, letterSpacing: '0.30em',
          textTransform: 'uppercase',
          fontWeight: Tokens.weight.semibold,
        }}>
          {footerMeta.map((m, i) => (
            <React.Fragment key={i}>
              {i > 0 && <span style={{ color: scheme.accent, margin: '0 14px' }}>·</span>}
              {m}
            </React.Fragment>
          ))}
        </div>
      )}

      {footer && (
        <div style={{ position: 'absolute', right: 80, bottom: 140, whiteSpace: 'nowrap' }}>
          <SourceCite>{footer}</SourceCite>
        </div>
      )}
    </SlideShell>
  );
}

// ─────────────────────────────────────────────────────────────────────────
// TemplateAppendix — Q aperte (Tier 1, Tier 2, Q chiuse)
//   • 3-column layout, monospace headings, list of bullets
//   • Used for the final appendix slide
// ─────────────────────────────────────────────────────────────────────────
function TemplateAppendix({ slide, scheme }) {
  const { kicker, title, subtitle, columns = [], source } = slide;
  return (
    <SlideShell scheme={scheme}>
      <div style={{
        position: 'absolute',
        left: 80, right: 80, top: 140,
      }}>
        {kicker && (
          <div style={{
            fontFamily: Tokens.fontMono, fontSize: 13,
            color: scheme.accent, letterSpacing: '0.32em',
            textTransform: 'uppercase',
            marginBottom: 18,
            fontWeight: Tokens.weight.semibold,
          }}>{kicker}</div>
        )}
        <div style={{
          fontFamily: Tokens.fontDisplay,
          fontSize: 60, fontWeight: Tokens.weight.black,
          color: scheme.text,
          letterSpacing: '-0.025em',
          textTransform: 'uppercase',
          lineHeight: 0.95,
        }}>{renderTitle(title)}</div>
        {subtitle && (
          <div style={{
            marginTop: 16,
            fontFamily: Tokens.fontDisplay,
            fontSize: 18,
            color: scheme.textDim,
            lineHeight: 1.5,
            maxWidth: 1200,
          }}>{subtitle}</div>
        )}
      </div>

      <div style={{
        position: 'absolute',
        left: 80, right: 80, top: 390, bottom: 200,
        display: 'grid',
        gridTemplateColumns: `repeat(${columns.length}, 1fr)`,
        gap: 24,
      }}>
        {columns.map((col, i) => (
          <div key={i} style={{
            padding: 28,
            background: col.highlight ? `${col.color || scheme.accent}12` : 'rgba(255,255,255,0.03)',
            border: `1px solid ${col.highlight ? (col.color || scheme.accent) : Tokens.inkLine}`,
            borderLeft: `3px solid ${col.color || scheme.accent}`,
            borderRadius: Tokens.radius.lg,
          }}>
            <div style={{
              fontFamily: Tokens.fontMono, fontSize: 12,
              color: col.color || scheme.accent,
              letterSpacing: '0.28em',
              fontWeight: Tokens.weight.bold,
              textTransform: 'uppercase',
              marginBottom: 18,
            }}>{col.header}</div>
            <div style={{
              display: 'flex', flexDirection: 'column', gap: 12,
            }}>
              {(col.items || []).map((it, k) => (
                <div key={k} style={{
                  display: 'flex', gap: 12,
                  fontFamily: Tokens.fontDisplay, fontSize: 14,
                  color: scheme.text,
                  lineHeight: 1.5,
                }}>
                  <span style={{
                    color: col.color || scheme.accent,
                    fontWeight: Tokens.weight.bold,
                    fontFamily: Tokens.fontMono,
                    fontSize: 12,
                    minWidth: 22,
                    paddingTop: 2,
                  }}>{it.tag || `${String(k + 1).padStart(2, '0')}.`}</span>
                  <span><strong style={{ color: scheme.text, fontWeight: 600 }}>{it.title}</strong>
                    {it.detail && <span style={{ color: scheme.textDim }}> · {it.detail}</span>}
                  </span>
                </div>
              ))}
            </div>
          </div>
        ))}
      </div>

      {source && (
        <div style={{ position: 'absolute', right: 80, bottom: 130, whiteSpace: 'nowrap' }}>
          <SourceCite>{source}</SourceCite>
        </div>
      )}
    </SlideShell>
  );
}

// ─────────────────────────────────────────────────────────────────────────
// TemplateMorphScroll — SCROLL-SNAP container with stacked pages.
// Hero + N sections + optional moreStories, each a 100%-height block.
// Wheel/trackpad scrolls naturally (content slides up, next slides in from
// bottom = continuità) and the container snaps to each section start. No
// fade, just real scroll motion with mandatory snap stops.
//
// Hero supports two modes via slide.heroType:
//   'morph' (default if iframeSrc)  — embedded Three.js morph iframe + title
//   'globe'                         — Globe (earth + arcing cards) + title
//   'static' (default if no iframe) — just kicker + title + subtitle centered
// ─────────────────────────────────────────────────────────────────────────
function TemplateMorphScroll({ slide, scheme, onJump, onSectionChange }) {
  const {
    kicker, title, subtitle, storyNum,
    iframeSrc, iframeWidth = 1280, iframeHeight = 460, iframeTop = 130,
    heroType, heroThumbs = [],
    sections = [], moreStories = [], source,
  } = slide;
  const _heroType = heroType || (iframeSrc ? 'morph' : 'static');

  const scrollRef = React.useRef(null);
  const iframeRef = React.useRef(null);
  const [iframeLoaded, setIframeLoaded] = React.useState(!iframeSrc);
  const [activeIdx, setActiveIdx] = React.useState(0);
  const lastReportedRef = React.useRef(null);
  const totalPages = 1 + sections.length + (moreStories.length > 0 ? 1 : 0);

  // Hide morph chrome + reveal iframe only after the first real frame.
  React.useEffect(() => {
    if (!iframeSrc) return;
    const el = iframeRef.current;
    if (!el) return;
    const stripChrome = () => {
      try {
        const doc = el.contentDocument;
        if (!doc) return;
        ['ui', 'morphbar', 'brand', 'hint', 'vignette'].forEach(id => {
          const node = doc.getElementById(id);
          if (node) node.style.display = 'none';
        });
      } catch (_) {}
    };
    const onMessage = (e) => {
      if (e.source === el.contentWindow && e.data && e.data.type === 'morph-ready') {
        stripChrome();
        setIframeLoaded(true);
      }
    };
    window.addEventListener('message', onMessage);
    el.addEventListener('load', stripChrome);
    const fallback = setTimeout(() => { stripChrome(); setIframeLoaded(true); }, 1500);
    return () => {
      window.removeEventListener('message', onMessage);
      el.removeEventListener('load', stripChrome);
      clearTimeout(fallback);
    };
  }, [iframeSrc]);

  // Scroll tracking: which page is most centered in viewport?
  // Triggers activeIdx update + onSectionChange bucket callback.
  React.useEffect(() => {
    const el = scrollRef.current;
    if (!el) return;
    const onScroll = () => {
      const pageEls = el.querySelectorAll('[data-page-idx]');
      const vpCenter = el.scrollTop + el.clientHeight / 2;
      const elR = el.getBoundingClientRect();
      let bestIdx = 0, bestDist = Infinity;
      pageEls.forEach(p => {
        const i = parseInt(p.dataset.pageIdx, 10);
        const r = p.getBoundingClientRect();
        const center = r.top - elR.top + el.scrollTop + r.height / 2;
        const d = Math.abs(center - vpCenter);
        if (d < bestDist) { bestDist = d; bestIdx = i; }
      });
      setActiveIdx(bestIdx);
      let bucketId = null;
      if (bestIdx >= 1 && bestIdx <= sections.length) {
        bucketId = sections[bestIdx - 1]?.bucketId ?? null;
      }
      if (bucketId !== lastReportedRef.current) {
        lastReportedRef.current = bucketId;
        if (onSectionChange) onSectionChange(bucketId);
      }
    };
    el.addEventListener('scroll', onScroll, { passive: true });
    onScroll();
    return () => el.removeEventListener('scroll', onScroll);
  }, [sections.length, onSectionChange]);

  const goTo = (idx) => {
    const el = scrollRef.current;
    if (!el) return;
    const target = el.querySelector(`[data-page-idx="${idx}"]`);
    if (target) target.scrollIntoView({ behavior: 'smooth', block: 'start' });
  };

  // Page wrapper — each section is a full-height snap stop.
  // scrollSnapStop:'normal' allows fast-wheel to skip past intermediate
  // sections; the user can rest on any but isn't forced to stop on every.
  const pageStyle = {
    position: 'relative',
    width: '100%',
    height: '100%',
    minHeight: '100%',
    scrollSnapAlign: 'start',
    scrollSnapStop: 'normal',
    flexShrink: 0,
  };

  return (
    <SlideShell scheme={scheme}>
      <div ref={scrollRef} style={{
        position: 'absolute', inset: 0,
        overflowY: 'auto',
        overflowX: 'hidden',
        scrollBehavior: 'smooth',
        // 'proximity' (not 'mandatory') so the browser respects free wheel
        // scroll motion — content scrolls up smoothly, next section emerges
        // from below — and only gently snaps to a section when the user
        // comes to rest near one. This gives the "sensazione di continuità"
        // the user asked for, with tap-stops as the resting position.
        scrollSnapType: 'y proximity',
        scrollbarWidth: 'none',
        msOverflowStyle: 'none',
      }}>
        {/* Hero — page 0 */}
        <div data-page-idx={0} style={pageStyle}>
          {/* Aurora glow */}
          <div style={{
            position: 'absolute',
            left: '50%', top: _heroType === 'morph' ? iframeTop + iframeHeight / 2 : '50%',
            width: 1800, height: 900,
            transform: 'translate(-50%, -50%)',
            background: `radial-gradient(ellipse, ${scheme.accent}30 0%, ${scheme.accent}10 35%, transparent 65%)`,
            mixBlendMode: 'screen',
            pointerEvents: 'none',
          }}/>

          {kicker && (
            <div style={{
              position: 'absolute',
              left: 0, right: 0, top: 92,
              textAlign: 'center',
              fontFamily: Tokens.fontMono, fontSize: 13,
              color: scheme.accent, letterSpacing: '0.32em',
              textTransform: 'uppercase',
              fontWeight: Tokens.weight.semibold,
            }}>{kicker}</div>
          )}

          {/* Morph iframe */}
          {_heroType === 'morph' && iframeSrc && (
            <div style={{
              position: 'absolute',
              left: '50%', top: iframeTop,
              transform: 'translateX(-50%)',
              width: iframeWidth, height: iframeHeight,
              pointerEvents: 'none',
            }}>
              <iframe
                ref={iframeRef}
                src={iframeSrc}
                title={title}
                allowtransparency="true"
                loading="eager"
                style={{
                  width: '100%', height: '100%',
                  border: 0, background: 'transparent',
                  opacity: iframeLoaded ? 1 : 0,
                  transition: 'opacity 240ms ease-out',
                  filter: 'brightness(1.22) saturate(1.18)',
                }}
              />
            </div>
          )}

          {/* Globe hero (earth + arcing thumbs) */}
          {_heroType === 'globe' && (
            <HeroGlobeArc thumbs={heroThumbs} scheme={scheme} storyNum={storyNum}/>
          )}

          {/* Title block */}
          <div style={{
            position: 'absolute',
            left: 80, right: 80,
            top: _heroType === 'morph' ? iframeTop + iframeHeight + 56
                : _heroType === 'globe' ? 720
                : '50%',
            transform: _heroType === 'static' ? 'translateY(-50%)' : 'none',
            textAlign: 'center',
          }}>
            <div style={{
              fontFamily: Tokens.fontDisplay,
              fontSize: _heroType === 'static'
                ? (title && title.length > 30 ? 84 : 108)
                : (title && title.length > 30 ? 56 : 72),
              fontWeight: Tokens.weight.black,
              color: scheme.text,
              letterSpacing: '-0.025em',
              textTransform: 'uppercase',
              lineHeight: 0.95,
            }}>{renderTitle(title)}</div>
            {subtitle && (
              <div style={{
                marginTop: 22,
                fontFamily: Tokens.fontDisplay,
                fontSize: _heroType === 'static' ? 22 : 17,
                fontWeight: Tokens.weight.regular,
                color: scheme.textDim,
                lineHeight: 1.5,
                maxWidth: 1100,
                margin: '22px auto 0',
                whiteSpace: 'pre-line',
              }}>{subtitle}</div>
            )}
          </div>
        </div>

        {/* Sections — pages 1..N. Decorative connector between each pair
            so user feels visual continuity as scroll passes through. */}
        {sections.map((s, i) => {
          const type = s.type || 'bucket';
          return (
            <React.Fragment key={i}>
              <div data-page-idx={i + 1} style={pageStyle}>
                {type === 'bullet'
                  ? <BulletPage section={s} scheme={scheme}/>
                  : type === 'action'
                  ? <ActionPage section={s} scheme={scheme}/>
                  : <BucketPage section={s} scheme={scheme} onJump={onJump}/>}
              </div>
              {i < sections.length - 1 && (
                <div aria-hidden="true" style={{
                  height: 96,
                  flexShrink: 0,
                  position: 'relative',
                  display: 'flex',
                  flexDirection: 'column',
                  alignItems: 'center',
                  justifyContent: 'center',
                  gap: 10,
                  opacity: 0.7,
                }}>
                  <div style={{
                    width: 1.5, height: 32,
                    background: `linear-gradient(to bottom, transparent, rgba(255,255,255,0.45))`,
                  }}/>
                  <div style={{
                    width: 6, height: 6, borderRadius: '50%',
                    background: 'rgba(255,255,255,0.6)',
                  }}/>
                  <div style={{
                    width: 1.5, height: 32,
                    background: `linear-gradient(to bottom, rgba(255,255,255,0.45), transparent)`,
                  }}/>
                </div>
              )}
            </React.Fragment>
          );
        })}

        {/* moreStories — last page */}
        {moreStories.length > 0 && (
          <div data-page-idx={sections.length + 1} style={pageStyle}>
            <div style={{
              position: 'absolute',
              left: 80, right: 80, top: 0, bottom: 0,
              display: 'flex', flexDirection: 'column', justifyContent: 'center',
            }}>
              <div style={{
                fontFamily: Tokens.fontMono, fontSize: 13,
                color: scheme.textDim, letterSpacing: '0.32em',
                textTransform: 'uppercase',
                marginBottom: 28, textAlign: 'center',
                fontWeight: Tokens.weight.semibold,
              }}>More stories for you</div>
              <div style={{
                display: 'grid',
                gridTemplateColumns: moreStories.length >= 4 ? 'repeat(4, 1fr)' : 'repeat(2, 1fr)',
                gap: 18,
                maxWidth: 1560, margin: '0 auto', width: '100%',
              }}>
                {moreStories.map((ms, mi) => (
                  <button key={mi}
                    className="thumb-card-outer"
                    onClick={() => onJump && ms.slideIdx != null && onJump(ms.slideIdx)}
                    style={{
                      position: 'relative',
                      height: 280,
                      cursor: 'pointer',
                      background: 'none', padding: 0, textAlign: 'left',
                      border: 'none',
                    }}>
                    <div
                      className="thumb-card-inner"
                      style={{
                        borderRadius: Tokens.radius.xl,
                        overflow: 'hidden',
                        border: `1px solid ${Tokens.inkLine}`,
                        position: 'relative',
                        boxShadow: '0 14px 40px rgba(0,0,0,0.45)',
                      }}
                      onMouseEnter={(e) => { e.currentTarget.style.boxShadow = `0 24px 70px rgba(0,0,0,0.65), 0 0 0 2px ${scheme.accent}`; }}
                      onMouseLeave={(e) => { e.currentTarget.style.boxShadow = '0 14px 40px rgba(0,0,0,0.45)'; }}
                    >
                      <div style={{
                        position: 'absolute', inset: 0,
                        backgroundImage: ms.thumb ? `url(${ms.thumb})` : 'none',
                        backgroundSize: 'cover',
                        backgroundPosition: ms.imagePos || 'center',
                        backgroundColor: Tokens.bgSurface,
                      }}/>
                      <div style={{
                        position: 'absolute', inset: 0,
                        background: 'linear-gradient(0deg, rgba(0,0,0,0.85) 0%, rgba(0,0,0,0.3) 60%, transparent 100%)',
                      }}/>
                      <div style={{ position: 'absolute', left: 24, bottom: 22, right: 24 }}>
                        <div style={{
                          fontFamily: Tokens.fontMono, fontSize: 11,
                          color: scheme.accent, letterSpacing: '0.24em',
                          textTransform: 'uppercase',
                          marginBottom: 6,
                          fontWeight: Tokens.weight.semibold,
                        }}>Story {String((ms.slideIdx ?? 0) + 1).padStart(2, '0')} →</div>
                        <div style={{
                          fontFamily: Tokens.fontDisplay, fontSize: 22,
                          fontWeight: Tokens.weight.extrabold,
                          color: Tokens.ink, letterSpacing: '-0.015em',
                          lineHeight: 1.1,
                          textTransform: 'uppercase',
                        }}>{ms.shortLabel}</div>
                      </div>
                    </div>
                  </button>
                ))}
              </div>
            </div>
          </div>
        )}
      </div>

      {/* Page indicator dots — right side, fixed (outside scroll) */}
      <div style={{
        position: 'absolute',
        right: 36, top: '50%',
        transform: 'translateY(-50%)',
        display: 'flex', flexDirection: 'column', gap: 12,
        zIndex: 40,
      }}>
        {Array.from({ length: totalPages }).map((_, i) => (
          <button key={i}
            onClick={() => goTo(i)}
            aria-label={`Page ${i + 1}`}
            style={{
              width: i === activeIdx ? 24 : 10,
              height: 10,
              borderRadius: Tokens.radius.pill,
              background: i === activeIdx ? scheme.text : Tokens.inkFaint,
              border: 'none', cursor: 'pointer',
              transition: 'all 260ms cubic-bezier(.4,0,.2,1)',
              padding: 0,
            }}/>
        ))}
      </div>

      {/* "Scorri" hint — visible only on hero, fades when user advances */}
      <div style={{
        position: 'absolute',
        left: 0, right: 0, bottom: 36,
        textAlign: 'center',
        opacity: activeIdx === 0 ? 0.7 : 0,
        transition: 'opacity 350ms',
        pointerEvents: 'none',
        zIndex: 30,
      }}>
        <div style={{
          display: 'inline-flex', alignItems: 'center', gap: 10,
          fontFamily: Tokens.fontMono, fontSize: 11,
          color: scheme.textDim, letterSpacing: '0.32em',
          textTransform: 'uppercase',
        }}>
          Scorri
          <span style={{ animation: 'scrollNudge 1.6s ease-in-out infinite' }}>↓</span>
        </div>
      </div>
    </SlideShell>
  );
}

// HeroGlobeArc — simplified Globe hero rendered inside MorphScroll page 0.
// Earth + arc of card thumbs above it, identical to TemplateGlobe but
// without the surrounding image-box and STORY label (those go in the
// regular title block of MorphScroll's hero).
function HeroGlobeArc({ thumbs = [], scheme, storyNum }) {
  const stageW = 1920;
  const earthCx = stageW / 2;
  const earthCy = 690;
  const earthR = 280;
  const arcR = earthR + 40;
  const N = thumbs.length;
  const arcSpanDeg = 90;
  const arcStartDeg = -135;

  const cardData = thumbs.map((t, i) => {
    const u = N === 1 ? 0.5 : i / (N - 1);
    const angDeg = arcStartDeg + u * arcSpanDeg;
    const angRad = angDeg * Math.PI / 180;
    const cx = earthCx + Math.cos(angRad) * arcR;
    const cy = earthCy + Math.sin(angRad) * arcR;
    const rot = angDeg + 90;
    return { ...t, cx, cy, rot };
  });

  return (
    <div style={{
      position: 'absolute',
      left: 220, right: 220, top: 100,
      height: 580,
      background: '#020610',
      borderRadius: Tokens.radius.lg,
      overflow: 'hidden',
    }}>
      {/* Earth sphere */}
      <div style={{
        position: 'absolute',
        left: '50%', top: '95%',
        width: earthR * 2, height: earthR * 2,
        transform: 'translate(-50%, -50%)',
        borderRadius: '50%',
        background: `radial-gradient(circle at 50% 35%,
          rgba(140,200,255,0.6) 0%,
          rgba(60,110,180,0.85) 25%,
          rgba(20,50,110,0.95) 55%,
          #0A1F4A 80%,
          #050E28 100%)`,
        boxShadow: `inset 0 0 60px rgba(0,0,0,0.4),
                    inset 30px -30px 100px rgba(0,0,0,0.4),
                    0 0 100px rgba(100,180,255,0.25)`,
      }}/>

      {/* Top rim glow */}
      <div style={{
        position: 'absolute',
        left: '50%', top: 'calc(95% - 280px)',
        width: 600, height: 100,
        transform: 'translate(-50%, 0)',
        background: `linear-gradient(180deg, ${scheme.accent}cc 0%, ${scheme.accent}33 60%, transparent 100%)`,
        filter: 'blur(14px)',
        opacity: 0.85,
        pointerEvents: 'none',
        borderRadius: '50% 50% 0 0',
      }}/>

      {/* Arcing cards */}
      {cardData.map((c, i) => {
        const w = c.w || 130;
        const h = c.h || 180;
        return (
          <div key={i} style={{
            position: 'absolute',
            left: c.cx - 220, top: c.cy - 100,
            width: w, height: h,
            transform: `translate(-50%, -50%) rotate(${c.rot}deg)`,
            borderRadius: Tokens.radius.lg,
            overflow: 'hidden',
            boxShadow: '0 20px 60px rgba(0,0,0,0.7)',
            border: `1px solid rgba(255,255,255,0.12)`,
          }}>
            <img src={c.src} alt="" style={{
              width: '100%', height: '100%', objectFit: 'cover', display: 'block',
            }}/>
            <div style={{
              position: 'absolute', inset: 0,
              background: 'linear-gradient(0deg, rgba(0,0,0,0.55) 0%, transparent 45%)',
            }}/>
          </div>
        );
      })}

      {/* Optional STORY label centered below arc */}
      {storyNum != null && (
        <div style={{
          position: 'absolute',
          left: 0, right: 0, bottom: 24,
          textAlign: 'center',
        }}>
          <StoryLabel num={storyNum} color={scheme.storyLabel}/>
        </div>
      )}
    </div>
  );
}

// BucketPage — fade-in page rendering one bucket section
// (stat + label + body + 4 thumbs at corner positions).
// BulletPage — single numbered bullet point per page (number + uppercase
// heading + body paragraph + optional source). Used by per-action slides
// like #06 European IP Studio where each Cardani sub-point becomes its
// own carousel page. Quiet, deliberate, one-at-a-time reading rhythm.
function BulletPage({ section, scheme }) {
  const { num, heading, body, image, imagePos = 'center 25%', source, sourceMeta } = section;
  const hasImage = !!image;
  return (
    <div style={{ position: 'absolute', inset: 0 }}>
      {/* Optional image — right side, large, soft fade */}
      {hasImage && (
        <div style={{
          position: 'absolute',
          right: 0, top: 0, bottom: 0, width: '40%',
          backgroundImage: `url(${image})`,
          backgroundSize: 'cover',
          backgroundPosition: imagePos,
          opacity: 0.45,
          maskImage: 'linear-gradient(to left, rgba(0,0,0,1) 0%, rgba(0,0,0,0) 90%)',
          WebkitMaskImage: 'linear-gradient(to left, rgba(0,0,0,1) 0%, rgba(0,0,0,0) 90%)',
        }}/>
      )}

      {/* Big number — top-left */}
      <div style={{
        position: 'absolute', left: 80, top: 96,
        fontFamily: Tokens.fontMono, fontSize: 14,
        color: scheme.accent, letterSpacing: '0.32em',
        fontWeight: Tokens.weight.bold,
      }}>{num}</div>

      {/* Heading */}
      <div style={{
        position: 'absolute',
        left: 80, top: 152,
        right: hasImage ? 720 : 80,
        fontFamily: Tokens.fontDisplay,
        fontSize: heading && heading.length > 36 ? 48 : heading && heading.length > 22 ? 60 : 72,
        fontWeight: Tokens.weight.black,
        color: scheme.text,
        letterSpacing: '-0.02em',
        textTransform: 'uppercase',
        lineHeight: 0.95,
      }}>{heading}</div>

      {/* Body paragraph */}
      {body && (
        <div style={{
          position: 'absolute',
          left: 80, top: 400,
          right: hasImage ? 720 : 80,
          fontFamily: Tokens.fontDisplay,
          fontSize: 19, fontWeight: Tokens.weight.regular,
          color: scheme.text,
          lineHeight: 1.55,
          maxWidth: 1100,
          // Drop-cap-ish weight contrast: first sentence pop, rest body
          letterSpacing: '0.005em',
        }}>{body}</div>
      )}

      {/* Optional meta line (small caps right below body) */}
      {sourceMeta && (
        <div style={{
          position: 'absolute',
          left: 80, bottom: 110,
          fontFamily: Tokens.fontMono, fontSize: 12,
          color: scheme.accent, letterSpacing: '0.22em',
          textTransform: 'uppercase',
          fontWeight: Tokens.weight.semibold,
        }}>{sourceMeta}</div>
      )}

      {/* Source bottom-left */}
      {source && (
        <div style={{ position: 'absolute', left: 80, bottom: 60 }}>
          <SourceCite>{source}</SourceCite>
        </div>
      )}
    </div>
  );
}

// ActionPage — fade-in page rendering a single bucket action with rich
// content: kicker, large title, 4 numbered text blocks in 2-col grid,
// optional killer stat (bottom right) and side image (top right).
// Used when consolidating multiple bucket actions into a single carousel.
function ActionPage({ section, scheme }) {
  const { kicker, title, blocks = [], killerNumber, killerPrefix, killerSuffix, killerLabel, image, source } = section;
  const hasImage = !!image;
  return (
    <div style={{ position: 'absolute', inset: 0 }}>
      {kicker && (
        <div style={{
          position: 'absolute', left: 80, top: 84,
          fontFamily: Tokens.fontMono, fontSize: 13,
          color: scheme.accent, letterSpacing: '0.28em',
          textTransform: 'uppercase',
          fontWeight: Tokens.weight.semibold,
        }}>{kicker}</div>
      )}

      {title && (
        <div style={{
          position: 'absolute',
          left: 80, top: 130,
          right: hasImage ? 720 : 80,
          fontFamily: Tokens.fontDisplay,
          fontSize: title.length > 32 ? 52 : 64,
          fontWeight: Tokens.weight.black,
          color: scheme.text,
          letterSpacing: '-0.025em',
          textTransform: 'uppercase',
          lineHeight: 0.95,
        }}>{renderTitle(title)}</div>
      )}

      {hasImage && (
        <div style={{
          position: 'absolute',
          right: 80, top: 110,
          width: 560, height: 360,
          borderRadius: Tokens.radius.lg,
          backgroundImage: `url(${image})`,
          backgroundSize: 'cover',
          backgroundPosition: 'center 25%',
          opacity: 0.92,
          boxShadow: '0 20px 60px rgba(0,0,0,0.45)',
          border: `1px solid ${Tokens.inkLine}`,
        }}/>
      )}

      {/* 4 numbered blocks in 2-col grid */}
      {blocks.length > 0 && (
        <div style={{
          position: 'absolute',
          left: 80, right: 80, top: hasImage ? 520 : 360, bottom: 200,
          display: 'grid',
          gridTemplateColumns: '1fr 1fr',
          gap: 20,
          maxWidth: 1500,
          alignContent: 'start',
        }}>
          {blocks.slice(0, 4).map((b, i) => (
            <div key={i} style={{
              padding: '20px 24px',
              borderLeft: `2px solid ${scheme.accent}`,
              background: 'rgba(255,255,255,0.03)',
              borderRadius: Tokens.radius.md,
            }}>
              <div style={{
                fontFamily: Tokens.fontMono, fontSize: 11,
                color: scheme.accent, letterSpacing: '0.20em',
                textTransform: 'uppercase',
                fontWeight: Tokens.weight.semibold,
                marginBottom: 8,
              }}>{String(i + 1).padStart(2, '0')} · {b.h}</div>
              <div style={{
                fontFamily: Tokens.fontDisplay, fontSize: 14,
                color: scheme.text, lineHeight: 1.55,
              }}>{b.p}</div>
            </div>
          ))}
        </div>
      )}

      {killerNumber && (
        <div style={{
          position: 'absolute',
          right: 80, bottom: 100,
          textAlign: 'right',
          maxWidth: 460,
        }}>
          <div style={{
            display: 'inline-flex', alignItems: 'baseline',
            fontFamily: Tokens.fontDisplay,
            fontWeight: Tokens.weight.black,
            color: scheme.accent,
            letterSpacing: '-0.04em',
            lineHeight: 0.9,
          }}>
            {killerPrefix && <span style={{ fontSize: 36, marginRight: 4 }}>{killerPrefix}</span>}
            <span style={{ fontSize: 72 }}>{killerNumber}</span>
            {killerSuffix && <span style={{ fontSize: 36, marginLeft: 4 }}>{killerSuffix}</span>}
          </div>
          {killerLabel && (
            <div style={{
              fontFamily: Tokens.fontDisplay, fontSize: 13,
              color: scheme.textDim, lineHeight: 1.45,
              marginTop: 6,
            }}>{killerLabel}</div>
          )}
        </div>
      )}

      {source && (
        <div style={{ position: 'absolute', left: 80, bottom: 60 }}>
          <SourceCite>{source}</SourceCite>
        </div>
      )}
    </div>
  );
}

function BucketPage({ section, scheme, onJump }) {
  const { stat, label, body, thumbs = [] } = section;
  return (
    <div style={{ position: 'absolute', inset: 0 }}>
      {/* Centered stat + label + body */}
      <div style={{
        position: 'absolute',
        left: '50%', top: '50%',
        transform: 'translate(-50%, -50%)',
        textAlign: 'center',
        maxWidth: 900,
        pointerEvents: 'none',
        zIndex: 1,
      }}>
        <div style={{
          fontFamily: Tokens.fontDisplay, fontWeight: Tokens.weight.black,
          fontSize: 220,
          color: scheme.accent,
          letterSpacing: '-0.05em', lineHeight: 0.9,
        }}>{stat}</div>
        {label && (
          <div style={{
            marginTop: 12,
            fontFamily: Tokens.fontDisplay,
            fontSize: 28, fontWeight: Tokens.weight.semibold,
            color: scheme.text,
            letterSpacing: '0.01em',
            textTransform: 'uppercase',
          }}>{label}</div>
        )}
        {body && (
          <div style={{
            marginTop: 22,
            fontFamily: Tokens.fontDisplay,
            fontSize: 18,
            color: scheme.textDim,
            lineHeight: 1.55,
            maxWidth: 760, margin: '22px auto 0',
          }}>{body}</div>
        )}
      </div>

      {/* Thumbs at corner positions (uses thumb.x/thumb.y).
          Top cards (y < 0.5) get higher z-index than bottom ones so
          when they're close together, the top card's bottom edge sits
          on top of the bottom card's top edge — not the other way
          around. Hover still bumps to z:200 (from thumb-card-outer CSS). */}
      {thumbs.map((t, i) => {
        const clickable = !!t.slideIdx;
        const cardW = t.w || 240;
        const cardH = t.h || 300;
        const isTopCard = (t.y ?? 0.5) < 0.5;
        return (
          <div key={i}
            className="thumb-card-outer"
            onClick={() => { if (clickable) onJump && onJump(t.slideIdx); }}
            style={{
              position: 'absolute',
              left: `${(t.x ?? 0.5) * 100}%`,
              top: `${(t.y ?? 0.5) * 100}%`,
              width: cardW, height: cardH,
              transform: `translate(-50%, -50%) rotate(${t.rotate || 0}deg)`,
              cursor: clickable ? 'pointer' : 'default',
              zIndex: isTopCard ? 4 : 2,
            }}>
            <div
              className="thumb-card-inner"
              style={{
                borderRadius: Tokens.radius.lg,
                overflow: 'hidden',
                boxShadow: '0 18px 50px rgba(0,0,0,0.55), 0 2px 6px rgba(0,0,0,0.35)',
                border: `1px solid ${Tokens.inkLine}`,
                position: 'relative',
              }}
              onMouseEnter={(e) => { if (clickable) e.currentTarget.style.boxShadow = `0 28px 80px rgba(0,0,0,0.75), 0 0 0 2px ${scheme.accent}`; }}
              onMouseLeave={(e) => { if (clickable) e.currentTarget.style.boxShadow = '0 18px 50px rgba(0,0,0,0.55), 0 2px 6px rgba(0,0,0,0.35)'; }}
            >
              <div style={{
                position: 'absolute', left: 0, right: 0, top: 0,
                height: cardH - 68,
                background: `url(${t.src}) center/cover no-repeat`,
              }}/>
              <div style={{
                position: 'absolute', left: 0, right: 0, bottom: 0,
                height: 68,
                background: 'linear-gradient(0deg, rgba(0,0,0,0.92) 0%, rgba(0,0,0,0.75) 100%)',
                padding: '10px 14px',
                display: 'flex', flexDirection: 'column', justifyContent: 'center',
                borderTop: `1px solid ${scheme.accent}44`,
              }}>
                {t.actionId && (
                  <div style={{
                    fontFamily: Tokens.fontMono, fontSize: 11,
                    color: scheme.accent, letterSpacing: '0.20em',
                    fontWeight: 700, marginBottom: 4,
                  }}>{t.actionId}</div>
                )}
                {t.label && (
                  <div style={{
                    fontFamily: Tokens.fontDisplay, fontSize: 14,
                    color: Tokens.ink, fontWeight: 700,
                    letterSpacing: '-0.005em',
                    lineHeight: 1.2,
                    overflow: 'hidden',
                    textOverflow: 'ellipsis',
                    whiteSpace: 'nowrap',
                  }}>{t.label}</div>
                )}
              </div>
            </div>
          </div>
        );
      })}
    </div>
  );
}

Object.assign(window, { TemplateMorph, TemplateMorphScroll, TemplateAppendix, BucketPage, BulletPage, ActionPage });
