// Missing Link: the before/after that carries the Solution slide.
//
// The two states are deliberately different LAYOUTS, not one layout recoloured.
// Before AdMel there is no convergence point and no unified dashboard, because
// a single operator view is a thing AdMel creates. Drawing one in the broken
// state would give the product away and make the problem look organised.
//
//   State A  2x2 grid of four isolated system panels, no lines, no centre.
//   State B  four sources feeding one decision layer feeding one dashboard.
//
// The transition is the pitch: the panels slide into a column, the connectors
// draw themselves, and four conflicting stock figures resolve into one.

const { useState, useEffect, useRef, useCallback } = React;

// ───────────────────────── Layout
const STAGE_W = 1820;
const H_MIN = 0.40 * STAGE_W;
const H_MAX = 0.715 * STAGE_W;

// All four panels are the same colour. The box IS the argument — four
// disconnected systems — so only the conflicting numbers carry coral.
const TINTS = ['var(--espresso)', 'var(--espresso)', 'var(--espresso)', 'var(--espresso)'];
// Slot 3 of the 2x2 is deliberately empty: no system of his sits there.
const FIFTH_SLOT = 3;

const lerp = (a, b, t) => a + (b - a) * t;

// Panel boxes in both states; `t` walks one into the other.
function geom(H, t, n) {
  n = n || 3;   // number of real source systems
  const gap = 34;
  // State A: 2x2 filling the upper two thirds
  const aTop = 0.045 * H, aH = (0.545 * H - gap) / 2;
  const aW = (STAGE_W - gap) / 2;
  const before = [0, 1, 2, 3].map(i => ({
    x: (i % 2) * (aW + gap),
    y: aTop + Math.floor(i / 2) * (aH + gap),
    w: aW, h: aH,
  }));

  // State B: a narrow source column on the left
  const bW = 340, bH = 0.145 * H, bTop = 0.10 * H, bStep = 0.20 * H;
  const after = [0, 1, 2, 3].map(i => ({ x: 0, y: bTop + i * bStep, w: bW, h: bH }));
  // Three sources feed the layer; the fourth box is the AdMel panel's slot.

  const panels = before.map((b, i) => ({
    x: lerp(b.x, after[i].x, t), y: lerp(b.y, after[i].y, t),
    w: lerp(b.w, after[i].w, t), h: lerp(b.h, after[i].h, t),
  }));

  const admel = { x: 550, w: 570, y: 0.235 * H, h: 0.53 * H };
  // Same top and height as the decision layer: the two cards are one row
  const dash  = { x: 1230, w: 590, y: 0.235 * H, h: 0.53 * H };
  const portL = { x: admel.x, y: admel.y + admel.h / 2 };
  const portR = { x: admel.x + admel.w, y: admel.y + admel.h / 2 };
  const dashPort = { x: dash.x, y: dash.y + dash.h / 2 };

  const curve = (a, b) => {
    const mx = (a.x + b.x) / 2;
    return `M ${a.x} ${a.y} C ${mx} ${a.y}, ${mx} ${b.y}, ${b.x} ${b.y}`;
  };
  const feeds = after.slice(0, n).map(p => curve({ x: p.x + p.w + 12, y: p.y + p.h / 2 }, portL));

  return { H, panels, admel, dash, feeds, trunk: curve(portR, dashPort) };
}

// ───────────────────────── Type (stage units; scale is ~1.02 at 1920x1080)
const TYPE = {
  panelVendor: 22, panelKey: 22, panelMetric: 24, panelValue: 86, panelFoot: 21,
  payload: 50, meta: 22, metricLabel: 23, metricValue: 60, was: 20,
  cardHead: 22, reduceNum: 62, recLabel: 20, recValue: 23, resolve: 74,
  dashTitle: 24, dashScope: 20, dashNum: 44, dashLabel: 20, dashFooter: 22, aiRecs: 21,
};

// ───────────────────────── Hooks
function useReducedMotion() {
  const [r, setR] = useState(false);
  useEffect(() => {
    const mq = window.matchMedia('(prefers-reduced-motion: reduce)');
    const on = () => setR(mq.matches); on();
    mq.addEventListener('change', on);
    return () => mq.removeEventListener('change', on);
  }, []);
  return r;
}

// Beats: hold the broken state, cite the number, transition, resolve, build the
// dashboard, hold. The resolve beat is the long one on purpose.
// Beat 0 now holds the setup line for two seconds before the panels
// populate, so the viewer knows whose morning this is first.
const BEAT_MS = [5000, 2200, 2000, 2600, 2400, 2200];
const BEATS = BEAT_MS.length;
const B_CITE = 1, B_MOVE = 2, B_RESOLVE = 3, B_DASH = 4;

function useBeats(running, reduced) {
  const [tick, setTick] = useState(0);
  // ?static=1 lands the simulation on its final beat and stops the loop, so a
  // headless capture is deterministic instead of whatever frame it caught.
  if (window.__STATIC) reduced = true;
  useEffect(() => {
    if (reduced || !running) return;
    const id = setTimeout(() => setTick(n => n + 1), BEAT_MS[tick % BEATS]);
    return () => clearTimeout(id);
  }, [running, reduced, tick]);
  if (reduced) return BEATS - 1;
  return tick % BEATS;
}

// Rests at the final value so a throttled frame clock can never leave a metric
// frozen at zero, which on a stage is worse than no animation.
function useCountUp(to, active, dur = 1100, delay = 0) {
  const [p, setP] = useState(1);
  const raf = useRef(null), t1 = useRef(null), t2 = useRef(null);
  useEffect(() => {
    if (!active) { setP(1); return; }
    setP(0);
    const begin = () => {
      const t0 = performance.now();
      const step = (now) => {
        const q = Math.min(1, (now - t0) / dur);
        setP(q);
        if (q < 1) raf.current = requestAnimationFrame(step);
      };
      raf.current = requestAnimationFrame(step);
    };
    t1.current = setTimeout(begin, delay);
    t2.current = setTimeout(() => setP(1), delay + dur + 80);
    return () => { clearTimeout(t1.current); clearTimeout(t2.current); cancelAnimationFrame(raf.current); };
  }, [to, active, dur, delay]);
  return to * (1 - Math.pow(1 - p, 3));
}

function useStageMode() {
  const [stage, setStage] = useState(() => {
    try { return new URLSearchParams(window.location.search).get('stage') === '1'; }
    catch (e) { return false; }
  });
  useEffect(() => {
    const root = document.documentElement;
    if (stage) root.dataset.stage = '1'; else delete root.dataset.stage;
    return () => { delete root.dataset.stage; };
  }, [stage]);

  const toggle = useCallback(() => {
    setStage(s => {
      const next = !s;
      try {
        if (next && !document.fullscreenElement) document.documentElement.requestFullscreen?.();
        if (!next && document.fullscreenElement) document.exitFullscreen?.();
      } catch (e) { /* fullscreen can be refused; stage mode still applies */ }
      return next;
    });
  }, []);

  useEffect(() => {
    const onKey = (e) => {
      if (e.key !== 's' && e.key !== 'S') return;
      const el = document.activeElement;
      if (el && (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA')) return;
      if (e.metaKey || e.ctrlKey || e.altKey) return;
      e.preventDefault(); toggle();
    };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, [toggle]);

  useEffect(() => {
    if (!stage) return;
    let id = null;
    const hide = () => { document.documentElement.dataset.idle = '1'; };
    const wake = () => { delete document.documentElement.dataset.idle; clearTimeout(id); id = setTimeout(hide, 2000); };
    wake();
    window.addEventListener('mousemove', wake);
    window.addEventListener('mousedown', wake);
    return () => {
      clearTimeout(id); delete document.documentElement.dataset.idle;
      window.removeEventListener('mousemove', wake);
      window.removeEventListener('mousedown', wake);
    };
  }, [stage]);

  return { stage, toggle };
}

// ───────────────────────── Pieces

// One system, drawn as a crude mock of its own product in state A and as a
// labelled source in state B. Same element throughout, so it can travel.
const MOVE = '1400ms cubic-bezier(0.65,0,0.2,1)';

function Panel({ p, box, tint, t, i }) {
  const detail = 1 - Math.min(1, t * 1.6);   // the mock UI fades as it becomes a source

  // Derived from the space this panel actually has, never a fixed number. On a
  // short stage (normal mode, where the chat dock eats height) a fixed 86 made
  // the content taller than its box, and because the column is centred inside
  // overflow:hidden it clipped at the top as well as the bottom.
  const headerH = lerp(32, 20, t) + TYPE.panelVendor * 1.2;
  const contentH = box.h - headerH;
  const restH = TYPE.panelKey * 1.2 + TYPE.panelFoot * 1.2 + lerp(12, 4, t) + 10;
  const bigValue = Math.max(44, Math.min(TYPE.panelValue, contentH - restH));
  const smallValue = p.value.length > 5 ? 22 : 34;
  return (
    <div style={{
      position: 'absolute', left: box.x, top: box.y, width: box.w, height: box.h,
      border: 'none',
      borderRadius: lerp(14, 12, t),
      background: 'var(--espresso)',
      boxShadow: t > 0.5 ? 'none' : '0 18px 44px -30px rgba(57,54,48,0.5)',
      overflow: 'hidden', display: 'flex', flexDirection: 'column',
      transition: `left ${MOVE}, top ${MOVE}, width ${MOVE}, height ${MOVE}, border-color 900ms ease, box-shadow 900ms ease, border-radius 900ms ease`,
    }}>
      {/* each panel keeps its own header bar so the four read as four products */}
      <div style={{
        padding: lerp(16, 10, t) + 'px ' + lerp(24, 16, t) + 'px',
        borderBottom: '1px solid var(--on-esp-30)',
        background: 'transparent',
        fontFamily: 'var(--sans)', fontSize: TYPE.panelVendor, fontWeight: 600,
        letterSpacing: '0.08em', textTransform: 'uppercase',
        color: 'var(--on-esp-60)',
        whiteSpace: 'nowrap',
        transition: `padding ${MOVE}, background 900ms ease, color 900ms ease`,
      }}>{p.vendor}</div>

      <div style={{
        flex: 1, minHeight: 0, padding: '0 ' + lerp(24, 16, t) + 'px',
        display: 'flex', flexDirection: 'column', justifyContent: 'center',
        gap: lerp(6, 2, t),
        transition: `padding ${MOVE}, gap ${MOVE}`,
      }}>
        <div style={{
          fontFamily: 'var(--sans)', fontSize: TYPE.panelKey, color: 'var(--on-esp-60)',
          opacity: detail, maxHeight: detail < 0.05 ? 0 : 48, overflow: 'hidden',
          transition: `opacity 600ms ease, max-height ${MOVE}`, whiteSpace: 'nowrap',
        }}>{p.key}</div>

        <div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', gap: 16 }}>
          <span style={{
            fontFamily: 'var(--sans)', fontSize: lerp(TYPE.panelMetric, 20, t), color: 'var(--on-esp-30)',
            letterSpacing: '0.08em', textTransform: 'uppercase', whiteSpace: 'nowrap', fontWeight: 600,
            transition: `font-size ${MOVE}`,
          }}>{p.metric}</span>
          <span className={'ml-val-' + i} style={{
            fontFamily: 'var(--sans)', fontWeight: 700, letterSpacing: '-0.03em',
            fontSize: lerp(bigValue, smallValue, t), lineHeight: 1,
            color: 'var(--coral)', whiteSpace: 'nowrap',
            transition: `font-size ${MOVE}, color 900ms ease`,
          }}>{p.value}</span>
        </div>

        <div style={{
          fontSize: TYPE.panelFoot, color: 'var(--on-esp-30)', opacity: detail,
          maxHeight: detail < 0.05 ? 0 : 48, overflow: 'hidden',
          transition: `opacity 600ms ease, max-height ${MOVE}`, whiteSpace: 'nowrap',
        }}>{p.foot}</div>
      </div>
    </div>
  );
}

// The four figures colliding and resolving. This is the argument.
//
// The resolved number must not equal any input. When it did, the slide read as
// though AdMel just picked whichever source it trusted, which proves nothing.
// Showing the arithmetic is the proof: every system is wrong in its own way,
// and the true figure is derived, not chosen.
function Resolve({ show, panels, T }) {
  const from = T.sim.resolveFrom;
  return (
    <div className="ml-resolve" style={{
      position: 'absolute', inset: 0, zIndex: 7,
      background: 'var(--paper)',
      display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center',
      gap: 20, opacity: show ? 1 : 0, pointerEvents: 'none',
      transition: 'opacity 420ms ease',
    }}>
      {/* what the four systems each claimed */}
      <div style={{ display: 'flex', alignItems: 'center', gap: 20 }}>
        {panels.map((p, i) => (
          <React.Fragment key={i}>
            {i > 0 && <span style={{ fontSize: 30, color: 'var(--dim)' }}>·</span>}
            <span style={{
              fontFamily: 'var(--sans)', fontWeight: 700, fontSize: TYPE.resolve * 0.62,
              letterSpacing: '-0.03em', color: 'var(--coral)',
              textDecoration: 'line-through', textDecorationThickness: '3px',
              whiteSpace: 'nowrap',
            }}>{p.value}</span>
          </React.Fragment>
        ))}
      </div>

      {/* the arithmetic, so an investor can check it */}
      <div style={{
        display: 'flex', flexDirection: 'column', gap: 8,
        borderTop: '1px solid var(--line)', paddingTop: 18, minWidth: 520,
      }}>
        <div style={{ display: 'flex', alignItems: 'baseline', gap: 16 }}>
          <span style={{ width: 34, textAlign: 'right', fontFamily: 'var(--sans)', fontSize: TYPE.meta, color: 'var(--dim)' }} />
          <span style={{ width: 84, textAlign: 'right', fontFamily: 'var(--sans)', fontWeight: 700, fontSize: 40, color: 'var(--text)' }}>{from.v}</span>
          <span style={{ fontSize: TYPE.meta, color: 'var(--muted)' }}>{from.k}</span>
        </div>
        {T.sim.resolveSteps.map(st => (
          <div key={st.k} style={{ display: 'flex', alignItems: 'baseline', gap: 16 }}>
            <span style={{ width: 34, textAlign: 'right', fontFamily: 'var(--sans)', fontSize: 34, color: 'var(--coral)' }}>{st.op}</span>
            <span style={{ width: 84, textAlign: 'right', fontFamily: 'var(--sans)', fontWeight: 700, fontSize: 40, color: 'var(--coral)' }}>{st.v}</span>
            <span style={{ fontSize: TYPE.meta, color: 'var(--muted)' }}>{st.k}</span>
          </div>
        ))}
      </div>

      <div style={{
        display: 'flex', alignItems: 'baseline', gap: 20,
        border: '3px solid var(--accent)', borderRadius: 18,
        background: 'var(--bg-3)', padding: '12px 32px',
        boxShadow: '0 30px 70px -30px rgba(57,54,48,0.45)',
      }}>
        <span style={{
          fontFamily: 'var(--sans)', fontSize: TYPE.meta, letterSpacing: '0.08em',
          textTransform: 'uppercase', color: 'var(--accent)',
        }}>{T.sim.resolveLabel}</span>
        <span style={{
          fontFamily: 'var(--sans)', fontWeight: 700, fontSize: TYPE.resolve,
          lineHeight: 1, letterSpacing: '-0.04em', color: 'var(--accent)',
        }}>{T.sim.resolveValue}</span>
      </div>

      <div style={{ fontSize: TYPE.meta, color: 'var(--dim)' }}>{T.sim.resolveNote}</div>
    </div>
  );
}

function AdMelCard({ show, box, T }) {
  const rows = [
    [T.sim.recWhat, T.sim.recWhatV], [T.sim.recHowMuch, T.sim.recHowMuchV],
    [T.sim.recWhen, T.sim.recWhenV], [T.sim.recWhy, T.sim.recWhyV],
    [T.sim.recConfidence, T.sim.recConfidenceV],
  ];
  return (
    <div style={{
      position: 'absolute', left: box.x, top: box.y, width: box.w, height: box.h,
      border: '3px solid var(--accent)', borderRadius: 22, background: 'var(--bg-3)',
      boxShadow: '0 34px 80px -30px rgba(57,54,48,0.45)',
      opacity: show ? 1 : 0, transform: show ? 'scale(1)' : 'scale(0.84)',
      transition: 'opacity 600ms cubic-bezier(0.2,0.8,0.2,1) 200ms, transform 600ms cubic-bezier(0.2,0.8,0.2,1) 200ms',
      display: 'flex', flexDirection: 'column', overflow: 'hidden',
    }}>
      <div style={{
        padding: '14px 22px', borderBottom: '1px solid var(--line)',
        display: 'flex', alignItems: 'center', gap: 12, whiteSpace: 'nowrap',
      }}>
        <img src="heroes/admel-logo.png" alt="AdMel" style={{ height: 34, width: 'auto', display: 'block' }} />
        <span style={{
          marginLeft: 'auto', fontFamily: 'var(--sans)', fontSize: TYPE.cardHead,
          color: 'var(--accent)', letterSpacing: '0.08em', textTransform: 'uppercase',
        }}>● live</span>
      </div>

      <div style={{
        padding: '10px 22px 0',
        fontFamily: 'var(--sans)', fontSize: TYPE.aiRecs, letterSpacing: '0.08em',
        textTransform: 'uppercase', color: 'var(--accent)', whiteSpace: 'nowrap',
      }}>{T.sim.aiRecs}</div>

      <div style={{
        padding: '14px 22px', borderBottom: '1px solid var(--line)',
        display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 20,
        fontFamily: 'var(--sans)', fontSize: TYPE.meta, letterSpacing: '0.1em',
        textTransform: 'uppercase', color: 'var(--dim)', whiteSpace: 'nowrap',
      }}>
        <span><b style={{ fontFamily: 'var(--sans)', fontSize: TYPE.reduceNum, color: 'var(--dim)', letterSpacing: '-0.02em' }}>4</b> in</span>
        <span style={{ color: 'var(--accent)', fontSize: 30 }}>→</span>
        <span style={{ color: 'var(--accent)' }}><b style={{ fontFamily: 'var(--sans)', fontSize: TYPE.reduceNum, letterSpacing: '-0.02em' }}>1</b> out</span>
      </div>

      {/* The reasoning is the differentiator, so every line gets equal weight */}
      <div style={{ flex: 1, minHeight: 0, padding: '14px 22px', display: 'flex', flexDirection: 'column', justifyContent: 'space-around' }}>
        {rows.map(([k, v]) => (
          <div key={k} style={{ display: 'flex', gap: 16, alignItems: 'baseline' }}>
            <span style={{
              flex: '0 0 115px', fontFamily: 'var(--sans)', fontSize: TYPE.recLabel,
              letterSpacing: '0.1em', textTransform: 'uppercase', color: 'var(--dim)',
            }}>{k}</span>
            <span style={{
              flex: 1, fontFamily: 'var(--sans)', fontWeight: 500,
              fontSize: TYPE.recValue, lineHeight: 1.25, color: 'var(--text)', whiteSpace: 'nowrap',
            }}>{v}</span>
          </div>
        ))}
      </div>
    </div>
  );
}

// What changed for the person doing the ordering. This card does not exist
// before AdMel, and the delta is the argument: both numbers in a row carry the
// same weight so the eye reads the movement, not a footnote.
function DashRow({ r, revealed, delay, lang }) {
  const n = useCountUp(r.target, revealed, 900, delay);
  const shown = revealed
    ? (r.dp ? n.toFixed(1) : String(Math.round(n))) + (r.suffix || '')
    : r.to;
  const out = lang === 'no' ? shown.replace('.', ',') : shown;
  return (
    <div style={{
      display: 'flex', alignItems: 'baseline', gap: 16,
      padding: '14px 0', borderTop: '1px solid var(--line)',
    }}>
      <span style={{
        fontFamily: 'var(--sans)', fontWeight: 700, fontSize: TYPE.dashNum, lineHeight: 1,
        letterSpacing: '-0.03em', color: 'var(--dim)', whiteSpace: 'nowrap',
      }}>{r.from}</span>

      {/* the arrow draws rather than appearing, so the movement is the event */}
      <span style={{
        flex: '0 0 44px', height: 3, background: 'var(--accent)', alignSelf: 'center',
        transformOrigin: 'left center',
        transform: revealed ? 'scaleX(1)' : 'scaleX(0)',
        transition: `transform 420ms cubic-bezier(0.3,0,0.2,1) ${delay}ms`,
        position: 'relative',
      }}>
        <span style={{
          position: 'absolute', right: -2, top: -5,
          borderLeft: '11px solid var(--accent)',
          borderTop: '6.5px solid transparent', borderBottom: '6.5px solid transparent',
          opacity: revealed ? 1 : 0, transition: `opacity 200ms ease ${delay + 380}ms`,
        }} />
      </span>

      <span style={{
        fontFamily: 'var(--sans)', fontWeight: 700, fontSize: TYPE.dashNum, lineHeight: 1,
        letterSpacing: '-0.03em', color: 'var(--accent)', whiteSpace: 'nowrap',
        opacity: revealed ? 1 : 0, transition: `opacity 260ms ease ${delay}ms`,
      }}>{out}</span>

      <span style={{
        flex: 1, fontSize: TYPE.dashLabel, lineHeight: 1.25, color: 'var(--muted)',
      }}>{r.label}</span>
    </div>
  );
}

function Dashboard({ show, box, T, lang }) {
  // Grey before-numbers land first; the movement starts 400ms later
  const [revealed, setRevealed] = useState(false);
  useEffect(() => {
    if (!show) { setRevealed(false); return; }
    const id = setTimeout(() => setRevealed(true), 400);
    return () => clearTimeout(id);
  }, [show]);

  return (
    <div style={{
      position: 'absolute', left: box.x, top: box.y, width: box.w, height: box.h,
      border: '2px solid var(--accent)', borderRadius: 20,
      background: 'transparent',
      opacity: show ? 1 : 0, transform: show ? 'translateX(0)' : 'translateX(40px)',
      transition: 'opacity 600ms ease 320ms, transform 600ms cubic-bezier(0.2,0.8,0.2,1) 320ms',
      display: 'flex', flexDirection: 'column', overflow: 'hidden',
      padding: '22px 22px 18px',
    }}>
      <div style={{
        fontFamily: 'var(--sans)', fontWeight: 500, fontSize: TYPE.dashTitle,
        lineHeight: 1.15, color: 'var(--text)',
      }}>{T.sim.dashTitle}</div>
      <div style={{
        marginTop: 6, fontSize: TYPE.dashScope, lineHeight: 1.3, color: 'var(--dim)',
      }}>{T.sim.dashScope}</div>

      <div style={{ marginTop: 16, flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column', justifyContent: 'center' }}>
        {T.sim.dashRows.map((r, i) => (
          <DashRow key={r.label} r={r} revealed={revealed} delay={i * 180} lang={lang} />
        ))}
      </div>

      <div style={{
        paddingTop: 14, borderTop: '1px solid var(--line)',
        fontFamily: 'var(--sans)', fontWeight: 600, fontSize: TYPE.dashFooter,
        lineHeight: 1.25, color: 'var(--accent)',
      }}>{T.sim.dashFooter}</div>
    </div>
  );
}

// ───────────────────────── Slide
function MissingLinkSlide({ embedPhase, fifth }) {
  const embedded = !!embedPhase;
  const slideStyles = window.__slideStyles;
  const lang = window.__useLang(); const T = window.TDATA[lang];
  const reduced = useReducedMotion();
  const { stage, toggle } = useStageMode();

  // ?state=a or ?state=b pins a state, so a screenshot of this slide is
  // deterministic instead of whatever beat the loop happened to be on.
  const [pinned, setPinned] = useState(() => {
    try {
      const v = new URLSearchParams(window.location.search).get('state');
      if (v === 'a') return false;
      if (v === 'b') return true;
    } catch (e) { /* fall through to autoplay */ }
    return null;
  });
  useEffect(() => { if (stage) setPinned(null); }, [stage]);
  const beat = useBeats(pinned === null, reduced);

  // Setup holds alone before anything populates.
  const [primed, setPrimed] = useState(embedded || reduced || pinned !== null);
  useEffect(() => {
    if (embedded || reduced || pinned !== null) { setPrimed(true); return; }
    setPrimed(false);
    const id = setTimeout(() => setPrimed(true), 2000);
    return () => clearTimeout(id);
  }, [reduced, pinned]);

  const auto = {
    cite:    beat === B_CITE,
    moved:   beat >= B_MOVE,
    resolve: beat === B_RESOLVE,
    dash:    beat >= B_DASH,
  };
  let ph = pinned === null ? auto
    : { cite: false, moved: pinned, resolve: false, dash: pinned };
  // Driven from the solution room's beat rail when embedded.
  if (embedded) {
    ph = {
      cite: false,
      moved: false,
      resolve: embedPhase === 'resolve',
      dash: false,
    };
  }
  const t = ph.moved ? 1 : 0;

  // Manual reconciliation clock, climbing while the state is broken
  const climb = useCountUp(4, !ph.moved && pinned === null, 2600, 0);

  const [ready, setReady] = useState(false);
  const [xform, setXform] = useState('');
  const [stageH, setStageH] = useState(900);
  const ref = useRef(null);

  useEffect(() => {
    const el = ref.current; if (!el) return;
    const apply = () => {
      const w = el.clientWidth, h = el.clientHeight; if (!w || !h) return;
      const H = Math.max(H_MIN, Math.min(H_MAX, STAGE_W * h / w));
      const s = Math.min(w / STAGE_W, h / H);
      setStageH(H);
      setXform(`translate(${(w - STAGE_W * s) / 2}px, ${(h - H * s) / 2}px) scale(${s})`);
      setReady(true);
    };
    apply();
    const raf = requestAnimationFrame(apply);
    const obs = new ResizeObserver(apply); obs.observe(el);
    return () => { cancelAnimationFrame(raf); obs.disconnect(); };
  }, []);

  const g = geom(stageH, t, (T.sim.panels || []).length);
  const P = T.sim.panels;

  return (
    <div className={'slide-wrap room room-field sim-slide' + (embedded ? ' sim-embed' : '')} style={embedded ? {
      position: 'relative', width: '100%', height: '100%',
      display: 'flex', flexDirection: 'column', padding: 0,
    } : {
      ...slideStyles.wrap,
      padding: 'clamp(30px, 3vw, 44px) clamp(12px, 1.6vw, 28px) clamp(190px, 21vh, 235px)',
    }}>
      <div className="sim-chrome" style={{
        display: embedded ? 'none' : undefined, display: 'flex', alignItems: 'center', gap: 14, flexWrap: 'wrap', flex: '0 0 auto' }}>
        <div style={{ display: 'inline-flex', padding: 3, gap: 3, border: '1px solid var(--line)', borderRadius: 999, background: 'var(--bg-3)' }}>
          {[false, true].map(v => (
            <button key={String(v)} onClick={() => setPinned(v)} style={{
              padding: '6px 16px', borderRadius: 999, fontFamily: 'var(--sans)', fontSize: 11,
              letterSpacing: '0.1em', textTransform: 'uppercase',
              background: pinned === v ? 'var(--card)' : 'transparent',
              color: pinned === v ? 'var(--espresso)' : 'var(--dim)',
              fontWeight: pinned === v ? 700 : 500,
            }}>{v ? T.sim.withLabel : T.sim.withoutLabel}</button>
          ))}
        </div>
        {pinned !== null && (
          <button onClick={() => setPinned(null)} style={{ fontFamily: 'var(--sans)', fontSize: 10, color: 'var(--dim)', letterSpacing: '0.1em', textTransform: 'uppercase' }}>↻ replay</button>
        )}
        <span style={{ fontFamily: 'var(--sans)', fontSize: 10, color: 'var(--dim)', letterSpacing: '0.08em' }}>{T.sim.compareHint}</span>
        <button onClick={toggle} style={{ marginLeft: 'auto', fontFamily: 'var(--sans)', fontSize: 10, color: 'var(--dim)', letterSpacing: '0.1em', textTransform: 'uppercase' }}>
          {stage ? '✕ exit stage' : '⛶ stage · s'}
        </button>
      </div>

      <div className="simulation-stage" ref={ref} style={{ marginTop: embedded ? 0 : 22, position: 'relative', width: '100%', flex: 1, minHeight: 200, overflow: 'hidden' }}>
        <div style={{ position: 'absolute', inset: 0, transformOrigin: 'top left', opacity: ready ? 1 : 0, transform: xform }}>
          <div style={{ position: 'relative', width: STAGE_W, height: stageH }}>

            {/* Connectors: they do not exist until AdMel draws them */}
            <svg viewBox={`0 0 ${STAGE_W} ${stageH}`} width={STAGE_W} height={stageH}
                 style={{ position: 'absolute', inset: 0, pointerEvents: 'none' }}>
              <defs>
                <marker id="ml-arw" viewBox="0 0 8 8" refX="6" refY="4" markerWidth="5" markerHeight="5" orient="auto">
                  <path d="M 0 0 L 8 4 L 0 8 z" fill="var(--accent)" />
                </marker>
              </defs>
              {ph.moved && g.feeds.map((d, i) => (
                <path key={i} d={d} fill="none" stroke="var(--accent)" strokeOpacity="0.55"
                  strokeWidth="3" strokeLinecap="round" pathLength="1"
                  style={{ strokeDasharray: 1, animation: `mlDraw 900ms ease ${300 + i * 110}ms both` }} />
              ))}
              {ph.dash && (
                <path d={g.trunk} fill="none" stroke="var(--accent)" strokeWidth="5" strokeLinecap="round"
                  pathLength="1" markerEnd="url(#ml-arw)"
                  style={{ strokeDasharray: 1, animation: 'mlDraw 700ms ease 200ms both' }} />
              )}
            </svg>

            {/* Panels wait for the setup line to land. The four dim when the
                fifth arrives so it owns the stage. */}
            <div style={{
              opacity: primed ? (fifth ? 0.4 : 1) : 0,
              transition: 'opacity 600ms ease',
            }}>
              {P.map((p, i) => (
                <Panel key={i} i={i} p={p} box={g.panels[i]} tint={TINTS[i]} t={t} />
              ))}
            </div>

            {/* The fifth panel. Same geometry as the other four, in the row
                where none of his systems sit. */}
            {fifth && (() => {
              const box = g.panels[FIFTH_SLOT];
              return (
                <div className="ml-fifth" style={{
                  position: 'absolute', left: box.x, top: box.y,
                  width: box.w, height: box.h,
                  border: '2px solid var(--blue)', borderRadius: 14,
                  background: 'var(--paper)', overflow: 'hidden',
                  display: 'flex', flexDirection: 'column',
                }}>
                  <div style={{
                    padding: '12px 24px', borderBottom: '1px solid var(--blue)',
                    background: 'var(--blue)', color: 'var(--paper)',
                    fontFamily: 'var(--sans)', fontSize: TYPE.panelVendor, fontWeight: 600,
                    letterSpacing: '0.08em', textTransform: 'uppercase', whiteSpace: 'nowrap',
                  }}>{T.sim.fifthVendor}</div>
                  <div style={{
                    flex: 1, minHeight: 0, padding: '0 24px',
                    display: 'flex', flexDirection: 'column', justifyContent: 'center', gap: 6,
                  }}>
                    <div style={{
                      fontFamily: 'var(--sans)', fontSize: TYPE.panelKey, color: 'var(--ink-50)',
                    }}>{T.sim.fifthKey}</div>
                    <div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', gap: 16 }}>
                      <span style={{
                        fontFamily: 'var(--sans)', fontSize: TYPE.panelMetric, color: 'var(--ink-50)',
                        letterSpacing: '0.08em', textTransform: 'uppercase', fontWeight: 600,
                      }}>{T.sim.fifthMetric}</span>
                      <span style={{
                        fontFamily: 'var(--sans)', fontWeight: 700, letterSpacing: '-0.03em',
                        fontSize: Math.max(40, Math.min(TYPE.panelValue, box.h * 0.30)),
                        lineHeight: 1, color: 'var(--blue)',
                      }}>{T.sim.fifthValue}</span>
                    </div>
                    <div style={{
                      fontSize: TYPE.panelFoot, color: 'var(--ink-50)',
                    }}>{T.sim.fifthFoot}</div>
                  </div>
                </div>
              );
            })()}

            {/* Setup. The viewer is told whose Tuesday this is before any
                number appears; without it the four boxes have no referent. */}
            <div style={{
              position: 'absolute', left: '10%', right: '10%', top: 0.30 * stageH,
              textAlign: 'center', pointerEvents: 'none', zIndex: 6,
              opacity: primed ? 0 : 1,
              transition: 'opacity 500ms ease',
            }}>
              <div style={{
                fontFamily: 'var(--serif)', fontWeight: 500, fontSize: TYPE.meta * 2,
                lineHeight: 1.3, color: 'var(--espresso)', maxWidth: '38ch', margin: '0 auto',
              }}>{T.sim.setup}</div>
            </div>

            {/* Payoff. Names what the resolution just demonstrated — the whole
                product thesis, which was previously left to inference. */}
            <div style={{
              position: 'absolute', left: '8%', right: '8%', top: 0.615 * stageH,
              textAlign: 'center', pointerEvents: 'none',
              opacity: ph.resolve ? 1 : 0,
              transition: 'opacity 500ms ease 300ms',
            }}>
              <div style={{
                fontFamily: 'var(--serif)', fontWeight: 500, fontSize: TYPE.meta * 1.6,
                lineHeight: 1.3, color: 'var(--espresso)', maxWidth: '46ch', margin: '0 auto',
              }}>{T.sim.payoff}</div>
            </div>

            {/* The cost of starting. The answer to the only real objection in
                this segment, and the reason the channel scales without
                services headcount. Blue marks the claim, nothing else. */}
            <div style={{
              position: 'absolute', left: '31%', right: '4%', top: 0.82 * stageH,
              opacity: ph.moved ? 1 : 0,
              transition: 'opacity 500ms ease 200ms', pointerEvents: 'none',
            }}>
              <div style={{
                fontFamily: 'var(--sans)', fontSize: 13, fontWeight: 600,
                letterSpacing: '0.08em', textTransform: 'uppercase', color: 'var(--ink-50)',
              }}>{T.sim.startLabel}</div>
              <div style={{
                marginTop: 8, maxWidth: '62ch',
                fontFamily: 'var(--sans)', fontSize: 18, lineHeight: 1.45,
                color: 'var(--ink-70)',
              }}>
                {T.sim.startBodyPre}
                <span style={{ color: 'var(--blue)', fontWeight: 600 }}>{T.sim.startBodyHi}</span>
                {T.sim.startBodyPost}
              </div>
            </div>

            {/* State A only: the payload, the manual grind, the retailer's cost */}
            <div style={{
              position: 'absolute', left: 0, right: 0, top: 0.615 * stageH,
              textAlign: 'center', opacity: (ph.moved || ph.cite || !primed || fifth) ? 0 : 1,
              transition: 'opacity 400ms ease', pointerEvents: 'none',
            }}>
              <div style={{
                fontFamily: 'var(--sans)', fontWeight: 700, fontSize: TYPE.payload,
                letterSpacing: '-0.02em', color: 'var(--espresso)', lineHeight: 1.1,
              }}>{T.sim.payload}</div>
              <div style={{
                marginTop: 16, display: 'inline-flex', alignItems: 'center', gap: 12,
                border: '1px solid var(--coral)', borderRadius: 999,
                padding: '7px 20px', fontFamily: 'var(--sans)', fontSize: TYPE.meta,
                letterSpacing: '0.08em', textTransform: 'uppercase', color: 'var(--coral)',
              }}>
                <span className="ml-spin">⟳</span>
                {T.sim.reconcilingLabel} · {window.__hm(Math.round(120 + climb * 6))}
              </div>
            </div>

            {/* The cursor that never settles */}
            {!ph.moved && !reduced && (
              <div className="ml-cursor" style={{
                position: 'absolute', width: 26, height: 26, borderRadius: 999,
                border: '3px solid var(--coral)',
                background: 'transparent', pointerEvents: 'none', zIndex: 5,
              }} />
            )}

            {/* Held citation. Sits in the payload's slot, not over the panels:
                covering the four conflicting numbers defeats the point of them. */}
            <div style={{
              position: 'absolute', left: '8%', right: '8%', top: 0.605 * stageH,
              textAlign: 'center', zIndex: 6, pointerEvents: 'none',
              opacity: ph.cite ? 1 : 0, transition: 'opacity 420ms ease',
            }}>
              <div style={{ fontFamily: 'var(--sans)', fontWeight: 600, fontSize: TYPE.payload * 0.78, lineHeight: 1.15, color: 'var(--text)' }}>
                {T.sim.distortion}
              </div>
              <div style={{
                marginTop: 14, fontFamily: 'var(--sans)', fontSize: TYPE.meta,
                letterSpacing: '0.08em', textTransform: 'uppercase', color: 'var(--dim)',
              }}>{T.sim.distortionSource}</div>
            </div>

            <AdMelCard show={ph.moved} box={g.admel} T={T} />
            <Dashboard show={ph.dash} box={g.dash} T={T} lang={lang} />

            {/* Four answers becoming one. The whole slide exists for this. */}
            {/* All four answer the same question in their own language, so all
                four are struck before the derived figure lands. */}
            <Resolve show={ph.resolve} panels={P} T={T} />
          </div>
        </div>
      </div>

      <style>{`
        @keyframes mlDraw { from { stroke-dashoffset: 1; } to { stroke-dashoffset: 0; } }
        @keyframes mlSpin { to { transform: rotate(360deg); } }
        .ml-spin { display: inline-block; animation: mlSpin 2.2s linear infinite; }
        @keyframes mlRoam {
          0%   { left: 24%; top: 20%; }
          22%  { left: 74%; top: 22%; }
          46%  { left: 26%; top: 46%; }
          70%  { left: 76%; top: 48%; }
          88%  { left: 48%; top: 30%; }
          100% { left: 24%; top: 20%; }
        }
        .ml-cursor { animation: mlRoam 6s cubic-bezier(0.6,0,0.4,1) infinite; }
      `}</style>
    </div>
  );
}

Object.assign(window, { MissingLinkSlide });
