/* AddModals.jsx — "Add organization" and "Add employee" dialogs.
 *
 * Both follow the same chrome as AddPeopleModal / ResetPasswordModal in
 * Lists.jsx (cream surface, petrol primary, cross-icon close).
 *
 * They post to the backend's /api/* endpoints:
 *   POST /api/orgs                     (super_admin only)
 *   POST /api/orgs/:id/employees       (admin or super_admin)
 *
 * On success they call onCreated(payload) so the host can refresh its data
 * (re-fetch /api/bootstrap or splice the new row in locally) and then close.
 */

const { useState: useS2, useEffect: useE2, useMemo: useM2, useRef: useR2 } = React;

// Pull wizard primitives off the global scope (Babel <script>s don't share lexical scope).
// Defined in EmployeeWizard.jsx, which loads before this file.
const WizardChrome    = window.WizardChrome;
const BasicsStep      = window.BasicsStep;
const IdentityStep    = window.IdentityStep;
const SoulStep        = window.SoulStep;
const ConnectorsStep  = window.ConnectorsStep;
const SkillEnvStep    = window.SkillEnvStep;
const SectionHeader   = window.SectionHeader;
const isValidJson     = window.isValidJson;

// ── shared styles ─────────────────────────────────────────────────────
const amInput = {
  height: 44, padding: '0 14px', borderRadius: 12,
  background: 'var(--c-cream-50)',
  border: '1.5px solid var(--border-subtle)',
  boxShadow: 'var(--shadow-inset-input)',
  fontFamily: "'Suisse Int\\'l', sans-serif", fontSize: 14, color: 'var(--c-ink-900)',
  width: '100%', outline: 'none', boxSizing: 'border-box',
};
const amLabel = {
  fontFamily: "'Suisse Int\\'l', sans-serif", fontWeight: 500,
  fontSize: 13, color: 'var(--c-ink-900)',
};
const amTextarea = {
  ...amInput, height: 'auto', minHeight: 96, padding: '12px 14px', lineHeight: 1.5,
  resize: 'vertical', fontFamily: "'JetBrains Mono', ui-monospace, monospace", fontSize: 13,
};
const amOverlay = {
  position: 'fixed', inset: 0, background: 'rgba(23,53,64,0.32)',
  zIndex: 200, display: 'flex', alignItems: 'center', justifyContent: 'center',
  padding: 24, animation: 'pr-fade .15s ease-out',
};
const amCard = {
  width: 520, maxWidth: '100%', maxHeight: 'calc(100vh - 48px)',
  background: 'var(--c-cream-50)',
  border: '1px solid var(--border-subtle)', borderRadius: 16,
  boxShadow: 'var(--shadow-float-lg)', overflow: 'hidden',
  display: 'flex', flexDirection: 'column',
  animation: 'pr-fade .18s ease-out',
};
const amHeader = {
  display: 'flex', alignItems: 'flex-start', gap: 12,
  padding: '20px 22px 16px', borderBottom: '1px solid var(--border-subtle)',
  background: 'var(--c-cream-100)', flex: 'none',
};
const amBody = {
  padding: '20px 22px', display: 'flex', flexDirection: 'column', gap: 16,
  overflow: 'auto', flex: 1,
};
const amFooter = {
  padding: '14px 22px', borderTop: '1px solid var(--border-subtle)',
  background: 'var(--c-cream-100)', display: 'flex', alignItems: 'center', gap: 10, flex: 'none',
};
const amCloseBtn = {
  width: 32, height: 32, borderRadius: 10, border: '1px solid var(--border-subtle)',
  background: 'var(--c-white)', boxShadow: 'var(--shadow-raised-sm)', cursor: 'pointer',
  display: 'inline-flex', alignItems: 'center', justifyContent: 'center', flex: 'none',
};
const amPrimary = (disabled) => ({
  height: 40, padding: '0 18px', borderRadius: 12,
  background: disabled ? 'var(--c-petrol-300, #6b8a93)' : 'var(--c-petrol-800)',
  color: '#fff', border: 'none', cursor: disabled ? 'not-allowed' : 'pointer',
  fontFamily: "'Suisse Int\\'l', sans-serif", fontWeight: 600, fontSize: 14, letterSpacing: '-0.01em',
  boxShadow: 'var(--shadow-raised-sm)', display: 'inline-flex', alignItems: 'center', gap: 8,
  opacity: disabled ? 0.7 : 1,
});
const amGhost = {
  height: 40, padding: '0 14px', borderRadius: 12,
  background: 'transparent', color: 'var(--c-ink-700)',
  border: '1px solid var(--border-subtle)', cursor: 'pointer',
  fontFamily: "'Suisse Int\\'l', sans-serif", fontWeight: 500, fontSize: 14,
};
const amError = { fontSize: 12, color: 'var(--c-terra-600, #C65A42)' };

// ── helper ────────────────────────────────────────────────────────────
function authHeaders() {
  let token = null;
  try { token = localStorage.getItem('pr.token'); } catch {}
  return token ? { Authorization: `Bearer ${token}` } : {};
}

// ── Knowledge-base file utilities ─────────────────────────────────────
const KB_ALLOWED_EXTS = ['pdf', 'doc', 'docx', 'ppt', 'pptx', 'xls', 'xlsx', 'md', 'txt', 'rtf', 'csv', 'json'];
const kbExtFromName = (n) => {
  const m = String(n || '').toLowerCase().match(/\.([a-z0-9]+)$/);
  return m ? m[1] : '';
};
const kbBytes = (n) => {
  if (!n && n !== 0) return '';
  if (n < 1024) return n + ' B';
  if (n < 1024 * 1024) return Math.round(n / 1024) + ' KB';
  return (n / (1024 * 1024)).toFixed(1) + ' MB';
};
const kbExtTint = (ext) => {
  const e = (ext || '').toLowerCase();
  if (e === 'pdf')                       return { bg: '#FCE6E1', fg: '#9F3A26' };
  if (e === 'doc' || e === 'docx')       return { bg: '#E1ECFC', fg: '#1E4A8B' };
  if (e === 'xls' || e === 'xlsx' || e === 'csv') return { bg: '#E1F4E8', fg: '#1E6B3B' };
  if (e === 'ppt' || e === 'pptx')       return { bg: '#FCEDE1', fg: '#9F5A1F' };
  if (e === 'md'  || e === 'txt' || e === 'rtf' || e === 'json') return { bg: '#EFEFEF', fg: '#3A3A3A' };
  return { bg: '#EFEFEF', fg: '#3A3A3A' };
};
const kbReadAsDataUrl = (file) => new Promise((res, rej) => {
  const r = new FileReader();
  r.onload = () => res(r.result);
  r.onerror = rej;
  r.readAsDataURL(file);
});

// ── EmployeeKBSection — reusable upload + list panel ──────────────────
// Two modes:
//   • { mode: 'pending', files, onChange } — AddEmployeeModal: files held
//     in caller state, sent after the employee is created.
//   • { mode: 'live', orgId, empId } — EditEmployeeModal: writes through
//     to /api/employees/:orgId/:empId/knowledge-base immediately.
function EmployeeKBSection(props) {
  // `onMutate` fires after every successful live add/delete so the host
  // modal can flip its own dirty flag (the live writes are already
  // persistent on the server, but the host wants Save enabled as visual
  // feedback that something changed).
  const { mode, files: pendingFiles, onChange, onMutate, orgId, empId, roleHint } = props;
  const [liveFiles, setLiveFiles] = useS2([]);
  const [busy, setBusy]     = useS2(false);
  const [err, setErr]       = useS2('');
  const [drag, setDrag]     = useS2(false);
  const fileInput = React.useRef(null);

  const isLive = mode === 'live';
  const files = isLive ? liveFiles : (pendingFiles || []);

  // For live mode, hydrate the list from the server.
  useE2(() => {
    if (!isLive || !orgId || !empId) return;
    let cancelled = false;
    (async () => {
      try {
        const r = await fetch(`/api/employees/${encodeURIComponent(orgId)}/${encodeURIComponent(empId)}/knowledge-base`, {
          headers: { ...authHeaders() },
        });
        const d = await r.json().catch(() => ({}));
        if (!cancelled && r.ok) setLiveFiles(d.files || []);
      } catch (e) { if (!cancelled) setErr(String(e?.message || e)); }
    })();
    return () => { cancelled = true; };
  }, [isLive, orgId, empId]);

  const handleFiles = async (fileList) => {
    setErr('');
    if (!fileList || !fileList.length) return;
    setBusy(true);
    try {
      for (const f of Array.from(fileList)) {
        const ext = kbExtFromName(f.name);
        if (!KB_ALLOWED_EXTS.includes(ext)) {
          setErr(`Unsupported type: ${f.name}. Allowed: ${KB_ALLOWED_EXTS.join(', ').toUpperCase()}.`);
          continue;
        }
        if (f.size > 6 * 1024 * 1024) {
          setErr(`${f.name} is too large for the preview (>6 MB).`);
          continue;
        }
        const dataUrl = await kbReadAsDataUrl(f);
        if (isLive) {
          const r = await fetch(`/api/employees/${encodeURIComponent(orgId)}/${encodeURIComponent(empId)}/knowledge-base`, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json', ...authHeaders() },
            body: JSON.stringify({ name: f.name, size: f.size, type: f.type || '', ext, dataUrl }),
          });
          const d = await r.json().catch(() => ({}));
          if (r.ok && d.files) {
            setLiveFiles(d.files);
            if (onMutate) onMutate(d.files);
          }
          else setErr(d.error || `Upload failed (${r.status})`);
        } else {
          // Pending: append locally; AddEmployeeModal flushes on submit.
          const entry = {
            id: 'tmp-' + Math.random().toString(36).slice(2, 10),
            name: f.name, size: f.size, type: f.type || '', ext,
            uploadedAt: new Date().toISOString(),
            dataUrl,
          };
          onChange([entry, ...(pendingFiles || [])]);
        }
      }
    } catch (e) {
      setErr(String(e?.message || e));
    } finally {
      setBusy(false);
      if (fileInput.current) fileInput.current.value = '';
    }
  };

  const removeOne = async (fileId) => {
    if (isLive) {
      try {
        const r = await fetch(`/api/employees/${encodeURIComponent(orgId)}/${encodeURIComponent(empId)}/knowledge-base?fileId=${encodeURIComponent(fileId)}`, {
          method: 'DELETE', headers: { ...authHeaders() },
        });
        const d = await r.json().catch(() => ({}));
        if (r.ok) {
          setLiveFiles(d.files || []);
          if (onMutate) onMutate(d.files || []);
        }
      } catch (e) { setErr(String(e?.message || e)); }
    } else {
      onChange((pendingFiles || []).filter(f => f.id !== fileId));
    }
  };

  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
      <label style={amLabel}>
        Knowledge base <span style={{ color: 'var(--c-ink-500)', fontWeight: 400 }}>
          ({files.length} {files.length === 1 ? 'document' : 'documents'})
        </span>
      </label>
      <span style={{ fontSize: 11, color: 'var(--c-ink-500)' }}>
        Documents this employee can reference at run time
        {roleHint ? <> — e.g. for an L&D Director, attach curriculum, rubrics, facilitator notes.</> : '.'}
      </span>

      {/* Drop zone */}
      <div
        onDragOver={(e) => { e.preventDefault(); setDrag(true); }}
        onDragLeave={() => setDrag(false)}
        onDrop={(e) => {
          e.preventDefault();
          setDrag(false);
          handleFiles(e.dataTransfer.files);
        }}
        onClick={() => fileInput.current && fileInput.current.click()}
        style={{
          border: drag ? '1.5px dashed var(--c-petrol-800)' : '1.5px dashed var(--border-subtle)',
          background: drag ? 'var(--c-cream-100)' : 'var(--c-cream-50)',
          borderRadius: 12, padding: '16px 14px', cursor: 'pointer',
          display: 'flex', alignItems: 'center', gap: 12,
          transition: 'background .12s, border-color .12s',
        }}>
        <div style={{
          width: 36, height: 36, borderRadius: 10, flex: 'none',
          background: 'var(--c-petrol-100, #DDE9EC)', color: 'var(--c-petrol-800)',
          display: 'flex', alignItems: 'center', justifyContent: 'center',
          fontFamily: "'Suisse Int\\'l', sans-serif", fontWeight: 700, fontSize: 18,
        }}>+</div>
        <div style={{ flex: 1, minWidth: 0 }}>
          <div style={{ fontFamily: "'Suisse Int\\'l', sans-serif", fontWeight: 600, fontSize: 13, color: 'var(--c-ink-900)' }}>
            {busy ? 'Uploading…' : 'Drop files or click to upload'}
          </div>
          <div style={{ fontSize: 11, color: 'var(--c-ink-500)', marginTop: 2 }}>
            {KB_ALLOWED_EXTS.map(e => e.toUpperCase()).join(' · ')} · up to 6 MB each
          </div>
        </div>
        <input ref={fileInput} type="file" multiple
          accept={KB_ALLOWED_EXTS.map(e => '.' + e).join(',')}
          onChange={(e) => handleFiles(e.target.files)}
          style={{ display: 'none' }} />
      </div>

      {/* File list */}
      {files.length > 0 && (
        <div style={{
          display: 'flex', flexDirection: 'column', gap: 6,
          maxHeight: 220, overflow: 'auto',
          border: '1px solid var(--border-subtle)', borderRadius: 10,
          padding: 6, background: 'var(--c-cream-50)',
        }}>
          {files.map(f => {
            const tint = kbExtTint(f.ext);
            return (
              <div key={f.id} style={{
                display: 'flex', alignItems: 'center', gap: 10,
                padding: '8px 10px', borderRadius: 8,
                background: 'var(--c-white)', border: '1px solid var(--border-subtle)',
              }}>
                <div style={{
                  width: 32, height: 32, borderRadius: 8, flex: 'none',
                  background: tint.bg, color: tint.fg,
                  display: 'flex', alignItems: 'center', justifyContent: 'center',
                  fontFamily: "'JetBrains Mono', monospace", fontWeight: 700, fontSize: 10,
                  textTransform: 'uppercase',
                }}>{(f.ext || '?').slice(0, 4)}</div>
                <div style={{ flex: 1, minWidth: 0 }}>
                  <div style={{
                    fontFamily: "'Suisse Int\\'l', sans-serif", fontSize: 13, fontWeight: 500,
                    color: 'var(--c-ink-900)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
                  }}>{f.name}</div>
                  <div style={{ fontSize: 11, color: 'var(--c-ink-500)' }}>
                    {kbBytes(f.size)}{f.uploadedBy ? ' · ' + f.uploadedBy : ''}
                  </div>
                </div>
                <button type="button" onClick={() => removeOne(f.id)} title="Remove" style={{
                  width: 28, height: 28, borderRadius: 6, flex: 'none',
                  background: 'transparent', color: 'var(--c-terra-600, #C65A42)',
                  border: '1px solid var(--border-subtle)', cursor: 'pointer',
                  display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
                }}>
                  <img src="../assets/icons/cross.svg" width={9} height={9} alt="Remove" />
                </button>
              </div>
            );
          })}
        </div>
      )}

      {err && <span style={amError}>{err}</span>}
    </div>
  );
}

// ══════════════════════════════════════════════════════════════════════
// AddOrgModal — super admin only. Captures the bare minimum needed to
// list the org in the directory; the rich settings (info, knowledge base,
// brand kit) are filled in afterwards via Organization settings.
// ══════════════════════════════════════════════════════════════════════
// Knowledge-base accepted extensions live in OrgSettings.jsx; we mirror
// the same set here so the upload step matches what the rest of the app
// will accept after the org exists.
const ORG_KB_EXTS = ['pdf', 'doc', 'docx', 'md', 'pptx', 'txt', 'rtf', 'xlsx', 'csv'];
const ORG_BRAND_KIND = (ext) => {
  const e = (ext || '').toLowerCase();
  if (['svg', 'png', 'jpg', 'jpeg', 'webp', 'gif'].includes(e)) return 'image';
  if (['ttf', 'otf', 'woff', 'woff2'].includes(e))               return 'font';
  if (['ai', 'sketch', 'fig', 'xd'].includes(e))                 return 'design';
  if (['indd', 'docx', 'pptx', 'key', 'pages'].includes(e))      return 'template';
  if (['pdf'].includes(e))                                       return 'doc';
  return 'other';
};
const orgFileToBytes = (n) => {
  if (!n && n !== 0) return '';
  if (n < 1024) return n + ' B';
  if (n < 1024 * 1024) return Math.round(n / 1024) + ' KB';
  return (n / (1024 * 1024)).toFixed(1) + ' MB';
};
const orgReadAsDataUrl = (file) => new Promise((res, rej) => {
  const r = new FileReader();
  r.onload = () => res(r.result);
  r.onerror = rej;
  r.readAsDataURL(file);
});

const ORG_STEPS = [
  { id: 'basics',    label: 'Basics' },
  { id: 'kb',        label: 'Knowledge base', optional: true },
  { id: 'brand',     label: 'Brand kit',      optional: true },
];

function AddOrgModal({ onClose, onCreated }) {
  const [step, setStep]       = useS2(0);
  const [maxStep, setMaxStep] = useS2(0);
  const [name, setName] = useS2('');
  const [kind, setKind] = useS2('');
  const [info, setInfo] = useS2('');
  // Queued uploads — actual POST happens after the org is created.
  const [kbDrafts,    setKbDrafts]    = useS2([]);    // [{ key, file, ext, error? }]
  const [brandDrafts, setBrandDrafts] = useS2([]);
  const [busy, setBusy] = useS2(false);
  const [err, setErr]   = useS2('');
  const [progress, setProgress] = useS2(''); // status string while uploading attachments

  const kbInputRef    = useR2(null);
  const brandInputRef = useR2(null);

  const valid = name.trim().length >= 2;

  useE2(() => {
    const onKey = (e) => { if (e.key === 'Escape') onClose(); };
    document.addEventListener('keydown', onKey);
    return () => document.removeEventListener('keydown', onKey);
  }, []);

  const goTo   = (i) => { if (i <= maxStep) setStep(i); };
  const goNext = () => { const n = step + 1; if (n < ORG_STEPS.length) { setStep(n); setMaxStep(m => Math.max(m, n)); } };
  const goBack = () => setStep(s => Math.max(0, s - 1));

  // Attach files into the queue. We don't gate on extension here for the
  // KB step (server validates), but we surface a quick message on
  // anything that would clearly be rejected.
  const queueKbFiles = (list) => {
    if (!list || !list.length) return;
    setErr('');
    const added = Array.from(list).map(f => {
      const ext = (f.name.match(/\.([a-z0-9]+)$/i) || [])[1]?.toLowerCase() || '';
      const error = !ORG_KB_EXTS.includes(ext)
        ? `Unsupported type · allowed ${ORG_KB_EXTS.map(e => e.toUpperCase()).join(', ')}`
        : (f.size > 6 * 1024 * 1024 ? 'Larger than 6 MB' : null);
      return { key: `${f.name}-${f.size}-${Math.random().toString(36).slice(2,7)}`, file: f, ext, error };
    });
    setKbDrafts(prev => [...prev, ...added]);
  };
  const queueBrandFiles = (list) => {
    if (!list || !list.length) return;
    setErr('');
    const added = Array.from(list).map(f => {
      const ext = (f.name.match(/\.([a-z0-9]+)$/i) || [])[1]?.toLowerCase() || '';
      return { key: `${f.name}-${f.size}-${Math.random().toString(36).slice(2,7)}`, file: f, ext, error: null };
    });
    setBrandDrafts(prev => [...prev, ...added]);
  };
  const removeKbDraft    = (k) => setKbDrafts(prev => prev.filter(x => x.key !== k));
  const removeBrandDraft = (k) => setBrandDrafts(prev => prev.filter(x => x.key !== k));

  // Upload a single asset to either the knowledge-base or brand-kit
  // endpoint of the *just-created* org. Errors bubble up so the wizard
  // can surface a partial-success message.
  const uploadKb = async (orgId, item) => {
    const dataUrl = await orgReadAsDataUrl(item.file);
    const r = await fetch(`/api/orgs/${encodeURIComponent(orgId)}/knowledge-base`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json', ...authHeaders() },
      body: JSON.stringify({
        name: item.file.name, size: item.file.size,
        type: item.file.type || '', ext: item.ext, dataUrl,
      }),
    });
    if (!r.ok) {
      const d = await r.json().catch(() => ({}));
      throw new Error(d.error || `KB upload failed (${r.status})`);
    }
  };
  const uploadBrand = async (orgId, item) => {
    const dataUrl = await orgReadAsDataUrl(item.file);
    const r = await fetch(`/api/orgs/${encodeURIComponent(orgId)}/brand-kit`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json', ...authHeaders() },
      body: JSON.stringify({
        name: item.file.name, size: item.file.size,
        type: item.file.type || '', ext: item.ext,
        kind: ORG_BRAND_KIND(item.ext), dataUrl,
      }),
    });
    if (!r.ok) {
      const d = await r.json().catch(() => ({}));
      throw new Error(d.error || `Brand upload failed (${r.status})`);
    }
  };

  const submit = async () => {
    if (!valid || busy) return;
    setBusy(true); setErr(''); setProgress('Creating organization…');
    try {
      const r = await fetch('/api/orgs', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', ...authHeaders() },
        body: JSON.stringify({ name: name.trim(), info: info.trim() }),
      });
      const data = await r.json().catch(() => ({}));
      if (!r.ok) {
        setErr(
          data.error === 'forbidden' ? 'You don’t have permission to add organizations.' :
          data.error === 'name_required' ? 'Organization name is required.' :
          data.error || `Failed (${r.status})`
        );
        return;
      }
      const orgId = data.org && data.org.id;
      // Upload KB files (skip ones marked invalid).
      const kbToUpload = kbDrafts.filter(x => !x.error);
      const failures = [];
      for (let i = 0; i < kbToUpload.length; i++) {
        setProgress(`Uploading knowledge base ${i + 1} / ${kbToUpload.length}…`);
        try { await uploadKb(orgId, kbToUpload[i]); }
        catch (e) { failures.push(`${kbToUpload[i].file.name}: ${e.message || e}`); }
      }
      for (let i = 0; i < brandDrafts.length; i++) {
        setProgress(`Uploading brand asset ${i + 1} / ${brandDrafts.length}…`);
        try { await uploadBrand(orgId, brandDrafts[i]); }
        catch (e) { failures.push(`${brandDrafts[i].file.name}: ${e.message || e}`); }
      }
      if (onCreated) onCreated(data.org);
      if (failures.length) {
        // Org exists; surface upload trouble but don't block the close.
        setErr(`Organization created. Some files didn't upload — open Organization settings to retry: ${failures.slice(0, 3).join(' · ')}${failures.length > 3 ? ' …' : ''}`);
        setBusy(false);
        setProgress('');
        return;
      }
      onClose();
    } catch (e) {
      setErr(String(e?.message || e));
    } finally {
      setBusy(false);
      setProgress('');
    }
  };

  // Per-step bodies kept inline — the wizard chrome handles step rail,
  // back/next buttons, and submit.
  const Body = () => {
    if (step === 0) {
      return (
        <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
          <p style={{ margin: 0, fontSize: 13, lineHeight: 1.55, color: 'var(--c-ink-600)', textWrap: 'pretty' }}>
            Create a new tenant. You can pre-load a knowledge base and brand kit
            in the next steps, or add them later from <strong>Organization settings</strong>.
          </p>
          <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
            <label style={amLabel}>Name</label>
            <input type="text" autoFocus value={name} onChange={e => setName(e.target.value)}
              placeholder="e.g. Northwind Logistics" style={amInput} />
          </div>
          <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
            <label style={amLabel}>What they do <span style={{ color: 'var(--c-ink-500)', fontWeight: 400 }}>(optional)</span></label>
            <input type="text" value={kind} onChange={e => setKind(e.target.value)}
              placeholder="e.g. Mid-market freight forwarding" style={amInput} />
          </div>
          <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
            <label style={amLabel}>Company info <span style={{ color: 'var(--c-ink-500)', fontWeight: 400 }}>(optional)</span></label>
            <textarea value={info} onChange={e => setInfo(e.target.value)}
              placeholder="A few paragraphs about who they are. You can edit this later in Organization settings."
              style={amTextarea} />
          </div>
        </div>
      );
    }
    if (step === 1) {
      return (
        <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
          <SectionHeader
            title="Knowledge base"
            sub={`Drop files the org's employees can ground answers in. ${ORG_KB_EXTS.map(e => e.toUpperCase()).join(' · ')} · up to 6 MB each.`} />
          <DropZoneCompact
            label={kbDrafts.length ? 'Add more files' : 'Drop files here, or click to browse'}
            onPick={() => kbInputRef.current && kbInputRef.current.click()}
            onDrop={(files) => queueKbFiles(files)} />
          <input ref={kbInputRef} type="file" multiple style={{ display: 'none' }}
            accept={ORG_KB_EXTS.map(e => '.' + e).join(',')}
            onChange={(e) => { queueKbFiles(e.target.files); e.target.value = ''; }} />
          {kbDrafts.length > 0 && (
            <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
              {kbDrafts.map(it => (
                <DraftFileRow key={it.key} item={it} onRemove={() => removeKbDraft(it.key)} />
              ))}
            </div>
          )}
        </div>
      );
    }
    // step === 2 — brand kit
    return (
      <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
        <SectionHeader
          title="Brand kit"
          sub="Logos, document templates, fonts, palette files. Anything goes." />
        <DropZoneCompact
          label={brandDrafts.length ? 'Add more assets' : 'Drop logos, templates, fonts here'}
          onPick={() => brandInputRef.current && brandInputRef.current.click()}
          onDrop={(files) => queueBrandFiles(files)} />
        <input ref={brandInputRef} type="file" multiple style={{ display: 'none' }}
          onChange={(e) => { queueBrandFiles(e.target.files); e.target.value = ''; }} />
        {brandDrafts.length > 0 && (
          <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
            {brandDrafts.map(it => (
              <DraftFileRow key={it.key} item={it} onRemove={() => removeBrandDraft(it.key)} />
            ))}
          </div>
        )}
      </div>
    );
  };

  const isLast        = step === ORG_STEPS.length - 1;
  const nextDisabled  = step === 0 ? !valid : false;
  const submitDisabled = !valid;

  return (
    <WizardChrome
      title="Add organization"
      eyebrow="Directory"
      steps={ORG_STEPS}
      stepIndex={step}
      maxReached={maxStep}
      onStep={goTo}
      onBack={goBack}
      onNext={goNext}
      onClose={onClose}
      onSubmit={submit}
      nextDisabled={nextDisabled}
      submitDisabled={submitDisabled}
      submitLabel="Create organization"
      busy={busy}
      err={err}
      secondaryNote={progress || (
        isLast
          ? `${kbDrafts.length} knowledge file${kbDrafts.length === 1 ? '' : 's'} · ${brandDrafts.length} brand asset${brandDrafts.length === 1 ? '' : 's'} queued`
          : `Step ${step + 1} of ${ORG_STEPS.length}`
      )}>
      {/* Render Body inline (call) — declaring `<Body />` here would have
          React treat it as a fresh component on every keystroke, unmounting
          and remounting the inputs and snapping focus back to the autoFocus'd
          Name field. Calling it returns the JSX directly, so the inputs
          stay mounted and keep their cursor/selection state. */}
      {Body()}
    </WizardChrome>
  );
}

// Compact dropzone shared between KB and brand-kit upload steps inside
// the AddOrg wizard. Sized to fit the wizard body, not the full settings
// page.
function DropZoneCompact({ label, onPick, onDrop }) {
  const [drag, setDrag] = useS2(false);
  return (
    <div
      onDragOver={(e) => { e.preventDefault(); setDrag(true); }}
      onDragLeave={() => setDrag(false)}
      onDrop={(e) => { e.preventDefault(); setDrag(false); onDrop(e.dataTransfer.files); }}
      onClick={onPick}
      style={{
        border: `2px dashed ${drag ? 'var(--c-petrol-500)' : 'var(--border-subtle)'}`,
        background: drag ? 'var(--c-petrol-050, rgba(23,53,64,0.06))' : 'var(--c-cream-50)',
        borderRadius: 12, padding: '18px 16px',
        display: 'flex', alignItems: 'center', gap: 12, cursor: 'pointer',
      }}>
      <div style={{
        width: 40, height: 40, borderRadius: 10,
        background: 'var(--c-cream-100)', display: 'flex',
        alignItems: 'center', justifyContent: 'center', flex: 'none',
      }}>
        <img src="../assets/icons/plus.svg" width={14} height={14} alt="" />
      </div>
      <div style={{ flex: 1, minWidth: 0 }}>
        <div style={{ fontFamily: "'Suisse Int\\'l', sans-serif", fontWeight: 600, fontSize: 13, color: 'var(--c-ink-900)' }}>
          {label}
        </div>
        <div style={{ fontSize: 11, color: 'var(--c-ink-500)', marginTop: 2 }}>
          Drag from your desktop or click to choose.
        </div>
      </div>
    </div>
  );
}

// Single row for a queued (not-yet-uploaded) attachment.
function DraftFileRow({ item, onRemove }) {
  return (
    <div style={{
      display: 'flex', alignItems: 'center', gap: 10,
      padding: '8px 10px', borderRadius: 10,
      background: 'var(--c-cream-50)',
      border: '1px solid var(--border-subtle)',
    }}>
      <div style={{
        width: 28, height: 28, borderRadius: 6, flex: 'none',
        background: 'var(--c-cream-100)', display: 'flex', alignItems: 'center', justifyContent: 'center',
        fontFamily: "'JetBrains Mono', monospace", fontSize: 9, fontWeight: 700,
        color: 'var(--c-ink-700)',
      }}>{(item.ext || '?').toUpperCase().slice(0, 4)}</div>
      <div style={{ flex: 1, minWidth: 0 }}>
        <div style={{
          fontFamily: "'Suisse Int\\'l', sans-serif", fontSize: 12, fontWeight: 500,
          color: 'var(--c-ink-900)',
          overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
        }}>{item.file.name}</div>
        <div style={{ fontSize: 10, color: item.error ? 'var(--c-terra-600, #C65A42)' : 'var(--c-ink-500)' }}>
          {item.error || orgFileToBytes(item.file.size)}
        </div>
      </div>
      <button type="button" onClick={onRemove} title="Remove" style={{
        width: 26, height: 26, borderRadius: 7,
        background: 'transparent', color: 'var(--c-ink-500)',
        border: '1px solid var(--border-subtle)', cursor: 'pointer',
        display: 'inline-flex', alignItems: 'center', justifyContent: 'center', flex: 'none',
      }}>
        <img src="../assets/icons/cross.svg" width={9} height={9} alt="" />
      </button>
    </div>
  );
}

// ══════════════════════════════════════════════════════════════════════
// AddEmployeeModal — admin / super_admin. Creates an employee inside an
// org, optionally with a soul.md, connector list, and a selection of
// existing skills to attach.
// ══════════════════════════════════════════════════════════════════════
// CONNECTOR_PRESETS is now defined in EmployeeWizard.jsx as window.CONNECTOR_CATALOG.

// ── ReviewStep — summary screen before submit ─────────────────────────
// Renders a read-only digest of every previous step. Each section header
// is clickable so the user can hop back to fix something without losing
// any in-progress data.
function ReviewStep({ name, role, soulMd, identity, connectors, skillEnvCount, kbCount, drafts, skillIds, allSkills, onJumpTo }) {
  const skillName = (id) => {
    const found = (allSkills || []).find(s => s.id === id);
    return found?.name || id;
  };
  const identitySet = !!(identity?.slack_user_token || identity?.slack_d_cookie || identity?.slack_user_id || identity?.avatar_user_id);

  const Row = ({ k, v, mono }) => (
    <div style={{ display: 'flex', gap: 10, fontSize: 12, lineHeight: 1.55 }}>
      <span style={{ minWidth: 110, color: 'var(--c-ink-500)' }}>{k}</span>
      <span style={{
        color: 'var(--c-ink-900)', flex: 1, minWidth: 0, wordBreak: 'break-word',
        fontFamily: mono ? "'JetBrains Mono', monospace" : "'Suisse Int\\'l', sans-serif",
      }}>{v || <em style={{ color: 'var(--c-ink-500)', fontStyle: 'normal' }}>—</em>}</span>
    </div>
  );

  const Card = ({ stepIdx, title, children }) => (
    <div style={{
      border: '1px solid var(--border-subtle)', borderRadius: 12,
      background: 'var(--c-cream-50)', padding: 14,
      display: 'flex', flexDirection: 'column', gap: 8,
    }}>
      <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
        <div style={{
          fontFamily: "'Suisse Int\\'l', sans-serif", fontWeight: 600, fontSize: 13,
          color: 'var(--c-ink-900)', flex: 1,
        }}>{title}</div>
        <button type="button" onClick={() => onJumpTo(stepIdx)} style={{
          height: 24, padding: '0 8px', borderRadius: 6,
          background: 'transparent', color: 'var(--c-petrol-800)',
          border: '1px solid var(--border-subtle)', cursor: 'pointer',
          fontFamily: "'Suisse Int\\'l', sans-serif", fontSize: 11,
        }}>Edit</button>
      </div>
      <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
        {children}
      </div>
    </div>
  );

  const soulPreview = (soulMd || '').trim();
  const soulSummary = soulPreview
    ? (soulPreview.length > 220 ? soulPreview.slice(0, 220) + '…' : soulPreview)
    : '';

  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
      <SectionHeader title="Review" sub="Confirm everything looks right. You can jump back to any step from here." />

      <Card stepIdx={0} title="Basics">
        <Row k="Name" v={name} />
        <Row k="Title" v={role} />
      </Card>

      <Card stepIdx={1} title="Identity">
        {identitySet ? (
          <>
            <Row k="Slack user token" v={identity.slack_user_token ? '••••••' + (identity.slack_user_token.slice(-4)) : ''} mono />
            <Row k="Slack d cookie"   v={identity.slack_d_cookie   ? '••••••' + (identity.slack_d_cookie.slice(-4))   : ''} mono />
            <Row k="Slack user ID"    v={identity.slack_user_id} mono />
            <Row k="Avatar user ID"   v={identity.avatar_user_id} mono />
          </>
        ) : <Row k="Status" v="No identity configured (skipped)." />}
      </Card>

      <Card stepIdx={2} title="soul.md">
        {soulSummary
          ? <pre style={{
              margin: 0, fontFamily: "'JetBrains Mono', monospace", fontSize: 11.5,
              color: 'var(--c-ink-900)', whiteSpace: 'pre-wrap', wordBreak: 'break-word',
            }}>{soulSummary}</pre>
          : <Row k="Status" v="Empty (skipped)." />}
      </Card>

      <Card stepIdx={3} title={`Connectors (${connectors.length})`}>
        {connectors.length === 0
          ? <Row k="Status" v="None enabled." />
          : connectors.map(c => {
              const cat = (window.CONNECTOR_CATALOG || []).find(x => x.id === c.id);
              const ok = isValidJson(c.params);
              return (
                <div key={c.id} style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 12 }}>
                  <span style={{
                    width: 8, height: 8, borderRadius: 999, flex: 'none',
                    background: ok ? 'var(--c-petrol-800)' : 'var(--c-terra-600, #C65A42)',
                  }} />
                  <span style={{ color: 'var(--c-ink-900)', minWidth: 130 }}>{cat?.label || c.id}</span>
                  <span style={{ color: 'var(--c-ink-500)', fontFamily: "'JetBrains Mono', monospace", fontSize: 11 }}>
                    {ok ? 'JSON OK' : 'invalid JSON'}
                  </span>
                </div>
              );
            })}
      </Card>

      <Card stepIdx={4} title={`Skill env (${skillEnvCount || 0})`}>
        <Row k="Variables" v={(skillEnvCount || 0) > 0
          ? `${skillEnvCount} variable${skillEnvCount === 1 ? '' : 's'} configured (values hidden).`
          : 'None.'} />
      </Card>

      <Card stepIdx={5} title={`Knowledge base (${kbCount})`}>
        <Row k="Files" v={kbCount > 0 ? `${kbCount} pending upload${kbCount === 1 ? '' : 's'}` : 'None.'} />
      </Card>

      <Card stepIdx={6} title="Skills">
        <Row k="New" v={drafts.length === 0 ? 'None.' : drafts.map(d => d.name || '(unnamed)').join(', ')} />
        <Row k="Attached" v={skillIds.length === 0 ? 'None.' : skillIds.map(skillName).join(', ')} />
      </Card>
    </div>
  );
}

function AddEmployeeModal({ orgId, orgName, allSkills = [], onClose, onCreated }) {
  const [step, setStep]       = useS2(0);
  const [maxStep, setMaxStep] = useS2(0);
  const [name, setName]       = useS2('');
  const [role, setRole]       = useS2('');
  // No sign-in account is created here — this app is invite-only. Use
  // the topbar's "Add people" flow to create a login when you need one.
  const [soulMd, setSoulMd]   = useS2('');
  const [responseStyle, setResponseStyle] = useS2(window.DEFAULT_RESPONSE_STYLE || '');
  const [voiceInstructions, setVoiceInstructions] = useS2(window.DEFAULT_VOICE_INSTRUCTIONS || '');
  // Slack identity + avatar — stored on the employee doc.
  const [identity, setIdentity] = useS2({
    slack_user_token: '', slack_d_cookie: '',
    slack_user_id: '', avatar_user_id: '',
  });
  // Connectors are now { id, params } objects with per-connector JSON config.
  const [connectors, setConn] = useS2([]);
  // Skill env vars: { key, value }[] in the UI, flat object on the wire.
  const [skillEnv, setSkillEnv] = useS2([]);
  const [skillIds, setSk]     = useS2([]);
  const [skillQ, setSkillQ]   = useS2('');
  // Pending knowledge-base uploads. Each: { id, name, size, type, ext, dataUrl }.
  // Sent to the server after the employee is created.
  const [kbFiles, setKbFiles] = useS2([]);
  // Drafted *new* skills queued for creation after the employee is added.
  // Each: { _key, skill_id, name, desc, inputs: [{name, description}] }
  const [drafts, setDrafts]   = useS2([]);
  const [busy, setBusy] = useS2(false);
  const [err, setErr]   = useS2('');
  // Drag/drop + click upload of `.skill` bundles. Each file is sent to
  // /api/skills/parse to extract its name + description; the result becomes
  // a draft the user can leave alone or open in the existing wizard.
  const skillFileInputRef = useR2(null);
  const [parsing, setParsing] = useS2(false);

  const handleSkillFiles = async (fileList) => {
    if (!fileList || !fileList.length) return;
    setErr('');
    setParsing(true);
    try {
      const files = Array.from(fileList).filter(f => /\.skill$/i.test(f.name));
      if (!files.length) {
        setErr('Pick one or more files ending in .skill.');
        return;
      }
      const results = await Promise.all(files.map(async (f) => {
        const fd = new FormData();
        fd.append('file', f, f.name);
        try {
          const r = await fetch('/api/skills/parse', {
            method: 'POST', headers: { ...authHeaders() }, body: fd,
          });
          const d = await r.json().catch(() => ({}));
          return { file: f, ok: r.ok, data: d };
        } catch (e) {
          return { file: f, ok: false, data: { error: String(e?.message || e) } };
        }
      }));

      const newDrafts = [];
      const failures = [];
      for (const res of results) {
        if (!res.ok) {
          failures.push(`${res.file.name}: ${res.data.error || 'parse failed'}`);
          continue;
        }
        const inferredName = res.data.name || res.file.name.replace(/\.skill$/i, '');
        newDrafts.push({
          _key: `d${Date.now().toString(36)}${Math.random().toString(36).slice(2, 5)}`,
          name: inferredName,
          desc: res.data.description || '',
          // Use whatever the bundle's frontmatter declared. Empty arrays
          // are fine — the wizard's Edit drawer lets the user fill them in.
          required_inputs: Array.isArray(res.data.required_inputs) ? res.data.required_inputs : [],
          // Runtime-facing endpoint specs from the meta-skill frontmatter.
          // Schema-shaped: [{url, method, headers, body, query_params}].
          api_config: Array.isArray(res.data.api_config) ? res.data.api_config : [],
          file: res.file,
        });
      }
      if (newDrafts.length) setDrafts(prev => [...prev, ...newDrafts]);
      if (failures.length) {
        setErr(`Some files couldn't be parsed: ${failures.slice(0, 3).join(' · ')}${failures.length > 3 ? ' …' : ''}`);
      }
    } finally {
      setParsing(false);
      if (skillFileInputRef.current) skillFileInputRef.current.value = '';
    }
  };

  // Derive a slug from the human name, prefixed `custom.` to match the
  // Drawer's create convention.
  const slugify = (s) =>
    'custom.' + (String(s || '').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '').slice(0, 40)
      || `skill-${Date.now().toString(36).slice(-5)}`);

  // Drafts: each is { _key, name, desc, required_inputs, api_config, file (File|null) }.
  // The Drawer wizard fills these in; we just store them until submit.
  const [draftDrawerOpen, setDraftDrawerOpen] = useS2(false);
  const [editingDraftKey, setEditingDraftKey] = useS2(null);

  const editingDraft = drafts.find(d => d._key === editingDraftKey) || null;
  // Drawer's `skill` prop expects a skill-shaped object. When editing an
  // existing draft we synthesize one from the stored payload.
  const editingDraftAsSkill = editingDraft ? {
    id: editingDraft._key,
    name: editingDraft.name,
    desc: editingDraft.desc,
    required_inputs: editingDraft.required_inputs || [],
    api_config: editingDraft.api_config || [],
    file: editingDraft.file
      ? { name: editingDraft.file.name, size: editingDraft.file.size, version: 'pending', updated: new Date().toISOString().slice(0, 10) }
      : null,
  } : null;

  const openNewDraft = () => { setEditingDraftKey(null); setDraftDrawerOpen(true); };
  const openEditDraft = (key) => { setEditingDraftKey(key); setDraftDrawerOpen(true); };
  const removeDraft = (key) => setDrafts(prev => prev.filter(d => d._key !== key));

  // Called when Drawer (in draftMode) saves. Either inserts a new draft
  // or replaces the editing one.
  const onDraftSaved = (payload) => {
    const d = payload._draft;
    if (!d) return;
    setDrafts(prev => {
      if (editingDraftKey) {
        return prev.map(x => x._key === editingDraftKey
          ? { ...x, name: d.name, desc: d.desc, required_inputs: d.required_inputs, api_config: d.api_config || [], file: d.file }
          : x);
      }
      return [...prev, {
        _key: `d${Date.now().toString(36)}${Math.random().toString(36).slice(2, 5)}`,
        name: d.name, desc: d.desc,
        required_inputs: d.required_inputs, api_config: d.api_config || [], file: d.file,
      }];
    });
    setDraftDrawerOpen(false);
    setEditingDraftKey(null);
  };

  const draftsValid = drafts.every(d => d.name && d.name.trim().length >= 2 && d.desc && d.desc.trim().length > 0);
  const basicsValid = name.trim().length >= 2;
  const valid = basicsValid && !!orgId && draftsValid;

  useE2(() => {
    const onKey = (e) => { if (e.key === 'Escape') onClose(); };
    document.addEventListener('keydown', onKey);
    return () => document.removeEventListener('keydown', onKey);
  }, []);

  // Skill catalog filtered by query. We allow attaching any existing skill —
  // the server will append this employee to the skill's owner list.
  const filteredSkills = useM2(() => {
    const s = skillQ.trim().toLowerCase();
    const list = Array.isArray(allSkills) ? allSkills : [];
    if (!s) return list.slice(0, 80);
    return list.filter(sk =>
      (sk.name || '').toLowerCase().includes(s) ||
      (sk.id || '').toLowerCase().includes(s)
    ).slice(0, 80);
  }, [allSkills, skillQ]);

  const toggleSkill = (id) => setSk(prev => prev.includes(id) ? prev.filter(x => x !== id) : [...prev, id]);

  // ── Wizard plumbing ─────────────────────────────────────────────────
  // The Add wizard has 7 steps. `stepValid` gates the Next button per
  // step; we don't gate hard on identity/connectors/KB because they're
  // genuinely optional, but we DO gate Basics (name required).
  const ADD_STEPS = [
    { id: 'basics',     label: 'Basics' },
    { id: 'identity',   label: 'Identity',   optional: true },
    { id: 'soul',       label: 'Prompts',    optional: true },
    { id: 'connectors', label: 'Connectors', optional: true },
    { id: 'env',        label: 'Skill env',  optional: true },
    { id: 'kb',         label: 'Knowledge',  optional: true },
    { id: 'skills',     label: 'Skills',     optional: true },
    { id: 'review',     label: 'Review' },
  ];

  // All connector params must be valid JSON before review/submit.
  const connectorsValid = connectors.every(c => isValidJson(c.params));

  const stepValid = (i) => {
    if (i === 0) return basicsValid;
    if (i === 3) return connectorsValid;
    return true;
  };

  const goNext = () => {
    if (!stepValid(step)) return;
    const next = Math.min(step + 1, ADD_STEPS.length - 1);
    setStep(next);
    setMaxStep(m => Math.max(m, next));
  };
  const goBack = () => setStep(Math.max(0, step - 1));
  const goTo   = (i) => { if (i <= maxStep) setStep(i); };

  const submit = async () => {
    if (!valid || busy) return;
    setBusy(true); setErr('');
    try {
      // Connectors: store id + parsed params (object) for the server. If
      // a user typed unparsable JSON we fall back to the raw string so
      // nothing they typed is silently lost.
      const connectorsPayload = connectors.map(c => {
        let params = {};
        try { params = c.params && c.params.trim() ? JSON.parse(c.params) : {}; }
        catch { params = { __raw: c.params }; }
        return { id: c.id, params };
      });

      // Pin the employee id (UUID v4) up-front. Skill blobs land at
      // skills/<empId>/<skillId>.skill on first write — no rename later.
      // Matches the convention in backend/src/oid.js so cross-system lookups
      // (orchestrator, livekit backend) hit the same key with O(1) cost.
      const empId = (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function')
        ? crypto.randomUUID()
        : (() => {
            // Fallback for ancient browsers without crypto.randomUUID.
            const hex = (n) => Array.from({ length: n }, () =>
              Math.floor(Math.random() * 256).toString(16).padStart(2, '0')).join('');
            const b = hex(16).split('');
            // RFC 4122 v4: set bits for version + variant.
            b[12] = '4';
            b[16] = (parseInt(b[16], 16) & 0x3 | 0x8).toString(16);
            return `${b.slice(0, 8).join('')}-${b.slice(8, 12).join('')}-${b.slice(12, 16).join('')}-${b.slice(16, 20).join('')}-${b.slice(20, 32).join('')}`;
          })();

      // Step 2: create every drafted skill *first* so the employee doc can
      // reference real skill_ids in `owned_skills` when it's inserted. Each
      // skill is uploaded individually — that matches the schema/skills.json
      // contract (one skill_uuid → one Mongo doc + one Azure blob).
      const newSkillIds = [];
      for (let i = 0; i < drafts.length; i++) {
        const d = drafts[i];
        const inputs = (d.required_inputs || [])
          .map(it => ({ name: (it.name || '').trim(), description: (it.description || '').trim() }))
          .filter(it => it.name);
        const fd = new FormData();
        // employee_id pins the blob path; owner_id stays for downstream code
        // that already reads it (the backend patches owner_id post-creation).
        fd.append('employee_id', empId);
        fd.append('skill_id', slugify(d.name));
        fd.append('name', d.name.trim());
        fd.append('desc', d.desc.trim());
        fd.append('owner_id', orgId);
        fd.append('required_inputs', JSON.stringify(inputs));
        // Always send api_config — the runtime needs it whether the user
        // typed it or it was parsed out of the bundle's frontmatter.
        fd.append('api_config', JSON.stringify(Array.isArray(d.api_config) ? d.api_config : []));
        if (d.file) fd.append('file', d.file);
        const sr = await fetch('/api/skills', {
          method: 'POST',
          headers: { ...authHeaders() },
          body: fd,
        });
        const sd = await sr.json().catch(() => ({}));
        if (!sr.ok) {
          setErr(
            `Skill “${d.name}”: ${
              sd.error === 'skill_id_already_exists' ? 'an id like that already exists.' :
              sd.error === 'skill_id_invalid'        ? 'invalid id.' :
              sd.error === 'name_required'           ? 'name is required.' :
              sd.error || `failed (${sr.status})`
            }`
          );
          return;
        }
        if (sd.skill_id) newSkillIds.push(sd.skill_id);
      }

      // Step 3: create the employee. owned_skills carries every skill_id
      // attached during the wizard — both pre-existing (skillIds) and the
      // freshly-uploaded drafts (newSkillIds).
      const r = await fetch(`/api/orgs/${encodeURIComponent(orgId)}/employees`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', ...authHeaders() },
        body: JSON.stringify({
          // Use the same uuid the skills uploads anchored to; the backend
          // adopts it as the employee _id so the blob paths match.
          _id: empId,
          name: name.trim(),
          role: role.trim(),
          soul_md: soulMd,
          response_style: responseStyle,
          voice_instructions: voiceInstructions,
          identity,
          connectors: connectorsPayload,
          skill_env: window.skillEnvRowsToObject(skillEnv),
          skill_ids: [...skillIds, ...newSkillIds],
        }),
      });
      const data = await r.json().catch(() => ({}));
      if (!r.ok) {
        setErr(
          data.error === 'forbidden' ? 'You don’t have permission to add employees here.' :
          data.error === 'wrong_org' ? 'You can only add employees to your own organization.' :
          data.error === 'name_required' ? 'Employee name is required.' :
          data.error === 'name_taken' ? 'An employee with this name already exists in this organization.' :
          data.error === 'org_not_found' ? 'Organization no longer exists.' :
          data.error || `Failed (${r.status})`
        );
        return;
      }

      if (onCreated) onCreated(data.employee);
      onClose();
    } catch (e) {
      setErr(String(e?.message || e));
    } finally {
      setBusy(false);
    }
  };

  return (
    <WizardChrome
      title="Add employee"
      overlay={draftDrawerOpen && window.SkillEditDrawer ? (
        <window.SkillEditDrawer
          open={draftDrawerOpen}
          draftMode={true}
          skill={editingDraftAsSkill}
          ownerId={orgId}
          onSave={onDraftSaved}
          onClose={() => { setDraftDrawerOpen(false); setEditingDraftKey(null); }}
        />
      ) : null}
      eyebrow={orgName || 'Organization'}
      steps={ADD_STEPS}
      stepIndex={step}
      maxReached={maxStep}
      onStep={goTo}
      onBack={goBack}
      onNext={goNext}
      onClose={onClose}
      onSubmit={submit}
      nextDisabled={!stepValid(step)}
      submitDisabled={!valid}
      submitLabel="Add employee"
      busy={busy}
      err={err}>

      {step === 0 && (
        <BasicsStep name={name} role={role} setName={setName} setRole={setRole} />
      )}

      {step === 1 && (
        <IdentityStep identity={identity} setIdentity={setIdentity} />
      )}

      {step === 2 && (
        <SoulStep
          soulMd={soulMd} setSoulMd={setSoulMd}
          responseStyle={responseStyle} setResponseStyle={setResponseStyle}
          voiceInstructions={voiceInstructions} setVoiceInstructions={setVoiceInstructions} />
      )}

      {step === 3 && (
        <ConnectorsStep connectors={connectors} setConnectors={setConn} />
      )}

      {step === 4 && (
        <SkillEnvStep rows={skillEnv} setRows={setSkillEnv} />
      )}

      {step === 5 && (
        <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
          <SectionHeader
            title="Knowledge base"
            sub="Documents this employee can reference at run time. Uploaded after the record is created." />
          <EmployeeKBSection mode="pending" files={kbFiles} onChange={setKbFiles} roleHint={role} />
        </div>
      )}

      {step === 6 && (
        <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
          <SectionHeader
            title="Skills"
            sub="Draft brand-new skills that will be created and owned by this employee, or attach existing skills as a co-owner." />

          <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
            <div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', gap: 8 }}>
              <label style={amLabel}>
                New skills <span style={{ color: 'var(--c-ink-500)', fontWeight: 400 }}>({drafts.length})</span>
              </label>
              <div style={{ display: 'flex', gap: 8 }}>
                <input ref={skillFileInputRef} type="file" accept=".skill" multiple
                  style={{ display: 'none' }}
                  onChange={(ev) => handleSkillFiles(ev.target.files)} />
                <button type="button"
                  onClick={() => skillFileInputRef.current && skillFileInputRef.current.click()}
                  disabled={parsing}
                  title="Upload one or more .skill bundles. Name + description are filled from each file's SKILL.md."
                  style={{
                    height: 28, padding: '0 10px', borderRadius: 8,
                    background: 'var(--c-petrol-800)', color: '#fff',
                    border: 'none', cursor: parsing ? 'wait' : 'pointer',
                    fontFamily: "'Suisse Int\\'l', sans-serif", fontWeight: 500, fontSize: 12,
                    display: 'inline-flex', alignItems: 'center', gap: 6,
                    opacity: parsing ? 0.7 : 1,
                  }}>
                  {parsing ? 'Parsing…' : 'Upload .skill files'}
                </button>
                <button type="button" onClick={openNewDraft} style={{
                  height: 28, padding: '0 10px', borderRadius: 8,
                  background: 'var(--c-cream-50)', color: 'var(--c-petrol-800)',
                  border: '1.5px solid var(--c-petrol-800)', cursor: 'pointer',
                  fontFamily: "'Suisse Int\\'l', sans-serif", fontWeight: 500, fontSize: 12,
                  display: 'inline-flex', alignItems: 'center', gap: 6,
                }}>
                  <img src="../assets/icons/plus.svg" width={10} height={10} alt=""
                    style={{ filter: 'invert(20%) sepia(15%) saturate(800%) hue-rotate(160deg)' }} />
                  Add manually
                </button>
              </div>
            </div>
            <span style={{ fontSize: 11, color: 'var(--c-ink-500)' }}>
              Drop one or more .skill bundles to auto-fill name and description from SKILL.md, or build a draft from scratch.
              Required inputs and API connections aren't read from the bundle — open any draft to add them, or save and edit later.
            </span>
            {drafts.length > 0 && (
              <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
                {drafts.map((d) => (
                  <div key={d._key} style={{
                    border: '1px solid var(--border-subtle)', borderRadius: 12,
                    background: 'var(--c-cream-50)', padding: 12,
                    display: 'flex', alignItems: 'center', gap: 10,
                  }}>
                    <div style={{ flex: 1, minWidth: 0, display: 'flex', flexDirection: 'column', gap: 4 }}>
                      <div style={{
                        fontFamily: "'Suisse Int\\'l', sans-serif", fontWeight: 600, fontSize: 13,
                        color: 'var(--c-ink-900)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
                      }}>{d.name || '(unnamed skill)'}</div>
                      <div style={{
                        fontSize: 11, color: 'var(--c-ink-500)',
                        overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', maxWidth: 460,
                      }}>{d.desc || 'No description.'}</div>
                      <div style={{ display: 'flex', gap: 10, fontSize: 10, color: 'var(--c-ink-500)', fontFamily: "'JetBrains Mono', monospace" }}>
                        <span>{(d.required_inputs || []).length} input{(d.required_inputs || []).length === 1 ? '' : 's'}</span>
                        <span>·</span>
                        <span>{(d.api_config || []).length} endpoint{(d.api_config || []).length === 1 ? '' : 's'}</span>
                        <span>·</span>
                        <span>{d.file ? `file: ${d.file.name}` : 'no file'}</span>
                      </div>
                    </div>
                    <button type="button" onClick={() => openEditDraft(d._key)} style={{
                      height: 28, padding: '0 10px', borderRadius: 8,
                      background: 'transparent', color: 'var(--c-petrol-800)',
                      border: '1px solid var(--border-subtle)', cursor: 'pointer',
                      fontFamily: "'Suisse Int\\'l', sans-serif", fontSize: 11, flex: 'none',
                    }}>Edit</button>
                    <button type="button" onClick={() => removeDraft(d._key)} title="Remove" style={{
                      width: 28, height: 28, borderRadius: 8, flex: 'none',
                      background: 'transparent', color: 'var(--c-terra-600, #C65A42)',
                      border: '1px solid var(--border-subtle)', cursor: 'pointer',
                      display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
                    }}>
                      <img src="../assets/icons/cross.svg" width={10} height={10} alt="Remove" />
                    </button>
                  </div>
                ))}
              </div>
            )}
          </div>

          <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
            <label style={amLabel}>
              Attach existing skills <span style={{ color: 'var(--c-ink-500)', fontWeight: 400 }}>({skillIds.length} selected)</span>
            </label>
            <input type="text" value={skillQ} onChange={e => setSkillQ(e.target.value)}
              placeholder="Search skills" style={{ ...amInput, height: 38 }} />
            <div style={{
              maxHeight: 180, overflow: 'auto', display: 'flex', flexDirection: 'column', gap: 4,
              border: '1px solid var(--border-subtle)', borderRadius: 10,
              background: 'var(--c-cream-50)', padding: 6,
            }}>
              {filteredSkills.length === 0 && (
                <div style={{ padding: 14, textAlign: 'center', fontSize: 12, color: 'var(--c-ink-500)' }}>
                  No skills match.
                </div>
              )}
              {filteredSkills.map(sk => {
                const on = skillIds.includes(sk.id);
                return (
                  <label key={sk.id} style={{
                    display: 'flex', alignItems: 'center', gap: 10, padding: '8px 10px', borderRadius: 8,
                    cursor: 'pointer',
                    background: on ? 'var(--c-cream-100)' : 'transparent',
                  }}>
                    <input type="checkbox" checked={on} onChange={() => toggleSkill(sk.id)}
                      style={{ accentColor: 'var(--c-petrol-800)' }} />
                    <span style={{
                      fontFamily: "'Suisse Int\\'l', sans-serif", fontSize: 13,
                      color: 'var(--c-ink-900)', flex: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
                    }}>{sk.name || sk.id}</span>
                    <span style={{ fontFamily: "'JetBrains Mono', monospace", fontSize: 10, color: 'var(--c-ink-500)' }}>
                      {sk.id}
                    </span>
                  </label>
                );
              })}
            </div>
          </div>
        </div>
      )}

      {step === 7 && (
        <ReviewStep
          name={name} role={role} soulMd={soulMd} identity={identity}
          connectors={connectors} skillEnvCount={skillEnv.filter(r => (r.key || '').trim()).length}
          kbCount={kbFiles.length}
          drafts={drafts} skillIds={skillIds} allSkills={allSkills}
          onJumpTo={goTo} />
      )}
    </WizardChrome>
  );
}

window.AddOrgModal = AddOrgModal;
window.AddEmployeeModal = AddEmployeeModal;

// ══════════════════════════════════════════════════════════════════════
// EditEmployeeModal — admin / super_admin. Lets you rename an employee
// and (the headline reason this exists) manage their knowledge base.
// ══════════════════════════════════════════════════════════════════════
function EditEmployeeModal({ orgId, orgName, employee, allSkills = [], onClose, onSaved }) {
  // ── Wizard step navigation ──────────────────────────────────────────
  const EDIT_STEPS = [
    { id: 'basics',     label: 'Basics' },
    { id: 'skills',     label: 'Skills',     optional: true },
    { id: 'identity',   label: 'Identity',   optional: true },
    { id: 'soul',       label: 'Prompts',    optional: true },
    { id: 'connectors', label: 'Connectors', optional: true },
    { id: 'env',        label: 'Skill env',  optional: true },
    { id: 'kb',         label: 'Knowledge',  optional: true },
  ];
  const [step, setStep]       = useS2(0);
  const [maxStep, setMaxStep] = useS2(EDIT_STEPS.length - 1); // every step is reachable from the start when editing

  const [name, setName] = useS2(employee?.name || '');
  const [role, setRole] = useS2(employee?.role || '');
  const [soulMd, setSoulMd] = useS2(typeof employee?.soul_md === 'string' ? employee.soul_md : '');
  const [responseStyle, setResponseStyle] = useS2(
    typeof employee?.response_style === 'string' ? employee.response_style : (window.DEFAULT_RESPONSE_STYLE || ''));
  const [voiceInstructions, setVoiceInstructions] = useS2(
    typeof employee?.voice_instructions === 'string' ? employee.voice_instructions : (window.DEFAULT_VOICE_INSTRUCTIONS || ''));

  const [identity, setIdentity] = useS2(employee?.identity && typeof employee.identity === 'object' ? {
    slack_user_token: '', slack_d_cookie: '', slack_user_id: '', avatar_user_id: '',
    ...employee.identity,
  } : {
    slack_user_token: '', slack_d_cookie: '', slack_user_id: '', avatar_user_id: '',
  });

  // Connectors are stored as [{id, params}] where params is a JSON STRING
  // (matching the editor surface). Inbound docs may have either string or
  // object params — normalize once on mount and on hydrate.
  const normalizeConnectors = (raw) =>
    (Array.isArray(raw) ? raw : []).map(c => ({
      id: c.id,
      params: typeof c.params === 'string'
        ? c.params
        : window.jsonPretty(c.params || {}),
    }));
  const [connectors, setConn] = useS2(normalizeConnectors(employee?.connectors));

  // Skill env vars: { key, value }[] in the UI, flat object on the wire.
  const [skillEnv, setSkillEnv] = useS2(
    employee?.skill_env && typeof employee.skill_env === 'object'
      ? window.skillEnvObjectToRows(employee.skill_env)
      : []);

  // Owned skills: list of skill_ids the employee carries on their row.
  // Mutations go through the dedicated /skills/adopt + /skills/:id
  // endpoints (live writes, like the KB section); we keep a local
  // mirror here so the UI updates immediately, and flip skillsDirty
  // so the Save button enables for visual feedback even though the
  // backend write already happened.
  const [ownedSkills, setOwnedSkills] = useS2(
    Array.isArray(employee?.skills) ? employee.skills.slice() : []);
  const [skillsDirty, setSkillsDirty] = useS2(false);
  const [skillQ, setSkillQ] = useS2('');
  const [skillBusy, setSkillBusy] = useS2('');  // skill_id currently mutating

  // Bookkeeping for the live-fetch hydrate (the bootstrap shape passed in
  // from the host doesn't always include soul_md / identity / connectors).
  const [hydrated, setHydrated] = useS2(typeof employee?.soul_md === 'string');
  const [origState, setOrigState] = useS2({
    name: employee?.name || '',
    role: employee?.role || '',
    soulMd: typeof employee?.soul_md === 'string' ? employee.soul_md : '',
    responseStyle: typeof employee?.response_style === 'string' ? employee.response_style : (window.DEFAULT_RESPONSE_STYLE || ''),
    voiceInstructions: typeof employee?.voice_instructions === 'string' ? employee.voice_instructions : (window.DEFAULT_VOICE_INSTRUCTIONS || ''),
    identity: JSON.stringify(employee?.identity || {}),
    connectors: JSON.stringify(normalizeConnectors(employee?.connectors)),
    skillEnv: JSON.stringify(employee?.skill_env && typeof employee.skill_env === 'object' ? employee.skill_env : {}),
  });

  const [busy, setBusy] = useS2(false);
  const [err, setErr]   = useS2('');
  // KB writes go through immediately (live mode) but the host modal
  // wants Save enabled on any change so the user gets visual feedback.
  // Flips on every successful add/delete from EmployeeKBSection.
  const [kbDirty, setKbDirty] = useS2(false);

  const connectorsValid = connectors.every(c => isValidJson(c.params));
  const dirty = (name.trim() !== origState.name.trim()) ||
                (role.trim() !== origState.role.trim()) ||
                (soulMd !== origState.soulMd) ||
                (responseStyle !== origState.responseStyle) ||
                (voiceInstructions !== origState.voiceInstructions) ||
                (JSON.stringify(identity) !== origState.identity) ||
                (JSON.stringify(connectors) !== origState.connectors) ||
                (JSON.stringify(window.skillEnvRowsToObject(skillEnv)) !== origState.skillEnv) ||
                kbDirty ||
                skillsDirty;

  // ── Skills: adopt / remove (live writes, like the KB section) ──────
  const adoptSkill = async (skillId) => {
    if (!skillId || ownedSkills.includes(skillId) || skillBusy) return;
    setSkillBusy(skillId);
    try {
      const r = await fetch(
        `/api/orgs/${encodeURIComponent(orgId)}/employees/${encodeURIComponent(employee.id)}/skills/adopt`,
        { method: 'POST', headers: { 'Content-Type': 'application/json', ...authHeaders() },
          body: JSON.stringify({ skill_id: skillId }) },
      );
      const d = await r.json().catch(() => ({}));
      if (r.ok && d.employee) {
        setOwnedSkills(Array.isArray(d.employee.skills) ? d.employee.skills.slice() : ownedSkills);
        setSkillsDirty(true);
      } else {
        setErr(d.error || `Adopt failed (${r.status})`);
      }
    } finally { setSkillBusy(''); }
  };

  const removeSkill = async (skillId) => {
    if (!skillId || !ownedSkills.includes(skillId) || skillBusy) return;
    setSkillBusy(skillId);
    try {
      const r = await fetch(
        `/api/orgs/${encodeURIComponent(orgId)}/employees/${encodeURIComponent(employee.id)}/skills/${encodeURIComponent(skillId)}`,
        { method: 'DELETE', headers: { ...authHeaders() } },
      );
      const d = await r.json().catch(() => ({}));
      if (r.ok && d.employee) {
        setOwnedSkills(Array.isArray(d.employee.skills) ? d.employee.skills.slice() : ownedSkills);
        setSkillsDirty(true);
      } else {
        setErr(d.error || `Remove failed (${r.status})`);
      }
    } finally { setSkillBusy(''); }
  };

  // Filter helper for the "Attach existing" picker. Mirrors the wizard's
  // skill list (Lists.jsx ↔ AddEmployeeModal) so search behaves the same.
  const filteredAvailable = (allSkills || []).filter(s => {
    if (ownedSkills.includes(s.id)) return false;
    if (!skillQ.trim()) return true;
    const q = skillQ.trim().toLowerCase();
    return (s.name || '').toLowerCase().includes(q) || (s.id || '').toLowerCase().includes(q);
  });
  const ownedSkillRecords = ownedSkills.map(id => {
    const found = (allSkills || []).find(s => s.id === id);
    return { id, name: found ? found.name : id };
  });
  const valid = name.trim().length >= 2 && connectorsValid;

  useE2(() => {
    const onKey = (e) => { if (e.key === 'Escape') onClose(); };
    document.addEventListener('keydown', onKey);
    return () => document.removeEventListener('keydown', onKey);
  }, []);

  // Hydrate from the live record — pulls the fields the bootstrap shape
  // doesn't carry (soul.md, identity, connector params).
  useE2(() => {
    if (!employee || !orgId) return;
    if (hydrated) return;
    let cancelled = false;
    (async () => {
      try {
        const r = await fetch(`/api/employees/${encodeURIComponent(orgId)}/${encodeURIComponent(employee.id)}`, {
          headers: { ...authHeaders() },
        });
        const d = await r.json().catch(() => ({}));
        if (cancelled) return;
        if (r.ok && d.employee) {
          const e = d.employee;
          const s = typeof e.soul_md === 'string' ? e.soul_md : '';
          const id = e.identity && typeof e.identity === 'object' ? {
            slack_user_token: '', slack_d_cookie: '', slack_user_id: '', avatar_user_id: '',
            ...e.identity,
          } : { slack_user_token: '', slack_d_cookie: '', slack_user_id: '', avatar_user_id: '' };
          const conn = normalizeConnectors(e.connectors);
          const rs = typeof e.response_style === 'string' ? e.response_style : (window.DEFAULT_RESPONSE_STYLE || '');
          const vi = typeof e.voice_instructions === 'string' ? e.voice_instructions : (window.DEFAULT_VOICE_INSTRUCTIONS || '');
          const envObj = (e.skill_env && typeof e.skill_env === 'object') ? e.skill_env : {};
          const envRows = window.skillEnvObjectToRows(envObj);
          setSoulMd(s);
          setResponseStyle(rs);
          setVoiceInstructions(vi);
          setIdentity(id);
          setConn(conn);
          setSkillEnv(envRows);
          if (Array.isArray(e.skills)) setOwnedSkills(e.skills.slice());
          setOrigState({
            name: e.name || '',
            role: e.role || '',
            soulMd: s,
            responseStyle: rs,
            voiceInstructions: vi,
            identity: JSON.stringify(id),
            connectors: JSON.stringify(conn),
            skillEnv: JSON.stringify(envObj),
          });
        }
        setHydrated(true);
      } catch {
        if (!cancelled) setHydrated(true);
      }
    })();
    return () => { cancelled = true; };
  }, [orgId, employee?.id]);

  const goNext = () => {
    const next = Math.min(step + 1, EDIT_STEPS.length - 1);
    setStep(next);
    setMaxStep(m => Math.max(m, next));
  };
  const goBack = () => setStep(Math.max(0, step - 1));
  const goTo   = (i) => { if (i <= maxStep) setStep(i); };
  const stepValid = (i) => {
    if (i === 0) return name.trim().length >= 2;
    if (i === 4) return connectorsValid;  // shifted from 3 -> 4 by the new Skills step
    return true;
  };

  const save = async () => {
    if (!valid || busy) return;
    if (!dirty) { onClose(); return; }
    setBusy(true); setErr('');
    try {
      // Convert connector params from edited JSON strings back to objects
      // for the wire payload. Unparsable params are passed through under
      // __raw so nothing the user typed is silently dropped.
      const connectorsPayload = connectors.map(c => {
        let params = {};
        try { params = c.params && c.params.trim() ? JSON.parse(c.params) : {}; }
        catch { params = { __raw: c.params }; }
        return { id: c.id, params };
      });
      const r = await fetch(`/api/orgs/${encodeURIComponent(orgId)}/employees/${encodeURIComponent(employee.id)}`, {
        method: 'PATCH',
        headers: { 'Content-Type': 'application/json', ...authHeaders() },
        body: JSON.stringify({
          name: name.trim(), role: role.trim(),
          soul_md: soulMd,
          response_style: responseStyle,
          voice_instructions: voiceInstructions,
          identity, connectors: connectorsPayload,
          skill_env: window.skillEnvRowsToObject(skillEnv),
        }),
      });
      const data = await r.json().catch(() => ({}));
      if (!r.ok) {
        setErr(
          data.error === 'forbidden' ? 'You don’t have permission to edit this employee.' :
          data.error === 'wrong_org' ? 'You can only edit employees in your own organization.' :
          data.error === 'name_taken' ? 'An employee with this name already exists in this organization.' :
          data.error || `Save failed (${r.status})`
        );
        return;
      }
      if (onSaved) onSaved(data.employee);
      onClose();
    } catch (e) {
      setErr(String(e?.message || e));
    } finally {
      setBusy(false);
    }
  };

  if (!employee) return null;

  return (
    <WizardChrome
      title={`Edit · ${employee.name}`}
      eyebrow={`${orgName || 'Organization'} · Edit employee`}
      steps={EDIT_STEPS}
      stepIndex={step}
      maxReached={maxStep}
      onStep={goTo}
      onBack={goBack}
      onNext={goNext}
      onClose={onClose}
      onSubmit={save}
      nextDisabled={!stepValid(step)}
      submitDisabled={!valid || !dirty}
      submitLabel={dirty ? 'Save changes' : 'No changes'}
      busy={busy}
      err={err}>

      {step === 0 && (
        <BasicsStep name={name} role={role} setName={setName} setRole={setRole} />
      )}

      {step === 1 && (
        <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
          <SectionHeader
            title="Skills"
            sub="Skills are shared resources. Adopting one adds it to this employee's owned_skills; removing it does NOT delete the skill itself. Persona resolves per-employee at run time, so two employees can share the same skill and each speaks in their own voice." />

          <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
            <label style={amLabel}>
              Owned skills <span style={{ color: 'var(--c-ink-500)', fontWeight: 400 }}>({ownedSkillRecords.length})</span>
            </label>
            {ownedSkillRecords.length === 0 ? (
              <div style={{ padding: 14, textAlign: 'center', fontSize: 12, color: 'var(--c-ink-500)',
                            border: '1px dashed var(--border-subtle)', borderRadius: 10 }}>
                No skills yet. Pick one below.
              </div>
            ) : (
              <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
                {ownedSkillRecords.map(sk => (
                  <div key={sk.id} style={{
                    display: 'flex', alignItems: 'center', gap: 10, padding: '8px 10px', borderRadius: 8,
                    background: 'var(--c-cream-100)',
                  }}>
                    <span style={{
                      fontFamily: "'Suisse Int\\'l', sans-serif", fontSize: 13,
                      color: 'var(--c-ink-900)', flex: 1, minWidth: 0,
                      overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
                    }}>{sk.name || sk.id}</span>
                    <span style={{ fontFamily: "'JetBrains Mono', monospace", fontSize: 10, color: 'var(--c-ink-500)' }}>
                      {sk.id}
                    </span>
                    <button type="button" onClick={() => removeSkill(sk.id)}
                      disabled={skillBusy === sk.id}
                      title="Remove from this employee (does not delete the skill)"
                      style={{
                        border: 'none', background: 'transparent', cursor: 'pointer',
                        padding: 4, opacity: skillBusy === sk.id ? 0.4 : 1,
                      }}>
                      <img src="../assets/icons/cross.svg" width={10} height={10} alt="Remove" />
                    </button>
                  </div>
                ))}
              </div>
            )}
          </div>

          <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
            <label style={amLabel}>Add an existing skill</label>
            <input type="text" value={skillQ} onChange={e => setSkillQ(e.target.value)}
              placeholder="Search skills"
              style={{ ...amInput, height: 38 }} disabled={!hydrated} />
            <div style={{
              maxHeight: 200, overflow: 'auto', display: 'flex', flexDirection: 'column', gap: 4,
              border: '1px solid var(--border-subtle)', borderRadius: 10,
              background: 'var(--c-cream-50)', padding: 6,
            }}>
              {filteredAvailable.length === 0 && (
                <div style={{ padding: 14, textAlign: 'center', fontSize: 12, color: 'var(--c-ink-500)' }}>
                  {(allSkills || []).length === 0
                    ? 'No skills exist in this org yet.'
                    : 'Every available skill is already owned (or no match).'}
                </div>
              )}
              {filteredAvailable.map(sk => (
                <button type="button" key={sk.id} onClick={() => adoptSkill(sk.id)}
                  disabled={skillBusy === sk.id}
                  style={{
                    display: 'flex', alignItems: 'center', gap: 10, padding: '8px 10px',
                    borderRadius: 8, border: 'none', cursor: 'pointer', textAlign: 'left',
                    background: 'transparent',
                    opacity: skillBusy === sk.id ? 0.4 : 1,
                  }}>
                  <span style={{
                    fontFamily: "'Suisse Int\\'l', sans-serif", fontSize: 13,
                    color: 'var(--c-ink-900)', flex: 1, minWidth: 0,
                    overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
                  }}>{sk.name || sk.id}</span>
                  <span style={{ fontFamily: "'JetBrains Mono', monospace", fontSize: 10, color: 'var(--c-ink-500)' }}>
                    {sk.id}
                  </span>
                </button>
              ))}
            </div>
          </div>
        </div>
      )}

      {step === 2 && (
        <IdentityStep identity={identity} setIdentity={setIdentity} />
      )}

      {step === 3 && (
        <SoulStep
          soulMd={soulMd} setSoulMd={setSoulMd}
          responseStyle={responseStyle} setResponseStyle={setResponseStyle}
          voiceInstructions={voiceInstructions} setVoiceInstructions={setVoiceInstructions}
          disabled={!hydrated} />
      )}

      {step === 4 && (
        <ConnectorsStep connectors={connectors} setConnectors={setConn} />
      )}

      {step === 5 && (
        <SkillEnvStep rows={skillEnv} setRows={setSkillEnv} disabled={!hydrated} />
      )}

      {step === 6 && (
        <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
          <SectionHeader
            title="Knowledge base"
            sub="Documents this employee can reference at run time. Uploads save immediately." />
          <EmployeeKBSection
            mode="live"
            orgId={orgId}
            empId={employee.id}
            roleHint={role}
            onMutate={() => setKbDirty(true)} />
        </div>
      )}
    </WizardChrome>
  );
}

window.EditEmployeeModal = EditEmployeeModal;
