// owner-dashboard.jsx — Owner / Admin overview.
//
// Three sections backed by GET /api/dashboards/owner:
//   1. Overview cards: total pending, overdue, due today, done in last 7d
//   2. Projects table: same numbers but per-project, sorted by pending desc
//   3. People table: per-user task load + last active timestamp + a
//      coloured "online / today / this week / cold" pill so the owner
//      can spot at a glance who's actually using the tool.
//
// Refresh: auto every 60s while the tab is visible. Manual reload
// button in the header. Last refresh time is shown so the owner
// trusts the numbers.

function OwnerDashboardView() {
  const [data, setData]     = React.useState(null);
  const [loading, setLoading] = React.useState(true);
  const [err, setErr]       = React.useState("");
  const [refreshing, setRefreshing] = React.useState(false);

  // ── Backup state ───────────────────────────────────────────────
  // CRITICAL: these MUST stay above the `if (loading && !data) return`
  // / `if (err && !data) return` early returns below. React's rules
  // require hooks to be called in the same order on every render — a
  // hook declared after a conditional return is called on render N+1
  // but not on render N, tripping "Rendered more hooks than during
  // the previous render" → white screen on the dashboard. Polled when
  // the rest of the dashboard refreshes so the "Last backup" pill
  // stays current without a separate poller.
  const [backup, setBackup] = React.useState(null);
  const [backupBusy, setBackupBusy] = React.useState(null); // 'download' | 'email' | null
  // ── Task drill-down modal ─────────────────────────────────────
  // Clicking a Pending / Overdue / Due-today number anywhere on the
  // dashboard opens this modal with the matching task list. The
  // hook MUST live above the early returns for the same rules-of-
  // hooks reason as `backup` above.
  // Shape: null | { kind: 'overdue'|'due_today'|'pending', scope: { projectId?, userId? }, title: string }
  const [taskList, setTaskList] = React.useState(null);
  function openTaskList(kind, scope, title) {
    setTaskList({ kind, scope: scope || {}, title });
  }

  // ── Section tabs ──────────────────────────────────────────────
  // Splits the page into four focused views so the owner can drill
  // into one concern at a time instead of scrolling through five
  // stacked sections. Persists the active tab in localStorage so
  // power users land back on the view they care about.
  //   "overview" — status band (KPIs + active-user pills)
  //   "workload" — heatmap + daily activity (team utilization)
  //   "projects" — per-project counts table
  //   "people"   — per-user counts table (promoted from collapsible)
  // Same rules-of-hooks rule: this MUST be above the early returns.
  const [activeTab, setActiveTab] = React.useState(() => {
    try {
      const v = window.localStorage.getItem("ownerdash.activeTab");
      if (v === "overview" || v === "workload" || v === "projects" || v === "people" || v === "delays" || v === "conflicts") return v;
    } catch {}
    return "overview";
  });
  React.useEffect(() => {
    try { window.localStorage.setItem("ownerdash.activeTab", activeTab); } catch {}
  }, [activeTab]);

  // ── Per-tab filter state ──────────────────────────────────────
  // Each tab gets its own filter bar. State lives at this level so
  // filters survive tab switches AND so the Workload tab can pass a
  // person search down to both WorkloadHeatmap and DailyActivityPanel.
  // Rules-of-hooks demands these stay above the early returns below.
  const [overviewProject, setOverviewProject] = React.useState("all");      // project id or "all"
  const [workloadQ, setWorkloadQ]             = React.useState("");          // person search (heatmap + daily)
  const [projectsQ, setProjectsQ]             = React.useState("");          // project name search
  const [projectsSort, setProjectsSort]       = React.useState("pending");   // pending | overdue | due_today | done | name
  const [projectsHideZero, setProjectsHideZero] = React.useState(false);
  const [peopleQ, setPeopleQ]                 = React.useState("");          // name/email search
  const [peopleRole, setPeopleRole]           = React.useState("all");       // ws role
  const [peopleActivity, setPeopleActivity]   = React.useState("all");       // online | today | week | cold | all
  const [peopleTeam, setPeopleTeam]           = React.useState("all");
  const [peopleHideEmpty, setPeopleHideEmpty] = React.useState(false);       // hide users with 0 of everything
  React.useEffect(() => {
    let cancelled = false;
    (async () => {
      if (!window.api || !window.api.admin || !window.api.admin.backupStatus) return;
      try {
        const r = await window.api.admin.backupStatus();
        if (!cancelled) setBackup(r);
      } catch {}
    })();
    return () => { cancelled = true; };
  }, [data && data.generated_at]);

  const reload = React.useCallback(async (silent = false) => {
    if (!silent) setLoading(true);
    setRefreshing(true);
    try {
      const r = await api.dashboards.owner();
      setData(r); setErr("");
    } catch (e) {
      // Surface the SQL/driver error message that the route now
      // returns in the response body, instead of the generic
      // "Server error". `e.body` is set by api.js's request helper
      // when the server returns JSON with a `message` field.
      const fromBody = e && e.body && (e.body.message || e.body.error);
      const msg =
        fromBody ||
        (e && e.message) ||
        "Could not load dashboard";
      setErr(msg);
    } finally {
      setLoading(false);
      setRefreshing(false);
    }
  }, []);

  React.useEffect(() => { reload(); }, [reload]);

  // 60s background refresh while the tab is visible. Pause when
  // hidden so we're not hammering the API in idle background tabs.
  React.useEffect(() => {
    let timer = null;
    function start() {
      stop();
      timer = setInterval(() => {
        if (document.visibilityState === "visible") reload(true);
      }, 60_000);
    }
    function stop() { if (timer) { clearInterval(timer); timer = null; } }
    start();
    document.addEventListener("visibilitychange", start);
    return () => {
      stop();
      document.removeEventListener("visibilitychange", start);
    };
  }, [reload]);

  if (loading && !data) {
    return (
      <div className="owner-dash">
        <div className="owner-dash-header">
          <h1>Owner dashboard</h1>
        </div>
        <div className="owner-dash-loading">Loading…</div>
      </div>
    );
  }

  if (err && !data) {
    return (
      <div className="owner-dash">
        <div className="owner-dash-header">
          <h1>Owner dashboard</h1>
        </div>
        <div className="owner-dash-err">
          {err}
          <button className="btn" style={{ marginLeft: 12 }} onClick={() => reload()}>Retry</button>
        </div>
      </div>
    );
  }

  const o = data.overall;
  const buckets = data.activity_buckets || { online: 0, today: 0, week: 0, cold: 0 };
  // (backup hooks live up at the top — see the rules-of-hooks comment.)

  async function downloadBackup() {
    if (backupBusy) return;
    setBackupBusy("download");
    const tFn = (typeof window !== "undefined" && typeof window.fbToast === "function") ? window.fbToast : null;
    try {
      const info = await window.api.admin.backupDownload();
      if (tFn) tFn(`Backup downloaded — ${info.tables || "?"} tables, ${info.rows || "?"} rows.`, 4000);
      try { setBackup(await window.api.admin.backupStatus()); } catch {}
    } catch (e) {
      const msg = (e && e.body && (e.body.message || e.body.error)) || (e && e.message) || "network error";
      alert("Couldn't download backup: " + msg);
    } finally { setBackupBusy(null); }
  }

  async function emailBackup() {
    if (backupBusy) return;
    // Hard-stop with a clear pointer to the env vars when SMTP isn't
    // configured, instead of letting the server return a 412.
    if (backup && backup.smtp_configured === false) {
      alert(
        "Email is not configured on the server.\n\n" +
        "Set these environment variables and restart:\n" +
        "  SMTP_HOST     (e.g. smtp.gmail.com)\n" +
        "  SMTP_PORT     (587 for STARTTLS, 465 for TLS)\n" +
        "  SMTP_USER     SMTP username / login email\n" +
        "  SMTP_PASS     SMTP password / app password\n" +
        "  SMTP_FROM     From: header (optional)\n" +
        "  BACKUP_TO_EMAIL  Default recipient address\n\n" +
        "Until then you can use Download backup to grab the JSON."
      );
      return;
    }
    const to = window.prompt(
      "Email the backup attachment to:\n\n" +
      "(Leave blank to use the server's BACKUP_TO_EMAIL default.)",
      ""
    );
    if (to === null) return; // cancelled
    setBackupBusy("email");
    const tFn = (typeof window !== "undefined" && typeof window.fbToast === "function") ? window.fbToast : null;
    try {
      const r = await window.api.admin.backupEmail((to || "").trim() || null);
      if (tFn) tFn(`Backup emailed to ${r.to}.`, 4500);
      try { setBackup(await window.api.admin.backupStatus()); } catch {}
    } catch (e) {
      const msg = (e && e.body && (e.body.message || e.body.error)) || (e && e.message) || "network error";
      alert("Couldn't email backup: " + msg);
    } finally { setBackupBusy(null); }
  }

  // Owner-only: force every connected tab to reload itself.
  // Useful right after a critical deploy when you don't want to
  // wait for the per-tab "New version" banner to be noticed.
  // Confirms before broadcasting; reason and delay are optional
  // (3-second default delay so reconnects don't all hit the same
  // millisecond and tank the dyno on Render free tier).
  async function broadcastForceReload() {
    if (!window.api || !window.api.admin) return;
    const reason = window.prompt(
      "Force every connected tab to reload?\n\n" +
      "Optional reason — shown as a toast on each user's screen:",
      "Updating to the latest version"
    );
    // null = user clicked Cancel.
    if (reason === null) return;
    try {
      const r = await window.api.admin.forceReload({
        reason: reason || null,
        delay_ms: 3000,
      });
      const tFn = (typeof window !== "undefined" && typeof window.fbToast === "function")
        ? window.fbToast
        : null;
      if (tFn) tFn(`Reload broadcast sent — every connected tab will reload in ~3s.`, 4000);
    } catch (e) {
      const msg = (e && e.body && (e.body.message || e.body.error)) || (e && e.message) || "network error";
      alert("Couldn't broadcast reload: " + msg);
    }
  }

  return (
    <div className="owner-dash">
      {/* ── Page header ──────────────────────────────────────────
          Single row: title block on the left (h1 + meta line),
          action buttons on the right (Refresh + Admin menu). Both
          groups baseline-align so the "Updated …" line and the
          buttons share the same horizontal axis. */}
      <header className="owner-dash-header">
        <div className="owner-dash-section-titles">
          <h1>Owner dashboard</h1>
          <div className="owner-dash-section-sub">
            Updated {new Date(data.generated_at).toLocaleTimeString()}
            {refreshing && <span className="owner-dash-refreshing"> · refreshing…</span>}
          </div>
        </div>
        <div className="owner-dash-section-controls">
          <button type="button" className="owner-dash-btn" onClick={() => reload()} title="Refresh data" disabled={refreshing}>
            <span className={`owner-dash-btn-icon ${refreshing ? "is-spinning" : ""}`}>↻</span>
            <span>Refresh</span>
          </button>
          <OwnerAdminMenu
            backup={backup}
            backupBusy={backupBusy}
            onDownload={downloadBackup}
            onEmail={emailBackup}
            onForceReload={broadcastForceReload}/>
        </div>
      </header>

      {/* Unified styles for section heads + buttons across the
          whole dashboard. Inline so a missed CSS bump never leaves
          the page mis-aligned while the rest of the app is fine. */}
      <style>{OWNER_DASH_LAYOUT_CSS}</style>

      {/* ── Section tabs ────────────────────────────────────────
          Replaces the previous five-stacked-sections scroll with a
          focused-view-at-a-time UX. Header (title + Refresh + Admin)
          stays page-wide above the tabs since those controls apply
          to every tab. */}
      <nav className="owner-dash-tabs" role="tablist" aria-label="Dashboard sections">
        {[
          { id: "overview", label: "Overview",
            sub: o.pending + o.overdue ? `${o.pending} pending · ${o.overdue} overdue` : null },
          { id: "workload", label: "Workload",
            sub: "Team utilization" },
          { id: "projects", label: "Projects",
            sub: data.projects.length ? `${data.projects.length}` : null },
          { id: "people",   label: "People",
            sub: data.people.length ? `${data.people.length}` : null },
          { id: "delays",   label: "Delays",
            sub: "Completed late" },
          { id: "conflicts", label: "Conflicts",
            sub: "Cross-PC priority" },
        ].map(tab => (
          <button
            key={tab.id}
            type="button"
            role="tab"
            aria-selected={activeTab === tab.id}
            className={"owner-dash-tab" + (activeTab === tab.id ? " is-active" : "")}
            onClick={() => setActiveTab(tab.id)}>
            <span className="owner-dash-tab-label">{tab.label}</span>
            {tab.sub && <span className="owner-dash-tab-sub">{tab.sub}</span>}
          </button>
        ))}
      </nav>

      {/* ── Tab: Overview ────────────────────────────────────────
          One compact row that answers "what's the state of work
          right now?" — five action-tracker KPIs on the left, four
          activity pills on the right. The project picker filter
          scopes the KPI cards (activity pills stay workspace-wide
          since they're user-not-project metrics). When a single
          project is selected we show its 30d done count instead of
          the workspace 7d count, since the per-project payload only
          carries 30d granularity. */}
      {activeTab === "overview" && (() => {
        const scoped = overviewProject === "all"
          ? { ...o, _doneLabel: "Done · 7d", _doneValue: o.done_last_7d, _scope: null }
          : (() => {
              const p = data.projects.find(x => x.id === overviewProject);
              if (!p) return { ...o, _doneLabel: "Done · 7d", _doneValue: o.done_last_7d, _scope: null };
              return {
                pending: p.pending, overdue: p.overdue, due_today: p.due_today,
                no_due: p.no_due || 0, total: p.total,
                _doneLabel: "Done · 30d", _doneValue: p.done_last_30d || 0,
                _scope: { projectId: p.id, projectName: p.name },
              };
            })();
        const scopeName = scoped._scope ? scoped._scope.projectName : null;
        return (
          <>
            <DashFilterBar>
              <DashFilterField label="Project">
                <select className="owner-dash-filter-select"
                        value={overviewProject}
                        onChange={(e) => setOverviewProject(e.target.value)}>
                  <option value="all">All projects</option>
                  {data.projects.map(p => (
                    <option key={p.id} value={p.id}>{p.name}</option>
                  ))}
                </select>
              </DashFilterField>
              {overviewProject !== "all" && (
                <button type="button" className="owner-dash-filter-clear"
                        onClick={() => setOverviewProject("all")}
                        title="Clear filters">
                  Clear
                </button>
              )}
            </DashFilterBar>
            <section className="owner-dash-status">
              <div className="owner-dash-status-cards">
                <DashCard label="Pending"   value={scoped.pending}   sub={`${scoped.total} total`}
                          onClick={scoped.pending  > 0 ? () => openTaskList("pending",   scoped._scope || {}, scopeName ? `Pending tasks in ${scopeName}` : "All pending tasks") : null}/>
                <DashCard label="Overdue"   value={scoped.overdue}   sub="past due"  tone="danger"
                          onClick={scoped.overdue  > 0 ? () => openTaskList("overdue",   scoped._scope || {}, scopeName ? `Overdue tasks in ${scopeName}` : "All overdue tasks") : null}/>
                <DashCard label="Due today" value={scoped.due_today} sub="ship today" tone="warn"
                          onClick={scoped.due_today > 0 ? () => openTaskList("due_today", scoped._scope || {}, scopeName ? `Tasks due today in ${scopeName}` : "All tasks due today") : null}/>
                <DashCard label="No date"   value={scoped.no_due || 0} sub="schedule"
                          onClick={(scoped.no_due || 0) > 0 ? () => openTaskList("no_due", scoped._scope || {}, scopeName ? `Unscheduled tasks in ${scopeName}` : "Open tasks with no due date") : null}/>
                <DashCard label={scoped._doneLabel} value={scoped._doneValue} sub="velocity" tone="ok"/>
              </div>
              <div className="owner-dash-status-pills">
                <div className="owner-dash-status-pills-label">Active users</div>
                <div className="owner-dash-status-pills-row">
                  <ActivityPill kind="online" count={buckets.online} label="Online"/>
                  <ActivityPill kind="today"  count={buckets.today}  label="Today"/>
                  <ActivityPill kind="week"   count={buckets.week}   label="Week"/>
                  <ActivityPill kind="cold"   count={buckets.cold}   label="Idle 7d+"/>
                </div>
              </div>
            </section>
          </>
        );
      })()}

      {/* ── Tab: Workload ────────────────────────────────────────
          Heatmap (who is buried this week, where the collisions are)
          + Daily activity (what users actually shipped). Together
          they answer "is the team's load balanced?". The person
          search field is shared — it filters rows in BOTH panels so
          you can scope to one team or one person at a time. */}
      {activeTab === "workload" && (
        <>
          <DashFilterBar>
            <DashFilterField label="Person">
              <input type="search"
                     className="owner-dash-filter-input"
                     placeholder="Filter by name or email…"
                     value={workloadQ}
                     onChange={(e) => setWorkloadQ(e.target.value)}/>
            </DashFilterField>
            {workloadQ && (
              <button type="button" className="owner-dash-filter-clear"
                      onClick={() => setWorkloadQ("")}
                      title="Clear filters">
                Clear
              </button>
            )}
          </DashFilterBar>
          <WorkloadHeatmap onOpenTaskList={openTaskList} personFilter={workloadQ}/>
          <DailyActivityPanel personFilter={workloadQ}/>
        </>
      )}

      {/* ── Tab: Projects ─────────────────────────────────────────
          Search by name + sort by any column + hide-zero-work toggle.
          Filtered count is surfaced in the section sub-line so the
          user can see how many projects matched. */}
      {activeTab === "projects" && (() => {
        const q = projectsQ.trim().toLowerCase();
        const filtered = data.projects
          .filter(p => !q || p.name.toLowerCase().includes(q))
          .filter(p => !projectsHideZero || (p.pending + p.overdue + p.due_today + (p.no_due || 0)) > 0)
          .slice()
          .sort((a, b) => {
            switch (projectsSort) {
              case "overdue":   return (b.overdue || 0)   - (a.overdue || 0);
              case "due_today": return (b.due_today || 0) - (a.due_today || 0);
              case "done":      return (b.done_last_30d || 0) - (a.done_last_30d || 0);
              case "total":     return (b.total || 0)     - (a.total || 0);
              case "name":      return a.name.localeCompare(b.name);
              case "pending":
              default:          return (b.pending || 0)   - (a.pending || 0);
            }
          });
        const filterActive = !!q || projectsHideZero || projectsSort !== "pending";
        return (
          <>
            <DashFilterBar>
              <DashFilterField label="Search">
                <input type="search"
                       className="owner-dash-filter-input"
                       placeholder="Project name…"
                       value={projectsQ}
                       onChange={(e) => setProjectsQ(e.target.value)}/>
              </DashFilterField>
              <DashFilterField label="Sort by">
                <select className="owner-dash-filter-select"
                        value={projectsSort}
                        onChange={(e) => setProjectsSort(e.target.value)}>
                  <option value="pending">Pending (high → low)</option>
                  <option value="overdue">Overdue (high → low)</option>
                  <option value="due_today">Due today (high → low)</option>
                  <option value="done">Done · 30d (high → low)</option>
                  <option value="total">Total (high → low)</option>
                  <option value="name">Name (A → Z)</option>
                </select>
              </DashFilterField>
              <label className="owner-dash-filter-toggle" title="Hide projects with zero open work">
                <input type="checkbox"
                       checked={projectsHideZero}
                       onChange={(e) => setProjectsHideZero(e.target.checked)}/>
                <span>Hide zero-work</span>
              </label>
              {filterActive && (
                <button type="button" className="owner-dash-filter-clear"
                        onClick={() => { setProjectsQ(""); setProjectsSort("pending"); setProjectsHideZero(false); }}
                        title="Clear filters">
                  Clear
                </button>
              )}
            </DashFilterBar>
            <section className="owner-dash-section">
              <div className="owner-dash-section-head">
                <div className="owner-dash-section-titles">
                  <h2>Projects</h2>
                  <div className="owner-dash-section-sub">
                    {filtered.length === data.projects.length
                      ? `${data.projects.length} ${data.projects.length === 1 ? "project" : "projects"}`
                      : `${filtered.length} of ${data.projects.length} projects`}
                  </div>
                </div>
              </div>
              <div className="owner-dash-table-wrap">
                <table className="owner-dash-table">
                  <thead>
                    <tr>
                      <th>Project</th>
                      <th className="num">Pending</th>
                      <th className="num">Overdue</th>
                      <th className="num">Due today</th>
                      <th className="num">No due</th>
                      <th className="num">Done · 30d</th>
                      <th className="num">Total</th>
                    </tr>
                  </thead>
                  <tbody>
                    {filtered.map(p => (
                      <tr key={p.id}>
                        <td>
                          <span className="owner-dash-dot" style={{ background: p.color }}/>
                          {p.name}
                        </td>
                        <td className="num">
                          <CountCell value={p.pending}
                                     onClick={p.pending > 0 ? () => openTaskList("pending", { projectId: p.id }, `Pending tasks in ${p.name}`) : null}/>
                        </td>
                        <td className={"num" + (p.overdue > 0 ? " is-danger" : "")}>
                          <CountCell value={p.overdue}
                                     onClick={p.overdue > 0 ? () => openTaskList("overdue", { projectId: p.id }, `Overdue tasks in ${p.name}`) : null}/>
                        </td>
                        <td className={"num" + (p.due_today > 0 ? " is-warn" : "")}>
                          <CountCell value={p.due_today}
                                     onClick={p.due_today > 0 ? () => openTaskList("due_today", { projectId: p.id }, `Tasks due today in ${p.name}`) : null}/>
                        </td>
                        <td className="num">
                          <CountCell value={p.no_due || 0}
                                     onClick={(p.no_due || 0) > 0 ? () => openTaskList("no_due", { projectId: p.id }, `Unscheduled tasks in ${p.name}`) : null}/>
                        </td>
                        <td className="num">{p.done_last_30d}</td>
                        <td className="num owner-dash-muted">{p.total}</td>
                      </tr>
                    ))}
                    {filtered.length === 0 && (
                      <tr><td colSpan={7} className="owner-dash-empty">
                        {data.projects.length === 0 ? "No projects yet." : "No projects match the filters."}
                      </td></tr>
                    )}
                  </tbody>
                </table>
              </div>
            </section>
          </>
        );
      })()}

      {/* ── Tab: People ──────────────────────────────────────────
          Per-user pending / overdue / due-today / done / last-active.
          Promoted from a collapsible block to a first-class tab now
          that the page no longer has to fit everything on one scroll.
          The Workload tab covers "who is buried" / "who shipped"; this
          tab is the canonical roster view with raw counts. Filters:
          name/email search, role, activity bucket, team (computed from
          data), and a hide-empty toggle for users with no open work. */}
      {activeTab === "people" && (() => {
        const teams = Array.from(new Set(
          (data.people || []).map(p => p.team).filter(t => t && t !== "—")
        )).sort();
        const q = peopleQ.trim().toLowerCase();
        const filtered = (data.people || []).filter(p => {
          if (q) {
            const hay = (p.name + " " + (p.email || "")).toLowerCase();
            if (!hay.includes(q)) return false;
          }
          if (peopleRole !== "all" && p.ws_role !== peopleRole) return false;
          if (peopleTeam !== "all" && p.team !== peopleTeam) return false;
          if (peopleActivity !== "all") {
            const a = activityState(p.last_active_at);
            if (a.kind !== peopleActivity) return false;
          }
          if (peopleHideEmpty) {
            const open = (p.pending || 0) + (p.overdue || 0) + (p.due_today || 0) + (p.no_due || 0);
            if (open === 0) return false;
          }
          return true;
        });
        const filterActive =
          !!q || peopleRole !== "all" || peopleTeam !== "all" ||
          peopleActivity !== "all" || peopleHideEmpty;
        return (
          <>
            <DashFilterBar>
              <DashFilterField label="Search">
                <input type="search"
                       className="owner-dash-filter-input"
                       placeholder="Name or email…"
                       value={peopleQ}
                       onChange={(e) => setPeopleQ(e.target.value)}/>
              </DashFilterField>
              <DashFilterField label="Role">
                <select className="owner-dash-filter-select"
                        value={peopleRole}
                        onChange={(e) => setPeopleRole(e.target.value)}>
                  <option value="all">All roles</option>
                  <option value="owner">Owner</option>
                  <option value="admin">Admin</option>
                  <option value="member">Member</option>
                  <option value="guest">Guest</option>
                </select>
              </DashFilterField>
              <DashFilterField label="Activity">
                <select className="owner-dash-filter-select"
                        value={peopleActivity}
                        onChange={(e) => setPeopleActivity(e.target.value)}>
                  <option value="all">All activity</option>
                  <option value="online">Online now</option>
                  <option value="today">Today</option>
                  <option value="week">This week</option>
                  <option value="cold">Idle 7+ days</option>
                </select>
              </DashFilterField>
              {teams.length > 0 && (
                <DashFilterField label="Team">
                  <select className="owner-dash-filter-select"
                          value={peopleTeam}
                          onChange={(e) => setPeopleTeam(e.target.value)}>
                    <option value="all">All teams</option>
                    {teams.map(t => <option key={t} value={t}>{t}</option>)}
                  </select>
                </DashFilterField>
              )}
              <label className="owner-dash-filter-toggle" title="Hide users with no open work">
                <input type="checkbox"
                       checked={peopleHideEmpty}
                       onChange={(e) => setPeopleHideEmpty(e.target.checked)}/>
                <span>Hide empty</span>
              </label>
              {filterActive && (
                <button type="button" className="owner-dash-filter-clear"
                        onClick={() => {
                          setPeopleQ(""); setPeopleRole("all");
                          setPeopleActivity("all"); setPeopleTeam("all");
                          setPeopleHideEmpty(false);
                        }}
                        title="Clear filters">
                  Clear
                </button>
              )}
            </DashFilterBar>
            <section className="owner-dash-section">
              <div className="owner-dash-section-head">
                <div className="owner-dash-section-titles">
                  <h2>People</h2>
                  <div className="owner-dash-section-sub">
                    Per-user pending / overdue / done counts and last active · {
                      filtered.length === data.people.length
                        ? `${data.people.length}`
                        : `${filtered.length} of ${data.people.length}`
                    }
                  </div>
                </div>
              </div>
              <div className="owner-dash-table-wrap">
          <table className="owner-dash-table">
            <thead>
              <tr>
                <th>Person</th>
                <th>Role</th>
                <th>Team</th>
                <th className="num">Pending</th>
                <th className="num">Overdue</th>
                <th className="num">Due today</th>
                <th className="num">No due</th>
                <th className="num">Done · 30d</th>
                <th>Last active</th>
              </tr>
            </thead>
            <tbody>
              {filtered.map(p => {
                const a = activityState(p.last_active_at);
                return (
                  <tr key={p.id}>
                    <td>
                      <div className="owner-dash-user">
                        {p.avatar
                          ? <img className="owner-dash-avatar" src={p.avatar} alt=""/>
                          : <span className="owner-dash-avatar owner-dash-avatar-fallback"
                                  style={{ background: p.color || "#b3b8c2" }}>
                              {String(p.name || "?").trim().split(/\s+/).map(s => s[0]).slice(0,2).join("").toUpperCase()}
                            </span>}
                        <div>
                          <div className="owner-dash-user-name">{p.name}</div>
                          <div className="owner-dash-user-sub">{p.email}</div>
                        </div>
                      </div>
                    </td>
                    <td className="owner-dash-muted">{p.ws_role}</td>
                    <td className="owner-dash-muted">{p.team || "—"}</td>
                    <td className="num">
                      <CountCell value={p.pending}
                                 onClick={p.pending > 0 ? () => openTaskList("pending", { userId: p.id }, `Pending tasks for ${p.name}`) : null}/>
                    </td>
                    <td className={"num" + (p.overdue > 0 ? " is-danger" : "")}>
                      <CountCell value={p.overdue}
                                 onClick={p.overdue > 0 ? () => openTaskList("overdue", { userId: p.id }, `Overdue tasks for ${p.name}`) : null}/>
                    </td>
                    <td className={"num" + (p.due_today > 0 ? " is-warn" : "")}>
                      <CountCell value={p.due_today}
                                 onClick={p.due_today > 0 ? () => openTaskList("due_today", { userId: p.id }, `Tasks due today for ${p.name}`) : null}/>
                    </td>
                    <td className="num">
                      <CountCell value={p.no_due || 0}
                                 onClick={(p.no_due || 0) > 0 ? () => openTaskList("no_due", { userId: p.id }, `Unscheduled tasks for ${p.name}`) : null}/>
                    </td>
                    <td className="num owner-dash-muted">{p.done_last_30d}</td>
                    <td>
                      <span className={"owner-dash-active-pill is-" + a.kind}
                            title={p.last_active_at ? new Date(p.last_active_at).toLocaleString() : "never seen"}>
                        <span className="owner-dash-active-dot"/>
                        {a.label}
                      </span>
                    </td>
                  </tr>
                );
              })}
              {filtered.length === 0 && (
                <tr><td colSpan={9} className="owner-dash-empty">
                  {data.people.length === 0 ? "No active users." : "No people match the filters."}
                </td></tr>
              )}
            </tbody>
          </table>
        </div>
      </section>
          </>
        );
      })()}

      {/* ── Tab: Delays ──────────────────────────────────────────
          Every project task that was completed PAST its due date,
          rolled up across a configurable window (7 / 30 / 90 / 365
          days). Powered by a dedicated /api/dashboards/late-tasks
          endpoint so the owner can spot WHERE delivery slips and
          WHY (top reasons, worst projects, worst owners). Clicking
          a row opens the matching task drawer via flowboard:nav. */}
      {activeTab === "delays" && (
        <DelaysPanel
          projects={data.projects || []}
          people={data.people || []}/>
      )}

      {/* ── Tab: Conflicts ──────────────────────────────────────
          Cross-coordinator priority picture. Surfaces:
            * People — developers stacked with high/critical work
              from MULTIPLE coordinators (most-conflicted first)
            * Coordinators — per-PC open high/critical usage so the
              owner can see who's burning their cap
            * Bumps — bump-request volume per PC over the last 30
              days (open / accepted / declined / negotiating)
          Powered by /api/priority/conflicts. */}
      {activeTab === "conflicts" && (
        <ConflictsPanel people={data.people || []}/>
      )}

      {/* Task drill-down modal — opens when any Pending / Overdue /
          Due-today number is clicked. Lists the matching tasks
          and routes clicks via flowboard:nav so the existing app
          opens the task drawer in the right project. */}
      {taskList && (
        <TaskListModal
          kind={taskList.kind}
          scope={taskList.scope}
          title={taskList.title}
          onClose={() => setTaskList(null)}/>
      )}
    </div>
  );
}

// ── helpers ──────────────────────────────────────────────────────────
// DashCard — when `onClick` is provided AND the value is non-zero,
// the whole card becomes a button-like clickable surface that opens
// the task drill-down modal. Cards with no onClick (e.g. velocity)
// keep the read-only feel.
function DashCard({ label, value, sub, tone, onClick }) {
  const isClickable = !!onClick;
  return (
    <div className={"owner-dash-card" + (tone ? " is-" + tone : "") + (isClickable ? " is-clickable" : "")}
         role={isClickable ? "button" : undefined}
         tabIndex={isClickable ? 0 : undefined}
         onClick={isClickable ? onClick : undefined}
         onKeyDown={isClickable ? ((e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); onClick(); } }) : undefined}
         style={isClickable ? { cursor: "pointer" } : null}
         title={isClickable ? "Click to see the matching tasks" : undefined}>
      <div className="owner-dash-card-label">{label}</div>
      <div className="owner-dash-card-value">{value}</div>
      {sub && <div className="owner-dash-card-sub">{sub}</div>}
    </div>
  );
}

// CountCell — small clickable wrapper for a number inside a table
// cell. Falls back to plain text when there's no onClick (so zero
// counts don't pretend to be interactive).
// ── Per-tab filter bar primitives ───────────────────────────────
// DashFilterBar — the wrapper row that sits under the tab strip.
// DashFilterField — labelled input slot. Children are the actual
// <input>/<select> so the consumer keeps ownership of state.
// Kept lightweight on purpose: a single CSS file, no portals, no
// overflow hacks. The Clear button is rendered by each tab's
// caller (since the "what to clear" set is tab-specific).
function DashFilterBar({ children }) {
  return <div className="owner-dash-filter-bar" role="search">{children}</div>;
}
function DashFilterField({ label, children }) {
  return (
    <label className="owner-dash-filter-field">
      <span className="owner-dash-filter-label">{label}</span>
      {children}
    </label>
  );
}

function CountCell({ value, onClick }) {
  if (!onClick) return <span>{value}</span>;
  return (
    <button type="button"
            onClick={onClick}
            title="Click to see the matching tasks"
            style={{
              background: "transparent", border: 0, padding: 0, margin: 0,
              font: "inherit", color: "inherit", cursor: "pointer",
              fontWeight: 700, textDecoration: "underline",
              textDecorationColor: "rgba(0,0,0,.2)",
              textUnderlineOffset: 3,
            }}>
      {value}
    </button>
  );
}

function ActivityPill({ kind, count, label }) {
  return (
    <div className={"owner-dash-activity-pill is-" + kind}>
      <span className="owner-dash-active-dot"/>
      <b>{count}</b>
      <span>{label}</span>
    </div>
  );
}

function activityState(iso) {
  if (!iso) return { kind: "cold", label: "Never seen" };
  const t = Date.parse(iso);
  if (!isFinite(t)) return { kind: "cold", label: "—" };
  const ageMs = Date.now() - t;
  if (ageMs <= 5 * 60_000)             return { kind: "online", label: "Online now" };
  if (ageMs <= 60 * 60_000)            return { kind: "today",  label: relMinutes(ageMs) + " ago" };
  if (ageMs <= 24 * 3600_000)          return { kind: "today",  label: relHours(ageMs)   + " ago" };
  if (ageMs <= 7 * 24 * 3600_000)      return { kind: "week",   label: Math.round(ageMs / (24 * 3600_000)) + "d ago" };
  return { kind: "cold", label: "7+ days idle" };
}
function relMinutes(ms) { return Math.max(1, Math.round(ms / 60_000)) + "m"; }
function relHours(ms)   { return Math.max(1, Math.round(ms / 3600_000)) + "h"; }

// ── TaskListModal ──────────────────────────────────────────────────
// Lightweight modal that filters window.ALL_TASKS for the chosen
// kind/scope and renders the result. We deliberately do client-side
// filtering instead of adding a server endpoint because the owner
// dashboard already has every task in memory (workspace owner sees
// all tasks via the bootstrap), and an extra round-trip would only
// add latency.
//
// kind:  'overdue' | 'due_today' | 'pending'
// scope: { projectId?, userId? }
//
// Click on a task row dispatches `flowboard:nav` with `/tasks/<id>`.
// The handler in app.jsx switches the active project, switches view
// to project, and opens the task drawer — no extra wiring needed.
function TaskListModal({ kind, scope, title, onClose }) {
  const all = (typeof window !== "undefined" && Array.isArray(window.ALL_TASKS)) ? window.ALL_TASKS : [];
  // Normalise the task's due value to a YYYY-MM-DD ordinal we can
  // compare against today. Handles both formats the codebase ships:
  //   * ISO         "2026-05-07"           — what the DB column stores
  //                                          (the dashboard's SQL uses
  //                                          due_date = CURDATE() so it
  //                                          MUST be in this format).
  //   * Short       "May 7" / "May 7 @ 2pm" — display-only format used
  //                                          by some UIs.
  // The original window.isOverdueNow only handles the short form, so
  // it returned false for every ISO-stored task — which is why the
  // modal was empty even though the count was non-zero.
  function dueOrdinal(t) {
    const raw = (t && t.due) || (t && t.due_date) || "";
    if (!raw || raw === "—") return null;
    const head = String(raw).split(" @ ")[0].trim();
    // ISO YYYY-MM-DD
    let m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(head);
    if (m) return Number(m[1]) * 10000 + Number(m[2]) * 100 + Number(m[3]);
    // Short "May 7" — assume current year for comparison.
    m = /^([A-Za-z]{3})\s+(\d{1,2})$/.exec(head);
    if (m) {
      const moMap = { Jan:1,Feb:2,Mar:3,Apr:4,May:5,Jun:6,Jul:7,Aug:8,Sep:9,Oct:10,Nov:11,Dec:12 };
      const mo = moMap[m[1]] || 0;
      if (!mo) return null;
      const year = new Date().getFullYear();
      return year * 10000 + mo * 100 + Number(m[2]);
    }
    return null;
  }
  function todayOrdinal() {
    const t = new Date();
    return t.getFullYear() * 10000 + (t.getMonth() + 1) * 100 + t.getDate();
  }
  // Render the due-date for the chip. Accepts both ISO and short
  // forms; ISO gets converted to the friendly "May 7" shape. Returns
  // null when the value is empty / unrecognised so the caller can
  // render "no date" instead.
  function prettyDue(raw) {
    if (!raw || raw === "—") return null;
    const head = String(raw).split(" @ ")[0].trim();
    const tail = String(raw).split(" @ ")[1] || "";
    const months = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];
    const iso = /^(\d{4})-(\d{2})-(\d{2})$/.exec(head);
    if (iso) {
      return months[Number(iso[2]) - 1] + " " + Number(iso[3]) + (tail ? " @ " + tail : "");
    }
    return raw;
  }
  // For kind === "on_day", scope.day is an ISO YYYY-MM-DD string.
  // Convert it to the same ordinal shape dueOrdinal returns so the
  // filter is a single integer compare.
  function isoDayOrdinal(iso) {
    if (!iso) return null;
    const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(iso).slice(0, 10));
    if (!m) return null;
    return Number(m[1]) * 10000 + Number(m[2]) * 100 + Number(m[3]);
  }
  const filtered = React.useMemo(() => {
    const today = todayOrdinal();
    const dayOrd = (kind === "on_day" && scope && scope.day) ? isoDayOrdinal(scope.day) : null;
    return all.filter(t => {
      if (!t || t.status === "done") return false;
      if (scope && scope.projectId && t.projectId !== scope.projectId) return false;
      if (scope && scope.userId    && !(Array.isArray(t.owners) && t.owners.includes(scope.userId))) return false;
      if (kind === "pending") return true; // any open task in scope
      const ord = dueOrdinal(t);
      // "No due date" — open task whose due field is missing/empty/—.
      // Mirrors the server SQL: due_date IS NULL OR '' OR '—'.
      if (kind === "no_due") return ord == null;
      if (ord == null) return false; // overdue / due_today / on_day need a date
      if (kind === "overdue")   return ord <  today;
      if (kind === "due_today") return ord === today;
      if (kind === "on_day")    return dayOrd != null && ord === dayOrd;
      return false;
    });
  }, [all, kind, scope && scope.projectId, scope && scope.userId, scope && scope.day]);

  // Close on Escape and on click outside the dialog.
  React.useEffect(() => {
    function onKey(e) { if (e.key === "Escape") onClose(); }
    document.addEventListener("keydown", onKey);
    return () => document.removeEventListener("keydown", onKey);
  }, [onClose]);

  function openTask(t) {
    if (!t || !t.id) return;
    onClose();
    try {
      window.dispatchEvent(new CustomEvent("flowboard:nav", {
        detail: { link: "/tasks/" + encodeURIComponent(t.id) },
      }));
    } catch {}
  }

  // Sort: overdue first (oldest first within overdue), then by
  // project name, then by task name. Uses the same dueOrdinal as
  // the filter so the dates are interpreted consistently.
  const sorted = React.useMemo(() => {
    const today = todayOrdinal();
    const arr = filtered.slice();
    arr.sort((a, b) => {
      const ao = dueOrdinal(a);
      const bo = dueOrdinal(b);
      const aOverdue = ao != null && ao < today;
      const bOverdue = bo != null && bo < today;
      if (aOverdue !== bOverdue) return aOverdue ? -1 : 1;
      if (aOverdue && bOverdue && ao !== bo) return ao - bo; // oldest first
      const ap = (a.projectName || "").toLowerCase();
      const bp = (b.projectName || "").toLowerCase();
      if (ap !== bp) return ap < bp ? -1 : 1;
      return (a.name || "").localeCompare(b.name || "");
    });
    return arr;
  }, [filtered]);

  return ReactDOM.createPortal(
    <div className="owner-tlm-backdrop"
         onMouseDown={(e) => { if (e.target === e.currentTarget) onClose(); }}
         style={{
           position: "fixed", inset: 0, background: "rgba(15,23,41,.45)",
           display: "flex", alignItems: "center", justifyContent: "center",
           zIndex: 9999,
         }}>
      <div className="owner-tlm"
           style={{
             background: "white", borderRadius: 10,
             width: "min(720px, 92vw)",
             maxHeight: "82vh", display: "flex", flexDirection: "column",
             boxShadow: "0 20px 50px rgba(15,23,41,.30)",
             overflow: "hidden",
           }}>
        <div style={{
          padding: "14px 18px", borderBottom: "1px solid var(--border)",
          display: "flex", alignItems: "center", gap: 10,
        }}>
          <span style={{
            display: "inline-flex", alignItems: "center", justifyContent: "center",
            width: 26, height: 26, borderRadius: 999,
            background: kind === "overdue" ? "rgba(226,68,92,.14)"
                      : kind === "due_today" ? "rgba(245,158,11,.14)"
                      : kind === "no_due" ? "rgba(107,114,128,.14)"
                      : "rgba(0,115,234,.10)",
            color: kind === "overdue" ? "#8a1024"
                  : kind === "due_today" ? "#7a4205"
                  : kind === "no_due" ? "#374151"
                  : "#0044a3",
            fontSize: 14, fontWeight: 700,
          }}>
            {kind === "overdue" ? "!"
             : kind === "due_today" ? "▲"
             : kind === "no_due" ? "?"
             : "•"}
          </span>
          <div style={{ flex: 1, minWidth: 0 }}>
            <div style={{ fontWeight: 700, color: "var(--ink-strong)", fontSize: 15 }}>{title}</div>
            <div style={{ fontSize: 12, color: "var(--ink-muted)" }}>
              {sorted.length} task{sorted.length === 1 ? "" : "s"}
            </div>
          </div>
          <button type="button" onClick={onClose}
                  style={{
                    background: "transparent", border: 0, cursor: "pointer",
                    fontSize: 20, color: "var(--ink-muted)", lineHeight: 1, padding: 4,
                  }}
                  title="Close (Esc)">×</button>
        </div>

        <div style={{ overflowY: "auto", padding: "8px 0" }}>
          {sorted.length === 0 && (
            <div style={{ padding: "32px 18px", textAlign: "center", color: "var(--ink-muted)" }}>
              Nothing matches this filter — every task is either done or scheduled past today.
            </div>
          )}
          {sorted.map(t => {
            const today = todayOrdinal();
            const ord = dueOrdinal(t);
            const overdue = ord != null && ord < today;
            const stMeta = (typeof STATUSES !== "undefined" ? STATUSES : []).find(x => x.id === t.status)
                        || { label: t.status || "—", cls: "pill-todo" };
            const owner = (Array.isArray(t.owners) && t.owners.length)
              ? (PEOPLE || []).find(p => p.id === t.owners[0]) : null;
            return (
              <div key={t.id}
                   onClick={() => openTask(t)}
                   style={{
                     display: "flex", alignItems: "center", gap: 12,
                     padding: "8px 18px",
                     borderBottom: "1px solid rgba(0,0,0,.04)",
                     cursor: "pointer",
                   }}
                   onMouseEnter={(e) => e.currentTarget.style.background = "rgba(0,0,0,.025)"}
                   onMouseLeave={(e) => e.currentTarget.style.background = "transparent"}
                   title={`Open ${t.id}`}>
                <div style={{ flex: 1, minWidth: 0 }}>
                  <div style={{
                    fontWeight: 600, color: "var(--ink-strong)", fontSize: 13,
                    overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap",
                  }}>{t.name}</div>
                  <div style={{ fontSize: 11, color: "var(--ink-muted)", display: "flex", gap: 6, alignItems: "center", marginTop: 4, flexWrap: "wrap" }}>
                    {/* Project chip with the project's brand colour —
                        more visible than a tiny dot so the user can
                        tell at a glance which project each task sits
                        in. Especially useful when the modal is the
                        unscoped "All overdue" / "All due today" view. */}
                    {t.projectName && (
                      <span style={{
                        display: "inline-flex", alignItems: "center", gap: 5,
                        padding: "2px 8px", borderRadius: 999,
                        background: t.projectColor ? `${t.projectColor}1f` : "rgba(0,0,0,.05)",
                        color: t.projectColor || "var(--ink-body)",
                        fontWeight: 600, fontSize: 11,
                      }}>
                        <span style={{
                          width: 7, height: 7, borderRadius: 999, flex: "none",
                          background: t.projectColor || "#a3a8b6",
                        }}/>
                        {t.projectName}
                      </span>
                    )}
                    {t.epicTitle && (
                      <span style={{ color: "var(--ink-muted)" }}>· {t.epicTitle}</span>
                    )}
                  </div>
                </div>
                {/* Due chip — red for overdue, amber for today, grey
                    otherwise. Display goes through prettyDue() so an
                    ISO "2026-05-07" reads as "May 7" rather than the
                    ugly raw column value. */}
                <span style={{
                  fontSize: 11, padding: "2px 8px", borderRadius: 999,
                  background: overdue ? "rgba(226,68,92,.14)"
                            : (ord != null && ord === today) ? "rgba(245,158,11,.14)"
                            : "rgba(0,0,0,.05)",
                  color: overdue ? "#8a1024"
                        : (ord != null && ord === today) ? "#7a4205"
                        : "var(--ink-muted)",
                  fontWeight: 600, whiteSpace: "nowrap",
                }}>{prettyDue(t.due) || "no date"}</span>
                <span className={"pill pill-sm " + stMeta.cls} style={{ minWidth: 80 }}>{stMeta.label}</span>
                {owner ? (
                  <Avatar person={owner} size="sm"/>
                ) : (
                  <span style={{
                    width: 22, height: 22, borderRadius: "50%",
                    border: "1.5px dashed var(--border-strong)",
                    color: "#a3a8b6", fontSize: 11,
                    display: "inline-flex", alignItems: "center", justifyContent: "center",
                  }}>?</span>
                )}
              </div>
            );
          })}
        </div>
      </div>
    </div>,
    document.body
  );
}

// ─────────────────────────────────────────────────────────────────────
// WorkloadHeatmap — per-user open-task counts grouped by due-day,
// rendered as a shaded grid. Rows = users, columns = days starting
// from today. Click a cell to drill into the per-user/per-day task
// list via the existing TaskListModal (kind="on_day", scope.day=ISO).
//
// The endpoint aggregates server-side because we can't reliably
// normalize the dual-format due_date string in JS without re-doing
// the DB's SQL. Polls on mount and refetches when the day-window
// selector changes; no auto-refresh — the parent dashboard already
// hits the keep-warm timer.
// ─────────────────────────────────────────────────────────────────────
function WorkloadHeatmap({ onOpenTaskList, personFilter }) {
  const [days, setDays]   = React.useState(14);
  const [data, setData]   = React.useState(null);
  const [err, setErr]     = React.useState("");
  const [loading, setLoading] = React.useState(true);
  // Person filter — when the parent (Workload tab) is filtering by
  // name/email, we hide non-matching rows here so the heatmap and the
  // daily-activity panel below it show the same scope.
  const _filterQ = String(personFilter || "").trim().toLowerCase();
  const _matchesFilter = (u) => {
    if (!_filterQ) return true;
    const hay = ((u && u.name) || "") + " " + ((u && u.email) || "");
    return hay.toLowerCase().includes(_filterQ);
  };

  const reload = React.useCallback(async () => {
    setLoading(true);
    try {
      const r = await api.dashboards.workloadHeatmap(days);
      setData(r); setErr("");
    } catch (e) {
      setErr((e && e.body && (e.body.message || e.body.error)) || (e && e.message) || "Couldn't load workload");
      setData(null);
    } finally { setLoading(false); }
  }, [days]);
  React.useEffect(() => { reload(); }, [reload]);

  // Heat colour ramp — six buckets keyed off task count. Tuned so a
  // user with 0 reads as "empty" (very pale neutral), 1-2 as "fine",
  // 3-5 as "busy", 6-9 as "full", 10+ as "danger" (saturated red).
  function bucket(n) {
    if (!n) return 0;
    if (n <= 2) return 1;
    if (n <= 5) return 2;
    if (n <= 9) return 3;
    return 4;
  }
  const HEAT = [
    "#f3f4f6", // 0
    "#cfe6ff", // 1-2
    "#7ab7ff", // 3-5
    "#2f8aff", // 6-9
    "#d24f4f", // 10+
  ];
  const HEAT_INK = ["#9aa0a6", "#16407c", "#0a3a85", "#fff", "#fff"];

  // Format a YYYY-MM-DD as "Fri 8" (weekday short + day-of-month).
  // Today gets a special "Today" pill so the eye snaps to it.
  function fmtHead(iso) {
    const d = new Date(iso + "T00:00:00");
    const wd = d.toLocaleString("en-US", { weekday: "short" });
    return { wd, dom: d.getDate(), iso };
  }
  const todayISO = (() => {
    const t = new Date();
    return [t.getFullYear(),
            String(t.getMonth() + 1).padStart(2, "0"),
            String(t.getDate()).padStart(2, "0")].join("-");
  })();

  return (
    <section className="owner-dash-section">
      <div className="owner-dash-section-head">
        <div className="owner-dash-section-titles">
          <h2>Workload heatmap</h2>
          <div className="owner-dash-section-sub">Open tasks by due date · click a cell to drill in</div>
        </div>
        <div className="owner-dash-section-controls">
          <span className="wlh-legend" aria-hidden="true">
            <span className="wlh-legend-label">Less</span>
            {HEAT.slice(0).map((c, i) => (
              <span key={i} className="wlh-legend-swatch" style={{ background: c }}/>
            ))}
            <span className="wlh-legend-label">More</span>
          </span>
          <div className="owner-dash-range">
            {[7, 14, 30].map(n => (
              <button
                key={n}
                type="button"
                className={`owner-dash-range-pill ${days === n ? "is-active" : ""}`}
                onClick={() => setDays(n)}>
                {n}d
              </button>
            ))}
          </div>
          <button type="button" className="owner-dash-reload" onClick={reload} disabled={loading} title="Refresh">↻</button>
        </div>
      </div>

      {err && <div className="owner-dash-error" style={{ marginTop: 8 }}>{err}</div>}

      {!err && (loading || !data) && (
        <div className="wlh-empty">Loading workload…</div>
      )}

      {!err && data && data.users.length === 0 && (
        <div className="wlh-empty">No active users.</div>
      )}

      {!err && data && data.users.length > 0 && (() => {
        const visibleUsers = data.users.filter(_matchesFilter);
        return (
        <div className="wlh-scroll">
          <table className="wlh-grid">
            <thead>
              <tr>
                <th className="wlh-name-col">Person</th>
                <th className="wlh-totals-col" title="Open tasks already past due">Overdue</th>
                <th className="wlh-totals-col" title="Open tasks with no due date">No due</th>
                {data.days.map(iso => {
                  const h = fmtHead(iso);
                  const isToday = iso === todayISO;
                  return (
                    <th key={iso} className={`wlh-day-col ${isToday ? "is-today" : ""}`}>
                      <div className="wlh-day-wd">{h.wd}</div>
                      <div className="wlh-day-dom">{h.dom}</div>
                      {isToday && <div className="wlh-day-today">Today</div>}
                    </th>
                  );
                })}
                <th className="wlh-totals-col" title="Total open tasks">Open</th>
              </tr>
            </thead>
            <tbody>
              {visibleUsers.length === 0 && (
                <tr>
                  <td colSpan={3 + (data.days ? data.days.length : 0) + 1}
                      className="wlh-empty" style={{ padding: "16px 12px" }}>
                    No people match the filter.
                  </td>
                </tr>
              )}
              {visibleUsers.map(u => (
                <tr key={u.id}>
                  <td className="wlh-name-cell">
                    <div className="owner-dash-user">
                      {u.avatar
                        ? <img className="owner-dash-avatar" src={u.avatar} alt=""/>
                        : <span className="owner-dash-avatar owner-dash-avatar-fallback"
                                style={{ background: u.color || "#b3b8c2" }}>
                            {String(u.name || "?").trim().split(/\s+/).map(s => s[0]).slice(0,2).join("").toUpperCase()}
                          </span>}
                      <div>
                        <div className="owner-dash-user-name">{u.name}</div>
                        <div className="owner-dash-user-sub">
                          {u.title || u.team || u.ws_role || ""}
                        </div>
                      </div>
                    </div>
                  </td>
                  <td className={"wlh-pill-cell" + (u.overdue > 0 ? " is-danger" : "")}>
                    <CountCell value={u.overdue}
                      onClick={u.overdue > 0
                        ? () => onOpenTaskList && onOpenTaskList("overdue", { userId: u.id }, `Overdue tasks for ${u.name}`)
                        : null}/>
                  </td>
                  <td className="wlh-pill-cell">
                    <CountCell value={u.no_due}
                      onClick={u.no_due > 0
                        ? () => onOpenTaskList && onOpenTaskList("no_due", { userId: u.id }, `Unscheduled tasks for ${u.name}`)
                        : null}/>
                  </td>
                  {data.days.map(iso => {
                    const n = u.by_day[iso] || 0;
                    const b = bucket(n);
                    const isToday = iso === todayISO;
                    const niceDay = new Date(iso + "T00:00:00")
                      .toLocaleString("en-US", { month: "short", day: "numeric" });
                    return (
                      <td key={iso} className={`wlh-cell ${n > 0 ? "is-active" : ""} ${isToday ? "is-today" : ""}`}>
                        <button
                          className="wlh-cell-btn"
                          style={{ background: HEAT[b], color: HEAT_INK[b] }}
                          disabled={n === 0}
                          title={n === 0
                            ? `${u.name}: no tasks on ${niceDay}`
                            : `${u.name}: ${n} task${n === 1 ? "" : "s"} due ${niceDay} — click to view`}
                          onClick={() => onOpenTaskList && onOpenTaskList(
                            "on_day",
                            { userId: u.id, day: iso },
                            `${u.name} — tasks due ${niceDay}`,
                          )}>
                          {n || ""}
                        </button>
                      </td>
                    );
                  })}
                  <td className="wlh-pill-cell wlh-total-cell">
                    <CountCell value={u.total_open}
                      onClick={u.total_open > 0
                        ? () => onOpenTaskList && onOpenTaskList("pending", { userId: u.id }, `All open tasks for ${u.name}`)
                        : null}/>
                  </td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>
        );
      })()}
      <style>{WORKLOAD_HEATMAP_CSS}</style>
    </section>
  );
}

const WORKLOAD_HEATMAP_CSS = `
.wlh-head {
  display: flex; align-items: center; gap: 12px;
  margin-bottom: 10px;
}
.wlh-head h2 {
  font-size: 13px; font-weight: 700;
  text-transform: uppercase; letter-spacing: .06em;
  color: var(--ink-muted, #676879);
}
.wlh-toolbar { margin-left: auto; display: inline-flex; gap: 12px; align-items: center; }
.wlh-legend { display: inline-flex; align-items: center; gap: 4px; }
.wlh-legend-label { font-size: 11px; color: var(--ink-muted); margin: 0 2px; }
.wlh-legend-swatch {
  width: 14px; height: 14px; border-radius: 3px;
  border: 1px solid rgba(0,0,0,.06);
}
.wlh-range {
  display: inline-flex; gap: 2px;
  background: var(--surface-2, #f1f4f9);
  border: 1px solid var(--border, #e6e9ef);
  border-radius: 999px; padding: 3px;
}
.wlh-range-pill {
  border: none; background: transparent;
  padding: 4px 12px; font: inherit; font-size: 12px; font-weight: 600;
  color: var(--ink-muted); border-radius: 999px; cursor: pointer;
}
.wlh-range-pill.is-active {
  background: #fff; color: var(--brand, #0073ea);
  box-shadow: 0 1px 3px rgba(15,23,41,.08);
}
.wlh-reload {
  border: 1px solid var(--border, #e6e9ef);
  background: #fff; cursor: pointer;
  width: 30px; height: 30px; border-radius: 8px;
  font-size: 14px; color: var(--ink-muted);
}
.wlh-reload:disabled { opacity: .5; cursor: progress; }
.wlh-reload:hover { color: var(--brand, #0073ea); border-color: var(--brand, #0073ea); }

.wlh-empty {
  background: var(--bg-surface, #fff);
  border: 1px solid var(--border, #e6e9ef);
  border-radius: 10px;
  padding: 36px; text-align: center; color: var(--ink-muted);
  font-size: 13px;
}

.wlh-scroll {
  background: var(--bg-surface, #fff);
  border: 1px solid var(--border, #e6e9ef);
  border-radius: 10px;
  overflow-x: auto;
}
.wlh-grid {
  border-collapse: separate; border-spacing: 0;
  width: 100%; min-width: 920px;
  font-size: 12px;
}
.wlh-grid th, .wlh-grid td { padding: 0; vertical-align: middle; }
.wlh-grid thead th {
  background: var(--surface-2, #f6f7fb);
  border-bottom: 1px solid var(--border, #e6e9ef);
  position: sticky; top: 0; z-index: 1;
}
.wlh-grid tbody tr { border-bottom: 1px solid var(--border, #e6e9ef); }
.wlh-grid tbody tr:last-child { border-bottom: none; }
.wlh-name-col, .wlh-name-cell {
  position: sticky; left: 0; z-index: 2;
  background: var(--bg-surface, #fff);
  text-align: left; padding: 10px 14px;
  min-width: 220px;
  border-right: 1px solid var(--border, #e6e9ef);
}
.wlh-grid thead .wlh-name-col {
  background: var(--surface-2, #f6f7fb);
  z-index: 3;
  font-size: 11px; font-weight: 700;
  color: var(--ink-muted); text-transform: uppercase; letter-spacing: .06em;
}
.wlh-totals-col {
  font-size: 10px; font-weight: 700;
  color: var(--ink-muted); text-transform: uppercase; letter-spacing: .06em;
  padding: 8px 10px; text-align: center; min-width: 64px;
}
.wlh-day-col {
  text-align: center; padding: 6px 4px; min-width: 46px;
  font-weight: 600;
}
.wlh-day-col.is-today {
  background: rgba(0, 115, 234, 0.06);
}
.wlh-day-wd  { font-size: 10px; color: var(--ink-muted); text-transform: uppercase; letter-spacing: .06em; }
.wlh-day-dom { font-size: 14px; color: var(--ink); font-weight: 700; }
.wlh-day-today {
  font-size: 9px; color: var(--brand, #0073ea); font-weight: 700;
  text-transform: uppercase; letter-spacing: .06em; margin-top: 1px;
}
.wlh-pill-cell { text-align: center; padding: 8px 6px; }
.wlh-pill-cell.is-danger .num-cell, .wlh-pill-cell.is-danger { color: #d24f4f; }
.wlh-total-cell { font-weight: 700; }
.wlh-cell { padding: 4px; text-align: center; }
.wlh-cell.is-today { background: rgba(0, 115, 234, 0.04); }
.wlh-cell-btn {
  width: 38px; height: 32px;
  border: 1px solid rgba(0,0,0,.06);
  border-radius: 6px;
  font: inherit; font-size: 12px; font-weight: 700;
  cursor: pointer; padding: 0;
  transition: transform .1s;
}
.wlh-cell-btn:disabled { cursor: default; }
.wlh-cell-btn:not(:disabled):hover { transform: scale(1.08); box-shadow: 0 1px 4px rgba(15,23,41,.18); }
`;

// ─────────────────────────────────────────────────────────────────────
// DailyActivityPanel — what each user actually did over the last N
// days. Reads task_activity (created / commented / completed / status
// changes). Each row has a stat strip and a sparkline of completions.
// ─────────────────────────────────────────────────────────────────────
function DailyActivityPanel({ personFilter } = {}) {
  const [days, setDays]   = React.useState(7);
  const [data, setData]   = React.useState(null);
  const [err, setErr]     = React.useState("");
  const [loading, setLoading] = React.useState(true);
  // Same person-filter contract as WorkloadHeatmap so the Workload
  // tab's filter bar can scope both panels with one input.
  const _filterQ = String(personFilter || "").trim().toLowerCase();
  const _matchesFilter = (u) => {
    if (!_filterQ) return true;
    const hay = ((u && u.name) || "") + " " + ((u && u.email) || "");
    return hay.toLowerCase().includes(_filterQ);
  };

  const reload = React.useCallback(async () => {
    setLoading(true);
    try {
      const r = await api.dashboards.activityByUser(days);
      setData(r); setErr("");
    } catch (e) {
      setErr((e && e.body && (e.body.message || e.body.error)) || (e && e.message) || "Couldn't load activity");
      setData(null);
    } finally { setLoading(false); }
  }, [days]);
  React.useEffect(() => { reload(); }, [reload]);

  // Group user list — top performers first, then everyone else (so
  // owners can see "who shipped" without losing the empty rows).
  // The person filter is applied here so the section sub-line + the
  // empty state both reflect the visible scope.
  const sorted = ((data && data.users) || []).filter(_matchesFilter);
  const totalCompleted = sorted.reduce((a, u) => a + (u.totals.completed || 0), 0);

  return (
    <section className="owner-dash-section">
      <div className="owner-dash-section-head">
        <div className="owner-dash-section-titles">
          <h2>Daily activity</h2>
          <div className="owner-dash-section-sub">
            {totalCompleted > 0
              ? `${totalCompleted} task${totalCompleted === 1 ? "" : "s"} completed in window`
              : "Nothing completed yet in this window"}
          </div>
        </div>
        <div className="owner-dash-section-controls">
          <div className="owner-dash-range">
            {[7, 14, 30].map(n => (
              <button
                key={n}
                type="button"
                className={`owner-dash-range-pill ${days === n ? "is-active" : ""}`}
                onClick={() => setDays(n)}>
                {n}d
              </button>
            ))}
          </div>
          <button type="button" className="owner-dash-reload" onClick={reload} disabled={loading} title="Refresh">↻</button>
        </div>
      </div>

      {err && <div className="owner-dash-error" style={{ marginTop: 8 }}>{err}</div>}

      {!err && (loading || !data) && (
        <div className="wlh-empty">Loading activity…</div>
      )}

      {!err && data && sorted.length === 0 && (
        <div className="wlh-empty">
          {_filterQ ? "No people match the filter." : "No active users."}
        </div>
      )}

      {!err && data && sorted.length > 0 && (
        <div className="dap-list">
          {sorted.map(u => (
            <DailyActivityRow key={u.id} user={u} days={data.days}/>
          ))}
        </div>
      )}
      <style>{DAILY_ACTIVITY_CSS}</style>
    </section>
  );
}

function DailyActivityRow({ user, days }) {
  const completedSeries = days.map(iso => (user.by_day[iso] && user.by_day[iso].completed) || 0);
  const max = Math.max(1, ...completedSeries);
  const t = user.totals || {};
  const totalAny = (t.completed || 0) + (t.created || 0) + (t.commented || 0) + (t.status_changes || 0);
  const isQuiet = totalAny === 0;

  return (
    <div className={"dap-row" + (isQuiet ? " is-quiet" : "")}>
      <div className="dap-person">
        {user.avatar
          ? <img className="owner-dash-avatar" src={user.avatar} alt=""/>
          : <span className="owner-dash-avatar owner-dash-avatar-fallback"
                  style={{ background: user.color || "#b3b8c2" }}>
              {String(user.name || "?").trim().split(/\s+/).map(s => s[0]).slice(0,2).join("").toUpperCase()}
            </span>}
        <div className="dap-person-info">
          <div className="dap-person-name">{user.name}</div>
          <div className="dap-person-sub">{user.title || user.team || user.ws_role || ""}</div>
        </div>
      </div>

      <div className="dap-stats">
        <DapStat label="Done" value={t.completed || 0} tone="ok"/>
        <DapStat label="Created" value={t.created || 0}/>
        <DapStat label="Comments" value={t.commented || 0}/>
        <DapStat label="Edits" value={t.status_changes || 0} tip="Status, priority, due, assignment, etc."/>
      </div>

      <div className="dap-spark" title={`Tasks completed each day: ${completedSeries.join(", ")}`}>
        {completedSeries.map((n, i) => {
          const h = (n / max) * 100;
          return (
            <span
              key={days[i]}
              className={"dap-bar" + (n === 0 ? " is-empty" : "")}
              style={{ height: (n === 0 ? 6 : Math.max(8, h)) + "%" }}
              title={`${days[i]}: ${n} done`}/>
          );
        })}
      </div>
    </div>
  );
}

function DapStat({ label, value, tone, tip }) {
  return (
    <div className={"dap-stat" + (tone ? " is-" + tone : "")} title={tip}>
      <div className="dap-stat-value">{value}</div>
      <div className="dap-stat-label">{label}</div>
    </div>
  );
}

const DAILY_ACTIVITY_CSS = `
.dap-list {
  background: var(--bg-surface, #fff);
  border: 1px solid var(--border, #e6e9ef);
  border-radius: 10px;
  overflow: hidden;
}
.dap-row {
  display: grid;
  grid-template-columns: minmax(220px, 1.4fr) minmax(280px, 1.6fr) minmax(160px, 1fr);
  gap: 18px;
  padding: 12px 18px;
  border-bottom: 1px solid var(--border, #e6e9ef);
  align-items: center;
}
.dap-row:last-child { border-bottom: none; }
.dap-row.is-quiet { background: rgba(15,23,41,.015); }
.dap-row.is-quiet .dap-person-name { color: var(--ink-muted); }

.dap-person {
  display: flex; align-items: center; gap: 10px; min-width: 0;
}
.dap-person-info { min-width: 0; }
.dap-person-name { font-size: 13px; font-weight: 700; color: var(--ink); }
.dap-person-sub  { font-size: 11px; color: var(--ink-muted); }

.dap-stats {
  display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px;
}
.dap-stat {
  background: var(--surface-2, #f6f7fb);
  border-radius: 8px; padding: 6px 10px;
  display: flex; flex-direction: column; align-items: center;
  border: 1px solid transparent;
}
.dap-stat.is-ok { background: #ecf9f0; border-color: #b9e7c7; }
.dap-stat-value { font-size: 16px; font-weight: 700; color: var(--ink); line-height: 1.1; }
.dap-stat.is-ok .dap-stat-value { color: #1f8a48; }
.dap-stat-label { font-size: 10px; color: var(--ink-muted); text-transform: uppercase; letter-spacing: .06em; margin-top: 2px; }

.dap-spark {
  display: flex; align-items: flex-end; gap: 3px;
  height: 36px; padding: 4px 0;
}
.dap-bar {
  flex: 1; min-width: 6px;
  background: linear-gradient(180deg, #2f8aff, #0073ea);
  border-radius: 3px 3px 0 0;
  transition: height .2s;
}
.dap-bar.is-empty {
  background: var(--surface-2, #eef0f5);
}
@media (max-width: 980px) {
  .dap-row {
    grid-template-columns: 1fr;
    gap: 10px;
  }
  .dap-spark { width: 100%; }
}
`;

// ─────────────────────────────────────────────────────────────────────
// DelaysPanel — Owner-dashboard tab listing every project task that
// was completed AFTER its due date. Pulls from /api/dashboards/late-
// tasks (server computes per-project / per-owner / per-reason
// rollups + headline KPIs in one round trip). Three filter knobs:
//
//   - Window: 7 / 30 / 90 / 365 days
//   - Project: workspace projects the owner can see (or "all")
//   - Owner:   workspace users with at least one late task in window
//
// The table sorts most-late first; clicking a row routes the user
// to the task drawer via flowboard:nav, matching the same UX as the
// existing TaskListModal drill-downs in this dashboard.
// ─────────────────────────────────────────────────────────────────────
function DelaysPanel({ projects, people }) {
  const [windowDays, setWindowDays] = React.useState(30);
  const [projectId, setProjectId]   = React.useState("all");
  const [ownerId, setOwnerId]       = React.useState("all");
  const [data, setData]             = React.useState(null);
  const [loading, setLoading]       = React.useState(true);
  const [err, setErr]               = React.useState("");
  const [reasonFilter, setReasonFilter] = React.useState("all");

  const reload = React.useCallback(async () => {
    setLoading(true); setErr("");
    try {
      const r = await api.dashboards.lateTasks({
        days: windowDays,
        project_id: projectId === "all" ? null : projectId,
        user_id:    ownerId === "all"   ? null : ownerId,
      });
      setData(r);
    } catch (e) {
      const msg = (e && e.body && (e.body.message || e.body.error)) || (e && e.message) || "Could not load delays";
      setErr(msg);
    } finally {
      setLoading(false);
    }
  }, [windowDays, projectId, ownerId]);

  React.useEffect(() => { reload(); }, [reload]);

  function openTask(t) {
    if (!t || !t.id) return;
    try {
      window.dispatchEvent(new CustomEvent("flowboard:nav", {
        detail: { link: "/tasks/" + encodeURIComponent(t.id) },
      }));
    } catch {}
  }

  const k = (data && data.kpis) || {
    total: 0, on_time: 0, late: 0, on_time_rate: 0, avg_days_late: 0,
    max_days_late: 0, this_week: 0, this_month: 0,
  };
  const tasks = (data && data.tasks) || [];
  const byProject = (data && data.by_project) || [];
  const byOwner = (data && data.by_owner) || [];
  const byReason = (data && data.by_reason) || [];

  // Owner filter list — every workspace person, sorted by their late
  // count in the current rollup (most-late first). Falls back to a
  // plain name sort when we don't have a rollup yet.
  const ownerOptions = React.useMemo(() => {
    const lateByOwner = new Map(byOwner.map(o => [o.user_id, o.late]));
    return (people || []).slice().sort((a, b) => {
      const la = lateByOwner.get(a.id) || 0;
      const lb = lateByOwner.get(b.id) || 0;
      if (lb !== la) return lb - la;
      return (a.name || "").localeCompare(b.name || "");
    });
  }, [people, byOwner]);

  // Reason filter — only show reasons that appear in the current
  // rollup, plus an "all" sentinel.
  const reasonOptions = React.useMemo(() => {
    return byReason.map(r => ({ id: r.reason, label: r.label, count: r.count }));
  }, [byReason]);

  const filteredTasks = React.useMemo(() => {
    if (reasonFilter === "all") return tasks;
    return tasks.filter(t => (t.delay_reason || "unspecified") === reasonFilter);
  }, [tasks, reasonFilter]);

  function prettyDate(raw) {
    if (!raw || raw === "—") return "—";
    const head = String(raw).split(" @ ")[0].trim();
    const months = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];
    const iso = /^(\d{4})-(\d{2})-(\d{2})$/.exec(head);
    if (iso) return months[Number(iso[2]) - 1] + " " + Number(iso[3]);
    return head;
  }

  const maxReasonCount = Math.max(1, ...byReason.map(r => r.count));
  const maxProjectLate = Math.max(1, ...byProject.map(p => p.late));

  function exportCsv() {
    if (!filteredTasks.length) return;
    const escape = (v) => {
      if (v == null) return "";
      const s = String(v);
      if (/[",\n]/.test(s)) return `"${s.replace(/"/g, '""')}"`;
      return s;
    };
    const rows = [
      ["Task", "Project", "Owners", "Due", "Completed", "Days late", "Reason", "Note"],
      ...filteredTasks.map(t => {
        const ownerNames = (t.owners || []).map(uid => {
          const p = (people || []).find(x => x.id === uid);
          return p ? p.name : uid;
        }).join("; ");
        return [
          t.name, t.project_name, ownerNames,
          prettyDate(t.due_date), prettyDate(t.completed_at),
          t.days_late == null ? "" : String(t.days_late),
          t.delay_reason_label || (t.delay_reason || ""),
          t.delay_note || "",
        ];
      }),
    ];
    const csv = rows.map(r => r.map(escape).join(",")).join("\n");
    const blob = new Blob([csv], { type: "text/csv;charset=utf-8" });
    const url = URL.createObjectURL(blob);
    const a = document.createElement("a");
    a.href = url;
    a.download = `delays-${new Date().toISOString().slice(0,10)}.csv`;
    document.body.appendChild(a); a.click(); a.remove();
    URL.revokeObjectURL(url);
  }

  return (
    <>
      <DashFilterBar>
        <DashFilterField label="Window">
          <select className="owner-dash-filter-select"
                  value={windowDays}
                  onChange={(e) => setWindowDays(Number(e.target.value))}>
            <option value={7}>Last 7 days</option>
            <option value={30}>Last 30 days</option>
            <option value={90}>Last 90 days</option>
            <option value={365}>Last 365 days</option>
          </select>
        </DashFilterField>
        <DashFilterField label="Project">
          <select className="owner-dash-filter-select"
                  value={projectId}
                  onChange={(e) => setProjectId(e.target.value)}>
            <option value="all">All projects</option>
            {projects.map(p => <option key={p.id} value={p.id}>{p.name}</option>)}
          </select>
        </DashFilterField>
        <DashFilterField label="Owner">
          <select className="owner-dash-filter-select"
                  value={ownerId}
                  onChange={(e) => setOwnerId(e.target.value)}>
            <option value="all">All owners</option>
            {ownerOptions.map(p => <option key={p.id} value={p.id}>{p.name}</option>)}
          </select>
        </DashFilterField>
        {reasonOptions.length > 1 && (
          <DashFilterField label="Reason">
            <select className="owner-dash-filter-select"
                    value={reasonFilter}
                    onChange={(e) => setReasonFilter(e.target.value)}>
              <option value="all">All reasons</option>
              {reasonOptions.map(r => (
                <option key={r.id} value={r.id}>{r.label} ({r.count})</option>
              ))}
            </select>
          </DashFilterField>
        )}
        {(projectId !== "all" || ownerId !== "all" || reasonFilter !== "all" || windowDays !== 30) && (
          <button type="button" className="owner-dash-filter-clear"
                  onClick={() => { setProjectId("all"); setOwnerId("all"); setReasonFilter("all"); setWindowDays(30); }}
                  title="Reset filters">
            Clear
          </button>
        )}
        <div style={{ marginLeft: "auto", display: "flex", gap: 8 }}>
          <button type="button" className="owner-dash-btn"
                  onClick={reload}
                  disabled={loading}
                  title="Refresh delays">
            <span className={`owner-dash-btn-icon ${loading ? "is-spinning" : ""}`}>↻</span>
            <span>Refresh</span>
          </button>
          <button type="button" className="owner-dash-btn"
                  onClick={exportCsv}
                  disabled={!filteredTasks.length}
                  title="Export the current list to CSV">
            Export CSV
          </button>
        </div>
      </DashFilterBar>

      {err ? (
        <div className="owner-dash-err">
          {err} <button className="btn" style={{ marginLeft: 12 }} onClick={reload}>Retry</button>
        </div>
      ) : (
        <>
          <section className="owner-dash-status">
            <div className="owner-dash-status-cards">
              <DashCard label="Late tasks" value={k.late}
                        sub={`${k.total} done in ${windowDays}d`} tone="danger"/>
              <DashCard label="On-time rate" value={(k.on_time_rate || 0) + "%"}
                        sub={`${k.on_time} on time`} tone={k.on_time_rate >= 80 ? "ok" : "warn"}/>
              <DashCard label="Avg days late" value={k.avg_days_late || 0}
                        sub={k.late ? "across late tasks" : "—"} tone="warn"/>
              <DashCard label="Max days late" value={k.max_days_late || 0}
                        sub={k.late ? "worst case" : "—"} tone="danger"/>
              <DashCard label="Late · 7d" value={k.this_week || 0}
                        sub={`of ${k.this_month || 0} · 30d`}/>
            </div>
          </section>

          <section className="owner-dash-section">
            <div className="owner-dash-section-head">
              <div className="owner-dash-section-titles">
                <h2>Late tasks</h2>
                <div className="owner-dash-section-sub">
                  {filteredTasks.length}{filteredTasks.length !== tasks.length ? ` of ${tasks.length}` : ""}{filteredTasks.length === 1 ? " task" : " tasks"} completed past their due date · click a row to open it
                </div>
              </div>
            </div>
            <div className="owner-dash-table-wrap">
              <table className="owner-dash-table delays-table">
                <thead>
                  <tr>
                    <th>Task</th>
                    <th>Project</th>
                    <th>Owners</th>
                    <th>Due</th>
                    <th>Completed</th>
                    <th className="num">Days late</th>
                    <th>Reason</th>
                  </tr>
                </thead>
                <tbody>
                  {loading && !data && (
                    <tr><td colSpan={7} className="owner-dash-empty">Loading…</td></tr>
                  )}
                  {!loading && filteredTasks.length === 0 && (
                    <tr><td colSpan={7} className="owner-dash-empty">
                      No late tasks in this window — nice!
                    </td></tr>
                  )}
                  {filteredTasks.map(t => {
                    const dn = t.days_late;
                    const tone = dn == null ? "" : dn >= 7 ? "is-danger" : dn >= 3 ? "is-warn" : "";
                    return (
                      <tr key={t.id} onClick={() => openTask(t)}
                          style={{ cursor: "pointer" }}
                          title={`Open ${t.name}`}>
                        <td>
                          <div style={{ fontWeight: 600, color: "var(--ink-strong)" }}>{t.name}</div>
                          {t.delay_note && (
                            <div style={{ fontSize: 11, color: "var(--ink-muted)", marginTop: 3, lineHeight: 1.35 }}>
                              {t.delay_note}
                            </div>
                          )}
                        </td>
                        <td>
                          <span style={{
                            display: "inline-flex", alignItems: "center", gap: 6,
                            padding: "2px 8px", borderRadius: 999,
                            background: t.project_color ? `${t.project_color}1f` : "rgba(0,0,0,.05)",
                            color: t.project_color || "var(--ink-body)",
                            fontWeight: 600, fontSize: 11,
                          }}>
                            <span style={{ width: 7, height: 7, borderRadius: 999, background: t.project_color || "#a3a8b6" }}/>
                            {t.project_name}
                          </span>
                        </td>
                        <td>
                          <div style={{ display: "flex", gap: -4 }}>
                            {(t.owners || []).slice(0, 3).map(uid => {
                              const p = (people || []).find(x => x.id === uid);
                              if (!p) return null;
                              return p.avatar
                                ? <img key={uid} src={p.avatar} alt={p.name}
                                       title={p.name}
                                       className="owner-dash-avatar"
                                       style={{ width: 22, height: 22, marginLeft: -4, border: "2px solid white" }}/>
                                : <span key={uid}
                                        title={p.name}
                                        className="owner-dash-avatar owner-dash-avatar-fallback"
                                        style={{ background: p.color || "#b3b8c2", width: 22, height: 22, marginLeft: -4, border: "2px solid white" }}>
                                    {String(p.name || "?").trim().split(/\s+/).map(s => s[0]).slice(0,2).join("").toUpperCase()}
                                  </span>;
                            })}
                            {(t.owners || []).length === 0 && (
                              <span style={{ color: "var(--ink-muted)", fontSize: 12 }}>—</span>
                            )}
                            {(t.owners || []).length > 3 && (
                              <span style={{ fontSize: 11, color: "var(--ink-muted)", marginLeft: 4, alignSelf: "center" }}>
                                +{t.owners.length - 3}
                              </span>
                            )}
                          </div>
                        </td>
                        <td className="owner-dash-muted">{prettyDate(t.due_date)}</td>
                        <td className="owner-dash-muted">{prettyDate(t.completed_at)}</td>
                        <td className={"num " + tone}>
                          {dn == null ? "—" : `${dn}d`}
                        </td>
                        <td>
                          {t.delay_reason_label
                            ? <span className="delays-reason-chip">{t.delay_reason_label}</span>
                            : <span style={{ color: "var(--ink-faint)" }}>—</span>}
                        </td>
                      </tr>
                    );
                  })}
                </tbody>
              </table>
            </div>
          </section>

          <div className="delays-rollup-grid">
            <section className="owner-dash-section">
              <div className="owner-dash-section-head">
                <div className="owner-dash-section-titles">
                  <h2>By project</h2>
                  <div className="owner-dash-section-sub">Which projects ship late most often</div>
                </div>
              </div>
              {byProject.length === 0 ? (
                <div className="owner-dash-empty" style={{ padding: "20px 16px" }}>No late tasks yet.</div>
              ) : (
                <div className="delays-bars">
                  {byProject.slice(0, 12).map(p => (
                    <div key={p.project_id} className="delays-bar-row">
                      <div className="delays-bar-label" title={p.project_name}>
                        <span className="owner-dash-dot" style={{ background: p.project_color || "#a3a8b6" }}/>
                        {p.project_name}
                      </div>
                      <div className="delays-bar-track">
                        <div className="delays-bar-fill"
                             style={{ width: ((p.late / maxProjectLate) * 100) + "%", background: p.project_color || "#e2445c" }}/>
                      </div>
                      <div className="delays-bar-meta">
                        <strong>{p.late}</strong>
                        {p.avg_days_late != null && <span> · {p.avg_days_late}d avg</span>}
                      </div>
                    </div>
                  ))}
                </div>
              )}
            </section>

            <section className="owner-dash-section">
              <div className="owner-dash-section-head">
                <div className="owner-dash-section-titles">
                  <h2>By reason</h2>
                  <div className="owner-dash-section-sub">Top delay reasons across the window</div>
                </div>
              </div>
              {byReason.length === 0 ? (
                <div className="owner-dash-empty" style={{ padding: "20px 16px" }}>No reasons logged.</div>
              ) : (
                <div className="delays-bars">
                  {byReason.slice(0, 10).map(r => (
                    <div key={r.reason} className={"delays-bar-row" + (reasonFilter === r.reason ? " is-active" : "")}
                         onClick={() => setReasonFilter(reasonFilter === r.reason ? "all" : r.reason)}
                         style={{ cursor: "pointer" }}
                         title={reasonFilter === r.reason ? "Clear reason filter" : `Filter the table to "${r.label}"`}>
                      <div className="delays-bar-label">{r.label}</div>
                      <div className="delays-bar-track">
                        <div className="delays-bar-fill"
                             style={{ width: ((r.count / maxReasonCount) * 100) + "%", background: r.reason === "unspecified" ? "#9ba2ad" : "#0073ea" }}/>
                      </div>
                      <div className="delays-bar-meta"><strong>{r.count}</strong></div>
                    </div>
                  ))}
                </div>
              )}
            </section>
          </div>

          <section className="owner-dash-section">
            <div className="owner-dash-section-head">
              <div className="owner-dash-section-titles">
                <h2>By owner</h2>
                <div className="owner-dash-section-sub">Late count per assignee · click a row to filter the table</div>
              </div>
            </div>
            <div className="owner-dash-table-wrap">
              <table className="owner-dash-table">
                <thead>
                  <tr>
                    <th>Person</th>
                    <th className="num">Late tasks</th>
                    <th className="num">Avg days late</th>
                  </tr>
                </thead>
                <tbody>
                  {byOwner.length === 0 && (
                    <tr><td colSpan={3} className="owner-dash-empty">No owners have late tasks.</td></tr>
                  )}
                  {byOwner.slice(0, 20).map(o => (
                    <tr key={o.user_id}
                        style={{ cursor: "pointer", background: ownerId === o.user_id ? "rgba(0,115,234,.05)" : undefined }}
                        onClick={() => setOwnerId(ownerId === o.user_id ? "all" : o.user_id)}
                        title={ownerId === o.user_id ? "Clear owner filter" : `Filter to ${o.name}`}>
                      <td>
                        <div className="owner-dash-user">
                          {o.avatar
                            ? <img className="owner-dash-avatar" src={o.avatar} alt=""/>
                            : <span className="owner-dash-avatar owner-dash-avatar-fallback"
                                    style={{ background: o.color || "#b3b8c2" }}>
                                {String(o.name || "?").trim().split(/\s+/).map(s => s[0]).slice(0,2).join("").toUpperCase()}
                              </span>}
                          <div>
                            <div className="owner-dash-user-name">{o.name}</div>
                          </div>
                        </div>
                      </td>
                      <td className="num">{o.late}</td>
                      <td className="num owner-dash-muted">{o.avg_days_late == null ? "—" : `${o.avg_days_late}d`}</td>
                    </tr>
                  ))}
                </tbody>
              </table>
            </div>
          </section>
        </>
      )}
      <style>{DELAYS_PANEL_CSS}</style>
    </>
  );
}

// ─────────────────────────────────────────────────────────────────────
// ConflictsPanel — cross-coordinator priority view. One round-trip
// to /api/priority/conflicts gives us three rollups (people /
// coordinators / bumps) we render as a three-section grid. Click a
// person → opens their task drawer in app for follow-up.
// ─────────────────────────────────────────────────────────────────────
function ConflictsPanel({ people = [] }) {
  const [data, setData]       = React.useState(null);
  const [loading, setLoading] = React.useState(true);
  const [err, setErr]         = React.useState("");

  const reload = React.useCallback(async () => {
    setLoading(true); setErr("");
    try {
      const r = await api.priority.conflicts();
      setData(r);
    } catch (e) {
      const msg = (e && e.body && (e.body.message || e.body.error)) || (e && e.message) || "Could not load conflicts";
      setErr(msg);
    } finally {
      setLoading(false);
    }
  }, []);
  React.useEffect(() => { reload(); }, [reload]);

  const ppl   = (data && data.people) || [];
  const coord = (data && data.coordinators) || [];
  const bumps = (data && data.bumps) || [];

  // Top-line KPIs.
  const conflicted   = ppl.filter(p => p.pc_count >= 2).length;
  const totalCrit    = ppl.reduce((s, p) => s + p.critical, 0);
  const totalHigh    = ppl.reduce((s, p) => s + p.high, 0);
  const openBumps    = bumps.reduce((s, b) => s + b.open + b.negotiating, 0);
  const recentBumps  = bumps.reduce((s, b) => s + b.total, 0);

  function avatarOf(person) {
    if (person.avatar) {
      return <img className="owner-dash-avatar" src={person.avatar} alt=""/>;
    }
    return (
      <span className="owner-dash-avatar owner-dash-avatar-fallback"
            style={{ background: person.color || "#b3b8c2" }}>
        {String(person.name || "?").trim().split(/\s+/).map(s => s[0]).slice(0,2).join("").toUpperCase()}
      </span>
    );
  }

  return (
    <>
      <DashFilterBar>
        <div style={{ marginLeft: "auto", display: "flex", gap: 8 }}>
          <button type="button" className="owner-dash-btn"
                  onClick={reload}
                  disabled={loading}
                  title="Refresh">
            <span className={`owner-dash-btn-icon ${loading ? "is-spinning" : ""}`}>↻</span>
            <span>Refresh</span>
          </button>
        </div>
      </DashFilterBar>

      {err ? (
        <div className="owner-dash-err">
          {err} <button className="btn" style={{ marginLeft: 12 }} onClick={reload}>Retry</button>
        </div>
      ) : (
        <>
          <section className="owner-dash-status">
            <div className="owner-dash-status-cards">
              <DashCard label="Conflicted devs" value={conflicted}
                        sub={`${ppl.length} carrying high/critical`} tone={conflicted > 2 ? "danger" : conflicted > 0 ? "warn" : "ok"}/>
              <DashCard label="Open critical" value={totalCrit}
                        sub="across the team" tone="danger"/>
              <DashCard label="Open high" value={totalHigh}
                        sub="across the team" tone="warn"/>
              <DashCard label="Bump requests" value={openBumps}
                        sub={`${recentBumps} in last 30d`}
                        tone={openBumps > 0 ? "warn" : "ok"}/>
            </div>
          </section>

          <section className="owner-dash-section">
            <div className="owner-dash-section-head">
              <div className="owner-dash-section-titles">
                <h2>People with stacked priority work</h2>
                <div className="owner-dash-section-sub">
                  Developers carrying high/critical from multiple coordinators · most-conflicted first
                </div>
              </div>
            </div>
            <div className="owner-dash-table-wrap">
              <table className="owner-dash-table">
                <thead>
                  <tr>
                    <th>Person</th>
                    <th className="num">Critical</th>
                    <th className="num">High</th>
                    <th className="num">From PCs</th>
                    <th className="num">Total</th>
                  </tr>
                </thead>
                <tbody>
                  {loading && !data && (
                    <tr><td colSpan={5} className="owner-dash-empty">Loading…</td></tr>
                  )}
                  {!loading && ppl.length === 0 && (
                    <tr><td colSpan={5} className="owner-dash-empty">No high or critical work currently assigned.</td></tr>
                  )}
                  {ppl.map(p => (
                    <tr key={p.id}>
                      <td>
                        <div className="owner-dash-user">
                          {avatarOf(p)}
                          <div className="owner-dash-user-name">{p.name}</div>
                        </div>
                      </td>
                      <td className={"num" + (p.critical > 0 ? " is-danger" : "")}>{p.critical}</td>
                      <td className={"num" + (p.high > 0 ? " is-warn" : "")}>{p.high}</td>
                      <td className={"num" + (p.pc_count >= 2 ? " is-warn" : "")}>{p.pc_count}</td>
                      <td className="num owner-dash-muted">{p.total}</td>
                    </tr>
                  ))}
                </tbody>
              </table>
            </div>
          </section>

          <div className="delays-rollup-grid">
            <section className="owner-dash-section">
              <div className="owner-dash-section-head">
                <div className="owner-dash-section-titles">
                  <h2>By coordinator</h2>
                  <div className="owner-dash-section-sub">Per-PC open high/critical usage</div>
                </div>
              </div>
              {coord.length === 0 ? (
                <div className="owner-dash-empty" style={{ padding: "20px 16px" }}>No usage to show.</div>
              ) : (
                <div className="delays-bars">
                  {coord.slice(0, 12).map(c => {
                    const max = Math.max(1, ...coord.map(x => x.critical_open * 3 + x.high_open));
                    const weight = c.critical_open * 3 + c.high_open;
                    return (
                      <div key={c.id} className="delays-bar-row">
                        <div className="delays-bar-label" title={c.name}>
                          {avatarOf(c)}
                          <span style={{ marginLeft: 6 }}>{c.name}</span>
                        </div>
                        <div className="delays-bar-track">
                          <div className="delays-bar-fill"
                               style={{ width: ((weight / max) * 100) + "%", background: c.critical_open > 0 ? "#c93636" : "#f59e0b" }}/>
                        </div>
                        <div className="delays-bar-meta">
                          <strong>{c.critical_open}</strong> crit · <strong>{c.high_open}</strong> high
                          <div style={{ fontSize: 11, color: "var(--ink-muted)" }}>on {c.people_touched} people</div>
                        </div>
                      </div>
                    );
                  })}
                </div>
              )}
            </section>

            <section className="owner-dash-section">
              <div className="owner-dash-section-head">
                <div className="owner-dash-section-titles">
                  <h2>Bump requests (30d)</h2>
                  <div className="owner-dash-section-sub">Open / accepted / declined / negotiating per PC</div>
                </div>
              </div>
              {bumps.length === 0 ? (
                <div className="owner-dash-empty" style={{ padding: "20px 16px" }}>No bump requests filed recently.</div>
              ) : (
                <div className="owner-dash-table-wrap">
                  <table className="owner-dash-table">
                    <thead>
                      <tr>
                        <th>Coordinator</th>
                        <th className="num">Open</th>
                        <th className="num">Accepted</th>
                        <th className="num">Declined</th>
                        <th className="num">Negotiating</th>
                      </tr>
                    </thead>
                    <tbody>
                      {bumps.slice(0, 12).map(b => (
                        <tr key={b.id}>
                          <td>
                            <div className="owner-dash-user">
                              {avatarOf(b)}
                              <div className="owner-dash-user-name">{b.name}</div>
                            </div>
                          </td>
                          <td className={"num" + (b.open > 0 ? " is-warn" : "")}>{b.open}</td>
                          <td className="num">{b.accepted}</td>
                          <td className="num owner-dash-muted">{b.declined}</td>
                          <td className="num">{b.negotiating}</td>
                        </tr>
                      ))}
                    </tbody>
                  </table>
                </div>
              )}
            </section>
          </div>
        </>
      )}
      <style>{DELAYS_PANEL_CSS}</style>
    </>
  );
}

const DELAYS_PANEL_CSS = `
.delays-table td { vertical-align: middle; }
.delays-table .num.is-warn   { color: #a16207; font-weight: 700; }
.delays-table .num.is-danger { color: #c0223a; font-weight: 800; }
.delays-reason-chip {
  display: inline-block;
  padding: 2px 8px;
  border-radius: 999px;
  background: rgba(0,115,234,.08);
  color: #0044a3;
  font-size: 11px;
  font-weight: 600;
}
.delays-rollup-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 12px;
}
.delays-bars {
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 8px 12px 14px;
}
.delays-bar-row {
  display: grid;
  grid-template-columns: minmax(120px, 220px) 1fr 78px;
  gap: 10px;
  align-items: center;
  font-size: 12.5px;
  padding: 4px 4px;
  border-radius: 6px;
  transition: background .15s;
}
.delays-bar-row.is-active { background: rgba(0,115,234,.07); }
.delays-bar-row:hover    { background: rgba(0,0,0,.025); }
.delays-bar-label {
  display: flex; align-items: center; gap: 6px;
  color: var(--ink-strong);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.delays-bar-track {
  position: relative;
  height: 8px;
  background: var(--surface-2, #eef0f5);
  border-radius: 999px;
  overflow: hidden;
}
.delays-bar-fill {
  position: absolute; inset: 0 auto 0 0;
  border-radius: 999px;
  transition: width .25s;
}
.delays-bar-meta {
  font-size: 12px;
  color: var(--ink-body);
  text-align: right;
  white-space: nowrap;
}
.delays-bar-meta strong { color: var(--ink-strong); }
@media (max-width: 980px) {
  .delays-rollup-grid { grid-template-columns: 1fr; }
}
`;

// ─────────────────────────────────────────────────────────────────────
// CollapsibleSection — disclosure block with a sticky header. Used
// for the People-details table so the page isn't dominated by a
// 30-row table when the heatmap above already answers most owner
// questions. Open/closed state persists in localStorage so power
// users don't have to reopen on every refresh.
// ─────────────────────────────────────────────────────────────────────
function CollapsibleSection({ title, sub, defaultOpen, storageKey, children }) {
  const [open, setOpen] = React.useState(() => {
    if (storageKey && typeof localStorage !== "undefined") {
      const raw = localStorage.getItem(storageKey);
      if (raw === "1") return true;
      if (raw === "0") return false;
    }
    return !!defaultOpen;
  });
  function toggle() {
    setOpen(prev => {
      const next = !prev;
      try { if (storageKey) localStorage.setItem(storageKey, next ? "1" : "0"); } catch {}
      return next;
    });
  }
  return (
    <section className="owner-dash-section">
      <div className="owner-dash-section-head owner-dash-section-head-clickable" onClick={toggle} role="button" tabIndex={0}
           onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); toggle(); } }}>
        <div className="owner-dash-section-titles">
          <h2 className="owner-dash-section-h2-row">
            <span className={`owner-dash-collapsible-caret ${open ? "is-open" : ""}`}>▸</span>
            {title}
          </h2>
          {sub && <div className="owner-dash-section-sub">{sub}</div>}
        </div>
      </div>
      {open && <div className="owner-dash-collapsible-body">{children}</div>}
      <style>{COLLAPSIBLE_CSS}</style>
    </section>
  );
}

// ─────────────────────────────────────────────────────────────────────
// OwnerAdminMenu — "..." popover that bundles the admin / maintenance
// actions (download backup, email backup, force reload all). They
// were previously crowding the header; tucking them behind a single
// menu keeps the title row scannable while keeping every action one
// click away. Includes the backup-status pill so owners can see at a
// glance whether the daily cron is running.
// ─────────────────────────────────────────────────────────────────────
function OwnerAdminMenu({ backup, backupBusy, onDownload, onEmail, onForceReload }) {
  const [open, setOpen] = React.useState(false);
  const ref = React.useRef(null);
  React.useEffect(() => {
    if (!open) return;
    function onDoc(e) { if (ref.current && !ref.current.contains(e.target)) setOpen(false); }
    function onKey(e) { if (e.key === "Escape") setOpen(false); }
    document.addEventListener("mousedown", onDoc);
    document.addEventListener("keydown", onKey);
    return () => {
      document.removeEventListener("mousedown", onDoc);
      document.removeEventListener("keydown", onKey);
    };
  }, [open]);

  const lastWhen = backup && backup.last_backup_at;
  const lastFmt = lastWhen ? new Date(lastWhen).toLocaleString() : null;

  return (
    <div className="owner-admin-menu" ref={ref}>
      <button type="button" className="owner-dash-btn" onClick={() => setOpen(o => !o)} title="Admin actions">
        <span className="owner-dash-btn-icon">⋯</span>
        <span>Admin</span>
      </button>
      {open && (
        <div className="owner-admin-pop" onMouseDown={(e) => e.stopPropagation()}>
          <div className="owner-admin-pop-section">
            <div className="owner-admin-pop-title">Backup</div>
            {lastFmt ? (
              <div className="owner-admin-pop-status">
                <span className="owner-admin-pop-dot" style={{
                  background: backup.last_backup_ok === false ? "#dc2626" : "#22c55e",
                }}/>
                Last backup: <b>{lastFmt}</b>
                <span className="owner-admin-pop-tag">{backup.last_backup_kind || "—"}</span>
              </div>
            ) : (
              <div className="owner-admin-pop-status owner-admin-pop-muted">
                No backups yet — take a snapshot below
              </div>
            )}
            {backup && backup.smtp_configured === false && (
              <div className="owner-admin-pop-warn">
                SMTP not configured · daily auto-backup disabled
              </div>
            )}
            {backup && backup.smtp_configured === true && (
              <div className="owner-admin-pop-muted">
                Daily auto-backup armed for 02:00 IST
              </div>
            )}
            <button className="owner-admin-pop-row" onClick={onDownload} disabled={backupBusy === "download"}>
              <span>⬇</span>
              <div>
                <div className="owner-admin-pop-row-name">{backupBusy === "download" ? "Building backup…" : "Download backup"}</div>
                <div className="owner-admin-pop-row-sub">JSON of every table — re-import to recover</div>
              </div>
            </button>
            <button className="owner-admin-pop-row" onClick={onEmail} disabled={backupBusy === "email"}
              style={backup && backup.smtp_configured === false ? { opacity: .55 } : null}>
              <span>✉</span>
              <div>
                <div className="owner-admin-pop-row-name">{backupBusy === "email" ? "Sending…" : "Email backup"}</div>
                <div className="owner-admin-pop-row-sub">Send the JSON dump as an attachment</div>
              </div>
            </button>
          </div>
          <div className="owner-admin-pop-section">
            <div className="owner-admin-pop-title">Workspace</div>
            <button className="owner-admin-pop-row" onClick={() => { setOpen(false); onForceReload(); }}>
              <span>⟳</span>
              <div>
                <div className="owner-admin-pop-row-name">Force reload all tabs</div>
                <div className="owner-admin-pop-row-sub">Push the latest version to every connected user</div>
              </div>
            </button>
          </div>
        </div>
      )}
      <style>{ADMIN_MENU_CSS}</style>
    </div>
  );
}

const COLLAPSIBLE_CSS = `
.owner-dash-collapsible {
  display: flex; align-items: center; gap: 10px;
  width: 100%; padding: 10px 12px;
  background: var(--bg-surface, #fff);
  border: 1px solid var(--border, #e6e9ef);
  border-radius: 10px;
  font: inherit; cursor: pointer;
  text-align: left;
}
.owner-dash-collapsible:hover { background: var(--surface-2, #f6f7fb); }
.owner-dash-collapsible-caret {
  display: inline-block; transition: transform .15s ease;
  font-size: 11px; color: var(--ink-muted, #9aa0a6);
}
.owner-dash-collapsible.is-open .owner-dash-collapsible-caret { transform: rotate(90deg); }
.owner-dash-collapsible-title { font-weight: 700; color: var(--ink); font-size: 13px; }
.owner-dash-collapsible-sub { font-size: 12px; color: var(--ink-muted); margin-left: auto; }
.owner-dash-collapsible-body { margin-top: 10px; }
`;

const ADMIN_MENU_CSS = `
.owner-admin-menu { position: relative; display: inline-block; }
.owner-admin-pop {
  position: absolute; top: calc(100% + 6px); right: 0;
  width: 320px;
  background: #fff;
  border: 1px solid var(--border, #e6e9ef);
  border-radius: 12px;
  box-shadow: 0 18px 42px rgba(15,23,41,.18), 0 4px 10px rgba(15,23,41,.06);
  z-index: 80;
  padding: 6px;
  display: flex; flex-direction: column; gap: 4px;
}
.owner-admin-pop-section { display: flex; flex-direction: column; gap: 4px; padding: 6px; }
.owner-admin-pop-section + .owner-admin-pop-section { border-top: 1px solid var(--border); padding-top: 8px; }
.owner-admin-pop-title {
  font-size: 10px; font-weight: 800; color: var(--ink-muted);
  text-transform: uppercase; letter-spacing: .08em;
  padding: 0 4px 2px;
}
.owner-admin-pop-status {
  display: flex; align-items: center; gap: 6px;
  font-size: 12px; color: var(--ink); padding: 4px 4px;
}
.owner-admin-pop-dot {
  width: 7px; height: 7px; border-radius: 999px; flex-shrink: 0;
}
.owner-admin-pop-tag {
  margin-left: auto;
  background: rgba(0,0,0,.05); border-radius: 999px;
  padding: 1px 8px; font-size: 11px;
}
.owner-admin-pop-muted { font-size: 11px; color: var(--ink-muted); padding: 0 4px 4px; }
.owner-admin-pop-warn {
  font-size: 11px; color: #8a1024;
  background: rgba(226,68,92,.10);
  padding: 4px 8px; border-radius: 6px;
}
.owner-admin-pop-row {
  display: flex; align-items: center; gap: 12px;
  padding: 8px 10px; border-radius: 8px;
  border: none; background: transparent;
  font: inherit; color: var(--ink); cursor: pointer; text-align: left;
  width: 100%;
}
.owner-admin-pop-row > span:first-child {
  font-size: 18px; line-height: 1;
  width: 28px; height: 28px;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--surface-2, #f6f7fb);
  border-radius: 8px; flex-shrink: 0;
}
.owner-admin-pop-row:hover { background: var(--surface-2, #f6f7fb); }
.owner-admin-pop-row:disabled { opacity: .55; cursor: progress; }
.owner-admin-pop-row-name { font-size: 13px; font-weight: 600; }
.owner-admin-pop-row-sub { font-size: 11px; color: var(--ink-muted); }

/* Header layout overrides — tighten the title row now that the
   admin actions are tucked away into the menu. */
.owner-dash-header.owner-dash-header-tight {
  align-items: center;
}
.owner-dash-head-right {
  display: inline-flex; gap: 8px; align-items: center;
}

/* Status band — KPI cards on the left, activity pills inline on
   the right so the at-a-glance information is one row instead of
   two stacked bands. Wraps cleanly on narrow viewports. */
.owner-dash-status {
  display: flex; gap: 14px; align-items: stretch;
  margin: 14px 0 18px;
  flex-wrap: wrap;
}
.owner-dash-status-cards {
  flex: 1 1 520px;
  display: grid;
  grid-template-columns: repeat(5, minmax(0, 1fr));
  gap: 10px;
}
.owner-dash-status-pills {
  display: flex; flex-direction: column; gap: 6px;
  background: var(--bg-surface, #fff);
  border: 1px solid var(--border, #e6e9ef);
  border-radius: 12px;
  padding: 10px 14px;
  min-width: 280px;
}
.owner-dash-status-pills-label {
  font-size: 10px; font-weight: 700; color: var(--ink-muted);
  text-transform: uppercase; letter-spacing: .08em;
}
.owner-dash-status-pills-row {
  display: inline-flex; gap: 6px; flex-wrap: wrap;
}
@media (max-width: 980px) {
  .owner-dash-status-cards { grid-template-columns: repeat(2, 1fr); }
  .owner-dash-status-pills { flex: 1 1 100%; }
}
`;

// ─────────────────────────────────────────────────────────────────────
// Unified section / button layout — gives every section the same
// header structure (title block left, controls right), the same
// button styles (Refresh / Admin / range pill / reload), and the
// same gap between sections. Keeps the page from looking like four
// different products bolted together.
// ─────────────────────────────────────────────────────────────────────
const OWNER_DASH_LAYOUT_CSS = `
/* Page-level vertical rhythm. Every section is .owner-dash-section
   already; we set a single bottom margin and let the wrappers stack.
   The .owner-dash-header sits above the section flow. */
.owner-dash-header {
  display: flex; align-items: flex-end; justify-content: space-between;
  gap: 16px; flex-wrap: wrap;
  margin: 0 0 4px 0;
  padding-bottom: 4px;
}
.owner-dash-header h1 {
  font-size: 24px; font-weight: 800; letter-spacing: -.012em;
  color: var(--ink-strong, #0f1729);
  margin: 0;
}

/* Shared section header — used by every section on the page. */
.owner-dash-section { margin-bottom: 22px; }
.owner-dash-section-head {
  display: flex; align-items: flex-end; justify-content: space-between;
  gap: 14px; flex-wrap: wrap;
  margin: 0 0 10px 0;
}
.owner-dash-section-head-clickable { cursor: pointer; user-select: none; }
.owner-dash-section-head-clickable:hover .owner-dash-section-titles h2 {
  color: var(--ink-strong, #0f1729);
}

/* Title block — h2 + sub-label sit baseline-aligned. */
.owner-dash-section-titles {
  display: flex; flex-direction: column; gap: 2px;
  min-width: 0;
}
.owner-dash-section-titles h2 {
  font-size: 14px; font-weight: 700; letter-spacing: 0;
  color: var(--ink-strong, #0f1729);
  text-transform: none;            /* override stories.css uppercase */
  margin: 0;
}
.owner-dash-section-h2-row {
  display: inline-flex; align-items: center; gap: 8px;
}
.owner-dash-section-sub {
  font-size: 12px; color: var(--ink-muted, #676879);
  line-height: 1.3;
}
.owner-dash-section-controls {
  display: inline-flex; align-items: center; gap: 8px;
  flex-wrap: wrap;
}

/* Caret on the collapsible section h2. */
.owner-dash-collapsible-caret {
  display: inline-block;
  font-size: 11px; color: var(--ink-muted, #9aa0a6);
  transition: transform .15s ease;
}
.owner-dash-collapsible-caret.is-open { transform: rotate(90deg); }
.owner-dash-collapsible-body { /* table sits inside */ }

/* Standard 32px-tall pill button — used for header Refresh + Admin. */
.owner-dash-btn {
  display: inline-flex; align-items: center; gap: 6px;
  height: 32px; padding: 0 12px;
  background: #fff;
  border: 1px solid var(--border, #e6e9ef);
  border-radius: 8px;
  font: inherit; font-size: 13px; font-weight: 600;
  color: var(--ink, #0f1729);
  cursor: pointer;
  transition: border-color .12s, background .12s;
}
.owner-dash-btn:hover:not(:disabled) {
  border-color: var(--brand, #0073ea);
  color: var(--brand, #0073ea);
}
.owner-dash-btn:disabled { opacity: .55; cursor: progress; }
.owner-dash-btn-icon {
  font-size: 14px; line-height: 1; display: inline-block;
  transition: transform .4s ease;
}
.owner-dash-btn-icon.is-spinning { animation: ownerDashSpin 1s linear infinite; }
@keyframes ownerDashSpin { to { transform: rotate(360deg); } }

/* Range pill group — shared between heatmap + daily activity. */
.owner-dash-range {
  display: inline-flex; gap: 2px;
  background: var(--surface-2, #f1f4f9);
  border: 1px solid var(--border, #e6e9ef);
  border-radius: 999px; padding: 3px;
  height: 32px; box-sizing: border-box;
  align-items: center;
}
.owner-dash-range-pill {
  border: none; background: transparent;
  padding: 0 12px; height: 24px;
  font: inherit; font-size: 12px; font-weight: 600;
  color: var(--ink-muted); border-radius: 999px; cursor: pointer;
  display: inline-flex; align-items: center;
}
.owner-dash-range-pill:hover { color: var(--ink, #0f1729); }
.owner-dash-range-pill.is-active {
  background: #fff; color: var(--brand, #0073ea);
  box-shadow: 0 1px 3px rgba(15,23,41,.08);
}

/* ── Section tabs ──────────────────────────────────────────
   Underline-style tab strip between the page header and the
   active section. Each tab carries an optional subtitle (count
   or hint) so the user sees at a glance what's behind a tab
   without clicking into it. The active tab gets a brand-coloured
   2px underline and ink-strong label; idle tabs are quietly
   muted with a subtle hover lift. */
.owner-dash-tabs {
  display: flex; gap: 4px;
  margin: 4px 0 18px;
  border-bottom: 1px solid var(--border, #e6e9ef);
  position: relative;
  overflow-x: auto;
  scrollbar-width: none;
}
.owner-dash-tabs::-webkit-scrollbar { display: none; }
.owner-dash-tab {
  appearance: none; background: transparent; border: none;
  font: inherit; cursor: pointer;
  padding: 12px 18px 14px; margin: 0;
  display: inline-flex; flex-direction: column; gap: 2px;
  align-items: flex-start; white-space: nowrap;
  color: var(--ink-muted, #676879);
  position: relative;
  border-radius: 8px 8px 0 0;
  transition: color .14s ease, background .14s ease;
}
.owner-dash-tab:hover {
  color: var(--ink-strong, #0f1729);
  background: rgba(15,23,41,.025);
}
.owner-dash-tab-label {
  font-size: 14px; font-weight: 700; letter-spacing: -.005em;
  line-height: 1.2;
}
.owner-dash-tab-sub {
  font-size: 11px; font-weight: 500;
  color: var(--ink-muted, #9aa0a6);
  letter-spacing: 0;
}
.owner-dash-tab.is-active {
  color: var(--ink-strong, #0f1729);
}
.owner-dash-tab.is-active .owner-dash-tab-sub {
  color: var(--brand, #a25ddc);
}
.owner-dash-tab.is-active::after {
  content: "";
  position: absolute; left: 6px; right: 6px; bottom: -1px;
  height: 2px; border-radius: 2px 2px 0 0;
  background: var(--brand, #a25ddc);
  box-shadow: 0 0 8px rgba(162,93,220,.45);
  animation: ownerDashTabUnderline .22s cubic-bezier(.2,.9,.25,1.15);
}
@keyframes ownerDashTabUnderline {
  from { transform: scaleX(.4); opacity: 0; }
  to   { transform: scaleX(1);  opacity: 1; }
}
.owner-dash-tab:focus-visible {
  outline: 2px solid var(--brand, #a25ddc);
  outline-offset: -3px;
}

/* On narrow viewports the strip becomes scrollable horizontally
   and tabs collapse to a tighter pad so 4 still fit on a phone. */
@media (max-width: 760px) {
  .owner-dash-tabs { gap: 0; }
  .owner-dash-tab { padding: 10px 12px 12px; }
  .owner-dash-tab-label { font-size: 13px; }
  .owner-dash-tab-sub { font-size: 10.5px; }
}

/* ── Per-tab filter bar ─────────────────────────────────────
   Sits under the tab strip on every tab. Each field has a
   tiny uppercase label + an input/select. Clear button on the
   right when filters are active. Wraps gracefully on narrow
   widths. */
.owner-dash-filter-bar {
  display: flex; gap: 12px; flex-wrap: wrap; align-items: flex-end;
  padding: 10px 12px;
  margin: 0 0 14px;
  background: var(--surface-2, #f7f8fb);
  border: 1px solid var(--border, #e6e9ef);
  border-radius: 10px;
}
.owner-dash-filter-field {
  display: flex; flex-direction: column; gap: 4px;
  min-width: 140px;
}
.owner-dash-filter-label {
  font-size: 10.5px; font-weight: 700;
  text-transform: uppercase; letter-spacing: .06em;
  color: var(--ink-muted, #676879);
  line-height: 1;
}
.owner-dash-filter-input,
.owner-dash-filter-select {
  appearance: none; -webkit-appearance: none;
  background: #fff;
  border: 1px solid var(--border, #e6e9ef);
  border-radius: 8px;
  height: 32px; padding: 0 12px;
  font: inherit; font-size: 13px; font-weight: 500;
  color: var(--ink, #0f1729);
  min-width: 180px;
  transition: border-color .12s, box-shadow .12s;
}
.owner-dash-filter-input:focus,
.owner-dash-filter-select:focus {
  outline: none;
  border-color: var(--brand, #a25ddc);
  box-shadow: 0 0 0 3px rgba(162,93,220,.15);
}
.owner-dash-filter-select {
  background-image:
    url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23676879' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><polyline points='6 9 12 15 18 9'/></svg>");
  background-repeat: no-repeat;
  background-position: right 10px center;
  background-size: 14px;
  padding-right: 30px;
}
/* Hide search input's native clear button — we use the
   field-level Clear button on the bar instead so the look is
   uniform across browsers. */
.owner-dash-filter-input::-webkit-search-cancel-button { display: none; }
.owner-dash-filter-input::-webkit-search-decoration  { display: none; }

.owner-dash-filter-toggle {
  display: inline-flex; align-items: center; gap: 6px;
  height: 32px; padding: 0 10px;
  background: #fff;
  border: 1px solid var(--border, #e6e9ef);
  border-radius: 8px;
  font: inherit; font-size: 12.5px; font-weight: 600;
  color: var(--ink, #0f1729); cursor: pointer; user-select: none;
  align-self: flex-end;
  transition: border-color .12s, color .12s;
}
.owner-dash-filter-toggle:hover {
  border-color: var(--brand, #a25ddc);
  color: var(--brand, #a25ddc);
}
.owner-dash-filter-toggle input[type="checkbox"] {
  margin: 0; accent-color: var(--brand, #a25ddc);
}

.owner-dash-filter-clear {
  margin-left: auto;
  height: 32px; padding: 0 14px;
  background: transparent;
  border: 1px dashed rgba(162,93,220,.4);
  border-radius: 8px;
  font: inherit; font-size: 12.5px; font-weight: 600;
  color: var(--brand, #a25ddc); cursor: pointer;
  align-self: flex-end;
  transition: border-color .12s, background .12s;
}
.owner-dash-filter-clear:hover {
  background: rgba(162,93,220,.08);
  border-color: var(--brand, #a25ddc);
  border-style: solid;
}

@media (max-width: 760px) {
  .owner-dash-filter-bar { gap: 8px; padding: 8px 10px; }
  .owner-dash-filter-field { min-width: 120px; flex: 1 1 calc(50% - 8px); }
  .owner-dash-filter-input,
  .owner-dash-filter-select { min-width: 0; width: 100%; }
  .owner-dash-filter-clear { margin-left: 0; flex: 1 1 100%; }
}

/* Reload icon button — same height as the pill button. */
.owner-dash-reload {
  border: 1px solid var(--border, #e6e9ef);
  background: #fff; cursor: pointer;
  width: 32px; height: 32px; border-radius: 8px;
  font-size: 15px; line-height: 1; color: var(--ink-muted);
  display: inline-flex; align-items: center; justify-content: center;
  transition: border-color .12s, color .12s;
}
.owner-dash-reload:disabled { opacity: .5; cursor: progress; }
.owner-dash-reload:hover:not(:disabled) {
  color: var(--brand, #0073ea); border-color: var(--brand, #0073ea);
}

/* Heatmap legend — keep the swatches but match the new control
   row's vertical alignment. */
.wlh-legend {
  display: inline-flex; align-items: center; gap: 4px;
  margin-right: 6px;
}
.wlh-legend-label {
  font-size: 11px; color: var(--ink-muted); margin: 0 2px;
}
.wlh-legend-swatch {
  width: 12px; height: 12px; border-radius: 3px;
  border: 1px solid rgba(0,0,0,.06);
}

@media (max-width: 760px) {
  .owner-dash-header { gap: 10px; }
  .owner-dash-header h1 { font-size: 20px; }
  .owner-dash-section-head { gap: 8px; }
  .owner-dash-section-controls { gap: 6px; }
  .wlh-legend { display: none; }   /* re-stated for safety */
}
`;

Object.assign(window, {
  OwnerDashboardView, TaskListModal, WorkloadHeatmap, DailyActivityPanel,
  CollapsibleSection, OwnerAdminMenu,
});
