diff --git a/DESIGN.md b/DESIGN.md index 59382c6..f454b25 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -32,7 +32,7 @@ Use `Segoe UI`, `Microsoft YaHei`, then system sans-serif. Numeric log paths and - Desktop application: fixed top operation bar, left module navigation, sticky filter row, one active analysis module in the main work area. - The primary desktop viewport is 1440×900 with a 960×640 minimum. Below 900px, module navigation becomes horizontal. -- Public site: generous editorial sections with a split hero, a single synthetic workbench illustration, capability rows, privacy section and installation steps. +- Public site: generous editorial sections with a split hero, a synthetic workbench illustration, and a full-width five-module interactive capability guide. On desktop the guide uses a client view beside its explanation panel; at ≤1050px the explanation moves below and focuses/reveals after a hotspot is selected. Privacy and installation sections follow. - Use spacing in a 4px-derived rhythm. Dense controls may use 8–12px gaps; sections use 24–40px; public-site sections use 64px or more. ## Components @@ -59,6 +59,7 @@ Visible columns use Chinese field names from shared metadata. Unknown fields mus - Provide semantic dialog roles for chart focus and lock background scrolling while open. - Respect `prefers-reduced-motion`; motion is short and communicates state rather than decoration. - Maintain readable contrast and never communicate anomaly severity with color alone. +- The public guide renders synthetic chart data on `Canvas`; module navigation uses `tablist`/`tab` semantics with roving keyboard focus, arrow keys and Home/End, while metric switches expose `aria-pressed`. Hotspots are keyboard-focusable; on narrow layouts, selecting one focuses and reveals the explanation panel. ## Content rules diff --git a/site/demo.js b/site/demo.js new file mode 100644 index 0000000..a0a7a38 --- /dev/null +++ b/site/demo.js @@ -0,0 +1,276 @@ +(() => { + "use strict"; + + const $ = id => document.getElementById(id); + const moduleTabs = [...document.querySelectorAll("[data-guide-module]")]; + const modulePanels = [...document.querySelectorAll("[data-guide-panel]")]; + const hotspots = [...document.querySelectorAll("[data-guide-hotspot]")]; + const moduleFirstHotspot = { + overview: "overview-review", + theory: "theory-band", + optical: "optical-endpoint", + device: "device-metric", + anomaly: "anomaly-metric" + }; + + const details = { + "overview-review": ["总览", "重点复核层", "汇总达到重点复核阈值的层。", "完成分析后确定优先检查顺序。", "进入总览,选择重点复核层查看对应记录。", "统计偏离仅提供复核线索,不代表质量结论。"], + "overview-quality": ["总览", "数据质量", "汇总缺失文件、字段异常和时间逻辑问题。", "解读趋势和复核提示前确认数据可用性。", "展开数据质量记录,按错误、警告和提示逐项核验。", "数据质量问题独立于统计复核提示。"], + "theory-band": ["理论趋势", "材料与监控方式", "按层显示 H/L 材料及 OMS/Timer 监控方式。", "核对材料切换和监控方式分布时使用。", "选择该区域,再结合层号检查连续性和切换位置。", "材料与监控方式用于分组解释,不直接表示异常。"], + "theory-rate": ["理论趋势", "理论与实际速率", "对比逐层理论速率和实际速率。", "检查速率偏差或材料分组差异时使用。", "查看同材料曲线及悬停数值,定位偏差层。", "不同材料的设定不同,应在同材料范围内比较。"], + "optical-endpoint": ["光学", "终点透过率趋势", "对比 Meas、动态 Calc 和理论 End T。", "判断全炉光学终点偏移及拟合变化时使用。", "先查看整体趋势,再进入偏移明显的单层详情。", "曲线用于确定复核范围,不替代成品光谱判定。"], + "optical-detail": ["光学", "单层详情", "关联单层 Meas/Calc 曲线、理论曲线和原始截图。", "复核特定层的终点偏差或曲线形态时使用。", "使用前一层和后一层切换,并核对同层截图。", "截图仅辅助核验,不参与统计计算。"], + "device-metric": ["设备", "设备指标", "按层显示功率、温度或真空的均值与波动。", "排查光学偏差对应的设备状态变化时使用。", "选择指标后,比较 H/L 材料曲线及变化范围。", "真空等未声明单位的字段按日志原始值显示。"], + "device-groups": ["设备", "H/L 分组统计", "分别汇总 H/L 材料的样本数、均值和范围。", "避免材料设定差异造成误判时使用。", "先选择设备指标,再对比分组统计和逐层趋势。", "分组统计描述当前炉次,不构成设备规格限值。"], + "anomaly-metric": ["异常关联", "复核指标", "显示稳健 Z 分数识别的相对偏离层。", "需要形成统计复核清单时使用。", "切换指标,查看红圈层并对照异常排名。", "样本不足、MAD 为零或字段恒定时不会检测。"], + "anomaly-correlation": ["异常关联", "相关关系", "列出光学偏差与工艺参数的 Spearman 相关。", "筛选可能共同变化的参数时使用。", "优先查看绝对相关系数较高且样本数充分的关系。", "相关不代表因果,必须结合单层和设备记录复核。"] + }; + + let activeModule = "overview"; + let activeHotspot = "overview-review"; + let selectedLayer = 96; + let deviceMetric = "power"; + let anomalyMetric = "residual"; + + function showDetail(key, reveal = false) { + const detail = details[key]; + if (!detail) return; + activeHotspot = key; + hotspots.forEach(node => node.classList.toggle("active-hotspot", node.dataset.guideHotspot === key)); + $("guideDetailModule").textContent = detail[0]; + $("guideDetailTitle").textContent = detail[1]; + $("guideDetailFunction").textContent = detail[2]; + $("guideDetailWhen").textContent = detail[3]; + $("guideDetailHow").textContent = detail[4]; + $("guideDetailNote").textContent = detail[5]; + if (reveal && window.matchMedia("(max-width: 1050px)").matches) requestAnimationFrame(() => { + const explanation = $("guideExplanation"); + explanation.focus({ preventScroll: true }); + explanation.scrollIntoView({ behavior: window.matchMedia("(prefers-reduced-motion: reduce)").matches ? "auto" : "smooth", block: "start" }); + }); + } + + function setModule(name, focusTab = false) { + activeModule = name; + moduleTabs.forEach(tab => { + const active = tab.dataset.guideModule === name; + tab.classList.toggle("active", active); + tab.setAttribute("aria-selected", String(active)); + tab.tabIndex = active ? 0 : -1; + if (active && focusTab) tab.focus(); + }); + modulePanels.forEach(panel => { + const active = panel.dataset.guidePanel === name; + panel.classList.toggle("active", active); + panel.hidden = !active; + }); + showDetail(moduleFirstHotspot[name]); + requestAnimationFrame(drawAll); + } + + moduleTabs.forEach((tab, index) => { + tab.addEventListener("click", () => setModule(tab.dataset.guideModule)); + tab.addEventListener("keydown", event => { + if (!["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", "Home", "End"].includes(event.key)) return; + event.preventDefault(); + const previous = event.key === "ArrowLeft" || event.key === "ArrowUp"; + const nextIndex = event.key === "Home" ? 0 : event.key === "End" ? moduleTabs.length - 1 : (index + (previous ? moduleTabs.length - 1 : 1)) % moduleTabs.length; + setModule(moduleTabs[nextIndex].dataset.guideModule, true); + }); + }); + + hotspots.forEach(node => { + node.addEventListener("click", event => { + if (event.target.closest("#guideLayerPrev, #guideLayerNext")) return; + showDetail(node.dataset.guideHotspot, true); + }); + node.addEventListener("keydown", event => { + if (event.key !== "Enter" && event.key !== " ") return; + if (event.currentTarget !== event.target && event.target.closest("button")) return; + event.preventDefault(); + showDetail(node.dataset.guideHotspot, true); + }); + }); + + function frame(canvas) { + const rect = canvas.getBoundingClientRect(); + if (!rect.width || !rect.height) return null; + const ratio = window.devicePixelRatio || 1; + canvas.width = Math.round(rect.width * ratio); + canvas.height = Math.round(rect.height * ratio); + const context = canvas.getContext("2d"); + context.setTransform(ratio, 0, 0, ratio, 0, 0); + return { context, width: rect.width, height: rect.height }; + } + + function lineChart(id, series, options = {}) { + const canvas = $(id); + const target = canvas && frame(canvas); + if (!target) return; + const { context, width, height } = target; + const pad = { left: 34, right: 10, top: 24, bottom: 18 }; + const values = series.flatMap(item => item.values); + const min = options.min ?? Math.min(...values); + const max = options.max ?? Math.max(...values); + const span = max - min || 1; + context.clearRect(0, 0, width, height); + context.font = '9px "Segoe UI", sans-serif'; + context.fillStyle = "#718290"; + context.strokeStyle = "#e1e8ed"; + context.lineWidth = 1; + for (let i = 0; i < 4; i += 1) { + const y = pad.top + (height - pad.top - pad.bottom) * i / 3; + context.beginPath(); context.moveTo(pad.left, y); context.lineTo(width - pad.right, y); context.stroke(); + const value = max - span * i / 3; + context.fillText(options.format ? options.format(value) : value.toFixed(2), 2, y + 3); + } + const point = (value, index, count) => ({ + x: pad.left + (width - pad.left - pad.right) * index / Math.max(1, count - 1), + y: pad.top + (height - pad.top - pad.bottom) * (max - value) / span + }); + series.forEach((item, seriesIndex) => { + const points = item.values.map((value, index) => point(value, index, item.values.length)); + context.strokeStyle = item.color; + context.lineWidth = item.width || 2; + context.beginPath(); + points.forEach((current, index) => { + if (!index) context.moveTo(current.x, current.y); + else if (options.smooth) { + const previous = points[index - 1]; + context.quadraticCurveTo(previous.x, previous.y, (previous.x + current.x) / 2, (previous.y + current.y) / 2); + if (index === points.length - 1) context.lineTo(current.x, current.y); + } else context.lineTo(current.x, current.y); + }); + context.stroke(); + if (item.points) { + context.fillStyle = item.color; + points.forEach(current => { context.beginPath(); context.arc(current.x, current.y, 2.4, 0, Math.PI * 2); context.fill(); }); + } + context.fillStyle = item.color; + context.fillRect(pad.left + seriesIndex * 78, 7, 13, 2); + context.fillStyle = "#586b79"; + context.fillText(item.name, pad.left + 18 + seriesIndex * 78, 10); + }); + (options.alerts || []).forEach(index => { + const value = series[0].values[index]; + const current = point(value, index, series[0].values.length); + context.strokeStyle = "#b42318"; + context.lineWidth = 2; + context.beginPath(); context.arc(current.x, current.y, 6, 0, Math.PI * 2); context.stroke(); + }); + } + + function barChart(id, entries) { + const canvas = $(id); + const target = canvas && frame(canvas); + if (!target) return; + const { context, width, height } = target; + context.clearRect(0, 0, width, height); + context.font = '9px "Segoe UI", sans-serif'; + const left = 82; + const max = Math.max(...entries.map(entry => entry.value)); + entries.forEach((entry, index) => { + const y = 18 + index * ((height - 34) / entries.length); + const barHeight = Math.min(18, (height - 44) / entries.length - 5); + context.fillStyle = "#586b79"; + context.fillText(entry.label, 4, y + barHeight - 3); + context.fillStyle = "#dce6ec"; + context.fillRect(left, y, width - left - 14, barHeight); + context.fillStyle = "#075e91"; + context.fillRect(left, y, (width - left - 14) * entry.value / max, barHeight); + context.fillStyle = "#063b5a"; + context.fillText(entry.value.toFixed(2), width - 32, y + barHeight - 3); + }); + } + + const overviewH = [0.22, 0.35, 0.18, 0.49, 0.31, 0.58, 0.26, 0.43, 0.19, 0.36, 0.28, 0.51]; + const overviewL = [-0.18, -0.31, -0.24, -0.42, -0.21, -0.35, -0.15, -0.29, -0.45, -0.22, -0.34, -0.18]; + const thickness = [121, 78, 118, 82, 124, 76, 119, 84, 122, 79, 117, 81]; + const rateTheory = [0.62, 0.48, 0.61, 0.47, 0.63, 0.49, 0.61, 0.48, 0.62, 0.47, 0.63, 0.48]; + const rateActual = [0.60, 0.50, 0.59, 0.46, 0.64, 0.47, 0.62, 0.49, 0.60, 0.45, 0.65, 0.47]; + const opticalMeas = [0.43, 0.46, 0.44, 0.49, 0.47, 0.52, 0.48, 0.54, 0.50, 0.53, 0.49, 0.55]; + const opticalCalc = [0.42, 0.45, 0.45, 0.48, 0.48, 0.50, 0.49, 0.52, 0.51, 0.52, 0.50, 0.53]; + const opticalTheory = [0.44, 0.44, 0.46, 0.46, 0.48, 0.48, 0.50, 0.50, 0.51, 0.51, 0.52, 0.52]; + + const deviceData = { + power: { title: "功率均值逐层趋势", unit: "日志原始值", h: [5.8, 6.0, 5.9, 6.2, 6.1, 6.3, 6.0, 6.2, 6.1, 6.4, 6.2, 6.3], l: [4.1, 4.2, 4.0, 4.3, 4.2, 4.4, 4.1, 4.3, 4.2, 4.5, 4.3, 4.4] }, + temperature: { title: "腔体温度均值逐层趋势", unit: "°C", h: [214, 216, 218, 217, 219, 221, 220, 222, 221, 223, 224, 223], l: [213, 214, 216, 215, 217, 219, 218, 220, 219, 221, 222, 221] }, + vacuum: { title: "真空均值逐层趋势", unit: "日志原始值", h: [2.2, 2.3, 2.1, 2.4, 2.2, 2.5, 2.3, 2.4, 2.2, 2.5, 2.4, 2.3], l: [2.0, 2.1, 1.9, 2.2, 2.0, 2.3, 2.1, 2.2, 2.0, 2.3, 2.2, 2.1] } + }; + const anomalyData = { + residual: { title: "终点拟合残差", values: [0.11, 0.14, 0.09, 0.18, 0.12, 0.61, 0.16, 0.13, 0.10, 0.17, 0.48, 0.12], alerts: [5, 10] }, + rate: { title: "实际速率偏差", values: [0.01, -0.02, 0.02, -0.01, 0.03, 0.09, -0.02, 0.01, -0.03, 0.02, -0.08, 0.01], alerts: [5, 10] } + }; + + function drawLayer() { + const shift = (selectedLayer - 96) * 0.008; + const meas = [0.18, 0.21, 0.29, 0.42, 0.58, 0.69, 0.63, 0.51, 0.44, 0.47, 0.55, 0.61].map(value => value + shift); + const calc = [0.17, 0.22, 0.31, 0.43, 0.56, 0.66, 0.62, 0.52, 0.45, 0.48, 0.54, 0.59].map(value => value + shift / 2); + lineChart("guideLayerCanvas", [{ name: "Meas", values: meas, color: "#075e91" }, { name: "动态 Calc", values: calc, color: "#c5691a" }], { min: 0.1, max: 0.75, smooth: true }); + } + + function drawDevice() { + const item = deviceData[deviceMetric]; + $("guideDeviceTitle").textContent = item.title; + $("guideDeviceUnit").textContent = item.unit; + lineChart("guideDeviceCanvas", [{ name: "H 材料", values: item.h, color: "#075e91", points: true }, { name: "L 材料", values: item.l, color: "#c5691a", points: true }], { smooth: true }); + const row = (name, values) => { + const mean = values.reduce((sum, value) => sum + value, 0) / values.length; + return `${name}${values.length}${mean.toFixed(2)}${Math.min(...values).toFixed(2)}-${Math.max(...values).toFixed(2)}`; + }; + $("guideDeviceStats").innerHTML = row("H 材料", item.h) + row("L 材料", item.l); + } + + function drawAnomaly() { + const item = anomalyData[anomalyMetric]; + $("guideAnomalyTitle").textContent = item.title; + lineChart("guideAnomalyCanvas", [{ name: item.title, values: item.values, color: "#075e91", points: true }], { alerts: item.alerts, smooth: true }); + barChart("guideCorrelationCanvas", [{ label: "时间偏差", value: 0.71 }, { label: "实际速率", value: 0.64 }, { label: "功率均值", value: 0.58 }, { label: "腔体温度", value: 0.49 }]); + } + + function drawAll() { + lineChart("guideOverviewCanvas", [{ name: "H 材料", values: overviewH, color: "#075e91" }, { name: "L 材料", values: overviewL, color: "#c5691a" }], { min: -0.6, max: 0.7, alerts: [5, 10] }); + lineChart("guideThicknessCanvas", [{ name: "物理厚度", values: thickness, color: "#075e91", points: true }], { min: 60, max: 135, smooth: true }); + lineChart("guideRateCanvas", [{ name: "理论速率", values: rateTheory, color: "#075e91", points: true }, { name: "实际速率", values: rateActual, color: "#c5691a", points: true }], { min: 0.4, max: 0.7, smooth: true }); + lineChart("guideOpticalCanvas", [{ name: "Meas", values: opticalMeas, color: "#075e91" }, { name: "动态 Calc", values: opticalCalc, color: "#c5691a" }, { name: "理论 End T", values: opticalTheory, color: "#6d68a8" }], { min: 0.38, max: 0.58, smooth: true }); + drawLayer(); + drawDevice(); + drawAnomaly(); + } + + function moveLayer(delta) { + selectedLayer = Math.max(95, Math.min(99, selectedLayer + delta)); + $("guideLayerLabel").textContent = `第 ${selectedLayer} 层`; + $("guideImageLayer").textContent = `Layer ${selectedLayer}`; + showDetail("optical-detail"); + drawLayer(); + } + + $("guideLayerPrev").addEventListener("click", event => { event.stopPropagation(); moveLayer(-1); }); + $("guideLayerNext").addEventListener("click", event => { event.stopPropagation(); moveLayer(1); }); + + document.querySelectorAll("[data-device-metric]").forEach(button => button.addEventListener("click", () => { + deviceMetric = button.dataset.deviceMetric; + document.querySelectorAll("[data-device-metric]").forEach(item => { + const active = item === button; + item.classList.toggle("active", active); + item.setAttribute("aria-pressed", String(active)); + }); + showDetail("device-metric"); + drawDevice(); + })); + + document.querySelectorAll("[data-anomaly-metric]").forEach(button => button.addEventListener("click", () => { + anomalyMetric = button.dataset.anomalyMetric; + document.querySelectorAll("[data-anomaly-metric]").forEach(item => { + const active = item === button; + item.classList.toggle("active", active); + item.setAttribute("aria-pressed", String(active)); + }); + showDetail("anomaly-metric"); + drawAnomaly(); + })); + + new ResizeObserver(drawAll).observe(document.querySelector(".guide-client")); + setModule(activeModule); +})(); diff --git a/site/index.html b/site/index.html index 650dc03..b88fd48 100644 --- a/site/index.html +++ b/site/index.html @@ -25,8 +25,8 @@ Log Analysis镀膜日志分析工作台 @@ -35,8 +35,8 @@
-

把一炉数百层,变成一张可追踪的工艺地图。

-

选择炉次文件夹,Log Analysis 自动关联理论、光学和设备日志,标出需要复核的层。原始数据只读,分析全程留在本机。

+

单炉次镀膜日志分析

+

自动关联逐层理论、光学与设备数据,定位需复核层。全程本地运行,原始日志只读。

下载 v0.1.0-beta.3 SHA-256 校验文件 @@ -55,7 +55,7 @@

把一炉数百层,变成一张可追踪的工艺地图。

总览理论趋势光学设备异常关联
-
示意炉次180 层工艺概览
H 96 / L 84
+
合成炉次180 层分析概览
H 96 / L 84
@@ -72,19 +72,99 @@

把一炉数百层,变成一张可追踪的工艺地图。

-
-

从文件完整性,到单层曲线。

五个模块沿同一层号组织数据,减少在多个 CSV、截图和事件日志之间来回核对。

-
-
总览

先确认这一炉是否值得信任

层数、计划与实际时间、完成状态、文件完整性和重点关注层集中呈现。

-
理论趋势

看清材料、监控方式和设定变化

物理厚度、理论速率以及理论与实际时间按层对齐。

-
光学

从全炉偏移进入单层细节

Meas、动态 Calc、理论 End T、残差、理论曲线和原始截图互相对应。

-
设备

让工艺波动回到材料分组中比较

功率、电流、电压、真空、温度、转速和气体以带点平滑曲线显示。

-
异常关联

把统计提示变成复核入口

稳健 Z 分数、相邻层突变和 Spearman 相关用于寻找线索,不输出合格判定。

+
+
+

客户端功能说明

+

切换模块并选择高亮区域,查看功能、适用场景、操作方法和结果说明。页面使用合成数据。

+
+ +
+
+
+ + Log Analysis + D:\Process\Demo-Lot + 分析完成 +
+
+ + +
+
层号 1-180材料 H / L监控方式 OMS / Timer
+ +
+

总览

完成状态、累计时间与优先复核入口

合成数据
+
+
炉号DEMO-180
+
层数180H 96 / L 84
+
完成状态已完成
+
累计镀膜20 h 18 m理论 19 h 52 m
+ + +
+
理论终点偏移概览Meas - 理论 End T(%)
层号
+
+ + + + + + + + +
+
+
+ +
-

日志留在设备旁,不去云端绕一圈。

Log Analysis 只监听本机回环地址,读取你主动选择的炉次目录。没有账号、数据库、分析追踪或上传接口。

+

本地处理,不上传生产数据

Log Analysis 仅访问用户选择的炉次目录。分析、图表与导出均在本机完成,无账号、数据库、分析追踪或上传接口。

  • 只读不改写原始日志内容和时间戳
  • 本地分析、图表和导出都在当前电脑完成
  • @@ -93,7 +173,7 @@

    把一炉数百层,变成一张可追踪的工艺地图。

-

安装测试版

当前版本是面向验证的未签名 Windows 测试版。部分安装环境的单层详情图表或原始截图可能不显示,逐层汇总和其他全炉分析页面不受影响。

+

安装 Windows 测试版

当前版本未进行代码签名,适用于功能验证。已知问题:部分环境下单层详情图表或原始截图可能不显示,其他全炉分析页面不受影响。

  1. 1
    关闭旧版本

    升级前先退出所有正在运行的 Log Analysis 窗口。

  2. @@ -116,5 +196,6 @@

    把一炉数百层,变成一张可追踪的工艺地图。

    Log Analysis本地镀膜日志分析工作台
    + diff --git a/site/styles.css b/site/styles.css index 0aeeaf8..bc8ecda 100644 --- a/site/styles.css +++ b/site/styles.css @@ -76,15 +76,90 @@ h1 { max-width: 12ch; margin: 0; font-size: clamp(3.3rem, 6vw, 6rem); line-heigh .demo-readout b { margin-left: auto; color: var(--red); } @keyframes draw-signal { to { stroke-dashoffset: 0; } } -.capabilities { padding: clamp(88px, 10vw, 150px) clamp(20px, 7vw, 120px); background: var(--surface); } -.section-intro { display: grid; grid-template-columns: 1.2fr .8fr; gap: 50px; align-items: end; max-width: 1220px; margin: 0 auto 64px; } -.section-intro h2, .privacy h2, .install h2 { max-width: 15ch; margin: 0; font-size: clamp(2.4rem, 4.5vw, 4.5rem); line-height: 1.04; letter-spacing: -.035em; text-wrap: balance; } -.section-intro p { max-width: 34rem; margin: 0; color: var(--muted); font-size: 18px; } -.capability-list { max-width: 1220px; margin: auto; border-top: 1px solid var(--line); } -.capability-list article { display: grid; grid-template-columns: 150px minmax(240px, .8fr) minmax(280px, 1fr); gap: 28px; align-items: baseline; padding: 28px 0; border-bottom: 1px solid var(--line); } -.capability-list article > span { color: var(--blue); font-size: 13px; font-weight: 800; } -.capability-list h3 { margin: 0; font-size: clamp(20px, 2vw, 27px); line-height: 1.2; } -.capability-list p { margin: 0; color: var(--muted); } +.client-guide { padding: clamp(88px, 9vw, 138px) clamp(20px, 4vw, 70px); background: var(--surface); } +.guide-intro { max-width: 1220px; margin: 0 auto 46px; } +.guide-intro h2, .privacy h2, .install h2 { max-width: 15ch; margin: 0; font-size: clamp(2.4rem, 4.5vw, 4.5rem); line-height: 1.04; letter-spacing: -.035em; text-wrap: balance; } +.guide-intro p { max-width: 46rem; margin: 22px 0 0; color: var(--muted); font-size: 18px; } +.guide-layout { display: grid; grid-template-columns: minmax(0, 1fr) 290px; gap: 18px; max-width: 1380px; margin: auto; align-items: start; } +.guide-client { min-width: 0; overflow: hidden; border: 1px solid #b9c7d2; border-radius: var(--radius); background: #f7f9fb; box-shadow: 0 24px 58px rgba(18, 43, 57, .14); } +.guide-windowbar { min-height: 58px; display: flex; align-items: center; gap: 10px; padding: 10px 14px; border-bottom: 1px solid var(--line); background: #fff; } +.guide-windowbar strong { white-space: nowrap; } +.guide-path { flex: 1; min-width: 0; margin-left: 12px; padding: 7px 10px; overflow: hidden; border: 1px solid var(--line); border-radius: 6px; color: var(--muted); font: 11px/1.2 Consolas, monospace; text-overflow: ellipsis; white-space: nowrap; } +.guide-ready { color: var(--green); font-size: 11px; font-weight: 700; white-space: nowrap; } +.guide-app { display: grid; grid-template-columns: 142px minmax(0, 1fr); min-height: 610px; } +.guide-module-nav { display: grid; align-content: start; gap: 4px; padding: 16px 10px; border-right: 1px solid var(--line); background: var(--soft); } +.guide-module-nav button { min-height: 38px; padding: 8px 11px; border: 1px solid transparent; border-radius: 6px; background: transparent; color: #405463; font: inherit; font-size: 12px; font-weight: 650; text-align: left; cursor: pointer; } +.guide-module-nav button:hover { background: rgba(255, 255, 255, .7); color: var(--blue); } +.guide-module-nav button.active { border-color: var(--line); background: #fff; color: var(--blue); box-shadow: inset 3px 0 var(--blue); } +.guide-screen-wrap { min-width: 0; } +.guide-filterbar { min-height: 45px; display: flex; align-items: center; gap: 8px; padding: 8px 16px; overflow-x: auto; border-bottom: 1px solid var(--line); background: #fff; color: var(--muted); font-size: 10px; white-space: nowrap; } +.guide-filterbar span { padding: 4px 8px; border: 1px solid var(--line); border-radius: 6px; } +.guide-screen { min-width: 0; padding: 18px; animation: guide-in .18s ease-out; } +.guide-screen[hidden] { display: none; } +@keyframes guide-in { from { opacity: .55; } to { opacity: 1; } } +.guide-page-title { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; margin-bottom: 14px; } +.guide-page-title h3 { margin: 0; font-size: 20px; line-height: 1.2; } +.guide-page-title p { margin: 3px 0 0; color: var(--muted); font-size: 11px; } +.guide-page-title > span { color: var(--muted); font-size: 10px; } +.guide-kpis { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; } +.guide-kpis > div, .guide-kpis > button { min-width: 0; min-height: 80px; display: grid; align-content: start; gap: 2px; padding: 10px; border: 1px solid var(--line); border-radius: 8px; background: #fff; color: var(--text); font: inherit; text-align: left; } +.guide-kpis > button { cursor: pointer; } +.guide-kpis span { color: var(--muted); font-size: 10px; } +.guide-kpis strong { overflow: hidden; font-size: 17px; line-height: 1.25; text-overflow: ellipsis; white-space: nowrap; } +.guide-kpis small { color: var(--muted); font-size: 9px; } +.guide-chart { min-width: 0; margin: 12px 0 0; padding: 12px; border: 1px solid var(--line); border-radius: 8px; background: #fff; } +.guide-chart figcaption { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 5px; } +.guide-chart figcaption strong { font-size: 12px; } +.guide-chart figcaption span { color: var(--muted); font-size: 9px; text-align: right; } +.guide-chart canvas { display: block; width: 100%; height: 168px; } +.guide-chart > small { display: block; color: var(--muted); font-size: 9px; text-align: right; } +.guide-chart-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; } +.guide-chart-grid .guide-chart canvas { height: 155px; } +.hotspot, [data-guide-hotspot] { transition: border-color .16s ease-out, box-shadow .16s ease-out, background-color .16s ease-out; } +.hotspot { cursor: pointer; } +.hotspot:hover, [data-guide-hotspot]:hover { border-color: #8bb8d2; } +.active-hotspot { border-color: var(--blue) !important; box-shadow: 0 0 0 2px rgba(7, 94, 145, .12); } +.material-band { width: 100%; min-height: 64px; display: grid; grid-template-columns: 150px 1.4fr 1fr 1.2fr 1fr; align-items: stretch; gap: 4px; padding: 8px; border: 1px solid var(--line); border-radius: 8px; background: #fff; color: var(--text); font: inherit; cursor: pointer; } +.material-band > span { display: flex; align-items: center; padding-left: 4px; font-size: 11px; font-weight: 700; } +.material-band i { display: grid; place-items: center; min-width: 0; border-radius: 5px; font-size: 9px; font-style: normal; font-weight: 700; } +.band-h { background: #d9eaf4; color: var(--blue-dark); } +.band-l { background: #f3dfc8; color: #824615; } +.guide-time-summary { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin-top: 10px; } +.guide-time-summary span { padding: 9px 10px; border: 1px solid var(--line); border-radius: 7px; background: #fff; color: var(--muted); font-size: 10px; } +.guide-time-summary strong { display: block; color: var(--text); font-size: 13px; } +.optical-endpoint canvas { height: 150px; } +.guide-layer-detail { margin-top: 10px; padding: 12px; border: 1px solid var(--line); border-radius: 8px; background: #fff; } +.guide-layer-detail > header { display: flex; align-items: center; justify-content: space-between; gap: 12px; } +.guide-layer-detail > header > div { display: flex; align-items: center; gap: 8px; } +.guide-layer-detail header strong { font-size: 12px; } +.guide-layer-detail header span { color: var(--muted); font-size: 10px; } +.guide-layer-detail header button, .guide-metric-select button { min-height: 30px; padding: 5px 9px; border: 1px solid var(--line); border-radius: 6px; background: #fff; color: #405463; font: inherit; font-size: 10px; cursor: pointer; } +.guide-layer-detail header button:hover, .guide-metric-select button:hover { border-color: #8bb8d2; color: var(--blue); } +.guide-detail-grid { display: grid; grid-template-columns: minmax(0, 1.45fr) minmax(150px, .55fr); gap: 10px; } +.guide-detail-grid .guide-chart { margin-top: 10px; } +.guide-detail-grid .guide-chart canvas { height: 118px; } +.guide-image-placeholder { min-height: 178px; display: grid; place-content: center; align-content: center; margin-top: 10px; border: 1px solid var(--line); border-radius: 8px; background: #17242e; color: #d7e5ed; text-align: center; } +.guide-image-placeholder span, .guide-image-placeholder small { color: #9fb0bb; font-size: 9px; } +.guide-image-placeholder strong { margin: 6px 0; font: 16px/1.2 Consolas, monospace; } +.guide-metric-select { display: flex; align-items: center; gap: 7px; margin-bottom: 2px; } +.guide-metric-select strong { margin-right: 4px; font-size: 11px; } +.guide-metric-select button.active { border-color: var(--blue); background: #e9f3f8; color: var(--blue); font-weight: 700; } +.device-chart canvas { height: 220px; } +.guide-stats { margin-top: 10px; padding: 12px; border: 1px solid var(--line); border-radius: 8px; background: #fff; cursor: pointer; } +.guide-stats > strong { font-size: 12px; } +.guide-stats table { width: 100%; margin-top: 7px; border-collapse: collapse; font-size: 10px; } +.guide-stats th, .guide-stats td { padding: 5px 7px; border-bottom: 1px solid #e7edf1; text-align: right; } +.guide-stats th:first-child, .guide-stats td:first-child { text-align: left; } +.guide-review-list { display: flex; gap: 8px; margin-top: 10px; } +.guide-review-list span { padding: 7px 9px; border: 1px solid var(--line); border-radius: 6px; background: #fff; color: var(--muted); font-size: 10px; } +.guide-review-list strong { color: var(--red); } +.guide-explanation { position: sticky; top: 18px; min-height: 404px; padding: 24px; border: 1px solid var(--line); border-radius: var(--radius); background: var(--blue-dark); color: #fff; } +.guide-explanation > span { color: var(--cyan); font-size: 12px; font-weight: 700; } +.guide-explanation h3 { margin: 9px 0 22px; font-size: 24px; line-height: 1.2; } +.guide-explanation dl { margin: 0; } +.guide-explanation dl div { padding: 15px 0; border-top: 1px solid rgba(255, 255, 255, .2); } +.guide-explanation dt { color: var(--cyan); font-size: 10px; font-weight: 700; } +.guide-explanation dd { margin: 4px 0 0; color: #d8e6ed; font-size: 13px; line-height: 1.55; } .privacy { display: grid; grid-template-columns: 1.05fr .95fr; gap: clamp(50px, 10vw, 150px); padding: clamp(86px, 10vw, 150px) clamp(20px, 7vw, 120px); background: var(--blue-dark); color: #fff; } .privacy > div > p { max-width: 38rem; margin: 28px 0 0; color: #c7d9e4; font-size: 18px; } @@ -111,6 +186,16 @@ footer strong { color: var(--text); } .workbench-demo { width: 100%; transform: none; } .hero-copy { max-width: 760px; } .privacy, .install { grid-template-columns: 1fr; } + .guide-layout { grid-template-columns: 1fr; } + .guide-explanation { position: static; min-height: 0; } + .guide-explanation dl { display: grid; grid-template-columns: repeat(2, 1fr); gap: 0 24px; } +} + +@media (max-width: 900px) { + .guide-app { display: block; } + .guide-module-nav { display: flex; overflow-x: auto; padding: 8px 10px; border-right: 0; border-bottom: 1px solid var(--line); } + .guide-module-nav button { flex: 0 0 auto; min-width: 92px; text-align: center; } + .guide-module-nav button.active { box-shadow: inset 0 -3px var(--blue); } } @media (max-width: 720px) { @@ -130,8 +215,17 @@ footer strong { color: var(--text); } .demo-main { padding: 14px; } .demo-readout { gap: 9px; } .demo-readout b { display: none; } - .section-intro { grid-template-columns: 1fr; gap: 24px; } - .capability-list article { grid-template-columns: 1fr; gap: 8px; } + .guide-windowbar strong, .guide-ready { display: none; } + .guide-path { margin-left: 0; } + .guide-screen { padding: 12px; } + .guide-kpis { grid-template-columns: repeat(2, minmax(0, 1fr)); } + .guide-chart-grid, .guide-detail-grid { grid-template-columns: 1fr; } + .material-band { grid-template-columns: 1fr 1fr; } + .material-band > span { grid-column: 1 / -1; } + .guide-time-summary { grid-template-columns: 1fr; } + .guide-explanation dl { grid-template-columns: 1fr; } + .guide-metric-select { align-items: stretch; flex-wrap: wrap; } + .guide-review-list { flex-direction: column; } .privacy li { grid-template-columns: 76px 1fr; } footer, footer div { align-items: flex-start; flex-direction: column; } } @@ -140,4 +234,6 @@ footer strong { color: var(--text); } html { scroll-behavior: auto; } .signal-line { animation: none; stroke-dashoffset: 0; } .text-action span { transition: none; } + .guide-screen { animation: none; } + .hotspot, [data-guide-hotspot] { transition: none; } }