/* app/set-prefs.jsx — Settings pages: Preferences + Profile.
   ============================================================================================
   OWNERSHIP: this file and app/set-prefs.css are owned by ONE agent, and nine other agents are
   editing their own page modules concurrently, so nothing outside these two files may be touched.
   The <script>/<link> tags are already wired and the router finds a page purely by its key in the
   map at the bottom. ONE loose end that this agent cannot close: the `?v=` tags live in
   "ELEV8 ATS.html", a shared file, so `app/set-prefs.css?v=1` still needs bumping by whoever owns
   that file — `node scripts/check-cachebust.mjs` names it, and an unbumped tag ships the old file.

   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-prefs.css under a `.e8-set-prefs-*` 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-*`.

   TWO CONVENTIONS THIS PAGE NOW HOLDS, written down because a critic found the surface holding
   both halves of each of them at once.
     - SECTION HEADINGS ARE TITLE CASE. "Time Display", "Workspace and Data" — matching
       docs/SETTINGS-REFERENCE.md §P1/§P4/§P5 ("Time Display", "Email & Calendar", "Note Taker").
       Minor words (and, or, of, the) stay lowercase. The surface was split 5/5 between this and
       sentence case across ten pages; the reference is the tie-break, so sentence-case headings on
       the other eight pages are the ones that move, not these. ROW LABELS ARE NOT PART OF THIS:
       they are sentence case here and on every other page, which is already one rule, so they stay
       as they are rather than being half-migrated alongside the headings.
     - ONE DEVICE FOR A WORKED EXAMPLE: `PrefsReadout`, a full-bleed tinted strip at the foot of the
       card. There is no in-description variant any more — `.e8-set-prefs-eg` (a left-ruled block
       inside `.e8-set-row-desc`) was the second grammar for the same job and is deleted, not kept
       for the short cases.

   WHAT IS REAL HERE (and what is not) — see the report for the full ledger.
     - Appearance and Workspace-and-data are the LEGACY SettingsScreen controls, ported whole, still
       driving the same setters: theme.setAppearance / setAccent / setDensity / setSidebarLabels,
       useSignals(), and E8_DATA_MODES + e8SwitchDataMode behind the same confirm() gate. No control
       was dropped and none of them became a mock.
     - General / Time Display are NEW preferences with no existing consumer in the app, so they are
       backed by a real persisted store (`e8-set-prefs-v1`) and prove themselves through live
       formatting: the currency readout formats a REAL bill rate off window.E8DATA, and the Time
       Display card carries a preview strip that re-renders from Intl on every change. Two of them
       reaches outside the page for real: Spell check writes document.body.spellcheck. Table row
       shading used to live here too, writing a `data-row-shading` attribute nothing consumed; it
       has been removed because the theme layer already owns that setting as `data-rowstripes`.
     - Two descriptions are live readings rather than copy: "Prototype data" reports the record
       counts actually loaded into window.E8DATA, and "Navigation and views" reports this persona's
       real rail membership out of E8Workspace. Both degrade to silence if the shape is absent.
     - Profile identity is the LIVE persona (window.e8ActivePersona), and edits are stored per
       persona so switching demo workspace does not leak one person's name onto another.
   ============================================================================================ */

const E8SPrefs = window.E8Set;

/* ---------- the preference store ---------------------------------------------------------------
   One localStorage key, one custom event, so both pages (and any later surface) stay in sync
   without either of them owning the other. Everything is guarded: a blocked or full localStorage
   degrades to in-memory state rather than taking the screen down. */
const E8SP_KEY = 'e8-set-prefs-v1';
const E8SP_PROFILE_KEY = 'e8-set-profile-v1';
const E8SP_EVENT = 'e8-set-prefs-changed';

const E8SP_DEFAULTS = {
  currency: 'USD',
  country: 'US',
  aiLanguage: 'en-US',
  distance: 'mi',
  spellCheck: true,
  timeZone: '',
  dateFormat: 'mdy',
  timeFormat: '12',
  timeDisplay: 'relative',
  firstDay: 'Monday',
};

function e8spRead(key, defaults) {
  try {
    const raw = window.localStorage.getItem(key);
    const parsed = raw ? JSON.parse(raw) : null;
    return parsed && typeof parsed === 'object' ? { ...defaults, ...parsed } : { ...defaults };
  } catch (e) {
    return { ...defaults };
  }
}

function e8spWrite(key, next) {
  try { window.localStorage.setItem(key, JSON.stringify(next)); } catch (e) { /* quota / private mode */ }
  window.dispatchEvent(new CustomEvent(E8SP_EVENT, { detail: { key } }));
  return next;
}

/* The two preferences that reach outside this page. Applied on change AND once at load, but only
   when the user has actually stored something — an untouched install gets zero behaviour change. */
function e8spApplyGlobal(prefs) {
  try {
    if (document.body) document.body.spellcheck = prefs.spellCheck !== false;
    /* Row shading is deliberately NOT here. It belongs to the theme layer, which owns a real
       three-state preference (off / subtle / strong) written as `data-rowstripes` and consumed by
       real rules in app/app.css. This page briefly carried its own `data-row-shading` boolean that
       nothing in the repo read - a control that persisted and repainted nothing - and it would have
       collided with the theme one the moment both landed on main. Do not re-add it: the setting
       appears in the Appearance section through `theme.setRowStripes`. */
  } catch (e) { /* no DOM yet */ }
}
try {
  if (window.localStorage.getItem(E8SP_KEY)) e8spApplyGlobal(e8spRead(E8SP_KEY, E8SP_DEFAULTS));
} catch (e) { /* storage unavailable */ }

function useSetPrefs() {
  const [prefs, setPrefs] = React.useState(() => e8spRead(E8SP_KEY, E8SP_DEFAULTS));
  React.useEffect(() => {
    const sync = () => setPrefs(e8spRead(E8SP_KEY, E8SP_DEFAULTS));
    window.addEventListener(E8SP_EVENT, sync);
    window.addEventListener('storage', sync);
    return () => { window.removeEventListener(E8SP_EVENT, sync); window.removeEventListener('storage', sync); };
  }, []);
  /* The write and the DOM side effect happen OUTSIDE the updater. A setState updater must be pure:
     React 18 StrictMode invokes it twice, which would double-write storage and fire the sync event
     twice on every keystroke. A ref mirrors the latest state so the callback can stay identity-
     stable without closing over a stale value. */
  const latest = React.useRef(prefs);
  latest.current = prefs;
  const patch = React.useCallback((next) => {
    const merged = { ...latest.current, ...next };
    latest.current = merged;
    setPrefs(merged);
    e8spWrite(E8SP_KEY, merged);
    e8spApplyGlobal(merged);
  }, []);
  return [prefs, patch];
}

/* ---------- reference data + formatting --------------------------------------------------------
   Currency/country/zone lists are reference data, not app data. The NUMBERS they format are real:
   the currency example is the live bill rate off the first engagement in window.E8DATA. */
const E8SP_CURRENCIES = [
  { value: 'USD', label: 'United States dollar', locale: 'en-US' },
  { value: 'CAD', label: 'Canadian dollar', locale: 'en-CA' },
  { value: 'EUR', label: 'Euro', locale: 'de-DE' },
  { value: 'GBP', label: 'Pound sterling', locale: 'en-GB' },
  { value: 'AUD', label: 'Australian dollar', locale: 'en-AU' },
  { value: 'INR', label: 'Indian rupee', locale: 'en-IN' },
  { value: 'PHP', label: 'Philippine peso', locale: 'en-PH' },
  { value: 'MXN', label: 'Mexican peso', locale: 'es-MX' },
];

const E8SP_COUNTRIES = [
  { value: 'US', label: 'United States', dial: '+1', flag: '🇺🇸' },
  { value: 'CA', label: 'Canada', dial: '+1', flag: '🇨🇦' },
  { value: 'GB', label: 'United Kingdom', dial: '+44', flag: '🇬🇧' },
  { value: 'IE', label: 'Ireland', dial: '+353', flag: '🇮🇪' },
  { value: 'DE', label: 'Germany', dial: '+49', flag: '🇩🇪' },
  { value: 'IN', label: 'India', dial: '+91', flag: '🇮🇳' },
  { value: 'PH', label: 'Philippines', dial: '+63', flag: '🇵🇭' },
  { value: 'MX', label: 'Mexico', dial: '+52', flag: '🇲🇽' },
  { value: 'AU', label: 'Australia', dial: '+61', flag: '🇦🇺' },
];

const E8SP_LANGUAGES = [
  { value: 'en-US', label: 'American English' },
  { value: 'en-GB', label: 'British English' },
  { value: 'es-419', label: 'Spanish (Latin America)' },
  { value: 'pt-BR', label: 'Portuguese (Brazil)' },
  { value: 'fr-FR', label: 'French' },
  { value: 'de-DE', label: 'German' },
  { value: 'tl-PH', label: 'Filipino' },
];

const E8SP_ZONES = [
  'America/Los_Angeles', 'America/Denver', 'America/Chicago', 'America/New_York',
  'America/Sao_Paulo', 'Europe/London', 'Europe/Dublin', 'Europe/Berlin', 'Europe/Madrid',
  'Africa/Johannesburg', 'Asia/Dubai', 'Asia/Kolkata', 'Asia/Manila', 'Asia/Singapore',
  'Asia/Tokyo', 'Australia/Sydney',
];

const E8SP_DAYS = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];

function e8spResolvedZone() {
  try { return Intl.DateTimeFormat().resolvedOptions().timeZone || 'America/Chicago'; }
  catch (e) { return 'America/Chicago'; }
}

/* "Central European Summer Time (Berlin)" — the long name alone collides (Berlin and Madrid are
   both CEST), and a select with two identical labels is worse than a wide one. */
function e8spZoneLabel(tz) {
  const city = String(tz).split('/').pop().replace(/_/g, ' ');
  try {
    const parts = new Intl.DateTimeFormat('en-US', { timeZone: tz, timeZoneName: 'long' }).formatToParts(new Date());
    const named = parts.find((p) => p.type === 'timeZoneName');
    return named && named.value !== tz ? named.value + ' (' + city + ')' : city;
  } catch (e) {
    return city;
  }
}

function e8spZoneOptions(current) {
  const list = E8SP_ZONES.slice();
  if (current && list.indexOf(current) === -1) list.unshift(current);
  return list.map((tz) => ({ value: tz, label: e8spZoneLabel(tz) }));
}

function e8spSampleRate() {
  try {
    const rows = (window.E8DATA || {}).engagements || [];
    const hit = rows.find((row) => typeof row.bill === 'number' && row.bill > 0);
    if (hit) return hit.bill;
  } catch (e) { /* prism-remapped data may not carry engagements */ }
  return 135;
}

function e8spMoney(amount, code) {
  const cur = E8SP_CURRENCIES.find((c) => c.value === code) || E8SP_CURRENCIES[0];
  try {
    return new Intl.NumberFormat(cur.locale, { style: 'currency', currency: cur.value }).format(amount);
  } catch (e) {
    return cur.value + ' ' + amount;
  }
}

function e8spDate(date, fmt, tz) {
  const base = tz ? { timeZone: tz } : {};
  try {
    if (fmt === 'iso') return new Intl.DateTimeFormat('en-CA', { ...base, year: 'numeric', month: '2-digit', day: '2-digit' }).format(date);
    if (fmt === 'dmy') return new Intl.DateTimeFormat('en-GB', { ...base, year: 'numeric', month: '2-digit', day: '2-digit' }).format(date);
    if (fmt === 'med') return new Intl.DateTimeFormat('en-GB', { ...base, year: 'numeric', month: 'short', day: 'numeric' }).format(date);
    return new Intl.DateTimeFormat('en-US', { ...base, year: 'numeric', month: '2-digit', day: '2-digit' }).format(date);
  } catch (e) {
    return date.toDateString();
  }
}

function e8spTime(date, fmt, tz) {
  try {
    return new Intl.DateTimeFormat('en-US', {
      ...(tz ? { timeZone: tz } : {}), hour: 'numeric', minute: '2-digit', hour12: fmt !== '24',
    }).format(date);
  } catch (e) {
    return date.toTimeString().slice(0, 5);
  }
}

function e8spRelative(hoursAgo) {
  try { return new Intl.RelativeTimeFormat('en', { numeric: 'auto' }).format(-hoursAgo, 'hour'); }
  catch (e) { return hoursAgo + ' hours ago'; }
}

function e8spWeekOrder(first) {
  const start = Math.max(0, E8SP_DAYS.indexOf(first));
  return E8SP_DAYS.map((_, i) => E8SP_DAYS[(start + i) % 7]);
}

/* "Prototype data" claims a dataset is loaded, so it should say what actually loaded rather than
   repeat the mode's own marketing line. Counts come off the live E8DATA the prism produced, which
   is why the demo dataset reports 46 jobs and not the number in data.js. Returns null rather than
   guessing if the shape is not there. */
function e8spLoadedCounts() {
  try {
    const D = window.E8DATA || {};
    const parts = [
      { n: (D.jobs || []).length, one: 'job', many: 'jobs' },
      { n: (D.candidates || []).length, one: 'candidate', many: 'candidates' },
      { n: (D.clients || []).length, one: 'client', many: 'clients' },
    ].filter((p) => p.n > 0);
    if (!parts.length) return null;
    return parts.map((p) => p.n.toLocaleString() + ' ' + (p.n === 1 ? p.one : p.many)).join(' · ');
  } catch (e) {
    return null;
  }
}

/* The Customize row is a route-out, so the only honest thing it can show is the shape of what it
   routes to: the rail this persona is actually looking at, read from E8Workspace.

   The derivation is `(roleDefaults[role] ∪ nav.extra) − nav.hidden`, copied from the one the rail
   and the More panel both use (app/shell.jsx `inRailNow`). `nav.order` is NOT the rail's membership
   — it is only the sort, and it holds ids the rail does not show; counting `order.length -
   hidden.length` gives a plausible-looking number that is wrong, which is the whole reason this
   comment exists. If the rail's model moves, this moves with it or it stops being a measurement. */
function e8spRailShape() {
  try {
    const W = window.E8Workspace;
    const persona = workspacePersona();
    if (!W || !W.get || !persona || !persona.name) return null;
    const roleIds = (W.roleDefaults && (W.roleDefaults[persona.role] || W.roleDefaults.recruiter)) || [];
    const nav = (W.get(persona.name) || {}).nav || {};
    const extra = Array.isArray(nav.extra) ? nav.extra : [];
    const hidden = Array.isArray(nav.hidden) ? nav.hidden : [];
    const inRail = new Set(roleIds.concat(extra).filter((id) => hidden.indexOf(id) === -1));
    if (!inRail.size) return null;
    const pins = W.listPinRows ? W.listPinRows(persona.name).length : 0;
    return inRail.size + ' destinations in your rail'
      + (pins ? ' · ' + pins + ' pinned record' + (pins === 1 ? '' : 's') : '');
  } catch (e) {
    return null;
  }
}

/* ---------- small page-local pieces -------------------------------------------------------------
   Neither of these is a new ROW/CARD/CONTROL grammar — they are the contents of one control cell.
   Anything dimensional still comes from set-core. */

function PrefsAccentGrid({ themes, accent, onPick }) {
  const current = (themes || []).find((t) => t.key === accent);
  return (
    <div className="e8-set-prefs-accents" role="group" aria-label="Accent color">
      <span className="e8-set-prefs-accent-name">{current ? current.name : 'Custom'}</span>
      <span className="e8-set-prefs-accent-row">
        {(themes || []).map((t) => (
          <button
            key={t.key}
            type="button"
            title={t.name}
            aria-label={t.name}
            aria-pressed={accent === t.key}
            className={'e8-set-prefs-accent' + (accent === t.key ? ' is-on' : '')}
            onClick={() => onPick && onPick(t.key)}
          >
            <span className="e8-set-prefs-accent-dot" style={{ background: t.key }} aria-hidden="true" />
          </button>
        ))}
      </span>
    </div>
  );
}

/* ONE device for "here is what this setting does", and it is this one.
   The page used to answer that question two ways 400px apart: a left-ruled example inside a row
   description on the General card, and a full-bleed tinted footer on the Time display card. Same
   job, two grammars, which is what makes a surface read as several authors. The footer wins — it
   is a card-level primitive rather than a decoration on one row's prose, it survives the 640px
   stack without re-flowing anything, and it inherits set-core's inset hairline for free by being
   an ordinary card child. So: every worked example on this page is a `PrefsReadout` at the foot of
   the card it belongs to, captioned with the setting it illustrates, and `.e8-set-prefs-eg` is
   gone rather than kept "just for the short ones".

   Content is data-shaped, never prose — a value, an arrow, a separator — because the line is
   monospaced and set in tabular figures so a re-render on every keystroke does not reflow. */
function PrefsReadout({ cap, children, extra }) {
  return (
    <div className="e8-set-prefs-readout">
      <span className="e8-set-prefs-readout-cap">{cap}</span>
      <span className="e8-set-prefs-readout-line">{children}</span>
      {extra || null}
    </div>
  );
}

function PrefsTimePreview({ prefs, zone }) {
  const now = new Date();
  const stamp = new Date(now.getTime() - 2 * 60 * 60 * 1000);
  return (
    <PrefsReadout
      cap="Preview"
      extra={(
        <span className="e8-set-prefs-week" aria-label={'Week starts on ' + prefs.firstDay}>
          {e8spWeekOrder(prefs.firstDay).map((day, i) => (
            <span key={day} className={i === 0 ? 'is-first' : undefined} aria-hidden="true">{day.slice(0, 2)}</span>
          ))}
        </span>
      )}
    >
      {e8spDate(stamp, prefs.dateFormat, zone)}
      <span className="e8-set-prefs-sep" aria-hidden="true">·</span>
      {e8spTime(stamp, prefs.timeFormat, zone)}
      <span className="e8-set-prefs-sep" aria-hidden="true">·</span>
      {prefs.timeDisplay === 'relative' ? e8spRelative(2) : 'exact time shown'}
    </PrefsReadout>
  );
}

/* ---------- P1 · Preferences -------------------------------------------------------------------- */

function SetPreferencesPage() {
  const { Page, Section, Card, Row, NavRow, Toggle, Select, Seg } = E8SPrefs;
  const ctx = React.useContext(window.E8Ctx) || {};
  const theme = ctx.theme || {};
  const [prefs, setPrefs] = useSetPrefs();
  const [signals, setSignals] = useSignals();
  const { confirm, dialog } = window.useConfirm();

  const deviceZone = e8spResolvedZone();
  const zone = prefs.timeZone || deviceZone;
  const country = E8SP_COUNTRIES.find((c) => c.value === prefs.country) || E8SP_COUNTRIES[0];
  const rate = e8spSampleRate();
  const dataMode = window.e8CurrentDataMode ? window.e8CurrentDataMode() : 'demo';
  const modes = window.E8_DATA_MODES || [];
  const activeMode = modes.find((m) => m.id === dataMode);
  const loaded = e8spLoadedCounts();
  const railShape = e8spRailShape();

  const chooseDataMode = (id) => {
    const item = modes.find((m) => m.id === id);
    if (!item || item.id === dataMode) return;
    confirm({
      title: 'Switch to ' + item.label.toLowerCase() + ' data?',
      body: item.sub + '. The app reloads. Records created in the current mode are kept and reappear when you switch back.',
      confirmLabel: 'Switch and reload',
      tone: 'primary',
      icon: item.icon,
      onConfirm: () => e8SwitchDataMode(item.id),
    });
  };

  return (
    <Page title="Preferences">
      <Section title="General">
        <Card>
          <Row
            label="Default currency"
            desc={'Used wherever a rate, a margin or a renewal value has no currency of its own — pay and bill '
              + 'rates, commission, and every total rolled up from them. Changing it re-labels those amounts; '
              + 'it does not convert them, and records that carry their own currency keep it.'}
            control={(
              <Select
                ariaLabel="Default currency"
                prefix={prefs.currency}
                value={prefs.currency}
                options={E8SP_CURRENCIES}
                onChange={(v) => setPrefs({ currency: v })}
              />
            )}
          />
          <Row
            label="Default country code"
            desc="Pre-selected when you add a phone number to a candidate or contact."
            control={(
              <Select
                ariaLabel="Default country code"
                flag={country.flag}
                value={prefs.country}
                options={E8SP_COUNTRIES.map((c) => ({ value: c.value, label: c.label + ' · ' + c.dial }))}
                onChange={(v) => setPrefs({ country: v })}
              />
            )}
          />
          <Row
            label="Default AI language"
            desc="The language ELEV8 AI writes in — summaries, req briefs and drafted replies."
            control={(
              <Select
                ariaLabel="Default AI language"
                value={prefs.aiLanguage}
                options={E8SP_LANGUAGES}
                onChange={(v) => setPrefs({ aiLanguage: v })}
              />
            )}
          />
          <Row
            label="Default distance unit"
            desc="Used for commute distance on matches and for job location radius."
            control={(
              <Select
                ariaLabel="Default distance unit"
                value={prefs.distance}
                options={[{ value: 'mi', label: 'Miles (mi)' }, { value: 'km', label: 'Kilometres (km)' }]}
                onChange={(v) => setPrefs({ distance: v })}
              />
            )}
          />
          <Row
            label="Spell check"
            desc="Check spelling as you type in notes, messages and drafted replies."
            control={(
              <Toggle
                label="Spell check"
                checked={prefs.spellCheck}
                onChange={(v) => setPrefs({ spellCheck: v })}
              />
            )}
          />
          {/* The number on the left is the real bill rate this readout reads off E8DATA; the one on
              the right is that same number under the currency selected above. Showing both is the
              honest demonstration of "re-labels, does not convert" — and the arrow carries that
              meaning, so a screen reader gets the words rather than a hidden glyph. */}
          <PrefsReadout cap="Currency">
            {rate}
            <span className="e8-set-prefs-arrow" aria-hidden="true">→</span>
            <span className="e8-set-prefs-sr">reads as</span>
            {e8spMoney(rate, prefs.currency)} / hr
          </PrefsReadout>
        </Card>
      </Section>

      <Section title="Time Display">
        <Card>
          <Row
            label="Time zone"
            desc={deviceZone && deviceZone !== zone
              ? 'Every timestamp in the workspace is shown in this zone. This device reports '
                + e8spZoneLabel(deviceZone) + '.'
              : 'Every timestamp in the workspace is shown in this zone, detected from this device.'}
            control={(
              <Select
                ariaLabel="Time zone"
                value={zone}
                options={e8spZoneOptions(zone)}
                onChange={(v) => setPrefs({ timeZone: v })}
              />
            )}
          />
          <Row
            label="Date format"
            control={(
              <Select
                ariaLabel="Date format"
                value={prefs.dateFormat}
                options={[
                  { value: 'mdy', label: 'MM/DD/YYYY' },
                  { value: 'dmy', label: 'DD/MM/YYYY' },
                  { value: 'iso', label: 'YYYY-MM-DD' },
                  { value: 'med', label: 'D MMM YYYY' },
                ]}
                onChange={(v) => setPrefs({ dateFormat: v })}
              />
            )}
          />
          <Row
            label="Time format"
            control={(
              <Select
                ariaLabel="Time format"
                value={prefs.timeFormat}
                options={[{ value: '12', label: '12-hour (AM/PM)' }, { value: '24', label: '24-hour' }]}
                onChange={(v) => setPrefs({ timeFormat: v })}
              />
            )}
          />
          <Row
            label="Time display"
            desc="How an activity stamp reads in lists and on record timelines."
            control={(
              <Select
                ariaLabel="Time display"
                value={prefs.timeDisplay}
                options={[
                  { value: 'relative', label: 'Relative (e.g. 2 hours ago)' },
                  { value: 'absolute', label: 'Exact date and time' },
                ]}
                onChange={(v) => setPrefs({ timeDisplay: v })}
              />
            )}
          />
          <Row
            label="First day of week"
            control={(
              <Select
                ariaLabel="First day of week"
                value={prefs.firstDay}
                options={['Monday', 'Sunday', 'Saturday']}
                onChange={(v) => setPrefs({ firstDay: v })}
              />
            )}
          />
          <PrefsTimePreview prefs={prefs} zone={zone} />
        </Card>
      </Section>

      <Section title="Appearance" desc="How the workspace looks on this device. Saved per browser, not per account.">
        <Card>
          <Row
            label="Color mode"
            desc="Use a light, dark, or system-matched interface."
            control={(
              <Seg
                ariaLabel="Color mode"
                value={theme.appearance}
                options={[
                  { value: 'light', label: 'Light' },
                  { value: 'dark', label: 'Dark' },
                  { value: 'system', label: 'Auto' },
                ]}
                onChange={(v) => theme.setAppearance && theme.setAppearance(v)}
              />
            )}
          />
          <Row
            label="Accent"
            desc="Reserved for actions, links and selection. Everything else stays neutral."
            control={(
              <PrefsAccentGrid
                themes={theme.themes}
                accent={theme.accent}
                onPick={(key) => theme.setAccent && theme.setAccent(key)}
              />
            )}
          />
          <Row
            label="Workspace density"
            desc="Controls the default row height and spacing of shared work surfaces."
            control={(
              <Seg
                ariaLabel="Workspace density"
                value={theme.density}
                options={[
                  { value: 'comfortable', label: 'Comfortable' },
                  { value: 'compact', label: 'Compact' },
                ]}
                onChange={(v) => theme.setDensity && theme.setDensity(v)}
              />
            )}
          />
          <Row
            label="Sidebar section label"
            desc="Show the heading above your destinations in the left rail."
            control={(
              <Toggle
                label="Sidebar section label"
                checked={!!theme.sidebarLabels}
                onChange={(v) => theme.setSidebarLabels && theme.setSidebarLabels(v)}
              />
            )}
          />
          {/* Row shading is owned by the THEME layer, not by this page - it writes `data-rowstripes`
              and app.css consumes it, so it belongs beside the other appearance settings that go
              through `theme`. This page briefly carried its own boolean copy that nothing read; that
              was removed, and this is the real control taking its place.

              It has to live here because the settings shell replaces the legacy panel that used to
              host it: every other appearance control was ported across, this one was added to the
              legacy screen afterwards, and the two changes crossed. Without this row the preference
              still applies (the default is "subtle") but nothing in the product can change it. */}
          <Row
            label={<>Table row shading <span className="e8-set-prefs-wink">(Sam Dever Mode)</span></>}
            desc="Alternate the background of table rows so a long list does not lose your place. Hover and selection stay stronger than the tint at every setting."
            control={(
              <Seg
                ariaLabel="Table row shading"
                value={theme.rowStripes || 'subtle'}
                options={[{ value: 'off', label: 'Off' }, { value: 'subtle', label: 'Subtle' }, { value: 'strong', label: 'Strong' }]}
                onChange={(v) => theme.setRowStripes && theme.setRowStripes(v)}
              />
            )}
          />
        </Card>
      </Section>

      <Section title="Workspace and Data" desc="Personalize navigation and choose which prototype dataset the app runs on.">
        <Card>
          <Row
            label="Platform signals"
            desc="Show the ELEV8 provenance marks that say which surface a value came from."
            control={(
              <Toggle
                label="Platform signals"
                checked={!!signals}
                onChange={(v) => setSignals(v)}
              />
            )}
          />
          {/* The switch reloads the app, so it is confirm-gated (`chooseDataMode` above) rather than
              a live segmented control, and the accessible name says so — the visible label alone
              reads like a display preference. It remains the one control on this surface that no
              page of the reference has; see the report. */}
          <Row
            label="Prototype data"
            desc={(activeMode ? activeMode.sub + '. ' : '')
              + 'Changing datasets asks first and then reloads the app; records you created in the '
              + 'current one are kept and reappear when you switch back.'}
            control={(
              <Seg
                ariaLabel="Prototype dataset — switching reloads the app"
                value={dataMode}
                options={modes.map((m) => ({ value: m.id, label: m.label }))}
                onChange={chooseDataMode}
              />
            )}
          />
          {/* A NAV ROW, not a button in a control cell: this is the one row on the page that leaves
              the settings surface, and the reference's navigation-row variant (whole row is the
              target, chevron at the right) is the grammar for exactly that. It still lands on the
              legacy `.e8-workspace` dialog, which is a different design system — that dialog lives
              in app/shell.jsx and cannot be fixed from here. See the report. */}
          <NavRow
            label="Navigation and views"
            desc={'Reorder the rail, choose default columns, and pick which views appear where.'
              + (railShape ? ' ' + railShape + '.' : '')}
            onClick={() => window.dispatchEvent(new CustomEvent('e8-open-workspace', { detail: { tab: 'navigation' } }))}
          />
          {loaded ? <PrefsReadout cap="Loaded">{loaded}</PrefsReadout> : null}
        </Card>
      </Section>
      {dialog}
    </Page>
  );
}

/* ---------- P2 · Profile -------------------------------------------------------------------------
   Identity comes from the live persona (window.e8ActivePersona via workspacePersona) and the edits
   are stored PER PERSONA, so switching demo workspace shows that person's own profile rather than
   leaking one persona's name onto another. */

/* `null` means "never edited, follow the persona"; `''` means "the user cleared this field". They
   have to be different values. With `''` as the default, `profile.username || personaName` snaps
   the persona's name back into the input the moment you delete the last character, and the field
   cannot be emptied at all. */
const E8SP_PROFILE_DEFAULTS = { username: null, initials: null, mfa: false };

function e8spInitials(name) {
  return String(name || '')
    .split(/\s+/)
    .filter(Boolean)
    .slice(0, 2)
    .map((part) => part[0])
    .join('')
    .toUpperCase() || 'E8';
}

function e8spProfileFor(personaName) {
  const all = e8spRead(E8SP_PROFILE_KEY, {});
  const row = all[personaName];
  return row && typeof row === 'object' ? { ...E8SP_PROFILE_DEFAULTS, ...row } : { ...E8SP_PROFILE_DEFAULTS };
}

function e8spSaveProfile(personaName, next) {
  const all = e8spRead(E8SP_PROFILE_KEY, {});
  all[personaName] = next;
  e8spWrite(E8SP_PROFILE_KEY, all);
  return next;
}

/* A pairing key is generated fresh each time setup opens and never leaves the tab. This is a
   prototype pairing flow, not a TOTP implementation — it proves the ROW STATES (off → pairing →
   on) that the reference page has, which is what this surface is being judged on. */
function e8spPairingKey() {
  const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
  let out = '';
  for (let i = 0; i < 16; i += 1) {
    out += alphabet[Math.floor(Math.random() * alphabet.length)];
    if (i % 4 === 3 && i !== 15) out += ' ';
  }
  return out;
}

function SetProfilePage() {
  const { Page, Section, Card, Row, Input, Btn, Pill, Icon } = E8SPrefs;
  const ctx = React.useContext(window.E8Ctx) || {};
  const showToast = ctx.showToast;
  const persona = workspacePersona() || {};
  const personaName = persona.name || 'You';
  const [profile, setProfile] = React.useState(() => e8spProfileFor(personaName));
  const [avatar, setAvatar] = React.useState(null);
  const [pairing, setPairing] = React.useState(null);
  const [code, setCode] = React.useState('');
  const [error, setError] = React.useState('');
  const fileRef = React.useRef(null);

  React.useEffect(() => { setProfile(e8spProfileFor(personaName)); }, [personaName]);
  React.useEffect(() => {
    if (!window.E8Events) return undefined;
    return window.E8Events.subscribe(['persona:changed'], () => setProfile(e8spProfileFor(
      (window.e8ActivePersona ? window.e8ActivePersona().name : personaName)
    )));
  }, [personaName]);

  const save = (next) => { setProfile(next); e8spSaveProfile(personaName, next); };
  const user = (window.E8DATA || {}).user || {};
  const personaInitials = personaName === user.name && user.initials ? user.initials : e8spInitials(personaName);
  const username = profile.username == null ? personaName : profile.username;
  const initials = profile.initials == null ? personaInitials : profile.initials;
  /* The avatar always has something to draw even while the initials field is empty mid-edit. */
  const avatarInitials = initials.trim() || personaInitials;
  const roleLine = [persona.label || user.role, user.org].filter(Boolean).join(' · ');

  const onPick = (event) => {
    const file = event.target.files && event.target.files[0];
    event.target.value = '';
    if (!file) return;
    const reader = new FileReader();
    reader.onload = () => {
      setAvatar(String(reader.result));
      if (showToast) showToast('Picture updated for this session — uploads are not stored in the prototype');
    };
    reader.readAsDataURL(file);
  };

  const startPairing = () => { setPairing(e8spPairingKey()); setCode(''); setError(''); };
  const cancelPairing = () => { setPairing(null); setCode(''); setError(''); };
  const verify = () => {
    if (!/^\d{6}$/.test(code)) { setError('Enter the six digits your authenticator app is showing.'); return; }
    setPairing(null);
    setCode('');
    setError('');
    save({ ...profile, mfa: true });
    if (showToast) showToast('Multi-factor authentication is on');
  };
  /* A failure reports itself INSIDE the pairing block, never as a toast: every toast in this app
     renders with a check_circle glyph (app/main.jsx), so "could not copy" would arrive ticked. The
     success case is genuinely affirmative and stays a toast. */
  const copyKey = () => {
    if (!navigator.clipboard) { setError('This browser will not let the page copy for you — select the key and copy it by hand.'); return; }
    navigator.clipboard.writeText(String(pairing).replace(/\s/g, ''))
      .then(() => { setError(''); if (showToast) showToast('Setup key copied'); })
      .catch(() => setError('Could not copy the setup key — select it and copy it by hand.'));
  };

  /* Three states, one control cell. The trigger used to stay on its "Set up authenticator app"
     label and merely go `disabled` while the panel below was open, which reads as broken rather
     than as in-progress; it now says what it does in the state it is actually in, and the panel
     no longer carries a second Cancel beside it. */
  let mfaControl;
  if (profile.mfa) {
    mfaControl = (
      <React.Fragment>
        <Pill tone="ok"><Icon name="check_circle" />On</Pill>
        <Btn kind="quiet" onClick={() => { save({ ...profile, mfa: false }); if (showToast) showToast('Multi-factor authentication is off'); }}>
          Turn off
        </Btn>
      </React.Fragment>
    );
  } else if (pairing) {
    mfaControl = <Btn kind="secondary" onClick={cancelPairing}>Cancel setup</Btn>;
  } else {
    mfaControl = <Btn kind="secondary" onClick={startPairing}>Set up authenticator app</Btn>;
  }

  return (
    <Page title="Profile">
      <Section>
        <Card>
          <Row
            label="Profile picture"
            desc={roleLine || 'Shown on your notes, submissions and activity.'}
            control={(
              <span className="e8-set-prefs-avatarwrap">
                <span className="e8-set-prefs-avatar">
                  {avatar
                    ? <img className="e8-set-prefs-avatar-img" src={avatar} alt="" />
                    : avatarInitials}
                </span>
                <input
                  ref={fileRef}
                  className="e8-set-prefs-file"
                  type="file"
                  accept="image/*"
                  tabIndex={-1}
                  aria-hidden="true"
                  onChange={onPick}
                />
                <Btn
                  kind="secondary"
                  icon="edit"
                  title="Change profile picture"
                  onClick={() => fileRef.current && fileRef.current.click()}
                >
                  <span className="e8-set-prefs-sr">Change profile picture</span>
                </Btn>
                {avatar ? (
                  <Btn kind="quiet" onClick={() => setAvatar(null)}>Remove</Btn>
                ) : null}
              </span>
            )}
          />
          {/* Both fields take set-core's `md` (190px), the nearest existing size to the reference's
              ~180px, so their left edges line up and the card has a column. They were `lg` (300px)
              and `sm` (120px), 180px apart. The size-to-content rule that makes the selects vary is
              about OPTION text — a free-text field has no content to size to, and two inputs that
              share only a right edge read as two unrelated cards. A local 180px width would have
              matched the reference more exactly and is exactly the drift this surface exists to
              avoid; if 180 is wanted it is one number in set-core.css. */}
          <Row
            label="Username"
            desc="How you are named on notes, submissions and @-mentions."
            control={(
              <Input
                ariaLabel="Username"
                size="md"
                value={username}
                placeholder={personaName}
                onChange={(v) => save({ ...profile, username: v })}
              />
            )}
          />
          <Row
            label="Initials"
            desc="Used on your avatar and in dense table cells. Two or three characters."
            control={(
              <Input
                ariaLabel="Initials"
                size="md"
                value={initials}
                placeholder={personaInitials}
                onChange={(v) => save({ ...profile, initials: v.toUpperCase().slice(0, 3) })}
              />
            )}
          />
        </Card>
      </Section>

      <Section title="Security">
        <Card>
          {/* No control. The row used to carry a "Change password" button whose entire effect was a
              toast restating this description — and the toast stack paints check_circle on every
              message (app/main.jsx), so a refusal shipped wearing a success mark. There is no
              identity-provider URL anywhere in this prototype to link out to, and inventing one
              would be worse than the button, so the row states the fact and stops. If an IdP with a
              real console URL is ever modelled, this becomes `<Btn href={idp.url}>` — SetBtn already
              renders the external-link glyph, exactly as the Calls page does for the app stores. */}
          <Row
            label="Password"
            desc="Managed by the identity provider your workspace signs in with. ELEV8 never stores it,
              so there is nothing to change here."
          />
          <Row
            label="Multi-factor authentication"
            desc="Ask for a one-time code from an authenticator app after your password. Recommended for
              anyone who can see candidate contact details, which is everyone with a recruiter seat."
            control={mfaControl}
          />
          {pairing ? (
            <div className="e8-set-prefs-mfa">
              <div className="e8-set-prefs-mfa-t">Pair an authenticator app</div>
              <ol className="e8-set-prefs-mfa-steps">
                <li>Add a new account in your authenticator app and enter this setup key.</li>
                <li>Type the six-digit code it shows to finish.</li>
              </ol>
              <div className="e8-set-prefs-key">
                <code>{pairing}</code>
                <Btn kind="quiet" icon="content_copy" onClick={copyKey}>Copy</Btn>
              </div>
              <div className="e8-set-prefs-mfa-act">
                <Input
                  ariaLabel="Six-digit code"
                  size="sm"
                  value={code}
                  placeholder="000000"
                  onChange={(v) => { setCode(v.replace(/\D/g, '').slice(0, 6)); setError(''); }}
                />
                <Btn kind="primary" onClick={verify} disabled={code.length !== 6}>Verify and turn on</Btn>
              </div>
              {error ? <div className="e8-set-prefs-err" role="alert">{error}</div> : null}
            </div>
          ) : null}
        </Card>
      </Section>
    </Page>
  );
}

window.E8SetPages = window.E8SetPages || {};
Object.assign(window.E8SetPages, { preferences: SetPreferencesPage, profile: SetProfilePage });
