<!-- SPEC: PJ2026-010404 项目管理 draft-2026-06-25-p0-project-management-mdtodo. -->
<!-- SPEC: PJ2026-010405 云端控制台. -->
<!-- Implementation reference: draft-2026-07-13-p0-cloud-console. -->
<!-- Responsibility: Project-management root page consuming only public project-management API DTOs. -->

<script setup lang="ts">
import { Grid2X2, List, RefreshCw, Rows3, Rows4, X } from "lucide-vue-next";
import { computed, onMounted, ref, watch } from "vue";
import { RouterLink, useRoute, useRouter } from "vue-router";
import { projectManagementAPI, type MdtodoTaskRecord, type ProjectNavigationResponse, type ProjectRecord, type ProjectSource, type ProjectWorkbenchLinkRecord } from "@/api";
import AsyncBoundary from "@/components/common/AsyncBoundary.vue";
import EmptyState from "@/components/common/EmptyState.vue";
import ResourceCollection from "@/components/common/ResourceCollection.vue";
import ViewModeSwitch from "@/components/console/ViewModeSwitch.vue";
import ProjectResourceCard from "@/components/projects/ProjectResourceCard.vue";
import BoundedWorkspace from "@/components/layout/BoundedWorkspace.vue";
import PageCommandBar from "@/components/layout/PageCommandBar.vue";
import StatusStrip from "@/components/layout/StatusStrip.vue";
import { useConsoleViewState } from "@/composables/useConsoleViewState";
import type { ApiError, ErrorDiagnostic } from "@/types";

const loading = ref(false);
const route = useRoute();
const router = useRouter();
const viewState = useConsoleViewState({ filter: "all" });
const error = ref<string | null>(null);
const apiError = ref<ApiError | null>(null);
const diagnostic = ref<ErrorDiagnostic | null>(null);
const navigation = ref<ProjectNavigationResponse | null>(null);
const projects = ref<ProjectRecord[]>([]);
const sources = ref<ProjectSource[]>([]);
const tasks = ref<MdtodoTaskRecord[]>([]);
const links = ref<ProjectWorkbenchLinkRecord[]>([]);
const selectedProjectTasks = ref<MdtodoTaskRecord[]>([]);
const selectedProjectTaskTotal = ref(0);
const selectedProjectCompletedTotal = ref(0);
const selectedProjectInProgressTotal = ref(0);
const selectedProjectTaskCountsExact = ref(false);
const selectedProjectTaskLoading = ref(false);
const selectedProjectTaskError = ref<string | null>(null);
let selectedProjectTaskRequest = 0;

const sourceById = computed(() => new Map(sources.value.map((source) => [source.sourceId, source])));
const recentTasks = computed(() => tasks.value.slice(0, 6));
const activeSourceCount = computed(() => sources.value.filter((source) => source.status !== "blocked" && source.status !== "error").length);
const projection = computed(() => navigation.value?.projection ?? null);
const query = computed({
  get: () => viewState.q.value,
  set: (value: string) => void viewState.update({ q: value.trim() })
});
const statusFilter = computed({
  get: () => viewState.filter.value || "all",
  set: (value: string) => void viewState.update({ filter: value === "all" ? "" : value })
});
const viewMode = computed<"cards" | "list">({
  get: () => viewState.view.value,
  set: (value) => void viewState.update({ view: value === "cards" ? undefined : value })
});
const density = computed<"compact" | "comfortable">({
  get: () => viewState.density.value,
  set: (value) => void viewState.update({ density: value === "comfortable" ? undefined : value })
});
const filteredProjects = computed(() => {
  const needle = query.value.trim().toLowerCase();
  return projects.value.filter((project) => {
    if (statusFilter.value !== "all" && (project.status || "unknown") !== statusFilter.value) return false;
    if (!needle) return true;
    return [project.projectId, project.name, project.title, project.sourceOfTruth, ...(project.sourceIds ?? [])].filter(Boolean).join(" ").toLowerCase().includes(needle);
  });
});
const selectedProject = computed(() => {
  const projectId = typeof route.params.projectId === "string" ? route.params.projectId : "";
  return projects.value.find((project) => project.projectId === projectId) ?? null;
});
const selectedSources = computed(() => selectedProject.value ? projectSources(selectedProject.value) : []);
const selectedTasks = computed(() => selectedProject.value ? selectedProjectTasks.value : []);
const selectedLinks = computed(() => selectedProject.value ? links.value.filter((link) => link.projectId === selectedProject.value?.projectId).slice(0, 8) : []);
const projectStatusItems = computed(() => [
  { id: "projects", label: "Projects", value: projects.value.length, detail: navigation.value?.serviceId || "hwlab-project-management" },
  { id: "sources", label: "Sources", value: `${activeSourceCount.value} / ${sources.value.length}`, detail: String(navigation.value?.navigation?.capabilities?.sourceOfTruth || "markdown-files") },
  { id: "tasks", label: "Tasks", value: displayCount(projection.value?.taskCount ?? tasks.value.length), detail: `documents ${displayCount(projection.value?.documentCount)}` },
  { id: "links", label: "Workbench Links", value: links.value.length, detail: navigation.value?.navigation?.capabilities?.workbenchLinks ? "link projection ready" : "link projection pending" }
]);
const asyncState = computed(() => {
  if (loading.value) return projects.value.length ? "refreshing" : "initial-loading";
  if (error.value) return projects.value.length ? "partial" : "error";
  return projects.value.length ? "ready" : "empty";
});

onMounted(() => void loadProjects());
watch(() => selectedProject.value?.projectId ?? null, (projectId) => { void loadSelectedProjectTasks(projectId); });

async function loadProjects(): Promise<void> {
  loading.value = true;
  error.value = null;
  apiError.value = null;
  diagnostic.value = null;
  try {
    const [navigationResponse, projectResponse, sourceResponse, taskResponse, linkResponse] = await Promise.all([
      projectManagementAPI.navigation(),
      projectManagementAPI.projects(),
      projectManagementAPI.sources(),
      projectManagementAPI.tasks({ limit: 6 }),
      projectManagementAPI.workbenchLinks()
    ]);
    for (const response of [navigationResponse, projectResponse, sourceResponse, taskResponse, linkResponse]) {
      if (!response.ok) throw response;
    }
    navigation.value = navigationResponse.data;
    projects.value = projectResponse.data?.projects ?? [];
    sources.value = sourceResponse.data?.sources ?? [];
    tasks.value = taskResponse.data?.tasks ?? [];
    links.value = linkResponse.data?.links ?? [];
  } catch (err) {
    const context = projectApiError(err);
    error.value = context.error;
    apiError.value = context.apiError;
    diagnostic.value = context.diagnostic;
  } finally {
    loading.value = false;
  }
}

async function loadSelectedProjectTasks(projectId: string | null): Promise<void> {
  const requestId = ++selectedProjectTaskRequest;
  selectedProjectTaskError.value = null;
  if (!projectId) {
    selectedProjectTasks.value = [];
    selectedProjectTaskTotal.value = 0;
    selectedProjectCompletedTotal.value = 0;
    selectedProjectInProgressTotal.value = 0;
    selectedProjectTaskCountsExact.value = false;
    return;
  }
  selectedProjectTasks.value = [];
  selectedProjectTaskTotal.value = 0;
  selectedProjectCompletedTotal.value = 0;
  selectedProjectInProgressTotal.value = 0;
  selectedProjectTaskCountsExact.value = false;
  selectedProjectTaskLoading.value = true;
  const [response, completedResponse, inProgressResponse] = await Promise.all([
    projectManagementAPI.tasks({ projectId, limit: 20, offset: 0 }),
    projectManagementAPI.tasks({ projectId, status: "completed", limit: 1, offset: 0 }),
    projectManagementAPI.tasks({ projectId, status: "in_progress", limit: 1, offset: 0 })
  ]);
  if (requestId !== selectedProjectTaskRequest) return;
  selectedProjectTaskLoading.value = false;
  if (!response.ok) {
    selectedProjectTaskError.value = response.error ?? `HTTP ${response.status}`;
    return;
  }
  selectedProjectTasks.value = response.data?.tasks ?? [];
  selectedProjectTaskTotal.value = Number(response.data?.page?.total ?? selectedProjectTasks.value.length);
  if (completedResponse.ok && inProgressResponse.ok) {
    selectedProjectCompletedTotal.value = Number(completedResponse.data?.page?.total ?? completedResponse.data?.tasks?.length ?? 0);
    selectedProjectInProgressTotal.value = Number(inProgressResponse.data?.page?.total ?? inProgressResponse.data?.tasks?.length ?? 0);
    selectedProjectTaskCountsExact.value = true;
  } else {
    selectedProjectCompletedTotal.value = selectedProjectTasks.value.filter((task) => task.status === "completed").length;
    selectedProjectInProgressTotal.value = selectedProjectTasks.value.filter((task) => task.status === "in_progress").length;
    selectedProjectTaskError.value = "任务状态总计读取失败；completed 计数仅代表当前 20 条窗口";
  }
}

function projectTitle(project: ProjectRecord): string {
  return project.name || project.title || project.projectId;
}

function projectKey(project: ProjectRecord): string {
  return project.projectId;
}

function projectSubtitle(project: ProjectRecord): string {
  return project.projectId;
}

function projectStatus(project: ProjectRecord): string {
  return project.status || "unknown";
}

function openProject(project: ProjectRecord): void {
  void router.push({ name: "ProjectDetail", params: { projectId: project.projectId }, query: route.query });
}

function projectSources(project: ProjectRecord): ProjectSource[] {
  return (project.sourceIds ?? []).map((sourceId) => sourceById.value.get(sourceId)).filter((source): source is ProjectSource => Boolean(source));
}

function statusTone(value?: string | null): string {
  if (value === "active" || value === "ok" || value === "ready") return "ok";
  if (value === "blocked" || value === "error" || value === "failed") return "error";
  return "pending";
}

function displayCount(value?: number | null): string {
  return Number.isFinite(value) ? String(value) : "0";
}

function taskStatusLabel(value?: string | null): string {
  if (value === "completed") return "完成";
  if (value === "open") return "待办";
  if (value === "in_progress") return "进行中";
  return value || "未知";
}

function taskRoute(task: MdtodoTaskRecord): Record<string, unknown> | null {
  if (!task.sourceId || !task.fileRef || !task.taskId) return null;
  return { name: "ProjectMdtodoTask", params: { sourceId: task.sourceId, fileRef: task.fileRef, taskId: task.taskId } };
}

function linkTaskRoute(link: ProjectWorkbenchLinkRecord): Record<string, unknown> | null {
  const context = link.link?.launchContext;
  if (!context?.sourceId || !context.fileRef || !context.taskId) return null;
  return { name: "ProjectMdtodoTask", params: { sourceId: context.sourceId, fileRef: context.fileRef, taskId: context.taskId } };
}

function workbenchRoute(link: ProjectWorkbenchLinkRecord): string | null {
  return link.link?.workbenchUrl || (link.sessionId ? `/workbench/sessions/${encodeURIComponent(link.sessionId)}` : null);
}

function projectApiError(err: unknown): { error: string; apiError: ApiError | null; diagnostic: ErrorDiagnostic | null } {
  if (err && typeof err === "object" && "error" in err) {
    const result = err as { error?: string | null; apiError?: ApiError | null; diagnostic?: ErrorDiagnostic | null; status?: number };
    return { error: result.error || `HTTP ${result.status ?? "unknown"}`, apiError: result.apiError ?? null, diagnostic: result.diagnostic ?? null };
  }
  return { error: err instanceof Error ? err.message : String(err || "项目管理加载失败"), apiError: null, diagnostic: null };
}

</script>

<template>
  <section class="route-stack projects-page" data-testid="project-management-root">
    <PageCommandBar eyebrow="Project Management" title="项目" description="查看项目、任务来源、最近任务和关联工作台摘要。" />
    <AsyncBoundary :state="asyncState" :has-content="Boolean(projects.length)" label="加载项目管理投影" empty-title="暂无项目" empty-description="Project Management API 当前未返回项目。" @retry="loadProjects">
      <template #error><section class="data-panel projects-error" data-testid="project-management-error">
        <EmptyState title="项目管理加载失败" :description="error || '请求未完成'" :api-error="apiError" :diagnostic="diagnostic" action-label="重试" @action="loadProjects" />
      </section></template>
      <section class="project-command-bar" aria-label="项目显示控制">
        <label class="project-search">
          <span>搜索项目</span>
          <input v-model="query" type="search" placeholder="名称、projectId 或 source" />
        </label>
        <label>
          <span>状态</span>
          <select v-model="statusFilter">
            <option value="all">全部</option>
            <option value="active">active</option>
            <option value="ready">ready</option>
            <option value="blocked">blocked</option>
            <option value="error">error</option>
          </select>
        </label>
        <ViewModeSwitch :model-value="viewMode" :options="[{ value: 'cards', label: '卡片', icon: Grid2X2 }, { value: 'list', label: '列表', icon: List }]" label="项目显示模式" @update:model-value="viewMode = $event as 'cards' | 'list'" />
        <ViewModeSwitch :model-value="density" :options="[{ value: 'comfortable', label: '舒适密度', icon: Rows4 }, { value: 'compact', label: '紧凑密度', icon: Rows3 }]" label="项目显示密度" @update:model-value="density = $event as 'compact' | 'comfortable'" />
        <button class="project-refresh icon-button" type="button" :disabled="loading" :title="loading ? '刷新中' : '刷新项目'" aria-label="刷新项目" @click="loadProjects"><RefreshCw :size="16" aria-hidden="true" /></button>
      </section>

      <StatusStrip :items="projectStatusItems" label="项目管理摘要" data-testid="project-management-summary" />

      <BoundedWorkspace class="project-workspace" :inspector-open="Boolean(selectedProject)" inspector-width="390px" label="项目集合与详情">
        <section class="data-panel project-list-panel" data-testid="project-list">
          <header class="project-panel-header">
            <div><h2>项目集合</h2><p>{{ filteredProjects.length }} / {{ projects.length }}，由 typed Project Management API 返回。</p></div>
            <RouterLink class="btn btn-primary btn-sm" to="/projects/mdtodo" data-testid="open-mdtodo-page">打开 MDTODO</RouterLink>
          </header>
          <EmptyState v-if="!filteredProjects.length" title="没有匹配项目" description="调整搜索或状态筛选；服务返回的项目事实没有被浏览器改写。" />
          <ResourceCollection v-else :items="filteredProjects" :item-key="projectKey" :item-title="projectTitle" :item-subtitle="projectSubtitle" :item-status="projectStatus" :view="viewMode" :density="density" :selected-id="selectedProject?.projectId" :show-controls="false" aria-label="项目集合" @select="openProject">
            <template #card="{ item: project }"><ProjectResourceCard :project="project" :sources="projectSources(project)" :selected="selectedProject?.projectId === project.projectId" :to="{ name: 'ProjectDetail', params: { projectId: project.projectId }, query: route.query }" /></template>
            <template #list="{ item: project }"><ProjectResourceCard :project="project" :sources="projectSources(project)" :selected="selectedProject?.projectId === project.projectId" :to="{ name: 'ProjectDetail', params: { projectId: project.projectId }, query: route.query }" /></template>
          </ResourceCollection>
        </section>

        <template #inspector>
        <section v-if="selectedProject" class="project-inspector" aria-label="项目详情">
          <header class="project-inspector-header">
            <div><small>PROJECT DETAIL</small><h2>{{ projectTitle(selectedProject) }}</h2><code>{{ selectedProject.projectId }}</code></div>
            <RouterLink :to="{ name: 'Projects', query: route.query }" title="关闭项目详情" aria-label="关闭项目详情"><X :size="16" aria-hidden="true" /></RouterLink>
          </header>
          <dl class="project-facts">
            <div><dt>状态</dt><dd><span class="pm-status" :data-tone="statusTone(selectedProject.status)">{{ selectedProject.status || 'unknown' }}</span></dd></div>
            <div><dt>Source of truth</dt><dd>{{ selectedProject.sourceOfTruth || navigation?.navigation?.capabilities?.sourceOfTruth || '-' }}</dd></div>
            <div><dt>Source</dt><dd>{{ selectedSources.length }}</dd></div>
            <div><dt>关联会话</dt><dd>{{ selectedLinks.length }}</dd></div>
            <div><dt>任务活动</dt><dd>{{ selectedProjectTaskTotal }} total / {{ selectedProjectInProgressTotal }} in progress</dd></div>
            <div><dt>Completed</dt><dd><span class="pm-status" data-tone="ok">{{ selectedProjectCompletedTotal }} completed</span><small>{{ selectedProjectTaskCountsExact ? '服务端总计' : '当前窗口' }}</small></dd></div>
          </dl>
          <section><h3>Source readiness</h3><ul class="source-list">
            <li v-for="source in selectedSources" :key="source.sourceId">
              <span class="pm-status" :data-tone="statusTone(source.status)">{{ source.status || 'unknown' }}</span>
              <strong>{{ source.displayName || source.sourceId }}</strong>
              <small>{{ source.kind || source.sourceKind || 'mdtodo' }} · {{ source.sourceId }}</small>
            </li>
          </ul><p v-if="!selectedSources.length" class="project-empty-line">当前项目没有可见 source。</p></section>
          <section><h3>任务活动（有界 20 条）</h3><p v-if="selectedProjectTaskError" class="project-empty-line">{{ selectedProjectTaskError }}；当前项目任务摘要未加载。</p><ul class="task-preview-list" :aria-busy="selectedProjectTaskLoading">
            <li v-for="task in selectedTasks" :key="task.taskRef"><span class="task-status" :data-status="task.status || 'unknown'">{{ taskStatusLabel(task.status) }}</span><strong>{{ task.title || task.taskId }}</strong><RouterLink v-if="taskRoute(task)" :to="taskRoute(task)!">{{ task.taskId }} · 打开 MDTODO</RouterLink><code v-else>{{ task.taskRef }}</code></li>
          </ul><p v-if="!selectedTasks.length && !selectedProjectTaskLoading" class="project-empty-line">当前项目没有任务活动。</p></section>
          <RouterLink class="project-primary-link" to="/projects/mdtodo">进入 MDTODO 工作区</RouterLink>
        </section>
        </template>
      </BoundedWorkspace>

      <section class="project-root-grid lower">
        <section class="data-panel project-task-panel" data-testid="project-recent-tasks">
          <header class="project-panel-header"><div><h2>最近任务</h2><p>只展示任务投影摘要和 opaque taskRef 片段。</p></div></header>
          <ul v-if="recentTasks.length" class="task-preview-list">
            <li v-for="task in recentTasks" :key="task.taskRef">
              <span class="task-status" :data-status="task.status || 'unknown'">{{ taskStatusLabel(task.status) }}</span>
              <strong>{{ task.title || task.taskId || 'Untitled task' }}</strong>
              <RouterLink v-if="taskRoute(task)" :to="taskRoute(task)!">{{ task.taskId }} · 打开 MDTODO</RouterLink>
              <code v-else>{{ task.taskRef }}</code>
            </li>
          </ul>
          <EmptyState v-else title="暂无任务投影" description="MDTODO source 当前没有可展示任务，或尚未发现 Markdown TODO 文件。" />
        </section>
        <section class="data-panel project-link-panel" data-testid="project-workbench-links">
          <header class="project-panel-header"><div><h2>Workbench Link</h2><p>展示任务通过公共 Workbench Launch API 产生的 link 投影。</p></div></header>
          <ul v-if="links.length" class="link-list">
            <li v-for="link in links" :key="link.linkId">
              <strong>{{ link.link?.launchContext?.title || link.sessionId || link.linkId }}</strong>
              <small>{{ link.taskRef || link.projectId || '-' }}</small>
              <div class="link-actions">
                <RouterLink v-if="workbenchRoute(link)" :to="workbenchRoute(link)!">打开 Workbench</RouterLink>
                <RouterLink v-if="linkTaskRoute(link)" :to="linkTaskRoute(link)!">查看 MDTODO 任务</RouterLink>
              </div>
            </li>
          </ul>
          <EmptyState v-else title="暂无 Workbench 关联" description="从 MDTODO 任务启动 Workbench 后，关联 session 会在这里出现。" />
        </section>
      </section>
    </AsyncBoundary>
  </section>
</template>

<style scoped>
.projects-page { min-width: 0; }
.project-command-bar { display: grid; grid-template-columns: minmax(220px, 1fr) 150px auto auto auto; gap: 8px; align-items: end; padding: 10px; border: 1px solid #c9d2dc; border-radius: 8px; background: linear-gradient(135deg, #f7f9fb, #edf2f4); box-shadow: inset 3px 0 #14b8a6; }
.project-command-bar label { display: grid; gap: 4px; color: #52606d; font-size: 10px; font-weight: 800; letter-spacing: 0; text-transform: uppercase; }
.project-command-bar input,
.project-command-bar select { width: 100%; min-width: 0; height: 34px; border: 1px solid #b9c4ce; border-radius: 6px; background: #fff; padding: 0 10px; color: #17212b; }
.project-refresh { height: 34px; border: 0; background: #fff; padding: 0 11px; color: #31404c; font-weight: 750; cursor: pointer; }
.project-refresh { border: 1px solid #aebac5; border-radius: 6px; }
.project-refresh { background: #16323a; color: #ecfeff; }
.project-summary-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; }
.project-summary-grid .metric-card strong { color: #0f172a; font-size: 28px; line-height: 1.15; }
.project-summary-grid .metric-card small { min-width: 0; overflow: hidden; color: #64748b; text-overflow: ellipsis; white-space: nowrap; }
.project-workspace { display: grid; grid-template-columns: minmax(0, 1fr); min-height: 0; gap: 12px; }
.project-workspace[data-inspector-open="true"] { grid-template-columns: minmax(0, 1fr) minmax(300px, 380px); }
.project-root-grid.lower { grid-template-columns: minmax(0, 1fr) minmax(320px, 0.72fr); }
.project-list-panel,
.project-source-panel,
.project-task-panel,
.project-link-panel,
.projects-error { display: grid; min-width: 0; align-content: start; gap: 12px; padding: 14px; }
.project-panel-header { display: flex; min-width: 0; align-items: flex-start; justify-content: space-between; gap: 12px; }
.project-panel-header h2 { margin: 0; color: #0f172a; font-size: 16px; line-height: 1.2; }
.project-panel-header p { margin: 4px 0 0; color: #64748b; font-size: 12px; line-height: 1.45; }
.project-collection { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 10px; min-width: 0; }
.project-collection[data-density="compact"] { gap: 6px; }
.project-collection[data-view="list"] { grid-template-columns: 1fr; }
.project-source-chips { display: flex; flex-wrap: wrap; gap: 6px; }
.project-inspector { position: sticky; top: 0; align-self: start; display: grid; max-height: calc(100dvh - 120px); min-height: 0; gap: 14px; overflow: auto; padding: 14px; border-color: #a7c8c7; box-shadow: inset 3px 0 #14b8a6; }
.project-inspector-header { display: flex; gap: 12px; justify-content: space-between; border-bottom: 1px solid #d8e1e7; padding-bottom: 12px; }
.project-inspector-header small { color: #0f766e; font: 800 10px/1 ui-monospace, monospace; letter-spacing: 0; }
.project-inspector-header h2 { margin: 5px 0; color: #14232b; font-size: 20px; }
.project-inspector-header code { color: #5d6d76; font-size: 11px; overflow-wrap: anywhere; }
.project-inspector-header > a { display: grid; width: 30px; height: 30px; place-items: center; border: 1px solid #c5cfd7; border-radius: 6px; color: #33434d; text-decoration: none; }
.project-inspector section { display: grid; gap: 7px; }
.project-inspector section h3 { margin: 0; color: #51606a; font-size: 11px; letter-spacing: 0; text-transform: uppercase; }
.project-facts { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin: 0; }
.project-facts div { min-width: 0; border: 1px solid #dbe3e8; border-radius: 6px; padding: 8px; background: #f7f9fa; }
.project-facts dt { color: #6d7b84; font-size: 10px; }
.project-facts dd { margin: 4px 0 0; color: #1b2c34; font-size: 12px; overflow-wrap: anywhere; }
.project-primary-link { display: flex; min-height: 36px; align-items: center; justify-content: center; border-radius: 6px; background: #16323a; color: #ecfeff; font-size: 12px; font-weight: 800; text-decoration: none; }
.project-empty-line { margin: 0; border: 1px dashed #ccd7dd; border-radius: 6px; padding: 10px; color: #697781; font-size: 12px; }
.source-chip,
.pm-status,
.task-status { display: inline-flex; width: fit-content; max-width: 100%; align-items: center; border-radius: 999px; border: 1px solid #cbd5e1; padding: 4px 8px; font-size: 11px; font-weight: 750; line-height: 1; }
.source-chip { background: white; color: #334155; }
.pm-status[data-tone="ok"] { border-color: #86efac; background: #dcfce7; color: #166534; }
.pm-status[data-tone="error"] { border-color: #fecaca; background: #fef2f2; color: #991b1b; }
.pm-status[data-tone="pending"] { border-color: #fde68a; background: #fffbeb; color: #92400e; }
.source-list,
.task-preview-list,
.link-list { display: grid; gap: 8px; margin: 0; padding: 0; list-style: none; }
.source-list li,
.task-preview-list li,
.link-list li { display: grid; min-width: 0; gap: 5px; border: 1px solid #e2e8f0; border-radius: 8px; background: #ffffff; padding: 10px; }
.source-list strong,
.task-preview-list strong,
.link-list strong { min-width: 0; overflow: hidden; color: #0f172a; text-overflow: ellipsis; white-space: nowrap; }
.source-list small,
.link-list small { min-width: 0; overflow: hidden; color: #64748b; font-size: 12px; text-overflow: ellipsis; white-space: nowrap; }
.link-actions { display: flex; flex-wrap: wrap; gap: 10px; }
.link-actions a,
.task-preview-list a { color: #0f766e; font-size: 12px; font-weight: 750; text-decoration-thickness: 1px; text-underline-offset: 2px; }
.task-preview-list code { min-width: 0; overflow: hidden; color: #475569; font-size: 11px; text-overflow: ellipsis; white-space: nowrap; }
.task-status[data-status="completed"] { border-color: #86efac; background: #dcfce7; color: #166534; }
.task-status[data-status="completed"] { border-color: #86efac; background: #dcfce7; color: #166534; }
.task-status { border-color: #bae6fd; background: #f0f9ff; color: #075985; }
@media (max-width: 1180px) {
  .project-summary-grid,
  .project-workspace[data-inspector-open="true"],
  .project-root-grid.lower { grid-template-columns: 1fr; }
  .project-inspector { position: static; max-height: none; }
}
@media (max-width: 760px) {
  .project-command-bar { grid-template-columns: 1fr 1fr; }
  .project-search { grid-column: 1 / -1; }
  .project-summary-grid { grid-template-columns: 1fr 1fr; }
  .project-collection { grid-template-columns: 1fr; }
  .project-root-grid.lower { grid-template-columns: 1fr; }
}
</style>
