Files
rond-with-you/internal/web/static/css/app.css
T
tamakyi 7ab33227a5 专题显示区改成在地图上点选(后台编辑器)
用户诉求的后半:不该让他手填经纬度和半径,应该在地图上圈。三个手填输入框
换成地图 + 圆列表:点地图加圆、拖圆心移动、拖圆边改半径、列表里可改半径/
定位/删除、可一次清空;改动立刻写进隐藏字段 areas(JSON) 与 crs,随专题表单
一起提交。

- 复用 /admin/fog-mask 那套约定:/api/map-config 取底图、底图坐标系随表单带上、
  后端换算成 GCJ-02。不另起炉灶。
- areas 的初值**渲染进隐藏字段**(不是只塞给 JS):脚本没加载上时提交的仍是原值,
  否则一次保存就把已有显示区静默清空。
- 地图初始视野取生效数据集的地点包围盒(placesBounds),一上来就落在自己的地界上,
  而不是全国视野。
- 上限(20 个 / 0.05~2000 km)前端也校验一份,并**禁用提交**;两边数字用守卫比对,
  免得出现「前端放行、后端拒绝,用户不知道改哪里」。

实测抓到并修掉的 bug:/admin/topics 没设 p.HasMap ⇒ 布局不引入 Leaflet,
表现是脚本加载了、rondTopicAreas 也在,就是 window.L 永远 undefined,init 静默
返回、控制台不报错、页面像「什么都没发生」。已加守卫钉住这一点。

其余:
- topic-areas.js 只暴露纯函数(校验/序列化/增删改的不可变性/几何换算)供 node 单测,
  另加一个最小测试钩子 api._map 供无头验证(同 window.rondMap._state 惯例)。
- CSS 用定宽容器装半径输入框:站点全局 input 规则是 width:100% 且带 10 个 :not()
  (特异性 (0,11,1)),直接给 input 写宽度压不过。
- gofmt:文档注释里的缩进列表按 Go 1.19 的规则重排;顺带把工作区的 CRLF 归一到 LF
  (.gitattributes 早就声明 eol=lf,blob 本来也是 LF,所以 git diff 无额外噪声)。

验证:
- topic-areas.test.js 44 条(含「增删改返回新数组不原地改」、半径换算自洽性)。
- 三个新守卫:两边上限一致、模板契约(旧输入框必须没了 + 挂点齐全 + 初值来源)、
  topicsAdminPage 必须设 HasMap。
- 无头端到端:两击成两圆 → 改半径 7.5 → 删第 1 个 → 越界半径被拦且禁用提交 →
  提交 200 且落库 areas=[{lat:24.72,lon:108.1,radiusKm:7.5}](废弃列镜像 7.5);全程无 JS 报错。
2026-10-12 17:35:00 +08:00

1325 lines
80 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 {
--accent: #2f7d6e;
--accent-soft: #e8f3f0;
--bg: #f6f7f8;
--panel: #ffffff;
--line: #e5e7ea;
--line-soft: #f0f1f3;
--text: #1f2328;
--text-2: #5b6169;
/* 次要文字。原来是 #8b9199,对白底只有 3.18:1、对浅灰底 2.81:1,
低于 WCAG AA 的 4.5:1(页面副标题、KPI 标签、筛选计数全用这个色阶)。
改成 #666d75 之后:白底 5.24 / 浅灰底 4.63 / 绿浅底 4.62,
既达标又仍比 --text-2(5.53)浅,层次没丢。 */
--text-3: #666d75;
/* 会随明暗切换的「面」色,抽出来暗色只需换这一组 */
--surface: #ffffff; /* 输入框 / 按钮 / 浮层的底 */
--glass: rgba(255, 255, 255, .92); /* 顶栏毛玻璃 */
--glass-strong: rgba(255, 255, 255, .97); /* 地图面板 / 图例 */
--row-hover: #fbfcfc;
--bg-2: #fafbfb; /* 比卡片略深一层的面(行内编辑表单、空态框) */
--on-accent: #ffffff; /* 主色底上的文字 */
/* ---------- 类型色(活动 / 交通方式)----------
色名是 rond 里的 Apple 日历色名(与 Go 侧 appleColors 的键一一对应,模板经
csscolor / csscolorsoft 取用)。地图那份调色板在 rond-map.js 里、**故意独立**:
地图瓦片在暗色下保持浅色,标记得用浅色底上那套值,不能跟着页面的暗色变量走。
⚠️ 这套色的**明度差异很大**:在白底上 yellow 1.87、mint 2.12、orange 2.20、green 2.22、
cyan 2.54、teal 2.57 —— 都远低于图形对象 3:1。所以它们只能当「身份标记」(圆点、柱条),
**别拿去当文字色或承载唯一信息的图形**,这也正是全站只用小圆点而不是彩色标签的原因。 */
--cat-red: #ff3b30; --cat-orange: #ff9500; --cat-yellow: #e8b700;
--cat-green: #34c759; --cat-mint: #00c7be; --cat-teal: #30b0c7;
--cat-cyan: #32ade6; --cat-blue: #007aff; --cat-indigo: #5856d6;
--cat-purple: #af52de; --cat-pink: #ff2d55; --cat-brown: #a2845e;
--cat-gray: #8e8e93;
/* 色名不认识时的兜底,与 Go 里 cssColor 的 var(--cat-none) 对齐 */
--cat-none: #5b6169;
/* 浅底(emoji 方块那种):半透明叠在卡片上,浅色暗色自适应。
原来是模板里给 hex 拼 "22" 当 alpha——换成 3 位色值或 var() 就散了,这里收口。 */
--cat-red-soft: rgba(255, 59, 48, .14); --cat-orange-soft: rgba(255, 149, 0, .14);
--cat-yellow-soft: rgba(232, 183, 0, .14); --cat-green-soft: rgba(52, 199, 89, .14);
--cat-mint-soft: rgba(0, 199, 190, .14); --cat-teal-soft: rgba(48, 176, 199, .14);
--cat-cyan-soft: rgba(50, 173, 230, .14); --cat-blue-soft: rgba(0, 122, 255, .14);
--cat-indigo-soft: rgba(88, 86, 214, .14); --cat-purple-soft: rgba(175, 82, 222, .14);
--cat-pink-soft: rgba(255, 45, 85, .14); --cat-brown-soft: rgba(162, 132, 94, .16);
--cat-gray-soft: rgba(142, 142, 147, .16);
--cat-none-soft: rgba(91, 97, 105, .14);
--ok-bg: #eef8f5; --ok-fg: #17604f; --ok-line: #cfe6e0;
--err-bg: #fdf3f2; --err-fg: #b42318; --err-line: #f3dcda;
--info-bg: #f2f6fd; --info-fg: #2c5282; --info-line: #dbe5f5;
/* 圆角 4 档。原来是 7 / 8 / 9 / 10 一堆近似值混着用,肉眼分不出、改一处要改一片。 */
--r-xs: 3px; /* 小方块(热力格、气温柱顶) */
--r-sm: 6px; /* 控件(按钮、输入框、导航链接、标签) */
--r-md: 8px; /* 中号面(缩略图、提示条) */
--r-lg: 12px; /* 卡片与浮层 */
/* 阴影 3 档,暗色模式整套覆盖(见文件末尾的 prefers-color-scheme) */
--sh-1: 0 1px 2px rgba(16, 24, 40, .04), 0 1px 3px rgba(16, 24, 40, .06);
--sh-2: 0 4px 14px rgba(16, 24, 40, .10), 0 1px 3px rgba(16, 24, 40, .06);
--sh-3: 0 10px 30px rgba(16, 24, 40, .12), 0 2px 6px rgba(16, 24, 40, .06);
/* 间距 5 档。本批只用在卡片内边距这类「主节奏」上,其余逐批替换。 */
--sp-1: 4px; --sp-2: 8px; --sp-3: 12px; --sp-4: 16px; --sp-5: 24px;
/* 卡片内边距单列一个变量:折叠卡片的标题行要用负 margin 顶到边(见 .filters-card),
两处必须永远相等,所以绑成同一个值。 */
--card-pad: 18px;
/* ---------- 字阶 ----------
文本只有 6 档。原来是 10/10.5/11/11.5/12/12.5/12.8/13/13.5/14/14.5/15/17/18/21px
十几档挤在一起,12~15px 之间就塞了 4 档 —— 层级挤不出来,只能靠加粗硬顶。 */
--fs-xs: 11px; /* 脚注 / 单位 / 轴标 / 图例 */
--fs-sm: 13px; /* 正文(默认) */
--fs-md: 15px; /* 卡片标题 / 强调文本 */
--fs-lg: 21px; /* 页面标题 */
--fs-xl: 26px; /* KPI 数值 */
--fs-2xl: 34px; /* 首屏主数 */
/* 表单控件字号单独一档:窄屏必须 ≥16px,否则 iOS 一聚焦就把整页放大。
它不属于文本层级,所以不并进上面的字阶。 */
--fs-control: 16px;
/* 字形尺寸(✕ / 箭头 / 空态图标 / 表情):按控件尺寸定,不是文字层级。
故意不进字阶 —— 把 22px 的 ✕ 塞进 26px 的字阶只会把按钮撑坏。 */
--glyph-xs: 12px;
--glyph-sm: 15px;
--glyph-md: 18px;
--glyph-lg: 22px;
--glyph-xl: 30px;
--glyph-2xl: 40px;
--mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
/* 顶栏高度:窄屏更矮。地图页的定位跟它对齐,避免出现几像素的缝。 */
--nav-h: 54px;
/* 地图页浮层的层级:Leaflet 自己的 z-index 封顶在 1000(.leaflet-top/.leaflet-bottom,
见 vendor/leaflet/leaflet.css),而 #map 那一层没有建立层叠上下文 ——
站点的浮层排不到 1000 之上,就会被左上角的缩放按钮、右下的比例尺压住。
分两档是为了保住原有次序:加载状态条要压在所有浮层之上。 */
--z-map-overlay: 1100;
--z-map-status: 1150;
/* 顶栏。必须压过地图页的浮层(--z-map-overlay = 1100):窄屏的导航菜单是从顶栏
展开的,而它属于 .nav 这个层叠上下文、自己抬不动 —— 只有把整条顶栏放到浮层之上,
展开后才不会被地图的筛选面板盖住(实测过 elementFromPoint)。 */
--z-nav: 1200;
/* 刘海屏 / 底部 Home 指示条的安全区。不支持 env() 的浏览器回落到 0。 */
--safe-t: env(safe-area-inset-top, 0px);
--safe-r: env(safe-area-inset-right, 0px);
--safe-b: env(safe-area-inset-bottom, 0px);
--safe-l: env(safe-area-inset-left, 0px);
}
* { box-sizing: border-box; }
html, body {
margin: 0;
padding: 0;
background: var(--bg);
color: var(--text);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Noto Sans SC", sans-serif;
font-size: var(--fs-sm);
line-height: 1.6;
-webkit-font-smoothing: antialiased;
}
a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }
h1, h2, h3, h4 { margin: 0; font-weight: 600; letter-spacing: -.01em; }
/* 显式给字号。这条规则以前只管字重和 margin,字号靠浏览器默认的 2em / 1.5em / 1.17em
—— 也就是跟着**根字号**漂:根从 14px 降到 13px 后,没被 `.card > h2` 命中的那些 h2
从 21px 悄悄变成 19.5px(/stats 的「我的结论」「访客点赞」两块),比同页卡片标题大一圈。 */
h1 { font-size: var(--fs-lg); }
h2 { font-size: var(--fs-md); }
h3, h4 { font-size: var(--fs-sm); }
/* <b>/<strong> 的浏览器默认是 bold(700),而全站层级只到 600 —— 不钉住的话
行内加粗会比标题还重。 */
b, strong { font-weight: 600; }
/* ---------- 顶栏 ---------- */
.nav {
position: sticky; top: 0; z-index: var(--z-nav);
background: var(--glass);
backdrop-filter: saturate(1.6) blur(10px);
border-bottom: 1px solid var(--line);
/* 横屏时刘海在左右,竖屏时状态栏在顶部 */
padding: var(--safe-t) var(--safe-r) 0 var(--safe-l);
}
.nav-inner {
max-width: 1120px; margin: 0 auto; padding: 0 20px;
/* 菜单是相对 .nav-inner 绝对定位的(要铺满整条),所以这里得有定位上下文 */
position: relative;
height: var(--nav-h); display: flex; align-items: center; gap: 22px;
}
.brand { display: flex; align-items: center; gap: 8px; font-weight: 600; color: var(--text); font-size: var(--fs-md); }
.brand:hover { text-decoration: none; }
.brand .avatar { font-size: var(--glyph-md); }
/* 链接行自带横向兜底:620px 以下整行会收进 .nav-menu,这条就只为「以后又加了入口、
宽屏也放不下」兜底 —— 宁可让它能滑,也别把最右边那个字切掉一半。 */
.nav-links { display: flex; gap: 4px; flex: 1; overflow-x: auto; scrollbar-width: none; }
.nav-links::-webkit-scrollbar { display: none; }
/* 还能往右滑时右缘渐隐(由 nav 的脚本按 scrollLeft 加/去这个类)。用 mask 而不是盖一层
渐变伪元素:伪元素会把链接文字一起遮住,mask 是让内容本身渐隐。 */
.nav-links.overflow-right {
-webkit-mask-image: linear-gradient(to right, #000 calc(100% - 28px), transparent);
mask-image: linear-gradient(to right, #000 calc(100% - 28px), transparent);
}
.nav-links a {
padding: 5px 11px; border-radius: var(--r-sm); color: var(--text-2); font-size: var(--fs-sm); white-space: nowrap;
}
.nav-links a:hover { background: var(--line-soft); color: var(--text); text-decoration: none; }
.nav-links a.on { background: var(--accent-soft); color: var(--accent); font-weight: 500; }
.nav-right { display: flex; align-items: center; gap: 10px; font-size: var(--fs-sm); color: var(--text-3); }
/* 窄屏导航菜单。620px 以下链接行一定放不下(390 溢 63px、320 溢 133px),整行收进这里。
用原生 <details>,不靠 JS 也能开合;JS 只补三件原生不给的事:点外面 / Esc / 点链接后收起。 */
.nav-menu { display: none; }
.nav-menu > summary {
list-style: none; cursor: pointer; user-select: none; white-space: nowrap;
display: inline-flex; align-items: center; gap: 4px;
min-height: 44px; padding: 0 10px; border-radius: var(--r-sm);
color: var(--text-2); font-size: var(--fs-sm);
}
.nav-menu > summary::-webkit-details-marker { display: none; }
.nav-menu > summary::after { content: "▾"; font-size: var(--glyph-xs); color: var(--text-3); }
.nav-menu[open] > summary { background: var(--accent-soft); color: var(--accent); }
.nav-menu[open] > summary::after { content: "▴"; }
.nav-menu-body {
position: absolute; left: 0; right: 0; top: 100%; z-index: 1;
display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 2px; padding: 6px;
background: var(--panel); border-bottom: 1px solid var(--line); box-shadow: var(--sh-2);
}
.nav-menu-body a {
display: flex; align-items: center; min-height: 44px; padding: 0 12px;
border-radius: var(--r-sm); color: var(--text-2); font-size: var(--fs-sm);
}
.nav-menu-body a:hover { background: var(--line-soft); color: var(--text); text-decoration: none; }
.nav-menu-body a.on { background: var(--accent-soft); color: var(--accent); font-weight: 500; }
/* ---------- 布局 ---------- */
.wrap { max-width: 1120px; margin: 0 auto;
padding: 22px calc(20px + var(--safe-r)) calc(60px + var(--safe-b)) calc(20px + var(--safe-l)); }
.page-head { margin-bottom: 18px; display: flex; align-items: flex-end; justify-content: space-between; gap: 16px; flex-wrap: wrap; }
.page-head h1 { font-size: var(--fs-lg); }
.page-head .sub { color: var(--text-3); font-size: var(--fs-sm); margin-top: 3px; }
.card {
background: var(--panel); border: 1px solid var(--line); border-radius: var(--r-lg);
box-shadow: var(--sh-1); padding: var(--card-pad); margin-bottom: var(--sp-4);
}
.card > h2 { font-size: var(--fs-md); margin-bottom: 2px; display: flex; align-items: center; gap: var(--sp-2); }
/* .card-body 是数据编辑页折叠用的容器,卡片内容被它包住后不再是 .card 的直接子元素,
所以下面两条用「后代」而不是「子」选择器——否则折叠脚本一加上,间距就全没了。 */
.card .card-sub { color: var(--text-3); font-size: var(--fs-sm); margin-bottom: var(--sp-3); }
.card.collapsed .card-body { display: none; }
/* 卡片三档。原来全站只有一种白盒(量下来 17 个盒子只有 2 种样式),KPI / 地图 / 图表 /
表格 / 筛选器全等重,扫视时没有落点。分档不是装饰,是为了「一眼知道先看哪」:
hero 只给首屏主数区与地图,quiet 只给纯说明性的内容。 */
.card--hero { border-color: transparent; box-shadow: var(--sh-3); padding: calc(var(--card-pad) + var(--sp-2)); }
/* quiet 用页面底色 + 细边 + 无阴影 = 贴在页面上的扁平面板。
注意别拿 --bg-2 (#fafbfb) 当底色:它和页面底 #f6f7f8 只差 4/255,
实测根本看不出边界,卡片会像「破了个洞」而不是「轻了一档」。 */
.card--quiet { background: var(--bg); border-color: var(--line); box-shadow: none; }
.card--quiet > h2 { font-size: var(--fs-sm); color: var(--text-2); }
/* 首屏主数:把最重要的那个数从 KPI 格里拎出来单独一行。
原来 8 个数字一样大一样重,扫过去不知道看哪个 —— 层级靠「只让一个特别大」,
而不是把所有的都调大。 */
.kpi-lead {
position: relative; overflow: hidden;
background: var(--panel); border: 1px solid var(--line); border-radius: var(--r-lg);
box-shadow: var(--sh-3);
padding: calc(var(--card-pad) + var(--sp-1)) var(--card-pad) var(--card-pad);
margin-bottom: var(--sp-3);
}
.kpi-lead::before {
content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 3px; background: var(--accent);
}
.kpi-lead .kl-v {
font-size: clamp(var(--fs-xl), 7.2vw, var(--fs-2xl)); font-weight: 600;
letter-spacing: -.02em; line-height: 1.1; font-variant-numeric: tabular-nums;
}
.kpi-lead .kl-v small { font-size: var(--fs-md); font-weight: 500; color: var(--text-3); margin-left: var(--sp-1); }
.kpi-lead .kl-k { color: var(--text-2); font-size: var(--fs-sm); margin-top: var(--sp-1); }
.kpi-lead .kl-x { color: var(--text-3); font-size: var(--fs-sm); margin-top: var(--sp-2); }
/* 只有带 data-card 的卡片(数据编辑页)能折叠:标题整行可点,右侧一个箭头 */
.card[data-card] > h2 { cursor: pointer; }
.card[data-card] > h2 .card-toggle {
margin-left: auto; flex: 0 0 auto; cursor: pointer;
background: none; border: 0; padding: 0 2px; color: var(--text-3);
font: inherit; font-size: var(--glyph-xs); line-height: 1;
}
.card[data-card] > h2 .card-toggle:hover { color: var(--text); }
/* align-items: start 是必须的:grid 默认 stretch,同一行里内容少的那张卡会被拉到与
最高的一样高,卡片内部留一大片空白(/stats 的「类型分布」60 多行 vs「一天中的分布」
24 根柱就是这么来的)。改成各按自然高度,短的那张下面露的是页面底色,而不是空白卡面。 */
.grid { display: grid; gap: 16px; align-items: start; }
/* grid 子项默认 min-width:auto(= 不小于自身 min-content),而卡片里常常放着
带 min-width 的表格——不加这条,卡片会拒绝收缩、把**整页**顶宽,出现横向滚动条
(/stats、/report 在手机上整页 692px 就是这么来的)。压到 0 之后由卡片内部的
.tw 自己横向滚动。 */
.grid > * { min-width: 0; }
.g2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.g3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.g4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
@media (max-width: 900px) { .g3, .g4 { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 620px) {
:root { --nav-h: 50px; }
.g2, .g3, .g4 { grid-template-columns: 1fr; }
.wrap { padding: 16px calc(14px + var(--safe-r)) calc(48px + var(--safe-b)) calc(14px + var(--safe-l)); }
.nav-inner { padding: 0 10px; gap: 6px; }
/* 窄屏只留头像 */
.brand .t { display: none; }
/* 链接行整行收进 .nav-menu(样式见上面那段),这里不再横向滚动 */
.nav-links { display: none; }
.nav-menu { display: block; flex: 1; }
.nav-right .who { display: none; }
.nav-right .btn.sm { padding: 4px 9px; font-size: var(--fs-sm); }
}
/* ---------- 窄屏触控与表单(移动端适配) ---------- */
/* 手指的落点比鼠标粗得多:导航链接原本只有 40×31、筛选标签 59×28、小按钮 45×30,
都低于 44×44 的通行下限,最容易点的就是它们。这里只放宽尺寸与内边距,不动结构。 */
@media (max-width: 620px) {
.nav-links a, .btn, .btn.sm, .chip { min-height: 44px; }
.nav-links a { display: inline-flex; align-items: center; padding: 8px 9px; }
/* 内边距与控件尺寸的窄屏覆盖在文件末尾(那几组选择器的基础规则在下面,
写在这里会被它们盖掉);这里只放基础规则里没有的属性。 */
/* iOS 聚焦字号小于 16px 的控件时会自动放大整页——用户点完输入框页面突然变大,
还要手动缩回去。窄屏统一提到 16px 规避(桌面端不受影响,保持紧凑)。 */
:root { --field-fs: 16px; }
/* 表格里的控件仍然紧凑:后台那张表列很多,16px 会把列撑得很难用。
表格里横向可滚,iOS 缩放的影响也小。 */
.photo-table, table.tb { --field-fs: 14px; }
}
.only-sm { display: none; }
@media (max-width: 620px) {
.hide-sm { display: none; }
.only-sm { display: inline; }
/* 窄屏用地势与手势缩放,把控件让给底部 */
.map-panel { top: 8px; }
}
/* ---------- 统计数字 ---------- */
/* grid-auto-rows: 1fr 让所有格子等高:否则某一格的值换行时,那一整行都比别行高 */
/* 用 flex + grow 而不是固定列数的网格:KPI 个数常常是 6、7 这种除不尽列数的,
网格会在最后一行留下空格子——而间隙是用底色画的,空格子就露成灰块,很难看。
flex 会让最后一行自己撑满,任何个数都不会有空格。 */
.kpis { display: flex; flex-wrap: wrap; gap: 1px; background: var(--line); border-radius: var(--r-lg); overflow: hidden; border: 1px solid var(--line); }
.kpi { flex: 1 1 158px; min-width: 0; background: var(--panel); padding: var(--sp-4); }
/* 字号跟着视口收:窄屏上「112天19小时」这种值放不下就会折成两行,把整行撑高 */
.kpi .v { font-size: clamp(var(--fs-md), 5.6vw, var(--fs-xl)); font-weight: 600; letter-spacing: -.02em; font-variant-numeric: tabular-nums; }
.kpi .v small { font-size: var(--fs-sm); font-weight: 500; color: var(--text-3); margin-left: 3px; }
.kpi .k { color: var(--text-3); font-size: var(--fs-sm); margin-top: 1px; }
/* ---------- 图表(纯 CSS/SVG) ---------- */
.bars { display: flex; flex-direction: column; gap: 9px; }
.bar-row { display: grid; grid-template-columns: 128px 1fr 104px; align-items: center; gap: 10px; font-size: var(--fs-sm); }
/* 整行可点(下钻为筛选):给一点悬停反馈,并让点击区域覆盖整行;
柱条本身只有 9px 高,靠「行」来接收点击才够手指点 */
a.bar-row { margin: 0 -6px; padding: 4px 6px; border-radius: var(--r-sm); color: inherit; text-decoration: none; }
a.bar-row:hover { background: var(--line-soft); text-decoration: none; }
a.col { display: block; text-decoration: none; }
a.col:hover { opacity: 1; filter: brightness(1.12); }
.bar-row .nm { color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.bar-track { background: var(--line-soft); border-radius: var(--r-sm); height: 9px; overflow: hidden; }
.bar-fill { height: 100%; border-radius: var(--r-sm); background: var(--accent); min-width: 2px; }
.bar-row .vv { text-align: right; color: var(--text-2); font-variant-numeric: tabular-nums; font-size: var(--fs-sm); white-space: nowrap; }
@media (max-width: 620px) {
.bar-row { grid-template-columns: 84px 1fr 92px; }
/* 图表行在手机上要能点中:整行 ~37px。再往上加(44px)会把「类型分布」这种
60 多行的图撑到两倍高,翻起来更难受,这里取个折中。 */
a.bar-row { padding: 8px 6px; }
}
/* 基线:柱子底下压一道浅线,让「高度从哪儿量起」有个参照(原来柱子直接悬在空白里,
高低只能靠互相比)。
padding-top 是给峰值柱顶上那个数字留的位置:height 走 border-box,内容区仍是 120px。 */
.cols { position: relative; display: flex; align-items: flex-end; gap: 3px; height: 136px; padding-top: 16px; border-bottom: 1px solid var(--line); }
.cols .col { position: relative; flex: 1; background: var(--accent); border-radius: var(--r-xs) var(--r-xs) 0 0; min-height: 2px; opacity: .8; }
.cols .col:hover { opacity: 1; }
/* 峰值柱:不透明拉满,并在顶上标出次数。数值本来写在副标题里,但它不告诉你「哪一根最高」,
标在柱子上才是能一眼找到的锚点。 */
.cols .col.peak { opacity: 1; }
.cols .col .pv {
position: absolute; left: 50%; bottom: calc(100% + 2px); transform: translateX(-50%);
font-size: var(--fs-xs); line-height: 1.2; color: var(--text-2);
font-variant-numeric: tabular-nums; white-space: nowrap;
}
.cols-x { display: flex; gap: 3px; font-size: var(--fs-xs); color: var(--text-3); margin-top: 5px; }
.cols-x span { flex: 1; text-align: center; overflow: hidden; white-space: nowrap; }
/* 一天 24 根柱,光看柱子分不出「上午 / 下午」。这条标尺用同一套 24 列网格(不是 flex:6),
所以每段边界和柱子严格对齐;每段直接写上时段和小时区间,就不用再单独留一行坐标轴。 */
.cols-band { display: grid; grid-template-columns: repeat(24, minmax(0, 1fr)); gap: 3px; margin-top: var(--sp-2); }
.cols-band span {
grid-column: span 6; text-align: center; white-space: nowrap;
font-size: var(--fs-xs); color: var(--text-3);
background: var(--line-soft); border-radius: var(--r-xs); padding: 2px 0;
}
/* ---------- 逐月气温 ---------- */
.temp-chart { margin-top: 16px; }
.temp-head { font-size: var(--fs-sm); color: var(--text-3); margin-bottom: 8px; }
/* 横向可滚动:柱子固定宽,十几二十个月挤在窄屏上也不会把标签压成一团 */
.temp-strip { display: flex; gap: 4px; overflow-x: auto; padding-bottom: 4px; }
.temp-col { flex: 0 0 36px; display: flex; flex-direction: column; align-items: center; gap: 3px; cursor: default; }
/* 宽屏反过来:柱子要跟着卡片长、轨道也要加高。原来固定 36×84,10 个月只占 1042px 里的
396px —— 右边 600 多像素全空着,整张图看着就「很小」。上限 96px 是防止只有两三个月时
柱子宽得不像柱状图。 */
@media (min-width: 720px) {
.temp-col { flex: 1 1 36px; max-width: 96px; }
.temp-col .tc-track { height: 130px; }
.temp-col .tc-v, .temp-col .tc-m { font-size: var(--fs-sm); }
}
.temp-col .tc-v { font-size: var(--fs-xs); color: var(--text-2); font-variant-numeric: tabular-nums; }
.temp-col .tc-track { width: 100%; height: 84px; display: flex; align-items: flex-end; }
.temp-col .tc-bar { width: 100%; border-radius: var(--r-xs) var(--r-xs) 0 0; min-height: 3px; }
.temp-col .tc-m { font-size: var(--fs-xs); color: var(--text-3); white-space: nowrap; }
.temp-col:hover .tc-bar { filter: brightness(1.08); }
.temp-col:hover .tc-v { color: var(--text); }
/* ---------- 日历热力 ---------- */
.heat { display: flex; gap: 3px; overflow-x: auto; padding-bottom: 4px; }
.heat .wk { display: flex; flex-direction: column; gap: 3px; }
.heat .d { width: 11px; height: 11px; border-radius: var(--r-xs); background: var(--line-soft); }
.heat .d.l1 { background: #cfe6e0; }
.heat .d.l2 { background: #9dcfc3; }
.heat .d.l3 { background: #5fae9c; }
.heat .d.l4 { background: var(--accent); }
.heat-legend { display: flex; align-items: center; gap: 5px; font-size: var(--fs-xs); color: var(--text-3); margin-top: 8px; }
.heat-legend .d { width: 11px; height: 11px; border-radius: var(--r-xs); }
/* ---------- 列表 ---------- */
.list { display: flex; flex-direction: column; }
.item {
display: grid; grid-template-columns: 40px 1fr auto; gap: 12px; align-items: center;
padding: 11px 2px; border-bottom: 1px solid var(--line-soft);
}
.item:last-child { border-bottom: 0; }
.item .ic {
width: 34px; height: 34px; border-radius: var(--r-md); display: flex; align-items: center; justify-content: center;
background: var(--line-soft); font-size: var(--glyph-md);
}
.item .t { font-weight: 500; }
.item .t a { color: var(--text); }
.item .m { color: var(--text-3); font-size: var(--fs-sm); margin-top: 1px; }
.item .r { text-align: right; color: var(--text-2); font-size: var(--fs-sm); white-space: nowrap; }
.chips { display: flex; flex-wrap: wrap; gap: 6px; }
.chip {
display: inline-flex; align-items: center; gap: 5px; padding: 3px 9px; border-radius: 999px;
background: var(--line-soft); color: var(--text-2); font-size: var(--fs-sm); border: 1px solid transparent;
}
.chip.on { background: var(--accent-soft); color: var(--accent); border-color: #cfe6e0; font-weight: 500; }
.chip .dot { width: 7px; height: 7px; border-radius: 50%; background: currentColor; }
.tag { display: inline-block; padding: 1px 7px; border-radius: var(--r-sm); background: var(--accent-soft); color: var(--accent); font-size: var(--fs-xs); }
/* ---------- 表单 ---------- */
label.f { display: block; margin-bottom: 13px; }
label.f > span { display: block; font-size: var(--fs-sm); color: var(--text-2); margin-bottom: 5px; }
/* 必填标记:字段名后的红色星号,以及表单顶部的统一说明 */
.req { color: #b42318; font-weight: 600; margin-left: 2px; }
.req-tip { font-size: var(--fs-sm); color: var(--text-2); margin: 0 0 10px; }
.req-tip .req { margin-right: 3px; }
/* flatpickr 日期时间选择器:贴合站点风格 */
.flatpickr-calendar { border-radius: var(--r-lg); box-shadow: var(--sh-2); font-family: inherit; }
.flatpickr-day.selected, .flatpickr-day.selected:hover { background: var(--accent); border-color: var(--accent); }
.flatpickr-day.today { border-color: var(--accent); }
.flatpickr-day.today:hover { background: var(--accent-soft); color: var(--text); }
.flatpickr-day:hover { background: var(--accent-soft); }
/* 控件字号走变量:窄屏要整体提到 16px(iOS 聚焦小于 16px 会自动放大整页),
而基础选择器 input[type=…], select, textarea 的特异性高于媒体查询里的单选择器,
直接在里面写 font-size 是压不过的——用可继承的自定义属性就没有这个问题。 */
/* 按「排除掉不该套文本框样式的类型」来写,而不是逐个枚举 text/number/date——
枚举漏了 datetime-local(后台编辑页有 160 多个),那些框就一直是浏览器默认外观:
没有圆角、没有内边距、13.33px 的字,跟旁边的输入框明显不是一套。 */
/* ⚠️ 还要排除 flatpickr **内部**的控件:`input.numInput`(年 / 时 / 分)与
`select.flatpickr-monthDropdown-months`(月份下拉)。它们是 type=number / select,
会被下面这条扫中 —— width:100% 把月份下拉撑满整行、压在年份输入上面(实测重叠成一团),
内边距与边框还会把时间格变成站点的方块输入框。
⚠️ 别指望「后面补一条 `.flatpickr-calendar input{...}` 去对冲」:这条选择器带 10 个
`:not([type=…])`,特异性是 (0,10,1),普通对冲压不过(实测年份框仍是站点样式)。
要排除就得在**这里**排除。 */
input:not([type=checkbox]):not([type=radio]):not([type=color]):not([type=range]):not([type=file]):not([type=submit]):not([type=button]):not([type=reset]):not([type=image]):not([type=hidden]):not(.numInput),
select:not(.flatpickr-monthDropdown-months), textarea {
width: 100%; padding: 7px 10px; border: 1px solid var(--line); border-radius: var(--r-sm); font: inherit;
font-size: var(--field-fs, inherit);
background: var(--surface); color: var(--text); outline: none;
}
input:not(.numInput):focus, select:not(.flatpickr-monthDropdown-months):focus, textarea:focus {
border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft);
}
.check { display: flex; align-items: center; gap: 7px; font-size: var(--fs-sm); color: var(--text-2); margin-bottom: 8px; word-break: keep-all; overflow-wrap: break-word; }
.check input { width: 15px; height: 15px; accent-color: var(--accent); }
.nobr { white-space: nowrap; }
.btn {
display: inline-flex; align-items: center; gap: 6px; padding: 7px 14px; border-radius: var(--r-sm);
border: 1px solid var(--line); background: var(--surface); color: var(--text); font: inherit; font-size: var(--fs-sm); cursor: pointer;
}
.btn:hover { background: var(--line-soft); text-decoration: none; }
.btn.primary { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }
.btn.primary:hover { filter: brightness(1.06); }
.btn.sm { padding: 4px 10px; font-size: var(--fs-sm); }
.btn.danger { color: #b42318; border-color: #f0cecb; }
.btn.danger:hover { background: var(--err-bg); }
.row { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
/* ---------- 表格 ---------- */
table.tb { width: 100%; border-collapse: collapse; font-size: var(--fs-sm); }
table.tb th {
text-align: left; font-weight: 500; color: var(--text-3); font-size: var(--fs-sm);
padding: 7px 10px; border-bottom: 1px solid var(--line); white-space: nowrap;
}
table.tb td { padding: 9px 10px; border-bottom: 1px solid var(--line-soft); }
/* 操作列(桌面要横着一排按钮,不折行)。原来是写在模板上的 inline style,而 white-space 会
继承进行内编辑表单,堆叠态下「已选:…重选」那一行就顶破了卡片——inline 又压不过后面的覆盖,
所以挪成类,放不下宽表时在文件末尾统一放开。 */
table.tb td.ops { white-space: nowrap; }
table.tb tr:hover td { background: var(--row-hover); }
table.tb td.num, table.tb th.num { text-align: right; font-variant-numeric: tabular-nums; }
.tw { overflow-x: auto; }
/* 表格要给个内容下限:table 是 width:100%,而 CJK 允许逐字断行,没有下限时窄屏会把
每列压到一两个字宽(「一般公共场所」会排成 6 行、一行一个字)。有了下限,放不下就
交给上面的 .tw 横向滚动,列宽不再被榨干。 */
.tw > table.tb { min-width: 640px; }
/* 地点库这张有 8 列(多一列「图片」),640px 下「河池市」这种 3 字城市会被挤成两行
(实测 640 给到 54px、700 给到 59px,正好回到一行)。只给这张加下限,别的表不动 ——
列少的表跟着抬到 700 会平白多出一截横向滚动。 */
.tw > table.tb.places-table { min-width: 700px; }
/* 从地点详情页跳进来定位到的那一行(?open=):淡色底,长列表里一眼能认出目标。
用底色而不是动画——动画会被 prefers-reduced-motion 跳过,那时就完全看不出来了。 */
table.tb tr.located td, table.tb tr.located:hover td { background: var(--accent-soft); }
/* ---------- 地图 ---------- */
#map { height: 100%; width: 100%; background: #eef1f2; }
.map-page {
position: fixed;
inset: calc(var(--nav-h) + var(--safe-t)) var(--safe-r) var(--safe-b) var(--safe-l);
}
.map-panel {
position: absolute; top: 12px; left: 12px; z-index: var(--z-map-overlay); width: 286px; max-height: calc(100% - 24px);
overflow-y: auto; background: var(--glass-strong); backdrop-filter: blur(8px);
border: 1px solid var(--line); border-radius: var(--r-lg); box-shadow: var(--sh-2);
}
/* 内边距挂在表单上,不挂面板:窄屏的折叠条与操作行要 sticky 到面板的**内边缘**,
面板自己带 padding 的话它们只能粘在内容区边界上,边上会留一条缝让滚动内容穿过去
(实测:折叠条粘在内容区顶、上方 13px 仍能看到内容滚过)。 */
#filter-form { padding: 13px; }
/* 时间范围预设行:原来靠行内 style 加的下边距,挪到类上,好让窄屏媒体查询能一起改。 */
.map-panel .presets { margin-bottom: 10px; }
/* 「含家/工作」单独一行(不属于时间段预设)。窄屏预设行是单行横滑的,
它留在里面会被滑到屏幕外;单独一行上下各留 10px,读起来也像另一个开关。 */
.map-panel .hw-row { margin-bottom: 10px; }
.map-panel h3 { font-size: var(--fs-sm); margin-bottom: 9px; color: var(--text-2); }
/* 筛选面板的折叠开关:桌面上面板只占 286px、不碍事,按钮直接不显示,只在窄屏出现 */
.panel-fold { display: none; }
/* 筛选面板里可折叠的两块(地点类型 / 更多筛选):这两块在窄屏各占两百多像素,
是面板高度的大头。默认值在 map.html 的脚本里按视口定(桌面展开、窄屏收起);
收起时标题里显示已选/已生效的那几项,否则收起后就不知道当前选了什么。 */
.type-fold > summary, .adv-fold > summary {
list-style: none; display: flex; align-items: center; gap: 6px;
font-size: var(--fs-sm); color: var(--text-2); cursor: pointer; padding: 2px 0 8px;
}
.type-fold > summary::-webkit-details-marker, .adv-fold > summary::-webkit-details-marker { display: none; }
.type-fold > summary:hover, .adv-fold > summary:hover { color: var(--accent); }
.type-fold > summary .tf-sel, .adv-fold > summary .tf-sel {
flex: 0 1 auto; min-width: 0; color: var(--text-3);
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.type-fold > summary .tf-arrow, .adv-fold > summary .tf-arrow { flex: 0 0 auto; margin-left: auto; font-size: var(--fs-xs); color: var(--text-3); }
.type-fold[open] > summary .tf-arrow, .adv-fold[open] > summary .tf-arrow { transform: rotate(90deg); }
/* 展开后标签行/控件自己就说明了当前值,标题里那份就不必再占一行 */
.type-fold[open] > summary .tf-sel, .adv-fold[open] > summary .tf-sel { display: none; }
/* 「更多筛选」里的 h3 是二级标题,别跟折叠标题撞成一样大 */
.adv-fold > h3 { font-size: var(--fs-sm); color: var(--text-3); margin-bottom: 8px; }
.map-legend {
/* bottom 让开右下角那行底图版权(约 15px 高),否则两者会叠在一起 */
position: absolute; bottom: 26px; right: 12px; z-index: var(--z-map-overlay); background: var(--glass-strong);
border: 1px solid var(--line); border-radius: var(--r-md); padding: 8px 11px; font-size: var(--fs-sm);
max-height: 40%; overflow-y: auto; box-shadow: var(--sh-2);
}
.map-legend .lg { display: flex; align-items: center; gap: 6px; margin-top: 4px; color: var(--text-2); }
.map-legend .lg i { width: 9px; height: 9px; border-radius: 50%; }
/* 迷雾浓度的样式从内联挪出来:窄屏要把它收成一个可展开的小标签——
不然「图层 / 迷雾浓度 / 缩放」三个控件竖着堆在右下角,占掉近 180px。 */
.fog-opacity { background: var(--surface); padding: 6px 8px; line-height: 1.2; }
.fog-opacity .fog-label {
display: block; text-align: left; border: 0; background: none; padding: 0;
font: inherit; font-size: var(--fs-xs); color: var(--text-2); cursor: default;
}
.fog-opacity .fog-slider { display: block; width: 96px; margin: 4px 0 0; accent-color: var(--accent); }
@media (max-width: 620px) {
.fog-opacity .fog-label { cursor: pointer; font-size: var(--fs-sm); }
.fog-opacity .fog-slider { display: none; }
.fog-opacity.open .fog-slider { display: block; }
}
/* 长列表划到底后的「回到顶部」。地图页不滚动,所以它不会出现。 */
.to-top {
position: fixed; right: calc(16px + var(--safe-r)); bottom: calc(20px + var(--safe-b)); z-index: var(--z-map-status);
width: 44px; height: 44px; border-radius: 50%; border: 1px solid var(--line);
background: var(--panel); color: var(--text-2); font-size: var(--glyph-lg); line-height: 1;
cursor: pointer; box-shadow: var(--sh-2);
}
.to-top[hidden] { display: none; }
.to-top:hover { color: var(--text); border-color: var(--text-3); }
/* 时间轴回放时新出现的点:冒出来 + 淡入(配合 JS 里的逐个延时,像"扫过来")。
点在 Leaflet 里是 overlay pane 里的 <path>,用 transform-box: fill-box 让缩放以自身
中心为原点,才不会跑到左上角去。 */
@media (prefers-reduced-motion: no-preference) {
@keyframes pt-in {
from { opacity: .1; transform: scale(.2); }
55% { opacity: 1; }
to { opacity: .9; transform: scale(1); }
}
.leaflet-overlay-pane path.pt-new {
animation: pt-in .55s cubic-bezier(.22, .9, .28, 1) both;
transform-box: fill-box;
transform-origin: center;
}
/* 聚合圆圈是 DOM 元素、而且 Leaflet 用 transform 给它定位,
所以只能动透明度 —— 再加 scale 会把它从原位挪走。 */
@keyframes pt-in-dom {
from { opacity: .1; }
60% { opacity: 1; }
to { opacity: .92; }
}
.leaflet-marker-pane .marker-cluster.pt-new {
animation: pt-in-dom .5s ease-out both;
}
/* 数字变了轻弹一下:弹内层 div(只放数字),外层带 Leaflet 的定位 transform 不能动 */
@keyframes pt-pulse {
0% { transform: scale(1); }
45% { transform: scale(1.3); }
100% { transform: scale(1); }
}
.leaflet-marker-pane .marker-cluster > div.pt-pulse {
animation: pt-pulse .32s ease-out;
}
}
/* 时间轴回放:地图底部中间的一条。窄屏上抬高一档,给右下角的底图版权让位。
和左侧的日期区间不是一回事——区间是「只看这段时间」,这里是「走到这一刻为止」。 */
.timeline-bar {
position: absolute; left: 50%; transform: translateX(-50%);
bottom: 12px; z-index: var(--z-map-overlay);
display: flex; align-items: center; gap: 10px;
max-width: min(560px, calc(100% - 24px));
padding: 7px 12px; border-radius: var(--r-lg);
background: var(--glass-strong); border: 1px solid var(--line); box-shadow: var(--sh-2);
backdrop-filter: blur(8px);
}
.timeline-bar[hidden] { display: none; }
.timeline-bar input[type=range] { flex: 1 1 auto; min-width: 90px; accent-color: var(--accent); margin: 0; }
.timeline-bar .tl-label { flex: 0 0 auto; font-size: var(--fs-sm); color: var(--text-3); white-space: nowrap; font-variant-numeric: tabular-nums; }
.timeline-bar button {
flex: 0 0 auto; border: 1px solid var(--line); background: var(--surface); color: var(--text);
border-radius: var(--r-sm); cursor: pointer; font: inherit; font-size: var(--fs-sm); line-height: 1;
}
.timeline-bar .tl-play { width: 30px; height: 30px; font-size: var(--fs-sm); padding: 0; }
.timeline-bar .tl-reset { padding: 7px 9px; }
.timeline-bar button:hover { background: var(--line-soft); }
@media (max-width: 620px) {
.timeline-bar { bottom: 26px; gap: 8px; padding: 6px 9px; }
.timeline-bar .tl-label { font-size: var(--fs-xs); }
}
/* 地图上的状态条(加载失败 + 重试)。放底部中间:左上角是筛选面板、右下是图例与版权。 */
.map-status {
position: absolute; left: 50%; transform: translateX(-50%);
bottom: 62px;
z-index: 600; display: flex; align-items: center; gap: 10px;
max-width: calc(100% - 24px); background: rgba(30,40,50,.92); color: #fff;
border-radius: var(--r-md); padding: 8px 12px; font-size: var(--fs-sm); box-shadow: var(--sh-2);
}
.map-status[hidden] { display: none; }
.map-status .btn { border-color: rgba(255,255,255,.35); background: rgba(255,255,255,.12); color: #fff; flex: 0 0 auto; }
.map-status .btn:hover { background: rgba(255,255,255,.22); }
@media (max-width: 620px) {
/* 上限 50%(原 44%):折叠条本身占 44px,不加这 6% 的话展开后能看到的筛选内容
反而比改动前更少。50% 刚好把那一条的高度补回来。 */
.map-panel { left: 8px; right: 8px; width: auto; max-height: 50%; top: 8px; }
.map-legend { display: none; }
/* 窄屏上面板铺满整行、最多能吃掉 50% 的屏幕,所以默认收起成一条:
只剩「筛选 + 数量 + 箭头」约 52px 高,想改条件再点开。
「筛选」两字与箭头之间的数量在展开时也保留——表单里那行重复的计数已去掉,
两块状态共用这一个数字,展开/收起都能看到当前筛出多少个点。
展开时它 sticky 在面板顶部:表单比可视窗高,滚到下面之后还得能点回来收起。
不透明底是因为滚动内容会从它下面穿过(面板本身已无内边距,它天然铺满整行)。 */
.panel-fold {
/* width:100% 不能省:这是 <button>,Chrome 下 display:flex + width:auto 仍是「收缩到内容宽」,
于是它只盖住左边一小条,右边的内容会从粘性条旁边穿过去。 */
display: flex; align-items: center; gap: 8px; width: 100%; min-height: 44px;
padding: 0 13px; border: 0; background: var(--glass-strong); color: var(--text-2);
font: inherit; font-size: var(--fs-sm); font-weight: 500; text-align: left; cursor: pointer;
position: sticky; top: 0; z-index: 2;
}
.map-panel:not(.collapsed) .panel-fold { border-bottom: 1px solid var(--line); }
.panel-fold .pf-n { color: var(--text-3); font-size: var(--fs-sm); font-weight: 400; }
.panel-fold .pf-c { margin-left: auto; padding-left: var(--sp-1); font-size: var(--glyph-xs); color: var(--text-3); }
.panel-fold:hover { color: var(--text); }
.panel-fold:hover .pf-c { color: var(--text); }
/* 「查询 / 重置」粘在面板底部:展开后表单 600+px、可视窗不到 400px,
不粘的话每改一次条件都要先滚到底才点得到查询。它必须是表单最后一个元素,
否则粘不住(实测后面还挂着内容时,底部会跟着内容一起往上跑)。 */
.filter-actions {
position: sticky; bottom: 0; z-index: 1;
margin: 0 -13px -13px; padding: 9px 13px;
background: var(--glass-strong); border-top: 1px solid var(--line);
}
.map-panel.collapsed { max-height: none; padding: 4px 0; }
.map-panel.collapsed #filter-form { display: none; }
/* 预设区间在窄屏会折成两三行(7 个预设 +「含家/工作」),单行横滑省下约 30~60px。 */
.map-panel .presets { flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none; }
.map-panel .presets::-webkit-scrollbar { display: none; }
.map-panel .presets > .chip { flex: 0 0 auto; }
.map-panel .map-count { display: none; }
}
/* Leaflet 把 .leaflet-container 的字号写成 0.75rem(见 vendor/leaflet/leaflet.css 的
「general typography」),于是它跟着**根字号**漂:根从 14px 降到 13px 后,
右下角的署名与比例尺缩到了 9.8px(掉到本站 11px 的下限之下),弹窗正文也跟着缩。
这里按自己的字阶钉死,分两档:容器 = 正文(弹窗正文是 1.08333em,跟着它走),
署名与比例尺 = 脚注。 */
.leaflet-container { font-size: var(--fs-sm); }
.leaflet-container .leaflet-control-attribution,
.leaflet-container .leaflet-control-scale-line { font-size: var(--fs-xs); }
.map-preview { height: 320px; border-radius: var(--r-md); overflow: hidden; border: 1px solid var(--line); }
/* ---------- 地点图片 ---------- */
/* 管理面板里的缩略图墙 */
.imgs-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 8px; }
.imgs-cell { position: relative; margin: 0; border-radius: var(--r-md); overflow: hidden; border: 1px solid var(--line); background: var(--line-soft); aspect-ratio: 4 / 3; }
.imgs-cell img { width: 100%; height: 100%; object-fit: cover; display: block; cursor: zoom-in; }
.imgs-del {
position: absolute; top: 5px; right: 5px; width: 22px; height: 22px; border-radius: 50%;
border: 0; background: rgba(0,0,0,.55); color: #fff; font-size: var(--glyph-sm); line-height: 1;
cursor: pointer; display: flex; align-items: center; justify-content: center;
}
.imgs-del:hover { background: #b42318; }
.imgs-busy { opacity: .45; pointer-events: none; }
.imgs-hint { font-size: var(--fs-sm); color: var(--text-3); margin-top: 8px; min-height: 18px; }
.imgs-hint.ok { color: #17604f; }
.imgs-hint.err { color: #b42318; }
/* 存储没配好时把「选择图片」变成不可点的样子,而不是留个能点但必失败的按钮 */
.btn.is-disabled { opacity: .5; cursor: not-allowed; }
/* 展示用:全屏画廊。深色底是为了不干扰照片本身,白底会把照片衬灰。
一次只看一张 —— 以前把图竖着堆起来,手机上既不能左右滑也不能放大。 */
.imgs-modal { position: fixed; inset: 0; z-index: 2000; background: rgba(12,14,16,.94); overflow: hidden; }
.imgs-modal[hidden] { display: none; }
.imgs-stage {
position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
overflow: auto; padding: calc(56px + var(--safe-t)) calc(14px + var(--safe-r)) calc(24px + var(--safe-b)) calc(14px + var(--safe-l));
/* 横滑留给我们做切换,纵向留给浏览器;pinch-zoom 让双指放大仍可用系统手势 */
touch-action: pan-y pinch-zoom;
}
.imgs-stage img { display: block; max-width: 100%; max-height: 100%; width: auto; height: auto; border-radius: var(--r-md); }
/* 双击放大:图放大到两倍、容器可拖动看局部(用尺寸而不是 transform,放大后不糊) */
.imgs-stage.zoomed { touch-action: pan-x pan-y; align-items: flex-start; justify-content: flex-start; }
.imgs-stage.zoomed img { max-width: none; max-height: none; width: 200%; border-radius: 0; }
.imgs-close {
position: fixed; top: calc(12px + var(--safe-t)); right: calc(14px + var(--safe-r)); z-index: 2001;
width: 44px; height: 44px; border-radius: 50%;
border: 1px solid rgba(255,255,255,.3); background: rgba(0,0,0,.4); color: #fff; font-size: var(--glyph-lg);
line-height: 1; cursor: pointer;
}
.imgs-close:hover { background: rgba(255,255,255,.18); }
.imgs-count {
position: fixed; top: calc(20px + var(--safe-t)); left: calc(18px + var(--safe-l)); z-index: 2001;
color: rgba(255,255,255,.78); font-size: var(--fs-sm);
}
/* 上一张 / 下一张:做成整条竖带,拇指够得着,不用瞄小按钮 */
.imgs-nav {
position: fixed; top: 50%; transform: translateY(-50%); z-index: 2001;
width: 44px; height: 76px; border: 0; border-radius: var(--r-lg); cursor: pointer;
background: rgba(255,255,255,.12); color: #fff; font-size: var(--glyph-xl); line-height: 1;
}
.imgs-nav:hover { background: rgba(255,255,255,.24); }
.imgs-prev { left: calc(6px + var(--safe-l)); }
.imgs-next { right: calc(6px + var(--safe-r)); }
/* ---------- 「停一下」提示页(内容对访客不开放之类) ---------- */
.notice-body {
min-height: 100vh; margin: 0; display: flex; align-items: center; justify-content: center;
padding: var(--sp-5) 18px; background: var(--bg);
}
.notice-card {
max-width: 420px; text-align: center; background: var(--panel); border: 1px solid var(--line);
border-radius: var(--r-lg); padding: 30px 26px; box-shadow: var(--sh-2);
}
.notice-card .notice-ico { font-size: var(--glyph-2xl); line-height: 1; margin-bottom: var(--sp-3); }
.notice-card h1 { font-size: var(--fs-lg); margin: 0 0 var(--sp-2); }
.notice-card p { color: var(--text-2); font-size: var(--fs-sm); margin: 0 0 18px; }
.notice-card .notice-tip { color: var(--text-3); font-size: var(--fs-sm); margin-top: 12px; }
/* 后台设置页的「标签 / 值」信息行(地图底图、图片存储状态)。
这个 class 一直没写样式,之前是两段光秃秃的 inline 文本;长一点的 URL
(比如对象存储的 endpoint)会把整页撑宽 —— 所以要 minmax(0,1fr) + 允许断词。 */
.kv { display: grid; grid-template-columns: 88px minmax(0, 1fr); gap: 4px 12px; font-size: var(--fs-sm); }
.kv .k { color: var(--text-3); }
.kv .v { color: var(--text); overflow-wrap: anywhere; }
@media (max-width: 620px) {
.kv { grid-template-columns: 1fr; gap: 2px; }
.kv .k { font-size: var(--fs-sm); }
}
/* URL 这类超长 token 不该把容器顶宽 */
.mono { overflow-wrap: anywhere; }
/* ---------- 暗色模式 ----------
跟随系统(prefers-color-scheme)。配色本来就是变量驱动的,所以这里主要换一套变量,
再覆盖几处硬编码的白色面。地图瓦片保持浅色——上游只有这一套免密钥瓦片,
所以地图页只暗掉面板与控件,瓦片不动。
色值是算过的:主文字 13.6:1、次要 8.0:1、弱化 5.4:1(都在 AA 之上)。 */
@media (prefers-color-scheme: dark) {
/* 用 html:root 而不是 :root:模板会在 CSS 之后注入一条 :root{--accent:…}(站长自定义主色),
同特异性下后者胜出,所以这里必须高一级才盖得住。 */
html:root {
/* 站长选的主色原值在深底上往往太暗(深绿只有 3.3:1),按比例掺白提亮 */
--accent: color-mix(in srgb, var(--accent-base, #2f7d6e) 58%, #ffffff);
--accent-soft: color-mix(in srgb, var(--accent-base, #2f7d6e) 24%, #1c2024);
--bg: #14171a;
--panel: #1c2024;
--line: #2b3137;
--line-soft: #23272c;
--text: #e8eaed;
--text-2: #b0b6bd;
--text-3: #99a0a8;
--surface: #22262b;
--glass: rgba(28, 32, 36, .92);
--glass-strong: rgba(28, 32, 36, .96);
--row-hover: #212529;
--bg-2: #191d21;
/* 浅绿底上用白字只有 2.4:1,反过来用深字才够 */
--on-accent: #0f1b18;
/* 类型色只有 indigo 在深底上不够:#5856d6 对 #1c2024 只有 2.90:1,其余都在 3.9 以上。
掺 10% 白提亮到 3.55:1。别的色一概不动 —— 它们本来就是浅亮色,深底上只会更清楚,
改了反而与手机端 / 地图上的颜色对不上。 */
--cat-indigo: #6967da;
--ok-bg: #16302a; --ok-fg: #7fd3bb; --ok-line: #245046;
--err-bg: #33201e; --err-fg: #f0a49b; --err-line: #5a3330;
--info-bg: #1b2534; --info-fg: #9dbdea; --info-line: #2f4055;
--sh-1: 0 1px 2px rgba(0, 0, 0, .45), 0 1px 3px rgba(0, 0, 0, .35);
--sh-2: 0 6px 18px rgba(0, 0, 0, .50);
--sh-3: 0 12px 34px rgba(0, 0, 0, .55);
}
/* 兜底:还有两处没走变量的白底 */
.flatpickr-calendar { background: var(--panel); border-color: var(--line); box-shadow: var(--sh-2); }
.flatpickr-day { color: var(--text); }
.flatpickr-day.prevMonthDay, .flatpickr-day.nextMonthDay { color: var(--text-3); }
.flatpickr-day:hover { background: var(--line-soft); color: var(--text); }
.flatpickr-months .flatpickr-month, .flatpickr-weekdays, .flatpickr-weekday { color: var(--text-2); fill: var(--text-2); background: transparent; }
.flatpickr-current-month .flatpickr-monthDropdown-months { background: var(--panel); color: var(--text); }
.flatpickr-time input, .flatpickr-time .flatpickr-am-pm { color: var(--text); }
/* Leaflet 自带的控件是白底,暗色下要跟着换,否则浅字压浅底看不清。
注意 leaflet.css 里角标的背景写的是 `.leaflet-container .leaflet-control-attribution`
(两级),所以这里必须带上 .leaflet-container 前缀,否则盖不住。 */
.leaflet-container .leaflet-bar a,
.leaflet-container .leaflet-bar a:hover,
.leaflet-container .leaflet-control-layers,
.leaflet-container .leaflet-control-layers-expanded,
.leaflet-container .leaflet-control-attribution,
.fog-opacity {
background: var(--surface); color: var(--text);
}
.leaflet-container .leaflet-bar a { border-bottom-color: var(--line); }
.leaflet-container .leaflet-bar a:hover { background: var(--line-soft); }
.leaflet-container .leaflet-control-attribution,
.leaflet-container .leaflet-control-attribution a { color: var(--text-2); }
.leaflet-control-scale-line {
background: rgba(28, 32, 36, .7); border-color: var(--text-3); color: var(--text-2);
}
.leaflet-container { background: #101315; }
/* 地图上的浅色瓦片在暗色 UI 里反差过大,整体压一点点亮度 */
#map .leaflet-tile-pane { filter: brightness(.86) saturate(.9); }
}
/* ---------- 键盘焦点 ----------
全站原来没有任何 :focus-visible 规则:用 Tab 走查时看不出焦点落在哪,
键盘用户(以及用读屏的)基本没法用。只在键盘操作时出现,鼠标点击不会看到这个圈。 */
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.btn:focus-visible, .chip:focus-visible, .vote-btn:focus-visible,
.nav-links a:focus-visible, .pager a:focus-visible, .imgs-nav:focus-visible, .to-top:focus-visible {
outline: 2px solid var(--accent); outline-offset: 2px;
}
/* 输入框本来就有 focus 描边,这里只补键盘态的对比度 */
input:focus-visible, select:focus-visible, textarea:focus-visible, summary:focus-visible {
outline: 2px solid var(--accent); outline-offset: 1px;
}
/* ---------- 顶部加载进度条 ---------- */
.load-bar {
position: fixed; top: 0; left: 0; height: 2px; width: 0; z-index: 3000;
background: var(--accent); opacity: 0;
transition: width .18s ease-out, opacity .26s ease;
}
.load-bar[hidden] { display: none; }
/* ---------- 「已选条件」行 ---------- */
.active-filters { margin-bottom: 12px; align-items: center; }
.active-filters .af-label { font-size: var(--fs-sm); color: var(--text-3); }
.active-filters .chip { user-select: none; }
.active-filters .af-x { margin-left: 4px; opacity: .6; font-size: var(--fs-sm); }
.active-filters .chip:hover .af-x { opacity: 1; }
.active-filters .af-clear { background: transparent; color: var(--text-3); border-color: var(--line); }
/* ---------- 其他 ---------- */
.alert { padding: 9px 13px; border-radius: var(--r-md); font-size: var(--fs-sm); margin-bottom: 14px; }
.alert.err { background: var(--err-bg); color: var(--err-fg); border: 1px solid var(--err-line); }
.alert.ok { background: var(--ok-bg); color: var(--ok-fg); border: 1px solid var(--ok-line); }
.alert.info { background: var(--info-bg); color: var(--info-fg); border: 1px solid var(--info-line); }
.muted { color: var(--text-3); }
.mono { font-family: var(--mono); font-size: var(--fs-sm); }
.empty { text-align: center; padding: 40px 16px; color: var(--text-3); }
.empty .big { font-size: var(--glyph-xl); margin-bottom: var(--sp-2); }
.pager { display: flex; gap: 6px; align-items: center; justify-content: center; margin-top: 18px; font-size: var(--fs-sm); }
.pager a, .pager span { padding: 4px 10px; border-radius: var(--r-sm); border: 1px solid var(--line); color: var(--text-2); }
.pager a:hover { background: var(--line-soft); text-decoration: none; }
.pager .cur { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }
footer.ft { max-width: 1120px; margin: 0 auto; padding: 20px; color: var(--text-3); font-size: var(--fs-sm); text-align: center; }
.timeline-day { margin-bottom: 20px; }
.timeline-day .dh {
position: sticky; top: var(--nav-h); background: var(--bg); z-index: 5; padding: 6px 0 7px;
font-size: var(--fs-sm); color: var(--text-2); font-weight: 500; display: flex; gap: 8px; align-items: baseline;
}
.timeline-day .dh .muted { font-size: var(--fs-sm); }
.tl-item { display: grid; grid-template-columns: 62px 20px 1fr; gap: 10px; align-items: start; padding: 7px 0; }
.tl-item .tm { color: var(--text-3); font-size: var(--fs-sm); font-variant-numeric: tabular-nums; padding-top: 1px; }
.tl-item .ln { display: flex; justify-content: center; position: relative; }
.tl-item .ln::before { content: ""; position: absolute; top: 12px; bottom: -12px; width: 1px; background: var(--line); }
.tl-item:last-child .ln::before { display: none; }
.tl-item .ln i { width: 9px; height: 9px; border-radius: 50%; background: var(--accent); margin-top: 5px; position: relative; z-index: 1; }
.tl-item .bd { font-size: var(--fs-sm); }
.tl-item .bd .meta { color: var(--text-3); font-size: var(--fs-sm); }
/* 聚合气泡改用主题色,默认的黄蓝配色和站点风格不一致 */
.marker-cluster-small,
.marker-cluster-medium,
.marker-cluster-large { background-color: color-mix(in srgb, var(--accent) 22%, transparent); }
.marker-cluster-small div,
.marker-cluster-medium div,
.marker-cluster-large div { background-color: var(--accent); color: var(--on-accent); font-weight: 500; }
/* 筛选面板占据左上角,把缩放控件挪到右上与图层切换并列 */
.map-panel { left: 12px; }
@media (max-width: 620px) { .map-panel { left: 8px; right: 8px; } }
/* 更多筛选类型 */
.more-types { margin: 2px 0 10px; }
.more-types summary { font-size: var(--fs-sm); color: var(--text-3); cursor: pointer; padding: 3px 0; }
.more-types summary:hover { color: var(--accent); }
.more-types .chips { margin-top: 8px; }
@media (max-width: 620px) {
.leaflet-bottom.leaflet-right .leaflet-control-zoom { margin-bottom: 34px; }
}
/* 推荐 / 踩雷 表态按钮 */
.vote-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-top: 8px; }
.vote-label { color: var(--text-3); font-size: var(--fs-sm); min-width: 62px; }
/* 「推荐 / 踩雷」必须并排:把它们包成不可换行的组,窄屏时只让后面那句提示换行 */
.vote-btns { display: inline-flex; gap: 8px; flex-wrap: nowrap; }
.vote-btn {
display: inline-flex; align-items: center; gap: 5px;
padding: 6px 14px; border-radius: 999px; white-space: nowrap; flex: 0 0 auto;
border: 1px solid var(--line); background: var(--surface); color: var(--text);
font-size: var(--fs-sm); font-weight: 500; cursor: pointer; line-height: 1;
transition: background .12s, border-color .12s, color .12s;
}
.vote-btn:hover { border-color: var(--accent); color: var(--accent); }
.vote-btn.up.on { background: #e7f6ec; border-color: #34c759; color: #1f8f43; }
.vote-btn.down.on { background: #fdeceb; border-color: #ff3b30; color: #c62a20; }
@media (prefers-color-scheme: dark) {
/* 浅底深字在深色页里太扎眼,换成深底亮字(对比度都在 AA 之上) */
.vote-btn.up.on { background: #16302a; border-color: #34c759; color: #7fd3bb; }
.vote-btn.down.on { background: #33201e; border-color: #ff3b30; color: #f0a49b; }
.vote-tip.up { color: #7fd3bb; }
.vote-tip.down { color: #f0a49b; }
/* 深红字在深底上只有 2.3:1,换成亮红 */
.btn.danger { color: var(--err-fg); border-color: var(--err-line); }
}
.vote-btn:disabled { opacity: .55; cursor: default; }
.vote-tip { font-size: var(--fs-sm); color: var(--text-3); }
.vote-tip.up { color: #1f8f43; }
.vote-tip.down { color: #c62a20; }
/* 站长页面上访客票数只读展示:沿用「我的结论」同一套行结构,尺寸/行距完全一致,只弱化配色 */
.vote-btn.ro {
cursor: default; color: var(--text-2);
border-style: dashed; border-color: var(--line); background: transparent;
}
.vote-btn.ro:hover { border-color: var(--line); color: var(--text-2); }
/* 列表 / 气泡里的推荐踩雷小标签 */
/* 坐标疑似没做过 GCJ-02 偏移的提醒标 */
.tag.warn { background: #fdf3e3; color: #a15c00; }
.tag.vote-up { background: #e7f6ec; color: #1f8f43; }
.tag.vote-down { background: #fdeceb; color: #c62a20; }
/* 地图气泡:地点信息 + 只读备注 + 就地表决 */
.mp { min-width: 240px; max-width: 300px; }
.mp-n { font-weight: 600; margin-bottom: 2px; }
.mp-m { color: var(--text-3); font-size: var(--fs-sm); line-height: 1.6; }
.mp-note {
margin-top: 6px; padding: 6px 8px; border-radius: var(--r-sm);
background: var(--bg, #f5f6f8); color: var(--text);
font-size: var(--fs-sm); line-height: 1.55; white-space: pre-line;
max-height: 150px; overflow-y: auto;
}
.mp-foot { margin-top: 7px; font-size: var(--fs-sm); }
/* nowrap:气泡本来就窄,「👎 踩雷」被挤到第二行会很难看 */
.mp-vote { display: flex; align-items: center; gap: 6px; margin-top: 8px; flex-wrap: nowrap; }
.mp-vl { flex: 0 0 auto; color: var(--text-3); font-size: var(--fs-sm); white-space: nowrap; }
.mp-vb {
display: inline-flex; align-items: center; gap: 4px;
padding: 4px 10px; border-radius: 999px;
border: 1px solid var(--line); background: var(--surface); color: var(--text);
font-size: var(--fs-sm); font-weight: 500; line-height: 1; cursor: pointer;
flex: 0 0 auto; white-space: nowrap;
transition: background .12s, border-color .12s, color .12s;
}
.mp-vb b { font-weight: 600; }
.mp-vb:hover { border-color: var(--accent); color: var(--accent); }
.mp-vb.up.on { background: #e7f6ec; border-color: #34c759; color: #1f8f43; }
.mp-vb.down.on { background: #fdeceb; border-color: #ff3b30; color: #c62a20; }
.mp-vb:disabled { opacity: .55; cursor: default; }
/* 备注块:rond 回填的备注是按次一行的,必须保留换行 */
.note-block {
margin-top: 4px; padding: 7px 9px; border-radius: var(--r-md);
background: var(--bg, #f5f6f8); color: var(--text-2);
font-size: var(--fs-sm); line-height: 1.65; white-space: pre-line;
}
/* 报告导出为 PDF / 长图:隐藏导航、页脚与操作按钮,卡片避免跨页断裂 */
@media print {
.nav, .ft, .no-print { display: none !important; }
body { background: #fff; }
.wrap { max-width: 820px; margin: 0 auto; padding: 0 12px; }
.page-head h1 { margin-top: 8px; }
.card, .kpi, .kpis { break-inside: avoid; }
.grid.g2 { grid-template-columns: 1fr 1fr; }
a { color: inherit; text-decoration: none; }
.bar-fill { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
.chip.on { background: var(--accent-soft); color: var(--accent); }
}
/* 数据编辑页:行内编辑用 details 折叠,summary 复用 .btn 外观 */
.inline-edit { display: inline-block; position: relative; }
.inline-edit summary { list-style: none; display: inline-block; }
.inline-edit summary::-webkit-details-marker { display: none; }
.inline-edit[open] { display: block; }
/* 行内编辑表单的宽度:桌面靠 --efw 撑到够填(各表不同,见模板里的 inline 变量),
放不下宽表时下面会放开这个下限——不然 320px 的表单会把 294px 的卡片顶破。 */
.inline-edit form { background: var(--bg-2, #fafbfb); border: 1px solid var(--line); border-radius: var(--r-lg); padding: 12px; min-width: var(--efw, 280px); }
.inline-edit form .f { margin-bottom: 8px; }
/* 「按名称搜坐标」输入框 + 按钮:必须始终同一行(含移动端),底边对齐。
* 不能直接复用 .row —— label.f 自带 margin-bottom,flex-end 按外边距盒对齐会把它顶高;
* 且 .row 会换行。这里显式 nowrap + 归零外边距,并让输入框可收缩。 */
.search-row { display: flex; flex-wrap: nowrap; align-items: flex-end; gap: 8px; margin-bottom: 12px; }
.search-row > label.f { flex: 1 1 auto; min-width: 0; margin-bottom: 0; }
.search-row .btn { flex: 0 0 auto; font-size: var(--fs-sm); }
.search-row > .act { display: flex; flex: 0 0 auto; gap: 8px; }
/* 按名称搜坐标的候选列表:整行可点,名称与地址分两行 */
.geo-list { display: flex; flex-direction: column; gap: 4px; margin: 0 0 12px; }
/* 到访选择器的候选列表有 max-height(200~240px),但 flex 子项不会因为上限而收缩、
overflow 又是 visible —— 30 条候选会实打实溢出上千像素,被后面的表单元素盖住。
必须给个 overflow 让它在自己的框里滚。 */
.picker-results { overflow-y: auto; }
.geo-list:empty { margin: 0; }
/* 行程列表的路线列:窄屏下每列只剩几十像素,城市名会把行高撑到到访列表的近两倍,
藏掉它(编号 + 地点名够认了,城市在紧邻的到访列表里就有),title 里仍留着全称。 */
@media (max-width: 620px) { .mv-city { display: none; } }
/* 筛选行:和 .row 一样窄屏会换行,但控件按底边对齐。
不能直接用 .row + align-items:flex-end——label.f 自带 margin-bottom,
flex-end 按外边距盒对齐会把按钮顶低几像素(就是那种「按钮有点歪」)。 */
.filter-row { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 8px; }
.filter-row > label.f { margin-bottom: 0; }
.filter-row > .act { display: flex; flex: 0 0 auto; gap: 8px; }
.geo-item {
display: block; width: 100%; text-align: left; cursor: pointer;
padding: 7px 10px; border: 1px solid var(--line); border-radius: var(--r-md);
background: var(--panel); color: var(--text); font: inherit;
}
.geo-item:hover { background: var(--line-soft); }
.geo-name { display: block; font-size: var(--fs-sm); }
.geo-addr { display: block; color: var(--text-3); font-size: var(--fs-sm); margin-top: 2px; }
/* ---------- 专题 ---------- */
/* 必须带上 #map:站点里 #map{height:100%} 是 ID 选择器,
单靠 .topic-map 这个类压不过它,容器会被算成 0 高(地图看不见)。 */
#map.topic-map { height: 520px; width: 100%; border-radius: var(--r-lg); overflow: hidden; }
@media (max-width: 620px) { #map.topic-map { height: 360px; } }
.topic-card { display: block; text-decoration: none; color: inherit; transition: box-shadow .15s, transform .15s; }
.topic-card:hover { box-shadow: var(--sh-2); transform: translateY(-1px); }
.topic-head { display: flex; align-items: center; gap: 8px; }
.topic-head h2 { margin: 0; }
/* 地图兜底提示:脚本没就绪时留在容器里,解释为什么没有地图 */
.map-fallback {
display: flex; align-items: center; justify-content: center; height: 100%;
padding: 20px; text-align: center; color: var(--text-3); font-size: var(--fs-sm);
}
/* 专题:时间范围独立成行(精确到分钟,塞进 KPI 网格会被挤成多行) */
.topic-range { margin-top: 5px; color: var(--text-2); font-size: var(--fs-sm); font-variant-numeric: tabular-nums; }
.topic-card .topic-range { margin: 7px 0 0; }
.topic-desc { color: var(--text-2); white-space: pre-wrap; line-height: 1.65; }
.kpis.kpis-3 .kpi { flex: 1 1 30%; }
@media (max-width: 620px) {
/* 三个数字都很短,窄屏保持三列比 2+1 更好看 */
.topic-card .kpis.kpis-3 .kpi { flex: 1 1 30%; }
.topic-card .kpi { padding: 11px 12px; }
.topic-range { font-size: var(--fs-sm); }
}
/* ---------- 表单里的「文字 + 内联按钮」组合 ---------- */
/* label 的文字行右侧放次要操作(活动改名/删除),不额外占高度,也就不会把整格撑高 */
label.f > span.span-actions { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.mini-actions { display: flex; gap: 8px; font-size: var(--fs-sm); }
.mini-actions a { color: var(--text-3); }
.mini-actions a:hover { color: var(--accent); }
.mini-actions a.danger:hover { color: #b42318; }
/* 输入框 + 按钮同一行:input 自适应、按钮不压缩(不设 min-width:0 的话 input 会被按钮顶到下一行) */
.input-btn { display: flex; gap: 6px; }
.input-btn > input { flex: 1 1 auto; min-width: 0; }
.input-btn > .btn { flex: 0 0 auto; padding-left: 12px; padding-right: 12px; }
/* 到访列表里的收藏星标:点一下就切换,不用展开行内编辑 */
.star {
background: none; border: 0; padding: 0 3px; cursor: pointer; line-height: 1;
font-size: var(--glyph-sm); color: var(--text-3); vertical-align: -1px;
}
.star:hover { color: #e8b700; }
.star.on { color: #e8b700; }
/* 单个点位的地图弹窗 */
#point-modal {
position: fixed; inset: 0; z-index: 1200; display: flex; align-items: flex-start;
justify-content: center; padding: 40px 16px; overflow: auto;
background: rgba(0, 0, 0, .45);
}
.point-modal-box {
width: 100%; max-width: 660px; background: var(--panel); border-radius: var(--r-lg);
padding: 14px; box-shadow: var(--sh-3);
}
.point-modal-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 10px; }
.point-modal-map { height: 360px; border-radius: var(--r-lg); overflow: hidden; }
@media (max-width: 620px) {
#point-modal { padding: 16px 10px; }
.point-modal-map { height: 260px; }
}
/* 表格里的行内操作链接不许折行:中文默认能在任意字间断开,
「地图」「编辑」这类两字链接在窄列里会被拆成两行 */
.tb a { white-space: nowrap; }
/* 照片导入表:固定列宽比例,输入框跟着列宽走。
否则每个 input 自带 min-width,8 列加起来必然超出视口,最右边的「说明」列要横向拖动才看得到 */
.photo-table { table-layout: fixed; width: 100%; min-width: 880px; }
.photo-table th, .photo-table td { padding: 7px 8px; vertical-align: top; }
.photo-table input, .photo-table select { width: 100%; min-width: 0; box-sizing: border-box; }
.photo-table input[type=datetime-local] { font-size: var(--fs-sm); padding: 6px 6px; }
.photo-table input[type=checkbox] { width: auto; }
.photo-table .chip { white-space: nowrap; }
/* 窄屏:9 列在手机上放不下,把信息量最低的「文件」列去掉(地点名已经能认出是哪张),
同时把最小宽度调小,需要横向滑动时也能更快看到最右边的操作 */
@media (max-width: 700px) {
.photo-table { min-width: 640px; }
.photo-table col:nth-child(3),
.photo-table thead th:nth-child(3),
.photo-table tbody tr td:nth-child(3) { display: none; }
}
/* 照片导入:确认表上方的总览图。
做成「粘」的是为了让「鼠标划到某一行 → 图上那个点跳出来」始终看得见——
表格几十行,图一滚出屏幕这个联动就白搭了。粘住时必须自带不透明底,
否则下面滚过去的行会透上来。 */
#map-card {
position: sticky; top: calc(var(--nav-h) + 8px); z-index: 5;
background: var(--panel); padding: 2px 0 10px; margin-bottom: 2px;
border-bottom: 1px solid var(--line-soft);
}
.photo-map {
height: 200px; width: 100%; border-radius: var(--r-lg); overflow: hidden;
border: 1px solid var(--line); transition: height .18s ease;
}
/* 粘住以后收矮:200px 的浮层会挡住从下面滚过去的半个屏幕,收到 132px 还能看清
高亮出来的那个点,占的高度也只剩一条。说明文字这时候就没必要留着了。 */
#map-card.stuck .photo-map { height: 132px; }
#map-card.stuck .card-sub { display: none; }
#map-card.stuck { box-shadow: var(--sh-1); }
@media (max-width: 700px) {
.photo-map { height: 170px; }
#map-card.stuck .photo-map { height: 110px; }
}
@media (prefers-reduced-motion: reduce) {
.photo-map { transition: none; }
}
/* 照片导入的进度/结果提示:脚本直接往元素里写文本,空的时候自己收起 */
#parse-status { margin: 12px 0 0; }
#parse-status:empty { display: none; }
/* ---------- 专题显示区编辑器(后台专题表单) ----------
地图 + 圆列表。半径/个数的上下限与后端一致,由 topic-areas.js 统一校验。 */
#area-map { height: 380px; border-radius: var(--r-md); background: var(--line-soft); }
.area-tools { display: flex; align-items: flex-end; gap: 10px; flex-wrap: wrap; margin: 10px 0; }
.area-tools #area-hint { flex: 1 1 240px; font-size: var(--fs-sm); }
#area-hint.err { color: var(--err-fg); }
#area-list { margin-top: 4px; }
#area-list:empty { display: none; }
.area-row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
padding: 7px 0; border-top: 1px solid var(--line-soft); }
.area-tag { color: var(--text-3); font-family: var(--mono); font-size: var(--fs-sm); min-width: 2.4em; }
.area-row .mono { flex: 1 1 180px; }
.area-km { display: flex; align-items: center; gap: 6px; margin: 0; }
/* 定宽容器:站点的全局 input 规则是 width:100% 且带 10 个 :not()(特异性 (0,11,1)),
直接给 input 写宽度压不过,用容器定宽就绕开了。 */
.area-km-box { flex: 0 0 96px; display: block; }
/* divIcon 默认没有背景,不补的话圆心和把手既看不见也抓不住 */
.area-dot { background: var(--accent); border: 2px solid #fff; border-radius: 50%; box-shadow: var(--sh-1); }
.area-handle { background: var(--surface); border: 2px solid var(--accent); border-radius: 50%; box-shadow: var(--sh-1); }
/* ---------- 窄屏触控与控件字号(必须放在最后) ----------
这几组选择器(.chip / .btn / .check input)的基础规则写在文件中部,
而 CSS 同特异性下靠后的赢 —— 把它们写在前面那份「窄屏触控」块里是被盖掉的
(实测 .check input 的 20px 一直没生效,量出来还是 15px)。所以统一放这里。
量法与判据见技能 rond-web-verify 的 audit/mobile.js:触控目标 ≥44px、
输入框字号 ≥16px(否则 iOS 聚焦时会自放大整页)。 */
@media (max-width: 620px) {
.chip { padding: 9px 12px; }
.btn, .btn.sm { padding: 10px 14px; }
/* 专题显示区编辑器:地图矮一点;坐标那列太长会挤掉按钮,让它独占一行排到最后 */
#area-map { height: 300px; }
.area-row .mono { flex: 1 1 100%; order: 3; }
/* 勾选框:原来 15px,手指很难点中(尤其后台设置页一屏几十个开关)。
有效目标仍是外面那圈 label,但控件本身大一点更好瞄。 */
.check input, input[type=checkbox], input[type=radio] { width: 20px; height: 20px; }
/* 下拉框 39px 差 5px 到 44px,补齐 */
select { min-height: 44px; }
/* 表格里的控件保持紧凑:那张表列多、行密,抬到 44px 会让整张表膨胀;
.photo-table input[type=checkbox]{width:auto} 是既有约定,别覆盖。 */
table.tb select, .photo-table select { min-height: 0; }
/* file / color 这两种被基础规则排除在外(不套文本框外观),也就没吃到
--field-fs,窄屏仍是 13.3px ⇒ iOS 一聚焦就放大。单独提上来。 */
input[type=file], input[type=color] { font-size: var(--fs-control); }
/* 11.5px 的标签在手机上偏小(/stats 一页 29 处、/ 15 处) */
.tag { font-size: var(--fs-sm); }
/* 导航左上角那个 logo 是回首页的入口,原来只有 25×29:给它撑到 44 高,
横向也补到 44(.brand .t 在窄屏被隐藏,加内边距就够了)。 */
.brand { min-width: 44px; min-height: 44px; justify-content: center; }
/* ---------- 地点库 / 时间线 / 统计三页共用的筛选卡:窄屏默认收成一条 ----------
与地图页那块 .panel-fold 同形——手机上它一上来就占掉大半屏(预设 + 已选 + 类型 + 六个
输入框),内容反而看不见。展开时折叠条用负外边距贴到卡片上沿(圆角照着卡片补,
卡片没有 overflow:hidden,否则折叠条就粘不住了);收起时卡片内边距归零、只留那一条。
top 取 --nav-h:导航是 sticky 的,写 0 会被压在它下面。
这些规则整条在媒体查询里 ⇒ 桌面端 .collapsed 不生效,等于没影响。 */
.filters-card > .panel-fold {
margin: calc(var(--card-pad) * -1) calc(var(--card-pad) * -1) 14px; border-radius: var(--r-lg) var(--r-lg) 0 0; top: var(--nav-h);
}
.filters-card.collapsed { padding: 0; }
.filters-card.collapsed > .panel-fold { margin: 0; border-radius: var(--r-lg); }
.filters-card.collapsed > :not(.panel-fold) { display: none; }
}
/* ---------- 放不下宽表时:一屏一行的「字段 + 值」 ----------
地点库(8 列)、统计、专题、后台这几张表在窄处要横滑 300+px 才看得全,滑着还对不上表头。
放不下时改成每行一块、每格「字段名 + 值」竖排:不用横滑,也不用记「第几列是什么」。
字段名取 td 的 data-label,每行的首格留空当小标题(如地点名),操作格给「操作」。
表格结构与脚本都没动(只是隐藏 thead),链接 / 表单 / 按钮照旧。
阈值 800 是从「卡片能不能放下表格」换算来的:卡片内容宽 ≈ 视口 − 93(页面 + 卡片的内边距),
这几张表的 min-width 最宽是 700 ⇒ 视口 793 以上才放得下,取 800 留几像素余量。
**必须用 @media,别改成 @container**:容器查询能按「卡片自己有多宽」判得更准,可它要
iOS 16 / Chrome 105 起才支持——旧手机整块规则直接失效,表格完全不堆叠、退化成横滑
328~388px(2026-09-19 真机上就是这么炸的,比改之前还糟)。现在宽表卡片已被要求不放 g2
(见 wide_table_layout_test.go),卡片宽度只由视口决定,两种判据在这些页面上等价,
那就用兼容性好的那个。若页面/卡片的内边距变了,这里的 800 要跟着重算。 */
/* ---------- 轻量输入弹层(替代 window.prompt) ----------
*
* 为什么不用 prompt():它会把提示文本与默认值写进地址栏(Chrome 会把
* 「?新的活动名称=吃饭」这类内容拼到 URL 上并留进历史),刷新即丢、
* 分享出去还带着原文。confirm/alert 不污染地址栏,只有 prompt 有这个毛病。
*
* 定位:原生 <dialog> 居中、带 ::backdrop 遮罩、按 Esc 关闭、焦点自动进输入框,
* 这几件事正好是 prompt 给的,手写一遍不划算;旧浏览器不支持时降级成页面顶部一条。
* showModal() 会让页面其余部分 inert,天然挡住「弹层开着还能点底层」。
*/
dialog.prompt-box {
border: 1px solid var(--line);
border-radius: var(--r-lg);
padding: 0;
min-width: 0; width: min(420px, calc(100vw - 32px));
background: var(--surface);
color: var(--text);
box-shadow: var(--sh-2);
}
dialog.prompt-box::backdrop { background: rgba(16, 24, 40, .38); }
.prompt-box .pb-body { padding: 14px 16px 12px; }
.prompt-box .pb-title { font-size: var(--fs-md); font-weight: 500; margin-bottom: 8px; }
/* 输入框字号必须 ≥16px:iOS Safari 聚焦时会自动放大页面,16px 以下会把布局顶歪 */
.prompt-box .pb-input {
width: 100%; font-size: var(--fs-control); font-family: inherit; color: var(--text);
background: var(--surface); border: 1px solid var(--line);
border-radius: var(--r-sm); padding: 7px 10px; min-height: 40px;
}
.prompt-box .pb-input:focus { outline: 2px solid var(--accent); outline-offset: -1px; border-color: var(--accent); }
.prompt-box .pb-acts { display: flex; gap: 8px; justify-content: flex-end; padding: 0 16px 14px; }
/* 触控目标下限 44px(窄屏见文件末尾的覆盖块) */
.prompt-box .pb-acts .btn { min-height: 38px; }
@media (max-width: 700px) {
.prompt-box .pb-acts .btn { min-height: 44px; }
}
/* 不支持 <dialog> 的旧浏览器:降级成页面顶部一条提示,
至少不丢内容、也不会把地址栏弄脏。 */
.prompt-fallback {
position: fixed; inset: 8px 8px auto 8px; z-index: 2000;
display: flex; gap: 8px; align-items: center;
padding: 10px 12px; border: 1px solid var(--line); border-radius: var(--r-lg);
background: var(--surface); box-shadow: var(--sh-2);
}
.prompt-fallback input {
flex: 1; min-width: 0; font-size: var(--fs-control); font-family: inherit;
border: 1px solid var(--line); border-radius: var(--r-sm); padding: 7px 10px; min-height: 40px;
}
@media (max-width: 800px) {
.tw > table.tb.tb-stack { min-width: 0; }
.tw > table.tb.tb-stack thead { display: none; }
.tw > table.tb.tb-stack,
.tw > table.tb.tb-stack > tbody,
.tw > table.tb.tb-stack > tbody > tr,
.tw > table.tb.tb-stack > tbody > tr > td { display: block; width: auto; }
.tw > table.tb.tb-stack > tbody > tr {
padding: 9px 11px; border-bottom: 1px solid var(--line-soft);
}
.tw > table.tb.tb-stack > tbody > tr:last-child { border-bottom: 0; }
/* 值用悬挂缩进:标签绝对定位在左边,值换行时对齐到值那一栏,而不是缩到标签下面 */
.tw > table.tb.tb-stack > tbody > tr > td {
position: relative; padding: 2px 0 2px 5.6em; border-bottom: 0;
}
.tw > table.tb.tb-stack > tbody > tr > td::before {
content: attr(data-label); position: absolute; left: 0; top: 2px;
width: 5em; color: var(--text-3); font-size: var(--fs-sm);
}
/* 首格是这一行的主角(地点名 / 天气 / 方式…):不缩进、字号抬一档当小标题;
操作格也是这一类(表头为空、整行铺开)。它们里面的内容是长名称或编辑表单,
而桌面模板给操作格挂了 nowrap(那一列要横向排按钮)——white-space 会继承到表单里,
于是「已选:#403 2026-09-13 19:53 · 某某地」这一行在窄处不换行、把卡片顶出 44px。
堆叠态每格本来就是一整行,nowrap 没有意义,放开。 */
.tw > table.tb.tb-stack > tbody > tr > td:not([data-label]) {
padding-left: 0; margin-bottom: 2px; font-size: var(--fs-sm); white-space: normal;
}
/* 操作格(有的带「操作」标签、有的表头本来就是空的)在堆叠态整行铺开,
桌面那条 nowrap 会让格里的编辑表单不换行、把卡片顶出几十像素 */
.tw > table.tb.tb-stack > tbody > tr > td.ops { white-space: normal; }
/* 数字列不必再右对齐(现在已经是「标签 + 值」两栏) */
.tw > table.tb.tb-stack td.num { text-align: left; }
/* 行内编辑表单的桌面宽度下限在这里放开:320px 的表单加上悬挂缩进会顶破卡片,
而表单字段本来就是 flex 自适应的(见 .row / label.f),窄处自然铺满即可。 */
.tw > table.tb.tb-stack .inline-edit form { min-width: 0; }
}