// CTox EAD — scene kit. Data-driven scene renderers shared by every module.
// Depends on app-shell.jsx (tokens, AppShell, Cursor, SubSprite, SynviaLogo).

// ── local helpers (namespaced to avoid collisions) ─────────────────────────
function kitTyped(text, localT, startT, cps = 16) {
  const n = Math.min(text.length, Math.floor(Math.max(0, localT - startT) * cps));
  return text.slice(0, n);
}
function kitCursor(localT, wps) {
  if (!wps || !wps.length) return null;
  let i = 0;
  while (i < wps.length - 1 && localT >= wps[i + 1].t) i++;
  const a = wps[i], b = wps[Math.min(i + 1, wps.length - 1)];
  const span = b.t - a.t;
  const p = span > 0 ? clamp((localT - a.t) / span, 0, 1) : 1;
  const e = Easing.easeInOutCubic(p);
  return {
    x: a.x + (b.x - a.x) * e,
    y: a.y + (b.y - a.y) * e,
    clicking: wps.some(w => w.click && localT >= w.t && localT < w.t + 0.35),
  };
}
// standard enter/exit envelope
function kitFade(localTime, duration, inD = 0.45, outD = 0.4) {
  const i = clamp(localTime / inD, 0, 1);
  const o = localTime > duration - outD ? clamp((localTime - (duration - outD)) / outD, 0, 1) : 0;
  return { opacity: Math.min(i, 1 - o), inT: Easing.easeOutCubic(i) };
}
// per-item stagger
const kitStep = (localTime, i, delay = 0.5, gap = 0.28, dur = 0.5) =>
  Easing.easeOutCubic(clamp((localTime - delay - i * gap) / dur, 0, 1));

const TONE = {
  warn: { bg: '#FFF4D6', bar: '#E0A32E', ink: '#5C4310', chip: '⚠️', label: 'ATENÇÃO' },
  stop: { bg: '#FDE7E7', bar: '#D64545', ink: '#5E1616', chip: '🚫', label: 'PROIBIDO' },
  crit: { bg: '#2A0F0F', bar: '#FF5A5A', ink: '#FFE3E3', chip: '🔴', label: 'REGRA DE OURO', dark: true },
  ok:   { bg: '#E8F6EC', bar: '#2E9E5B', ink: '#0F3D22', chip: '✅', label: 'BOA PRÁTICA' },
  info: { bg: '#E9F1F5', bar: '#2E7C9E', ink: '#12313F', chip: 'ℹ️', label: 'INFORMAÇÃO' },
};

// ════════════════════════════════════════════════════════════════════════════
// Persistent HUD — keeps the "course" feel during content beats
// ════════════════════════════════════════════════════════════════════════════
function ModuleHUD({ mod, beatIx, beatCount, dark = false }) {
  const fg = dark ? 'rgba(255,255,255,0.82)' : 'rgba(28,65,55,0.72)';
  const bg = dark ? 'rgba(255,255,255,0.10)' : 'rgba(28,65,55,0.07)';
  return (
    <div style={{
      position: 'absolute', top: 34, right: 44, zIndex: 300,
      display: 'flex', alignItems: 'center', gap: 12,
      background: bg, borderRadius: 999, padding: '9px 18px 9px 14px',
      backdropFilter: 'blur(6px)',
    }}>
      <SynviaLogo size={20} color={dark ? '#fff' : CTX_BLUE} />
      <span style={{ fontSize: 15, fontWeight: 700, color: fg, letterSpacing: '0.04em' }}>
        MÓDULO {mod.n}
      </span>
      <span style={{ width: 1, height: 15, background: fg, opacity: 0.3 }} />
      <span data-tnum style={{ fontSize: 15, fontWeight: 700, color: fg, opacity: 0.8 }}>
        {beatIx}/{beatCount}
      </span>
    </div>
  );
}

// ════════════════════════════════════════════════════════════════════════════
// MODULE INTRO — module number, title, lesson list
// ════════════════════════════════════════════════════════════════════════════
function BeatModuleIntro({ mod }) {
  const { localTime, duration } = useSprite();
  const { opacity, inT } = kitFade(localTime, duration, 0.6, 0.5);
  return (
    <div style={{ position: 'absolute', inset: 0, background: CTX_BLUE, color: '#fff', opacity, overflow: 'hidden' }}>
      <div style={{ position: 'absolute', right: -180, bottom: -170, opacity: 0.055, filter: 'brightness(0) invert(1)' }}>
        <SynviaArrow size={880} />
      </div>
      <div style={{ position: 'absolute', left: 120, top: 132, transform: `translateY(${(1 - inT) * -14}px)`, opacity: inT,
        display: 'flex', alignItems: 'center', gap: 16 }}>
        <SynviaLogo size={38} color="#fff" />
        <span style={{ fontSize: 18, letterSpacing: '0.28em', fontWeight: 700, color: CTX_GREEN_LIME }}>
          TREINAMENTO CTOX · SYNVIA
        </span>
      </div>

      <div style={{ position: 'absolute', left: 120, top: 300, width: 900 }}>
        <div style={{ display: 'flex', alignItems: 'baseline', gap: 26,
          opacity: kitStep(localTime, 0, 0.2, 0, 0.6),
          transform: `translateY(${(1 - kitStep(localTime, 0, 0.2, 0, 0.6)) * 26}px)` }}>
          <span style={{ fontSize: 28, fontWeight: 700, letterSpacing: '0.18em', color: 'rgba(255,255,255,0.5)' }}>MÓDULO</span>
          <span data-tnum style={{ fontSize: 200, fontWeight: 800, lineHeight: 0.82, letterSpacing: '-0.045em' }}>{mod.n}</span>
        </div>
        <div style={{ fontSize: 68, fontWeight: 800, letterSpacing: '-0.028em', marginTop: 22, lineHeight: 1.05,
          opacity: kitStep(localTime, 1, 0.35, 0, 0.5), textWrap: 'balance' }}>{mod.title}</div>
        <div style={{ fontSize: 26, color: 'rgba(255,255,255,0.72)', marginTop: 16, maxWidth: 820, lineHeight: 1.45,
          opacity: kitStep(localTime, 2, 0.55, 0, 0.5), textWrap: 'pretty' }}>{mod.sub}</div>
      </div>

      {/* Topic list */}
      <div style={{ position: 'absolute', right: 120, top: 300, width: 620, display: 'flex', flexDirection: 'column', gap: 12 }}>
        {(mod.topics || []).map((tp, i) => {
          const t = kitStep(localTime, i, 0.6, 0.15, 0.5);
          return (
            <div key={tp} style={{ opacity: t, transform: `translateX(${(1 - t) * 34}px)`,
              display: 'flex', alignItems: 'center', gap: 18,
              background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.10)',
              borderRadius: 14, padding: '18px 24px' }}>
              <span data-tnum style={{ fontSize: 17, fontWeight: 800, color: CTX_GREEN_LIME, width: 26 }}>
                {String(i + 1).padStart(2, '0')}
              </span>
              <span style={{ fontSize: 23, fontWeight: 600, letterSpacing: '-0.01em' }}>{tp}</span>
            </div>
          );
        })}
      </div>

      <div style={{ position: 'absolute', left: 120, bottom: 100, display: 'flex', alignItems: 'center', gap: 16,
        opacity: kitStep(localTime, 0, 1.0, 0, 0.5) }}>
        <div style={{ background: 'rgba(255,255,255,0.08)', border: '1px solid rgba(255,255,255,0.14)',
          color: 'rgba(255,255,255,0.82)', borderRadius: 999, padding: '10px 22px',
          fontSize: 17, fontWeight: 700 }}>{mod.audience}</div>
      </div>
    </div>
  );
}

// ════════════════════════════════════════════════════════════════════════════
// LESSON CARD — divider inside a module
// ════════════════════════════════════════════════════════════════════════════
function BeatLesson({ beat }) {
  const { localTime, duration } = useSprite();
  const { opacity, inT } = kitFade(localTime, duration, 0.4, 0.4);
  const numIn = Easing.easeOutBack(clamp((localTime - 0.1) / 0.55, 0, 1));
  return (
    <div style={{ position: 'absolute', inset: 0, background: CTX_BLUE, color: '#fff', opacity, overflow: 'hidden',
      display: 'flex', alignItems: 'center' }}>
      <div style={{ position: 'absolute', right: -150, bottom: -140, opacity: 0.06, filter: 'brightness(0) invert(1)' }}>
        <SynviaArrow size={720} />
      </div>
      <div style={{ paddingLeft: 140, maxWidth: 1400 }}>
        <div style={{ display: 'flex', alignItems: 'baseline', gap: 24, opacity: numIn,
          transform: `translateY(${(1 - numIn) * 20}px)` }}>
          <span style={{ fontSize: 24, fontWeight: 700, letterSpacing: '0.2em', color: 'rgba(255,255,255,0.5)' }}>PARTE</span>
          <span data-tnum style={{ fontSize: 140, fontWeight: 800, lineHeight: 0.85, letterSpacing: '-0.04em', color: CTX_GREEN_LIME }}>
            {beat.n}
          </span>
        </div>
        <div style={{ fontSize: 74, fontWeight: 800, letterSpacing: '-0.03em', marginTop: 18, lineHeight: 1.04,
          opacity: inT, transform: `translateY(${(1 - inT) * 16}px)`, textWrap: 'balance' }}>{beat.title}</div>
        {beat.sub && (
          <div style={{ fontSize: 27, color: 'rgba(255,255,255,0.7)', marginTop: 18, maxWidth: 1050,
            opacity: kitStep(localTime, 0, 0.4, 0, 0.5), textWrap: 'pretty' }}>{beat.sub}</div>
        )}
      </div>
    </div>
  );
}

// ════════════════════════════════════════════════════════════════════════════
// BULLETS — kicker + title + list
// ════════════════════════════════════════════════════════════════════════════
function BeatBullets({ beat }) {
  const { localTime, duration } = useSprite();
  const { opacity, inT } = kitFade(localTime, duration);
  const dark = beat.dark;
  return (
    <div style={{ position: 'absolute', inset: 0, background: dark ? '#0E1A16' : '#F4F6F5',
      color: dark ? '#fff' : CTX_INK, opacity, overflow: 'hidden', padding: '100px 140px',
      display: 'flex', flexDirection: 'column', justifyContent: 'center' }}>
      {beat.kicker && (
        <div style={{ fontSize: 17, letterSpacing: '0.28em', fontWeight: 800,
          color: dark ? CTX_GREEN_LIME : CTX_BLUE, opacity: inT, marginBottom: 20 }}>{beat.kicker}</div>
      )}
      <div style={{ fontSize: 64, fontWeight: 800, letterSpacing: '-0.028em', lineHeight: 1.06, maxWidth: 1450,
        opacity: inT, transform: `translateY(${(1 - inT) * 16}px)`, textWrap: 'balance' }}>{beat.title}</div>
      {beat.lead && (
        <div style={{ fontSize: 26, marginTop: 20, maxWidth: 1250, lineHeight: 1.5,
          color: dark ? 'rgba(255,255,255,0.7)' : 'rgba(44,57,66,0.72)',
          opacity: kitStep(localTime, 0, 0.3, 0, 0.5), textWrap: 'pretty' }}>{beat.lead}</div>
      )}
      <div style={{ marginTop: 52, display: 'flex', flexDirection: 'column', gap: 18, maxWidth: 1500 }}>
        {beat.items.map((it, i) => {
          const t = kitStep(localTime, i, 0.6, 0.32);
          const txt = typeof it === 'string' ? it : it.t;
          const strong = typeof it === 'object' && it.strong;
          return (
            <div key={i} style={{ opacity: t, transform: `translateX(${(1 - t) * 26}px)`,
              display: 'flex', alignItems: 'flex-start', gap: 22 }}>
              <div style={{ width: 12, height: 12, borderRadius: 3, marginTop: 14, flexShrink: 0,
                background: strong ? CTX_RED : (dark ? CTX_GREEN_LIME : CTX_BLUE),
                transform: 'rotate(45deg)' }} />
              <div style={{ fontSize: 30, lineHeight: 1.4, fontWeight: strong ? 700 : 500,
                color: strong ? (dark ? '#FFB4B4' : '#B32020') : 'inherit', textWrap: 'pretty' }}>{txt}</div>
            </div>
          );
        })}
      </div>
    </div>
  );
}

// ════════════════════════════════════════════════════════════════════════════
// RULE — warning / prohibition / golden-rule panel
// ════════════════════════════════════════════════════════════════════════════
function BeatRule({ beat }) {
  const { localTime, duration } = useSprite();
  const { opacity } = kitFade(localTime, duration, 0.4, 0.4);
  const tone = TONE[beat.tone] || TONE.warn;
  const pop = Easing.easeOutBack(clamp(localTime / 0.55, 0, 1));
  return (
    <div style={{ position: 'absolute', inset: 0, background: tone.dark ? '#160707' : '#F4F6F5',
      opacity, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '0 130px' }}>
      <div style={{ width: '100%', maxWidth: 1520, background: tone.bg, color: tone.ink,
        borderLeft: `14px solid ${tone.bar}`, borderRadius: 18, padding: '58px 70px',
        transform: `scale(${0.94 + 0.06 * pop})`, opacity: clamp(localTime / 0.4, 0, 1),
        boxShadow: tone.dark ? 'none' : '0 24px 60px rgba(0,0,0,0.10)' }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 16, marginBottom: 26 }}>
          <span style={{ fontSize: 30 }}>{tone.chip}</span>
          <span style={{ fontSize: 17, letterSpacing: '0.26em', fontWeight: 800, color: tone.bar }}>
            {beat.label || tone.label}
          </span>
        </div>
        <div style={{ fontSize: 56, fontWeight: 800, letterSpacing: '-0.028em', lineHeight: 1.08,
          textWrap: 'balance' }}>{beat.title}</div>
        <div style={{ marginTop: 38, display: 'flex', flexDirection: 'column', gap: 16 }}>
          {(beat.items || []).map((it, i) => {
            const t = kitStep(localTime, i, 0.5, 0.3);
            return (
              <div key={i} style={{ opacity: t, transform: `translateY(${(1 - t) * 12}px)`,
                display: 'flex', alignItems: 'flex-start', gap: 18, fontSize: 28, lineHeight: 1.4 }}>
                <span style={{ color: tone.bar, fontWeight: 800, flexShrink: 0 }}>—</span>
                <span style={{ textWrap: 'pretty' }}>{it}</span>
              </div>
            );
          })}
        </div>
      </div>
    </div>
  );
}

// ════════════════════════════════════════════════════════════════════════════
// TABLE — status / comparison tables
// ════════════════════════════════════════════════════════════════════════════
function BeatTable({ beat }) {
  const { localTime, duration } = useSprite();
  const { opacity, inT } = kitFade(localTime, duration);
  const cols = beat.cols;
  const grid = beat.grid || cols.map(() => '1fr').join(' ');
  return (
    <div style={{ position: 'absolute', inset: 0, background: '#F4F6F5', color: CTX_INK, opacity,
      padding: '90px 110px', overflow: 'hidden',
      display: 'flex', flexDirection: 'column', justifyContent: 'center' }}>
      <div style={{ fontSize: 17, letterSpacing: '0.28em', fontWeight: 800, color: CTX_BLUE, opacity: inT }}>
        {beat.kicker}
      </div>
      <div style={{ fontSize: 56, fontWeight: 800, letterSpacing: '-0.028em', marginTop: 14, marginBottom: 40,
        opacity: inT, transform: `translateY(${(1 - inT) * 12}px)` }}>{beat.title}</div>

      <div style={{ display: 'grid', gridTemplateColumns: grid, gap: 0,
        background: '#fff', borderRadius: 16, overflow: 'hidden', boxShadow: '0 14px 40px rgba(0,0,0,0.07)' }}>
        {cols.map((c) => (
          <div key={c} style={{ background: CTX_BLUE, color: '#fff', padding: '20px 26px',
            fontSize: 16, fontWeight: 800, letterSpacing: '0.10em' }}>{c}</div>
        ))}
        {beat.rows.map((row, ri) => {
          const t = kitStep(localTime, ri, 0.45, 0.26, 0.45);
          return row.map((cell, ci) => (
            <div key={ri + '-' + ci} style={{
              padding: '20px 26px',
              borderBottom: `1px solid ${CTX_DIVIDER}`,
              background: ri % 2 ? '#FAFBFA' : '#fff',
              opacity: t, transform: `translateY(${(1 - t) * 8}px)`,
              fontSize: ci === 0 ? 22 : 20,
              fontWeight: ci === 0 ? 700 : 400,
              color: ci === 0 ? CTX_BLUE : 'rgba(44,57,66,0.86)',
              lineHeight: 1.35, display: 'flex', alignItems: 'center',
              textWrap: 'pretty',
            }}>{cell}</div>
          ));
        })}
      </div>
    </div>
  );
}

// ════════════════════════════════════════════════════════════════════════════
// STEPS — numbered process flow
// ════════════════════════════════════════════════════════════════════════════
function BeatSteps({ beat }) {
  const { localTime, duration } = useSprite();
  const { opacity, inT } = kitFade(localTime, duration);
  const n = beat.steps.length;
  const cols = n <= 4 ? n : Math.ceil(n / 2);
  return (
    <div style={{ position: 'absolute', inset: 0, background: '#0E1A16', color: '#fff', opacity,
      padding: '100px 120px', overflow: 'hidden',
      display: 'flex', flexDirection: 'column', justifyContent: 'center' }}>
      <div style={{ position: 'absolute', right: -160, top: -140, opacity: 0.045, filter: 'brightness(0) invert(1)' }}>
        <SynviaArrow size={700} />
      </div>
      <div style={{ fontSize: 17, letterSpacing: '0.28em', fontWeight: 800, color: CTX_GREEN_LIME, opacity: inT }}>
        {beat.kicker}
      </div>
      <div style={{ fontSize: 60, fontWeight: 800, letterSpacing: '-0.028em', marginTop: 14,
        opacity: inT, transform: `translateY(${(1 - inT) * 14}px)`, textWrap: 'balance' }}>{beat.title}</div>

      <div style={{ marginTop: 60, display: 'grid', gridTemplateColumns: `repeat(${cols}, 1fr)`, gap: 26 }}>
        {beat.steps.map((s, i) => {
          const t = kitStep(localTime, i, 0.5, 0.26);
          const txt = typeof s === 'string' ? s : s.t;
          const note = typeof s === 'object' ? s.note : null;
          return (
            <div key={i} style={{ opacity: t, transform: `translateY(${(1 - t) * 28}px)`,
              background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.10)',
              borderRadius: 18, padding: '30px 28px 34px', position: 'relative', minHeight: 190 }}>
              <div data-tnum style={{ fontSize: 15, fontWeight: 800, letterSpacing: '0.2em',
                color: CTX_GREEN_LIME, marginBottom: 18 }}>PASSO {String(i + 1).padStart(2, '0')}</div>
              <div style={{ fontSize: 26, fontWeight: 700, lineHeight: 1.3, letterSpacing: '-0.01em',
                textWrap: 'pretty' }}>{txt}</div>
              {note && <div style={{ fontSize: 19, color: 'rgba(255,255,255,0.55)', marginTop: 12, lineHeight: 1.4 }}>{note}</div>}
            </div>
          );
        })}
      </div>
    </div>
  );
}

// ════════════════════════════════════════════════════════════════════════════
// CARDS — 2-4 comparison cards, optional highlight
// ════════════════════════════════════════════════════════════════════════════
function BeatCards({ beat }) {
  const { localTime, duration } = useSprite();
  const { opacity, inT } = kitFade(localTime, duration);
  return (
    <div style={{ position: 'absolute', inset: 0, background: '#0E1A16', color: '#fff', opacity,
      padding: '96px 110px', overflow: 'hidden',
      display: 'flex', flexDirection: 'column', justifyContent: 'center' }}>
      <div style={{ position: 'absolute', right: -170, top: -150, opacity: 0.05, filter: 'brightness(0) invert(1)' }}>
        <SynviaArrow size={760} />
      </div>
      <div style={{ fontSize: 17, letterSpacing: '0.28em', fontWeight: 800, color: CTX_GREEN_LIME, opacity: inT }}>
        {beat.kicker}
      </div>
      <div style={{ fontSize: 62, fontWeight: 800, letterSpacing: '-0.028em', marginTop: 14, maxWidth: 1400,
        opacity: inT, transform: `translateY(${(1 - inT) * 14}px)`, lineHeight: 1.06, textWrap: 'balance' }}>
        {beat.title}
      </div>
      <div style={{ marginTop: 56, display: 'grid',
        gridTemplateColumns: `repeat(${beat.cards.length}, 1fr)`, gap: 28 }}>
        {beat.cards.map((c, i) => {
          const t = kitStep(localTime, i, 0.5, 0.28);
          const hi = c.hi;
          return (
            <div key={i} style={{ opacity: t, transform: `translateY(${(1 - t) * 34}px)`,
              background: hi ? CTX_GREEN_DK : 'rgba(255,255,255,0.045)',
              border: hi ? `2px solid ${CTX_GREEN_LIME}` : '1px solid rgba(255,255,255,0.11)',
              borderRadius: 20, padding: '34px 32px 40px', minHeight: 360 }}>
              {c.tag && (
                <div style={{ fontSize: 15, fontWeight: 800, letterSpacing: '0.24em',
                  color: hi ? CTX_GREEN_LIME : 'rgba(255,255,255,0.42)', marginBottom: 22 }}>{c.tag}</div>
              )}
              <div style={{ fontSize: 38, fontWeight: 800, letterSpacing: '-0.02em', lineHeight: 1.1,
                marginBottom: 16, textWrap: 'balance' }}>{c.title}</div>
              {c.sub && (
                <div style={{ fontSize: 21, lineHeight: 1.45, color: 'rgba(255,255,255,0.68)',
                  whiteSpace: 'pre-line', textWrap: 'pretty' }}>{c.sub}</div>
              )}
              {c.list && (
                <div style={{ marginTop: 18, display: 'flex', flexDirection: 'column', gap: 10 }}>
                  {c.list.map((l, li) => (
                    <div key={li} style={{ display: 'flex', gap: 12, fontSize: 20, lineHeight: 1.35,
                      color: 'rgba(255,255,255,0.8)' }}>
                      <span style={{ color: CTX_GREEN_LIME, fontWeight: 800 }}>·</span>
                      <span style={{ textWrap: 'pretty' }}>{l}</span>
                    </div>
                  ))}
                </div>
              )}
            </div>
          );
        })}
      </div>
    </div>
  );
}

// ════════════════════════════════════════════════════════════════════════════
// STATS — big number callouts
// ════════════════════════════════════════════════════════════════════════════
function BeatStats({ beat }) {
  const { localTime, duration } = useSprite();
  const { opacity, inT } = kitFade(localTime, duration);
  return (
    <div style={{ position: 'absolute', inset: 0, background: CTX_BLUE, color: '#fff', opacity,
      display: 'flex', flexDirection: 'column', justifyContent: 'center', padding: '0 130px', overflow: 'hidden' }}>
      <div style={{ position: 'absolute', left: -160, bottom: -160, opacity: 0.05, filter: 'brightness(0) invert(1)' }}>
        <SynviaArrow size={720} />
      </div>
      <div style={{ fontSize: 17, letterSpacing: '0.28em', fontWeight: 800, color: CTX_GREEN_LIME, opacity: inT }}>
        {beat.kicker}
      </div>
      <div style={{ fontSize: 62, fontWeight: 800, letterSpacing: '-0.028em', marginTop: 14, maxWidth: 1300,
        opacity: inT, transform: `translateY(${(1 - inT) * 14}px)`, textWrap: 'balance' }}>{beat.title}</div>
      <div style={{ marginTop: 64, display: 'grid', gridTemplateColumns: `repeat(${beat.stats.length}, 1fr)`, gap: 34 }}>
        {beat.stats.map((s, i) => {
          const t = kitStep(localTime, i, 0.5, 0.26);
          return (
            <div key={i} style={{ opacity: t, transform: `translateY(${(1 - t) * 26}px)`,
              borderTop: `4px solid ${CTX_GREEN_LIME}`, paddingTop: 26 }}>
              <div data-tnum style={{ fontSize: 84, fontWeight: 800, letterSpacing: '-0.04em', lineHeight: 1 }}>{s.v}</div>
              <div style={{ fontSize: 23, color: 'rgba(255,255,255,0.72)', marginTop: 14, lineHeight: 1.35,
                textWrap: 'pretty' }}>{s.l}</div>
            </div>
          );
        })}
      </div>
    </div>
  );
}

// ════════════════════════════════════════════════════════════════════════════
// QUOTE — single strong statement
// ════════════════════════════════════════════════════════════════════════════
function BeatQuote({ beat }) {
  const { localTime, duration } = useSprite();
  const { opacity, inT } = kitFade(localTime, duration, 0.55, 0.45);
  return (
    <div style={{ position: 'absolute', inset: 0, background: CTX_GREEN_DK, color: '#fff', opacity,
      display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '0 180px', textAlign: 'center' }}>
      <div>
        <div style={{ width: 70, height: 4, background: CTX_GREEN_LIME, margin: '0 auto 40px', opacity: inT }} />
        <div style={{ fontSize: 76, fontWeight: 800, letterSpacing: '-0.03em', lineHeight: 1.14,
          opacity: inT, transform: `translateY(${(1 - inT) * 18}px)`, textWrap: 'balance' }}>{beat.text}</div>
        {beat.sub && (
          <div style={{ fontSize: 27, color: 'rgba(255,255,255,0.62)', marginTop: 34,
            opacity: kitStep(localTime, 0, 0.5, 0, 0.5), textWrap: 'pretty' }}>{beat.sub}</div>
        )}
      </div>
    </div>
  );
}

// ════════════════════════════════════════════════════════════════════════════
// CLOSING — module complete + recap
// ════════════════════════════════════════════════════════════════════════════
function BeatClosing({ mod, beat }) {
  const { localTime, duration } = useSprite();
  const { opacity, inT } = kitFade(localTime, duration, 0.55, 0.35);
  return (
    <div style={{ position: 'absolute', inset: 0, background: CTX_GREEN_DK, color: '#fff', opacity,
      display: 'flex', alignItems: 'center', justifyContent: 'center', overflow: 'hidden' }}>
      <div style={{ position: 'absolute', inset: 0, opacity: 0.11 }}>
        <div style={{ position: 'absolute', top: -140, right: -140, width: 720, height: 720,
          borderRadius: '50%', border: `2px solid ${CTX_GREEN_LIME}` }} />
        <div style={{ position: 'absolute', bottom: -240, left: -240, width: 840, height: 840,
          borderRadius: '50%', border: `2px solid ${CTX_GREEN_LIME}` }} />
      </div>
      <div style={{ textAlign: 'center', padding: '0 120px', position: 'relative', zIndex: 2,
        opacity: inT, transform: `translateY(${(1 - inT) * 18}px)` }}>
        <div style={{ display: 'inline-flex', alignItems: 'center', gap: 13, marginBottom: 30 }}>
          <div style={{ width: 38, height: 38, borderRadius: '50%', background: CTX_GREEN_LIME, color: CTX_GREEN_DK,
            display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 22, fontWeight: 800 }}>✓</div>
          <span style={{ fontSize: 18, letterSpacing: '0.30em', fontWeight: 800, color: CTX_GREEN_LIME }}>
            MÓDULO {mod.n} CONCLUÍDO
          </span>
        </div>
        <div style={{ fontSize: 82, fontWeight: 800, lineHeight: 1.05, letterSpacing: '-0.03em', textWrap: 'balance' }}>
          {beat.title || mod.title}
        </div>
        {beat.recap && (
          <div style={{ marginTop: 44, display: 'flex', gap: 13, justifyContent: 'center', flexWrap: 'wrap',
            maxWidth: 1400, opacity: kitStep(localTime, 0, 0.6, 0, 0.6) }}>
            {beat.recap.map((r) => (
              <div key={r} style={{ display: 'inline-flex', alignItems: 'center', gap: 9,
                background: 'rgba(255,255,255,0.07)', border: '1px solid rgba(178,232,71,0.32)',
                borderRadius: 999, padding: '10px 20px', fontSize: 18, fontWeight: 600 }}>
                <span style={{ color: CTX_GREEN_LIME, fontWeight: 800 }}>✓</span>{r}
              </div>
            ))}
          </div>
        )}
        {beat.next && (
          <div style={{ marginTop: 46, fontSize: 25, color: 'rgba(255,255,255,0.75)',
            opacity: kitStep(localTime, 0, 0.9, 0, 0.6) }}>
            A seguir — <b style={{ color: CTX_GREEN_LIME }}>{beat.next}</b>
          </div>
        )}
      </div>
    </div>
  );
}

Object.assign(window, {
  kitTyped, kitCursor, kitFade, kitStep, TONE,
  ModuleHUD, BeatModuleIntro, BeatLesson, BeatBullets, BeatRule,
  BeatTable, BeatSteps, BeatCards, BeatStats, BeatQuote, BeatClosing,
});
