/* ELEV8 ATS - screens: Inbox (unified messages across every channel) and Notes (AI notetaker).
   The inbox is one thread per person; the composer switches channels per message -
   email, LinkedIn, WhatsApp, or text through RingCentral - without leaving the thread. */

const DSm2 = window.Stand8DesignSystem_b5c975;

/* Channel registry - visual identity for each comms channel. */
const CHANNELS = {
  email: { label: 'Email', icon: 'mail', cls: 'email' },
  whatsapp: { label: 'WhatsApp', icon: 'chat', cls: 'wa' },
  sms: { label: 'Text · RingCentral', icon: 'sms', cls: 'sms' },
  linkedin: { label: 'LinkedIn', text: 'in', cls: 'li' },
  inmail: { label: 'LinkedIn InMail', text: 'in', cls: 'li' },
  call: { label: 'Call', icon: 'call', cls: 'call' },
};

function ChannelDot({ ch, size = 14 }) {
  const c = CHANNELS[ch] || CHANNELS.email;
  return (
    <span className={'e8-chdot ' + c.cls} style={{ width: size, height: size }} title={c.label}>
      {c.text ? <b>{c.text}</b> : <span className="material-symbols-outlined" style={{ fontSize: size - 4 }}>{c.icon}</span>}
    </span>
  );
}

function ChannelChip({ ch }) {
  const c = CHANNELS[ch] || CHANNELS.email;
  return (
    <span className={'e8-chchip ' + c.cls}>
      {c.text ? <b>{c.text}</b> : <span className="material-symbols-outlined" style={{ fontSize: 12 }}>{c.icon}</span>}
      {c.label}
    </span>
  );
}

/* ============================== UNIFIED INBOX ============================== */
/* Lifts the composer above the on-screen keyboard on mobile (visualViewport). 0 on desktop. */
function useKeyboardInset() {
  // While the on-screen keyboard is open, return the EXACT visible height (visualViewport.height)
  // so the thread can be sized to it directly. Sizing off vv.height beats "100dvh minus a computed
  // inset" - mixing a CSS dvh unit with a JS innerHeight measurement drifts a few px mid-animation.
  const [visH, setVisH] = React.useState(0);
  React.useEffect(() => {
    const vv = window.visualViewport;
    if (!vv) return;
    const onR = () => {
      const inset = window.innerHeight - vv.height - vv.offsetTop; // keyboard height, roughly
      setVisH(inset > 80 ? Math.round(vv.height) : 0);
    };
    onR();
    vv.addEventListener('resize', onR);
    vv.addEventListener('scroll', onR);
    return () => { vv.removeEventListener('resize', onR); vv.removeEventListener('scroll', onR); };
  }, []);
  return visH;
}

/* Mobile conversation-row gesture: full-swipe left to Archive (row slides off, with Undo),
   full-swipe right to toggle Read/Unread (acts, then springs back). Reuses the .e8-triage
   visuals from shell.jsx; lives here because the actions are messages-specific. Vertical drags
   fall through to the list scroll (axis lock), and a no-move release is treated as a tap. */
function SwipeConvo({ isUnread, onArchive, onToggleRead, onTap, children }) {
  const [tx, setTx] = React.useState(0);
  const txRef = React.useRef(0);
  const setBoth = (v) => { txRef.current = v; setTx(v); };
  const [dragging, setDragging] = React.useState(false);
  const st = React.useRef(null);
  const moved = React.useRef(0);
  const wrapRef = React.useRef(null);
  const widthOf = () => (wrapRef.current ? wrapRef.current.offsetWidth : 320);
  const onTouchStart = (e) => {
    const t = e.touches[0];
    st.current = { x: t.clientX, y: t.clientY, axis: null, w: widthOf() };
    moved.current = 0;
    setDragging(true);
  };
  const onTouchMove = (e) => {
    if (!st.current) return;
    const t = e.touches[0];
    const dx = t.clientX - st.current.x;
    const dy = t.clientY - st.current.y;
    moved.current = Math.max(moved.current, Math.abs(dx) + Math.abs(dy));
    if (!st.current.axis) st.current.axis = Math.abs(dx) > Math.abs(dy) ? 'x' : 'y';
    if (st.current.axis !== 'x') return;
    e.preventDefault();
    const th = st.current.w * 0.42;
    let v = dx;
    if (Math.abs(v) > th) v = Math.sign(v) * (th + (Math.abs(v) - th) * 0.38);
    setBoth(v);
  };
  const onTouchEnd = () => {
    if (!st.current) return;
    const th = st.current.w * 0.42;
    const v = txRef.current;
    st.current = null;
    setDragging(false);
    if (v <= -th) {
      if (navigator.vibrate) { try { navigator.vibrate(12); } catch (x) {} }
      setBoth(-(widthOf() + 40));
      window.setTimeout(() => { if (onArchive) onArchive(); }, 200);
    } else if (v >= th) {
      if (navigator.vibrate) { try { navigator.vibrate(10); } catch (x) {} }
      if (onToggleRead) onToggleRead();
      setBoth(0);
    } else {
      setBoth(0);
    }
  };
  const onClick = () => { if (moved.current < 8 && onTap) onTap(); };
  const th = widthOf() * 0.42;
  const armedRead = tx >= th;
  const armedArch = tx <= -th;
  return (
    <div className="e8-triage e8-convo-swipe" ref={wrapRef}>
      <div className={'e8-triage-pad left' + (armedRead ? ' armed' : '')} style={{ width: Math.max(0, tx), background: 'var(--ui-accent, var(--ui-accent))' }}>
        <span className="material-symbols-outlined">{isUnread ? 'mark_email_read' : 'mark_email_unread'}</span>
        <span className="e8-triage-lbl">{armedRead ? 'Release' : (isUnread ? 'Read' : 'Unread')}</span>
      </div>
      <div className={'e8-triage-pad right' + (armedArch ? ' armed' : '')} style={{ width: Math.max(0, -tx), background: 'var(--ui-text-secondary)' }}>
        <span className="material-symbols-outlined">archive</span>
        <span className="e8-triage-lbl">{armedArch ? 'Release' : 'Archive'}</span>
      </div>
      <div className={'e8-triage-card' + (dragging ? ' dragging' : '')} style={{ transform: 'translateX(' + tx + 'px)' }}
        onTouchStart={onTouchStart} onTouchMove={onTouchMove} onTouchEnd={onTouchEnd}
        {...(onTap ? window.clickableProps(onClick) : { onClick })}>
        {children}
      </div>
    </div>
  );
}

/* On-device inbox triage: intent taxonomy (weight drives the "needs attention" sort) + the exemplars
   the embedder matches each latest inbound message against (tuned to recruiter threads). */
const INTENTS = {
  at_risk: { label: 'At risk', weight: 5 },
  reschedule: { label: 'Reschedule', weight: 4 },
  question: { label: 'Question', weight: 3 },
  scheduling: { label: 'Scheduling', weight: 2 },
  positive: { label: 'On track', weight: 1 },
};
const INTENT_EXEMPLARS = [
  { key: 'question', examples: ['What is the pay rate?', 'Can you tell me more about the role?', 'Does the timeline affect the offer?', 'Anything I should prepare?', 'One question about the process'] },
  { key: 'reschedule', examples: ['Can we reschedule?', 'Can we do the call tomorrow instead?', 'Can we push our call to next week?', 'I need to move our meeting'] },
  { key: 'scheduling', examples: ['Tuesday 10am works for me', 'I can hold interview slots Tuesday and Wednesday', 'Let us set up a call', 'I am available Thursday afternoon'] },
  { key: 'at_risk', examples: ['Any word on the renewal?', 'The PO is almost burned', 'Where do we stand?', 'Picking this back up after time away', 'Just checking in, I have not heard back'] },
  { key: 'positive', examples: ['Thanks, reviewing with the team this week', 'Got them, thank you', 'Sounds great', 'Perfect, appreciate it'] },
];

/* R78: comms persistence (e8-comms-v1) + the one shared unread counter that the sidebar,
   mobile tab bar and app-icon badge all read. Saving emits 'comms:changed' on the bus. */
const E8Comms = {
  KEY: 'e8-comms-v1',
  load() { try { return JSON.parse(localStorage.getItem(this.KEY)) || {}; } catch (e) { return {}; } },
  save(patch) {
    const next = { ...this.load(), ...patch };
    try { localStorage.setItem(this.KEY, JSON.stringify(next)); } catch (e) {}
    if (window.E8Events) window.E8Events.emit('comms:changed', {});
  },
  unreadCount() {
    const D = window.E8DATA || {}; const o = this.load();
    const unread = o.unread || {}; const archived = o.archived || {};
    return (D.conversations || []).filter((c) => !archived[c.id] && (unread[c.id] != null ? unread[c.id] : !!c.unread)).length;
  },
};
window.E8Comms = E8Comms;

function commsConversationOpened(state, convoId) {
  if (!convoId) return state;
  return {
    unread: { ...(state.unread || {}), [convoId]: false },
    archived: state.archived && state.archived[convoId]
      ? { ...state.archived, [convoId]: false }
      : (state.archived || {}),
  };
}

function MessagesScreen({ convoId }) {
  const D = window.E8DATA;
  const { showToast } = React.useContext(E8Ctx);
  const [tab, setTab] = React.useState('inbox');
  const [convoSearch, setConvoSearch] = React.useState('');
  const [activeId, setActiveId] = React.useState(convoId || (D.conversations[0] && D.conversations[0].id) || null);
  /* R78: seed from data, then fold in the persisted overlay (e8-comms-v1) so read state,
     archives, sent messages and channel choices survive reload. */
  const ov = React.useMemo(() => (window.E8Comms ? window.E8Comms.load() : {}), []);
  const [unread, setUnread] = React.useState({ ...Object.fromEntries(D.conversations.map((c) => [c.id, !!c.unread])), ...(ov.unread || {}) });
  const [archived, setArchived] = React.useState(ov.archived || {});
  const [threads, setThreads] = React.useState({ ...Object.fromEntries(D.conversations.map((c) => [c.id, c.thread])), ...(ov.threads || {}) });
  const [channelSel, setChannelSel] = React.useState({
    ...Object.fromEntries(D.conversations.map((c) => [c.id, (c.identities.find((i) => i.selected) || c.identities[0])])),
    ...(ov.channelSel || {}),
  });
  const didMount = React.useRef(false);
  const savingRef = React.useRef(false);
  React.useEffect(() => {
    if (!didMount.current) { didMount.current = true; return; } // don't snapshot pristine seed state
    if (window.E8Comms) {
      savingRef.current = true; // comms:changed dispatches synchronously - skip our own echo below
      window.E8Comms.save({ unread, archived, threads, channelSel });
      savingRef.current = false;
    }
  }, [unread, archived, threads, channelSel]);
  /* R80: external writers exist now (notification replies, other tabs) - fold their
     overlay changes into state instead of letting the save effect clobber them. */
  React.useEffect(() => (window.E8Events ? window.E8Events.subscribe(['comms:changed'], () => {
    if (savingRef.current || !window.E8Comms) return;
    const o = window.E8Comms.load();
    if (o.threads) setThreads((prev) => ({ ...prev, ...o.threads }));
    if (o.unread) setUnread((prev) => ({ ...prev, ...o.unread }));
    if (o.archived) setArchived((prev) => ({ ...prev, ...o.archived }));
  }) : undefined), []);
  /* R78: if an approved 'load-draft' is undone after the composer consumed it, clear the draft. */
  React.useEffect(() => (window.E8Events ? window.E8Events.subscribe(['draft:revoked'], (e) => {
    if (convo && e.payload && e.payload.convoId === convo.id) { setDraft(''); setDraftByAI(false); }
  }) : undefined), [convo && convo.id]);
  const [pickerOpen, setPickerOpen] = React.useState(false);
  const [slashOpen, setSlashOpen] = React.useState(false);
  const [draft, setDraft] = React.useState('');
  const [draftByAI, setDraftByAI] = React.useState(false);
  const [drafting, setDrafting] = React.useState(false);
  const [draftDetail, setDraftDetail] = React.useState('');
  const [triageMap, setTriageMap] = React.useState(null); // { convoId: { key, score } } when triaged
  const [triaging, setTriaging] = React.useState(false);
  const [threadSummary, setThreadSummary] = React.useState({}); // { convoId: text }
  const [summarizing, setSummarizing] = React.useState(false);
  const [tmplSuggest, setTmplSuggest] = React.useState(null); // ordered template ids when ranked to a thread
  const [suggesting, setSuggesting] = React.useState(false);
  const [tmplSearch, setTmplSearch] = React.useState('');
  const [tmplCat, setTmplCat] = React.useState('All');
  const [reachOpen, setReachOpen] = React.useState(false);
  const scrollRef = React.useRef(null);
  const isMobile = useIsMobile();
  const [mThread, setMThread] = React.useState(!!convoId);
  const [railOpen, setRailOpen] = React.useState(false);
  const kbH = useKeyboardInset();

  React.useEffect(() => {
    if (!convoId) return;
    setActiveId(convoId);
    setUnread((current) => commsConversationOpened({ unread: current }, convoId).unread);
    setArchived((current) => commsConversationOpened({ archived: current }, convoId).archived);
    if (window.E8Comms) {
      const current = window.E8Comms.load();
      window.E8Comms.save(commsConversationOpened(current, convoId));
    }
    setMThread(true);
  }, [convoId]);

  const convos = D.conversations.filter((c) => !archived[c.id]);
  const convo = convos.find((c) => c.id === activeId) || convos[0];
  const thread = convo ? threads[convo.id] : [];
  const identity = convo ? channelSel[convo.id] : null;
  const unreadCount = convos.filter((c) => unread[c.id]).length;
  React.useEffect(() => { if (isMobile && mThread && !convo) setMThread(false); }, [isMobile, mThread, convo]);
  // Jump to the newest message whenever a conversation is opened or switched.
  React.useEffect(() => { requestAnimationFrame(() => { if (scrollRef.current) scrollRef.current.scrollTop = scrollRef.current.scrollHeight; }); }, [activeId]);
  // R78: an approved 'load-draft' queue item leaves drafted text for this thread - pick it up once.
  React.useEffect(() => {
    if (!convo) return;
    try {
      const k = 'e8-composer-draft-' + convo.id;
      const pending = localStorage.getItem(k);
      if (pending) { setDraft(pending); setDraftByAI(true); localStorage.removeItem(k); }
    } catch (e) {}
  }, [activeId]);

  const open = (c) => {
    setActiveId(c.id);
    setUnread((u) => ({ ...u, [c.id]: false }));
    setDraft(''); setDraftByAI(false); setPickerOpen(false); setDrafting(false); setDraftDetail('');
    if (isMobile) setMThread(true);
  };

  const send = () => {
    if (!draft.trim() || !convo) return;
    /* R82: guardrails are code - the composer checks before anything sends.
       A deliberate human send is blocked only by hard rules (DNC, the daily cap);
       quiet hours give the human a heads-up instead of a block. */
    if (window.E8Policy) {
      const v = window.E8Policy.check({ type: 'send', by: 'human', convoId: convo.id, name: convo.name });
      if (!v.allowed) { showToast('Held by guardrail · ' + v.label + ' - ' + v.reason); return; }
      if (v.warn) showToast(v.warn.label + ' · ' + v.warn.reason);
    }
    const msg = { who: 'me', name: D.user.name, ch: identity.ch, when: 'Just now', text: draft.trim(), ai: draftByAI ? 'Drafted with AI' : undefined };
    setThreads((t) => ({ ...t, [convo.id]: [...t[convo.id], msg] }));
    setDraft(''); setDraftByAI(false);
    if (window.E8Events) window.E8Events.emit('message:sent', { convoId: convo.id, ch: identity.ch, ai: !!draftByAI });
    if (window.E8Audit) window.E8Audit.log({ agent: 'You', prov: 'human', action: 'Sent ' + identity.ch + ' message', target: convo.name, route: 'inbox/' + convo.id });
    requestAnimationFrame(() => { if (scrollRef.current) scrollRef.current.scrollTop = scrollRef.current.scrollHeight; });
  };

  // Draft a reply with the on-device LLM, prompted from the latest inbound message. Falls back to the
  // thread's scripted draft if the model is unavailable - either way nothing leaves the browser.
  const aiDraft = async () => {
    if (!convo || drafting) return;
    const fallback = convo.draftReply || 'Thanks - let me check and come right back to you.';
    setDrafting(true); setDraftDetail('');
    try {
      const th = threads[convo.id] || [];
      let lastIn = null;
      for (let i = th.length - 1; i >= 0; i--) { if (th[i].who !== 'me') { lastIn = th[i]; break; } }
      const first = (convo.name || '').split(' ')[0];
      const prompt = lastIn
        ? ('You are Sarah, a friendly staffing recruiter. Write a short, warm, professional reply to this message from ' + first + ': "' + lastIn.text + '"')
        : ('You are Sarah, a friendly staffing recruiter. Write a short, warm opening message to ' + first + '.');
      const out = await E8AI.generate(prompt, (s) => setDraftDetail(s.detail || ''), { max_new_tokens: 80 });
      const clean = (out || '').trim().replace(/^["']+|["']+$/g, '').trim();
      setDraft(clean || fallback);
      setDraftByAI(true);
    } catch (e) {
      setDraft(fallback); setDraftByAI(true);
      if (showToast) showToast('Used a saved draft (on-device AI unavailable here)');
    } finally { setDrafting(false); setDraftDetail(''); }
  };

  const lastInboundText = (c) => { const t = threads[c.id] || []; for (let i = t.length - 1; i >= 0; i--) { if (t[i].who !== 'me') return t[i].text; } return ''; };
  const runTriage = async () => {
    if (triaging) return;
    setTriaging(true);
    try {
      const items = convos.map((c) => ({ id: c.id, text: lastInboundText(c) }));
      const res = await E8AI.classifyBatch(items.map((i) => i.text), INTENT_EXEMPLARS);
      const map = {}; items.forEach((it, i) => { map[it.id] = res[i] || { key: null }; });
      setTriageMap(map);
      if (showToast) showToast('Inbox triaged on-device by intent');
    } catch (e) { if (showToast) showToast('On-device triage unavailable in this browser'); }
    finally { setTriaging(false); }
  };
  const clearTriage = () => setTriageMap(null);
  const summarizeThread = async () => {
    if (!convo || summarizing) return;
    setSummarizing(true);
    try {
      const t = threads[convo.id] || [];
      const text = t.map((m) => (m.who === 'me' ? 'Recruiter: ' : ((convo.name || '').split(' ')[0] + ': ')) + m.text).join('\n');
      const s = await E8AI.summarizeNote(text, null);
      setThreadSummary((p) => Object.assign({}, p, { [convo.id]: (s && s.trim()) || '(could not summarize)' }));
    } catch (e) { if (showToast) showToast('On-device summary unavailable in this browser'); }
    finally { setSummarizing(false); }
  };
  const cq = convoSearch.trim().toLowerCase();
  const searchedConvos = cq
    ? convos.filter((c) => {
        const t = threads[c.id] || [];
        const hay = [c.name, c.sub].concat(t.map((m) => m.text)).filter(Boolean).join(' ').toLowerCase();
        return hay.includes(cq);
      })
    : convos;
  const listConvos = triageMap
    ? searchedConvos.slice().sort((a, b) => ((INTENTS[(triageMap[b.id] || {}).key] || {}).weight || 0) - ((INTENTS[(triageMap[a.id] || {}).key] || {}).weight || 0))
    : searchedConvos;
  // R98 Task 6: page the conversation list at 50 so a large inbox stays fast (no virtualization).
  const CONVO_PAGE = 50;
  const [convoShown, setConvoShown] = React.useState(CONVO_PAGE);
  const pagedConvos = listConvos.length > convoShown ? listConvos.slice(0, convoShown) : listConvos;

  // Fill {placeholders} in a template from the open thread (candidate/contact + recruiter).
  const fillTemplate = (body, c) => {
    if (!c) return body;
    const first = ((c.name || '').split(' ')[0]) || 'there';
    const cand = (c.candId && D.candidates) ? D.candidates.find((x) => x.id === c.candId) : null;
    const sub = String(c.sub || '');
    const role = (cand && (cand.title || cand.role)) || (sub.indexOf(' at ') > -1 ? sub.split(' at ')[0].trim() : (sub.split(/[·,]/)[0] || '').trim()) || 'the role';
    const company = (cand && cand.company) || (sub.indexOf(' at ') > -1 ? sub.split(' at ')[1].trim() : (sub.indexOf('·') > -1 ? sub.split('·').pop().trim() : '')) || 'your team';
    const rate = (cand && cand.rate) || 'the target rate';
    const map = { first_name: first, name: c.name || first, role: role, job: role, company: company, client: company, rate: rate, day: 'Tuesday or Wednesday', candidates: 'the shortlist', recruiter: (D.user && D.user.name) || 'Sarah' };
    return String(body).replace(/\{(\w+)\}/g, (m, k) => (map[k] != null ? map[k] : m));
  };
  const suggestTemplates = async () => {
    if (!convo || suggesting) return;
    setSuggesting(true);
    try {
      const q = lastInboundText(convo) || ((threads[convo.id] || []).slice(-1)[0] || {}).text || convo.name;
      const scored = await E8AI.rank(q, D.messageTemplates.map((t) => ({ key: t.id, text: t.name + '. ' + t.body })));
      setTmplSuggest(scored.map((s) => s.key));
      if (showToast) showToast('Templates ranked for ' + (convo.name || '').split(' ')[0]);
    } catch (e) { if (showToast) showToast('On-device suggestions unavailable in this browser'); }
    finally { setSuggesting(false); }
  };
  const templateList = (function () {
    const list = D.messageTemplates.slice();
    if (tmplSuggest) { const order = {}; tmplSuggest.forEach((id, i) => { order[id] = i; }); list.sort((a, b) => (order[a.id] != null ? order[a.id] : 99) - (order[b.id] != null ? order[b.id] : 99)); }
    return list;
  })();

  return (
    <React.Fragment>
      {!(isMobile && mThread) ? (
        <Topbar
          crumbs={[{ label: 'Inbox' }]}
          actions={
            <React.Fragment>
              <DSm2.Button variant="ghost" size="sm" icon="description" onClick={() => setTab('templates')}>Templates · {D.messageTemplates.length}</DSm2.Button>
              <DSm2.Button variant="primary" size="sm" icon="edit_square">New message</DSm2.Button>
            </React.Fragment>
          }
        />
      ) : null}
      <div className={'e8-msgs' + (isMobile ? (mThread ? ' e8-msgs-m-thread' : ' e8-msgs-m-list') : '')}>
        {/* Conversation list */}
        <div className="e8-msgs-list">
          <div style={{ padding: '10px 10px 8px', display: 'grid', gap: 8 }}>
            <DSm2.SubTabs
              items={[
                { id: 'inbox', label: 'Inbox', count: unreadCount || undefined },
                { id: 'outbox', label: 'Outbox' },
                { id: 'templates', label: 'Templates', count: D.messageTemplates.length },
              ]}
              active={tab}
              onChange={setTab}
            />
            {tab === 'inbox' ? <DSm2.Input icon="search" placeholder="Search conversations…" value={convoSearch} onChange={(e) => setConvoSearch(e.target ? e.target.value : e)} /> : null}
            {tab === 'inbox' ? (
              <DSm2.Button variant={triageMap ? 'secondary' : 'ghost'} size="sm" icon={triaging ? 'progress_activity' : 'auto_awesome'} onClick={triageMap ? clearTriage : runTriage} disabled={triaging}>{triaging ? 'Triaging on-device…' : triageMap ? 'Sorted by attention · clear' : 'AI triage'}</DSm2.Button>
            ) : null}
          </div>
          <div className="e8-msgs-list-scroll">
            {tab === 'inbox' && listConvos.length === 0 ? (
              <div style={{ padding: '32px 20px' }}>
                {cq
                  ? <DSm2.EmptyState icon="search_off" title="No conversations match" body={'Nothing matches “' + convoSearch.trim() + '”.'} cta={<DSm2.Button variant="secondary" icon="close" onClick={() => setConvoSearch('')}>Clear search</DSm2.Button>} />
                  : <DSm2.EmptyState icon="mark_email_read" title="Inbox zero" body="No open conversations. New replies from candidates and clients land here." />}
              </div>
            ) : null}
            {tab === 'inbox' ? pagedConvos.map((c) => {
              const t = threads[c.id]; const last = t[t.length - 1];
              const intent = triageMap ? triageMap[c.id] : null;
              const cls = 'e8-convo' + (convo && c.id === convo.id ? ' active' : '') + (unread[c.id] ? ' unread' : '');
              const rowBody = (
                <React.Fragment>
                  <span className="e8-convo-avatar">
                    <DSm2.Avatar name={c.name} size="md" />
                    <ChannelDot ch={c.channel} />
                  </span>
                  <span style={{ flex: 1, minWidth: 0 }}>
                    <span className="e8-convo-top">
                      <span className="e8-convo-name">{unread[c.id] ? <span className="e8-unread-dot"></span> : null}{c.name}</span>
                      <span className="tnum e8-convo-when">{c.lastWhen}</span>
                    </span>
                    <span className="e8-convo-preview">{intent && intent.key ? <span className={'e8-intent ' + intent.key} style={{ marginRight: 6 }}>{INTENTS[intent.key].label}</span> : null}{last.who === 'me' ? 'You: ' : ''}{last.text}</span>
                  </span>
                </React.Fragment>
              );
              if (isMobile) {
                return (
                  <SwipeConvo
                    key={c.id}
                    isUnread={!!unread[c.id]}
                    onTap={() => open(c)}
                    onToggleRead={() => { const wasUnread = !!unread[c.id]; setUnread((u) => ({ ...u, [c.id]: !u[c.id] })); showToast(wasUnread ? 'Marked as read' : 'Marked as unread'); }}
                    onArchive={() => { setArchived((a) => ({ ...a, [c.id]: true })); showToast(c.name + ' archived', 'Undo', () => { setArchived((a) => ({ ...a, [c.id]: false })); if (window.E8Comms) E8Comms.save({ archived: { ...(E8Comms.load().archived || {}), [c.id]: false } }); }); }}
                  >
                    <div className={cls}>{rowBody}</div>
                  </SwipeConvo>
                );
              }
              return (
                <button key={c.id} type="button" className={cls} onClick={() => open(c)}>{rowBody}</button>
              );
            }) : null}
            {tab === 'inbox' && listConvos.length > pagedConvos.length ? (
              <div className="e8-showmore">
                <button type="button" className="e8-showmore-btn" onClick={() => setConvoShown(convoShown + CONVO_PAGE)}>Show more<span className="e8-showmore-n">{listConvos.length - pagedConvos.length} more</span></button>
              </div>
            ) : null}
            {tab === 'outbox' ? (
              <div style={{ padding: '28px 16px' }}>
                <DSm2.EmptyState icon="schedule_send" title="Nothing queued" body="Scheduled and queued messages wait here - schedule any reply from the clock in the composer." />
              </div>
            ) : null}
            {tab === 'templates' ? (
              <div style={{ padding: '4px 8px 8px' }}>
                <div style={{ padding: '2px 4px 8px' }}>
                  <DSm2.Input icon="search" placeholder="Search templates…" value={tmplSearch} onChange={(e) => setTmplSearch(e.target.value)} />
                  <div style={{ display: 'flex', alignItems: 'center', gap: 6, marginTop: 8, flexWrap: 'wrap' }}>
                    {['All'].concat(Array.from(new Set(D.messageTemplates.map((t) => t.category || 'Other')))).map((cat) => (
                      <button key={cat} type="button" className={'e8-reason-chip' + (tmplCat === cat ? ' on' : '')} onClick={() => setTmplCat(cat)}>{cat}</button>
                    ))}
                  </div>
                  {convo ? (
                    <div style={{ marginTop: 8 }}>
                      <DSm2.Button variant={tmplSuggest ? 'secondary' : 'ghost'} size="sm" icon={suggesting ? 'progress_activity' : 'auto_awesome'} onClick={tmplSuggest ? () => setTmplSuggest(null) : suggestTemplates} disabled={suggesting}>{suggesting ? 'Ranking on-device…' : tmplSuggest ? 'Suggested for ' + (convo.name || '').split(' ')[0] + ' · clear' : 'Suggest for this thread'}</DSm2.Button>
                    </div>
                  ) : null}
                </div>
                {templateList.filter((t) => (tmplCat === 'All' || (t.category || 'Other') === tmplCat) && (!tmplSearch.trim() || (t.name + ' ' + t.body).toLowerCase().indexOf(tmplSearch.toLowerCase()) > -1)).map((t) => {
                  const filled = fillTemplate(t.body, convo);
                  const top = !!tmplSuggest && tmplSuggest[0] === t.id && tmplCat === 'All' && !tmplSearch.trim();
                  return (
                    <button key={t.id} type="button" className="e8-convo" style={{ alignItems: 'flex-start' }} onClick={() => { setDraft(filled); setDraftByAI(false); setTab('inbox'); showToast('Template added' + (convo ? ' - filled from the thread' : '')); }}>
                      <span className="material-symbols-outlined" style={{ fontSize: 16, color: 'var(--ui-text-tertiary)', marginTop: 2 }}>description</span>
                      <span style={{ flex: 1, minWidth: 0 }}>
                        <span style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
                          <span style={{ fontSize: 'var(--ui-text-sm)', fontWeight: 500 }}>{t.name}</span>
                          {top ? <span style={{ fontSize: 'var(--ui-text-xs)', fontWeight: 600, color: 'var(--ui-accent)', background: 'var(--ui-daybreak-wash)', padding: '0 6px', borderRadius: 'var(--ui-radius-pill)' }}>Top match</span> : <span style={{ fontSize: 'var(--ui-text-xs)', color: 'var(--ui-text-tertiary)' }}>{t.category}</span>}
                        </span>
                        <span className="e8-convo-preview">{filled}</span>
                      </span>
                    </button>
                  );
                })}
              </div>
            ) : null}
          </div>
        </div>

        {/* Thread */}
        {convo ? (
          <div className="e8-msgs-thread" style={isMobile && mThread && kbH ? { height: kbH + 'px' } : undefined}>
            <div className="e8-msgs-thread-head">
              {isMobile ? <button type="button" className="e8-msgs-back" aria-label="Back to inbox" onClick={() => setMThread(false)}><span className="material-symbols-outlined">arrow_back</span></button> : null}
              <DSm2.Avatar name={convo.name} size="sm" />
              <span style={{ minWidth: 0 }}>
                <span style={{ display: 'block', fontSize: 'var(--ui-text-base)', fontWeight: 500, whiteSpace: 'nowrap' }}>{convo.name}</span>
                <span style={{ display: 'block', fontSize: 'var(--ui-text-xs)', color: 'var(--ui-text-tertiary)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{convo.sub}</span>
              </span>
              <span style={{ marginLeft: 'auto', display: 'flex', gap: 6, flex: 'none' }}>
                {isMobile
                  ? <DSm2.Button variant="ghost" size="sm" icon="info" iconOnly title="Conversation details" onClick={() => setRailOpen(true)}></DSm2.Button>
                  : (convo.candId ? <DSm2.Button variant="ghost" size="sm" icon="person" onClick={() => navigate('candidate/' + convo.candId)}>Open record</DSm2.Button> : null)}
                <DSm2.Button variant="ghost" size="sm" icon={summarizing ? 'progress_activity' : 'summarize'} iconOnly title="Summarize thread on-device" onClick={summarizeThread} disabled={summarizing}></DSm2.Button>
                <DSm2.Button variant="ghost" size="sm" icon="campaign" iconOnly title="Candidate notifications & delivery" onClick={() => setReachOpen(true)}></DSm2.Button>
                {!isMobile ? <DSm2.Button variant="ghost" size="sm" icon="mark_email_unread" iconOnly title="Mark as unread" onClick={() => { setUnread((u) => ({ ...u, [convo.id]: true })); showToast('Marked as unread'); }}></DSm2.Button> : null}
                <DSm2.Button variant="ghost" size="sm" icon="archive" iconOnly title="Archive" onClick={() => { const cid = convo.id, cname = convo.name; setArchived((a) => ({ ...a, [cid]: true })); if (isMobile) setMThread(false); showToast(cname + ' archived', 'Undo', () => { setArchived((a) => ({ ...a, [cid]: false })); if (window.E8Comms) E8Comms.save({ archived: { ...(E8Comms.load().archived || {}), [cid]: false } }); }); }}></DSm2.Button>
              </span>
            </div>

            {convo.pendingDraft ? (
              <div className="e8-msgs-banner">
                <DSm2.ProvenanceBadge kind="drafted" />
                A feedback nudge for this thread is waiting in Approvals.
                <DSm2.Button variant="secondary" size="sm" style={{ marginLeft: 'auto' }} onClick={() => navigate('approvals')}>Review</DSm2.Button>
              </div>
            ) : null}

            {threadSummary[convo.id] ? (
              <div className="e8-msgs-banner">
                <DSm2.ProvenanceBadge kind="drafted" />
                <span style={{ flex: 1 }}><b>Thread summary</b> · {threadSummary[convo.id]}</span>
                <DSm2.Button variant="ghost" size="sm" icon="close" iconOnly title="Dismiss" onClick={() => setThreadSummary((p) => { const n = Object.assign({}, p); delete n[convo.id]; return n; })}></DSm2.Button>
              </div>
            ) : null}

            <div className="e8-msgs-scroll" ref={scrollRef}>
              {thread.map((m, i) => (
                <div key={i} className="e8-msg">
                  <span className="e8-convo-avatar" style={{ marginTop: 1 }}>
                    {m.agent ? <DSm2.Avatar agent size="sm" /> : <DSm2.Avatar name={m.who === 'me' ? (m.name || 'Sarah Kim') : convo.name} size="sm" />}
                    <ChannelDot ch={m.ch} size={12} />
                  </span>
                  <span style={{ flex: 1, minWidth: 0 }}>
                    <span style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
                      <span style={{ fontSize: 'var(--ui-text-sm)', fontWeight: 500 }}>{m.who === 'me' ? (m.name || 'Sarah Kim') : m.agent ? m.name : convo.name}</span>
                      <ChannelChip ch={m.ch} />
                      <span className="tnum" style={{ fontSize: 'var(--ui-text-xs)', color: 'var(--ui-text-tertiary)' }}>{m.when}</span>
                    </span>
                    <span style={{ display: 'block', fontSize: 'var(--ui-text-base)', lineHeight: 1.5, color: 'var(--ui-text)', marginTop: 3, maxWidth: 560, textWrap: 'pretty' }}>{m.text}</span>
                    {m.ai ? (
                      <span style={{ display: 'inline-flex', alignItems: 'center', gap: 6, marginTop: 4, fontSize: 'var(--ui-text-xs)', color: 'var(--ui-text-tertiary)' }}>
                        <DSm2.ProvenanceBadge kind="qa" /> {typeof m.ai === 'string' && m.ai !== 'Drafted with AI' ? m.ai + ' drafted · approved by you' : 'Drafted with AI · approved by you'}
                      </span>
                    ) : null}
                  </span>
                </div>
              ))}
            </div>

            {/* Composer */}
            <div className="e8-composer">
              {draftByAI ? (
                <div style={{ display: 'flex', alignItems: 'center', gap: 7, fontSize: 'var(--ui-text-xs)', color: 'var(--ui-text-tertiary)', marginBottom: 7 }}>
                  <DSm2.ProvenanceBadge kind="drafted" /> Drafted on-device from this thread - edit anything before sending.
                </div>
              ) : null}
              <div className="e8-composer-meta">
                <span className="e8-composer-row">
                  <span className="e8-composer-lbl">From</span>
                  <span className="e8-composer-from">
                    <DSm2.Avatar name="Sarah Kim" size="sm" />
                    Sarah Kim · {identity.ch === 'email' ? 'sarah.kim@stand8.com' : identity.ch === 'linkedin' || identity.ch === 'inmail' ? 'sarah-kim' : 'Stand8 line'}
                  </span>
                </span>
                <span className="e8-composer-row">
                  <span className="e8-composer-lbl">To</span>
                  <span style={{ position: 'relative' }}>
                    <button type="button" className="e8-chpill" onClick={() => setPickerOpen((v) => !v)}>
                      <ChannelDot ch={identity.ch} size={14} />
                      <b>{convo.name}</b> <span style={{ color: 'var(--ui-text-tertiary)' }}>{identity.label}</span>
                      <span className="material-symbols-outlined" style={{ fontSize: 14 }}>{pickerOpen ? 'expand_less' : 'expand_more'}</span>
                    </button>
                    {pickerOpen ? (
                      <div className="e8-chpicker">
                        <div style={{ fontSize: 'var(--ui-text-xs)', color: 'var(--ui-text-tertiary)', padding: '5px 9px 3px' }}>Reply on any channel {convo.name.split(' ')[0]} is reachable</div>
                        {convo.identities.map((opt) => (
                          <button
                            key={opt.ch + opt.label}
                            type="button"
                            className="e8-chpicker-item"
                            onClick={() => { setChannelSel((s) => ({ ...s, [convo.id]: opt })); setPickerOpen(false); }}
                          >
                            <ChannelDot ch={opt.ch} size={15} />
                            <span style={{ flex: 1, minWidth: 0, textAlign: 'left' }}>{opt.label}</span>
                            {identity.ch === opt.ch && identity.label === opt.label ? <span className="material-symbols-outlined" style={{ fontSize: 14, color: 'var(--ui-accent)' }}>check</span> : null}
                          </button>
                        ))}
                      </div>
                    ) : null}
                  </span>
                </span>
              </div>
              <span style={{ position: 'relative', display: 'block' }}>
                {slashOpen ? (
                  <div className="e8-chpicker" style={{ minWidth: 320 }}>
                    <div style={{ fontSize: 'var(--ui-text-xs)', color: 'var(--ui-text-tertiary)', padding: '5px 9px 3px' }}>{tmplSuggest ? 'Suggested for this thread' : 'Templates'} - filled from this thread</div>
                    {templateList.slice(0, 6).map((t) => {
                      const filled = fillTemplate(t.body, convo);
                      return (
                        <button key={t.id} type="button" className="e8-chpicker-item" onClick={() => { setDraft(filled); setDraftByAI(false); setSlashOpen(false); }}>
                          <span className="material-symbols-outlined" style={{ fontSize: 15, color: 'var(--ui-text-tertiary)' }}>description</span>
                          <span style={{ flex: 1, minWidth: 0, textAlign: 'left' }}>
                            <span style={{ display: 'block', fontWeight: 500 }}>{t.name}</span>
                            <span style={{ display: 'block', fontSize: 'var(--ui-text-xs)', color: 'var(--ui-text-tertiary)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{filled}</span>
                          </span>
                        </button>
                      );
                    })}
                  </div>
                ) : null}
                <textarea
                  className="e8-composer-input"
                  rows={2}
                  placeholder={'Write a ' + (CHANNELS[identity.ch].label.split(' ')[0].toLowerCase() === 'text' ? 'text' : CHANNELS[identity.ch].label.toLowerCase().replace(' · ringcentral', '')) + ' message… type / for templates'}
                  value={draft}
                  onChange={(e) => { const v = e.target.value; setDraft(v); setSlashOpen(v === '/'); if (draftByAI && !v) setDraftByAI(false); }}
                  onKeyDown={(e) => { if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) send(); if (e.key === 'Escape') setSlashOpen(false); }}
                />
              </span>
              <div className="e8-composer-foot">
                <DSm2.Button variant="ghost" size="sm" icon={drafting ? 'progress_activity' : 'smart_toy'} onClick={aiDraft} disabled={drafting}>{drafting ? (draftDetail || 'Drafting on-device…') : 'Draft reply'}</DSm2.Button>
                <DSm2.Button variant="ghost" size="sm" icon="description" iconOnly title="Templates" onClick={() => setTab('templates')}></DSm2.Button>
                <DSm2.Button variant="ghost" size="sm" icon="schedule" iconOnly title="Schedule send" onClick={() => showToast('Pick a send time - scheduled messages wait in Outbox')}></DSm2.Button>
                <span style={{ marginLeft: 'auto', display: 'inline-flex', alignItems: 'center', gap: 8 }}>
                  <span className="e8-kbd">⌘↵</span>
                  <DSm2.Button variant="primary" size="sm" icon="send" onClick={send} disabled={!draft.trim()}>Send</DSm2.Button>
                </span>
              </div>
            </div>
          </div>
        ) : (
          <div className="e8-msgs-thread" style={{ alignItems: 'center', justifyContent: 'center' }}>
            <DSm2.EmptyState icon="forum" title="No conversation selected" body="Pick a thread from the list, or start a new message." />
          </div>
        )}

        {/* Context rail (desktop) */}
        {!isMobile && convo ? (
          <div className="e8-msgs-rail">
            <TeamPanel convo={convo} />
            <ConvoContext convo={convo} />
          </div>
        ) : null}
      </div>
      {isMobile ? (
        <BottomSheet open={railOpen} onClose={() => setRailOpen(false)} title={convo ? convo.name : 'Conversation'}>
          {convo ? <React.Fragment><TeamPanel convo={convo} /><ConvoContext convo={convo} /></React.Fragment> : null}
        </BottomSheet>
      ) : null}
      {convo ? (
        <BottomSheet open={reachOpen} onClose={() => setReachOpen(false)} title={'Reach - ' + (convo.name || '')}>
          <ReachPanel convo={convo} />
        </BottomSheet>
      ) : null}
    </React.Fragment>
  );
}

/* Team collaboration (R56): a fake teammate roster for assignment + @mentions. */
const TEAMMATES = ['Sarah Kim', 'Mike Torres', 'Jess Alvarez', 'Dana Brooks', 'Priya Nair'];

/* Render an internal note, highlighting @mentions of known teammates. */
function renderNoteText(text) {
  const out = []; const rest = String(text || ''); const re = /@([A-Z][a-z]+ [A-Z][a-z]+)/g;
  let last = 0, m;
  while ((m = re.exec(rest))) {
    if (TEAMMATES.indexOf(m[1]) > -1) {
      if (m.index > last) out.push(rest.slice(last, m.index));
      out.push(<span key={m.index} className="e8-mention">@{m[1]}</span>);
      last = m.index + m[0].length;
    }
  }
  if (last < rest.length) out.push(rest.slice(last));
  return out.length ? out : rest;
}

/* Internal team lane for a conversation: assignment, simulated presence, and private @mention
   notes (never sent to the candidate). Prototype: state seeds per conversation and is local. */
function TeamPanel({ convo }) {
  const D = window.E8DATA;
  const { showToast } = React.useContext(E8Ctx);
  const me = (D.user && D.user.name) || 'Sarah Kim';
  const first = (convo.name || '').split(' ')[0];
  const others = TEAMMATES.filter((t) => t !== me);
  const seedBy = others[String(convo.id).length % others.length];
  const viewer = others[String(convo.id).split('').reduce((a, c) => a + c.charCodeAt(0), 0) % others.length];
  const seedNote = () => [{ by: seedBy, when: 'earlier', text: '@' + me + ' looping you in on ' + first + ' - flagging so we keep momentum.' }];
  /* R78: assignment + team notes persist per conversation via the e8-comms-v1 overlay. */
  const savedAssign = () => (window.E8Comms ? (E8Comms.load().assigned || {})[convo.id] : null);
  const savedNotes = () => (window.E8Comms ? (E8Comms.load().teamNotes || {})[convo.id] : null);
  const [assignee, setAssignee] = React.useState(() => savedAssign() || convo.owner || me);
  const [assignOpen, setAssignOpen] = React.useState(false);
  const [notes, setNotes] = React.useState(() => savedNotes() || seedNote());
  const [input, setInput] = React.useState('');
  const [mentionOpen, setMentionOpen] = React.useState(false);
  React.useEffect(() => { setAssignee(savedAssign() || convo.owner || me); setNotes(savedNotes() || seedNote()); setInput(''); setMentionOpen(false); setAssignOpen(false); }, [convo.id]);
  const partial = (input.match(/@(\w*)$/) || [])[1] || '';
  const post = () => {
    const t = input.trim(); if (!t) return;
    setNotes((n) => {
      const next = n.concat([{ by: me, when: 'just now', text: t }]);
      if (window.E8Comms) E8Comms.save({ teamNotes: { ...(E8Comms.load().teamNotes || {}), [convo.id]: next } });
      return next;
    });
    setInput(''); setMentionOpen(false);
  };
  const pick = (name) => { setInput((v) => v.replace(/@\w*$/, '@' + name + ' ')); setMentionOpen(false); };
  return (
    <div style={{ marginBottom: 16 }}>
      <div className="e8-sectionlabel" style={{ marginBottom: 8 }}>Team</div>
      <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8, position: 'relative', flexWrap: 'wrap' }}>
        <span style={{ fontSize: 'var(--ui-text-sm)', color: 'var(--ui-text-secondary)' }}>Assigned to</span>
        <button type="button" className="e8-chpill" onClick={() => setAssignOpen((v) => !v)}>
          <DSm2.Avatar name={assignee} size="sm" /><b style={{ fontSize: 'var(--ui-text-sm)' }}>{assignee === me ? 'You' : assignee}</b>
          <span className="material-symbols-outlined" style={{ fontSize: 14 }}>{assignOpen ? 'expand_less' : 'expand_more'}</span>
        </button>
        {assignOpen ? (
          <div className="e8-chpicker">
            {TEAMMATES.map((t) => (
              <button key={t} type="button" className="e8-chpicker-item" onClick={() => { setAssignee(t); setAssignOpen(false); if (window.E8Comms) E8Comms.save({ assigned: { ...(E8Comms.load().assigned || {}), [convo.id]: t } }); if (window.E8Audit) window.E8Audit.log({ agent: 'You', prov: 'human', action: 'Assigned conversation to ' + t, target: convo.name, route: 'inbox/' + convo.id }); if (showToast) showToast('Assigned to ' + (t === me ? 'you' : t)); }}>
                <DSm2.Avatar name={t} size="sm" /><span style={{ flex: 1, minWidth: 0, textAlign: 'left' }}>{t === me ? 'You' : t}</span>
                {t === assignee ? <span className="material-symbols-outlined" style={{ fontSize: 14, color: 'var(--ui-accent)' }}>check</span> : null}
              </button>
            ))}
          </div>
        ) : null}
      </div>
      <div style={{ display: 'inline-flex', alignItems: 'center', gap: 5, fontSize: 'var(--ui-text-xs)', color: 'var(--ui-text-tertiary)', marginBottom: 10 }}>
        <span style={{ width: 6, height: 6, borderRadius: 'var(--ui-radius-pill)', background: 'var(--ui-success)', display: 'inline-block' }}></span>{viewer} is viewing
      </div>
      <div style={{ fontSize: 'var(--ui-text-xs)', color: 'var(--ui-text-tertiary)', marginBottom: 6 }}>Internal notes - not sent to {first}</div>
      {notes.map((n, i) => (
        <div key={i} className="e8-teamnote">
          <div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 3 }}>
            <DSm2.Avatar name={n.by} size="sm" /><b style={{ fontSize: 'var(--ui-text-xs)' }}>{n.by === me ? 'You' : n.by}</b>
            <span style={{ fontSize: 'var(--ui-text-xs)', color: 'var(--ui-text-tertiary)' }}>{n.when}</span>
          </div>
          <div style={{ fontSize: 'var(--ui-text-sm)', color: 'var(--ui-text-secondary)', lineHeight: 1.45 }}>{renderNoteText(n.text)}</div>
        </div>
      ))}
      <div style={{ position: 'relative', marginTop: 6 }}>
        {mentionOpen ? (
          <div className="e8-chpicker" style={{ bottom: 46, top: 'auto' }}>
            {TEAMMATES.filter((t) => t.toLowerCase().indexOf(partial.toLowerCase()) > -1).map((t) => (
              <button key={t} type="button" className="e8-chpicker-item" onMouseDown={(e) => { e.preventDefault(); pick(t); }}>
                <DSm2.Avatar name={t} size="sm" /><span style={{ flex: 1, minWidth: 0, textAlign: 'left' }}>{t}</span>
              </button>
            ))}
          </div>
        ) : null}
        <textarea className="e8-cap-textarea" style={{ minHeight: 52 }} value={input} placeholder="Add an internal note - type @ to mention" onChange={(e) => { const v = e.target.value; setInput(v); setMentionOpen(/@\w*$/.test(v)); }} />
        <div style={{ display: 'flex', justifyContent: 'flex-end', marginTop: 6 }}>
          <DSm2.Button variant="secondary" size="sm" icon="send" onClick={post} disabled={!input.trim()}>Add note</DSm2.Button>
        </div>
      </div>
    </div>
  );
}

/* Candidate-facing reach (R57): push re-engagement, an actionable notification preview, the
   push -> SMS -> email delivery-fallback chain, and outbound Web Share. Notification permission +
   Web Share are real client-side APIs; delivery + the SMS/email legs need a backend (labeled). */
function ReachPanel({ convo }) {
  const { showToast } = React.useContext(E8Ctx);
  const first = (convo.name || '').split(' ')[0];
  const [perm, setPerm] = React.useState((typeof Notification !== 'undefined' && Notification.permission) || 'unsupported');
  /* R80: rides E8Notify - permission + label cache + a real Web Push subscription attempt. */
  const enablePush = async () => {
    if (!window.E8Notify || !window.E8Notify.state().supported) { if (showToast) showToast('Notifications are not supported in this browser'); return; }
    const res = await window.E8Notify.enable('candidate');
    setPerm(window.E8Notify.state().permission);
    if (res.ok) { if (showToast) showToast(res.pushed ? 'Push enabled - real delivery active' : 'Notifications on for this device'); }
    else if (showToast) showToast(res.reason === 'denied' ? 'Blocked - enable notifications in the browser' : 'Push permission: ' + res.reason);
  };
  /* Send the actual actionable notification through the service worker - Confirm/Reschedule
     on the OS notification writes the reply into this thread. */
  const sendReal = async () => {
    const ok = await window.E8Notify.show('interview', { convoId: convo.id, name: convo.name, client: 'FedEx', when: 'Tuesday 10:00 AM' });
    if (showToast) showToast(ok ? 'Notification sent - try Confirm on it' : 'Enable push first, then send');
  };
  const share = async () => {
    const data = { title: convo.name, text: first + ' - candidate one-pager', url: window.location.origin + '/#/candidate/' + (convo.candId || '') };
    try {
      if (navigator.share) { await navigator.share(data); }
      else if (navigator.clipboard) { await navigator.clipboard.writeText(data.url); if (showToast) showToast('Link copied to clipboard'); }
      else if (showToast) showToast('Sharing not supported here');
    } catch (e) {}
  };
  return (
    <div style={{ display: 'grid', gap: 16 }}>
      <div>
        <div className="e8-sectionlabel" style={{ marginBottom: 6 }}>Re-engagement push</div>
        <div style={{ fontSize: 'var(--ui-text-sm)', color: 'var(--ui-text-secondary)', marginBottom: 8, lineHeight: 1.5 }}>Interview reminders, status changes and new matches reach {first} even when the tab is closed.</div>
        <DSm2.Button variant={perm === 'granted' ? 'secondary' : 'primary'} size="sm" icon={perm === 'granted' ? 'notifications_active' : 'notifications'} onClick={enablePush} disabled={perm === 'granted'}>{perm === 'granted' ? 'Push enabled' : perm === 'denied' ? 'Blocked - enable in browser' : 'Enable push'}</DSm2.Button>
      </div>
      <div>
        <div className="e8-sectionlabel" style={{ marginBottom: 6 }}>What {first} receives</div>
        <div style={{ border: '1px solid var(--ui-border)', borderRadius: 'var(--ui-radius-xl)', padding: 12, background: 'var(--ui-surface)' }}>
          <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 6 }}>
            <span style={{ width: 18, height: 18, borderRadius: 5, background: 'var(--ui-accent)', color: 'var(--ui-accent-text)', fontSize: 'var(--ui-text-xs)', fontWeight: 700, display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }}>8</span>
            <b style={{ fontSize: 'var(--ui-text-sm)' }}>ELEV8</b><span style={{ fontSize: 'var(--ui-text-xs)', color: 'var(--ui-text-tertiary)' }}>now</span>
          </div>
          <div style={{ fontSize: 'var(--ui-text-sm)', fontWeight: 600 }}>Interview Tuesday 10:00 AM</div>
          <div style={{ fontSize: 'var(--ui-text-sm)', color: 'var(--ui-text-secondary)', marginBottom: 8 }}>Confirm your screen with FedEx, or pick another time.</div>
          <div style={{ display: 'flex', gap: 8 }}>
            <DSm2.Button variant="secondary" size="sm" onClick={() => window.E8Notify.exec('confirm', { act: 'interview', convoId: convo.id, route: 'inbox/' + convo.id })}>Confirm</DSm2.Button>
            <DSm2.Button variant="ghost" size="sm" onClick={() => window.E8Notify.exec('reschedule', { act: 'interview', convoId: convo.id, route: 'inbox/' + convo.id })}>Reschedule</DSm2.Button>
          </div>
        </div>
        <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginTop: 6 }}>
          <DSm2.Button variant="secondary" size="sm" icon="notifications" onClick={sendReal} disabled={perm !== 'granted'}>Send it as a real notification</DSm2.Button>
        </div>
        <div style={{ fontSize: 'var(--ui-text-xs)', color: 'var(--ui-text-tertiary)', marginTop: 6 }}>Actionable notification - Confirm/Reschedule (here or on the OS notification) writes the reply into this thread. Candidate-device delivery activates with the push backend.</div>
      </div>
      <div>
        <div className="e8-sectionlabel" style={{ marginBottom: 6 }}>Delivery fallback</div>
        <div style={{ display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap' }}>
          {[{ k: 'Push', on: true }, { k: 'SMS', on: false }, { k: 'Email', on: false }].map((s, i) => (
            <React.Fragment key={s.k}>
              {i ? <span className="material-symbols-outlined" style={{ fontSize: 14, color: 'var(--ui-text-tertiary)' }}>chevron_right</span> : null}
              <span style={{ display: 'inline-flex', alignItems: 'center', gap: 4, padding: '2px 9px', borderRadius: 'var(--ui-radius-pill)', background: s.on ? 'var(--ui-success-tint)' : 'var(--ui-fill)', color: s.on ? 'var(--ui-success-text)' : 'var(--ui-text-secondary)', fontWeight: 600, fontSize: 11 }}>{s.on ? <span className="material-symbols-outlined" style={{ fontSize: 13 }}>check</span> : null}{s.k}</span>
            </React.Fragment>
          ))}
        </div>
        <div style={{ fontSize: 'var(--ui-text-xs)', color: 'var(--ui-text-tertiary)', marginTop: 6 }}>Push first, then falls back to SMS then email by delivery confirmation. SMS/RCS via RingCentral or Twilio (backend).</div>
      </div>
      <div>
        <DSm2.Button variant="ghost" size="sm" icon="share" onClick={share}>Share {first}'s one-pager</DSm2.Button>
      </div>
    </div>
  );
}

/* Right rail: the same collapsible record panel as the candidate/contact pages,
   so the inbox shows who you're talking to in one consistent shape. */
function ConvoContext({ convo }) {
  const D = window.E8DATA;
  const RailGroup = window.RailGroup;
  const cand = convo.candId ? D.matches.find((m) => m.id === convo.candId) : null;
  const listMeta = cand ? (D.candidates.find((x) => x.id === cand.id) || {}) : {};
  const prof = cand ? window.profileOf(cand, listMeta) : null;
  const contact = D.contacts.find((c) => c.name === convo.name);
  const engagement = D.engagements.find((e) => e.consultant === convo.name);
  const cl = contact ? D.clients.find((x) => x.name === contact.client) : null;

  const kv = (k, v) => <div className="e8-kv" key={k}><span className="e8-kv-k">{k}</span><span className="e8-kv-v">{v}</span></div>;

  return (
    <React.Fragment>
      <RailGroup title="Personal">
        {cand ? (
          <React.Fragment>
            {kv('Location', cand.location)}
            {kv('Email', cand.email)}
            <div className="e8-kv"><span className="e8-kv-k">Phone</span><span className="e8-kv-v tnum">{cand.phone}</span></div>
            {kv('Languages', prof.languages.join(', '))}
            <div className="e8-kv"><span className="e8-kv-k">Rate</span><span className="e8-kv-v tnum">{cand.rate} W2</span></div>
            <div style={{ marginTop: 8 }}>
              <div className="e8-kv-k" style={{ marginBottom: 6 }}>Tech stack</div>
              <div style={{ display: 'flex', gap: 5, flexWrap: 'wrap' }}>
                {prof.techStack.map((s) => <DSm2.SkillChip key={s}>{s}</DSm2.SkillChip>)}
              </div>
            </div>
          </React.Fragment>
        ) : contact ? (
          <React.Fragment>
            {kv('Role', contact.role)}
            {kv('Client', <a className="e8-link" tabIndex={0} onKeyDown={window.keyActivate} onClick={() => (cl ? navigate('client/' + cl.id) : null)}>{contact.client}</a>)}
            {kv('Email', contact.email)}
            <div className="e8-kv"><span className="e8-kv-k">Phone</span><span className="e8-kv-v tnum">{contact.phone}</span></div>
            {kv('Replies', contact.reply)}
          </React.Fragment>
        ) : engagement ? (
          <React.Fragment>
            {kv('Role', engagement.role)}
            {kv('Client', engagement.client)}
            <div className="e8-kv"><span className="e8-kv-k">Ends</span><span className="e8-kv-v tnum">{engagement.ends}</span></div>
          </React.Fragment>
        ) : (
          <div style={{ fontSize: 'var(--ui-text-sm)', color: 'var(--ui-text-secondary)' }}>{convo.sub}</div>
        )}
      </RailGroup>

      <RailGroup title="Reachable on" count={convo.identities.length}>
        <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
          {convo.identities.map((i) => (
            <span key={i.ch + i.label} style={{ display: 'inline-flex', alignItems: 'center', gap: 7, fontSize: 'var(--ui-text-sm)', color: 'var(--ui-text-secondary)' }}>
              <ChannelDot ch={i.ch} size={14} /> {i.label}
            </span>
          ))}
        </div>
      </RailGroup>

      {cand ? (
        <RailGroup title="Active applications" count={prof.applications.length}>
          {prof.applications.length ? prof.applications.map((a, i) => (
            <button key={i} type="button" className="e8-applrow" onClick={() => navigate('job/' + a.jobId + '/overview')}>
              <span style={{ minWidth: 0 }}>
                <span style={{ display: 'block', fontSize: 'var(--ui-text-sm)', fontWeight: 500, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{a.job}</span>
                <span style={{ display: 'block', fontSize: 'var(--ui-text-xs)', color: 'var(--ui-text-tertiary)' }}>{a.jobId} · {a.client}</span>
              </span>
              <span style={{ marginLeft: 'auto', display: 'inline-flex', alignItems: 'center', gap: 6, flex: 'none' }}>
                <DSm2.Badge tone={a.tone || 'info'} dot>{a.stage}</DSm2.Badge>
              </span>
            </button>
          )) : <div style={{ fontSize: 'var(--ui-text-sm)', color: 'var(--ui-text-tertiary)' }}>Not in any active process.</div>}
        </RailGroup>
      ) : null}

      {cand ? (
        <RailGroup title="Match · JO-44219" defaultOpen={false}>
          <DSm2.MatchBadge tier={cand.tier} reasons={cand.reasons} footnote="Weighted by the job’s 3 criteria" />
        </RailGroup>
      ) : null}

      {contact ? (
        <RailGroup title="Working on">
          <div style={{ fontSize: 'var(--ui-text-sm)', color: 'var(--ui-text-secondary)', marginBottom: 6 }}>{contact.owns}</div>
          <a className="e8-link" style={{ fontSize: 'var(--ui-text-sm)' }} tabIndex={0} onKeyDown={window.keyActivate} onClick={() => navigate('contact/' + contact.id)}>Open contact record →</a>
        </RailGroup>
      ) : null}

      {engagement ? (
        <RailGroup title="Engagement">
          <div className="e8-kv"><span className="e8-kv-k">PO burn</span><span className="e8-kv-v tnum">{engagement.poBurn}%</span></div>
          <div className="e8-kv"><span className="e8-kv-k">Renewal</span><span className="e8-kv-v">{engagement.renewal}</span></div>
          <div style={{ marginTop: 6 }}><a className="e8-link" style={{ fontSize: 'var(--ui-text-sm)' }} tabIndex={0} onKeyDown={window.keyActivate} onClick={() => navigate('renewals')}>Open renewal →</a></div>
        </RailGroup>
      ) : null}

      {cand && prof.lists.length ? (
        <RailGroup title="Lists" count={prof.lists.length} defaultOpen={false}>
          <div style={{ display: 'flex', gap: 5, flexWrap: 'wrap' }}>
            {prof.lists.map((l) => <span key={l} className="e8-listpill"><span className="material-symbols-outlined" style={{ fontSize: 13 }}>bookmark</span>{l}</span>)}
          </div>
        </RailGroup>
      ) : null}

      {cand && prof.campaigns.length ? (
        <RailGroup title="Campaigns" count={prof.campaigns.length} defaultOpen={false}>
          {prof.campaigns.map((cp) => (
            <button key={cp.name} type="button" className="e8-applrow" onClick={() => navigate('sequences')}>
              <span style={{ fontSize: 'var(--ui-text-sm)', fontWeight: 500, minWidth: 0, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{cp.name}</span>
              <span style={{ marginLeft: 'auto', flex: 'none' }}><DSm2.Badge tone="success" dot>{cp.state}</DSm2.Badge></span>
            </button>
          ))}
        </RailGroup>
      ) : null}
    </React.Fragment>
  );
}

/* ============================== NOTES ============================== */
function NotesScreen() {
  const D = window.E8DATA;
  const { showToast } = React.useContext(E8Ctx);
  const cap = React.useContext(window.CaptureCtx);
  const [mine, setMine] = React.useState(false);
  const [, setNotesTick] = React.useState(0);
  React.useEffect(() => (window.E8Events ? window.E8Events.subscribe(['store:changed'], () => setNotesTick((t) => t + 1)) : undefined), []);
  const [query, setQuery] = React.useState('');
  const q = query.trim().toLowerCase();
  const rows = D.notes.filter((n) => (!mine || n.by === D.user.name) && (!q || Object.values(n).some((v) => typeof v === 'string' && v.toLowerCase().includes(q))));
  const weeks = [['this', 'This week'], ['last', 'Last week']];

  return (
    <React.Fragment>
      <Topbar
        crumbs={[{ label: 'Notes' }]}
        actions={
          <React.Fragment>
            <DSm2.Button variant="ghost" size="sm" icon={mine ? 'check_box' : 'check_box_outline_blank'} onClick={() => setMine(!mine)}>Only my notes</DSm2.Button>
            <DSm2.Button variant="secondary" size="sm" icon="description">Templates · 4</DSm2.Button>
            <DSm2.Button variant="primary" size="sm" icon="add" onClick={() => (cap ? cap.open({ target: 'note' }) : null)}>Add note</DSm2.Button>
          </React.Fragment>
        }
      />
      <div className="e8-content">
        <div className="e8-page">
          <div style={{ display: 'flex', alignItems: 'center', gap: 12, margin: '0 0 16px', flexWrap: 'wrap' }}>
            <div style={{ flex: '0 1 420px', minWidth: 260 }}>
              <DSm2.Input icon="search" placeholder="Search notes…" value={query} onChange={(e) => setQuery(e.target ? e.target.value : e)} />
            </div>
            <span style={{ fontSize: 'var(--ui-text-sm)', color: 'var(--ui-text-tertiary)' }}>
              Meeting and call notes are drafted automatically - every summary links back to its recording.
            </span>
          </div>

          {weeks.map(([wk, label]) => {
            const group = rows.filter((n) => n.week === wk);
            if (!group.length) return null;
            return (
              <div key={wk} style={{ marginBottom: 22 }}>
                <div className="e8-sectionlabel" style={{ marginBottom: 10 }}>{label}</div>
                <div className="e8-notes-grid">
                  {group.map((n) => (
                    <div key={n.id} className="e8-note-card" {...window.clickableProps(() => showToast(n.ai ? 'Full note opens with the recording and transcript' : 'Note opens here'))}>
                      <div style={{ display: 'flex', alignItems: 'center', gap: 7, flexWrap: 'wrap' }}>
                        <span className="e8-note-src">
                          <span className="material-symbols-outlined" style={{ fontSize: 13 }}>{n.icon}</span>
                          {n.source}
                        </span>
                        <span className="e8-note-src" style={{ background: 'transparent' }}>{n.entity}</span>
                        {n.ai ? <DSm2.ProvenanceBadge kind="drafted" /> : null}
                      </div>
                      <div style={{ fontSize: 'var(--ui-text-base)', fontWeight: 500, lineHeight: 1.35 }}>{n.title}</div>
                      <div>
                        <div style={{ fontSize: 'var(--ui-text-xs)', fontWeight: 500, color: 'var(--ui-text-tertiary)', marginBottom: 4 }}>Key takeaways</div>
                        <ul style={{ margin: 0, paddingLeft: 16, display: 'grid', gap: 3 }}>
                          {n.points.map((p, i) => (
                            <li key={i} style={{ fontSize: 'var(--ui-text-sm)', lineHeight: 1.45, color: 'var(--ui-text-secondary)' }}>{p}</li>
                          ))}
                        </ul>
                      </div>
                      <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginTop: 'auto', paddingTop: 8, borderTop: '1px solid var(--ui-border)' }}>
                        <ActorChip name={n.by} ai={!!n.agent} />
                        <span className="tnum" style={{ marginLeft: 'auto', fontSize: 'var(--ui-text-xs)', color: 'var(--ui-text-tertiary)' }}>{n.when}</span>
                      </div>
                    </div>
                  ))}
                </div>
              </div>
            );
          })}
        </div>
      </div>
    </React.Fragment>
  );
}

/* ============================== SEQUENCES ============================== */
/* Multi-step, multi-channel outreach. The AI writes and personalizes each step;
   a reply pauses the sequence for that person instantly. */
/* Role-aware default scope: an ops lead (the sequence manager) lands on the whole desk (All);
   a recruiter/CSM lands on their own book (Mine). Reads the live persona so a switch snaps to
   the right scope. */
function seqDefaultScope() {
  const p = window.e8ActivePersona ? window.e8ActivePersona() : null;
  return p && p.role === 'ops' ? 'all' : 'mine';
}
function SequencesScreen() {
  const D = window.E8DATA;
  const C = window.E8Charts;
  const { showToast } = React.useContext(E8Ctx);
  // Lazy init: at scale (50k) this map is built once on mount, not on every render.
  const [states, setStates] = React.useState(() => Object.fromEntries(D.sequences.map((s) => [s.id, s.state])));
  const [filter, setFilter] = React.useState('all');
  const [view, setView] = React.useState('list');
  const [seqFilter, setSeqFilter] = React.useState('all');
  const [scope, setScope] = React.useState(seqDefaultScope);
  const [replies, setReplies] = React.useState(() => (D.sequenceReplies || []).map((r) => ({ ...r })));
  const [archived, setArchived] = React.useState(() => new Set());
  const [enrolling, setEnrolling] = React.useState(false);
  // R103 Task 3: search + paging so the list holds up from 0 to 50,000 sequences.
  const [search, setSearch] = React.useState('');
  const SEQ_PAGE = 50;      // rows revealed per "Show more" click, and the table's page size
  const SEQ_TABLE_AT = 200; // above this many matches, cards give way to a compact paged table
  const [shown, setShown] = React.useState(SEQ_PAGE);
  const [reassign, setReassign] = React.useState(null); // R103 Task 4: the sequence being reassigned, or null

  // R103: ownership scoping. Re-render on persona/store changes so the "Viewing as" switch (and an
  // ops reassignment) re-scopes the list, KPIs, counts and Replies live. On a persona switch, snap to
  // that role's default scope (ops -> All, recruiter/CSM -> Mine). storeVer is a monotonic bump that
  // is threaded into the model memo deps below: a reassignment is an in-place E8Store.set on the same
  // D.sequences array, so without a changing dep the memo would return its stale scoping.
  const [storeVer, setStoreVer] = React.useState(0);
  React.useEffect(() => {
    if (!window.E8Events) return undefined;
    return window.E8Events.subscribe(['persona:changed', 'store:changed'], (ev) => {
      if (ev && ev.type === 'persona:changed') setScope(seqDefaultScope());
      setStoreVer((v) => v + 1);
    });
  }, []);
  const persona = window.e8ActivePersona ? window.e8ActivePersona() : null;
  const isOps = !!persona && persona.role === 'ops';
  const personaKey = persona ? persona.name : '';

  // R103 Task 3: every O(n) pass over the scoped source (which can be 50k rows) is memoized so
  // that unrelated re-renders - a paging click, a view switch, a store ping - don't re-filter and
  // re-reduce the whole desk. Keyed on the data, the active persona, the scope + search + subtab,
  // and the local pause/archive maps that change the running/paused split.
  const model = React.useMemo(() => {
    const all = D.sequences || [];
    // Ops leads see the whole desk; recruiter/CSM see only their book (repId OR owner-name match).
    const scoped = window.mySequences ? window.mySequences(persona) : all;
    // Ops "Mine" = the sequences the ops lead personally owns; ops "All" = every sequence. Non-ops
    // personas get no toggle - they always see their book, so "All" never leaks another owner's work.
    const opsOwned = all.filter((s) => persona && ((persona.repId && s.ownerId === persona.repId) || s.owner === persona.name));
    const base = !isOps ? scoped : (scope === 'mine' ? opsOwned : all);
    const scopedIds = new Set(base.map((s) => s.id));
    const live = base.filter((s) => !archived.has(s.id));
    const runningCount = live.filter((s) => states[s.id] === 'running').length;
    const pausedCount = live.length - runningCount;
    const enrolled = base.reduce((n, s) => n + (s.enrolled || 0), 0);
    const meetings = base.reduce((n, s) => n + (s.meetings || 0), 0);
    const replied = base.reduce((n, s) => n + (s.replied || 0), 0);
    const replyRate = Math.round((replied / Math.max(1, enrolled)) * 100);
    const q = search.trim().toLowerCase();
    const searched = q
      ? live.filter((s) => (s.name + ' ' + (s.audience || '') + ' ' + (s.owner || '')).toLowerCase().includes(q))
      : live;
    const visible = searched.filter((s) => filter === 'all' || (filter === 'running' ? states[s.id] === 'running' : states[s.id] !== 'running'));
    return { base, opsOwned, scopedIds, live, runningCount, pausedCount, enrolled, meetings, replied, replyRate, visible };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [D.sequences, personaKey, isOps, scope, search, filter, states, archived, storeVer]);
  const { base, opsOwned, scopedIds, live, runningCount, pausedCount, enrolled, meetings, replyRate, visible } = model;

  const scopedReplies = replies.filter((r) => scopedIds.has(r.seqId));
  const unhandledOf = (sid) => replies.filter((r) => r.seqId === sid && !r.handled).length;
  const totalUnhandled = scopedReplies.filter((r) => !r.handled).length;
  const openReplies = (sid) => { setSeqFilter(sid || 'all'); setView('replies'); };
  const T = D.sequenceTrends;
  // Snap the card window back to the first page whenever the result set changes underneath it.
  React.useEffect(() => { setShown(SEQ_PAGE); }, [search, scope, filter, personaKey]);

  const toggle = (s) => {
    const next = states[s.id] === 'running' ? 'paused' : 'running';
    setStates((x) => ({ ...x, [s.id]: next }));
    showToast(s.name + (next === 'paused' ? ' paused' : ' resumed'), 'Undo', () => setStates((x) => ({ ...x, [s.id]: states[s.id] })));
  };
  const duplicate = (s) => { showToast('Duplicated ' + s.name + ' - opening the builder'); navigate('sequences/new'); };
  const archive = (s) => {
    setArchived((a) => { const n = new Set(a); n.add(s.id); return n; });
    showToast(s.name + ' archived', 'Undo', () => setArchived((a) => { const n = new Set(a); n.delete(s.id); return n; }));
  };

  const filterEmpty = {
    all: { icon: 'campaign', title: 'No active sequences', body: 'Start from a template below, or build your own to begin reaching people.' },
    running: { icon: 'play_circle', title: 'No running sequences', body: 'Resume a paused sequence, or start a new one to begin sending.' },
    paused: { icon: 'pause_circle', title: 'No paused sequences', body: 'Every sequence is currently running - pause one to hold its sends.' },
  };

  // Above ~200 matches the rich cards give way to this compact table so heavy card mounts stay capped.
  const seqCols = [
    { key: 'name', label: 'Sequence', locked: true, render: (s) => (
      <span style={{ minWidth: 0 }}>
        <a className="e8-link" role="link" tabIndex={0}
          onClick={(e) => { e.stopPropagation(); navigate('sequence/' + s.id); }}
          onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); e.stopPropagation(); navigate('sequence/' + s.id); } }}
          style={{ display: 'block', fontWeight: 500 }}>{s.name}</a>
        <span style={{ display: 'block', fontSize: 'var(--ui-text-sm)', color: 'var(--ui-text-tertiary)' }}>{s.audience} · {s.steps.length} steps</span>
      </span>
    ) },
    { key: 'owner', label: 'Owner', render: (s) => <ActorChip name={s.owner} /> },
    { key: 'status', label: 'Status', render: (s) => {
      const running = states[s.id] === 'running';
      return <DSm2.Badge tone={running ? 'success' : 'neutral'} dot>{running ? 'Running' : 'Paused'}</DSm2.Badge>;
    } },
    { key: 'enrolled', label: 'Enrolled', num: true, render: (s) => <span className="tnum">{(s.enrolled || 0).toLocaleString()}</span> },
    { key: 'replyRate', label: 'Reply rate', render: (s) => {
      const rr = parseInt(String(s.replyRate).replace(/\D/g, ''), 10) || 0;
      return (
        <span className="e8-seqt-rate">
          <span className="e8-seqt-rate-track"><span className="e8-seqt-rate-fill" style={{ width: rr + '%' }}></span></span>
          <span className="tnum">{rr}%</span>
        </span>
      );
    } },
    { key: 'replies', label: 'Replies', render: (s) => {
      const nNew = unhandledOf(s.id);
      return nNew > 0 ? <DSm2.Badge tone="accent">{nNew} new</DSm2.Badge> : <span style={{ color: 'var(--ui-text-tertiary)' }}>-</span>;
    } },
  ];

  // Count-aware header framing: N on the current page of a possibly huge match set.
  const matchTotal = visible.length;
  const pageShown = matchTotal > SEQ_TABLE_AT ? Math.min(SEQ_PAGE, matchTotal) : Math.min(shown, matchTotal);
  const q = search.trim();

  return (
    <React.Fragment>
      <Topbar
        crumbs={[{ label: 'Sequences' }]}
        actions={
          <React.Fragment>
            <DSm2.Button variant="secondary" size="sm" icon="person_add" onClick={() => setEnrolling(true)}>Enroll people</DSm2.Button>
            <DSm2.Button variant="primary" size="sm" icon="add" onClick={() => navigate('sequences/new')}>New sequence</DSm2.Button>
          </React.Fragment>
        }
      />
      <div className="e8-content">
        <div className="e8-page" style={{ maxWidth: 1000 }}>
          <div className="e8-pagehead e8-pagehead-wrap">
            <div className="e8-pagehead-main">
              <h1 className="e8-h1">Sequences</h1>
              <div className="e8-pagehead-sub" style={{ maxWidth: 620 }}>
                Multi-step outreach across email, LinkedIn, calls and text. Every step is drafted and personalized for you - a reply pauses that person’s sequence instantly.
              </div>
            </div>
            <div className="e8-pagehead-actions">
              <window.PersonaViewAs />
            </div>
          </div>

          {/* Ops-only ownership scope: Mine (owned) | All (the whole desk). Recruiter/CSM see only
              their own book, so the toggle would be a no-op for them - it's shown to ops leads only. */}
          {isOps ? (
            <div style={{ margin: '2px 0 10px' }}>
              <DSm2.SubTabs
                items={[
                  { id: 'mine', label: 'Mine', count: opsOwned.length },
                  { id: 'all', label: 'All sequences', count: (D.sequences || []).length },
                ]}
                active={scope}
                onChange={setScope}
              />
            </div>
          ) : null}

          {/* Primary view tabs: Sequences | Replies */}
          <div style={{ margin: '2px 0 12px' }}>
            <DSm2.Tabs
              items={[
                { id: 'list', label: 'Sequences', count: base.length },
                { id: 'replies', label: 'Replies', count: totalUnhandled },
              ]}
              active={view}
              onChange={setView}
            />
          </div>

          {view === 'replies' ? (
            <SequenceRepliesView replies={scopedReplies} setReplies={setReplies} seqs={base} seqFilter={seqFilter} setSeqFilter={setSeqFilter} showToast={showToast} />
          ) : (
          <React.Fragment>
          {/* Program KPI band */}
          <div className="e8-kpi-row" style={{ gridTemplateColumns: 'repeat(4, 1fr)' }}>
            {window.KpiCard ? (
              <React.Fragment>
                <window.KpiCard label="Active sequences" value={runningCount} delta={pausedCount + ' paused'} dir="flat" spark={T.active} />
                <window.KpiCard label="People enrolled" value={enrolled} delta="+12% this month" dir="up" spark={T.enrolled} />
                <window.KpiCard label="Reply rate" value={replyRate + '%'} delta="+5 pts vs prior" dir="up" spark={T.replyRate} />
                <window.KpiCard label="Meetings booked" value={meetings} delta="from sequences" dir="up" spark={T.meetings} />
              </React.Fragment>
            ) : null}
          </div>

          {/* Filter tabs */}
          <div style={{ margin: '4px 0 10px' }}>
            <DSm2.SubTabs
              items={[
                { id: 'all', label: 'All', count: base.length },
                { id: 'running', label: 'Running', count: runningCount },
                { id: 'paused', label: 'Paused', count: pausedCount },
              ]}
              active={filter}
              onChange={setFilter}
            />
          </div>

          {/* Search + count-aware header - keeps the list usable from 0 to 50,000 sequences */}
          <div className="e8-seq-listhead">
            <div className="e8-seq-listhead-search">
              <DSm2.Input icon="search" placeholder="Search by name, audience or owner…" value={search} onChange={(e) => setSearch(e.target ? e.target.value : e)} aria-label="Search sequences" />
            </div>
            <span className="e8-seq-count">
              {matchTotal === 0
                ? 'No matches'
                : matchTotal > SEQ_TABLE_AT
                  ? matchTotal.toLocaleString() + ' sequences'
                  : pageShown < matchTotal
                    ? 'Showing ' + pageShown.toLocaleString() + ' of ' + matchTotal.toLocaleString()
                    : matchTotal.toLocaleString() + (matchTotal === 1 ? ' sequence' : ' sequences')}
            </span>
          </div>

          {visible.length === 0 ? (
            live.length === 0 ? (
              <DSm2.EmptyState
                icon="campaign"
                title={isOps && scope === 'mine' ? 'You do not own any sequences' : 'No sequences yet'}
                body={isOps && scope === 'mine' ? 'Switch to All to see the whole desk, or start a sequence you own.' : 'Start from a template below, or build your own to begin reaching people.'}
                cta={<DSm2.Button variant="primary" size="sm" icon="add" onClick={() => navigate('sequences/new')}>New sequence</DSm2.Button>}
                style={{ margin: '8px 0' }}
              />
            ) : q ? (
              <DSm2.EmptyState
                icon="search_off"
                title="No sequences match"
                body={'Nothing matches “' + q + '”.'}
                cta={<DSm2.Button variant="secondary" size="sm" icon="close" onClick={() => setSearch('')}>Clear search</DSm2.Button>}
                style={{ margin: '8px 0' }}
              />
            ) : (
              <DSm2.EmptyState
                icon={(filterEmpty[filter] || filterEmpty.all).icon}
                title={(filterEmpty[filter] || filterEmpty.all).title}
                body={(filterEmpty[filter] || filterEmpty.all).body}
                cta={<DSm2.Button variant="primary" size="sm" icon="add" onClick={() => navigate('sequences/new')}>New sequence</DSm2.Button>}
                style={{ margin: '8px 0' }}
              />
            )
          ) : visible.length > SEQ_TABLE_AT ? (
            <window.ConfigurableTable
              listKey="sequences"
              registry={seqCols}
              rows={visible}
              pageSize={SEQ_PAGE}
              onRowClick={(s) => navigate('sequence/' + s.id)}
              rowActions={isOps ? (s) => (
                <DSm2.Button variant="secondary" size="sm" icon="swap_horiz" onClick={() => setReassign(s)}>Reassign</DSm2.Button>
              ) : undefined}
            />
          ) : (
          <div className="e8-seq-cards">
            {visible.slice(0, shown).map((s) => {
              const running = states[s.id] === 'running';
              const rr = parseInt(String(s.replyRate).replace(/\D/g, ''), 10) || 0;
              const tone = running ? 'success' : 'neutral';
              const nNew = unhandledOf(s.id);
              const goDetail = () => navigate('sequence/' + s.id);
              return (
                <div key={s.id} className="e8-card e8-seq-card">
                  <div className="e8-seq-head">
                    <div className="e8-seq-titlewrap">
                      <div className="e8-seq-titlerow">
                        <a className="e8-link e8-seq-name" role="link" tabIndex={0} onClick={goDetail}
                          onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); goDetail(); } }}>{s.name}</a>
                        <DSm2.Badge tone={tone} dot>{running ? 'Running' : 'Paused'}</DSm2.Badge>
                        {nNew > 0 ? <DSm2.Badge tone="accent">{nNew} new</DSm2.Badge> : null}
                      </div>
                      <div className="e8-seq-sub">{s.audience} · {s.steps.length} steps</div>
                    </div>
                    <div className="e8-seq-metrics">
                      {[['Enrolled', s.enrolled], ['Active', s.active], ['Replies', s.replied], ['Meetings', s.meetings]].map(([k, v]) => (
                        <span key={k} className="e8-seq-metric">
                          <span className="e8-seq-metric-lbl">{k}</span>
                          <span className="tnum e8-seq-metric-val">{v}</span>
                        </span>
                      ))}
                      <span className="e8-seq-spark" title="Weekly replies">{C ? <C.Sparkline data={s.spark} tone={running ? 'up' : 'flat'} w={72} h={28} /> : null}</span>
                      <span className="e8-seq-owner"><ActorChip name={s.owner} /></span>
                    </div>
                  </div>

                  {/* Step flow + reply-rate meter */}
                  <div className="e8-seq-body">
                    <div className="e8-seq-flow">
                      {s.steps.map((st, i) => (
                        <React.Fragment key={i}>
                          {i > 0 ? <span className="material-symbols-outlined e8-seq-arrow">arrow_forward</span> : null}
                          <span className="e8-seq-step" title={st.day + ' · ' + st.label}>
                            <ChannelDot ch={st.ch} size={15} />
                            <span style={{ minWidth: 0 }}>
                              <span className="e8-seq-step-day">{st.day}</span>
                              <span className="e8-seq-step-label">{st.label}</span>
                            </span>
                          </span>
                        </React.Fragment>
                      ))}
                    </div>
                    <span className="e8-seq-meter">
                      <span className="e8-seq-meter-head"><span>Reply rate</span><span className="tnum">{rr}%</span></span>
                      <span className="e8-seq-meter-track"><span className="e8-seq-meter-fill" style={{ width: rr + '%' }}></span></span>
                    </span>
                  </div>

                  <div className="e8-seq-foot">
                    <DSm2.ProvenanceBadge kind="drafted" />
                    <span className="e8-seq-note">{s.note}</span>
                    <span className="e8-seq-foot-actions">
                      <DSm2.Button variant="ghost" size="sm" icon={running ? 'pause' : 'play_arrow'} onClick={() => toggle(s)}>{running ? 'Pause' : 'Resume'}</DSm2.Button>
                      <DSm2.Button variant="ghost" size="sm" icon="forum" onClick={() => openReplies(s.id)}>{nNew > 0 ? 'View replies · ' + nNew : 'View replies'}</DSm2.Button>
                      <DSm2.Button variant="ghost" size="sm" icon="edit" onClick={() => navigate('sequences/' + s.id + '/edit')}>Edit</DSm2.Button>
                      <DSm2.MenuButton
                        items={[
                          { icon: 'content_copy', label: 'Duplicate', onClick: () => duplicate(s) },
                          { icon: 'group', label: 'View enrolled', onClick: () => navigate('sequence/' + s.id + '/enrolled') },
                        ].concat(isOps ? [{ icon: 'swap_horiz', label: 'Reassign owner', onClick: () => setReassign(s) }] : []).concat([
                          { separator: true },
                          { icon: 'archive', label: 'Archive sequence', danger: true, onClick: () => archive(s) },
                        ])}
                      />
                    </span>
                  </div>
                </div>
              );
            })}
          </div>
          )}

          {visible.length > SEQ_TABLE_AT || visible.length <= shown ? null : (
            <div className="e8-showmore">
              <button type="button" className="e8-showmore-btn" onClick={() => setShown(shown + SEQ_PAGE)}>Show more<span className="e8-showmore-n">{(visible.length - shown).toLocaleString()} more</span></button>
            </div>
          )}

          {/* Templates gallery */}
          <div style={{ marginTop: 26 }}>
            <div style={{ display: 'flex', alignItems: 'baseline', gap: 8, marginBottom: 12 }}>
              <h2 style={{ fontSize: 'var(--ui-text-lg)' }}>Start a new sequence</h2>
              <span style={{ fontSize: 'var(--ui-text-sm)', color: 'var(--ui-text-tertiary)' }}>from a template, or build your own</span>
            </div>
            <div className="e8-seq-tmpls">
              <button type="button" className="e8-seq-tmpl e8-seq-tmpl-blank" onClick={() => navigate('sequences/new')}>
                <span className="e8-seq-tmpl-icon" style={{ background: 'var(--ui-accent-tint)', color: 'var(--ui-accent)' }}><span className="material-symbols-outlined">add</span></span>
                <span style={{ display: 'block', fontSize: 'var(--ui-text-base)', fontWeight: 500, marginTop: 8 }}>Blank sequence</span>
                <span style={{ display: 'block', fontSize: 'var(--ui-text-xs)', color: 'var(--ui-text-tertiary)' }}>Start from scratch in the builder.</span>
              </button>
              {D.sequenceTemplates.map((t) => (
                <button key={t.id} type="button" className="e8-seq-tmpl" onClick={() => { showToast('Template “' + t.name + '” opened in the builder'); navigate('sequences/new'); }}>
                  <span className="e8-seq-tmpl-icon"><span className="material-symbols-outlined">{t.icon}</span></span>
                  <span style={{ display: 'block', fontSize: 'var(--ui-text-base)', fontWeight: 500, marginTop: 8 }}>{t.name}</span>
                  <span style={{ display: 'block', fontSize: 'var(--ui-text-xs)', color: 'var(--ui-text-tertiary)', lineHeight: 1.4 }}>{t.desc}</span>
                  <span style={{ display: 'inline-flex', gap: 4, marginTop: 8 }}>
                    {t.chans.map((ch, i) => <ChannelDot key={i} ch={ch} size={15} />)}
                  </span>
                </button>
              ))}
            </div>
          </div>

          <p style={{ fontSize: 'var(--ui-text-sm)', color: 'var(--ui-text-tertiary)', margin: '20px 2px 0', maxWidth: 620 }}>
            Sends respect each person’s quiet hours and channel preferences. Every sent step shows up in their thread in your inbox.
          </p>
          </React.Fragment>
          )}
        </div>
      </div>
      {enrolling ? <SeqEnrollDialog onClose={() => setEnrolling(false)} showToast={showToast} /> : null}
      {reassign ? <SeqReassignDialog seq={reassign} onClose={() => setReassign(null)} showToast={showToast} /> : null}
    </React.Fragment>
  );
}

/* Enroll people - pick a source (candidates / contacts / consultants / a saved list), then land
   on that table to select the exact people and bulk-add them to a sequence. A prototype hand-off:
   it deep-links with a bulk-add framing toast rather than running the enrollment itself. */
const SEQ_ENROLL_SOURCES = [
  { id: 'candidates', icon: 'badge', label: 'Candidates', sub: 'Your candidate pool', route: 'candidates' },
  { id: 'contacts', icon: 'contacts', label: 'Client contacts', sub: 'Sponsors and hiring managers', route: 'contacts' },
  { id: 'consultants', icon: 'engineering', label: 'Consultants', sub: 'Bench and rolling-off consultants', route: 'consultants' },
  { id: 'list', icon: 'bookmark', label: 'A saved list', sub: 'Start from a saved segment', route: 'candidates' },
];
function SeqEnrollDialog({ onClose, showToast }) {
  const [source, setSource] = React.useState('candidates');
  const go = () => {
    const s = SEQ_ENROLL_SOURCES.find((x) => x.id === source) || SEQ_ENROLL_SOURCES[0];
    onClose();
    showToast('Select people in ' + s.label + ', then bulk-add them to a sequence');
    navigate(s.route);
  };
  return (
    <DSm2.Modal title="Enroll people" ariaLabel="Enroll people" closeTitle="Cancel" onClose={onClose}>
        <div style={{ padding: '14px 16px', display: 'grid', gap: 12 }}>
          <div style={{ fontSize: 'var(--ui-text-sm)', color: 'var(--ui-text-secondary)', lineHeight: 1.5 }}>
            Choose where to add people from. You pick the exact people on the next screen and bulk-add them to a sequence.
          </div>
          <div className="e8-seq-enroll-opts" role="radiogroup" aria-label="Source">
            {SEQ_ENROLL_SOURCES.map((o) => {
              const on = source === o.id;
              return (
                <div key={o.id} className={'e8-seq-enroll-opt' + (on ? ' on' : '')} role="radio" aria-checked={on} tabIndex={0}
                  onClick={() => setSource(o.id)}
                  onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); setSource(o.id); } }}>
                  <span className="e8-seq-enroll-ic"><span className="material-symbols-outlined">{o.icon}</span></span>
                  <span style={{ minWidth: 0 }}>
                    <span className="e8-seq-enroll-lbl">{o.label}</span>
                    <span className="e8-seq-enroll-sub">{o.sub}</span>
                  </span>
                  <span className="material-symbols-outlined e8-seq-enroll-chk" aria-hidden="true">{on ? 'radio_button_checked' : 'radio_button_unchecked'}</span>
                </div>
              );
            })}
          </div>
          <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginTop: 2 }}>
            <span style={{ flex: 1 }}></span>
            <DSm2.Button variant="ghost" size="sm" onClick={onClose}>Cancel</DSm2.Button>
            <DSm2.Button variant="primary" size="sm" icon="arrow_forward" onClick={go}>Continue</DSm2.Button>
          </div>
        </div>
    </DSm2.Modal>
  );
}

/* ---------- All-sequences Replies inbox ----------
   Every sequence reply, AI-tagged with an intent + one-line summary, filterable by intent and
   by sequence, with per-row actions. Reply mutations live in local state - a reload restores the
   seed (`D.sequenceReplies`). */
const SEQR_INTENT_TABS = [
  { id: 'all', label: 'All' },
  { id: 'needs', label: 'Needs action' },
  { id: 'interested', label: 'Interested' },
  { id: 'question', label: 'Questions' },
  { id: 'opt_out', label: 'Opt-outs' },
];
function seqrMatch(f, r) {
  if (f === 'all') return true;
  if (f === 'needs') return !r.handled;
  return r.intent === f;
}
const SEQR_EMPTY = {
  all: { title: 'No replies yet', body: 'Replies to your sequences land here, tagged by intent.' },
  needs: { title: 'No replies need action - you’re all caught up', body: 'Handled replies drop out of this view.' },
  interested: { title: 'No interested replies here', body: 'Positive replies will collect in this view.' },
  question: { title: 'No open questions', body: 'Questions from prospects will show up here.' },
  opt_out: { title: 'No opt-outs', body: 'People who asked to stop will be listed here.' },
};
/* Shared reply-row action handlers - one place for the reply mutations + toasts so the
   all-sequences Replies inbox and the per-sequence detail Replies tab behave identically. */
function makeSeqReplyHandlers(setReplies, showToast) {
  const setReply = (id, patch) => setReplies((rs) => rs.map((r) => (r.id === id ? { ...r, ...patch } : r)));
  return {
    draftReply: (r) => { navigate(r.convId ? 'inbox/' + r.convId : 'inbox'); showToast('AI drafted a reply for ' + r.person.name); },
    bookMeeting: (r) => { setReply(r.id, { meetingBooked: true, handled: true }); showToast('Meeting invite sent to ' + r.person.name); },
    advance: (r) => { setReply(r.id, { handled: true }); showToast('Moved ' + r.person.name + ' to a submission'); },
    markHandled: (r) => { setReply(r.id, { handled: true }); showToast('Marked ' + r.person.name + ' handled', 'Undo', () => setReply(r.id, { handled: false })); },
    optOut: (r) => { setReply(r.id, { intent: 'opt_out', handled: true }); showToast(r.person.name + ' opted out - removed from the sequence'); },
  };
}

/* Shared reply row (`.e8-seqr-row`) - a flat, hairline-separated reply with the AI intent chip,
   channel + step, message text, an AI-summary provenance line, and the per-row actions. Used by
   both the all-sequences Replies inbox and the SequenceDetail Replies tab. */
function SeqReplyRow({ r, h }) {
  const im = window.seqIntentMeta(r.intent);
  return (
    <div className={'e8-seqr-row' + (r.handled ? ' is-handled' : ' is-unhandled')}>
      <DSm2.Avatar name={r.person.name} />
      <div style={{ flex: 1, minWidth: 0, display: 'grid', gap: 6 }}>
        <div className="e8-seqr-head">
          <span className="e8-seqr-name">{r.person.name}</span>
          <span className="e8-seqr-sub">{r.person.sub}</span>
          <span className="e8-seqr-chip" style={{ color: 'var(--ui-' + im.tone + '-text)', background: 'var(--ui-' + im.tone + '-tint)' }}>
            <span className="material-symbols-outlined" aria-hidden="true">{im.icon}</span>{im.label}
          </span>
          <span className="e8-seqr-when tnum">{r.when}</span>
        </div>
        <div className="e8-seqr-meta">
          <ChannelDot ch={r.ch} size={14} />
          <span>{r.stepDay} · {r.seqName}</span>
        </div>
        <div className="e8-seqr-text">{r.text}</div>
        <div className="e8-seqr-ai">
          <DSm2.ProvenanceBadge kind="drafted" />
          <span className="e8-seqr-ai-text">{r.aiSummary}</span>
          {r.meetingBooked ? <DSm2.Badge tone="success" dot>Meeting booked</DSm2.Badge> : null}
        </div>
        <div className="e8-seqr-actions">
          <DSm2.Button variant="ghost" size="sm" icon="edit_note" onClick={() => h.draftReply(r)}>Draft reply</DSm2.Button>
          <DSm2.Button variant="ghost" size="sm" icon="event" onClick={() => h.bookMeeting(r)}>Book meeting</DSm2.Button>
          <DSm2.Button variant="ghost" size="sm" icon="arrow_forward" onClick={() => h.advance(r)}>Advance</DSm2.Button>
          <DSm2.MenuButton
            items={[
              { icon: 'check_circle', label: 'Mark handled', onClick: () => h.markHandled(r) },
              { separator: true },
              { icon: 'block', label: 'Opt out', danger: true, onClick: () => h.optOut(r) },
            ]}
          />
        </div>
      </div>
    </div>
  );
}
/* R103 Task 4: a searchable sequence typeahead for the Replies inbox filter. At scale the native
   per-sequence <select> would build one <option> per sequence (50k DOM nodes that froze the browser);
   this renders only the current value plus up to 50 live matches, so it never materializes the full
   desk. Scoped to `seqs` (the persona's book), keyboard-navigable, Esc / outside-click closes. */
function SeqFilterTypeahead({ seqs, value, onChange }) {
  const list = seqs || [];
  const [open, setOpen] = React.useState(false);
  const [q, setQ] = React.useState('');
  const [active, setActive] = React.useState(0);
  const wrapRef = React.useRef(null);
  const selected = value === 'all' ? null : list.find((s) => s.id === value);
  const CAP = 50;
  const ql = q.trim().toLowerCase();
  const matches = [];
  let more = false;
  for (let i = 0; i < list.length; i++) {
    const s = list[i];
    if (ql && (s.name + ' ' + (s.audience || '')).toLowerCase().indexOf(ql) === -1) continue;
    if (matches.length >= CAP) { more = true; break; }
    matches.push(s);
  }
  const showAll = !ql || 'all sequences'.indexOf(ql) !== -1;
  const opts = (showAll ? [{ id: 'all', name: 'All sequences', sub: list.length.toLocaleString() + (list.length === 1 ? ' sequence' : ' sequences') }] : []).concat(matches);

  React.useEffect(() => {
    if (!open) return undefined;
    const onDoc = (e) => { if (wrapRef.current && !wrapRef.current.contains(e.target)) { setOpen(false); setQ(''); } };
    document.addEventListener('mousedown', onDoc);
    return () => document.removeEventListener('mousedown', onDoc);
  }, [open]);
  React.useEffect(() => { setActive(0); }, [q, open]);

  const pick = (id) => { onChange(id); setOpen(false); setQ(''); };
  const onKey = (e) => {
    if (e.key === 'ArrowDown') { e.preventDefault(); setOpen(true); setActive((a) => Math.min(a + 1, opts.length - 1)); }
    else if (e.key === 'ArrowUp') { e.preventDefault(); setActive((a) => Math.max(a - 1, 0)); }
    else if (e.key === 'Enter') { e.preventDefault(); if (open && opts[active]) pick(opts[active].id); else setOpen(true); }
    else if (e.key === 'Escape') { if (open) { e.preventDefault(); e.stopPropagation(); setOpen(false); setQ(''); } }
  };
  return (
    <div className="e8-seqta" ref={wrapRef}>
      <div className={'e8-input-wrap e8-seqta-control' + (open ? ' is-open' : '')}>
        <span className="material-symbols-outlined" aria-hidden="true">filter_list</span>
        <input
          type="text"
          className="e8-seqta-input"
          role="combobox"
          aria-expanded={open}
          aria-label="Filter by sequence"
          value={open ? q : (selected ? selected.name : 'All sequences')}
          placeholder="All sequences"
          onChange={(e) => { setQ(e.target.value); setOpen(true); }}
          onFocus={() => setOpen(true)}
          onKeyDown={onKey}
        />
        <span className="material-symbols-outlined e8-seqta-caret" aria-hidden="true">expand_more</span>
      </div>
      {open ? (
        <div className="e8-seqta-menu" role="listbox" aria-label="Sequences">
          {opts.length === 0 ? (
            <div className="e8-seqta-empty">No sequences match</div>
          ) : opts.map((o, i) => (
            <div
              key={o.id}
              role="option"
              aria-selected={value === o.id}
              tabIndex={0}
              className={'e8-seqta-opt' + (i === active ? ' is-active' : '') + (value === o.id ? ' is-sel' : '')}
              onMouseEnter={() => setActive(i)}
              onMouseDown={(e) => { e.preventDefault(); pick(o.id); }}
              onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); pick(o.id); } }}
            >
              <span className="e8-seqta-opt-name">{o.name}</span>
              {o.sub || o.audience ? <span className="e8-seqta-opt-sub">{o.sub || o.audience}</span> : null}
            </div>
          ))}
          {more ? <div className="e8-seqta-more">Showing the first {CAP} - keep typing to narrow.</div> : null}
        </div>
      ) : null}
    </div>
  );
}
function SequenceRepliesView({ replies, setReplies, seqs, seqFilter, setSeqFilter, showToast }) {
  const D = window.E8DATA;
  // The per-sequence filter only offers the scoped sequences, so a recruiter's inbox never lists
  // (or leaks the names of) another owner's sequences. Falls back to the full set if unscoped.
  const seqList = seqs || D.sequences;
  const [intentF, setIntentF] = React.useState('all');
  const SEQR_PAGE = 50;                          // replies revealed per "Show more" click
  const [shown, setShown] = React.useState(SEQR_PAGE);
  const h = makeSeqReplyHandlers(setReplies, showToast);

  // The scoped replies pool is bounded (~1.2k even at 50k sequences), but every pass over it is still
  // memoized so an intent-tab click or a paging click doesn't re-filter and re-reduce the whole pool.
  const { kpis, counts, sorted } = React.useMemo(() => {
    const scoped = replies.filter((r) => seqFilter === 'all' || r.seqId === seqFilter);
    const k = {
      needs: scoped.filter((r) => !r.handled).length,
      interested: scoped.filter((r) => r.intent === 'interested').length,
      meetings: scoped.filter((r) => r.meetingBooked).length,
      optouts: scoped.filter((r) => r.intent === 'opt_out').length,
    };
    const c = Object.fromEntries(SEQR_INTENT_TABS.map((t) => [t.id, scoped.filter((r) => seqrMatch(t.id, r)).length]));
    const s = scoped.filter((r) => seqrMatch(intentF, r)).slice().sort((a, b) => (a.handled === b.handled ? 0 : a.handled ? 1 : -1));
    return { kpis: k, counts: c, sorted: s };
  }, [replies, seqFilter, intentF]);

  // Snap the reply window back to the first page whenever the result set changes underneath it.
  React.useEffect(() => { setShown(SEQR_PAGE); }, [intentF, seqFilter]);
  const paged = sorted.slice(0, shown);

  const kpiTiles = [
    { label: 'Needs action', value: kpis.needs, icon: 'inbox' },
    { label: 'Interested', value: kpis.interested, icon: 'thumb_up' },
    { label: 'Meetings booked', value: kpis.meetings, icon: 'event_available' },
    { label: 'Opt-outs', value: kpis.optouts, icon: 'block' },
  ];

  return (
    <React.Fragment>
      {/* Replies mini KPI band */}
      <div className="e8-seqr-kpis">
        {kpiTiles.map((k) => (
          <div key={k.label} className="e8-card e8-seqr-kpi">
            <span className="e8-seqr-kpi-icon"><span className="material-symbols-outlined">{k.icon}</span></span>
            <span className="tnum e8-seqr-kpi-val">{k.value}</span>
            <span className="e8-seqr-kpi-lbl">{k.label}</span>
          </div>
        ))}
      </div>

      {/* Intent filter + per-sequence typeahead */}
      <div className="e8-seqr-filters">
        <DSm2.SubTabs
          items={SEQR_INTENT_TABS.map((t) => ({ id: t.id, label: t.label, count: counts[t.id] }))}
          active={intentF}
          onChange={setIntentF}
        />
        <div className="e8-seqr-seqsel">
          <SeqFilterTypeahead seqs={seqList} value={seqFilter} onChange={setSeqFilter} />
        </div>
      </div>

      {sorted.length === 0 ? (
        <DSm2.EmptyState icon="mark_email_read" title={(SEQR_EMPTY[intentF] || SEQR_EMPTY.all).title} body={(SEQR_EMPTY[intentF] || SEQR_EMPTY.all).body} style={{ margin: '8px 0' }} />
      ) : (
        <React.Fragment>
          <div className="e8-seqr-list e8-card" style={{ padding: 0 }}>
            {paged.map((r) => <SeqReplyRow key={r.id} r={r} h={h} />)}
          </div>
          {sorted.length > paged.length ? (
            <div className="e8-showmore">
              <button type="button" className="e8-showmore-btn" onClick={() => setShown(shown + SEQR_PAGE)}>Show more<span className="e8-showmore-n">{(sorted.length - paged.length).toLocaleString()} more</span></button>
            </div>
          ) : null}
        </React.Fragment>
      )}
    </React.Fragment>
  );
}

/* R103 Task 4: ops-only reassignment of a sequence's owner. Owner options are the active reps (the
   departed Chris Vale is excluded) plus the CSM 'Dana Whitfield' (owner name only, ownerId null),
   defaulting to the sequence's current owner. On confirm it snapshots the prior {owner, ownerId},
   writes the new pair through the replayable plain-array store op (E8Store.set persists across reload),
   logs an audit entry, and raises a single Undo toast that restores the snapshot. Gated by every caller
   to the ops persona; Esc, outside-click and the close button all cancel. Reuses the enroll-option
   radio rows for the picker and the .e8-cown-dlg shell from the R101 consultant reassign. */
function SeqReassignDialog({ seq, onClose, showToast }) {
  const D = window.E8DATA;
  const ReassignDialog = window.E8ReassignDialog;
  const options = (D.reps || [])
    .filter((r) => !r.departed)
    .map((r) => ({ key: r.id, name: r.name, ownerId: r.id, sub: r.role }))
    .concat([{ key: 'csm-dana', name: 'Dana Whitfield', ownerId: null, sub: 'Sales' }]);
  const currentKey = (function () {
    if (seq.ownerId) { const m = options.find((o) => o.ownerId === seq.ownerId); if (m) return m.key; }
    const byName = options.find((o) => o.name === seq.owner);
    return byName ? byName.key : (options[0] ? options[0].key : '');
  })();
  const doReassign = (opt) => {
    if (!opt) return;
    if (opt.name === seq.owner && (opt.ownerId || null) === (seq.ownerId || null)) { onClose(); return; }
    const prev = { owner: seq.owner, ownerId: seq.ownerId != null ? seq.ownerId : null };
    window.E8Store.set('sequences', seq.id, { owner: opt.name, ownerId: opt.ownerId });
    if (window.E8Audit) window.E8Audit.log({ agent: 'You', prov: 'human', action: 'Reassigned sequence ' + seq.name + ' to ' + opt.name, target: seq.name, route: 'sequence/' + seq.id });
    onClose();
    showToast('Reassigned ' + seq.name + ' to ' + opt.name, 'Undo', () => {
      window.E8Store.set('sequences', seq.id, { owner: prev.owner, ownerId: prev.ownerId });
    });
  };
  return (
    <ReassignDialog
      ariaLabel="Reassign sequence owner"
      subjectLabel="Sequence"
      subject={
        <div className="e8-cown-dlg-row">
          <span className="material-symbols-outlined" aria-hidden="true" style={{ color: 'var(--ui-text-tertiary)' }}>campaign</span>
          <span style={{ minWidth: 0, flex: 1 }}>
            <span style={{ display: 'block', fontWeight: 500, fontSize: 'var(--ui-text-base)' }}>{seq.name}</span>
            <span style={{ display: 'block', fontSize: 'var(--ui-text-xs)', color: 'var(--ui-text-tertiary)' }}>{seq.audience} · currently {seq.owner}</span>
          </span>
        </div>
      }
      owners={options}
      currentKey={currentKey}
      hint="Reassigning moves the sequence into the new owner's book."
      onReassign={doReassign}
      onClose={onClose}
    />
  );
}

/* ============================== SEQUENCE DETAIL ============================== */
/* A real per-sequence page: a step funnel (sent -> delivered -> opened -> replied per step),
   the enrolled people with their current step + status, and this sequence's replies (reusing the
   shared reply row). Reached at #/sequence/:id (singular, so it never clashes with the builder's
   #/sequences/:id/edit). Local state only - a reload restores the seed. */
const SEQD_STATUS = {
  active: { tone: 'accent', label: 'Active' },
  replied: { tone: 'success', label: 'Replied' },
  completed: { tone: 'neutral', label: 'Completed' },
  paused: { tone: 'warning', label: 'Paused' },
  opted_out: { tone: 'danger', label: 'Opted out' },
};
function SequenceDetail({ id, sub }) {
  const D = window.E8DATA;
  const { showToast } = React.useContext(E8Ctx);
  const seq = D.sequences.find((s) => s.id === id) || null;
  const [running, setRunning] = React.useState(seq ? seq.state === 'running' : false);
  const [tab, setTab] = React.useState(['enrolled', 'replies'].includes(sub) ? sub : 'overview');
  const [replies, setReplies] = React.useState(() => (seq ? window.seqRepliesFor(seq) : []).map((r) => ({ ...r })));
  const [reassign, setReassign] = React.useState(false); // R103 Task 4: ops reassign dialog open
  const SEQD_REP_PAGE = 50;                              // R103 Task 4: replies revealed per "Show more"
  const [repShown, setRepShown] = React.useState(SEQD_REP_PAGE);
  // R103: memoize the (possibly synthesized) enrollees so they are not rebuilt every render.
  // Kept above the not-found guard so the hook order is stable across valid/invalid ids.
  const enrollees = React.useMemo(() => (seq ? window.seqEnrolleesFor(seq) : []), [seq && seq.id]);
  // R103 Task 4: re-render when the owner is reassigned (an in-place E8Store.set) or the persona
  // switches, so the header owner chip and the ops-only reassign affordance stay live.
  const [, force] = React.useReducer((x) => x + 1, 0);
  React.useEffect(() => {
    if (!window.E8Events) return undefined;
    return window.E8Events.subscribe(['persona:changed', 'store:changed'], force);
  }, []);

  if (!seq) {
    return (
      <React.Fragment>
        <Topbar crumbs={[{ label: 'Sequences', to: 'sequences' }, { label: 'Not found' }]} />
        <div className="e8-content">
          <div className="e8-page" style={{ maxWidth: 1000 }}>
            <DSm2.EmptyState
              icon="link_off"
              title="Sequence not found"
              body="This sequence does not exist, or the link is out of date."
              cta={<DSm2.Button variant="primary" icon="arrow_back" onClick={() => navigate('sequences')}>Back to sequences</DSm2.Button>}
            />
          </div>
        </div>
      </React.Fragment>
    );
  }

  const h = makeSeqReplyHandlers(setReplies, showToast);
  const persona = window.e8ActivePersona ? window.e8ActivePersona() : null;
  const isOps = !!persona && persona.role === 'ops';
  const unhandled = replies.filter((r) => !r.handled).length;
  const rr = parseInt(String(seq.replyRate).replace(/\D/g, ''), 10) || 0;

  const toggle = () => {
    const next = !running;
    setRunning(next);
    showToast(seq.name + (next ? ' resumed' : ' paused'), 'Undo', () => setRunning(running));
  };
  const openEnrollee = (e) => {
    if (e.kind === 'candidate') navigate('candidate/' + e.id);
    else if (e.kind === 'contact') navigate('contact/' + e.id);
    else if (e.kind === 'consultant') navigate('consultant/' + e.id);
    else navigate('inbox');
  };

  const metrics = [
    { k: 'Enrolled', v: seq.enrolled },
    { k: 'Active', v: seq.active },
    { k: 'Replied', v: seq.replied },
    { k: 'Reply rate', v: rr + '%' },
    { k: 'Meetings', v: seq.meetings },
  ];

  const stepStats = seq.stepStats || [];

  const enrollCols = [
    { key: 'name', label: 'Person', render: (e) => (
      <span style={{ display: 'inline-flex', alignItems: 'center', gap: 9, minWidth: 0 }}>
        <DSm2.Avatar name={e.name} size="md" />
        <span style={{ minWidth: 0 }}>
          <span style={{ display: 'block', fontSize: 'var(--ui-text-base)', fontWeight: 500, color: 'var(--ui-text)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{e.name}</span>
          <span style={{ display: 'block', fontSize: 'var(--ui-text-xs)', color: 'var(--ui-text-tertiary)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{e.sub}</span>
        </span>
      </span>
    ) },
    { key: 'step', label: 'Current step', render: (e) => (
      <span style={{ display: 'inline-flex', alignItems: 'center', gap: 7 }}>
        <ChannelDot ch={e.ch} size={14} />
        <span className="tnum" style={{ fontSize: 'var(--ui-text-sm)', color: 'var(--ui-text-secondary)' }}>{e.currentStep}</span>
      </span>
    ) },
    { key: 'status', label: 'Status', sortable: true, sortValue: (e) => (SEQD_STATUS[e.status] || {}).label, render: (e) => {
      const st = SEQD_STATUS[e.status] || { tone: 'neutral', label: e.status };
      return <DSm2.Badge tone={st.tone} dot>{st.label}</DSm2.Badge>;
    } },
    { key: 'last', label: 'Last activity', render: (e) => <span className="tnum" style={{ fontSize: 'var(--ui-text-sm)', color: 'var(--ui-text-secondary)' }}>{e.lastActivity}</span> },
    { key: 'ch', label: 'Channel', render: (e) => <span style={{ fontSize: 'var(--ui-text-sm)', color: 'var(--ui-text-tertiary)' }}>{(CHANNELS[e.ch] || CHANNELS.email).label}</span> },
  ];

  const tabs = [
    { id: 'overview', label: 'Overview' },
    { id: 'enrolled', label: 'Enrolled', count: enrollees.length },
    { id: 'replies', label: 'Replies', count: unhandled },
  ];

  return (
    <React.Fragment>
      <Topbar
        crumbs={[{ label: 'Sequences', to: 'sequences' }, { label: seq.name }]}
        actions={
          <React.Fragment>
            <DSm2.Button variant="ghost" size="sm" icon={running ? 'pause' : 'play_arrow'} onClick={toggle}>{running ? 'Pause' : 'Resume'}</DSm2.Button>
            <DSm2.Button variant="secondary" size="sm" icon="person_add" onClick={() => showToast('Pick people from Candidates or Contacts - bulk-add works from any table')}>Enroll people</DSm2.Button>
            <DSm2.Button variant="primary" size="sm" icon="edit" onClick={() => navigate('sequences/' + seq.id + '/edit')}>Edit</DSm2.Button>
          </React.Fragment>
        }
      />
      <div className="e8-content">
        <div className="e8-page" style={{ maxWidth: 1000 }}>
          <div className="e8-pagehead">
            <div>
              <div style={{ display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap' }}>
                <h1 className="e8-h1">{seq.name}</h1>
                <DSm2.Badge tone={running ? 'success' : 'neutral'} dot>{running ? 'Running' : 'Paused'}</DSm2.Badge>
                {unhandled > 0 ? <DSm2.Badge tone="accent">{unhandled} new</DSm2.Badge> : null}
              </div>
              <div className="e8-pagehead-sub" style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap', maxWidth: 660 }}>
                <span>{seq.audience}</span>
                <span style={{ color: 'var(--ui-text-tertiary)' }}>·</span>
                <span>{seq.steps.length} steps</span>
                <span style={{ color: 'var(--ui-text-tertiary)' }}>·</span>
                <span style={{ display: 'inline-flex', alignItems: 'center', gap: 6 }}>Owner <ActorChip name={seq.owner} />
                  {isOps ? <DSm2.Button variant="ghost" size="sm" icon="swap_horiz" onClick={() => setReassign(true)}>Reassign</DSm2.Button> : null}
                </span>
              </div>
            </div>
          </div>

          {/* Metric strip */}
          <div className="e8-seqd-metrics e8-card">
            {metrics.map((m) => (
              <span key={m.k} className="e8-seqd-metric">
                <span className="e8-seqd-metric-lbl">{m.k}</span>
                <span className="tnum e8-seqd-metric-val">{m.v}</span>
              </span>
            ))}
          </div>

          <div style={{ margin: '16px 0 14px' }}>
            <DSm2.Tabs items={tabs} active={tab} onChange={setTab} />
          </div>

          {tab === 'overview' ? (
            <React.Fragment>
              <div className="e8-seqd-steps">
                {seq.steps.map((st, i) => {
                  const fs = stepStats[i] || { sent: 0, delivered: 0, opened: 0, replied: 0 };
                  const denom = Math.max(1, fs.sent);
                  const rows = [
                    { lbl: 'Sent', v: fs.sent },
                    { lbl: 'Delivered', v: fs.delivered },
                    { lbl: 'Opened', v: fs.opened },
                    { lbl: 'Replied', v: fs.replied },
                  ];
                  const stepRr = Math.round((fs.replied / denom) * 100);
                  return (
                    <div key={i} className="e8-seqd-step e8-card">
                      <div className="e8-seqd-step-head">
                        <ChannelDot ch={st.ch} size={16} />
                        <span className="e8-seqd-step-day tnum">{st.day}</span>
                        <span className="e8-seqd-step-label">{st.label}</span>
                        <span className="e8-seqd-step-rr">
                          <span className="e8-seqd-step-rr-lbl">Reply rate</span>
                          <span className="tnum e8-seqd-step-rr-val">{stepRr}%</span>
                        </span>
                      </div>
                      <div className="e8-seqd-funnel">
                        {rows.map((f) => (
                          <div key={f.lbl} className="e8-seqd-fun">
                            <span className="e8-seqd-fun-lbl">{f.lbl}</span>
                            <span className="e8-seqd-fun-track">
                              <span className="e8-seqd-fun-fill" style={{ width: Math.round((f.v / denom) * 100) + '%' }}></span>
                            </span>
                            <span className="tnum e8-seqd-fun-val">{f.v}</span>
                          </div>
                        ))}
                      </div>
                    </div>
                  );
                })}
              </div>
              <div className="e8-seqd-note e8-card">
                <DSm2.ProvenanceBadge kind="drafted" />
                <span className="e8-seqd-note-text">{seq.note}</span>
              </div>
            </React.Fragment>
          ) : tab === 'enrolled' ? (
            enrollees.length === 0 ? (
              <DSm2.EmptyState icon="group_off" title="No one enrolled yet" body="Enroll people from Candidates or Contacts to start this sequence." style={{ margin: '8px 0' }} />
            ) : (
              // R103 Task 4: a paged table (50/page) so a scale sequence with hundreds enrolled
              // never mounts every row at once.
              <window.ConfigurableTable
                listKey="seq-enrolled"
                registry={enrollCols}
                rows={enrollees}
                pageSize={50}
                onRowClick={openEnrollee}
              />
            )
          ) : (
            replies.length === 0 ? (
              <DSm2.EmptyState icon="mark_email_read" title="No replies yet" body="Replies to this sequence land here, tagged by intent." style={{ margin: '8px 0' }} />
            ) : (() => {
              // R103 Task 4: page the replies (50/click) so a busy sequence stays fast.
              const sorted = replies.slice().sort((a, b) => (a.handled === b.handled ? 0 : a.handled ? 1 : -1));
              const paged = sorted.slice(0, repShown);
              return (
                <React.Fragment>
                  <div className="e8-seqr-list e8-card" style={{ padding: 0 }}>
                    {paged.map((r) => <SeqReplyRow key={r.id} r={r} h={h} />)}
                  </div>
                  {sorted.length > paged.length ? (
                    <div className="e8-showmore">
                      <button type="button" className="e8-showmore-btn" onClick={() => setRepShown(repShown + SEQD_REP_PAGE)}>Show more<span className="e8-showmore-n">{(sorted.length - paged.length).toLocaleString()} more</span></button>
                    </div>
                  ) : null}
                </React.Fragment>
              );
            })()
          )}
        </div>
      </div>
      {reassign ? <SeqReassignDialog seq={seq} onClose={() => setReassign(false)} showToast={showToast} /> : null}
    </React.Fragment>
  );
}

/* ============================== ★ SEQUENCE BUILDER ============================== */
/* Best-practice cadence builder: a vertical step timeline (the cadence) on the left,
   a focused step editor / settings panel on the right. Each step picks a channel,
   a delay, and an AI-draftable message with personalization tokens + live preview. */
const SEQ_STEP_CHANNELS = ['email', 'linkedin', 'whatsapp', 'call', 'sms'];
const SEQ_TOKENS = ['{first name}', '{role}', '{company}', '{recruiter}'];
const SEQ_AUDIENCES = ['Stale Java candidates · 30+ days quiet', 'Bench consultants', 'Client sponsors · quiet 14 days', 'Shortlisted - not yet contacted', 'Custom list…'];
const SEQ_SAMPLE = { '{first name}': 'Daniel', '{role}': 'Senior Java Developer', '{company}': 'FedEx', '{recruiter}': 'Sarah' };
const seqRender = (t) => (t || '').replace(/\{first name\}/g, SEQ_SAMPLE['{first name}']).replace(/\{role\}/g, SEQ_SAMPLE['{role}']).replace(/\{company\}/g, SEQ_SAMPLE['{company}']).replace(/\{recruiter\}/g, SEQ_SAMPLE['{recruiter}']);
const SEQ_AI = {
  email: { subject: 'Senior Java roles at {company}, {first name}?', body: 'Hi {first name},\n\nYour {role} background stood out - Stand8 is staffing Java engineers for {company} in Memphis. Worth a quick chat this week?\n\n{recruiter} · Stand8' },
  linkedin: { body: 'Hi {first name} - I place {role}s in Memphis and have a contract at {company}. Open to a quick conversation?' },
  whatsapp: { body: 'Hi {first name}, it’s {recruiter} at Stand8 - we have a {role} contract at {company}. Free for a 10-minute call this week?' },
  sms: { body: '{first name}, {recruiter} at Stand8 here re: a {role} role at {company}. Good time to talk today?' },
  call: { body: 'Voice agent confirms rate, availability and must-haves, then books a follow-up if it’s a fit.' },
};
let SEQ_NID = 0;

function SequenceBuilder({ id }) {
  const D = window.E8DATA;
  const { showToast } = React.useContext(E8Ctx);
  const existing = id ? D.sequences.find((s) => s.id === id) : null;

  const initSteps = existing
    ? existing.steps.map((st, i) => ({ id: 'se' + (SEQ_NID++), ch: st.ch, day: parseInt(String(st.day).replace(/\D/g, ''), 10) || 0, title: st.label, subject: st.ch === 'email' ? 'Quick question, {first name}' : '', body: (SEQ_AI[st.ch] || SEQ_AI.email).body, ifNoReply: i > 0 }))
    : [
        { id: 'se' + (SEQ_NID++), ch: 'email', day: 0, title: 'Intro email', subject: 'Senior Java roles at {company}, {first name}?', body: SEQ_AI.email.body, ifNoReply: false },
        { id: 'se' + (SEQ_NID++), ch: 'linkedin', day: 2, title: 'LinkedIn touch', subject: '', body: SEQ_AI.linkedin.body, ifNoReply: true },
        { id: 'se' + (SEQ_NID++), ch: 'call', day: 4, title: 'Screening call', subject: '', body: SEQ_AI.call.body, ifNoReply: true },
      ];

  const [name, setName] = React.useState(existing ? existing.name : 'Untitled sequence');
  const [audience, setAudience] = React.useState(existing ? existing.audience : SEQ_AUDIENCES[0]);
  const [steps, setSteps] = React.useState(initSteps);
  const [selId, setSelId] = React.useState(initSteps[0].id);
  const [settings, setSettings] = React.useState({ window: '9:00 am - 6:00 pm', tz: true, cap: 50, stop: true, weekends: false, ai: true });
  const bodyRef = React.useRef(null);

  const sel = steps.find((s) => s.id === selId) || null;
  const selIdx = steps.findIndex((s) => s.id === selId);
  const patch = (sid, p) => setSteps((ss) => ss.map((s) => (s.id === sid ? { ...s, ...p } : s)));

  const addStep = (afterIdx) => {
    const st = { id: 'se' + (SEQ_NID++), ch: 'email', day: (steps[afterIdx] ? steps[afterIdx].day : 0) + 2, title: 'New step', subject: '', body: '', ifNoReply: afterIdx >= 0 };
    setSteps((ss) => { const c = ss.slice(); c.splice(afterIdx + 1, 0, st); return c; });
    setSelId(st.id);
  };
  const moveStep = (idx, dir) => setSteps((ss) => { const c = ss.slice(); const j = idx + dir; if (j < 0 || j >= c.length) return ss; const t = c[idx]; c[idx] = c[j]; c[j] = t; return c; });
  const delStep = (sid) => setSteps((ss) => { const c = ss.filter((s) => s.id !== sid); if (selId === sid) setSelId(c.length ? c[0].id : null); return c; });

  const insertToken = (tok) => {
    if (!sel) return;
    const el = bodyRef.current;
    const s = el ? el.selectionStart : (sel.body || '').length;
    const e = el ? el.selectionEnd : s;
    const nv = (sel.body || '').slice(0, s) + tok + (sel.body || '').slice(e);
    patch(sel.id, { body: nv });
    requestAnimationFrame(() => { if (el) { el.focus(); el.selectionStart = el.selectionEnd = s + tok.length; } });
  };
  const aiDraft = () => { if (!sel) return; const d = SEQ_AI[sel.ch] || SEQ_AI.email; patch(sel.id, { subject: d.subject || sel.subject, body: d.body }); showToast('Drafted with AI - edit before it sends'); };

  const isEmail = sel && (sel.ch === 'email' || sel.ch === 'inmail');
  const chName = (ch) => (CHANNELS[ch] || {}).label || ch;

  return (
    <React.Fragment>
      <Topbar
        crumbs={[{ label: 'Sequences', to: 'sequences' }, { label: existing ? name : 'New sequence' }]}
        actions={
          <React.Fragment>
            <DSm2.Button variant="ghost" size="sm" onClick={() => navigate('sequences')}>Cancel</DSm2.Button>
            <DSm2.Button variant="secondary" size="sm" icon="save" onClick={() => { showToast('Saved as draft'); navigate('sequences'); }}>Save draft</DSm2.Button>
            <DSm2.Button variant="primary" size="sm" icon="bolt" onClick={() => { showToast(name + ' activated · ' + steps.length + ' steps'); navigate('sequences'); }}>{existing ? 'Save & activate' : 'Activate'}</DSm2.Button>
          </React.Fragment>
        }
      />
      <div className="e8-content">
        <div className="e8-page" style={{ maxWidth: 1100 }}>
          <div className="e8-seqb">
            {/* Cadence (left) */}
            <div className="e8-seqb-cadence">
              <input className="e8-wf-name" style={{ fontSize: 'var(--ui-text-xl)' }} value={name} onChange={(e) => setName(e.target.value)} placeholder="Sequence name" />
              <div style={{ display: 'flex', alignItems: 'center', gap: 8, margin: '10px 0 4px' }}>
                <span style={{ fontSize: 'var(--ui-text-sm)', color: 'var(--ui-text-tertiary)', flex: 'none' }}>Enroll</span>
                <div style={{ flex: 1 }}><DSm2.Select value={audience} onChange={(e) => setAudience(e.target.value)} options={SEQ_AUDIENCES.map((a) => ({ value: a, label: a }))} /></div>
              </div>
              <div style={{ fontSize: 'var(--ui-text-xs)', color: 'var(--ui-text-tertiary)', marginBottom: 14 }}>~23 people match · they enter at step 1</div>

              <div className="e8-cadence">
                {steps.map((st, i) => (
                  <React.Fragment key={st.id}>
                    <div className={'e8-cstep' + (selId === st.id ? ' sel' : '')} {...window.clickableProps(() => setSelId(st.id))}>
                      <span className="e8-cstep-rail">
                        <ChannelDot ch={st.ch} size={26} />
                        {i < steps.length - 1 ? <span className="e8-cstep-line"></span> : null}
                      </span>
                      <span style={{ flex: 1, minWidth: 0 }}>
                        <span style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
                          <span className="e8-cstep-day">{st.day === 0 && i === 0 ? 'Day 0' : '+' + st.day + 'd'}</span>
                          <span style={{ fontSize: 'var(--ui-text-sm)', fontWeight: 500, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{st.title}</span>
                        </span>
                        <span style={{ display: 'block', fontSize: 'var(--ui-text-xs)', color: 'var(--ui-text-tertiary)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', marginTop: 1 }}>{chName(st.ch)}{st.ifNoReply ? ' · if no reply' : ''}</span>
                      </span>
                      <span className="e8-cstep-acts" onClick={(e) => e.stopPropagation()} onKeyDown={(e) => e.stopPropagation()}>
                        <button type="button" title="Move up" disabled={i === 0} onClick={() => moveStep(i, -1)}><span className="material-symbols-outlined">keyboard_arrow_up</span></button>
                        <button type="button" title="Move down" disabled={i === steps.length - 1} onClick={() => moveStep(i, 1)}><span className="material-symbols-outlined">keyboard_arrow_down</span></button>
                        <button type="button" title="Delete step" onClick={() => delStep(st.id)}><span className="material-symbols-outlined">close</span></button>
                      </span>
                    </div>
                    <button type="button" className="e8-cadd" onClick={() => addStep(i)}><span className="material-symbols-outlined">add</span>Add step</button>
                  </React.Fragment>
                ))}
                <div className="e8-cend"><span className="material-symbols-outlined" style={{ fontSize: 14 }}>flag</span>Stops automatically when they reply</div>
              </div>

              <button type="button" className={'e8-seqb-settings-link' + (selId === null ? ' on' : '')} onClick={() => setSelId(null)}>
                <span className="material-symbols-outlined" style={{ fontSize: 15 }}>tune</span>Sending settings
              </button>
            </div>

            {/* Editor / settings (right) */}
            <div className="e8-seqb-editor">
              {sel ? (
                <React.Fragment>
                  <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 14 }}>
                    <span style={{ fontSize: 'var(--ui-text-base)', fontWeight: 500 }}>Step {selIdx + 1}</span>
                    <input value={sel.title} onChange={(e) => patch(sel.id, { title: e.target.value })} className="e8-seqb-title" />
                  </div>

                  {/* Channel segmented */}
                  <div className="e8-seqb-lbl">Channel</div>
                  <div className="e8-seqb-chans">
                    {SEQ_STEP_CHANNELS.map((ch) => (
                      <button key={ch} type="button" className={'e8-seqb-chan' + (sel.ch === ch ? ' on' : '')} onClick={() => patch(sel.id, { ch })}>
                        <ChannelDot ch={ch} size={16} />{chName(ch).replace(' · RingCentral', '')}
                      </button>
                    ))}
                  </div>

                  {/* Timing */}
                  <div className="e8-seqb-lbl" style={{ marginTop: 14 }}>Timing</div>
                  {selIdx === 0 ? (
                    <div style={{ fontSize: 'var(--ui-text-sm)', color: 'var(--ui-text-secondary)' }}>Sends <b>immediately</b> when someone is enrolled (Day 0).</div>
                  ) : (
                    <div style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 'var(--ui-text-sm)', color: 'var(--ui-text-secondary)' }}>
                      Send
                      <input type="number" min="0" value={sel.day} onChange={(e) => patch(sel.id, { day: Math.max(0, parseInt(e.target.value, 10) || 0) })} className="e8-seqb-num" />
                      days after the previous step
                    </div>
                  )}
                  {selIdx > 0 ? (
                    <label style={{ display: 'inline-flex', alignItems: 'center', gap: 8, marginTop: 10, fontSize: 'var(--ui-text-sm)', color: 'var(--ui-text-secondary)', cursor: 'pointer' }}>
                      <DSm2.Toggle checked={sel.ifNoReply} onChange={() => patch(sel.id, { ifNoReply: !sel.ifNoReply })} /> Only send if they haven’t replied
                    </label>
                  ) : null}

                  {/* Message */}
                  <div style={{ display: 'flex', alignItems: 'center', marginTop: 16, marginBottom: 7 }}>
                    <span className="e8-seqb-lbl" style={{ margin: 0 }}>{sel.ch === 'call' ? 'Call objective' : 'Message'}</span>
                    <span style={{ marginLeft: 'auto' }}><DSm2.Button variant="ghost" size="sm" icon="smart_toy" onClick={aiDraft}>Draft with AI</DSm2.Button></span>
                  </div>
                  {isEmail ? (
                    <input className="e8-seqb-subject" placeholder="Subject line" value={sel.subject} onChange={(e) => patch(sel.id, { subject: e.target.value })} />
                  ) : null}
                  <textarea ref={bodyRef} className="e8-seqb-body" rows={sel.ch === 'call' ? 3 : 6} value={sel.body} onChange={(e) => patch(sel.id, { body: e.target.value })} placeholder={sel.ch === 'call' ? 'What should the voice agent confirm?' : 'Write your message…'} />
                  {sel.ch !== 'call' ? (
                    <div style={{ display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap', marginTop: 8 }}>
                      <span style={{ fontSize: 'var(--ui-text-xs)', color: 'var(--ui-text-tertiary)' }}>Insert</span>
                      {SEQ_TOKENS.map((t) => <button key={t} type="button" className="e8-token" onClick={() => insertToken(t)}>{t}</button>)}
                    </div>
                  ) : null}

                  {/* Live preview */}
                  <div className="e8-seqb-lbl" style={{ marginTop: 16 }}>Preview</div>
                  <div className="e8-seqb-preview">
                    <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}>
                      <ChannelChip ch={sel.ch} />
                      <span style={{ fontSize: 'var(--ui-text-xs)', color: 'var(--ui-text-tertiary)' }}>to Daniel Okafor</span>
                    </div>
                    {isEmail && sel.subject ? <div style={{ fontSize: 'var(--ui-text-base)', fontWeight: 600, marginBottom: 4 }}>{seqRender(sel.subject)}</div> : null}
                    <div style={{ fontSize: 'var(--ui-text-base)', lineHeight: 1.55, color: 'var(--ui-text)', whiteSpace: 'pre-wrap' }}>{seqRender(sel.body) || <span style={{ color: 'var(--ui-text-tertiary)' }}>Nothing yet - write a message or draft with AI.</span>}</div>
                  </div>
                </React.Fragment>
              ) : (
                <React.Fragment>
                  <div style={{ fontSize: 'var(--ui-text-base)', fontWeight: 500, marginBottom: 14 }}>Sending settings</div>
                  <div className="e8-seqb-set"><span>Sending window</span><DSm2.Select value={settings.window} onChange={(e) => setSettings({ ...settings, window: e.target.value })} options={['9:00 am - 6:00 pm', '8:00 am - 8:00 pm', 'Any time'].map((w) => ({ value: w, label: w }))} /></div>
                  <div className="e8-seqb-set"><span>Use each recipient’s timezone</span><DSm2.Toggle checked={settings.tz} onChange={() => setSettings({ ...settings, tz: !settings.tz })} /></div>
                  <div className="e8-seqb-set"><span>Skip weekends</span><DSm2.Toggle checked={settings.weekends} onChange={() => setSettings({ ...settings, weekends: !settings.weekends })} /></div>
                  <div className="e8-seqb-set"><span>Daily send cap</span><input type="number" className="e8-seqb-num" value={settings.cap} onChange={(e) => setSettings({ ...settings, cap: parseInt(e.target.value, 10) || 0 })} /></div>
                  <div className="e8-seqb-set"><span><b>Stop on reply</b><br /><span style={{ fontSize: 'var(--ui-text-xs)', color: 'var(--ui-text-tertiary)' }}>A reply on any channel pauses the rest</span></span><DSm2.Toggle checked={settings.stop} onChange={() => setSettings({ ...settings, stop: !settings.stop })} /></div>
                  <div className="e8-seqb-set"><span><b>AI personalization</b><br /><span style={{ fontSize: 'var(--ui-text-xs)', color: 'var(--ui-text-tertiary)' }}>Each step tailored to the profile · drafts wait for approval</span></span><DSm2.Toggle checked={settings.ai} onChange={() => setSettings({ ...settings, ai: !settings.ai })} /></div>
                  <div style={{ marginTop: 14, fontSize: 'var(--ui-text-sm)', color: 'var(--ui-text-tertiary)', lineHeight: 1.5, display: 'flex', gap: 7 }}>
                    <span className="material-symbols-outlined" style={{ fontSize: 15 }}>shield</span>
                    Nothing sends outside the window, over the cap, or after a reply. Pick a step on the left to edit its message.
                  </div>
                </React.Fragment>
              )}
            </div>
          </div>
        </div>
      </div>
    </React.Fragment>
  );
}

Object.assign(window, { MessagesScreen, NotesScreen, SequencesScreen, SequenceBuilder, SequenceDetail, CHANNELS, ChannelDot, ChannelChip, commsConversationOpened });
