<!-- SPEC: PJ2026-0103 HarnessRL; implementation reference: draft-2026-07-18-caserun-independent-page. -->
<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, ref, watch } from "vue";
import { useRoute, useRouter } from "vue-router";
import {
  Activity,
  Archive,
  Check,
  CheckCircle2,
  Clock3,
  Clipboard,
  Cpu,
  ExternalLink,
  FileCheck2,
  FileCode2,
  GitBranch,
  History,
  ListChecks,
  RefreshCw,
  Search,
  ShieldAlert,
  TerminalSquare,
  XCircle,
} from "lucide-vue-next";
import PageCommandBar from "@/components/layout/PageCommandBar.vue";
import StatusStrip, { type ConsoleStatusItem } from "@/components/layout/StatusStrip.vue";
import StatusBadge from "@/components/common/StatusBadge.vue";
import { useCaseRunStore } from "@/stores/caserun";
import type { CaseRunArtifact, CaseRunCaseSummary, CaseRunEvent, CaseRunListItem } from "@/types";

const route = useRoute();
const router = useRouter();
const caserun = useCaseRunStore();
const selectedCaseId = ref("");
type SidebarTab = "runs" | "cases";
const sidebarTab = ref<SidebarTab>("runs");
const runLookup = ref("");
type EvidenceTab = "aggregate" | "artifacts" | "workflow" | "hwpod";
const evidenceTabs: Array<{ id: EvidenceTab; label: string }> = [
  { id: "aggregate", label: "结果" },
  { id: "artifacts", label: "产物" },
  { id: "workflow", label: "编排" },
  { id: "hwpod", label: "HWPOD" },
];
const selectedTab = ref<EvidenceTab>("aggregate");
let pollTimer: ReturnType<typeof setInterval> | null = null;

const runId = computed(() => typeof route.params.runId === "string" ? route.params.runId : "");
const currentRun = computed(() => caserun.currentRun);
const currentStatusCode = computed(() => currentRun.value?.status ?? "idle");
const currentStageCode = computed(() => currentRun.value?.stage ?? "pending-selection");
const currentStatus = computed(() => statusLabel(currentStatusCode.value));
const currentStage = computed(() => stageLabel(currentStageCode.value));
const selectedCase = computed<CaseRunCaseSummary | null>(() => caserun.cases.find((item) => item.caseId === selectedCaseId.value) ?? null);
const currentEvents = computed(() => caserun.events ?? []);
const currentBlocker = computed(() => currentRun.value?.blocker ?? null);
const aggregate = computed(() => caserun.aggregate);
const terminal = computed(() => currentRun.value?.terminal === true);
const resultSummary = computed(() => aggregate.value?.result?.summary ?? aggregate.value?.summary ?? currentRun.value?.result?.summary ?? null);
const resultEvidence = computed(() => aggregate.value?.result?.evidence ?? aggregate.value?.evidence ?? currentRun.value?.result?.evidence ?? null);
const resultRun = computed(() => aggregate.value?.result?.run ?? aggregate.value?.run ?? currentRun.value?.result?.run ?? null);
const artifacts = computed<CaseRunArtifact[]>(() => resultSummary.value?.artifacts ?? resultEvidence.value?.artifacts ?? []);
const artifactCount = computed(() => resultSummary.value?.artifactCount ?? currentRun.value?.facts?.artifactCount ?? artifacts.value.length);
const evidenceStatus = computed(() => String(resultEvidence.value?.status ?? currentRun.value?.facts?.evidenceStatus ?? "pending"));
const duration = computed(() => formatDuration(currentRun.value?.createdAt, currentRun.value?.updatedAt));
const statusTone = computed(() => {
  if (currentStatusCode.value === "completed") return "ok";
  if (["failed", "blocked"].includes(currentStatusCode.value)) return "error";
  if (["queued", "running", "cancel_requested"].includes(currentStatusCode.value)) return "running";
  return "pending";
});
const fixtureMode = computed(() => String(resultSummary.value?.mode ?? resultRun.value?.mode ?? aggregate.value?.mode ?? currentRun.value?.mode ?? selectedCase.value?.mode ?? "live"));
const fixtureModeLabel = computed(() => modeLabel(fixtureMode.value));
const statusItems = computed<ConsoleStatusItem[]>(() => [
  { id: "status", label: "当前状态", value: currentStatus.value, tone: statusTone.value === "ok" ? "ok" : statusTone.value === "error" ? "error" : "info" },
  { id: "stage", label: "当前阶段", value: currentStage.value, tone: "neutral" },
  { id: "duration", label: "运行耗时", value: duration.value, tone: "neutral" },
  { id: "events", label: "事件", value: currentEvents.value.length, tone: "neutral" },
  { id: "artifacts", label: "产物", value: artifactCount.value, tone: "neutral" },
  { id: "evidence", label: "证据", value: statusLabel(evidenceStatus.value), tone: evidenceStatus.value === "recorded" ? "ok" : "neutral" },
]);

onMounted(async () => {
  selectedCaseId.value = typeof route.query.case === "string" ? route.query.case : "";
  if (selectedCaseId.value && !runId.value) sidebarTab.value = "cases";
  selectedTab.value = parseTab(route.query.tab);
  await Promise.all([caserun.refreshCases(), caserun.refreshRuns()]);
  if (!selectedCaseId.value) selectedCaseId.value = caserun.cases[0]?.caseId ?? "";
  if (runId.value) {
    await loadRun(runId.value);
  }
  pollTimer = setInterval(() => {
    if (caserun.shouldPoll) void caserun.refreshRun();
  }, 5000);
});

onBeforeUnmount(() => {
  if (pollTimer !== null) clearInterval(pollTimer);
});

watch(() => route.params.runId, async (value) => {
  if (typeof value === "string" && value) await loadRun(value);
});

watch(() => route.query.tab, (value) => { selectedTab.value = parseTab(value); });

function parseTab(value: unknown): EvidenceTab {
  return value === "artifacts" || value === "workflow" || value === "hwpod" ? value : "aggregate";
}

async function startRun(): Promise<void> {
  if (!selectedCaseId.value) return;
  const started = await caserun.startRun(selectedCaseId.value);
  if (started?.runId) await router.push({ name: "CaseRunDetail", params: { runId: started.runId }, query: { case: selectedCaseId.value } });
}

async function refresh(): Promise<void> {
  await Promise.all([caserun.refreshCases(), caserun.refreshRuns()]);
  if (runId.value) await caserun.refreshRun(runId.value);
}

async function selectCase(caseId: string): Promise<void> {
  sidebarTab.value = "cases";
  selectedCaseId.value = caseId;
  await router.replace({ name: runId.value ? "CaseRunDetail" : "CaseRun", params: runId.value ? { runId: runId.value } : {}, query: { case: caseId, tab: selectedTab.value } });
}

async function loadRun(value: string): Promise<void> {
  runLookup.value = value;
  const loaded = await caserun.refreshRun(value);
  if (loaded && caserun.currentRun?.caseId) selectedCaseId.value = caserun.currentRun.caseId;
}

async function openRun(value = runLookup.value): Promise<void> {
  const target = value.trim();
  if (!target) return;
  sidebarTab.value = "runs";
  runLookup.value = target;
  if (target === runId.value) {
    await loadRun(target);
    return;
  }
  await router.push({ name: "CaseRunDetail", params: { runId: target } });
}

async function selectTab(tab: EvidenceTab): Promise<void> {
  selectedTab.value = tab;
  await router.replace({ query: { ...route.query, tab } });
}

async function copy(value: string): Promise<void> {
  if (value) await navigator.clipboard?.writeText(value);
}

function eventLabel(event: CaseRunEvent): string {
  return `${stageLabel(event.stage)} · ${statusLabel(event.status)}`;
}

function eventDetail(event: CaseRunEvent): string {
  const payload = event.payload ?? {};
  if (typeof payload.artifactSha256 === "string") return `产物 SHA256 · ${payload.artifactSha256}`;
  if (typeof payload.worker === "string") return `执行者 · ${payload.worker}`;
  if (typeof payload.mode === "string") return `运行模式 · ${modeLabel(payload.mode)}`;
  if (typeof payload.caseId === "string") return `用例 · ${payload.caseId}`;
  const entries = Object.entries(payload).filter(([, value]) => value !== null && value !== undefined && typeof value !== "object");
  return entries.length > 0 ? entries.map(([key, value]) => `${key}=${String(value)}`).join(" · ") : "阶段事实已记录";
}

function statusLabel(value: unknown): string {
  const code = String(value ?? "unknown");
  return ({ idle: "空闲", pending: "等待中", queued: "已排队", running: "运行中", completed: "已完成", recorded: "已记录", failed: "失败", blocked: "受阻", canceled: "已取消", cancelled: "已取消", cancel_requested: "正在取消", unknown: "未知" } as Record<string, string>)[code] ?? code;
}

function stageLabel(value: unknown): string {
  const code = String(value ?? "unknown");
  return ({ "pending-selection": "等待选择用例", queued: "进入队列", prepare: "准备运行", preparing: "准备运行", prepared: "准备完成", "agent-running": "代理执行", building: "构建中", "build-completed": "构建完成", downloading: "下载", "uart-reading": "串口读取", "collect-completed": "证据归集", aggregating: "结果汇总", completed: "运行完成", failed: "运行失败", blocked: "运行受阻", canceled: "已取消", cancelled: "已取消", unknown: "未知阶段" } as Record<string, string>)[code] ?? code;
}

function modeLabel(value: string): string {
  if (value === "native-test") return "原生测试 · native-test";
  if (value === "software-smoke") return "软件冒烟 · software-smoke";
  if (value === "live") return "真实运行 · live";
  return value;
}

function eventTime(value?: string): string {
  if (!value) return "--:--:--";
  const date = new Date(value);
  return Number.isNaN(date.getTime()) ? "--:--:--" : date.toLocaleTimeString("zh-CN", { hour12: false });
}

function historyTime(value?: string): string {
  if (!value) return "时间未知";
  const date = new Date(value);
  if (Number.isNaN(date.getTime())) return "时间未知";
  return date.toLocaleString("zh-CN", { month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", hour12: false });
}

function historyStatusClass(run: CaseRunListItem): string {
  if (run.status === "completed") return "complete";
  if (run.status === "failed" || run.status === "blocked") return "failed";
  if (run.status === "canceled") return "canceled";
  return "running";
}

function formatDuration(start?: string, end?: string): string {
  if (!start || !end) return "-";
  const elapsed = new Date(end).getTime() - new Date(start).getTime();
  if (!Number.isFinite(elapsed) || elapsed < 0) return "-";
  if (elapsed < 10_000) return `${(elapsed / 1000).toFixed(1)} 秒`;
  if (elapsed < 60_000) return `${Math.round(elapsed / 1000)} 秒`;
  return `${Math.floor(elapsed / 60_000)} 分 ${Math.round((elapsed % 60_000) / 1000)} 秒`;
}

function artifactName(artifact: CaseRunArtifact): string {
  return artifact.path || artifact.name || "未命名产物";
}

function artifactSize(bytes?: number): string {
  if (!Number.isFinite(bytes)) return "大小未知";
  if (Number(bytes) < 1024) return `${bytes} B`;
  return `${(Number(bytes) / 1024).toFixed(1)} KiB`;
}
</script>

<template>
  <section class="caserun-page" id="caserun-page">
    <div class="caserun-page-head">
      <PageCommandBar eyebrow="强化学习 / 用例运行" title="CaseRun 执行台" description="从 HWPOD 用例提交到硬件证据归档的统一运行视图。">
        <template #actions>
          <span id="caserun-mode" class="caserun-origin"><Activity :size="14" aria-hidden="true" />{{ fixtureModeLabel }}</span>
          <button id="caserun-refresh" class="btn btn-secondary" type="button" title="刷新 CaseRun" aria-label="刷新 CaseRun" @click="refresh"><RefreshCw :size="15" aria-hidden="true" />刷新</button>
          <button v-if="runId" class="btn btn-secondary" type="button" title="复制 runId" aria-label="复制 runId" @click="copy(runId)"><Clipboard :size="15" aria-hidden="true" />复制 runId</button>
        </template>
      </PageCommandBar>
      <StatusStrip :items="statusItems" label="CaseRun 状态摘要" live="polite" />
    </div>

    <div class="caserun-workspace">
      <aside class="caserun-sidebar" aria-label="CaseRun 用例选择">
        <div class="caserun-sidebar-heading">
          <div><span class="caserun-kicker">运行入口</span><h2>{{ sidebarTab === "runs" ? "最近运行" : "可执行用例" }}</h2></div>
          <span class="caserun-count">{{ sidebarTab === "runs" ? caserun.runs.length : caserun.cases.length }}</span>
        </div>
        <div class="caserun-sidebar-tabs" role="tablist" aria-label="CaseRun 运行入口">
          <button type="button" role="tab" :aria-selected="sidebarTab === 'runs'" @click="sidebarTab = 'runs'"><History :size="14" aria-hidden="true" />最近运行</button>
          <button type="button" role="tab" :aria-selected="sidebarTab === 'cases'" @click="sidebarTab = 'cases'"><ListChecks :size="14" aria-hidden="true" />可执行用例</button>
        </div>
        <template v-if="sidebarTab === 'runs'">
          <form class="caserun-run-lookup" aria-label="按 runId 加载运行" @submit.prevent="openRun()">
            <Search :size="15" aria-hidden="true" />
            <input id="caserun-run-lookup" v-model="runLookup" type="search" autocomplete="off" placeholder="输入 CLI 输出的 runId" aria-label="输入 CLI 输出的 runId" />
            <button type="submit" :disabled="!runLookup.trim()" title="加载 runId" aria-label="加载 runId">加载</button>
          </form>
          <div class="caserun-run-list">
            <button v-for="item in caserun.runs" :key="item.runId" class="caserun-run-row" :class="{ selected: item.runId === runId }" type="button" @click="openRun(item.runId)">
              <span class="run-row-top"><strong>{{ item.caseId }}</strong><span class="run-row-status" :class="historyStatusClass(item)">{{ statusLabel(item.status) }}</span></span>
              <code>{{ item.runId }}</code>
              <span class="run-row-meta"><span>{{ stageLabel(item.stage) }}</span><time>{{ historyTime(item.createdAt) }}</time><span>{{ item.artifactCount ?? 0 }} 项产物</span></span>
            </button>
            <div v-if="!caserun.runs.length && !caserun.runsLoading" class="caserun-empty">{{ caserun.runsError || "共享 registry 中暂无运行。" }}</div>
            <button v-if="caserun.nextRunsCursor" class="caserun-load-more" type="button" :disabled="caserun.runsLoading" @click="caserun.refreshRuns({ append: true })">{{ caserun.runsLoading ? "读取中" : "加载更早运行" }}</button>
          </div>
        </template>
        <template v-else>
          <div class="caserun-case-list">
            <button v-for="item in caserun.cases" :key="item.caseId" class="caserun-case-row" :class="{ selected: item.caseId === selectedCaseId }" type="button" @click="selectCase(item.caseId)">
              <span class="case-row-icon"><Cpu :size="16" aria-hidden="true" /></span>
              <span class="case-row-copy"><strong>{{ item.title || item.caseId }}</strong><small>{{ item.caseId }}</small><small>{{ item.hwpodSpec || "未声明 HWPOD 规格" }}</small></span>
              <Check v-if="item.available !== false" class="case-row-check" :size="15" aria-hidden="true" />
            </button>
            <div v-if="!caserun.cases.length && !caserun.loading" class="caserun-empty">当前入口没有可执行用例。</div>
          </div>
          <div v-if="selectedCase" class="caserun-context">
            <span class="caserun-kicker">已选上下文</span>
            <strong>{{ selectedCase.title || selectedCase.caseId }}</strong>
            <dl>
              <div><dt>用例 ID</dt><dd>{{ selectedCase.caseId }}</dd></div>
              <div><dt>运行模式</dt><dd>{{ modeLabel(selectedCase.mode || "live") }}</dd></div>
              <div><dt>HWPOD</dt><dd>{{ selectedCase.hwpodSpec || "-" }}</dd></div>
            </dl>
          </div>
          <button id="caserun-start" class="caserun-start" type="button" :disabled="!selectedCaseId || caserun.starting" @click="startRun"><TerminalSquare :size="17" aria-hidden="true" />{{ caserun.starting ? "提交中" : "启动 CaseRun" }}</button>
        </template>
      </aside>

      <main class="caserun-main">
        <section class="caserun-run-header" id="caserun-panel">
          <div class="run-heading">
            <span class="caserun-kicker">当前运行</span>
            <div class="run-title-line"><h2>{{ currentRun?.caseId || selectedCase?.caseId || "未选择运行" }}</h2><StatusBadge :status="statusTone" :label="currentStatus" /></div>
            <p id="caserun-run-id">{{ currentRun?.runId || "启动 CaseRun 后，这里会显示稳定的运行标识。" }}</p>
          </div>
          <dl class="run-facts">
            <div><dt>模式</dt><dd>{{ modeLabel(fixtureMode) }}</dd></div>
            <div><dt>耗时</dt><dd><Clock3 :size="12" aria-hidden="true" />{{ duration }}</dd></div>
            <div><dt>证据</dt><dd>{{ statusLabel(evidenceStatus) }}</dd></div>
            <div><dt>产物</dt><dd>{{ artifactCount }} 项</dd></div>
          </dl>
        </section>

        <section class="caserun-timeline" aria-label="CaseRun 执行阶段">
          <div class="section-heading"><div><span class="caserun-kicker">实际路径</span><h3>服务端阶段</h3></div><span class="timeline-note">{{ currentEvents.length }} 个事实节点 · {{ duration }}</span></div>
          <div v-if="currentEvents.length" class="stage-track" :style="{ gridTemplateColumns: `repeat(${currentEvents.length}, minmax(96px, 1fr))` }">
            <div v-for="(event, index) in currentEvents" :key="event.id ?? `${event.createdAt || event.at}-${index}`" class="stage-node reached" :class="{ active: index === currentEvents.length - 1 }">
              <span class="stage-dot"><Check :size="12" aria-hidden="true" /></span>
              <span>{{ stageLabel(event.stage) }}</span>
              <time>{{ eventTime(event.createdAt || event.at) }}</time>
            </div>
          </div>
          <div v-else class="timeline-empty">运行事件到达后，将按 API 返回顺序显示真实执行路径。</div>
          <div v-if="currentBlocker" class="caserun-blocker" role="status"><ShieldAlert :size="18" aria-hidden="true" /><div><strong>{{ currentBlocker.code }}</strong><p>{{ currentBlocker.summary }}</p><small>{{ currentBlocker.layer || "runtime" }}</small></div></div>
          <div v-else-if="caserun.error" class="caserun-error" role="alert"><XCircle :size="18" aria-hidden="true" /><span>{{ caserun.error }}</span></div>
        </section>

        <section class="caserun-events" aria-label="CaseRun 事件流">
          <div class="section-heading"><div><span class="caserun-kicker">事件流</span><h3>运行事件</h3></div><span class="timeline-note">{{ currentEvents.length }} 条事件</span></div>
          <div v-if="!currentEvents.length" class="event-empty">尚未读取事件。启动 CaseRun 或从深链载入 run 后，服务端事件会显示在这里。</div>
          <ol v-else class="event-list" id="caserun-event-text">
            <li v-for="(event, index) in currentEvents" :key="event.id ?? `${event.createdAt || event.at}-${index}`"><time>{{ eventTime(event.createdAt || event.at) }}</time><span class="event-dot" /><div><strong>{{ eventLabel(event) }}</strong><small>{{ eventDetail(event) }}</small></div></li>
          </ol>
        </section>
      </main>

      <aside class="caserun-inspector" aria-label="CaseRun 证据检查器">
        <div class="inspector-heading"><div><span class="caserun-kicker">证据</span><h2>运行证据</h2></div><Archive :size="18" aria-hidden="true" /></div>
        <div class="evidence-tabs" role="tablist" aria-label="运行证据标签页">
          <button v-for="tab in evidenceTabs" :key="tab.id" type="button" role="tab" :aria-selected="selectedTab === tab.id" @click="selectTab(tab.id)">{{ tab.label }}</button>
        </div>
        <div class="evidence-body" id="caserun-aggregate">
          <template v-if="selectedTab === 'aggregate'">
            <div class="result-banner" :class="{ complete: terminal }">
              <CheckCircle2 :size="22" aria-hidden="true" />
              <div class="result-copy"><span class="state-label">运行结果</span><strong id="caserun-aggregate-status">{{ statusLabel(aggregate?.status || currentRun?.status || "pending") }}</strong><small id="caserun-aggregate-run-id">{{ aggregate?.runId || currentRun?.runId || "等待 CaseRun" }}</small></div>
            </div>
            <dl class="evidence-facts result-facts">
              <div><dt>运行模式</dt><dd>{{ modeLabel(fixtureMode) }}</dd></div>
              <div><dt>执行耗时</dt><dd>{{ duration }}</dd></div>
              <div><dt>证据状态</dt><dd id="caserun-evidence-status">{{ statusLabel(evidenceStatus) }}</dd></div>
              <div><dt>产物数量</dt><dd>{{ artifactCount }} 项</dd></div>
              <div><dt>需要硬件</dt><dd>{{ resultRun?.hardwareRequired === false ? "否" : resultRun?.hardwareRequired === true ? "是" : "-" }}</dd></div>
            </dl>
            <div v-if="aggregate?.sha256" class="hash-row"><span>汇总 SHA256</span><button type="button" title="复制汇总 SHA" aria-label="复制汇总 SHA" @click="copy(aggregate.sha256 || '')"><code id="caserun-aggregate-sha">{{ aggregate.sha256 }}</code><Clipboard :size="14" aria-hidden="true" /></button></div>
          </template>
          <template v-else-if="selectedTab === 'artifacts'">
            <div class="evidence-section-heading"><span class="state-label">产物清单</span><strong>{{ artifactCount }} 项</strong></div>
            <div v-if="artifacts.length" class="artifact-list">
              <article v-for="artifact in artifacts" :key="artifactName(artifact)" class="artifact-row caserun-artifact-row">
                <FileCheck2 :size="17" aria-hidden="true" />
                <div><strong>{{ artifactName(artifact) }}</strong><small>{{ artifactSize(artifact.bytes) }}</small><code>{{ artifact.sha256 || "未提供 SHA256" }}</code></div>
                <button v-if="artifact.sha256" type="button" title="复制产物 SHA256" aria-label="复制产物 SHA256" @click="copy(artifact.sha256)"><Clipboard :size="14" aria-hidden="true" /></button>
              </article>
            </div>
            <div v-else class="evidence-empty">当前运行尚未登记产物。</div>
          </template>
          <template v-else-if="selectedTab === 'workflow'">
            <div class="workflow-identity"><GitBranch :size="20" aria-hidden="true" /><div><span class="state-label">Temporal Workflow</span><strong id="caserun-workflow-id">{{ currentRun?.workflowId || "暂无 workflowId" }}</strong></div></div>
            <dl class="identity-list">
              <div><dt>workflowRunId</dt><dd>{{ currentRun?.workflowRunId || "-" }}</dd></div>
              <div><dt>runId</dt><dd>{{ currentRun?.runId || "-" }}</dd></div>
              <div><dt>创建时间</dt><dd>{{ currentRun?.createdAt || "-" }}</dd></div>
              <div><dt>更新时间</dt><dd>{{ currentRun?.updatedAt || "-" }}</dd></div>
              <div v-if="currentRun?.references?.trace?.traceId"><dt>traceId</dt><dd>{{ currentRun.references.trace.traceId }}</dd></div>
            </dl>
          </template>
          <template v-else><div class="evidence-state"><span class="state-label">HWPOD 操作</span><strong>{{ currentRun?.references?.hwpod?.hwpodId || "暂无 HWPOD" }}</strong><small>节点 {{ currentRun?.references?.hwpod?.nodeId || "-" }}</small></div></template>
        </div>
        <div class="inspector-footer"><FileCode2 :size="15" aria-hidden="true" /><span>证据来自当前 CaseRun API，不在客户端合成终态。</span><ExternalLink :size="14" aria-hidden="true" /></div>
      </aside>
    </div>
  </section>
</template>

<style scoped>
.caserun-page { display: grid; height: 100%; min-height: 0; grid-template-rows: auto minmax(0, 1fr); gap: 8px; overflow: hidden; color: var(--console-graphite-950); }
.caserun-page-head { display: grid; min-width: 0; }
.caserun-page-head :deep(.page-command-bar) { min-height: 0; grid-template-columns: minmax(0, 1fr) auto; align-items: center; border-radius: var(--console-radius-sm) var(--console-radius-sm) 0 0; border-bottom: 0; padding: 7px 10px 7px 14px; box-shadow: none; }
.caserun-page-head :deep(.page-command-copy) { grid-template-columns: auto minmax(0, 1fr); align-items: baseline; gap: 2px 12px; }
.caserun-page-head :deep(.page-command-eyebrow) { grid-column: 1 / -1; }
.caserun-page-head :deep(.page-command-title) { grid-column: 1; font-size: 21px; white-space: nowrap; }
.caserun-page-head :deep(.page-command-description) { grid-column: 2; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.caserun-page-head :deep(.console-status-strip) { border-radius: 0 0 var(--console-radius-sm) var(--console-radius-sm); box-shadow: none; }
.caserun-page-head :deep(.console-status-item) { min-height: 40px; padding-block: 5px; }
.caserun-origin { display: inline-flex; align-items: center; gap: 6px; border: 1px solid var(--console-border); border-radius: var(--console-radius-sm); background: var(--console-surface-muted); padding: 7px 9px; color: var(--console-cyan-700); font-family: var(--console-font-mono); font-size: 11px; }
.caserun-workspace { display: grid; height: 100%; min-height: 0; grid-template-columns: minmax(220px, 0.78fr) minmax(420px, 1.7fr) minmax(250px, 0.92fr); overflow: hidden; overscroll-behavior: contain; border: 1px solid var(--console-border); border-radius: var(--console-radius-md); background: var(--console-surface-raised); box-shadow: var(--console-shadow-sm); }
.caserun-sidebar, .caserun-inspector { min-width: 0; background: var(--console-surface-muted); }
.caserun-sidebar { display: flex; min-height: 0; flex-direction: column; border-right: 1px solid var(--console-border); }
.caserun-inspector { display: grid; min-height: 0; grid-template-rows: auto auto minmax(0, 1fr) auto; overflow: hidden; border-left: 1px solid var(--console-border); }
.caserun-sidebar-heading, .inspector-heading { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; border-bottom: 1px solid var(--console-border); padding: 15px; }
.caserun-sidebar-heading h2, .inspector-heading h2, .run-heading h2, .section-heading h3 { margin: 3px 0 0; font-size: 15px; font-weight: 780; }
.caserun-kicker { color: var(--console-cyan-700); font-family: var(--console-font-mono); font-size: 10px; font-weight: 780; letter-spacing: .08em; }
.caserun-count { min-width: 24px; padding: 3px 7px; border: 1px solid var(--console-border); border-radius: 99px; color: var(--console-graphite-600); font-family: var(--console-font-mono); font-size: 11px; text-align: center; }
.caserun-sidebar-tabs { display: grid; grid-template-columns: repeat(2, 1fr); gap: 3px; margin: 8px; border: 1px solid var(--console-border); border-radius: var(--console-radius-sm); background: var(--console-surface-raised); padding: 3px; }
.caserun-sidebar-tabs button { display: inline-flex; min-width: 0; align-items: center; justify-content: center; gap: 6px; border: 0; border-radius: 4px; background: transparent; padding: 7px 5px; color: var(--console-graphite-600); font-size: 11px; cursor: pointer; }
.caserun-sidebar-tabs button[aria-selected="true"] { background: var(--console-cyan-100); color: var(--console-cyan-700); font-weight: 760; }
.caserun-run-lookup { display: grid; grid-template-columns: 16px minmax(0, 1fr) auto; gap: 7px; align-items: center; margin: 0 8px 7px; border: 1px solid var(--console-border); border-radius: var(--console-radius-sm); background: var(--console-surface-raised); padding: 4px 4px 4px 9px; color: var(--console-graphite-500); }
.caserun-run-lookup:focus-within { border-color: var(--console-cyan-500); box-shadow: 0 0 0 2px var(--console-cyan-100); }
.caserun-run-lookup input { width: 100%; min-width: 0; border: 0; outline: 0; background: transparent; color: var(--console-graphite-950); font-family: var(--console-font-mono); font-size: 10px; }
.caserun-run-lookup button { border: 0; border-radius: 4px; background: var(--console-cyan-700); padding: 6px 8px; color: white; font-size: 10px; cursor: pointer; }
.caserun-run-lookup button:disabled { cursor: not-allowed; opacity: .45; }
.caserun-run-list { min-height: 0; flex: 1 1 auto; overflow: auto; overscroll-behavior: contain; padding: 0 8px 8px; }
.caserun-run-row { display: grid; width: 100%; gap: 5px; border: 1px solid transparent; border-bottom-color: var(--console-border); background: transparent; padding: 10px 7px; color: inherit; text-align: left; cursor: pointer; }
.caserun-run-row:hover, .caserun-run-row.selected { border-color: var(--console-cyan-200); border-radius: var(--console-radius-sm); background: var(--console-cyan-100); }
.run-row-top, .run-row-meta { display: flex; min-width: 0; align-items: center; justify-content: space-between; gap: 7px; }
.run-row-top strong { overflow: hidden; font-size: 12px; text-overflow: ellipsis; white-space: nowrap; }
.run-row-status { flex: 0 0 auto; border-radius: 99px; padding: 2px 6px; background: var(--console-graphite-100); color: var(--console-graphite-600); font-size: 9px; }
.run-row-status.complete { background: var(--console-green-100); color: var(--console-green-700); }
.run-row-status.failed { background: var(--console-red-100); color: var(--console-red-700); }
.run-row-status.running { background: var(--console-cyan-100); color: var(--console-cyan-700); }
.caserun-run-row code { overflow: hidden; color: var(--console-graphite-600); font-family: var(--console-font-mono); font-size: 9px; text-overflow: ellipsis; white-space: nowrap; }
.run-row-meta { justify-content: flex-start; color: var(--console-graphite-500); font-family: var(--console-font-mono); font-size: 9px; }
.run-row-meta time { margin-left: auto; }
.caserun-load-more { width: 100%; border: 0; background: transparent; padding: 10px; color: var(--console-cyan-700); font-size: 10px; cursor: pointer; }
.caserun-case-list { min-height: 0; flex: 1 1 auto; overflow: auto; overscroll-behavior: contain; padding: 8px; }
.caserun-case-row { display: grid; width: 100%; grid-template-columns: 28px minmax(0, 1fr) 16px; gap: 8px; align-items: start; border: 1px solid transparent; border-radius: var(--console-radius-sm); background: transparent; padding: 10px 8px; color: inherit; text-align: left; cursor: pointer; }
.caserun-case-row:hover, .caserun-case-row.selected { border-color: var(--console-cyan-200); background: var(--console-cyan-100); }
.case-row-icon { display: grid; width: 28px; height: 28px; place-items: center; border-radius: 6px; background: var(--console-surface-raised); color: var(--console-cyan-700); }
.case-row-copy { display: grid; min-width: 0; gap: 2px; }
.case-row-copy strong, .case-row-copy small { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.case-row-copy strong { font-size: 12px; }
.case-row-copy small { color: var(--console-graphite-600); font-family: var(--console-font-mono); font-size: 10px; }
.case-row-check { margin-top: 5px; color: var(--console-green-700); }
.caserun-context { display: grid; gap: 8px; margin: 8px; border-top: 1px solid var(--console-border); padding: 13px 4px 4px; }
.caserun-context strong { font-size: 13px; }
.caserun-context dl, .run-facts, .evidence-facts { display: grid; gap: 6px; margin: 0; }
.caserun-context dl div, .run-facts div, .evidence-facts div { display: flex; justify-content: space-between; gap: 8px; }
.caserun-context dt, .run-facts dt, .evidence-facts dt { color: var(--console-graphite-500); font-family: var(--console-font-mono); font-size: 10px; text-transform: uppercase; }
.caserun-context dd, .run-facts dd, .evidence-facts dd { margin: 0; overflow: hidden; color: var(--console-graphite-800); font-family: var(--console-font-mono); font-size: 11px; text-overflow: ellipsis; white-space: nowrap; }
.caserun-start { display: inline-flex; min-height: 40px; align-items: center; justify-content: center; gap: 8px; margin: auto 12px 13px; border: 1px solid var(--console-cyan-700); border-radius: var(--console-radius-sm); background: var(--console-cyan-700); color: white; font-weight: 760; cursor: pointer; }
.caserun-start:disabled { cursor: not-allowed; opacity: .5; }
.caserun-main { display: grid; min-width: 0; min-height: 0; grid-template-rows: auto auto minmax(0, 1fr); overflow: hidden; }
.caserun-run-header { display: flex; min-width: 0; align-items: flex-start; justify-content: space-between; gap: 18px; border-bottom: 1px solid var(--console-border); padding: 18px 20px; }
.run-heading { min-width: 0; }
.run-title-line { display: flex; flex-wrap: wrap; align-items: center; gap: 9px; }
.run-heading p { max-width: 45ch; margin: 6px 0 0; overflow: hidden; color: var(--console-graphite-500); font-family: var(--console-font-mono); font-size: 11px; text-overflow: ellipsis; white-space: nowrap; }
.run-facts { min-width: 190px; }
.caserun-timeline, .caserun-events { min-width: 0; padding: 16px 20px; }
.caserun-timeline { overflow-x: auto; border-bottom: 1px solid var(--console-border); }
.section-heading { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; }
.timeline-note { color: var(--console-graphite-500); font-family: var(--console-font-mono); font-size: 10px; }
.stage-track { display: grid; min-width: 680px; grid-template-columns: repeat(8, 1fr); margin: 25px 0 19px; }
.stage-node { position: relative; display: grid; justify-items: center; gap: 8px; color: var(--console-graphite-500); font-family: var(--console-font-mono); font-size: 10px; text-align: center; }
.stage-node::before { position: absolute; top: 7px; right: 50%; width: 100%; height: 2px; background: var(--console-border); content: ""; }
.stage-node:first-child::before { display: none; }
.stage-node.reached { color: var(--console-cyan-700); }
.stage-node.reached::before { background: var(--console-cyan-500); }
.stage-node.active { color: var(--console-graphite-950); font-weight: 780; }
.stage-dot { position: relative; z-index: 1; display: grid; width: 16px; height: 16px; place-items: center; border: 2px solid var(--console-border-strong); border-radius: 50%; background: var(--console-surface-raised); }
.stage-node.reached .stage-dot { border-color: var(--console-cyan-600); background: var(--console-cyan-600); color: white; }
.stage-node.active .stage-dot { box-shadow: 0 0 0 4px var(--console-cyan-100); }
.caserun-blocker, .caserun-error { display: flex; align-items: flex-start; gap: 10px; border: 1px solid var(--console-red-700); border-radius: var(--console-radius-sm); background: var(--console-red-100); padding: 10px 12px; color: var(--console-red-700); }
.caserun-blocker strong { font-family: var(--console-font-mono); font-size: 11px; }
.caserun-blocker p { margin: 3px 0; font-size: 12px; }
.caserun-blocker small { font-family: var(--console-font-mono); font-size: 10px; }
.caserun-events { min-height: 0; overflow: auto; overscroll-behavior: contain; }
.event-empty, .caserun-empty { color: var(--console-graphite-500); font-size: 12px; line-height: 1.6; }
.event-list { display: grid; gap: 0; margin: 13px 0 0; padding: 0; list-style: none; }
.event-list li { display: grid; grid-template-columns: 64px 12px minmax(0, 1fr); gap: 10px; align-items: start; padding: 9px 0; border-bottom: 1px solid var(--console-graphite-100); }
.event-list time, .event-list small { color: var(--console-graphite-500); font-family: var(--console-font-mono); font-size: 10px; }
.event-list li > div { display: grid; min-width: 0; gap: 3px; }
.event-list strong { font-size: 12px; }
.event-list small { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.event-dot { width: 8px; height: 8px; margin-top: 4px; border: 2px solid var(--console-cyan-600); border-radius: 50%; background: var(--console-surface-raised); }
.evidence-tabs { display: grid; grid-template-columns: repeat(4, 1fr); border-bottom: 1px solid var(--console-border); }
.evidence-tabs button { border: 0; border-bottom: 2px solid transparent; background: transparent; padding: 10px 4px; color: var(--console-graphite-500); font-family: var(--console-font-mono); font-size: 10px; cursor: pointer; }
.evidence-tabs button[aria-selected="true"] { border-bottom-color: var(--console-cyan-600); color: var(--console-cyan-700); font-weight: 780; }
.evidence-body { min-height: 0; overflow: auto; overscroll-behavior: contain; padding: 16px; }
.result-banner { display: grid; grid-template-columns: 22px minmax(0, 1fr); gap: 10px; align-items: start; border-bottom: 1px solid var(--console-border); padding-bottom: 14px; color: var(--console-graphite-600); }
.result-banner.complete { color: var(--console-green-700); }
.result-copy { display: grid; min-width: 0; gap: 4px; }
.result-copy strong { color: var(--console-graphite-950); font-size: 15px; }
.result-copy small { overflow: hidden; color: var(--console-graphite-600); font-family: var(--console-font-mono); font-size: 10px; text-overflow: ellipsis; white-space: nowrap; }
.evidence-state { display: grid; gap: 8px; }
.state-label, .hash-row > span { color: var(--console-graphite-500); font-family: var(--console-font-mono); font-size: 10px; text-transform: uppercase; }
.evidence-state strong { overflow-wrap: anywhere; font-size: 14px; }
.evidence-state small { color: var(--console-graphite-600); line-height: 1.5; overflow-wrap: anywhere; }
.evidence-section-heading { display: flex; align-items: center; justify-content: space-between; gap: 12px; border-bottom: 1px solid var(--console-border); padding-bottom: 10px; }
.evidence-section-heading strong { font-family: var(--console-font-mono); font-size: 11px; }
.artifact-list { display: grid; }
.artifact-row { display: grid; grid-template-columns: 18px minmax(0, 1fr) 28px; gap: 9px; align-items: start; border-bottom: 1px solid var(--console-border); padding: 12px 0; color: var(--console-cyan-700); }
.artifact-row > div { display: grid; min-width: 0; gap: 4px; }
.artifact-row strong { overflow: hidden; color: var(--console-graphite-950); font-size: 12px; text-overflow: ellipsis; white-space: nowrap; }
.artifact-row small { color: var(--console-graphite-500); font-family: var(--console-font-mono); font-size: 10px; }
.artifact-row code { overflow: hidden; color: var(--console-graphite-600); font-family: var(--console-font-mono); font-size: 9px; text-overflow: ellipsis; white-space: nowrap; }
.artifact-row button { display: grid; width: 28px; height: 28px; place-items: center; border: 0; background: transparent; color: var(--console-cyan-700); cursor: pointer; }
.workflow-identity { display: flex; align-items: flex-start; gap: 10px; border-bottom: 1px solid var(--console-border); padding-bottom: 14px; color: var(--console-cyan-700); }
.workflow-identity > div { display: grid; min-width: 0; gap: 5px; }
.workflow-identity strong { color: var(--console-graphite-950); font-family: var(--console-font-mono); font-size: 11px; overflow-wrap: anywhere; }
.identity-list { display: grid; gap: 0; margin: 8px 0 0; }
.identity-list div { display: grid; gap: 4px; border-bottom: 1px solid var(--console-border); padding: 9px 0; }
.identity-list dt { color: var(--console-graphite-500); font-size: 10px; }
.identity-list dd { margin: 0; color: var(--console-graphite-800); font-family: var(--console-font-mono); font-size: 10px; overflow-wrap: anywhere; }
.evidence-empty { padding: 18px 0; color: var(--console-graphite-500); font-size: 12px; }
.hash-row { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin: 20px 0; border-top: 1px solid var(--console-border); border-bottom: 1px solid var(--console-border); padding: 10px 0; }
.hash-row button { display: inline-flex; min-width: 0; align-items: center; gap: 6px; border: 0; background: transparent; color: var(--console-cyan-700); cursor: pointer; }
.hash-row code { max-width: 150px; overflow: hidden; font-family: var(--console-font-mono); font-size: 10px; text-overflow: ellipsis; }
.evidence-facts { margin-top: 18px; }
.inspector-footer { display: flex; align-items: center; gap: 7px; border-top: 1px solid var(--console-border); padding: 12px 16px; color: var(--console-graphite-500); font-size: 10px; line-height: 1.45; }
.inspector-footer svg:last-child { flex: 0 0 auto; color: var(--console-cyan-700); }
@media (max-width: 1180px) { .caserun-workspace { grid-template-columns: minmax(205px, .7fr) minmax(420px, 1.4fr); grid-template-rows: minmax(0, 1fr) auto; overflow-y: auto; } .caserun-inspector { grid-column: 1 / -1; border-top: 1px solid var(--console-border); border-left: 0; } .evidence-body { max-height: 180px; } }
@media (max-width: 760px) { .caserun-page-head :deep(.page-command-bar) { grid-template-columns: minmax(0, 1fr); padding: 9px 10px; } .caserun-page-head :deep(.page-command-copy) { display: grid; grid-template-columns: minmax(0, 1fr); gap: 2px; } .caserun-page-head :deep(.page-command-eyebrow), .caserun-page-head :deep(.page-command-title), .caserun-page-head :deep(.page-command-description) { grid-column: 1; } .caserun-page-head :deep(.page-command-description) { display: none; } .caserun-page-head :deep(.page-command-actions) { justify-content: flex-start; } .caserun-workspace { display: flex; min-height: 0; flex-direction: column; overflow-y: auto; } .caserun-sidebar, .caserun-inspector { flex: 0 0 auto; border: 0; } .caserun-sidebar { border-bottom: 1px solid var(--console-border); } .caserun-case-list { max-height: 210px; } .caserun-main { min-height: 520px; flex: 0 0 auto; } .caserun-run-header { flex-direction: column; } .run-facts { width: 100%; } .stage-track { min-width: 620px; } .caserun-inspector { border-top: 1px solid var(--console-border); } }
</style>
