feat(圆桌): 大屏多页浅色模板重写为蓝图工程图风(参考设计图)

- 浅色 palette 改玫瑰红(#E11D48)+slate 中性+白底网格,席位色序对齐设计图
- 卡片四角红色角标 + slate 网格 overlay(蓝图风);KPI 加彩色图标
- 共识竖条摘要 + 要点列表;席位卡进度条 + 风险药丸;席位专页精致化
- 新增折线图 svgLine(趋势),donut 升级镂空中心数字+图例;契约加 line 类型
- 图表保持纯 SVG(零依赖离线自包含,沙箱/新窗口/下载/分享通用,与参考源码一致)
- 深色保持深蓝 token 套同结构

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@
This commit is contained in:
董宏凯 committed 2026-06-13 10:51:10 +08:00
1 parent f3202f95b6
commit e9f44c8f0f
2 files changed
+201 -122

No files matched your search

@@ -107,9 +107,10 @@ test("renderDashboardHtml:主题跟随 —— dark=深蓝规范 / light=浅红
assert.ok(dark.includes("#122347"));
assert.ok(dark.includes("#1677FF"));
const light = renderDashboardHtml(d, { theme: "light" });
// 浅红规范:页面 #FAFAFA / 容器 #FFFFFF / 主色 #F5222D(§1 色彩)
assert.ok(light.includes("#FAFAFA"));
assert.ok(light.includes("#F5222D"));
// 蓝图工程图风(参考设计图):页面 #F8FAFC / 玫瑰红主色 #E11D48 / slate 文字
assert.ok(light.includes("#F8FAFC"));
assert.ok(light.includes("#E11D48"));
assert.ok(light.includes("#1E293B"));
assert.ok(!light.includes("#0C1836"));
// 缺省 = dark(向后兼容)
const def = renderDashboardHtml(d);
@@ -48,7 +48,7 @@ export interface ReportChartItem {
}
export interface ReportChart {
type: "bar" | "donut" | "radar";
type: "bar" | "donut" | "radar" | "line";
title: string;
items: ReportChartItem[];
}
@@ -240,10 +240,9 @@ export function coerceReportData(raw: unknown): ReportData | null {
const co = (c ?? {}) as Record<string, unknown>;
const type = str(co.type);
return {
type: (type === "bar" || type === "donut" || type === "radar" ? type : "bar") as
| "bar"
| "donut"
| "radar",
type: (type === "bar" || type === "donut" || type === "radar" || type === "line"
? type
: "bar") as "bar" | "donut" | "radar" | "line",
title: str(co.title),
items: arr(co.items)
.map((it) => {
@@ -297,7 +296,7 @@ export function buildDashboardPrompt(opts: {
'{"title":"大屏主标题(≤20字)","subtitle":"一句话定位",',
' "kpis":[{"label":"指标名","value":"数值","unit":"单位","trend":"up|down|flat"}], // 3-6 个核心指标,从研讨内容提取或合理估算',
' "consensus":{"headline":"一句话最终结论","points":["结论要点,3-6 条"]},',
' "charts":[{"type":"bar|donut|radar","title":"图表标题","items":[{"label":"维度","value":数字}]}], // 1-3 个总览图表(风险分布/资源占比/方案评分等),3-6 项/图',
' "charts":[{"type":"line|bar|donut|radar","title":"图表标题","items":[{"label":"维度","value":数字}]}], // 1-3 个总览图表:line=趋势(销量/渗透率随时间)、donut=占比(份额/构成)、radar=多维对比(竞争力)、bar=排名;3-8 项/图',
' "timeline":[{"label":"阶段名","desc":"一句话说明"}], // 执行路线 3-6 阶段',
' "seats":[{"id":"seat-1","name":"席位名(与交付席位一致)","summary":"该席位结论一两句",',
' "sections":[{"heading":"小节标题","bullets":["要点,每条≤40字"]}], // 2-4 节,提炼该席位交付',
@@ -364,27 +363,29 @@ const PALETTES: Record<DashboardTheme, Palette> = {
risk: { high: "#F5222D", mid: "#FAAD14", low: "#52C41A" },
modalMask: "rgba(6,14,36,.78)",
},
// 蓝图/工程图风(参考大屏设计图):白底 + slate 网格、玫瑰红(rose-600)强调、
// slate 中性文字、卡片四角红色角标、席位色序(scheme红/ux靛/ad绿/risk琥珀/summary天蓝)。
light: {
bgPage: "#FAFAFA",
bgPage: "#F8FAFC",
bgGradient:
"radial-gradient(1200px 600px at 70% -10%,rgba(245,34,45,.05),transparent),linear-gradient(160deg,#FFFFFF,#FAFAFA)",
gridLine: "rgba(245,34,45,.035)",
"radial-gradient(1100px 520px at 78% -12%,rgba(225,29,72,.05),transparent),linear-gradient(180deg,#FFFFFF,#F8FAFC)",
gridLine: "rgba(148,163,184,.16)",
card: "#FFFFFF",
cardBorder: "#E8E8E8",
cardGlow: "0 2px 8px rgba(0,0,0,.04)",
bracket: "#F5222D",
text: "#222222",
textSecondary: "#575757",
textStrong: "#222222",
heading: "#A8071A",
primary: "#F5222D",
link: "#0079FE",
titleColor: "linear-gradient(90deg,#F5222D,#CF1322 60%,#A8071A)",
navBg: "rgba(255,255,255,.92)",
trackBg: "#F0F0F0",
accents: ["#F5222D", "#0079FE", "#189D72", "#FF9900", "#45C3F3", "#7F7F7F"],
risk: { high: "#F5222D", mid: "#FF9900", low: "#189D72" },
modalMask: "rgba(34,34,34,.45)",
cardBorder: "rgba(226,232,240,.9)",
cardGlow: "0 3px 10px rgba(15,23,42,.04)",
bracket: "#E11D48",
text: "#475569",
textSecondary: "#64748B",
textStrong: "#1E293B",
heading: "#1E293B",
primary: "#E11D48",
link: "#E11D48",
titleColor: "linear-gradient(90deg,#1E293B,#334155)",
navBg: "rgba(255,255,255,.95)",
trackBg: "#F1F5F9",
accents: ["#E11D48", "#4F46E5", "#059669", "#D97706", "#0284C7", "#7C3AED"],
risk: { high: "#DC2626", mid: "#D97706", low: "#059669" },
modalMask: "rgba(15,23,42,.42)",
},
};
@@ -427,24 +428,59 @@ function svgBar(chart: ReportChart): string {
function svgDonut(chart: ReportChart): string {
const items = chart.items.slice(0, 6);
const total = items.reduce((s, i) => s + Math.max(0, i.value), 0) || 1;
const R = 56;
const R = 50;
const SW = 14;
const C = 2 * Math.PI * R;
let offset = 0;
let acc = 0;
const segs = items
.map((it, i) => {
const frac = Math.max(0, it.value) / total;
const seg = `<circle cx="80" cy="80" r="${R}" fill="none" stroke="${P.accents[i % P.accents.length]}" stroke-width="20" stroke-dasharray="${(frac * C).toFixed(2)} ${C.toFixed(2)}" stroke-dashoffset="${(-offset * C).toFixed(2)}" transform="rotate(-90 80 80)" opacity="0.9"/>`;
offset += frac;
const len = (Math.max(0, it.value) / total) * C;
const seg = `<circle cx="70" cy="70" r="${R}" fill="none" stroke="${P.accents[i % P.accents.length]}" stroke-width="${SW}" stroke-dasharray="${len.toFixed(2)} ${(C - len).toFixed(2)}" stroke-dashoffset="${(-acc).toFixed(2)}" transform="rotate(-90 70 70)"/>`;
acc += len;
return seg;
})
.join("");
// 中心:总分类数 + 标签(对齐设计图环形图的镂空中心)。
const center = `<text x="70" y="68" text-anchor="middle" class="dc-num">${items.length}</text><text x="70" y="82" text-anchor="middle" class="dc-cap">类</text>`;
const legend = items
.map(
(it, i) =>
`<div class="lg"><i style="background:${P.accents[i % P.accents.length]}"></i>${esc(it.label)} <b>${esc(String(it.value))}</b></div>`,
`<div class="lg"><i style="background:${P.accents[i % P.accents.length]}"></i><span>${esc(it.label)}</span><b>${esc(String(it.value))}</b></div>`,
)
.join("");
return `<div class="donut"><svg viewBox="0 0 160 160" role="img">${segs}</svg><div class="lgs">${legend}</div></div>`;
return `<div class="donut"><svg viewBox="0 0 140 140" role="img"><circle cx="70" cy="70" r="${R}" fill="none" stroke="var(--track)" stroke-width="${SW}"/>${segs}${center}</svg><div class="lgs">${legend}</div></div>`;
}
// 折线图(趋势):多取前 8 点;自动归一化到画布。设计图的销量趋势用折线。
function svgLine(chart: ReportChart): string {
const items = chart.items.slice(0, 8);
if (items.length < 2) return svgBar(chart);
const W = 300;
const H = 150;
const padL = 30;
const padR = 12;
const padT = 12;
const padB = 24;
const max = Math.max(...items.map((i) => i.value), 1);
const min = Math.min(...items.map((i) => i.value), 0);
const span = max - min || 1;
const x = (i: number) => padL + (i / (items.length - 1)) * (W - padL - padR);
const y = (v: number) => padT + (1 - (v - min) / span) * (H - padT - padB);
const pts = items.map((it, i) => `${x(i).toFixed(1)},${y(it.value).toFixed(1)}`).join(" ");
const grid = [0, 0.5, 1]
.map((k) => {
const gy = (padT + k * (H - padT - padB)).toFixed(1);
return `<line x1="${padL}" y1="${gy}" x2="${W - padR}" y2="${gy}" class="gr"/>`;
})
.join("");
const area = `<polygon points="${padL},${(H - padB).toFixed(1)} ${pts} ${(W - padR).toFixed(1)},${(H - padB).toFixed(1)}" fill="${P.primary}14"/>`;
const dots = items
.map((it, i) => `<circle cx="${x(i).toFixed(1)}" cy="${y(it.value).toFixed(1)}" r="2.6" fill="${P.primary}"/>`)
.join("");
const labels = items
.map((it, i) => `<text x="${x(i).toFixed(1)}" y="${H - 8}" text-anchor="middle" class="cl">${esc(it.label)}</text>`)
.join("");
return `<svg viewBox="0 0 ${W} ${H}" role="img">${grid}${area}<polyline points="${pts}" fill="none" stroke="${P.primary}" stroke-width="2.2" stroke-linejoin="round" stroke-linecap="round"/>${dots}${labels}</svg>`;
}
function svgRadar(chart: ReportChart): string {
@@ -480,7 +516,13 @@ function svgRadar(chart: ReportChart): string {
function chartHtml(chart: ReportChart): string {
const body =
chart.type === "donut" ? svgDonut(chart) : chart.type === "radar" ? svgRadar(chart) : svgBar(chart);
chart.type === "donut"
? svgDonut(chart)
: chart.type === "radar"
? svgRadar(chart)
: chart.type === "line"
? svgLine(chart)
: svgBar(chart);
return `<div class="card chart"><h3>${esc(chart.title || "图表")}</h3>${body}</div>`;
}
@@ -501,7 +543,7 @@ function metricBars(metrics: ReportSeatMetric[]): string {
return `<div class="metrics">${metrics
.map(
(m, i) =>
`<div class="metric"><span>${esc(m.label)}</span><div class="track"><div class="fill" style="width:${m.value}%;background:${P.accents[i % P.accents.length]}"></div></div><b>${m.value}</b></div>`,
`<div class="metric"><div class="mrow"><span>${esc(m.label)}</span><b>${m.value}</b></div><div class="track"><div class="fill" style="width:${m.value}%;background:${P.accents[i % P.accents.length]}"></div></div></div>`,
)
.join("")}</div>`;
}
@@ -533,20 +575,23 @@ export function renderDashboardHtml(
`<a class="tab" href="#overview">总览</a>` +
seats.map((s) => `<a class="tab" href="#${esc(s.id)}">${esc(s.name)}</a>`).join("");
const KPI_ICONS = ["📊", "📈", "⚠️", "🧩", "🏆", "📅"];
const kpis = data.kpis
.map(
(k, i) =>
`<div class="card kpi"><span class="klabel">${esc(k.label)}</span><div class="kval" style="color:${P.accents[i % P.accents.length]}">${esc(k.value)}<small>${esc(k.unit ?? "")}</small>${
.map((k, i) => {
const ac = P.accents[i % P.accents.length];
return `<div class="card kpi">
<div class="khead"><span class="klabel">${esc(k.label)}</span><span class="kico" style="background:${ac}1a;color:${ac}">${KPI_ICONS[i % KPI_ICONS.length]}</span></div>
<div class="kval">${esc(k.value)}<small>${esc(k.unit ?? "")}</small>${
k.trend === "up" ? '<i class="tr up">▲</i>' : k.trend === "down" ? '<i class="tr dn">▼</i>' : ""
}</div></div>`,
)
}</div></div>`;
})
.join("");
const seatCards = seats
.map((s, i) => {
const color = P.accents[i % P.accents.length];
return `<a class="card seat" href="#${esc(s.id)}" style="--ac:${color}">
<div class="shead"><i></i><h3>${esc(s.name)}</h3><span class="go">进入席位页 →</span></div>
<div class="shead"><i></i><h3>${esc(s.name)}</h3><span class="go">席位页 →</span></div>
<p>${esc(s.summary || "(无摘要)")}</p>
${metricBars(s.metrics.slice(0, 2))}
${riskChips(s.risks.slice(0, 2))}
@@ -562,17 +607,20 @@ export function renderDashboardHtml(
const consensusPoints = data.consensus.points.map((p) => `<li>${esc(p)}</li>`).join("");
const chartsBlock = data.charts.length
? `<div class="charts">${data.charts.map(chartHtml).join("")}</div>`
: "";
const overview = `<section class="page" id="overview">
<div class="kpis">${kpis}</div>
<div class="grid2">
<div class="ovgrid">
<div class="card consensus"><h3>总控最终共识</h3>
<p class="headline">${esc(data.consensus.headline || "(未提供)")}</p>
<ul>${consensusPoints}</ul>
<a class="more" href="#consensus-modal">查看详情</a>
<a class="more" href="#consensus-modal">查看研讨详情 →</a>
</div>
<div class="charts">${data.charts.map(chartHtml).join("")}</div>
${chartsBlock}
</div>
<h2 class="sec">席位交付总览 <small>点击卡片进入席位专页</small></h2>
<h2 class="sec">席位交付总览 <small>点击卡片进入对应席位专页</small></h2>
<div class="seats">${seatCards}</div>
${timeline}
</section>`;
@@ -620,95 +668,125 @@ export function renderDashboardHtml(
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>${esc(data.title)}</title>
<style>
/* 主题色板(light=浅红色 UI 规范 / dark=深蓝色 UI 规范,渲染时注入) */
/* 蓝图工程图风模板(light=参考大屏设计图 / dark=深蓝 token,渲染时注入 palette) */
:root{--line:${P.cardBorder};--txt:${P.text};--dim:${P.textSecondary};--strong:${P.textStrong};
--primary:${P.primary};--link:${P.link};--heading:${P.heading};--track:${P.trackBg}}
--primary:${P.primary};--link:${P.link};--heading:${P.heading};--track:${P.trackBg};--bracket:${P.bracket}}
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:"Noto Sans S Chinese","Microsoft YaHei",system-ui,sans-serif;color:var(--txt);min-height:100vh;
background:${P.bgGradient};background-color:${P.bgPage};background-attachment:fixed}
body::before{content:"";position:fixed;inset:0;pointer-events:none;opacity:.6;
body{font-family:"Inter","Noto Sans SC","Microsoft YaHei",system-ui,sans-serif;color:var(--txt);min-height:100vh;
font-size:13px;background:${P.bgGradient};background-color:${P.bgPage};background-attachment:fixed}
body::before{content:"";position:fixed;inset:0;pointer-events:none;
background:linear-gradient(${P.gridLine} 1px,transparent 1px),linear-gradient(90deg,${P.gridLine} 1px,transparent 1px);
background-size:42px 42px}
background-size:22px 22px}
.top{position:sticky;top:0;z-index:5;display:flex;flex-wrap:wrap;align-items:center;gap:12px;
padding:14px 26px;background:${P.navBg};backdrop-filter:blur(6px);border-bottom:1px solid var(--line)}
.top h1{font-size:21px;letter-spacing:2px;background:${P.titleColor};
padding:13px 26px;background:${P.navBg};backdrop-filter:blur(8px);border-bottom:1px solid var(--line)}
.top h1{font-size:18px;font-weight:800;letter-spacing:.5px;background:${P.titleColor};
-webkit-background-clip:text;background-clip:text;color:transparent;white-space:nowrap}
.top .sub{font-size:12px;color:var(--dim)}
.top .meta{margin-left:auto;font-size:11px;color:var(--dim)}
nav{display:flex;flex-wrap:wrap;gap:8px;padding:10px 26px;border-bottom:1px solid var(--line)}
.tab{font-size:12px;padding:5px 14px;border:1px solid var(--line);border-radius:2px;color:var(--dim);text-decoration:none;transition:.15s}
.tab:hover{color:${P.primary};border-color:${P.primary};box-shadow:0 0 10px ${P.primary}40}
main{padding:20px 26px 40px;max-width:1480px;margin:0 auto}
.top .sub{font-size:11.5px;color:var(--dim);padding:2px 8px;border:1px solid var(--line);border-radius:4px}
.top .meta{margin-left:auto;font-size:10.5px;color:var(--dim);font-family:"JetBrains Mono",Consolas,monospace;letter-spacing:.5px}
nav{position:sticky;top:51px;z-index:4;display:flex;flex-wrap:wrap;gap:7px;padding:9px 26px;
background:${P.navBg};backdrop-filter:blur(8px);border-bottom:1px solid var(--line)}
.tab{font-size:12px;font-weight:600;padding:5px 13px;border:1px solid var(--line);border-radius:4px;color:var(--dim);text-decoration:none;transition:.15s;background:var(--track)}
.tab:hover{color:#fff;border-color:var(--primary);background:var(--primary)}
main{padding:22px 26px 48px;max-width:1500px;margin:0 auto}
.page{display:none;animation:fi .25s ease}
#overview{display:block}
.page:target{display:block}
body:has(.page:target) #overview:not(:target){display:none}
@keyframes fi{from{opacity:0;transform:translateY(6px)}to{opacity:1}}
/* 卡片:规范圆角 4px;浅色实底白卡 + 轻阴影,深色深蓝渐变 + 内发光 */
.card{position:relative;border:1px solid var(--line);border-radius:4px;padding:16px 18px;
background:${P.card};box-shadow:${P.cardGlow}}
.card::before,.card::after{content:"";position:absolute;width:14px;height:14px;border-color:${P.bracket};border-style:solid;opacity:.55}
.card::before{top:-1px;left:-1px;border-width:2px 0 0 2px;border-radius:4px 0 0 0}
.card::after{bottom:-1px;right:-1px;border-width:0 2px 2px 0;border-radius:0 0 4px 0}
.card h3{font-size:13px;letter-spacing:1.5px;color:var(--heading);margin-bottom:10px}
.kpis{display:grid;grid-template-columns:repeat(auto-fit,minmax(170px,1fr));gap:14px;margin-bottom:18px}
.kpi .klabel{font-size:11px;color:var(--dim);letter-spacing:1px}
.kpi .kval{font-size:30px;font-weight:800;margin-top:4px;font-family:"D-DIN",Consolas,monospace}
.kpi small{font-size:12px;margin-left:4px;color:var(--dim)}
.kpi .tr{font-style:normal;font-size:12px;margin-left:6px}.tr.up{color:${P.risk.low}}.tr.dn{color:${P.risk.high}}
.grid2{display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-bottom:18px}
@media(max-width:980px){.grid2{grid-template-columns:1fr}}
.consensus .headline{font-size:15px;font-weight:700;color:var(--strong);margin-bottom:8px}
.consensus ul{padding-left:18px;display:grid;gap:5px;font-size:12.5px;color:var(--txt)}
.consensus .more{display:inline-block;margin-top:10px;font-size:11px;color:${P.link};text-decoration:none;border-bottom:1px dashed ${P.link}}
.charts{display:grid;gap:14px}
.chart svg{width:100%;height:auto}
.cl{fill:var(--dim);font-size:11px}.cv{fill:var(--txt);font-size:11px;font-weight:700}
.ct{fill:var(--track)}.gr{fill:none;stroke:${P.primary}2e;stroke-width:1}
.donut{display:flex;align-items:center;gap:14px}.donut svg{width:130px;flex:none}
.lgs{display:grid;gap:5px}.lg{font-size:11.5px;color:var(--dim)}.lg b{color:var(--txt);margin-left:4px}
.lg i{display:inline-block;width:9px;height:9px;border-radius:2px;margin-right:6px}
.sec{font-size:14px;letter-spacing:2px;color:var(--heading);margin:6px 0 12px}
.sec small{font-size:11px;color:var(--dim);font-weight:400;margin-left:8px}
.seats{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:14px;margin-bottom:18px}
.seat{display:block;text-decoration:none;color:inherit;transition:.15s}
.seat:hover{border-color:var(--ac);box-shadow:0 0 16px ${P.primary}30;transform:translateY(-2px)}
.shead{display:flex;align-items:center;gap:8px;margin-bottom:8px}
.shead i{width:8px;height:8px;border-radius:50%;background:var(--ac);box-shadow:0 0 8px var(--ac)}
.shead h3{margin:0;flex:1}
.go{font-size:10.5px;color:var(--dim)}
@keyframes fi{from{opacity:0;transform:translateY(8px)}to{opacity:1}}
/* 蓝图卡片:白底 + 四角玫瑰红角标 + 极淡网格 overlay */
.card{position:relative;border:1px solid var(--line);border-radius:6px;padding:17px 19px;
background:${P.card};box-shadow:${P.cardGlow};transition:box-shadow .3s,transform .3s,border-color .3s}
.card::before,.card::after{content:"";position:absolute;width:12px;height:12px;border-color:var(--bracket);border-style:solid;z-index:2}
.card::before{top:-1px;left:-1px;border-width:2px 0 0 2px}
.card::after{bottom:-1px;right:-1px;border-width:0 2px 2px 0}
.card>*{position:relative;z-index:1}
.card h3{font-size:13.5px;font-weight:700;letter-spacing:.3px;color:var(--strong);margin-bottom:12px;
display:flex;align-items:center;gap:7px;padding-bottom:11px;border-bottom:1px solid var(--line)}
.card h3::before{content:"";width:9px;height:9px;border-radius:2px;background:var(--primary);flex:none}
/* KPI */
.kpis{display:grid;grid-template-columns:repeat(auto-fit,minmax(168px,1fr));gap:13px;margin-bottom:18px}
.kpi{padding:15px 17px;min-height:104px;display:flex;flex-direction:column;justify-content:space-between}
.kpi .khead{display:flex;justify-content:space-between;align-items:flex-start;gap:6px}
.kpi .klabel{font-size:11.5px;color:var(--dim);font-weight:500;letter-spacing:.3px}
.kpi .kico{width:30px;height:30px;border-radius:6px;display:flex;align-items:center;justify-content:center;font-size:15px;flex:none}
.kpi .kval{font-size:30px;font-weight:800;letter-spacing:-.5px;color:var(--strong);font-family:"JetBrains Mono",Consolas,monospace;line-height:1;display:flex;align-items:baseline;gap:3px;margin-top:8px}
.kpi small{font-size:12px;font-weight:500;color:var(--dim)}
.kpi .tr{font-size:11px;font-weight:700;padding:1px 5px;border-radius:3px;margin-left:4px}
.tr.up{color:${P.risk.low};background:${P.risk.low}1a}.tr.dn{color:${P.risk.high};background:${P.risk.high}1a}
.ovgrid{display:grid;grid-template-columns:1.25fr 1fr;gap:13px;margin-bottom:20px;align-items:start}
@media(max-width:980px){.ovgrid{grid-template-columns:1fr}}
.grid2{display:grid;grid-template-columns:1fr 1fr;gap:13px;margin-bottom:14px}
@media(max-width:760px){.grid2{grid-template-columns:1fr}}
/* 共识:玫瑰红竖条摘要 + 要点列表 */
.consensus .headline{font-size:13.5px;font-weight:600;color:var(--strong);line-height:1.7;margin-bottom:13px;
background:${P.primary}0d;padding:12px 14px;border-radius:4px;border-left:3px solid var(--primary)}
.consensus ul{list-style:none;display:grid;gap:11px}
.consensus li{display:flex;gap:9px;font-size:12px;line-height:1.65;color:var(--txt)}
.consensus li::before{content:"";width:6px;height:6px;border-radius:50%;background:var(--primary);margin-top:6px;flex:none}
.consensus .more{display:inline-block;margin-top:13px;font-size:11.5px;font-weight:600;color:var(--primary);text-decoration:none}
.consensus .more:hover{text-decoration:underline}
.charts{display:grid;gap:13px}
.chart svg{width:100%;height:auto;display:block}
.cl{fill:var(--dim);font-size:9.5px}.cv{fill:var(--strong);font-size:11px;font-weight:700;font-family:monospace}
.ct{fill:var(--track)}.gr{fill:none;stroke:${P.gridLine.replace(/[\d.]+\)$/, "0.6)")};stroke:var(--line);stroke-width:1}
.dc-num{fill:var(--strong);font-size:26px;font-weight:800;font-family:monospace}
.dc-cap{fill:var(--dim);font-size:9px}
.donut{display:flex;align-items:center;gap:14px;flex-wrap:wrap;justify-content:center}.donut svg{width:128px;flex:none}
.lgs{display:grid;gap:7px;min-width:130px}.lg{display:flex;align-items:center;gap:7px;font-size:11.5px;color:var(--txt)}
.lg span{flex:1}.lg b{color:var(--strong);font-family:monospace;font-weight:700}
.lg i{display:inline-block;width:9px;height:9px;border-radius:2px;flex:none}
.sec{font-size:15px;font-weight:700;color:var(--strong);margin:8px 0 13px;display:flex;align-items:center;gap:8px}
.sec small{font-size:11.5px;color:var(--dim);font-weight:400}
.seats{display:grid;grid-template-columns:repeat(auto-fill,minmax(270px,1fr));gap:13px;margin-bottom:20px}
.seat{display:flex;flex-direction:column;text-decoration:none;color:inherit}
.seat:hover{border-color:var(--ac);box-shadow:0 8px 22px rgba(15,23,42,.09);transform:translateY(-2px)}
.shead{display:flex;align-items:center;gap:7px;padding-bottom:10px;border-bottom:1px solid var(--line);margin-bottom:0}
.shead::before{display:none}
.shead i{width:9px;height:9px;border-radius:50%;background:var(--ac);flex:none}
.shead h3{margin:0;padding:0;border:0;font-size:12.5px;flex:1}
.shead h3::before{display:none}
.go{font-size:10px;color:var(--dim);display:flex;align-items:center;gap:2px}
.seat:hover .go{color:var(--ac)}
.seat p{font-size:12px;color:var(--dim);line-height:1.6;margin-bottom:8px}
.metrics{display:grid;gap:7px}
.metric{display:flex;align-items:center;gap:8px;font-size:11px;color:var(--dim)}
.metric span{width:72px;flex:none}.metric b{width:26px;text-align:right;color:var(--txt)}
.track{flex:1;height:7px;border-radius:4px;background:var(--track);overflow:hidden}
.fill{height:100%;border-radius:4px}
.chips{display:flex;flex-wrap:wrap;gap:6px;margin-top:8px}
.chip{font-size:10.5px;padding:2px 9px;border:1px solid;border-radius:2px}
.tl ol{list-style:none;display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px;counter-reset:tl}
.tl li{position:relative;padding:10px 12px 10px 14px;border-left:2px solid ${P.primary};background:${P.primary}10;border-radius:0 4px 4px 0}
.tl b{display:block;font-size:12.5px;color:var(--heading);margin-bottom:3px}
.tl span{font-size:11px;color:var(--dim);line-height:1.5}
.pbar{display:flex;justify-content:space-between;align-items:center;margin-bottom:14px;font-size:12px}
.pbar a{color:${P.link};text-decoration:none}.pnav{display:flex;gap:14px}.pnav a{color:var(--dim)}.pnav a:hover{color:${P.link}}
.phead{display:flex;align-items:center;gap:12px;margin-bottom:12px}
.phead i{width:10px;height:34px;border-radius:3px;background:var(--ac);box-shadow:0 0 10px var(--ac)}
.phead h2{font-size:19px;letter-spacing:2px;color:var(--strong)}
.phead p{font-size:12px;color:var(--dim);margin-top:3px}
.deps{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:14px}
.dep{font-size:11px;padding:3px 11px;border:1px dashed ${P.link};border-radius:2px;color:${P.link};text-decoration:none}
.dep.sup{border-color:${P.risk.low};color:${P.risk.low}}
.dep:hover{background:${P.link}1a}
.psections{display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:14px;margin-bottom:14px}
.sect ul{padding-left:16px;display:grid;gap:6px;font-size:12.5px;line-height:1.6}
.seat .anly{font-size:10px;color:var(--dim);font-family:monospace;text-transform:uppercase;letter-spacing:.5px;margin:11px 0 6px}
.seat p{font-size:11.5px;color:var(--dim);line-height:1.65;margin-bottom:12px;flex:1;
display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}
.metrics{display:grid;gap:9px}
.metric span{font-size:10.5px;color:var(--dim);font-weight:500}
.metric .mrow{display:flex;justify-content:space-between;margin-bottom:4px}
.metric b{font-size:11px;color:var(--strong);font-family:monospace;font-weight:700}
.track{height:6px;border-radius:99px;background:var(--track);overflow:hidden}
.fill{height:100%;border-radius:99px}
.chips{display:flex;flex-direction:column;gap:6px;margin-top:12px;padding-top:11px;border-top:1px solid var(--line)}
.chip{font-size:10px;padding:3px 8px;border-radius:4px;border-left:2px solid;display:flex;align-items:center;gap:5px}
.chip::before{content:"⚠";font-size:9px}
.tl{margin-top:2px}
.tl ol{list-style:none;display:grid;grid-template-columns:repeat(auto-fit,minmax(190px,1fr));gap:11px}
.tl li{position:relative;padding:12px 14px;border:1px solid var(--line);border-left:3px solid var(--primary);background:var(--track);border-radius:0 5px 5px 0}
.tl b{display:block;font-size:12.5px;font-weight:700;color:var(--strong);margin-bottom:5px}
.tl span{font-size:11px;color:var(--dim);line-height:1.6}
/* 席位专页 */
.pbar{display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;font-size:12px}
.pbar a{color:var(--primary);text-decoration:none;font-weight:600}.pnav{display:flex;gap:16px}.pnav a{color:var(--dim);font-weight:500}.pnav a:hover{color:var(--primary)}
.phead{display:flex;align-items:center;gap:13px;margin-bottom:14px;padding-bottom:14px;border-bottom:1px solid var(--line)}
.phead i{width:6px;height:38px;border-radius:3px;background:var(--ac);flex:none}
.phead h2{font-size:20px;font-weight:800;color:var(--strong)}
.phead p{font-size:12px;color:var(--dim);margin-top:4px;line-height:1.6}
.deps{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:16px}
.dep{font-size:11px;font-weight:600;padding:4px 11px;border:1px dashed var(--primary);border-radius:4px;color:var(--primary);text-decoration:none}
.dep.sup{border-color:${P.accents[4]};color:${P.accents[4]}}
.dep:hover{background:var(--primary)12}
.psections{display:grid;grid-template-columns:repeat(auto-fit,minmax(290px,1fr));gap:13px;margin-bottom:14px}
.sect ul{list-style:none;display:grid;gap:8px;font-size:12px;line-height:1.65}
.sect li{display:flex;gap:8px;color:var(--txt)}
.sect li::before{content:"";width:5px;height:5px;border-radius:50%;background:var(--primary);margin-top:7px;flex:none}
.sect .empty{font-size:12px;color:var(--dim)}
.modal{display:none;position:fixed;inset:0;z-index:20;background:${P.modalMask};padding:6vh 16px}
.modal:target{display:flex;justify-content:center;align-items:flex-start}
.mbody{max-width:680px;width:100%;max-height:84vh;overflow:auto}
.mbody ul{padding-left:18px;display:grid;gap:6px;font-size:13px}
.mclose{position:absolute;top:8px;right:14px;font-size:20px;color:var(--dim);text-decoration:none}
.mclose:hover{color:${P.risk.high}}
.mbody{max-width:700px;width:100%;max-height:84vh;overflow:auto}
.mbody ul{list-style:none;display:grid;gap:9px;font-size:13px;margin-top:6px}
.mbody li{display:flex;gap:9px;line-height:1.65}.mbody li::before{content:"";width:6px;height:6px;border-radius:50%;background:var(--primary);margin-top:7px;flex:none}
.mclose{position:absolute;top:10px;right:16px;font-size:22px;color:var(--dim);text-decoration:none;line-height:1}
.mclose:hover{color:var(--primary)}
</style></head>
<body>
<header class="top"><h1>${esc(data.title)}</h1><span class="sub">${esc(data.subtitle)}</span><span class="meta">${generated}</span></header>