/* App + timeline orchestration.
   A single `t` (seconds in the loop) drives all phases. Cursor target
   is computed from refs to the current focus element, so it always
   lands on the right button regardless of layout. */

const { useEffect, useLayoutEffect, useMemo, useRef, useState } = React;

const TYPED_COMMAND = '/agntux triage';

/* ── Timeline milestones (seconds) ─────────────────────────────
   Scenes:
     S1 = triage → slack reply (Northstream)
     S2 = triage (again) → expand details → gmail reply (Catalyst) */
const T = {
  // Scene 1
  s1_type_start:    1.0,
  s1_type_per_char: 0.10,
  s1_cursor_send:   2.7,
  s1_send_press:    3.1,
  s1_user_msg:      3.15,
  s1_tool_status:   3.5,
  s1_orb_in:        4.0,
  s1_triage_in:    4.4,
  s1_cursor_draft:  7.4,
  s1_draft_press:   8.0,
  s1_fill_open:     8.2,
  s1_cursor_send2:  8.7,
  s1_send_press2:   9.2,
  s1_user_open:     9.25,
  s1_hero_resolved: 9.3,
  s1_tool_compose:  9.7,
  s1_slack_in:     10.4,
  s1_cursor_body:  10.8,
  s1_body_focus:   11.5,
  s1_append_start: 11.7,
  s1_append_per:    0.13,
  s1_cursor_card_send: 14.7,
  s1_card_send_press:  15.5,
  s1_submitted:    15.8,
  s1_fill_send:    15.85,
  s1_cursor_send3: 16.4,
  s1_send_press3:  17.0,
  s1_user_send:    17.05,
  s1_connector_in: 17.6,
  s1_connector_done: 18.7,

  // gap
  s2_anchor:       20.5,

  // Scene 2
  s2_type_start:   20.7,
  s2_type_per_char: 0.10,
  s2_cursor_send:  22.4,
  s2_send_press:   22.8,
  s2_user_msg:     22.85,
  s2_tool_status:  23.2,
  s2_triage_in:    23.9,
  s2_cursor_details: 26.5,
  s2_details_press: 27.1,
  s2_expanded:     27.3,
  s2_cursor_draft: 29.7,
  s2_draft_press:  30.2,
  s2_fill_open:    30.35,
  s2_hero_resolved: 30.5,
  s2_cursor_send2: 30.8,
  s2_send_press2:  31.3,
  s2_user_open:    31.35,
  s2_tool_compose: 31.8,
  s2_gmail_in:     32.6,
  s2_cursor_body:  33.0,
  s2_body_focus:   33.7,
  s2_append_start: 33.9,
  s2_append_per:    0.085,
  s2_cursor_card_send: 37.9,
  s2_card_send_press:  38.5,
  s2_submitted:    38.75,
  s2_fill_send:    38.8,
  s2_cursor_send3: 39.4,
  s2_send_press3:  39.9,
  s2_user_send:    39.95,
  s2_connector_in: 40.4,
  s2_connector_done: 41.8,

  hold_until:      43.5,
  fade_out:        44.5,
  loop:            45.7,
};

const LOOP_LENGTH = T.loop;

function useLoopClock() {
  const [t, setT] = useState(0);
  useEffect(() => {
    let raf = 0;
    let start = performance.now();
    const tick = (now) => {
      const elapsed = (now - start) / 1000;
      if (elapsed >= LOOP_LENGTH) {
        start = now;
        setT(0);
      } else {
        setT(elapsed);
      }
      raf = requestAnimationFrame(tick);
    };
    raf = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(raf);
  }, []);
  return t;
}

function relativeCenter(el, ancestor) {
  if (!el || !ancestor) return null;
  let x = 0, y = 0, cur = el;
  while (cur && cur !== ancestor) {
    x += cur.offsetLeft;
    y += cur.offsetTop;
    cur = cur.offsetParent;
  }
  // Adjust for thread scroll offset (the body lives inside a scrollable container)
  const thread = ancestor.querySelector('.thread');
  if (thread && thread.contains(el)) {
    y -= thread.scrollTop;
  }
  return { x: x + el.offsetWidth / 2, y: y + el.offsetHeight / 2 };
}

function relativeRight(el, ancestor, insetX = 16, fracY = 0.5) {
  if (!el || !ancestor) return null;
  let x = 0, y = 0, cur = el;
  while (cur && cur !== ancestor) {
    x += cur.offsetLeft;
    y += cur.offsetTop;
    cur = cur.offsetParent;
  }
  const thread = ancestor.querySelector('.thread');
  if (thread && thread.contains(el)) {
    y -= thread.scrollTop;
  }
  return { x: x + el.offsetWidth - insetX, y: y + el.offsetHeight * fracY };
}

function useCanvasScale(stageRef, canvasRef) {
  useLayoutEffect(() => {
    const compute = () => {
      const s = stageRef.current, c = canvasRef.current;
      if (!s || !c) return;
      const r = s.getBoundingClientRect();
      if (r.width === 0 || r.height === 0) return;
      const scale = Math.min(r.width / 1600, r.height / 900);
      // translate(-800, -450) re-centers the canvas (which has its
      // top-left origin pinned to viewport center via CSS) before
      // scaling — composes cleanly with scale().
      c.style.transform = `translate(-800px, -450px) scale(${scale})`;
      c.style.transformOrigin = '50% 50%';
    };
    compute();
    // ResizeObserver fires for any size change, including iframe
    // boot-time when getBoundingClientRect was briefly 0.
    const ro = new ResizeObserver(() => compute());
    if (stageRef.current) ro.observe(stageRef.current);
    window.addEventListener('resize', compute);
    // Recompute again on next frame in case fonts loaded after first paint.
    const raf = requestAnimationFrame(compute);
    return () => {
      ro.disconnect();
      window.removeEventListener('resize', compute);
      cancelAnimationFrame(raf);
    };
  }, [stageRef, canvasRef]);
}

/* ── derive: read `t` and produce all UI inputs ─────────────── */
function derive(t) {
  // ── Composer text + send-button state ──────────────────────
  let composerText = '';
  let composerCaret = true;

  // Scene 1 typing
  if (t >= T.s1_type_start && t < T.s1_send_press) {
    const chars = Math.min(TYPED_COMMAND.length,
      Math.floor((t - T.s1_type_start) / T.s1_type_per_char));
    composerText = TYPED_COMMAND.slice(0, chars);
  }
  // After Scene 1 first send: blank until open-slack fill
  else if (t >= T.s1_send_press && t < T.s1_fill_open) composerText = '';
  // open_slack prompt fills the composer
  else if (t >= T.s1_fill_open && t < T.s1_send_press2) composerText = window.PROMPTS.open_slack;
  // cleared after second send
  else if (t >= T.s1_send_press2 && t < T.s1_fill_send) composerText = '';
  // send_slack envelope fills
  else if (t >= T.s1_fill_send && t < T.s1_send_press3) composerText = window.PROMPTS.send_slack;
  // cleared
  else if (t >= T.s1_send_press3 && t < T.s2_type_start) composerText = '';
  // Scene 2 typing
  else if (t >= T.s2_type_start && t < T.s2_send_press) {
    const chars = Math.min(TYPED_COMMAND.length,
      Math.floor((t - T.s2_type_start) / T.s2_type_per_char));
    composerText = TYPED_COMMAND.slice(0, chars);
  }
  else if (t >= T.s2_send_press && t < T.s2_fill_open) composerText = '';
  else if (t >= T.s2_fill_open && t < T.s2_send_press2) composerText = window.PROMPTS.open_gmail;
  else if (t >= T.s2_send_press2 && t < T.s2_fill_send) composerText = '';
  else if (t >= T.s2_fill_send && t < T.s2_send_press3) composerText = window.PROMPTS.save_gmail;
  else if (t >= T.s2_send_press3) composerText = '';

  // ── Visibility flags for thread items ──────────────────────
  const flags = {
    s1_user_msg:        t >= T.s1_user_msg,
    s1_tool_status:     t >= T.s1_tool_status,
    s1_orb:             t >= T.s1_orb_in && t < T.s1_triage_in + 0.2,
    s1_triage:          t >= T.s1_triage_in && t < T.s2_triage_in - 0.4,
    s1_user_open:       t >= T.s1_user_open,
    s1_tool_compose:    t >= T.s1_tool_compose,
    s1_slack:           t >= T.s1_slack_in,
    s1_user_send:       t >= T.s1_user_send,
    s1_connector:       t >= T.s1_connector_in,

    s2_user_msg:        t >= T.s2_user_msg,
    s2_tool_status:     t >= T.s2_tool_status,
    s2_triage:          t >= T.s2_triage_in,
    s2_user_open:       t >= T.s2_user_open,
    s2_tool_compose:    t >= T.s2_tool_compose,
    s2_gmail:           t >= T.s2_gmail_in,
    s2_user_send:       t >= T.s2_user_send,
    s2_connector:       t >= T.s2_connector_in,
  };

  // ── Hero row state on scene 1 triage card (Northstream) ────
  let s1_heroState = 'idle';
  if (t >= T.s1_cursor_draft && t < T.s1_draft_press) s1_heroState = 'hover';
  else if (t >= T.s1_draft_press && t < T.s1_hero_resolved) s1_heroState = 'pressed';
  else if (t >= T.s1_hero_resolved) s1_heroState = 'resolved';

  // ── Hero row state on scene 2 triage card (Catalyst) ───────
  let s2_heroState = 'idle';
  if (t >= T.s2_cursor_details && t < T.s2_details_press) s2_heroState = 'hover-details';
  else if (t >= T.s2_details_press && t < T.s2_expanded) s2_heroState = 'expanded-pressed';
  else if (t >= T.s2_expanded && t < T.s2_cursor_draft) s2_heroState = 'expanded';
  else if (t >= T.s2_cursor_draft && t < T.s2_draft_press) s2_heroState = 'expanded';
  else if (t >= T.s2_draft_press && t < T.s2_hero_resolved) s2_heroState = 'expanded-action-pressed';
  else if (t >= T.s2_hero_resolved) s2_heroState = 'resolved';

  // ── Slack card phase ────────────────────────────────────────
  let slackPhase = 'idle';
  if (t >= T.s1_body_focus && t < T.s1_card_send_press) slackPhase = 'editing';
  else if (t >= T.s1_card_send_press && t < T.s1_submitted) slackPhase = 'pressed';
  else if (t >= T.s1_submitted) slackPhase = 'submitted';
  const slackAppendLen = window.SLACK_DATA.appended_text.length;
  const slackAppended = t < T.s1_append_start
    ? 0
    : Math.min(slackAppendLen, Math.floor((t - T.s1_append_start) / T.s1_append_per));

  // ── Gmail card phase ────────────────────────────────────────
  let gmailPhase = 'idle';
  if (t >= T.s2_body_focus && t < T.s2_card_send_press) gmailPhase = 'editing';
  else if (t >= T.s2_card_send_press && t < T.s2_submitted) gmailPhase = 'pressed';
  else if (t >= T.s2_submitted) gmailPhase = 'submitted';
  const gmailAppendLen = window.GMAIL_DATA.appended_text.length;
  const gmailAppended = t < T.s2_append_start
    ? 0
    : Math.min(gmailAppendLen, Math.floor((t - T.s2_append_start) / T.s2_append_per));

  // ── Connector statuses ─────────────────────────────────────
  const s1_connector_state = t >= T.s1_connector_done ? 'done' : 'running';
  const s2_connector_state = t >= T.s2_connector_done ? 'done' : 'running';

  // ── Cursor target ──────────────────────────────────────────
  let cursorTarget = 'composer';
  // Scene 1
  if (t < T.s1_cursor_send) cursorTarget = 'composer';
  else if (t < T.s1_cursor_draft) cursorTarget = 'composerSend';
  else if (t < T.s1_cursor_send2) cursorTarget = 'triageDraft1';
  else if (t < T.s1_cursor_body) cursorTarget = 'composerSend';
  else if (t < T.s1_cursor_card_send) cursorTarget = 'slackBody';
  else if (t < T.s1_cursor_send3) cursorTarget = 'slackSend';
  else if (t < T.s2_type_start) cursorTarget = 'composerSend';
  // Scene 2
  else if (t < T.s2_cursor_send) cursorTarget = 'composer';
  else if (t < T.s2_cursor_details) cursorTarget = 'composerSend';
  else if (t < T.s2_cursor_draft) cursorTarget = 'triageDetails2';
  else if (t < T.s2_cursor_send2) cursorTarget = 'triageDraft2';
  else if (t < T.s2_cursor_body) cursorTarget = 'composerSend';
  else if (t < T.s2_cursor_card_send) cursorTarget = 'gmailBody';
  else if (t < T.s2_cursor_send3) cursorTarget = 'gmailSend';
  else cursorTarget = 'composerSend';

  // ── Press fires (clickCount ticks) ──────────────────────────
  const pressMoments = [
    [T.s1_send_press,    'composerSend'],
    [T.s1_draft_press,   'triageDraft1'],
    [T.s1_send_press2,   'composerSend'],
    [T.s1_card_send_press,'slackSend'],
    [T.s1_send_press3,   'composerSend'],
    [T.s2_send_press,    'composerSend'],
    [T.s2_details_press, 'triageDetails2'],
    [T.s2_draft_press,   'triageDraft2'],
    [T.s2_send_press2,   'composerSend'],
    [T.s2_card_send_press,'gmailSend'],
    [T.s2_send_press3,   'composerSend'],
  ];
  // Identify which press window we're currently in (0.3s after the moment)
  let activePress = null;
  let activePressIdx = -1;
  for (let i = 0; i < pressMoments.length; i++) {
    const [m, name] = pressMoments[i];
    if (t >= m && t < m + 0.3) {
      activePress = name;
      activePressIdx = i;
      break;
    }
  }

  // Progress for right panel
  let progress = 0;
  if (t < T.s1_triage_in) progress = (t / T.s1_triage_in) * 0.1;
  else if (t < T.s1_connector_done) progress = 0.1 + ((t - T.s1_triage_in) / (T.s1_connector_done - T.s1_triage_in)) * 0.4;
  else if (t < T.s2_triage_in) progress = 0.5;
  else if (t < T.s2_connector_done) progress = 0.5 + ((t - T.s2_triage_in) / (T.s2_connector_done - T.s2_triage_in)) * 0.45;
  else progress = 0.95;
  progress = Math.max(0, Math.min(1, progress));

  const fading = t >= T.fade_out;

  return {
    composerText,
    composerCaret,
    flags,
    s1_heroState,
    s2_heroState,
    slackPhase,
    slackAppended,
    gmailPhase,
    gmailAppended,
    s1_connector_state,
    s2_connector_state,
    cursorTarget,
    activePress,
    activePressIdx,
    progress,
    fading,
  };
}

/* ── App ────────────────────────────────────────────────────── */
function App() {
  const stageRef = useRef(null);
  const canvasRef = useRef(null);
  useCanvasScale(stageRef, canvasRef);

  const t = useLoopClock();
  const d = useMemo(() => derive(t), [t]);

  // Refs for cursor targets
  const composerSendRef = useRef(null);
  const composerWrapRef = useRef(null);
  const triage1DraftRef = useRef(null);
  const triage2DraftRef = useRef(null);
  const triage2DetailsRef = useRef(null);
  const slackBodyRef = useRef(null);
  const slackSendRef = useRef(null);
  const gmailBodyRef = useRef(null);
  const gmailSendRef = useRef(null);
  const threadRef = useRef(null);

  // Cursor position state (canvas coords)
  const [cursor, setCursor] = useState({ x: 720, y: 820 });
  const [clickCount, setClickCount] = useState(0);
  const [pressed, setPressed] = useState(false);
  const lastTargetRef = useRef('composer');
  const lastPressIdxRef = useRef(-1);

  // Auto-scroll: pin the most-recently-appended content into view. We
  // walk children from the end and pick the last meaningful child (skipping
  // the spacer). Placing its top ~24px below the thread's top means tall
  // cards start fully visible — gmail compose especially. The follow-up
  // setTimeout is SKIPPED when the cursor target is a send-button: in
  // that case the second effect (below) owns the scroll, and we don't
  // want to fight it.
  const flagCount = Object.values(d.flags).filter(Boolean).length;
  const cursorOwnsScrollRef = useRef(false);
  cursorOwnsScrollRef.current =
    d.cursorTarget === 'slackSend' || d.cursorTarget === 'slackBody' ||
    d.cursorTarget === 'gmailSend' || d.cursorTarget === 'gmailBody';
  useLayoutEffect(() => {
    const el = threadRef.current;
    if (!el) return;
    const kids = Array.from(el.children);
    let last = null;
    for (let i = kids.length - 1; i >= 0; i--) {
      const k = kids[i];
      if (k.classList.contains('thread-spacer')) continue;
      last = k;
      break;
    }
    if (!last) return;
    const doScroll = () => {
      if (cursorOwnsScrollRef.current) return;
      const target = Math.max(0, last.offsetTop - 24);
      el.scrollTo({ top: target, behavior: 'smooth' });
    };
    doScroll();
    const raf = requestAnimationFrame(doScroll);
    const t1 = setTimeout(doScroll, 500);
    return () => { cancelAnimationFrame(raf); clearTimeout(t1); };
  }, [flagCount]);

  // Refocus scroll on the active in-card button. The button's `offsetTop`
  // only matches "offset within the thread" if the thread is its
  // offsetParent — which may not hold if an ancestor (card, wrapper)
  // is given position:relative later. We walk the offsetParent chain
  // explicitly up to the thread to compute the button's offset in
  // thread-space; this matches what `relativeCenter` does for the cursor.
  function offsetWithinThread(el, thread) {
    let y = 0, cur = el;
    while (cur && cur !== thread) {
      y += cur.offsetTop;
      cur = cur.offsetParent;
    }
    return y;
  }
  useLayoutEffect(() => {
    const el = threadRef.current;
    if (!el) return;
    const tgt = d.cursorTarget;
    let btn = null;
    if (tgt === 'slackSend' || tgt === 'slackBody') btn = slackSendRef.current;
    else if (tgt === 'gmailSend' || tgt === 'gmailBody') btn = gmailSendRef.current;
    if (!btn) return;
    const buttonTop = offsetWithinThread(btn, el);
    const buttonBottom = buttonTop + btn.offsetHeight;
    // Want the button bottom to sit ~120px above the thread's bottom edge.
    const desiredScroll = Math.max(0, buttonBottom - (el.clientHeight - 120));
    // Only adjust if the current scroll leaves the button hidden or too
    // close to the composer (within 60px of the bottom edge). Setting
    // .scrollTop directly is synchronous, immediate, and overrides any
    // pending smooth scroll — important because the flagCount effect's
    // 500ms setTimeout would otherwise overwrite us.
    const buttonVisibleY = buttonBottom - el.scrollTop;
    if (buttonVisibleY > el.clientHeight - 60) {
      el.scrollTop = desiredScroll;
    }
  });

  // Drive cursor target
  useLayoutEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas) return;
    const target = d.cursorTarget;
    if (target !== lastTargetRef.current) lastTargetRef.current = target;

    let el = null;
    let mode = 'center';
    if (target === 'composer') {
      // Park inside the composer text area
      const wrap = composerWrapRef.current;
      if (wrap) {
        const c = relativeCenter(wrap, canvas);
        if (c) setCursor({ x: c.x - wrap.offsetWidth / 2 + 100, y: c.y - 18 });
        return;
      }
    }
    if (target === 'composerSend') el = composerSendRef.current;
    else if (target === 'triageDraft1') el = triage1DraftRef.current;
    else if (target === 'triageDraft2') el = triage2DraftRef.current;
    else if (target === 'triageDetails2') el = triage2DetailsRef.current;
    else if (target === 'slackBody') { el = slackBodyRef.current; mode = 'right'; }
    else if (target === 'slackSend') el = slackSendRef.current;
    else if (target === 'gmailBody') { el = gmailBodyRef.current; mode = 'right'; }
    else if (target === 'gmailSend') el = gmailSendRef.current;

    if (el) {
      const pt = mode === 'right'
        ? relativeRight(el, canvas, 30, 0.7)
        : relativeCenter(el, canvas);
      if (pt) setCursor(pt);
    }
  }, [d.cursorTarget, d.flags.s1_triage, d.flags.s1_slack, d.flags.s2_triage,
      d.flags.s2_gmail, d.s2_heroState, t]);

  // Fire click ring at press moments
  useEffect(() => {
    if (d.activePress && d.activePressIdx !== lastPressIdxRef.current) {
      lastPressIdxRef.current = d.activePressIdx;
      setClickCount((c) => c + 1);
      setPressed(true);
      const id = setTimeout(() => setPressed(false), 180);
      return () => clearTimeout(id);
    }
    if (!d.activePress) lastPressIdxRef.current = -1;
  }, [d.activePressIdx, d.activePress]);

  // Composer caret blink
  const caretBlink = Math.floor(t * 2) % 2 === 0;

  // Send-button pressed-pulse
  const composerSendPressed = d.activePress === 'composerSend';

  return (
    <div className="stage" ref={stageRef}>
      <div className="canvas" ref={canvasRef}>
        <div className="host">
          <Sidebar />

          <main className="main">
            <div className="main-top">
              <div />
              <div className="main-top-actions">
                <HostIcon name="sidebar" />
              </div>
            </div>

            <div className="thread" ref={threadRef}>
              {/* SCENE 1 */}
              {d.flags.s1_user_msg ? <UserMessage text="/agntux triage" /> : null}
              {d.flags.s1_tool_status ? <ToolStatus text="Read 3 files, loaded tools, found files" /> : null}
              {d.flags.s1_orb ? <AssistantOrb /> : null}
              {d.flags.s1_triage ? (
                <div className="app-card-wrap">
                  <TriageCard
                    data={window.TRIAGE_DATA_INITIAL}
                    heroRowId={window.ACTION_IDS.ID_NORTHSTREAM}
                    heroState={d.s1_heroState}
                    heroBtnRef={triage1DraftRef}
                  />
                  <div style={{ paddingTop: 4 }}>
                    <AssistMessageActions />
                  </div>
                </div>
              ) : null}
              {d.flags.s1_user_open ? (
                <UserMessage text={window.PROMPTS.open_slack} compact />
              ) : null}
              {d.flags.s1_tool_compose ? (
                <ToolStatus text="Calling agntux-slack:compose…" />
              ) : null}
              {d.flags.s1_slack ? (
                <div className="app-card-wrap">
                  <SlackCompose
                    phase={d.slackPhase}
                    appendedChars={d.slackAppended}
                    sendBtnRef={slackSendRef}
                    bodyRef={slackBodyRef}
                    bodyCaretBlink={caretBlink}
                  />
                </div>
              ) : null}
              {d.flags.s1_user_send ? (
                <UserMessage text={window.PROMPTS.send_slack} compact />
              ) : null}
              {d.flags.s1_connector ? (
                <ConnectorCall
                  brand="slack"
                  tool="chat.postMessage"
                  desc="POST /chat.postMessage channel=C0AB12CDE thread_ts=…"
                  state={d.s1_connector_state}
                  result="Message delivered to #partner-success · ts 1779256104.001500"
                />
              ) : null}

              {/* SCENE 2 */}
              {d.flags.s2_user_msg ? <UserMessage text="/agntux triage" /> : null}
              {d.flags.s2_tool_status ? <ToolStatus text="Re-reading 3 files, refreshed tools" /> : null}
              {d.flags.s2_triage ? (
                <div className="app-card-wrap">
                  <TriageCard
                    data={window.TRIAGE_DATA_AFTER_SLACK}
                    heroRowId={window.ACTION_IDS.ID_CATALYST}
                    heroState={d.s2_heroState}
                    heroBtnRef={triage2DraftRef}
                    detailsBtnRef={triage2DetailsRef}
                  />
                  <div style={{ paddingTop: 4 }}>
                    <AssistMessageActions />
                  </div>
                </div>
              ) : null}
              {d.flags.s2_user_open ? (
                <UserMessage text={window.PROMPTS.open_gmail} compact />
              ) : null}
              {d.flags.s2_tool_compose ? (
                <ToolStatus text="Calling agntux-gmail:compose…" />
              ) : null}
              {d.flags.s2_gmail ? (
                <div className="app-card-wrap">
                  <GmailCompose
                    phase={d.gmailPhase}
                    appendedChars={d.gmailAppended}
                    sendBtnRef={gmailSendRef}
                    bodyRef={gmailBodyRef}
                    bodyCaretBlink={caretBlink}
                  />
                </div>
              ) : null}
              {d.flags.s2_user_send ? (
                <UserMessage text={window.PROMPTS.save_gmail} compact />
              ) : null}
              {d.flags.s2_connector ? (
                <ConnectorCall
                  brand="gmail"
                  tool="create_draft"
                  desc="POST /gmail/v1/users/me/drafts (reply to 19e3b4d3dfbc1694)"
                  state={d.s2_connector_state}
                  result={'Draft saved \u2192 open in Gmail to review and send'}
                />
              ) : null}

              {/* Spacer so the last item has breathing room above the composer */}
              <div className="thread-spacer" style={{ minHeight: 16 }} />
            </div>

            <div ref={composerWrapRef}>
              <Composer
                text={d.composerText}
                caretBlink={caretBlink}
                sendBtnRef={composerSendRef}
                pressedSend={composerSendPressed}
              />
            </div>
          </main>

          <RightPanel
            contextActive={d.flags.s1_tool_status}
          />
        </div>

        <Cursor x={cursor.x} y={cursor.y} click={clickCount} pressed={pressed} fast={true} />

        <div className={['wash', d.fading ? 'show' : ''].join(' ')} />
      </div>
    </div>
  );
}

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
