/* @jsxRuntime classic */
/* @jsx React.createElement */
// Orchestra — Project Management module  (Delivery surface)
//
// Self-contained: loaded by index.html as its own <script type="text/babel">,
// exposes window.ProjectsPage, rendered by app.jsx's page switch for the three
// Projects sub-nav pages (pm-board / pm-calendar / pm-releases).
//
// What it provides:
//   • Kanban board — work items, tasks, training, milestones across delivery
//     columns, with drag-and-drop, owners, priorities, blocked flags.
//   • Calendar — month view aggregating card due dates, releases, and scheduled
//     events (training, milestones, deadlines) — everything that "doesn't
//     happen immediately".
//   • Releases — scheduled go-lives with environment, status, and a readiness
//     checklist; surfaced on the calendar.
//
// Integration with the core engine: a card can link to a Run (the workflow
// execution that produced deliverables). The card then shows that run's status
// and the deliverables it generated (resolved from the runs/allDocs props the
// app already loads). Cards can also carry a workflow template to run later.
//
// Approved terminology only: deliverable, owner, review gate, run workflow.
// ─────────────────────────────────────────────────────────────────────────────

(function () {
  const { useState, useEffect, useMemo, useCallback } = React;

  // ── Cross-remount memory ────────────────────────────────────────────────
  // The app sub-nav remounts this component when switching Board/Calendar/
  // Releases. Keep the selected project and calendar month here so context
  // survives the switch (module-scoped, session-only — no storage).
  const _mem = { projectId: null, calYear: null, calMonth: null };

  // ── Local API helper (this file loads before app.jsx; stay self-contained) ─
  function toCamel(s) { return s.replace(/_([a-z])/g, (_, c) => c.toUpperCase()); }
  function toSnake(s) { return s.replace(/([A-Z])/g, '_$1').toLowerCase(); }
  function deepCamel(o) {
    if (Array.isArray(o)) return o.map(deepCamel);
    if (o && typeof o === 'object') {
      const r = {}; for (const k of Object.keys(o)) r[toCamel(k)] = deepCamel(o[k]); return r;
    }
    return o;
  }
  function deepSnake(o) {
    if (Array.isArray(o)) return o.map(deepSnake);
    if (o && typeof o === 'object') {
      const r = {}; for (const k of Object.keys(o)) r[toSnake(k)] = deepSnake(o[k]); return r;
    }
    return o;
  }
  async function api(method, path, body) {
    const opts = { method, headers: { 'Content-Type': 'application/json' } };
    if (body !== undefined) opts.body = JSON.stringify(deepSnake(body));
    const res = await fetch(path, opts);
    if (!res.ok) {
      const t = await res.text().catch(() => '');
      throw new Error(`API ${method} ${path} → ${res.status}: ${t}`);
    }
    if (res.status === 204) return null;
    return deepCamel(await res.json());
  }

  // ── Meta maps ────────────────────────────────────────────────────────────
  const TYPE_META = {
    work_item: { label: 'Work Item', icon: '📋', color: '#6baede' },
    task:      { label: 'Task',      icon: '✓',  color: '#8a96aa' },
    training:  { label: 'Training',  icon: '🎓', color: '#4ade80' },
    milestone: { label: 'Milestone', icon: '◆',  color: '#c8a84a' },
    release:   { label: 'Release',   icon: '🚀', color: '#a78bda' },
  };
  const typeMeta = (t) => TYPE_META[t] || TYPE_META.work_item;

  const PRIORITY_META = {
    urgent: { label: 'Urgent', color: '#e08080' },
    high:   { label: 'High',   color: '#c8a84a' },
    normal: { label: 'Normal', color: '#748090' },
    low:    { label: 'Low',    color: '#5a6b8c' },
  };
  const RELEASE_STATUS = {
    planned:     { label: 'Planned',     color: '#6baede' },
    staged:      { label: 'Staged',      color: '#c8a84a' },
    released:    { label: 'Released',    color: '#4ade80' },
    rolled_back: { label: 'Rolled Back', color: '#e08080' },
  };
  const EVENT_TYPE = {
    training:  { label: 'Training',  color: '#4ade80' },
    milestone: { label: 'Milestone', color: '#c8a84a' },
    review:    { label: 'Review Gate', color: '#6baede' },
    deadline:  { label: 'Deadline',  color: '#e08080' },
    meeting:   { label: 'Meeting',   color: '#a78bda' },
    custom:    { label: 'Event',     color: '#8a96aa' },
  };
  const RUN_STATUS_COLOR = {
    running: '#c8a84a', completed: '#4ade80', failed: '#e08080',
    cancelled: '#748090', pending_human: '#c8a84a', idle: '#6b7a99',
  };
  const RUN_STATUS_LABEL = {
    running: 'In Progress', completed: 'Completed', failed: 'Failed',
    cancelled: 'Stopped', pending_human: 'Awaiting Review', idle: 'Idle',
  };
  const RUN_TERMINAL = new Set(['completed', 'failed', 'cancelled', 'error']);

  // Run-log event + verdict vocabulary (mirrors the app's run timeline).
  const EVENT_META = {
    stage_start: { label: 'Stage started', color: '#c8a84a' },
    generating: { label: 'Generating', color: '#4c8ab5' },
    stage_done: { label: 'Stage done', color: '#4ade80' },
    review_start: { label: 'Review started', color: '#6baede' },
    review_done: { label: 'Review complete', color: '#4ade80' },
    revise: { label: 'Revising', color: '#c8a84a' },
    revised: { label: 'Revised', color: '#c8a84a' },
    rework: { label: 'Rework requested', color: '#e08080' },
    human_gate: { label: 'Human checkpoint', color: '#c8a84a' },
    advisory: { label: 'Advisory note', color: '#4c8ab5' },
    workflow_done: { label: 'Workflow complete', color: '#4ade80' },
    error: { label: 'Error', color: '#e08080' },
  };
  const VERDICT_LABEL = {
    approve: 'Approved', approved: 'Approved', accept_with_notes: 'Accepted with notes',
    advisory: 'Advisory', rework: 'Rework', rework_earlier: 'Rework (upstream)',
    pending: 'Pending', escalate: 'Escalated',
  };
  const VERDICT_COLOR = {
    approve: '#4ade80', approved: '#4ade80', accept_with_notes: '#4ade80', advisory: '#4c8ab5',
    rework: '#e08080', rework_earlier: '#e08080', pending: '#c8a84a', escalate: '#e0a060',
  };

  // Summarise review activity from a run log for compact display on a card.
  function reviewSummary(log) {
    let approvals = 0, reworks = 0, pendingGate = false;
    (log || []).forEach(e => {
      const t = e.event || e.type, v = e.verdict || '';
      if (t === 'rework' || v === 'rework' || v === 'rework_earlier') reworks++;
      else if (t === 'review_done') approvals++;
      if (t === 'human_gate' && v === 'pending') pendingGate = true;
    });
    return { approvals, reworks, pendingGate };
  }
  function fmtTime(iso) {
    if (!iso) return '';
    const d = new Date(iso);
    if (isNaN(d)) return '';
    return String(d.getHours()).padStart(2, '0') + ':' + String(d.getMinutes()).padStart(2, '0');
  }

  // ── Date helpers (browser-local; matches stored YYYY-MM-DD) ───────────────
  const WEEKDAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
  const MONTHS = ['January', 'February', 'March', 'April', 'May', 'June', 'July',
                  'August', 'September', 'October', 'November', 'December'];

  function isoOf(dt) {
    const y = dt.getFullYear();
    const m = String(dt.getMonth() + 1).padStart(2, '0');
    const d = String(dt.getDate()).padStart(2, '0');
    return `${y}-${m}-${d}`;
  }
  function todayIso() { return isoOf(new Date()); }
  function addDaysIso(iso, n) {
    if (!iso) return '';
    const p = String(iso).slice(0, 10).split('-');
    const d = new Date(Number(p[0]), Number(p[1]) - 1, Number(p[2]));
    d.setDate(d.getDate() + (Number(n) || 0));
    return isoOf(d);
  }
  // Post-release activity kinds → calendar event_type.
  const POST_KIND = {
    hypercare: { label: 'Hypercare', evt: 'milestone' },
    training: { label: 'Training', evt: 'training' },
    handover: { label: 'Handover / KT', evt: 'meeting' },
    review: { label: 'Review', evt: 'review' },
    milestone: { label: 'Milestone', evt: 'milestone' },
    retro: { label: 'Retro', evt: 'meeting' },
  };
  const DEFAULT_POST_PLAN = [
    { label: 'Hypercare window opens', kind: 'hypercare', offsetDays: 0, done: false, scheduled: false },
    { label: 'End-user training session', kind: 'training', offsetDays: 1, done: false, scheduled: false },
    { label: 'Admin knowledge transfer & runbook handover', kind: 'handover', offsetDays: 3, done: false, scheduled: false },
    { label: 'Hypercare window closes', kind: 'hypercare', offsetDays: 14, done: false, scheduled: false },
    { label: 'Post-release retrospective', kind: 'retro', offsetDays: 14, done: false, scheduled: false },
  ];
  function fmtDate(iso) {
    if (!iso) return '';
    const parts = String(iso).slice(0, 10).split('-');
    if (parts.length !== 3) return iso;
    const dt = new Date(Number(parts[0]), Number(parts[1]) - 1, Number(parts[2]));
    return `${dt.getDate()} ${MONTHS[dt.getMonth()].slice(0, 3)}`;
  }
  function daysUntil(iso) {
    if (!iso) return null;
    const t = new Date(todayIso()).getTime();
    const d = new Date(String(iso).slice(0, 10)).getTime();
    return Math.round((d - t) / 86400000);
  }
  function monthMatrix(year, month) {
    const first = new Date(year, month, 1);
    const start = new Date(year, month, 1 - first.getDay());
    const weeks = [];
    const cur = new Date(start);
    for (let w = 0; w < 6; w++) {
      const week = [];
      for (let d = 0; d < 7; d++) { week.push(new Date(cur)); cur.setDate(cur.getDate() + 1); }
      weeks.push(week);
    }
    return weeks;
  }

  // ── Small UI atoms ─────────────────────────────────────────────────────────
  function Pill({ label, color }) {
    return (
      <span style={{
        display: 'inline-flex', alignItems: 'center', padding: '2px 8px',
        borderRadius: 5, fontSize: '0.7rem', fontWeight: 600, letterSpacing: '0.02em',
        background: (color || '#748090') + '22', color: color || '#748090',
      }}>{label}</span>
    );
  }

  function OwnerAvatar({ role, agents, size = 20 }) {
    if (!role) return null;
    const a = (agents || []).find(x => x.role === role);
    const color = (a && a.color) || '#3d4e6a';
    const initials = (a && a.initials) ||
      role.split(' ').map(w => w[0]).join('').slice(0, 3).toUpperCase();
    return (
      <span title={role} style={{
        width: size, height: size, borderRadius: '50%', background: color,
        display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
        fontSize: size <= 20 ? '0.55rem' : '0.62rem', fontWeight: 700, color: '#fff',
        flexShrink: 0,
      }}>{initials}</span>
    );
  }

  function Field({ label, children }) {
    return (
      <div style={{ marginBottom: 12 }}>
        <label>{label}</label>
        {children}
      </div>
    );
  }

  function Modal({ title, subtitle, onClose, children, footer, wide }) {
    return (
      <div className="modal-overlay" onClick={(e) => e.target === e.currentTarget && onClose()}>
        <div className="modal" style={wide ? { maxWidth: 760 } : {}}>
          <div className="modal-head">
            <div>
              <div style={{ fontWeight: 600, fontSize: '0.95rem' }}>{title}</div>
              {subtitle && <div className="text-muted" style={{ marginTop: 2 }}>{subtitle}</div>}
            </div>
            <button className="btn btn-ghost btn-sm" onClick={onClose}>✕</button>
          </div>
          <div className="modal-body">{children}</div>
          {footer && (
            <div style={{ padding: '14px 20px', borderTop: '1px solid #1b2640',
                          display: 'flex', justifyContent: 'flex-end', gap: 8, flexWrap: 'wrap' }}>
              {footer}
            </div>
          )}
        </div>
      </div>
    );
  }

  // Type-to-confirm delete. Projects (which cascade-delete their cards,
  // releases, and events) require typing the project name; everything else
  // requires typing "delete". Nothing is removed until the word matches.
  function ConfirmDelete({ kind, name, onConfirm, onClose }) {
    const [text, setText] = useState('');
    const word = kind === 'project' ? (name || 'project') : 'delete';
    const ok = text.trim().toLowerCase() === word.toLowerCase();
    const detail = kind === 'project'
      ? <span>This permanently deletes <b>{name || 'this project'}</b> and <b>all</b> of its work items, releases, and events. This cannot be undone.</span>
      : <span>This permanently deletes this {kind}{name ? <span> (<b>{name}</b>)</span> : null}. This cannot be undone.</span>;
    return (
      <Modal title={`Delete ${kind}?`} onClose={onClose} footer={[
        <button key="c" className="btn btn-outline btn-sm" onClick={onClose}>Cancel</button>,
        <button key="d" className="btn btn-danger btn-sm" disabled={!ok} onClick={onConfirm}>Delete</button>,
      ]}>
        <div className="text-muted" style={{ marginBottom: 14, lineHeight: 1.6 }}>{detail}</div>
        <Field label={`Type ${kind === 'project' ? `the project name to confirm` : `"${word}" to confirm`}`}>
          <input style={{ width: '100%' }} value={text} autoFocus placeholder={word}
            onChange={(e) => setText(e.target.value)}
            onKeyDown={(e) => { if (e.key === 'Enter' && ok) onConfirm(); }} />
        </Field>
      </Modal>
    );
  }

  // Lightweight deliverable viewer — shows a generated document's content with
  // download + a jump to the full Documents page.
  function DocViewer({ doc, navigateTo, onClose }) {
    const title = doc.title || doc.deliverableName || 'Deliverable';
    function download() {
      const blob = new Blob([doc.content || ''], { type: 'text/markdown' });
      const url = URL.createObjectURL(blob);
      const a = document.createElement('a');
      a.href = url;
      a.download = title.replace(/[^a-z0-9]/gi, '_').toLowerCase() + '.md';
      a.click();
      URL.revokeObjectURL(url);
    }
    return (
      <Modal wide title={title} subtitle={doc.docType || 'Document'} onClose={onClose} footer={[
        <button key="dl" className="btn btn-outline btn-sm" onClick={download}>Download .md</button>,
        navigateTo && <button key="d" className="btn btn-ghost btn-sm" onClick={() => { navigateTo('documents'); onClose(); }}>Open in Documents →</button>,
        <button key="c" className="btn btn-gold btn-sm" onClick={onClose}>Close</button>,
      ]}>
        <pre style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-word', fontSize: '0.82rem',
          lineHeight: 1.6, color: '#c8c4bc', fontFamily: 'inherit', margin: 0 }}>
          {doc.content || '(no content)'}
        </pre>
      </Modal>
    );
  }

  // ════════════════════════════════════════════════════════════════════════
  //  Main component
  // ════════════════════════════════════════════════════════════════════════
  function ProjectsPage({ view, runs, allDocs, workflows, agents, navigateTo, onRunCard }) {
    const [projects, setProjects] = useState([]);
    const [activeId, setActiveId] = useState(_mem.projectId);
    const [board, setBoard] = useState(null);     // {project, columns, cards, releases, events, cardTypes}
    const [loading, setLoading] = useState(true);
    const [error, setError] = useState(null);

    const [cardModal, setCardModal] = useState(null);     // card object or { _new:true, status, cardType }
    const [releaseModal, setReleaseModal] = useState(null);
    const [eventModal, setEventModal] = useState(null);
    const [projectModal, setProjectModal] = useState(null);
    const [confirmDel, setConfirmDel] = useState(null);   // { kind, id, name }
    const [docView, setDocView] = useState(null);         // a deliverable to view

    const now = new Date();
    const [calYear, setCalYear] = useState(_mem.calYear == null ? now.getFullYear() : _mem.calYear);
    const [calMonth, setCalMonth] = useState(_mem.calMonth == null ? now.getMonth() : _mem.calMonth);
    useEffect(() => { _mem.calYear = calYear; _mem.calMonth = calMonth; }, [calYear, calMonth]);

    const runsById = useMemo(() => {
      const m = {}; (runs || []).forEach(r => { m[r.id] = r; }); return m;
    }, [runs]);
    const docsByRun = useMemo(() => {
      const m = {}; (allDocs || []).forEach(d => { (m[d.runId] = m[d.runId] || []).push(d); }); return m;
    }, [allDocs]);

    // ── Loaders ──
    const loadProjects = useCallback(async () => {
      setLoading(true); setError(null);
      try {
        const list = await api('GET', '/api/pm/projects');
        setProjects(list);
        let pick = _mem.projectId && list.find(p => p.id === _mem.projectId) ? _mem.projectId : null;
        if (!pick && list.length) pick = list[0].id;
        _mem.projectId = pick;
        setActiveId(pick);
      } catch (e) { setError(String(e.message || e)); }
      finally { setLoading(false); }
    }, []);

    const loadBoard = useCallback(async (id) => {
      if (!id) { setBoard(null); return; }
      try {
        const b = await api('GET', `/api/pm/projects/${id}/board`);
        setBoard(b);
      } catch (e) { setError(String(e.message || e)); }
    }, []);

    useEffect(() => { loadProjects(); }, [loadProjects]);
    useEffect(() => { if (activeId) loadBoard(activeId); }, [activeId, loadBoard]);

    function selectProject(id) { _mem.projectId = id; setActiveId(id); }
    const reload = () => loadBoard(activeId);

    // ── Mutations ──
    async function saveCard(data) {
      try {
        if (data.id) await api('PATCH', `/api/pm/cards/${data.id}`, data);
        else await api('POST', '/api/pm/cards', { ...data, projectId: activeId });
        setCardModal(null); await reload();
      } catch (e) { alert('Could not save card: ' + e.message); }
    }
    // Run a card's workflow through Orchestra, then link the run back to it.
    // Persists any in-modal edits first so the run uses the latest request.
    async function runCardFromModal(f) {
      if (!f.workflowId) { alert('Choose a workflow for this work item first.'); return; }
      let cardId = f.id;
      try {
        if (f.id) await api('PATCH', `/api/pm/cards/${f.id}`, f);
        else { const c = await api('POST', '/api/pm/cards', { ...f, projectId: activeId }); cardId = c.id; }
      } catch (e) { alert('Could not save the card before running: ' + e.message); return; }
      setCardModal(null);
      onRunCard && onRunCard({ id: cardId, title: f.title, description: f.description, workflowId: f.workflowId });
    }
    async function moveCard(card, status) {
      if (card.status === status) return;
      setBoard(b => b && ({ ...b, cards: b.cards.map(c => c.id === card.id ? { ...c, status } : c) }));
      try { await api('PATCH', `/api/pm/cards/${card.id}`, { status }); await reload(); }
      catch (e) { alert('Could not move card: ' + e.message); await reload(); }
    }
    // Drag reorder (within or across columns). `optimistic` is the full card list
    // with new statuses/order applied; we render it immediately then persist.
    async function reorderCards(updates, optimistic) {
      if (optimistic) setBoard(b => b && ({ ...b, cards: optimistic }));
      try { await api('POST', '/api/pm/cards/reorder', { updates }); await reload(); }
      catch (e) { alert('Could not reorder: ' + e.message); await reload(); }
    }
    async function saveRelease(data) {
      try {
        if (data.id) await api('PATCH', `/api/pm/releases/${data.id}`, data);
        else await api('POST', '/api/pm/releases', { ...data, projectId: activeId });
        setReleaseModal(null); await reload();
      } catch (e) { alert('Could not save release: ' + e.message); }
    }
    async function saveEvent(data) {
      try {
        if (data.id) await api('PATCH', `/api/pm/events/${data.id}`, data);
        else await api('POST', '/api/pm/events', { ...data, projectId: activeId });
        setEventModal(null); await reload();
      } catch (e) { alert('Could not save event: ' + e.message); }
    }
    async function saveProject(data) {
      try {
        if (data.id) { await api('PATCH', `/api/pm/projects/${data.id}`, data); }
        else {
          const p = await api('POST', '/api/pm/projects', data);
          _mem.projectId = p.id;
        }
        setProjectModal(null);
        await loadProjects();
        if (data.id) await reload();
      } catch (e) { alert('Could not save project: ' + e.message); }
    }

    // ── Hard-confirm deletes (type-to-confirm) ──
    // requestDelete opens a confirmation; performDelete runs the real API call.
    function requestDelete(kind, id, name) { setConfirmDel({ kind, id, name }); }
    async function performDelete() {
      const { kind, id } = confirmDel;
      try {
        await api('DELETE', `/api/pm/${kind === 'project' ? 'projects' : kind + 's'}/${id}`);
        setConfirmDel(null);
        setCardModal(null); setReleaseModal(null); setEventModal(null); setProjectModal(null);
        if (kind === 'project') {
          if (_mem.projectId === id) _mem.projectId = null;
          setActiveId(null);
          await loadProjects();
        } else {
          await reload();
        }
      } catch (e) { alert('Could not delete: ' + e.message); }
    }

    // ── Render: loading / error / empty ──
    if (loading) return <div className="page"><div className="empty-state">Loading projects…</div></div>;
    if (error) return (
      <div className="page">
        <div className="card" style={{ borderColor: 'rgba(224,128,128,0.3)' }}>
          <div className="text-red" style={{ fontWeight: 600, marginBottom: 6 }}>Couldn’t load Project Management</div>
          <div className="text-muted" style={{ marginBottom: 12 }}>{error}</div>
          <button className="btn btn-outline btn-sm" onClick={loadProjects}>Retry</button>
        </div>
      </div>
    );
    if (!projects.length) return (
      <div className="page">
        <div className="empty-state">
          <div style={{ fontSize: '1rem', color: '#8a96aa', marginBottom: 10 }}>No projects yet</div>
          <button className="btn btn-gold btn-sm" onClick={() => setProjectModal({ _new: true })}>+ New Project</button>
        </div>
        {projectModal && <ProjectModal initial={projectModal} onSave={saveProject} onClose={() => setProjectModal(null)} />}
      </div>
    );

    const project = board && board.project;
    const cards = (board && board.cards) || [];
    const columns = (board && board.columns) || [];
    const releases = (board && board.releases) || [];
    const events = (board && board.events) || [];

    const viewLabel = view === 'pm-calendar' ? 'Calendar' : view === 'pm-releases' ? 'Releases' : 'Board';

    return (
      <div className="page">
        {/* ── Header: project selector + view action ── */}
        <div className="row-between" style={{ marginBottom: 18, flexWrap: 'wrap', gap: 10 }}>
          <div className="row" style={{ gap: 12, flexWrap: 'wrap' }}>
            <select value={activeId || ''} onChange={(e) => selectProject(e.target.value)}
              style={{ fontSize: '0.95rem', fontWeight: 600, minWidth: 220 }}>
              {projects.map(p => <option key={p.id} value={p.id}>{p.name}</option>)}
            </select>
            {project && <Pill label={(project.status || 'active').replace(/_/g, ' ')}
              color={project.status === 'complete' ? '#4ade80' : project.status === 'on_hold' ? '#c8a84a' : '#6baede'} />}
            {project && project.targetDate && (
              <span className="text-xs">Target {fmtDate(project.targetDate)}</span>
            )}
          </div>
          <div className="row" style={{ gap: 8 }}>
            <button className="btn btn-ghost btn-sm" title="Edit project"
              onClick={() => setProjectModal({ ...project })}>⚙</button>
            <button className="btn btn-outline btn-sm" onClick={() => setProjectModal({ _new: true })}>+ Project</button>
            {view === 'pm-board' && <button className="btn btn-gold btn-sm" onClick={() => setCardModal({ _new: true, status: 'backlog' })}>+ Work Item</button>}
            {view === 'pm-releases' && <button className="btn btn-gold btn-sm" onClick={() => setReleaseModal({ _new: true })}>+ Release</button>}
            {view === 'pm-calendar' && <button className="btn btn-gold btn-sm" onClick={() => setEventModal({ _new: true, startDate: todayIso() })}>+ Event</button>}
          </div>
        </div>

        {/* Work items are units of delivery work fed into Orchestra workflows. */}
        {view === 'pm-board' && (
          <Board columns={columns} cards={cards} agents={agents} runsById={runsById} docsByRun={docsByRun}
            workflows={workflows} onRunCard={onRunCard} onOpenDoc={(d) => setDocView(d)}
            onReorder={reorderCards} onOpen={(c) => setCardModal(c)} onAdd={(status) => setCardModal({ _new: true, status })} />
        )}
        {view === 'pm-calendar' && (
          <Calendar year={calYear} month={calMonth} cards={cards} releases={releases} events={events}
            setYear={setCalYear} setMonth={setCalMonth}
            onOpenCard={(c) => setCardModal(c)} onOpenRelease={(r) => setReleaseModal(r)}
            onOpenEvent={(e) => setEventModal(e)} onAddOnDay={(iso) => setEventModal({ _new: true, startDate: iso })} />
        )}
        {view === 'pm-releases' && (
          <Releases releases={releases} cards={cards} agents={agents}
            onOpen={(r) => setReleaseModal(r)} onSave={saveRelease}
            onNew={() => setReleaseModal({ _new: true })} />
        )}
        {view === 'pm-portfolio' && (
          <Portfolio navigateTo={navigateTo}
            onOpenProject={(id) => { selectProject(id); navigateTo('pm-board'); }} />
        )}

        {/* ── Modals ── */}
        {cardModal && (
          <CardModal initial={cardModal} columns={columns} cardTypes={(board && board.cardTypes) || []}
            agents={agents} workflows={workflows} runs={runs} releases={releases}
            runsById={runsById} docsByRun={docsByRun} navigateTo={navigateTo}
            onSave={saveCard} onRun={runCardFromModal} onOpenDoc={(d) => setDocView(d)}
            onDelete={() => requestDelete('card', cardModal.id, cardModal.title)}
            onClose={() => setCardModal(null)} />
        )}
        {releaseModal && (
          <ReleaseModal initial={releaseModal} cards={cards} agents={agents}
            onSave={saveRelease}
            onDelete={() => requestDelete('release', releaseModal.id, releaseModal.name)}
            onClose={() => setReleaseModal(null)} />
        )}
        {eventModal && (
          <EventModal initial={eventModal} cards={cards} releases={releases}
            onSave={saveEvent}
            onDelete={() => requestDelete('event', eventModal.id, eventModal.title)}
            onClose={() => setEventModal(null)} />
        )}
        {projectModal && (
          <ProjectModal initial={projectModal} onSave={saveProject}
            onDelete={() => requestDelete('project', projectModal.id, projectModal.name)}
            onClose={() => setProjectModal(null)} />
        )}
        {confirmDel && (
          <ConfirmDelete kind={confirmDel.kind} name={confirmDel.name}
            onConfirm={performDelete} onClose={() => setConfirmDel(null)} />
        )}
        {docView && (
          <DocViewer doc={docView} navigateTo={navigateTo} onClose={() => setDocView(null)} />
        )}
      </div>
    );
  }

  // ════════════════════════════════════════════════════════════════════════
  //  Kanban board
  // ════════════════════════════════════════════════════════════════════════
  function StatChip({ label, n, color }) {
    return (
      <span style={{ display: 'inline-flex', alignItems: 'center', gap: 6, padding: '4px 11px',
        borderRadius: 8, background: '#0d1220', border: '1px solid #1e2d4a' }}>
        <span style={{ fontWeight: 700, color, fontSize: '0.9rem' }}>{n}</span>
        <span className="text-xs">{label}</span>
      </span>
    );
  }
  function ToggleChip({ label, active, onClick, color }) {
    return (
      <button onClick={onClick} style={{
        padding: '5px 12px', borderRadius: 20, fontSize: '0.76rem', cursor: 'pointer',
        border: '1px solid ' + (active ? (color || '#c8a84a') : '#273558'),
        background: active ? (color || '#c8a84a') + '22' : 'transparent',
        color: active ? (color || '#c8a84a') : '#748090',
      }}>{label}</button>
    );
  }

  function Board({ columns, cards, agents, runsById, docsByRun, workflows, onRunCard, onOpenDoc, onReorder, onOpen, onAdd }) {
    const [dragId, setDragId] = useState(null);
    const [over, setOver] = useState(null);   // { col, before }  — before=null means append
    const [search, setSearch] = useState('');
    const [fType, setFType] = useState('');
    const [fPriority, setFPriority] = useState('');
    const [fOwner, setFOwner] = useState('');
    const [fBlocked, setFBlocked] = useState(false);
    const [fOverdue, setFOverdue] = useState(false);

    const owners = useMemo(() =>
      Array.from(new Set(cards.map(c => c.ownerRole).filter(Boolean))).sort(), [cards]);

    const q = search.toLowerCase();
    const anyFilter = !!(fType || fPriority || fOwner || fBlocked || fOverdue || q);
    function matches(c) {
      if (fType && c.cardType !== fType) return false;
      if (fPriority && c.priority !== fPriority) return false;
      if (fOwner && c.ownerRole !== fOwner) return false;
      if (fBlocked && !c.blocked) return false;
      if (fOverdue) { const du = c.dueDate ? daysUntil(c.dueDate) : null; if (!(du != null && du < 0 && c.status !== 'done')) return false; }
      if (q && !(((c.title || '') + ' ' + (c.description || '')).toLowerCase().includes(q))) return false;
      return true;
    }
    function clearFilters() { setSearch(''); setFType(''); setFPriority(''); setFOwner(''); setFBlocked(false); setFOverdue(false); }

    const sum = {
      total: cards.length,
      inProgress: cards.filter(c => c.status === 'in_progress').length,
      blocked: cards.filter(c => c.blocked).length,
      overdue: cards.filter(c => c.dueDate && daysUntil(c.dueDate) < 0 && c.status !== 'done').length,
      done: cards.filter(c => c.status === 'done').length,
    };

    function computeReorder(colId, beforeId) {
      const dragged = cards.find(c => c.id === dragId);
      if (!dragged) return null;
      const col = cards.filter(c => c.status === colId && c.id !== dragId)
        .sort((a, b) => (a.orderIndex || 0) - (b.orderIndex || 0));
      const idx = beforeId ? col.findIndex(c => c.id === beforeId) : -1;
      const ins = { ...dragged, status: colId };
      if (idx < 0) col.push(ins); else col.splice(idx, 0, ins);
      const updates = col.map((c, i) => ({ id: c.id, status: colId, orderIndex: i * 10 }));
      const byId = {}; updates.forEach(u => { byId[u.id] = u; });
      const optimistic = cards.map(c => byId[c.id]
        ? { ...c, status: colId, orderIndex: byId[c.id].orderIndex } : c);
      return { updates, optimistic };
    }
    function commit(colId) {
      const beforeId = over && over.col === colId ? over.before : null;
      const r = computeReorder(colId, beforeId);
      setDragId(null); setOver(null);
      if (r) onReorder(r.updates, r.optimistic);
    }

    return (
      <div>
        <div className="row" style={{ gap: 8, marginBottom: 12, flexWrap: 'wrap' }}>
          <StatChip label="Total" n={sum.total} color="#8a96aa" />
          <StatChip label="In Progress" n={sum.inProgress} color="#c8a84a" />
          <StatChip label="Blocked" n={sum.blocked} color="#e08080" />
          <StatChip label="Overdue" n={sum.overdue} color="#e08080" />
          <StatChip label="Done" n={sum.done} color="#4ade80" />
        </div>

        <div className="row" style={{ gap: 8, marginBottom: 14, flexWrap: 'wrap' }}>
          <input style={{ width: 200 }} placeholder="Search cards…" value={search} onChange={e => setSearch(e.target.value)} />
          <select value={fType} onChange={e => setFType(e.target.value)}>
            <option value="">All types</option>
            {Object.keys(TYPE_META).map(t => <option key={t} value={t}>{typeMeta(t).label}</option>)}
          </select>
          <select value={fPriority} onChange={e => setFPriority(e.target.value)}>
            <option value="">Any priority</option>
            {Object.keys(PRIORITY_META).map(p => <option key={p} value={p}>{PRIORITY_META[p].label}</option>)}
          </select>
          {owners.length > 0 && (
            <select value={fOwner} onChange={e => setFOwner(e.target.value)}>
              <option value="">Any owner</option>
              {owners.map(o => <option key={o} value={o}>{o}</option>)}
            </select>
          )}
          <ToggleChip label="⛔ Blocked" active={fBlocked} onClick={() => setFBlocked(v => !v)} color="#e08080" />
          <ToggleChip label="Overdue" active={fOverdue} onClick={() => setFOverdue(v => !v)} color="#e08080" />
          {anyFilter && <button className="btn btn-ghost btn-sm" onClick={clearFilters}>✕ Clear</button>}
        </div>

        <div style={{ display: 'flex', gap: 14, overflowX: 'auto', alignItems: 'flex-start', paddingBottom: 10 }}>
          {columns.map(col => {
            const all = cards.filter(c => c.status === col.id).sort((a, b) => (a.orderIndex || 0) - (b.orderIndex || 0));
            const colCards = all.filter(matches);
            const isOverCol = over && over.col === col.id;
            return (
              <div key={col.id}
                onDragOver={(e) => { e.preventDefault(); setOver(o => (o && o.col === col.id) ? o : { col: col.id, before: null }); }}
                onDrop={(e) => { e.preventDefault(); commit(col.id); }}
                style={{
                  width: 270, minWidth: 270, background: '#0d1220',
                  border: '1px solid ' + (isOverCol && dragId ? '#c8a84a' : '#1e2d4a'),
                  borderRadius: 12, padding: 10, transition: 'border-color 0.15s',
                  display: 'flex', flexDirection: 'column', maxHeight: 'calc(100vh - 280px)',
                }}>
                <div className="row-between" style={{ marginBottom: 8, padding: '2px 4px' }}>
                  <span style={{ fontSize: '0.78rem', fontWeight: 600, letterSpacing: '0.04em', textTransform: 'uppercase', color: '#8a96aa' }}>
                    {col.name}
                  </span>
                  <span className="text-xs" style={{ background: '#172038', borderRadius: 10, padding: '1px 8px' }}>
                    {anyFilter ? `${colCards.length}/${all.length}` : all.length}
                  </span>
                </div>
                <div style={{ overflowY: 'auto', display: 'flex', flexDirection: 'column', gap: 8, flex: 1, minHeight: 24 }}>
                  {colCards.map(card => (
                    <CardTile key={card.id} card={card} agents={agents}
                      run={card.runId ? runsById[card.runId] : null}
                      docs={docsByRun[card.runId] || []} onOpenDoc={onOpenDoc}
                      onOpen={() => onOpen(card)}
                      onRun={onRunCard && card.workflowId ? () => onRunCard(card) : null}
                      dragging={dragId === card.id}
                      showDropLine={!!dragId && dragId !== card.id && isOverCol && over.before === card.id}
                      onDragStart={() => setDragId(card.id)}
                      onDragEnd={() => { setDragId(null); setOver(null); }}
                      onCardDragOver={() => setOver({ col: col.id, before: card.id })} />
                  ))}
                  {!colCards.length && (
                    <div className="text-xs" style={{ textAlign: 'center', padding: '14px 0', color: '#3d4e6a' }}>
                      {anyFilter && all.length ? 'No matches' : '—'}
                    </div>
                  )}
                </div>
                <button className="btn btn-ghost btn-sm" style={{ marginTop: 6, justifyContent: 'flex-start', color: '#5a6b8c' }}
                  onClick={() => onAdd(col.id)}>+ Add</button>
              </div>
            );
          })}
        </div>
      </div>
    );
  }

  function CardTile({ card, agents, run, docs, onOpenDoc, onOpen, onRun, onDragStart, onDragEnd, onCardDragOver, dragging, showDropLine }) {
    docs = docs || [];
    const tm = typeMeta(card.cardType);
    const pm = PRIORITY_META[card.priority] || PRIORITY_META.normal;
    const rv = run ? reviewSummary(run.log) : null;
    const runTerminal = run && RUN_TERMINAL.has(run.status);
    const due = card.dueDate ? daysUntil(card.dueDate) : null;
    const overdue = due != null && due < 0 && card.status !== 'done';
    const dueSoon = due != null && due >= 0 && due <= 2 && card.status !== 'done';
    return (
      <div draggable onDragStart={onDragStart} onDragEnd={onDragEnd} onClick={onOpen}
        onDragOver={(e) => { if (onCardDragOver) { e.preventDefault(); e.stopPropagation(); onCardDragOver(); } }}
        style={{
          background: '#111827', border: '1px solid #1e2d4a', borderRadius: 9,
          borderLeft: '3px solid ' + tm.color, padding: '10px 11px', cursor: 'grab',
          transition: 'border-color 0.15s', opacity: dragging ? 0.4 : 1,
          boxShadow: showDropLine ? 'inset 0 3px 0 #c8a84a' : 'none',
        }}
        onMouseEnter={(e) => { e.currentTarget.style.borderColor = '#3a4f70'; e.currentTarget.style.borderLeftColor = tm.color; }}
        onMouseLeave={(e) => { e.currentTarget.style.borderColor = '#1e2d4a'; e.currentTarget.style.borderLeftColor = tm.color; }}>
        <div className="row" style={{ gap: 6, marginBottom: 6, alignItems: 'flex-start' }}>
          <span style={{ fontSize: '0.8rem', flexShrink: 0 }}>{tm.icon}</span>
          <span style={{ fontSize: '0.84rem', fontWeight: 500, lineHeight: 1.4 }}>{card.title}</span>
        </div>
        {card.blocked && (
          <div style={{ marginBottom: 6 }}>
            <Pill label={'⛔ Blocked' + (card.blockedReason ? ' — ' + card.blockedReason : '')} color="#e08080" />
          </div>
        )}
        <div className="row" style={{ gap: 5, flexWrap: 'wrap', alignItems: 'center' }}>
          {card.priority && card.priority !== 'normal' && <Pill label={pm.label} color={pm.color} />}
          {card.dueDate && (
            <span style={{ fontSize: '0.7rem', fontWeight: 600,
              color: overdue ? '#e08080' : dueSoon ? '#c8a84a' : '#6b7a99' }}>
              📅 {fmtDate(card.dueDate)}
            </span>
          )}
          {run && (
            <span title={RUN_STATUS_LABEL[run.status] || run.status} style={{ fontSize: '0.68rem', fontWeight: 600,
              display: 'inline-flex', alignItems: 'center', gap: 3,
              color: RUN_STATUS_COLOR[run.status] || '#6b7a99' }}>
              <span style={{ width: 6, height: 6, borderRadius: '50%', background: RUN_STATUS_COLOR[run.status] || '#6b7a99', display: 'inline-block' }} />
              Run
            </span>
          )}
          {rv && rv.approvals > 0 && <span title="Review gates passed" style={{ fontSize: '0.68rem', fontWeight: 600, color: '#4ade80' }}>✓{rv.approvals}</span>}
          {rv && rv.reworks > 0 && <span title="Rework cycles" style={{ fontSize: '0.68rem', fontWeight: 600, color: '#e08080' }}>↺{rv.reworks}</span>}
          {docs.length > 0 && <span className="text-xs" title="Deliverables generated">📄 {docs.length}</span>}
          <span style={{ marginLeft: 'auto' }}>
            <OwnerAvatar role={card.ownerRole} agents={agents} />
          </span>
        </div>
        {docs.length > 0 && (
          <div style={{ marginTop: 8, display: 'flex', flexDirection: 'column', gap: 2 }}>
            {docs.slice(0, 3).map(d => (
              <button key={d.id} title={d.title || d.deliverableName}
                onClick={(e) => { e.stopPropagation(); onOpenDoc && onOpenDoc(d); }}
                style={{
                  textAlign: 'left', background: 'none', border: 'none', cursor: 'pointer', padding: 0,
                  color: '#6baede', fontSize: '0.72rem', display: 'flex', gap: 4, alignItems: 'center',
                  overflow: 'hidden',
                }}>
                <span style={{ flexShrink: 0 }}>📄</span>
                <span style={{ textDecoration: 'underline', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{d.title || d.deliverableName || 'Deliverable'}</span>
              </button>
            ))}
            {docs.length > 3 && (
              <button onClick={(e) => { e.stopPropagation(); onOpen(); }}
                style={{ textAlign: 'left', background: 'none', border: 'none', cursor: 'pointer', padding: 0, color: '#5a6b8c', fontSize: '0.7rem' }}>
                +{docs.length - 3} more
              </button>
            )}
          </div>
        )}
        {onRun && (!run || runTerminal) && (
          <button title={run ? 'Start a new run for this work item' : 'Run this work item through its Orchestra workflow'}
            onClick={(e) => { e.stopPropagation(); onRun(); }}
            style={{
              marginTop: 8, width: '100%', padding: '5px 0', borderRadius: 6, cursor: 'pointer',
              fontSize: '0.74rem', fontWeight: 600, border: '1px solid rgba(200,168,74,0.35)',
              background: 'rgba(200,168,74,0.1)', color: '#c8a84a',
            }}>{run ? '↻ Re-run workflow' : '▶ Run workflow'}</button>
        )}
      </div>
    );
  }

  // ════════════════════════════════════════════════════════════════════════
  //  Calendar
  // ════════════════════════════════════════════════════════════════════════
  function Calendar({ year, month, cards, releases, events, setYear, setMonth,
                      onOpenCard, onOpenRelease, onOpenEvent, onAddOnDay }) {
    // Build a date → items index.
    const byDay = useMemo(() => {
      const m = {};
      const push = (iso, item) => { if (!iso) return; const k = String(iso).slice(0, 10); (m[k] = m[k] || []).push(item); };
      cards.forEach(c => { if (c.dueDate) push(c.dueDate, { kind: 'card', color: typeMeta(c.cardType).color, label: c.title, obj: c }); });
      releases.forEach(r => push(r.releaseDate, { kind: 'release', color: '#a78bda', label: '🚀 ' + r.name, obj: r }));
      events.forEach(e => push(e.startDate, { kind: 'event', color: (EVENT_TYPE[e.eventType] || EVENT_TYPE.custom).color, label: e.title, obj: e }));
      return m;
    }, [cards, releases, events]);

    const weeks = monthMatrix(year, month);
    const todayK = todayIso();

    function prev() { if (month === 0) { setYear(year - 1); setMonth(11); } else setMonth(month - 1); }
    function next() { if (month === 11) { setYear(year + 1); setMonth(0); } else setMonth(month + 1); }
    function goToday() { const n = new Date(); setYear(n.getFullYear()); setMonth(n.getMonth()); }

    function openItem(item) {
      if (item.kind === 'card') onOpenCard(item.obj);
      else if (item.kind === 'release') onOpenRelease(item.obj);
      else onOpenEvent(item.obj);
    }

    return (
      <div>
        <div className="row-between" style={{ marginBottom: 14, flexWrap: 'wrap', gap: 10 }}>
          <div className="row" style={{ gap: 6 }}>
            <button className="btn btn-outline btn-sm" onClick={prev}>‹</button>
            <div style={{ fontSize: '1rem', fontWeight: 600, minWidth: 160, textAlign: 'center' }}>{MONTHS[month]} {year}</div>
            <button className="btn btn-outline btn-sm" onClick={next}>›</button>
            <button className="btn btn-ghost btn-sm" onClick={goToday}>Today</button>
          </div>
          <div className="row" style={{ gap: 12, flexWrap: 'wrap' }}>
            <LegendDot color={TYPE_META.work_item.color} label="Work / Task" />
            <LegendDot color={TYPE_META.training.color} label="Training" />
            <LegendDot color="#a78bda" label="Release" />
            <LegendDot color={EVENT_TYPE.deadline.color} label="Deadline" />
          </div>
        </div>

        <div style={{ display: 'grid', gridTemplateColumns: 'repeat(7,1fr)', gap: 1, background: '#1e2d4a', border: '1px solid #1e2d4a', borderRadius: 10, overflow: 'hidden' }}>
          {WEEKDAYS.map(w => (
            <div key={w} style={{ background: '#0d1220', padding: '8px 10px', fontSize: '0.7rem', fontWeight: 600, color: '#8a96aa', textTransform: 'uppercase', letterSpacing: '0.05em' }}>{w}</div>
          ))}
          {weeks.map((week, wi) => week.map((day, di) => {
            const k = isoOf(day);
            const inMonth = day.getMonth() === month;
            const items = byDay[k] || [];
            const isToday = k === todayK;
            return (
              <div key={wi + '-' + di} onClick={() => onAddOnDay(k)}
                style={{
                  background: inMonth ? '#111827' : '#0c111d', minHeight: 104, padding: 6,
                  cursor: 'pointer', display: 'flex', flexDirection: 'column', gap: 3,
                  opacity: inMonth ? 1 : 0.5,
                }}>
                <div style={{
                  fontSize: '0.72rem', fontWeight: isToday ? 700 : 500,
                  color: isToday ? '#08090f' : '#8a96aa', alignSelf: 'flex-start',
                  background: isToday ? '#c8a84a' : 'transparent',
                  borderRadius: 10, padding: isToday ? '0 6px' : '0 2px', lineHeight: '18px',
                }}>{day.getDate()}</div>
                {items.slice(0, 3).map((it, i) => (
                  <div key={i} onClick={(e) => { e.stopPropagation(); openItem(it); }}
                    title={it.label}
                    style={{
                      fontSize: '0.68rem', padding: '2px 5px', borderRadius: 4,
                      background: it.color + '22', color: it.color, fontWeight: 500,
                      whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
                      borderLeft: '2px solid ' + it.color,
                    }}>{it.label}</div>
                ))}
                {items.length > 3 && <div className="text-xs" style={{ paddingLeft: 4 }}>+{items.length - 3} more</div>}
              </div>
            );
          }))}
        </div>
        <div className="text-xs" style={{ marginTop: 10 }}>Tip: click a day to schedule an event (training, milestone, deadline). Click an item to open it.</div>
      </div>
    );
  }
  function LegendDot({ color, label }) {
    return (
      <span className="row" style={{ gap: 5 }}>
        <span style={{ width: 9, height: 9, borderRadius: 2, background: color, display: 'inline-block' }} />
        <span className="text-xs">{label}</span>
      </span>
    );
  }

  // ════════════════════════════════════════════════════════════════════════
  //  Releases
  // ════════════════════════════════════════════════════════════════════════
  function Releases({ releases, cards, agents, onOpen, onSave, onNew }) {
    if (!releases.length) return (
      <div className="empty-state">
        <div style={{ marginBottom: 10 }}>No releases scheduled.</div>
        <button className="btn btn-gold btn-sm" onClick={onNew}>+ Schedule a Release</button>
      </div>
    );
    const sorted = [...releases].sort((a, b) => (a.releaseDate || '').localeCompare(b.releaseDate || ''));
    return (
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill,minmax(320px,1fr))', gap: 14 }}>
        {sorted.map(rel => {
          const rs = RELEASE_STATUS[rel.status] || RELEASE_STATUS.planned;
          const linked = cards.filter(c => c.releaseId === rel.id);
          const done = linked.filter(c => c.status === 'done').length;
          const chk = rel.checklist || [];
          const chkDone = chk.filter(i => i.done).length;
          const days = daysUntil(rel.releaseDate);
          return (
            <div key={rel.id} className="card" style={{ cursor: 'pointer', padding: 18 }} onClick={() => onOpen(rel)}>
              <div className="row-between" style={{ marginBottom: 8 }}>
                <div className="row" style={{ gap: 8 }}>
                  <span style={{ fontSize: '1.1rem' }}>🚀</span>
                  <div>
                    <div style={{ fontWeight: 600, fontSize: '0.95rem' }}>{rel.name}</div>
                    {rel.version && <div className="text-xs mono">{rel.version} · {rel.environment}</div>}
                  </div>
                </div>
                <Pill label={rs.label} color={rs.color} />
              </div>
              <div className="row" style={{ gap: 8, marginBottom: 12, flexWrap: 'wrap' }}>
                <span style={{ fontSize: '0.8rem', fontWeight: 600, color: rel.releaseDate ? '#c8a84a' : '#6b7a99' }}>
                  📅 {rel.releaseDate ? fmtDate(rel.releaseDate) : 'Unscheduled'}
                </span>
                {days != null && rel.status !== 'released' && (
                  <span className="text-xs">{days < 0 ? `${-days}d overdue` : days === 0 ? 'Today' : `in ${days}d`}</span>
                )}
              </div>
              {/* Readiness — linked work items */}
              <div style={{ marginBottom: 10 }}>
                <div className="text-xs" style={{ marginBottom: 4 }}>Work items: {done}/{linked.length} done</div>
                <ProgressBar value={linked.length ? done / linked.length : 0} color="#4ade80" />
              </div>
              {/* Readiness — checklist */}
              {chk.length > 0 && (
                <div>
                  <div className="text-xs" style={{ marginBottom: 4 }}>Checklist: {chkDone}/{chk.length}</div>
                  <ProgressBar value={chk.length ? chkDone / chk.length : 0} color="#6baede" />
                </div>
              )}

              {/* Readiness + quick status advance */}
              {(() => {
                const openItems = (linked.length - done) + (chk.length - chkDone);
                const ready = openItems === 0;
                const NEXT = { planned: 'staged', staged: 'released' };
                const next = NEXT[rel.status];
                return (
                  <div className="row-between" style={{ marginTop: 12, paddingTop: 10, borderTop: '1px solid #1b2640' }}>
                    {rel.status === 'released' ? (
                      <Pill label="✓ Released" color="#4ade80" />
                    ) : ready ? (
                      <Pill label="✓ Ready" color="#4ade80" />
                    ) : (
                      <span className="text-xs" style={{ color: '#e0a060' }}>⚠ {openItems} open item{openItems === 1 ? '' : 's'}</span>
                    )}
                    {next && (
                      <button className="btn btn-outline btn-xs"
                        onClick={(e) => {
                          e.stopPropagation();
                          if (next === 'released' && !ready &&
                              !confirm(`This release still has ${openItems} open item(s). Mark it released anyway?`)) return;
                          onSave({ id: rel.id, status: next });
                        }}>
                        {next === 'staged' ? 'Mark staged →' : 'Mark released →'}
                      </button>
                    )}
                  </div>
                );
              })()}
            </div>
          );
        })}
      </div>
    );
  }
  function ProgressBar({ value, color }) {
    return (
      <div style={{ height: 6, background: '#1a2540', borderRadius: 4, overflow: 'hidden' }}>
        <div style={{ width: Math.round((value || 0) * 100) + '%', height: '100%', background: color || '#c8a84a' }} />
      </div>
    );
  }

  // Compact run-history timeline for the card editor — every stage, review,
  // rework, gate, and verdict from the linked run's log.
  function RunActivity({ log }) {
    const [open, setOpen] = useState(false);
    const entries = log || [];
    if (!entries.length) return null;
    const shown = open ? entries : entries.slice(-6);
    return (
      <div style={{ marginTop: 12, borderTop: '1px solid #1b2640', paddingTop: 10 }}>
        <div className="row-between" style={{ marginBottom: 8 }}>
          <span className="inspector-section-title" style={{ margin: 0 }}>Activity</span>
          {entries.length > 6 && (
            <button className="btn btn-ghost btn-xs" onClick={() => setOpen(o => !o)}>
              {open ? 'Show recent' : `Show all (${entries.length})`}
            </button>
          )}
        </div>
        <div style={{ display: 'flex', flexDirection: 'column', maxHeight: open ? 300 : 'none', overflowY: open ? 'auto' : 'visible' }}>
          {shown.map((e, i) => {
            const t = e.event || e.type || '';
            const meta = EVENT_META[t] || { label: t.replace(/_/g, ' ') || 'Event', color: '#8a96aa' };
            const v = e.verdict || '';
            return (
              <div key={i} style={{ display: 'flex', gap: 10, padding: '6px 0', borderBottom: '1px solid #141d33', fontSize: '0.78rem' }}>
                <span style={{ color: '#6b7a99', minWidth: 38, fontSize: '0.7rem', paddingTop: 2 }}>{fmtTime(e.time)}</span>
                <span style={{ width: 8, height: 8, borderRadius: '50%', background: meta.color, marginTop: 5, flexShrink: 0 }} />
                <div style={{ flex: 1, minWidth: 0 }}>
                  <div>
                    <b style={{ color: '#ddd9d1' }}>{meta.label}</b>
                    {e.stage ? <span className="text-xs" style={{ color: '#8a96aa' }}> · {e.stage}</span> : null}
                    {e.agent ? <span className="text-xs" style={{ color: '#6b7a99' }}> · {e.agent}</span> : null}
                  </div>
                  {e.detail ? <div className="text-xs" style={{ color: '#8a96aa', lineHeight: 1.5 }}>{e.detail}</div> : null}
                  {v ? <span style={{ display: 'inline-block', marginTop: 3, fontSize: '0.68rem', fontWeight: 600, color: VERDICT_COLOR[v] || '#8a96aa' }}>{VERDICT_LABEL[v] || v}</span> : null}
                </div>
              </div>
            );
          })}
        </div>
      </div>
    );
  }

  // ════════════════════════════════════════════════════════════════════════
  //  Portfolio — cross-project rollup
  // ════════════════════════════════════════════════════════════════════════
  function Portfolio({ navigateTo, onOpenProject }) {
    const [projects, setProjects] = useState([]);
    const [cards, setCards] = useState([]);
    const [releases, setReleases] = useState([]);
    const [loading, setLoading] = useState(true);
    useEffect(() => {
      (async () => {
        try {
          const [ps, cs, rs] = await Promise.all([
            api('GET', '/api/pm/projects'),
            api('GET', '/api/pm/cards'),
            api('GET', '/api/pm/releases'),
          ]);
          setProjects(ps); setCards(cs); setReleases(rs);
        } catch (e) { /* show empty */ }
        finally { setLoading(false); }
      })();
    }, []);

    if (loading) return <div className="empty-state">Loading portfolio…</div>;
    if (!projects.length) return <div className="empty-state">No projects yet.</div>;

    const rollups = projects.map(p => {
      const pc = cards.filter(c => c.projectId === p.id);
      const by = (s) => pc.filter(c => c.status === s).length;
      const done = by('done');
      const total = pc.length;
      const blocked = pc.filter(c => c.blocked).length;
      const overdue = pc.filter(c => c.dueDate && daysUntil(c.dueDate) < 0 && c.status !== 'done').length;
      const runsLinked = pc.filter(c => c.runId).length;
      const upcoming = releases
        .filter(r => r.projectId === p.id && r.status !== 'released' && r.releaseDate)
        .sort((a, b) => a.releaseDate.localeCompare(b.releaseDate))[0];
      return { p, total, done, blocked, overdue, runsLinked,
               inProgress: by('in_progress'), inReview: by('review'), upcoming };
    });

    const tot = {
      projects: projects.length,
      items: cards.length,
      blocked: cards.filter(c => c.blocked).length,
      overdue: cards.filter(c => c.dueDate && daysUntil(c.dueDate) < 0 && c.status !== 'done').length,
      upcoming: releases.filter(r => r.status !== 'released' && r.releaseDate && daysUntil(r.releaseDate) >= 0).length,
    };

    return (
      <div>
        <div className="row" style={{ gap: 8, marginBottom: 16, flexWrap: 'wrap' }}>
          <StatChip label="Projects" n={tot.projects} color="#8a96aa" />
          <StatChip label="Work items" n={tot.items} color="#6baede" />
          <StatChip label="Blocked" n={tot.blocked} color="#e08080" />
          <StatChip label="Overdue" n={tot.overdue} color="#e08080" />
          <StatChip label="Upcoming releases" n={tot.upcoming} color="#a78bda" />
        </div>
        <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill,minmax(330px,1fr))', gap: 14 }}>
          {rollups.map(({ p, total, done, blocked, overdue, runsLinked, inProgress, inReview, upcoming }) => {
            const target = p.targetDate ? daysUntil(p.targetDate) : null;
            return (
              <div key={p.id} className="card" style={{ cursor: 'pointer', padding: 18, borderLeft: '3px solid ' + (p.color || '#c8a84a') }}
                onClick={() => onOpenProject(p.id)}>
                <div className="row-between" style={{ marginBottom: 10 }}>
                  <div style={{ fontWeight: 600, fontSize: '0.95rem' }}>{p.name}</div>
                  <Pill label={(p.status || 'active').replace(/_/g, ' ')}
                    color={p.status === 'complete' ? '#4ade80' : p.status === 'on_hold' ? '#c8a84a' : '#6baede'} />
                </div>
                <div className="text-xs" style={{ marginBottom: 4 }}>{done}/{total} work items done</div>
                <ProgressBar value={total ? done / total : 0} color="#4ade80" />
                <div className="row" style={{ gap: 6, flexWrap: 'wrap', marginTop: 12 }}>
                  <Pill label={`${inProgress} in progress`} color="#c8a84a" />
                  <Pill label={`${inReview} in review`} color="#6baede" />
                  {blocked > 0 && <Pill label={`${blocked} blocked`} color="#e08080" />}
                  {overdue > 0 && <Pill label={`${overdue} overdue`} color="#e08080" />}
                </div>
                <div className="row-between" style={{ marginTop: 12, paddingTop: 10, borderTop: '1px solid #1b2640' }}>
                  <span className="text-xs">🔗 {runsLinked} run{runsLinked === 1 ? '' : 's'} linked</span>
                  {target != null && (
                    <span className="text-xs" style={{ color: target < 0 ? '#e08080' : '#6b7a99' }}>
                      {target < 0 ? `${-target}d past target` : `Target in ${target}d`}
                    </span>
                  )}
                </div>
                {upcoming && (
                  <div className="text-xs" style={{ marginTop: 8, color: '#a78bda' }}>
                    🚀 Next: {upcoming.name}{upcoming.releaseDate ? ` · ${fmtDate(upcoming.releaseDate)}` : ''}
                  </div>
                )}
              </div>
            );
          })}
        </div>
      </div>
    );
  }

  // ════════════════════════════════════════════════════════════════════════
  //  Modals
  // ════════════════════════════════════════════════════════════════════════
  function CardModal({ initial, columns, cardTypes, agents, workflows, runs, releases,
                       runsById, docsByRun, navigateTo, onSave, onRun, onOpenDoc, onDelete, onClose }) {
    const isNew = !!initial._new;
    const [f, setF] = useState({
      id: initial.id, title: initial.title || '', description: initial.description || '',
      cardType: initial.cardType || 'work_item', status: initial.status || 'backlog',
      priority: initial.priority || 'normal', ownerRole: initial.ownerRole || '',
      workflowId: initial.workflowId || '', runId: initial.runId || '',
      releaseId: initial.releaseId || '', dueDate: initial.dueDate || '',
      startDate: initial.startDate || '', blocked: !!initial.blocked,
      blockedReason: initial.blockedReason || '',
    });
    const set = (k, v) => setF(p => ({ ...p, [k]: v }));
    const run = f.runId ? runsById[f.runId] : null;
    const docs = (docsByRun[f.runId] || []);
    const roles = (agents || []).filter(a => !a.meta).map(a => a.role);

    const footer = [
      !isNew && <button key="del" className="btn btn-danger btn-sm" onClick={onDelete}>Delete</button>,
      <button key="cancel" className="btn btn-outline btn-sm" onClick={onClose}>Cancel</button>,
      <button key="save" className="btn btn-gold btn-sm" disabled={!f.title.trim()} onClick={() => onSave(f)}>{isNew ? 'Create' : 'Save'}</button>,
    ];

    return (
      <Modal wide title={isNew ? 'New Work Item' : 'Edit Work Item'}
        subtitle="A unit of delivery work fed into an Orchestra workflow" onClose={onClose} footer={footer}>
        <Field label="What needs doing? (this is the request fed into the workflow)">
          <input style={{ width: '100%' }} value={f.title} onChange={(e) => set('title', e.target.value)} autoFocus />
        </Field>
        <Field label="Details">
          <textarea style={{ width: '100%', minHeight: 70 }} value={f.description} onChange={(e) => set('description', e.target.value)} />
        </Field>

        {/* Orchestra link — the whole point of a work item: feed it into a workflow */}
        <div className="surface" style={{ marginBottom: 14, border: '1px solid rgba(200,168,74,0.25)' }}>
          <div className="inspector-section-title" style={{ color: '#c8a84a' }}>Run through Orchestra</div>
          <div className="row" style={{ gap: 10, alignItems: 'flex-end', flexWrap: 'wrap' }}>
            <div style={{ flex: 1, minWidth: 180 }}>
              <Field label="Workflow">
                <select style={{ width: '100%' }} value={f.workflowId} onChange={(e) => set('workflowId', e.target.value)}>
                  <option value="">— Choose a workflow —</option>
                  {(workflows || []).map(w => <option key={w.id} value={w.id}>{w.name}</option>)}
                </select>
              </Field>
            </div>
            {(!run || RUN_TERMINAL.has(run.status)) && (
              <button className="btn btn-gold btn-sm" disabled={!f.workflowId || !f.title.trim()}
                title={!f.workflowId ? 'Choose a workflow first' : run ? 'Start a new run for this work item' : 'Run this work item now'}
                onClick={() => onRun && onRun(f)} style={{ marginBottom: 12 }}>{run ? '↻ Re-run' : '▶ Run workflow'}</button>
            )}
          </div>
          {run ? (
            <div>
              <div className="row-between" style={{ marginBottom: docs.length ? 8 : 0 }}>
                <span className="text-muted">Linked run</span>
                <span style={{ fontSize: '0.78rem', fontWeight: 600, color: RUN_STATUS_COLOR[run.status] || '#8a96aa' }}>
                  {RUN_STATUS_LABEL[run.status] || run.status}
                </span>
              </div>
              {docs.length > 0 && (
                <div>
                  <div className="text-xs" style={{ marginBottom: 4 }}>{docs.length} deliverable{docs.length === 1 ? '' : 's'} generated:</div>
                  {docs.slice(0, 8).map(d => (
                    <button key={d.id} onClick={() => onOpenDoc && onOpenDoc(d)} title="View deliverable"
                      style={{ display: 'block', textAlign: 'left', background: 'none', border: 'none', cursor: 'pointer',
                        padding: '1px 0', color: '#6baede', fontSize: '0.78rem' }}>
                      • <span style={{ textDecoration: 'underline' }}>{d.title || d.deliverableName}</span>
                    </button>
                  ))}
                  {navigateTo && <button className="btn btn-ghost btn-xs" style={{ marginTop: 6, paddingLeft: 0 }} onClick={() => navigateTo('documents')}>View all in Documents →</button>}
                </div>
              )}
              {navigateTo && <button className="btn btn-ghost btn-xs" style={{ paddingLeft: 0 }} onClick={() => navigateTo('run')}>Open run in Workspace →</button>}
              <RunActivity log={run.log} />
            </div>
          ) : (
            <div className="text-xs">{f.workflowId
              ? 'Running feeds this work item into the workflow and links the run back here, with its deliverables.'
              : 'Choose a workflow to run this work item through the agent pipeline.'}</div>
          )}
        </div>

        <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
          <Field label="Type">
            <select style={{ width: '100%' }} value={f.cardType} onChange={(e) => set('cardType', e.target.value)}>
              {(cardTypes.length ? cardTypes : Object.keys(TYPE_META)).map(t => <option key={t} value={t}>{typeMeta(t).label}</option>)}
            </select>
          </Field>
          <Field label="Column">
            <select style={{ width: '100%' }} value={f.status} onChange={(e) => set('status', e.target.value)}>
              {columns.map(c => <option key={c.id} value={c.id}>{c.name}</option>)}
            </select>
          </Field>
          <Field label="Priority">
            <select style={{ width: '100%' }} value={f.priority} onChange={(e) => set('priority', e.target.value)}>
              {Object.keys(PRIORITY_META).map(p => <option key={p} value={p}>{PRIORITY_META[p].label}</option>)}
            </select>
          </Field>
          <Field label="Owner">
            <select style={{ width: '100%' }} value={f.ownerRole} onChange={(e) => set('ownerRole', e.target.value)}>
              <option value="">— Unassigned —</option>
              {roles.map(r => <option key={r} value={r}>{r}</option>)}
            </select>
          </Field>
          <Field label="Start date">
            <input type="date" style={{ width: '100%' }} value={f.startDate} onChange={(e) => set('startDate', e.target.value)} />
          </Field>
          <Field label="Due date">
            <input type="date" style={{ width: '100%' }} value={f.dueDate} onChange={(e) => set('dueDate', e.target.value)} />
          </Field>
        </div>

        <div style={{ borderTop: '1px solid #1b2640', margin: '6px 0 14px' }} />
        <div className="inspector-section-title">Advanced links</div>
        <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
          <Field label="Linked run (override)">
            <select style={{ width: '100%' }} value={f.runId} onChange={(e) => set('runId', e.target.value)}>
              <option value="">— None —</option>
              {(runs || []).map(r => <option key={r.id} value={r.id}>{(r.wfName || 'Run') + ' · ' + r.id.slice(0, 8)}</option>)}
            </select>
          </Field>
          <Field label="Release">
            <select style={{ width: '100%' }} value={f.releaseId} onChange={(e) => set('releaseId', e.target.value)}>
              <option value="">— None —</option>
              {(releases || []).map(r => <option key={r.id} value={r.id}>{r.name}</option>)}
            </select>
          </Field>
        </div>

        <div style={{ borderTop: '1px solid #1b2640', margin: '6px 0 14px' }} />
        <label className="row" style={{ gap: 8, cursor: 'pointer', marginBottom: f.blocked ? 10 : 0 }}>
          <input type="checkbox" style={{ width: 'auto' }} checked={f.blocked} onChange={(e) => set('blocked', e.target.checked)} />
          <span style={{ color: '#e08080', fontWeight: 500, fontSize: '0.85rem' }}>Blocked</span>
        </label>
        {f.blocked && (
          <input style={{ width: '100%' }} placeholder="What's blocking this?" value={f.blockedReason} onChange={(e) => set('blockedReason', e.target.value)} />
        )}
      </Modal>
    );
  }

  function ReleaseModal({ initial, cards, agents, onSave, onDelete, onClose }) {
    const isNew = !!initial._new;
    const projectId = initial.projectId;
    const [busy, setBusy] = useState(false);
    const [f, setF] = useState({
      id: initial.id, name: initial.name || '', version: initial.version || '',
      status: initial.status || 'planned', environment: initial.environment || 'Production',
      releaseDate: initial.releaseDate || '', notes: initial.notes || '',
      checklist: (initial.checklist || []).map(i => ({ ...i })),
      postPlan: (initial.postPlan || []).map(i => ({ ...i })),
    });
    const set = (k, v) => setF(p => ({ ...p, [k]: v }));
    const setChk = (i, patch) => setF(p => ({ ...p, checklist: p.checklist.map((c, idx) => idx === i ? { ...c, ...patch } : c) }));
    const addChk = () => setF(p => ({ ...p, checklist: [...p.checklist, { text: '', done: false }] }));
    const rmChk = (i) => setF(p => ({ ...p, checklist: p.checklist.filter((_, idx) => idx !== i) }));
    const setPost = (i, patch) => setF(p => ({ ...p, postPlan: p.postPlan.map((a, idx) => idx === i ? { ...a, ...patch } : a) }));
    const addPost = () => setF(p => ({ ...p, postPlan: [...p.postPlan, { label: '', kind: 'training', offsetDays: 1, done: false, scheduled: false }] }));
    const rmPost = (i) => setF(p => ({ ...p, postPlan: p.postPlan.filter((_, idx) => idx !== i) }));
    const genDefault = () => setF(p => ({ ...p, postPlan: DEFAULT_POST_PLAN.map(a => ({ ...a })) }));
    const linked = isNew ? [] : cards.filter(c => c.releaseId === f.id);

    // Schedule the unscheduled post-release activities onto the calendar, dated
    // relative to the release date, then persist the plan.
    async function schedulePost() {
      if (!f.releaseDate) { alert('Set a release date first — activities are scheduled relative to it.'); return; }
      setBusy(true);
      const updated = f.postPlan.map(a => ({ ...a }));
      let created = 0;
      for (let i = 0; i < updated.length; i++) {
        const a = updated[i];
        if (a.scheduled || !a.label.trim()) continue;
        try {
          await api('POST', '/api/pm/events', {
            projectId, title: a.label, eventType: (POST_KIND[a.kind] || {}).evt || 'custom',
            startDate: addDaysIso(f.releaseDate, a.offsetDays), releaseId: f.id, notes: 'Post-release plan',
          });
          updated[i] = { ...a, scheduled: true }; created++;
        } catch (e) { /* skip */ }
      }
      onSave({ ...f, postPlan: updated });   // persists post_plan + reloads + closes
    }

    const footer = [
      !isNew && <button key="del" className="btn btn-danger btn-sm" onClick={() => onDelete(f.id)}>Delete</button>,
      <button key="cancel" className="btn btn-outline btn-sm" onClick={onClose}>Cancel</button>,
      <button key="save" className="btn btn-gold btn-sm" disabled={!f.name.trim()} onClick={() => onSave(f)}>{isNew ? 'Schedule' : 'Save'}</button>,
    ];

    return (
      <Modal title={isNew ? 'Schedule Release' : 'Edit Release'} subtitle="Shown on the delivery calendar" onClose={onClose} footer={footer}>
        <Field label="Name">
          <input style={{ width: '100%' }} value={f.name} onChange={(e) => set('name', e.target.value)} autoFocus />
        </Field>
        <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
          <Field label="Version"><input style={{ width: '100%' }} value={f.version} onChange={(e) => set('version', e.target.value)} placeholder="v1.0.0" /></Field>
          <Field label="Environment"><input style={{ width: '100%' }} value={f.environment} onChange={(e) => set('environment', e.target.value)} /></Field>
          <Field label="Release date"><input type="date" style={{ width: '100%' }} value={f.releaseDate} onChange={(e) => set('releaseDate', e.target.value)} /></Field>
          <Field label="Status">
            <select style={{ width: '100%' }} value={f.status} onChange={(e) => set('status', e.target.value)}>
              {Object.keys(RELEASE_STATUS).map(s => <option key={s} value={s}>{RELEASE_STATUS[s].label}</option>)}
            </select>
          </Field>
        </div>
        <Field label="Notes">
          <textarea style={{ width: '100%', minHeight: 60 }} value={f.notes} onChange={(e) => set('notes', e.target.value)} />
        </Field>

        <div className="row-between" style={{ margin: '6px 0 8px' }}>
          <span className="inspector-section-title" style={{ margin: 0 }}>Readiness checklist</span>
          <button className="btn btn-ghost btn-xs" onClick={addChk}>+ Item</button>
        </div>
        {f.checklist.map((c, i) => (
          <div key={i} className="row" style={{ gap: 8, marginBottom: 6 }}>
            <input type="checkbox" style={{ width: 'auto' }} checked={!!c.done} onChange={(e) => setChk(i, { done: e.target.checked })} />
            <input style={{ flex: 1 }} value={c.text} onChange={(e) => setChk(i, { text: e.target.value })} placeholder="Checklist item" />
            <button className="btn btn-ghost btn-xs text-red" onClick={() => rmChk(i)}>✕</button>
          </div>
        ))}
        {!f.checklist.length && <div className="text-xs" style={{ marginBottom: 6 }}>No checklist items yet.</div>}

        {!isNew && (
          <div style={{ marginTop: 14 }}>
            <div className="inspector-section-title">Linked work items ({linked.filter(c => c.status === 'done').length}/{linked.length} done)</div>
            {linked.length ? linked.map(c => (
              <div key={c.id} className="row" style={{ gap: 8, padding: '3px 0' }}>
                <span style={{ fontSize: '0.78rem' }}>{typeMeta(c.cardType).icon}</span>
                <span style={{ fontSize: '0.82rem', flex: 1 }}>{c.title}</span>
                <Pill label={c.status === 'done' ? 'Done' : c.status.replace(/_/g, ' ')} color={c.status === 'done' ? '#4ade80' : '#748090'} />
              </div>
            )) : <div className="text-xs">Link work items to this release from each card’s “Release” field.</div>}
          </div>
        )}

        {/* Post-release & hypercare — the structured phase after go-live */}
        {!isNew && (
          <div style={{ marginTop: 16, borderTop: '1px solid #1b2640', paddingTop: 12 }}>
            <div className="row-between" style={{ marginBottom: 8 }}>
              <span className="inspector-section-title" style={{ margin: 0 }}>Post-release &amp; hypercare</span>
              {f.postPlan.length === 0
                ? <button className="btn btn-ghost btn-xs" onClick={genDefault}>Generate plan</button>
                : <button className="btn btn-ghost btn-xs" onClick={addPost}>+ Activity</button>}
            </div>
            {f.postPlan.length === 0 && (
              <div className="text-xs" style={{ marginBottom: 6 }}>
                Training, hypercare, knowledge transfer, and a retro — scheduled relative to go-live. Generate a default plan or add your own.
              </div>
            )}
            {f.postPlan.map((a, i) => (
              <div key={i} className="row" style={{ gap: 6, marginBottom: 6, alignItems: 'center' }}>
                <input type="checkbox" style={{ width: 'auto' }} checked={!!a.done} onChange={(e) => setPost(i, { done: e.target.checked })} title="Done" />
                <input style={{ flex: 1, minWidth: 0 }} value={a.label} onChange={(e) => setPost(i, { label: e.target.value })} placeholder="Activity" />
                <select style={{ width: 110 }} value={a.kind} onChange={(e) => setPost(i, { kind: e.target.value })}>
                  {Object.keys(POST_KIND).map(k => <option key={k} value={k}>{POST_KIND[k].label}</option>)}
                </select>
                <span className="text-xs" style={{ whiteSpace: 'nowrap' }}>+
                  <input type="number" style={{ width: 46, padding: '4px 6px' }} value={a.offsetDays}
                    onChange={(e) => setPost(i, { offsetDays: e.target.value })} title="Days after go-live" />d
                </span>
                {a.scheduled
                  ? <span title="On calendar" style={{ color: '#4ade80', fontSize: '0.8rem' }}>📅</span>
                  : <span style={{ width: 16 }} />}
                <button className="btn btn-ghost btn-xs text-red" onClick={() => rmPost(i)}>✕</button>
              </div>
            ))}
            {f.postPlan.length > 0 && (
              <div className="row" style={{ gap: 8, marginTop: 8, alignItems: 'center', flexWrap: 'wrap' }}>
                <button className="btn btn-outline btn-sm" disabled={busy || !f.releaseDate} onClick={schedulePost}
                  title={!f.releaseDate ? 'Set a release date first' : 'Create calendar events for unscheduled activities'}>
                  {busy ? 'Scheduling…' : '📅 Add unscheduled to calendar'}
                </button>
                <span className="text-xs">Dated relative to the release date{f.releaseDate ? ` (${fmtDate(f.releaseDate)})` : ''}.</span>
              </div>
            )}
          </div>
        )}
      </Modal>
    );
  }

  function EventModal({ initial, cards, releases, onSave, onDelete, onClose }) {
    const isNew = !!initial._new;
    const [f, setF] = useState({
      id: initial.id, title: initial.title || '', eventType: initial.eventType || 'training',
      startDate: initial.startDate || todayIso(), notes: initial.notes || '',
      cardId: initial.cardId || '', releaseId: initial.releaseId || '',
    });
    const set = (k, v) => setF(p => ({ ...p, [k]: v }));
    const footer = [
      !isNew && <button key="del" className="btn btn-danger btn-sm" onClick={() => onDelete(f.id)}>Delete</button>,
      <button key="cancel" className="btn btn-outline btn-sm" onClick={onClose}>Cancel</button>,
      <button key="save" className="btn btn-gold btn-sm" disabled={!f.title.trim()} onClick={() => onSave(f)}>{isNew ? 'Add' : 'Save'}</button>,
    ];
    return (
      <Modal title={isNew ? 'Schedule Event' : 'Edit Event'} subtitle="Training, milestones, deadlines & more" onClose={onClose} footer={footer}>
        <Field label="Title"><input style={{ width: '100%' }} value={f.title} onChange={(e) => set('title', e.target.value)} autoFocus /></Field>
        <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
          <Field label="Type">
            <select style={{ width: '100%' }} value={f.eventType} onChange={(e) => set('eventType', e.target.value)}>
              {Object.keys(EVENT_TYPE).map(t => <option key={t} value={t}>{EVENT_TYPE[t].label}</option>)}
            </select>
          </Field>
          <Field label="Date"><input type="date" style={{ width: '100%' }} value={f.startDate} onChange={(e) => set('startDate', e.target.value)} /></Field>
        </div>
        <Field label="Notes"><textarea style={{ width: '100%', minHeight: 60 }} value={f.notes} onChange={(e) => set('notes', e.target.value)} /></Field>
      </Modal>
    );
  }

  function ProjectModal({ initial, onSave, onDelete, onClose }) {
    const isNew = !!initial._new;
    const [f, setF] = useState({
      id: initial.id, name: initial.name || '', description: initial.description || '',
      status: initial.status || 'active', color: initial.color || '#c8a84a',
      startDate: initial.startDate || '', targetDate: initial.targetDate || '',
    });
    const set = (k, v) => setF(p => ({ ...p, [k]: v }));
    const footer = [
      !isNew && onDelete && <button key="del" className="btn btn-danger btn-sm" onClick={onDelete}>Delete</button>,
      <button key="cancel" className="btn btn-outline btn-sm" onClick={onClose}>Cancel</button>,
      <button key="save" className="btn btn-gold btn-sm" disabled={!f.name.trim()} onClick={() => onSave(f)}>{isNew ? 'Create' : 'Save'}</button>,
    ];
    return (
      <Modal title={isNew ? 'New Project' : 'Edit Project'} onClose={onClose} footer={footer}>
        <div className="text-xs" style={{ marginBottom: 14, lineHeight: 1.6 }}>
          A project groups related work items — e.g. one client engagement, an internal initiative, or a delivery wave. Switch between projects from the selector at the top of the board.
        </div>
        <Field label="Name"><input style={{ width: '100%' }} value={f.name} onChange={(e) => set('name', e.target.value)} autoFocus /></Field>
        <Field label="Description"><textarea style={{ width: '100%', minHeight: 60 }} value={f.description} onChange={(e) => set('description', e.target.value)} /></Field>
        <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
          <Field label="Status">
            <select style={{ width: '100%' }} value={f.status} onChange={(e) => set('status', e.target.value)}>
              <option value="active">Active</option>
              <option value="on_hold">On Hold</option>
              <option value="complete">Complete</option>
              <option value="archived">Archived</option>
            </select>
          </Field>
          <Field label="Accent color"><input type="color" style={{ width: '100%', height: 38, padding: 2 }} value={f.color} onChange={(e) => set('color', e.target.value)} /></Field>
          <Field label="Start date"><input type="date" style={{ width: '100%' }} value={f.startDate} onChange={(e) => set('startDate', e.target.value)} /></Field>
          <Field label="Target date"><input type="date" style={{ width: '100%' }} value={f.targetDate} onChange={(e) => set('targetDate', e.target.value)} /></Field>
        </div>
      </Modal>
    );
  }

  // ════════════════════════════════════════════════════════════════════════
  //  Run → card link picker  (shown by app.jsx when a run starts)
  // ════════════════════════════════════════════════════════════════════════
  const STOPWORDS = new Set(['the', 'and', 'for', 'with', 'add', 'new', 'this', 'that', 'from',
    'into', 'will', 'should', 'when', 'make', 'a', 'an', 'to', 'of', 'on', 'in', 'is', 'are',
    'be', 'as', 'by', 'or', 'it', 'request', 'enhancement', 'change', 'update', 'create', 'build']);
  function tokenize(s) {
    return ((s || '').toLowerCase().match(/[a-z0-9]{3,}/g) || []).filter(t => !STOPWORDS.has(t));
  }
  function scoreCards(cards, query) {
    const qset = new Set(tokenize(query));
    if (!qset.size) return cards.map(c => ({ card: c, score: 0 }));
    // Title matches count 3×, description matches 1× — a card whose *title*
    // overlaps the request is a stronger candidate than one matched only via
    // its description.
    return cards.map(c => {
      const title = new Set(tokenize(c.title || ''));
      const desc = new Set(tokenize(c.description || ''));
      let s = 0;
      qset.forEach(t => { if (title.has(t)) s += 3; else if (desc.has(t)) s += 1; });
      return { card: c, score: s };
    }).sort((a, b) => b.score - a.score);
  }

  function RunLinkModal({ runId, input, wfName, onClose, onLinked }) {
    const [projects, setProjects] = useState([]);
    const [cards, setCards] = useState([]);
    const [loading, setLoading] = useState(true);
    const [mode, setMode] = useState('link');       // 'link' | 'create'
    const [selectedId, setSelectedId] = useState(null);
    const [search, setSearch] = useState('');
    const [busy, setBusy] = useState(false);
    const [newProjectId, setNewProjectId] = useState('');
    const [newTitle, setNewTitle] = useState((input || '').trim().slice(0, 90));
    const [newStatus, setNewStatus] = useState('in_progress');

    useEffect(() => {
      (async () => {
        try {
          const [ps, cs] = await Promise.all([
            api('GET', '/api/pm/projects'),
            api('GET', '/api/pm/cards?unlinked=true'),
          ]);
          setProjects(ps); setCards(cs);
          if (ps.length) setNewProjectId(_mem.projectId && ps.find(p => p.id === _mem.projectId) ? _mem.projectId : ps[0].id);
          else setMode('create');
        } catch (e) { /* leave empty; create mode still works via project creation */ }
        finally { setLoading(false); }
      })();
    }, []);

    const scored = useMemo(() => scoreCards(cards, (input || '') + ' ' + (wfName || '')), [cards, input, wfName]);
    const suggestion = scored.length && scored[0].score > 0 ? scored[0].card : null;
    useEffect(() => { if (suggestion && selectedId == null) setSelectedId(suggestion.id); }, [suggestion]);

    const q = search.toLowerCase();
    const filtered = cards.filter(c =>
      !q || (c.title || '').toLowerCase().includes(q) ||
      (c.projectName || '').toLowerCase().includes(q));
    // Group filtered cards by project for an organized list.
    const groups = {};
    filtered.forEach(c => { (groups[c.projectName || 'Project'] = groups[c.projectName || 'Project'] || []).push(c); });

    async function doLink() {
      if (!selectedId) return;
      setBusy(true);
      try {
        await api('PATCH', `/api/pm/cards/${selectedId}`, { runId });
        const card = cards.find(c => c.id === selectedId);
        onLinked && onLinked({ cardId: selectedId, title: card ? card.title : '', projectName: card ? card.projectName : '' });
        onClose();
      } catch (e) { alert('Could not link: ' + e.message); setBusy(false); }
    }
    async function doCreate() {
      if (!newTitle.trim()) return;
      setBusy(true);
      try {
        let pid = newProjectId;
        if (!pid) {
          const p = await api('POST', '/api/pm/projects', { name: 'Delivery' });
          pid = p.id; _mem.projectId = pid;
        }
        const card = await api('POST', '/api/pm/cards', {
          projectId: pid, title: newTitle.trim(), status: newStatus,
          cardType: 'work_item', runId,
        });
        onLinked && onLinked({ cardId: card.id, title: card.title, projectName: (projects.find(p => p.id === pid) || {}).name || 'Delivery' });
        onClose();
      } catch (e) { alert('Could not create card: ' + e.message); setBusy(false); }
    }

    const tab = (id, label) => (
      <button onClick={() => setMode(id)} style={{
        flex: 1, padding: '8px 0', fontSize: '0.82rem', fontWeight: 500, cursor: 'pointer',
        background: mode === id ? '#1a2540' : 'transparent', color: mode === id ? '#c8a84a' : '#748090',
        border: '1px solid ' + (mode === id ? '#c8a84a' : '#273558'), borderRadius: 7,
      }}>{label}</button>
    );

    const cardRow = (c, isSuggestion) => (
      <div key={c.id} onClick={() => setSelectedId(c.id)} style={{
        display: 'flex', alignItems: 'center', gap: 8, padding: '8px 10px', borderRadius: 8,
        cursor: 'pointer', marginBottom: 6,
        background: selectedId === c.id ? 'rgba(200,168,74,0.1)' : '#111827',
        border: '1px solid ' + (selectedId === c.id ? '#c8a84a' : '#1e2d4a'),
      }}>
        <span style={{ fontSize: '0.85rem' }}>{typeMeta(c.cardType).icon}</span>
        <div style={{ flex: 1, minWidth: 0 }}>
          <div style={{ fontSize: '0.84rem', fontWeight: 500, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{c.title}</div>
          <div className="text-xs" style={{ color: '#5a6b8c' }}>{c.projectName}</div>
        </div>
        {isSuggestion && <Pill label="✦ Suggested" color="#c8a84a" />}
        {selectedId === c.id && !isSuggestion && <span style={{ color: '#c8a84a', fontSize: '0.9rem' }}>✓</span>}
      </div>
    );

    const footer = mode === 'link'
      ? [
          <button key="skip" className="btn btn-ghost btn-sm" onClick={onClose}>Not now</button>,
          <button key="link" className="btn btn-gold btn-sm" disabled={!selectedId || busy} onClick={doLink}>{busy ? 'Linking…' : 'Link run'}</button>,
        ]
      : [
          <button key="skip" className="btn btn-ghost btn-sm" onClick={onClose}>Not now</button>,
          <button key="create" className="btn btn-gold btn-sm" disabled={!newTitle.trim() || busy} onClick={doCreate}>{busy ? 'Creating…' : 'Create & link'}</button>,
        ];

    return (
      <Modal title="Run started — link it to delivery work?"
        subtitle={wfName ? ('Workflow: ' + wfName) : 'Track this run on the delivery board'}
        onClose={onClose} footer={footer}>
        <div className="row" style={{ gap: 8, marginBottom: 14 }}>
          {tab('link', 'Link to a card')}
          {tab('create', 'Create a new task')}
        </div>

        {mode === 'link' && (
          loading ? <div className="empty-state">Loading cards…</div> :
          !cards.length ? (
            <div className="text-muted" style={{ padding: '8px 0' }}>
              No unlinked cards available. Switch to <b>Create a new task</b> to add one for this run.
            </div>
          ) : (
            <div>
              {suggestion && (
                <div style={{ marginBottom: 12 }}>
                  <div className="inspector-section-title">Best match</div>
                  {cardRow(suggestion, true)}
                </div>
              )}
              <input style={{ width: '100%', marginBottom: 10 }} placeholder="Search cards…"
                value={search} onChange={(e) => setSearch(e.target.value)} />
              <div style={{ maxHeight: 300, overflowY: 'auto' }}>
                {Object.keys(groups).map(pname => (
                  <div key={pname} style={{ marginBottom: 10 }}>
                    <div className="text-xs" style={{ color: '#5a6b8c', textTransform: 'uppercase', letterSpacing: '0.05em', margin: '4px 0 6px' }}>{pname}</div>
                    {groups[pname].map(c => cardRow(c, false))}
                  </div>
                ))}
                {!filtered.length && <div className="text-xs">No cards match your search.</div>}
              </div>
            </div>
          )
        )}

        {mode === 'create' && (
          <div>
            <Field label="Task title">
              <input style={{ width: '100%' }} value={newTitle} onChange={(e) => setNewTitle(e.target.value)} autoFocus />
            </Field>
            <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
              <Field label="Project">
                {projects.length ? (
                  <select style={{ width: '100%' }} value={newProjectId} onChange={(e) => setNewProjectId(e.target.value)}>
                    {projects.map(p => <option key={p.id} value={p.id}>{p.name}</option>)}
                  </select>
                ) : (
                  <input style={{ width: '100%' }} value="Delivery (new)" disabled />
                )}
              </Field>
              <Field label="Column">
                <select style={{ width: '100%' }} value={newStatus} onChange={(e) => setNewStatus(e.target.value)}>
                  <option value="backlog">Backlog</option>
                  <option value="ready">Ready</option>
                  <option value="in_progress">In Progress</option>
                  <option value="review">In Review</option>
                </select>
              </Field>
            </div>
            <div className="text-xs">The new card will be linked to this run, so its generated deliverables show up on the card.</div>
          </div>
        )}
      </Modal>
    );
  }

  window.ProjectsPage = ProjectsPage;
  window.RunLinkModal = RunLinkModal;
})();
