diff --git a/project-management/PJ2026-01/showcase/hwlab-first-principles/app.js b/project-management/PJ2026-01/showcase/hwlab-first-principles/app.js new file mode 100644 index 00000000..f595f5cb --- /dev/null +++ b/project-management/PJ2026-01/showcase/hwlab-first-principles/app.js @@ -0,0 +1,404 @@ +const STAGES = [ + { + title: "分析工程", + summary: "识别 TIM2 输入捕获路径与 APB 时钟倍频条件。", + log: "scan TIM2 capture path · APB1 prescaler = 2", + }, + { + title: "修改代码", + summary: "修正定时器时钟计算,并注入实板标定偏移。", + log: "patch Core/Src/frequency.c · +5 -3", + }, + { + title: "Keil 编译", + summary: "使用受控工程配置生成可追溯固件产物。", + log: "build 0 error(s), 0 warning(s) · freq.axf", + }, + { + title: "下载固件", + summary: "通过 CMSIS-DAP 将本次构建产物写入目标板。", + log: "flash 32.18 KiB · verify OK · reset target", + }, + { + title: "UART 采集", + summary: "读取固件运行状态,确认输入捕获链路稳定。", + log: "uart capture stable · sample_count = 128", + }, + { + title: "物理测量", + summary: "由 ioProbe 独立测量 FREQ_OUT,形成物理事实。", + log: "probe CH1 frequency · 999.8 Hz · error -0.02%", + }, +]; + +const EVIDENCE = [ + { + idle: "任务尚未进入受控执行。", + passed: "任务 freq-1khz-accuracy 已绑定 Workspace、Board、Probe 与目标指标。", + }, + { + idle: "尚无 Agent 执行 trace。", + active: "Agent 正在受控工程上下文中分析、修改与执行。", + passed: "Agent trace 已封存:工程分析与代码变更均可回放。", + }, + { + idle: "尚无本次构建产物。", + passed: "Keil 构建成功:0 error(s),0 warning(s),产物摘要已记录。", + }, + { + idle: "目标板尚未写入本次固件。", + passed: "CMSIS-DAP 下载与校验通过,固件摘要与构建产物一致。", + }, + { + idle: "UART 运行事实尚未采集。", + passed: "UART 连续采样 128 次,输入捕获状态稳定,无溢出。", + }, + { + idle: "ioProbe 物理测量尚未开始。", + active: "ioProbe 正在独立采集 FREQ_OUT 波形。", + passed: "ioProbe 实测 999.8 Hz,误差 −0.02%,满足 ±0.10% 阈值。", + failed: "ioProbe 实测 972.4 Hz,误差 −2.76%,超出 ±0.10% 阈值。", + }, + { + idle: "Aggregate 尚未汇总,不能保存为回归 Case。", + passed: "代码、构建、下载、运行与物理测量证据完整:Aggregate PASS。", + blocked: "物理测量未通过:Aggregate BLOCKED,阻止错误结果进入回归集。", + }, +]; + +const stageButtons = [...document.querySelectorAll("[data-stage]")]; +const evidenceButtons = [...document.querySelectorAll("[data-evidence]")]; +const app = document.querySelector("#app"); +const runButton = document.querySelector("#runButton"); +const resetButton = document.querySelector("#resetButton"); +const failureMode = document.querySelector("#failureMode"); +const saveCaseButton = document.querySelector("#saveCaseButton"); +const runState = document.querySelector("#runState"); +const stageDetail = document.querySelector("#stageDetail"); +const runLog = document.querySelector("#runLog"); +const traceCounter = document.querySelector("#traceCounter"); +const evidenceDetail = document.querySelector("#evidenceDetail"); +const actualFrequency = document.querySelector("#actualFrequency"); +const frequencyError = document.querySelector("#frequencyError"); +const scopeStatus = document.querySelector("#scopeStatus"); +const benchState = document.querySelector("#benchState"); +const uartOutput = document.querySelector("#uartOutput"); +const uartIndicator = document.querySelector("#uartIndicator"); +const toast = document.querySelector("#toast"); +const scopeCanvas = document.querySelector("#scopeCanvas"); +const scopeContext = scopeCanvas.getContext("2d"); + +const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)"); +let runToken = 0; +let currentState = "idle"; +let selectedEvidence = -1; +let selectedStage = -1; +let scopeAnimationFrame = 0; +let toastTimer = 0; + +function delay(milliseconds, token) { + const duration = reducedMotion.matches ? Math.min(milliseconds, 120) : milliseconds; + return new Promise((resolve) => { + window.setTimeout(() => resolve(token === runToken), duration); + }); +} + +function setRunState(state, label) { + currentState = state; + app.dataset.runState = state; + runState.textContent = label; + runButton.disabled = state === "running"; + failureMode.disabled = state === "running"; + saveCaseButton.disabled = state !== "passed"; + + if (state === "running" && !scopeAnimationFrame) { + animateScope(); + } +} + +function setStageStatus(index, status) { + const button = stageButtons[index]; + if (!button) return; + if (status === "idle") { + button.removeAttribute("data-status"); + return; + } + button.dataset.status = status; +} + +function setEvidenceStatus(index, status, label) { + const button = evidenceButtons[index]; + if (!button) return; + if (status === "idle") { + button.removeAttribute("data-status"); + } else { + button.dataset.status = status; + } + if (label) { + button.querySelector("span").textContent = label; + } + if (selectedEvidence === index) { + showEvidence(index); + } +} + +function selectStage(index) { + selectedStage = index; + stageButtons.forEach((button, buttonIndex) => { + button.classList.toggle("is-selected", buttonIndex === index); + }); + + const stage = STAGES[index]; + if (!stage) { + stageDetail.innerHTML = ` + READY +

等待开始工程闭环

+

执行过程将绑定代码、构建、下载、串口与物理测量事实。

+ `; + return; + } + + const status = stageButtons[index].dataset.status || "idle"; + const statusLabel = { + idle: "PENDING", + active: "RUNNING", + passed: "VERIFIED", + failed: "FAILED", + }[status]; + + stageDetail.innerHTML = ` + ${String(index + 1).padStart(2, "0")} · ${statusLabel} +

${stage.title}

+

${stage.summary}

+ `; +} + +function showEvidence(index) { + selectedEvidence = index; + evidenceButtons.forEach((button, buttonIndex) => { + button.classList.toggle("is-selected", buttonIndex === index); + }); + + const status = evidenceButtons[index]?.dataset.status || "idle"; + const detail = EVIDENCE[index]?.[status] || EVIDENCE[index]?.idle; + evidenceDetail.textContent = detail || "暂无证据详情。"; +} + +function appendLog(message, tone = "") { + const line = document.createElement("span"); + line.className = tone ? `log-${tone}` : ""; + line.textContent = `${runLog.textContent.trim() ? "\n" : ""}${message}`; + runLog.append(line); + runLog.scrollTop = runLog.scrollHeight; +} + +function resetDemo({ keepFailure = true } = {}) { + runToken += 1; + window.cancelAnimationFrame(scopeAnimationFrame); + scopeAnimationFrame = 0; + if (!keepFailure) failureMode.checked = false; + + stageButtons.forEach((button) => { + button.removeAttribute("data-status"); + button.classList.remove("is-selected"); + }); + evidenceButtons.forEach((button, index) => { + button.removeAttribute("data-status"); + button.classList.remove("is-selected"); + if (index === 5) button.querySelector("span").textContent = "ioProbe 待测"; + if (index === 6) button.querySelector("span").textContent = "Aggregate 待定"; + }); + + selectedStage = -1; + selectedEvidence = -1; + traceCounter.textContent = "0 / 6"; + runLog.innerHTML = '$ hwlab case run freq-1khz-accuracy\n 尚未执行'; + evidenceDetail.textContent = "选择证据节点查看边界事实。"; + actualFrequency.innerHTML = "— Hz"; + frequencyError.textContent = "—"; + scopeStatus.textContent = "等待采集"; + benchState.textContent = "设备待命"; + uartOutput.textContent = "[idle] waiting for firmware\n[idle] measurement channel ready"; + uartIndicator.classList.remove("is-active"); + setRunState("idle", "等待指令"); + selectStage(-1); + drawScope(0); +} + +async function runDemo() { + if (currentState === "running") return; + + resetDemo(); + const token = runToken; + const shouldFail = failureMode.checked; + setRunState("running", "闭环执行中"); + benchState.textContent = "HWPOD 已锁定"; + scopeStatus.textContent = "同步设备"; + runLog.innerHTML = ""; + appendLog("$ hwlab case run freq-1khz-accuracy", "muted"); + setEvidenceStatus(0, "passed"); + setEvidenceStatus(1, "active"); + + for (let index = 0; index < STAGES.length; index += 1) { + if (token !== runToken) return; + + setStageStatus(index, "active"); + selectStage(index); + traceCounter.textContent = `${index} / 6`; + appendLog(`→ ${STAGES[index].title}`, "active"); + + if (index === 3) benchState.textContent = "正在写入固件"; + if (index === 4) { + benchState.textContent = "固件运行中"; + uartIndicator.classList.add("is-active"); + uartOutput.textContent = "[boot] 71-FREQ rev.C\n[capture] timer input ready"; + } + if (index === 5) { + benchState.textContent = "ioProbe 采集中"; + scopeStatus.textContent = "采集中"; + setEvidenceStatus(5, "active", "ioProbe 采集中"); + } + + const shouldContinue = await delay(index === 5 ? 850 : 650, token); + if (!shouldContinue) return; + + if (index === 5 && shouldFail) { + setStageStatus(index, "failed"); + appendLog("probe CH1 frequency · 972.4 Hz · outside tolerance", "fail"); + traceCounter.textContent = "5 / 6"; + setEvidenceStatus(5, "failed", "ioProbe 972.4 Hz"); + setEvidenceStatus(6, "blocked", "Aggregate BLOCKED"); + actualFrequency.innerHTML = "972.4 Hz"; + frequencyError.textContent = "−2.76%"; + scopeStatus.textContent = "阈值超限"; + benchState.textContent = "物理验证未通过"; + uartOutput.textContent = "[capture] stable, samples=128\n[result] freq=972.4Hz, tolerance=FAIL"; + setRunState("failed", "证据阻断"); + selectStage(index); + showEvidence(6); + drawScope(0); + return; + } + + setStageStatus(index, "passed"); + appendLog(STAGES[index].log, "pass"); + traceCounter.textContent = `${index + 1} / 6`; + selectStage(index); + + if (index === 1) setEvidenceStatus(1, "passed"); + if (index === 2) setEvidenceStatus(2, "passed"); + if (index === 3) setEvidenceStatus(3, "passed"); + if (index === 4) { + setEvidenceStatus(4, "passed"); + uartOutput.textContent = "[capture] stable, samples=128\n[timer] clock=72000000Hz, overflow=0"; + } + } + + setEvidenceStatus(5, "passed", "ioProbe 999.8 Hz"); + setEvidenceStatus(6, "passed", "Aggregate PASS"); + actualFrequency.innerHTML = "999.8 Hz"; + frequencyError.textContent = "−0.02%"; + scopeStatus.textContent = "测量通过"; + benchState.textContent = "验证完成 · PASS"; + uartOutput.textContent = "[capture] stable, samples=128\n[result] freq=999.8Hz, tolerance=PASS"; + appendLog("verdict Aggregate PASS · evidence sealed", "pass"); + setRunState("passed", "Aggregate PASS"); + showEvidence(6); + drawScope(0); +} + +function showToast(message) { + window.clearTimeout(toastTimer); + toast.textContent = message; + toast.classList.add("is-visible"); + toastTimer = window.setTimeout(() => toast.classList.remove("is-visible"), 2600); +} + +function drawScope(phase = 0) { + const bounds = scopeCanvas.getBoundingClientRect(); + const ratio = Math.min(window.devicePixelRatio || 1, 2); + const width = Math.max(1, Math.round(bounds.width * ratio)); + const height = Math.max(1, Math.round(bounds.height * ratio)); + + if (scopeCanvas.width !== width || scopeCanvas.height !== height) { + scopeCanvas.width = width; + scopeCanvas.height = height; + } + + scopeContext.setTransform(ratio, 0, 0, ratio, 0, 0); + const cssWidth = width / ratio; + const cssHeight = height / ratio; + scopeContext.clearRect(0, 0, cssWidth, cssHeight); + scopeContext.fillStyle = "#0c1418"; + scopeContext.fillRect(0, 0, cssWidth, cssHeight); + + scopeContext.strokeStyle = "rgba(94, 132, 143, 0.18)"; + scopeContext.lineWidth = 1; + for (let x = 0; x <= cssWidth; x += cssWidth / 10) { + scopeContext.beginPath(); + scopeContext.moveTo(x, 0); + scopeContext.lineTo(x, cssHeight); + scopeContext.stroke(); + } + for (let y = 0; y <= cssHeight; y += cssHeight / 4) { + scopeContext.beginPath(); + scopeContext.moveTo(0, y); + scopeContext.lineTo(cssWidth, y); + scopeContext.stroke(); + } + + const amplitude = cssHeight * 0.27; + const center = cssHeight * 0.5; + const period = currentState === "failed" ? 51 : 43; + const waveColor = currentState === "failed" ? "#ff766e" : "#53dce1"; + const muted = currentState === "idle"; + + scopeContext.beginPath(); + for (let x = 0; x <= cssWidth; x += 2) { + const signal = muted + ? Math.sin(x / 25) * 1.2 + : Math.sin((x + phase) * ((Math.PI * 2) / period)) * amplitude; + const noise = currentState === "running" ? Math.sin((x + phase * 1.9) / 5) * 0.8 : 0; + const y = center + signal + noise; + if (x === 0) scopeContext.moveTo(x, y); + else scopeContext.lineTo(x, y); + } + scopeContext.strokeStyle = muted ? "#496169" : waveColor; + scopeContext.lineWidth = 1.4; + scopeContext.shadowColor = muted ? "transparent" : waveColor; + scopeContext.shadowBlur = muted ? 0 : 5; + scopeContext.stroke(); + scopeContext.shadowBlur = 0; +} + +function animateScope(timestamp = 0) { + if (currentState !== "running") { + scopeAnimationFrame = 0; + return; + } + drawScope(timestamp * 0.08); + scopeAnimationFrame = window.requestAnimationFrame(animateScope); +} + +stageButtons.forEach((button, index) => { + button.addEventListener("click", () => selectStage(index)); +}); + +evidenceButtons.forEach((button, index) => { + button.addEventListener("click", () => showEvidence(index)); +}); + +runButton.addEventListener("click", runDemo); +resetButton.addEventListener("click", () => resetDemo({ keepFailure: true })); +failureMode.addEventListener("change", () => { + if (currentState !== "idle") resetDemo({ keepFailure: true }); + showToast(failureMode.checked ? "失败演示已开启:物理测量将阻断聚合结果" : "已恢复通过路径"); +}); +saveCaseButton.addEventListener("click", () => { + if (currentState !== "passed") return; + showToast("回归 Case 已保存:freq-1khz-accuracy / evidence sealed"); +}); + +window.addEventListener("resize", () => drawScope(0)); +window.addEventListener("load", () => drawScope(0)); +resetDemo({ keepFailure: false }); diff --git a/project-management/PJ2026-01/showcase/hwlab-first-principles/assets/hwpod-bench.png b/project-management/PJ2026-01/showcase/hwlab-first-principles/assets/hwpod-bench.png new file mode 100644 index 00000000..bdfe773c Binary files /dev/null and b/project-management/PJ2026-01/showcase/hwlab-first-principles/assets/hwpod-bench.png differ diff --git a/project-management/PJ2026-01/showcase/hwlab-first-principles/index.html b/project-management/PJ2026-01/showcase/hwlab-first-principles/index.html new file mode 100644 index 00000000..5dba0a94 --- /dev/null +++ b/project-management/PJ2026-01/showcase/hwlab-first-principles/index.html @@ -0,0 +1,251 @@ + + + + + + + HWLAB · 真实硬件研发闭环 + + + +
+
+
+ + HWLAB + + 云端真实硬件研发闭环 +
+ +
+ TARGET + 71-FREQ / STM32F103 +
+ +
+ NC01 · v03 + HWPOD 已占用 + Session 就绪 +
+
+ +
+ + +
+
+
+ ENGINEERING AGENT +

受控工程工作台

+
+ 等待指令 +
+ +
+ +

修复 1 kHz 输入下的频率误差,并在真实板上完成验证

+ + + +
+ +
    +
  1. +
  2. +
  3. +
  4. +
  5. +
  6. +
+ +
+
+
+
+ + +
+ +5 −3 +
+
+
118static float capture_to_hz(uint32_t ticks)
+
119{
+
120- return TIMER_CLOCK_HZ / ticks;
+
121- // APB prescaler assumed to be 1
+
120+ const uint32_t timer_clock = get_timer_clock_hz();
+
121+ const float calibrated_ticks =
+
122+ (float)ticks - capture_offset_ticks;
+
123+ return timer_clock / calibrated_ticks;
+
124}
+
125
+
126float frequency_read(void)
+
127{
+
128+ capture_offset_ticks = calibration.capture_offset;
+
129 return capture_to_hz(last_capture_ticks);
+
130}
+
+
+ + +
+
+ + +
+ + +
+ +
+ + + diff --git a/project-management/PJ2026-01/showcase/hwlab-first-principles/styles.css b/project-management/PJ2026-01/showcase/hwlab-first-principles/styles.css new file mode 100644 index 00000000..51dc3b82 --- /dev/null +++ b/project-management/PJ2026-01/showcase/hwlab-first-principles/styles.css @@ -0,0 +1,1525 @@ +:root { + color-scheme: light; + --ink: #172126; + --ink-soft: #4d5d64; + --line: #d7dfe2; + --line-strong: #b8c5ca; + --paper: #f4f7f7; + --surface: #ffffff; + --surface-muted: #edf2f3; + --cyan: #0798a4; + --cyan-dark: #087680; + --cyan-pale: #e3f5f5; + --green: #16845b; + --green-pale: #e8f5ef; + --amber: #b36a00; + --amber-pale: #fff1d8; + --red: #bd3e35; + --red-pale: #fae9e7; + --night: #10181c; + --night-2: #182328; + --night-line: #35444a; + --mono: "JetBrains Mono", "Cascadia Code", "SFMono-Regular", Consolas, monospace; + --sans: Bahnschrift, "DIN Alternate", "Noto Sans CJK SC", "Microsoft YaHei", sans-serif; +} + +* { + box-sizing: border-box; +} + +html, +body { + margin: 0; + min-width: 320px; + min-height: 100%; + background: var(--paper); + color: var(--ink); + font-family: var(--sans); +} + +body { + overflow: hidden; +} + +button, +input { + font: inherit; +} + +button { + color: inherit; +} + +button:focus-visible, +input:focus-visible, +[tabindex]:focus-visible { + outline: 2px solid #0b9da8; + outline-offset: 2px; +} + +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +.app-shell { + display: grid; + grid-template-rows: 52px minmax(0, 1fr) 138px; + width: 100vw; + height: 100dvh; + min-height: 560px; + overflow: hidden; +} + +.topbar { + display: grid; + grid-template-columns: minmax(310px, auto) minmax(250px, 1fr) auto; + align-items: center; + gap: 24px; + padding: 0 18px; + border-bottom: 1px solid var(--line-strong); + background: #f9fbfb; +} + +.brand-block, +.system-status, +.target-context, +.panel-title-row, +.hwpod-heading, +.evidence-heading, +.scope-header, +.uart-panel header, +.surface-header { + display: flex; + align-items: center; +} + +.brand-block { + min-width: 0; +} + +.brand-mark { + display: grid; + place-items: center; + width: 30px; + height: 30px; + border: 1px solid var(--ink); + background: var(--ink); + color: white; + font-family: var(--mono); + font-size: 10px; + font-weight: 800; + letter-spacing: -0.06em; +} + +.brand-name { + margin-left: 9px; + font-family: var(--mono); + font-size: 15px; + font-weight: 800; + letter-spacing: 0.08em; +} + +.brand-divider { + width: 1px; + height: 17px; + margin: 0 12px; + background: var(--line-strong); +} + +.brand-tagline { + overflow: hidden; + color: var(--ink-soft); + font-size: 12px; + white-space: nowrap; + text-overflow: ellipsis; +} + +.target-context { + min-width: 0; + gap: 10px; + justify-self: start; + font-size: 12px; +} + +.target-context strong { + overflow: hidden; + font-family: var(--mono); + font-size: 12px; + white-space: nowrap; + text-overflow: ellipsis; +} + +.context-label, +.eyebrow { + color: #728187; + font-family: var(--mono); + font-size: 9px; + font-weight: 700; + letter-spacing: 0.12em; +} + +.system-status { + gap: 8px; + justify-content: flex-end; +} + +.status-pill { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 5px 7px; + border: 1px solid var(--line); + color: #415157; + font-family: var(--mono); + font-size: 9px; + white-space: nowrap; +} + +.status-dot, +.state-dot, +.live-indicator i { + display: inline-block; + width: 6px; + height: 6px; + border-radius: 50%; + background: #8d999e; +} + +.status-pill .status-dot, +.state-dot--done { + background: var(--green); +} + +.status-pill--hardware .status-dot, +.state-dot--active { + background: var(--amber); +} + +.workspace { + display: grid; + grid-template-columns: 250px minmax(540px, 1fr) minmax(370px, 420px); + min-width: 0; + min-height: 0; + overflow: hidden; +} + +.task-panel, +.agent-panel, +.hwpod-panel { + min-width: 0; + min-height: 0; +} + +.task-panel { + display: flex; + flex-direction: column; + padding: 20px 14px 14px; + border-right: 1px solid var(--line); + background: #f7f9f9; +} + +.task-panel-heading { + position: relative; + min-height: 66px; + padding: 0 8px 14px; + border-bottom: 1px solid var(--line); +} + +.project-copy h1 { + margin: 6px 0 0; + font-family: var(--mono); + font-size: 22px; + line-height: 1; + letter-spacing: -0.04em; +} + +.project-copy p { + margin: 4px 0 0; + color: var(--ink-soft); + font-size: 12px; +} + +.project-code { + display: none; +} + +.task-list { + display: grid; + gap: 4px; + padding: 12px 0; +} + +.task-item { + display: flex; + width: 100%; + min-width: 0; + min-height: 58px; + gap: 10px; + align-items: flex-start; + padding: 9px 8px; + border: 1px solid transparent; + background: transparent; + text-align: left; + cursor: pointer; +} + +.task-item:hover { + border-color: var(--line); + background: var(--surface); +} + +.task-item--active { + border-color: #a6ced1; + background: var(--cyan-pale); + box-shadow: inset 3px 0 0 var(--cyan); +} + +.task-marker { + display: grid; + flex: 0 0 27px; + height: 27px; + place-items: center; + border: 1px solid var(--line-strong); + color: #647278; + font-family: var(--mono); + font-size: 9px; +} + +.task-item--active .task-marker { + border-color: var(--cyan); + color: var(--cyan-dark); +} + +.task-text { + display: grid; + min-width: 0; + gap: 5px; +} + +.task-text strong { + overflow: hidden; + font-size: 12px; + font-weight: 650; + line-height: 1.35; + text-overflow: ellipsis; +} + +.task-text small { + display: flex; + align-items: center; + gap: 6px; + color: #748187; + font-size: 10px; +} + +.context-facts { + margin-top: auto; + padding: 14px 8px 0; + border-top: 1px solid var(--line); +} + +.context-facts dl { + display: grid; + gap: 9px; + margin: 11px 0 0; +} + +.context-facts dl div { + min-width: 0; +} + +.context-facts dt { + color: #7b898e; + font-family: var(--mono); + font-size: 9px; +} + +.context-facts dd { + overflow: hidden; + margin: 2px 0 0; + font-family: var(--mono); + font-size: 10px; + white-space: nowrap; + text-overflow: ellipsis; +} + +.agent-panel { + display: grid; + grid-template-rows: 50px 62px 58px minmax(0, 1fr); + padding: 0 16px 14px; + background: var(--paper); +} + +.panel-title-row, +.hwpod-heading { + justify-content: space-between; +} + +.panel-title-row h2, +.hwpod-heading h2, +.evidence-heading h2 { + margin: 3px 0 0; + font-size: 15px; + line-height: 1; +} + +.run-state { + padding: 4px 7px; + border: 1px solid var(--line); + color: var(--ink-soft); + font-family: var(--mono); + font-size: 9px; +} + +.app-shell[data-run-state="running"] .run-state { + border-color: #e1b660; + background: var(--amber-pale); + color: #895000; +} + +.app-shell[data-run-state="passed"] .run-state { + border-color: #8bc7ad; + background: var(--green-pale); + color: #0d704b; +} + +.app-shell[data-run-state="failed"] .run-state { + border-color: #dda19c; + background: var(--red-pale); + color: #9f3029; +} + +.command-bar { + display: grid; + grid-template-columns: auto minmax(180px, 1fr) auto 34px auto; + align-items: center; + gap: 10px; + padding: 8px 9px; + border: 1px solid var(--line-strong); + background: var(--surface); + box-shadow: 0 2px 8px rgb(31 53 61 / 6%); +} + +.command-symbol { + display: grid; + width: 33px; + height: 33px; + place-items: center; + background: var(--ink); + color: #86edf0; + font-family: var(--mono); + font-size: 13px; + font-weight: 700; +} + +.command-bar p { + overflow: hidden; + margin: 0; + font-size: 12px; + font-weight: 650; + white-space: nowrap; + text-overflow: ellipsis; +} + +.failure-toggle { + display: flex; + align-items: center; + gap: 6px; + color: #66757a; + font-size: 10px; + cursor: pointer; + white-space: nowrap; +} + +.failure-toggle input { + position: absolute; + opacity: 0; + pointer-events: none; +} + +.toggle-track { + display: inline-flex; + width: 26px; + height: 14px; + align-items: center; + padding: 2px; + border: 1px solid var(--line-strong); + border-radius: 8px; + background: #e8edef; + transition: background 140ms ease, border-color 140ms ease; +} + +.toggle-track i { + width: 8px; + height: 8px; + border-radius: 50%; + background: #7c8a8f; + transition: transform 140ms ease, background 140ms ease; +} + +.failure-toggle input:checked + .toggle-track { + border-color: var(--red); + background: var(--red-pale); +} + +.failure-toggle input:checked + .toggle-track i { + transform: translateX(12px); + background: var(--red); +} + +.icon-button, +.run-button, +#saveCaseButton { + height: 34px; + border: 1px solid var(--line-strong); + background: var(--surface); + cursor: pointer; +} + +.icon-button { + width: 34px; + color: #536369; + font-size: 17px; +} + +.run-button { + display: inline-flex; + align-items: center; + gap: 7px; + padding: 0 14px; + border-color: #087b84; + background: var(--cyan-dark); + color: white; + font-size: 11px; + font-weight: 700; +} + +.run-button:hover:not(:disabled) { + background: #05636b; +} + +.run-button:disabled, +#saveCaseButton:disabled { + cursor: not-allowed; + opacity: 0.48; +} + +.stage-strip { + display: grid; + grid-template-columns: repeat(6, minmax(0, 1fr)); + align-items: end; + gap: 4px; + margin: 0; + padding: 8px 0 7px; + list-style: none; +} + +.stage-strip button { + position: relative; + display: grid; + width: 100%; + height: 42px; + grid-template-columns: auto minmax(0, 1fr); + align-items: center; + gap: 6px; + padding: 0 7px; + overflow: hidden; + border: 1px solid var(--line); + background: #f9fbfb; + cursor: pointer; + text-align: left; +} + +.stage-strip button:hover, +.stage-strip button.is-selected { + border-color: #8fbcc0; + background: var(--cyan-pale); +} + +.stage-strip button span { + color: #8a979b; + font-family: var(--mono); + font-size: 8px; +} + +.stage-strip button strong { + overflow: hidden; + font-size: 10px; + font-weight: 650; + white-space: nowrap; + text-overflow: ellipsis; +} + +.stage-strip button i { + position: absolute; + right: 0; + bottom: 0; + left: 0; + height: 2px; + background: #cbd4d7; +} + +.stage-strip button[data-status="active"] i { + background: var(--amber); + animation: stage-pulse 850ms ease-in-out infinite alternate; +} + +.stage-strip button[data-status="passed"] i { + background: var(--green); +} + +.stage-strip button[data-status="failed"] i { + background: var(--red); +} + +@keyframes stage-pulse { + from { opacity: 0.35; } + to { opacity: 1; } +} + +.work-surface { + display: grid; + grid-template-columns: minmax(0, 1.5fr) minmax(230px, 0.72fr); + min-height: 0; + overflow: hidden; + border: 1px solid var(--line-strong); + background: var(--surface); +} + +.editor-pane, +.trace-pane { + display: grid; + min-width: 0; + min-height: 0; + grid-template-rows: 36px minmax(0, 1fr); +} + +.editor-pane { + border-right: 1px solid var(--line); +} + +.trace-pane { + grid-template-rows: 36px auto minmax(0, 1fr); + background: #fafcfc; +} + +.surface-header { + min-width: 0; + justify-content: space-between; + border-bottom: 1px solid var(--line); + background: #f2f5f5; +} + +.surface-header > strong { + padding-left: 10px; + font-size: 10px; +} + +.surface-header > span { + padding-right: 10px; + color: #718086; + font-family: var(--mono); + font-size: 9px; +} + +.file-tabs { + display: flex; + height: 100%; + min-width: 0; +} + +.file-tab { + position: relative; + min-width: 0; + padding: 0 12px; + overflow: hidden; + border: 0; + border-right: 1px solid var(--line); + background: transparent; + color: #68777d; + font-family: var(--mono); + font-size: 9px; + text-overflow: ellipsis; + white-space: nowrap; + cursor: pointer; +} + +.file-tab--active { + background: white; + color: var(--ink); +} + +.file-tab i { + position: absolute; + top: 8px; + right: 7px; + width: 5px; + height: 5px; + border-radius: 50%; + background: var(--amber); +} + +.change-summary b, +.change-summary em { + font-style: normal; + font-weight: 700; +} + +.change-summary b { color: var(--green); } +.change-summary em { color: var(--red); } + +.code-view { + min-width: 0; + overflow: auto; + padding: 8px 0 20px; + background: #fff; + font-family: var(--mono); + font-size: 10px; + line-height: 1.7; +} + +.code-line { + display: grid; + min-width: max-content; + grid-template-columns: 43px auto; + padding-right: 14px; +} + +.code-line > span { + padding-right: 10px; + border-right: 1px solid #edf0f1; + color: #a0aaae; + text-align: right; + user-select: none; +} + +.code-line code { + padding-left: 11px; + white-space: pre; +} + +.code-line--removed { + background: var(--red-pale); + color: #8f312b; +} + +.code-line--added { + background: var(--green-pale); + color: #116844; +} + +.code-line--context { + color: #526167; +} + +.stage-detail { + padding: 12px 11px 10px; + border-bottom: 1px solid var(--line); +} + +.detail-index { + color: var(--cyan-dark); + font-family: var(--mono); + font-size: 8px; + font-weight: 700; + letter-spacing: 0.08em; +} + +.stage-detail h3 { + margin: 5px 0; + font-size: 12px; + line-height: 1.25; +} + +.stage-detail p { + margin: 0; + color: #68777c; + font-size: 10px; + line-height: 1.5; +} + +.run-log { + margin: 0; + overflow: auto; + padding: 10px 11px 16px; + background: #11191d; + color: #d4e2e5; + font-family: var(--mono); + font-size: 9px; + line-height: 1.65; + white-space: pre-wrap; +} + +.log-muted { color: #72858d; } +.log-active { color: #ffc86e; } +.log-pass { color: #6ce0ad; } +.log-fail { color: #ff8c84; } + +.hwpod-panel { + display: grid; + grid-template-rows: 50px minmax(180px, 1fr) auto auto; + padding: 0 14px 12px; + overflow: hidden; + border-left: 1px solid #27343a; + background: var(--night); + color: #eef5f6; +} + +.hwpod-heading .eyebrow { + color: #6f858d; +} + +.hwpod-heading h2 { + color: #f3f7f7; +} + +.live-indicator { + display: flex; + align-items: center; + gap: 6px; + color: #6ed2ae; + font-family: var(--mono); + font-size: 9px; +} + +.live-indicator i { + background: #40c58e; + box-shadow: 0 0 0 3px rgb(64 197 142 / 12%); +} + +.bench-view { + position: relative; + min-height: 0; + margin: 0; + overflow: hidden; + border: 1px solid var(--night-line); + background: #111a1f; +} + +.bench-view::after { + position: absolute; + inset: 0; + border: 1px solid rgb(255 255 255 / 4%); + content: ""; + pointer-events: none; +} + +.bench-view img { + display: block; + width: 100%; + height: 100%; + object-fit: cover; + object-position: center; + filter: saturate(0.88) contrast(1.04) brightness(0.78); +} + +.hardware-label, +.bench-state { + position: absolute; + z-index: 1; + display: inline-flex; + align-items: center; + gap: 5px; + padding: 4px 6px; + border: 1px solid rgb(153 210 215 / 40%); + background: rgb(11 20 24 / 78%); + color: #d3e9eb; + font-family: var(--mono); + font-size: 8px; + backdrop-filter: blur(3px); +} + +.hardware-label i { + width: 4px; + height: 4px; + border-radius: 50%; + background: #5bd6d9; +} + +.hardware-label--board { top: 16%; left: 4%; } +.hardware-label--probe { top: 40%; left: 43%; } +.hardware-label--ioprobe { right: 4%; bottom: 18%; } + +.bench-state { + right: 8px; + bottom: 8px; + border-color: rgb(255 198 103 / 48%); + color: #ffd089; +} + +.app-shell[data-run-state="passed"] .bench-state { + border-color: rgb(91 214 163 / 50%); + color: #74dfb3; +} + +.app-shell[data-run-state="failed"] .bench-state { + border-color: rgb(255 122 112 / 54%); + color: #ff8c84; +} + +.measurement-panel, +.uart-panel { + border-right: 1px solid var(--night-line); + border-bottom: 1px solid var(--night-line); + border-left: 1px solid var(--night-line); + background: var(--night-2); +} + +.measurement-panel { + padding: 9px 10px 8px; +} + +.scope-header { + justify-content: space-between; + font-family: var(--mono); + font-size: 8px; +} + +.scope-header span { color: #7d929a; } +.scope-header strong { color: #85e3e6; font-weight: 600; } + +#scopeCanvas { + display: block; + width: 100%; + height: 65px; + margin: 7px 0 8px; + border: 1px solid #304148; + background: #0c1418; +} + +.measurement-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 8px; + margin: 0; +} + +.measurement-grid div { + min-width: 0; + padding-left: 8px; + border-left: 1px solid #34454c; +} + +.measurement-grid div:first-child { + padding-left: 0; + border-left: 0; +} + +.measurement-grid dt { + color: #71878f; + font-size: 8px; +} + +.measurement-grid dd { + margin: 3px 0 0; + color: #eef6f7; + font-family: var(--mono); + font-size: 14px; + font-weight: 700; + white-space: nowrap; +} + +.measurement-grid dd small { + color: #748a92; + font-size: 8px; + font-weight: 400; +} + +.app-shell[data-run-state="passed"] #actualFrequency, +.app-shell[data-run-state="passed"] #frequencyError { + color: #62dda8; +} + +.app-shell[data-run-state="failed"] #actualFrequency, +.app-shell[data-run-state="failed"] #frequencyError { + color: #ff8178; +} + +.uart-panel { + min-height: 69px; + padding: 8px 10px; +} + +.uart-panel header { + justify-content: space-between; + color: #72868e; + font-family: var(--mono); + font-size: 8px; +} + +.uart-panel header i { + width: 6px; + height: 6px; + border-radius: 50%; + background: #596970; +} + +.uart-panel header i.is-active { + background: #58d2a1; + box-shadow: 0 0 0 3px rgb(88 210 161 / 10%); +} + +.uart-panel pre { + margin: 6px 0 0; + overflow: hidden; + color: #a9b9be; + font-family: var(--mono); + font-size: 8px; + line-height: 1.45; + white-space: pre-wrap; +} + +.evidence-band { + display: grid; + grid-template-rows: 48px minmax(0, 1fr); + min-width: 0; + padding: 0 18px 10px; + overflow: hidden; + border-top: 1px solid var(--line-strong); + background: #f8fafa; +} + +.evidence-heading { + min-width: 0; + gap: 18px; +} + +.evidence-heading > div { + flex: 0 0 180px; +} + +.evidence-heading p { + min-width: 0; + flex: 1 1 auto; + overflow: hidden; + margin: 0; + color: #67767b; + font-size: 10px; + white-space: nowrap; + text-overflow: ellipsis; +} + +#saveCaseButton { + flex: 0 0 auto; + height: 30px; + padding: 0 12px; + border-color: var(--cyan-dark); + color: var(--cyan-dark); + font-size: 10px; + font-weight: 700; +} + +#saveCaseButton:not(:disabled):hover { + background: var(--cyan-dark); + color: white; +} + +.evidence-chain { + display: grid; + grid-template-columns: repeat(7, minmax(90px, 1fr)); + min-width: 0; + margin: 0; + padding: 0; + list-style: none; +} + +.evidence-chain li { + position: relative; + min-width: 0; +} + +.evidence-chain li:not(:last-child)::after { + position: absolute; + z-index: 0; + top: 16px; + right: -50%; + width: 100%; + height: 1px; + background: #c7d1d4; + content: ""; +} + +.evidence-chain button { + position: relative; + z-index: 1; + display: grid; + width: 100%; + min-width: 0; + grid-template-columns: 12px minmax(0, auto); + grid-template-rows: 17px 14px; + align-items: center; + justify-content: center; + column-gap: 7px; + padding: 4px 5px; + border: 1px solid transparent; + background: transparent; + cursor: pointer; +} + +.evidence-chain button:hover, +.evidence-chain button.is-selected { + border-color: var(--line); + background: white; +} + +.evidence-chain button i { + grid-row: 1 / 3; + width: 10px; + height: 10px; + border: 2px solid #aab6ba; + border-radius: 50%; + background: #f8fafa; +} + +.evidence-chain button span, +.evidence-chain button small { + min-width: 0; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} + +.evidence-chain button span { + font-size: 10px; + font-weight: 700; +} + +.evidence-chain button small { + color: #879399; + font-family: var(--mono); + font-size: 8px; +} + +.evidence-chain button[data-status="active"] i { + border-color: var(--amber); + background: var(--amber-pale); + box-shadow: 0 0 0 3px rgb(179 106 0 / 10%); +} + +.evidence-chain button[data-status="passed"] i { + border-color: var(--green); + background: var(--green); + box-shadow: inset 0 0 0 2px white; +} + +.evidence-chain button[data-status="failed"] i, +.evidence-chain button[data-status="blocked"] i { + border-color: var(--red); + background: var(--red); + box-shadow: inset 0 0 0 2px white; +} + +.evidence-final button { + border-left: 1px solid var(--line-strong); +} + +.toast { + position: fixed; + z-index: 20; + right: 18px; + bottom: 154px; + max-width: min(360px, calc(100vw - 36px)); + padding: 10px 12px; + transform: translateY(8px); + border: 1px solid #65b894; + background: #10231c; + color: #dff8ed; + font-size: 11px; + opacity: 0; + pointer-events: none; + transition: opacity 160ms ease, transform 160ms ease; +} + +.toast.is-visible { + transform: translateY(0); + opacity: 1; +} + +@media (max-width: 1180px) and (min-width: 721px) { + .topbar { + grid-template-columns: auto minmax(180px, 1fr) auto; + gap: 12px; + padding: 0 10px; + } + + .brand-divider, + .brand-tagline, + .system-status .status-pill:first-child { + display: none; + } + + .workspace { + grid-template-columns: 72px minmax(0, 1fr) 320px; + } + + .task-panel { + align-items: center; + padding: 14px 7px 10px; + } + + .task-panel-heading { + display: grid; + width: 100%; + min-height: 48px; + place-items: center; + padding: 0 0 10px; + } + + .task-panel-heading .eyebrow, + .project-copy, + .task-text, + .context-facts { + display: none; + } + + .project-code { + display: grid; + width: 34px; + height: 34px; + place-items: center; + border: 1px solid var(--ink); + font-family: var(--mono); + font-size: 11px; + font-weight: 700; + } + + .task-list { + width: 100%; + } + + .task-item { + min-height: 46px; + justify-content: center; + padding: 8px 5px; + } + + .task-marker { + flex-basis: 29px; + height: 29px; + } + + .agent-panel { + padding-right: 10px; + padding-left: 10px; + } + + .failure-toggle > span:last-child { + display: none; + } + + .command-bar { + grid-template-columns: auto minmax(100px, 1fr) auto 32px auto; + gap: 6px; + padding-right: 6px; + padding-left: 6px; + } + + .command-symbol, + .icon-button { + width: 32px; + } + + .run-button { + padding: 0 9px; + } + + .work-surface { + grid-template-columns: minmax(0, 1.35fr) minmax(190px, 0.65fr); + } + + .hwpod-panel { + padding-right: 9px; + padding-left: 9px; + } + + .hardware-label--probe { + left: 36%; + } + + .measurement-grid { + gap: 4px; + } + + .measurement-grid dd { + font-size: 12px; + } + + .evidence-band { + padding-right: 10px; + padding-left: 10px; + } + + .evidence-heading > div { + flex-basis: 155px; + } +} + +@media (max-width: 900px) and (min-width: 721px) { + .system-status .status-pill:nth-child(2), + .target-context .context-label, + .file-tabs .file-tab:nth-child(2) { + display: none; + } + + .trace-pane { + grid-template-rows: 36px auto minmax(0, 1fr); + } + + .stage-detail p { + display: none; + } +} + +@media (max-width: 720px) { + body { + overflow: auto; + } + + .app-shell { + display: block; + width: 100%; + height: auto; + min-height: 100dvh; + overflow: visible; + } + + .topbar { + position: sticky; + z-index: 10; + top: 0; + display: flex; + min-height: 52px; + justify-content: space-between; + gap: 8px; + padding: 0 10px; + } + + .brand-divider, + .brand-tagline, + .target-context, + .system-status .status-pill:not(:last-child) { + display: none; + } + + .workspace { + display: flex; + flex-direction: column; + overflow: visible; + } + + .task-panel { + display: grid; + grid-template-columns: 122px minmax(0, 1fr); + gap: 10px; + padding: 14px 10px 10px; + border-right: 0; + border-bottom: 1px solid var(--line); + } + + .task-panel-heading { + min-height: 0; + padding: 0 8px; + border-bottom: 0; + border-right: 1px solid var(--line); + } + + .project-copy h1 { + font-size: 18px; + } + + .task-list { + padding: 0; + } + + .task-item { + display: none; + min-height: 50px; + } + + .task-item--active { + display: flex; + } + + .context-facts { + display: none; + } + + .agent-panel { + display: block; + padding: 0 10px 12px; + } + + .panel-title-row { + min-height: 50px; + } + + .command-bar { + grid-template-columns: auto minmax(0, 1fr) 34px; + gap: 7px; + padding: 7px; + } + + .command-bar p { + white-space: normal; + line-height: 1.35; + } + + .failure-toggle { + grid-column: 1 / 3; + grid-row: 2; + padding-left: 2px; + } + + .icon-button { + grid-column: 3; + grid-row: 2; + } + + .run-button { + grid-column: 3; + grid-row: 1; + width: 34px; + justify-content: center; + padding: 0; + font-size: 0; + } + + .run-button span { + font-size: 11px; + } + + .stage-strip { + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 5px; + } + + .work-surface { + display: block; + overflow: visible; + } + + .editor-pane, + .trace-pane { + min-height: 260px; + } + + .editor-pane { + border-right: 0; + border-bottom: 1px solid var(--line); + } + + .trace-pane { + min-height: 240px; + } + + .hwpod-panel { + display: block; + padding: 0 10px 12px; + border-left: 0; + } + + .hwpod-heading { + min-height: 52px; + } + + .bench-view { + height: 250px; + } + + #scopeCanvas { + height: 90px; + } + + .uart-panel { + min-height: 76px; + } + + .evidence-band { + display: block; + padding: 12px 10px 18px; + overflow: visible; + } + + .evidence-heading { + display: grid; + grid-template-columns: 1fr auto; + gap: 8px 12px; + } + + .evidence-heading > div { + flex-basis: auto; + } + + .evidence-heading p { + grid-column: 1 / 3; + grid-row: 2; + white-space: normal; + } + + #saveCaseButton { + grid-column: 2; + grid-row: 1; + } + + .evidence-chain { + display: grid; + grid-template-columns: 1fr; + gap: 0; + margin-top: 10px; + } + + .evidence-chain li:not(:last-child)::after { + top: 32px; + right: auto; + bottom: -8px; + left: 18px; + width: 1px; + height: 18px; + } + + .evidence-chain button { + min-height: 48px; + grid-template-columns: 18px 1fr auto; + grid-template-rows: 1fr; + justify-content: stretch; + padding: 7px 12px; + text-align: left; + } + + .evidence-chain button i { + grid-row: auto; + } + + .evidence-chain button small { + text-align: right; + } + + .evidence-final button { + border-top: 1px solid var(--line-strong); + border-left: 0; + } + + .toast { + right: 10px; + bottom: 10px; + } +} + +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + scroll-behavior: auto !important; + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + } +}