Files
shiroko-monitor/public/assets/style.css
T
tamakyi f345626840 fix: 管理入口改为「填链接」而非绑定特定隧道方案,并修掉配置接口的密钥泄露
用户反馈:「你不需要帮我处理 tunnel 这部分,也许我不是通过 tunnel 开放内网呢?
你只需要给我一个填写开放的管理端链接的地方。」

之前把「Cloudflare Tunnel」当成既定方案写进了配置与文档,这是替用户做了假设。
现在改为:项目只提供一个填地址的地方,怎么暴露完全由使用者决定。

- 删除 edge/cloudflare/tunnel.yml 与全部 tunnel 相关文档
- 新增 settings.adminUrl:管理界面里填一个地址,公开面板页脚出现「管理」按钮
  指向它;留空则完全不显示(默认,避免在公网面板暴露管理面位置)
- 只接受 http/https,javascript: 等协议被服务端拒绝(会被渲染成 <a href>)
- 设置落在 local.monitors.json(私有层),只有自己的面板会出现该按钮
- 面板启动时额外拉 /api/monitors 合并 settings:原先只读静态 monitors.json
  (公开层),导致私有层的设置面板看不到;纯静态部署无该接口时沿用公开配置

顺带修掉一个既存漏洞(与本轮改动无关,是引入私有层叠加机制时埋下的):
/api/monitors 无鉴权却回吐合并后的完整配置,包含私有层的 UptimeRobot API Key ——
只要配了 key,任何访客 curl /api/monitors 就能拿到。现在两侧(Node 与 Worker)
都对对外响应剥离 uptimeRobot.apiKey,保留 enabled 等非敏感字段供面板读取;
探针内部同步仍用完整配置,剥离只作用于 HTTP 出口。

测试:verify-worker 36→40 项(新增 [E] 配置接口密钥泄露 4 条)、
verify-security 20→25 项(新增 [D2] 密钥泄露 5 条)。全项目 8 套 314 项通过。
2026-10-14 15:23:22 +08:00

570 lines
20 KiB
CSS
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
:root {
--bg: #f5f6f8;
--surface: #ffffff;
--border: #e5e7eb;
--text: #111827;
--text-2: #6b7280;
--up: #16a34a;
--up-text: #15803d;
--up-bg: #f0fdf4;
--down: #dc2626;
--down-text: #b91c1c;
--down-bg: #fef2f2;
--pending: #d97706;
--pending-text: #92400e;
--pending-bg: #fffbeb;
--accent: #2563eb;
--uptime-ok: #16a34a;
--uptime-warn: #f59e0b;
--uptime-bad: #dc2626;
--uptime-none: #e5e7eb;
--mono: ui-monospace, SFMono-Regular, "Cascadia Code", Consolas, "Courier New", monospace;
--sans: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
--shadow: 0 1px 2px rgb(0 0 0 / 0.04);
/* 表格行高基准:固定像素,行高与内容长度彻底解耦(任意长度内容下行高都相同)。
72px = 名称两行(36) + 徽标行(18) + 上下内边距(16),监控表与事件表共用同一个值,
两张表因此在视觉上也严格对齐。 */
--row-h: 72px;
--group-row-h: 32px;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #0f1115;
--surface: #171a21;
--border: #2a2f3a;
--text: #e5e7eb;
--text-2: #9ca3af;
--up: #4ade80;
--up-text: #4ade80;
--up-bg: #10281c;
--down: #f87171;
--down-text: #f87171;
--down-bg: #2a1517;
--pending: #fbbf24;
--pending-text: #fbbf24;
--pending-bg: #2a2113;
--accent: #60a5fa;
--uptime-none: #2a2f3a;
--shadow: none;
}
}
* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
margin: 0;
background: var(--bg);
color: var(--text);
font-family: var(--sans);
font-size: 14px;
line-height: 1.5;
}
/* ---------- 顶栏 ---------- */
.topbar {
position: sticky;
top: 0;
z-index: 10;
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 12px 20px;
padding: 12px 20px;
background: var(--surface);
border-bottom: 1px solid var(--border);
box-shadow: var(--shadow);
}
.brand { display: flex; align-items: center; gap: 10px; min-width: 0; }
.logo {
width: 26px;
height: 26px;
flex: none;
border-radius: 50%;
background: var(--up);
box-shadow: 0 0 0 4px color-mix(in srgb, var(--up) 18%, transparent);
animation: pulse 2.4s ease-out infinite;
}
@keyframes pulse {
0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--up) 30%, transparent); }
70% { box-shadow: 0 0 0 7px color-mix(in srgb, var(--up) 0%, transparent); }
100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--up) 0%, transparent); }
}
.brand-text { min-width: 0; }
.brand-text h1 { margin: 0; font-size: 15px; font-weight: 650; white-space: nowrap; }
.brand-text p { margin: 0; font-size: 12px; color: var(--text-2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.summary { display: flex; align-items: center; flex-wrap: wrap; gap: 8px 16px; }
.stats { display: flex; align-items: center; flex-wrap: wrap; gap: 8px 16px; }
.controls { display: flex; align-items: center; gap: 8px; }
.pill {
padding: 4px 12px;
border-radius: 999px;
font-size: 13px;
font-weight: 650;
white-space: nowrap;
}
.pill.ok { background: var(--up-bg); color: var(--up-text); }
.pill.bad { background: var(--down-bg); color: var(--down-text); }
.pill.pending { background: var(--pending-bg); color: var(--pending-text); }
.stat { text-align: center; min-width: 44px; }
.stat b { display: block; font-size: 16px; font-weight: 700; font-variant-numeric: tabular-nums; }
.stat label { font-size: 11px; color: var(--text-2); }
.stat-up b { color: var(--up-text); }
.stat-down b { color: var(--down-text); }
#refresh-btn {
display: inline-flex;
align-items: center;
padding: 6px 14px;
border: 1px solid var(--border);
border-radius: 8px;
background: var(--surface);
color: var(--text);
font-size: 13px;
font-family: var(--sans);
cursor: pointer;
transition: background 0.15s;
}
#refresh-btn:hover { background: var(--bg); }
#refresh-btn:disabled { opacity: 0.6; cursor: default; }
/* 加载态用的车轮图标(砂狼白子骑行意象) */
.wheel { width: 14px; height: 14px; margin-right: 6px; animation: spin 0.9s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
/* 图标按钮(主题切换) */
.icon-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 30px;
height: 30px;
padding: 0;
border: 1px solid var(--border);
border-radius: 8px;
background: var(--surface);
color: var(--text-2);
cursor: pointer;
transition: color 0.15s, border-color 0.15s, box-shadow 0.15s;
}
.icon-btn svg { width: 16px; height: 16px; }
.icon-btn:hover { color: var(--accent); border-color: var(--accent); }
.icon-btn.active { color: var(--accent); border-color: var(--accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 12%, transparent); }
/* ---------- 视图切换 ---------- */
.view-switch { display: flex; border: 1px solid var(--border); border-radius: 8px; overflow: hidden; }
.view-btn {
padding: 5px 10px;
border: 0;
background: var(--surface);
color: var(--text-2);
font-size: 12px;
font-family: var(--sans);
cursor: pointer;
transition: background 0.15s, color 0.15s;
}
.view-btn + .view-btn { border-left: 1px solid var(--border); }
.view-btn:hover { color: var(--text); }
.view-btn.active { background: var(--accent); color: #fff; }
/* ---------- 分组页签 ---------- */
.group-tabs {
display: flex;
flex-wrap: wrap;
gap: 8px;
max-width: 1200px;
margin: 0 auto;
padding: 16px 20px 0;
}
.tab {
padding: 5px 14px;
border: 1px solid var(--border);
border-radius: 999px;
background: var(--surface);
color: var(--text-2);
font-size: 13px;
font-family: var(--sans);
cursor: pointer;
transition: all 0.15s;
}
.tab:hover { color: var(--text); }
.tab.active { background: var(--accent); border-color: var(--accent); color: #fff; }
/* ---------- 卡片网格 ---------- */
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(min(300px, 100%), 1fr));
gap: 14px;
max-width: 1200px;
margin: 0 auto;
padding: 16px 20px 8px;
}
.group-title {
grid-column: 1 / -1;
margin: 8px 0 0;
font-size: 13px;
font-weight: 650;
color: var(--text-2);
}
.card {
display: flex;
flex-direction: column;
gap: 10px;
/* 网格项默认 min-width:auto,长 URL/名称会把卡片撑破容器 —— 显式允许收缩并裁剪 */
min-width: 0;
overflow: hidden;
padding: 14px 16px;
background: var(--surface);
border: 1px solid var(--border);
border-left: 3px solid var(--pending);
border-radius: 12px;
box-shadow: var(--shadow);
}
.card[data-status="up"] { border-left-color: var(--up); }
.card[data-status="down"] { border-left-color: var(--down); }
.card-head { display: flex; align-items: center; gap: 8px; min-width: 0; }
.card-head h3 { flex: 1; margin: 0; font-size: 14px; font-weight: 650; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.dot { width: 10px; height: 10px; flex: none; border-radius: 50%; background: var(--pending); }
/* 通用着色:卡片 / 表格行 / 极简列表 都以 data-status 标记状态 */
[data-status="up"] .dot { background: var(--up); }
[data-status="down"] .dot { background: var(--down); }
[data-status="pending"] .dot { background: var(--pending); }
.card[data-status="up"] .dot { animation: pulse-dot 2.4s ease-out infinite; }
@keyframes pulse-dot {
0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--up) 35%, transparent); }
70% { box-shadow: 0 0 0 6px color-mix(in srgb, var(--up) 0%, transparent); }
100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--up) 0%, transparent); }
}
.type-badge {
flex: none;
padding: 1px 7px;
border: 1px solid var(--border);
border-radius: 6px;
font-family: var(--mono);
font-size: 11px;
color: var(--text-2);
}
.card-target {
font-family: var(--mono);
font-size: 12px;
color: var(--text-2);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.metrics { display: flex; gap: 8px; }
.metric { flex: 1; min-width: 0; }
.metric b { display: block; font-size: 14px; font-weight: 700; font-variant-numeric: tabular-nums; white-space: nowrap; }
.metric label { font-size: 11px; color: var(--text-2); }
.heartbeat { height: 32px; }
.heartbeat svg { display: block; width: 100%; height: 100%; }
.hb-empty { display: flex; align-items: center; height: 100%; font-size: 12px; color: var(--text-2); }
/* 90 天可用率 */
.uptime { display: flex; align-items: center; gap: 8px; }
.uptime-bar { display: flex; gap: 1px; flex: 1; min-width: 0; height: 10px; }
.uptime-cell { flex: 1; min-width: 0; border-radius: 1px; background: var(--uptime-none); }
.uptime-cell.lv-ok { background: var(--uptime-ok); }
.uptime-cell.lv-warn { background: var(--uptime-warn); }
.uptime-cell.lv-bad { background: var(--uptime-bad); }
.uptime-label { font-size: 11px; color: var(--text-2); white-space: nowrap; font-variant-numeric: tabular-nums; }
.card-detail {
font-family: var(--mono);
font-size: 12px;
color: var(--text-2);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.card[data-status="down"] .card-detail { color: var(--down-text); }
.empty {
grid-column: 1 / -1;
padding: 60px 0;
text-align: center;
color: var(--text-2);
}
/* 过滤口径不一致时的提示条 */
.notice {
grid-column: 1 / -1;
margin-bottom: 10px;
padding: 10px 14px;
border: 1px solid color-mix(in srgb, var(--pending) 45%, var(--border));
border-radius: 10px;
background: var(--pending-bg);
color: var(--pending);
font-size: 12px;
line-height: 1.7;
}
.notice code { font-family: var(--mono); background: color-mix(in srgb, var(--pending) 14%, transparent); padding: 0 4px; border-radius: 4px; }
/* ---------- 底栏 ---------- */
.footer {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 6px 16px;
max-width: 1200px;
margin: 0 auto;
padding: 8px 20px 28px;
font-size: 12px;
color: var(--text-2);
}
.footer .version { margin-left: auto; }
/* 管理入口:仅在配置了 settings.adminUrl 时出现;默认隐藏,避免在公网面板上暴露管理面位置 */
.admin-link {
padding: 1px 9px;
border: 1px solid var(--border);
border-radius: 999px;
color: var(--text-2);
text-decoration: none;
white-space: nowrap;
transition: color 0.15s, border-color 0.15s;
}
.admin-link:hover { color: var(--accent); border-color: var(--accent); }
.demo-chip { padding: 1px 8px; border-radius: 999px; background: var(--pending-bg); color: var(--pending-text); font-weight: 600; }
.warn-chip { padding: 1px 8px; border-radius: 999px; background: var(--down-bg); color: var(--down-text); font-weight: 600; }
/* 窄屏(手机 / 竖屏平板):顶栏改为三行——标题 / 统计 / 控件,避免挤成一团 */
@media (max-width: 820px) {
.topbar { flex-direction: column; align-items: stretch; gap: 10px; padding: 12px 14px 14px; }
.summary { flex-direction: column; align-items: stretch; gap: 8px; }
.stats { justify-content: space-between; gap: 4px; }
.stat { min-width: 0; flex: 1 1 0; }
.stat b { font-size: 15px; }
.stat label { font-size: 10px; }
.controls { justify-content: flex-end; gap: 10px; }
.footer { flex-direction: column; align-items: flex-start; gap: 4px; padding-bottom: 20px; }
.footer .version { margin-left: 0; }
}
@media (max-width: 640px) {
.grid { grid-template-columns: 1fr; padding: 12px 14px 8px; }
.group-tabs { padding: 12px 14px 0; }
.footer { padding: 8px 14px 24px; }
}
/* ================= 视图二:表格(高密度列表) ================= */
body[data-view="table"] .grid { display: block; }
.table-wrap {
overflow-x: auto;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
box-shadow: var(--shadow);
}
.monitor-table {
width: 100%;
/* 列宽按百分比分配 + 固定表格算法:表格永远恰好铺满容器宽度,不会撑出横向滚动 */
table-layout: fixed;
border-collapse: collapse;
font-size: 13px;
}
/* 行高基准:所有数据行恒为 --row-h,与内容长度彻底解耦。
内容再长也不会把某一行撑高(文本列各自限行截断 + 单元格 overflow:hidden)。
分组标题行另设 --group-row-h,用 :not() 排除掉,否则同样会被这条规则套上。 */
.monitor-table tbody tr:not(.group-row) { height: var(--row-h); }
.group-row td { height: var(--group-row-h); }
/* 表格列宽:状态 / 服务 / 目标 / 延迟 / 结果 / 最近检测 / 心率 / 90天可用率 = 100%
地址与结果列给足比例,避免长 URL、长名称被截断 */
.monitor-table th:nth-child(1) { width: 7%; }
.monitor-table th:nth-child(2) { width: 15%; }
.monitor-table th:nth-child(3) { width: 24%; }
.monitor-table th:nth-child(4) { width: 7%; }
.monitor-table th:nth-child(5) { width: 9%; }
.monitor-table th:nth-child(6) { width: 10%; }
.monitor-table th:nth-child(7) { width: 15%; }
.monitor-table th:nth-child(8) { width: 13%; }
.monitor-table th {
padding: 10px 12px;
text-align: left;
font-size: 12px;
font-weight: 600;
color: var(--text-2);
white-space: normal;
overflow: hidden;
text-overflow: ellipsis;
background: var(--bg);
border-bottom: 1px solid var(--border);
}
.monitor-table td { padding: 8px 12px; border-bottom: 1px solid var(--border); vertical-align: middle; overflow: hidden; line-height: 1.4; }
/* 所有行统一基线:可用率的 mini 条是 inline-flex,其基线会把行盒撑高十几像素,
导致带条的行比不带的行高出一截(实测 58px vs 43px) */
.monitor-table td > * { vertical-align: middle; }
.u-mini { display: inline-flex; align-items: center; gap: 6px; vertical-align: middle; }
/* 限行截断:文本再多也只占固定的两行,行高因此与内容长度彻底解耦。
height 而非 max-height —— 只有 1 行内容时也照样占满 2 行,行高才恒定;
max-height 会让短内容缩成 1 行、长内容撑到 2 行,反而出现两种行高。
-webkit-line-clamp 是 Chrome/Safari/Edge 的实现,height + overflow 兜底 Firefox。 */
.clamp2 {
display: -webkit-box;
-webkit-line-clamp: 2;
line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
height: 2.8em;
}
.monitor-table tbody tr[data-status="pending"] { background: color-mix(in srgb, var(--pending) 6%, transparent); }
.monitor-table tbody tr:last-child td { border-bottom: 0; }
.monitor-table tbody tr:hover { background: color-mix(in srgb, var(--accent) 5%, transparent); }
.monitor-table tbody tr[data-status="down"] { background: color-mix(in srgb, var(--down) 6%, transparent); }
.group-row td { padding: 7px 12px; font-size: 12px; font-weight: 650; color: var(--text-2); background: var(--bg); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.t-state { white-space: nowrap; }
.t-state .dot { display: inline-block; margin-right: 6px; vertical-align: middle; }
tr[data-status="up"] .t-state { color: var(--up-text); }
tr[data-status="down"] .t-state { color: var(--down-text); }
tr[data-status="pending"] .t-state { color: var(--pending-text); }
/* 名称列:文本区限两行,徽标区固定单行高度 —— 整格高度恒定 */
.t-name { white-space: normal; }
.t-name .clamp2 { overflow-wrap: anywhere; }
.t-tags {
display: flex;
align-items: center;
gap: 6px;
height: 16px;
margin-top: 2px;
overflow: hidden;
white-space: nowrap;
}
.t-name .type-badge { margin-left: 0; }
.t-target {
font-family: var(--mono);
font-size: 11.5px;
line-height: 1.45;
color: var(--text-2);
white-space: normal;
}
.t-target .clamp2 { overflow-wrap: anywhere; word-break: break-all; }
.t-num { font-variant-numeric: tabular-nums; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* 证书 / 维护徽标:出现在名称旁,不占独立列 */
.tls-badge, .maint-badge {
margin-left: 8px;
padding: 0 6px;
border-radius: 999px;
font-size: 10px;
font-weight: 650;
vertical-align: middle;
white-space: nowrap;
cursor: help;
}
.tls-badge.warn { background: var(--pending-bg); color: var(--pending-text); }
.tls-badge.danger { background: var(--down-bg); color: var(--down-text); }
.maint-badge { background: color-mix(in srgb, var(--accent) 14%, transparent); color: var(--accent); }
.card-head .tls-badge, .card-head .maint-badge { flex: none; }
.t-hb svg { display: block; width: 100%; max-width: 150px; height: 24px; }
.t-uptime { white-space: normal; }
/* 可用率条随视口伸缩;极窄时只留百分比,避免挤压其他列 */
.uptime-bar-mini { display: flex; flex: none; width: clamp(32px, 6vw, 84px); height: 8px; margin-right: 0; vertical-align: middle; }
.t-pct { font-size: 12px; color: var(--text-2); font-variant-numeric: tabular-nums; }
@media (max-width: 640px) {
.uptime-bar-mini { display: none; }
}
/* ---------- 最近事件:时间线风格(故障 / 恢复 / 证书提醒) ---------- */
.events {
max-width: 1200px;
margin: 18px auto 0;
padding: 0 20px;
}
.events-head { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; }
.events-head h2 { margin: 0; font-size: 13px; font-weight: 650; color: var(--text); letter-spacing: 0.02em; }
.events-count { font-size: 11px; color: var(--text-2); }
.events-more {
margin-left: auto;
padding: 3px 10px;
font-size: 11px;
color: var(--accent);
background: color-mix(in srgb, var(--accent) 8%, transparent);
border: 1px solid color-mix(in srgb, var(--accent) 30%, transparent);
border-radius: 999px;
cursor: pointer;
}
.events-more:hover { background: color-mix(in srgb, var(--accent) 16%, transparent); }
/* 事件表:直接复用监控表格的外观(圆角、边框、行分隔、hover、状态底色),
只额外定义列宽与类型徽标,保证两处视觉完全一致 */
/* 事件表:显式声明与监控表格相同的观感(不共用 .monitor-table 类,
避免被其他按类名统计/定位的逻辑误算),列宽与类型徽标单独定义 */
.events-table { width: 100%; table-layout: fixed; border-collapse: collapse; font-size: 13px; }
.events-table tbody tr { height: var(--row-h); }
.events-table th {
padding: 10px 12px;
text-align: left;
font-size: 12px;
font-weight: 600;
color: var(--text-2);
background: var(--bg);
border-bottom: 1px solid var(--border);
white-space: normal;
overflow: hidden;
text-overflow: ellipsis;
}
.events-table td { padding: 10px 12px; border-bottom: 1px solid var(--border); vertical-align: middle; line-height: 1.4; overflow: hidden; }
.events-table tbody tr:last-child td { border-bottom: 0; }
.events-table tbody tr:hover { background: color-mix(in srgb, var(--accent) 5%, transparent); }
.events-table tbody tr[data-status="down"] { background: color-mix(in srgb, var(--down) 6%, transparent); }
.events-table tbody tr[data-status="pending"] { background: color-mix(in srgb, var(--pending) 6%, transparent); }
.events-wrap { margin-top: 0; }
.events-table th:nth-child(1) { width: 11%; }
.events-table th:nth-child(2) { width: 9%; }
.events-table th:nth-child(3) { width: 26%; }
.events-table th:nth-child(4) { width: 54%; }
.ev-time { color: var(--text-2); font-variant-numeric: tabular-nums; white-space: nowrap; }
.ev-badge {
display: inline-block;
padding: 1px 8px;
border-radius: 999px;
font-size: 11px;
font-weight: 650;
}
tr[data-status="down"] .ev-badge { background: var(--down-bg); color: var(--down-text); }
tr[data-status="up"] .ev-badge { background: var(--up-bg); color: var(--up-text); }
tr[data-status="pending"] .ev-badge { background: var(--pending-bg); color: var(--pending-text); }
.ev-maint { margin-left: 6px; border-radius: 999px; padding: 0 6px; font-size: 10px; font-weight: 650; background: color-mix(in srgb, var(--accent) 14%, transparent); color: var(--accent); }
.ev-name { font-weight: 600; }
.ev-detail { color: var(--text-2); }
/* 事件表的长文本同样限两行,保证与监控表格一致的等高行 */
.ev-name, .ev-detail { white-space: normal; }
.ev-name > .clamp2, .ev-detail > .clamp2 { overflow-wrap: anywhere; }
.events-empty { margin: 0; padding: 12px; font-size: 12px; color: var(--text-2); background: var(--surface); border: 1px dashed var(--border); border-radius: 12px; }
@media (max-width: 640px) {
.events { padding: 0 14px; }
.events-table th:nth-child(4), .ev-detail { display: none; }
}
/* ---------- 卡片:90 天日均延迟趋势 ---------- */
.lat-trend { display: flex; align-items: center; gap: 8px; margin-top: 2px; }
.lat-label { font-size: 11px; color: var(--text-2); flex: none; }
.lat-bars { display: flex; align-items: flex-end; gap: 1px; height: 20px; flex: 1; min-width: 0; }
.lat-cell { flex: 1; min-width: 2px; background: color-mix(in srgb, var(--accent) 50%, transparent); border-radius: 1px; }
.lat-cell:hover { background: var(--accent); }
.lat-avg { font-size: 11px; color: var(--text-2); font-variant-numeric: tabular-nums; flex: none; }