// Gantt view — day-based timeline with drag to move/resize
function GanttView({ tasks, allTasks, displayIds, updateTask, reorderTasks, openTask, onAddTaskAt, t, lang, density, perms, milestones: msProp, justAddedId, workstreams, phaseConfig }) {
  const { parseDate, fmtDate, diffDays, addDays } = AOX_UTIL;
  const rawMs = msProp || window.AOX_MILESTONES || [];
  const milestones = rawMs.map(m => {
    const isNew = 'target_date' in m || 'name' in m;
    const date = isNew ? m.target_date : m.date;
    const label = isNew ? m.name : m.label;
    const labelEn = isNew ? m.name : m.labelEn;
    let color = m.color;
    if (!color || /^(amber|violet|emerald|green|blue)$/i.test(color)) {
      color = color === 'amber' ? 'var(--phase1)'
           : color === 'violet' ? 'var(--phase2)'
           : 'var(--phase3)';
    }
    return { id: m.id, _date: date, _label: label, _labelEn: labelEn, _color: color };
  });
  const canEdit = perms?.canEdit !== false;

  // Build phase → color map from phaseConfig prop (falls back to CSS vars)
  const phaseColorMap = useMemo(() => {
    const m = {};
    (phaseConfig || []).forEach(p => {
      if (p.color) m[p.phase_code] = p.color;
    });
    return m;
  }, [phaseConfig]);

  function phaseColorFor(phaseCode) {
    if (phaseColorMap[phaseCode]) return phaseColorMap[phaseCode];
    const n = (phaseCode || '').match(/\d/)?.[0] || '1';
    return `var(--phase${n})`;
  }

  // Determine range
  const [minD, maxD] = useMemo(() => {
    let min = null, max = null;
    tasks.forEach(x => {
      const s = parseDate(x.start), e = parseDate(x.end);
      if (s && (!min || s < min)) min = s;
      if (e && (!max || e > max)) max = e;
    });
    if (!min) min = new Date(2026, 3, 15);
    if (!max) max = new Date(2026, 8, 15);
    // pad
    return [addDays(min, -4), addDays(max, 4)];
  }, [tasks]);

  const [dayW, setDayW] = useState(() => {
    return parseInt(localStorage.getItem('aox_gantt_dayw') || '14');
  });
  useEffect(()=>{ localStorage.setItem('aox_gantt_dayw', String(dayW)); },[dayW]);

  const rowH = density === 'compact' ? 30 : 40;
  const totalDays = diffDays(minD, maxD) + 1;
  const totalW = totalDays * dayW;

  // Group by phase → workstream. Items keep their displayed order (sortOrder,
  // then start, then id) — SAME policy as computeDisplayIds so the bars line
  // up 1:1 with the displayed row numbers.
  const groups = useMemo(() => {
    const g = {};
    tasks.forEach(tk => {
      const key = tk.phase + ' · ' + tk.workstream;
      (g[key] = g[key] || { phase: tk.phase, track: tk.workstream, items: [] }).items.push(tk);
    });
    Object.values(g).forEach(grp => grp.items.sort((a,b) => {
      const sa = a.sortOrder, sb = b.sortOrder;
      if (sa != null && sb != null && sa !== sb) return sa - sb;
      if (sa != null && sb == null) return -1;
      if (sa == null && sb != null) return 1;
      if (a.start !== b.start) return (a.start||'').localeCompare(b.start||'');
      return AOX_UTIL.compareWbs(a.id, b.id);
    }));
    const wsOrder = workstreams && workstreams.length
      ? Object.fromEntries(workstreams.map(ws => [ws.key, ws.sort_order]))
      : AOX_UTIL.WBS_WORKSTREAM_N;
    const ordered = Object.entries(g).sort((a,b) => {
      const pa = AOX_UTIL.phaseNum(a[1].phase), pb = AOX_UTIL.phaseNum(b[1].phase);
      if (pa !== pb) return pa - pb;
      return (wsOrder[a[1].track] ?? 9) - (wsOrder[b[1].track] ?? 9);
    });
    return ordered;
  }, [tasks]);

  // Build month header
  const months = useMemo(() => {
    const out = [];
    let cur = new Date(minD.getFullYear(), minD.getMonth(), 1);
    while (cur <= maxD) {
      const next = new Date(cur.getFullYear(), cur.getMonth()+1, 1);
      const startIdx = Math.max(0, diffDays(minD, cur));
      const endIdx = Math.min(totalDays, diffDays(minD, next));
      out.push({
        label: `${cur.getFullYear()}.${String(cur.getMonth()+1).padStart(2,'0')}`,
        x: startIdx * dayW,
        w: (endIdx - startIdx) * dayW
      });
      cur = next;
    }
    return out;
  }, [minD, maxD, dayW, totalDays]);

  // Weeks grid
  const weekLines = useMemo(() => {
    const out = [];
    for (let i=0; i<totalDays; i++) {
      const d = addDays(minD, i);
      if (d.getDay() === 1) out.push(i * dayW);
    }
    return out;
  }, [minD, totalDays, dayW]);

  // Today line
  const today = new Date();
  const todayX = diffDays(minD, today) * dayW;

  // Drag state
  const [drag, setDrag] = useState(null); // { id, mode: 'move'|'start'|'end', startX, origStart, origEnd }

  function onBarMouseDown(e, task, mode) {
    if (!canEdit) return;
    if (e.button !== 0) return;
    e.preventDefault();
    e.stopPropagation();
    setDrag({
      id: task.id, mode,
      startX: e.clientX,
      origStart: task.start, origEnd: task.end
    });
  }

  useEffect(() => {
    if (!drag) return;
    function onMove(e) {
      const dx = e.clientX - drag.startX;
      const days = Math.round(dx / dayW);
      const tk = tasks.find(x => x.id === drag.id);
      if (!tk) return;
      let s = parseDate(drag.origStart), en = parseDate(drag.origEnd);
      if (drag.mode === 'move') { s = addDays(s, days); en = addDays(en, days); }
      else if (drag.mode === 'start') { s = addDays(s, days); if (s > en) s = en; }
      else if (drag.mode === 'end') { en = addDays(en, days); if (en < s) en = s; }
      updateTask(tk.id, {
        start: fmtDate(s), end: fmtDate(en),
        duration: diffDays(s, en) + 1
      });
    }
    function onUp(){ setDrag(null); }
    window.addEventListener('mousemove', onMove);
    window.addEventListener('mouseup', onUp);
    return () => {
      window.removeEventListener('mousemove', onMove);
      window.removeEventListener('mouseup', onUp);
    };
  }, [drag, dayW, tasks, updateTask]);

  // ---- Row drag (reorder + cross-phase drop) ---------------------------
  // State shape: { id, fromKey, clientY, targetKey, targetIndex }
  //   `targetIndex` is the INSERT slot in the target group (0..N). When
  //   hovering below the last item, targetIndex === items.length.
  const [rowDrag, setRowDrag] = useState(null);
  const rowDragRef = useRef(null);
  rowDragRef.current = rowDrag;

  /**
   * Compute a flat row layout: header row, then one row per item, with
   * absolute Y for each. Used both for visual placement AND for mapping
   * a pointer Y back to a (groupKey, insertIndex) drop target.
   */
  const flatLayout = useMemo(() => {
    let y = 0;
    const rows = [];       // { type:'header'|'bar', y, key, task?, group }
    const groupMeta = [];  // { key, phase, track, headerY, firstItemY, lastItemY, items }
    groups.forEach(([key, grp]) => {
      const headerY = y;
      rows.push({ type:'header', y, key, phase: grp.phase, track: grp.track, count: grp.items.length });
      y += rowH;
      const firstItemY = y;
      grp.items.forEach(task => {
        rows.push({ type:'bar', y, key, task });
        y += rowH;
      });
      groupMeta.push({ key, phase: grp.phase, track: grp.track, headerY, firstItemY, lastItemY: y, items: grp.items });
    });
    return { rows, groupMeta, contentH: y };
  }, [groups, rowH]);
  const contentH = flatLayout.contentH;
  const groupRows = groups.map(([key, grp], gi) => {
    const meta = flatLayout.groupMeta[gi];
    return {
      header: { type:'header', y: meta.headerY, key, phase: grp.phase, track: grp.track, items: grp.items },
      items: grp.items.map((task, i) => ({ type:'bar', task, y: meta.firstItemY + i * rowH })),
    };
  });

  function onRowDragStart(e, task, groupKey) {
    if (!canEdit || !reorderTasks) return;
    if (e.button !== 0) return;
    e.preventDefault();
    setRowDrag({ id: task.id, fromKey: groupKey, clientY: e.clientY, targetKey: groupKey, targetIndex: null });
  }

  useEffect(() => {
    if (!rowDrag) return;

    function findDropTarget(clientY) {
      // Translate clientY to local Y inside the body using the left-column's
      // scroll position. Both panes scroll together so either reference works.
      const pane = leftScrollRef.current;
      if (!pane) return null;
      const rect = pane.getBoundingClientRect();
      // Account for the sticky 56px header at the top of the left pane
      const localY = clientY - rect.top - 56 + pane.scrollTop;
      // Walk groupMeta to find which one contains this Y
      for (const gm of flatLayout.groupMeta) {
        if (localY >= gm.headerY && localY < gm.lastItemY + rowH * 0.5) {
          // Inside this group (or just past the last row). Map Y → insertIndex.
          const relative = localY - gm.firstItemY;
          let idx = Math.max(0, Math.round(relative / rowH));
          idx = Math.min(idx, gm.items.length);
          return { key: gm.key, index: idx, phase: gm.phase, track: gm.track };
        }
      }
      return null;
    }

    function onMove(e) {
      const tgt = findDropTarget(e.clientY);
      setRowDrag(prev => prev ? { ...prev, clientY: e.clientY, targetKey: tgt?.key ?? prev.fromKey, targetIndex: tgt?.index ?? null } : prev);
    }
    function onUp() {
      const rd = rowDragRef.current;
      setRowDrag(null);
      if (!rd || rd.targetIndex == null || !reorderTasks) return;
      const srcTask = tasks.find(x => x.id === rd.id);
      if (!srcTask) return;
      const targetMeta = flatLayout.groupMeta.find(g => g.key === rd.targetKey);
      if (!targetMeta) return;

      // Build the target group's new item order
      const sameGroup = rd.fromKey === rd.targetKey;
      const targetItems = targetMeta.items.filter(x => x.id !== rd.id);
      let insertAt = rd.targetIndex;
      if (sameGroup) {
        // If dragging down within same group, the original slot has been removed,
        // so subtract 1 when insertAt > original position.
        const origIdx = targetMeta.items.findIndex(x => x.id === rd.id);
        if (origIdx !== -1 && insertAt > origIdx) insertAt -= 1;
      }
      insertAt = Math.max(0, Math.min(insertAt, targetItems.length));
      const newOrder = [...targetItems.slice(0, insertAt), srcTask, ...targetItems.slice(insertAt)];

      // Emit updates with sortOrder = 10, 20, 30… (sparse so future inserts
      // rarely need a full renumber). Only push rows whose values actually change.
      const updates = [];
      newOrder.forEach((tk, i) => {
        const newSort = (i + 1) * 10;
        const patch = { id: tk.id };
        let changed = false;
        if (tk.sortOrder !== newSort) { patch.sortOrder = newSort; changed = true; }
        if (tk.id === srcTask.id && !sameGroup) {
          if (tk.phase !== targetMeta.phase)       { patch.phase = targetMeta.phase; changed = true; }
          if (tk.workstream !== targetMeta.track)  { patch.workstream = targetMeta.track; changed = true; }
        }
        if (changed) updates.push(patch);
      });
      if (updates.length) reorderTasks(updates);
    }
    window.addEventListener('mousemove', onMove);
    window.addEventListener('mouseup', onUp);
    return () => {
      window.removeEventListener('mousemove', onMove);
      window.removeEventListener('mouseup', onUp);
    };
  }, [rowDrag, tasks, flatLayout, reorderTasks, rowH]);

  // ---- Scroll sync: left list ↔ right timeline ------------------------
  const leftScrollRef = useRef(null);
  const rightScrollRef = useRef(null);
  useEffect(() => {
    const l = leftScrollRef.current, r = rightScrollRef.current;
    if (!l || !r) return;
    let syncing = false;
    const fromL = () => { if (syncing) return; syncing = true; r.scrollTop = l.scrollTop; syncing = false; };
    const fromR = () => { if (syncing) return; syncing = true; l.scrollTop = r.scrollTop; syncing = false; };
    l.addEventListener('scroll', fromL, { passive: true });
    r.addEventListener('scroll', fromR, { passive: true });
    return () => { l.removeEventListener('scroll', fromL); r.removeEventListener('scroll', fromR); };
  }, []);

  // Compute rows layout (replaced by flatLayout above — keep legacy ref)
  let cursor = contentH;

  const leftColW = 260;

  return (
    <div style={{
      display:'grid',
      gridTemplateColumns: `${leftColW}px 1fr`,
      height: '100%',
      background: 'var(--bg)'
    }}>
      {/* LEFT — task list */}
      <div ref={leftScrollRef} style={{
        borderRight: '1px solid var(--border)',
        overflow: 'auto',
        background: 'var(--bg-solid)',
        position: 'relative',
      }}>
        <div style={{
          height: 56,
          padding: '8px 14px',
          display:'flex', flexDirection:'column', justifyContent:'flex-end',
          borderBottom: '1px solid var(--border)',
          position: 'sticky', top: 0, background: 'var(--bg-solid)', zIndex: 2
        }}>
          <div style={{fontSize: 11, color: 'var(--fg-muted)', textTransform:'uppercase', letterSpacing: 0.06, fontWeight: 600}}>
            {t.labels.task}
          </div>
          <div style={{fontSize: 12, color: 'var(--fg-muted)', marginTop: 2}}>
            {tasks.length} {t.labels.tasksCount}
          </div>
        </div>
        <div style={{ position: 'relative' }}>
          {groupRows.map(({header, items}) => (
            <React.Fragment key={header.key}>
              <div className="group-header-row" style={{
                height: rowH, padding: '0 14px',
                display:'flex', alignItems:'center', gap: 8,
                background: 'var(--surface-2)',
                borderBottom: '1px solid var(--divider)',
                fontSize: 12, fontWeight: 600
              }}>
                <PhaseTag phase={header.phase} color={phaseColorFor(header.phase)}/>
                <TrackTag track={header.track} workstreams={workstreams}/>
                <span style={{marginLeft:'auto', display:'inline-flex', alignItems:'center', gap: 8}}>
                  {perms?.canCreate && onAddTaskAt && (
                    <button
                      type="button"
                      className="group-add-btn"
                      onClick={(e) => { e.stopPropagation(); onAddTaskAt(header.phase, header.track); }}
                      aria-label={`Phase ${AOX_UTIL.phaseNum(header.phase)} · ${header.track} 에 새 태스크 추가`}
                      title={t.labels.addTask || '+ Add task'}>
                      +
                    </button>
                  )}
                  <span style={{color:'var(--fg-muted)', fontWeight:400, fontSize:11}}>{header.items.length}</span>
                </span>
              </div>
              {items.map(({task}) => {
                const isDragging = rowDrag && rowDrag.id === task.id;
                const dispId = displayIds?.[task.id] || task.id;
                const isJustAdded = task.id === justAddedId;
                return (
                  <div key={task.id} onClick={() => { if (!rowDrag) openTask(task); }}
                    className={isJustAdded ? 'aox-just-added' : undefined}
                    style={{
                    height: rowH, padding: '0 14px 0 10px',
                    display:'flex', alignItems:'center', gap: 8,
                    borderBottom: '1px solid var(--divider)',
                    fontSize: 12.5, cursor: 'pointer',
                    overflow: 'hidden', whiteSpace:'nowrap', textOverflow:'ellipsis',
                    opacity: isDragging ? 0.4 : 1,
                    background: isDragging ? 'var(--hover)' : undefined,
                  }}>
                    {/* drag handle */}
                    {canEdit && reorderTasks && (
                      <span
                        onMouseDown={(e)=>onRowDragStart(e, task, header.key)}
                        onClick={(e)=>e.stopPropagation()}
                        title={lang==='en'?'Drag to reorder':'드래그해서 순서 변경'}
                        style={{
                          width: 14, height: 14, display:'inline-flex',
                          alignItems:'center', justifyContent:'center',
                          cursor:'grab', color:'var(--fg-subtle)',
                          flexShrink: 0, userSelect:'none',
                        }}>⋮⋮</span>
                    )}
                    <span className="mono muted" style={{fontSize:10.5, minWidth: 36}}>{dispId}</span>
                    <span style={{flex:1, overflow:'hidden', textOverflow:'ellipsis'}}>{task.task}</span>
                    {task.links?.length > 0 && <span style={{fontSize:10, color:'var(--fg-muted)', flexShrink:0}}>🔗</span>}
                  </div>
                );
              })}
            </React.Fragment>
          ))}
          {/* Drop indicator in left pane */}
          {rowDrag && rowDrag.targetIndex != null && (() => {
            const gm = flatLayout.groupMeta.find(g => g.key === rowDrag.targetKey);
            if (!gm) return null;
            const y = gm.firstItemY + rowDrag.targetIndex * rowH;
            const crossGroup = rowDrag.fromKey !== rowDrag.targetKey;
            return (
              <div style={{
                position:'absolute', left:0, right:0, top: y - 1, height: 2,
                background: crossGroup ? 'var(--accent)' : 'var(--fg)',
                zIndex: 5, pointerEvents:'none',
              }}/>
            );
          })()}
        </div>
      </div>

      {/* RIGHT — timeline */}
      <div ref={rightScrollRef} style={{ overflow: 'auto', position: 'relative' }}>
        <div style={{ width: totalW, position: 'relative' }}>
          {/* Header: months + days */}
          <div style={{
            position: 'sticky', top: 0, zIndex: 3,
            background: 'var(--bg-solid)',
            borderBottom: '1px solid var(--border)'
          }}>
            <div style={{ height: 28, position:'relative' }}>
              {months.map((m,i) => (
                <div key={i} style={{
                  position:'absolute', left: m.x, width: m.w, top:0, bottom:0,
                  display:'flex', alignItems:'center', paddingLeft:10,
                  fontSize: 11.5, fontWeight: 600, color:'var(--fg)',
                  borderLeft: '1px solid var(--divider)'
                }}>{m.label}</div>
              ))}
            </div>
            <div style={{ height: 28, position:'relative', borderTop:'1px solid var(--divider)' }}>
              {Array.from({length: totalDays}).map((_,i) => {
                const d = addDays(minD, i);
                const isMonday = d.getDay() === 1;
                const isWeekend = d.getDay() === 0 || d.getDay() === 6;
                return (
                  <div key={i} style={{
                    position:'absolute', left: i*dayW, width: dayW,
                    top:0, bottom:0,
                    display:'flex', alignItems:'center', justifyContent:'center',
                    fontSize: 10, color: isWeekend ? 'var(--fg-subtle)' : 'var(--fg-muted)',
                    borderLeft: isMonday ? '1px solid var(--divider)' : 'none',
                  }}>{d.getDate()}</div>
                );
              })}
            </div>
          </div>

          {/* Body */}
          <div style={{ position:'relative', height: contentH }}>
            {/* Weekend shading */}
            {Array.from({length: totalDays}).map((_,i) => {
              const d = addDays(minD, i);
              if (d.getDay() !== 0 && d.getDay() !== 6) return null;
              return <div key={'we'+i} style={{
                position:'absolute', left: i*dayW, width: dayW,
                top:0, bottom:0,
                background: 'var(--hover)',
              }}/>;
            })}
            {/* Week grid lines */}
            {weekLines.map((x,i) => (
              <div key={'wl'+i} style={{
                position:'absolute', left:x, top:0, bottom:0,
                borderLeft:'1px solid var(--divider)'
              }}/>
            ))}
            {/* Milestone lines */}
            {milestones.map(m => {
              const x = diffDays(minD, parseDate(m._date)) * dayW;
              return (
                <div key={m.id} style={{
                  position:'absolute', left: x, top: 0, bottom: 0,
                  borderLeft: '2px dashed ' + m._color,
                  pointerEvents: 'none'
                }}>
                  <div style={{
                    position:'absolute', top: -22, left: 6,
                    fontSize: 10.5, fontWeight: 600,
                    color: m._color,
                    whiteSpace:'nowrap'
                  }}>◆ {lang === 'en' ? m._labelEn : m._label}</div>
                </div>
              );
            })}
            {/* Today */}
            {todayX >= 0 && todayX <= totalW && (
              <div style={{
                position:'absolute', left: todayX, top:0, bottom:0,
                borderLeft:'2px solid var(--status-blocked)',
                boxShadow: '0 0 6px rgba(255,59,48,0.3)',
                pointerEvents:'none', zIndex: 2
              }}/>
            )}

            {/* Row separators */}
            {groupRows.map(({header, items}) => (
              <React.Fragment key={header.key}>
                <div style={{
                  position:'absolute', left:0, right:0, top: header.y, height: rowH,
                  background: 'var(--surface-2)',
                  borderBottom: '1px solid var(--divider)',
                  opacity: 0.5
                }}/>
                {items.map(({task, y}) => (
                  <div key={'r'+task.id} style={{
                    position:'absolute', left:0, right:0, top: y, height: rowH,
                    borderBottom:'1px solid var(--divider)'
                  }}/>
                ))}
              </React.Fragment>
            ))}

            {/* Drop indicator — rendered in the right pane. We draw a horizontal
                line at the insertion Y of the target group. */}
            {rowDrag && rowDrag.targetIndex != null && (() => {
              const gm = flatLayout.groupMeta.find(g => g.key === rowDrag.targetKey);
              if (!gm) return null;
              const y = gm.firstItemY + rowDrag.targetIndex * rowH;
              const crossGroup = rowDrag.fromKey !== rowDrag.targetKey;
              return (
                <div style={{
                  position:'absolute', left:0, right:0, top: y - 1, height: 2,
                  background: crossGroup ? 'var(--accent)' : 'var(--fg)',
                  boxShadow: crossGroup ? '0 0 8px var(--accent)' : '0 0 4px rgba(0,0,0,0.4)',
                  zIndex: 5, pointerEvents:'none',
                }}/>
              );
            })()}

            {/* Bars */}
            {groupRows.map(({items}) => items.map(({task, y}) => {
              const s = parseDate(task.start);
              const e = parseDate(task.end);
              if (!s || !e) return null;
              const x = diffDays(minD, s) * dayW;
              const w = Math.max(dayW, (diffDays(s,e) + 1) * dayW);
              const barColor = phaseColorFor(task.phase);
              const barY = y + (rowH - 22) / 2;
              const done = task.status === 'Done' || task.progress >= 100;
              const blocked = task.status === 'Blocked';
              const inProgress = task.status === 'In Progress';
              const notStarted = task.status === 'Not Started';
              const barBg = done ? 'var(--status-done)'
                : blocked ? 'var(--status-blocked)'
                : barColor;
              return (
                <div key={task.id}
                  onMouseDown={(ev)=>onBarMouseDown(ev, task, 'move')}
                  onClick={(ev)=>{ if (!drag) openTask(task); }}
                  style={{
                    position:'absolute',
                    left: x, top: barY, width: w, height: 22,
                    borderRadius: 6,
                    background: notStarted
                      ? `color-mix(in srgb, ${barColor} 22%, var(--bg-solid))`
                      : barBg,
                    opacity: 1,
                    border: notStarted
                      ? `1px dashed color-mix(in srgb, ${barColor} 65%, transparent)`
                      : blocked ? '1.5px solid var(--status-blocked)' : 'none',
                    display:'flex', alignItems:'center',
                    paddingLeft: 8, paddingRight: 8,
                    fontSize: 10.5, fontWeight: 600,
                    color: notStarted ? `color-mix(in srgb, ${barColor} 85%, var(--fg))` : '#fff',
                    letterSpacing: -0.005,
                    overflow: 'hidden',
                    cursor: canEdit ? 'grab' : 'pointer', userSelect:'none',
                    boxShadow: '0 1px 3px rgba(0,0,0,0.15)'
                  }}>
                  {/* progress overlay — filled solid + thin bar */}
                  {inProgress && task.progress > 0 && task.progress < 100 && (
                    <>
                      <div style={{
                        position:'absolute', left:0, top:0, bottom:0,
                        width: task.progress + '%',
                        background: 'rgba(255,255,255,0.38)',
                        borderRight: '1.5px solid rgba(255,255,255,0.85)',
                        pointerEvents:'none'
                      }}/>
                      <div style={{
                        position:'absolute', left:0, bottom:0, height: 3,
                        width: task.progress + '%',
                        background: '#fff',
                        pointerEvents:'none',
                        borderBottomLeftRadius: 6,
                        borderBottomRightRadius: task.progress >= 100 ? 6 : 0,
                      }}/>
                    </>
                  )}
                  {/* Done checkmark */}
                  {done && (
                    <span style={{
                      position:'absolute', left: 6, top:'50%', transform:'translateY(-50%)',
                      width: 14, height: 14, borderRadius: '50%',
                      background: 'rgba(255,255,255,0.95)',
                      color: 'var(--status-done)',
                      display:'flex', alignItems:'center', justifyContent:'center',
                      fontSize: 10, fontWeight: 900,
                      pointerEvents:'none'
                    }}>✓</span>
                  )}
                  {/* Blocked hatch + warning icon */}
                  {blocked && (
                    <>
                      <div style={{
                        position:'absolute', inset:0,
                        background: 'repeating-linear-gradient(45deg, transparent, transparent 5px, rgba(255,255,255,0.25) 5px, rgba(255,255,255,0.25) 9px)',
                        pointerEvents:'none', borderRadius: 6,
                      }}/>
                      <span style={{
                        position:'absolute', left: 6, top:'50%', transform:'translateY(-50%)',
                        fontSize: 11, fontWeight: 900, color:'#fff', pointerEvents:'none'
                      }}>!</span>
                    </>
                  )}
                  {/* resize handles */}
                  {canEdit && (<>
                    <div onMouseDown={(ev)=>onBarMouseDown(ev, task, 'start')} style={{
                      position:'absolute', left:0, top:0, bottom:0, width:5, cursor:'ew-resize'
                    }}/>
                    <div onMouseDown={(ev)=>onBarMouseDown(ev, task, 'end')} style={{
                      position:'absolute', right:0, top:0, bottom:0, width:5, cursor:'ew-resize'
                    }}/>
                  </>)}
                  <span style={{
                    position:'relative',
                    paddingLeft: (done || blocked) ? 16 : 0,
                    overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap'
                  }}>
                    {task.task}
                  </span>
                  {/* Progress % label on right */}
                  {inProgress && task.progress > 0 && (
                    <span style={{
                      position:'absolute', right: 8, top:'50%', transform:'translateY(-50%)',
                      fontSize: 10, fontWeight: 700, color:'#fff',
                      background:'rgba(0,0,0,0.25)', padding:'1px 5px', borderRadius: 4,
                      pointerEvents:'none'
                    }}>{task.progress}%</span>
                  )}
                </div>
              );
            }))}
          </div>
        </div>

        {/* Zoom control + Legend */}
        <div style={{
          position:'sticky', bottom: 14, marginLeft: 'auto',
          width: 'fit-content', right: 14, float:'right',
          display:'flex', alignItems:'center', gap: 10,
          padding: 5,
          background: 'var(--bg-elev)',
          backdropFilter: 'blur(20px)',
          borderRadius: 999,
          border:'1px solid var(--border)',
          boxShadow: 'var(--shadow-2)',
          marginRight: 14,
        }}>
          {/* Legend chips */}
          <div style={{display:'flex', alignItems:'center', gap: 8, paddingLeft: 8, fontSize: 10.5, color:'var(--fg-muted)'}}>
            <span style={{display:'inline-flex', alignItems:'center', gap: 4}}>
              <span style={{
                width: 18, height: 10, borderRadius: 3,
                background: 'color-mix(in srgb, var(--phase3) 22%, var(--bg-solid))',
                border: '1px dashed color-mix(in srgb, var(--phase3) 65%, transparent)'
              }}/>
              {lang==='en'?'Not started':'시작 전'}
            </span>
            <span style={{display:'inline-flex', alignItems:'center', gap: 4}}>
              <span style={{
                width: 18, height: 10, borderRadius: 3, position:'relative',
                background: 'var(--phase3)', overflow:'hidden'
              }}>
                <span style={{position:'absolute', inset:0, left:0, width:'55%', background:'rgba(255,255,255,0.4)'}}/>
                <span style={{position:'absolute', left:0, bottom:0, width:'55%', height: 2, background:'#fff'}}/>
              </span>
              {lang==='en'?'In progress':'진행 중'}
            </span>
            <span style={{display:'inline-flex', alignItems:'center', gap: 4}}>
              <span style={{
                width: 18, height: 10, borderRadius: 3,
                background: 'var(--status-blocked)',
                backgroundImage: 'repeating-linear-gradient(45deg, transparent, transparent 3px, rgba(255,255,255,0.35) 3px, rgba(255,255,255,0.35) 5px)'
              }}/>
              {lang==='en'?'Blocked':'지연'}
            </span>
            <span style={{display:'inline-flex', alignItems:'center', gap: 4}}>
              <span style={{
                width: 18, height: 10, borderRadius: 3,
                background: 'var(--status-done)',
                display:'flex', alignItems:'center', justifyContent:'center',
                color:'#fff', fontSize: 8, fontWeight: 900
              }}>✓</span>
              {lang==='en'?'Done':'완료'}
            </span>
          </div>
          <span style={{width: 1, height: 18, background:'var(--border)'}}/>
          <button className="iconbtn" onClick={()=>setDayW(x => Math.max(6, x-2))}><Icon name="chevron-left" size={14}/></button>
          <span className="mono" style={{fontSize:11}}>{dayW}px/{t.labels.days}</span>
          <button className="iconbtn" onClick={()=>setDayW(x => Math.min(60, x+2))}><Icon name="chevron-right" size={14}/></button>
        </div>
      </div>
    </div>
  );
}

window.GanttView = GanttView;
