// Workspace — Tracker module: kanban, phases, timeline, my tasks, task drawer

// Phase lifecycle. Order matters — used as the canonical column order in
// the kanban-by-status view and in the tabs strip. Matches PHASE_STATUSES
// in src/services/phases.ts.
const PHASE_STATUS_ORDER = ['pending', 'active', 'blocked', 'paused', 'discarded', 'done'];
const PHASE_STATUS_META = {
  pending:   { label: 'Pendiente',  color: 'var(--text-2)', bg: 'var(--bg-2)',                    accent: 'var(--text-2)' },
  active:    { label: 'Activa',     color: '#9DBDF5',       bg: 'rgba(79,142,255,.10)',           accent: 'var(--accent)' },
  blocked:   { label: 'Bloqueada',  color: '#FCA5A5',       bg: 'rgba(239,68,68,.10)',            accent: 'var(--danger)' },
  paused:    { label: 'Pausada',    color: '#F4C770',       bg: 'rgba(245,158,11,.10)',           accent: 'var(--warn)' },
  discarded: { label: 'Descartada', color: 'var(--text-3)', bg: 'rgba(255,255,255,.04)',          accent: 'var(--text-3)' },
  done:      { label: 'Cerrada',    color: '#7BD9B0',       bg: 'rgba(16,185,129,.10)',           accent: 'var(--success)' },
};
function phaseStatusMeta(s) { return PHASE_STATUS_META[s] || PHASE_STATUS_META.pending; }
function phaseStatusLabel(s) { return phaseStatusMeta(s).label; }

const COLUMNS = [
  { id: 'backlog',   label: 'Backlog' },
  { id: 'ready',     label: 'Listo para empezar' },
  { id: 'progress',  label: 'En curso' },
  { id: 'review',    label: 'En revisión' },
  { id: 'blocked',   label: 'Bloqueado' },
  { id: 'closed',    label: 'Cerrado' },
  { id: 'discarded', label: 'Descartada' },
];

// ── Kanban ──────────────────────────────────────────────────────────
function Kanban({ onOpenTask }) {
  const [showNewTask, setShowNewTask] = React.useState(null); // null | { status?: string }
  const [filters, setFilters] = React.useState({ assignee: null, phase: null, priority: null });
  const [draggingId, setDraggingId] = React.useState(null);
  const [dropTarget, setDropTarget] = React.useState(null); // column id being hovered

  // Drop a task on a column → patch status. Optimistic: window.patchTask
  // updates window.TASKS and calls publish() before the server replies.
  const onDropTask = async (taskId, targetStatus) => {
    setDraggingId(null); setDropTarget(null);
    if (!taskId) return;
    const task = (window.TASKS || []).find(t => t.id === taskId);
    if (!task || task.status === targetStatus) return;
    try {
      await window.patchTask(taskId, { status: targetStatus });
    } catch (e) { console.error('drag-drop status update failed', e); }
  };
  const filteredTasks = React.useMemo(() => TASKS.filter(t => {
    if (filters.assignee && t.assignee !== filters.assignee) return false;
    if (filters.phase && t.phase !== filters.phase) return false;
    if (filters.priority && t.prio !== filters.priority) return false;
    return true;
  }), [filters, TASKS]);

  const filterPills = [
    { key: 'assignee', label: 'Asignado', value: filters.assignee ? ACTORS[filters.assignee]?.name : 'Todos',
      options: [{ id: null, label: 'Todos' }, ...ACTOR_LIST.map(id => ({ id, label: ACTORS[id].name }))] },
    { key: 'phase', label: 'Fase', value: filters.phase || 'Todas',
      options: [{ id: null, label: 'Todas' }, ...(window.PHASES || []).map(p => ({ id: p.id, label: `${p.id} · ${p.name}` }))] },
    { key: 'priority', label: 'Prioridad', value: filters.priority || 'Todas',
      options: [{ id: null, label: 'Todas' }, ...['P0','P1','P2','P3'].map(p => ({ id: p, label: p }))] },
  ];

  const totalTasks = (window.TASKS || []).length;
  return (
    <div style={{ flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
      <div style={{ padding: '24px 32px 14px', flexShrink: 0 }}>
        <SectionHead
          level="h1"
          title="Tablero"
          sub={`${filteredTasks.filter(t => t.status !== 'closed' && t.status !== 'discarded').length} tareas abiertas`}
          right={<div style={{ display: 'flex', gap: 8 }}>
            <Button variant="secondary" size="md" icon="filter-x"
              onClick={() => setFilters({ assignee: null, phase: null, priority: null })}
              disabled={!filters.assignee && !filters.phase && !filters.priority}>
              Limpiar filtros
            </Button>
            <Button variant="primary" size="md" icon="plus" onClick={() => setShowNewTask({})}>Nueva tarea</Button>
          </div>} />
        {totalTasks === 0 && <div style={{ marginTop: 20 }}><EmptyWorkspaceNudge kind="tracker" /></div>}
        <div style={{ display: 'flex', gap: 8, marginTop: 14 }}>
          {filterPills.map((f) => (
            <FilterPill
              key={f.key}
              label={f.label}
              value={f.value}
              isActive={!!filters[f.key]}
              options={f.options}
              selectedId={filters[f.key]}
              onPick={(id) => setFilters((prev) => ({ ...prev, [f.key]: id }))}
            />
          ))}
        </div>
      </div>

      <div style={{ flex: 1, overflowX: 'auto', overflowY: 'hidden', padding: '0 32px 24px' }}>
        <div style={{ display: 'flex', gap: 14, height: '100%', minWidth: 'max-content' }}>
          {COLUMNS.map(col => {
            const colTasks = filteredTasks.filter(t => t.status === col.id);
            const meta = STATUS_META[col.id];
            const isDropTarget = dropTarget === col.id && draggingId;
            return (
              <div key={col.id}
                onDragOver={(e) => { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; if (dropTarget !== col.id) setDropTarget(col.id); }}
                onDragLeave={(e) => {
                  // Only clear if we left the column entirely (not just entered a child).
                  if (e.currentTarget.contains(e.relatedTarget)) return;
                  if (dropTarget === col.id) setDropTarget(null);
                }}
                onDrop={(e) => { e.preventDefault(); onDropTask(e.dataTransfer.getData('text/plain'), col.id); }}
                style={{
                  width: 280, flexShrink: 0, display: 'flex', flexDirection: 'column',
                  background: isDropTarget ? 'var(--accent-dim)' : 'var(--bg-1)',
                  border: `1px solid ${isDropTarget ? 'var(--accent)' : 'var(--border)'}`,
                  borderRadius: 'var(--r-lg)',
                  transition: 'background var(--t-fast), border-color var(--t-fast)',
                }}>
                <div style={{
                  padding: '12px 14px', display: 'flex', alignItems: 'center', justifyContent: 'space-between',
                  borderBottom: '1px solid var(--border)',
                }}>
                  <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
                    <StatusDot s={col.id} />
                    <span style={{ fontSize: 12, fontWeight: 600, color: meta.color, textTransform: 'uppercase', letterSpacing: 0.6 }}>{meta.label}</span>
                    <span className="mono" style={{ fontSize: 11, color: 'var(--text-3)' }}>{colTasks.length}</span>
                  </div>
                  <button className="btn-reset" title="Nueva tarea en esta columna"
                    onClick={() => setShowNewTask({ status: col.id })}
                    style={{ width: 22, height: 22, display: 'flex', alignItems: 'center', justifyContent: 'center', borderRadius: 4, color: 'var(--text-2)' }}>
                    <Icon name="plus" size={13} />
                  </button>
                </div>
                <div style={{ flex: 1, overflowY: 'auto', padding: 8, display: 'flex', flexDirection: 'column', gap: 8 }}>
                  {colTasks.map((t, i) => (
                    <TaskCard
                      key={t.id}
                      task={t}
                      hover={col.id === 'progress' && i === 0}
                      onClick={() => onOpenTask(t.id)}
                      draggable
                      isDragging={draggingId === t.id}
                      onDragStart={(e) => {
                        e.dataTransfer.setData('text/plain', t.id);
                        e.dataTransfer.effectAllowed = 'move';
                        setDraggingId(t.id);
                      }}
                      onDragEnd={() => { setDraggingId(null); setDropTarget(null); }}
                    />
                  ))}
                  {colTasks.length === 0 && (
                    <div style={{ padding: '20px 8px', fontSize: 11.5, color: isDropTarget ? 'var(--accent)' : 'var(--text-3)', textAlign: 'center', fontStyle: 'italic' }}>
                      {isDropTarget ? 'Soltar aquí' : 'Vacío'}
                    </div>
                  )}
                </div>
              </div>
            );
          })}
        </div>
      </div>

      {showNewTask && (
        <NewTaskModal onClose={() => setShowNewTask(null)}
          onCreated={(id) => { setShowNewTask(null); onOpenTask && onOpenTask(id); }}
          presetStatus={showNewTask.status} />
      )}
    </div>
  );
}

// FilterPill: button + inline dropdown. Used in the Kanban filter strip.
// Replaces the Popover-based version which had a race: a mousedown inside
// the popover triggered the document-level "outside click" handler before
// the click on the option could fire, so picks were silently dropped.
//
// This version is self-contained: a transparent overlay (z-index 40)
// catches outside clicks, and the menu (z-index 50) sits above it. Clicks
// on options apply the filter AND close the menu in the same handler.
function FilterPill({ label, value, isActive, options, selectedId, onPick }) {
  const [open, setOpen] = React.useState(false);
  return (
    <div style={{ position: 'relative' }}>
      <button className="btn-reset" onClick={() => setOpen((o) => !o)}
        style={{
          display: 'flex', alignItems: 'center', gap: 6,
          padding: '5px 10px', height: 26,
          background: isActive ? 'var(--accent-dim)' : (open ? 'var(--bg-2)' : 'var(--bg-1)'),
          border: `1px solid ${isActive ? 'var(--accent)' : 'var(--border)'}`,
          borderRadius: 6, fontSize: 12, color: 'var(--text-1)', cursor: 'pointer',
        }}>
        <span style={{ color: 'var(--text-2)' }}>{label}:</span>
        <span style={{ color: isActive ? 'var(--accent)' : 'var(--text-0)', fontWeight: 500 }}>{value}</span>
        <Icon name="chevron-down" size={11} color="var(--text-2)" />
      </button>
      {open && (
        <>
          <div onClick={() => setOpen(false)} style={{ position: 'fixed', inset: 0, zIndex: 40 }} />
          <div style={{
            position: 'absolute', top: '100%', left: 0, marginTop: 4,
            width: 260, maxHeight: 360, overflowY: 'auto', padding: 4,
            background: 'var(--bg-1)', border: '1px solid var(--border)',
            borderRadius: 'var(--r-md)', boxShadow: '0 12px 32px rgba(0,0,0,.5)',
            zIndex: 50,
          }}>
            {options.map((o) => {
              const active = selectedId === o.id || (selectedId == null && o.id == null);
              return (
                <button key={o.id ?? '_all'} className="btn-reset"
                  onClick={() => { onPick(o.id); setOpen(false); }}
                  style={{
                    display: 'flex', alignItems: 'center', gap: 8, width: '100%',
                    padding: '7px 10px', borderRadius: 5, textAlign: 'left',
                    background: active ? 'var(--bg-2)' : 'transparent',
                    color: active ? 'var(--accent)' : 'var(--text-0)',
                    fontSize: 12.5, fontWeight: active ? 500 : 400,
                  }}
                  onMouseEnter={(e) => { if (!active) e.currentTarget.style.background = 'var(--bg-2)'; }}
                  onMouseLeave={(e) => { if (!active) e.currentTarget.style.background = 'transparent'; }}>
                  <span style={{ flex: 1 }}>{o.label}</span>
                  {active && <Icon name="check" size={12} color="var(--accent)" />}
                </button>
              );
            })}
          </div>
        </>
      )}
    </div>
  );
}

function TaskCard({ task, hover: forceHover, onClick, draggable, isDragging, onDragStart, onDragEnd }) {
  const [hover, setHover] = React.useState(false);
  const isHover = hover || forceHover;
  const isBlocked = task.status === 'blocked';
  const blockerCount = (task.blockedBy && task.blockedBy.length) || 0;
  const blockerLabel = blockerCount > 0
    ? (task.isReady
        ? `Bloqueada históricamente por ${blockerCount} tarea${blockerCount === 1 ? '' : 's'} (todas cerradas — lista para empezar)`
        : `Bloqueada por ${blockerCount} tarea${blockerCount === 1 ? '' : 's'}: ${task.blockedBy.join(', ')}`)
    : null;
  return (
    <div onClick={onClick}
      draggable={draggable}
      onDragStart={onDragStart}
      onDragEnd={onDragEnd}
      onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}
      style={{
        background: 'var(--bg-2)',
        border: '1px solid var(--border)',
        borderLeft: isBlocked ? '2px solid var(--danger)' : '1px solid var(--border)',
        borderRadius: 8, padding: 10,
        cursor: draggable ? (isDragging ? 'grabbing' : 'grab') : 'pointer',
        position: 'relative',
        transition: 'transform var(--t-mid), box-shadow var(--t-mid), border-color var(--t-fast), opacity var(--t-fast)',
        transform: isHover && !isDragging ? 'translateY(-1px)' : 'none',
        boxShadow: isHover && !isDragging ? '0 8px 20px rgba(0,0,0,.35), 0 0 0 1px var(--border-strong)' : 'none',
        opacity: isDragging ? 0.4 : 1,
      }}>
      {isHover && (
        <div style={{ position: 'absolute', left: 4, top: '50%', transform: 'translateY(-50%)', color: 'var(--text-3)', display: 'flex' }}>
          <Icon name="grip-vertical" size={12} />
        </div>
      )}
      <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 6 }}>
        <span className="mono" style={{ fontSize: 10.5, color: 'var(--text-3)' }}>{task.id}</span>
        <PriorityChip p={task.prio} />
      </div>
      <div style={{ fontSize: 13, color: 'var(--text-0)', lineHeight: 1.4, marginBottom: 10, fontWeight: 500 }}>{task.title}</div>
      <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 6 }}>
        <Tag mono>{task.phase}</Tag>
        <div style={{ display: 'flex', alignItems: 'center', gap: 8, color: 'var(--text-2)', fontSize: 11 }}>
          {task.dueIn != null && (
            <span style={{ display: 'inline-flex', alignItems: 'center', gap: 3, color: task.dueIn <= 0 ? 'var(--danger)' : 'var(--warn)' }}>
              <Icon name="calendar" size={10} /> {task.dueIn === 0 ? 'hoy' : task.dueIn < 0 ? `${-task.dueIn}d tarde` : `${task.dueIn}d`}
            </span>
          )}
          {task.comments > 0 && (
            <span style={{ display: 'inline-flex', alignItems: 'center', gap: 3 }}>
              <Icon name="message-square" size={10} /> {task.comments}
            </span>
          )}
          {task.attach > 0 && (
            <span style={{ display: 'inline-flex', alignItems: 'center', gap: 3 }}>
              <Icon name="paperclip" size={10} /> {task.attach}
            </span>
          )}
          {(task.estimatedHours != null || task.actualHours != null) && (
            <span title={
              task.estimatedHours != null && task.actualHours != null
                ? `${task.actualHours}h reales / ${task.estimatedHours}h estimadas`
                : task.estimatedHours != null
                  ? `${task.estimatedHours}h estimadas (sin reales)`
                  : `${task.actualHours}h reales (sin estimación)`
            } style={{ display: 'inline-flex', alignItems: 'center', gap: 3, color: 'var(--text-2)' }}>
              <Icon name="timer" size={10} />
              {task.estimatedHours != null && task.actualHours != null
                ? `${task.estimatedHours}h/${task.actualHours}h`
                : task.estimatedHours != null
                  ? `${task.estimatedHours}h est.`
                  : `${task.actualHours}h`}
            </span>
          )}
          {blockerCount > 0 && (
            <span title={blockerLabel}
              style={{
                display: 'inline-flex', alignItems: 'center', gap: 3,
                color: task.isReady ? 'var(--success)' : 'var(--warn)',
              }}>
              <Icon name={task.isReady ? 'lock-open' : 'lock'} size={10} /> {blockerCount}
            </span>
          )}
          <Avatar actor={task.assignee} size={18} />
        </div>
      </div>
    </div>
  );
}

// ── Phases view (list with status tabs) ────────────────────────────
function PhasesView({ onOpenTask }) {
  const [open, setOpen] = React.useState({});
  const [showNewPhase, setShowNewPhase] = React.useState(false);
  const [editPhase, setEditPhase] = React.useState(null);
  const [confirmDelete, setConfirmDelete] = React.useState(null);
  const [tab, setTab] = React.useState('active'); // 'all' | one of PHASE_STATUS_ORDER

  const counts = React.useMemo(() => {
    const c = { all: PHASES.length };
    for (const s of PHASE_STATUS_ORDER) c[s] = 0;
    for (const ph of PHASES) {
      const s = PHASE_STATUS_ORDER.includes(ph.status) ? ph.status : 'pending';
      c[s] = (c[s] || 0) + 1;
    }
    return c;
  }, [PHASES.length, PHASES.map(p => p.status).join(',')]);

  // If the user lands on a tab with 0 items but "Activas" also has 0, fall
  // back to "Todas" so they don't see an empty page on first load.
  React.useEffect(() => {
    if (tab !== 'all' && counts[tab] === 0 && counts.all > 0) setTab('all');
  }, []); // eslint-disable-line

  const filtered = React.useMemo(() => {
    if (tab === 'all') return PHASES;
    return PHASES.filter((ph) => (PHASE_STATUS_ORDER.includes(ph.status) ? ph.status : 'pending') === tab);
  }, [tab, PHASES.length, PHASES.map(p => p.status + p.id).join(',')]);

  const onDelete = async (id) => {
    try {
      await window.api('DELETE', `/api/phases/${encodeURIComponent(id)}`);
      await window.loadAll();
    } catch (e) { console.error(e); }
    setConfirmDelete(null);
  };

  const fmtRange = (ph) => {
    const s = ph.startsAt ? new Date(ph.startsAt + 'T00:00:00Z').toLocaleDateString('es', { day: 'numeric', month: 'short' }) : null;
    const e = ph.dueAt ? new Date(ph.dueAt + 'T00:00:00Z').toLocaleDateString('es', { day: 'numeric', month: 'short' }) : null;
    if (s && e) return `${s} → ${e}`;
    if (e) return `hasta ${e}`;
    if (s) return `desde ${s}`;
    return 'sin fechas';
  };

  const TABS = [
    { id: 'all', label: 'Todas' },
    ...PHASE_STATUS_ORDER.map((s) => ({ id: s, label: phaseStatusMeta(s).label + 's' })),
    // pluralization: Pendiente → Pendientes, Activa → Activas, …  the trailing s
    // works for all current labels in Spanish; if a future label breaks this,
    // switch to a per-meta `pluralLabel` field.
  ];

  return (
    <div style={{ flex: 1, overflowY: 'auto', padding: '24px 32px 60px' }}>
      <SectionHead level="h1" title="Fases"
        sub={`${PHASES.length} ${PHASES.length === 1 ? 'fase' : 'fases'} en el roadmap`}
        right={<Button variant="primary" size="md" icon="plus" onClick={() => setShowNewPhase(true)}>Nueva fase</Button>} />

      {/* Status tabs */}
      <div style={{
        display: 'flex', gap: 4, marginBottom: 16, padding: 4,
        background: 'var(--bg-1)', border: '1px solid var(--border)',
        borderRadius: 'var(--r-md)', overflowX: 'auto',
      }}>
        {TABS.map((t) => {
          const active = tab === t.id;
          const meta = t.id === 'all' ? null : phaseStatusMeta(t.id);
          const count = counts[t.id] ?? 0;
          return (
            <button key={t.id} className="btn-reset" onClick={() => setTab(t.id)}
              style={{
                display: 'flex', alignItems: 'center', gap: 6, whiteSpace: 'nowrap',
                padding: '6px 12px', borderRadius: 6,
                background: active ? 'var(--bg-3)' : 'transparent',
                color: active ? 'var(--text-0)' : 'var(--text-2)',
                fontSize: 12.5, fontWeight: active ? 600 : 500,
                cursor: 'pointer',
              }}
              onMouseEnter={(e) => { if (!active) e.currentTarget.style.background = 'var(--bg-2)'; }}
              onMouseLeave={(e) => { if (!active) e.currentTarget.style.background = 'transparent'; }}>
              {meta && <span style={{ width: 6, height: 6, borderRadius: '50%', background: meta.accent }} />}
              <span>{t.label}</span>
              <span className="mono" style={{
                fontSize: 10.5,
                color: active ? 'var(--text-2)' : 'var(--text-3)',
                padding: '1px 6px',
                background: active ? 'var(--bg-1)' : 'var(--bg-2)',
                borderRadius: 999,
              }}>{count}</span>
            </button>
          );
        })}
      </div>

      {PHASES.length === 0 && (
        <div style={{
          padding: '60px 24px', textAlign: 'center',
          border: '1px dashed var(--border)', borderRadius: 'var(--r-lg)',
          background: 'rgba(22,25,34,.4)',
        }}>
          <Icon name="layers" size={28} color="var(--text-3)" />
          <div style={{ fontSize: 14, fontWeight: 600, color: 'var(--text-1)', marginTop: 14 }}>No hay fases definidas</div>
          <div style={{ fontSize: 12.5, color: 'var(--text-2)', marginTop: 4, marginBottom: 18 }}>Crea la primera fase para empezar a planificar el roadmap.</div>
          <Button variant="primary" icon="plus" onClick={() => setShowNewPhase(true)}>Crear fase</Button>
        </div>
      )}
      {PHASES.length > 0 && filtered.length === 0 && (
        <div style={{
          padding: '40px 24px', textAlign: 'center',
          border: '1px dashed var(--border)', borderRadius: 'var(--r-lg)',
          background: 'rgba(22,25,34,.4)', color: 'var(--text-2)', fontSize: 13,
        }}>
          No hay fases en <strong style={{ color: 'var(--text-1)' }}>{TABS.find((t) => t.id === tab)?.label.toLowerCase()}</strong>.
        </div>
      )}
      <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
        {filtered.map(ph => {
          const isOpen = open[ph.id];
          const pct = ph.total > 0 ? Math.round((ph.done / ph.total) * 100) : 0;
          return (
            <div key={ph.id} style={{
              background: 'var(--bg-1)', border: '1px solid var(--border)', borderRadius: 'var(--r-lg)', overflow: 'hidden',
            }}>
              {/* Outer is a div (not button) so the inner Edit/Delete buttons
                  are valid HTML and receive their own clicks reliably. */}
              <div onClick={() => setOpen(o => ({ ...o, [ph.id]: !o[ph.id] }))}
                role="button" tabIndex={0}
                onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); setOpen(o => ({ ...o, [ph.id]: !o[ph.id] })); } }}
                style={{
                  display: 'flex', alignItems: 'center', gap: 14, width: '100%',
                  padding: '14px 18px', textAlign: 'left', cursor: 'pointer',
                  userSelect: 'none',
                }}>
                <Icon name={isOpen ? 'chevron-down' : 'chevron-right'} size={14} color="var(--text-2)" />
                <span className="mono" style={{ fontSize: 12, color: 'var(--text-2)', minWidth: 44 }}>{ph.id}</span>
                <span style={{ fontSize: 14, fontWeight: 500, color: 'var(--text-0)', minWidth: 200 }}>{ph.name}</span>
                <div style={{ flex: 1, display: 'flex', alignItems: 'center', gap: 12 }}>
                  <div style={{ flex: 1, maxWidth: 280, height: 6, background: 'var(--bg-3)', borderRadius: 3, overflow: 'hidden' }}>
                    <div style={{ width: `${pct}%`, height: '100%', background: ph.status === 'done' ? 'var(--success)' : 'var(--accent)', transition: 'width var(--t-mid)' }} />
                  </div>
                  <span className="mono" style={{ fontSize: 11.5, color: 'var(--text-2)', minWidth: 60 }}>{ph.done}/{ph.total}</span>
                </div>
                {ph.ownerId && <Avatar actor={ph.ownerId} size={20} />}
                <span style={{ fontSize: 12, color: 'var(--text-2)', display: 'inline-flex', alignItems: 'center', gap: 5 }}>
                  <Icon name="calendar-range" size={11} /> {fmtRange(ph)}
                </span>
                <Badge color={phaseStatusMeta(ph.status).accent}
                       bg={phaseStatusMeta(ph.status).bg}
                       dot>{phaseStatusLabel(ph.status).toLowerCase()}</Badge>
                <button className="btn-reset" title="Editar fase"
                  onClick={(e) => { e.stopPropagation(); setEditPhase(ph); }}
                  style={{ width: 24, height: 24, display: 'flex', alignItems: 'center', justifyContent: 'center', borderRadius: 5, color: 'var(--text-3)' }}
                  onMouseEnter={(e) => { e.currentTarget.style.background = 'var(--bg-2)'; e.currentTarget.style.color = 'var(--text-1)'; }}
                  onMouseLeave={(e) => { e.currentTarget.style.background = 'transparent'; e.currentTarget.style.color = 'var(--text-3)'; }}>
                  <Icon name="pencil" size={12} />
                </button>
                <button className="btn-reset" title="Borrar fase"
                  onClick={(e) => { e.stopPropagation(); setConfirmDelete(ph); }}
                  style={{ width: 24, height: 24, display: 'flex', alignItems: 'center', justifyContent: 'center', borderRadius: 5, color: 'var(--text-3)' }}
                  onMouseEnter={(e) => { e.currentTarget.style.background = 'rgba(239,68,68,.10)'; e.currentTarget.style.color = 'var(--danger)'; }}
                  onMouseLeave={(e) => { e.currentTarget.style.background = 'transparent'; e.currentTarget.style.color = 'var(--text-3)'; }}>
                  <Icon name="trash-2" size={12} />
                </button>
              </div>
              {isOpen && (
                <div style={{ borderTop: '1px solid var(--border)', padding: '16px 18px', background: 'var(--bg-0)' }}>
                  {(ph.description || ph.tags?.length > 0 || ph.ownerId) && (
                    <div style={{ marginBottom: 16, paddingBottom: 14, borderBottom: '1px solid var(--border)' }}>
                      {ph.description && (
                        <div style={{ fontSize: 12.5, color: 'var(--text-1)', lineHeight: 1.6, marginBottom: 10 }}>
                          <RenderedMd md={ph.description} />
                        </div>
                      )}
                      <div style={{ display: 'flex', alignItems: 'center', gap: 12, flexWrap: 'wrap', fontSize: 11.5 }}>
                        {ph.ownerId && (
                          <span style={{ display: 'inline-flex', alignItems: 'center', gap: 6, color: 'var(--text-2)' }}>
                            <span style={{ color: 'var(--text-3)', textTransform: 'uppercase', letterSpacing: 0.5 }}>Responsable</span>
                            <Avatar actor={ph.ownerId} size={18} />
                            <span>{ACTORS[ph.ownerId]?.name || ph.ownerId}</span>
                          </span>
                        )}
                        {ph.tags?.length > 0 && (
                          <span style={{ display: 'inline-flex', alignItems: 'center', gap: 6, flexWrap: 'wrap' }}>
                            <span style={{ color: 'var(--text-3)', textTransform: 'uppercase', letterSpacing: 0.5 }}>Tags</span>
                            {ph.tags.map(t => <Tag key={t}>{t}</Tag>)}
                          </span>
                        )}
                      </div>
                    </div>
                  )}
                  <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 16 }}>
                    {['progress', 'review', 'ready'].map(s => {
                      const tasks = TASKS.filter(t => t.phase === ph.id && t.status === s);
                      const m = STATUS_META[s];
                      return (
                        <div key={s}>
                          <div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 10 }}>
                            <StatusDot s={s} />
                            <span style={{ fontSize: 11, fontWeight: 600, color: m.color, textTransform: 'uppercase', letterSpacing: 0.5 }}>{m.label}</span>
                            <span className="mono" style={{ fontSize: 11, color: 'var(--text-3)' }}>{tasks.length}</span>
                          </div>
                          <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
                            {tasks.map(t => <TaskCardCompact key={t.id} task={t} onClick={() => onOpenTask(t.id)} />)}
                            {tasks.length === 0 && <div style={{ fontSize: 11, color: 'var(--text-3)', fontStyle: 'italic', padding: '4px 6px' }}>—</div>}
                          </div>
                        </div>
                      );
                    })}
                  </div>
                </div>
              )}
            </div>
          );
        })}
      </div>

      {showNewPhase && <NewPhaseModal onClose={() => setShowNewPhase(false)} />}
      {editPhase && <NewPhaseModal initial={editPhase} onClose={() => setEditPhase(null)} />}
      {confirmDelete && (
        <Modal title={`Borrar ${confirmDelete.id}`} onClose={() => setConfirmDelete(null)} width={420}
          footer={<>
            <Button variant="ghost" onClick={() => setConfirmDelete(null)}>Cancelar</Button>
            <Button variant="dangerSolid" onClick={() => onDelete(confirmDelete.id)}>Borrar fase</Button>
          </>}>
          <div style={{ fontSize: 13, color: 'var(--text-1)', lineHeight: 1.6 }}>
            ¿Borrar la fase <span className="mono" style={{ color: 'var(--text-0)' }}>{confirmDelete.id}</span> ({confirmDelete.name})?
            Las {confirmDelete.total} {confirmDelete.total === 1 ? 'tarea asociada quedará' : 'tareas asociadas quedarán'} sin fase pero no se eliminan.
          </div>
        </Modal>
      )}
    </div>
  );
}

// ── Phases kanban view (6 columns by status, drag-drop) ────────────
function PhasesKanban({ onOpenTask }) {
  const [draggingId, setDraggingId] = React.useState(null);
  const [dropTarget, setDropTarget] = React.useState(null);
  const [showNewPhase, setShowNewPhase] = React.useState(null); // null | { status }
  const [editPhase, setEditPhase] = React.useState(null);

  // Optimistic move: patch on the server, then loadAll to pick up the
  // canonical state (counts, position changes, etc.). On failure, log and
  // let loadAll's eventual sync fix the UI.
  const onDropPhase = async (phaseId, targetStatus) => {
    setDraggingId(null); setDropTarget(null);
    if (!phaseId) return;
    const ph = (window.PHASES || []).find((p) => p.id === phaseId);
    if (!ph || ph.status === targetStatus) return;
    try {
      await window.api('PATCH', `/api/phases/${encodeURIComponent(phaseId)}`, { status: targetStatus });
      await window.loadAll();
    } catch (e) { console.error('phase status drag failed', e); }
  };

  const phasesByStatus = React.useMemo(() => {
    const m = Object.fromEntries(PHASE_STATUS_ORDER.map((s) => [s, []]));
    for (const ph of PHASES) {
      const s = PHASE_STATUS_ORDER.includes(ph.status) ? ph.status : 'pending';
      m[s].push(ph);
    }
    return m;
  }, [PHASES.length, PHASES.map((p) => p.id + p.status + p.position).join(',')]);

  return (
    <div style={{ flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
      <div style={{ padding: '20px 32px 12px', flexShrink: 0 }}>
        <SectionHead level="h1" title="Fases · Tablero"
          sub="Arrastra una fase para cambiar su estado"
          right={<Button variant="primary" size="md" icon="plus" onClick={() => setShowNewPhase({})}>Nueva fase</Button>} />
      </div>

      <div style={{
        flex: 1, minHeight: 0, padding: '0 24px 24px',
        display: 'grid',
        gridTemplateColumns: `repeat(${PHASE_STATUS_ORDER.length}, minmax(220px, 1fr))`,
        gap: 12, overflowX: 'auto',
      }}>
        {PHASE_STATUS_ORDER.map((s) => {
          const meta = phaseStatusMeta(s);
          const phases = phasesByStatus[s] || [];
          const isTarget = dropTarget === s;
          return (
            <div key={s}
              onDragOver={(e) => { e.preventDefault(); if (dropTarget !== s) setDropTarget(s); }}
              onDragLeave={(e) => { if (e.currentTarget.contains(e.relatedTarget)) return; setDropTarget(null); }}
              onDrop={(e) => { e.preventDefault(); onDropPhase(draggingId, s); }}
              style={{
                display: 'flex', flexDirection: 'column', minHeight: 0,
                background: isTarget ? meta.bg : 'var(--bg-1)',
                border: `1px solid ${isTarget ? meta.accent : 'var(--border)'}`,
                borderRadius: 'var(--r-md)',
                transition: 'background var(--t-fast), border-color var(--t-fast)',
              }}>
              {/* column header */}
              <div style={{
                display: 'flex', alignItems: 'center', gap: 8,
                padding: '12px 14px', borderBottom: '1px solid var(--border)',
                flexShrink: 0,
              }}>
                <span style={{ width: 8, height: 8, borderRadius: '50%', background: meta.accent }} />
                <span style={{ fontSize: 11.5, fontWeight: 600, color: 'var(--text-0)', textTransform: 'uppercase', letterSpacing: 0.6 }}>{meta.label}</span>
                <span className="mono" style={{ fontSize: 10.5, color: 'var(--text-3)', marginLeft: 'auto' }}>{phases.length}</span>
                <button className="btn-reset"
                  onClick={() => setShowNewPhase({ status: s })}
                  title={`Nueva fase en ${meta.label.toLowerCase()}`}
                  style={{ width: 20, height: 20, display: 'flex', alignItems: 'center', justifyContent: 'center', borderRadius: 4, color: 'var(--text-3)' }}
                  onMouseEnter={(e) => { e.currentTarget.style.background = 'var(--bg-2)'; e.currentTarget.style.color = 'var(--text-1)'; }}
                  onMouseLeave={(e) => { e.currentTarget.style.background = 'transparent'; e.currentTarget.style.color = 'var(--text-3)'; }}>
                  <Icon name="plus" size={11} />
                </button>
              </div>
              {/* cards */}
              <div style={{ flex: 1, overflowY: 'auto', padding: 8, display: 'flex', flexDirection: 'column', gap: 8 }}>
                {phases.length === 0 && (
                  <div style={{ padding: '20px 8px', textAlign: 'center', color: 'var(--text-3)', fontSize: 11.5, fontStyle: 'italic' }}>
                    sin fases
                  </div>
                )}
                {phases.map((ph) => (
                  <PhaseCard key={ph.id} phase={ph}
                    isDragging={draggingId === ph.id}
                    onDragStart={() => setDraggingId(ph.id)}
                    onDragEnd={() => { setDraggingId(null); setDropTarget(null); }}
                    onClick={() => setEditPhase(ph)}
                    onOpenTask={onOpenTask}
                  />
                ))}
              </div>
            </div>
          );
        })}
      </div>

      {showNewPhase && <NewPhaseModal initial={showNewPhase} onClose={() => setShowNewPhase(null)} />}
      {editPhase && <NewPhaseModal initial={editPhase} onClose={() => setEditPhase(null)} />}
    </div>
  );
}

function PhaseCard({ phase: ph, isDragging, onDragStart, onDragEnd, onClick }) {
  const pct = ph.total > 0 ? Math.round((ph.done / ph.total) * 100) : 0;
  const meta = phaseStatusMeta(ph.status);
  const fmtDue = (s) => s ? new Date(s + 'T00:00:00Z').toLocaleDateString('es', { day: 'numeric', month: 'short' }) : null;
  return (
    <div
      draggable
      onDragStart={(e) => { e.dataTransfer.setData('text/plain', ph.id); e.dataTransfer.effectAllowed = 'move'; onDragStart(); }}
      onDragEnd={onDragEnd}
      onClick={onClick}
      style={{
        background: 'var(--bg-2)', border: '1px solid var(--border)',
        borderRadius: 6, padding: '10px 12px', cursor: 'grab',
        opacity: isDragging ? 0.4 : 1,
        transition: 'opacity var(--t-fast), border-color var(--t-fast)',
      }}
      onMouseEnter={(e) => { e.currentTarget.style.borderColor = meta.accent; }}
      onMouseLeave={(e) => { e.currentTarget.style.borderColor = 'var(--border)'; }}>
      <div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 6 }}>
        <span className="mono" style={{ fontSize: 10.5, color: meta.color, fontWeight: 600 }}>{ph.id}</span>
        {ph.ownerId && <Avatar actor={ph.ownerId} size={16} style={{ marginLeft: 'auto' }} />}
      </div>
      <div style={{ fontSize: 12.5, fontWeight: 500, color: 'var(--text-0)', lineHeight: 1.3, marginBottom: 8, overflow: 'hidden', display: '-webkit-box', WebkitLineClamp: 2, WebkitBoxOrient: 'vertical' }}>
        {ph.name}
      </div>
      {ph.total > 0 && (
        <div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 6 }}>
          <div style={{ flex: 1, height: 4, background: 'var(--bg-3)', borderRadius: 2, overflow: 'hidden' }}>
            <div style={{ width: `${pct}%`, height: '100%', background: meta.accent, transition: 'width var(--t-mid)' }} />
          </div>
          <span className="mono" style={{ fontSize: 10, color: 'var(--text-3)' }}>{ph.done}/{ph.total}</span>
        </div>
      )}
      <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', fontSize: 10.5, color: 'var(--text-3)' }}>
        {ph.dueAt ? (
          <span style={{ display: 'inline-flex', alignItems: 'center', gap: 4 }}>
            <Icon name="calendar" size={10} /> {fmtDue(ph.dueAt)}
          </span>
        ) : <span />}
        {ph.tags?.length > 0 && (
          <span className="mono">{ph.tags.length} tag{ph.tags.length === 1 ? '' : 's'}</span>
        )}
      </div>
    </div>
  );
}

function TaskCardCompact({ task, onClick }) {
  return (
    <div onClick={onClick} style={{
      background: 'var(--bg-1)', border: '1px solid var(--border)',
      borderRadius: 6, padding: '8px 10px', cursor: 'pointer',
      display: 'flex', alignItems: 'center', gap: 8,
    }}>
      <span className="mono" style={{ fontSize: 10.5, color: 'var(--text-3)', flexShrink: 0 }}>{task.id}</span>
      <span style={{ flex: 1, fontSize: 12.5, color: 'var(--text-0)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{task.title}</span>
      <PriorityChip p={task.prio} />
      <Avatar actor={task.assignee} size={16} />
    </div>
  );
}

// ── Timeline ────────────────────────────────────────────────────────
// Phase-driven Gantt view. Reads PHASES + TASKS; groups tasks by their
// phaseId. Each task with a dueAt inside the visible 14-day window gets a
// 1-day bar on that day. Tasks without dueAt (or outside the window) are
// shown in a "Sin fecha" panel at the bottom of each phase row.
const DAY_MS = 86_400_000;
const TIMELINE_DAY_COUNT = 14;
const TIMELINE_BACKWARD = 2; // days shown before today
function startOfDayUtc(d) { const x = new Date(d); x.setUTCHours(0,0,0,0); return x; }
function dayDiff(a, b) { return Math.round((startOfDayUtc(a) - startOfDayUtc(b)) / DAY_MS); }
function fmtDay(date) {
  const wd = ['D','L','M','X','J','V','S'][date.getUTCDay()];
  return `${wd} ${date.getUTCDate()}`;
}
function TimelineView({ onOpenTask }) {
  const [showNewTask, setShowNewTask] = React.useState(false);
  const today = startOfDayUtc(new Date());
  const days = React.useMemo(() => Array.from({ length: TIMELINE_DAY_COUNT }, (_, i) => {
    const d = new Date(today);
    d.setUTCDate(d.getUTCDate() + i - TIMELINE_BACKWARD);
    return d;
  }), [today.getTime()]);

  const COL_W = 80;
  const ROW_H = 32;
  const LABEL_W = 240;

  const phases = window.PHASES || [];
  const tasks = window.TASKS || [];

  if (phases.length === 0) {
    return (
      <div style={{ flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
        <div style={{ padding: '24px 32px 14px', flexShrink: 0 }}>
          <SectionHead level="h1" title="Timeline"
            sub="Vista temporal — agrupada por fase y posicionada por fecha objetivo."
            right={<Button variant="primary" size="md" icon="plus" onClick={() => setShowNewTask(true)}>Nueva tarea</Button>} />
        </div>
        <div style={{ flex: 1, padding: '0 32px 60px', overflow: 'auto' }}>
          <div style={{
            padding: '60px 24px', textAlign: 'center',
            border: '1px dashed var(--border)', borderRadius: 'var(--r-lg)',
            background: 'rgba(22,25,34,.4)',
          }}>
            <Icon name="gantt-chart" size={28} color="var(--text-3)" />
            <div style={{ fontSize: 14, fontWeight: 600, color: 'var(--text-1)', marginTop: 14 }}>Timeline vacío</div>
            <div style={{ fontSize: 12.5, color: 'var(--text-2)', marginTop: 4, marginBottom: 6 }}>
              Necesitas al menos una fase para que el Timeline tenga contenido. Esta vista no es independiente:
              proyecta las mismas fases del Tracker en una línea de tiempo.
            </div>
          </div>
        </div>
        {showNewTask && <NewTaskModal onClose={() => setShowNewTask(false)} onCreated={(id) => onOpenTask && onOpenTask(id)} />}
      </div>
    );
  }

  const groups = phases.map((p) => ({
    phase: p,
    placed: [], // tasks with dueAt within window
    floating: [], // tasks without dueAt or outside window
  }));
  const phaseIndex = new Map(groups.map((g, i) => [g.phase.id, i]));
  const noPhase = { phase: null, placed: [], floating: [] };

  for (const t of tasks) {
    const target = t.phase && phaseIndex.has(t.phase) ? groups[phaseIndex.get(t.phase)] : noPhase;
    if (t.dueIn != null) {
      const dayIndex = TIMELINE_BACKWARD + t.dueIn;
      if (dayIndex >= 0 && dayIndex < TIMELINE_DAY_COUNT) {
        target.placed.push({ ...t, dayIndex });
        continue;
      }
    }
    target.floating.push(t);
  }
  if (noPhase.placed.length || noPhase.floating.length) groups.push(noPhase);

  const todayIdx = TIMELINE_BACKWARD;

  return (
    <div style={{ flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
      <div style={{ padding: '24px 32px 14px', flexShrink: 0 }}>
        <SectionHead level="h1" title="Timeline"
          sub={`${phases.length} ${phases.length === 1 ? 'fase' : 'fases'} · ventana 14 días alrededor de hoy`}
          right={<Button variant="primary" size="md" icon="plus" onClick={() => setShowNewTask(true)}>Nueva tarea</Button>} />
      </div>
      <div style={{ flex: 1, overflow: 'auto', padding: '0 32px 32px' }}>
        <div style={{
          background: 'var(--bg-1)', border: '1px solid var(--border)', borderRadius: 'var(--r-lg)', overflow: 'hidden',
          minWidth: LABEL_W + COL_W * TIMELINE_DAY_COUNT + 20,
        }}>
          <div style={{ display: 'flex', borderBottom: '1px solid var(--border)', background: 'var(--bg-2)' }}>
            <div style={{ width: LABEL_W, flexShrink: 0, padding: '10px 14px', fontSize: 11, color: 'var(--text-2)', fontWeight: 600, textTransform: 'uppercase', letterSpacing: 0.6 }}>Fase / Tarea</div>
            {days.map((d, i) => (
              <div key={i} style={{
                width: COL_W, flexShrink: 0, padding: '10px 0', textAlign: 'center',
                fontSize: 11.5, color: i === todayIdx ? 'var(--accent)' : 'var(--text-2)', fontWeight: i === todayIdx ? 600 : 500,
                borderLeft: '1px solid var(--border)',
              }}>{fmtDay(d)}</div>
            ))}
          </div>
          <div style={{ position: 'relative' }}>
            <div style={{
              position: 'absolute', top: 0, bottom: 0,
              left: LABEL_W + COL_W * todayIdx + COL_W / 2 - 1,
              width: 2, background: 'rgba(239,68,68,.4)',
              pointerEvents: 'none', zIndex: 5,
            }}>
              <div style={{ position: 'absolute', top: -8, left: -18, padding: '1px 6px', background: 'var(--danger)', color: '#fff', fontSize: 10, borderRadius: 3, fontWeight: 600 }}>HOY</div>
            </div>
            <div style={{ position: 'absolute', top: 0, bottom: 0, left: LABEL_W, right: 0, display: 'flex', pointerEvents: 'none' }}>
              {days.map((_, i) => (
                <div key={i} style={{ width: COL_W, borderLeft: '1px solid var(--border)' }} />
              ))}
            </div>
            {groups.map((g, gi) => {
              // Compute phase range bar position if the phase has dates that
              // overlap the visible 14-day window.
              let phaseBar = null;
              if (g.phase && (g.phase.startsAt || g.phase.dueAt)) {
                const startDate = g.phase.startsAt ? startOfDayUtc(new Date(g.phase.startsAt + 'T00:00:00Z')) : null;
                const endDate = g.phase.dueAt ? startOfDayUtc(new Date(g.phase.dueAt + 'T00:00:00Z')) : null;
                const winStart = days[0];
                const winEnd = days[days.length - 1];
                const rawStart = startDate ?? winStart;
                const rawEnd = endDate ?? winEnd;
                if (rawStart <= winEnd && rawEnd >= winStart) {
                  const from = Math.max(0, dayDiff(rawStart, winStart));
                  const to = Math.min(TIMELINE_DAY_COUNT - 1, dayDiff(rawEnd, winStart));
                  phaseBar = {
                    from, to,
                    truncatedLeft: !!startDate && startDate < winStart,
                    truncatedRight: !!endDate && endDate > winEnd,
                  };
                }
              }
              return (
              <React.Fragment key={g.phase?.id || '_no_phase_' + gi}>
                <div style={{
                  display: 'flex', alignItems: 'stretch', height: 28,
                  background: 'var(--bg-2)', fontSize: 11, fontWeight: 600, color: 'var(--text-2)',
                  textTransform: 'uppercase', letterSpacing: 0.5,
                  borderTop: '1px solid var(--border)',
                  position: 'relative', zIndex: 3,
                }}>
                  <div style={{ width: LABEL_W, flexShrink: 0, display: 'flex', alignItems: 'center', gap: 8, padding: '0 14px' }}>
                    {g.phase ? (<>
                      <span className="mono" style={{ color: 'var(--text-3)' }}>{g.phase.id}</span>
                      <span style={{ color: 'var(--text-1)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{g.phase.name}</span>
                    </>) : (<span>Sin fase</span>)}
                  </div>
                  {/* Phase range bar */}
                  {phaseBar && (
                    <div style={{
                      position: 'absolute',
                      left: LABEL_W + phaseBar.from * COL_W + 4,
                      width: (phaseBar.to - phaseBar.from + 1) * COL_W - 8,
                      top: 6, bottom: 6,
                      background: g.phase.status === 'active' ? 'rgba(79,142,255,.18)' : g.phase.status === 'done' ? 'rgba(16,185,129,.16)' : 'rgba(255,255,255,.06)',
                      border: `1px solid ${g.phase.status === 'active' ? 'var(--accent)' : g.phase.status === 'done' ? 'var(--success)' : 'var(--border-strong)'}`,
                      borderRadius: 4,
                      borderTopLeftRadius: phaseBar.truncatedLeft ? 0 : 4,
                      borderBottomLeftRadius: phaseBar.truncatedLeft ? 0 : 4,
                      borderTopRightRadius: phaseBar.truncatedRight ? 0 : 4,
                      borderBottomRightRadius: phaseBar.truncatedRight ? 0 : 4,
                      display: 'flex', alignItems: 'center',
                      padding: '0 8px',
                      fontSize: 10.5, color: 'var(--text-2)',
                      textTransform: 'none', letterSpacing: 0,
                      pointerEvents: 'none',
                    }}>
                      <span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
                        {g.placed.length + g.floating.length} {g.placed.length + g.floating.length === 1 ? 'tarea' : 'tareas'}
                      </span>
                    </div>
                  )}
                  {/* Tasks counter (when no range bar shown) */}
                  {!phaseBar && (
                    <div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'flex-end', padding: '0 14px' }}>
                      <span className="mono" style={{ color: 'var(--text-3)', textTransform: 'none' }}>
                        {g.placed.length + g.floating.length} {g.placed.length + g.floating.length === 1 ? 'tarea' : 'tareas'}
                      </span>
                    </div>
                  )}
                </div>
                {g.placed.length === 0 && g.floating.length === 0 && (
                  <div style={{ display: 'flex', alignItems: 'center', height: ROW_H, padding: '0 14px', borderTop: '1px solid var(--border)', fontSize: 11.5, color: 'var(--text-3)', fontStyle: 'italic' }}>
                    Sin tareas
                  </div>
                )}
                {g.placed.map((t) => (
                  <div key={t.id} style={{ display: 'flex', height: ROW_H, alignItems: 'center', position: 'relative', borderTop: '1px solid var(--border)' }}>
                    <div style={{ width: LABEL_W, flexShrink: 0, padding: '0 14px', display: 'flex', alignItems: 'center', gap: 8, minWidth: 0 }}>
                      <span className="mono" style={{ fontSize: 10.5, color: 'var(--text-3)' }}>{t.id}</span>
                      <span style={{ fontSize: 12, color: 'var(--text-1)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{t.title}</span>
                    </div>
                    <div onClick={() => onOpenTask && onOpenTask(t.id)} style={{
                      position: 'absolute', left: LABEL_W + t.dayIndex * COL_W + 4, top: 6, height: ROW_H - 12,
                      width: COL_W - 8,
                      background: PRIORITY_META[t.prio]?.bg || 'var(--bg-2)',
                      border: `1px solid ${PRIORITY_META[t.prio]?.border || 'var(--border)'}`,
                      borderRadius: 5, padding: '0 8px',
                      display: 'flex', alignItems: 'center', gap: 6,
                      fontSize: 11.5, color: PRIORITY_META[t.prio]?.color || 'var(--text-1)', fontWeight: 500,
                      cursor: 'pointer', zIndex: 2,
                    }}>
                      <Avatar actor={t.assignee} size={14} />
                      <span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{t.title}</span>
                    </div>
                  </div>
                ))}
                {g.floating.length > 0 && (
                  <div style={{ borderTop: '1px solid var(--border)', padding: '8px 14px', background: 'var(--bg-0)', display: 'flex', flexWrap: 'wrap', gap: 6 }}>
                    <span style={{ fontSize: 10.5, color: 'var(--text-3)', textTransform: 'uppercase', letterSpacing: 0.5, marginRight: 4, alignSelf: 'center' }}>Sin fecha</span>
                    {g.floating.map((t) => (
                      <button key={t.id} className="btn-reset" onClick={() => onOpenTask && onOpenTask(t.id)}
                        style={{
                          display: 'inline-flex', alignItems: 'center', gap: 6,
                          padding: '4px 8px',
                          background: 'var(--bg-1)', border: '1px solid var(--border)', borderRadius: 6,
                          fontSize: 11.5, color: 'var(--text-1)',
                        }}>
                        <span className="mono" style={{ color: 'var(--text-3)' }}>{t.id}</span>
                        <span>{t.title}</span>
                        <Avatar actor={t.assignee} size={12} />
                      </button>
                    ))}
                  </div>
                )}
              </React.Fragment>
            );
            })}
          </div>
        </div>
      </div>
      {showNewTask && <NewTaskModal onClose={() => setShowNewTask(false)} onCreated={(id) => onOpenTask && onOpenTask(id)} />}
    </div>
  );
}

function DepLine({ x1, y1, x2, y2 }) {
  const mid = (x1 + x2) / 2;
  return (
    <g>
      <path d={`M ${x1} ${y1} L ${x1 + 8} ${y1} Q ${x1 + 12} ${y1} ${x1 + 12} ${y1 + 4} L ${x1 + 12} ${y2 - 4} Q ${x1 + 12} ${y2} ${x1 + 16} ${y2} L ${x2} ${y2}`}
        stroke="var(--text-3)" strokeWidth="1" fill="none" strokeDasharray="3 3" />
      <polygon points={`${x2-4},${y2-3} ${x2},${y2} ${x2-4},${y2+3}`} fill="var(--text-3)" />
    </g>
  );
}

// ── My Tasks ────────────────────────────────────────────────────────
function MyTasks({ identity, onOpenTask }) {
  const a = ACTORS[identity];
  const mine = TASKS.filter(t => t.assignee === identity);
  const inProg = mine.filter(t => t.status === 'progress' || t.status === 'review');
  // A task counts as "blocked by me" if any of its blockers is open and assigned to me.
  const blockedByMe = TASKS.filter(t => {
    const ids = Array.isArray(t.blockedBy) ? t.blockedBy : (t.blockedBy ? [t.blockedBy] : []);
    return ids.some((bid) => {
      const blocker = TASKS.find(x => x.id === bid);
      return blocker && blocker.assignee === identity && blocker.status !== 'closed' && blocker.status !== 'discarded';
    });
  });
  const upcoming = mine.filter(t => t.dueIn != null && t.dueIn <= 3);
  // Recent comments are fetched live; the hardcoded mock was removed when the
  // workspace was wiped. Once a per-actor comments feed exists we'll plug it in.
  const comments = [];

  return (
    <div style={{ flex: 1, overflowY: 'auto', padding: '32px 40px 60px' }}>
      <div style={{ maxWidth: 1280, margin: '0 auto' }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 16, marginBottom: 32 }}>
          <Avatar actor={identity} size={56} ring />
          <div>
            <h1 style={{ fontSize: 26, fontWeight: 600, margin: 0, letterSpacing: -0.4 }}>Buen día, {a.name}</h1>
            <div style={{ fontSize: 13, color: 'var(--text-2)', marginTop: 4 }}>
              {inProg.length} en curso · {mine.filter(t => t.status === 'ready').length} listas para empezar
            </div>
          </div>
        </div>

        <div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: 16 }}>
          <MineCard title="En curso" icon="play-circle" count={inProg.length}>
            {inProg.length ? inProg.map(t => <TaskCardCompact key={t.id} task={t} onClick={() => onOpenTask(t.id)} />)
              : <MiniEmpty msg="Nada en curso. Toma una de Listo." />}
          </MineCard>
          <MineCard title="Bloqueadas por mí" icon="ban" count={blockedByMe.length} accent="var(--danger)">
            {blockedByMe.length ? blockedByMe.map(t => <TaskCardCompact key={t.id} task={t} onClick={() => onOpenTask(t.id)} />)
              : <MiniEmpty msg="Nadie está esperando tu input." />}
          </MineCard>
          <MineCard title="Comentarios recientes" icon="message-square" count={comments.length}>
            {comments.length === 0
              ? <MiniEmpty msg="Sin comentarios recientes." />
              : comments.map((c, i) => (
                <div key={i} style={{ display: 'flex', gap: 10, padding: '10px 0', borderBottom: i < comments.length - 1 ? '1px solid var(--border)' : 'none' }}>
                  <Avatar actor={c.actor} size={24} />
                  <div style={{ flex: 1, minWidth: 0 }}>
                    <div style={{ display: 'flex', alignItems: 'baseline', gap: 8, marginBottom: 2 }}>
                      <span style={{ fontSize: 12.5, fontWeight: 500, color: 'var(--text-0)' }}>{ACTORS[c.actor].name}</span>
                      <span className="mono" style={{ fontSize: 10.5, color: 'var(--text-3)' }}>{c.taskId}</span>
                      <span style={{ fontSize: 11, color: 'var(--text-3)' }}>· {relTime(c.mins)}</span>
                    </div>
                    <div style={{ fontSize: 12.5, color: 'var(--text-1)', lineHeight: 1.5 }}>{c.text}</div>
                  </div>
                </div>
              ))
            }
          </MineCard>
          <MineCard title="Próximas fechas" icon="calendar-clock" count={upcoming.length} accent="var(--warn)">
            {upcoming.length ? upcoming.map(t => (
              <div key={t.id} onClick={() => onOpenTask(t.id)} style={{
                display: 'flex', alignItems: 'center', gap: 10,
                padding: '10px 0', borderBottom: '1px solid var(--border)', cursor: 'pointer',
              }}>
                <span className="mono" style={{ fontSize: 10.5, color: 'var(--text-3)', minWidth: 40 }}>{t.id}</span>
                <span style={{ flex: 1, fontSize: 12.5, color: 'var(--text-0)' }}>{t.title}</span>
                <Badge color={t.dueIn <= 0 ? 'var(--danger)' : 'var(--warn)'} bg={t.dueIn <= 0 ? 'rgba(239,68,68,.10)' : 'rgba(245,158,11,.10)'}>
                  {t.dueIn === 0 ? 'hoy' : t.dueIn < 0 ? `${-t.dueIn}d tarde` : `en ${t.dueIn}d`}
                </Badge>
              </div>
            )) : <MiniEmpty msg="Sin fechas urgentes." />}
          </MineCard>
        </div>
      </div>
    </div>
  );
}

function MineCard({ title, icon, count, accent, children }) {
  return (
    <div style={{
      background: 'var(--bg-1)', border: '1px solid var(--border)', borderRadius: 'var(--r-lg)',
      padding: 18,
    }}>
      <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 12 }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
          <div style={{
            width: 28, height: 28, borderRadius: 7,
            background: accent ? `color-mix(in srgb, ${accent} 14%, transparent)` : 'var(--bg-2)',
            color: accent || 'var(--text-1)',
            display: 'flex', alignItems: 'center', justifyContent: 'center',
          }}>
            <Icon name={icon} size={14} />
          </div>
          <span style={{ fontSize: 14, fontWeight: 600 }}>{title}</span>
        </div>
        <span className="mono" style={{ fontSize: 11.5, color: 'var(--text-2)' }}>{count}</span>
      </div>
      <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>{children}</div>
    </div>
  );
}

function MiniEmpty({ msg }) {
  return <div style={{ padding: '14px 0', fontSize: 12.5, color: 'var(--text-3)', textAlign: 'center', fontStyle: 'italic' }}>{msg}</div>;
}

Object.assign(window, { Kanban, PhasesView, PhasesKanban, TimelineView, MyTasks, TaskCard, TaskCardCompact });
