/* ELEV8 ATS - app shell: sidebar, topbar, Ask AI panel, command palette.
   Exposes components on window for other babel scripts. */

const DS = window.Stand8DesignSystem_b5c975;
const { Avatar, AgentChip, Button, E8Mark, Tabs, Select, Toggle } = DS;

const E8Ctx = React.createContext({});
/* Lets the active screen register a contextual action for the mobile top bar. */
const MobileActionCtx = React.createContext({ setAction: () => {} });

/* Re-render subscribers when pipeline stages are edited + saved in the Stages config. */
function useStagesVersion() {
  const [v, setV] = React.useState(0);
  React.useEffect(() => {
    const fn = () => setV((n) => n + 1);
    window.addEventListener('e8-stages-changed', fn);
    return () => window.removeEventListener('e8-stages-changed', fn);
  }, []);
  return v;
}

/* R78: re-render subscribers when the event bus reports queue/comms/store changes -
   this is what keeps nav badges honest as approvals clear and messages are read. */
function useBusVersion() {
  const [v, setV] = React.useState(0);
  React.useEffect(() => (
    window.E8Events
      ? window.E8Events.subscribe(['queue:changed', 'comms:changed', 'store:changed'], () => setV((n) => n + 1))
      : undefined
  ), []);
  return v;
}

function useWorkspaceVersion() {
  const [version, setVersion] = React.useState(0);
  React.useEffect(() => (
    window.E8Events
      ? window.E8Events.subscribe(['workspace:changed', 'store:changed', 'persona:changed'], () => setVersion((v) => v + 1))
      : undefined
  ), []);
  return version;
}

function WorkspacePinButton({ type, id, label, compact }) {
  useWorkspaceVersion();
  const persona = window.e8ActivePersona ? window.e8ActivePersona() : window.E8DATA.user;
  if (!persona || !window.E8Workspace) return null;
  const pinned = window.E8Workspace.isPinned(persona.name, type, id);
  return (
    <DS.Button
      variant="ghost"
      size="sm"
      icon={pinned ? 'keep' : 'keep_off'}
      iconOnly={compact}
      title={pinned ? 'Remove from saved work' : 'Save to focus set'}
      onClick={(e) => {
        e.stopPropagation();
        if (pinned) window.E8Workspace.unpin(persona.name, type, id);
        else window.E8Workspace.pin(persona.name, { type, id });
      }}
    >
      {compact ? null : (pinned ? 'Saved' : label || 'Save')}
    </DS.Button>
  );
}

/* ---------- Data mode (Demo / Empty / Scale) - R98 Task 6 ----------
   The mode is applied pre-store by app/data-mode.js; switching is confirm + write key + reload
   (module-level captures across the app make a hot swap unsafe). On switch we clear the
   id-coupled overlay keys whose record ids no longer exist in the target dataset, but we keep
   the per-mode patch key and the per-mode dashboard layout (records + layout created in a mode
   survive a round-trip through other modes) and user prefs (appearance, accent, columns, views). */
var E8_DATA_MODES = [
  { id: 'demo', label: 'Demo', icon: 'dataset', sub: 'The full sample dataset' },
  { id: 'empty', label: 'Empty', icon: 'inbox', sub: 'First-time setup, no data' },
  { id: 'scale', label: 'Scale', icon: 'stacked_bar_chart', sub: 'About 100 rows per list' },
];
function e8CurrentDataMode() { return window.E8_DATA_MODE || 'demo'; }
function e8ClearModeScopedKeys() {
  /* e8-dash-layout-v1 is NOT cleared: it is scoped per mode at the read site (screens-core), so
     each mode keeps its own dashboard and empty mode still gets the first-run default. */
  var EXACT = ['e8-approvals-v1', 'e8-watch-v1', 'e8-watch-sweep', 'e8-match-v1', 'e8-match-feedback-v1',
    'e8-ap-screen-v1', 'e8-notify-v1', 'e8-cand-segments-v1', 'e8-tp-migrated-v1', 'e8-audit-v1', 'e8-events-v1'];
  var PREFIX = ['e8-ts-', 'e8-match-decisions-', 'e8-match-weights-', 'e8-composer-draft-'];
  try {
    EXACT.forEach(function (k) { localStorage.removeItem(k); });
    var kill = [];
    for (var i = 0; i < localStorage.length; i++) {
      var k = localStorage.key(i);
      if (k && PREFIX.some(function (p) { return k.indexOf(p) === 0; })) kill.push(k);
    }
    kill.forEach(function (k) { localStorage.removeItem(k); });
  } catch (e) {}
}
function e8SwitchDataMode(next) {
  if (next === e8CurrentDataMode()) return;
  e8ClearModeScopedKeys();
  try { localStorage.setItem('e8-data-mode', next); } catch (e) {}
  window.location.reload();
}

/* ---------- Platform signals (ELEV8 provenance marks) kill switch ----------
   localStorage 'e8-signals' ('on' default); when off, .e8-signals-off on the app root
   (body carries .elev8) hides every .e8-pm mark - the CSS rule ships from Badge.jsx. */
function e8SignalsOn() {
  try { return window.localStorage.getItem('e8-signals') !== 'off'; } catch (e) { return true; }
}
function e8SetSignals(on) {
  try { window.localStorage.setItem('e8-signals', on ? 'on' : 'off'); } catch (e) {}
  document.body.classList.toggle('e8-signals-off', !on);
  window.dispatchEvent(new Event('e8-signals-changed'));
}
/* Apply the persisted state at load so marks never flash before React mounts. */
e8SetSignals(e8SignalsOn());

function useSignals() {
  const [on, setOn] = React.useState(e8SignalsOn);
  React.useEffect(() => {
    const fn = () => setOn(e8SignalsOn());
    window.addEventListener('e8-signals-changed', fn);
    return () => window.removeEventListener('e8-signals-changed', fn);
  }, []);
  return [on, e8SetSignals];
}

/* ---------- Simulated ELEV8 platform sync (sidebar card + ⌘K action) ---------- */
const E8_SYNC_SEED_STAMP = '8:40 AM'; // the fiction's "last synced" time before any manual sync
let e8SyncStamp = null; // set by e8SyncNow; SyncCard reads it on the 'e8-synced' event
function e8SyncNow(showToast) {
  e8SyncStamp = new Date().toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' });
  window.dispatchEvent(new Event('e8-synced'));
  if (showToast) showToast('Synced from ELEV8 - 3 records updated');
}

/* Live values for the badges that change at runtime; everything else keeps its load-time value.
   Zero hides the badge (a cleared queue should look clear). */
function liveBadge(id, fallback) {
  try {
    if (id === 'approvals' && window.E8Queue) { const n = window.E8Queue.counts().review; return n ? String(n) : undefined; }
    if (id === 'inbox' && window.E8Comms) { const n = window.E8Comms.unreadCount(); return n ? String(n) : undefined; }
    // R98: open (not snoozed, not done) tasks owned by the signed-in user. The sidebar
    // already re-renders on 'store:changed' (useBusVersion), so this stays live.
    if (id === 'tasks' && window.E8DATA) {
      const me = window.E8DATA.user.name;
      const n = (window.E8DATA.tasks || []).filter((t) => t.owner === me && t.state === 'open').length;
      return n ? String(n) : undefined;
    }
    // R111: persona-scoped possible-duplicate pairs to review. The sidebar re-renders on 'store:changed'
    // / 'persona:changed' (useBusVersion), so this tracks merges, dismissals, and persona switches live.
    if (id === 'duplicates' && window.candidateDupPairs) {
      const n = window.candidateDupPairs(window.e8ActivePersona ? window.e8ActivePersona() : null).length;
      return n ? String(n) : undefined;
    }
  } catch (e) {}
  return fallback;
}

function inboxShortcutState(activePage, unread) {
  const count = Math.max(0, Number(unread) || 0);
  return {
    active: activePage === 'inbox',
    badge: count > 99 ? '99+' : (count || null),
    label: count ? `Inbox, ${count} unread message${count === 1 ? '' : 's'}` : 'Inbox, no unread messages',
  };
}

function InboxShortcut({ route }) {
  useBusVersion();
  const unread = (() => {
    try {
      return window.E8Comms
        ? window.E8Comms.unreadCount()
        : ((window.E8DATA && window.E8DATA.conversations) || []).filter((conversation) => conversation.unread).length;
    } catch (error) {
      return 0;
    }
  })();
  const state = inboxShortcutState(activePageOf(route), unread);
  return (
    <button
      type="button"
      className={'e8-global-inbox' + (state.active ? ' active' : '')}
      aria-label={state.label}
      aria-current={state.active ? 'page' : undefined}
      title={state.label}
      onClick={() => navigate('inbox')}
    >
      <span className="material-symbols-outlined" aria-hidden="true">inbox</span>
      <span>Inbox</span>
      {state.badge ? <span className="e8-global-inbox-badge" aria-hidden="true">{state.badge}</span> : null}
    </button>
  );
}

/* ---------- Navigation model (exact IA from spec) ---------- */
// Derive nav badge counts from data so they never drift from reality.
const RENEWALS_DUE = (window.E8DATA.engagements || []).filter((e) => e.renewal && e.renewal !== 'Not due').length;
const VOICE_LIVE = (window.E8DATA.liveCalls || []).length;
const NEW_APPLICANTS = (window.E8DATA.applications || []).filter((a) => a.stage === 'New').length;
/* R111: load-time seed for the Duplicates nav count (persona-scoped pairs to review). liveBadge('duplicates')
   re-derives it against the ACTIVE persona on every store:changed / persona:changed so a merge or dismiss
   (or a persona switch) keeps the badge honest. */
const DUP_PAIRS = window.candidateDupPairs ? window.candidateDupPairs(window.e8ActivePersona ? window.e8ActivePersona() : null).length : 0;

const NAV_GROUPS = [
  {
    label: 'Work',
    items: [
      { id: 'today', label: 'Work queue', icon: 'checklist' },
      { id: 'tasks', label: 'Tasks', icon: 'task_alt' },
      { id: 'dashboard', label: 'Dashboard', icon: 'space_dashboard' },
      { id: 'jobs', label: 'Job orders', icon: 'work', badge: window.E8DATA.jobs.length ? String(window.E8DATA.jobs.length) : undefined },
      { id: 'applicants', label: 'Applicants', icon: 'move_to_inbox', badge: NEW_APPLICANTS ? String(NEW_APPLICANTS) : undefined, badgeTone: 'due' },
      { id: 'candidates', label: 'Candidates', icon: 'group' },
      { id: 'duplicates', label: 'Duplicates', icon: 'content_copy', badge: DUP_PAIRS ? String(DUP_PAIRS) : undefined, badgeTone: 'due' },
      { id: 'submissions', label: 'Submissions', icon: 'send' },
      { id: 'sequences', label: 'Sequences', icon: 'campaign' },
      { id: 'templates', label: 'Templates', icon: 'description' },
      { id: 'engagements', label: 'Engagements', icon: 'badge' },
      { id: 'clients', label: 'Clients', icon: 'apartment' },
      { id: 'contacts', label: 'Contacts', icon: 'contacts' },
      { id: 'csms', label: 'CSMs', icon: 'support_agent' },
      { id: 'consultants', label: 'Consultants', icon: 'engineering' },
      { id: 'renewals', label: 'Renewals', icon: 'autorenew', badge: RENEWALS_DUE ? RENEWALS_DUE + ' due' : undefined, badgeTone: 'due' },
      { id: 'rates', label: 'Rate library', icon: 'payments' },
    ],
  },
  {
    label: 'Ops',
    items: [
      { id: 'reports', label: 'Reports', icon: 'monitoring' },
      { id: 'commissions', label: 'Commissions', icon: 'payments' },
      { id: 'time', label: 'Time', icon: 'schedule' },
      { id: 'reconciliation', label: 'Reconciliation', icon: 'balance', soon: true },
      { id: 'syncqueue', label: 'Sync queue', icon: 'sync' },
      { id: 'audit', label: 'Audit log', icon: 'history' },
    ],
  },
  {
    label: 'AI',
    items: [
      { id: 'voice', label: 'Voice ops', icon: 'call', badge: VOICE_LIVE ? VOICE_LIVE + ' live' : undefined, badgeTone: 'live' },
      { id: 'matching', label: 'Matching engine', icon: 'join_inner' },
      { id: 'workfloweditor', label: 'Workflow editor', icon: 'account_tree' },
      { id: 'agents', label: 'Agents', icon: 'smart_toy' },
      { id: 'enrichment', label: 'Enrichment', icon: 'auto_awesome' },
    ],
  },
  {
    label: 'Config',
    items: [
      { id: 'fields', label: 'Fields', icon: 'input' },
      { id: 'stages', label: 'Stages', icon: 'linear_scale' },
      { id: 'workflows', label: 'Workflows', icon: 'schema' },
      { id: 'forms', label: 'Forms', icon: 'list_alt' },
      { id: 'system', label: 'Components & tokens', icon: 'palette' },
    ],
  },
  {
    label: 'Me',
    items: [
      { id: 'inbox', label: 'Inbox', icon: 'inbox', badge: String(window.E8DATA.conversations.filter((c) => c.unread).length) },
      { id: 'approvals', label: 'Approvals', icon: 'fact_check', badge: String(window.E8DATA.approvals.filter((i) => i.state === 'review').length) },
      { id: 'notes', label: 'Notes', icon: 'sticky_note_2' },
      { id: 'workspace', label: 'Workspace', icon: 'grid_view', soon: true },
      { id: 'settings', label: 'Settings', icon: 'settings', soon: true },
    ],
  },
];

/* Pages with built screens; everything else gets the unbuilt-module state. */
const BUILT = ['home', 'today', 'tasks', 'dashboard', 'jobs', 'applicants', 'candidates', 'duplicates', 'submissions', 'sequences', 'engagements', 'engagement', 'renewals', 'casestudies', 'clients', 'client', 'contacts', 'contact', 'inbox', 'approvals', 'notes', 'agents', 'matching', 'workfloweditor', 'voice', 'reports', 'commissions', 'time', 'consultants', 'care', 'csms', 'rates', 'audit', 'syncqueue', 'system', 'fields', 'stages', 'workflows', 'forms', 'templates', 'enrichment'];

/* ---------- Hash router helpers ---------- */
function parseHash() {
  const raw = (location.hash || '').replace(/^#\/?/, '');
  const [path, queryStr] = raw.split('?');
  const [page, id, sub] = path.split('/');
  const query = {};
  if (queryStr) queryStr.split('&').forEach((kv) => { const i = kv.indexOf('='); const k = i < 0 ? kv : kv.slice(0, i); if (k) query[k] = decodeURIComponent(i < 0 ? '' : kv.slice(i + 1)); });
  if (page) return { page, id: id || null, sub: sub || null, query };
  const persona = window.e8ActivePersona ? window.e8ActivePersona() : window.E8DATA && window.E8DATA.user;
  const preferred = window.E8Workspace && persona ? window.E8Workspace.get(persona.name).defaults.landingPage : 'home';
  const valid = preferred === 'home' || NAV_GROUPS.some((group) => group.items.some((item) => item.id === preferred));
  return { page: valid ? preferred : 'home', id: null, sub: null, query };
}
function navigate(path) {
  const clean = String(path || 'home').replace(/^#\/?/, '');
  const [page, id] = clean.split(/[/?]/);
  const recordTypes = { job: 'job', candidate: 'candidate', submission: 'submission', contact: 'contact', task: 'task', tasks: 'task' };
  const persona = window.e8ActivePersona ? window.e8ActivePersona() : window.E8DATA && window.E8DATA.user;
  if (id && recordTypes[page] && window.E8Workspace && persona) {
    window.E8Workspace.touchRecent(persona.name, { type: recordTypes[page], id });
  }
  if (location.hash !== '#/' + clean) location.hash = '#/' + clean;
}

function openHomeCommand(prompt) {
  if (prompt) window.__e8HomePrompt = String(prompt);
  navigate('home');
  window.setTimeout(() => window.dispatchEvent(new Event('e8-focus-home-command')), 0);
}
function useRoute() {
  const [route, setRoute] = React.useState(parseHash());
  React.useEffect(() => {
    const fn = () => setRoute(parseHash());
    window.addEventListener('hashchange', fn);
    return () => window.removeEventListener('hashchange', fn);
  }, []);
  return route;
}

/* R107: the candidate self-service portal (#/portal/:candId) renders chrome-free - no sidebar /
   topbar / mobile tab bar / cmdk / Ask AI. main.jsx uses this predicate to short-circuit the shell
   for portal routes; every other route keeps the normal app chrome. */
function isPortalRoute(route) { return !!route && route.page === 'portal'; }

/* ---------- Sidebar (desktop rail + mobile drawer) ---------- */
function activePageOf(route) {
  return route.page === 'candidate' ? 'candidates' : route.page === 'job' ? 'jobs' : route.page === 'client' ? 'clients' : route.page === 'contact' ? 'contacts' : route.page === 'consultant' ? 'consultants' : route.page === 'csm' ? 'csms' : route.page === 'engagement' ? 'engagements' : route.page === 'care' ? 'engagements' : route.page === 'sequence' ? 'sequences' : route.page;
}

/* "Install app" affordance - appears once the browser offers the PWA prompt. */
function InstallButton() {
  const [avail, setAvail] = React.useState(typeof window !== 'undefined' && !!window.__e8InstallPrompt);
  React.useEffect(() => {
    const on = () => setAvail(true);
    window.addEventListener('e8-installable', on);
    return () => window.removeEventListener('e8-installable', on);
  }, []);
  if (!avail) return null;
  const install = async () => {
    const p = window.__e8InstallPrompt;
    if (!p) return;
    p.prompt();
    try { await p.userChoice; } catch (e) {}
    window.__e8InstallPrompt = null;
    setAvail(false);
  };
  return (
    <button className="e8-install-btn" type="button" onClick={install}>
      <span className="material-symbols-outlined" style={{ fontSize: 16 }}>install_mobile</span>
      Install app
    </button>
  );
}

// In-app Appearance + theme control. Lives in the sidebar + mobile menu so end
// users can switch appearance/theme without the host-only Tweaks panel.
function AppearanceMenu({ theme }) {
  const [open, setOpen] = React.useState(false);
  const [signals, setSignals] = useSignals();
  const { confirm, dialog } = useConfirm();
  const dataMode = e8CurrentDataMode();
  const ref = React.useRef(null);
  React.useEffect(() => {
    if (!open) return;
    const onDoc = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
    const onEsc = (e) => { if (e.key === 'Escape') setOpen(false); };
    document.addEventListener('mousedown', onDoc);
    document.addEventListener('keydown', onEsc);
    return () => { document.removeEventListener('mousedown', onDoc); document.removeEventListener('keydown', onEsc); };
  }, [open]);
  if (!theme) return null;
  const modes = [
    { id: 'light', label: 'Light', icon: 'light_mode' },
    { id: 'dark', label: 'Dark', icon: 'dark_mode' },
    { id: 'system', label: 'Auto', icon: 'contrast' },
  ];
  const cur = modes.find((m) => m.id === theme.appearance) || modes[0];
  return (
    <div className="e8-appr" ref={ref}>
      <button type="button" className="e8-appr-btn" onClick={() => setOpen(!open)} aria-haspopup="true" aria-expanded={open} title="Appearance and theme">
        <span className="material-symbols-outlined" style={{ fontSize: 16 }}>{cur.icon}</span>
        Theme
        <span className="e8-appr-dot" style={{ background: theme.accent }} />
      </button>
      {open ? (
        <div className="e8-appr-pop" role="menu">
          <div className="e8-appr-h">Appearance</div>
          <div className="e8-appr-modes">
            {modes.map((m) => (
              <button key={m.id} type="button" className={'e8-appr-mode' + (theme.appearance === m.id ? ' on' : '')}
                      aria-pressed={theme.appearance === m.id} onClick={() => theme.setAppearance(m.id)}>
                <span className="material-symbols-outlined" style={{ fontSize: 18 }}>{m.icon}</span>
                <span>{m.label}</span>
              </button>
            ))}
          </div>
          <div className="e8-appr-h">Theme</div>
          <div className="e8-appr-swatches">
            {(theme.themes || []).map((th) => (
              <button key={th.key} type="button" className={'e8-appr-sw' + (theme.accent === th.key ? ' on' : '')}
                      title={th.name} aria-label={th.name} style={{ background: th.key }} onClick={() => theme.setAccent(th.key)}>
                {theme.accent === th.key ? <span className="material-symbols-outlined">check</span> : null}
              </button>
            ))}
          </div>
          <button type="button" className={'e8-appr-sig' + (signals ? ' on' : '')} role="switch" aria-checked={signals}
                  onClick={() => setSignals(!signals)}>
            <span className="e8-appr-sig-txt">
              <span className="e8-appr-sig-t">Platform signals</span>
              <span className="e8-appr-sig-d">Show ELEV8 provenance marks</span>
            </span>
            <span className="e8-appr-sig-track" aria-hidden="true"></span>
          </button>
          <div className="e8-appr-h">Data</div>
          <div className="e8-appr-modes e8-appr-data">
            {E8_DATA_MODES.map((m) => (
              <button key={m.id} type="button" className={'e8-appr-mode' + (dataMode === m.id ? ' on' : '')}
                      aria-pressed={dataMode === m.id} title={m.sub}
                      onClick={() => {
                        if (m.id === dataMode) return;
                        confirm({
                          title: 'Switch to ' + m.label.toLowerCase() + ' data?',
                          body: m.sub + '. The app reloads. Records you created in the current mode are kept and reappear when you switch back.',
                          confirmLabel: 'Switch and reload', tone: 'primary', icon: m.icon,
                          onConfirm: () => e8SwitchDataMode(m.id),
                        });
                      }}>
                <span className="material-symbols-outlined" style={{ fontSize: 18 }}>{m.icon}</span>
                <span>{m.label}</span>
              </button>
            ))}
          </div>
        </div>
      ) : null}
      {dialog}
    </div>
  );
}

/* ELEV8 platform sync status - desktop sidebar foot only (the mobile menu stays lean).
   The refresh button (and the ⌘K "Sync from ELEV8" action) fire a simulated pull. */
function SyncCard() {
  const { showToast } = React.useContext(E8Ctx);
  const [stamp, setStamp] = React.useState(() => e8SyncStamp || E8_SYNC_SEED_STAMP);
  React.useEffect(() => {
    const fn = () => setStamp(e8SyncStamp || E8_SYNC_SEED_STAMP);
    window.addEventListener('e8-synced', fn);
    return () => window.removeEventListener('e8-synced', fn);
  }, []);
  return (
    <div className="e8-sync-card">
      <div className="e8-sync-main">
        {/* e8-pm-keep: this mark explains the signals toggle itself, so it survives signals-off */}
        <div className="e8-sync-name e8-pm-keep">{E8Mark ? <E8Mark /> : null}ELEV8 platform</div>
        <div className="e8-sync-meta"><span className="e8-sync-dot" aria-hidden="true"></span>Synced {stamp}</div>
      </div>
      <button type="button" className="e8-sync-btn" title="Sync now" aria-label="Sync from ELEV8" onClick={() => e8SyncNow(showToast)}>
        <span className="material-symbols-outlined">sync</span>
      </button>
    </div>
  );
}

const CUSTOMIZE_TABS = [
  { id: 'focus', label: 'Saved work' },
  { id: 'navigation', label: 'Navigation' },
  { id: 'defaults', label: 'Defaults' },
  { id: 'views', label: 'Views' },
];
const MOBILE_JOB_FIELDS = [
  { key: 'sla', label: 'SLA and age' },
  { key: 'owner', label: 'Owner' },
  { key: 'location', label: 'Location and work model' },
  { key: 'pipeline', label: 'Pipeline' },
  { key: 'health', label: 'Health' },
  { key: 'forecast', label: 'Forecast' },
];
const LANDING_OPTIONS = [{ id: 'home', label: 'Home' }].concat(
  NAV_GROUPS.flatMap((group) => group.items.map((item) => ({ id: item.id, label: item.label }))),
);

function workspacePersona() {
  return window.e8ActivePersona ? window.e8ActivePersona() : window.E8DATA.user;
}

function navItems() {
  return NAV_GROUPS.flatMap((group) => group.items.map((item) => ({ ...item, group: group.label })));
}

function navItemById(id) {
  return navItems().find((item) => item.id === id) || null;
}

function orderedNavIds(pref) {
  const stored = pref && pref.nav && Array.isArray(pref.nav.order) ? pref.nav.order : [];
  if (window.E8Workspace && window.E8Workspace.defaultNavOrder) {
    const defaults = window.E8Workspace.defaultNavOrder;
    const known = stored.filter((id) => defaults.includes(id));
    return known.concat(defaults.filter((id) => !known.includes(id)));
  }
  return stored.slice();
}

function orderedNavGroups(pref) {
  const hidden = new Set((pref && pref.nav && pref.nav.hidden) || []);
  const order = orderedNavIds(pref);
  return NAV_GROUPS.map((group) => {
    const ids = order.filter((id) => group.items.some((item) => item.id === id) && !hidden.has(id));
    return {
      label: group.label,
      items: ids.map((id) => navItemById(id)).filter(Boolean),
    };
  }).filter((group) => group.items.length);
}

function WorkspaceCustomizer({ open, onClose, initialTab }) {
  const ref = React.useRef(null);
  const [tab, setTab] = React.useState(initialTab || 'focus');
  const [newName, setNewName] = React.useState('');
  const [dragId, setDragId] = React.useState(null);
  useDialog(open, ref);
  useWorkspaceVersion();
  React.useEffect(() => {
    if (open && initialTab) setTab(initialTab);
  }, [open, initialTab]);
  React.useEffect(() => {
    if (!open) return undefined;
    const onKey = (e) => {
      if (e.key === 'Escape') {
        e.preventDefault();
        onClose();
      }
    };
    document.addEventListener('keydown', onKey);
    return () => document.removeEventListener('keydown', onKey);
  }, [open, onClose]);
  if (!open) return null;
  const persona = workspacePersona();
  const pref = window.E8Workspace.get(persona.name);
  const activeSet = pref.pinSets.find((set) => set.id === pref.activePinSetId) || pref.pinSets[0];
  const navOrder = orderedNavIds(pref);
  const mobileJobFields = (pref.mobileCards && pref.mobileCards.jobs ? pref.mobileCards.jobs : ['sla', 'owner'])
    .filter((field) => MOBILE_JOB_FIELDS.some((option) => option.key === field))
    .slice(0, 4);
  const saveSet = () => { const name = newName.trim(); if (name) { window.E8Workspace.savePinSet(persona.name, name); setNewName(''); } };
  const goToViews = (path) => { onClose(); navigate(path); };
  const setMobileJobField = (field, checked) => {
    const fields = checked
      ? mobileJobFields.concat(field)
      : mobileJobFields.filter((selected) => selected !== field);
    window.E8Workspace.setMobileFields(persona.name, 'jobs', fields);
  };
  const toggleNavHidden = (id, show) => {
    const hidden = show
      ? pref.nav.hidden.filter((hiddenId) => hiddenId !== id)
      : pref.nav.hidden.concat(id);
    window.E8Workspace.setNav(persona.name, { order: navOrder, hidden });
  };
  const canMoveNav = (id, direction) => {
    const index = navOrder.indexOf(id);
    const neighbor = navOrder[index + direction];
    if (index < 0 || !neighbor) return false;
    const item = navItemById(id);
    const other = navItemById(neighbor);
    return !!(item && other && item.group === other.group);
  };
  const onNavDrop = (targetId) => {
    if (!dragId || dragId === targetId) { setDragId(null); return; }
    const from = navOrder.indexOf(dragId);
    const to = navOrder.indexOf(targetId);
    const source = navItemById(dragId);
    const target = navItemById(targetId);
    setDragId(null);
    if (from < 0 || to < 0 || !source || !target || source.group !== target.group) return;
    const next = navOrder.slice();
    next.splice(from, 1);
    next.splice(to, 0, dragId);
    window.E8Workspace.setNav(persona.name, { order: next, hidden: pref.nav.hidden });
  };
  return <div className="e8-overlay" onMouseDown={(e) => { if (e.target === e.currentTarget) onClose(); }}>
    <section ref={ref} className="e8-workspace" role="dialog" aria-modal="true" aria-label="Customize workspace">
      <header><div><b>Customize workspace</b><small>Personal settings for {persona.name}</small></div><Button variant="ghost" size="sm" icon="close" iconOnly title="Close" onClick={onClose} /></header>
      <Tabs items={CUSTOMIZE_TABS} active={tab} onChange={setTab} />
      <div className="e8-workspace-body">
        {tab === 'focus' ? <React.Fragment>
          <label>Focus set<Select value={pref.activePinSetId} options={pref.pinSets.map((set) => ({ value: set.id, label: set.name === 'My desk' ? 'Saved work' : set.name }))} onChange={(e) => window.E8Workspace.activatePinSet(persona.name, e.target.value)} /></label>
          <div className="e8-workspace-inline"><input value={newName} placeholder="New focus set name" onChange={(e) => setNewName(e.target.value)} /><Button size="sm" onClick={saveSet}>Create set</Button></div>
          <div className="e8-workspace-inline"><Button variant="secondary" size="sm" onClick={() => { const currentName = activeSet.name === 'My desk' ? 'Saved work' : activeSet.name; const name = window.prompt('Rename focus set', currentName); if (name && name.trim()) window.E8Workspace.renamePinSet(persona.name, activeSet.id, name.trim()); }}>Rename</Button><Button variant="reject" size="sm" disabled={pref.pinSets.length === 1} onClick={() => window.E8Workspace.deletePinSet(persona.name, activeSet.id)}>Delete</Button></div>
          <div className="e8-workspace-pins">
            <b>Saved records</b>
            {activeSet.targets.length ? activeSet.targets.map((target, index) => {
              const row = window.E8Workspace.resolve(target);
              const label = row ? row.label : target.type + ' · ' + target.id;
              return <div className="e8-workspace-pin" key={target.type + ':' + target.id}>
                <span>{label}</span>
                <button type="button" aria-label={'Move ' + label + ' up'} disabled={index === 0} onClick={() => window.E8Workspace.movePin(persona.name, target.type, target.id, -1)}><span className="material-symbols-outlined">arrow_upward</span></button>
                <button type="button" aria-label={'Move ' + label + ' down'} disabled={index === activeSet.targets.length - 1} onClick={() => window.E8Workspace.movePin(persona.name, target.type, target.id, 1)}><span className="material-symbols-outlined">arrow_downward</span></button>
                <button type="button" aria-label={'Remove ' + label + ' from saved work'} onClick={() => window.E8Workspace.unpin(persona.name, target.type, target.id)}><span className="material-symbols-outlined">close</span></button>
              </div>;
            }) : <p>No records saved in this set.</p>}
          </div>
          <label>Recent records<Select value={String(pref.recentLimit)} options={[0, 3, 5, 10].map((value) => ({ value: String(value), label: value ? value + ' records' : 'Off' }))} onChange={(e) => window.E8Workspace.setRecentLimit(persona.name, e.target.value)} /></label>
          <Button variant="ghost" size="sm" onClick={() => window.E8Workspace.resetSection(persona.name, 'desk')}>Reset saved work</Button>
        </React.Fragment> : null}
        {tab === 'navigation' ? <div className="e8-workspace-nav">
          <p>Home stays permanent. Reorder within each group, and hide any destination you do not need. Hidden items remain available in search.</p>
          {NAV_GROUPS.map((group) => {
            const ids = navOrder.filter((id) => group.items.some((item) => item.id === id));
            return <div className="e8-workspace-nav-group" key={group.label}>
              <b>{group.label}</b>
              {ids.map((id) => {
                const item = navItemById(id);
                return <div
                  className={'e8-workspace-nav-row' + (dragId === id ? ' is-dragging' : '')}
                  key={id}
                  draggable
                  onDragStart={() => setDragId(id)}
                  onDragOver={(e) => e.preventDefault()}
                  onDrop={() => onNavDrop(id)}
                  onDragEnd={() => setDragId(null)}
                >
                  <span className="e8-workspace-nav-grip" aria-hidden="true">⠿</span>
                  <Toggle aria-label={'Show ' + item.label} checked={!pref.nav.hidden.includes(id)} onChange={(e) => toggleNavHidden(id, e.target.checked)} />
                  <span>{item.label}</span>
                  <button type="button" aria-label={'Move ' + item.label + ' up'} disabled={!canMoveNav(id, -1)} onClick={() => window.E8Workspace.moveNavItem(persona.name, id, -1)}><span className="material-symbols-outlined">arrow_upward</span></button>
                  <button type="button" aria-label={'Move ' + item.label + ' down'} disabled={!canMoveNav(id, 1)} onClick={() => window.E8Workspace.moveNavItem(persona.name, id, 1)}><span className="material-symbols-outlined">arrow_downward</span></button>
                </div>;
              })}
            </div>;
          })}
          <Button variant="ghost" size="sm" onClick={() => window.E8Workspace.resetSection(persona.name, 'navigation')}>Reset navigation</Button>
        </div> : null}
        {tab === 'defaults' ? <React.Fragment><label>Landing page<Select value={pref.defaults.landingPage} options={LANDING_OPTIONS.map((item) => ({ value: item.id, label: item.label }))} onChange={(e) => window.E8Workspace.setDefaults(persona.name, { landingPage: e.target.value })} /></label><label>Job section<Select value={pref.defaults.jobSection} options={[{ value: 'pipeline', label: 'Pipeline' }, { value: 'matching', label: 'Candidates' }, { value: 'submissions', label: 'Client' }, { value: 'activity', label: 'Activity' }, { value: 'overview', label: 'More' }]} onChange={(e) => window.E8Workspace.setDefaults(persona.name, { jobSection: e.target.value })} /></label><Button variant="ghost" size="sm" onClick={() => window.E8Workspace.resetSection(persona.name, 'defaults')}>Reset defaults</Button></React.Fragment> : null}
        {tab === 'views' ? <div className="e8-workspace-views">
          <p>Saved views are managed with each list.</p>
          <Button variant="secondary" size="sm" icon="work" onClick={() => goToViews('jobs')}>Open Job orders views</Button>
          <Button variant="secondary" size="sm" icon="move_to_inbox" onClick={() => goToViews('applicants')}>Open Applicants views</Button>
          <div className="e8-workspace-mobilefields">
            <b>Mobile job cards</b>
            <p>Status, Next step, pin, and the primary action are always shown. Choose up to four supporting fields.</p>
            {MOBILE_JOB_FIELDS.map((field) => {
              const checked = mobileJobFields.includes(field.key);
              return <label key={field.key}>
                <Toggle
                  aria-label={'Show ' + field.label + ' on mobile job cards'}
                  checked={checked}
                  disabled={!checked && mobileJobFields.length >= 4}
                  onChange={(e) => setMobileJobField(field.key, e.target.checked)}
                />
                <span>{field.label}</span>
              </label>;
            })}
            <small>{mobileJobFields.length} of 4 supporting fields selected</small>
          </div>
          <Button variant="ghost" size="sm" onClick={() => window.E8Workspace.resetSection(persona.name, 'views')}>Reset views</Button>
        </div> : null}
      </div>
      <footer><Button variant="ghost" size="sm" onClick={() => window.E8Workspace.reset(persona.name)}>Reset all personal workspace</Button><Button size="sm" onClick={onClose}>Done</Button></footer>
    </section>
  </div>;
}

function Sidebar({ route, onAskAI, onCmdK, open, onClose, theme }) {
  const activePage = activePageOf(route);
  useBusVersion();
  useWorkspaceVersion();
  const persona = workspacePersona();
  const pref = window.E8Workspace.get(persona.name);
  const groups = orderedNavGroups(pref);
  const go = (id) => { navigate(id); onClose && onClose(); };
  if (activePage === 'home') {
    const config = window.E8HomeData ? window.E8HomeData.roleConfig(persona) : null;
    const homeItems = config && config.rail ? config.rail : [
      { id: 'home', label: 'Home', icon: 'home' },
      { id: 'today', label: 'Work', icon: 'checklist' },
    ];
    return (
      <nav className="e8-side e8-home-rail" aria-label="Primary navigation">
        <div className="e8-home-rail-brand" aria-label="ELEV8">8</div>
        <div className="e8-home-rail-items">
          {homeItems.map((item) => (
            <button key={item.id} type="button" className={'e8-home-rail-item' + (item.id === 'home' ? ' active' : '')} aria-current={item.id === 'home' ? 'page' : undefined} onClick={() => go(item.id)}>
              <span className="material-symbols-outlined" aria-hidden="true">{item.icon}</span>
              <span>{item.label}</span>
            </button>
          ))}
        </div>
        <button type="button" className="e8-home-rail-search" aria-label="Open all navigation" onClick={() => onCmdK && onCmdK()}>
          <span className="material-symbols-outlined" aria-hidden="true">apps</span>
        </button>
      </nav>
    );
  }
  return (
    <nav className={'e8-side' + (open ? ' is-open' : '')}>
      <div className="e8-side-head">
        <span className="e8-logomark" aria-hidden="true">8</span>
        <span className="e8-logotype">ELEV8</span>
        <span className="e8-logo-sub">ATS</span>
        <button className="e8-side-close" type="button" aria-label="Close menu" onClick={onClose}>
          <span className="material-symbols-outlined">close</span>
        </button>
      </div>
      <button className="e8-side-search" type="button" onClick={() => { onCmdK && onCmdK(); onClose && onClose(); }}>
        <span className="material-symbols-outlined">search</span>
        Search
        <span className="e8-kbd">⌘K</span>
      </button>
      <div className="e8-side-scroll">
        <div className="e8-side-group">
          <button type="button" className={'e8-nav-item' + (activePage === 'home' ? ' active' : '')} aria-current={activePage === 'home' ? 'page' : undefined} onClick={() => go('home')}>
            <span className="material-symbols-outlined">home</span>
            <span className="e8-nav-label">Home</span>
          </button>
        </div>
        {groups.map((group) => (
          <div className="e8-side-group" key={group.label}>
            <div className="e8-side-group-label">{group.label}</div>
            {group.items.map((it) => (
              <button key={it.id} type="button" className={'e8-nav-item' + (activePage === it.id ? ' active' : '')} aria-current={activePage === it.id ? 'page' : undefined} onClick={() => go(it.id)}>
                <span className="material-symbols-outlined">{it.icon}</span>
                <span className="e8-nav-label">{it.label}</span>
                {liveBadge(it.id, it.badge) ? <span className={'e8-nav-badge' + (it.badgeTone ? ' ' + it.badgeTone : '')}>{liveBadge(it.id, it.badge)}</span> : null}
                {it.soon ? <span className="e8-nav-badge" style={{ background: 'var(--ui-fill)', color: 'var(--ui-text-tertiary)', fontWeight: 500 }}>Soon</span> : null}
              </button>
            ))}
          </div>
        ))}
      </div>
      <div className="e8-side-foot">
        <SyncCard />
        <InstallButton />
        <AppearanceMenu theme={theme} />
        <button className="e8-askai-btn" type="button" onClick={() => { onAskAI && onAskAI(); onClose && onClose(); }}>
          <span className="material-symbols-outlined" style={{ fontSize: 16 }}>forum</span>
          Ask AI
          <span className="e8-kbd">⌘J</span>
        </button>
        <div className="e8-user-row">
          <Avatar name={window.E8DATA.user.name} size="md" />
          <div>
            <div className="e8-user-name">{window.E8DATA.user.name}</div>
            <div className="e8-user-role">{window.E8DATA.user.org}</div>
          </div>
        </div>
      </div>
    </nav>
  );
}

/* ---------- Shared mobile primitives ---------- */

/* True when the viewport is at/under the mobile breakpoint (mirrors app.css 840px). */
function useIsMobile(bp = 840) {
  const q = '(max-width: ' + bp + 'px)';
  const [m, setM] = React.useState(() => !!(window.matchMedia && window.matchMedia(q).matches));
  React.useEffect(() => {
    if (!window.matchMedia) return;
    const mq = window.matchMedia(q);
    const fn = (e) => setM(e.matches);
    mq.addEventListener ? mq.addEventListener('change', fn) : mq.addListener(fn);
    return () => { mq.removeEventListener ? mq.removeEventListener('change', fn) : mq.removeListener(fn); };
  }, [q]);
  return m;
}

/* Drag-down-to-dismiss for bottom sheets. Touch-only - zero desktop impact.
   Spread `bind` on the grab handle + sheet head; apply `style` to the sheet element. */
function useSheetDrag(onClose) {
  const [dragY, setDragY] = React.useState(0);
  const start = React.useRef(null);
  const cur = React.useRef(0);
  const set = (y) => { cur.current = y; setDragY(y); };
  const onTouchStart = (e) => { start.current = e.touches[0].clientY; };
  const onTouchMove = (e) => {
    if (start.current == null) return;
    set(Math.max(0, e.touches[0].clientY - start.current));
  };
  const onTouchEnd = () => {
    const d = cur.current;
    start.current = null;
    set(0);
    if (d > 110) onClose();
  };
  const style = dragY ? { transform: 'translateY(' + dragY + 'px)', transition: 'none' } : null;
  return { dragY, style, bind: { onTouchStart, onTouchMove, onTouchEnd } };
}

/* Swipe-to-triage. Drag right past the threshold commits `left` (advance), drag left
   commits `right` (reject). Reveal pads ramp with distance; past threshold the row "arms"
   (saturates + label flips to Release) with elastic resistance. Release before threshold
   springs back. Tap (move < 8px) -> onTap. Touch-only, axis-locked so vertical scroll is
   never hijacked. The caller removes the row on commit (auto-advance) + offers Undo. */
function SwipeTriage({ left, right, onCommit, onTap, onLongPress, 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 lpTimer = React.useRef(null);
  const lpFired = React.useRef(false);
  const clearLP = () => { if (lpTimer.current) { clearTimeout(lpTimer.current); lpTimer.current = null; } };
  React.useEffect(() => clearLP, []);
  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);
    lpFired.current = false;
    if (onLongPress) { clearLP(); lpTimer.current = setTimeout(() => { lpFired.current = true; if (navigator.vibrate) { try { navigator.vibrate(14); } catch (x) {} } onLongPress(); }, 480); }
  };
  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 (moved.current > 10) clearLP();
    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 (v > 0 && !left) v = 0;
    if (v < 0 && !right) v = 0;
    if (Math.abs(v) > th) v = Math.sign(v) * (th + (Math.abs(v) - th) * 0.38);
    setBoth(v);
  };
  const onTouchEnd = () => {
    clearLP();
    if (!st.current) return;
    const th = st.current.w * 0.42;
    const v = txRef.current;
    st.current = null;
    setDragging(false);
    const dir = (v >= th && left) ? 1 : (v <= -th && right) ? -1 : 0;
    if (dir !== 0) {
      if (navigator.vibrate) { try { navigator.vibrate(12); } catch (x) {} }
      setBoth(dir * (widthOf() + 40));
      window.setTimeout(() => { if (onCommit) onCommit(dir === 1 ? 'advance' : 'reject'); }, 200);
    } else {
      setBoth(0);
    }
  };
  const onClick = () => { if (lpFired.current) { lpFired.current = false; return; } if (moved.current < 8 && onTap) onTap(); };
  const th = widthOf() * 0.42;
  const armed = Math.abs(tx) >= th;
  const pad = (side, cfg) => cfg ? (
    <div className={'e8-triage-pad ' + side + (armed ? ' armed' : '')} style={{ width: Math.max(0, side === 'left' ? tx : -tx), background: cfg.bg }}>
      <span className="material-symbols-outlined">{cfg.icon}</span>
      <span className="e8-triage-lbl">{armed ? 'Release' : cfg.label}</span>
    </div>
  ) : null;
  return (
    <div className="e8-triage" ref={wrapRef}>
      {pad('left', left)}
      {pad('right', right)}
      <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>
  );
}

/* Detent bottom sheet (mobile). Snaps between half and full; drag the handle/header to
   move it; fling down or drag below the half line to dismiss. Scrim opacity ramps with
   openness so the list behind stays bright at half. Touch-drag + reduced-motion aware. */
function BottomSheet({ open, onClose, title, children, footer, initial }) {
  const [detent, setDetent] = React.useState(initial || 'half');
  const [dragY, setDragY] = React.useState(null);
  const drag = React.useRef(null);
  const dlgRef = React.useRef(null);
  useDialog(open, dlgRef);
  const H = () => window.innerHeight || 800;
  const detentY = (d) => d === 'full' ? 0 : Math.round(H() * 0.42);
  const closedY = () => Math.round(H() * 0.92) + 48;
  React.useEffect(() => { if (open) { setDetent(initial || 'half'); setDragY(null); } }, [open]);
  React.useEffect(() => {
    if (!open) return;
    const onKey = (e) => { if (e.key === 'Escape') onClose(); };
    document.addEventListener('keydown', onKey);
    return () => document.removeEventListener('keydown', onKey);
  }, [open, onClose]);
  const baseY = open ? detentY(detent) : closedY();
  const curY = (dragY != null) ? dragY : baseY;
  const onStart = (e) => { const t = e.touches[0]; const now = window.performance ? performance.now() : 0; drag.current = { y: t.clientY, base: baseY, ly: t.clientY, lt: now, v: 0 }; };
  const onMove = (e) => {
    if (!drag.current) return;
    const t = e.touches[0];
    const now = window.performance ? performance.now() : 0;
    const ny = drag.current.base + (t.clientY - drag.current.y);
    const dt = now - drag.current.lt;
    if (dt > 0) drag.current.v = (t.clientY - drag.current.ly) / dt;
    drag.current.ly = t.clientY; drag.current.lt = now;
    setDragY(ny < 0 ? ny * 0.4 : ny);
  };
  const onEnd = () => {
    if (!drag.current) return;
    const v = drag.current.v;
    const y = (dragY != null) ? dragY : baseY;
    drag.current = null;
    const half = detentY('half');
    if (v > 0.6 || y > half + H() * 0.16) { setDragY(null); onClose(); return; }
    let target = 'half';
    if (v < -0.4) target = 'full';
    else if (v > 0.4) target = 'half';
    else target = (y < half / 2) ? 'full' : 'half';
    setDetent(target); setDragY(null);
  };
  const prog = Math.max(0, Math.min(1, (closedY() - curY) / closedY()));
  /* Element opacity 0->1; the scrim's darkness (alpha) lives in the --ui-scrim token. */
  const scrim = open ? Number((prog * prog).toFixed(3)) : 0;
  const bind = { onTouchStart: onStart, onTouchMove: onMove, onTouchEnd: onEnd };
  return (
    <React.Fragment>
      <div className={'e8-bsheet-backdrop' + (open ? ' is-open' : '')} style={{ opacity: scrim }} onClick={onClose} aria-hidden={!open}></div>
      <div ref={dlgRef} className={'e8-bsheet' + (dragY != null ? ' dragging' : '')} role="dialog" aria-modal="true" aria-label={title || 'Details'} aria-hidden={!open}
        style={{ transform: 'translateY(' + curY + 'px)' }}>
        <div className="e8-bsheet-grab" {...bind}></div>
        {title ? (
          <div className="e8-bsheet-head" {...bind}>
            <span className="e8-bsheet-title">{title}</span>
            <button type="button" className="e8-bsheet-close" aria-label="Close" onClick={onClose}><span className="material-symbols-outlined">close</span></button>
          </div>
        ) : null}
        <div className="e8-bsheet-body">{children}</div>
        {footer ? <div className="e8-bsheet-foot">{footer}</div> : null}
      </div>
    </React.Fragment>
  );
}

/* Swipe horizontally to move between record tabs (mobile). Spread `bind` on the tab-panel
   wrapper and merge `style`; no-ops on desktop. Axis-locked so vertical scroll stays free. */
function useSwipeTabs(tabs, active, onChange) {
  const isMobile = useIsMobile();
  const [dx, setDx] = React.useState(0);
  const dxRef = React.useRef(0);
  const st = React.useRef(null);
  const idx = tabs.indexOf(active);
  const set = (v) => { dxRef.current = v; setDx(v); };
  const onTouchStart = (e) => { const t = e.touches[0]; st.current = { x: t.clientX, y: t.clientY, axis: null }; };
  const onTouchMove = (e) => {
    if (!st.current) return;
    const t = e.touches[0];
    const ddx = t.clientX - st.current.x;
    const ddy = t.clientY - st.current.y;
    if (!st.current.axis) st.current.axis = Math.abs(ddx) > Math.abs(ddy) + 6 ? 'x' : 'y';
    if (st.current.axis !== 'x') return;
    e.preventDefault();
    let v = ddx;
    if ((idx <= 0 && v > 0) || (idx >= tabs.length - 1 && v < 0)) v *= 0.3;
    set(v);
  };
  const onTouchEnd = () => {
    if (!st.current) return;
    const v = dxRef.current; const ax = st.current.axis; st.current = null;
    if (ax === 'x') {
      let changed = false;
      if (v <= -56 && idx < tabs.length - 1) { onChange(tabs[idx + 1]); changed = true; }
      else if (v >= 56 && idx > 0) { onChange(tabs[idx - 1]); changed = true; }
      if (changed && navigator.vibrate) { try { navigator.vibrate(8); } catch (e) {} }
    }
    set(0);
  };
  if (!isMobile) return { bind: {}, style: null };
  return { bind: { onTouchStart, onTouchMove, onTouchEnd }, style: { touchAction: 'pan-y', transform: dx ? 'translateX(' + dx + 'px)' : undefined, transition: dx ? 'none' : 'transform .2s ease' } };
}

/* Pull down at the top of a list to sync (mobile). Hooks the main scroll container; the
   screen renders the fixed indicator from { pull, refreshing }. Simulated - no live fetch. */
function usePullToRefresh(onRefresh) {
  const isMobile = useIsMobile();
  const cbRef = React.useRef(onRefresh);
  cbRef.current = onRefresh;
  const [pull, setPull] = React.useState(0);
  const [refreshing, setRefreshing] = React.useState(false);
  const pullRef = React.useRef(0);
  const refRef = React.useRef(false);
  const st = React.useRef(null);
  const setPullBoth = (v) => { pullRef.current = v; setPull(v); };
  const setRefBoth = (v) => { refRef.current = v; setRefreshing(v); };
  React.useEffect(() => {
    if (!isMobile) return;
    const sc = document.querySelector('.e8-content');
    if (!sc) return;
    const TH = 72;
    const onStart = (e) => { st.current = (sc.scrollTop <= 0 && !refRef.current) ? { y: e.touches[0].clientY } : null; };
    const onMove = (e) => {
      if (!st.current || refRef.current) return;
      const dy = e.touches[0].clientY - st.current.y;
      if (dy <= 0 || sc.scrollTop > 0) { if (pullRef.current) setPullBoth(0); return; }
      e.preventDefault();
      setPullBoth(Math.min(110, dy * 0.5));
    };
    const finish = () => {
      if (!st.current) return;
      st.current = null;
      if (pullRef.current >= TH) {
        if (navigator.vibrate) { try { navigator.vibrate(10); } catch (e) {} }
        setRefBoth(true); setPullBoth(64);
        Promise.resolve(cbRef.current && cbRef.current()).then(() => { setRefBoth(false); setPullBoth(0); });
      } else { setPullBoth(0); }
    };
    sc.addEventListener('touchstart', onStart, { passive: true });
    sc.addEventListener('touchmove', onMove, { passive: false });
    sc.addEventListener('touchend', finish, { passive: true });
    sc.addEventListener('touchcancel', finish, { passive: true });
    return () => { sc.removeEventListener('touchstart', onStart); sc.removeEventListener('touchmove', onMove); sc.removeEventListener('touchend', finish); sc.removeEventListener('touchcancel', finish); };
  }, [isMobile]);
  return { pull, refreshing };
}

/* Dialog a11y for an always-mounted overlay: move focus in on open, trap Tab within,
   restore focus to the trigger on close, and mark the container `inert` while closed so
   its controls leave the tab order + a11y tree. Spread nothing - pass (open, ref). */
function useDialog(open, ref) {
  /* Capture the opener SYNCHRONOUSLY during the render that opens the dialog - before React's
     autoFocus (commit phase) or the move-in below pulls focus into the dialog. Capturing in the
     effect below is too late: an autoFocus field is already document.activeElement by then, so
     close would "restore" to that unmounting field and drop focus to <body>. Guarded to the first
     open render (prevFocus stays null until then) and reset on close so a re-open re-captures. */
  const prevFocus = React.useRef(null);
  if (open && prevFocus.current === null && typeof document !== 'undefined') {
    const ae = document.activeElement;
    prevFocus.current = (ae && ae !== document.body) ? ae : null;
  }
  React.useEffect(() => {
    const node = ref.current;
    if (!node) return;
    try { node.inert = !open; } catch (e) {}
    if (!open) { prevFocus.current = null; return; }
    const SEL = 'a[href],button:not([disabled]),input:not([disabled]),textarea:not([disabled]),select:not([disabled]),[tabindex]:not([tabindex="-1"])';
    const list = () => Array.prototype.slice.call(node.querySelectorAll(SEL)).filter((el) => el.offsetWidth > 0 || el.offsetHeight > 0 || el === document.activeElement);
    const f = list();
    /* R98 fix: if an autoFocus already landed inside the dialog, keep it - refocusing the first
       focusable (usually the header close button) blurred the input and tripped its required
       error. Otherwise prefer the first FORM CONTROL over buttons/links so create dialogs open
       on their first field rather than on close. */
    if (!node.contains(document.activeElement)) {
      const ctrl = f.find((el) => /^(INPUT|SELECT|TEXTAREA)$/.test(el.tagName)) || f.find((el) => el.tagName === 'BUTTON') || f[0];
      if (ctrl) { try { ctrl.focus(); } catch (e) {} } else { try { node.focus(); } catch (e) {} }
    }
    const onKey = (e) => {
      if (e.key !== 'Tab') return;
      const items = list();
      if (!items.length) return;
      const a = items[0], b = items[items.length - 1];
      if (e.shiftKey && document.activeElement === a) { e.preventDefault(); b.focus(); }
      else if (!e.shiftKey && document.activeElement === b) { e.preventDefault(); a.focus(); }
    };
    node.addEventListener('keydown', onKey);
    /* Restore focus to the opener on close/unmount. Guard if it's gone (isConnected): a dialog
       opened from a menu item whose trigger unmounted has no live opener to return to. */
    return () => {
      node.removeEventListener('keydown', onKey);
      const p = prevFocus.current;
      prevFocus.current = null;
      if (p && p.isConnected && p.focus) { try { p.focus(); } catch (e) {} }
    };
  }, [open]);
}

/* Reusable confirmation for destructive actions. Focus-trapped, Esc cancels, Enter confirms.
   Use via the useConfirm() hook: const { confirm, dialog } = useConfirm(); render {dialog}; call
   confirm({ title, body, confirmLabel, tone, icon, onConfirm }). */
function ConfirmDialog({ title, body, confirmLabel = 'Confirm', cancelLabel = 'Cancel', tone = 'danger', icon = 'warning', onConfirm, onClose }) {
  const ref = React.useRef(null);
  useDialog(true, ref);
  React.useEffect(() => {
    const onKey = (e) => { if (e.key === 'Escape') { e.preventDefault(); onClose(); } else if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) { e.preventDefault(); onConfirm(); } };
    document.addEventListener('keydown', onKey);
    return () => document.removeEventListener('keydown', onKey);
  }, [onClose, onConfirm]);
  return (
    <div className="e8-overlay" onMouseDown={(e) => { if (e.target === e.currentTarget) onClose(); }}>
      <div ref={ref} className="e8-reject e8-confirm" role="alertdialog" aria-modal="true" aria-label={title}>
        <div style={{ display: 'flex', gap: 12, padding: '18px 18px 6px' }}>
          <span className={'e8-confirm-glyph ' + tone} aria-hidden="true"><span className="material-symbols-outlined">{icon}</span></span>
          <div style={{ minWidth: 0 }}>
            <div style={{ fontSize: 'var(--ui-text-md)', fontWeight: 600, color: 'var(--ui-text)' }}>{title}</div>
            {body ? <div style={{ fontSize: 'var(--ui-text-base)', lineHeight: 1.5, color: 'var(--ui-text-secondary)', marginTop: 4 }}>{body}</div> : null}
          </div>
        </div>
        <div style={{ display: 'flex', justifyContent: 'flex-end', gap: 8, padding: '12px 16px 16px' }}>
          <DS.Button variant="ghost" size="sm" onClick={onClose}>{cancelLabel}</DS.Button>
          <DS.Button variant={tone === 'danger' ? 'reject' : 'primary'} size="sm" icon={icon} onClick={onConfirm}>{confirmLabel}</DS.Button>
        </div>
      </div>
    </div>
  );
}

function useConfirm() {
  const [state, setState] = React.useState(null);
  const confirm = (opts) => setState(opts);
  const close = () => setState(null);
  const dialog = state
    ? <ConfirmDialog {...state} onClose={close} onConfirm={() => { const fn = state.onConfirm; close(); if (fn) fn(); }} />
    : null;
  return { confirm, dialog };
}

/* ---------- Mobile chrome: top app bar + bottom tab bar ---------- */
function MobileBar({ onMenu, onCmdK, onAskAI, action }) {
  return (
    <header className="e8-mobilebar">
      <button className="e8-mobilebar-btn" type="button" aria-label="Open menu" onClick={onMenu}>
        <span className="material-symbols-outlined">menu</span>
      </button>
      <span className="e8-mobilebar-brand">
        <span className="e8-logomark" aria-hidden="true">8</span>
        <span className="e8-logotype">ELEV8</span>
      </span>
      <span className="e8-mobilebar-actions">
        {action ? (
          <button className="e8-mobilebar-btn e8-mobilebar-ctx" type="button" aria-label={action.label} title={action.label} onClick={action.onClick}>
            <span className="material-symbols-outlined">{action.icon}</span>
            {action.badge ? <span className="e8-mobilebar-ctx-badge">{action.badge}</span> : null}
          </button>
        ) : null}
        <button className="e8-mobilebar-btn" type="button" aria-label="Search" onClick={onCmdK}><span className="material-symbols-outlined">search</span></button>
        <button className="e8-mobilebar-btn" type="button" aria-label="Ask AI" onClick={onAskAI}><span className="material-symbols-outlined">forum</span></button>
      </span>
    </header>
  );
}

/* Re-thought for a recruiter on the road: the 4 surfaces they live in + capture in the
   centre. Everything else (Jobs, Clients, Config...) is one tap away via the top-bar menu. */
function MobileTabBar({ route, menuOpen, onCreate, onMore }) {
  const active = activePageOf(route);
  const D = window.E8DATA;
  useBusVersion();
  const unread = window.E8Comms ? window.E8Comms.unreadCount() : D.conversations.filter((c) => c.unread).length;
  const left = [{ id: 'home', icon: 'home', label: 'Home' }, { id: 'today', icon: 'checklist', label: 'Work' }];
  const right = [{ id: 'inbox', icon: 'inbox', label: 'Inbox', badge: unread }, { id: 'more', icon: 'more_horiz', label: 'More' }];
  const tab = (t) => (
    <button key={t.id} type="button" className={'e8-tab' + (((t.id === 'more' && menuOpen) || (active === t.id && !menuOpen)) ? ' active' : '')} onClick={() => t.id === 'more' ? onMore() : navigate(t.id)}>
      <span className="e8-tab-ic">
        <span className="material-symbols-outlined">{t.icon}</span>
        {t.badge ? <span className="e8-tab-badge">{t.badge > 9 ? '9+' : t.badge}</span> : null}
      </span>
      <span className="e8-tab-label">{t.label}</span>
    </button>
  );
  return (
    <nav className="e8-tabbar">
      {left.map(tab)}
      <button type="button" className="e8-tab e8-tab-create" aria-label="Create" onClick={onCreate}>
        <span className="e8-tab-fab"><span className="material-symbols-outlined">add</span></span>
      </button>
      {right.map(tab)}
    </nav>
  );
}

/* ---------- Mobile menu (bottom sheet) - the full nav, mobile-grade ---------- */
function MobileMenu({ open, onClose, route, onAskAI, onCmdK, theme }) {
  const active = activePageOf(route);
  useBusVersion();
  useWorkspaceVersion();
  const pref = window.E8Workspace.get(workspacePersona().name);
  const groups = orderedNavGroups(pref);
  const go = (id) => { navigate(id); onClose(); };
  const drag = useSheetDrag(onClose);
  const mref = React.useRef(null);
  useDialog(open, mref);
  return (
    <React.Fragment>
      <div className={'e8-msheet-backdrop' + (open ? ' is-open' : '')} onClick={onClose}></div>
      <div ref={mref} className={'e8-msheet' + (open ? ' is-open' : '')} role="dialog" aria-modal="true" aria-label="Menu" aria-hidden={!open} style={drag.style}>
        <div className="e8-msheet-grab" {...drag.bind} onClick={onClose}></div>
        <div className="e8-msheet-head" {...drag.bind}>
          <Avatar name={window.E8DATA.user.name} size="lg" />
          <div style={{ flex: 1, minWidth: 0 }}>
            <div className="e8-user-name">{window.E8DATA.user.name}</div>
            <div className="e8-user-role">{window.E8DATA.user.org}</div>
          </div>
          <button className="e8-msheet-close" type="button" aria-label="Close" onClick={onClose}><span className="material-symbols-outlined">close</span></button>
        </div>
        <button className="e8-msheet-search" type="button" onClick={() => { onCmdK(); onClose(); }}>
          <span className="material-symbols-outlined">search</span>
          Search anything
          <span className="e8-kbd">⌘K</span>
        </button>
        <div className="e8-msheet-scroll">
          <div className="e8-msheet-group">
            <button type="button" className={'e8-msheet-item' + (active === 'home' ? ' active' : '')} onClick={() => go('home')}>
              <span className="e8-msheet-ic"><span className="material-symbols-outlined">home</span></span>
              <span className="e8-msheet-label">Home</span>
              <span className="material-symbols-outlined e8-msheet-chev">chevron_right</span>
            </button>
          </div>
          {groups.map((g) => (
            <div className="e8-msheet-group" key={g.label}>
              <div className="e8-side-group-label">{g.label}</div>
              {g.items.map((it) => (
                <button key={it.id} type="button" className={'e8-msheet-item' + (active === it.id ? ' active' : '')} onClick={() => go(it.id)}>
                  <span className="e8-msheet-ic"><span className="material-symbols-outlined">{it.icon}</span></span>
                  <span className="e8-msheet-label">{it.label}</span>
                  {liveBadge(it.id, it.badge) ? <span className={'e8-nav-badge' + (it.badgeTone ? ' ' + it.badgeTone : '')}>{liveBadge(it.id, it.badge)}</span> : null}{it.soon ? <span className="e8-nav-badge" style={{ background: 'var(--ui-fill)', color: 'var(--ui-text-tertiary)', fontWeight: 500 }}>Soon</span> : null}
                  <span className="material-symbols-outlined e8-msheet-chev">chevron_right</span>
                </button>
              ))}
            </div>
          ))}
        </div>
        <div className="e8-msheet-foot">
          <InstallButton />
          <AppearanceMenu theme={theme} />
          <button className="e8-askai-btn" type="button" onClick={() => { onAskAI(); onClose(); }}>
            <span className="material-symbols-outlined" style={{ fontSize: 16 }}>forum</span>
            Ask AI
            <span className="e8-kbd">⌘J</span>
          </button>
        </div>
      </div>
    </React.Fragment>
  );
}

/* ---------- Quick create (sheet) ---------- */
function CreateSheet({ open, onClose }) {
  const { showToast } = React.useContext(E8Ctx);
  const cap = React.useContext(CaptureCtx);
  const run = (fn) => { fn(); onClose(); };
  const items = [
    { icon: 'lab_profile', label: 'New requisition', sub: 'Structured intake form', on: () => window.e8OpenReqForm && window.e8OpenReqForm({}) },
    { icon: 'graphic_eq', label: 'Capture with agent', sub: 'Talk it through', on: () => cap.open({ target: 'req' }) },
    { icon: 'person_add', label: 'Add candidate', sub: 'From a résumé', on: () => cap.open({ target: 'candidate' }) },
    { icon: 'campaign', label: 'New sequence', sub: 'Multi-step outreach', on: () => navigate('sequences/new') },
    { icon: 'mic', label: 'Add note', sub: 'Talk or type a note', on: () => cap.open({ target: 'note' }) },
  ];
  const drag = useSheetDrag(onClose);
  const cref = React.useRef(null);
  useDialog(open, cref);
  return (
    <React.Fragment>
      <div className={'e8-msheet-backdrop' + (open ? ' is-open' : '')} onClick={onClose}></div>
      <div ref={cref} className={'e8-createsheet' + (open ? ' is-open' : '')} role="dialog" aria-modal="true" aria-label="Create" aria-hidden={!open} style={drag.style}>
        <div className="e8-msheet-grab" {...drag.bind} onClick={onClose}></div>
        <div className="e8-createsheet-title" {...drag.bind}>Create</div>
        {items.map((it) => (
          <button key={it.label} type="button" className="e8-create-item" onClick={() => run(it.on)}>
            <span className="e8-create-ic"><span className="material-symbols-outlined">{it.icon}</span></span>
            <span style={{ flex: 1, minWidth: 0, textAlign: 'left' }}>
              <span className="e8-create-label">{it.label}</span>
              <span className="e8-create-sub">{it.sub}</span>
            </span>
            <span className="material-symbols-outlined e8-msheet-chev">chevron_right</span>
          </button>
        ))}
      </div>
    </React.Fragment>
  );
}

/* ---------- Topbar / page header ---------- */
function Topbar({ crumbs = [], actions }) {
  return (
    <div className="e8-topbar">
      <div className="e8-crumb">
        {crumbs.map((c, i) => (
          <React.Fragment key={i}>
            {i > 0 ? <span className="material-symbols-outlined">chevron_right</span> : null}
            {i === crumbs.length - 1 ? <b>{c.label}</b> : <a onClick={() => c.to && navigate(c.to)}>{c.label}</a>}
          </React.Fragment>
        ))}
      </div>
      <div className="e8-topbar-actions">{actions}</div>
    </div>
  );
}

/* Shared in-page header: canonical title (.e8-h1) + optional subtitle + right-aligned actions.
   Adopt across screens to kill the h1-size / header-layout drift. */
function PageHead({ title, sub, actions, children }) {
  return (
    <div className="e8-pagehead">
      <div className="e8-pagehead-main">
        <h1 className="e8-h1">{title}</h1>
        {sub ? <div className="e8-pagehead-sub">{sub}</div> : null}
        {children}
      </div>
      {actions ? <div className="e8-pagehead-actions">{actions}</div> : null}
    </div>
  );
}

/* Shared RECORD-detail header: mark/avatar + title row (with status badges) + meta line + optional
   right-aligned actions. One layout so candidate/client/contact/consultant/CSM headers stop drifting. */
function RecordHeader({ mark, title, badges, meta, actions }) {
  return (
    <div className="e8-rechead">
      {mark ? <div className="e8-rechead-mark">{mark}</div> : null}
      <div className="e8-rechead-main">
        <div className="e8-rechead-titlerow">
          <h1 className="e8-h1">{title}</h1>
          {badges}
        </div>
        {meta ? <div className="e8-rechead-meta">{meta}</div> : null}
      </div>
      {actions ? <div className="e8-rechead-actions">{actions}</div> : null}
    </div>
  );
}

/* ---------- Unbuilt module state ---------- */
function ComingSoon({ label }) {
  const { EmptyState } = DS;
  return (
    <div className="e8-page" style={{ display: 'flex', justifyContent: 'center', paddingTop: 96 }}>
      <div style={{ width: 420 }}>
        <EmptyState
          icon="design_services"
          title={label + ' is on the roadmap'}
          body="This area is scoped for a later round. The core ATS, CRM and AI surfaces are live - explore from the sidebar, or press ⌘K to jump anywhere."
          cta={<Button variant="secondary" size="sm" className="e8-empty-cta" onClick={() => navigate('dashboard')}>Back to dashboard</Button>}
        />
      </div>
    </div>
  );
}

/* ---------- Ask AI panel ---------- */
const ASK_SUGGESTIONS = {
  job: ['Summarize this job’s pipeline health', 'Draft a submittal note for Daniel Okafor', 'Why is Priya Raman ranked below Daniel?'],
  candidate: ['Summarize Daniel’s screening call', 'Draft outreach for similar profiles', 'Compare Daniel with Aisha Patel'],
  clients: ['Which clients are cooling off?', 'Draft a QBR agenda for FedEx', 'Who hasn’t been touched in two weeks?'],
  client: ['Summarize this relationship', 'Draft a QBR agenda', 'What’s at risk here right now?'],
  sequences: ['Which sequence gets the best replies?', 'Draft a new step for quiet sponsors', 'Who replied this week?'],
  contacts: ['Who should I re-engage this week?', 'Draft a check-in for Derek Lin', 'Which sponsors are slowing approvals?'],
  contact: ['Summarize this relationship', 'Draft an email to this contact', 'What are they waiting on from us?'],
  inbox: ['Summarize my unread messages', 'Draft a reply to Daniel Okafor', 'Which threads are waiting on me?'],
  approvals: ['What’s safe to approve in one pass?', 'Which drafts touch FedEx?', 'Summarize what the agents did overnight'],
  notes: ['Summarize this week’s intake calls', 'What did Janet ask for in the kickoff?', 'Draft a note from my last call'],
  agents: ['What did the agents do today?', 'Where should I raise autonomy?', 'What’s waiting on my review?'],
  engagements: ['Which renewals are at risk?', 'Draft Omar Haddad’s escalation', 'Summarize care-call sentiment this month'],
  renewals: ['Which renewals are at risk?', 'Draft a renewal email for Devon Carter', 'What rates changed since last term?'],
  submissions: ['Which submittals need a nudge?', 'Summarize client feedback this week', 'Draft prep notes for Carla’s interview'],
  voice: ['Summarize today’s screening calls', 'Who qualified this morning?', 'Queue callbacks for missed calls'],
  default: ['What needs my attention today?', 'Which renewals are at risk?', 'Draft a morning update for my pod'],
};

function AskAIPanel({ route, onClose, open }) {
  const persona = window.e8ActivePersona ? window.e8ActivePersona() : window.E8DATA.user;
  const personaName = persona.name || 'there';
  const welcomeMessage = () => ({ who: 'bot', text: `Hi ${personaName.split(' ')[0]} - I can help with the record you’re viewing. Replies identify their source context.` });
  const [msgs, setMsgs] = React.useState([welcomeMessage()]);
  const [val, setVal] = React.useState('');
  const aref = React.useRef(null);
  React.useEffect(() => {
    setMsgs([welcomeMessage()]);
    setVal('');
  }, [personaName]);
  useDialog(open, aref);
  React.useEffect(() => {
    if (!open) return;
    const onKey = (e) => { if (e.key === 'Escape') onClose(); };
    document.addEventListener('keydown', onKey);
    return () => document.removeEventListener('keydown', onKey);
  }, [open]);
  const context =
    route.page === 'job' ? (route.id || 'JO-44219') + ' · job order'
    : route.page === 'jobs' ? 'Job orders'
    : route.page === 'candidate' ? 'Daniel Okafor · candidate'
    : route.page === 'voice' ? 'Voice ops'
    : route.page === 'clients' ? 'Clients'
    : route.page === 'client' ? 'Client record'
    : route.page === 'sequences' ? 'Sequences'
    : route.page === 'contacts' || route.page === 'contact' ? 'Contacts'
    : route.page === 'inbox' ? 'Inbox'
    : route.page === 'approvals' ? 'Approvals'
    : route.page === 'notes' ? 'Notes'
    : route.page === 'agents' || route.page === 'matching' ? 'Agents'
    : route.page === 'engagements' ? 'Engagements'
    : route.page === 'renewals' ? 'Renewals'
    : route.page === 'submissions' ? 'Submissions'
    : 'Workspace';
  const suggestions = ASK_SUGGESTIONS[route.page === 'jobs' ? 'job' : route.page] || ASK_SUGGESTIONS.default;
  const send = (text) => {
    if (!text.trim()) return;
    setMsgs((m) => [
      ...m,
      { who: 'user', text },
      { who: 'bot', text: 'Drafted - sources are the records on this page, cited inline. Review before it’s used anywhere; nothing sends without your approval.' },
    ]);
    setVal('');
  };
  return (
    <div className={'e8-ai-overlay' + (open ? ' is-open' : '')} onClick={onClose} aria-hidden={open ? undefined : true}>
    <aside ref={aref} className="e8-ai-panel" role="dialog" aria-modal="true" aria-label="Ask AI" onClick={(e) => e.stopPropagation()}>
      <div className="e8-ai-head">
        <span className="material-symbols-outlined" style={{ fontSize: 17 }}>forum</span>
        <b>Ask AI</b>
        <span style={{ marginLeft: 'auto' }}>
          <Button variant="ghost" size="sm" icon="close" iconOnly title="Close panel" onClick={onClose}></Button>
        </span>
      </div>
      <div className="e8-ai-body">
        <div className="e8-ai-context">
          <span className="material-symbols-outlined" style={{ fontSize: 13 }}>my_location</span>
          Acting on: {context}
        </div>
        {msgs.map((m, i) => (
          <div key={i} className={'e8-ai-msg ' + m.who}>{m.text}</div>
        ))}
        {msgs.length <= 1 ? (
          <div style={{ display: 'flex', flexDirection: 'column', gap: 6, marginTop: 4 }}>
            {suggestions.map((s) => (
              <button key={s} type="button" className="e8-ai-suggest" onClick={() => send(s)}>{s}</button>
            ))}
          </div>
        ) : null}
      </div>
      <div className="e8-ai-foot">
        <DS.Input
          icon="forum"
          placeholder="Ask about this record…"
          value={val}
          onChange={(e) => setVal(e.target.value)}
          onKeyDown={(e) => { if (e.key === 'Enter') send(val); }}
        />
        <button type="button" className="e8-ai-universal" onClick={() => { onClose(); openHomeCommand(val.trim()); }}>
          <span className="material-symbols-outlined">auto_awesome</span>
          Open universal command
        </button>
      </div>
    </aside>
    </div>
  );
}

/* ---------- Command palette ---------- */
const CMDK_ITEMS = [
  { group: 'Ask ELEV8', icon: 'auto_awesome', label: 'Open the universal command', act: 'home-command' },
  { group: 'Navigate', icon: 'home', label: 'Home - command canvas', to: 'home' },
  { group: 'Navigate', icon: 'checklist', label: 'Work queue - priorities and daily brief', to: 'today' },
  { group: 'Navigate', icon: 'task_alt', label: 'Tasks - your queue', to: 'tasks' },
  { group: 'Navigate', icon: 'space_dashboard', label: 'Dashboard', to: 'dashboard' },
  { group: 'Navigate', icon: 'work', label: 'Job orders', to: 'jobs' },
  { group: 'Navigate', icon: 'lab_profile', label: 'Job intake queue', to: 'jobs/intake', hint: String((window.E8DATA.intake || []).length || '') || undefined },
  { group: 'Navigate', icon: 'move_to_inbox', label: 'Applicants - inbound triage', to: 'applicants', hint: NEW_APPLICANTS ? NEW_APPLICANTS + ' new' : undefined },
  { group: 'Navigate', icon: 'group', label: 'Candidates', to: 'candidates' },
  { group: 'Navigate', icon: 'content_copy', label: 'Duplicates - review possible matches', to: 'duplicates', hint: DUP_PAIRS ? String(DUP_PAIRS) : undefined },
  { group: 'Navigate', icon: 'send', label: 'Submissions', to: 'submissions' },
  { group: 'Navigate', icon: 'apartment', label: 'Clients', to: 'clients' },
  { group: 'Navigate', icon: 'campaign', label: 'Sequences', to: 'sequences' },
  { group: 'Navigate', icon: 'contacts', label: 'Contacts', to: 'contacts' },
  { group: 'Navigate', icon: 'inbox', label: 'Inbox - messages', to: 'inbox', hint: '3' /* R78: overridden live at render */ },
  { group: 'Navigate', icon: 'fact_check', label: 'Approvals - needs review', to: 'approvals', hint: '5' /* R78: overridden live at render */ },
  { group: 'Navigate', icon: 'sticky_note_2', label: 'Notes', to: 'notes' },
  { group: 'Navigate', icon: 'smart_toy', label: 'Agents', to: 'agents' },
  { group: 'Navigate', icon: 'call', label: 'Voice ops', to: 'voice', hint: VOICE_LIVE ? VOICE_LIVE + ' live' : undefined },
  { group: 'Navigate', icon: 'auto_awesome', label: 'Enrichment', to: 'enrichment' },
  { group: 'Navigate', icon: 'badge', label: 'Engagements', to: 'engagements' },
  { group: 'Navigate', icon: 'description', label: 'Templates', to: 'templates' },
  { group: 'Navigate', icon: 'autorenew', label: 'Renewals', to: 'renewals' },
  { group: 'Navigate', icon: 'monitoring', label: 'Reports', to: 'reports' },
  { group: 'Navigate', icon: 'payments', label: 'Commissions', to: 'commissions' },
  { group: 'Navigate', icon: 'schedule', label: 'Time', to: 'time' },
  { group: 'Navigate', icon: 'engineering', label: 'Consultants', to: 'consultants' },
  { group: 'Navigate', icon: 'support_agent', label: 'CSMs - client success', to: 'csms' },
  { group: 'Navigate', icon: 'join_inner', label: 'Matching engine', to: 'matching' },
  { group: 'Navigate', icon: 'payments', label: 'Rate library', to: 'rates' },
  { group: 'Navigate', icon: 'sync', label: 'Sync queue', to: 'syncqueue' },
  { group: 'Navigate', icon: 'history', label: 'Audit log', to: 'audit' },
  { group: 'Configure', icon: 'input', label: 'Fields - custom field schema', to: 'fields' },
  { group: 'Configure', icon: 'linear_scale', label: 'Stages - pipeline configuration', to: 'stages' },
  { group: 'Configure', icon: 'schema', label: 'Workflows - automations', to: 'workflows' },
  { group: 'Configure', icon: 'list_alt', label: 'Forms - intake & feedback', to: 'forms' },
  { group: 'Configure', icon: 'palette', label: 'Components & tokens', to: 'system' },
  { group: 'Records', icon: 'person', label: 'Daniel Okafor - Sr. Software Engineer @ Optum', to: 'candidate/c-okafor' },
  { group: 'Records', icon: 'person', label: 'Aisha Patel - Sr. Software Engineer @ Capital One', to: 'candidate/c-patel' },
  { group: 'Records', icon: 'work', label: 'JO-44219 - Senior Java Developer · FedEx', to: 'job/JO-44219/matching' },
  { group: 'Records', icon: 'work', label: 'JO-44087 - Scrum Master · ALSAC (at risk)', to: 'job/JO-44087/overview' },
  { group: 'Records', icon: 'apartment', label: 'FedEx - strategic client', to: 'client/cl-fedex' },
  { group: 'Records', icon: 'contacts', label: 'Janet Moss - hiring manager @ FedEx', to: 'contact/ct-moss' },
  { group: 'Records', icon: 'contacts', label: 'Maya Singh - feedback owner @ International Paper', to: 'contact/ct-singh' },
  { group: 'AI actions', icon: 'mail', label: 'Approve the drafted nudge to Maya Singh', to: 'approvals' },
  { group: 'AI actions', icon: 'send', label: 'Approve Daniel Okafor’s submittal packet', to: 'approvals' },
  { group: 'AI actions', icon: 'forum', label: 'Reply to Daniel Okafor - WhatsApp', to: 'inbox/cv-okafor' },
  { group: 'AI actions', icon: 'call', label: 'Start a screening batch…', to: 'voice' },
  { group: 'AI actions', icon: 'join_inner', label: 'Review matches on JO-44219', to: 'job/JO-44219/matching' },
  { group: 'AI actions', icon: 'tune', label: 'Tune agent autonomy', to: 'agents' },
  { group: 'AI actions', icon: 'account_tree', label: 'Open the workflow editor', to: 'workfloweditor' },
  /* Create - real creation flows (R98). `act` runs instead of navigating (handled in CommandPalette). */
  { group: 'Create', icon: 'lab_profile', label: 'New job order', act: 'reqform' },
  { group: 'Create', icon: 'graphic_eq', label: 'Capture job order with agent', act: 'create-req' },
  { group: 'Create', icon: 'person_add', label: 'Add candidate', act: 'create-candidate' },
  { group: 'Create', icon: 'send', label: 'New submission', act: 'create-submission' },
  { group: 'Create', icon: 'badge', label: 'New engagement', act: 'create-engagement' },
  { group: 'Create', icon: 'add_task', label: 'New task', act: 'newtask' },
  /* Workspace actions. */
  { group: 'Actions', icon: 'sync', label: 'Sync from ELEV8', act: 'sync' },
  { group: 'Actions', icon: 'palette', label: 'Switch accent theme', act: 'accent' },
  { group: 'Actions', icon: 'verified', label: 'Toggle platform signals', act: 'signals' },
];

function CommandPalette({ onClose }) {
  const { showToast, theme } = React.useContext(E8Ctx);
  const cap = React.useContext(CaptureCtx);
  const [q, setQ] = React.useState('');
  const [sel, setSel] = React.useState(0);
  const inputRef = React.useRef(null);
  const cmdRef = React.useRef(null);
  useDialog(true, cmdRef);
  React.useEffect(() => { inputRef.current && inputRef.current.focus(); }, []);
  React.useEffect(() => { setSel(0); }, [q]);
  const query = q.trim();
  const items = CMDK_ITEMS.filter((it) => it.label.toLowerCase().includes(query.toLowerCase()));
  if (query) items.push({ group: 'Ask ELEV8', icon: 'auto_awesome', label: `Ask ELEV8: ${query}`, act: 'home-command', prompt: query });
  const groups = [...new Set(items.map((i) => i.group))];
  /* Flat order matches render order (grouped), so ↑↓ tracks what's on screen. */
  const flat = groups.flatMap((g) => items.filter((i) => i.group === g));
  const go = (it) => {
    if (it.act === 'home-command') {
      openHomeCommand(it.prompt || '');
    } else if (it.act === 'reqform') {
      if (window.e8OpenReqForm) window.e8OpenReqForm({});
    } else if (it.act === 'create-req') {
      cap.open({ target: 'req' });
    } else if (it.act === 'create-candidate') {
      cap.open({ target: 'candidate' });
    } else if (it.act === 'create-submission' || it.act === 'create-engagement') {
      if (window.e8OpenCreate) window.e8OpenCreate(it.act.replace('create-', ''));
    } else if (it.act === 'newtask') {
      // R98: land on #/tasks with the quick-add focused. The flag covers the not-yet-mounted
      // case (TasksScreen consumes it on mount); the event covers the already-mounted one.
      window.__e8TaskAddPending = true;
      navigate('tasks');
      try { window.dispatchEvent(new CustomEvent('e8-task-add')); } catch (e) {}
    } else if (it.act === 'sync') {
      e8SyncNow(showToast);
    } else if (it.act === 'accent') {
      if (theme && (theme.themes || []).length) {
        const keys = theme.themes.map((x) => x.key);
        const next = theme.themes[(keys.indexOf(theme.accent) + 1) % keys.length];
        theme.setAccent(next.key);
        if (showToast) showToast('Accent theme: ' + next.name);
      }
    } else if (it.act === 'signals') {
      const on = !e8SignalsOn();
      e8SetSignals(on);
      if (showToast) showToast(on ? 'Platform signals on' : 'Platform signals off');
    } else {
      navigate(it.to);
    }
    onClose();
  };
  return (
    <div className="e8-overlay" onMouseDown={(e) => { if (e.target === e.currentTarget) onClose(); }}>
      <div ref={cmdRef} className="e8-cmdk" role="dialog" aria-modal="true" aria-label="Global search">
        <div className="e8-cmdk-input">
          <span className="material-symbols-outlined" style={{ fontSize: 18 }}>search</span>
          <input
            ref={inputRef}
            placeholder="Search records or ask ELEV8…"
            value={q}
            onChange={(e) => setQ(e.target.value)}
            onKeyDown={(e) => {
              if (e.key === 'Escape') onClose();
              if (e.key === 'ArrowDown') { e.preventDefault(); setSel((s) => Math.min(s + 1, flat.length - 1)); }
              if (e.key === 'ArrowUp') { e.preventDefault(); setSel((s) => Math.max(s - 1, 0)); }
              if (e.key === 'Enter' && flat.length) go(flat[Math.min(sel, flat.length - 1)]);
            }}
          />
          <span className="e8-kbd">esc</span>
        </div>
        <div className="e8-cmdk-list">
          {groups.map((g) => (
            <React.Fragment key={g}>
              <div className="e8-cmdk-group">{g}</div>
              {items.filter((i) => i.group === g).map((it) => (
                <button
                  key={it.label}
                  type="button"
                  className={'e8-cmdk-item' + (flat.indexOf(it) === sel ? ' sel' : '')}
                  onMouseEnter={() => setSel(flat.indexOf(it))}
                  onClick={() => go(it)}
                >
                  <span className="material-symbols-outlined">{it.icon}</span>
                  {it.label}
                  {(() => { const h = (it.to === 'inbox' || it.to === 'approvals') ? liveBadge(it.to) : it.hint; return h ? <span className="e8-cmdk-hint">{h}</span> : null; })()}
                </button>
              ))}
            </React.Fragment>
          ))}
          {!items.length ? <div style={{ padding: '18px 12px', fontSize: 'var(--ui-text-base)', color: 'var(--ui-text-tertiary)' }}>No results for “{q}”</div> : null}
        </div>
      </div>
    </div>
  );
}

/* R80: one-tap notification opt-in for a work surface (Approvals, Time hub).
   Enabling turns on rung-1 local notifications immediately and attempts the real
   Web Push subscription (rung 2) - which activates once the API is deployed. */
function NotifyBell({ audience }) {
  const { showToast } = React.useContext(E8Ctx);
  const [st, setSt] = React.useState(() => (window.E8Notify ? window.E8Notify.state() : { supported: false }));
  React.useEffect(() => (window.E8Events ? window.E8Events.subscribe(['notify:changed'], () => setSt(window.E8Notify.state())) : undefined), []);
  if (!st.supported) return null;
  const on = st.enabled && st.permission === 'granted';
  const click = async () => {
    if (on) { window.E8Notify.disable(); showToast('Notifications off'); return; }
    const res = await window.E8Notify.enable(audience);
    if (res.ok) showToast(res.pushed ? 'Notifications on - real push active' : 'Notifications on for this device');
    else showToast(res.reason === 'denied' ? 'Notifications are blocked - enable them in your browser settings' : 'Notifications not enabled');
  };
  return <DS.Button variant="ghost" size="sm" icon={on ? 'notifications_active' : 'notifications'} iconOnly title={on ? 'Notifications on - click to turn off' : 'Notify me when new items land here'} onClick={click}></DS.Button>;
}

Object.assign(window, {
  E8Ctx, MobileActionCtx, NAV_GROUPS, BUILT, parseHash, navigate, openHomeCommand, useRoute, isPortalRoute,
  Sidebar, Topbar, PageHead, RecordHeader, ComingSoon, AskAIPanel, CommandPalette, InboxShortcut, inboxShortcutState,
  MobileBar, MobileTabBar, MobileMenu, WorkspaceCustomizer, CreateSheet, InstallButton,
  useIsMobile, useSheetDrag, SwipeTriage, BottomSheet, useSwipeTabs, usePullToRefresh, useDialog, useStagesVersion, useWorkspaceVersion, WorkspacePinButton,
  ConfirmDialog, useConfirm, NotifyBell,
});
