// Settings, List, Calendar, Timeline screens.

// ─── DatabaseCard (shown inside Settings) ──────────────────────────────────
function DatabaseCard() {
  const isElectron = !!window.electronAPI;
  const [dbPath, setDbPath] = React.useState('');
  const [loading, setLoading] = React.useState(false);
  const fileInputRef = React.useRef(null);

  function parseName(p) {
    if (!p) return '';
    return p.replace(/\\/g, '/').split('/').pop();
  }
  function parseDir(p) {
    if (!p) return '';
    const parts = p.replace(/\\/g, '/').split('/');
    parts.pop();
    return parts.join('/') || '/';
  }

  React.useEffect(() => {
    if (isElectron) window.electronAPI.getDbPath().then(p => setDbPath(p || ''));
  }, []);

  async function handleNew() {
    if (!isElectron || loading) return;
    setLoading(true);
    try {
      const newPath = await window.electronAPI.createNewDatabase();
      if (newPath) {
        showToast('New database created — reloading…');
        setTimeout(() => window.location.reload(), 800);
      }
    } finally { setLoading(false); }
  }

  async function handleOpen() {
    if (!isElectron || loading) return;
    setLoading(true);
    try {
      const newPath = await window.electronAPI.openDatabase();
      if (newPath) {
        showToast('Opening database — reloading…');
        setTimeout(() => window.location.reload(), 800);
      }
    } finally { setLoading(false); }
  }

  // Export/Import — the way to move data between the desktop app and a
  // web-hosted workspace (or between two web workspaces): download the
  // ENTIRE local database as one JSON file here, then use "Import from
  // file…" on the destination to load it there. Works the same on desktop
  // and web since both just read/write the same state shape.
  function handleExport() {
    const data = window.store.state;
    const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = 'cookie-planner-export.json';
    document.body.appendChild(a);
    a.click();
    a.remove();
    URL.revokeObjectURL(url);
    showToast('Exported — file downloaded');
  }

  function handleImportFile(e) {
    const file = e.target.files && e.target.files[0];
    e.target.value = '';
    if (!file) return;
    const reader = new FileReader();
    reader.onload = () => {
      try {
        const data = JSON.parse(reader.result);
        if (!window.confirm('This replaces everything currently in this ' + (isElectron ? 'database' : 'workspace') + ' with the imported file. Continue?')) return;
        window.actions.importData(data);
        showToast('Imported — data replaced');
      } catch (err) {
        window.alert('That file could not be read as a valid export: ' + err.message);
      }
    };
    reader.readAsText(file);
  }


  const dbName = parseName(dbPath);
  const dbDir  = parseDir(dbPath);

  return (
    <div className="settings-card">
      <div className="sp-section-title" style={{ marginBottom: 10 }}>Database</div>

      {isElectron && (
        <div className="field-row" style={{ flexDirection: 'column', alignItems: 'flex-start', gap: 4 }}>
          <div className="field-label">Current file</div>
          <div style={{ display: 'flex', alignItems: 'center', gap: 8, width: '100%' }}>
            <span style={{ fontSize: 20 }}>🗄</span>
            <div style={{ flex: 1, minWidth: 0 }}>
              <div style={{ fontWeight: 600, fontSize: 13, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
                {dbName || 'No database selected'}
              </div>
              <div style={{ fontSize: 11, color: 'var(--ink-3)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
                {dbDir}
              </div>
            </div>
            {dbPath && (
              <button className="btn btn-ghost" style={{ flexShrink: 0 }}
                onClick={() => window.electronAPI.showDbInFolder()}
                title="Reveal in Finder / Explorer">
                Show in folder ↗
              </button>
            )}
          </div>
        </div>
      )}

      {isElectron && (
        <div style={{ display: 'flex', gap: 8, marginTop: 14 }}>
          <button className="btn" onClick={handleNew} disabled={loading}>
            + New database…
          </button>
          <button className="btn btn-ghost" onClick={handleOpen} disabled={loading}>
            ↗ Open existing…
          </button>
        </div>
      )}
      {isElectron && (
        <div style={{ marginTop: 8, fontSize: 11, color: 'var(--ink-3)', lineHeight: 1.5 }}>
          Creating a new database starts fresh. Your current data stays in the existing file — nothing is deleted.
        </div>
      )}

      <div style={{ display: 'flex', gap: 8, marginTop: isElectron ? 14 : 0 }}>
        <button className="btn btn-ghost" onClick={handleExport}>
          ⬇ Export to file…
        </button>
        <button className="btn btn-ghost" onClick={() => fileInputRef.current?.click()}>
          ⬆ Import from file…
        </button>
        <input ref={fileInputRef} type="file" accept="application/json" style={{ display: 'none' }} onChange={handleImportFile} />
      </div>
      <div style={{ marginTop: 8, fontSize: 11, color: 'var(--ink-3)', lineHeight: 1.5 }}>
        Export downloads everything in this {isElectron ? 'database' : 'workspace'} as one file — use it to move data between the desktop app and a web workspace, or between two web workspaces. Import replaces everything currently here.
      </div>
    </div>
  );
}

// ─── Epics / Stories shared "magnet board" ──────────────────────────────
// Cards sit at a grid cell (epicPos.x/.y or storyPos.x/.y) so drags always
// settle back into clean rows and columns, but nothing is labelled — it's
// just an invisible grid, not real columns. The canvas itself scrolls
// (rather than clipping) once cards spread past the visible area.
const EPIC_CELL_W = 220;
const EPIC_CELL_H = 132;
const EPIC_GAP = 16;
const EPIC_COL_W = EPIC_CELL_W + EPIC_GAP;
const EPIC_ROW_H = EPIC_CELL_H + EPIC_GAP;
const EPIC_DEFAULT_COLS = 4;

// Computes a non-overlapping position for every item: keeps each item's
// stored pos if it's valid AND not already claimed by an earlier item in the
// list, otherwise assigns the next free cell. This is what catches (and
// heals) two cards ending up on the exact same cell — e.g. a freshly
// converted card defaulting to a slot an existing card already occupies,
// which would otherwise render one completely behind the other and make it
// look like it "disappeared". Any item that needed a new slot gets that
// slot persisted back to the store once, so the fix sticks.
function useSanitizedPositions(items, posKey, setPos) {
  const occupied = new Set();
  const positioned = [];
  const needsFix = [];
  items.forEach(e => {
    const p = e[posKey];
    const valid = p && Number.isFinite(p.x) && Number.isFinite(p.y) && p.x >= 0 && p.y >= 0;
    const key = valid ? p.x + ',' + p.y : null;
    if (valid && !occupied.has(key)) {
      occupied.add(key);
      positioned.push({ e, pos: p });
    } else {
      needsFix.push(e);
    }
  });
  needsFix.forEach(e => {
    let x = 0, y = 0;
    while (occupied.has(x + ',' + y)) { x++; if (x >= EPIC_DEFAULT_COLS) { x = 0; y++; } }
    occupied.add(x + ',' + y);
    positioned.push({ e, pos: { x, y } });
  });

  const fixedRef = React.useRef(new Set());
  const sig = positioned.map(p => p.e.id + ':' + p.pos.x + ',' + p.pos.y).join('|');
  React.useEffect(() => {
    positioned.forEach(({ e, pos }) => {
      const stored = e[posKey];
      const matches = stored && stored.x === pos.x && stored.y === pos.y;
      if (!matches && !fixedRef.current.has(e.id)) {
        fixedRef.current.add(e.id);
        setPos(e.id, pos.x, pos.y);
      }
    });
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [sig]);

  return positioned;
}

function StoriesScreen() {
  const s = useStore();
  const project = s.projects.find(p => p.id === s.activeProjectId);
  const [addingStory, setAddingStory] = React.useState(false);
  const [drag, setDrag] = React.useState(null);
  const dragInfo = React.useRef(null);
  const suppressClick = React.useRef(false);
  const stories = project ? s.tasks.filter(t => t.projectId === project.id && t.isStory) : [];
  const positioned = useSanitizedPositions(stories, 'storyPos', actions.setStoryPos);
  if (!project) return null;

  function newStory(title) {
    const id = actions.addTask(project.id, project.stages[0]?.id, title);
    actions.setStoryFlag(id, true);
    actions.setActiveTask(id);
    setAddingStory(false);
  }

  const maxX = positioned.reduce((m, p) => Math.max(m, p.pos.x), EPIC_DEFAULT_COLS - 1);
  const maxY = positioned.reduce((m, p) => Math.max(m, p.pos.y), 0);

  function onPointerDown(ev, story, pos) {
    if (ev.button !== 0) return;
    ev.preventDefault();
    dragInfo.current = { id: story.id, pos, startX: ev.clientX, startY: ev.clientY };
    setDrag({ id: story.id, dx: 0, dy: 0 });
    ev.currentTarget.setPointerCapture(ev.pointerId);
  }
  function onPointerMove(ev) {
    if (!dragInfo.current) return;
    setDrag({ id: dragInfo.current.id, dx: ev.clientX - dragInfo.current.startX, dy: ev.clientY - dragInfo.current.startY });
  }
  function onPointerUp(ev) {
    if (!dragInfo.current) return;
    const { id, pos, startX, startY } = dragInfo.current;
    const dx = ev.clientX - startX, dy = ev.clientY - startY;
    const moved = Math.abs(dx) > 4 || Math.abs(dy) > 4;
    dragInfo.current = null;
    setDrag(null);
    if (!moved) return;
    suppressClick.current = true;
    const newX = Math.max(0, Math.round(pos.x + dx / EPIC_COL_W));
    const newY = Math.max(0, Math.round(pos.y + dy / EPIC_ROW_H));
    if (newX === pos.x && newY === pos.y) return;
    const occupant = positioned.find(p => p.e.id !== id && p.pos.x === newX && p.pos.y === newY);
    if (occupant) actions.setStoryPos(occupant.e.id, pos.x, pos.y);
    actions.setStoryPos(id, newX, newY);
  }

  return (
    <div className="board-wrap">
      <div className="epics-page">
        <div className="epics-head">
          <div>
            <h2>Stories</h2>
            <div className="sub">Group related tasks under a mid-size body of work. Drag cards to arrange them.</div>
          </div>
          <button className="btn btn-primary" onClick={() => setAddingStory(true)}>+ New story</button>
        </div>
        {addingStory && (
          <PromptDialog
            title="New story"
            placeholder="Story name…"
            confirmLabel="Create"
            onConfirm={newStory}
            onCancel={() => setAddingStory(false)}
          />
        )}
        {stories.length === 0 &&
          <div className="empty-fields" style={{ marginTop: 8 }}>
            No stories yet. Create one, then link tasks to it from the task's Parent field.
          </div>
        }
        <div className="epics-canvas-wrap">
          <div
            className="epics-canvas"
            style={{ height: (maxY + 1) * EPIC_ROW_H + EPIC_GAP, minWidth: (maxX + 1) * EPIC_COL_W + EPIC_GAP }}
            onPointerMove={onPointerMove}
            onPointerUp={onPointerUp}
            onPointerCancel={onPointerUp}
          >
            {positioned.map(({ e, pos }) => {
              const { done, total } = getStoryProgress(e.id);
              const pct = total ? Math.round(done / total * 100) : 0;
              const isDragging = drag && drag.id === e.id;
              const left = EPIC_GAP + pos.x * EPIC_COL_W + (isDragging ? drag.dx : 0);
              const top = EPIC_GAP + pos.y * EPIC_ROW_H + (isDragging ? drag.dy : 0);
              const epic = e.epicId ? getTask(e.epicId) : null;
              return (
                <div key={e.id} className={'epic-card' + (isDragging ? ' dragging' : '')}
                  style={{ borderLeftColor: e.storyColor || 'var(--border-2)', left, top }}
                  onPointerDown={(ev) => onPointerDown(ev, e, pos)}
                  onClick={() => {
                    if (suppressClick.current) { suppressClick.current = false; return; }
                    actions.setActiveTask(e.id);
                  }}>
                  {epic && <div className="epic-parent-tag">{epic.title}</div>}
                  <div className="epic-card-title">{e.title}</div>
                  <div className="epic-progress-bar">
                    <div className="epic-progress-fill" style={{ width: pct + '%', background: e.storyColor || '#7a9bc4' }} />
                  </div>
                  <div className="epic-card-meta">{total ? `${done}/${total} tasks done` : 'No linked tasks yet'}</div>
                </div>
              );
            })}
          </div>
        </div>
      </div>
    </div>
  );
}
window.StoriesScreen = StoriesScreen;

// ─── Epics ──────────────────────────────
function EpicsScreen() {
  const s = useStore();
  const project = s.projects.find(p => p.id === s.activeProjectId);
  const [addingEpic, setAddingEpic] = React.useState(false);
  const [drag, setDrag] = React.useState(null); // { id, dx, dy }
  const dragInfo = React.useRef(null);
  const suppressClick = React.useRef(false);
  const epics = project ? s.tasks.filter(t => t.projectId === project.id && t.isEpic) : [];
  const positioned = useSanitizedPositions(epics, 'epicPos', actions.setEpicPos);
  if (!project) return null;

  function newEpic(title) {
    const id = actions.addTask(project.id, project.stages[0]?.id, title);
    actions.setEpicFlag(id, true);
    actions.setActiveTask(id);
    setAddingEpic(false);
  }

  const maxX = positioned.reduce((m, p) => Math.max(m, p.pos.x), EPIC_DEFAULT_COLS - 1);
  const maxY = positioned.reduce((m, p) => Math.max(m, p.pos.y), 0);

  function onPointerDown(ev, epic, pos) {
    if (ev.button !== 0) return;
    ev.preventDefault();
    dragInfo.current = { id: epic.id, pos, startX: ev.clientX, startY: ev.clientY };
    setDrag({ id: epic.id, dx: 0, dy: 0 });
    ev.currentTarget.setPointerCapture(ev.pointerId);
  }
  function onPointerMove(ev) {
    if (!dragInfo.current) return;
    setDrag({ id: dragInfo.current.id, dx: ev.clientX - dragInfo.current.startX, dy: ev.clientY - dragInfo.current.startY });
  }
  function onPointerUp(ev) {
    if (!dragInfo.current) return;
    const { id, pos, startX, startY } = dragInfo.current;
    const dx = ev.clientX - startX, dy = ev.clientY - startY;
    const moved = Math.abs(dx) > 4 || Math.abs(dy) > 4;
    dragInfo.current = null;
    setDrag(null);
    if (!moved) return;
    suppressClick.current = true;
    const newX = Math.max(0, Math.round(pos.x + dx / EPIC_COL_W));
    const newY = Math.max(0, Math.round(pos.y + dy / EPIC_ROW_H));
    if (newX === pos.x && newY === pos.y) return;
    const occupant = positioned.find(p => p.e.id !== id && p.pos.x === newX && p.pos.y === newY);
    if (occupant) actions.setEpicPos(occupant.e.id, pos.x, pos.y);
    actions.setEpicPos(id, newX, newY);
  }

  return (
    <div className="board-wrap">
      <div className="epics-page">
        <div className="epics-head">
          <div>
            <h2>Epics</h2>
            <div className="sub">Group related tickets under a larger body of work. Drag cards to arrange them.</div>
          </div>
          <button className="btn btn-primary" onClick={() => setAddingEpic(true)}>+ New epic</button>
        </div>
        {addingEpic && (
          <PromptDialog
            title="New epic"
            placeholder="Epic name…"
            confirmLabel="Create"
            onConfirm={newEpic}
            onCancel={() => setAddingEpic(false)}
          />
        )}
        {epics.length === 0 &&
          <div className="empty-fields" style={{ marginTop: 8 }}>
            No epics yet. Create one, then link tickets to it from the ticket's Epic field.
          </div>
        }
        <div className="epics-canvas-wrap">
          <div
            className="epics-canvas"
            style={{ height: (maxY + 1) * EPIC_ROW_H + EPIC_GAP, minWidth: (maxX + 1) * EPIC_COL_W + EPIC_GAP }}
            onPointerMove={onPointerMove}
            onPointerUp={onPointerUp}
            onPointerCancel={onPointerUp}
          >
            {positioned.map(({ e, pos }) => {
              const { done, total } = getEpicProgress(project, e.id);
              const pct = total ? Math.round(done / total * 100) : 0;
              const isDragging = drag && drag.id === e.id;
              const left = EPIC_GAP + pos.x * EPIC_COL_W + (isDragging ? drag.dx : 0);
              const top = EPIC_GAP + pos.y * EPIC_ROW_H + (isDragging ? drag.dy : 0);
              return (
                <div key={e.id} className={'epic-card is-epic-card' + (isDragging ? ' dragging' : '')}
                  style={{ borderLeftColor: e.epicColor || 'var(--border-2)', left, top }}
                  onPointerDown={(ev) => onPointerDown(ev, e, pos)}
                  onClick={() => {
                    if (suppressClick.current) { suppressClick.current = false; return; }
                    actions.setActiveTask(e.id);
                  }}>
                  <div className="epic-card-title">{e.title}</div>
                  <div className="epic-progress-bar">
                    <div className="epic-progress-fill" style={{ width: pct + '%', background: e.epicColor || '#9a948a' }} />
                  </div>
                  <div className="epic-card-meta">{total ? `${done}/${total} linked items done` : 'No linked items yet'}</div>
                </div>
              );
            })}
          </div>
        </div>
      </div>
    </div>
  );
}
window.EpicsScreen = EpicsScreen;

// ─── Settings ──────────────────────────────
function SettingsScreen() {
  const s = useStore();
  const project = s.projects.find(p => p.id === s.activeProjectId);
  if (!project) return null;
  const [dragStage, setDragStage] = React.useState(null);
  const [dragEpic, setDragEpic] = React.useState(null);
  const [dragStory, setDragStory] = React.useState(null);
  const epics = getEpics(project.id);
  const stories = getStories(project.id);

  const customFields = project.fields.filter(f => !f.builtin);
  const builtinFields = project.fields.filter(f => f.builtin);

  return (
    <div className="board-wrap">
    <div className="settings-page">
      <h2>Project settings</h2>
      <div className="sub">{project.name} — stages, fields, colors and visibility.</div>

      <DatabaseCard />

      <div className="settings-card">
        <div className="sp-section-title" style={{marginBottom:10}}>Project</div>
        <div className="field-row">
          <div className="field-label">Name</div>
          <div className="field-value">
            <input type="text" defaultValue={project.name} onBlur={e => actions.updateProject(project.id, { name: e.target.value || 'Untitled' })} />
          </div>
        </div>
        <div className="field-row">
          <div className="field-label">Color</div>
          <div className="field-value">
            <ColorSwatchPicker value={project.color} onChange={(c) => actions.updateProject(project.id, { color: c })} />
          </div>
        </div>
      </div>

      <div className="settings-card">
        <div className="sp-section-title" style={{marginBottom:10}}>Calendar</div>
        <div className="field-row">
          <div className="field-label">Week starts on</div>
          <div className="field-value">
            <select value={s.weekStartsMonday === false ? 'sunday' : 'monday'}
              onChange={e => actions.setWeekStartsMonday(e.target.value !== 'sunday')}>
              <option value="monday">Monday</option>
              <option value="sunday">Sunday</option>
            </select>
          </div>
        </div>
      </div>

      <div className="settings-card">
        <div className="sp-section-title" style={{marginBottom:10}}>Priority colors</div>
        <div className="prio-color-row">
          {['High','Med','Low'].map(level => {
            const c = priorityColor(project, level);
            return (
              <div key={level} className="prio-color-cell">
                <span className="prio-dot" style={{ background: c, width:12, height:12 }} />
                <span style={{flex:1}}>{level}</span>
                <ColorSwatchPicker value={c}
                  onChange={(v) => actions.setPriorityColor(project.id, level, v)} />
              </div>
            );
          })}
        </div>
      </div>

      <div className="settings-card">
        <div className="sp-section-title" style={{marginBottom:10, justifyContent:'space-between', display:'flex'}}>
          <span>Stages</span>
          <span style={{color:'var(--ink-3)', fontWeight:400, textTransform:'none', letterSpacing:0}}>Drag to reorder · click swatch for column color</span>
        </div>
        {project.stages.map((st, i) => {
          return (
            <div key={st.id} className="row-edit"
              draggable
              onDragStart={() => setDragStage(i)}
              onDragOver={e => e.preventDefault()}
              onDrop={() => { if (dragStage !== null && dragStage !== i) actions.reorderStages(project.id, dragStage, i); setDragStage(null); }}
            >
              <span className="grip">⋮⋮</span>
              <input type="text" defaultValue={st.name} onBlur={e => actions.updateStage(project.id, st.id, { name: e.target.value || 'Stage' })} />
              <button title="Delete stage" onClick={() => {
                if (project.stages.length <= 1) return showToast('Need at least one stage');
                if (confirm(`Delete stage "${st.name}"? Tasks will move to "${project.stages.find(x => x.id !== st.id).name}".`)) actions.deleteStage(project.id, st.id);
              }}>✕</button>
            </div>
          );
        })}
        <button className="add-inline" onClick={() => actions.addStage(project.id, 'New stage')}>+ Add stage</button>
      </div>

      <div className="settings-card">
        <div className="sp-section-title" style={{marginBottom:10, justifyContent:'space-between', display:'flex'}}>
          <span>Epics</span>
          <span style={{color:'var(--ink-3)', fontWeight:400, textTransform:'none', letterSpacing:0}}>Drag to reorder — sets order in the parent dropdown</span>
        </div>
        {epics.length === 0 && <div className="empty-fields">No epics yet.</div>}
        {epics.map((e, i) => (
          <div key={e.id} className="row-edit"
            draggable
            onDragStart={() => setDragEpic(i)}
            onDragOver={ev => ev.preventDefault()}
            onDrop={() => { if (dragEpic !== null && dragEpic !== i) actions.reorderEpics(project.id, dragEpic, i); setDragEpic(null); }}
          >
            <span className="grip">⋮⋮</span>
            <span className="opt-chip" style={{ background: tint(e.epicColor || '#9a948a', 22), color: '#1c1b18' }}>{e.title}</span>
          </div>
        ))}
      </div>

      <div className="settings-card">
        <div className="sp-section-title" style={{marginBottom:10, justifyContent:'space-between', display:'flex'}}>
          <span>Stories</span>
          <span style={{color:'var(--ink-3)', fontWeight:400, textTransform:'none', letterSpacing:0}}>Drag to reorder — sets order in the parent dropdown</span>
        </div>
        {stories.length === 0 && <div className="empty-fields">No stories yet.</div>}
        {stories.map((st, i) => (
          <div key={st.id} className="row-edit"
            draggable
            onDragStart={() => setDragStory(i)}
            onDragOver={ev => ev.preventDefault()}
            onDrop={() => { if (dragStory !== null && dragStory !== i) actions.reorderStories(project.id, dragStory, i); setDragStory(null); }}
          >
            <span className="grip">⋮⋮</span>
            <span className="opt-chip" style={{ background: tint(st.storyColor || '#7a9bc4', 22), color: '#1c1b18' }}>{st.title}</span>
          </div>
        ))}
      </div>

      <div className="settings-card">
        <div className="sp-section-title" style={{marginBottom:10, justifyContent:'space-between', display:'flex'}}>
          <span>Built-in fields</span>
          <span style={{color:'var(--ink-3)', fontWeight:400, textTransform:'none', letterSpacing:0}}>Color and card visibility</span>
        </div>
        {builtinFields.map(f => (
          <FieldSettingsRow key={f.id} project={project} field={f} editable={false} />
        ))}
      </div>

      <div className="settings-card">
        <div className="sp-section-title" style={{marginBottom:10}}>Custom fields</div>
        {customFields.map(f => (
          <FieldSettingsRow key={f.id} project={project} field={f} editable={true} />
        ))}
        <button className="add-inline" onClick={() => actions.addField(project.id, { name: 'New field', type: 'text' })}>+ Add custom field</button>
      </div>

      <div className="settings-card" style={{borderColor:'var(--border)'}}>
        <div className="sp-section-title" style={{marginBottom:10, color:'var(--danger)'}}>Danger zone</div>
        <button className="btn btn-danger" onClick={() => {
          if (confirm(`Delete project "${project.name}" and all its tasks?`)) actions.deleteProject(project.id);
        }}>Delete project</button>
      </div>
    </div>
    </div>
  );
}

function StageColorPicker_REMOVED() { return null; }

function FieldSettingsRow({ project, field, editable }) {
  const isSelect = field.type === 'select' || field.type === 'multi_select';
  return (
    <div className="row-edit" style={{flexWrap:'wrap', alignItems:'center'}}>
      <span className="grip">⋮⋮</span>
      <ColorSwatchPicker value={field.color || '#9a948a'}
        onChange={(c) => actions.updateField(project.id, field.id, { color: c })} />
      <input type="text" defaultValue={field.name}
        onBlur={e => actions.updateField(project.id, field.id, { name: e.target.value || 'Field' })}
        style={{maxWidth:180}} />
      {editable && (
        <select value={field.type} onChange={e => actions.updateField(project.id, field.id, { type: e.target.value })}
          style={{ background:'var(--bg-2)', border:'1px solid var(--border)', borderRadius:3, padding:'2px 4px', fontSize:12 }}>
          {FIELD_TYPES.map(t => <option key={t} value={t}>{t}</option>)}
        </select>
      )}
      {!editable && (
        <span className="type-pill">{field.type}</span>
      )}
      {isSelect && editable && (
        <input type="text" placeholder="comma-separated options"
          defaultValue={(field.options || []).join(', ')}
          onBlur={e => actions.updateField(project.id, field.id, { options: e.target.value.split(',').map(s => s.trim()).filter(Boolean) })}
          style={{flex:1, minWidth:200}} />
      )}
      <label className="show-on-card" title="Show this field as a chip on board cards">
        <input type="checkbox" checked={field.showOnCard !== false}
          onChange={e => actions.updateField(project.id, field.id, { showOnCard: e.target.checked })} />
        <span>on card</span>
      </label>
      {editable && (
        <button onClick={() => { if (confirm(`Delete field "${field.name}"?`)) actions.deleteField(project.id, field.id); }}>✕</button>
      )}
    </div>
  );
}

// ─── List ──────────────────────────────
function ListScreen() {
  const s = useStore();
  const project = s.projects.find(p => p.id === s.activeProjectId);
  if (!project) return null;
  const tasks = applyFilters(project, s.tasks.filter(t => t.projectId === project.id));
  const fields = project.fields;

  // Only show a field column if at least one task has a value for it
  function hasAnyValue(f) {
    return tasks.some(t => {
      const v = t.values[f.id];
      return v !== undefined && v !== null && v !== '' && !(Array.isArray(v) && v.length === 0);
    });
  }
  const visibleFields = fields.filter(hasAnyValue);
  const hasSubtasks = tasks.some(t => t.subtasks.length > 0);
  const hasDeps = tasks.some(t => (t.depsIn?.length || 0) > 0 || (t.depsOut?.length || 0) > 0);
  const totalCols = 2 + visibleFields.length + (hasSubtasks ? 1 : 0) + (hasDeps ? 1 : 0);

  return (
    <div className="board-wrap list-board-wrap">
      <table className="list-table">
        <thead>
          <tr>
            <th className="lt-dot-col"></th>
            <th className="lt-title-col">Title</th>
            {visibleFields.map(f => (
              <th key={f.id} className="lt-field-col">
                <span className="field-color-dot" style={{ background: f.color || '#9a948a', marginRight: 4 }} />
                {f.name}
              </th>
            ))}
            {hasSubtasks && <th className="lt-field-col">Subtasks</th>}
            {hasDeps && <th className="lt-field-col">Dependencies</th>}
          </tr>
        </thead>
        <tbody>
          {project.stages.map(stage => {
            const stageTasks = tasks.filter(t => t.stageId === stage.id).sort((a,b) => a.order - b.order);
            return (
              <React.Fragment key={stage.id}>
                <tr className="stage-row">
                  <td colSpan={totalCols}>{stage.name} · {stageTasks.length}</td>
                </tr>
                {stageTasks.map(t => {
                  const parentInfo = getParentInfo(t);
                  return (
                  <tr key={t.id} onClick={() => actions.setActiveTask(t.id)}>
                    <td className="lt-dot-col">
                      <span className="prio-dot" style={{ background: priorityColor(project, t.values.priority) }}></span>
                    </td>
                    <td className="lt-title-col">
                      {t.title}
                      {parentInfo && (
                        <button className="parent-jump-btn" onClick={(e) => { e.stopPropagation(); actions.setActiveTask(parentInfo.id); }}
                          title={`Jump to ${parentInfo.label}: ${parentInfo.title}`}>
                          ↥ {parentInfo.title}
                        </button>
                      )}
                    </td>
                    {visibleFields.map(f => (
                      <td key={f.id} className="lt-field-col">
                        <FieldChip field={f} value={t.values[f.id]} project={project} />
                      </td>
                    ))}
                    {hasSubtasks && (
                      <td className="lt-field-col lt-muted">
                        {t.subtasks.length > 0
                          ? <span className="lt-count">{t.subtasks.filter(s => s.done).length}<span className="lt-count-sep">/</span>{t.subtasks.length}</span>
                          : <span className="lt-dash">—</span>}
                      </td>
                    )}
                    {hasDeps && (
                      <td className="lt-field-col lt-muted">
                        {(t.depsIn?.length || 0) > 0 && <span className="lt-dep" title="Blocked by">⛔ {t.depsIn.length}</span>}
                        {(t.depsOut?.length || 0) > 0 && <span className="lt-dep" title="Blocks">↗ {t.depsOut.length}</span>}
                        {!(t.depsIn?.length) && !(t.depsOut?.length) && <span className="lt-dash">—</span>}
                      </td>
                    )}
                  </tr>
                  );
                })}
              </React.Fragment>
            );
          })}
        </tbody>
      </table>
    </div>
  );
}

// ─── Calendar ──────────────────────────────
const CAL_MAX_ROWS = 3;

function CalendarScreen() {
  const s = useStore();
  const project = s.projects.find(p => p.id === s.activeProjectId);
  const [cursor, setCursor] = React.useState(() => { const d = new Date(); d.setDate(1); return d; });
  const [dayModal, setDayModal] = React.useState(null); // { iso, label, tasks: [] }
  if (!project) return null;

  // Only tasks that have BOTH a start date AND a due date
  const tasks = applyFilters(project, s.tasks.filter(t =>
    t.projectId === project.id && t.values.start && t.values.due
  ));

  const year = cursor.getFullYear();
  const month = cursor.getMonth();
  const last = new Date(year, month + 1, 0);
  const weekStartsMonday = s.weekStartsMonday !== false;
  const rawDow = new Date(year, month, 1).getDay();
  const startDow = weekStartsMonday ? (rawDow + 6) % 7 : rawDow;
  const dowLabels = weekStartsMonday ? ['Mon','Tue','Wed','Thu','Fri','Sat','Sun'] : ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'];
  const todayStr = new Date().toDateString();

  // Timezone-safe ISO: use local year/month/day, not UTC
  function dayISO(date) {
    return date.getFullYear() + '-' +
      String(date.getMonth() + 1).padStart(2, '0') + '-' +
      String(date.getDate()).padStart(2, '0');
  }

  // Build flat day list then chunk into weeks
  const days = [];
  for (let i = 0; i < startDow; i++)
    days.push({ date: new Date(year, month, i - startDow + 1), other: true });
  for (let d = 1; d <= last.getDate(); d++)
    days.push({ date: new Date(year, month, d), other: false });
  while (days.length % 7 !== 0)
    days.push({ date: new Date(year, month + 1, days.length - (startDow + last.getDate()) + 1), other: true });

  const weeks = [];
  for (let i = 0; i < days.length; i += 7) weeks.push(days.slice(i, i + 7));

  // For a given week, return positioned events (no overlaps within a row)
  function weekEvents(week) {
    const ws = dayISO(week[0].date);
    const we = dayISO(week[6].date);

    const evs = tasks
      .filter(t => t.values.start <= we && t.values.due >= ws)
      .map(t => {
        const cs = t.values.start < ws ? ws : t.values.start;
        const ce = t.values.due   > we ? we : t.values.due;
        const sc = week.findIndex(d => dayISO(d.date) === cs);
        const ec = week.findIndex(d => dayISO(d.date) === ce);
        return {
          task: t,
          startCol: sc < 0 ? 0 : sc,
          endCol:   ec < 0 ? 6 : ec,
          startsHere: t.values.start >= ws,
          endsHere:   t.values.due   <= we,
          row: 0,
        };
      });

    // Sort by start col, then longer spans first
    evs.sort((a, b) => a.startCol - b.startCol || (b.endCol - b.startCol) - (a.endCol - a.startCol));

    // Greedy row packing — no two events in the same row overlap
    const rows = [];
    evs.forEach(ev => {
      let r = rows.findIndex(row => row[row.length - 1].endCol < ev.startCol);
      if (r < 0) { r = rows.length; rows.push([]); }
      ev.row = r;
      rows[r].push(ev);
    });
    return evs;
  }

  // All tasks touching a given day (used by the "+more" agenda popup)
  function tasksOnDay(iso) {
    return tasks.filter(t => t.values.start <= iso && t.values.due >= iso)
      .sort((a, b) => a.values.start.localeCompare(b.values.start));
  }

  return (
    <div className="board-wrap" style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
      <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
        <button className="btn" onClick={() => { const d = new Date(cursor); d.setMonth(d.getMonth() - 1); setCursor(d); }}>‹</button>
        <h2 style={{ margin: 0, fontSize: 15, fontWeight: 600 }}>
          {cursor.toLocaleDateString(undefined, { month: 'long', year: 'numeric' })}
        </h2>
        <button className="btn" onClick={() => { const d = new Date(cursor); d.setMonth(d.getMonth() + 1); setCursor(d); }}>›</button>
        <button className="btn btn-ghost" onClick={() => { const d = new Date(); d.setDate(1); setCursor(d); }}>Today</button>
      </div>

      <div className="cal-grid" style={{ flex: 1 }}>
        {/* Day-name header row */}
        <div className="cal-head-row">
          {dowLabels.map(d =>
            <div key={d} className="cal-head">{d}</div>
          )}
        </div>

        {/* One grid row per calendar week */}
        {weeks.map((week, wi) => {
          const evs = weekEvents(week);
          const visible = evs.filter(e => e.row < CAL_MAX_ROWS);
          const hidden = evs.filter(e => e.row >= CAL_MAX_ROWS);

          // Per-day overflow count, for the "+N more" chips
          const overflowByCol = week.map((_, di) =>
            hidden.filter(e => e.startCol <= di && e.endCol >= di).length);

          return (
            <div key={wi} className="cal-week">
              {/* Full-height per-day background (other-month tint), behind everything */}
              {week.map(({ other }, di) =>
                <div key={'bg' + di} className={'cal-daybg' + (other ? ' other' : '')}
                  style={{ left: `${(di / 7) * 100}%`, width: `${(1 / 7) * 100}%` }} />
              )}

              {/* Day-boundary guides — span full height, always on top of bars */}
              {[1, 2, 3, 4, 5, 6].map(i =>
                <div key={'sep' + i} className="cal-daysep" style={{ left: `${(i / 7) * 100}%` }} />
              )}

              {/* Row 1: day-number cells */}
              {week.map(({ date, other }, di) => {
                const isToday = date.toDateString() === todayStr;
                return (
                  <div key={di}
                    className={'cal-cell' + (other ? ' other' : '') + (isToday ? ' today' : '')}
                    style={{ gridColumn: di + 1, gridRow: 1 }}>
                    <span className="day-num">{date.getDate()}</span>
                  </div>
                );
              })}

              {/* Rows 2+: spanning event bars, capped at CAL_MAX_ROWS */}
              {visible.map(({ task: t, startCol, endCol, row, startsHere, endsHere }) => {
                const color = taskColor(t);
                const cls = 'cal-span-bar' +
                  (startsHere ? ' bar-starts' : '') +
                  (endsHere   ? ' bar-ends'   : '');
                return (
                  <div key={t.id}
                    className={cls}
                    style={{
                      gridColumn: `${startCol + 1} / ${endCol + 2}`,
                      gridRow: row + 2,
                      background: `color-mix(in oklab, ${color} 28%, white)`,
                      border: `1px solid color-mix(in oklab, ${color} 50%, white)`,
                    }}
                    onClick={() => actions.setActiveTask(t.id)}
                    title={`${t.title} · ${t.values.start} → ${t.values.due}`}>
                    {t.title}
                  </div>
                );
              })}

              {/* Overflow chips — one per day that has hidden events, own column only */}
              {week.map(({ date }, di) => {
                const n = overflowByCol[di];
                if (!n) return null;
                const iso = dayISO(date);
                return (
                  <div key={'more' + di}
                    className="cal-more-chip"
                    style={{ gridColumn: di + 1, gridRow: CAL_MAX_ROWS + 2 }}
                    onClick={() => setDayModal({
                      iso,
                      label: date.toLocaleDateString(undefined, { weekday: 'long', month: 'long', day: 'numeric' }),
                      tasks: tasksOnDay(iso),
                    })}>
                    +{n} more
                  </div>
                );
              })}
            </div>
          );
        })}
      </div>

      {dayModal && (
        <div className="cal-day-modal-backdrop" onClick={() => setDayModal(null)}>
          <div className="cal-day-modal" onClick={e => e.stopPropagation()}>
            <div className="cal-day-modal-head">
              <span>{dayModal.label}</span>
              <button className="btn btn-ghost" onClick={() => setDayModal(null)}>✕</button>
            </div>
            <div className="cal-day-modal-list">
              {dayModal.tasks.map(t => (
                <div key={t.id} className="cal-day-modal-item"
                  onClick={() => { actions.setActiveTask(t.id); setDayModal(null); }}>
                  <span className="cal-day-modal-dot" style={{ background: taskColor(t) }}></span>
                  <span style={{overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap'}}>{t.title}</span>
                </div>
              ))}
            </div>
          </div>
        </div>
      )}
    </div>
  );
}

// ─── Timeline ──────────────────────────────
const ZOOMS = {
  day:    { dayWidth: 70,  major: 'week',  minor: 'day' },
  week:   { dayWidth: 26,  major: 'month', minor: 'week' },
  month:  { dayWidth: 8,   major: 'month', minor: 'week' },
  quarter:{ dayWidth: 3,   major: 'quarter', minor: 'month' },
};

function isoFor(date) {
  const d = new Date(date); d.setHours(0,0,0,0);
  return d.getFullYear() + '-' + String(d.getMonth()+1).padStart(2,'0') + '-' + String(d.getDate()).padStart(2,'0');
}

function TimelineScreen({ zoom = 'week' }) {
  const s = useStore();
  const project = s.projects.find(p => p.id === s.activeProjectId);
  if (!project) return null;
  const conf = ZOOMS[zoom] || ZOOMS.week;
  const allTasks = s.tasks.filter(t => t.projectId === project.id);
  const tlGroupBy = project.tlGroupBy || 'none';
  const tasks = applyFilters(project, allTasks, { ignoreCollapse: tlGroupBy === '__epic__' || tlGroupBy === '__story__' });

  const groupField = tlGroupBy === 'none' ? null : project.fields.find(f => f.id === tlGroupBy);

  function sortByDate(arr) {
    return [...arr].sort((a, b) => {
      const da = a.values.start ? new Date(a.values.start).getTime() :
                 a.values.due   ? new Date(a.values.due).getTime()   : Infinity;
      const db = b.values.start ? new Date(b.values.start).getTime() :
                 b.values.due   ? new Date(b.values.due).getTime()   : Infinity;
      return da - db;
    });
  }

  let groups;
  if (tlGroupBy === 'none') {
    groups = [{ key: null, label: null, tasks: sortByDate(tasks) }];
  } else {
    const lanes = laneValuesForField(project, tlGroupBy, tasks);
    groups = lanes.map(lane => ({
      key: lane.key,
      label: lane.label,
      tasks: sortByDate(tasks.filter(t => taskInLane(t, project, tlGroupBy, lane.key))),
    }));
  }

  const today = new Date(); today.setHours(0,0,0,0);
  const dues   = tasks.map(t => t.values.due).filter(Boolean).map(d => new Date(d));
  const starts = tasks.map(t => t.values.start).filter(Boolean).map(d => new Date(d));
  const allDates = [...dues, ...starts];
  const maxD = allDates.length ? new Date(Math.max(...allDates.map(d=>d.getTime()))) : new Date(today.getTime() + 30*86400000);
  const minD = allDates.length ? new Date(Math.min(...allDates.map(d=>d.getTime()))) : today;
  const start = new Date(Math.min(minD.getTime(), today.getTime())); start.setDate(start.getDate() - 7);
  const end = new Date(Math.max(maxD.getTime(), today.getTime()) + 14*86400000);
  const totalDays = Math.ceil((end - start) / 86400000);
  const px = totalDays * conf.dayWidth;
  function dayIndex(d) { return Math.floor((new Date(d) - start) / 86400000); }
  function dateAtX(x) {
    const dayIdx = Math.round(x / conf.dayWidth);
    const d = new Date(start); d.setDate(d.getDate() + dayIdx);
    return d;
  }

  // Header bands
  const minorBands = [];
  if (conf.minor === 'day') {
    for (let i = 0; i < totalDays; i++) {
      const d = new Date(start); d.setDate(d.getDate()+i);
      minorBands.push({ left: i*conf.dayWidth, width: conf.dayWidth, label: d.getDate() });
    }
  } else if (conf.minor === 'week') {
    let i = 0;
    while (i < totalDays) {
      const d = new Date(start); d.setDate(d.getDate()+i);
      const dow = d.getDay(); const nextSun = (7 - dow) % 7 || 7;
      const w = Math.min(nextSun, totalDays - i);
      minorBands.push({ left: i*conf.dayWidth, width: w*conf.dayWidth, label: `${d.getMonth()+1}/${d.getDate()}` });
      i += w;
    }
  } else if (conf.minor === 'month') {
    let i = 0;
    while (i < totalDays) {
      const d = new Date(start); d.setDate(d.getDate()+i);
      const eom = new Date(d.getFullYear(), d.getMonth()+1, 0);
      const remain = Math.ceil((eom - d) / 86400000) + 1;
      const w = Math.min(remain, totalDays - i);
      minorBands.push({ left: i*conf.dayWidth, width: w*conf.dayWidth, label: d.toLocaleDateString(undefined, {month:'short'}) });
      i += w;
    }
  }
  const majorBands = [];
  if (conf.major === 'week') {
    let i = 0;
    while (i < totalDays) {
      const d = new Date(start); d.setDate(d.getDate()+i);
      const dow = d.getDay(); const nextSun = (7 - dow) % 7 || 7;
      const w = Math.min(nextSun, totalDays - i);
      majorBands.push({ left: i*conf.dayWidth, width: w*conf.dayWidth, label: `Week of ${d.getMonth()+1}/${d.getDate()}` });
      i += w;
    }
  } else if (conf.major === 'month') {
    let i = 0;
    while (i < totalDays) {
      const d = new Date(start); d.setDate(d.getDate()+i);
      const eom = new Date(d.getFullYear(), d.getMonth()+1, 0);
      const remain = Math.ceil((eom - d) / 86400000) + 1;
      const w = Math.min(remain, totalDays - i);
      majorBands.push({ left: i*conf.dayWidth, width: w*conf.dayWidth, label: d.toLocaleDateString(undefined, {month:'long', year:'2-digit'}) });
      i += w;
    }
  } else {
    let i = 0;
    while (i < totalDays) {
      const d = new Date(start); d.setDate(d.getDate()+i);
      const q = Math.floor(d.getMonth()/3);
      const eq = new Date(d.getFullYear(), q*3+3, 0);
      const remain = Math.ceil((eq - d) / 86400000) + 1;
      const w = Math.min(remain, totalDays - i);
      majorBands.push({ left: i*conf.dayWidth, width: w*conf.dayWidth, label: `Q${q+1} ${d.getFullYear()}` });
      i += w;
    }
  }

  function rangeFor(task) {
    const due = task.values.due ? new Date(task.values.due) : null;
    const startV = task.values.start ? new Date(task.values.start) : null;
    if (!due && !startV) return null;
    let s2, e;
    if (startV && due) { s2 = startV; e = due; }
    else if (due) {
      const estMin = parseDuration(task.values.est) || 0;
      const estDays = Math.max(0, Math.ceil(estMin / 480));
      s2 = new Date(due.getTime() - estDays*86400000); e = due;
    } else { s2 = startV; e = new Date(startV.getTime() + 86400000); }
    if (e < s2) e = s2;
    const sIdx = dayIndex(s2); const eIdx = dayIndex(e);
    const days = Math.max(1, eIdx - sIdx + 1);
    return { left: sIdx * conf.dayWidth, width: days * conf.dayWidth, sIdx, eIdx };
  }

  const todayLeft = dayIndex(today) * conf.dayWidth;

  return (
    <div className="board-wrap">
      <div className="tl-wrap">
        <div className="tl-left">
          <div className="tl-left-head">Task</div>
          {groups.map(group => (
            <React.Fragment key={group.key ?? '__default__'}>
              {group.label && (
                <div className="tl-group-label">
                  <span>{group.label}</span>
                  {groupField && <span className="tl-group-meta">{group.tasks.length}</span>}
                </div>
              )}
              {group.tasks.map(t => (
                <div key={t.id} className="tl-row-label" onClick={() => actions.setActiveTask(t.id)}>
                  <span className="prio-dot" style={{ background: taskColor(t) }}></span>
                  <span style={{overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap'}}>{t.title}</span>
                </div>
              ))}
            </React.Fragment>
          ))}
        </div>
        <div className="tl-right">
          <div className="tl-canvas" style={{width: px}}>
            <div className="tl-head">
              <div className="tl-head-row1">
                {majorBands.map((b, i) => <div key={i} className="tl-band" style={{left:b.left, width:b.width}}>{b.label}</div>)}
              </div>
              <div className="tl-head-row2" style={{borderTop:'1px solid var(--border)'}}>
                {minorBands.map((b, i) => <div key={i} className="tl-band minor" style={{left:b.left, width:b.width}}>{b.label}</div>)}
              </div>
            </div>
            {majorBands.map((b, i) => <div key={'g'+i} className="tl-grid-line" style={{left:b.left}} />)}
            {todayLeft >= 0 && todayLeft < px && <div className="tl-today" style={{left: todayLeft}} />}
            {groups.map(group => (
              <React.Fragment key={group.key ?? '__default__'}>
                {group.label && (
                  <div className="tl-group-row">
                    <span>{group.label}</span>
                  </div>
                )}
                {group.tasks.map(t => (
                  <TLRow key={t.id} task={t} project={project} range={rangeFor(t)}
                    dayWidth={conf.dayWidth} dateAtX={dateAtX} totalPx={px} />
                ))}
              </React.Fragment>
            ))}
          </div>
        </div>
      </div>
    </div>
  );
}

function TLRow({ task, project, range, dayWidth, dateAtX, totalPx }) {
  const stage = project.stages.find(st => st.id === task.stageId);
  const stageIdx = project.stages.findIndex(st => st.id === task.stageId);
  const progress = (stageIdx + 1) / project.stages.length;

  const [drag, setDrag] = React.useState(null); // {mode:'move'|'l'|'r', startX, origLeft, origWidth}
  const [previewLeft, setPreviewLeft] = React.useState(null);
  const [previewWidth, setPreviewWidth] = React.useState(null);
  const justDraggedRef = React.useRef(false);

  React.useEffect(() => {
    if (!drag) return;
    function onMove(e) {
      const dx = e.clientX - drag.startX;
      if (Math.abs(dx) > 2) justDraggedRef.current = true;
      if (drag.mode === 'move') {
        const days = Math.round(dx / dayWidth);
        setPreviewLeft(drag.origLeft + days * dayWidth);
        setPreviewWidth(drag.origWidth);
      } else if (drag.mode === 'l') {
        const days = Math.round(dx / dayWidth);
        const newLeft = drag.origLeft + days * dayWidth;
        const newWidth = drag.origWidth - days * dayWidth;
        if (newWidth >= dayWidth) { setPreviewLeft(newLeft); setPreviewWidth(newWidth); }
      } else if (drag.mode === 'r') {
        const days = Math.round(dx / dayWidth);
        const newWidth = drag.origWidth + days * dayWidth;
        if (newWidth >= dayWidth) { setPreviewLeft(drag.origLeft); setPreviewWidth(newWidth); }
      }
    }
    function onUp() {
      // commit
      if (previewLeft !== null && previewWidth !== null) {
        const newStart = dateAtX(previewLeft);
        const newEnd = dateAtX(previewLeft + previewWidth - dayWidth); // inclusive
        const patch = {};
        if (drag.mode === 'move') {
          if (task.values.start) patch.start = isoFor(newStart);
          if (task.values.due)   patch.due = isoFor(newEnd);
          if (!task.values.start && !task.values.due) {
            patch.due = isoFor(newEnd);
          }
        } else if (drag.mode === 'l') {
          patch.start = isoFor(newStart);
          if (task.values.due) patch.due = isoFor(newEnd);
        } else if (drag.mode === 'r') {
          patch.due = isoFor(newEnd);
          if (task.values.start) patch.start = isoFor(newStart);
        }
        const values = { ...task.values, ...patch };
        actions.updateTask(task.id, { values });
      }
      setDrag(null); setPreviewLeft(null); setPreviewWidth(null);
      // Suppress the click that follows mouseup if we actually dragged
      if (justDraggedRef.current) {
        setTimeout(() => { justDraggedRef.current = false; }, 50);
      }
    }
    document.addEventListener('mousemove', onMove);
    document.addEventListener('mouseup', onUp);
    return () => { document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); };
  }, [drag, previewLeft, previewWidth, dayWidth, task]);

  function startDrag(mode, e) {
    e.stopPropagation();
    e.preventDefault();
    if (!range) return;
    setDrag({ mode, startX: e.clientX, origLeft: range.left, origWidth: range.width });
    setPreviewLeft(range.left); setPreviewWidth(range.width);
  }

  // If task has no dates, render empty row with a "drop / click" affordance
  if (!range) {
    return (
      <div className="tl-row" onClick={() => actions.setActiveTask(task.id)}>
        <div className="tl-bar tl-bar-empty"
          style={{ left: 0, width: 80, opacity: 0.3 }}
          title="No dates set — open task to add">
          <span style={{ position:'relative', zIndex:1 }}>—</span>
        </div>
      </div>
    );
  }

  const pColor = taskColor(task);
  const left = previewLeft !== null ? previewLeft : range.left;
  const width = previewWidth !== null ? previewWidth : range.width;

  return (
    <div className="tl-row">
      <div className={'tl-bar' + (drag ? ' dragging' : '')}
        onClick={(e) => { if (justDraggedRef.current) { e.stopPropagation(); return; } actions.setActiveTask(task.id); }}
        onMouseDown={(e) => startDrag('move', e)}
        style={{
          left, width,
          background: `color-mix(in oklab, ${pColor} 32%, white)`,
          border: `1.5px solid color-mix(in oklab, ${pColor} 65%, white)`,
          cursor: drag?.mode === 'move' ? 'grabbing' : 'grab',
        }}>
        <div className="tl-handle tl-handle-l" onMouseDown={(e) => startDrag('l', e)} title="Drag to change start"></div>
        <div className="tl-handle tl-handle-r" onMouseDown={(e) => startDrag('r', e)} title="Drag to change end"></div>
        <span style={{position:'relative', zIndex:1, color:'var(--ink)', overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap'}}>{task.title}</span>
      </div>
      {drag && previewLeft !== null && (
        <div className="tl-drag-tooltip" style={{ left: previewLeft + previewWidth + 8 }}>
          {fmtDate(dateAtX(previewLeft))} → {fmtDate(dateAtX(previewLeft + previewWidth - dayWidth))}
        </div>
      )}
    </div>
  );
}

Object.assign(window, { SettingsScreen, ListScreen, CalendarScreen, TimelineScreen });
