// ─── Projetecture ─── editable project mind maps with pan/zoom, subtasks,
// sprint launch, and progress tracking. Constant-size text labels stay
// readable at any zoom level — only positions scale.

const { useState, useEffect, useRef, useCallback, useLayoutEffect } = React;
const useLFState = window.useLFState;
const MODES = window.MODES;
const modeOf = window.modeOf;
const taskProgress = window.taskProgress;
const milestoneProgress = window.milestoneProgress;
const projectProgress = window.projectProgress;
const defaultTask = window.defaultTask;
const defaultSubtask = window.defaultSubtask;
const updateTaskById = window.updateTaskById;

// Layout constants — canvas-space
const PJ_W = 1600, PJ_H = 900, PJ_CX = 800, PJ_CY = 450;
const PJ_DEG = Math.PI / 180;
const PJ_R_MAIN = 280;
const PJ_R_LEAF = 180;
const PJ_LEAF_SPACING = 60;

const PJ_LEGACY_KEY = 'kb-projetecture-v1';

const PJ_DEFAULT_PROJECT_COLOR = '#3B82F6';
const PJ_DEFAULT_BRANCH_COLOR = '#3B82F6';

function pjBezier(x1, y1, x2, y2, t = 0.4) {
  const dx = x2 - x1, dy = y2 - y1;
  return `M${x1} ${y1} C${x1 + dx * t} ${y1 + dy * t} ${x2 - dx * t} ${y2 - dy * t} ${x2} ${y2}`;
}
function pjUid() { return Math.random().toString(36).slice(2, 9); }

function pjSeedProjects() {
  return [
    {
      id: pjUid(), name: 'Laminar Flow', color: '#8B5CF6', status: 'active', createdAt: Date.now(),
      branches: [
        {
          id: pjUid(), label: 'V1 Tasks', color: '#4C1D95',
          leaves: [
            { id: pjUid(), label: 'Pan + zoom + constant text', notes: '', done: true, mode: 'production', today: false, subtasks: [], images: [] },
            { id: pjUid(), label: 'Subtasks with checkboxes', notes: '', done: true, mode: 'production', today: false, subtasks: [], images: [] },
            { id: pjUid(), label: 'Sprint full-screen overlay', notes: '', done: true, mode: 'production', today: false, subtasks: [], images: [] },
          ],
        },
        {
          id: pjUid(), label: 'AI Chief of Staff', color: '#8B5CF6',
          leaves: [
            { id: pjUid(), label: 'Wire Claude API', notes: '', done: true, mode: 'production', today: false, subtasks: [], images: [] },
            { id: pjUid(), label: 'AI auto-creates entries', notes: 'Tool-use roundtrip — call structured update on lf.projects.', done: false, mode: 'lab', today: true, subtasks: [
              { id: pjUid(), title: 'Define tool schema for branch/leaf creation', done: false },
              { id: pjUid(), title: 'Wire tool-use loop in callClaude', done: false },
            ], images: [] },
          ],
        },
      ],
    },
  ];
}

function pjMigrateLegacy() {
  try {
    const legacyRaw = localStorage.getItem(PJ_LEGACY_KEY);
    if (!legacyRaw) return null;
    const legacy = JSON.parse(legacyRaw);
    if (!legacy?.branches) return null;
    return [{
      id: pjUid(), name: legacy.center || 'Project', color: PJ_DEFAULT_PROJECT_COLOR,
      status: 'active', createdAt: Date.now(),
      branches: legacy.branches.map((b) => ({
        ...b,
        leaves: (b.leaves || []).map((lf) => ({ ...defaultTask(lf.label), ...lf, done: !!lf.done, subtasks: lf.subtasks || [], images: lf.images || [] })),
      })),
    }];
  } catch { return null; }
}

function pjLoadProjects() {
  try {
    const raw = localStorage.getItem('lf.projects');
    if (raw) {
      const parsed = JSON.parse(raw);
      if (Array.isArray(parsed) && parsed.length) return parsed;
    }
  } catch {}
  const migrated = pjMigrateLegacy();
  if (migrated) return migrated;
  return pjSeedProjects();
}

// Compute layout positions in canvas-space (pre-scale). Tasks fan out on an
// arc instead of a straight line — repels overlap when many tasks per branch.
// Archived milestones are dropped from the layout slot allocation but their
// positions can optionally be appended afterwards by the renderer for display.
function pjLayout(branches, includeArchived = false) {
  const live = branches.filter((b) => !b.archived);
  const archived = branches.filter((b) => b.archived);
  const n = live.length || 1;
  const liveLaid = live.map((b, i) => {
    const angle = (i * 360) / n - 90;
    const main = {
      x: PJ_CX + Math.cos(angle * PJ_DEG) * PJ_R_MAIN,
      y: PJ_CY + Math.sin(angle * PJ_DEG) * PJ_R_MAIN,
    };
    const total = (b.leaves || []).length;
    // Fan arc width, capped by neighboring branches' angular slice (so leaves
    // from adjacent milestones don't collide).
    const maxSpan = Math.min(150, (360 / n) * 0.85);
    const span = Math.min(maxSpan, Math.max(0, total - 1) * 18);
    const step = total <= 1 ? 0 : span / (total - 1);
    // Leaf distance scales modestly with count so dense branches push outward
    const leafDist = PJ_R_LEAF + Math.max(0, total - 4) * 12;
    const leaves = (b.leaves || []).map((lf, j) => {
      const phi = angle + (total <= 1 ? 0 : (j - (total - 1) / 2) * step);
      return {
        ...lf,
        x: main.x + Math.cos(phi * PJ_DEG) * leafDist,
        y: main.y + Math.sin(phi * PJ_DEG) * leafDist,
      };
    });
    return { ...b, angle, main, leaves };
  });
  if (!includeArchived) return liveLaid;
  // Layer archived in a tight outer ring so they're visible but pushed back
  const archivedLaid = archived.map((b, i) => {
    const angle = (i * 360) / (archived.length || 1) - 90;
    const R = PJ_R_MAIN + 380;
    const main = {
      x: PJ_CX + Math.cos(angle * PJ_DEG) * R,
      y: PJ_CY + Math.sin(angle * PJ_DEG) * R,
    };
    return { ...b, angle, main, leaves: [] }; // tasks hidden when archived
  });
  return [...liveLaid, ...archivedLaid];
}

// ─── IMAGE LIGHTBOX ─────────────────────────────────────────────────
// Click any pasted image → fullscreen viewer with scroll-to-zoom & drag-pan.
function ImageLightbox({ src, onClose }) {
  const [zoom, setZoom] = useState(1);
  const [pan, setPan] = useState({ x: 0, y: 0 });
  const [dragging, setDragging] = useState(null);
  useEffect(() => {
    const h = (e) => { if (e.key === 'Escape') onClose(); };
    window.addEventListener('keydown', h);
    return () => window.removeEventListener('keydown', h);
  }, [onClose]);
  const onWheel = (e) => {
    e.preventDefault();
    setZoom((z) => Math.max(0.2, Math.min(10, z * (1 + -e.deltaY * 0.0015))));
  };
  const wrapRef = useRef(null);
  useEffect(() => {
    const el = wrapRef.current;
    if (!el) return;
    el.addEventListener('wheel', onWheel, { passive: false });
    return () => el.removeEventListener('wheel', onWheel);
  }, []);
  return (
    <div ref={wrapRef} onClick={onClose} style={{
      position: 'fixed', inset: 0, zIndex: 1100,
      background: 'rgba(0,0,0,0.95)', display: 'flex', alignItems: 'center', justifyContent: 'center',
      cursor: dragging ? 'grabbing' : 'zoom-out', overflow: 'hidden',
    }}>
      <img src={src} alt=""
        onMouseDown={(e) => { e.stopPropagation(); setDragging({ sx: e.clientX, sy: e.clientY, px: pan.x, py: pan.y }); }}
        onMouseMove={(e) => { if (dragging) setPan({ x: dragging.px + (e.clientX - dragging.sx), y: dragging.py + (e.clientY - dragging.sy) }); }}
        onMouseUp={() => setDragging(null)}
        onMouseLeave={() => setDragging(null)}
        onClick={(e) => e.stopPropagation()}
        style={{ maxWidth: '90vw', maxHeight: '90vh', transform: `translate(${pan.x}px, ${pan.y}px) scale(${zoom})`, transition: dragging ? 'none' : 'transform 0.15s', imageRendering: zoom > 2 ? 'pixelated' : 'auto', userSelect: 'none' }}
      />
      <div style={{ position: 'fixed', top: 20, right: 20, color: '#888', fontSize: 12, fontWeight: 600, letterSpacing: '0.1em', textTransform: 'uppercase' }}>
        {Math.round(zoom * 100)}% · scroll to zoom · drag to pan · esc/click to close
      </div>
      <button onClick={onClose} style={{ position: 'fixed', top: 20, left: 20, background: 'transparent', border: '1px solid #333', color: '#888', borderRadius: 7, padding: '6px 14px', fontSize: 12, fontWeight: 600, cursor: 'pointer', fontFamily: 'inherit' }}>✕ Close</button>
    </div>
  );
}
window.ImageLightbox = ImageLightbox;

// ─── RICH TEXT EDITOR ──────────────────────────────────────────────
// Contenteditable with a formatting toolbar. Uses document.execCommand.
// Selection is captured before opening color/highlight pickers and restored
// before applying the command — otherwise clicking a picker blurs the editor
// and the format applies to nothing.
// WCAG-style relative luminance — drives the highlight auto-invert logic.
function relativeLuminance(hex) {
  const h = String(hex || '').replace('#', '').trim();
  if (h.length < 6) return 0.5;
  const r = parseInt(h.slice(0, 2), 16) / 255;
  const g = parseInt(h.slice(2, 4), 16) / 255;
  const b = parseInt(h.slice(4, 6), 16) / 255;
  if (isNaN(r) || isNaN(g) || isNaN(b)) return 0.5;
  const lin = (c) => (c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4));
  return 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b);
}
window.relativeLuminance = relativeLuminance;

function RichEditor({ value, onChange, onPaste, placeholder, color, minHeight = 120, formattingDefault = false }) {
  const ref = useRef(null);
  // Sentinel ref — used by handleInput to remember what the editor's HTML
  // was set to most recently. NOT a "lastValue" gate on innerHTML updates
  // (that was the bug: useRef(value) meant the initial-mount comparison
  // was always equal, so non-empty initial values never got rendered).
  const lastValue = useRef(null);
  const savedRange = useRef(null);
  // Toolbar is collapsed by default — user wanted formatting hidden until
  // explicitly revealed via the "Show formatting" button.
  const [showFormat, setShowFormat] = useState(!!formattingDefault);
  // null | 'text' | 'hilite' — which color picker is open
  const [pickerMode, setPickerMode] = useState(null);
  const [pickedColor, setPickedColor] = useState(null);

  useEffect(() => {
    // Sync innerHTML whenever it doesn't match the value prop. Covers both
    // initial mount (innerHTML starts empty) and external value changes.
    // Doesn't clobber the user's in-progress edits because handleInput
    // fires onChange first, which makes value match innerHTML again
    // before this effect runs.
    if (ref.current && ref.current.innerHTML !== (value || '')) {
      ref.current.innerHTML = value || '';
      lastValue.current = value;
    }
  }, [value]);

  const saveSel = () => {
    const sel = window.getSelection();
    if (sel && sel.rangeCount && ref.current && ref.current.contains(sel.anchorNode)) {
      savedRange.current = sel.getRangeAt(0).cloneRange();
    }
  };
  const restoreSel = () => {
    if (!savedRange.current) return;
    ref.current?.focus();
    const sel = window.getSelection();
    sel.removeAllRanges();
    sel.addRange(savedRange.current);
  };

  const exec = (cmd, arg) => {
    ref.current?.focus();
    document.execCommand(cmd, false, arg);
    handleInput();
  };
  const handleInput = () => {
    const html = ref.current?.innerHTML || '';
    lastValue.current = html;
    onChange(html);
  };

  // Curated palettes — bright + dark options arranged in a perceivable spectrum
  const HILITE_PALETTE = [
    '#FFEB3B', '#FFC107', '#FF9800', '#F44336', '#E91E63', '#9C27B0',
    '#3F51B5', '#2196F3', '#00BCD4', '#4CAF50', '#8BC34A', '#FFFFFF',
  ];
  const TEXT_PALETTE = [
    '#FFFFFF', '#E5E5E5', '#9CA3AF', '#000000',
    '#FCD34D', '#FB923C', '#F87171', '#F472B6',
    '#A78BFA', '#60A5FA', '#22D3EE', '#4ADE80',
  ];

  const openPicker = (mode) => { saveSel(); setPickedColor(null); setPickerMode(mode); };
  const cancelPick = () => { setPickerMode(null); setPickedColor(null); };
  const applyPick = () => {
    if (!pickedColor || !pickerMode) return;
    restoreSel();
    if (pickerMode === 'text') {
      document.execCommand('foreColor', false, pickedColor);
    } else {
      document.execCommand('hiliteColor', false, pickedColor);
      // Auto-invert text when contrast against the highlight would be poor.
      // Light highlight → flip text to black. Dark highlight → flip to white.
      const lum = relativeLuminance(pickedColor);
      if (lum > 0.55) document.execCommand('foreColor', false, '#000000');
      else if (lum < 0.20) document.execCommand('foreColor', false, '#FFFFFF');
    }
    handleInput();
    cancelPick();
  };

  const tbBtn = (label, onClick, title, active) => (
    <button type="button" onMouseDown={(e) => { e.preventDefault(); saveSel(); }} onClick={onClick} title={title}
      style={{
        background: active ? color + '22' : 'transparent', color: active ? color : '#888',
        border: '1px solid #1c1c1c', borderRadius: 5, padding: '4px 8px',
        fontSize: 11, fontWeight: 700, cursor: 'pointer', fontFamily: 'inherit', minWidth: 26, height: 26,
      }}>
      {label}
    </button>
  );

  return (
    <div style={{ background: '#050505', border: '1px solid #1c1c1c', borderRadius: 8, overflow: 'hidden' }}>
      {/* Always-visible header: just the toggle when collapsed, full toolbar when expanded */}
      <div style={{ display: 'flex', flexWrap: 'wrap', gap: 4, padding: 6, borderBottom: (showFormat || pickerMode) ? '1px solid #161616' : 'none', background: '#0a0a0a', alignItems: 'center' }}>
        <button type="button" onClick={() => { setShowFormat((s) => !s); if (showFormat) cancelPick(); }}
          title={showFormat ? 'Hide formatting options' : 'Show formatting options'}
          style={{
            background: showFormat ? color + '22' : 'transparent',
            color: showFormat ? color : '#666',
            border: `1px solid ${showFormat ? color + '55' : '#1c1c1c'}`,
            borderRadius: 5, padding: '4px 10px', fontSize: 10, fontWeight: 700,
            cursor: 'pointer', fontFamily: 'inherit', height: 26, letterSpacing: '0.04em',
          }}>
          {showFormat ? '▴ Hide formatting' : '▾ Show formatting'}
        </button>
        {showFormat && (
          <>
            {tbBtn(<b>B</b>, () => exec('bold'), 'Bold')}
            {tbBtn(<i>I</i>, () => exec('italic'), 'Italic')}
            {tbBtn(<u>U</u>, () => exec('underline'), 'Underline')}
            {tbBtn(<s>S</s>, () => exec('strikeThrough'), 'Strikethrough')}
            <div style={{ width: 1, background: '#222', margin: '0 2px', height: 18 }} />
            {tbBtn('A−', () => exec('fontSize', '2'), 'Smaller')}
            {tbBtn('A', () => exec('fontSize', '4'), 'Default size')}
            {tbBtn('A+', () => exec('fontSize', '6'), 'Larger')}
            <div style={{ width: 1, background: '#222', margin: '0 2px', height: 18 }} />
            <button type="button" onMouseDown={(e) => e.preventDefault()} onClick={() => openPicker('text')}
              style={{ background: pickerMode === 'text' ? color + '22' : 'transparent', color: pickerMode === 'text' ? color : '#888', border: '1px solid #1c1c1c', borderRadius: 5, padding: '4px 8px', fontSize: 11, fontWeight: 700, fontFamily: 'inherit', height: 26, display: 'inline-flex', alignItems: 'center', gap: 4, cursor: 'pointer' }}
              title="Text color (pick → apply)">
              <span style={{ width: 12, height: 12, borderRadius: 2, background: pickerMode === 'text' && pickedColor ? pickedColor : color }} /> Color
            </button>
            <button type="button" onMouseDown={(e) => e.preventDefault()} onClick={() => openPicker('hilite')}
              style={{ background: pickerMode === 'hilite' ? '#EAB30822' : 'transparent', color: pickerMode === 'hilite' ? '#EAB308' : '#888', border: '1px solid #1c1c1c', borderRadius: 5, padding: '4px 8px', fontSize: 11, fontWeight: 700, fontFamily: 'inherit', height: 26, display: 'inline-flex', alignItems: 'center', gap: 4, cursor: 'pointer' }}
              title="Highlight (pick → apply, text auto-inverts on low contrast)">
              <span style={{ width: 12, height: 12, borderRadius: 2, background: pickerMode === 'hilite' && pickedColor ? pickedColor : '#EAB308' }} /> Highlight
            </button>
            <div style={{ width: 1, background: '#222', margin: '0 2px', height: 18 }} />
            {tbBtn('• List', () => exec('insertUnorderedList'), 'Bulleted list')}
            {tbBtn('1. List', () => exec('insertOrderedList'), 'Numbered list')}
            {tbBtn('☐ Check', () => exec('insertHTML', '<div><input type="checkbox"> </div>'), 'Checkbox')}
            {tbBtn('Clear', () => exec('removeFormat'), 'Clear formatting')}
          </>
        )}
      </div>

      {/* Color/highlight picker drawer — pick a swatch then hit Apply.
          Auto-inversion preview shown for highlights. */}
      {pickerMode && (
        <div style={{ padding: '10px 10px 12px', background: '#080808', borderBottom: '1px solid #161616', display: 'flex', flexDirection: 'column', gap: 8 }}>
          <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
            <div style={{ fontSize: 9, color: '#888', letterSpacing: '0.12em', textTransform: 'uppercase', fontWeight: 700 }}>
              {pickerMode === 'text' ? 'Pick text color' : 'Pick highlight'}
            </div>
            <div style={{ flex: 1 }} />
            <button type="button" onClick={cancelPick}
              style={{ background: 'transparent', border: '1px solid #1c1c1c', borderRadius: 5, color: '#666', fontSize: 10, fontFamily: 'inherit', cursor: 'pointer', padding: '4px 10px', fontWeight: 600 }}>
              Cancel
            </button>
            <button type="button" onMouseDown={(e) => e.preventDefault()} onClick={applyPick} disabled={!pickedColor}
              style={{
                background: pickedColor || '#1c1c1c',
                color: pickedColor && relativeLuminance(pickedColor) > 0.5 ? '#000' : '#fff',
                border: 'none', borderRadius: 5, fontSize: 10, fontWeight: 800, fontFamily: 'inherit',
                cursor: pickedColor ? 'pointer' : 'default', padding: '4px 14px', letterSpacing: '0.08em',
                opacity: pickedColor ? 1 : 0.35,
              }}>
              Apply
            </button>
          </div>
          <div style={{ display: 'grid', gridTemplateColumns: 'repeat(12, 1fr)', gap: 4 }}>
            {(pickerMode === 'text' ? TEXT_PALETTE : HILITE_PALETTE).map((c) => (
              <button key={c} type="button" onMouseDown={(e) => e.preventDefault()} onClick={() => setPickedColor(c)} title={c}
                style={{
                  aspectRatio: '1/1', borderRadius: 4, background: c,
                  border: `2px solid ${pickedColor === c ? '#fff' : 'transparent'}`,
                  cursor: 'pointer', padding: 0,
                  transform: pickedColor === c ? 'scale(1.12)' : 'scale(1)',
                  transition: 'transform 0.1s, border-color 0.1s',
                  boxShadow: pickedColor === c ? '0 0 8px rgba(255,255,255,0.35)' : 'none',
                }} />
            ))}
          </div>
          {pickerMode === 'hilite' && pickedColor && (() => {
            const lum = relativeLuminance(pickedColor);
            const inverts = lum > 0.55 || lum < 0.20;
            const textOn = lum > 0.55 ? '#000' : (lum < 0.20 ? '#fff' : '#e8e8e8');
            return (
              <div style={{ fontSize: 10, color: '#888', display: 'flex', alignItems: 'center', gap: 8 }}>
                <span style={{ background: pickedColor, color: textOn, padding: '2px 8px', borderRadius: 3, fontWeight: 700, fontSize: 11 }}>
                  Preview text
                </span>
                {inverts ? (
                  <span>text auto-inverts to {lum > 0.55 ? 'BLACK' : 'WHITE'} for legibility</span>
                ) : (
                  <span>contrast OK — text stays as-is</span>
                )}
              </div>
            );
          })()}
        </div>
      )}

      <div
        ref={ref}
        className="lf-richtext"
        contentEditable
        suppressContentEditableWarning
        onMouseUp={saveSel}
        onKeyUp={saveSel}
        onInput={handleInput}
        onPaste={onPaste}
        onKeyDown={(e) => {
          // Enter inside a checkbox line → auto-insert another checkbox.
          // Enter on an EMPTY checkbox line → exit the checklist.
          // Shift+Enter still creates a plain line break.
          if (e.key !== 'Enter' || e.shiftKey) return;
          const sel = window.getSelection();
          if (!sel || !sel.rangeCount) return;
          let block = sel.anchorNode;
          if (block && block.nodeType === 3) block = block.parentNode;
          while (block && block !== ref.current && !['DIV', 'P', 'LI'].includes(block.tagName)) {
            block = block.parentNode;
          }
          if (!block || block === ref.current) return;
          // Is the first interactive child a checkbox?
          let firstCheckbox = null;
          for (const child of block.childNodes) {
            if (child.nodeType === 1 && child.tagName === 'INPUT' && child.type === 'checkbox') { firstCheckbox = child; break; }
            if (child.nodeType === 1 && child.tagName !== 'INPUT') break;
            if (child.nodeType === 3 && child.textContent.trim() !== '') break;
          }
          if (!firstCheckbox) return;
          e.preventDefault();
          const textPart = (block.textContent || '').trim();
          if (textPart === '') {
            // Empty checkbox line → exit checklist: remove this line, insert plain line
            const next = document.createElement('div');
            next.innerHTML = '<br>';
            block.parentNode?.insertBefore(next, block.nextSibling);
            block.parentNode?.removeChild(block);
            // Place cursor in the new empty div
            const range = document.createRange();
            range.setStart(next, 0);
            range.collapse(true);
            sel.removeAllRanges();
            sel.addRange(range);
          } else {
            // Insert a fresh checkbox line after this one
            document.execCommand('insertHTML', false, '<div><input type="checkbox"> </div>');
          }
          handleInput();
        }}
        data-placeholder={placeholder}
        style={{
          minHeight, padding: 12, color: '#e8e8e8', fontFamily: 'inherit', fontSize: 13, lineHeight: 1.6, outline: 'none',
        }}
      />
    </div>
  );
}
window.RichEditor = RichEditor;

// ─── TASK EDITOR MODAL ──────────────────────────────────────────────
// Used by both Projetecture (when leaf clicked) and the Tasks tab.
function TaskEditor({ projectId, milestoneId, milestoneColor, milestoneLabel, task, onClose, onDelete }) {
  // Look up the live project + milestone so the breadcrumb in the editor
  // header uses the actual colors set in Projetecture (consistency with
  // the mind map). Falls back to the passed-in props if state lookup
  // misses for any reason.
  const _projects = window.LF?.get?.('projects') || [];
  const _projectObj = _projects.find((p) => p.id === projectId);
  const _milestoneObj = _projectObj?.branches?.find((b) => b.id === milestoneId);
  const headerProjectName = _projectObj?.name || (milestoneLabel || '').split(' › ')[0] || '';
  const headerProjectColor = _projectObj?.color || milestoneColor || '#888';
  const headerMilestoneName = _milestoneObj?.label || (milestoneLabel || '').split(' › ').slice(1).join(' › ') || '';
  const headerMilestoneColor = _milestoneObj?.color || milestoneColor || '#888';
  const milestoneDueDate = _milestoneObj?.dueDate || null;
  const [draft, setDraft] = useState({
    label: task.label || '',
    notes: task.notes || '',
    mode: task.mode || 'production',
    priority: task.priority || 'medium',
    today: !!task.today,
    done: !!task.done,
    dueDate: task.dueDate || null,
    subtasks: task.subtasks || [],
    images: task.images || [],
  });
  const [launching, setLaunching] = useState(null);
  const [lightbox, setLightbox] = useState(null);

  // Track which fields the user has touched. While a field is "clean" (not
  // touched in this modal session), external updates from the AI tool flow
  // through and refresh the draft. Once the user types in a field, internal
  // draft owns it — last-write-wins to protect in-progress edits.
  const touched = useRef({ label: false, notes: false, mode: false, priority: false, today: false, done: false, dueDate: false, subtasks: false });

  // Sync external (AI/tool) updates into the draft for fields the user hasn't touched.
  useEffect(() => {
    setDraft((d) => {
      const next = { ...d };
      if (!touched.current.label && task.label !== d.label) next.label = task.label || '';
      if (!touched.current.notes && (task.notes || '') !== d.notes) next.notes = task.notes || '';
      if (!touched.current.mode && task.mode && task.mode !== d.mode) next.mode = task.mode;
      if (!touched.current.priority && task.priority && task.priority !== d.priority) next.priority = task.priority;
      if (!touched.current.today && !!task.today !== d.today) next.today = !!task.today;
      if (!touched.current.done && !!task.done !== d.done) next.done = !!task.done;
      if (!touched.current.dueDate && (task.dueDate || null) !== d.dueDate) next.dueDate = task.dueDate || null;
      if (!touched.current.subtasks && task.subtasks !== d.subtasks) next.subtasks = task.subtasks || [];
      return next;
    });
  }, [task.label, task.notes, task.mode, task.priority, task.today, task.done, task.dueDate, task.subtasks]);

  // Persist draft to backing store on every change so cross-component views stay in sync
  useEffect(() => {
    updateTaskById(projectId, milestoneId, task.id, (t) => ({ ...t, ...draft }));
  }, [draft, projectId, milestoneId, task.id]);

  // No keyboard closer — user explicitly wants the editor to only close via
  // backdrop click or the Done button. Escape was the most likely culprit
  // for the "closes on typing" bug since it's easy to brush accidentally.

  const m = modeOf(draft.mode);
  const progress = taskProgress({ ...task, ...draft });

  // Mark a field as touched so AI writes stop overwriting it
  const setField = (field, value) => {
    touched.current[field] = true;
    setDraft((d) => ({ ...d, [field]: value }));
  };
  // Legacy patch helper (touches every patched field)
  const set = (patch) => {
    Object.keys(patch).forEach((k) => { touched.current[k] = true; });
    setDraft((d) => ({ ...d, ...patch }));
  };
  // When the task's done flag flips, cascade to all subtasks.
  const setDone = (done) => {
    touched.current.done = true;
    touched.current.subtasks = true;
    setDraft((d) => ({ ...d, done, subtasks: (d.subtasks || []).map((s) => ({ ...s, done })) }));
  };
  const addSub = () => set({ subtasks: [...draft.subtasks, defaultSubtask()] });
  const updateSub = (id, p) => set({ subtasks: draft.subtasks.map((s) => s.id === id ? { ...s, ...p } : s) });
  const deleteSub = (id) => set({ subtasks: draft.subtasks.filter((s) => s.id !== id) });

  const onPaste = (e) => {
    const items = e.clipboardData?.items || [];
    for (const it of items) {
      if (it.type?.startsWith('image/')) {
        const f = it.getAsFile();
        const reader = new FileReader();
        reader.onload = () => set({ images: [...draft.images, reader.result] });
        reader.readAsDataURL(f);
        e.preventDefault();
      }
    }
  };

  const sprintTask = () => {
    setLaunching({
      projectId, milestoneId, taskId: task.id,
      label: draft.label || 'Untitled task',
      defaultMode: draft.mode,
    });
  };
  const sprintSubtask = (sub) => {
    setLaunching({
      projectId, milestoneId, taskId: task.id, subtaskId: sub.id,
      label: sub.title || 'Untitled sub-task',
      defaultMode: draft.mode,
    });
  };

  return (
    <>
    <div onClick={onClose} style={{
      position: 'fixed', inset: 0, zIndex: 200,
      background: 'rgba(3,5,12,0.85)', backdropFilter: 'blur(10px)',
      display: 'flex', alignItems: 'center', justifyContent: 'center',
      animation: 'fadeIn 0.18s ease',
    }}>
      <div onClick={(e) => e.stopPropagation()} onPaste={onPaste} style={{
        width: 640, maxWidth: '94vw', maxHeight: '88vh', overflowY: 'auto',
        background: '#0a0a0a', borderRadius: 14,
        border: `1px solid ${m.color}40`,
        boxShadow: `0 0 0 1px ${m.color}10, 0 30px 60px rgba(0,0,0,0.8)`,
        animation: 'slideUp 0.22s cubic-bezier(0.16,1,0.3,1)',
      }}>
        {/* Header */}
        <div style={{ padding: '16px 22px', borderBottom: '1px solid #161616', display: 'flex', alignItems: 'center', gap: 10, position: 'sticky', top: 0, background: '#0a0a0a', zIndex: 1 }}>
          <input type="checkbox" checked={draft.done} onChange={(e) => setDone(e.target.checked)}
            style={{ width: 18, height: 18, accentColor: m.color, cursor: 'pointer' }} />
          <div style={{ display: 'flex', flexDirection: 'column', flex: 1, minWidth: 0 }}>
            <div style={{ fontSize: 9, fontWeight: 700, letterSpacing: '0.14em', textTransform: 'uppercase', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
              <span style={{ color: headerProjectColor }}>{headerProjectName}</span>
              {headerMilestoneName && <>
                <span style={{ color: '#222', margin: '0 7px' }}>›</span>
                <span style={{ color: headerMilestoneColor }}>{headerMilestoneName}</span>
              </>}
            </div>
            <input
              value={draft.label}
              onChange={(e) => set({ label: e.target.value })}
              placeholder="Task title"
              // Auto-focus + select when opening a freshly-created task so
              // the user can just start typing without clicking the input.
              autoFocus={task.label === 'New task' || !task.label}
              onFocus={(e) => { if (task.label === 'New task' || !task.label) e.target.select(); }}
              style={{ background: 'transparent', border: 'none', outline: 'none', color: draft.done ? '#666' : '#e8e8e8', fontFamily: 'inherit', fontSize: 17, fontWeight: 700, width: '100%', textDecoration: draft.done ? 'line-through' : 'none' }}
            />
          </div>
          <button onClick={onClose} style={{ background: 'transparent', border: '1px solid #222', borderRadius: 7, color: '#666', fontSize: 14, cursor: 'pointer', width: 30, height: 30 }}>×</button>
        </div>

        {/* Action bar */}
        <div style={{ padding: '12px 22px', display: 'flex', gap: 8, alignItems: 'center', borderBottom: '1px solid #161616', flexWrap: 'wrap' }}>
          <button onClick={sprintTask} style={{ background: m.color, color: '#fff', border: 'none', borderRadius: 7, padding: '7px 14px', fontSize: 12, fontWeight: 700, cursor: 'pointer', fontFamily: 'inherit' }}>▶ Sprint task</button>
          <button onClick={() => set({ today: !draft.today })} style={{ background: draft.today ? '#EAB30822' : 'transparent', color: draft.today ? '#EAB308' : '#666', border: `1px solid ${draft.today ? '#EAB30855' : '#222'}`, borderRadius: 7, padding: '7px 12px', fontSize: 11, fontWeight: 600, cursor: 'pointer', fontFamily: 'inherit' }}>★ {draft.today ? 'On today\'s mission' : 'Add to today'}</button>
          <div style={{ marginLeft: 'auto', fontSize: 11, color: '#666' }}>{progress}% complete</div>
          {progress > 0 && (
            <div style={{ width: 80, height: 4, background: '#161616', borderRadius: 2, overflow: 'hidden' }}>
              <div style={{ height: '100%', width: `${progress}%`, background: m.color, transition: 'width 0.3s' }} />
            </div>
          )}
        </div>

        {/* Mode picker */}
        <div style={{ padding: '14px 22px', borderBottom: '1px solid #161616' }}>
          <div style={{ fontSize: 9, fontWeight: 700, color: '#444', letterSpacing: '0.12em', textTransform: 'uppercase', marginBottom: 8 }}>Mode</div>
          <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(120px, 1fr))', gap: 6 }}>
            {MODES.map((mm) => (
              <button key={mm.id} onClick={() => set({ mode: mm.id })} style={{
                padding: '6px 10px', borderRadius: 6,
                border: `1.5px solid ${draft.mode === mm.id ? mm.color : '#1c1c1c'}`,
                background: draft.mode === mm.id ? `${mm.color}22` : 'transparent',
                color: draft.mode === mm.id ? mm.color : '#888',
                cursor: 'pointer', fontFamily: 'inherit',
                display: 'flex', alignItems: 'center', gap: 6, fontSize: 10, fontWeight: 600, textAlign: 'left',
              }}>
                <div style={{ width: 7, height: 7, borderRadius: '50%', background: mm.color, flexShrink: 0 }} />
                {mm.label}
              </button>
            ))}
          </div>
        </div>

        {/* Priority */}
        <div style={{ padding: '14px 22px', borderBottom: '1px solid #161616', display: 'flex', alignItems: 'center', gap: 12 }}>
          <div style={{ fontSize: 9, fontWeight: 700, color: '#444', letterSpacing: '0.12em', textTransform: 'uppercase' }}>Priority</div>
          <div style={{ display: 'flex', gap: 6 }}>
            {[['high', '#EF4444'], ['medium', '#F59E0B'], ['low', '#22C55E']].map(([p, c]) => (
              <button key={p} onClick={() => set({ priority: p })} style={{
                padding: '4px 12px', borderRadius: 6,
                border: `1.5px solid ${draft.priority === p ? c : '#1c1c1c'}`,
                background: draft.priority === p ? `${c}22` : 'transparent',
                color: draft.priority === p ? c : '#888',
                cursor: 'pointer', fontFamily: 'inherit', fontSize: 11, fontWeight: 700, textTransform: 'capitalize',
              }}>{p}</button>
            ))}
          </div>
        </div>

        {/* Due date — own deadline. Floor = milestone due date. Ceiling = earliest subtask date. */}
        <div style={{ padding: '14px 22px', borderBottom: '1px solid #161616' }}>
          <div style={{ fontSize: 9, fontWeight: 700, color: '#444', letterSpacing: '0.12em', textTransform: 'uppercase', marginBottom: 8 }}>
            Due date
            {milestoneDueDate && <span style={{ marginLeft: 8, color: '#666', fontWeight: 600, letterSpacing: '0.04em', textTransform: 'none' }}>milestone due {milestoneDueDate}</span>}
          </div>
          <window.DueDateInput
            value={draft.dueDate}
            onChange={(d) => setField('dueDate', d)}
            parentDate={milestoneDueDate}
            childLatest={(() => {
              let latest = null;
              for (const s of (draft.subtasks || [])) {
                if (s.dueDate && (!latest || s.dueDate > latest)) latest = s.dueDate;
              }
              return latest;
            })()}
            placeholder="Try 'fri', 'in 2 weeks', 'jun 15', or leave blank"
          />
        </div>

        {/* Notes — rich text with formatting toolbar */}
        <div style={{ padding: '14px 22px', borderBottom: '1px solid #161616' }}>
          <div style={{ fontSize: 9, fontWeight: 700, color: '#444', letterSpacing: '0.12em', textTransform: 'uppercase', marginBottom: 8 }}>Notes · click "Show formatting" for toolbar · paste images</div>
          <RichEditor
            value={draft.notes}
            onChange={(html) => set({ notes: html })}
            onPaste={onPaste}
            placeholder="Anything you want — context, links, references. Paste an image to attach."
            color={m.color}
            minHeight={120}
          />
        </div>

        {/* Pasted images — click to enlarge */}
        {draft.images.length > 0 && (
          <div style={{ padding: '14px 22px', borderBottom: '1px solid #161616' }}>
            <div style={{ fontSize: 9, fontWeight: 700, color: '#444', letterSpacing: '0.12em', textTransform: 'uppercase', marginBottom: 8 }}>Pasted images · click to enlarge</div>
            <div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
              {draft.images.map((src, i) => (
                <div key={i} style={{ position: 'relative' }}>
                  <img src={src} alt="" onClick={() => setLightbox(src)} style={{ maxWidth: 140, maxHeight: 100, borderRadius: 6, border: '1px solid #222', cursor: 'zoom-in' }} />
                  <button onClick={() => set({ images: draft.images.filter((_, j) => j !== i) })} style={{ position: 'absolute', top: 4, right: 4, background: 'rgba(0,0,0,0.7)', border: 'none', color: '#EF4444', borderRadius: 4, fontSize: 11, cursor: 'pointer', width: 18, height: 18 }}>×</button>
                </div>
              ))}
            </div>
          </div>
        )}

        {/* Subtasks */}
        <div style={{ padding: '14px 22px', borderBottom: '1px solid #161616' }}>
          <div style={{ display: 'flex', alignItems: 'center', marginBottom: 10 }}>
            <div style={{ fontSize: 9, fontWeight: 700, color: '#444', letterSpacing: '0.12em', textTransform: 'uppercase' }}>Sub-tasks · bite-sized</div>
            <button onClick={addSub} style={{ marginLeft: 'auto', background: 'transparent', border: '1px solid #222', color: m.color, borderRadius: 6, padding: '4px 10px', fontSize: 10, fontWeight: 700, cursor: 'pointer', fontFamily: 'inherit' }}>+ Sub-task</button>
          </div>
          {draft.subtasks.length === 0 ? (
            <div style={{ fontSize: 11, color: '#3a3a3a', fontStyle: 'italic' }}>No sub-tasks. Break this down if it feels too big to start.</div>
          ) : (
            <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
              {draft.subtasks.map((s) => {
                const sDays = window.daysUntil?.(s.dueDate);
                const sUrg = !s.done && sDays !== null && sDays !== undefined && sDays <= 5;
                const sColor = sUrg ? window.urgencyColor?.(s.dueDate) : null;
                const trySetSubDate = (iso) => {
                  if (iso && draft.dueDate && iso > draft.dueDate) {
                    alert(`Sub-task date can't be after the task's due date (${draft.dueDate}). Push the task's date later first, or pick ${draft.dueDate} or earlier.`);
                    return;
                  }
                  updateSub(s.id, { dueDate: iso });
                };
                return (
                <div key={s.id} style={{
                  display: 'flex', alignItems: 'center', gap: 10, padding: '8px 10px',
                  background: s.done ? 'rgba(34,197,94,0.04)' : (sUrg ? `${sColor}10` : '#0a0a0a'),
                  border: `1px solid ${s.done ? 'rgba(34,197,94,0.18)' : (sUrg ? sColor : '#161616')}`,
                  borderRadius: 8,
                  boxShadow: sUrg ? `0 0 10px ${sColor}44` : 'none',
                }}>
                  <input type="checkbox" checked={!!s.done} onChange={(e) => updateSub(s.id, { done: e.target.checked })}
                    style={{ width: 16, height: 16, accentColor: sUrg ? sColor : '#22C55E', cursor: 'pointer', flexShrink: 0 }} />
                  <input
                    value={s.title}
                    onChange={(e) => updateSub(s.id, { title: e.target.value })}
                    placeholder="Sub-task title"
                    style={{ flex: 1, background: 'transparent', border: 'none', outline: 'none', color: s.done ? '#555' : '#d0d0d0', fontFamily: 'inherit', fontSize: 12, textDecoration: s.done ? 'line-through' : 'none' }}
                  />
                  {s.dueDate ? (
                    <span style={{ display: 'inline-flex', alignItems: 'center', gap: 4 }}>
                      <window.DueDateChip date={s.dueDate} compact />
                      <button onClick={() => updateSub(s.id, { dueDate: null })} title="Clear due date"
                        style={{ background: 'none', border: 'none', color: '#3a3a3a', cursor: 'pointer', fontSize: 11, padding: 0, lineHeight: 1 }}>×</button>
                    </span>
                  ) : (
                    <window.InlineDueDateSetter onSet={trySetSubDate} />
                  )}
                  <button onClick={() => sprintSubtask(s)} title="Sprint this sub-task" style={{ background: 'transparent', border: `1px solid ${m.color}55`, color: m.color, borderRadius: 5, padding: '3px 8px', fontSize: 10, fontWeight: 700, cursor: 'pointer', fontFamily: 'inherit' }}>▶ Sprint</button>
                  <button onClick={() => deleteSub(s.id)} style={{ background: 'transparent', border: 'none', color: '#3a3a3a', cursor: 'pointer', fontSize: 14, padding: 0, width: 18 }}>×</button>
                </div>
                );
              })}
            </div>
          )}
        </div>

        {/* Footer */}
        <div style={{ padding: '12px 22px', display: 'flex', gap: 10, alignItems: 'center' }}>
          <button onClick={() => { if (confirm('Delete this task?')) { onDelete(); onClose(); } }} style={{ background: 'transparent', border: '1px solid rgba(239,68,68,0.25)', color: '#EF4444', borderRadius: 8, padding: '7px 12px', fontSize: 11, fontWeight: 600, cursor: 'pointer', fontFamily: 'inherit' }}>Delete task</button>
          {task.aiCreated && !task.validated && (
            <button onClick={() => { updateTaskById(projectId, milestoneId, task.id, { aiCreated: false, validated: true }); onClose(); }}
              style={{ background: '#22C55E22', border: '1px solid #22C55E55', color: '#22C55E', borderRadius: 8, padding: '7px 14px', fontSize: 11, fontWeight: 700, cursor: 'pointer', fontFamily: 'inherit' }}
              title="Mark this AI-created task as reviewed">✓ Validate</button>
          )}
          <button onClick={onClose} style={{ marginLeft: 'auto', background: m.color, color: '#fff', border: 'none', borderRadius: 8, padding: '7px 14px', fontSize: 11, fontWeight: 700, cursor: 'pointer', fontFamily: 'inherit' }}>Done</button>
        </div>
      </div>
    </div>
    {launching && <window.SprintLauncherModal launch={launching} onClose={() => { setLaunching(null); onClose(); }} />}
    {lightbox && <ImageLightbox src={lightbox} onClose={() => setLightbox(null)} />}
    </>
  );
}
window.TaskEditor = TaskEditor;

// ─── BRANCH POPOVER ──────────────────────────────────────────────────
function PJBranchMenu({ branch, onUpdate, onDelete, onAddLeaf, onArchive, onUnarchive, canArchive, onClose, anchorPos }) {
  const [label, setLabel] = useState(branch.label);
  const ref = useRef(null);
  // Flip above the anchor if popover would overflow viewport bottom
  const [flipUp, setFlipUp] = useState(false);
  useLayoutEffect(() => {
    if (!ref.current) return;
    const h = ref.current.getBoundingClientRect().height;
    setFlipUp(anchorPos.screenY + h + 24 > window.innerHeight);
  }, [anchorPos]);
  useEffect(() => {
    const h = (e) => { if (e.key === 'Escape') { onUpdate({ label }); onClose(); } };
    window.addEventListener('keydown', h);
    return () => window.removeEventListener('keydown', h);
  }, [label, onClose, onUpdate]);
  const topPos = flipUp ? anchorPos.screenY - (anchorPos.height || 60) - 12 : anchorPos.screenY + 12;
  const transformY = flipUp ? 'translate(-50%, -100%)' : 'translate(-50%, 0)';
  return (
    <div ref={ref} style={{
      position: 'fixed', left: anchorPos.screenX, top: topPos,
      transform: transformY,
      width: 280, background: '#0a0a0a',
      border: `1px solid ${branch.color}55`,
      borderRadius: 12, padding: 14, zIndex: 300,
      boxShadow: '0 8px 32px rgba(0,0,0,0.7)',
      display: 'flex', flexDirection: 'column', gap: 12,
      animation: 'slideUp 0.18s cubic-bezier(0.16,1,0.3,1)',
    }}>
      <div>
        <div style={{ fontSize: 9, fontWeight: 700, letterSpacing: '0.12em', color: '#444', textTransform: 'uppercase', marginBottom: 6 }}>Milestone label</div>
        <input
          autoFocus
          value={label}
          onChange={(e) => setLabel(e.target.value)}
          onBlur={() => onUpdate({ label })}
          onKeyDown={(e) => { if (e.key === 'Enter') e.target.blur(); }}
          placeholder="Milestone name"
          style={{ width: '100%', background: 'rgba(255,255,255,0.04)', border: '1px solid rgba(255,255,255,0.12)', borderRadius: 6, padding: '5px 9px', color: '#e0e0e0', fontFamily: 'inherit', fontSize: 13, fontWeight: 600, outline: 'none', boxSizing: 'border-box' }}
        />
      </div>
      <div>
        <div style={{ fontSize: 9, fontWeight: 700, letterSpacing: '0.12em', color: '#444', textTransform: 'uppercase', marginBottom: 6 }}>Mode color</div>
        <div style={{ display: 'grid', gridTemplateColumns: 'repeat(6, 1fr)', gap: 4 }}>
          {MODES.map((m) => (
            <button key={m.id} title={m.label} onClick={() => onUpdate({ color: m.color })}
              style={{ width: '100%', aspectRatio: '1/1', borderRadius: 5, background: m.color, border: `2px solid ${branch.color === m.color ? '#fff' : 'transparent'}`, cursor: 'pointer' }} />
          ))}
        </div>
      </div>
      {/* Due date — floor for child tasks/subtasks */}
      <div>
        <div style={{ fontSize: 9, fontWeight: 700, letterSpacing: '0.12em', color: '#444', textTransform: 'uppercase', marginBottom: 6 }}>Due date</div>
        <window.DueDateInput
          value={branch.dueDate}
          onChange={(d) => onUpdate({ dueDate: d })}
          childLatest={(() => {
            let latest = null;
            for (const t of (branch.leaves || [])) {
              if (t.dueDate && (!latest || t.dueDate > latest)) latest = t.dueDate;
              for (const s of (t.subtasks || [])) {
                if (s.dueDate && (!latest || s.dueDate > latest)) latest = s.dueDate;
              }
            }
            return latest;
          })()}
          placeholder="fri / jun 15 / in 2 weeks"
          compact
        />
      </div>
      <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
        <button onClick={onAddLeaf} style={{ flex: 1, minWidth: 80, background: branch.color, color: '#fff', border: 'none', borderRadius: 7, padding: '7px 0', fontSize: 11, fontWeight: 700, cursor: 'pointer', fontFamily: 'inherit' }}>+ Task</button>
        {branch.archived ? (
          <button onClick={() => { onUnarchive(); onClose(); }} style={{ background: '#EAB30822', border: '1px solid #EAB30855', color: '#EAB308', borderRadius: 7, padding: '7px 12px', fontSize: 11, fontWeight: 700, cursor: 'pointer', fontFamily: 'inherit' }} title="Unarchive milestone">⤴ Unarchive</button>
        ) : (canArchive && (
          <button onClick={() => { onArchive(); onClose(); }} style={{ background: 'transparent', border: '1px solid #22C55E55', color: '#22C55E', borderRadius: 7, padding: '7px 12px', fontSize: 11, fontWeight: 700, cursor: 'pointer', fontFamily: 'inherit' }} title="Archive — only available when 100% complete">📦 Archive</button>
        ))}
        <button onClick={() => { if (confirm('Delete this milestone and all its tasks?')) { onDelete(); onClose(); } }}
          style={{ background: 'transparent', border: '1px solid rgba(239,68,68,0.3)', color: '#EF4444', borderRadius: 7, padding: '7px 12px', fontSize: 11, fontWeight: 600, cursor: 'pointer', fontFamily: 'inherit' }}>Delete</button>
      </div>
    </div>
  );
}

// ─── PROJECT LIST VIEW ───────────────────────────────────────────────
function ProjectListView({ projects, onOpen, onAdd, onDelete, onRename, onRecolor, onArchive, onUnarchive }) {
  const [editingId, setEditingId] = useState(null);
  const [editDraft, setEditDraft] = useState('');
  const [showArchived, setShowArchived] = useLFState('showArchivedProjects', false);

  const visible = projects.filter((p) => showArchived ? p.archived : !p.archived);
  const archivedCount = projects.filter((p) => p.archived).length;

  return (
    <div className="screen screen-enter" style={{ padding: 22 }}>
      <div style={{ display: 'flex', alignItems: 'center', marginBottom: 16, gap: 10, flexWrap: 'wrap' }}>
        <div>
          <div style={{ fontSize: 18, fontWeight: 700 }}>{showArchived ? 'Archived projects' : 'Active projects'}</div>
          <div style={{ fontSize: 11, color: '#3a3a3a', marginTop: 2 }}>{visible.length} project{visible.length !== 1 ? 's' : ''} · click to open mind map</div>
        </div>
        <div style={{ marginLeft: 'auto', display: 'flex', gap: 8, alignItems: 'center' }}>
          {archivedCount > 0 && (
            <button onClick={() => setShowArchived(!showArchived)}
              style={{ background: showArchived ? '#EAB30822' : '#111', border: `1px solid ${showArchived ? '#EAB30855' : '#222'}`, color: showArchived ? '#EAB308' : '#888', borderRadius: 7, padding: '5px 10px', fontSize: 11, fontWeight: 700, cursor: 'pointer', fontFamily: 'inherit' }}>
              📦 {showArchived ? 'Active' : `Archived (${archivedCount})`}
            </button>
          )}
          {!showArchived && <button onClick={onAdd} className="btn btn-primary btn-sm">+ New project</button>}
        </div>
      </div>

      {visible.length === 0 ? (
        <div style={{ padding: 60, textAlign: 'center', color: '#444', border: '1px dashed #1c1c1c', borderRadius: 12 }}>
          {showArchived ? 'No archived projects.' : 'No projects yet. Click "+ New project" to start.'}
        </div>
      ) : (
        <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(280px, 1fr))', gap: 14 }}>
          {visible.map((p) => {
            const ms = p.branches || [];
            const taskCount = ms.reduce((s, b) => s + (b.leaves?.length || 0), 0);
            const doneCount = ms.reduce((s, b) => s + (b.leaves || []).filter((lf) => taskProgress(lf) === 100).length, 0);
            const progress = projectProgress(p);
            const canArchive = progress === 100 && !p.archived;
            const pending = p.aiCreated && !p.validated;
            return (
              <div key={p.id} className="card" style={{
                cursor: 'pointer', position: 'relative', overflow: 'hidden',
                borderColor: pending ? '#8B5CF6' : '#1c1c1c',
                borderStyle: pending ? 'dashed' : 'solid',
                transition: 'all 0.15s',
                borderLeft: `3px solid ${p.color}`,
                background: pending ? 'rgba(139,92,246,0.06)' : undefined,
                opacity: p.archived ? 0.55 : 1,
              }}
                onClick={() => editingId !== p.id && onOpen(p.id)}
                onMouseEnter={(e) => { if (!pending) e.currentTarget.style.borderColor = p.color + '88'; }}
                onMouseLeave={(e) => { if (!pending) e.currentTarget.style.borderColor = '#1c1c1c'; }}>
                {pending && (
                  <div style={{ position: 'absolute', top: -8, right: 14, background: '#0a0a0a', padding: '0 8px', fontSize: 9, color: '#8B5CF6', fontWeight: 700, letterSpacing: '0.1em', textTransform: 'uppercase', zIndex: 1 }}>
                    ✦ AI · review
                  </div>
                )}
                <div style={{ position: 'absolute', top: 0, right: 0, width: 100, height: 100, background: `radial-gradient(circle at top right, ${p.color}11 0%, transparent 70%)`, pointerEvents: 'none' }} />

                <div style={{ display: 'flex', alignItems: 'flex-start', gap: 10, marginBottom: 10 }}>
                  {editingId === p.id ? (
                    <input
                      autoFocus
                      value={editDraft}
                      onChange={(e) => setEditDraft(e.target.value)}
                      onClick={(e) => e.stopPropagation()}
                      onBlur={() => { onRename(p.id, editDraft); setEditingId(null); }}
                      onKeyDown={(e) => { if (e.key === 'Enter') e.target.blur(); if (e.key === 'Escape') setEditingId(null); }}
                      style={{ flex: 1, background: '#0a0a0a', border: '1px solid #222', borderRadius: 6, padding: '4px 8px', color: '#e8e8e8', fontFamily: 'inherit', fontSize: 14, fontWeight: 700, outline: 'none' }}
                    />
                  ) : (
                    <div style={{ flex: 1, fontSize: 14, fontWeight: 700, lineHeight: 1.35, color: '#e8e8e8' }}>{p.name}</div>
                  )}
                  <button onClick={(e) => { e.stopPropagation(); setEditingId(p.id); setEditDraft(p.name); }}
                    style={{ background: 'transparent', border: 'none', color: '#3a3a3a', cursor: 'pointer', fontSize: 12, padding: 2 }}
                    title="Rename">✎</button>
                </div>

                <div style={{ display: 'flex', gap: 14, fontSize: 11, color: '#555', marginBottom: 10 }}>
                  <span><span style={{ color: '#888', fontWeight: 700 }}>{ms.length}</span> milestone{ms.length !== 1 ? 's' : ''}</span>
                  <span><span style={{ color: '#888', fontWeight: 700 }}>{doneCount}</span>/<span style={{ color: '#888' }}>{taskCount}</span> done</span>
                </div>

                <div style={{ marginBottom: 10 }}>
                  <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', fontSize: 10, color: '#3a3a3a', marginBottom: 5 }}>
                    <span>Progress</span>
                    <span style={{ display: 'inline-flex', alignItems: 'center', gap: 6 }}>
                      <span onClick={(e) => e.stopPropagation()}>
                        <window.UrgencyTriangle item={p} size={14} />
                      </span>
                      <span style={{ color: p.color, fontWeight: 700 }}>{progress}%</span>
                    </span>
                  </div>
                  <div style={{ height: 4, background: '#161616', borderRadius: 2, overflow: 'hidden' }}>
                    <div style={{ height: '100%', width: `${progress}%`, background: p.color, transition: 'width 0.4s' }} />
                  </div>
                </div>

                <div onClick={(e) => e.stopPropagation()} style={{ display: 'flex', alignItems: 'center', gap: 3, flexWrap: 'wrap', maxWidth: '70%' }}>
                  {(window.PROJECT_COLORS || []).map((c) => (
                    <button key={c} title={c} onClick={() => onRecolor(p.id, c)}
                      style={{ width: 12, height: 12, borderRadius: 3, background: c, border: `1.5px solid ${p.color === c ? '#fff' : 'transparent'}`, cursor: 'pointer', padding: 0, flexShrink: 0 }} />
                  ))}
                  <div style={{ marginLeft: 'auto', display: 'flex', gap: 6 }}>
                    {pending && (
                      <button onClick={(e) => { e.stopPropagation(); window.LF.update('projects', (ps = []) => ps.map((pp) => pp.id === p.id ? { ...pp, aiCreated: false, validated: true } : pp)); }}
                        style={{ background: '#22C55E22', border: '1px solid #22C55E55', color: '#22C55E', borderRadius: 6, padding: '3px 8px', fontSize: 10, fontWeight: 700, cursor: 'pointer', fontFamily: 'inherit' }}
                        title="Mark as validated — removes the AI border">✓ Validate</button>
                    )}
                    {p.archived ? (
                      <button onClick={(e) => { e.stopPropagation(); onUnarchive(p.id); }}
                        style={{ background: '#EAB30822', border: '1px solid #EAB30855', color: '#EAB308', borderRadius: 6, padding: '3px 8px', fontSize: 10, fontWeight: 700, cursor: 'pointer', fontFamily: 'inherit' }}
                        title="Bring back to active">⤴ Unarchive</button>
                    ) : (canArchive && (
                      <button onClick={(e) => { e.stopPropagation(); onArchive(p.id); }}
                        style={{ background: 'transparent', border: '1px solid #22C55E55', color: '#22C55E', borderRadius: 6, padding: '3px 8px', fontSize: 10, fontWeight: 700, cursor: 'pointer', fontFamily: 'inherit' }}
                        title="Archive — only available when 100% complete">📦 Archive</button>
                    ))}
                    <button onClick={(e) => { e.stopPropagation(); if (confirm(`Delete project "${p.name}"?`)) onDelete(p.id); }}
                      style={{ background: 'transparent', border: 'none', color: '#3a3a3a', cursor: 'pointer', fontSize: 11, padding: 2 }}
                      title="Delete project">🗑</button>
                  </div>
                </div>
              </div>
            );
          })}
        </div>
      )}
    </div>
  );
}

// ─── PROJECT MIND MAP ────────────────────────────────────────────────
function ProjectMindMap({ project, onUpdate, onBack }) {
  const [editingCenter, setEditingCenter] = useState(false);
  const [centerDraft, setCenterDraft] = useState('');
  const [openTask, setOpenTask] = useState(null); // {milestoneId, taskId}
  const [openBranchMenu, setOpenBranchMenu] = useState(null);
  const [hover, setHover] = useState(null);
  const [fitScale, setFitScale] = useState(1);
  const [zoom, setZoom] = useLFState(`pj.zoom.${project.id}`, 1);
  const [pan, setPan] = useLFState(`pj.pan.${project.id}`, { x: 0, y: 0 });
  const [panning, setPanning] = useState(null); // {sx, sy, px, py}
  const [showArchived, setShowArchived] = useLFState(`pj.showArchived.${project.id}`, false);
  const [archivedWarn, setArchivedWarn] = useState(null); // milestoneId
  const wrapRef = useRef(null);

  const allBranches = project.branches || [];
  const archivedCount = allBranches.filter((b) => b.archived).length;
  const branches = pjLayout(allBranches, showArchived);
  const totalScale = fitScale * zoom;

  // Auto-fit + observe resize
  useEffect(() => {
    const fit = () => {
      if (!wrapRef.current) return;
      const r = wrapRef.current.getBoundingClientRect();
      if (r.width < 10 || r.height < 10) return;
      setFitScale(Math.min(r.width / PJ_W, r.height / PJ_H) * 0.9);
    };
    fit();
    const ro = new ResizeObserver(fit);
    if (wrapRef.current) ro.observe(wrapRef.current);
    window.addEventListener('resize', fit);
    return () => { ro.disconnect(); window.removeEventListener('resize', fit); };
  }, []);

  // Wheel zoom (non-passive so we can prevent page scroll)
  useEffect(() => {
    const el = wrapRef.current;
    if (!el) return;
    const onWheel = (e) => {
      e.preventDefault();
      const delta = -e.deltaY * 0.0015;
      setZoom((z) => Math.max(0.3, Math.min(4, z * (1 + delta))));
    };
    el.addEventListener('wheel', onWheel, { passive: false });
    return () => el.removeEventListener('wheel', onWheel);
  }, []);

  // Pan: mousedown on wrap background only (children stop propagation)
  const onPanStart = (e) => {
    if (e.button !== 0) return;
    setPanning({ sx: e.clientX, sy: e.clientY, px: pan.x, py: pan.y });
  };
  useEffect(() => {
    if (!panning) return;
    const onMove = (e) => setPan({ x: panning.px + (e.clientX - panning.sx), y: panning.py + (e.clientY - panning.sy) });
    const onUp = () => setPanning(null);
    window.addEventListener('mousemove', onMove);
    window.addEventListener('mouseup', onUp);
    return () => { window.removeEventListener('mousemove', onMove); window.removeEventListener('mouseup', onUp); };
  }, [panning]);

  // Helpers — convert canvas-space (xc, yc) to screen-space (xs, ys) within wrap
  // Used to render constant-size text labels above the scaled canvas.
  function toScreen(xc, yc) {
    if (!wrapRef.current) return { x: 0, y: 0 };
    const r = wrapRef.current.getBoundingClientRect();
    const cx = r.width / 2 + pan.x;
    const cy = r.height / 2 + pan.y;
    return { x: cx + (xc - PJ_CX) * totalScale, y: cy + (yc - PJ_CY) * totalScale };
  }

  // CRUD on the project
  const setBranches = (next) => onUpdate({ branches: typeof next === 'function' ? next(project.branches || []) : next });
  const updateCenter = (label) => onUpdate({ name: label.trim() || 'Project' });
  const addBranch = () => setBranches((bs) => [...bs, { id: pjUid(), label: 'New milestone', color: PJ_DEFAULT_BRANCH_COLOR, leaves: [] }]);
  const updateBranch = (id, patch) => setBranches((bs) => bs.map((b) => b.id === id ? { ...b, ...patch } : b));
  const deleteBranch = (id) => setBranches((bs) => bs.filter((b) => b.id !== id));
  const addLeaf = (bid) => setBranches((bs) => bs.map((b) => b.id === bid ? { ...b, leaves: [...(b.leaves || []), defaultTask()] } : b));
  const deleteLeaf = (bid, lid) => setBranches((bs) => bs.map((b) => b.id === bid ? { ...b, leaves: (b.leaves || []).filter((lf) => lf.id !== lid) } : b));
  const archiveBranch = (id) => setBranches((bs) => bs.map((b) => b.id === id ? { ...b, archived: true } : b));
  const unarchiveBranch = (id) => setBranches((bs) => bs.map((b) => b.id === id ? { ...b, archived: false } : b));

  const activeBranch = openBranchMenu ? branches.find((b) => b.id === openBranchMenu.branchId) : null;
  const activeTask = openTask ? branches.find((b) => b.id === openTask.milestoneId)?.leaves.find((lf) => lf.id === openTask.taskId) : null;
  const activeTaskMilestone = openTask ? branches.find((b) => b.id === openTask.milestoneId) : null;

  // Render constant-size text labels and clickable hit boxes positioned at
  // screen-space coordinates derived from canvas positions. Leaves stack
  // outward from each milestone in screen-space (fixed pixel spacing) so they
  // never overlap each other at any zoom level. This trades the radial fan
  // for a more readable "roots radiating outward" look.
  const centerScreen = toScreen(PJ_CX, PJ_CY);
  const LEAF_GAP = 30;        // px between consecutive leaves on a milestone's ray
  const MS_HALF_W = 85;       // milestone box half-width (170/2)
  const MS_HALF_H = 30;       // milestone box half-height (60/2)
  const LEAF_PADDING = 18;    // px gap between milestone box edge and first leaf
  const screenBranches = branches.map((b) => {
    const ms = toScreen(b.main.x, b.main.y);
    // Unit vector from project center outward through this milestone (screen)
    const dx = ms.x - centerScreen.x;
    const dy = ms.y - centerScreen.y;
    const len = Math.hypot(dx, dy) || 1;
    const ux = dx / len, uy = dy / len;
    // Distance from milestone center to its box edge in the outward direction
    // — for an axis-aligned rect, min of (halfW/|cos|, halfH/|sin|)
    const absUx = Math.abs(ux) || 1e-6;
    const absUy = Math.abs(uy) || 1e-6;
    const boxExtent = Math.min(MS_HALF_W / absUx, MS_HALF_H / absUy);
    const startD = boxExtent + LEAF_PADDING;
    const leavesScreen = (b.leaves || []).map((lf, j) => {
      const d = startD + j * LEAF_GAP;
      return { ...lf, screen: { x: ms.x + ux * d, y: ms.y + uy * d }, _arm: { ux, uy, d } };
    });
    return { ...b, mainScreen: ms, leavesScreen, _outward: { ux, uy } };
  });

  return (
    <div className="screen screen-enter" style={{ padding: 0, position: 'relative' }}>
      {/* Toolbar */}
      <div style={{ position: 'absolute', top: 14, left: 18, right: 18, zIndex: 30, display: 'flex', gap: 10, alignItems: 'center', pointerEvents: 'none' }}>
        <button onClick={onBack} style={{ pointerEvents: 'auto', background: '#111', border: '1px solid #222', color: '#888', borderRadius: 7, padding: '5px 10px', fontSize: 11, fontWeight: 600, cursor: 'pointer', fontFamily: 'inherit' }}>← Projects</button>
        <div style={{ fontSize: 10, color: '#3a3a3a', fontWeight: 600, letterSpacing: '0.14em', textTransform: 'uppercase', pointerEvents: 'auto' }}>
          {branches.length} milestone{branches.length === 1 ? '' : 's'} · {branches.reduce((s, b) => s + (b.leaves?.length || 0), 0)} tasks · {projectProgress(project)}% done
        </div>
        <div style={{ marginLeft: 'auto', display: 'flex', gap: 6, pointerEvents: 'auto' }}>
          {archivedCount > 0 && (
            <button onClick={() => setShowArchived(!showArchived)} title={showArchived ? 'Hide archived milestones' : 'Show archived milestones'}
              style={{ background: showArchived ? '#EAB30822' : '#111', border: `1px solid ${showArchived ? '#EAB30855' : '#222'}`, color: showArchived ? '#EAB308' : '#888', borderRadius: 7, padding: '0 10px', fontSize: 10, fontWeight: 700, cursor: 'pointer', fontFamily: 'inherit', height: 28 }}>
              📦 {showArchived ? 'Hide' : 'Show'} archived ({archivedCount})
            </button>
          )}
          <button onClick={() => setZoom((z) => Math.max(0.3, z / 1.2))} style={{ width: 28, height: 28, background: '#111', border: '1px solid #222', color: '#888', borderRadius: 7, cursor: 'pointer', fontSize: 14 }}>−</button>
          <button onClick={() => { setZoom(1); setPan({ x: 0, y: 0 }); }} style={{ background: '#111', border: '1px solid #222', color: '#888', borderRadius: 7, padding: '0 8px', fontSize: 10, fontWeight: 600, cursor: 'pointer', fontFamily: 'inherit', height: 28 }} title="Reset zoom & pan">{Math.round(zoom * 100)}%</button>
          <button onClick={() => setZoom((z) => Math.min(4, z * 1.2))} style={{ width: 28, height: 28, background: '#111', border: '1px solid #222', color: '#888', borderRadius: 7, cursor: 'pointer', fontSize: 14 }}>+</button>
          <button onClick={addBranch} className="btn btn-primary btn-sm">+ Milestone</button>
        </div>
      </div>

      {/* Hint */}
      <div style={{ position: 'absolute', bottom: 14, left: '50%', transform: 'translateX(-50%)', zIndex: 30, fontSize: 10, color: '#2a2a2a', letterSpacing: '0.1em', textTransform: 'uppercase', pointerEvents: 'none', whiteSpace: 'nowrap' }}>
        Scroll to zoom · drag empty space to pan · click center / milestone / task to edit
      </div>

      {/* Wrap */}
      <div
        ref={wrapRef}
        onMouseDown={onPanStart}
        onClick={() => setOpenBranchMenu(null)}
        style={{
          position: 'absolute', inset: 0,
          background: 'radial-gradient(ellipse 140% 80% at 50% 50%, #0c1020 0%, #050505 70%)',
          overflow: 'hidden',
          cursor: panning ? 'grabbing' : 'grab',
          userSelect: 'none',
        }}>
        {/* Dot grid */}
        <svg style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', pointerEvents: 'none' }}>
          <defs>
            <pattern id={`pj-dots-${project.id}`} x={pan.x % (40 * totalScale)} y={pan.y % (40 * totalScale)} width={40 * totalScale} height={40 * totalScale} patternUnits="userSpaceOnUse">
              <circle cx="1" cy="1" r="0.8" fill="rgba(80,100,200,0.10)" />
            </pattern>
          </defs>
          <rect width="100%" height="100%" fill={`url(#pj-dots-${project.id})`} />
        </svg>

        {/* SVG layer drawn entirely in screen-space — arms follow the same
            outward-ray layout as the leaf labels above. */}
        <svg style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', pointerEvents: 'none', overflow: 'visible' }}>
          {screenBranches.map((b) => (
            <g key={b.id}>
              {/* Center → milestone arm (curved) */}
              <path d={pjBezier(centerScreen.x, centerScreen.y, b.mainScreen.x, b.mainScreen.y)} stroke={b.color} strokeWidth={6} fill="none" opacity={0.16} />
              <path d={pjBezier(centerScreen.x, centerScreen.y, b.mainScreen.x, b.mainScreen.y)} stroke={b.color} strokeWidth={1.5} fill="none" opacity={0.55} />
              {/* Milestone → leaves arm (straight ray) — one shared line through all leaves */}
              {b.leavesScreen.length > 0 && (() => {
                const lastLeaf = b.leavesScreen[b.leavesScreen.length - 1];
                return (
                  <line x1={b.mainScreen.x} y1={b.mainScreen.y} x2={lastLeaf.screen.x} y2={lastLeaf.screen.y}
                    stroke={b.color} strokeWidth={1} opacity={0.3} />
                );
              })()}
              {/* Leaf dots */}
              {b.leavesScreen.map((lf) => {
                const h = hover === `${b.id}:${lf.id}`;
                return (
                  <circle key={lf.id} cx={lf.screen.x} cy={lf.screen.y} r={h ? 5.5 : 3.5}
                    fill={b.color} opacity={lf.done ? 0.4 : (h ? 1 : 0.6)} style={{ transition: 'all 0.2s' }} />
                );
              })}
            </g>
          ))}
        </svg>

        {/* Constant-size HTML labels — positioned in screen space */}
        {(() => {
          const cs = toScreen(PJ_CX, PJ_CY);
          return (
            <div style={{ position: 'absolute', inset: 0, pointerEvents: 'none' }}>
              {/* Center node */}
              <div
                onClick={(e) => { e.stopPropagation(); setEditingCenter(true); setCenterDraft(project.name); }}
                onMouseDown={(e) => e.stopPropagation()}
                style={{
                  position: 'absolute', left: cs.x, top: cs.y,
                  transform: 'translate(-50%, -50%)',
                  width: 156, height: 156, borderRadius: '50%',
                  background: 'radial-gradient(circle at 38% 30%, rgba(255,255,255,0.10), rgba(255,255,255,0.02))',
                  border: `1px solid ${project.color}55`,
                  display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 6,
                  cursor: 'pointer', zIndex: 20, backdropFilter: 'blur(16px)',
                  boxShadow: `0 0 60px ${project.color}22, inset 0 1px 0 rgba(255,255,255,0.10)`,
                  padding: 14, transition: 'transform 0.2s',
                  pointerEvents: 'auto',
                }}>
                {editingCenter ? (
                  <input
                    autoFocus
                    value={centerDraft}
                    onChange={(e) => setCenterDraft(e.target.value)}
                    onClick={(e) => e.stopPropagation()}
                    onBlur={() => { updateCenter(centerDraft); setEditingCenter(false); }}
                    onKeyDown={(e) => { if (e.key === 'Enter') e.target.blur(); if (e.key === 'Escape') setEditingCenter(false); }}
                    placeholder="Project name"
                    style={{ width: '100%', textAlign: 'center', background: 'transparent', border: 'none', outline: 'none', color: '#fff', fontFamily: 'inherit', fontSize: 12, fontWeight: 700, letterSpacing: '0.08em', textTransform: 'uppercase' }}
                  />
                ) : (
                  <>
                    <div style={{ fontSize: 12, fontWeight: 700, letterSpacing: '0.12em', color: '#fff', textAlign: 'center', textTransform: 'uppercase', lineHeight: 1.5 }}>{project.name}</div>
                    <div style={{ fontSize: 9, color: '#555', letterSpacing: '0.1em', textTransform: 'uppercase' }}>{projectProgress(project)}% complete</div>
                  </>
                )}
              </div>

              {/* Branch + leaf labels */}
              {screenBranches.map((b) => {
                const isActive = openBranchMenu?.branchId === b.id;
                // Milestone's own due-date urgency (colors the bounding box)
                const msDays = window.daysUntil?.(b.dueDate);
                const msUrgent = !b.archived && msDays !== null && msDays !== undefined && msDays <= 5;
                const msUrgColor = msUrgent ? window.urgencyColor?.(b.dueDate) : null;
                const msOverdue = !b.archived && msDays !== null && msDays !== undefined && msDays < 0;
                // Descendant-only urgency (shows triangle on the box, no color)
                const descMin = window.descendantMinDays?.(b);
                const descUrgent = !b.archived && !msUrgent && descMin !== null && descMin !== undefined && descMin <= 5;
                const boxBorderColor = msUrgent ? msUrgColor : (isActive ? b.color : `${b.color}55`);
                const boxGlow = msUrgent ? `0 0 26px ${msUrgColor}77` : (isActive ? `0 0 32px ${b.color}66` : `0 0 14px ${b.color}1a`);
                return (
                  <React.Fragment key={b.id}>
                    {/* Milestone box */}
                    <div
                      onMouseDown={(e) => e.stopPropagation()}
                      onClick={(e) => {
                        e.stopPropagation();
                        if (b.archived) {
                          const r = e.currentTarget.getBoundingClientRect();
                          setOpenBranchMenu({ branchId: b.id, screenX: r.left + r.width / 2, screenY: r.bottom, height: r.height });
                          return;
                        }
                        if (isActive) { setOpenBranchMenu(null); return; }
                        const r = e.currentTarget.getBoundingClientRect();
                        setOpenBranchMenu({ branchId: b.id, screenX: r.left + r.width / 2, screenY: r.bottom, height: r.height });
                      }}
                      style={{
                        position: 'absolute', left: b.mainScreen.x, top: b.mainScreen.y,
                        transform: 'translate(-50%, -50%)',
                        width: 170, height: 56, borderRadius: 12,
                        cursor: 'pointer', zIndex: 15,
                        background: isActive
                          ? `radial-gradient(circle at 40% 30%, ${b.color}45, ${b.color}14)`
                          : (msUrgent ? `${msUrgColor}1a` : `${b.color}1c`),
                        border: `1.5px solid ${boxBorderColor}`,
                        boxShadow: boxGlow,
                        display: 'flex', alignItems: 'center', justifyContent: 'center',
                        backdropFilter: 'blur(10px)', padding: 8,
                        pointerEvents: 'auto',
                        opacity: b.archived ? 0.5 : 1,
                        transition: 'background 0.2s, border 0.2s, box-shadow 0.2s, opacity 0.2s',
                      }}>
                      <div style={{ fontSize: 11, fontWeight: 700, letterSpacing: '0.06em', color: isActive ? '#fff' : (msUrgent ? msUrgColor : `${b.color}ee`), textAlign: 'center', textTransform: 'uppercase', lineHeight: 1.3, textShadow: msUrgent ? `0 0 6px ${msUrgColor}77` : 'none', animation: msOverdue ? 'pulse 1.1s ease-in-out infinite' : 'none' }}>
                        {b.label}
                        <div style={{ fontSize: 9, fontWeight: 700, letterSpacing: '0.1em', marginTop: 4, display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 2 }}>
                          <span style={{ color: b.archived ? '#EAB308' : (msUrgent ? msUrgColor : '#666') }}>
                            {b.archived ? 'ARCHIVED' : `${milestoneProgress(b)}%`}
                          </span>
                          {b.dueDate && !b.archived && (
                            <span style={{
                              color: msUrgent ? msUrgColor : '#999',
                              textShadow: msUrgent ? `0 0 6px ${msUrgColor}88` : 'none',
                              fontVariantNumeric: 'tabular-nums',
                            }}>
                              DUE {window.formatDueLabel(b.dueDate)}
                            </span>
                          )}
                        </div>
                      </div>
                      {/* Triangle overlay: shows when descendant tasks/subtasks are
                          ≤5d but the milestone itself isn't dated. Click stops
                          propagation so hover popover stays clean. */}
                      {descUrgent && (
                        <div onClick={(e) => e.stopPropagation()}
                          style={{ position: 'absolute', top: -8, right: -8, background: '#0a0a0a', borderRadius: '50%', padding: 2, lineHeight: 0, zIndex: 16 }}>
                          <window.UrgencyTriangle item={b} includeSelf={false} size={16} />
                        </div>
                      )}
                    </div>

                    {/* Leaves */}
                    {b.leavesScreen.map((lf) => {
                      const id = `${b.id}:${lf.id}`;
                      const h = hover === id;
                      const tp = taskProgress(lf);
                      const tDays = window.daysUntil?.(lf.dueDate);
                      const tUrgent = !lf.done && tDays !== null && tDays !== undefined && tDays <= 5;
                      const tUrgColor = tUrgent ? window.urgencyColor?.(lf.dueDate) : null;
                      const tOverdue = !lf.done && tDays !== null && tDays !== undefined && tDays < 0;
                      const sMin = window.descendantMinDays?.({ subtasks: lf.subtasks || [] });
                      const subUrgent = !lf.done && !tUrgent && sMin !== null && sMin !== undefined && sMin <= 5;
                      return (
                        <div key={lf.id}
                          onMouseDown={(e) => e.stopPropagation()}
                          onMouseEnter={() => setHover(id)}
                          onMouseLeave={() => setHover(null)}
                          onClick={(e) => { e.stopPropagation(); setOpenTask({ milestoneId: b.id, taskId: lf.id }); }}
                          style={{
                            position: 'absolute', left: lf.screen.x, top: lf.screen.y,
                            transform: 'translate(-50%, -50%)',
                            display: 'flex', alignItems: 'center', gap: 6,
                            padding: '5px 12px 5px 7px', borderRadius: 20,
                            cursor: 'pointer', zIndex: 12,
                            background: h ? `${b.color}1c` : (tUrgent ? `${tUrgColor}10` : 'transparent'),
                            border: `1px solid ${h ? b.color + '55' : (tUrgent ? `${tUrgColor}55` : 'transparent')}`,
                            boxShadow: h ? `0 0 16px ${b.color}33` : (tUrgent ? `0 0 12px ${tUrgColor}55` : 'none'),
                            whiteSpace: 'nowrap', transition: 'all 0.18s',
                            pointerEvents: 'auto',
                            opacity: lf.done ? 0.55 : 1,
                          }}>
                          <div style={{ width: 7, height: 7, borderRadius: '50%', flexShrink: 0, background: tUrgent ? tUrgColor : b.color, opacity: h || tUrgent ? 1 : 0.6, boxShadow: (h || tUrgent) ? `0 0 8px ${tUrgent ? tUrgColor : b.color}` : 'none' }} />
                          <span style={{
                            fontSize: 11,
                            fontWeight: tUrgent ? 800 : (h ? 600 : 500),
                            color: lf.done ? '#555' : (tUrgent ? tUrgColor : (h ? b.color : 'rgba(255,255,255,0.7)')),
                            textDecoration: lf.done ? 'line-through' : 'none',
                            textShadow: tUrgent ? `0 0 6px ${tUrgColor}88` : 'none',
                            animation: tOverdue ? 'pulse 1.1s ease-in-out infinite' : 'none',
                          }}>{lf.label}</span>
                          {tp > 0 && tp < 100 && (
                            <span style={{ fontSize: 9, color: tUrgent ? tUrgColor : b.color, fontWeight: 700, marginLeft: 2 }}>{tp}%</span>
                          )}
                          {tp === 100 && <span style={{ fontSize: 10, color: '#22C55E', marginLeft: 2 }}>✓</span>}
                          {/* Subtask-only urgency triangle — task itself has no date but a subtask is ≤5d */}
                          {subUrgent && (
                            <span onClick={(e) => e.stopPropagation()} style={{ marginLeft: 2, lineHeight: 0 }}>
                              <window.UrgencyTriangle item={{ subtasks: lf.subtasks || [] }} includeSelf={false} size={11} />
                            </span>
                          )}
                        </div>
                      );
                    })}
                  </React.Fragment>
                );
              })}
            </div>
          );
        })()}
      </div>

      {/* Branch popover */}
      {openBranchMenu && activeBranch && (
        <div onClick={(e) => e.stopPropagation()}>
          <PJBranchMenu
            branch={activeBranch}
            anchorPos={openBranchMenu}
            onUpdate={(patch) => updateBranch(activeBranch.id, patch)}
            onDelete={() => deleteBranch(activeBranch.id)}
            onAddLeaf={() => { addLeaf(activeBranch.id); setOpenBranchMenu(null); }}
            onArchive={() => archiveBranch(activeBranch.id)}
            onUnarchive={() => unarchiveBranch(activeBranch.id)}
            canArchive={milestoneProgress(activeBranch) === 100 && !activeBranch.archived}
            onClose={() => setOpenBranchMenu(null)}
          />
        </div>
      )}

      {/* Empty state */}
      {branches.length === 0 && (
        <div style={{ position: 'absolute', left: '50%', top: '60%', transform: 'translate(-50%, 0)', fontSize: 12, color: '#444', letterSpacing: '0.1em', textTransform: 'uppercase', pointerEvents: 'none' }}>
          No milestones yet · click "+ Milestone" to start
        </div>
      )}

      {/* Task editor */}
      {activeTask && activeTaskMilestone && (
        <TaskEditor
          projectId={project.id}
          milestoneId={activeTaskMilestone.id}
          milestoneColor={activeTaskMilestone.color}
          milestoneLabel={activeTaskMilestone.label}
          task={activeTask}
          onClose={() => setOpenTask(null)}
          onDelete={() => deleteLeaf(activeTaskMilestone.id, activeTask.id)}
        />
      )}
    </div>
  );
}

// ─── ROOT PROJETECTURE SCREEN ─────────────────────────────────────
function ProjetectureScreen() {
  const [projects, setProjects] = useLFState('projects', pjLoadProjects);
  const [activeId, setActiveId] = useLFState('activeProjectId', null);

  const updateProject = (id, patch) => setProjects((ps) => ps.map((p) => p.id === id ? { ...p, ...patch } : p));
  const addProject = () => {
    const newP = { id: pjUid(), name: 'New project', color: PJ_DEFAULT_PROJECT_COLOR, status: 'active', createdAt: Date.now(), branches: [] };
    setProjects((ps) => [...ps, newP]);
    setActiveId(newP.id);
  };
  const deleteProject = (id) => {
    setProjects((ps) => ps.filter((p) => p.id !== id));
    if (activeId === id) setActiveId(null);
  };
  const renameProject = (id, name) => updateProject(id, { name: name.trim() || 'Project' });
  const recolorProject = (id, color) => updateProject(id, { color });
  const archiveProject = (id) => updateProject(id, { archived: true });
  const unarchiveProject = (id) => updateProject(id, { archived: false });

  const active = activeId ? projects.find((p) => p.id === activeId) : null;
  if (!active) {
    return (
      <ProjectListView
        projects={projects}
        onOpen={setActiveId}
        onAdd={addProject}
        onArchive={archiveProject}
        onUnarchive={unarchiveProject}
        onDelete={deleteProject}
        onRename={renameProject}
        onRecolor={recolorProject}
      />
    );
  }
  return (
    <ProjectMindMap
      project={active}
      onUpdate={(patch) => updateProject(active.id, patch)}
      onBack={() => setActiveId(null)}
    />
  );
}

Object.assign(window, { ProjetectureScreen });
