<script setup lang="ts">
import { computed, nextTick, onMounted, ref, watch } from "vue";
import { useRoute, useRouter } from "vue-router";
import { ChevronDown, ChevronRight, RefreshCw, ZoomIn, ZoomOut } from "lucide-vue-next";
import { tasktreeAPI, type TaskTreeGroup, type TaskTreeGroupOverview, type TaskTreeTask, type TaskTreeTimeline } from "@/api/tasktree";
import AsyncBoundary from "@/components/common/AsyncBoundary.vue";
import BaseDialog from "@/components/common/BaseDialog.vue";
import TaskTreeDetailPanel from "@/components/tasktree/TaskTreeDetailPanel.vue";
import MessageMarkdown from "@/components/workbench/MessageMarkdown.vue";
import { displayTimeConfig } from "@/config/runtime";

const route = useRoute();
const router = useRouter();
const groups = ref<TaskTreeGroup[]>([]);
const overview = ref<TaskTreeGroupOverview[]>([]);
const timeline = ref<TaskTreeTimeline | null>(null);
const selectedTask = ref<TaskTreeTask | null>(null);
const detailMode = ref<"dialog" | "docked">(route.query.detail === "docked" ? "docked" : "dialog");
const collapsedTaskIds = ref(new Set<string>());
const timelineHeaderElement = ref<HTMLElement | null>(null);
const taskLabelsElement = ref<HTMLElement | null>(null);
const timelineBodyElement = ref<HTMLElement | null>(null);
const timelineScrollbarWidth = ref(0);
const labelChildScrollers = new Map<string, HTMLElement>();
const timelineChildScrollers = new Map<string, HTMLElement>();
const initializedChildGroups = new Set<string>();
const childScrollEdges = ref(new Map<string, { atTop: boolean; atBottom: boolean }>());
const timelineScaleIndex = ref(1);
const taskColumnWidth = ref(typeof window !== "undefined" ? Math.round(window.innerWidth * (window.innerWidth <= 720 ? 0.55 : 0.21)) : 400);
const loading = ref(false);
const error = ref<string | null>(null);
let positionedView = "";
const groupId = computed(() => String(route.params.groupId || ""));
const displayTime = displayTimeConfig();
const displayPartsFormatter = new Intl.DateTimeFormat(displayTime.locale, {
  timeZone: displayTime.timeZone,
  year: "numeric",
  month: "2-digit",
  day: "2-digit",
  hour: "2-digit",
  minute: "2-digit",
  second: "2-digit",
  hourCycle: "h23"
});
const taskRowHeight = 46;
const requestedTaskId = computed(() => String(route.query.task || ""));
const requestedDetailMode = computed<"dialog" | "docked">(() => route.query.detail === "docked" ? "docked" : "dialog");
const isGlobalView = computed(() => !groupId.value);
const datedItems = computed(() => isGlobalView.value
  ? overview.value.filter((item) => item.startAt || item.dueAt)
  : timeline.value?.tasks.filter((task) => task.startAt || task.dueAt) ?? []);
const childrenByParent = computed(() => {
  const children = new Map<string, TaskTreeTask[]>();
  for (const task of timeline.value?.tasks ?? []) {
    if (!task.parentId) continue;
    children.set(task.parentId, [...(children.get(task.parentId) ?? []), task]);
  }
  return children;
});
const tasksById = computed(() => new Map((timeline.value?.tasks ?? []).map((task) => [task.id, task])));
const rootTasks = computed(() => (timeline.value?.tasks ?? []).filter((task) => !task.parentId));
const timelineScales = [
  { label: "周", stepMs: 2 * 86400000, tickWidth: 96, precision: "date" },
  { label: "日", stepMs: 86400000, tickWidth: 144, precision: "date" },
  { label: "时", stepMs: 6 * 3600000, tickWidth: 72, precision: "hour" },
  { label: "分", stepMs: 3600000, tickWidth: 64, precision: "minute" },
  { label: "秒", stepMs: 15 * 60000, tickWidth: 88, precision: "second" }
] as const;
const timelineScale = computed(() => timelineScales[timelineScaleIndex.value]);
const range = computed(() => timelineRange(datedItems.value));
const timelineTicks = computed(() => Array.from(
  { length: Math.ceil(range.value.dayCount * 86400000 / timelineScale.value.stepMs) },
  (_, index) => new Date(range.value.start.getTime() + index * timelineScale.value.stepMs)
));
const timelineWidth = computed(() => timelineTicks.value.length * timelineScale.value.tickWidth);
const todayOffset = computed(() => {
  const offset = (displaySerial(new Date()) - displaySerial(range.value.start)) / 86400000;
  return offset < 0 || offset >= range.value.dayCount ? null : offset * 100 / range.value.dayCount;
});
const weekendWash = computed(() => {
  const unit = 100 / range.value.dayCount;
  const stops: string[] = [];
  for (let index = 0; index < range.value.dayCount; index += 1) {
    const parts = displayParts(addDisplayDays(range.value.start, index));
    const day = new Date(Date.UTC(parts.year, parts.month - 1, parts.day)).getUTCDay();
    const color = day === 0 || day === 6 ? "rgb(67 83 88 / 3.2%)" : "transparent";
    stops.push(`${color} ${index * unit}%`, `${color} ${(index + 1) * unit}%`);
  }
  return `linear-gradient(to right, ${stops.join(", ")})`;
});
const hasActiveData = computed(() => isGlobalView.value ? overview.value.length > 0 : Boolean(timeline.value));
const asyncState = computed(() => loading.value ? hasActiveData.value ? "refreshing" : "initial-loading" : error.value ? hasActiveData.value ? "partial" : "error" : hasActiveData.value ? "ready" : "empty");
const overviewTotals = computed(() => overview.value.reduce((totals, item) => ({
  tasks: totals.tasks + item.taskCount,
  subtasks: totals.subtasks + item.subtaskCount,
  subsubtasks: totals.subsubtasks + item.subsubtaskCount,
  reports: totals.reports + item.reportCount
}), { tasks: 0, subtasks: 0, subsubtasks: 0, reports: 0 }));

onMounted(() => void loadActiveView());
watch(groupId, () => void loadActiveView());
watch(requestedTaskId, syncSelectedTaskFromRoute);
watch(requestedDetailMode, (mode) => { detailMode.value = mode; });

async function loadActiveView() {
  loading.value = true; error.value = null;
  const overviewResponse = await tasktreeAPI.overview();
  if (!overviewResponse.ok || overviewResponse.data?.ok === false) {
    loading.value = false;
    error.value = overviewResponse.data?.error?.message || overviewResponse.error || "TaskTree 全局视图加载失败";
    return;
  }
  overview.value = overviewResponse.data?.data?.groups ?? [];
  groups.value = overview.value.map((item) => item.group);
  if (!groupId.value) {
    timeline.value = null;
    loading.value = false;
    await nextTick();
    positionTimelineAtNow("global");
    return;
  }
  const timelineResponse = await tasktreeAPI.timeline(groupId.value);
  loading.value = false;
  if (!timelineResponse.ok || timelineResponse.data?.ok === false || !timelineResponse.data?.data) {
    error.value = timelineResponse.data?.error?.message || timelineResponse.error || "TaskTree timeline 加载失败";
    return;
  }
  timeline.value = timelineResponse.data.data;
  initializedChildGroups.clear();
  syncSelectedTaskFromRoute();
  await nextTick();
  positionTimelineAtNow(groupId.value);
}
function selectGroup(event: Event) {
  const value = (event.target as HTMLSelectElement).value;
  void router.push(value ? `/projects/tasktree/${encodeURIComponent(value)}` : "/projects/tasktree");
}
function openGroup(id: string) { void router.push(`/projects/tasktree/${encodeURIComponent(id)}`); }
function toggleTask(taskId: string) {
  const next = new Set(collapsedTaskIds.value);
  if (next.has(taskId)) next.delete(taskId);
  else next.add(taskId);
  collapsedTaskIds.value = next;
  if (!next.has(taskId)) {
    initializedChildGroups.delete(taskId);
  }
}
function visibleDescendants(rootId: string) {
  const result: TaskTreeTask[] = [];
  const append = (parentId: string) => {
    if (collapsedTaskIds.value.has(parentId)) return;
    for (const child of childrenByParent.value.get(parentId) ?? []) {
      result.push(child);
      append(child.id);
    }
  };
  append(rootId);
  return result;
}
function hierarchyAncestors(task: TaskTreeTask) {
  const result: TaskTreeTask[] = [];
  let parentId = task.parentId;
  while (parentId) {
    const parent = tasksById.value.get(parentId);
    if (!parent) break;
    if (parent.startAt || parent.dueAt) result.unshift(parent);
    parentId = parent.parentId;
  }
  return result;
}
function hierarchyBandClass(task: TaskTreeTask, ancestor: TaskTreeTask) {
  const descendants = visibleDescendants(ancestor.id);
  return {
    "is-nested": ancestor.kind !== "task",
    "is-last": descendants.at(-1)?.id === task.id
  };
}
function setChildScroller(side: "labels" | "timeline", groupId: string, element: Element | null) {
  const map = side === "labels" ? labelChildScrollers : timelineChildScrollers;
  if (!element) {
    map.delete(groupId);
    const edges = new Map(childScrollEdges.value);
    edges.delete(`${side}:${groupId}`);
    childScrollEdges.value = edges;
    return;
  }
  const scroller = element as HTMLElement;
  map.set(groupId, scroller);
  if (initializedChildGroups.has(groupId)) {
    updateChildScrollEdges(side, groupId, scroller);
    return;
  }
  void nextTick(() => initializeChildGroup(groupId));
}
function initializeChildGroup(groupId: string) {
  if (initializedChildGroups.has(groupId)) return;
  const labels = labelChildScrollers.get(groupId);
  const timelineScroller = timelineChildScrollers.get(groupId);
  if (!labels || !timelineScroller) return;
  const desiredTop = Math.max(0, (visibleDescendants(groupId).length - 8) * taskRowHeight);
  labels.scrollTop = Math.min(desiredTop, Math.max(0, labels.scrollHeight - labels.clientHeight));
  timelineScroller.scrollTop = Math.min(desiredTop, Math.max(0, timelineScroller.scrollHeight - timelineScroller.clientHeight));
  initializedChildGroups.add(groupId);
  updateChildScrollEdges("labels", groupId, labels);
  updateChildScrollEdges("timeline", groupId, timelineScroller);
}
function syncChildScroller(groupId: string, source: "labels" | "timeline") {
  const from = (source === "labels" ? labelChildScrollers : timelineChildScrollers).get(groupId);
  const to = (source === "labels" ? timelineChildScrollers : labelChildScrollers).get(groupId);
  if (from && to && to.scrollTop !== from.scrollTop) to.scrollTop = from.scrollTop;
  if (from) updateChildScrollEdges(source, groupId, from);
  if (to) updateChildScrollEdges(source === "labels" ? "timeline" : "labels", groupId, to);
}
function childScrollerClass(side: "labels" | "timeline", groupId: string, count: number) {
  const edge = childScrollEdges.value.get(`${side}:${groupId}`);
  return {
    scrollable: count > 8,
    "has-hidden-above": count > 8 && edge !== undefined && !edge.atTop,
    "has-hidden-below": count > 8 && edge !== undefined && !edge.atBottom
  };
}
function updateChildScrollEdges(side: "labels" | "timeline", groupId: string, scroller: HTMLElement) {
  const next = {
    atTop: scroller.scrollTop <= 1,
    atBottom: scroller.scrollTop >= scroller.scrollHeight - scroller.clientHeight - 1
  };
  const key = `${side}:${groupId}`;
  const current = childScrollEdges.value.get(key);
  if (current?.atTop === next.atTop && current.atBottom === next.atBottom) return;
  const edges = new Map(childScrollEdges.value);
  edges.set(key, next);
  childScrollEdges.value = edges;
}
function handleChildWheel(event: WheelEvent, groupId: string, side: "labels" | "timeline") {
  if (Math.abs(event.deltaY) <= Math.abs(event.deltaX)) return;
  const scroller = (side === "labels" ? labelChildScrollers : timelineChildScrollers).get(groupId);
  if (!scroller) return;
  const delta = event.deltaY * (event.deltaMode === WheelEvent.DOM_DELTA_LINE ? 40 : event.deltaMode === WheelEvent.DOM_DELTA_PAGE ? scroller.clientHeight : 1);
  const maxScrollTop = Math.max(0, scroller.scrollHeight - scroller.clientHeight);
  const available = Math.max(0, delta < 0 ? scroller.scrollTop : maxScrollTop - scroller.scrollTop);
  if (Math.abs(delta) <= available + 1) return;
  event.preventDefault();
  scroller.scrollTop = delta < 0 ? 0 : maxScrollTop;
  syncChildScroller(groupId, side);
  scrollOuterBy(side, delta < 0 ? delta + available : delta - available);
}
function scrollOuterBy(side: "labels" | "timeline", delta: number) {
  const source = side === "labels" ? taskLabelsElement.value : timelineBodyElement.value;
  const target = side === "labels" ? timelineBodyElement.value : taskLabelsElement.value;
  if (!source) return;
  const nextTop = Math.max(0, Math.min(source.scrollHeight - source.clientHeight, source.scrollTop + delta));
  source.scrollTop = nextTop;
  if (target) target.scrollTop = Math.max(0, Math.min(target.scrollHeight - target.clientHeight, nextTop));
}
function taskColumnLimit() { return Math.max(170, Math.min(760, window.innerWidth - 280)); }
function resizeTaskColumn(width: number) { taskColumnWidth.value = Math.max(170, Math.min(taskColumnLimit(), Math.round(width))); }
function startTaskColumnResize(event: PointerEvent) {
  const handle = event.currentTarget as HTMLElement;
  const startX = event.clientX;
  const startWidth = taskColumnWidth.value;
  handle.setPointerCapture(event.pointerId);
  const move = (moveEvent: PointerEvent) => resizeTaskColumn(startWidth + moveEvent.clientX - startX);
  const stop = (stopEvent: PointerEvent) => {
    handle.releasePointerCapture(stopEvent.pointerId);
    handle.removeEventListener("pointermove", move);
    handle.removeEventListener("pointerup", stop);
    handle.removeEventListener("pointercancel", stop);
  };
  handle.addEventListener("pointermove", move);
  handle.addEventListener("pointerup", stop);
  handle.addEventListener("pointercancel", stop);
}
function resizeTaskColumnByKeyboard(event: KeyboardEvent) {
  if (event.key !== "ArrowLeft" && event.key !== "ArrowRight") return;
  event.preventDefault();
  resizeTaskColumn(taskColumnWidth.value + (event.key === "ArrowLeft" ? -16 : 16));
}
async function changeTimelineScale(direction: -1 | 1) {
  const body = timelineBodyElement.value;
  const centerRatio = body && body.scrollWidth > 0 ? (body.scrollLeft + body.clientWidth / 2) / body.scrollWidth : 0;
  timelineScaleIndex.value = Math.min(timelineScales.length - 1, Math.max(0, timelineScaleIndex.value + direction));
  await nextTick();
  if (body) {
    body.scrollLeft = Math.max(0, centerRatio * body.scrollWidth - body.clientWidth / 2);
    if (timelineHeaderElement.value) timelineHeaderElement.value.scrollLeft = body.scrollLeft;
  }
}
function syncTimelineBody() {
  const body = timelineBodyElement.value;
  if (!body) return;
  timelineScrollbarWidth.value = Math.max(0, body.offsetWidth - body.clientWidth);
  if (timelineHeaderElement.value && timelineHeaderElement.value.scrollLeft !== body.scrollLeft) timelineHeaderElement.value.scrollLeft = body.scrollLeft;
  if (taskLabelsElement.value && taskLabelsElement.value.scrollTop !== body.scrollTop) taskLabelsElement.value.scrollTop = body.scrollTop;
}
function positionTimelineAtNow(view: string) {
  const body = timelineBodyElement.value;
  if (!body || positionedView === view) return;
  timelineScrollbarWidth.value = Math.max(0, body.offsetWidth - body.clientWidth);
  const now = todayOffset.value;
  body.scrollLeft = now === null ? 0 : Math.max(0, Math.min(body.scrollWidth - body.clientWidth, body.scrollWidth * now / 100 - body.clientWidth * 0.8));
  if (timelineHeaderElement.value) timelineHeaderElement.value.scrollLeft = body.scrollLeft;
  positionedView = view;
}
function syncTaskLabels() {
  const labels = taskLabelsElement.value;
  const body = timelineBodyElement.value;
  if (labels && body && body.scrollTop !== labels.scrollTop) body.scrollTop = labels.scrollTop;
}
function timelineRange(items: Array<{ startAt: string | null; dueAt: string | null }>) {
  const values = items.flatMap((item) => [item.startAt, item.dueAt]).filter(Boolean).map((value) => new Date(value as string).getTime());
  const today = startDisplayDay(new Date()); const min = values.length ? Math.min(...values) : today.getTime(); const max = values.length ? Math.max(...values) : addDisplayDays(today, 6).getTime();
  const start = addDisplayDays(startDisplayDay(new Date(min)), -1);
  const dayCount = Math.round((displayDaySerial(startDisplayDay(new Date(max))) - displayDaySerial(start)) / 86400000) + 2;
  return { start, dayCount: Math.min(120, Math.max(7, dayCount)) };
}
function displayParts(date: Date) {
  const values = Object.fromEntries(displayPartsFormatter.formatToParts(date).filter((part) => part.type !== "literal").map((part) => [part.type, Number(part.value)]));
  return { year: values.year, month: values.month, day: values.day, hour: values.hour, minute: values.minute, second: values.second };
}
function instantForDisplayParts(parts: { year: number; month: number; day: number; hour?: number; minute?: number; second?: number }) {
  const target = Date.UTC(parts.year, parts.month - 1, parts.day, parts.hour ?? 0, parts.minute ?? 0, parts.second ?? 0);
  let guess = target;
  for (let index = 0; index < 2; index += 1) {
    const shown = displayParts(new Date(guess));
    guess += target - Date.UTC(shown.year, shown.month - 1, shown.day, shown.hour, shown.minute, shown.second);
  }
  return new Date(guess);
}
function startDisplayDay(date: Date) { const parts = displayParts(date); return instantForDisplayParts({ year: parts.year, month: parts.month, day: parts.day }); }
function addDisplayDays(date: Date, days: number) { const parts = displayParts(date); const serial = new Date(Date.UTC(parts.year, parts.month - 1, parts.day + days)); return instantForDisplayParts({ year: serial.getUTCFullYear(), month: serial.getUTCMonth() + 1, day: serial.getUTCDate(), hour: parts.hour, minute: parts.minute, second: parts.second }); }
function displaySerial(date: Date) { const parts = displayParts(date); return Date.UTC(parts.year, parts.month - 1, parts.day, parts.hour, parts.minute, parts.second); }
function displayDaySerial(date: Date) { const parts = displayParts(date); return Date.UTC(parts.year, parts.month - 1, parts.day); }
function barStyle(item: { startAt: string | null; dueAt: string | null }) {
  const start = displaySerial(new Date(item.startAt || item.dueAt || range.value.start)); const end = displaySerial(new Date(item.dueAt || item.startAt || range.value.start)); const unit = 100 / range.value.dayCount;
  const rangeStart = displaySerial(range.value.start);
  const left = Math.max(0, (start - rangeStart) / 86400000) * unit;
  const rightEdge = Math.min(100, Math.max(left, (end - rangeStart) / 86400000 * unit));
  return { right: `${100 - rightEdge}%`, width: `${Math.max(0, rightEdge - left)}%` };
}
function trackStyle() {
  return {
    backgroundImage: `${weekendWash.value}, linear-gradient(to right, #edf1f0 1px, transparent 1px)`,
    backgroundSize: `100% 100%, ${100 / timelineTicks.value.length}% 100%`
  };
}
function milestoneStyle(occursAt: string) {
  const time = displaySerial(new Date(occursAt));
  const left = Math.max(0, (time - displaySerial(range.value.start)) / 86400000) * (100 / range.value.dayCount);
  return { left: `${Math.min(100, left)}%` };
}
function tickLabel(date: Date) {
  const parts = displayParts(date);
  const monthDay = `${parts.month}/${parts.day}`;
  if (timelineScale.value.precision === "date") return monthDay;
  const hour = String(parts.hour).padStart(2, "0");
  if (timelineScale.value.precision === "hour") return `${monthDay} ${hour}时`;
  const minute = String(parts.minute).padStart(2, "0");
  if (timelineScale.value.precision === "minute") return `${monthDay} ${hour}:${minute}`;
  const second = String(parts.second).padStart(2, "0");
  return `${monthDay} ${hour}:${minute}:${second}`;
}
function tickClass(date: Date) {
  const end = date.getTime() + timelineScale.value.stepMs;
  const now = Date.now();
  const parts = displayParts(date);
  const weekday = new Date(Date.UTC(parts.year, parts.month - 1, parts.day)).getUTCDay();
  return {
    "is-current": date.getTime() <= now && end > now,
    "is-weekend": timelineScale.value.precision === "date" && (weekday === 0 || weekday === 6)
  };
}
function statusLabel(status: string) { return ({ pending: "待处理", in_progress: "进行中", completed: "已完成", blocked: "阻塞" } as Record<string, string>)[status] || status; }
function taskKindLabel(kind: TaskTreeTask["kind"]) { return ({ task: "Task", subtask: "Subtask", subsubtask: "Subsubtask" })[kind]; }
function plainTaskTitle(task: TaskTreeTask | null) {
  return String(task?.title || "任务详情").replace(/\[([^\]]+)\]\([^)]+\)/gu, "$1").replace(/`([^`]+)`/gu, "$1").trim();
}
function selectTaskFromTitle(event: MouseEvent, task: TaskTreeTask) {
  if ((event.target as Element).closest("a")) return;
  selectTask(task);
}
function selectTask(task: TaskTreeTask | null) {
  if (!task) return;
  selectedTask.value = task;
  if (requestedTaskId.value !== task.id) void router.replace({ query: { ...route.query, task: task.id } });
}
function closeTask() {
  selectedTask.value = null;
  if (!requestedTaskId.value) return;
  const query = { ...route.query };
  delete query.task;
  delete query.detail;
  void router.replace({ query });
}
function setDetailMode(mode: "dialog" | "docked") {
  detailMode.value = mode;
  const query = { ...route.query };
  if (mode === "docked") query.detail = "docked";
  else delete query.detail;
  void router.replace({ query });
}
function syncSelectedTaskFromRoute() {
  if (!requestedTaskId.value) { selectedTask.value = null; return; }
  selectedTask.value = timeline.value?.tasks.find((task) => task.id === requestedTaskId.value) ?? null;
}
</script>

<template>
  <main class="tasktree-page" data-testid="tasktree-page">
    <AsyncBoundary
      :state="asyncState"
      empty-title="TaskTree 暂无任务"
      empty-description="通过 CLI 创建 taskgroup 和任务后会显示在时间轴中。"
      error-title="TaskTree 加载失败"
      :error="error || '请求未完成，请稍后重试。'"
      @retry="loadActiveView"
    >
      <div v-if="isGlobalView || timeline" class="tasktree-view-layout" :class="{ docked: detailMode === 'docked' && selectedTask }">
        <section class="tasktree-workspace" aria-label="TaskTree 甘特图">
        <header class="tasktree-context">
          <div class="tasktree-context-copy">
            <div class="tasktree-product-title"><span>项目时间线</span><strong>TaskTree</strong></div>
            <span class="tasktree-context-separator" aria-hidden="true" />
            <div v-if="isGlobalView" class="tasktree-current-group"><strong>全局 TaskGroups</strong><p>跨项目时间范围与工作量概览</p></div>
            <div v-else-if="timeline" class="tasktree-current-group"><strong>{{ timeline.group.name }}</strong><p>{{ timeline.group.description || '未填写 taskgroup 说明' }}</p></div>
          </div>
          <div class="tasktree-context-actions">
            <div class="tasktree-timeline-tools">
              <span>{{ range.dayCount }} 天</span>
              <div class="tasktree-zoom" role="group" aria-label="时间轴缩放">
                <button type="button" title="缩小时间尺度" aria-label="缩小时间尺度" :disabled="timelineScaleIndex === 0" @click="changeTimelineScale(-1)"><ZoomOut :size="15" aria-hidden="true" /></button>
                <output :title="`当前时间尺度：${timelineScale.label}`" aria-live="polite">{{ timelineScale.label }}</output>
                <button type="button" title="放大时间尺度" aria-label="放大时间尺度" :disabled="timelineScaleIndex === timelineScales.length - 1" @click="changeTimelineScale(1)"><ZoomIn :size="15" aria-hidden="true" /></button>
              </div>
            </div>
            <label class="tasktree-group-select"><span class="sr-only">Taskgroup</span><select :value="groupId" @change="selectGroup"><option value="">全部 TaskGroups</option><option v-for="group in groups" :key="group.id" :value="group.id">{{ group.name }}</option></select></label>
            <button class="icon-button tasktree-refresh" type="button" title="刷新" aria-label="刷新" :disabled="loading" @click="loadActiveView"><RefreshCw :size="15" aria-hidden="true" /></button>
          </div>
        </header>
        <div class="tasktree-gantt" :class="{ 'global-view': isGlobalView, 'has-today': todayOffset !== null }" :style="{ '--task-column-width': `${taskColumnWidth}px`, '--tick-count': timelineTicks.length, '--tick-width': `${timelineScale.tickWidth}px`, '--timeline-width': `${timelineWidth}px`, '--today-left': `${todayOffset ?? 0}%`, '--timeline-scrollbar-width': `${timelineScrollbarWidth}px` }">
          <div class="tasktree-corner">{{ isGlobalView ? 'TaskGroup' : '任务' }}</div>
          <div
            class="tasktree-column-resizer"
            role="separator"
            aria-label="调整任务列表宽度"
            aria-orientation="vertical"
            aria-valuemin="170"
            :aria-valuemax="taskColumnLimit()"
            :aria-valuenow="taskColumnWidth"
            tabindex="0"
            @pointerdown="startTaskColumnResize"
            @keydown="resizeTaskColumnByKeyboard"
          />
          <div ref="timelineHeaderElement" class="tasktree-timeline-header">
            <div class="tasktree-days">
              <span v-for="tick in timelineTicks" :key="tick.toISOString()" :class="tickClass(tick)">{{ tickLabel(tick) }}</span>
            </div>
          </div>
          <div ref="taskLabelsElement" class="tasktree-labels" @scroll="syncTaskLabels">
            <div v-if="isGlobalView" class="tasktree-labels-canvas tasktree-overview-labels">
              <div v-for="item in overview" :key="item.group.id" class="tasktree-label tasktree-group-label" role="link" tabindex="0" @click="openGroup(item.group.id)" @keydown.enter="openGroup(item.group.id)">
                <div class="tasktree-group-title">
                  <MessageMarkdown class="tasktree-title-markdown" :source="item.group.name" />
                  <ChevronRight :size="17" aria-hidden="true" />
                </div>
                <span>{{ item.taskCount }} tasks · {{ item.subtaskCount + item.subsubtaskCount }} children · {{ item.reportCount }} reports</span>
              </div>
            </div>
            <div v-else class="tasktree-labels-canvas tasktree-detail-labels">
              <section v-for="root in rootTasks" :key="root.id" class="tasktree-task-group">
                <div class="tasktree-label" :data-task-id="root.id" :data-dated="Boolean(root.startAt || root.dueAt)">
                  <button
                    v-if="childrenByParent.has(root.id)"
                    class="tasktree-disclosure"
                    type="button"
                    :title="collapsedTaskIds.has(root.id) ? '展开子任务' : '收起子任务'"
                    :aria-label="collapsedTaskIds.has(root.id) ? `展开 ${root.title} 的子任务` : `收起 ${root.title} 的子任务`"
                    :aria-expanded="!collapsedTaskIds.has(root.id)"
                    @click="toggleTask(root.id)"
                  >
                    <ChevronRight v-if="collapsedTaskIds.has(root.id)" :size="15" aria-hidden="true" />
                    <ChevronDown v-else :size="15" aria-hidden="true" />
                  </button>
                  <span v-else class="tasktree-disclosure-spacer" aria-hidden="true" />
                  <div class="tasktree-title" role="button" tabindex="0" @click="selectTaskFromTitle($event, root)" @keydown.enter="selectTask(root)">
                    <span class="tasktree-status" :data-status="root.status" />
                    <span class="tasktree-outline-ref">{{ root.outlineRef }}</span>
                    <MessageMarkdown class="tasktree-title-markdown" :source="root.title" />
                  </div>
                </div>
                <div
                  v-if="visibleDescendants(root.id).length"
                  :ref="(element) => setChildScroller('labels', root.id, element as Element | null)"
                  class="tasktree-child-viewport"
                  :class="childScrollerClass('labels', root.id, visibleDescendants(root.id).length)"
                  @scroll="syncChildScroller(root.id, 'labels')"
                  @wheel="handleChildWheel($event, root.id, 'labels')"
                >
                  <div v-for="task in visibleDescendants(root.id)" :key="task.id" class="tasktree-label" :class="task.kind" :data-task-id="task.id" :data-dated="Boolean(task.startAt || task.dueAt)">
                    <button
                      v-if="childrenByParent.has(task.id)"
                      class="tasktree-disclosure"
                      type="button"
                      :title="collapsedTaskIds.has(task.id) ? '展开下级任务' : '收起下级任务'"
                      :aria-label="collapsedTaskIds.has(task.id) ? `展开 ${task.title} 的下级任务` : `收起 ${task.title} 的下级任务`"
                      :aria-expanded="!collapsedTaskIds.has(task.id)"
                      @click="toggleTask(task.id)"
                    >
                      <ChevronRight v-if="collapsedTaskIds.has(task.id)" :size="15" aria-hidden="true" />
                      <ChevronDown v-else :size="15" aria-hidden="true" />
                    </button>
                    <span v-else class="tasktree-disclosure-spacer" aria-hidden="true" />
                    <div class="tasktree-title" role="button" tabindex="0" @click="selectTaskFromTitle($event, task)" @keydown.enter="selectTask(task)">
                      <span class="tasktree-status" :data-status="task.status" />
                      <span class="tasktree-outline-ref">{{ task.outlineRef }}</span>
                      <MessageMarkdown class="tasktree-title-markdown" :source="task.title" />
                    </div>
                  </div>
                </div>
              </section>
            </div>
          </div>
          <div ref="timelineBodyElement" class="tasktree-timeline-body" @scroll="syncTimelineBody">
            <div v-if="isGlobalView" class="tasktree-timeline-canvas tasktree-overview-timeline">
              <div v-for="item in overview" :key="item.group.id" class="tasktree-track tasktree-group-track" :style="trackStyle()">
                <button
                  v-if="item.startAt || item.dueAt"
                  class="tasktree-group-bar"
                  :style="barStyle(item)"
                  type="button"
                  :aria-label="`打开 ${item.group.name}`"
                  @click="openGroup(item.group.id)"
                >
                  <span>{{ item.taskCount + item.subtaskCount + item.subsubtaskCount }} 项</span>
                </button>
              </div>
            </div>
            <div v-else-if="timeline" class="tasktree-timeline-canvas tasktree-detail-timeline">
              <section v-for="root in rootTasks" :key="root.id" class="tasktree-task-group">
                <div class="tasktree-track" :data-task-id="root.id" :style="trackStyle()">
                  <span v-if="childrenByParent.has(root.id) && (root.startAt || root.dueAt)" class="tasktree-summary-connector" :style="barStyle(root)" aria-hidden="true" />
                  <button v-if="root.startAt || root.dueAt" class="tasktree-bar" :class="[`status-${root.status}`, { 'is-summary': childrenByParent.has(root.id) }]" :style="barStyle(root)" type="button" :aria-label="`${root.title}，${statusLabel(root.status)}`" @click="selectTask(root)"><span>{{ statusLabel(root.status) }}</span></button>
                  <span v-for="milestone in timeline.milestones.filter((item) => item.taskId === root.id)" :key="milestone.id" class="tasktree-milestone" role="img" :aria-label="`里程碑：${milestone.title}`" :title="milestone.title" :style="milestoneStyle(milestone.occursAt)" />
                </div>
                <div
                  v-if="visibleDescendants(root.id).length"
                  :ref="(element) => setChildScroller('timeline', root.id, element as Element | null)"
                  class="tasktree-child-viewport"
                  :class="childScrollerClass('timeline', root.id, visibleDescendants(root.id).length)"
                  @scroll="syncChildScroller(root.id, 'timeline')"
                  @wheel="handleChildWheel($event, root.id, 'timeline')"
                >
                  <div v-for="task in visibleDescendants(root.id)" :key="task.id" class="tasktree-track" :data-task-id="task.id" :style="trackStyle()">
                    <span
                      v-for="ancestor in hierarchyAncestors(task)"
                      :key="ancestor.id"
                      class="tasktree-hierarchy-band"
                      :class="hierarchyBandClass(task, ancestor)"
                      :style="barStyle(ancestor)"
                      aria-hidden="true"
                    />
                    <span v-if="childrenByParent.has(task.id) && (task.startAt || task.dueAt)" class="tasktree-summary-connector is-nested" :style="barStyle(task)" aria-hidden="true" />
                    <button v-if="task.startAt || task.dueAt" class="tasktree-bar" :class="[`status-${task.status}`, task.kind, { 'is-summary': childrenByParent.has(task.id) }]" :style="barStyle(task)" type="button" :aria-label="`${task.title}，${statusLabel(task.status)}`" @click="selectTask(task)"><span>{{ statusLabel(task.status) }}</span></button>
                    <span v-for="milestone in timeline.milestones.filter((item) => item.taskId === task.id)" :key="milestone.id" class="tasktree-milestone" role="img" :aria-label="`里程碑：${milestone.title}`" :title="milestone.title" :style="milestoneStyle(milestone.occursAt)" />
                  </div>
                </div>
              </section>
            </div>
          </div>
        </div>
        <footer class="tasktree-statusbar" aria-label="TaskTree 状态">
          <span><strong>{{ overview.length }}</strong> Taskgroups</span>
          <span><strong>{{ isGlobalView ? overviewTotals.tasks : timeline?.tasks.filter((task) => task.kind === 'task').length }}</strong> Tasks</span>
          <span><strong>{{ isGlobalView ? overviewTotals.subtasks : timeline?.tasks.filter((task) => task.kind === 'subtask').length }}</strong> Subtasks</span>
          <span><strong>{{ isGlobalView ? overviewTotals.subsubtasks : timeline?.tasks.filter((task) => task.kind === 'subsubtask').length }}</strong> Subsubtasks</span>
          <span><strong>{{ isGlobalView ? overviewTotals.reports : timeline?.reports.length }}</strong> Reports</span>
          <span v-if="!isGlobalView"><strong>{{ timeline?.milestones.length }}</strong> Milestones</span>
          <span class="tasktree-statusbar-authority">PostgreSQL</span>
        </footer>
        </section>
        <aside v-if="detailMode === 'docked' && selectedTask && timeline" class="tasktree-docked-detail" aria-label="任务详情">
          <TaskTreeDetailPanel :task="selectedTask" :tasks="timeline.tasks" :reports="timeline.reports" mode="docked" @select="selectTask" @close="closeTask" @mode="setDetailMode" />
        </aside>
      </div>
    </AsyncBoundary>
    <BaseDialog :open="Boolean(selectedTask) && detailMode === 'dialog'" :title="plainTaskTitle(selectedTask)" :description="selectedTask ? `${taskKindLabel(selectedTask.kind)} · ${statusLabel(selectedTask.status)}` : ''" surface-class="tasktree-detail-dialog" wide @close="closeTask">
      <TaskTreeDetailPanel v-if="selectedTask && timeline" :task="selectedTask" :tasks="timeline.tasks" :reports="timeline.reports" mode="dialog" @select="selectTask" @close="closeTask" @mode="setDetailMode" />
    </BaseDialog>
  </main>
</template>

<style scoped>
:global(.platform-content:has(> .tasktree-page)) { background: var(--console-canvas); }
.tasktree-page { display: grid; min-width: 0; }
.tasktree-view-layout { display: grid; grid-template-columns: minmax(0, 1fr); min-width: 0; gap: 10px; }
.tasktree-view-layout.docked { grid-template-columns: minmax(0, 1fr) clamp(520px, 34vw, 680px); }
.tasktree-workspace { display: grid; grid-template-rows: auto minmax(0, 1fr) auto; height: calc(100dvh - 82px); min-height: 360px; border: 1px solid #d9e0df; background: #fff; min-width: 0; overflow: hidden; }
.tasktree-docked-detail { height: calc(100dvh - 82px); min-width: 0; min-height: 360px; overflow: hidden; border: 1px solid var(--border-color); background: var(--surface-primary); }
.tasktree-context { min-height: 48px; padding: 6px 8px 6px 12px; border-bottom: 1px solid #dfe5e4; display: flex; align-items: center; justify-content: space-between; gap: 12px; background: #fff; }
.tasktree-context-copy { display: flex; min-width: 0; align-items: center; gap: 10px; }
.tasktree-product-title { display: flex; flex: 0 0 auto; align-items: baseline; gap: 8px; }
.tasktree-product-title span { color: var(--console-cyan-700); font-family: var(--console-font-mono); font-size: 9px; font-weight: 700; }
.tasktree-product-title strong { color: var(--text-primary); font-size: 18px; line-height: 1; }
.tasktree-context-separator { width: 1px; height: 24px; flex: 0 0 1px; background: var(--border-color); }
.tasktree-current-group { display: flex; min-width: 0; max-width: min(38vw, 560px); align-items: baseline; gap: 8px; }
.tasktree-current-group strong { flex: 0 1 auto; overflow: hidden; color: var(--text-primary); font-size: 13px; text-overflow: ellipsis; white-space: nowrap; }
.tasktree-current-group p { min-width: 0; margin: 0; overflow: hidden; color: var(--text-secondary); font-size: 10px; text-overflow: ellipsis; white-space: nowrap; }
.tasktree-context-actions { display: flex; flex: 0 0 auto; align-items: center; gap: 7px; }
.tasktree-view-layout.docked .tasktree-current-group { max-width: 190px; }
.tasktree-view-layout.docked .tasktree-current-group p { display: none; }
.tasktree-view-layout.docked .tasktree-current-group strong { max-width: 190px; }
.tasktree-view-layout.docked .tasktree-group-select select { width: clamp(140px, 13vw, 180px); }
.tasktree-timeline-tools { display: flex; align-items: center; gap: 10px; color: var(--text-secondary); font-size: 12px; }
.tasktree-zoom { display: grid; grid-template-columns: 28px 42px 28px; height: 28px; align-items: stretch; border: 1px solid var(--border-color); background: var(--surface-primary); }
.tasktree-zoom button { display: grid; place-items: center; padding: 0; border: 0; background: transparent; color: var(--text-secondary); cursor: pointer; }
.tasktree-zoom button:hover:not(:disabled) { background: var(--surface-secondary); color: var(--text-primary); }
.tasktree-zoom output { display: grid; place-items: center; border-inline: 1px solid var(--border-color); color: var(--text-primary); font-family: var(--console-font-mono); font-size: 10px; }
.tasktree-gantt { display: grid; grid-template-areas: "corner resize header" "labels resize timeline"; grid-template-columns: var(--task-column-width) 7px minmax(0, 1fr); grid-template-rows: 34px minmax(0, 1fr); min-height: 0; height: 100%; overflow: hidden; }
.tasktree-corner { grid-area: corner; }
.tasktree-timeline-header { grid-area: header; }
.tasktree-labels { grid-area: labels; }
.tasktree-timeline-body { grid-area: timeline; }
.tasktree-column-resizer { position: relative; z-index: 3; grid-area: resize; width: 7px; min-height: 0; border: 0; border-inline: 1px solid var(--border-color); background: var(--surface-secondary); cursor: col-resize; touch-action: none; }
.tasktree-column-resizer::after { position: absolute; inset: 0 2px; background: transparent; content: ""; transition: background-color 120ms ease; }
.tasktree-column-resizer:hover::after,
.tasktree-column-resizer:focus-visible::after,
.tasktree-column-resizer:active::after { background: #2376c9; }
.tasktree-corner { display: flex; align-items: center; padding: 0 14px; border-right: 1px solid var(--border-color); border-bottom: 1px solid var(--border-color); background: transparent; font-size: 11px; font-weight: 700; }
.tasktree-timeline-header { min-width: 0; overflow: hidden; padding-right: var(--timeline-scrollbar-width); border-bottom: 1px solid #dfe5e4; background: #fbfcfc; scrollbar-width: none; }
.tasktree-timeline-header::-webkit-scrollbar { display: none; }
.tasktree-days { position: relative; width: max(100%, var(--timeline-width)); height: 100%; display: grid; grid-template-columns: repeat(var(--tick-count), minmax(var(--tick-width), 1fr)); }
.tasktree-days span { position: relative; display: flex; align-items: center; justify-content: center; min-width: 0; padding: 0 4px; overflow: hidden; border-right: 1px solid #e6ebea; color: #68777a; font-family: var(--console-font-mono); font-size: 10px; white-space: nowrap; }
.tasktree-days span.is-weekend { background: #f3f6f5; color: #879194; }
.tasktree-days span.is-current { color: #b55739; font-weight: 700; }
.tasktree-days span.is-current::after { position: absolute; right: 22%; bottom: 3px; left: 22%; height: 2px; border-radius: 2px; background: #d46d4a; content: ""; }
.tasktree-labels { min-width: 0; min-height: 0; max-height: 100%; overflow-y: auto; overflow-x: hidden; border-right: 1px solid #dfe5e4; background: #fcfdfd; scrollbar-width: thin; overscroll-behavior-block: contain; }
.tasktree-labels-canvas { min-height: 100%; align-content: start; }
.tasktree-overview-labels { display: grid; grid-auto-rows: 68px; }
.tasktree-task-group { min-width: 0; }
.tasktree-label { height: 46px; border-bottom: 1px solid #edf0ef; background: #fff; padding: 0 8px; display: flex; align-items: center; color: var(--text-primary); min-width: 0; }
.tasktree-task-group > .tasktree-label { background: #f7f9f8; font-weight: 600; }
.tasktree-label:hover { background: #f2f7f5; }
.tasktree-outline-ref { flex: 0 0 auto; color: #587078; font-family: var(--console-font-mono); font-size: 11px; font-weight: 700; }
.tasktree-label.subtask { padding-left: 30px; color: var(--text-secondary); }
.tasktree-label.subsubtask { padding-left: 54px; color: var(--text-secondary); }
.tasktree-group-label { height: 68px; padding: 9px 14px; display: grid; align-content: center; gap: 5px; cursor: pointer; }
.tasktree-group-label > span { color: var(--text-secondary); font-family: var(--console-font-mono); font-size: 10px; }
.tasktree-group-title { display: flex; min-width: 0; align-items: center; gap: 8px; color: var(--text-primary); font-size: 15px; font-weight: 700; }
.tasktree-group-title svg { flex: 0 0 auto; color: var(--text-secondary); }
.tasktree-disclosure,
.tasktree-disclosure-spacer { width: 24px; height: 28px; flex: 0 0 24px; }
.tasktree-disclosure { display: grid; place-items: center; padding: 0; border: 0; background: transparent; color: var(--text-secondary); cursor: pointer; }
.tasktree-disclosure:hover { color: var(--text-primary); background: var(--surface-secondary); }
.tasktree-title { display: flex; flex: 1 1 auto; align-items: center; gap: 8px; min-width: 0; height: 100%; padding: 0 4px; color: inherit; text-align: left; cursor: pointer; }
.tasktree-title:focus-visible { outline: 2px solid #2376c9; outline-offset: -2px; }
.tasktree-title-markdown { min-width: 0; overflow: hidden; }
.tasktree-title-markdown :deep(p) { min-width: 0; margin: 0; overflow: hidden; font: inherit; text-overflow: ellipsis; white-space: nowrap; }
.tasktree-title-markdown :deep(a) { color: #1766ad; text-decoration: underline; text-underline-offset: 2px; }
.tasktree-title-markdown :deep(code) { padding: 1px 3px; border: 1px solid var(--border-subtle); background: var(--surface-secondary); color: inherit; font-size: 0.9em; }
.tasktree-status { width: 7px; height: 7px; border-radius: 50%; background: #83909c; flex: 0 0 auto; }
.tasktree-status[data-status="completed"] { background: #1f9d67; }
.tasktree-status[data-status="in_progress"] { background: #2376c9; }
.tasktree-status[data-status="blocked"] { background: #c73e48; }
.tasktree-timeline-body { min-width: 0; min-height: 0; max-height: 100%; overflow: auto; background: #fff; scrollbar-gutter: stable; overscroll-behavior: contain; touch-action: pan-x pan-y; }
.tasktree-timeline-canvas { width: max(100%, var(--timeline-width)); min-height: 100%; align-content: start; }
.tasktree-overview-timeline { display: grid; grid-auto-rows: 68px; }
.tasktree-track { position: relative; height: 46px; border-bottom: 1px solid #edf0ef; background-color: #fff; background-image: linear-gradient(to right, #edf1f0 1px, transparent 1px); }
.tasktree-task-group > .tasktree-track { background-color: #f7f9f8; }
.tasktree-gantt.has-today .tasktree-track::before { position: absolute; top: 0; bottom: -1px; left: var(--today-left); z-index: 1; width: 1px; background: rgb(204 91 57 / 48%); content: ""; pointer-events: none; }
.tasktree-group-track { height: 68px; }
.tasktree-hierarchy-band,
.tasktree-summary-connector { --summary-ink: rgb(64 91 109 / 27%); --summary-wash: rgb(64 91 109 / 4.5%); position: absolute; z-index: 1; box-sizing: border-box; border-inline: 1px solid var(--summary-ink); background: linear-gradient(90deg, var(--summary-wash), transparent 12px, transparent calc(100% - 12px), var(--summary-wash)); pointer-events: none; }
.tasktree-hierarchy-band { inset-block: 0; }
.tasktree-hierarchy-band.is-last::before,
.tasktree-hierarchy-band.is-last::after { position: absolute; bottom: 0; width: 9px; height: 1px; background: var(--summary-ink); content: ""; }
.tasktree-hierarchy-band.is-last::before { left: 0; }
.tasktree-hierarchy-band.is-last::after { right: 0; }
.tasktree-hierarchy-band.is-nested,
.tasktree-summary-connector.is-nested { --summary-ink: rgb(91 111 124 / 22%); --summary-wash: rgb(91 111 124 / 3.5%); }
.tasktree-hierarchy-band.is-nested { inset-block: 3px; }
.tasktree-summary-connector { top: 20px; bottom: 0; }
.tasktree-summary-connector.is-nested { top: 20px; }
.tasktree-bar { position: absolute; top: 11px; z-index: 2; height: 24px; min-width: 18px; padding: 0; border: 1px solid rgb(31 89 126 / 58%); border-radius: 4px; background: linear-gradient(90deg, #3875a4, #4a87b4); color: white; overflow: visible; box-shadow: inset 0 1px rgb(255 255 255 / 24%), 0 1px 2px rgb(22 46 64 / 14%); transition: filter 120ms ease, box-shadow 120ms ease, transform 120ms ease; }
.tasktree-bar:hover { filter: brightness(0.96) saturate(1.08); box-shadow: inset 0 1px rgb(255 255 255 / 28%), 0 3px 7px rgb(22 46 64 / 18%); transform: translateY(-1px); }
.tasktree-bar span { display: none; }
.tasktree-bar.subtask { top: 15px; height: 16px; border-color: rgb(67 101 125 / 52%); border-radius: 3px; background: linear-gradient(90deg, #607c92, #7895aa); }
.tasktree-bar.subsubtask { top: 17px; height: 12px; border-color: rgb(92 109 122 / 45%); border-radius: 2px; background: linear-gradient(90deg, #8194a2, #98aab7); }
.tasktree-bar.is-summary { top: 17px; height: 3px; border: 0; border-radius: 1px; background: #466579; box-shadow: none; }
.tasktree-bar.is-summary:hover { filter: brightness(0.9); transform: none; }
.tasktree-bar.is-summary::before,
.tasktree-bar.is-summary::after { position: absolute; top: 0; width: 2px; height: 8px; border-radius: 0 0 1px 1px; background: inherit; content: ""; }
.tasktree-bar.is-summary::before { left: 0; }
.tasktree-bar.is-summary::after { right: 0; }
.tasktree-group-bar { position: absolute; top: 16px; height: 36px; min-width: 28px; overflow: hidden; border: 1px solid #135b94; border-radius: 4px; background: #176ba8; color: white; box-shadow: inset 0 1px 0 rgb(255 255 255 / 22%), 0 2px 5px rgb(17 67 101 / 20%); cursor: pointer; }
.tasktree-group-bar:hover { background: #125d95; }
.tasktree-group-bar span { display: block; padding: 0 10px; overflow: hidden; font-size: 11px; font-weight: 700; line-height: 34px; text-overflow: ellipsis; white-space: nowrap; }
.tasktree-bar.status-completed { border-color: rgb(29 111 76 / 62%); background: linear-gradient(90deg, #277d5a, #4a9a72); }
.tasktree-bar.status-blocked { border-color: rgb(144 43 50 / 62%); background: linear-gradient(90deg, #a83c45, #c55a60); }
.tasktree-bar.status-pending { border-color: rgb(79 92 102 / 58%); background: linear-gradient(90deg, #63717b, #81909a); }
.tasktree-bar.status-completed.is-summary,
.tasktree-bar.status-blocked.is-summary,
.tasktree-bar.status-pending.is-summary { background: #466579; }
.tasktree-milestone { position: absolute; top: 15px; z-index: 2; width: 16px; height: 16px; transform: translateX(-50%); background: #d39114; clip-path: polygon(50% 0, 100% 50%, 50% 100%, 0 50%); filter: drop-shadow(0 0 0.5px #664000) drop-shadow(0 1px 1px rgb(0 0 0 / 35%)); }
.tasktree-task-group:has(> .tasktree-child-viewport.scrollable) > .tasktree-label,
.tasktree-task-group:has(> .tasktree-child-viewport.scrollable) > .tasktree-track { position: relative; z-index: 3; border-bottom-color: #91a4b3; }
.tasktree-task-group:has(> .tasktree-child-viewport.has-hidden-above) > .tasktree-label,
.tasktree-task-group:has(> .tasktree-child-viewport.has-hidden-above) > .tasktree-track { box-shadow: 0 9px 15px -8px rgb(24 45 61 / 62%); }
.tasktree-child-viewport { width: 100%; max-height: calc(46px * 8); overflow: hidden; background: color-mix(in srgb, var(--surface-secondary) 55%, var(--surface-primary)); overscroll-behavior-y: none; scrollbar-color: #7891a5 transparent; scrollbar-width: thin; }
.tasktree-child-viewport.scrollable { overflow-y: auto; box-shadow: inset 0 1px 0 rgb(18 53 76 / 12%), inset 0 -1px 0 rgb(18 53 76 / 10%); }
.tasktree-child-viewport.has-hidden-above:not(.has-hidden-below) { box-shadow: inset 0 14px 12px -14px rgb(20 43 58 / 58%); }
.tasktree-child-viewport.has-hidden-below:not(.has-hidden-above) { box-shadow: inset 0 -14px 12px -14px rgb(20 43 58 / 52%); }
.tasktree-child-viewport.has-hidden-above.has-hidden-below { box-shadow: inset 0 14px 12px -14px rgb(20 43 58 / 58%), inset 0 -14px 12px -14px rgb(20 43 58 / 52%); }
.tasktree-child-viewport::-webkit-scrollbar { width: 9px; }
.tasktree-child-viewport::-webkit-scrollbar-track { background: transparent; }
.tasktree-child-viewport::-webkit-scrollbar-thumb { border: 2px solid transparent; border-radius: 999px; background: #7891a5; background-clip: padding-box; }
.tasktree-child-viewport::-webkit-scrollbar-thumb:hover { background: #526f86; background-clip: padding-box; }
.tasktree-statusbar { min-width: 0; min-height: 28px; padding: 0 10px; display: flex; align-items: center; gap: 0; overflow-x: auto; border-top: 1px solid var(--border-color); background: var(--surface-secondary); color: var(--text-secondary); font-family: var(--console-font-mono); font-size: 10px; white-space: nowrap; scrollbar-width: thin; }
.tasktree-statusbar span { display: inline-flex; align-items: center; gap: 4px; }
.tasktree-statusbar span + span::before { margin: 0 9px; color: var(--border-color); content: "|"; }
.tasktree-statusbar strong { color: var(--text-primary); font-weight: 700; }
.tasktree-statusbar-authority { margin-left: auto; }
.tasktree-statusbar-authority::after { width: 6px; height: 6px; margin-left: 6px; border-radius: 50%; background: #1f9d67; content: ""; }
.tasktree-group-select { display: flex; align-items: center; }
.tasktree-group-select select { width: clamp(150px, 16vw, 240px); height: 30px; padding: 4px 8px; border: 1px solid var(--border-color); background: var(--surface-primary); color: var(--text-primary); }
.tasktree-refresh { width: 30px; height: 30px; min-height: 30px; padding: 0; }
:global(.console-overlay-surface.tasktree-detail-dialog) { height: 90dvh; min-height: 90dvh; max-height: 90dvh; }
:global(.console-overlay-surface.tasktree-detail-dialog .console-overlay-body) { min-height: 0; overflow: hidden; padding: 0; }

@media (max-width: 1100px) {
  .tasktree-view-layout.docked { grid-template-columns: minmax(0, 1fr) minmax(320px, 42vw); }
  .tasktree-current-group { max-width: 150px; }
  .tasktree-current-group p { display: none; }
  .tasktree-current-group strong { max-width: 150px; }
  .tasktree-timeline-tools > span { display: none; }
  .tasktree-group-select select { width: 132px; }
  .tasktree-context-copy,
  .tasktree-context-actions { gap: 6px; }
}

@media (max-width: 720px) {
  .tasktree-view-layout.docked { grid-template-columns: minmax(0, 1fr); }
  .tasktree-workspace { height: calc(100dvh - 68px); min-height: 400px; }
  .tasktree-docked-detail { height: 80dvh; min-height: 480px; }
  .tasktree-context { min-height: 82px; align-items: flex-start; flex-wrap: wrap; }
  .tasktree-current-group p { display: none; }
  .tasktree-context-actions { width: 100%; justify-content: space-between; }
  .tasktree-timeline-tools { gap: 4px; }
  .tasktree-statusbar-authority { margin-left: 0; }
  .tasktree-group-select select { width: min(42vw, 180px); }
}
</style>
