// hero-story.jsx — animated hero engine, v3.
//
// Architecture:
//   HeroStory (parent)        — owns cycleIdx; mounts ONE HeroStoryCycle at a
//                                time via a key. When a cycle completes, the
//                                old instance unmounts and a new one mounts —
//                                state and CSS animations reset cleanly.
//   HeroStoryCycle (child)    — plays through one conversation: ringing →
//                                active typewriter → callEnd → slide1 →
//                                emailWrite → emailHold → slideOut → gap.
//                                Calls onComplete at the end of gap.
//
// LiveWaveform now has a fill-in animation: a paper-colored veil covers the
// strip, then retreats from left over ~4s, revealing bars from right to left
// as the call comes online.

(function () {
  const { PALETTE } = window;

  // --- Tunables ---------------------------------------------------------
  const BAR_W = 4;
  const BAR_GAP = 5;
  const BARS_PER_LOOP = 90;
  const SCROLL_DURATION_S = 16;
  const BUILDUP_DURATION_S = 4;

  const TYPE_RATE_MS = 32;
  const PUNCT_EXTRA_MS = 90;
  const PAUSE_BETWEEN_LINES_MS = 750;
  const PAUSE_AFTER_LAST_LINE_MS = 1600;
  const RINGING_MS = 1300;
  const CALL_END_MS = 1900;
  const SLIDE_MS = 1000;
  const POST_SLIDE_SETTLE_MS = 600;
  // --- Email typewriter (character-by-character across whole email) ---
  const EMAIL_CHAR_MS = 9;
  const EMAIL_PUNCT_MS = 30;
  const EMAIL_HOLD_MS = 1800;     // pause after typing completes
  const FADE_AND_SENT_MS = 900;   // email text fades, "Sent!" appears
  const SENT_HOLD_MS = 1200;      // "Sent!" stays visible
  const SLIDE_OFF_MS = 1100;      // both zones slide left
  const GAP_MS = 700;             // empty pause before next cycle

  function generateBars(count, seed = 1) {
    const out = [];
    for (let i = 0; i < count; i++) {
      const v =
        0.5 +
        0.35 * Math.sin(i * 0.32 + seed) +
        0.18 * Math.sin(i * 0.83 + seed * 2) +
        0.12 * Math.sin(i * 1.55 + seed * 0.7);
      out.push(Math.max(0.16, Math.min(0.96, v)));
    }
    return out;
  }

  // ----------------------------------------------------------------------
  // LiveWaveform — continuous-scroll oscilloscope. New for v3: a "fill veil"
  // covers the strip initially and retreats from left as the call goes
  // active, revealing bars right-to-left.
  // ----------------------------------------------------------------------
  function LiveWaveform({ color = PALETTE.ink, height = 140, phase = "ringing", veilColor = PALETTE.bg }) {
    const bars = React.useMemo(() => generateBars(BARS_PER_LOOP, 1.3), []);
    const stripWidth = BARS_PER_LOOP * (BAR_W + BAR_GAP);

    let dim = 1;
    if (phase === "ringing") dim = 0.5;
    else if (phase === "callEnd" || phase === "gone") dim = 0;

    // Veil is fully covering during ringing; retreats once the call is active.
    const veilOpen = phase === "active" || phase === "callEnd" || phase === "gone";

    return (
      <div
        aria-hidden="true"
        style={{
          position: "relative",
          width: "100%",
          height,
          overflow: "hidden",
          opacity: dim,
          transition: "opacity .55s ease",
          maskImage:
            "linear-gradient(to right, transparent 0%, black 10%, black 90%, transparent 100%)",
          WebkitMaskImage:
            "linear-gradient(to right, transparent 0%, black 10%, black 90%, transparent 100%)",
        }}
      >
        {/* Bars strip — continuous left drift */}
        <div
          style={{
            position: "absolute",
            top: 0,
            left: 0,
            height: "100%",
            display: "flex",
            alignItems: "center",
            gap: BAR_GAP,
            animation: `lw-scroll ${SCROLL_DURATION_S}s linear infinite`,
            willChange: "transform",
          }}
        >
          {[...bars, ...bars].map((h, i) => (
            <div
              key={i}
              style={{
                width: BAR_W,
                height: `${h * 100}%`,
                minHeight: 3,
                background: color,
                borderRadius: 2,
                animation: `lw-pulse-${i % 6} ${1.1 + (i % 5) * 0.18}s ease-in-out ${(i % 13) * 0.06}s infinite alternate`,
                transformOrigin: "center",
                opacity: 0.92,
              }}
            />
          ))}
        </div>

        {/* Fill-in veil — anchored left, shrinks to width:0 to reveal bars right-to-left */}
        <div
          style={{
            position: "absolute",
            left: 0,
            top: 0,
            bottom: 0,
            width: veilOpen ? "0%" : "100%",
            background: `linear-gradient(to left, transparent 0%, ${veilColor} 36px)`,
            transition: `width ${BUILDUP_DURATION_S}s cubic-bezier(.2,.6,.3,1)`,
            pointerEvents: "none",
          }}
        />

        <style>{`
          @keyframes lw-scroll {
            from { transform: translateX(0); }
            to   { transform: translateX(-${stripWidth}px); }
          }
          @keyframes lw-pulse-0 { 0%{transform:scaleY(.55)} 100%{transform:scaleY(1.08)} }
          @keyframes lw-pulse-1 { 0%{transform:scaleY(.7)} 100%{transform:scaleY(.92)} }
          @keyframes lw-pulse-2 { 0%{transform:scaleY(.4)} 100%{transform:scaleY(1.12)} }
          @keyframes lw-pulse-3 { 0%{transform:scaleY(.85)} 100%{transform:scaleY(.55)} }
          @keyframes lw-pulse-4 { 0%{transform:scaleY(.6)} 100%{transform:scaleY(1)} }
          @keyframes lw-pulse-5 { 0%{transform:scaleY(.5)} 100%{transform:scaleY(1.05)} }
        `}</style>
      </div>
    );
  }

  // ----------------------------------------------------------------------
  // Checkmark — SVG with stroke-dasharray draw animation
  // ----------------------------------------------------------------------
  function Checkmark({ color = PALETTE.ink, accent = PALETTE.accent, visible, size = 96 }) {
    return (
      <div
        style={{
          position: "absolute",
          top: "50%",
          left: "50%",
          transform: `translate(-50%, -50%) scale(${visible ? 1 : 0.7})`,
          opacity: visible ? 1 : 0,
          transition: "opacity .45s ease, transform .55s cubic-bezier(.2,.7,.3,1)",
          width: size,
          height: size,
          pointerEvents: "none",
        }}
      >
        <svg viewBox="0 0 100 100" width={size} height={size}>
          <circle
            cx="50" cy="50" r="46"
            fill="none" stroke={color} strokeWidth="1.3"
            style={{
              strokeDasharray: 290,
              strokeDashoffset: visible ? 0 : 290,
              transition: "stroke-dashoffset .9s cubic-bezier(.3,.7,.3,1)",
            }}
          />
          <circle
            cx="50" cy="50" r="40"
            fill="none" stroke={accent} strokeWidth="0.6" opacity="0.45"
            style={{
              strokeDasharray: 252,
              strokeDashoffset: visible ? 0 : 252,
              transition: "stroke-dashoffset 1.1s cubic-bezier(.3,.7,.3,1) .1s",
            }}
          />
          <polyline
            points="32,52 45,66 70,38"
            fill="none" stroke={color} strokeWidth="2.5"
            strokeLinecap="round" strokeLinejoin="round"
            style={{
              strokeDasharray: 65,
              strokeDashoffset: visible ? 0 : 65,
              transition: "stroke-dashoffset .55s cubic-bezier(.3,.7,.3,1) .35s",
            }}
          />
        </svg>
      </div>
    );
  }

  function Cursor({ color = PALETTE.ink, size = "1em" }) {
    return (
      <span
        aria-hidden="true"
        style={{
          display: "inline-block",
          width: 2,
          height: size,
          background: color,
          marginLeft: 3,
          verticalAlign: "-0.12em",
          animation: "ol-cursor 1s steps(2) infinite",
        }}
      >
        <style>{`
          @keyframes ol-cursor {
            0%, 50% { opacity: 1; }
            50.01%, 100% { opacity: 0; }
          }
        `}</style>
      </span>
    );
  }

  // ----------------------------------------------------------------------
  // TranscriptStack — accumulating lines, last 3 visible. Agent lines get
  // accent bar + brand label + large serif. Caller lines are quiet italic.
  // ----------------------------------------------------------------------
  function TranscriptStack({ lines, activeIdx, charIdx, isTyping, dimmed = false }) {
    const windowStart = Math.max(0, activeIdx - 2);
    const visibleLines = [];
    for (let i = windowStart; i <= activeIdx && i < lines.length; i++) {
      visibleLines.push({ ...lines[i], _idx: i, _age: activeIdx - i, _isActive: i === activeIdx });
    }

    return (
      <div
        style={{
          display: "flex",
          flexDirection: "column",
          gap: 22,
          opacity: dimmed ? 0.55 : 1,
          transition: "opacity .6s ease",
        }}
      >
        {visibleLines.map((line) => {
          const isAgent = line.who === "agent";
          const displayText = line._isActive ? line.text.slice(0, charIdx) : line.text;
          const showCursor = line._isActive && isTyping && charIdx < line.text.length;
          const ageOpacity = line._age === 0 ? 1 : line._age === 1 ? 0.55 : 0.28;

          if (isAgent) {
            return (
              <div
                key={line._idx}
                style={{
                  opacity: ageOpacity,
                  display: "grid",
                  gridTemplateColumns: "3px 1fr",
                  gap: 16,
                  transition: "opacity .55s ease",
                }}
              >
                <div style={{ width: 3, background: PALETTE.accent, borderRadius: 1, alignSelf: "stretch" }} />
                <div>
                  <div
                    style={{
                      display: "flex",
                      alignItems: "center",
                      gap: 8,
                      marginBottom: 8,
                      fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
                      fontSize: 10.5,
                      letterSpacing: 1.6,
                      textTransform: "uppercase",
                      color: PALETTE.accent,
                      fontWeight: 700,
                    }}
                  >
                    <span
                      style={{
                        display: "inline-block",
                        width: 6,
                        height: 6,
                        borderRadius: 999,
                        background: PALETTE.accent,
                      }}
                    />
                    OpenLines
                  </div>
                  <div
                    style={{
                      fontFamily: '"Iowan Old Style", "Hoefler Text", Georgia, serif',
                      fontSize: 21,
                      lineHeight: 1.35,
                      color: PALETTE.ink,
                      fontWeight: 500,
                      letterSpacing: -0.2,
                    }}
                  >
                    {displayText}
                    {showCursor ? <Cursor color={PALETTE.ink} /> : null}
                  </div>
                </div>
              </div>
            );
          }

          return (
            <div
              key={line._idx}
              style={{
                opacity: ageOpacity,
                paddingLeft: 19,
                transition: "opacity .55s ease",
              }}
            >
              <div
                style={{
                  fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
                  fontSize: 10.5,
                  letterSpacing: 1.6,
                  textTransform: "uppercase",
                  color: PALETTE.sub,
                  marginBottom: 6,
                  fontWeight: 600,
                }}
              >
                Caller
              </div>
              <div
                style={{
                  fontFamily: '"Iowan Old Style", "Hoefler Text", Georgia, serif',
                  fontSize: 15.5,
                  lineHeight: 1.4,
                  color: PALETTE.sub,
                  fontStyle: "italic",
                }}
              >
                {displayText}
                {showCursor ? <Cursor color={PALETTE.sub} /> : null}
              </div>
            </div>
          );
        })}
      </div>
    );
  }

  // ----------------------------------------------------------------------
  // Email chunks — flat list of text segments that get typed character-by-
  // character (just like the transcript). Each chunk is rendered in DOM
  // order; revealChars is the global typing cursor.
  // ----------------------------------------------------------------------
  function getEmailChunks(conversation) {
    if (!conversation) return [];
    const { business, timestamp, duration, endReason, summary } = conversation;
    const time = timestamp.split(" · ")[1] || "";
    const callerText =
      summary.caller.name === "Not collected" && summary.caller.phone === "Not collected"
        ? "Not collected — caller did not share contact info"
        : `${summary.caller.name} · ${summary.caller.phone}`;

    const chunks = [
      { id: "subject", text: `${business} · Call summary · ${time} · ${duration}` },
      { id: "header-name", text: business },
      { id: "header-end", text: endReason },
      { id: "sum-body", text: summary.summary, section: 0 },
      { id: "caller-body", text: callerText, section: 1 },
      ...summary.handled.map((h, i) => ({ id: `did-${i}`, text: h, section: 2, itemIdx: i })),
      { id: "actions", text: summary.actions, section: 3 },
      { id: "notes", text: summary.notes, section: 4 },
    ];
    let offset = 0;
    for (const c of chunks) {
      c.start = offset;
      offset += c.text.length;
    }
    return chunks;
  }

  function getEmailTotalChars(conversation) {
    return getEmailChunks(conversation).reduce((s, c) => s + c.text.length, 0);
  }

  function getEmailFlatText(conversation) {
    return getEmailChunks(conversation)
      .map((c) => c.text)
      .join("");
  }

  // ----------------------------------------------------------------------
  // EmailDocument — borderless or hairline integration
  // ----------------------------------------------------------------------
  function EmailDocument({ conversation, mode = "borderless", revealChars = Infinity }) {
    if (!conversation) return null;
    const { business, timestamp, duration, endReason, summary } = conversation;
    const urgent = /URGENT/i.test(summary.actions || "") || /priority/i.test(summary.notes || "");

    const chunks = React.useMemo(() => getEmailChunks(conversation), [conversation]);
    const totalChars = chunks.reduce((s, c) => s + c.text.length, 0);
    const typingComplete = revealChars >= totalChars;

    const isHairline = mode === "hairline";
    const containerStyle = isHairline
      ? { border: `1px solid ${PALETTE.hairline}`, padding: "22px 24px 18px", background: "transparent" }
      : { border: "none", padding: "4px 0 0", background: "transparent" };

    // Per-chunk visible state
    function vis(id) {
      const c = chunks.find((c) => c.id === id);
      if (!c) return { visible: "", isComplete: false, isActive: false, hasStarted: false };
      const count = Math.max(0, Math.min(c.text.length, revealChars - c.start));
      return {
        visible: c.text.slice(0, count),
        isComplete: count >= c.text.length,
        isActive: count > 0 && count < c.text.length,
        hasStarted: count > 0,
      };
    }

    function sectionVisible(sectionIdx) {
      const first = chunks.find((c) => c.section === sectionIdx);
      if (!first) return false;
      return revealChars > first.start;
    }

    function MiniCursor() {
      return (
        <span
          aria-hidden="true"
          style={{
            display: "inline-block",
            width: 1.5,
            height: "0.95em",
            background: PALETTE.ink,
            marginLeft: 2,
            verticalAlign: "-0.1em",
            animation: "ol-email-cursor 0.9s steps(2) infinite",
          }}
        >
          <style>{`
            @keyframes ol-email-cursor {
              0%,50% { opacity: 1; }
              50.01%,100% { opacity: 0; }
            }
          `}</style>
        </span>
      );
    }

    const Section = ({ idx, label, children, last }) => {
      const visible = sectionVisible(idx);
      return (
        <div
          style={{
            opacity: visible ? 1 : 0,
            transform: visible ? "translateY(0)" : "translateY(6px)",
            transition: "opacity .45s ease, transform .45s cubic-bezier(.2,.7,.3,1)",
            padding: "10px 0",
            borderBottom: last ? "none" : `1px solid ${PALETTE.line}`,
          }}
        >
          <div
            style={{
              fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
              fontSize: 10,
              letterSpacing: 1.4,
              textTransform: "uppercase",
              color: PALETTE.sub,
              marginBottom: 4,
              fontWeight: 600,
            }}
          >
            {label}
          </div>
          <div
            style={{
              fontFamily: '"Iowan Old Style", "Hoefler Text", Georgia, serif',
              fontSize: 13.5,
              lineHeight: 1.5,
              color: PALETTE.ink,
              minHeight: 14,
            }}
          >
            {children}
          </div>
        </div>
      );
    };

    const subj = vis("subject");
    const hdrName = vis("header-name");
    const hdrEnd = vis("header-end");
    const sumBody = vis("sum-body");
    const callerBody = vis("caller-body");
    const actionsBody = vis("actions");
    const notesBody = vis("notes");

    return (
      <div style={{ ...containerStyle, color: PALETTE.ink }}>
        {/* From / date line — instant */}
        <div
          style={{
            fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
            fontSize: 10,
            letterSpacing: 0.6,
            textTransform: "uppercase",
            color: PALETTE.sub,
            marginBottom: 6,
            display: "flex",
            justifyContent: "space-between",
            gap: 12,
          }}
        >
          <span>From · OpenLines</span>
          <span>{timestamp.split(" · ")[0]}</span>
        </div>
        {/* Subject — typed */}
        <div
          style={{
            fontFamily: '"Iowan Old Style", "Hoefler Text", Georgia, serif',
            fontSize: 14.5,
            fontWeight: 600,
            color: PALETTE.ink,
            lineHeight: 1.35,
            marginBottom: 10,
            paddingBottom: 10,
            borderBottom: `1px solid ${PALETTE.line}`,
            minHeight: 18,
          }}
        >
          {subj.visible}
          {subj.isActive ? <MiniCursor /> : null}
        </div>

        {/* Header row — business name + endReason */}
        <div
          style={{
            display: "flex",
            justifyContent: "space-between",
            alignItems: "baseline",
            marginBottom: 2,
            gap: 10,
          }}
        >
          <div>
            <div
              style={{
                fontFamily: '"Iowan Old Style", "Hoefler Text", Georgia, serif',
                fontSize: 19,
                fontWeight: 500,
                letterSpacing: -0.3,
                color: PALETTE.ink,
                lineHeight: 1.1,
                marginBottom: 3,
                minHeight: 20,
              }}
            >
              {hdrName.visible}
              {hdrName.isActive ? <MiniCursor /> : null}
            </div>
            <div style={{ fontSize: 11, color: PALETTE.sub, minHeight: 14 }}>
              {hdrEnd.visible}
              {hdrEnd.isActive ? <MiniCursor /> : null}
            </div>
          </div>
          {urgent && hdrName.hasStarted ? (
            <div
              style={{
                fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
                fontSize: 9.5,
                letterSpacing: 1.2,
                textTransform: "uppercase",
                padding: "3px 8px",
                color: PALETTE.accent,
                border: `1px solid ${PALETTE.accent}`,
                whiteSpace: "nowrap",
                fontWeight: 600,
                opacity: 1,
                transition: "opacity .35s ease",
              }}
            >
              Urgent
            </div>
          ) : null}
        </div>

        <Section idx={0} label="Summary">
          {sumBody.visible}
          {sumBody.isActive ? <MiniCursor /> : null}
        </Section>
        <Section idx={1} label="Caller info">
          {summary.caller.name === "Not collected" &&
          summary.caller.phone === "Not collected" ? (
            <span style={{ color: PALETTE.sub }}>
              {callerBody.visible}
              {callerBody.isActive ? <MiniCursor /> : null}
            </span>
          ) : (
            <>
              {callerBody.visible}
              {callerBody.isActive ? <MiniCursor /> : null}
            </>
          )}
        </Section>
        <Section idx={2} label="What the agent did">
          <ul style={{ margin: 0, paddingLeft: 18, display: "flex", flexDirection: "column", gap: 3 }}>
            {summary.handled.map((h, i) => {
              const v = vis(`did-${i}`);
              return (
                <li
                  key={i}
                  style={{
                    opacity: v.hasStarted ? 1 : 0,
                    transition: "opacity .35s ease",
                    minHeight: v.hasStarted ? "auto" : 0,
                  }}
                >
                  {v.visible}
                  {v.isActive ? <MiniCursor /> : null}
                </li>
              );
            })}
          </ul>
        </Section>
        <Section idx={3} label="Action items">
          {actionsBody.visible}
          {actionsBody.isActive ? <MiniCursor /> : null}
        </Section>
        <Section idx={4} label="Notes" last>
          {notesBody.visible}
          {notesBody.isActive ? <MiniCursor /> : null}
        </Section>

        {/* Footer — appears once typing is complete */}
        <div
          style={{
            marginTop: 10,
            paddingTop: 8,
            borderTop: `1px solid ${PALETTE.line}`,
            fontSize: 10,
            color: PALETTE.sub,
            fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
            letterSpacing: 0.4,
            opacity: typingComplete ? 1 : 0,
            transition: "opacity .5s ease",
          }}
        >
          Generated by OpenLines for {business}
        </div>
      </div>
    );
  }

  // ----------------------------------------------------------------------
  // SentCaption — fades in over the email zone once the email content
  // starts fading. Italic serif oxblood "Sent." with mono sub-caption.
  // ----------------------------------------------------------------------
  function SentCaption({ visible }) {
    return (
      <div
        aria-hidden="true"
        style={{
          position: "absolute",
          inset: 0,
          display: "flex",
          alignItems: "center",
          justifyContent: "center",
          flexDirection: "column",
          gap: 14,
          pointerEvents: "none",
          opacity: visible ? 1 : 0,
          transform: `translateY(${visible ? "0%" : "10%"})`,
          transition: "opacity 0.85s cubic-bezier(.4,0,.2,1) 0.2s, transform 0.85s cubic-bezier(.3,.7,.3,1) 0.2s",
        }}
      >
        <div
          style={{
            fontFamily: '"Iowan Old Style", "Hoefler Text", Georgia, serif',
            fontStyle: "italic",
            fontSize: 76,
            color: PALETTE.accent,
            fontWeight: 500,
            letterSpacing: -1.6,
            lineHeight: 1,
          }}
        >
          Sent.
        </div>
        <div
          style={{
            fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
            fontSize: 11,
            letterSpacing: 1.8,
            textTransform: "uppercase",
            color: PALETTE.sub,
            fontWeight: 600,
            display: "flex",
            alignItems: "center",
            gap: 10,
          }}
        >
          <span style={{ display: "inline-block", width: 14, height: 1, background: PALETTE.accent }} />
          Delivered to your inbox
          <span style={{ display: "inline-block", width: 14, height: 1, background: PALETTE.accent }} />
        </div>
      </div>
    );
  }


  // ----------------------------------------------------------------------
  // HeroStoryCycle — runs ONE conversation through the full sequence.
  // No reset logic — when done, calls onComplete. Parent remounts via key.
  // ----------------------------------------------------------------------
  function HeroStoryCycle({ conversation, emailMode, onComplete, isMobile = false }) {
    const [phase, setPhase] = React.useState("ringing");
    const [lineIdx, setLineIdx] = React.useState(-1);
    const [charIdx, setCharIdx] = React.useState(0);
    const [emailCharIdx, setEmailCharIdx] = React.useState(0);
    const [trackPos, setTrackPos] = React.useState(0);

    const emailTotalChars = React.useMemo(() => getEmailTotalChars(conversation), [conversation]);
    const emailFlatText = React.useMemo(() => getEmailFlatText(conversation), [conversation]);

    React.useEffect(() => {
      let cancelled = false;
      const t = (ms, fn) => {
        const id = setTimeout(() => { if (!cancelled) fn(); }, ms);
        return id;
      };
      let tid;

      if (phase === "ringing") {
        tid = t(RINGING_MS, () => {
          setLineIdx(0);
          setCharIdx(0);
          setPhase("active");
        });
      } else if (phase === "active") {
        const line = conversation.transcript[lineIdx];
        if (!line) return;
        if (charIdx < line.text.length) {
          const nextChar = line.text[charIdx];
          const isPunct = /[.,?!]/.test(nextChar);
          tid = t(TYPE_RATE_MS + (isPunct ? PUNCT_EXTRA_MS : 0), () => setCharIdx((c) => c + 1));
        } else {
          if (lineIdx < conversation.transcript.length - 1) {
            tid = t(PAUSE_BETWEEN_LINES_MS, () => {
              setLineIdx((i) => i + 1);
              setCharIdx(0);
            });
          } else {
            tid = t(PAUSE_AFTER_LAST_LINE_MS, () => setPhase("callEnd"));
          }
        }
      } else if (phase === "callEnd") {
        tid = t(CALL_END_MS, () => {
          setTrackPos(1);
          setPhase("slide1");
        });
      } else if (phase === "slide1") {
        tid = t(SLIDE_MS + POST_SLIDE_SETTLE_MS, () => {
          setEmailCharIdx(0);
          setPhase("emailWrite");
        });
      } else if (phase === "emailWrite") {
        if (emailCharIdx < emailTotalChars) {
          const nextChar = emailFlatText[emailCharIdx];
          const isPunct = /[.,?!:]/.test(nextChar);
          tid = t(EMAIL_CHAR_MS + (isPunct ? EMAIL_PUNCT_MS : 0), () => setEmailCharIdx((i) => i + 1));
        } else {
          tid = t(0, () => setPhase("emailHold"));
        }
      } else if (phase === "emailHold") {
        tid = t(EMAIL_HOLD_MS, () => setPhase("fadeAndSent"));
      } else if (phase === "fadeAndSent") {
        tid = t(FADE_AND_SENT_MS, () => setPhase("sentHold"));
      } else if (phase === "sentHold") {
        tid = t(SENT_HOLD_MS, () => setPhase("slideOff"));
      } else if (phase === "slideOff") {
        tid = t(SLIDE_OFF_MS, () => setPhase("gap"));
      } else if (phase === "gap") {
        tid = t(GAP_MS, () => {
          if (onComplete) onComplete();
        });
      }

      return () => { cancelled = true; clearTimeout(tid); };
    }, [phase, lineIdx, charIdx, emailCharIdx, emailTotalChars, emailFlatText, conversation, onComplete]);

    const inCall = phase === "ringing" || phase === "active" || phase === "callEnd";
    const slidPast = trackPos >= 1;
    const inEmailPhase = phase === "slide1" || phase === "emailWrite" || phase === "emailHold";
    const isSending = phase === "fadeAndSent" || phase === "sentHold";
    const slidOff = phase === "slideOff" || phase === "gap";
    const everythingOut = phase === "gap";
    const trackOffsetPct = trackPos === 0 ? 0 : -53;

    // Email content fades during fadeAndSent; gone in sentHold/slideOff/gap.
    const emailContentVisible = inEmailPhase;
    // "Sent." caption shows from fadeAndSent onward, slides with the zone.
    const showSent = isSending || slidOff;

    let wfPhase = "active";
    if (phase === "ringing") wfPhase = "ringing";
    else if (phase === "callEnd") wfPhase = "callEnd";
    else if (slidPast) wfPhase = "gone";

    const isTypingActive = phase === "active";

    // ====== MOBILE LAYOUT ======
    // Stacked, opacity-based phase swap. No horizontal slide.
    if (isMobile) {
      return (
        <div
          style={{
            position: "relative",
            width: "100%",
            height: "100%",
            overflow: "hidden",
            color: PALETTE.ink,
            opacity: everythingOut ? 0 : 1,
            transition: "opacity .7s ease",
          }}
        >
          {/* CALL VIEW — waveform on top, transcript below */}
          <div
            style={{
              position: "absolute",
              inset: 0,
              display: "flex",
              flexDirection: "column",
              gap: 18,
              opacity: inCall ? 1 : 0,
              transform: inCall ? "translateY(0)" : "translateY(-12px)",
              transition: "opacity .55s ease, transform .55s ease",
              pointerEvents: inCall ? "auto" : "none",
            }}
          >
            {/* Top meta + waveform */}
            <div style={{ display: "flex", flexDirection: "column", gap: 14 }}>
              <ZoneMeta
                left={
                  <>
                    <LiveDotInline color={inCall && phase !== "callEnd" ? PALETTE.accent : PALETTE.sub} />
                    <span style={{ color: PALETTE.ink, fontWeight: 600 }}>{conversation.business}</span>
                  </>
                }
                right={conversation.timestamp.split(" · ")[1]}
              />
              <div style={{ position: "relative", height: 84 }}>
                <LiveWaveform color={PALETTE.ink} height={84} phase={wfPhase} veilColor={PALETTE.bg} />
                <Checkmark color={PALETTE.ink} accent={PALETTE.accent} visible={phase === "callEnd"} size={64} />
              </div>
              <ZoneMeta
                left={
                  phase === "ringing" ? "Incoming call"
                    : phase === "active" ? "Live · audio"
                    : phase === "callEnd" ? "Call complete"
                    : "Call ended"
                }
                right={<span style={{ fontVariantNumeric: "tabular-nums" }}>{conversation.duration}</span>}
              />
            </div>

            {/* Transcript label */}
            <div
              style={{
                fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
                fontSize: 11,
                letterSpacing: 1.4,
                textTransform: "uppercase",
                color: PALETTE.sub,
                marginTop: 8,
                display: "flex",
                alignItems: "center",
                gap: 10,
                fontWeight: 600,
              }}
            >
              <span style={{ display: "inline-block", width: 14, height: 1, background: PALETTE.accent }} />
              Live transcript
            </div>

            {/* Transcript stack — fills remaining space */}
            <div style={{ flex: 1, overflow: "hidden" }}>
              <TranscriptStack
                lines={conversation.transcript}
                activeIdx={lineIdx < 0 ? 0 : lineIdx}
                charIdx={charIdx}
                isTyping={isTypingActive}
                dimmed={false}
              />
            </div>
          </div>

          {/* EMAIL VIEW — takes over the same space when call is done */}
          <div
            style={{
              position: "absolute",
              inset: 0,
              opacity: inEmailPhase ? 1 : 0,
              transform: inEmailPhase ? "translateY(0)" : "translateY(12px)",
              transition: "opacity 1s cubic-bezier(.4,0,.2,1), transform 1s cubic-bezier(.4,0,.2,1)",
              pointerEvents: inEmailPhase ? "auto" : "none",
              display: "flex",
              flexDirection: "column",
              gap: 14,
            }}
          >
            <div
              style={{
                fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
                fontSize: 11,
                letterSpacing: 1.4,
                textTransform: "uppercase",
                color: PALETTE.sub,
                display: "flex",
                alignItems: "center",
                gap: 10,
                fontWeight: 600,
              }}
            >
              <span style={{ display: "inline-block", width: 14, height: 1, background: PALETTE.accent }} />
              Morning summary
              <span style={{ color: PALETTE.accent, marginLeft: 4 }}>
                {phase === "emailWrite" ? "· drafting" : phase === "emailHold" ? "· ready to send" : ""}
              </span>
            </div>
            <div style={{ flex: 1, position: "relative", overflow: "hidden" }}>
              <div
                style={{
                  position: "absolute",
                  inset: 0,
                  opacity: emailContentVisible ? 1 : 0,
                  transition: "opacity 0.7s cubic-bezier(.4,0,.2,1)",
                }}
              >
                <EmailDocument
                  conversation={conversation}
                  mode={emailMode}
                  revealChars={inEmailPhase || isSending || slidOff ? emailCharIdx : 0}
                />
              </div>
              <SentCaption visible={showSent} />
            </div>
          </div>

        </div>
      );
    }

    // ====== DESKTOP LAYOUT ======

    return (
      <div
        style={{
          position: "relative",
          width: "100%",
          height: "100%",
          overflow: "hidden",
          color: PALETTE.ink,
        }}
      >
        <div
          style={{
            position: "absolute",
            inset: 0,
            transform: `translateX(${trackOffsetPct}%)`,
            transition: "transform 1s cubic-bezier(.7,.0,.2,1)",
          }}
        >
          {/* === ZONE 1: WAVEFORM / CHECKMARK === */}
          <Zone left="0%">
            <ZoneMeta
              left={
                <>
                  <LiveDotInline color={inCall && phase !== "callEnd" ? PALETTE.accent : PALETTE.sub} />
                  <span style={{ color: PALETTE.ink, fontWeight: 600 }}>{conversation.business}</span>
                </>
              }
              right={conversation.timestamp.split(" · ")[1]}
            />
            <div style={{ position: "relative", flex: "0 0 auto", margin: "auto 0" }}>
              <LiveWaveform color={PALETTE.ink} height={140} phase={wfPhase} veilColor={PALETTE.bg} />
              <Checkmark color={PALETTE.ink} accent={PALETTE.accent} visible={phase === "callEnd"} size={88} />
            </div>
            <ZoneMeta
              left={
                phase === "ringing" ? "Incoming call"
                  : phase === "active" ? "Live · audio"
                  : phase === "callEnd" ? "Call complete"
                  : "Call ended"
              }
              right={<span style={{ fontVariantNumeric: "tabular-nums" }}>{conversation.duration}</span>}
            />
          </Zone>

          {/* === ZONE 2: TRANSCRIPT === */}
          <Zone
            left="53%"
            style={{
              transform: `translateX(${slidOff ? "-115%" : "0%"})`,
              opacity: everythingOut ? 0 : 1,
              transition:
                "transform 1.0s cubic-bezier(.7,0,.2,1), opacity .8s ease",
            }}
          >
            <div
              style={{
                fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
                fontSize: 11,
                letterSpacing: 1.4,
                textTransform: "uppercase",
                color: PALETTE.sub,
                marginBottom: 22,
                display: "flex",
                alignItems: "center",
                gap: 10,
                fontWeight: 600,
              }}
            >
              <span style={{ display: "inline-block", width: 14, height: 1, background: PALETTE.accent }} />
              Live transcript{slidPast ? " · saved" : ""}
            </div>
            <div style={{ flex: 1, overflow: "hidden" }}>
              <TranscriptStack
                lines={conversation.transcript}
                activeIdx={lineIdx < 0 ? 0 : lineIdx}
                charIdx={charIdx}
                isTyping={isTypingActive}
                dimmed={slidPast}
              />
            </div>
          </Zone>

          {/* === ZONE 3: EMAIL → COLLAPSE → ENVELOPE → MAILBOX → SENT === */}
          <Zone
            left="106%"
            style={{
              transform: `translateX(${slidOff ? "-115%" : "0%"})`,
              opacity: everythingOut ? 0 : 1,
              transition:
                "transform 1.0s cubic-bezier(.7,0,.2,1), opacity .8s ease",
            }}
          >
            <div
              style={{
                fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
                fontSize: 11,
                letterSpacing: 1.4,
                textTransform: "uppercase",
                color: PALETTE.sub,
                marginBottom: 14,
                display: "flex",
                alignItems: "center",
                gap: 10,
                fontWeight: 600,
              }}
            >
              <span style={{ display: "inline-block", width: 14, height: 1, background: PALETTE.accent }} />
              Morning summary
              <span style={{ color: PALETTE.accent, marginLeft: 4 }}>
                {phase === "emailWrite"
                  ? "· drafting"
                  : phase === "emailHold"
                  ? "· ready to send"
                  : isSending || slidOff
                  ? "· sent"
                  : ""}
              </span>
            </div>
            <div style={{ flex: 1, position: "relative", overflow: "hidden" }}>
              {/* Email document — visible during emailWrite/Hold; fades during fadeAndSent */}
              <div
                style={{
                  position: "absolute",
                  inset: 0,
                  opacity: emailContentVisible ? 1 : 0,
                  transition: "opacity 0.7s cubic-bezier(.4,0,.2,1)",
                  overflow: "hidden",
                }}
              >
                <EmailDocument
                  conversation={conversation}
                  mode={emailMode}
                  revealChars={inEmailPhase || isSending || slidOff ? emailCharIdx : 0}
                />
              </div>
              {/* "Sent." caption — fades in as email fades out */}
              <SentCaption visible={showSent} />
            </div>
          </Zone>
        </div>
      </div>
    );
  }

  // ----------------------------------------------------------------------
  // HeroStory — top-level. Owns cycleIdx; mounts ONE HeroStoryCycle at a
  // time keyed by cycle. When cycle completes, advance and remount.
  // ----------------------------------------------------------------------
  function HeroStory({ conversations, emailMode = "borderless", isMobile = false }) {
    const [cycleIdx, setCycleIdx] = React.useState(0);
    const advance = React.useCallback(() => {
      setCycleIdx((i) => i + 1);
    }, []);
    const conv = conversations[cycleIdx % conversations.length];
    return (
      <HeroStoryCycle
        key={cycleIdx}
        conversation={conv}
        emailMode={emailMode}
        onComplete={advance}
        isMobile={isMobile}
      />
    );
  }

  // --- Zone helpers -----------------------------------------------------
  function Zone({ left, children, style }) {
    return (
      <div
        style={{
          position: "absolute",
          left,
          top: 0,
          width: "47%",
          height: "100%",
          display: "flex",
          flexDirection: "column",
          padding: "8px 0",
          ...style,
        }}
      >
        {children}
      </div>
    );
  }

  function ZoneMeta({ left, right }) {
    return (
      <div
        style={{
          display: "flex",
          alignItems: "center",
          justifyContent: "space-between",
          fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
          fontSize: 11,
          letterSpacing: 0.5,
          textTransform: "uppercase",
          color: PALETTE.sub,
          gap: 12,
        }}
      >
        <div style={{ display: "flex", alignItems: "center", gap: 10 }}>{left}</div>
        <div>{right}</div>
      </div>
    );
  }

  function LiveDotInline({ color }) {
    return (
      <span
        style={{
          display: "inline-block",
          width: 7,
          height: 7,
          borderRadius: 999,
          background: color,
          animation: "ol-pulse-inline 1.6s ease-out infinite",
        }}
      >
        <style>{`
          @keyframes ol-pulse-inline {
            0% { box-shadow: 0 0 0 0 ${color}; opacity: 1; }
            70% { box-shadow: 0 0 0 6px transparent; opacity: 0.5; }
            100% { box-shadow: 0 0 0 0 transparent; opacity: 1; }
          }
        `}</style>
      </span>
    );
  }

  Object.assign(window, {
    LiveWaveform,
    Checkmark,
    TranscriptStack,
    EmailDocument,
    HeroStory,
  });
})();
