/* ==========================================================================
   江城拾光 · UI 升级补丁（Phase 3 落地件）
   --------------------------------------------------------------------------
   接入方式（只有两步，不改任何 HTML 结构）：
     1) 把本文件放到 public/ui-upgrade.css
     2) 在每个页面的 <link rel="stylesheet" href="/style.css"> **之后**再加一行
        <link rel="stylesheet" href="/ui-upgrade.css">
   生效原理：全部是"同名选择器后置覆盖"，不加类、不改 DOM。

   本文件的三条纪律（写新规则前请先读）：
     · 不写任何 font-weight。全站只有得意黑 400，500/600/700 会被浏览器
       合成加粗，字形立刻变糊。强调靠颜色/描边/字号，不靠粗。
     · 不用 filter: brightness() / blur() 做 hover。brightness 会新建合成层，
       且与金色叠加后偏色（已在 .btn.primary:hover 上踩过）。
     · 不新增 backdrop-filter。这个站有过事故：被完全遮挡的全屏图层继续出帧，
       会把渲染进程拖死。已有玻璃层够用，不再加。

   动效 5 档 + 1 个别名（team-lead 裁决，全站统一，不要再出现第 6 个值）：
     --t-micro    80ms  仅 :active 按下反馈（★ 不许用于颜色 —— 80ms 的颜色变化像"闪了一下"）
     --t-fast    160ms  颜色 / 描边 / 图标态 / 焦点环
     --t-base    240ms  列表项进场、卡片悬停、按钮悬停
     --t-slow    420ms  大块进出、图集换页、CSS 侧灯箱遮罩淡入
     --t-reveal  900ms  仅「区块级」滚动进场（与 motion.js 现值一致，不要动 motion.js）
     --t-reveal-list: var(--t-base)   别名，指向实际生效的 240（不是第二个数，见 §0 注释）
   ★ 灯箱的**开合**是 coverflow.js:699 手写的 rAF 补间 `dur = 560`，CSS 变量管不到它；
     CSS 侧只管遮罩淡入，用 --t-slow(420)。两个数天生不同步 —— 改任一处必须两处一起看。
     不为它加第 6 档 token（没有 CSS 消费方的档位是空的），写在文档里即可。

   曲线 3 条，一条曲线一个 job：
     --ease      默认（离场也用它）：cubic-bezier(.22,.8,.24,1)
     --ease-in   离场专用（先慢后快，像让路）
     --ease-pop  回弹（只给按下 / 弹层出现）
   ★ 配套硬规则：列表项 / 卡片网格**禁止**用 --t-reveal。
     一行 18 张卡 × 900ms，用户要站着等它慢慢显完（实测最坏 1335ms，
     而一张 9:16 卡在 1440 视口里从进到出只有 950ms —— 动画还没完就滚出去了）。
     列表项一律 --t-base + 40ms 错峰（--lr-step），封顶 7 档（--act-cap）= 240+280ms。
     --t-reveal 只给「一屏一个」的区块标题容器。

   ★★ 前置条件（最容易漏，漏了整个 token 表就是装饰品）：
     style.css:1159-1168 把 5 个时长写死成 .85/.95/.95/.95/.8s，1188-1193 的
     [data-stagger] 又是 .8/.9s + 85ms。**只写变量、不改这两段，页面动效一个像素都不会变。**
     所以 §1.4 是"静态值 → var()"的替换层，它的优先级高于 §0。
   ========================================================================== */


/* ==========================================================================
   §0 · token
   --------------------------------------------------------------------------
   【解决】phase1 P2-12：14 种圆角 / 24 种字号 / 1 个 token + 11 条一次性投影 /
           31 个间距值 / 19 个过渡时长 / 14 个 z-index，全站没有刻度。
   【风险】**中**。纯新增的 token 是低风险，但其中两条会立刻改变全站观感：
           · --radius-sm: 10px → 12px —— 影响所有 .route-card / .wall-item /
             .tl-item / .gb-reply / .input / .ph-nav a 等引用它的地方（约 20 处）。
             这是刻意的（10 与 12 肉眼不可分，8/12/16 才是递进），但它确实是
             一次全站可见变化。若本轮只想上 P0，把这一行注释掉即可，其余不受影响。
           · --shadow-lg 变成 --shadow-3 的别名 —— 深色下**完全等价**（同一条值），
             浅色下会跟着 §4 的浅色投影变淡（这是修 bug，不是副作用）。
           想零风险上线？只保留本节里 --sp-* / --t-* / --radius-xs / --radius-pill /
           --shadow-1 / --shadow-2 / --focus-ring，把 --radius-sm 与 --shadow-lg
           两行注释掉。
   【验证】改前改后各截一张 05-wall.jpg 同位置：卡片圆角从"10"变"12"，
           肉眼几乎看不出差别；但把两张叠在一起能看到外框各外扩 2px。
   ========================================================================== */
:root {
  /* ---- 间距：4px 刻度，替代现有 31 个手感值 ---- */
  --sp-1: 4px;  --sp-2: 8px;  --sp-3: 12px; --sp-4: 16px;
  --sp-5: 24px; --sp-6: 32px; --sp-7: 48px; --sp-8: 64px;

  /* ---- 圆角：4 档（小件 / 控件 / 容器 / 胶囊）。超过 16 就不是圆角，是形状 ---- */
  --radius-xs: 8px;       /* 新增：缩略图 / 小徽标 / 输入框（原来散落 5/6/8px） */
  --radius-sm: 12px;      /* 原 10px → 12px（**中风险**，见本节头注释） */
  --radius: 16px;         /* 容器，不动 */
  --radius-pill: 999px;   /* 新增：把 17 处 999px 收进一个名字 */

  /* ---- 投影：3 级（原来是 1 个 token + 11 条一次性值）---- */
  --shadow-1: 0 2px 10px -4px rgba(0, 0, 0, .45);    /* 贴地：chip / 输入框 / 行内卡 */
  --shadow-2: 0 14px 32px -14px rgba(0, 0, 0, .80);  /* 悬停上浮：卡片 / 主按钮 */
  --shadow-3: 0 30px 70px -30px rgba(0, 0, 0, .75);  /* 浮层：面板 / 地图壳 / 播放器 */
  --shadow-lg: var(--shadow-3);                      /* 兼容旧名，不删（全站 6 处引用它） */

  /* ---- 位移：只留一档，且只给卡片（按钮不做位移，「能点开」的信号不该有两个）---- */
  --lift: -2px;

  /* ---- 动效：5 档时长（team-lead 裁决值，见文件头）+ 3 条曲线 ---- */
  --t-micro: 80ms;    /* 仅 :active。★ 不许用于颜色/描边 */
  --t-fast: 160ms;    /* 颜色 / 描边 / 图标态 */
  --t-base: 240ms;    /* 列表项进场 / 卡片悬停 / 按钮悬停 */
  /* ★ 灯箱时长在 JS 里：coverflow.js:699 `var t0 = performance.now(), dur = 560;`
     是手写 rAF 补间，CSS 变量对它**无效**。CSS 侧只有遮罩淡入，用 --t-slow(420)。
     所以同一个灯箱在 JS 和 CSS 里是两个数（560 / 420），改任一处必须两处一起看。
     这里**不**为它加第 6 档 token —— 没有 CSS 消费方的档位是空的，只写在文档里。 */
  --t-slow: 420ms;    /* 大块进出 / 图集换页 / CSS 侧灯箱遮罩淡入 */
  --t-reveal: 900ms;  /* 仅区块级滚动进场，与 motion.js 一致 */

  /* ★ 别名指向**实际生效**的 240，不写第二个 420。
     理由：§1.4 里 [data-reveal="zoom"] 读的是 --t-base；如果这个别名指向 --t-slow(420)，
     它会变成一条没有任何引用的死代码 —— 下一个人看到"有个 420 的 reveal-list 没人用"，
     第一反应就是把它接上，然后列表项从 240 静默回归 420。别留这个陷阱。 */
  --t-reveal-list: var(--t-base);

  --ease-out: var(--ease);                       /* = 现有 --ease，改名不改值 */
  --ease-in: cubic-bezier(.4, 0, 1, 1);          /* 离场：先慢后快，像让路 */
  --ease-pop: cubic-bezier(.34, 1.28, .64, 1);   /* 回弹：只给按下 / 弹层出现 */
  /* ↳ 想要"零观感差异"的话，把上面两行都改成 var(--ease) 即可 —— 代价只是 toast 与
     chip 失去 1.28 的轻微回弹。默认给三条不同曲线，因为 team-lead 终稿要求各不相同。 */

  /* ---- 交互刻度（phase2 新增，替代各处手写的魔法值）---- */
  --act-cap: 7;          /* 列表项错峰档数上限：第 8 张起不再往后推 */
  --lr-step: 40ms;       /* 列表项（list reveal）逐项错峰步长。40×7=280ms 是上限 */
  --inline-max: 34em;    /* 行内可读宽度上限：正文 / 空态说明 / 区块副标题 */
  --row-min: 44px;       /* 单行可点元素的最小高度（触控命中区下限） */
  --field-min: 38px;     /* 表单控件最小高度；粗指针（触屏）下提到 var(--row-min) */
  --focus-ring: var(--accent);  /* 焦点环颜色。浅色主题下由 §4 的 --accent 自动跟上 */

  /* ---- 状态色：与四季色分离（现在「已通过」借用了 --summer=夏天绿，
          同一屏里绿色既表示夏天又表示审核通过）---- */
  --ok: #4fb6a3;
  --warn: #e0a04c;
}

/* 浮层层级也收成命名（8 档）。只新增名字，现有 z-index 数值不动 ——
   逐个替换有踩空的风险，等下一次改到那个组件时再换。 */
:root {
  --z-map: 500;    --z-panel: 600;   --z-nav: 1200;  --z-progress: 1300;
  --z-overlay: 3000; --z-player: 3200; --z-toast: 4000; --z-guard: 5200;
}

/* 浅色主题的投影必须单独降一档：黑底上那份 .75 / .80 的透明度，
   在纸白底上会变成一块脏灰。只覆盖三条，不动其余 token。 */
html[data-theme="light"] {
  --shadow-1: 0 1px 3px rgba(30, 26, 20, .07);
  --shadow-2: 0 12px 26px -14px rgba(30, 26, 20, .20);
  --shadow-3: 0 26px 56px -28px rgba(30, 26, 20, .24);
}


/* ==========================================================================
   §1 · 三处 P0 修复（纯 CSS 能修的部分）
   ========================================================================== */

/* --------------------------------------------------------------------------
   §1.1 一日路线卡片：标题与元信息挤在同一行 → 一行标题 + 一行元信息
   【对应】style.css:439-441（.rc-body / .rc-name / .rc-meta）→ 04-routes.jpg
   【原因】.rc-name 与 .rc-meta 是两个 <span>，没有任何 display 声明，
           于是同一行里连续流动，换行位置由剩余宽度决定 —— 不是折行，是文字随机断裂。
           margin-top: 3px 对 inline 元素无效（行内元素不吃垂直 margin），
           所以两者之间实际是 0 间距。
   【风险】低。只影响 .route-list 内的三行文字。
   【验证】04-routes.jpg 同位置重截：12 张卡应全部变成「沙湖公园 → 东湖」一行、
           「5 个机位 · 直线 10.3 km · 69 张 · 东湖」一行；不再出现孤零零的「· 东湖」。
   -------------------------------------------------------------------------- */
.rc-body {
  min-width: 0;
  flex: 1 1 auto;
  display: grid;
  gap: var(--sp-1);          /* 3px → 4px（4px 刻度；inline 上无效的 margin 换成 grid gap） */
}
.rc-name {
  /* ★ display 从 inline 改成块级 —— 这一条是 P0 的全部（原来它和 .rc-meta 挤一行乱断）。
     但**不要**配 white-space: nowrap：右边 4 个缩略图占掉约 160px，一行只剩 80px，
     路线名会被截成「王家墩公园 → 宝岛…」—— 而路线名本身就是这张卡的内容。
     允许两行，两行还放不下才省略。 */
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  font-size: 15px;
  color: var(--text);
  line-height: 1.35;
  overflow: hidden; white-space: normal;
}
.rc-meta {
  display: block;            /* ← 同上 */
  font-size: 12px;
  color: var(--muted);
  line-height: 1.4;
  font-variant-numeric: tabular-nums;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* --------------------------------------------------------------------------
   §1.2 照片页：9:16 竖图按 100% 宽度铺开（单张高约 1880px，标题在 2 屏之外）
   【对应】photo.html:33-37（页面自带 <style> 里的 .ph-figure img）→ 10-photo.jpg
   【做法】竖图改成「整张可见 / 居中 / 两侧留暗」，与灯箱 object-fit: contain 一致。
   【风险】低。横图不受影响（max-width 先命中）。
   ★ 特异性说明（必须这样写）：photo.html 的样式写在页面自带的 <style> 里，
     而那个 <style> 排在 <link> 之后 —— 光靠"本文件加载更晚"是压不过它的。
     所以这里统一用 `main .ph-figure img`（0,1,2）比它原生的 `.ph-figure img`（0,1,1）
     高一级，**与 <link> 插在哪里无关**。整份补丁里只有 photo 页需要这招。
   ★ 放大态必须不受影响：img.zoomed 是"看原尺寸"，高度上限必须解除，
     否则点开只会得到一张被压扁的图。多写一条 .zoomed 覆盖。
   【验证】打开任意一张 9:16 的照片页（例如 /photo/<id>）：
           改前 —— 整屏只有照片上半截；
           改后 —— 整张照片可见，下方 24px 处出现标题/作者/参数/两个按钮。
           再点一下照片：应放大到原像素尺寸并可横向滚动（高度不再被截）。
   -------------------------------------------------------------------------- */
main .ph-figure img {
  display: block;
  width: auto;
  max-width: 100%;
  max-height: calc(100svh - 200px);   /* = --nav-h(66) + 134；改 --nav-h 时记得同步 */
  margin: 0 auto;
  border-radius: var(--radius);
  border: 1px solid var(--line);
  background: var(--elev);
  cursor: zoom-in;
}
main .ph-figure img.zoomed {
  /* 放大态 = 查看原尺寸，必须能突破上面的高度上限 */
  max-height: none;
  max-width: none;
  width: auto;
}

/* --------------------------------------------------------------------------
   §1.3 灯箱提示行：2.54:1 → 6.24:1
   【对应】style.css:1043（.lb-tip）→ 08-lightbox.jpg 底部右侧那行几乎看不见的灰字
   【原因】rgba(255,255,255,.3) 叠在 rgba(6,7,9,.965) 上 = 2.54:1，11px 文字，
           AA 要求 4.5:1。而它承载的是灯箱**全部**的操作方式（轻点放大 / 拖动 / ← →）。
   【风险】低。只改一个颜色。
   【验证】08-lightbox.jpg 同位置重截，底部那行「轻点放大 · 拖动 · ← →」应能直接读出来。
   -------------------------------------------------------------------------- */
.lb-tip {
  color: rgba(255, 255, 255, .55);   /* 6.24:1，过 AA；仍是"提示"而不是"正文" */
  font-size: 11px;
  letter-spacing: .08em;
}
/* 顺带核对过、**不需要动**的：.lb-meta(.62)=6.42:1、.lb-hint(.62)=7.79:1、
   .lb-title=16.19:1。深色主题整体健康，不要为了"统一"去改已达标的色阶。 */


/* ==========================================================================
   §1.4 · 前置条件：把硬编码的 7 个时长换成 var() —— 不改这段，§0 全是装饰
   --------------------------------------------------------------------------
   【为什么是 §1 而不是 §0】它不是"新增命名"，是"把已有的静态值接上变量"，
           属于让 token 生效的必要动作。放 §0 会让人误以为只是加变量。
   【解决】style.css:1159-1168 的 `opacity .85s / translate .95s / scale .95s /
           rotate .95s / filter .8s`（5 个不同时长、5 个不同曲线都写成同一条 var(--ease)），
           以及 1188-1193 `[data-stagger] > *` 的 `.8s / .9s + 85ms×i`。
   【风险】**低**，但动的是全站每一处滚动进场。改完请把首屏→影集墙整段滚一遍。
   【验证】影集墙、四季卡、区块标题三处的进出时长应分别落在 240 / 900 / 900ms
           （用 DevTools → Animations 面板看，或在 ui-kit.html ⑨ 点「重播」读实测）。
   ★ 不改动的东西：`[data-stagger]` 的 85ms 错峰**保留原值**（那是首屏文字 5 段的节奏，
     与列表项无关）；`[data-reveal]` 的 `--d` 偏移也保留。
   ========================================================================== */

/* ── 1.4.1 基规则：5 个属性全部走同一个 --t-reveal ───────────────────────── */
html.motion-ready [data-reveal] {
  opacity: 0;
  transition:
    opacity var(--t-reveal) var(--ease),
    translate var(--t-reveal) var(--ease),
    scale var(--t-reveal) var(--ease),
    rotate var(--t-reveal) var(--ease),
    filter var(--t-reveal) var(--ease);
  transition-delay: calc(var(--d, 0ms) + var(--i, 0) * 55ms);
}

/* ── 1.4.2 列表项例外：影集墙的图不许用 900ms ───────────────────────────────
   ★★ 三条会"静默失效"的实现约束，写错不报错、只是看起来没变：

   ① 只能用 `transition-duration` 单属性。
      写成 `transition: opacity var(--t-base) var(--ease)` 这种简写，会把上面基规则里
      5 个属性的 timing function 一起重置成浏览器默认的 `ease` —— 等于把 --ease 绕过去了。
   ② 必须排在 1.4.1 **之后**。
      两者特异性同为 (0,2,1)，靠源码顺序取胜。写反了不报错，240ms 就是不生效。
      ← 这一类 bug 最难查，所以这里不按"字母序"整理，按"生效顺序"排。
   ③ 不要顺手把 [data-stagger] 的 55ms 也改掉（首屏文字段落不是列表项）。

   为什么列表项要拆出来：一张 9:16 在 1440 视口里高 565px，以 1000px/s 滚过去，
   从进视口到出视口只有约 0.95s。用 900ms 进场 = 照片全程半透明划过去，
   而且和"图片没加载完"长得一模一样，用户分不清是慢还是坏。 */
html.motion-ready [data-reveal="zoom"] {
  transition-duration: var(--t-base);            /* 240ms，覆盖上面 5 个属性 */
  /* 错峰 40ms。不需要再套 min(--i, --act-cap)：app.js:609 的 wallCard 已经把
     `--i` 钉在 `(i || 0) % 8`，最大 7 → 280ms，合计 520ms < 屏内停留 950ms。
     ★ 如果哪天有人把 `% 8` 去掉（让 --i 一路涨到 17），这里要改成
       calc(var(--i, 0) * var(--lr-step)) 外面套 min(..., var(--act-cap))，
       否则第 18 张会等到 920ms。 */
  transition-delay: calc(var(--d, 0ms) + var(--i, 0) * var(--lr-step));
}
/* 落地态（.is-in）不需要重写：基规则 [data-reveal].is-in 已经负责
   opacity/translate/scale/rotate/filter —— 少一处可漂移的副本。 */

/* ── 1.4.3 机位页的照片网格：也是"卡片网格"，同样是列表项 ─────────────────────
   ★ 这一条**不在 team-lead 给的清单里**（他给的是 [data-reveal="zoom"]），
     但 `place.js:289` 的模板用的是 `data-reveal="rise"` 而不是 zoom
     —— 只改 zoom 会漏掉整个机位页网格（4 列 × N 行，同样是列表项）。
     不想要就删掉这一条，视觉上退回 900ms，其它不受影响。
   ★ 注意选择器必须带 html.motion-ready 前缀（(0,3,1)），否则特异性低于基规则，
     写了也不生效 —— 同 1.4.2 的第 ② 条。 */
html.motion-ready .pg-item[data-reveal="rise"] {
  transition-duration: var(--t-base);
  transition-delay: calc(var(--i, 0) * var(--lr-step));
}


/* ==========================================================================
   §2 · 六个核心组件的覆盖样式（同名覆盖，不改 HTML）
   --------------------------------------------------------------------------
   【解决】phase1 §3.3：按钮 / chip / 卡片 / 表单 / 空态五套写法各不相同，
           同一屏里三个同类元素的圆角、时长、位移都不一样。
   【风险】低～中。按钮统一到 42px 会让动作行整体紧约 6px（见 ①）：
           这是"统一高度"的必然结果，不是 bug。
   【验证】§2 每个组件后面都写了对应的截图。
   ========================================================================== */

/* --------------------------------------------------------------------------
   ① 次按钮 —— 统一高度，去掉位移（位移是卡片的事）
   【对应】style.css:227-244
   【改了什么】hover 时不再 translateY(-1px)：按钮位移和卡片位移混用，
               会让人分不清"这个按下去是换页还是弹层"。
   【风险】低。★ 实测现值高度 47.8px（padding 11+11 + 14px×1.7 行高 + 2 边框），
           统一到 42px 会矮 5.8px。**不要为了"零跳动"改回 47.8** —— 那等于没有统一。
   【验证】11-404.jpg（页面上唯一的按钮）：改后按钮矮 6px、圆角不变、hover 时不再上浮。
   -------------------------------------------------------------------------- */
.btn {
  display: inline-flex; align-items: center; gap: var(--sp-2);
  height: 42px; padding: 0 var(--sp-5);
  border-radius: var(--radius-pill);
  border: 1px solid var(--line-2);
  background: transparent;
  color: var(--text);
  font-size: 15px;
  letter-spacing: .04em;
  transition: color var(--t-fast) var(--ease),
              border-color var(--t-fast) var(--ease),
              background var(--t-fast) var(--ease);
}
.btn:hover { color: var(--accent); border-color: var(--accent); background: var(--accent-soft); }
.btn.small { height: 32px; padding: 0 var(--sp-4); font-size: 12.5px; }
.btn.ghost { border-color: var(--line); }
.btn:disabled {
  color: var(--muted); border-color: var(--line); background: transparent;
  opacity: .5; cursor: not-allowed;
}

/* --------------------------------------------------------------------------
   ② 主按钮 —— filter: brightness() 换成显式色值
   【对应】style.css:240-241
   【改了什么】hover 的变亮从 `filter: brightness(1.08)` 换成写死的 #e3b273。
               filter 会给元素新建一个合成层（每次 hover 都要重新栅格化），
               而且叠在金色上会偏色。
   【风险】低。金底深字实测 8.42:1。
   【验证】DevTools → Rendering → Paint flashing 打开，hover 主按钮：
           改前整块闪（新建合成层），改后不闪（只重绘颜色）。
   -------------------------------------------------------------------------- */
.btn.primary {
  background: var(--accent);
  border-color: var(--accent);
  color: #14100a;
  letter-spacing: .02em;
  transition: background var(--t-fast) var(--ease),
              border-color var(--t-fast) var(--ease),
              box-shadow var(--t-fast) var(--ease);
}
.btn.primary:hover {
  filter: none;                    /* ← 关键：把旧规则里的 brightness 显式关掉 */
  background: #e3b273;
  border-color: #e3b273;
  color: #14100a;
  box-shadow: var(--shadow-2);
}
.btn.primary:active {              /* 80ms 只给按下：这是"手指已经落下"的确认 */
  background: #c8924a; border-color: #c8924a; box-shadow: none;
  transition-duration: var(--t-micro);
}
.btn.primary:disabled {
  background: var(--elev-2); border-color: var(--line); color: var(--muted);
  box-shadow: none; opacity: 1; cursor: not-allowed;
}

/* --------------------------------------------------------------------------
   ③ chip 筛选 —— 从"实心胶囊"改成"描边胶囊"
   【对应】style.css:362-380 → 02-map.jpg（三排胶囊彼此没有主次）
   【改了什么】background 从 var(--elev) 变透明。地图区现在有两排实心胶囊 +
              一条实心月份条，全部同色同形；透明之后「季节/标签=一级（描边）、
              月份=二级（实心条）」才立得住。
   【风险】低（颜色）＋ **中**（高度 38px→30px：筛选行会整体变紧，地图容器跟着上移约 8px）。
           若不想让地图位置变，只留颜色相关的三条，删掉 height / font-size。
   【验证】02-map.jpg：三排控件应能一眼分出层级；地图顶边比改前高约 8px。
   -------------------------------------------------------------------------- */
.chip {
  display: inline-flex; align-items: center; gap: var(--sp-1);
  height: 30px; padding: 0 var(--sp-3);
  border-radius: var(--radius-pill);
  border: 1px solid var(--line);
  background: transparent;
  color: var(--text-2);
  font-size: 12.5px;
  cursor: pointer;
  transition: color var(--t-fast) var(--ease),
              border-color var(--t-fast) var(--ease),
              background var(--t-fast) var(--ease);
}
.chip:hover {
  color: var(--text); border-color: var(--line-2); background: var(--elev);
  transform: none;                   /* 旧规则里的 translateY(-1px) 去掉：位移只给卡片 */
}
.chip.active { color: var(--accent); border-color: var(--accent); background: var(--accent-soft); }
.chip:disabled, .chip[aria-disabled="true"] { color: var(--muted); opacity: .42; cursor: not-allowed; }
.chip i { width: 6px; height: 6px; border-radius: 50%; display: inline-block; }
.chip .n { font-size: 11px; color: var(--muted); font-variant-numeric: tabular-nums; }

/* --------------------------------------------------------------------------
   ④ 内容卡片 —— 路线卡 / 影集项 / 留言 / 时间轴行共用一套（只覆盖，不加类）
   【对应】style.css:431-443(.route-card) / 771-783(.wall-item) / 850-854(.gb-item)
           / 703-711(.tl-item)
   【改了什么】四种写法（圆角 10 / 10 / 16 / 10，悬停 -3 / -3 / 0 / +2px，
              时长 .28 / .4 / .5 / .25s）收敛成：圆角 12、悬停 -2px、时长 240ms。
              位移只留 --lift 一档，且只有「点进去会换页/弹层」的卡才有。
   【风险】低。视觉差异集中在"悬停上浮从 3px 变 2px"和"时长从 400ms 变 240ms"。
   【验证】05-wall.jpg：hover 任意一张照片，上浮幅度略小、更快落定；
           快速划过一排时不再有"追着跑"的迟滞感。
   -------------------------------------------------------------------------- */
.route-card,
.wall-item,
.gb-item,
.tl-item {
  border-radius: var(--radius-sm);
  transition: border-color var(--t-base) var(--ease),
              box-shadow var(--t-base) var(--ease),
              transform var(--t-base) var(--ease);
}
/* 位移只给**点进去会换页/弹层**的卡：路线卡（<button>）与影集项（<a>）。
   .gb-item 是留言卡、.tl-item 是时间轴行，都不是"能点开"的入口，
   给它们上浮会让人误以为可以点 —— 这一条是 phase1 §3.1 原则 4 的具体落地。 */
.route-card:hover,
.wall-item:hover {
  border-color: var(--line-2);
  box-shadow: var(--shadow-2);
  transform: translateY(var(--lift));
}
/* 图内缩放慢一档（420ms）：产生"容器先到、图片跟随"的层次。
   这是全站唯一值得用不同时长的地方，别把这条也统一掉。 */
.wall-item img { transition: transform var(--t-slow) var(--ease); }
.wall-item:hover img { transform: scale(1.03); }
/* 路线卡自己也要知道"我现在是选中状态"（现在只有地图知道，卡片没有任何痕迹） */
.route-card.active { border-color: var(--accent); background: var(--accent-soft); }
/* 时间轴行保持它原来的横移，只统一时长与颜色 */
.tl-item:hover { background: var(--elev); border-color: var(--line); }

/* ④-b（滚动进场：列表项不许用 900ms）已上移到 §1.4 —— 那里才是"让 token 生效"
   的位置，而且必须排在基规则之后（见 §1.4.2 第 ② 条）。这里不再重复声明，
   否则用 `transition:` 简写会静默把 --ease 重置成默认 ease。 */

/* --------------------------------------------------------------------------
   ⑤ 表单控件 —— 补 hover / focus / placeholder / error 四态
   【对应】style.css:759-768(.select) / 831-836(.input) / 839-842(.emoji)
   【改了什么】① 圆角 10 → 8（--radius-xs，控件是"小件"不是"容器"）
              ② 补 hover 描边（现在只有 :hover 在 .select 上有，.input 没有）
              ③ placeholder 显式声明颜色（浏览器默认 #757575 在深底上只有 3.9:1）
              ④ 补 focus 的 3px 柔光环 + 描边转金（沿用它原来的路子，别改成 outline）
   【风险】低。高度 11+11 → 8+8 + min-height 38px：实测现值约 47px，会矮约 9px
           （表单整体上移一点）。若不想要这个变化，删掉 min-height 与 padding 两行。
   【验证】07-guestbook.jpg：输入框边框在纸白主题下应能看见（§4 之后）；
           点进输入框，描边转金 + 3px 柔光环；占位文字比现在更淡但可读。
   -------------------------------------------------------------------------- */
.input,
.select,
.textarea {
  min-height: var(--field-min);
  padding: var(--sp-2) var(--sp-3);
  border: 1px solid var(--line);
  border-radius: var(--radius-xs);
  background: var(--bg-2);
  color: var(--text);
  transition: border-color var(--t-fast) var(--ease),
              box-shadow var(--t-fast) var(--ease);
}
.input:hover, .select:hover, .textarea:hover { border-color: var(--line-2); }
.input:focus, .select:focus, .textarea:focus {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-soft);
  outline: none;
}
.input::placeholder, .textarea::placeholder { color: var(--muted); opacity: 1; }
/* ★ 多行框必须单独把最小高度还回去：上面那条把 min-height 统一成 --field-min(38px)，
   而 style.css:837 给 .textarea 的是 88px。不还回去，留言框会从 88 掉到 38 —— 只能写一行。
   （phase1/phase2 给的片段里都没有这一条，实测才发现。） */
.textarea { min-height: 88px; }
/* 错误态：由 JS 在 .input 的父级写 data-err="1"（当前无节点，属 §5 预留） */
[data-err="1"] .input,
[data-err="1"] .textarea {
  border-color: #e0714c;
  box-shadow: 0 0 0 3px rgba(224, 113, 76, .16);
}
/* 触屏（粗指针）下把控件撑到 44px：手指落点误差 ±20px，38px 的框会点空 */
@media (pointer: coarse) {
  :root { --field-min: var(--row-min); }
}

/* --------------------------------------------------------------------------
   ⑥ 空状态 —— 空态不该比内容更亮
   【对应】style.css:648-654(.panel-empty/.empty-art/.empty-tips) / 794(.wall-empty)
   【改了什么】.empty-art 现在是 color: var(--accent) 的 96px 靶心，
              比右侧任何真实内容都亮 —— 空态抢走了内容的注意力。
              改成 --line-2 描边（灰），并把文案结构固定成
              「结论一行 + 下一步一行」。
   【风险】低。只改颜色与间距，DOM 不动（原有的 ul.empty-tips 保留）。
   【验证】02-map.jpg 右侧面板：靶心从金色变成灰色细线，标题读起来是主体。
   -------------------------------------------------------------------------- */
.panel-empty {
  display: grid; justify-items: center; gap: var(--sp-2);
  padding: var(--sp-6) var(--sp-5); text-align: center;
}
.empty-art {
  color: var(--line-2);      /* 金色 → 灰描边 */
  margin-bottom: var(--sp-1);
}
.panel-empty h2, .panel-empty h3 { font-size: 17px; color: var(--text); }
.panel-empty p {
  color: var(--muted); font-size: 12.5px; line-height: 1.65;
  max-width: var(--inline-max);
}
.empty-tips { margin-top: var(--sp-4); gap: var(--sp-2); }
.empty-tips li { font-size: 12.5px; color: var(--muted); }
.empty-tips li::before { background: var(--line-2); opacity: 1; }   /* 金点也收掉，空态不用强调色 */
.wall-empty {
  padding: var(--sp-7) var(--sp-5);
  font-size: 12.5px; color: var(--muted);
  max-width: var(--inline-max); margin: 0 auto; line-height: 1.65;
}


/* ==========================================================================
   §3 · 交互与可达性
   --------------------------------------------------------------------------
   【解决】phase2 P1-7（全站只有 1 条 :focus-visible）、P1-8（触控命中区 <44px）、
           3.3 改法 A（toast 吃掉点击）、P1-9 v2（触屏没有 hover ⇒ 影集墙整墙无地点）、
           3.3 改法 B（播放器 musicNudge 无限重绘，改成有限 6 次）、
           §8 ②（遮罩开合时整页横跳 15px）。
   【风险】低。全部是"加"而不是"改"。
   ★ 本文件**不含**「灯箱打开时把 .music 冻住」那条规则（不写 paused）—— 那条已被否决：
     被明确否决（播放器 z 3500 高于灯箱 3000，灯箱态可见可点、音频在放）。见 3.4。
   ========================================================================== */

/* --------------------------------------------------------------------------
   3.1 统一焦点环：2px 实线 + 2px 偏移
   【为什么 outline-offset 是 2px 而不是 0】.btn / .chip 都是 999px 胶囊，
        offset: 0 时焦点环贴着描边，与 hover 态撞色（金色描边 + 金色环 = 看不出边界）。
   【为什么用 :where()】特异性归零，任何组件自己的 outline 声明都能压过它 ——
        这条是兜底，不是霸王条款。
   【风险】低。★ 一个已知取舍：原 .input 有 `outline: none`（style.css:834），
        本文件后置，所以键盘聚焦时输入框会同时出现「金描边 + 柔光环 + 2px 焦点环」。
        看起来略重，但 Windows 高对比度模式下 box-shadow 会被系统丢弃，
        只有 outline 留得下 —— 这一层不能省。
   【验证】Tab 走一遍首屏：导航 5 项 / 主题钮 / 菜单钮 / 2 个主按钮 / 5 个季节 chip /
        12 个标签 chip / 12 个月份格 / 3 个地图工具 / 18 张照片，**任意一帧**
        都能看到那枚 2px 金环。灯箱内是白环 + 金色外晕（金环在照片上会被吃掉）。
   -------------------------------------------------------------------------- */
:where(a, button, [tabindex], select, textarea, input):focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: 2px;
}
/* ★ 不要在这里写 border-radius: inherit（phase2 的片段里有这一条，实测有害）：
   它会把**元素自己**的圆角改成父级的圆角 —— 聚焦一枚 999px 的胶囊按钮时，
   按钮的圆角会当场消失、变成方块。而现代浏览器的 outline 本来就跟着元素自己的
   border-radius 走，不需要这一句。 */
/* 照片/玻璃底上的元素：金环被照片吃掉，换白环 + 金色外晕 */
.lightbox :focus-visible,
.cf-plate :focus-visible {
  outline-color: #fff;
  outline-offset: 3px;
  box-shadow: 0 0 0 5px rgba(217, 163, 90, .55);
}

/* --------------------------------------------------------------------------
   3.2 toast 不再吃掉下面的点击
   【对应】style.css:1089-1097 → phase2 3.3 改法 A
   【原因】一条 40px 高、居中的金色胶囊会在底部压 2.6 秒，这 2.6 秒里它
           下方的所有点击都被吞掉（移动端正好落在播放器/底部内容上）。
   【风险】低。1 行，无布局影响。
   【验证】提交留言后 toast 出现时，立刻点它正下方的内容 —— 改前没反应，改后有反应。
   -------------------------------------------------------------------------- */
.toast {
  pointer-events: none;
  background: var(--accent);
  color: #14100a;
  border-radius: var(--radius-pill);
  padding: var(--sp-3) var(--sp-5);
  font-size: 12.5px;
  z-index: var(--z-toast);
  box-shadow: var(--shadow-3);
  /* 进 200ms（pop，允许过冲）/ 出 160ms（ease-in，让路要快） */
  transition: opacity var(--t-fast) var(--ease-in), transform var(--t-fast) var(--ease-in);
}
.toast.show {
  opacity: 1;
  transform: translateX(-50%);
  transition: opacity var(--t-base) var(--ease-pop), transform var(--t-base) var(--ease-pop);
}

/* --------------------------------------------------------------------------
   3.3 触屏没有 hover：影集墙的地点名给一个左下角实心小标签
   【对应】style.css:784-791（.wall-cap 只在 :hover 出来）
   【为什么】手机上没有 hover，轻点直接进照片页/灯箱 ⇒ **整墙照片没有任何
           "这是哪"的信息**；而机位页每张图下面的日期是常驻的 —— 同一件事全站两套规则。
   【为什么不用"全宽渐变常显"】.wall-cap 在 style.css:785-786 是
           `inset:auto 0 0 0 + padding 22px 12px 10px + 全宽黑渐变`。一旦常显，
           等于每张照片下半 42% 永久压一层黑，18 张全如此 —— 而这一页的主体就是照片。
   【做法】只盖住照片左下角一小块实心标签，宽度自适应；日期不进标签（留给桌面 hover）。
   【尺寸/对比度】高度为零（absolute，不影响瀑布流，也不影响 §5.1 骨架的高度公式）；
           标签底 rgba(10,11,13,.62) 叠在**纯白照片**上最坏是 #676767，
           白字 5.65:1（过 AA）；纯黑照片上 17:1。只有一块实色、**没有 backdrop-filter**
           ——18 张各挂一层 blur 正是站点那条事故的形状。
   【为什么媒体查询写两个条件】`(hover: none), (pointer: coarse)` 不是冗余：
           触屏笔记本（Surface / 翻转本）接鼠标时 `hover: hover` 成立，
           只写 `hover: none` 会让这类设备**既没有 hover 效果、又不显示标签**，两边落空。
           站内 style.css:1449 已有 `@media (hover: none)` 的先例（隐藏音量条），这里沿用同一技巧。
   【风险】低。纯 CSS，**只影响无 hover 设备**；桌面（hover: hover）完全不变。
   【验证】DevTools 设备模拟（390×844）+ 手指光标：每张照片左下角有一枚
           「青山公园」这样的小标签，长按/轻点仍能进照片页；桌面鼠标悬停的观感与改前一致。
   -------------------------------------------------------------------------- */
@media (hover: none), (pointer: coarse) {
  .wall-cap {
    inset: auto auto 8px 8px;        /* 左下角、宽度自适应，不再是全宽条 */
    padding: 0;
    background: none;                /* 不要全宽黑渐变 */
    opacity: 1;
    transform: none;
    transition: none;                /* 常驻的东西不需要过渡 */
  }
  .wall-cap b {
    display: inline-block;
    padding: 3px 8px;
    border-radius: var(--radius-xs);
    background: rgba(10, 11, 13, .62);
    color: #fff;
    font-size: 12px;
    line-height: 1.6;
    letter-spacing: .02em;
  }
  .wall-cap span { display: none; }   /* 日期不在触屏上占位 */
}
/* 键盘用户等价于"悬停"：聚焦到这张照片时也把说明浮出来。
   不加半透明白描边 —— 会跟上面的实心底叠成双线。 */
.wall-item:focus-visible .wall-cap,
.wall-item:focus-within .wall-cap { opacity: 1; transform: none; }

/* 地图 pin 的名称：hover / active / 键盘聚焦时都显示（原来只有前两种） */
.pin:focus-within .pin-name { opacity: 1; transform: translateX(-50%) translateY(0); }

/* --------------------------------------------------------------------------
   3.4 播放器的呼吸提示：把无限循环改成有限次（治"无限重绘"这个根）
   【对应】style.css:1421（.music.is-nudge .music-toggle）与 :1425（.music-mini）
   【原因】musicNudge 动的是 **box-shadow（重绘属性）**，1.7s **infinite**，
           而这个按钮就叠在 backdrop-filter 上 —— "永远每帧重绘"是问题本身。
           改成 6 次（≈10 秒）后它自己安静：普通页与灯箱态同时解决，
           不需要为任何状态单开规则。
   【为什么**不**去冻住它】播放器 z-index 是 3500，高于灯箱 3000
           （style.css:1272 注释原文「高于看图遮罩 .lightbox(3000) —— 看照片时还能切歌」）。
           灯箱打开时它是**可见、可点、音频在放**的图层，冻住它会造出
           "音乐在响、盘片卡住"的可见 bug；`*` 通配还会把播放态的旋转一起停掉。
           这一条已被明确否决，不要加回来。
   【风险】低。
   【验证】1) 首屏等你点播放时，左下角呼吸约 10 秒后自己停（改前一直抖到永远）；
           2) 打开灯箱：播放器仍在、仍可点、点一下能切歌；
           3) Performance 录 15 秒：前 10 秒有 paint，之后无持续 paint。
   ★ 必须用长写的「迭代次数」属性覆盖，不要用 `animation` 简写：
     简写会把 style.css 里的 duration / easing 一起重置成本行写的字面值。
   -------------------------------------------------------------------------- */
.music.is-nudge .music-toggle,
.music.is-nudge .music-mini { animation-iteration-count: 6; }
/* 反过来核对：.music[data-state="playing"] 的 musicSpin（盘片 / 折叠态小圆钮）
   必须继续转 —— 它是"正在播放"唯一的非文字指示器。本补丁不碰它。 */

/* --------------------------------------------------------------------------
   3.5 遮罩开合时整页横跳 15px
   【对应】phase2 §8 ②；触发点 coverflow.js:689、place.js:59、任何 body.overflow:hidden
   【原因】滚动条消失 → 内容区变宽 15px → 整页左移。点开/关闭每一张照片都抖一次。
   【风险】**中低**：会在本来就该有滚动条的页面上常驻一条 15px 的留白槽
           （视觉上是右侧多一点空白，不影响布局与点击）。这是修它的唯一纯 CSS 办法。
   【验证】在影集墙点开一张照片再关掉：改前整页左右抖一次，改后纹丝不动。
   -------------------------------------------------------------------------- */
html { scrollbar-gutter: stable; }

/* --------------------------------------------------------------------------
   3.6 触控命中区：视觉尺寸不变，只把热区撑到 44px
   【对应】phase2 P1-8：.ms-cell 24px、.gb-like 19px、.chip 33px、.emoji 32px、
           .icon-btn 38px、.lb-close 38px
   【风险】低。只在 ≤900px 生效，桌面（hover 指针）完全不受影响。
   【为什么用伪元素而不是 padding】月份格的高度被 `line-height: 1` 钉在 24px
       （padding 5+5 + 12 + 边框 2），加 padding 会把它撑到 38–44px 并把地图往下推。
       ::before 不参与布局，只扩热区，所以**这里只能改 ::before，不能加 padding**。
       （phase2 的片段里同时写了 padding 与 ::before，两者互相抵消、还多推了 14px ——
        实测后只保留 ::before。）
   【已知取舍】12 个相邻格子的热区会互相重叠 2px（inset: -10px -1px），
       重叠区判定给后一个 —— 与"点偏了就选到隔壁"相比，这是更好的失败方向。
   【验证】DevTools 设备模拟（390×844）+ 手指光标，逐个数：月份格 / chip / 点赞 /
       emoji / 主题钮 / 灯箱关闭键 的**可点高度**都 ≥44px，
       而 .month-slice 的总高变化 ≤1px。
   -------------------------------------------------------------------------- */
@media (max-width: 900px) {
  .ms-cell::before { content: ""; position: absolute; inset: -10px -1px; }
  .chip { height: var(--row-min); padding: 0 var(--sp-3); }
  .emoji { width: 44px; height: 44px; }
  .icon-btn { width: 44px; height: 44px; }
  .gb-like { padding: 12px 0; margin: -12px 0; }
  .lb-close { width: 44px; height: 44px; }
  .lb-nav { width: 48px; height: 48px; }
}

/* --------------------------------------------------------------------------
   3.7 prefers-reduced-motion：把"滚动进场"真正关掉
   【对应】style.css:1264 的 `* { animation-duration: .01ms !important }`
   【原因】那条对 infinite 动画等于"用最快速度无限重复"；对滚动进场它会变成 0.01ms，
           但仍要走一遍过渡管线。
   ★ 注意：在 §1.4 落地**之前**，这条对滚动进场是无效的 —— 因为 style.css:1159 那段
     是写死的 .85/.95/.8s，变量根本管不到它。§1.4 把时长接到 var(--t-reveal) 之后，
     这条才开始真正生效。两处必须一起上，否则你会误以为"归零了但没变化"。
   【风险】低。
   【验证】系统开启"减少动态效果"后刷新：影集墙的图应**立刻**全部可见（无显影过程）；
           播放器的呼吸停止（不是加速）。
   ★ 不在这里停 musicSpin（盘片 / 折叠态小圆钮的旋转）：它是"正在播放"唯一的非文字
     指示器，一旦停掉，减少动效的用户就完全看不出音乐是不是在放。它转得很慢、面积很小。
   -------------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
  :root { --t-reveal: 0ms; --lr-step: 0ms; --t-slow: 0ms; }
  .music.is-nudge .music-toggle, .music.is-nudge .music-mini,
  .hero-bg, .scroll-hint span { animation: none !important; }
}

/* --------------------------------------------------------------------------
   3.8 .sr-only（读屏专用文本容器）
   【用途】phase2 §6 里那几条"给图标/图表补文本替代"都要用它。
   -------------------------------------------------------------------------- */
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}


/* ==========================================================================
   §4 · 浅色主题对比度（4 个值，不动结构、不动深色）
   --------------------------------------------------------------------------
   【解决】phase1 P1-5：纸白底上 --muted 3.58:1、--accent 3.30:1、
           选中 chip 上的 --accent 2.91:1、输入框描边 1.22:1 —— 全线低于 4.5:1。
   【风险】低。只覆盖 4 个 token，且全部落在 html[data-theme="light"] 里，
           深色主题一个像素都不动（深色侧 --muted 6.65:1、--accent 8.75:1 本来就达标）。
   【数值依据】WCAG 2.1 + alpha 合成，见 phase1 附表 A。
   【验证】切到浅色主题，重截 07-guestbook.jpg 与 02-map.jpg：
           · 11px 的 eyebrow / 卡片计数 / 日期行从"几乎消失"变成可读；
           · 金色小标签从"污渍"变回"标签"；
           · 输入框能看出边界（1.22 → 2.60:1），并且填充改成纯白拉开与卡片的差。
   -------------------------------------------------------------------------- */
html[data-theme="light"] {
  --accent: #9a611f;                  /* 3.30 → 4.62:1（白底 5.12:1） */
  --muted:  #63686f;                  /* 3.58 → 5.07:1（白底 5.61:1） */
  --line:   rgba(30, 26, 20, .16);
  --line-2: rgba(30, 26, 20, .42);    /* 1.51 → 2.60:1，输入框/卡片边界才立得住 */
}
/* 浅色下靠"填充差"而不是"边框差"来标识输入区 */
html[data-theme="light"] .input,
html[data-theme="light"] .textarea,
html[data-theme="light"] .select {
  background: #fff;
  border-color: var(--line-2);
}
/* ::selection 前景色：原来是 #fff on --accent = 2.25:1（选中文字变糊） */
::selection { background: var(--accent); color: #14100a; }   /* 8.42:1 */


/* ==========================================================================
   §5 · 预留样式（当前无对应节点，**不会生效**；等 HTML/JS 补丁落地即刻可用）
   --------------------------------------------------------------------------
   放在这里是为了"一次交付、分两次上线"：JS 补丁（见 ui-upgrade-map.md 的
   「JS 补丁」一节）先合，样式不用再改一遍。
   全部是新增类名，不会碰到任何现有元素 —— 风险：低。
   ========================================================================== */

/* 5.1 骨架屏：影集墙 reset 态的等高占位（修 phase2 P0-2 的 2600px 高度塌陷）
       高度与真实条目逐像素一致（同一个 aspect-ratio + 同一个 margin-bottom） */
.wall-skel {
  display: block; width: 100%; aspect-ratio: 9 / 16;
  margin-bottom: var(--sp-4); break-inside: avoid;
  border-radius: var(--radius-sm);
  border: 1px solid var(--line);
  background: var(--elev-2);
  animation: skBreath 1.6s var(--ease-out) infinite;
}
/* 通用骨架块 / 单行扫光（计数器、统计条用扫光，不要骨架） */
.sk {
  display: block; background: var(--elev-2); border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  animation: skBreath 1.6s var(--ease-out) infinite;
}
.sk-line {
  height: 12px; border-radius: var(--radius-pill);
  background: linear-gradient(90deg, var(--elev-2) 0%, var(--elev) 40%, var(--elev-2) 80%);
  background-size: 200% 100%;
  animation: skSweep 1.2s linear infinite;
}
@keyframes skBreath { 0%, 100% { opacity: .55 } 50% { opacity: .85 } }
@keyframes skSweep { to { background-position: -200% 0 } }
@media (prefers-reduced-motion: reduce) {
  .wall-skel, .sk, .sk-line { animation: none; opacity: .7; }
}

/* 5.2 地图筛选空态浮层（修 phase2 P1-4：筛到空集时地图一片空白且永不解释）
       pointer-events: none 是关键 —— 空态只是"说明"，不能变成盖住地图的膜，
       否则用户连"拖一下试试"都做不到。只有按钮吃点击。 */
.map-empty {
  position: absolute; inset: 0; z-index: 620;
  display: grid; place-content: center; justify-items: center; gap: var(--sp-2);
  text-align: center; padding: var(--sp-5);
  pointer-events: none;
  background: color-mix(in srgb, var(--bg) 72%, transparent);
  opacity: 0; transition: opacity var(--t-base) var(--ease-out);
}
.map-empty[hidden] { display: none; }
.map-empty.on { opacity: 1; }
.map-empty p { font-size: 12.5px; color: var(--muted); max-width: var(--inline-max); }
.map-empty b { font-size: 15px; color: var(--text); }
.map-empty .btn { pointer-events: auto; }

/* 5.3 错误态：虚线框 + 一句人话 + 一个重试动作（不要把 err.message 当主文案） */
.err-state {
  display: grid; justify-items: center; gap: var(--sp-2);
  padding: var(--sp-6) var(--sp-5); text-align: center;
  border: 1px dashed rgba(224, 113, 76, .45);
  border-radius: var(--radius-sm);
  background: rgba(224, 113, 76, .06);
}
.err-state .msg { font-size: 12.5px; color: #e0714c; }
.err-state .why { font-size: 11px; color: var(--muted); }

/* 5.4 权限 / 待审态 */
.gate {
  display: grid; gap: var(--sp-2); padding: var(--sp-5);
  border: 1px solid var(--line); border-radius: var(--radius-sm);
  background: var(--elev-2);
}
.gate .lock { font-size: 11px; color: var(--muted); letter-spacing: .08em; }
.gate.is-warn { border-color: rgba(224, 160, 76, .4); background: rgba(224, 160, 76, .06); }
/* 留言进审核：虚线 + 0.72 透明度 = "这是你的，但还没上线"，一眼可辨且不用新颜色 */
.gb-item.is-pending { border-style: dashed; opacity: .72; }
.gb-hint-line { margin: var(--sp-2) 0 0; font-size: 12px; color: var(--accent); }

/* 5.5 灯箱出场（修 phase2 P1-6：入场 560ms、出场 0ms 硬切）
       160ms 而不是 320ms：出场必须比入场快 —— 入场是"我给你看东西"，
       出场是"我要把路让开"。用 --ease-in（加速离开），与入场对称。 */
.lightbox.is-closing {
  animation: lbOut var(--t-fast) var(--ease-in) forwards;
  pointer-events: none;
}
@keyframes lbOut { to { opacity: 0 } }
@media (prefers-reduced-motion: reduce) {
  .lightbox.is-closing { animation: none; }
}
/* ★ 注意：is-closing 期间 pointer-events: none 是必须的，
   防止 160ms 内二次点击穿透到灯箱内部按钮。JS 侧见 map.md 的 JS 补丁 c。 */

/* ==========================================================================
   §6 · 明确"不要写进来的东西"（写给下一个改这里的人）
   --------------------------------------------------------------------------
   ❌ 不要给任何文字加 font-weight。除 400 外的值一律触发合成加粗，得意黑会糊。
   ❌ 不要用 filter: brightness() 做 hover 变亮（新建合成层 + 偏色）——
      要变亮就换一个色值，像 §2 ② 那样。
   ❌ 不要新增 backdrop-filter。同屏最多 2 层玻璃、单层 blur ≤14px。
   ❌ 不要新增第 5 种圆角 / 第 9 个字号 / 第 4 级投影 / 第 6 档时长。
      先问"能不能用现有档"，不能再说。
   ❌ 列表项 / 卡片网格不要用 --t-reveal（900ms）。用 --t-base + --lr-step。
   ❌ 任何 infinite / rAF 动效都必须能回答"什么时候停"，且停的条件挂在状态上
      （lb-open / document.hidden / 出视口），不能挂在"用户应该会滚开"上。
   ========================================================================== */
