/* app/set-data.jsx — Settings pages: Data Model + Features.
   ============================================================================================
   OWNERSHIP: this file and app/set-data.css are owned by ONE agent. Nothing else in the repo needs
   to change to build these pages — the <script>/<link> tags are already wired and the router finds
   a page purely by its key in the map below. The ONE exception is the cache key: `?v=` lives in
   "ELEV8 ATS.html", which this agent does not own, so an edit here needs the tag bumped by whoever
   does. `node scripts/check-cachebust.mjs` names the file and the tag.

   CONTRACT
     - Read docs/SETTINGS-REFERENCE.md for what each page must contain, then the root CLAUDE.md.
     - Build every page out of `window.E8Set` primitives. Do not hand-roll a row, card or control;
       if a primitive is missing, say so rather than inventing a local one — dimensional drift
       between pages is the single most visible defect on this surface.
     - All CSS goes in app/set-data.css under a `.e8-set-data-*` prefix. Never edit app/set-core.css.
     - ZERO inline `style={{…}}` touching fontSize/color/display/gap/flex/margin/padding: a new
       file's inline-style budget is 0 and one such object fails `npm run lint`.
     - Colours are `--ui-*` tokens or `color-mix()` on them. Text sizes are `--ui-text-*`
       (11.5 / 12.5 / 13.5 / 15 / 18 / 24 / 28 — there is no 14px). Icons are `--ui-icon-*`.

   WHERE THE NUMBERS ON THESE TWO PAGES COME FROM
   Both pages are wired to the live dataset rather than to literals, because both are pages whose
   whole job is to *report* what the workspace contains — a hardcoded "12 attributes" beside a
   schema the user just edited is worse than no number at all.

     Data Model  attribute counts come from `window.E8DATA.fieldSchema`, preferring the copy the
                 field editor (#/fields) persists to localStorage['e8-fields-v2'] — that screen
                 only writes the edited schema back onto E8DATA once IT has mounted, so reading the
                 stored copy first keeps these counts honest in a session that never opened it.
                 Two of the six objects (Applications, Files) have no schema entry, so their
                 attributes are DERIVED from the loaded records: the union of keys, with coverage
                 measured as the share of records carrying a non-empty value. Those rows are
                 labelled `Derived` rather than passed off as schema.
     Features    provider names, credit balances, run counts, workflow and pool counts are read
                 from E8DATA. The toggles themselves are this page's own state, persisted under
                 localStorage['e8-set-features-v1'] — written on CHANGE only, never on mount, so
                 merely loading Settings leaves the profile untouched.

   MEASURED, not assumed. Executing the helpers below against app/data.js (no prism, no scale mode)
   returns: Candidates 12 attrs / 16 records, Jobs 11 / 11, Applications 18 DERIVED / 31,
   Companies 10 / 8, Contacts 9 / 8, Files 9 DERIVED / 12; base providers pdl + coresignal,
   waterfall 4 (dropcontact not enabled), compliance none + cognism (not enabled). The live app
   runs the prism remap over this, which changes the record counts and not the attribute sets —
   which is the whole reason the page reads them at render rather than baking them in.
   ============================================================================================ */

const E8SData = window.E8Set;

/* ---------- shared helpers -------------------------------------------------------------------- */

/* Normalises first, exactly like e8sGo in app/set-connect.jsx, so the two helpers in this feature
   accept the same input and emit the same output. They did not: this one required a full `#/...`
   hash from its callers while that one required a bare path, and BOTH worked, because `navigate`
   (shell.jsx) strips a leading `#/` itself. That is what makes it worth fixing rather than leaving
   - nothing fails when you copy a call from one file to the other, so the divergence is invisible
   until someone changes `navigate` or reads the two side by side. The `location.hash` fallback does
   no normalising of its own, which is why it is built from the stripped value. */
function e8SetDataGo(path) {
  const clean = String(path || '').replace(/^#\/?/, '');
  if (window.navigate) window.navigate('#/' + clean);
  else location.hash = '#/' + clean;
}

/* Scale mode multiplies the dataset x100, so a bare `1024918` in a row is unreadable. */
function e8SetDataNum(n) {
  return typeof n === 'number' && isFinite(n) ? n.toLocaleString() : '0';
}
const e8SetDataPlural = (n, one, many) => e8SetDataNum(n) + ' ' + (n === 1 ? one : many);

/* The editor at #/fields persists edits to `e8-fields-v2` and only assigns them onto
   window.E8DATA.fieldSchema from inside its own effect. Read the stored copy first so the counts
   here agree with the editor even when nobody has opened it. Read-only: this page never writes it.

   BOTH keys are read, newest first, and that is not belt-and-braces. This page was written against
   `v1`, which was correct at the time; the Fields work on another branch then moved the editor to
   `v2` and deliberately abandoned the old copy, because v1 held edits to a vocabulary the records no
   longer speak. Reading only v1 would leave this page showing seeded defaults forever, silently,
   the moment that branch lands. Reading only v2 would blank the counts for anyone whose browser
   still holds a v1 copy and has not reopened the editor since. So: prefer v2, fall back to v1, and
   let the editor's own migration retire v1 in its own time.

   The fallback is what makes this safe to delete later: when v1 is gone from every browser, the
   second lookup simply stops matching. */
const E8S_DM_SCHEMA_KEYS = ['e8-fields-v2', 'e8-fields-v1'];
function e8SetDataSchema() {
  for (let i = 0; i < E8S_DM_SCHEMA_KEYS.length; i++) {
    try {
      const stored = JSON.parse(localStorage.getItem(E8S_DM_SCHEMA_KEYS[i]));
      if (stored && typeof stored === 'object') return stored;
    } catch (e) { /* corrupt or unavailable — try the next key, then the shipped schema */ }
  }
  return (window.E8DATA || {}).fieldSchema || {};
}

/* Turn a record key into a label: `rejectReason` → "Reject reason", `aiScreen` → "AI screen".
   Only used for the two objects that have no authored schema. */
const E8S_WORDS = { id: 'ID', ai: 'AI', cv: 'CV', po: 'PO', url: 'URL', msa: 'MSA', ar: 'AR', csm: 'CSM' };
function e8SetDataTitle(key) {
  const words = String(key).replace(/([a-z0-9])([A-Z])/g, '$1 $2').replace(/[_-]+/g, ' ')
    .toLowerCase().split(' ').filter(Boolean);
  return words.map((w, i) => E8S_WORDS[w] || (i === 0 ? w.charAt(0).toUpperCase() + w.slice(1) : w)).join(' ');
}

function e8SetDataKind(v) {
  if (Array.isArray(v)) return 'list';
  if (typeof v === 'boolean') return 'boolean';
  if (typeof v === 'number') return 'number';
  if (v && typeof v === 'object') return 'object';
  return 'text';
}

/* Attributes inferred from real records: every key any record carries, in first-seen order, with
   coverage measured rather than declared. An empty collection honestly yields no attributes. */
function e8SetDataInfer(rows) {
  const order = [];
  const seen = {};
  rows.forEach((r) => Object.keys(r || {}).forEach((k) => {
    if (!seen[k]) { seen[k] = { key: k, filled: 0, type: null }; order.push(seen[k]); }
  }));
  rows.forEach((r) => order.forEach((f) => {
    const v = (r || {})[f.key];
    const empty = v === undefined || v === null || v === '' || (Array.isArray(v) && !v.length);
    if (empty) return;
    f.filled += 1;
    if (!f.type) f.type = e8SetDataKind(v);
  }));
  const n = rows.length || 1;
  return order.map((f) => ({
    label: e8SetDataTitle(f.key), key: f.key, type: f.type || 'text', source: 'derived',
    required: false, coverage: Math.round((f.filled / n) * 100), options: null
  }));
}

const E8S_TYPE = {
  text: { label: 'Text', icon: 'title' },
  email: { label: 'Email', icon: 'alternate_email' },
  phone: { label: 'Phone', icon: 'call' },
  select: { label: 'Single select', icon: 'arrow_drop_down_circle' },
  multiselect: { label: 'Multi-select', icon: 'checklist' },
  currency: { label: 'Currency', icon: 'payments' },
  rating: { label: 'Rating', icon: 'star_rate' },
  textarea: { label: 'Long text', icon: 'notes' },
  date: { label: 'Date', icon: 'event' },
  boolean: { label: 'Yes / no', icon: 'toggle_on' },
  number: { label: 'Number', icon: 'tag' },
  list: { label: 'List', icon: 'data_array' },
  object: { label: 'Structured', icon: 'data_object' },
  user: { label: 'User', icon: 'person' }
};
const E8S_SOURCE = { standard: 'Standard', custom: 'Custom', ai: 'AI', zoominfo: 'Synced', derived: 'Derived' };

/* ---------- W4 · Data Model -------------------------------------------------------------------- */

/* The six objects the reference lists, mapped onto what ELEV8 actually stores. `schema` names the
   fieldSchema entry when there is one; the two without fall back to inference over `rows`.

   `noun` and `from` build the row's description, and the grammar is deliberate: a row says what the
   object IS and where its shape comes from, never what the product has not built yet. The earlier
   copy read "Derived from 31 records — no attribute editor yet" on two of six rows, which turned a
   third of the page into a changelog. "Derived from the candidate and job records they join" is the
   same fact stated as a property of the data — and the drill-in already renders those attributes,
   read-only, which is the answer to the question the old sentence provoked. */
const E8S_OBJECTS = [
  { key: 'candidates', label: 'Candidates', icon: 'person', schema: 'candidate',
    noun: ['candidate', 'candidates'],
    rows: (D) => D.candidates || [] },
  { key: 'jobs', label: 'Jobs', icon: 'work', schema: 'job',
    noun: ['job', 'jobs'],
    rows: (D) => D.jobs || [] },
  { key: 'applications', label: 'Applications', icon: 'how_to_reg', schema: null,
    noun: ['application', 'applications'],
    from: 'derived from the candidate and job records they join',
    rows: (D) => D.applications || [],
    note: 'An application joins a candidate to a job and carries no schema of its own, so its attributes are read from those records rather than authored here.' },
  { key: 'companies', label: 'Companies', icon: 'apartment', schema: 'client',
    noun: ['company', 'companies'],
    rows: (D) => D.clients || [] },
  { key: 'contacts', label: 'Contacts', icon: 'contacts', schema: 'contact',
    noun: ['contact', 'contacts'],
    rows: (D) => D.contacts || [] },
  { key: 'files', label: 'Files', icon: 'folder', schema: null,
    noun: ['file', 'files'],
    from: 'derived from the documents attached to candidates and jobs',
    rows: (D) => [].concat(
      ...Object.keys(D.candidateFiles || {}).map((k) => D.candidateFiles[k] || []),
      ...Object.keys(D.jobFiles || {}).map((k) => D.jobFiles[k] || [])),
    note: 'File attributes are read off the documents already attached to candidates and jobs.' }
];

function e8SetDataObjects() {
  const D = window.E8DATA || {};
  const schema = e8SetDataSchema();
  return E8S_OBJECTS.map((o) => {
    const rows = o.rows(D) || [];
    const ent = o.schema ? schema[o.schema] : null;
    const authored = !!(ent && Array.isArray(ent.fields));
    const fields = authored ? ent.fields.map((f) => ({
      label: f.label, key: f.key, type: f.type, source: f.source || 'standard',
      required: !!f.required, coverage: typeof f.coverage === 'number' ? f.coverage : null,
      options: f.options || null
    })) : e8SetDataInfer(rows);
    return {
      key: o.key, label: o.label, icon: o.icon, note: o.note || null, count: rows.length,
      blurb: e8SetDataPlural(rows.length, o.noun[0], o.noun[1])
        + (o.from ? ', ' + o.from : ' in this workspace'),
      derived: !authored, fields: fields,
      suggest: (authored && (D.suggestedFields || {})[o.schema]) || []
    };
  });
}

/* Coverage below this reads as a field people are not actually filling in, and saying so is the
   only reason to draw the bar at all. Above it the meter is accent; below it, warning-tinted. */
const E8S_THIN = 70;

function SetDataAttrRow({ field }) {
  const { Row, Pill } = E8SData;
  const meta = E8S_TYPE[field.type] || E8S_TYPE.text;
  const bits = [field.key, meta.label];
  if (field.required) bits.push('Required');
  if (field.options && field.options.length) bits.push(field.options.length + ' options');
  const pct = typeof field.coverage === 'number' ? Math.max(0, Math.min(100, field.coverage)) : null;
  /* `derived` is a property of the whole object, already stated in the section description — a pill
     repeating it on all 18 rows is noise, so only a source that varies row to row earns one. */
  const badge = field.source && field.source !== 'standard' && field.source !== 'derived'
    ? E8S_SOURCE[field.source] || field.source : null;
  /* The two coverage numbers are NOT the same claim and the tooltip has to say which one it is:
     an authored field carries a figure the schema declares, an inferred one carries a figure this
     page counted off the loaded records a moment ago. */
  const covTitle = field.source === 'derived'
    ? pct + '% of loaded records carry a value'
    : pct + '% coverage, as declared in the field schema';
  return (
    <Row
      icon={meta.icon}
      label={field.label}
      desc={bits.join(' · ')}
      control={
        <React.Fragment>
          {badge ? <Pill tone={field.source === 'ai' ? 'info' : undefined}>{badge}</Pill> : null}
          {pct === null ? null : (
            <span className={'e8-set-data-cov' + (pct < E8S_THIN ? ' is-thin' : '')} title={covTitle}>
              <span className="e8-set-data-cov-track">
                {/* A measured value is data, not styling — a custom property is the correct carrier. */}
                <span className="e8-set-data-cov-fill" style={{ '--e8-set-data-pct': pct + '%' }} />
              </span>
              <span className="e8-set-data-cov-n">{pct}%</span>
            </span>
          )}
        </React.Fragment>
      }
    />
  );
}

/* The drill-in is a ROUTE, not a piece of component state. Held in state it was invisible to the
   browser: Back skipped straight past it to whatever preceded Settings, and an attribute list could
   not be linked, bookmarked or reloaded. `#/settings/datamodel/<object>` parses to
   { page: 'settings', id: 'datamodel', sub: '<object>' } (app/shell.jsx parseHash), so the shell
   still resolves this page from `id` and only the sub-segment is ours to read.

   One thing this page CANNOT do from here: the breadcrumb. `.e8-set-crumbs` is built in
   app/set-core.jsx from the nav entry alone and has no slot a page can contribute a fourth chip to,
   so the trail still reads Home › Settings › Data Model inside an object. That needs a core change,
   not a second breadcrumb rendered locally. */
const E8S_DM_ROOT = '#/settings/datamodel';

function SetDatamodelPage() {
  const { Page, Section, Card, NavRow, Btn, Pill, Banner, Empty } = E8SData;
  const route = window.useRoute();
  /* Read once per MOUNT, not once per session and not once per render. Per session would go stale
     the moment the field editor adds an attribute; per render would re-infer the Applications and
     Files attribute sets on every drill-down. Leaving this page and coming back re-reads. */
  const objects = React.useMemo(e8SetDataObjects, []);
  /* A hash naming an object that does not exist falls back to the list rather than to an error
     page — the same reason record screens guard instead of `find() || [0]`. */
  const detail = route.sub ? objects.find((o) => o.key === route.sub) || null : null;
  const open = detail ? detail.key : null;

  /* Drilling in and back out replaces the whole view, so whichever control the user activated has
     been unmounted by the time the new one paints — and a keyboard user is dropped to the top of
     the document on every hop. Send focus to the way out on the way in, and back to the row they
     came from on the way out. `fromRow` starts at -1 so this never fires on first mount and steals
     focus from the rail; and because the focus is programmatic the browser's own :focus-visible
     heuristic still applies, so a mouse click does not paint a ring the mouse user did not ask for. */
  const backRef = React.useRef(null);
  const listRef = React.useRef(null);
  const fromRow = React.useRef(-1);
  React.useEffect(() => {
    if (fromRow.current < 0) return;
    const host = open ? backRef.current : listRef.current;
    if (!host) return;
    const target = open ? host.querySelector('button')
      : host.querySelectorAll('.e8-set-navrow')[fromRow.current];
    if (target) target.focus();
  }, [open]);
  /* Now that the view is linkable, someone can arrive INSIDE an object without having clicked a row,
     and leaving would then drop focus on the unmounted button with nowhere to send it. Aim at the
     first row in that case; a real click still returns to the row it came from. */
  const closeObject = React.useCallback(() => {
    if (fromRow.current < 0) fromRow.current = 0;
    e8SetDataGo(E8S_DM_ROOT);
  }, []);

  if (detail) {
    const rows = detail.fields;
    const desc = detail.derived
      ? e8SetDataPlural(rows.length, 'attribute', 'attributes') + ' derived from '
        + e8SetDataPlural(detail.count, 'loaded record', 'loaded records')
        + ' · coverage is measured here, not declared'
      : e8SetDataPlural(rows.length, 'attribute', 'attributes') + ' · '
        + e8SetDataPlural(detail.count, 'record', 'records') + ' in this workspace';
    return (
      <Page title="Data Model" subtitle="Every object ELEV8 stores, and the attributes captured on each one.">
        <div className="e8-set-data-back" ref={backRef}>
          <Btn kind="quiet" icon="chevron_left" onClick={closeObject}>All objects</Btn>
        </div>
        {/* A derived object has no editor to open, and the honest label for that is what the list
            IS — read-only — not a sentence about what has not been built. */}
        <Section title={detail.label} desc={desc}
          action={detail.derived ? <Pill>Read-only</Pill>
            : <Btn icon="tune" onClick={() => e8SetDataGo('#/fields')}>Open field editor</Btn>}>
          {detail.suggest.length ? (
            <Banner tone="info">
              ELEV8 has {detail.suggest.length === 1 ? 'one suggested attribute' : detail.suggest.length + ' suggested attributes'} for {detail.label.toLowerCase()} — {detail.suggest.map((s) => s.label).join(', ')}. Review them in the field editor.
            </Banner>
          ) : null}
          {detail.note ? <Banner tone="info" icon="info">{detail.note}</Banner> : null}
          <Card>
            {rows.length
              ? rows.map((f) => <SetDataAttrRow key={f.key} field={f} />)
              : <Empty inline title="No records are loaded, so no attributes can be derived." />}
          </Card>
        </Section>
      </Page>
    );
  }

  return (
    <Page title="Data Model" subtitle="Every object ELEV8 stores, and the attributes captured on each one.">
      {/* A real §2.2 section heading, 18px/600 with core's 30px above and 12px below. This page
          used a titleless Section for its clearance alone, which left it the only page in the beat
          with no heading and no 18px step in its type scale — the rhythm reads as broken next to
          its seven siblings even though nothing here is misaligned. */}
      <Section title="Objects">
        <div ref={listRef}>
          <Card>
            {objects.map((o, i) => (
              <NavRow key={o.key} icon={o.icon} label={o.label} desc={o.blurb}
                meta={e8SetDataPlural(o.fields.length, 'attribute', 'attributes')}
                onClick={() => { fromRow.current = i; e8SetDataGo(E8S_DM_ROOT + '/' + o.key); }} />
            ))}
          </Card>
        </div>
      </Section>
    </Page>
  );
}

/* ---------- W5 · Features ---------------------------------------------------------------------- */

const E8S_FEAT_KEY = 'e8-set-features-v1';
/* Card 3's master ships OFF: the reference uses it to demonstrate the dimmed-dependent state, and
   a page where nothing is off cannot show that state at all. */
const E8S_FEAT_DEFAULT = {
  candEnrich: true, candBase: 'pdl', candWhen: 'add', candWrite: 'blanks',
  orgEnrich: true, orgWaterfall: 'fullenrich', orgCompliance: 'cognism',
  autoProfiles: false, autoWindow: 'monthly', autoConflict: 'flag',
  dedupe: true,
  consent: false, replyIntent: true, sendWindow: false,
  pools: true, automations: false, opportunities: false, recordIds: false
};

function e8SetDataFeatures() {
  try {
    const stored = JSON.parse(localStorage.getItem(E8S_FEAT_KEY));
    if (stored && typeof stored === 'object') return Object.assign({}, E8S_FEAT_DEFAULT, stored);
  } catch (e) { /* corrupt or unavailable — ship the defaults */ }
  return Object.assign({}, E8S_FEAT_DEFAULT);
}

/* Provider options come from the real waterfall in E8DATA. A provider the workspace has not turned
   on is still listed — hiding it would misrepresent the waterfall — but says so in its label. */
function e8SetDataProviders(layer) {
  const list = ((window.E8DATA || {}).enrichProviders || []).filter((p) => p.layer === layer);
  return list.map((p) => ({ value: p.id, label: p.enabled ? p.name : p.name + ' · not enabled' }));
}

/* A stored preference can name a provider the waterfall no longer carries. A <select> whose value
   matches no <option> renders BLANK rather than erroring, so the page would silently show an empty
   control — fall back to the first real option instead. */
function e8SetDataPick(value, options) {
  return (options || []).some((o) => o.value === value) ? value : ((options || [])[0] || {}).value;
}

/* One master row on a tint, then the dependent block, then any row that stays live while the master
   is off. The dependents are wrapped in ONE element carrying the lock class — dimming them
   individually is the named failure mode that leaves a label at full strength beside a grey
   control. `live` sits outside that wrapper, which is exactly what "stays live" means.

   The dim is CSS; the inertness is not. `pointer-events: none` stops a mouse and nothing else, so
   every dependent control also takes the master's state as its own `disabled` — that is what keeps
   a locked select out of the tab order, and it is why the dependency is real rather than painted.
   React 18 here, so no `inert` attribute; `aria-disabled` carries the state to a screen reader. */
function SetFeatureCard({ icon, label, desc, on, onToggle, children, live }) {
  const { Card, Row, Toggle } = E8SData;
  return (
    <Card>
      <Row tinted className="e8-set-data-master" icon={icon} label={label} desc={desc}
        control={<Toggle checked={on} onChange={onToggle} label={label} />} />
      <div className={'e8-set-data-dep' + (on ? '' : ' is-locked')}
        {...(on ? {} : { 'aria-disabled': 'true' })}>{children}</div>
      {live || null}
    </Card>
  );
}

function SetFeaturesPage() {
  const { Page, Section, Card, Row, NavRow, Toggle, Select } = E8SData;
  const [feat, setFeat] = React.useState(e8SetDataFeatures);
  const first = React.useRef(true);
  /* Persist on CHANGE only. The first pass is the mount, and a settings page that writes to the
     profile merely by being looked at is a page nobody can measure around. */
  React.useEffect(() => {
    if (first.current) { first.current = false; return; }
    try { localStorage.setItem(E8S_FEAT_KEY, JSON.stringify(feat)); } catch (e) { /* private mode */ }
  }, [feat]);
  const set = React.useCallback((key, value) => {
    setFeat((prev) => Object.assign({}, prev, { [key]: value }));
  }, []);

  const D = window.E8DATA || {};
  const credits = D.enrichCredits || {};
  const num = e8SetDataNum;
  const base = e8SetDataProviders('base');
  const waterfall = e8SetDataProviders('waterfall');
  const compliance = [{ value: 'none', label: 'None' }].concat(e8SetDataProviders('compliance'));
  const runs = (D.enrichRuns || []).length;
  const audit = (D.audit || []).length;
  const replies = (D.sequenceReplies || []).length;
  const flows = (D.workflows || []).length;
  const pools = (D.talentPools || []).length;
  const lists = (D.candidateLists || []).length;

  return (
    <Page title="Features">
      <Section title="Enrichment"
        desc="What ELEV8 fills in for you, from which provider, and what it is allowed to overwrite.">

        <SetFeatureCard icon="person_search" label="Candidate enrichment"
          desc={'Resolve a new candidate against ' + base.length + ' base providers before a recruiter opens the record.'}
          on={feat.candEnrich} onToggle={(v) => set('candEnrich', v)}
          live={<NavRow icon="history" label="Enrichment activity"
            meta={e8SetDataPlural(runs, 'recent run', 'recent runs')}
            onClick={() => e8SetDataGo('#/enrichment')} />}>
          <Row icon="database" label="Base provider"
            desc="First hop of the waterfall — person and employer resolution."
            control={<Select value={e8SetDataPick(feat.candBase, base)} options={base} disabled={!feat.candEnrich}
              ariaLabel="Base provider" onChange={(v) => set('candBase', v)} />} />
          {/* Not `bolt` — that glyph is Automations further down the page, and two rows on one
              page wearing the same icon reads as a copy-paste rather than a category. */}
          <Row icon="play_circle" label="Run enrichment"
            desc="When a candidate record is created or changes hands."
            control={<Select value={feat.candWhen} disabled={!feat.candEnrich} ariaLabel="Run enrichment"
              options={[{ value: 'add', label: 'On add' }, { value: 'qualify', label: 'On qualify' },
                { value: 'manual', label: 'Manual only' }]}
              onChange={(v) => set('candWhen', v)} />} />
          <Row icon="edit_note" label="Existing values"
            desc="Candidate-verified answers always outrank an enrichment result, whatever this is set to."
            control={<Select value={feat.candWrite} disabled={!feat.candEnrich} ariaLabel="Existing values"
              options={[{ value: 'blanks', label: 'Fill blanks only' },
                { value: 'refresh', label: 'Refresh enrichment-sourced fields' },
                { value: 'never', label: 'Never overwrite' }]}
              onChange={(v) => set('candWrite', v)} />} />
        </SetFeatureCard>

        <SetFeatureCard icon="domain" label="Company and contact enrichment"
          desc="Firmographics on accounts, and the email and phone waterfall behind every contact."
          on={feat.orgEnrich} onToggle={(v) => set('orgEnrich', v)}
          live={<NavRow icon="toll" label="Credit usage"
            meta={num(credits.used) + ' of ' + num(credits.total) + ' credits'}
            onClick={() => e8SetDataGo('#/enrichment')} />}>
          <Row icon="alternate_email" label="Contact waterfall"
            desc="Tried in order until a deliverable address or a mobile comes back."
            control={<Select value={e8SetDataPick(feat.orgWaterfall, waterfall)} options={waterfall}
              disabled={!feat.orgEnrich}
              ariaLabel="Contact waterfall" onChange={(v) => set('orgWaterfall', v)} />} />
          <Row icon="gavel" label="Compliance screen"
            desc="Checks lawful basis and suppression lists before a contact becomes reachable."
            control={<Select value={e8SetDataPick(feat.orgCompliance, compliance)} options={compliance}
              disabled={!feat.orgEnrich}
              ariaLabel="Compliance screen" onChange={(v) => set('orgCompliance', v)} />} />
        </SetFeatureCard>

        {/* The reference's demonstration card: master OFF, dependents inert, history still reachable. */}
        <SetFeatureCard icon="autorenew" label="Auto-updating profiles"
          desc="Re-check enriched records on a schedule and pull forward anything that has moved."
          on={feat.autoProfiles} onToggle={(v) => set('autoProfiles', v)}
          live={<NavRow icon="manage_history" label="Field change history"
            meta={e8SetDataPlural(audit, 'entry', 'entries')}
            onClick={() => e8SetDataGo('#/audit')} />}>
          <Row icon="update" label="Refresh window"
            desc="How often a record is eligible to be looked at again."
            control={<Select value={feat.autoWindow} disabled={!feat.autoProfiles} ariaLabel="Refresh window"
              options={[{ value: 'weekly', label: 'Weekly' }, { value: 'monthly', label: 'Monthly' },
                { value: 'quarterly', label: 'Quarterly' }]}
              onChange={(v) => set('autoWindow', v)} />} />
          <Row icon="rule" label="When a value conflicts"
            desc="A returned value that disagrees with what is on the record."
            control={<Select value={feat.autoConflict} disabled={!feat.autoProfiles} ariaLabel="When a value conflicts"
              options={[{ value: 'flag', label: 'Flag for review' },
                { value: 'keep', label: 'Keep the recorded value' },
                { value: 'newest', label: 'Take the newest' }]}
              onChange={(v) => set('autoConflict', v)} />} />
        </SetFeatureCard>

        <Card>
          <Row icon="content_copy" label="Duplicate detection"
            desc="Match on email, phone, LinkedIn and name at intake, then route likely twins to a review queue instead of merging them."
            control={<Toggle checked={feat.dedupe} onChange={(v) => set('dedupe', v)} label="Duplicate detection" />} />
        </Card>
      </Section>

      <Section title="Communication" desc="Applies to every outbound email, message and call.">
        <Card>
          <Row icon="verified_user" label="Consent tracking"
            desc="Record a lawful basis per contact and block a send that has none."
            control={<Toggle checked={feat.consent} onChange={(v) => set('consent', v)} label="Consent tracking" />} />
          <Row icon="psychology" label="Reply intent tagging"
            desc={'Label every inbound reply — interested, referral, not now — as it lands. '
              + e8SetDataPlural(replies, 'reply is', 'replies are') + ' tagged so far.'}
            control={<Toggle checked={feat.replyIntent} onChange={(v) => set('replyIntent', v)} label="Reply intent tagging" />} />
          <Row icon="schedule" label="Send window"
            desc="Hold anything scheduled outside working hours in the recipient's own timezone."
            control={<Toggle checked={feat.sendWindow} onChange={(v) => set('sendWindow', v)} label="Send window" />} />
        </Card>
      </Section>

      <Section title="Workspace" desc="Objects and surfaces available to everyone in this workspace.">
        <Card>
          <Row icon="list_alt" label="Talent pools and lists"
            desc={e8SetDataPlural(pools, 'pool', 'pools') + ' and '
              + e8SetDataPlural(lists, 'saved list', 'saved lists') + ' are in use here.'}
            control={<Toggle checked={feat.pools} onChange={(v) => set('pools', v)} label="Talent pools and lists" />} />
          <Row icon="bolt" label="Automations"
            desc={e8SetDataPlural(flows, 'workflow is', 'workflows are')
              + ' built; they stay paused while this is off.'}
            control={<Toggle checked={feat.automations} onChange={(v) => set('automations', v)} label="Automations" />} />
          <Row icon="donut_small" label="Opportunities"
            desc="Track pre-requisition demand separately from open jobs."
            control={<Toggle checked={feat.opportunities} onChange={(v) => set('opportunities', v)} label="Opportunities" />} />
          <Row icon="tag" label="Show record IDs"
            desc="Print the internal id next to every record title — useful while migrating off another ATS."
            control={<Toggle checked={feat.recordIds} onChange={(v) => set('recordIds', v)} label="Show record IDs" />} />
        </Card>
      </Section>
    </Page>
  );
}

window.E8SetPages = window.E8SetPages || {};
Object.assign(window.E8SetPages, { datamodel: SetDatamodelPage, features: SetFeaturesPage });
