/* ELEV8 ATS - the navigation rail's satellite surfaces.
   Split out of app/shell.jsx (R144), unchanged. Everything the sidebar OPENS or composes lives
   here - quick add, the pinned/recent record rows and popovers, the More panel, the workspace
   customizer, the profile menu, the install/sync footers and the collapsed rail's flyout label.
   The rail itself (Sidebar, SidebarRecords) stays in shell.jsx beside the nav model it reads.
   Loaded before app/shell.jsx; every reference across the two files is resolved at RENDER time. */

/* ---------- R124: quick-add (sidebar + A-then-X chord) ---------- */
/* Every entry routes to a REAL flow - no toast stand-ins. Client/contact have no global
   creation host (their dialogs are local state on the CRM screens), so those two use the same
   pending-flag + event handshake the task quick-add already uses: the flag covers the
   not-yet-mounted screen, the event covers the already-mounted one (navigating to the hash you
   are on does not remount). */
const QUICK_ADD_ITEMS = [
  { kind: 'candidate', label: 'Add candidate', icon: 'person_add', key: 'C' },
  { kind: 'job', label: 'Add job order', icon: 'work', key: 'J' },
  { kind: 'client', label: 'Add client', icon: 'apartment', key: 'L' },
  { kind: 'contact', label: 'Add contact', icon: 'contacts', key: 'O' },
  { kind: 'note', label: 'Add note', icon: 'edit_note', key: 'N' },
  { kind: 'task', label: 'Add task', icon: 'check_circle', key: 'T' },
];
function e8QuickCreate(kind, cap) {
  if (kind === 'candidate') { if (window.e8OpenCreate) window.e8OpenCreate('candidate'); return; }
  if (kind === 'job') { if (window.e8OpenReqForm) window.e8OpenReqForm({}); return; }
  if (kind === 'note') { if (cap && cap.open) cap.open({ target: 'note' }); return; }
  if (kind === 'task') {
    /* R125: tasks are composed in the modal, in place - no jump to the tasks screen. The old
       navigate+focus handshake stays as the fallback if the composer script failed to load. */
    if (window.e8OpenTask) { window.e8OpenTask({}); return; }
    window.__e8TaskAddPending = true;
    navigate('today/tasks');
    try { window.dispatchEvent(new CustomEvent('e8-task-add')); } catch (e) {}
    return;
  }
  if (kind === 'client' || kind === 'contact') {
    window.__e8CreatePending = kind;
    navigate(kind === 'client' ? 'clients' : 'contacts');
    try { window.dispatchEvent(new CustomEvent('e8-create-pending')); } catch (e) {}
  }
}

function QuickAddButton({ onClose, railCollapsed }) {
  const cap = React.useContext(CaptureCtx);
  const [open, setOpen] = React.useState(false);
  const ref = React.useRef(null);
  React.useEffect(() => {
    if (!open) return undefined;
    const away = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
    const key = (e) => { if (e.key === 'Escape') setOpen(false); };
    document.addEventListener('mousedown', away);
    document.addEventListener('keydown', key);
    return () => { document.removeEventListener('mousedown', away); document.removeEventListener('keydown', key); };
  }, [open]);
  const run = (kind) => {
    setOpen(false);
    if (onClose) onClose();
    e8QuickCreate(kind, cap);
  };
  return (
    <span className="e8-qadd-wrap" ref={ref}>
      {/* Collapsed, the flyout says "Quick add · Press A" instantly and the OS then stacks
          "Add - press A" over it about a second later - two labels that do not even agree.
          Expanded there is no flyout and the button is icon-only, so the title has to stay. */}
      <button type="button" className="e8-qadd-btn" title={railCollapsed ? undefined : 'Add - press A'} aria-label="Quick add"
        data-e8-tip="Quick add" data-e8-tipsub="Press A" aria-haspopup="menu" aria-expanded={open} onClick={() => setOpen((v) => !v)}>
        <span className="material-symbols-outlined" aria-hidden="true">add_box</span>
      </button>
      {open ? (
        <div className="e8-qadd-menu" role="menu" aria-label="Quick add">
          {QUICK_ADD_ITEMS.map((it) => (
            <button key={it.kind} type="button" role="menuitem" className="e8-qadd-item" onClick={() => run(it.kind)}>
              <span className="material-symbols-outlined" aria-hidden="true">{it.icon}</span>
              <span className="e8-qadd-label">{it.label}</span>
              <span className="e8-qadd-keys" aria-hidden="true"><span className="e8-kbd">A</span><i>then</i><span className="e8-kbd">{it.key}</span></span>
            </button>
          ))}
        </div>
      ) : null}
    </span>
  );
}
/* ---------- R124: sidebar record lists (Pinned + Recent) ---------- */
/* R142 step 1: how many pinned rows the Shortcuts section shows before folding into "View all".
   The block sits ABOVE the destinations, so every row it spends pushes one of them toward the
   fold. Three pins plus one header is four rows - down from ten when this was two sections of
   five and three. Recents are not rendered in the rail at all any more; they live in ⌘K search
   and behind View all. Raising this number is the thing that would quietly undo step 1. */
const PIN_SHOWN = 3;
const E8_RECORD_GLYPH = { job: 'work', client: 'apartment', contact: 'contacts', submission: 'send', task: 'check_circle' };
/* R143: the SINGULAR record kind, for the collapsed rail's flyout. Collapsed, a shortcut row is
   two initials or a generic glyph - "DO" and a briefcase say nothing about what they open - so the
   tip names the record AND what kind of thing it is. Derived from the same type ids the plural
   chips in the See-all popover use (E8_RECENT_TYPE_CHIPS), not a second vocabulary. */
const E8_RECORD_KIND = { candidate: 'Candidate', job: 'Job order', client: 'Client', contact: 'Contact', submission: 'Submission', task: 'Task' };
function e8Initials(label) {
  return String(label || '?').trim().split(/\s+/).slice(0, 2).map((w) => w[0]).join('').toUpperCase();
}
/* Age label for viewedAt timestamps (real Date.now() ms; pre-R124 entries have none). */
function e8ViewedAgo(ts) {
  if (!ts) return '—';
  const s = Math.max(0, (Date.now() - ts) / 1000);
  if (s < 60) return 'now';
  if (s < 3600) return Math.floor(s / 60) + 'm';
  if (s < 86400) return Math.floor(s / 3600) + 'h';
  return Math.floor(s / 86400) + 'd';
}

/* One row anatomy for Pinned and Recent: entity glyph, ellipsised label, and a PERMANENTLY
   reserved pin slot so the control fading in never shifts the text. The row opens the record;
   the pin acts independently. Keyboard: Enter/Space opens, P toggles the pin. Touch (hover:
   none) keeps the pin permanently visible - right-click is not a touch fallback. */
function SidebarRecordRow({ row, current, pinned, onOpen, onTogglePin, onCtx, railCollapsed }) {
  const disabled = !!row.deleted;
  const glyph = row.type === 'candidate' && !disabled
    ? <span className="e8-recrow-glyph" aria-hidden="true">{e8Initials(row.label)}</span>
    : <span className="material-symbols-outlined e8-recrow-ic" aria-hidden="true">{disabled ? 'block' : (E8_RECORD_GLYPH[row.type] || 'description')}</span>;
  return (
    <div
      role="button"
      tabIndex={disabled ? -1 : 0}
      aria-label={'Open ' + row.label + ' - ' + (E8_RECORD_KIND[row.type] || 'record') + (disabled ? ', no longer accessible' : '')}
      aria-disabled={disabled || undefined}
      /* Expanded the title is load-bearing - .e8-recrow-label ellipsises, so it is the only way
         to read a truncated record name. Collapsed the flyout says more than it does. */
      title={railCollapsed ? undefined : (disabled ? row.label + ' — no longer accessible' : row.label)}
      data-e8-tip={row.label}
      /* The sub when there is one - for a job that is now "JO-10864 · Zayo Group", which is
         strictly more useful than the word "Job order" the glyph already conveys. */
      data-e8-tipsub={disabled ? 'No longer accessible' : (row.sub || E8_RECORD_KIND[row.type] || 'Record')}
      className={'e8-recrow' + (current ? ' is-current' : '') + (disabled ? ' is-dead' : '')}
      onClick={() => onOpen(row)}
      onKeyDown={(e) => {
        /* Only act when the ROW itself is focused - Enter/Space on the nested pin button must
           stay native button activation, not open the record. */
        if (e.target !== e.currentTarget) return;
        if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onOpen(row); }
        else if (e.key.toLowerCase() === 'p' && !disabled) { e.preventDefault(); onTogglePin(row); }
      }}
      onContextMenu={onCtx && !disabled ? (e) => { e.preventDefault(); onCtx(row, e.clientX, e.clientY); } : undefined}
    >
      {glyph}
      <span className="e8-recrow-label">{row.label}</span>
      <span className="e8-recrow-pinslot">
        {!disabled ? (
          <button
            type="button"
            className={'e8-recrow-pin' + (pinned ? ' is-on' : '')}
            aria-label={(pinned ? 'Unpin ' : 'Pin ') + row.label}
            title={pinned ? 'Unpin' : 'Pin to sidebar'}
            onClick={(e) => { e.stopPropagation(); onTogglePin(row); }}
          >
            <span className="material-symbols-outlined" aria-hidden="true">{pinned ? 'keep_off' : 'keep'}</span>
          </button>
        ) : null}
      </span>
    </div>
  );
}

const E8_RECENT_TYPE_CHIPS = [
  { id: 'all', label: 'All' },
  { id: 'candidate', label: 'Candidates' },
  { id: 'job', label: 'Jobs' },
  { id: 'client', label: 'Clients' },
  { id: 'contact', label: 'Contacts' },
  { id: 'submission', label: 'Submissions' },
];

/* 312px popover anchored beside the sidebar; closes on outside click and Esc. */
function SidebarPopover({ label, top, onClose, children }) {
  const ref = React.useRef(null);
  React.useEffect(() => {
    const away = (e) => { if (ref.current && !ref.current.contains(e.target)) onClose(); };
    const key = (e) => { if (e.key === 'Escape') onClose(); };
    document.addEventListener('mousedown', away);
    document.addEventListener('keydown', key);
    return () => { document.removeEventListener('mousedown', away); document.removeEventListener('keydown', key); };
  }, [onClose]);
  const clampedTop = Math.max(12, Math.min(top || 120, (window.innerHeight || 800) - 440));
  return (
    <div ref={ref} className="e8-recpop" role="dialog" aria-label={label} style={{ top: clampedTop }}>
      {children}
    </div>
  );
}
/* "Install app" affordance - appears once the browser offers the PWA prompt. */
function InstallButton({ profile = false }) {
  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={profile ? 'e8-profile-tool' : 'e8-install-btn'} type="button" onClick={install}>
      <span className="material-symbols-outlined" style={{ fontSize: 'var(--ui-icon-sm)' }}>install_mobile</span>
      {profile ? <span><b>Install app</b><small>Use ELEV8 from your device</small></span> : 'Install app'}
      {profile ? <span className="material-symbols-outlined" aria-hidden="true">chevron_right</span> : null}
    </button>
  );
}

/* 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' },
];
/* R142 step 2: the More panel - every destination the rail no longer carries permanently.
   The rail became a 6-7 row role launcher, so this is what stops that being a loss: it lists
   EVERY destination the persona may reach, grouped, searchable by name, with the ones already in
   the rail marked. Hidden destinations are recovered here too, which is why the old standalone
   "N hidden · Restore" row could go.
   Search matches the label, so a recruiter who thinks of it as "requisitions" still finds Job
   orders via ⌘K; this panel is for browsing, ⌘K is for jumping. */
function NavMorePanel({ persona, pref, activePage, onClose, onGo }) {
  const [q, setQ] = React.useState('');
  const inputRef = React.useRef(null);
  const panelRef = React.useRef(null);
  /* aria-modal="true" is a promise: focus stays inside, the background is inert, and closing
     returns focus where it came from. useDialog is what actually delivers that here (it sets
     .inert on the rest of the app and restores the opener) - the command palette uses the same
     hook. Declaring the role without it left Tab walking straight out of the dialog into the
     sidebar behind the scrim. */
  useDialog(true, panelRef);
  /* The global G/A navigation chords are armed app-wide from main.jsx and only stand down for the
     command palette and Ask AI. Without this flag, pressing G then C while the panel is open
     navigates behind it and leaves you on a new screen with a modal still over it. Same idiom the
     candidate record's triage mode uses (__e8TriageActive). */
  React.useEffect(() => {
    window.__e8NavMoreOpen = true;
    return () => { window.__e8NavMoreOpen = false; };
  }, []);
  React.useEffect(() => { if (inputRef.current) inputRef.current.focus(); }, []);
  React.useEffect(() => {
    const onKey = (e) => { if (e.key === 'Escape') { e.preventDefault(); onClose(); } };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, [onClose]);

  const W = window.E8Workspace;
  const navPref = (pref && pref.nav) || {};
  const hidden = navPref.hidden || [];
  const extra = navPref.extra || [];
  const roleIds = (W.roleDefaults && W.roleDefaults[persona.role]) || W.roleDefaults.recruiter;
  /* TWO sets, deliberately. `persistent` is what is actually SAVED in the sidebar; `onScreen` adds
     the page you are currently on, which the rail force-shows so it never stops answering "where
     am I". Collapsing them into one made the pin a dead control on exactly that page: sitting on
     Sequences (not a role default, not promoted), `inRail.has('sequences')` was true, so the click
     took the demote branch, which writes `hidden` only for role defaults and filters an `extra`
     the id was never in - two no-ops, and the pin bounced straight back to "on".
     The pin now reflects and edits `persistent`; `onScreen` only drives the "you are here"
     read-out, so promoting the page you are on works and removing it is honest. */
  const persistent = new Set(roleIds.concat(extra).filter((id) => !hidden.includes(id)));
  const onScreen = new Set(persistent);
  if (activePage && activePage !== 'home') onScreen.add(activePage);
  /* Ids the model refuses to remove (normalizeNavHidden strips them), so a pin toggle on them
     would write nothing and silently do nothing. They get a static marker instead of a control. */
  const PINNED_FOREVER = new Set(['today']);

  const query = q.trim().toLowerCase();
  const groups = NAV_GROUPS
    .map((g) => ({
      label: g.label,
      items: g.items
        .filter((it) => navVisibleToPersona(it, persona))
        .filter((it) => !query || it.label.toLowerCase().includes(query) || it.id.includes(query)),
    }))
    .filter((g) => g.items.length);

  /* Promote / demote writes the SAME persisted pref the rail reads, so the panel and the rail can
     never disagree about what is in the sidebar. */
  const toggleRail = (it) => {
    if (persistent.has(it.id)) {
      /* Removing writes `hidden` whenever the id would otherwise come back - that is any role
         default, and also anything left in `extra`. Filtering extra alone was not enough for a
         role default, and writing hidden alone was not enough for a promoted one. */
      const wouldReturn = roleIds.includes(it.id);
      W.setNav(persona.name, {
        order: navPref.order,
        hidden: wouldReturn ? hidden.concat(it.id) : hidden,
        extra: extra.filter((x) => x !== it.id),
      });
      return;
    }
    W.setNav(persona.name, {
      order: navPref.order,
      hidden: hidden.filter((x) => x !== it.id),
      extra: roleIds.includes(it.id) ? extra : extra.concat(it.id),
    });
  };

  return (
    <React.Fragment>
      <div className="e8-navmore-scrim" onMouseDown={onClose}></div>
      <div className="e8-navmore" role="dialog" aria-modal="true" aria-label="All destinations">
        <div className="e8-navmore-head">
          <span className="material-symbols-outlined" aria-hidden="true">search</span>
          <input ref={inputRef} className="e8-navmore-input" type="text" value={q} placeholder="Search all destinations…"
            aria-label="Search all destinations" onChange={(e) => setQ(e.target.value)} />
          <button type="button" className="e8-navmore-close" aria-label="Close" onClick={onClose}>
            <span className="material-symbols-outlined" aria-hidden="true">close</span>
          </button>
        </div>
        <div className="e8-navmore-body">
          {groups.length ? groups.map((g) => (
            /* The caption is the panel's whole organising idea, so it has to be ASSOCIATED with
               its rows, not just painted above them - otherwise a screen reader reads it as loose
               prose between buttons. */
            <div className="e8-navmore-group" key={g.label} role="group" aria-labelledby={'e8-navmore-h-' + g.label}>
              <div className="e8-eyebrow" id={'e8-navmore-h-' + g.label}>{g.label}</div>
              {g.items.map((it) => {
                /* The pin reflects SAVED membership, not the transient "you are here" row - so on
                   the page you are currently viewing it still offers to pin, and pinning sticks. */
                const on = persistent.has(it.id);
                return (
                  <div className="e8-navmore-row" key={it.id}>
                    <button type="button" className="e8-navmore-go" onClick={() => { onGo(it.id); onClose(); }}>
                      <span className="material-symbols-outlined" aria-hidden="true">{it.icon}</span>
                      <span className="e8-navmore-label">{it.label}</span>
                      {it.soon ? <span className="e8-navmore-soon">Soon</span> : null}
                    </button>
                    {PINNED_FOREVER.has(it.id) ? (
                      <span className="e8-navmore-always" title="Always in the sidebar">Always</span>
                    ) : (
                      /* Distinct GLYPHS, not just colour: on hover both states share the same fill
                         background, so a colour-only difference is unreadable at the moment you are
                         about to click. keep/keep_off is the same pair the record pin uses. */
                      <button type="button" className={'e8-navmore-pin' + (on ? ' is-on' : '')}
                        title={on ? 'Remove from sidebar' : 'Keep in sidebar'}
                        aria-pressed={on} aria-label={(on ? 'Remove ' : 'Keep ') + it.label + ' in the sidebar'}
                        onClick={() => toggleRail(it)}>
                        <span className="material-symbols-outlined" aria-hidden="true">{on ? 'keep' : 'keep_off'}</span>
                      </button>
                    )}
                  </div>
                );
              })}
            </div>
          )) : <div className="e8-navmore-empty">Nothing matches “{q.trim()}”.</div>}
        </div>
      </div>
    </React.Fragment>
  );
}
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, persona);
  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);
  };
  /* R142 step 2: this tab has to be a VIEW OVER THE SAME MODEL the rail reads, which is now
     (roleDefaults[role] ∪ nav.extra) − nav.hidden. Writing only `hidden` made it able to subtract
     from the rail but never add to it, so for every destination outside the role defaults - 12 of
     19 for a recruiter - the toggle rendered ON while the rail did not show it, and switching it
     did nothing visible either way. Same branch NavMorePanel.toggleRail uses, so the two editors
     cannot disagree. */
  const roleNavIds = (window.E8Workspace.roleDefaults && window.E8Workspace.roleDefaults[persona.role])
    || window.E8Workspace.roleDefaults.recruiter;
  const navExtra = (pref.nav && pref.nav.extra) || [];
  const inRailNow = (id) => (roleNavIds.includes(id) || navExtra.includes(id)) && !pref.nav.hidden.includes(id);
  const toggleNavHidden = (id, show) => {
    if (show) {
      window.E8Workspace.setNav(persona.name, {
        order: navOrder,
        hidden: pref.nav.hidden.filter((hiddenId) => hiddenId !== id),
        extra: roleNavIds.includes(id) ? navExtra : navExtra.concat(id),
      });
      return;
    }
    window.E8Workspace.setNav(persona.name, {
      order: navOrder,
      hidden: roleNavIds.includes(id) ? pref.nav.hidden.concat(id) : pref.nav.hidden,
      extra: navExtra.filter((x) => x !== id),
    });
  };
  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 and Today stay permanent. Switch on the destinations you want kept in the sidebar; everything else stays one click away in More, and in search.</p>
          {NAV_GROUPS.map((group) => {
            const ids = navOrder.filter((id) => group.items.some((item) => item.id === id && navVisibleToPersona(item, persona)));
            if (!ids.length) return null;
            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={'Keep ' + item.label + ' in the sidebar'} checked={inRailNow(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={landingValueFor(pref, persona)} options={landingOptionsFor(persona).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 PersonaProfileMenu({ showToast, onAskAI }) {
  const [open, setOpen] = React.useState(false);
  const ref = React.useRef(null);
  const persona = workspacePersona();
  const personas = window.e8Personas ? window.e8Personas() : [persona];
  const approvals = window.E8Queue ? window.E8Queue.counts().review : 0;
  const profile = persona.role === 'ops'
    ? { title: 'Ops lead · Stand8 delivery', scope: 'Memphis pod · 6 recruiters', state: 'On call', policy: 'Pod defaults · 85% confidence floor' }
    : persona.role === 'csm'
      ? { title: 'Senior CSM · Mid-South book', scope: '5 accounts · 12 renewals', state: 'Client hours', policy: 'Account signals · advisory' }
      : { title: 'Recruiter · Stand8 delivery', scope: 'Memphis pod · personal desk', state: 'Available', policy: 'Pod default · approve routine work' };
  React.useEffect(() => {
    if (!open) return undefined;
    const close = (event) => { if (ref.current && !ref.current.contains(event.target)) setOpen(false); };
    const key = (event) => { if (event.key === 'Escape') setOpen(false); };
    document.addEventListener('mousedown', close);
    document.addEventListener('keydown', key);
    return () => { document.removeEventListener('mousedown', close); document.removeEventListener('keydown', key); };
  }, [open]);
  const go = (route) => { setOpen(false); navigate(route); };
  const openWorkspace = () => {
    setOpen(false);
    window.dispatchEvent(new CustomEvent('e8-open-workspace', { detail: { tab: 'navigation' } }));
  };
  const openAskAI = () => {
    setOpen(false);
    if (onAskAI) onAskAI();
  };
  const switchPersona = (name) => {
    if (window.e8SetPersona) window.e8SetPersona(name);
    setOpen(false);
    if (showToast) showToast('Workspace changed to ' + name);
  };
  return (
    <div className="e8-profile-wrap" ref={ref}>
      {/* The name is the accessible name of this control when the copy is visible; collapsed, the
          copy is display:none, so the title carries it instead - otherwise the rail's only footer
          control reads as an unlabelled button. */}
      <button type="button" className="e8-user-row e8-user-trigger"
        aria-label={persona.name + ' - ' + profile.scope}
        data-e8-tip={persona.name} data-e8-tipsub={profile.scope}
        aria-haspopup="dialog" aria-expanded={open} onClick={() => setOpen(!open)}>
        <Avatar name={persona.name} size="md" />
        <span className="e8-user-copy">
          <span className="e8-user-name">{persona.name}</span>
          <span className="e8-user-role">{profile.scope}</span>
        </span>
        <span className="material-symbols-outlined e8-user-chevron" aria-hidden="true">{open ? 'expand_more' : 'unfold_more'}</span>
      </button>
      {open ? (
        <section className="e8-profile-menu" role="dialog" aria-label="Profile and workspace">
          <header className="e8-profile-head">
            <Avatar name={persona.name} size="lg" />
            <div><b>{persona.name}</b><span>{profile.title}</span></div>
            <span className="e8-profile-role">{persona.label || persona.role}</span>
          </header>
          <div className="e8-profile-row">
            <span className="material-symbols-outlined" aria-hidden="true">location_city</span>
            <span><b>{profile.scope}</b><small>Role controls the sections shown in navigation</small></span>
          </div>
          <div className="e8-profile-row">
            <span className="e8-profile-status" aria-hidden="true"></span>
            <span><b>{profile.state}</b><small>{persona.role === 'ops' ? 'Approval coverage active' : 'Quiet hours 6 pm – 8 am'}</small></span>
          </div>
          <button type="button" className="e8-profile-row is-action" onClick={() => go('today/approvals')}>
            <span className="material-symbols-outlined" aria-hidden="true">fact_check</span>
            <span><b>Approvals routed to you</b><small>{approvals ? approvals + ' items waiting' : 'Nothing waiting'}</small></span>
            <span className="e8-profile-link">Open</span>
          </button>

          <div className="e8-profile-section">
            <span className="e8-profile-section-label">AI policy · {persona.role === 'ops' ? 'pod defaults' : 'read only'}</span>
            {persona.role === 'ops' ? (
              <button type="button" className="e8-profile-policy" onClick={() => go('agents')}>
                <span><b>Agents on your desk</b><small>{profile.policy}</small></span>
                <span className="material-symbols-outlined" aria-hidden="true">chevron_right</span>
              </button>
            ) : (
              <div className="e8-profile-policy is-readonly">
                <span><b>AI in your workflow</b><small>{profile.policy}</small></span>
                <span className="e8-profile-managed">Managed by Ops</span>
              </div>
            )}
          </div>

          {persona.role === 'ops' ? (
            <div className="e8-profile-section">
              <span className="e8-profile-section-label">Administration</span>
              {[
                ['schema', 'Fields, stages & workflows', 'fields'],
                ['palette', 'Components & tokens', 'system'],
                ['history', 'Audit log', 'audit'],
                ['sync', 'Data sync', 'syncqueue'],
              ].map(([icon, label, route]) => (
                <button key={label} type="button" className="e8-profile-admin" onClick={() => go(route)}>
                  <span className="material-symbols-outlined" aria-hidden="true">{icon}</span><span>{label}</span><span className="material-symbols-outlined" aria-hidden="true">chevron_right</span>
                </button>
              ))}
            </div>
          ) : null}

          <div className="e8-profile-section">
            <span className="e8-profile-section-label">Platform</span>
            <SyncCard />
          </div>

          <div className="e8-profile-section">
            <span className="e8-profile-section-label">Workspace tools</span>
            <button type="button" className="e8-profile-tool" onClick={openAskAI}>
              <span className="material-symbols-outlined" aria-hidden="true">forum</span>
              <span><b>Ask AI</b><small>Ask about the work in front of you</small></span>
              <span className="e8-kbd">⌘J</span>
            </button>
            <button type="button" className="e8-profile-tool" onClick={() => go('settings')}>
              <span className="material-symbols-outlined" aria-hidden="true">settings</span>
              <span><b>Settings</b><small>Appearance, notifications and workspace</small></span>
              <span className="material-symbols-outlined" aria-hidden="true">chevron_right</span>
            </button>
            <InstallButton profile />
          </div>

          <div className="e8-profile-section">
            <span className="e8-profile-section-label">Switch demo workspace</span>
            <div className="e8-profile-personas">
              {personas.map((item) => (
                <button key={item.name} type="button" className={item.name === persona.name ? 'is-active' : ''} aria-pressed={item.name === persona.name}
                  onClick={() => switchPersona(item.name)}>{item.label}</button>
              ))}
            </div>
          </div>
          <footer className="e8-profile-foot">
            <button type="button" onClick={openWorkspace}>Customize workspace</button>
            <button type="button" onClick={() => showToast && showToast('Sign out is disabled in this prototype')}>Sign out</button>
          </footer>
        </section>
      ) : null}
    </div>
  );
}
/* ---------- R143: the collapsed rail's hover/focus label (.e8-railtip-*) ----------

   At 56px every row is an icon and nothing else, so the label IS the affordance. Every control
   already carried a native `title`, and that is not good enough: the browser waits about a second
   before showing it, renders it in system chrome that ignores the theme, and never fires on
   keyboard focus at all - so a keyboard user tabbing the rail is told nothing.

   Why a JS-positioned FIXED node rather than a CSS ::after on each row: .e8-side-scroll is
   `overflow: auto` (verified in the browser, and it must stay so - the rail scrolls), which clips
   absolutely-positioned descendants horizontally. That is the same trap CLAUDE.md records for the
   note composer's @-suggestion list. `position: fixed` escapes it, and it escapes CLEANLY here
   because nothing in the chain from a nav row up to <html> sets transform, filter or contain -
   any of which would have made `fixed` resolve against an ancestor instead of the viewport.

   One node for the whole rail, not one per row: the rail has sixteen controls and this way the
   DOM cost is constant.

   The tip is aria-hidden. It is a VISUAL echo of an accessible name the control must carry in its
   own right (see the aria-label work on the rows below) - announcing it as well would read the
   destination twice. */
function useRailTip(enabled) {
  const [tip, setTip] = React.useState(null); /* { label, sub, top } */
  /* The rail NODE in state, set by a callback ref, rather than a document.querySelector run once.
     Sidebar returns two different trees - Home renders its own rail - and today React reconciles
     both into the same <nav> element, so a one-time lookup happens to keep working. That is a fact
     about React's reconciliation, not about this code: change the Home branch's root element or
     wrap it and the node is replaced, the listeners stay on the detached one, and every tooltip
     stops with no error to find. Deps on the node re-bind whenever it really does change, which
     costs four lines and removes the need to know any of the above. */
  const [railEl, setRailEl] = React.useState(null);
  React.useEffect(() => {
    if (!enabled || !railEl) { setTip(null); return undefined; }
    const side = railEl;
    const show = (e) => {
      const el = e.target && e.target.closest ? e.target.closest('[data-e8-tip]') : null;
      if (!el) return;
      const r = el.getBoundingClientRect();
      setTip({ label: el.getAttribute('data-e8-tip'), sub: el.getAttribute('data-e8-tipsub') || '', top: r.top + r.height / 2 });
    };
    const hide = (e) => {
      /* Only clear when the pointer/focus actually LEFT the labelled control - mouseout fires for
         every child boundary crossed inside it (the icon span, the badge), which would flicker. */
      const from = e.target && e.target.closest ? e.target.closest('[data-e8-tip]') : null;
      const to = e.relatedTarget && e.relatedTarget.closest ? e.relatedTarget.closest('[data-e8-tip]') : null;
      if (from && from === to) return;
      setTip(null);
    };
    side.addEventListener('mouseover', show);
    side.addEventListener('mouseout', hide);
    side.addEventListener('focusin', show);
    side.addEventListener('focusout', hide);
    /* A fixed node cannot follow a scrolling row, and a stale label pointing at the wrong icon is
       worse than none - so scrolling the rail dismisses it. Same for Escape. */
    const scroller = side.querySelector('.e8-side-scroll');
    const onScroll = () => setTip(null);
    const onKey = (e) => { if (e.key === 'Escape') setTip(null); };
    if (scroller) scroller.addEventListener('scroll', onScroll, { passive: true });
    window.addEventListener('scroll', onScroll, { passive: true });
    document.addEventListener('keydown', onKey);
    return () => {
      side.removeEventListener('mouseover', show);
      side.removeEventListener('mouseout', hide);
      side.removeEventListener('focusin', show);
      side.removeEventListener('focusout', hide);
      if (scroller) scroller.removeEventListener('scroll', onScroll);
      window.removeEventListener('scroll', onScroll);
      document.removeEventListener('keydown', onKey);
    };
  }, [enabled, railEl]);
  return { tip, railRef: setRailEl };
}

function RailTip({ tip }) {
  if (!tip) return null;
  return (
    <div className="e8-railtip" aria-hidden="true" style={{ top: Math.round(tip.top) }}>
      <span className="e8-railtip-l">{tip.label}</span>
      {tip.sub ? <span className="e8-railtip-s">{tip.sub}</span> : null}
    </div>
  );
}

/* Tooltip props for a collapsed-rail control, as one call instead of a pair of ternaries.
   Twelve controls carried `data-e8-tip={railCollapsed ? x : undefined}` beside
   `data-e8-tipsub={railCollapsed ? y : undefined}`, which is four chances per control to get the
   condition or the pairing wrong, and it read as ceremony rather than as "this control names
   itself when the rail is folded". Returns an empty object when expanded, so spreading it adds
   nothing to the element. */
function railTipProps(collapsed, label, sub) {
  if (!collapsed || !label) return {};
  return sub ? { 'data-e8-tip': label, 'data-e8-tipsub': sub } : { 'data-e8-tip': label };
}

/* Matched to the platform, like UNDO_CHORD - telling a Mac user to press Ctrl+\ is the kind of
   detail that makes people distrust the rest of the product. */
const FOLD_CHORD = (typeof navigator !== 'undefined' && /Mac|iPhone|iPad/.test(navigator.platform || navigator.userAgent || ''))
  ? '\u2318\\' : 'Ctrl+\\';
