/* ============================================================================
   cand-overview.jsx — the candidate record's OVERVIEW TAB BODY: the About /
   AI-summary block, the prescreen completeness strip, the confirmed-from-call
   chips, the latest-activity feed, and the "why we matched" decision column.

   Split out of screens-core.jsx's CandidateDetailScreen. Loaded AFTER
   screens-core.jsx, so the helpers this file calls unqualified - RunMatchingCard,
   RiskFirstReasons, OvDisclose, SourceLabel, DSc, e8Lockdown, e8PrescreenState,
   e8AgoLabel, navigate - are already declared in the shared global scope.

   WHY THREE BLOCKS ARE NOW LOCAL COMPONENTS RATHER THAN SHARED HELPERS.
   Pieces 6/7/8 of docs/CANDIDATE-CARD-REFERENCE.md are About, the prescreen
   strip and Activity. Two of them used to be rendered by components that live in
   screens-core.jsx and are rendered by OTHER surfaces too:
     - LockdownStrip is also drawn by PrescreenPanel and the submission packet;
     - DSc.Timeline (components/ats-timeline) is drawn by every record.
   Restyling either in place would have changed screens nobody asked about, and
   this pass owns exactly two files. So the record's OWN presentation of those two
   ideas is authored here (CandOvPrescreen, CandOvActivity) and the shared
   components keep serving their other callers unchanged. The DATA still comes
   from the shared engines - e8Lockdown() and timelineFor() - so there is one
   source of truth and only the rendering is local.

   No inline `style` here: a new .jsx carries an inline-style budget of 0
   (scripts/check-inline-style.mjs). The one inline style in the file is the
   progress meter's `--e8-cov-pct` custom property, which is a MEASURED VALUE
   carried into CSS - data, not styling, and explicitly exempt.
   ============================================================================ */

/* ---------------------------------------------------------------------------
   Shared: "is this element actually clipped?"

   Both the About paragraph and every activity body collapse to a line clamp, and
   a disclosure control that appears when there is nothing more to show is worse
   than no control at all. So the clamp is MEASURED, never assumed.

   The trap this shape avoids: once expanded, scrollHeight === clientHeight, so a
   naive measure flips the answer back to "fits" and the control that got you
   there vanishes under your cursor. Measuring only while COLLAPSED means the
   expanded state simply retains the last true answer.
   --------------------------------------------------------------------------- */
function useCandOvClamped(ref, expanded, dep) {
  const [clipped, setClipped] = React.useState(false);
  React.useEffect(() => {
    const el = ref.current;
    if (!el) return undefined;
    const measure = () => { if (!expanded) setClipped(el.scrollHeight - el.clientHeight > 1); };
    measure();
    if (typeof ResizeObserver === 'undefined') return undefined;
    /* The rail resizes the content column without touching the viewport, so a
       resize listener on window would miss the case that matters most here. */
    const ro = new ResizeObserver(measure);
    ro.observe(el);
    return () => ro.disconnect();
  }, [expanded, dep]);
  return clipped;
}

/* ============================== 6. ABOUT ==============================
   One narrative block. Provenance stays VISIBLE - "AI-drafted · compiled from
   LinkedIn + calls" - because a summary the machine wrote must say so; what
   changed is the measure (capped in `ch`, so it holds at every column width) and
   that anything past three lines is behind a real disclosure with aria-expanded
   rather than eating the fold.

   R143: About is now a FULL-WIDTH ROW above the two columns rather than the first
   block of the left one. Measured at 1440: the left column is 403px, so the
   paragraph ran at 45ch - 40% under the 75ch cap in §3 #6, and short lines cost
   MORE vertical space, not less. Across the row the same text sits at 66ch (the
   cap below), which is inside the 55-75 band and one line shorter. */
function CandOvAbout({ c, prof }) {
  const ai = !!c.aiSummary;
  const text = ai ? c.aiSummary : prof.about;
  const [open, setOpen] = React.useState(false);
  const ref = React.useRef(null);
  const clipped = useCandOvClamped(ref, open, text);
  return (
    <section className="e8-cand-overview-ab" aria-labelledby="e8-cov-ab-label">
      {ai ? (
        <div className="e8-cand-overview-abhead">
          <span className="e8-sectionlabel" id="e8-cov-ab-label">About</span>
          <DSc.ProvenanceBadge kind="drafted" />
          <window.Text size="xs" tone="tertiary" className="e8-cand-overview-absrc">compiled from LinkedIn + calls · updated 2h ago</window.Text>
        </div>
      ) : (
        <div className="e8-cand-overview-abhead" id="e8-cov-ab-label">
          <SourceLabel label="About" source={prof.aboutSource} />
        </div>
      )}
      <p ref={ref} id="e8-cov-ab-text"
        className={'e8-cand-overview-abtext' + (open ? '' : ' is-clamped')}>{text}</p>
      {clipped || open ? (
        <button type="button" className="e8-cand-overview-abmore"
          aria-expanded={open} aria-controls="e8-cov-ab-text" onClick={() => setOpen(!open)}>
          {open ? 'Show less' : 'Show more'}
          {/* WAVE-3 SEAM FIX — THE GLYPH SWAPS, IT DOES NOT ROTATE. Every other disclosure a
              recruiter meets on this record swaps the ligature: the header's score chip
              (cand-header.jsx:243), the stage chip (cand-record.jsx:1163) and the shared
              `OvDisclose` behind "Also in play" (screens-core.jsx:2992, `chevron_right` ->
              `expand_more`), which is co-visible with this block on the Overview tab. This
              module was the only one turning the same arrow over, and `grep -c "rotate(180deg)"
              app/app.css` is 0 - the rotation was a house style of one file, not of the app.
              Swapping costs no transition, needs no prefers-reduced-motion guard, and cannot
              animate from the wrong angle on first paint. */}
          <span className="material-symbols-outlined e8-cand-overview-abchev" aria-hidden="true">{open ? 'expand_less' : 'expand_more'}</span>
        </button>
      ) : null}
    </section>
  );
}

/* ===================== 7. PRESCREEN COMPLETENESS =====================
   The strip this replaces led with "LOCKDOWN 7/10" - a word from the internal
   data model, an unlabelled fraction, and a list of what is missing that was
   `white-space: nowrap; text-overflow: ellipsis`, so at 390 it clipped to
   "Other opportunities / offer…" mid-word (measured: 326px of text in a 273px
   box). Three fixes, in order of importance:

   1. It names the thing the recruiter is progressing TOWARDS - the answers you
      need before you can submit - not the internal name of the field group.
   2. The missing answers are CHIPS that wrap. A chip can be long; it can never be
      cut in half. Past three, the rest are behind "+N more", which is a choice
      rather than an accident of the box width.
   3. One action. `Complete prescreen` stays SECONDARY on purpose: R138 P2
      established that prescreen is a step toward the record's primary action
      (submit, in the header), not a rival to it. The portal request stays as a
      subordinate text link - it is the same job by another route, so promoting it
      to a second button would be two actions competing for one decision.

   Progress counts FRESH answers: total - missing - stale. The old meter read
   `done/total`, which reaches 100% while volatile answers have expired - a full
   bar over the words "needs reconfirming". */
function CandOvNeedRow({ label, fields, stale }) {
  const [all, setAll] = React.useState(false);
  if (!fields.length) return null;
  const shown = all ? fields : fields.slice(0, 3);
  const rest = fields.length - shown.length;
  return (
    <div className="e8-cand-overview-psneed">
      {/* `.e8-eyebrow` composed, not a fourth bespoke uppercase rule - the class this used to
          carry declared exactly what the eyebrow declares plus a 650 weight nobody chose. */}
      <span className="e8-eyebrow">{label}</span>
      <ul className="e8-cand-overview-psneed-list">
        {shown.map((f) => (
          <li key={f.key} className={'e8-cand-overview-psneed-chip' + (stale ? ' is-stale' : '')}>{f.label}</li>
        ))}
        {rest > 0 ? (
          <li>
            <button type="button" className="e8-cand-overview-psneed-more" onClick={() => setAll(true)}>+{rest} more</button>
          </li>
        ) : null}
      </ul>
    </div>
  );
}

function CandOvPrescreen({ c, cmeta, jobId, facts, active, onRequest, onPrescreen }) {
  /* A live submission for this job is the strongest source - its block was
     stamped at submit time. Fall back to prescreen drafts only when nothing is
     in flight. (Same resolution order as the shared strip; same engine call.) */
  const D = window.E8DATA;
  const [expanded, setExpanded] = React.useState(false);
  const sub = jobId
    ? (D.submissions || []).find((s) => s.candId === c.id && s.jobId === jobId && s.open !== false)
    : (D.submissions || []).find((s) => s.candId === c.id && s.open !== false && s.lockdown);
  const lock = e8Lockdown(c, cmeta, sub, jobId);
  const suggested = (facts || []).filter((f) => f.state !== 'confirmed');
  const psState = e8PrescreenState(cmeta);
  const first = String(c.name || '').trim().split(/\s+/)[0] || 'the candidate';
  const val = (k) => { const f = lock.fields.find((x) => x.key === k); return f && f.value ? String(f.value) : null; };

  /* Not today's work: the gap is real but the record is parked. One quiet chip
     that says what it is and opens the full strip. */
  if (lock.state !== 'ready' && !active && !expanded) {
    return (
      <button type="button" className="e8-cand-overview-psmini" aria-expanded={false}
        title="This record is not in an active working stage. Open the prescreen detail."
        onClick={() => setExpanded(true)}>
        <i aria-hidden="true"></i>
        <span className="e8-cand-overview-psmini-t">Prescreen <b className="tnum">{lock.done} of {lock.total}</b>{lock.stale.length ? ' · needs reconfirming' : ''}</span>
        <span className="e8-cand-overview-psmini-sub">not an active stage — open</span>
      </button>
    );
  }

  if (lock.state === 'ready' && !suggested.length) {
    const bits = [
      val('rate'),
      cmeta.workAuthNoSponsorship === 'Yes' ? 'no sponsorship needed' : cmeta.workAuthNoSponsorship === 'No' ? 'visa details on file' : null,
      val('startAvailability') ? 'start ' + val('startAvailability').toLowerCase() : null,
    ].filter(Boolean);
    return (
      <div className="e8-cand-overview-ps is-ready">
        <span className="material-symbols-outlined e8-cand-overview-ps-ic" aria-hidden="true">task_alt</span>
        <p className="e8-cand-overview-ps-readytxt">
          <b>Prescreen complete</b> — all {lock.total} answers captured{bits.length ? '. ' + bits.join(' · ') : '.'}
          {psState.ps ? <span className="e8-cand-overview-ps-prov"> Prescreened {e8AgoLabel(psState.ps.at)}{psState.ps.by ? ' by ' + psState.ps.by : ''}.</span> : null}
        </p>
      </div>
    );
  }

  const fresh = Math.max(0, lock.total - lock.missing.length - lock.stale.length);
  const pct = lock.total ? Math.round((fresh / lock.total) * 100) : 0;
  const cta = psState.state === 'none' ? 'Run prescreen' : psState.state === 'stale' || lock.stale.length ? 'Re-run prescreen' : 'Complete prescreen';
  /* THE THIRD CASE IS REAL AND WAS BRIEFLY WRONG. `state === 'ready'` still lands
     here when there are unconfirmed call facts to show, and keying the sentence
     off `missing.length` alone then printed "the volatile answers have expired"
     over a record with nothing missing and nothing stale. Read the state, not the
     absence of one of its causes. */
  const why = lock.missing.length
    ? 'Needed before ' + first + ' can be submitted.'
    : lock.stale.length
      ? 'The volatile answers have expired — reconfirm before submitting.'
      : 'Every answer is captured. Confirm what the call picked up below.';
  return (
    <section className={'e8-cand-overview-ps' + (lock.stale.length ? ' is-reconfirm' : '')} aria-labelledby="e8-cov-ps-label">
      <div className="e8-cand-overview-pshead">
        <h3 className="e8-cand-overview-ps-title" id="e8-cov-ps-label">Prescreen</h3>
        {/* ONE notation for one fact. "7 of 10 answers captured" and "70%" said the
            same thing twice in the same line; the bar below already carries the
            proportion, and its role="progressbar" carries the percentage to a
            screen reader through aria-valuenow. */}
        <span className="e8-cand-overview-ps-count"><b className="tnum">{fresh} of {lock.total}</b> answers captured</span>
      </div>
      <div className="e8-cand-overview-ps-track" role="progressbar"
        aria-valuenow={pct} aria-valuemin={0} aria-valuemax={100}
        aria-label={'Prescreen: ' + fresh + ' of ' + lock.total + ' answers captured'}>
        {/* the fill's width IS the measurement - a data value carried into CSS */}
        <i style={{ '--e8-cov-pct': pct + '%' }}></i>
      </div>
      <p className="e8-cand-overview-ps-why">{why}</p>
      <CandOvNeedRow label="Still needed" fields={lock.missing} />
      <CandOvNeedRow label="Needs reconfirming" fields={lock.stale} stale />
      {/* ONE action, and it is now sized like one. `size="sm"` rendered a 24px
          button - 55% of the 44px touch minimum - for the single CTA the block
          exists to deliver; `lg` is 34px and the narrow-container rule below takes
          it to 44. It stays SECONDARY rather than primary on purpose: exactly one
          cobalt primary per view, and the record's is Submit in the header.
          The email route is the same job by another road, so it is a subordinate
          text link, not a second button competing for the same decision. */}
      <div className="e8-cand-overview-psact">
        <DSc.Button variant="secondary" size="lg" icon="support_agent" onClick={onPrescreen}>{cta}</DSc.Button>
        <button type="button" className="e8-cand-overview-psalt" onClick={onRequest}>or ask {first} to confirm by email</button>
      </div>
      {suggested.length ? (
        <div className="e8-cand-overview-pssugg">
          <span className="e8-cand-overview-pssugg-k">Heard on the call — confirm:</span>
          {suggested.map((f, i) => <DSc.ConfidenceChip key={i} state={f.state}>{f.label}</DSc.ConfidenceChip>)}
        </div>
      ) : null}
    </section>
  );
}

/* ========================= 8. ACTIVITY FEED =========================
   The strongest block on the screen already; this deepens it rather than
   replacing it. Three additions:
     - DAY GROUPING with a sticky day header. The scroll container is
       `.e8-content.e8-rbody-content` (measured: the only `overflow-y: auto`
       ancestor of a group, and every element between is `overflow: visible`),
       so `top: 0` sticks against the right box.
     - ACTOR DISTINCTION at a glance: a person gets a round initials avatar and a
       plain name; an agent gets the square agent mark, a bot glyph and an "Agent"
       tag. Two independent signals, neither of them colour alone.
     - EXPANDABLE entries: the title never truncates, the body clamps to two lines
       and opens on demand, and the deep link appears with the detail.

   The unreviewed-AI flag is reproduced here rather than borrowed: it lives inside
   components/ats-timeline and is not exported. Same storage key, same audit
   entries, so verifying from either surface clears it on both. */
const E8_COV_PROV_KEY = 'e8-prov-verified-v1';
function e8CovProvVerified() {
  try { return new Set(JSON.parse(localStorage.getItem(E8_COV_PROV_KEY)) || []); } catch (e) { return new Set(); }
}
function e8CovDayStart(ts) { const d = new Date(ts); return new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime(); }
function e8CovDayLabel(ts) {
  const diff = Math.round((e8CovDayStart(Date.now()) - e8CovDayStart(ts)) / 864e5);
  if (diff < 0) return 'Upcoming';
  if (diff === 0) return 'Today';
  if (diff === 1) return 'Yesterday';
  const d = new Date(ts);
  if (diff < 7) return d.toLocaleDateString(undefined, { weekday: 'long' });
  const sameYear = d.getFullYear() === new Date().getFullYear();
  return d.toLocaleDateString(undefined, sameYear ? { month: 'short', day: 'numeric' } : { month: 'short', day: 'numeric', year: 'numeric' });
}

/* THE DAY HEADER READS THE SAME SOURCE THE ENTRY'S STAMP DOES, and this is the
   one non-obvious thing in this file.

   Most events on this record carry NO epoch: the seeded timeline is five rows of
   authored text ("Mon · 11:02 am") and e8MergeEvents fabricates a `ts` for each by
   pattern-matching that string (screens-core.jsx:2587). "Mon · 11:02 am" matches
   none of its patterns and falls through to `now - 4 days`, which today is a
   SUNDAY. Grouping on the epoch therefore printed a "SUNDAY" header over two rows
   that say "Mon" - the header contradicting the entries beneath it, which is
   worse than no header at all. It was on screen before this was caught.

   So: when the author named a day, that name is the group. Only when they did not
   (a note stamped "2d ago", a submission with a real `at`) does the epoch decide -
   and in those cases the epoch is either real or genuinely relative to now. */
const E8_COV_WEEKDAYS = {
  sun: 'Sunday', mon: 'Monday', tue: 'Tuesday', wed: 'Wednesday',
  thu: 'Thursday', fri: 'Friday', sat: 'Saturday',
};
function e8CovDayWord(when) {
  const head = String(when || '').split(/\s*(?:·|—|-)\s*/)[0].trim().toLowerCase();
  if (!/^[a-z]+$/.test(head)) return null;
  if (head === 'today') return 'Today';
  if (head === 'yesterday') return 'Yesterday';
  return E8_COV_WEEKDAYS[head.slice(0, 3)] || null;
}
function e8CovDayOf(ev) {
  return e8CovDayWord(ev.when) || e8CovDayLabel(ev.ts != null ? ev.ts : Date.now());
}
/* The header now carries the day, so the entry's stamp keeps only what the header
   does not already say: "Mon · 11:02 am" under MONDAY becomes "11:02 am". Edited
   rather than re-derived from the epoch, for the reason above - printing a time
   off a fabricated epoch would be invented precision. */
function e8CovWhenLabel(when, dayLabel) {
  let t = String(when || '').trim();
  const m = t.match(/^([A-Za-z]+)\s*(?:·|—|-)\s*(.+)$/);
  if (m && e8CovDayWord(m[1])) t = m[2].trim();
  if (!t) return '';
  if (t.toLowerCase() === String(dayLabel || '').toLowerCase()) return '';
  return t;
}

function CandOvAiFlag({ ev }) {
  const [open, setOpen] = React.useState(false);
  const [verified, setVerified] = React.useState(() => e8CovProvVerified().has(ev.id));
  const ref = React.useRef(null);
  React.useEffect(() => {
    if (!open) return undefined;
    const onDoc = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
    const onEsc = (e) => { if (e.key === 'Escape') setOpen(false); };
    document.addEventListener('mousedown', onDoc);
    document.addEventListener('keydown', onEsc);
    return () => { document.removeEventListener('mousedown', onDoc); document.removeEventListener('keydown', onEsc); };
  }, [open]);
  if (verified) return null;
  const act = (what) => {
    setOpen(false);
    if (what === 'verify') {
      const s = e8CovProvVerified(); s.add(ev.id);
      try { localStorage.setItem(E8_COV_PROV_KEY, JSON.stringify(Array.from(s))); } catch (e) {}
      setVerified(true);
    }
    if (window.E8Audit) {
      window.E8Audit.log({
        agent: 'You', prov: 'human', target: ev.title,
        action: what === 'verify' ? 'Verified AI activity' : what === 'correct' ? 'Flagged AI activity for correction' : 'Escalated AI activity to lead',
      });
    }
  };
  return (
    <span className="e8-cand-overview-actflagwrap" ref={ref}>
      <button type="button" className="e8-cand-overview-actflag" aria-haspopup="menu" aria-expanded={open}
        title="Unreviewed AI activity - verify, correct or escalate" onClick={() => setOpen(!open)}>Unreviewed</button>
      {open ? (
        <span className="e8-cand-overview-actflagmenu" role="menu">
          <button type="button" role="menuitem" onClick={() => act('verify')}>Verify — looks right</button>
          <button type="button" role="menuitem" onClick={() => act('correct')}>Correct the record…</button>
          <button type="button" role="menuitem" onClick={() => act('escalate')}>Escalate to lead</button>
        </span>
      ) : null}
    </span>
  );
}

/* THE FEED'S ACTOR MARK IS NOT `DSc.Avatar`, AND THAT IS THE FIX FOR THE 9.5px.
   `components/ats-avatar/Avatar.jsx` writes its measurements as INLINE styles -
   `fontSize: Math.round(px * 0.4 * 2) / 2` - so `size="sm"` (24px) renders its
   initials at 9.5px: the smallest type on the record and off the --ui-text-*
   scale entirely (the scale is 11.5 / 12.5 / 13.5 / 15 / 18 / 24 / 28). No prop
   value lands on the scale either - the five sizes compute 8 / 9.5 / 10.5 / 14.5
   / 22.5 - and no class rule can reach an inline style without `!important`.

   So this file does what `cand-header.jsx` already does for the identity mark
   (cand-header.css:63): render the DS avatar's CLASSES on a plain span and let
   `.e8-cand-overview-actav` supply the measurements. `.e8-avatar` still gives the
   shape, the Daybreak tint and the ink, and `.e8-avatar-agent` still draws the
   square agent mark through its own `::after`, so this is the DS avatar in
   everything but its two numbers. The box stays 24px - only the glyph moves,
   9.5 → 11.5.

   The component itself is the RIGHT place for this (its whole size table is off
   the scale, on every surface that draws an avatar); that file is not in this
   pass's two-file scope, and its fix needs an `app/ds-live.js` bump. Reported. */
function e8CovInitials(name) {
  const parts = String(name || '').trim().split(/\s+/);
  return (((parts[0] || '')[0] || '') + ((parts.length > 1 ? parts[parts.length - 1] : '')[0] || '')).toUpperCase();
}

function CandOvActItem({ ev, dayLabel, last }) {
  const [open, setOpen] = React.useState(false);
  const ref = React.useRef(null);
  const clipped = useCandOvClamped(ref, open, ev.body);
  const when = e8CovWhenLabel(ev.when, dayLabel);
  const canOpen = clipped || open || !!ev.link;
  return (
    <li className={'e8-cand-overview-actitem' + (ev.ai ? ' is-agent' : ' is-human') + (last ? ' is-last' : '')}>
      <span className="e8-cand-overview-actrail" aria-hidden="true">
        {ev.ai
          ? <span className="e8-avatar e8-avatar-agent e8-cand-overview-actav" title={ev.actor}></span>
          : <span className="e8-avatar e8-cand-overview-actav" title={ev.actor}>{e8CovInitials(ev.actor)}</span>}
        <span className="e8-cand-overview-actline"></span>
      </span>
      <div className="e8-cand-overview-actbody">
        <div className="e8-cand-overview-actmeta">
          <span className="e8-cand-overview-actwho">
            {ev.ai ? <span className="material-symbols-outlined e8-cand-overview-actwho-ic" aria-hidden="true">smart_toy</span> : null}
            {ev.actor}
          </span>
          <span className="e8-cand-overview-actkind">{ev.ai ? 'Agent' : 'Person'}</span>
          {ev.ai && ev.prov === 'advised' ? <CandOvAiFlag ev={ev} /> : null}
          {when ? <span className="e8-cand-overview-actwhen">{when}</span> : null}
        </div>
        {canOpen ? (
          <button type="button" className="e8-cand-overview-actsum" aria-expanded={open} onClick={() => setOpen(!open)}>
            <span className="e8-cand-overview-acttitle">{ev.title}</span>
            {/* Swaps rather than rotates - same reason as the About disclosure above. */}
            <span className="material-symbols-outlined e8-cand-overview-actchev" aria-hidden="true">{open ? 'expand_less' : 'expand_more'}</span>
          </button>
        ) : (
          <div className="e8-cand-overview-actsum is-static"><span className="e8-cand-overview-acttitle">{ev.title}</span></div>
        )}
        {ev.body ? <p ref={ref} className={'e8-cand-overview-acttext' + (open ? '' : ' is-clamped')}>{ev.body}</p> : null}
        {open && ev.link ? (
          <a className="e8-link e8-cand-overview-actgo" tabIndex={0} onKeyDown={window.keyActivate}
            onClick={() => navigate(ev.linkTo || 'voice')}>{ev.link}&nbsp;→</a>
        ) : null}
      </div>
    </li>
  );
}

/* Latest activity - the recent slice and a door. Activity's one home is the rail
   (R124), so the link opens the rail rather than rendering a third full timeline
   in the page; the inline feed is CAPPED rather than paginated.

   R143, THE CAP IS NOW 3 AND THAT IS THE POINT. At 1440 this block and the rail's
   Activity pane were co-visible in one viewport rendering the SAME five entries -
   the single largest piece of duplicated ink on the screen. The two surfaces are
   not rivals: the rail is the deep feed, this is a DIGEST that says what just
   happened and hands over. Three entries is enough to answer "what just happened"
   and cannot fill the fold; the count line names the remainder ("most recent 3 of
   5") so the door is a stated choice rather than a silent cut. */
function CandOvLatestActivity({ events, onOpenActivity }) {
  const all = events || [];
  const shown = all.slice(0, 3);
  /* Contiguous runs, keyed by the LABEL - so an event carrying a real epoch for
     today and one carrying the word "Today" land under one header rather than two
     identical ones. */
  const groups = [];
  shown.forEach((ev) => {
    const label = e8CovDayOf(ev);
    const tail = groups[groups.length - 1];
    if (tail && tail.label === label) tail.items.push(ev);
    else groups.push({ label, items: [ev] });
  });
  return (
    <section className="e8-cand-overview-act" aria-labelledby="e8-cov-act-label">
      <div className="e8-cand-overview-acthead">
        <span className="e8-sectionlabel" id="e8-cov-act-label">Latest activity</span>
        <window.Text size="xs" tone="tertiary" className="e8-cand-overview-actcount">
          {all.length > shown.length ? 'most recent ' + shown.length + ' of ' + all.length : all.length + (all.length === 1 ? ' entry' : ' entries')}
        </window.Text>
      </div>
      {groups.map((g, gi) => (
        <div className="e8-cand-overview-actgroup" key={g.label + '-' + gi}>
          {/* A day divider, drawn the way the Messages thread draws its day divider - see the note
              in cand-overview.css. Stays an <h4>; the CSS anchors the type selector to beat
              `.elev8 h4`, which was silently supplying its size, weight, tracking and ink. */}
          <h4 className="e8-cand-overview-actday">{g.label}</h4>
          <ul className="e8-cand-overview-actlist">
            {g.items.map((ev, i) => (
              <CandOvActItem key={ev.id || (g.label + '-' + i)} ev={ev} dayLabel={g.label}
                last={gi === groups.length - 1 && i === g.items.length - 1} />
            ))}
          </ul>
        </div>
      ))}
      {!shown.length ? <p className="e8-cand-overview-actempty">Nothing has happened on this record yet. A call, a note or a submission will show up here.</p> : null}
      <a className="e8-link e8-cand-ov-actlink" tabIndex={0} onKeyDown={window.keyActivate} onClick={onOpenActivity}>View full activity →</a>
    </section>
  );
}

/* The Overview tab body: who they are on the left, why we matched on the right. */
function CandOverviewTab({
  c, cmeta, prof, facts, hasMatch, dm, decision, placement, headerStage, tl,
  requestReverify, setPsOpen, setActTick, openRailActivity,
}) {
  const D = window.E8DATA;
  return (
    <div className={'e8-cand-overview' + (hasMatch ? '' : ' no-why')}>
      {/* R111 B1: ONE narrative block - when the AI summary exists it IS the about
          (compression, not addition); the raw LinkedIn bio stays on Experience.
          R143: it spans the row above both columns - see the note on CandOvAbout. */}
      <CandOvAbout c={c} prof={prof} />
      {/* Left - who they are */}
      <div className="e8-cand-ov-main">
        {/* R111 B1: the match slot. Unmatched -> an action card (run the on-device engine),
            never an empty "why we matched" shell. */}
        {!hasMatch ? <RunMatchingCard c={c} onRan={() => setActTick((t) => t + 1)} /> : null}
        {/* R111 W2.2: one strip carries readiness + unconfirmed call facts - the old
            AI-facts section + readiness card repeated the rail's data twice over.
            B1: loud only while the record is actively being worked. */}
        <CandOvPrescreen c={c} cmeta={cmeta} jobId={dm && dm.job ? dm.job.id : null} facts={facts}
          active={!placement && ['Sourced', 'Screening', 'Submitted', 'Engaged'].indexOf(headerStage) > -1}
          onRequest={requestReverify} onPrescreen={() => setPsOpen(true)} />

        {/* R124: activity's one home is the rail - the link opens it (and the sheet on mobile). */}
        <CandOvLatestActivity events={tl} onOpenActivity={openRailActivity} />


        {/* R125.3: the enrichment box left the record - Enrichment lives in its own hub. */}
      </div>

      {/* Right - why we matched (the decision column). R111 B1: risk-first, no
          duplicate score badge (the header chip is canonical), one container. */}
      {hasMatch ? (
        <div className="e8-cand-ov-why">
          <RiskFirstReasons reasons={dm.reasons} job={dm.job} decision={!!decision} />
          {/* R125.3: the candidate's OTHER ranked jobs live directly under the
              decision - "also in play", same panel, one glance. */}
          {(() => {
            /* R79: live E8Match runs override/extend the seeded best-fit list.
               R104 T5: the "Why we matched" panel + rail read this same ranking (via
               e8TopMatch) so the score/tier/reasons all describe the candidate's real
               best-fit job, not a hardcoded flagship.
               R111 B1: collapsed to an "Other matches" disclosure - the decision panel
               owns the recommended job; the rest is one click away. */
            if (!hasMatch) return null;
            const rankedAll = window.E8Match ? window.E8Match.candidateJobs(c.id) : ((D.candidateJobs && D.candidateJobs[c.id]) || []);
            const ranked = (dm.job && rankedAll.length > 1) ? rankedAll.filter((rj) => rj.jobId !== dm.job.id) : rankedAll;
            if (!ranked.length) return null;
            const anyLive = ranked.some((rj) => rj.live);
            return (
              <div className="e8-cand-ov-why-more"><OvDisclose label="Also in play" count={ranked.length} sub={anyLive ? 'live on-device scores' : 'ranked by the matching engine'} defaultOpen>
                <div className="e8-cand-ov-alsolist">
                  {ranked.map((rj) => {
                    const job = D.jobs.find((j) => j.id === rj.jobId) || {};
                    return (
                      <button type="button" key={rj.jobId} className="e8-jobmatch" onClick={() => navigate('job/' + rj.jobId + '/matching')}>
                        <span className="e8-cand-ov-jmmain">
                          <window.Row as="span" gap={7} wrap>
                            <DSc.MatchBadge tier={rj.tier} score={rj.score} dot />
                            <window.Text size="base" weight="semibold">{job.title || rj.jobId}</window.Text>
                            <window.Text size="sm" tone="tertiary">{job.client}{job.rate ? ' · ' + job.rate : ''}</window.Text>
                            {anyLive && !rj.live ? <span className="e8-cand-ov-simchip">simulated</span> : null}
                          </window.Row>
                          <span className="e8-cand-ov-jmwhys">
                            {/* `|| []` because a ranked entry is not all authored seed data: the
                                live half comes back from E8Match's overlay, which is persisted
                                localStorage written by whatever build ran matching last. Every
                                other reader of this same list already guards it the same way
                                (screens-core.jsx:3938, cand-record.jsx:901); this was the one
                                that did not, and here an absent field is a thrown render. */}
                            {(rj.reasons || []).map((why, i) => (
                              <span key={i} className="e8-jobmatch-why"><span className="material-symbols-outlined e8-cand-ov-whyic">check</span>{why}</span>
                            ))}
                          </span>
                        </span>
                        <span className="material-symbols-outlined e8-cand-ov-jmchev">chevron_right</span>
                      </button>
                    );
                  })}
                </div>
              </OvDisclose></div>
            );
          })()}
        </div>
      ) : null}
    </div>
  );
}

Object.assign(window, {
  CandOvAbout, CandOvPrescreen, CandOvNeedRow, CandOvAiFlag, CandOvActItem,
  CandOvLatestActivity, CandOverviewTab,
});
