// kova/app.jsx — root App · routing · auth · decisions state · keyboard map
// Wave B (shell) + C (organisms) + D (journey) wired together.

const { useState, useEffect, useCallback, useRef, useMemo } = React;
const {
  TopBar, Rail, RAIL_ITEMS,
  Drawer, Modal, CommandPalette, NotificationsDrawer, ShortcutsOverlay,
  ProfileOverlay, WorkspaceSwitcher, ToastStack,
  Btn, Chip, Kbd, Icon, VerificationStamp, RunBadge, StatusBadge,
  ConfidenceMeter, ApprovalChain, Tabs,
  AuthShell, LoginPage, SessionBootstrap, AccessPending, AccessDenied, IdpError, SignedOutPage,
  DevLoginPage, ROLES,
  HomePage, ApprovalsInbox, ApprovalsDetail, AskKovaPage,
  RunDetailPage,
  OrbitOverviewPage, OrbitBriefsPage, OrbitBriefDetailPage, OrbitSignalsPage,
  OrbitOpportunitiesPage,
  VaultOverviewPage, VaultQueuePage, VaultItemDetailPage, VaultActionDetailPage,
  VaultCeoDashboard,
  PriceOverviewPage, PriceCosignQueuePage, PriceCosignDetailPage,
  PriceRatesPage, PriceQuotesPage, PriceQuoteDetailPage,
  PriceMarginRiskPage, PriceSkuDetailPage,
  SEED_DECISIONS, SEED_ACTIVITY,
  SEED_HOM_DRAFTS, SEED_SIGNALS, SEED_OPPORTUNITIES,
  SEED_VAULT_ITEMS, SEED_VAULT_ACTIONS,
  SEED_PRICE_RATES, SEED_PRICE_QUOTES, SEED_PRICE_MARGIN_RISK,
  SEED_PRICING_COSIGN_QUEUE,
  SEED_KRAFT_BRIEFS,
} = window.K;

const ROUTES = ['home', 'orbit', 'vault', 'price', 'kraft',
                'approvals', 'monitor', 'connect', 'admin', 'wiki', 'library', 'ask'];

/* Subdomain → tenant resolution (architecture §1.2 + §4.2 step 8).
   Production: `acme.kova.app` → tenant `acme`.
   Local dev: localhost reads `?tenant=` query, defaults to `acme`. */
const TENANT_REGISTRY = {
  acme: { id: 'acme', name: 'Acme Jewels',     persona: 'Rohan Patel',  initials: 'RP' },
  beta: { id: 'beta', name: 'Beta Diamonds',   persona: 'Priya Shah',   initials: 'PS' },
  demo: { id: 'demo', name: 'Demo Workspace',  persona: 'Demo User',    initials: 'DU' },
};
const resolveTenant = () => {
  const host = (typeof window !== 'undefined' && window.location.hostname) || 'localhost';
  // Production: *.kova.app
  const m = host.match(/^([a-z0-9-]+)\.kova\.app$/i);
  if (m) {
    const id = m[1].toLowerCase();
    return TENANT_REGISTRY[id] || { id, name: id[0].toUpperCase() + id.slice(1), persona: 'CEO', initials: 'CE' };
  }
  // Local: read ?tenant= override
  if (host === 'localhost' || host === '127.0.0.1' || host.startsWith('192.168.')) {
    const params = new URLSearchParams(window.location.search);
    const id = (params.get('tenant') || 'acme').toLowerCase();
    return TENANT_REGISTRY[id] || { id, name: id[0].toUpperCase() + id.slice(1), persona: 'CEO', initials: 'CE' };
  }
  return TENANT_REGISTRY.acme;
};

/* Role × module access — which rail items a role can see/enter */
// Architecture §1.3 + §4.6 — non-CEO roles' primary surface IS their module,
// not /home. CEO is the only role with /home as a launchpad ("cross-company
// dashboards and approvals"). Other roles get their workspace as their home.
//
// Fix R4 · KOVA_DESIGN_STRATEGY.md role × module matrix grants each role
// read access to neighboring modules so cross-module references (e.g., a PL
// clicking a source-ORBIT-signal from a cosign item) actually navigate.
// We honor that here: each role's include list contains its edit/approve
// modules AND the read-only ones it must be able to traverse.
const ROLE_MODULES = {
  // CEO has `kraft: approve` (KOVA_DESIGN_STRATEGY §5.3) — they approve KRAFT
  // launch packs in /approvals, but do not enter the KRAFT studio workspace.
  // Excluding kraft hides it from the rail; /approvals/:id for a kraft
  // decision still resolves because the route guard checks `approvals`, not
  // the source module.
  ceo:           { all: true, exclude: ['kraft'] },
  hom:           { all: false, include: ['orbit', 'vault', 'price', 'kraft', 'wiki', 'library'] },
  designer:      { all: false, include: ['kraft', 'orbit', 'wiki', 'library'] },
  inventory:     { all: false, include: ['vault', 'orbit', 'price', 'approvals', 'wiki', 'library'] },
  pricing:       { all: false, include: ['price', 'orbit', 'vault', 'approvals', 'wiki', 'library'] },
  admin:         { all: false, include: ['admin', 'connect', 'monitor', 'wiki', 'library'] },
  implementation:{ all: false, include: ['monitor', 'connect', 'admin', 'wiki', 'library'] },
};
const canAccess = (role, modId) => {
  const conf = ROLE_MODULES[role] || ROLE_MODULES.ceo;
  if (conf.all) return !conf.exclude?.includes(modId);
  return conf.include.includes(modId);
};

/* Flow indicator — always-visible pill showing CEO journey stage.
   Removable for production by passing `false` to <App showFlow>. */
const FlowIndicator = ({ stage }) => {
  const steps = [
    ['login',     '1 · Login'],
    ['authed',    '2 · Dashboard'],
    ['signed-out','3 · Signed out'],
  ];
  // bootstrap, pending, denied, idp-error all roll up into stage 1 (login flow)
  const effective = ['bootstrap', 'pending', 'denied', 'idp-error'].includes(stage) ? 'login' : stage;
  const order = ['login', 'authed', 'signed-out'];
  const curIdx = order.indexOf(effective);
  return (
    <div className="k-flow-indicator">
      {steps.map(([id, label], i) => (
        <React.Fragment key={id}>
          {i > 0 && <span className="k-flow-sep">›</span>}
          <span className={`k-flow-step${id === effective ? ' is-on' : (i < curIdx ? ' is-done' : '')}`}>
            {label}
          </span>
        </React.Fragment>
      ))}
    </div>
  );
};

/* hash → { route, subId } */
const parseHash = () => {
  const h = (location.hash || '').replace(/^#/, '');
  const [r, ...rest] = h.split('/');
  return {
    route: ROUTES.includes(r) ? r : 'home',
    subId: rest.length ? rest.join('/') : null,
  };
};

const CMDK_SECTIONS_BASE = [
  { group: 'Jump to', items: [
    { id: 'home',      label: 'Home',       mod: 'home',      kbd: 'g h' },
    { id: 'orbit',     label: 'Orbit',      mod: 'orbit',     kbd: 'g o' },
    { id: 'vault',     label: 'Vault',      mod: 'vault',     kbd: 'g v' },
    { id: 'price',     label: 'Price',      mod: 'price',     kbd: 'g p' },
    { id: 'kraft',     label: 'Kraft',      mod: 'kraft',     kbd: 'g k' },
    { id: 'approvals', label: 'Approvals',  mod: 'approvals', kbd: 'g a' },
    { id: 'monitor',   label: 'Monitor',    mod: 'monitor',   kbd: 'g m' },
    { id: 'connect',   label: 'Connect',    mod: 'connect' },
    { id: 'admin',     label: 'Admin',      mod: 'admin' },
    { id: 'wiki',      label: 'Wiki',       mod: 'wiki' },
    { id: 'library',   label: 'Design library', mod: 'home',  kbd: 'g l' },
  ]},
  { group: 'Actions', items: [
    { id: 'approve-next', label: 'Approve next decision',     mod: 'approvals', kbd: '⌘ ⏎' },
    { id: 'evidence',     label: 'Open evidence drawer',      mod: 'monitor',   kbd: '⌘ E' },
    { id: 'run-brief',    label: 'Run Orbit daily brief now', mod: 'orbit' },
    { id: 'scan-vault',   label: 'Run Vault deadstock scan',  mod: 'vault' },
  ]},
  { group: 'Account', items: [
    { id: 'profile',     label: 'Profile & preferences',     kbd: '⌘ ,',     kw: ['profile', 'preferences', 'settings', 'account', 'me'] },
    { id: 'switch-ws',   label: 'Switch workspace',          kbd: '⌘ ⇧ O',   kw: ['workspace', 'tenant', 'switch', 'company'] },
    { id: 'shortcuts',   label: 'Keyboard shortcuts',        kbd: '?',       kw: ['help', 'keys', 'shortcuts'] },
    { id: 'sign-out',    label: 'Sign out',                                  kw: ['logout', 'sign out', 'exit'] },
  ]},
];


const App = () => {
  /* ---------- auth ---------- */
  const [authStage, setAuthStage] = useState('login');
  const [authCtx, setAuthCtx] = useState(() => {
    const t = resolveTenant();
    return {
      method: null,
      tenantId: t.id,
      tenant: t.name,
      email: '',
      role: 'ceo',
      persona: t.persona,
      initials: t.initials,
    };
  });
  const [avatarMenuOpen, setAvatarMenuOpen] = useState(false);

  const onAuthStart = useCallback((ctx) => {
    if (ctx._devLogin) {
      setAuthCtx(c => ({ ...c, method: 'dev' }));
      setAuthStage('dev-login');
      return;
    }
    setAuthCtx(c => ({ ...c, ...ctx }));
    setAuthStage('bootstrap');
  }, []);

  const onPickDevRole = useCallback((role) => {
    setAuthCtx(c => ({
      ...c,
      method: 'dev',
      role: role.id,
      persona: role.persona,
      initials: role.persona.split(' ').map(w => w[0]).join('').slice(0, 2).toUpperCase(),
    }));
    setAuthStage('bootstrap');
    // Land the role on its primary surface · architecture §4.6 default workspaces
    history.replaceState(null, '', `#${role.mod}`);
    _setRouteState({ route: role.mod, subId: null });
  }, []);
  const onBootstrapReady = useCallback(() => setAuthStage('authed'), []);
  const onSignOut = useCallback(() => {
    // Explicit logout page — architecture v7 §4.3 lists /sign-out as a route.
    setAuthStage('signed-out');
    setAvatarMenuOpen(false);
    // Wipe per-session state so a fresh sign-in feels fresh
    history.replaceState(null, '', '#');
  }, []);
  const onSignInAgain = useCallback(() => setAuthStage('login'), []);

  /* ---------- workspace / tenant switching ----------
     Architecture §1.2 — tenant resolution is by subdomain in production
     (`{tenantId}.kova.app`). The frontend hands the user a redirect; the
     destination tenant's worker resolves the new context fresh. Locally we
     rewrite `?tenant=` and rehydrate the session in-place. */
  const onPickTenant = useCallback((tenant) => {
    const host = (typeof window !== 'undefined' && window.location.hostname) || 'localhost';
    setWswOpen(false);
    if (host === 'localhost' || host === '127.0.0.1' || host.startsWith('192.168.')) {
      // Local rehydrate
      setAuthCtx(c => ({ ...c, tenantId: tenant.id, tenant: tenant.name,
                              persona: tenant.persona || c.persona,
                              initials: (tenant.persona || c.persona || 'CE')
                                .split(' ').map(w => w[0]).join('').slice(0, 2).toUpperCase() }));
      const url = new URL(window.location.href);
      url.searchParams.set('tenant', tenant.id);
      history.replaceState(null, '', url.toString());
      pushToast({ kind: 'success', title: `Switched to ${tenant.name}`,
                  body: 'Tenant context rehydrated for this session.' });
    } else {
      // Production redirect to {tenantId}.kova.app
      const target = `https://${tenant.id}.kova.app${window.location.pathname}${window.location.hash}`;
      pushToast({ kind: 'info', title: `Switching to ${tenant.name}…`,
                  body: `Redirecting to ${tenant.id}.kova.app` });
      setTimeout(() => { window.location.href = target; }, 400);
    }
  }, []);

  const onPickWorkspace = useCallback((ws) => {
    setWswOpen(false);
    if (ws.module) {
      setRoute(ws.module);
      pushToast({ kind: 'success', title: `Opened ${ws.name}`,
                  body: `${ws.module.toUpperCase()} · ${ws.scope || 'private'} workspace` });
    }
  }, []);

  /* ---------- routing (hash → route + subId) ---------- */
  const [{ route, subId }, _setRouteState] = useState(parseHash);
  const setRoute = useCallback((r, sub) => {
    const hash = sub ? `${r}/${sub}` : r;
    history.replaceState(null, '', `#${hash}`);
    _setRouteState({ route: r, subId: sub || null });
    window.scrollTo({ top: 0, behavior: 'instant' });
  }, []);
  useEffect(() => {
    const onHash = () => _setRouteState(parseHash());
    window.addEventListener('hashchange', onHash);
    return () => window.removeEventListener('hashchange', onHash);
  }, []);

  /* ---------- decisions state ---------- */
  const [decisions, setDecisions] = useState(() => SEED_DECISIONS.map(d => ({ ...d })));

  /* ---------- KRAFT briefs state · Studio OS inbound queue ----------
     Briefs from HOM / VAULT / ORBIT / CEO land here as first-class objects.
     The designer triages → accepts → designs → submits; CEO feedback syncs
     back onto the brief (see decision-action handlers below). */
  const [briefs, setBriefs] = useState(() => SEED_KRAFT_BRIEFS.map(b => ({ ...b })));
  const updateBrief = useCallback((id, patch) => {
    setBriefs(prev => prev.map(b => (b.id === id
      ? { ...b, ...(typeof patch === 'function' ? patch(b) : patch) }
      : b)));
  }, []);
  const createBrief = useCallback((brief) => {
    setBriefs(prev => [{ ...brief }, ...prev]);
  }, []);
  // Reflect a CEO decision (on a kraft launch pack) back onto its source brief,
  // and notify the designer (closes the feedback loop home — see KRAFT Studio OS).
  const syncBriefFromDecision = useCallback((launchPackId, status, note, who = 'You · CEO') => {
    if (!launchPackId) return;
    let hit = null;
    setBriefs(prev => prev.map(b => {
      if (b.launchPackId !== launchPackId) return b;
      hit = b;
      const at = 'just now';
      return {
        ...b, status,
        thread: note ? [...(b.thread || []), { who, role: 'ceo', at, text: note }] : b.thread,
        history: [...(b.history || []), { state: status, at }],
      };
    }));
    if (hit) {
      const label = status === 'changes_requested'
        ? `Kraft · changes requested · ${hit.title}`
        : status === 'approved' || status === 'in_production'
          ? `Kraft · launch pack approved · ${hit.title}`
          : `Kraft · ${status.replace(/_/g, ' ')} · ${hit.title}`;
      setNotifications(ns => [
        { id: 'n-brf-' + Math.random().toString(36).slice(2, 6), mod: 'kraft', kind: 'run',
          title: label, meta: 'just now', read: false, hash: 'kraft' },
        ...ns,
      ]);
    }
  }, []);

  /* ---------- HoM (ORBIT) state · drafts · signals · opportunities ---------- */
  const [drafts,        setDrafts]        = useState(() => SEED_HOM_DRAFTS.map(d => ({ ...d })));
  const [signals,       setSignals]       = useState(() => SEED_SIGNALS.map(s => ({ ...s })));
  const [opportunities, setOpportunities] = useState(() => SEED_OPPORTUNITIES.map(o => ({ ...o })));

  const updateDraft = useCallback((id, patch) => {
    setDrafts(prev => prev.map(d => (d.id === id ? { ...d, ...patch(d) } : d)));
  }, []);
  const updateSignal = useCallback((id, patch) => {
    setSignals(prev => prev.map(s => (s.id === id ? { ...s, ...patch(s) } : s)));
  }, []);

  /* ---------- IM (VAULT) state · items · action drafts ---------- */
  const [vaultItems,   setVaultItems]   = useState(() => SEED_VAULT_ITEMS.map(i => ({ ...i })));
  const [vaultActions, setVaultActions] = useState(() => SEED_VAULT_ACTIONS.map(a => ({ ...a })));

  const updateVaultAction = useCallback((id, patch) => {
    setVaultActions(prev => prev.map(a => (a.id === id ? { ...a, ...patch(a) } : a)));
  }, []);

  /* ---------- PL (PRICE) state · rates · quotes · margin risk · cosign queue ---------- */
  const [priceRates,         setPriceRates]         = useState(() => SEED_PRICE_RATES.map(r => ({ ...r })));
  const [priceQuotes,        setPriceQuotes]        = useState(() => SEED_PRICE_QUOTES.map(q => ({ ...q })));
  const [marginRiskItems,    setMarginRiskItems]    = useState(() => SEED_PRICE_MARGIN_RISK.map(r => ({ ...r })));
  const [pricingCosignQueue, setPricingCosignQueue] = useState(() => SEED_PRICING_COSIGN_QUEUE.map(c => ({ ...c })));

  const updatePriceQuote = useCallback((id, patch) => {
    setPriceQuotes(prev => prev.map(q => (q.id === id ? { ...q, ...patch(q) } : q)));
  }, []);
  const updatePricingCosign = useCallback((id, patch) => {
    setPricingCosignQueue(prev => prev.map(c => (c.id === id ? { ...c, ...patch(c) } : c)));
  }, []);
  const [drawerDecisionId, setDrawerDecisionId] = useState(null);  // which decision's evidence to show
  const [rejectId, setRejectId] = useState(null);                  // which decision the modal is targeting
  const [rejectReason, setRejectReason] = useState('Delhi storefront is mid-renovation through 28 May; transfer next week.');

  const currentDetail = useMemo(
    () => decisions.find(d => d.id === subId),
    [decisions, subId]
  );
  const drawerDecision = useMemo(
    () => decisions.find(d => d.id === drawerDecisionId) || decisions.find(d => d.status === 'pending'),
    [decisions, drawerDecisionId]
  );

  /* ---------- approve / reject ---------- */
  const updateDecision = useCallback((id, patch) => {
    setDecisions(prev => prev.map(d => (d.id === id ? { ...d, ...patch(d) } : d)));
  }, []);

  // Approve & sign animates the workflow timeline through Execute → Outcome
  const approveDecision = useCallback((id) => {
    const d = decisions.find(x => x.id === id);
    if (!d || d.status !== 'pending') return;

    // Phase 1: status executing · workflow step 4 done, step 5 now
    updateDecision(id, () => ({
      status: 'executing',
      statusLabel: 'Executing · Connector',
      workflow: d.workflow.map((s, i) => ({
        ...s,
        state: i < 5 ? 'done' : i === 5 ? 'now' : 'pending',
        time:  i === 4 ? 'just now' : (i === 5 ? 'now' : s.time),
      })),
    }));
    pushToast({ kind: 'success', title: `Approved · ${id}`, body: 'Workflow queued for CONNECT.' });

    // Phase 2: step 5 done, step 6 now
    setTimeout(() => {
      updateDecision(id, (cur) => ({
        statusLabel: 'Outcome tracking',
        workflow: cur.workflow.map((s, i) => ({
          ...s,
          state: i < 6 ? 'done' : i === 6 ? 'now' : 'pending',
          time:  i === 5 ? 'just now' : (i === 6 ? 'now' : s.time),
        })),
      }));
    }, 1600);

    // Phase 3: outcome done, status approved
    setTimeout(() => {
      updateDecision(id, (cur) => ({
        status: 'approved',
        statusLabel: 'Signed · executed',
        runWhen: 'just now',
        workflow: cur.workflow.map((s, i) => ({ ...s, state: 'done', time: i === 6 ? 'just now' : s.time })),
        chain: cur.chain.map((step, i) =>
          i === cur.chain.length - 1 || step.state === 'now'
            ? { ...step, state: 'done', meta: step.meta.replace('Awaiting', 'Signed') }
            : step
        ),
      }));
      // KRAFT Studio OS feedback loop: a signed launch pack → brief approved.
      if (d.mod === 'kraft') syncBriefFromDecision(id, 'approved', 'Approved — routed to execution.');
    }, 3200);
  }, [decisions, syncBriefFromDecision]);

  const rejectDecision = useCallback((id, reason) => {
    updateDecision(id, (cur) => ({
      status: 'rejected',
      statusLabel: 'Returned · revise',
      runWhen: 'just now',
      workflow: cur.workflow.map((s, i) => ({
        ...s,
        state: i < 4 ? 'done' : i === 4 ? 'failed' : 'pending',
        time:  i === 4 ? 'just now' : s.time,
      })),
      chain: [
        ...cur.chain.slice(0, 2),
        { who: 'You · CEO', meta: `Rejected · ${reason || 'see note'}`, state: 'done' },
        { who: 'Returned to author', meta: 'Awaiting revision', state: 'pending' },
      ],
    }));
    pushToast({ kind: 'warn', title: `Rejected · ${id}`, body: 'Returned to author with your note.' });
    // KRAFT Studio OS feedback loop: a returned launch pack → brief changes_requested.
    const d = decisions.find(x => x.id === id);
    if (d?.mod === 'kraft') syncBriefFromDecision(id, 'changes_requested', reason || 'Changes requested.');
  }, [decisions, syncBriefFromDecision]);

  /* ---------- HoM workflow handlers (Wave E · ORBIT) ---------- */
  // Submit draft → cross-module routing per architecture §2 runtime loop.
  // Fix R1 · if the chain demands Pricing co-sign, the brief must land in
  // pricingCosignQueue first (PL surface) BEFORE reaching CEO. Only items
  // that need no Pricing review go directly to /api/decisions.
  const submitDraft = useCallback((id) => {
    const d = drafts.find(x => x.id === id);
    if (!d) return;

    const needsPricing  = d.cosign?.includes('Pricing');
    const submittedBy   = `${(authCtx.persona || 'Priya').split(' ')[0]} · HoM`;
    const baseDoneChain = d.chain.filter(s => s.state === 'done');
    const baseWorkflow  = d.workflow.map((s, i) => ({
      ...s,
      state: i <= 4 ? 'done' : i === 5 ? 'now' : 'pending',
      time:  i === 4 ? 'just now' : i === 5 ? 'now' : s.time,
    }));

    if (needsPricing) {
      // → PL queue · normalize to PricingCosignItem shape (architecture §13·7)
      const cosignItem = {
        ...d,
        sourceModule: 'orbit',
        sourceRunId:  d.runId,
        type:         'ceiling_change',
        mod:          'orbit',
        stamp:        d.stamp || 'Verified · Orbit submitted',
        status:       'pending',
        statusLabel:  'Awaiting Pricing co-sign',
        runWhen:      'just now',
        submittedBy,
        ceilingCheck: 'policy_change',
        chain: [
          ...baseDoneChain,
          { who: submittedBy,             meta: 'Submitted · just now',          state: 'done' },
          { who: 'You · Pricing Lead',    meta: 'Awaits co-sign',                state: 'now' },
          { who: 'CEO',                   meta: 'Final approval',                state: 'pending' },
          { who: 'CONNECT · execute',     meta: 'Applies on approval',           state: 'pending' },
        ],
        workflow: baseWorkflow,
      };
      setPricingCosignQueue(prev => [cosignItem, ...prev]);
      setDrafts(prev => prev.filter(x => x.id !== id));
      pushToast({ kind: 'success', title: `Submitted · ${id}`, body: 'Moved to Pricing co-sign queue.' });
    } else {
      // → CEO directly
      const promoted = {
        ...d,
        status: 'pending',
        statusLabel: 'Pending CEO sign',
        stamp: d.stamp || 'Verified · Orbit daily brief',
        runWhen: 'just now',
        chain: [
          ...baseDoneChain,
          { who: submittedBy,             meta: 'Submitted · just now',         state: 'done' },
          { who: 'CEO',                   meta: 'Awaiting sign · ⌘⏎',           state: 'now' },
          { who: 'Logistics · execution', meta: 'Will queue on approval',       state: 'pending' },
        ],
        workflow: baseWorkflow,
      };
      setDecisions(prev => [promoted, ...prev]);
      setDrafts(prev => prev.filter(x => x.id !== id));
      pushToast({ kind: 'success', title: `Submitted · ${id}`, body: 'Moved to CEO Approvals.' });
    }
    setRoute('orbit');
  }, [drafts, authCtx]);

  // Discard a draft. Calls POST /api/orbit/briefs/:id/discard.
  const discardDraft = useCallback((id) => {
    setDrafts(prev => prev.filter(x => x.id !== id));
    pushToast({ kind: 'warn', title: `Discarded · ${id}`, body: 'Draft removed. The original signals remain.' });
    setRoute('orbit');
  }, []);

  // Refine with agent — opens the existing Revise modal (reuses the CEO flow).
  // The handler bumps draft version after the modal confirms.
  const refineDraft = useCallback((id) => {
    setReviseId(id);
    setReviseConstraint('Try a different cohort or storefront — refine recommendation.');
  }, []);

  // Signal triage actions · architecture §8.2 buttons.
  const sendSignalToVault = useCallback((id) => {
    updateSignal(id, () => ({ status: 'triaged' }));
    pushToast({ kind: 'info', title: 'Signal sent to VAULT', body: `Signal ${id} forwarded as a triage candidate.` });
  }, [updateSignal]);

  const saveSignalToWiki = useCallback((id) => {
    updateSignal(id, () => ({ status: 'triaged' }));
    pushToast({ kind: 'info', title: 'Saved to WIKI', body: `Signal ${id} queued as a memory candidate.` });
  }, [updateSignal]);

  const createBriefFromSignal = useCallback((id) => {
    const sig = signals.find(s => s.id === id);
    if (!sig) return;
    const newDraft = {
      id: `rcp-${Math.floor(Math.random() * 9000 + 1000)}`,
      mod: 'orbit',
      priority: 'P2',
      impact: 'launch',
      stamp: 'Drafted · Signal triage',
      status: 'pending',
      statusLabel: 'Drafting · awaiting submit',
      runId: `rcp-${Math.floor(Math.random() * 9000 + 1000)}`,
      runVersion: 'v1',
      runWhen: 'just now',
      title: `Brief from signal · ${sig.title}`,
      reasoning: `Auto-drafted from signal ${id}. ${sig.description} Refine the cohort, tactic, and projected impact before submitting.`,
      stats: sig.metrics?.length ? sig.metrics.slice(0, 4) : [
        { label: 'Source', value: sig.source.toUpperCase() },
        { label: 'Confidence', value: ['Low', 'Med', 'High', 'Verified'][sig.confidence - 1] },
        { label: 'Tagged', value: sig.modulesTagged.join(' · ').toUpperCase() },
        { label: 'Freshness', value: sig.freshness },
      ],
      confidence: sig.confidence,
      confidenceLabel: ['Low', 'Medium', 'High', 'Verified'][sig.confidence - 1],
      cosign: 'You · Merch',
      chain: [
        { who: 'Auto · evidence sweep',  meta: 'Signal context attached', state: 'done' },
        { who: 'You · HoM',              meta: 'Drafting · refine and submit', state: 'now' },
        { who: 'CEO',                    meta: 'Awaits your submit', state: 'pending' },
      ],
      workflow: ['Queued','Context built','Agent ran','Verified','HoM review','CEO approval','Outcome tracked'].map((label, i) => ({
        label,
        step: ['queued','context_built','agent_ran','verified','hom_review','awaiting_approval','outcome_tracked'][i],
        meta: ['workflow.start','signal attached','orbit_brief_agent v2.1','signal sources confirmed','HoM · drafting','CEO sign','curator records'][i],
        time: ['just now','just now','just now','just now','now','—','—'][i],
        state: i < 4 ? 'done' : i === 4 ? 'now' : 'pending',
      })),
      claims: [
        { text: sig.description, source: sig.source, receipt: `${id} / signal` },
      ],
      receipts: [
        { tool: 'orbit.signal', scope: sig.source, id: `signal-receipt-${id}` },
      ],
      math: sig.metrics?.map(m => ({ expr: m.label.toLowerCase(), value: m.value })) || [],
    };
    setDrafts(prev => [newDraft, ...prev]);
    updateSignal(id, () => ({ status: 'triaged' }));
    pushToast({ kind: 'success', title: 'Draft created from signal', body: `Open the draft to refine and submit.` });
    setRoute('orbit', newDraft.id);
  }, [signals, updateSignal]);

  const dismissSignal = useCallback((id) => {
    updateSignal(id, () => ({ status: 'dismissed' }));
    pushToast({ kind: 'info', title: 'Signal dismissed', body: 'Marked as low-priority. Stays visible under the Dismissed tab.' });
  }, [updateSignal]);

  // Frontend fixture pool · 2 realistic briefs that the orbit.daily_brief workflow
  // would produce. Each Generate Brief click pulls the next one and animates the
  // 4-step pre-verification (queued → context → agent → verified) before placing
  // the new draft in HoM's queue.
  const GENERATE_POOL = useRef([
    {
      title: 'Open Pune trial window for editorial-line necklaces',
      reasoning: 'Pune cohort sentiment up 18 % vs. forecast in the last 9 days. Editorial-line necklaces have 12 SKUs at the margin floor that haven\'t been featured in store this quarter.',
      stats: [
        { label: 'Cohort',     value: 'Pune · 28-44' },
        { label: 'SKUs',       value: '12' },
        { label: 'Projection', value: '+ ₹6.8 L', color: 'success' },
        { label: 'Window',     value: '3 weeks' },
      ],
      confidence: 3, confidenceLabel: 'High',
      cosign: 'You · Merch',
    },
    {
      title: 'Pre-Diwali stack-ring campaign · Bengaluru + Hyderabad',
      reasoning: 'Two cities show 26 % search-to-engagement lift on stackable rings over 14 days. Pre-Diwali window aligns with vendor capacity for new SKUs.',
      stats: [
        { label: 'Cities',     value: 'BLR · HYD' },
        { label: 'SKUs new',   value: '8 prototypes' },
        { label: 'Projection', value: '+ ₹11.2 L', color: 'success' },
        { label: 'Lead time',  value: '5 weeks' },
      ],
      confidence: 2, confidenceLabel: 'Medium',
      cosign: 'You · Merch',
    },
  ]);
  const generatePoolIdx = useRef(0);

  // Generate a fresh ORBIT draft. Backed by
  // POST /api/workflows/orbit.daily_brief/start (see kova-openapi.yaml).
  const generateBrief = useCallback(() => {
    pushToast({ kind: 'info', title: 'Orbit brief generation queued', body: 'orbit.daily_brief · context build → agent run · ~3 s' });

    const idx = generatePoolIdx.current % GENERATE_POOL.current.length;
    generatePoolIdx.current += 1;
    const tpl = GENERATE_POOL.current[idx];
    const newId = `rcp-${Math.floor(Math.random() * 9000 + 1000)}`;

    // Insert the draft immediately in a "running" state so HoM sees workflow progress.
    const initialWorkflow = ['Queued','Context built','Agent ran','Verified','HoM review','CEO approval','Outcome tracked'];
    const baseSteps = (states) => initialWorkflow.map((label, i) => ({
      label,
      step: ['queued','context_built','agent_ran','verified','hom_review','awaiting_approval','outcome_tracked'][i],
      meta: ['workflow.start','context building…','orbit_brief_agent v2.1','verifier · running','HoM · drafting','CEO sign','curator records'][i],
      time: states[i].time,
      state: states[i].state,
    }));

    const draft = {
      id: newId,
      mod: 'orbit', priority: 'P1', impact: 'launch',
      stamp: 'Drafted · Orbit daily brief',
      status: 'pending',
      statusLabel: 'Generating · agent running',
      runId: newId, runVersion: 'v1', runWhen: 'just now',
      title: tpl.title,
      reasoning: tpl.reasoning,
      stats: tpl.stats,
      confidence: tpl.confidence, confidenceLabel: tpl.confidenceLabel,
      cosign: tpl.cosign,
      chain: [
        { who: 'Orbit · daily scan',    meta: 'Workflow started · just now', state: 'now' },
        { who: 'Auto · evidence sweep', meta: 'pending',                     state: 'pending' },
        { who: 'You · HoM',             meta: 'Awaits draft',                state: 'pending' },
        { who: 'CEO',                   meta: 'Awaits submit',               state: 'pending' },
      ],
      workflow: baseSteps([
        { time: 'now',     state: 'now'     },
        { time: '—',       state: 'pending' },
        { time: '—',       state: 'pending' },
        { time: '—',       state: 'pending' },
        { time: '—',       state: 'pending' },
        { time: '—',       state: 'pending' },
        { time: '—',       state: 'pending' },
      ]),
      claims: [], receipts: [], math: [],
    };
    setDrafts(prev => [draft, ...prev]);

    // Phase 2 (1.2s) → context built · agent now running
    setTimeout(() => updateDraft(newId, () => ({
      workflow: baseSteps([
        { time: 'just now', state: 'done'    },
        { time: 'just now', state: 'done'    },
        { time: 'now',      state: 'now'     },
        { time: '—',        state: 'pending' },
        { time: '—',        state: 'pending' },
        { time: '—',        state: 'pending' },
        { time: '—',        state: 'pending' },
      ]),
    })), 1200);

    // Phase 3 (2.6s) → verified, ready for HoM review
    setTimeout(() => updateDraft(newId, () => ({
      statusLabel: 'Drafting · awaiting submit',
      workflow: baseSteps([
        { time: 'just now', state: 'done' },
        { time: 'just now', state: 'done' },
        { time: 'just now', state: 'done' },
        { time: 'just now', state: 'done' },
        { time: 'now',      state: 'now'  },
        { time: '—',        state: 'pending' },
        { time: '—',        state: 'pending' },
      ]),
      chain: [
        { who: 'Orbit · daily scan',    meta: 'Workflow ran · just now',     state: 'done' },
        { who: 'Auto · evidence sweep', meta: '3 / 3 claims sourced',         state: 'done' },
        { who: 'You · HoM',             meta: 'Drafting · ready to submit',   state: 'now'  },
        { who: 'CEO',                   meta: 'Awaits your submit',           state: 'pending' },
      ],
      claims: tpl.stats.slice(0, 3).map((s, i) => ({
        text: `${s.label} · ${s.value}`, source: 'sales.read', receipt: `${newId} / claim ${i + 1}`,
      })),
      receipts: [
        { tool: 'sql.query',   scope: 'sales.read',     id: `tool-receipt-${Math.floor(Math.random() * 9000)}` },
        { tool: 'wiki.lookup', scope: 'wiki.read',      id: `tool-receipt-${Math.floor(Math.random() * 9000)}` },
      ],
    })), 2600);

    setTimeout(() => {
      pushToast({ kind: 'success', title: 'Brief ready · review',
                  body: `${newId} · ${tpl.title.slice(0, 48)}…` });
    }, 2700);
  }, [updateDraft]);

  // Convert an opportunity along a named path. Backed by
  // POST /api/orbit/opportunities/:id/convert (see kova-openapi.yaml).
  const convertOpportunity = useCallback((id, path) => {
    const opp = opportunities.find(o => o.id === id);
    if (!opp) return;
    const chosenPath = path || opp.conversionPaths?.[0];
    if (!chosenPath) return;

    // Mark the opportunity converted
    setOpportunities(prev => prev.map(o => (o.id === id ? { ...o, status: 'converted' } : o)));

    // Dispatch by path · each path maps to a different backend endpoint
    const TARGETS = {
      create_brief:   { module: 'orbit', title: 'Converted to draft brief', body: `${id} → new ORBIT draft`,
                       sideEffect: () => {
                         // Build a draft scaffold from the opportunity's first signal
                         const sig = opp.relatedSignals?.[0]
                           ? signals.find(s => s.id === opp.relatedSignals[0])
                           : null;
                         const newDraft = {
                           id: `rcp-${Math.floor(Math.random() * 9000 + 1000)}`,
                           mod: 'orbit', priority: 'P2', impact: 'launch',
                           stamp: 'Drafted · Opportunity converted',
                           status: 'pending', statusLabel: 'Drafting · awaiting submit',
                           runId: `rcp-${Math.floor(Math.random() * 9000 + 1000)}`,
                           runVersion: 'v1', runWhen: 'just now',
                           title: opp.title,
                           reasoning: `${opp.description}  Converted from opportunity ${id}; refine and submit.`,
                           stats: [
                             { label: 'Projected impact', value: opp.projectedImpact.value, color: 'success' },
                             { label: 'Confidence',       value: ['Low','Medium','High','Verified'][opp.confidence - 1] },
                             { label: 'Source signals',   value: String(opp.relatedSignals?.length || 0) },
                             { label: 'Expires',          value: new Date(opp.expiresAt).toLocaleDateString('en-IN', { day: 'numeric', month: 'short' }) },
                           ],
                           confidence: opp.confidence,
                           confidenceLabel: ['Low','Medium','High','Verified'][opp.confidence - 1],
                           cosign: 'You · Merch',
                           chain: [
                             { who: 'Auto · opportunity converter', meta: `From ${id}`, state: 'done' },
                             { who: 'You · HoM', meta: 'Drafting · refine and submit', state: 'now' },
                             { who: 'CEO', meta: 'Awaits your submit', state: 'pending' },
                           ],
                           workflow: ['Queued','Context built','Agent ran','Verified','HoM review','CEO approval','Outcome tracked'].map((label, i) => ({
                             label,
                             step: ['queued','context_built','agent_ran','verified','hom_review','awaiting_approval','outcome_tracked'][i],
                             meta: ['workflow.start','opportunity attached','orbit_brief_agent v2.1','sources merged','HoM · drafting','CEO sign','curator records'][i],
                             time: ['just now','just now','just now','just now','now','—','—'][i],
                             state: i < 4 ? 'done' : i === 4 ? 'now' : 'pending',
                           })),
                           claims: sig ? [{ text: sig.description, source: sig.source, receipt: `${sig.id} / signal` }] : [],
                           receipts: [{ tool: 'orbit.opportunity', scope: 'system', id: `opp-receipt-${id}` }],
                           math: [],
                         };
                         setDrafts(prev => [newDraft, ...prev]);
                         setRoute('orbit', `briefs/${newDraft.id}`);
                       }},
      send_to_vault:  { module: 'vault', title: 'Sent to VAULT', body: `${id} routed as triage candidate` },
      send_to_kraft:  { module: 'kraft', title: 'Sent to KRAFT', body: `${id} → KRAFT brief inbox`,
                        sideEffect: () => {
                          const s0 = opp.relatedSignals?.[0] ? signals.find(s => s.id === opp.relatedSignals[0]) : null;
                          const bid = 'brf-' + Math.random().toString(36).slice(2, 6);
                          createBrief({
                            id: bid, title: opp.title || `Design brief from ${id}`,
                            intent: opp.description || s0?.description || 'Translate this signal into design directions.',
                            source: { module: 'orbit', role: 'hom', person: 'Priya Shah' },
                            origin: { type: 'signal', refId: s0?.id || id, label: opp.title || id },
                            constraints: { pieceType: '', metal: '', karat: '', weightCeilingG: null, stone: '', motif: '', occasion: '', budget: null, currency: '₹' },
                            references: [], priority: 'mid', dueDays: 7, createdAt: 'just now', assignee: 'Vikram',
                            status: 'new', sessionId: null, launchPackId: null,
                            thread: [{ who: 'Priya Shah', role: 'hom', at: 'just now', text: 'Sent from ORBIT — please turn this into directions.' }],
                            history: [{ state: 'new', at: 'just now' }],
                          });
                        } },
      cosign_pricing: { module: 'price', title: 'Sent to Pricing co-sign', body: `${id} queued for ceiling review` },
      save_to_wiki:   { module: 'wiki',  title: 'Saved to WIKI', body: `${id} queued as memory candidate` },
    };
    const t = TARGETS[chosenPath];
    if (t?.sideEffect) t.sideEffect();
    pushToast({ kind: 'success', title: t?.title || 'Opportunity converted', body: t?.body || '' });
  }, [opportunities, signals, createBrief]);

  // From a brief, forward to other modules (architecture §8.2 buttons).
  const sendBriefToVault = useCallback((id) => {
    pushToast({ kind: 'info', title: 'Signal sent to VAULT', body: `Brief ${id} forwarded as a triage candidate.` });
  }, []);
  const createKraftBriefFromBrief = useCallback((id) => {
    const d = drafts.find(x => x.id === id);
    const bid = 'brf-' + Math.random().toString(36).slice(2, 6);
    createBrief({
      id: bid, title: d?.title || `Design brief from ${id}`,
      intent: d?.reasoning || d?.subtitle || 'Translate this brief into design directions.',
      source: { module: 'orbit', role: 'hom', person: 'Priya Shah' },
      origin: { type: 'signal', refId: id, label: d?.title || id },
      constraints: { pieceType: '', metal: '', karat: '', weightCeilingG: null, stone: '', motif: '', occasion: '', budget: null, currency: '₹' },
      references: [], priority: 'mid', dueDays: 7, createdAt: 'just now', assignee: 'Vikram',
      status: 'new', sessionId: null, launchPackId: null,
      thread: [{ who: 'Priya Shah', role: 'hom', at: 'just now', text: 'Sent from ORBIT — please turn this into directions.' }],
      history: [{ state: 'new', at: 'just now' }],
    });
    pushToast({ kind: 'info', title: 'KRAFT brief created', body: `Brief ${id} → KRAFT inbox (${bid}).` });
  }, [drafts, createBrief]);
  const saveBriefToWiki = useCallback((id) => {
    pushToast({ kind: 'info', title: 'Saved to WIKI', body: `Brief ${id} queued as a memory candidate.` });
  }, []);

  /* ---------- IM (VAULT) workflow handlers · Wave E ---------- */
  // Draft action from an item recommendation. Frontend simulation of
  // POST /api/vault/actions/draft (see kova-openapi.yaml).
  const draftVaultAction = useCallback((itemId, type) => {
    const item = vaultItems.find(i => i.id === itemId);
    if (!item) return;
    const rec = item.recommendedAction;
    const newId = `rcp-vlt${Math.floor(Math.random() * 900 + 100)}`;
    const cosignClause =
      type === 'transfer' ? 'You · Inventory · Logistics co-sign' :
      type === 'reprice'  ? 'You · Inventory · Pricing co-sign'   :
                            'You · Inventory · IM-only';

    const newAction = {
      id: newId, mod: 'vault', type,
      priority: 'P2', impact: type === 'hold' ? 'ops' : 'money',
      stamp: 'Verified · Vault action draft',
      status: 'pending',
      statusLabel: 'Drafting · awaiting submit',
      runId: newId, runVersion: 'v1', runWhen: 'just now',
      title: type === 'transfer'
        ? `Transfer ${item.name} · ${item.city} → ${rec.toCity || '—'} · ${item.value.display}`
        : type === 'reprice'
        ? `Reprice ${item.name} · ${rec.newPriceHint || '−5 %'} · unlock ${rec.projectedUnlock?.display || '—'}`
        : `Hold ${item.name} for ${rec.holdWindowDays || 14} days · ${item.city}`,
      reasoning: rec.rationale,
      itemIds: [item.id],
      fromCity: item.city, toCity: rec.toCity || null,
      repricePercent: rec.percentDelta || null,
      holdDays: rec.holdWindowDays || null,
      stats: type === 'transfer' ? [
        { label: 'Unlock',     value: rec.projectedUnlock?.display || '—', color: 'success' },
        { label: 'SKUs',       value: '1' },
        { label: 'Route',      value: `${item.city} → ${rec.toCity}` },
        { label: 'Margin Δ',   value: '+ ~4 %', color: 'success' },
      ] : type === 'reprice' ? [
        { label: 'Unlock',     value: rec.projectedUnlock?.display || '—', color: 'success' },
        { label: 'SKUs',       value: '1' },
        { label: 'Reprice',    value: rec.newPriceHint || '—' },
        { label: 'Margin Δ',   value: '− ~2 %', color: 'danger' },
      ] : [
        { label: 'Items',      value: '1' },
        { label: 'Window',     value: `${rec.holdWindowDays || 14} d` },
        { label: 'Risk',       value: 'low' },
        { label: 'Cost',       value: '₹0', color: 'success' },
      ],
      confidence: rec.confidence,
      confidenceLabel: ['Low','Medium','High','Verified'][rec.confidence - 1],
      cosign: cosignClause,
      chain: type === 'transfer' ? [
        { who: 'Auto · evidence sweep', meta: 'Sourced from item snapshot', state: 'done' },
        { who: 'You · Inventory',       meta: 'Drafting · ready to submit', state: 'now' },
        { who: 'Logistics',             meta: 'Co-sign on manifest',         state: 'pending' },
        { who: 'CEO',                   meta: 'Final approval',              state: 'pending' },
        { who: 'CONNECT · execute',     meta: 'Tally + WMS update',          state: 'pending' },
      ] : type === 'reprice' ? [
        { who: 'Auto · evidence sweep', meta: 'Sourced from item snapshot', state: 'done' },
        { who: 'You · Inventory',       meta: 'Drafting · ready to submit', state: 'now' },
        { who: 'Pricing Lead',          meta: 'Co-sign for ceiling check',   state: 'pending' },
        { who: 'CEO',                   meta: 'Final approval',              state: 'pending' },
        { who: 'CONNECT · execute',     meta: 'PRICE engine apply',          state: 'pending' },
      ] : [
        { who: 'Auto · evidence sweep', meta: 'Sourced from item snapshot',  state: 'done' },
        { who: 'You · Inventory',       meta: 'Drafting · IM signs only',    state: 'now' },
        { who: 'Watchlist',             meta: `${rec.holdWindowDays || 14} d window`, state: 'pending' },
      ],
      workflow: ['Queued','Context built','Agent ran','Verified','IM review',
                  type === 'hold' ? 'Hold active' : (type === 'reprice' ? 'Pricing co-sign' : 'Logistics co-sign'),
                  type === 'hold' ? 'Auto-revisit' : 'CEO approval'].map((label, i) => ({
        label,
        step: ['queued','context_built','agent_ran','verified','im_review',
               type === 'hold' ? 'executed' : (type === 'reprice' ? 'cosign_pricing' : 'cosign_logistics'),
               type === 'hold' ? 'outcome_tracked' : 'awaiting_approval'][i],
        meta: ['workflow.start','snapshot','vault_action_agent v2.1','sourced','IM · drafting',
               type === 'hold' ? 'Watchlist active' : 'Co-sign pending',
               type === 'hold' ? 'Signal monitor' : 'CEO sign'][i],
        time: ['just now','just now','just now','just now','now','—','—'][i],
        state: i < 4 ? 'done' : i === 4 ? 'now' : 'pending',
      })),
      claims: [
        { text: rec.rationale, source: 'vault_action_agent', receipt: `${newId} / claim 1` },
        ...(item.signals.length ? item.signals.map((sid, i) => ({
          text: `Source signal ${sid} attached to this SKU.`,
          source: 'orbit.signal', receipt: `${newId} / claim ${i + 2}`,
        })) : []),
      ],
      receipts: [
        { tool: 'sql.query',     scope: 'inventory.read', id: `tool-receipt-${Math.floor(Math.random() * 9000)}` },
        { tool: 'sql.query',     scope: 'sales.read',     id: `tool-receipt-${Math.floor(Math.random() * 9000)}` },
        ...(type !== 'hold' ? [{
          tool: 'price.calculate', scope: 'deterministic', id: `tool-receipt-${Math.floor(Math.random() * 9000)}`,
        }] : []),
      ],
      math: type === 'transfer' ? [
        { expr: 'unlock = sku_value',                          value: item.value.display },
        { expr: 'transfer_cost · estimated',                    value: '₹12,400' },
      ] : type === 'reprice' ? [
        { expr: `reprice = listed × ${rec.percentDelta}`,       value: rec.projectedUnlock?.display || '—' },
        { expr: 'margin_loss',                                  value: '~2 %' },
      ] : [
        { expr: 'hold_carry_cost',                              value: '₹0' },
        { expr: 'reassess_after_days',                          value: String(rec.holdWindowDays || 14) },
      ],
    };
    setVaultActions(prev => [newAction, ...prev]);
    pushToast({ kind: 'success', title: `${type[0].toUpperCase() + type.slice(1)} draft created`,
                body: `${newId} · ${item.sku} · refine or submit.` });
    setRoute('vault', `actions/${newId}`);
  }, [vaultItems]);

  // Submit a VAULT action → cross-module routing per architecture §2 + §8.3.
  // Fix R1 · reprice must land in pricingCosignQueue first (PL surface) before
  // reaching CEO. Transfer needs Logistics co-sign (no Logistics UI yet — falls
  // through to CEO with the chain step pending). Hold is IM-only, applied
  // immediately (architecture §1.5 exception: no external write).
  const submitVaultAction = useCallback((id) => {
    const a = vaultActions.find(x => x.id === id);
    if (!a) return;

    const needsPricing   = a.type === 'reprice'  || a.cosign?.includes('Pricing');
    const needsLogistics = a.type === 'transfer' || a.cosign?.includes('Logistics');
    const submittedBy    = `${(authCtx.persona || 'Asha').split(' ')[0]} · Inventory`;
    const baseDoneChain  = a.chain.filter(s => s.state === 'done');
    const baseWorkflow   = a.workflow.map((s, i) => ({
      ...s,
      state: i <= 4 ? 'done' : i === 5 ? 'now' : 'pending',
      time:  i === 4 ? 'just now' : i === 5 ? 'now' : s.time,
    }));

    if (a.type === 'hold') {
      // Hold · IM-only, applied immediately, audited
      updateVaultAction(id, () => ({
        status: 'approved',
        statusLabel: 'Hold applied · auto-revisit in window',
        runWhen: 'just now',
        chain: a.chain.map(s => ({ ...s, state: 'done' })),
        workflow: a.workflow.map(s => ({ ...s, state: 'done' })),
      }));
      pushToast({ kind: 'success', title: `Hold applied · ${id}`,
                  body: `${a.holdDays || 14}-day watchlist active · auto-revisit on signal change.` });
    } else if (needsPricing) {
      // → PL queue · architecture §13·7 cross-module bridge
      // Heuristic for ceilingCheck: deeper than -10% reprice = breach
      const depth = Math.abs(a.repricePercent || 0);
      const ceilingCheck = depth > 0.10 ? 'breach' : depth >= 0.08 ? 'at_floor' : 'pass';
      const cosignItem = {
        ...a,
        sourceModule: 'vault',
        sourceRunId:  a.runId,
        type:         'reprice',
        mod:          'vault',
        stamp:        a.stamp || 'Verified · Vault action draft',
        status:       'pending',
        statusLabel:  ceilingCheck === 'breach'
          ? 'Awaiting Pricing co-sign · ⚠ ceiling breach'
          : 'Awaiting Pricing co-sign',
        runWhen:      'just now',
        submittedBy,
        ceilingCheck,
        chain: [
          ...baseDoneChain,
          { who: submittedBy,             meta: 'Submitted · just now',           state: 'done' },
          { who: 'You · Pricing Lead',    meta: ceilingCheck === 'breach' ? 'Awaits co-sign · ceiling breach' : 'Awaits co-sign',     state: 'now' },
          { who: 'CEO',                   meta: 'Final approval',                 state: 'pending' },
          { who: 'CONNECT · PRICE engine', meta: 'Apply on approval',             state: 'pending' },
        ],
        workflow: baseWorkflow,
      };
      setPricingCosignQueue(prev => [cosignItem, ...prev]);
      setVaultActions(prev => prev.filter(x => x.id !== id));
      pushToast({ kind: 'success', title: `Submitted · ${id}`,
                  body: 'Moved to Pricing co-sign queue.' });
    } else if (needsLogistics) {
      // → Logistics co-sign · no Logistics UI yet · falls through to CEO with
      //   the Logistics step pending in the chain (production: Logistics would
      //   see this in their queue; CEO would then become 'now' after co-sign).
      const promoted = {
        ...a,
        mod: 'vault',
        status: 'pending',
        statusLabel: 'Pending Logistics co-sign · then CEO',
        runWhen: 'just now',
        chain: [
          ...baseDoneChain,
          { who: submittedBy,             meta: 'Submitted · just now',           state: 'done' },
          { who: 'Logistics',             meta: 'Co-sign on manifest feasibility', state: 'now' },
          { who: 'CEO',                   meta: 'Final approval',                 state: 'pending' },
          { who: 'CONNECT · execute',     meta: 'Tally + WMS update',             state: 'pending' },
        ],
        workflow: baseWorkflow,
      };
      setDecisions(prev => [promoted, ...prev]);
      setVaultActions(prev => prev.filter(x => x.id !== id));
      pushToast({ kind: 'success', title: `Submitted · ${id}`,
                  body: 'Moved to Logistics co-sign · CEO follows.' });
    } else {
      // → CEO directly
      const promoted = {
        ...a,
        mod: 'vault',
        status: 'pending',
        statusLabel: 'Pending CEO sign',
        runWhen: 'just now',
        chain: [
          ...baseDoneChain,
          { who: submittedBy,             meta: 'Submitted · just now',           state: 'done' },
          { who: 'CEO',                   meta: 'Awaiting sign · ⌘⏎',             state: 'now' },
          { who: 'CONNECT · execute',     meta: 'On approval',                    state: 'pending' },
        ],
        workflow: baseWorkflow,
      };
      setDecisions(prev => [promoted, ...prev]);
      setVaultActions(prev => prev.filter(x => x.id !== id));
      pushToast({ kind: 'success', title: `Submitted · ${id}`, body: 'Moved to CEO Approvals.' });
    }
    setRoute('vault');
  }, [vaultActions, authCtx]);

  const discardVaultAction = useCallback((id) => {
    setVaultActions(prev => prev.filter(x => x.id !== id));
    pushToast({ kind: 'warn', title: `Discarded · ${id}`,
                body: 'Draft removed. The aged SKU remains in the queue.' });
    setRoute('vault');
  }, []);

  const refineVaultAction = useCallback((id) => {
    setReviseId(id);
    setReviseConstraint('Try a tighter destination filter or smaller cohort — refine the action.');
  }, []);

  // Workflow trigger · backend: POST /api/workflows/vault.deadstock_scan/start
  const runVaultScan = useCallback(() => {
    pushToast({ kind: 'info', title: 'Vault deadstock scan queued',
                body: 'vault.deadstock_scan · context build → agent run · ~30 s' });
    // Frontend simulation only · no fresh items are added (architecture says backend computes).
    setTimeout(() => {
      pushToast({ kind: 'success', title: 'Scan complete',
                  body: 'Inventory queue refreshed · no new candidates this cycle.' });
    }, 2500);
  }, []);

  // Cross-module IM actions
  const requestVaultCosignPricing = useCallback((id) => {
    updateVaultAction(id, (cur) => ({
      statusLabel: 'Awaiting Pricing co-sign',
      chain: cur.chain.map(s => s.who === 'Pricing Lead' ? { ...s, state: 'now' } : s),
    }));
    pushToast({ kind: 'info', title: 'Pricing co-sign requested', body: `Karan will review ${id}.` });
  }, [updateVaultAction]);

  const requestVaultCosignLogistics = useCallback((id) => {
    updateVaultAction(id, (cur) => ({
      statusLabel: 'Awaiting Logistics co-sign',
      chain: cur.chain.map(s => s.who === 'Logistics' ? { ...s, state: 'now' } : s),
    }));
    pushToast({ kind: 'info', title: 'Logistics co-sign requested', body: `Manifest queued for review · ${id}.` });
  }, [updateVaultAction]);

  const createKraftRemakeBrief = useCallback((itemId) => {
    const it = vaultItems.find(i => i.id === itemId);
    const bid = 'brf-' + Math.random().toString(36).slice(2, 6);
    createBrief({
      id: bid, title: 'Remake: ' + (it?.name || itemId),
      intent: 'Aged inventory recovery — redesign this SKU into current taste; preserve recoverable value where possible.',
      source: { module: 'vault', role: 'inventory', person: 'Asha Iyer' },
      origin: { type: 'sku', refId: itemId, label: it?.name ? `${it.name} · ${itemId}` : itemId },
      constraints: { pieceType: it?.cohort || '', metal: '', karat: '', weightCeilingG: null, stone: '', motif: 'Contemporary', occasion: '', budget: it?.value?.amount || null, currency: '₹' },
      references: [], priority: 'mid', dueDays: 10, createdAt: 'just now', assignee: 'Vikram',
      status: 'new', sessionId: null, launchPackId: null,
      thread: [{ who: 'Asha Iyer', role: 'inventory', at: 'just now', text: 'Recovery play from VAULT — anything sellable beats melting.' }],
      history: [{ state: 'new', at: 'just now' }],
    });
    pushToast({ kind: 'info', title: 'KRAFT remake brief created', body: `${itemId} → KRAFT inbox (${bid}).` });
  }, [vaultItems, createBrief]);

  const assignVaultOwner = useCallback((itemId) => {
    pushToast({ kind: 'info', title: 'Owner assigned',
                body: `${itemId} assigned to follow-up queue.` });
  }, []);

  const exportVaultList = useCallback(() => {
    pushToast({ kind: 'info', title: 'Action list exported',
                body: 'CSV generated · backend: POST /api/vault/items/export' });
  }, []);

  /* ---------- PL (PRICE) workflow handlers · Wave E ---------- */
  // Approve a co-sign item · advances the chain to CEO. Backend:
  // POST /api/price/cosign/:id/approve.
  // The architecture's cross-module bridge: IM/HoM submitted, PL co-signs,
  // CEO becomes the next approver. Item moves into the global decisions array.
  const approvePricingCosign = useCallback((id) => {
    const item = pricingCosignQueue.find(c => c.id === id);
    if (!item) return;
    const breach = item.ceilingCheck === 'breach';

    // Promote into decisions for CEO inbox
    const promoted = {
      ...item,
      mod: item.mod || 'price',
      status: 'pending',
      statusLabel: breach
        ? 'Pending CEO override · ceiling breach'
        : 'Pending CEO sign · Pricing co-signed',
      runWhen: 'just now',
      chain: item.chain.map(s =>
        s.who === 'You · Pricing Lead' ? { ...s, who: 'Karan · Pricing Lead', state: 'done', meta: 'Co-signed · just now' } :
        s.who === 'CEO'                ? { ...s, state: 'now' } :
        s
      ),
      workflow: item.workflow.map((s, i) => ({
        ...s,
        state: i <= 5 ? 'done' : i === 6 ? 'now' : 'pending',
        time:  i === 5 ? 'just now' : i === 6 ? 'now' : s.time,
      })),
    };
    setDecisions(prev => [promoted, ...prev]);
    setPricingCosignQueue(prev => prev.filter(c => c.id !== id));
    pushToast({ kind: 'success', title: `Co-signed · ${id}`,
                body: breach ? 'Breach acknowledged · CEO must override.' : 'Advanced to CEO Approvals.' });
    setRoute('price');
  }, [pricingCosignQueue]);

  // Reject a co-sign item · sends it back to the source author with a reason.
  // Backend: POST /api/price/cosign/:id/reject
  const [pricingRejectId, setPricingRejectId] = useState(null);
  const [pricingRejectReason, setPricingRejectReason] = useState('');
  const openPricingReject = useCallback((id) => {
    setPricingRejectId(id);
    setPricingRejectReason('Margin floor would breach further than acceptable · please reduce reprice depth.');
  }, []);
  const closePricingReject = useCallback(() => setPricingRejectId(null), []);
  const confirmPricingReject = useCallback(() => {
    if (!pricingRejectId) return;
    const id = pricingRejectId;
    setPricingCosignQueue(prev => prev.map(c =>
      c.id === id ? {
        ...c,
        status: 'rejected',
        statusLabel: 'Returned to author · Pricing rejected',
        chain: c.chain.map(s =>
          s.who === 'You · Pricing Lead'
            ? { ...s, state: 'failed', meta: `Rejected · ${pricingRejectReason.slice(0, 56)}…` }
            : s
        ),
      } : c
    ));
    setPricingRejectId(null);
    pushToast({ kind: 'warn', title: `Rejected · ${id}`,
                body: `Returned to source author with reason.` });
  }, [pricingRejectId, pricingRejectReason]);

  // Refine cosign item · uses the shared Revise modal (Bug-#1 generalization).
  const refinePricingCosign = useCallback((id) => {
    setReviseId(id);
    setReviseConstraint('Try a shallower discount that keeps margin at or above floor.');
  }, []);

  // Workflow trigger · backend: POST /api/workflows/price.refresh_rates/start
  const refreshPriceRates = useCallback(() => {
    pushToast({ kind: 'info', title: 'Refreshing rates · market feed',
                body: 'price.refresh_rates · ~3 s for metals, ~8 s for stones' });
    // Frontend simulation only · bump timestamps and minor delta
    setTimeout(() => {
      setPriceRates(prev => prev.map(r => ({
        ...r,
        updatedAt: 'just now',
      })));
      pushToast({ kind: 'success', title: 'Rates refreshed',
                  body: '10 of 10 rates updated · feed health green.' });
    }, 2500);
  }, []);

  // Edit a single rate (proposes a Decision · CEO sign for ceiling change)
  const editPriceRate = useCallback((rateId) => {
    pushToast({ kind: 'info', title: 'Rate edit drafted',
                body: `${rateId} · rate-change proposal queued for CEO sign.` });
  }, []);

  // Quote actions
  const createPriceQuote = useCallback(() => {
    const newId = `q-${Math.floor(Math.random() * 9000 + 1000)}`;
    const newQuote = {
      id: newId,
      customer: 'New customer · unnamed',
      segment: 'retail',
      skuCount: 0,
      totalAmount: { amount: 0, currency: 'INR', display: '₹0' },
      discountPercent: 0,
      margin: 0,
      marginFloor: 32,
      ceilingCheck: 'pass',
      status: 'draft',
      validUntil: '14 days',
      updatedAt: 'just now',
      items: [],
      formula: 'awaiting line items',
    };
    setPriceQuotes(prev => [newQuote, ...prev]);
    pushToast({ kind: 'success', title: 'Quote drafted',
                body: `${newId} · add line items to compute margin.` });
    setRoute('price', `quotes/${newId}`);
  }, []);

  const simulatePriceQuote = useCallback((id) => {
    pushToast({ kind: 'info', title: 'Simulating quote',
                body: `${id} · PRICE engine recalculating · deterministic.` });
  }, []);

  const submitPriceQuote = useCallback((id) => {
    updatePriceQuote(id, () => ({
      status: 'submitted',
      statusLabel: 'Submitted to CEO · ceiling breach',
      updatedAt: 'just now',
    }));
    pushToast({ kind: 'success', title: `Quote submitted · ${id}`,
                body: 'Moved to CEO Approvals · breach override required.' });
  }, [updatePriceQuote]);

  const approvePriceQuote = useCallback((id) => {
    updatePriceQuote(id, () => ({
      status: 'approved',
      statusLabel: 'Approved by Pricing · ready to send',
      updatedAt: 'just now',
    }));
    pushToast({ kind: 'success', title: `Quote approved · ${id}`,
                body: 'Within ceiling · Pricing signs directly · export PDF to send.' });
  }, [updatePriceQuote]);

  const exportPriceQuote = useCallback((id) => {
    pushToast({ kind: 'info', title: 'Quote export queued',
                body: `${id} · PDF + email · backend: POST /api/price/quotes/:id/export` });
  }, []);

  const openPriceFormula = useCallback((id) => {
    pushToast({ kind: 'info', title: 'View formula',
                body: `Formula shown above in main pane · architecture §8.4 transparency.` });
  }, []);

  // Forward a margin-risk SKU to VAULT for IM to draft a reprice-up.
  // Fix R2 · previously a toast-only no-op. Now actually inserts a
  // synthetic VaultItem with a reprice-up recommendation so IM sees it
  // in their queue. Backend: POST /api/price/margin-risk/:id/forward-to-vault
  const forwardMarginRiskToVault = useCallback((skuId) => {
    const sku = marginRiskItems.find(r => r.id === skuId);
    if (!sku) return;
    if (vaultItems.some(i => i.id === skuId)) {
      pushToast({ kind: 'info', title: 'Already in VAULT queue',
                  body: `${skuId} is already flagged · IM will see it.` });
      return;
    }
    const isBreach = sku.risk === 'breach';
    const newItem = {
      id: skuId, sku: skuId, name: sku.name,
      city: sku.city,
      cohort: 'Forwarded from PRICE · margin risk',
      ageDays: 180 + (sku.daysAtRisk * 4),  // synthetic minimum-aged
      value: { amount: 0, currency: 'INR', display: '—' },
      status: 'recommended_action',
      signals: [],
      recommendedAction: {
        type: 'reprice',
        confidence: isBreach ? 3 : 2,
        rationale: `Forwarded from PRICE margin-risk monitor. Current margin ${sku.margin} % vs floor ${sku.floor} % · ${isBreach ? 'BREACHING' : 'at risk'} for ${sku.daysAtRisk} d. Reprice-UP recommended to restore margin.`,
        percentDelta:    isBreach ?  0.06 :  0.03,
        newPriceHint:    isBreach ?  '+ 6 % MRP' : '+ 3 % MRP',
        projectedUnlock: { amount: 0, currency: 'INR', display: 'margin restoration' },
      },
    };
    setVaultItems(prev => [newItem, ...prev]);
    setMarginRiskItems(prev => prev.map(r =>
      r.id === skuId ? { ...r, action: 'forwarded' } : r
    ));
    pushToast({ kind: 'success', title: `Forwarded to VAULT · ${skuId}`,
                body: 'IM queue updated · reprice-up recommendation visible to Asha.' });
  }, [marginRiskItems, vaultItems]);

  /* ---------- overlays ---------- */
  const [drawerOpen, setDrawerOpen] = useState(false);
  const [notifsOpen, setNotifsOpen] = useState(false);
  const [shortcutsOpen, setShortcutsOpen] = useState(false);
  const [cmdkOpen, setCmdkOpen] = useState(false);
  const [profileOpen, setProfileOpen] = useState(false);
  const [wswOpen, setWswOpen] = useState(false);
  const [toasts, setToasts] = useState([]);

  /* ---------- user preferences (session-local; backend persistence is
       PATCH /api/profile/preferences → users.preferences_json in tenant D1
       per architecture §4.8). Defaults match the §4.6 provisioning seed. */
  const [userPrefs, setUserPrefs] = useState({
    dailyBrief: true,
    approvalsDigest: true,
    monitorAlerts: true,
    marketingNews: false,
  });
  const setPref = useCallback((key, value) => {
    setUserPrefs(p => ({ ...p, [key]: value }));
  }, []);

  /* ---------- notifications state ---------- */
  const [notifications, setNotifications] = useState([
    { id: 'n1', mod: 'orbit',     kind: 'run',      title: 'Orbit · new daily brief · 3 recommendations',         meta: '2 min ago',  read: false, runId: 'rcp-44c9' },
    { id: 'n2', mod: 'approvals', kind: 'approval', title: 'Vault · awaiting your approval on rcp-44c9',           meta: '9 min ago',  read: false, runId: 'rcp-44c9' },
    { id: 'n3', mod: 'price',     kind: 'approval', title: 'Price · 24K rate update +1.4 % awaiting sign',         meta: '4 min ago',  read: false, runId: 'rcp-44cb' },
    { id: 'n4', mod: 'connect',   kind: 'system',   title: 'Tally sync completed · 412 SKUs reconciled',           meta: '1 h ago',    read: true  },
    { id: 'n5', mod: 'kraft',     kind: 'run',      title: 'Kraft · CAD-117 packet ready for review',              meta: 'Yesterday',  read: true,  runId: 'rcp-44cc' },
    { id: 'n6', mod: 'monitor',   kind: 'error',    title: 'Verifier rejected 1 of 4 claims · rcp-44cd',           meta: 'Yesterday',  read: true,  runId: 'rcp-44cd' },
  ]);
  const markAllNotifsRead = useCallback(() => setNotifications(ns => ns.map(n => ({ ...n, read: true }))), []);
  const pushToast = useCallback((t) => {
    const id = Math.random().toString(36).slice(2);
    setToasts(s => [...s, { id, ...t }]);
    setTimeout(() => setToasts(s => s.filter(x => x.id !== id)), 6000);
  }, []);

  const openEvidence = useCallback((id) => {
    if (id) setDrawerDecisionId(id);
    setDrawerOpen(true);
  }, []);

  const openReject = useCallback((id) => {
    setRejectId(id);
    setRejectReason('Delhi storefront is mid-renovation through 28 May; transfer next week.');
  }, []);
  const closeReject = useCallback(() => setRejectId(null), []);
  const confirmReject = useCallback(() => {
    if (rejectId) rejectDecision(rejectId, rejectReason);
    setRejectId(null);
  }, [rejectId, rejectReason, rejectDecision]);

  /* ---------- revise flow (architecture §8 "Request Revision" button) ---------- */
  const [reviseId, setReviseId] = useState(null);
  const [reviseDesignId, setReviseDesignId] = useState(null);
  const [reviseConstraint, setReviseConstraint] = useState('');

  // Default presets cover Orbit/Vault/Price (logistics/pricing constraints).
  const REVISE_PRESETS_DEFAULT = [
    'Try a different city or cohort.',
    'Lower the discount cap below 5 %.',
    'Hold a margin floor of 40 %.',
    'Limit to top-margin SKUs only.',
    'Test on a smaller cohort first.',
  ];

  // KRAFT presets are about design language — motif / material / weight /
  // budget — never logistics. Matches the constraints designers actually
  // act on when revising a launch pack.
  const REVISE_PRESETS_KRAFT_PACK = [
    'Try a different motif direction.',
    'Lighter weight target — under 8 g.',
    'Switch to platinum / 18k white.',
    'More traditional · less contemporary.',
    'Tighter budget — under ₹50k BOM.',
    'Reduce stone count, keep the centre.',
  ];
  const REVISE_PRESETS_KRAFT_TILE = [
    'Make the centerpiece smaller.',
    'Switch to platinum / 18k white.',
    'Drop the side accents.',
    'Lighter shank · under 4 mm.',
    'Tighten lead time — same brief, faster.',
  ];

  // Look up the decision being revised so the modal can be module-aware.
  const reviseDecision = useMemo(
    () => (reviseId ? decisions.find(d => d.id === reviseId) : null),
    [reviseId, decisions]
  );
  const reviseIsKraft = reviseDecision?.mod === 'kraft';
  const revisePresets = reviseIsKraft
    ? (reviseDesignId ? REVISE_PRESETS_KRAFT_TILE : REVISE_PRESETS_KRAFT_PACK)
    : REVISE_PRESETS_DEFAULT;

  const openRevise = useCallback((id, opts = {}) => {
    setReviseId(id);
    setReviseDesignId(opts.designId || null);
    setReviseConstraint('');  // no pre-filled VAULT example — let the user write or click a preset
  }, []);
  const closeRevise = useCallback(() => {
    setReviseId(null);
    setReviseDesignId(null);
  }, []);

  const bumpVersion = (v) => {
    const m = (v || 'v1').match(/^v(\d+)$/);
    return m ? `v${parseInt(m[1], 10) + 1}` : `${v}+1`;
  };

  const confirmRevise = useCallback(() => {
    if (!reviseId) return;
    const id = reviseId;
    const constraint = reviseConstraint.trim() || 'Revision requested without constraint text.';
    setReviseId(null);

    // The same modal is reused by CEO (revising a decision), HoM (refining a brief
    // draft), IM (refining a VAULT action), and PL (requesting revision on a
    // cross-module co-sign item). Detect the collection and route accordingly.
    const isDraft         = drafts.some(d => d.id === id);
    const isVaultAction   = vaultActions.some(a => a.id === id);
    const isPricingCosign = pricingCosignQueue.some(c => c.id === id);
    const updater         = isDraft         ? updateDraft
                          : isVaultAction   ? updateVaultAction
                          : isPricingCosign ? updatePricingCosign
                          : updateDecision;
    const actor           = isDraft         ? 'You · HoM'
                          : isVaultAction   ? 'You · Inventory'
                          : isPricingCosign ? 'You · Pricing Lead'
                          : 'You · CEO';
    const nextLabel       = isDraft         ? 'Drafting · awaiting submit · revised'
                          : isVaultAction   ? 'Drafting · awaiting submit · revised'
                          : isPricingCosign ? 'Returned to author for revision'
                          : 'Pending CEO sign · revised';

    // Phase 1: status → revising, workflow reset to "queued"
    updater(id, (cur) => ({
      status: 'revising',
      statusLabel: isDraft ? 'Refining · agent re-running' : 'Revising · agent re-running',
      runWhen: 'just now',
      workflow: cur.workflow.map((s, i) => ({
        ...s,
        state: i === 0 ? 'now' : 'pending',
        time:  i === 0 ? 'now' : '—',
      })),
      chain: [
        ...cur.chain.filter(s => s.state === 'done').slice(0, 2),
        { who: actor, meta: `Requested refine · ${constraint.slice(0, 48)}${constraint.length > 48 ? '…' : ''}`, state: 'done' },
        { who: 'Agent · re-running', meta: 'New constraints applied · context rebuilding', state: 'now' },
      ],
    }));
    pushToast({ kind: 'info', title: `Revision requested · ${id}`, body: 'Agent re-running with your constraints.' });

    // Phase 2: context built · agent ran
    setTimeout(() => {
      updater(id, (cur) => ({
        workflow: cur.workflow.map((s, i) => ({
          ...s,
          state: i < 3 ? 'done' : i === 3 ? 'now' : 'pending',
          time:  i === 0 ? 'just now' : i === 1 ? 'just now' : i === 2 ? 'just now' : i === 3 ? 'now' : '—',
        })),
      }));
    }, 1400);

    // Phase 3: verified · back to caller with bumped version
    setTimeout(() => {
      updater(id, (cur) => ({
        status: 'pending',
        statusLabel: nextLabel,
        runVersion: bumpVersion(cur.runVersion),
        runWhen: 'just now',
        reasoning: `${cur.reasoning}  Revision applied: "${constraint}"`,
        workflow: cur.workflow.map((s, i) => ({
          ...s,
          state: i < 4 ? 'done' : i === 4 ? 'now' : 'pending',
          time:  i < 4 ? 'just now' : i === 4 ? 'now' : '—',
        })),
        chain: isDraft ? [
          { who: 'Agent · re-ran with constraints', meta: 'New version produced',     state: 'done' },
          { who: 'Auto · evidence sweep',           meta: '3 / 3 claims re-sourced',  state: 'done' },
          { who: 'You · HoM',                       meta: 'Drafting · ready to submit', state: 'now' },
          { who: cur.cosign?.includes('Pricing') ? 'Pricing co-sign' : 'CEO', meta: 'Awaits your submit', state: 'pending' },
        ] : [
          { who: 'Agent · re-ran with constraints', meta: 'New version produced',     state: 'done' },
          { who: 'Auto · evidence sweep',           meta: '3 / 3 claims re-sourced',  state: 'done' },
          { who: 'You · CEO',                       meta: 'Awaiting sign · ⌘⏎',       state: 'now' },
          { who: 'Logistics · execution',           meta: 'Will queue on approval',   state: 'pending' },
        ],
      }));
      pushToast({ kind: 'success', title: 'Revision complete',
                  body: (isDraft || isVaultAction) ? 'New version ready for your submit.'
                      : isPricingCosign ? 'Returned to source · awaiting fresh draft.'
                      : 'New version ready for your sign.' });
    }, 3200);
  }, [reviseId, reviseConstraint, drafts, vaultActions, pricingCosignQueue,
      updateDecision, updateDraft, updateVaultAction, updatePricingCosign]);

  /* ---------- keyboard map ---------- */
  const gNavSeq = useRef({ active: false });
  useEffect(() => {
    if (authStage !== 'authed') return;
    const G_MAP = { h: 'home', o: 'orbit', v: 'vault', p: 'price', k: 'kraft',
                    a: 'approvals', m: 'monitor', l: 'library' };

    const onKey = (e) => {
      const meta = e.metaKey || e.ctrlKey;
      const tag = e.target?.tagName;
      const isInput = tag === 'INPUT' || tag === 'TEXTAREA';

      if (meta && e.key.toLowerCase() === 'k') { e.preventDefault(); setCmdkOpen(o => !o); return; }
      if (meta && e.key.toLowerCase() === 'e' && !isInput) { e.preventDefault(); openEvidence(); return; }
      // ⌘, → profile & preferences
      if (meta && e.key === ',') { e.preventDefault(); setProfileOpen(o => !o); return; }
      // ⌘⇧O → switch workspace / tenant
      // (⌘⇧W is "close window" in every Mac browser and isn't safely
      //  preventable from JS — using ⇧O for "Open workspace" instead.)
      if (meta && e.shiftKey && e.key.toLowerCase() === 'o') { e.preventDefault(); setWswOpen(o => !o); return; }
      // ? · shortcuts overlay
      if (!meta && e.key === '?' && !isInput) { e.preventDefault(); setShortcutsOpen(o => !o); return; }

      // ⌘⏎ on detail page approves the current decision; otherwise approves the next pending
      if (meta && e.key === 'Enter' && !isInput) {
        e.preventDefault();
        const target = (route === 'approvals' && subId) ? subId
                      : decisions.find(d => d.status === 'pending')?.id;
        if (target) approveDecision(target);
        return;
      }
      // ⌘⇧⏎ rejects on detail page
      if (meta && e.shiftKey && e.key === 'Enter' && !isInput) {
        e.preventDefault();
        const target = (route === 'approvals' && subId) ? subId
                      : decisions.find(d => d.status === 'pending')?.id;
        if (target) openReject(target);
        return;
      }

      if (isInput) return;

      if (!meta) {
        if (e.key === 'g' && !gNavSeq.current.active) {
          gNavSeq.current.active = true;
          setTimeout(() => { gNavSeq.current.active = false; }, 1200);
          return;
        }
        if (gNavSeq.current.active && G_MAP[e.key.toLowerCase()]) {
          e.preventDefault();
          setRoute(G_MAP[e.key.toLowerCase()]);
          gNavSeq.current.active = false;
          return;
        }
      }
    };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, [authStage, route, subId, decisions, approveDecision, openEvidence, openReject, setRoute]);

  /* ---------- cmdk handler ---------- */
  const onCmdKPick = useCallback((item) => {
    if (ROUTES.includes(item.id)) {
      setRoute(item.id);
    } else if (item.id === 'evidence') {
      openEvidence();
    } else if (item.id === 'approve-next') {
      const next = decisions.find(d => d.status === 'pending');
      if (next) approveDecision(next.id);
    } else if (item.id === 'run-brief' || item.id === 'scan-vault') {
      pushToast({ kind: 'info', title: 'Workflow queued', body: `${item.label} · run posted to Workflows` });
    } else if (item.id === 'profile')     { setProfileOpen(true); }
    else if   (item.id === 'switch-ws')   { setWswOpen(true); }
    else if   (item.id === 'shortcuts')   { setShortcutsOpen(true); }
    else if   (item.id === 'sign-out')    { onSignOut(); }
    else if   (item.id.startsWith('rcp-')) {
      setRoute('approvals', item.id);
    }
  }, [decisions, approveDecision, openEvidence, pushToast, setRoute, onSignOut]);

  const cmdKSections = useMemo(() => [
    ...CMDK_SECTIONS_BASE,
    { group: 'Recent runs', items: decisions.slice(0, 4).map(d => ({
        id: d.id, label: `${d.id} · ${d.title.slice(0, 48)}…`, mod: d.mod, kw: [d.mod, d.id]
    })) },
  ], [decisions]);

  /* ---------- auth surface gate ---------- */
  if (authStage !== 'authed') {
    return (
      <AuthShell hint={`Tenant · ${authCtx.tenant}`}>
        {authStage === 'login' && (
          <LoginPage tenant={authCtx.tenant} tenantId={authCtx.tenantId} onAuthStart={onAuthStart}
                     onForgotPassword={() => setAuthStage('idp-error')} />
        )}
        {authStage === 'dev-login' && (
          <DevLoginPage tenant={authCtx.tenant} onPickRole={onPickDevRole} />
        )}
        {authStage === 'bootstrap' && (
          <SessionBootstrap method={authCtx.method} tenant={authCtx.tenant} onReady={onBootstrapReady} />
        )}
        {authStage === 'pending' && (
          <AccessPending email={authCtx.email || 'rohan@acme.com'} tenant={authCtx.tenant}
                         onSignOut={onSignOut} onContactAdmin={() => {}} />
        )}
        {authStage === 'denied' && (
          <AccessDenied email={authCtx.email} tenant={authCtx.tenant}
                        onSignOut={onSignOut} onRetry={() => setAuthStage('login')} />
        )}
        {authStage === 'idp-error' && (
          <IdpError provider={authCtx.method} errorCode="oauth2_invalid_grant · 401"
                    onRetry={() => setAuthStage('login')} onSignOut={onSignOut} />
        )}
        {authStage === 'signed-out' && (
          <SignedOutPage user="Rohan" tenant={authCtx.tenant} method={authCtx.method}
                         onSignInAgain={onSignInAgain} />
        )}
        <FlowIndicator stage={authStage} />
      </AuthShell>
    );
  }

  /* ---------- module page renderer · role-aware ---------- */
  const renderPage = () => {
    // RBAC gate · architecture §4.5
    if (route !== 'library' && route !== 'ask' && !canAccess(authCtx.role, route)) {
      // Fix R3 · "Back to Home" looped because non-CEO roles don't have access
      // to /home. Route the CTA to the role's primary surface instead.
      const primaryMod = ROLES.find(r => r.id === authCtx.role)?.mod
                      || (authCtx.role === 'ceo' ? 'home' : 'home');
      const primaryModLabel = primaryMod === 'home' ? 'Home' : primaryMod.toUpperCase();
      return (
        <div className="k-page-enter">
          <window.K.PageHead crumb={[route.toUpperCase()]} title="Access restricted" />
          <window.K.RestrictedState
            badge={`${(authCtx.role || 'ceo').toUpperCase()} ROLE`}
            title={`This role does not have access to ${route.toUpperCase()}.`}
            body="Module visibility is controlled by RBAC and the tenant policy. Contact an administrator if this is unexpected."
            cta={<Btn size="sm" onClick={() => setRoute(primaryMod)}>Back to {primaryModLabel}</Btn>}
          />
        </div>
      );
    }

    if (route === 'library')   return <window.LibraryPage
                                        openDrawer={() => openEvidence()}
                                        openModal={() => openReject(decisions.find(d => d.status === 'pending')?.id || null)}
                                        openCmdK={() => setCmdkOpen(true)}
                                        pushToast={pushToast} />;
    if (route === 'ask')       return <AskKovaPage
                                        onClose={() => setRoute('home')}
                                        onOpenEvidence={() => openEvidence()} />;
    if (route === 'home')      return <HomePage
                                        user={(authCtx.persona || 'Rohan').split(' ')[0]}
                                        decisions={decisions}
                                        activity={SEED_ACTIVITY}
                                        onOpenDecision={(id) => setRoute('approvals', id)}
                                        onApprove={approveDecision}
                                        onReject={openReject}
                                        onRevise={openRevise}
                                        onOpenDrawer={(id) => openEvidence(id)}
                                        onOpenCmdK={() => setCmdkOpen(true)}
                                        onAskKova={() => setRoute('ask')} />;
    if (route === 'approvals' && !subId) return <ApprovalsInbox
                                        decisions={decisions}
                                        onOpenDecision={(id) => setRoute('approvals', id)}
                                        onApprove={approveDecision}
                                        onReject={openReject}
                                        onOpenCmdK={() => setCmdkOpen(true)} />;
    if (route === 'approvals' && subId)  return <ApprovalsDetail
                                        decision={currentDetail}
                                        onBack={() => setRoute('approvals')}
                                        onApprove={approveDecision}
                                        onReject={openReject}
                                        onRevise={openRevise}
                                        onOpenEvidence={() => openEvidence(currentDetail?.id)}
                                        onOpenTrace={() => currentDetail?.runId && setRoute('monitor', currentDetail.runId)}
                                        isExecuting={currentDetail?.status === 'executing'} />;
    if (route === 'monitor' && subId)    return <RunDetailPage
                                        runId={subId}
                                        decision={decisions.find(d => d.runId === subId)}
                                        role={authCtx.role}
                                        onBack={() => setRoute('monitor')}
                                        onOpenOutputCard={() => setRoute('approvals', subId)} />;

    /* ───── ORBIT (Wave E · Head of Merchandising) · architecture §8.2 routes ───── */
    if (route === 'orbit' && !subId)                       return <OrbitOverviewPage
                                        user={(authCtx.persona || 'Priya').split(' ')[0]}
                                        drafts={drafts}
                                        signals={signals}
                                        opportunities={opportunities}
                                        onOpenBrief={(id) => setRoute('orbit', `briefs/${id}`)}
                                        onGenerateBrief={generateBrief}
                                        onAskMarket={() => setRoute('ask')}
                                        onSubmit={submitDraft}
                                        onRevise={refineDraft}
                                        onDiscard={discardDraft}
                                        onOpenDrawer={(id) => openEvidence(id)}
                                        onOpenCmdK={() => setCmdkOpen(true)}
                                        onTriageSignal={(id) => createBriefFromSignal(id)}
                                        onConvertOpportunity={convertOpportunity} />;
    if (route === 'orbit' && subId === 'briefs')            return <OrbitBriefsPage
                                        drafts={drafts}
                                        onOpenBrief={(id) => setRoute('orbit', `briefs/${id}`)}
                                        onSubmit={submitDraft}
                                        onDiscard={discardDraft}
                                        onGenerateBrief={generateBrief}
                                        onOpenCmdK={() => setCmdkOpen(true)} />;
    if (route === 'orbit' && subId === 'signals')           return <OrbitSignalsPage
                                        signals={signals}
                                        onSendToVault={sendSignalToVault}
                                        onSaveToWiki={saveSignalToWiki}
                                        onCreateBrief={createBriefFromSignal}
                                        onDismiss={dismissSignal}
                                        onOpenCmdK={() => setCmdkOpen(true)} />;
    if (route === 'orbit' && subId === 'opportunities')     return <OrbitOpportunitiesPage
                                        opportunities={opportunities}
                                        onConvert={convertOpportunity}
                                        onOpenCmdK={() => setCmdkOpen(true)} />;
    if (route === 'orbit' && subId?.startsWith('briefs/'))  return <OrbitBriefDetailPage
                                        draft={drafts.find(d => d.id === subId.slice(7))}
                                        onBack={() => setRoute('orbit', 'briefs')}
                                        onSubmit={submitDraft}
                                        onRevise={refineDraft}
                                        onDiscard={discardDraft}
                                        onSendToVault={sendBriefToVault}
                                        onCreateKraftBrief={createKraftBriefFromBrief}
                                        onSaveToWiki={saveBriefToWiki} />
    // Back-compat: `#orbit/<runId>` (the pre-fix URL shape) still resolves to a brief detail.
    if (route === 'orbit' && subId)                         return <OrbitBriefDetailPage
                                        draft={drafts.find(d => d.id === subId)}
                                        onBack={() => setRoute('orbit', 'briefs')}
                                        onSubmit={submitDraft}
                                        onRevise={refineDraft}
                                        onDiscard={discardDraft}
                                        onSendToVault={sendBriefToVault}
                                        onCreateKraftBrief={createKraftBriefFromBrief}
                                        onSaveToWiki={saveBriefToWiki} />;

    /* ───── VAULT v2 · CEO capital-intelligence dashboard ─────
       The CEO landing for VAULT. The Inventory-Manager surface (overview /
       queue / item / action) is preserved beneath it at vault/im + sub-routes. */
    if (route === 'vault' && !subId)                            return <VaultCeoDashboard
                                        user={(authCtx.persona || 'Rohan').split(' ')[0]}
                                        onCrossModule={(mod) => setRoute(mod)} />;
    if (route === 'vault' && subId === 'im')                    return <VaultOverviewPage
                                        user={(authCtx.persona || 'Asha').split(' ')[0]}
                                        items={vaultItems}
                                        actions={vaultActions}
                                        onRunScan={runVaultScan}
                                        onOpenQueue={() => setRoute('vault', 'queue')}
                                        onOpenItem={(id) => setRoute('vault', `items/${id}`)}
                                        onOpenAction={(id) => setRoute('vault', `actions/${id}`)}
                                        onDraftAction={draftVaultAction}
                                        onSubmit={submitVaultAction}
                                        onDiscard={discardVaultAction}
                                        onRefine={refineVaultAction}
                                        onExport={exportVaultList}
                                        onOpenDrawer={(id) => openEvidence(id)}
                                        onOpenCmdK={() => setCmdkOpen(true)} />;
    if (route === 'vault' && subId === 'queue')                  return <VaultQueuePage
                                        items={vaultItems}
                                        onOpenItem={(id) => setRoute('vault', `items/${id}`)}
                                        onDraftAction={draftVaultAction}
                                        onRunScan={runVaultScan}
                                        onExport={exportVaultList}
                                        onOpenCmdK={() => setCmdkOpen(true)} />;
    if (route === 'vault' && subId?.startsWith('items/'))        return <VaultItemDetailPage
                                        item={vaultItems.find(i => i.id === subId.slice(6))}
                                        onBack={() => setRoute('vault', 'queue')}
                                        onDraftAction={draftVaultAction}
                                        onAssignOwner={assignVaultOwner}
                                        onCreateKraftRemake={createKraftRemakeBrief}
                                        onOpenSignals={(sid) => setRoute('orbit', 'signals')} />;
    if (route === 'vault' && subId?.startsWith('actions/'))      return <VaultActionDetailPage
                                        action={vaultActions.find(a => a.id === subId.slice(8))}
                                        items={vaultItems}
                                        onBack={() => setRoute('vault')}
                                        onSubmit={submitVaultAction}
                                        onRefine={refineVaultAction}
                                        onDiscard={discardVaultAction}
                                        onCosignPricing={requestVaultCosignPricing}
                                        onCosignLogistics={requestVaultCosignLogistics} />;

    /* ───── PRICE (Wave E · Pricing Lead) · architecture §8.4 ───── */
    if (route === 'price' && !subId)                              return <PriceOverviewPage
                                        user={(authCtx.persona || 'Karan').split(' ')[0]}
                                        rates={priceRates}
                                        quotes={priceQuotes}
                                        cosignQueue={pricingCosignQueue}
                                        marginRisk={marginRiskItems}
                                        onRefreshRates={refreshPriceRates}
                                        onCreateQuote={createPriceQuote}
                                        onOpenCosign={(id) => setRoute('price', `cosign/${id}`)}
                                        onApproveCosign={approvePricingCosign}
                                        onRejectCosign={openPricingReject}
                                        onOpenQuotes={() => setRoute('price', 'quotes')}
                                        onOpenRates={() => setRoute('price', 'rates')}
                                        onOpenMarginRisk={() => setRoute('price', 'margin-risk')}
                                        onOpenCmdK={() => setCmdkOpen(true)} />;
    if (route === 'price' && subId === 'cosign')                   return <PriceCosignQueuePage
                                        items={pricingCosignQueue}
                                        onOpen={(id) => setRoute('price', `cosign/${id}`)}
                                        onApprove={approvePricingCosign}
                                        onReject={openPricingReject}
                                        onOpenCmdK={() => setCmdkOpen(true)} />;
    if (route === 'price' && subId?.startsWith('cosign/'))         return <PriceCosignDetailPage
                                        item={pricingCosignQueue.find(c => c.id === subId.slice(7))}
                                        onBack={() => setRoute('price', 'cosign')}
                                        onApprove={approvePricingCosign}
                                        onReject={openPricingReject}
                                        onRefine={refinePricingCosign}
                                        onOpenFormula={openPriceFormula} />;
    if (route === 'price' && subId === 'rates')                    return <PriceRatesPage
                                        rates={priceRates}
                                        onRefreshRates={refreshPriceRates}
                                        onEditRate={editPriceRate}
                                        onOpenCmdK={() => setCmdkOpen(true)} />;
    if (route === 'price' && subId === 'quotes')                   return <PriceQuotesPage
                                        quotes={priceQuotes}
                                        onOpen={(id) => setRoute('price', `quotes/${id}`)}
                                        onCreateQuote={createPriceQuote}
                                        onOpenCmdK={() => setCmdkOpen(true)} />;
    if (route === 'price' && subId?.startsWith('quotes/'))         return <PriceQuoteDetailPage
                                        quote={priceQuotes.find(q => q.id === subId.slice(7))}
                                        onBack={() => setRoute('price', 'quotes')}
                                        onSimulate={simulatePriceQuote}
                                        onSubmit={submitPriceQuote}
                                        onApprove={approvePriceQuote}
                                        onExport={exportPriceQuote}
                                        onOpenFormula={openPriceFormula} />;
    if (route === 'price' && subId === 'margin-risk')              return <PriceMarginRiskPage
                                        items={marginRiskItems}
                                        onOpenSku={(id) => setRoute('price', `skus/${id}`)}
                                        onForwardToVault={forwardMarginRiskToVault}
                                        onOpenCmdK={() => setCmdkOpen(true)} />;
    if (route === 'price' && subId?.startsWith('skus/'))           return <PriceSkuDetailPage
                                        skuId={subId.slice(5)}
                                        onBack={() => setRoute('price', 'margin-risk')} />

    /* ───── KRAFT (Wave E · Designer · K1 chat-first surface) · architecture §8.5 ───── */
    if (route === 'kraft' && !subId) return <window.KraftStudioPage
                                        designerInitials={authCtx.initials || 'VS'}
                                        designerName={(authCtx.persona || 'Designer').split(' ')[0]}
                                        pushToast={pushToast}
                                        openCmdK={() => setCmdkOpen(true)}
                                        openDrawer={() => openEvidence()}
                                        pushDecision={(d) => setDecisions(prev => [d, ...prev])}
                                        briefs={briefs}
                                        onUpdateBrief={updateBrief} />;

    return <window.ModulePage moduleId={route}
                              role={authCtx.role}
                              tenantId={authCtx.tenantId}
                              openDrawer={() => openEvidence()}
                              openModal={() => openReject(decisions.find(d => d.status === 'pending')?.id || null)}
                              openCmdK={() => setCmdkOpen(true)} />;
  };

  return (
    <div className="kv">
      <div className="k-shell">
        <TopBar
          tenant={authCtx.tenant}
          user={authCtx.initials || 'RP'}
          userName={authCtx.persona || 'CEO'}
          userEmail={authCtx.email || `${(authCtx.persona || 'rohan').split(' ')[0].toLowerCase()}@${authCtx.tenantId || 'acme'}.com`}
          approvalCount={decisions.filter(d => d.status === 'pending').length}
          onCmdK={() => setCmdkOpen(true)}
          onBell={() => setNotifsOpen(true)}
          onAvatar={() => setAvatarMenuOpen(o => !o)}
          avatarMenuOpen={avatarMenuOpen}
          onSignOut={onSignOut}
          authMethod={authCtx.method}
          onShortcuts={() => setShortcutsOpen(true)}
          onProfile={() => setProfileOpen(true)}
          onSwitchWorkspace={() => setWswOpen(true)}
        />
        <Rail active={route}
              onNavigate={(id) => setRoute(id)}
              role={authCtx.role}
              canAccess={canAccess} />
        <main className="k-shell-main">
          {renderPage()}
        </main>
      </div>

      {/* Evidence drawer · scoped to current decision */}
      <Drawer
        open={drawerOpen}
        eyebrow={drawerDecision ? `Evidence · ${drawerDecision.claims?.length || 0} claims · ${drawerDecision.runId}` : 'Evidence'}
        title={drawerDecision?.title || 'Evidence'}
        onClose={() => setDrawerOpen(false)}
        footer={
          drawerDecision && drawerDecision.status === 'pending' ? (
            <>
              <Btn variant="ghost" onClick={() => setDrawerOpen(false)}>Close</Btn>
              <Btn onClick={() => { setDrawerOpen(false); setRoute('approvals', drawerDecision.id); }}>Open detail</Btn>
              <Btn variant="primary" kbd="⌘⏎"
                   onClick={() => { setDrawerOpen(false); approveDecision(drawerDecision.id); }}>
                Approve &amp; sign
              </Btn>
            </>
          ) : (
            <Btn variant="ghost" onClick={() => setDrawerOpen(false)}>Close</Btn>
          )
        }
      >
        {drawerDecision ? (
          <div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
            <div style={{ display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap' }}>
              <VerificationStamp>{drawerDecision.stamp}</VerificationStamp>
              <RunBadge id={drawerDecision.runId} version={drawerDecision.runVersion} when={drawerDecision.runWhen} />
            </div>
            <div className="k-evidence-tabs">
              <Chip variant="solid">Claims</Chip>
              <Chip variant="mute">Receipts</Chip>
              <Chip variant="mute">Math</Chip>
            </div>
            {(drawerDecision.claims || []).map((c, i) => (
              <div className="k-evidence-claim" key={i}>
                <div className="t-eyebrow" style={{ marginBottom: 6 }}>Claim {i + 1}</div>
                <div className="k-claim-body">{c.text}</div>
                <div className="k-claim-receipt">
                  <Chip variant="info">{c.source}</Chip>
                  <span className="t-mono" style={{ fontSize: 11 }}>{c.receipt}</span>
                </div>
              </div>
            ))}
            <div style={{ marginTop: 8 }}>
              <div className="t-eyebrow" style={{ marginBottom: 10 }}>Approval chain</div>
              <ApprovalChain mod={drawerDecision.mod} steps={drawerDecision.chain} />
            </div>
          </div>
        ) : (
          <div className="t-meta">No decision selected.</div>
        )}
      </Drawer>

      {/* Reject modal · with reason */}
      <Modal
        open={!!rejectId}
        title="Reject &amp; return?"
        onClose={closeReject}
        footer={
          <>
            <Btn variant="ghost" onClick={closeReject}>Cancel</Btn>
            <Btn variant="reject" icon="cross" onClick={confirmReject} kbd="⌘⇧⏎">
              Reject &amp; return
            </Btn>
          </>
        }
      >
        <p>
          <span className="t-mono">{rejectId}</span> will be returned to its author with your note.
          The workflow will not execute. This is an audited action.
        </p>
        <div className="k-field" style={{ marginTop: 14 }}>
          <div className="k-field-label">Reason · required</div>
          <textarea className="k-textarea"
                    value={rejectReason}
                    onChange={(e) => setRejectReason(e.target.value)}
                    rows={4} />
        </div>
      </Modal>

      {/* Pricing reject modal — captures the reason for cross-module cosign rejection */}
      <Modal
        open={!!pricingRejectId}
        title="Reject co-sign request?"
        onClose={closePricingReject}
        footer={
          <>
            <Btn variant="ghost" onClick={closePricingReject}>Cancel</Btn>
            <Btn variant="reject" icon="cross" onClick={confirmPricingReject} kbd="⌘⇧⏎">
              Reject &amp; return
            </Btn>
          </>
        }
      >
        <p>
          <span className="t-mono">{pricingRejectId}</span> will be returned to its source author
          (IM for VAULT, HoM for ORBIT) with your reason. No external write occurs.
          This is an audited action.
        </p>
        <div className="k-field" style={{ marginTop: 14 }}>
          <div className="k-field-label">Reason · required</div>
          <textarea className="k-textarea"
                    value={pricingRejectReason}
                    onChange={(e) => setPricingRejectReason(e.target.value)}
                    rows={4}
                    placeholder="What needs to change?" />
        </div>
      </Modal>

      {/* Revise modal — captures the constraint for agent re-run.
          Module-aware: KRAFT shows design presets, optionally scoped
          to a specific tile when the CEO selected one before clicking
          Revise. Other modules keep the original cohort/pricing presets. */}
      <Modal
        open={!!reviseId}
        title={reviseIsKraft
          ? (reviseDesignId ? `Refine ${reviseDesignId}` : 'Request a revision · whole launch pack')
          : 'Request a revision'}
        onClose={closeRevise}
        footer={
          <>
            <Btn variant="ghost" onClick={closeRevise}>Cancel</Btn>
            <Btn variant="primary" onClick={confirmRevise} kbd="⌘⏎"
                 disabled={!reviseConstraint.trim()}>
              {reviseIsKraft ? (reviseDesignId ? 'Send refine to designer' : 'Send for revision') : 'Send for revision'}
            </Btn>
          </>
        }
      >
        {reviseIsKraft ? (
          <p>
            Sent to <strong>{reviseDecision?.designerName || 'the designer'}</strong>
            {reviseDesignId
              ? <> as a refine on <span className="t-mono">{reviseDesignId}</span> — they'll see a new turn in the session
                  with your constraint and produce a v+1 of that design (lineage preserved).</>
              : <> as a revision on the whole launch pack — they'll regenerate the directions with your constraint
                  before re-submitting.</>}
            {' '}This is an audited action.
          </p>
        ) : (
          <p>
            The agent will re-run for <span className="t-mono">{reviseId}</span> with your constraint.
            A new run version will be created, linked to the current one. This is an audited action.
          </p>
        )}
        <div className="k-field" style={{ marginTop: 14 }}>
          <div className="k-field-label">Constraint or feedback · required</div>
          <textarea className="k-textarea"
                    value={reviseConstraint}
                    onChange={(e) => setReviseConstraint(e.target.value)}
                    rows={4}
                    placeholder={reviseIsKraft
                      ? (reviseDesignId
                          ? 'What should change on this design? e.g. "Smaller centre, drop the side stones."'
                          : 'What should the next batch try? e.g. "More minimalist, switch to platinum."')
                      : 'What should the agent try differently?'} />
        </div>
        <div style={{ marginTop: 12 }}>
          <div className="t-meta" style={{ marginBottom: 8 }}>Or pick a common constraint:</div>
          <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
            {revisePresets.map(p => (
              <Chip key={p} variant="mute" onClick={() => setReviseConstraint(p)}>{p}</Chip>
            ))}
          </div>
        </div>
      </Modal>

      {/* Command palette */}
      <CommandPalette
        open={cmdkOpen}
        onClose={() => setCmdkOpen(false)}
        sections={cmdKSections}
        onPick={onCmdKPick}
      />

      {/* Notifications drawer · bell icon target */}
      <NotificationsDrawer
        open={notifsOpen}
        notifs={notifications}
        onClose={() => setNotifsOpen(false)}
        onMarkAll={markAllNotifsRead}
        onPick={(n) => {
          setNotifications(ns => ns.map(x => x.id === n.id ? { ...x, read: true } : x));
          if (n.runId) {
            setNotifsOpen(false);
            setRoute('approvals', n.runId);
          }
        }}
      />

      {/* Shortcuts overlay · ? key */}
      <ShortcutsOverlay
        open={shortcutsOpen}
        onClose={() => setShortcutsOpen(false)}
      />

      {/* Profile & preferences · ⌘, ·
          maps to architecture §4.6 (provisioning) + §4.8 (users / identity_accounts).
          Backend: GET/PATCH /api/profile + GET/PATCH /api/profile/preferences. */}
      <ProfileOverlay
        open={profileOpen}
        onClose={() => setProfileOpen(false)}
        user={{
          name: authCtx.persona || 'CEO',
          email: authCtx.email || `${(authCtx.persona || 'rohan').split(' ')[0].toLowerCase()}@${authCtx.tenantId || 'acme'}.com`,
          role: authCtx.role,
          roleLabel: (ROLES.find(r => r.id === authCtx.role)?.name) || 'CEO',
          tenant: authCtx.tenant,
          tenantId: authCtx.tenantId,
          initials: authCtx.initials,
          authMethod: authCtx.method,
          lastSignInLabel: 'Just now · this session',
        }}
        prefs={userPrefs}
        onPrefChange={setPref}
        onOpenShortcuts={() => { setProfileOpen(false); setShortcutsOpen(true); }}
        onSignOut={() => { setProfileOpen(false); onSignOut(); }}
      />

      {/* Switch workspace / tenant · ⌘⇧O ·
          maps to architecture §1.2 (subdomain → tenant) + §4.8 (user_workspaces).
          Backend: GET /api/workspaces + POST /api/workspaces/switch. */}
      <WorkspaceSwitcher
        open={wswOpen}
        onClose={() => setWswOpen(false)}
        currentTenantId={authCtx.tenantId}
        tenants={Object.values(TENANT_REGISTRY).map(t => ({
          id: t.id, name: t.name, persona: t.persona,
          role: (ROLES.find(r => r.id === authCtx.role)?.name) || 'CEO',
        }))}
        workspaces={
          // Architecture §4.6 default workspaces. CEO gets none (the whole tenant is the surface).
          authCtx.role === 'designer'  ? [{ id: 'ws-kraft-priv',  name: 'My KRAFT workspace',    module: 'kraft', scope: 'private' }] :
          authCtx.role === 'inventory' ? [{ id: 'ws-vault-queue', name: 'VAULT queue',           module: 'vault', scope: 'team'    }] :
          authCtx.role === 'pricing'   ? [{ id: 'ws-price-quote', name: 'PRICE quote workspace', module: 'price', scope: 'team'    }] :
          authCtx.role === 'hom'       ? [{ id: 'ws-orbit-brief', name: 'ORBIT brief intake',    module: 'orbit', scope: 'team'    }] :
          []
        }
        onPickTenant={onPickTenant}
        onPickWorkspace={onPickWorkspace}
      />

      {/* Toast stack */}
      <ToastStack toasts={toasts} />

      {/* Flow indicator — always visible so the stage is obvious */}
      <FlowIndicator stage={authStage} />
    </div>
  );
};

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
