/* ELEV8 ATS - screens: Job orders (list + board + intake), Job detail + ★ Matching review queue. */

const DSj = window.Stand8DesignSystem_b5c975;

/* ---------- Job helpers ---------- */
const JOB_PRIORITIES = {
  1: { label: 'Urgent', icon: 'keyboard_double_arrow_up', c: 'var(--ui-danger)' },
  2: { label: 'High', icon: 'keyboard_arrow_up', c: 'var(--ui-text-secondary)' },
  3: { label: 'Medium', icon: 'drag_handle', c: 'var(--ui-text-tertiary)' },
  4: { label: 'Low', icon: 'keyboard_arrow_down', c: 'var(--ui-text-tertiary)' },
};

function PriorityGlyph({ p, withLabel }) {
  const x = JOB_PRIORITIES[p] || JOB_PRIORITIES[3];
  return (
    <span title={x.label + ' priority'} style={{ display: 'inline-flex', alignItems: 'center', gap: 5 }}>
      <span className="material-symbols-outlined" style={{ fontSize: 16, color: x.c }}>{x.icon}</span>
      {withLabel ? <span style={{ fontSize: 'var(--ui-text-sm)', color: 'var(--ui-text-secondary)' }}>{x.label}</span> : null}
    </span>
  );
}

/* Owner cell: a single recruiter, or an overlapping avatar stack for a multi-recruiter team.
   The lead (job.owner) leads the stack; hover shows the full roster. */
function OwnerStack({ owner, team, size }) {
  const roster = (team && team.length ? team : [owner]).filter(Boolean);
  if (roster.length <= 1) return <ActorChip name={owner} />;
  const lead = roster.indexOf(owner) > -1 ? [owner, ...roster.filter((n) => n !== owner)] : roster;
  return (
    <span className="e8-ownerstack" title={'Team · ' + roster.join(', ') + ' (lead: ' + owner + ')'}>
      <DSj.AvatarGroup names={lead} size={size || 'sm'} max={3} />
      <span style={{ fontSize: 'var(--ui-text-sm)', color: 'var(--ui-text-secondary)', whiteSpace: 'nowrap' }}>{owner.split(' ')[0]} +{roster.length - 1}</span>
    </span>
  );
}

/* Lifecycle status (derived from pipeline) → badge tone. The "Status" column. */
const JOB_STATUS = {
  Sourcing: 'neutral',
  Submitting: 'info',
  Interviewing: 'accent',
  Offer: 'warning',
  Filled: 'success',
};

const JOB_POLICY = window.E8Workspace.jobPolicy;
const JOB_GROUPS = JOB_POLICY.groups;
const JOB_GROUP_LABELS = {
  pipeline: 'Pipeline',
  candidates: 'Candidates',
  client: 'Client',
  activity: 'Activity',
  more: 'More',
};

/* Derive a job's lifecycle stage from its live pipeline counts. */
function jobStageOf(j) {
  const p = j.pipeline || {};
  if (j.filled >= j.openings && j.openings > 0) return 'Filled';
  if ((p.placement || 0) > 0) return 'Filled';
  if ((p.interview || 0) > 0) return 'Interviewing';
  if ((p.client_submission || 0) > 0 || (p.submission || 0) > 0) return 'Submitting';
  return 'Sourcing';
}

function pipelineTotal(p) {
  return (window.E8DATA.pipelineStages || []).reduce((n, s) => n + (p[s.key] || 0), 0);
}

/* Candidates-by-stage breakdown - the funnel story for a job.
   List variant: flat aligned numbers (labels live once in the column header).
   compact=true: the dense numbers-in-chips board-card version. */
function StageBreakdown({ p, compact, labeled }) {
  const stages = window.E8DATA.pipelineStages;
  if (!compact && pipelineTotal(p) === 0) {
    return <span style={{ fontSize: 'var(--ui-text-sm)', color: 'var(--ui-text-tertiary)' }}>No candidates yet</span>;
  }
  return (
    <span className={'e8-stages' + (compact ? ' compact' : '') + (labeled ? ' labeled' : '')}>
      {stages.map((s) => {
        const n = p[s.key] || 0;
        const cls = 'e8-stage' + (n ? ' on' : '') + (s.kind ? ' ' + s.kind : '');
        return (
          <span key={s.key} className={cls} title={s.label + ': ' + n}>
            <span className="e8-stage-n">{n || '·'}</span>
            {labeled ? <span className="e8-stage-l">{s.abbrev || s.label}</span> : null}
          </span>
        );
      })}
    </span>
  );
}

/* Column header for the breakdown: the stage labels, shown once, aligned over the rows. */
function StageHeader() {
  const stages = window.E8DATA.pipelineStages;
  return (
    <span className="e8-stages e8-stages-head">
      {stages.map((s) => (
        <span key={s.key} className={'e8-stage' + (s.kind ? ' ' + s.kind : '')}>
          <span className="e8-stage-l" title={s.label}>{s.abbrev || s.label}</span>
        </span>
      ))}
    </span>
  );
}

/* Back-compat alias - the board card and any old callers use the compact breakdown. */
function PipelineCell({ p }) {
  return <StageBreakdown p={p} compact />;
}

/* Compact pipeline meter - a 90x6 stacked bar, one proportional segment per stage in
   funnel order. A columns-picker alternative to the wide StageBreakdown (off by default).
   Open stages walk the neutral->accent ramp; won fills success, lost a muted danger. */
const PIPEMETER_RAMP = ['var(--ui-border-strong)', 'var(--ui-daybreak)', 'var(--ui-info)', 'var(--ui-accent)'];
function PipelineMeter({ p }) {
  const stages = window.E8DATA.pipelineStages || [];
  const open = stages.filter((s) => !s.kind);
  const colorOf = (s) => {
    if (s.kind === 'won') return 'var(--ui-success)';
    if (s.kind === 'lost') return 'color-mix(in srgb, var(--ui-danger) 45%, var(--ui-surface))';
    const i = Math.max(open.indexOf(s), 0);
    const t = open.length > 1 ? i / (open.length - 1) : 0;
    return PIPEMETER_RAMP[Math.round(t * (PIPEMETER_RAMP.length - 1))];
  };
  const total = pipelineTotal(p);
  const title = total
    ? stages.map((s) => s.label + ' ' + (p[s.key] || 0)).join(' · ')
    : 'No candidates yet';
  return (
    <span className="e8-pipemeter" role="img" title={title} aria-label={'Pipeline: ' + title}>
      {stages.map((s) => (p[s.key] ? <span key={s.key} style={{ flexGrow: p[s.key], background: colorOf(s) }}></span> : null))}
    </span>
  );
}

/* Parse a natural-language jobs query into filter chips. */
function parseJobsQuery(q, jobs) {
  const lower = q.toLowerCase();
  const chips = [];
  const matched = new Set();
  [...new Set(jobs.map((j) => j.client))].forEach((c) => {
    if (lower.includes(c.toLowerCase()) || lower.includes(c.toLowerCase().split(' ')[0])) {
      chips.push({ field: 'client', value: c });
      c.toLowerCase().split(' ').forEach((w) => matched.add(w));
    }
  });
  const HEALTH = [['at risk', 'danger'], ['risk', 'danger'], ['stalled', 'warning'], ['aging', 'warning'], ['overdue', 'warning'], ['on track', 'success'], ['healthy', 'success'], ['new', 'neutral']];
  for (const [term, tone] of HEALTH) {
    if (lower.includes(term)) { chips.push({ field: 'health', value: term, tone }); term.split(' ').forEach((w) => matched.add(w)); break; }
  }
  [...new Set(jobs.map((j) => j.owner))].forEach((o) => {
    const first = o.split(' ')[0].toLowerCase();
    if (lower.includes(first)) { chips.push({ field: 'owner', value: o }); matched.add(first); }
  });
  const SKIP = new Set(['open', 'jobs', 'roles', 'orders', 'with', 'that', 'this', 'week', 'the', 'and', 'for', 'are', 'all']);
  const leftovers = lower.split(/[^a-z0-9]+/).filter((w) => w.length > 2 && !matched.has(w) && !SKIP.has(w));
  const roleWord = leftovers.find((w) => jobs.some((j) => j.title.toLowerCase().includes(w)));
  if (roleWord) chips.push({ field: 'role', value: roleWord });
  return chips;
}

function applyJobChips(rows, chips) {
  return rows.filter((r) =>
    chips.every((c) =>
      c.field === 'client' ? r.client === c.value
      : c.field === 'health' ? r.health.tone === c.tone
      : c.field === 'owner' ? r.owner === c.value
      : c.field === 'role' ? r.title.toLowerCase().includes(c.value)
      : true
    )
  );
}

/* Sort the open-jobs rows. Default is priority (urgent first). */
const JOB_SORTS = {
  priority: { label: 'Priority', icon: 'keyboard_double_arrow_up', fn: (a, b) => a.priority - b.priority || (b.day / b.days) - (a.day / a.days) },
  aging: { label: 'Most aging', icon: 'schedule', fn: (a, b) => (b.day / b.days) - (a.day / a.days) },
  pipeline: { label: 'Pipeline depth', icon: 'filter_alt', fn: (a, b) => pipelineTotal(b.pipeline) - pipelineTotal(a.pipeline) },
  status: { label: 'Status', icon: 'flag', fn: (a, b) => window.E8DATA.jobStages.indexOf(jobStageOf(b)) - window.E8DATA.jobStages.indexOf(jobStageOf(a)) },
  newest: { label: 'Newest', icon: 'fiber_new', fn: (a, b) => a.day - b.day },
};
function sortJobs(rows, sort) {
  const s = JOB_SORTS[sort] || JOB_SORTS.priority;
  return [...rows].sort(s.fn);
}

/* ---------- R100: close / reopen a job order ----------
   D.jobs holds the OPEN set; closing moves a job to D.closedJobs (with a pipeline snapshot and
   the rest of its shape preserved), reopening rebuilds an open row. Both write through E8Store
   (persists + emits store:changed so the list/detail re-render), log to audit and toast with a
   working Undo. The job's requisition (D.jobDetails[id]) is keyed by id and untouched, so a closed
   job keeps its rich Requisition view. */
const CLOSE_REASONS = ['Filled internally', 'Req cancelled', 'Lost to competitor', 'On hold', 'Position filled by us'];

function closeJobOrder(job, reason, showToast) {
  const closedRow = {
    id: job.id, title: job.title, client: job.client,
    location: job.location, loc: job.loc, type: job.type, rate: job.rate, openings: job.openings,
    reason: reason, closeReason: reason, closedDate: 'Just now',
    daysOpen: job.day != null ? job.day : 0,
    owner: job.owner, team: job.team,
    pipeline: job.pipeline ? Object.assign({}, job.pipeline) : undefined,
    status: 'closed',
  };
  window.E8Store.remove('jobs', job.id);
  window.E8Store.add('closedJobs', closedRow);
  if (window.E8Audit) window.E8Audit.log({ agent: 'You', prov: 'human', action: 'Closed job · ' + reason, target: job.title + ' (' + job.id + ')', route: 'jobs/closed' });
  if (window.E8Events) window.E8Events.emit('job:updated', { id: job.id, status: 'closed' });
  if (showToast) showToast(job.title + ' closed · ' + reason, 'Undo', () => {
    window.E8Store.remove('closedJobs', job.id);
    window.E8Store.add('jobs', job);
    if (window.E8Events) window.E8Events.emit('job:updated', { id: job.id, status: 'open' });
  });
}

function reopenJobOrder(closedRow, showToast) {
  const openRow = Object.assign({}, closedRow);
  ['reason', 'closeReason', 'closedDate', 'daysOpen', 'status'].forEach((k) => delete openRow[k]);
  openRow.day = 1;
  openRow.days = closedRow.days || 30;
  openRow.filled = 0;
  openRow.priority = closedRow.priority || 3;
  openRow.pipeline = closedRow.pipeline ? Object.assign({}, closedRow.pipeline) : { source: 0, prescreen: 0, submission: 0, client_submission: 0, interview: 0, placement: 0, rejection: 0 };
  openRow.health = { label: 'Reopened', tone: 'neutral', note: 'Reopened from closed - matching refreshes tonight' };
  openRow.next = { label: 'Queue sourcing sweep', toast: 'Sourcing sweep queued for ' + openRow.title };
  window.E8Store.remove('closedJobs', closedRow.id);
  window.E8Store.add('jobs', openRow);
  if (window.E8Audit) window.E8Audit.log({ agent: 'You', prov: 'human', action: 'Reopened job', target: openRow.title + ' (' + openRow.id + ')', route: 'jobs' });
  if (window.E8Events) window.E8Events.emit('job:updated', { id: openRow.id, status: 'open' });
  navigate('job/' + openRow.id + '/overview');
  if (showToast) showToast(openRow.title + ' reopened', 'Undo', () => {
    window.E8Store.remove('jobs', openRow.id);
    window.E8Store.add('closedJobs', closedRow);
    if (window.E8Events) window.E8Events.emit('job:updated', { id: openRow.id, status: 'closed' });
    navigate('job/' + closedRow.id + '/overview');
  });
}

/* Reason picker for closing a job order. Esc cancels; overlay click cancels. Mirrors RejectReasonDialog. */
function CloseJobDialog({ job, onClose, onConfirm }) {
  const [reason, setReason] = React.useState(CLOSE_REASONS[0]);
  return (
    <DSj.Modal title={'Close ' + job.title} ariaLabel="Close job order" closeTitle="Cancel" onClose={onClose}>
        <div style={{ padding: '14px 16px', display: 'grid', gap: 12 }}>
          <div>
            <div className="e8-sectionlabel" style={{ marginBottom: 7 }}>Reason for closing</div>
            <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
              {CLOSE_REASONS.map((r) => (
                <button key={r} type="button" className={'e8-reason-chip' + (reason === r ? ' on' : '')} onClick={() => setReason(r)}>{r}</button>
              ))}
            </div>
          </div>
          <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
            <span style={{ fontSize: 'var(--ui-text-xs)', color: 'var(--ui-text-tertiary)', flex: 1 }}>The job moves to Closed - its data stays viewable and you can reopen it.</span>
            <DSj.Button variant="ghost" size="sm" onClick={onClose}>Cancel</DSj.Button>
            <DSj.Button variant="reject" size="sm" icon="archive" onClick={() => onConfirm(reason)}>Close job</DSj.Button>
          </div>
        </div>
    </DSj.Modal>
  );
}

/* ============================== JOB ORDERS LIST ============================== */
/* One surface for the whole job lifecycle. Reqs that aren't approved yet live
   in the Intake sub-tab (Intake → Scoped → Approval → Open) and become open
   job orders on approval - no separate "Requisitions" module. */
function JobsScreen({ tab = 'open' }) {
  const { density, showToast } = React.useContext(E8Ctx);
  const cap = React.useContext(CaptureCtx); /* R98: New req/job opens the intake agent */
  useStagesVersion(); /* re-render when pipeline stages are edited in config */
  useBusVersion(); /* R100: re-render on store:changed so Open/Filled/Closed counts + rows stay live after close/reopen */
  const setTab = (t) => navigate(t === 'open' ? 'jobs' : 'jobs/' + t);
  const D = window.E8DATA;
  const [intakeInsights, setIntakeInsights] = React.useState(D.insights.intake);
  const [jobInsights, setJobInsights] = React.useState(D.insights.jobs);
  const [view, setView] = React.useState('list');
  const [group, setGroup] = React.useState('none');
  const [sort, setSort] = React.useState('priority');
  const [query, setQuery] = React.useState('');
  const [chips, setChips] = React.useState([]);
  const preservedQueryRef = React.useRef(null);
  const [viewDensity, setViewDensity] = React.useState(density);
  const [filterSheetOpen, setFilterSheetOpen] = React.useState(false);
  const jobsCols = useColumns('jobs', jobColumns(showToast));
  const isMobile = useIsMobile();
  const mobileAct = React.useContext(MobileActionCtx);
  const openJobsFilter = React.useCallback(() => setFilterSheetOpen(true), []);

  React.useEffect(() => {
    if (preservedQueryRef.current === query) {
      preservedQueryRef.current = null;
      return;
    }
    preservedQueryRef.current = null;
    setChips(query.trim() ? parseJobsQuery(query, D.jobs) : []);
  }, [query]);
  React.useEffect(() => {
    if (!isMobile || tab !== 'open') { mobileAct.setAction(null); return; }
    mobileAct.setAction({ icon: 'swap_vert', label: 'Sort & group', onClick: openJobsFilter });
    return () => mobileAct.setAction(null);
  }, [isMobile, tab]);

  const openRows = sortJobs(applyJobChips(D.jobs, chips), sort);

  return (
    <React.Fragment>
      <Topbar
        crumbs={[{ label: 'Job orders' }]}
        actions={
          <React.Fragment>
            {tab === 'intake' ? (
              <DSj.InsightsButton
                items={intakeInsights.map((i) => ({
                  id: i.id,
                  text: <span><b>{i.strong}</b>{i.rest}</span>,
                  action: { label: i.action, onClick: () => showToast(i.action + ' - queued') },
                }))}
                onDismiss={(id) => setIntakeInsights(intakeInsights.filter((x) => x.id !== id))}
              />
            ) : (
              <DSj.InsightsButton
                items={jobInsights.map((i) => ({
                  id: i.id,
                  text: <span><b>{i.strong}</b>{i.rest}</span>,
                  action: { label: i.action, onClick: () => showToast(i.action + ' - drafted, review in Approvals', 'Open approvals', () => navigate('approvals')) },
                }))}
                onDismiss={(id) => setJobInsights(jobInsights.filter((x) => x.id !== id))}
              />
            )}
            {/* Columns menu moved above the table */}
            <DSj.Button variant="secondary" size="sm" icon="upload">Export</DSj.Button>
            {/* R99: New splits into the conversational intake agent and the structured form. */}
            <DSj.MenuButton
              variant="primary" size="sm" icon="add" label={tab === 'intake' ? 'New req' : 'New job'} align="right"
              items={[
                { icon: 'graphic_eq', label: 'Capture with agent', onClick: () => cap.open({ target: 'req' }) },
                { icon: 'lab_profile', label: 'Fill the form', onClick: () => window.e8OpenReqForm && window.e8OpenReqForm({}) },
              ]}
            />
          </React.Fragment>
        }
      />
      <div className="e8-content">
        <div className="e8-page">
          <div style={{ display: 'flex', alignItems: 'center', gap: 12, flexWrap: 'wrap', margin: '0 0 12px' }}>
            <DSj.SubTabs
              items={[
                { id: 'open', label: 'Open', count: D.jobs.length },
                { id: 'intake', label: 'Intake', count: D.intake.length },
                { id: 'filled', label: 'Filled', count: D.filledJobs.length },
                { id: 'closed', label: 'Closed', count: D.closedJobs.length },
              ]}
              active={tab}
              onChange={setTab}
            />
            <div style={{ flex: 1, minWidth: 240, maxWidth: 440 }}>
              <DSj.Input
                icon="search"
                placeholder={tab === 'intake' ? 'reqs waiting on client approval' : 'Describe the jobs you’re looking for… try “fedex at risk”'}
                value={query}
                onChange={(e) => setQuery(e.target.value)}
              />
            </div>
            {tab === 'open' ? (
              <span className="e8-list-toolbar-desktop" style={{ marginLeft: 'auto', display: 'inline-flex', alignItems: 'center', gap: 8 }}>
                <DSj.MenuButton
                  label={'Sort: ' + JOB_SORTS[sort].label}
                  icon="swap_vert"
                  size="sm"
                  variant="ghost"
                  align="right"
                  items={Object.keys(JOB_SORTS).map((k) => ({
                    icon: sort === k ? 'check' : JOB_SORTS[k].icon,
                    label: JOB_SORTS[k].label,
                    onClick: () => setSort(k),
                  }))}
                />
                <DSj.SubTabs
                  items={[{ id: 'list', label: 'List' }, { id: 'board', label: 'Board' }]}
                  active={view}
                  onChange={setView}
                />
                {view === 'list' ? (
                  <DSj.MenuButton
                    label={group === 'none' ? 'Group' : 'Group: ' + group[0].toUpperCase() + group.slice(1)}
                    icon="splitscreen"
                    size="sm"
                    variant="ghost"
                    align="right"
                    items={[
                      { icon: group === 'none' ? 'check' : 'remove', label: 'No grouping', onClick: () => setGroup('none') },
                      { icon: group === 'client' ? 'check' : 'apartment', label: 'Client', onClick: () => setGroup('client') },
                      { icon: group === 'owner' ? 'check' : 'person', label: 'Owner', onClick: () => setGroup('owner') },
                      { icon: group === 'priority' ? 'check' : 'keyboard_double_arrow_up', label: 'Priority', onClick: () => setGroup('priority') },
                    ]}
                  />
                ) : null}
              </span>
            ) : null}
          </div>

          {tab === 'open' && chips.length ? (
            <div style={{ display: 'flex', gap: 6, alignItems: 'center', margin: '0 0 12px', flexWrap: 'wrap' }}>
              <span style={{ fontSize: 'var(--ui-text-xs)', color: 'var(--ui-text-tertiary)' }}>Parsed as</span>
              {chips.map((c, i) => (
                <DSj.FilterChip key={i} field={c.field} value={c.value} onRemove={() => setChips(chips.filter((_, j) => j !== i))} />
              ))}
              <span style={{ fontSize: 'var(--ui-text-xs)', color: 'var(--ui-text-tertiary)' }}>· {openRows.length} of {D.jobs.length} jobs</span>
            </div>
          ) : null}

          {tab === 'open' ? <JobsAnalytics rows={openRows} /> : null}
          {tab === 'open' ? (
            view === 'board'
              ? <JobsBoard rows={openRows} />
              : isMobile
                ? <JobCardList rows={openRows} group={group} />
                : <OpenJobsTab
                    density={viewDensity}
                    rows={openRows}
                    group={group}
                    cols={jobsCols}
                    viewState={{ sort, group, query, chips, density: viewDensity }}
                    onApplyViewState={(state) => {
                      if (state.sort && JOB_SORTS[state.sort]) setSort(state.sort);
                      if (state.group && ['none', 'client', 'owner', 'priority'].includes(state.group)) setGroup(state.group);
                      if (typeof state.query === 'string') {
                        preservedQueryRef.current = Array.isArray(state.chips) ? state.query : null;
                        setQuery(state.query);
                      }
                      if (Array.isArray(state.chips)) setChips(state.chips);
                      if (state.density === 'comfortable' || state.density === 'compact') setViewDensity(state.density);
                    }}
                  />
          ) : null}
          <JobsFilterSheet open={filterSheetOpen} onClose={() => setFilterSheetOpen(false)} sort={sort} setSort={setSort} group={group} setGroup={setGroup} />
          {tab === 'intake' ? <IntakeTab density={density} /> : null}
          {tab === 'filled' ? (
            <React.Fragment>
              <div style={{ fontSize: 'var(--ui-text-sm)', color: 'var(--ui-text-tertiary)', margin: '0 0 10px' }}>Placed reqs convert to engagements - billing, PO burn and renewals live in <a className="e8-link" tabIndex={0} onKeyDown={window.keyActivate} onClick={() => navigate('engagements')}>Engagements</a>.</div>
              <div className="e8-tablescroll">
                <ConfigurableTable listKey="jobs-filled"
                  density={density}
                  onRowClick={(r) => navigate('job/' + r.id + '/overview')}
                  registry={[
                    { key: 'id', label: 'Job order', render: (r) => <span><span style={{ fontWeight: 500 }}>{r.title}</span><span style={{ display: 'block', fontSize: 'var(--ui-text-sm)', color: 'var(--ui-text-tertiary)' }}>{r.id} · {r.client}</span></span> },
                    { key: 'consultant', label: 'Placed', icon: 'person', render: (r) => <ActorChip name={r.consultant} /> },
                    { key: 'fill', label: 'Time to fill', icon: 'timer', render: (r) => <span className="tnum">{r.fillDays} days</span> },
                    { key: 'rate', label: 'Bill rate', icon: 'payments', render: (r) => <span className="tnum">{r.rate}</span> },
                    { key: 'owner', label: 'Owner', icon: 'person', render: (r) => <OwnerStack owner={r.owner} /> },
                  ]}
                  rows={D.filledJobs}
                />
              </div>
            </React.Fragment>
          ) : null}
          {tab === 'closed' ? (
            <div className="e8-tablescroll">
              <ConfigurableTable listKey="jobs-closed"
                density={density}
                onRowClick={(r) => navigate('job/' + r.id + '/overview')}
                registry={[
                  { key: 'id', label: 'Job order', render: (r) => <span><span style={{ fontWeight: 500 }}>{r.title}</span><span style={{ display: 'block', fontSize: 'var(--ui-text-sm)', color: 'var(--ui-text-tertiary)' }}>{r.id} · {r.client}</span></span> },
                  { key: 'reason', label: 'Outcome', icon: 'flag', render: (r) => <DSj.Badge tone={(r.closeReason || r.reason) === 'Lost to competitor' ? 'danger' : 'neutral'}>{r.closeReason || r.reason}</DSj.Badge> },
                  { key: 'days', label: 'Days open', icon: 'schedule', render: (r) => <span className="tnum">{r.daysOpen}</span> },
                  { key: 'owner', label: 'Owner', icon: 'person', render: (r) => <OwnerStack owner={r.owner} /> },
                ]}
                rows={D.closedJobs}
              />
            </div>
          ) : null}
        </div>
      </div>
    </React.Fragment>
  );
}

function openJob(r) {
  navigate('job/' + r.id + '/' + (r.id === 'JO-44219' ? 'pipeline' : 'overview'));
}

function groupJobRows(rows, group) {
  if (group === 'none') return [[null, rows]];
  const keyFn = group === 'client' ? (r) => r.client
    : group === 'owner' ? (r) => r.owner
    : (r) => 'P' + r.priority + ' · ' + JOB_PRIORITIES[r.priority].label;
  const m = new Map();
  rows.forEach((r) => { const k = keyFn(r); if (!m.has(k)) m.set(k, []); m.get(k).push(r); });
  return [...m.entries()].sort((a, b) => String(a[0]).localeCompare(String(b[0])));
}

/* Forecast + SLA for an open job. fp = AI fill probability; ratio = days open ÷ target. */
function jobFill(r) {
  const D = window.E8DATA;
  const fp = (D.jobForecast && D.jobForecast[r.id]) || 0;
  const ratio = r.days ? r.day / r.days : 0;
  const sla = ratio > 1 ? { label: 'Overdue', tone: 'danger' } : ratio >= 0.7 ? { label: 'At risk', tone: 'warning' } : { label: 'On track', tone: 'success' };
  return { fp, ratio, sla };
}

/* Analytics strip across the open jobs (forecast + SLA health at a glance). */
function JobsAnalytics({ rows }) {
  const D = window.E8DATA;
  if (!rows.length) return null;
  const openings = rows.reduce((n, r) => n + (r.openings || 0), 0);
  const avgFill = Math.round((rows.reduce((n, r) => n + ((D.jobForecast && D.jobForecast[r.id]) || 0), 0) / rows.length) * 100);
  const atRisk = rows.filter((r) => jobFill(r).ratio >= 0.85).length;
  const avgDays = Math.round(rows.reduce((n, r) => n + (r.day || 0), 0) / rows.length);
  const tiles = [
    ['Open jobs', String(rows.length)],
    ['Openings', String(openings)],
    ['Avg fill probability', avgFill + '%'],
    ['SLA at risk', String(atRisk)],
    ['Avg days open', avgDays + 'd'],
  ];
  const Kpi = window.KpiCard;
  return (
    <div className="e8-jobstats">
      {tiles.map(([k, v], i) => (
        <Kpi key={k} label={k} value={v} valueColor={i === 3 && atRisk > 0 ? 'var(--ui-warning-text)' : undefined} />
      ))}
    </div>
  );
}

/* ---- Mobile job cards ---- */
const JOB_TONE_COLOR = { success: 'var(--ui-success)', warning: 'var(--ui-warning)', danger: 'var(--ui-danger)', info: 'var(--ui-info)', accent: 'var(--ui-accent, var(--ui-accent))', neutral: 'var(--ui-text-tertiary)' };

function JobMobileField({ field, r }) {
  const f = jobFill(r);
  if (field === 'sla') {
    return <div className="e8-mjob-field"><span>SLA</span><b className="tnum">Day {r.day} of {r.days}</b><DSj.Badge tone={f.sla.tone} dot>{f.sla.label}</DSj.Badge></div>;
  }
  if (field === 'owner') {
    return <div className="e8-mjob-field"><span>Owner</span><OwnerStack owner={r.owner} team={r.team} size="sm" /></div>;
  }
  if (field === 'location') {
    return <div className="e8-mjob-field"><span>Location</span><b title={window.jobLocSummary ? window.jobLocSummary(r) : r.location}>{window.jobLocSummary ? window.jobLocSummary(r) : r.location}</b></div>;
  }
  if (field === 'pipeline') {
    return <div className="e8-mjob-field e8-mjob-pipeline"><span>Pipeline</span><StageBreakdown p={r.pipeline} compact labeled /></div>;
  }
  if (field === 'health') {
    return <div className="e8-mjob-field"><span>Health</span><DSj.Badge tone={r.health.tone} dot={r.health.tone !== 'neutral'}>{r.health.label}</DSj.Badge></div>;
  }
  if (field === 'forecast') {
    return <div className="e8-mjob-field"><span>Forecast</span><span className="e8-mjob-forecast"><span><DSj.Progress value={Math.round(f.fp * 100)} soft={f.fp < 0.6} /></span><b className="tnum">{Math.round(f.fp * 100)}%</b></span></div>;
  }
  return null;
}

function JobCard({ r }) {
  const { showToast } = React.useContext(E8Ctx);
  useWorkspaceVersion();
  const persona = window.e8ActivePersona ? window.e8ActivePersona() : window.E8DATA.user;
  const fields = window.E8Workspace && persona
    ? window.E8Workspace.get(persona.name).mobileCards.jobs.filter((field) => JOB_POLICY.mobileFields.includes(field)).slice(0, 4)
    : ['sla', 'owner'];
  const s = jobStageOf(r);
  return (
    <div className="e8-mcard">
      <span className="e8-mcard-rail" style={{ background: JOB_TONE_COLOR[r.health.tone] || 'var(--ui-text-tertiary)' }}></span>
      <div className="e8-mcard-body">
        <div className="e8-mcard-top">
          <div style={{ flex: 1, minWidth: 0 }}>
            <div className="e8-mcard-name">{r.title}</div>
            <div className="e8-mcard-role">{r.client} · {r.id}</div>
          </div>
          <WorkspacePinButton type="job" id={r.id} compact />
        </div>
        <div className="e8-mjob-status">
          <span className="e8-sectionlabel">Status</span>
          <DSj.Badge tone={JOB_STATUS[s] || 'neutral'} dot={s !== 'Sourcing'}>{s}</DSj.Badge>
        </div>
        {r.next ? (
          <div className="e8-mjob-next">
            <span className="e8-sectionlabel">Next step</span>
            <button type="button" onClick={(e) => {
              e.stopPropagation();
              if (r.next.to) navigate(r.next.to);
              else showToast(r.next.toast || r.next.label + ' - queued', 'Undo');
            }}>{r.next.label}<span className="material-symbols-outlined">arrow_forward</span></button>
          </div>
        ) : null}
        {fields.length ? <div className="e8-mjob-fields">{fields.map((field) => <JobMobileField key={field} field={field} r={r} />)}</div> : null}
      </div>
    </div>
  );
}

function JobCardList({ rows, group }) {
  // R98 Task 6: page the flat list at 50 so scale-mode (100+ jobs) stays fast.
  const PAGE = 50;
  const [shown, setShown] = React.useState(PAGE);
  if (!rows.length) return (
    <div className="e8-card" style={{ boxShadow: 'none', marginTop: 4 }}>
      <DSj.EmptyState icon="work_off" title="No jobs match this search" body="Try removing a filter chip, or widen the description." />
    </div>
  );
  const paged = rows.length > shown ? rows.slice(0, shown) : rows;
  const remaining = rows.length - paged.length;
  const groups = groupJobRows(paged, group);
  return (
    <React.Fragment>
      {groups.map(([label, rs]) => (
        <div key={label || 'all'} style={{ marginBottom: label ? 18 : 0 }}>
          {label ? (
            <div className="e8-grouphead">
              {group === 'owner' ? <ActorChip name={label} /> : <span className="e8-grouphead-label">{label}</span>}
              <span className="tnum" style={{ fontSize: 'var(--ui-text-sm)', color: 'var(--ui-text-tertiary, var(--ui-text-tertiary))' }}>{rs.length}</span>
            </div>
          ) : null}
          <div className="e8-cards">
            {rs.map((r) => (
              <div key={r.id} className="e8-swipe" style={{ cursor: 'pointer' }} {...window.clickableProps(() => openJob(r))}>
                <JobCard r={r} />
              </div>
            ))}
          </div>
        </div>
      ))}
      {remaining > 0 ? (
        <div className="e8-showmore">
          <button type="button" className="e8-showmore-btn" onClick={() => setShown(shown + PAGE)}>Show more<span className="e8-showmore-n">{remaining} more</span></button>
        </div>
      ) : null}
    </React.Fragment>
  );
}

function JobsFilterSheet({ open, onClose, sort, setSort, group, setGroup }) {
  const drag = useSheetDrag(onClose);
  return (
    <React.Fragment>
      <div className={'e8-msheet-backdrop' + (open ? ' is-open' : '')} onClick={onClose}></div>
      <div className={'e8-createsheet' + (open ? ' is-open' : '')} role="dialog" aria-label="Sort & group" aria-hidden={!open} style={drag.style}>
        <div className="e8-msheet-grab" {...drag.bind} onClick={onClose}></div>
        <div className="e8-createsheet-title" {...drag.bind}>Sort &amp; group</div>
        <div className="e8-fsheet-group">
          <div className="e8-fsheet-k">Sort by</div>
          <div className="e8-fsheet-chips">
            {Object.keys(JOB_SORTS).map((k) => (
              <button key={k} type="button" className={'e8-reason-chip' + (sort === k ? ' on' : '')} onClick={() => setSort(k)}>{JOB_SORTS[k].label}</button>
            ))}
          </div>
        </div>
        <div className="e8-fsheet-group">
          <div className="e8-fsheet-k">Group by</div>
          <div className="e8-fsheet-chips">
            {[['none', 'No grouping'], ['client', 'Client'], ['owner', 'Owner'], ['priority', 'Priority']].map(([k, lbl]) => (
              <button key={k} type="button" className={'e8-reason-chip' + (group === k ? ' on' : '')} onClick={() => setGroup(k)}>{lbl}</button>
            ))}
          </div>
        </div>
        <div className="e8-candfilter-foot" style={{ marginTop: 8 }}>
          <DSj.Button variant="primary" size="sm" onClick={onClose}>Done</DSj.Button>
        </div>
      </div>
    </React.Fragment>
  );
}

function jobColumns(showToast) {
  const columns = [
    { key: 'priority', label: '', menuLabel: 'Priority', width: 36, defaultHidden: true, render: (r) => <PriorityGlyph p={r.priority} /> },
    { key: 'id', label: 'Job order', locked: true, render: (r) => (
      <span>
        <span style={{ fontWeight: 500 }}>{r.title}</span>
        <span style={{ display: 'block', fontSize: 'var(--ui-text-sm)', color: 'var(--ui-text-tertiary)' }}>{r.id} · {r.client} · {r.openings} opening{r.openings > 1 ? 's' : ''} · <span className="tnum">{r.rate}</span> · <span className="tnum">day {r.day}/{r.days}</span></span>
      </span>
    ) },
    { key: 'status', label: 'Status', icon: 'flag', width: 124, render: (r) => {
      const s = jobStageOf(r);
      return <DSj.Badge tone={JOB_STATUS[s] || 'neutral'} dot={s !== 'Sourcing'}>{s}</DSj.Badge>;
    } },
    { key: 'pipeline', label: <StageHeader />, menuLabel: 'Pipeline', width: 360, render: (r) => <StageBreakdown p={r.pipeline} /> },
    { key: 'pipemeter', label: 'Pipeline', menuLabel: 'Pipeline (compact meter)', width: 110, defaultHidden: true, render: (r) => <PipelineMeter p={r.pipeline} /> },
    { key: 'location', label: 'Location', icon: 'location_on', width: 220, defaultHidden: true, render: (r) => (
      <span style={{ fontSize: 'var(--ui-text-sm)', color: 'var(--ui-text-secondary)', display: 'block', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }} title={window.jobLocSummary ? window.jobLocSummary(r) : r.location}>{window.jobLocSummary ? window.jobLocSummary(r) : r.location}</span>
    ) },
    { key: 'health', label: 'Health', icon: 'monitor_heart', render: (r) => (
      <span title={r.health.note}>
        <DSj.Badge tone={r.health.tone} dot={r.health.tone !== 'neutral'}>{r.health.label}</DSj.Badge>
      </span>
    ) },
    { key: 'forecast', label: 'Forecast', icon: 'insights', width: 170, defaultHidden: true, render: (r) => {
      const f = jobFill(r);
      return (
        <span style={{ display: 'inline-flex', alignItems: 'center', gap: 8 }} title={'AI fill probability ' + Math.round(f.fp * 100) + '% · day ' + r.day + ' of ' + r.days}>
          <span style={{ width: 44, display: 'inline-flex' }}><DSj.Progress value={Math.round(f.fp * 100)} soft={f.fp < 0.6} /></span>
          <span className="tnum" style={{ fontSize: 'var(--ui-text-xs)', color: 'var(--ui-text-tertiary)', minWidth: 26 }}>{Math.round(f.fp * 100)}%</span>
          <DSj.Badge tone={f.sla.tone} dot>{f.sla.label}</DSj.Badge>
        </span>
      );
    } },
    { key: 'next', label: 'Next step', icon: 'arrow_forward', render: (r) => (r.next ? (
      <a
        className="e8-link"
        style={{ fontSize: 'var(--ui-text-sm)', color: 'var(--ui-text-secondary)', borderBottom: '1px dashed var(--ui-border-strong)' }}
        title="Suggested by ELEV8 - based on this job’s pipeline"
        tabIndex={0} onKeyDown={window.keyActivate} onClick={(e) => { e.stopPropagation(); r.next.to ? navigate(r.next.to) : showToast(r.next.toast, 'Undo'); }}
      >{r.next.label}</a>
    ) : <span style={{ fontSize: 'var(--ui-text-sm)', color: 'var(--ui-text-tertiary)' }}>-</span>) },
    { key: 'owner', label: 'Owner', icon: 'person', render: (r) => <OwnerStack owner={r.owner} team={r.team} /> },
  ];
  const defaultOrder = ['id', 'status', 'next', 'pipeline', 'health', 'owner', 'priority', 'pipemeter', 'location', 'forecast'];
  return defaultOrder.map((key) => columns.find((column) => column.key === key));
}

function OpenJobsTab({ density, rows, group, cols, viewState, onApplyViewState }) {
  const { showToast } = React.useContext(E8Ctx);
  const [closing, setClosing] = React.useState(null); /* R100: job pending close (reason picker) */
  const groups = groupJobRows(rows, group);
  const jbMobile = useIsMobile();

  return (
    <React.Fragment>
      {!jbMobile ? (
        <div className="e8-ctable-toolbar">
          <div className="e8-ctable-toolbar-left" />
          <ViewsMenu listKey="jobs" cols={cols} viewState={viewState} onApplyState={onApplyViewState} />
          <ColumnsMenu cols={cols} advanced />
        </div>
      ) : null}
      {groups.map(([label, rs]) => (
        <div key={label || 'all'} style={{ marginBottom: label ? 18 : 0 }}>
          {label ? (
            <div className="e8-grouphead">
              {group === 'owner' ? <ActorChip name={label} /> : <span className="e8-grouphead-label">{label}</span>}
              <span className="tnum" style={{ fontSize: 'var(--ui-text-sm)', color: 'var(--ui-text-tertiary)' }}>{rs.length}</span>
            </div>
          ) : null}
          <div className="e8-tablescroll">
          <ConfigurableTable listKey="jobs" registry={cols.allColumns} columnsState={cols} controls={false} pageSize={50}
            onRowClick={openJob}
            rows={rs}
            rowActions={(r) => (
              <React.Fragment>
                {r.pipeline.source > 0
                  ? <DSj.Button variant="ghost" size="sm" icon="join_inner" onClick={() => navigate('job/' + r.id + '/matching')}>Review {r.pipeline.source}</DSj.Button>
                  : <DSj.Button variant="ghost" size="sm" icon="join_inner" onClick={() => showToast('Matching queued for ' + r.id, 'Undo')}>Run matching</DSj.Button>}
                <WorkspacePinButton type="job" id={r.id} compact />
                <DSj.MenuButton
                  items={[
                    { icon: 'edit', label: 'Edit job' },
                    { icon: 'forum', label: 'Draft client update' },
                    { icon: 'history', label: 'View intake notes' },
                    { separator: true },
                    { icon: 'archive', label: 'Close job', danger: true, onClick: () => setClosing(r) },
                  ]}
                />
              </React.Fragment>
            )}
          />
          </div>
        </div>
      ))}
      {closing ? (
        <CloseJobDialog
          job={closing}
          onClose={() => setClosing(null)}
          onConfirm={(reason) => { const j = closing; setClosing(null); closeJobOrder(j, reason, showToast); }}
        />
      ) : null}
      {!rows.length ? (
        <div className="e8-card" style={{ boxShadow: 'none', marginTop: 4 }}>
          <DSj.EmptyState icon="work_off" title="No jobs match this search" body="Try removing a filter chip, or widen the description." />
        </div>
      ) : null}
    </React.Fragment>
  );
}

/* ---------- Board view: jobs by lifecycle stage ---------- */
const JOB_BOARD_CAP = 15; // R98 Task 6: cap cards per column at scale, with a "+N more" tail
function JobsBoard({ rows }) {
  const { showToast } = React.useContext(E8Ctx);
  const cols = ['Sourcing', 'Submitting', 'Interviewing'];
  const [stageById, setStageById] = React.useState(() => Object.fromEntries(rows.map((r) => [r.id, jobStageOf(r)])));
  const [dragId, setDragId] = React.useState(null);
  const [over, setOver] = React.useState(null);
  const stageOf = (r) => (cols.indexOf(stageById[r.id]) > -1 ? stageById[r.id] : jobStageOf(r));
  const byStage = Object.fromEntries(cols.map((c) => [c, []]));
  rows.forEach((r) => { const s = stageOf(r); (byStage[cols.indexOf(s) > -1 ? s : 'Sourcing']).push(r); });
  const drop = (col) => {
    setOver(null);
    if (!dragId) return;
    const r = rows.find((x) => x.id === dragId);
    if (r && stageOf(r) !== col) { setStageById((m) => ({ ...m, [dragId]: col })); showToast(r.title + ' moved to ' + col, 'Undo'); }
    setDragId(null);
  };
  return (
    <div className="e8-board">
      {cols.map((c) => (
        <div className={'e8-board-col' + (over === c ? ' is-dragover' : '')} key={c}
          onDragOver={(e) => { e.preventDefault(); if (over !== c) setOver(c); }}
          onDragLeave={(e) => { if (e.currentTarget === e.target) setOver(null); }}
          onDrop={() => drop(c)}>
          <div className="e8-board-colhead">
            {c}
            <span className="tnum" style={{ fontWeight: 400, color: 'var(--ui-text-tertiary)' }}>{byStage[c].length}</span>
          </div>
          {byStage[c].slice(0, JOB_BOARD_CAP).map((r) => (
            <div key={r.id} className={'e8-board-card' + (dragId === r.id ? ' is-dragging' : '')}
              draggable
              onDragStart={() => setDragId(r.id)}
              onDragEnd={() => { setDragId(null); setOver(null); }}
              {...window.clickableProps(() => { if (!dragId) openJob(r); })}>
              <div style={{ display: 'flex', alignItems: 'flex-start', gap: 7 }}>
                <PriorityGlyph p={r.priority} />
                <span style={{ minWidth: 0 }}>
                  <span style={{ display: 'block', fontSize: 'var(--ui-text-base)', fontWeight: 500, lineHeight: 1.3 }}>{r.title}</span>
                  <span style={{ display: 'block', fontSize: 'var(--ui-text-xs)', color: 'var(--ui-text-tertiary)', marginTop: 1 }}>{r.id} · {r.client}</span>
                </span>
              </div>
              <PipelineCell p={r.pipeline} />
              <span title={r.health.note} style={{ display: 'inline-flex' }}>
                <DSj.Badge tone={r.health.tone} dot={r.health.tone !== 'neutral'}>{r.health.label}</DSj.Badge>
              </span>
              <div style={{ display: 'flex', alignItems: 'center', gap: 7 }}>
                {r.team && r.team.length > 1
                  ? <DSj.AvatarGroup names={[r.owner, ...r.team.filter((n) => n !== r.owner)]} size="sm" max={3} />
                  : <DSj.Avatar name={r.owner} size="sm" />}
                <span style={{ fontSize: 'var(--ui-text-xs)', color: 'var(--ui-text-tertiary)' }}>{r.owner.split(' ')[0]}{r.team && r.team.length > 1 ? ' +' + (r.team.length - 1) : ''}</span>
                <span className="tnum" style={{ marginLeft: 'auto', fontSize: 'var(--ui-text-xs)', color: jobFill(r).sla.tone === 'danger' ? 'var(--ui-danger-text)' : jobFill(r).sla.tone === 'warning' ? 'var(--ui-warning-text)' : 'var(--ui-text-tertiary)' }} title={'AI fill ' + Math.round(jobFill(r).fp * 100) + '% · ' + jobFill(r).sla.label}>{Math.round(jobFill(r).fp * 100)}% · day {r.day}/{r.days}</span>
              </div>
            </div>
          ))}
          {byStage[c].length > JOB_BOARD_CAP ? <button type="button" className="e8-board-more" onClick={() => navigate('jobs')}>+{byStage[c].length - JOB_BOARD_CAP} more</button> : null}
          {!byStage[c].length ? <div className="e8-board-empty">Drop a job here</div> : null}
        </div>
      ))}
    </div>
  );
}

/* ---------- Intake: reqs on their way to becoming open job orders ---------- */
function IntakeTab({ density }) {
  const D = window.E8DATA;
  const { showToast } = React.useContext(E8Ctx);
  return (
    <React.Fragment>
      <ConfigurableTable listKey="jobs-intake"
        density={density}
        registry={[
          { key: 'id', label: 'Req', render: (r) => (
            <span title={r.note}>
              <span style={{ display: 'inline-flex', alignItems: 'center', gap: 7 }}>
                <span style={{ fontWeight: 500 }}>{r.title}</span>
                {r.prov === 'drafted' ? <DSj.ProvenanceBadge kind="drafted" /> : null}
              </span>
              <span style={{ display: 'block', fontSize: 'var(--ui-text-sm)', color: 'var(--ui-text-tertiary)' }}>{r.id} · {r.client} · {r.openings} opening{r.openings > 1 ? 's' : ''}</span>
            </span>
          ) },
          { key: 'stage', label: 'Approval', width: 200, render: (r) => (
            <DSj.StageCell stages={D.intakeStages} current={r.stage} />
          ) },
          { key: 'waiting', label: 'Status', render: (r) => <DSj.Badge tone={r.tone} dot={r.tone !== 'neutral'}>{r.waiting}</DSj.Badge> },
          { key: 'sponsor', label: 'Sponsor', render: (r) => <ActorChip name={r.sponsor} /> },
          { key: 'rate', label: 'Est. rate', num: true },
        ]}
        rows={D.intake}
        rowActions={(r) => (
          <React.Fragment>
            {r.stage === 3
              ? <DSj.Button variant="accept" size="sm" icon="work" onClick={() => showToast(r.id + ' opened as job order', 'Undo')}>Open job order</DSj.Button>
              : <DSj.Button variant="secondary" size="sm" icon="mail" onClick={() => showToast('Nudge drafted to ' + r.sponsor, 'Review')}>Nudge sponsor</DSj.Button>}
            <DSj.MenuButton
              items={[
                { icon: 'edit', label: 'Edit scope' },
                { icon: 'history', label: 'View intake notes' },
                { separator: true },
                { icon: 'archive', label: 'Archive req', danger: true },
              ]}
            />
          </React.Fragment>
        )}
      />
      <p style={{ fontSize: 'var(--ui-text-sm)', color: 'var(--ui-text-tertiary)', margin: '10px 2px 0' }}>
        Reqs become open job orders when the client sponsor approves - approved reqs open with one click.
      </p>
    </React.Fragment>
  );
}

/* ============================== JOB DETAIL ============================== */
function JobDetailScreen({ id, sub }) {
  const D = window.E8DATA;
  /* R100: resolve across open/closed/filled. D.jobs is only the OPEN set; closed + filled reqs live
     in their own arrays and are shown read-only. */
  const resolved = window.jobById ? window.jobById(id)
    : (D.jobs.find((j) => j.id === id) ? { row: D.jobs.find((j) => j.id === id), status: 'open' } : null);
  const J = resolved ? resolved.row : null;
  const status = resolved ? resolved.status : null;
  const { showToast } = React.useContext(E8Ctx);
  const cap = React.useContext(CaptureCtx); /* R98 5A: Add candidate opens the résumé intake with this job as context */
  const [matchInsights, setMatchInsights] = React.useState(D.insights.matching);
  const [closing, setClosing] = React.useState(null); /* R100: reason picker for closing an open job from the detail */
  const [, setDecTick] = React.useState(0);
  /* R99/R100: re-render when the requisition is edited, submissions/decisions change, or the job's
     lifecycle flips (close/reopen) so the header, tabs and status banner stay live without a reload. */
  React.useEffect(() => (window.E8Events ? window.E8Events.subscribe(['match:decided', 'match:accepted', 'store:changed', 'job:updated'], () => setDecTick((t) => t + 1)) : undefined), []);

  if (!J) {
    return (
      <React.Fragment>
        <Topbar crumbs={[{ label: 'Job orders', to: 'jobs' }, { label: 'Not found' }]} />
        <div className="e8-content"><div className="e8-page"><div style={{ maxWidth: 460, margin: '48px auto' }}>
          <DSj.EmptyState icon="work_off" title="Job order not found" body="This job order does not exist, or the link is out of date." cta={<DSj.Button variant="primary" icon="work" onClick={() => navigate('jobs')}>Back to job orders</DSj.Button>} />
        </div></div></div>
      </React.Fragment>
    );
  }
  const isOpen = status === 'open';
  const flagship = J.id === 'JO-44219';
  const det = D.jobDetails[J.id] || null;
  /* Closed/filled jobs are read-only: a reduced tab set (Overview/Activity/Files/Notes) with no live
     Matching/Pipeline/Submissions surfaces. Coerce a stale sub (e.g. .../pipeline) back to overview. */
  const allowedRoutes = JOB_POLICY.routesForStatus(status);
  const tab = JOB_POLICY.resolveSection(status, sub, null);
  const setTab = (t) => navigate('job/' + J.id + '/' + t);

  /* R99: form/agent-created reqs carry mustHaves + a structured hiringManager but not the legacy
     requirements/contacts arrays the rails read, so fall back to those before the generic scope. */
  const requirements = flagship ? D.job.requirements
    : (det && det.requirements) ? det.requirements
    : (det && det.mustHaves && det.mustHaves.length) ? det.mustHaves
    : [
      'Scope drafted from the intake call - confirm details with the sponsor',
      J.openings + ' opening' + (J.openings > 1 ? 's' : '') + ' · ' + J.type + ' · ' + J.rate + ' bill rate',
      'On-site expectations pending sponsor confirmation',
    ];
  const draftedScope = !flagship && !det;
  const railContacts = flagship ? D.job.contacts
    : (det && det.contacts) ? det.contacts
    : (det && det.hiringManager && det.hiringManager.name) ? [{ name: det.hiringManager.name, role: det.hiringManager.title || 'Hiring manager' }]
    : D.contacts.filter((c) => c.client === J.client).map((c) => ({ name: c.name, role: c.role + ' - sponsor' }));
  const subRows = D.submissions.filter((s) => s.jobId === J.id);
  const stage = status === 'filled' ? 'Filled' : jobStageOf(J);
  /* R100: filled jobs link to the placement's engagement (via the consultant record where the
     engagement lives); null engagementId falls back to the Engagements list. */
  const engRow = (status === 'filled' && J.engagementId) ? (D.engagements || []).find((e) => e.id === J.engagementId) : null;

  const liveNext = (() => {
    const n = J.next;
    if (!n || !n.to || n.to.indexOf('job/' + J.id + '/matching') !== 0) return n;
    const count = matchReviewCount(J.id);
    return count ? { ...n, label: 'Review ' + count + ' match' + (count === 1 ? '' : 'es') } : null;
  })();

  const nextActions = [
    liveNext,
    flagship ? { label: 'Submit Daniel Okafor - screened 2h ago', to: 'candidate/c-okafor' } : null,
    railContacts.length ? { label: 'Draft status update for ' + railContacts[0].name.split(' ')[0], toast: 'Status update drafted for ' + railContacts[0].name + ' - review in Approvals' } : null,
  ].filter(Boolean).slice(0, 3);
  const sectionItems = {
    pipeline: { id: 'pipeline', label: 'Pipeline', count: flagship ? pipelineTotal(J.pipeline) : undefined },
    matching: { id: 'matching', label: 'Matching', count: (J.pipeline && J.pipeline.source) || undefined },
    inbound: { id: 'inbound', label: 'Applicants', count: (D.applications || []).filter((a) => a.jobId === J.id && a.stage === 'New').length || undefined },
    submissions: { id: 'submissions', label: 'Submissions', count: subRows.length || undefined },
    campaigns: { id: 'campaigns', label: 'Campaigns' },
    advertising: { id: 'advertising', label: 'Advertising' },
    portal: { id: 'portal', label: 'Company portal', count: flagship ? (D.portal[J.id] ? D.portal[J.id].candidates.length : undefined) : undefined },
    activity: { id: 'activity', label: 'Activity' },
    notes: { id: 'notes', label: 'Notes' },
    files: { id: 'files', label: 'Files', count: (D.jobFiles[J.id] || []).length || undefined },
    overview: { id: 'overview', label: 'Overview' },
    analytics: { id: 'analytics', label: 'Analytics' },
  };
  const availableGroups = Object.keys(JOB_GROUPS).filter((group) => JOB_GROUPS[group].some((route) => allowedRoutes.includes(route)));
  const activeGroup = JOB_POLICY.groupFor(tab);
  const activeGroupRoutes = JOB_GROUPS[activeGroup].filter((route) => allowedRoutes.includes(route));

  return (
    <React.Fragment>
      <Topbar
        crumbs={[{ label: 'Job orders', to: 'jobs' }, { label: J.id }]}
        actions={
          <React.Fragment>
            {tab === 'matching' && flagship ? (
              <DSj.InsightsButton
                items={matchInsights.map((i) => ({
                  id: i.id,
                  text: <span><b>{i.strong}</b>{i.rest}</span>,
                  action: { label: i.action, onClick: () => showToast('Rate exception flagged for Janet Moss', 'Undo') },
                }))}
                onDismiss={(id2) => setMatchInsights(matchInsights.filter((i) => i.id !== id2))}
              />
            ) : null}
            <WorkspacePinButton type="job" id={J.id} compact />
            <DSj.Button variant="ghost" size="sm" icon="upload">Export</DSj.Button>
            {/* R100: live-only actions (edit req, run matching, add candidate, close) apply to OPEN jobs
                only. Closed/filled reqs are read-only - their banner carries reopen / open-engagement. */}
            {isOpen ? (
              <React.Fragment>
                {/* R99: open the structured requisition form pre-filled from this job's req + typed loc. */}
                <DSj.Button variant="secondary" size="sm" icon="edit_note" onClick={() => window.e8OpenReqForm && window.e8OpenReqForm({ jobId: J.id, prefill: { job: J, req: (window.reqOf ? window.reqOf(J.id) : null) || {}, loc: J.loc } })}>Edit requisition</DSj.Button>
                {tab !== 'matching' ? <DSj.Button variant="secondary" size="sm" icon="join_inner" onClick={() => navigate('job/' + J.id + '/matching')}>Run matching</DSj.Button> : null}
                <DSj.Button variant="primary" size="sm" icon="person_add" onClick={() => cap.open({ target: 'candidate', jobContext: { id: J.id, title: J.title, client: J.client } })}>Add candidate</DSj.Button>
                <DSj.MenuButton
                  align="right"
                  items={[
                    { icon: 'forum', label: 'Draft client update', onClick: () => showToast('Client update drafted for ' + J.client + ' - review in Approvals', 'Open approvals', () => navigate('approvals')) },
                    { separator: true },
                    { icon: 'archive', label: 'Close job', danger: true, onClick: () => setClosing(J) },
                  ]}
                />
              </React.Fragment>
            ) : null}
          </React.Fragment>
        }
      />
      <div className="e8-content">
        <div className="e8-page">
          {/* R100: status banner for read-only closed/filled reqs, at the very top of the detail. */}
          {!isOpen ? <JobStatusBanner job={J} status={status} engRow={engRow} showToast={showToast} /> : null}
          {/* Header */}
          <div style={{ display: 'flex', alignItems: 'center', gap: 14, marginBottom: 12, flexWrap: 'wrap' }}>
            <div style={{ minWidth: 0, flex: 1 }}>
              <div style={{ display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap' }}>
                <h1 className="e8-h1" style={{ whiteSpace: 'nowrap' }}>{J.title}</h1>
                {isOpen
                  ? <DSj.Badge tone={JOB_STATUS[stage] || 'neutral'} dot>{stage}</DSj.Badge>
                  : status === 'filled'
                    ? <DSj.Badge tone="success" dot>Filled</DSj.Badge>
                    : <DSj.Badge tone={(J.closeReason || J.reason) === 'Lost to competitor' ? 'danger' : 'neutral'} dot>Closed</DSj.Badge>}
                {isOpen && J.priority != null ? <PriorityGlyph p={J.priority} withLabel /> : null}
              </div>
              <div style={{ fontSize: 'var(--ui-text-base)', color: 'var(--ui-text-secondary)', marginTop: 3, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
                {J.client} · {window.jobLocSummary ? window.jobLocSummary(J) : J.location} · {J.type} · <span className="tnum">{J.rate}</span> · {J.openings} opening{J.openings > 1 ? 's' : ''}
              </div>
            </div>
            {isOpen && J.day != null && J.days != null ? (
              <div style={{ display: 'flex', alignItems: 'center', gap: 10, flex: 'none' }}>
                <span className="tnum" style={{ fontSize: 'var(--ui-text-sm)', color: 'var(--ui-text-secondary)', whiteSpace: 'nowrap' }}>day {J.day} of {J.days}</span>
                <span style={{ width: 120, display: 'inline-flex' }}><DSj.Progress value={(J.day / J.days) * 100} /></span>
              </div>
            ) : null}
          </div>

          {/* Lifecycle (live-only) */}
          {isOpen ? (
            <div style={{ marginBottom: 16 }}>
              <DSj.StageFlow stages={D.jobStages} current={Math.max(D.jobStages.indexOf(stage), 0)} />
            </div>
          ) : null}

          <div className="e8-record-2col">
            <div style={{ flex: 1, minWidth: 0 }}>
              <div className="e8-job-groupnav">
                <DSj.Tabs
                  items={availableGroups.map((group) => ({ id: group, label: JOB_GROUP_LABELS[group] }))}
                  active={activeGroup}
                  onChange={(group) => setTab(JOB_POLICY.primaryRoute(group))}
                />
                {activeGroupRoutes.length > 1 ? (
                  <DSj.SubTabs items={activeGroupRoutes.map((route) => sectionItems[route])} active={tab} onChange={setTab} />
                ) : null}
              </div>
              <div style={{ paddingTop: 16 }}>
                {tab === 'pipeline' ? (flagship ? <PipelineBoard /> : (
                  <div style={{ maxWidth: 460, margin: '24px auto' }}>
                    <DSj.EmptyState
                      icon="view_kanban"
                      title="Pipeline opens once candidates are accepted"
                      body="Accept matches into the pipeline and they appear here as cards you can drag between stages. The full board is live on JO-44219."
                      cta={<DSj.Button variant="secondary" size="sm" className="e8-empty-cta" onClick={() => navigate('job/JO-44219/pipeline')}>Open the JO-44219 board</DSj.Button>}
                    />
                  </div>
                )) : null}
                {tab === 'matching' ? <MatchingTab job={J} /> : null}
                {tab === 'submissions' ? (subRows.length ? <JobSubmissionsTab rows={subRows} /> : (
                  <div style={{ maxWidth: 460, margin: '24px auto' }}>
                    <DSj.EmptyState icon="send" title="No submittals yet" body="Submittals to the client appear here once you submit candidates from the pipeline." />
                  </div>
                )) : null}
                {tab === 'inbound' ? <window.JobInboundTab J={J} /> : null}
                {tab === 'campaigns' ? <window.JobCampaignsTab J={J} /> : null}
                {tab === 'advertising' ? <window.JobAdvertisingTab J={J} /> : null}
                {tab === 'portal' ? <window.CompanyPortalTab J={J} /> : null}
                {tab === 'files' ? <window.JobFilesTab J={J} /> : null}
                {tab === 'notes' ? <window.JobNotesTab J={J} /> : null}
                {tab === 'overview' ? <JobOverviewTab job={J} det={det} flagship={flagship} draftedScope={draftedScope} /> : null}
                {tab === 'activity' ? (
                  !isOpen
                    ? <Timeline events={status === 'filled'
                        ? [
                            { id: 'g2', when: J.filledDate || 'Filled', actor: J.owner, ai: false, prov: 'human', title: 'Placed ' + (J.consultant || 'a consultant'), body: (J.consultant || 'The consultant') + ' started at ' + J.client + (J.fillDays != null ? ' · ' + J.fillDays + '-day time to fill' : '') + '.' },
                            { id: 'g1', when: 'Job opened', actor: J.owner, ai: false, prov: 'human', title: 'Job order opened', body: 'Opened from approved intake · ' + J.client + '.' },
                          ]
                        : [
                            { id: 'g2', when: J.closedDate || 'Closed', actor: J.owner, ai: false, prov: 'human', title: 'Job order closed · ' + (J.closeReason || J.reason || 'Closed'), body: 'Closed after ' + (J.daysOpen != null ? J.daysOpen + ' days open' : 'the search') + ' · ' + J.client + '.' },
                            { id: 'g1', when: 'Job opened', actor: J.owner, ai: false, prov: 'human', title: 'Job order opened', body: 'Opened from approved intake · ' + J.client + '.' },
                          ]} />
                    : flagship
                    ? <Timeline events={D.timeline.filter((t) => t.id !== 't5')} />
                    : <Timeline events={[
                        { id: 'g2', when: 'Today · 8:05 am', actor: 'Matching', ai: true, prov: 'advised', title: 'Matching refresh completed', body: J.pipeline.source + ' candidate' + (J.pipeline.source === 1 ? '' : 's') + ' surfaced for review.' },
                        { id: 'g1', when: 'Day 1', actor: J.owner, ai: false, prov: 'human', title: 'Job order opened', body: 'Opened from approved intake · ' + J.client + '.' },
                      ]} />
                ) : null}
                {tab === 'analytics' ? <JobAnalyticsTab job={J} flagship={flagship} setTab={setTab} /> : null}
              </div>
            </div>

            {/* Right rail */}
            <div className="e8-rail">
              {/* Next best actions are live advice - only for open jobs. */}
              {isOpen ? (
                <div className="e8-rail-block" style={{ background: 'var(--ui-daybreak-wash)', borderColor: 'var(--ui-daybreak-tint)' }}>
                  <div className="e8-rail-title" style={{ display: 'flex', alignItems: 'center', gap: 7 }}>
                    Next best actions <DSj.ProvenanceBadge kind="advised" />
                  </div>
                  <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
                    {nextActions.map((a, i) => (
                      <button
                        key={i}
                        type="button"
                        className={'e8-nextaction' + (i === 0 ? ' primary' : '')}
                        onClick={() => (a.to ? navigate(a.to) : showToast(a.toast || a.label + ' - queued', 'Undo'))}
                      >
                        <span className="material-symbols-outlined" style={{ fontSize: 14 }}>arrow_forward</span>
                        {a.label}
                      </button>
                    ))}
                  </div>
                  <div style={{ fontSize: 'var(--ui-text-xs)', color: 'var(--ui-text-tertiary)', marginTop: 8 }}>
                    Based on this job’s pipeline and client history. Nothing runs without you.
                  </div>
                </div>
              ) : null}
              <window.JobDetailsRail J={J} status={status} />
              {window.RecordTasksRail ? <window.RecordTasksRail refType="job" refId={J.id} /> : null}
              <div className="e8-rail-block">
                <div className="e8-rail-title" style={{ display: 'flex', alignItems: 'center', gap: 7 }}>
                  Requirements {draftedScope ? <DSj.ProvenanceBadge kind="drafted" /> : null}
                </div>
                {requirements.map((r, i) => (
                  <div key={i} style={{ display: 'flex', gap: 7, fontSize: 'var(--ui-text-sm)', color: 'var(--ui-text-secondary)', padding: '3px 0' }}>
                    <span className="material-symbols-outlined" style={{ fontSize: 14, marginTop: 1 }}>check</span>
                    {r}
                  </div>
                ))}
              </div>
              <div className="e8-rail-block">
                <div className="e8-rail-title">Client contacts</div>
                {railContacts.map((c) => (
                  <div key={c.name} style={{ display: 'flex', gap: 8, alignItems: 'center', padding: '4px 0' }}>
                    <DSj.Avatar name={c.name} size="sm" />
                    <div style={{ minWidth: 0 }}>
                      <div style={{ fontSize: 'var(--ui-text-sm)', fontWeight: 500, whiteSpace: 'nowrap' }}>{c.name}</div>
                      <div style={{ fontSize: 'var(--ui-text-xs)', color: 'var(--ui-text-tertiary)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{c.role}</div>
                    </div>
                  </div>
                ))}
              </div>
              {isOpen ? (
                <div className="e8-rail-block">
                  <div className="e8-rail-title">Sync</div>
                  <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
                    <DSj.Badge tone="success" dot>Synced</DSj.Badge>
                    <span style={{ fontSize: 'var(--ui-text-sm)', color: 'var(--ui-text-tertiary)' }}>Bullhorn · 24 min ago</span>
                  </div>
                </div>
              ) : null}
            </div>
          </div>
        </div>
      </div>
      {closing ? (
        <CloseJobDialog
          job={closing}
          onClose={() => setClosing(null)}
          onConfirm={(reason) => { const j = closing; setClosing(null); closeJobOrder(j, reason, showToast); }}
        />
      ) : null}
    </React.Fragment>
  );
}

/* R100: read-only status banner shown at the top of a closed/filled job detail.
   Closed → warning/danger-tinted with Reopen. Filled → success-tinted with Open engagement. */
function JobStatusBanner({ job, status, engRow, showToast }) {
  const filled = status === 'filled';
  const lost = (job.closeReason || job.reason) === 'Lost to competitor';
  const tone = filled ? 'success' : (lost ? 'danger' : 'warning');
  const bg = 'color-mix(in srgb, var(--ui-' + tone + ') 8%, var(--ui-surface))';
  const border = 'var(--ui-' + tone + '-tint)';
  const textColor = 'var(--ui-' + tone + '-text)';
  const icon = filled ? 'check_circle' : 'archive';
  const title = filled
    ? 'Filled · placed ' + (job.consultant || 'a consultant') + (job.filledDate ? ' · ' + job.filledDate : '')
    : 'Closed · ' + (job.closeReason || job.reason || 'Closed') + (job.closedDate ? ' · ' + job.closedDate : '');
  const sub = filled
    ? (job.fillDays != null ? job.fillDays + '-day time to fill · ' : '') + 'billing, PO burn and renewals live in the engagement.'
    : (job.daysOpen != null ? job.daysOpen + ' days open. ' : '') + 'The requisition and its data stay viewable - reopen to make it active again.';
  const openEngagement = () => {
    if (engRow && engRow.cid) navigate('consultant/' + engRow.cid);
    else navigate('engagements');
  };
  return (
    <div style={{ display: 'flex', alignItems: 'center', gap: 12, padding: '12px 14px', marginBottom: 14, borderRadius: 10, background: bg, border: '1px solid ' + border }}>
      <span className="material-symbols-outlined" style={{ fontSize: 22, color: textColor }}>{icon}</span>
      <div style={{ minWidth: 0, flex: 1 }}>
        <div style={{ fontSize: 'var(--ui-text-base)', fontWeight: 600, color: textColor }}>{title}</div>
        <div style={{ fontSize: 'var(--ui-text-sm)', color: 'var(--ui-text-secondary)', marginTop: 2 }}>{sub}</div>
      </div>
      {filled ? (
        <DSj.Button variant="secondary" size="sm" icon="badge" onClick={openEngagement}>Open engagement</DSj.Button>
      ) : (
        <DSj.Button variant="secondary" size="sm" icon="restart_alt" onClick={() => reopenJobOrder(job, showToast)}>Reopen job</DSj.Button>
      )}
    </div>
  );
}

/* ============================== ★ MATCHING TAB ============================== */
/* The talent pool a job ranks/reviews: roster candidates (no jobId) + that job's own
   qualified applicants - never another job's. qualify() stamps jobId (R78). */
const matchPoolFor = (jobId) => (window.E8DATA.matches || []).filter((m) => !m.jobId || m.jobId === jobId);

/* Effective queue state for a pool row: a persisted decision (E8Match) overrides the seed state.
   A match's authored state applies only to the job it was authored against - a job-specific applicant
   (m.jobId) to that job, a roster seed decision to its m.stateJob - so on every other job the row is
   undecided ('review'). No id-check: the flagship is just the job its seed decisions name (data-driven).
   Shared with the candidate review flow's queue resolution (screens-core.jsx). */
const matchStateOf = (m, jobId, decisions) => {
  if (decisions[m.id] && decisions[m.id].state) return decisions[m.id].state;
  const authoredJob = m.jobId || m.stateJob || null;
  return (!authoredJob || authoredJob === jobId) ? (m.state || 'review') : 'review';
};
const matchReviewCount = (jobId) => {
  const dec = window.E8Match ? window.E8Match.decisions(jobId) : {};
  return matchPoolFor(jobId).filter((m) => matchStateOf(m, jobId, dec) === 'review').length;
};

/* ---- Shared accept/reject semantics (matching-tab rows AND the candidate review flow) ----
   One decision path: E8Match decision + real submission + feedback nudge + audit + bus event.
   Each returns { existed?, undo } so every entry point gets a working Undo. */
function e8AcceptDecision(jobRow, m) {
  const D = window.E8DATA;
  const jobId = jobRow.id;
  const prevDec = window.E8Match.decisions(jobId)[m.id] || null;
  const prevW = window.E8Match.weights(jobId);
  window.E8Match.decide(jobId, m.id, 'accepted');
  const sid = 's-m-' + m.id + '-' + jobId;
  // A submission for this candidate+job may already exist (e.g. created by qualify) - never duplicate.
  const existed = D.submissions.some((x) => x.id === sid || (x.candId === m.id && x.jobId === jobId && x.open !== false));
  if (!existed) window.E8Store.add('submissions', { id: sid, cand: m.name, candId: m.id, candTitle: (m.title || '') + ' · ' + (m.company || ''), job: jobRow.title, jobId: jobId, client: jobRow.client, rate: m.rate || jobRow.rate || '$\u2014', stage: 0, when: 'Just now', owner: D.user.name, resp: { label: 'New', tone: 'neutral' }, open: true, stageHistory: [{ stage: 0, at: Date.now(), by: D.user.name }] });
  window.E8Match.feedback(jobId, m.id, 'accept', m.score != null ? m.score : null);
  if (window.E8Audit) window.E8Audit.log({ agent: 'You', prov: 'human', action: existed ? 'Accepted match' : 'Accepted match - submission created', target: m.name + ' \u2192 ' + (jobRow.title || jobId), route: 'submissions' });
  if (window.E8Events) window.E8Events.emit('match:accepted', { jobId: jobId, candId: m.id });
  return {
    existed,
    undo() {
      window.E8Match.decide(jobId, m.id, prevDec ? prevDec.state : null, prevDec);
      window.E8Match.setWeights(jobId, prevW);
      if (!existed) window.E8Store.remove('submissions', sid);
    },
  };
}
function e8RejectDecision(jobRow, m, value) {
  const jobId = jobRow.id;
  const decision = window.E8RejectPolicy.normalize(value);
  const prevDec = window.E8Match.decisions(jobId)[m.id] || null;
  const prevW = window.E8Match.weights(jobId);
  window.E8Match.decide(jobId, m.id, 'rejected', { rejectReason: decision });
  if (!decision.restricted) {
    window.E8Match.feedback(jobId, m.id, 'reject', m.score != null ? m.score : null);
  }
  if (window.E8Audit) window.E8Audit.log({
    agent: 'You',
    prov: 'human',
    action: 'Rejected candidate · ' + decision.category + ' · ' + decision.reason,
    target: m.name + ' \u2192 ' + (jobRow.title || jobId),
    route: 'job/' + jobId + '/matching',
    category: decision.category,
    reason: decision.reason,
    restricted: decision.restricted,
  });
  if (window.E8Events) window.E8Events.emit(
    'match:rejected',
    Object.assign({ jobId, candId: m.id }, window.E8RejectPolicy.metadata(decision)),
  );
  return {
    decision,
    undo() {
      window.E8Match.decide(jobId, m.id, prevDec ? prevDec.state : null, prevDec);
      window.E8Match.setWeights(jobId, prevW);
    },
  };
}

/* candidateJobs seed reasons are plain strings; give them why?-popover weights.
   Caveat-flavored lines read as negatives; the rest carry the tier's positive weight. */
const seedReasonWeight = (label, tier) =>
  (/\b(no|not|needed|ramp|junior|declined|legacy|unconfirmed|upgrade|but)\b/i.test(label) ? -1 : tier === 1 ? 3 : tier === 2 ? 2 : 1);

/* ---- Redeployment matches strip - closes the radar -> matching loop. ----
   Verified ELEV8 consultants (rolling off soon or on the bench) whose redeploy scores
   include THIS job, shown above the review queue. Eligibility comes from the radar's
   shared helper (rdrRollOffs, screens-today.jsx); scores from E8Match.redeployJobs. */
function RedeployMatchStrip({ jobId }) {
  const D = window.E8DATA;
  if (!window.E8Match || !window.rdrRollOffs) return null;
  const seen = {};
  const rows = [].concat(
    window.rdrRollOffs(6).map((e) => ({ id: e.cid, name: e.consultant, role: e.role, skills: e.skills, ends: e.ends })),
    (D.bench || []).map((b) => ({ id: b.id, name: b.name, role: b.role, skills: b.skills, bench: true, avail: b.available }))
  )
    .filter((p) => (seen[p.id] ? false : (seen[p.id] = true)))
    .map((p) => {
      const m = window.E8Match.redeployJobs(p.id, p.skills).find((x) => x.jobId === jobId);
      return m ? Object.assign({}, p, { m }) : null;
    })
    .filter(Boolean)
    .sort((a, b) => b.m.score - a.m.score)
    .slice(0, 3);
  if (!rows.length) return null;
  return (
    <div className="e8-rdm">
      <div className="e8-rdm-head">
        <DSj.E8Mark />
        <span className="e8-rdm-title">Redeployment matches - verified ELEV8 consultants</span>
        <span className="e8-rdm-sub">rolling off or on the bench, scored against this req</span>
      </div>
      {rows.map((p) => (
        <div key={p.id} className="e8-rdm-row">
          <DSj.Avatar name={p.name} size="sm" />
          <span className="e8-rdm-id">
            <span className="e8-rdm-name">{p.name} <DSj.E8Mark title="ELEV8-verified consultant" /></span>
            <span className="e8-rdm-role">{p.role}</span>
          </span>
          <span className="e8-rdm-rolloff">{p.bench ? 'On bench · available ' + p.avail : (window.rdrRolloffLabel ? window.rdrRolloffLabel(p.ends) : 'Rolls off ' + p.ends)}</span>
          <DSj.MatchBadge
            tier={p.m.tier}
            score={p.m.score}
            reasons={(p.m.reasons || []).map((label) => ({ label, weight: seedReasonWeight(label, p.m.tier) }))}
            footnote={p.m.overlap ? 'Skill overlap vs the job requirements - run matching for measured scores' : p.m.live ? 'Live on-device score' : 'Simulated sample score'}
          />
          <span style={{ flex: 1 }}></span>
          <DSj.Button variant="secondary" size="sm" icon="badge" onClick={() => navigate(p.bench ? 'consultants/bench' : 'consultant/' + p.id)}>
            {p.bench ? 'View bench' : 'View consultant'}
          </DSj.Button>
        </div>
      ))}
    </div>
  );
}

function MatchingTab({ job }) {
  const D = window.E8DATA;
  const j = job || D.job;
  const jobId = j.id;
  const { showToast, density } = React.useContext(E8Ctx);
  const ai = useOnDeviceAI();
  /* R79: scores come from E8Match (real, on-device) when a run exists; the seed tiers are the
     labeled simulated sample until then. Decisions persist per job; accept creates a submission. */
  const [run, setRun] = React.useState(() => window.E8Match.scores(jobId));
  const [running, setRunning] = React.useState(false);
  const [runDetail, setRunDetail] = React.useState('');
  const [weights, setWeights] = React.useState(() => window.E8Match.weights(jobId));
  const [tuneOpen, setTuneOpen] = React.useState(false);
  const [decisions, setDecisions] = React.useState(() => window.E8Match.decisions(jobId));
  const [queue, setQueue] = React.useState('review');
  const [rejecting, setRejecting] = React.useState(null);

  const pool = matchPoolFor(jobId);
  const rows0 = pool.map((m) => {
    const r = run && run.results[m.id];
    if (r) return { ...m, tier: r.tier, score: r.score, reasons: r.reasons, live: true };
    /* Tier pills always carry a number: seed scores (D.candidateJobs) show until a live run
       replaces them. Tier joins per-job from the same candidateJobs entry, so a roster row's
       JO-44219-era authored tier never contradicts a job-specific score on another job's tab
       (e.g. Hana Kobayashi: very weak on the Java req, very strong 90 on the Buckman data req).
       When the joined tier differs from the authored one, the authored why-reasons would lie
       too - swap in the entry's own reasons with synthesized weights (if the entry carries no
       reasons, the authored ones are silently kept). */
    const cj = ((D.candidateJobs || {})[m.id] || []).find((e) => e.jobId === jobId);
    if (!cj) return { ...m, score: undefined, live: false };
    const reasons = cj.tier !== m.tier && cj.reasons && cj.reasons.length
      ? cj.reasons.map((label) => ({ label, weight: seedReasonWeight(label, cj.tier) }))
      : m.reasons;
    return { ...m, tier: cj.tier || m.tier, score: cj.score, reasons, live: false };
  });
  const stateOf = (m) => matchStateOf(m, jobId, decisions);
  const refresh = () => setDecisions({ ...window.E8Match.decisions(jobId) });
  const relTime = (iso) => {
    const mins = Math.max(0, Math.round((Date.now() - new Date(iso).getTime()) / 60000));
    return mins < 1 ? 'just now' : mins < 60 ? mins + 'm ago' : Math.round(mins / 60) + 'h ago';
  };

  /* Accept/reject go through the shared decision path (e8AcceptDecision / e8RejectDecision) -
     the same semantics the candidate review flow uses, so both entries create real
     submissions/decisions with working undo. */
  const accept = (m) => {
    const res = e8AcceptDecision(j, m);
    setWeights(window.E8Match.weights(jobId));
    refresh();
    showToast(m.name + (res.existed ? ' accepted' : ' accepted - submission created'), 'Undo', () => {
      res.undo();
      setWeights(window.E8Match.weights(jobId));
      if (run) setRun({ ...window.E8Match.reblend(jobId) });
      refresh();
    });
  };

  const rejectWithReason = (id, payload) => {
    const m = rows0.find((x) => x.id === id);
    const res = e8RejectDecision(j, m, payload);
    setWeights(window.E8Match.weights(jobId));
    setRejecting(null); refresh();
    showToast(`${m.name} rejected \u00b7 ${res.decision.reason}`, 'Undo', () => {
      res.undo();
      setWeights(window.E8Match.weights(jobId));
      if (run) setRun({ ...window.E8Match.reblend(jobId) });
      refresh();
    });
  };

  const reopen = (m, from) => {
    const prevDec = window.E8Match.decisions(jobId)[m.id] || null;
    const sid = 's-m-' + m.id + '-' + jobId;
    const hadSub = from === 'accepted' && D.submissions.some((x) => x.id === sid);
    const sub = hadSub ? D.submissions.find((x) => x.id === sid) : null;
    if (hadSub) window.E8Store.remove('submissions', sid);
    /* Reopening always lands the row back in the review queue with an explicit decision, on any job -
       this overrides an authored accepted/rejected seed state (e.g. the flagship's seeded picks) without
       privileging a specific job id. Undo restores the prior decision below. */
    window.E8Match.decide(jobId, m.id, 'review');
    refresh();
    showToast(m.name + ' reopened' + (hadSub ? ' - submission withdrawn' : ''), 'Undo', () => {
      window.E8Match.decide(jobId, m.id, prevDec ? prevDec.state : from, prevDec);
      if (hadSub && sub) window.E8Store.add('submissions', sub);
      refresh();
    });
  };

  const runMatching = async () => {
    if (running) return;
    setRunning(true); setRunDetail('');
    try {
      if (!window.E8AI.getState().enabled) window.E8AI.enable();
      const res = await window.E8Match.rank(j, { onStatus: (st) => setRunDetail(st.detail || '') });
      setRun({ ...res });
      showToast('Ranked ' + Object.keys(res.results).length + ' candidates on-device in ' + res.secs + 's');
    } catch (e) { showToast('On-device matching unavailable in this browser'); }
    finally { setRunning(false); setRunDetail(''); }
  };

  const setW = (f, v) => {
    const w = { ...weights, [f]: v };
    setWeights(w);
    window.E8Match.setWeights(jobId, w);
    if (run) setRun({ ...window.E8Match.reblend(jobId) });
  };

  const counts = { review: 0, accepted: 0, rejected: 0 };
  rows0.forEach((m) => { const st = stateOf(m); if (counts[st] != null) counts[st] += 1; });
  const rows = rows0.filter((m) => stateOf(m) === queue).sort((a, b) => (run ? (b.score || 0) - (a.score || 0) : a.tier - b.tier));

  return (
    <div>
      {/* Queue sub-tabs + criteria chip (opens the weights editor) + the run affordance */}
      <div style={{ display: 'flex', alignItems: 'center', gap: 14, flexWrap: 'wrap', marginBottom: 12 }}>
        <DSj.SubTabs
          items={[
            { id: 'review', label: 'For review', count: counts.review },
            { id: 'accepted', label: 'Accepted', count: counts.accepted },
            { id: 'rejected', label: 'Rejected', count: counts.rejected },
          ]}
          active={queue}
          onChange={setQueue}
        />
        <button type="button" className="e8-mcrit" aria-expanded={tuneOpen} title="Matching criteria and weights" onClick={() => setTuneOpen((v) => !v)}>
          <span className="material-symbols-outlined" aria-hidden="true">tune</span>
          Matching · {window.E8Match.FACETS.length} criteria
          <span className="material-symbols-outlined" aria-hidden="true">{tuneOpen ? 'expand_less' : 'expand_more'}</span>
        </button>
        <span style={{ marginLeft: 'auto', display: 'inline-flex', alignItems: 'center', gap: 8 }}>
          <span style={{ fontSize: 'var(--ui-text-sm)', color: 'var(--ui-text-tertiary)' }}>
            {running ? (runDetail || ai.detail || 'Ranking on-device\u2026') : run ? 'Ranked ' + relTime(run.at) + ' \u00b7 on-device \u00b7 ' + run.secs + 's' : 'Sample ranking (simulated)'}
          </span>
          <DSj.Button variant="secondary" size="sm" icon={running ? 'progress_activity' : 'join_inner'} disabled={running} onClick={runMatching}>
            {run ? 'Re-run matching' : (ai.enabled ? 'Run matching on-device' : 'Enable on-device AI & rank')}
          </DSj.Button>
        </span>
      </div>

      {run ? (
        <div className="e8-qrefresh" style={{ margin: '-4px 0 12px' }}>
          <span className="material-symbols-outlined" aria-hidden="true">auto_awesome</span>
          <span>Queue refreshed {new Date(run.at).toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })} - re-scores on ELEV8 sync</span>
        </div>
      ) : null}

      {tuneOpen ? (
        <div className="e8-card" style={{ boxShadow: 'none', padding: '10px 14px', marginBottom: 12, display: 'grid', gap: 8 }}>
          {window.E8Match.FACETS.map((f) => (
            <label key={f} style={{ display: 'flex', alignItems: 'center', gap: 10, fontSize: 'var(--ui-text-sm)' }}>
              <span style={{ width: 170, flex: 'none', color: 'var(--ui-text-secondary)' }}>{window.E8Match.FACET_LABEL[f]}</span>
              <input type="range" min="0.5" max="4" step="0.5" value={weights[f]} onChange={(e) => setW(f, parseFloat(e.target.value))} style={{ flex: 1, accentColor: 'var(--ui-accent)' }} aria-label={window.E8Match.FACET_LABEL[f] + ' weight'} />
              <span className="tnum" style={{ width: 34, flex: 'none' }}>×{weights[f]}</span>
            </label>
          ))}
          <span style={{ fontSize: 'var(--ui-text-xs)', color: 'var(--ui-text-tertiary)' }}>Weights re-blend the stored facet scores instantly - no re-embedding. Your accept/reject decisions nudge them too.</span>
        </div>
      ) : null}

      {queue === 'review' ? <RedeployMatchStrip jobId={jobId} /> : null}

      {rows.length ? (
        <ConfigurableTable listKey="job-matches"
          density={density}
          onRowClick={(r) => navigate('candidate/' + r.id + (queue === 'review' ? '?q=review-' + jobId : ''))}
          registry={[
            { key: 'name', label: 'Name', render: (r) => <NameCell name={r.name} sub={r.location} linkedin={r.linkedin} /> },
            { key: 'tier', label: 'Score', icon: 'join_inner', render: (r) => (
              <DSj.MatchBadge tier={r.tier} score={r.score} reasons={r.reasons} footnote={r.live
                ? 'On-device blend \u00b7 ' + Object.keys(run.results[r.id].facets).map((f) => window.E8Match.FACET_LABEL[f].split(' ')[0] + ' ' + Math.round(run.results[r.id].facets[f] * 100) + '%').join(' \u00b7 ')
                : 'Simulated sample \u00b7 run matching for live on-device scores'} />
            ) },
            { key: 'email', label: 'Personal email', icon: 'alternate_email', render: (r) => <CellChip icon="mail">{r.email}</CellChip> },
            { key: 'phone', label: 'Personal phone', icon: 'call', render: (r) => <CellChip icon="call">{r.phone}</CellChip> },
            { key: 'exp', label: 'Experience', icon: 'work', render: (r) => <ExpCell title={r.title} company={r.company} years={r.years} more={r.moreRoles} /> },
            { key: 'skills', label: 'Skills', icon: 'sell', render: (r) => <DSj.SkillChips skills={r.skills} max={3} /> },
          ]}
          rows={rows}
          rowActions={(r) =>
            queue === 'review' ? (
              <React.Fragment>
                <DSj.Button variant="accept" size="sm" icon="check" onClick={() => accept(r)}>Accept</DSj.Button>
                <DSj.Button variant="reject" size="sm" icon="close" onClick={() => setRejecting(r.id)}>Reject</DSj.Button>
              </React.Fragment>
            ) : (
              <React.Fragment>
                {queue === 'rejected' ? <RejectionSummary value={(decisions[r.id] || {}).rejectReason || ((decisions[r.id] || {}).reason ? decisions[r.id] : null) || r.rejectReason} /> : null}
                <DSj.Button variant="ghost" size="sm" icon="history" onClick={() => reopen(r, queue)}>Reopen</DSj.Button>
              </React.Fragment>
            )
          }
        />
      ) : (
        <div className="e8-card" style={{ boxShadow: 'none' }}>
          {queue === 'review' ? (
            <DSj.EmptyState
              icon="auto_awesome"
              title="Queue clear"
              body="New candidates are scored automatically when ELEV8 syncs."
              cta={<DSj.Button variant="secondary" size="sm" className="e8-empty-cta" icon="travel_explore" onClick={() => showToast('Sourcing agent widened the search - 4 new candidates queued for scoring')}>Widen search</DSj.Button>}
            />
          ) : queue === 'accepted' ? (
            <DSj.EmptyState
              icon="thumb_up"
              title="No accepted candidates yet"
              body="Accept candidates from the review queue to build your submittal shortlist."
              cta={<DSj.Button variant="secondary" size="sm" className="e8-empty-cta" onClick={() => setQueue('review')}>Open review queue</DSj.Button>}
            />
          ) : (
            <DSj.EmptyState
              icon="thumb_down"
              title="Nothing rejected"
              body="Rejected candidates land here with their reason, so decisions stay auditable."
              cta={<DSj.Button variant="secondary" size="sm" className="e8-empty-cta" onClick={() => setQueue('review')}>Open review queue</DSj.Button>}
            />
          )}
        </div>
      )}

      <div style={{ display: 'flex', alignItems: 'center', gap: 10, marginTop: 12, fontSize: 'var(--ui-text-base)', color: 'var(--ui-text-secondary)' }}>
        {run ? (
          <React.Fragment>
            <DSj.ProvenanceBadge kind="advised" />
            Scores computed on-device ({run.engine}) - candidate data never left this browser. Accepting creates a real submission.
          </React.Fragment>
        ) : queue === 'accepted' && counts.accepted > 0 ? (
          <React.Fragment>
            <DSj.ProvenanceBadge kind="qa" />
            Accepted matches carry AI + human QA provenance on submission.
          </React.Fragment>
        ) : null}
      </div>

      {rejecting ? (
        <RejectReasonDialog
          name={(rows0.find((m) => m.id === rejecting) || {}).name}
          onConfirm={(payload) => rejectWithReason(rejecting, payload)}
          onClose={() => setRejecting(null)}
        />
      ) : null}
    </div>
  );
}

/* ---------- Secondary job tabs ---------- */
/* R99 Task 2: rich requisition view building blocks. Each section renders only when it
   has data; tokens only (ink via --ui-*-text; status via success/warning tones). */
const REQ_WEEK = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri'];
function reqCap(s) { s = String(s || ''); return s ? s.charAt(0).toUpperCase() + s.slice(1) : s; }
function reqHas(v) { return v != null && v !== '' && !(Array.isArray(v) && v.length === 0); }

function ReqSection({ label, badge, children }) {
  return (
    <div className="e8-req-sec">
      <div className="e8-req-head">
        <span className="e8-sectionlabel">{label}</span>
        {badge || null}
      </div>
      {children}
    </div>
  );
}

function ReqFact({ k, v, tnum, tone }) {
  if (!reqHas(v)) return null;
  return (
    <div className="e8-req-fact">
      <div className="e8-req-k">{k}</div>
      <div className={'e8-req-v' + (tnum ? ' tnum' : '')} style={tone ? { color: tone } : null}>{v}</div>
    </div>
  );
}

/* The star of the requisition: the conditional location/schedule block. */
function ReqLocationBlock({ loc }) {
  if (!loc) return null;
  const type = loc.type || 'onsite';
  const title = type === 'remote' ? 'Remote' : type === 'hybrid'
    ? 'Hybrid · ' + loc.daysOnsite + ' day' + (loc.daysOnsite === 1 ? '' : 's') + ' onsite'
    : 'Onsite · 5 days onsite';
  const remoteSub = [loc.timezone ? 'works ' + loc.timezone + ' hours' : null, loc.geo].filter(Boolean).join(' · ');
  return (
    <div className="e8-req-loc">
      <div className="e8-req-loc-head">
        <span className="material-symbols-outlined e8-req-loc-icon">{type === 'remote' ? 'home_work' : 'location_on'}</span>
        <div style={{ minWidth: 0 }}>
          <div className="e8-req-loc-title">{title}</div>
          {type === 'remote'
            ? (remoteSub ? <div className="e8-req-loc-sub">{remoteSub}</div> : null)
            : (loc.address ? <div className="e8-req-loc-sub">{loc.address}</div> : null)}
        </div>
      </div>
      {type === 'hybrid' ? (
        <div className="e8-req-days" role="list" aria-label="Weekly onsite schedule">
          {REQ_WEEK.map((d) => {
            const on = (loc.onsiteDays || []).indexOf(d) > -1;
            return (
              <span key={d} role="listitem" className={'e8-req-day' + (on ? ' on' : '')} title={on ? d + ' - onsite' : d + ' - remote'}>{d}</span>
            );
          })}
        </div>
      ) : null}
      {type === 'hybrid' && loc.remoteDays && loc.remoteDays.length ? (
        <div className="e8-req-loc-note">Remote {loc.remoteDays.join(', ')}</div>
      ) : null}
      {loc.flexNote ? <div className="e8-req-loc-note">{loc.flexNote}</div> : null}
    </div>
  );
}

function JobOverviewTab({ job, det, flagship, draftedScope }) {
  const req = window.reqOf ? window.reqOf(job.id) : null;

  /* Graceful fallback - a job with a typed loc but no full requisition keeps the thin scope view. */
  if (!req) {
    const description = det ? det.description : null;
    const loc = job.loc && window.jobLocDetail ? window.jobLocDetail(job.loc) : null;
    return (
      <div style={{ display: 'grid', gap: 18, maxWidth: 760 }}>
        <div>
          <div className="e8-sectionlabel" style={{ marginBottom: 8 }}>Engagement</div>
          <div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, auto)', gap: 24, justifyContent: 'start' }}>
            {[['Bill rate', job.rate], ['Type', job.type], ['Openings', String(job.openings)], ['Owner', job.owner]].map(([k, v]) => (
              <div key={k}>
                <div style={{ fontSize: 'var(--ui-text-sm)', color: 'var(--ui-text-tertiary)' }}>{k}</div>
                <div className="tnum" style={{ fontSize: 'var(--ui-text-md)', fontWeight: 500, marginTop: 2 }}>{v}</div>
              </div>
            ))}
          </div>
        </div>
        {loc ? <ReqSection label="Location & schedule"><ReqLocationBlock loc={loc} /></ReqSection> : null}
        <div>
          <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}>
            <span className="e8-sectionlabel">Description</span>
            {draftedScope ? <DSj.ProvenanceBadge kind="drafted" /> : null}
          </div>
          <p style={{ margin: 0, fontSize: 'var(--ui-text-base)', lineHeight: 1.6, color: 'var(--ui-text-secondary)', maxWidth: 720, textWrap: 'pretty' }}>
            {description || 'Scope summary drafted from the intake notes - confirm rate, on-site cadence and must-haves with the sponsor before first submittals. Source emails are linked from the intake record.'}
          </p>
        </div>
      </div>
    );
  }

  const prov = req.prov || {};
  const loc = job.loc && window.jobLocDetail ? window.jobLocDetail(job.loc) : null;
  const isContract = req.roleType === 'contract' || req.roleType === 'c2h';
  const margin = isContract && req.billRate ? Math.round(((req.billRate - req.payRate) / req.billRate) * 100) : null;
  const jdParas = (req.jobDescription || '').split('\n\n').filter(Boolean);
  const hm = req.hiringManager || null;
  const compliance = [
    ['Active MSA', req.activeMSA], ['SOW signed', req.sowSigned], ['Req approved', req.reqApproved],
  ];
  const hasContext = reqHas(req.reasonForOpening) || reqHas(req.howLongOpen) || reqHas(req.totalOpenings)
    || reqHas(req.startTarget) || reqHas(req.nextMeeting) || req.candidatesInProcess != null;

  return (
    <div className="e8-req">
      {/* Job description */}
      {jdParas.length ? (
        <ReqSection label="Job description" badge={prov.jobDescription === 'drafted' ? <DSj.ProvenanceBadge kind="drafted" /> : null}>
          <div className="e8-req-jd">
            {jdParas.map((p, i) => <p key={i}>{p}</p>)}
          </div>
          {req.projectDescription ? (
            <div style={{ marginTop: 12 }}>
              <div className="e8-req-subhead">
                The project {prov.projectDescription === 'drafted' ? <DSj.ProvenanceBadge kind="drafted" /> : null}
              </div>
              <div className="e8-req-jd"><p>{req.projectDescription}</p></div>
            </div>
          ) : null}
        </ReqSection>
      ) : null}

      {/* Location & schedule - the star */}
      {loc ? (
        <ReqSection label="Location & schedule">
          <ReqLocationBlock loc={loc} />
          {(reqHas(req.hoursPerWeek) || reqHas(req.shift) || req.equipmentProvided != null) ? (
            <div className="e8-req-grid" style={{ marginTop: 12 }}>
              <ReqFact k="Hours / week" v={reqHas(req.hoursPerWeek) ? req.hoursPerWeek : null} tnum />
              <ReqFact k="Shift" v={req.shift} />
              <ReqFact k="Equipment" v={req.equipmentProvided != null ? (req.equipmentProvided ? 'Provided' : 'Bring your own') : null} />
            </div>
          ) : null}
        </ReqSection>
      ) : null}

      {/* Compensation - role-type aware */}
      {(isContract ? reqHas(req.billRate) : reqHas(req.salaryRange)) ? (
        <ReqSection label="Compensation">
          {isContract ? (
            <div className="e8-req-grid">
              <ReqFact k="Bill rate" v={reqHas(req.billRate) ? '$' + req.billRate + '/hr' : null} tnum />
              <ReqFact k="Pay rate" v={reqHas(req.payRate) ? '$' + req.payRate + '/hr' : null} tnum />
              {margin != null ? (
                <div className="e8-req-fact">
                  <div className="e8-req-k">Margin</div>
                  <div className="e8-req-v tnum" style={{ color: 'var(--ui-success-text)' }}>
                    {margin}% <span className="e8-req-computed">computed</span>
                  </div>
                </div>
              ) : null}
              <ReqFact k="Max bill" v={reqHas(req.maxBill) ? '$' + req.maxBill + '/hr' : null} tnum />
              <ReqFact k="Rate" v={req.flexRate ? 'Flexible' : null} />
              <ReqFact k="Extension likelihood" v={reqHas(req.extensionLikelihood) ? reqCap(req.extensionLikelihood) : null} />
            </div>
          ) : (
            <div className="e8-req-grid">
              <ReqFact k="Salary range" v={req.salaryRange} tnum />
            </div>
          )}
        </ReqSection>
      ) : null}

      {/* Requirements */}
      {(reqHas(req.mustHaves) || reqHas(req.niceToHaves) || reqHas(req.workAuth) || req.backgroundCheck != null || reqHas(req.screeningQuestions)) ? (
        <ReqSection label="Requirements" badge={prov.mustHaves === 'drafted' ? <DSj.ProvenanceBadge kind="drafted" /> : null}>
          {reqHas(req.mustHaves) ? (
            <div>
              <div className="e8-req-subhead">Must-haves</div>
              <div className="e8-req-list">
                {req.mustHaves.map((m, i) => (
                  <div key={i} className="e8-req-li">
                    <span className="material-symbols-outlined e8-req-check">check_circle</span>
                    <span>{m}</span>
                  </div>
                ))}
              </div>
            </div>
          ) : null}
          {reqHas(req.niceToHaves) ? (
            <div style={{ marginTop: 12 }}>
              <div className="e8-req-subhead">Nice to have</div>
              <div className="e8-req-chips">
                <DSj.SkillChips skills={req.niceToHaves} max={(req.niceToHaves || []).length} style={{ flexWrap: 'wrap' }} />
              </div>
            </div>
          ) : null}
          {(reqHas(req.workAuth) || req.backgroundCheck != null) ? (
            <div className="e8-req-grid" style={{ marginTop: 12 }}>
              <ReqFact k="Work authorization" v={req.workAuth} />
              <ReqFact k="Background / drug screen" v={req.backgroundCheck != null ? (req.backgroundCheck ? 'Required' : 'Not required') : null} />
            </div>
          ) : null}
          {reqHas(req.screeningQuestions) ? (
            <div style={{ marginTop: 12 }}>
              <div className="e8-req-subhead">
                Screening questions {prov.screeningQuestions === 'drafted' ? <DSj.ProvenanceBadge kind="drafted" /> : null}
              </div>
              <p className="e8-req-para">{req.screeningQuestions}</p>
            </div>
          ) : null}
        </ReqSection>
      ) : null}

      {/* Interview process */}
      {reqHas(req.interviewRounds) ? (
        <ReqSection label="Interview process">
          <div className="e8-req-rounds">
            {req.interviewRounds.map((r, i) => (
              <div key={i} className="e8-req-round">
                <span className="e8-req-num tnum">{i + 1}</span>
                <div style={{ minWidth: 0 }}>
                  <div className="e8-req-round-fmt">{r.format}</div>
                  {r.interviewer ? <div className="e8-req-round-who">{r.interviewer}</div> : null}
                </div>
              </div>
            ))}
          </div>
        </ReqSection>
      ) : null}

      {/* Why this role */}
      {(reqHas(req.sellingPoints) || reqHas(req.advantages) || reqHas(req.competition)) ? (
        <ReqSection label="Why this role" badge={prov.sellingPoints === 'drafted' ? <DSj.ProvenanceBadge kind="drafted" /> : null}>
          {reqHas(req.sellingPoints) ? <p className="e8-req-para">{req.sellingPoints}</p> : null}
          {reqHas(req.advantages) ? (
            <div style={{ marginTop: 10 }}>
              <div className="e8-req-subhead">Our advantage</div>
              <p className="e8-req-para">{req.advantages}</p>
            </div>
          ) : null}
          {reqHas(req.competition) ? (
            <div className="e8-req-grid" style={{ marginTop: 12 }}>
              <ReqFact k="Competition" v={req.competition} />
            </div>
          ) : null}
        </ReqSection>
      ) : null}

      {/* Context & compliance */}
      {(hasContext || hm) ? (
        <ReqSection label="Context & compliance">
          {hasContext ? (
            <div className="e8-req-grid">
              <ReqFact k="Reason for opening" v={reqHas(req.reasonForOpening) ? reqCap(req.reasonForOpening) : null} />
              <ReqFact k="Open for" v={req.howLongOpen} />
              <ReqFact k="Candidates in process" v={req.candidatesInProcess != null ? (req.candidatesInProcess ? 'Yes' : 'None yet') : null} />
              <ReqFact k="Total openings" v={reqHas(req.totalOpenings) ? String(req.totalOpenings) : null} tnum />
              <ReqFact k="Target start" v={req.startTarget} />
              <ReqFact k="Next meeting" v={req.nextMeeting} />
            </div>
          ) : null}
          <div className="e8-req-status" style={{ marginTop: hasContext ? 14 : 0 }}>
            {compliance.map(([label, ok]) => (
              <span key={label} className={'e8-req-tag ' + (ok ? 'ok' : 'missing')}>
                <span className="material-symbols-outlined">{ok ? 'check' : 'error'}</span>
                {label}
              </span>
            ))}
          </div>
          {hm ? (
            <div className="e8-req-hm">
              <DSj.Avatar name={hm.name} size="sm" />
              <div style={{ minWidth: 0, flex: 1 }}>
                <div className="e8-req-hm-name">{hm.name}</div>
                {hm.title ? <div className="e8-req-hm-title">{hm.title}</div> : null}
                <div className="e8-req-hm-links">
                  {hm.email ? <a className="e8-req-hm-link" href={'mailto:' + hm.email}><span className="material-symbols-outlined">mail</span>{hm.email}</a> : null}
                  {hm.phone ? <a className="e8-req-hm-link" href={'tel:' + hm.phone.replace(/[^0-9+]/g, '')}><span className="material-symbols-outlined">call</span>{hm.phone}</a> : null}
                </div>
              </div>
            </div>
          ) : null}
        </ReqSection>
      ) : null}
    </div>
  );
}

function JobSubmissionsTab({ rows }) {
  const D = window.E8DATA;
  const { density, showToast } = React.useContext(E8Ctx);
  if (!rows.length) {
    return (
      <div style={{ maxWidth: 460, margin: '24px auto' }}>
        <DSj.EmptyState icon="send" title="No submittals yet" body="Accepted matches become submittals - the packet is drafted for you the moment you accept." />
      </div>
    );
  }
  return (
    <ConfigurableTable listKey="job-submissions"
      density={density}
      onRowClick={(r) => (r.candId ? navigate('candidate/' + r.candId) : showToast('Candidate record not in demo data'))}
      registry={[
        { key: 'cand', label: 'Candidate', render: (r) => <NameCell name={r.cand} sub={r.candTitle} size="sm" /> },
        { key: 'stage', label: 'Stage', width: 190, render: (r) => <DSj.StageCell stages={D.submissionStages} current={r.stage} /> },
        { key: 'resp', label: 'Client response', render: (r) => <DSj.Badge tone={r.resp.tone} dot={r.resp.tone !== 'neutral'}>{r.resp.label}</DSj.Badge> },
        { key: 'rate', label: 'Submitted rate', num: true },
        { key: 'owner', label: 'Submitted by', render: (r) => <ActorChip name={r.owner} /> },
        { key: 'when', label: 'Submitted', secondary: true },
      ]}
      rows={rows}
    />
  );
}

/* ---------- Analytics: live funnel from pipeline counts ---------- */
function JobAnalyticsTab({ job, flagship, setTab }) {
  const p = job.pipeline;
  const hasData = pipelineTotal(p) > 0;
  if (!hasData) {
    return (
      <div style={{ maxWidth: 460, margin: '24px auto' }}>
        <DSj.EmptyState
          icon="monitoring"
          title="Analytics arrive with pipeline activity"
          body="Funnel and time-in-stage charts populate once matching surfaces the first candidates."
          cta={<DSj.Button variant="secondary" size="sm" className="e8-empty-cta" onClick={() => setTab('matching')}>Go to matching</DSj.Button>}
        />
      </div>
    );
  }
  const stages = window.E8DATA.pipelineStages.filter((s) => s.kind !== 'lost').map((s) => [s.label, p[s.key] || 0]);
  const max = Math.max(...stages.map(([, n]) => n), 1);

  /* R104 T3: time-in-stage + interview/offer counts derived from the real submission stageHistory for
     this job (no hardcoded strings). Each consecutive {stage, at} pair is a real transition; the avg of
     its day-deltas is the time in that stage. Advancing a submittal appends a new entry, so these numbers
     move with the pipeline. */
  const subStages = window.E8DATA.submissionStages || [];
  const jobSubs = (window.E8DATA.submissions || []).filter((s) => s.jobId === job.id);
  const iInterview = subStages.indexOf('Interview'), iOffer = subStages.indexOf('Offer');
  const trans = {};
  let interviews = 0, offers = 0;
  jobSubs.forEach((s) => {
    const h = (Array.isArray(s.stageHistory) ? s.stageHistory.slice() : []).sort((a, b) => a.at - b.at);
    for (let i = 1; i < h.length; i++) {
      const from = subStages[h[i - 1].stage], to = subStages[h[i].stage];
      if (from == null || to == null || from === to) continue;
      const key = from + ' → ' + to;
      (trans[key] = trans[key] || []).push(Math.max(0, (h[i].at - h[i - 1].at) / 864e5));
    }
    if ((s.stage | 0) === iInterview) interviews++;
    if ((s.stage | 0) === iOffer) offers++;
  });
  const timeRows = Object.keys(trans).map((k) => {
    const arr = trans[k];
    const avg = arr.reduce((n, d) => n + d, 0) / arr.length;
    return [k, avg.toFixed(1) + ' days', arr.length > 1 ? 'avg across ' + arr.length : null];
  });
  return (
    <div style={{ display: 'grid', gap: 24, maxWidth: 720 }}>
      <div>
        <div className="e8-sectionlabel" style={{ marginBottom: 10 }}>Pipeline funnel</div>
        <div style={{ display: 'grid', gap: 7 }}>
          {stages.map(([label, n], i) => {
            const prev = i > 0 ? stages[i - 1][1] : null;
            const conv = prev ? Math.round((n / prev) * 100) : null;
            return (
              <div key={label} style={{ display: 'grid', gridTemplateColumns: '90px 1fr 90px', gap: 12, alignItems: 'center' }}>
                <span style={{ fontSize: 'var(--ui-text-sm)', color: 'var(--ui-text-secondary)' }}>{label}</span>
                <span style={{ height: 20, background: 'var(--ui-fill)', borderRadius: 5, overflow: 'hidden', display: 'flex' }}>
                  <span style={{ width: Math.max((n / max) * 100, n ? 4 : 0) + '%', background: n ? 'var(--ui-daybreak)' : 'transparent', borderRadius: 5, transition: 'width 200ms ease-out' }}></span>
                </span>
                <span className="tnum" style={{ fontSize: 'var(--ui-text-sm)', color: 'var(--ui-text-secondary)' }}>
                  {n}{conv !== null && prev ? <span style={{ color: 'var(--ui-text-tertiary)' }}> · {conv}%</span> : null}
                </span>
              </div>
            );
          })}
        </div>
        <div style={{ fontSize: 'var(--ui-text-sm)', color: 'var(--ui-text-tertiary)', marginTop: 8 }}>Live counts as of this morning’s matching refresh.</div>
      </div>
      {jobSubs.length ? (
        <div>
          <div className="e8-sectionlabel" style={{ marginBottom: 10 }}>Time in stage</div>
          {timeRows.length ? timeRows.map(([k, v, note]) => (
            <div key={k} style={{ display: 'flex', gap: 12, alignItems: 'baseline', padding: '7px 0', borderBottom: '1px solid var(--ui-border)' }}>
              <span style={{ width: 220, flex: 'none', fontSize: 'var(--ui-text-sm)', color: 'var(--ui-text-secondary)' }}>{k}</span>
              <span className="tnum" style={{ fontSize: 'var(--ui-text-sm)', fontWeight: 500 }}>{v}</span>
              {note ? <span style={{ fontSize: 'var(--ui-text-sm)', color: 'var(--ui-text-tertiary)' }}>{note}</span> : null}
            </div>
          )) : <div style={{ fontSize: 'var(--ui-text-sm)', color: 'var(--ui-text-tertiary)', padding: '7px 0' }}>No stage moves recorded yet - advance a submittal to start the clock.</div>}
          <div style={{ display: 'flex', gap: 20, marginTop: 12, fontSize: 'var(--ui-text-sm)', color: 'var(--ui-text-secondary)' }}>
            <span>Interviews <span className="tnum" style={{ fontWeight: 600, color: 'var(--ui-text)' }}>{interviews}</span></span>
            <span>Offers <span className="tnum" style={{ fontWeight: 600, color: 'var(--ui-text)' }}>{offers}</span></span>
            <span>Submittals <span className="tnum" style={{ fontWeight: 600, color: 'var(--ui-text)' }}>{jobSubs.length}</span></span>
          </div>
          <div style={{ fontSize: 'var(--ui-text-sm)', color: 'var(--ui-text-tertiary)', marginTop: 8 }}>Derived from submission stage history - updates as submittals move.</div>
        </div>
      ) : null}
    </div>
  );
}

/* ============================== ★ PIPELINE BOARD (drag & drop) ============================== */
/* Kanban of candidate cards across pipeline stages. Drag a card to a column to move it;
   drop it in the Rejected band (or use the card menu) and a reason is captured. */
function RejectionSummary({ value }) {
  if (!value) return null;
  const decision = window.E8RejectPolicy.normalize(value);
  return (
    <span className={'e8-rejection-summary' + (decision.restricted ? ' restricted' : '')}>
      <span className="material-symbols-outlined" aria-hidden="true">{decision.restricted ? 'policy' : 'block'}</span>
      {decision.restricted ? 'Restricted review' : decision.reason}
    </span>
  );
}

function RejectReasonDialog({ name, onConfirm, onClose }) {
  const P = window.E8RejectPolicy;
  const [category, setCategory] = React.useState(P.categories[0].id);
  const group = P.categories.find((item) => item.id === category);
  const [reason, setReason] = React.useState(group.reasons[0]);
  const [note, setNote] = React.useState('');
  const noteRequired = P.requiresNote(category);
  const chooseCategory = (item) => {
    setCategory(item.id);
    setReason(item.reasons[0]);
  };
  return (
    <DSj.Modal title={'Reject ' + name} ariaLabel="Reason for rejecting" closeTitle="Cancel" onClose={onClose}>
        <div style={{ padding: '14px 16px', display: 'grid', gap: 12 }}>
          <div>
            <div className="e8-sectionlabel" style={{ marginBottom: 7 }}>Category</div>
            <div className="e8-reject-categories" role="group" aria-label="Rejection category">
              {P.categories.map((item) => (
                <button key={item.id} type="button" className={'e8-reject-category' + (category === item.id ? ' on' : '')} aria-pressed={category === item.id} onClick={() => chooseCategory(item)}>{item.label}</button>
              ))}
            </div>
          </div>
          <div>
            <div className="e8-sectionlabel" style={{ marginBottom: 7 }}>Reason</div>
            <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
              {group.reasons.map((r) => (
                <button key={r} type="button" className={'e8-reason-chip' + (reason === r ? ' on' : '')} onClick={() => setReason(r)}>{r}</button>
              ))}
            </div>
          </div>
          <div>
            <div className="e8-sectionlabel" style={{ marginBottom: 7 }}>Factual note <span style={{ fontWeight: 400, color: 'var(--ui-text-tertiary)' }}>· {noteRequired ? 'required, attached to restricted review' : 'optional, shared with the team'}</span></div>
            <textarea className="e8-reject-note" rows={2} placeholder={noteRequired ? 'Record only the facts that need review…' : 'Add context…'} value={note} onChange={(e) => setNote(e.target.value)} aria-required={noteRequired} />
          </div>
          {noteRequired ? <div className="e8-restricted-notice"><span className="material-symbols-outlined" aria-hidden="true">policy</span><span>This creates a restricted review flag. It does not mark the person as fake or train fit scoring.</span></div> : null}
          <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
            <span style={{ fontSize: 'var(--ui-text-xs)', color: 'var(--ui-text-tertiary)', flex: 1 }}>Logged to the candidate timeline and win-rate reporting.</span>
            <DSj.Button variant="ghost" size="sm" onClick={onClose}>Cancel</DSj.Button>
            <DSj.Button variant="reject" size="sm" icon="close" disabled={noteRequired && !note.trim()} onClick={() => onConfirm(P.record(category, reason, note))}>Reject candidate</DSj.Button>
          </div>
        </div>
    </DSj.Modal>
  );
}

function PipelineCard({ c, owner, reason, onDragStart, onDragEnd, onReopen, dragging }) {
  const decision = reason ? window.E8RejectPolicy.normalize(reason) : null;
  return (
    <div
      className={'e8-pcard' + (dragging ? ' dragging' : '') + (decision ? ' rejected' : '') + (decision && decision.restricted ? ' restricted' : '')}
      draggable={!decision}
      onDragStart={onDragStart}
      onDragEnd={onDragEnd}
      {...window.clickableProps(() => navigate('candidate/' + c.id))}
    >
      <div style={{ display: 'flex', alignItems: 'center', gap: 7 }}>
        <DSj.Avatar name={c.name} size="sm" />
        <span style={{ fontSize: 'var(--ui-text-sm)', fontWeight: 500, minWidth: 0, flex: 1, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{c.name}</span>
        {c.linkedin ? <span className="e8-srcdot li" title="LinkedIn"><b>in</b></span> : null}
      </div>
      <div className="e8-pcard-line"><span className="material-symbols-outlined">work</span>{c.title}</div>
      <div className="e8-pcard-line"><span className="material-symbols-outlined">location_on</span>{c.location}</div>
      <div className="e8-pcard-line"><span className="material-symbols-outlined">apartment</span>{c.company}</div>
      {decision ? (
        <div className="e8-pcard-reason" onClick={(e) => e.stopPropagation()}>
          <span className="material-symbols-outlined" style={{ fontSize: 13 }}>{decision.restricted ? 'policy' : 'block'}</span>
          <span style={{ flex: 1, minWidth: 0 }}>{decision.restricted ? 'Restricted review · ' : ''}{decision.reason}{!decision.restricted && decision.note ? ' - ' + decision.note : ''}</span>
          <a className="e8-link" tabIndex={0} onKeyDown={window.keyActivate} onClick={() => onReopen()}>Reopen</a>
        </div>
      ) : (
        <div style={{ display: 'flex', alignItems: 'center', gap: 6, marginTop: 1 }}>
          <DSj.MatchBadge tier={c.tier} dot />
          <span style={{ marginLeft: 'auto', display: 'inline-flex', alignItems: 'center', gap: 4 }} title={'Owner · ' + owner}>
            <DSj.Avatar name={owner} size="sm" />
          </span>
        </div>
      )}
    </div>
  );
}

function PipelineBoard() {
  const D = window.E8DATA;
  const MATCHES = matchPoolFor('JO-44219');
  const { showToast } = React.useContext(E8Ctx);
  const cols = D.pipelineStages.filter((s) => s.kind !== 'lost'); // active columns; the lost stage = Rejected band
  const lostKey = (D.pipelineStages.find((s) => s.kind === 'lost') || {}).key || 'rejection';
  const firstKey = (cols[0] || {}).key || 'source';
  const byId = Object.fromEntries(MATCHES.map((m) => [m.id, m]));
  const ownerOf = (id) => (D.candidates.find((x) => x.id === id) || {}).owner || 'Sarah Kim';
  const labelOf = (k) => (D.pipelineStages.find((s) => s.key === k) || {}).label || k;

  const [stageOf, setStageOf] = React.useState(Object.fromEntries(MATCHES.map((m) => [m.id, m.stage || firstKey])));
  const [reasons, setReasons] = React.useState(Object.fromEntries(MATCHES.filter((m) => m.rejectReason).map((m) => [m.id, window.E8RejectPolicy.normalize(m.rejectReason)])));
  const [dragId, setDragId] = React.useState(null);
  const [overCol, setOverCol] = React.useState(null);
  const [rejecting, setRejecting] = React.useState(null);
  const [rejectOpen, setRejectOpen] = React.useState(false);

  const inStage = (k) => MATCHES.filter((m) => stageOf[m.id] === k && !(reasons[m.id] && k !== lostKey));
  const rejected = MATCHES.filter((m) => stageOf[m.id] === lostKey);
  const inProcess = MATCHES.length - rejected.length;

  const drop = (colKey, e) => {
    let id = dragId;
    try { id = (e && e.dataTransfer && e.dataTransfer.getData('text/plain')) || dragId; } catch (x) {}
    setOverCol(null);
    if (!id) return;
    if (stageOf[id] === colKey) return;
    if (colKey === lostKey) { setRejecting(id); setRejectOpen(true); return; }
    const prev = stageOf[id];
    /* R78: stage moves write through E8Store (matches.stage), so the funnel survives reload. */
    window.E8Store.set('matches', id, { stage: colKey });
    if (window.E8Events) window.E8Events.emit('match:staged', { id, from: prev, to: colKey });
    setStageOf((s) => ({ ...s, [id]: colKey }));
    showToast(byId[id].name + ' → ' + labelOf(colKey), 'Undo', () => {
      window.E8Store.set('matches', id, { stage: prev });
      setStageOf((s) => ({ ...s, [id]: prev }));
    });
  };

  const confirmReject = (payload) => {
    const id = rejecting;
    const prev = stageOf[id];
    const decision = window.E8RejectPolicy.normalize(payload);
    window.E8Store.set('matches', id, { stage: lostKey, rejectReason: decision });
    if (window.E8Audit) window.E8Audit.log({
      agent: 'You',
      prov: 'human',
      action: 'Rejected candidate · ' + decision.category + ' · ' + decision.reason,
      target: byId[id].name,
      route: 'candidate/' + id,
      category: decision.category,
      reason: decision.reason,
      restricted: decision.restricted,
    });
    if (window.E8Events) window.E8Events.emit(
      'match:rejected',
      Object.assign({ id }, window.E8RejectPolicy.metadata(decision)),
    );
    setStageOf((s) => ({ ...s, [id]: lostKey }));
    setReasons((r) => ({ ...r, [id]: decision }));
    setRejectOpen(false); setRejecting(null); setDragId(null);
    showToast(byId[id].name + ' rejected · ' + decision.reason, 'Undo', () => {
      const back = prev === lostKey ? firstKey : prev;
      window.E8Store.set('matches', id, { stage: back, rejectReason: null });
      setStageOf((s) => ({ ...s, [id]: back }));
      setReasons((r) => { const n = { ...r }; delete n[id]; return n; });
    });
  };

  const reopen = (id) => {
    const prevReason = reasons[id];
    window.E8Store.set('matches', id, { stage: firstKey, rejectReason: null });
    setStageOf((s) => ({ ...s, [id]: firstKey }));
    setReasons((r) => { const n = { ...r }; delete n[id]; return n; });
    showToast(byId[id].name + ' reopened to ' + labelOf(firstKey), 'Undo', () => {
      const restoredReason = prevReason || window.E8RejectPolicy.normalize('Rejected');
      window.E8Store.set('matches', id, { stage: lostKey, rejectReason: restoredReason });
      setStageOf((s) => ({ ...s, [id]: lostKey }));
      setReasons((r) => ({ ...r, [id]: restoredReason }));
    });
  };

  return (
    <div>
      <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 10 }}>
        <span className="e8-sectionlabel">In process</span>
        <span className="tnum" style={{ fontSize: 'var(--ui-text-sm)', color: 'var(--ui-text-tertiary)' }}>{inProcess}</span>
        <span style={{ marginLeft: 'auto', fontSize: 'var(--ui-text-xs)', color: 'var(--ui-text-tertiary)' }}>Drag a card to move it · drop in Rejected for a reason</span>
      </div>

      <div className="e8-pboard">
        {cols.map((col) => {
          const cards = inStage(col.key);
          return (
            <div
              key={col.key}
              className={'e8-pcol' + (overCol === col.key ? ' over' : '')}
              onDragOver={(e) => { e.preventDefault(); if (overCol !== col.key) setOverCol(col.key); }}
              onDragLeave={(e) => { if (e.currentTarget === e.target) setOverCol(null); }}
              onDrop={(e) => drop(col.key, e)}
            >
              <div className="e8-pcol-head">
                <span className={'e8-pcol-dot ' + (col.kind === 'won' ? 'won' : '')}></span>
                {col.label}
                <span className="e8-rail-count" style={{ marginLeft: 'auto' }}>{cards.length}</span>
              </div>
              <div className="e8-pcol-body">
                {cards.map((c) => (
                  <PipelineCard
                    key={c.id} c={c} owner={ownerOf(c.id)}
                    dragging={dragId === c.id}
                    onDragStart={(e) => { setDragId(c.id); e.dataTransfer.effectAllowed = 'move'; try { e.dataTransfer.setData('text/plain', c.id); } catch (x) {} }}
                    onDragEnd={() => { setDragId(null); setOverCol(null); }}
                  />
                ))}
                {!cards.length ? <div className="e8-pcol-empty">Drop here</div> : null}
              </div>
            </div>
          );
        })}
      </div>

      {/* Rejected band - drop target + list with reasons */}
      <div
        className={'e8-rejband' + (overCol === lostKey ? ' over' : '')}
        onDragOver={(e) => { e.preventDefault(); if (overCol !== lostKey) setOverCol(lostKey); }}
        onDragLeave={(e) => { if (e.currentTarget === e.target) setOverCol(null); }}
        onDrop={(e) => drop(lostKey, e)}
      >
        <div className="e8-pcol-head" style={{ padding: '0 2px 8px' }}>
          <span className="e8-pcol-dot lost"></span>
          Rejected
          <span className="e8-rail-count">{rejected.length}</span>
          <span style={{ marginLeft: 'auto', fontSize: 'var(--ui-text-xs)', color: 'var(--ui-text-tertiary)' }}>Drop a card here to reject with a reason</span>
        </div>
        {rejected.length ? (
          <div className="e8-rejband-grid">
            {rejected.map((c) => (
              <PipelineCard key={c.id} c={c} owner={ownerOf(c.id)} reason={reasons[c.id] || { reason: 'Rejected' }} onReopen={() => reopen(c.id)} />
            ))}
          </div>
        ) : (
          <div className="e8-pcol-empty" style={{ margin: 0 }}>No rejected candidates</div>
        )}
      </div>

      {rejectOpen && rejecting ? (
        <RejectReasonDialog name={byId[rejecting].name} onConfirm={confirmReject} onClose={() => { setRejectOpen(false); setRejecting(null); setDragId(null); }} />
      ) : null}
    </div>
  );
}

Object.assign(window, { JobsScreen, JobDetailScreen, MatchingTab, IntakeTab, JobsBoard, OpenJobsTab, JobCard, JobCardList, JobsFilterSheet, PriorityGlyph, PipelineCell, PipelineMeter, StageBreakdown, StageHeader, OwnerStack, pipelineTotal, jobStageOf, JOB_GROUPS, PipelineBoard, PipelineCard, RejectReasonDialog, matchPoolFor, matchStateOf, matchReviewCount, e8AcceptDecision, e8RejectDecision });
