// Team Overview mount for mission-control.
// Strips the original design's own top-bar, nav-rail and tab state —
// mission-control already provides those. Renders only:
//   Sidebar (roster)  +  RoomsGrid  +  BottomPanel
// with a single agent (JARVIS) placed in JARVIS OFFICE.

const { useState, useEffect, useMemo, useRef, useLayoutEffect } = React;

// ---------------------------------------------------------------
// Agents — single source of truth lives in window.MissionState
// (see /mc-state/mission-state.js). We normalize the shape here so
// the rest of the component stays unchanged.
// ---------------------------------------------------------------
// Fallback defensivo para el caso extremo en que window.MissionState no esté
// disponible. El role/status se mantienen en su forma canónica interna (inglés)
// porque se pasan por ROLE_LABELS/STATUS_LABELS al render. El `task` y el `progress`
// van directo al panel "TAREA ACTUAL" — por eso se definen ya en español.
const AGENT_FALLBACK = [{
  id: "jarvis", name: "JARVIS", role: "Core AI",
  color: "cyan", status: "Working", room: "office", activity: "working",
  task: "Supervisando los sistemas de Mission Control", progress: 72,
}];

function normalizeAgent(a) {
  return {
    id:       a.id,
    name:     a.name,
    role:     a.role,
    color:    a.avatarVariant || a.color || "cyan",
    status:   a.status || "Working",
    room:     a.room,
    activity: a.activity || "working",
    task:     a.task || "",
    progress: typeof a.progress === "number" ? a.progress : 0,
  };
}

function readMissionAgents() {
  const src = (window.MissionState && window.MissionState.getAgents()) || AGENT_FALLBACK;
  return src.map(normalizeAgent);
}

// Hook: subscribe to MissionState and trigger a re-render on any notify().
// Any agent movement / status change / log add / task change refreshes the
// React tree so the UI reflects live state.
function useMissionTick() {
  const [, setTick] = useState(0);
  useEffect(() => {
    if (!window.MissionState) return;
    return window.MissionState.subscribe(() => setTick(v => (v + 1) | 0));
  }, []);
}

// Hook: exposes the most recent message for an agent IF it was sent in the
// last RECENT_MSG_WINDOW_MS ms. Re-evaluates every second while active so
// the "Nns ago" label stays fresh without a global re-render.
const RECENT_MSG_WINDOW_MS = 60000;
function useRecentMessage(agentId) {
  const [now, setNow] = useState(() => Date.now());
  useEffect(() => {
    const id = setInterval(() => setNow(Date.now()), 1000);
    return () => clearInterval(id);
  }, []);
  if (!agentId || !window.MissionState) return null;
  const msgs = window.MissionState.getMessagesForAgent(agentId) || [];
  if (msgs.length === 0) return null;
  const last = msgs[msgs.length - 1];
  const ts   = last.iso ? Date.parse(last.iso) : 0;
  if (!ts) return null;
  const ageMs = now - ts;
  if (ageMs < 0 || ageMs > RECENT_MSG_WINDOW_MS) return null;
  const ageSec = Math.max(1, Math.floor(ageMs / 1000));
  return { message: last, ageSec };
}

const STATUS_STYLES = {
  "Working":     { bg: "rgba(79, 216, 255, 0.15)",  fg: "#7EE8FF", dot: "#4FD8FF" },
  "Moving":      { bg: "rgba(126, 232, 255, 0.18)", fg: "#BEF2FF", dot: "#7EE8FF" },
  "Researching": { bg: "rgba(255, 210, 112, 0.15)", fg: "#FFD170", dot: "#FFC24F" },
  "Idle":        { bg: "rgba(160, 180, 210, 0.12)", fg: "#A8BDD6", dot: "#7A8FA8" },
  "In Meeting":  { bg: "rgba(193, 122, 255, 0.18)", fg: "#D1A5FF", dot: "#C17AFF" },
  "On Break":    { bg: "rgba(255, 122, 191, 0.15)", fg: "#FFA8CF", dot: "#FF7ABF" },
};
const STATUS_FALLBACK = STATUS_STYLES["Idle"];

// Etiquetas en español para display. Los valores internos siguen en inglés
// (canónicos, persistidos en localStorage, usados por toda la lógica).
// Esto permite traducir la UI sin romper datos ni lógica.
const STATUS_LABELS = {
  "Working":     "Activo",
  "Moving":      "En tránsito",
  "Researching": "Investigando",
  "Idle":        "Inactivo",
  "In Meeting":  "En reunión",
  "On Break":    "En pausa",
};
const statusLabel = (s) => STATUS_LABELS[s] || s;

const PRIORITY_LABELS = {
  low:      "Baja",
  medium:   "Media",
  high:     "Alta",
  critical: "Crítica",
};
const priorityLabel = (p) => PRIORITY_LABELS[p] || p;

const TASK_STATUS_LABELS = {
  running:   "En ejecución",
  paused:    "Pausada",
  completed: "Completada",
  cancelled: "Cancelada",
};
const taskStatusLabel = (s) => TASK_STATUS_LABELS[s] || s;

const ROLE_LABELS = {
  Strategy:  "Estrategia",
  Security:  "Seguridad",
  Design:    "Diseño",
  Analytics: "Analítica",
  Community: "Comunidad",
  Data:      "Datos",
  Ops:       "Operaciones",
  Research:  "Investigación",
  Writer:    "Escritura",
  Marketing: "Marketing",
  "Core AI": "Núcleo IA",
};
const roleLabel = (r) => ROLE_LABELS[r] || r;

// ------------------------------------------------------------
// ADD-AGENT form: roles, avatar variants, helpers.
// ------------------------------------------------------------
const ROLE_OPTIONS = [
  "Strategy", "Security", "Design", "Analytics",
  "Community", "Data", "Ops", "Research", "Writer", "Marketing",
];
const STATUS_OPTIONS = ["Working", "Idle", "In Meeting", "Researching", "On Break"];
const VARIANT_OPTIONS = ["auto", "cyan", "teal", "green", "violet", "pink", "magenta", "orange", "amber", "lime"];

// When the operator picks "auto" variant, derive one from the role; fall back
// to a deterministic cycle so successive auto-adds don't collide on colour.
const ROLE_VARIANT = {
  Strategy:  "teal",
  Security:  "green",
  Design:    "violet",
  Analytics: "cyan",
  Community: "magenta",
  Data:      "orange",
  Ops:       "amber",
  Research:  "violet",
  Writer:    "pink",
  Marketing: "pink",
};
const AUTO_CYCLE = ["teal", "violet", "pink", "orange", "amber", "green", "magenta", "lime"];

function statusToActivity(status) {
  const s = String(status || "").toLowerCase();
  if (s.indexOf("meet")      !== -1) return "meeting";
  if (s.indexOf("research")  !== -1) return "researching";
  if (s.indexOf("idle")      !== -1) return "idle";
  if (s.indexOf("break")     !== -1) return "idle";
  if (s.indexOf("move")      !== -1) return "moving";
  return "working";
}

function slugifyId(name) {
  return String(name || "")
    .trim().toLowerCase()
    .replace(/[^a-z0-9]+/g, "-")
    .replace(/^-+|-+$/g, "")
    .slice(0, 24);
}

function uniqueAgentId(baseId, existing) {
  if (!existing.some(a => a.id === baseId)) return baseId;
  let i = 2;
  while (existing.some(a => a.id === baseId + "-" + i)) i += 1;
  return baseId + "-" + i;
}

function resolveAvatarVariant(chosen, role, count) {
  if (chosen && chosen !== "auto") return chosen;
  if (ROLE_VARIANT[role]) return ROLE_VARIANT[role];
  return AUTO_CYCLE[count % AUTO_CYCLE.length];
}

// Mapping from core-room id → its rich diorama component.
const CORE_ROOM_COMPONENTS = {
  "office":    JarvisOffice,
  "strategy":  StrategyRoom,
  "research":  ResearchLab,
  "content":   ContentStudio,
  "auto":      AutomationBay,
  "analytics": AnalyticsRoom,
};

// Default icon per room type → used for the header chip.
const ROOM_TYPE_ICON = {
  command:   "office",
  planning:  "chess",
  research:  "flask",
  creative:  "camera",
  ops:       "gear",
  data:      "chart",
  custom:    "grid",
};

// Localised label per room type.
const ROOM_TYPE_LABELS = {
  command:   "Comando",
  planning:  "Planificación",
  research:  "Investigación",
  creative:  "Creativo",
  ops:       "Operaciones",
  data:      "Datos",
  custom:    "Personalizada",
};

// Palette key per room type for auto-colour.
const ROOM_TYPE_TINT_KEY = {
  command:   "cyan",
  planning:  "green",
  research:  "violet",
  creative:  "green",
  ops:       "orange",
  data:      "violet",
  custom:    "cyan",
};

// Derive the renderable ROOMS list from MissionState on every render. The
// six core rooms keep their rich dioramas; custom rooms created at runtime
// render with GenericRoom. Order preserves state.rooms ordering so new rooms
// appear after the core ones.
function deriveRooms() {
  const ms = window.MissionState;
  const msRooms = ms && typeof ms.getRooms === "function" ? ms.getRooms() : [];
  return msRooms.map(r => {
    const icon = r.isCore === true
      ? ({ office:"office", strategy:"chess", research:"flask", content:"camera", auto:"gear", analytics:"chart" })[r.id]
      : (ROOM_TYPE_ICON[r.type] || "grid");
    const tint = r.tint || ({
      office:"#4FD8FF", strategy:"#4FE0A8", research:"#B78BFF",
      content:"#4FE08A", auto:"#FFA55C", analytics:"#C17AFF"
    })[r.id] || "#7EE8FF";
    const Component = r.isCore === true && CORE_ROOM_COMPONENTS[r.id]
      ? CORE_ROOM_COMPONENTS[r.id]
      : null; // null → GenericRoom used in render with tint prop.
    return {
      id:        r.id,
      name:      r.title,
      icon:      icon,
      tint:      tint,
      isCore:    r.isCore === true,
      Component: Component,
    };
  });
}

// ---------------------------------------------------------------
// Inline icon set (subset used by sidebar / rooms / bottom panel)
// ---------------------------------------------------------------
function Icon({ name, size = 14, color = "currentColor" }) {
  const s = { width: size, height: size, display: "inline-block", verticalAlign: "middle" };
  const c = { fill: "none", stroke: color, strokeWidth: 1.6, strokeLinecap: "round", strokeLinejoin: "round" };
  switch (name) {
    case "office":   return <svg viewBox="0 0 24 24" style={s}><g {...c}><rect x="3" y="4" width="18" height="14" rx="1" /><path d="M3 8h18M7 12h3M7 15h3M14 12h3M14 15h3" /></g></svg>;
    case "chess":    return <svg viewBox="0 0 24 24" style={s}><g {...c}><path d="M10 2h4l-1 3h1l1 3-2 1v4l2 4H9l2-4v-4L9 8l1-3h1z" /></g></svg>;
    case "flask":    return <svg viewBox="0 0 24 24" style={s}><g {...c}><path d="M9 3h6M10 3v6l-5 10a2 2 0 0 0 1.8 3h10.4a2 2 0 0 0 1.8-3l-5-10V3" /><path d="M7 15h10" /></g></svg>;
    case "camera":   return <svg viewBox="0 0 24 24" style={s}><g {...c}><rect x="2" y="7" width="14" height="12" rx="1.5" /><path d="M16 11l5-3v8l-5-3z" /><circle cx="9" cy="13" r="3" /></g></svg>;
    case "gear":     return <svg viewBox="0 0 24 24" style={s}><g {...c}><circle cx="12" cy="12" r="3" /><path d="M12 2v3M12 19v3M4.2 4.2l2.1 2.1M17.7 17.7l2.1 2.1M2 12h3M19 12h3M4.2 19.8l2.1-2.1M17.7 6.3l2.1-2.1" /></g></svg>;
    case "chart":    return <svg viewBox="0 0 24 24" style={s}><g {...c}><path d="M3 20V4M3 20h18" /><path d="M7 16v-4M11 16V8M15 16v-6M19 16v-2" /></g></svg>;
    case "plus":     return <svg viewBox="0 0 24 24" style={s}><g {...c}><path d="M12 5v14M5 12h14" /></g></svg>;
    case "send":     return <svg viewBox="0 0 24 24" style={s}><g {...c}><path d="M3 11l18-8-8 18-2-8-8-2z" /></g></svg>;
    case "list":     return <svg viewBox="0 0 24 24" style={s}><g {...c}><path d="M8 6h13M8 12h13M8 18h13M3 6h.01M3 12h.01M3 18h.01" /></g></svg>;
    case "msg":      return <svg viewBox="0 0 24 24" style={s}><g {...c}><path d="M21 11.5a8.4 8.4 0 0 1-9 8.5 8.5 8.5 0 0 1-3.9-.9L3 21l1.9-5a8.5 8.5 0 0 1-.9-3.9A8.4 8.4 0 0 1 12.5 3a8.4 8.4 0 0 1 8.5 8.5z" /></g></svg>;
    default: return null;
  }
}

// ---------------------------------------------------------------
// SIDEBAR — agent roster (single agent JARVIS)
// ---------------------------------------------------------------
function Sidebar({ agents, active, onSelect, onOpenAddAgent }) {
  const showEmptySlot = agents.length < 2;
  const onAddAgent = () => {
    if (typeof onOpenAddAgent === "function") onOpenAddAgent();
  };
  return (
    <div style={sbStyles.sidebar}>
      <div style={sbStyles.header}><div style={sbStyles.headerLabel}>ROSTER DE AGENTES</div></div>
      <div style={sbStyles.list}>
        {agents.map(a => {
          const isActive = active === a.id;
          const s = STATUS_STYLES[a.status] || STATUS_FALLBACK;
          return (
            <button key={a.id} onClick={() => onSelect(a.id)}
              style={{ ...sbStyles.card, ...(isActive ? sbStyles.cardActive : {}) }}>
              <div style={sbStyles.avatarWrap}>
                <div className="agent-halo" style={{ ...sbStyles.avatarBg,
                  background: `radial-gradient(circle, ${ROBOT_PALETTES[a.color].glow}33, transparent 70%)` }} />
                <RobotAvatar color={a.color} size={42} />
              </div>
              <div style={sbStyles.cardText}>
                <div style={sbStyles.cardName}>{a.name}</div>
                <div style={sbStyles.cardRole}>{roleLabel(a.role)}</div>
              </div>
              <div style={{ ...sbStyles.statusPill, background: s.bg, color: s.fg,
                boxShadow: isActive ? `0 0 12px ${s.dot}40` : "none" }}>
                {statusLabel(a.status)}
              </div>
            </button>
          );
        })}
        {showEmptySlot && (
          <div style={sbStyles.emptySlot} aria-label="Slot vacío del roster">
            <div style={sbStyles.emptySlotTitle}>Aún no hay más agentes</div>
            <div style={sbStyles.emptySlotSub}>Usa <em>Agregar agente</em> para expandir el roster.</div>
          </div>
        )}
      </div>
      <button style={sbStyles.addBtn} onClick={onAddAgent} aria-label="Agregar agente">
        <Icon name="plus" size={14} color="#7EE8FF" />
        <span>Agregar agente</span>
      </button>
    </div>
  );
}

const sbStyles = {
  sidebar: {
    width: 258, minWidth: 258,
    background: "linear-gradient(180deg, rgba(15, 28, 48, 0.9), rgba(8, 16, 30, 0.85))",
    border: "1px solid rgba(79, 216, 255, 0.15)",
    borderRadius: "14px", margin: "0 10px 0 0",
    display: "flex", flexDirection: "column", padding: "14px",
    boxShadow: "inset 0 0 40px rgba(79, 216, 255, 0.04)",
  },
  header: { padding: "2px 4px 10px" },
  headerLabel: { fontSize: 11, letterSpacing: 2, color: "#7EE8FF", fontWeight: 700, fontFamily: "Inter, sans-serif" },
  list: { display: "flex", flexDirection: "column", gap: 6, flex: 1, overflow: "hidden" },
  card: {
    display: "flex", alignItems: "center", gap: 10, padding: "7px 9px",
    background: "rgba(18, 32, 54, 0.5)", border: "1px solid rgba(79, 216, 255, 0.08)",
    borderRadius: 10, cursor: "pointer", textAlign: "left", transition: "all 0.2s ease",
  },
  cardActive: {
    background: "linear-gradient(135deg, rgba(79, 216, 255, 0.18), rgba(79, 216, 255, 0.04))",
    border: "1px solid rgba(79, 216, 255, 0.5)",
    boxShadow: "0 0 18px rgba(79, 216, 255, 0.2), inset 0 0 20px rgba(79, 216, 255, 0.05)",
  },
  avatarWrap: { position: "relative", width: 42, height: 42, flexShrink: 0,
    display: "flex", alignItems: "center", justifyContent: "center" },
  avatarBg: { position: "absolute", inset: -4, borderRadius: "50%" },
  cardText: { flex: 1, minWidth: 0 },
  cardName: { color: "#EAF6FF", fontWeight: 600, fontSize: 13, fontFamily: "Inter" },
  cardRole: { color: "#7A95B4", fontSize: 10.5, marginTop: 1, fontFamily: "Inter" },
  statusPill: {
    fontSize: 9, padding: "3px 7px", borderRadius: 6,
    fontFamily: "Inter", fontWeight: 600, whiteSpace: "nowrap",
    border: "1px solid rgba(255,255,255,0.05)",
  },
  emptySlot: {
    marginTop: 8, padding: "12px 12px",
    border: "1px dashed rgba(79, 216, 255, 0.22)",
    borderRadius: 10,
    background: "rgba(79, 216, 255, 0.03)",
    display: "flex", flexDirection: "column", gap: 4,
    textAlign: "center",
  },
  emptySlotTitle: {
    fontFamily: "Inter", fontSize: 11, fontWeight: 600,
    color: "rgba(234, 246, 255, 0.55)", letterSpacing: 0.2,
  },
  emptySlotSub: {
    fontFamily: "Inter", fontSize: 10, lineHeight: 1.4,
    color: "rgba(255, 255, 255, 0.3)",
  },
  addBtn: {
    marginTop: 10, display: "flex", alignItems: "center", justifyContent: "center", gap: 6,
    padding: "10px", background: "rgba(79, 216, 255, 0.06)",
    border: "1px dashed rgba(79, 216, 255, 0.35)", borderRadius: 10,
    color: "#7EE8FF", fontSize: 12, fontWeight: 600, fontFamily: "Inter", cursor: "pointer",
  },
};

// ---------------------------------------------------------------
// ROOMS GRID — 2×3. JARVIS only lives in "office".
// ---------------------------------------------------------------
function RoomsGrid({ agentsByRoom, selectedRoom, onSelectRoom, onRequestAddRoom, onRequestEditRoom, onRequestRemoveRoom }) {
  const [hoveredRoom, setHoveredRoom] = useState(null);
  const rooms = deriveRooms();

  return (
    <div style={rgStyles.wrap}>
      <div style={rgStyles.label}>VISTA EN VIVO</div>
      <div style={rgStyles.grid}>
        {rooms.map(r => {
          const isSelected = selectedRoom === r.id;
          const isHovered  = hoveredRoom === r.id;
          const borderColor = isSelected ? r.tint
                             : isHovered  ? r.tint + "AA"
                             : "rgba(79, 216, 255, 0.18)";
          const boxShadow = isSelected
            ? `0 0 30px ${r.tint}55, inset 0 0 40px rgba(79, 216, 255, 0.05), inset 0 1px 0 ${r.tint}66`
            : isHovered
              ? `0 0 22px ${r.tint}33, inset 0 0 30px rgba(79, 216, 255, 0.03)`
              : "0 4px 16px rgba(0,0,0,0.35), inset 0 0 30px rgba(79, 216, 255, 0.02)";
          const agentsInRoom = agentsByRoom[r.id] || [];
          return (
            <div key={r.id}
              role="button"
              tabIndex={0}
              aria-pressed={isSelected}
              aria-label={`Seleccionar ${r.name}`}
              onMouseEnter={() => setHoveredRoom(r.id)}
              onMouseLeave={() => setHoveredRoom(null)}
              onClick={() => onSelectRoom && onSelectRoom(r.id)}
              onKeyDown={(e) => {
                if (e.key === "Enter" || e.key === " ") {
                  e.preventDefault();
                  onSelectRoom && onSelectRoom(r.id);
                }
              }}
              style={{
                ...rgStyles.room,
                cursor: "pointer",
                outline: "none",
                borderColor,
                boxShadow,
              }}>
              <div style={rgStyles.roomHeader}>
                <div style={rgStyles.roomTitle}>
                  <div style={{ ...rgStyles.roomIcon, background: r.tint + "22", color: r.tint, border: `1px solid ${r.tint}55` }}>
                    <Icon name={r.icon} size={13} color={r.tint} />
                  </div>
                  <span style={{ color: "#EAF6FF", fontWeight: 700, fontSize: 11, letterSpacing: 1.6 }}>{r.name}</span>
                </div>
                <div style={rgStyles.roomMeta}>
                  <span style={{ color: r.tint, fontSize: 9.5, fontWeight: 700, fontFamily: "'JetBrains Mono', monospace", opacity: 0.85 }}>
                    {agentsInRoom.length} · AGENTES
                  </span>
                  <span style={{ ...rgStyles.liveDot, background: r.tint, boxShadow: `0 0 6px ${r.tint}`, animation: "pulseGlow 1.8s ease-in-out infinite" }} />
                  {!r.isCore && (
                    <button
                      type="button"
                      className="rg-room-edit"
                      onClick={(e) => { e.stopPropagation(); if (onRequestEditRoom) onRequestEditRoom(r.id); }}
                      aria-label={`Editar sala ${r.name}`}
                      title="Editar sala"
                      style={{ color: r.tint, borderColor: r.tint + "55" }}
                    >✎</button>
                  )}
                  {!r.isCore && (
                    <button
                      type="button"
                      className="rg-room-remove"
                      onClick={(e) => { e.stopPropagation(); if (onRequestRemoveRoom) onRequestRemoveRoom(r.id); }}
                      aria-label={`Eliminar sala ${r.name}`}
                      title="Eliminar sala"
                    >×</button>
                  )}
                </div>
              </div>
              <div style={rgStyles.roomBody}>
                {r.Component
                  ? <r.Component agents={agentsInRoom} />
                  : <GenericRoom agents={agentsInRoom} tint={r.tint} title={r.name} />}
              </div>
            </div>
          );
        })}

        {/* Tile "+ Nueva sala" — siempre visible al final del grid. */}
        <button
          type="button"
          className="rg-new-room-tile"
          onClick={() => onRequestAddRoom && onRequestAddRoom()}
          aria-label="Crear nueva sala"
        >
          <span className="rg-new-room-plus">＋</span>
          <span className="rg-new-room-label">NUEVA SALA</span>
          <span className="rg-new-room-hint">Click para crear</span>
        </button>
      </div>
    </div>
  );
}

const rgStyles = {
  wrap: { flex: 1, display: "flex", flexDirection: "column", minHeight: 0, gap: 8 },
  label: { fontSize: 11, letterSpacing: 2, color: "#7EE8FF", fontWeight: 700, padding: "0 2px" },
  grid: { flex: 1, display: "grid",
    // Responsive fluid columns: fit as many as the container allows between
    // a 280 px minimum (readable diorama) and a 360 px maximum (avoids giant
    // cards on ultra-wide displays). 1fr stretches cards so no dead band
    // remains on the right edge of the last row.
    gridTemplateColumns: "repeat(auto-fit, minmax(clamp(280px, 30%, 360px), 1fr))",
    // Fixed row height (not minmax + 1fr). With 1fr grid preferred "squeeze
    // every row to fit the container" over "overflow + scroll", so adding
    // a third row silently shrank each row to ~226 px — below the diorama's
    // readable floor. A rigid track size forces the grid to overflow instead
    // of compressing, which is exactly what activates overflowY: auto.
    gridAutoRows: 280,
    // With pocas salas, don't distribute the leftover vertical space as
    // inter-row gap — keep cards anchored to the top. Scroll will use the
    // bottom area naturally when rows overflow.
    alignContent: "start",
    gap: 10, minHeight: 0, position: "relative",
    overflowY: "auto", overflowX: "hidden" },
  room: { background: "rgba(10, 20, 36, 0.6)", border: "1px solid rgba(79, 216, 255, 0.18)",
    borderRadius: 12, overflow: "hidden", display: "flex", flexDirection: "column",
    transition: "all 0.25s ease", position: "relative" },
  roomHeader: { display: "flex", alignItems: "center", justifyContent: "space-between",
    padding: "8px 10px", background: "rgba(10, 22, 40, 0.8)",
    borderBottom: "1px solid rgba(79, 216, 255, 0.12)", zIndex: 2 },
  roomTitle: { display: "flex", alignItems: "center", gap: 7 },
  roomIcon: { width: 22, height: 22, borderRadius: 6,
    display: "flex", alignItems: "center", justifyContent: "center" },
  roomMeta: { display: "flex", alignItems: "center", gap: 5 },
  liveDot: { width: 7, height: 7, borderRadius: "50%" },
  roomBody: { flex: 1, minHeight: 0, position: "relative" },
};

// ---------------------------------------------------------------
// BOTTOM PANEL — selected agent + task + collaborators + actions
// ---------------------------------------------------------------
function BottomPanel({ agent, onRequestRemove, onRequestEdit, onRequestDispatch, onRequestTasks, onRequestMessage, onRequestCollaborators, onRequestQueueHistory }) {
  // Real collaborators — resolved from MissionState against the selected
  // agent's active task. Survives reloads (persisted with the task).
  const collaborators = (window.MissionState && window.MissionState.getCollaboratorsForAgent(agent.id)) || [];
  const s = STATUS_STYLES[agent.status] || STATUS_FALLBACK;
  const canRemove = agent.id !== "jarvis";
  const canEdit   = !!agent;  // all agents are editable; JARVIS has protected fields inside the modal

  // ---- CURRENT TASK panel: sourced from the selected agent now that
  //      agents carry task + progress from MissionState.
  // The "En espera en <sala>" placeholder is synthesized at create/edit time
  // and persisted on agent.task. Room moves don't refresh it, so a stale
  // room name can survive across moves. Detect the placeholder shape and
  // re-render it against the agent's CURRENT room. Real tasks (anything not
  // matching the placeholder prefix) are preserved verbatim.
  const roomObj   = window.MissionState ? window.MissionState.getRoomById(agent.room) : null;
  const rawTask   = (agent.task && agent.task.trim()) || "";
  const isWaitingPlaceholder = rawTask === "" || /^en espera(\s+en\s+.+)?$/i.test(rawTask);
  const taskTitle = isWaitingPlaceholder
    ? (roomObj ? "En espera en " + roomObj.title : "En espera")
    : rawTask;
  const taskSub   = roomObj
    ? `${statusLabel(agent.status)} · ${roomObj.title}`
    : statusLabel(agent.status);
  const progress  = Math.max(0, Math.min(100, Number.isFinite(agent.progress) ? agent.progress : 0));

  // Enriquecimiento operativo: si el agente tiene una tarea activa en state,
  // se extraen prioridad y colaboradores para llenar el panel TAREA ACTUAL
  // con data útil en lugar de aire.
  const activeTaskRow = (window.MissionState && window.MissionState.getActiveTaskForAgent)
    ? window.MissionState.getActiveTaskForAgent(agent.id)
    : null;
  const taskPriority  = activeTaskRow && activeTaskRow.priority ? activeTaskRow.priority : null;
  const taskCollabCount = activeTaskRow && Array.isArray(activeTaskRow.collaborators)
    ? activeTaskRow.collaborators.length : 0;

  // Secondary buttons still log (placeholders). Dispatch now opens the modal.
  const opsLog = (message, source = "OPS") => {
    if (window.MissionState) {
      window.MissionState.addLog({ type: "OPS", source, severity: "low", message });
    }
  };
  const onDispatch  = () => { if (onRequestDispatch) onRequestDispatch(agent.id); };
  const onViewTasks = () => { if (onRequestTasks) onRequestTasks(); };
  const onMessage   = () => { if (onRequestMessage) onRequestMessage(agent.id); };
  const onAddCollab = () => { if (onRequestCollaborators) onRequestCollaborators(agent.id); };

  // "Last message sent Xs ago" indicator — shows for ~60s after a message
  // goes out. Pulls from MissionState messages for this agent.
  const recentMessage = useRecentMessage(agent.id);

  return (
    <div style={bpStyles.wrap}>
      {/* Selected Agent */}
      <div style={{ ...bpStyles.block, flex: "0 0 264px" }}>
        <div style={bpStyles.blockHeader}>AGENTE SELECCIONADO</div>
        <div style={bpStyles.agentBlock}>
          <div style={bpStyles.bigAvatarWrap}>
            <div className="agent-halo" style={{ ...bpStyles.avatarGlow,
              background: `radial-gradient(circle, ${ROBOT_PALETTES[agent.color].glow}55, transparent 68%)` }} />
            <RobotFull color={agent.color} size={80} />
            <div className="agent-shadow" style={{ ...bpStyles.avatarBase,
              background: `radial-gradient(ellipse, ${ROBOT_PALETTES[agent.color].glow}99, transparent 70%)` }} />
          </div>
          <div style={{ flex: 1, minWidth: 0 }}>
            <div style={bpStyles.agentName}>{agent.name}</div>
            <div style={bpStyles.agentRow}>
              <Icon name="chart" size={11} color="#7A95B4" />
              <span style={bpStyles.agentRole}>{roleLabel(agent.role)}</span>
            </div>
            <div style={bpStyles.agentRow}>
              <span style={{ ...bpStyles.statusDot, background: s.dot, boxShadow: `0 0 6px ${s.dot}` }} />
              <span style={{ color: s.fg, fontSize: 11.5, fontWeight: 600 }}>{statusLabel(agent.status)}</span>
            </div>
            {(canEdit || canRemove) && (
              <div className="bp-agent-actions">
                {canEdit && (
                  <button
                    type="button"
                    className="bp-action-btn bp-edit-btn"
                    onClick={() => onRequestEdit && onRequestEdit(agent.id)}
                    aria-label={`Editar ${agent.name}`}
                  >
                    ✎ Editar
                  </button>
                )}
                {canRemove && (
                  <button
                    type="button"
                    className="bp-action-btn bp-remove-btn"
                    onClick={() => onRequestRemove && onRequestRemove(agent.id)}
                    aria-label={`Eliminar ${agent.name}`}
                  >
                    × Eliminar
                  </button>
                )}
              </div>
            )}
          </div>
        </div>
      </div>

      {/* Current Task — dynamic per selected agent */}
      <div style={{ ...bpStyles.block, flex: "1 1 auto", minWidth: 216 }}>
        <div style={bpStyles.blockHeader}>TAREA ACTUAL</div>
        <div style={{ padding: "4px 4px 0" }}>
          <div style={bpStyles.taskHeadRow}>
            <div style={bpStyles.taskTitle}>{taskTitle}</div>
            {taskPriority && (
              <span style={{ ...bpStyles.taskPriorityChip, ...bpStyles["taskPrio_" + taskPriority] }}>
                {priorityLabel(taskPriority).toUpperCase()}
              </span>
            )}
          </div>
          <div style={bpStyles.taskSub}>
            {taskSub}
            {taskCollabCount > 0 && (
              <>
                <span style={bpStyles.taskSubDot}>·</span>
                <span>{taskCollabCount} {taskCollabCount === 1 ? "colaborador" : "colaboradores"}</span>
              </>
            )}
          </div>
          <div style={bpStyles.progressWrap}>
            <div style={bpStyles.progressBar}>
              <div style={{ ...bpStyles.progressFill, width: `${progress}%` }} />
            </div>
            <div style={bpStyles.progressLabel}>{progress}%</div>
          </div>
          {(() => {
            const ms = window.MissionState;
            const queuedCount  = ms && ms.getQueuedTasksForAgent ? ms.getQueuedTasksForAgent(agent.id).length  : 0;
            const historyCount = ms && ms.getHistoryForAgent     ? ms.getHistoryForAgent(agent.id).length      : 0;
            if (queuedCount === 0 && historyCount === 0) return null;
            return (
              <div className="bp-queue-chips">
                {queuedCount > 0 && (
                  <button
                    type="button"
                    className="bp-chip bp-chip-queue"
                    onClick={() => onRequestQueueHistory && onRequestQueueHistory(agent.id, "queue")}
                    aria-label={`Ver cola de ${agent.name}`}
                  >
                    <span className="bp-chip-icon">◷</span>
                    <span className="bp-chip-label">EN COLA</span>
                    <span className="bp-chip-count">{queuedCount}</span>
                  </button>
                )}
                {historyCount > 0 && (
                  <button
                    type="button"
                    className="bp-chip bp-chip-history"
                    onClick={() => onRequestQueueHistory && onRequestQueueHistory(agent.id, "history")}
                    aria-label={`Ver historial de ${agent.name}`}
                  >
                    <span className="bp-chip-icon">✓</span>
                    <span className="bp-chip-label">HISTORIAL</span>
                    <span className="bp-chip-count">{historyCount}</span>
                  </button>
                )}
              </div>
            );
          })()}
        </div>
      </div>

      {/* Collaborators */}
      <div style={{ ...bpStyles.block, flex: "0 0 184px" }}>
        <div style={bpStyles.blockHeader}>COLABORADORES</div>
        <div style={bpStyles.collabRow}>
          {collaborators.map((c, i) => {
            // collaborators come straight from MissionState.agents (avatarVariant),
            // so normalize the colour key the same way readMissionAgents does.
            const colorKey = c.avatarVariant || c.color || "cyan";
            const palette  = ROBOT_PALETTES[colorKey] || ROBOT_PALETTES.cyan;
            return (
              <div key={c.id} style={{ ...bpStyles.collabWrap, marginLeft: i === 0 ? 0 : -10 }}>
                <div className="agent-halo" style={{ ...bpStyles.collabGlow,
                  background: `radial-gradient(circle, ${palette.glow}66, transparent 70%)` }} />
                <div style={bpStyles.collabAvatar} title={`${c.name} · ${roleLabel(c.role)}`}>
                  <RobotAvatar color={colorKey} size={36} delay={i * 0.4} />
                </div>
              </div>
            );
          })}
          <button style={bpStyles.collabAdd} onClick={onAddCollab} aria-label="Agregar colaborador">
            <Icon name="plus" size={14} color="#7EE8FF" />
          </button>
        </div>
      </div>

      {/* Logs + actions — flexible so it absorbs any extra horizontal space
          while respecting the right-side breathing gutter on the wrap.
          Inner right padding of 10px gives the log card and the action
          buttons clear air from the wrap's right edge; box-sizing:border-box
          means the padding fits inside the flex-basis, no overflow added. */}
      <div style={{ ...bpStyles.block, flex: "1 1 320px", minWidth: 296, padding: "0 10px 0 2px", background: "transparent", border: "none", boxShadow: "none" }}>
        <div style={bpStyles.logs}>
          <div style={bpStyles.logLine}>
            <span style={bpStyles.logTime}>08:41</span>
            <span style={{ color: "#7EE8FF", fontWeight: 600 }}>JARVIS:</span>
            <span style={bpStyles.logText}>Sistemas nominales. En espera.</span>
          </div>
          <div style={bpStyles.logLine}>
            <span style={bpStyles.logTime}>08:39</span>
            <span style={{ color: "#7EE8FF", fontWeight: 600 }}>JARVIS:</span>
            <span style={bpStyles.logText}>Roster listo para despliegue.</span>
          </div>
          <div style={bpStyles.logLine}>
            <span style={bpStyles.logTime}>08:37</span>
            <span style={{ color: "#7EE8FF", fontWeight: 600 }}>JARVIS:</span>
            <span style={bpStyles.logText}>Mission Control en línea.</span>
          </div>
        </div>
        <div style={bpStyles.actionsCol}>
          <button style={bpStyles.primaryBtn} onClick={onDispatch} aria-label={`Desplegar a ${agent.name}`}>
            <Icon name="send" size={13} color="#0a1a2e" />
            <span>DESPLEGAR {agent.name.toUpperCase()}</span>
          </button>
          <div style={bpStyles.actionsRow}>
            <button style={bpStyles.secondaryBtn} onClick={onViewTasks} aria-label="Ver tareas">
              <Icon name="list" size={12} color="#7EE8FF" />
              <span>VER TAREAS</span>
            </button>
            <button
              style={{ ...bpStyles.secondaryBtn, background: "rgba(193, 122, 255, 0.12)", border: "1px solid rgba(193, 122, 255, 0.4)", color: "#D1A5FF" }}
              onClick={onMessage}
              aria-label="Enviar mensaje"
            >
              <Icon name="msg" size={12} color="#D1A5FF" />
              <span>MENSAJE</span>
            </button>
          </div>
          {recentMessage && (
            <div className="bp-message-hint" title={recentMessage.message.subject || recentMessage.message.body}>
              Último mensaje enviado hace {recentMessage.ageSec}s
            </div>
          )}
        </div>
      </div>
    </div>
  );
}

const bpStyles = {
  wrap: {
    display: "flex", gap: 8, padding: 10,
    marginTop: 10, marginLeft: 10, marginRight: 10,
    background: "linear-gradient(180deg, rgba(15, 28, 48, 0.6), rgba(10, 20, 36, 0.7))",
    border: "1px solid rgba(79, 216, 255, 0.18)", borderRadius: 14, height: 188,
    // Prevent the bottom panel ever pushing beyond its parent's inner width.
    // The 10 px horizontal margins give the panel a small breathing gutter so
    // it no longer sits flush against the canvas edge.
    minWidth: 0, maxWidth: "calc(100% - 20px)",
  },
  block: { background: "rgba(10, 20, 36, 0.55)", border: "1px solid rgba(79, 216, 255, 0.12)",
    borderRadius: 10, padding: "10px 12px 12px", display: "flex", flexDirection: "column", minWidth: 0,
    minHeight: 0 },
  blockHeader: { fontSize: 10, letterSpacing: 1.8, color: "#7EE8FF", fontWeight: 700, marginBottom: 8 },
  agentBlock: { display: "flex", alignItems: "flex-start", gap: 12, flex: 1, minHeight: 0, paddingTop: 2, paddingBottom: 4 },
  bigAvatarWrap: { position: "relative", width: 86, height: 86, flexShrink: 0,
    display: "flex", alignItems: "center", justifyContent: "center" },
  avatarGlow: { position: "absolute", inset: -8, borderRadius: "50%" },
  avatarBase: { position: "absolute", bottom: -4, left: 8, right: 8, height: 8, borderRadius: "50%" },
  agentName: { color: "#EAF6FF", fontSize: 22, fontWeight: 800, fontFamily: "Inter", lineHeight: 1 },
  agentRow: { display: "flex", alignItems: "center", gap: 6, marginTop: 6 },
  agentRole: { color: "#A8BDD6", fontSize: 12, fontFamily: "Inter" },
  statusDot: { width: 7, height: 7, borderRadius: "50%" },
  taskHeadRow: { display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap" },
  taskTitle: { color: "#EAF6FF", fontSize: 14, fontWeight: 700, fontFamily: "Inter",
    flex: 1, minWidth: 0, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" },
  taskSub: { color: "#7A95B4", fontSize: 11.5, marginTop: 3, fontFamily: "Inter",
    display: "flex", alignItems: "center", gap: 5, flexWrap: "wrap" },
  taskSubDot: { opacity: 0.35, margin: "0 1px" },
  taskPriorityChip: {
    fontFamily: "'JetBrains Mono', monospace", fontSize: 9, fontWeight: 800, letterSpacing: 0.8,
    padding: "2px 7px", borderRadius: 4, border: "1px solid", flexShrink: 0,
  },
  taskPrio_low:      { color: "#A8BDD6", borderColor: "rgba(168,189,214,0.4)", background: "rgba(168,189,214,0.08)" },
  taskPrio_medium:   { color: "#7EE8FF", borderColor: "rgba(126,232,255,0.4)", background: "rgba(126,232,255,0.08)" },
  taskPrio_high:     { color: "#FFD170", borderColor: "rgba(255,209,112,0.45)", background: "rgba(255,209,112,0.1)" },
  taskPrio_critical: { color: "#FF8A7A", borderColor: "rgba(255,138,122,0.5)", background: "rgba(255,138,122,0.12)" },
  progressWrap: { display: "flex", alignItems: "center", gap: 10, marginTop: 14 },
  progressBar: { flex: 1, height: 8, borderRadius: 4, background: "rgba(10, 20, 36, 0.9)",
    border: "1px solid rgba(79, 216, 255, 0.15)", overflow: "hidden" },
  progressFill: { width: "0%", height: "100%",
    background: "linear-gradient(90deg, #4FD8FF, #7EE8FF)",
    boxShadow: "0 0 10px rgba(79, 216, 255, 0.6)", borderRadius: 3,
    transition: "width 0.3s ease" },
  progressLabel: { color: "#7EE8FF", fontSize: 13, fontWeight: 700, fontFamily: "'JetBrains Mono', monospace" },
  collabRow: { display: "flex", alignItems: "center", flex: 1, paddingTop: 2 },
  collabWrap: { position: "relative", width: 40, height: 40 },
  collabGlow: { position: "absolute", inset: -4, borderRadius: "50%" },
  collabAvatar: { width: 40, height: 40, borderRadius: "50%",
    background: "rgba(10, 20, 36, 0.9)", border: "2px solid rgba(10, 20, 36, 0.9)",
    display: "flex", alignItems: "center", justifyContent: "center", position: "relative" },
  collabAdd: { marginLeft: 6, width: 30, height: 30, borderRadius: "50%",
    background: "rgba(79, 216, 255, 0.08)", border: "1px dashed rgba(79, 216, 255, 0.4)",
    display: "flex", alignItems: "center", justifyContent: "center", cursor: "pointer" },
  logs: { flex: 1, display: "flex", flexDirection: "column", gap: 3,
    padding: "8px 10px", background: "rgba(10, 20, 36, 0.55)",
    border: "1px solid rgba(79, 216, 255, 0.12)", borderRadius: 10,
    fontSize: 10.5, fontFamily: "'JetBrains Mono', monospace", overflow: "hidden", marginBottom: 6 },
  logLine: { display: "flex", gap: 6, alignItems: "baseline" },
  logTime: { color: "#5A7498", fontSize: 10 },
  logText: { color: "#A8BDD6", fontSize: 10.5 },
  actionsCol: { display: "flex", flexDirection: "column", gap: 5, flex: "0 0 auto" },
  actionsRow: { display: "flex", gap: 5 },
  primaryBtn: { display: "flex", alignItems: "center", justifyContent: "center", gap: 6,
    padding: "9px 14px", background: "linear-gradient(90deg, #4FD8FF, #7EE8FF)",
    border: "none", borderRadius: 8, color: "#0a1a2e", fontSize: 12, fontWeight: 800,
    fontFamily: "Inter", letterSpacing: 0.8,
    boxShadow: "0 0 16px rgba(79, 216, 255, 0.45)", cursor: "pointer" },
  secondaryBtn: { flex: 1, display: "flex", alignItems: "center", justifyContent: "center", gap: 5,
    padding: "8px 10px", background: "rgba(79, 216, 255, 0.08)",
    border: "1px solid rgba(79, 216, 255, 0.35)", borderRadius: 8,
    color: "#7EE8FF", fontSize: 11, fontWeight: 700, fontFamily: "Inter", letterSpacing: 0.6, cursor: "pointer" },
};

// ---------------------------------------------------------------
// ADD AGENT MODAL
// Small, focused form. Runs OUTSIDE the scale wrapper so it reads
// crisp on any viewport. On submit it calls MissionState.addAgent,
// which already keeps rooms.agentsAssigned / occupancy in sync.
// ---------------------------------------------------------------
function AddAgentModal({ onClose, onCreated }) {
  const [name, setName]       = useState("");
  const [role, setRole]       = useState("Strategy");
  const [room, setRoom]       = useState("office");
  const [status, setStatus]   = useState("Working");
  const [variant, setVariant] = useState("auto");
  const [task, setTask]       = useState("");
  const [error, setError]     = useState("");
  const nameRef = useRef(null);

  useEffect(() => {
    // Autofocus + ESC to close.
    if (nameRef.current) nameRef.current.focus();
    const onKey = (e) => { if (e.key === "Escape") onClose(); };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [onClose]);

  const submit = (e) => {
    e.preventDefault();
    const ms = window.MissionState;
    if (!ms) { setError("MissionState not available."); return; }

    const trimmed = name.trim();
    if (!trimmed)                   { setError("El nombre es obligatorio.");      return; }
    if (!ms.getRoomById(room))      { setError("Sala inválida.");                 return; }
    if (!STATUS_STYLES[status])     { setError("Estado inválido.");               return; }

    const existing = ms.getAgents() || [];
    const baseId   = slugifyId(trimmed) || ("agent-" + (existing.length + 1));
    const id       = uniqueAgentId(baseId, existing);
    const variantK = resolveAvatarVariant(variant, role, existing.length);
    const palette  = (window.ROBOT_PALETTES && window.ROBOT_PALETTES[variantK]) || null;
    const accent   = palette ? palette.glow : "#4FD8FF";
    const roomObj  = ms.getRoomById(room);
    const taskTrim = task.trim();

    const newAgent = {
      id,
      name:          trimmed,
      role,
      status,
      activity:      statusToActivity(status),
      room,
      task:          taskTrim || ("En espera en " + roomObj.title),
      progress:      taskTrim ? 5 : 0,
      accent,
      avatarVariant: variantK,
      isActive:      true,
      online:        true,
    };

    ms.addAgent(newAgent);
    if (taskTrim) {
      ms.assignTaskToAgent(id, { title: taskTrim, progress: 5, priority: "medium", status: "running" });
    }
    ms.addLog({
      type: "OPS", source: "ROSTER", severity: "low",
      message: "Agente " + trimmed.toUpperCase() + " creado — asignado a " + roomObj.title + ".",
    });

    onCreated(id);
  };

  return (
    <div style={modalStyles.backdrop} onMouseDown={onClose} role="dialog" aria-modal="true" aria-label="Agregar nuevo agente">
      <div style={modalStyles.card} onMouseDown={(e) => e.stopPropagation()}>
        <div style={modalStyles.head}>
          <div>
            <div style={modalStyles.eyebrow}>ROSTER · NUEVO AGENTE</div>
            <div style={modalStyles.title}>Agregar agente</div>
          </div>
          <button type="button" onClick={onClose} style={modalStyles.closeBtn} aria-label="Cerrar">×</button>
        </div>

        <form onSubmit={submit} style={modalStyles.form}>
          <label style={modalStyles.row}>
            <span style={modalStyles.label}>NOMBRE <em style={modalStyles.req}>*</em></span>
            <input
              ref={nameRef}
              type="text"
              value={name}
              onChange={(e) => { setName(e.target.value); setError(""); }}
              placeholder="ej. PIXEL"
              style={modalStyles.input}
              maxLength={32}
              autoComplete="off"
            />
          </label>

          <div style={modalStyles.grid2}>
            <label style={modalStyles.row}>
              <span style={modalStyles.label}>ROL</span>
              <select value={role} onChange={(e) => setRole(e.target.value)} style={modalStyles.input}>
                {ROLE_OPTIONS.map(r => <option key={r} value={r}>{roleLabel(r)}</option>)}
              </select>
            </label>
            <label style={modalStyles.row}>
              <span style={modalStyles.label}>ESTADO</span>
              <select value={status} onChange={(e) => setStatus(e.target.value)} style={modalStyles.input}>
                {STATUS_OPTIONS.map(s => <option key={s} value={s}>{statusLabel(s)}</option>)}
              </select>
            </label>
          </div>

          <div style={modalStyles.grid2}>
            <label style={modalStyles.row}>
              <span style={modalStyles.label}>SALA INICIAL</span>
              <select value={room} onChange={(e) => setRoom(e.target.value)} style={modalStyles.input}>
                {deriveRooms().map(r => <option key={r.id} value={r.id}>{r.name}</option>)}
              </select>
            </label>
            <label style={modalStyles.row}>
              <span style={modalStyles.label}>COLOR DEL AVATAR</span>
              <select value={variant} onChange={(e) => setVariant(e.target.value)} style={modalStyles.input}>
                {VARIANT_OPTIONS.map(v => (
                  <option key={v} value={v}>{v === "auto" ? "automático (derivar del rol)" : v}</option>
                ))}
              </select>
            </label>
          </div>

          <label style={modalStyles.row}>
            <span style={modalStyles.label}>TAREA INICIAL <em style={modalStyles.opt}>(opcional)</em></span>
            <input
              type="text"
              value={task}
              onChange={(e) => setTask(e.target.value)}
              placeholder="ej. Redactar brief estratégico Q2"
              style={modalStyles.input}
              maxLength={80}
              autoComplete="off"
            />
          </label>

          {/* Live preview */}
          <div style={modalStyles.preview}>
            <div style={modalStyles.previewAvatarWrap}>
              <div style={{
                ...modalStyles.previewGlow,
                background: `radial-gradient(circle, ${(window.ROBOT_PALETTES?.[resolveAvatarVariant(variant, role, 0)] || {}).glow || "#4FD8FF"}44, transparent 70%)`,
              }} />
              <RobotAvatar color={resolveAvatarVariant(variant, role, 0)} size={44} />
            </div>
            <div>
              <div style={modalStyles.previewName}>{name.trim() || "NUEVO AGENTE"}</div>
              <div style={modalStyles.previewMeta}>
                {roleLabel(role)} · {statusLabel(status)} · {(window.MissionState?.getRoomById(room) || { title: "—" }).title}
              </div>
            </div>
          </div>

          {error && <div style={modalStyles.error}>{error}</div>}

          <div style={modalStyles.foot}>
            <button type="button" onClick={onClose} style={modalStyles.btnGhost}>CANCELAR</button>
            <button type="submit" style={modalStyles.btnPrimary}>＋ CREAR AGENTE</button>
          </div>
        </form>
      </div>
    </div>
  );
}

const modalStyles = {
  backdrop: {
    position: "absolute", inset: 0, zIndex: 40,
    background: "rgba(2, 8, 16, 0.72)",
    backdropFilter: "blur(6px)",
    WebkitBackdropFilter: "blur(6px)",
    display: "flex", alignItems: "center", justifyContent: "center",
    padding: 20,
  },
  card: {
    width: 460, maxWidth: "100%",
    background: "linear-gradient(180deg, rgba(15, 28, 48, 0.96), rgba(8, 16, 30, 0.96))",
    border: "1px solid rgba(79, 216, 255, 0.35)",
    borderRadius: 14,
    boxShadow: "0 20px 60px rgba(0,0,0,0.6), 0 0 28px rgba(79, 216, 255, 0.15), inset 0 0 40px rgba(79, 216, 255, 0.03)",
    padding: 20,
    color: "#EAF6FF",
    fontFamily: "Inter, sans-serif",
    position: "relative",
  },
  head: {
    display: "flex", alignItems: "flex-start", justifyContent: "space-between",
    paddingBottom: 14,
    borderBottom: "1px solid rgba(79, 216, 255, 0.12)",
    marginBottom: 14,
  },
  eyebrow: {
    fontFamily: "'JetBrains Mono', monospace",
    fontSize: 10, letterSpacing: 2, color: "#7EE8FF",
    fontWeight: 700, textTransform: "uppercase",
  },
  title: { fontSize: 18, fontWeight: 800, marginTop: 3, color: "#fff", letterSpacing: 0.3 },
  closeBtn: {
    width: 28, height: 28, borderRadius: 6,
    background: "rgba(255,255,255,0.04)",
    border: "1px solid rgba(255,255,255,0.08)",
    color: "#A8BDD6", fontSize: 18, lineHeight: 1,
    cursor: "pointer",
  },
  form: { display: "flex", flexDirection: "column", gap: 12 },
  row: { display: "flex", flexDirection: "column", gap: 4, minWidth: 0 },
  label: {
    fontFamily: "'JetBrains Mono', monospace",
    fontSize: 9.5, letterSpacing: 1.4, color: "rgba(255,255,255,0.45)",
    fontWeight: 700, textTransform: "uppercase",
  },
  req: { color: "#FF8A7A", fontStyle: "normal", marginLeft: 3 },
  opt: { color: "rgba(255,255,255,0.35)", fontStyle: "normal", marginLeft: 4, textTransform: "none", letterSpacing: 0 },
  input: {
    width: "100%",
    padding: "8px 10px",
    background: "rgba(10, 20, 36, 0.7)",
    border: "1px solid rgba(79, 216, 255, 0.18)",
    borderRadius: 7,
    color: "#EAF6FF",
    fontFamily: "Inter, sans-serif",
    fontSize: 12.5,
    outline: "none",
    transition: "border-color 0.15s, box-shadow 0.15s",
  },
  grid2: { display: "grid", gridTemplateColumns: "1fr 1fr", gap: 10 },
  preview: {
    display: "flex", alignItems: "center", gap: 12,
    padding: "10px 12px", marginTop: 2,
    background: "rgba(79, 216, 255, 0.04)",
    border: "1px dashed rgba(79, 216, 255, 0.22)",
    borderRadius: 10,
  },
  previewAvatarWrap: {
    position: "relative", width: 44, height: 44, flexShrink: 0,
    display: "flex", alignItems: "center", justifyContent: "center",
  },
  previewGlow: { position: "absolute", inset: -4, borderRadius: "50%" },
  previewName: { fontSize: 13, fontWeight: 700, color: "#EAF6FF", letterSpacing: 0.2 },
  previewMeta: { fontSize: 10.5, color: "#7A95B4", marginTop: 2 },
  error: {
    color: "#FF8A7A",
    background: "rgba(255, 138, 122, 0.08)",
    border: "1px solid rgba(255, 138, 122, 0.3)",
    padding: "7px 10px", borderRadius: 7,
    fontSize: 11.5, fontFamily: "'JetBrains Mono', monospace",
  },
  foot: {
    display: "flex", gap: 8, justifyContent: "flex-end",
    paddingTop: 6, marginTop: 2,
    borderTop: "1px solid rgba(79, 216, 255, 0.08)",
  },
  btnGhost: {
    padding: "8px 14px",
    background: "transparent",
    border: "1px solid rgba(255,255,255,0.12)",
    borderRadius: 7, cursor: "pointer",
    color: "rgba(255,255,255,0.6)",
    fontFamily: "'JetBrains Mono', monospace", fontSize: 11, fontWeight: 700, letterSpacing: 1,
  },
  btnPrimary: {
    padding: "8px 16px",
    background: "linear-gradient(90deg, #4FD8FF, #7EE8FF)",
    border: "none", borderRadius: 7, cursor: "pointer",
    color: "#0a1a2e",
    fontFamily: "Inter, sans-serif", fontSize: 11.5, fontWeight: 800, letterSpacing: 0.8,
    boxShadow: "0 0 16px rgba(79, 216, 255, 0.4)",
  },
};

// ---------------------------------------------------------------
// COLLABORATORS MODAL
// Collaboration is scoped to the selected agent's active TASK.
// No active task → explicit empty state (dispatch first, then collaborate).
// ---------------------------------------------------------------
function CollaboratorsModal({ agent, onClose }) {
  const ms = window.MissionState;
  useEffect(() => {
    const onKey = (e) => { if (e.key === "Escape") onClose(); };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [onClose]);

  if (!ms || !agent) return null;

  const allAgents = ms.getAgents() || [];
  const task      = ms.getActiveTaskForAgent(agent.id);
  const collabIds = task && Array.isArray(task.collaborators) ? task.collaborators : [];
  const candidates = allAgents.filter(a => a.id !== agent.id); // cannot collab with self

  const activeCollabs = collabIds
    .map(id => candidates.find(a => a.id === id))
    .filter(Boolean);
  const available = candidates.filter(a => collabIds.indexOf(a.id) === -1);

  const logCollab = (msg, severity = "low") => {
    ms.addLog({ type: "OPS", source: "COLLAB", severity, message: msg });
  };

  const onAdd = (collab) => {
    if (!task) return;
    const res = ms.addCollaborator(task.id, collab.id);
    if (res) {
      logCollab(
        collab.name.toUpperCase() + ' se unió a la operación de ' + agent.name.toUpperCase() +
        ' ("' + (task.title || "sin título") + '").'
      );
    }
  };

  const onRemove = (collab) => {
    if (!task) return;
    const res = ms.removeCollaborator(task.id, collab.id);
    if (res) {
      logCollab(
        collab.name.toUpperCase() + ' removido como colaborador de la operación de ' +
        agent.name.toUpperCase() + '.'
      );
    }
  };

  const roomTitle = (window.MissionState?.getRoomById(agent.room) || { title: "—" }).title;

  return (
    <div style={modalStyles.backdrop} onMouseDown={onClose} role="dialog" aria-modal="true" aria-label={`Colaboradores de ${agent.name}`}>
      <div style={{ ...modalStyles.card, width: 520 }} onMouseDown={(e) => e.stopPropagation()}>
        <div style={modalStyles.head}>
          <div>
            <div style={modalStyles.eyebrow}>OPS · COLABORADORES</div>
            <div style={modalStyles.title}>Operación de {agent.name}</div>
            <div style={collabStyles.subhead}>
              {task
                ? <>Tarea <b>&ldquo;{task.title || "sin título"}&rdquo;</b> en {roomTitle} · {activeCollabs.length} {activeCollabs.length === 1 ? "colaborador" : "colaboradores"}</>
                : <>No hay tarea activa para {agent.name}.</>}
            </div>
          </div>
          <button type="button" onClick={onClose} style={modalStyles.closeBtn} aria-label="Cerrar">×</button>
        </div>

        {!task && (
          <div className="ca-empty">
            <div className="ca-empty-title">SIN TAREA ACTIVA</div>
            <div className="ca-empty-sub">
              Los colaboradores se vinculan a la tarea actual del agente.
              Despliega a {agent.name} primero (🚀 DESPLEGAR desde el panel inferior)
              y luego agrega colaboradores aquí.
            </div>
          </div>
        )}

        {task && (
          <>
            {/* Active collaborators */}
            <div style={collabStyles.section}>
              <div style={collabStyles.sectionTitle}>COLABORADORES ACTIVOS</div>
              {activeCollabs.length === 0 && (
                <div style={collabStyles.sectionHint}>Aún no hay colaboradores — agrega uno abajo.</div>
              )}
              {activeCollabs.length > 0 && (
                <div style={collabStyles.rows}>
                  {activeCollabs.map(c => (
                    <CollabRow key={c.id} agent={c} isActive={true} onClick={() => onRemove(c)} />
                  ))}
                </div>
              )}
            </div>

            {/* Divider */}
            <div style={collabStyles.divider} />

            {/* Available pool */}
            <div style={collabStyles.section}>
              <div style={collabStyles.sectionTitle}>
                AGENTES DISPONIBLES
                <span style={collabStyles.sectionCount}>{available.length}</span>
              </div>
              {available.length === 0 && candidates.length === 0 && (
                <div style={collabStyles.sectionHint}>
                  Solo {agent.name} existe en el roster — agrega más agentes con <i>＋ Agregar agente</i>.
                </div>
              )}
              {available.length === 0 && candidates.length > 0 && (
                <div style={collabStyles.sectionHint}>
                  Todos los agentes disponibles ya están colaborando en esta tarea.
                </div>
              )}
              {available.length > 0 && (
                <div style={collabStyles.rows}>
                  {available.map(c => (
                    <CollabRow key={c.id} agent={c} isActive={false} onClick={() => onAdd(c)} />
                  ))}
                </div>
              )}
            </div>
          </>
        )}

        <div style={{ ...modalStyles.foot, borderTop: "1px solid rgba(79, 216, 255, 0.08)" }}>
          <div style={collabStyles.footHint}>
            Los cambios se aplican al instante y persisten con la tarea.
          </div>
          <button type="button" onClick={onClose} style={modalStyles.btnGhost}>LISTO</button>
        </div>
      </div>
    </div>
  );
}

// One row per agent inside the CollaboratorsModal.
function CollabRow({ agent, isActive, onClick }) {
  const ms = window.MissionState;
  const roomTitle = (ms && ms.getRoomById(agent.room) || { title: "—" }).title;
  return (
    <div className={"ca-row" + (isActive ? " ca-active" : "")}>
      <RobotAvatar color={agent.avatarVariant || agent.color || "cyan"} size={36} />
      <div className="ca-row-body">
        <div className="ca-row-name">{agent.name}</div>
        <div className="ca-row-meta" data-role={roleLabel(agent.role)} data-status={statusLabel(agent.status)}>
          <span>{roleLabel(agent.role)}</span>
          <span style={{ opacity: 0.35 }}>·</span>
          <span>{roomTitle}</span>
          <span style={{ opacity: 0.35 }}>·</span>
          <span>{statusLabel(agent.status)}</span>
        </div>
      </div>
      <button
        type="button"
        className={"ca-toggle " + (isActive ? "ca-toggle-remove" : "ca-toggle-add")}
        onClick={onClick}
        aria-label={(isActive ? "Remover a " : "Agregar a ") + agent.name}
      >
        {isActive ? "× REMOVER" : "+ AGREGAR"}
      </button>
    </div>
  );
}

const collabStyles = {
  subhead: {
    fontFamily: "Inter, sans-serif",
    fontSize: 11.5, color: "rgba(234, 246, 255, 0.55)",
    marginTop: 4,
  },
  section: { display: "flex", flexDirection: "column", gap: 8 },
  sectionTitle: {
    fontFamily: "'JetBrains Mono', monospace",
    fontSize: 10, letterSpacing: 1.6, fontWeight: 700,
    color: "#7EE8FF", textTransform: "uppercase",
    display: "flex", alignItems: "center", justifyContent: "space-between",
  },
  sectionCount: {
    fontFamily: "'JetBrains Mono', monospace",
    fontSize: 10, fontWeight: 700,
    padding: "2px 8px",
    color: "#7EE8FF",
    background: "rgba(79, 216, 255, 0.08)",
    border: "1px solid rgba(79, 216, 255, 0.25)",
    borderRadius: 4,
  },
  sectionHint: {
    fontFamily: "'JetBrains Mono', monospace",
    fontSize: 10.5, color: "rgba(255, 255, 255, 0.32)",
    padding: "6px 2px",
    letterSpacing: 0.3,
  },
  rows: {
    display: "flex", flexDirection: "column", gap: 6,
    maxHeight: 260, overflowY: "auto",
    paddingRight: 4,
  },
  divider: {
    height: 1,
    background: "rgba(79, 216, 255, 0.08)",
    margin: "14px 0",
  },
  footHint: {
    flex: 1,
    fontFamily: "'JetBrains Mono', monospace",
    fontSize: 10, color: "rgba(255, 255, 255, 0.35)",
    letterSpacing: 0.5,
  },
};

// ---------------------------------------------------------------
// MESSAGE AGENT MODAL
// Sends a real instruction / note to the selected agent. Persists
// through MissionState.messages (survives reload). Violet accent to
// distinguish from the cyan ops flows.
// ---------------------------------------------------------------
const MESSAGE_CATEGORIES = [
  { key: "instruction", label: "Instrucción" },
  { key: "note",        label: "Nota" },
  { key: "directive",   label: "Directiva" },
  { key: "alert",       label: "Alerta" },
];

function MessageAgentModal({ agent, onClose, onSent }) {
  const [subject, setSubject]   = useState("");
  const [body, setBody]         = useState("");
  const [priority, setPriority] = useState("medium");
  const [category, setCategory] = useState("instruction");
  const [error, setError]       = useState("");
  const bodyRef = useRef(null);

  useEffect(() => {
    if (bodyRef.current) bodyRef.current.focus();
    const onKey = (e) => { if (e.key === "Escape") onClose(); };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [onClose]);

  // Ctrl/Cmd + Enter submits quickly.
  const onBodyKeyDown = (e) => {
    if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
      e.preventDefault();
      submit(e);
    }
  };

  const submit = (e) => {
    if (e && e.preventDefault) e.preventDefault();
    const ms = window.MissionState;
    if (!ms) { setError("MissionState no disponible."); return; }

    const b = body.trim();
    if (!b) { setError("El cuerpo del mensaje no puede estar vacío."); return; }
    if (!agent || !agent.id) { setError("Ningún agente seleccionado."); return; }
    if (PRIORITY_OPTIONS.indexOf(priority) === -1) { setError("Prioridad inválida."); return; }

    const msg = ms.addMessage({
      agentId:  agent.id,
      subject:  subject.trim() || null,
      body:     b,
      priority: priority,
      category: category,
    });
    if (!msg) { setError("No se pudo enviar el mensaje."); return; }

    // Log — severity escalates with priority so ALERT filter in Tactical picks up criticals.
    const sevMap    = { low: "low", medium: "low", high: "medium", critical: "high" };
    const name      = agent.name.toUpperCase();
    const catLabel  = (MESSAGE_CATEGORIES.find(c => c.key === category) || { label: "Mensaje" }).label;
    const subjPart  = subject.trim() ? ' — "' + subject.trim().slice(0, 60) + '"' : "";
    ms.addLog({
      type:     "OPS",
      source:   "MSG",
      severity: sevMap[priority] || "low",
      message:  catLabel + " enviado a " + name + subjPart + " [" + priorityLabel(priority).toUpperCase() + "].",
    });

    onSent(agent.id, msg);
  };

  const priorityColors = {
    low:      "#A8BDD6",
    medium:   "#7EE8FF",
    high:     "#FFD170",
    critical: "#FF8A7A",
  };

  return (
    <div style={msgStyles.backdrop} onMouseDown={onClose} role="dialog" aria-modal="true" aria-label={`Enviar mensaje a ${agent.name}`}>
      <div style={msgStyles.card} onMouseDown={(e) => e.stopPropagation()}>
        <div style={modalStyles.head}>
          <div>
            <div style={msgStyles.eyebrow}>COMMS · ENVIAR MENSAJE</div>
            <div style={modalStyles.title}>Mensaje para {agent.name}</div>
          </div>
          <button type="button" onClick={onClose} style={modalStyles.closeBtn} aria-label="Cerrar">×</button>
        </div>

        {/* Recipient banner */}
        <div style={msgStyles.context}>
          <div style={{ position: "relative", width: 40, height: 40, flexShrink: 0,
                        display: "flex", alignItems: "center", justifyContent: "center" }}>
            <div style={{
              position: "absolute", inset: -4, borderRadius: "50%",
              background: `radial-gradient(circle, ${(window.ROBOT_PALETTES?.[agent.color] || {}).glow || "#C17AFF"}44, transparent 70%)`,
            }} />
            <RobotAvatar color={agent.color} size={40} />
          </div>
          <div style={{ minWidth: 0 }}>
            <div style={msgStyles.contextName}>{agent.name}</div>
            <div style={msgStyles.contextMeta}>
              {roleLabel(agent.role)} · {(window.MissionState?.getRoomById(agent.room) || { title: "—" }).title} · {statusLabel(agent.status)}
            </div>
          </div>
        </div>

        <form onSubmit={submit} style={modalStyles.form}>
          <label style={modalStyles.row}>
            <span style={modalStyles.label}>ASUNTO <em style={modalStyles.opt}>(opcional)</em></span>
            <input
              type="text"
              value={subject}
              onChange={(e) => setSubject(e.target.value)}
              placeholder="ej. Revisar pipeline Q2"
              style={modalStyles.input}
              maxLength={60}
              autoComplete="off"
            />
          </label>

          <label style={modalStyles.row}>
            <span style={modalStyles.label}>
              CUERPO DEL MENSAJE <em style={modalStyles.req}>*</em>
              <em style={msgStyles.hint}>  (Ctrl/⌘ + Enter para enviar)</em>
            </span>
            <textarea
              ref={bodyRef}
              value={body}
              onChange={(e) => { setBody(e.target.value); setError(""); }}
              onKeyDown={onBodyKeyDown}
              placeholder="Escribe tu instrucción, nota o directiva…"
              style={{ ...modalStyles.input, ...msgStyles.textarea }}
              maxLength={500}
            />
            <div style={msgStyles.charCount}>{body.length}/500</div>
          </label>

          <div style={modalStyles.grid2}>
            <label style={modalStyles.row}>
              <span style={modalStyles.label}>CATEGORÍA</span>
              <select value={category} onChange={(e) => setCategory(e.target.value)} style={modalStyles.input}>
                {MESSAGE_CATEGORIES.map(c => <option key={c.key} value={c.key}>{c.label}</option>)}
              </select>
            </label>
            <label style={modalStyles.row}>
              <span style={modalStyles.label}>PRIORIDAD</span>
              <select value={priority} onChange={(e) => setPriority(e.target.value)} style={modalStyles.input}>
                {PRIORITY_OPTIONS.map(p => <option key={p} value={p}>{priorityLabel(p).toUpperCase()}</option>)}
              </select>
            </label>
          </div>

          {/* Summary preview */}
          <div style={msgStyles.summary}>
            <span style={msgStyles.summaryLabel}>SALIENTE</span>
            <span style={msgStyles.summaryText}>
              {(MESSAGE_CATEGORIES.find(c => c.key === category) || { label: "Mensaje" }).label} para <b>{agent.name}</b>
              {subject.trim() ? <> — <b>{subject.trim()}</b></> : null}
            </span>
            <span style={{
              ...msgStyles.priorityChip,
              color: priorityColors[priority],
              borderColor: priorityColors[priority] + "88",
              background: priorityColors[priority] + "18",
            }}>
              {priorityLabel(priority).toUpperCase()}
            </span>
          </div>

          {error && <div style={modalStyles.error}>{error}</div>}

          <div style={modalStyles.foot}>
            <button type="button" onClick={onClose} style={modalStyles.btnGhost}>CANCELAR</button>
            <button type="submit" style={msgStyles.btnSend}>📡 ENVIAR MENSAJE</button>
          </div>
        </form>
      </div>
    </div>
  );
}

const msgStyles = {
  backdrop: {
    position: "absolute", inset: 0, zIndex: 40,
    background: "rgba(2, 8, 16, 0.72)",
    backdropFilter: "blur(6px)",
    WebkitBackdropFilter: "blur(6px)",
    display: "flex", alignItems: "center", justifyContent: "center",
    padding: 20,
  },
  card: {
    width: 480, maxWidth: "100%",
    background: "linear-gradient(180deg, rgba(22, 14, 38, 0.96), rgba(12, 8, 24, 0.96))",
    border: "1px solid rgba(193, 122, 255, 0.38)",
    borderRadius: 14,
    boxShadow: "0 20px 60px rgba(0,0,0,0.6), 0 0 28px rgba(193, 122, 255, 0.18), inset 0 0 40px rgba(193, 122, 255, 0.03)",
    padding: 20,
    color: "#EAF6FF",
    fontFamily: "Inter, sans-serif",
  },
  eyebrow: {
    fontFamily: "'JetBrains Mono', monospace",
    fontSize: 10, letterSpacing: 2, color: "#D1A5FF",
    fontWeight: 700, textTransform: "uppercase",
  },
  context: {
    display: "flex", alignItems: "center", gap: 12,
    padding: "10px 12px", marginBottom: 12,
    background: "rgba(193, 122, 255, 0.06)",
    border: "1px solid rgba(193, 122, 255, 0.18)",
    borderRadius: 10,
  },
  contextName: { fontSize: 13.5, fontWeight: 700, color: "#EAF6FF", letterSpacing: 0.2 },
  contextMeta: { fontSize: 10.5, color: "rgba(209, 165, 255, 0.7)", marginTop: 2 },
  textarea: {
    minHeight: 90,
    resize: "vertical",
    fontFamily: "Inter, sans-serif",
    fontSize: 12.5, lineHeight: 1.45,
    padding: "10px 12px",
  },
  charCount: {
    alignSelf: "flex-end",
    marginTop: 2,
    fontFamily: "'JetBrains Mono', monospace",
    fontSize: 9.5, color: "rgba(255,255,255,0.3)",
    letterSpacing: 0.06,
  },
  hint: {
    color: "rgba(255,255,255,0.3)",
    fontStyle: "normal",
    textTransform: "none",
    letterSpacing: 0,
    fontWeight: 500,
    marginLeft: 4,
  },
  summary: {
    display: "flex", alignItems: "center", gap: 8,
    padding: "9px 12px",
    background: "rgba(12, 6, 24, 0.6)",
    border: "1px dashed rgba(193, 122, 255, 0.28)",
    borderRadius: 9,
    fontSize: 11.5, color: "rgba(234, 246, 255, 0.82)",
  },
  summaryLabel: {
    fontFamily: "'JetBrains Mono', monospace",
    fontSize: 9.5, letterSpacing: 1.6, color: "#D1A5FF",
    fontWeight: 700, flexShrink: 0,
  },
  summaryText: { flex: 1, minWidth: 0, lineHeight: 1.4 },
  priorityChip: {
    fontFamily: "'JetBrains Mono', monospace",
    fontSize: 9.5, fontWeight: 800, letterSpacing: 0.8,
    padding: "3px 7px", borderRadius: 4,
    border: "1px solid", flexShrink: 0,
  },
  btnSend: {
    padding: "8px 16px",
    background: "linear-gradient(90deg, #9C5FE0, #C17AFF)",
    border: "none", borderRadius: 7, cursor: "pointer",
    color: "#140820",
    fontFamily: "Inter, sans-serif", fontSize: 11.5, fontWeight: 800, letterSpacing: 0.8,
    boxShadow: "0 0 16px rgba(193, 122, 255, 0.45)",
  },
};

// ---------------------------------------------------------------
// TASK CONTROL MODAL  —  "VIEW TASKS"
// Reads live state via MissionState.getTasks(). Quick-progress
// buttons + chip actions mutate through setTaskProgress / setTaskStatus.
// Re-reads on every render because App subscribes to notify().
// ---------------------------------------------------------------
const TASK_TABS = [
  { key: "selected",  label: "Seleccionado" },
  { key: "all",       label: "Todas" },
  { key: "active",    label: "Activas" },
  { key: "completed", label: "Completadas" },
  { key: "cancelled", label: "Canceladas" },
];

function TaskControlModal({ selectedAgentId, onClose }) {
  const ms = window.MissionState;
  const [tab, setTab] = useState("selected");

  useEffect(() => {
    const onKey = (e) => { if (e.key === "Escape") onClose(); };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [onClose]);

  if (!ms) return null;

  const allTasks     = ms.getTasks() || [];
  const agents       = ms.getAgents() || [];
  const rooms        = ms.getRooms()  || [];
  const selectedAgent = agents.find(a => a.id === selectedAgentId) || null;

  const byId = (id, list) => list.find(x => x.id === id) || null;

  const matchers = {
    selected:  (t) => t.assignedAgentId === selectedAgentId,
    all:       () => true,
    active:    (t) => t.status === "running",
    completed: (t) => t.status === "completed",
    cancelled: (t) => t.status === "cancelled",
  };
  const counts = {
    selected:  allTasks.filter(matchers.selected).length,
    all:       allTasks.length,
    active:    allTasks.filter(matchers.active).length,
    completed: allTasks.filter(matchers.completed).length,
    cancelled: allTasks.filter(matchers.cancelled).length,
  };
  // Sort: running > paused > completed > cancelled, then by priority weight.
  const statusWeight   = { running: 0, paused: 1, completed: 2, cancelled: 3 };
  const priorityWeight = { critical: 0, high: 1, medium: 2, low: 3 };
  const visibleTasks = allTasks
    .filter(matchers[tab] || matchers.all)
    .slice()
    .sort((a, b) => {
      const sw = (statusWeight[a.status] ?? 9) - (statusWeight[b.status] ?? 9);
      if (sw !== 0) return sw;
      return (priorityWeight[a.priority] ?? 9) - (priorityWeight[b.priority] ?? 9);
    });

  // -------- Actions --------
  const logTask = (task, message, source = "TASKS", severity = "low") => {
    ms.addLog({ type: "OPS", source, severity, message });
  };
  const onProgress = (task, pct) => {
    ms.setTaskProgress(task.id, pct);
    logTask(task, 'Tarea "' + task.title + '" progreso actualizado a ' + pct + "%.");
    if (pct === 100 && task.status !== "completed") {
      // Auto-log a hint — but do NOT auto-complete. Operator decides explicitly.
      logTask(task, 'Tarea "' + task.title + '" alcanzó el 100%.');
    }
  };
  const onStatus = (task, newStatus) => {
    ms.setTaskStatus(task.id, newStatus);
    const verb = {
      running:   "reanudada",
      paused:    "pausada",
      completed: "marcada como completada",
      cancelled: "cancelada",
    }[newStatus] || "actualizada";
    logTask(task, 'Tarea "' + task.title + '" ' + verb + ".", "TASKS", newStatus === "cancelled" ? "medium" : "low");
  };

  return (
    <div style={modalStyles.backdrop} onMouseDown={onClose} role="dialog" aria-modal="true" aria-label="Control de tareas">
      <div style={{ ...modalStyles.card, width: 720 }} onMouseDown={(e) => e.stopPropagation()}>
        {/* Head */}
        <div style={modalStyles.head}>
          <div>
            <div style={modalStyles.eyebrow}>OPS · CONTROL DE TAREAS</div>
            <div style={modalStyles.title}>Cola de tareas</div>
            {selectedAgent && (
              <div style={tcStyles.subheadContext}>
                Enfocado en <b>{selectedAgent.name}</b> · {roleLabel(selectedAgent.role)}
              </div>
            )}
          </div>
          <button type="button" onClick={onClose} style={modalStyles.closeBtn} aria-label="Cerrar">×</button>
        </div>

        {/* Tabs */}
        <div style={tcStyles.tabs}>
          {TASK_TABS.map(t => (
            <button
              key={t.key}
              type="button"
              className={"tc-tab" + (tab === t.key ? " active" : "")}
              onClick={() => setTab(t.key)}
            >
              {t.label}
              <span className="tc-tab-count">{counts[t.key]}</span>
            </button>
          ))}
        </div>

        {/* Task list */}
        <div style={tcStyles.list}>
          {visibleTasks.length === 0 && (
            <div style={tcStyles.empty}>
              <div style={tcStyles.emptyDot} />
              <div>Ninguna tarea coincide con esta vista.</div>
              <div style={tcStyles.emptySub}>Despliega un agente para crear una.</div>
            </div>
          )}

          {visibleTasks.map(task => {
            const agent  = byId(task.assignedAgentId, agents);
            const room   = byId(task.room || (agent && agent.room), rooms);
            const status = task.status || "running";
            const priority = task.priority || "medium";
            const progress = Math.max(0, Math.min(100, Number(task.progress) || 0));
            const canResume   = status === "paused";
            const canPause    = status === "running";
            const canComplete = status !== "completed" && status !== "cancelled";
            const canCancel   = status !== "completed" && status !== "cancelled";

            return (
              <div key={task.id} className="tc-row" data-status={status}>
                {/* Row header: avatar + name + priority chip + status chip */}
                <div style={tcStyles.rowHead}>
                  <div style={tcStyles.rowIdent}>
                    {agent && <RobotAvatar color={agent.avatarVariant || agent.color || "cyan"} size={28} />}
                    <div style={{ minWidth: 0 }}>
                      <div style={tcStyles.rowTitle}>{task.title || "(sin título)"}</div>
                      <div style={tcStyles.rowMeta}>
                        <span>{agent ? agent.name : "—"}</span>
                        <span style={tcStyles.rowDot}>·</span>
                        <span>{room ? room.title : "—"}</span>
                        <span style={tcStyles.rowDot}>·</span>
                        <span className={"tc-prio-" + priority} style={{ fontWeight: 700 }}>
                          ● {priorityLabel(priority).toUpperCase()}
                        </span>
                        <span style={tcStyles.rowDot}>·</span>
                        <span style={{ ...tcStyles.statusBadge, ...tcStyles["status_" + status] }}>
                          {taskStatusLabel(status).toUpperCase()}
                        </span>
                      </div>
                    </div>
                  </div>
                </div>

                {/* Progress bar */}
                <div style={tcStyles.progressWrap}>
                  <div style={tcStyles.progressBar}>
                    <div style={{
                      ...tcStyles.progressFill,
                      width: progress + "%",
                      background: status === "completed"
                        ? "linear-gradient(90deg, #4FE08A, #8EF3B0)"
                        : status === "cancelled"
                          ? "linear-gradient(90deg, #7A8FA8, #A8BDD6)"
                          : status === "paused"
                            ? "linear-gradient(90deg, #FFD170, #FFE4A8)"
                            : "linear-gradient(90deg, #4FD8FF, #7EE8FF)",
                    }} />
                  </div>
                  <div style={tcStyles.progressLabel}>{progress}%</div>
                </div>

                {/* Actions */}
                <div style={tcStyles.rowActions}>
                  <div style={tcStyles.qpGroup}>
                    <span style={tcStyles.qpLabel}>AJUSTE RÁPIDO</span>
                    {[0, 25, 50, 75, 100].map(p => (
                      <button
                        key={p}
                        type="button"
                        className={"tc-qp-btn" + (progress === p ? " active" : "")}
                        onClick={() => onProgress(task, p)}
                        disabled={status === "completed" || status === "cancelled"}
                        style={(status === "completed" || status === "cancelled") ? { opacity: 0.4, cursor: "not-allowed" } : {}}
                      >
                        {p}%
                      </button>
                    ))}
                  </div>
                  <div style={tcStyles.chipGroup}>
                    {canResume && (
                      <button type="button" className="tc-chip tc-chip-primary" onClick={() => onStatus(task, "running")}>▶ REANUDAR</button>
                    )}
                    {canPause && (
                      <button type="button" className="tc-chip tc-chip-pause" onClick={() => onStatus(task, "paused")}>⏸ PAUSAR</button>
                    )}
                    {canComplete && (
                      <button type="button" className="tc-chip tc-chip-ok" onClick={() => onStatus(task, "completed")}>✓ COMPLETAR</button>
                    )}
                    {canCancel && (
                      <button type="button" className="tc-chip tc-chip-danger" onClick={() => onStatus(task, "cancelled")}>× CANCELAR</button>
                    )}
                  </div>
                </div>
              </div>
            );
          })}
        </div>

        {/* Footer */}
        <div style={{ ...modalStyles.foot, borderTop: "1px solid rgba(79, 216, 255, 0.08)" }}>
          <div style={tcStyles.footHint}>
            {allTasks.length} {allTasks.length === 1 ? "tarea" : "tareas"} en el sistema · en vivo desde MissionState
          </div>
          <button type="button" onClick={onClose} style={modalStyles.btnGhost}>CERRAR</button>
        </div>
      </div>
    </div>
  );
}

const tcStyles = {
  subheadContext: {
    fontFamily: "Inter, sans-serif",
    fontSize: 11.5, color: "rgba(234, 246, 255, 0.55)",
    marginTop: 4,
  },
  tabs: {
    display: "flex", gap: 6, flexWrap: "wrap",
    paddingBottom: 12, marginBottom: 12,
    borderBottom: "1px solid rgba(79, 216, 255, 0.08)",
  },
  list: {
    display: "flex", flexDirection: "column", gap: 8,
    maxHeight: 420, overflowY: "auto",
    paddingRight: 4,
  },
  rowHead: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: 10 },
  rowIdent: { display: "flex", alignItems: "center", gap: 10, minWidth: 0, flex: 1 },
  rowTitle: {
    fontFamily: "Inter, sans-serif",
    fontSize: 12.5, fontWeight: 700,
    color: "#EAF6FF",
    whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis",
    letterSpacing: 0.2,
  },
  rowMeta: {
    display: "flex", alignItems: "center", gap: 5,
    fontFamily: "'JetBrains Mono', monospace",
    fontSize: 10, color: "rgba(168, 189, 214, 0.8)",
    marginTop: 3,
    flexWrap: "wrap",
  },
  rowDot: { opacity: 0.35 },
  statusBadge: {
    fontSize: 9, padding: "1px 6px", borderRadius: 3,
    border: "1px solid", letterSpacing: 0.08,
  },
  status_running:   { color: "#7EE8FF", borderColor: "rgba(79, 216, 255, 0.4)",  background: "rgba(79, 216, 255, 0.1)" },
  status_paused:    { color: "#FFD170", borderColor: "rgba(255, 209, 112, 0.45)", background: "rgba(255, 209, 112, 0.1)" },
  status_completed: { color: "#4FE08A", borderColor: "rgba(79, 224, 138, 0.45)",  background: "rgba(79, 224, 138, 0.1)" },
  status_cancelled: { color: "#A8BDD6", borderColor: "rgba(168, 189, 214, 0.3)",  background: "rgba(168, 189, 214, 0.05)" },

  progressWrap: { display: "flex", alignItems: "center", gap: 10, marginTop: 10 },
  progressBar: {
    flex: 1, height: 6, borderRadius: 3,
    background: "rgba(0, 0, 0, 0.55)",
    border: "1px solid rgba(79, 216, 255, 0.1)",
    overflow: "hidden",
  },
  progressFill: {
    height: "100%", borderRadius: 3,
    boxShadow: "0 0 8px rgba(79, 216, 255, 0.35)",
    transition: "width 0.3s ease, background 0.3s ease",
  },
  progressLabel: {
    fontFamily: "'JetBrains Mono', monospace",
    fontSize: 10.5, fontWeight: 700, color: "#7EE8FF",
    minWidth: 36, textAlign: "right",
  },

  rowActions: {
    display: "flex", alignItems: "center", justifyContent: "space-between",
    gap: 12, marginTop: 10, flexWrap: "wrap",
  },
  qpGroup: { display: "flex", alignItems: "center", gap: 5, flexWrap: "wrap" },
  qpLabel: {
    fontFamily: "'JetBrains Mono', monospace",
    fontSize: 9, letterSpacing: 1.3, fontWeight: 700,
    color: "rgba(255, 255, 255, 0.38)",
    marginRight: 4,
  },
  chipGroup: { display: "flex", gap: 5, flexWrap: "wrap" },

  empty: {
    display: "flex", flexDirection: "column", alignItems: "center", gap: 4,
    padding: 28,
    fontFamily: "'JetBrains Mono', monospace",
    fontSize: 11, color: "rgba(255, 255, 255, 0.45)",
    letterSpacing: 0.6,
    border: "1px dashed rgba(79, 216, 255, 0.15)",
    borderRadius: 10,
    background: "rgba(79, 216, 255, 0.02)",
  },
  emptyDot: {
    width: 6, height: 6, borderRadius: "50%",
    background: "#4FD8FF",
    boxShadow: "0 0 6px #4FD8FF",
    animation: "pulseGlow 1.8s ease-in-out infinite",
    marginBottom: 6,
  },
  emptySub: { opacity: 0.6, fontSize: 10, marginTop: 2 },

  footHint: {
    flex: 1,
    fontFamily: "'JetBrains Mono', monospace",
    fontSize: 10, color: "rgba(255, 255, 255, 0.35)",
    letterSpacing: 0.5,
  },
};

// ---------------------------------------------------------------
// DISPATCH AGENT MODAL
// Primary ops action: send {agent} to {room} with {task, priority, status}.
// Works for any agent (including JARVIS). Core-agent identity stays
// protected elsewhere; here the operator only chooses operational fields.
// ---------------------------------------------------------------
const PRIORITY_OPTIONS = ["low", "medium", "high", "critical"];
// Dispatch status options — skip "Moving" in the UI (internal only).
const DISPATCH_STATUS = ["Working", "Researching", "In Meeting", "Idle", "On Break"];

// ---------------------------------------------------------------
// LOCAL/DEMO OPERATION — JARVIS coordinates ATLAS + KAREN to draft a
// social piece. No network, no external AI, no publishing. Runs as a
// staged sequence of MissionState mutations + logs so it reads as live
// coordination instead of a chat transcript.
// Idempotency: a module-level flag prevents overlapping runs while the
// timed phases are still flowing.
// ---------------------------------------------------------------
let __socialPieceOpRunning = false;
let __signalsOpRunning     = false;

// Op keys — used as the snapshot discriminator so the single result panel
// can render different sections per operation.
const OP_KEY_SOCIAL_PIECE   = "social_piece";
const OP_KEY_SIGNALS_REVIEW = "signals_review";

// Single demo-op texts — kept identical to the messages logged below so the
// panel shows exactly what runSocialPieceOperation announces. Edit here, both
// surfaces stay in sync.
const SOCIAL_OP_TEXTS = {
  piece:    "Crear pieza social sobre el proyecto JARVIS",
  hook:     'Hook: "Construí mi propio JARVIS sin contratar a nadie."',
  caption:  'Caption: "Mission Control personal — local, modular, sin atajos."',
  carrusel: "Idea de carrusel: 1) Problema · 2) Diseño · 3) Agentes · 4) Demo · 5) CTA.",
  tono:        "Tono sugerido: directo, técnico, sin hype.",
  observacion: 'Observación social: el ángulo "sin contratar a nadie" funciona; reforzar con prueba visual.',
  respuesta:   'Respuesta sugerida para comunidad: "Sí, todo corre local. En el siguiente post explico el stack."',
  result:   "Draft listo para aprobación",
};

// Derives the per-run text bundle from Danny's custom "Idea base". Empty input
// → static fallback (SOCIAL_OP_TEXTS), so the demo still works untouched. Any
// non-empty idea threads itself into the title, hook, caption, carousel,
// KAREN tono / observación / respuesta so ATLAS + KAREN don't read like a
// frozen script. Pure function — call site decides where to stash the result.
// Mini transformaciones locales — sin IA — para que ATLAS no devuelva la idea
// literal. Buscan el "núcleo" de lo que Danny escribió y lo recombinan en
// Hook / Caption / Carrusel con plantillas más fuertes. Reglas simples:
//   · core(): toma la idea, la limpia, le quita prefijos como "explicar que",
//             "mostrar que", "contar que", el sujeto "JARVIS" si quedó al frente,
//             y normaliza puntuación final. Devuelve un fragmento utilizable
//             como predicado: "ya puede coordinar agentes".
//   · cap1(): primera letra en mayúscula sin tocar el resto.
function _atlasCore(idea) {
  let s = String(idea || "").trim();
  if (!s) return "";
  // Quita signos finales repetidos para poder recomponer.
  s = s.replace(/[\s.!?·…]+$/g, "");
  // Quita prefijos típicos de "intención" para quedarnos con la afirmación.
  const prefixes = [
    /^explicar\s+que\s+/i,
    /^explicar\s+/i,
    /^mostrar\s+que\s+/i,
    /^mostrar\s+/i,
    /^contar\s+que\s+/i,
    /^contar\s+/i,
    /^decir\s+que\s+/i,
    /^hablar\s+sobre\s+/i,
    /^hablar\s+de\s+/i,
    /^anunciar\s+que\s+/i,
    /^anunciar\s+/i,
    /^compartir\s+que\s+/i,
    /^compartir\s+/i,
    /^demostrar\s+que\s+/i,
    /^demostrar\s+/i,
  ];
  for (const re of prefixes) s = s.replace(re, "");
  // Si arranca con "JARVIS …", lo quitamos para que el Hook pueda anteponer
  // su propio sujeto sin duplicar el nombre.
  s = s.replace(/^jarvis\s+/i, "");
  return s.trim();
}
function _cap1(s) {
  if (!s) return s;
  return s.charAt(0).toUpperCase() + s.slice(1);
}

function buildSocialOpTexts(rawIdea) {
  const idea = (rawIdea || "").trim();
  if (!idea) return { ...SOCIAL_OP_TEXTS, ideaBase: "" };
  const short = idea.length > 110 ? idea.slice(0, 107).trimEnd() + "…" : idea;
  const core  = _atlasCore(idea) || idea;        // predicado limpio
  const coreShort = core.length > 90 ? core.slice(0, 87).trimEnd() + "…" : core;

  // Plantillas locales — no son IA, pero ya no copian la idea literal.
  const hook     = 'Hook: "JARVIS ya no solo está en pantalla: ' + coreShort + '."';
  const caption  = 'Caption: "Primero construí el cuerpo. Ahora ' + coreShort
                 + ' — todo dentro de Mission Control, local y modular."';
  const carrusel = 'Idea de carrusel: 1) El problema: una interfaz sola no sirve si no coordina nada · '
                 + '2) El avance: ' + _cap1(coreShort) + ' · '
                 + '3) ATLAS crea contenido · '
                 + '4) KAREN revisa tono y señales · '
                 + '5) Danny aprueba antes de publicar.';

  return {
    piece:    "Crear pieza social: " + short,
    hook:     hook,
    caption:  caption,
    carrusel: carrusel,
    tono:        "Tono sugerido: directo y técnico, alineado con tu idea base.",
    observacion: 'Observación social: el ángulo "' + coreShort + '" se lee honesto; reforzar con prueba visual del estado actual.',
    respuesta:   'Respuesta sugerida para comunidad: "Sí — ' + coreShort + '. Todo corre local; el siguiente post explica el stack."',
    result:   "Draft listo para aprobación",
    ideaBase: idea,
  };
}

// ---------------------------------------------------------------
// ATLAS bridge — punto único de entrada para que la operación pida
// "Hook + Caption + Carrusel + revisión" sin saber si vienen de IA real o del
// fallback local. Hoy SIEMPRE devuelve fallback (`buildSocialOpTexts`).
// Más adelante, cuando exista puente seguro a IA real, esta función podrá:
//   · si ATLAS_AI_ENABLED=true  → llamar generador remoto + validar salida
//   · si no                     → usar fallback local
//   · si falla IA               → usar fallback local
// El contrato de retorno es el mismo objeto-de-textos que ya consume
// runSocialPieceOperation, más una propiedad `source` informativa que el
// panel puede ignorar sin romperse. Ver:
//   docs/ATLAS_AI_SPEC.md
//   docs/AI_CONFIG_PLAN.md
//   docs/AI_INTEGRATION_ROADMAP.md
// ---------------------------------------------------------------
// URL del endpoint mock local. El backend Express ya expone esta ruta
// (ver server.js · POST /api/atlas/generate-content). Sin IA real detrás.
const ATLAS_ENDPOINT_URL = (window.JARVIS_API_BASE || "http://localhost:3001") + "/api/atlas/generate-content";
// Si el endpoint no responde, abortamos y caemos a local. AI_TEST con modelo
// real tarda más que el mock, así que el cap se elevó a 15s para no abortar
// llamadas legítimas a OpenAI. La operación sigue avanzando con setTimeout
// internos; este límite solo protege de cuelgues de red, no marca el ritmo.
const ATLAS_ENDPOINT_TIMEOUT_MS = 15000;

// Construye el objeto-de-textos legacy (hook/caption/carrusel/tono/...) a
// partir del payload validado del endpoint mock. Los campos KAREN no vienen
// del endpoint todavía → se derivan del fallback local sobre la misma idea
// base, así la operación se lee coherente. Mantiene los prefijos `Hook:` /
// `Caption:` y el wrapping en comillas para empatar con el formato legacy
// que ya consume el panel y los logs.
function _atlasMapPayloadToTexts(payload, ideaBase) {
  const trimmed = String(ideaBase || "").trim();
  const short = trimmed.length > 110 ? trimmed.slice(0, 107).trimEnd() + "…" : trimmed;
  const fallback = buildSocialOpTexts(ideaBase);
  const carouselText = "Idea de carrusel: " + payload.carousel.map((s, i) => {
    const body = (s.body && s.body.length > 80) ? s.body.slice(0, 77).trimEnd() + "…" : (s.body || "");
    return (i + 1) + ") " + s.title + (body ? ": " + body : "");
  }).join(" · ");
  return {
    piece:    "Crear pieza social: " + (short || "(sin idea base)"),
    hook:     'Hook: "' + payload.hook + '"',
    caption:  'Caption: "' + payload.caption + '"',
    carrusel: carouselText,
    // KAREN: el endpoint mock no la cubre → reusamos el fallback local
    // derivado de la misma ideaBase. Cuando KAREN tenga su propio endpoint,
    // este bloque será el punto natural de empalme.
    tono:        fallback.tono,
    observacion: fallback.observacion,
    respuesta:   fallback.respuesta,
    result:   "Draft listo para aprobación",
    ideaBase: trimmed || fallback.ideaBase,
  };
}

// Bridge ATLAS — ahora intenta el endpoint mock primero y cae a local si:
//   · ideaBase vacía (el endpoint la rechazaría con 400 de todos modos)
//   · timeout
//   · status no-2xx
//   · json.ok !== true / payload faltante
//   · payload no pasa validateAtlasContentPayload
//   · cualquier excepción de red/JSON
// Siempre resuelve — nunca rechaza — para que el orquestador pueda hacer
// `await` sin try/catch obligatorio. La propiedad `source` indica el origen
// real: `"ai"` si el backend reporta IA real (AI_TEST), `"mock_endpoint"` si
// el backend respondió mock o no envió meta.source, o `"local_fallback"` si
// hubo que recurrir a buildSocialOpTexts.
async function generateAtlasContent(ideaBase, options) {
  void options;
  const trimmed = String(ideaBase || "").trim();

  // Sin idea propia → fallback inmediato. El endpoint rechazaría con
  // missing_ideaBase de todas formas; ahorrar el round-trip mantiene la
  // experiencia legacy del demo idéntica al pasado.
  if (!trimmed) {
    return Object.assign({}, buildSocialOpTexts(ideaBase), { source: "local_fallback" });
  }

  // AbortController para no bloquear la operación si el backend está caído.
  const ctrl = (typeof AbortController !== "undefined") ? new AbortController() : null;
  const timer = ctrl ? setTimeout(() => ctrl.abort(), ATLAS_ENDPOINT_TIMEOUT_MS) : null;

  try {
    const res = await fetch(ATLAS_ENDPOINT_URL, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        ideaBase: trimmed,
        format:   "carousel",
        tone:     "directa",
        context:  {
          project:  "Mission Control/JARVIS",
          audience: "LATAM",
          mode:     "LOCAL_DEMO",
        },
      }),
      signal: ctrl ? ctrl.signal : undefined,
    });
    if (timer) clearTimeout(timer);
    if (!res.ok) throw new Error("http_" + res.status);
    const json = await res.json();
    if (!json || json.ok !== true || !json.payload) throw new Error("response_not_ok");

    const validation = validateAtlasContentPayload(json.payload);
    if (!validation.ok) throw new Error("invalid_payload:" + validation.errors.join(","));

    const mapped = _atlasMapPayloadToTexts(json.payload, ideaBase);
    // Propaga la fuente reportada por el backend (`payload.meta.source`) al
    // snapshot. El backend es la autoridad: el handler AI_TEST sobreescribe
    // meta con valores fijos antes de responder, así que cuando viene "ai"
    // es porque pasó por el modelo real. Si por algún motivo falta, caemos
    // a "mock_endpoint" — sigue siendo respuesta válida del backend, no del
    // fallback local.
    const meta = json.payload && json.payload.meta;
    const reportedSource = (meta && typeof meta.source === "string" && meta.source.trim())
      ? meta.source.trim()
      : "mock_endpoint";
    return Object.assign({}, mapped, { source: reportedSource });
  } catch (e) {
    if (timer) clearTimeout(timer);
    if (typeof console !== "undefined" && console.warn) {
      console.warn("[atlas] endpoint no disponible o payload inválido — fallback local:",
        e && e.message ? e.message : e);
    }
    return Object.assign({}, buildSocialOpTexts(ideaBase), { source: "local_fallback" });
  }
}

// ---------------------------------------------------------------
// ATLAS payload validator — pensado para ejecutarse SOBRE la respuesta cruda
// que devuelva la IA real cuando ATLAS_AI_ENABLED=true. Hoy no se invoca
// porque el puente sigue cayendo al fallback local en `generateAtlasContent`.
// Queda listo como red de seguridad para que ningún payload mal formado
// pueda guardarse como draft. Si !ok → la operación debe usar fallback.
// Reglas validadas (ver docs/ATLAS_AI_SPEC.md y docs/AI_CONFIG_PLAN.md):
//   1. payload es objeto plano
//   2. hook: string no vacío
//   3. caption: string no vacío
//   4. carousel: array
//   5. carousel: mínimo 3 elementos
//   6. cada carousel item: title y body string no vacío
//   7. safety: existe (objeto)
//   8. safety.needsHumanApproval === true
//   9. safety.canPublish === false
// Salida: { ok: boolean, errors: string[] }. errors son códigos cortos,
// no mensajes para usuario final — la UI puede traducirlos si hace falta.
// ---------------------------------------------------------------
function validateAtlasContentPayload(payload) {
  const errors = [];
  const isNonEmptyString = (v) => typeof v === "string" && v.trim().length > 0;
  const isPlainObject = (v) => v !== null && typeof v === "object" && !Array.isArray(v);

  if (!isPlainObject(payload)) {
    return { ok: false, errors: ["invalid_payload"] };
  }
  if (!isNonEmptyString(payload.hook))    errors.push("missing_hook");
  if (!isNonEmptyString(payload.caption)) errors.push("missing_caption");

  if (!Array.isArray(payload.carousel)) {
    errors.push("missing_carousel");
  } else {
    if (payload.carousel.length < 3) errors.push("carousel_too_short");
    for (let i = 0; i < payload.carousel.length; i++) {
      const item = payload.carousel[i];
      if (!isPlainObject(item)) {
        errors.push("invalid_carousel_item:" + i);
        continue;
      }
      if (!isNonEmptyString(item.title)) errors.push("missing_carousel_title:" + i);
      if (!isNonEmptyString(item.body))  errors.push("missing_carousel_body:" + i);
    }
  }

  if (!isPlainObject(payload.safety)) {
    errors.push("missing_safety");
  } else {
    if (payload.safety.needsHumanApproval !== true) errors.push("invalid_safety_needsHumanApproval");
    if (payload.safety.canPublish        !== false) errors.push("invalid_safety_canPublish");
  }

  return { ok: errors.length === 0, errors: errors };
}

// Signals-review op — fixed strings. Counts are derived from the live inbox
// at execution time (see runSignalsReviewOperation), so the panel reflects
// the real mock data.
const SIGNALS_OP_TEXTS = {
  title:        "Revisar señales sociales",
  acciones:     "Acciones sugeridas: priorizar colaboraciones y oportunidades; responder a comunidad; archivar spam.",
  result:       "Señales sociales listas para revisión",
};

// Single bus event reused by every team-level operation. Snapshot's opKey
// field discriminates which sections the panel renders. Identifier keeps
// its legacy name to avoid touching saveSocialOpAsContentDraft.
const SOCIAL_OP_EVENT = "social-piece-op:update";

// Tiny pub/sub on window so the module-level orchestrator and the React panel
// share state without rewiring callbacks. The latest snapshot lives on
// window.__socialPieceOpResult; subscribers listen to SOCIAL_OP_EVENT.
function publishSocialOpResult(result) {
  window.__socialPieceOpResult = result;
  try { window.dispatchEvent(new CustomEvent(SOCIAL_OP_EVENT, { detail: result })); } catch (_) {}
}
function dismissSocialOpResult() { publishSocialOpResult(null); }

// Save the current operation result as a Content draft. Reuses the existing
// MissionState.addContentDraft path — no parallel store, no schema changes.
// Format: "carousel" because it's the only kind whose schema accepts an
// arbitrary list of titled blocks, which fits packaging Hook + Caption +
// Carousel-idea + KAREN review notes into a single draft. Idempotent: if
// the snapshot already carries a draftId, returns it unchanged.
function saveSocialOpAsContentDraft() {
  const ms = window.MissionState;
  const snap = window.__socialPieceOpResult;
  if (!ms || !snap) return null;
  if (snap.state !== "done") return null;
  if (snap.draftId) return ms.getContentDraftById(snap.draftId) || null;
  if (typeof ms.addContentDraft !== "function") return null;

  // Re-derive from buildSocialOpTexts(snap.ideaBase) so the draft mirrors the
  // exact strings the operation announced — including Danny's custom idea
  // when present, or the legacy demo strings when the input was empty.
  const T = buildSocialOpTexts(snap.ideaBase);
  // Only insert the "Idea base (Danny)" slide when there's a real custom idea —
  // empty-idea draft stays visually identical to the legacy demo output.
  const slides = [];
  let idx = 1;
  if (T.ideaBase) {
    slides.push({ index: idx++, title: "Idea base (Danny)", body: '"' + T.ideaBase + '"' });
  }
  slides.push(
    { index: idx++, title: "Hook (ATLAS)",                  body: T.hook },
    { index: idx++, title: "Caption (ATLAS)",               body: T.caption },
    { index: idx++, title: "Idea de carrusel (ATLAS)",      body: T.carrusel },
    { index: idx++, title: "Tono sugerido (KAREN)",         body: T.tono },
    { index: idx++, title: "Observación social (KAREN)",    body: T.observacion },
    { index: idx++, title: "Respuesta para comunidad (KAREN)", body: T.respuesta },
    { index: idx++, title: "Estado",                        body: "Pendiente de aprobación." },
  );
  // Campo principal del draft: cuando Danny escribió una idea, mostramos solo
  // "Idea base de Danny: <idea>" — sin "Crear pieza social:", sin créditos del
  // flujo y sin comillas. Toda esa metadata operativa ya vive en las slides.
  // Sin idea propia: se conserva el texto fallback original del demo.
  const ideaSummary = T.ideaBase
    ? "Idea base de Danny: " + T.ideaBase
    : T.piece + " · Coordinado por JARVIS · Redactado por ATLAS · Revisado por KAREN · Pendiente de aprobación.";
  const draft = ms.addContentDraft({
    idea:       ideaSummary,
    format:     "carousel",
    activeTone: "directa",
    variants: {
      directa: {
        output:       { kind: "carousel", slides: slides },
        variantIndex: 0,
      },
    },
  });
  if (!draft) return null;

  ms.addLog({
    type: "OPS", source: "JARVIS", severity: "low",
    message: 'Draft enviado a CONTENIDO: "' + T.piece + '" (pendiente de aprobación).',
  });

  // Mirror the saved id into the snapshot so the panel shows "Guardado…"
  // and the button stays disabled until a new operation overwrites it.
  publishSocialOpResult({ ...snap, draftId: draft.id, savedAt: Date.now() });
  return draft;
}

function runSocialPieceOperation(onComplete, ideaBase) {
  const ms = window.MissionState;
  if (!ms) { console.warn("[ops] MissionState no disponible."); return false; }
  if (__socialPieceOpRunning) {
    ms.addLog({ type: "OPS", source: "JARVIS", severity: "low",
      message: "Operación 'Crear pieza social' ya en curso — ignorando nueva orden." });
    return false;
  }
  const jarvis = ms.getAgentById("jarvis");
  const atlas  = ms.getAgentById("atlas");
  const karen  = ms.getAgentById("karen");
  if (!jarvis) { ms.addLog({ type: "OPS", source: "JARVIS", severity: "high",
    message: "Operación abortada: JARVIS no disponible." }); return false; }
  if (!atlas) { ms.addLog({ type: "OPS", source: "JARVIS", severity: "high",
    message: "Operación abortada: ATLAS no encontrado en el equipo." }); return false; }
  if (!karen) { ms.addLog({ type: "OPS", source: "JARVIS", severity: "high",
    message: "Operación abortada: KAREN no encontrada en el equipo." }); return false; }

  __socialPieceOpRunning = true;
  // Phase-0 prep: usamos `buildSocialOpTexts` síncrono solo para `piece` y
  // `ideaBase`, que dependen únicamente del input de Danny. Tanto el endpoint
  // como el fallback coinciden en estos dos campos, así que se pueden mostrar
  // de inmediato sin esperar la red. Los textos de ATLAS/KAREN reales se
  // resuelven más abajo (Phase 2) cuando llegue `atlasPromise`.
  const phase0 = buildSocialOpTexts(ideaBase);
  const PIECE = phase0.piece;
  // Bridge ATLAS — kicks off ya. Endpoint mock o fallback local. La promesa
  // siempre resuelve, nunca rechaza, así que `await` aquí es seguro sin
  // try/catch obligatorio. Para el momento en que dispare Phase 2 (~2.3s
  // desde aquí) lo normal es que ya haya respondido.
  const atlasPromise = generateAtlasContent(ideaBase);

  const prevAtlas = { task: atlas.task || "", status: atlas.status, activity: atlas.activity, progress: atlas.progress || 0 };
  const prevKaren = { task: karen.task || "", status: karen.status, activity: karen.activity, progress: karen.progress || 0 };

  // Snapshot reset — running operation always replaces the previous result.
  // ideaBase is persisted on the snapshot so the panel and the save-to-Content
  // path can both re-derive Danny's wording without re-asking him.
  const snap = {
    opKey:  OP_KEY_SOCIAL_PIECE,
    startedAt: Date.now(),
    updatedAt: Date.now(),
    state:  "running",
    status: "Coordinando agentes…",
    ideaBase: phase0.ideaBase,
    jarvis: ["Orden recibida"],
    atlas:  [],
    karen:  [],
    result: null,
  };
  publishSocialOpResult(snap);

  // Phase 0 — JARVIS receives the order
  ms.addLog({ type: "OPS", source: "JARVIS", severity: "medium",
    message: "Operación recibida [demo local]: " + PIECE + "." });
  if (phase0.ideaBase) {
    ms.addLog({ type: "OPS", source: "JARVIS", severity: "low",
      message: 'Idea base de Danny: "' + phase0.ideaBase + '"' });
  }
  ms.updateAgent("jarvis", { task: PIECE, status: "Working", activity: "working", progress: 10 });
  ms.addLog({ type: "OPS", source: "JARVIS", severity: "low",
    message: "JARVIS coordina a ATLAS (contenido) y KAREN (señales sociales)." });
  snap.jarvis = phase0.ideaBase
    ? ["Orden recibida", 'Idea base: "' + phase0.ideaBase + '"', "Coordinación iniciada"]
    : ["Orden recibida", "Coordinación iniciada"];
  snap.updatedAt = Date.now();
  publishSocialOpResult({ ...snap });

  // Phase 1 — assign content to ATLAS
  setTimeout(() => {
    ms.updateAgent("atlas", { task: "Redactar pieza social: hook + caption + carrusel",
      status: "Working", activity: "working", progress: 15 });
    ms.addLog({ type: "OPS", source: "JARVIS", severity: "low",
      message: "JARVIS → ATLAS: redactar contenido (hook, caption, idea de carrusel)." });
    ms.updateAgent("jarvis", { progress: 25 });

    // Phase 2 — espera a ATLAS (endpoint mock o fallback) y publica su salida
    setTimeout(async () => {
      let T;
      try {
        T = await atlasPromise;
      } catch (_e) {
        // Defensa extra: generateAtlasContent ya promete no rechazar nunca,
        // pero si pasa, recuperamos con fallback local puro.
        T = Object.assign({}, buildSocialOpTexts(ideaBase), { source: "local_fallback" });
      }
      ms.addLog({ type: "OPS", source: "ATLAS", severity: "low", message: T.hook });
      ms.addLog({ type: "OPS", source: "ATLAS", severity: "low", message: T.caption });
      ms.addLog({ type: "OPS", source: "ATLAS", severity: "low", message: T.carrusel });
      ms.updateAgent("atlas", { task: prevAtlas.task || "En espera",
        status: prevAtlas.status || "Idle", activity: prevAtlas.activity || "idle", progress: prevAtlas.progress });
      ms.updateAgent("jarvis", { progress: 55 });
      snap.atlas = [T.hook, T.caption, T.carrusel];
      // Stash de origen en el snapshot — campo extra inerte para el panel y
      // disponible para herramientas/consola si se quiere inspeccionar.
      snap.source = T.source || "local_fallback";
      snap.updatedAt = Date.now();
      publishSocialOpResult({ ...snap });

      // Phase 3 — JARVIS routes draft to KAREN for review
      setTimeout(() => {
        ms.addLog({ type: "OPS", source: "JARVIS", severity: "low",
          message: "JARVIS → KAREN: revisar tono, señal social y respuesta para comunidad." });
        ms.updateAgent("karen", { task: "Revisar pieza social: tono + señal + respuesta",
          status: "Researching", activity: "researching", progress: 20 });
        ms.updateAgent("jarvis", { progress: 70 });

        // Phase 4 — KAREN reviews
        setTimeout(() => {
          ms.addLog({ type: "OPS", source: "KAREN", severity: "low", message: T.tono });
          ms.addLog({ type: "OPS", source: "KAREN", severity: "low", message: T.observacion });
          ms.addLog({ type: "OPS", source: "KAREN", severity: "low", message: T.respuesta });
          ms.updateAgent("karen", { task: prevKaren.task || "En espera",
            status: prevKaren.status || "Idle", activity: prevKaren.activity || "idle", progress: prevKaren.progress });
          snap.karen = [T.tono, T.observacion, T.respuesta];
          snap.updatedAt = Date.now();
          publishSocialOpResult({ ...snap });

          // Phase 5 — final
          ms.updateAgent("jarvis", { task: T.result, progress: 100 });
          ms.addLog({ type: "OPS", source: "JARVIS", severity: "medium",
            message: "Operación completada [demo local]: " + T.result + "." });
          snap.state = "done";
          snap.status = T.result;
          snap.result = T.result;
          snap.updatedAt = Date.now();
          publishSocialOpResult({ ...snap });
          __socialPieceOpRunning = false;
          if (typeof onComplete === "function") onComplete();
        }, 1500);
      }, 1200);
    }, 1500);
  }, 800);

  return true;
}

// ---------------------------------------------------------------
// LOCAL/DEMO OPERATION #2 — JARVIS → KAREN: review the inbox.
// Reads MissionState's inbox mock (seeded with realistic categories) and
// produces a structured review. Same staged-timing pattern as
// runSocialPieceOperation; same bus, different opKey so the panel renders
// JARVIS / KAREN / BANDEJA / RESULT instead of ATLAS-flavored sections.
// Fully local — no network, no Instagram, no replies sent.
// ---------------------------------------------------------------
function runSignalsReviewOperation(onComplete) {
  const ms = window.MissionState;
  if (!ms) { console.warn("[ops] MissionState no disponible."); return false; }
  if (__signalsOpRunning) {
    ms.addLog({ type: "OPS", source: "JARVIS", severity: "low",
      message: "Operación 'Revisar señales sociales' ya en curso — ignorando nueva orden." });
    return false;
  }
  const jarvis = ms.getAgentById("jarvis");
  const karen  = ms.getAgentById("karen");
  if (!jarvis) { ms.addLog({ type: "OPS", source: "JARVIS", severity: "high",
    message: "Operación abortada: JARVIS no disponible." }); return false; }
  if (!karen) { ms.addLog({ type: "OPS", source: "JARVIS", severity: "high",
    message: "Operación abortada: KAREN no encontrada en el equipo." }); return false; }

  __signalsOpRunning = true;
  const TITLE = SIGNALS_OP_TEXTS.title;

  // Read live mock inbox so the demo reflects whatever the operator sees
  // when they open Bandeja. Falls back to zeros if the API isn't there.
  const inbox = (typeof ms.getInbox === "function" ? ms.getInbox() : []) || [];
  const countBy = (pred) => inbox.filter(pred).length;
  const collabCount      = countBy(m => m.category === "collab");
  const communityCount   = countBy(m => m.category === "community");
  const opportunityCount = countBy(m => m.category === "opportunity");
  const spamCount        = countBy(m => m.category === "spam");
  const urgentCount      = countBy(m => m.category === "urgent");
  const pendingCount     = countBy(m => m.status === "new" || m.status === "pending");
  const total            = inbox.length;

  const karenItems = [
    "Colaboración detectada: " + collabCount + " mensaje" + (collabCount === 1 ? "" : "s") + " — atender primero.",
    "Comunidad detectada: " + communityCount + " mensaje" + (communityCount === 1 ? "" : "s") + " — responder con tono cercano.",
    "Respuestas pendientes: " + pendingCount + " sin atender.",
    "Ruido / spam filtrado: " + spamCount + " descartado" + (spamCount === 1 ? "" : "s") + ".",
  ];
  const bandejaItems = [
    "Resumen: " + total + " señales · " + collabCount + " colab · " + communityCount + " comunidad · "
      + opportunityCount + " oportunidad" + (opportunityCount === 1 ? "" : "es")
      + " · " + urgentCount + " urgente" + (urgentCount === 1 ? "" : "s")
      + " · " + spamCount + " spam.",
    SIGNALS_OP_TEXTS.acciones,
  ];

  const prevKaren = { task: karen.task || "", status: karen.status, activity: karen.activity, progress: karen.progress || 0 };

  // Snapshot reset — replaces any prior op result.
  const snap = {
    opKey:  OP_KEY_SIGNALS_REVIEW,
    startedAt: Date.now(),
    updatedAt: Date.now(),
    state:  "running",
    status: "Coordinando revisión…",
    jarvis: ["Orden recibida"],
    karen:  [],
    bandeja:[],
    result: null,
  };
  publishSocialOpResult(snap);

  // Phase 0 — JARVIS receives
  ms.addLog({ type: "OPS", source: "JARVIS", severity: "medium",
    message: "Operación recibida [demo local]: " + TITLE + "." });
  ms.updateAgent("jarvis", { task: TITLE, status: "Working", activity: "working", progress: 10 });
  ms.addLog({ type: "OPS", source: "JARVIS", severity: "low",
    message: "JARVIS asigna revisión de Bandeja a KAREN." });
  snap.jarvis = ["Orden recibida", "Revisión asignada a KAREN"];
  snap.updatedAt = Date.now();
  publishSocialOpResult({ ...snap });

  // Phase 1 — KAREN starts analysis
  setTimeout(() => {
    ms.updateAgent("karen", { task: "Revisar señales sociales (Bandeja)",
      status: "Researching", activity: "researching", progress: 20 });
    ms.addLog({ type: "OPS", source: "JARVIS", severity: "low",
      message: "JARVIS → KAREN: analizar Bandeja (colab, comunidad, oportunidades, spam)." });
    ms.updateAgent("jarvis", { progress: 30 });

    // Phase 2 — KAREN produces review items
    setTimeout(() => {
      karenItems.forEach(item => {
        ms.addLog({ type: "OPS", source: "KAREN", severity: "low", message: item });
      });
      snap.karen = karenItems.slice();
      snap.updatedAt = Date.now();
      publishSocialOpResult({ ...snap });
      ms.updateAgent("jarvis", { progress: 60 });

      // Phase 3 — JARVIS rolls up Bandeja summary
      setTimeout(() => {
        bandejaItems.forEach(item => {
          ms.addLog({ type: "OPS", source: "JARVIS", severity: "low", message: item });
        });
        snap.bandeja = bandejaItems.slice();
        snap.updatedAt = Date.now();
        publishSocialOpResult({ ...snap });
        ms.updateAgent("jarvis", { progress: 85 });

        // Phase 4 — final
        setTimeout(() => {
          ms.updateAgent("karen", { task: prevKaren.task || "En espera",
            status: prevKaren.status || "Idle", activity: prevKaren.activity || "idle", progress: prevKaren.progress });
          ms.updateAgent("jarvis", { task: SIGNALS_OP_TEXTS.result, progress: 100 });
          ms.addLog({ type: "OPS", source: "JARVIS", severity: "medium",
            message: "Operación completada [demo local]: " + SIGNALS_OP_TEXTS.result + "." });
          snap.state  = "done";
          snap.status = SIGNALS_OP_TEXTS.result;
          snap.result = SIGNALS_OP_TEXTS.result;
          snap.updatedAt = Date.now();
          publishSocialOpResult({ ...snap });
          __signalsOpRunning = false;
          if (typeof onComplete === "function") onComplete();
        }, 900);
      }, 1100);
    }, 1400);
  }, 800);

  return true;
}

function DispatchAgentModal({ agent, onClose, onSaved }) {
  // JARVIS is the operations director — its agent.task often carries the
  // result of the last demo operation (e.g. "Señales sociales listas para
  // revisión"). Pre-filling that into the manual dispatch field reads as if
  // the form is tied to the completed operation, which it isn't. Start the
  // manual title empty for JARVIS so the form is unmistakably a fresh order.
  // Other agents keep the existing prefill behavior.
  const isJarvis = agent.id === "jarvis";
  const [title, setTitle]       = useState(isJarvis ? "" : (agent.task || ""));
  const [room, setRoom]         = useState(agent.room || "office");
  const [priority, setPriority] = useState("medium");
  const [status, setStatus]     = useState(agent.status && DISPATCH_STATUS.includes(agent.status) ? agent.status : "Working");
  const [progress, setProgress] = useState(0);
  const [error, setError]       = useState("");
  // Custom "Idea base" for the Crear pieza social op. Empty string falls
  // back to the static demo text inside runSocialPieceOperation.
  const [socialIdea, setSocialIdea] = useState("");
  const titleRef = useRef(null);

  useEffect(() => {
    if (titleRef.current) titleRef.current.focus();
    const onKey = (e) => { if (e.key === "Escape") onClose(); };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [onClose]);

  const submit = (e) => {
    e.preventDefault();
    const ms = window.MissionState;
    if (!ms) { setError("MissionState no disponible."); return; }

    const t = title.trim();
    if (!t)                                 { setError("El título de la tarea es obligatorio."); return; }
    if (!ms.getRoomById(room))              { setError("Sala de destino inválida.");            return; }
    if (!STATUS_STYLES[status])             { setError("Estado inválido.");                     return; }
    if (PRIORITY_OPTIONS.indexOf(priority) === -1) { setError("Prioridad inválida.");           return; }
    const prog = Math.max(0, Math.min(100, Number(progress) || 0));

    const prevRoom   = agent.room;
    const prevStatus = agent.status;
    const prevTask   = agent.task || "";
    const roomChanged   = room !== prevRoom;
    const statusChanged = status !== prevStatus;
    const taskChanged   = t !== prevTask;

    // --- Announce intent first ---
    ms.addLog({
      type: "OPS", source: "DISPATCH", severity: "low",
      message: "Despliegue solicitado para " + agent.name.toUpperCase() + ".",
    });

    // --- Apply changes (updateAgent first so moveAgentToRoom finalStatus is correct) ---
    ms.updateAgent(agent.id, {
      status:   status,
      activity: statusToActivity(status),
      task:     t,
      progress: prog,
    });

    if (roomChanged) {
      ms.moveAgentToRoom(agent.id, room, {
        transitionMs:  0,
        autoLog:       true,
        finalStatus:   status,
        finalActivity: statusToActivity(status),
      });
    }

    // --- Upsert the task row in state.tasks (authoritative task record) ---
    ms.assignTaskToAgent(agent.id, {
      title:    t,
      status:   "running",
      priority: priority,
      progress: prog,
    });

    // --- Summary logs (only for actual changes) ---
    const label    = agent.name.toUpperCase();
    const roomName = ms.getRoomById(room).title;

    if (roomChanged) {
      ms.addLog({
        type: "OPS", source: "DISPATCH", severity: "low",
        message: label + " asignado a " + roomName + ".",
      });
    }
    if (taskChanged) {
      ms.addLog({
        type: "OPS", source: "DISPATCH", severity: priority === "critical" ? "high" : "low",
        message: 'Tarea "' + t + '" asignada a ' + label + " [" + priorityLabel(priority).toUpperCase() + "].",
      });
    }
    if (statusChanged) {
      ms.addLog({
        type: "OPS", source: "DISPATCH", severity: "low",
        message: label + " estado cambiado a " + statusLabel(status) + ".",
      });
    }

    onSaved(agent.id);
  };

  const roomObj = window.MissionState ? window.MissionState.getRoomById(room) : null;
  const sameRoom = room === agent.room;

  return (
    <div style={modalStyles.backdrop} onMouseDown={onClose} role="dialog" aria-modal="true" aria-label={`Desplegar a ${agent.name}`}>
      <div style={modalStyles.card} onMouseDown={(e) => e.stopPropagation()}>
        <div style={modalStyles.head}>
          <div>
            <div style={modalStyles.eyebrow}>OPS · DESPLEGAR AGENTE</div>
            <div style={modalStyles.title}>Desplegar a {agent.name}</div>
          </div>
          <button type="button" onClick={onClose} style={modalStyles.closeBtn} aria-label="Cerrar">×</button>
        </div>

        {/* Agent context (read-only) */}
        <div style={dispatchStyles.context}>
          <div style={dispatchStyles.contextAvatar}>
            <div style={{
              ...modalStyles.previewGlow,
              background: `radial-gradient(circle, ${(window.ROBOT_PALETTES?.[agent.color] || {}).glow || "#4FD8FF"}44, transparent 70%)`,
            }} />
            <RobotAvatar color={agent.color} size={40} />
          </div>
          <div style={{ minWidth: 0 }}>
            <div style={dispatchStyles.contextName}>{agent.name}</div>
            <div style={dispatchStyles.contextMeta}>
              {roleLabel(agent.role)} · actualmente en {(window.MissionState?.getRoomById(agent.room) || { title: "—" }).title}
            </div>
          </div>
        </div>

        {/* Demo operations for the selected agent. JARVIS sees both core
            operations; KAREN sees the signals-review op only. Other agents
            see no menu — only the manual dispatch form below.
            Fully local — no external API, no publishing, no real Instagram. */}
        {(() => {
          const ops = [];
          if (agent.id === "jarvis") {
            ops.push({
              key:   "social_piece",
              title: "Crear pieza social",
              crumb: "JARVIS · ATLAS · KAREN → CONTENIDO",
              desc:  "Coordina ATLAS y KAREN para producir un draft y guardarlo en CONTENIDO.",
              run:   runSocialPieceOperation,
            });
          }
          if (agent.id === "jarvis" || agent.id === "karen") {
            ops.push({
              key:   "signals_review",
              title: "Revisar señales sociales",
              crumb: "JARVIS · KAREN → BANDEJA",
              desc:  "Coordina KAREN para revisar Bandeja y resumir señales sociales.",
              run:   runSignalsReviewOperation,
            });
          }
          if (ops.length === 0) return null;
          return (
            <>
              <div style={dispatchStyles.opPanel}>
                <div style={dispatchStyles.opHeader}>
                  <span style={dispatchStyles.opEyebrow}>
                    {ops.length > 1 ? "OPERACIONES DISPONIBLES · LOCAL / DEMO" : "OPERACIÓN DISPONIBLE · LOCAL / DEMO"}
                  </span>
                  <span style={dispatchStyles.opMockTag}>MOCK · SIN API</span>
                </div>
                <div style={dispatchStyles.opGrid}>
                  {ops.map(op => {
                    const isSocialPiece = op.key === "social_piece";
                    return (
                      <div key={op.key} style={dispatchStyles.opCard}>
                        <div style={dispatchStyles.opCardCrumb}>{op.crumb}</div>
                        <div style={dispatchStyles.opCardTitle}>{op.title}</div>
                        <div style={dispatchStyles.opCardDesc}>{op.desc}</div>
                        {isSocialPiece && (
                          <label style={dispatchStyles.opIdeaRow}>
                            <span style={dispatchStyles.opIdeaLabel}>IDEA BASE <em style={dispatchStyles.opIdeaHint}>(opcional)</em></span>
                            <textarea
                              value={socialIdea}
                              onChange={(e) => setSocialIdea(e.target.value)}
                              placeholder="Ej: explicar que JARVIS ya coordina agentes, pero todavía no tiene cerebro real conectado."
                              style={dispatchStyles.opIdeaInput}
                              rows={2}
                              maxLength={220}
                              aria-label="Idea base para la pieza social (opcional)"
                            />
                          </label>
                        )}
                        <button
                          type="button"
                          style={dispatchStyles.opCardBtn}
                          onClick={() => {
                            const ok = isSocialPiece
                              ? op.run(undefined, socialIdea)
                              : op.run();
                            if (ok) onClose();
                          }}
                          aria-label={"Ejecutar operación demo: " + op.title}
                        >
                          ▶ EJECUTAR OPERACIÓN
                        </button>
                      </div>
                    );
                  })}
                </div>
              </div>

              {/* Discreet separator so the manual dispatch reads as a
                  fallback below the operations menu, not a co-equal block. */}
              <div style={dispatchStyles.manualSeparator}>
                <span style={dispatchStyles.manualSeparatorLine} />
                <span style={dispatchStyles.manualSeparatorLabel}>DESPLIEGUE MANUAL</span>
                <span style={dispatchStyles.manualSeparatorLine} />
              </div>
            </>
          );
        })()}

        <form onSubmit={submit} style={modalStyles.form}>
          <label style={modalStyles.row}>
            <span style={modalStyles.label}>TÍTULO DE LA TAREA <em style={modalStyles.req}>*</em></span>
            <input
              ref={titleRef}
              type="text"
              value={title}
              onChange={(e) => { setTitle(e.target.value); setError(""); }}
              placeholder={isJarvis ? "Escribe una orden manual…" : "ej. Redactar brief competitivo Q2"}
              style={modalStyles.input}
              maxLength={80}
              autoComplete="off"
            />
          </label>

          <div style={modalStyles.grid2}>
            <label style={modalStyles.row}>
              <span style={modalStyles.label}>
                SALA DE DESTINO
                {!sameRoom && <em style={dispatchStyles.changeHint}>  reubicación</em>}
              </span>
              <select value={room} onChange={(e) => setRoom(e.target.value)} style={modalStyles.input}>
                {deriveRooms().map(r => <option key={r.id} value={r.id}>{r.name}</option>)}
              </select>
            </label>
            <label style={modalStyles.row}>
              <span style={modalStyles.label}>PRIORIDAD</span>
              <select value={priority} onChange={(e) => setPriority(e.target.value)} style={modalStyles.input}>
                {PRIORITY_OPTIONS.map(p => <option key={p} value={p}>{priorityLabel(p).toUpperCase()}</option>)}
              </select>
            </label>
          </div>

          <div style={modalStyles.grid2}>
            <label style={modalStyles.row}>
              <span style={modalStyles.label}>ESTADO RESULTANTE</span>
              <select value={status} onChange={(e) => setStatus(e.target.value)} style={modalStyles.input}>
                {DISPATCH_STATUS.map(s => <option key={s} value={s}>{statusLabel(s)}</option>)}
              </select>
            </label>
            <label style={modalStyles.row}>
              <span style={modalStyles.label}>PROGRESO INICIAL <em style={modalStyles.opt}>(0–100)</em></span>
              <input
                type="number"
                min={0} max={100} step={1}
                value={progress}
                onChange={(e) => setProgress(e.target.value)}
                style={modalStyles.input}
              />
            </label>
          </div>

          {/* Priority preview chip */}
          <div style={dispatchStyles.summary}>
            <span style={dispatchStyles.summaryLabel}>ÓRDENES</span>
            <span style={dispatchStyles.summaryText}>
              Enviar a <b>{agent.name}</b> a <b>{(roomObj || { title: "—" }).title}</b> con tarea <b>&ldquo;{title.trim() || "—"}&rdquo;</b>
            </span>
            <span style={{ ...dispatchStyles.priorityChip, ...dispatchStyles["prio_" + priority] }}>
              {priorityLabel(priority).toUpperCase()}
            </span>
          </div>

          {error && <div style={modalStyles.error}>{error}</div>}

          <div style={modalStyles.foot}>
            <button type="button" onClick={onClose} style={modalStyles.btnGhost}>CANCELAR</button>
            <button type="submit" style={modalStyles.btnPrimary}>🚀 DESPLEGAR</button>
          </div>
        </form>
      </div>
    </div>
  );
}

const dispatchStyles = {
  context: {
    display: "flex", alignItems: "center", gap: 12,
    padding: "10px 12px", marginBottom: 12,
    background: "rgba(79, 216, 255, 0.05)",
    border: "1px solid rgba(79, 216, 255, 0.18)",
    borderRadius: 10,
  },
  contextAvatar: {
    position: "relative", width: 40, height: 40, flexShrink: 0,
    display: "flex", alignItems: "center", justifyContent: "center",
  },
  contextName: { fontSize: 13.5, fontWeight: 700, color: "#EAF6FF", letterSpacing: 0.2 },
  contextMeta: { fontSize: 10.5, color: "#7A95B4", marginTop: 2 },
  changeHint: {
    color: "#FFD170", fontStyle: "normal",
    textTransform: "none", letterSpacing: 0, fontWeight: 600,
  },
  summary: {
    display: "flex", alignItems: "center", gap: 8,
    padding: "9px 12px",
    background: "rgba(5, 14, 26, 0.5)",
    border: "1px dashed rgba(79, 216, 255, 0.25)",
    borderRadius: 9,
    fontSize: 11.5, color: "rgba(234, 246, 255, 0.8)",
  },
  summaryLabel: {
    fontFamily: "'JetBrains Mono', monospace",
    fontSize: 9.5, letterSpacing: 1.6, color: "#7EE8FF",
    fontWeight: 700,
    flexShrink: 0,
  },
  summaryText: { flex: 1, minWidth: 0, lineHeight: 1.4 },
  priorityChip: {
    fontFamily: "'JetBrains Mono', monospace",
    fontSize: 9.5, fontWeight: 800, letterSpacing: 0.8,
    padding: "3px 7px", borderRadius: 4,
    border: "1px solid", flexShrink: 0,
  },
  prio_low:      { color: "#A8BDD6", borderColor: "rgba(168,189,214,0.4)", background: "rgba(168,189,214,0.08)" },
  prio_medium:   { color: "#7EE8FF", borderColor: "rgba(126,232,255,0.4)", background: "rgba(126,232,255,0.08)" },
  prio_high:     { color: "#FFD170", borderColor: "rgba(255,209,112,0.45)", background: "rgba(255,209,112,0.1)" },
  prio_critical: { color: "#FF8A7A", borderColor: "rgba(255,138,122,0.5)", background: "rgba(255,138,122,0.12)" },

  opPanel: {
    padding: "11px 13px 12px", marginBottom: 12,
    background: "linear-gradient(180deg, rgba(126,232,255,0.07), rgba(126,232,255,0.02))",
    border: "1px solid rgba(126,232,255,0.28)",
    borderRadius: 10,
  },
  opHeader: { display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 4 },
  opEyebrow: {
    fontFamily: "'JetBrains Mono', monospace",
    fontSize: 9.5, letterSpacing: 1.6, color: "#7EE8FF", fontWeight: 700,
  },
  opMockTag: {
    fontFamily: "'JetBrains Mono', monospace",
    fontSize: 9, letterSpacing: 1.2, color: "#A8BDD6",
    padding: "2px 6px", borderRadius: 4,
    border: "1px solid rgba(168,189,214,0.3)",
    background: "rgba(168,189,214,0.06)",
  },
  // Cards-grid menu — each operation reads as its own tile so the modal feels
  // like an operations menu instead of a wall of text.
  opGrid: {
    display: "grid", gap: 8, marginTop: 6,
    gridTemplateColumns: "repeat(auto-fit, minmax(240px, 1fr))",
  },
  opCard: {
    display: "flex", flexDirection: "column",
    padding: "10px 12px 11px",
    background: "rgba(10, 20, 36, 0.55)",
    border: "1px solid rgba(126, 232, 255, 0.22)",
    borderRadius: 9,
    minWidth: 0,
  },
  opCardCrumb: {
    fontFamily: "'JetBrains Mono', monospace",
    fontSize: 9, letterSpacing: 1.4, color: "#7EE8FF",
    fontWeight: 700, marginBottom: 4, textTransform: "none",
  },
  opCardTitle: { fontSize: 13.5, fontWeight: 700, color: "#EAF6FF", letterSpacing: 0.2, marginBottom: 5 },
  opCardDesc:  { fontSize: 11, color: "rgba(234,246,255,0.7)", lineHeight: 1.45, marginBottom: 10, flex: 1 },
  opCardBtn: {
    alignSelf: "flex-start",
    padding: "7px 12px",
    background: "linear-gradient(90deg, #4FD8FF, #7EE8FF)",
    border: "none", borderRadius: 6, cursor: "pointer",
    color: "#0a1a2e",
    fontFamily: "Inter, sans-serif", fontSize: 10.5, fontWeight: 800, letterSpacing: 0.8,
    boxShadow: "0 0 12px rgba(79, 216, 255, 0.3)",
  },

  // Optional "Idea base" input that lives inside the Crear pieza social card.
  // Stays compact so the card still reads as a tile, not a full form.
  opIdeaRow: {
    display: "flex", flexDirection: "column", gap: 4,
    marginBottom: 9,
  },
  opIdeaLabel: {
    fontFamily: "'JetBrains Mono', monospace",
    fontSize: 9, letterSpacing: 1.4, color: "#7EE8FF",
    fontWeight: 700,
  },
  opIdeaHint: {
    color: "#7A95B4", fontStyle: "normal", fontWeight: 500, letterSpacing: 1,
  },
  opIdeaInput: {
    width: "100%", boxSizing: "border-box",
    padding: "7px 9px",
    background: "rgba(5, 14, 26, 0.55)",
    border: "1px solid rgba(126, 232, 255, 0.22)",
    borderRadius: 7,
    color: "#EAF6FF",
    fontFamily: "Inter, sans-serif", fontSize: 11, lineHeight: 1.4,
    resize: "vertical", outline: "none",
  },

  // Discreet "DESPLIEGUE MANUAL" divider that sits between the ops menu and
  // the manual form so the form reads as a fallback, not a co-equal block.
  manualSeparator: {
    display: "flex", alignItems: "center", gap: 8,
    margin: "8px 0 10px",
  },
  manualSeparatorLine: {
    flex: 1, height: 1, background: "rgba(126, 232, 255, 0.18)",
  },
  manualSeparatorLabel: {
    fontFamily: "'JetBrains Mono', monospace",
    fontSize: 9.5, letterSpacing: 1.6, color: "#7A95B4",
    fontWeight: 700, padding: "0 4px",
  },
};

// ---------------------------------------------------------------
// SOCIAL-PIECE OP RESULT PANEL
// Renders the demo operation's structured output (JARVIS / ATLAS / KAREN /
// Resultado) below the BottomPanel. Subscribes to SOCIAL_OP_EVENT so it
// updates progressively as each phase completes. Hidden until the operation
// has been triggered at least once. Re-running replaces the contents.
// Pure read-only — never mutates MissionState.
// ---------------------------------------------------------------
function SocialPieceOpPanel() {
  const [snap, setSnap] = useState(() => window.__socialPieceOpResult || null);
  useEffect(() => {
    const onUpdate = (e) => setSnap(e && e.detail !== undefined ? e.detail : (window.__socialPieceOpResult || null));
    window.addEventListener(SOCIAL_OP_EVENT, onUpdate);
    return () => window.removeEventListener(SOCIAL_OP_EVENT, onUpdate);
  }, []);
  if (!snap) return null;

  const stateChip = snap.state === "done"
    ? { label: "COMPLETADA", style: opPanelStyles.chipDone }
    : { label: "EN CURSO",   style: opPanelStyles.chipRunning };

  const isSignals = snap.opKey === OP_KEY_SIGNALS_REVIEW;
  const isSocial  = !isSignals;   // backwards-compatible default for older snapshots

  // Chip de fuente — solo aplica a la op social (la única que produce snap.source).
  // Se calcula aquí para mantener el render del header limpio. Si no hay
  // snap.source (snapshot viejo, op de señales), cae en "LOCAL".
  const sourceChip = (() => {
    if (!isSocial) return null;
    switch (snap.source) {
      case "ai":             return { label: "FUENTE · AI TEST",        style: opPanelStyles.sourceChipAi };
      case "mock_endpoint":  return { label: "FUENTE · MOCK ENDPOINT",  style: opPanelStyles.sourceChipMock };
      case "local_fallback": return { label: "FUENTE · FALLBACK LOCAL", style: opPanelStyles.sourceChipFallback };
      default:               return { label: "FUENTE · LOCAL",          style: opPanelStyles.sourceChipFallback };
    }
  })();

  const headerEyebrow = isSignals
    ? "OPERACIÓN · REVISAR SEÑALES SOCIALES"
    : "OPERACIÓN · CREAR PIEZA SOCIAL";
  const ariaLabel = isSignals
    ? "Resultado operación revisar señales sociales"
    : "Resultado operación crear pieza social";

  // Per-op section configuration. JARVIS + RESULTADO are shared; ATLAS only
  // appears for social-piece, BANDEJA only for signals-review.
  const sections = [
    { label: "JARVIS", accent: "#7EE8FF", items: snap.jarvis || [], placeholder: "A la espera…" },
  ];
  if (isSocial) {
    sections.push({ label: "ATLAS", accent: "#A1F0C4", items: snap.atlas || [],
      placeholder: "ATLAS aún no entrega contenido…" });
    sections.push({ label: "KAREN", accent: "#D1A5FF", items: snap.karen || [],
      placeholder: "KAREN aún no entrega revisión…" });
  } else {
    sections.push({ label: "KAREN", accent: "#D1A5FF", items: snap.karen || [],
      placeholder: "KAREN aún no analiza Bandeja…" });
    sections.push({ label: "BANDEJA", accent: "#FFB3D6", items: snap.bandeja || [],
      placeholder: "Resumen de señales pendiente…" });
  }
  sections.push({ label: "RESULTADO", accent: "#FFD170",
    items: snap.result ? [snap.result] : [],
    placeholder: "Pendiente — esperando revisión completa…" });

  return (
    <div style={opPanelStyles.wrap} role="region" aria-label={ariaLabel}>
      <div style={opPanelStyles.head}>
        <div style={{ minWidth: 0 }}>
          <div style={opPanelStyles.eyebrow}>{headerEyebrow}</div>
          <div style={opPanelStyles.title}>
            Estado: <span style={opPanelStyles.titleAccent}>{snap.status || "—"}</span>
          </div>
        </div>
        <div style={opPanelStyles.headRight}>
          <span style={{ ...opPanelStyles.chipBase, ...stateChip.style }}>{stateChip.label}</span>
          <span style={opPanelStyles.mockChip}>LOCAL · DEMO</span>
          {sourceChip && (
            <span
              style={{ ...opPanelStyles.chipBase, ...sourceChip.style }}
              title="Origen del contenido de ATLAS"
              aria-label={sourceChip.label}
            >
              {sourceChip.label}
            </span>
          )}
          <button
            type="button"
            onClick={dismissSocialOpResult}
            style={opPanelStyles.closeBtn}
            aria-label="Cerrar panel de operación"
          >×</button>
        </div>
      </div>

      {/* Idea base banner — only the social-piece op carries a custom idea, and
          only when Danny actually typed one. Reads as "this run was driven by
          your input" so the demo strings don't look disconnected. */}
      {isSocial && snap.ideaBase && (
        <div style={opPanelStyles.ideaBanner}>
          <span style={opPanelStyles.ideaBannerLabel}>IDEA BASE · DANNY</span>
          <span style={opPanelStyles.ideaBannerText}>{snap.ideaBase}</span>
        </div>
      )}

      <div style={opPanelStyles.grid}>
        {sections.map(s => (
          <SocialOpSection key={s.label} label={s.label} accent={s.accent}
            items={s.items} placeholder={s.placeholder} />
        ))}
      </div>

      {/* Save-to-Content action — only the social-piece op produces a Content
          draft. Signals-review has no draft to save. Idempotent: once saved
          for a given run, the button locks until the operation is re-run. */}
      {isSocial && snap.state === "done" && (
        <div style={opPanelStyles.actionsRow}>
          {snap.draftId ? (
            <button
              type="button"
              disabled
              style={{ ...opPanelStyles.saveBtn, ...opPanelStyles.saveBtnSaved }}
              aria-label="Draft ya guardado en CONTENIDO"
            >
              ✓ GUARDADO EN CONTENIDO
            </button>
          ) : (
            <button
              type="button"
              style={opPanelStyles.saveBtn}
              onClick={() => saveSocialOpAsContentDraft()}
              aria-label="Guardar resultado en CONTENIDO como draft"
            >
              GUARDAR EN CONTENIDO
            </button>
          )}
          <span style={opPanelStyles.actionsHint}>
            Crea un draft local en CONTENIDO · Drafts recientes. Sin API externa.
          </span>
        </div>
      )}

      {/* Open-Inbox action — signals-review only. Pure navigation: hands off
          to the global tab switcher (window.MissionControlNav). Does NOT
          modify, read or send any messages. */}
      {isSignals && snap.state === "done" && (
        <div style={opPanelStyles.actionsRow}>
          <button
            type="button"
            style={opPanelStyles.saveBtn}
            onClick={() => {
              const nav = window.MissionControlNav;
              if (nav && typeof nav.goTo === "function") nav.goTo("inbox");
            }}
            aria-label="Abrir Bandeja Operativa"
          >
            ▶ ABRIR BANDEJA
          </button>
          <span style={opPanelStyles.actionsHint}>
            Solo navega a Bandeja para revisar señales. No modifica mensajes.
          </span>
        </div>
      )}
    </div>
  );
}

function SocialOpSection({ label, accent, items, placeholder }) {
  const empty = !items || items.length === 0;
  return (
    <div style={opPanelStyles.section}>
      <div style={{ ...opPanelStyles.sectionHead, color: accent, borderColor: accent + "55" }}>{label}</div>
      {empty ? (
        <div style={opPanelStyles.placeholder}>{placeholder}</div>
      ) : (
        <ul style={opPanelStyles.list}>
          {items.map((it, i) => (
            <li key={i} style={opPanelStyles.listItem}>
              <span style={{ ...opPanelStyles.bullet, background: accent }} />
              <span style={opPanelStyles.listText}>{it}</span>
            </li>
          ))}
        </ul>
      )}
    </div>
  );
}

const opPanelStyles = {
  wrap: {
    margin: "10px 10px 0",
    padding: "11px 13px 12px",
    background: "linear-gradient(180deg, rgba(15, 28, 48, 0.65), rgba(10, 20, 36, 0.7))",
    border: "1px solid rgba(126, 232, 255, 0.28)",
    borderRadius: 14,
    minWidth: 0, maxWidth: "calc(100% - 20px)",
    boxShadow: "0 0 22px rgba(79, 216, 255, 0.08)",
  },
  head: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: 10, marginBottom: 10 },
  eyebrow: {
    fontFamily: "'JetBrains Mono', monospace",
    fontSize: 9.5, letterSpacing: 1.6, color: "#7EE8FF", fontWeight: 700,
    marginBottom: 3,
  },
  title: { fontSize: 13.5, color: "rgba(234,246,255,0.9)", fontWeight: 600 },
  titleAccent: { color: "#EAF6FF", fontWeight: 800 },
  headRight: { display: "flex", alignItems: "center", gap: 8, flexShrink: 0 },
  chipBase: {
    fontFamily: "'JetBrains Mono', monospace",
    fontSize: 9, letterSpacing: 1.2, fontWeight: 800,
    padding: "3px 7px", borderRadius: 4, border: "1px solid",
  },
  chipRunning: { color: "#FFD170", borderColor: "rgba(255,209,112,0.45)", background: "rgba(255,209,112,0.1)" },
  chipDone:    { color: "#A1F0C4", borderColor: "rgba(161,240,196,0.45)", background: "rgba(161,240,196,0.1)" },
  // Source chips — discreto, mantiene la grilla mono del header.
  // mock_endpoint  → cian suave (mismo origen que el resto del HUD)
  // local_fallback → gris-azul neutro (estilo "off")
  // ai             → magenta para que se note inmediatamente cuándo hay IA real activa
  sourceChipMock:     { color: "#7EE8FF", borderColor: "rgba(126,232,255,0.45)", background: "rgba(126,232,255,0.08)" },
  sourceChipFallback: { color: "#A8BDD6", borderColor: "rgba(168,189,214,0.4)",  background: "rgba(168,189,214,0.06)" },
  sourceChipAi:       { color: "#FF9DD8", borderColor: "rgba(255,157,216,0.5)",  background: "rgba(255,157,216,0.12)" },
  mockChip: {
    fontFamily: "'JetBrains Mono', monospace",
    fontSize: 9, letterSpacing: 1.2, color: "#A8BDD6", fontWeight: 700,
    padding: "3px 7px", borderRadius: 4,
    border: "1px solid rgba(168,189,214,0.3)", background: "rgba(168,189,214,0.06)",
  },
  closeBtn: {
    width: 24, height: 24, borderRadius: 6,
    background: "rgba(255,255,255,0.04)", border: "1px solid rgba(255,255,255,0.12)",
    color: "rgba(234,246,255,0.7)", cursor: "pointer",
    fontSize: 14, lineHeight: 1, fontWeight: 700,
  },
  grid: {
    display: "grid", gap: 8,
    gridTemplateColumns: "repeat(auto-fit, minmax(220px, 1fr))",
  },
  ideaBanner: {
    display: "flex", alignItems: "baseline", gap: 8, flexWrap: "wrap",
    padding: "7px 11px",
    marginBottom: 9,
    background: "rgba(126, 232, 255, 0.06)",
    border: "1px dashed rgba(126, 232, 255, 0.3)",
    borderRadius: 8,
  },
  ideaBannerLabel: {
    fontFamily: "'JetBrains Mono', monospace",
    fontSize: 9, letterSpacing: 1.6, color: "#7EE8FF",
    fontWeight: 800, flexShrink: 0,
  },
  ideaBannerText: {
    fontSize: 11.5, color: "#EAF6FF",
    lineHeight: 1.4,
  },
  section: {
    background: "rgba(10, 20, 36, 0.55)",
    border: "1px solid rgba(79, 216, 255, 0.12)",
    borderRadius: 10, padding: "9px 11px 10px",
    minWidth: 0,
  },
  sectionHead: {
    fontFamily: "'JetBrains Mono', monospace",
    fontSize: 9.5, letterSpacing: 1.6, fontWeight: 800,
    paddingBottom: 6, marginBottom: 7,
    borderBottom: "1px solid",
  },
  placeholder: {
    fontSize: 10.5, color: "rgba(168,189,214,0.55)",
    fontStyle: "italic", lineHeight: 1.4,
  },
  list: { listStyle: "none", margin: 0, padding: 0, display: "flex", flexDirection: "column", gap: 5 },
  listItem: { display: "flex", alignItems: "flex-start", gap: 7, lineHeight: 1.4 },
  bullet: { width: 5, height: 5, borderRadius: "50%", marginTop: 6, flexShrink: 0,
    boxShadow: "0 0 6px currentColor" },
  listText: { fontSize: 11, color: "rgba(234,246,255,0.85)" },

  actionsRow: {
    display: "flex", alignItems: "center", gap: 10, flexWrap: "wrap",
    marginTop: 10, paddingTop: 9,
    borderTop: "1px dashed rgba(126,232,255,0.2)",
  },
  saveBtn: {
    padding: "8px 14px",
    background: "linear-gradient(90deg, #4FD8FF, #7EE8FF)",
    border: "none", borderRadius: 7, cursor: "pointer",
    color: "#0a1a2e",
    fontFamily: "Inter, sans-serif", fontSize: 11, fontWeight: 800, letterSpacing: 0.8,
    boxShadow: "0 0 14px rgba(79, 216, 255, 0.35)",
  },
  saveBtnSaved: {
    background: "rgba(161,240,196,0.12)",
    color: "#A1F0C4",
    border: "1px solid rgba(161,240,196,0.4)",
    boxShadow: "none",
    cursor: "not-allowed",
  },
  actionsHint: {
    fontSize: 10.5, color: "rgba(168,189,214,0.7)",
    fontStyle: "italic", flex: 1, minWidth: 160,
  },
};

// ---------------------------------------------------------------
// EDIT AGENT MODAL
// Pre-loads the target agent's current fields and applies changes via
// MissionState. Id never changes (update-in-place).
// JARVIS: name / role / avatar locked; status / task / room editable.
// ---------------------------------------------------------------
function EditAgentModal({ agent, onClose, onSaved }) {
  const isCoreAgent = agent.id === "jarvis";

  // Preload current values. If the current value is missing from the option
  // lists we still include it so it's not lost.
  const roleOptions    = ROLE_OPTIONS.includes(agent.role) ? ROLE_OPTIONS : [agent.role, ...ROLE_OPTIONS];
  const variantOptions = VARIANT_OPTIONS.includes(agent.color) ? VARIANT_OPTIONS : ["auto", agent.color, ...VARIANT_OPTIONS.filter(v => v !== "auto")];

  const [name, setName]       = useState(agent.name);
  const [role, setRole]       = useState(agent.role);
  const [status, setStatus]   = useState(agent.status);
  const [room, setRoom]       = useState(agent.room);
  const [variant, setVariant] = useState(agent.color);
  const [task, setTask]       = useState(agent.task || "");
  const [error, setError]     = useState("");
  const nameRef = useRef(null);

  useEffect(() => {
    if (!isCoreAgent && nameRef.current) nameRef.current.focus();
    const onKey = (e) => { if (e.key === "Escape") onClose(); };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [onClose, isCoreAgent]);

  const submit = (e) => {
    e.preventDefault();
    const ms = window.MissionState;
    if (!ms) { setError("MissionState no disponible."); return; }

    const trimmed = name.trim();
    if (!trimmed)                   { setError("El nombre es obligatorio."); return; }
    if (!ms.getRoomById(room))      { setError("Sala inválida.");            return; }
    if (!STATUS_STYLES[status])     { setError("Estado inválido.");          return; }

    // Protected core fields — never mutate these for JARVIS.
    const nextName    = isCoreAgent ? agent.name    : trimmed;
    const nextRole    = isCoreAgent ? agent.role    : role;
    const nextVariant = isCoreAgent ? agent.color   : variant;
    const nextActivity = statusToActivity(status);
    const palette      = window.ROBOT_PALETTES && window.ROBOT_PALETTES[nextVariant];
    const nextAccent   = palette ? palette.glow : agent.accent || "#4FD8FF";

    // ---- Compute deltas so we only log what actually changed ----
    const deltas = [];
    if (nextName     !== agent.name)   deltas.push(["name",    agent.name, nextName]);
    if (nextRole     !== agent.role)   deltas.push(["role",    agent.role, nextRole]);
    if (status       !== agent.status) deltas.push(["status",  agent.status, status]);
    if (nextVariant  !== agent.color)  deltas.push(["variant", agent.color, nextVariant]);
    const prevTask = agent.task || "";
    const nextTask = task.trim();
    if (nextTask !== prevTask)         deltas.push(["task",    prevTask, nextTask]);
    const roomChanged = room !== agent.room;

    // ---- Apply changes ----
    // 1. Field patch FIRST so that when moveAgentToRoom captures `finalStatus`
    //    below it reads the already-updated status (avoids Phase-2 reverting it).
    //    id NEVER changes.
    const patch = {
      name:          nextName,
      role:          nextRole,
      status:        status,
      activity:      nextActivity,
      avatarVariant: nextVariant,
      accent:        nextAccent,
      task:          nextTask || ("En espera en " + ms.getRoomById(room).title),
    };
    if (nextTask === "" && prevTask !== "") {
      patch.progress = 0;
    }
    ms.updateAgent(agent.id, patch);

    // 2. Room change (delegates to the existing movement pipeline).
    //    Pass finalStatus/activity explicitly so the brief "Moving" flash
    //    settles back to the new status, not the old one.
    if (roomChanged) {
      ms.moveAgentToRoom(agent.id, room, {
        transitionMs:   0,
        autoLog:        true,
        finalStatus:    status,
        finalActivity:  nextActivity,
      });
    }

    // 3. Task table sync.
    if (nextTask && nextTask !== prevTask) {
      ms.assignTaskToAgent(agent.id, { title: nextTask, status: "running", priority: "medium" });
    }

    // ---- Logs ----
    const label = (nextName || agent.name).toUpperCase();
    if (roomChanged) {
      const prevRoomTitle = (ms.getRoomById(agent.room) || { title: agent.room || "—" }).title;
      const nextRoomTitle = ms.getRoomById(room).title;
      ms.addLog({
        type: "OPS", source: "ROSTER", severity: "low",
        message: label + " movido de " + prevRoomTitle + " a " + nextRoomTitle + ".",
      });
    }
    if (deltas.find(d => d[0] === "status")) {
      ms.addLog({
        type: "OPS", source: "ROSTER", severity: "low",
        message: label + " estado cambiado a " + statusLabel(status) + ".",
      });
    }
    if (deltas.find(d => d[0] === "task")) {
      ms.addLog({
        type: "OPS", source: "ROSTER", severity: "low",
        message: nextTask ? (label + " tarea actualizada.") : (label + " tarea limpiada."),
      });
    }
    if (deltas.length > 0 || roomChanged) {
      // Summary log (only if at least one field actually changed).
      const totalChanges = deltas.length + (roomChanged ? 1 : 0);
      ms.addLog({
        type: "OPS", source: "ROSTER", severity: "low",
        message: "Agente " + label + " actualizado (" + totalChanges + (totalChanges === 1 ? " cambio" : " cambios") + ").",
      });
    }

    onSaved(agent.id);
  };

  const previewVariant = isCoreAgent ? agent.color : variant;
  const previewPal     = window.ROBOT_PALETTES && window.ROBOT_PALETTES[previewVariant];
  const previewGlowHex = previewPal ? previewPal.glow : "#4FD8FF";
  const currentRoomObj = window.MissionState && window.MissionState.getRoomById(room);

  return (
    <div style={modalStyles.backdrop} onMouseDown={onClose} role="dialog" aria-modal="true" aria-label={`Editar ${agent.name}`}>
      <div style={modalStyles.card} onMouseDown={(e) => e.stopPropagation()}>
        <div style={modalStyles.head}>
          <div>
            <div style={modalStyles.eyebrow}>ROSTER · EDITAR AGENTE</div>
            <div style={modalStyles.title}>Editar a {agent.name}</div>
          </div>
          <button type="button" onClick={onClose} style={modalStyles.closeBtn} aria-label="Cerrar">×</button>
        </div>

        <form onSubmit={submit} style={modalStyles.form}>
          <label style={modalStyles.row}>
            <span style={modalStyles.label}>
              NOMBRE <em style={modalStyles.req}>*</em>
              {isCoreAgent && <em style={editStyles.lockNote}>  bloqueado (agente core)</em>}
            </span>
            <input
              ref={nameRef}
              type="text"
              value={name}
              disabled={isCoreAgent}
              onChange={(e) => { setName(e.target.value); setError(""); }}
              style={{ ...modalStyles.input, ...(isCoreAgent ? editStyles.lockedInput : {}) }}
              maxLength={32}
              autoComplete="off"
            />
          </label>

          <div style={modalStyles.grid2}>
            <label style={modalStyles.row}>
              <span style={modalStyles.label}>
                ROL
                {isCoreAgent && <em style={editStyles.lockNote}>  bloqueado</em>}
              </span>
              <select
                value={role}
                disabled={isCoreAgent}
                onChange={(e) => setRole(e.target.value)}
                style={{ ...modalStyles.input, ...(isCoreAgent ? editStyles.lockedInput : {}) }}
              >
                {roleOptions.map(r => <option key={r} value={r}>{roleLabel(r)}</option>)}
              </select>
            </label>
            <label style={modalStyles.row}>
              <span style={modalStyles.label}>ESTADO</span>
              <select value={status} onChange={(e) => setStatus(e.target.value)} style={modalStyles.input}>
                {STATUS_OPTIONS.map(s => <option key={s} value={s}>{statusLabel(s)}</option>)}
              </select>
            </label>
          </div>

          <div style={modalStyles.grid2}>
            <label style={modalStyles.row}>
              <span style={modalStyles.label}>SALA</span>
              <select value={room} onChange={(e) => setRoom(e.target.value)} style={modalStyles.input}>
                {deriveRooms().map(r => <option key={r.id} value={r.id}>{r.name}</option>)}
              </select>
            </label>
            <label style={modalStyles.row}>
              <span style={modalStyles.label}>
                COLOR DEL AVATAR
                {isCoreAgent && <em style={editStyles.lockNote}>  bloqueado</em>}
              </span>
              <select
                value={variant}
                disabled={isCoreAgent}
                onChange={(e) => setVariant(e.target.value)}
                style={{ ...modalStyles.input, ...(isCoreAgent ? editStyles.lockedInput : {}) }}
              >
                {variantOptions.map(v => (
                  <option key={v} value={v}>{v === "auto" ? "automático (derivar del rol)" : v}</option>
                ))}
              </select>
            </label>
          </div>

          <label style={modalStyles.row}>
            <span style={modalStyles.label}>
              TAREA ACTUAL <em style={modalStyles.opt}>(vaciar para limpiar)</em>
            </span>
            <input
              type="text"
              value={task}
              onChange={(e) => setTask(e.target.value)}
              placeholder="Dejar vacío para limpiar la tarea actual"
              style={modalStyles.input}
              maxLength={80}
              autoComplete="off"
            />
          </label>

          {/* Live preview */}
          <div style={modalStyles.preview}>
            <div style={modalStyles.previewAvatarWrap}>
              <div style={{
                ...modalStyles.previewGlow,
                background: `radial-gradient(circle, ${previewGlowHex}44, transparent 70%)`,
              }} />
              <RobotAvatar color={previewVariant} size={44} />
            </div>
            <div>
              <div style={modalStyles.previewName}>{(name && name.trim()) || agent.name}</div>
              <div style={modalStyles.previewMeta}>
                {roleLabel(role)} · {statusLabel(status)} · {(currentRoomObj || { title: "—" }).title}
              </div>
            </div>
          </div>

          {error && <div style={modalStyles.error}>{error}</div>}

          <div style={modalStyles.foot}>
            <button type="button" onClick={onClose} style={modalStyles.btnGhost}>CANCELAR</button>
            <button type="submit" style={modalStyles.btnPrimary}>✓ GUARDAR CAMBIOS</button>
          </div>
        </form>
      </div>
    </div>
  );
}

const editStyles = {
  lockNote: {
    color: "rgba(255, 255, 255, 0.28)",
    fontStyle: "normal",
    textTransform: "none",
    letterSpacing: 0,
    fontWeight: 600,
  },
  lockedInput: {
    opacity: 0.55,
    cursor: "not-allowed",
    background: "rgba(10, 20, 36, 0.4)",
  },
};

// ---------------------------------------------------------------
// ADD ROOM MODAL — create a dynamic (non-core) room.
// ---------------------------------------------------------------
const ROOM_TYPE_OPTIONS = [
  { key: "command",   label: "Comando" },
  { key: "planning",  label: "Planificación" },
  { key: "research",  label: "Investigación" },
  { key: "creative",  label: "Creativo" },
  { key: "ops",       label: "Operaciones" },
  { key: "data",      label: "Datos" },
  { key: "custom",    label: "Personalizada" },
];
const ROOM_TINT_OPTIONS = [
  { key: "auto",    label: "automático (derivar del tipo)", hex: null },
  { key: "cyan",    label: "cyan",      hex: "#4FD8FF" },
  { key: "teal",    label: "teal",      hex: "#3FE0D4" },
  { key: "green",   label: "verde",     hex: "#4FE08A" },
  { key: "violet",  label: "violeta",   hex: "#B78BFF" },
  { key: "pink",    label: "rosa",      hex: "#FF7ABF" },
  { key: "magenta", label: "magenta",   hex: "#E866E2" },
  { key: "orange",  label: "naranja",   hex: "#FFA55C" },
  { key: "amber",   label: "ámbar",     hex: "#FFD170" },
  { key: "lime",    label: "lima",      hex: "#C5F060" },
];
const TYPE_TO_HEX = {
  command: "#4FD8FF", planning: "#4FE0A8", research: "#B78BFF",
  creative: "#4FE08A", ops: "#FFA55C", data: "#C17AFF", custom: "#7EE8FF",
};

function slugifyRoomId(name) {
  const base = String(name || "").trim().toLowerCase()
    .replace(/[^a-z0-9]+/g, "-")
    .replace(/^-+|-+$/g, "")
    .slice(0, 24);
  return base || ("sala-" + Math.floor(Math.random() * 9000 + 1000));
}

function uniqueRoomId(baseId, existing) {
  if (!existing.some(r => r.id === baseId)) return baseId;
  let i = 2;
  while (existing.some(r => r.id === baseId + "-" + i)) i += 1;
  return baseId + "-" + i;
}

function AddRoomModal({ onClose, onCreated }) {
  const [name, setName]         = useState("");
  const [type, setType]         = useState("custom");
  const [tint, setTint]         = useState("auto");
  const [capacity, setCapacity] = useState(8);
  const [error, setError]       = useState("");
  const nameRef = useRef(null);

  useEffect(() => {
    if (nameRef.current) nameRef.current.focus();
    const onKey = (e) => { if (e.key === "Escape") onClose(); };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [onClose]);

  const resolvedHex = tint === "auto"
    ? (TYPE_TO_HEX[type] || "#7EE8FF")
    : ((ROOM_TINT_OPTIONS.find(o => o.key === tint) || {}).hex || "#7EE8FF");

  const submit = (e) => {
    e.preventDefault();
    const ms = window.MissionState;
    if (!ms) { setError("MissionState no disponible."); return; }

    const trimmed = name.trim();
    if (!trimmed)                              { setError("El nombre es obligatorio.");  return; }
    if (!ROOM_TYPE_OPTIONS.some(o => o.key === type)) { setError("Tipo inválido.");      return; }
    const cap = Math.max(1, Math.min(20, Number(capacity) || 8));

    const existing = ms.getRooms() || [];
    const baseId   = slugifyRoomId(trimmed);
    const id       = uniqueRoomId(baseId, existing);

    const created = ms.addRoom({
      id:       id,
      title:    trimmed,
      type:     type,
      tint:     resolvedHex,
      capacity: cap,
    });
    if (!created) { setError("No se pudo crear la sala."); return; }

    onCreated(created.id);
  };

  return (
    <div style={modalStyles.backdrop} onMouseDown={onClose} role="dialog" aria-modal="true" aria-label="Crear nueva sala">
      <div style={modalStyles.card} onMouseDown={(e) => e.stopPropagation()}>
        <div style={modalStyles.head}>
          <div>
            <div style={modalStyles.eyebrow}>OPS · NUEVA SALA</div>
            <div style={modalStyles.title}>Crear sala</div>
          </div>
          <button type="button" onClick={onClose} style={modalStyles.closeBtn} aria-label="Cerrar">×</button>
        </div>

        <form onSubmit={submit} style={modalStyles.form}>
          <label style={modalStyles.row}>
            <span style={modalStyles.label}>NOMBRE <em style={modalStyles.req}>*</em></span>
            <input
              ref={nameRef}
              type="text"
              value={name}
              onChange={(e) => { setName(e.target.value); setError(""); }}
              placeholder="ej. Sala de Diseño"
              style={modalStyles.input}
              maxLength={32}
              autoComplete="off"
            />
          </label>

          <div style={modalStyles.grid2}>
            <label style={modalStyles.row}>
              <span style={modalStyles.label}>TIPO</span>
              <select value={type} onChange={(e) => setType(e.target.value)} style={modalStyles.input}>
                {ROOM_TYPE_OPTIONS.map(o => <option key={o.key} value={o.key}>{o.label}</option>)}
              </select>
            </label>
            <label style={modalStyles.row}>
              <span style={modalStyles.label}>COLOR</span>
              <select value={tint} onChange={(e) => setTint(e.target.value)} style={modalStyles.input}>
                {ROOM_TINT_OPTIONS.map(o => <option key={o.key} value={o.key}>{o.label}</option>)}
              </select>
            </label>
          </div>

          <label style={modalStyles.row}>
            <span style={modalStyles.label}>CAPACIDAD <em style={modalStyles.opt}>(1–20)</em></span>
            <input
              type="number"
              min={1} max={20} step={1}
              value={capacity}
              onChange={(e) => setCapacity(e.target.value)}
              style={modalStyles.input}
            />
          </label>

          {/* Preview */}
          <div style={modalStyles.preview}>
            <div style={{ width: 40, height: 40, borderRadius: 10,
                          background: resolvedHex + "22", border: `1px solid ${resolvedHex}66`,
                          display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0 }}>
              <span style={{ color: resolvedHex, fontSize: 18, fontWeight: 800 }}>⌘</span>
            </div>
            <div>
              <div style={modalStyles.previewName}>{name.trim() || "NUEVA SALA"}</div>
              <div style={modalStyles.previewMeta}>
                {(ROOM_TYPE_OPTIONS.find(o => o.key === type) || { label: "—" }).label} · capacidad {Math.max(1, Math.min(20, Number(capacity) || 8))}
              </div>
            </div>
          </div>

          {error && <div style={modalStyles.error}>{error}</div>}

          <div style={modalStyles.foot}>
            <button type="button" onClick={onClose} style={modalStyles.btnGhost}>CANCELAR</button>
            <button type="submit" style={modalStyles.btnPrimary}>＋ CREAR SALA</button>
          </div>
        </form>
      </div>
    </div>
  );
}

// ---------------------------------------------------------------
// EDIT ROOM MODAL — modify a custom (non-core) room.
// Preloads current values. Core rooms are protected at 3 layers
// (UI icon hidden, App handler guard, MissionState.updateRoom reject).
// Only title / type / tint / capacity can change — id stays stable
// so agent & task references survive untouched.
// ---------------------------------------------------------------
function EditRoomModal({ roomId, onClose, onSaved }) {
  const ms   = window.MissionState;
  const room = ms ? ms.getRoomById(roomId) : null;

  // Resolve the tint key that best matches the current hex (or "custom").
  const resolveInitialTintKey = (hex) => {
    const match = ROOM_TINT_OPTIONS.find(o => o.hex && o.hex.toLowerCase() === String(hex || "").toLowerCase());
    return match ? match.key : "custom";
  };

  const [name, setName]         = useState(room ? room.title    : "");
  const [type, setType]         = useState(room ? room.type     : "custom");
  const [tintKey, setTintKey]   = useState(room ? resolveInitialTintKey(room.tint) : "auto");
  const [customHex, setCustomHex] = useState(room ? (room.tint || "#7EE8FF") : "#7EE8FF");
  const [capacity, setCapacity] = useState(room ? String(room.capacity || 8) : "8");
  const [error, setError]       = useState("");
  const nameRef = useRef(null);

  useEffect(() => {
    if (nameRef.current) nameRef.current.focus();
    const onKey = (e) => { if (e.key === "Escape") onClose(); };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [onClose]);

  if (!room) return null;

  // Final protection at the UI layer: never let the modal render for a
  // core room even if callers misroute. Match the handler + state guards.
  if (room.isCore === true) return null;

  const HEX_RE = /^#[0-9a-fA-F]{6}$/;
  const resolvedHex = tintKey === "auto"
    ? (TYPE_TO_HEX[type] || "#7EE8FF")
    : tintKey === "custom"
      ? (HEX_RE.test(customHex) ? customHex : "#7EE8FF")
      : ((ROOM_TINT_OPTIONS.find(o => o.key === tintKey) || {}).hex || "#7EE8FF");

  const submit = (e) => {
    e.preventDefault();
    if (!ms) { setError("MissionState no disponible."); return; }

    const trimmed = name.trim();
    if (!trimmed)                                           { setError("El nombre no puede estar vacío."); return; }
    if (trimmed.length > 32)                                { setError("El nombre es demasiado largo (máx. 32)."); return; }
    if (!ROOM_TYPE_OPTIONS.some(o => o.key === type))       { setError("Tipo inválido.");                   return; }
    if (tintKey === "custom" && !HEX_RE.test(customHex))    { setError("Color inválido. Usa formato #RRGGBB.");  return; }

    const capNum = Math.round(Number(capacity));
    if (!Number.isFinite(capNum) || capNum < 1 || capNum > 20) {
      setError("Capacidad inválida (debe ser un entero entre 1 y 20).");
      return;
    }

    const updated = ms.updateRoom(roomId, {
      title:    trimmed,
      type:     type,
      tint:     resolvedHex,
      capacity: capNum,
    });
    if (!updated) {
      setError("No se pudo guardar. Revisa los campos.");
      return;
    }
    onSaved(roomId);
  };

  return (
    <div style={modalStyles.backdrop} onMouseDown={onClose} role="dialog" aria-modal="true" aria-label={`Editar sala ${room.title}`}>
      <div style={modalStyles.card} onMouseDown={(e) => e.stopPropagation()}>
        <div style={modalStyles.head}>
          <div>
            <div style={modalStyles.eyebrow}>OPS · EDITAR SALA</div>
            <div style={modalStyles.title}>Editar {room.title}</div>
          </div>
          <button type="button" onClick={onClose} style={modalStyles.closeBtn} aria-label="Cerrar">×</button>
        </div>

        <form onSubmit={submit} style={modalStyles.form}>
          <label style={modalStyles.row}>
            <span style={modalStyles.label}>NOMBRE <em style={modalStyles.req}>*</em></span>
            <input
              ref={nameRef}
              type="text"
              value={name}
              onChange={(e) => { setName(e.target.value); setError(""); }}
              placeholder="ej. Sala de Diseño"
              style={modalStyles.input}
              maxLength={32}
              autoComplete="off"
            />
          </label>

          <div style={modalStyles.grid2}>
            <label style={modalStyles.row}>
              <span style={modalStyles.label}>TIPO</span>
              <select value={type} onChange={(e) => { setType(e.target.value); setError(""); }} style={modalStyles.input}>
                {ROOM_TYPE_OPTIONS.map(o => <option key={o.key} value={o.key}>{o.label}</option>)}
              </select>
            </label>
            <label style={modalStyles.row}>
              <span style={modalStyles.label}>COLOR</span>
              <select value={tintKey} onChange={(e) => { setTintKey(e.target.value); setError(""); }} style={modalStyles.input}>
                {ROOM_TINT_OPTIONS.map(o => <option key={o.key} value={o.key}>{o.label}</option>)}
                <option value="custom">personalizado (HEX)</option>
              </select>
            </label>
          </div>

          {tintKey === "custom" && (
            <label style={modalStyles.row}>
              <span style={modalStyles.label}>HEX <em style={modalStyles.opt}>(#RRGGBB)</em></span>
              <input
                type="text"
                value={customHex}
                onChange={(e) => { setCustomHex(e.target.value); setError(""); }}
                placeholder="#7EE8FF"
                style={modalStyles.input}
                maxLength={7}
                autoComplete="off"
              />
            </label>
          )}

          <label style={modalStyles.row}>
            <span style={modalStyles.label}>CAPACIDAD <em style={modalStyles.opt}>(1–20)</em></span>
            <input
              type="number"
              min={1} max={20} step={1}
              value={capacity}
              onChange={(e) => { setCapacity(e.target.value); setError(""); }}
              style={modalStyles.input}
            />
          </label>

          {/* Preview */}
          <div style={modalStyles.preview}>
            <div style={{ width: 40, height: 40, borderRadius: 10,
                          background: resolvedHex + "22", border: `1px solid ${resolvedHex}66`,
                          display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0 }}>
              <span style={{ color: resolvedHex, fontSize: 18, fontWeight: 800 }}>⌘</span>
            </div>
            <div>
              <div style={modalStyles.previewName}>{name.trim() || room.title}</div>
              <div style={modalStyles.previewMeta}>
                {(ROOM_TYPE_OPTIONS.find(o => o.key === type) || { label: "—" }).label} · capacidad {capacity || "—"}
              </div>
            </div>
          </div>

          {error && <div style={modalStyles.error} role="alert">{error}</div>}

          <div style={modalStyles.foot}>
            <button type="button" onClick={onClose} style={modalStyles.btnGhost}>CANCELAR</button>
            <button type="submit" style={modalStyles.btnPrimary}>✎ GUARDAR CAMBIOS</button>
          </div>
        </form>
      </div>
    </div>
  );
}

// ---------------------------------------------------------------
// REMOVE ROOM CONFIRM — destructive. Shows downstream impact (agents / tasks).
// ---------------------------------------------------------------
function RemoveRoomConfirm({ roomId, onCancel, onConfirm }) {
  const ms    = window.MissionState;
  const room  = ms ? ms.getRoomById(roomId) : null;
  const agents = ms && room ? (ms.getAgentsInRoom(roomId) || []) : [];
  const tasks  = ms && room ? (ms.getTasks() || []).filter(t => t.room === roomId) : [];
  const fallback = ms ? ms.getRoomById("office") : null;
  const fallbackName = fallback ? fallback.title : "Oficina de JARVIS";

  useEffect(() => {
    const onKey = (e) => { if (e.key === "Escape") onCancel(); };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [onCancel]);

  if (!room) return null;

  return (
    <div style={confirmStyles.backdrop} onMouseDown={onCancel} role="alertdialog" aria-modal="true" aria-label={`Eliminar sala ${room.title}`}>
      <div style={confirmStyles.card} onMouseDown={(e) => e.stopPropagation()}>
        <div style={confirmStyles.eyebrow}>OPS · ELIMINAR SALA</div>
        <div style={confirmStyles.title}>¿Eliminar {room.title}?</div>
        <div style={confirmStyles.body}>
          La sala <strong>{room.title}</strong> será removida del sistema.
          {agents.length > 0 && (
            <>
              <br /><br />
              <strong style={{ color: "#FFB3A6" }}>
                {agents.length} {agents.length === 1 ? "agente" : "agentes"}
              </strong> ({agents.map(a => a.name).join(", ")}) {agents.length === 1 ? "será reasignado" : "serán reasignados"} a <strong>{fallbackName}</strong>.
            </>
          )}
          {tasks.length > 0 && (
            <>
              <br /><br />
              <strong style={{ color: "#FFB3A6" }}>
                {tasks.length} {tasks.length === 1 ? "tarea activa" : "tareas activas"}
              </strong> {tasks.length === 1 ? "será reubicada" : "serán reubicadas"} a <strong>{fallbackName}</strong>. No se cancelan.
            </>
          )}
          {agents.length === 0 && tasks.length === 0 && (
            <> No hay agentes ni tareas en esta sala.</>
          )}
          <br /><br />
          Esta acción no se puede deshacer.
        </div>
        <div style={confirmStyles.foot}>
          <button type="button" onClick={onCancel} style={confirmStyles.btnGhost}>CANCELAR</button>
          <button type="button" onClick={onConfirm} style={confirmStyles.btnDanger}>× ELIMINAR SALA</button>
        </div>
      </div>
    </div>
  );
}

// ---------------------------------------------------------------
// REMOVE AGENT CONFIRM
// Destructive action — distinct coral palette so the operator can
// read the intent at a glance without bloating the UI.
// ---------------------------------------------------------------
function RemoveAgentConfirm({ agent, onCancel, onConfirm }) {
  const ms       = window.MissionState;
  const room     = ms && agent ? ms.getRoomById(agent.room) : null;
  const roomName = room ? room.title : (agent && agent.room) || "—";

  useEffect(() => {
    const onKey = (e) => { if (e.key === "Escape") onCancel(); };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [onCancel]);

  if (!agent) return null;

  return (
    <div style={confirmStyles.backdrop} onMouseDown={onCancel} role="alertdialog" aria-modal="true" aria-label={`Eliminar a ${agent.name}`}>
      <div style={confirmStyles.card} onMouseDown={(e) => e.stopPropagation()}>
        <div style={confirmStyles.eyebrow}>ROSTER · ELIMINAR AGENTE</div>
        <div style={confirmStyles.title}>¿Eliminar a {agent.name}?</div>
        <div style={confirmStyles.body}>
          <strong>{agent.name}</strong> será removido del roster y de{" "}
          <strong style={{ color: "#FFB3A6" }}>{roomName}</strong>. Todas las tareas actualmente
          asignadas a {agent.name} serán limpiadas. Esta acción no se puede deshacer.
        </div>
        <div style={confirmStyles.foot}>
          <button type="button" onClick={onCancel} style={confirmStyles.btnGhost}>CANCELAR</button>
          <button type="button" onClick={onConfirm} style={confirmStyles.btnDanger}>× ELIMINAR AGENTE</button>
        </div>
      </div>
    </div>
  );
}

const confirmStyles = {
  backdrop: {
    position: "absolute", inset: 0, zIndex: 40,
    background: "rgba(2, 8, 16, 0.72)",
    backdropFilter: "blur(6px)",
    WebkitBackdropFilter: "blur(6px)",
    display: "flex", alignItems: "center", justifyContent: "center",
    padding: 20,
  },
  card: {
    width: 420, maxWidth: "100%",
    background: "linear-gradient(180deg, rgba(32, 16, 20, 0.96), rgba(18, 10, 14, 0.96))",
    border: "1px solid rgba(255, 138, 122, 0.45)",
    borderRadius: 14,
    boxShadow: "0 20px 60px rgba(0,0,0,0.6), 0 0 28px rgba(255, 138, 122, 0.15), inset 0 0 40px rgba(255, 138, 122, 0.03)",
    padding: 20,
    color: "#EAF6FF",
    fontFamily: "Inter, sans-serif",
  },
  eyebrow: {
    fontFamily: "'JetBrains Mono', monospace",
    fontSize: 10, letterSpacing: 2, color: "#FFB3A6",
    fontWeight: 700, textTransform: "uppercase",
    marginBottom: 4,
  },
  title: {
    fontSize: 18, fontWeight: 800, color: "#fff",
    letterSpacing: 0.3, marginBottom: 12,
    paddingBottom: 12, borderBottom: "1px solid rgba(255, 138, 122, 0.12)",
  },
  body: {
    fontSize: 12.5, lineHeight: 1.55,
    color: "rgba(234, 246, 255, 0.75)",
    marginBottom: 16,
  },
  foot: {
    display: "flex", gap: 8, justifyContent: "flex-end",
    paddingTop: 12,
    borderTop: "1px solid rgba(255, 138, 122, 0.1)",
  },
  btnGhost: {
    padding: "8px 14px",
    background: "transparent",
    border: "1px solid rgba(255,255,255,0.12)",
    borderRadius: 7, cursor: "pointer",
    color: "rgba(255,255,255,0.6)",
    fontFamily: "'JetBrains Mono', monospace", fontSize: 11, fontWeight: 700, letterSpacing: 1,
  },
  btnDanger: {
    padding: "8px 16px",
    background: "linear-gradient(90deg, #FF6B6B, #FF8A7A)",
    border: "none", borderRadius: 7, cursor: "pointer",
    color: "#2b0810",
    fontFamily: "Inter, sans-serif", fontSize: 11.5, fontWeight: 800, letterSpacing: 0.8,
    boxShadow: "0 0 16px rgba(255, 107, 107, 0.35)",
  },
};

// ---------------------------------------------------------------
// QUEUE + HISTORY MODAL
// Lightweight panel showing the queued + terminal tasks for a single
// agent. Reuses modalStyles for look parity with the rest of the UI.
// Opens in either "queue" or "history" initial focus; both lists are
// always visible so the operator can cross-reference without tabs.
// ---------------------------------------------------------------
function formatTimestamp(iso) {
  if (!iso) return "—";
  const d = new Date(iso);
  if (isNaN(d.getTime())) return "—";
  const pad = (n) => String(n).padStart(2, "0");
  return pad(d.getHours()) + ":" + pad(d.getMinutes()) + " · " +
         pad(d.getDate()) + "/" + pad(d.getMonth() + 1);
}

function QueueHistoryModal({ agent, initialFocus, onClose }) {
  useMissionTick(); // keep lists live while open
  const ms = window.MissionState;

  useEffect(() => {
    const onKey = (e) => { if (e.key === "Escape") onClose(); };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [onClose]);

  if (!ms || !agent) return null;

  const queue    = ms.getQueuedTasksForAgent ? ms.getQueuedTasksForAgent(agent.id) : [];
  const history  = ms.getHistoryForAgent     ? ms.getHistoryForAgent(agent.id)     : [];
  const active   = ms.getActiveTaskForAgent  ? ms.getActiveTaskForAgent(agent.id)  : null;
  const hasActive = !!active;

  const onActivate = (taskId) => {
    if (hasActive) return; // guard also in state, but avoid noise
    ms.activateQueuedTask(taskId);
  };
  const onRemoveQueued = (taskId) => {
    ms.removeQueuedTask(taskId);
  };

  return (
    <div style={modalStyles.backdrop} onMouseDown={onClose} role="dialog" aria-modal="true" aria-label={`Cola e historial de ${agent.name}`}>
      <div style={{ ...modalStyles.card, width: 520 }} onMouseDown={(e) => e.stopPropagation()}>
        <div style={modalStyles.head}>
          <div>
            <div style={modalStyles.eyebrow}>OPS · COLA E HISTORIAL</div>
            <div style={modalStyles.title}>{agent.name}</div>
          </div>
          <button type="button" onClick={onClose} style={modalStyles.closeBtn} aria-label="Cerrar">×</button>
        </div>

        <div style={qhStyles.sections}>
          {/* ---- QUEUE ---- */}
          <section>
            <div style={qhStyles.sectionHead}>
              <span style={qhStyles.sectionLabel}>◷ EN COLA</span>
              <span style={qhStyles.sectionCount}>{queue.length}</span>
            </div>
            {queue.length === 0 ? (
              <div style={qhStyles.empty}>No hay tareas en cola.</div>
            ) : (
              <ul style={qhStyles.list}>
                {queue.map(t => (
                  <li key={t.id} style={qhStyles.row}>
                    <div style={qhStyles.rowMain}>
                      <div style={qhStyles.rowTitle}>{t.title || t.id}</div>
                      <div style={qhStyles.rowMeta}>
                        <span style={qhStyles.prioTag}>{priorityLabel(t.priority || "medium").toUpperCase()}</span>
                        <span>encolada {formatTimestamp(t.queuedAt)}</span>
                      </div>
                    </div>
                    <div style={qhStyles.rowActions}>
                      <button
                        type="button"
                        className="qh-action qh-activate"
                        disabled={hasActive}
                        title={hasActive ? "El agente ya tiene una tarea activa" : "Activar ahora"}
                        onClick={() => onActivate(t.id)}
                      >▶</button>
                      <button
                        type="button"
                        className="qh-action qh-remove"
                        title="Quitar de cola"
                        onClick={() => onRemoveQueued(t.id)}
                      >×</button>
                    </div>
                  </li>
                ))}
              </ul>
            )}
          </section>

          {/* ---- HISTORY ---- */}
          <section>
            <div style={qhStyles.sectionHead}>
              <span style={qhStyles.sectionLabel}>✓ HISTORIAL</span>
              <span style={qhStyles.sectionCount}>{history.length}</span>
            </div>
            {history.length === 0 ? (
              <div style={qhStyles.empty}>Sin tareas terminadas aún.</div>
            ) : (
              <ul style={qhStyles.list}>
                {history.map(t => {
                  const isDone = t.status === "completed";
                  return (
                    <li key={t.id} style={qhStyles.row}>
                      <div style={qhStyles.rowMain}>
                        <div style={qhStyles.rowTitle}>{t.title || t.id}</div>
                        <div style={qhStyles.rowMeta}>
                          <span style={{ ...qhStyles.statusTag, ...(isDone ? qhStyles.statusDone : qhStyles.statusCancelled) }}>
                            {isDone ? "COMPLETADA" : "CANCELADA"}
                          </span>
                          <span>{formatTimestamp(t.endedAt)}</span>
                        </div>
                      </div>
                    </li>
                  );
                })}
              </ul>
            )}
          </section>
        </div>

        <div style={modalStyles.foot}>
          <button type="button" onClick={onClose} style={modalStyles.btnGhost}>CERRAR</button>
        </div>
      </div>
    </div>
  );
}

const qhStyles = {
  sections: { display: "flex", flexDirection: "column", gap: 14, maxHeight: 420, overflowY: "auto", paddingRight: 4 },
  sectionHead: {
    display: "flex", alignItems: "baseline", justifyContent: "space-between",
    padding: "0 2px 6px", borderBottom: "1px solid rgba(79, 216, 255, 0.1)", marginBottom: 8,
  },
  sectionLabel: {
    fontFamily: "'JetBrains Mono', monospace",
    fontSize: 10, letterSpacing: 1.6, color: "#7EE8FF", fontWeight: 700,
  },
  sectionCount: {
    fontFamily: "'JetBrains Mono', monospace",
    fontSize: 11, color: "rgba(255,255,255,0.45)", fontWeight: 700,
  },
  empty: {
    fontSize: 11.5, color: "rgba(255,255,255,0.35)",
    padding: "8px 6px", fontStyle: "italic",
  },
  list: { listStyle: "none", margin: 0, padding: 0, display: "flex", flexDirection: "column", gap: 6 },
  row: {
    display: "flex", alignItems: "center", gap: 8,
    padding: "8px 10px",
    background: "rgba(10, 20, 36, 0.55)",
    border: "1px solid rgba(79, 216, 255, 0.1)",
    borderRadius: 8,
  },
  rowMain: { flex: 1, minWidth: 0 },
  rowTitle: { color: "#EAF6FF", fontSize: 12.5, fontWeight: 600, marginBottom: 3,
              whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" },
  rowMeta: { display: "flex", alignItems: "center", gap: 8,
             fontSize: 10.5, color: "rgba(255,255,255,0.45)",
             fontFamily: "'JetBrains Mono', monospace", letterSpacing: 0.3 },
  rowActions: { display: "flex", gap: 4, flexShrink: 0 },
  prioTag: {
    fontSize: 9, padding: "2px 6px", borderRadius: 4,
    background: "rgba(126, 232, 255, 0.12)", color: "#7EE8FF",
    border: "1px solid rgba(126, 232, 255, 0.25)", fontWeight: 700,
  },
  statusTag: {
    fontSize: 9, padding: "2px 6px", borderRadius: 4, fontWeight: 700,
    border: "1px solid transparent",
  },
  statusDone:      { background: "rgba(79, 224, 138, 0.12)",  color: "#4FE08A", borderColor: "rgba(79, 224, 138, 0.3)"  },
  statusCancelled: { background: "rgba(255, 138, 122, 0.12)", color: "#FF8A7A", borderColor: "rgba(255, 138, 122, 0.3)" },
};

// ---------------------------------------------------------------
// ALERTS CHIP — floating indicator shown only when there are open alerts.
// Colour follows the highest severity currently live.
// ---------------------------------------------------------------
function AlertsChip({ onOpen }) {
  useMissionTick();
  const ms = window.MissionState;
  if (!ms || typeof ms.getActiveAlerts !== "function") return null;
  const active = ms.getActiveAlerts();
  if (!active.length) return null;

  const rank = { info: 0, warning: 1, critical: 2 };
  const topSev = active.reduce((m, a) => (rank[a.severity] > rank[m] ? a.severity : m), "info");
  const cls = "alerts-chip alerts-chip-" + topSev;

  return (
    <button type="button" className={cls} onClick={onOpen} aria-label={`Ver ${active.length} alertas activas`}>
      <span className="alerts-chip-icon">⚠</span>
      <span className="alerts-chip-count">{active.length}</span>
    </button>
  );
}

// ---------------------------------------------------------------
// ALERTS MODAL — three sections: active, acknowledged, resolved.
// Active rows expose Atender + Descartar; acknowledged expose Descartar;
// resolved is read-only history.
// ---------------------------------------------------------------
function AlertsModal({ onClose }) {
  useMissionTick();
  const ms = window.MissionState;

  useEffect(() => {
    const onKey = (e) => { if (e.key === "Escape") onClose(); };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [onClose]);

  if (!ms) return null;

  const active       = ms.getActiveAlerts       ? ms.getActiveAlerts()       : [];
  const acknowledged = ms.getAcknowledgedAlerts ? ms.getAcknowledgedAlerts() : [];
  const resolved     = ms.getResolvedAlerts     ? ms.getResolvedAlerts(30)   : [];

  const onAck     = (id) => ms.acknowledgeAlert && ms.acknowledgeAlert(id);
  const onDismiss = (id) => ms.dismissAlert     && ms.dismissAlert(id);

  const renderRow = (a, withActions) => (
    <li key={a.id} className={"am-row am-sev-" + a.severity}>
      <span className={"am-severity am-sev-tag-" + a.severity}>{a.severity.toUpperCase()}</span>
      <div className="am-row-main">
        <div className="am-row-title">{a.title}</div>
        <div className="am-row-detail">{a.detail}</div>
        <div className="am-row-meta">{formatTimestamp(a.createdAt)}{a.resolvedAt ? " → " + formatTimestamp(a.resolvedAt) : ""}</div>
      </div>
      {withActions && (
        <div className="am-row-actions">
          {a.status === "open" && (
            <button type="button" className="am-action am-ack" title="Atender" onClick={() => onAck(a.id)}>✓</button>
          )}
          <button type="button" className="am-action am-dismiss" title="Descartar" onClick={() => onDismiss(a.id)}>×</button>
        </div>
      )}
    </li>
  );

  return (
    <div style={modalStyles.backdrop} onMouseDown={onClose} role="dialog" aria-modal="true" aria-label="Alertas operativas">
      <div style={{ ...modalStyles.card, width: 560 }} onMouseDown={(e) => e.stopPropagation()}>
        <div style={modalStyles.head}>
          <div>
            <div style={modalStyles.eyebrow}>OPS · ALERTAS</div>
            <div style={modalStyles.title}>Señales operativas</div>
          </div>
          <button type="button" onClick={onClose} style={modalStyles.closeBtn} aria-label="Cerrar">×</button>
        </div>

        <div className="am-sections">
          <section>
            <div className="am-section-head"><span>⚠ ACTIVAS</span><span className="am-count">{active.length}</span></div>
            {active.length === 0
              ? <div className="am-empty">Sin alertas activas.</div>
              : <ul className="am-list">{active.map(a => renderRow(a, true))}</ul>}
          </section>

          <section>
            <div className="am-section-head"><span>◯ ATENDIDAS</span><span className="am-count">{acknowledged.length}</span></div>
            {acknowledged.length === 0
              ? <div className="am-empty">Nada atendido pendiente.</div>
              : <ul className="am-list">{acknowledged.map(a => renderRow(a, true))}</ul>}
          </section>

          <section>
            <div className="am-section-head"><span>✓ RESUELTAS</span><span className="am-count">{resolved.length}</span></div>
            {resolved.length === 0
              ? <div className="am-empty">Sin historial todavía.</div>
              : <ul className="am-list">{resolved.map(a => renderRow(a, false))}</ul>}
          </section>
        </div>

        <div style={modalStyles.foot}>
          <button type="button" onClick={onClose} style={modalStyles.btnGhost}>CERRAR</button>
        </div>
      </div>
    </div>
  );
}

// ---------------------------------------------------------------
// ROOT — mounts into #lo-react-root (inside <section id="view-liveops">)
// Scale-wraps the 1300x800 canvas to fit the container.
// ---------------------------------------------------------------
function App() {
  // Subscribe to MissionState — any mutation (move, status, task, log, addAgent)
  // triggers a re-render so the UI always reflects shared state.
  useMissionTick();
  const agents = readMissionAgents();

  const [selected, setSelected] = useState("jarvis");
  // Default selected room follows the selected agent's current room (so the
  // highlight moves with JARVIS when it relocates via MissionState.moveAgentToRoom).
  const [selectedRoom, setSelectedRoom] = useState(null);
  const [addAgentOpen, setAddAgentOpen] = useState(false);
  // Holds the id of the agent pending removal confirmation (null = closed).
  const [removeTargetId, setRemoveTargetId] = useState(null);
  // Holds the id of the agent currently being edited (null = closed).
  const [editTargetId, setEditTargetId] = useState(null);
  // Holds the id of the agent currently being dispatched (null = closed).
  const [dispatchTargetId, setDispatchTargetId] = useState(null);
  // Task Control modal (VIEW TASKS) — single global modal, not per-agent.
  const [tasksOpen, setTasksOpen] = useState(false);
  // Holds the id of the agent pending MESSAGE send (null = closed).
  const [messageTargetId, setMessageTargetId] = useState(null);
  // Collaborators modal — holds the id of the owner-agent whose task collabs we're editing.
  const [collabTargetId, setCollabTargetId] = useState(null);
  // Room management modals.
  const [addRoomOpen, setAddRoomOpen] = useState(false);
  const [removeRoomTargetId, setRemoveRoomTargetId] = useState(null);
  const [editRoomTargetId, setEditRoomTargetId] = useState(null);
  // Queue + history panel — shows queued and terminal tasks for one agent.
  const [queueHistoryTarget, setQueueHistoryTarget] = useState(null); // { agentId, initialFocus }
  // Alerts panel — operational signals curated by MissionState.
  const [alertsOpen, setAlertsOpen] = useState(false);
  const containerRef = useRef(null);

  const agent = agents.find(a => a.id === selected) || agents[0] || normalizeAgent(AGENT_FALLBACK[0]);
  // NOTE: collaborators are now sourced inside BottomPanel from MissionState's
  // active task — they're real, persisted, and cleanly scoped to the current
  // operation instead of a decorative "first 3 other agents" filter.

  // Agent creation: close modal, focus new agent, clear manual room pick so
  // the highlight follows the new agent's room naturally.
  const handleAgentCreated = (newId) => {
    setSelected(newId);
    setSelectedRoom(null);
    setAddAgentOpen(false);
  };

  // Remove-agent flow ------------------------------------------------------
  const handleRequestRemove = (agentId) => {
    if (!agentId || agentId === "jarvis") return;       // JARVIS is protected
    if (!agents.some(a => a.id === agentId)) return;
    setRemoveTargetId(agentId);
  };
  const handleCancelRemove = () => setRemoveTargetId(null);
  const handleConfirmRemove = () => {
    const id = removeTargetId;
    if (!id) return;
    const ms = window.MissionState;
    if (ms) ms.removeAgent(id);
    // If we just removed the currently selected agent, fall back to JARVIS.
    if (selected === id) setSelected("jarvis");
    // Reset any manual room pick so the highlight doesn't stay on an
    // orphaned room selection.
    setSelectedRoom(null);
    setRemoveTargetId(null);
  };
  const removeTargetAgent = removeTargetId
    ? agents.find(a => a.id === removeTargetId)
    : null;

  // Edit-agent flow --------------------------------------------------------
  const handleRequestEdit = (agentId) => {
    if (!agentId) return;
    if (!agents.some(a => a.id === agentId)) return;
    setEditTargetId(agentId);
  };
  const handleCancelEdit = () => setEditTargetId(null);
  const handleSavedEdit  = (savedId) => {
    // Keep the saved agent focused after the edit commits.
    setSelected(savedId);
    // Clear manual room pick so the highlight follows the (possibly new) room.
    setSelectedRoom(null);
    setEditTargetId(null);
  };
  const editTargetAgent = editTargetId
    ? agents.find(a => a.id === editTargetId)
    : null;

  // Dispatch-agent flow ----------------------------------------------------
  const handleRequestDispatch = (agentId) => {
    if (!agentId) return;
    if (!agents.some(a => a.id === agentId)) return;
    setDispatchTargetId(agentId);
  };
  const handleCancelDispatch = () => setDispatchTargetId(null);
  const handleSavedDispatch  = (savedId) => {
    setSelected(savedId);       // keep the dispatched agent selected
    setSelectedRoom(null);      // let the room highlight follow them
    setDispatchTargetId(null);
  };
  const dispatchTargetAgent = dispatchTargetId
    ? agents.find(a => a.id === dispatchTargetId)
    : null;

  // Task Control flow ------------------------------------------------------
  const handleOpenTasks = () => {
    setTasksOpen(true);
    if (window.MissionState) {
      window.MissionState.addLog({
        type: "OPS", source: "TASKS", severity: "low",
        message: "Control de Tareas abierto" + (agent ? " para " + agent.name.toUpperCase() + "." : "."),
      });
    }
  };
  const handleCloseTasks = () => setTasksOpen(false);

  // Message flow -----------------------------------------------------------
  const handleRequestMessage = (agentId) => {
    if (!agentId) return;
    if (!agents.some(a => a.id === agentId)) return;
    setMessageTargetId(agentId);
  };
  const handleCancelMessage = () => setMessageTargetId(null);
  const handleSentMessage   = (/*sentToId, message*/) => setMessageTargetId(null);
  const messageTargetAgent  = messageTargetId
    ? agents.find(a => a.id === messageTargetId)
    : null;

  // Queue + History flow ---------------------------------------------------
  const handleOpenQueueHistory = (agentId, initialFocus) => {
    if (!agentId || !agents.some(a => a.id === agentId)) return;
    setQueueHistoryTarget({ agentId: agentId, initialFocus: initialFocus || "queue" });
  };
  const handleCloseQueueHistory = () => setQueueHistoryTarget(null);
  const queueHistoryAgent = queueHistoryTarget
    ? agents.find(a => a.id === queueHistoryTarget.agentId)
    : null;

  // Collaborators flow -----------------------------------------------------
  const handleOpenCollaborators = (agentId) => {
    if (!agentId || !agents.some(a => a.id === agentId)) return;
    setCollabTargetId(agentId);
  };
  const handleCloseCollaborators = () => setCollabTargetId(null);
  const collabTargetAgent = collabTargetId
    ? agents.find(a => a.id === collabTargetId)
    : null;

  // Room management flow ---------------------------------------------------
  const handleRequestAddRoom = () => setAddRoomOpen(true);
  const handleCancelAddRoom  = () => setAddRoomOpen(false);
  const handleCreatedRoom    = (newRoomId) => {
    setAddRoomOpen(false);
    setSelectedRoom(newRoomId);   // highlight it immediately
  };
  const handleRequestRemoveRoom = (roomId) => {
    if (!roomId) return;
    const ms = window.MissionState;
    const room = ms && ms.getRoomById(roomId);
    if (!room || room.isCore === true) return;  // defensive guard
    setRemoveRoomTargetId(roomId);
  };
  const handleRequestEditRoom = (roomId) => {
    if (!roomId) return;
    const ms = window.MissionState;
    const room = ms && ms.getRoomById(roomId);
    if (!room || room.isCore === true) return;  // defensive guard: core rooms locked
    setEditRoomTargetId(roomId);
  };
  const handleCancelEditRoom = () => setEditRoomTargetId(null);
  const handleSavedEditRoom  = (savedRoomId) => {
    setEditRoomTargetId(null);
    // Keep the saved room highlighted so the operator sees the change land.
    setSelectedRoom(savedRoomId);
  };
  const handleCancelRemoveRoom = () => setRemoveRoomTargetId(null);
  const handleConfirmRemoveRoom = () => {
    const id = removeRoomTargetId;
    if (!id) return;
    const ms = window.MissionState;
    if (ms) ms.removeRoom(id);
    // If the currently selected agent was in that room, MissionState already
    // relocated them to "office" — reflect that focus in the UI.
    if (selectedRoom === id) setSelectedRoom(null);
    setRemoveRoomTargetId(null);
  };
  const agentsByRoom = useMemo(() => {
    const m = {};
    agents.forEach(a => { (m[a.room] = m[a.room] || []).push(a); });
    return m;
  }, [agents]);

  // Effective room highlight: explicit user pick, or the currently selected agent's room.
  const effectiveSelectedRoom = selectedRoom || (agent && agent.room) || null;

  // Clicking a room highlights it. If that room has an agent, promote the
  // first one to Selected Agent in the bottom panel — keeps the panel useful.
  const onSelectRoom = (roomId) => {
    setSelectedRoom(roomId);
    const first = (agentsByRoom[roomId] || [])[0];
    if (first) setSelected(first.id);
  };

  return (
    <div ref={containerRef} style={{
      position: "absolute", inset: 0, overflow: "hidden",
      background: "radial-gradient(ellipse at top, #0F2544 0%, #06101E 60%, #02060E 100%)",
      fontFamily: "Inter, sans-serif", color: "#EAF6FF",
      // Fluid block layout — no more virtual 1300×800 canvas + scale transform.
      // The viewport's real width is what the UI uses, which kills the dead
      // band on the right in wide viewports while preserving responsiveness.
      display: "flex",
      alignItems: "stretch",
      justifyContent: "flex-start",
    }}>
      {/* decorative grid */}
      <div style={{
        position: "absolute", inset: 0, pointerEvents: "none", opacity: 0.35,
        backgroundImage:
          "linear-gradient(rgba(79, 216, 255, 0.05) 1px, transparent 1px), " +
          "linear-gradient(90deg, rgba(79, 216, 255, 0.05) 1px, transparent 1px)",
        backgroundSize: "40px 40px",
      }} />

      <div style={{
        position: "relative",
        flex: 1,                                  // fill the container horizontally
        minWidth: 0, minHeight: 0,
        maxWidth: 1800,                           // sensible cap for ultra-wide displays
        marginLeft: 10,
        padding: 10,
        display: "flex",
      }}>
        <SharedDefs />
        <AlertsChip onOpen={() => setAlertsOpen(true)} />
        <Sidebar agents={agents} active={selected} onSelect={setSelected}
                 onOpenAddAgent={() => setAddAgentOpen(true)} />
        <div style={{ flex: 1, display: "flex", flexDirection: "column", minWidth: 0 }}>
          <RoomsGrid
            agentsByRoom={agentsByRoom}
            selectedRoom={effectiveSelectedRoom}
            onSelectRoom={onSelectRoom}
            onRequestAddRoom={handleRequestAddRoom}
            onRequestEditRoom={handleRequestEditRoom}
            onRequestRemoveRoom={handleRequestRemoveRoom}
          />
          <BottomPanel agent={agent}
                       onRequestRemove={handleRequestRemove}
                       onRequestEdit={handleRequestEdit}
                       onRequestDispatch={handleRequestDispatch}
                       onRequestTasks={handleOpenTasks}
                       onRequestMessage={handleRequestMessage}
                       onRequestCollaborators={handleOpenCollaborators}
                       onRequestQueueHistory={handleOpenQueueHistory} />
          {/* Demo-op result panel — only visible after "Crear pieza social"
              has been triggered. Re-runs replace its contents. */}
          <SocialPieceOpPanel />
        </div>
      </div>

      {/* Add Agent modal — rendered OUTSIDE the scale wrapper so it stays crisp. */}
      {addAgentOpen && (
        <AddAgentModal
          onClose={() => setAddAgentOpen(false)}
          onCreated={handleAgentCreated}
        />
      )}

      {/* Remove Agent confirmation — same pattern, danger palette. */}
      {removeTargetAgent && (
        <RemoveAgentConfirm
          agent={removeTargetAgent}
          onCancel={handleCancelRemove}
          onConfirm={handleConfirmRemove}
        />
      )}

      {/* Edit Agent modal — preloads current values, same pattern as Add. */}
      {editTargetAgent && (
        <EditAgentModal
          agent={editTargetAgent}
          onClose={handleCancelEdit}
          onSaved={handleSavedEdit}
        />
      )}

      {/* Dispatch Agent modal — assigns task + room + priority + status. */}
      {dispatchTargetAgent && (
        <DispatchAgentModal
          agent={dispatchTargetAgent}
          onClose={handleCancelDispatch}
          onSaved={handleSavedDispatch}
        />
      )}

      {/* Task Control modal — the VIEW TASKS console. */}
      {tasksOpen && (
        <TaskControlModal
          selectedAgentId={selected}
          onClose={handleCloseTasks}
        />
      )}

      {/* Message modal — send an instruction / note to the selected agent. */}
      {messageTargetAgent && (
        <MessageAgentModal
          agent={messageTargetAgent}
          onClose={handleCancelMessage}
          onSent={handleSentMessage}
        />
      )}

      {/* Collaborators modal — add/remove task collaborators. */}
      {collabTargetAgent && (
        <CollaboratorsModal
          agent={collabTargetAgent}
          onClose={handleCloseCollaborators}
        />
      )}

      {/* Add Room modal — create a new (non-core) room. */}
      {addRoomOpen && (
        <AddRoomModal
          onClose={handleCancelAddRoom}
          onCreated={handleCreatedRoom}
        />
      )}

      {/* Remove Room confirmation — destructive; relocates agents/tasks to office. */}
      {removeRoomTargetId && (
        <RemoveRoomConfirm
          roomId={removeRoomTargetId}
          onCancel={handleCancelRemoveRoom}
          onConfirm={handleConfirmRemoveRoom}
        />
      )}

      {/* Edit Room modal — non-destructive; updates title / type / tint / capacity. */}
      {editRoomTargetId && (
        <EditRoomModal
          roomId={editRoomTargetId}
          onClose={handleCancelEditRoom}
          onSaved={handleSavedEditRoom}
        />
      )}

      {/* Queue + History panel — per-agent read view + queue controls. */}
      {queueHistoryAgent && (
        <QueueHistoryModal
          agent={queueHistoryAgent}
          initialFocus={queueHistoryTarget.initialFocus}
          onClose={handleCloseQueueHistory}
        />
      )}

      {/* Alerts panel — operational signals, three sections. */}
      {alertsOpen && (
        <AlertsModal onClose={() => setAlertsOpen(false)} />
      )}
    </div>
  );
}

const mountEl = document.getElementById("lo-react-root");
if (mountEl) {
  const root = ReactDOM.createRoot(mountEl);
  root.render(<App />);
}
