/* global React, ReactDOM */
const { useState, useMemo, useEffect, useRef, useCallback } = React;

// ============================================================
// CONFIG
// ============================================================

// Today as a real date (zeroed to midnight local).
const TODAY = (() => { const d = new Date(); d.setHours(0,0,0,0); return d; })();

// Linear access is proxied by a Cloudflare Worker (worker/src/index.js).
// The API key lives in Worker env; the frontend never sees it. The user
// picks which team to visualize on first load, same as before.
const LS_KEY      = "squads-allocation-v4-state";
const LS_KEY_TEAM = "squads-allocation-v4-team";   // { id, name }

const SQUAD_PALETTE = [
  { stroke: "#aafbb2", fill: "rgba(170,251,178,0.14)" }, // green
  { stroke: "#83bafd", fill: "rgba(131,186,253,0.14)" }, // blue
  { stroke: "#ffb37a", fill: "rgba(255,179,122,0.14)" }, // orange
  { stroke: "#b69bff", fill: "rgba(182,155,255,0.14)" }, // purple
  { stroke: "#f0d56b", fill: "rgba(240,213,107,0.14)" }, // yellow
  { stroke: "#7ad9c8", fill: "rgba(122,217,200,0.14)" }, // teal
  { stroke: "#f49ad6", fill: "rgba(244,154,214,0.14)" }, // pink
];

const NATO = ["Alpha","Bravo","Charlie","Delta","Echo","Foxtrot","Golf","Hotel","India","Juliet",
  "Kilo","Lima","Mike","November","Oscar","Papa","Quebec","Romeo","Sierra","Tango",
  "Uniform","Victor","Whiskey","X-ray","Yankee","Zulu"];

// Eisenhower priority quadrants. Each project may carry one of these as its
// priority; that quadrant's color then drives the project's color everywhere
// (timeline bar, list row, drawer, matrix). Local-only field, preserved
// across Refresh from Linear.
const QUADRANTS = [
  { id: "q1", label: "Do",       sublabel: "Important · Urgent",     color: "#ef4444" },
  { id: "q2", label: "Plan",     sublabel: "Important · Not urgent", color: "#3b82f6" },
  { id: "q3", label: "Delegate", sublabel: "Urgent · Not important", color: "#f59e0b" },
  { id: "q4", label: "Drop",     sublabel: "Neither",                color: "#6b7280" },
];
function quadrantById(id) { return QUADRANTS.find(q => q.id === id) || null; }
function quadrantPalette(id) {
  const q = quadrantById(id);
  if (!q) return null;
  // Same shape as squadColor() — { stroke, fill }
  return { stroke: q.color, fill: q.color + "26" };
}

const PHASE_STYLE = {
  forming:     { fill: "rgba(255, 179, 122, 0.08)", stroke: "#ffb37a", label: "Forming",
                 pattern: "repeating-linear-gradient(45deg, rgba(255,179,122,0.10) 0 6px, transparent 6px 12px)" },
  maintenance: { fill: "rgba(131, 186, 253, 0.08)", stroke: "#83bafd", label: "Maintenance",
                 pattern: "repeating-linear-gradient(135deg, rgba(131,186,253,0.10) 0 6px, transparent 6px 12px)" },
  waiting:     { fill: "rgba(168, 168, 177, 0.06)", stroke: "#a8a8b1", label: "Waiting",
                 pattern: "none" },
  disbanded:   { fill: "rgba(255, 142, 142, 0.04)", stroke: "#ff8e8e", label: "Disbanded",
                 pattern: "repeating-linear-gradient(45deg, rgba(255,142,142,0.10) 0 4px, transparent 4px 10px)" },
};

const STATUS = {
  in_progress: { label: "In Progress", color: "var(--primary)" },
  backlog:     { label: "Backlog",     color: "#f0d56b" },
  completed:   { label: "Completed",   color: "var(--fg-3)" },
};

const STATE_LABEL = {
  active:      { label: "Active",      color: "var(--primary)" },
  forming:     { label: "Forming",     color: "#ffb37a" },
  maintenance: { label: "Maintenance", color: "#83bafd" },
  waiting:     { label: "Waiting",     color: "#a8a8b1" },
  disbanded:   { label: "Disbanded",   color: "#ff8e8e" },
  idle:        { label: "Idle",        color: "var(--fg-3)" },
  none:        { label: "—",           color: "var(--fg-3)" },
};

// ============================================================
// PERSISTENCE
// ============================================================

function loadJSON(key) {
  try { const raw = localStorage.getItem(key); return raw ? JSON.parse(raw) : null; }
  catch (e) { return null; }
}
function saveJSON(key, value) {
  try { localStorage.setItem(key, JSON.stringify(value)); } catch (e) {}
}
function clearKey(key) { try { localStorage.removeItem(key); } catch (e) {} }

// ============================================================
// HELPERS
// ============================================================

const parseDate = (s) => {
  const [y, m, d] = s.split("-").map(Number);
  return new Date(y, m - 1, d);
};
const fmtShort = (d) => d.toLocaleDateString("en-US", { month: "short", day: "numeric" });
const daysBetween = (a, b) => Math.round((b - a) / 86400000);
const fmtISO = (d) => `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,"0")}-${String(d.getDate()).padStart(2,"0")}`;
const addDays = (dateStr, days) => {
  const d = parseDate(dateStr);
  d.setDate(d.getDate() + days);
  return fmtISO(d);
};

function initialsFor(name) {
  if (!name) return "??";
  const parts = name.trim().split(/\s+/);
  if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase();
  return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
}

function personById(state, id) { return state.people.find(p => p.id === id); }
function squadById(state, id)  { return state.squads.find(s => s.id === id); }

function squadColor(state, id) {
  const idx = state.squads.findIndex(s => s.id === id);
  if (idx < 0) return { stroke: "#666", fill: "rgba(102,102,102,0.14)" };
  return SQUAD_PALETTE[idx % SQUAD_PALETTE.length];
}

// Color a project should be drawn in. Priority (Eisenhower quadrant) wins;
// otherwise fall back to its squad's color.
function projectColor(state, project) {
  if (!project) return { stroke: "#666", fill: "rgba(102,102,102,0.14)" };
  const fromPriority = quadrantPalette(project.priority);
  if (fromPriority) return fromPriority;
  return squadColor(state, project.squadId);
}

function squadStateAt(state, squad, date) {
  if (!squad) return { kind: "none" };
  const activeProj = state.projects.find(p =>
    p.squadId === squad.id && p.status !== "completed" &&
    p.start && p.end &&
    date >= parseDate(p.start) && date <= parseDate(p.end));
  if (activeProj) return { kind: "active", project: activeProj };
  for (const ph of (squad.phases || [])) {
    const s = parseDate(ph.start);
    const e = ph.end ? parseDate(ph.end) : new Date(2999, 0, 1);
    if (date >= s && date <= e) return { kind: ph.kind, phase: ph };
  }
  return { kind: "idle" };
}

function detectConflicts(state) {
  const squadActive = {};
  state.projects.forEach(p => {
    if (p.status === "completed") return;
    if (!p.squadId) return;
    if (!p.start || !p.end) return;
    const start = parseDate(p.start);
    const end   = parseDate(p.end);
    const past    = new Date(TODAY); past.setDate(past.getDate() - 21);
    const horizon = new Date(TODAY); horizon.setDate(horizon.getDate() + 21);
    if (end >= past && start <= horizon) {
      (squadActive[p.squadId] = squadActive[p.squadId] || []).push(p);
    }
  });
  const squadConflicts = {};
  Object.entries(squadActive).forEach(([sid, projs]) => {
    if (projs.length > 1) squadConflicts[sid] = projs;
  });

  const personSquads = {};
  state.squads.forEach(s => s.members.forEach(pid => {
    (personSquads[pid] = personSquads[pid] || []).push(s.id);
  }));
  const personConflicts = {};
  Object.entries(personSquads).forEach(([pid, sids]) => {
    if (sids.length > 1) personConflicts[pid] = sids;
  });

  const noSquad = state.projects.filter(p => p.status !== "completed" && !p.squadId);
  const noLead  = state.projects.filter(p => p.status !== "completed" && (!p.leads || p.leads.length === 0));

  return { squadConflicts, personConflicts, noSquad, noLead, squadActive, personSquads };
}

// ============================================================
// LINEAR GRAPHQL CLIENT
// ============================================================

// Same-origin proxy hosted by the Cloudflare Worker (see worker/src/index.js).
// The Worker adds the Linear Authorization header server-side and injects the
// team ID into queries that declare $teamId.
const LINEAR_ENDPOINT = "/api/graphql";

async function linearQuery(query, variables = {}) {
  const res = await fetch(LINEAR_ENDPOINT, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ query, variables }),
  });
  if (!res.ok) {
    const text = await res.text();
    throw new Error(`Linear API ${res.status}: ${text.slice(0, 200)}`);
  }
  const json = await res.json();
  if (json.errors && json.errors.length) {
    throw new Error(json.errors.map(e => e.message).join("; "));
  }
  return json.data;
}

const Q_VIEWER_AND_TEAMS = `
  query ViewerAndTeams {
    viewer { id name email }
    teams(first: 100) {
      nodes { id name key }
    }
  }
`;

// Linear enforces a query-complexity budget. Nesting team → projects → members
// trips the limit quickly, so we split the fetch into a few smaller queries.

const Q_TEAM_META = `
  query TeamMeta($teamId: String!) {
    team(id: $teamId) {
      id
      name
      members(first: 200) {
        nodes { id name displayName active }
      }
    }
  }
`;

const Q_TEAM_PROJECTS = `
  query TeamProjects($teamId: String!, $after: String) {
    team(id: $teamId) {
      projects(first: 50, after: $after) {
        pageInfo { hasNextPage endCursor }
        nodes {
          id
          name
          icon
          description
          startDate
          targetDate
          progress
          health
          state
          lead { id }
        }
      }
    }
  }
`;

// Batched per-project member fetch using GraphQL aliases. One request fetches
// member IDs for up to PROJECT_MEMBERS_BATCH projects at a time.
const PROJECT_MEMBERS_BATCH = 10;

function buildProjectMembersQuery(projectIds) {
  // Aliases must be valid identifiers; project IDs are UUIDs, so we use index.
  const fields = projectIds.map((id, i) =>
    `  p${i}: project(id: ${JSON.stringify(id)}) { id members(first: 50) { nodes { id } } }`
  ).join("\n");
  return `query ProjectMembers {\n${fields}\n}`;
}

// Lookup names for user IDs that weren't returned by team.members (e.g. a
// project member who isn't on the team). Batched the same way.
const USERS_LOOKUP_BATCH = 25;

function buildUsersLookupQuery(userIds) {
  const fields = userIds.map((id, i) =>
    `  u${i}: user(id: ${JSON.stringify(id)}) { id name displayName }`
  ).join("\n");
  return `query UsersLookup {\n${fields}\n}`;
}

// Coalesce the three (or four) queries into the single `team` payload shape
// that deriveFreshState / mergeLinearData expect.
async function fetchTeamPayload(teamId) {
  // 1) Team meta + members
  const meta = await linearQuery(Q_TEAM_META, { teamId });
  if (!meta.team) throw new Error("Team not found.");
  const teamMembers = meta.team.members?.nodes || [];
  const knownUsers = new Map(teamMembers.map(u => [u.id, u]));

  // 2) Team projects (paginated, no nested members)
  const projects = [];
  let cursor = null;
  do {
    const page = await linearQuery(Q_TEAM_PROJECTS, { teamId, after: cursor });
    const conn = page.team?.projects;
    if (!conn) break;
    projects.push(...(conn.nodes || []));
    cursor = conn.pageInfo?.hasNextPage ? conn.pageInfo.endCursor : null;
  } while (cursor);

  // 3) Per-project members, in batches
  const projectMembers = {}; // projectId -> [userId]
  for (let i = 0; i < projects.length; i += PROJECT_MEMBERS_BATCH) {
    const batch = projects.slice(i, i + PROJECT_MEMBERS_BATCH);
    const data = await linearQuery(buildProjectMembersQuery(batch.map(p => p.id)));
    Object.values(data).forEach(p => {
      if (!p || !p.id) return;
      projectMembers[p.id] = (p.members?.nodes || []).map(m => m.id);
    });
  }

  // 4) Resolve unknown user IDs (project members/leads not in team.members)
  const unknownIds = new Set();
  projects.forEach(p => {
    if (p.lead && !knownUsers.has(p.lead.id)) unknownIds.add(p.lead.id);
    (projectMembers[p.id] || []).forEach(uid => {
      if (!knownUsers.has(uid)) unknownIds.add(uid);
    });
  });
  if (unknownIds.size > 0) {
    const ids = [...unknownIds];
    for (let i = 0; i < ids.length; i += USERS_LOOKUP_BATCH) {
      const batch = ids.slice(i, i + USERS_LOOKUP_BATCH);
      const data = await linearQuery(buildUsersLookupQuery(batch));
      Object.values(data).forEach(u => {
        if (u && u.id) knownUsers.set(u.id, u);
      });
    }
  }

  // 5) Build the unified payload shape expected downstream.
  return {
    id: meta.team.id,
    name: meta.team.name,
    members: { nodes: teamMembers },
    projects: {
      nodes: projects.map(p => ({
        ...p,
        lead: p.lead ? (knownUsers.get(p.lead.id) || { id: p.lead.id }) : null,
        members: {
          nodes: (projectMembers[p.id] || [])
            .map(uid => knownUsers.get(uid) || { id: uid }),
        },
      })),
    },
  };
}

function mapProjectState(s) {
  if (!s) return "backlog";
  const v = String(s).toLowerCase();
  if (v === "started") return "in_progress";
  if (v === "completed" || v === "canceled" || v === "cancelled") return "completed";
  return "backlog";
}

function mapProjectHealth(h) {
  if (!h) return "on_track";
  const v = String(h).toLowerCase();
  if (v === "ontrack" || v === "on_track") return "on_track";
  if (v === "atrisk"  || v === "at_risk")  return "blocked";
  if (v === "offtrack"|| v === "off_track") return "overdue";
  return "on_track";
}

// A "continuous" project is an ongoing maintenance/operations stream rather
// than a finite deliverable — these clutter the squad view and are excluded.
// Matched by a bracketed tag in the name: [Maintenance], [Continuous],
// [Ongoing]. Case-insensitive.
const CONTINUOUS_NAME_RE = /\[(maintenance|continuous|ongoing)\]/i;
function isContinuousProject(p) {
  return CONTINUOUS_NAME_RE.test(p?.name || "");
}

// Project icon may be an emoji string or null
function projectIcon(p) {
  if (p.icon && /\p{Emoji}/u.test(p.icon)) return p.icon;
  // Fall back to first character of the name (uppercased)
  return (p.name || "?").trim().charAt(0).toUpperCase();
}

// Build a fresh app state from a Linear team payload.
function deriveFreshState(teamPayload) {
  if (!teamPayload) return { people: [], squads: [], projects: [], meta: {} };

  // ---- People: team members + anyone appearing in projects (lead/member) ----
  const peopleByLinearId = new Map();
  const addPerson = (u) => {
    if (!u || !u.id) return;
    if (peopleByLinearId.has(u.id)) return;
    const name = u.displayName || u.name || "Unknown";
    peopleByLinearId.set(u.id, {
      id: `p-${u.id}`,
      linearId: u.id,
      name,
      initials: initialsFor(name),
    });
  };
  const rawProjects = (teamPayload.projects?.nodes || []).filter(pr => !isContinuousProject(pr));

  (teamPayload.members?.nodes || []).forEach(addPerson);
  rawProjects.forEach(pr => {
    if (pr.lead) addPerson(pr.lead);
    (pr.members?.nodes || []).forEach(addPerson);
  });

  // ---- Projects ----
  const projects = rawProjects.map(pr => {
    const leadIds = pr.lead ? [`p-${pr.lead.id}`] : [];
    const memberIds = (pr.members?.nodes || []).map(m => `p-${m.id}`);
    // Sensible date fallbacks: if either missing, use today / +14d.
    const today = fmtISO(TODAY);
    const start = pr.startDate || today;
    const end   = pr.targetDate || addDays(start, 14);
    return {
      id: `proj-${pr.id}`,
      linearId: pr.id,
      name: pr.name || "(untitled)",
      icon: projectIcon(pr),
      start, end,
      progress: typeof pr.progress === "number" ? pr.progress : 0,
      status: mapProjectState(pr.state),
      health: mapProjectHealth(pr.health),
      leads: leadIds,
      squadId: null, // set below via auto-derivation
      notes: "",
      // squad-derivation key: lead + member ids, sorted
      _lineupKey: [...new Set([...leadIds, ...memberIds])].sort().join("|"),
      _memberIds: memberIds,
    };
  });

  // ---- Squads: group projects by lineup key. Empty lineups stay unassigned. ----
  // Squad name = the project that triggered the squad's creation. Users can
  // rename via the matrix.
  const squadByKey = new Map();
  let squadIdx = 0;
  projects.forEach(pr => {
    if (!pr._lineupKey) return; // no people => unassigned
    let sq = squadByKey.get(pr._lineupKey);
    if (!sq) {
      squadIdx++;
      const members = [...new Set([...pr.leads, ...pr._memberIds])];
      sq = {
        id: `sq-${pr._lineupKey.slice(0, 12)}-${squadIdx}`,
        name: pr.name,
        members,
        phases: [],
        _lineupKey: pr._lineupKey,
      };
      squadByKey.set(pr._lineupKey, sq);
    }
    pr.squadId = sq.id;
  });

  // Strip internal fields from projects
  const cleanProjects = projects.map(({ _lineupKey, _memberIds, ...rest }) => rest);
  const squads = [...squadByKey.values()].map(({ _lineupKey, ...rest }) => rest);

  return {
    people: [...peopleByLinearId.values()],
    squads,
    projects: cleanProjects,
    meta: {
      teamId: teamPayload.id,
      teamName: teamPayload.name,
      lastFetched: new Date().toISOString(),
    },
  };
}

// Merge new Linear data into existing local state.
// Preserves: notes, squadId on projects; squads (members + phases + names); people the user added.
// Replaces: project core fields (name, dates, status, health, progress, leads).
function mergeLinearData(prev, teamPayload) {
  if (!prev || !prev.projects) return deriveFreshState(teamPayload);

  // 1. People — add new, update names of existing.
  const peopleByLinearId = new Map(prev.people.filter(p => p.linearId).map(p => [p.linearId, p]));
  const localOnlyPeople  = prev.people.filter(p => !p.linearId);
  const seenLinearIds = new Set();
  const upsertPerson = (u) => {
    if (!u || !u.id) return;
    seenLinearIds.add(u.id);
    const existing = peopleByLinearId.get(u.id);
    const name = u.displayName || u.name || "Unknown";
    if (existing) {
      // Preserve any local rename if user changed it from the Linear-given name
      if (existing.name === existing._lastSyncedName || !existing._lastSyncedName) {
        existing.name = name;
        existing.initials = initialsFor(name);
      }
      existing._lastSyncedName = name;
    } else {
      peopleByLinearId.set(u.id, {
        id: `p-${u.id}`,
        linearId: u.id,
        name,
        initials: initialsFor(name),
        _lastSyncedName: name,
      });
    }
  };
  const rawProjects = (teamPayload.projects?.nodes || []).filter(pr => !isContinuousProject(pr));

  (teamPayload.members?.nodes || []).forEach(upsertPerson);
  rawProjects.forEach(pr => {
    if (pr.lead) upsertPerson(pr.lead);
    (pr.members?.nodes || []).forEach(upsertPerson);
  });

  const mergedPeople = [...localOnlyPeople, ...peopleByLinearId.values()];

  // 2. Projects — update existing matched by linearId; add new; keep removed (user can delete).
  const prevProjectsByLinearId = new Map(prev.projects.filter(p => p.linearId).map(p => [p.linearId, p]));
  const localOnlyProjects = prev.projects.filter(p => !p.linearId);
  const today = fmtISO(TODAY);
  const incomingProjects = rawProjects.map(pr => {
    const leadIds = pr.lead ? [`p-${pr.lead.id}`] : [];
    const memberIds = (pr.members?.nodes || []).map(m => `p-${m.id}`);
    const start = pr.startDate || today;
    const end   = pr.targetDate || addDays(start, 14);
    const fromLinear = {
      linearId: pr.id,
      name: pr.name || "(untitled)",
      icon: projectIcon(pr),
      start, end,
      progress: typeof pr.progress === "number" ? pr.progress : 0,
      status: mapProjectState(pr.state),
      health: mapProjectHealth(pr.health),
      leads: leadIds,
    };
    const existing = prevProjectsByLinearId.get(pr.id);
    if (existing) {
      // Preserve squadId + notes from local
      return { ...existing, ...fromLinear };
    }
    return {
      id: `proj-${pr.id}`,
      ...fromLinear,
      squadId: null,
      notes: "",
      // remember lineup for new-project squad matching below
      _lineupKey: [...new Set([...leadIds, ...memberIds])].sort().join("|"),
      _memberIds: memberIds,
    };
  });

  // 3. Squads — keep all existing; for NEW projects (with lineup), try to attach to an existing
  // squad with the exact same member set; otherwise create a new squad.
  const squadByMemberSet = new Map(
    prev.squads.map(s => [[...new Set(s.members)].sort().join("|"), s])
  );
  const squads = [...prev.squads];
  let newSquadIdx = squads.length;

  incomingProjects.forEach(pr => {
    if (pr.squadId !== undefined && pr.squadId !== null) return; // already attached (existing)
    if (!pr._lineupKey) return;
    let sq = squadByMemberSet.get(pr._lineupKey);
    if (!sq) {
      newSquadIdx++;
      sq = {
        id: `sq-${pr._lineupKey.slice(0, 12)}-${newSquadIdx}`,
        name: pr.name,
        members: [...new Set([...pr.leads, ...pr._memberIds])],
        phases: [],
      };
      squads.push(sq);
      squadByMemberSet.set(pr._lineupKey, sq);
    }
    pr.squadId = sq.id;
  });

  const cleanProjects = incomingProjects.map(({ _lineupKey, _memberIds, ...rest }) => rest);
  const mergedProjects = [...localOnlyProjects, ...cleanProjects];

  return {
    people: mergedPeople,
    squads,
    projects: mergedProjects,
    meta: {
      teamId: teamPayload.id,
      teamName: teamPayload.name,
      lastFetched: new Date().toISOString(),
    },
  };
}

// ============================================================
// SHARED PRIMITIVES
// ============================================================

function Avatar({ person, size = 22, dim = false }) {
  if (!person) return null;
  const palette = ["#5AB966", "#3D7CB8", "#F03C3C", "#FF8F00", "#B69BFF", "#83BAFD", "#F0D56B", "#FF8E8E"];
  const id = person.id || "";
  const idx = id.length ? (id.charCodeAt(0) + id.charCodeAt(id.length - 1)) % palette.length : 0;
  return (
    <span style={{
      display: "inline-flex", alignItems: "center", justifyContent: "center",
      width: size, height: size, borderRadius: "50%",
      background: palette[idx], color: "rgba(0,0,0,0.85)",
      fontSize: size * 0.42, fontWeight: 700, letterSpacing: 0,
      fontFamily: "var(--font-sans)",
      opacity: dim ? 0.4 : 1,
      flexShrink: 0,
    }} title={person.name}>{person.initials}</span>
  );
}

function StatusDot({ status, health }) {
  let bg = "var(--fg-3)";
  if (status === "in_progress" && health === "on_track") bg = "var(--primary)";
  if (status === "in_progress" && health === "overdue")  bg = "#ff8e8e";
  if (status === "in_progress" && health === "blocked")  bg = "#f0d56b";
  if (status === "in_progress" && health === "no_lead")  bg = "#ff8e8e";
  if (status === "backlog"     && health === "urgent")   bg = "#ff8e8e";
  if (status === "backlog")                              bg = "#f0d56b";
  if (status === "completed")                            bg = "var(--neutral-38)";
  return <span style={{
    display: "inline-block", width: 8, height: 8, borderRadius: "50%",
    background: bg, flexShrink: 0,
  }} />;
}

// ============================================================
// TIMELINE (squads × time)
// ============================================================

function Timeline({ state, conflicts, onSelectProject, onProjectUpdate, onSelectSquad }) {
  const trackRef = useRef(null);
  const rowsRef = useRef({});
  const scrollerRef = useRef(null);
  const didAutoScroll = useRef(false);
  const [drag, setDrag] = useState(null);

  const dates = state.projects
    .filter(p => p.status !== "completed" && p.start && p.end)
    .flatMap(p => [parseDate(p.start), parseDate(p.end)]);
  let minD, maxD;
  if (dates.length === 0) {
    minD = new Date(TODAY); minD.setDate(minD.getDate() - 14);
    maxD = new Date(TODAY); maxD.setDate(maxD.getDate() + 56);
  } else {
    minD = new Date(Math.min(...dates));
    maxD = new Date(Math.max(...dates));
  }
  minD.setDate(minD.getDate() - 3);
  maxD.setDate(maxD.getDate() + 5);
  const total = Math.max(1, daysBetween(minD, maxD));

  // Fixed pixel-per-day so the timeline can scroll horizontally when the
  // range is long, instead of cramping everything into the viewport.
  const PX_PER_DAY = 18;
  const trackWidth = Math.max(total * PX_PER_DAY, 480);
  const LABEL_COL = 180;

  const xPct = (d) => (daysBetween(minD, d) / total) * 100;
  const pxToDays = (px) => {
    const anyRow = Object.values(rowsRef.current).find(Boolean);
    const w = anyRow?.offsetWidth || trackRef.current?.offsetWidth || 1;
    return Math.round((px / w) * total);
  };

  useEffect(() => {
    if (!drag) return;
    const onMove = (e) => {
      const dx = e.clientX - drag.startX;
      const dy = e.clientY - drag.startY;
      let hoverSquadId = drag.hoverSquadId;
      if (drag.mode === "move") {
        for (const [sid, el] of Object.entries(rowsRef.current)) {
          if (!el) continue;
          const r = el.getBoundingClientRect();
          if (e.clientY >= r.top && e.clientY <= r.bottom) {
            hoverSquadId = sid === "__unassigned" ? null : sid;
            break;
          }
        }
      }
      const moved = drag.moved || Math.abs(dx) > 4 || Math.abs(dy) > 4;
      setDrag(d => ({ ...d, currentDx: dx, currentDy: dy, hoverSquadId, moved }));
    };
    const onUp = () => {
      const p = state.projects.find(x => x.id === drag.projectId);
      if (!p) { setDrag(null); return; }
      if (!drag.moved) {
        onSelectProject(drag.projectId);
        setDrag(null);
        return;
      }
      const dxDays = pxToDays(drag.currentDx);
      let next = { ...p };
      if (drag.mode === "move") {
        next.start = addDays(drag.originalStart, dxDays);
        next.end   = addDays(drag.originalEnd,   dxDays);
        next.squadId = drag.hoverSquadId === undefined ? p.squadId : drag.hoverSquadId;
      } else if (drag.mode === "resize-l") {
        const newStart = addDays(drag.originalStart, dxDays);
        if (parseDate(newStart) < parseDate(drag.originalEnd)) next.start = newStart;
      } else if (drag.mode === "resize-r") {
        const newEnd = addDays(drag.originalEnd, dxDays);
        if (parseDate(newEnd) > parseDate(drag.originalStart)) next.end = newEnd;
      }
      onProjectUpdate(next);
      setDrag(null);
    };
    window.addEventListener("pointermove", onMove);
    window.addEventListener("pointerup", onUp);
    return () => {
      window.removeEventListener("pointermove", onMove);
      window.removeEventListener("pointerup", onUp);
    };
  }, [drag, state.projects]);

  // Auto-scroll the timeline so "today" is centered on first render. Only
  // fires once per mount; switching months / refreshing doesn't yank scroll.
  useEffect(() => {
    if (didAutoScroll.current) return;
    if (!scrollerRef.current) return;
    if (state.projects.length === 0) return;
    const xPxOfToday = (daysBetween(minD, TODAY) / total) * trackWidth;
    const scroller = scrollerRef.current;
    const visible = scroller.clientWidth - LABEL_COL;
    scroller.scrollLeft = Math.max(0, xPxOfToday - visible / 2);
    didAutoScroll.current = true;
  }, [state.projects, minD, total, trackWidth]);

  const startDrag = (e, p, mode) => {
    e.preventDefault();
    e.stopPropagation();
    setDrag({
      projectId: p.id, mode,
      startX: e.clientX, startY: e.clientY,
      currentDx: 0, currentDy: 0, moved: false,
      hoverSquadId: p.squadId,
      originalStart: p.start, originalEnd: p.end,
      originalSquad: p.squadId,
    });
  };

  const ticks = [];
  let cursor = new Date(minD);
  while (cursor <= maxD) {
    if (cursor.getDay() === 1) ticks.push(new Date(cursor));
    cursor.setDate(cursor.getDate() + 1);
  }

  const rowProjects = (rowId) => state.projects
    .filter(p => p.status !== "completed" && p.start && p.end)
    .filter(p => (rowId === "__unassigned" ? !p.squadId : p.squadId === rowId))
    .sort((a, b) => parseDate(a.start) - parseDate(b.start));

  // Only render rows that have at least one project — squad without project = hidden.
  const rows = [
    ...state.squads.map(s => ({ id: s.id, name: s.name, squad: s })),
    { id: "__unassigned", name: "Unassigned", squad: null },
  ].filter(row => rowProjects(row.id).length > 0);

  function assignLanes(projs) {
    const lanes = [];
    return projs.map(p => {
      const start = parseDate(p.start);
      const end   = parseDate(p.end);
      let lane = lanes.findIndex(le => le <= start);
      if (lane === -1) { lanes.push(end); lane = lanes.length - 1; }
      else { lanes[lane] = end; }
      return { ...p, _lane: lane };
    });
  }

  const ROW_GUTTER = 32;
  const BAR_H = 26;
  const LANE_GAP = 4;

  return (
    <div style={{
      background: "var(--paper-1)",
      border: "1px solid var(--border-soft, rgba(168,168,177,0.23))",
      borderRadius: 12,
      padding: "16px 0 20px",
    }}>
      <div style={{
        padding: "0 20px 12px",
        display: "flex", alignItems: "baseline", justifyContent: "space-between",
      }}>
        <h2 style={{
          fontFamily: "var(--font-display)", fontWeight: 600,
          fontSize: 17, letterSpacing: -0.2, margin: 0, color: "var(--fg-1)"
        }}>Timeline · squads × projects</h2>
        <span style={{ fontSize: 11, color: "var(--fg-3)", letterSpacing: 0.3, textTransform: "uppercase" }}>
          {fmtShort(minD)} → {fmtShort(maxD)}
        </span>
      </div>

      <div ref={scrollerRef} style={{ overflowX: "auto", overflowY: "hidden" }}>
      <div ref={trackRef} style={{
        display: "grid",
        gridTemplateColumns: `${LABEL_COL}px ${trackWidth}px`,
        gap: 0,
        minWidth: LABEL_COL + trackWidth,
      }}>
        <div style={{
          position: "sticky", left: 0, zIndex: 3,
          background: "var(--paper-1)",
        }} />
        <div style={{ position: "relative", height: 24, borderBottom: "1px solid rgba(255,255,255,0.06)" }}>
          {ticks.map((t, i) => (
            <div key={i} style={{
              position: "absolute", left: `${xPct(t)}%`,
              top: 0, height: "100%",
              fontSize: 10, color: "var(--fg-3)", letterSpacing: 0.4,
              transform: "translateX(-50%)",
              fontFamily: "var(--font-mono)",
            }}>{fmtShort(t)}</div>
          ))}
          <div style={{
            position: "absolute", left: `${xPct(TODAY)}%`, top: 0, bottom: 0,
            borderLeft: "1px dashed var(--primary)",
          }}>
            <div style={{
              position: "absolute", top: -2, left: 0, transform: "translateX(-50%)",
              fontSize: 9, color: "var(--primary)", fontWeight: 600, letterSpacing: 0.5,
              background: "var(--paper-1)", padding: "0 4px", whiteSpace: "nowrap",
            }}>TODAY</div>
          </div>
        </div>

        {rows.map((row, ri) => {
          const projs = assignLanes(rowProjects(row.id));
          const laneCount = Math.max(1, ...projs.map(p => p._lane + 1));
          const rowH = laneCount * BAR_H + (laneCount - 1) * LANE_GAP + ROW_GUTTER;
          const isConflict = row.squad && conflicts.squadConflicts[row.id];
          const color = row.squad ? squadColor(state, row.id) : { stroke: "#555", fill: "rgba(85,85,85,0.14)" };
          const currentState = row.squad ? squadStateAt(state, row.squad, TODAY) : { kind: "none" };
          const isDisbanded = currentState.kind === "disbanded";

          return (
            <React.Fragment key={row.id}>
              <div
                onClick={row.squad ? () => onSelectSquad(row.id) : undefined}
                style={{
                  padding: "12px 16px 12px 20px",
                  borderTop: ri === 0 ? "none" : "1px solid rgba(255,255,255,0.04)",
                  display: "flex", flexDirection: "column", gap: 4,
                  position: "sticky", left: 0, zIndex: 2,
                  background: "var(--paper-1)",
                  cursor: row.squad ? "pointer" : "default",
                  opacity: isDisbanded ? 0.55 : 1,
                }}>
                {isConflict && (
                  <span style={{
                    position: "absolute", left: 8, top: 16,
                    width: 4, height: rowH - 30,
                    background: "#ff8e8e", borderRadius: 2,
                  }} />
                )}
                <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
                  <span style={{
                    width: 10, height: 10, borderRadius: 3,
                    background: color.stroke, flexShrink: 0,
                  }} />
                  <span style={{
                    fontFamily: "var(--font-sans)", fontWeight: 600,
                    fontSize: 13, color: isConflict ? "#ff8e8e" : "var(--fg-1)",
                    textDecoration: isDisbanded ? "line-through" : "none",
                    textDecorationColor: "rgba(255,142,142,0.7)",
                  }}>{row.name}</span>
                </div>
                {row.squad && (
                  <div style={{ display: "flex", gap: 4, alignItems: "center", paddingLeft: 18 }}>
                    <span style={{
                      fontSize: 9, fontWeight: 700, letterSpacing: 0.5,
                      color: STATE_LABEL[currentState.kind].color,
                      textTransform: "uppercase",
                      background: `${STATE_LABEL[currentState.kind].color}1F`,
                      border: `1px solid ${STATE_LABEL[currentState.kind].color}55`,
                      padding: "1px 6px", borderRadius: 99,
                      flexShrink: 0,
                    }}>{STATE_LABEL[currentState.kind].label}</span>
                    <div style={{ display: "flex", gap: 3, marginLeft: 2 }}>
                      {row.squad.members.map(mid => (
                        <Avatar key={mid} person={personById(state, mid)} size={18} dim={isDisbanded} />
                      ))}
                    </div>
                  </div>
                )}
                {!row.squad && (
                  <div style={{ paddingLeft: 18, fontSize: 11, color: "var(--fg-3)" }}>
                    no squad
                  </div>
                )}
              </div>

              <div ref={el => { rowsRef.current[row.id] = el; }} style={{
                position: "relative",
                borderTop: ri === 0 ? "none" : "1px solid rgba(255,255,255,0.04)",
                height: rowH,
                background: drag && drag.mode === "move" && drag.hoverSquadId === (row.id === "__unassigned" ? null : row.id)
                  ? "rgba(170,251,178,0.05)" : "transparent",
                outline: drag && drag.mode === "move" && drag.hoverSquadId === (row.id === "__unassigned" ? null : row.id)
                  ? "1px dashed var(--primary)" : "none",
                outlineOffset: -1,
                transition: "background 0.12s",
              }}>
                {ticks.map((t, i) => (
                  <div key={i} style={{
                    position: "absolute", left: `${xPct(t)}%`, top: 0, bottom: 0,
                    borderLeft: "1px solid rgba(255,255,255,0.04)",
                  }} />
                ))}
                <div style={{
                  position: "absolute", left: `${xPct(TODAY)}%`, top: 0, bottom: 0,
                  borderLeft: "1px dashed var(--primary)", opacity: 0.6,
                }} />
                {isConflict && (
                  <div style={{
                    position: "absolute", inset: "8px 4px",
                    background: "rgba(255,142,142,0.05)",
                    borderRadius: 6,
                  }} />
                )}
                {row.squad?.phases?.map((ph) => {
                  const left  = xPct(parseDate(ph.start));
                  const right = ph.end ? xPct(parseDate(ph.end)) : 100;
                  const style = PHASE_STYLE[ph.kind] || PHASE_STYLE.waiting;
                  const w = Math.max(right - left, 0.4);
                  return (
                    <div key={ph.id} title={`${style.label}: ${fmtShort(parseDate(ph.start))}${ph.end ? " → " + fmtShort(parseDate(ph.end)) : " → open-ended"}`}
                      style={{
                        position: "absolute",
                        left: `${left}%`, width: `${w}%`,
                        top: 8, bottom: 8,
                        background: style.fill,
                        backgroundImage: style.pattern,
                        border: `1px dashed ${style.stroke}44`,
                        borderRadius: 4,
                        pointerEvents: "none",
                      }}>
                      <div style={{
                        position: "absolute", top: 4, left: 6,
                        fontSize: 9, fontWeight: 700, letterSpacing: 0.6,
                        color: style.stroke, textTransform: "uppercase",
                        fontFamily: "var(--font-sans)",
                        whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis",
                        maxWidth: "calc(100% - 12px)",
                      }}>{style.label}</div>
                    </div>
                  );
                })}
                {projs.map(p => {
                  const isDragging = drag && drag.projectId === p.id;
                  const dxDays = isDragging ? pxToDays(drag.currentDx) : 0;
                  let dispStart = p.start, dispEnd = p.end;
                  if (isDragging) {
                    if (drag.mode === "move")     { dispStart = addDays(drag.originalStart, dxDays); dispEnd = addDays(drag.originalEnd, dxDays); }
                    if (drag.mode === "resize-l") { const ns = addDays(drag.originalStart, dxDays); if (parseDate(ns) < parseDate(drag.originalEnd)) dispStart = ns; }
                    if (drag.mode === "resize-r") { const ne = addDays(drag.originalEnd, dxDays);   if (parseDate(ne) > parseDate(drag.originalStart)) dispEnd = ne; }
                  }
                  const hideFromRow = isDragging && drag.mode === "move" &&
                                      drag.hoverSquadId !== (row.id === "__unassigned" ? null : row.id);
                  if (hideFromRow) return null;

                  const left  = xPct(parseDate(dispStart));
                  const right = xPct(parseDate(dispEnd));
                  const top = 14 + p._lane * (BAR_H + LANE_GAP);
                  const isBacklog = p.status === "backlog";
                  const pc = projectColor(state, p);
                  return (
                    <div key={p.id}
                      onClick={() => onSelectProject(p.id)}
                      style={{
                        position: "absolute",
                        left: `${left}%`, width: `${Math.max(right - left, 1.2)}%`,
                        top, height: BAR_H,
                        background: pc.fill,
                        border: `1px solid ${pc.stroke}`,
                        borderStyle: isBacklog ? "dashed" : "solid",
                        borderRadius: 6,
                        color: "var(--fg-1)",
                        cursor: "pointer",
                        display: "flex", alignItems: "center",
                        padding: "0 8px", gap: 6,
                        fontFamily: "var(--font-sans)", fontSize: 11.5, fontWeight: 500,
                        textAlign: "left", overflow: "hidden",
                        whiteSpace: "nowrap", textOverflow: "ellipsis",
                        userSelect: "none",
                        zIndex: 1,
                      }}
                      title={`${p.name} · ${fmtShort(parseDate(dispStart))} → ${fmtShort(parseDate(dispEnd))} · click to view`}
                    >
                      <StatusDot status={p.status} health={p.health} />
                      <span style={{ overflow: "hidden", textOverflow: "ellipsis", pointerEvents: "none" }}>{p.name}</span>
                      {p.progress > 0 && p.progress < 1 && (
                        <span style={{ fontSize: 10, color: "var(--fg-3)",
                          fontFamily: "var(--font-mono)", marginLeft: "auto",
                          pointerEvents: "none" }}>{Math.round(p.progress * 100)}%</span>
                      )}
                    </div>
                  );
                })}
                {drag && drag.mode === "move" &&
                 drag.hoverSquadId === (row.id === "__unassigned" ? null : row.id) &&
                 !rowProjects(row.id).find(x => x.id === drag.projectId) && (() => {
                  const p = state.projects.find(x => x.id === drag.projectId);
                  if (!p) return null;
                  const dxDays = pxToDays(drag.currentDx);
                  const dispStart = addDays(drag.originalStart, dxDays);
                  const dispEnd   = addDays(drag.originalEnd,   dxDays);
                  const left = xPct(parseDate(dispStart));
                  const right = xPct(parseDate(dispEnd));
                  return (
                    <div style={{
                      position: "absolute",
                      left: `${left}%`, width: `${Math.max(right - left, 1.2)}%`,
                      top: 14, height: BAR_H,
                      background: color.fill,
                      border: `1px dashed ${color.stroke}`,
                      borderRadius: 6, color: "var(--fg-1)",
                      display: "flex", alignItems: "center",
                      padding: "0 8px", gap: 6,
                      fontFamily: "var(--font-sans)", fontSize: 11.5, fontWeight: 500,
                      overflow: "hidden", whiteSpace: "nowrap", textOverflow: "ellipsis",
                      boxShadow: "0 6px 18px rgba(0,0,0,0.4)", opacity: 0.85,
                      pointerEvents: "none", zIndex: 9,
                    }}>
                      <StatusDot status={p.status} health={p.health} />
                      <span style={{ overflow: "hidden", textOverflow: "ellipsis" }}>{p.name}</span>
                    </div>
                  );
                })()}
                {isConflict && (
                  <div style={{
                    position: "absolute", right: 8, top: 8,
                    fontSize: 9, fontWeight: 700, letterSpacing: 0.6,
                    color: "#ff8e8e", textTransform: "uppercase",
                    background: "rgba(255,142,142,0.12)",
                    border: "1px solid rgba(255,142,142,0.4)",
                    padding: "2px 6px", borderRadius: 4,
                  }}>{projs.length} active</div>
                )}
              </div>
            </React.Fragment>
          );
        })}
      </div>
      </div>
    </div>
  );
}

// ============================================================
// PERSON × SQUAD MATRIX
// ============================================================

function PersonMatrix({ state, conflicts, onToggleMember, onChange, onSelectSquad, visibleSquadIds }) {
  // Squads visible in this view (e.g. only ones with projects this month).
  // Edits still target the full state, so onChange handlers below stay correct.
  const visibleSquads = visibleSquadIds
    ? state.squads.filter(s => visibleSquadIds.has(s.id))
    : state.squads;
  const renamePerson = (pid, name) => {
    onChange({
      ...state,
      people: state.people.map(p => p.id === pid ? {
        ...p, name,
        initials: name.trim().split(/\s+/).map(w => w[0]).join("").slice(0, 2).toUpperCase() || p.initials,
      } : p),
    });
  };
  const renameSquad = (sid, name) => {
    onChange({ ...state, squads: state.squads.map(s => s.id === sid ? { ...s, name } : s) });
  };
  const removePerson = (pid) => {
    const p = personById(state, pid);
    if (!confirm(`Remove ${p?.name || "person"}? They'll be unassigned from all squads & projects.`)) return;
    onChange({
      ...state,
      people: state.people.filter(x => x.id !== pid),
      squads: state.squads.map(s => ({ ...s, members: s.members.filter(m => m !== pid) })),
      projects: state.projects.map(pr => ({ ...pr, leads: (pr.leads || []).filter(l => l !== pid) })),
    });
  };
  const addPerson = () => {
    const id = `p-${Date.now().toString(36)}`;
    onChange({
      ...state,
      people: [...state.people, { id, name: "New person", initials: "NP" }],
    });
  };

  const nameInputStyle = {
    background: "transparent", border: "1px solid transparent",
    color: "inherit", fontSize: 13, fontWeight: 500,
    padding: "3px 6px", borderRadius: 4, outline: "none",
    fontFamily: "var(--font-sans)", width: "100%",
    minWidth: 0,
  };
  const nameInputFocus = {
    border: "1px solid rgba(255,255,255,0.15)",
    background: "var(--paper-3)",
  };

  return (
    <div style={{
      background: "var(--paper-1)",
      border: "1px solid var(--border-soft, rgba(168,168,177,0.23))",
      borderRadius: 12, padding: 20,
    }}>
      <div style={{
        display: "flex", alignItems: "baseline", justifyContent: "space-between",
        marginBottom: 14,
      }}>
        <h2 style={{
          fontFamily: "var(--font-display)", fontWeight: 600,
          fontSize: 17, letterSpacing: -0.2, margin: 0, color: "var(--fg-1)"
        }}>People × Squads</h2>
        <div style={{ display: "flex", gap: 8, alignItems: "center" }}>
          <span style={{ fontSize: 11, color: "var(--fg-3)", letterSpacing: 0.3, textTransform: "uppercase" }}>
            click cells to reassign · names are editable
          </span>
          <button onClick={addPerson} style={{
            background: "transparent", border: "1px solid rgba(255,255,255,0.1)",
            color: "var(--fg-2)", padding: "4px 10px", borderRadius: 6,
            fontFamily: "var(--font-sans)", fontSize: 10.5, cursor: "pointer",
            letterSpacing: 0.3, textTransform: "uppercase", fontWeight: 600,
          }}>+ Person</button>
        </div>
      </div>

      <div style={{ overflow: "auto" }}>
        <table style={{ borderCollapse: "separate", borderSpacing: 0, width: "100%", fontFamily: "var(--font-sans)" }}>
          <thead>
            <tr>
              <th style={{ textAlign: "left", padding: "6px 10px", fontSize: 11, fontWeight: 500,
                color: "var(--fg-3)", letterSpacing: 0.3, textTransform: "uppercase",
                position: "sticky", left: 0, background: "var(--paper-1)", zIndex: 1 }}>Person</th>
              {visibleSquads.map((s, i) => {
                const c = SQUAD_PALETTE[i % SQUAD_PALETTE.length];
                return (
                  <th key={s.id} style={{
                    padding: "4px 4px", whiteSpace: "nowrap",
                    borderBottom: `2px solid ${c.stroke}`,
                  }}>
                    <input
                      value={s.name.replace(/^Squad\s+/i, "")}
                      onChange={(e) => renameSquad(s.id, "Squad " + e.target.value)}
                      onDoubleClick={() => onSelectSquad?.(s.id)}
                      onFocus={(e) => Object.assign(e.target.style, nameInputFocus)}
                      onBlur={(e) => Object.assign(e.target.style, { border: "1px solid transparent", background: "transparent" })}
                      style={{
                        ...nameInputStyle,
                        color: c.stroke, fontWeight: 600,
                        textAlign: "center", letterSpacing: 0.2,
                        minWidth: 60,
                      }}
                      title="Edit name · double-click to open squad editor"
                    />
                  </th>
                );
              })}
              <th style={{ padding: "6px 8px", fontSize: 11, fontWeight: 500, color: "var(--fg-3)",
                letterSpacing: 0.3, textTransform: "uppercase", textAlign: "center" }}>#</th>
              <th style={{ padding: "6px 8px", width: 28 }}></th>
            </tr>
          </thead>
          <tbody>
            {state.people.map((p, ri) => {
              const inSquads = visibleSquads.filter(s => s.members.includes(p.id));
              const conflict = inSquads.length > 1;
              const orphan = inSquads.length === 0;
              const stripedBg = ri % 2 ? "rgba(255,255,255,0.015)" : "transparent";
              const stripedStickyBg = ri % 2 ? "rgb(20,20,22)" : "var(--paper-1)";
              return (
                <tr key={p.id} style={{ background: stripedBg }}>
                  <td style={{
                    padding: "6px 10px",
                    position: "sticky", left: 0,
                    background: stripedStickyBg,
                    minWidth: 180,
                  }}>
                    <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
                      <Avatar person={p} size={24} />
                      <input
                        value={p.name}
                        onChange={(e) => renamePerson(p.id, e.target.value)}
                        onFocus={(e) => Object.assign(e.target.style, nameInputFocus)}
                        onBlur={(e) => Object.assign(e.target.style, { border: "1px solid transparent", background: "transparent" })}
                        style={{
                          ...nameInputStyle,
                          color: conflict ? "#ff8e8e" : "var(--fg-1)",
                          fontWeight: 500,
                        }}
                      />
                    </div>
                  </td>
                  {visibleSquads.map((s, i) => {
                    const c = SQUAD_PALETTE[i % SQUAD_PALETTE.length];
                    const on = s.members.includes(p.id);
                    return (
                      <td key={s.id} style={{ padding: 0, textAlign: "center" }}>
                        <button onClick={() => onToggleMember(s.id, p.id)} style={{
                          width: 36, height: 36, background: "transparent", border: "none",
                          cursor: "pointer", color: "var(--fg-1)",
                          display: "inline-flex", alignItems: "center", justifyContent: "center",
                        }}>
                          {on ? (
                            <span style={{
                              width: 18, height: 18, borderRadius: 4,
                              background: c.stroke, color: "rgba(0,0,0,0.85)",
                              fontSize: 13, fontWeight: 700,
                              display: "inline-flex", alignItems: "center", justifyContent: "center",
                            }}>✓</span>
                          ) : (
                            <span style={{
                              width: 18, height: 18, borderRadius: 4,
                              border: "1px solid rgba(255,255,255,0.1)",
                            }} />
                          )}
                        </button>
                      </td>
                    );
                  })}
                  <td style={{
                    padding: "8px 8px", textAlign: "center",
                    fontFamily: "var(--font-mono)", fontSize: 12,
                    color: conflict ? "#ff8e8e" : orphan ? "var(--fg-3)" : "var(--fg-2)",
                    fontWeight: conflict ? 700 : 400,
                  }}>
                    {conflict && <span style={{
                      background: "rgba(255,142,142,0.15)", border: "1px solid rgba(255,142,142,0.4)",
                      padding: "2px 6px", borderRadius: 4, fontSize: 10,
                    }}>{inSquads.length}×</span>}
                    {!conflict && !orphan && inSquads.length}
                    {orphan && <span style={{ fontSize: 10, fontStyle: "italic" }}>—</span>}
                  </td>
                  <td style={{ padding: "0 4px", textAlign: "center" }}>
                    <button onClick={() => removePerson(p.id)} title="Remove person" style={{
                      width: 22, height: 22, background: "transparent",
                      border: "1px solid transparent", color: "var(--fg-3)",
                      borderRadius: 4, cursor: "pointer", fontSize: 14, lineHeight: 1,
                      opacity: 0.4, transition: "opacity 0.12s, color 0.12s",
                    }}
                    onMouseEnter={(e) => { e.target.style.opacity = 1; e.target.style.color = "#ff8e8e"; }}
                    onMouseLeave={(e) => { e.target.style.opacity = 0.4; e.target.style.color = "var(--fg-3)"; }}
                    >×</button>
                  </td>
                </tr>
              );
            })}
          </tbody>
        </table>
      </div>
    </div>
  );
}

// ============================================================
// CONFLICTS BANNER
// ============================================================

function ConflictsBanner({ state, conflicts }) {
  const items = [];
  Object.entries(conflicts.squadConflicts).forEach(([sid, projs]) => {
    const s = squadById(state, sid);
    if (!s) return;
    items.push({
      severity: "high",
      label: `${s.name}`,
      detail: `${projs.length} active projects: ${projs.map(p => p.name).join(" + ")}`,
    });
  });
  Object.entries(conflicts.personConflicts).forEach(([pid, sids]) => {
    const p = personById(state, pid);
    if (!p) return;
    items.push({
      severity: "med",
      label: `${p.name}`,
      detail: `In ${sids.length} squads: ${sids.map(id => squadById(state, id)?.name).filter(Boolean).join(", ")}`,
    });
  });
  conflicts.noSquad.forEach(p => {
    items.push({ severity: "med", label: p.name, detail: "No squad assigned" });
  });
  conflicts.noLead.forEach(p => {
    items.push({ severity: "low", label: p.name, detail: "No lead" });
  });

  const summary = [
    { n: Object.keys(conflicts.squadConflicts).length, label: "Squad conflicts", color: "#ff8e8e" },
    { n: Object.keys(conflicts.personConflicts).length, label: "People in >1 squad", color: "#ff8e8e" },
    { n: conflicts.noSquad.length, label: "No squad", color: "#f0d56b" },
    { n: conflicts.noLead.length, label: "No lead", color: "#f0d56b" },
  ];

  return (
    <div style={{
      display: "grid", gridTemplateColumns: "auto 1fr", gap: 20,
      padding: "14px 20px",
      background: "var(--paper-1)",
      border: "1px solid var(--border-soft, rgba(168,168,177,0.23))",
      borderRadius: 12,
      alignItems: "center",
    }}>
      <div style={{ display: "flex", gap: 18 }}>
        {summary.map((s, i) => (
          <div key={i} style={{ display: "flex", flexDirection: "column", alignItems: "flex-start" }}>
            <div style={{
              fontFamily: "var(--font-display)", fontSize: 32, fontWeight: 600,
              color: s.n > 0 ? s.color : "var(--fg-3)", lineHeight: 1,
              letterSpacing: -1,
            }}>{s.n}</div>
            <div style={{
              fontSize: 10, color: "var(--fg-3)", letterSpacing: 0.5,
              textTransform: "uppercase", marginTop: 4,
            }}>{s.label}</div>
          </div>
        ))}
      </div>
      <div style={{
        display: "flex", flexWrap: "wrap", gap: 6,
        borderLeft: "1px solid rgba(255,255,255,0.06)",
        paddingLeft: 20, maxHeight: 100, overflow: "auto",
      }}>
        {items.length === 0 && (
          <div style={{ color: "var(--primary)", fontSize: 13, fontFamily: "var(--font-sans)" }}>
            ✓ All squads have ≤1 active project and all people are in exactly one squad
          </div>
        )}
        {items.map((it, i) => {
          const color = it.severity === "high" ? "#ff8e8e" : it.severity === "med" ? "#f0d56b" : "var(--fg-3)";
          return (
            <div key={i} style={{
              fontSize: 11.5, color: "var(--fg-2)",
              background: "rgba(255,255,255,0.03)",
              border: `1px solid ${color}33`,
              padding: "4px 10px", borderRadius: 6,
              display: "flex", gap: 8, alignItems: "center",
              fontFamily: "var(--font-sans)",
            }}>
              <span style={{ width: 6, height: 6, borderRadius: "50%", background: color, flexShrink: 0 }} />
              <span style={{ fontWeight: 600, color: "var(--fg-1)" }}>{it.label}</span>
              <span style={{ color: "var(--fg-3)" }}>·</span>
              <span>{it.detail}</span>
            </div>
          );
        })}
      </div>
    </div>
  );
}

// ============================================================
// PROJECT DRAWER
// ============================================================

function ProjectDrawer({ state, projectId, onClose, onChange }) {
  if (!projectId) return null;
  const p = state.projects.find(x => x.id === projectId);
  if (!p) return null;

  // Notes are local-only annotations; everything else is read-only mirrored from Linear.
  const setNotes = (notes) => {
    onChange({
      ...state,
      projects: state.projects.map(x => x.id === p.id ? { ...x, notes } : x),
    });
  };
  const onHide = () => {
    if (!confirm(`Hide "${p.name}" from this view?\n(Local only — the project stays in Linear; "Refresh from Linear" will bring it back.)`)) return;
    onChange({ ...state, projects: state.projects.filter(x => x.id !== p.id) });
    onClose();
  };

  const Label = ({ children }) => (
    <div style={{ fontSize: 10, color: "var(--fg-3)", letterSpacing: 0.6,
      textTransform: "uppercase", marginBottom: 6, fontWeight: 600 }}>{children}</div>
  );
  const readonlyBox = {
    width: "100%", background: "var(--paper-3)",
    border: "1px solid rgba(255,255,255,0.04)", color: "var(--fg-1)",
    padding: "8px 10px", borderRadius: 6, fontFamily: "var(--font-sans)",
    fontSize: 13,
  };

  const statusLabel = STATUS[p.status]?.label || p.status;
  const statusColor = STATUS[p.status]?.color || "var(--fg-3)";
  const healthLabel = { on_track: "On track", overdue: "Overdue", blocked: "Blocked", no_lead: "No lead", urgent: "Urgent" }[p.health] || p.health;
  const squad = squadById(state, p.squadId);
  const squadIdx = squad ? state.squads.findIndex(s => s.id === squad.id) : -1;
  const squadC = squadIdx >= 0 ? SQUAD_PALETTE[squadIdx % SQUAD_PALETTE.length] : null;

  return (
    <div style={{
      position: "fixed", top: 0, right: 0, bottom: 0, width: 380,
      background: "var(--paper-2)",
      borderLeft: "1px solid rgba(255,255,255,0.06)",
      padding: "22px 24px 80px", overflow: "auto",
      zIndex: 100,
      boxShadow: "-40px 0 80px rgba(0,0,0,0.5)",
      fontFamily: "var(--font-sans)",
    }}>
      <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 14 }}>
        <span style={{ fontSize: 10, color: "var(--fg-3)", letterSpacing: 0.6, textTransform: "uppercase", fontWeight: 600 }}>
          Project · read-only{p.linearId && <span style={{ color: "var(--primary)", marginLeft: 6 }}>from Linear</span>}
        </span>
        <button onClick={onClose} style={{
          background: "transparent", border: "none", color: "var(--fg-3)",
          fontSize: 22, cursor: "pointer", padding: 0, lineHeight: 1,
        }}>×</button>
      </div>

      <div style={{ display: "flex", gap: 10, alignItems: "center", marginBottom: 16 }}>
        <div style={{
          width: 44, height: 44, display: "flex", alignItems: "center", justifyContent: "center",
          background: "var(--paper-3)", borderRadius: 6,
          fontSize: 22,
        }}>{p.icon}</div>
        <div style={{
          fontFamily: "var(--font-display)", fontWeight: 600, fontSize: 18,
          color: "var(--fg-1)", lineHeight: 1.2,
        }}>{p.name}</div>
      </div>

      <div style={{
        fontSize: 11, color: "var(--fg-3)", marginBottom: 16,
        padding: "8px 10px", border: "1px dashed rgba(255,255,255,0.08)",
        borderRadius: 6, lineHeight: 1.5,
      }}>
        Project data mirrors Linear and is <strong>read-only</strong>. To change name, dates, status, leads, or members — edit the project in Linear and click "Refresh from Linear". Only <strong>notes</strong> below are local-editable.
      </div>

      <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 10, marginBottom: 14 }}>
        <div>
          <Label>Status</Label>
          <div style={{
            ...readonlyBox,
            display: "inline-flex", alignItems: "center", gap: 6,
            color: statusColor, fontWeight: 600,
          }}>
            <StatusDot status={p.status} health={p.health} />
            {statusLabel}
          </div>
        </div>
        <div>
          <Label>Health</Label>
          <div style={readonlyBox}>{healthLabel}</div>
        </div>
      </div>

      <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 10, marginBottom: 14 }}>
        <div>
          <Label>Start</Label>
          <div style={readonlyBox}>{p.start ? fmtShort(parseDate(p.start)) + ", " + parseDate(p.start).getFullYear() : "—"}</div>
        </div>
        <div>
          <Label>End</Label>
          <div style={readonlyBox}>{p.end ? fmtShort(parseDate(p.end)) + ", " + parseDate(p.end).getFullYear() : "—"}</div>
        </div>
      </div>

      <div style={{ marginBottom: 14 }}>
        <Label>Progress · {Math.round((p.progress || 0) * 100)}%</Label>
        <div style={{
          width: "100%", height: 8, background: "var(--paper-3)",
          borderRadius: 99, overflow: "hidden",
        }}>
          <div style={{
            width: `${Math.round((p.progress || 0) * 100)}%`, height: "100%",
            background: "var(--primary)",
          }} />
        </div>
      </div>

      <div style={{ marginBottom: 14 }}>
        <Label>Squad</Label>
        {squad ? (
          <div style={{
            ...readonlyBox,
            display: "inline-flex", alignItems: "center", gap: 8,
            color: squadC.stroke, fontWeight: 600,
            background: squadC.fill,
            border: `1px solid ${squadC.stroke}55`,
          }}>
            <span style={{ width: 8, height: 8, borderRadius: 2, background: squadC.stroke }} />
            {squad.name}
            <span style={{ marginLeft: 6, display: "flex", gap: 3 }}>
              {squad.members.map(mid => <Avatar key={mid} person={personById(state, mid)} size={16} />)}
            </span>
          </div>
        ) : (
          <div style={{ ...readonlyBox, color: "#ff8e8e" }}>No squad</div>
        )}
      </div>

      <div style={{ marginBottom: 14 }}>
        <Label>Priority · local only</Label>
        <div style={{ display: "flex", flexWrap: "wrap", gap: 4 }}>
          {QUADRANTS.map(q => {
            const on = p.priority === q.id;
            return (
              <button key={q.id} onClick={() => onChange({
                ...state,
                projects: state.projects.map(x => x.id === p.id ? { ...x, priority: q.id } : x),
              })} style={{
                background: on ? q.color + "26" : "transparent",
                border: `1px solid ${on ? q.color : "rgba(255,255,255,0.08)"}`,
                color: on ? q.color : "var(--fg-2)",
                fontWeight: on ? 700 : 500,
                padding: "4px 10px", borderRadius: 99,
                fontSize: 11, cursor: "pointer", letterSpacing: 0.3,
                fontFamily: "var(--font-sans)",
              }}>{q.label}</button>
            );
          })}
          <button onClick={() => onChange({
            ...state,
            projects: state.projects.map(x => x.id === p.id ? { ...x, priority: null } : x),
          })} style={{
            background: !p.priority ? "rgba(255,255,255,0.04)" : "transparent",
            border: `1px solid rgba(255,255,255,0.08)`,
            color: "var(--fg-3)",
            padding: "4px 10px", borderRadius: 99,
            fontSize: 11, cursor: "pointer", letterSpacing: 0.3,
            fontFamily: "var(--font-sans)",
          }}>Clear</button>
        </div>
      </div>

      <div style={{ marginBottom: 14 }}>
        <Label>Leads</Label>
        <div style={{ display: "flex", gap: 6, flexWrap: "wrap" }}>
          {(p.leads || []).length === 0 && (
            <span style={{ fontSize: 12, color: "#ff8e8e", fontStyle: "italic" }}>no lead</span>
          )}
          {(p.leads || []).map(lid => {
            const person = personById(state, lid);
            if (!person) return null;
            return (
              <span key={lid} style={{
                display: "flex", alignItems: "center", gap: 6,
                background: "var(--paper-3)",
                border: "1px solid rgba(255,255,255,0.06)",
                borderRadius: 99, padding: "3px 10px 3px 3px",
                fontSize: 11.5, color: "var(--fg-1)",
              }}>
                <Avatar person={person} size={18} />
                {person.name}
              </span>
            );
          })}
        </div>
      </div>

      <div style={{ marginBottom: 18 }}>
        <Label>Notes · local only</Label>
        <textarea value={p.notes || ""} onChange={(e) => setNotes(e.target.value)}
          rows={3} placeholder="Local annotations about this project…"
          style={{
            width: "100%", background: "var(--paper-3)",
            border: "1px solid rgba(255,255,255,0.08)", color: "var(--fg-1)",
            padding: "8px 10px", borderRadius: 6, fontFamily: "var(--font-sans)",
            fontSize: 13, outline: "none", resize: "vertical", lineHeight: 1.5,
          }} />
      </div>

      <button onClick={onHide} style={{
        background: "transparent", border: "1px solid rgba(255,142,142,0.3)",
        color: "#ff8e8e", padding: "8px 12px", borderRadius: 6, cursor: "pointer",
        fontSize: 12, letterSpacing: 0.3, textTransform: "uppercase",
        fontFamily: "var(--font-sans)",
      }}>Hide from view</button>
    </div>
  );
}

// ============================================================
// SQUAD DRAWER
// ============================================================

function SquadDrawer({ state, squadId, onClose, onChange }) {
  if (!squadId) return null;
  const sq = state.squads.find(s => s.id === squadId);
  if (!sq) return null;

  const patch = (changes) => {
    onChange({
      ...state,
      squads: state.squads.map(s => s.id === sq.id ? { ...s, ...changes } : s),
    });
  };
  const toggleMember = (pid) => {
    patch({
      members: sq.members.includes(pid)
        ? sq.members.filter(m => m !== pid)
        : [...sq.members, pid],
    });
  };
  const updatePhase = (phId, changes) => {
    patch({ phases: sq.phases.map(ph => ph.id === phId ? { ...ph, ...changes } : ph) });
  };
  const addPhase = (kind = "waiting") => {
    const lastEnd = sq.phases.length > 0
      ? (sq.phases[sq.phases.length - 1].end || sq.phases[sq.phases.length - 1].start)
      : fmtISO(TODAY);
    const newPh = {
      id: `ph-${Date.now().toString(36)}`,
      kind,
      start: lastEnd || fmtISO(TODAY),
      end: kind === "disbanded" ? null : addDays(lastEnd || fmtISO(TODAY), 7),
    };
    patch({ phases: [...(sq.phases || []), newPh] });
  };
  const removePhase = (phId) => {
    patch({ phases: sq.phases.filter(ph => ph.id !== phId) });
  };
  const onDeleteSquad = () => {
    if (!confirm(`Delete ${sq.name}? Its projects will become unassigned.`)) return;
    onChange({
      ...state,
      squads: state.squads.filter(s => s.id !== sq.id),
      projects: state.projects.map(p => p.squadId === sq.id ? { ...p, squadId: null } : p),
    });
    onClose();
  };

  const Label = ({ children }) => (
    <div style={{ fontSize: 10, color: "var(--fg-3)", letterSpacing: 0.6,
      textTransform: "uppercase", marginBottom: 6, fontWeight: 600 }}>{children}</div>
  );
  const inputStyle = {
    background: "var(--paper-3)",
    border: "1px solid rgba(255,255,255,0.08)", color: "var(--fg-1)",
    padding: "6px 8px", borderRadius: 5, fontFamily: "var(--font-sans)",
    fontSize: 12, outline: "none",
  };

  const currentState = squadStateAt(state, sq, TODAY);

  return (
    <div style={{
      position: "fixed", top: 0, right: 0, bottom: 0, width: 400,
      background: "var(--paper-2)",
      borderLeft: "1px solid rgba(255,255,255,0.06)",
      padding: "22px 24px 80px", overflow: "auto",
      zIndex: 100,
      boxShadow: "-40px 0 80px rgba(0,0,0,0.5)",
      fontFamily: "var(--font-sans)",
    }}>
      <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 14 }}>
        <span style={{ fontSize: 10, color: "var(--fg-3)", letterSpacing: 0.6, textTransform: "uppercase", fontWeight: 600 }}>
          Edit squad · local only
        </span>
        <button onClick={onClose} style={{
          background: "transparent", border: "none", color: "var(--fg-3)",
          fontSize: 22, cursor: "pointer", padding: 0, lineHeight: 1,
        }}>×</button>
      </div>

      <input value={sq.name} onChange={(e) => patch({ name: e.target.value })}
        style={{ ...inputStyle, width: "100%", fontSize: 18, fontWeight: 600,
          fontFamily: "var(--font-display)", padding: "8px 10px", marginBottom: 6 }} />
      <div style={{ marginBottom: 18 }}>
        <span style={{
          fontSize: 10, fontWeight: 700, letterSpacing: 0.6,
          color: STATE_LABEL[currentState.kind].color, textTransform: "uppercase",
          background: `${STATE_LABEL[currentState.kind].color}1F`,
          border: `1px solid ${STATE_LABEL[currentState.kind].color}55`,
          padding: "2px 8px", borderRadius: 99,
        }}>Current state · {STATE_LABEL[currentState.kind].label}</span>
        {currentState.project && (
          <span style={{ fontSize: 11, color: "var(--fg-3)", marginLeft: 8 }}>
            on {currentState.project.name}
          </span>
        )}
      </div>

      <div style={{ marginBottom: 22 }}>
        <Label>Members · click to toggle</Label>
        <div style={{ display: "flex", gap: 6, flexWrap: "wrap" }}>
          {state.people.map(person => {
            const on = sq.members.includes(person.id);
            return (
              <button key={person.id} onClick={() => toggleMember(person.id)} style={{
                display: "flex", alignItems: "center", gap: 6,
                background: on ? "rgba(170,251,178,0.12)" : "var(--paper-3)",
                border: `1px solid ${on ? "var(--primary)" : "rgba(255,255,255,0.06)"}`,
                borderRadius: 99, padding: "3px 10px 3px 3px",
                fontSize: 11.5, color: on ? "var(--fg-1)" : "var(--fg-3)",
                cursor: "pointer", fontFamily: "var(--font-sans)",
              }}>
                <Avatar person={person} size={18} dim={!on} />
                {person.name}
              </button>
            );
          })}
        </div>
      </div>

      <div style={{ marginBottom: 18 }}>
        <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 8 }}>
          <Label>Lifecycle phases</Label>
          <div style={{ display: "flex", gap: 4 }}>
            {Object.entries(PHASE_STYLE).map(([k, s]) => (
              <button key={k} onClick={() => addPhase(k)} title={`Add ${s.label} phase`} style={{
                background: "transparent", border: `1px solid ${s.stroke}55`,
                color: s.stroke, fontSize: 10, fontWeight: 600,
                letterSpacing: 0.4, textTransform: "uppercase",
                padding: "3px 8px", borderRadius: 99, cursor: "pointer",
                fontFamily: "var(--font-sans)",
              }}>+ {s.label}</button>
            ))}
          </div>
        </div>
        {(sq.phases || []).length === 0 && (
          <div style={{
            fontSize: 12, color: "var(--fg-3)", fontStyle: "italic",
            padding: "8px 0",
          }}>
            No lifecycle phases. While a project is active the squad is "Active"; otherwise "Idle". Add a phase to mark Forming / Maintenance / Waiting / Disbanded.
          </div>
        )}
        <div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
          {(sq.phases || []).map(ph => {
            const s = PHASE_STYLE[ph.kind] || PHASE_STYLE.waiting;
            return (
              <div key={ph.id} style={{
                background: s.fill, backgroundImage: s.pattern,
                border: `1px solid ${s.stroke}55`,
                borderRadius: 6, padding: 10,
                display: "flex", flexDirection: "column", gap: 8,
              }}>
                <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
                  <select value={ph.kind} onChange={(e) => updatePhase(ph.id, { kind: e.target.value })}
                    style={{ ...inputStyle, color: s.stroke, fontWeight: 600 }}>
                    {Object.entries(PHASE_STYLE).map(([k, st]) => (
                      <option key={k} value={k}>{st.label}</option>
                    ))}
                  </select>
                  <button onClick={() => removePhase(ph.id)} style={{
                    marginLeft: "auto", background: "transparent",
                    border: "1px solid rgba(255,142,142,0.3)",
                    color: "#ff8e8e", padding: "3px 8px", borderRadius: 4,
                    fontSize: 10, letterSpacing: 0.3, textTransform: "uppercase",
                    cursor: "pointer", fontFamily: "var(--font-sans)",
                  }}>Remove</button>
                </div>
                <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 8 }}>
                  <div>
                    <div style={{ fontSize: 9, color: "var(--fg-3)", marginBottom: 2, letterSpacing: 0.4, textTransform: "uppercase" }}>Start</div>
                    <input type="date" value={ph.start} onChange={(e) => updatePhase(ph.id, { start: e.target.value })}
                      style={{ ...inputStyle, width: "100%" }} />
                  </div>
                  <div>
                    <div style={{ fontSize: 9, color: "var(--fg-3)", marginBottom: 2, letterSpacing: 0.4, textTransform: "uppercase" }}>
                      End {ph.kind === "disbanded" ? "(optional — empty = open-ended)" : ""}
                    </div>
                    <input type="date" value={ph.end || ""} onChange={(e) => updatePhase(ph.id, { end: e.target.value || null })}
                      style={{ ...inputStyle, width: "100%" }} />
                  </div>
                </div>
              </div>
            );
          })}
        </div>
      </div>

      <button onClick={onDeleteSquad} style={{
        background: "transparent", border: "1px solid rgba(255,142,142,0.3)",
        color: "#ff8e8e", padding: "8px 12px", borderRadius: 6, cursor: "pointer",
        fontSize: 12, letterSpacing: 0.3, textTransform: "uppercase",
        fontFamily: "var(--font-sans)",
      }}>Delete squad</button>
    </div>
  );
}


// ============================================================
// SETUP SCREEN — pick a team. The Worker handles auth; we just need to know
// which team to load.
// ============================================================

function SetupScreen({ initialTeamId, onPicked }) {
  const [teams, setTeams] = useState([]);
  const [selectedTeamId, setSelectedTeamId] = useState(initialTeamId || "");
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    (async () => {
      try {
        const data = await linearQuery(Q_VIEWER_AND_TEAMS);
        const list = data.teams?.nodes || [];
        setTeams(list);
        if (!selectedTeamId) {
          const core = list.find(t => t.name.toLowerCase() === "team core")
                    || list.find(t => t.name.toLowerCase().includes("core"))
                    || list[0];
          setSelectedTeamId(core?.id || "");
        }
      } catch (err) {
        setError(err.message || "Could not load teams from Linear.");
      } finally {
        setLoading(false);
      }
    })();
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  const confirmPick = () => {
    const team = teams.find(t => t.id === selectedTeamId);
    if (!team) { setError("Pick a team."); return; }
    saveJSON(LS_KEY_TEAM, { id: team.id, name: team.name });
    onPicked(team);
  };

  const card = {
    background: "var(--paper-1)",
    border: "1px solid var(--border-soft, rgba(168,168,177,0.23))",
    borderRadius: 12, padding: 32,
    maxWidth: 480, width: "100%",
    fontFamily: "var(--font-sans)",
  };
  const input = {
    width: "100%", background: "var(--paper-3)",
    border: "1px solid rgba(255,255,255,0.08)", color: "var(--fg-1)",
    padding: "10px 12px", borderRadius: 6,
    fontFamily: "var(--font-sans)", fontSize: 13, outline: "none",
  };
  const button = {
    background: "var(--primary)", border: "1px solid var(--primary)",
    color: "rgba(0,0,0,0.85)", padding: "10px 16px", borderRadius: 6,
    fontFamily: "var(--font-sans)", fontSize: 13, fontWeight: 600,
    cursor: "pointer", letterSpacing: 0.3,
  };

  return (
    <div style={{
      minHeight: "100vh", background: "var(--bg)",
      display: "flex", alignItems: "center", justifyContent: "center",
      padding: 24,
    }}>
      <div style={card}>
        <div style={{
          fontSize: 10, color: "var(--primary)", letterSpacing: 1.2,
          textTransform: "uppercase", fontWeight: 600, marginBottom: 8,
        }}>Squad Allocation</div>
        <h1 style={{
          fontFamily: "var(--font-display)", fontWeight: 600,
          fontSize: 26, letterSpacing: -0.5, margin: "0 0 18px",
          color: "var(--fg-1)",
        }}>Pick a team</h1>

        <p style={{ color: "var(--fg-2)", fontSize: 13, lineHeight: 1.6, margin: "0 0 14px" }}>
          {loading ? "Loading teams from Linear…" : "Choose the team to visualize."}
        </p>

        <select value={selectedTeamId} onChange={(e) => setSelectedTeamId(e.target.value)}
          disabled={loading}
          style={{ ...input, fontFamily: "var(--font-sans)" }}>
          <option value="">— pick a team —</option>
          {teams.map(t => (
            <option key={t.id} value={t.id}>{t.name} ({t.key})</option>
          ))}
        </select>

        {error && (
          <div style={{
            color: "#ff8e8e", fontSize: 12, marginTop: 8,
            background: "rgba(255,142,142,0.06)",
            border: "1px solid rgba(255,142,142,0.25)",
            padding: "8px 10px", borderRadius: 6,
          }}>{error}</div>
        )}

        <div style={{ marginTop: 18, display: "flex", justifyContent: "flex-end" }}>
          <button type="button" onClick={confirmPick} disabled={loading || !selectedTeamId} style={{
            ...button,
            opacity: (loading || !selectedTeamId) ? 0.6 : 1,
          }}>
            Load squad allocation →
          </button>
        </div>
      </div>
    </div>
  );
}

// ============================================================
// LOADING / ERROR SCREENS
// ============================================================

function CenterCard({ children }) {
  return (
    <div style={{
      minHeight: "100vh", background: "var(--bg)",
      display: "flex", alignItems: "center", justifyContent: "center",
      padding: 24,
    }}>
      <div style={{
        background: "var(--paper-1)",
        border: "1px solid var(--border-soft, rgba(168,168,177,0.23))",
        borderRadius: 12, padding: 32,
        maxWidth: 480, width: "100%",
        fontFamily: "var(--font-sans)",
        color: "var(--fg-1)",
      }}>{children}</div>
    </div>
  );
}

// ============================================================
// APP SHELL
// ============================================================

// ============================================================
// PROJECT LIST — compact table with remove buttons
// ============================================================

function ProjectList({ state, onSelectProject, onRemoveProject, periodLabel }) {
  const remove = (p) => {
    if (!confirm(`Remove "${p.name}" from this view?\n(Local only — the project stays in Linear; "Refresh from Linear" will bring it back.)`)) return;
    onRemoveProject(p.id);
  };

  const projects = [...state.projects].sort((a, b) => {
    const sa = a.start ? parseDate(a.start).getTime() : 0;
    const sb = b.start ? parseDate(b.start).getTime() : 0;
    return sa - sb;
  });

  return (
    <div style={{
      background: "var(--paper-1)",
      border: "1px solid var(--border-soft, rgba(168,168,177,0.23))",
      borderRadius: 12, padding: 20,
    }}>
      <div style={{
        display: "flex", alignItems: "baseline", justifyContent: "space-between",
        marginBottom: 14,
      }}>
        <h2 style={{
          fontFamily: "var(--font-display)", fontWeight: 600,
          fontSize: 17, letterSpacing: -0.2, margin: 0, color: "var(--fg-1)"
        }}>Projects {periodLabel ? <span style={{ fontSize: 12, color: "var(--fg-3)", fontWeight: 400, marginLeft: 8 }}>· {periodLabel}</span> : null}</h2>
        <span style={{ fontSize: 11, color: "var(--fg-3)", letterSpacing: 0.3, textTransform: "uppercase" }}>
          {projects.length} project{projects.length === 1 ? "" : "s"}
        </span>
      </div>

      {projects.length === 0 && (
        <div style={{
          color: "var(--fg-3)", fontSize: 13, fontStyle: "italic",
          padding: "12px 0",
        }}>No projects in this period.</div>
      )}

      <div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
        {projects.map(p => {
          const squad = squadById(state, p.squadId);
          const squadIdx = squad ? state.squads.findIndex(s => s.id === squad.id) : -1;
          const squadC = squadIdx >= 0 ? SQUAD_PALETTE[squadIdx % SQUAD_PALETTE.length] : null;
          const pc = projectColor(state, p);
          return (
            <div key={p.id}
              style={{
                display: "grid",
                gridTemplateColumns: "20px 1.5fr 1fr 0.8fr 100px 28px",
                gap: 12, alignItems: "center",
                padding: "8px 10px", borderRadius: 6,
                border: "1px solid rgba(255,255,255,0.04)",
                borderLeft: `4px solid ${pc.stroke}`,
                background: "var(--paper-2)",
              }}>
              <StatusDot status={p.status} health={p.health} />
              <div onClick={() => onSelectProject(p.id)}
                style={{
                  display: "flex", alignItems: "center", gap: 6,
                  minWidth: 0, cursor: "pointer",
                }}
                title="Click to edit">
                <span style={{ fontSize: 14, flexShrink: 0 }}>{p.icon}</span>
                <span style={{
                  fontSize: 13, fontWeight: 500, color: "var(--fg-1)",
                  overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap",
                }}>{p.name}</span>
              </div>
              <div>
                {squad ? (
                  <span style={{
                    display: "inline-flex", alignItems: "center", gap: 6,
                    fontSize: 11, color: squadC.stroke,
                    background: squadC.fill,
                    border: `1px solid ${squadC.stroke}55`,
                    padding: "2px 8px", borderRadius: 99,
                  }}>
                    <span style={{ width: 6, height: 6, borderRadius: 2, background: squadC.stroke }} />
                    {squad.name}
                  </span>
                ) : (
                  <span style={{
                    fontSize: 10, color: "#ff8e8e",
                    background: "rgba(255,142,142,0.08)",
                    border: "1px solid rgba(255,142,142,0.3)",
                    padding: "2px 8px", borderRadius: 99,
                    textTransform: "uppercase", letterSpacing: 0.4, fontWeight: 600,
                  }}>No squad</span>
                )}
              </div>
              <div style={{ display: "flex", gap: 3 }}>
                {(p.leads || []).slice(0, 4).map(lid => (
                  <Avatar key={lid} person={personById(state, lid)} size={18} />
                ))}
                {(!p.leads || p.leads.length === 0) && (
                  <span style={{ fontSize: 10, color: "#ff8e8e", fontStyle: "italic" }}>no lead</span>
                )}
              </div>
              <div style={{
                fontSize: 11, color: "var(--fg-3)", fontFamily: "var(--font-mono)",
                whiteSpace: "nowrap",
              }}>
                {p.start && p.end
                  ? `${fmtShort(parseDate(p.start))} → ${fmtShort(parseDate(p.end))}`
                  : "—"}
              </div>
              <button onClick={() => remove(p)} title="Remove (local only)" style={{
                width: 24, height: 24, background: "transparent",
                border: "1px solid rgba(255,255,255,0.06)", color: "var(--fg-3)",
                borderRadius: 4, cursor: "pointer", fontSize: 14, lineHeight: 1,
                transition: "color 0.12s, border-color 0.12s",
              }}
              onMouseEnter={(e) => { e.target.style.color = "#ff8e8e"; e.target.style.borderColor = "rgba(255,142,142,0.4)"; }}
              onMouseLeave={(e) => { e.target.style.color = "var(--fg-3)"; e.target.style.borderColor = "rgba(255,255,255,0.06)"; }}
              >×</button>
            </div>
          );
        })}
      </div>
    </div>
  );
}

// ============================================================
// ============================================================
// EISENHOWER MATRIX — drag projects between quadrants to set priority
// ============================================================

function EisenhowerMatrix({ state, onSetPriority, onSelectProject }) {
  const [dragId, setDragId] = useState(null);
  const [hoverQ, setHoverQ] = useState(null);

  const projects = state.projects.filter(p => p.status !== "completed");
  const byQ = { q1: [], q2: [], q3: [], q4: [], none: [] };
  projects.forEach(p => byQ[p.priority || "none"].push(p));

  const onDragStart = (e, pid) => {
    setDragId(pid);
    e.dataTransfer.effectAllowed = "move";
    try { e.dataTransfer.setData("text/plain", pid); } catch (_) {}
  };
  const onDragEnd = () => { setDragId(null); setHoverQ(null); };
  const allowDrop = (e, qid) => {
    e.preventDefault();
    e.dataTransfer.dropEffect = "move";
    if (hoverQ !== qid) setHoverQ(qid);
  };
  const dropAt = (e, qid) => {
    e.preventDefault();
    const id = dragId || (e.dataTransfer && e.dataTransfer.getData("text/plain"));
    if (id) onSetPriority(id, qid === "none" ? null : qid);
    setDragId(null); setHoverQ(null);
  };

  const Chip = ({ p }) => {
    const pc = projectColor(state, p);
    return (
      <div
        draggable
        onDragStart={(e) => onDragStart(e, p.id)}
        onDragEnd={onDragEnd}
        onClick={() => onSelectProject(p.id)}
        style={{
          display: "inline-flex", alignItems: "center", gap: 6,
          padding: "4px 10px 4px 6px",
          background: pc.fill,
          border: `1px solid ${pc.stroke}`,
          borderRadius: 99,
          fontSize: 11.5, color: "var(--fg-1)",
          fontFamily: "var(--font-sans)",
          cursor: dragId === p.id ? "grabbing" : "grab",
          userSelect: "none",
          opacity: dragId === p.id ? 0.4 : 1,
        }}
        title="Drag between quadrants · click to view">
        <span style={{ fontSize: 13 }}>{p.icon}</span>
        <span style={{
          overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", maxWidth: 200,
        }}>{p.name}</span>
      </div>
    );
  };

  const Quadrant = ({ q }) => {
    const items = byQ[q.id];
    const isHover = hoverQ === q.id;
    return (
      <div
        onDragOver={(e) => allowDrop(e, q.id)}
        onDragLeave={() => { if (hoverQ === q.id) setHoverQ(null); }}
        onDrop={(e) => dropAt(e, q.id)}
        style={{
          background: q.color + (isHover ? "1A" : "0D"),
          border: `1px ${isHover ? "solid" : "dashed"} ${q.color}${isHover ? "" : "66"}`,
          borderRadius: 8,
          padding: 12,
          minHeight: 140,
          display: "flex", flexDirection: "column", gap: 10,
          transition: "background 0.12s, border 0.12s",
        }}>
        <div>
          <div style={{
            fontSize: 12, fontWeight: 700, color: q.color,
            letterSpacing: 0.5, textTransform: "uppercase",
          }}>{q.label}</div>
          <div style={{
            fontSize: 10, color: "var(--fg-3)",
            letterSpacing: 0.4, marginTop: 2,
          }}>{q.sublabel}</div>
        </div>
        <div style={{ display: "flex", flexWrap: "wrap", gap: 6 }}>
          {items.map(p => <Chip key={p.id} p={p} />)}
          {items.length === 0 && (
            <div style={{ color: "var(--fg-3)", fontStyle: "italic", fontSize: 11 }}>
              Drop projects here
            </div>
          )}
        </div>
      </div>
    );
  };

  return (
    <div style={{
      background: "var(--paper-1)",
      border: "1px solid var(--border-soft, rgba(168,168,177,0.23))",
      borderRadius: 12, padding: 20,
    }}>
      <div style={{
        display: "flex", alignItems: "baseline", justifyContent: "space-between",
        marginBottom: 14,
      }}>
        <h2 style={{
          fontFamily: "var(--font-display)", fontWeight: 600,
          fontSize: 17, letterSpacing: -0.2, margin: 0, color: "var(--fg-1)"
        }}>Priorities · Eisenhower matrix</h2>
        <span style={{ fontSize: 11, color: "var(--fg-3)", letterSpacing: 0.3, textTransform: "uppercase" }}>
          drag chips between quadrants
        </span>
      </div>

      {/* Unassigned tray */}
      <div
        onDragOver={(e) => allowDrop(e, "none")}
        onDragLeave={() => { if (hoverQ === "none") setHoverQ(null); }}
        onDrop={(e) => dropAt(e, "none")}
        style={{
          background: hoverQ === "none" ? "rgba(255,255,255,0.04)" : "var(--paper-2)",
          border: `1px dashed rgba(255,255,255,${hoverQ === "none" ? "0.25" : "0.10"})`,
          borderRadius: 8, padding: 10,
          marginBottom: 12,
          transition: "background 0.12s, border 0.12s",
        }}>
        <div style={{
          fontSize: 10, color: "var(--fg-3)", letterSpacing: 0.4,
          textTransform: "uppercase", marginBottom: 6, fontWeight: 600,
        }}>
          Unprioritized — drop a chip here to clear priority
        </div>
        <div style={{ display: "flex", flexWrap: "wrap", gap: 6 }}>
          {byQ.none.map(p => <Chip key={p.id} p={p} />)}
          {byQ.none.length === 0 && (
            <div style={{ color: "var(--fg-3)", fontStyle: "italic", fontSize: 11 }}>
              Everything is prioritized.
            </div>
          )}
        </div>
      </div>

      {/* 2x2 grid */}
      <div style={{
        display: "grid",
        gridTemplateColumns: "1fr 1fr",
        gap: 10,
      }}>
        {QUADRANTS.map(q => <Quadrant key={q.id} q={q} />)}
      </div>

      <div style={{ fontSize: 11, color: "var(--fg-3)", fontStyle: "italic", marginTop: 10, lineHeight: 1.7 }}>
        Each quadrant has a color; once you assign a project, that color follows it on the timeline, in the project list, and in the drawer. Priority is local only.
      </div>
    </div>
  );
}

// ============================================================
// MONTH HELPERS
// ============================================================

const MONTH_NAMES = ["January","February","March","April","May","June",
  "July","August","September","October","November","December"];

function monthLabel(y, m) { return `${MONTH_NAMES[m]} ${y}`; }
function monthBounds(y, m) {
  return { start: new Date(y, m, 1), end: new Date(y, m + 1, 0) };
}
function shiftMonth({ year, month }, delta) {
  const d = new Date(year, month + delta, 1);
  return { year: d.getFullYear(), month: d.getMonth() };
}

function applyMonthFilter(state, monthFilter) {
  if (!state) return null;
  if (!monthFilter) return state;
  const { start, end } = monthBounds(monthFilter.year, monthFilter.month);
  const projects = state.projects.filter(p => {
    if (!p.start || !p.end) return false;
    const ps = parseDate(p.start);
    const pe = parseDate(p.end);
    return pe >= start && ps <= end;
  });
  // A squad only exists if it has a project in this period — squads and
  // projects are tied together (one squad per project, by design).
  const usedSquadIds = new Set(projects.map(p => p.squadId).filter(Boolean));
  const squads = state.squads.filter(s => usedSquadIds.has(s.id));
  return { ...state, projects, squads };
}

function App() {
  const [team, setTeam] = useState(() => loadJSON(LS_KEY_TEAM));
  const [state, setState] = useState(() => loadJSON(LS_KEY));
  const [phase, setPhase] = useState(() => {
    if (!loadJSON(LS_KEY_TEAM)) return "setup";
    if (!loadJSON(LS_KEY)) return "loading";
    return "ready";
  });
  const [error, setError] = useState(null);

  const [selectedProject, setSelectedProject] = useState(null);
  const [selectedSquad,   setSelectedSquad]   = useState(null);

  // Month filter (default: current month). Drives Timeline + ProjectList + conflicts.
  const [monthFilter, setMonthFilter] = useState(() => ({
    year: TODAY.getFullYear(), month: TODAY.getMonth(),
  }));
  const filteredState = useMemo(
    () => applyMonthFilter(state, monthFilter),
    [state, monthFilter]
  );

  // Persist on every state change.
  useEffect(() => {
    if (state && phase === "ready") saveJSON(LS_KEY, state);
  }, [state, phase]);

  // The Worker handles auth; we just need to know which team to query.
  const fetchFromLinear = useCallback(async (mode = "merge") => {
    if (!team?.id) return;
    setPhase(state ? "refreshing" : "loading");
    setError(null);
    try {
      const teamPayload = await fetchTeamPayload(team.id);
      const next = (mode === "fresh" || !state)
        ? deriveFreshState(teamPayload)
        : mergeLinearData(state, teamPayload);
      setState(next);
      setPhase("ready");
    } catch (err) {
      setError(err.message || "Linear fetch failed.");
      setPhase(state ? "ready" : "error");
    }
  }, [team, state]);

  // First visit after team picked (or after a local-reset) — initial fetch.
  useEffect(() => {
    if (phase === "loading" && team?.id && !state) {
      fetchFromLinear("fresh");
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [phase]);

  const onPicked = (t) => {
    setTeam({ id: t.id, name: t.name });
    setState(null);
    setPhase("loading");
  };

  const onChangeTeam = () => {
    if (!confirm("Pick a different team? This will clear local data.")) return;
    clearKey(LS_KEY); clearKey(LS_KEY_TEAM);
    setTeam(null); setState(null); setPhase("setup");
  };

  const onRefresh = () => fetchFromLinear("merge");

  const onResetFromLinear = () => {
    if (!confirm("Re-derive squads from Linear?\nAll local squad edits (members, phases, names) will be lost.")) return;
    fetchFromLinear("fresh");
  };

  const onResetLocal = () => {
    if (!confirm("Wipe local data and re-fetch from Linear?")) return;
    clearKey(LS_KEY);
    setState(null);
    setPhase("loading");
  };

  // Main view handlers
  const conflicts = useMemo(
    () => filteredState ? detectConflicts(filteredState) : null,
    [filteredState]
  );

  const onToggleMember = (squadId, personId) => {
    setState(s => ({
      ...s,
      squads: s.squads.map(sq => sq.id === squadId
        ? { ...sq, members: sq.members.includes(personId)
            ? sq.members.filter(m => m !== personId)
            : [...sq.members, personId] }
        : sq),
    }));
  };

  const onProjectUpdate = (next) => {
    setState(s => ({ ...s, projects: s.projects.map(x => x.id === next.id ? next : x) }));
  };

  const onAddProject = () => {
    const today = fmtISO(TODAY);
    const inTwoWeeks = addDays(today, 14);
    const id = `proj-local-${Date.now().toString(36)}`;
    const newP = {
      id, name: "New project", icon: "🆕",
      start: today, end: inTwoWeeks, progress: 0,
      status: "backlog", health: "on_track",
      leads: [], squadId: null, notes: "",
    };
    setState(s => ({ ...s, projects: [...s.projects, newP] }));
    setSelectedProject(id);
  };

  const onAddSquad = () => {
    const id = `sq-local-${Date.now().toString(36)}`;
    const used = new Set(state.squads.map(s => s.name));
    const next = NATO.find(n => !used.has(`Squad ${n}`)) || `New ${state.squads.length + 1}`;
    const newSq = { id, name: `Squad ${next}`, members: [], phases: [] };
    setState(s => ({ ...s, squads: [...s.squads, newSq] }));
    setSelectedSquad(id);
  };

  const onSetPriority = (projectId, quadrantId) => {
    setState(s => ({
      ...s,
      projects: s.projects.map(p => p.id === projectId ? { ...p, priority: quadrantId } : p),
    }));
  };

  const onExport = () => {
    const blob = new Blob([JSON.stringify(state, null, 2)], { type: "application/json" });
    const url = URL.createObjectURL(blob);
    const a = document.createElement("a");
    const d = new Date();
    a.href = url;
    a.download = `squads-${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,"0")}-${String(d.getDate()).padStart(2,"0")}.json`;
    document.body.appendChild(a); a.click(); document.body.removeChild(a);
    URL.revokeObjectURL(url);
  };

  const fileInputRef = useRef(null);
  const onImportClick = () => fileInputRef.current?.click();
  const onImportFile = (e) => {
    const file = e.target.files?.[0];
    if (!file) return;
    const reader = new FileReader();
    reader.onload = (ev) => {
      try {
        const data = JSON.parse(ev.target.result);
        if (!data.people || !data.squads || !data.projects) {
          alert("That file doesn't look like a squads export (missing people / squads / projects).");
          return;
        }
        if (!confirm("Replace current data with imported file? Use Export first to back up.")) return;
        setState(data);
      } catch (err) {
        alert("Could not parse JSON: " + err.message);
      }
    };
    reader.readAsText(file);
    e.target.value = "";
  };

  // ---- Render ----
  if (phase === "setup") {
    return <SetupScreen initialTeamId={team?.id} onPicked={onPicked} />;
  }
  if (phase === "loading") {
    return <CenterCard>
      <div style={{ fontSize: 11, color: "var(--fg-3)", letterSpacing: 0.6, textTransform: "uppercase", marginBottom: 8 }}>Loading</div>
      <h2 style={{ fontFamily: "var(--font-display)", margin: 0 }}>Fetching {team?.name || "team"} from Linear…</h2>
    </CenterCard>;
  }
  if (phase === "error") {
    return <CenterCard>
      <div style={{ fontSize: 11, color: "#ff8e8e", letterSpacing: 0.6, textTransform: "uppercase", marginBottom: 8 }}>Could not load</div>
      <h2 style={{ fontFamily: "var(--font-display)", margin: "0 0 12px", fontSize: 22 }}>Linear fetch failed</h2>
      <pre style={{
        fontSize: 12, color: "var(--fg-2)",
        background: "var(--paper-3)", padding: 12,
        borderRadius: 6, whiteSpace: "pre-wrap", margin: "0 0 18px",
      }}>{error}</pre>
      <div style={{ display: "flex", gap: 8 }}>
        <button onClick={() => fetchFromLinear("fresh")} style={{
          background: "var(--primary)", border: "1px solid var(--primary)",
          color: "rgba(0,0,0,0.85)", padding: "8px 14px", borderRadius: 6,
          fontFamily: "var(--font-sans)", fontSize: 12, fontWeight: 600,
          cursor: "pointer", letterSpacing: 0.3,
        }}>Retry</button>
        <button onClick={onResetLocal} style={{
          background: "transparent", border: "1px solid rgba(255,255,255,0.1)",
          color: "var(--fg-2)", padding: "8px 14px", borderRadius: 6,
          fontFamily: "var(--font-sans)", fontSize: 12, cursor: "pointer",
        }}>Reset local data</button>
      </div>
    </CenterCard>;
  }
  if (!state) return null;

  const lastFetched = state.meta?.lastFetched ? new Date(state.meta.lastFetched) : null;
  const refreshing = phase === "refreshing";

  return (
    <div style={{
      minHeight: "100vh", background: "var(--bg)",
      color: "var(--fg-1)", fontFamily: "var(--font-sans)",
    }}>
      <header style={{
        padding: "20px 32px 0",
        display: "flex", justifyContent: "space-between", alignItems: "flex-end",
        gap: 24,
      }}>
        <div>
          <div style={{
            fontSize: 11, color: "var(--primary)", letterSpacing: 1.2,
            textTransform: "uppercase", fontWeight: 700, marginBottom: 6,
            fontFamily: "var(--font-sans)",
            display: "flex", alignItems: "center", gap: 8,
          }}>
            <span style={{
              background: "rgba(170,251,178,0.10)",
              border: "1px solid rgba(170,251,178,0.35)",
              padding: "2px 8px", borderRadius: 99,
            }}>Team · {state.meta?.teamName || "Linear team"}</span>
            <span style={{ color: "var(--fg-3)", fontWeight: 500, letterSpacing: 0.4 }}>
              {lastFetched ? `Synced ${lastFetched.toLocaleString("en-US", { month: "short", day: "numeric", hour: "numeric", minute: "2-digit" })}` : "Local"}
            </span>
          </div>
          <h1 style={{
            fontFamily: "var(--font-display)", fontWeight: 600,
            fontSize: 30, letterSpacing: -0.7, margin: 0,
          }}>Squad Allocation</h1>
          <div style={{ fontSize: 13, color: "var(--fg-3)", marginTop: 6, maxWidth: 600 }}>
            One project per squad. One squad per person. Anything else is highlighted in red.
          </div>
          <div style={{
            display: "inline-flex", alignItems: "center", gap: 4,
            marginTop: 14, padding: "4px 4px 4px 6px",
            background: "var(--paper-2)",
            border: "1px solid rgba(255,255,255,0.06)",
            borderRadius: 99,
          }}>
            <button onClick={() => setMonthFilter(m => shiftMonth(m, -1))} title="Previous month" style={{
              width: 26, height: 26, background: "transparent", border: "none",
              color: "var(--fg-2)", cursor: "pointer", fontSize: 14,
              borderRadius: "50%",
            }} onMouseEnter={(e) => e.target.style.background = "rgba(255,255,255,0.06)"}
               onMouseLeave={(e) => e.target.style.background = "transparent"}
            >‹</button>
            <span style={{
              fontSize: 12, fontWeight: 600, color: "var(--fg-1)",
              letterSpacing: 0.2, padding: "0 8px", minWidth: 110, textAlign: "center",
              fontFamily: "var(--font-sans)",
            }}>{monthLabel(monthFilter.year, monthFilter.month)}</span>
            <button onClick={() => setMonthFilter(m => shiftMonth(m, 1))} title="Next month" style={{
              width: 26, height: 26, background: "transparent", border: "none",
              color: "var(--fg-2)", cursor: "pointer", fontSize: 14,
              borderRadius: "50%",
            }} onMouseEnter={(e) => e.target.style.background = "rgba(255,255,255,0.06)"}
               onMouseLeave={(e) => e.target.style.background = "transparent"}
            >›</button>
            <button onClick={() => setMonthFilter({ year: TODAY.getFullYear(), month: TODAY.getMonth() })}
              title="Jump to current month" style={{
                marginLeft: 2, background: "transparent",
                border: "1px solid rgba(255,255,255,0.08)",
                color: "var(--fg-3)", padding: "3px 10px", borderRadius: 99,
                fontFamily: "var(--font-sans)", fontSize: 10, cursor: "pointer",
                letterSpacing: 0.4, textTransform: "uppercase", fontWeight: 600,
              }}>Today</button>
          </div>
        </div>
        <div style={{ display: "flex", gap: 8, flexWrap: "wrap", justifyContent: "flex-end" }}>
          <button onClick={onRefresh} disabled={refreshing} title="Re-fetch from Linear, merge changes" style={{
            background: "transparent", border: "1px solid var(--primary)",
            color: "var(--primary)", padding: "6px 12px", borderRadius: 6,
            fontFamily: "var(--font-sans)", fontSize: 11, cursor: refreshing ? "default" : "pointer",
            letterSpacing: 0.3, textTransform: "uppercase", fontWeight: 600,
            opacity: refreshing ? 0.6 : 1,
          }}>{refreshing ? "Refreshing…" : "↻ Refresh from Linear"}</button>
          <button onClick={onResetFromLinear} disabled={refreshing} title="Re-derive everything (loses squad edits)" style={{
            background: "transparent", border: "1px solid rgba(255,255,255,0.1)",
            color: "var(--fg-2)", padding: "6px 12px", borderRadius: 6,
            fontFamily: "var(--font-sans)", fontSize: 11, cursor: "pointer",
            letterSpacing: 0.3, textTransform: "uppercase",
          }}>Re-derive</button>
          <button onClick={onExport} title="Download current state as JSON" style={{
            background: "transparent", border: "1px solid rgba(255,255,255,0.1)",
            color: "var(--fg-2)", padding: "6px 12px", borderRadius: 6,
            fontFamily: "var(--font-sans)", fontSize: 11, cursor: "pointer",
            letterSpacing: 0.3, textTransform: "uppercase",
          }}>⤓ Export</button>
          <button onClick={onImportClick} title="Load state from a JSON file" style={{
            background: "transparent", border: "1px solid rgba(255,255,255,0.1)",
            color: "var(--fg-2)", padding: "6px 12px", borderRadius: 6,
            fontFamily: "var(--font-sans)", fontSize: 11, cursor: "pointer",
            letterSpacing: 0.3, textTransform: "uppercase",
          }}>⤒ Import</button>
          <input ref={fileInputRef} type="file" accept="application/json,.json"
            onChange={onImportFile} style={{ display: "none" }} />
          <span style={{ width: 1, background: "rgba(255,255,255,0.08)", margin: "0 2px" }} />
          <button onClick={onAddSquad} style={{
            background: "transparent", border: "1px solid var(--primary)",
            color: "var(--primary)", padding: "6px 12px", borderRadius: 6,
            fontFamily: "var(--font-sans)", fontSize: 11, cursor: "pointer",
            letterSpacing: 0.3, textTransform: "uppercase", fontWeight: 600,
          }}>+ New squad</button>
          <button onClick={onChangeTeam} title="Pick a different team" style={{
            background: "transparent", border: "1px solid rgba(255,255,255,0.1)",
            color: "var(--fg-3)", padding: "6px 12px", borderRadius: 6,
            fontFamily: "var(--font-sans)", fontSize: 11, cursor: "pointer",
            letterSpacing: 0.3, textTransform: "uppercase",
          }}>Change team</button>
        </div>
      </header>

      {error && (
        <div style={{
          margin: "12px 32px 0", padding: "10px 14px",
          background: "rgba(255,142,142,0.06)",
          border: "1px solid rgba(255,142,142,0.25)",
          borderRadius: 6, color: "#ff8e8e", fontSize: 12,
        }}>{error}</div>
      )}

      <main style={{
        padding: "20px 32px 60px",
        display: "flex", flexDirection: "column", gap: 16,
        maxWidth: 1600, margin: "0 auto",
      }}>
        <ConflictsBanner state={filteredState} conflicts={conflicts} />
        <Timeline state={filteredState} conflicts={conflicts}
          onSelectProject={setSelectedProject} onProjectUpdate={onProjectUpdate}
          onSelectSquad={setSelectedSquad} />
        <PersonMatrix state={state} conflicts={conflicts}
          visibleSquadIds={new Set(filteredState.squads.map(s => s.id))}
          onToggleMember={onToggleMember} onChange={setState}
          onSelectSquad={setSelectedSquad} />
        <ProjectList state={filteredState}
          periodLabel={monthLabel(monthFilter.year, monthFilter.month)}
          onSelectProject={setSelectedProject}
          onRemoveProject={(pid) => setState(s => ({ ...s, projects: s.projects.filter(x => x.id !== pid) }))} />
        <EisenhowerMatrix state={filteredState}
          onSetPriority={onSetPriority}
          onSelectProject={setSelectedProject} />

        <div style={{
          fontSize: 11, color: "var(--fg-3)", marginTop: 4,
          fontStyle: "italic", lineHeight: 1.7,
        }}>
          <strong style={{ color: "var(--fg-2)" }}>Squads have lifecycles.</strong> Click a squad name to edit members and add lifecycle phases — Forming, Maintenance, Waiting, Disbanded. The current state shows next to each squad name.<br />
          <strong style={{ color: "var(--fg-2)" }}>Projects are read-only</strong> — they mirror Linear. Click a bar or row in the project list to view details, or use the <strong style={{ color: "var(--fg-2)" }}>×</strong> to hide a project from this local view (Refresh from Linear brings it back). <strong style={{ color: "var(--fg-2)" }}>Squads, members, and notes</strong> are local and stay editable.
        </div>
      </main>

      <ProjectDrawer state={state} projectId={selectedProject}
        onClose={() => setSelectedProject(null)} onChange={setState} />
      <SquadDrawer   state={state} squadId={selectedSquad}
        onClose={() => setSelectedSquad(null)} onChange={setState} />
    </div>
  );
}

ReactDOM.createRoot(document.getElementById("root")).render(<App />);
